@swarmclawai/swarmclaw 0.7.5 → 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/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/heartbeat-service.ts +5 -1
- 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
|
},
|
|
@@ -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>
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
5
|
+
import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import { api } from '@/lib/api-client'
|
|
7
8
|
import { toast } from 'sonner'
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
Credential,
|
|
11
|
+
OpenClawDevicePairRequest,
|
|
12
|
+
OpenClawNode,
|
|
13
|
+
OpenClawNodePairRequest,
|
|
14
|
+
OpenClawPairedDevice,
|
|
15
|
+
} from '@/types'
|
|
9
16
|
|
|
10
17
|
interface DiscoveryResult {
|
|
11
18
|
host: string
|
|
@@ -46,6 +53,7 @@ export function GatewaySheet() {
|
|
|
46
53
|
const [name, setName] = useState('')
|
|
47
54
|
const [endpoint, setEndpoint] = useState('http://localhost:18789')
|
|
48
55
|
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
56
|
+
const [tokenDraft, setTokenDraft] = useState('')
|
|
49
57
|
const [notes, setNotes] = useState('')
|
|
50
58
|
const [tags, setTags] = useState('')
|
|
51
59
|
const [isDefault, setIsDefault] = useState(false)
|
|
@@ -81,6 +89,7 @@ export function GatewaySheet() {
|
|
|
81
89
|
setName(editing.name)
|
|
82
90
|
setEndpoint(editing.endpoint)
|
|
83
91
|
setCredentialId(editing.credentialId || null)
|
|
92
|
+
setTokenDraft('')
|
|
84
93
|
setNotes(editing.notes || '')
|
|
85
94
|
setTags((editing.tags || []).join(', '))
|
|
86
95
|
setIsDefault(editing.isDefault === true)
|
|
@@ -89,6 +98,7 @@ export function GatewaySheet() {
|
|
|
89
98
|
setName('')
|
|
90
99
|
setEndpoint('http://localhost:18789')
|
|
91
100
|
setCredentialId(null)
|
|
101
|
+
setTokenDraft('')
|
|
92
102
|
setNotes('')
|
|
93
103
|
setTags('')
|
|
94
104
|
setIsDefault(gatewayProfiles.length === 0)
|
|
@@ -157,10 +167,19 @@ export function GatewaySheet() {
|
|
|
157
167
|
const handleSave = async () => {
|
|
158
168
|
setSaving(true)
|
|
159
169
|
try {
|
|
170
|
+
let nextCredentialId = credentialId
|
|
171
|
+
if (tokenDraft.trim()) {
|
|
172
|
+
const created = await api<Credential>('POST', '/credentials', {
|
|
173
|
+
provider: 'openclaw',
|
|
174
|
+
name: `${name.trim() || 'OpenClaw Gateway'} token`,
|
|
175
|
+
apiKey: tokenDraft.trim(),
|
|
176
|
+
})
|
|
177
|
+
nextCredentialId = created.id
|
|
178
|
+
}
|
|
160
179
|
const payload = {
|
|
161
180
|
name: name.trim() || 'OpenClaw Gateway',
|
|
162
181
|
endpoint: endpoint.trim() || 'http://localhost:18789',
|
|
163
|
-
credentialId:
|
|
182
|
+
credentialId: nextCredentialId || null,
|
|
164
183
|
notes: notes.trim() || null,
|
|
165
184
|
tags: tags.split(',').map((item) => item.trim()).filter(Boolean),
|
|
166
185
|
isDefault,
|
|
@@ -172,7 +191,7 @@ export function GatewaySheet() {
|
|
|
172
191
|
await api('POST', '/gateways', payload)
|
|
173
192
|
toast.success('Gateway added')
|
|
174
193
|
}
|
|
175
|
-
await loadGatewayProfiles()
|
|
194
|
+
await Promise.all([loadGatewayProfiles(), loadCredentials()])
|
|
176
195
|
onClose()
|
|
177
196
|
} catch (err: unknown) {
|
|
178
197
|
toast.error(err instanceof Error ? err.message : 'Failed to save gateway')
|
|
@@ -188,6 +207,7 @@ export function GatewaySheet() {
|
|
|
188
207
|
const params = new URLSearchParams()
|
|
189
208
|
params.set('endpoint', endpoint.trim() || 'http://localhost:18789')
|
|
190
209
|
if (credentialId) params.set('credentialId', credentialId)
|
|
210
|
+
if (tokenDraft.trim()) params.set('token', tokenDraft.trim())
|
|
191
211
|
const result = await api<{ ok: boolean; models: string[]; error?: string; hint?: string }>('GET', `/providers/openclaw/health?${params.toString()}`)
|
|
192
212
|
if (result.ok) {
|
|
193
213
|
setCheckMessage(`Connected. ${result.models?.length ? `${result.models.length} model${result.models.length === 1 ? '' : 's'} visible.` : 'Gateway responded normally.'}`)
|
|
@@ -278,6 +298,23 @@ export function GatewaySheet() {
|
|
|
278
298
|
|
|
279
299
|
const inputClass = 'w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow'
|
|
280
300
|
|
|
301
|
+
const applyDeployPatch = (patch: { endpoint?: string; token?: string; name?: string; notes?: string }) => {
|
|
302
|
+
if (patch.endpoint) {
|
|
303
|
+
setEndpoint(patch.endpoint)
|
|
304
|
+
setCheckMessage('')
|
|
305
|
+
}
|
|
306
|
+
if (patch.token) {
|
|
307
|
+
setTokenDraft(patch.token)
|
|
308
|
+
setCredentialId(null)
|
|
309
|
+
}
|
|
310
|
+
if (patch.name && !name.trim()) {
|
|
311
|
+
setName(patch.name)
|
|
312
|
+
}
|
|
313
|
+
if (patch.notes && !notes.trim()) {
|
|
314
|
+
setNotes(patch.notes)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
281
318
|
return (
|
|
282
319
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
283
320
|
<div className="mb-10">
|
|
@@ -309,6 +346,17 @@ export function GatewaySheet() {
|
|
|
309
346
|
<p className="text-[11px] text-text-3/60 mt-2">Remote HTTPS URLs and local loopback endpoints are both supported.</p>
|
|
310
347
|
</div>
|
|
311
348
|
|
|
349
|
+
<div className="mb-6">
|
|
350
|
+
<OpenClawDeployPanel
|
|
351
|
+
endpoint={endpoint}
|
|
352
|
+
token={tokenDraft}
|
|
353
|
+
suggestedName={name || null}
|
|
354
|
+
title="Deploy OpenClaw From SwarmClaw"
|
|
355
|
+
description="Use official OpenClaw sources only. Start it on this host, or generate a pre-configured remote bundle for VPS and hosted deployments."
|
|
356
|
+
onApply={applyDeployPatch}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
312
360
|
{discoveries.length > 0 && (
|
|
313
361
|
<div className="mb-6">
|
|
314
362
|
<div className="text-[12px] text-text-3/70 mb-2">Detected healthy gateways</div>
|
|
@@ -342,6 +390,18 @@ export function GatewaySheet() {
|
|
|
342
390
|
<option key={item.id} value={item.id}>{item.name}</option>
|
|
343
391
|
))}
|
|
344
392
|
</select>
|
|
393
|
+
<input
|
|
394
|
+
value={tokenDraft}
|
|
395
|
+
onChange={(e) => {
|
|
396
|
+
setTokenDraft(e.target.value)
|
|
397
|
+
if (e.target.value) setCredentialId(null)
|
|
398
|
+
}}
|
|
399
|
+
placeholder="Or paste/generate a new gateway token"
|
|
400
|
+
className={`${inputClass} mt-3 font-mono text-[13px]`}
|
|
401
|
+
/>
|
|
402
|
+
<p className="mt-2 text-[11px] text-text-3/60">
|
|
403
|
+
A pasted token is stored as a new encrypted OpenClaw credential when you save this gateway.
|
|
404
|
+
</p>
|
|
345
405
|
</div>
|
|
346
406
|
|
|
347
407
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|