@swarmclawai/swarmclaw 0.4.5 → 0.5.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 (113) hide show
  1. package/README.md +8 -2
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +5 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +23 -5
  6. package/src/app/api/agents/trash/route.ts +44 -0
  7. package/src/app/api/connectors/[id]/route.ts +7 -4
  8. package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
  9. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  10. package/src/app/api/openclaw/approvals/route.ts +46 -0
  11. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  12. package/src/app/api/openclaw/cron/route.ts +52 -0
  13. package/src/app/api/openclaw/directory/route.ts +4 -3
  14. package/src/app/api/openclaw/discover/route.ts +3 -2
  15. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  16. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  17. package/src/app/api/openclaw/gateway/route.ts +72 -0
  18. package/src/app/api/openclaw/history/route.ts +109 -0
  19. package/src/app/api/openclaw/media/route.ts +53 -0
  20. package/src/app/api/openclaw/models/route.ts +12 -0
  21. package/src/app/api/openclaw/permissions/route.ts +39 -0
  22. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  23. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  24. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  25. package/src/app/api/openclaw/skills/route.ts +82 -0
  26. package/src/app/api/openclaw/sync/route.ts +3 -2
  27. package/src/app/api/projects/[id]/route.ts +1 -1
  28. package/src/app/api/projects/route.ts +1 -1
  29. package/src/app/api/secrets/[id]/route.ts +1 -1
  30. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  31. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  32. package/src/app/api/sessions/[id]/messages/route.ts +18 -1
  33. package/src/app/api/sessions/[id]/route.ts +12 -3
  34. package/src/app/api/sessions/route.ts +6 -2
  35. package/src/app/globals.css +14 -0
  36. package/src/app/layout.tsx +5 -20
  37. package/src/cli/index.js +33 -1
  38. package/src/cli/spec.js +40 -0
  39. package/src/components/agents/agent-avatar.tsx +45 -0
  40. package/src/components/agents/agent-card.tsx +19 -5
  41. package/src/components/agents/agent-chat-list.tsx +31 -24
  42. package/src/components/agents/agent-files-editor.tsx +185 -0
  43. package/src/components/agents/agent-list.tsx +82 -3
  44. package/src/components/agents/agent-sheet.tsx +31 -0
  45. package/src/components/agents/cron-job-form.tsx +137 -0
  46. package/src/components/agents/exec-config-panel.tsx +147 -0
  47. package/src/components/agents/inspector-panel.tsx +310 -0
  48. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  49. package/src/components/agents/permission-preset-selector.tsx +79 -0
  50. package/src/components/agents/personality-builder.tsx +111 -0
  51. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  52. package/src/components/agents/skill-install-dialog.tsx +102 -0
  53. package/src/components/agents/trash-list.tsx +109 -0
  54. package/src/components/chat/chat-area.tsx +14 -2
  55. package/src/components/chat/chat-header.tsx +168 -4
  56. package/src/components/chat/chat-preview-panel.tsx +113 -0
  57. package/src/components/chat/exec-approval-card.tsx +89 -0
  58. package/src/components/chat/message-bubble.tsx +218 -36
  59. package/src/components/chat/message-list.tsx +135 -31
  60. package/src/components/chat/streaming-bubble.tsx +59 -10
  61. package/src/components/chat/suggestions-bar.tsx +74 -0
  62. package/src/components/chat/thinking-indicator.tsx +20 -6
  63. package/src/components/chat/tool-call-bubble.tsx +89 -16
  64. package/src/components/chat/tool-request-banner.tsx +20 -2
  65. package/src/components/chat/trace-block.tsx +103 -0
  66. package/src/components/projects/project-list.tsx +1 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  68. package/src/components/shared/avatar.tsx +13 -2
  69. package/src/components/shared/settings/settings-page.tsx +1 -0
  70. package/src/components/tasks/task-board.tsx +1 -1
  71. package/src/components/tasks/task-sheet.tsx +12 -12
  72. package/src/hooks/use-continuous-speech.ts +42 -5
  73. package/src/hooks/use-openclaw-gateway.ts +63 -0
  74. package/src/lib/notification-sounds.ts +58 -0
  75. package/src/lib/personality-parser.ts +97 -0
  76. package/src/lib/providers/openclaw.ts +17 -2
  77. package/src/lib/runtime-loop.ts +2 -2
  78. package/src/lib/server/chat-execution.ts +44 -2
  79. package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
  80. package/src/lib/server/connectors/bluebubbles.ts +5 -2
  81. package/src/lib/server/connectors/googlechat.ts +14 -10
  82. package/src/lib/server/connectors/manager.ts +37 -15
  83. package/src/lib/server/connectors/openclaw.ts +1 -0
  84. package/src/lib/server/daemon-state.ts +11 -0
  85. package/src/lib/server/main-agent-loop.ts +2 -3
  86. package/src/lib/server/main-session.ts +21 -0
  87. package/src/lib/server/openclaw-config-sync.ts +107 -0
  88. package/src/lib/server/openclaw-exec-config.ts +52 -0
  89. package/src/lib/server/openclaw-gateway.ts +291 -0
  90. package/src/lib/server/openclaw-history-merge.ts +36 -0
  91. package/src/lib/server/openclaw-models.ts +56 -0
  92. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  93. package/src/lib/server/openclaw-sync.ts +11 -10
  94. package/src/lib/server/queue.ts +2 -1
  95. package/src/lib/server/session-tools/connector.ts +4 -4
  96. package/src/lib/server/session-tools/delegate.ts +19 -3
  97. package/src/lib/server/session-tools/file.ts +20 -20
  98. package/src/lib/server/session-tools/index.ts +2 -2
  99. package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
  100. package/src/lib/server/session-tools/sandbox.ts +2 -2
  101. package/src/lib/server/session-tools/search-providers.ts +13 -6
  102. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  103. package/src/lib/server/session-tools/shell.ts +1 -1
  104. package/src/lib/server/session-tools/web.ts +8 -8
  105. package/src/lib/server/storage.ts +62 -11
  106. package/src/lib/server/stream-agent-chat.ts +24 -4
  107. package/src/lib/server/suggestions.ts +20 -0
  108. package/src/lib/server/ws-hub.ts +14 -0
  109. package/src/stores/use-app-store.ts +53 -1
  110. package/src/stores/use-approval-store.ts +78 -0
  111. package/src/stores/use-chat-store.ts +153 -5
  112. package/src/types/index.ts +127 -1
  113. package/tsconfig.json +13 -4
