@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.
Files changed (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. 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
+ })