@swarmclawai/swarmclaw 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +19 -10
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +16 -0
  4. package/src/app/api/agents/route.ts +2 -0
  5. package/src/app/api/chats/[id]/route.ts +21 -1
  6. package/src/app/api/chats/route.ts +13 -1
  7. package/src/app/api/connectors/[id]/route.ts +20 -2
  8. package/src/app/api/connectors/route.ts +12 -8
  9. package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
  10. package/src/app/api/external-agents/[id]/route.ts +38 -6
  11. package/src/app/api/external-agents/route.ts +17 -1
  12. package/src/app/api/gateways/[id]/health/route.ts +8 -0
  13. package/src/app/api/gateways/[id]/route.ts +53 -1
  14. package/src/app/api/gateways/route.ts +53 -0
  15. package/src/app/api/openclaw/deploy/route.ts +139 -0
  16. package/src/app/api/projects/[id]/route.ts +6 -2
  17. package/src/app/api/projects/route.ts +4 -3
  18. package/src/app/api/secrets/[id]/route.ts +1 -0
  19. package/src/app/api/secrets/route.ts +2 -1
  20. package/src/app/api/settings/route.ts +2 -0
  21. package/src/cli/index.js +40 -0
  22. package/src/cli/index.test.js +68 -0
  23. package/src/cli/spec.js +60 -0
  24. package/src/components/agents/agent-sheet.tsx +281 -33
  25. package/src/components/auth/setup-wizard.tsx +75 -2
  26. package/src/components/chat/chat-area.tsx +36 -19
  27. package/src/components/chat/chat-header.tsx +4 -0
  28. package/src/components/chat/delegation-banner.test.ts +14 -1
  29. package/src/components/chat/delegation-banner.tsx +1 -1
  30. package/src/components/gateways/gateway-sheet.tsx +140 -8
  31. package/src/components/layout/app-layout.tsx +40 -23
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
  33. package/src/components/projects/project-detail.tsx +217 -0
  34. package/src/components/projects/project-sheet.tsx +176 -4
  35. package/src/components/providers/provider-list.tsx +221 -17
  36. package/src/components/shared/settings/section-capability-policy.tsx +38 -0
  37. package/src/components/shared/settings/section-voice.tsx +11 -3
  38. package/src/components/tasks/approvals-panel.tsx +177 -18
  39. package/src/components/tasks/task-board.tsx +137 -23
  40. package/src/components/tasks/task-card.tsx +29 -0
  41. package/src/components/tasks/task-sheet.tsx +16 -4
  42. package/src/lib/server/agent-runtime-config.ts +142 -7
  43. package/src/lib/server/agent-thread-session.ts +9 -1
  44. package/src/lib/server/capability-router.test.ts +22 -0
  45. package/src/lib/server/capability-router.ts +54 -18
  46. package/src/lib/server/chat-execution.ts +33 -3
  47. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  48. package/src/lib/server/connectors/manager.ts +99 -74
  49. package/src/lib/server/daemon-state.ts +83 -46
  50. package/src/lib/server/elevenlabs.test.ts +59 -1
  51. package/src/lib/server/heartbeat-service.ts +5 -1
  52. package/src/lib/server/main-agent-loop.test.ts +260 -0
  53. package/src/lib/server/main-agent-loop.ts +559 -14
  54. package/src/lib/server/openclaw-deploy.test.ts +8 -0
  55. package/src/lib/server/openclaw-deploy.ts +679 -19
  56. package/src/lib/server/orchestrator-lg.ts +1 -0
  57. package/src/lib/server/orchestrator.ts +11 -0
  58. package/src/lib/server/plugins.ts +6 -1
  59. package/src/lib/server/project-context.ts +162 -0
  60. package/src/lib/server/project-utils.ts +150 -0
  61. package/src/lib/server/queue-followups.test.ts +147 -2
  62. package/src/lib/server/queue.ts +278 -8
  63. package/src/lib/server/session-run-manager.ts +31 -0
  64. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  65. package/src/lib/server/session-tools/connector.ts +26 -1
  66. package/src/lib/server/session-tools/context.ts +5 -0
  67. package/src/lib/server/session-tools/crud.ts +265 -76
  68. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  69. package/src/lib/server/session-tools/delegate.ts +38 -2
  70. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  71. package/src/lib/server/session-tools/memory.ts +14 -2
  72. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  73. package/src/lib/server/session-tools/platform.ts +60 -19
  74. package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
  75. package/src/lib/server/session-tools/web.ts +153 -6
  76. package/src/lib/server/stream-agent-chat.test.ts +27 -2
  77. package/src/lib/server/stream-agent-chat.ts +104 -30
  78. package/src/lib/server/tool-aliases.ts +2 -0
  79. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  80. package/src/lib/server/tool-capability-policy.ts +29 -1
  81. package/src/lib/server/tool-planning.test.ts +44 -0
  82. package/src/lib/server/tool-planning.ts +269 -0
  83. package/src/lib/setup-defaults.ts +2 -2
  84. package/src/lib/tool-definitions.ts +2 -1
  85. package/src/lib/validation/schemas.ts +9 -0
  86. package/src/types/index.ts +104 -0
