@swarmclawai/swarmclaw 0.6.4 → 0.6.7

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 (143) hide show
  1. package/README.md +62 -30
  2. package/package.json +10 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +34 -2
  8. package/src/app/api/chatrooms/route.ts +26 -3
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  14. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  15. package/src/app/api/sessions/route.ts +11 -2
  16. package/src/app/api/tasks/[id]/route.ts +18 -13
  17. package/src/app/api/tasks/route.ts +44 -1
  18. package/src/app/api/usage/route.ts +16 -7
  19. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  20. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  21. package/src/app/api/wallets/[id]/route.ts +118 -0
  22. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  23. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  24. package/src/app/api/wallets/route.ts +74 -0
  25. package/src/app/globals.css +8 -0
  26. package/src/cli/index.js +20 -0
  27. package/src/cli/index.ts +223 -39
  28. package/src/cli/spec.js +14 -0
  29. package/src/components/agents/agent-avatar.tsx +15 -1
  30. package/src/components/agents/agent-card.tsx +38 -6
  31. package/src/components/agents/agent-chat-list.tsx +79 -3
  32. package/src/components/agents/agent-sheet.tsx +191 -26
  33. package/src/components/auth/setup-wizard.tsx +268 -353
  34. package/src/components/chat/chat-area.tsx +24 -9
  35. package/src/components/chat/chat-header.tsx +48 -19
  36. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  37. package/src/components/chat/delegation-banner.test.ts +27 -0
  38. package/src/components/chat/delegation-banner.tsx +109 -23
  39. package/src/components/chat/message-bubble.tsx +17 -16
  40. package/src/components/chat/message-list.tsx +6 -5
  41. package/src/components/chat/streaming-bubble.tsx +3 -2
  42. package/src/components/chat/thinking-indicator.tsx +3 -2
  43. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  44. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  45. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  46. package/src/components/chatrooms/chatroom-message.tsx +165 -23
  47. package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
  48. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  49. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  50. package/src/components/connectors/connector-health.tsx +120 -0
  51. package/src/components/connectors/connector-list.tsx +1 -1
  52. package/src/components/connectors/connector-sheet.tsx +9 -0
  53. package/src/components/home/home-view.tsx +25 -3
  54. package/src/components/input/chat-input.tsx +8 -1
  55. package/src/components/knowledge/knowledge-list.tsx +1 -1
  56. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  57. package/src/components/layout/app-layout.tsx +35 -4
  58. package/src/components/memory/memory-agent-list.tsx +1 -1
  59. package/src/components/memory/memory-browser.tsx +1 -0
  60. package/src/components/memory/memory-card.tsx +3 -2
  61. package/src/components/memory/memory-detail.tsx +3 -3
  62. package/src/components/memory/memory-sheet.tsx +2 -2
  63. package/src/components/projects/project-detail.tsx +4 -4
  64. package/src/components/schedules/schedule-list.tsx +55 -9
  65. package/src/components/schedules/schedule-sheet.tsx +134 -23
  66. package/src/components/secrets/secret-sheet.tsx +1 -1
  67. package/src/components/secrets/secrets-list.tsx +1 -1
  68. package/src/components/sessions/session-card.tsx +1 -1
  69. package/src/components/shared/agent-picker-list.tsx +1 -1
  70. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  71. package/src/components/shared/command-palette.tsx +237 -0
  72. package/src/components/shared/connector-platform-icon.tsx +1 -0
  73. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  74. package/src/components/skills/skill-list.tsx +1 -1
  75. package/src/components/skills/skill-sheet.tsx +1 -1
  76. package/src/components/tasks/task-board.tsx +3 -3
  77. package/src/components/tasks/task-card.tsx +22 -2
  78. package/src/components/tasks/task-sheet.tsx +112 -17
  79. package/src/components/usage/metrics-dashboard.tsx +13 -25
  80. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  81. package/src/components/wallets/wallet-panel.tsx +616 -0
  82. package/src/components/wallets/wallet-section.tsx +100 -0
  83. package/src/hooks/use-swipe.ts +49 -0
  84. package/src/lib/providers/anthropic.ts +16 -2
  85. package/src/lib/providers/claude-cli.ts +7 -1
  86. package/src/lib/providers/index.ts +7 -0
  87. package/src/lib/providers/ollama.ts +16 -2
  88. package/src/lib/providers/openai.ts +7 -2
  89. package/src/lib/providers/openclaw.ts +6 -1
  90. package/src/lib/providers/provider-defaults.ts +7 -0
  91. package/src/lib/schedule-templates.ts +115 -0
  92. package/src/lib/server/agent-registry.ts +2 -2
  93. package/src/lib/server/alert-dispatch.ts +64 -0
  94. package/src/lib/server/chat-execution.ts +76 -4
  95. package/src/lib/server/chatroom-health.ts +60 -0
  96. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  97. package/src/lib/server/chatroom-helpers.ts +86 -12
  98. package/src/lib/server/chatroom-routing.ts +65 -0
  99. package/src/lib/server/connectors/discord.ts +3 -0
  100. package/src/lib/server/connectors/email.ts +267 -0
  101. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  102. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  103. package/src/lib/server/connectors/manager.ts +239 -5
  104. package/src/lib/server/connectors/openclaw.ts +3 -0
  105. package/src/lib/server/connectors/slack.ts +6 -0
  106. package/src/lib/server/connectors/telegram.ts +18 -0
  107. package/src/lib/server/connectors/types.ts +2 -0
  108. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  109. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  110. package/src/lib/server/connectors/whatsapp.ts +17 -5
  111. package/src/lib/server/cost.ts +70 -0
  112. package/src/lib/server/create-notification.ts +2 -0
  113. package/src/lib/server/daemon-state.ts +124 -0
  114. package/src/lib/server/dag-validation.ts +115 -0
  115. package/src/lib/server/memory-db.ts +12 -7
  116. package/src/lib/server/openclaw-doctor.ts +48 -0
  117. package/src/lib/server/orchestrator-lg.ts +12 -2
  118. package/src/lib/server/orchestrator.ts +6 -1
  119. package/src/lib/server/queue-followups.test.ts +224 -0
  120. package/src/lib/server/queue.ts +238 -24
  121. package/src/lib/server/scheduler.ts +3 -0
  122. package/src/lib/server/session-run-manager.ts +22 -1
  123. package/src/lib/server/session-tools/chatroom.ts +11 -2
  124. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  125. package/src/lib/server/session-tools/index.ts +8 -2
  126. package/src/lib/server/session-tools/memory.ts +23 -4
  127. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  128. package/src/lib/server/session-tools/shell.ts +1 -1
  129. package/src/lib/server/session-tools/wallet.ts +124 -0
  130. package/src/lib/server/session-tools/web.ts +2 -2
  131. package/src/lib/server/solana.ts +122 -0
  132. package/src/lib/server/storage.ts +158 -6
  133. package/src/lib/server/stream-agent-chat.ts +126 -63
  134. package/src/lib/server/task-mention.test.ts +41 -0
  135. package/src/lib/server/task-mention.ts +3 -2
  136. package/src/lib/setup-defaults.ts +277 -0
  137. package/src/lib/tool-definitions.ts +1 -0
  138. package/src/lib/validation/schemas.ts +69 -0
  139. package/src/lib/view-routes.ts +1 -0
  140. package/src/stores/use-app-store.ts +15 -3
  141. package/src/stores/use-chatroom-store.ts +52 -2
  142. package/src/types/index.ts +98 -2
  143. package/tsconfig.json +2 -1
