@swarmclawai/swarmclaw 1.5.3 → 1.5.33

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 (78) hide show
  1. package/README.md +167 -147
  2. package/bin/swarmclaw.js +35 -1
  3. package/package.json +2 -2
  4. package/scripts/run-next-build.mjs +87 -2
  5. package/skills/swarmclaw/SKILL.md +151 -0
  6. package/src/app/api/agents/[id]/route.ts +8 -0
  7. package/src/app/api/agents/agents-route.test.ts +114 -0
  8. package/src/app/api/agents/route.ts +10 -0
  9. package/src/app/api/connectors/route.ts +25 -13
  10. package/src/app/api/credentials/[id]/route.ts +8 -1
  11. package/src/app/api/schedules/[id]/route.ts +8 -0
  12. package/src/app/api/secrets/[id]/route.ts +10 -0
  13. package/src/app/api/setup/check-provider/route.ts +45 -0
  14. package/src/app/api/setup/doctor/route.ts +5 -0
  15. package/src/cli/binary.test.js +11 -0
  16. package/src/cli/index.js +4 -4
  17. package/src/cli/index.test.js +5 -2
  18. package/src/cli/index.ts +1 -1
  19. package/src/components/agents/agent-sheet.tsx +16 -4
  20. package/src/components/agents/inspector-panel.tsx +5 -0
  21. package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
  22. package/src/components/chat/activity-moment.tsx +3 -0
  23. package/src/components/chat/chat-header.tsx +23 -2
  24. package/src/components/chat/tool-call-bubble.tsx +20 -0
  25. package/src/components/layout/sidebar-rail.tsx +29 -0
  26. package/src/hooks/setup-done-detection.test.ts +4 -2
  27. package/src/hooks/setup-done-detection.ts +1 -1
  28. package/src/lib/orchestrator-config.ts +5 -0
  29. package/src/lib/provider-sets.ts +4 -4
  30. package/src/lib/providers/acp-client.ts +116 -0
  31. package/src/lib/providers/cli-utils.test.ts +9 -1
  32. package/src/lib/providers/cli-utils.ts +89 -4
  33. package/src/lib/providers/cursor-cli.ts +172 -0
  34. package/src/lib/providers/goose.ts +149 -0
  35. package/src/lib/providers/index.ts +29 -1
  36. package/src/lib/providers/qwen-code-cli.ts +152 -0
  37. package/src/lib/server/agents/agent-availability.ts +2 -2
  38. package/src/lib/server/agents/agent-thread-session.ts +8 -0
  39. package/src/lib/server/agents/task-session.ts +8 -0
  40. package/src/lib/server/capability-router.ts +8 -2
  41. package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
  42. package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
  43. package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
  44. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +3 -0
  45. package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
  46. package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
  47. package/src/lib/server/chat-execution/prompt-sections.ts +3 -1
  48. package/src/lib/server/chat-execution/stream-agent-chat.ts +5 -0
  49. package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
  50. package/src/lib/server/chats/chat-session-service.ts +18 -0
  51. package/src/lib/server/connectors/session.ts +8 -0
  52. package/src/lib/server/context-manager.ts +5 -0
  53. package/src/lib/server/provider-health.ts +13 -3
  54. package/src/lib/server/provider-model-discovery.test.ts +8 -0
  55. package/src/lib/server/provider-model-discovery.ts +1 -1
  56. package/src/lib/server/runtime/daemon-state/core.ts +2 -2
  57. package/src/lib/server/runtime/queue/core.ts +30 -4
  58. package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
  59. package/src/lib/server/session-reset-policy.test.ts +16 -0
  60. package/src/lib/server/session-reset-policy.ts +9 -1
  61. package/src/lib/server/session-tools/context.ts +2 -2
  62. package/src/lib/server/session-tools/delegate.ts +334 -14
  63. package/src/lib/server/session-tools/index.ts +5 -2
  64. package/src/lib/server/session-tools/session-info.ts +4 -1
  65. package/src/lib/server/storage-auth-docker.test.ts +244 -0
  66. package/src/lib/server/storage-auth.test.ts +150 -0
  67. package/src/lib/server/storage-auth.ts +57 -22
  68. package/src/lib/server/storage-normalization.ts +19 -0
  69. package/src/lib/server/storage.ts +3 -0
  70. package/src/lib/server/tasks/task-resume.ts +23 -1
  71. package/src/lib/server/tool-aliases.ts +1 -1
  72. package/src/lib/server/tool-capability-policy.ts +4 -1
  73. package/src/lib/setup-defaults.test.ts +6 -0
  74. package/src/lib/setup-defaults.ts +146 -0
  75. package/src/lib/tool-definitions.ts +1 -1
  76. package/src/types/misc.ts +4 -1
  77. package/src/types/provider.ts +1 -1
  78. package/src/types/session.ts +9 -0
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: swarmclaw
3
+ description: AI agent runtime and multi-agent orchestration platform. Teaches agents how to use SwarmClaw's 6 primitive tools, persistent memory, dreaming, delegation, connectors, credentials, and the skill system. Use when an agent is running on SwarmClaw and needs to understand the platform's capabilities.
4
+ metadata:
5
+ openclaw:
6
+ emoji: "\U0001F41D"
7
+ privacyPolicy: All data stays on the local SwarmClaw instance. Memory, workspace files, and session data are stored on the host machine. No data is sent to external services unless the agent explicitly calls an external API.
8
+ dataHandling: Agent memory is stored locally in the SwarmClaw data directory. Workspace files are scoped per agent. Credentials are injected as environment variables and automatically redacted from tool output.
9
+ version: 2.4.1
10
+ author: swarmclawai
11
+ homepage: https://swarmclaw.ai
12
+ tags: [agents, orchestration, multi-agent, runtime, memory, delegation, skills, connectors, dreaming]
13
+ ---
14
+
15
+ # SwarmClaw Platform
16
+
17
+ SwarmClaw is an AI agent runtime and multi-agent orchestration platform. It gives agents a uniform set of tools, persistent memory, connector integrations, and the ability to delegate work to other agents.
18
+
19
+ Website: https://swarmclaw.ai
20
+ Docs: https://swarmclaw.ai/docs
21
+ GitHub: https://github.com/swarmclawai/swarmclaw
22
+ npm: `npm install -g swarmclaw`
23
+
24
+ ## The 6 Primitive Tools
25
+
26
+ Every agent has access to these core tools. They cover the full range of agent capabilities.
27
+
28
+ | Tool | Purpose | When to Use |
29
+ |------|---------|-------------|
30
+ | **files** | Read, write, edit, list, search files | Any file operation on the workspace filesystem |
31
+ | **execute** | Run bash scripts (sandboxed or host) | Shell commands, curl, data processing, package management |
32
+ | **memory** | Store and retrieve persistent knowledge | Facts, preferences, decisions that should survive across sessions |
33
+ | **platform** | Tasks, communication, delegation, projects | Coordinating with humans and other agents |
34
+ | **browser** | Control a headless browser | Interactive web pages, JavaScript-rendered content |
35
+ | **skills** | Discover and load skill documentation | Learning how to use tools, APIs, or workflows |
36
+
37
+ ### Tool Selection Guide
38
+
39
+ | Task | Tool |
40
+ |------|------|
41
+ | Edit a source file | `files` (edit action) |
42
+ | Run tests | `execute` |
43
+ | Call a REST API (JSON) | `execute` (curl) |
44
+ | Scrape a dynamic web page | `browser` |
45
+ | Remember a user preference | `memory` |
46
+ | Ask the user a question | `platform` (communicate.ask_human) |
47
+ | Send a Slack message | `platform` (communicate.send_message) |
48
+ | Hand off work to another agent | `platform` (communicate.delegate) |
49
+ | Find out how a tool works | `skills` (read action) |
50
+
51
+ ## Credentials
52
+
53
+ Credentials are configured per agent in the SwarmClaw UI. They are:
54
+
55
+ - **Injected as environment variables** into `execute` tool runs (e.g., `$OPENAI_API_KEY`, `$GITHUB_TOKEN`)
56
+ - **Automatically redacted** from all tool output -- secrets never appear in chat history
57
+ - **Named by convention**: `<PROVIDER>_API_KEY` or custom names set in the credential config
58
+
59
+ You never need to ask the user for API keys directly. If a credential is configured, it's available as an env var. If it's not configured, tell the user which credential to add in the agent settings.
60
+
61
+ ## The Skill System
62
+
63
+ Skills are markdown files that teach agents how to use tools, APIs, and workflows. They are documentation, not executable code.
64
+
65
+ ### Loading Skills
66
+
67
+ ```json
68
+ { "tool": "skills", "action": "list" }
69
+ { "tool": "skills", "action": "read", "name": "tools/files" }
70
+ { "tool": "skills", "action": "search", "query": "github pr" }
71
+ ```
72
+
73
+ ### Skill Locations
74
+
75
+ - `skills/` -- built-in skills shipped with SwarmClaw
76
+ - `data/skills/` -- user-created skills added at runtime
77
+
78
+ ### When to Load Skills
79
+
80
+ - Before using a tool you're unfamiliar with
81
+ - When a task involves an API or workflow you haven't used before
82
+ - When the user asks you to do something and you're unsure of the best approach
83
+
84
+ ## Agent Capabilities
85
+
86
+ ### Memory
87
+
88
+ Agents have persistent memory across sessions:
89
+
90
+ - **Working memory** (session-scoped): scratch notes, intermediate results
91
+ - **Durable memory** (cross-session): user preferences, project facts, decisions
92
+ - Memories are automatically surfaced in context when relevant
93
+ - Store important learnings proactively -- don't wait to be asked
94
+
95
+ ### Dreaming
96
+
97
+ Agents with dreaming enabled automatically consolidate memories during idle periods. You can also trigger a dream manually:
98
+
99
+ #### Check dream status
100
+ ```json
101
+ { "tool": "memory", "action": "list", "category": "dream_reflection" }
102
+ ```
103
+
104
+ #### Manual dream trigger
105
+ Use the platform API to trigger a dream cycle:
106
+ ```json
107
+ { "tool": "execute", "command": "curl -s -X POST http://localhost:3456/api/memory/dream -H 'Content-Type: application/json' -d '{\"agentId\":\"YOUR_AGENT_ID\"}'" }
108
+ ```
109
+
110
+ Dream cycles produce `dream_reflection` and `consolidated_insight` memories that help maintain a clean, coherent memory store over time.
111
+
112
+ ### Delegation
113
+
114
+ Agents can delegate work to other agents:
115
+
116
+ - **delegate**: route a task to a specific agent and wait for the result
117
+ - **spawn**: create a subagent that runs independently (fire-and-forget or session-based)
118
+ - Use `agents.list` to discover available agents and their specializations
119
+
120
+ ### Connectors
121
+
122
+ Agents can communicate through external platforms:
123
+
124
+ - Discord, Slack, Telegram, and custom webhooks
125
+ - Messages sent via `platform` tool with `communicate.send_message`
126
+ - Inbound messages from connectors trigger agent sessions automatically
127
+
128
+ ## Workspace Conventions
129
+
130
+ - The workspace root is the agent's working directory
131
+ - File paths in tool calls are relative to the workspace root
132
+ - `/workspace/...` paths are resolved to the workspace root automatically
133
+ - The `$WORKSPACE` env var points to the workspace root in execute tool runs
134
+
135
+ ## Best Practices
136
+
137
+ 1. **Load skills before unfamiliar operations.** A 30-second skill read prevents minutes of trial and error.
138
+
139
+ 2. **Use the right tool for the job.** Don't use `execute` with `echo > file.txt` when `files` write action is cleaner. Don't use `browser` when `curl` in `execute` suffices.
140
+
141
+ 3. **Store important context in memory.** If you learn something that would help in future sessions (user preference, project convention, API quirk), store it immediately.
142
+
143
+ 4. **Ask rather than guess.** When genuinely uncertain about user intent, use `communicate.ask_human`. A brief clarification is better than wasted work on the wrong approach.
144
+
145
+ 5. **Delegate when appropriate.** If another agent is better suited for a subtask, delegate. Check `agents.list` to know what's available.
146
+
147
+ 6. **Be explicit about what you're doing.** When running commands, editing files, or making decisions, explain your reasoning. Transparency builds trust.
148
+
149
+ 7. **Respect file access boundaries.** Stay within the workspace unless the agent has machine-scope access. Never write to system directories.
150
+
151
+ 8. **Handle errors gracefully.** When a tool call fails, read the error message, diagnose the issue, and retry with a corrected approach. Don't repeat the same failing call.
@@ -1,9 +1,17 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { notFound } from '@/lib/server/collection-helpers'
3
3
  import { trashAgent, updateAgent } from '@/lib/server/agents/agent-service'