@@ -1,11 +1,19 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import {
3
3
  buildOpenClawDeployBundle,
4
+ deployOpenClawOverSsh,
4
5
  getOpenClawLocalDeployStatus,
6
+ getOpenClawRemoteDeployStatus,
7
+ restartOpenClawLocalDeploy,
8
+ runOpenClawRemoteLifecycle,
5
9
  startOpenClawLocalDeploy,
6
10
  stopOpenClawLocalDeploy,
11
+ verifyOpenClawDeployment,
12
+ type OpenClawExposurePreset,
7
13
  type OpenClawRemoteDeployProvider,
8
14
  type OpenClawRemoteDeployTemplate,
15
+ type OpenClawSshConfig,
16
+ type OpenClawUseCaseTemplate,
9
17
  } from '@/lib/server/openclaw-deploy'
10
18
 
11
19
  export const dynamic = 'force-dynamic'
@@ -19,6 +27,12 @@ function parsePort(value: unknown): number | undefined {
19
27
  return Number.isFinite(parsed) ? parsed : undefined
20
28
  }
21
29
 
30
+ function parseIntBounded(value: unknown, fallback: number, min: number, max: number): number {
31
+ const parsed = parsePort(value)
32
+ if (typeof parsed !== 'number') return fallback
33
+ return Math.max(min, Math.min(max, parsed))
34
+ }
35
+
22
36
  function parseTemplate(value: unknown): OpenClawRemoteDeployTemplate | undefined {
23
37
  if (value === 'docker' || value === 'render' || value === 'fly' || value === 'railway') {
24
38
  return value
@@ -43,9 +57,48 @@ function parseProvider(value: unknown): OpenClawRemoteDeployProvider | undefined
43
57
  return undefined
44
58
  }
45
59
 
60
+ function parseUseCase(value: unknown): OpenClawUseCaseTemplate | undefined {
61
+ if (
62
+ value === 'local-dev'
63
+ || value === 'single-vps'
64
+ || value === 'private-tailnet'
65
+ || value === 'browser-heavy'
66
+ || value === 'team-control'
67
+ ) {
68
+ return value
69
+ }
70
+ return undefined
71
+ }
72
+
73
+ function parseExposure(value: unknown): OpenClawExposurePreset | undefined {
74
+ if (
75
+ value === 'private-lan'
76
+ || value === 'tailscale'
77
+ || value === 'caddy'
78
+ || value === 'nginx'
79
+ || value === 'ssh-tunnel'
80
+ ) {
81
+ return value
82
+ }
83
+ return undefined
84
+ }
85
+
86
+ function parseSsh(value: unknown): Partial<OpenClawSshConfig> | null {
87
+ if (!value || typeof value !== 'object') return null
88
+ const ssh = value as Record<string, unknown>
89
+ return {
90
+ host: typeof ssh.host === 'string' ? ssh.host : '',
91
+ user: typeof ssh.user === 'string' ? ssh.user : null,
92
+ port: parsePort(ssh.port),
93
+ keyPath: typeof ssh.keyPath === 'string' ? ssh.keyPath : null,
94
+ targetDir: typeof ssh.targetDir === 'string' ? ssh.targetDir : null,
95
+ }
96
+ }
97
+
46
98
  export async function GET() {
47
99
  return NextResponse.json({
48
100
  local: getOpenClawLocalDeployStatus(),
101
+ remote: getOpenClawRemoteDeployStatus(),
49
102
  })
50
103
  }
51
104
 
@@ -73,6 +126,18 @@ export async function POST(req: Request) {
73
126
  })