@@ -135,14 +135,14 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
135
135
  await inst.pinMessage(targetChannel, targetMessageId)
136
136
  return JSON.stringify({ status: 'pinned', connectorId, messageId: targetMessageId })
137
137
  }
138
- } catch (err: any) {
139
- return `Error: ${err.message || String(err)}`
138
+ } catch (err: unknown) {
139
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
140
140
  }
141
141
  }
142
142
 
143
143
  return 'Unknown action. Use list_running, list_targets, or send.'
144
- } catch (err: any) {
145
- return `Error: ${err.message || String(err)}`
144
+ } catch (err: unknown) {
145
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
146
146
  }
147
147
  },
148
148
  {
@@ -199,6 +199,21 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
199
199
  return
200
200
  }
201
201
 
202
+ // If resume failed because the session no longer exists, clear the stale ID
203
+ // and return a targeted error so the agent retries without resume
204
+ if (resumeIdToUse && /No conversation found/i.test(stdout + stderr)) {
205
+ persistDelegateResumeId('claudeCode', null)
206
+ log.warn('session-tools', 'delegate_to_claude_code stale resume ID cleared', {
207
+ sessionId: ctx?.sessionId || null,
208
+ staleResumeId: resumeIdToUse,
209
+ })
210
+ finish(
211
+ `Error: The previous Claude Code session (${resumeIdToUse}) has expired and was cleared. ` +
212
+ 'Retry the task with resume=false to start a fresh session.',
213
+ )
214
+ return
215
+ }
216
+
202
217
  const successText = assistantText.trim() || stdout.trim() || stderr.trim()
203
218
  if (code === 0 && successText) {
204
219
  const out = discoveredSessionId
@@ -230,7 +245,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
230
245
  },
