myshell-tools 2.3.0 → 2.6.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +26 -10
  3. package/dist/cli.js +43 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/cost.js +4 -1
  6. package/dist/commands/cost.js.map +1 -1
  7. package/dist/commands/doctor.js +2 -2
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/install.d.ts +66 -0
  10. package/dist/commands/install.js +174 -0
  11. package/dist/commands/install.js.map +1 -0
  12. package/dist/commands/login.d.ts +41 -2
  13. package/dist/commands/login.js +116 -11
  14. package/dist/commands/login.js.map +1 -1
  15. package/dist/core/assess.js +2 -62
  16. package/dist/core/assess.js.map +1 -1
  17. package/dist/core/budget.d.ts +26 -0
  18. package/dist/core/budget.js +37 -0
  19. package/dist/core/budget.js.map +1 -0
  20. package/dist/core/history.d.ts +35 -0
  21. package/dist/core/history.js +116 -0
  22. package/dist/core/history.js.map +1 -0
  23. package/dist/core/json-envelope.d.ts +49 -0
  24. package/dist/core/json-envelope.js +117 -0
  25. package/dist/core/json-envelope.js.map +1 -0
  26. package/dist/core/orchestrate.js +107 -8
  27. package/dist/core/orchestrate.js.map +1 -1
  28. package/dist/core/policy.js +17 -9
  29. package/dist/core/policy.js.map +1 -1
  30. package/dist/core/prompt.d.ts +9 -4
  31. package/dist/core/prompt.js +14 -5
  32. package/dist/core/prompt.js.map +1 -1
  33. package/dist/core/review.js +2 -49
  34. package/dist/core/review.js.map +1 -1
  35. package/dist/core/route.d.ts +13 -5
  36. package/dist/core/route.js +20 -6
  37. package/dist/core/route.js.map +1 -1
  38. package/dist/core/types.d.ts +37 -0
  39. package/dist/infra/pricing.d.ts +17 -4
  40. package/dist/infra/pricing.js +73 -3
  41. package/dist/infra/pricing.js.map +1 -1
  42. package/dist/interface/menu.d.ts +12 -0
  43. package/dist/interface/menu.js +110 -25
  44. package/dist/interface/menu.js.map +1 -1
  45. package/dist/providers/detect.d.ts +17 -5
  46. package/dist/providers/detect.js +56 -4
  47. package/dist/providers/detect.js.map +1 -1
  48. package/dist/providers/install.js +1 -0
  49. package/dist/providers/install.js.map +1 -1
  50. package/dist/providers/opencode-parse.d.ts +49 -0
  51. package/dist/providers/opencode-parse.js +181 -0
  52. package/dist/providers/opencode-parse.js.map +1 -0
  53. package/dist/providers/opencode.d.ts +43 -0
  54. package/dist/providers/opencode.js +121 -0
  55. package/dist/providers/opencode.js.map +1 -0
  56. package/dist/providers/port.d.ts +1 -1
  57. package/dist/providers/registry.d.ts +2 -2
  58. package/dist/providers/registry.js +6 -2
  59. package/dist/providers/registry.js.map +1 -1
  60. package/package.json +2 -2
@@ -15,16 +15,24 @@ import { getCheapestForTier } from '../infra/pricing.js';
15
15
  * Algorithm:
16
16
  * 1. Walk `policy.providerOrderByTier[tier]` in order.
17
17
  * 2. For the first provider that is present in `available`, resolve the
18
- * cheapest model for that provider+tier via `getCheapestForTier`.
18
+ * cheapest model for that provider+tier via `getCheapestForTier`, further
19
+ * restricted to `availableModels[provider]` when that set is non-empty.
19
20
  * 3. If none of the policy-preferred providers are available but `available`
20
21
  * is non-empty, fall back to the globally cheapest model for that tier.
21
22
  * 4. If `available` is empty, throw — there is nothing to route to.
22
23
  *
