@stratusagent/cli 0.13.5 → 0.13.7

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 (99) hide show
  1. package/README.md +4 -2
  2. package/dist/commands/agent-new.d.ts.map +1 -1
  3. package/dist/commands/agent-new.js +3 -11
  4. package/dist/commands/agent-new.js.map +1 -1
  5. package/dist/commands/channel.d.ts.map +1 -1
  6. package/dist/commands/channel.js +5 -0
  7. package/dist/commands/channel.js.map +1 -1
  8. package/dist/commands/dashboard.d.ts.map +1 -1
  9. package/dist/commands/dashboard.js +157 -7
  10. package/dist/commands/dashboard.js.map +1 -1
  11. package/dist/commands/doctor.d.ts.map +1 -1
  12. package/dist/commands/doctor.js +81 -14
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/plugins.d.ts.map +1 -1
  15. package/dist/commands/plugins.js +2 -2
  16. package/dist/commands/plugins.js.map +1 -1
  17. package/dist/commands/serve.d.ts +8 -0
  18. package/dist/commands/serve.d.ts.map +1 -1
  19. package/dist/commands/serve.js +12 -5
  20. package/dist/commands/serve.js.map +1 -1
  21. package/dist/commands/service.d.ts.map +1 -1
  22. package/dist/commands/service.js +8 -2
  23. package/dist/commands/service.js.map +1 -1
  24. package/dist/commands/sessions.d.ts +17 -0
  25. package/dist/commands/sessions.d.ts.map +1 -0
  26. package/dist/commands/sessions.js +68 -0
  27. package/dist/commands/sessions.js.map +1 -0
  28. package/dist/commands/setup.d.ts.map +1 -1
  29. package/dist/commands/setup.js +136 -127
  30. package/dist/commands/setup.js.map +1 -1
  31. package/dist/commands/signin.d.ts.map +1 -1
  32. package/dist/commands/signin.js +32 -3
  33. package/dist/commands/signin.js.map +1 -1
  34. package/dist/commands/update.d.ts.map +1 -1
  35. package/dist/commands/update.js +92 -25
  36. package/dist/commands/update.js.map +1 -1
  37. package/dist/companions.d.ts.map +1 -1
  38. package/dist/companions.js +2 -2
  39. package/dist/companions.js.map +1 -1
  40. package/dist/daemon.d.ts.map +1 -1
  41. package/dist/daemon.js +1 -0
  42. package/dist/daemon.js.map +1 -1
  43. package/dist/environment.d.ts +7 -0
  44. package/dist/environment.d.ts.map +1 -1
  45. package/dist/help.d.ts +1 -1
  46. package/dist/help.d.ts.map +1 -1
  47. package/dist/help.js +22 -10
  48. package/dist/help.js.map +1 -1
  49. package/dist/index.d.ts +2 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/main.d.ts.map +1 -1
  54. package/dist/main.js +18 -7
  55. package/dist/main.js.map +1 -1
  56. package/dist/npm.d.ts +17 -1
  57. package/dist/npm.d.ts.map +1 -1
  58. package/dist/npm.js +21 -6
  59. package/dist/npm.js.map +1 -1
  60. package/dist/parse.d.ts +18 -2
  61. package/dist/parse.d.ts.map +1 -1
  62. package/dist/parse.js +84 -2
  63. package/dist/parse.js.map +1 -1
  64. package/dist/plugin-catalog.d.ts +0 -9
  65. package/dist/plugin-catalog.d.ts.map +1 -1
  66. package/dist/plugin-catalog.js +3 -14
  67. package/dist/plugin-catalog.js.map +1 -1
  68. package/dist/plugin-index.d.ts +136 -0
  69. package/dist/plugin-index.d.ts.map +1 -0
  70. package/dist/plugin-index.js +293 -0
  71. package/dist/plugin-index.js.map +1 -0
  72. package/dist/plugin-updates.d.ts +54 -0
  73. package/dist/plugin-updates.d.ts.map +1 -0
  74. package/dist/plugin-updates.js +68 -0
  75. package/dist/plugin-updates.js.map +1 -0
  76. package/dist/promotion.d.ts.map +1 -1
  77. package/dist/promotion.js +4 -3
  78. package/dist/promotion.js.map +1 -1
  79. package/dist/provider-packages.d.ts +34 -0
  80. package/dist/provider-packages.d.ts.map +1 -0
  81. package/dist/{harness-packages.js → provider-packages.js} +19 -16
  82. package/dist/provider-packages.js.map +1 -0
  83. package/dist/roster.d.ts.map +1 -1
  84. package/dist/roster.js +19 -2
  85. package/dist/roster.js.map +1 -1
  86. package/dist/runtime.d.ts.map +1 -1
  87. package/dist/runtime.js +5 -0
  88. package/dist/runtime.js.map +1 -1
  89. package/dist/service.d.ts +11 -0
  90. package/dist/service.d.ts.map +1 -1
  91. package/dist/service.js +129 -37
  92. package/dist/service.js.map +1 -1
  93. package/dist/slack.d.ts.map +1 -1
  94. package/dist/slack.js +4 -0
  95. package/dist/slack.js.map +1 -1
  96. package/package.json +22 -25
  97. package/dist/harness-packages.d.ts +0 -31
  98. package/dist/harness-packages.d.ts.map +0 -1
  99. package/dist/harness-packages.js.map +0 -1