231
246
  {
232
247
  name: 'delegate_to_claude_code',
233
- description: 'Delegate a complex task to Claude Code CLI. Use for tasks that need deep code understanding, multi-file refactoring, or running tests. The task runs in the session working directory.',
248
+ description: 'Delegate a complex multi-file coding task to Claude Code CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
234
249
  schema: z.object({
235
250
  task: z.string().describe('Detailed description of the task for Claude Code'),
236
251
  resume: z.boolean().optional().describe('If true, try to resume the last saved Claude delegation session for this SwarmClaw session'),
@@ -442,7 +457,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
442
457
  },
443
458
  {
444
459
  name: 'delegate_to_codex_cli',
445
- description: 'Delegate a complex task to Codex CLI. Use for deep coding/refactor tasks and shell-driven implementation work.',
460
+ description: 'Delegate a complex multi-file coding task to Codex CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
446
461
  schema: z.object({
447
462
  task: z.string().describe('Detailed description of the task for Codex CLI'),
448
463
  resume: z.boolean().optional().describe('If true, try to resume the last saved Codex delegation thread for this SwarmClaw session'),
@@ -607,7 +622,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
607
622
  },
608
623
  {
609
624
  name: 'delegate_to_opencode_cli',
610
- description: 'Delegate a complex task to OpenCode CLI. Use for deep coding/refactor tasks and shell-driven implementation work.',
625
+ description: 'Delegate a complex multi-file coding task to OpenCode CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
611
626
  schema: z.object({
612
627
  task: z.string().describe('Detailed description of the task for OpenCode CLI'),
613
628
  resume: z.boolean().optional().describe('If true, try to resume the last saved OpenCode delegation session for this SwarmClaw session'),
@@ -648,6 +663,7 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
648
663
  description: taskDesc || taskPrompt,
649
664
  status: 'todo',
650
665
  agentId: resolvedId,
666
+ cwd,
651
667
  sourceType: 'delegation' as const,
652
668
  delegatedByAgentId: ctx.agentId!,
653
669
  createdAt: now,
@@ -26,8 +26,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
26
26
  const resolved = safePath(bctx.cwd, filePath)
27
27
  const content = fs.readFileSync(resolved, 'utf-8')
28
28
  return truncate(content, MAX_FILE)
29
- } catch (err: any) {
30
- return `Error reading file: ${err.message}`
29
+ } catch (err: unknown) {
30
+ return `Error reading file: ${err instanceof Error ? err.message : String(err)}`
31
31
  }
32
32
  },
33
33
  {
@@ -55,8 +55,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
55
55
  }
56
56
  fs.writeFileSync(resolved, content, 'utf-8')
57
57
  return `File written: ${filePath} (${content.length} bytes)`
58
- } catch (err: any) {
59
- return `Error writing file: ${err.message}`
58
+ } catch (err: unknown) {
59
+ return `Error writing file: ${err instanceof Error ? err.message : String(err)}`
60
60
  }
61
61
  },
62
62
  {
@@ -80,8 +80,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
80
80
  const resolved = safePath(bctx.cwd, dirPath || '.')
81
81
  const tree = listDirRecursive(resolved, 0, 3)
82
82
  return tree.length ? tree.join('\n') : '(empty directory)'
83
- } catch (err: any) {
84
- return `Error listing files: ${err.message}`
83
+ } catch (err: unknown) {
84
+ return `Error listing files: ${err instanceof Error ? err.message : String(err)}`
85
85
  }
86
86
  },
87
87
  {
@@ -109,8 +109,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
109
109
  fs.mkdirSync(path.dirname(destination), { recursive: true })
110
110
  fs.copyFileSync(source, destination)
111
111
  return `File copied: ${sourcePath} -> ${destinationPath}`
112
- } catch (err: any) {
113
- return `Error copying file: ${err.message}`
112
+ } catch (err: unknown) {
113
+ return `Error copying file: ${err instanceof Error ? err.message : String(err)}`
114
114
  }
115
115
  },
116
116
  {
@@ -141,8 +141,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
141
141
  if (fs.existsSync(destination) && overwrite) fs.unlinkSync(destination)
142
142
  fs.renameSync(source, destination)
143
143
  return `File moved: ${sourcePath} -> ${destinationPath}`
144
- } catch (err: any) {
145
- return `Error moving file: ${err.message}`
144
+ } catch (err: unknown) {
145
+ return `Error moving file: ${err instanceof Error ? err.message : String(err)}`
146
146
  }
147
147
  },
148
148
  {
@@ -175,8 +175,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
175
175
  }
176
176
  fs.rmSync(resolved, { recursive: !!recursive, force: !!force })
177
177
  return `Deleted: ${filePath}`