74
127
  }
75
128
 
129
+ if (action === 'restart-local') {
130
+ const result = await restartOpenClawLocalDeploy({
131
+ port: parsePort(body.port),
132
+ token: typeof body.token === 'string' ? body.token : null,
133
+ })
134
+ return NextResponse.json({
135
+ ok: true,
136
+ local: result.local,
137
+ token: result.token,
138
+ })
139
+ }
140
+
76
141
  if (action === 'bundle') {
77
142
  const bundle = buildOpenClawDeployBundle({
78
143
  template: parseTemplate(body.template),
@@ -81,6 +146,8 @@ export async function POST(req: Request) {
81
146
  scheme: body.scheme === 'http' ? 'http' : 'https',
82
147
  port: parsePort(body.port),
83
148
  provider: parseProvider(body.provider),
149
+ useCase: parseUseCase(body.useCase),
150
+ exposure: parseExposure(body.exposure),
84
151
  })
85
152
  return NextResponse.json({
86
153
  ok: true,
@@ -88,6 +155,78 @@ export async function POST(req: Request) {
88
155
  })
89
156
  }
90
157
 
158
+ if (action === 'ssh-deploy') {
159
+ const result = await deployOpenClawOverSsh({
160
+ template: parseTemplate(body.template),
161
+ target: typeof body.target === 'string' ? body.target : null,
162
+ token: typeof body.token === 'string' ? body.token : null,
163
+ scheme: body.scheme === 'http' ? 'http' : 'https',
164
+ port: parsePort(body.port),
165
+ provider: parseProvider(body.provider),
166
+ useCase: parseUseCase(body.useCase),
167
+ exposure: parseExposure(body.exposure),
168
+ ssh: parseSsh(body.ssh),
169
+ })
170
+ return NextResponse.json({
171
+ ok: result.ok,
172
+ remote: getOpenClawRemoteDeployStatus(),
173
+ processId: result.processId || null,
174
+ token: result.token,
175
+ bundle: result.bundle,
176
+ summary: result.summary,
177
+ commandPreview: result.commandPreview,
178
+ })
179
+ }
180
+
181
+ if (
182
+ action === 'remote-start'
183
+ || action === 'remote-stop'
184
+ || action === 'remote-restart'
185
+ || action === 'remote-upgrade'
186
+ || action === 'remote-backup'
187
+ || action === 'remote-restore'
188
+ || action === 'remote-rotate-token'
189
+ ) {
190
+ const actionMap = {
191
+ 'remote-start': 'start',
192
+ 'remote-stop': 'stop',
193
+ 'remote-restart': 'restart',
194
+ 'remote-upgrade': 'upgrade',
195
+ 'remote-backup': 'backup',
196
+ 'remote-restore': 'restore',
197
+ 'remote-rotate-token': 'rotate-token',
198
+ } as const
199
+ const lifecycleAction = action as keyof typeof actionMap
200
+ const result = await runOpenClawRemoteLifecycle({
201
+ action: actionMap[lifecycleAction],
202
+ ssh: parseSsh(body.ssh),
203
+ token: typeof body.token === 'string' ? body.token : null,
204
+ backupPath: typeof body.backupPath === 'string' ? body.backupPath : null,
205
+ })
206
+ return NextResponse.json({
207
+ ok: result.ok,
208
+ remote: getOpenClawRemoteDeployStatus(),
209
+ processId: result.processId || null,
210
+ token: result.token,
211
+ summary: result.summary,
212
+ commandPreview: result.commandPreview,
213
+ })
214
+ }
215
+
216
+ if (action === 'verify') {
217
+ const result = await verifyOpenClawDeployment({
218
+ endpoint: typeof body.endpoint === 'string' ? body.endpoint : null,
219
+ credentialId: typeof body.credentialId === 'string' ? body.credentialId : null,
220
+ token: typeof body.token === 'string' ? body.token : null,
221
+ model: typeof body.model === 'string' ? body.model : null,
222
+ timeoutMs: parseIntBounded(body.timeoutMs, 8000, 1000, 30000),
223
+ })
224
+ return NextResponse.json({
225
+ ok: result.ok,
226
+ verify: result,
227
+ })
228
+ }
229
+
91
230
  return NextResponse.json({ ok: false, error: 'Unknown deploy action.' }, { status: 400 })
92
231
  } catch (err: unknown) {
93
232
  return NextResponse.json(
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills } from '@/lib/server/storage'
2
+ import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills, loadSecrets, saveSecrets } from '@/lib/server/storage'
3
3
  import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+ import { ensureProjectWorkspace, normalizeProjectPatchInput } from '@/lib/server/project-utils'
4
5
  import { notify } from '@/lib/server/ws-hub'
5
6
 
6
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -17,12 +18,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
17
18
  const { id } = await params
18
19
  const body = await req.json()
19
20
  const result = mutateItem(ops, id, (project) => {
20
- Object.assign(project, body, { updatedAt: Date.now() })
21
+ const patch = normalizeProjectPatchInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
22
+ Object.assign(project, patch, { updatedAt: Date.now() })
21
23
  delete (project as Record<string, unknown>).id
22
24
  project.id = id
23
25
  return project
24
26
  })
25
27
  if (!result) return notFound()
28
+ ensureProjectWorkspace(id, result.name)
26
29
  return NextResponse.json(result)
27
30
  }