4
+ import { loadAgent } from '@/lib/server/agents/agent-repository'
4
5
  import { notify } from '@/lib/server/ws-hub'
5
6
  import { safeParseBody } from '@/lib/server/safe-parse-body'
6
7
 
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params
10
+ const agent = loadAgent(id)
11
+ if (!agent) return notFound()
12
+ return NextResponse.json(agent)
13
+ }
14
+
7
15
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
16
  const { id } = await params
9
17
  const { data: body, error } = await safeParseBody(req)
@@ -0,0 +1,114 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ // Disable daemon autostart during tests
5
+ process.env.SWARMCLAW_DAEMON_AUTOSTART = '0'
6
+
7
+ import { GET as getAgent } from './[id]/route'
8
+ import { POST as createAgent } from './route'
9
+ import { loadAgents, saveAgents } from '@/lib/server/storage'
10
+
11
+ const originalAgents = loadAgents()
12
+
13
+ function routeParams(id: string) {
14
+ return { params: Promise.resolve({ id }) }
15
+ }
16
+
17
+ function seedAgent(id: string, overrides: Record<string, unknown> = {}) {
18
+ const agents = loadAgents()
19
+ const now = Date.now()
20
+ agents[id] = {
21
+ id,
22
+ name: 'Test Agent',
23
+ description: 'Route test',
24
+ systemPrompt: '',
25
+ provider: 'ollama',
26
+ model: 'qwen3.5',
27
+ credentialId: null,
28
+ fallbackCredentialIds: [],
29
+ apiEndpoint: null,
30
+ gatewayProfileId: null,
31
+ extensions: [],
32
+ createdAt: now,
33
+ updatedAt: now,
34
+ ...overrides,
35
+ }
36
+ saveAgents(agents)
37
+ }
38
+
39
+ afterEach(() => {
40
+ saveAgents(originalAgents)
41
+ })
42
+
43
+ // --- GET /api/agents/:id ---
44
+
45
+ test('GET /api/agents/:id returns the agent when it exists', async () => {
46
+ seedAgent('agent-get-test', { name: 'GetMe' })
47
+
48
+ const response = await getAgent(
49
+ new Request('http://local/api/agents/agent-get-test'),
50
+ routeParams('agent-get-test'),
51
+ )
52
+
53
+ assert.equal(response.status, 200)
54
+ const body = await response.json()
55
+ assert.equal(body.id, 'agent-get-test')
56
+ assert.equal(body.name, 'GetMe')
57
+ })
58
+
59
+ test('GET /api/agents/:id returns 404 for a non-existent agent', async () => {
60
+ const response = await getAgent(
61
+ new Request('http://local/api/agents/does-not-exist'),
62
+ routeParams('does-not-exist'),
63
+ )
64
+
65
+ assert.equal(response.status, 404)
66
+ const body = await response.json()
67
+ assert.equal(body.error, 'Not found')
68
+ })
69
+
70
+ // --- POST /api/agents (provider validation) ---
71
+
72
+ test('POST /api/agents rejects an unknown provider with a 400', async () => {
73
+ const response = await createAgent(new Request('http://local/api/agents', {
74
+ method: 'POST',
75
+ headers: { 'content-type': 'application/json' },
76
+ body: JSON.stringify({ name: 'Bad Provider Agent', provider: 'nonexistent_provider', model: 'x' }),
77
+ }))
78
+
79
+ assert.equal(response.status, 400)
80
+ const body = await response.json()
81
+ assert.equal(body.error, 'Validation failed')
82
+ assert.ok(body.issues.some((i: { path: string; message: string }) => i.path === 'provider'))
83
+ })
84
+
85
+ test('POST /api/agents accepts a valid provider and creates the agent', async () => {
86
+ const response = await createAgent(new Request('http://local/api/agents', {
87
+ method: 'POST',
88
+ headers: { 'content-type': 'application/json' },
89
+ body: JSON.stringify({ name: 'Good Agent', provider: 'ollama', model: 'qwen3.5' }),
90
+ }))
91
+
92
+ assert.equal(response.status, 200)
93
+ const body = await response.json()
94
+ assert.equal(body.name, 'Good Agent')
95
+ assert.equal(body.provider, 'ollama')
96
+ assert.ok(body.id)
97
+
98
+ // Clean up
99
+ const agents = loadAgents()
100
+ delete agents[body.id]
101
+ saveAgents(agents)
102
+ })
103
+
104
+ test('POST /api/agents rejects missing required fields with a 400', async () => {
105
+ const response = await createAgent(new Request('http://local/api/agents', {
106
+ method: 'POST',
107
+ headers: { 'content-type': 'application/json' },
108
+ body: JSON.stringify({}),
109
+ }))
110
+
111
+ assert.equal(response.status, 400)
112
+ const body = await response.json()
113
+ assert.equal(body.error, 'Validation failed')
114
+ })
@@ -3,6 +3,7 @@ import { perf } from '@/lib/server/runtime/perf'
3
3
  import { listAgentsForApi, createAgent } from '@/lib/server/agents/agent-service'
