@stratusagent/cli 0.11.6 → 0.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +13 -9
  2. package/dist/approvals.d.ts.map +1 -1
  3. package/dist/approvals.js +30 -1
  4. package/dist/approvals.js.map +1 -1
  5. package/dist/commands/channel.d.ts +17 -0
  6. package/dist/commands/channel.d.ts.map +1 -0
  7. package/dist/commands/channel.js +115 -0
  8. package/dist/commands/channel.js.map +1 -0
  9. package/dist/commands/chat.js +3 -3
  10. package/dist/commands/chat.js.map +1 -1
  11. package/dist/commands/doctor.d.ts.map +1 -1
  12. package/dist/commands/doctor.js +50 -2
  13. package/dist/commands/doctor.js.map +1 -1
  14. package/dist/commands/grants.d.ts.map +1 -1
  15. package/dist/commands/grants.js +30 -7
  16. package/dist/commands/grants.js.map +1 -1
  17. package/dist/commands/health.d.ts +18 -0
  18. package/dist/commands/health.d.ts.map +1 -0
  19. package/dist/commands/health.js +95 -0
  20. package/dist/commands/health.js.map +1 -0
  21. package/dist/commands/serve.d.ts.map +1 -1
  22. package/dist/commands/serve.js +91 -19
  23. package/dist/commands/serve.js.map +1 -1
  24. package/dist/commands/service.d.ts +13 -0
  25. package/dist/commands/service.d.ts.map +1 -1
  26. package/dist/commands/service.js +36 -2
  27. package/dist/commands/service.js.map +1 -1
  28. package/dist/commands/setup.d.ts.map +1 -1
  29. package/dist/commands/setup.js +189 -8
  30. package/dist/commands/setup.js.map +1 -1
  31. package/dist/commands/skills.d.ts +1 -1
  32. package/dist/commands/skills.d.ts.map +1 -1
  33. package/dist/commands/skills.js +14 -3
  34. package/dist/commands/skills.js.map +1 -1
  35. package/dist/commands/update.d.ts.map +1 -1
  36. package/dist/commands/update.js +27 -3
  37. package/dist/commands/update.js.map +1 -1
  38. package/dist/daemon.d.ts +3 -1
  39. package/dist/daemon.d.ts.map +1 -1
  40. package/dist/daemon.js +4 -1
  41. package/dist/daemon.js.map +1 -1
  42. package/dist/environment.d.ts +4 -0
  43. package/dist/environment.d.ts.map +1 -1
  44. package/dist/events.d.ts.map +1 -1
  45. package/dist/events.js +20 -0
  46. package/dist/events.js.map +1 -1
  47. package/dist/help.d.ts +1 -1
  48. package/dist/help.d.ts.map +1 -1
  49. package/dist/help.js +48 -24
  50. package/dist/help.js.map +1 -1
  51. package/dist/index.d.ts +2 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/loaders.d.ts +11 -0
  56. package/dist/loaders.d.ts.map +1 -1
  57. package/dist/loaders.js +8 -0
  58. package/dist/loaders.js.map +1 -1
  59. package/dist/logs.d.ts +20 -0
  60. package/dist/logs.d.ts.map +1 -1
  61. package/dist/logs.js +37 -0
  62. package/dist/logs.js.map +1 -1
  63. package/dist/main.d.ts +1 -1
  64. package/dist/main.d.ts.map +1 -1
  65. package/dist/main.js +25 -3
  66. package/dist/main.js.map +1 -1
  67. package/dist/npm.d.ts +3 -2
  68. package/dist/npm.d.ts.map +1 -1
  69. package/dist/npm.js +5 -2
  70. package/dist/npm.js.map +1 -1
  71. package/dist/parse.d.ts +30 -11
  72. package/dist/parse.d.ts.map +1 -1
  73. package/dist/parse.js +114 -10
  74. package/dist/parse.js.map +1 -1
  75. package/dist/prompter.d.ts +2 -2
  76. package/dist/prompter.d.ts.map +1 -1
  77. package/dist/prompter.js +4 -3
  78. package/dist/prompter.js.map +1 -1
  79. package/dist/runtime.d.ts +2 -1
  80. package/dist/runtime.d.ts.map +1 -1
  81. package/dist/runtime.js +27 -4
  82. package/dist/runtime.js.map +1 -1
  83. package/dist/service.d.ts.map +1 -1
  84. package/dist/service.js +2 -0
  85. package/dist/service.js.map +1 -1
  86. package/dist/supervisor.d.ts +9 -0
  87. package/dist/supervisor.d.ts.map +1 -1
  88. package/dist/supervisor.js +16 -1
  89. package/dist/supervisor.js.map +1 -1
  90. package/package.json +28 -26
