claudish 14.2.0 → 14.4.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/dist/index.js CHANGED
@@ -575,7 +575,7 @@ var init_onepassword_config = __esm(() => {
575
575
  });
576
576
 
577
577
  // src/version.ts
578
- var VERSION = "14.2.0";
578
+ var VERSION = "14.4.0";
579
579
 
580
580
  // src/redact.ts
581
581
  function isPlaceholder(value) {
@@ -40453,6 +40453,20 @@ var init_claude_sse_sink = __esm(() => {
40453
40453
  init_redact();
40454
40454
  });
40455
40455
 
40456
+ // src/in-band-api-error.ts
40457
+ function formatInBandApiError(code, message) {
40458
+ return `
40459
+
40460
+ [API Error: ${code} ${message}]`;
40461
+ }
40462
+ function parseInBandApiError(answer, opts = {}) {
40463
+ const trimmed = answer.trim();
40464
+ const m = /^\[API Error: (\S+)([\s\S]*)\]$/.exec(trimmed) ?? (opts.trailing ? /\n\n\[API Error: (\S+)([^\n\]]*)\]$/.exec(trimmed) : null);
40465
+ if (!m)
40466
+ return null;
40467
+ return { code: m[1], text: `${m[1]}${m[2]}` };
40468
+ }
40469
+
40456
40470
  // src/handlers/shared/stream-parsers/responses/parallel-wrapper.ts
40457
40471
  function stripFunctionsPrefix(name) {
40458
40472
  return name.startsWith("functions.") ? name.slice("functions.".length) : name;
@@ -41161,9 +41175,7 @@ function sum(values) {
41161
41175
  function apiErrorText(code, message, opts) {
41162
41176
  const isCtxOverflow = code === "context_length_exceeded" || /context (length|window)|exceeds? the context|too long|maximum context/i.test(message);
41163
41177
  if (!isCtxOverflow)
41164
- return `
41165
-
41166
- [API Error: ${code} ${message}]`;
41178
+ return formatInBandApiError(code, message);
41167
41179
  const cap = opts.contextWindow && opts.contextWindow > 0 ? ` ~${Math.round(opts.contextWindow / 1000)}K tokens` : "";
41168
41180
  return `
41169
41181
 
@@ -57687,6 +57699,10 @@ function findCatalogEntry(entries, model) {
57687
57699
  return loweredId;
57688
57700
  return entries.find((entry) => entry.aliases.some((alias) => alias.toLowerCase() === lowered));
57689
57701
  }
57702
+ function catalogModelId(model, cachePath) {
57703
+ const entries = cachePath ? readAllModelsCache(cachePath)?.entries : getCatalogEntries();
57704
+ return entries ? findCatalogEntry(entries, model)?.modelId : undefined;
57705
+ }
57690
57706
  function gatherFromConnections(entry, candidates, excluded, unmappedRoutes, cachePath) {
57691
57707
  for (const connection of entry.aggregators ?? []) {
57692
57708
  if (connection.routeStatus !== "mapped")
@@ -59655,7 +59671,9 @@ function toSlotRow(id, snap, acct, now = Date.now(), upstreamRecords = []) {
59655
59671
  idle_seconds: idle,
59656
59672
  activity: terminal ? null : snap.turnEndRecordMissing && snap.activity ? `${snap.activity}: turn_end_record_missing` : snap.agentCountMismatch && snap.activity ? `${snap.activity}: agent_count_mismatch` : snap.activity,
59657
59673
  pane: snap.paneId || null,
59658
- recent: snap.recent
59674
+ recent: snap.recent,
59675
+ agent_requested: id.agentRequested ?? null,
59676
+ agent: snap.agent ?? null
59659
59677
  };
59660
59678
  }
59661
59679
  var init_slot_row = __esm(() => {
@@ -80167,6 +80185,18 @@ function reservedMessage(flag) {
80167
80185
  return `${flag} would start a different claudish mode inside the pane`;
80168
80186
  return null;
80169
80187
  }
80188
+ function requestedAgentOf(flags) {
80189
+ let agent = null;
80190
+ for (let i = 0;i < flags.length; i++) {
80191
+ const [name, inline] = splitFlag(flags[i]);
80192
+ if (name !== "--agent")
80193
+ continue;
80194
+ const value = inline ?? flags[i + 1];
80195
+ if (value && !value.startsWith("-"))
80196
+ agent = value;
80197
+ }
80198
+ return agent;
80199
+ }
80170
80200
  function flagsRemoveRead(flags) {
80171
80201
  for (let i = 0;i < flags.length; i++) {
80172
80202
  const [name, inline] = splitFlag(flags[i]);
@@ -82466,6 +82496,7 @@ function initialFollowerState(resolvePath = safeRealpath) {
82466
82496
  mainAssistantIds: [],
82467
82497
  mainAssistantSeen: new Set,
82468
82498
  session: { stopHooksSeen: false, provenHookless: false },
82499
+ agentSetting: null,
82469
82500
  records: 0,
82470
82501
  resolvePath
82471
82502
  };
@@ -82790,6 +82821,8 @@ function applyRecord(state, record, offset) {
82790
82821
  if (r.isSidechain)
82791
82822
  return state;
82792
82823
  noteMainAssistant(state, r);
82824
+ if (r.type === "agent-setting" && typeof r.agentSetting === "string" && r.agentSetting)
82825
+ state.agentSetting = r.agentSetting;
82793
82826
  if (r.type === "system" && r.subtype === "stop_hook_summary") {
82794
82827
  state.session.stopHooksSeen = true;
82795
82828
  state.session.provenHookless = false;
@@ -83072,7 +83105,8 @@ class TranscriptFollower {
83072
83105
  usage: { tokensIn, tokensOut },
83073
83106
  toolCalls: this.state.toolUseIds.size,
83074
83107
  lastAppendAt: this.lastAppend,
83075
- session: { ...this.state.session }
83108
+ session: { ...this.state.session },
83109
+ agentSetting: this.state.agentSetting
83076
83110
  };
83077
83111
  }
83078
83112
  }
@@ -85146,7 +85180,8 @@ ${screenTailOf(this.screen)}`
85146
85180
  shape: this.shape,
85147
85181
  turnEndRecordMissing: !terminal && this.phase === "RUNNING" && this.activity === "finishing" && this.turnEndMissing,
85148
85182
  agentCountMismatch: !terminal && this.phase === "RUNNING" && this.agentMismatch !== null,
85149
- recent: cur && cur.recent.length > 0 ? cur.recent : null
85183
+ recent: cur && cur.recent.length > 0 ? cur.recent : null,
85184
+ agent: view.agentSetting
85150
85185
  };
85151
85186
  }
85152
85187
  phaseActivity() {
@@ -86381,14 +86416,16 @@ function neverSpawnedRow(e, m) {
86381
86416
  idle_seconds: null,
86382
86417
  activity: null,
86383
86418
  pane: null,
86384
- recent: null
86419
+ recent: null,
86420
+ agent_requested: e.agentRequested,
86421
+ agent: null
86385
86422
  };
86386
86423
  }
86387
86424
  function liveSlotRow(run, e) {
86388
86425
  if (!e.session)
86389
86426
  return neverSpawnedRow(e, run.status.models[e.id]);
86390
86427
  const snap = e.session.snapshot();
86391
- return toSlotRow({ slot: e.id, model: e.model, spawnModel: e.spawnModel }, snap, refreshAccounting(e, snap), Date.now(), upstreamRecordsFor(snap, upstreamLogPath(run.path, e.id)));
86428
+ return toSlotRow({ slot: e.id, model: e.model, spawnModel: e.spawnModel, agentRequested: e.agentRequested }, snap, refreshAccounting(e, snap), Date.now(), upstreamRecordsFor(snap, upstreamLogPath(run.path, e.id)));
86392
86429
  }
86393
86430
  function outcomeOf(states) {
86394
86431
  const ok = states.filter((s) => s === "COMPLETED").length;
@@ -86445,7 +86482,9 @@ function teamRunRowFromDisk(path, status) {
86445
86482
  idle_seconds: null,
86446
86483
  activity: null,
86447
86484
  pane: m?.pane ?? null,
86448
- recent: null
86485
+ recent: null,
86486
+ agent_requested: m?.agentRequested ?? null,
86487
+ agent: m?.agent ?? null
86449
86488
  };
86450
86489
  });