4
4
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
5
  import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
6
+ import { getProvider } from '@/lib/providers'
6
7
  import { z } from 'zod'
7
8
  import { safeParseBody } from '@/lib/server/safe-parse-body'
8
9
  import { loadSettings } from '@/lib/server/storage'
@@ -40,6 +41,15 @@ export async function POST(req: Request) {
40
41
  }
41
42
  const body = parsed.data as unknown as Record<string, unknown>
42
43
 
44
+ // Validate provider exists
45
+ const providerId = String(body.provider || '')
46
+ if (providerId && !getProvider(providerId)) {
47
+ return NextResponse.json(
48
+ { error: 'Validation failed', issues: [{ path: 'provider', message: `Unknown provider: "${providerId}"` }] },
49
+ { status: 400 },
50
+ )
51
+ }
52
+
43
53
  // Check approval policy — if enabled, create an approval request instead of the agent
44
54
  const settings = loadSettings()
45
55
  if (settings.approvalPolicies?.requireApprovalForAgentCreate) {
@@ -13,21 +13,33 @@ import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
13
13
  export const dynamic = 'force-dynamic'
14
14
 
15
15
  export async function GET() {
16
- const endPerf = perf.start('api', 'GET /api/connectors')
17
- const connectors = await listConnectorsWithRuntime()
18
- endPerf({ count: Object.keys(connectors).length })
19
- return NextResponse.json(connectors)
16
+ try {
17
+ const endPerf = perf.start('api', 'GET /api/connectors')
18
+ const connectors = await listConnectorsWithRuntime()
19
+ endPerf({ count: Object.keys(connectors).length })
20
+ return NextResponse.json(connectors)
21
+ } catch (err) {
22
+ return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 })
23
+ }
20
24
  }
