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.
@@ -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
- * On startup, `applyStoredCredentials` injects the token into `process.env` so
9
- * that both the provider detection (`claude auth status`) and the spawned
10
- * `claude -p …` child process see it via the inherited environment.
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
- await mkdir(dir, { recursive: true });
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 uses rename ensures no partial writes on crash.
82
- await atomicWrite(path, JSON.stringify(updated, null, 2));
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.includes('sk-ant-oat'))
264
+ if (s.startsWith('sk-ant-oat'))
214
265
  return 'oauth-token';
215
- if (s.includes('sk-ant-api'))
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":"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"}
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/infra/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAU1C,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E,SAAS,iBAAiB,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;AAC3D,CAAC;AAED,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E;;;GAGG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAClD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,GAAG,GAAG,MAAiC,CAAC;QAC9C,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,IAAI,OAAO,GAAG,CAAC,kBAAkB,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtF,MAAM,CAAC,gBAAgB,GAAG,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgB;IACpD,MAAM,IAAI,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QAC7D,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAgB;IACpD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QAC7C,OAAO,KAAK,CAAC,gBAAgB,IAAI,IAAI,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,SAAS,CACvB,OAA0B,EAC1B,KAAoB;IAEpB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,OAAO,CAAC,yBAAyB,CAAC,KAAK,SAAS,EAAE,CAAC;QACrD,0DAA0D;QAC1D,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,EAAE,GAAG,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,CAAC;AACxD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAAa,EAAE,OAAgB;IACnE,MAAM,IAAI,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;IAClC,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAEtC,2EAA2E;IAC3E,qEAAqE;IACrE,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAEnD,iFAAiF;IACjF,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,OAAO,GAAgB,EAAE,GAAG,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC;IAEtE,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAEjE,2EAA2E;IAC3E,iEAAiE;IACjE,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,iDAAiD;IACnD,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAgB;IACrD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAEtC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEtC,qDAAqD;QACrD,IAAI,MAAM,GAA4B,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;YAC1C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBAClD,MAAM,GAAG,MAAiC,CAAC;YAC7C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,6CAA6C;QAC/C,CAAC;QAED,OAAO,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAClC,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,GAAsB,EACtB,OAAgB;IAEhB,IAAI,CAAC;QACH,+DAA+D;QAC/D,IAAI,GAAG,CAAC,yBAAyB,CAAC,KAAK,SAAS,EAAE,CAAC;YACjD,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QAC7C,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACzC,GAAG,CAAC,yBAAyB,CAAC,GAAG,KAAK,CAAC,gBAAgB,CAAC;QAC1D,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,mDAAmD;AACnD,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QAC/D,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAW;IAClD,IAAI,CAAC;QACH,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACnB,IACE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EACtC,CAAC;YACD,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,oBAAoB,CAAC,CAAS;IAC5C,IAAI,CAAC;QACH,IAAI,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,OAAO,aAAa,CAAC;QACrD,IAAI,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,OAAO,SAAS,CAAC;QACjD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC;IAChB,CAAC;AACH,CAAC"}
@@ -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.
@@ -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
- lines.push(`✅ ${ps.id}: ready${planSuffix}`);
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 / default-shell options ----------------------------------------
350
- out.write('\n');
351
- out.write(' [c] Customize mode\n');
352
- out.write(' [Enter] Continue\n\n');
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 (key === null) {
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 updated = mutableConfig;
366
- if (key === 'c') {
367
- updated = await runModeSelect(updated, out, readLine);
368
- }
369
- // [Enter] or anything else → fall through to save & go
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 NO for auto-update require explicit 'y' to enable.
375
- out.write('Keep myshell-tools up to date automatically? (y/N) ');
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, false);
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: true } : {}),
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 === true ? 'on' : 'off'}`,
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
- const enable = config.autoUpdate !== true;
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: true } : {}),
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' signal is communicated from the SIGINT handler to the main
781
- // loop via this flag (the handler can't break the outer while directly).
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
- // Signal the main loop; the loop checks this flag after each await.
804
- shouldExit = false;
805
- // We can't directly break the loop from an event handler, so we set a
806
- // flag and resolve any pending readLine by closing (the loop checks the
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('myshell-tools> ');
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 per-provider advertised model sets from the live env so route()
870
- // can prefer a model the CLI actually advertises. Only include installed
871
- // providers (exactOptionalPropertyTypes is ON).
872
- // Use mutableCtx.env (not ctx.env) so post-login re-detect is reflected.
873
- const menuAvailableModels = {};
874
- if (mutableCtx.env.claude.installed && mutableCtx.env.claude.availableModels.length > 0) {
875
- menuAvailableModels['claude'] = mutableCtx.env.claude.availableModels;
876
- }
877
- if (mutableCtx.env.codex.installed && mutableCtx.env.codex.availableModels.length > 0) {
878
- menuAvailableModels['codex'] = mutableCtx.env.codex.availableModels;
879
- }
880
- if (mutableCtx.env.opencode.installed && mutableCtx.env.opencode.availableModels.length > 0) {
881
- menuAvailableModels['opencode'] = mutableCtx.env.opencode.availableModels;
882
- }
883
- // Collect authenticated providers from the live env so route() prefers
884
- // signed-in providers over signed-out ones. Uses mutableCtx.env so
885
- // post-login re-detection is reflected without restart.
886
- const menuAuthenticatedProviders = [];
887
- if (mutableCtx.env.claude.authenticated)
888
- menuAuthenticatedProviders.push('claude');
889
- if (mutableCtx.env.codex.authenticated)
890
- menuAuthenticatedProviders.push('codex');
891
- if (mutableCtx.env.opencode.authenticated)
892
- menuAuthenticatedProviders.push('opencode');
893
- const deps = {
894
- clock: ctx.clock,
895
- session: ctx.store.writer(convId),
896
- ledger: ctx.ledger,
897
- policy,
898
- providers: ctx.providers,
899
- cwd: ctx.cwd,
900
- sandbox: ctx.sandbox,
901
- timeoutMs: ctx.timeoutMs,
902
- ...(priorHistory.length > 0 ? { history: priorHistory } : {}),
903
- ...(Object.keys(menuAvailableModels).length > 0 ? { availableModels: menuAvailableModels } : {}),
904
- ...(menuAuthenticatedProviders.length > 0 ? { authenticatedProviders: menuAuthenticatedProviders } : {}),
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 a loopBreaker signal that fired during the task (e.g. 3×Ctrl+C
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, deps, out, retryAc.signal);
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
- // ---- Opt-in auto-update at launch ----------------------------------------
1149
+ // ---- Auto-update at launch (default ON) ----------------------------------
1086
1150
  // Guard: only runs once; requires both the update and relaunch seams to be wired.
1087
- if (mutableCtx.config.autoUpdate === true &&
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}…\n`);
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) {