@stratusagent/cli 0.11.5 → 0.11.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.
- package/README.md +8 -7
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +30 -1
- package/dist/approvals.js.map +1 -1
- package/dist/commands/channel.d.ts +17 -0
- package/dist/commands/channel.d.ts.map +1 -0
- package/dist/commands/channel.js +115 -0
- package/dist/commands/channel.js.map +1 -0
- package/dist/commands/chat.js +3 -3
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +80 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/grants.d.ts.map +1 -1
- package/dist/commands/grants.js +33 -9
- package/dist/commands/grants.js.map +1 -1
- package/dist/commands/schedules.d.ts.map +1 -1
- package/dist/commands/schedules.js +7 -3
- package/dist/commands/schedules.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +73 -7
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/service.d.ts +13 -0
- package/dist/commands/service.d.ts.map +1 -1
- package/dist/commands/service.js +36 -2
- package/dist/commands/service.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +200 -15
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skills.d.ts +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +14 -3
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/template.d.ts.map +1 -1
- package/dist/commands/template.js +4 -1
- package/dist/commands/template.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +25 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/environment.d.ts +2 -0
- package/dist/environment.d.ts.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +20 -0
- package/dist/events.js.map +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +31 -17
- package/dist/help.js.map +1 -1
- package/dist/loaders.d.ts +11 -0
- package/dist/loaders.d.ts.map +1 -1
- package/dist/loaders.js +8 -0
- package/dist/loaders.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -0
- package/dist/main.js.map +1 -1
- package/dist/npm.d.ts +1 -1
- package/dist/npm.js +1 -1
- package/dist/parse.d.ts +16 -11
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +72 -10
- package/dist/parse.js.map +1 -1
- package/dist/prompter.d.ts +2 -2
- package/dist/prompter.d.ts.map +1 -1
- package/dist/prompter.js +4 -3
- package/dist/prompter.js.map +1 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +26 -4
- package/dist/runtime.js.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +2 -0
- package/dist/service.js.map +1 -1
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +9 -0
- package/dist/slack.js.map +1 -1
- package/dist/supervisor.d.ts +9 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +9 -0
- package/dist/supervisor.js.map +1 -1
- package/package.json +29 -26
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return
|
|
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;
|
|
@@ -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 {
|
|
@@ -822,27 +833,31 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
822
833
|
writeLine(streams.stdout);
|
|
823
834
|
writeLine(streams.stdout, `Create a Slack app for ${agentName}:`);
|
|
824
835
|
writeLine(streams.stdout, ' 1. Open https://api.slack.com/apps → Create New App → From a manifest');
|
|
825
|
-
writeLine(streams.stdout, ' 2. Pick your workspace, then paste this manifest:');
|
|
836
|
+
writeLine(streams.stdout, ' 2. Pick your workspace → Next, then paste this manifest:');
|
|
826
837
|
writeLine(streams.stdout);
|
|
827
838
|
for (const line of slackAppManifest(agentName).split('\n')) {
|
|
828
839
|
writeLine(streams.stdout, ` ${line}`);
|
|
829
840
|
}
|
|
830
841
|
writeLine(streams.stdout);
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
842
|
+
// The app-level token is the step people got lost on: it lives on
|
|
843
|
+
// Basic Information rather than OAuth & Permissions, where the other
|
|
844
|
+
// token is, and needs a scope added in the dialog that generates it.
|
|
845
|
+
writeLine(streams.stdout, ' 3. Click Next, then Create. You land on Basic Information — scroll down to');
|
|
846
|
+
writeLine(streams.stdout, ' App-Level Tokens and click Generate Token and Scopes (scope: connections:write)');
|
|
847
|
+
writeLine(streams.stdout, ' 4. Sidebar → Install App → Install to your workspace → Allow, then copy');
|
|
848
|
+
writeLine(streams.stdout, ' the Bot User OAuth Token — it starts with xoxb-');
|
|
834
849
|
writeLine(streams.stdout, ` 5. Basic Information → Display Information → upload ${agentName}'s avatar`);
|
|
835
850
|
writeLine(streams.stdout);
|
|
836
|
-
const appToken = await prompter.askSecret('Paste the app-level token (xapp-…, Enter to cancel; input is hidden): ');
|
|
851
|
+
const appToken = await prompter.askSecret('Paste the app-level token from step 3 (xapp-…, Enter to cancel; input is hidden): ');
|
|
837
852
|
if (!appToken) {
|
|
838
853
|
writeLine(streams.stdout, 'Cancelled — nothing was saved.');
|
|
839
854
|
return;
|
|
840
855
|
}
|
|
841
856
|
if (!appToken.startsWith('xapp-')) {
|
|
842
|
-
writeLine(streams.stdout, '✗ That does not look like an app-level token (they start with xapp-). Nothing was saved.');
|
|
857
|
+
writeLine(streams.stdout, '✗ That does not look like an app-level token (they start with xapp-, and live under Basic Information → App-Level Tokens, not OAuth & Permissions). Nothing was saved.');
|
|
843
858
|
return;
|
|
844
859
|
}
|
|
845
|
-
const botToken = await prompter.askSecret('Paste the
|
|
860
|
+
const botToken = await prompter.askSecret('Paste the Bot User OAuth Token from step 4 (xoxb-…, Enter to cancel; input is hidden): ');
|
|
846
861
|
if (!botToken) {
|
|
847
862
|
writeLine(streams.stdout, 'Cancelled — nothing was saved.');
|
|
848
863
|
return;
|
|
@@ -860,7 +875,7 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
860
875
|
}
|
|
861
876
|
const app = await verifySlackAppToken(appToken, fetchImpl);
|
|
862
877
|
if (app.status === 'rejected') {
|
|
863
|
-
writeLine(streams.stdout, `✗ Slack rejected the app-level token (${app.detail}). Nothing was saved —
|
|
878
|
+
writeLine(streams.stdout, `✗ Slack rejected the app-level token (${app.detail}). Nothing was saved — generate a new one under Basic Information → App-Level Tokens with the connections:write scope.`);
|
|
864
879
|
return;
|
|
865
880
|
}
|
|
866
881
|
const slack = { ...(state.channels.slack ?? {}) };
|
|
@@ -1162,6 +1177,7 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
1162
1177
|
// The qualified form the loader stages them under, which is what a
|
|
1163
1178
|
// `skills:` entry has to match.
|
|
1164
1179
|
skills: manifest.contributes.skills.map((skill) => `${manifest.packageName}:${skill.id}`),
|
|
1180
|
+
channels: manifest.contributes.channels.map((channel) => channel.name),
|
|
1165
1181
|
};
|
|
1166
1182
|
}
|
|
1167
1183
|
catch {
|
|
@@ -1932,7 +1948,159 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
1932
1948
|
state.approvals = { ...(state.approvals ?? {}), mode: 'remote', agents };
|
|
1933
1949
|
}
|
|
1934
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
|
+
*/
|
|
1935
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 () => {
|
|
1936
2104
|
while (true) {
|
|
1937
2105
|
const { entries: roster, loaded: rosterLoaded } = await channelRoster();
|
|
1938
2106
|
const slack = state.channels.slack ?? {};
|
|
@@ -2226,6 +2394,9 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2226
2394
|
if (state.promptCacheTtl !== undefined) {
|
|
2227
2395
|
config.promptCacheTtl = state.promptCacheTtl;
|
|
2228
2396
|
}
|
|
2397
|
+
if (state.language !== undefined) {
|
|
2398
|
+
config.language = state.language;
|
|
2399
|
+
}
|
|
2229
2400
|
// `plugins` and `approvals` have menus above; `api`, `principals`, and
|
|
2230
2401
|
// `slack` do not and are written back exactly as they were read. Both cases
|
|
2231
2402
|
// land here the same way, because the menus edit this state rather
|
|
@@ -2290,6 +2461,19 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2290
2461
|
? `Saved Slack tokens for ${connected} agent${connected === 1 ? '' : 's'} to ${credentialsPath(env)} — run \`${serveCommand()}\` to bring them online.`
|
|
2291
2462
|
: `Removed the stored Slack tokens from ${credentialsPath(env)}.`);
|
|
2292
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
|
+
}
|
|
2293
2477
|
// Before the service block below, deliberately. A package installed
|
|
2294
2478
|
// after the daemon starts is invisible to it — installing does not
|
|
2295
2479
|
// reload a running process — so offering here is what makes the
|
|
@@ -2427,12 +2611,13 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2427
2611
|
// corrupts every redraw after the first arrow key. Wrapping is not
|
|
2428
2612
|
// the cosmetic cost I took it for when I let this line grow.
|
|
2429
2613
|
const approvals = fitMenuRow((await approvalsSummary()).join('; '));
|
|
2614
|
+
const channelKinds = (await pluginChannelKinds()).map((entry) => entry.kind);
|
|
2430
2615
|
const choice = await prompter.select('', [
|
|
2431
2616
|
`Providers ${providersSummary()}`,
|
|
2432
2617
|
`Models ${modelsSummary()}`,
|
|
2433
2618
|
`Agent ${agentSummary()}`,
|
|
2434
2619
|
`Plugins ${fitMenuRow(pluginsSummary())}`,
|
|
2435
|
-
`Channels ${channelsSummary()}`,
|
|
2620
|
+
`Channels ${fitMenuRow(channelsSummary(channelKinds))}`,
|
|
2436
2621
|
`Approvals ${approvals}`,
|
|
2437
2622
|
`Always on ${serviceSummary()}`,
|
|
2438
2623
|
'Test run say hello with the current settings',
|