package/src/cli/index.ts CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  import { Command } from 'commander'
4
4
  import { pathToFileURL } from 'node:url'
5
+ import {
6
+ SETUP_PROVIDERS,
7
+ DEFAULT_AGENTS,
8
+ STARTER_AGENT_TOOLS,
9
+ type SetupProvider,
10
+ } from '../lib/setup-defaults.ts'
5
11
 
6
12
  interface CliContext {
7
13
  baseUrl: string
@@ -9,8 +15,6 @@ interface CliContext {
9
15
  rawOutput: boolean
10
16
  }
11
17
 
12
- type SetupProvider = 'openai' | 'anthropic' | 'ollama' | 'openclaw'
13
-
14
18
  interface SetupAuthStatus {
15
19
  firstTime?: boolean
16
20
  key?: string
@@ -23,34 +27,7 @@ interface SetupProviderCheckResponse {
23
27
  recommendedModel?: string
24
28
  }
25
29
 
26
- const SUPPORTED_SETUP_PROVIDERS = new Set<SetupProvider>(['openai', 'anthropic', 'ollama', 'openclaw'])
27
-
28
- const DEFAULT_SETUP_AGENTS: Record<SetupProvider, { name: string; description: string; systemPrompt: string; model: string }> = {
29
- openai: {
30
- name: 'Assistant',
31
- description: 'A helpful GPT-powered assistant.',
32
- systemPrompt: 'You are a helpful, pragmatic assistant. Be concise, concrete, and action-oriented.',
33
- model: 'gpt-4o',
34
- },
35
- anthropic: {
36
- name: 'Assistant',
37
- description: 'A helpful Claude-powered assistant.',
38
- systemPrompt: 'You are a helpful, pragmatic assistant. Be concise, concrete, and action-oriented.',
39
- model: 'claude-sonnet-4-6',
40
- },
41
- ollama: {
42
- name: 'Assistant',
43
- description: 'A local assistant running through Ollama.',
44
- systemPrompt: 'You are a helpful, pragmatic assistant. Be concise, concrete, and action-oriented.',
45
- model: 'llama3',
46
- },
47
- openclaw: {
48
- name: 'OpenClaw Operator',
49
- description: 'A manager agent for talking to and coordinating OpenClaw instances.',
50
- systemPrompt: 'You are an operator focused on reliable execution, clear status updates, and task completion.',
51
- model: 'default',
52
- },
53
- }
30
+ const SUPPORTED_SETUP_PROVIDERS = new Set<SetupProvider>(SETUP_PROVIDERS.map((p) => p.id))
54
31
 
55
32
  const DEFAULT_BASE_URL =
56
33
  process.env.SWARMCLAW_URL
@@ -206,7 +183,8 @@ async function apiRequestWithAccessKey<T = unknown>(
206
183
  function normalizeSetupProvider(value: string | undefined): SetupProvider {
207
184
  const lower = (value || '').trim().toLowerCase()
208
185
  if (SUPPORTED_SETUP_PROVIDERS.has(lower as SetupProvider)) return lower as SetupProvider
209
- throw new Error(`Unsupported provider "${value}". Supported: openai, anthropic, ollama, openclaw`)
186
+ const supported = SETUP_PROVIDERS.map((p) => p.id).join(', ')
187
+ throw new Error(`Unsupported provider "${value}". Supported: ${supported}`)
210
188
  }
211
189
 
212
190
  function maskToken(value: string): string {
@@ -288,6 +266,201 @@ async function runWithHandler(command: Command, task: (ctx: CliContext) => Promi
288
266
  }
289
267
  }
290
268
 
269
+ async function readSecret(prompt: string): Promise<string> {
270
+ const { stdin, stdout } = process
271
+ stdout.write(prompt)
272
+ return new Promise((resolve) => {
273
+ let buf = ''
274
+ const wasRaw = stdin.isRaw
275
+ stdin.setRawMode?.(true)
276
+ stdin.resume()
277
+ stdin.setEncoding('utf8')
278
+ const onData = (ch: string) => {
279
+ if (ch === '\n' || ch === '\r') {
280
+ stdin.setRawMode?.(wasRaw ?? false)
281
+ stdin.pause()
282
+ stdin.removeListener('data', onData)
283
+ stdout.write('\n')
284
+ resolve(buf)
285
+ } else if (ch === '\u0003') {
286
+ // Ctrl+C
287
+ process.exit(130)
288
+ } else if (ch === '\u007f' || ch === '\b') {
289
+ buf = buf.slice(0, -1)
290
+ } else {
291
+ buf += ch
292
+ }
293
+ }
294
+ stdin.on('data', onData)
295
+ })
296
+ }
297
+
298
+ async function runInteractiveSetup(ctx: CliContext): Promise<unknown> {
299
+ const { createInterface } = await import('node:readline/promises')
300
+
301
+ const auth = await resolveSetupAccessKey(ctx)
302
+ const configuredProviders: string[] = []
303
+ const createdAgents: Array<{ name: string; provider: string; model: string }> = []
304
+
305
+ // Wraps readline so we can destroy/recreate after raw-mode readSecret
306
+ let rl = createInterface({ input: process.stdin, output: process.stdout })
307
+
308
+ const freshRl = () => {
309
+ try { rl.close() } catch { /* already closed */ }
310
+ rl = createInterface({ input: process.stdin, output: process.stdout })
311
+ }
312
+
313
+ const ask = async (question: string, defaultValue?: string): Promise<string> => {
314
+ const suffix = defaultValue ? ` (${defaultValue})` : ''
315
+ const answer = (await rl.question(`${question}${suffix}: `)).trim()
316
+ return answer || defaultValue || ''
317
+ }
318
+
319
+ const askYN = async (question: string, defaultYes: boolean): Promise<boolean> => {
320
+ const hint = defaultYes ? 'Y/n' : 'y/N'
321
+ const answer = (await rl.question(`${question} [${hint}]: `)).trim().toLowerCase()
322
+ if (!answer) return defaultYes
323
+ return answer.startsWith('y')
324
+ }
325
+
326
+ const askSecret = async (prompt: string): Promise<string> => {
327
+ rl.close()
328
+ const value = await readSecret(prompt)
329
+ freshRl()
330
+ return value
331
+ }
332
+
333
+ console.log('\n SwarmClaw Interactive Setup\n')
334
+
335
+ let addMore = true
336
+ while (addMore) {
337
+ console.log(' Available providers:\n')
338
+ const available = SETUP_PROVIDERS.filter((p) => !configuredProviders.includes(p.id))
339
+ if (available.length === 0) {
340
+ console.log(' All providers configured!\n')
341
+ break
342
+ }
343
+ available.forEach((p, i) => {
344
+ const badge = p.badge ? ` (${p.badge})` : ''
345
+ console.log(` ${i + 1}. ${p.name}${badge}`)
346
+ })
347
+ console.log()
348
+
349
+ const choiceStr = await ask('Pick a provider', '1')
350
+ const choiceNum = parseInt(choiceStr, 10)
351
+ const selected = (choiceNum >= 1 && choiceNum <= available.length)
352
+ ? available[choiceNum - 1]
353
+ : available.find((p) => p.id === choiceStr.toLowerCase() || p.name.toLowerCase() === choiceStr.toLowerCase())
354
+
355
+ if (!selected) {
356
+ console.log(` Invalid choice "${choiceStr}". Try again.\n`)
357
+ continue
358
+ }
359
+
360
+ const provider = selected.id
361
+ const defaults = DEFAULT_AGENTS[provider]
362
+ console.log(`\n Setting up ${selected.name}...\n`)
363
+
364
+ // Collect inputs
365
+ let inputApiKey = ''
366
+ if (selected.requiresKey) {
367
+ inputApiKey = await askSecret(' API key: ')
368
+ if (!inputApiKey) {
369
+ console.log(' API key is required for this provider.\n')
370
+ continue
371
+ }
372
+ } else if (selected.optionalKey) {
373
+ inputApiKey = await askSecret(' Token (optional, press Enter to skip): ')
374
+ }
375
+
376
+ let inputEndpoint = ''
377
+ if (selected.supportsEndpoint) {
378
+ inputEndpoint = await ask(' Endpoint', selected.defaultEndpoint)
379
+ }
380
+
381
+ const agentName = await ask(' Agent name', defaults.name)
382
+ const runCheck = await askYN(' Run connection check?', true)
383
+
384
+ // Connection check
385
+ let normalizedEndpoint = inputEndpoint || undefined
386
+ let selectedModel: string | undefined
387
+
388
+ if (runCheck) {
389
+ process.stdout.write(' Checking connection...')
390
+ try {
391
+ const check = await apiRequestWithAccessKey<SetupProviderCheckResponse>(
392
+ ctx, 'POST', '/setup/check-provider', auth.accessKey,
393
+ compactObject({
394
+ provider,
395
+ apiKey: inputApiKey || undefined,
396
+ endpoint: selected.supportsEndpoint ? normalizedEndpoint : undefined,
397
+ }),
398
+ )
399
+ if (check?.ok) {
400
+ console.log(' OK')
401
+ if (check.normalizedEndpoint) normalizedEndpoint = check.normalizedEndpoint
402
+ if (check.recommendedModel) selectedModel = check.recommendedModel
403
+ } else {
404
+ console.log(` FAILED: ${check?.message || 'Unknown error'}`)
405
+ }
406
+ } catch (err: unknown) {
407
+ console.log(` FAILED: ${err instanceof Error ? err.message : String(err)}`)
408
+ }
409
+ }
410
+
411
+ // Save credential
412
+ let credentialId: string | null = null
413
+ if (inputApiKey) {
414
+ const credential = await apiRequestWithAccessKey<{ id?: string }>(
415
+ ctx, 'POST', '/credentials', auth.accessKey,
416
+ { provider, name: `${selected.name} key`, apiKey: inputApiKey },
417
+ )
418
+ credentialId = typeof credential?.id === 'string' ? credential.id : null
419
+ }
420
+
421
+ // Create agent
422
+ await apiRequestWithAccessKey<Record<string, unknown>>(
423
+ ctx, 'POST', '/agents', auth.accessKey,
424
+ compactObject({
425
+ name: agentName || defaults.name,
426
+ description: defaults.description,
427
+ systemPrompt: defaults.systemPrompt,
428
+ provider,
429
+ model: selectedModel || defaults.model,
430
+ credentialId: credentialId || null,
431
+ apiEndpoint: selected.supportsEndpoint ? (normalizedEndpoint || undefined) : undefined,
432
+ tools: STARTER_AGENT_TOOLS,
433
+ }),
434
+ )
435
+
436
+ configuredProviders.push(provider)
437
+ createdAgents.push({ name: agentName || defaults.name, provider, model: selectedModel || defaults.model })
438
+ console.log(` Agent "${agentName || defaults.name}" created.\n`)
439
+
440
+ addMore = await askYN(' Add another provider?', false)
441
+ }
442
+
443
+ rl.close()
444
+
445
+ await apiRequestWithAccessKey(ctx, 'PUT', '/settings', auth.accessKey, { setupCompleted: true })
446
+
447
+ console.log('\n Setup complete!\n')
448
+ console.log(' Created agents:')
449
+ for (const a of createdAgents) {
450
+ console.log(` - ${a.name} (${a.provider}, ${a.model})`)
451
+ }
452
+ console.log()
453
+
454
+ return {
455
+ ok: true,
456
+ interactive: true,
457
+ providers: configuredProviders,
458
+ agents: createdAgents,
459
+ accessKeyMasked: maskToken(auth.accessKey),
460
+ firstTimeSetup: auth.firstTime,
461
+ }
462
+ }
463
+
291
464
  export function buildProgram(): Command {
292
465
  const program = new Command()
293
466
 
@@ -743,8 +916,8 @@ export function buildProgram(): Command {
743
916
  setup
744
917
  .command('init')
745
918
  .description('Run command-line first-time setup (provider check, credential, starter agent)')
746
- .option('--provider <provider>', 'Provider id (openai|anthropic|ollama|openclaw)', 'openai')
747
- .option('--api-key <apiKey>', 'API key or token (required for openai/anthropic)')
919
+ .option('--provider <provider>', 'Provider id (e.g. openai, anthropic, ollama, google)')
920
+ .option('--api-key <apiKey>', 'API key or token')
748
921
  .option('--endpoint <endpoint>', 'Provider endpoint override')
749
922
  .option('--model <model>', 'Model override')
750
923
  .option('--agent-name <name>', 'Starter agent name')
@@ -752,6 +925,7 @@ export function buildProgram(): Command {
752
925
  .option('--system-prompt <systemPrompt>', 'Starter agent system prompt')
753
926
  .option('--skip-check', 'Skip provider connection check')
754
927
  .option('--no-create-agent', 'Do not create a starter agent')
928
+ .option('--no-interactive', 'Disable interactive prompts (flag-only mode)')
755
929
  .action(async function (opts: {
756
930
  provider?: string
757
931
  apiKey?: string
@@ -762,12 +936,21 @@ export function buildProgram(): Command {
762
936
  systemPrompt?: string
763
937
  skipCheck?: boolean
764
938
  createAgent?: boolean
939
+ interactive?: boolean
765
940
  }) {
766
941
  await runWithHandler(this as Command, async (ctx) => {
767
- const provider = normalizeSetupProvider(opts.provider)
768
- const defaults = DEFAULT_SETUP_AGENTS[provider]
769
- const requiresApiKey = provider === 'openai' || provider === 'anthropic'
770
- const supportsEndpoint = provider === 'openai' || provider === 'ollama' || provider === 'openclaw'
942
+ const hasFlags = !!(opts.provider && opts.provider !== 'openai') || !!opts.apiKey || !!opts.endpoint
943
+ const wantInteractive = opts.interactive !== false && !hasFlags && process.stdin.isTTY
944
+
945
+ if (wantInteractive) {
946
+ return runInteractiveSetup(ctx)
947
+ }
948
+
949
+ const provider = normalizeSetupProvider(opts.provider || 'openai')
950
+ const defaults = DEFAULT_AGENTS[provider]
951
+ const meta = SETUP_PROVIDERS.find((p) => p.id === provider)
952
+ const requiresApiKey = meta?.requiresKey ?? false
953
+ const supportsEndpoint = meta?.supportsEndpoint ?? false
771
954
 
772
955
  const inputApiKey = (opts.apiKey || '').trim()
773
956
  const inputEndpoint = (opts.endpoint || '').trim()
@@ -807,7 +990,7 @@ export function buildProgram(): Command {
807
990
  }
808
991
 
809
992
  let credentialId: string | null = null
810
- if (inputApiKey && (provider === 'openai' || provider === 'anthropic' || provider === 'openclaw')) {
993
+ if (inputApiKey && (requiresApiKey || meta?.optionalKey)) {
811
994
  const credential = await apiRequestWithAccessKey<{ id?: string; name?: string }>(
812
995
  ctx,
813
996
  'POST',
@@ -815,7 +998,7 @@ export function buildProgram(): Command {
815
998
  auth.accessKey,
816
999
  {
817
1000
  provider,
818
- name: `${provider} key`,
1001
+ name: `${meta?.name || provider} key`,
819
1002
  apiKey: inputApiKey,
820
1003
  },
821
1004
  )
@@ -837,6 +1020,7 @@ export function buildProgram(): Command {
837
1020
  model: selectedModel || defaults.model,
838
1021
  credentialId: credentialId || null,
839
1022
  apiEndpoint: supportsEndpoint ? (normalizedEndpoint || undefined) : undefined,
1023
+ tools: STARTER_AGENT_TOOLS,
840
1024
  }),
841
1025
  )
842
1026
  }
package/src/cli/spec.js CHANGED
@@ -351,6 +351,20 @@ const COMMAND_GROUPS = {
351
351
  metrics: { description: 'Get task board metrics (supports --query range=24h|7d|30d)', method: 'GET', path: '/tasks/metrics' },
352
352
  },
353
353
  },
354
+ wallets: {
355
+ description: 'Agent wallet operations',
356
+ commands: {
357
+ list: { description: 'List wallets', method: 'GET', path: '/wallets' },
358
+ get: { description: 'Get wallet by id', method: 'GET', path: '/wallets/:id', params: ['id'] },
359
+ create: { description: 'Create wallet', method: 'POST', path: '/wallets' },
360
+ update: { description: 'Update wallet settings', method: 'PATCH', path: '/wallets/:id', params: ['id'] },
361
+ delete: { description: 'Delete wallet', method: 'DELETE', path: '/wallets/:id', params: ['id'] },
362
+ send: { description: 'Send funds from wallet', method: 'POST', path: '/wallets/:id/send', params: ['id'] },
363
+ approve: { description: 'Approve or deny pending wallet transaction', method: 'POST', path: '/wallets/:id/approve', params: ['id'] },
364
+ transactions: { description: 'List wallet transactions', method: 'GET', path: '/wallets/:id/transactions', params: ['id'] },
365
+ 'balance-history': { description: 'Get wallet balance history', method: 'GET', path: '/wallets/:id/balance-history', params: ['id'] },
366
+ },
367
+ },
354
368
  webhooks: {
355
369
  description: 'Inbound webhook triggers',
356
370
  commands: {
@@ -13,6 +13,7 @@ function sanitizeSvg(svg: string): string {
13
13
 
14
14
  interface Props {
15
15
  seed?: string | null
16
+ avatarUrl?: string | null
16
17
  name: string
17
18
  size?: number
18
19
  className?: string
@@ -27,7 +28,7 @@ const STATUS_COLORS: Record<string, string> = {
27
28
 
28
29
  const HEART_PATH = 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'
29
30
 
30
- export function AgentAvatar({ seed, name, size = 32, className = '', status, heartbeatPulse }: Props) {
31
+ export function AgentAvatar({ seed, avatarUrl, name, size = 32, className = '', status, heartbeatPulse }: Props) {
31
32
  const svgHtml = useMemo(() => {
32
33
  if (!seed) return null
33
34
  return sanitizeSvg(multiavatar(seed))
@@ -53,6 +54,19 @@ export function AgentAvatar({ seed, name, size = 32, className = '', status, hea
53
54
  </svg>
54
55
  ) : null
55
56
 
57
+ if (avatarUrl) {
58
+ return (
59
+ <div className={`relative shrink-0 ${className}`} style={{ width: size, height: size }}>
60
+ <div className="rounded-full overflow-hidden w-full h-full">
61
+ {/* eslint-disable-next-line @next/next/no-img-element */}
62
+ <img src={avatarUrl} alt={name} className="w-full h-full object-cover" draggable={false} />
63
+ </div>
64
+ {heartEl}
65
+ {dot}
66
+ </div>
67
+ )
68
+ }
69
+
56
70
  if (svgHtml) {
57
71
  return (
58
72
  <div className={`relative shrink-0 ${className}`} style={{ width: size, height: size }}>
@@ -6,7 +6,7 @@ import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { useWs } from '@/hooks/use-ws'
8
8
  import { api } from '@/lib/api-client'
9
- import { createAgent, deleteAgent } from '@/lib/agents'
9
+ import { deleteAgent } from '@/lib/agents'
10
10
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
11
11
  import {
12
12
  DropdownMenu,
@@ -79,11 +79,18 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
79
79
  setRunning(false)
80
80
  }
81
81
 
82
+ const [cloning, setCloning] = useState(false)
82
83
  const handleDuplicate = async () => {
83
- const { id: _id, createdAt: _ca, updatedAt: _ua, ...rest } = agent
84
- await createAgent({ ...rest, name: agent.name + ' (Copy)' })
85
- await loadAgents()
86
- toast.success('Agent duplicated')
84
+ setCloning(true)
85
+ try {
86
+ await api('POST', `/agents/${agent.id}/clone`)
87
+ await loadAgents()
88
+ toast.success('Agent duplicated')
89
+ } catch (err: unknown) {
90
+ toast.error(err instanceof Error ? err.message : String(err))
91
+ } finally {
92
+ setCloning(false)
93
+ }
87
94
  }
88
95
 
89
96
  const handleDelete = async () => {
@@ -140,7 +147,9 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
140
147
  <DropdownMenuItem onClick={() => { togglePinAgent(agent.id); toast.success(agent.pinned ? 'Agent unpinned' : 'Agent pinned') }}>
141
148
  {agent.pinned ? 'Unpin' : 'Pin'}
142
149
  </DropdownMenuItem>
143
- <DropdownMenuItem onClick={handleDuplicate}>Duplicate</DropdownMenuItem>
150
+ <DropdownMenuItem onClick={handleDuplicate} disabled={cloning}>
151
+ {cloning ? 'Duplicating...' : 'Duplicate'}
152
+ </DropdownMenuItem>
144
153
  {!isDefault && onSetDefault && (
145
154
  <DropdownMenuItem onClick={() => { onSetDefault(agent.id); toast.success(`${agent.name} set as default`) }}>Set Default</DropdownMenuItem>
146
155
  )}
@@ -157,6 +166,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
157
166
  <div className="flex items-center gap-2.5">
158
167
  <AgentAvatar
159
168
  seed={agent.avatarSeed}
169
+ avatarUrl={agent.avatarUrl}
160
170
  name={agent.name}
161
171
  size={28}
162
172
  status={isRunning ? 'busy' : isOnline ? 'online' : undefined}
@@ -216,6 +226,28 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
216
226
  <span>Cost: ${agent.totalCost.toFixed(2)}</span>
217
227
  )}
218
228
  </div>
229
+ {typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0 && (
230
+ <div className="mt-2">
231
+ <div className="flex items-center justify-between text-[10px] text-text-3/60 mb-1">
232
+ <span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${agent.monthlyBudget.toFixed(2)}</span>
233
+ <span className={`font-600 ${(agent.monthlySpend ?? 0) >= agent.monthlyBudget ? 'text-red-400' : 'text-text-3/50'}`}>
234
+ {agent.budgetAction === 'block' ? 'hard cap' : 'soft cap'}
235
+ </span>
236
+ </div>
237
+ <div className="h-1 rounded-full bg-white/[0.06] overflow-hidden">
238
+ <div
239
+ className={`h-full rounded-full transition-all duration-300 ${
240
+ (agent.monthlySpend ?? 0) >= agent.monthlyBudget
241
+ ? 'bg-red-400'
242
+ : (agent.monthlySpend ?? 0) >= agent.monthlyBudget * 0.8
243
+ ? 'bg-amber-400'
244
+ : 'bg-accent'
245
+ }`}
246
+ style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) / agent.monthlyBudget) * 100)}%` }}
247
+ />
248
+ </div>
249
+ </div>
250
+ )}
219
251
  </div>
220
252
 
221
253
  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -5,6 +5,8 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useChatStore } from '@/stores/use-chat-store'
6
6
  import { useChatroomStore } from '@/stores/use-chatroom-store'
7
7
  import { fetchMessages } from '@/lib/sessions'
8
+ import { api } from '@/lib/api-client'
9
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
8
10
  import type { Agent, Session } from '@/types'
9
11
  import { AgentAvatar } from './agent-avatar'
10
12
  import { toast } from 'sonner'
@@ -32,6 +34,39 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
32
34
  const chatrooms = useChatroomStore((s) => s.chatrooms)
33
35
  const chatroomStreaming = useChatroomStore((s) => s.streamingAgents)
34
36
  const [search, setSearch] = useState('')
37
+ const [bulkMode, setBulkMode] = useState(false)
38
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
39
+ const [confirmBulkDelete, setConfirmBulkDelete] = useState(false)
40
+ const loadSessions = useAppStore((s) => s.loadSessions)
41
+
42
+ const toggleSelected = useCallback((id: string) => {
43
+ setSelectedIds((prev) => {
44
+ const next = new Set(prev)
45
+ if (next.has(id)) next.delete(id)
46
+ else next.add(id)
47
+ return next
48
+ })
49
+ }, [])
50
+
51
+ const handleBulkDelete = useCallback(async () => {
52
+ // Collect session IDs for selected agents
53
+ const sessionIds = [...selectedIds]
54
+ .map((agentId) => {
55
+ const agent = agents[agentId]
56
+ return agent?.threadSessionId
57
+ })
58
+ .filter(Boolean) as string[]
59
+ if (!sessionIds.length) { toast.error('No chats to delete'); return }
60
+ try {
61
+ await api('DELETE', '/sessions', { ids: sessionIds })
62
+ await loadSessions()
63
+ toast.success(`Deleted ${sessionIds.length} chat(s)`)
64
+ setBulkMode(false)
65
+ setSelectedIds(new Set())
66
+ } catch {
67
+ toast.error('Failed to delete chats')
68
+ }
69
+ }, [selectedIds, agents, loadSessions])
35
70
 
36
71
  // FLIP animation refs
37
72
  const rowRefs = useRef<Map<string, HTMLElement>>(new Map())
@@ -169,7 +204,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
169
204
 
170
205
  return (
171
206
  <div className="flex-1 overflow-y-auto">
172
- {/* Filter control */}
207
+ {/* Filter control + bulk mode toggle */}
173
208
  {sortedAgents.length > 2 && (
174
209
  <div className="flex items-center gap-1 px-4 pt-2.5 pb-1">
175
210
  {(['all', 'active', 'recent'] as const).map((f) => (
@@ -185,6 +220,28 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
185
220
  {f}
186
221
  </button>
187
222
  ))}
223
+ <button
224
+ type="button"
225
+ onClick={() => { setBulkMode(!bulkMode); setSelectedIds(new Set()) }}
226
+ aria-label={bulkMode ? 'Exit selection mode' : 'Select chats'}
227
+ className={`ml-auto label-mono px-2.5 py-1 rounded-[6px] border-none cursor-pointer transition-colors
228
+ ${bulkMode ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
229
+ >
230
+ {bulkMode ? 'Cancel' : 'Select'}
231
+ </button>
232
+ </div>
233
+ )}
234
+ {/* Bulk action bar */}
235
+ {bulkMode && selectedIds.size > 0 && (
236
+ <div className="flex items-center gap-2 px-4 py-2 bg-white/[0.02] border-b border-white/[0.04]">
237
+ <span className="text-[12px] text-text-2 font-500 flex-1">{selectedIds.size} selected</span>
238
+ <button
239
+ onClick={() => setConfirmBulkDelete(true)}
240
+ className="px-3 py-1.5 rounded-[8px] border-none bg-red-500/10 text-red-400 text-[12px] font-600 cursor-pointer hover:bg-red-500/20 transition-colors"
241
+ style={{ fontFamily: 'inherit' }}
242
+ >
243
+ Delete
244
+ </button>
188
245
  </div>
189
246
  )}
190
247
  {(sortedAgents.length > 5 || search) && (
@@ -219,11 +276,21 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
219
276
  ${isActive
220
277
  ? 'bg-accent-soft/80 border border-accent-bright/20'
221
278
  : 'bg-transparent hover:bg-white/[0.02]'}`}
222
- onClick={() => handleSelect(agent)}
279
+ onClick={() => bulkMode ? toggleSelected(agent.id) : handleSelect(agent)}
223
280
  >
224
281
  <div className="flex items-center gap-2.5">
282
+ {bulkMode && (
283
+ <div className={`w-5 h-5 rounded-[6px] border-2 flex items-center justify-center shrink-0 transition-colors
284
+ ${selectedIds.has(agent.id) ? 'bg-accent-bright border-accent-bright' : 'border-white/20 bg-transparent'}`}>
285
+ {selectedIds.has(agent.id) && (
286
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
287
+ <polyline points="20 6 9 17 4 12" />
288
+ </svg>
289
+ )}
290
+ </div>
291
+ )}
225
292
  <div className="relative shrink-0">
226
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={36} />
293
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={36} />
227
294
  <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-bg ${
228
295
  isWorking ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]' : 'bg-text-3/30'
229
296
  }`} />
@@ -303,6 +370,15 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
303
370
  )
304
371
  })}
305
372
  </div>
373
+ <ConfirmDialog
374
+ open={confirmBulkDelete}
375
+ title="Delete Chats"
376
+ message={`Delete ${selectedIds.size} chat(s)? This cannot be undone.`}
377
+ confirmLabel="Delete"
378
+ danger
379
+ onConfirm={() => { setConfirmBulkDelete(false); handleBulkDelete() }}
380
+ onCancel={() => setConfirmBulkDelete(false)}
381
+ />
306
382
  </div>
307
383
  )
308
384
  }