@swarmclawai/swarmclaw 0.6.6 → 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 (80) hide show
  1. package/README.md +57 -27
  2. package/package.json +6 -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 +17 -1
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +19 -1
  8. package/src/app/api/chatrooms/route.ts +12 -2
  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/sessions/[id]/chat/route.ts +5 -1
  14. package/src/app/api/sessions/route.ts +11 -2
  15. package/src/app/api/tasks/[id]/route.ts +18 -13
  16. package/src/app/api/tasks/route.ts +20 -1
  17. package/src/app/api/usage/route.ts +16 -7
  18. package/src/cli/index.js +5 -0
  19. package/src/cli/index.ts +223 -39
  20. package/src/components/agents/agent-card.tsx +37 -6
  21. package/src/components/agents/agent-chat-list.tsx +78 -2
  22. package/src/components/agents/agent-sheet.tsx +79 -0
  23. package/src/components/auth/setup-wizard.tsx +268 -353
  24. package/src/components/chat/chat-area.tsx +22 -7
  25. package/src/components/chat/message-bubble.tsx +14 -14
  26. package/src/components/chat/message-list.tsx +1 -1
  27. package/src/components/chatrooms/chatroom-message.tsx +164 -22
  28. package/src/components/chatrooms/chatroom-sheet.tsx +288 -3
  29. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  30. package/src/components/connectors/connector-health.tsx +120 -0
  31. package/src/components/connectors/connector-sheet.tsx +9 -0
  32. package/src/components/home/home-view.tsx +23 -2
  33. package/src/components/input/chat-input.tsx +8 -1
  34. package/src/components/layout/app-layout.tsx +17 -1
  35. package/src/components/schedules/schedule-list.tsx +55 -9
  36. package/src/components/schedules/schedule-sheet.tsx +134 -23
  37. package/src/components/shared/command-palette.tsx +237 -0
  38. package/src/components/shared/connector-platform-icon.tsx +1 -0
  39. package/src/components/tasks/task-card.tsx +22 -2
  40. package/src/components/tasks/task-sheet.tsx +91 -16
  41. package/src/components/usage/metrics-dashboard.tsx +13 -25
  42. package/src/hooks/use-swipe.ts +49 -0
  43. package/src/lib/providers/anthropic.ts +16 -2
  44. package/src/lib/providers/claude-cli.ts +7 -1
  45. package/src/lib/providers/index.ts +7 -0
  46. package/src/lib/providers/ollama.ts +16 -2
  47. package/src/lib/providers/openai.ts +7 -2
  48. package/src/lib/providers/openclaw.ts +6 -1
  49. package/src/lib/providers/provider-defaults.ts +7 -0
  50. package/src/lib/schedule-templates.ts +115 -0
  51. package/src/lib/server/alert-dispatch.ts +64 -0
  52. package/src/lib/server/chat-execution.ts +41 -1
  53. package/src/lib/server/chatroom-helpers.ts +22 -1
  54. package/src/lib/server/chatroom-routing.ts +65 -0
  55. package/src/lib/server/connectors/discord.ts +3 -0
  56. package/src/lib/server/connectors/email.ts +267 -0
  57. package/src/lib/server/connectors/manager.ts +159 -3
  58. package/src/lib/server/connectors/openclaw.ts +3 -0
  59. package/src/lib/server/connectors/slack.ts +6 -0
  60. package/src/lib/server/connectors/telegram.ts +18 -0
  61. package/src/lib/server/connectors/types.ts +2 -0
  62. package/src/lib/server/connectors/whatsapp.ts +9 -0
  63. package/src/lib/server/cost.ts +70 -0
  64. package/src/lib/server/create-notification.ts +2 -0
  65. package/src/lib/server/daemon-state.ts +124 -0
  66. package/src/lib/server/dag-validation.ts +115 -0
  67. package/src/lib/server/memory-db.ts +12 -7
  68. package/src/lib/server/openclaw-doctor.ts +48 -0
  69. package/src/lib/server/queue.ts +12 -0
  70. package/src/lib/server/session-run-manager.ts +22 -1
  71. package/src/lib/server/session-tools/index.ts +2 -0
  72. package/src/lib/server/session-tools/memory.ts +22 -3
  73. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  74. package/src/lib/server/storage.ts +120 -6
  75. package/src/lib/setup-defaults.ts +277 -0
  76. package/src/lib/validation/schemas.ts +69 -0
  77. package/src/stores/use-app-store.ts +7 -3
  78. package/src/stores/use-chatroom-store.ts +52 -2
  79. package/src/types/index.ts +38 -1
  80. package/tsconfig.json +2 -1
