@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,422 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import crypto from 'crypto'
|
|
4
|
+
import { loadSessions, saveSessions, loadAgents } from '../storage'
|
|
5
|
+
import type { ToolBuildContext } from './context'
|
|
6
|
+
|
|
7
|
+
export function buildSessionInfoTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
8
|
+
const tools: StructuredToolInterface[] = []
|
|
9
|
+
const { cwd, ctx } = bctx
|
|
10
|
+
|
|
11
|
+
if (bctx.hasTool('manage_sessions')) {
|
|
12
|
+
tools.push(
|
|
13
|
+
tool(
|
|
14
|
+
async () => {
|
|
15
|
+
try {
|
|
16
|
+
const sessions = loadSessions()
|
|
17
|
+
const current = ctx?.sessionId ? sessions[ctx.sessionId] : null
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
sessionId: ctx?.sessionId || null,
|
|
20
|
+
sessionName: current?.name || null,
|
|
21
|
+
sessionType: current?.sessionType || null,
|
|
22
|
+
user: current?.user || null,
|
|
23
|
+
agentId: ctx?.agentId || current?.agentId || null,
|
|
24
|
+
parentSessionId: current?.parentSessionId || null,
|
|
25
|
+
heartbeatEnabled: typeof current?.heartbeatEnabled === 'boolean'
|
|
26
|
+
? current.heartbeatEnabled
|
|
27
|
+
: null,
|
|
28
|
+
})
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
return `Error: ${err.message || String(err)}`
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'whoami_tool',
|
|
35
|
+
description: 'Return identity/runtime context for this agent execution (current session id, agent id, session owner, and parent session).',
|
|
36
|
+
schema: z.object({}),
|
|
37
|
+
},
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
tools.push(
|
|
42
|
+
tool(
|
|
43
|
+
async ({ action, sessionId, message, limit, agentId, name, waitForReply, timeoutSec, queueMode, heartbeatEnabled, heartbeatIntervalSec, heartbeatIntervalMs, finalStatus, envelopeId, type, correlationId, ttlSec }) => {
|
|
44
|
+
try {
|
|
45
|
+
const sessions = loadSessions()
|
|
46
|
+
if (action === 'list') {
|
|
47
|
+
const { getSessionRunState } = await import('../session-run-manager')
|
|
48
|
+
const items = Object.values(sessions)
|
|
49
|
+
.sort((a: any, b: any) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0))
|
|
50
|
+
.slice(0, Math.max(1, Math.min(limit || 50, 200)))
|
|
51
|
+
.map((s: any) => {
|
|
52
|
+
const runState = getSessionRunState(s.id)
|
|
53
|
+
return {
|
|
54
|
+
id: s.id,
|
|
55
|
+
name: s.name,
|
|
56
|
+
sessionType: s.sessionType || 'human',
|
|
57
|
+
agentId: s.agentId || null,
|
|
58
|
+
provider: s.provider,
|
|
59
|
+
model: s.model,
|
|
60
|
+
parentSessionId: s.parentSessionId || null,
|
|
61
|
+
active: !!runState.runningRunId,
|
|
62
|
+
queuedCount: runState.queueLength,
|
|
63
|
+
heartbeatEnabled: s.heartbeatEnabled !== false,
|
|
64
|
+
lastActiveAt: s.lastActiveAt,
|
|
65
|
+
createdAt: s.createdAt,
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
return JSON.stringify(items)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (action === 'history') {
|
|
72
|
+
const targetSessionId = sessionId || ctx?.sessionId || null
|
|
73
|
+
if (!targetSessionId) return 'Error: sessionId is required for history when no current session context exists.'
|
|
74
|
+
const target = sessions[targetSessionId]
|
|
75
|
+
if (!target) return `Not found: session "${targetSessionId}"`
|
|
76
|
+
const max = Math.max(1, Math.min(limit || 20, 100))
|
|
77
|
+
const history = (target.messages || []).slice(-max).map((m: any) => ({
|
|
78
|
+
role: m.role,
|
|
79
|
+
text: m.text,
|
|
80
|
+
time: m.time,
|
|
81
|
+
kind: m.kind || 'chat',
|
|
82
|
+
}))
|
|
83
|
+
return JSON.stringify({ sessionId: target.id, name: target.name, history, currentSessionDefaulted: !sessionId })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (action === 'status') {
|
|
87
|
+
if (!sessionId) return 'Error: sessionId is required for status.'
|
|
88
|
+
const target = sessions[sessionId]
|
|
89
|
+
if (!target) return `Not found: session "${sessionId}"`
|
|
90
|
+
const { getSessionRunState } = await import('../session-run-manager')
|
|
91
|
+
const run = getSessionRunState(sessionId)
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
id: target.id,
|
|
94
|
+
name: target.name,
|
|
95
|
+
runningRunId: run.runningRunId || null,
|
|
96
|
+
queuedCount: run.queueLength,
|
|
97
|
+
heartbeatEnabled: target.heartbeatEnabled !== false,
|
|
98
|
+
lastActiveAt: target.lastActiveAt,
|
|
99
|
+
messageCount: (target.messages || []).length,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (action === 'stop') {
|
|
104
|
+
if (!sessionId) return 'Error: sessionId is required for stop.'
|
|
105
|
+
if (!sessions[sessionId]) return `Not found: session "${sessionId}"`
|
|
106
|
+
const { cancelSessionRuns } = await import('../session-run-manager')
|
|
107
|
+
const out = cancelSessionRuns(sessionId, 'Stopped by manage_sessions')
|
|
108
|
+
return JSON.stringify({ sessionId, ...out })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (action === 'send') {
|
|
112
|
+
if (!sessionId) return 'Error: sessionId is required for send.'
|
|
113
|
+
if (!message?.trim()) return 'Error: message is required for send.'
|
|
114
|
+
if (!sessions[sessionId]) return `Not found: session "${sessionId}"`
|
|
115
|
+
if (ctx?.sessionId && sessionId === ctx.sessionId) return 'Error: cannot send to the current session itself.'
|
|
116
|
+
|
|
117
|
+
const sourceSession = ctx?.sessionId ? sessions[ctx.sessionId] : null
|
|
118
|
+
const sourceLabel = sourceSession
|
|
119
|
+
? `${sourceSession.name} (${sourceSession.id})`
|
|
120
|
+
: (ctx?.agentId ? `agent:${ctx.agentId}` : 'platform')
|
|
121
|
+
const bridgedMessage = `[Session message from ${sourceLabel}]\n${message.trim()}`
|
|
122
|
+
|
|
123
|
+
const { enqueueSessionRun } = await import('../session-run-manager')
|
|
124
|
+
const mode = queueMode === 'steer' || queueMode === 'collect' || queueMode === 'followup'
|
|
125
|
+
? queueMode
|
|
126
|
+
: 'followup'
|
|
127
|
+
const run = enqueueSessionRun({
|
|
128
|
+
sessionId,
|
|
129
|
+
message: bridgedMessage,
|
|
130
|
+
source: 'session-send',
|
|
131
|
+
internal: false,
|
|
132
|
+
mode,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (waitForReply === false) {
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
sessionId,
|
|
138
|
+
runId: run.runId,
|
|
139
|
+
status: 'queued',
|
|
140
|
+
mode,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const timeoutMs = Math.max(5, Math.min(timeoutSec || 120, 900)) * 1000
|
|
145
|
+
const result = await Promise.race([
|
|
146
|
+
run.promise,
|
|
147
|
+
new Promise<never>((_, reject) =>
|
|
148
|
+
setTimeout(() => reject(new Error(`Timed out waiting for session reply after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs),
|
|
149
|
+
),
|
|
150
|
+
])
|
|
151
|
+
return JSON.stringify({
|
|
152
|
+
sessionId,
|
|
153
|
+
runId: run.runId,
|
|
154
|
+
status: result.error ? 'failed' : 'completed',
|
|
155
|
+
reply: result.text || '',
|
|
156
|
+
error: result.error || null,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (action === 'spawn') {
|
|
161
|
+
if (!agentId) return 'Error: agentId is required for spawn.'
|
|
162
|
+
const agents = loadAgents()
|
|
163
|
+
const agent = agents[agentId]
|
|
164
|
+
if (!agent) return `Not found: agent "${agentId}"`
|
|
165
|
+
const sourceSession = ctx?.sessionId ? sessions[ctx.sessionId] : null
|
|
166
|
+
const ownerUser = sourceSession?.user || 'system'
|
|
167
|
+
|
|
168
|
+
const id = crypto.randomBytes(4).toString('hex')
|
|
169
|
+
const now = Date.now()
|
|
170
|
+
const entry = {
|
|
171
|
+
id,
|
|
172
|
+
name: (name || `${agent.name} Session`).trim(),
|
|
173
|
+
cwd,
|
|
174
|
+
user: ownerUser,
|
|
175
|
+
provider: agent.provider || 'claude-cli',
|
|
176
|
+
model: agent.model || '',
|
|
177
|
+
credentialId: agent.credentialId || null,
|
|
178
|
+
apiEndpoint: agent.apiEndpoint || null,
|
|
179
|
+
claudeSessionId: null,
|
|
180
|
+
codexThreadId: null,
|
|
181
|
+
opencodeSessionId: null,
|
|
182
|
+
delegateResumeIds: {
|
|
183
|
+
claudeCode: null,
|
|
184
|
+
codex: null,
|
|
185
|
+
opencode: null,
|
|
186
|
+
},
|
|
187
|
+
messages: [],
|
|
188
|
+
createdAt: now,
|
|
189
|
+
lastActiveAt: now,
|
|
190
|
+
sessionType: 'orchestrated',
|
|
191
|
+
agentId: agent.id,
|
|
192
|
+
parentSessionId: ctx?.sessionId || null,
|
|
193
|
+
tools: agent.tools || [],
|
|
194
|
+
heartbeatEnabled: agent.heartbeatEnabled ?? true,
|
|
195
|
+
heartbeatIntervalSec: agent.heartbeatIntervalSec ?? null,
|
|
196
|
+
}
|
|
197
|
+
sessions[id] = entry as any
|
|
198
|
+
saveSessions(sessions)
|
|
199
|
+
|
|
200
|
+
let runId: string | null = null
|
|
201
|
+
if (message?.trim()) {
|
|
202
|
+
const { enqueueSessionRun } = await import('../session-run-manager')
|
|
203
|
+
const run = enqueueSessionRun({
|
|
204
|
+
sessionId: id,
|
|
205
|
+
message: message.trim(),
|
|
206
|
+
source: 'session-spawn',
|
|
207
|
+
internal: false,
|
|
208
|
+
mode: 'followup',
|
|
209
|
+
})
|
|
210
|
+
runId = run.runId
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return JSON.stringify({
|
|
214
|
+
sessionId: id,
|
|
215
|
+
name: entry.name,
|
|
216
|
+
agentId: agent.id,
|
|
217
|
+
queuedRunId: runId,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (action === 'set_heartbeat') {
|
|
222
|
+
const targetSessionId = sessionId || ctx?.sessionId || null
|
|
223
|
+
if (!targetSessionId) return 'Error: sessionId is required when no current session context exists.'
|
|
224
|
+
const target = sessions[targetSessionId]
|
|
225
|
+
if (!target) return `Not found: session "${targetSessionId}"`
|
|
226
|
+
const intervalFromMs = typeof heartbeatIntervalMs === 'number'
|
|
227
|
+
? Math.max(0, Math.round(heartbeatIntervalMs / 1000))
|
|
228
|
+
: undefined
|
|
229
|
+
const nextIntervalSecRaw = typeof heartbeatIntervalSec === 'number'
|
|
230
|
+
? heartbeatIntervalSec
|
|
231
|
+
: intervalFromMs
|
|
232
|
+
const nextIntervalSec = typeof nextIntervalSecRaw === 'number'
|
|
233
|
+
? Math.max(0, Math.min(3600, Math.round(nextIntervalSecRaw)))
|
|
234
|
+
: undefined
|
|
235
|
+
|
|
236
|
+
if (typeof heartbeatEnabled !== 'boolean' && typeof nextIntervalSec !== 'number') {
|
|
237
|
+
return 'Error: set_heartbeat requires heartbeatEnabled and/or heartbeatIntervalSec/heartbeatIntervalMs.'
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof heartbeatEnabled === 'boolean') target.heartbeatEnabled = heartbeatEnabled
|
|
241
|
+
if (typeof nextIntervalSec === 'number') target.heartbeatIntervalSec = nextIntervalSec
|
|
242
|
+
target.lastActiveAt = Date.now()
|
|
243
|
+
|
|
244
|
+
let statusMessageAdded = false
|
|
245
|
+
if (target.heartbeatEnabled === false && finalStatus?.trim()) {
|
|
246
|
+
if (!Array.isArray(target.messages)) target.messages = []
|
|
247
|
+
target.messages.push({
|
|
248
|
+
role: 'assistant',
|
|
249
|
+
text: finalStatus.trim(),
|
|
250
|
+
time: Date.now(),
|
|
251
|
+
kind: 'heartbeat',
|
|
252
|
+
})
|
|
253
|
+
statusMessageAdded = true
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
saveSessions(sessions)
|
|
257
|
+
return JSON.stringify({
|
|
258
|
+
sessionId: targetSessionId,
|
|
259
|
+
heartbeatEnabled: target.heartbeatEnabled !== false,
|
|
260
|
+
heartbeatIntervalSec: target.heartbeatIntervalSec ?? null,
|
|
261
|
+
heartbeatIntervalMs: typeof target.heartbeatIntervalSec === 'number' ? target.heartbeatIntervalSec * 1000 : null,
|
|
262
|
+
statusMessageAdded,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (action === 'mailbox_send') {
|
|
267
|
+
if (!sessionId) return 'Error: sessionId (target session) is required for mailbox_send.'
|
|
268
|
+
if (!message?.trim()) return 'Error: message is required for mailbox_send.'
|
|
269
|
+
const { sendMailboxEnvelope } = await import('../session-mailbox')
|
|
270
|
+
const envelope = sendMailboxEnvelope({
|
|
271
|
+
toSessionId: sessionId,
|
|
272
|
+
type: type?.trim() || 'message',
|
|
273
|
+
payload: message.trim(),
|
|
274
|
+
fromSessionId: ctx?.sessionId || null,
|
|
275
|
+
fromAgentId: ctx?.agentId || null,
|
|
276
|
+
correlationId: correlationId?.trim() || null,
|
|
277
|
+
ttlSec: typeof ttlSec === 'number' ? ttlSec : null,
|
|
278
|
+
})
|
|
279
|
+
return JSON.stringify({ ok: true, envelope })
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (action === 'mailbox_inbox') {
|
|
283
|
+
const targetSessionId = sessionId || ctx?.sessionId || null
|
|
284
|
+
if (!targetSessionId) return 'Error: sessionId is required for mailbox_inbox when no current session context exists.'
|
|
285
|
+
const { listMailbox } = await import('../session-mailbox')
|
|
286
|
+
const envelopes = listMailbox(targetSessionId, { limit, includeAcked: false })
|
|
287
|
+
return JSON.stringify({
|
|
288
|
+
sessionId: targetSessionId,
|
|
289
|
+
count: envelopes.length,
|
|
290
|
+
envelopes,
|
|
291
|
+
currentSessionDefaulted: !sessionId,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (action === 'mailbox_ack') {
|
|
296
|
+
const targetSessionId = sessionId || ctx?.sessionId || null
|
|
297
|
+
if (!targetSessionId) return 'Error: sessionId is required for mailbox_ack when no current session context exists.'
|
|
298
|
+
if (!envelopeId?.trim()) return 'Error: envelopeId is required for mailbox_ack.'
|
|
299
|
+
const { ackMailboxEnvelope } = await import('../session-mailbox')
|
|
300
|
+
const envelope = ackMailboxEnvelope(targetSessionId, envelopeId.trim())
|
|
301
|
+
if (!envelope) return `Not found: envelope "${envelopeId.trim()}"`
|
|
302
|
+
return JSON.stringify({ ok: true, envelope })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (action === 'mailbox_clear') {
|
|
306
|
+
const targetSessionId = sessionId || ctx?.sessionId || null
|
|
307
|
+
if (!targetSessionId) return 'Error: sessionId is required for mailbox_clear when no current session context exists.'
|
|
308
|
+
const { clearMailbox } = await import('../session-mailbox')
|
|
309
|
+
const cleared = clearMailbox(targetSessionId, true)
|
|
310
|
+
return JSON.stringify({ ok: true, ...cleared })
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return 'Unknown action. Use list, history, status, send, spawn, stop, set_heartbeat, mailbox_send, mailbox_inbox, mailbox_ack, or mailbox_clear.'
|
|
314
|
+
} catch (err: any) {
|
|
315
|
+
return `Error: ${err.message || String(err)}`
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'sessions_tool',
|
|
320
|
+
description: 'Session-to-session operations: list/status/history sessions, send messages to other sessions, spawn new agent sessions, stop active runs, control per-session heartbeat, and exchange protocol envelopes via mailbox_* actions.',
|
|
321
|
+
schema: z.object({
|
|
322
|
+
action: z.enum(['list', 'history', 'status', 'send', 'spawn', 'stop', 'set_heartbeat', 'mailbox_send', 'mailbox_inbox', 'mailbox_ack', 'mailbox_clear']).describe('Session action'),
|
|
323
|
+
sessionId: z.string().optional().describe('Target session id (history defaults to current session when omitted; status/send/stop still require explicit sessionId)'),
|
|
324
|
+
message: z.string().optional().describe('Message body (required for send, optional initial task for spawn)'),
|
|
325
|
+
limit: z.number().optional().describe('Max items/messages for list/history'),
|
|
326
|
+
agentId: z.string().optional().describe('Agent id to spawn (required for spawn)'),
|
|
327
|
+
name: z.string().optional().describe('Optional session name for spawn'),
|
|
328
|
+
waitForReply: z.boolean().optional().describe('For send: if false, queue and return immediately'),
|
|
329
|
+
timeoutSec: z.number().optional().describe('For send with waitForReply=true, max wait time in seconds (default 120)'),
|
|
330
|
+
queueMode: z.enum(['followup', 'steer', 'collect']).optional().describe('Queue mode for send'),
|
|
331
|
+
heartbeatEnabled: z.boolean().optional().describe('For set_heartbeat: true to enable heartbeat, false to disable'),
|
|
332
|
+
heartbeatIntervalSec: z.number().optional().describe('For set_heartbeat: optional heartbeat interval in seconds (0-3600).'),
|
|
333
|
+
heartbeatIntervalMs: z.number().optional().describe('For set_heartbeat: optional heartbeat interval in milliseconds (alias of heartbeatIntervalSec).'),
|
|
334
|
+
finalStatus: z.string().optional().describe('For set_heartbeat when disabling: optional final status update to append in the session'),
|
|
335
|
+
envelopeId: z.string().optional().describe('For mailbox_ack: envelope id to acknowledge.'),
|
|
336
|
+
type: z.string().optional().describe('For mailbox_send: protocol message type (default "message").'),
|
|
337
|
+
correlationId: z.string().optional().describe('For mailbox_send: optional request/response correlation id.'),
|
|
338
|
+
ttlSec: z.number().optional().describe('For mailbox_send: optional envelope TTL in seconds.'),
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
tools.push(
|
|
345
|
+
tool(
|
|
346
|
+
async ({ query, sessionId, limit, dateRange }) => {
|
|
347
|
+
try {
|
|
348
|
+
const sessions = loadSessions()
|
|
349
|
+
const targetSessionId = sessionId || ctx?.sessionId || null
|
|
350
|
+
if (!targetSessionId) return 'Error: sessionId is required when no current session context exists.'
|
|
351
|
+
const target = sessions[targetSessionId]
|
|
352
|
+
if (!target) return `Not found: session "${targetSessionId}"`
|
|
353
|
+
|
|
354
|
+
const from = typeof dateRange?.from === 'number' ? dateRange.from : Number.NEGATIVE_INFINITY
|
|
355
|
+
const to = typeof dateRange?.to === 'number' ? dateRange.to : Number.POSITIVE_INFINITY
|
|
356
|
+
const max = Math.max(1, Math.min(limit || 20, 200))
|
|
357
|
+
const q = (query || '').trim().toLowerCase()
|
|
358
|
+
const terms = q ? q.split(/\s+/).filter(Boolean) : []
|
|
359
|
+
|
|
360
|
+
const scoredAll = (target.messages || [])
|
|
361
|
+
.map((m: any, idx: number) => ({ ...m, _idx: idx }))
|
|
362
|
+
.filter((m: any) => {
|
|
363
|
+
const t = typeof m.time === 'number' ? m.time : 0
|
|
364
|
+
if (t < from || t > to) return false
|
|
365
|
+
if (!terms.length) return true
|
|
366
|
+
const hay = `${m.role || ''}\n${m.kind || ''}\n${m.text || ''}`.toLowerCase()
|
|
367
|
+
return terms.every((term) => hay.includes(term))
|
|
368
|
+
})
|
|
369
|
+
.map((m: any) => {
|
|
370
|
+
const hay = `${m.text || ''}`.toLowerCase()
|
|
371
|
+
let score = 0
|
|
372
|
+
if (q && hay.includes(q)) score += 5
|
|
373
|
+
for (const term of terms) {
|
|
374
|
+
if (hay.includes(term)) score += 1
|
|
375
|
+
}
|
|
376
|
+
const ageBoost = Math.max(0, (m.time || 0) / 1e13)
|
|
377
|
+
score += ageBoost
|
|
378
|
+
return { ...m, _score: score }
|
|
379
|
+
})
|
|
380
|
+
.sort((a: any, b: any) => b._score - a._score)
|
|
381
|
+
const scored = scoredAll
|
|
382
|
+
.slice(0, max)
|
|
383
|
+
.map((m: any) => ({
|
|
384
|
+
index: m._idx,
|
|
385
|
+
role: m.role,
|
|
386
|
+
kind: m.kind || 'chat',
|
|
387
|
+
time: m.time,
|
|
388
|
+
text: typeof m.text === 'string' && m.text.length > 1200 ? `${m.text.slice(0, 1200)}...` : (m.text || ''),
|
|
389
|
+
}))
|
|
390
|
+
|
|
391
|
+
return JSON.stringify({
|
|
392
|
+
sessionId: target.id,
|
|
393
|
+
name: target.name,
|
|
394
|
+
query: query || '',
|
|
395
|
+
limit: max,
|
|
396
|
+
matches: scored,
|
|
397
|
+
totalMatches: scoredAll.length,
|
|
398
|
+
currentSessionDefaulted: !sessionId,
|
|
399
|
+
})
|
|
400
|
+
} catch (err: any) {
|
|
401
|
+
return `Error: ${err.message || String(err)}`
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: 'search_history_tool',
|
|
406
|
+
description: 'Search message history for the current session by default, or another session if sessionId is provided. Useful for recalling prior commitments, decisions, and details.',
|
|
407
|
+
schema: z.object({
|
|
408
|
+
query: z.string().describe('Search query text (keywords, phrase, or topic).'),
|
|
409
|
+
sessionId: z.string().optional().describe('Optional target session id; defaults to current session.'),
|
|
410
|
+
limit: z.number().optional().describe('Maximum number of matches to return (default 20, max 200).'),
|
|
411
|
+
dateRange: z.object({
|
|
412
|
+
from: z.number().optional().describe('Unix epoch ms lower bound (inclusive).'),
|
|
413
|
+
to: z.number().optional().describe('Unix epoch ms upper bound (inclusive).'),
|
|
414
|
+
}).optional().describe('Optional time filter for message timestamps.'),
|
|
415
|
+
}),
|
|
416
|
+
},
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return tools
|
|
422
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// 1. Module export verification
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
describe('module exports', () => {
|
|
8
|
+
it('buildSessionTools is exported from index', async () => {
|
|
9
|
+
const mod = await import('./index')
|
|
10
|
+
assert.equal(typeof mod.buildSessionTools, 'function')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('ToolContext type is re-exported from index (via SessionToolsResult)', async () => {
|
|
14
|
+
// ToolContext is a type-only export so we can't check it at runtime.
|
|
15
|
+
// Instead we verify the companion runtime exports from context.ts exist.
|
|
16
|
+
const ctx = await import('./context')
|
|
17
|
+
assert.equal(typeof ctx.safePath, 'function')
|
|
18
|
+
assert.equal(typeof ctx.truncate, 'function')
|
|
19
|
+
assert.equal(typeof ctx.MAX_OUTPUT, 'number')
|
|
20
|
+
assert.equal(typeof ctx.MAX_FILE, 'number')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('buildMemoryTools is exported from memory', async () => {
|
|
24
|
+
const mem = await import('./memory')
|
|
25
|
+
assert.equal(typeof mem.buildMemoryTools, 'function')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// 2. ToolContext type verification (compile-time)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
describe('ToolContext type', () => {
|
|
33
|
+
it('accepts mcpServerIds property', () => {
|
|
34
|
+
// This is a compile-time check. If ToolContext doesn't have mcpServerIds,
|
|
35
|
+
// TypeScript will reject this file at compilation.
|
|
36
|
+
const ctx: import('./context').ToolContext = {
|
|
37
|
+
agentId: 'a1',
|
|
38
|
+
sessionId: 's1',
|
|
39
|
+
mcpServerIds: ['mcp-1', 'mcp-2'],
|
|
40
|
+
}
|
|
41
|
+
assert.ok(ctx.mcpServerIds)
|
|
42
|
+
assert.equal(ctx.mcpServerIds.length, 2)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('mcpServerIds is optional', () => {
|
|
46
|
+
const ctx: import('./context').ToolContext = {}
|
|
47
|
+
assert.equal(ctx.mcpServerIds, undefined)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// 3. buildSessionTools function signature
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
describe('buildSessionTools signature', () => {
|
|
55
|
+
it('accepts (cwd, enabledTools, ctx?) and returns {tools, cleanup}', async () => {
|
|
56
|
+
const { buildSessionTools } = await import('./index')
|
|
57
|
+
// Verify the function has arity of at least 2
|
|
58
|
+
assert.ok(buildSessionTools.length >= 2, 'buildSessionTools should accept at least 2 params')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// 4. Memory tool schema — knowledge actions
|
|
64
|
+
// buildMemoryTools calls getMemoryDb() eagerly so we cannot invoke it
|
|
65
|
+
// without a real SQLite DB. Instead we read the source and verify the
|
|
66
|
+
// action enum includes the knowledge actions.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
describe('memory tool knowledge actions (source verification)', () => {
|
|
69
|
+
it('action enum in memory.ts includes knowledge_store and knowledge_search', async () => {
|
|
70
|
+
const fs = await import('fs')
|
|
71
|
+
const src = fs.readFileSync(
|
|
72
|
+
new URL('./memory.ts', import.meta.url).pathname,
|
|
73
|
+
'utf-8',
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// Find the z.enum([...]) for the action field
|
|
77
|
+
const enumMatch = src.match(/z\.enum\(\[([^\]]+)\]\)\.describe\([^)]*action/s)
|
|
78
|
+
assert.ok(enumMatch, 'Should find a z.enum() for the action field')
|
|
79
|
+
|
|
80
|
+
const enumBody = enumMatch![1]
|
|
81
|
+
assert.ok(enumBody.includes("'knowledge_store'"), 'action enum should include knowledge_store')
|
|
82
|
+
assert.ok(enumBody.includes("'knowledge_search'"), 'action enum should include knowledge_search')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('action enum includes all expected base actions', async () => {
|
|
86
|
+
const fs = await import('fs')
|
|
87
|
+
const src = fs.readFileSync(
|
|
88
|
+
new URL('./memory.ts', import.meta.url).pathname,
|
|
89
|
+
'utf-8',
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const enumMatch = src.match(/z\.enum\(\[([^\]]+)\]\)/)
|
|
93
|
+
assert.ok(enumMatch)
|
|
94
|
+
const enumBody = enumMatch![1]
|
|
95
|
+
|
|
96
|
+
const expectedActions = ['store', 'get', 'search', 'list', 'delete', 'link', 'unlink', 'knowledge_store', 'knowledge_search']
|
|
97
|
+
for (const action of expectedActions) {
|
|
98
|
+
assert.ok(
|
|
99
|
+
enumBody.includes(`'${action}'`),
|
|
100
|
+
`action enum should include '${action}'`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// 5. MCP tool block — compile-time type check
|
|
108
|
+
// Verifying that buildSessionTools accepts ToolContext with mcpServerIds.
|
|
109
|
+
// We can't call it without the full server env, so this is a type assertion.
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
describe('MCP tool block type wiring', () => {
|
|
112
|
+
it('buildSessionTools third parameter accepts ToolContext with mcpServerIds', () => {
|
|
113
|
+
// Compile-time assertion: if the types don't match, tsc will reject this.
|
|
114
|
+
type Params = Parameters<typeof import('./index').buildSessionTools>
|
|
115
|
+
type ThirdParam = Params[2]
|
|
116
|
+
|
|
117
|
+
// ThirdParam should be ToolContext | undefined
|
|
118
|
+
// We verify by assigning a value with mcpServerIds — if it compiles, it passes.
|
|
119
|
+
const _check: ThirdParam = {
|
|
120
|
+
agentId: 'a1',
|
|
121
|
+
sessionId: 's1',
|
|
122
|
+
mcpServerIds: ['server-1'],
|
|
123
|
+
}
|
|
124
|
+
assert.ok(_check, 'Type assignment compiled successfully')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('index.ts source has MCP tool block gated on mcpServerIds', async () => {
|
|
128
|
+
const fs = await import('fs')
|
|
129
|
+
const src = fs.readFileSync(
|
|
130
|
+
new URL('./index.ts', import.meta.url).pathname,
|
|
131
|
+
'utf-8',
|
|
132
|
+
)
|
|
133
|
+
assert.ok(src.includes('mcpServerIds'), 'index.ts should reference mcpServerIds')
|
|
134
|
+
assert.ok(src.includes('mcp_list_tools'), 'index.ts should define mcp_list_tools tool')
|
|
135
|
+
assert.ok(src.includes('mcp_call'), 'index.ts should define mcp_call tool')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// 6. Context utility functions
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
describe('context utility functions', () => {
|
|
143
|
+
it('safePath blocks traversal', async () => {
|
|
144
|
+
const { safePath } = await import('./context')
|
|
145
|
+
assert.throws(
|
|
146
|
+
() => safePath('/home/user/project', '../../etc/passwd'),
|
|
147
|
+
/Path traversal not allowed/,
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('safePath allows valid paths', async () => {
|
|
152
|
+
const { safePath } = await import('./context')
|
|
153
|
+
const result = safePath('/home/user/project', 'src/index.ts')
|
|
154
|
+
assert.equal(result, '/home/user/project/src/index.ts')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('truncate respects max length', async () => {
|
|
158
|
+
const { truncate } = await import('./context')
|
|
159
|
+
const short = truncate('hello', 100)
|
|
160
|
+
assert.equal(short, 'hello')
|
|
161
|
+
|
|
162
|
+
const long = truncate('a'.repeat(200), 50)
|
|
163
|
+
assert.ok(long.length > 50, 'truncated output includes suffix')
|
|
164
|
+
assert.ok(long.includes('[truncated'), 'should include truncation marker')
|
|
165
|
+
})
|
|
166
|
+
})
|