21
25
 
22
26
  export async function POST(req: Request) {
23
- await ensureDaemonProcessRunning('api/connectors:post')
24
- const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
25
- if (error) return error
26
- const parsed = ConnectorCreateSchema.safeParse(raw)
27
- if (!parsed.success) {
28
- return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
27
+ try {
28
+ await ensureDaemonProcessRunning('api/connectors:post')
29
+ const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
30
+ if (error) return error
31
+ const parsed = ConnectorCreateSchema.safeParse(raw)
32
+ if (!parsed.success) {
33
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
34
+ }
35
+ const connector = createConnector(parsed.data as unknown as Record<string, unknown>)
36
+ try {
37
+ await autoStartConnectorIfNeeded(connector, parsed.data as unknown as Record<string, unknown>)
38
+ } catch {
39
+ // Auto-start failure is non-fatal — the connector is still saved.
40
+ }
41
+ return NextResponse.json(await getConnectorWithRuntime(connector.id) || connector)
42
+ } catch (err) {
43
+ return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 })
29
44
  }
30
- const connector = createConnector(parsed.data as unknown as Record<string, unknown>)
31
- await autoStartConnectorIfNeeded(connector, parsed.data as unknown as Record<string, unknown>)
32
- return NextResponse.json(await getConnectorWithRuntime(connector.id) || connector)
33
45
  }
