mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
ability to symlink a vault
This commit is contained in:
parent
b238089e2d
commit
63cbe83e3e
11 changed files with 518 additions and 19 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { ipcMain, BrowserWindow, shell } from 'electron';
|
import { ipcMain, BrowserWindow, shell, dialog } from 'electron';
|
||||||
import { ipc } from '@x/shared';
|
import { ipc } from '@x/shared';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
|
@ -26,6 +26,11 @@ import type { IOAuthRepo } from '@x/core/dist/auth/repo.js';
|
||||||
import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
|
import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
|
||||||
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
|
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
|
||||||
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
||||||
|
import {
|
||||||
|
addKnowledgeVault,
|
||||||
|
listKnowledgeVaults,
|
||||||
|
removeKnowledgeVault,
|
||||||
|
} from '@x/core/dist/config/knowledge_vaults.js';
|
||||||
import * as composioHandler from './composio-handler.js';
|
import * as composioHandler from './composio-handler.js';
|
||||||
import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js';
|
import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js';
|
||||||
import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js';
|
import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js';
|
||||||
|
|
@ -318,6 +323,30 @@ export function setupIpcHandlers() {
|
||||||
'workspace:remove': async (_event, args) => {
|
'workspace:remove': async (_event, args) => {
|
||||||
return workspace.remove(args.path, args.opts);
|
return workspace.remove(args.path, args.opts);
|
||||||
},
|
},
|
||||||
|
'knowledge:listVaults': async () => {
|
||||||
|
return { vaults: listKnowledgeVaults() };
|
||||||
|
},
|
||||||
|
'knowledge:pickVaultDirectory': async () => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
});
|
||||||
|
if (result.canceled || result.filePaths.length === 0) {
|
||||||
|
return { path: null };
|
||||||
|
}
|
||||||
|
return { path: result.filePaths[0] };
|
||||||
|
},
|
||||||
|
'knowledge:addVault': async (_event, args) => {
|
||||||
|
const vault = addKnowledgeVault({
|
||||||
|
name: args.name,
|
||||||
|
path: args.path,
|
||||||
|
readOnly: args.readOnly,
|
||||||
|
});
|
||||||
|
return { vault };
|
||||||
|
},
|
||||||
|
'knowledge:removeVault': async (_event, args) => {
|
||||||
|
const removed = removeKnowledgeVault(args.nameOrMountPath);
|
||||||
|
return { removed };
|
||||||
|
},
|
||||||
'mcp:listTools': async (_event, args) => {
|
'mcp:listTools': async (_event, args) => {
|
||||||
return mcpCore.listTools(args.serverName, args.cursor);
|
return mcpCore.listTools(args.serverName, args.cursor);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,15 @@ import {
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
|
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
|
||||||
import { OnboardingModal } from '@/components/onboarding-modal'
|
import { OnboardingModal } from '@/components/onboarding-modal'
|
||||||
import { SearchDialog } from '@/components/search-dialog'
|
import { SearchDialog } from '@/components/search-dialog'
|
||||||
|
|
@ -356,6 +365,10 @@ function App() {
|
||||||
const [tree, setTree] = useState<TreeNode[]>([])
|
const [tree, setTree] = useState<TreeNode[]>([])
|
||||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set())
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set())
|
||||||
const [recentWikiFiles, setRecentWikiFiles] = useState<string[]>([])
|
const [recentWikiFiles, setRecentWikiFiles] = useState<string[]>([])
|
||||||
|
const [isAddVaultOpen, setIsAddVaultOpen] = useState(false)
|
||||||
|
const [pendingVaultPath, setPendingVaultPath] = useState<string | null>(null)
|
||||||
|
const [pendingVaultName, setPendingVaultName] = useState<string>('')
|
||||||
|
const [isAddingVault, setIsAddingVault] = useState(false)
|
||||||
const [isGraphOpen, setIsGraphOpen] = useState(false)
|
const [isGraphOpen, setIsGraphOpen] = useState(false)
|
||||||
const [expandedFrom, setExpandedFrom] = useState<{ path: string | null; graph: boolean } | null>(null)
|
const [expandedFrom, setExpandedFrom] = useState<{ path: string | null; graph: boolean } | null>(null)
|
||||||
const [graphData, setGraphData] = useState<{ nodes: GraphNode[]; edges: GraphEdge[] }>({
|
const [graphData, setGraphData] = useState<{ nodes: GraphNode[]; edges: GraphEdge[] }>({
|
||||||
|
|
@ -2186,6 +2199,62 @@ function App() {
|
||||||
setExpandedPaths(newExpanded)
|
setExpandedPaths(newExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle sidebar section changes - switch to chat view for tasks
|
||||||
|
const handleSectionChange = useCallback((section: ActiveSection) => {
|
||||||
|
if (section === 'tasks') {
|
||||||
|
if (selectedBackgroundTask) return
|
||||||
|
if (selectedPath || isGraphOpen) {
|
||||||
|
void navigateToView({ type: 'chat', runId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isGraphOpen, navigateToView, runId, selectedBackgroundTask, selectedPath])
|
||||||
|
|
||||||
|
const deriveVaultName = useCallback((vaultPath: string) => {
|
||||||
|
const trimmed = vaultPath.replace(/[\\/]+$/, '')
|
||||||
|
const parts = trimmed.split(/[/\\]/)
|
||||||
|
return parts[parts.length - 1] || 'Folder'
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAddVault = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('knowledge:pickVaultDirectory', null)
|
||||||
|
if (!result.path) return
|
||||||
|
const defaultName = deriveVaultName(result.path)
|
||||||
|
setPendingVaultPath(result.path)
|
||||||
|
setPendingVaultName(defaultName)
|
||||||
|
setIsAddVaultOpen(true)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to pick vault directory:', err)
|
||||||
|
toast('Failed to open folder picker')
|
||||||
|
}
|
||||||
|
}, [deriveVaultName])
|
||||||
|
|
||||||
|
const handleConfirmAddVault = useCallback(async () => {
|
||||||
|
if (!pendingVaultPath) return
|
||||||
|
const name = pendingVaultName.trim()
|
||||||
|
if (!name) {
|
||||||
|
toast('Folder name is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsAddingVault(true)
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke('knowledge:addVault', {
|
||||||
|
path: pendingVaultPath,
|
||||||
|
name,
|
||||||
|
readOnly: false,
|
||||||
|
})
|
||||||
|
setIsAddVaultOpen(false)
|
||||||
|
setPendingVaultPath(null)
|
||||||
|
setPendingVaultName('')
|
||||||
|
loadDirectory().then(setTree)
|
||||||
|
toast(`Added knowledge folder "${name}"`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to add vault:', err)
|
||||||
|
toast('Failed to add knowledge folder')
|
||||||
|
} finally {
|
||||||
|
setIsAddingVault(false)
|
||||||
|
}
|
||||||
|
}, [loadDirectory, pendingVaultName, pendingVaultPath])
|
||||||
// Knowledge quick actions
|
// Knowledge quick actions
|
||||||
const knowledgeFiles = React.useMemo(() => {
|
const knowledgeFiles = React.useMemo(() => {
|
||||||
const files = collectFilePaths(tree).filter((path) => path.endsWith('.md'))
|
const files = collectFilePaths(tree).filter((path) => path.endsWith('.md'))
|
||||||
|
|
@ -2278,6 +2347,9 @@ function App() {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
addVault: () => {
|
||||||
|
void handleAddVault()
|
||||||
|
},
|
||||||
createFolder: async (parentPath: string = 'knowledge') => {
|
createFolder: async (parentPath: string = 'knowledge') => {
|
||||||
try {
|
try {
|
||||||
await window.ipc.invoke('workspace:mkdir', {
|
await window.ipc.invoke('workspace:mkdir', {
|
||||||
|
|
@ -2289,6 +2361,17 @@ function App() {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
unlinkVault: async (mountPath: string) => {
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke('knowledge:removeVault', { nameOrMountPath: mountPath })
|
||||||
|
if (selectedPath && (selectedPath === mountPath || selectedPath.startsWith(`${mountPath}/`))) {
|
||||||
|
setSelectedPath(null)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to unlink knowledge folder:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
openGraph: () => {
|
openGraph: () => {
|
||||||
void navigateToView({ type: 'graph' })
|
void navigateToView({ type: 'graph' })
|
||||||
},
|
},
|
||||||
|
|
@ -2355,7 +2438,19 @@ function App() {
|
||||||
onOpenInNewTab: (path: string) => {
|
onOpenInNewTab: (path: string) => {
|
||||||
openFileInNewTab(path)
|
openFileInNewTab(path)
|
||||||
},
|
},
|
||||||
}), [tree, selectedPath, workspaceRoot, navigateToFile, navigateToView, openFileInNewTab, fileTabs, closeFileTab, removeEditorCacheForPath])
|
}), [
|
||||||
|
tree,
|
||||||
|
selectedPath,
|
||||||
|
workspaceRoot,
|
||||||
|
collectDirPaths,
|
||||||
|
navigateToFile,
|
||||||
|
navigateToView,
|
||||||
|
handleAddVault,
|
||||||
|
openFileInNewTab,
|
||||||
|
fileTabs,
|
||||||
|
closeFileTab,
|
||||||
|
removeEditorCacheForPath,
|
||||||
|
])
|
||||||
|
|
||||||
// Handler for when a voice note is created/updated
|
// Handler for when a voice note is created/updated
|
||||||
const handleVoiceNoteCreated = useCallback(async (notePath: string) => {
|
const handleVoiceNoteCreated = useCallback(async (notePath: string) => {
|
||||||
|
|
@ -3089,6 +3184,50 @@ function App() {
|
||||||
onSelectRun={(id) => { void navigateToView({ type: 'chat', runId: id }) }}
|
onSelectRun={(id) => { void navigateToView({ type: 'chat', runId: id }) }}
|
||||||
/>
|
/>
|
||||||
</SidebarSectionProvider>
|
</SidebarSectionProvider>
|
||||||
|
<Dialog
|
||||||
|
open={isAddVaultOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setIsAddVaultOpen(false)
|
||||||
|
setPendingVaultPath(null)
|
||||||
|
setPendingVaultName('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Knowledge Folder</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Link an existing folder into knowledge.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-xs text-muted-foreground break-all">
|
||||||
|
{pendingVaultPath ?? 'No folder selected'}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={pendingVaultName}
|
||||||
|
onChange={(event) => setPendingVaultName(event.target.value)}
|
||||||
|
placeholder="Folder name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddVaultOpen(false)
|
||||||
|
setPendingVaultPath(null)
|
||||||
|
setPendingVaultName('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirmAddVault} disabled={isAddingVault}>
|
||||||
|
{isAddingVault ? 'Adding...' : 'Add Folder'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<OnboardingModal
|
<OnboardingModal
|
||||||
open={showOnboarding}
|
open={showOnboarding}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
Pencil,
|
Pencil,
|
||||||
Plug,
|
Plug,
|
||||||
LoaderIcon,
|
LoaderIcon,
|
||||||
|
PlusCircle,
|
||||||
Settings,
|
Settings,
|
||||||
Square,
|
Square,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
|
@ -100,6 +101,8 @@ interface TreeNode {
|
||||||
type KnowledgeActions = {
|
type KnowledgeActions = {
|
||||||
createNote: (parentPath?: string) => void
|
createNote: (parentPath?: string) => void
|
||||||
createFolder: (parentPath?: string) => void
|
createFolder: (parentPath?: string) => void
|
||||||
|
addVault: () => void
|
||||||
|
unlinkVault: (mountPath: string) => Promise<void>
|
||||||
openGraph: () => void
|
openGraph: () => void
|
||||||
expandAll: () => void
|
expandAll: () => void
|
||||||
collapseAll: () => void
|
collapseAll: () => void
|
||||||
|
|
@ -824,6 +827,7 @@ function KnowledgeSection({
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
{ icon: FilePlus, label: "New Note", action: () => actions.createNote() },
|
{ icon: FilePlus, label: "New Note", action: () => actions.createNote() },
|
||||||
{ icon: FolderPlus, label: "New Folder", action: () => actions.createFolder() },
|
{ icon: FolderPlus, label: "New Folder", action: () => actions.createFolder() },
|
||||||
|
{ icon: PlusCircle, label: "Add Knowledge Folder", action: () => actions.addVault() },
|
||||||
{ icon: Network, label: "Graph View", action: () => actions.openGraph() },
|
{ icon: Network, label: "Graph View", action: () => actions.openGraph() },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -911,6 +915,8 @@ function Tree({
|
||||||
const isDir = item.kind === 'dir'
|
const isDir = item.kind === 'dir'
|
||||||
const isExpanded = expandedPaths.has(item.path)
|
const isExpanded = expandedPaths.has(item.path)
|
||||||
const isSelected = selectedPath === item.path
|
const isSelected = selectedPath === item.path
|
||||||
|
const [isSymlinkMount, setIsSymlinkMount] = useState(false)
|
||||||
|
const [symlinkChecked, setSymlinkChecked] = useState(false)
|
||||||
const [isRenaming, setIsRenaming] = useState(false)
|
const [isRenaming, setIsRenaming] = useState(false)
|
||||||
const isSubmittingRef = React.useRef(false)
|
const isSubmittingRef = React.useRef(false)
|
||||||
|
|
||||||
|
|
@ -925,6 +931,20 @@ function Tree({
|
||||||
setNewName(baseName)
|
setNewName(baseName)
|
||||||
}, [baseName])
|
}, [baseName])
|
||||||
|
|
||||||
|
const ensureSymlinkStatus = async () => {
|
||||||
|
if (symlinkChecked || !isDir) return isSymlinkMount
|
||||||
|
try {
|
||||||
|
const stat = await window.ipc.invoke('workspace:stat', { path: item.path })
|
||||||
|
const isLink = Boolean(stat.isSymlink)
|
||||||
|
setIsSymlinkMount(isLink)
|
||||||
|
setSymlinkChecked(true)
|
||||||
|
return isLink
|
||||||
|
} catch {
|
||||||
|
setSymlinkChecked(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleRename = async () => {
|
const handleRename = async () => {
|
||||||
// Prevent double submission
|
// Prevent double submission
|
||||||
if (isSubmittingRef.current) return
|
if (isSubmittingRef.current) return
|
||||||
|
|
@ -933,6 +953,11 @@ function Tree({
|
||||||
const trimmedName = newName.trim()
|
const trimmedName = newName.trim()
|
||||||
if (trimmedName && trimmedName !== baseName) {
|
if (trimmedName && trimmedName !== baseName) {
|
||||||
try {
|
try {
|
||||||
|
if (await ensureSymlinkStatus()) {
|
||||||
|
toast('Linked folders cannot be renamed here', 'error')
|
||||||
|
setIsRenaming(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
await actions.rename(item.path, trimmedName, isDir)
|
await actions.rename(item.path, trimmedName, isDir)
|
||||||
toast('Renamed successfully', 'success')
|
toast('Renamed successfully', 'success')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -947,11 +972,18 @@ function Tree({
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
|
let isLink = false
|
||||||
try {
|
try {
|
||||||
await actions.remove(item.path)
|
isLink = await ensureSymlinkStatus()
|
||||||
toast('Moved to trash', 'success')
|
if (isLink) {
|
||||||
|
await actions.unlinkVault(item.path)
|
||||||
|
toast('Unlinked knowledge folder', 'success')
|
||||||
|
} else {
|
||||||
|
await actions.remove(item.path)
|
||||||
|
toast('Moved to trash', 'success')
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast('Failed to delete', 'error')
|
toast(isLink ? 'Failed to unlink' : 'Failed to delete', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -998,17 +1030,32 @@ function Tree({
|
||||||
Copy Path
|
Copy Path
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
<ContextMenuItem onClick={() => { setNewName(baseName); isSubmittingRef.current = false; setIsRenaming(true) }}>
|
{!isSymlinkMount && (
|
||||||
<Pencil className="mr-2 size-4" />
|
<ContextMenuItem onClick={async () => {
|
||||||
Rename
|
if (await ensureSymlinkStatus()) {
|
||||||
</ContextMenuItem>
|
toast('Linked folders cannot be renamed here', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setNewName(baseName)
|
||||||
|
isSubmittingRef.current = false
|
||||||
|
setIsRenaming(true)
|
||||||
|
}}>
|
||||||
|
<Pencil className="mr-2 size-4" />
|
||||||
|
Rename
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
<ContextMenuItem variant="destructive" onClick={handleDelete}>
|
<ContextMenuItem variant="destructive" onClick={handleDelete}>
|
||||||
<Trash2 className="mr-2 size-4" />
|
<Trash2 className="mr-2 size-4" />
|
||||||
Delete
|
{isSymlinkMount ? 'Unlink Knowledge Folder' : 'Delete'}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleContextMenuOpenChange = (open: boolean) => {
|
||||||
|
if (!open || symlinkChecked || !isDir) return
|
||||||
|
void ensureSymlinkStatus()
|
||||||
|
}
|
||||||
|
|
||||||
// Inline rename input
|
// Inline rename input
|
||||||
if (isRenaming) {
|
if (isRenaming) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1043,7 +1090,7 @@ function Tree({
|
||||||
|
|
||||||
if (!isDir) {
|
if (!isDir) {
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<SidebarMenuItem className="group/file-item">
|
<SidebarMenuItem className="group/file-item">
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
|
|
@ -1068,7 +1115,7 @@ function Tree({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
|
|
|
||||||
|
|
@ -582,6 +582,8 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
execute: async ({ pattern, cwd }: { pattern: string; cwd?: string }) => {
|
execute: async ({ pattern, cwd }: { pattern: string; cwd?: string }) => {
|
||||||
try {
|
try {
|
||||||
const searchDir = cwd ? path.join(WorkDir, cwd) : WorkDir;
|
const searchDir = cwd ? path.join(WorkDir, cwd) : WorkDir;
|
||||||
|
const normalizedCwd = (cwd ?? '').split(path.sep).join('/');
|
||||||
|
const followSymlinks = normalizedCwd === 'knowledge' || normalizedCwd.startsWith('knowledge/');
|
||||||
|
|
||||||
// Ensure search directory is within workspace
|
// Ensure search directory is within workspace
|
||||||
const resolvedSearchDir = path.resolve(searchDir);
|
const resolvedSearchDir = path.resolve(searchDir);
|
||||||
|
|
@ -593,6 +595,7 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
cwd: searchDir,
|
cwd: searchDir,
|
||||||
nodir: true,
|
nodir: true,
|
||||||
ignore: ['node_modules/**', '.git/**'],
|
ignore: ['node_modules/**', '.git/**'],
|
||||||
|
follow: followSymlinks,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -630,6 +633,8 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
maxResults?: number;
|
maxResults?: number;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
const normalizedSearchPath = (searchPath ?? '').split(path.sep).join('/');
|
||||||
|
const followSymlinks = normalizedSearchPath === 'knowledge' || normalizedSearchPath.startsWith('knowledge/');
|
||||||
const targetPath = searchPath ? path.join(WorkDir, searchPath) : WorkDir;
|
const targetPath = searchPath ? path.join(WorkDir, searchPath) : WorkDir;
|
||||||
|
|
||||||
// Ensure target path is within workspace
|
// Ensure target path is within workspace
|
||||||
|
|
@ -641,6 +646,7 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
// Try ripgrep first
|
// Try ripgrep first
|
||||||
try {
|
try {
|
||||||
const rgArgs = [
|
const rgArgs = [
|
||||||
|
followSymlinks ? '--follow' : '',
|
||||||
'--json',
|
'--json',
|
||||||
'-e', JSON.stringify(pattern),
|
'-e', JSON.stringify(pattern),
|
||||||
contextLines > 0 ? `-C ${contextLines}` : '',
|
contextLines > 0 ? `-C ${contextLines}` : '',
|
||||||
|
|
@ -679,7 +685,7 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||||
} catch (rgError) {
|
} catch (rgError) {
|
||||||
// Fallback to basic grep if ripgrep not available or failed
|
// Fallback to basic grep if ripgrep not available or failed
|
||||||
const grepArgs = [
|
const grepArgs = [
|
||||||
'-rn',
|
followSymlinks ? '-Rn' : '-rn',
|
||||||
fileGlob ? `--include=${JSON.stringify(fileGlob)}` : '',
|
fileGlob ? `--include=${JSON.stringify(fileGlob)}` : '',
|
||||||
JSON.stringify(pattern),
|
JSON.stringify(pattern),
|
||||||
JSON.stringify(resolvedTargetPath),
|
JSON.stringify(resolvedTargetPath),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { IMcpConfigRepo } from "../mcp/repo.js";
|
||||||
import type { IAgentScheduleRepo } from "../agent-schedule/repo.js";
|
import type { IAgentScheduleRepo } from "../agent-schedule/repo.js";
|
||||||
import type { IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
|
import type { IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
|
||||||
import { ensureSecurityConfig } from "./security.js";
|
import { ensureSecurityConfig } from "./security.js";
|
||||||
|
import { ensureKnowledgeVaultsConfig } from "./knowledge_vaults.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all config files at app startup.
|
* Initialize all config files at app startup.
|
||||||
|
|
@ -22,5 +23,6 @@ export async function initConfigs(): Promise<void> {
|
||||||
agentScheduleRepo.ensureConfig(),
|
agentScheduleRepo.ensureConfig(),
|
||||||
agentScheduleStateRepo.ensureState(),
|
agentScheduleStateRepo.ensureState(),
|
||||||
ensureSecurityConfig(),
|
ensureSecurityConfig(),
|
||||||
|
Promise.resolve(ensureKnowledgeVaultsConfig()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
apps/x/packages/core/src/config/knowledge_vaults.ts
Normal file
159
apps/x/packages/core/src/config/knowledge_vaults.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { WorkDir } from './config.js';
|
||||||
|
|
||||||
|
export interface KnowledgeVault {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
mountPath: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
addedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KnowledgeVaultsConfig {
|
||||||
|
vaults: KnowledgeVault[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_FILE = path.join(WorkDir, 'config', 'knowledge_vaults.json');
|
||||||
|
const RESERVED_NAMES = new Set([
|
||||||
|
'People',
|
||||||
|
'Organizations',
|
||||||
|
'Projects',
|
||||||
|
'Topics',
|
||||||
|
'.assets',
|
||||||
|
'.trash',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normalizeVaultName(input: string): string {
|
||||||
|
return input
|
||||||
|
.trim()
|
||||||
|
.replace(/[\\/]/g, '-')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRelPath(relPath: string): string {
|
||||||
|
return relPath.split(path.sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfig(): KnowledgeVaultsConfig {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(CONFIG_FILE)) {
|
||||||
|
return { vaults: [] };
|
||||||
|
}
|
||||||
|
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw) as KnowledgeVaultsConfig;
|
||||||
|
if (!parsed || !Array.isArray(parsed.vaults)) {
|
||||||
|
return { vaults: [] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
vaults: parsed.vaults.filter((vault) => typeof vault?.mountPath === 'string'),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { vaults: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig(config: KnowledgeVaultsConfig): void {
|
||||||
|
const dir = path.dirname(CONFIG_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureKnowledgeVaultsConfig(): void {
|
||||||
|
if (!fs.existsSync(CONFIG_FILE)) {
|
||||||
|
writeConfig({ vaults: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listKnowledgeVaults(): KnowledgeVault[] {
|
||||||
|
return readConfig().vaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKnowledgeVaultMountPaths(): string[] {
|
||||||
|
return readConfig().vaults.map((vault) => normalizeRelPath(vault.mountPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKnowledgeVaultMountPath(relPath: string): boolean {
|
||||||
|
const normalized = normalizeRelPath(relPath);
|
||||||
|
return getKnowledgeVaultMountPaths().includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addKnowledgeVault({
|
||||||
|
name,
|
||||||
|
path: vaultPath,
|
||||||
|
readOnly = false,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}): KnowledgeVault {
|
||||||
|
const normalizedName = normalizeVaultName(name);
|
||||||
|
if (!normalizedName) {
|
||||||
|
throw new Error('Vault name is required');
|
||||||
|
}
|
||||||
|
if (RESERVED_NAMES.has(normalizedName)) {
|
||||||
|
throw new Error('Vault name is reserved');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(vaultPath);
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
throw new Error('Vault path must be a directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = readConfig();
|
||||||
|
const mountPath = `knowledge/${normalizedName}`;
|
||||||
|
if (config.vaults.some((vault) => vault.name.toLowerCase() === normalizedName.toLowerCase())) {
|
||||||
|
throw new Error('A vault with that name already exists');
|
||||||
|
}
|
||||||
|
if (config.vaults.some((vault) => vault.path === vaultPath)) {
|
||||||
|
throw new Error('That vault is already added');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountAbsPath = path.join(WorkDir, mountPath);
|
||||||
|
if (fs.existsSync(mountAbsPath)) {
|
||||||
|
throw new Error(`Mount path already exists: ${mountPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
||||||
|
fs.symlinkSync(vaultPath, mountAbsPath, linkType);
|
||||||
|
|
||||||
|
const vault: KnowledgeVault = {
|
||||||
|
name: normalizedName,
|
||||||
|
path: vaultPath,
|
||||||
|
mountPath,
|
||||||
|
readOnly: readOnly === true,
|
||||||
|
addedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
config.vaults.push(vault);
|
||||||
|
writeConfig(config);
|
||||||
|
return vault;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeKnowledgeVault(nameOrMountPath: string): KnowledgeVault | null {
|
||||||
|
const config = readConfig();
|
||||||
|
const normalizedInput = nameOrMountPath.trim();
|
||||||
|
const mountPath = normalizedInput.startsWith('knowledge/')
|
||||||
|
? normalizedInput
|
||||||
|
: `knowledge/${normalizeVaultName(normalizedInput)}`;
|
||||||
|
const idx = config.vaults.findIndex((vault) => vault.mountPath === mountPath);
|
||||||
|
if (idx === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const [removed] = config.vaults.splice(idx, 1);
|
||||||
|
writeConfig(config);
|
||||||
|
|
||||||
|
const mountAbsPath = path.join(WorkDir, mountPath);
|
||||||
|
try {
|
||||||
|
const stats = fs.lstatSync(mountAbsPath);
|
||||||
|
if (stats.isSymbolicLink()) {
|
||||||
|
fs.unlinkSync(mountAbsPath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore missing or invalid mount path
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
@ -180,6 +180,7 @@ async function createNotesFromBatch(
|
||||||
message += `- Use the KNOWLEDGE BASE INDEX below to resolve entities - DO NOT grep/search for existing notes\n`;
|
message += `- Use the KNOWLEDGE BASE INDEX below to resolve entities - DO NOT grep/search for existing notes\n`;
|
||||||
message += `- Extract entities (people, organizations, projects, topics) from ALL files below\n`;
|
message += `- Extract entities (people, organizations, projects, topics) from ALL files below\n`;
|
||||||
message += `- Create or update notes in "knowledge" directory (workspace-relative paths like "knowledge/People/Name.md")\n`;
|
message += `- Create or update notes in "knowledge" directory (workspace-relative paths like "knowledge/People/Name.md")\n`;
|
||||||
|
message += `- For NEW notes, always write to the base knowledge folders (knowledge/People, knowledge/Organizations, knowledge/Projects, knowledge/Topics), not mounted vaults\n`;
|
||||||
message += `- If the same entity appears in multiple files, merge the information into a single note\n`;
|
message += `- If the same entity appears in multiple files, merge the information into a single note\n`;
|
||||||
message += `- Use workspace tools to read existing notes (when you need full content) and write updates\n`;
|
message += `- Use workspace tools to read existing notes (when you need full content) and write updates\n`;
|
||||||
message += `- Follow the note templates and guidelines in your instructions\n\n`;
|
message += `- Follow the note templates and guidelines in your instructions\n\n`;
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,19 @@ function getFolderType(filePath: string): string {
|
||||||
return 'root';
|
return 'root';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the first folder name
|
const categoryFolders = new Set(['People', 'Organizations', 'Projects', 'Topics']);
|
||||||
|
|
||||||
|
// Standard layout: knowledge/<Category>/...
|
||||||
|
if (categoryFolders.has(parts[0])) {
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vault layout: knowledge/<VaultName>/<Category>/...
|
||||||
|
if (parts.length > 1 && categoryFolders.has(parts[1])) {
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first folder name for non-standard layouts
|
||||||
return parts[0];
|
return parts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export async function createWorkspaceWatcher(
|
||||||
|
|
||||||
const watcher = chokidar.watch(WorkDir, {
|
const watcher = chokidar.watch(WorkDir, {
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
|
followSymlinks: true,
|
||||||
awaitWriteFinish: {
|
awaitWriteFinish: {
|
||||||
stabilityThreshold: 150,
|
stabilityThreshold: 150,
|
||||||
pollInterval: 50,
|
pollInterval: 50,
|
||||||
|
|
@ -74,4 +75,3 @@ export async function createWorkspaceWatcher(
|
||||||
|
|
||||||
return watcher;
|
return watcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { workspace } from '@x/shared';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js';
|
import { RemoveOptions, WriteFileOptions, WriteFileResult } from 'packages/shared/dist/workspace.js';
|
||||||
import { WorkDir } from '../config/config.js';
|
import { WorkDir } from '../config/config.js';
|
||||||
|
import { getKnowledgeVaultMountPaths } from '../config/knowledge_vaults.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Path Utilities
|
// Path Utilities
|
||||||
|
|
@ -82,6 +83,15 @@ export function statToSchema(stats: Stats, kind: z.infer<typeof workspace.NodeKi
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRelPath(relPath: string): string {
|
||||||
|
return relPath.split(path.sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedVaultSymlink(relPath: string, vaultMounts: Set<string>): boolean {
|
||||||
|
const normalized = normalizeRelPath(relPath);
|
||||||
|
return vaultMounts.has(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure workspace root exists
|
* Ensure workspace root exists
|
||||||
*/
|
*/
|
||||||
|
|
@ -111,6 +121,17 @@ export async function exists(relPath: string): Promise<{ exists: boolean }> {
|
||||||
export async function stat(relPath: string): Promise<z.infer<typeof workspace.Stat>> {
|
export async function stat(relPath: string): Promise<z.infer<typeof workspace.Stat>> {
|
||||||
const filePath = resolveWorkspacePath(relPath);
|
const filePath = resolveWorkspacePath(relPath);
|
||||||
const stats = await fs.lstat(filePath);
|
const stats = await fs.lstat(filePath);
|
||||||
|
if (stats.isSymbolicLink()) {
|
||||||
|
const targetStats = await fs.stat(filePath);
|
||||||
|
const kind = targetStats.isDirectory() ? 'dir' : 'file';
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
size: targetStats.size,
|
||||||
|
mtimeMs: targetStats.mtimeMs,
|
||||||
|
ctimeMs: targetStats.ctimeMs,
|
||||||
|
isSymlink: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
const kind = stats.isDirectory() ? 'dir' : 'file';
|
const kind = stats.isDirectory() ? 'dir' : 'file';
|
||||||
return statToSchema(stats, kind);
|
return statToSchema(stats, kind);
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +142,8 @@ export async function readdir(
|
||||||
): Promise<Array<z.infer<typeof workspace.DirEntry>>> {
|
): Promise<Array<z.infer<typeof workspace.DirEntry>>> {
|
||||||
const dirPath = resolveWorkspacePath(relPath);
|
const dirPath = resolveWorkspacePath(relPath);
|
||||||
const entries: Array<z.infer<typeof workspace.DirEntry>> = [];
|
const entries: Array<z.infer<typeof workspace.DirEntry>> = [];
|
||||||
|
const vaultMounts = new Set(getKnowledgeVaultMountPaths());
|
||||||
|
const visitedRealPaths = new Set<string>();
|
||||||
|
|
||||||
async function readDir(currentPath: string, currentRelPath: string): Promise<void> {
|
async function readDir(currentPath: string, currentRelPath: string): Promise<void> {
|
||||||
const items = await fs.readdir(currentPath, { withFileTypes: true });
|
const items = await fs.readdir(currentPath, { withFileTypes: true });
|
||||||
|
|
@ -145,7 +168,42 @@ export async function readdir(
|
||||||
let itemKind: z.infer<typeof workspace.NodeKind>;
|
let itemKind: z.infer<typeof workspace.NodeKind>;
|
||||||
let itemStat: { size: number; mtimeMs: number } | undefined;
|
let itemStat: { size: number; mtimeMs: number } | undefined;
|
||||||
|
|
||||||
if (item.isDirectory()) {
|
if (item.isSymbolicLink()) {
|
||||||
|
if (!isAllowedVaultSymlink(itemRelPath, vaultMounts)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let targetStats: Stats;
|
||||||
|
try {
|
||||||
|
targetStats = await fs.stat(itemPath);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targetIsDir = targetStats.isDirectory();
|
||||||
|
const targetIsFile = targetStats.isFile();
|
||||||
|
if (!targetIsDir && !targetIsFile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (targetIsFile && opts?.allowedExtensions && opts.allowedExtensions.length > 0) {
|
||||||
|
const ext = path.extname(item.name);
|
||||||
|
if (!opts.allowedExtensions.includes(ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.includeStats) {
|
||||||
|
itemStat = { size: targetStats.size, mtimeMs: targetStats.mtimeMs };
|
||||||
|
}
|
||||||
|
itemKind = targetIsDir ? 'dir' : 'file';
|
||||||
|
entries.push({ name: item.name, path: itemRelPath, kind: itemKind, stat: itemStat });
|
||||||
|
|
||||||
|
if (targetIsDir && opts?.recursive) {
|
||||||
|
const realPath = await fs.realpath(itemPath).catch(() => null);
|
||||||
|
if (realPath && !visitedRealPaths.has(realPath)) {
|
||||||
|
visitedRealPaths.add(realPath);
|
||||||
|
await readDir(itemPath, itemRelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (item.isDirectory()) {
|
||||||
itemKind = 'dir';
|
itemKind = 'dir';
|
||||||
if (opts?.includeStats) {
|
if (opts?.includeStats) {
|
||||||
const stats = await fs.lstat(itemPath);
|
const stats = await fs.lstat(itemPath);
|
||||||
|
|
@ -187,6 +245,8 @@ export async function readFile(
|
||||||
): Promise<z.infer<typeof workspace.ReadFileResult>> {
|
): Promise<z.infer<typeof workspace.ReadFileResult>> {
|
||||||
const filePath = resolveWorkspacePath(relPath);
|
const filePath = resolveWorkspacePath(relPath);
|
||||||
const stats = await fs.lstat(filePath);
|
const stats = await fs.lstat(filePath);
|
||||||
|
const isSymlink = stats.isSymbolicLink();
|
||||||
|
const targetStats = isSymlink ? await fs.stat(filePath) : stats;
|
||||||
|
|
||||||
let data: string;
|
let data: string;
|
||||||
if (encoding === 'utf8') {
|
if (encoding === 'utf8') {
|
||||||
|
|
@ -200,8 +260,14 @@ export async function readFile(
|
||||||
data = buffer.toString('base64');
|
data = buffer.toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stat = statToSchema(stats, 'file');
|
const stat: z.infer<typeof workspace.Stat> = {
|
||||||
const etag = computeEtag(stats.size, stats.mtimeMs);
|
kind: 'file',
|
||||||
|
size: targetStats.size,
|
||||||
|
mtimeMs: targetStats.mtimeMs,
|
||||||
|
ctimeMs: targetStats.ctimeMs,
|
||||||
|
isSymlink: isSymlink ? true : undefined,
|
||||||
|
};
|
||||||
|
const etag = computeEtag(targetStats.size, targetStats.mtimeMs);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: relPath,
|
path: relPath,
|
||||||
|
|
@ -383,4 +449,4 @@ export async function remove(
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,14 @@ import { ServiceEvent } from './service-events.js';
|
||||||
// Runtime Validation Schemas (Single Source of Truth)
|
// Runtime Validation Schemas (Single Source of Truth)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
const KnowledgeVault = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
path: z.string(),
|
||||||
|
mountPath: z.string(),
|
||||||
|
readOnly: z.boolean(),
|
||||||
|
addedAt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const ipcSchemas = {
|
const ipcSchemas = {
|
||||||
'app:getVersions': {
|
'app:getVersions': {
|
||||||
req: z.null(),
|
req: z.null(),
|
||||||
|
|
@ -104,6 +112,36 @@ const ipcSchemas = {
|
||||||
req: WorkspaceChangeEvent,
|
req: WorkspaceChangeEvent,
|
||||||
res: z.null(),
|
res: z.null(),
|
||||||
},
|
},
|
||||||
|
'knowledge:listVaults': {
|
||||||
|
req: z.null(),
|
||||||
|
res: z.object({
|
||||||
|
vaults: z.array(KnowledgeVault),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'knowledge:pickVaultDirectory': {
|
||||||
|
req: z.null(),
|
||||||
|
res: z.object({
|
||||||
|
path: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'knowledge:addVault': {
|
||||||
|
req: z.object({
|
||||||
|
path: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
readOnly: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
vault: KnowledgeVault,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'knowledge:removeVault': {
|
||||||
|
req: z.object({
|
||||||
|
nameOrMountPath: z.string(),
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
removed: KnowledgeVault.nullable(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
'mcp:listTools': {
|
'mcp:listTools': {
|
||||||
req: z.object({
|
req: z.object({
|
||||||
serverName: z.string(),
|
serverName: z.string(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue