@swarmclawai/swarmclaw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { UPLOAD_DIR } from '../storage'
|
|
6
|
+
import type { ToolBuildContext } from './context'
|
|
7
|
+
import { safePath, truncate, listDirRecursive, MAX_OUTPUT, MAX_FILE } from './context'
|
|
8
|
+
|
|
9
|
+
export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
10
|
+
const tools: StructuredToolInterface[] = []
|
|
11
|
+
|
|
12
|
+
const filesEnabled = bctx.hasTool('files')
|
|
13
|
+
const canReadFiles = filesEnabled || bctx.hasTool('read_file')
|
|
14
|
+
const canWriteFiles = filesEnabled || bctx.hasTool('write_file')
|
|
15
|
+
const canListFiles = filesEnabled || bctx.hasTool('list_files')
|
|
16
|
+
const canSendFiles = filesEnabled || bctx.hasTool('send_file')
|
|
17
|
+
const canCopyFiles = filesEnabled || bctx.hasTool('copy_file')
|
|
18
|
+
const canMoveFiles = filesEnabled || bctx.hasTool('move_file')
|
|
19
|
+
const canDeleteFiles = bctx.hasTool('delete_file')
|
|
20
|
+
|
|
21
|
+
if (canReadFiles) {
|
|
22
|
+
tools.push(
|
|
23
|
+
tool(
|
|
24
|
+
async ({ filePath }) => {
|
|
25
|
+
try {
|
|
26
|
+
const resolved = safePath(bctx.cwd, filePath)
|
|
27
|
+
const content = fs.readFileSync(resolved, 'utf-8')
|
|
28
|
+
return truncate(content, MAX_FILE)
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
return `Error reading file: ${err.message}`
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'read_file',
|
|
35
|
+
description: 'Read a file from the session working directory.',
|
|
36
|
+
schema: z.object({
|
|
37
|
+
filePath: z.string().describe('Relative path to the file'),
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (canWriteFiles) {
|
|
45
|
+
tools.push(
|
|
46
|
+
tool(
|
|
47
|
+
async ({ filePath, content }) => {
|
|
48
|
+
try {
|
|
49
|
+
const resolved = safePath(bctx.cwd, filePath)
|
|
50
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
51
|
+
fs.writeFileSync(resolved, content, 'utf-8')
|
|
52
|
+
return `File written: ${filePath} (${content.length} bytes)`
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
return `Error writing file: ${err.message}`
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'write_file',
|
|
59
|
+
description: 'Write content to a file in the session working directory. Creates directories if needed.',
|
|
60
|
+
schema: z.object({
|
|
61
|
+
filePath: z.string().describe('Relative path to the file'),
|
|
62
|
+
content: z.string().describe('The content to write'),
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (canListFiles) {
|
|
70
|
+
tools.push(
|
|
71
|
+
tool(
|
|
72
|
+
async ({ dirPath }) => {
|
|
73
|
+
try {
|
|
74
|
+
const resolved = safePath(bctx.cwd, dirPath || '.')
|
|
75
|
+
const tree = listDirRecursive(resolved, 0, 3)
|
|
76
|
+
return tree.length ? tree.join('\n') : '(empty directory)'
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
return `Error listing files: ${err.message}`
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'list_files',
|
|
83
|
+
description: 'List files in the session working directory recursively (max depth 3).',
|
|
84
|
+
schema: z.object({
|
|
85
|
+
dirPath: z.string().optional().describe('Relative path to list (defaults to working directory)'),
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (canCopyFiles) {
|
|
93
|
+
tools.push(
|
|
94
|
+
tool(
|
|
95
|
+
async ({ sourcePath, destinationPath, overwrite }) => {
|
|
96
|
+
try {
|
|
97
|
+
const source = safePath(bctx.cwd, sourcePath)
|
|
98
|
+
const destination = safePath(bctx.cwd, destinationPath)
|
|
99
|
+
if (!fs.existsSync(source)) return `Error: source file not found: ${sourcePath}`
|
|
100
|
+
const sourceStat = fs.statSync(source)
|
|
101
|
+
if (sourceStat.isDirectory()) return `Error: source must be a file (directories are not supported by copy_file).`
|
|
102
|
+
if (fs.existsSync(destination) && !overwrite) return `Error: destination already exists: ${destinationPath} (set overwrite=true to replace).`
|
|
103
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
104
|
+
fs.copyFileSync(source, destination)
|
|
105
|
+
return `File copied: ${sourcePath} -> ${destinationPath}`
|
|
106
|
+
} catch (err: any) {
|
|
107
|
+
return `Error copying file: ${err.message}`
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'copy_file',
|
|
112
|
+
description: 'Copy a file to a new location in the working directory.',
|
|
113
|
+
schema: z.object({
|
|
114
|
+
sourcePath: z.string().describe('Source file path (relative to working directory)'),
|
|
115
|
+
destinationPath: z.string().describe('Destination file path (relative to working directory)'),
|
|
116
|
+
overwrite: z.boolean().optional().describe('Overwrite destination if it exists (default false)'),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (canMoveFiles) {
|
|
124
|
+
tools.push(
|
|
125
|
+
tool(
|
|
126
|
+
async ({ sourcePath, destinationPath, overwrite }) => {
|
|
127
|
+
try {
|
|
128
|
+
const source = safePath(bctx.cwd, sourcePath)
|
|
129
|
+
const destination = safePath(bctx.cwd, destinationPath)
|
|
130
|
+
if (!fs.existsSync(source)) return `Error: source file not found: ${sourcePath}`
|
|
131
|
+
const sourceStat = fs.statSync(source)
|
|
132
|
+
if (sourceStat.isDirectory()) return `Error: source must be a file (directories are not supported by move_file).`
|
|
133
|
+
if (fs.existsSync(destination) && !overwrite) return `Error: destination already exists: ${destinationPath} (set overwrite=true to replace).`
|
|
134
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
135
|
+
if (fs.existsSync(destination) && overwrite) fs.unlinkSync(destination)
|
|
136
|
+
fs.renameSync(source, destination)
|
|
137
|
+
return `File moved: ${sourcePath} -> ${destinationPath}`
|
|
138
|
+
} catch (err: any) {
|
|
139
|
+
return `Error moving file: ${err.message}`
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'move_file',
|
|
144
|
+
description: 'Move (rename) a file to a new location in the working directory.',
|
|
145
|
+
schema: z.object({
|
|
146
|
+
sourcePath: z.string().describe('Source file path (relative to working directory)'),
|
|
147
|
+
destinationPath: z.string().describe('Destination file path (relative to working directory)'),
|
|
148
|
+
overwrite: z.boolean().optional().describe('Overwrite destination if it exists (default false)'),
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (canDeleteFiles) {
|
|
156
|
+
tools.push(
|
|
157
|
+
tool(
|
|
158
|
+
async ({ filePath, recursive, force }) => {
|
|
159
|
+
try {
|
|
160
|
+
const resolved = safePath(bctx.cwd, filePath)
|
|
161
|
+
const root = path.resolve(bctx.cwd)
|
|
162
|
+
if (resolved === root) return 'Error: refusing to delete the session working directory root.'
|
|
163
|
+
if (!fs.existsSync(resolved)) {
|
|
164
|
+
return force ? `Path already absent: ${filePath}` : `Error: path not found: ${filePath}`
|
|
165
|
+
}
|
|
166
|
+
const stat = fs.statSync(resolved)
|
|
167
|
+
if (stat.isDirectory() && !recursive) {
|
|
168
|
+
return 'Error: target is a directory. Set recursive=true to delete directories.'
|
|
169
|
+
}
|
|
170
|
+
fs.rmSync(resolved, { recursive: !!recursive, force: !!force })
|
|
171
|
+
return `Deleted: ${filePath}`
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
return `Error deleting file: ${err.message}`
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'delete_file',
|
|
178
|
+
description: 'Delete a file or directory from the working directory. Disabled by default and must be explicitly enabled.',
|
|
179
|
+
schema: z.object({
|
|
180
|
+
filePath: z.string().describe('Path to delete (relative to working directory)'),
|
|
181
|
+
recursive: z.boolean().optional().describe('Required for deleting directories'),
|
|
182
|
+
force: z.boolean().optional().describe('Ignore missing paths and force deletion where possible'),
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (canSendFiles) {
|
|
190
|
+
tools.push(
|
|
191
|
+
tool(
|
|
192
|
+
async ({ filePath: rawPath }) => {
|
|
193
|
+
try {
|
|
194
|
+
// Resolve relative to cwd, but also allow absolute paths
|
|
195
|
+
const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(bctx.cwd, rawPath)
|
|
196
|
+
if (!fs.existsSync(resolved)) return `Error: file not found: ${rawPath}`
|
|
197
|
+
const stat = fs.statSync(resolved)
|
|
198
|
+
if (stat.isDirectory()) return `Error: cannot send a directory. Send individual files instead.`
|
|
199
|
+
if (stat.size > 100 * 1024 * 1024) return `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 100MB.`
|
|
200
|
+
|
|
201
|
+
const ext = path.extname(resolved).slice(1).toLowerCase()
|
|
202
|
+
const basename = path.basename(resolved)
|
|
203
|
+
const filename = `${Date.now()}-${basename}`
|
|
204
|
+
const dest = path.join(UPLOAD_DIR, filename)
|
|
205
|
+
fs.copyFileSync(resolved, dest)
|
|
206
|
+
|
|
207
|
+
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']
|
|
208
|
+
const VIDEO_EXTS = ['mp4', 'webm', 'mov', 'avi', 'mkv']
|
|
209
|
+
|
|
210
|
+
if (IMAGE_EXTS.includes(ext)) {
|
|
211
|
+
return ``
|
|
212
|
+
} else if (VIDEO_EXTS.includes(ext)) {
|
|
213
|
+
return ``
|
|
214
|
+
} else {
|
|
215
|
+
return `[Download ${basename}](/api/uploads/${filename})`
|
|
216
|
+
}
|
|
217
|
+
} catch (err: any) {
|
|
218
|
+
return `Error sending file: ${err.message}`
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'send_file',
|
|
223
|
+
description: 'Send a file to the user so they can view or download it in the chat. Works with images, videos, PDFs, documents, and any other file type. The file will appear inline for images/videos, or as a download link for other types.',
|
|
224
|
+
schema: z.object({
|
|
225
|
+
filePath: z.string().describe('Path to the file (relative to working directory, or absolute)'),
|
|
226
|
+
}),
|
|
227
|
+
},
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (bctx.hasTool('edit_file')) {
|
|
233
|
+
tools.push(
|
|
234
|
+
tool(
|
|
235
|
+
async ({ filePath, oldText, newText }) => {
|
|
236
|
+
try {
|
|
237
|
+
const resolved = safePath(bctx.cwd, filePath)
|
|
238
|
+
if (!fs.existsSync(resolved)) return `Error: File not found: ${filePath}`
|
|
239
|
+
const content = fs.readFileSync(resolved, 'utf-8')
|
|
240
|
+
const count = content.split(oldText).length - 1
|
|
241
|
+
if (count === 0) return `Error: oldText not found in ${filePath}`
|
|
242
|
+
if (count > 1) return `Error: oldText found ${count} times in ${filePath}. Make it more specific.`
|
|
243
|
+
const updated = content.replace(oldText, newText)
|
|
244
|
+
fs.writeFileSync(resolved, updated, 'utf-8')
|
|
245
|
+
return `Successfully edited ${filePath}`
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
return `Error editing file: ${err.message}`
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'edit_file',
|
|
252
|
+
description: 'Search and replace text in a file. The oldText must match exactly once in the file.',
|
|
253
|
+
schema: z.object({
|
|
254
|
+
filePath: z.string().describe('Relative path to the file'),
|
|
255
|
+
oldText: z.string().describe('Exact text to find (must be unique in the file)'),
|
|
256
|
+
newText: z.string().describe('Text to replace it with'),
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return tools
|
|
264
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { loadSettings, loadSessions, saveSessions, loadMcpServers } from '../storage'
|
|
4
|
+
import { loadRuntimeSettings } from '../runtime-settings'
|
|
5
|
+
import { log } from '../logger'
|
|
6
|
+
import { resolveSessionToolPolicy } from '../tool-capability-policy'
|
|
7
|
+
import type { ToolContext, SessionToolsResult, ToolBuildContext } from './context'
|
|
8
|
+
import { buildShellTools } from './shell'
|
|
9
|
+
import { buildFileTools } from './file'
|
|
10
|
+
import { buildDelegateTools } from './delegate'
|
|
11
|
+
import { buildWebTools, sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser } from './web'
|
|
12
|
+
import { buildMemoryTools } from './memory'
|
|
13
|
+
import { buildCrudTools } from './crud'
|
|
14
|
+
import { buildSessionInfoTools } from './session-info'
|
|
15
|
+
import { buildConnectorTools } from './connector'
|
|
16
|
+
import { buildContextTools } from './context-mgmt'
|
|
17
|
+
|
|
18
|
+
export type { ToolContext, SessionToolsResult }
|
|
19
|
+
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
20
|
+
|
|
21
|
+
export async function buildSessionTools(cwd: string, enabledTools: string[], ctx?: ToolContext): Promise<SessionToolsResult> {
|
|
22
|
+
const tools: StructuredToolInterface[] = []
|
|
23
|
+
const cleanupFns: (() => Promise<void>)[] = []
|
|
24
|
+
const runtime = loadRuntimeSettings()
|
|
25
|
+
const commandTimeoutMs = runtime.shellCommandTimeoutMs
|
|
26
|
+
const claudeTimeoutMs = runtime.claudeCodeTimeoutMs
|
|
27
|
+
const cliProcessTimeoutMs = runtime.cliProcessTimeoutMs
|
|
28
|
+
const appSettings = loadSettings()
|
|
29
|
+
const toolPolicy = resolveSessionToolPolicy(enabledTools, appSettings)
|
|
30
|
+
const activeTools = toolPolicy.enabledTools
|
|
31
|
+
const hasTool = (toolName: string) => activeTools.includes(toolName)
|
|
32
|
+
|
|
33
|
+
if (toolPolicy.blockedTools.length > 0) {
|
|
34
|
+
log.info('session-tools', 'Capability policy blocked tool families', {
|
|
35
|
+
sessionId: ctx?.sessionId || null,
|
|
36
|
+
agentId: ctx?.agentId || null,
|
|
37
|
+
blockedTools: toolPolicy.blockedTools.map((entry) => `${entry.tool}:${entry.reason}`),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolveCurrentSession = (): any | null => {
|
|
42
|
+
if (!ctx?.sessionId) return null
|
|
43
|
+
const sessions = loadSessions()
|
|
44
|
+
return sessions[ctx.sessionId] || null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const readStoredDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode'): string | null => {
|
|
48
|
+
const session = resolveCurrentSession()
|
|
49
|
+
if (!session?.delegateResumeIds || typeof session.delegateResumeIds !== 'object') return null
|
|
50
|
+
const raw = session.delegateResumeIds[key]
|
|
51
|
+
return typeof raw === 'string' && raw.trim() ? raw.trim() : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const persistDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode', resumeId: string | null | undefined): void => {
|
|
55
|
+
const normalized = typeof resumeId === 'string' ? resumeId.trim() : ''
|
|
56
|
+
if (!normalized || !ctx?.sessionId) return
|
|
57
|
+
const sessions = loadSessions()
|
|
58
|
+
const target = sessions[ctx.sessionId]
|
|
59
|
+
if (!target) return
|
|
60
|
+
const current = (target.delegateResumeIds && typeof target.delegateResumeIds === 'object')
|
|
61
|
+
? target.delegateResumeIds
|
|
62
|
+
: {}
|
|
63
|
+
target.delegateResumeIds = {
|
|
64
|
+
...current,
|
|
65
|
+
[key]: normalized,
|
|
66
|
+
}
|
|
67
|
+
target.updatedAt = Date.now()
|
|
68
|
+
sessions[ctx.sessionId] = target
|
|
69
|
+
saveSessions(sessions)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const bctx: ToolBuildContext = {
|
|
73
|
+
cwd,
|
|
74
|
+
ctx,
|
|
75
|
+
hasTool,
|
|
76
|
+
cleanupFns,
|
|
77
|
+
commandTimeoutMs,
|
|
78
|
+
claudeTimeoutMs,
|
|
79
|
+
cliProcessTimeoutMs,
|
|
80
|
+
persistDelegateResumeId,
|
|
81
|
+
readStoredDelegateResumeId,
|
|
82
|
+
resolveCurrentSession,
|
|
83
|
+
activeTools,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
tools.push(
|
|
87
|
+
...buildShellTools(bctx),
|
|
88
|
+
...buildFileTools(bctx),
|
|
89
|
+
...buildDelegateTools(bctx),
|
|
90
|
+
...buildWebTools(bctx),
|
|
91
|
+
...buildMemoryTools(bctx),
|
|
92
|
+
...buildCrudTools(bctx),
|
|
93
|
+
...buildSessionInfoTools(bctx),
|
|
94
|
+
...buildConnectorTools(bctx),
|
|
95
|
+
...buildContextTools(bctx),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// MCP server tools — first-class injection (each MCP tool becomes its own LangChain tool)
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
const disabledMcpToolNames = new Set<string>(ctx?.mcpDisabledTools ?? [])
|
|
102
|
+
|
|
103
|
+
if (ctx?.mcpServerIds?.length) {
|
|
104
|
+
const mcpConnections: Array<{ client: any; transport: any }> = []
|
|
105
|
+
const allMcpServers = loadMcpServers()
|
|
106
|
+
|
|
107
|
+
for (const serverId of ctx.mcpServerIds) {
|
|
108
|
+
const config = allMcpServers[serverId]
|
|
109
|
+
if (!config) continue
|
|
110
|
+
try {
|
|
111
|
+
const { connectMcpServer, mcpToolsToLangChain } = await import('../mcp-client')
|
|
112
|
+
const conn = await connectMcpServer(config)
|
|
113
|
+
mcpConnections.push(conn)
|
|
114
|
+
const mcpLcTools = await mcpToolsToLangChain(conn.client, config.name)
|
|
115
|
+
for (const t of mcpLcTools) {
|
|
116
|
+
if (!disabledMcpToolNames.has(t.name)) {
|
|
117
|
+
tools.push(t)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
log.warn('session-tools', `Failed to connect MCP server "${config.name}"`, { serverId, error: err.message })
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Register cleanup for all MCP connections
|
|
126
|
+
cleanupFns.push(async () => {
|
|
127
|
+
const { disconnectMcpServer } = await import('../mcp-client')
|
|
128
|
+
for (const conn of mcpConnections) {
|
|
129
|
+
await disconnectMcpServer(conn.client, conn.transport)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// request_tool_access: always available
|
|
135
|
+
tools.push(
|
|
136
|
+
tool(
|
|
137
|
+
async ({ toolId, reason }) => {
|
|
138
|
+
return JSON.stringify({
|
|
139
|
+
type: 'tool_request',
|
|
140
|
+
toolId,
|
|
141
|
+
reason,
|
|
142
|
+
message: `Tool access request sent to user for "${toolId}". Wait for the user to grant access before trying to use it.`,
|
|
143
|
+
})
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'request_tool_access',
|
|
147
|
+
description: 'Request access to a tool that is currently disabled. The user will be prompted to grant access. Use this when you need a tool from the disabled tools list.',
|
|
148
|
+
schema: z.object({
|
|
149
|
+
toolId: z.string().describe('The tool ID to request access for (e.g. manage_tasks, shell, claude_code)'),
|
|
150
|
+
reason: z.string().describe('Brief explanation of why you need this tool'),
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
tools,
|
|
158
|
+
cleanup: async () => {
|
|
159
|
+
for (const fn of cleanupFns) {
|
|
160
|
+
try { await fn() } catch { /* ignore */ }
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import crypto from 'crypto'
|
|
5
|
+
import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset } from '../memory-db'
|
|
6
|
+
import { loadSettings } from '../storage'
|
|
7
|
+
import type { ToolBuildContext } from './context'
|
|
8
|
+
|
|
9
|
+
export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
10
|
+
const tools: StructuredToolInterface[] = []
|
|
11
|
+
const { ctx, hasTool } = bctx
|
|
12
|
+
|
|
13
|
+
if (hasTool('memory')) {
|
|
14
|
+
const memDb = getMemoryDb()
|
|
15
|
+
|
|
16
|
+
tools.push(
|
|
17
|
+
tool(
|
|
18
|
+
async ({ action, key, value, category, query, scope, filePaths, references, project, imagePath, linkedMemoryIds, depth, linkedLimit, targetIds, tags }) => {
|
|
19
|
+
try {
|
|
20
|
+
const scopeMode = scope || 'auto'
|
|
21
|
+
const currentAgentId = ctx?.agentId || null
|
|
22
|
+
const canAccessMemory = (m: any) => !m?.agentId || m.agentId === currentAgentId
|
|
23
|
+
const filterScope = (rows: any[]) => {
|
|
24
|
+
if (scopeMode === 'shared') return rows.filter((m) => !m.agentId)
|
|
25
|
+
if (scopeMode === 'agent') return rows.filter((m) => currentAgentId && m.agentId === currentAgentId)
|
|
26
|
+
return rows.filter(canAccessMemory)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const limits = getMemoryLookupLimits(loadSettings())
|
|
30
|
+
const requestedDepth = typeof depth === 'number' ? depth : 0
|
|
31
|
+
const requestedLinkedLimit = typeof linkedLimit === 'number' ? linkedLimit : limits.maxLinkedExpansion
|
|
32
|
+
const effectiveDepth = Math.max(0, Math.min(requestedDepth, limits.maxDepth))
|
|
33
|
+
const effectiveLinkedLimit = Math.max(0, Math.min(requestedLinkedLimit, limits.maxLinkedExpansion))
|
|
34
|
+
const maxPerLookup = limits.maxPerLookup
|
|
35
|
+
|
|
36
|
+
const normalizedLegacyRefs = Array.isArray(filePaths)
|
|
37
|
+
? filePaths.map((f: any) => ({
|
|
38
|
+
type: f.kind === 'project' ? 'project' : (f.kind === 'folder' ? 'folder' : 'file'),
|
|
39
|
+
path: f.path,
|
|
40
|
+
projectRoot: f.projectRoot,
|
|
41
|
+
projectName: f.projectName,
|
|
42
|
+
note: f.contextSnippet,
|
|
43
|
+
timestamp: typeof f.timestamp === 'number' ? f.timestamp : Date.now(),
|
|
44
|
+
}))
|
|
45
|
+
: []
|
|
46
|
+
const normalizedRefs = Array.isArray(references) ? references : []
|
|
47
|
+
if (project?.rootPath) {
|
|
48
|
+
normalizedRefs.push({
|
|
49
|
+
type: 'project',
|
|
50
|
+
path: project.rootPath,
|
|
51
|
+
projectRoot: project.rootPath,
|
|
52
|
+
projectName: project.name,
|
|
53
|
+
title: project.name,
|
|
54
|
+
note: project.note,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
const mergedRefs = [...normalizedLegacyRefs, ...normalizedRefs]
|
|
59
|
+
|
|
60
|
+
const formatEntry = (m: any) => {
|
|
61
|
+
let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
|
|
62
|
+
if (m.references?.length) {
|
|
63
|
+
line += `\n refs: ${m.references.map((r: any) => {
|
|
64
|
+
const core = r.path || r.title || r.type
|
|
65
|
+
const projectMeta = r.projectName ? ` @${r.projectName}` : ''
|
|
66
|
+
const existsMeta = typeof r.exists === 'boolean' ? (r.exists ? ' (exists)' : ' (missing)') : ''
|
|
67
|
+
return `${r.type}:${core}${projectMeta}${existsMeta}`
|
|
68
|
+
}).join(', ')}`
|
|
69
|
+
} else if (m.filePaths?.length) {
|
|
70
|
+
line += `\n files: ${m.filePaths.map((f: any) => `${f.path}${f.contextSnippet ? ` (${f.contextSnippet})` : ''}`).join(', ')}`
|
|
71
|
+
}
|
|
72
|
+
if (m.image?.path || m.imagePath) line += `\n image: ${m.image?.path || m.imagePath}`
|
|
73
|
+
if (m.linkedMemoryIds?.length) line += `\n linked: ${m.linkedMemoryIds.join(', ')}`
|
|
74
|
+
return line
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (action === 'store') {
|
|
78
|
+
let storedImage: any = null
|
|
79
|
+
if (imagePath) {
|
|
80
|
+
if (!fs.existsSync(imagePath)) {
|
|
81
|
+
return `Error: image file not found: ${imagePath}`
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
storedImage = await storeMemoryImageAsset(imagePath, crypto.randomBytes(6).toString('hex'))
|
|
85
|
+
} catch {
|
|
86
|
+
return `Error: failed to process image at ${imagePath}`
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const entry = memDb.add({
|
|
91
|
+
agentId: scopeMode === 'shared' ? null : currentAgentId,
|
|
92
|
+
sessionId: ctx?.sessionId || null,
|
|
93
|
+
category: category || 'note',
|
|
94
|
+
title: key,
|
|
95
|
+
content: value || '',
|
|
96
|
+
references: mergedRefs as any,
|
|
97
|
+
filePaths: filePaths as any,
|
|
98
|
+
image: storedImage,
|
|
99
|
+
imagePath: storedImage?.path || undefined,
|
|
100
|
+
linkedMemoryIds,
|
|
101
|
+
})
|
|
102
|
+
const memoryScope = entry.agentId ? 'agent' : 'shared'
|
|
103
|
+
let result = `Stored ${memoryScope} memory "${key}" (id: ${entry.id})`
|
|
104
|
+
if (mergedRefs.length) result += ` with ${mergedRefs.length} reference(s)`
|
|
105
|
+
if (storedImage?.path) result += ` with image`
|
|
106
|
+
if (linkedMemoryIds?.length) result += ` linked to ${linkedMemoryIds.length} memor${linkedMemoryIds.length === 1 ? 'y' : 'ies'}`
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
if (action === 'get') {
|
|
110
|
+
if (effectiveDepth > 0) {
|
|
111
|
+
const result = memDb.getWithLinked(key, effectiveDepth, maxPerLookup, effectiveLinkedLimit)
|
|
112
|
+
if (!result) return `Memory not found: ${key}`
|
|
113
|
+
const accessible = result.entries.filter(canAccessMemory)
|
|
114
|
+
if (!accessible.length) return 'Error: you do not have access to that memory.'
|
|
115
|
+
let output = accessible.map(formatEntry).join('\n---\n')
|
|
116
|
+
if (result.truncated) output += `\n\n[Results truncated at ${maxPerLookup} memories / ${effectiveLinkedLimit} linked expansions]`
|
|
117
|
+
return output
|
|
118
|
+
}
|
|
119
|
+
const found = memDb.get(key)
|
|
120
|
+
if (!found) return `Memory not found: ${key}`
|
|
121
|
+
if (!canAccessMemory(found)) return 'Error: you do not have access to that memory.'
|
|
122
|
+
return formatEntry(found)
|
|
123
|
+
}
|
|
124
|
+
if (action === 'search') {
|
|
125
|
+
if (effectiveDepth > 0) {
|
|
126
|
+
const result = memDb.searchWithLinked(query || key, undefined, effectiveDepth, maxPerLookup, effectiveLinkedLimit)
|
|
127
|
+
const accessible = filterScope(result.entries)
|
|
128
|
+
if (!accessible.length) return 'No memories found.'
|
|
129
|
+
let output = accessible.map(formatEntry).join('\n')
|
|
130
|
+
if (result.truncated) output += `\n\n[Results truncated at ${maxPerLookup} memories / ${effectiveLinkedLimit} linked expansions]`
|
|
131
|
+
return output
|
|
132
|
+
}
|
|
133
|
+
const results = filterScope(memDb.search(query || key))
|
|
134
|
+
if (!results.length) return 'No memories found.'
|
|
135
|
+
return results.slice(0, maxPerLookup).map(formatEntry).join('\n')
|
|
136
|
+
}
|
|
137
|
+
if (action === 'list') {
|
|
138
|
+
const results = filterScope(memDb.list(undefined, maxPerLookup))
|
|
139
|
+
if (!results.length) return 'No memories stored yet.'
|
|
140
|
+
return results.map(formatEntry).join('\n')
|
|
141
|
+
}
|
|
142
|
+
if (action === 'delete') {
|
|
143
|
+
const found = memDb.get(key)
|
|
144
|
+
if (!found) return `Memory not found: ${key}`
|
|
145
|
+
if (!canAccessMemory(found)) return 'Error: you do not have access to that memory.'
|
|
146
|
+
memDb.delete(key)
|
|
147
|
+
return `Deleted memory "${key}"`
|
|
148
|
+
}
|
|
149
|
+
if (action === 'link') {
|
|
150
|
+
if (!targetIds?.length) return 'Error: targetIds required for link action.'
|
|
151
|
+
const result = memDb.link(key, targetIds, true)
|
|
152
|
+
if (!result) return `Memory not found: ${key}`
|
|
153
|
+
return `Linked memory "${key}" to ${targetIds.length} memor${targetIds.length === 1 ? 'y' : 'ies'} (bidirectional): ${targetIds.join(', ')}`
|
|
154
|
+
}
|
|
155
|
+
if (action === 'unlink') {
|
|
156
|
+
if (!targetIds?.length) return 'Error: targetIds required for unlink action.'
|
|
157
|
+
const result = memDb.unlink(key, targetIds, true)
|
|
158
|
+
if (!result) return `Memory not found: ${key}`
|
|
159
|
+
return `Unlinked ${targetIds.length} memor${targetIds.length === 1 ? 'y' : 'ies'} from "${key}" (bidirectional)`
|
|
160
|
+
}
|
|
161
|
+
if (action === 'knowledge_store') {
|
|
162
|
+
const { addKnowledge } = await import('../memory-db')
|
|
163
|
+
if (!value) return 'Error: value (content) is required for knowledge_store'
|
|
164
|
+
const entry = addKnowledge({
|
|
165
|
+
title: key || 'Untitled',
|
|
166
|
+
content: value,
|
|
167
|
+
tags: tags,
|
|
168
|
+
createdByAgentId: ctx?.agentId || null,
|
|
169
|
+
createdBySessionId: ctx?.sessionId || null,
|
|
170
|
+
})
|
|
171
|
+
return `Knowledge stored: "${entry.title}" (id: ${entry.id})`
|
|
172
|
+
}
|
|
173
|
+
if (action === 'knowledge_search') {
|
|
174
|
+
const { searchKnowledge } = await import('../memory-db')
|
|
175
|
+
const results = searchKnowledge(query || key || '', tags, 10)
|
|
176
|
+
if (!results.length) return 'No knowledge entries found.'
|
|
177
|
+
return results.map(r => `[${r.id}] ${r.title}: ${r.content.slice(0, 200)}`).join('\n---\n')
|
|
178
|
+
}
|
|
179
|
+
return `Unknown action "${action}". Use: store, get, search, list, delete, link, unlink, knowledge_store, or knowledge_search.`
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
return `Error: ${err.message}`
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'memory_tool',
|
|
186
|
+
description: 'Store and retrieve long-term memories that persist across sessions. Memories can be shared or agent-scoped. Supports file references, image attachments, and linking memories together with depth traversal. Also supports a cross-agent knowledge base via "knowledge_store" and "knowledge_search". Use "store", "get", "search", "list", "delete", "link", "unlink", "knowledge_store", or "knowledge_search".',
|
|
187
|
+
schema: z.object({
|
|
188
|
+
action: z.enum(['store', 'get', 'search', 'list', 'delete', 'link', 'unlink', 'knowledge_store', 'knowledge_search']).describe('The action to perform'),
|
|
189
|
+
key: z.string().describe('For store: memory title. For get/delete/link/unlink: memory ID. For search: optional query fallback.'),
|
|
190
|
+
value: z.string().optional().describe('The memory content (for store action)'),
|
|
191
|
+
category: z.string().optional().describe('Category like "note", "fact", "preference", "project", "identity" (for store action, defaults to "note")'),
|
|
192
|
+
query: z.string().optional().describe('Search query (alternative to key for search action)'),
|
|
193
|
+
scope: z.enum(['auto', 'shared', 'agent']).optional().describe('Scope hint: auto (shared + own), shared, or agent'),
|
|
194
|
+
filePaths: z.array(z.object({
|
|
195
|
+
path: z.string().describe('File or folder path'),
|
|
196
|
+
contextSnippet: z.string().optional().describe('Brief context about this file reference'),
|
|
197
|
+
kind: z.enum(['file', 'folder', 'project']).optional().describe('Reference type for legacy filePaths compatibility'),
|
|
198
|
+
projectRoot: z.string().optional().describe('Optional project root path'),
|
|
199
|
+
projectName: z.string().optional().describe('Optional project display name'),
|
|
200
|
+
exists: z.boolean().optional().describe('Optional known existence state'),
|
|
201
|
+
timestamp: z.number().describe('When this file was referenced'),
|
|
202
|
+
})).optional().describe('File/folder references to attach to the memory (for store action)'),
|
|
203
|
+
references: z.array(z.object({
|
|
204
|
+
type: z.enum(['project', 'folder', 'file', 'task', 'session', 'url']),
|
|
205
|
+
path: z.string().optional(),
|
|
206
|
+
projectRoot: z.string().optional(),
|
|
207
|
+
projectName: z.string().optional(),
|
|
208
|
+
title: z.string().optional(),
|
|
209
|
+
note: z.string().optional(),
|
|
210
|
+
timestamp: z.number().optional(),
|
|
211
|
+
})).optional().describe('Structured references attached to the memory (preferred over filePaths).'),
|
|
212
|
+
project: z.object({
|
|
213
|
+
rootPath: z.string().describe('Project/workspace root path'),
|
|
214
|
+
name: z.string().optional().describe('Optional project display name'),
|
|
215
|
+
note: z.string().optional().describe('Optional note about the project context'),
|
|
216
|
+
}).optional().describe('Shortcut to add a project reference on store action.'),
|
|
217
|
+
imagePath: z.string().optional().describe('Path to an image file to attach (will be compressed and stored). For store action.'),
|
|
218
|
+
linkedMemoryIds: z.array(z.string()).optional().describe('IDs of other memories to link to (for store action)'),
|
|
219
|
+
depth: z.number().optional().describe('How deep to traverse linked memories (for get/search). Respects configured maxDepth limit. Default: 0 (no traversal).'),
|
|
220
|
+
linkedLimit: z.number().optional().describe('Max linked memories expanded during traversal. Respects configured server cap.'),
|
|
221
|
+
targetIds: z.array(z.string()).optional().describe('Memory IDs to link/unlink (for link/unlink actions)'),
|
|
222
|
+
tags: z.array(z.string()).optional().describe('Tags for categorizing knowledge entries'),
|
|
223
|
+
}),
|
|
224
|
+
},
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return tools
|
|
230
|
+
}
|