28
31
 
@@ -50,6 +53,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
50
53
  clearProjectId(loadTasks, saveTasks, 'tasks')
51
54
  clearProjectId(loadSchedules, saveSchedules, 'schedules')
52
55
  clearProjectId(loadSkills, saveSkills, 'skills')
56
+ clearProjectId(loadSecrets, saveSecrets, 'secrets')
53
57
 
54
58
  return NextResponse.json({ ok: true })
55
59
  }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadProjects, saveProjects } from '@/lib/server/storage'
4
+ import { ensureProjectWorkspace, normalizeProjectCreateInput } from '@/lib/server/project-utils'
4
5
  import { notify } from '@/lib/server/ws-hub'
5
6
  export const dynamic = 'force-dynamic'
6
7
 
@@ -13,15 +14,15 @@ export async function POST(req: Request) {
13
14
  const id = genId()
14
15
  const now = Date.now()
15
16
  const projects = loadProjects()
17
+ const normalized = normalizeProjectCreateInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
16
18
  projects[id] = {
17
19
  id,
18
- name: body.name || 'Unnamed Project',
19
- description: body.description || '',
20
- color: body.color || undefined,
20
+ ...normalized,
21
21
  createdAt: now,
22
22
  updatedAt: now,
23
23
  }
24
24
  saveProjects(projects)
25
+ ensureProjectWorkspace(id, projects[id].name)
25
26
  notify('projects')
26
27
  return NextResponse.json(projects[id])
27
28
  }
@@ -19,6 +19,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
19
19
  if (body.service !== undefined) secret.service = body.service
20
20
  if (body.scope !== undefined) secret.scope = body.scope
21
21
  if (body.agentIds !== undefined) secret.agentIds = body.agentIds
22
+ if (body.projectId !== undefined) secret.projectId = body.projectId || undefined
22
23
  secret.updatedAt = Date.now()
23
24
  return secret
24
25
  })