@@ -0,0 +1,68 @@
1
+ import { escapeControlCharacters } from '@stratusagent/core';
2
+ import { callRunningGateway, gatewayErrorMessage, noRunningDaemonMessage, runningGatewayBase, } from "../daemon.js";
3
+ import { writeLine } from "../io.js";
4
+ /** How many sessions a listing shows when no --limit is given. */
5
+ export const DEFAULT_SESSIONS_LIMIT = 20;
6
+ /**
7
+ * `stratus sessions` — what the running daemon's sessions are doing, newest
8
+ * first: running, waiting on an approval, failed, or done.
9
+ *
10
+ * A thin read of `GET /api/v1/sessions`, which filters by agent and cuts
11
+ * to a limit but knows nothing of status. So a status filter reads every
12
+ * row and filters here: cutting first would answer "no failed sessions"
13
+ * whenever the newest few happened to be running. Without one, the route
14
+ * does the cutting, asked for one row more than is shown so the listing
15
+ * can say there are more.
16
+ */
17
+ export const runSessions = async (command, streams, env = {}) => {
18
+ const base = await runningGatewayBase(env, command);
19
+ if (!base) {
20
+ writeLine(streams.stderr, `Error: ${noRunningDaemonMessage(env)}`);
21
+ return 1;
22
+ }
23
+ const limit = command.limit ?? DEFAULT_SESSIONS_LIMIT;
24
+ const query = new URLSearchParams();
25
+ if (command.agentId !== undefined) {
26
+ query.set('agent', command.agentId);
27
+ }
28
+ if (command.status === undefined) {
29
+ query.set('limit', String(limit + 1));
30
+ }
31
+ const search = query.toString();
32
+ let rows;
33
+ try {
34
+ const response = await callRunningGateway(env, command, base, `/api/v1/sessions${search ? `?${search}` : ''}`, undefined, 'GET');
35
+ if (!response.ok) {
36
+ writeLine(streams.stderr, `Error: ${await gatewayErrorMessage(response)}`);
37
+ return 1;
38
+ }
39
+ rows = (await response.json()).sessions ?? [];
40
+ }
41
+ catch (error) {
42
+ writeLine(streams.stderr, `Error: ${error instanceof Error ? error.message : String(error)}`);
43
+ return 1;
44
+ }
45
+ const matching = command.status === undefined ? rows : rows.filter((row) => row.status === command.status);
46
+ const shown = matching.slice(0, limit);
47
+ const more = matching.length > limit;
48
+ if (command.format === 'json') {
49
+ writeLine(streams.stdout, JSON.stringify({ sessions: shown, more }));
50
+ return 0;
51
+ }
52
+ if (shown.length === 0) {
53
+ writeLine(streams.stdout, command.status === undefined ? 'No sessions.' : `No ${command.status} sessions.`);
54
+ return 0;
55
+ }
56
+ // Escaped like `stratus memory list`: an id is whatever a channel or an
57
+ // API caller named the session, and this goes to a terminal.
58
+ const lines = shown.map((row) => [row.status, row.agentId, row.updatedAt ?? '-', row.id].map(escapeControlCharacters));
59
+ const widths = [0, 1, 2].map((column) => Math.max(...lines.map((cells) => cells[column]?.length ?? 0)));
60
+ for (const cells of lines) {
61
+ writeLine(streams.stdout, cells.map((cell, column) => (column < 3 ? cell.padEnd(widths[column] ?? 0) : cell)).join(' '));
62
+ }
63
+ if (more) {
64
+ writeLine(streams.stdout, `Showing the newest ${limit}. --limit <n> shows more.`);
65
+ }
66
+ return 0;
67
+ };
68
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAwEA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAiB,MAAM,mBAAmB,CAAC;AAInF,OAAO,KAAK,EAAmB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAYvE,eAAO,MAAM,oBAAoB,QAKrB,CAAC;AAkFb,eAAO,MAAM,QAAQ,GACnB,SAAS,kBAAkB,EAC3B,SAAS,UAAU,EACnB,MAAK,cAAmB,KACvB,OAAO,CAAC,MAAM,CA++FhB,CAAC"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAsEA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAiB,MAAM,mBAAmB,CAAC;AAInF,OAAO,KAAK,EAAmB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAavE,eAAO,MAAM,oBAAoB,QAKrB,CAAC;AAkFb,eAAO,MAAM,QAAQ,GACnB,SAAS,kBAAkB,EAC3B,SAAS,UAAU,EACnB,MAAK,cAAmB,KACvB,OAAO,CAAC,MAAM,CAo+FhB,CAAC"}
@@ -1,18 +1,17 @@
1
- import { harnessesMissingMessage, installHarnessPackages, missingHarnessPackages } from "../harness-packages.js";
1
+ import { providerPackagesMissingMessage, installProviderPackages, missingProviderPackages } 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}.`);
@@ -2359,116 +2424,53 @@ export const runSetup = async (command, streams, env = {}) => {
2359
2424
  };
2360
2425
  };
2361
2426
  /**
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.
2427
+ * The Slack channel package, when this setup has Slack tokens stored and
2428
+ * it is not installed. Setup knows that before the daemon does, so it is
2429
+ * the one place that can offer the install rather than leave the gap to
2430
+ * be found in a log after the fact.
2431
+ *
2432
+ * The dashboard is deliberately not offered here: its control API binds a
2433
+ * port, and the time to ask about that is when someone runs
2434
+ * `stratus dashboard`, which offers its own packages (card 308).
2435
+ * Declining is a real answer and prints the command.
2400
2436
  */
2401
2437
  const offerOptionalPackages = async () => {
2402
- const groups = missingPackageGroups();
2403
- if (groups.length === 0) {
2438
+ const slackPackage = '@stratusagent/channel-slack';
2439
+ const slackAgents = Object.keys(state.channels.slack ?? {}).length;
2440
+ if (slackAgents === 0 || packageInstalled(slackPackage, env)) {
2404
2441
  return;
2405
2442
  }
2406
2443
  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
- }
2444
+ writeLine(streams.stdout, 'One optional package is not installed:');
2445
+ writeLine(streams.stdout, ` ${slackPackage}`);
2446
+ writeLine(streams.stdout, ` Slack tokens are stored for ${slackAgents} agent(s), but nothing connects to Slack without it.`);
2447
+ const answer = await prompter.select('Install now with npm install -g?', ['Install it now', 'Skip']);
2448
+ if (answer.kind !== 'index' || answer.index !== 0) {
2449
+ writeLine(streams.stdout, `Skipped. Install it yourself with: npm install -g ${slackPackage}`);
2450
+ return;
2451
+ }
2452
+ writeLine(streams.stdout, `Running: npm install -g ${slackPackage}`);
2453
+ // Never fails setup — not even its exit code, unlike the always-on
2454
+ // service step below: the config and credentials are already written,
2455
+ // and a package that did not install is a warning at the next start,
2456
+ // not a broken machine.
2457
+ const result = await (env.packageInstaller ?? defaultPackageInstaller)([slackPackage])
2458
+ .catch((error) => ({
2459
+ ok: false,
2460
+ message: error instanceof Error ? error.message : String(error),
2461
+ }));
2462
+ if (result.ok) {
2463
+ writeLine(streams.stdout, `Installed ${slackPackage}.`);
2460
2464
  }
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}`);
2465
+ else {
2466
+ writeLine(streams.stderr, `Could not install: ${result.message}`);
2467
+ writeLine(streams.stderr, `Setup is saved either way — run \`npm install -g ${slackPackage}\` yourself.`);
2466
2468
  }
