@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -6,7 +6,6 @@ import Database from 'better-sqlite3'
6
6
 
7
7
  import { DATA_DIR, WORKSPACE_DIR } from './data-dir'
8
8
  import type { Message } from '@/types'
9
- import { ensureMainSessionFlag } from './main-session'
10
9
  export const UPLOAD_DIR = path.join(DATA_DIR, 'uploads')
11
10
 
12
11
  // --- LRU Cache ---
@@ -157,8 +156,13 @@ const COLLECTIONS = [
157
156
  'souls',
158
157
  'benchmarks',
159
158
  'approvals',
159
+ 'browser_sessions',
160
+ 'watch_jobs',
161
+ 'delegation_jobs',
160
162
  ] as const
161
163
 
164
+ export type StorageCollection = (typeof COLLECTIONS)[number]
165
+
162
166
  for (const table of COLLECTIONS) {
163
167
  db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
164
168
  }
@@ -186,12 +190,34 @@ function getCollectionRawCache(table: string): LRUMap<string, string> {
186
190
  return loaded
187
191
  }
188
192
 
193
+ function normalizeStoredRecord(table: string, value: any): any {
194
+ if (table !== 'sessions') return value
195
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value
196
+
197
+ const session = value as Record<string, any>
198
+ if (session.sessionType !== 'human') session.sessionType = 'human'
199
+ const isLegacyShortcut = (
200
+ (typeof session.id === 'string' && session.id.startsWith('agent-thread-'))
201
+ || (typeof session.name === 'string' && session.name.startsWith('agent-thread:'))
202
+ )
203
+ if (
204
+ isLegacyShortcut
205
+ && typeof session.agentId === 'string'
206
+ && session.agentId.trim()
207
+ && (!session.shortcutForAgentId || session.shortcutForAgentId !== session.agentId)
208
+ ) {
209
+ session.shortcutForAgentId = session.agentId
210
+ }
211
+ if ('mainLoopState' in session) delete session.mainLoopState
212
+ return session
213
+ }
214
+
189
215
  function loadCollection(table: string): Record<string, any> {
190
216
  const raw = getCollectionRawCache(table)
191
217
  const result: Record<string, any> = {}
192
218
  for (const [id, data] of raw.entries()) {
193
219
  try {
194
- result[id] = JSON.parse(data)
220
+ result[id] = normalizeStoredRecord(table, JSON.parse(data))
195
221
  } catch {
196
222
  // Ignore malformed records instead of crashing list endpoints.
197
223
  }
@@ -206,7 +232,8 @@ function saveCollection(table: string, data: Record<string, any>) {
206
232
  const toDelete: string[] = []
207
233
 
208
234
  for (const [id, val] of Object.entries(data)) {
209
- const serialized = JSON.stringify(val)
235
+ const normalized = normalizeStoredRecord(table, val)
236
+ const serialized = JSON.stringify(normalized)
210
237
  if (typeof serialized !== 'string') continue
211
238
  next.set(id, serialized)
212
239
  if (current.get(id) !== serialized) {
@@ -252,7 +279,7 @@ function deleteCollectionItem(table: string, id: string) {
252
279
  * concurrent processes are modifying different items.
253
280
  */
254
281
  function upsertCollectionItem(table: string, id: string, value: any) {
255
- const serialized = JSON.stringify(value)
282
+ const serialized = JSON.stringify(normalizeStoredRecord(table, value))
256
283
  db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`).run(id, serialized)
257
284
  // Update the in-memory cache
258
285
  const cached = collectionCache.get(table)
@@ -261,6 +288,51 @@ function upsertCollectionItem(table: string, id: string, value: any) {
261
288
  }
262
289
  }
263
290
 
291
+ function loadCollectionItem(table: string, id: string): any | null {
292
+ const row = db.prepare(`SELECT data FROM ${table} WHERE id = ?`).get(id) as { data: string } | undefined
293
+ if (!row) return null
294
+ try {
295
+ return normalizeStoredRecord(table, JSON.parse(row.data))
296
+ } catch {
297
+ return null
298
+ }
299
+ }
300
+
301
+ function upsertCollectionItems(table: string, entries: Array<[string, any]>): void {
302
+ if (!entries.length) return
303
+ const prepared = entries
304
+ .map(([id, value]) => [id, JSON.stringify(normalizeStoredRecord(table, value))] as const)
305
+ .filter(([, serialized]) => typeof serialized === 'string')
306
+ if (!prepared.length) return
307
+
308
+ const transaction = db.transaction(() => {
309
+ const upsert = db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`)
310
+ for (const [id, serialized] of prepared) {
311
+ upsert.run(id, serialized)
312
+ }
313
+ })
314
+ transaction()
315
+
316
+ const cached = collectionCache.get(table)
317
+ if (cached) {
318
+ for (const [id, serialized] of prepared) {
319
+ cached.set(id, serialized)
320
+ }
321
+ }
322
+ }
323
+
324
+ export function loadStoredItem(table: StorageCollection, id: string): any | null {
325
+ return loadCollectionItem(table, id)
326
+ }
327
+
328
+ export function upsertStoredItem(table: StorageCollection, id: string, value: any): void {
329
+ upsertCollectionItem(table, id, value)
330
+ }
331
+
332
+ export function upsertStoredItems(table: StorageCollection, entries: Array<[string, any]>): void {
333
+ upsertCollectionItems(table, entries)
334
+ }
335
+
264
336
  function loadSingleton(table: string, fallback: any): any {
265
337
  const row = db.prepare(`SELECT data FROM ${table} WHERE id = 1`).get() as { data: string } | undefined
266
338
  return row ? JSON.parse(row.data) : fallback
@@ -393,7 +465,7 @@ if (!IS_BUILD_BOOTSTRAP) {
393
465
 
394
466
  ## Platform
395
467
 
396
- - **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Toggle "Orchestrator" to let an agent delegate work to others.
468
+ - **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Enable cross-agent delegation when an agent should assign work to others.
397
469
  - **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
398
470
  - **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
399
471
  - **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
@@ -420,8 +492,8 @@ Use your platform management tools proactively:
420
492
  You have opinions about good agent design. You suggest creative approaches, warn about common pitfalls, and get excited when someone gets something cool working. You're not a manual — you're a collaborator.
421
493
 
422
494
  Be concise but not curt. Warmth doesn't require verbosity. When someone asks "how do I...?", give them the direct steps. Offer to do things rather than just explaining — if someone wants an agent created, create it. Use your tools when actions speak louder than words. If you don't know something, say so honestly.`,
423
- isOrchestrator: false,
424
- tools: defaultStarterTools,
495
+ isOrchestrator: true,
496
+ plugins: defaultStarterTools,
425
497
  heartbeatEnabled: true,
426
498
  platformAssignScope: 'all',
427
499
  skillIds: [],
@@ -435,11 +507,21 @@ Be concise but not curt. Warmth doesn't require verbosity. When someone asks "ho
435
507
  if (row?.data) {
436
508
  try {
437
509
  const existing = JSON.parse(row.data) as Record<string, unknown>
438
- const existingTools = Array.isArray(existing.tools) ? existing.tools : []
439
- const mergedTools = Array.from(new Set([...existingTools, ...defaultStarterTools])).filter((t) => t !== 'delete_file')
440
- if (JSON.stringify(existingTools) !== JSON.stringify(mergedTools)) {
441
- existing.tools = mergedTools
510
+ const existingPlugins = Array.isArray(existing.plugins) ? existing.plugins : Array.isArray(existing.tools) ? existing.tools : []
511
+ const mergedPlugins = Array.from(new Set([...existingPlugins, ...defaultStarterTools])).filter((t) => t !== 'delete_file')
512
+ if (JSON.stringify(existingPlugins) !== JSON.stringify(mergedPlugins)) {
513
+ existing.plugins = mergedPlugins
514
+ delete existing.tools
442
515
  existing.updatedAt = Date.now()
516
+ }
517
+ if (existing.platformAssignScope === 'all' || existing.platformAssignScope === 'self') {
518
+ const derivedIsOrchestrator = existing.platformAssignScope === 'all'
519
+ if (existing.isOrchestrator !== derivedIsOrchestrator) {
520
+ existing.isOrchestrator = derivedIsOrchestrator
521
+ existing.updatedAt = Date.now()
522
+ }
523
+ }
524
+ if (JSON.stringify(JSON.parse(row.data)) !== JSON.stringify(existing)) {
443
525
  db.prepare('UPDATE agents SET data = ? WHERE id = ?').run(JSON.stringify(existing), 'default')
444
526
  }
445
527
  } catch {
@@ -516,15 +598,19 @@ export function loadSessions(): Record<string, any> {
516
598
  changed = true
517
599
  }
518
600
 
519
- const beforeMainFlag = session.mainSession === true
520
- ensureMainSessionFlag(session)
521
- if (!beforeMainFlag && session.mainSession === true) changed = true
522
601
 
523
602
  const agentId = typeof session.agentId === 'string' ? session.agentId.trim() : ''
524
603
  if (agentId && !Object.prototype.hasOwnProperty.call(agents, agentId)) {
525
604
  session.agentId = null
526
605
  changed = true
527
606
  }
607
+
608
+ // Migrate tools → plugins
609
+ if (Array.isArray(session.tools) && !Array.isArray(session.plugins)) {
610
+ session.plugins = session.tools
611
+ delete session.tools
612
+ changed = true
613
+ }
528
614
  }
529
615
 
530
616
  if (changed) saveCollection('sessions', sessions)
@@ -596,8 +682,24 @@ export function decryptKey(encrypted: string): string {
596
682
  }
597
683
 
598
684
  // --- Agents ---
685
+
686
+ /** Migrate legacy `tools` field → `plugins` on agent objects. */
687
+ function migrateAgentPlugins(agents: Record<string, Record<string, unknown>>): boolean {
688
+ let changed = false
689
+ for (const agent of Object.values(agents)) {
690
+ if (!agent || typeof agent !== 'object') continue
691
+ if (Array.isArray(agent.tools) && !Array.isArray(agent.plugins)) {
692
+ agent.plugins = agent.tools
693
+ delete agent.tools
694
+ changed = true
695
+ }
696
+ }
697
+ return changed
698
+ }
699
+
599
700
  export function loadAgents(opts?: { includeTrashed?: boolean }): Record<string, any> {
600
701
  const all = loadCollection('agents')
702
+ if (migrateAgentPlugins(all)) saveCollection('agents', all)
601
703
  if (opts?.includeTrashed) return all
602
704
  const result: Record<string, any> = {}
603
705
  for (const [id, agent] of Object.entries(all)) {
@@ -665,12 +767,106 @@ export function saveQueue(q: string[]) {
665
767
  }
666
768
 
667
769
  // --- Settings ---
770
+ const APP_SETTINGS_SECRET_FIELDS = [
771
+ 'elevenLabsApiKey',
772
+ 'tavilyApiKey',
773
+ 'braveApiKey',
774
+ ] as const
775
+
776
+ const ENCRYPTED_APP_SETTINGS_KEY = '__encryptedAppSettings'
777
+
778
+ type PersistedSettingsRecord = Record<string, any> & {
779
+ [ENCRYPTED_APP_SETTINGS_KEY]?: Record<string, string>
780
+ }
781
+
782
+ function cloneRecord<T extends Record<string, any>>(value: T): T {
783
+ return JSON.parse(JSON.stringify(value || {})) as T
784
+ }
785
+
786
+ function isPlainRecord(value: unknown): value is Record<string, any> {
787
+ return !!value && typeof value === 'object' && !Array.isArray(value)
788
+ }
789
+
790
+ function getEncryptedAppSettings(settings: PersistedSettingsRecord): Record<string, string> {
791
+ return isPlainRecord(settings[ENCRYPTED_APP_SETTINGS_KEY])
792
+ ? { ...(settings[ENCRYPTED_APP_SETTINGS_KEY] as Record<string, string>) }
793
+ : {}
794
+ }
795
+
796
+ function isClearedSecretValue(value: unknown): boolean {
797
+ return value === null || (typeof value === 'string' && value.trim() === '')
798
+ }
799
+
800
+ function isProvidedSecretValue(value: unknown): value is string {
801
+ return typeof value === 'string' && value.trim().length > 0
802
+ }
803
+
804
+ function buildPersistedSettings(input: Record<string, any>, existing?: PersistedSettingsRecord): PersistedSettingsRecord {
805
+ const next = cloneRecord(input) as PersistedSettingsRecord
806
+ const encrypted = {
807
+ ...(existing ? getEncryptedAppSettings(existing) : {}),
808
+ ...getEncryptedAppSettings(next),
809
+ }
810
+
811
+ delete next[ENCRYPTED_APP_SETTINGS_KEY]
812
+
813
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
814
+ const raw = next[field]
815
+ if (isClearedSecretValue(raw)) {
816
+ delete encrypted[field]
817
+ delete next[field]
818
+ continue
819
+ }
820
+ if (isProvidedSecretValue(raw)) {
821
+ encrypted[field] = encryptKey(raw)
822
+ delete next[field]
823
+ }
824
+ }
825
+
826
+ if (Object.keys(encrypted).length > 0) next[ENCRYPTED_APP_SETTINGS_KEY] = encrypted
827
+ return next
828
+ }
829
+
830
+ function resolveSettingsSecrets(settings: PersistedSettingsRecord): Record<string, any> {
831
+ const resolved = cloneRecord(settings)
832
+ delete resolved[ENCRYPTED_APP_SETTINGS_KEY]
833
+
834
+ const encrypted = getEncryptedAppSettings(settings)
835
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
836
+ if (isProvidedSecretValue(resolved[field])) continue
837
+ const value = encrypted[field]
838
+ if (typeof value !== 'string' || !value) continue
839
+ try {
840
+ resolved[field] = decryptKey(value)
841
+ } catch {
842
+ // Ignore malformed encrypted settings instead of breaking all settings reads.
843
+ }
844
+ }
845
+
846
+ return resolved
847
+ }
848
+
668
849
  export function loadSettings(): Record<string, any> {
669
- return loadSingleton('settings', {})
850
+ const persisted = loadSingleton('settings', {}) as PersistedSettingsRecord
851
+ const normalized = buildPersistedSettings(persisted, persisted)
852
+ if (JSON.stringify(persisted) !== JSON.stringify(normalized)) {
853
+ saveSingleton('settings', normalized)
854
+ }
855
+ return resolveSettingsSecrets(normalized)
670
856
  }
671
857
 
672
858
  export function saveSettings(s: Record<string, any>) {
673
- saveSingleton('settings', s)
859
+ const existing = loadSingleton('settings', {}) as PersistedSettingsRecord
860
+ saveSingleton('settings', buildPersistedSettings(s, existing))
861
+ }
862
+
863
+ export function loadPublicSettings(): Record<string, any> {
864
+ const settings = cloneRecord(loadSettings())
865
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
866
+ settings[`${field}Configured`] = isProvidedSecretValue(settings[field])
867
+ settings[field] = null
868
+ }
869
+ return settings
674
870
  }
675
871
 
676
872
  // --- Secrets (service keys for orchestrators) ---
@@ -1006,6 +1202,49 @@ export function loadApprovals(): Record<string, unknown> {
1006
1202
  return loadCollection('approvals')
1007
1203
  }
1008
1204
 
1205
+ // --- Browser Sessions ---
1206
+ export function loadBrowserSessions(): Record<string, unknown> {
1207
+ return loadCollection('browser_sessions')
1208
+ }
1209
+
1210
+ export function upsertBrowserSession(id: string, data: unknown) {
1211
+ upsertCollectionItem('browser_sessions', id, data)
1212
+ }
1213
+
1214
+ export function deleteBrowserSession(id: string) {
1215
+ deleteCollectionItem('browser_sessions', id)
1216
+ }
1217
+
1218
+ // --- Watch Jobs ---
1219
+ export function loadWatchJobs(): Record<string, unknown> {
1220
+ return loadCollection('watch_jobs')
1221
+ }
1222
+
1223
+ export function upsertWatchJob(id: string, data: unknown) {
1224
+ upsertCollectionItem('watch_jobs', id, data)
1225
+ }
1226
+
1227
+ export function upsertWatchJobs(entries: Array<[string, unknown]>) {
1228
+ upsertCollectionItems('watch_jobs', entries)
1229
+ }
1230
+
1231
+ export function deleteWatchJob(id: string) {
1232
+ deleteCollectionItem('watch_jobs', id)
1233
+ }
1234
+
1235
+ // --- Delegation Jobs ---
1236
+ export function loadDelegationJobs(): Record<string, unknown> {
1237
+ return loadCollection('delegation_jobs')
1238
+ }
1239
+
1240
+ export function upsertDelegationJob(id: string, data: unknown) {
1241
+ upsertCollectionItem('delegation_jobs', id, data)
1242
+ }
1243
+
1244
+ export function deleteDelegationJob(id: string) {
1245
+ deleteCollectionItem('delegation_jobs', id)
1246
+ }
1247
+
1009
1248
  export function upsertApproval(id: string, approval: unknown) {
1010
1249
  upsertCollectionItem('approvals', id, approval)
1011
1250
  }