@stratusagent/cli 0.13.6 → 0.14.0

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 (116) hide show
  1. package/README.md +3 -1
  2. package/dist/bin.js +8 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/agent-new.d.ts.map +1 -1
  5. package/dist/commands/agent-new.js +3 -11
  6. package/dist/commands/agent-new.js.map +1 -1
  7. package/dist/commands/channel.d.ts.map +1 -1
  8. package/dist/commands/channel.js +5 -0
  9. package/dist/commands/channel.js.map +1 -1
  10. package/dist/commands/credential.d.ts +1 -1
  11. package/dist/commands/credential.d.ts.map +1 -1
  12. package/dist/commands/credential.js +59 -10
  13. package/dist/commands/credential.js.map +1 -1
  14. package/dist/commands/dashboard.d.ts.map +1 -1
  15. package/dist/commands/dashboard.js +132 -7
  16. package/dist/commands/dashboard.js.map +1 -1
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +81 -14
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/events.d.ts +14 -0
  21. package/dist/commands/events.d.ts.map +1 -0
  22. package/dist/commands/events.js +94 -0
  23. package/dist/commands/events.js.map +1 -0
  24. package/dist/commands/logs.d.ts.map +1 -1
  25. package/dist/commands/logs.js +8 -3
  26. package/dist/commands/logs.js.map +1 -1
  27. package/dist/commands/plugins.d.ts.map +1 -1
  28. package/dist/commands/plugins.js +2 -2
  29. package/dist/commands/plugins.js.map +1 -1
  30. package/dist/commands/schedules.d.ts.map +1 -1
  31. package/dist/commands/schedules.js +6 -1
  32. package/dist/commands/schedules.js.map +1 -1
  33. package/dist/commands/serve.d.ts +8 -0
  34. package/dist/commands/serve.d.ts.map +1 -1
  35. package/dist/commands/serve.js +44 -7
  36. package/dist/commands/serve.js.map +1 -1
  37. package/dist/commands/setup.d.ts.map +1 -1
  38. package/dist/commands/setup.js +137 -126
  39. package/dist/commands/setup.js.map +1 -1
  40. package/dist/commands/signin.d.ts.map +1 -1
  41. package/dist/commands/signin.js +32 -3
  42. package/dist/commands/signin.js.map +1 -1
  43. package/dist/commands/update.d.ts.map +1 -1
  44. package/dist/commands/update.js +71 -16
  45. package/dist/commands/update.js.map +1 -1
  46. package/dist/companions.d.ts.map +1 -1
  47. package/dist/companions.js +2 -2
  48. package/dist/companions.js.map +1 -1
  49. package/dist/environment.d.ts +12 -0
  50. package/dist/environment.d.ts.map +1 -1
  51. package/dist/help.d.ts +1 -1
  52. package/dist/help.d.ts.map +1 -1
  53. package/dist/help.js +33 -11
  54. package/dist/help.js.map +1 -1
  55. package/dist/index.d.ts +2 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/io.d.ts +24 -0
  60. package/dist/io.d.ts.map +1 -1
  61. package/dist/io.js +29 -0
  62. package/dist/io.js.map +1 -1
  63. package/dist/loaders.d.ts +7 -0
  64. package/dist/loaders.d.ts.map +1 -1
  65. package/dist/loaders.js +17 -9
  66. package/dist/loaders.js.map +1 -1
  67. package/dist/main.d.ts.map +1 -1
  68. package/dist/main.js +19 -7
  69. package/dist/main.js.map +1 -1
  70. package/dist/npm.d.ts +17 -1
  71. package/dist/npm.d.ts.map +1 -1
  72. package/dist/npm.js +21 -6
  73. package/dist/npm.js.map +1 -1
  74. package/dist/parse.d.ts +27 -3
  75. package/dist/parse.d.ts.map +1 -1
  76. package/dist/parse.js +59 -7
  77. package/dist/parse.js.map +1 -1
  78. package/dist/plugin-catalog.d.ts +0 -9
  79. package/dist/plugin-catalog.d.ts.map +1 -1
  80. package/dist/plugin-catalog.js +4 -14
  81. package/dist/plugin-catalog.js.map +1 -1
  82. package/dist/plugin-index.d.ts +136 -0
  83. package/dist/plugin-index.d.ts.map +1 -0
  84. package/dist/plugin-index.js +293 -0
  85. package/dist/plugin-index.js.map +1 -0
  86. package/dist/plugin-updates.d.ts +54 -0
  87. package/dist/plugin-updates.d.ts.map +1 -0
  88. package/dist/plugin-updates.js +68 -0
  89. package/dist/plugin-updates.js.map +1 -0
  90. package/dist/provider-packages.d.ts +42 -0
  91. package/dist/provider-packages.d.ts.map +1 -0
  92. package/dist/{harness-packages.js → provider-packages.js} +28 -16
  93. package/dist/provider-packages.js.map +1 -0
  94. package/dist/roster.d.ts.map +1 -1
  95. package/dist/roster.js +19 -2
  96. package/dist/roster.js.map +1 -1
  97. package/dist/runtime.d.ts.map +1 -1
  98. package/dist/runtime.js +5 -0
  99. package/dist/runtime.js.map +1 -1
  100. package/dist/service.d.ts.map +1 -1
  101. package/dist/service.js +67 -1
  102. package/dist/service.js.map +1 -1
  103. package/dist/slack.d.ts.map +1 -1
  104. package/dist/slack.js +4 -0
  105. package/dist/slack.js.map +1 -1
  106. package/dist/supervisor.d.ts.map +1 -1
  107. package/dist/supervisor.js +1 -0
  108. package/dist/supervisor.js.map +1 -1
  109. package/dist/trusted-config.d.ts +8 -1
  110. package/dist/trusted-config.d.ts.map +1 -1
  111. package/dist/trusted-config.js +19 -0
  112. package/dist/trusted-config.js.map +1 -1
  113. package/package.json +22 -25
  114. package/dist/harness-packages.d.ts +0 -31
  115. package/dist/harness-packages.d.ts.map +0 -1
  116. package/dist/harness-packages.js.map +0 -1