@@ -25,7 +25,7 @@ export async function POST(req: Request) {
25
25
  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
26
26
  }
27
27
 
28
- const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
28
+ const { title, content, tags, scope, agentIds, source, sourceUrl } = body as Record<string, unknown>
29
29
 
30
30
  if (typeof title !== 'string' || !title.trim()) {
31
31
  return NextResponse.json({ error: 'title is required.' }, { status: 400 })
@@ -43,12 +43,17 @@ export async function POST(req: Request) {
43
43
  ? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
44
44
  : []
45
45
 
46
+ const normalizedSource = typeof source === 'string' && source.trim() ? source.trim() : undefined
47
+ const normalizedSourceUrl = typeof sourceUrl === 'string' && sourceUrl.trim() ? sourceUrl.trim() : undefined
48
+
46
49
  const entry = addKnowledge({
47
50
  title: title.trim(),
48
51
  content,
49
52
  tags: normalizedTags,
50
53
  scope: normalizedScope,
51
54
  agentIds: normalizedAgentIds,
55
+ source: normalizedSource,
56
+ sourceUrl: normalizedSourceUrl,
52
57
  })
53
58
 
54
59
  return NextResponse.json(entry)
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { runOpenClawDoctor } from '@/lib/server/openclaw-doctor'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET() {
7
+ const result = await runOpenClawDoctor()
8
+ return NextResponse.json(result)
9
+ }
10
+
11
+ export async function POST(req: Request) {
12
+ const body = await req.json().catch(() => ({}))
13
+ const fix = typeof body.fix === 'boolean' ? body.fix : false
14
+ const workspace = typeof body.workspace === 'string' ? body.workspace : undefined
15
+ const result = await runOpenClawDoctor({ fix, workspace })
16
+ return NextResponse.json(result)
17
+ }
@@ -25,6 +25,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
25
25
  }
26
26
 
27
27
  const encoder = new TextEncoder()
28
+ let abortRun: (() => void) | null = null
28
29
  const stream = new ReadableStream({
29
30
  start(controller) {
30
31
  let closed = false
@@ -48,7 +49,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
48
49
  mode: queueMode,
49
50
  onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
50
51
  replyToId,
52
+ callerSignal: req.signal,
51
53
  })
54
+ abortRun = run.abort
52
55
 
53
56
  log.info('chat', `Enqueued session run ${run.runId}`, {
54
57
  sessionId: id,
@@ -86,7 +89,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
86
89
  })
87
90
  },
88
91
  cancel() {
89
- // Client disconnected; subsequent writes should be ignored.
92
+ // Client disconnected abort the run so the LLM stream is cancelled.
93
+ abortRun?.()
90
94
  },
91
95
  })
92
96
 
@@ -11,7 +11,7 @@ import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main
11
11
  export const dynamic = 'force-dynamic'
12
12
 
13
13
 
