@swarmclawai/swarmclaw 0.7.4 → 0.7.6
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 +32 -9
- package/package.json +2 -2
- package/src/app/api/agents/[id]/thread/route.ts +4 -89
- package/src/app/api/openclaw/deploy/route.ts +101 -0
- package/src/cli/index.js +13 -0
- package/src/cli/index.test.js +34 -0
- package/src/cli/spec.js +19 -0
- package/src/components/auth/setup-wizard.tsx +36 -52
- package/src/components/gateways/gateway-sheet.tsx +63 -3
- package/src/components/openclaw/openclaw-deploy-panel.tsx +626 -0
- package/src/components/providers/provider-list.tsx +103 -8
- package/src/lib/server/agent-thread-session.test.ts +85 -0
- package/src/lib/server/agent-thread-session.ts +123 -0
- package/src/lib/server/data-dir.test.ts +56 -0
- package/src/lib/server/data-dir.ts +15 -9
- package/src/lib/server/heartbeat-service.ts +18 -5
- package/src/lib/server/heartbeat-wake.ts +6 -2
- package/src/lib/server/openclaw-deploy.test.ts +67 -0
- package/src/lib/server/openclaw-deploy.ts +724 -0
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Inspired by [OpenClaw](https://github.com/openclaw).
|
|
|
18
18
|
|
|
19
19
|
- [Getting Started](https://swarmclaw.ai/docs/getting-started) - install and first-run setup
|
|
20
20
|
- [Providers](https://swarmclaw.ai/docs/providers) - provider setup and failover options
|
|
21
|
+
- [OpenClaw Setup](https://swarmclaw.ai/docs/openclaw-setup) - local, VPS, and hosted OpenClaw deployment paths
|
|
21
22
|
- [Agents](https://swarmclaw.ai/docs/agents) - agent configuration, tools, and platform capabilities
|
|
22
23
|
- [Tools](https://swarmclaw.ai/docs/tools) - built-in tool reference and guardrails
|
|
23
24
|
- [Orchestration](https://swarmclaw.ai/docs/orchestration) - multi-agent flows, checkpoints, and restore
|
|
@@ -39,6 +40,20 @@ SwarmClaw includes the `openclaw` CLI as a bundled dependency, so there is no se
|
|
|
39
40
|
|
|
40
41
|
The Providers screen now supports named OpenClaw gateway profiles with discovery, health checks, default-gateway selection, and an External Agent Runtimes view for remote workers that register/heartbeat into SwarmClaw.
|
|
41
42
|
|
|
43
|
+
SwarmClaw now also includes **Smart Deploy** for OpenClaw in three places:
|
|
44
|
+
|
|
45
|
+
- **Onboarding** - non-technical users can launch a local OpenClaw runtime or generate a remote bundle before they finish first-run setup
|
|
46
|
+
- **Providers -> OpenClaw Gateways** - operators can deploy or prepare more gateways later without leaving the main app
|
|
47
|
+
- **Gateway editor** - every gateway profile includes the same deploy panel for local restarts, VPS bundles, and hosted repo-backed deployments
|
|
48
|
+
|
|
49
|
+
The deployment flow stays **in-house and official-only**:
|
|
50
|
+
|
|
51
|
+
- local deploys run the bundled official `openclaw` CLI directly from SwarmClaw
|
|
52
|
+
- VPS deploys use the official OpenClaw Docker image with prefilled `.env`, `docker-compose.yml`, `bootstrap.sh`, and `cloud-init.yaml`
|
|
53
|
+
- hosted templates target the official OpenClaw repo for Render, Fly.io, and Railway
|
|
54
|
+
|
|
55
|
+
Supported VPS presets currently include Hetzner, DigitalOcean, Vultr, Linode, Lightsail, Google Cloud, Azure, OCI, and a generic Ubuntu host path. Smart defaults prefill the gateway token, endpoint, storage paths, and copy-paste commands so the resulting gateway can be saved into SwarmClaw with minimal manual editing.
|
|
56
|
+
|
|
42
57
|
The OpenClaw Control Plane in SwarmClaw adds:
|
|
43
58
|
- Reload mode switching (`hot`, `hybrid`, `full`)
|
|
44
59
|
- Config issue detection and guided repair
|
|
@@ -59,6 +74,15 @@ Each agent can point to a **different** OpenClaw gateway profile or direct endpo
|
|
|
59
74
|
|
|
60
75
|
URLs without a protocol are auto-prefixed with `http://`. For remote gateways with TLS, use `https://` explicitly.
|
|
61
76
|
|
|
77
|
+
CLI operators can use the same deploy surface without opening the UI:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
swarmclaw openclaw deploy-status
|
|
81
|
+
swarmclaw openclaw deploy-local-start --data '{"port":18789}'
|
|
82
|
+
swarmclaw openclaw deploy-bundle --data '{"template":"docker","provider":"hetzner","target":"openclaw.example.com"}'
|
|
83
|
+
swarmclaw openclaw deploy-bundle --data '{"template":"render","target":"https://openclaw.example.com"}'
|
|
84
|
+
```
|
|
85
|
+
|
|
62
86
|
## SwarmClaw ClawHub Skill
|
|
63
87
|
|
|
64
88
|
Use the `swarmclaw` ClawHub skill when you want an OpenClaw agent to operate your SwarmClaw control plane directly from chat: list agents, dispatch tasks, check chats, run diagnostics, and coordinate multi-agent work.
|
|
@@ -78,7 +102,6 @@ Skill source and runbook: [`swarmclaw/SKILL.md`](swarmclaw/SKILL.md).
|
|
|
78
102
|
|
|
79
103
|
## Requirements
|
|
80
104
|
|
|
81
|
-
- **Node.js** 22.6+
|
|
82
105
|
- **Node.js** 22.6+
|
|
83
106
|
- One of: **npm** 10+, **pnpm**, **Yarn**, or **Bun**
|
|
84
107
|
- **Claude Code CLI** (optional, for `claude-cli` provider) — [Install](https://docs.anthropic.com/en/docs/claude-code/overview)
|
|
@@ -117,7 +140,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
117
140
|
```
|
|
118
141
|
|
|
119
142
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
120
|
-
To pin a version: `SWARMCLAW_VERSION=v0.7.
|
|
143
|
+
To pin a version: `SWARMCLAW_VERSION=v0.7.6 curl ... | bash`
|
|
121
144
|
|
|
122
145
|
Or run locally from the repo (friendly for non-technical users):
|
|
123
146
|
|
|
@@ -670,7 +693,7 @@ npm run update:easy # safe update helper for local installs
|
|
|
670
693
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
671
694
|
|
|
672
695
|
```bash
|
|
673
|
-
# example patch release (v0.7.
|
|
696
|
+
# example patch release (v0.7.6 style)
|
|
674
697
|
npm version patch
|
|
675
698
|
git push origin main --follow-tags
|
|
676
699
|
```
|
|
@@ -680,15 +703,15 @@ On `v*` tags, GitHub Actions will:
|
|
|
680
703
|
2. Create a GitHub Release
|
|
681
704
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
682
705
|
|
|
683
|
-
#### v0.7.
|
|
706
|
+
#### v0.7.6 Release Readiness Notes
|
|
684
707
|
|
|
685
|
-
Before shipping `v0.7.
|
|
708
|
+
Before shipping `v0.7.6`, confirm the following user-facing changes are reflected in docs:
|
|
686
709
|
|
|
687
710
|
1. Sandbox docs are updated everywhere to reflect the current Deno-only `sandbox_exec` behavior and the guidance to prefer `http_request` for simple API calls.
|
|
688
|
-
2. OpenClaw docs cover the current gateway/runtime behavior, including
|
|
689
|
-
3. Site and README install/version strings are updated to `v0.7.
|
|
690
|
-
4. Release notes summarize the user-visible setup/auth/runtime changes from the current worktree, especially
|
|
691
|
-
5. CLI and tool docs do not reference removed or non-functional
|
|
711
|
+
2. OpenClaw docs cover the current gateway/runtime behavior, including Smart Deploy, official-only Docker/repo paths, local one-click startup, and the main-provider-screen deploy entry points.
|
|
712
|
+
3. Site and README install/version strings are updated to `v0.7.6`, including install snippets, release notes index text, and sidebar/footer labels.
|
|
713
|
+
4. Release notes summarize the user-visible setup/auth/runtime changes from the current worktree, especially Smart Deploy, VPS presets, and onboarding/provider-screen improvements.
|
|
714
|
+
5. CLI and tool docs include the new `openclaw deploy-*` surfaces and do not reference removed or non-functional bridges.
|
|
692
715
|
|
|
693
716
|
## CLI
|
|
694
717
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
|
|
58
58
|
"cli": "node ./bin/swarmclaw.js",
|
|
59
59
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js",
|
|
60
|
-
"test:openclaw": "tsx --test src/lib/openclaw-agent-id.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw-agent-resolver.test.ts src/lib/server/openclaw-skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts",
|
|
60
|
+
"test:openclaw": "tsx --test src/lib/openclaw-agent-id.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw-agent-resolver.test.ts src/lib/server/openclaw-deploy.test.ts src/lib/server/openclaw-skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts",
|
|
61
61
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
62
62
|
"postinstall": "node ./scripts/postinstall.mjs"
|
|
63
63
|
},
|
|
@@ -1,98 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
3
|
-
import { loadAgents, saveAgents, loadSessions, saveSessions } from '@/lib/server/storage'
|
|
4
|
-
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
5
|
-
import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
|
|
2
|
+
import { ensureAgentThreadSession } from '@/lib/server/agent-thread-session'
|
|
6
3
|
|
|
7
4
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
5
|
const { id: agentId } = await params
|
|
9
|
-
const agents = loadAgents()
|
|
10
|
-
const agent = agents[agentId]
|
|
11
|
-
if (!agent) {
|
|
12
|
-
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
13
|
-
}
|
|
14
|
-
|
|
15
6
|
const body = await req.json().catch(() => ({}))
|
|
16
7
|
const user = body.user || 'default'
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (agent.threadSessionId && sessions[agent.threadSessionId]) {
|
|
21
|
-
const existing = sessions[agent.threadSessionId] as Record<string, unknown>
|
|
22
|
-
let changed = false
|
|
23
|
-
if (existing.shortcutForAgentId !== agentId) {
|
|
24
|
-
existing.shortcutForAgentId = agentId
|
|
25
|
-
changed = true
|
|
26
|
-
}
|
|
27
|
-
if (existing.name !== agent.name) {
|
|
28
|
-
existing.name = agent.name
|
|
29
|
-
changed = true
|
|
30
|
-
}
|
|
31
|
-
if (changed) saveSessions(sessions)
|
|
32
|
-
return NextResponse.json(existing)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Legacy fallback for older shortcut sessions that were named using the
|
|
36
|
-
// old agent-thread convention before the explicit link was persisted.
|
|
37
|
-
const existing = Object.values(sessions).find(
|
|
38
|
-
(s: Record<string, unknown>) =>
|
|
39
|
-
(
|
|
40
|
-
s.shortcutForAgentId === agentId
|
|
41
|
-
|| s.name === `agent-thread:${agentId}`
|
|
42
|
-
)
|
|
43
|
-
&& s.user === user
|
|
44
|
-
)
|
|
45
|
-
if (existing) {
|
|
46
|
-
agent.threadSessionId = (existing as Record<string, unknown>).id as string
|
|
47
|
-
agent.updatedAt = Date.now()
|
|
48
|
-
saveAgents(agents)
|
|
49
|
-
let changed = false
|
|
50
|
-
const existingRecord = existing as Record<string, unknown>
|
|
51
|
-
if (existingRecord.shortcutForAgentId !== agentId) {
|
|
52
|
-
existingRecord.shortcutForAgentId = agentId
|
|
53
|
-
changed = true
|
|
54
|
-
}
|
|
55
|
-
if (existingRecord.name !== agent.name) {
|
|
56
|
-
existingRecord.name = agent.name
|
|
57
|
-
changed = true
|
|
58
|
-
}
|
|
59
|
-
if (changed) saveSessions(sessions)
|
|
60
|
-
return NextResponse.json(existing)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Create a new shortcut chat session for this agent.
|
|
64
|
-
const sessionId = `agent-chat-${agentId}-${genId()}`
|
|
65
|
-
const now = Date.now()
|
|
66
|
-
const baseSession = {
|
|
67
|
-
id: sessionId,
|
|
68
|
-
name: agent.name,
|
|
69
|
-
shortcutForAgentId: agentId,
|
|
70
|
-
cwd: WORKSPACE_DIR,
|
|
71
|
-
user: user,
|
|
72
|
-
provider: agent.provider,
|
|
73
|
-
model: agent.model,
|
|
74
|
-
credentialId: agent.credentialId || null,
|
|
75
|
-
fallbackCredentialIds: agent.fallbackCredentialIds || [],
|
|
76
|
-
apiEndpoint: agent.apiEndpoint || null,
|
|
77
|
-
claudeSessionId: null,
|
|
78
|
-
messages: [],
|
|
79
|
-
createdAt: now,
|
|
80
|
-
lastActiveAt: now,
|
|
81
|
-
active: false,
|
|
82
|
-
sessionType: 'human' as const,
|
|
83
|
-
agentId,
|
|
84
|
-
plugins: agent.plugins || agent.tools || [],
|
|
85
|
-
heartbeatEnabled: agent.heartbeatEnabled || false,
|
|
86
|
-
heartbeatIntervalSec: agent.heartbeatIntervalSec || null,
|
|
8
|
+
const session = ensureAgentThreadSession(agentId, user)
|
|
9
|
+
if (!session) {
|
|
10
|
+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
87
11
|
}
|
|
88
|
-
const session = applyResolvedRoute(baseSession, resolvePrimaryAgentRoute(agent))
|
|
89
|
-
|
|
90
|
-
sessions[sessionId] = session as Record<string, unknown>
|
|
91
|
-
saveSessions(sessions)
|
|
92
|
-
|
|
93
|
-
agent.threadSessionId = sessionId
|
|
94
|
-
agent.updatedAt = Date.now()
|
|
95
|
-
saveAgents(agents)
|
|
96
|
-
|
|
97
12
|
return NextResponse.json(session)
|
|
98
13
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
buildOpenClawDeployBundle,
|
|
4
|
+
getOpenClawLocalDeployStatus,
|
|
5
|
+
startOpenClawLocalDeploy,
|
|
6
|
+
stopOpenClawLocalDeploy,
|
|
7
|
+
type OpenClawRemoteDeployProvider,
|
|
8
|
+
type OpenClawRemoteDeployTemplate,
|
|
9
|
+
} from '@/lib/server/openclaw-deploy'
|
|
10
|
+
|
|
11
|
+
export const dynamic = 'force-dynamic'
|
|
12
|
+
|
|
13
|
+
function parsePort(value: unknown): number | undefined {
|
|
14
|
+
const parsed = typeof value === 'number'
|
|
15
|
+
? value
|
|
16
|
+
: typeof value === 'string'
|
|
17
|
+
? Number.parseInt(value, 10)
|
|
18
|
+
: Number.NaN
|
|
19
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseTemplate(value: unknown): OpenClawRemoteDeployTemplate | undefined {
|
|
23
|
+
if (value === 'docker' || value === 'render' || value === 'fly' || value === 'railway') {
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseProvider(value: unknown): OpenClawRemoteDeployProvider | undefined {
|
|
30
|
+
if (
|
|
31
|
+
value === 'hetzner'
|
|
32
|
+
|| value === 'digitalocean'
|
|
33
|
+
|| value === 'vultr'
|
|
34
|
+
|| value === 'linode'
|
|
35
|
+
|| value === 'lightsail'
|
|
36
|
+
|| value === 'gcp'
|
|
37
|
+
|| value === 'azure'
|
|
38
|
+
|| value === 'oci'
|
|
39
|
+
|| value === 'generic'
|
|
40
|
+
) {
|
|
41
|
+
return value
|
|
42
|
+
}
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function GET() {
|
|
47
|
+
return NextResponse.json({
|
|
48
|
+
local: getOpenClawLocalDeployStatus(),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function POST(req: Request) {
|
|
53
|
+
const body = await req.json().catch(() => ({}))
|
|
54
|
+
const action = typeof body?.action === 'string' ? body.action : ''
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (action === 'start-local') {
|
|
58
|
+
const result = await startOpenClawLocalDeploy({
|
|
59
|
+
port: parsePort(body.port),
|
|
60
|
+
token: typeof body.token === 'string' ? body.token : null,
|
|
61
|
+
})
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
ok: true,
|
|
64
|
+
local: result.local,
|
|
65
|
+
token: result.token,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (action === 'stop-local') {
|
|
70
|
+
return NextResponse.json({
|
|
71
|
+
ok: true,
|
|
72
|
+
local: stopOpenClawLocalDeploy(),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (action === 'bundle') {
|
|
77
|
+
const bundle = buildOpenClawDeployBundle({
|
|
78
|
+
template: parseTemplate(body.template),
|
|
79
|
+
target: typeof body.target === 'string' ? body.target : null,
|
|
80
|
+
token: typeof body.token === 'string' ? body.token : null,
|
|
81
|
+
scheme: body.scheme === 'http' ? 'http' : 'https',
|
|
82
|
+
port: parsePort(body.port),
|
|
83
|
+
provider: parseProvider(body.provider),
|
|
84
|
+
})
|
|
85
|
+
return NextResponse.json({
|
|
86
|
+
ok: true,
|
|
87
|
+
bundle,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return NextResponse.json({ ok: false, error: 'Unknown deploy action.' }, { status: 400 })
|
|
92
|
+
} catch (err: unknown) {
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{
|
|
95
|
+
ok: false,
|
|
96
|
+
error: err instanceof Error ? err.message : 'OpenClaw deploy action failed.',
|
|
97
|
+
},
|
|
98
|
+
{ status: 500 },
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -310,6 +310,19 @@ const COMMAND_GROUPS = [
|
|
|
310
310
|
description: 'OpenClaw discovery, gateway control, and runtime APIs',
|
|
311
311
|
commands: [
|
|
312
312
|
cmd('discover', 'GET', '/openclaw/discover', 'Discover OpenClaw gateways'),
|
|
313
|
+
cmd('deploy-status', 'GET', '/openclaw/deploy', 'Get managed OpenClaw deploy status'),
|
|
314
|
+
cmd('deploy-local-start', 'POST', '/openclaw/deploy', 'Start a managed local OpenClaw deployment (use --data JSON for port/token overrides)', {
|
|
315
|
+
expectsJsonBody: true,
|
|
316
|
+
defaultBody: { action: 'start-local' },
|
|
317
|
+
}),
|
|
318
|
+
cmd('deploy-local-stop', 'POST', '/openclaw/deploy', 'Stop the managed local OpenClaw deployment', {
|
|
319
|
+
expectsJsonBody: true,
|
|
320
|
+
defaultBody: { action: 'stop-local' },
|
|
321
|
+
}),
|
|
322
|
+
cmd('deploy-bundle', 'POST', '/openclaw/deploy', 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)', {
|
|
323
|
+
expectsJsonBody: true,
|
|
324
|
+
defaultBody: { action: 'bundle' },
|
|
325
|
+
}),
|
|
313
326
|
cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'),
|
|
314
327
|
cmd('gateway-status', 'GET', '/openclaw/gateway', 'Check OpenClaw gateway connection status'),
|
|
315
328
|
cmd('gateway', 'POST', '/openclaw/gateway', 'Call OpenClaw gateway RPC/control action', { expectsJsonBody: true }),
|
package/src/cli/index.test.js
CHANGED
|
@@ -163,6 +163,40 @@ test('runCli sends authenticated request and emits compact JSON when --json is s
|
|
|
163
163
|
assert.equal(stderr.toString(), '')
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
test('openclaw deploy bundle command merges action with provided JSON body', async () => {
|
|
167
|
+
const stdout = makeWritable()
|
|
168
|
+
const stderr = makeWritable()
|
|
169
|
+
const calls = []
|
|
170
|
+
|
|
171
|
+
const fetchImpl = async (url, init) => {
|
|
172
|
+
calls.push({ url: String(url), init })
|
|
173
|
+
return jsonResponse({ ok: true, bundle: { template: 'docker' } })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const exitCode = await runCli(
|
|
177
|
+
['openclaw', 'deploy-bundle', '--data', '{"template":"docker","target":"openclaw.example.com"}', '--json'],
|
|
178
|
+
{
|
|
179
|
+
fetchImpl,
|
|
180
|
+
stdout,
|
|
181
|
+
stderr,
|
|
182
|
+
env: {},
|
|
183
|
+
cwd: process.cwd(),
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert.equal(exitCode, 0)
|
|
188
|
+
assert.equal(calls.length, 1)
|
|
189
|
+
assert.match(calls[0].url, /\/api\/openclaw\/deploy$/)
|
|
190
|
+
assert.equal(calls[0].init.method, 'POST')
|
|
191
|
+
assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
|
|
192
|
+
action: 'bundle',
|
|
193
|
+
template: 'docker',
|
|
194
|
+
target: 'openclaw.example.com',
|
|
195
|
+
})
|
|
196
|
+
assert.equal(stdout.toString().trim(), '{"ok":true,"bundle":{"template":"docker"}}')
|
|
197
|
+
assert.equal(stderr.toString(), '')
|
|
198
|
+
})
|
|
199
|
+
|
|
166
200
|
test('runCli falls back to platform-api-key.txt when env key is missing', async () => {
|
|
167
201
|
const stdout = makeWritable()
|
|
168
202
|
const stderr = makeWritable()
|
package/src/cli/spec.js
CHANGED
|
@@ -211,6 +211,25 @@ const COMMAND_GROUPS = {
|
|
|
211
211
|
description: 'OpenClaw discovery, gateway control, and runtime APIs',
|
|
212
212
|
commands: {
|
|
213
213
|
discover: { description: 'Discover OpenClaw gateways', method: 'GET', path: '/openclaw/discover' },
|
|
214
|
+
'deploy-status': { description: 'Get managed OpenClaw deploy status', method: 'GET', path: '/openclaw/deploy' },
|
|
215
|
+
'deploy-local-start': {
|
|
216
|
+
description: 'Start a managed local OpenClaw deployment (use --data JSON for port/token overrides)',
|
|
217
|
+
method: 'POST',
|
|
218
|
+
path: '/openclaw/deploy',
|
|
219
|
+
staticBody: { action: 'start-local' },
|
|
220
|
+
},
|
|
221
|
+
'deploy-local-stop': {
|
|
222
|
+
description: 'Stop the managed local OpenClaw deployment',
|
|
223
|
+
method: 'POST',
|
|
224
|
+
path: '/openclaw/deploy',
|
|
225
|
+
staticBody: { action: 'stop-local' },
|
|
226
|
+
},
|
|
227
|
+
'deploy-bundle': {
|
|
228
|
+
description: 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)',
|
|
229
|
+
method: 'POST',
|
|
230
|
+
path: '/openclaw/deploy',
|
|
231
|
+
staticBody: { action: 'bundle' },
|
|
232
|
+
},
|
|
214
233
|
directory: { description: 'List directory entries from running OpenClaw connectors', method: 'GET', path: '/openclaw/directory' },
|
|
215
234
|
'gateway-status': { description: 'Check OpenClaw gateway connection status', method: 'GET', path: '/openclaw/gateway' },
|
|
216
235
|
gateway: { description: 'Call OpenClaw gateway RPC/control action', method: 'POST', path: '/openclaw/gateway' },
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
|
+
import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import type { ProviderType, Credential, GatewayProfile } from '@/types'
|
|
7
8
|
import {
|
|
@@ -142,12 +143,6 @@ function isLocalOpenClawEndpoint(value: string | null | undefined): boolean {
|
|
|
142
143
|
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0'
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
function resolveOpenClawPort(value: string | null | undefined): number {
|
|
146
|
-
const parsed = parseProviderUrl(value)
|
|
147
|
-
const port = parsed ? Number(parsed.port) : NaN
|
|
148
|
-
return Number.isFinite(port) && port > 0 ? port : 18789
|
|
149
|
-
}
|
|
150
|
-
|
|
151
146
|
function resolveOpenClawDashboardUrl(value: string | null | undefined): string {
|
|
152
147
|
const parsed = parseProviderUrl(value)
|
|
153
148
|
if (!parsed) return 'http://localhost:18789'
|
|
@@ -342,7 +337,6 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
342
337
|
const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
|
|
343
338
|
const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
|
|
344
339
|
const [providerSuggestedModel, setProviderSuggestedModel] = useState('')
|
|
345
|
-
const [commandCopyState, setCommandCopyState] = useState<'idle' | 'copied' | 'failed'>('idle')
|
|
346
340
|
|
|
347
341
|
const [doctorState, setDoctorState] = useState<'idle' | 'checking' | 'done' | 'error'>('idle')
|
|
348
342
|
const [doctorError, setDoctorError] = useState('')
|
|
@@ -381,9 +375,6 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
381
375
|
? resolveOpenClawDashboardUrl(openClawEndpointValue)
|
|
382
376
|
: null
|
|
383
377
|
const openClawLocal = provider === 'openclaw' ? isLocalOpenClawEndpoint(openClawEndpointValue) : false
|
|
384
|
-
const openClawPort = provider === 'openclaw' ? resolveOpenClawPort(openClawEndpointValue) : 18789
|
|
385
|
-
const openClawLocalCommand = `npx openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
|
|
386
|
-
const openClawLocalCommandPnpm = `pnpm openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
|
|
387
378
|
|
|
388
379
|
const resetProviderForm = () => {
|
|
389
380
|
setProvider(null)
|
|
@@ -396,7 +387,6 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
396
387
|
setCheckErrorCode(null)
|
|
397
388
|
setOpenclawDeviceId(null)
|
|
398
389
|
setProviderSuggestedModel('')
|
|
399
|
-
setCommandCopyState('idle')
|
|
400
390
|
setError('')
|
|
401
391
|
}
|
|
402
392
|
|
|
@@ -447,11 +437,31 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
447
437
|
setCheckErrorCode(null)
|
|
448
438
|
setOpenclawDeviceId(null)
|
|
449
439
|
setProviderSuggestedModel(getDefaultModelForProvider(nextProvider))
|
|
450
|
-
setCommandCopyState('idle')
|
|
451
440
|
setError('')
|
|
452
441
|
setStep('connect')
|
|
453
442
|
}
|
|
454
443
|
|
|
444
|
+
const applyOpenClawDeployPatch = (patch: {
|
|
445
|
+
endpoint?: string
|
|
446
|
+
token?: string
|
|
447
|
+
name?: string
|
|
448
|
+
}) => {
|
|
449
|
+
if (patch.endpoint) {
|
|
450
|
+
setEndpoint(patch.endpoint)
|
|
451
|
+
}
|
|
452
|
+
if (patch.token) {
|
|
453
|
+
setApiKey(patch.token)
|
|
454
|
+
setCredentialId(null)
|
|
455
|
+
}
|
|
456
|
+
if (patch.name && (!providerLabel.trim() || providerLabel.trim() === (selectedProvider?.name || ''))) {
|
|
457
|
+
setProviderLabel(patch.name)
|
|
458
|
+
}
|
|
459
|
+
setCheckState('idle')
|
|
460
|
+
setCheckMessage('')
|
|
461
|
+
setCheckErrorCode(null)
|
|
462
|
+
setError('')
|
|
463
|
+
}
|
|
464
|
+
|
|
455
465
|
const runConnectionCheck = async (): Promise<boolean> => {
|
|
456
466
|
if (!provider || !selectedProvider) return false
|
|
457
467
|
if (requiresKey && !apiKey.trim()) {
|
|
@@ -603,17 +613,6 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
603
613
|
}))
|
|
604
614
|
}
|
|
605
615
|
|
|
606
|
-
const copyOpenClawLocalCommand = async () => {
|
|
607
|
-
try {
|
|
608
|
-
await navigator.clipboard.writeText(openClawLocalCommand)
|
|
609
|
-
setCommandCopyState('copied')
|
|
610
|
-
window.setTimeout(() => setCommandCopyState('idle'), 1200)
|
|
611
|
-
} catch {
|
|
612
|
-
setCommandCopyState('failed')
|
|
613
|
-
window.setTimeout(() => setCommandCopyState('idle'), 1800)
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
616
|
const createAgentsAndFinish = async () => {
|
|
618
617
|
const enabledDrafts = draftAgents.filter((draft) => draft.enabled)
|
|
619
618
|
if (enabledDrafts.some((draft) => !draft.provider)) {
|
|
@@ -1063,6 +1062,16 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1063
1062
|
|
|
1064
1063
|
{provider === 'openclaw' && (
|
|
1065
1064
|
<div className="rounded-[14px] border border-white/[0.08] bg-surface p-4 space-y-4">
|
|
1065
|
+
<OpenClawDeployPanel
|
|
1066
|
+
compact
|
|
1067
|
+
endpoint={openClawEndpointValue}
|
|
1068
|
+
token={apiKey}
|
|
1069
|
+
suggestedName={providerLabel || selectedProvider.name}
|
|
1070
|
+
title="Smart Deploy OpenClaw"
|
|
1071
|
+
description="Launch the bundled official OpenClaw gateway locally, or generate an official-image VPS bundle for major providers without relying on third-party deployment services."
|
|
1072
|
+
onApply={applyOpenClawDeployPatch}
|
|
1073
|
+
/>
|
|
1074
|
+
|
|
1066
1075
|
<div className="grid gap-3 md:grid-cols-2">
|
|
1067
1076
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1068
1077
|
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Remote gateway</div>
|
|
@@ -1077,37 +1086,12 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1077
1086
|
</p>
|
|
1078
1087
|
</div>
|
|
1079
1088
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1080
|
-
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">
|
|
1089
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Safe defaults</div>
|
|
1081
1090
|
<p className="text-[13px] text-text-2 leading-relaxed">
|
|
1082
|
-
|
|
1091
|
+
Smart Deploy generates a gateway token for you, defaults to the standard OpenClaw ports, and prefills this setup form automatically.
|
|
1083
1092
|
</p>
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1086
|
-
{openClawLocalCommand}
|
|
1087
|
-
</code>
|
|
1088
|
-
</div>
|
|
1089
|
-
<div className="mt-2 flex items-center gap-2">
|
|
1090
|
-
<button
|
|
1091
|
-
type="button"
|
|
1092
|
-
onClick={copyOpenClawLocalCommand}
|
|
1093
|
-
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
|
|
1094
|
-
>
|
|
1095
|
-
{commandCopyState === 'copied'
|
|
1096
|
-
? 'Copied'
|
|
1097
|
-
: commandCopyState === 'failed'
|
|
1098
|
-
? 'Copy failed'
|
|
1099
|
-
: 'Copy command'}
|
|
1100
|
-
</button>
|
|
1101
|
-
<button
|
|
1102
|
-
type="button"
|
|
1103
|
-
onClick={() => { setEndpoint(selectedProvider.defaultEndpoint || 'http://localhost:18789/v1'); setCheckState('idle'); setCheckMessage(''); setCheckErrorCode(null) }}
|
|
1104
|
-
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
|
|
1105
|
-
>
|
|
1106
|
-
Use local default
|
|
1107
|
-
</button>
|
|
1108
|
-
</div>
|
|
1109
|
-
<p className="mt-2 text-[11px] text-text-3">
|
|
1110
|
-
In a source checkout, use <code className="text-text-2">{openClawLocalCommandPnpm}</code>.
|
|
1093
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1094
|
+
Local quickstart uses the bundled official OpenClaw CLI. Remote quickstart uses the official OpenClaw Docker image or the official repo for managed hosts.
|
|
1111
1095
|
</p>
|
|
1112
1096
|
</div>
|
|
1113
1097
|
</div>
|