178
- } catch (err: any) {
179
- return `Error deleting file: ${err.message}`
178
+ } catch (err: unknown) {
179
+ return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`
180
180
  }
181
181
  },
182
182
  {
@@ -220,8 +220,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
220
220
  } else {
221
221
  return `[Download ${basename}](/api/uploads/${filename})`
222
222
  }
223
- } catch (err: any) {
224
- return `Error sending file: ${err.message}`
223
+ } catch (err: unknown) {
224
+ return `Error sending file: ${err instanceof Error ? err.message : String(err)}`
225
225
  }
226
226
  },
227
227
  {
@@ -297,8 +297,8 @@ img{max-width:100%}
297
297
  } finally {
298
298
  await browser.close()
299
299
  }
300
- } catch (err: any) {
301
- return `Error creating document: ${err.message}`
300
+ } catch (err: unknown) {
301
+ return `Error creating document: ${err instanceof Error ? err.message : String(err)}`
302
302
  }
303
303
  },
304
304
  {
@@ -383,8 +383,8 @@ img{max-width:100%}
383
383
  await workbook.xlsx.writeFile(resolved)
384
384
  const size = fs.statSync(resolved).size
385
385
  return `Excel spreadsheet created: ${outName} (${rows.length} rows, ${cols.length} columns, ${(size / 1024).toFixed(1)} KB)`
386
- } catch (err: any) {
387
- return `Error creating spreadsheet: ${err.message}`
386
+ } catch (err: unknown) {
387
+ return `Error creating spreadsheet: ${err instanceof Error ? err.message : String(err)}`
388
388
  }
389
389
  },
390
390
  {
@@ -416,8 +416,8 @@ img{max-width:100%}
416
416
  const updated = content.replace(oldText, newText)
417
417
  fs.writeFileSync(resolved, updated, 'utf-8')
418
418
  return `Successfully edited ${filePath}`
419
- } catch (err: any) {
420
- return `Error editing file: ${err.message}`
419
+ } catch (err: unknown) {
420
+ return `Error editing file: ${err instanceof Error ? err.message : String(err)}`
421
421
  }
422
422
  },
423
423
  {
@@ -143,12 +143,12 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
143
143
  type: 'tool_request',
144
144
  toolId,
145
145
  reason,
146
- message: `Tool access request sent to user for "${toolId}". Wait for the user to grant access before trying to use it.`,
146
+ message: `Tool access request sent to user for "${toolId}". The user will be prompted to grant access once granted, a follow-up message will arrive and you should immediately proceed with the original task using the newly available tool.`,
147
147
  })
148
148
  },
149
149
  {
150
150
  name: 'request_tool_access',
151
- description: 'Request access to a tool that is currently disabled. The user will be prompted to grant access. Use this when you need a tool from the disabled tools list.',
151
+ description: 'Request access to a tool that is currently disabled. The user will be prompted to grant access, and a follow-up "Continue" message will be sent automatically once granted. End your current response after calling this do NOT tell the user to "let you know" or ask them to confirm; the continuation is automatic.',
152
152
  schema: z.object({
153
153
  toolId: z.string().describe('The tool ID to request access for (e.g. manage_tasks, shell, claude_code)'),
154
154
  reason: z.string().describe('Brief explanation of why you need this tool'),
@@ -28,8 +28,8 @@ export function buildOpenClawNodeTools(bctx: ToolBuildContext): StructuredToolIn
28
28
  connectorId: openclawConnectors[0].id,
29
29
  note: 'This feature requires the OpenClaw gateway to support nodes.* RPCs.',
30
30
  })
31
- } catch (err: any) {
32
- return JSON.stringify({ error: err.message })
31
+ } catch (err: unknown) {
32
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
33
33
  }
34
34
  },
35
35
  {
@@ -59,8 +59,8 @@ export function buildOpenClawNodeTools(bctx: ToolBuildContext): StructuredToolIn
59
59
  params: params || null,
60
60
  connectorId: openclawConnectors[0].id,
61
61
  })
62
- } catch (err: any) {
63
- return JSON.stringify({ error: err.message })
62
+ } catch (err: unknown) {
63
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
64
64
  }
65
65
  },
66
66
  {
@@ -93,8 +93,8 @@ export function buildOpenClawNodeTools(bctx: ToolBuildContext): StructuredToolIn
93
93
  message,
94
94
  connectorId: openclawConnectors[0].id,
95
95
  })
96
- } catch (err: any) {
97
- return JSON.stringify({ error: err.message })
96
+ } catch (err: unknown) {
97
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
98
98
  }
99
99
  },
100
100
  {
@@ -177,8 +177,8 @@ export function buildSandboxTools(bctx: ToolBuildContext): StructuredToolInterfa
177
177
  const stdout = truncate((result.stdout || '').trim(), MAX_OUTPUT)
178
178
  const stderr = truncate((result.stderr || '').trim(), MAX_OUTPUT)
179
179
  return JSON.stringify({ exitCode: result.status ?? 0, stdout, stderr })
180
- } catch (err: any) {
181
- return JSON.stringify({ error: err.message })
180
+ } catch (err: unknown) {
181
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
182
182
  }
183
183
  },
184
184
  {
@@ -13,6 +13,13 @@ export interface WebSearchProvider {
13
13
  search(query: string, maxResults: number): Promise<SearchResult[]>
14
14
  }
15
15
 
16
+ interface RawSearchResult {
17
+ title?: string
18
+ url?: string
19
+ content?: string
20
+ description?: string
21
+ }
22
+
16
23
  const UA = 'Mozilla/5.0 (compatible; SwarmClaw/1.0)'
17
24
 
18
25
  // ---------------------------------------------------------------------------
@@ -162,8 +169,8 @@ class SearXNGProvider implements WebSearchProvider {
162
169
  })
163
170
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
164
171
  const data = await res.json()
165
- const rawResults = Array.isArray(data.results) ? data.results : []
166
- return rawResults.slice(0, maxResults).map((r: any) => ({
172
+ const rawResults: RawSearchResult[] = Array.isArray(data.results) ? data.results : []
173
+ return rawResults.slice(0, maxResults).map((r) => ({
167
174
  title: r.title || '',
168
175
  url: r.url || '',
169
176
  snippet: r.content || '',
@@ -194,8 +201,8 @@ class TavilyProvider implements WebSearchProvider {
194
201
  })
195
202
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
196
203
  const data = await res.json()
197
- const rawResults = Array.isArray(data.results) ? data.results : []
198
- return rawResults.slice(0, maxResults).map((r: any) => ({
204
+ const rawResults: RawSearchResult[] = Array.isArray(data.results) ? data.results : []
205
+ return rawResults.slice(0, maxResults).map((r) => ({
199
206
  title: r.title || '',
200
207
  url: r.url || '',
201
208
  snippet: r.content || '',
@@ -227,8 +234,8 @@ class BraveProvider implements WebSearchProvider {
227
234
  )
228
235
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
229
236
  const data = await res.json()
230
- const rawResults = Array.isArray(data.web?.results) ? data.web.results : []
231
- return rawResults.slice(0, maxResults).map((r: any) => ({
237
+ const rawResults: RawSearchResult[] = Array.isArray(data.web?.results) ? data.web.results : []
238
+ return rawResults.slice(0, maxResults).map((r) => ({
232
239
  title: r.title || '',
233
240
  url: r.url || '',
234
241
  snippet: r.description || '',
@@ -131,8 +131,8 @@ describe('MCP tool block type wiring', () => {
131
131
  'utf-8',
132
132
  )
133
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')
134
+ assert.ok(src.includes('connectMcpServer'), 'index.ts should connect configured MCP servers')
135
+ assert.ok(src.includes('mcpToolsToLangChain'), 'index.ts should inject MCP tools dynamically')
136
136
  })
137
137
  })
138
138
 
@@ -49,7 +49,7 @@ export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface
49
49
  },
50
50
  {
51
51
  name: 'execute_command',
52
- description: 'Execute a shell command in the session working directory. Supports background mode and timeout/yield controls.',
52
+ description: 'Execute a shell command in the session working directory. This is the PRIMARY tool for running servers, dev servers, installing packages, running scripts, git operations, and any command the user wants to run or test. Use background=true for long-running processes like servers. Supports timeout/yield controls.',
53
53
  schema: z.object({
54
54
  command: z.string().describe('The shell command to execute'),
55
55
  background: z.boolean().optional().describe('If true, start command in background immediately'),
@@ -73,8 +73,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
73
73
  return results.length > 0
74
74
  ? JSON.stringify(results, null, 2)
75
75
  : 'No results found.'
76
- } catch (err: any) {
77
- return `Error searching web: ${err.message}`
76
+ } catch (err: unknown) {
77
+ return `Error searching web: ${err instanceof Error ? err.message : String(err)}`
78
78
  }
79
79
  },
80
80
  {
@@ -111,8 +111,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
111
111
  .replace(/\s+/g, ' ')
112
112
  .trim()
113
113
  return truncate(text, MAX_OUTPUT)
114
- } catch (err: any) {
115
- return `Error fetching URL: ${err.message}`
114
+ } catch (err: unknown) {
115
+ return `Error fetching URL: ${err instanceof Error ? err.message : String(err)}`
116
116
  }
117
117
  },
118
118
  {
@@ -316,8 +316,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
316
316
  ? params.saveTo.trim()
317
317
  : undefined
318
318
  return await callMcpTool(mcpTool, args, { saveTo })
319
- } catch (err: any) {
320
- return `Error: ${err.message}`
319
+ } catch (err: unknown) {
320
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
321
321
  }
322
322
  },
323
323
  {
@@ -368,8 +368,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
368
368
  return `Error (exit ${result.status}): ${stderr || stdout || 'unknown error'}`
369
369
  }
370
370
  return truncate(stdout || '(no output)', MAX_OUTPUT)
371
- } catch (err: any) {
372
- return `Error: ${err.message}`
371
+ } catch (err: unknown) {
372
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
373
373
  }
374
374
  },
375
375
  {
@@ -5,6 +5,8 @@ import os from 'os'
5
5
  import Database from 'better-sqlite3'
6
6
 
7
7
  import { DATA_DIR, WORKSPACE_DIR } from './data-dir'
8
+ import type { Message } from '@/types'
9
+ import { ensureMainSessionFlag } from './main-session'
8
10
  export const UPLOAD_DIR = path.join(DATA_DIR, 'uploads')
9
11
 
10
12
  // Ensure directories exist
@@ -13,9 +15,12 @@ for (const dir of [DATA_DIR, UPLOAD_DIR, WORKSPACE_DIR]) {
13
15
  }
14
16
 
15
17
  // --- SQLite Database ---
16
- const DB_PATH = path.join(DATA_DIR, 'swarmclaw.db')
18
+ const IS_BUILD_BOOTSTRAP = process.env.SWARMCLAW_BUILD_MODE === '1'
19
+ const DB_PATH = IS_BUILD_BOOTSTRAP ? ':memory:' : path.join(DATA_DIR, 'swarmclaw.db')
17
20
  const db = new Database(DB_PATH)
18
- db.pragma('journal_mode = WAL')
21
+ if (!IS_BUILD_BOOTSTRAP) {
22
+ db.pragma('journal_mode = WAL')
23
+ }
19
24
  db.pragma('foreign_keys = ON')
20
25
 
21
26
  const collectionCacheKey = '__swarmclaw_storage_collection_cache__' as const
@@ -243,10 +248,12 @@ function migrateFromJson() {
243
248
  console.log('[storage] Migration complete. JSON files preserved as backup.')
244
249
  }
245
250
 
246
- migrateFromJson()
251
+ if (!IS_BUILD_BOOTSTRAP) {
252
+ migrateFromJson()
253
+ }
247
254
 
248
255
  // Seed default agent if agents table is empty
249
- {
256
+ if (!IS_BUILD_BOOTSTRAP) {
250
257
  const defaultStarterTools = [
251
258
  'memory',
252
259
  'files',
@@ -305,6 +312,7 @@ Be concise and helpful. When users ask how to do something, guide them to the sp
305
312
  soul: '',
306
313
  isOrchestrator: false,
307
314
  tools: defaultStarterTools,
315
+ heartbeatEnabled: true,
308
316
  platformAssignScope: 'all',
309
317
  skillIds: [],
310
318
  subAgentIds: [],
@@ -341,10 +349,12 @@ function loadEnv() {
341
349
  })
342
350
  }
343
351
  }
344
- loadEnv()
352
+ if (!IS_BUILD_BOOTSTRAP) {
353
+ loadEnv()
354
+ }
345
355
 
346
356
  // Auto-generate CREDENTIAL_SECRET if missing
347
- if (!process.env.CREDENTIAL_SECRET) {
357
+ if (!IS_BUILD_BOOTSTRAP && !process.env.CREDENTIAL_SECRET) {
348
358
  const secret = crypto.randomBytes(32).toString('hex')
349
359
  const envPath = path.join(process.cwd(), '.env.local')
350
360
  fs.appendFileSync(envPath, `\nCREDENTIAL_SECRET=${secret}\n`)
@@ -354,7 +364,7 @@ if (!process.env.CREDENTIAL_SECRET) {
354
364
 
355
365
  // Auto-generate ACCESS_KEY if missing (used for simple auth)
356
366
  const SETUP_FLAG = path.join(DATA_DIR, '.setup_pending')
357
- if (!process.env.ACCESS_KEY) {
367
+ if (!IS_BUILD_BOOTSTRAP && !process.env.ACCESS_KEY) {
358
368
  const key = crypto.randomBytes(16).toString('hex')
359
369
  const envPath = path.join(process.cwd(), '.env.local')
360
370
  fs.appendFileSync(envPath, `\nACCESS_KEY=${key}\n`)
@@ -384,7 +394,31 @@ export function markSetupComplete(): void {
384
394
 
385
395
  // --- Sessions ---
386
396
  export function loadSessions(): Record<string, any> {
387
- return loadCollection('sessions')
397
+ const sessions = loadCollection('sessions')
398
+ const agents = loadCollection('agents')
399
+ let changed = false
400
+
401
+ for (const [id, session] of Object.entries(sessions)) {
402
+ if (!session || typeof session !== 'object') continue
403
+
404
+ if (typeof session.id !== 'string' || !session.id.trim()) {
405
+ session.id = id
406
+ changed = true
407
+ }
408
+
409
+ const beforeMainFlag = session.mainSession === true
410
+ ensureMainSessionFlag(session)
411
+ if (!beforeMainFlag && session.mainSession === true) changed = true
412
+
413
+ const agentId = typeof session.agentId === 'string' ? session.agentId.trim() : ''
414
+ if (agentId && !Object.prototype.hasOwnProperty.call(agents, agentId)) {
415
+ session.agentId = null
416
+ changed = true
417
+ }
418
+ }
419
+
420
+ if (changed) saveCollection('sessions', sessions)
421
+ return sessions
388
422
  }
389
423
 
390
424
  export function saveSessions(s: Record<string, any>) {
@@ -452,8 +486,23 @@ export function decryptKey(encrypted: string): string {
452
486
  }
453
487
 
454
488
  // --- Agents ---
455
- export function loadAgents(): Record<string, any> {
456
- return loadCollection('agents')
489
+ export function loadAgents(opts?: { includeTrashed?: boolean }): Record<string, any> {
490
+ const all = loadCollection('agents')
491
+ if (opts?.includeTrashed) return all
492
+ const result: Record<string, any> = {}
493
+ for (const [id, agent] of Object.entries(all)) {
494
+ if (!agent.trashedAt) result[id] = agent
495
+ }
496
+ return result
497
+ }
498
+
499
+ export function loadTrashedAgents(): Record<string, any> {
500
+ const all = loadCollection('agents')
501
+ const result: Record<string, any> = {}
502
+ for (const [id, agent] of Object.entries(all)) {
503
+ if (agent.trashedAt) result[id] = agent
504
+ }
505
+ return result
457
506
  }
458
507
 
459
508
  export function saveAgents(p: Record<string, any>) {
@@ -527,12 +576,14 @@ export async function getSecret(key: string): Promise<{
527
576
  if (!needle) return null
528
577
 
529
578
  const secrets = loadSecrets()
579
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
530
580
  const matches = Object.values(secrets).find((secret: any) => {
531
581
  if (!secret || typeof secret !== 'object') return false
532
582
  const id = typeof secret.id === 'string' ? secret.id.toLowerCase() : ''
533
583
  const name = typeof secret.name === 'string' ? secret.name.toLowerCase() : ''
534
584
  const service = typeof secret.service === 'string' ? secret.service.toLowerCase() : ''
535
585
  return id === needle || name === needle || service === needle
586
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
536
587
  }) as any | undefined
537
588
 
538
589
  if (!matches) return null
@@ -690,7 +741,7 @@ export function appendWebhookLog(id: string, entry: any) {
690
741
  upsertCollectionItem('webhook_logs', id, entry)
691
742
  }
692
743
 
693
- export function getSessionMessages(sessionId: string): any[] {
744
+ export function getSessionMessages(sessionId: string): Message[] {
694
745
  const stmt = db.prepare('SELECT data FROM sessions WHERE id = ?')
695
746
  const row = stmt.get(sessionId) as { data: string } | undefined
696
747
  if (!row) return []
@@ -10,6 +10,7 @@ import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-setti
10
10
  import { getMemoryDb } from './memory-db'
11
11
  import { logExecution } from './execution-log'
12
12
  import type { Session, Message, UsageRecord } from '@/types'
13
+ import { extractSuggestions } from './suggestions'
13
14
 
14
15
  interface StreamAgentChatOpts {
15
16
  session: Session
@@ -26,7 +27,7 @@ interface StreamAgentChatOpts {
26
27
 
27
28
  function buildToolCapabilityLines(enabledTools: string[]): string[] {
28
29
  const lines: string[] = []
29
- if (enabledTools.includes('shell')) lines.push('- Shell execution is available (`execute_command`). Use it for real checks/build/test steps.')
30
+ if (enabledTools.includes('shell')) lines.push('- Shell execution is available (`execute_command`). Use it for running servers, installing deps, running scripts, git commands, build/test steps, and any single or chained shell commands. Supports background mode for long-running processes like dev servers.')
30
31
  if (enabledTools.includes('process')) lines.push('- Process control is available (`process_tool`) for long-running commands (poll/log/write/kill).')
31
32
  if (enabledTools.includes('files') || enabledTools.includes('copy_file') || enabledTools.includes('move_file') || enabledTools.includes('delete_file')) {
32
33
  lines.push('- File operations are available (`read_file`, `write_file`, `list_files`, `copy_file`, `move_file`, `send_file`). `delete_file` is destructive and may be disabled unless explicitly enabled.')
@@ -111,10 +112,10 @@ function buildAgenticExecutionPolicy(opts: {
111
112
  ? 'When coordinating platform work, inspect existing sessions and avoid duplicating active efforts.'
112
113
  : '',
113
114
  hasDelegationTool
114
- ? `For substantial coding/build/refactor/test requests, prefer CLI delegation first using this order: ${delegationOrder.join(' -> ')}.`
115
+ ? 'CRITICAL — tool selection: ALWAYS use `execute_command` for running servers, dev servers, HTTP servers, installing dependencies, running scripts, git operations, process management, starting/stopping services, or any command the user wants to "run". Delegation tools (Claude/Codex/OpenCode) CANNOT keep a server running — their session ends and the process dies. `execute_command` with background=true is the ONLY way to run persistent processes.'
115
116
  : '',
116
117
  hasDelegationTool
117
- ? 'Use direct shell/file tool loops yourself mainly for small edits, quick verification, or when delegation tools are unavailable/failing.'
118
+ ? `Only use CLI delegation (${delegationOrder.join(' -> ')}) for tasks that need deep code understanding across multiple files: large refactors, complex debugging, multi-file code generation, or test suites. Never delegate when the user says "run", "start", "serve", "execute", or "test it locally".`
118
119
  : '',
119
120
  opts.enabledTools.includes('memory')
120
121
  ? 'Memory is active and required for long-horizon work: before major tasks, run memory_tool search/list for relevant prior work; after each meaningful step, store concise reusable notes (what changed, where it lives, constraints, next step). Treat memory as shared context plus your own agent notes, not as user-owned personal profile data.'
@@ -346,6 +347,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
346
347
  }
347
348
  }
348
349
 
350
+ stateModifierParts.push(
351
+ [
352
+ '## Follow-up Suggestions',
353
+ 'At the end of every response, include a <suggestions> block with exactly 3 short',
354
+ 'follow-up prompts the user might want to send next, as a JSON array. Keep each under 60 chars.',
355
+ 'Make them contextual to what you just said. Example:',
356
+ '<suggestions>["Set up a Discord connector", "Create a research agent", "Show the task board"]</suggestions>',
357
+ ].join('\n'),
358
+ )
359
+
349
360
  stateModifierParts.push(
350
361
  buildAgenticExecutionPolicy({
351
362
  enabledTools: session.tools || [],
@@ -606,6 +617,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
606
617
  if (signal) signal.removeEventListener('abort', abortFromSignal)
607
618
  }
608
619
 
620
+ // Extract LLM-generated suggestions from the response and strip the tag
621
+ const extracted = extractSuggestions(fullText)
622
+ fullText = extracted.clean
623
+ if (extracted.suggestions) {
624
+ write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ suggestions: extracted.suggestions }) })}\n\n`)
625
+ }
626
+
609
627
  // Track cost
