@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,673 @@
|
|
|
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 crypto from 'crypto'
|
|
6
|
+
import { spawnSync } from 'child_process'
|
|
7
|
+
import * as cheerio from 'cheerio'
|
|
8
|
+
import {
|
|
9
|
+
loadAgents, saveAgents,
|
|
10
|
+
loadTasks, saveTasks,
|
|
11
|
+
loadSchedules, saveSchedules,
|
|
12
|
+
loadSkills, saveSkills,
|
|
13
|
+
loadConnectors, saveConnectors,
|
|
14
|
+
loadDocuments, saveDocuments,
|
|
15
|
+
loadWebhooks, saveWebhooks,
|
|
16
|
+
loadSecrets, saveSecrets,
|
|
17
|
+
loadSessions, saveSessions,
|
|
18
|
+
encryptKey,
|
|
19
|
+
decryptKey,
|
|
20
|
+
} from '../storage'
|
|
21
|
+
import { resolveScheduleName } from '@/lib/schedule-name'
|
|
22
|
+
import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
|
|
23
|
+
import type { ToolBuildContext } from './context'
|
|
24
|
+
import { safePath, findBinaryOnPath } from './context'
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Document helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const MAX_DOCUMENT_TEXT_CHARS = 500_000
|
|
31
|
+
|
|
32
|
+
function extractDocumentText(filePath: string): { text: string; method: string } {
|
|
33
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
34
|
+
|
|
35
|
+
const readUtf8Text = (): string => {
|
|
36
|
+
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
37
|
+
const cleaned = raw.replace(/\u0000/g, '')
|
|
38
|
+
return cleaned
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (ext === '.pdf') {
|
|
42
|
+
const pdftotextBinary = findBinaryOnPath('pdftotext')
|
|
43
|
+
if (!pdftotextBinary) throw new Error('pdftotext is not installed. Install poppler to index PDF files.')
|
|
44
|
+
const out = spawnSync(pdftotextBinary, ['-layout', '-nopgbrk', '-q', filePath, '-'], {
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
maxBuffer: 25 * 1024 * 1024,
|
|
47
|
+
timeout: 20_000,
|
|
48
|
+
})
|
|
49
|
+
if ((out.status ?? 1) !== 0) {
|
|
50
|
+
throw new Error(`pdftotext failed: ${(out.stderr || out.stdout || '').trim() || 'unknown error'}`)
|
|
51
|
+
}
|
|
52
|
+
return { text: out.stdout || '', method: 'pdftotext' }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (['.txt', '.md', '.markdown', '.json', '.csv', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.yaml', '.yml'].includes(ext)) {
|
|
56
|
+
return { text: readUtf8Text(), method: 'utf8' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (ext === '.html' || ext === '.htm') {
|
|
60
|
+
const html = fs.readFileSync(filePath, 'utf-8')
|
|
61
|
+
const $ = cheerio.load(html)
|
|
62
|
+
const text = $('body').text() || $.text()
|
|
63
|
+
return { text, method: 'html-strip' }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (['.doc', '.docx', '.rtf'].includes(ext)) {
|
|
67
|
+
const out = spawnSync('/usr/bin/textutil', ['-convert', 'txt', '-stdout', filePath], {
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
maxBuffer: 25 * 1024 * 1024,
|
|
70
|
+
timeout: 20_000,
|
|
71
|
+
})
|
|
72
|
+
if ((out.status ?? 1) === 0 && out.stdout?.trim()) {
|
|
73
|
+
return { text: out.stdout, method: 'textutil' }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fallback = readUtf8Text()
|
|
78
|
+
if (fallback.trim()) return { text: fallback, method: 'utf8-fallback' }
|
|
79
|
+
throw new Error(`Unsupported document type: ${ext || '(no extension)'}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function trimDocumentContent(text: string): string {
|
|
83
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\u0000/g, '').trim()
|
|
84
|
+
if (normalized.length <= MAX_DOCUMENT_TEXT_CHARS) return normalized
|
|
85
|
+
return normalized.slice(0, MAX_DOCUMENT_TEXT_CHARS)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// RESOURCE_DEFAULTS
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
|
|
93
|
+
manage_agents: (p) => ({
|
|
94
|
+
name: p.name || 'Unnamed Agent',
|
|
95
|
+
description: p.description || '',
|
|
96
|
+
systemPrompt: p.systemPrompt || '',
|
|
97
|
+
soul: p.soul || '',
|
|
98
|
+
provider: p.provider || 'claude-cli',
|
|
99
|
+
model: p.model || '',
|
|
100
|
+
isOrchestrator: p.isOrchestrator || false,
|
|
101
|
+
tools: p.tools || [],
|
|
102
|
+
skills: p.skills || [],
|
|
103
|
+
skillIds: p.skillIds || [],
|
|
104
|
+
subAgentIds: p.subAgentIds || [],
|
|
105
|
+
...p,
|
|
106
|
+
}),
|
|
107
|
+
manage_tasks: (p) => ({
|
|
108
|
+
title: p.title || 'Untitled Task',
|
|
109
|
+
description: p.description || '',
|
|
110
|
+
status: p.status || 'backlog',
|
|
111
|
+
agentId: p.agentId || null,
|
|
112
|
+
sessionId: p.sessionId || null,
|
|
113
|
+
result: null,
|
|
114
|
+
error: null,
|
|
115
|
+
queuedAt: null,
|
|
116
|
+
startedAt: null,
|
|
117
|
+
completedAt: null,
|
|
118
|
+
...p,
|
|
119
|
+
}),
|
|
120
|
+
manage_schedules: (p) => {
|
|
121
|
+
const now = Date.now()
|
|
122
|
+
const base = {
|
|
123
|
+
name: resolveScheduleName({ name: p.name, taskPrompt: p.taskPrompt }),
|
|
124
|
+
agentId: p.agentId || null,
|
|
125
|
+
taskPrompt: p.taskPrompt || '',
|
|
126
|
+
scheduleType: p.scheduleType || 'interval',
|
|
127
|
+
status: p.status || 'active',
|
|
128
|
+
...p,
|
|
129
|
+
}
|
|
130
|
+
if (!base.nextRunAt) {
|
|
131
|
+
if (base.scheduleType === 'once' && base.runAt) base.nextRunAt = base.runAt
|
|
132
|
+
else if (base.scheduleType === 'interval' && base.intervalMs) base.nextRunAt = now + base.intervalMs
|
|
133
|
+
}
|
|
134
|
+
return base
|
|
135
|
+
},
|
|
136
|
+
manage_skills: (p) => ({
|
|
137
|
+
name: p.name || 'Unnamed Skill',
|
|
138
|
+
description: p.description || '',
|
|
139
|
+
content: p.content || '',
|
|
140
|
+
filename: p.filename || '',
|
|
141
|
+
...p,
|
|
142
|
+
}),
|
|
143
|
+
manage_connectors: (p) => ({
|
|
144
|
+
name: p.name || 'Unnamed Connector',
|
|
145
|
+
platform: p.platform || 'discord',
|
|
146
|
+
agentId: p.agentId || null,
|
|
147
|
+
enabled: p.enabled ?? false,
|
|
148
|
+
...p,
|
|
149
|
+
}),
|
|
150
|
+
manage_webhooks: (p) => ({
|
|
151
|
+
name: p.name || 'Unnamed Webhook',
|
|
152
|
+
source: p.source || 'custom',
|
|
153
|
+
events: Array.isArray(p.events) ? p.events : [],
|
|
154
|
+
agentId: p.agentId || null,
|
|
155
|
+
secret: p.secret || '',
|
|
156
|
+
isEnabled: p.isEnabled ?? true,
|
|
157
|
+
...p,
|
|
158
|
+
}),
|
|
159
|
+
manage_secrets: (p) => ({
|
|
160
|
+
name: p.name || 'Unnamed Secret',
|
|
161
|
+
service: p.service || 'custom',
|
|
162
|
+
scope: p.scope || 'global',
|
|
163
|
+
agentIds: Array.isArray(p.agentIds) ? p.agentIds : [],
|
|
164
|
+
...p,
|
|
165
|
+
}),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// PLATFORM_RESOURCES
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
const PLATFORM_RESOURCES: Record<string, {
|
|
173
|
+
toolId: string
|
|
174
|
+
label: string
|
|
175
|
+
load: () => Record<string, any>
|
|
176
|
+
save: (d: Record<string, any>) => void
|
|
177
|
+
readOnly?: boolean
|
|
178
|
+
}> = {
|
|
179
|
+
manage_agents: { toolId: 'manage_agents', label: 'agents', load: loadAgents, save: saveAgents },
|
|
180
|
+
manage_tasks: { toolId: 'manage_tasks', label: 'tasks', load: loadTasks, save: saveTasks },
|
|
181
|
+
manage_schedules: { toolId: 'manage_schedules', label: 'schedules', load: loadSchedules, save: saveSchedules },
|
|
182
|
+
manage_skills: { toolId: 'manage_skills', label: 'skills', load: loadSkills, save: saveSkills },
|
|
183
|
+
manage_connectors: { toolId: 'manage_connectors', label: 'connectors', load: loadConnectors, save: saveConnectors },
|
|
184
|
+
manage_webhooks: { toolId: 'manage_webhooks', label: 'webhooks', load: loadWebhooks, save: saveWebhooks },
|
|
185
|
+
manage_sessions: { toolId: 'manage_sessions', label: 'sessions', load: loadSessions, save: saveSessions, readOnly: true },
|
|
186
|
+
manage_secrets: { toolId: 'manage_secrets', label: 'secrets', load: loadSecrets, save: saveSecrets },
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// buildCrudTools
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
194
|
+
const tools: StructuredToolInterface[] = []
|
|
195
|
+
const { cwd, ctx, hasTool } = bctx
|
|
196
|
+
|
|
197
|
+
// Build dynamic agent summary for tools that need agent awareness
|
|
198
|
+
const assignScope = ctx?.platformAssignScope || 'self'
|
|
199
|
+
let agentSummary = ''
|
|
200
|
+
if (hasTool('manage_tasks') || hasTool('manage_schedules')) {
|
|
201
|
+
if (assignScope === 'all') {
|
|
202
|
+
try {
|
|
203
|
+
const agents = loadAgents()
|
|
204
|
+
const agentList = Object.values(agents)
|
|
205
|
+
.map((a: any) => ` - "${a.id}": ${a.name}${a.description ? ` — ${a.description}` : ''}`)
|
|
206
|
+
.join('\n')
|
|
207
|
+
if (agentList) agentSummary = `\n\nAvailable agents:\n${agentList}`
|
|
208
|
+
} catch { /* ignore */ }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const [toolKey, res] of Object.entries(PLATFORM_RESOURCES)) {
|
|
213
|
+
if (!hasTool(toolKey)) continue
|
|
214
|
+
|
|
215
|
+
let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
|
|
216
|
+
if (toolKey === 'manage_tasks') {
|
|
217
|
+
if (assignScope === 'self') {
|
|
218
|
+
description += `\n\nSet "agentId" to assign a task to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign tasks to yourself. Valid statuses: backlog, queued, running, completed, failed.`
|
|
219
|
+
} else {
|
|
220
|
+
description += `\n\nSet "agentId" to assign a task to an agent (including yourself: "${ctx?.agentId || 'unknown'}"). Valid statuses: backlog, queued, running, completed, failed.` + agentSummary
|
|
221
|
+
}
|
|
222
|
+
} else if (toolKey === 'manage_agents') {
|
|
223
|
+
description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field.`
|
|
224
|
+
} else if (toolKey === 'manage_schedules') {
|
|
225
|
+
if (assignScope === 'self') {
|
|
226
|
+
description += `\n\nSet "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Set taskPrompt for what the agent should do. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
|
|
227
|
+
} else {
|
|
228
|
+
description += `\n\nSet "agentId" to assign a schedule to an agent (including yourself: "${ctx?.agentId || 'unknown'}"). Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Set taskPrompt for what the agent should do. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).` + agentSummary
|
|
229
|
+
}
|
|
230
|
+
} else if (toolKey === 'manage_webhooks') {
|
|
231
|
+
description += '\n\nUse `source`, `events`, `agentId`, and `secret` when creating webhooks. Inbound calls should POST to `/api/webhooks/{id}` with header `x-webhook-secret` when a secret is configured.'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
tools.push(
|
|
235
|
+
tool(
|
|
236
|
+
async ({ action, id, data }) => {
|
|
237
|
+
const canAccessSecret = (secret: any): boolean => {
|
|
238
|
+
if (!secret) return false
|
|
239
|
+
if (secret.scope !== 'agent') return true
|
|
240
|
+
if (!ctx?.agentId) return false
|
|
241
|
+
return Array.isArray(secret.agentIds) && secret.agentIds.includes(ctx.agentId)
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
if (action === 'list') {
|
|
245
|
+
if (toolKey === 'manage_secrets') {
|
|
246
|
+
const values = Object.values(res.load())
|
|
247
|
+
.filter((s: any) => canAccessSecret(s))
|
|
248
|
+
.map((s: any) => ({
|
|
249
|
+
id: s.id,
|
|
250
|
+
name: s.name,
|
|
251
|
+
service: s.service,
|
|
252
|
+
scope: s.scope || 'global',
|
|
253
|
+
agentIds: s.agentIds || [],
|
|
254
|
+
createdAt: s.createdAt,
|
|
255
|
+
updatedAt: s.updatedAt,
|
|
256
|
+
}))
|
|
257
|
+
return JSON.stringify(values)
|
|
258
|
+
}
|
|
259
|
+
return JSON.stringify(Object.values(res.load()))
|
|
260
|
+
}
|
|
261
|
+
if (action === 'get') {
|
|
262
|
+
if (!id) return 'Error: "id" is required for get action.'
|
|
263
|
+
const all = res.load()
|
|
264
|
+
if (!all[id]) return `Not found: ${res.label} "${id}"`
|
|
265
|
+
if (toolKey === 'manage_secrets') {
|
|
266
|
+
if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
|
|
267
|
+
let value = ''
|
|
268
|
+
try {
|
|
269
|
+
value = all[id].encryptedValue ? decryptKey(all[id].encryptedValue) : ''
|
|
270
|
+
} catch {
|
|
271
|
+
value = ''
|
|
272
|
+
}
|
|
273
|
+
return JSON.stringify({
|
|
274
|
+
id: all[id].id,
|
|
275
|
+
name: all[id].name,
|
|
276
|
+
service: all[id].service,
|
|
277
|
+
scope: all[id].scope || 'global',
|
|
278
|
+
agentIds: all[id].agentIds || [],
|
|
279
|
+
value,
|
|
280
|
+
createdAt: all[id].createdAt,
|
|
281
|
+
updatedAt: all[id].updatedAt,
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
return JSON.stringify(all[id])
|
|
285
|
+
}
|
|
286
|
+
if (res.readOnly) return `Cannot ${action} ${res.label} via this tool (read-only).`
|
|
287
|
+
if (action === 'create') {
|
|
288
|
+
const all = res.load()
|
|
289
|
+
const raw = data ? JSON.parse(data) : {}
|
|
290
|
+
const defaults = RESOURCE_DEFAULTS[toolKey]
|
|
291
|
+
const parsed = defaults ? defaults(raw) : raw
|
|
292
|
+
if (parsed && typeof parsed === 'object' && 'id' in parsed) {
|
|
293
|
+
delete (parsed as Record<string, unknown>).id
|
|
294
|
+
}
|
|
295
|
+
// Enforce assignment scope for tasks and schedules
|
|
296
|
+
if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
|
|
297
|
+
if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
|
|
298
|
+
return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const now = Date.now()
|
|
302
|
+
if (toolKey === 'manage_schedules') {
|
|
303
|
+
const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
|
|
304
|
+
agentId: parsed.agentId || null,
|
|
305
|
+
taskPrompt: parsed.taskPrompt || '',
|
|
306
|
+
scheduleType: parsed.scheduleType || 'interval',
|
|
307
|
+
cron: parsed.cron,
|
|
308
|
+
intervalMs: parsed.intervalMs,
|
|
309
|
+
runAt: parsed.runAt,
|
|
310
|
+
createdByAgentId: ctx?.agentId || null,
|
|
311
|
+
createdInSessionId: ctx?.sessionId || null,
|
|
312
|
+
}, {
|
|
313
|
+
creatorScope: {
|
|
314
|
+
agentId: ctx?.agentId || null,
|
|
315
|
+
sessionId: ctx?.sessionId || null,
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
if (duplicate) {
|
|
319
|
+
let changed = false
|
|
320
|
+
const duplicateId = typeof duplicate.id === 'string' ? duplicate.id : ''
|
|
321
|
+
const nextName = resolveScheduleName({
|
|
322
|
+
name: parsed.name ?? duplicate.name,
|
|
323
|
+
taskPrompt: parsed.taskPrompt ?? duplicate.taskPrompt,
|
|
324
|
+
})
|
|
325
|
+
if (nextName && nextName !== duplicate.name) {
|
|
326
|
+
duplicate.name = nextName
|
|
327
|
+
changed = true
|
|
328
|
+
}
|
|
329
|
+
const normalizedStatus = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
|
|
330
|
+
if ((normalizedStatus === 'active' || normalizedStatus === 'paused') && duplicate.status !== normalizedStatus) {
|
|
331
|
+
duplicate.status = normalizedStatus
|
|
332
|
+
changed = true
|
|
333
|
+
}
|
|
334
|
+
if (changed) {
|
|
335
|
+
duplicate.updatedAt = now
|
|
336
|
+
if (duplicateId) all[duplicateId] = duplicate
|
|
337
|
+
res.save(all)
|
|
338
|
+
}
|
|
339
|
+
return JSON.stringify({
|
|
340
|
+
...duplicate,
|
|
341
|
+
deduplicated: true,
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const newId = crypto.randomBytes(4).toString('hex')
|
|
346
|
+
const entry = {
|
|
347
|
+
id: newId,
|
|
348
|
+
...parsed,
|
|
349
|
+
createdByAgentId: ctx?.agentId || null,
|
|
350
|
+
createdInSessionId: ctx?.sessionId || null,
|
|
351
|
+
createdAt: now,
|
|
352
|
+
updatedAt: now,
|
|
353
|
+
}
|
|
354
|
+
let responseEntry: any = entry
|
|
355
|
+
if (toolKey === 'manage_secrets') {
|
|
356
|
+
const secretValue = typeof parsed.value === 'string' ? parsed.value : null
|
|
357
|
+
if (!secretValue) return 'Error: data.value is required to create a secret.'
|
|
358
|
+
const normalizedScope = parsed.scope === 'agent' ? 'agent' : 'global'
|
|
359
|
+
const normalizedAgentIds = normalizedScope === 'agent'
|
|
360
|
+
? Array.from(new Set([
|
|
361
|
+
...(Array.isArray(parsed.agentIds) ? parsed.agentIds.filter((x: any) => typeof x === 'string') : []),
|
|
362
|
+
...(ctx?.agentId ? [ctx.agentId] : []),
|
|
363
|
+
]))
|
|
364
|
+
: []
|
|
365
|
+
const stored = {
|
|
366
|
+
...entry,
|
|
367
|
+
scope: normalizedScope,
|
|
368
|
+
agentIds: normalizedAgentIds,
|
|
369
|
+
encryptedValue: encryptKey(secretValue),
|
|
370
|
+
}
|
|
371
|
+
delete (stored as any).value
|
|
372
|
+
all[newId] = stored
|
|
373
|
+
const { encryptedValue, ...safe } = stored
|
|
374
|
+
responseEntry = safe
|
|
375
|
+
} else {
|
|
376
|
+
all[newId] = entry
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (toolKey === 'manage_tasks' && entry.status === 'completed') {
|
|
380
|
+
const { formatValidationFailure, validateTaskCompletion } = await import('../task-validation')
|
|
381
|
+
const { ensureTaskCompletionReport } = await import('../task-reports')
|
|
382
|
+
const report = ensureTaskCompletionReport(entry as any)
|
|
383
|
+
if (report?.relativePath) (entry as any).completionReportPath = report.relativePath
|
|
384
|
+
const validation = validateTaskCompletion(entry as any, { report })
|
|
385
|
+
;(entry as any).validation = validation
|
|
386
|
+
if (!validation.ok) {
|
|
387
|
+
entry.status = 'failed'
|
|
388
|
+
;(entry as any).completedAt = null
|
|
389
|
+
;(entry as any).error = formatValidationFailure(validation.reasons).slice(0, 500)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
res.save(all)
|
|
394
|
+
if (toolKey === 'manage_tasks' && entry.status === 'queued') {
|
|
395
|
+
const { enqueueTask } = await import('../queue')
|
|
396
|
+
enqueueTask(newId)
|
|
397
|
+
} else if (
|
|
398
|
+
toolKey === 'manage_tasks'
|
|
399
|
+
&& (entry.status === 'completed' || entry.status === 'failed')
|
|
400
|
+
&& entry.sessionId
|
|
401
|
+
) {
|
|
402
|
+
const { disableSessionHeartbeat } = await import('../queue')
|
|
403
|
+
disableSessionHeartbeat(entry.sessionId)
|
|
404
|
+
}
|
|
405
|
+
return JSON.stringify(responseEntry)
|
|
406
|
+
}
|
|
407
|
+
if (action === 'update') {
|
|
408
|
+
if (!id) return 'Error: "id" is required for update action.'
|
|
409
|
+
const all = res.load()
|
|
410
|
+
if (!all[id]) return `Not found: ${res.label} "${id}"`
|
|
411
|
+
const parsed = data ? JSON.parse(data) : {}
|
|
412
|
+
const prevStatus = all[id]?.status
|
|
413
|
+
// Enforce assignment scope for tasks and schedules
|
|
414
|
+
if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
|
|
415
|
+
if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
|
|
416
|
+
return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
|
|
420
|
+
if (toolKey === 'manage_secrets') {
|
|
421
|
+
if (!canAccessSecret(all[id])) return 'Error: you do not have access to this secret.'
|
|
422
|
+
const nextScope = parsed.scope === 'agent'
|
|
423
|
+
? 'agent'
|
|
424
|
+
: parsed.scope === 'global'
|
|
425
|
+
? 'global'
|
|
426
|
+
: (all[id].scope === 'agent' ? 'agent' : 'global')
|
|
427
|
+
if (nextScope === 'agent') {
|
|
428
|
+
const incomingIds = Array.isArray(parsed.agentIds)
|
|
429
|
+
? parsed.agentIds.filter((x: any) => typeof x === 'string')
|
|
430
|
+
: Array.isArray(all[id].agentIds)
|
|
431
|
+
? all[id].agentIds
|
|
432
|
+
: []
|
|
433
|
+
all[id].agentIds = Array.from(new Set([
|
|
434
|
+
...incomingIds,
|
|
435
|
+
...(ctx?.agentId ? [ctx.agentId] : []),
|
|
436
|
+
]))
|
|
437
|
+
} else {
|
|
438
|
+
all[id].agentIds = []
|
|
439
|
+
}
|
|
440
|
+
all[id].scope = nextScope
|
|
441
|
+
if (typeof parsed.value === 'string' && parsed.value.trim()) {
|
|
442
|
+
all[id].encryptedValue = encryptKey(parsed.value)
|
|
443
|
+
}
|
|
444
|
+
delete all[id].value
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (toolKey === 'manage_tasks' && all[id].status === 'completed') {
|
|
448
|
+
const { formatValidationFailure, validateTaskCompletion } = await import('../task-validation')
|
|
449
|
+
const { ensureTaskCompletionReport } = await import('../task-reports')
|
|
450
|
+
const report = ensureTaskCompletionReport(all[id] as any)
|
|
451
|
+
if (report?.relativePath) (all[id] as any).completionReportPath = report.relativePath
|
|
452
|
+
const validation = validateTaskCompletion(all[id] as any, { report })
|
|
453
|
+
;(all[id] as any).validation = validation
|
|
454
|
+
if (!validation.ok) {
|
|
455
|
+
all[id].status = 'failed'
|
|
456
|
+
;(all[id] as any).completedAt = null
|
|
457
|
+
;(all[id] as any).error = formatValidationFailure(validation.reasons).slice(0, 500)
|
|
458
|
+
} else if ((all[id] as any).completedAt == null) {
|
|
459
|
+
;(all[id] as any).completedAt = Date.now()
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
res.save(all)
|
|
464
|
+
if (toolKey === 'manage_tasks' && prevStatus !== 'queued' && all[id].status === 'queued') {
|
|
465
|
+
const { enqueueTask } = await import('../queue')
|
|
466
|
+
enqueueTask(id)
|
|
467
|
+
} else if (
|
|
468
|
+
toolKey === 'manage_tasks'
|
|
469
|
+
&& prevStatus !== all[id].status
|
|
470
|
+
&& (all[id].status === 'completed' || all[id].status === 'failed')
|
|
471
|
+
&& all[id].sessionId
|
|
472
|
+
) {
|
|
473
|
+
const { disableSessionHeartbeat } = await import('../queue')
|
|
474
|
+
disableSessionHeartbeat(all[id].sessionId)
|
|
475
|
+
}
|
|
476
|
+
if (toolKey === 'manage_secrets') {
|
|
477
|
+
const { encryptedValue, ...safe } = all[id]
|
|
478
|
+
return JSON.stringify(safe)
|
|
479
|
+
}
|
|
480
|
+
return JSON.stringify(all[id])
|
|
481
|
+
}
|
|
482
|
+
if (action === 'delete') {
|
|
483
|
+
if (!id) return 'Error: "id" is required for delete action.'
|
|
484
|
+
const all = res.load()
|
|
485
|
+
if (!all[id]) return `Not found: ${res.label} "${id}"`
|
|
486
|
+
if (toolKey === 'manage_secrets' && !canAccessSecret(all[id])) {
|
|
487
|
+
return 'Error: you do not have access to this secret.'
|
|
488
|
+
}
|
|
489
|
+
delete all[id]
|
|
490
|
+
res.save(all)
|
|
491
|
+
return JSON.stringify({ deleted: id })
|
|
492
|
+
}
|
|
493
|
+
return `Unknown action "${action}". Valid: list, get, create, update, delete`
|
|
494
|
+
} catch (err: any) {
|
|
495
|
+
return `Error: ${err.message}`
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: toolKey,
|
|
500
|
+
description,
|
|
501
|
+
schema: z.object({
|
|
502
|
+
action: z.enum(['list', 'get', 'create', 'update', 'delete']).describe('The CRUD action to perform'),
|
|
503
|
+
id: z.string().optional().describe('Resource ID (required for get, update, delete)'),
|
|
504
|
+
data: z.string().optional().describe('JSON string of fields for create/update'),
|
|
505
|
+
}),
|
|
506
|
+
},
|
|
507
|
+
),
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (hasTool('manage_documents')) {
|
|
512
|
+
tools.push(
|
|
513
|
+
tool(
|
|
514
|
+
async ({ action, id, filePath, query, limit, metadata, title }) => {
|
|
515
|
+
try {
|
|
516
|
+
const documents = loadDocuments()
|
|
517
|
+
|
|
518
|
+
if (action === 'list') {
|
|
519
|
+
const rows = Object.values(documents)
|
|
520
|
+
.sort((a: any, b: any) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
521
|
+
.slice(0, Math.max(1, Math.min(limit || 100, 500)))
|
|
522
|
+
.map((doc: any) => ({
|
|
523
|
+
id: doc.id,
|
|
524
|
+
title: doc.title,
|
|
525
|
+
fileName: doc.fileName,
|
|
526
|
+
sourcePath: doc.sourcePath,
|
|
527
|
+
textLength: doc.textLength,
|
|
528
|
+
method: doc.method,
|
|
529
|
+
metadata: doc.metadata || {},
|
|
530
|
+
createdAt: doc.createdAt,
|
|
531
|
+
updatedAt: doc.updatedAt,
|
|
532
|
+
}))
|
|
533
|
+
return JSON.stringify(rows)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (action === 'get') {
|
|
537
|
+
if (!id) return 'Error: id is required for get.'
|
|
538
|
+
const doc = documents[id]
|
|
539
|
+
if (!doc) return `Not found: document "${id}"`
|
|
540
|
+
const maxContentChars = 60_000
|
|
541
|
+
return JSON.stringify({
|
|
542
|
+
...doc,
|
|
543
|
+
content: typeof doc.content === 'string' && doc.content.length > maxContentChars
|
|
544
|
+
? `${doc.content.slice(0, maxContentChars)}\n... [truncated]`
|
|
545
|
+
: (doc.content || ''),
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (action === 'delete') {
|
|
550
|
+
if (!id) return 'Error: id is required for delete.'
|
|
551
|
+
if (!documents[id]) return `Not found: document "${id}"`
|
|
552
|
+
delete documents[id]
|
|
553
|
+
saveDocuments(documents)
|
|
554
|
+
return JSON.stringify({ ok: true, id })
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (action === 'upload') {
|
|
558
|
+
if (!filePath?.trim()) return 'Error: filePath is required for upload.'
|
|
559
|
+
const sourcePath = path.isAbsolute(filePath) ? filePath : safePath(cwd, filePath)
|
|
560
|
+
if (!fs.existsSync(sourcePath)) return `Error: file not found: ${filePath}`
|
|
561
|
+
const stat = fs.statSync(sourcePath)
|
|
562
|
+
if (!stat.isFile()) return 'Error: upload expects a file path.'
|
|
563
|
+
|
|
564
|
+
const extracted = extractDocumentText(sourcePath)
|
|
565
|
+
const content = trimDocumentContent(extracted.text)
|
|
566
|
+
if (!content) return 'Error: extracted document text is empty.'
|
|
567
|
+
|
|
568
|
+
const docId = crypto.randomBytes(6).toString('hex')
|
|
569
|
+
const now = Date.now()
|
|
570
|
+
const parsedMetadata = metadata && typeof metadata === 'string'
|
|
571
|
+
? (() => {
|
|
572
|
+
try {
|
|
573
|
+
const m = JSON.parse(metadata)
|
|
574
|
+
return (m && typeof m === 'object' && !Array.isArray(m)) ? m : {}
|
|
575
|
+
} catch {
|
|
576
|
+
return {}
|
|
577
|
+
}
|
|
578
|
+
})()
|
|
579
|
+
: {}
|
|
580
|
+
|
|
581
|
+
const entry = {
|
|
582
|
+
id: docId,
|
|
583
|
+
title: title?.trim() || path.basename(sourcePath),
|
|
584
|
+
fileName: path.basename(sourcePath),
|
|
585
|
+
sourcePath,
|
|
586
|
+
method: extracted.method,
|
|
587
|
+
textLength: content.length,
|
|
588
|
+
content,
|
|
589
|
+
metadata: parsedMetadata,
|
|
590
|
+
uploadedByAgentId: ctx?.agentId || null,
|
|
591
|
+
uploadedInSessionId: ctx?.sessionId || null,
|
|
592
|
+
createdAt: now,
|
|
593
|
+
updatedAt: now,
|
|
594
|
+
}
|
|
595
|
+
documents[docId] = entry
|
|
596
|
+
saveDocuments(documents)
|
|
597
|
+
return JSON.stringify({
|
|
598
|
+
id: entry.id,
|
|
599
|
+
title: entry.title,
|
|
600
|
+
fileName: entry.fileName,
|
|
601
|
+
textLength: entry.textLength,
|
|
602
|
+
method: entry.method,
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (action === 'search') {
|
|
607
|
+
const q = (query || '').trim().toLowerCase()
|
|
608
|
+
if (!q) return 'Error: query is required for search.'
|
|
609
|
+
const terms = q.split(/\s+/).filter(Boolean)
|
|
610
|
+
const max = Math.max(1, Math.min(limit || 5, 50))
|
|
611
|
+
|
|
612
|
+
const matches = Object.values(documents)
|
|
613
|
+
.map((doc: any) => {
|
|
614
|
+
const hay = (doc.content || '').toLowerCase()
|
|
615
|
+
if (!hay) return null
|
|
616
|
+
if (!terms.every((term) => hay.includes(term))) return null
|
|
617
|
+
let score = hay.includes(q) ? 10 : 0
|
|
618
|
+
for (const term of terms) {
|
|
619
|
+
let pos = hay.indexOf(term)
|
|
620
|
+
while (pos !== -1) {
|
|
621
|
+
score += 1
|
|
622
|
+
pos = hay.indexOf(term, pos + term.length)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const firstTerm = terms[0] || q
|
|
626
|
+
const at = firstTerm ? hay.indexOf(firstTerm) : -1
|
|
627
|
+
const start = at >= 0 ? Math.max(0, at - 120) : 0
|
|
628
|
+
const end = Math.min((doc.content || '').length, start + 320)
|
|
629
|
+
const snippet = ((doc.content || '').slice(start, end) || '').replace(/\s+/g, ' ').trim()
|
|
630
|
+
return {
|
|
631
|
+
id: doc.id,
|
|
632
|
+
title: doc.title,
|
|
633
|
+
score,
|
|
634
|
+
snippet,
|
|
635
|
+
textLength: doc.textLength,
|
|
636
|
+
updatedAt: doc.updatedAt,
|
|
637
|
+
}
|
|
638
|
+
})
|
|
639
|
+
.filter(Boolean)
|
|
640
|
+
.sort((a: any, b: any) => b.score - a.score)
|
|
641
|
+
.slice(0, max)
|
|
642
|
+
|
|
643
|
+
return JSON.stringify({
|
|
644
|
+
query,
|
|
645
|
+
total: matches.length,
|
|
646
|
+
matches,
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return 'Unknown action. Use list, upload, search, get, or delete.'
|
|
651
|
+
} catch (err: any) {
|
|
652
|
+
return `Error: ${err.message || String(err)}`
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: 'manage_documents',
|
|
657
|
+
description: 'Upload and index documents, then search/get/delete them for long-term retrieval. Supports PDFs (via pdftotext) and common text/doc formats.',
|
|
658
|
+
schema: z.object({
|
|
659
|
+
action: z.enum(['list', 'upload', 'search', 'get', 'delete']).describe('Document action'),
|
|
660
|
+
id: z.string().optional().describe('Document id (for get/delete)'),
|
|
661
|
+
filePath: z.string().optional().describe('Path to document file for upload (relative to working directory or absolute)'),
|
|
662
|
+
title: z.string().optional().describe('Optional title override for upload'),
|
|
663
|
+
query: z.string().optional().describe('Search query text (for search)'),
|
|
664
|
+
limit: z.number().optional().describe('Max results (default 5 for search, 100 for list)'),
|
|
665
|
+
metadata: z.string().optional().describe('Optional JSON string metadata for upload'),
|
|
666
|
+
}),
|
|
667
|
+
},
|
|
668
|
+
),
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return tools
|
|
673
|
+
}
|