index.js
1 'use strict' 2 3 /* eslint-disable @typescript-eslint/no-require-imports */ 4 5 const fs = require('fs') 6 const path = require('path') 7 8 function cmd(action, method, route, description, extra = {}) { 9 return { action, method, route, description, ...extra } 10 } 11 12 const COMMAND_GROUPS = [ 13 { 14 name: 'agents', 15 description: 'Manage agents', 16 commands: [ 17 cmd('list', 'GET', '/agents', 'List agents'), 18 cmd('get', 'GET', '/agents/:id', 'Get an agent by id'), 19 cmd('create', 'POST', '/agents', 'Create an agent', { expectsJsonBody: true }), 20 cmd('update', 'PUT', '/agents/:id', 'Update an agent', { expectsJsonBody: true }), 21 cmd('delete', 'DELETE', '/agents/:id', 'Delete an agent'), 22 cmd('trash', 'GET', '/agents/trash', 'List trashed agents'), 23 cmd('restore', 'POST', '/agents/trash', 'Restore a trashed agent', { expectsJsonBody: true }), 24 cmd('purge', 'DELETE', '/agents/trash', 'Permanently delete a trashed agent', { expectsJsonBody: true }), 25 cmd('thread', 'POST', '/agents/:id/thread', 'Get or create agent thread session'), 26 cmd('clone', 'POST', '/agents/:id/clone', 'Clone an agent'), 27 cmd('bulk-update', 'PATCH', '/agents/bulk', 'Bulk update agents', { expectsJsonBody: true }), 28 cmd('status', 'GET', '/agents/:id/status', 'Get live status for an agent'), 29 cmd('dream', 'GET', '/agents/:id/dream', 'Get agent dream config and recent cycles'), 30 cmd('dream-update', 'PATCH', '/agents/:id/dream', 'Update agent dream config', { expectsJsonBody: true }), 31 ], 32 }, 33 { 34 name: 'activity', 35 description: 'Query activity feed events', 36 commands: [ 37 cmd('list', 'GET', '/activity', 'List activity events (use --query limit=50, --query entityType=task, --query action=updated)'), 38 ], 39 }, 40 { 41 name: 'auth', 42 description: 'Access key auth helpers', 43 commands: [ 44 cmd('status', 'GET', '/auth', 'Check auth setup status'), 45 cmd('login', 'POST', '/auth', 'Validate an access key', { 46 expectsJsonBody: true, 47 bodyFlagMap: { key: 'key' }, 48 }), 49 ], 50 }, 51 { 52 name: 'autonomy', 53 description: 'Inspect supervisor incidents and reflection output', 54 commands: [ 55 cmd('incidents', 'GET', '/autonomy/incidents', 'List supervisor incidents (use --query sessionId=..., --query taskId=..., --query limit=50)'), 56 cmd('reflections', 'GET', '/autonomy/reflections', 'List run reflections (use --query sessionId=..., --query taskId=..., --query limit=50)'), 57 cmd('estop', 'GET', '/autonomy/estop', 'Get autonomy emergency-stop state'), 58 cmd('estop-set', 'POST', '/autonomy/estop', 'Engage or resume autonomy emergency-stop state', { expectsJsonBody: true }), 59 cmd('guardian-restore', 'POST', '/autonomy/guardian/restore', 'Restore the latest guardian checkpoint after approval', { expectsJsonBody: true }), 60 ], 61 }, 62 { 63 name: 'approvals', 64 description: 'List and resolve human-loop approvals', 65 commands: [ 66 cmd('list', 'GET', '/approvals', 'List pending human-loop approvals'), 67 cmd('resolve', 'POST', '/approvals', 'Resolve a human-loop approval', { expectsJsonBody: true }), 68 ], 69 }, 70 { 71 name: 'claude-skills', 72 description: 'Read local Claude skills directory metadata', 73 commands: [ 74 cmd('list', 'GET', '/claude-skills', 'List Claude skills discovered on host'), 75 ], 76 }, 77 { 78 name: 'clawhub', 79 description: 'Browse and install ClawHub skills', 80 commands: [ 81 cmd('search', 'GET', '/clawhub/search', 'Search ClawHub skills catalog'), 82 cmd('preview', 'POST', '/clawhub/preview', 'Preview a ClawHub skill install without writing files', { expectsJsonBody: true }), 83 cmd('install', 'POST', '/clawhub/install', 'Install a skill from ClawHub', { expectsJsonBody: true }), 84 ], 85 }, 86 { 87 name: 'chatrooms', 88 description: 'Manage multi-agent chatrooms', 89 commands: [ 90 cmd('list', 'GET', '/chatrooms', 'List chatrooms'), 91 cmd('get', 'GET', '/chatrooms/:id', 'Get chatroom by id'), 92 cmd('create', 'POST', '/chatrooms', 'Create a chatroom', { expectsJsonBody: true }), 93 cmd('update', 'PUT', '/chatrooms/:id', 'Update a chatroom', { expectsJsonBody: true }), 94 cmd('delete', 'DELETE', '/chatrooms/:id', 'Delete a chatroom'), 95 cmd('chat', 'POST', '/chatrooms/:id/chat', 'Post a message to a chatroom and stream agent replies', { 96 expectsJsonBody: true, 97 responseType: 'sse', 98 }), 99 cmd('add-member', 'POST', '/chatrooms/:id/members', 'Add an agent to a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }), 100 cmd('remove-member', 'DELETE', '/chatrooms/:id/members', 'Remove an agent from a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }), 101 cmd('react', 'POST', '/chatrooms/:id/reactions', 'Toggle a reaction on a chatroom message', { 102 expectsJsonBody: true, 103 }), 104 cmd('pin', 'POST', '/chatrooms/:id/pins', 'Toggle pin on a chatroom message', { 105 expectsJsonBody: true, 106 }), 107 cmd('moderate', 'POST', '/chatrooms/:id/moderate', 'Run moderation action on a chatroom', { expectsJsonBody: true }), 108 ], 109 }, 110 { 111 name: 'connectors', 112 description: 'Manage chat connectors', 113 commands: [ 114 cmd('list', 'GET', '/connectors', 'List connectors'), 115 cmd('get', 'GET', '/connectors/:id', 'Get connector'), 116 cmd('create', 'POST', '/connectors', 'Create connector', { expectsJsonBody: true }), 117 cmd('update', 'PUT', '/connectors/:id', 'Update connector', { expectsJsonBody: true }), 118 cmd('delete', 'DELETE', '/connectors/:id', 'Delete connector'), 119 cmd('webhook', 'POST', '/connectors/:id/webhook', 'Trigger connector webhook ingress', { expectsJsonBody: true }), 120 cmd('start', 'PUT', '/connectors/:id', 'Start connector', { 121 expectsJsonBody: true, 122 defaultBody: { action: 'start' }, 123 }), 124 cmd('stop', 'PUT', '/connectors/:id', 'Stop connector', { 125 expectsJsonBody: true, 126 defaultBody: { action: 'stop' }, 127 }), 128 cmd('repair', 'PUT', '/connectors/:id', 'Repair connector', { 129 expectsJsonBody: true, 130 defaultBody: { action: 'repair' }, 131 }), 132 cmd('health', 'GET', '/connectors/:id/health', 'Get connector health status'), 133 cmd('access-get', 'GET', '/connectors/:id/access', 'Get connector access and ownership state'), 134 cmd('access-set', 'PUT', '/connectors/:id/access', 'Update connector access and ownership state', { expectsJsonBody: true }), 135 cmd('doctor', 'GET', '/connectors/:id/doctor', 'Get connector doctor diagnostics'), 136 cmd('doctor-preview', 'POST', '/connectors/:id/doctor', 'Preview connector doctor diagnostics with temporary overrides', { expectsJsonBody: true }), 137 cmd('doctor-draft', 'POST', '/connectors/doctor', 'Preview connector doctor diagnostics before saving a connector', { expectsJsonBody: true }), 138 ], 139 }, 140 { 141 name: 'credentials', 142 description: 'Manage encrypted provider credentials', 143 commands: [ 144 cmd('list', 'GET', '/credentials', 'List credentials'), 145 cmd('get', 'GET', '/credentials/:id', 'Get credential metadata by id'), 146 cmd('create', 'POST', '/credentials', 'Create credential', { expectsJsonBody: true }), 147 cmd('delete', 'DELETE', '/credentials/:id', 'Delete credential'), 148 ], 149 }, 150 { 151 name: 'daemon', 152 description: 'Control background daemon', 153 commands: [ 154 cmd('status', 'GET', '/daemon', 'Get daemon status'), 155 cmd('action', 'POST', '/daemon', 'Set daemon action via JSON body', { expectsJsonBody: true }), 156 cmd('start', 'POST', '/daemon', 'Start daemon', { 157 expectsJsonBody: true, 158 defaultBody: { action: 'start' }, 159 }), 160 cmd('stop', 'POST', '/daemon', 'Stop daemon', { 161 expectsJsonBody: true, 162 defaultBody: { action: 'stop' }, 163 }), 164 cmd('health-check', 'POST', '/daemon/health-check', 'Run daemon health checks immediately'), 165 ], 166 }, 167 { 168 name: 'delegation-jobs', 169 description: 'Delegation job status', 170 commands: [ 171 cmd('list', 'GET', '/delegation-jobs', 'List active and recent delegation jobs'), 172 ], 173 }, 174 { 175 name: 'dirs', 176 description: 'Directory listing and native picker', 177 commands: [ 178 cmd('list', 'GET', '/dirs', 'List directories (use --query path=/abs/path)'), 179 cmd('pick', 'POST', '/dirs/pick', 'Open native picker (mode=file|folder)', { expectsJsonBody: true }), 180 ], 181 }, 182 { 183 name: 'perf', 184 description: 'Inspect or control runtime perf tracing', 185 commands: [ 186 cmd('status', 'GET', '/perf', 'Get current perf tracing status and recent entries'), 187 cmd('enable', 'POST', '/perf', 'Enable perf tracing and clear existing entries', { 188 expectsJsonBody: true, 189 defaultBody: { action: 'enable' }, 190 }), 191 cmd('disable', 'POST', '/perf', 'Disable perf tracing', { 192 expectsJsonBody: true, 193 defaultBody: { action: 'disable' }, 194 }), 195 cmd('clear', 'POST', '/perf', 'Clear recent perf entries', { 196 expectsJsonBody: true, 197 defaultBody: { action: 'clear' }, 198 }), 199 ], 200 }, 201 { 202 name: 'documents', 203 description: 'Manage documents', 204 commands: [ 205 cmd('list', 'GET', '/documents', 'List documents'), 206 cmd('get', 'GET', '/documents/:id', 'Get document by id'), 207 cmd('create', 'POST', '/documents', 'Create document', { expectsJsonBody: true }), 208 cmd('update', 'PUT', '/documents/:id', 'Update document', { expectsJsonBody: true }), 209 cmd('delete', 'DELETE', '/documents/:id', 'Delete document'), 210 cmd('revisions', 'GET', '/documents/:id/revisions', 'List document revisions'), 211 ], 212 }, 213 { 214 name: 'eval', 215 description: 'Run agent evaluation scenarios', 216 commands: [ 217 cmd('scenarios', 'GET', '/eval/scenarios', 'List available eval scenarios'), 218 cmd('suites', 'GET', '/eval/suites', 'List available eval suites (core, swe-bench-lite, gaia-l1, ...)'), 219 cmd('status', 'GET', '/eval/run', 'Get eval run status'), 220 cmd('run', 'POST', '/eval/run', 'Run an eval scenario against an agent', { expectsJsonBody: true }), 221 cmd('suite', 'POST', '/eval/suite', 'Run a full eval suite against an agent (pass { suite: "swe-bench-lite" } in body)', { expectsJsonBody: true }), 222 ], 223 }, 224 { 225 name: 'a2a', 226 description: 'A2A Protocol gateway', 227 commands: [ 228 cmd('send', 'POST', '/a2a', 'Send a JSON-RPC request to the A2A endpoint', { expectsJsonBody: true }), 229 cmd('agent-card', 'GET', '/.well-known/agent-card', 'Get agent card for a SwarmClaw agent'), 230 cmd('task-status', 'GET', '/a2a/tasks/:taskId/status', 'Check A2A task status'), 231 ], 232 }, 233 { 234 name: 'external-agents', 235 description: 'Manage external agent runtimes', 236 commands: [ 237 cmd('list', 'GET', '/external-agents', 'List external agent runtimes'), 238 cmd('create', 'POST', '/external-agents', 'Register an external agent runtime', { expectsJsonBody: true }), 239 cmd('update', 'PUT', '/external-agents/:id', 'Update an external agent runtime', { expectsJsonBody: true }), 240 cmd('delete', 'DELETE', '/external-agents/:id', 'Delete an external agent runtime'), 241 cmd('heartbeat', 'POST', '/external-agents/:id/heartbeat', 'Record an external agent heartbeat', { expectsJsonBody: true }), 242 ], 243 }, 244 { 245 name: 'files', 246 description: 'Serve and manage local files', 247 commands: [ 248 cmd('serve', 'GET', '/files/serve', 'Serve a local file (use --query path=/abs/path)'), 249 cmd('open', 'POST', '/files/open', 'Open a local file path via the host default app/browser', { expectsJsonBody: true }), 250 ], 251 }, 252 { 253 name: 'gateways', 254 description: 'Manage named OpenClaw gateway profiles', 255 commands: [ 256 cmd('list', 'GET', '/gateways', 'List configured gateway profiles'), 257 cmd('create', 'POST', '/gateways', 'Create a gateway profile', { expectsJsonBody: true }), 258 cmd('update', 'PUT', '/gateways/:id', 'Update a gateway profile', { expectsJsonBody: true }), 259 cmd('delete', 'DELETE', '/gateways/:id', 'Delete a gateway profile'), 260 cmd('health', 'GET', '/gateways/:id/health', 'Run a gateway health check'), 261 ], 262 }, 263 { 264 name: 'ip', 265 description: 'Get local IP/port metadata', 266 commands: [ 267 cmd('get', 'GET', '/ip', 'Get host IP and port'), 268 ], 269 }, 270 { 271 name: 'knowledge', 272 description: 'Manage knowledge base entries', 273 commands: [ 274 cmd('list', 'GET', '/knowledge', 'List knowledge entries'), 275 cmd('get', 'GET', '/knowledge/:id', 'Get knowledge entry by id'), 276 cmd('create', 'POST', '/knowledge', 'Create knowledge entry', { expectsJsonBody: true }), 277 cmd('update', 'PUT', '/knowledge/:id', 'Update knowledge entry', { expectsJsonBody: true }), 278 cmd('delete', 'DELETE', '/knowledge/:id', 'Delete knowledge entry'), 279 cmd('upload', 'POST', '/knowledge/upload', 'Upload document for knowledge extraction', { 280 requestType: 'upload', 281 inputPositional: 'filePath', 282 }), 283 cmd('hygiene', 'GET', '/knowledge/hygiene', 'Get knowledge hygiene summary'), 284 cmd('hygiene-run', 'POST', '/knowledge/hygiene', 'Run knowledge hygiene maintenance'), 285 cmd('sources', 'GET', '/knowledge/sources', 'List knowledge sources'), 286 cmd('source-create', 'POST', '/knowledge/sources', 'Create knowledge source', { expectsJsonBody: true }), 287 cmd('source-get', 'GET', '/knowledge/sources/:id', 'Get knowledge source detail'), 288 cmd('source-update', 'PUT', '/knowledge/sources/:id', 'Update knowledge source', { expectsJsonBody: true }), 289 cmd('source-delete', 'DELETE', '/knowledge/sources/:id', 'Delete knowledge source'), 290 cmd('source-archive', 'POST', '/knowledge/sources/:id/archive', 'Archive knowledge source', { expectsJsonBody: true }), 291 cmd('source-restore', 'POST', '/knowledge/sources/:id/restore', 'Restore archived knowledge source'), 292 cmd('source-supersede', 'POST', '/knowledge/sources/:id/supersede', 'Mark source as superseded', { expectsJsonBody: true }), 293 cmd('source-sync', 'POST', '/knowledge/sources/:id/sync', 'Re-sync file/URL knowledge source'), 294 ], 295 }, 296 { 297 name: 'logs', 298 description: 'Read or clear app logs', 299 commands: [ 300 cmd('list', 'GET', '/logs', 'List logs (use --query lines=200, --query level=INFO,ERROR)'), 301 cmd('clear', 'DELETE', '/logs', 'Clear logs file'), 302 cmd('report', 'POST', '/logs', 'Write a client/browser error entry to the application log', { 303 expectsJsonBody: true, 304 }), 305 ], 306 }, 307 { 308 name: 'memory', 309 description: 'Manage memory entries', 310 commands: [ 311 cmd('list', 'GET', '/memory', 'List memory entries (use --query q=, --query agentId=)'), 312 cmd('get', 'GET', '/memory/:id', 'Get memory by id'), 313 cmd('create', 'POST', '/memory', 'Create memory entry', { expectsJsonBody: true }), 314 cmd('update', 'PUT', '/memory/:id', 'Update memory entry', { expectsJsonBody: true }), 315 cmd('delete', 'DELETE', '/memory/:id', 'Delete memory entry'), 316 cmd('maintenance', 'GET', '/memory/maintenance', 'Analyze memory dedupe/prune candidates'), 317 cmd('maintenance-run', 'POST', '/memory/maintenance', 'Run memory dedupe/prune maintenance', { expectsJsonBody: true }), 318 cmd('graph', 'GET', '/memory/graph', 'Get memory graph (nodes and links) for visualization'), 319 cmd('dream', 'GET', '/memory/dream', 'List dream cycles'), 320 cmd('dream-trigger', 'POST', '/memory/dream', 'Trigger a dream cycle', { expectsJsonBody: true }), 321 cmd('dream-get', 'GET', '/memory/dream/:id', 'Get dream cycle by id'), 322 ], 323 }, 324 { 325 name: 'memory-images', 326 description: 'Fetch stored memory image assets', 327 commands: [ 328 cmd('get', 'GET', '/memory-images/:filename', 'Download memory image by filename', { responseType: 'binary' }), 329 ], 330 }, 331 332 { 333 name: 'notifications', 334 description: 'Manage in-app notifications', 335 commands: [ 336 cmd('list', 'GET', '/notifications', 'List notifications (use --query unreadOnly=true --query limit=100)'), 337 cmd('create', 'POST', '/notifications', 'Create notification', { expectsJsonBody: true }), 338 cmd('clear', 'DELETE', '/notifications', 'Clear read notifications'), 339 cmd('mark-read', 'PUT', '/notifications/:id', 'Mark notification as read'), 340 cmd('delete', 'DELETE', '/notifications/:id', 'Delete notification by id'), 341 ], 342 }, 343 { 344 name: 'protocols', 345 description: 'Manage Structured Session runs and templates', 346 commands: [ 347 cmd('list', 'GET', '/protocols/runs', 'List structured session runs'), 348 cmd('get', 'GET', '/protocols/runs/:id', 'Get structured session run detail'), 349 cmd('events', 'GET', '/protocols/runs/:id/events', 'Get structured session run events'), 350 cmd('create', 'POST', '/protocols/runs', 'Create a structured session run', { expectsJsonBody: true }), 351 cmd('action', 'POST', '/protocols/runs/:id/actions', 'Run a structured session action', { expectsJsonBody: true }), 352 cmd('templates', 'GET', '/protocols/templates', 'List structured session templates'), 353 cmd('template-get', 'GET', '/protocols/templates/:id', 'Get structured session template detail'), 354 cmd('template-create', 'POST', '/protocols/templates', 'Create a structured session template', { expectsJsonBody: true }), 355 cmd('template-update', 'PATCH', '/protocols/templates/:id', 'Update a structured session template', { expectsJsonBody: true }), 356 cmd('template-delete', 'DELETE', '/protocols/templates/:id', 'Delete a structured session template'), 357 ], 358 }, 359 { 360 name: 'mcp-servers', 361 description: 'Manage MCP server configurations', 362 commands: [ 363 cmd('list', 'GET', '/mcp-servers', 'List MCP servers'), 364 cmd('get', 'GET', '/mcp-servers/:id', 'Get MCP server by id'), 365 cmd('create', 'POST', '/mcp-servers', 'Create MCP server', { expectsJsonBody: true }), 366 cmd('update', 'PUT', '/mcp-servers/:id', 'Update MCP server', { expectsJsonBody: true }), 367 cmd('delete', 'DELETE', '/mcp-servers/:id', 'Delete MCP server'), 368 cmd('test', 'POST', '/mcp-servers/:id/test', 'Test MCP server connection'), 369 cmd('tools', 'GET', '/mcp-servers/:id/tools', 'List tools available on an MCP server'), 370 cmd('tools-info', 'GET', '/mcp-servers/:id/tools-info', 'List tools with token-cost estimates and exposure status'), 371 cmd('conformance', 'POST', '/mcp-servers/:id/conformance', 'Run MCP conformance checks for a server', { expectsJsonBody: true }), 372 cmd('invoke', 'POST', '/mcp-servers/:id/invoke', 'Invoke an MCP tool on a server', { expectsJsonBody: true }), 373 ], 374 }, 375 { 376 name: 'mcp-registry', 377 description: 'Browse the public SwarmDock MCP Registry', 378 commands: [ 379 cmd('search', 'GET', '/mcp-registry', 'Search registry servers (supports --query q=postgres,limit=20)'), 380 cmd('get', 'GET', '/mcp-registry/:slug', 'Get registry server detail by slug'), 381 ], 382 }, 383 { 384 name: 'memories', 385 description: 'Alias of memory command group', 386 aliasFor: 'memory', 387 commands: [], 388 }, 389 { 390 name: 'openclaw', 391 description: 'OpenClaw discovery, gateway control, and runtime APIs', 392 commands: [ 393 cmd('discover', 'GET', '/openclaw/discover', 'Discover OpenClaw gateways'), 394 cmd('deploy-status', 'GET', '/openclaw/deploy', 'Get managed OpenClaw deploy status'), 395 cmd('deploy-local-start', 'POST', '/openclaw/deploy', 'Start a managed local OpenClaw deployment (use --data JSON for port/token overrides)', { 396 expectsJsonBody: true, 397 defaultBody: { action: 'start-local' }, 398 }), 399 cmd('deploy-local-stop', 'POST', '/openclaw/deploy', 'Stop the managed local OpenClaw deployment', { 400 expectsJsonBody: true, 401 defaultBody: { action: 'stop-local' }, 402 }), 403 cmd('deploy-local-restart', 'POST', '/openclaw/deploy', 'Restart the managed local OpenClaw deployment (use --data JSON for port/token overrides)', { 404 expectsJsonBody: true, 405 defaultBody: { action: 'restart-local' }, 406 }), 407 cmd('deploy-bundle', 'POST', '/openclaw/deploy', 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)', { 408 expectsJsonBody: true, 409 defaultBody: { action: 'bundle' }, 410 }), 411 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)', { 412 expectsJsonBody: true, 413 defaultBody: { action: 'ssh-deploy' }, 414 }), 415 cmd('deploy-verify', 'POST', '/openclaw/deploy', 'Verify an OpenClaw endpoint/token pair (use --data JSON for endpoint/token)', { 416 expectsJsonBody: true, 417 defaultBody: { action: 'verify' }, 418 }), 419 cmd('remote-start', 'POST', '/openclaw/deploy', 'Start a remote SSH-managed OpenClaw stack', { 420 expectsJsonBody: true, 421 defaultBody: { action: 'remote-start' }, 422 }), 423 cmd('remote-stop', 'POST', '/openclaw/deploy', 'Stop a remote SSH-managed OpenClaw stack', { 424 expectsJsonBody: true, 425 defaultBody: { action: 'remote-stop' }, 426 }), 427 cmd('remote-restart', 'POST', '/openclaw/deploy', 'Restart a remote SSH-managed OpenClaw stack', { 428 expectsJsonBody: true, 429 defaultBody: { action: 'remote-restart' }, 430 }), 431 cmd('remote-upgrade', 'POST', '/openclaw/deploy', 'Upgrade a remote SSH-managed OpenClaw stack', { 432 expectsJsonBody: true, 433 defaultBody: { action: 'remote-upgrade' }, 434 }), 435 cmd('remote-backup', 'POST', '/openclaw/deploy', 'Create a remote backup on an SSH-managed OpenClaw host', { 436 expectsJsonBody: true, 437 defaultBody: { action: 'remote-backup' }, 438 }), 439 cmd('remote-restore', 'POST', '/openclaw/deploy', 'Restore a remote backup on an SSH-managed OpenClaw host', { 440 expectsJsonBody: true, 441 defaultBody: { action: 'remote-restore' }, 442 }), 443 cmd('remote-rotate-token', 'POST', '/openclaw/deploy', 'Rotate the gateway token on an SSH-managed OpenClaw host', { 444 expectsJsonBody: true, 445 defaultBody: { action: 'remote-rotate-token' }, 446 }), 447 cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'), 448 cmd('gateway-status', 'GET', '/openclaw/gateway', 'Check OpenClaw gateway connection status'), 449 cmd('gateway', 'POST', '/openclaw/gateway', 'Call OpenClaw gateway RPC/control action', { expectsJsonBody: true }), 450 cmd('config-sync', 'GET', '/openclaw/config-sync', 'Detect OpenClaw gateway config issues'), 451 cmd('config-sync-repair', 'POST', '/openclaw/config-sync', 'Repair a detected OpenClaw config issue', { expectsJsonBody: true }), 452 cmd('approvals', 'GET', '/openclaw/approvals', 'List pending OpenClaw execution approvals'), 453 cmd('approvals-resolve', 'POST', '/openclaw/approvals', 'Resolve an OpenClaw execution approval', { expectsJsonBody: true }), 454 cmd('cron', 'GET', '/openclaw/cron', 'List OpenClaw cron jobs'), 455 cmd('cron-action', 'POST', '/openclaw/cron', 'Create/run/remove OpenClaw cron jobs', { expectsJsonBody: true }), 456 cmd('agent-files', 'GET', '/openclaw/agent-files', 'Fetch OpenClaw agent files'), 457 cmd('agent-files-set', 'PUT', '/openclaw/agent-files', 'Save an OpenClaw agent file', { expectsJsonBody: true }), 458 cmd('dotenv-keys', 'GET', '/openclaw/dotenv-keys', 'List gateway .env keys'), 459 cmd('exec-config', 'GET', '/openclaw/exec-config', 'Fetch OpenClaw exec approval config'), 460 cmd('exec-config-set', 'PUT', '/openclaw/exec-config', 'Save OpenClaw exec approval config', { expectsJsonBody: true }), 461 cmd('history-preview', 'GET', '/openclaw/history', 'Preview OpenClaw session history'), 462 cmd('history-merge', 'POST', '/openclaw/history', 'Merge OpenClaw session history into local session', { expectsJsonBody: true }), 463 cmd('media', 'GET', '/openclaw/media', 'Proxy OpenClaw media/file content'), 464 cmd('models', 'GET', '/openclaw/models', 'List allowed OpenClaw models'), 465 cmd('permissions', 'GET', '/openclaw/permissions', 'Get OpenClaw permission preset/config'), 466 cmd('permissions-set', 'PUT', '/openclaw/permissions', 'Apply OpenClaw permission preset', { expectsJsonBody: true }), 467 cmd('sandbox-env', 'GET', '/openclaw/sandbox-env', 'List OpenClaw sandbox env allowlist'), 468 cmd('sandbox-env-set', 'PUT', '/openclaw/sandbox-env', 'Update OpenClaw sandbox env allowlist', { expectsJsonBody: true }), 469 cmd('skills', 'GET', '/openclaw/skills', 'List OpenClaw skills and eligibility'), 470 cmd('skills-update', 'PATCH', '/openclaw/skills', 'Update OpenClaw skill state/config', { expectsJsonBody: true }), 471 cmd('skills-save', 'PUT', '/openclaw/skills', 'Save OpenClaw skill allowlist mode/config', { expectsJsonBody: true }), 472 cmd('skills-install', 'POST', '/openclaw/skills/install', 'Install OpenClaw skill dependencies', { expectsJsonBody: true }), 473 cmd('skills-remove', 'POST', '/openclaw/skills/remove', 'Remove OpenClaw skill', { expectsJsonBody: true }), 474 cmd('sync', 'POST', '/openclaw/sync', 'Run OpenClaw sync action', { expectsJsonBody: true }), 475 cmd('dashboard-url', 'GET', '/openclaw/dashboard-url', 'Get tokenized OpenClaw dashboard URL for an agent'), 476 cmd('doctor', 'GET', '/openclaw/doctor', 'Run OpenClaw doctor check (read-only)'), 477 cmd('doctor-fix', 'POST', '/openclaw/doctor', 'Run OpenClaw doctor with auto-fix', { expectsJsonBody: true }), 478 ], 479 }, 480 { 481 name: 'preview-server', 482 description: 'Manage preview dev servers', 483 commands: [ 484 cmd('manage', 'POST', '/preview-server', 'Start/stop/status/detect preview server', { expectsJsonBody: true }), 485 ], 486 }, 487 { 488 name: 'projects', 489 description: 'Manage projects', 490 commands: [ 491 cmd('list', 'GET', '/projects', 'List projects'), 492 cmd('get', 'GET', '/projects/:id', 'Get project by id'), 493 cmd('create', 'POST', '/projects', 'Create project', { expectsJsonBody: true }), 494 cmd('update', 'PUT', '/projects/:id', 'Update project', { expectsJsonBody: true }), 495 cmd('delete', 'DELETE', '/projects/:id', 'Delete project'), 496 ], 497 }, 498 { 499 name: 'extensions', 500 description: 'Manage extensions and marketplace', 501 commands: [ 502 cmd('list', 'GET', '/extensions', 'List installed extensions'), 503 cmd('set', 'POST', '/extensions', 'Enable or disable an extension', { expectsJsonBody: true }), 504 cmd('delete', 'DELETE', '/extensions', 'Delete an external extension (use --query filename=extension.js)'), 505 cmd('update', 'PATCH', '/extensions', 'Update an extension (use --query id=extension.js or --query all=true)'), 506 cmd('install', 'POST', '/extensions/install', 'Install an extension from URL', { expectsJsonBody: true }), 507 cmd('install-deps', 'POST', '/extensions/dependencies', 'Install or refresh extension workspace dependencies', { expectsJsonBody: true }), 508 cmd('marketplace', 'GET', '/extensions/marketplace', 'Get extension marketplace catalog'), 509 cmd('settings-get', 'GET', '/extensions/settings', 'Get extension settings (use --query extensionId=extension_name)'), 510 cmd('settings-set', 'PUT', '/extensions/settings', 'Set extension settings (use --query extensionId=extension_name and --data JSON)', { expectsJsonBody: true }), 511 cmd('ui', 'GET', '/extensions/ui', 'List extension UI modules (use --query type=sidebar|header|chat_actions|connectors)'), 512 cmd('builtins', 'GET', '/extensions/builtins', 'List built-in extensions'), 513 ], 514 }, 515 { 516 name: 'providers', 517 description: 'Manage providers and model overrides', 518 commands: [ 519 cmd('list', 'GET', '/providers', 'List providers'), 520 cmd('get', 'GET', '/providers/:id', 'Get provider config'), 521 cmd('create', 'POST', '/providers', 'Create custom provider', { expectsJsonBody: true }), 522 cmd('update', 'PUT', '/providers/:id', 'Update provider', { expectsJsonBody: true }), 523 cmd('delete', 'DELETE', '/providers/:id', 'Delete provider'), 524 cmd('configs', 'GET', '/providers/configs', 'List saved provider configs'), 525 cmd('discover-models', 'GET', '/providers/:id/discover-models', 'Discover provider models via endpoint or credential checks'), 526 cmd('ollama', 'GET', '/providers/ollama', 'List local Ollama models (use --query endpoint=http://localhost:11434)'), 527 cmd('openclaw-health', 'GET', '/providers/openclaw/health', 'Probe OpenClaw endpoint/auth (use --query endpoint= --query credentialId= --query model=)'), 528 cmd('models', 'GET', '/providers/:id/models', 'Get provider model overrides'), 529 cmd('models-set', 'PUT', '/providers/:id/models', 'Set provider model overrides', { expectsJsonBody: true }), 530 cmd('models-clear', 'DELETE', '/providers/:id/models', 'Clear provider model overrides'), 531 ], 532 }, 533 { 534 name: 'search', 535 description: 'Global search across app resources', 536 commands: [ 537 cmd('query', 'GET', '/search', 'Search agents/tasks/chats/schedules/webhooks/skills (use --query q=term)'), 538 ], 539 }, 540 { 541 name: 'runs', 542 description: 'Session run queue/history', 543 commands: [ 544 cmd('list', 'GET', '/runs', 'List runs (use --query sessionId=, --query status=, --query limit=)'), 545 cmd('get', 'GET', '/runs/:id', 'Get run by id'), 546 cmd('events', 'GET', '/runs/:id/events', 'Get run event history by run id'), 547 ], 548 }, 549 { 550 name: 'schedules', 551 description: 'Manage schedules', 552 commands: [ 553 cmd('list', 'GET', '/schedules', 'List schedules'), 554 cmd('get', 'GET', '/schedules/:id', 'Get schedule by id'), 555 cmd('create', 'POST', '/schedules', 'Create schedule', { expectsJsonBody: true }), 556 cmd('update', 'PUT', '/schedules/:id', 'Update schedule', { expectsJsonBody: true }), 557 cmd('delete', 'DELETE', '/schedules/:id', 'Delete schedule'), 558 cmd('run', 'POST', '/schedules/:id/run', 'Trigger schedule now'), 559 ], 560 }, 561 { 562 name: 'secrets', 563 description: 'Manage reusable encrypted secrets', 564 commands: [ 565 cmd('list', 'GET', '/secrets', 'List secrets metadata'), 566 cmd('get', 'GET', '/secrets/:id', 'Get secret metadata by id'), 567 cmd('create', 'POST', '/secrets', 'Create secret', { expectsJsonBody: true }), 568 cmd('update', 'PUT', '/secrets/:id', 'Update secret metadata', { expectsJsonBody: true }), 569 cmd('delete', 'DELETE', '/secrets/:id', 'Delete secret'), 570 ], 571 }, 572 { 573 name: 'chats', 574 description: 'Manage agent chats and runtime controls', 575 commands: [ 576 cmd('list', 'GET', '/chats', 'List chats'), 577 cmd('get', 'GET', '/chats/:id', 'Get chat by id'), 578 cmd('create', 'POST', '/chats', 'Create chat', { expectsJsonBody: true }), 579 cmd('update', 'PUT', '/chats/:id', 'Update chat', { expectsJsonBody: true }), 580 cmd('delete', 'DELETE', '/chats/:id', 'Delete chat'), 581 cmd('delete-many', 'DELETE', '/chats', 'Delete multiple chats (body: {"ids":[...]})', { expectsJsonBody: true }), 582 cmd('heartbeat-disable-all', 'POST', '/chats/heartbeat', 'Disable all chat heartbeats and cancel queued heartbeat runs', { 583 expectsJsonBody: true, 584 defaultBody: { action: 'disable_all' }, 585 }), 586 cmd('messages', 'GET', '/chats/:id/messages', 'Get chat messages'), 587 cmd('messages-update', 'PUT', '/chats/:id/messages', 'Update chat message metadata (e.g. bookmark)', { expectsJsonBody: true }), 588 cmd('messages-send', 'POST', '/chats/:id/messages', 'Append a user/system message to a chat', { expectsJsonBody: true }), 589 cmd('messages-delete', 'DELETE', '/chats/:id/messages', 'Delete a message from a chat', { expectsJsonBody: true }), 590 cmd('edit-resend', 'POST', '/chats/:id/edit-resend', 'Edit and resend from a specific message index', { expectsJsonBody: true }), 591 cmd('turn-snapshot', 'GET', '/chats/:id/turns/:index/snapshot', 'Snapshot the input state of a prior user turn (for external replay)'), 592 cmd('chat', 'POST', '/chats/:id/chat', 'Send chat message (streaming)', { 593 expectsJsonBody: true, 594 responseType: 'sse', 595 }), 596 cmd('stop', 'POST', '/chats/:id/stop', 'Stop chat run(s)'), 597 cmd('clear', 'POST', '/chats/:id/clear', 'Clear chat messages (returns undoToken with a 30s TTL)'), 598 cmd('clear-undo', 'POST', '/chats/:id/clear/undo', 'Restore a cleared chat via its undoToken', { expectsJsonBody: true }), 599 cmd('compact', 'POST', '/chats/:id/compact', 'Summarize and compact chat history (accepts optional keepLastN)', { expectsJsonBody: true }), 600 cmd('context-status', 'GET', '/chats/:id/context-status', 'Report token usage and context-window status for a chat'), 601 cmd('browser-status', 'GET', '/chats/:id/browser', 'Check browser status'), 602 cmd('browser-close', 'DELETE', '/chats/:id/browser', 'Close browser'), 603 cmd('mailbox', 'GET', '/chats/:id/mailbox', 'List chat mailbox envelopes'), 604 cmd('mailbox-action', 'POST', '/chats/:id/mailbox', 'Send/ack/clear mailbox envelopes', { expectsJsonBody: true }), 605 cmd('queue', 'GET', '/chats/:id/queue', 'List queued follow-up turns for a chat'), 606 cmd('queue-add', 'POST', '/chats/:id/queue', 'Enqueue a follow-up turn for a busy chat', { expectsJsonBody: true }), 607 cmd('queue-clear', 'DELETE', '/chats/:id/queue', 'Remove queued follow-up turns from a chat', { expectsJsonBody: true }), 608 cmd('retry', 'POST', '/chats/:id/retry', 'Retry last assistant message'), 609 cmd('deploy', 'POST', '/chats/:id/deploy', 'Deploy current chat branch', { expectsJsonBody: true }), 610 cmd('devserver', 'POST', '/chats/:id/devserver', 'Dev server action via JSON body', { expectsJsonBody: true }), 611 cmd('devserver-start', 'POST', '/chats/:id/devserver', 'Start chat dev server', { 612 expectsJsonBody: true, 613 defaultBody: { action: 'start' }, 614 }), 615 cmd('devserver-stop', 'POST', '/chats/:id/devserver', 'Stop chat dev server', { 616 expectsJsonBody: true, 617 defaultBody: { action: 'stop' }, 618 }), 619 cmd('devserver-status', 'POST', '/chats/:id/devserver', 'Check chat dev server status', { 620 expectsJsonBody: true, 621 defaultBody: { action: 'status' }, 622 }), 623 cmd('checkpoints', 'GET', '/chats/:id/checkpoints', 'List checkpoint history for a chat'), 624 cmd('execution-log', 'GET', '/chats/:id/execution-log', 'Get execution log entries for a chat'), 625 cmd('migrate-messages', 'POST', '/chats/migrate-messages', 'Migrate messages from session blobs to relational table'), 626 ], 627 }, 628 { 629 name: 'settings', 630 description: 'Read/update app settings', 631 commands: [ 632 cmd('get', 'GET', '/settings', 'Get settings'), 633 cmd('update', 'PUT', '/settings', 'Update settings', { expectsJsonBody: true }), 634 ], 635 }, 636 { 637 name: 'setup', 638 description: 'Setup and provider validation helpers', 639 commands: [ 640 cmd('check-provider', 'POST', '/setup/check-provider', 'Validate provider credentials/endpoint', { expectsJsonBody: true }), 641 cmd('doctor', 'GET', '/setup/doctor', 'Run local setup diagnostics'), 642 cmd('openclaw-device', 'GET', '/setup/openclaw-device', 'Show the local OpenClaw device ID'), 643 ], 644 }, 645 { 646 name: 'learned-skills', 647 description: 'Inspect agent-scoped learned skills', 648 commands: [ 649 cmd('list', 'GET', '/learned-skills', 'List learned skills'), 650 cmd('promote', 'POST', '/learned-skills/:id?action=promote', 'Promote a review-ready skill to active'), 651 cmd('dismiss', 'POST', '/learned-skills/:id?action=dismiss', 'Dismiss a learned skill'), 652 cmd('delete', 'DELETE', '/learned-skills/:id', 'Delete a learned skill'), 653 cmd('review-counts', 'GET', '/skill-review-counts', 'Show pending review counts'), 654 ], 655 }, 656 { 657 name: 'share', 658 description: 'Public share links for missions, skills, and sessions', 659 commands: [ 660 cmd('list', 'GET', '/share', 'List share links (supports --query entityType=mission,entityId=...)'), 661 cmd('mint', 'POST', '/share', 'Mint a new share link', { expectsJsonBody: true }), 662 cmd('get', 'GET', '/share/:id', 'Get a share link by id'), 663 cmd('revoke', 'DELETE', '/share/:id', 'Revoke a share link'), 664 cmd('resolve', 'GET', '/s/:token', 'Resolve a public share token to its scrubbed payload'), 665 cmd('raw', 'GET', '/s/:token/raw', 'Fetch the raw markdown body for a share token (skill/mission/session)'), 666 ], 667 }, 668 { 669 name: 'skills', 670 description: 'Manage reusable skills', 671 commands: [ 672 cmd('list', 'GET', '/skills', 'List skills'), 673 cmd('get', 'GET', '/skills/:id', 'Get skill'), 674 cmd('create', 'POST', '/skills', 'Create skill', { expectsJsonBody: true }), 675 cmd('update', 'PUT', '/skills/:id', 'Update skill', { expectsJsonBody: true }), 676 cmd('delete', 'DELETE', '/skills/:id', 'Delete skill'), 677 cmd('import', 'POST', '/skills/import', 'Import skill from URL', { expectsJsonBody: true }), 678 ], 679 }, 680 { 681 name: 'skill-suggestions', 682 description: 'Review conversation-derived skill drafts', 683 commands: [ 684 cmd('list', 'GET', '/skill-suggestions', 'List skill suggestions'), 685 cmd('draft', 'POST', '/skill-suggestions', 'Generate or refresh a draft from a session', { 686 expectsJsonBody: true, 687 bodyFlagMap: { session: 'sessionId' }, 688 }), 689 cmd('approve', 'POST', '/skill-suggestions/:id/approve', 'Approve a skill suggestion and materialize it'), 690 cmd('reject', 'POST', '/skill-suggestions/:id/reject', 'Reject a skill suggestion draft'), 691 ], 692 }, 693 { 694 name: 'souls', 695 description: 'Browse and manage soul library templates', 696 commands: [ 697 cmd('list', 'GET', '/souls', 'List soul templates'), 698 cmd('get', 'GET', '/souls/:id', 'Get soul template by id'), 699 cmd('create', 'POST', '/souls', 'Create custom soul template', { expectsJsonBody: true }), 700 cmd('update', 'PUT', '/souls/:id', 'Update soul template', { expectsJsonBody: true }), 701 cmd('delete', 'DELETE', '/souls/:id', 'Delete soul template'), 702 ], 703 }, 704 { 705 name: 'tasks', 706 description: 'Manage task board items', 707 commands: [ 708 cmd('list', 'GET', '/tasks', 'List tasks'), 709 cmd('get', 'GET', '/tasks/:id', 'Get task'), 710 cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }), 711 cmd('bulk', 'POST', '/tasks/bulk', 'Bulk update tasks (status/agent/project)', { expectsJsonBody: true }), 712 cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }), 713 cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'), 714 cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }), 715 cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }), 716 cmd('claim', 'POST', '/tasks/claim', 'Claim a pool-mode task for an agent', { expectsJsonBody: true }), 717 cmd('import-github', 'POST', '/tasks/import/github', 'Import GitHub issues into tasks', { expectsJsonBody: true }), 718 cmd('metrics', 'GET', '/tasks/metrics', 'Get task board metrics (supports --query range=24h|7d|30d)'), 719 ], 720 }, 721 { 722 name: 'tts', 723 description: 'Text-to-speech endpoint', 724 commands: [ 725 cmd('speak', 'POST', '/tts', 'Generate TTS audio', { 726 expectsJsonBody: true, 727 responseType: 'binary', 728 bodyFlagMap: { text: 'text' }, 729 }), 730 cmd('stream', 'POST', '/tts/stream', 'Generate streaming TTS audio', { 731 expectsJsonBody: true, 732 responseType: 'binary', 733 bodyFlagMap: { text: 'text' }, 734 }), 735 ], 736 }, 737 { 738 name: 'upload', 739 description: 'Upload raw file/blob', 740 commands: [ 741 cmd('file', 'POST', '/upload', 'Upload file', { 742 requestType: 'upload', 743 inputPositional: 'filePath', 744 }), 745 ], 746 }, 747 { 748 name: 'uploads', 749 description: 'Manage uploaded artifacts', 750 commands: [ 751 cmd('list', 'GET', '/uploads', 'List uploaded artifacts'), 752 cmd('get', 'GET', '/uploads/:filename', 'Download uploaded artifact', { responseType: 'binary' }), 753 cmd('delete', 'DELETE', '/uploads/:filename', 'Delete uploaded artifact by filename'), 754 cmd('delete-many', 'DELETE', '/uploads', 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', { expectsJsonBody: true }), 755 ], 756 }, 757 { 758 name: 'system-status', 759 description: 'Lightweight system health summary', 760 commands: [ 761 cmd('get', 'GET', '/system/status', 'Get system health summary (safe for external monitors)'), 762 ], 763 }, 764 { 765 name: 'healthz', 766 description: 'Public liveness probe', 767 commands: [ 768 cmd('get', 'GET', '/healthz', 'Get public health check payload'), 769 ], 770 }, 771 { 772 name: 'usage', 773 description: 'Usage and cost summary', 774 commands: [ 775 cmd('get', 'GET', '/usage', 'Get usage summary'), 776 cmd('live', 'GET', '/usage/live', 'Get live per-session usage (use --query sessionId=...)'), 777 ], 778 }, 779 { 780 name: 'version', 781 description: 'Version and update checks', 782 commands: [ 783 cmd('get', 'GET', '/version', 'Get local/remote version info'), 784 cmd('update', 'POST', '/version/update', 'Update to latest stable release tag (fallback: main) and install deps when needed'), 785 ], 786 }, 787 { 788 name: 'webhooks', 789 description: 'Manage and trigger webhooks', 790 commands: [ 791 cmd('list', 'GET', '/webhooks', 'List webhooks'), 792 cmd('get', 'GET', '/webhooks/:id', 'Get webhook by id'), 793 cmd('create', 'POST', '/webhooks', 'Create webhook', { expectsJsonBody: true }), 794 cmd('update', 'PUT', '/webhooks/:id', 'Update webhook', { expectsJsonBody: true }), 795 cmd('delete', 'DELETE', '/webhooks/:id', 'Delete webhook'), 796 cmd('trigger', 'POST', '/webhooks/:id', 'Trigger webhook by id', { 797 expectsJsonBody: true, 798 waitEntityFrom: 'runId', 799 }), 800 cmd('history', 'GET', '/webhooks/:id/history', 'Get webhook delivery history'), 801 ], 802 }, 803 { 804 name: 'portability', 805 description: 'Export and import agent configurations', 806 commands: [ 807 cmd('export', 'GET', '/portability/export', 'Export agents, skills, and schedules as a portable JSON manifest'), 808 cmd('import', 'POST', '/portability/import', 'Import a portable JSON manifest', { expectsJsonBody: true }), 809 ], 810 }, 811 { 812 name: 'wallets', 813 description: 'Manage agent wallets', 814 commands: [ 815 cmd('list', 'GET', '/wallets', 'List wallets'), 816 cmd('get', 'GET', '/wallets/:id', 'Get wallet by id'), 817 cmd('create', 'POST', '/wallets', 'Create a wallet', { expectsJsonBody: true }), 818 cmd('generate', 'POST', '/wallets/generate', 'Generate a new wallet for an agent', { expectsJsonBody: true }), 819 cmd('update', 'PATCH', '/wallets/:id', 'Update wallet settings', { expectsJsonBody: true }), 820 cmd('delete', 'DELETE', '/wallets/:id', 'Delete a wallet'), 821 ], 822 }, 823 { 824 name: 'goals', 825 description: 'Manage goal hierarchy', 826 commands: [ 827 cmd('list', 'GET', '/goals', 'List goals'), 828 cmd('get', 'GET', '/goals/:id', 'Get goal by id'), 829 cmd('create', 'POST', '/goals', 'Create a goal', { expectsJsonBody: true }), 830 cmd('update', 'PATCH', '/goals/:id', 'Update a goal', { expectsJsonBody: true }), 831 cmd('delete', 'DELETE', '/goals/:id', 'Delete a goal'), 832 ], 833 }, 834 { 835 name: 'missions', 836 description: 'Manage autonomous missions', 837 commands: [ 838 cmd('list', 'GET', '/missions', 'List autonomous missions'), 839 cmd('get', 'GET', '/missions/:id', 'Get a mission by id'), 840 cmd('create', 'POST', '/missions', 'Create an autonomous mission', { expectsJsonBody: true }), 841 cmd('update', 'PUT', '/missions/:id', 'Update a mission', { expectsJsonBody: true }), 842 cmd('delete', 'DELETE', '/missions/:id', 'Delete a mission'), 843 cmd('control', 'POST', '/missions/:id/control', 'Start, pause, resume, cancel, complete, or fail a mission', { expectsJsonBody: true }), 844 cmd('reports', 'GET', '/missions/:id/reports', 'List mission reports'), 845 cmd('report-now', 'POST', '/missions/:id/reports', 'Force-generate a mission report now'), 846 cmd('events', 'GET', '/missions/:id/events', 'List mission events (use --query sinceAt=..., --query untilAt=...)'), 847 cmd('templates', 'GET', '/missions/templates', 'List built-in mission templates'), 848 cmd('instantiate', 'POST', '/missions/templates/:id/instantiate', 'Create a mission from a template', { expectsJsonBody: true }), 849 ], 850 }, 851 { 852 name: 'workspaces', 853 description: 'Manage logical workspaces (multi-workspace scaffolding)', 854 commands: [ 855 cmd('list', 'GET', '/workspaces', 'List workspaces'), 856 cmd('create', 'POST', '/workspaces', 'Create a workspace', { expectsJsonBody: true }), 857 cmd('update', 'PATCH', '/workspaces', 'Update a workspace', { expectsJsonBody: true }), 858 cmd('delete', 'DELETE', '/workspaces', 'Delete a workspace (use --query id=...)'), 859 cmd('active', 'GET', '/workspaces/active', 'Get the active workspace'), 860 cmd('set-active', 'POST', '/workspaces/active', 'Set the active workspace', { expectsJsonBody: true }), 861 ], 862 }, 863 { 864 name: 'workflow-states', 865 description: 'Manage customizable task workflow states', 866 commands: [ 867 cmd('list', 'GET', '/task-workflow-states', 'List workflow states'), 868 cmd('create', 'POST', '/task-workflow-states', 'Create or update a workflow state', { expectsJsonBody: true }), 869 cmd('delete', 'DELETE', '/task-workflow-states', 'Delete a workflow state (use --query id=... or --query reset=true)'), 870 ], 871 }, 872 { 873 name: 'config-versions', 874 description: 'Inspect and restore configuration version history', 875 commands: [ 876 cmd('list', 'GET', '/config-versions', 'List versions for an entity (use --query entityKind=agent,entityId=...)'), 877 cmd('restore', 'POST', '/config-versions/restore', 'Restore an entity to a prior version', { expectsJsonBody: true }), 878 ], 879 }, 880 { 881 name: 'cost-attribution', 882 description: 'Aggregate LLM cost by billing-code tags', 883 commands: [ 884 cmd('by-code', 'GET', '/usage/by-code', 'Roll up cost by billing code (use --query codes=foo,bar,range=7d)'), 885 ], 886 }, 887 { 888 name: 'chatroom-policy', 889 description: 'Configure chatroom delegation refusal policies', 890 commands: [ 891 cmd('set', 'POST', '/chatrooms/refusal-policy', 'Set onRefusal policy for a chatroom', { expectsJsonBody: true }), 892 cmd('simulate', 'PUT', '/chatrooms/refusal-policy', 'Simulate a refusal-handling decision', { expectsJsonBody: true }), 893 ], 894 }, 895 { 896 name: 'swarmfeed', 897 description: 'SwarmFeed social network', 898 commands: [ 899 cmd('feed', 'GET', '/swarmfeed', 'Get SwarmFeed timeline'), 900 cmd('search', 'GET', '/swarmfeed/search', 'Search SwarmFeed posts, agents, channels, or hashtags'), 901 cmd('channels', 'GET', '/swarmfeed/channels', 'List SwarmFeed channels'), 902 cmd('bookmarks', 'GET', '/swarmfeed/bookmarks', 'Get bookmarked SwarmFeed posts for an agent'), 903 cmd('notifications', 'GET', '/swarmfeed/notifications', 'Get SwarmFeed notifications for an agent'), 904 cmd('suggested', 'GET', '/swarmfeed/suggested', 'Get suggested SwarmFeed follows'), 905 cmd('posts', 'GET', '/swarmfeed/posts', 'Get recent posts'), 906 cmd('post-get', 'GET', '/swarmfeed/posts/:postId', 'Get a SwarmFeed post by id'), 907 cmd('replies', 'GET', '/swarmfeed/posts/:postId/replies', 'Get replies for a SwarmFeed post'), 908 cmd('post', 'POST', '/swarmfeed/posts', 'Create a post', { expectsJsonBody: true }), 909 cmd('profile', 'GET', '/swarmfeed/profiles/:agentId', 'Get a SwarmFeed agent profile'), 910 cmd('profile-posts', 'GET', '/swarmfeed/profiles/:agentId/posts', 'Get recent posts for a SwarmFeed agent profile'), 911 cmd('action', 'POST', '/swarmfeed/actions', 'Run a SwarmFeed action such as follow, bookmark, repost, or quote repost', { 912 expectsJsonBody: true, 913 }), 914 ], 915 }, 916 { 917 name: 'swarmdock', 918 description: 'SwarmDock marketplace', 919 commands: [ 920 cmd('browse', 'GET', '/swarmdock', 'Browse SwarmDock marketplace tasks and agents'), 921 ], 922 }, 923 ] 924 925 const GROUP_MAP = new Map(COMMAND_GROUPS.map((group) => [group.name, group])) 926 927 function resolveGroup(name) { 928 const group = GROUP_MAP.get(name) 929 if (!group) return null 930 if (group.aliasFor) { 931 return GROUP_MAP.get(group.aliasFor) || null 932 } 933 return group 934 } 935 936 const COMMANDS = COMMAND_GROUPS.flatMap((group) => { 937 if (group.aliasFor) return [] 938 return group.commands.map((command) => ({ ...command, group: group.name })) 939 }) 940 941 function getCommand(groupName, action) { 942 const group = resolveGroup(groupName) 943 if (!group) return null 944 return group.commands.find((command) => command.action === action) || null 945 } 946 947 function extractPathParams(route) { 948 return [...route.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]) 949 } 950 951 function isPlainObject(value) { 952 return value && typeof value === 'object' && !Array.isArray(value) 953 } 954 955 function parseKeyValue(raw, kind) { 956 const idx = raw.indexOf('=') 957 if (idx === -1) { 958 throw new Error(`${kind} value must be key=value: ${raw}`) 959 } 960 const key = raw.slice(0, idx).trim() 961 const value = raw.slice(idx + 1) 962 if (!key) throw new Error(`${kind} key cannot be empty`) 963 return [key, value] 964 } 965 966 function parseDataInput(raw, stdin) { 967 if (raw === '-') { 968 return parseJsonText(readStdin(stdin), 'stdin') 969 } 970 if (raw.startsWith('@')) { 971 const filePath = raw.slice(1) 972 if (!filePath) throw new Error('Expected file path after @ for --data') 973 const fileText = fs.readFileSync(filePath, 'utf8') 974 return parseJsonText(fileText, filePath) 975 } 976 return parseJsonText(raw, '--data') 977 } 978 979 function parseJsonText(text, sourceName) { 980 try { 981 return JSON.parse(text) 982 } catch (err) { 983 const msg = err instanceof Error ? err.message : String(err) 984 throw new Error(`Invalid JSON from ${sourceName}: ${msg}`) 985 } 986 } 987 988 function readStdin(stdin) { 989 const fd = stdin && typeof stdin.fd === 'number' ? stdin.fd : 0 990 return fs.readFileSync(fd, 'utf8') 991 } 992 993 function normalizeBaseUrl(raw) { 994 const trimmed = String(raw || '').trim() 995 const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}` 996 return withProtocol.replace(/\/+$/, '') 997 } 998 999 function resolveAccessKey(opts, env, cwd) { 1000 if (opts.accessKey) return String(opts.accessKey).trim() 1001 const envKey = env.SWARMCLAW_API_KEY || env.SC_ACCESS_KEY || env.SWARMCLAW_ACCESS_KEY || '' 1002 if (envKey) return String(envKey).trim() 1003 1004 const keyFile = path.join(cwd, 'platform-api-key.txt') 1005 if (fs.existsSync(keyFile)) { 1006 const content = fs.readFileSync(keyFile, 'utf8').trim() 1007 if (content) return content 1008 } 1009 return '' 1010 } 1011 1012 function parseArgv(argv) { 1013 const result = { 1014 group: '', 1015 action: '', 1016 positionals: [], 1017 opts: { 1018 baseUrl: '', 1019 accessKey: '', 1020 jsonOutput: false, 1021 wait: false, 1022 timeoutMs: 300000, 1023 intervalMs: 2000, 1024 out: '', 1025 data: '', 1026 headers: [], 1027 query: [], 1028 key: '', 1029 text: '', 1030 file: '', 1031 filename: '', 1032 secret: '', 1033 event: '', 1034 help: false, 1035 version: false, 1036 }, 1037 } 1038 1039 const valueOptions = new Set([ 1040 'base-url', 1041 'access-key', 1042 'timeout-ms', 1043 'interval-ms', 1044 'out', 1045 'data', 1046 'header', 1047 'query', 1048 'key', 1049 'text', 1050 'file', 1051 'filename', 1052 'secret', 1053 'event', 1054 ]) 1055 1056 const tokens = [...argv] 1057 for (let i = 0; i < tokens.length; i += 1) { 1058 const token = tokens[i] 1059 if (token === '--') { 1060 result.positionals.push(...tokens.slice(i + 1)) 1061 break 1062 } 1063 1064 if (token === '-h' || token === '--help') { 1065 result.opts.help = true 1066 continue 1067 } 1068 1069 if (token === '--version') { 1070 result.opts.version = true 1071 continue 1072 } 1073 1074 if (token === '--json') { 1075 result.opts.jsonOutput = true 1076 continue 1077 } 1078 1079 if (token === '--wait') { 1080 result.opts.wait = true 1081 continue 1082 } 1083 1084 if (token.startsWith('--')) { 1085 const eqIndex = token.indexOf('=') 1086 const hasInline = eqIndex > -1 1087 const rawName = hasInline ? token.slice(2, eqIndex) : token.slice(2) 1088 const rawValue = hasInline ? token.slice(eqIndex + 1) : '' 1089 1090 if (!valueOptions.has(rawName)) { 1091 throw new Error(`Unknown option: --${rawName}`) 1092 } 1093 1094 const value = hasInline ? rawValue : tokens[i + 1] 1095 if (!hasInline) i += 1 1096 if (value === undefined) { 1097 throw new Error(`Missing value for --${rawName}`) 1098 } 1099 1100 switch (rawName) { 1101 case 'base-url': 1102 result.opts.baseUrl = value 1103 break 1104 case 'access-key': 1105 result.opts.accessKey = value 1106 break 1107 case 'timeout-ms': 1108 result.opts.timeoutMs = Number.parseInt(value, 10) 1109 if (!Number.isFinite(result.opts.timeoutMs) || result.opts.timeoutMs <= 0) { 1110 throw new Error(`Invalid --timeout-ms value: ${value}`) 1111 } 1112 break 1113 case 'interval-ms': 1114 result.opts.intervalMs = Number.parseInt(value, 10) 1115 if (!Number.isFinite(result.opts.intervalMs) || result.opts.intervalMs <= 0) { 1116 throw new Error(`Invalid --interval-ms value: ${value}`) 1117 } 1118 break 1119 case 'out': 1120 result.opts.out = value 1121 break 1122 case 'data': 1123 result.opts.data = value 1124 break 1125 case 'header': 1126 result.opts.headers.push(value) 1127 break 1128 case 'query': 1129 result.opts.query.push(value) 1130 break 1131 case 'key': 1132 result.opts.key = value 1133 break 1134 case 'text': 1135 result.opts.text = value 1136 break 1137 case 'file': 1138 result.opts.file = value 1139 break 1140 case 'filename': 1141 result.opts.filename = value 1142 break 1143 case 'secret': 1144 result.opts.secret = value 1145 break 1146 case 'event': 1147 result.opts.event = value 1148 break 1149 default: 1150 throw new Error(`Unhandled option parser branch: --${rawName}`) 1151 } 1152 continue 1153 } 1154 1155 result.positionals.push(token) 1156 } 1157 1158 if (result.positionals.length > 0) { 1159 result.group = result.positionals[0] 1160 } 1161 if (result.positionals.length > 1) { 1162 result.action = result.positionals[1] 1163 } 1164 1165 return result 1166 } 1167 1168 function buildRoute(routeTemplate, args) { 1169 const pathParams = extractPathParams(routeTemplate) 1170 if (args.length < pathParams.length) { 1171 throw new Error(`Missing required path args: ${pathParams.slice(args.length).join(', ')}`) 1172 } 1173 1174 let route = routeTemplate 1175 for (let i = 0; i < pathParams.length; i += 1) { 1176 route = route.replace(`:${pathParams[i]}`, encodeURIComponent(String(args[i]))) 1177 } 1178 1179 const remaining = args.slice(pathParams.length) 1180 return { route, remaining, pathParams } 1181 } 1182 1183 function buildApiUrl(baseUrl, route, queryEntries) { 1184 const normalizedBase = normalizeBaseUrl(baseUrl) 1185 const hasApiSuffix = normalizedBase.endsWith('/api') 1186 const url = new URL(`${normalizedBase}${hasApiSuffix ? '' : '/api'}${route}`) 1187 for (const [key, value] of queryEntries) { 1188 url.searchParams.set(key, value) 1189 } 1190 return url 1191 } 1192 1193 async function parseResponse(res, forceType) { 1194 const ct = (res.headers.get('content-type') || '').toLowerCase() 1195 1196 if (forceType === 'sse' || ct.includes('text/event-stream')) { 1197 return { type: 'sse', value: res.body } 1198 } 1199 1200 if (forceType === 'binary') { 1201 const buf = Buffer.from(await res.arrayBuffer()) 1202 return { type: 'binary', value: buf, contentType: ct } 1203 } 1204 1205 if (ct.includes('application/json')) { 1206 const json = await res.json().catch(() => null) 1207 return { type: 'json', value: json } 1208 } 1209 1210 if (ct.startsWith('text/') || ct.includes('xml') || ct.includes('javascript')) { 1211 const text = await res.text() 1212 return { type: 'text', value: text } 1213 } 1214 1215 const buf = Buffer.from(await res.arrayBuffer()) 1216 return { type: 'binary', value: buf, contentType: ct } 1217 } 1218 1219 function writeJson(stdout, value, compact) { 1220 const text = compact ? JSON.stringify(value) : JSON.stringify(value, null, 2) 1221 stdout.write(`${text}\n`) 1222 } 1223 1224 function writeText(stdout, value) { 1225 stdout.write(String(value)) 1226 if (!String(value).endsWith('\n')) stdout.write('\n') 1227 } 1228 1229 function writeBinary(stdout, stderr, buffer, outPath, cwd) { 1230 if (outPath) { 1231 const resolved = path.isAbsolute(outPath) ? outPath : path.join(cwd, outPath) 1232 fs.writeFileSync(resolved, buffer) 1233 stderr.write(`Saved ${buffer.length} bytes to ${resolved}\n`) 1234 return 1235 } 1236 1237 if (stdout.isTTY) { 1238 throw new Error('Binary response requires --out <file> when writing to a TTY') 1239 } 1240 stdout.write(buffer) 1241 } 1242 1243 async function consumeSse(body, stdout, stderr, jsonOutput) { 1244 if (!body || typeof body.getReader !== 'function') { 1245 throw new Error('Streaming response does not expose a reader') 1246 } 1247 1248 const reader = body.getReader() 1249 const decoder = new TextDecoder() 1250 let buffer = '' 1251 const eventBoundary = /\r?\n\r?\n/ 1252 1253 function flushChunk(rawChunk) { 1254 const lines = rawChunk 1255 .replace(/\r\n/g, '\n') 1256 .split('\n') 1257 .map((line) => line.trimEnd()) 1258 .filter(Boolean) 1259 1260 const dataLines = lines 1261 .filter((line) => line.startsWith('data:')) 1262 .map((line) => line.slice(5).trim()) 1263 1264 if (!dataLines.length) return 1265 const payload = dataLines.join('\n') 1266 1267 let parsed 1268 try { 1269 parsed = JSON.parse(payload) 1270 } catch { 1271 writeText(stdout, payload) 1272 return 1273 } 1274 1275 if (jsonOutput) { 1276 writeJson(stdout, parsed, true) 1277 return 1278 } 1279 1280 if (isPlainObject(parsed) && parsed.t === 'md' && typeof parsed.text === 'string') { 1281 writeText(stdout, parsed.text) 1282 return 1283 } 1284 1285 if (isPlainObject(parsed) && parsed.t === 'err' && typeof parsed.text === 'string') { 1286 writeText(stderr, parsed.text) 1287 return 1288 } 1289 1290 writeJson(stdout, parsed, false) 1291 } 1292 1293 while (true) { 1294 const { done, value } = await reader.read() 1295 if (done) break 1296 buffer += decoder.decode(value, { stream: true }) 1297 1298 let match = eventBoundary.exec(buffer) 1299 while (match) { 1300 const splitIndex = match.index 1301 const delimiterLength = match[0].length 1302 const chunk = buffer.slice(0, splitIndex) 1303 buffer = buffer.slice(splitIndex + delimiterLength) 1304 flushChunk(chunk) 1305 match = eventBoundary.exec(buffer) 1306 } 1307 } 1308 1309 const finalText = decoder.decode() 1310 if (finalText) buffer += finalText 1311 if (buffer.trim()) flushChunk(buffer) 1312 } 1313 1314 async function fetchJson(fetchImpl, url, headers, timeoutMs) { 1315 const res = await fetchImpl(url, { 1316 method: 'GET', 1317 headers, 1318 signal: AbortSignal.timeout(timeoutMs), 1319 }) 1320 1321 const parsed = await parseResponse(res) 1322 if (!res.ok) { 1323 throw new Error(`Request failed (${res.status}): ${serializePayload(parsed.value)}`) 1324 } 1325 1326 if (parsed.type !== 'json') { 1327 throw new Error(`Expected JSON response from ${url}`) 1328 } 1329 1330 return parsed.value 1331 } 1332 1333 function serializePayload(value) { 1334 if (typeof value === 'string') return value 1335 try { 1336 return JSON.stringify(value) 1337 } catch { 1338 return String(value) 1339 } 1340 } 1341 1342 function getWaitId(payload, command) { 1343 if (!isPlainObject(payload)) return null 1344 1345 if (command.waitEntityFrom && typeof payload[command.waitEntityFrom] === 'string') { 1346 return { type: command.waitEntityFrom === 'taskId' ? 'task' : 'run', id: payload[command.waitEntityFrom] } 1347 } 1348 1349 if (typeof payload.runId === 'string') return { type: 'run', id: payload.runId } 1350 if (isPlainObject(payload.run) && typeof payload.run.id === 'string') return { type: 'run', id: payload.run.id } 1351 if (typeof payload.taskId === 'string') return { type: 'task', id: payload.taskId } 1352 1353 return null 1354 } 1355 1356 function isTerminalStatus(status) { 1357 const terminal = new Set([ 1358 'completed', 1359 'complete', 1360 'done', 1361 'failed', 1362 'error', 1363 'stopped', 1364 'cancelled', 1365 'canceled', 1366 'timeout', 1367 'timed_out', 1368 ]) 1369 return terminal.has(String(status || '').toLowerCase()) 1370 } 1371 1372 async function waitForEntity(opts) { 1373 const { 1374 entityType, 1375 entityId, 1376 fetchImpl, 1377 baseUrl, 1378 headers, 1379 timeoutMs, 1380 intervalMs, 1381 stdout, 1382 jsonOutput, 1383 } = opts 1384 1385 const route = entityType === 'run' ? `/runs/${encodeURIComponent(entityId)}` : `/tasks/${encodeURIComponent(entityId)}` 1386 const deadline = Date.now() + timeoutMs 1387 1388 while (Date.now() <= deadline) { 1389 const url = buildApiUrl(baseUrl, route, []) 1390 const payload = await fetchJson(fetchImpl, url, headers, timeoutMs) 1391 1392 const status = isPlainObject(payload) ? payload.status : undefined 1393 if (status !== undefined) { 1394 stdout.write(`[wait] ${entityType} ${entityId}: ${status}\n`) 1395 } 1396 1397 if (status !== undefined && isTerminalStatus(status)) { 1398 if (jsonOutput) writeJson(stdout, payload, true) 1399 else writeJson(stdout, payload, false) 1400 return 1401 } 1402 1403 await new Promise((resolve) => setTimeout(resolve, intervalMs)) 1404 } 1405 1406 throw new Error(`Timed out waiting for ${entityType} ${entityId}`) 1407 } 1408 1409 function renderGeneralHelp() { 1410 const lines = [ 1411 'SwarmClaw CLI', 1412 '', 1413 'Usage:', 1414 ' swarmclaw', 1415 ' swarmclaw help [command]', 1416 ' swarmclaw run|start|stop|status|doctor|update|version', 1417 ' swarmclaw <group> <command> [args] [options]', 1418 '', 1419 'Global options:', 1420 ' --base-url <url> API base URL (default: http://localhost:3456)', 1421 ' --access-key <key> Access key override (else SWARMCLAW_API_KEY/SWARMCLAW_ACCESS_KEY or platform-api-key.txt)', 1422 ' --data <json|@file|-> Request JSON body', 1423 ' --query key=value Query parameter (repeatable)', 1424 ' --header key=value Extra HTTP header (repeatable)', 1425 ' --json Compact JSON output', 1426 ' --wait Wait for run/task completion when runId/taskId is returned', 1427 ' --timeout-ms <ms> Request/wait timeout (default: 300000)', 1428 ' --interval-ms <ms> Poll interval for --wait (default: 2000)', 1429 ' --out <file> Write binary response to file', 1430 ' --help Show help', 1431 ' --version Show package version', 1432 '', 1433 'Top-level commands:', 1434 ' run, start Start the SwarmClaw server', 1435 ' stop Stop the detached SwarmClaw server', 1436 ' status Show local server status', 1437 ' doctor Show local install/build diagnostics', 1438 ' help Show root or command help', 1439 ' update Update this SwarmClaw installation', 1440 ' version Show package version', 1441 '', 1442 'Groups:', 1443 ] 1444 1445 for (const group of COMMAND_GROUPS) { 1446 if (group.aliasFor) { 1447 lines.push(` ${group.name} (alias for ${group.aliasFor})`) 1448 } else { 1449 lines.push(` ${group.name}`) 1450 } 1451 } 1452 1453 lines.push('', 'Use "swarmclaw help <command>" or "swarmclaw <group> --help" for more detail.') 1454 return lines.join('\n') 1455 } 1456 1457 function renderGroupHelp(groupName) { 1458 const group = GROUP_MAP.get(groupName) 1459 if (!group) { 1460 throw new Error(`Unknown command group: ${groupName}`) 1461 } 1462 1463 const resolved = resolveGroup(groupName) 1464 if (!resolved) throw new Error(`Unable to resolve command group: ${groupName}`) 1465 1466 const lines = [ 1467 `Group: ${groupName}${group.aliasFor ? ` (alias for ${group.aliasFor})` : ''}`, 1468 group.description ? `Description: ${group.description}` : '', 1469 '', 1470 'Commands:', 1471 ].filter(Boolean) 1472 1473 for (const command of resolved.commands) { 1474 const params = extractPathParams(command.route).map((name) => `<${name}>`).join(' ') 1475 const suffix = params ? ` ${params}` : '' 1476 lines.push(` ${command.action}${suffix} ${command.description}`) 1477 } 1478 1479 return lines.join('\n') 1480 } 1481 1482 async function runCli(argv, deps = {}) { 1483 const stdout = deps.stdout || process.stdout 1484 const stderr = deps.stderr || process.stderr 1485 const stdin = deps.stdin || process.stdin 1486 const env = deps.env || process.env 1487 const cwd = deps.cwd || process.cwd() 1488 const fetchImpl = deps.fetchImpl || globalThis.fetch 1489 1490 if (typeof fetchImpl !== 'function') { 1491 stderr.write('Global fetch is unavailable in this Node runtime. Use Node 18+ or provide a fetch implementation.\n') 1492 return 1 1493 } 1494 1495 let parsed 1496 try { 1497 parsed = parseArgv(argv) 1498 } catch (err) { 1499 stderr.write(`${err instanceof Error ? err.message : String(err)}\n`) 1500 return 1 1501 } 1502 1503 if (parsed.opts.version) { 1504 const pkgPath = path.join(__dirname, '..', '..', 'package.json') 1505 const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) 1506 stdout.write(`${pkg.name || 'swarmclaw'} ${pkg.version || '0.0.0'}\n`) 1507 return 0 1508 } 1509 1510 if (!parsed.group || parsed.opts.help) { 1511 if (parsed.group) { 1512 try { 1513 stdout.write(`${renderGroupHelp(parsed.group)}\n`) 1514 return 0 1515 } catch { 1516 // Fall through to general help for unknown group 1517 } 1518 } 1519 stdout.write(`${renderGeneralHelp()}\n`) 1520 return 0 1521 } 1522 1523 if (!parsed.action) { 1524 try { 1525 stdout.write(`${renderGroupHelp(parsed.group)}\n`) 1526 return 0 1527 } catch (err) { 1528 stderr.write(`${err instanceof Error ? err.message : String(err)}\n`) 1529 return 1 1530 } 1531 } 1532 1533 const command = getCommand(parsed.group, parsed.action) 1534 if (!command) { 1535 stderr.write(`Unknown command: ${parsed.group} ${parsed.action}\n`) 1536 const group = resolveGroup(parsed.group) 1537 if (group) { 1538 stderr.write(`${renderGroupHelp(parsed.group)}\n`) 1539 } else { 1540 stderr.write(`${renderGeneralHelp()}\n`) 1541 } 1542 return 1 1543 } 1544 1545 const pathArgs = parsed.positionals.slice(2) 1546 let routeInfo 1547 try { 1548 routeInfo = buildRoute(command.route, pathArgs) 1549 } catch (err) { 1550 stderr.write(`${err instanceof Error ? err.message : String(err)}\n`) 1551 return 1 1552 } 1553 1554 const accessKey = resolveAccessKey(parsed.opts, env, cwd) 1555 const baseUrl = parsed.opts.baseUrl || env.SWARMCLAW_BASE_URL || env.SWARMCLAW_URL || 'http://localhost:3456' 1556 1557 const headerEntries = [] 1558 for (const raw of parsed.opts.headers) { 1559 try { 1560 headerEntries.push(parseKeyValue(raw, 'header')) 1561 } catch (err) { 1562 stderr.write(`${err instanceof Error ? err.message : String(err)}\n`) 1563 return 1 1564 } 1565 } 1566 1567 if (parsed.opts.secret) { 1568 headerEntries.push(['x-webhook-secret', parsed.opts.secret]) 1569 } 1570 1571 const queryEntries = [] 1572 for (const raw of parsed.opts.query) { 1573 try { 1574 queryEntries.push(parseKeyValue(raw, 'query')) 1575 } catch (err) { 1576 stderr.write(`${err instanceof Error ? err.message : String(err)}\n`) 1577 return 1 1578 } 1579 } 1580 1581 if (parsed.opts.event) { 1582 queryEntries.push(['event', parsed.opts.event]) 1583 } 1584 1585 let url 1586 try { 1587 url = buildApiUrl(baseUrl, routeInfo.route, queryEntries) 1588 } catch (err) { 1589 stderr.write(`Invalid --base-url: ${err instanceof Error ? err.message : String(err)}\n`) 1590 return 1 1591 } 1592 1593 const headers = { 1594 ...Object.fromEntries(headerEntries), 1595 } 1596 if (accessKey) headers['X-Access-Key'] = accessKey 1597 1598 try { 1599 if (command.clientGetRoute) { 1600 const collectionUrl = buildApiUrl(baseUrl, command.clientGetRoute, queryEntries) 1601 const payload = await fetchJson(fetchImpl, collectionUrl, headers, parsed.opts.timeoutMs) 1602 const id = pathArgs[0] 1603 const entity = extractById(payload, id) 1604 if (!entity) { 1605 stderr.write(`Entity not found for id: ${id}\n`) 1606 return 1 1607 } 1608 if (parsed.opts.jsonOutput) writeJson(stdout, entity, true) 1609 else writeJson(stdout, entity, false) 1610 return 0 1611 } 1612 1613 const init = { 1614 method: command.method, 1615 headers, 1616 signal: AbortSignal.timeout(parsed.opts.timeoutMs), 1617 } 1618 1619 if (command.requestType === 'upload') { 1620 const uploadPath = parsed.opts.file || routeInfo.remaining[0] 1621 if (!uploadPath) { 1622 throw new Error(`Missing file path. Usage: ${parsed.group} ${parsed.action} <filePath>`) } 1623 1624 const resolvedUploadPath = path.isAbsolute(uploadPath) ? uploadPath : path.join(cwd, uploadPath) 1625 const fileBuffer = fs.readFileSync(resolvedUploadPath) 1626 const filename = parsed.opts.filename || path.basename(resolvedUploadPath) 1627 init.body = fileBuffer 1628 init.headers['x-filename'] = filename 1629 if (!init.headers['Content-Type']) init.headers['Content-Type'] = 'application/octet-stream' 1630 } else if (command.method !== 'GET' && command.method !== 'HEAD') { 1631 let body = undefined 1632 if (parsed.opts.data) { 1633 body = parseDataInput(parsed.opts.data, stdin) 1634 } 1635 1636 if (!isPlainObject(body) && command.expectsJsonBody) { 1637 body = {} 1638 } 1639 1640 if (command.defaultBody) { 1641 body = { ...(command.defaultBody || {}), ...(isPlainObject(body) ? body : {}) } 1642 } 1643 1644 if (command.bodyFlagMap) { 1645 const mapped = {} 1646 for (const [flagName, bodyKey] of Object.entries(command.bodyFlagMap)) { 1647 const val = parsed.opts[flagName] 1648 if (val !== undefined && val !== '') { 1649 mapped[bodyKey] = val 1650 } 1651 } 1652 body = { ...(isPlainObject(body) ? body : {}), ...mapped } 1653 } 1654 1655 if (body !== undefined) { 1656 init.body = JSON.stringify(body) 1657 init.headers['Content-Type'] = 'application/json' 1658 } 1659 } 1660 1661 const res = await fetchImpl(url, init) 1662 const parsedResponse = await parseResponse(res, command.responseType) 1663 1664 if (!res.ok) { 1665 const serialized = serializePayload(parsedResponse.value) 1666 stderr.write(`Request failed (${res.status} ${res.statusText}): ${serialized}\n`) 1667 return 1 1668 } 1669 1670 if (parsedResponse.type === 'sse') { 1671 await consumeSse(parsedResponse.value, stdout, stderr, parsed.opts.jsonOutput) 1672 return 0 1673 } 1674 1675 if (parsedResponse.type === 'binary') { 1676 writeBinary(stdout, stderr, parsedResponse.value, parsed.opts.out, cwd) 1677 return 0 1678 } 1679 1680 if (parsedResponse.type === 'json') { 1681 if (parsed.opts.jsonOutput) writeJson(stdout, parsedResponse.value, true) 1682 else writeJson(stdout, parsedResponse.value, false) 1683 1684 if (parsed.opts.wait) { 1685 const waitMeta = getWaitId(parsedResponse.value, command) 1686 if (waitMeta) { 1687 await waitForEntity({ 1688 entityType: waitMeta.type, 1689 entityId: waitMeta.id, 1690 fetchImpl, 1691 baseUrl, 1692 headers, 1693 timeoutMs: parsed.opts.timeoutMs, 1694 intervalMs: parsed.opts.intervalMs, 1695 stdout, 1696 jsonOutput: parsed.opts.jsonOutput, 1697 }) 1698 } else { 1699 stderr.write('--wait requested, but response did not include runId/taskId\n') 1700 } 1701 } 1702 return 0 1703 } 1704 1705 writeText(stdout, parsedResponse.value) 1706 return 0 1707 } catch (err) { 1708 stderr.write(`${err instanceof Error ? err.message : String(err)}\n`) 1709 return 1 1710 } 1711 } 1712 1713 function extractById(payload, id) { 1714 if (!id) return null 1715 1716 if (Array.isArray(payload)) { 1717 return payload.find((entry) => entry && String(entry.id) === String(id)) || null 1718 } 1719 1720 if (isPlainObject(payload)) { 1721 if (payload[id]) return payload[id] 1722 if (Array.isArray(payload.items)) { 1723 return payload.items.find((entry) => entry && String(entry.id) === String(id)) || null 1724 } 1725 } 1726 1727 return null 1728 } 1729 1730 function getApiCoveragePairs() { 1731 return COMMANDS 1732 .filter((command) => !command.virtual) 1733 .map((command) => `${command.method} ${command.route.split('?')[0]}`) 1734 } 1735 1736 module.exports = { 1737 COMMAND_GROUPS, 1738 COMMANDS, 1739 parseArgv, 1740 runCli, 1741 getCommand, 1742 getApiCoveragePairs, 1743 buildApiUrl, 1744 extractPathParams, 1745 resolveGroup, 1746 renderGeneralHelp, 1747 renderGroupHelp, 1748 }