@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.
- package/README.md +8 -2
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +5 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +23 -5
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/connectors/[id]/route.ts +7 -4
- package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +4 -3
- package/src/app/api/openclaw/discover/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +3 -2
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/api/secrets/[id]/route.ts +1 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +18 -1
- package/src/app/api/sessions/[id]/route.ts +12 -3
- package/src/app/api/sessions/route.ts +6 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/cli/index.js +33 -1
- package/src/cli/spec.js +40 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +82 -3
- package/src/components/agents/agent-sheet.tsx +31 -0
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +14 -2
- package/src/components/chat/chat-header.tsx +168 -4
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +89 -16
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/projects/project-list.tsx +1 -0
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/settings/settings-page.tsx +1 -0
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +42 -5
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/providers/openclaw.ts +17 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/chat-execution.ts +44 -2
- package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
- package/src/lib/server/connectors/bluebubbles.ts +5 -2
- package/src/lib/server/connectors/googlechat.ts +14 -10
- package/src/lib/server/connectors/manager.ts +37 -15
- package/src/lib/server/connectors/openclaw.ts +1 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/main-agent-loop.ts +2 -3
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +11 -10
- package/src/lib/server/queue.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +4 -4
- package/src/lib/server/session-tools/delegate.ts +19 -3
- package/src/lib/server/session-tools/file.ts +20 -20
- package/src/lib/server/session-tools/index.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
- package/src/lib/server/session-tools/sandbox.ts +2 -2
- package/src/lib/server/session-tools/search-providers.ts +13 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +8 -8
- package/src/lib/server/storage.ts +62 -11
- package/src/lib/server/stream-agent-chat.ts +24 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/stores/use-app-store.ts +53 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +153 -5
- package/src/types/index.ts +127 -1
- 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:
|
|
139
|
-
return `Error: ${err.message
|
|
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:
|
|
145
|
-
return `Error: ${err.message
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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}".
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
135
|
-
assert.ok(src.includes('
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
|
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
|
-
? `
|
|
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
|
-
?
|
|
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
|
-
? (
|
|
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
|
+
}
|
package/src/lib/server/ws-hub.ts
CHANGED
|
@@ -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
|
+
}
|