@@ -1,18 +1,17 @@
1
- import { harnessesMissingMessage, installHarnessPackages, missingHarnessPackages } from "../harness-packages.js";
1
+ import { providerPackagesMissingMessage, installProviderPackages, missingProviderPackages, uninstalledProviderPackages } from "../provider-packages.js";
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { preflightPlugin, readPluginManifest, PluginConfigError, PluginManifestError, } from '@stratusagent/plugins';
5
- import { DEFAULT_ANTHROPIC_MODEL } from '@stratusagent/provider-anthropic';
6
- import { DEFAULT_CODEX_MODEL } from '@stratusagent/providers';
7
5
  import { formatSoul, parseSoul } from '@stratusagent/agents';
8
- import { claimSoulFile, collectAvailableModels as collectModels, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, globalConfigPath, loadChannelCredentials, loadChannelTransportSecrets, loadCredentials, loadRosterSouls, loadSoulFile, isRegisteredProviderName, languageFor, readNonEmptyString, registeredProviderNameOf, readProcessEnv, readWorkingDirectory, resolveAgentApprovals, resolveRuntimeConfig, DEFAULT_CONFIG_FILENAME, loadConfigFile, saveChannelCredentials, saveChannelTransportSecrets, removeChannelTransportSecrets, CREDENTIAL_NAME_PATTERN, saveConfigFile, saveCredentials, resolveAgentPrincipals, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, } from '@stratusagent/state';
6
+ import { claimSoulFile, collectAvailableModels as collectModels, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, defaultModelFor as builtInModelDefault, DEFAULT_STRATUS_AGENT, globalConfigPath, loadChannelCredentials, loadChannelTransportSecrets, loadCredentials, loadRosterSouls, loadSoulFile, isRegisteredProviderName, languageFor, readNonEmptyString, registeredProviderNameOf, readProcessEnv, readWorkingDirectory, resolveAgentApprovals, resolveRuntimeConfig, DEFAULT_CONFIG_FILENAME, loadConfigFile, saveChannelCredentials, saveChannelTransportSecrets, removeChannelTransportSecrets, CREDENTIAL_NAME_PATTERN, saveConfigFile, saveCredentials, resolveAgentPrincipals, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, } from '@stratusagent/state';
9
7
  import { installService, readServiceStatus, servicePlatform, serviceUnitPath, uninstallService, } from "../service.js";