@@ -1,11 +1,18 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { deleteCredentialRecord } from '@/lib/server/credentials/credential-service'
2
+ import { deleteCredentialRecord, getCredentialSummary } from '@/lib/server/credentials/credential-service'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { log } from '@/lib/server/logger'
5
5
  import { logActivity } from '@/lib/server/activity/activity-log'
6
6
 
7
7
  const TAG = 'api-credentials'
8
8
 
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const summary = getCredentialSummary(id)
12
+ if (!summary) return notFound()
13
+ return NextResponse.json(summary)
14
+ }
15
+
9
16
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
17
  const { id: credId } = await params
11
18
  if (!deleteCredentialRecord(credId)) {
@@ -1,11 +1,19 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { notFound } from '@/lib/server/collection-helpers'
3
3
  import { safeParseBody } from '@/lib/server/safe-parse-body'
4
+ import { loadSchedule } from '@/lib/server/schedules/schedule-repository'
4
5
  import {
5
6
  deleteScheduleFromRoute,
6
7
  updateScheduleFromRoute,
7
8
  } from '@/lib/server/schedules/schedule-route-service'
8
9
 
10
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
11
+ const { id } = await params
12
+ const schedule = loadSchedule(id)
13
+ if (!schedule) return notFound()
14
+ return NextResponse.json(schedule)
15
+ }
16
+
9
17
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
18
  const { id } = await params
11
19
  const { data: body, error } = await safeParseBody(req)
@@ -6,6 +6,16 @@ import { safeParseBody } from '@/lib/server/safe-parse-body'
6
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
7
  const ops: CollectionOps<any> = { load: loadSecrets, save: saveSecrets }
8
8
 
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const secrets = loadSecrets()
12
+ const secret = secrets[id]
13
+ if (!secret) return notFound()
14
+ // Never expose the encrypted value
15
+ const { encryptedValue, ...safe } = secret as Record<string, unknown>
16
+ return NextResponse.json(safe)
17
+ }
18
+
9
19
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
20
  const { id } = await params