23
- * @param tier - The orchestration tier to route.
24
- * @param available - Provider IDs that are currently reachable.
25
- * @param policy - Active routing policy (from `DEFAULT_POLICY` or overrides).
24
+ * The `availableModels` parameter is additive/opt-in:
25
+ * - When absent or undefined behaviour is IDENTICAL to today (no change).
26
+ * - When a provider's entry is present and non-empty prefer a model in that
27
+ * list; if none match the pricing table, fall back to cheapest-for-tier
28
+ * (graceful degradation, never throws, never returns nothing).
29
+ *
30
+ * @param tier - The orchestration tier to route.
31
+ * @param available - Provider IDs that are currently reachable.
32
+ * @param policy - Active routing policy (from `DEFAULT_POLICY` or overrides).
33
+ * @param availableModels - Optional per-provider advertised model sets from detection.
26
34
  */
27
- export function route(tier, available, policy) {
35
+ export function route(tier, available, policy, availableModels) {
28
36
  if (available.length === 0) {
29
37
  throw new Error(`route: no providers available for tier "${tier}" — start at least one provider`);
30
38
  }
@@ -32,7 +40,13 @@ export function route(tier, available, policy) {
32
40
  // Walk the preferred order and pick the first available provider.
33
41
  for (const preferred of preferredOrder) {
34
42
  if (available.includes(preferred)) {
35
- const pricing = getCheapestForTier(tier, [preferred]);
43
+ // When advertised models are supplied for this provider, pass them as
44
+ // the allowed-model filter so we prefer a model the CLI actually has.
45
+ const providerAllowed = availableModels?.[preferred];
46
+ const allowedSet = providerAllowed !== undefined && providerAllowed.length > 0
47
+ ? providerAllowed
48
+ : undefined;
49
+ const pricing = getCheapestForTier(tier, [preferred], allowedSet);
36
50
  return {
37
51
  tier,
38
52
  provider: preferred,
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/core/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,KAAK,CACnB,IAAU,EACV,SAAuB,EACvB,MAAc;IAEd,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,2CAA2C,IAAI,iCAAiC,CACjF,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAExD,kEAAkE;IAClE,KAAK,MAAM,SAAS,IAAI,cAAc,EAAE,CAAC;QACvC,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;YACtD,OAAO;gBACL,IAAI;gBACJ,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE,OAAO,CAAC,KAAK;aACrB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,0DAA0D;IAC1D,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,QAAQ,CAAC,QAAsB;QACzC,KAAK,EAAE,QAAQ,CAAC,KAAK;KACtB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/core/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,KAAK,CACnB,IAAU,EACV,SAAuB,EACvB,MAAc,EACd,eAAgE;IAEhE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,2CAA2C,IAAI,iCAAiC,CACjF,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAExD,kEAAkE;IAClE,KAAK,MAAM,SAAS,IAAI,cAAc,EAAE,CAAC;QACvC,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAClC,sEAAsE;YACtE,sEAAsE;YACtE,MAAM,eAAe,GAAG,eAAe,EAAE,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,UAAU,GACd,eAAe,KAAK,SAAS,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC;gBACzD,CAAC,CAAC,eAAe;gBACjB,CAAC,CAAC,SAAS,CAAC;YAChB,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,CAAC;YAClE,OAAO;gBACL,IAAI;gBACJ,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE,OAAO,CAAC,KAAK;aACrB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,0DAA0D;IAC1D,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,QAAQ,CAAC,QAAsB;QACzC,KAAK,EAAE,QAAQ,CAAC,KAAK;KACtB,CAAC;AACJ,CAAC"}
@@ -85,6 +85,25 @@ export interface Policy {
85
85
  readonly escalateBelowConfidence: Record<Risk, number>;
86
86
  /** Ordered provider preference per tier; route() honours availability. */
87
87
  readonly providerOrderByTier: Record<Tier, readonly ProviderId[]>;
88
+ /**
89
+ * Controls when cross-vendor review runs automatically.
90
+ *
91
+ * - `'auto'` : review when risk is high/critical OR the model sets needsReview
92
+ * (current default behaviour).
93
+ * - `'critical-only'` : review only when risk is `critical` (or needsReview AND critical).
94
+ * - `'off'` : never trigger an automatic cross-vendor review.
95
+ *
96
+ * Omitting the field is equivalent to `'auto'` (backward-compatible).
97
+ */
98
+ readonly reviewPolicy?: 'auto' | 'critical-only' | 'off';
99
+ /**
100
+ * Per-task cost budget cap in USD. When `totalCostUsd` reaches or exceeds
101
+ * this value, orchestrate() stops spending (no new escalation, no new review)
102
+ * and accepts the best result produced so far.
103
+ *
104
+ * `null` or `undefined` (the default) means no cap is applied.
105
+ */
106
+ readonly maxCostUsd?: number | null;
88
107
  }
89
108
  export interface OrchestrateDeps {
90
109
  /** Available providers, keyed by id. Absent key = provider unavailable. */
@@ -96,6 +115,24 @@ export interface OrchestrateDeps {
96
115
  readonly cwd: string;
97
116
  readonly sandbox: SandboxLevel;
98
117
  readonly timeoutMs: number;
118
+ /**
119
+ * Prior conversation history for context continuity. When provided, the most
120
+ * recent turns are compacted and injected into the first provider prompt so
121
+ * stateless one-shot providers (claude -p / codex exec) have multi-turn
122
+ * awareness. Leave undefined for fresh (one-shot) sessions.
123
+ */
124
+ readonly history?: readonly SessionEntry[];
125
+ /**
126
+ * Advertised model lists from provider detection, keyed by provider id.
127
+ * When supplied, route() restricts candidates to models that the provider CLI
128
+ * actually advertises, preventing the CLI from routing to a model it cannot run.
129
+ *
130
+ * Absence (undefined) or an empty list for a provider → fall back to the
131
+ * standard cheapest-for-tier pricing-table behaviour (backward-compatible).
132
+ *
133
+ * Only include providers that are installed; exactOptionalPropertyTypes is ON.
134
+ */
135
+ readonly availableModels?: Partial<Record<ProviderId, readonly string[]>>;
99
136
  }
100
137
  /**
101
138
  * High-level events emitted by orchestrate(). The interface/render layer
@@ -10,7 +10,7 @@
10
10
  * Captured : 2026-05-29
11
11
  */
12
12
  export interface ModelPricing {
13
- readonly provider: 'claude' | 'codex';
13
+ readonly provider: 'claude' | 'codex' | 'opencode';
14
14
  readonly model: string;
15
15
  readonly aliases: readonly string[];
16
16
  readonly tier: 'worker' | 'ic' | 'manager';
@@ -35,11 +35,24 @@ export declare function getModelPricing(provider: string, model: string): ModelP
35
35
  export declare function calculateCost(inputTokens: number, outputTokens: number, pricing: ModelPricing): number;
36
36
  /**
37
37
  * Return the cheapest model (lowest inputPer1M) for a given tier,
38
- * optionally restricted to the supplied provider IDs.
38
+ * optionally restricted to the supplied provider IDs and/or an allowed-model set.
39
39
  *
40
- * Throws if no matching model exists.
40
+ * When `allowedModels` is supplied and non-empty for the relevant provider(s),
41
+ * only models whose `model` id or any alias appears in `allowedModels` are
42
+ * considered. The match is case-insensitive (mirrors getModelPricing behaviour).
43
+ * If no candidates survive the allowed-model filter, the filter is ignored and
44
+ * the full provider-scoped set is used (graceful degradation — never throws due
45
+ * to a missing advertised model).
46
+ *
47
+ * Throws if no matching model exists (no tier entries at all, or no entries for
48
+ * the given providers).
49
+ *
50
+ * @param tier - Orchestration tier to select for.
51
+ * @param availableProviders - Restrict to these provider IDs when supplied.
52
+ * @param allowedModels - Further restrict to models advertised by the CLI.
53
+ * The set contains model IDs and/or aliases (any case).
41
54
  */
42
- export declare function getCheapestForTier(tier: 'worker' | 'ic' | 'manager', availableProviders?: string[]): ModelPricing;
55
+ export declare function getCheapestForTier(tier: 'worker' | 'ic' | 'manager', availableProviders?: string[], allowedModels?: readonly string[]): ModelPricing;
43
56
  /**
44
57
  * Returns true when the pricing table is older than maxAgeDays (default 90).
45
58
  * Useful for emitting a staleness warning at runtime.
@@ -17,6 +17,7 @@ export const PRICING_TABLE = {
17
17
  sourceUrls: [
18
18
  'https://www.anthropic.com/pricing',
19
19
  'https://platform.openai.com/docs/pricing',
20
+ 'https://opencode.ai/docs',
20
21
  ],
21
22
  models: [
22
23
  // ---- Anthropic / Claude ------------------------------------------------
@@ -93,6 +94,48 @@ export const PRICING_TABLE = {
93
94
  outputPer1M: 14,
94
95
  contextWindow: 128_000,
95
96
  },
97
+ // ---- opencode ----------------------------------------------------------
98
+ // opencode ships free models whose real cost is reported at runtime via the
99
+ // step_finish `cost` field in JSONL output (see opencode.ts). The zero-cost
100
+ // entries below exist solely for model SELECTION by getCheapestForTier/route
101
+ // when opencode is the only available provider. They must NOT displace
102
+ // claude/codex in the pricing sort when those providers are also available,
103
+ // because opencode's cost=0 entries would always win. The route() function
104
+ // respects providerOrderByTier (opencode last) before falling back to the
105
+ // pricing table, so this zero-cost sentinel is safe.
106
+ {
107
+ provider: 'opencode',
108
+ model: 'opencode/mimo-v2.5-free',
109
+ aliases: ['mimo-v2.5-free', 'opencode-worker'],
110
+ tier: 'worker',
111
+ // Real cost is reported at runtime by opencode's step_finish event.
112
+ // Zero here is a placeholder for model selection only — not for billing.
113
+ inputPer1M: 0,
114
+ outputPer1M: 0,
115
+ contextWindow: 32_000,
116
+ },
117
+ {
118
+ provider: 'opencode',
119
+ model: 'opencode/deepseek-v4-flash-free',
120
+ aliases: ['deepseek-v4-flash-free', 'opencode-free'],
121
+ tier: 'ic',
122
+ // Real cost is reported at runtime by opencode's step_finish event.
123
+ // Zero here is a placeholder for model selection only — not for billing.
124
+ inputPer1M: 0,
125
+ outputPer1M: 0,
126
+ contextWindow: 128_000,
127
+ },
128
+ {
129
+ provider: 'opencode',
130
+ model: 'opencode/big-pickle',
131
+ aliases: ['big-pickle', 'opencode-manager'],
132
+ tier: 'manager',
133
+ // Real cost is reported at runtime by opencode's step_finish event.
134
+ // Zero here is a placeholder for model selection only — not for billing.
135
+ inputPer1M: 0,
136
+ outputPer1M: 0,
137
+ contextWindow: 128_000,
138
+ },
96
139
  ],
97
140
  };
98
141
  // ---------------------------------------------------------------------------
@@ -118,11 +161,24 @@ export function calculateCost(inputTokens, outputTokens, pricing) {
118
161
  }
119
162
  /**
120
163
  * Return the cheapest model (lowest inputPer1M) for a given tier,
121
- * optionally restricted to the supplied provider IDs.
164
+ * optionally restricted to the supplied provider IDs and/or an allowed-model set.
122
165
  *
123
- * Throws if no matching model exists.
166
+ * When `allowedModels` is supplied and non-empty for the relevant provider(s),
167
+ * only models whose `model` id or any alias appears in `allowedModels` are
168
+ * considered. The match is case-insensitive (mirrors getModelPricing behaviour).
169
+ * If no candidates survive the allowed-model filter, the filter is ignored and
170
+ * the full provider-scoped set is used (graceful degradation — never throws due
171
+ * to a missing advertised model).
172
+ *
173
+ * Throws if no matching model exists (no tier entries at all, or no entries for
174
+ * the given providers).
175
+ *
176
+ * @param tier - Orchestration tier to select for.
177
+ * @param availableProviders - Restrict to these provider IDs when supplied.
178
+ * @param allowedModels - Further restrict to models advertised by the CLI.
179
+ * The set contains model IDs and/or aliases (any case).
124
180
  */
125
- export function getCheapestForTier(tier, availableProviders) {
181
+ export function getCheapestForTier(tier, availableProviders, allowedModels) {
126
182
  let candidates = PRICING_TABLE.models.filter((m) => m.tier === tier);
127
183
  if (availableProviders && availableProviders.length > 0) {
128
184
  candidates = candidates.filter((m) => availableProviders.includes(m.provider));
@@ -131,6 +187,20 @@ export function getCheapestForTier(tier, availableProviders) {
131
187
  throw new Error(`No models available for tier "${tier}"` +
132
188
  (availableProviders ? ` with providers [${availableProviders.join(', ')}]` : ''));
133
189
  }
190
+ // When an allowed-model set is provided and non-empty, further restrict
191
+ // candidates to those whose model id or any alias appears in the set.
192
+ // Case-insensitive to match getModelPricing behaviour.
193
+ if (allowedModels !== undefined && allowedModels.length > 0) {
194
+ const allowed = new Set(allowedModels.map((a) => a.toLowerCase()));
195
+ const filtered = candidates.filter((m) => allowed.has(m.model.toLowerCase()) ||
196
+ m.aliases.some((a) => allowed.has(a.toLowerCase())));
197
+ // Graceful degradation: if the filter eliminates all candidates (e.g. the
198
+ // provider advertised a model not yet in our pricing table), fall back to the
199
+ // full provider-scoped set — never throw, never return nothing.
200
+ if (filtered.length > 0) {
201
+ candidates = filtered;
202
+ }
203
+ }
134
204
  // Primary sort: inputPer1M ascending; secondary: outputPer1M ascending
135
205
  return candidates.reduce((cheapest, m) => m.inputPer1M < cheapest.inputPer1M ||
136
206
  (m.inputPer1M === cheapest.inputPer1M && m.outputPer1M < cheapest.outputPer1M)
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../src/infra/pricing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../src/infra/pricing.ts"],"names":[],"mappings":"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"}
@@ -41,6 +41,18 @@ export interface MenuContext {
41
41
  * Returns the next trimmed line of input, or `null` on EOF/close.
42
42
  */
43
43
  readonly readLine?: () => Promise<string | null>;
44
+ /**
45
+ * Optional injected installProvider for testing. When provided, `startMenu`
46
+ * uses this instead of the real `installProvider` from providers/install.ts,
47
+ * preventing real `npm install -g …` subprocesses from spawning during tests.
48
+ */
49
+ readonly installProvider?: (id: ProviderId, out: OutputSink) => Promise<boolean>;
50
+ /**
51
+ * Optional injected login function for testing. When provided, `startMenu`
52
+ * uses this instead of the real `runLogin` from commands/login.ts, preventing
53
+ * real `claude`/`codex login` subprocesses from spawning during tests.
54
+ */
55
+ readonly login?: (out: OutputSink, providerArg?: string) => Promise<number>;
44
56
  }
45
57
  /**
46
58
  * Return the shell alias hint the user can add to their shell profile to make
@@ -28,6 +28,7 @@ import { runTask } from './run.js';
28
28
  import { runLogin } from '../commands/login.js';
29
29
  import { runDoctor } from '../commands/doctor.js';
30
30
  import { runCost } from '../commands/cost.js';
31
+ import { runInstall } from '../commands/install.js';
31
32
  import { box, separator, menu, prompt } from '../ui/tui.js';
32
33
  // ---------------------------------------------------------------------------
33
34
  // Pure helpers — exported for unit tests
@@ -104,6 +105,18 @@ export function renderHeaderLines(env, _version) {
104
105
  lines.push(`⚠️ ${ps.id}: not signed in${planSuffix}`);
105
106
  }
106
107
  }
108
+ // opencode: only show when installed (never nag users who only use claude/codex).
109
+ // opencode is authenticated-when-installed (free models, no credentials required).
110
+ if (env.opencode.installed) {
111
+ const ps = env.opencode;
112
+ const planSuffix = ps.plan != null ? ` (${ps.plan})` : '';
113
+ if (ps.authenticated) {
114
+ lines.push(`✅ ${ps.id}: ready${planSuffix}`);
115
+ }
116
+ else {
117
+ lines.push(`⚠️ ${ps.id}: not signed in${planSuffix}`);
118
+ }
119
+ }
107
120
  return lines;
108
121
  }
109
122
  /**
@@ -197,7 +210,7 @@ function createLineReader(rl) {
197
210
  // ---------------------------------------------------------------------------
198
211
  // Welcome screen (first run)
199
212
  // ---------------------------------------------------------------------------
200
- async function runWelcome(ctx, out, readLine, mutableConfig) {
213
+ async function runWelcome(ctx, out, readLine, mutableConfig, installProviderFn, loginFn) {
201
214
  // Use the mutable env so re-detection after installs is visible downstream.
202
215
  let env = ctx.env;
203
216
  const headerLines = renderHeaderLines(env, ctx.version);
@@ -217,7 +230,7 @@ async function runWelcome(ctx, out, readLine, mutableConfig) {
217
230
  // EOF or 'n'/'no' → skip; anything else (including '') → yes
218
231
  const skip = ans === null || ans.toLowerCase() === 'n' || ans.toLowerCase() === 'no';
219
232
  if (!skip) {
220
- const ok = await installProvider(id, out);
233
+ const ok = await installProviderFn(id, out);
221
234
  if (ok) {
222
235
  didInstallAny = true;
223
236
  }
@@ -240,7 +253,9 @@ async function runWelcome(ctx, out, readLine, mutableConfig) {
240
253
  const ans = await readLine();
241
254
  const skip = ans === null || ans.toLowerCase() === 'n' || ans.toLowerCase() === 'no';
242
255
  if (!skip) {
243
- await runLogin(out, id);
256
+ // loginFn auto-detects the right method (code in containers/SSH where the
257
+ // localhost OAuth callback can't be reached, browser on a desktop).
258
+ await loginFn(out, id);
244
259
  }
245
260
  }
246
261
  // ---- Mode / default-shell options ----------------------------------------
@@ -274,10 +289,10 @@ async function runWelcome(ctx, out, readLine, mutableConfig) {
274
289
  ...(updated.mode !== undefined ? { mode: updated.mode } : {}),
275
290
  };
276
291
  await saveConfig(saved);
277
- // When the user opted in, print the alias hint so the prompt is honest.
292
+ // When the user opts in, actually write the shell startup hook (real install,
293
+ // not just a hint). runInstall reports what it wrote and how to reverse.
278
294
  if (setAsDefault) {
279
- const hint = defaultAliasHint(process.env['SHELL'], process.platform);
280
- out.write('\n[info] To make myshell-tools your default, ' + hint + '\n\n');
295
+ await runInstall(out);
281
296
  }
282
297
  return saved;
283
298
  }
@@ -314,8 +329,48 @@ async function runModeSelect(config, out, readLine) {
314
329
  out.write(`Mode set to: ${newMode ?? 'balanced'}\n`);
315
330
  return updated;
316
331
  }
317
- async function runSettings(ctx, mutableCtx, out, readLine) {
318
- mutableCtx.config = await runModeSelect(mutableCtx.config, out, readLine);
332
+ /**
333
+ * Toggle the "set as default shell" preference and actually install/uninstall
334
+ * the shell startup hook to match. The config flag is only flipped when the
335
+ * hook write succeeds, so the stored value never lies about the real state.
336
+ */
337
+ async function toggleDefaultShell(config, out) {
338
+ const enable = !config.setAsDefault;
339
+ // runInstall reports exactly what it wrote (or removed) and how to reverse.
340
+ const code = await runInstall(out, enable ? undefined : { uninstall: true });
341
+ // Only adopt the new state if the hook write succeeded; otherwise keep the old.
342
+ const setAsDefault = code === 0 ? enable : config.setAsDefault;
343
+ const updated = {
344
+ onboarded: config.onboarded,
345
+ setAsDefault,
346
+ ...(config.mode !== undefined ? { mode: config.mode } : {}),
347
+ };
348
+ await saveConfig(updated);
349
+ return updated;
350
+ }
351
+ async function runSettings(_ctx, mutableCtx, out, readLine) {
352
+ const cfg = mutableCtx.config;
353
+ const settingsLines = [
354
+ '',
355
+ ` [1] Mode: ${cfg.mode ?? 'balanced'}`,
356
+ ` [2] Set as default shell: ${cfg.setAsDefault ? 'on' : 'off'}`,
357
+ '',
358
+ ' [Enter] Back',
359
+ '',
360
+ ];
361
+ out.write('\n' + box('Settings', settingsLines) + '\n\n');
362
+ out.write('> ');
363
+ const key = await readLine();
364
+ // EOF or Enter → back, no change
365
+ if (key === null || key.length === 0)
366
+ return;
367
+ if (key === '1') {
368
+ mutableCtx.config = await runModeSelect(mutableCtx.config, out, readLine);
369
+ }
370
+ else if (key === '2') {
371
+ mutableCtx.config = await toggleDefaultShell(mutableCtx.config, out);
372
+ }
373
+ // anything else → back
319
374
  }
320
375
  // ---------------------------------------------------------------------------
321
376
  // Manage conversations screen
@@ -476,28 +531,33 @@ async function runImportNative(ctx, mutableCtx, out, readLine) {
476
531
  // Raw provider passthrough
477
532
  // ---------------------------------------------------------------------------
478
533
  /**
479
- * Launch the native `claude` or `codex` interactive CLI directly (stdio:inherit),
480
- * so the user gets a raw provider session. The session is owned by the native CLI
481
- * (not by myshell-tools); we simply hand over the terminal and wait.
534
+ * Launch the native `claude`, `codex`, or `opencode` interactive CLI directly
535
+ * (stdio:inherit), so the user gets a raw provider session. The session is owned
536
+ * by the native CLI (not by myshell-tools); we simply hand over the terminal and wait.
482
537
  *
483
538
  * On exit (any exit code), control returns to the myshell-tools menu.
484
539
  */
485
- async function runRawProviderSession(out, readLine) {
486
- out.write('\nOpen raw session with:\n [1] Claude\n [2] Codex\n\n> ');
540
+ async function runRawProviderSession(out, readLine, env) {
541
+ // Build the choice list dynamically: opencode only when installed.
542
+ const choices = [
543
+ { label: 'Claude', bin: 'claude' },
544
+ { label: 'Codex', bin: 'codex' },
545
+ ];
546
+ if (env.opencode.installed) {
547
+ choices.push({ label: 'opencode', bin: 'opencode' });
548
+ }
549
+ const choiceLines = choices.map((c, i) => ` [${i + 1}] ${c.label}`).join('\n');
550
+ out.write(`\nOpen raw session with:\n${choiceLines}\n\n> `);
487
551
  const choice = await readLine();
488
552
  if (choice === null)
489
553
  return;
490
- let bin;
491
- if (choice === '1') {
492
- bin = 'claude';
493
- }
494
- else if (choice === '2') {
495
- bin = 'codex';
496
- }
497
- else {
554
+ const idx = parseInt(choice, 10) - 1;
555
+ const selected = choices[idx];
556
+ if (selected === undefined) {
498
557
  out.write('Cancelled.\n');
499
558
  return;
500
559
  }
560
+ const bin = selected.bin;
501
561
  out.write(`\nLaunching ${bin} — press Ctrl+C or type /exit inside ${bin} to return.\n`);
502
562
  // stdio:'inherit' hands the terminal to the native CLI so its interactive
503
563
  // session runs in place. reject:false so we return to menu on any exit code.
@@ -551,6 +611,24 @@ async function runChatLoop(ctx, mutableCtx, convId, out, readLine) {
551
611
  const policy = mutableCtx.config.mode !== undefined
552
612
  ? POLICY_PRESETS[mutableCtx.config.mode]
553
613
  : DEFAULT_POLICY;
614
+ // Load prior history before each turn so the provider receives conversation
615
+ // context. load() returns only the entries persisted so far — the current
616
+ // user turn is appended by orchestrate() after this point, so there is no
617
+ // double-inclusion risk.
618
+ const priorHistory = await ctx.store.load(convId);
619
+ // Build per-provider advertised model sets from the live env so route()
620
+ // can prefer a model the CLI actually advertises. Only include installed
621
+ // providers (exactOptionalPropertyTypes is ON).
622
+ const menuAvailableModels = {};
623
+ if (ctx.env.claude.installed && ctx.env.claude.availableModels.length > 0) {
624
+ menuAvailableModels['claude'] = ctx.env.claude.availableModels;
625
+ }
626
+ if (ctx.env.codex.installed && ctx.env.codex.availableModels.length > 0) {
627
+ menuAvailableModels['codex'] = ctx.env.codex.availableModels;
628
+ }
629
+ if (ctx.env.opencode.installed && ctx.env.opencode.availableModels.length > 0) {
630
+ menuAvailableModels['opencode'] = ctx.env.opencode.availableModels;
631
+ }
554
632
  const deps = {
555
633
  clock: ctx.clock,
556
634
  session: ctx.store.writer(convId),
@@ -560,6 +638,8 @@ async function runChatLoop(ctx, mutableCtx, convId, out, readLine) {
560
638
  cwd: ctx.cwd,
561
639
  sandbox: ctx.sandbox,
562
640
  timeoutMs: ctx.timeoutMs,
641
+ ...(priorHistory.length > 0 ? { history: priorHistory } : {}),
642
+ ...(Object.keys(menuAvailableModels).length > 0 ? { availableModels: menuAvailableModels } : {}),
563
643
  };
564
644
  const ac = new AbortController();
565
645
  currentAc = ac;
@@ -632,6 +712,9 @@ async function renderMainScreen(ctx, mutableCtx, metas, out) {
632
712
  * that EOF resolves gracefully instead of throwing.
633
713
  */
634
714
  export async function startMenu(ctx, out) {
715
+ // Resolve injected seams — use the real implementations when not provided.
716
+ const installProviderFn = ctx.installProvider !== undefined ? ctx.installProvider : installProvider;
717
+ const loginFn = ctx.login !== undefined ? ctx.login : runLogin;
635
718
  // Build the readLine function — either injected (for tests) or backed by a
636
719
  // real readline interface driven by the event-driven LineReader queue.
637
720
  let readLine;
@@ -664,7 +747,7 @@ export async function startMenu(ctx, out) {
664
747
  try {
665
748
  // ---- A. First-run welcome -----------------------------------------------
666
749
  if (!mutableCtx.config.onboarded) {
667
- mutableCtx.config = await runWelcome(ctx, out, readLine, mutableCtx.config);
750
+ mutableCtx.config = await runWelcome(ctx, out, readLine, mutableCtx.config, installProviderFn, loginFn);
668
751
  }
669
752
  // ---- B. Main screen loop -------------------------------------------------
670
753
  while (true) {
@@ -726,18 +809,20 @@ export async function startMenu(ctx, out) {
726
809
  }
727
810
  // ---- [r] Open a raw provider session ------------------------------------
728
811
  if (key === 'r') {
729
- await runRawProviderSession(out, readLine);
812
+ await runRawProviderSession(out, readLine, mutableCtx.env);
730
813
  continue;
731
814
  }
732
815
  // ---- [j] Login Claude ---------------------------------------------------
816
+ // loginFn auto-detects the right sign-in method (code in containers/SSH,
817
+ // browser on a desktop). Force either with `myshell-tools login claude --code|--browser`.
733
818
  if (key === 'j') {
734
- await runLogin(out, 'claude');
819
+ await loginFn(out, 'claude');
735
820
  mutableCtx.env = await detectEnvironment();
736
821
  continue;
737
822
  }
738
823
  // ---- [k] Login Codex ----------------------------------------------------
739
824
  if (key === 'k') {
740
- await runLogin(out, 'codex');
825
+ await loginFn(out, 'codex');
741
826
  mutableCtx.env = await detectEnvironment();
742
827
  continue;
743
828
  }