10
8
  import { classifyApprovalReach, unattendedReachParts } from "../approvals.js";
11
9
  import { serviceEnvFor } from "../daemon.js";
12
10
  import { writeLine } from "../io.js";
13
11
  import { packageInstalled } from "../loaders.js";
14
12
  import { isPackageName, isInstallableSpecifier, defaultPackageInstaller } from "../npm.js";
15
- import { PLUGIN_MARKETPLACE_URL, FIRST_PARTY_CAPABILITY_PACKAGES } from "../plugin-catalog.js";
13
+ import { FIRST_PARTY_CAPABILITY_PACKAGES } from "../plugin-catalog.js";
14
+ import { checkPluginReleases, currentPlatformHost, installSpecifier, readPluginIndex, releaseVerdict } from "../plugin-index.js";
16
15
  import { menuPrefixWidth, quoteShellArg, stratusHeaderLines, createSetupPrompter, } from "../prompter.js";
17
16
  import { soulGrantsTool } from "../roster.js";
18
17
  import { runSingleLoop, printSessionSummary, formatRuntimeBanner } from "../runtime.js";
@@ -145,11 +144,9 @@ export const runSetup = async (command, streams, env = {}) => {
145
144
  header: stratusHeaderLines,
146
145
  consumeNotices: () => recentNotices.splice(0),
147
146
  });
148
- const builtInDefaultModel = (provider) => provider === 'openai'
149
- ? DEFAULT_OPENAI_MODEL
150
- : provider === 'codex'
151
- ? DEFAULT_CODEX_MODEL
152
- : DEFAULT_ANTHROPIC_MODEL;
147
+ // The demo model has none of its own; setup names Anthropic's for it,
148
+ // as it always has.
149
+ const builtInDefaultModel = (provider) => builtInModelDefault(provider === 'demo' ? 'anthropic' : provider);
153
150
  // A contributed provider's default is its own, so setup has none to
154
151
  // name for it — and none to write into a config or a soul.
155
152
  const defaultModelFor = (provider) => isRegisteredProviderName(provider) ? undefined : builtInDefaultModel(provider);
@@ -1524,11 +1521,16 @@ export const runSetup = async (command, streams, env = {}) => {
1524
1521
  : 'installed, not enabled';
1525
1522
  return fitMenuRow(short.padEnd(18) + status, reserved);
1526
1523
  });
1527
- options.push('Back');
1528
- const choice = await prompter.select('Plugins — what your agents can do (installing one grants nothing on its own)', options, { footnote: `More plugins — ${PLUGIN_MARKETPLACE_URL}` });
1524
+ const more = options.length;
1525
+ options.push('More plugins… (checks npm)', 'Back');
1526
+ const choice = await prompter.select('Plugins — what your agents can do (installing one grants nothing on its own)', options);
1529
1527
  if (choice.kind !== 'index' || choice.index === options.length - 1) {
1530
1528
  return;
1531
1529
  }
1530
+ if (choice.index === more) {
1531
+ await chooseMorePlugins();
1532
+ continue;
1533
+ }
1532
1534
  const name = packages[choice.index];
1533
1535
  if (!name) {
1534
1536
  return;
@@ -1536,7 +1538,70 @@ export const runSetup = async (command, streams, env = {}) => {
1536
1538
  await choosePlugin(name);
1537
1539
  }
1538
1540
  };