14
- export async function GET(_req: Request) {
14
+ export async function GET(req: Request) {
15
15
  const sessions = loadSessions()
16
16
  for (const id of Object.keys(sessions)) {
17
17
  const run = getSessionRunState(id)
@@ -19,7 +19,16 @@ export async function GET(_req: Request) {
19
19
  sessions[id].queuedCount = run.queueLength
20
20
  sessions[id].currentRunId = run.runningRunId || null
21
21
  }
22
- return NextResponse.json(sessions)
22
+
23
+ const { searchParams } = new URL(req.url)
24
+ const limitParam = searchParams.get('limit')
25
+ if (!limitParam) return NextResponse.json(sessions)
26
+
27
+ const limit = Math.max(1, Number(limitParam) || 50)
28
+ const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
29
+ const all = Object.values(sessions).sort((a, b) => (b.lastActiveAt ?? b.createdAt) - (a.lastActiveAt ?? a.createdAt))
30
+ const items = all.slice(offset, offset + limit)
31
+ return NextResponse.json({ items, total: all.length, hasMore: offset + limit < all.length })
23
32
  }
24
33
 
25
34
  export async function DELETE(req: Request) {
@@ -10,6 +10,7 @@ import { notify } from '@/lib/server/ws-hub'
10
10
  import { createNotification } from '@/lib/server/create-notification'
11
11
  import { enqueueSystemEvent } from '@/lib/server/system-events'
12
12
  import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
+ import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
13
14
 
14
15
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
15
16
  // Keep completed queue integrity even if daemon is not running.
@@ -29,6 +30,17 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
29
30
 
30
31
  const prevStatus = tasks[id].status
31
32
 
33
+ // DAG validation: reject if proposed blockedBy would create a cycle
34
+ if (Array.isArray(body.blockedBy)) {
35
+ const dagResult = validateDag(tasks, id, body.blockedBy)
36
+ if (!dagResult.valid) {
37
+ return NextResponse.json(
38
+ { error: 'Dependency cycle detected', cycle: dagResult.cycle },
39
+ { status: 400 },
40
+ )
41
+ }
42
+ }
43
+
32
44
  // Support atomic comment append to avoid race conditions
33
45
  if (body.appendComment) {
34
46
  if (!tasks[id].comments) tasks[id].comments = []
@@ -114,20 +126,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
114
126
  }
115
127
  }
116
128
 
117
- // When a task is completed, auto-unblock dependent tasks
129
+ // When a task is completed, cascade unblock dependent tasks
118
130
  if (tasks[id].status === 'completed') {
119
- const blockedIds = Array.isArray(tasks[id].blocks) ? tasks[id].blocks as string[] : []
120
- for (const blockedId of blockedIds) {
121
- const blocked = tasks[blockedId]
122
- if (!blocked) continue
123
- const deps = Array.isArray(blocked.blockedBy) ? blocked.blockedBy as string[] : []
124
- const allDone = deps.every((depId: string) => tasks[depId]?.status === 'completed')
125
- if (allDone && (blocked.status === 'backlog' || blocked.status === 'todo')) {
126
- blocked.status = 'queued'
127
- blocked.queuedAt = Date.now()
128
- blocked.updatedAt = Date.now()
129
- saveTasks(tasks)
130
- enqueueTask(blockedId)
131
+ const unblockedIds = cascadeUnblock(tasks, id)
132
+ if (unblockedIds.length > 0) {
133
+ saveTasks(tasks)
134
+ for (const uid of unblockedIds) {
135
+ enqueueTask(uid)
131
136
  }
132
137
  }
133
138
  }
@@ -1,6 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
4
+ import { TaskCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
+ import { z } from 'zod'
4
6
  import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
7
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
8
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
@@ -8,6 +10,7 @@ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
10
  import { notify } from '@/lib/server/ws-hub'
9
11
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
10
12
  import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
13
+ import { validateDag } from '@/lib/server/dag-validation'
11
14
 
12
15
  export async function GET(req: Request) {
13
16
  // Keep completed queue integrity even if daemon is not running.
@@ -55,7 +58,12 @@ export async function DELETE(req: Request) {
55
58
  }
56
59
 
57
60
  export async function POST(req: Request) {
58
- const body = await req.json()
61
+ const raw = await req.json()
62
+ const parsed = TaskCreateSchema.safeParse(raw)
63
+ if (!parsed.success) {
64
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
65
+ }
66
+ const body = { ...raw, ...parsed.data }
59
67
  const id = genId()
60
68
  const now = Date.now()
61
69
  const tasks = loadTasks()
@@ -66,6 +74,17 @@ export async function POST(req: Request) {
66
74
  const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
67
75
  ? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
68
76
  : Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
77
+ // DAG validation: reject if proposed blockedBy would create a cycle
78
+ if (Array.isArray(body.blockedBy) && body.blockedBy.length > 0) {
79
+ const dagResult = validateDag(tasks, id, body.blockedBy)
80
+ if (!dagResult.valid) {
81
+ return NextResponse.json(
82
+ { error: 'Dependency cycle detected', cycle: dagResult.cycle },
83
+ { status: 400 },
84
+ )
85
+ }
86
+ }
87
+
69
88
  // Resolve @mentions in description to auto-assign agent
70
89
  const resolvedAgentId = body.description
71
90
  ? resolveTaskAgentFromDescription(body.description, body.agentId || '', loadAgents())
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadUsage } from '@/lib/server/storage'
2
+ import { loadUsage, loadSessions, loadAgents } from '@/lib/server/storage'
3
3
  import type { UsageRecord } from '@/types'
4
4
  export const dynamic = 'force-dynamic'
5
5
 
@@ -41,10 +41,14 @@ export async function GET(req: Request) {
41
41
  }
42
42
  }
43
43
 
44
+ // Build session→agent lookup
45
+ const sessions = loadSessions() as Record<string, { agentId?: string }>
46
+ const agents = loadAgents() as Record<string, { name?: string }>
47
+
44
48
  // Compute summaries
45
49
  let totalTokens = 0
46
50
  let totalCost = 0
47
- const byAgent: Record<string, { tokens: number; cost: number }> = {}
51
+ const byAgent: Record<string, { name: string; cost: number; tokens: number; count: number }> = {}
48
52
  const byProvider: Record<string, { tokens: number; cost: number }> = {}
49
53
  const bucketMap: Record<string, { tokens: number; cost: number }> = {}
50
54
 
@@ -60,11 +64,16 @@ export async function GET(req: Request) {
60
64
  byProvider[prov].tokens += tokens
61
65
  byProvider[prov].cost += cost
62
66
 
63
- // by agent (using sessionId as proxy agents map to sessions)
64
- const agentKey = r.sessionId || 'unknown'
65
- if (!byAgent[agentKey]) byAgent[agentKey] = { tokens: 0, cost: 0 }
66
- byAgent[agentKey].tokens += tokens
67
- byAgent[agentKey].cost += cost
67
+ // by agent resolve sessionId agentId agent name
68
+ const session = r.sessionId ? sessions[r.sessionId] : undefined
69
+ const agentId = session?.agentId || 'unknown'
70
+ const agentName = agentId !== 'unknown' && agents[agentId]?.name
71
+ ? agents[agentId].name
72
+ : agentId
73
+ if (!byAgent[agentId]) byAgent[agentId] = { name: agentName, cost: 0, tokens: 0, count: 0 }
74
+ byAgent[agentId].cost += cost
75
+ byAgent[agentId].tokens += tokens
76
+ byAgent[agentId].count += 1
68
77
 
69
78
  // time series bucketing
70
79
  const bk = bucketKey(r.timestamp || now, range)
package/src/cli/index.js CHANGED
@@ -21,6 +21,7 @@ const COMMAND_GROUPS = [
21
21
  cmd('restore', 'POST', '/agents/trash', 'Restore a trashed agent', { expectsJsonBody: true }),
22
22
  cmd('purge', 'DELETE', '/agents/trash', 'Permanently delete a trashed agent', { expectsJsonBody: true }),
23
23
  cmd('thread', 'POST', '/agents/:id/thread', 'Get or create agent thread session'),
24
+ cmd('clone', 'POST', '/agents/:id/clone', 'Clone an agent'),
24
25
  ],
25
26
  },
26
27
  {
@@ -77,6 +78,7 @@ const COMMAND_GROUPS = [
77
78
  cmd('pin', 'POST', '/chatrooms/:id/pins', 'Toggle pin on a chatroom message', {
78
79
  expectsJsonBody: true,
79
80
  }),
81
+ cmd('moderate', 'POST', '/chatrooms/:id/moderate', 'Run moderation action on a chatroom', { expectsJsonBody: true }),
80
82
  ],
81
83
  },
82
84
  {
@@ -109,6 +111,7 @@ const COMMAND_GROUPS = [
109
111
  expectsJsonBody: true,
110
112
  defaultBody: { action: 'repair' },
111
113
  }),
114
+ cmd('health', 'GET', '/connectors/:id/health', 'Get connector health status'),
112
115
  ],
113
116
  },
114
117
  {
@@ -289,6 +292,8 @@ const COMMAND_GROUPS = [
289
292
  cmd('skills-install', 'POST', '/openclaw/skills/install', 'Install OpenClaw skill dependencies', { expectsJsonBody: true }),
290
293
  cmd('skills-remove', 'POST', '/openclaw/skills/remove', 'Remove OpenClaw skill', { expectsJsonBody: true }),
291
294
  cmd('sync', 'POST', '/openclaw/sync', 'Run OpenClaw sync action', { expectsJsonBody: true }),
295
+ cmd('doctor', 'GET', '/openclaw/doctor', 'Run OpenClaw doctor check (read-only)'),
296
+ cmd('doctor-fix', 'POST', '/openclaw/doctor', 'Run OpenClaw doctor with auto-fix', { expectsJsonBody: true }),
292
297
  ],
293
298
  },
294
299
  {
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
  }