@swarmclawai/swarmclaw 1.7.0 → 1.7.2

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 (42) hide show
  1. package/README.md +25 -9
  2. package/bin/swarmclaw.js +87 -0
  3. package/electron-dist/main.js +218 -0
  4. package/package.json +2 -2
  5. package/scripts/run-next-build.mjs +1 -1
  6. package/src/app/api/setup/check-provider/route.ts +5 -62
  7. package/src/app/api/setup/doctor/route.ts +19 -9
  8. package/src/app/home/page.tsx +19 -10
  9. package/src/cli/index.js +8 -2
  10. package/src/cli/index.ts +12 -3
  11. package/src/components/agents/inspector-panel.tsx +25 -3
  12. package/src/components/auth/setup-wizard/index.tsx +6 -2
  13. package/src/components/auth/setup-wizard/step-next.tsx +46 -39
  14. package/src/components/auth/setup-wizard/step-providers.tsx +113 -140
  15. package/src/components/auth/setup-wizard/types.ts +5 -2
  16. package/src/components/auth/setup-wizard/utils.test.ts +0 -19
  17. package/src/components/auth/setup-wizard/utils.ts +0 -69
  18. package/src/components/chat/chat-card.tsx +5 -0
  19. package/src/components/home/home-launchpad.tsx +123 -71
  20. package/src/components/layout/update-banner.tsx +43 -9
  21. package/src/lib/home-launchpad.test.ts +1 -31
  22. package/src/lib/home-launchpad.ts +0 -58
  23. package/src/lib/provider-sets.test.ts +19 -0
  24. package/src/lib/provider-sets.ts +8 -3
  25. package/src/lib/providers/cli-provider-metadata.test.ts +38 -0
  26. package/src/lib/providers/cli-provider-metadata.ts +208 -0
  27. package/src/lib/providers/cli-utils.test.ts +65 -1
  28. package/src/lib/providers/cli-utils.ts +26 -44
  29. package/src/lib/providers/codex-cli.ts +71 -75
  30. package/src/lib/providers/generic-cli.ts +2 -31
  31. package/src/lib/providers/index.ts +14 -44
  32. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +189 -0
  33. package/src/lib/server/chat-execution/chat-turn-finalization.ts +26 -19
  34. package/src/lib/server/cli-provider-readiness.test.ts +45 -0
  35. package/src/lib/server/cli-provider-readiness.ts +84 -0
  36. package/src/lib/server/provider-health.test.ts +6 -0
  37. package/src/lib/server/provider-health.ts +2 -2
  38. package/src/lib/setup-defaults.test.ts +8 -0
  39. package/src/lib/setup-defaults.ts +38 -178
  40. package/src/stores/slices/session-slice.test.ts +40 -2
  41. package/src/stores/slices/session-slice.ts +41 -1
  42. package/tsconfig.json +1 -0
package/README.md CHANGED
@@ -399,24 +399,40 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
- ### v1.7.0 Highlights
402
+ ### v1.7.2 Highlights
403
+
404
+ CLI provider usability follow-up for v1.7.0/v1.7.1. The expanded coding-agent roster is now easier to find, configure, and validate from onboarding and setup diagnostics.
405
+
406
+ - **Shared CLI provider registry.** Bespoke and generic CLI providers now share one metadata source for display names, binary names, capabilities, setup defaults, and provider-set behavior, reducing drift across onboarding, runtime routing, setup doctor, and capability prompts.
407
+ - **Onboarding exposes the full CLI roster.** The setup wizard groups providers by CLI agents, gateways/local runtimes, API providers, and custom endpoints, with search so the 31 extended CLI providers added in v1.7 are usable without digging through settings.
408
+ - **Connection checks for every CLI provider.** Bespoke CLIs keep auth-aware checks, while generic CLIs verify that the expected binary is on PATH and return actionable install guidance when missing.
409
+ - **Update banner polish.** Source installs now show the target stable tag/version, remember dismissal per release target, and make the required restart after update explicit.
410
+ - **macOS desktop note.** macOS builds remain ad-hoc signed and not notarized in this release, so the existing Gatekeeper/quarantine workaround still applies until Developer ID signing is available.
411
+
412
+ ### v1.7.1 Highlights
403
413
 