1539
- const choosePlugin = async (name) => {
1541
+ /**
1542
+ * The plugins that release apart from this CLI, from the curated index
1543
+ * (`plugin-index.ts`). Asked for, never fetched on its own: it runs npm,
1544
+ * which reaches the registry, and the main menu has to work offline.
1545
+ *
1546
+ * An entry is offered only when its latest release would install and
1547
+ * load here (`releaseVerdict` says what that takes), and the install is
1548
+ * pinned to that release. The rest are named with the reason, so a plugin
1549
+ * missing from the list is not a mystery. Picking one goes to the same
1550
+ * install-then-enable path as every other row, which still asks first and
1551
+ * still reads what it contributes from its own manifest.
1552
+ */
1553
+ const chooseMorePlugins = async () => {
1554
+ writeLine(streams.stdout);
1555
+ writeLine(streams.stdout, 'Checking npm for plugins…');
1556
+ const index = await readPluginIndex(env);
1557
+ if (index.source === 'bundled') {
1558
+ writeLine(streams.stdout, 'npm has no plugin index this CLI can read, so this is the list it shipped with.');
1559
+ }
1560
+ const listed = new Set(pluginPackages());
1561
+ const candidates = index.entries.filter((entry) => !listed.has(entry.name));
1562
+ const releases = await checkPluginReleases(candidates.map((entry) => entry.name), env);
1563
+ const host = currentPlatformHost();
1564
+ const offered = [];
1565
+ const skipped = [];
1566
+ for (const entry of candidates) {
1567
+ const verdict = releaseVerdict(entry.name, releases.get(entry.name) ?? { status: 'unknown' }, host);
1568
+ if (verdict.offer) {
1569
+ offered.push({ ...entry, version: verdict.version });
1570
+ }
1571
+ else {
1572
+ // At least one space after a name longer than the column: a
1573
+ // third-party name has no @stratusagent/ to shorten.
1574
+ skipped.push(` ${entry.name.replace('@stratusagent/', '').padEnd(17)} ${verdict.reason}`);
1575
+ }
1576
+ }
1577
+ if (skipped.length > 0) {
1578
+ writeLine(streams.stdout, 'Not offered here:');
1579
+ for (const line of skipped) {
1580
+ writeLine(streams.stdout, line);
1581
+ }
1582
+ }
1583
+ if (offered.length === 0) {
1584
+ writeLine(streams.stdout, 'There are no more plugins to offer here right now.');
1585
+ await prompter.ask('Press Enter to return to the menu… ');
1586
+ return;
1587
+ }
1588
+ const reserved = menuPrefixWidth(offered.length + 1);
1589
+ const options = offered.map((entry) => fitMenuRow(`${entry.name.replace('@stratusagent/', '').padEnd(17)} ${entry.description}`, reserved));
1590
+ options.push('Back');
1591
+ const choice = await prompter.select('More plugins — installing one grants nothing on its own', options);
1592
+ if (choice.kind !== 'index' || choice.index === options.length - 1) {
1593
+ return;
1594
+ }
1595
+ const entry = offered[choice.index];
1596
+ if (entry) {
1597
+ await choosePlugin(entry.name, installSpecifier(entry.name, entry.version));
1598
+ }
1599
+ };
1600
+ /**
1601
+ * `install` is what `npm install -g` is handed, when it isn't the bare
1602
+ * name: More plugins pins the version it checked.
1603
+ */
1604
+ const choosePlugin = async (name, install = name) => {
1540
1605
  const setup = PLUGIN_SETUP[name];
1541
1606
  const installed = packageInstalled(name, env);
1542
1607
  const enabled = pluginEnabled(name);
@@ -1605,15 +1670,15 @@ export const runSetup = async (command, streams, env = {}) => {
1605
1670
  disablePlugin(name);
1606
1671
  return;
1607
1672
  }
1608
- writeLine(streams.stdout, `Running: npm install -g ${name}`);
1609
- const result = await (env.packageInstaller ?? defaultPackageInstaller)([name])
1673
+ writeLine(streams.stdout, `Running: npm install -g ${install}`);
1674
+ const result = await (env.packageInstaller ?? defaultPackageInstaller)([install])
1610
1675
  .catch((error) => ({
1611
1676
  ok: false,
1612
1677
  message: error instanceof Error ? error.message : String(error),
1613
1678
  }));
1614
1679
  if (!result.ok) {
1615
1680
  writeLine(streams.stderr, `Could not install: ${result.message}`);
1616
- writeLine(streams.stderr, `Run \`npm install -g ${name}\` yourself, then re-run setup to enable it.`);
1681
+ writeLine(streams.stderr, `Run \`npm install -g ${install}\` yourself, then re-run setup to enable it.`);
1617
1682
  return;
1618
1683
  }
1619
1684
  writeLine(streams.stdout, `Installed ${name}.`);
@@ -2320,6 +2385,15 @@ export const runSetup = async (command, streams, env = {}) => {
2320
2385
  if (!runtime) {
2321
2386
  return;
2322
2387
  }
2388
+ // The CLI carries no provider package, and this row comes before Save,
2389
+ // which installs the one picked. Without this, the first hello on a
2390
+ // fresh install failed on the missing package and told someone already
2391
+ // in setup to choose the provider again in setup. Installed the way
2392
+ // Save does it, without a question: choosing the provider was the
2393
+ // decision. A failed install has already said so, with the command.
2394
+ if (!(await installProviderPackages(uninstalledProviderPackages([runtime], env), streams, env))) {
2395
+ return;
2396
+ }
2323
2397
  writeLine(streams.stdout, `Running a quick hello (${formatRuntimeBanner(runtime).replace('Starting Stratus Agent local loop with ', '')})…`);
2324
2398
  try {
2325
2399
  const session = await runSingleLoop('Say hello and introduce yourself in one short sentence.', streams, { events: false, runtime, env });
@@ -2359,116 +2433,53 @@ export const runSetup = async (command, streams, env = {}) => {
2359
2433
  };
2360
2434
  };
2361
2435
  /**
2362
- * Whether `stratus dashboard` can work. Read before the offer and set by
2363
- * it, so the closing suggestions never name a command this machine cannot
2364
- * run — the failure that sends someone to the logs to find out why.
2365
- */
2366
- let dashboardReady = packageInstalled('@stratusagent/control-api', env)
2367
- && packageInstalled('@stratusagent/dashboard', env);
2368
- const missingPackageGroups = () => {
2369
- const groups = [];
2370
- const slackAgents = Object.keys(state.channels.slack ?? {}).length;
2371
- if (slackAgents > 0 && !packageInstalled('@stratusagent/channel-slack', env)) {
2372
- groups.push({
2373
- id: 'slack',
2374
- label: 'Slack channel',
2375
- why: `Slack tokens are stored for ${slackAgents} agent(s), but nothing connects to Slack without it`,
2376
- packages: ['@stratusagent/channel-slack'],
2377
- });
2378
- }
2379
- // Both, because the dashboard is what was offered: the control API is
2380
- // the port and the dashboard is the page served on it. Only the missing
2381
- // half is installed, so accepting this on a machine that already has
2382
- // the API does not reinstall it.
2383
- const dashboardPackages = ['@stratusagent/control-api', '@stratusagent/dashboard']
2384
- .filter((name) => !packageInstalled(name, env));
2385
- if (dashboardPackages.length > 0) {
2386
- groups.push({
2387
- id: 'dashboard',
2388
- label: 'Web dashboard',
2389
- why: '`stratus dashboard` needs it, and it opens an authenticated port on 127.0.0.1',
2390
- packages: dashboardPackages,
2391
- });
2392
- }
2393
- return groups;
2394
- };
2395
- /**
2396
- * Offers the packages above. Declining is a real answer and prints the
2397
- * command; the control API in particular binds a port, and installing it
2398
- * is how an operator says they want one open, so this asks rather than
2399
- * deciding for them.
2436
+ * The Slack channel package, when this setup has Slack tokens stored and
2437
+ * it is not installed. Setup knows that before the daemon does, so it is
2438
+ * the one place that can offer the install rather than leave the gap to
2439
+ * be found in a log after the fact.
2440
+ *
2441
+ * The dashboard is deliberately not offered here: its control API binds a
2442
+ * port, and the time to ask about that is when someone runs
2443
+ * `stratus dashboard`, which offers its own packages (card 308).
2444
+ * Declining is a real answer and prints the command.
2400
2445
  */
2401
2446
  const offerOptionalPackages = async () => {
2402
- const groups = missingPackageGroups();
2403
- if (groups.length === 0) {
2447
+ const slackPackage = '@stratusagent/channel-slack';
2448
+ const slackAgents = Object.keys(state.channels.slack ?? {}).length;
2449
+ if (slackAgents === 0 || packageInstalled(slackPackage, env)) {
2404
2450
  return;
2405
2451
  }
2406
2452
  writeLine(streams.stdout);
2407
- writeLine(streams.stdout, groups.length === 1
2408
- ? 'One optional package is not installed:'
2409
- : `${groups.length} optional packages are not installed:`);
2410
- for (const group of groups) {
2411
- writeLine(streams.stdout, ` ${group.packages.join(' ')}`);
2412
- writeLine(streams.stdout, ` ${group.why}.`);
2413
- }
2414
- const answer = await prompter.select('Install now with npm install -g?', groups.length === 1
2415
- ? ['Install it now', 'Skip']
2416
- : ['Install all of them now', ...groups.map((group) => `Install the ${group.label} only`), 'Skip']);
2417
- const chosen = (() => {
2418
- if (answer.kind !== 'index') {
2419
- return [];
2420
- }
2421
- if (groups.length === 1) {
2422
- return answer.index === 0 ? groups : [];
2423
- }
2424
- if (answer.index === 0) {
2425
- return groups;
2426
- }
2427
- const only = groups[answer.index - 1];
2428
- return only ? [only] : [];
2429
- })();
2430
- // Held before the install rather than derived after it: picking one
2431
- // group is not a decision about the other, and the daemon starts a few
2432
- // lines below and warns about exactly what is still missing — the
2433
- // notice this whole offer exists to pre-empt.
2434
- const declined = groups.filter((group) => !chosen.includes(group));
2435
- if (chosen.length > 0) {
2436
- const packages = chosen.flatMap((group) => group.packages);
2437
- writeLine(streams.stdout, `Running: npm install -g ${packages.join(' ')}`);
2438
- // Never fails setup — not even its exit code, unlike the always-on
2439
- // service step below: the config and credentials are already written,
2440
- // and a package that did not install is a warning at the next start,
2441
- // not a broken machine.
2442
- const result = await (env.packageInstaller ?? defaultPackageInstaller)(packages)
2443
- .catch((error) => ({
2444
- ok: false,
2445
- message: error instanceof Error ? error.message : String(error),
2446
- }));
2447
- if (result.ok) {
2448
- writeLine(streams.stdout, `Installed ${packages.join(' ')}.`);
2449
- // npm's exit code, not a second resolve: a package written into the
2450
- // global prefix a moment ago need not be resolvable from THIS
2451
- // process, whose module resolution was fixed when it started.
2452
- if (chosen.some((group) => group.id === 'dashboard')) {
2453
- dashboardReady = true;
2454
- }
2455
- }
2456
- else {
2457
- writeLine(streams.stderr, `Could not install: ${result.message}`);
2458
- writeLine(streams.stderr, `Setup is saved either way — run \`npm install -g ${packages.join(' ')}\` yourself.`);
2459
- }
2453
+ writeLine(streams.stdout, 'One optional package is not installed:');
2454
+ writeLine(streams.stdout, ` ${slackPackage}`);
2455
+ writeLine(streams.stdout, ` Slack tokens are stored for ${slackAgents} agent(s), but nothing connects to Slack without it.`);
2456
+ const answer = await prompter.select('Install now with npm install -g?', ['Install it now', 'Skip']);
2457
+ if (answer.kind !== 'index' || answer.index !== 0) {
2458
+ writeLine(streams.stdout, `Skipped. Install it yourself with: npm install -g ${slackPackage}`);
2459
+ return;
2460
2460
  }
2461
- if (declined.length > 0) {
2462
- const rest = declined.flatMap((group) => group.packages).join(' ');
2463
- writeLine(streams.stdout, chosen.length > 0
2464
- ? `Still missing. Install with: npm install -g ${rest}`
2465
- : `Skipped. Install them yourself with: npm install -g ${rest}`);
2461
+ writeLine(streams.stdout, `Running: npm install -g ${slackPackage}`);
2462
+ // Never fails setup — not even its exit code, unlike the always-on
2463
+ // service step below: the config and credentials are already written,
2464
+ // and a package that did not install is a warning at the next start,
2465
+ // not a broken machine.
2466
+ const result = await (env.packageInstaller ?? defaultPackageInstaller)([slackPackage])
2467
+ .catch((error) => ({
2468
+ ok: false,
2469
+ message: error instanceof Error ? error.message : String(error),
2470
+ }));
2471
+ if (result.ok) {
2472
+ writeLine(streams.stdout, `Installed ${slackPackage}.`);
2473
+ }
2474
+ else {
2475
+ writeLine(streams.stderr, `Could not install: ${result.message}`);
2476
+ writeLine(streams.stderr, `Setup is saved either way — run \`npm install -g ${slackPackage}\` yourself.`);
2466
2477
  }
2467
2478
  };
2468
2479
  /**
2469
2480
  * Write everything the menu decided, then report whether the always-on
2470
2481
  * service step it performed — installing the service, or removing one the
2471
- * user chose not to run — and the install of the provider harness the
2482
+ * user chose not to run — and the install of the provider package the
2472
2483
  * config runs on both succeeded.
2473
2484
  *
2474
2485
  * A boolean rather than a throw: the service is the one optional part of
@@ -2620,14 +2631,15 @@ export const runSetup = async (command, streams, env = {}) => {
2620
2631
  // already present, instead of warning about them in a log nobody reads
2621
2632
  // until the dashboard fails.
2622
2633
  // The provider package comes first, and without a question: choosing
2623
- // Claude by subscription or Codex was the decision, and the CLI no
2624
- // longer carries either harness, so a daemon started without it fails
2625
- // every turn. Through the saved files, so a soul's own pin counts.
2634
+ // the provider was the decision, and the CLI carries none of the
2635
+ // packages (the Anthropic adapter, the claude-code and codex harnesses),
2636
+ // so a daemon started without it fails every turn. Through the saved
2637
+ // files, so a soul's own pin counts.
2626
2638
  // A failed install is carried like a failed service step: everything is
2627
2639
  // saved, but the exit code and the closing line must not call a config
2628
2640
  // whose every turn fails ready.
2629
- const harnesses = await missingHarnessPackages(env, configPath, { asService: true });
2630
- const harnessInstallFailed = !(await installHarnessPackages(harnesses, streams, env));
2641
+ const providerPackages = await missingProviderPackages(env, configPath, { asService: true });
2642
+ const providerInstallFailed = !(await installProviderPackages(providerPackages, streams, env));
2631
2643
  await offerOptionalPackages();
2632
2644
  // Last, so the daemon starts against the config and credentials that
2633
2645
  // were just written rather than the ones it would have found a moment
@@ -2730,8 +2742,8 @@ export const runSetup = async (command, streams, env = {}) => {
2730
2742
  ...(flagged.has('STRATUS_MODEL') ? { model: String(state.model ?? defaultModelFor(state.provider)) } : {}),
2731
2743
  ...(flagged.has('STRATUS_BASE_URL') ? { baseUrl: state.baseUrl ?? DEFAULT_OPENAI_BASE_URL } : {}),
2732
2744
  };
2733
- const readBackFailure = harnessInstallFailed
2734
- ? harnessesMissingMessage(harnesses)
2745
+ const readBackFailure = providerInstallFailed
2746
+ ? providerPackagesMissingMessage(providerPackages)
2735
2747
  : await resolveRuntimeConfig(readBackSelection, env).then(() => undefined, (error) => (error instanceof Error ? error.message : String(error)));
2736
2748
  const failedReadBack = () => {
2737
2749
  if (readBackFailure === undefined) {
@@ -2771,10 +2783,9 @@ export const runSetup = async (command, streams, env = {}) => {
2771
2783
  }
2772
2784
  }
2773
2785
  writeLine(streams.stdout, ` stratus run${extraFlags}${runConfigFlag}${shadowConfigFlag} "say hello"`);
2774
- if (dashboardReady) {
2775
- writeLine(streams.stdout, ' stratus dashboard');
2776
- }
2777
- return !serviceStepFailed && !harnessInstallFailed;
2786
+ // Whether or not its packages are here: the command offers them itself.
2787
+ writeLine(streams.stdout, ' stratus dashboard');
2788
+ return !serviceStepFailed && !providerInstallFailed;
2778
2789
  };
2779
2790
  try {
2780
2791
  if (!interactive) {