@@ -4,7 +4,7 @@ import { preflightPlugin, readPluginManifest, PluginConfigError, PluginManifestE
4
4
  import { DEFAULT_ANTHROPIC_MODEL } from '@stratusagent/provider-anthropic';
5
5
  import { DEFAULT_CODEX_MODEL } from '@stratusagent/provider-codex';
6
6
  import { formatSoul, parseSoul } from '@stratusagent/agents';
7
- import { claimSoulFile, collectAvailableModels as collectModels, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, globalConfigPath, loadChannelCredentials, loadCredentials, loadRosterSouls, loadSoulFile, isRegisteredProviderName, readNonEmptyString, registeredProviderNameOf, readProcessEnv, readWorkingDirectory, resolveAgentApprovals, DEFAULT_CONFIG_FILENAME, loadConfigFile, saveChannelCredentials, saveConfigFile, saveCredentials, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, } from '@stratusagent/state';
7
+ 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, DEFAULT_CONFIG_FILENAME, loadConfigFile, saveChannelCredentials, saveChannelTransportSecrets, removeChannelTransportSecrets, CREDENTIAL_NAME_PATTERN, saveConfigFile, saveCredentials, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, } from '@stratusagent/state';
8
8
  import { installService, readServiceStatus, servicePlatform, serviceUnitPath, uninstallService, } from "../service.js";
9
9
  import { classifyApprovalReach, unattendedReachParts } from "../approvals.js";
10
10
  import { serviceEnvFor } from "../daemon.js";