610
628
  const totalTokens = totalInputTokens + totalOutputTokens
611
629
  if (totalTokens > 0) {
@@ -647,8 +665,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
647
665
  // If tools were called, finalResponse is the text from the last LLM turn only.
648
666
  // Fall back to fullText if the last segment is empty (e.g. agent ended on a tool call
649
667
  // with no summary text).
668
+ // Strip suggestions tag from lastSegment too (connector delivery)
669
+ const cleanLastSegment = extractSuggestions(lastSegment).clean
650
670
  const finalResponse = hasToolCalls
651
- ? (lastSegment.trim() || fullText)
671
+ ? (cleanLastSegment.trim() || fullText)
652
672
  : fullText
653
673
 
654
674
  return { fullText, finalResponse }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod'
2
+
3
+ const suggestionsSchema = z.array(z.string().min(1).max(80)).length(3)
4
+
5
+ const SUGGESTIONS_RE = /<suggestions>\s*([\s\S]*?)\s*<\/suggestions>\s*$/
6
+
7
+ export function extractSuggestions(text: string): { clean: string; suggestions: string[] | null } {
8
+ const match = text.match(SUGGESTIONS_RE)
9
+ if (!match) return { clean: text, suggestions: null }
10
+
11
+ const clean = text.slice(0, match.index).trimEnd()
12
+
13
+ try {
14
+ const parsed = JSON.parse(match[1])
15
+ const validated = suggestionsSchema.parse(parsed)
16
+ return { clean, suggestions: validated }
17
+ } catch {
18
+ return { clean, suggestions: null }
19
+ }
20
+ }
@@ -83,3 +83,17 @@ export function notify(topic: string, action = 'update', id?: string) {
83
83
  }
84
84
  }
85
85
  }
86
+
87
+ /** Send an event with a data payload to subscribed browser clients. */
88
+ export function notifyWithPayload(topic: string, data: unknown) {
89
+ const hub = getHub()
90
+ if (!hub) return
91
+
92
+ const payload = JSON.stringify({ topic, action: 'event', data })
93
+
94
+ for (const client of hub.clients) {
95
+ if (client.topics.has(topic) && client.ws.readyState === WebSocket.OPEN) {
96
+ client.ws.send(payload)
97
+ }
98
+ }
99
+ }