@swarmclawai/swarmclaw 0.4.0 → 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 (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -5,29 +5,9 @@ import path from 'path'
5
5
  import * as cheerio from 'cheerio'
6
6
  import { UPLOAD_DIR } from '../storage'
7
7
  import type { ToolBuildContext } from './context'
8
- import { safePath, truncate, MAX_OUTPUT } from './context'
9
-
10
- // ---------------------------------------------------------------------------
11
- // DuckDuckGo redirect-URL decoder
12
- // ---------------------------------------------------------------------------
13
-
14
- function decodeDuckDuckGoUrl(rawUrl: string): string {
15
- if (!rawUrl) return rawUrl
16
- try {
17
- const url = rawUrl.startsWith('http')
18
- ? new URL(rawUrl)
19
- : new URL(rawUrl, 'https://duckduckgo.com')
20
- const uddg = url.searchParams.get('uddg')
21
- if (uddg) return decodeURIComponent(uddg)
22
- return url.toString()
23
- } catch {
24
- const fromQuery = rawUrl.match(/[?&]uddg=([^&]+)/)?.[1]
25
- if (fromQuery) {
26
- try { return decodeURIComponent(fromQuery) } catch { /* noop */ }
27
- }
28
- return rawUrl
29
- }
30
- }
8
+ import { spawnSync } from 'child_process'
9
+ import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
10
+ import { getSearchProvider } from './search-providers'
31
11
 
32
12
  // ---------------------------------------------------------------------------
33
13
  // Global registry of active browser instances for cleanup sweeps
@@ -86,58 +66,20 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
86
66
  async ({ query, maxResults }) => {
87
67
  try {
88
68
  const limit = Math.min(maxResults || 5, 10)
89
- const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`
90
- const res = await fetch(url, {
91
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SwarmClaw/1.0)' },
92
- signal: AbortSignal.timeout(15000),
93
- })
94
- if (!res.ok) {
95
- return `Error searching web: HTTP ${res.status} ${res.statusText}`
96
- }
97
- const html = await res.text()
98
- const $ = cheerio.load(html)
99
- const results: { title: string; url: string; snippet: string }[] = []
100
-
101
- // Primary parser: DuckDuckGo result cards
102
- $('.result').each((_i, el) => {
103
- if (results.length >= limit) return false
104
- const link = $(el).find('a.result__a').first()
105
- const rawHref = link.attr('href') || ''
106
- const title = link.text().replace(/\s+/g, ' ').trim()
107
- if (!rawHref || !title) return
108
- const snippet = $(el).find('.result__snippet').first().text().replace(/\s+/g, ' ').trim()
109
- results.push({
110
- title,
111
- url: decodeDuckDuckGoUrl(rawHref),
112
- snippet,
113
- })
114
- })
115
-
116
- // Fallback parser: any result__a anchors
117
- if (results.length === 0) {
118
- $('a.result__a').each((_i, el) => {
119
- if (results.length >= limit) return false
120
- const rawHref = $(el).attr('href') || ''
121
- const title = $(el).text().replace(/\s+/g, ' ').trim()
122
- if (!rawHref || !title) return
123
- results.push({
124
- title,
125
- url: decodeDuckDuckGoUrl(rawHref),
126
- snippet: '',
127
- })
128
- })
129
- }
130
-
69
+ const { loadSettings } = await import('../storage')
70
+ const settings = loadSettings()
71
+ const provider = await getSearchProvider(settings)
72
+ const results = await provider.search(query, limit)
131
73
  return results.length > 0
132
74
  ? JSON.stringify(results, null, 2)
133
75
  : 'No results found.'
134
- } catch (err: any) {
135
- return `Error searching web: ${err.message}`
76
+ } catch (err: unknown) {
77
+ return `Error searching web: ${err instanceof Error ? err.message : String(err)}`
136
78
  }
137
79
  },
138
80
  {
139
81
  name: 'web_search',
140
- description: 'Search the web using DuckDuckGo. Returns an array of results with title, url, and snippet.',
82
+ description: 'Search the web. Returns an array of results with title, url, and snippet.',
141
83
  schema: z.object({
142
84
  query: z.string().describe('Search query'),
143
85
  maxResults: z.number().optional().describe('Maximum results to return (default 5, max 10)'),
@@ -169,8 +111,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
169
111
  .replace(/\s+/g, ' ')
170
112
  .trim()
171
113
  return truncate(text, MAX_OUTPUT)
172
- } catch (err: any) {
173
- return `Error fetching URL: ${err.message}`
114
+ } catch (err: unknown) {
115
+ return `Error fetching URL: ${err instanceof Error ? err.message : String(err)}`
174
116
  }
175
117
  },
176
118
  {
@@ -374,8 +316,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
374
316
  ? params.saveTo.trim()
375
317
  : undefined
376
318
  return await callMcpTool(mcpTool, args, { saveTo })
377
- } catch (err: any) {
378
- return `Error: ${err.message}`
319
+ } catch (err: unknown) {
320
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
379
321
  }
380
322
  },
381
323
  {
@@ -404,5 +346,44 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
404
346
  )
405
347
  }
406
348
 
349
+ // ---- openclaw_browser (CLI passthrough) -----------------------------------
350
+
351
+ if (bctx.hasTool('browser') || bctx.hasTool('openclaw_browser')) {
352
+ const openclawPath = findBinaryOnPath('openclaw') || findBinaryOnPath('clawdbot')
353
+ if (openclawPath) {
354
+ tools.push(
355
+ tool(
356
+ async ({ command, args: cmdArgs }) => {
357
+ try {
358
+ const spawnArgs = ['browser', command, '--json']
359
+ if (cmdArgs) spawnArgs.push(...cmdArgs.split(/\s+/).filter(Boolean))
360
+ const result = spawnSync(openclawPath, spawnArgs, {
361
+ encoding: 'utf-8',
362
+ timeout: 60_000,
363
+ maxBuffer: MAX_OUTPUT,
364
+ })
365
+ const stdout = (result.stdout || '').trim()
366
+ const stderr = (result.stderr || '').trim()
367
+ if (result.status !== 0) {
368
+ return `Error (exit ${result.status}): ${stderr || stdout || 'unknown error'}`
369
+ }
370
+ return truncate(stdout || '(no output)', MAX_OUTPUT)
371
+ } catch (err: unknown) {
372
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
373
+ }
374
+ },
375
+ {
376
+ name: 'openclaw_browser',
377
+ description: 'Control a browser through the OpenClaw CLI. Requires openclaw/clawdbot CLI on PATH. Passes through to `openclaw browser <command> --json`.',
378
+ schema: z.object({
379
+ command: z.string().describe('Browser command (navigate, screenshot, click, type, evaluate, etc.)'),
380
+ args: z.string().optional().describe('Additional arguments as a space-separated string'),
381
+ }),
382
+ },
383
+ ),
384
+ )
385
+ }
386
+ }
387
+
407
388
  return tools
408
389
  }
@@ -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
@@ -43,6 +48,7 @@ const COLLECTIONS = [
43
48
  'model_overrides',
44
49
  'mcp_servers',
45
50
  'webhook_logs',
51
+ 'projects',
46
52
  ] as const
47
53
 
48
54
  for (const table of COLLECTIONS) {
@@ -242,10 +248,12 @@ function migrateFromJson() {
242
248
  console.log('[storage] Migration complete. JSON files preserved as backup.')
243
249
  }
244
250
 
245
- migrateFromJson()
251
+ if (!IS_BUILD_BOOTSTRAP) {
252
+ migrateFromJson()
253
+ }
246
254
 
247
255
  // Seed default agent if agents table is empty
248
- {
256
+ if (!IS_BUILD_BOOTSTRAP) {
249
257
  const defaultStarterTools = [
250
258
  'memory',
251
259
  'files',
@@ -304,6 +312,7 @@ Be concise and helpful. When users ask how to do something, guide them to the sp
304
312
  soul: '',
305
313
  isOrchestrator: false,
306
314
  tools: defaultStarterTools,
315
+ heartbeatEnabled: true,
307
316
  platformAssignScope: 'all',
308
317
  skillIds: [],
309
318
  subAgentIds: [],
@@ -340,10 +349,12 @@ function loadEnv() {
340
349
  })
341
350
  }
342
351
  }
343
- loadEnv()
352
+ if (!IS_BUILD_BOOTSTRAP) {
353
+ loadEnv()
354
+ }
344
355
 
345
356
  // Auto-generate CREDENTIAL_SECRET if missing
346
- if (!process.env.CREDENTIAL_SECRET) {
357
+ if (!IS_BUILD_BOOTSTRAP && !process.env.CREDENTIAL_SECRET) {
347
358
  const secret = crypto.randomBytes(32).toString('hex')
348
359
  const envPath = path.join(process.cwd(), '.env.local')
349
360
  fs.appendFileSync(envPath, `\nCREDENTIAL_SECRET=${secret}\n`)
@@ -353,7 +364,7 @@ if (!process.env.CREDENTIAL_SECRET) {
353
364
 
354
365
  // Auto-generate ACCESS_KEY if missing (used for simple auth)
355
366
  const SETUP_FLAG = path.join(DATA_DIR, '.setup_pending')
356
- if (!process.env.ACCESS_KEY) {
367
+ if (!IS_BUILD_BOOTSTRAP && !process.env.ACCESS_KEY) {
357
368
  const key = crypto.randomBytes(16).toString('hex')
358
369
  const envPath = path.join(process.cwd(), '.env.local')
359
370
  fs.appendFileSync(envPath, `\nACCESS_KEY=${key}\n`)
@@ -383,7 +394,31 @@ export function markSetupComplete(): void {
383
394
 
384
395
  // --- Sessions ---
385
396
  export function loadSessions(): Record<string, any> {
386
- 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
387
422
  }
388
423
 
389
424
  export function saveSessions(s: Record<string, any>) {
@@ -451,8 +486,23 @@ export function decryptKey(encrypted: string): string {
451
486
  }
452
487
 
453
488
  // --- Agents ---
454
- export function loadAgents(): Record<string, any> {
455
- 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
456
506
  }
457
507
 
458
508
  export function saveAgents(p: Record<string, any>) {
@@ -526,12 +576,14 @@ export async function getSecret(key: string): Promise<{
526
576
  if (!needle) return null
527
577
 
528
578
  const secrets = loadSecrets()
579
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
529
580
  const matches = Object.values(secrets).find((secret: any) => {
530
581
  if (!secret || typeof secret !== 'object') return false
531
582
  const id = typeof secret.id === 'string' ? secret.id.toLowerCase() : ''
532
583
  const name = typeof secret.name === 'string' ? secret.name.toLowerCase() : ''
533
584
  const service = typeof secret.service === 'string' ? secret.service.toLowerCase() : ''
534
585
  return id === needle || name === needle || service === needle
586
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
587
  }) as any | undefined
536
588
 
537
589
  if (!matches) return null
@@ -576,6 +628,17 @@ export function saveModelOverrides(m: Record<string, string[]>) {
576
628
  saveCollection('model_overrides', m)
577
629
  }
578
630
 
631
+ // --- Projects ---
632
+ export function loadProjects(): Record<string, any> {
633
+ return loadCollection('projects')
634
+ }
635
+
636
+ export function saveProjects(s: Record<string, any>) {
637
+ saveCollection('projects', s)
638
+ }
639
+
640
+ export function deleteProject(id: string) { deleteCollectionItem('projects', id) }
641
+
579
642
  // --- Skills ---
580
643
  export function loadSkills(): Record<string, any> {
581
644
  return loadCollection('skills')
@@ -678,7 +741,7 @@ export function appendWebhookLog(id: string, entry: any) {
678
741
  upsertCollectionItem('webhook_logs', id, entry)
679
742
  }
680
743
 
681
- export function getSessionMessages(sessionId: string): any[] {
744
+ export function getSessionMessages(sessionId: string): Message[] {
682
745
  const stmt = db.prepare('SELECT data FROM sessions WHERE id = ?')
683
746
  const row = stmt.get(sessionId) as { data: string } | undefined
684
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.'
@@ -168,11 +169,20 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
168
169
 
169
170
  // fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
170
171
  void fallbackCredentialIds
172
+
173
+ // Resolve agent's thinking level for provider-native params
174
+ let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
175
+ if (session.agentId) {
176
+ const agentsForThinking = loadAgents()
177
+ agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
178
+ }
179
+
171
180
  const llm = buildChatModel({
172
181
  provider: session.provider,
173
182
  model: session.model,
174
183
  apiKey,
175
184
  apiEndpoint: session.apiEndpoint,
185
+ thinkingLevel: agentThinkingLevel,
176
186
  })
177
187
 
178
188
  // Build stateModifier
@@ -228,6 +238,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
228
238
  stateModifierParts.push('You are a capable AI assistant with tool access. Be execution-oriented and outcome-focused.')
229
239
  }
230
240
 
241
+ // Thinking level guidance (applies to all providers via system prompt)
242
+ if (agentThinkingLevel) {
243
+ const thinkingGuidance: Record<string, string> = {
244
+ minimal: 'Be direct and concise. Skip extended analysis.',
245
+ low: 'Keep reasoning brief. Focus on key conclusions.',
246
+ medium: 'Provide moderate depth of analysis and reasoning.',
247
+ high: 'Think deeply and thoroughly. Show detailed reasoning.',
248
+ }
249
+ stateModifierParts.push(`## Reasoning Depth\n${thinkingGuidance[agentThinkingLevel]}`)
250
+ }
251
+
231
252
  if ((session.tools || []).includes('memory') && session.agentId) {
232
253
  try {
233
254
  const memDb = getMemoryDb()
@@ -326,6 +347,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
326
347
  }
327
348
  }
328
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
+
329
360
  stateModifierParts.push(
330
361
  buildAgenticExecutionPolicy({
331
362
  enabledTools: session.tools || [],
@@ -586,6 +617,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
586
617
  if (signal) signal.removeEventListener('abort', abortFromSignal)
587
618
  }
588
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
+
589
627
  // Track cost
590
628
  const totalTokens = totalInputTokens + totalOutputTokens
591
629
  if (totalTokens > 0) {
@@ -612,14 +650,25 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
612
650
  // Plugin hooks: afterAgentComplete
613
651
  await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
614
652
 
653
+ // OpenClaw auto-sync: push memory if enabled
654
+ try {
655
+ const { loadSyncConfig, pushMemoryToOpenClaw } = await import('./openclaw-sync')
656
+ const syncConfig = loadSyncConfig()
657
+ if (syncConfig.autoSyncMemory) {
658
+ pushMemoryToOpenClaw(session.agentId || undefined)
659
+ }
660
+ } catch { /* OpenClaw sync not available — ignore */ }
661
+
615
662
  // Clean up browser and other session resources
616
663
  await cleanup()
617
664
 
618
665
  // If tools were called, finalResponse is the text from the last LLM turn only.
619
666
  // Fall back to fullText if the last segment is empty (e.g. agent ended on a tool call
620
667
  // with no summary text).
668
+ // Strip suggestions tag from lastSegment too (connector delivery)
669
+ const cleanLastSegment = extractSuggestions(lastSegment).clean
621
670
  const finalResponse = hasToolCalls
622
- ? (lastSegment.trim() || fullText)
671
+ ? (cleanLastSegment.trim() || fullText)
623
672
  : fullText
624
673
 
625
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
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { extractTaskResult } from './task-result'
4
+
5
+ describe('extractTaskResult', () => {
6
+ it('limits artifact extraction to messages from the current run window', () => {
7
+ const session = {
8
+ messages: [
9
+ {
10
+ role: 'assistant',
11
+ time: 1_000,
12
+ text: 'old run artifact: /api/uploads/wiki-old.png',
13
+ },
14
+ {
15
+ role: 'assistant',
16
+ time: 2_000,
17
+ text: 'new run artifact: /api/uploads/wiki-new.png',
18
+ },
19
+ ],
20
+ }
21
+
22
+ const result = extractTaskResult(session, 'done', { sinceTime: 1_500 })
23
+ assert.deepEqual(result.artifacts.map((a) => a.url), ['/api/uploads/wiki-new.png'])
24
+ })
25
+
26
+ it('excludes messages without timestamps when sinceTime is provided', () => {
27
+ const session = {
28
+ messages: [
29
+ {
30
+ role: 'assistant',
31
+ text: 'undated artifact: /api/uploads/undated.png',
32
+ },
33
+ {
34
+ role: 'assistant',
35
+ time: 5_000,
36
+ text: 'dated artifact: /api/uploads/dated.png',
37
+ },
38
+ ],
39
+ }
40
+
41
+ const result = extractTaskResult(session, 'done', { sinceTime: 4_000 })
42
+ assert.deepEqual(result.artifacts.map((a) => a.url), ['/api/uploads/dated.png'])
43
+ })
44
+ })
@@ -43,6 +43,7 @@ function classifyArtifact(filename: string): Artifact['type'] {
43
43
  interface MessageLike {
44
44
  role?: string
45
45
  text?: string
46
+ time?: number
46
47
  imageUrl?: string
47
48
  imagePath?: string
48
49
  toolEvents?: Array<{ name?: string; output?: string }>
@@ -52,6 +53,10 @@ interface SessionLike {
52
53
  messages?: MessageLike[]
53
54
  }
54
55
 
56
+ interface ExtractTaskResultOptions {
57
+ sinceTime?: number | null
58
+ }
59
+
55
60
  // ---------------------------------------------------------------------------
56
61
  // Core extraction
57
62
  // ---------------------------------------------------------------------------
@@ -64,9 +69,13 @@ interface SessionLike {
64
69
  export function extractTaskResult(
65
70
  session: SessionLike | null | undefined,
66
71
  rawResultText: string | null | undefined,
72
+ options?: ExtractTaskResultOptions,
67
73
  ): TaskResult {
68
74
  const seen = new Set<string>()
69
75
  const artifacts: Artifact[] = []
76
+ const sinceTime = typeof options?.sinceTime === 'number' && Number.isFinite(options.sinceTime)
77
+ ? options.sinceTime
78
+ : null
70
79
 
71
80
  function addUrl(raw: string) {
72
81
  const url = stripSandbox(raw)
@@ -79,6 +88,11 @@ export function extractTaskResult(
79
88
  // Walk session messages to collect all artifact URLs
80
89
  if (Array.isArray(session?.messages)) {
81
90
  for (const msg of session.messages) {
91
+ if (sinceTime !== null) {
92
+ const msgTime = typeof msg.time === 'number' && Number.isFinite(msg.time) ? msg.time : null
93
+ if (msgTime === null || msgTime < sinceTime) continue
94
+ }
95
+
82
96
  // Explicit image fields
83
97
  if (msg.imageUrl) addUrl(msg.imageUrl)
84
98
  if (msg.imagePath) {
@@ -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
+ }
@@ -18,8 +18,10 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
18
18
  { id: 'codex_cli', label: 'Codex CLI', description: 'Delegate complex tasks to OpenAI Codex CLI' },
19
19
  { id: 'opencode_cli', label: 'OpenCode CLI', description: 'Delegate complex tasks to OpenCode CLI' },
20
20
  { id: 'browser', label: 'Browser', description: 'Playwright — browse, scrape, interact with web pages' },
21
- { id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across sessions' },
21
+ { id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across conversations' },
22
22
  { id: 'sandbox', label: 'Sandbox', description: 'Run JS/TS/Python code in an isolated Deno sandbox' },
23
+ { id: 'create_document', label: 'Create Document', description: 'Render markdown to PDF, HTML, or image' },
24
+ { id: 'create_spreadsheet', label: 'Create Spreadsheet', description: 'Create Excel or CSV files from structured data' },
23
25
  ]
24
26
 
25
27
  export const PLATFORM_TOOLS: ToolDefinition[] = [
@@ -28,9 +30,9 @@ export const PLATFORM_TOOLS: ToolDefinition[] = [
28
30
  { id: 'manage_schedules', label: 'Schedules', description: 'Create, edit, and delete schedules' },
29
31
  { id: 'manage_skills', label: 'Skills', description: 'Create, edit, and delete skills' },
30
32
  { id: 'manage_documents', label: 'Documents', description: 'Upload, search, and delete indexed documents' },
31
- { id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent sessions' },
33
+ { id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent workflows' },
32
34
  { id: 'manage_connectors', label: 'Connectors', description: 'Create, edit, and delete connectors' },
33
- { id: 'manage_sessions', label: 'Sessions', description: 'List sessions, send messages, and spawn session work' },
35
+ { id: 'manage_sessions', label: 'Chats', description: 'List chats, send messages, and spawn agent work' },
34
36
  { id: 'manage_secrets', label: 'Secrets', description: 'Store and retrieve encrypted service secrets' },
35
37
  ]
36
38