@@ -10,7 +10,7 @@ export async function GET(_req: Request) {
10
10
  const safe = Object.fromEntries(
11
11
  Object.entries(secrets).map(([id, s]: [string, any]) => [
12
12
  id,
13
- { id: s.id, name: s.name, service: s.service, scope: s.scope, agentIds: s.agentIds, createdAt: s.createdAt, updatedAt: s.updatedAt },
13
+ { id: s.id, name: s.name, service: s.service, scope: s.scope, agentIds: s.agentIds, projectId: s.projectId, createdAt: s.createdAt, updatedAt: s.updatedAt },
14
14
  ])
15
15
  )
16
16
  return NextResponse.json(safe)
@@ -33,6 +33,7 @@ export async function POST(req: Request) {
33
33
  encryptedValue: encryptKey(body.value),
34
34
  scope: body.scope || 'global',
35
35
  agentIds: body.agentIds || [],
36
+ projectId: typeof body.projectId === 'string' && body.projectId.trim() ? body.projectId.trim() : undefined,
36
37
  createdAt: now,
37
38
  updatedAt: now,
38
39
  }
@@ -126,6 +126,8 @@ export async function PUT(req: Request) {
126
126
  settings.taskQualityGateRequireVerification = parseBoolSetting(settings.taskQualityGateRequireVerification, false)
127
127
  settings.taskQualityGateRequireArtifact = parseBoolSetting(settings.taskQualityGateRequireArtifact, false)
128
128
  settings.taskQualityGateRequireReport = parseBoolSetting(settings.taskQualityGateRequireReport, false)
129
+ settings.taskManagementEnabled = parseBoolSetting(settings.taskManagementEnabled, true)
130
+ settings.projectManagementEnabled = parseBoolSetting(settings.projectManagementEnabled, true)
129
131
  settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
130
132
  settings.sessionResetMode = settings.sessionResetMode === 'daily' ? 'daily' : settings.sessionResetMode === 'idle' ? 'idle' : null
131
133
  settings.sessionIdleTimeoutSec = parseIntSetting(
package/src/cli/index.js CHANGED
@@ -319,10 +319,50 @@ const COMMAND_GROUPS = [
319
319
  expectsJsonBody: true,
320
320
  defaultBody: { action: 'stop-local' },
321
321
  }),
322
+ cmd('deploy-local-restart', 'POST', '/openclaw/deploy', 'Restart the managed local OpenClaw deployment (use --data JSON for port/token overrides)', {
323
+ expectsJsonBody: true,
324
+ defaultBody: { action: 'restart-local' },
325
+ }),
322
326
  cmd('deploy-bundle', 'POST', '/openclaw/deploy', 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)', {
323
327
  expectsJsonBody: true,
324
328
  defaultBody: { action: 'bundle' },
325
329
  }),
330
+ cmd('deploy-ssh', 'POST', '/openclaw/deploy', 'Push the official-image OpenClaw bundle to a remote host over SSH (use --data JSON for target/ssh/provider)', {
331
+ expectsJsonBody: true,
332
+ defaultBody: { action: 'ssh-deploy' },
333
+ }),
334
+ cmd('deploy-verify', 'POST', '/openclaw/deploy', 'Verify an OpenClaw endpoint/token pair (use --data JSON for endpoint/token)', {
335
+ expectsJsonBody: true,
336
+ defaultBody: { action: 'verify' },
337
+ }),
338
+ cmd('remote-start', 'POST', '/openclaw/deploy', 'Start a remote SSH-managed OpenClaw stack', {
339
+ expectsJsonBody: true,
340
+ defaultBody: { action: 'remote-start' },
341
+ }),
342
+ cmd('remote-stop', 'POST', '/openclaw/deploy', 'Stop a remote SSH-managed OpenClaw stack', {
343
+ expectsJsonBody: true,
344
+ defaultBody: { action: 'remote-stop' },
345
+ }),
346
+ cmd('remote-restart', 'POST', '/openclaw/deploy', 'Restart a remote SSH-managed OpenClaw stack', {
347
+ expectsJsonBody: true,
348
+ defaultBody: { action: 'remote-restart' },
349
+ }),
350
+ cmd('remote-upgrade', 'POST', '/openclaw/deploy', 'Upgrade a remote SSH-managed OpenClaw stack', {
351
+ expectsJsonBody: true,
352
+ defaultBody: { action: 'remote-upgrade' },
353
+ }),
354
+ cmd('remote-backup', 'POST', '/openclaw/deploy', 'Create a remote backup on an SSH-managed OpenClaw host', {
355
+ expectsJsonBody: true,
356
+ defaultBody: { action: 'remote-backup' },
357
+ }),
358
+ cmd('remote-restore', 'POST', '/openclaw/deploy', 'Restore a remote backup on an SSH-managed OpenClaw host', {
359
+ expectsJsonBody: true,
360
+ defaultBody: { action: 'remote-restore' },
361
+ }),
362
+ cmd('remote-rotate-token', 'POST', '/openclaw/deploy', 'Rotate the gateway token on an SSH-managed OpenClaw host', {
363
+ expectsJsonBody: true,
364
+ defaultBody: { action: 'remote-rotate-token' },
365
+ }),
326
366
  cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'),