11
21
  if (!deleteItem(ops, id)) return notFound()
@@ -1,11 +1,20 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadCredentials, decryptKey } from '@/lib/server/storage'
3
3
  import { getDeviceId, wsConnect, rpcOnConnectedGateway } from '@/lib/providers/openclaw'
4
+ import { buildCliEnv, probeCliAuth, resolveCliBinary } from '@/lib/providers/cli-utils'
4
5
  import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
5
6
  import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
6
7
  import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
7
8
 
8
9
  type SetupProvider =
10
+ | 'claude-cli'
11
+ | 'codex-cli'
12
+ | 'opencode-cli'
13
+ | 'gemini-cli'
14
+ | 'copilot-cli'
15
+ | 'cursor-cli'
16
+ | 'qwen-code-cli'
17
+ | 'goose'
9
18
  | 'openai'
10
19
  | 'openrouter'
11
20
  | 'anthropic'
@@ -22,6 +31,8 @@ type SetupProvider =
22
31
  | 'openclaw'
23
32
  | 'hermes'
24
33
 
34
+ type CliSetupProvider = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'cursor-cli' | 'qwen-code-cli' | 'goose'
35
+
25
36
  interface SetupCheckBody {
26
37
  provider?: string
27
38
  apiKey?: string
@@ -266,6 +277,34 @@ async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok:
266
277
  return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId, recommendedModel }
267
278
  }
268
279
 