2467
2469
  };
2468
2470
  /**
2469
2471
  * Write everything the menu decided, then report whether the always-on
2470
2472
  * 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
2473
+ * user chose not to run — and the install of the provider package the
2472
2474
  * config runs on both succeeded.
2473
2475
  *
2474
2476
  * A boolean rather than a throw: the service is the one optional part of
@@ -2620,14 +2622,15 @@ export const runSetup = async (command, streams, env = {}) => {
2620
2622
  // already present, instead of warning about them in a log nobody reads
2621
2623
  // until the dashboard fails.
2622
2624
  // 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.
2625
+ // the provider was the decision, and the CLI carries none of the
2626
+ // packages (the Anthropic adapter, the claude-code and codex harnesses),
2627
+ // so a daemon started without it fails every turn. Through the saved
2628
+ // files, so a soul's own pin counts.
2626
2629
  // A failed install is carried like a failed service step: everything is
2627
2630
  // saved, but the exit code and the closing line must not call a config
2628
2631
  // whose every turn fails ready.
2629
- const harnesses = await missingHarnessPackages(env, configPath, { asService: true });
2630
- const harnessInstallFailed = !(await installHarnessPackages(harnesses, streams, env));
2632
+ const providerPackages = await missingProviderPackages(env, configPath, { asService: true });
2633
+ const providerInstallFailed = !(await installProviderPackages(providerPackages, streams, env));
2631
2634
  await offerOptionalPackages();
2632
2635
  // Last, so the daemon starts against the config and credentials that
2633
2636
  // were just written rather than the ones it would have found a moment
@@ -2676,13 +2679,20 @@ export const runSetup = async (command, streams, env = {}) => {
2676
2679
  ok: false,
2677
2680
  messages: [`Could not install the always-on service: ${error instanceof Error ? error.message : String(error)}`],
2678
2681
  }));
2682
+ // Installed but not running is a failed step too: the always-on
2683
+ // service is the part of setup that is meant to be answering.
2684
+ const running = !('running' in result) || result.running === true;
2679
2685
  for (const message of result.messages) {
2680
- writeLine(result.ok ? streams.stdout : streams.stderr, message);
2686
+ writeLine(result.ok && running ? streams.stdout : streams.stderr, message);
2681
2687
  }
2682
2688
  if (!result.ok) {
2683
2689
  writeLine(streams.stderr, `Setup is saved either way — start the daemon yourself with \`${serveCommand()}\`.`);
2684
2690
  serviceStepFailed = true;
2685
2691
  }
2692
+ else if (!running) {
2693
+ writeLine(streams.stderr, 'Setup is saved either way. Check `stratus service status` once you have looked at the logs.');
2694
+ serviceStepFailed = true;
2695
+ }
2686
2696
  }
2687
2697
  writeLine(streams.stdout);
2688
2698
  // Exported STRATUS_* variables outrank the config file, so warn when one
@@ -2723,8 +2733,8 @@ export const runSetup = async (command, streams, env = {}) => {
2723
2733
  ...(flagged.has('STRATUS_MODEL') ? { model: String(state.model ?? defaultModelFor(state.provider)) } : {}),
2724
2734
  ...(flagged.has('STRATUS_BASE_URL') ? { baseUrl: state.baseUrl ?? DEFAULT_OPENAI_BASE_URL } : {}),
2725
2735
  };
2726
- const readBackFailure = harnessInstallFailed
2727
- ? harnessesMissingMessage(harnesses)
2736
+ const readBackFailure = providerInstallFailed
2737
+ ? providerPackagesMissingMessage(providerPackages)
2728
2738
  : await resolveRuntimeConfig(readBackSelection, env).then(() => undefined, (error) => (error instanceof Error ? error.message : String(error)));
2729
2739
  const failedReadBack = () => {
2730
2740
  if (readBackFailure === undefined) {
@@ -2764,10 +2774,9 @@ export const runSetup = async (command, streams, env = {}) => {
2764
2774
  }
2765
2775
  }
2766
2776
  writeLine(streams.stdout, ` stratus run${extraFlags}${runConfigFlag}${shadowConfigFlag} "say hello"`);
2767
- if (dashboardReady) {
2768
- writeLine(streams.stdout, ' stratus dashboard');
2769
- }
2770
- return !serviceStepFailed && !harnessInstallFailed;
2777
+ // Whether or not its packages are here: the command offers them itself.
2778
+ writeLine(streams.stdout, ' stratus dashboard');
2779
+ return !serviceStepFailed && !providerInstallFailed;
2771
2780
  };
2772
2781
  try {
2773
2782
  if (!interactive) {