@stratusagent/cli 0.9.1 → 0.10.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.
- package/README.md +78 -990
- package/dist/bin.js +4 -1
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +58 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +288 -63
- package/dist/index.js.map +1 -1
- package/package.json +22 -21
package/dist/index.js
CHANGED
|
@@ -17,8 +17,9 @@ import { createLocalCommandExecutor, } from '@stratusagent/executor-local';
|
|
|
17
17
|
import { loadPlugins } from '@stratusagent/plugins';
|
|
18
18
|
import { createFileCommandWhitelist, createPermissionPolicy, describeCommandScope, } from '@stratusagent/permissions';
|
|
19
19
|
import { DEFAULT_ANTHROPIC_MODEL, redactAnthropicRawTurns, } from '@stratusagent/provider-anthropic';
|
|
20
|
+
import { DEFAULT_CODEX_MODEL } from '@stratusagent/provider-codex';
|
|
20
21
|
import { canonicalDestination, createForgetTool, createRecallTool, createRememberTool, defineAgent, describeCadence, describeSchedule, FORGET_TOOL_NAME, MEMORY_TOOL_NAME, formatSoul, generateAgentName, parseSoul, } from '@stratusagent/agents';
|
|
21
|
-
import { agentsDirPath, claimSoulFile, collectAvailableModels as collectModels, createDemoTool, createFileMemoryStore, createRuntimeProvider, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, discoverActiveConfig, globalConfigPath, loadChannelCredentials, loadCredentials, installSkillsFromDirectory, loadOperatorSkills, loadRosterSouls, skillsDirPath, listAgentSummaries, loadSoulFile, memoryFilePath, migrateLegacyMemory, parseProviderName, readNonEmptyString, readProcessEnv, resolveConfiguredSoul, logsDirPath, readWorkingDirectory, resolveAgentApprovals, DEFAULT_CONFIG_FILENAME, loadConfigFile, resolveConfigLocation, resolveRuntimeConfig as resolveStateRuntimeConfig, saveChannelCredentials, saveConfigFile, readTrustedConfigBlock, saveCredentials, servedRuntimes, verifyProviderKey, withLegacyDefaultMemories, workspacesDirPath, gatewayInfoPath, gatewayTokenPath, newerStateMessage, pendingStateMigrations, readStateStamp, runStateMigrations, STATE_SCHEMA_VERSION, } from '@stratusagent/state';
|
|
22
|
+
import { agentsDirPath, claimSoulFile, collectAvailableModels as collectModels, createDemoTool, createFileMemoryStore, createRuntimeProvider, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, discoverActiveConfig, globalConfigPath, loadChannelCredentials, loadCredentials, installSkillsFromDirectory, loadOperatorSkills, loadRosterSouls, skillsDirPath, listAgentSummaries, loadSoulFile, memoryFilePath, migrateLegacyMemory, parseProviderName, readNonEmptyString, readProcessEnv, resolveConfiguredSoul, logsDirPath, readWorkingDirectory, resolveAgentApprovals, DEFAULT_CONFIG_FILENAME, loadConfigFile, resolveConfigLocation, resolveRuntimeConfig as resolveStateRuntimeConfig, saveChannelCredentials, saveConfigFile, readTrustedConfigBlock, saveCredentials, servedRuntimes, CREDENTIAL_PROVIDER_NAMES, verifyProviderKey, withLegacyDefaultMemories, workspacesDirPath, gatewayInfoPath, gatewayTokenPath, newerStateMessage, pendingStateMigrations, readStateStamp, runStateMigrations, STATE_SCHEMA_VERSION, } from '@stratusagent/state';
|
|
22
23
|
import { installService, readServiceCommand, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, uninstallService, } from "./service.js";
|
|
23
24
|
export { installService, launchdPlist, readServiceCommand, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, systemdUnit, uninstallService, SERVICE_LABEL, } from "./service.js";
|
|
24
25
|
import { createLogWriter, currentLogPosition, truncateRedirectLogs, formatLogRecord, readRecentRecords, tailLog, } from "./logs.js";
|
|
@@ -122,7 +123,7 @@ const defaultPackageInstaller = async (packages) => {
|
|
|
122
123
|
});
|
|
123
124
|
});
|
|
124
125
|
};
|
|
125
|
-
export const CLI_VERSION = '0.
|
|
126
|
+
export const CLI_VERSION = '0.10.0';
|
|
126
127
|
const DASHBOARD_TITLE = 'Stratus Agent Dashboard';
|
|
127
128
|
/**
|
|
128
129
|
* The range every package declares in its `engines` field. Two floors, not
|
|
@@ -161,6 +162,70 @@ export const unsupportedNodeMessage = (version) => {
|
|
|
161
162
|
+ "The gateway's session store uses node:sqlite, unflagged in 22.13.0 and, on the 23.x line, not until 23.4.0.\n"
|
|
162
163
|
+ 'Upgrade with `brew install node` on macOS, or your package manager or nvm on Linux.';
|
|
163
164
|
};
|
|
165
|
+
/**
|
|
166
|
+
* The warning listeners to install in place of Node's default one, so the
|
|
167
|
+
* `node:sqlite` ExperimentalWarning stops being the first thing the CLI
|
|
168
|
+
* prints.
|
|
169
|
+
*
|
|
170
|
+
* The session store imports `node:sqlite`, which is experimental on every
|
|
171
|
+
* Node release this supports — that is exactly why
|
|
172
|
+
* {@link SUPPORTED_NODE_RANGE} is written the way it is — so the warning
|
|
173
|
+
* announces a dependency the project chose deliberately and a user can do
|
|
174
|
+
* nothing about. It landed above `stratusd ready` on every `stratus serve`,
|
|
175
|
+
* and on `stratus schedules`.
|
|
176
|
+
*
|
|
177
|
+
* Filtered rather than silenced wholesale (`--no-warnings` would be the
|
|
178
|
+
* blunt version): every other warning still reaches stderr through the
|
|
179
|
+
* listeners passed in, because a deprecation in our own dependencies is
|
|
180
|
+
* ours to act on.
|
|
181
|
+
*/
|
|
182
|
+
export const withoutSqliteExperimentalWarning = (listeners) => (warning) => {
|
|
183
|
+
if (warning.name === 'ExperimentalWarning' && /\bSQLite\b/i.test(warning.message)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const listener of listeners) {
|
|
187
|
+
listener(warning);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* The name Node gives its own `warning` printer. Matched rather than assumed
|
|
192
|
+
* so that a Node which renames it simply stops being filtered — the warning
|
|
193
|
+
* comes back, which is noise, where guessing wrong would rewrite somebody
|
|
194
|
+
* else's listener.
|
|
195
|
+
*/
|
|
196
|
+
const NODE_DEFAULT_WARNING_LISTENER = 'onWarning';
|
|
197
|
+
/**
|
|
198
|
+
* Swap Node's default warning printer for the filtered one above.
|
|
199
|
+
*
|
|
200
|
+
* Node installs its printer as an ordinary `warning` listener, so adding one
|
|
201
|
+
* alongside it would print twice rather than filter; the default has to come
|
|
202
|
+
* off and be handed to the replacement.
|
|
203
|
+
*
|
|
204
|
+
* **Only ever Node's own printer.** Wrapping a listener somebody else
|
|
205
|
+
* registered changes it in two ways that are not ours to change: `listeners()`
|
|
206
|
+
* hands back the *unwrapped* function for a `once` listener, so re-registering
|
|
207
|
+
* it makes it permanent — it then runs for every later warning instead of one
|
|
208
|
+
* — and a caller that keeps a reference can no longer `process.off('warning',
|
|
209
|
+
* theirs)`, because what is registered is this wrapper. So when anything other
|
|
210
|
+
* than the default is present — a `--require` preload, an embedding host — this
|
|
211
|
+
* does nothing at all and the warning stays. A host that took an interest in
|
|
212
|
+
* warnings owns them.
|
|
213
|
+
*
|
|
214
|
+
* Called once from the binary, before anything imports the session store.
|
|
215
|
+
*/
|
|
216
|
+
export const filterSqliteExperimentalWarning = (process_ = process) => {
|
|
217
|
+
// rawListeners, not listeners: the wrapper is the registration, and only it
|
|
218
|
+
// carries the once-ness. It matters even here, where the guard below means
|
|
219
|
+
// the sole listener is Node's own — the next reader should not have to know
|
|
220
|
+
// that to see why this is safe.
|
|
221
|
+
const existing = process_.rawListeners('warning');
|
|
222
|
+
const [only] = existing;
|
|
223
|
+
if (existing.length !== 1 || only?.name !== NODE_DEFAULT_WARNING_LISTENER) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
process_.removeAllListeners('warning');
|
|
227
|
+
process_.on('warning', withoutSqliteExperimentalWarning([only]));
|
|
228
|
+
};
|
|
164
229
|
const HELP_TEXT = `Stratus Agent CLI
|
|
165
230
|
|
|
166
231
|
Usage:
|
|
@@ -265,8 +330,8 @@ Agent options:
|
|
|
265
330
|
Options:
|
|
266
331
|
--prompt, -p Prompt to send to the local agent loop
|
|
267
332
|
--stdin Read the prompt from stdin
|
|
268
|
-
--provider Provider to use: anthropic, openai, or demo
|
|
269
|
-
--model Model name for real providers (anthropic default: ${DEFAULT_ANTHROPIC_MODEL}, openai default: gpt-4.1-mini)
|
|
333
|
+
--provider Provider to use: anthropic, openai, codex, or demo
|
|
334
|
+
--model Model name for real providers (anthropic default: ${DEFAULT_ANTHROPIC_MODEL}, openai default: gpt-4.1-mini, codex default: ${DEFAULT_CODEX_MODEL})
|
|
270
335
|
--base-url Override the provider API base URL
|
|
271
336
|
--soul Run as the agent defined by a soul file (markdown + frontmatter, see examples/souls)
|
|
272
337
|
--config Config file path (run: load settings from it, setup: write it)
|
|
@@ -1082,30 +1147,42 @@ export const warnOnCredentialOverride = async (runtime, streams, env = {}) => {
|
|
|
1082
1147
|
if (runtime.provider === 'demo') {
|
|
1083
1148
|
return;
|
|
1084
1149
|
}
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1150
|
+
const credentials = await loadCredentials(env);
|
|
1151
|
+
const processEnv = readProcessEnv(env);
|
|
1089
1152
|
// The resolver records the variable that actually won — guessing it here
|
|
1090
1153
|
// would name the wrong one whenever a custom apiKeyEnv supplied the key,
|
|
1091
1154
|
// sending the reader to unset something that was never the cause.
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
// once the primary is already failing — the worst moment to discover it.
|
|
1095
|
-
// The fallback path consults the provider's own conventional variable.
|
|
1096
|
-
const processEnv = readProcessEnv(env);
|
|
1097
|
-
const fallbackVar = runtime.fallback?.provider === 'anthropic'
|
|
1098
|
-
&& runtime.fallback.apiKey !== undefined
|
|
1099
|
-
&& readNonEmptyString(processEnv.ANTHROPIC_API_KEY) === runtime.fallback.apiKey
|
|
1100
|
-
? 'ANTHROPIC_API_KEY'
|
|
1155
|
+
const primaryOverride = runtime.apiKey !== undefined
|
|
1156
|
+
? { provider: runtime.provider, envVar: runtime.apiKeyEnvVar }
|
|
1101
1157
|
: undefined;
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1158
|
+
// Both subscription sign-ins demote the same way: a Claude setup token
|
|
1159
|
+
// and the codex ChatGPT marker each lose to an environment API key, and
|
|
1160
|
+
// the run otherwise looks identical while billing per token.
|
|
1161
|
+
for (const target of ['anthropic', 'codex']) {
|
|
1162
|
+
if (credentials[target]?.type !== 'oauth_token') {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
const planName = target === 'anthropic'
|
|
1166
|
+
? 'Claude subscription sign-in'
|
|
1167
|
+
: 'ChatGPT (codex login) sign-in';
|
|
1168
|
+
const primary = primaryOverride?.provider === target ? primaryOverride.envVar : undefined;
|
|
1169
|
+
// A fallback demoted the same way costs exactly as much, and only bites
|
|
1170
|
+
// once the primary is already failing — the worst moment to discover it.
|
|
1171
|
+
// The fallback path consults the provider's own conventional variable.
|
|
1172
|
+
const conventionalVar = defaultApiKeyEnvName(target);
|
|
1173
|
+
const fallbackVar = runtime.fallback?.provider === target
|
|
1174
|
+
&& runtime.fallback.apiKey !== undefined
|
|
1175
|
+
&& readNonEmptyString(processEnv[conventionalVar]) === runtime.fallback.apiKey
|
|
1176
|
+
? conventionalVar
|
|
1177
|
+
: undefined;
|
|
1178
|
+
const culprit = primary ?? fallbackVar;
|
|
1179
|
+
if (!culprit) {
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
writeLine(streams.stderr, `Warning: ${culprit} in your environment outranks the ${planName} saved by \`stratus setup\`, `
|
|
1183
|
+
+ `so ${primary ? 'this run is' : 'a fallback retry would be'} billed per token instead of through your plan. `
|
|
1184
|
+
+ 'Unset it to use the subscription, or run `stratus doctor` to see everything that is being overridden.');
|
|
1105
1185
|
}
|
|
1106
|
-
writeLine(streams.stderr, `Warning: ${culprit} in your environment outranks the Claude subscription sign-in saved by \`stratus setup\`, `
|
|
1107
|
-
+ `so ${primary ? 'this run is' : 'a fallback retry would be'} billed per token instead of through your plan. `
|
|
1108
|
-
+ 'Unset it to use the subscription, or run `stratus doctor` to see everything that is being overridden.');
|
|
1109
1186
|
};
|
|
1110
1187
|
const createApprovalPolicy = (mode, streams, env, ask) => {
|
|
1111
1188
|
if (mode === 'always') {
|
|
@@ -2051,15 +2128,21 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2051
2128
|
header: stratusHeaderLines,
|
|
2052
2129
|
consumeNotices: () => recentNotices.splice(0),
|
|
2053
2130
|
});
|
|
2054
|
-
const defaultModelFor = (provider) => provider === 'openai'
|
|
2131
|
+
const defaultModelFor = (provider) => provider === 'openai'
|
|
2132
|
+
? DEFAULT_OPENAI_MODEL
|
|
2133
|
+
: provider === 'codex'
|
|
2134
|
+
? DEFAULT_CODEX_MODEL
|
|
2135
|
+
: DEFAULT_ANTHROPIC_MODEL;
|
|
2055
2136
|
// Widened to CliProviderName only so setup can ask about the provider it
|
|
2056
2137
|
// currently has selected, `demo` included; the rule itself is the shared one.
|
|
2057
|
-
const defaultKeyEnvFor = (provider) => defaultApiKeyEnvName(provider === '
|
|
2058
|
-
const credentialLabel = (credential) => credential.type === 'oauth_token'
|
|
2138
|
+
const defaultKeyEnvFor = (provider) => defaultApiKeyEnvName(provider === 'demo' ? 'anthropic' : provider);
|
|
2139
|
+
const credentialLabel = (provider, credential) => credential.type === 'oauth_token'
|
|
2140
|
+
? (provider === 'codex' ? 'ChatGPT sign-in' : 'Claude subscription')
|
|
2141
|
+
: 'API key';
|
|
2059
2142
|
const providerSignInStatus = (provider) => {
|
|
2060
2143
|
const credential = state.credentials[provider];
|
|
2061
2144
|
if (credential) {
|
|
2062
|
-
return `signed in (${credentialLabel(credential)})`;
|
|
2145
|
+
return `signed in (${credentialLabel(provider, credential)})`;
|
|
2063
2146
|
}
|
|
2064
2147
|
if (readNonEmptyString(processEnv[defaultKeyEnvFor(provider)])) {
|
|
2065
2148
|
return `using ${defaultKeyEnvFor(provider)} from your environment`;
|
|
@@ -2068,10 +2151,10 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2068
2151
|
};
|
|
2069
2152
|
const providersSummary = () => {
|
|
2070
2153
|
const parts = [];
|
|
2071
|
-
for (const provider of
|
|
2154
|
+
for (const provider of CREDENTIAL_PROVIDER_NAMES) {
|
|
2072
2155
|
const credential = state.credentials[provider];
|
|
2073
2156
|
if (credential) {
|
|
2074
|
-
parts.push(`${provider} (${credentialLabel(credential)})`);
|
|
2157
|
+
parts.push(`${provider} (${credentialLabel(provider, credential)})`);
|
|
2075
2158
|
}
|
|
2076
2159
|
else if (readNonEmptyString(processEnv[defaultKeyEnvFor(provider)])) {
|
|
2077
2160
|
parts.push(`${provider} (env key)`);
|
|
@@ -2099,7 +2182,9 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2099
2182
|
const credential = state.credentials[state.provider];
|
|
2100
2183
|
if (credential) {
|
|
2101
2184
|
return credential.type === 'oauth_token'
|
|
2102
|
-
?
|
|
2185
|
+
? (state.provider === 'codex'
|
|
2186
|
+
? 'using your ChatGPT (codex login) sign-in'
|
|
2187
|
+
: 'signed in with your Claude subscription')
|
|
2103
2188
|
: 'signed in with an API key';
|
|
2104
2189
|
}
|
|
2105
2190
|
const keyEnv = state.apiKeyEnv ?? defaultKeyEnvFor(state.provider);
|
|
@@ -2233,6 +2318,59 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2233
2318
|
writeLine(streams.stdout, `! Could not reach the API to verify (${verdict.detail}). Saved the key anyway — it will be checked on your first run.`);
|
|
2234
2319
|
}
|
|
2235
2320
|
};
|
|
2321
|
+
const signInCodex = async () => {
|
|
2322
|
+
const signedIn = state.credentials.codex !== undefined;
|
|
2323
|
+
const answer = await prompter.select('How should Stratus connect to Codex?', [
|
|
2324
|
+
'ChatGPT subscription — uses this machine\'s `codex login` sign-in, no per-token cost',
|
|
2325
|
+
'OpenAI API key — pay per use (platform.openai.com)',
|
|
2326
|
+
'Skip for now',
|
|
2327
|
+
...(signedIn ? ['Sign out'] : []),
|
|
2328
|
+
]);
|
|
2329
|
+
if (signedIn && answer.kind === 'index' && answer.index === 3) {
|
|
2330
|
+
delete state.credentials.codex;
|
|
2331
|
+
state.credentialsDirty = true;
|
|
2332
|
+
writeLine(streams.stdout, 'Signed out of Codex. (A `codex login` sign-in, if any, stays with codex itself — run `codex logout` to clear it.)');
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
if (answer.kind !== 'index' || answer.index === 2) {
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
if (answer.index === 1) {
|
|
2339
|
+
const key = await prompter.askSecret('Paste your OpenAI API key (Enter to skip; input is hidden): ');
|
|
2340
|
+
if (!key) {
|
|
2341
|
+
writeLine(streams.stdout, 'Skipped — you can sign in any time by re-running this menu.');
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
writeLine(streams.stdout, 'Checking the key against the OpenAI API…');
|
|
2345
|
+
const verdict = await verifyProviderKey('codex', key, undefined, env.fetch ?? globalThis.fetch);
|
|
2346
|
+
if (verdict.status === 'ok') {
|
|
2347
|
+
storeCredential('codex', { type: 'api_key', value: key });
|
|
2348
|
+
writeLine(streams.stdout, '✓ Key verified — Codex runs will bill this OpenAI API key.');
|
|
2349
|
+
}
|
|
2350
|
+
else if (verdict.status === 'rejected') {
|
|
2351
|
+
writeLine(streams.stdout, `✗ OpenAI rejected that key (${verdict.detail}). It was NOT saved — check platform.openai.com and try again from this menu.`);
|
|
2352
|
+
}
|
|
2353
|
+
else {
|
|
2354
|
+
storeCredential('codex', { type: 'api_key', value: key });
|
|
2355
|
+
writeLine(streams.stdout, `! Could not reach the OpenAI API to verify (${verdict.detail}). Saved the key anyway — it will be checked on your first run.`);
|
|
2356
|
+
}
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
// Default: the machine's own ChatGPT sign-in. Codex keeps those tokens
|
|
2360
|
+
// in its own auth store; Stratus records only that this machine uses
|
|
2361
|
+
// it, so nothing secret is written here.
|
|
2362
|
+
writeLine(streams.stdout, 'Your ChatGPT plan covers usage made through Codex.');
|
|
2363
|
+
writeLine(streams.stdout, 'If you have not signed in yet, run this in another terminal on this machine:');
|
|
2364
|
+
writeLine(streams.stdout, ' codex login');
|
|
2365
|
+
writeLine(streams.stdout, '(requires the Codex CLI: npm install -g @openai/codex)');
|
|
2366
|
+
const confirmed = await prompter.select('Use this machine\'s codex sign-in?', ['Yes — codex is (or will be) signed in here', 'Skip for now']);
|
|
2367
|
+
if (confirmed.kind !== 'index' || confirmed.index !== 0) {
|
|
2368
|
+
writeLine(streams.stdout, 'Skipped — you can sign in any time by re-running this menu.');
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
storeCredential('codex', { type: 'oauth_token', value: 'chatgpt' });
|
|
2372
|
+
writeLine(streams.stdout, '✓ Recorded — Codex runs use this machine\'s ChatGPT sign-in. It is verified on your first run.');
|
|
2373
|
+
};
|
|
2236
2374
|
// Changing the default provider invalidates settings that were chosen
|
|
2237
2375
|
// for the old one: the model and apiKeyEnv are cleared (defaults take
|
|
2238
2376
|
// over), while the openai base URL is kept — it belongs to the openai
|
|
@@ -2292,17 +2430,25 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2292
2430
|
const answer = await prompter.select('Providers — sign in to one or more:', [
|
|
2293
2431
|
`Claude (Anthropic) — ${providerSignInStatus('anthropic')}`,
|
|
2294
2432
|
`OpenAI-compatible — ${providerSignInStatus('openai')}`,
|
|
2433
|
+
`Codex (ChatGPT) — ${providerSignInStatus('codex')}`,
|
|
2295
2434
|
'Demo — built-in fake model, offline, no account',
|
|
2296
2435
|
'Back',
|
|
2297
2436
|
]);
|
|
2298
|
-
if (answer.kind !== 'index' || answer.index ===
|
|
2437
|
+
if (answer.kind !== 'index' || answer.index === 4) {
|
|
2299
2438
|
return;
|
|
2300
2439
|
}
|
|
2301
|
-
if (answer.index ===
|
|
2440
|
+
if (answer.index === 3) {
|
|
2302
2441
|
await switchDefaultProvider('demo');
|
|
2303
2442
|
writeLine(streams.stdout, 'Demo selected — no sign-in needed. Mention "echo" or "tool" in a prompt to see tool calls.');
|
|
2304
2443
|
return;
|
|
2305
2444
|
}
|
|
2445
|
+
if (answer.index === 2) {
|
|
2446
|
+
await signInCodex();
|
|
2447
|
+
if (state.credentials.codex) {
|
|
2448
|
+
await maybeSwitchDefault('codex');
|
|
2449
|
+
}
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2306
2452
|
if (answer.index === 1) {
|
|
2307
2453
|
await signInOpenAI();
|
|
2308
2454
|
if (state.credentials.openai) {
|
|
@@ -2349,10 +2495,10 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2349
2495
|
if (typed.includes(':')) {
|
|
2350
2496
|
const [providerPart, ...idParts] = typed.split(':');
|
|
2351
2497
|
const id = idParts.join(':').trim();
|
|
2352
|
-
if ((providerPart === 'anthropic' || providerPart === 'openai') && id) {
|
|
2498
|
+
if ((providerPart === 'anthropic' || providerPart === 'openai' || providerPart === 'codex') && id) {
|
|
2353
2499
|
return { provider: providerPart, id };
|
|
2354
2500
|
}
|
|
2355
|
-
writeLine(streams.stdout, 'Use provider:model, e.g. anthropic:claude-opus-5.');
|
|
2501
|
+
writeLine(streams.stdout, 'Use provider:model, e.g. anthropic:claude-opus-5 or codex:gpt-5.5.');
|
|
2356
2502
|
return undefined;
|
|
2357
2503
|
}
|
|
2358
2504
|
// A typed id that appears in the collected list belongs to that
|
|
@@ -2487,10 +2633,17 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2487
2633
|
? readNonEmptyString(processEnv.STRATUS_API_KEY)
|
|
2488
2634
|
: undefined)
|
|
2489
2635
|
?? readNonEmptyString(processEnv[defaultKeyEnvFor(fallbackProvider)]);
|
|
2490
|
-
const
|
|
2636
|
+
const candidate = envKey ? undefined : state.credentials[fallbackProvider];
|
|
2637
|
+
// A codex fallback consumes no endpoint URL, so a stored key bound to
|
|
2638
|
+
// one cannot serve it — the same skip resolveRuntimeConfig performs.
|
|
2639
|
+
const credential = fallbackProvider === 'codex' && candidate?.type === 'api_key' && candidate.baseUrl !== undefined
|
|
2640
|
+
? undefined
|
|
2641
|
+
: candidate;
|
|
2491
2642
|
const apiKey = envKey ?? (credential?.type === 'api_key' ? credential.value : undefined);
|
|
2492
|
-
|
|
2493
|
-
|
|
2643
|
+
// A codex oauth entry is the subscription marker, not a token to send.
|
|
2644
|
+
const codexSubscription = fallbackProvider === 'codex' && !apiKey && credential?.type === 'oauth_token';
|
|
2645
|
+
const authToken = fallbackProvider !== 'codex' && credential?.type === 'oauth_token' ? credential.value : undefined;
|
|
2646
|
+
if (!apiKey && !authToken && !codexSubscription) {
|
|
2494
2647
|
return undefined;
|
|
2495
2648
|
}
|
|
2496
2649
|
return {
|
|
@@ -2503,13 +2656,17 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2503
2656
|
?? (state.provider === 'openai' ? state.baseUrl : undefined)
|
|
2504
2657
|
?? DEFAULT_OPENAI_BASE_URL,
|
|
2505
2658
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2659
|
+
// The codex harness owns its endpoints; only anthropic can carry one.
|
|
2660
|
+
: fallbackProvider === 'codex'
|
|
2661
|
+
? {}
|
|
2662
|
+
: (() => {
|
|
2663
|
+
const url = (fallbackProvider === state.provider ? state.baseUrl : undefined)
|
|
2664
|
+
?? (credential?.type === 'api_key' ? credential.baseUrl : undefined);
|
|
2665
|
+
return url ? { baseUrl: url } : {};
|
|
2666
|
+
})()),
|
|
2511
2667
|
...(apiKey ? { apiKey } : {}),
|
|
2512
2668
|
...(authToken ? { authToken } : {}),
|
|
2669
|
+
...(codexSubscription ? { codexSubscription: true } : {}),
|
|
2513
2670
|
};
|
|
2514
2671
|
};
|
|
2515
2672
|
const buildTestRuntime = async () => {
|
|
@@ -2558,6 +2715,32 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2558
2715
|
...(fallback ? { fallback } : {}),
|
|
2559
2716
|
};
|
|
2560
2717
|
}
|
|
2718
|
+
if (state.provider === 'codex') {
|
|
2719
|
+
// The harness owns its endpoints: a key bound to one cannot be
|
|
2720
|
+
// honored on codex — resolveRuntimeConfig refuses this outright, and
|
|
2721
|
+
// the inline test mirrors it rather than quietly unbinding the key.
|
|
2722
|
+
if (boundUrl !== undefined) {
|
|
2723
|
+
writeLine(streams.stdout, `Your saved codex key is bound to ${boundUrl}, and codex does not use a custom base URL. Store the key without one to run on codex.`);
|
|
2724
|
+
return undefined;
|
|
2725
|
+
}
|
|
2726
|
+
const apiKey = envKey ?? (credential?.type === 'api_key' ? credential.value : undefined);
|
|
2727
|
+
// The subscription marker means the machine's own codex sign-in
|
|
2728
|
+
// serves the run — nothing more to resolve here.
|
|
2729
|
+
if (!apiKey && credential?.type !== 'oauth_token') {
|
|
2730
|
+
writeLine(streams.stdout, 'You are not signed in yet — pick option 1 first (or export CODEX_API_KEY).');
|
|
2731
|
+
return undefined;
|
|
2732
|
+
}
|
|
2733
|
+
const fallback = buildTestFallback();
|
|
2734
|
+
return {
|
|
2735
|
+
provider: 'codex',
|
|
2736
|
+
model,
|
|
2737
|
+
...(apiKey ? { apiKey } : {}),
|
|
2738
|
+
...(state.systemPrompt ? { systemPrompt: state.systemPrompt } : {}),
|
|
2739
|
+
...(env.fetch ? { fetch: env.fetch } : {}),
|
|
2740
|
+
...(soul ? { soul } : {}),
|
|
2741
|
+
...(fallback ? { fallback } : {}),
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2561
2744
|
const apiKey = envKey ?? (credential?.type === 'api_key' ? credential.value : undefined);
|
|
2562
2745
|
if (!apiKey) {
|
|
2563
2746
|
writeLine(streams.stdout, 'You are not signed in yet — pick option 1 first (or export OPENAI_API_KEY).');
|
|
@@ -2996,7 +3179,22 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
2996
3179
|
: `Skipped. Install them yourself with: npm install -g ${rest}`);
|
|
2997
3180
|
}
|
|
2998
3181
|
};
|
|
3182
|
+
/**
|
|
3183
|
+
* Write everything the menu decided, then report whether the always-on
|
|
3184
|
+
* install it performed succeeded.
|
|
3185
|
+
*
|
|
3186
|
+
* A boolean rather than a throw: the install is the one optional part of
|
|
3187
|
+
* setup and everything else is already on disk by the time it runs, so
|
|
3188
|
+
* failing outright would lose the saved config to a service that can be
|
|
3189
|
+
* installed later. The exit code carries it instead — the same answer
|
|
3190
|
+
* `stratus service install` gives for the identical failure, which is what
|
|
3191
|
+
* a script driving setup has to be able to see.
|
|
3192
|
+
*/
|
|
2999
3193
|
const save = async () => {
|
|
3194
|
+
// The always-on install is the only step here that can fail without
|
|
3195
|
+
// taking the rest of setup with it, so it is the only one the exit code
|
|
3196
|
+
// has to carry.
|
|
3197
|
+
let serviceInstallFailed = false;
|
|
3000
3198
|
const config = { provider: state.provider };
|
|
3001
3199
|
if (state.provider !== 'demo') {
|
|
3002
3200
|
config.model = state.model ?? defaultModelFor(state.provider);
|
|
@@ -3114,6 +3312,7 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
3114
3312
|
}
|
|
3115
3313
|
if (!result.ok) {
|
|
3116
3314
|
writeLine(streams.stderr, `Setup is saved either way — start the daemon yourself with \`${serveCommand()}\`.`);
|
|
3315
|
+
serviceInstallFailed = true;
|
|
3117
3316
|
}
|
|
3118
3317
|
}
|
|
3119
3318
|
writeLine(streams.stdout);
|
|
@@ -3164,6 +3363,7 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
3164
3363
|
if (dashboardReady) {
|
|
3165
3364
|
writeLine(streams.stdout, ' stratus dashboard');
|
|
3166
3365
|
}
|
|
3366
|
+
return !serviceInstallFailed;
|
|
3167
3367
|
};
|
|
3168
3368
|
try {
|
|
3169
3369
|
if (!interactive) {
|
|
@@ -3207,8 +3407,12 @@ export const runSetup = async (command, streams, env = {}) => {
|
|
|
3207
3407
|
await testRun();
|
|
3208
3408
|
}
|
|
3209
3409
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3410
|
+
// Non-zero when the always-on install failed, matching `stratus service
|
|
3411
|
+
// install`. Everything setup saved is still saved, and the output above
|
|
3412
|
+
// says so — but a daemon that will not come up at login is not a
|
|
3413
|
+
// successful setup, and a script that only reads the exit code had no
|
|
3414
|
+
// way to tell.
|
|
3415
|
+
return (await save()) ? 0 : 1;
|
|
3212
3416
|
}
|
|
3213
3417
|
finally {
|
|
3214
3418
|
prompter.close();
|
|
@@ -3489,9 +3693,11 @@ export const collectDoctorReport = async (command, warn, env = {}) => {
|
|
|
3489
3693
|
const usedKeyEnvVar = resolved && resolved.provider !== 'demo' ? resolved.apiKeyEnvVar : undefined;
|
|
3490
3694
|
const usesAuthToken = resolved?.provider === 'anthropic' && resolved.authToken !== undefined;
|
|
3491
3695
|
const fallback = resolved && resolved.provider !== 'demo' ? resolved.fallback : undefined;
|
|
3492
|
-
for (const target of
|
|
3696
|
+
for (const target of CREDENTIAL_PROVIDER_NAMES) {
|
|
3493
3697
|
const stored = credentials[target];
|
|
3494
|
-
const label = stored?.type === 'oauth_token'
|
|
3698
|
+
const label = stored?.type === 'oauth_token'
|
|
3699
|
+
? (target === 'codex' ? 'ChatGPT sign-in (codex login)' : 'Claude subscription (Pro/Max)')
|
|
3700
|
+
: 'API key';
|
|
3495
3701
|
const isDefault = resolved?.provider === target;
|
|
3496
3702
|
const isFallback = fallback?.provider === target;
|
|
3497
3703
|
// A provider no resolved run consults is reported, never diagnosed:
|
|
@@ -3505,7 +3711,7 @@ export const collectDoctorReport = async (command, warn, env = {}) => {
|
|
|
3505
3711
|
});
|
|
3506
3712
|
continue;
|
|
3507
3713
|
}
|
|
3508
|
-
const envVar = isDefault ? usedKeyEnvVar : (target
|
|
3714
|
+
const envVar = isDefault ? usedKeyEnvVar : defaultApiKeyEnvName(target);
|
|
3509
3715
|
const viaEnv = isDefault
|
|
3510
3716
|
? usedKeyEnvVar !== undefined
|
|
3511
3717
|
: fallback?.apiKey !== undefined && fallback.apiKey === readNonEmptyString(processEnv[String(envVar)]);
|
|
@@ -4404,6 +4610,37 @@ export const runSkills = async (streams, env = {}) => {
|
|
|
4404
4610
|
}
|
|
4405
4611
|
return 0;
|
|
4406
4612
|
};
|
|
4613
|
+
/**
|
|
4614
|
+
* The provider/model a newly created soul pins. Frontmatter pins what a run
|
|
4615
|
+
* from this directory would actually use: env vars outrank the active
|
|
4616
|
+
* config file (project-local or explicit first, global otherwise), and no
|
|
4617
|
+
* provider anywhere falls back to demo — the exact precedence of stratus
|
|
4618
|
+
* run. Demo produces no model at all: the soul keeps following the
|
|
4619
|
+
* machine's configuration instead of demanding credentials nothing has
|
|
4620
|
+
* signed in for. The active config's model was written for the provider
|
|
4621
|
+
* named in that config, so it only travels into the soul when they still
|
|
4622
|
+
* match; otherwise the selected provider's own default model stands in.
|
|
4623
|
+
*
|
|
4624
|
+
* Exported for tests: the interactive path that consumes it needs a TTY.
|
|
4625
|
+
*/
|
|
4626
|
+
export const soulPinForNewAgent = (activeConfig, processEnv) => {
|
|
4627
|
+
// The map already validates, so the value is a provider name; the cast
|
|
4628
|
+
// only undoes `readNonEmptyString`'s widening to `string`.
|
|
4629
|
+
const envProvider = readNonEmptyString(processEnv.STRATUS_PROVIDER, (value) => parseProviderName(value, 'STRATUS_PROVIDER'));
|
|
4630
|
+
const provider = envProvider ?? activeConfig.provider ?? 'demo';
|
|
4631
|
+
if (provider === 'demo') {
|
|
4632
|
+
return { provider };
|
|
4633
|
+
}
|
|
4634
|
+
const configModelApplies = (activeConfig.provider ?? 'openai') === provider;
|
|
4635
|
+
const model = readNonEmptyString(processEnv.STRATUS_MODEL)
|
|
4636
|
+
?? (configModelApplies ? activeConfig.model : undefined)
|
|
4637
|
+
?? (provider === 'openai'
|
|
4638
|
+
? DEFAULT_OPENAI_MODEL
|
|
4639
|
+
: provider === 'codex'
|
|
4640
|
+
? DEFAULT_CODEX_MODEL
|
|
4641
|
+
: DEFAULT_ANTHROPIC_MODEL);
|
|
4642
|
+
return { provider, model };
|
|
4643
|
+
};
|
|
4407
4644
|
export const runAgentNew = async (command, streams, env = {}) => {
|
|
4408
4645
|
// On a real terminal, creating an agent is the same guided experience as
|
|
4409
4646
|
// setup: a headed screen, a prefilled (editable) name, a personality, and
|
|
@@ -4427,28 +4664,16 @@ export const runAgentNew = async (command, streams, env = {}) => {
|
|
|
4427
4664
|
const name = (await prompter.ask('Choose your name: ', { prefill: suggested })) || suggested;
|
|
4428
4665
|
const instructions = await prompter.ask('Describe their personality (Enter for a starter you can edit later): ', ...(command.instructions ? [{ prefill: command.instructions }] : []));
|
|
4429
4666
|
const persona = instructions || DEFAULT_SOUL_STARTER;
|
|
4430
|
-
// Frontmatter pins what a run from this directory would actually use:
|
|
4431
|
-
// env vars outrank the active config file (project-local or explicit
|
|
4432
|
-
// first, global otherwise), and no provider anywhere falls back to
|
|
4433
|
-
// demo — the exact precedence of stratus run. Demo produces no pin
|
|
4434
|
-
// at all: the soul keeps following the machine's configuration
|
|
4435
|
-
// instead of demanding credentials nothing has signed in for.
|
|
4436
4667
|
const processEnv = readProcessEnv(env);
|
|
4437
4668
|
// Creating an agent must not be blocked by a broken config — it only
|
|
4438
4669
|
// feeds the soul's provider/model hint, so fall back to defaults.
|
|
4439
4670
|
const { location: configLocation, config: activeConfig } = await discoverActiveConfig(env, (message) => {
|
|
4440
4671
|
writeLine(streams.stdout, `Note: ${message}.`);
|
|
4441
4672
|
});
|
|
4442
|
-
const
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
// that config; it only travels into the soul when they still match.
|
|
4447
|
-
const configModelApplies = (activeConfig.provider ?? 'openai') === soulProvider;
|
|
4448
|
-
const soulModel = readNonEmptyString(processEnv.STRATUS_MODEL)
|
|
4449
|
-
?? (configModelApplies ? activeConfig.model : undefined)
|
|
4450
|
-
?? (soulProvider === 'openai' ? DEFAULT_OPENAI_MODEL : DEFAULT_ANTHROPIC_MODEL);
|
|
4451
|
-
const soulPin = soulProvider !== 'demo' ? { provider: soulProvider, model: soulModel } : {};
|
|
4673
|
+
const { provider: soulProvider, model: soulModel } = soulPinForNewAgent(activeConfig, processEnv);
|
|
4674
|
+
const soulPin = soulProvider !== 'demo' && soulModel !== undefined
|
|
4675
|
+
? { provider: soulProvider, model: soulModel }
|
|
4676
|
+
: {};
|
|
4452
4677
|
const claimed = await claimSoulFile(env, { name, instructions: persona }, (candidate) => formatSoul({ agent: candidate, ...soulPin }), (message) => writeLine(streams.stdout, message));
|
|
4453
4678
|
const { agent, soulPath } = claimed;
|
|
4454
4679
|
const makeDefault = await prompter.select(`Make ${agent.name} your default agent?`, [
|