@@ -97,6 +97,7 @@ export const runSetup = async (command, streams, env = {}) => {
97
97
  ...(existing.vision !== undefined ? { vision: existing.vision } : {}),
98
98
  ...(existing.promptCache !== undefined ? { promptCache: existing.promptCache } : {}),
99
99
  ...(existing.promptCacheTtl !== undefined ? { promptCacheTtl: existing.promptCacheTtl } : {}),
100
+ ...(existing.language !== undefined ? { language: existing.language } : {}),
100
101
  ...(existing.plugins !== undefined ? { plugins: existing.plugins } : {}),
101
102
  ...(existing.approvals !== undefined ? { approvals: existing.approvals } : {}),
102
103
  ...(existing.api !== undefined ? { api: existing.api } : {}),
@@ -116,6 +117,8 @@ export const runSetup = async (command, streams, env = {}) => {
116
117
  credentialsDirty: false,
117
118
  channels: await loadChannelCredentials(env),
118
119
  channelsDirty: false,
120
+ pluginChannelSecrets: {},
121
+ pluginChannelEdits: [],
119
122
  // On by default: setup's whole promise is that you finish it and the
120
123
  // agents are working. An always-on runtime you have to remember to
121
124
  // start is not always-on, and every Slack app configured here stays
@@ -221,12 +224,12 @@ export const runSetup = async (command, streams, env = {}) => {
221
224
  }
222
225
  return state.soulPath;
223
226
  };
224
- const channelsSummary = () => {
227
+ const channelsSummary = (pluginKinds) => {
225
228
  const connected = Object.keys(state.channels.slack ?? {}).length;
226
- if (connected === 0) {
227
- return 'Slack: not connected';
228
- }
229
- return `Slack: ${connected} agent${connected === 1 ? '' : 's'} connected`;
229
+ const slack = connected === 0
230
+ ? 'Slack: not connected'
231
+ : `Slack: ${connected} agent${connected === 1 ? '' : 's'} connected`;
232
+ return pluginKinds.length === 0 ? slack : `${slack}; also ${pluginKinds.join(', ')}`;
230
233
  };
231
234
  const storeCredential = (provider, credential) => {
232
235
  state.credentials[provider] = credential;
@@ -526,7 +529,7 @@ export const runSetup = async (command, streams, env = {}) => {
526
529
  if ((providerPart === 'anthropic' || providerPart === 'openai' || providerPart === 'codex') && id) {
527
530
  return { provider: providerPart, id };
528
531
  }
529
- writeLine(streams.stdout, 'Use provider:model, e.g. anthropic:claude-opus-5 or codex:gpt-5.5.');
532
+ writeLine(streams.stdout, 'Use provider:model, e.g. anthropic:claude-opus-5-5 or codex:gpt-5.5.');
530
533
  return undefined;
531
534
  }
532
535
  // A typed id that appears in the collected list belongs to that
@@ -705,7 +708,15 @@ export const runSetup = async (command, streams, env = {}) => {
705
708
  ...(codexSubscription ? { codexSubscription: true } : {}),
706
709
  };
707
710
  };
711
+ // The language the saved config would run in, soul over config, carried
712
+ // onto whichever variant the test run builds: it is part of what the
713
+ // quick hello is there to exercise.
708
714
  const buildTestRuntime = async () => {
715
+ const runtime = await buildTestRuntimeVariant();
716
+ const language = runtime ? languageFor(runtime.soul, state) : undefined;
717
+ return runtime && language !== undefined ? { ...runtime, language } : runtime;
718
+ };
719
+ const buildTestRuntimeVariant = async () => {
709
720
  let soul;
710
721
  if (state.soulPath) {
711
722
  try {
@@ -1166,6 +1177,7 @@ export const runSetup = async (command, streams, env = {}) => {
1166
1177
  // The qualified form the loader stages them under, which is what a
1167
1178
  // `skills:` entry has to match.
1168
1179
  skills: manifest.contributes.skills.map((skill) => `${manifest.packageName}:${skill.id}`),
1180
+ channels: manifest.contributes.channels.map((channel) => channel.name),
1169
1181
  };
1170
1182
  }
1171
1183
  catch {
@@ -1936,7 +1948,159 @@ export const runSetup = async (command, streams, env = {}) => {
1936
1948
  state.approvals = { ...(state.approvals ?? {}), mode: 'remote', agents };
1937
1949
  }
1938
1950
  };
1951
+ /**
1952
+ * The channel kinds the enabled plugins declare, Slack's own aside, each
1953
+ * with the config key of every plugin declaring it — from manifests,
1954
+ * never by loading a package, as the Plugins row reads them. Every one,
1955
+ * not the first: two plugins may carry the same kind for different agents
1956
+ * (channels key on agent and kind), and an agent bound by the second one's
1957
+ * config is as bound as one bound by the first's.
1958
+ */
1959
+ const pluginChannelKinds = async () => {
1960
+ const kinds = [];
1961
+ for (const name of pluginPackages()) {
1962
+ if (!pluginEnabled(name)) {
1963
+ continue;
1964
+ }
1965
+ for (const kind of (await pluginContributions(name))?.channels ?? []) {
1966
+ if (kind === 'slack') {
1967
+ continue;
1968
+ }
1969
+ const entry = kinds.find((candidate) => candidate.kind === kind);
1970
+ if (entry === undefined) {
1971
+ kinds.push({ kind, plugins: [name] });
1972
+ }
1973
+ else if (!entry.plugins.includes(name)) {
1974
+ entry.plugins.push(name);
1975
+ }
1976
+ }
1977
+ }
1978
+ return kinds;
1979
+ };
1980
+ /**
1981
+ * Slack alone, as it always was, until a plugin declares a channel: then
1982
+ * the row asks which channel first. A setup with no channel plugin sees
1983
+ * exactly the screens it saw before there could be one.
1984
+ */
1939
1985
  const chooseChannels = async () => {
1986
+ const kinds = await pluginChannelKinds();
1987
+ if (kinds.length === 0) {
1988
+ await chooseSlackChannel();
1989
+ return;
1990
+ }
1991
+ while (true) {
1992
+ const options = [
1993
+ 'Slack'.padEnd(20) + 'built in',
1994
+ ...kinds.map(({ kind, plugins }) => kind.padEnd(20) + `from ${plugins.join(', ')}`),
1995
+ 'Back',
1996
+ ];
1997
+ const choice = await prompter.select('Channels', options);
1998
+ if (choice.kind !== 'index' || choice.index === options.length - 1) {
1999
+ return;
2000
+ }
2001
+ const picked = kinds[choice.index - 1];
2002
+ if (picked === undefined) {
2003
+ await chooseSlackChannel();
2004
+ }
2005
+ else {
2006
+ await choosePluginChannel(picked.kind, picked.plugins);
2007
+ }
2008
+ }
2009
+ };
2010
+ /**
2011
+ * One plugin channel's agents, and whether each is bound — by secrets
2012
+ * stored under `channels.<kind>.<agentId>`, or by an entry under the
2013
+ * plugin's own config `agents` block. Either is a binding: a channel that
2014
+ * needs no secret (iMessage on this Mac) is bound by config alone, so a
2015
+ * menu that knew only secrets would call it not connected.
2016
+ *
2017
+ * Setup stores secrets and never writes the plugin's config: which keys
2018
+ * that block takes is the plugin's schema, which only its README states.
2019
+ */
2020
+ const choosePluginChannel = async (kind, plugins) => {
2021
+ const secretsFor = async () => {
2022
+ state.pluginChannelSecrets[kind] ??= await loadChannelTransportSecrets(env, kind);
2023
+ return state.pluginChannelSecrets[kind];
2024
+ };
2025
+ while (true) {
2026
+ const { entries: roster, loaded: rosterLoaded } = await channelRoster();
2027
+ const stored = await secretsFor();
2028
+ // Any plugin carrying this kind binds the agents its own block lists.
2029
+ const inConfig = (id) => plugins.some((plugin) => {
2030
+ const configured = state.plugins?.[plugin]?.agents;
2031
+ return typeof configured === 'object' && configured !== null && !Array.isArray(configured) && Object.hasOwn(configured, id);
2032
+ });
2033
+ const options = roster.map((entry) => {
2034
+ const id = entry.soul.agent.id;
2035
+ const bindings = [
2036
+ ...(stored[id] !== undefined ? [`✓ secrets: ${Object.keys(stored[id]).sort().join(', ')}`] : []),
2037
+ ...(inConfig(id) ? ['✓ in its plugin config'] : []),
2038
+ ];
2039
+ return `${entry.soul.agent.name} (${id})`.padEnd(34) + (bindings.length > 0 ? bindings.join('; ') : '— not connected');
2040
+ });
2041
+ // Only against a roster that loaded, for the reason the Slack menu
2042
+ // gives: an unreadable roster would make every binding look orphaned.
2043
+ const orphans = rosterLoaded
2044
+ ? Object.keys(stored).filter((id) => !roster.some((entry) => entry.soul.agent.id === id))
2045
+ : [];
2046
+ for (const id of orphans) {
2047
+ options.push(`${id}`.padEnd(34) + '! secrets without a matching agent');
2048
+ }
2049
+ options.push('Back');
2050
+ const blocks = plugins.map((plugin) => `plugins["${plugin}"].agents`).join(' or ');
2051
+ const choice = await prompter.select(`Channels — ${kind} (from ${plugins.join(', ')})`, options, {
2052
+ footnote: `Bound by secrets stored here, or by an entry under ${blocks} in your config — the plugin's README says which. Run \`${serveCommand()}\` afterwards.`,
2053
+ });
2054
+ if (choice.kind !== 'index' || choice.index === options.length - 1) {
2055
+ return;
2056
+ }
2057
+ const agentId = choice.index < roster.length
2058
+ ? roster[choice.index]?.soul.agent.id
2059
+ : orphans[choice.index - roster.length];
2060
+ if (agentId === undefined) {
2061
+ return;
2062
+ }
2063
+ const actions = [
2064
+ ...(choice.index < roster.length ? [stored[agentId] !== undefined ? 'Replace the stored secrets' : 'Store secrets'] : []),
2065
+ ...(stored[agentId] !== undefined ? ['Remove the stored secrets'] : []),
2066
+ 'Back',
2067
+ ];
2068
+ const action = await prompter.select(`${agentId} on ${kind}`, actions);
2069
+ if (action.kind !== 'index' || actions[action.index] === 'Back') {
2070
+ continue;
2071
+ }
2072
+ if (actions[action.index] === 'Remove the stored secrets') {
2073
+ delete stored[agentId];
2074
+ state.pluginChannelEdits.push({ kind, agentId });
2075
+ writeLine(streams.stdout, `Removed ${agentId}'s ${kind} secrets.`);
2076
+ continue;
2077
+ }
2078
+ const names = (await prompter.ask(`Secret names for ${kind}, separated by spaces (its README lists them): `))
2079
+ .split(/\s+/)
2080
+ .filter((name) => name.length > 0);
2081
+ if (names.length === 0) {
2082
+ continue;
2083
+ }
2084
+ const bad = names.find((name, index) => !CREDENTIAL_NAME_PATTERN.test(name) || names.indexOf(name) !== index);
2085
+ if (bad !== undefined) {
2086
+ writeLine(streams.stderr, `${bad} is not a secret name, or is named twice. Nothing was stored.`);
2087
+ continue;
2088
+ }
2089
+ const secrets = {};
2090
+ for (const name of names) {
2091
+ secrets[name] = await prompter.askSecret(`${name} (not echoed): `);
2092
+ }
2093
+ const empty = names.find((name) => (secrets[name] ?? '').length === 0);
2094
+ if (empty !== undefined) {
2095
+ writeLine(streams.stderr, `${empty} was empty, so nothing was stored. A binding missing a secret would never come online.`);
2096
+ continue;
2097
+ }
2098
+ stored[agentId] = secrets;
2099
+ state.pluginChannelEdits.push({ kind, agentId, secrets });
2100
+ writeLine(streams.stdout, `Stored ${names.join(', ')} for ${agentId} on ${kind}; saved when you finish.`);
2101
+ }
2102
+ };
2103
+ const chooseSlackChannel = async () => {
1940
2104
  while (true) {
1941
2105
  const { entries: roster, loaded: rosterLoaded } = await channelRoster();
1942
2106
  const slack = state.channels.slack ?? {};
@@ -2230,6 +2394,9 @@ export const runSetup = async (command, streams, env = {}) => {
2230
2394
  if (state.promptCacheTtl !== undefined) {
2231
2395
  config.promptCacheTtl = state.promptCacheTtl;
2232
2396
  }
2397
+ if (state.language !== undefined) {
2398
+ config.language = state.language;
2399
+ }
2233
2400
  // `plugins` and `approvals` have menus above; `api`, `principals`, and
2234
2401
  // `slack` do not and are written back exactly as they were read. Both cases
2235
2402
  // land here the same way, because the menus edit this state rather
@@ -2294,6 +2461,19 @@ export const runSetup = async (command, streams, env = {}) => {
2294
2461
  ? `Saved Slack tokens for ${connected} agent${connected === 1 ? '' : 's'} to ${credentialsPath(env)} — run \`${serveCommand()}\` to bring them online.`
2295
2462
  : `Removed the stored Slack tokens from ${credentialsPath(env)}.`);
2296
2463
  }
2464
+ if (state.pluginChannelEdits.length > 0) {
2465
+ // In the order they were made, so a store then a remove of the same
2466
+ // binding ends removed, as the menu last showed it.
2467
+ for (const edit of state.pluginChannelEdits) {
2468
+ if (edit.secrets !== undefined) {
2469
+ await saveChannelTransportSecrets(env, edit.kind, edit.agentId, edit.secrets);
2470
+ }
2471
+ else {
2472
+ await removeChannelTransportSecrets(env, edit.kind, edit.agentId);
2473
+ }
2474
+ }
2475
+ writeLine(streams.stdout, `Saved your channel secrets to ${credentialsPath(env)} — run \`${serveCommand()}\` to bring them online.`);
2476
+ }
2297
2477
  // Before the service block below, deliberately. A package installed
2298
2478
  // after the daemon starts is invisible to it — installing does not
2299
2479
  // reload a running process — so offering here is what makes the
@@ -2431,12 +2611,13 @@ export const runSetup = async (command, streams, env = {}) => {
2431
2611
  // corrupts every redraw after the first arrow key. Wrapping is not
2432
2612
  // the cosmetic cost I took it for when I let this line grow.
2433
2613
  const approvals = fitMenuRow((await approvalsSummary()).join('; '));
2614
+ const channelKinds = (await pluginChannelKinds()).map((entry) => entry.kind);
2434
2615
  const choice = await prompter.select('', [
2435
2616
  `Providers ${providersSummary()}`,
2436
2617
  `Models ${modelsSummary()}`,
2437
2618
  `Agent ${agentSummary()}`,
2438
2619
  `Plugins ${fitMenuRow(pluginsSummary())}`,
2439
- `Channels ${channelsSummary()}`,
2620
+ `Channels ${fitMenuRow(channelsSummary(channelKinds))}`,
2440
2621
  `Approvals ${approvals}`,
2441
2622
  `Always on ${serviceSummary()}`,
2442
2623
  'Test run say hello with the current settings',