404
- Extended CLI provider roster, first-run polish, and a batch of v1.6 follow-ups including [#61](https://github.com/swarmclawai/swarmclaw/pull/61) by [@latentwill](https://github.com/latentwill). Thanks latentwill!
414
+ Republish of v1.7.0 from the correct commit. The v1.7.0 tarball on npm was inadvertently published from a pre-rebase tree that did not include the v1.6.1 codex continuity fixes (PR #62) or the v1.6.2 plan doc. v1.7.1 ships the same coding-agent-roster expansion on top of the correct v1.6.1+ history.
405
415
 
406
- **Coding-agent reach**
416
+ Use v1.7.1 instead of v1.7.0; v1.7.0 has been deprecated on npm.
407
417
 
408
- - **31 new CLI providers.** Aider, Amp, Augment, AdaL, IBM Bob, Cline, CodeBuddy, Command Code, Continue, Cortex Code, Crush, Deep Agents, Firebender, iFlow, Junie, Kilo Code, Kimi, Kode, MCPJam, Mistral Vibe, Mux, Neovate, OpenHands, Pochi, Qoder, Replit Agent, Roo Code, TRAE CN, Warp Agent, Windsurf, and Zencoder are now first-class provider IDs in `ProviderType`, matching the SwarmSkills roster.
418
+ ### v1.7.0 Highlights
419
+
420
+ Extended CLI provider roster — every coding agent recognized by SwarmSkills now has a corresponding CLI provider in SwarmClaw, routed through a generic streamer when no bespoke parser exists.
421
+
422
+ - **31 new CLI providers.** Aider, Amp, Augment, AdaL, IBM Bob, Cline, CodeBuddy, Command Code, Continue, Cortex Code, Crush, Deep Agents, Firebender, iFlow, Junie, Kilo Code, Kimi, Kode, MCPJam, Mistral Vibe, Mux, Neovate, OpenHands, Pochi, Qoder, Replit Agent, Roo Code, TRAE CN, Warp Agent, Windsurf, and Zencoder are now first-class provider IDs in `ProviderType`.
409
423
  - **Generic CLI streamer.** New `streamGenericCliChat` (`src/lib/providers/generic-cli.ts`) spawns the configured binary with the prompt as final argv and emits stdout lines as SSE deltas. Used by the new providers when no bespoke parser is available; existing bespoke parsers (Claude, Codex, Cursor, Gemini, Copilot, Droid, Qwen, OpenCode, Goose) are untouched.
410
424
  - **Capability metadata.** `CLI_PROVIDER_CAPABILITIES` (`src/lib/providers/cli-utils.ts`) carries a one-line description for each new provider so the UI and `isCliProvider()` recognize them.
425
+ - **Tests.** New `generic-cli.test.ts` exercises the streamer against `echo` and asserts the missing-binary error path; `cli-utils.test.ts` extends the CLI provider recognition coverage.
426
+
427
+ ### v1.6.1 Highlights
411
428
 
412
- **First-run and operator polish**
429
+ Follow-up release for v1.6 with workflow starts, safer metadata handling, A2A discovery polish, and [#61](https://github.com/swarmclawai/swarmclaw/pull/61) by [@latentwill](https://github.com/latentwill). Thanks latentwill!
413
430
 
414
- - **First useful action after install.** Setup completion and the home launchpad share the same assistant, workflow, and mission paths.
415
- - **Provider setup is easier to scan.** The setup wizard groups providers into fast local/no-key starts, recommended API providers, and advanced catalog/custom options, with the readiness check visible before provider details.
416
431
  - **Mission and protocol templates for real work.** New starter paths cover codebase review sprints, research bureau scans, content studio cycles, release readiness panels, synthesis panels, and builder review loops.
417
- - **A2A discovery is easier to integrate.** The canonical `/.well-known/agent-card.json` endpoint works alongside the legacy API route and hides disabled or trashed agents from public discovery.
432
+ - **Home launchpad paths.** First-run users can choose a self-hosted assistant, visual workflow, or autonomous mission path, with quality actions still one click away.
433
+ - **A2A discovery is easier to integrate.** The canonical `/.well-known/agent-card.json` endpoint now works alongside the legacy API route and hides disabled or trashed agents from public discovery.
418
434
  - **Internal metadata stripping is safer.** Side-channel JSON is removed with balanced-object parsing and zod validation so nested payloads are scrubbed without deleting ordinary user JSON.
419
- - **Browser smoke gate restored.** `npm run test:e2e` runs a Playwright smoke against health, A2A discovery, `/home` launch paths, and `/quality`, either against a live URL or a temporary local dev server.
435
+ - **Browser smoke gate restored.** `npm run test:e2e` now runs a Playwright smoke against health, A2A discovery, `/home`, and `/quality`, either against a live URL or a temporary local dev server.
420
436
  - **OpenCode CLI hang fixed.** OpenCode CLI delegation no longer keeps an inherited stdin pipe open, preventing hangs in non-interactive runs.
421
437
 
422
438
  ### v1.6.0 Highlights
package/bin/swarmclaw.js CHANGED
@@ -3,6 +3,8 @@
3
3
  /* eslint-disable @typescript-eslint/no-require-imports */
4
4
 
5
5
  const path = require('node:path')
6
+ const fs = require('node:fs')
7
+ const os = require('node:os')
6
8
  const { spawnSync } = require('node:child_process')
7
9
 
8
10
  // Legacy TS CLI groups/actions that provide richer, command-specific options.
@@ -142,6 +144,79 @@ Show the installed SwarmClaw package version.
142
144
  `.trim() + '\n')
143
145
  }
144
146
 
147
+ function readUserServiceSwarmclawHome() {
148
+ const homeDir = process.env.HOME || os.homedir()
149
+ const servicePath = path.join(homeDir, '.config', 'systemd', 'user', 'swarmclaw.service')
150
+ try {
151
+ const text = fs.readFileSync(servicePath, 'utf8')
152
+ const lines = text.split(/\r?\n/)
153
+ for (const line of lines) {
154
+ const trimmed = line.trim()
155
+ if (!trimmed.startsWith('Environment=')) continue
156
+ let value = trimmed.slice('Environment='.length).trim()
157
+ if (value.startsWith('"') && value.endsWith('"')) {
158
+ value = value.slice(1, -1)
159
+ }
160
+ if (!value.startsWith('SWARMCLAW_HOME=')) continue
161
+ const homeValue = value.slice('SWARMCLAW_HOME='.length).trim()
162
+ if (homeValue) return homeValue
163
+ }
164
+ } catch {
165
+ return null
166
+ }
167
+ return null
168
+ }
169
+
170
+ function getUserServicePath() {
171
+ const homeDir = process.env.HOME || os.homedir()
172
+ return path.join(homeDir, '.config', 'systemd', 'user', 'swarmclaw.service')
173
+ }
174
+
175
+ function hasUserSystemdService() {
176
+ return fs.existsSync(getUserServicePath())
177
+ }
178
+
179
+ function runSystemctlUser(args) {
180
+ return spawnSync('systemctl', ['--user', ...args], { stdio: 'inherit', env: process.env })
181
+ }
182
+
183
+ function hasServerLifecycleFlags(argv) {
184
+ return argv.some((arg) => (
185
+ arg === '--build'
186
+ || arg === '-d'
187
+ || arg === '--detach'
188
+ || arg === '--port'
189
+ || arg === '--ws-port'
190
+ || arg === '--host'
191
+ ))
192
+ }
193
+
194
+ function handleServiceLifecycle(top, argv) {
195
+ // If the user passed host/port/build flags, fall back to local server-cmd behavior.
196
+ const hasServerFlags = hasServerLifecycleFlags(argv)
197
+ if (hasServerFlags || !hasUserSystemdService()) return false
198
+
199
+ const unit = 'swarmclaw.service'
200
+ if (top === 'run' || top === 'start') {
201
+ const started = runSystemctlUser(['start', unit])
202
+ if (typeof started.status === 'number') process.exitCode = started.status
203
+ return true
204
+ }
205
+ if (top === 'stop') {
206
+ const stopped = runSystemctlUser(['stop', unit])
207
+ if (typeof stopped.status === 'number') process.exitCode = stopped.status
208
+ return true
209
+ }
210
+
211
+ return false
212
+ }
213
+
214
+ function applyServiceHomeDefault() {
215
+ if (typeof process.env.SWARMCLAW_HOME === 'string' && process.env.SWARMCLAW_HOME.trim()) return
216
+ const serviceHome = readUserServiceSwarmclawHome()
217
+ if (serviceHome) process.env.SWARMCLAW_HOME = serviceHome
218
+ }
219
+
145
220
  async function runMappedCli(argv) {
146
221
  const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.js')
147
222
  const cliModule = await import(cliPath)
@@ -196,6 +271,8 @@ async function runHelp(argv) {
196
271
  }
197
272
 
198
273
  async function main() {
274
+ applyServiceHomeDefault()
275
+
199
276
  const argv = process.argv.slice(2)
200
277
  const top = argv[0]
201
278
 
@@ -233,10 +310,20 @@ async function main() {
233
310
  }
234
311
  }
235
312
  if (top === 'run' || top === 'start') {
313
+ if (handleServiceLifecycle(top, argv.slice(1))) return
236
314
  await require('./server-cmd.js').main(argv.slice(1))
237
315
  return
238
316
  }
239
317
  if (top === 'status' || top === 'stop') {
318
+ if (top === 'status' && hasUserSystemdService() && !hasServerLifecycleFlags(argv.slice(1))) {
319
+ const serviceStatus = runSystemctlUser(['status', 'swarmclaw.service', '--no-pager'])
320
+ const apiStatusCode = await runMappedCli(['system-status', 'get'])
321
+ const serviceCode = typeof serviceStatus.status === 'number' ? serviceStatus.status : 1
322
+ const apiCode = typeof apiStatusCode === 'number' ? apiStatusCode : 1
323
+ process.exitCode = serviceCode !== 0 ? serviceCode : apiCode
324
+ return
325
+ }
326
+ if (handleServiceLifecycle(top, argv.slice(1))) return
240
327
  await require('./server-cmd.js').main([top, ...argv.slice(1)])
241
328
  return
242
329
  }
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const electron_1 = require("electron");
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const paths_1 = require("./paths");
43
+ const server_lifecycle_1 = require("./server-lifecycle");
44
+ const menu_1 = require("./menu");
45
+ const DEV_URL_DEFAULT = 'http://127.0.0.1:3456';
46
+ const LOG_TAIL_BYTES = 1500;
47
+ let mainWindow = null;
48
+ let serverHandle = null;
49
+ let serverLogFile = null;
50
+ let isQuitting = false;
51
+ const gotLock = electron_1.app.requestSingleInstanceLock();
52
+ if (!gotLock) {
53
+ electron_1.app.quit();
54
+ }
55
+ else {
56
+ electron_1.app.on('second-instance', () => {
57
+ if (mainWindow) {
58
+ if (mainWindow.isMinimized())
59
+ mainWindow.restore();
60
+ mainWindow.focus();
61
+ }
62
+ });
63
+ electron_1.app.on('ready', () => void onReady());
64
+ electron_1.app.on('window-all-closed', () => {
65
+ if (process.platform !== 'darwin')
66
+ electron_1.app.quit();
67
+ });
68
+ electron_1.app.on('activate', () => {
69
+ if (mainWindow !== null)
70
+ return;
71
+ if (serverHandle) {
72
+ createMainWindow(serverHandle.url);
73
+ }
74
+ else if (!electron_1.app.isPackaged) {
75
+ createMainWindow(process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT);
76
+ }
77
+ });
78
+ electron_1.app.on('before-quit', () => {
79
+ isQuitting = true;
80
+ });
81
+ electron_1.app.on('will-quit', async (event) => {
82
+ if (!serverHandle)
83
+ return;
84
+ event.preventDefault();
85
+ try {
86
+ await serverHandle.stop();
87
+ }
88
+ finally {
89
+ serverHandle = null;
90
+ electron_1.app.exit(0);
91
+ }
92
+ });
93
+ }
94
+ async function onReady() {
95
+ const paths = (0, paths_1.resolveRuntimePaths)();
96
+ (0, menu_1.buildAppMenu)(paths, () => mainWindow);
97
+ const iconPath = resolveIconPath();
98
+ if (process.platform === 'darwin' && iconPath && electron_1.app.dock) {
99
+ const img = electron_1.nativeImage.createFromPath(iconPath);
100
+ if (!img.isEmpty())
101
+ electron_1.app.dock.setIcon(img);
102
+ }
103
+ if (!electron_1.app.isPackaged) {
104
+ const devUrl = process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT;
105
+ console.log(`[swarmclaw] dev mode, loading ${devUrl}`);
106
+ createMainWindow(devUrl);
107
+ return;
108
+ }
109
+ serverLogFile = node_path_1.default.join(electron_1.app.getPath('userData'), 'logs', 'server.log');
110
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(serverLogFile), { recursive: true });
111
+ try {
112
+ serverHandle = await (0, server_lifecycle_1.startEmbeddedServer)({
113
+ paths,
114
+ logFile: serverLogFile,
115
+ onStdout: (c) => process.stdout.write(`[swarmclaw] ${c}`),
116
+ onStderr: (c) => process.stderr.write(`[swarmclaw] ${c}`),
117
+ onExit: (code, signal) => {
118
+ if (!isQuitting) {
119
+ console.error(`[swarmclaw] server exited unexpectedly (code=${code}, signal=${signal ?? 'none'})`);
120
+ void showServerCrashDialog(code, signal);
121
+ }
122
+ },
123
+ });
124
+ }
125
+ catch (err) {
126
+ await showStartupFailureDialog(err, paths);
127
+ electron_1.app.exit(1);
128
+ return;
129
+ }
130
+ createMainWindow(serverHandle.url);
131
+ void Promise.resolve().then(() => __importStar(require('./updater'))).then((m) => m.initAutoUpdater());
132
+ }
133
+ function resolveIconPath() {
134
+ const candidate = electron_1.app.isPackaged
135
+ ? node_path_1.default.join(process.resourcesPath, 'icon.png')
136
+ : node_path_1.default.join(__dirname, '..', 'resources', 'icon.png');
137
+ return node_fs_1.default.existsSync(candidate) ? candidate : undefined;
138
+ }
139
+ function createMainWindow(startUrl) {
140
+ const iconPath = resolveIconPath();
141
+ mainWindow = new electron_1.BrowserWindow({
142
+ width: 1440,
143
+ height: 900,
144
+ minWidth: 1024,
145
+ minHeight: 640,
146
+ backgroundColor: '#0b0b0f',
147
+ show: true,
148
+ ...(iconPath ? { icon: iconPath } : {}),
149
+ webPreferences: {
150
+ contextIsolation: true,
151
+ nodeIntegration: false,
152
+ sandbox: false,
153
+ },
154
+ });
155
+ const wc = mainWindow.webContents;
156
+ if (!electron_1.app.isPackaged)
157
+ wc.openDevTools({ mode: 'detach' });
158
+ wc.on('did-start-loading', () => console.log('[swarmclaw] did-start-loading'));
159
+ wc.on('did-finish-load', () => console.log('[swarmclaw] did-finish-load'));
160
+ wc.on('did-fail-load', (_e, code, desc, url) => console.error(`[swarmclaw] did-fail-load code=${code} desc=${desc} url=${url}`));
161
+ wc.on('render-process-gone', (_e, details) => console.error(`[swarmclaw] render-process-gone reason=${details.reason}`));
162
+ wc.on('unresponsive', () => console.error('[swarmclaw] webContents unresponsive'));
163
+ mainWindow.on('closed', () => {
164
+ mainWindow = null;
165
+ });
166
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
167
+ if (url.startsWith(startUrl))
168
+ return { action: 'allow' };
169
+ void electron_1.shell.openExternal(url);
170
+ return { action: 'deny' };
171
+ });
172
+ void mainWindow.loadURL(startUrl).catch((err) => {
173
+ console.error('[swarmclaw] loadURL rejected:', err);
174
+ });
175
+ }
176
+ async function showServerCrashDialog(code, signal) {
177
+ const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
178
+ const quitButtonId = buttons.length - 1;
179
+ const detail = buildLogDetail(`code=${code ?? 'null'} signal=${signal ?? 'none'}`);
180
+ const res = await electron_1.dialog.showMessageBox({
181
+ type: 'error',
182
+ buttons,
183
+ defaultId: quitButtonId,
184
+ cancelId: quitButtonId,
185
+ title: 'SwarmClaw stopped',
186
+ message: 'The SwarmClaw server exited unexpectedly.',
187
+ detail,
188
+ });
189
+ if (serverLogFile && res.response === 0)
190
+ electron_1.shell.showItemInFolder(serverLogFile);
191
+ electron_1.app.exit(1);
192
+ }
193
+ async function showStartupFailureDialog(err, paths) {
194
+ const message = err instanceof Error ? err.message : String(err);
195
+ const base = `${message}\n\nStandalone entry: ${paths.standaloneEntry}\nData dir: ${paths.dataDir}`;
196
+ const detail = buildLogDetail(base);
197
+ const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
198
+ const quitButtonId = buttons.length - 1;
199
+ const res = await electron_1.dialog.showMessageBox({
200
+ type: 'error',
201
+ buttons,
202
+ defaultId: quitButtonId,
203
+ cancelId: quitButtonId,
204
+ title: 'SwarmClaw failed to start',
205
+ message: 'The embedded server did not start.',
206
+ detail,
207
+ });
208
+ if (serverLogFile && res.response === 0)
209
+ electron_1.shell.showItemInFolder(serverLogFile);
210
+ }
211
+ function buildLogDetail(base) {
212
+ if (!serverLogFile)
213
+ return base;
214
+ const tail = (0, server_lifecycle_1.tailLogFile)(serverLogFile, LOG_TAIL_BYTES).trim();
215
+ if (!tail)
216
+ return `${base}\n\nLog file: ${serverLogFile}\n(no output captured yet)`;
217
+ return `${base}\n\nLog tail (${serverLogFile}):\n${tail}`;
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -87,7 +87,7 @@
87
87
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88
88
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
89
89
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
90
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
91
91
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
92
92
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
93
93
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -11,7 +11,7 @@ import { ensureBuildBootstrapPaths } from './build-bootstrap-env.mjs'
11
11
 
12
12
  const require = createRequire(import.meta.url)
13
13
 
14
- export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '8192'
14
+ export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '12288'
15
15
  export const MIN_MAX_OLD_SPACE_SIZE_MB = 1024
16
16
  export const FALLBACK_MIN_MAX_OLD_SPACE_SIZE_MB = 512
17
17
  export const RESERVED_BUILD_MEMORY_MB = 768
@@ -2,39 +2,12 @@ import { NextResponse } from 'next/server'
2
2
  import { loadCredentials, decryptKey, loadProviderConfigs } from '@/lib/server/storage'
3
3
  import { listCredentialIdsByProvider } from '@/lib/server/credentials/credential-service'
4
4
  import { getDeviceId, wsConnect, rpcOnConnectedGateway } from '@/lib/providers/openclaw'
5
- import { buildCliEnv, probeCliAuth, resolveCliBinary } from '@/lib/providers/cli-utils'
5
+ import { isCliProviderId } from '@/lib/providers/cli-provider-metadata'
6
+ import { checkCliProviderReady } from '@/lib/server/cli-provider-readiness'
6
7
  import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
7
8
  import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
8
9
  import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
9
10
 
10
- type SetupProvider =
11
- | 'claude-cli'
12
- | 'codex-cli'
13
- | 'opencode-cli'
14
- | 'gemini-cli'
15
- | 'copilot-cli'
16
- | 'droid-cli'
17
- | 'cursor-cli'
18
- | 'qwen-code-cli'
19
- | 'goose'
20
- | 'openai'
21
- | 'openrouter'
22
- | 'anthropic'
23
- | 'google'
24
- | 'deepseek'
25
- | 'groq'
26
- | 'together'
27
- | 'mistral'
28
- | 'xai'
29
- | 'fireworks'
30
- | 'nebius'
31
- | 'deepinfra'
32
- | 'ollama'
33
- | 'openclaw'
34
- | 'hermes'
35
-
36
- type CliSetupProvider = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'droid-cli' | 'cursor-cli' | 'qwen-code-cli' | 'goose'
37
-
38
11
  interface SetupCheckBody {
39
12
  provider?: string
40
13
  apiKey?: string
@@ -280,43 +253,13 @@ async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok:
280
253
  return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId, recommendedModel }
281
254
  }
282
255
 
283
- function checkCliProvider(provider: CliSetupProvider): { ok: boolean; message: string } {
284
- const env = buildCliEnv()
285
- const config = {
286
- 'claude-cli': { binary: 'claude', backend: 'claude' as const, label: 'Claude Code CLI' },
287
- 'codex-cli': { binary: 'codex', backend: 'codex' as const, label: 'OpenAI Codex CLI' },
288
- 'opencode-cli': { binary: 'opencode', backend: 'opencode' as const, label: 'OpenCode CLI' },
289
- 'gemini-cli': { binary: 'gemini', backend: 'gemini' as const, label: 'Gemini CLI' },
290
- 'copilot-cli': { binary: 'copilot', backend: 'copilot' as const, label: 'GitHub Copilot CLI' },
291
- 'droid-cli': { binary: 'droid', backend: 'droid' as const, label: 'Factory Droid CLI' },
292
- 'cursor-cli': { binary: 'cursor-agent', backend: 'cursor' as const, label: 'Cursor Agent CLI' },
293
- 'qwen-code-cli': { binary: 'qwen', backend: 'qwen' as const, label: 'Qwen Code CLI' },
294
- goose: { binary: 'goose', backend: 'goose' as const, label: 'Goose CLI' },
295
- }[provider]
296
-
297
- if (!config) return { ok: false, message: 'Unknown CLI provider.' }
298
- const binary = resolveCliBinary(config.binary)
299
- if (!binary) {
300
- return {
301
- ok: false,
302
- message: `${config.label} is not installed. Install \`${config.binary}\` and ensure it is on your PATH.`,
303
- }
304
- }
305
- const auth = probeCliAuth(binary, config.backend, env, process.cwd())
306
- if (!auth.authenticated) {
307
- return { ok: false, message: auth.errorMessage || `${config.label} is not configured.` }
308
- }
309
- return { ok: true, message: `${config.label} is installed and ready.` }
310
- }
311
-
312
256
  export async function POST(req: Request) {
313
257
  const body = parseBody(await req.json().catch(() => ({})))
314
- const provider = clean(body.provider) as SetupProvider
258
+ const provider = clean(body.provider)
315
259
  let apiKey = clean(body.apiKey)
316
260
  const credentialId = clean(body.credentialId)
317
261
  let endpoint = clean(body.endpoint)
318
262
  const model = clean(body.model)
319
- const CLI_PROVIDERS = new Set<CliSetupProvider>(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose'])
320
263
 
321
264
  // Resolve credentialId to an API key if no raw key was provided
322
265
  if (!apiKey && credentialId) {
@@ -355,8 +298,8 @@ export async function POST(req: Request) {
355
298
  } catch { /* best effort */ }
356
299
  }
357
300
 
358
- if (CLI_PROVIDERS.has(provider as CliSetupProvider)) {
359
- const result = checkCliProvider(provider as CliSetupProvider)
301
+ if (isCliProviderId(provider)) {
302
+ const result = checkCliProviderReady(provider)
360
303
  return NextResponse.json(result)
361
304
  }
362
305
 
@@ -6,6 +6,7 @@ import { DATA_DIR } from '@/lib/server/data-dir'
6
6
  import { loadAgents, loadCredentials, loadSettings, loadCollection } from '@/lib/server/storage'
7
7
  import { dedup, errorMessage } from '@/lib/shared-utils'
8
8
  import { detectDocker } from '@/lib/server/sandbox/docker-detect'
9
+ import { BESPOKE_CLI_PROVIDER_METADATA, GENERIC_CLI_PROVIDER_METADATA } from '@/lib/providers/cli-provider-metadata'
9
10
 
10
11
  type CheckStatus = 'pass' | 'warn' | 'fail'
11
12
 
@@ -198,15 +199,11 @@ export async function GET(req: Request) {
198
199
  } catch { /* best-effort */ }
199
200
 
200
201
  const optionalBinaries: Array<{ id: string; label: string; command: string }> = [
201
- { id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
202
- { id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
203
- { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
204
- { id: 'gemini-cli', label: 'Gemini CLI', command: 'gemini' },
205
- { id: 'copilot-cli', label: 'GitHub Copilot CLI', command: 'copilot' },
206
- { id: 'droid-cli', label: 'Factory Droid CLI', command: 'droid' },
207
- { id: 'cursor-cli', label: 'Cursor Agent CLI', command: 'cursor-agent' },
208
- { id: 'qwen-code-cli', label: 'Qwen Code CLI', command: 'qwen' },
209
- { id: 'goose', label: 'Goose CLI', command: 'goose' },
202
+ ...BESPOKE_CLI_PROVIDER_METADATA.map((provider) => ({
203
+ id: provider.id,
204
+ label: provider.displayName,
205
+ command: provider.binaryName,
206
+ })),
210
207
  { id: 'google-workspace-cli', label: 'Google Workspace CLI', command: 'gws' },
211
208
  ]
212
209
 
@@ -223,6 +220,19 @@ export async function GET(req: Request) {
223
220
  )
224
221
  }
225
222
 
223
+ const genericCliInstalled = GENERIC_CLI_PROVIDER_METADATA
224
+ .filter((provider) => commandExists(provider.binaryName))
225
+ .map((provider) => provider.displayName)
226
+ pushCheck(
227
+ checks,
228
+ 'extended-cli-providers',
229
+ 'Extended CLI provider roster',
230
+ genericCliInstalled.length > 0 ? 'pass' : 'warn',
231
+ genericCliInstalled.length > 0
232
+ ? `${genericCliInstalled.length} extended CLI provider(s) detected: ${genericCliInstalled.slice(0, 8).join(', ')}${genericCliInstalled.length > 8 ? ', ...' : ''}.`
233
+ : `${GENERIC_CLI_PROVIDER_METADATA.length} optional extended CLI provider(s) are available in SwarmClaw; install any matching CLI when you want to use it.`,
234
+ )
235
+
226
236
  const extensionSettings = (settings?.extensionSettings && typeof settings.extensionSettings === 'object')
227
237
  ? settings.extensionSettings as Record<string, Record<string, unknown>>
228
238
  : {}
@@ -14,13 +14,7 @@ import { useNavigate } from '@/lib/app/navigation'
14
14
  import { safeStorageGet, safeStorageRemove } from '@/lib/app/safe-storage'
15
15
  import { isLocalhostBrowser, isVisibleSessionForViewer } from '@/lib/observability/local-observability'
16
16
  import { getSessionLastMessage } from '@/lib/chat/session-summary'
17
- import {
18
- deriveHomeMode,
19
- HOME_LAUNCHPAD_AFTER_SETUP_KEY,
20
- resolveLaunchPathHref,
21
- type LaunchPathAction,
22
- type LaunchPathId,
23
- } from '@/lib/home-launchpad'
17
+ import { DEFAULT_BUILDER_ROUTE, deriveHomeMode, HOME_LAUNCHPAD_AFTER_SETUP_KEY } from '@/lib/home-launchpad'
24
18
  import { getNotificationActivityAt, getNotificationOccurrenceCount } from '@/lib/notifications/notification-utils'
25
19
  import { timeAgo, timeUntil } from '@/lib/time-format'
26
20
  import type { Agent, Session, BoardTask, AppNotification, ActivityEntry } from '@/types'
@@ -260,8 +254,16 @@ export default function HomePage() {
260
254
  todayCost,
261
255
  })
262
256
 
263
- const handleLaunchPathAction = (id: LaunchPathId, action: LaunchPathAction) => {
264
- router.push(resolveLaunchPathHref(id, action, firstAgent?.id))
257
+ const openFirstAgent = () => {
258
+ if (firstAgent) {
259
+ navigateTo('agents', firstAgent.id)
260
+ return
261
+ }
262
+ navigateTo('agents')
263
+ }
264
+
265
+ const openBuilder = () => {
266
+ router.push(DEFAULT_BUILDER_ROUTE)
265
267
  }
266
268
 
267
269
  if (homeMode === 'launchpad') {
@@ -276,8 +278,15 @@ export default function HomePage() {
276
278
  scheduleCount={scheduleCount}
277
279
  connectorCount={connectorCount}
278
280
  todayCost={todayCost}
279
- onLaunchPathAction={handleLaunchPathAction}
281
+ onOpenFirstAgent={openFirstAgent}
282
+ onOpenProtocols={() => navigateTo('protocols')}
283
+ onOpenBuilder={openBuilder}
284
+ onOpenConnectors={() => navigateTo('connectors')}
280
285
  onOpenUsage={() => navigateTo('usage')}
286
+ onRunEvalSuite={() => navigateTo('quality')}
287
+ onReviewApprovals={() => navigateTo('quality')}
288
+ onInspectFailedRuns={() => navigateTo('quality')}
289
+ onStartReleaseQaMission={() => navigateTo('missions')}
281
290
  />
282
291
  </div>
283
292
  </MainContent>
package/src/cli/index.js CHANGED
@@ -1001,8 +1001,14 @@ function resolveAccessKey(opts, env, cwd) {
1001
1001
  const envKey = env.SWARMCLAW_API_KEY || env.SC_ACCESS_KEY || env.SWARMCLAW_ACCESS_KEY || ''
1002
1002
  if (envKey) return String(envKey).trim()
1003
1003
 
1004
- const keyFile = path.join(cwd, 'platform-api-key.txt')
1005
- if (fs.existsSync(keyFile)) {
1004
+ const keyLocations = [
1005
+ path.join(cwd, 'platform-api-key.txt'),
1006
+ ]
1007
+ const homeDir = String(env.SWARMCLAW_HOME || '').trim()
1008
+ if (homeDir) keyLocations.push(path.join(homeDir, 'platform-api-key.txt'))
1009
+
1010
+ for (const keyFile of keyLocations) {
1011
+ if (!fs.existsSync(keyFile)) continue
1006
1012
  const content = fs.readFileSync(keyFile, 'utf8').trim()
1007
1013
  if (content) return content
1008
1014
  }