myshell-tools 2.14.0 → 3.0.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/CHANGELOG.md +36 -0
- package/README.md +8 -24
- package/dist/cli.js +12 -17
- package/dist/cli.js.map +1 -1
- package/dist/commands/cost.js +6 -2
- package/dist/commands/cost.js.map +1 -1
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/login.d.ts +18 -10
- package/dist/commands/login.js +81 -109
- package/dist/commands/login.js.map +1 -1
- package/dist/core/json-envelope.js +31 -3
- package/dist/core/json-envelope.js.map +1 -1
- package/dist/core/orchestrate.js +82 -40
- package/dist/core/orchestrate.js.map +1 -1
- package/dist/infra/atomic.d.ts +9 -1
- package/dist/infra/atomic.js +12 -2
- package/dist/infra/atomic.js.map +1 -1
- package/dist/infra/config.d.ts +4 -3
- package/dist/infra/config.js +1 -0
- package/dist/infra/config.js.map +1 -1
- package/dist/infra/credentials.d.ts +34 -3
- package/dist/infra/credentials.js +59 -8
- package/dist/infra/credentials.js.map +1 -1
- package/dist/interface/menu.d.ts +20 -0
- package/dist/interface/menu.js +138 -72
- package/dist/interface/menu.js.map +1 -1
- package/dist/interface/render.js +9 -5
- package/dist/interface/render.js.map +1 -1
- package/dist/providers/claude.js +12 -0
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/detect.js +22 -1
- package/dist/providers/detect.js.map +1 -1
- package/dist/providers/registry.d.ts +8 -4
- package/dist/providers/registry.js +7 -6
- package/dist/providers/registry.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* shape `{ claudeOauthToken?: string }` so the Claude OAuth token captured
|
|
6
6
|
* during `myshell-tools login claude --code` is available across restarts.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
8
|
+
* Token scoping: instead of injecting the token into the global `process.env`
|
|
9
|
+
* at startup (which would expose it to every child process), callers use
|
|
10
|
+
* `loadClaudeToken()` + `claudeEnv()` to build a scoped env object that is
|
|
11
|
+
* passed only to Claude CLI invocations. Other providers (codex, opencode,
|
|
12
|
+
* npm) never see the token.
|
|
11
13
|
*
|
|
12
14
|
* Security: the file is written with mode 0o600 (owner-read-only) on POSIX
|
|
13
15
|
* systems. The chmod is best-effort — a failure is silently ignored so the
|
|
@@ -66,6 +68,49 @@ export async function loadCredentials(homeDir) {
|
|
|
66
68
|
return {};
|
|
67
69
|
}
|
|
68
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Load the stored Claude OAuth token. Returns `null` when no token is stored
|
|
73
|
+
* or when the credentials file is missing/corrupt. Never throws.
|
|
74
|
+
*
|
|
75
|
+
* This is a thin convenience wrapper over `loadCredentials` that returns the
|
|
76
|
+
* token string directly so callers don't need to destructure `Credentials`.
|
|
77
|
+
*/
|
|
78
|
+
export async function loadClaudeToken(homeDir) {
|
|
79
|
+
try {
|
|
80
|
+
const creds = await loadCredentials(homeDir);
|
|
81
|
+
return creds.claudeOauthToken ?? null;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build a child-process environment that injects the Claude OAuth token into
|
|
89
|
+
* ONLY the `CLAUDE_CODE_OAUTH_TOKEN` variable, leaving all other variables
|
|
90
|
+
* from `baseEnv` intact.
|
|
91
|
+
*
|
|
92
|
+
* Rules (pure — no I/O):
|
|
93
|
+
* - Returns `baseEnv` unchanged when `token` is `null` (nothing stored).
|
|
94
|
+
* - Returns `baseEnv` unchanged when `baseEnv.CLAUDE_CODE_OAUTH_TOKEN` is
|
|
95
|
+
* already set — the user's explicitly-exported value always wins.
|
|
96
|
+
* - Otherwise returns `{ ...baseEnv, CLAUDE_CODE_OAUTH_TOKEN: token }`.
|
|
97
|
+
*
|
|
98
|
+
* Pass the result as the `env` option to execa for Claude CLI spawns only.
|
|
99
|
+
* Never pass it to codex/opencode/npm children — they do not need it and
|
|
100
|
+
* should not see the token.
|
|
101
|
+
*
|
|
102
|
+
* Pure / never throws.
|
|
103
|
+
*/
|
|
104
|
+
export function claudeEnv(baseEnv, token) {
|
|
105
|
+
if (token === null) {
|
|
106
|
+
return baseEnv;
|
|
107
|
+
}
|
|
108
|
+
if (baseEnv['CLAUDE_CODE_OAUTH_TOKEN'] !== undefined) {
|
|
109
|
+
// User's explicitly-exported env wins — do not overwrite.
|
|
110
|
+
return baseEnv;
|
|
111
|
+
}
|
|
112
|
+
return { ...baseEnv, CLAUDE_CODE_OAUTH_TOKEN: token };
|
|
113
|
+
}
|
|
69
114
|
/**
|
|
70
115
|
* Persist the Claude OAuth token atomically to
|
|
71
116
|
* `~/.myshell-tools/credentials.json` with restrictive permissions (0o600).
|
|
@@ -74,12 +119,15 @@ export async function saveClaudeToken(token, homeDir) {
|
|
|
74
119
|
const home = homeDir ?? homedir();
|
|
75
120
|
const dir = getCredentialsDir(home);
|
|
76
121
|
const path = getCredentialsPath(home);
|
|
77
|
-
|
|
122
|
+
// Create the directory with restrictive permissions (0o700) so it is never
|
|
123
|
+
// world-readable. recursive:true is a no-op when it already exists.
|
|
124
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
78
125
|
// Load existing credentials so we only replace the token key, preserving others.
|
|
79
126
|
const existing = await loadCredentials(homeDir);
|
|
80
127
|
const updated = { ...existing, claudeOauthToken: token };
|
|
81
|
-
// atomicWrite
|
|
82
|
-
|
|
128
|
+
// atomicWrite with mode 0o600 guarantees the temp file is never more permissive
|
|
129
|
+
// than the final destination — no world-readable window before the rename.
|
|
130
|
+
await atomicWrite(path, JSON.stringify(updated, null, 2), 0o600);
|
|
83
131
|
// Best-effort: restrict to owner-read-only. Silently ignored on Windows or
|
|
84
132
|
// unusual filesystems where chmod is unavailable or unsupported.
|
|
85
133
|
try {
|
|
@@ -200,6 +248,8 @@ export function stripPastedSecretWrapper(raw) {
|
|
|
200
248
|
* - `'api-key'` — starts with `sk-ant-api` (a raw Anthropic API key, NOT what we want).
|
|
201
249
|
* - `'none'` — neither; blank or unrecognised.
|
|
202
250
|
*
|
|
251
|
+
* Uses `startsWith` semantics: mid-string occurrences of `sk-ant-oat` or
|
|
252
|
+
* `sk-ant-api` do NOT classify as oauth-token or api-key respectively.
|
|
203
253
|
* Input is pre-normalised (trimmed, quotes stripped) by the caller.
|
|
204
254
|
* Pure / never throws.
|
|
205
255
|
*
|
|
@@ -207,12 +257,13 @@ export function stripPastedSecretWrapper(raw) {
|
|
|
207
257
|
* classifyPastedSecret('sk-ant-oat01-abc-XYZ') // → 'oauth-token'
|
|
208
258
|
* classifyPastedSecret('sk-ant-api03-abc-XYZ') // → 'api-key'
|
|
209
259
|
* classifyPastedSecret('not-a-token') // → 'none'
|
|
260
|
+
* classifyPastedSecret('prefix sk-ant-oat01-x') // → 'none'
|
|
210
261
|
*/
|
|
211
262
|
export function classifyPastedSecret(s) {
|
|
212
263
|
try {
|
|
213
|
-
if (s.
|
|
264
|
+
if (s.startsWith('sk-ant-oat'))
|
|
214
265
|
return 'oauth-token';
|
|
215
|
-
if (s.
|
|
266
|
+
if (s.startsWith('sk-ant-api'))
|
|
216
267
|
return 'api-key';
|
|
217
268
|
return 'none';
|
|
218
269
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/infra/credentials.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/infra/credentials.ts"],"names":[],"mappings":"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"}
|
package/dist/interface/menu.d.ts
CHANGED
|
@@ -110,6 +110,26 @@ export interface MenuContext {
|
|
|
110
110
|
* @returns True for yes, false for no.
|
|
111
111
|
*/
|
|
112
112
|
export declare function parseYesNo(input: string | null, defaultYes: boolean): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Decide whether auto-update is enabled for this launch.
|
|
115
|
+
*
|
|
116
|
+
* Auto-update is considered ENABLED when:
|
|
117
|
+
* - `config.autoUpdate` is `true` or `undefined` (absent = default-on), AND
|
|
118
|
+
* - `env['MYSHELL_NO_UPDATE']` is not set (any non-empty value disables it).
|
|
119
|
+
*
|
|
120
|
+
* A user who explicitly set `autoUpdate: false` in their config keeps it off.
|
|
121
|
+
* Setting `MYSHELL_NO_UPDATE=1` (or any non-empty value) in the environment
|
|
122
|
+
* overrides even an explicit `autoUpdate: true`.
|
|
123
|
+
*
|
|
124
|
+
* Pure — no I/O, no side effects, never throws.
|
|
125
|
+
*
|
|
126
|
+
* @param config - The loaded AppConfig (only `autoUpdate` field is read).
|
|
127
|
+
* @param env - A `NodeJS.ProcessEnv`-shaped object to read `MYSHELL_NO_UPDATE` from.
|
|
128
|
+
* @returns True when auto-update should run at launch.
|
|
129
|
+
*/
|
|
130
|
+
export declare function autoUpdateEnabled(config: {
|
|
131
|
+
autoUpdate?: boolean;
|
|
132
|
+
}, env: NodeJS.ProcessEnv): boolean;
|
|
113
133
|
/**
|
|
114
134
|
* Return the shell alias hint the user can add to their shell profile to make
|
|
115
135
|
* `myshell-tools` the default command-line assistant.
|
package/dist/interface/menu.js
CHANGED
|
@@ -59,6 +59,29 @@ export function parseYesNo(input, defaultYes) {
|
|
|
59
59
|
return false;
|
|
60
60
|
return defaultYes;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Decide whether auto-update is enabled for this launch.
|
|
64
|
+
*
|
|
65
|
+
* Auto-update is considered ENABLED when:
|
|
66
|
+
* - `config.autoUpdate` is `true` or `undefined` (absent = default-on), AND
|
|
67
|
+
* - `env['MYSHELL_NO_UPDATE']` is not set (any non-empty value disables it).
|
|
68
|
+
*
|
|
69
|
+
* A user who explicitly set `autoUpdate: false` in their config keeps it off.
|
|
70
|
+
* Setting `MYSHELL_NO_UPDATE=1` (or any non-empty value) in the environment
|
|
71
|
+
* overrides even an explicit `autoUpdate: true`.
|
|
72
|
+
*
|
|
73
|
+
* Pure — no I/O, no side effects, never throws.
|
|
74
|
+
*
|
|
75
|
+
* @param config - The loaded AppConfig (only `autoUpdate` field is read).
|
|
76
|
+
* @param env - A `NodeJS.ProcessEnv`-shaped object to read `MYSHELL_NO_UPDATE` from.
|
|
77
|
+
* @returns True when auto-update should run at launch.
|
|
78
|
+
*/
|
|
79
|
+
export function autoUpdateEnabled(config, env) {
|
|
80
|
+
if (env['MYSHELL_NO_UPDATE'] !== undefined && env['MYSHELL_NO_UPDATE'].length > 0) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return config.autoUpdate !== false;
|
|
84
|
+
}
|
|
62
85
|
/**
|
|
63
86
|
* Return the shell alias hint the user can add to their shell profile to make
|
|
64
87
|
* `myshell-tools` the default command-line assistant.
|
|
@@ -137,7 +160,8 @@ export function renderHeaderLines(env, _version) {
|
|
|
137
160
|
const ps = env.opencode;
|
|
138
161
|
const planSuffix = ps.plan != null ? ` (${ps.plan})` : '';
|
|
139
162
|
if (ps.authenticated) {
|
|
140
|
-
|
|
163
|
+
// Be explicit that "ready" is based on free models, not a credential probe.
|
|
164
|
+
lines.push(`✅ ${ps.id}: ready (free models)${planSuffix}`);
|
|
141
165
|
}
|
|
142
166
|
else {
|
|
143
167
|
lines.push(`⚠️ ${ps.id}: not signed in${planSuffix}`);
|
|
@@ -290,6 +314,8 @@ async function runWelcome(ctx, out, readLine, mutableConfig, installProviderFn,
|
|
|
290
314
|
let env = ctx.env;
|
|
291
315
|
const headerLines = renderHeaderLines(env, ctx.version);
|
|
292
316
|
out.write('\n' + box(`🧠 myshell-tools v${ctx.version} — Setup`, headerLines) + '\n\n');
|
|
317
|
+
// ---- Orientation header --------------------------------------------------
|
|
318
|
+
out.write('Quick setup — a few questions, ~30 seconds. Press Enter to accept the [Capitalized] default.\n\n');
|
|
293
319
|
// ---- Offer to install any missing provider (claude / codex) --------------
|
|
294
320
|
// Consent is required: we ask once per missing provider.
|
|
295
321
|
// Display: (Y/n) — default YES, so Enter installs; explicit n skips.
|
|
@@ -346,14 +372,12 @@ async function runWelcome(ctx, out, readLine, mutableConfig, installProviderFn,
|
|
|
346
372
|
await loginFn(out, id, { readLine });
|
|
347
373
|
}
|
|
348
374
|
}
|
|
349
|
-
// ---- Mode
|
|
350
|
-
|
|
351
|
-
out.write(' [
|
|
352
|
-
|
|
353
|
-
out.write('> ');
|
|
354
|
-
const key = await readLine();
|
|
375
|
+
// ---- Mode selection — single collapsed prompt ----------------------------
|
|
376
|
+
// Accepts 1/2/3 directly; Enter (or empty) keeps current default (balanced).
|
|
377
|
+
out.write('\nMode — [1] cost-saver [2] balanced (default) [3] quality-first (Enter = balanced): ');
|
|
378
|
+
const modeKey = await readLine();
|
|
355
379
|
// EOF during setup — save bare onboarded config and return
|
|
356
|
-
if (
|
|
380
|
+
if (modeKey === null) {
|
|
357
381
|
const saved = {
|
|
358
382
|
onboarded: true,
|
|
359
383
|
setAsDefault: false,
|
|
@@ -362,24 +386,32 @@ async function runWelcome(ctx, out, readLine, mutableConfig, installProviderFn,
|
|
|
362
386
|
await saveConfig(saved);
|
|
363
387
|
return saved;
|
|
364
388
|
}
|
|
365
|
-
let
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
389
|
+
let newMode = mutableConfig.mode;
|
|
390
|
+
if (modeKey === '1')
|
|
391
|
+
newMode = 'cost-saver';
|
|
392
|
+
else if (modeKey === '2')
|
|
393
|
+
newMode = 'balanced';
|
|
394
|
+
else if (modeKey === '3')
|
|
395
|
+
newMode = 'quality-first';
|
|
396
|
+
// Enter/empty/anything else → keep current (balanced default)
|
|
397
|
+
const updated = {
|
|
398
|
+
onboarded: mutableConfig.onboarded,
|
|
399
|
+
setAsDefault: mutableConfig.setAsDefault,
|
|
400
|
+
...(newMode !== undefined ? { mode: newMode } : {}),
|
|
401
|
+
};
|
|
370
402
|
// Default is NO for set-as-default — require explicit 'y' to enable.
|
|
371
403
|
out.write('Set myshell-tools as your default shell tool? (y/N) ');
|
|
372
404
|
const defaultAns = await readLine();
|
|
373
405
|
const setAsDefault = parseYesNo(defaultAns, false);
|
|
374
|
-
// Default is
|
|
375
|
-
out.write('Keep myshell-tools up to date automatically? (
|
|
406
|
+
// Default is YES for auto-update (recommended; user can opt out with n or via Settings).
|
|
407
|
+
out.write('Keep myshell-tools up to date automatically? (Y/n) ');
|
|
376
408
|
const autoUpdateAns = await readLine();
|
|
377
|
-
const autoUpdate = parseYesNo(autoUpdateAns,
|
|
409
|
+
const autoUpdate = parseYesNo(autoUpdateAns, true);
|
|
378
410
|
const saved = {
|
|
379
411
|
onboarded: true,
|
|
380
412
|
setAsDefault,
|
|
381
413
|
...(updated.mode !== undefined ? { mode: updated.mode } : {}),
|
|
382
|
-
...(autoUpdate ? { autoUpdate:
|
|
414
|
+
...(!autoUpdate ? { autoUpdate: false } : {}),
|
|
383
415
|
};
|
|
384
416
|
await saveConfig(saved);
|
|
385
417
|
// When the user opts in, actually write the shell startup hook (real install,
|
|
@@ -447,7 +479,7 @@ async function runSettings(_ctx, mutableCtx, out, readLine) {
|
|
|
447
479
|
'',
|
|
448
480
|
` [1] Mode: ${cfg.mode ?? 'balanced'}`,
|
|
449
481
|
` [2] Set as default shell: ${cfg.setAsDefault ? 'on' : 'off'}`,
|
|
450
|
-
` [3] Auto-update: ${cfg.autoUpdate
|
|
482
|
+
` [3] Auto-update: ${cfg.autoUpdate !== false ? 'on' : 'off'}`,
|
|
451
483
|
'',
|
|
452
484
|
' [Enter] Back',
|
|
453
485
|
'',
|
|
@@ -472,14 +504,20 @@ async function runSettings(_ctx, mutableCtx, out, readLine) {
|
|
|
472
504
|
/**
|
|
473
505
|
* Toggle the auto-update preference and persist the updated config.
|
|
474
506
|
* Reports the new state so the user knows what changed.
|
|
507
|
+
*
|
|
508
|
+
* Since auto-update now defaults to ON (undefined → enabled), toggling when
|
|
509
|
+
* currently enabled (true or undefined) sets it explicitly to false; toggling
|
|
510
|
+
* when currently disabled (false) removes the explicit flag (restores default-on).
|
|
475
511
|
*/
|
|
476
512
|
async function toggleAutoUpdate(config, out) {
|
|
477
|
-
|
|
513
|
+
// Currently enabled when autoUpdate !== false (undefined counts as on)
|
|
514
|
+
const currentlyEnabled = config.autoUpdate !== false;
|
|
515
|
+
const enable = !currentlyEnabled;
|
|
478
516
|
const updated = {
|
|
479
517
|
onboarded: config.onboarded,
|
|
480
518
|
setAsDefault: config.setAsDefault,
|
|
481
519
|
...(config.mode !== undefined ? { mode: config.mode } : {}),
|
|
482
|
-
...(enable ? { autoUpdate:
|
|
520
|
+
...(!enable ? { autoUpdate: false } : {}),
|
|
483
521
|
};
|
|
484
522
|
await saveConfig(updated);
|
|
485
523
|
out.write(`Auto-update: ${enable ? 'on' : 'off'}\n`);
|
|
@@ -777,9 +815,10 @@ async function runChatLoop(ctx, mutableCtx, convId, out, readLine, loginFn, dete
|
|
|
777
815
|
// can drive time with a fake clock.
|
|
778
816
|
const interruptTimes = [];
|
|
779
817
|
const INTERRUPT_WINDOW_MS = 1_500;
|
|
780
|
-
// The 'exit'
|
|
781
|
-
// loop via
|
|
818
|
+
// The 'exit' and 'menu' signals are communicated from the SIGINT handler to
|
|
819
|
+
// the main loop via these flags (the handler can't break the outer while directly).
|
|
782
820
|
let shouldExit = false;
|
|
821
|
+
let shouldMenu = false;
|
|
783
822
|
// Handle Ctrl+C with the press-counting model.
|
|
784
823
|
const sigintHandler = () => {
|
|
785
824
|
const now = ctx.clock.now();
|
|
@@ -800,13 +839,10 @@ async function runChatLoop(ctx, mutableCtx, convId, out, readLine, loginFn, dete
|
|
|
800
839
|
currentAc.abort();
|
|
801
840
|
currentAc = null;
|
|
802
841
|
}
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
// flag). In practice the loop is either awaiting readLine() or runTask().
|
|
808
|
-
// For the readLine case the user typed nothing yet — we need to interrupt
|
|
809
|
-
// that await. We use a shared resolver pattern: see loopBreaker below.
|
|
842
|
+
// Set shouldMenu so the loop returns 'menu' after any running task settles.
|
|
843
|
+
shouldMenu = true;
|
|
844
|
+
// For the readLine case (idle prompt) we can interrupt the await immediately
|
|
845
|
+
// via the loopBreaker resolver.
|
|
810
846
|
loopBreaker?.('menu');
|
|
811
847
|
}
|
|
812
848
|
else {
|
|
@@ -826,7 +862,7 @@ async function runChatLoop(ctx, mutableCtx, convId, out, readLine, loginFn, dete
|
|
|
826
862
|
let loopResult = 'menu';
|
|
827
863
|
try {
|
|
828
864
|
while (true) {
|
|
829
|
-
out.write('
|
|
865
|
+
out.write('> ');
|
|
830
866
|
// Race readLine() against a loopBreak signal from the SIGINT handler.
|
|
831
867
|
// When Ctrl+C fires (to-menu or exit-app), loopBreaker is called with the
|
|
832
868
|
// desired result, which wins the race and breaks the loop.
|
|
@@ -861,58 +897,79 @@ async function runChatLoop(ctx, mutableCtx, convId, out, readLine, loginFn, dete
|
|
|
861
897
|
const policy = mutableCtx.config.mode !== undefined
|
|
862
898
|
? POLICY_PRESETS[mutableCtx.config.mode]
|
|
863
899
|
: DEFAULT_POLICY;
|
|
900
|
+
// ---- Bug 4 fix: no-provider gate ----------------------------------------
|
|
901
|
+
// Check whether any provider is actually authenticated before dispatching a
|
|
902
|
+
// task that is doomed to fail. opencode counts as authenticated-when-installed.
|
|
903
|
+
const hasAuthenticatedProvider = mutableCtx.env.claude.authenticated ||
|
|
904
|
+
mutableCtx.env.codex.authenticated ||
|
|
905
|
+
mutableCtx.env.opencode.authenticated ||
|
|
906
|
+
mutableCtx.env.opencode.installed;
|
|
907
|
+
if (!hasAuthenticatedProvider) {
|
|
908
|
+
out.write('\n[info] No signed-in provider yet — press Ctrl+C to go back, then [j] Claude / [k] Codex / [o] opencode to sign in.\n');
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
864
911
|
// Load prior history before each turn so the provider receives conversation
|
|
865
912
|
// context. load() returns only the entries persisted so far — the current
|
|
866
913
|
// user turn is appended by orchestrate() after this point, so there is no
|
|
867
914
|
// double-inclusion risk.
|
|
868
915
|
const priorHistory = await ctx.store.load(convId);
|
|
869
|
-
// Build
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
916
|
+
// ---- Build deps from the live mutableCtx.env ----------------------------
|
|
917
|
+
// This helper is inlined as a function so it can be called again after
|
|
918
|
+
// inline re-login with the refreshed env (bug 5 fix: no stale auth state).
|
|
919
|
+
const buildDeps = () => {
|
|
920
|
+
// Build per-provider advertised model sets from the live env so route()
|
|
921
|
+
// can prefer a model the CLI actually advertises. Only include installed
|
|
922
|
+
// providers (exactOptionalPropertyTypes is ON).
|
|
923
|
+
// Use mutableCtx.env (not ctx.env) so post-login re-detect is reflected.
|
|
924
|
+
const availableModels = {};
|
|
925
|
+
if (mutableCtx.env.claude.installed && mutableCtx.env.claude.availableModels.length > 0) {
|
|
926
|
+
availableModels['claude'] = mutableCtx.env.claude.availableModels;
|
|
927
|
+
}
|
|
928
|
+
if (mutableCtx.env.codex.installed && mutableCtx.env.codex.availableModels.length > 0) {
|
|
929
|
+
availableModels['codex'] = mutableCtx.env.codex.availableModels;
|
|
930
|
+
}
|
|
931
|
+
if (mutableCtx.env.opencode.installed && mutableCtx.env.opencode.availableModels.length > 0) {
|
|
932
|
+
availableModels['opencode'] = mutableCtx.env.opencode.availableModels;
|
|
933
|
+
}
|
|
934
|
+
// Collect authenticated providers from the live env so route() prefers
|
|
935
|
+
// signed-in providers over signed-out ones. Uses mutableCtx.env so
|
|
936
|
+
// post-login re-detection is reflected without restart.
|
|
937
|
+
const authenticatedProviders = [];
|
|
938
|
+
if (mutableCtx.env.claude.authenticated)
|
|
939
|
+
authenticatedProviders.push('claude');
|
|
940
|
+
if (mutableCtx.env.codex.authenticated)
|
|
941
|
+
authenticatedProviders.push('codex');
|
|
942
|
+
if (mutableCtx.env.opencode.authenticated)
|
|
943
|
+
authenticatedProviders.push('opencode');
|
|
944
|
+
return {
|
|
945
|
+
clock: ctx.clock,
|
|
946
|
+
session: ctx.store.writer(convId),
|
|
947
|
+
ledger: ctx.ledger,
|
|
948
|
+
policy,
|
|
949
|
+
providers: ctx.providers,
|
|
950
|
+
cwd: ctx.cwd,
|
|
951
|
+
sandbox: ctx.sandbox,
|
|
952
|
+
timeoutMs: ctx.timeoutMs,
|
|
953
|
+
...(priorHistory.length > 0 ? { history: priorHistory } : {}),
|
|
954
|
+
...(Object.keys(availableModels).length > 0 ? { availableModels } : {}),
|
|
955
|
+
...(authenticatedProviders.length > 0 ? { authenticatedProviders } : {}),
|
|
956
|
+
};
|
|
905
957
|
};
|
|
958
|
+
const deps = buildDeps();
|
|
906
959
|
const ac = new AbortController();
|
|
907
960
|
currentAc = ac;
|
|
908
961
|
const result = await runTask(line, deps, out, ac.signal);
|
|
909
962
|
currentAc = null;
|
|
910
|
-
// Check for
|
|
911
|
-
// while runTask was awaited — the abort fires and the flag is set).
|
|
963
|
+
// Check for SIGINT-driven signals that fired while runTask was awaited.
|
|
912
964
|
if (shouldExit) {
|
|
913
965
|
loopResult = 'exit';
|
|
914
966
|
break;
|
|
915
967
|
}
|
|
968
|
+
// Bug 3 fix: shouldMenu may have been set by a 2×Ctrl+C during the task.
|
|
969
|
+
if (shouldMenu) {
|
|
970
|
+
loopResult = 'menu';
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
916
973
|
// Inline re-login on auth failure: offer to sign in and retry once.
|
|
917
974
|
if (result.final !== undefined &&
|
|
918
975
|
!result.final.success &&
|
|
@@ -924,16 +981,23 @@ async function runChatLoop(ctx, mutableCtx, convId, out, readLine, loginFn, dete
|
|
|
924
981
|
const ans = await readLine();
|
|
925
982
|
if (parseYesNo(ans, true)) {
|
|
926
983
|
await loginFn(out, failingProvider, { readLine });
|
|
984
|
+
// Bug 5 fix: re-detect with the freshly-authenticated env so the retry
|
|
985
|
+
// deps reflect the now-signed-in provider (not the stale pre-login state).
|
|
927
986
|
mutableCtx.env = await detectEnvironmentFn();
|
|
987
|
+
const retryDeps = buildDeps();
|
|
928
988
|
// Retry the same task once.
|
|
929
989
|
const retryAc = new AbortController();
|
|
930
990
|
currentAc = retryAc;
|
|
931
|
-
const retryResult = await runTask(line,
|
|
991
|
+
const retryResult = await runTask(line, retryDeps, out, retryAc.signal);
|
|
932
992
|
currentAc = null;
|
|
933
993
|
if (shouldExit) {
|
|
934
994
|
loopResult = 'exit';
|
|
935
995
|
break;
|
|
936
996
|
}
|
|
997
|
+
if (shouldMenu) {
|
|
998
|
+
loopResult = 'menu';
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
937
1001
|
// If still auth failure after retry, inform and continue to prompt.
|
|
938
1002
|
if (retryResult.final !== undefined &&
|
|
939
1003
|
!retryResult.final.success &&
|
|
@@ -1082,13 +1146,15 @@ export async function startMenu(ctx, out) {
|
|
|
1082
1146
|
if (checkForUpdateFn !== undefined) {
|
|
1083
1147
|
updateInfo = await checkForUpdateFn().catch(() => undefined);
|
|
1084
1148
|
}
|
|
1085
|
-
// ----
|
|
1149
|
+
// ---- Auto-update at launch (default ON) ----------------------------------
|
|
1086
1150
|
// Guard: only runs once; requires both the update and relaunch seams to be wired.
|
|
1087
|
-
|
|
1151
|
+
// Disabled when MYSHELL_NO_UPDATE is set in the environment or autoUpdate===false.
|
|
1152
|
+
if (autoUpdateEnabled(mutableCtx.config, process.env) &&
|
|
1088
1153
|
updateInfo?.updateAvailable === true &&
|
|
1089
1154
|
updateInfo.latest !== null &&
|
|
1090
1155
|
updateSelfFn !== undefined) {
|
|
1091
|
-
out.write(`▲ Auto-updating ${updateInfo.current} → ${updateInfo.latest}
|
|
1156
|
+
out.write(`▲ Auto-updating ${updateInfo.current} → ${updateInfo.latest}…` +
|
|
1157
|
+
` (disable: Settings → Auto-update, or MYSHELL_NO_UPDATE=1)\n`);
|
|
1092
1158
|
const ok = await updateSelfFn(out).catch(() => false);
|
|
1093
1159
|
if (ok) {
|
|
1094
1160
|
if (relaunchFn !== undefined) {
|