327
367
  cmd('gateway-status', 'GET', '/openclaw/gateway', 'Check OpenClaw gateway connection status'),
328
368
  cmd('gateway', 'POST', '/openclaw/gateway', 'Call OpenClaw gateway RPC/control action', { expectsJsonBody: true }),
@@ -197,6 +197,74 @@ test('openclaw deploy bundle command merges action with provided JSON body', asy
197
197
  assert.equal(stderr.toString(), '')
198
198
  })
199
199
 
200
+ test('openclaw deploy ssh command merges action with provided JSON body', async () => {
201
+ const stdout = makeWritable()
202
+ const stderr = makeWritable()
203
+ const calls = []
204
+
205
+ const fetchImpl = async (url, init) => {
206
+ calls.push({ url: String(url), init })
207
+ return jsonResponse({ ok: true, processId: 'remote-1' })
208
+ }
209
+
210
+ const exitCode = await runCli(
211
+ ['openclaw', 'deploy-ssh', '--data', '{"target":"openclaw.example.com","ssh":{"host":"1.2.3.4"}}', '--json'],
212
+ {
213
+ fetchImpl,
214
+ stdout,
215
+ stderr,
216
+ env: {},
217
+ cwd: process.cwd(),
218
+ }
219
+ )
220
+
221
+ assert.equal(exitCode, 0)
222
+ assert.equal(calls.length, 1)
223
+ assert.match(calls[0].url, /\/api\/openclaw\/deploy$/)
224
+ assert.equal(calls[0].init.method, 'POST')
225
+ assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
226
+ action: 'ssh-deploy',
227
+ target: 'openclaw.example.com',
228
+ ssh: { host: '1.2.3.4' },
229
+ })
230
+ assert.equal(stdout.toString().trim(), '{"ok":true,"processId":"remote-1"}')
231
+ assert.equal(stderr.toString(), '')
232
+ })
233
+
234
+ test('openclaw remote restore command merges action with provided JSON body', async () => {
235
+ const stdout = makeWritable()
236
+ const stderr = makeWritable()
237
+ const calls = []
238
+
239
+ const fetchImpl = async (url, init) => {
240
+ calls.push({ url: String(url), init })
241
+ return jsonResponse({ ok: true, remote: { status: 'running' } })
242
+ }
243
+
244
+ const exitCode = await runCli(
245
+ ['openclaw', 'remote-restore', '--data', '{"backupPath":"/opt/openclaw/backups/latest.tgz","ssh":{"host":"1.2.3.4"}}', '--json'],
246
+ {
247
+ fetchImpl,
248
+ stdout,
249
+ stderr,
250
+ env: {},
251
+ cwd: process.cwd(),
252
+ }
253
+ )
254
+
255
+ assert.equal(exitCode, 0)
256
+ assert.equal(calls.length, 1)
257
+ assert.match(calls[0].url, /\/api\/openclaw\/deploy$/)
258
+ assert.equal(calls[0].init.method, 'POST')
259
+ assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
260
+ action: 'remote-restore',
261
+ backupPath: '/opt/openclaw/backups/latest.tgz',
262
+ ssh: { host: '1.2.3.4' },
263
+ })
264
+ assert.equal(stdout.toString().trim(), '{"ok":true,"remote":{"status":"running"}}')
265
+ assert.equal(stderr.toString(), '')
266
+ })
267
+
200
268
  test('runCli falls back to platform-api-key.txt when env key is missing', async () => {
201
269
  const stdout = makeWritable()
202
270
  const stderr = makeWritable()
package/src/cli/spec.js CHANGED
@@ -224,12 +224,72 @@ const COMMAND_GROUPS = {
224
224
  path: '/openclaw/deploy',
225
225
  staticBody: { action: 'stop-local' },
226
226
  },
227
+ 'deploy-local-restart': {
228
+ description: 'Restart the managed local OpenClaw deployment (use --data JSON for port/token overrides)',
229
+ method: 'POST',
230
+ path: '/openclaw/deploy',
231
+ staticBody: { action: 'restart-local' },
232
+ },
227
233
  'deploy-bundle': {
228
234
  description: 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)',
229
235
  method: 'POST',
230
236
  path: '/openclaw/deploy',
231
237
  staticBody: { action: 'bundle' },
232
238
  },
239
+ 'deploy-ssh': {
240
+ description: 'Push the official-image OpenClaw bundle to a remote host over SSH (use --data JSON for target/ssh/provider)',
241
+ method: 'POST',
242
+ path: '/openclaw/deploy',
243
+ staticBody: { action: 'ssh-deploy' },
244
+ },
245
+ 'deploy-verify': {
246
+ description: 'Verify an OpenClaw endpoint/token pair (use --data JSON for endpoint/token)',
247
+ method: 'POST',
248
+ path: '/openclaw/deploy',
249
+ staticBody: { action: 'verify' },
250
+ },
251
+ 'remote-start': {
252
+ description: 'Start a remote SSH-managed OpenClaw stack',
253
+ method: 'POST',
254
+ path: '/openclaw/deploy',
255
+ staticBody: { action: 'remote-start' },
256
+ },
257
+ 'remote-stop': {
258
+ description: 'Stop a remote SSH-managed OpenClaw stack',
259
+ method: 'POST',
260
+ path: '/openclaw/deploy',
261
+ staticBody: { action: 'remote-stop' },
262
+ },
263
+ 'remote-restart': {
264
+ description: 'Restart a remote SSH-managed OpenClaw stack',
265
+ method: 'POST',
266
+ path: '/openclaw/deploy',
267
+ staticBody: { action: 'remote-restart' },
268
+ },
269
+ 'remote-upgrade': {
270
+ description: 'Upgrade a remote SSH-managed OpenClaw stack',
271
+ method: 'POST',
272
+ path: '/openclaw/deploy',
273
+ staticBody: { action: 'remote-upgrade' },
274
+ },
275
+ 'remote-backup': {
276
+ description: 'Create a remote backup on an SSH-managed OpenClaw host',
277
+ method: 'POST',
278
+ path: '/openclaw/deploy',
279
+ staticBody: { action: 'remote-backup' },
280
+ },
281
+ 'remote-restore': {
282
+ description: 'Restore a remote backup on an SSH-managed OpenClaw host',
283
+ method: 'POST',
284
+ path: '/openclaw/deploy',
285
+ staticBody: { action: 'remote-restore' },
286
+ },
287
+ 'remote-rotate-token': {
288
+ description: 'Rotate the gateway token on an SSH-managed OpenClaw host',
289
+ method: 'POST',
290
+ path: '/openclaw/deploy',
291
+ staticBody: { action: 'remote-rotate-token' },
292
+ },
233
293
  directory: { description: 'List directory entries from running OpenClaw connectors', method: 'GET', path: '/openclaw/directory' },
234
294
  'gateway-status': { description: 'Check OpenClaw gateway connection status', method: 'GET', path: '/openclaw/gateway' },
235
295
  gateway: { description: 'Call OpenClaw gateway RPC/control action', method: 'POST', path: '/openclaw/gateway' },