280
+ function checkCliProvider(provider: CliSetupProvider): { ok: boolean; message: string } {
281
+ const env = buildCliEnv()
282
+ const config = {
283
+ 'claude-cli': { binary: 'claude', backend: 'claude' as const, label: 'Claude Code CLI' },
284
+ 'codex-cli': { binary: 'codex', backend: 'codex' as const, label: 'OpenAI Codex CLI' },
285
+ 'opencode-cli': { binary: 'opencode', backend: 'opencode' as const, label: 'OpenCode CLI' },
286
+ 'gemini-cli': { binary: 'gemini', backend: 'gemini' as const, label: 'Gemini CLI' },
287
+ 'copilot-cli': { binary: 'copilot', backend: 'copilot' as const, label: 'GitHub Copilot CLI' },
288
+ 'cursor-cli': { binary: 'cursor-agent', backend: 'cursor' as const, label: 'Cursor Agent CLI' },
289
+ 'qwen-code-cli': { binary: 'qwen', backend: 'qwen' as const, label: 'Qwen Code CLI' },
290
+ goose: { binary: 'goose', backend: 'goose' as const, label: 'Goose CLI' },
291
+ }[provider]
292
+
293
+ if (!config) return { ok: false, message: 'Unknown CLI provider.' }
294
+ const binary = resolveCliBinary(config.binary)
295
+ if (!binary) {
296
+ return {
297
+ ok: false,
298
+ message: `${config.label} is not installed. Install \`${config.binary}\` and ensure it is on your PATH.`,
299
+ }
300
+ }
301
+ const auth = probeCliAuth(binary, config.backend, env, process.cwd())
302
+ if (!auth.authenticated) {
303
+ return { ok: false, message: auth.errorMessage || `${config.label} is not configured.` }
304
+ }
305
+ return { ok: true, message: `${config.label} is installed and ready.` }
306
+ }
307
+
269
308
  export async function POST(req: Request) {
270
309
  const body = parseBody(await req.json().catch(() => ({})))
271
310
  const provider = clean(body.provider) as SetupProvider
@@ -273,6 +312,7 @@ export async function POST(req: Request) {
273
312
  const credentialId = clean(body.credentialId)
274
313
  const endpoint = clean(body.endpoint)
275
314
  const model = clean(body.model)
315
+ const CLI_PROVIDERS = new Set<CliSetupProvider>(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'])
276
316
 
277
317
  // Resolve credentialId to an API key if no raw key was provided
278
318
  if (!apiKey && credentialId) {
@@ -287,6 +327,11 @@ export async function POST(req: Request) {
287
327
  }
288
328
  }
289
329
 
330
+ if (CLI_PROVIDERS.has(provider as CliSetupProvider)) {
331
+ const result = checkCliProvider(provider as CliSetupProvider)
332
+ return NextResponse.json(result)
333
+ }
334
+
290
335
  if (!provider) {
291
336
  return NextResponse.json({ ok: false, message: 'Provider is required.' }, { status: 400 })
292
337
  }
@@ -171,6 +171,11 @@ export async function GET(req: Request) {
171
171
  { id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
172
172
  { id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
173
173
  { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
174
+ { id: 'gemini-cli', label: 'Gemini CLI', command: 'gemini' },
175
+ { id: 'copilot-cli', label: 'GitHub Copilot CLI', command: 'copilot' },
176
+ { id: 'cursor-cli', label: 'Cursor Agent CLI', command: 'cursor-agent' },
177
+ { id: 'qwen-code-cli', label: 'Qwen Code CLI', command: 'qwen' },
178
+ { id: 'goose', label: 'Goose CLI', command: 'goose' },
174
179
  { id: 'google-workspace-cli', label: 'Google Workspace CLI', command: 'gws' },
175
180
  ]
176
181
 
@@ -113,6 +113,17 @@ test('legacy-routed binary commands fall back to platform-api-key.txt', () => {
113
113
  fs.rmSync(tmpDir, { recursive: true, force: true })
114
114
  })
115
115
 
116
+ test('legacy-routed binary commands accept documented global aliases after the subcommand', () => {
117
+ const { result, capture } = runWithMockedFetch(
118
+ ['agents', 'list', '--access-key', 'alias-key', '--base-url', 'http://127.0.0.1:4567', '--json'],
119
+ )
120
+
121
+ assert.equal(result.status, 0, result.stderr)
122
+ assert.equal(result.stdout.trim(), '[]')
123
+ assert.equal(capture.headers['X-Access-Key'], 'alias-key')
124
+ assert.equal(capture.url, 'http://127.0.0.1:4567/api/agents')
125
+ })
126
+
116
127
  test('binary server help exits successfully', () => {
117
128
  const result = runBinary(['server', '--help'])
118
129
  assert.equal(result.status, 0, result.stderr)
package/src/cli/index.js CHANGED
@@ -15,7 +15,7 @@ const COMMAND_GROUPS = [
15
15
  description: 'Manage agents',
16
16
  commands: [
17
17
  cmd('list', 'GET', '/agents', 'List agents'),
18
- cmd('get', 'GET', '/agents/:id', 'Get an agent by id', { virtual: true, clientGetRoute: '/agents' }),
18
+ cmd('get', 'GET', '/agents/:id', 'Get an agent by id'),
19
19
  cmd('create', 'POST', '/agents', 'Create an agent', { expectsJsonBody: true }),
20
20
  cmd('update', 'PUT', '/agents/:id', 'Update an agent', { expectsJsonBody: true }),
21
21
  cmd('delete', 'DELETE', '/agents/:id', 'Delete an agent'),
@@ -142,7 +142,7 @@ const COMMAND_GROUPS = [
142
142
  description: 'Manage encrypted provider credentials',
143
143
  commands: [
144
144
  cmd('list', 'GET', '/credentials', 'List credentials'),
145
- cmd('get', 'GET', '/credentials/:id', 'Get credential metadata by id', { virtual: true, clientGetRoute: '/credentials' }),
145
+ cmd('get', 'GET', '/credentials/:id', 'Get credential metadata by id'),
146
146
  cmd('create', 'POST', '/credentials', 'Create credential', { expectsJsonBody: true }),
147
147
  cmd('delete', 'DELETE', '/credentials/:id', 'Delete credential'),
148
148
  ],
@@ -541,7 +541,7 @@ const COMMAND_GROUPS = [
541
541
  description: 'Manage schedules',
542
542
  commands: [
543
543
  cmd('list', 'GET', '/schedules', 'List schedules'),
544
- cmd('get', 'GET', '/schedules/:id', 'Get schedule by id', { virtual: true, clientGetRoute: '/schedules' }),
544
+ cmd('get', 'GET', '/schedules/:id', 'Get schedule by id'),
545
545
  cmd('create', 'POST', '/schedules', 'Create schedule', { expectsJsonBody: true }),
546
546
  cmd('update', 'PUT', '/schedules/:id', 'Update schedule', { expectsJsonBody: true }),
547
547
  cmd('delete', 'DELETE', '/schedules/:id', 'Delete schedule'),
@@ -553,7 +553,7 @@ const COMMAND_GROUPS = [
553
553
  description: 'Manage reusable encrypted secrets',
554
554
  commands: [
555
555
  cmd('list', 'GET', '/secrets', 'List secrets metadata'),
556
- cmd('get', 'GET', '/secrets/:id', 'Get secret metadata by id', { virtual: true, clientGetRoute: '/secrets' }),
556
+ cmd('get', 'GET', '/secrets/:id', 'Get secret metadata by id'),
557
557
  cmd('create', 'POST', '/secrets', 'Create secret', { expectsJsonBody: true }),
558
558
  cmd('update', 'PUT', '/secrets/:id', 'Update secret metadata', { expectsJsonBody: true }),
559
559
  cmd('delete', 'DELETE', '/secrets/:id', 'Delete secret'),
@@ -453,7 +453,10 @@ test('client-side collection lookups fail cleanly when the entity is missing', a
453
453
  const stdout = makeWritable()
454
454
  const stderr = makeWritable()
455
455
 
456
- const fetchImpl = async () => jsonResponse([{ id: 'agent-2', name: 'Other Agent' }])
456
+ const fetchImpl = async () => new Response(JSON.stringify({ error: 'Not found' }), {
457
+ status: 404,
458
+ headers: { 'content-type': 'application/json' },
459
+ })
457
460
 
458
461
  const exitCode = await runCli(
459
462
  ['agents', 'get', 'agent-1'],
@@ -468,7 +471,7 @@ test('client-side collection lookups fail cleanly when the entity is missing', a
468
471
 
469
472
  assert.equal(exitCode, 1)
470
473
  assert.equal(stdout.toString(), '')
471
- assert.match(stderr.toString(), /entity not found for id: agent-1/i)
474
+ assert.match(stderr.toString(), /not found/i)
472
475
  })
473
476
 
474
477
  test('runCli loads request JSON from @file inputs', async () => {
package/src/cli/index.ts CHANGED
@@ -479,7 +479,7 @@ export function buildProgram(): Command {
479
479
  .description('SwarmClaw CLI')
480
480
  .option('-u, --url <url>', 'SwarmClaw base URL', DEFAULT_BASE_URL)
481
481
  .option('-k, --key <key>', 'SwarmClaw access key', DEFAULT_ACCESS_KEY)
482
- .option('--raw', 'Print compact JSON output')
482
+ .option('--raw, --json', 'Print compact JSON output')
483
483
  .showHelpAfterError()
484
484
 
485
485
  const agents = program.command('agents').description('Manage agents')
@@ -2506,10 +2506,22 @@ export function AgentSheet() {
2506
2506
  {provider === 'claude-cli'
2507
2507
  ? 'Claude CLI uses its own built-in capabilities — no additional local tool/platform configuration is needed.'
2508
2508
  : provider === 'codex-cli'
2509
- ? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'
2510
- : provider === 'hermes'
2511
- ? 'Hermes Agent runs behind its own API server and tool runtime. SwarmClaw sends prompts to Hermes directly instead of injecting local platform tools.'
2512
- : 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
2509
+ ? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration is needed.'
2510
+ : provider === 'opencode-cli'
2511
+ ? 'OpenCode CLI uses its own built-in tools (shell, files, etc.) no additional local tool configuration is needed.'
2512
+ : provider === 'gemini-cli'
2513
+ ? 'Gemini CLI uses its own built-in tools and runtime — SwarmClaw does not inject local platform tools for it.'
2514
+ : provider === 'copilot-cli'
2515
+ ? 'GitHub Copilot CLI uses its own built-in tools and runtime — SwarmClaw does not inject local platform tools for it.'
2516
+ : provider === 'cursor-cli'
2517
+ ? 'Cursor Agent CLI runs with its own native tool/runtime layer — SwarmClaw sends prompts directly without injecting local platform tools.'
2518
+ : provider === 'qwen-code-cli'
2519
+ ? 'Qwen Code CLI uses its own native tools and runtime — SwarmClaw does not inject local platform tools for it.'
2520
+ : provider === 'goose'
2521
+ ? 'Goose manages its own runtime, tools, and extensions — SwarmClaw sends prompts directly instead of injecting local platform tools.'
2522
+ : provider === 'hermes'
2523
+ ? 'Hermes Agent runs behind its own API server and tool runtime. SwarmClaw sends prompts to Hermes directly instead of injecting local platform tools.'
2524
+ : 'This provider manages its own native capabilities and runtime.'}
2513
2525
  </p>
2514
2526
  </div>
2515
2527
  )}
@@ -51,6 +51,11 @@ const PROVIDER_LABELS: Record<string, string> = {
51
51
  'claude-cli': 'Claude CLI',
52
52
  'codex-cli': 'Codex CLI',
53
53
  'opencode-cli': 'OpenCode CLI',
54
+ 'gemini-cli': 'Gemini CLI',
55
+ 'copilot-cli': 'Copilot CLI',
56
+ 'cursor-cli': 'Cursor CLI',
57
+ 'qwen-code-cli': 'Qwen Code CLI',
58
+ goose: 'Goose',
54
59
  openai: 'OpenAI',
55
60
  anthropic: 'Anthropic',
56
61
  openclaw: 'OpenClaw',