86451
86490
  const settled = slots.length > 0 && slots.every((s) => isTerminalState(s.state));
@@ -86597,7 +86636,9 @@ function classifyRunOutput(input) {
86597
86636
  return v;
86598
86637
  }
86599
86638
  function classifyTurnSignals(input) {
86600
- const { apiError, stopReason, promptRead } = input;
86639
+ const { stopReason, promptRead } = input;
86640
+ const inBand = input.apiError ? null : parseInBandApiError(input.answer, { trailing: promptRead?.complete === false });
86641
+ const apiError = input.apiError ?? (inBand ? { status: null, text: inBand.text } : null);
86601
86642
  if (apiError) {
86602
86643
  const status = apiError.status === null ? "" : ` (status ${apiError.status})`;
86603
86644
  return {
@@ -86824,6 +86865,7 @@ async function startModelsIn(flight, sessionPath, opts) {
86824
86865
  const flagCheck = checkChildFlags(claudeFlags);
86825
86866
  if (!flagCheck.ok)
86826
86867
  throw new Error(`invalid_args: ${flagCheck.message}`);
86868
+ const agentRequested = requestedAgentOf(claudeFlags);
86827
86869
  const path = resolve5(sessionPath);
86828
86870
  const manifest = JSON.parse(readFileSync34(join52(path, "manifest.json"), "utf-8"));
86829
86871
  const statusPath = join52(path, "status.json");
@@ -86940,6 +86982,7 @@ async function startModelsIn(flight, sessionPath, opts) {
86940
86982
  lastActivityAt: snap.lastActivityAt,
86941
86983
  turnsCompleted: snap.turnsCompleted,
86942
86984
  claudeCodeVersion: snap.claudeCodeVersion,
86985
+ agent: snap.agent ?? null,
86943
86986
  anomalies: notableAnomalies(snap),
86944
86987
  ...accountingFields(refreshAccounting(e, snap))
86945
86988
  };
@@ -87111,6 +87154,7 @@ async function startModelsIn(flight, sessionPath, opts) {
87111
87154
  id,
87112
87155
  model: entry.model,
87113
87156
  spawnModel,
87157
+ agentRequested,
87114
87158
  tokenFile,
87115
87159
  session: null,
87116
87160
  acct: emptyAccounting(provider),
@@ -87129,6 +87173,8 @@ async function startModelsIn(flight, sessionPath, opts) {
87129
87173
  model: entry.model,
87130
87174
  spawnModel,
87131
87175
  provider,
87176
+ agentRequested,
87177
+ agent: null,
87132
87178
  sessionUuid: uuid,
87133
87179
  transcriptPath,
87134
87180
  pane: null,
@@ -87668,6 +87714,8 @@ function sessionRowOf(info, upstreamRecords = []) {
87668
87714
  activity: terminal ? null : info.activity,
87669
87715
  pane: info.pane,
87670
87716
  recent: info.recent,
87717
+ agent_requested: info.agentRequested ?? null,
87718
+ agent: info.agent ?? null,
87671
87719
  session_id: info.sessionId,
87672
87720
  started_at: info.startedAt,
87673
87721
  completed_at: info.completedAt,
@@ -87705,6 +87753,8 @@ function toMetaRecord(info, cwd) {
87705
87753
  captureSource: info.captureSource,
87706
87754
  turnSource: info.turnSource,
87707
87755
  timeoutSeconds: info.timeoutSeconds,
87756
+ agentRequested: info.agentRequested ?? null,
87757
+ agent: info.agent ?? null,
87708
87758
  cwd
87709
87759
  };
87710
87760
  }
@@ -87998,6 +88048,7 @@ class SessionManager {
87998
88048
  const sessionId = opts.sessionId ?? randomUUID8().slice(0, 8);
87999
88049
  const claudeSessionId = randomUUID8();
88000
88050
  const timeout = normaliseTimeoutSeconds(opts.timeoutSeconds);
88051
+ const agentRequested = requestedAgentOf(opts.claudishFlags ?? []);
88001
88052
  const startedAt = new Date().toISOString();
88002
88053
  const sessionDir = opts.sessionDir ?? join54(this.sessionsDir, sessionId);
88003
88054
  mkdirSync23(sessionDir, { recursive: true });
@@ -88011,7 +88062,8 @@ class SessionManager {
88011
88062
  model: opts.model,
88012
88063
  timeoutSeconds: timeout,
88013
88064
  claudeSessionId,
88014
- ...opts.setupFrom ? { setupFrom: opts.setupFrom } : {}
88065
+ ...opts.setupFrom ? { setupFrom: opts.setupFrom } : {},
88066
+ ...agentRequested ? { agentRequested } : {}
88015
88067
  });
88016
88068
  if (inheritedLayer) {
88017
88069
  writeFileSync22(join54(sessionDir, INHERITED_SETTINGS), `${JSON.stringify(inheritedLayer, null, 2)}
@@ -88044,6 +88096,8 @@ class SessionManager {
88044
88096
  idleSeconds: null,
88045
88097
  activity: null,
88046
88098
  recent: null,
88099
+ agentRequested,
88100
+ agent: null,
88047
88101
  reason: null,
88048
88102
  detail: null,
88049
88103
  pendingInputs: opts.prompt ? 1 : 0,
@@ -88299,6 +88353,7 @@ class SessionManager {
88299
88353
  info.idleSeconds = snap.idleSeconds;
88300
88354
  info.activity = snap.activity;
88301
88355
  info.recent = snap.recent;
88356
+ info.agent = snap.agent ?? info.agent ?? null;
88302
88357
  info.reason = snap.reason;
88303
88358
  info.detail = snap.detail;
88304
88359
  info.pendingInputs = snap.pendingInputs;
@@ -88823,6 +88878,8 @@ function diskInfo(d) {
88823
88878
  idleSeconds: null,
88824
88879
  activity: null,
88825
88880
  recent: null,
88881
+ agentRequested: metaString(meta?.agentRequested) ?? metaString(spawnRecord?.agentRequested),
88882
+ agent: metaString(meta?.agent),
88826
88883
  reason,
88827
88884
  detail: meta === null ? NO_TERMINAL_RECORD : metaString(meta.detail),
88828
88885
  pendingInputs: 0,
@@ -89654,15 +89711,15 @@ function stripUnrequestedProvider(spec, required, isCatalogModel = isCanonicalCa
89654
89711
  return spec;
89655
89712
  return spec.slice(spec.indexOf("@") + 1);
89656
89713
  }
89657
- function userRequiredProvidersArg(value, specs) {
89714
+ function userRequiredProvidersArg(value, specs, name = "user_required_provider") {
89658
89715
  if (value === undefined || value === null)
89659
89716
  return new Set;
89660
89717
  if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
89661
- throw new Error(`Invalid 'user_required_provider': expected an array of model specs.`);
89718
+ throw new Error(`Invalid '${name}': expected an array of model specs.`);
89662
89719
  }
89663
89720
  for (const spec of value) {
89664
89721
  if (!specs.includes(spec)) {
89665
- throw new Error(`Invalid 'user_required_provider': ${JSON.stringify(spec)} is not one of the ` + `models (${specs.join(", ")}). Entries must match a 'models' or 'judges' entry exactly.`);
89722
+ throw new Error(`Invalid '${name}': ${JSON.stringify(spec)} is not one of the ` + `models (${specs.join(", ")}). Entries must match a 'models' or 'judges' entry exactly.`);
89666
89723
  }
89667
89724
  }
89668
89725
  return new Set(value);
@@ -89690,6 +89747,132 @@ function strippedNote(stripped) {
89690
89747
  return;
89691
89748
  return `Provider removed so claudish routing decides: ${stripped.map((x) => `${x.from} -> ${x.to}`).join(", ")}. ` + "Pass 'user_required_provider' only when the user named the provider.";
89692
89749
  }
89750
+ function liveModelChoiceFacts(opts = {}) {
89751
+ const recommended = readLastRecommendedModels()?.models ?? null;
89752
+ if (!recommended || recommended.length === 0) {
89753
+ log("[mcp] model check skipped: no recommended models list on disk");
89754
+ }
89755
+ let rules;
89756
+ return {
89757
+ recommended: recommended && recommended.length > 0 ? recommended : null,
89758
+ userRule: (model) => {
89759
+ rules ??= loadRoutingRules();
89760
+ return matchRoutingRuleKey(model, rules) !== null;
89761
+ },
89762
+ inCatalog: (model) => !gatherRouteCandidates(model).catalogMiss,
89763
+ namespaceClaim: (model) => gatherRouteCandidates(model).candidates.length > 0,
89764
+ canonicalId: (model) => catalogModelId(model),
89765
+ catalogMissAllowed: (model) => {
89766
+ if (opts.projectRulesReadable === false)
89767
+ return true;
89768
+ if (!gatherRouteCandidates(model).catalogReadable)
89769
+ return true;
89770
+ const fallback = effectiveDefaultProvider();
89771
+ return fallback !== "" && fallback !== DEFAULT_FALLBACK_PROVIDER;
89772
+ }
89773
+ };
89774
+ }
89775
+ function familyOf(id) {
89776
+ return /^[a-z]+/i.exec(id)?.[0].toLowerCase() ?? id.toLowerCase();
89777
+ }
89778
+ function bareNameVerdict(spec, recommendedIds, facts) {
89779
+ if (parseModelChain(spec).length !== 1 || nativeRouteFor(spec) !== null)
89780
+ return;
89781
+ const parsed = parseModelSpec(spec);
89782
+ if (parsed.isExplicitProvider || parsed.isLegacySyntax)
89783
+ return;
89784
+ const model = parsed.model;
89785
+ if (recommendedIds.has(model) || facts.userRule(model))
89786
+ return;
89787
+ const canonical = facts.canonicalId?.(model);
89788
+ if (canonical !== undefined && recommendedIds.has(canonical))
89789
+ return;
89790
+ if (facts.inCatalog(model))
89791
+ return { kind: "not_recommended", model };
89792
+ if (facts.namespaceClaim(model) || facts.catalogMissAllowed?.(model))
89793
+ return;
89794
+ return { kind: "unknown_model", model };
89795
+ }
89796
+ function modelChoiceProblems(pairs, required, facts = liveModelChoiceFacts()) {
89797
+ const recommended = facts.recommended;
89798
+ if (!recommended)
89799
+ return [];
89800
+ const ids = new Set(recommended.map((m) => m.id));
89801
+ const problems = [];
89802
+ for (const { given, spec } of pairs) {
89803
+ if (required.has(given))
89804
+ continue;
89805
+ const verdict = bareNameVerdict(spec, ids, facts);
89806
+ if (!verdict)
89807
+ continue;
89808
+ const { kind, model } = verdict;
89809
+ const family = familyOf(model);
89810
+ const matching = [
89811
+ ...new Set(recommended.filter((m) => familyOf(m.id) === family || m.provider.toLowerCase() === model.toLowerCase()).sort(compareByReleaseDateDesc).map((m) => m.id))
89812
+ ];
89813
+ problems.push({ spec: given, kind, matching });
89814
+ }
89815
+ if (problems.length > 0) {
89816
+ log(`[mcp] model check: ${problems.map((p) => `${p.spec} ${p.kind}`).join(", ")}`);
89817
+ }
89818
+ return problems;
89819
+ }
89820
+ function recommendedChoices(recommended) {
89821
+ const byId = new Map;
89822
+ for (const m of recommended) {
89823
+ const row = byId.get(m.id);
89824
+ if (row) {
89825
+ row.tiers.push(m.recommendationTier);
89826
+ continue;
89827
+ }
89828
+ byId.set(m.id, {
89829
+ id: m.id,
89830
+ vendor: m.provider,
89831
+ tiers: [m.recommendationTier],
89832
+ ...m.releaseDate ? { released: m.releaseDate } : {}
89833
+ });
89834
+ }
89835
+ return [...byId.values()];
89836
+ }
89837
+ function recommendedSearchMatches(query, recommended) {
89838
+ const words = query.toLowerCase().split(/[\s,/]+/).filter((w) => w.length >= 2);
89839
+ if (words.length === 0)
89840
+ return [];
89841
+ const hits = recommended.filter((m) => {
89842
+ const id = m.id.toLowerCase();
89843
+ return words.some((w) => id.includes(w) || familyOf(m.id) === familyOf(w) || m.provider.toLowerCase() === w);
89844
+ });
89845
+ return recommendedChoices([...hits].sort(compareByReleaseDateDesc));
89846
+ }
89847
+ function modelChoiceResult(problems, recommended, how) {
89848
+ const unknown = problems.filter((p) => p.kind === "unknown_model");
89849
+ const old = problems.filter((p) => p.kind === "not_recommended");
89850
+ const lines = [
89851
+ `${unknown.length > 0 ? "unknown_model" : "not_recommended"}: nothing was started.`
89852
+ ];
89853
+ if (unknown.length > 0) {
89854
+ lines.push(`Not a model id: ${unknown.map((p) => JSON.stringify(p.spec)).join(", ")}. ` + "Pick the recommended model that best fits the task (for a family name, the newest " + "of the matching ids) and call again with its id.");
89855
+ }
89856
+ if (old.length > 0) {
89857
+ lines.push(`Not in the recommended list: ${old.map((p) => JSON.stringify(p.spec)).join(", ")}. ` + "Re-evaluate the choice against the recommended models below and call again with one " + `of them. If the user named this exact model, call again ${how}.`);
89858
+ }
89859
+ const matching = Object.fromEntries(problems.map((p) => [p.spec, p.matching]));
89860
+ lines.push(`Matching recommended ids: ${JSON.stringify(matching, null, 2)}`);
89861
+ const efforts = effortClarification([...new Set(problems.flatMap((p) => p.matching))]);
89862
+ if (efforts) {
89863
+ lines.push("Recommended effort for those ids, by task type (pass your task's level with the model " + `you pick): ${JSON.stringify(efforts, null, 2)}`);
89864
+ }
89865
+ lines.push(`Recommended models: ${JSON.stringify(recommendedChoices(recommended), null, 2)}`);
89866
+ return { content: [{ type: "text", text: lines.join(`
89867
+ `) }], isError: true };
89868
+ }
89869
+ function modelChoiceStop(pairs, required, how, opts = {}) {
89870
+ const facts = liveModelChoiceFacts(opts);
89871
+ const problems = modelChoiceProblems(pairs, required, facts);
89872
+ if (problems.length === 0 || !facts.recommended)
89873
+ return;
89874
+ return modelChoiceResult(problems, facts.recommended, how);
89875
+ }
89693
89876
  function buildChildClaudeFlags(agent, claudeFlags) {
89694
89877
  const extra = typeof claudeFlags === "string" ? claudeFlags.split(/\s+/).filter(Boolean) : [];
89695
89878
  const named = typeof agent === "string" ? agent.trim() : "";
@@ -89806,6 +89989,12 @@ function optionalString(raw, name) {
89806
89989
  throw new ContractErrorException("invalid_args", `'${name}' must be a non-empty string`);
89807
89990
  return raw;
89808
89991
  }
89992
+ function requiredString(raw, name) {
89993
+ const value = optionalString(raw, name);
89994
+ if (value === undefined)
89995
+ throw new ContractErrorException("invalid_args", `'${name}' is required`);
89996
+ return value;
89997
+ }
89809
89998
  function optionalBoolean(raw, name) {
89810
89999
  if (raw === undefined || raw === null)
89811
90000
  return false;
@@ -89914,6 +90103,7 @@ ${block.join(`
89914
90103
  type: "boolean",
89915
90104
  description: "True only when the USER named the provider ('use OpenRouter for it'): the " + "'provider@model' spec is then sent as given."
89916
90105
  },
90106
+ user_required_model: { type: "boolean", description: USER_REQUIRED_MODEL_DESCRIPTION },
89917
90107
  prompt: { type: "string", description: "The prompt to send to the model" },
89918
90108
  system_prompt: { type: "string", description: "Optional system prompt" },
89919
90109
  effort: {
@@ -89929,6 +90119,9 @@ ${block.join(`
89929
90119
  handler: async (args, ctx) => {
89930
90120
  try {
89931
90121
  const { specs, stripped } = strippedProviders([args.model], new Set(args.user_required_provider === true ? [args.model] : []));
90122
+ const choice = modelChoiceStop([{ given: args.model, spec: specs[0] }], new Set(args.user_required_model === true ? [args.model] : []), "with user_required_model: true");
90123
+ if (choice)
90124
+ return choice;
89932
90125
  const recommended = args.effort === undefined ? effortClarification([args.model]) : undefined;
89933
90126
  if (recommended) {
89934
90127
  return needsEffortResult(recommended, `with effort: "<level>" (effort: "${KEEP_EFFORT}" keeps Claude Code's own)`);
@@ -90026,6 +90219,26 @@ ${block.join(`
90026
90219
  return compareByReleaseDateDesc(orderingKey(a.model), orderingKey(b.model));
90027
90220
  }).slice(0, maxResults);
90028
90221
  const catalogMatches = searchCatalogModels(query, Math.max(maxResults, 5));
90222
+ const recommendedHits = recommendedSearchMatches(query, readLastRecommendedModels()?.models ?? []);
90223
+ const renderRecommendedSection = () => {
90224
+ if (recommendedHits.length === 0)
90225
+ return "";
90226
+ let s = `## Recommended models (pass one of these)
90227
+
90228
+ `;
90229
+ s += `| Bare name | Vendor | Tiers | Released |
90230
+ `;
90231
+ s += `|-----------|--------|-------|----------|
90232
+ `;
90233
+ for (const m of recommendedHits) {
90234
+ s += `| ${m.id} | ${m.vendor} | ${m.tiers.join(", ")} | ${m.released ?? "-"} |
90235
+ `;
90236
+ }
90237
+ return `${s}
90238
+ `;
90239
+ };
90240
+ const useWith = () => recommendedHits.length > 0 ? `
90241
+ Use with: run_prompt(model="${recommendedHits[0].id}", prompt="your prompt")` : "\nNo recommended model matches this query: call `list_models` for the recommended set.";
90029
90242
  const renderCatalogSection = () => {
90030
90243
  if (catalogMatches.length === 0)
90031
90244
  return "";
@@ -90042,12 +90255,17 @@ ${block.join(`
90042
90255
  s += `| ${m.modelId} | ${m.matchedAlias ?? "-"} | ${plans} |
90043
90256
  `;
90044
90257
  }
90045
- s += `
90046
- Pass the **bare name**. Routing puts a subscription ahead of the metered API and ` + "rewrites the model to that plan's wire id for you. An aggregator-qualified id " + "(`moonshotai/...`, `accounts/fireworks/...`) pins that aggregator and bills per token.\n";
90258
+ s += "\nOnly the recommended models above start without `user_required_model`. " + "Pass the **bare name**. Routing puts a subscription ahead of the metered API and " + "rewrites the model to that plan's wire id for you. An aggregator-qualified id " + "(`moonshotai/...`, `accounts/fireworks/...`) pins that aggregator and bills per token.\n";
90047
90259
  return s;
90048
90260
  };
90049
90261
  if (results.length === 0) {
90050
90262
  const catalog = renderCatalogSection();
90263
+ if (recommendedHits.length > 0) {
90264
+ const text = `No OpenRouter listing matches "${query}".
90265
+
90266
+ ${renderRecommendedSection()}${catalog}${useWith()}`;
90267
+ return { content: [{ type: "text", text }] };
90268
+ }
90051
90269
  const text = catalog ? `No OpenRouter listing matches "${query}", but claudish's catalog knows these:
90052
90270
  ${catalog}` : `No models found matching "${query}".
90053
90271
 
@@ -90057,6 +90275,7 @@ ${catalog}` : `No models found matching "${query}".
90057
90275
  let output = `# Search Results for "${query}"
90058
90276
 
90059
90277
  `;
90278
+ output += renderRecommendedSection();
90060
90279
  output += `## OpenRouter listing
90061
90280
 
90062
90281
  `;
@@ -90075,9 +90294,7 @@ ${catalog}` : `No models found matching "${query}".
90075
90294
  `;
90076
90295
  }
90077
90296
  output += renderCatalogSection();
90078
- const suggested = catalogMatches[0]?.modelId ?? results[0].model.id;
90079
- output += `
90080
- Use with: run_prompt(model="${suggested}", prompt="your prompt")`;
90297
+ output += useWith();
90081
90298
  return { content: [{ type: "text", text: output }] };
90082
90299
  }
90083
90300
  });
@@ -90261,6 +90478,11 @@ Use with: run_prompt(model="${suggested}", prompt="your prompt")`;
90261
90478
  items: { type: "string" },
90262
90479
  description: "The 'models' or 'judges' entries whose provider the USER named ('run Kimi on " + "OpenRouter'), exactly as written there. Every other 'provider@' on a catalog " + "model is removed so claudish routing picks the provider from the user's setup; " + "pass bare names unless the user asked for a provider."
90263
90480
  },
90481
+ user_required_model: {
90482
+ type: "array",
90483
+ items: { type: "string" },
90484
+ description: "The 'models' or 'judges' entries the USER named as exact models, exactly as " + "written there: they run even when not in the recommended list. Never list a " + "model you chose yourself."
90485
+ },
90264
90486
  input_file: {
90265
90487
  type: "string",
90266
90488
  description: "PREFERRED. Path to a file holding the task prompt, relative to the working " + "directory. Use this rather than `input` for anything longer than a sentence: " + "a prompt passed inline is echoed verbatim in the caller's terminal, where a " + "200-line review brief buries every other argument and makes the call " + "unreadable. Write the brief to the session directory first (input.md is the " + "conventional name) and point here."
@@ -90345,7 +90567,15 @@ Use with: run_prompt(model="${suggested}", prompt="your prompt")`;
90345
90567
  const existing = join56(resolved, "input.md");
90346
90568
  return existsSync37(existing) ? readFileSync37(existing, "utf-8") : undefined;
90347
90569
  };
90348
- if ((mode === "run" || mode === "run-and-judge") && givenModels && args.efforts === undefined) {
90570
+ const runsModels = mode === "run" || mode === "run-and-judge";
90571
+ const runsJudges = mode === "judge" || mode === "run-and-judge";
90572
+ const choice = modelChoiceStop([
90573
+ ...runsModels && givenModels && models ? givenModels.map((given, i) => ({ given, spec: models[i] })) : [],
90574
+ ...runsJudges && givenJudges && judges ? givenJudges.map((given, i) => ({ given, spec: judges[i] })) : []
90575
+ ], userRequiredProvidersArg(args.user_required_model, [...givenModels ?? [], ...givenJudges ?? []], "user_required_model"), 'with user_required_model: ["<that entry>"]');
90576
+ if (choice)
90577
+ return choice;
90578
+ if (runsModels && givenModels && args.efforts === undefined) {
90349
90579
  const recommended = effortClarification(givenModels);
90350
90580
  if (recommended) {
90351
90581
  return needsEffortResult(recommended, `with efforts: {"<model>": "<level>"} (efforts: {} keeps Claude Code's own for every model)`);
@@ -90634,6 +90864,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90634
90864
  type: "boolean",
90635
90865
  description: "True only when the USER named the provider: the 'provider@model' spec is then " + "used as given."
90636
90866
  },
90867
+ user_required_model: { type: "boolean", description: USER_REQUIRED_MODEL_DESCRIPTION },
90637
90868
  prompt: {
90638
90869
  type: "string",
90639
90870
  description: "Initial prompt to send. If omitted, send later via send_input."
@@ -90681,6 +90912,11 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90681
90912
  await assertMagmuxAvailable();
90682
90913
  const { specs, stripped } = strippedProviders([args.model], new Set(args.user_required_provider === true ? [args.model] : []));
90683
90914
  const requestedModel = specs[0];
90915
+ const choice = modelChoiceStop([{ given: args.model, spec: requestedModel }], new Set(args.user_required_model === true ? [args.model] : []), "with user_required_model: true", {
90916
+ projectRulesReadable: typeof args.work_dir !== "string" || resolve8(args.work_dir) === process.cwd()
90917
+ });
90918
+ if (choice)
90919
+ return choice;
90684
90920
  const recommended = args.effort === undefined ? effortClarification([args.model]) : undefined;
90685
90921
  if (recommended) {
90686
90922
  return needsEffortResult(recommended, `with effort: "<level>" (effort: "${KEEP_EFFORT}" keeps Claude Code's own)`);
@@ -90748,7 +90984,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90748
90984
  },
90749
90985
  group: "session",
90750
90986
  handler: async (args) => {
90751
- const result = sessionManager.sendInput(args.session_id, args.text);
90987
+ const result = sessionManager.sendInput(requiredString(args.session_id, "session_id"), requiredString(args.text, "text"));
90752
90988
  return {
90753
90989
  content: [{ type: "text", text: JSON.stringify(result) }]
90754
90990
  };
@@ -90771,7 +91007,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90771
91007
  group: "session",
90772
91008
  handler: async (args) => {
90773
91009
  try {
90774
- const output = sessionManager.getOutput(args.session_id, args.tail_lines);
91010
+ const output = sessionManager.getOutput(requiredString(args.session_id, "session_id"), args.tail_lines);
90775
91011
  return {
90776
91012
  content: [{ type: "text", text: JSON.stringify(output) }]
90777
91013
  };
@@ -90852,7 +91088,7 @@ Report manually at https://github.com/anthropics/claudish/issues${autoSendHint}`
90852
91088
  group: "session",
90853
91089
  handler: async (args) => {
90854
91090
  try {
90855
- const diagnostics = sessionManager.getDiagnostics(args.session_id, args.event_limit);
91091
+ const diagnostics = sessionManager.getDiagnostics(requiredString(args.session_id, "session_id"), args.event_limit);
90856
91092
  return {
90857
91093
  content: [{ type: "text", text: JSON.stringify(diagnostics) }]
90858
91094
  };
@@ -91075,7 +91311,7 @@ Each session is an interactive Claude Code running the model in its own headless
91075
91311
  5. FAILED or TIMEOUT \u2014 or COMPLETED with an empty or surprising answer \u2014 call get_diagnostics. It returns the child's final screen, the upstream error bodies, the recent state records, the resolved model chain and the paths to the full records. It needs no re-run and no debug flag.
91076
91312
  6. capture_session reads a session's current screen; resize_session sets its terminal size (any time, mid-turn included); cancel_session stops it. Nothing stops a session for being idle: a child inside a long build is silent and working.
91077
91313
 
91078
- The session_id from create_session is the key for every session tool.`, proxyInstance = null, proxyStarting = null, SCORE_TIE_EPSILON = 0.000001, NEXT_STEP, KEEP_EFFORT = "default", sanitize2, TOOL_MODES;
91314
+ The session_id from create_session is the key for every session tool.`, proxyInstance = null, proxyStarting = null, SCORE_TIE_EPSILON = 0.000001, NEXT_STEP, KEEP_EFFORT = "default", USER_REQUIRED_MODEL_DESCRIPTION, sanitize2, TOOL_MODES;
91079
91315
  var init_mcp_server = __esm(() => {
91080
91316
  init_server2();
91081
91317
  init_stdio2();
@@ -91107,6 +91343,8 @@ var init_mcp_server = __esm(() => {
91107
91343
  init_preflight_chain();
91108
91344
  init_probe_live();
91109
91345
  init_provider_definitions();
91346
+ init_route_candidates();
91347
+ init_routing_rules();
91110
91348
  init_proxy_server();
91111
91349
  init_recommended_listing();
91112
91350
  init_redact();
@@ -91133,6 +91371,7 @@ var init_mcp_server = __esm(() => {
91133
91371
  empty_output: "retry once; if it repeats, drop the model",
91134
91372
  shape_mismatch: "the answer (assistant text after the task was read) did not match. Every assistant " + "message of the turn was read from the transcript, so nothing was lost in transit \u2014 " + "the model did not produce it. Re-prompt with the required format restated; do NOT " + "count this slot as a vote"
91135
91373
  };
91374
+ USER_REQUIRED_MODEL_DESCRIPTION = "True only when the USER named this exact model: it then runs even when it is not in the " + "recommended list. Never set it for a model you chose yourself.";
91136
91375
  sanitize2 = sanitizeForReport;
91137
91376
  TOOL_MODES = ["all", "low-level", "agentic", "session", "debug"];
91138
91377
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "14.2.0",
3
+ "version": "14.4.0",
4
4
  "description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -71,10 +71,10 @@
71
71
  "ai"
72
72
  ],
73
73
  "optionalDependencies": {
74
- "@claudish/magmux-darwin-arm64": "14.2.0",
75
- "@claudish/magmux-darwin-x64": "14.2.0",
76
- "@claudish/magmux-linux-arm64": "14.2.0",
77
- "@claudish/magmux-linux-x64": "14.2.0"
74
+ "@claudish/magmux-darwin-arm64": "14.4.0",
75
+ "@claudish/magmux-darwin-x64": "14.4.0",
76
+ "@claudish/magmux-linux-arm64": "14.4.0",
77
+ "@claudish/magmux-linux-x64": "14.4.0"
78
78
  },
79
79
  "author": "Jack Rudenko <i@madappgang.com>",
80
80
  "license": "MIT",
@@ -58,9 +58,13 @@ subscriptions first, their preferences and manual overrides. Do not choose it yo
58
58
  `create_session` / `run_prompt`: `true`).
59
59
  3. **User named exact models:** pass them as given. No `preflight` or `search_models`
60
60
  call is needed to check them: claudish routes and checks them. You still read
61
- `list_models` once for their efforts (next section).
61
+ `list_models` once for their efforts (next section). A named model outside the
62
+ recommended list answers `not_recommended`; list it in `user_required_model` (`team`:
63
+ the entries; `create_session` / `run_prompt`: `true`) and call again.
62
64
  4. **User named only a family or vendor** ("a GPT model", "minimax", "latest Grok"): call
63
- `list_models` and take the newest model of that family from it, as a bare name.
65
+ `list_models` and take the newest model of that family from it, as a bare name. A
66
+ family name (`gpt`) or a model you recall (`gpt-5`) answers `unknown_model` /
67
+ `not_recommended` with the recommended ids: pick from them and call again.
64
68
 
65
69
  ### 🎚️ Reasoning Effort per Model
66
70