@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.
- package/README.md +57 -27
- package/package.json +6 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +17 -1
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +19 -1
- package/src/app/api/chatrooms/route.ts +12 -2
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +20 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/cli/index.js +5 -0
- package/src/cli/index.ts +223 -39
- package/src/components/agents/agent-card.tsx +37 -6
- package/src/components/agents/agent-chat-list.tsx +78 -2
- package/src/components/agents/agent-sheet.tsx +79 -0
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +22 -7
- package/src/components/chat/message-bubble.tsx +14 -14
- package/src/components/chat/message-list.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +164 -22
- package/src/components/chatrooms/chatroom-sheet.tsx +288 -3
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +23 -2
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/layout/app-layout.tsx +17 -1
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +91 -16
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +41 -1
- package/src/lib/server/chatroom-helpers.ts +22 -1
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/manager.ts +159 -3
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +9 -0
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/queue.ts +12 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +22 -3
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/storage.ts +120 -6
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/stores/use-app-store.ts +7 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +38 -1
- 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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
129
|
+
// When a task is completed, cascade unblock dependent tasks
|
|
118
130
|
if (tasks[id].status === 'completed') {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
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;
|
|
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
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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>(
|
|
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
|
-
|
|
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
|
|
747
|
-
.option('--api-key <apiKey>', 'API key or token
|
|
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
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
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 && (
|
|
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
|
}
|