@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.
- package/README.md +167 -147
- package/bin/swarmclaw.js +35 -1
- package/package.json +2 -2
- package/scripts/run-next-build.mjs +87 -2
- package/skills/swarmclaw/SKILL.md +151 -0
- package/src/app/api/agents/[id]/route.ts +8 -0
- package/src/app/api/agents/agents-route.test.ts +114 -0
- package/src/app/api/agents/route.ts +10 -0
- package/src/app/api/connectors/route.ts +25 -13
- package/src/app/api/credentials/[id]/route.ts +8 -1
- package/src/app/api/schedules/[id]/route.ts +8 -0
- package/src/app/api/secrets/[id]/route.ts +10 -0
- package/src/app/api/setup/check-provider/route.ts +45 -0
- package/src/app/api/setup/doctor/route.ts +5 -0
- package/src/cli/binary.test.js +11 -0
- package/src/cli/index.js +4 -4
- package/src/cli/index.test.js +5 -2
- package/src/cli/index.ts +1 -1
- package/src/components/agents/agent-sheet.tsx +16 -4
- package/src/components/agents/inspector-panel.tsx +5 -0
- package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
- package/src/components/chat/activity-moment.tsx +3 -0
- package/src/components/chat/chat-header.tsx +23 -2
- package/src/components/chat/tool-call-bubble.tsx +20 -0
- package/src/components/layout/sidebar-rail.tsx +29 -0
- package/src/hooks/setup-done-detection.test.ts +4 -2
- package/src/hooks/setup-done-detection.ts +1 -1
- package/src/lib/orchestrator-config.ts +5 -0
- package/src/lib/provider-sets.ts +4 -4
- package/src/lib/providers/acp-client.ts +116 -0
- package/src/lib/providers/cli-utils.test.ts +9 -1
- package/src/lib/providers/cli-utils.ts +89 -4
- package/src/lib/providers/cursor-cli.ts +172 -0
- package/src/lib/providers/goose.ts +149 -0
- package/src/lib/providers/index.ts +29 -1
- package/src/lib/providers/qwen-code-cli.ts +152 -0
- package/src/lib/server/agents/agent-availability.ts +2 -2
- package/src/lib/server/agents/agent-thread-session.ts +8 -0
- package/src/lib/server/agents/task-session.ts +8 -0
- package/src/lib/server/capability-router.ts +8 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +3 -0
- package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
- package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +3 -1
- package/src/lib/server/chat-execution/stream-agent-chat.ts +5 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
- package/src/lib/server/chats/chat-session-service.ts +18 -0
- package/src/lib/server/connectors/session.ts +8 -0
- package/src/lib/server/context-manager.ts +5 -0
- package/src/lib/server/provider-health.ts +13 -3
- package/src/lib/server/provider-model-discovery.test.ts +8 -0
- package/src/lib/server/provider-model-discovery.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +2 -2
- package/src/lib/server/runtime/queue/core.ts +30 -4
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
- package/src/lib/server/session-reset-policy.test.ts +16 -0
- package/src/lib/server/session-reset-policy.ts +9 -1
- package/src/lib/server/session-tools/context.ts +2 -2
- package/src/lib/server/session-tools/delegate.ts +334 -14
- package/src/lib/server/session-tools/index.ts +5 -2
- package/src/lib/server/session-tools/session-info.ts +4 -1
- package/src/lib/server/storage-auth-docker.test.ts +244 -0
- package/src/lib/server/storage-auth.test.ts +150 -0
- package/src/lib/server/storage-auth.ts +57 -22
- package/src/lib/server/storage-normalization.ts +19 -0
- package/src/lib/server/storage.ts +3 -0
- package/src/lib/server/tasks/task-resume.ts +23 -1
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy.ts +4 -1
- package/src/lib/setup-defaults.test.ts +6 -0
- package/src/lib/setup-defaults.ts +146 -0
- package/src/lib/tool-definitions.ts +1 -1
- package/src/types/misc.ts +4 -1
- package/src/types/provider.ts +1 -1
- 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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
package/src/cli/binary.test.js
CHANGED
|
@@ -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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'),
|
package/src/cli/index.test.js
CHANGED
|
@@ -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 () =>
|
|
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(), /
|
|
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 === '
|
|
2511
|
-
? '
|
|
2512
|
-
:
|
|
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',
|