@sonnechasser/ntrp 0.1.8 → 0.2.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.
@@ -359,7 +359,7 @@ function migrateUsageIfNeeded(state) {
359
359
  if (state.usage?.first_active_at) return { state, changed: false };
360
360
  const fromCredits = rebuildFromCredits(state.credits);
361
361
  const prior = state.usage;
362
- const usage2 = {
362
+ const usage3 = {
363
363
  sessions_closed: prior?.sessions_closed ?? 0,
364
364
  llm_calls: prior?.llm_calls ?? 0,
365
365
  input_tokens: prior?.input_tokens ?? 0,
@@ -367,7 +367,7 @@ function migrateUsageIfNeeded(state) {
367
367
  ...fromCredits,
368
368
  weekly: mergeWeekly(prior?.weekly ?? [], fromCredits.weekly)
369
369
  };
370
- return { state: { ...state, usage: usage2 }, changed: true };
370
+ return { state: { ...state, usage: usage3 }, changed: true };
371
371
  }
372
372
  var init_usage_backfill = __esm({
373
373
  "src/whimsy/usage-backfill.ts"() {
@@ -1212,48 +1212,48 @@ function bumpWeekly2(weekly, patch) {
1212
1212
  }
1213
1213
  function touchUsage(state, patch) {
1214
1214
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
1215
- const usage2 = ensureUsage(state);
1215
+ const usage3 = ensureUsage(state);
1216
1216
  return {
1217
1217
  ...state,
1218
1218
  usage: {
1219
- ...usage2,
1219
+ ...usage3,
1220
1220
  ...patch,
1221
- first_active_at: usage2.first_active_at ?? now2,
1221
+ first_active_at: usage3.first_active_at ?? now2,
1222
1222
  last_active_at: now2,
1223
- weekly: patch.weekly ?? usage2.weekly
1223
+ weekly: patch.weekly ?? usage3.weekly
1224
1224
  }
1225
1225
  };
1226
1226
  }
1227
1227
  function recordUsageFromCredit(action, minutes) {
1228
1228
  if (minutes <= 0) return;
1229
1229
  let state = loadProgress();
1230
- const usage2 = ensureUsage(state);
1231
- const weekly = bumpWeekly2(usage2.weekly, { minutes_saved: minutes, actions: 1 });
1230
+ const usage3 = ensureUsage(state);
1231
+ const weekly = bumpWeekly2(usage3.weekly, { minutes_saved: minutes, actions: 1 });
1232
1232
  const counters = { weekly };
1233
- if (action === "diagnose" || action === "diagnose_findings") counters.diagnoses = usage2.diagnoses + 1;
1234
- if (action === "metrics" || action === "metrics_findings") counters.metrics_runs = usage2.metrics_runs + 1;
1235
- if (action === "deliverable" || action === "deliverable_deck") counters.deliverables = usage2.deliverables + 1;
1236
- if (action === "nl_answer") counters.nl_exchanges = usage2.nl_exchanges + 1;
1233
+ if (action === "diagnose" || action === "diagnose_findings") counters.diagnoses = usage3.diagnoses + 1;
1234
+ if (action === "metrics" || action === "metrics_findings") counters.metrics_runs = usage3.metrics_runs + 1;
1235
+ if (action === "deliverable" || action === "deliverable_deck") counters.deliverables = usage3.deliverables + 1;
1236
+ if (action === "nl_answer") counters.nl_exchanges = usage3.nl_exchanges + 1;
1237
1237
  state = touchUsage(state, counters);
1238
1238
  saveProgress(state);
1239
1239
  }
1240
1240
  function recordSessionClosed() {
1241
1241
  let state = loadProgress();
1242
- const usage2 = ensureUsage(state);
1242
+ const usage3 = ensureUsage(state);
1243
1243
  state = touchUsage(state, {
1244
- sessions_closed: usage2.sessions_closed + 1,
1245
- weekly: bumpWeekly2(usage2.weekly, { actions: 1 })
1244
+ sessions_closed: usage3.sessions_closed + 1,
1245
+ weekly: bumpWeekly2(usage3.weekly, { actions: 1 })
1246
1246
  });
1247
1247
  saveProgress(state);
1248
1248
  }
1249
1249
  function recordLlmUsage(tokenUsage) {
1250
1250
  let state = loadProgress();
1251
- const usage2 = ensureUsage(state);
1252
- const weekly = bumpWeekly2(usage2.weekly, { llm_calls: 1 });
1251
+ const usage3 = ensureUsage(state);
1252
+ const weekly = bumpWeekly2(usage3.weekly, { llm_calls: 1 });
1253
1253
  state = touchUsage(state, {
1254
- llm_calls: usage2.llm_calls + 1,
1255
- input_tokens: usage2.input_tokens + (tokenUsage?.input_tokens ?? 0),
1256
- output_tokens: usage2.output_tokens + (tokenUsage?.output_tokens ?? 0),
1254
+ llm_calls: usage3.llm_calls + 1,
1255
+ input_tokens: usage3.input_tokens + (tokenUsage?.input_tokens ?? 0),
1256
+ output_tokens: usage3.output_tokens + (tokenUsage?.output_tokens ?? 0),
1257
1257
  weekly
1258
1258
  });
1259
1259
  saveProgress(state);
@@ -6639,55 +6639,108 @@ var init_markdown = __esm({
6639
6639
  }
6640
6640
  });
6641
6641
 
6642
- // src/ai/llm/catalog.ts
6643
- function getCatalogEntry(id) {
6644
- return byId.get(id);
6645
- }
6646
- function listCatalogEntries(provider) {
6647
- if (!provider) return [...ENTRIES];
6648
- return ENTRIES.filter((e) => e.provider === provider);
6649
- }
6650
- function resolveRetiredModel(id) {
6651
- const seen = /* @__PURE__ */ new Set();
6652
- let current = id;
6653
- while (true) {
6654
- if (seen.has(current)) break;
6655
- seen.add(current);
6656
- const entry = byId.get(current);
6657
- if (!entry) return current;
6658
- if (entry.status === "active") return entry.id;
6659
- if (!entry.successor_id) {
6660
- const fallback = cheapestActiveInTier(entry.provider, entry.tier);
6661
- return fallback?.id ?? current;
6662
- }
6663
- current = entry.successor_id;
6642
+ // src/ai/llm/models-cache.ts
6643
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
6644
+ import { join as join9 } from "path";
6645
+ function cachePath() {
6646
+ return join9(ntrpHome(), "models.json");
6647
+ }
6648
+ function loadFile() {
6649
+ if (cached) return cached;
6650
+ const path = cachePath();
6651
+ if (!existsSync9(path)) {
6652
+ cached = { version: 1, providers: {} };
6653
+ return cached;
6664
6654
  }
6665
- return id;
6655
+ try {
6656
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
6657
+ cached = { version: 1, providers: parsed.providers ?? {} };
6658
+ } catch {
6659
+ cached = { version: 1, providers: {} };
6660
+ }
6661
+ return cached;
6662
+ }
6663
+ function saveFile(file) {
6664
+ writeFileSync7(cachePath(), JSON.stringify(file, null, 2) + "\n");
6665
+ cached = file;
6666
+ }
6667
+ function getProviderModels(provider) {
6668
+ return loadFile().providers[provider];
6666
6669
  }
6667
- function cheapestActiveInTier(provider, tier) {
6670
+ function setProviderModels(provider, entry) {
6671
+ const file = loadFile();
6672
+ file.providers[provider] = entry;
6673
+ saveFile(file);
6674
+ }
6675
+ function getCachedTierModel(provider, tier) {
6676
+ return getProviderModels(provider)?.tier_stack?.[tier];
6677
+ }
6678
+ function findCachedModel(provider, modelId) {
6679
+ return getProviderModels(provider)?.models.find((m) => m.id === modelId);
6680
+ }
6681
+ function cachedModelProvider(modelId) {
6682
+ const file = loadFile();
6683
+ for (const [provider, entry] of Object.entries(file.providers)) {
6684
+ if (entry.models.some((m) => m.id === modelId)) return provider;
6685
+ }
6686
+ return void 0;
6687
+ }
6688
+ function markModelNoTools(provider, modelId) {
6689
+ const file = loadFile();
6690
+ const entry = file.providers[provider];
6691
+ if (!entry) return;
6692
+ const noTools = new Set(entry.quirks?.no_tools ?? []);
6693
+ if (noTools.has(modelId)) return;
6694
+ noTools.add(modelId);
6695
+ entry.quirks = { ...entry.quirks, no_tools: [...noTools] };
6696
+ saveFile(file);
6697
+ }
6698
+ function modelHasNoToolsQuirk(provider, modelId) {
6699
+ return !!getProviderModels(provider)?.quirks?.no_tools?.includes(modelId);
6700
+ }
6701
+ function isProviderCacheStale(provider, ttlMs = CACHE_TTL_MS) {
6702
+ const entry = getProviderModels(provider);
6703
+ if (!entry) return true;
6704
+ const fetched = Date.parse(entry.fetched_at);
6705
+ if (Number.isNaN(fetched)) return true;
6706
+ return Date.now() - fetched > ttlMs;
6707
+ }
6708
+ var CACHE_TTL_MS, cached;
6709
+ var init_models_cache = __esm({
6710
+ "src/ai/llm/models-cache.ts"() {
6711
+ "use strict";
6712
+ init_store();
6713
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
6714
+ cached = null;
6715
+ }
6716
+ });
6717
+
6718
+ // src/ai/llm/catalog.ts
6719
+ function catalogTierDefault(provider, tier) {
6668
6720
  const candidates = ENTRIES.filter(
6669
6721
  (e) => e.provider === provider && e.tier === tier && e.status === "active"
6670
6722
  );
6671
6723
  if (candidates.length === 0) return void 0;
6672
6724
  return candidates.sort((a, b) => a.relative_cost - b.relative_cost)[0];
6673
6725
  }
6674
- function getTierDefault(provider, tier) {
6675
- const entry = cheapestActiveInTier(provider, tier);
6676
- if (!entry) {
6677
- throw new Error(`No active ${tier}-tier model for provider ${provider} in catalog`);
6678
- }
6679
- return entry;
6726
+ function modelProviderHint(modelId) {
6727
+ return cachedModelProvider(modelId) ?? byId.get(modelId)?.provider;
6680
6728
  }
6681
- function resolveModel(provider, tier, override) {
6682
- if (override) {
6683
- const entry = byId.get(override);
6684
- if (!entry) return resolveRetiredModel(override);
6685
- if (entry.status === "retired") return resolveRetiredModel(override);
6686
- return entry.id;
6687
- }
6688
- return getTierDefault(provider, tier).id;
6729
+ function overrideForProvider(override, provider, activeProvider) {
6730
+ if (!override) return void 0;
6731
+ const hint = modelProviderHint(override);
6732
+ if (hint) return hint === provider ? override : void 0;
6733
+ return provider === activeProvider ? override : void 0;
6734
+ }
6735
+ function resolveModelSafe(provider, tier, override) {
6736
+ if (override) return override;
6737
+ const discovered = getCachedTierModel(provider, tier);
6738
+ if (discovered) return discovered;
6739
+ return catalogTierDefault(provider, tier)?.id;
6689
6740
  }
6690
6741
  function formatModelLabel(provider, modelId) {
6742
+ const cachedName = findCachedModel(provider, modelId)?.display_name;
6743
+ if (cachedName) return `${provider}/${cachedName}`;
6691
6744
  const entry = byId.get(modelId);
6692
6745
  return entry ? `${provider}/${entry.display_name}` : `${provider}/${modelId}`;
6693
6746
  }
@@ -6695,6 +6748,7 @@ var ENTRIES, byId;
6695
6748
  var init_catalog = __esm({
6696
6749
  "src/ai/llm/catalog.ts"() {
6697
6750
  "use strict";
6751
+ init_models_cache();
6698
6752
  ENTRIES = [
6699
6753
  {
6700
6754
  id: "claude-opus-4-6",
@@ -6779,6 +6833,9 @@ function formatLlmAttribution(meta) {
6779
6833
  return line;
6780
6834
  }
6781
6835
  function printLlmAttribution(meta) {
6836
+ for (const notice of meta.notices ?? []) {
6837
+ console.log(chalk6.dim(` ${notice}`));
6838
+ }
6782
6839
  const line = formatLlmAttribution(meta);
6783
6840
  if (line) console.log(chalk6.dim(` ${line}`));
6784
6841
  }
@@ -7096,6 +7153,7 @@ async function renderDiagnoseStream(options) {
7096
7153
  let modelUsed = "";
7097
7154
  let providerUsed;
7098
7155
  let failover;
7156
+ let notices;
7099
7157
  let rawPrompt = "";
7100
7158
  try {
7101
7159
  for await (const event of runFindings(fullResult)) {
@@ -7111,6 +7169,7 @@ async function renderDiagnoseStream(options) {
7111
7169
  modelUsed = event.model_used;
7112
7170
  providerUsed = event.provider_used;
7113
7171
  failover = event.failover;
7172
+ notices = event.usage?.notices;
7114
7173
  rawPrompt = event.raw_prompt;
7115
7174
  }
7116
7175
  }
@@ -7134,7 +7193,8 @@ async function renderDiagnoseStream(options) {
7134
7193
  printLlmAttribution({
7135
7194
  model_used: modelUsed,
7136
7195
  provider_used: providerUsed,
7137
- failover
7196
+ failover,
7197
+ notices
7138
7198
  });
7139
7199
  } catch (err) {
7140
7200
  findingsSpinner.fail(deep ? "Agentic investigation failed" : "AI findings failed");
@@ -7189,10 +7249,232 @@ var init_terminal = __esm({
7189
7249
  }
7190
7250
  });
7191
7251
 
7252
+ // src/ai/llm/providers.ts
7253
+ var providers_exports = {};
7254
+ __export(providers_exports, {
7255
+ findSpecByConfigKey: () => findSpecByConfigKey,
7256
+ getProviderSpec: () => getProviderSpec,
7257
+ isEndpointEnabled: () => isEndpointEnabled,
7258
+ keyConfigNameFor: () => keyConfigNameFor,
7259
+ listProviderSpecs: () => listProviderSpecs,
7260
+ loadCustomProviders: () => loadCustomProviders,
7261
+ modelsUrl: () => modelsUrl,
7262
+ providerLabel: () => providerLabel,
7263
+ removeCustomProvider: () => removeCustomProvider,
7264
+ resetProvidersCache: () => resetProvidersCache,
7265
+ saveCustomProvider: () => saveCustomProvider
7266
+ });
7267
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
7268
+ import { join as join10 } from "path";
7269
+ function providersPath() {
7270
+ return join10(ntrpHome(), "providers.json");
7271
+ }
7272
+ function loadCustomProviders() {
7273
+ if (cachedEntries) return cachedEntries;
7274
+ const path = providersPath();
7275
+ if (!existsSync10(path)) {
7276
+ cachedEntries = [];
7277
+ return cachedEntries;
7278
+ }
7279
+ try {
7280
+ const parsed = JSON.parse(readFileSync8(path, "utf-8"));
7281
+ cachedEntries = Array.isArray(parsed.providers) ? parsed.providers : [];
7282
+ } catch {
7283
+ cachedEntries = [];
7284
+ }
7285
+ return cachedEntries;
7286
+ }
7287
+ function saveCustomProvider(entry) {
7288
+ const entries = loadCustomProviders().filter((e) => e.id !== entry.id);
7289
+ entries.push(entry);
7290
+ writeFileSync8(providersPath(), JSON.stringify({ version: 1, providers: entries }, null, 2) + "\n");
7291
+ cachedEntries = entries;
7292
+ }
7293
+ function removeCustomProvider(id) {
7294
+ const entries = loadCustomProviders().filter((e) => e.id !== id);
7295
+ writeFileSync8(providersPath(), JSON.stringify({ version: 1, providers: entries }, null, 2) + "\n");
7296
+ cachedEntries = entries;
7297
+ }
7298
+ function resetProvidersCache() {
7299
+ cachedEntries = null;
7300
+ }
7301
+ function customEntryToSpec(entry) {
7302
+ return {
7303
+ id: entry.id,
7304
+ label: entry.label ?? entry.id,
7305
+ api: "openai-compat",
7306
+ base_url: entry.base_url.replace(/\/+$/, ""),
7307
+ key_prefixes: [],
7308
+ shared_prefixes: [],
7309
+ key_config_name: keyConfigNameFor(entry.id),
7310
+ requires_key: entry.requires_key ?? false,
7311
+ custom: true
7312
+ };
7313
+ }
7314
+ function keyConfigNameFor(providerId) {
7315
+ return providerId === "anthropic" ? "api-key" : `${providerId}-api-key`;
7316
+ }
7317
+ function listProviderSpecs() {
7318
+ const customs = loadCustomProviders();
7319
+ const customById = new Map(customs.map((e) => [e.id, e]));
7320
+ const specs = BUILTIN_SPECS.map((spec) => {
7321
+ const override = customById.get(spec.id);
7322
+ if (override?.base_url) {
7323
+ return { ...spec, base_url: override.base_url.replace(/\/+$/, "") };
7324
+ }
7325
+ return spec;
7326
+ });
7327
+ for (const entry of customs) {
7328
+ if (!BUILTIN_SPECS.some((s) => s.id === entry.id)) {
7329
+ specs.push(customEntryToSpec(entry));
7330
+ }
7331
+ }
7332
+ return specs;
7333
+ }
7334
+ function getProviderSpec(id) {
7335
+ return listProviderSpecs().find((s) => s.id === id);
7336
+ }
7337
+ function findSpecByConfigKey(configKey) {
7338
+ return listProviderSpecs().find((s) => s.key_config_name === configKey);
7339
+ }
7340
+ function isEndpointEnabled(id) {
7341
+ const entry = loadCustomProviders().find((e) => e.id === id);
7342
+ return !!entry && entry.enabled !== false;
7343
+ }
7344
+ function modelsUrl(spec) {
7345
+ if (spec.api === "anthropic") return `${spec.base_url}/v1/models?limit=100`;
7346
+ return `${spec.base_url}/models`;
7347
+ }
7348
+ function providerLabel(id) {
7349
+ return getProviderSpec(id)?.label ?? id;
7350
+ }
7351
+ var BUILTIN_SPECS, cachedEntries;
7352
+ var init_providers = __esm({
7353
+ "src/ai/llm/providers.ts"() {
7354
+ "use strict";
7355
+ init_store();
7356
+ BUILTIN_SPECS = [
7357
+ {
7358
+ id: "anthropic",
7359
+ label: "Anthropic",
7360
+ api: "anthropic",
7361
+ base_url: "https://api.anthropic.com",
7362
+ key_prefixes: ["sk-ant-"],
7363
+ shared_prefixes: [],
7364
+ key_config_name: "api-key",
7365
+ requires_key: true
7366
+ },
7367
+ {
7368
+ id: "openai",
7369
+ label: "OpenAI",
7370
+ api: "openai-compat",
7371
+ base_url: "https://api.openai.com/v1",
7372
+ key_prefixes: ["sk-proj-", "sk-svcacct-", "sk-admin-"],
7373
+ shared_prefixes: ["sk-"],
7374
+ key_config_name: "openai-api-key",
7375
+ env_var: "OPENAI_API_KEY",
7376
+ requires_key: true
7377
+ },
7378
+ {
7379
+ id: "google",
7380
+ label: "Google Gemini",
7381
+ api: "openai-compat",
7382
+ base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
7383
+ key_prefixes: ["AIza"],
7384
+ shared_prefixes: [],
7385
+ key_config_name: "google-api-key",
7386
+ requires_key: true
7387
+ },
7388
+ {
7389
+ id: "groq",
7390
+ label: "Groq",
7391
+ api: "openai-compat",
7392
+ base_url: "https://api.groq.com/openai/v1",
7393
+ key_prefixes: ["gsk_"],
7394
+ shared_prefixes: [],
7395
+ key_config_name: "groq-api-key",
7396
+ requires_key: true
7397
+ },
7398
+ {
7399
+ id: "mistral",
7400
+ label: "Mistral",
7401
+ api: "openai-compat",
7402
+ base_url: "https://api.mistral.ai/v1",
7403
+ key_prefixes: [],
7404
+ shared_prefixes: [],
7405
+ key_config_name: "mistral-api-key",
7406
+ requires_key: true
7407
+ },
7408
+ {
7409
+ id: "deepseek",
7410
+ label: "DeepSeek",
7411
+ api: "openai-compat",
7412
+ base_url: "https://api.deepseek.com/v1",
7413
+ key_prefixes: [],
7414
+ shared_prefixes: ["sk-"],
7415
+ key_config_name: "deepseek-api-key",
7416
+ requires_key: true
7417
+ },
7418
+ {
7419
+ id: "xai",
7420
+ label: "xAI",
7421
+ api: "openai-compat",
7422
+ base_url: "https://api.x.ai/v1",
7423
+ key_prefixes: ["xai-"],
7424
+ shared_prefixes: [],
7425
+ key_config_name: "xai-api-key",
7426
+ requires_key: true
7427
+ },
7428
+ {
7429
+ id: "openrouter",
7430
+ label: "OpenRouter",
7431
+ api: "openai-compat",
7432
+ base_url: "https://openrouter.ai/api/v1",
7433
+ key_prefixes: ["sk-or-"],
7434
+ shared_prefixes: [],
7435
+ key_config_name: "openrouter-api-key",
7436
+ requires_key: true
7437
+ },
7438
+ {
7439
+ id: "together",
7440
+ label: "Together AI",
7441
+ api: "openai-compat",
7442
+ base_url: "https://api.together.xyz/v1",
7443
+ key_prefixes: [],
7444
+ shared_prefixes: [],
7445
+ key_config_name: "together-api-key",
7446
+ requires_key: true
7447
+ },
7448
+ {
7449
+ id: "fireworks",
7450
+ label: "Fireworks AI",
7451
+ api: "openai-compat",
7452
+ base_url: "https://api.fireworks.ai/inference/v1",
7453
+ key_prefixes: ["fw_"],
7454
+ shared_prefixes: [],
7455
+ key_config_name: "fireworks-api-key",
7456
+ requires_key: true
7457
+ },
7458
+ {
7459
+ id: "ollama",
7460
+ label: "Ollama (local)",
7461
+ api: "openai-compat",
7462
+ base_url: "http://localhost:11434/v1",
7463
+ key_prefixes: [],
7464
+ shared_prefixes: [],
7465
+ key_config_name: "ollama-api-key",
7466
+ requires_key: false
7467
+ }
7468
+ ];
7469
+ cachedEntries = null;
7470
+ }
7471
+ });
7472
+
7192
7473
  // src/config/llm-config.ts
7193
7474
  function parseProvider(raw) {
7194
- if (raw === "anthropic" || raw === "openai") return raw;
7195
- return void 0;
7475
+ if (!raw?.trim()) return void 0;
7476
+ const id = raw.trim();
7477
+ return getProviderSpec(id) ? id : void 0;
7196
7478
  }
7197
7479
  function parseTier(raw) {
7198
7480
  if (raw === "high" || raw === "medium" || raw === "low") return raw;
@@ -7200,7 +7482,7 @@ function parseTier(raw) {
7200
7482
  }
7201
7483
  function parseFailoverOrder(raw) {
7202
7484
  if (!raw?.trim()) return ["openai"];
7203
- return raw.split(",").map((s) => s.trim()).filter((s) => s === "anthropic" || s === "openai");
7485
+ return raw.split(",").map((s) => s.trim()).filter((s) => !!s && !!getProviderSpec(s));
7204
7486
  }
7205
7487
  function parseAutoFailover(raw) {
7206
7488
  if (!raw) return false;
@@ -7215,30 +7497,42 @@ function getOpenAiApiKey() {
7215
7497
  if (fromConfig) return fromConfig;
7216
7498
  return process.env.OPENAI_API_KEY?.trim() || void 0;
7217
7499
  }
7500
+ function getProviderApiKey(provider) {
7501
+ const spec = getProviderSpec(provider);
7502
+ if (!spec) return void 0;
7503
+ const record = loadConfig();
7504
+ const fromConfig = record[spec.key_config_name]?.trim();
7505
+ if (fromConfig) return fromConfig;
7506
+ if (spec.env_var) {
7507
+ const fromEnv = process.env[spec.env_var]?.trim();
7508
+ if (fromEnv) return fromEnv;
7509
+ }
7510
+ return void 0;
7511
+ }
7218
7512
  function hasProviderKey(provider) {
7219
- if (provider === "anthropic") return !!getAnthropicApiKey();
7220
- return !!getOpenAiApiKey();
7513
+ const spec = getProviderSpec(provider);
7514
+ if (!spec) return false;
7515
+ if (!spec.requires_key) return isEndpointEnabled(spec.id) || !!getProviderApiKey(provider);
7516
+ return !!getProviderApiKey(provider);
7221
7517
  }
7222
7518
  function getAvailableProviders() {
7223
- const out = [];
7224
- if (hasProviderKey("anthropic")) out.push("anthropic");
7225
- if (hasProviderKey("openai")) out.push("openai");
7226
- return out;
7519
+ return listProviderSpecs().filter((s) => hasProviderKey(s.id)).map((s) => s.id);
7227
7520
  }
7228
7521
  function hasAnyLlmProvider() {
7229
7522
  return getAvailableProviders().length > 0;
7230
7523
  }
7524
+ function hasKeylessConfiguredProvider() {
7525
+ return listProviderSpecs().some((s) => !s.requires_key && hasProviderKey(s.id));
7526
+ }
7231
7527
  function applyLazyMigration(config) {
7232
7528
  if (migrated) return;
7233
7529
  migrated = true;
7234
7530
  let changed = false;
7235
7531
  const record = config;
7236
7532
  if (!record["llm-primary"]) {
7237
- if (record["api-key"]) {
7238
- record["llm-primary"] = "anthropic";
7239
- changed = true;
7240
- } else if (record["openai-api-key"] || process.env.OPENAI_API_KEY) {
7241
- record["llm-primary"] = "openai";
7533
+ const available = getAvailableProviders();
7534
+ if (available.length > 0) {
7535
+ record["llm-primary"] = available[0];
7242
7536
  changed = true;
7243
7537
  }
7244
7538
  }
@@ -7278,20 +7572,20 @@ function loadLlmConfig() {
7278
7572
  openaiKey: getOpenAiApiKey()
7279
7573
  };
7280
7574
  }
7281
- function getProviderApiKey(provider) {
7282
- if (provider === "anthropic") return getAnthropicApiKey();
7283
- return getOpenAiApiKey();
7284
- }
7285
7575
  function getInvestigationApiKey(provider) {
7286
7576
  if (provider === "anthropic") {
7287
7577
  return process.env.NTRP_INVESTIGATION_API_KEY?.trim() || getAnthropicApiKey();
7288
7578
  }
7289
- return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
7579
+ if (provider === "openai") {
7580
+ return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
7581
+ }
7582
+ return getProviderApiKey(provider);
7290
7583
  }
7291
7584
  var migrated;
7292
7585
  var init_llm_config = __esm({
7293
7586
  "src/config/llm-config.ts"() {
7294
7587
  "use strict";
7588
+ init_providers();
7295
7589
  init_store();
7296
7590
  migrated = false;
7297
7591
  }
@@ -7309,7 +7603,13 @@ function resolvePrimaryApiKey(ctx) {
7309
7603
  if (isInvestigationMode(ctx)) {
7310
7604
  return getInvestigationApiKey(primary) ?? getInvestigationApiKey("anthropic") ?? getInvestigationApiKey("openai");
7311
7605
  }
7312
- return getProviderApiKey(primary) ?? getProviderApiKey("anthropic") ?? getProviderApiKey("openai");
7606
+ const primaryKey = getProviderApiKey(primary);
7607
+ if (primaryKey) return primaryKey;
7608
+ for (const provider of getAvailableProviders()) {
7609
+ const key = getProviderApiKey(provider);
7610
+ if (key) return key;
7611
+ }
7612
+ return void 0;
7313
7613
  }
7314
7614
  function canUseReplAi(ctx) {
7315
7615
  if (!ctx) return false;
@@ -7320,25 +7620,21 @@ function canUseReplAi(ctx) {
7320
7620
  }
7321
7621
  function assertReplAi(ctx) {
7322
7622
  if (!ctx) {
7323
- throw new Error(
7324
- "AI features require stored API keys. Run `ntrp`, then /config set api-key or /config set openai-api-key."
7325
- );
7623
+ throw new Error(`AI features require stored API keys. Run \`ntrp\`, then /connect.`);
7326
7624
  }
7327
7625
  if (!canUseReplAi(ctx)) {
7328
7626
  if (!hasAnyLlmProvider()) {
7329
- throw new Error(
7330
- "No LLM API key configured. Run: /config set api-key (Anthropic) and/or /config set openai-api-key"
7331
- );
7627
+ throw new Error(NO_KEY_MESSAGE);
7332
7628
  }
7333
7629
  throw new Error(
7334
7630
  "AI features run only in the interactive REPL or headless mode with stored keys."
7335
7631
  );
7336
7632
  }
7337
7633
  const key = resolvePrimaryApiKey(ctx);
7338
- if (!key) {
7339
- throw new Error("No LLM API key configured. Run: /config set api-key or /config set openai-api-key");
7634
+ if (!key && !hasKeylessConfiguredProvider()) {
7635
+ throw new Error(NO_KEY_MESSAGE);
7340
7636
  }
7341
- return key;
7637
+ return key ?? "";
7342
7638
  }
7343
7639
  function hasEnvApiKeyHint() {
7344
7640
  return !!(process.env.ANTHROPIC_API_KEY ?? process.env.NTRP_API_KEY ?? process.env.OPENAI_API_KEY);
@@ -7351,10 +7647,12 @@ function describeLlmReadiness() {
7351
7647
  openai: providers.includes("openai")
7352
7648
  };
7353
7649
  }
7650
+ var NO_KEY_MESSAGE;
7354
7651
  var init_gate = __esm({
7355
7652
  "src/ai/llm/gate.ts"() {
7356
7653
  "use strict";
7357
7654
  init_llm_config();
7655
+ NO_KEY_MESSAGE = "No LLM API key configured. Run /connect and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).";
7358
7656
  }
7359
7657
  });
7360
7658
 
@@ -7379,18 +7677,18 @@ var init_repl_api = __esm({
7379
7677
  });
7380
7678
 
7381
7679
  // src/demo/taxonomy-cache.ts
7382
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync9, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
7680
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync11, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
7383
7681
  import { homedir as homedir3 } from "os";
7384
- import { join as join9 } from "path";
7682
+ import { join as join11 } from "path";
7385
7683
  function ensureDir6() {
7386
- if (!existsSync9(NTRP_DIR4)) {
7684
+ if (!existsSync11(NTRP_DIR4)) {
7387
7685
  mkdirSync7(NTRP_DIR4, { recursive: true });
7388
7686
  }
7389
7687
  }
7390
7688
  function loadCachedTaxonomy(profile) {
7391
- if (!existsSync9(TAXONOMY_PATH)) return null;
7689
+ if (!existsSync11(TAXONOMY_PATH)) return null;
7392
7690
  try {
7393
- const parsed = JSON.parse(readFileSync7(TAXONOMY_PATH, "utf-8"));
7691
+ const parsed = JSON.parse(readFileSync9(TAXONOMY_PATH, "utf-8"));
7394
7692
  if (!parsed || typeof parsed !== "object") return null;
7395
7693
  if (parsed.profile_updated_at !== profile.updated_at) return null;
7396
7694
  return parsed;
@@ -7400,10 +7698,10 @@ function loadCachedTaxonomy(profile) {
7400
7698
  }
7401
7699
  function saveCachedTaxonomy(taxonomy) {
7402
7700
  ensureDir6();
7403
- writeFileSync7(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
7701
+ writeFileSync9(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
7404
7702
  }
7405
7703
  function invalidateTaxonomy() {
7406
- if (existsSync9(TAXONOMY_PATH)) {
7704
+ if (existsSync11(TAXONOMY_PATH)) {
7407
7705
  try {
7408
7706
  unlinkSync4(TAXONOMY_PATH);
7409
7707
  } catch {
@@ -7414,8 +7712,8 @@ var NTRP_DIR4, TAXONOMY_PATH;
7414
7712
  var init_taxonomy_cache = __esm({
7415
7713
  "src/demo/taxonomy-cache.ts"() {
7416
7714
  "use strict";
7417
- NTRP_DIR4 = join9(homedir3(), ".ntrp");
7418
- TAXONOMY_PATH = join9(NTRP_DIR4, "demo-taxonomy.json");
7715
+ NTRP_DIR4 = join11(homedir3(), ".ntrp");
7716
+ TAXONOMY_PATH = join11(NTRP_DIR4, "demo-taxonomy.json");
7419
7717
  }
7420
7718
  });
7421
7719
 
@@ -7440,6 +7738,12 @@ var init_types2 = __esm({
7440
7738
  });
7441
7739
 
7442
7740
  // src/ai/llm/errors.ts
7741
+ function isToolsUnsupportedMessage(message) {
7742
+ const msg = message.toLowerCase();
7743
+ const mentionsTools = msg.includes("tool") || msg.includes("function");
7744
+ const mentionsUnsupported = msg.includes("not support") || msg.includes("unsupported") || msg.includes("no support") || msg.includes("not available") || msg.includes("not enabled");
7745
+ return mentionsTools && mentionsUnsupported;
7746
+ }
7443
7747
  function mapAnthropicError(err, provider) {
7444
7748
  const e = err;
7445
7749
  const status = e.status;
@@ -7457,6 +7761,9 @@ function mapAnthropicError(err, provider) {
7457
7761
  if (status === 503) {
7458
7762
  return new LlmError("OVERLOADED", message, provider, status);
7459
7763
  }
7764
+ if (isToolsUnsupportedMessage(message)) {
7765
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
7766
+ }
7460
7767
  if (status === 404 || message.toLowerCase().includes("model")) {
7461
7768
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
7462
7769
  }
@@ -7465,6 +7772,11 @@ function mapAnthropicError(err, provider) {
7465
7772
  }
7466
7773
  return new LlmError("UNKNOWN", message, provider, status);
7467
7774
  }
7775
+ function isModelNotFoundMessage(message) {
7776
+ const msg = message.toLowerCase();
7777
+ if (!msg.includes("model")) return false;
7778
+ return msg.includes("not found") || msg.includes("does not exist") || msg.includes("decommissioned") || msg.includes("deprecated") || msg.includes("retired") || msg.includes("do not have access") || msg.includes("invalid model");
7779
+ }
7468
7780
  function mapOpenAiError(err, provider) {
7469
7781
  const e = err;
7470
7782
  const status = e.status;
@@ -7479,7 +7791,10 @@ function mapOpenAiError(err, provider) {
7479
7791
  if (status === 503 || code === "server_error") {
7480
7792
  return new LlmError("OVERLOADED", message, provider, status);
7481
7793
  }
7482
- if (status === 404 || code === "model_not_found") {
7794
+ if (isToolsUnsupportedMessage(message)) {
7795
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
7796
+ }
7797
+ if (status === 404 || code === "model_not_found" || code === "model_decommissioned" || isModelNotFoundMessage(message)) {
7483
7798
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
7484
7799
  }
7485
7800
  if (code === "context_length_exceeded") {
@@ -7615,8 +7930,15 @@ var init_anthropic = __esm({
7615
7930
  }
7616
7931
  });
7617
7932
 
7618
- // src/ai/llm/adapters/openai.ts
7933
+ // src/ai/llm/adapters/openai-compat.ts
7619
7934
  import OpenAI from "openai";
7935
+ function makeClient(apiKey, baseUrl) {
7936
+ return new OpenAI({
7937
+ // Keyless endpoints (Ollama) still need a non-empty string for the SDK.
7938
+ apiKey: apiKey || "local",
7939
+ ...baseUrl ? { baseURL: baseUrl } : {}
7940
+ });
7941
+ }
7620
7942
  function toOpenAiTools(tools) {
7621
7943
  return tools.map((t) => ({
7622
7944
  type: "function",
@@ -7685,9 +8007,8 @@ function parseResponse2(message) {
7685
8007
  assistant_message: { role: "assistant", content: text, tool_calls }
7686
8008
  };
7687
8009
  }
7688
- async function openaiComplete(apiKey, model, req) {
7689
- const provider = "openai";
7690
- const client = new OpenAI({ apiKey });
8010
+ async function openaiCompatComplete(provider, baseUrl, apiKey, model, req) {
8011
+ const client = makeClient(apiKey, baseUrl);
7691
8012
  try {
7692
8013
  const response = await client.chat.completions.create({
7693
8014
  model,
@@ -7697,7 +8018,7 @@ async function openaiComplete(apiKey, model, req) {
7697
8018
  });
7698
8019
  const choice = response.choices[0];
7699
8020
  if (!choice?.message) {
7700
- throw new Error("OpenAI returned no message");
8021
+ throw new Error(`${provider} returned no message`);
7701
8022
  }
7702
8023
  const parsed = parseResponse2(choice.message);
7703
8024
  if (response.usage) {
@@ -7708,15 +8029,11 @@ async function openaiComplete(apiKey, model, req) {
7708
8029
  }
7709
8030
  return parsed;
7710
8031
  } catch (err) {
7711
- if (err instanceof OpenAI.APIError) {
7712
- throw mapOpenAiError(err, provider);
7713
- }
7714
8032
  throw mapOpenAiError(err, provider);
7715
8033
  }
7716
8034
  }
7717
- async function* openaiStream(apiKey, model, req) {
7718
- const provider = "openai";
7719
- const client = new OpenAI({ apiKey });
8035
+ async function* openaiCompatStream(provider, baseUrl, apiKey, model, req) {
8036
+ const client = makeClient(apiKey, baseUrl);
7720
8037
  try {
7721
8038
  const stream = await client.chat.completions.create({
7722
8039
  model,
@@ -7729,19 +8046,316 @@ async function* openaiStream(apiKey, model, req) {
7729
8046
  if (delta) yield { type: "text_delta", text: delta };
7730
8047
  }
7731
8048
  } catch (err) {
7732
- if (err instanceof OpenAI.APIError) {
7733
- throw mapOpenAiError(err, provider);
7734
- }
7735
8049
  throw mapOpenAiError(err, provider);
7736
8050
  }
7737
8051
  }
7738
- var init_openai = __esm({
7739
- "src/ai/llm/adapters/openai.ts"() {
8052
+ var init_openai_compat = __esm({
8053
+ "src/ai/llm/adapters/openai-compat.ts"() {
7740
8054
  "use strict";
7741
8055
  init_errors2();
7742
8056
  }
7743
8057
  });
7744
8058
 
8059
+ // src/ai/llm/http.ts
8060
+ import { readFileSync as readFileSync10 } from "fs";
8061
+ function fixtureResponse(url, headers) {
8062
+ try {
8063
+ const raw = readFileSync10(process.env.NTRP_LLM_HTTP_FIXTURE, "utf-8");
8064
+ const entries = JSON.parse(raw);
8065
+ const headerValues = Object.values(headers).join(" ");
8066
+ for (const entry of entries) {
8067
+ if (!url.includes(entry.url_includes)) continue;
8068
+ if (entry.auth_includes && !headerValues.includes(entry.auth_includes)) continue;
8069
+ return { status: entry.status, ok: entry.status >= 200 && entry.status < 300, body: entry.body };
8070
+ }
8071
+ } catch {
8072
+ }
8073
+ return { status: 0, ok: false, body: void 0 };
8074
+ }
8075
+ async function llmHttpGetJson(url, headers, timeoutMs = 6e3) {
8076
+ if (process.env.NTRP_LLM_HTTP_FIXTURE) {
8077
+ return fixtureResponse(url, headers);
8078
+ }
8079
+ const controller = new AbortController();
8080
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
8081
+ try {
8082
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
8083
+ let body;
8084
+ try {
8085
+ body = await res.json();
8086
+ } catch {
8087
+ body = void 0;
8088
+ }
8089
+ return { status: res.status, ok: res.ok, body };
8090
+ } catch {
8091
+ return { status: 0, ok: false, body: void 0 };
8092
+ } finally {
8093
+ clearTimeout(timer);
8094
+ }
8095
+ }
8096
+ var init_http = __esm({
8097
+ "src/ai/llm/http.ts"() {
8098
+ "use strict";
8099
+ }
8100
+ });
8101
+
8102
+ // src/ai/llm/ranking.ts
8103
+ function compareModels(a, b) {
8104
+ const createdA = a.created ?? 0;
8105
+ const createdB = b.created ?? 0;
8106
+ if (createdA !== createdB) return createdB - createdA;
8107
+ const versionA = extractVersion(a.id);
8108
+ const versionB = extractVersion(b.id);
8109
+ if (versionA !== versionB) return versionB - versionA;
8110
+ if (a.id.length !== b.id.length) return a.id.length - b.id.length;
8111
+ return a.id.localeCompare(b.id);
8112
+ }
8113
+ function extractVersion(id) {
8114
+ const match = id.match(/(\d+(?:\.\d+)?)/);
8115
+ return match ? Number(match[1]) : 0;
8116
+ }
8117
+ function pickByPatterns(models, patterns) {
8118
+ for (const pattern of patterns) {
8119
+ const matches = models.filter((m) => pattern.test(m.id));
8120
+ if (matches.length > 0) return [...matches].sort(compareModels)[0];
8121
+ }
8122
+ return void 0;
8123
+ }
8124
+ function genericBucket(model) {
8125
+ if (GENERIC_HIGH.test(model.id)) return "high";
8126
+ if (GENERIC_LOW.test(model.id)) return "low";
8127
+ return "medium";
8128
+ }
8129
+ function genericPick(models, tier) {
8130
+ const bucket = models.filter((m) => genericBucket(m) === tier);
8131
+ if (bucket.length > 0) return [...bucket].sort(compareModels)[0];
8132
+ return void 0;
8133
+ }
8134
+ function rankModels(providerId, models) {
8135
+ if (models.length === 0) return null;
8136
+ const preferences = PROVIDER_PREFERENCES[providerId];
8137
+ const picks = {};
8138
+ for (const tier of ["high", "medium", "low"]) {
8139
+ const preferred = preferences ? pickByPatterns(models, preferences[tier]) : void 0;
8140
+ const generic = preferred ?? genericPick(models, tier);
8141
+ if (generic) picks[tier] = generic.id;
8142
+ }
8143
+ const anyModel = [...models].sort(compareModels)[0].id;
8144
+ const high = picks.high ?? picks.medium ?? picks.low ?? anyModel;
8145
+ const medium = picks.medium ?? picks.high ?? picks.low ?? anyModel;
8146
+ const low = picks.low ?? picks.medium ?? picks.high ?? anyModel;
8147
+ return { high, medium, low };
8148
+ }
8149
+ var PROVIDER_PREFERENCES, GENERIC_LOW, GENERIC_HIGH;
8150
+ var init_ranking = __esm({
8151
+ "src/ai/llm/ranking.ts"() {
8152
+ "use strict";
8153
+ PROVIDER_PREFERENCES = {
8154
+ anthropic: {
8155
+ high: [/^claude-opus/i, /^claude-sonnet/i],
8156
+ medium: [/^claude-sonnet/i, /^claude-haiku/i],
8157
+ low: [/^claude-haiku/i, /^claude-sonnet/i]
8158
+ },
8159
+ openai: {
8160
+ high: [/^gpt-5(?!.*(mini|nano|chat))/i, /^gpt-4\.1(?!.*(mini|nano))/i, /^gpt-4o(?!.*mini)/i, /^o3(?!.*mini)/i],
8161
+ medium: [/^gpt-5.*mini/i, /^gpt-4\.1-mini/i, /^gpt-4o-mini/i, /^o4-mini/i],
8162
+ low: [/^gpt-5.*nano/i, /^gpt-4\.1-nano/i, /^gpt-4o-mini/i]
8163
+ },
8164
+ google: {
8165
+ high: [/^gemini-[\d.]+-pro/i, /^gemini-[\d.]+-flash(?!-lite)/i],
8166
+ medium: [/^gemini-[\d.]+-flash(?!-lite|-8b)/i, /^gemini-[\d.]+-pro/i],
8167
+ low: [/^gemini-[\d.]+-flash-lite/i, /flash-8b/i, /^gemini-[\d.]+-flash(?!-lite)/i]
8168
+ },
8169
+ groq: {
8170
+ high: [/llama-3\.3-70b/i, /gpt-oss-120b/i, /70b/i, /deepseek-r1/i],
8171
+ medium: [/llama-3\.1-8b-instant/i, /gpt-oss-20b/i, /llama.*8b/i],
8172
+ low: [/8b-instant/i, /llama.*8b/i, /gemma/i]
8173
+ },
8174
+ deepseek: {
8175
+ high: [/reasoner/i, /chat/i],
8176
+ medium: [/chat/i],
8177
+ low: [/chat/i]
8178
+ },
8179
+ mistral: {
8180
+ high: [/large/i, /medium/i],
8181
+ medium: [/medium/i, /^mistral-small/i],
8182
+ low: [/ministral/i, /small/i, /tiny/i]
8183
+ },
8184
+ xai: {
8185
+ high: [/^grok-\d+(?!.*(mini|fast))/i, /^grok(?!.*(mini|fast))/i],
8186
+ medium: [/^grok.*mini(?!.*fast)/i, /^grok.*fast/i],
8187
+ low: [/^grok.*mini.*fast/i, /^grok.*mini/i]
8188
+ },
8189
+ openrouter: {
8190
+ high: [/^openrouter\/auto$/i, /claude.*opus/i, /^openai\/gpt-5(?!.*(mini|nano))/i, /gemini.*pro/i],
8191
+ medium: [/claude.*sonnet/i, /gpt-5.*mini/i, /gpt-4\.1-mini/i, /gemini.*flash(?!-lite)/i],
8192
+ low: [/claude.*haiku/i, /nano/i, /flash-lite/i, /mini/i]
8193
+ }
8194
+ };
8195
+ GENERIC_LOW = /(mini|nano|lite|tiny|micro|small|haiku|instant|flash|turbo|\b0?\.?5b\b|\b[1-8]b\b)/i;
8196
+ GENERIC_HIGH = /(opus|ultra|large|max\b|\bpro\b|405b|253b|235b|120b|72b|70b|reason|-r1\b|think|deep)/i;
8197
+ }
8198
+ });
8199
+
8200
+ // src/ai/llm/discovery.ts
8201
+ var discovery_exports = {};
8202
+ __export(discovery_exports, {
8203
+ fetchProviderModels: () => fetchProviderModels,
8204
+ filterChatModels: () => filterChatModels,
8205
+ refreshProviderModels: () => refreshProviderModels,
8206
+ refreshStaleProviderCaches: () => refreshStaleProviderCaches,
8207
+ rerankExcluding: () => rerankExcluding,
8208
+ storeDiscoveredModels: () => storeDiscoveredModels
8209
+ });
8210
+ function authHeaders(spec, apiKey) {
8211
+ if (spec.api === "anthropic") {
8212
+ return { "x-api-key": apiKey ?? "", "anthropic-version": "2023-06-01" };
8213
+ }
8214
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
8215
+ }
8216
+ function normalizeItem(spec, item) {
8217
+ let id = item.id ?? "";
8218
+ if (!id) return null;
8219
+ if (id.startsWith("models/")) id = id.slice("models/".length);
8220
+ const model = { id };
8221
+ const display2 = item.display_name ?? item.name;
8222
+ if (display2 && display2 !== id) model.display_name = display2;
8223
+ if (typeof item.created === "number") model.created = item.created;
8224
+ else if (item.created_at) {
8225
+ const parsed = Date.parse(item.created_at);
8226
+ if (!Number.isNaN(parsed)) model.created = Math.floor(parsed / 1e3);
8227
+ }
8228
+ if (typeof item.context_length === "number") model.context_length = item.context_length;
8229
+ if (Array.isArray(item.supported_parameters)) {
8230
+ model.supports_tools = item.supported_parameters.includes("tools");
8231
+ }
8232
+ return model;
8233
+ }
8234
+ async function fetchProviderModels(spec, apiKey, timeoutMs = 6e3) {
8235
+ const headers = authHeaders(spec, apiKey);
8236
+ if (spec.api === "anthropic") {
8237
+ const models2 = [];
8238
+ let url = modelsUrl(spec);
8239
+ for (let page = 0; page < 5 && url; page++) {
8240
+ const res2 = await llmHttpGetJson(url, headers, timeoutMs);
8241
+ if (!res2.ok) return models2.length > 0 ? { ok: true, models: models2 } : { ok: false, status: res2.status };
8242
+ const body2 = res2.body;
8243
+ for (const item of body2?.data ?? []) {
8244
+ const model = normalizeItem(spec, item);
8245
+ if (model) models2.push(model);
8246
+ }
8247
+ url = body2?.has_more && body2.last_id ? `${spec.base_url}/v1/models?limit=100&after_id=${encodeURIComponent(body2.last_id)}` : null;
8248
+ }
8249
+ return { ok: true, models: models2 };
8250
+ }
8251
+ const res = await llmHttpGetJson(modelsUrl(spec), headers, timeoutMs);
8252
+ if (!res.ok) return { ok: false, status: res.status };
8253
+ const body = res.body;
8254
+ const list = Array.isArray(body) ? body : body?.data ?? [];
8255
+ const models = [];
8256
+ for (const item of list) {
8257
+ const model = normalizeItem(spec, item);
8258
+ if (model) models.push(model);
8259
+ }
8260
+ return { ok: true, models };
8261
+ }
8262
+ function filterChatModels(spec, models) {
8263
+ const extra = PROVIDER_EXCLUDE[spec.id];
8264
+ return models.filter((m) => !NON_CHAT.test(m.id) && !(extra && extra.test(m.id)));
8265
+ }
8266
+ function storeDiscoveredModels(providerId, rawModels) {
8267
+ const spec = getProviderSpec(providerId);
8268
+ if (!spec || rawModels.length === 0) return null;
8269
+ const chat = filterChatModels(spec, rawModels);
8270
+ const usable = chat.length > 0 ? chat : rawModels;
8271
+ const stack = rankModels(providerId, usable);
8272
+ if (!stack) return null;
8273
+ const prior = getProviderModels(providerId);
8274
+ const entry = {
8275
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
8276
+ models: usable,
8277
+ tier_stack: stack,
8278
+ ...prior?.quirks ? { quirks: prior.quirks } : {}
8279
+ };
8280
+ setProviderModels(providerId, entry);
8281
+ return entry;
8282
+ }
8283
+ async function refreshProviderModels(providerId, opts = {}) {
8284
+ const spec = getProviderSpec(providerId);
8285
+ if (!spec) return null;
8286
+ if (!opts.force && !isProviderCacheStale(providerId)) {
8287
+ return getProviderModels(providerId) ?? null;
8288
+ }
8289
+ const apiKey = opts.apiKey ?? getProviderApiKey(providerId);
8290
+ if (spec.requires_key && !apiKey) return null;
8291
+ const result = await fetchProviderModels(spec, apiKey);
8292
+ if (!result.ok) return null;
8293
+ return storeDiscoveredModels(providerId, result.models);
8294
+ }
8295
+ function rerankExcluding(providerId, deadModelId) {
8296
+ const prior = getProviderModels(providerId);
8297
+ if (!prior) return null;
8298
+ const survivors = prior.models.filter((m) => m.id !== deadModelId);
8299
+ const stack = rankModels(providerId, survivors);
8300
+ if (!stack) return null;
8301
+ const entry = { ...prior, models: survivors, tier_stack: stack };
8302
+ setProviderModels(providerId, entry);
8303
+ return entry;
8304
+ }
8305
+ async function refreshStaleProviderCaches() {
8306
+ await Promise.allSettled(
8307
+ getAvailableProviders().filter((p) => isProviderCacheStale(p)).map((p) => refreshProviderModels(p))
8308
+ );
8309
+ }
8310
+ var NON_CHAT, PROVIDER_EXCLUDE;
8311
+ var init_discovery = __esm({
8312
+ "src/ai/llm/discovery.ts"() {
8313
+ "use strict";
8314
+ init_llm_config();
8315
+ init_http();
8316
+ init_models_cache();
8317
+ init_providers();
8318
+ init_ranking();
8319
+ NON_CHAT = /(embed|embedding|whisper|tts|dall-e|davinci|babbage|curie|\bada\b|moderation|-audio|realtime|transcribe|-image|rerank|guard|voice|sora|distil-whisper)/i;
8320
+ PROVIDER_EXCLUDE = {
8321
+ openai: /(chatgpt|-search|deep-research|-pro\b|computer-use|codex-mini|-instruct\b)/i
8322
+ };
8323
+ }
8324
+ });
8325
+
8326
+ // src/ai/llm/heal.ts
8327
+ async function healModelNotFound(opts) {
8328
+ const { provider, tier, deadModel } = opts;
8329
+ const refreshed = await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true });
8330
+ let candidate = refreshed?.tier_stack?.[tier] ?? getCachedTierModel(provider, tier);
8331
+ if (!candidate || candidate === deadModel) {
8332
+ const reranked = rerankExcluding(provider, deadModel);
8333
+ candidate = reranked?.tier_stack?.[tier];
8334
+ }
8335
+ if (!candidate || candidate === deadModel) return null;
8336
+ clearDeadOverride(deadModel, opts.ctx);
8337
+ return {
8338
+ model: candidate,
8339
+ notice: `model ${deadModel} is no longer available \u2014 switched to ${candidate}`
8340
+ };
8341
+ }
8342
+ function clearDeadOverride(deadModel, ctx) {
8343
+ if (getConfigValue("llm-model-override")?.trim() === deadModel) {
8344
+ deleteConfigValue("llm-model-override");
8345
+ }
8346
+ if (ctx?.llm?.modelOverride === deadModel) {
8347
+ ctx.llm.modelOverride = void 0;
8348
+ }
8349
+ }
8350
+ var init_heal = __esm({
8351
+ "src/ai/llm/heal.ts"() {
8352
+ "use strict";
8353
+ init_store();
8354
+ init_discovery();
8355
+ init_models_cache();
8356
+ }
8357
+ });
8358
+
7745
8359
  // src/ai/llm/surfaces.ts
7746
8360
  function tierForSurface(surface, userTier) {
7747
8361
  const spec = SURFACE_SPECS[surface];
@@ -7769,6 +8383,26 @@ var init_surfaces = __esm({
7769
8383
  });
7770
8384
 
7771
8385
  // src/ai/llm/session-state.ts
8386
+ var session_state_exports = {};
8387
+ __export(session_state_exports, {
8388
+ availableEngineLabels: () => availableEngineLabels,
8389
+ clearLlmSession: () => clearLlmSession,
8390
+ countAvailableEngines: () => countAvailableEngines,
8391
+ ensureLlmSession: () => ensureLlmSession,
8392
+ formatActiveStack: () => formatActiveStack,
8393
+ formatActiveStackShort: () => formatActiveStackShort,
8394
+ getSessionModelOverride: () => getSessionModelOverride,
8395
+ getSessionProvider: () => getSessionProvider,
8396
+ getSessionTier: () => getSessionTier,
8397
+ isSessionAutoFailover: () => isSessionAutoFailover,
8398
+ resolveActiveProvider: () => resolveActiveProvider,
8399
+ resolveAutoFailoverEnabled: () => resolveAutoFailoverEnabled,
8400
+ resolveEffectiveModelOverride: () => resolveEffectiveModelOverride,
8401
+ resolveEffectiveTier: () => resolveEffectiveTier,
8402
+ resolveModelForActive: () => resolveModelForActive,
8403
+ resolveProviderOrder: () => resolveProviderOrder,
8404
+ validateModelForProvider: () => validateModelForProvider
8405
+ });
7772
8406
  function ensureLlmSession(ctx) {
7773
8407
  if (!ctx.llm) ctx.llm = {};
7774
8408
  return ctx.llm;
@@ -7793,7 +8427,7 @@ function resolveActiveProvider(ctx) {
7793
8427
  if (session && hasProviderKey(session)) return session;
7794
8428
  const cfg = loadLlmConfig();
7795
8429
  if (hasProviderKey(cfg.primary)) return cfg.primary;
7796
- const available = ["anthropic", "openai"].filter(hasProviderKey);
8430
+ const available = getAvailableProviders();
7797
8431
  if (available.length > 0) return available[0];
7798
8432
  return cfg.primary;
7799
8433
  }
@@ -7815,8 +8449,8 @@ function resolveModelForActive(ctx, surface) {
7815
8449
  const provider = resolveActiveProvider(ctx);
7816
8450
  const tier = resolveEffectiveTier(ctx, surface);
7817
8451
  const override = resolveEffectiveModelOverride(ctx);
7818
- const providerOverride = override && getCatalogEntry(override)?.provider === provider ? override : void 0;
7819
- const modelId = resolveModel(provider, tier, providerOverride);
8452
+ const providerOverride = overrideForProvider(override, provider, provider);
8453
+ const modelId = resolveModelSafe(provider, tier, providerOverride);
7820
8454
  return { provider, tier, modelId };
7821
8455
  }
7822
8456
  function resolveProviderOrder(ctx) {
@@ -7827,26 +8461,30 @@ function resolveProviderOrder(ctx) {
7827
8461
  for (const p of cfg.failoverOrder) {
7828
8462
  if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
7829
8463
  }
7830
- for (const p of ["anthropic", "openai"]) {
7831
- if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
8464
+ for (const p of getAvailableProviders()) {
8465
+ if (p !== active && !order.includes(p)) order.push(p);
7832
8466
  }
7833
8467
  return order;
7834
8468
  }
7835
8469
  function formatActiveStack(ctx, surface = "agentic_investigation") {
7836
8470
  const { provider, tier, modelId } = resolveModelForActive(ctx, surface);
7837
- return `${provider} \xB7 ${tier} \xB7 ${modelId}`;
8471
+ return `${provider} \xB7 ${tier} \xB7 ${modelId ?? "no models yet (run /connect)"}`;
8472
+ }
8473
+ function formatActiveStackShort(ctx, surface = "agentic_investigation") {
8474
+ const { provider, tier } = resolveModelForActive(ctx, surface);
8475
+ return `${provider} \xB7 ${tier}`;
7838
8476
  }
7839
8477
  function countAvailableEngines() {
7840
- return ["anthropic", "openai"].filter(hasProviderKey).length;
8478
+ return getAvailableProviders().length;
7841
8479
  }
7842
8480
  function availableEngineLabels() {
7843
- return ["anthropic", "openai"].filter(hasProviderKey);
8481
+ return getAvailableProviders();
7844
8482
  }
7845
8483
  function validateModelForProvider(modelId, provider) {
7846
- const entry = getCatalogEntry(modelId);
7847
- if (!entry) return null;
7848
- if (entry.provider !== provider) {
7849
- return `Model ${modelId} belongs to ${entry.provider}. Run /provider ${entry.provider} first.`;
8484
+ const hint = modelProviderHint(modelId);
8485
+ if (!hint) return null;
8486
+ if (hint !== provider) {
8487
+ return `Model ${modelId} belongs to ${hint}. Run /provider ${hint} first.`;
7850
8488
  }
7851
8489
  return null;
7852
8490
  }
@@ -7860,23 +8498,16 @@ var init_session_state = __esm({
7860
8498
  });
7861
8499
 
7862
8500
  // src/ai/llm/resolver.ts
7863
- function getProviderOrder(config, ctx) {
7864
- void config;
7865
- return resolveProviderOrder(ctx);
7866
- }
7867
8501
  function resolveCompletionContext(surface, opts = {}) {
7868
8502
  const activeProvider = resolveActiveProvider(opts.ctx);
7869
8503
  const tier = opts.tier ?? resolveEffectiveTier(opts.ctx, surface);
7870
8504
  const override = opts.modelOverride ?? resolveEffectiveModelOverride(opts.ctx);
7871
8505
  const providerOrder = resolveProviderOrder(opts.ctx);
7872
8506
  const modelByProvider = {};
7873
- for (const provider of providerOrder) {
7874
- const providerOverride = override && getCatalogEntry(override)?.provider === provider ? override : void 0;
7875
- modelByProvider[provider] = resolveModel(provider, tier, providerOverride);
7876
- }
7877
- if (!modelByProvider[activeProvider]) {
7878
- const activeOverride = override && getCatalogEntry(override)?.provider === activeProvider ? override : void 0;
7879
- modelByProvider[activeProvider] = resolveModel(activeProvider, tier, activeOverride);
8507
+ for (const provider of /* @__PURE__ */ new Set([...providerOrder, activeProvider])) {
8508
+ const providerOverride = overrideForProvider(override, provider, activeProvider);
8509
+ const model = resolveModelSafe(provider, tier, providerOverride);
8510
+ if (model) modelByProvider[provider] = model;
7880
8511
  }
7881
8512
  return {
7882
8513
  providerOrder,
@@ -7902,70 +8533,142 @@ var init_resolver2 = __esm({
7902
8533
 
7903
8534
  // src/ai/llm/failover.ts
7904
8535
  async function completeOnProvider(provider, model, apiKey, req) {
7905
- if (provider === "anthropic") return anthropicComplete(apiKey, model, req);
7906
- return openaiComplete(apiKey, model, req);
8536
+ const spec = getProviderSpec(provider);
8537
+ if (!spec) {
8538
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
8539
+ }
8540
+ if (spec.api === "anthropic") {
8541
+ return anthropicComplete(apiKey ?? "", model, req);
8542
+ }
8543
+ return openaiCompatComplete(provider, spec.base_url, apiKey, model, req);
8544
+ }
8545
+ function usableKey(provider, ctx) {
8546
+ const spec = getProviderSpec(provider);
8547
+ if (!spec) return { ok: false };
8548
+ const apiKey = getApiKeyForProvider(provider, ctx);
8549
+ if (spec.requires_key && !apiKey) return { ok: false };
8550
+ return { ok: true, apiKey };
8551
+ }
8552
+ async function resolveModelWithDiscovery(provider, cfg, opts) {
8553
+ const known = cfg.modelByProvider[provider];
8554
+ if (known) return known;
8555
+ const override = overrideForProvider(opts.modelOverride, provider, cfg.activeProvider);
8556
+ const direct = resolveModelSafe(provider, cfg.tier, override);
8557
+ if (direct) return direct;
8558
+ await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true }).catch(() => null);
8559
+ return resolveModelSafe(provider, cfg.tier, override);
8560
+ }
8561
+ function stripTools(req) {
8562
+ const { tools: _tools, ...rest } = req;
8563
+ return rest;
7907
8564
  }
7908
8565
  async function completeWithFailover(req, opts = {}) {
7909
- const ctx = resolveCompletionContext(req.surface, {
8566
+ const cfg = resolveCompletionContext(req.surface, {
7910
8567
  max_tokens: req.max_tokens,
7911
8568
  tier: opts.tier,
7912
8569
  modelOverride: opts.modelOverride,
7913
8570
  ctx: opts.ctx
7914
8571
  });
7915
- const providers = ctx.providerOrder;
8572
+ const providers = cfg.providerOrder;
7916
8573
  if (providers.length === 0) {
7917
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
8574
+ throw new Error(NO_PROVIDER_MESSAGE);
7918
8575
  }
8576
+ const notices = [];
7919
8577
  let lastError;
7920
8578
  let failoverFrom;
8579
+ const buildMeta = (provider, model, response) => ({
8580
+ provider_used: provider,
8581
+ model_used: model,
8582
+ ...response.token_usage ?? {},
8583
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
8584
+ ...notices.length > 0 ? { notices: [...notices] } : {}
8585
+ });
7921
8586
  for (let i = 0; i < providers.length; i++) {
7922
8587
  const provider = providers[i];
7923
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
7924
- if (!apiKey) continue;
7925
- let model = ctx.modelByProvider[provider] ?? resolveModel(provider, ctx.tier, opts.modelOverride);
8588
+ const key = usableKey(provider, opts.ctx);
8589
+ if (!key.ok) continue;
8590
+ let model = await resolveModelWithDiscovery(provider, cfg, {
8591
+ modelOverride: opts.modelOverride,
8592
+ apiKey: key.apiKey
8593
+ });
8594
+ if (!model) {
8595
+ lastError = new LlmError(
8596
+ "MODEL_NOT_FOUND",
8597
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
8598
+ provider
8599
+ );
8600
+ continue;
8601
+ }
8602
+ let effectiveReq = req;
8603
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
8604
+ effectiveReq = stripTools(req);
8605
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
8606
+ }
7926
8607
  try {
7927
- const response = await completeOnProvider(provider, model, apiKey, req);
7928
- const meta = {
7929
- provider_used: provider,
7930
- model_used: model,
7931
- ...response.token_usage ?? {},
7932
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
7933
- };
8608
+ const response = await completeOnProvider(provider, model, key.apiKey, effectiveReq);
8609
+ const meta = buildMeta(provider, model, response);
7934
8610
  recordLlmUsage(response.token_usage);
7935
8611
  return { response, meta };
7936
8612
  } catch (err) {
7937
- const llmErr = err;
8613
+ let llmErr = err;
7938
8614
  if (llmErr.name !== "LlmError") throw err;
7939
8615
  lastError = llmErr;
7940
- if (llmErr.code === "MODEL_NOT_FOUND") {
7941
- model = resolveModel(provider, ctx.tier);
8616
+ if (llmErr.code === "TOOLS_UNSUPPORTED" && effectiveReq.tools?.length) {
8617
+ markModelNoTools(provider, model);
8618
+ notices.push(`${model} doesn't support tool calling \u2014 retrying without live data tools`);
7942
8619
  try {
7943
- const response = await completeOnProvider(provider, model, apiKey, req);
7944
- const meta = {
7945
- provider_used: provider,
7946
- model_used: model,
7947
- ...response.token_usage ?? {},
7948
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
7949
- };
8620
+ const response = await completeOnProvider(provider, model, key.apiKey, stripTools(effectiveReq));
8621
+ const meta = buildMeta(provider, model, response);
7950
8622
  recordLlmUsage(response.token_usage);
7951
8623
  return { response, meta };
7952
8624
  } catch (retryErr) {
7953
8625
  const retryLlm = retryErr;
7954
- if (retryLlm.name === "LlmError") lastError = retryLlm;
7955
- else throw retryErr;
8626
+ if (retryLlm.name !== "LlmError") throw retryErr;
8627
+ lastError = retryLlm;
8628
+ llmErr = retryLlm;
8629
+ }
8630
+ }
8631
+ if (llmErr.code === "MODEL_NOT_FOUND") {
8632
+ const healed = await healModelNotFound({
8633
+ provider,
8634
+ tier: cfg.tier,
8635
+ deadModel: model,
8636
+ apiKey: key.apiKey,
8637
+ ctx: opts.ctx
8638
+ }).catch(() => null);
8639
+ if (healed) {
8640
+ notices.push(healed.notice);
8641
+ model = healed.model;
8642
+ let retryReq = req;
8643
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
8644
+ retryReq = stripTools(req);
8645
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
8646
+ }
8647
+ try {
8648
+ const response = await completeOnProvider(provider, model, key.apiKey, retryReq);
8649
+ const meta = buildMeta(provider, model, response);
8650
+ recordLlmUsage(response.token_usage);
8651
+ return { response, meta };
8652
+ } catch (retryErr) {
8653
+ const retryLlm = retryErr;
8654
+ if (retryLlm.name !== "LlmError") throw retryErr;
8655
+ lastError = retryLlm;
8656
+ llmErr = retryLlm;
8657
+ }
7956
8658
  }
7957
8659
  }
7958
8660
  if (!isFailoverEligible(llmErr.code)) throw llmErr;
7959
8661
  const next = providers[i + 1];
7960
8662
  if (next) {
7961
8663
  failoverFrom = failoverFrom ?? provider;
8664
+ notices.push(`${provider} unavailable (${llmErr.code.toLowerCase()}) \u2014 trying ${next}`);
7962
8665
  opts.onFailover?.(provider, next, llmErr.code);
7963
8666
  continue;
7964
8667
  }
7965
8668
  throw llmErr;
7966
8669
  }
7967
8670
  }
7968
- throw lastError ?? new Error("No LLM provider available");
8671
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
7969
8672
  }
7970
8673
  async function* streamWithFailover(req, opts = {}) {
7971
8674
  const cfg = resolveCompletionContext(req.surface, {
@@ -7974,23 +8677,47 @@ async function* streamWithFailover(req, opts = {}) {
7974
8677
  modelOverride: opts.modelOverride,
7975
8678
  ctx: opts.ctx
7976
8679
  });
7977
- const providers = getProviderOrder();
8680
+ const providers = cfg.providerOrder;
7978
8681
  if (providers.length === 0) {
7979
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
8682
+ throw new Error(NO_PROVIDER_MESSAGE);
7980
8683
  }
8684
+ const notices = [];
7981
8685
  let lastError;
7982
8686
  let failoverFrom;
8687
+ async function* streamOnProvider(provider, model, apiKey) {
8688
+ const spec = getProviderSpec(provider);
8689
+ if (!spec) {
8690
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
8691
+ }
8692
+ if (spec.api === "anthropic") {
8693
+ yield* anthropicStream(apiKey ?? "", model, req);
8694
+ return;
8695
+ }
8696
+ yield* openaiCompatStream(provider, spec.base_url, apiKey, model, req);
8697
+ }
7983
8698
  for (let i = 0; i < providers.length; i++) {
7984
8699
  const provider = providers[i];
7985
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
7986
- if (!apiKey) continue;
7987
- const model = cfg.modelByProvider[provider] ?? resolveModel(provider, cfg.tier, opts.modelOverride);
7988
- try {
8700
+ const key = usableKey(provider, opts.ctx);
8701
+ if (!key.ok) continue;
8702
+ let model = await resolveModelWithDiscovery(provider, cfg, {
8703
+ modelOverride: opts.modelOverride,
8704
+ apiKey: key.apiKey
8705
+ });
8706
+ if (!model) {
8707
+ lastError = new LlmError(
8708
+ "MODEL_NOT_FOUND",
8709
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
8710
+ provider
8711
+ );
8712
+ continue;
8713
+ }
8714
+ let yieldedAny = false;
8715
+ const attempt = async function* (attemptModel) {
7989
8716
  let fullText = "";
7990
- const streamFn = provider === "anthropic" ? anthropicStream : openaiStream;
7991
- for await (const event of streamFn(apiKey, model, req)) {
8717
+ for await (const event of streamOnProvider(provider, attemptModel, key.apiKey)) {
7992
8718
  if (event.type === "text_delta") {
7993
8719
  fullText += event.text;
8720
+ yieldedAny = true;
7994
8721
  yield event;
7995
8722
  }
7996
8723
  }
@@ -7998,10 +8725,11 @@ async function* streamWithFailover(req, opts = {}) {
7998
8725
  recordLlmUsage({ input_tokens: 0, output_tokens: estimatedOut });
7999
8726
  const meta = {
8000
8727
  provider_used: provider,
8001
- model_used: model,
8728
+ model_used: attemptModel,
8002
8729
  input_tokens: 0,
8003
8730
  output_tokens: estimatedOut,
8004
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
8731
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
8732
+ ...notices.length > 0 ? { notices: [...notices] } : {}
8005
8733
  };
8006
8734
  yield {
8007
8735
  type: "done",
@@ -8013,32 +8741,66 @@ async function* streamWithFailover(req, opts = {}) {
8013
8741
  },
8014
8742
  meta
8015
8743
  };
8744
+ };
8745
+ try {
8746
+ yield* attempt(model);
8016
8747
  return;
8017
8748
  } catch (err) {
8018
8749
  const llmErr = err;
8019
8750
  if (llmErr.name !== "LlmError") throw err;
8020
8751
  lastError = llmErr;
8021
- if (!isFailoverEligible(llmErr.code)) throw llmErr;
8752
+ if (yieldedAny) throw llmErr;
8753
+ if (llmErr.code === "MODEL_NOT_FOUND") {
8754
+ const healed = await healModelNotFound({
8755
+ provider,
8756
+ tier: cfg.tier,
8757
+ deadModel: model,
8758
+ apiKey: key.apiKey,
8759
+ ctx: opts.ctx
8760
+ }).catch(() => null);
8761
+ if (healed) {
8762
+ notices.push(healed.notice);
8763
+ model = healed.model;
8764
+ try {
8765
+ yield* attempt(model);
8766
+ return;
8767
+ } catch (retryErr) {
8768
+ const retryLlm = retryErr;
8769
+ if (retryLlm.name !== "LlmError") throw retryErr;
8770
+ lastError = retryLlm;
8771
+ if (yieldedAny) throw retryLlm;
8772
+ }
8773
+ }
8774
+ }
8775
+ if (!isFailoverEligible(lastError.code)) throw lastError;
8022
8776
  const next = providers[i + 1];
8023
8777
  if (next) {
8024
8778
  failoverFrom = failoverFrom ?? provider;
8025
- opts.onFailover?.(provider, next, llmErr.code);
8779
+ notices.push(`${provider} unavailable (${lastError.code.toLowerCase()}) \u2014 trying ${next}`);
8780
+ opts.onFailover?.(provider, next, lastError.code);
8026
8781
  continue;
8027
8782
  }
8028
- throw llmErr;
8783
+ throw lastError;
8029
8784
  }
8030
8785
  }
8031
- throw lastError ?? new Error("No LLM provider available");
8786
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
8032
8787
  }
8788
+ var NO_PROVIDER_MESSAGE;
8033
8789
  var init_failover = __esm({
8034
8790
  "src/ai/llm/failover.ts"() {
8035
8791
  "use strict";
8036
8792
  init_usage_stats();
8037
8793
  init_anthropic();
8038
- init_openai();
8794
+ init_openai_compat();
8039
8795
  init_catalog();
8796
+ init_discovery();
8040
8797
  init_errors2();
8798
+ init_heal();
8799
+ init_models_cache();
8800
+ init_providers();
8801
+ init_types2();
8041
8802
  init_resolver2();
8803
+ NO_PROVIDER_MESSAGE = "No LLM provider configured. Run /connect and paste any API key (Anthropic, OpenAI, Groq, Gemini, ...).";
8042
8804
  }
8043
8805
  });
8044
8806
 
@@ -8407,8 +9169,8 @@ function markFailure(ctx) {
8407
9169
  }
8408
9170
  async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
8409
9171
  if (!forceRegen) {
8410
- const cached = loadCachedTaxonomy(profile);
8411
- if (cached) return cached;
9172
+ const cached2 = loadCachedTaxonomy(profile);
9173
+ if (cached2) return cached2;
8412
9174
  }
8413
9175
  const spinnerText = forceRegen ? "Rebuilding market taxonomy\u2026" : "Researching your market taxonomy\u2026";
8414
9176
  const spinner = ora2({ text: spinnerText, discardStdin: false }).start();
@@ -8566,7 +9328,7 @@ __export(ingest_exports, {
8566
9328
  });
8567
9329
  import chalk9 from "chalk";
8568
9330
  import ora3 from "ora";
8569
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
9331
+ import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
8570
9332
  import { basename as basename2 } from "path";
8571
9333
  async function handler2(args, ctx) {
8572
9334
  const { positional, flags } = parseArgs(args, [
@@ -8590,7 +9352,7 @@ async function handler2(args, ctx) {
8590
9352
  console.error(chalk9.dim(" /ingest --demo [--scenario <name>]"));
8591
9353
  process.exit(1);
8592
9354
  }
8593
- if (!existsSync10(file)) {
9355
+ if (!existsSync12(file)) {
8594
9356
  console.error(chalk9.red(` File not found: ${file}`));
8595
9357
  process.exit(1);
8596
9358
  }
@@ -8608,7 +9370,7 @@ async function handler2(args, ctx) {
8608
9370
  try {
8609
9371
  await initSchema();
8610
9372
  spinner.text = "Parsing CSV...";
8611
- const content = readFileSync8(file, "utf-8");
9373
+ const content = readFileSync11(file, "utf-8");
8612
9374
  const { rows, headers } = parseCSV(content);
8613
9375
  if (rows.length === 0) {
8614
9376
  spinner.fail("CSV is empty");
@@ -10933,16 +11695,16 @@ var init_compute = __esm({
10933
11695
  });
10934
11696
 
10935
11697
  // src/data/playbook.ts
10936
- import { existsSync as existsSync11, readFileSync as readFileSync9, appendFileSync } from "fs";
10937
- import { join as join10 } from "path";
11698
+ import { existsSync as existsSync13, readFileSync as readFileSync12, appendFileSync } from "fs";
11699
+ import { join as join12 } from "path";
10938
11700
  function playsPath() {
10939
- return join10(getMemoryDir(), PLAYS_FILE);
11701
+ return join12(getMemoryDir(), PLAYS_FILE);
10940
11702
  }
10941
11703
  function getCustomPlays() {
10942
11704
  const path = playsPath();
10943
- if (!existsSync11(path)) return [];
11705
+ if (!existsSync13(path)) return [];
10944
11706
  const out = [];
10945
- for (const line of readFileSync9(path, "utf-8").split("\n")) {
11707
+ for (const line of readFileSync12(path, "utf-8").split("\n")) {
10946
11708
  const trimmed = line.trim();
10947
11709
  if (!trimmed) continue;
10948
11710
  try {
@@ -11534,7 +12296,7 @@ async function runMetricsAnalysis(options = {}) {
11534
12296
  if (options.findings) {
11535
12297
  if (!canUseReplAi(options.ctx)) {
11536
12298
  throw new Error(
11537
- "AI metrics findings require stored API keys. Run `ntrp`, set api-key and/or openai-api-key, then /metrics --findings."
12299
+ "AI metrics findings require stored API keys. Run `ntrp`, then /connect (any provider key), then /metrics --findings."
11538
12300
  );
11539
12301
  }
11540
12302
  options.onProgress?.("findings");
@@ -11943,7 +12705,8 @@ async function* streamFindings(input, ctx) {
11943
12705
  findings,
11944
12706
  model_used: meta.model_used,
11945
12707
  provider_used: meta.provider_used,
11946
- raw_prompt: userMessage
12708
+ raw_prompt: userMessage,
12709
+ usage: meta
11947
12710
  };
11948
12711
  }
11949
12712
  }
@@ -12244,9 +13007,9 @@ var init_tool_schemas = __esm({
12244
13007
  });
12245
13008
 
12246
13009
  // src/ai/privacy.ts
12247
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
13010
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
12248
13011
  import { homedir as homedir4 } from "os";
12249
- import { join as join11 } from "path";
13012
+ import { join as join13 } from "path";
12250
13013
  function stripPII(obj) {
12251
13014
  if (obj === null || obj === void 0) return obj;
12252
13015
  if (typeof obj !== "object") return obj;
@@ -12261,14 +13024,14 @@ function stripPII(obj) {
12261
13024
  return out;
12262
13025
  }
12263
13026
  function ensureAuditDir() {
12264
- if (!existsSync12(AUDIT_DIR)) {
13027
+ if (!existsSync14(AUDIT_DIR)) {
12265
13028
  mkdirSync8(AUDIT_DIR, { recursive: true });
12266
13029
  }
12267
13030
  }
12268
13031
  function logToolCall(entry) {
12269
13032
  ensureAuditDir();
12270
13033
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
12271
- const path = join11(AUDIT_DIR, `agentic-${date}.jsonl`);
13034
+ const path = join13(AUDIT_DIR, `agentic-${date}.jsonl`);
12272
13035
  appendFileSync2(path, JSON.stringify(entry) + "\n");
12273
13036
  }
12274
13037
  var PII_FIELDS, AUDIT_DIR;
@@ -12292,7 +13055,7 @@ var init_privacy = __esm({
12292
13055
  "raw_data",
12293
13056
  "metadata"
12294
13057
  ]);
12295
- AUDIT_DIR = join11(homedir4(), ".ntrp", "audit");
13058
+ AUDIT_DIR = join13(homedir4(), ".ntrp", "audit");
12296
13059
  }
12297
13060
  });
12298
13061
 
@@ -12768,7 +13531,7 @@ __export(ingest_chat_exports, {
12768
13531
  loadDemoFromChat: () => loadDemoFromChat,
12769
13532
  looksLikeFilePath: () => looksLikeFilePath
12770
13533
  });
12771
- import { existsSync as existsSync13 } from "fs";
13534
+ import { existsSync as existsSync15 } from "fs";
12772
13535
  import { basename as basename3, resolve as resolve4 } from "path";
12773
13536
  import { homedir as homedir5 } from "os";
12774
13537
  import chalk15 from "chalk";
@@ -12788,11 +13551,11 @@ function extractFilePath(input) {
12788
13551
  const m = trimmed.match(re);
12789
13552
  if (m?.[1]) {
12790
13553
  const p = expandPath(m[1]);
12791
- if (existsSync13(p)) return p;
13554
+ if (existsSync15(p)) return p;
12792
13555
  }
12793
13556
  if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
12794
13557
  const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
12795
- if (existsSync13(p)) return p;
13558
+ if (existsSync15(p)) return p;
12796
13559
  }
12797
13560
  }
12798
13561
  return null;
@@ -12822,12 +13585,12 @@ async function ingestFromChat(ctx, filePath) {
12822
13585
  }
12823
13586
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
12824
13587
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
12825
- const { readFileSync: readFileSync16 } = await import("fs");
13588
+ const { readFileSync: readFileSync19 } = await import("fs");
12826
13589
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
12827
13590
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
12828
13591
  let headerCheckFailed = false;
12829
13592
  try {
12830
- const raw = readFileSync16(filePath, "utf-8");
13593
+ const raw = readFileSync19(filePath, "utf-8");
12831
13594
  const { headers } = parseCSV2(raw);
12832
13595
  const detected = detectEntityType2(headers, "unknown");
12833
13596
  if (!detected) headerCheckFailed = true;
@@ -13575,12 +14338,12 @@ async function handleDraftHandoff(input) {
13575
14338
  };
13576
14339
  }
13577
14340
  async function executeToolCall(name, input, ctx) {
13578
- const handler45 = HANDLERS[name];
13579
- if (!handler45) {
14341
+ const handler46 = HANDLERS[name];
14342
+ if (!handler46) {
13580
14343
  return JSON.stringify({ error: `Unknown tool '${name}'` });
13581
14344
  }
13582
14345
  const start = Date.now();
13583
- const rawResult = await handler45(input, ctx);
14346
+ const rawResult = await handler46(input, ctx);
13584
14347
  const safeResult = stripPII(rawResult);
13585
14348
  const resultJson = JSON.stringify(safeResult);
13586
14349
  const duration = Date.now() - start;
@@ -13973,7 +14736,7 @@ async function runDiagnosis(options = {}) {
13973
14736
  if (options.findings) {
13974
14737
  if (!canUseReplAi(options.ctx)) {
13975
14738
  throw new Error(
13976
- "AI findings require stored API keys. Run `ntrp`, then /config set api-key and/or /config set openai-api-key, and use /diagnose --findings."
14739
+ "AI findings require stored API keys. Run `ntrp`, then /connect (any provider key), and use /diagnose --findings."
13977
14740
  );
13978
14741
  }
13979
14742
  if (options.deep) {
@@ -14116,7 +14879,7 @@ async function handler3(args, ctx) {
14116
14879
  console.log();
14117
14880
  console.log(" " + chalk16.red("AI findings run only in the interactive REPL."));
14118
14881
  console.log(" " + chalk16.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
14119
- console.log(" " + chalk16.dim("Start with ") + paint("accent", "ntrp") + chalk16.dim(", set ") + paint("accent", "/config set api-key") + chalk16.dim(" or openai-api-key, then /diagnose --findings."));
14882
+ console.log(" " + chalk16.dim("Start with ") + paint("accent", "ntrp") + chalk16.dim(", run ") + paint("accent", "/connect") + chalk16.dim(" (any provider key), then /diagnose --findings."));
14120
14883
  console.log();
14121
14884
  return;
14122
14885
  }
@@ -14776,6 +15539,235 @@ var init_profile2 = __esm({
14776
15539
  }
14777
15540
  });
14778
15541
 
15542
+ // src/ai/llm/detect.ts
15543
+ function detectProviderByKey(key) {
15544
+ const k = key.trim();
15545
+ const specs = listProviderSpecs();
15546
+ let best;
15547
+ for (const spec of specs) {
15548
+ for (const prefix of spec.key_prefixes) {
15549
+ if (k.startsWith(prefix) && (!best || prefix.length > best.length)) {
15550
+ best = { id: spec.id, length: prefix.length };
15551
+ }
15552
+ }
15553
+ }
15554
+ if (best) return { certain: best.id, candidates: [best.id] };
15555
+ const shared = specs.filter((s) => s.shared_prefixes.some((p) => k.startsWith(p)));
15556
+ if (shared.length > 0) return { candidates: shared.map((s) => s.id) };
15557
+ const noPrefix = specs.filter(
15558
+ (s) => s.requires_key && !s.custom && s.key_prefixes.length === 0 && s.shared_prefixes.length === 0
15559
+ );
15560
+ return { candidates: noPrefix.map((s) => s.id) };
15561
+ }
15562
+ async function probeProviders(key, candidateIds, timeoutMs = 5e3) {
15563
+ const results = await Promise.all(
15564
+ candidateIds.map(async (id) => {
15565
+ const spec = getProviderSpec(id);
15566
+ if (!spec) return { id, ok: false, status: 0 };
15567
+ const result = await fetchProviderModels(spec, key, timeoutMs);
15568
+ if (result.ok) return { id, ok: true, models: result.models };
15569
+ return { id, ok: false, status: result.status };
15570
+ })
15571
+ );
15572
+ const accepted = [];
15573
+ let sawNetworkFailure = false;
15574
+ for (const r of results) {
15575
+ if (r.ok && r.models.length > 0) accepted.push({ provider: r.id, models: r.models });
15576
+ else if (!r.ok && r.status === 0) sawNetworkFailure = true;
15577
+ }
15578
+ return { accepted, sawNetworkFailure };
15579
+ }
15580
+ var init_detect = __esm({
15581
+ "src/ai/llm/detect.ts"() {
15582
+ "use strict";
15583
+ init_discovery();
15584
+ init_providers();
15585
+ }
15586
+ });
15587
+
15588
+ // src/services/connect.ts
15589
+ var connect_exports = {};
15590
+ __export(connect_exports, {
15591
+ ConnectCancelled: () => ConnectCancelled,
15592
+ ConnectError: () => ConnectError,
15593
+ connectCustomEndpoint: () => connectCustomEndpoint,
15594
+ connectKeyless: () => connectKeyless,
15595
+ connectWithKey: () => connectWithKey,
15596
+ describeConnectOutcome: () => describeConnectOutcome
15597
+ });
15598
+ function finishConnect(spec, opts) {
15599
+ const before = getAvailableProviders();
15600
+ if (opts.key) {
15601
+ setConfigValue(spec.key_config_name, opts.key);
15602
+ }
15603
+ const entry = opts.models && opts.models.length > 0 ? storeDiscoveredModels(spec.id, opts.models) : null;
15604
+ const cfg = loadLlmConfig();
15605
+ let becamePrimary = false;
15606
+ if (cfg.primary !== spec.id && (before.length === 0 || !hasProviderKey(cfg.primary))) {
15607
+ setConfigValue("llm-primary", spec.id);
15608
+ becamePrimary = true;
15609
+ }
15610
+ return {
15611
+ provider: spec.id,
15612
+ label: spec.label,
15613
+ modelCount: entry?.models.length ?? 0,
15614
+ ...entry ? { stack: entry.tier_stack } : {},
15615
+ becamePrimary,
15616
+ offline: !!opts.offline
15617
+ };
15618
+ }
15619
+ async function connectWithKey(rawKey, opts = {}) {
15620
+ const key = rawKey.trim();
15621
+ if (!key) throw new ConnectError("Empty key.");
15622
+ if (opts.providerId) {
15623
+ const spec = getProviderSpec(opts.providerId);
15624
+ if (!spec) {
15625
+ throw new ConnectError(
15626
+ `Unknown provider "${opts.providerId}". Use a built-in id or /connect --base-url <url> --id ${opts.providerId} for a custom endpoint.`
15627
+ );
15628
+ }
15629
+ const result = await fetchProviderModels(spec, key);
15630
+ if (result.ok) return finishConnect(spec, { key, models: result.models });
15631
+ if (result.status === 0) {
15632
+ return finishConnect(spec, { key, offline: true });
15633
+ }
15634
+ throw new ConnectError(`${spec.label} rejected this key (HTTP ${result.status}) \u2014 double-check it and try again.`);
15635
+ }
15636
+ const detection = detectProviderByKey(key);
15637
+ if (detection.certain) {
15638
+ const spec = getProviderSpec(detection.certain);
15639
+ const result = await fetchProviderModels(spec, key);
15640
+ if (result.ok) return finishConnect(spec, { key, models: result.models });
15641
+ if (result.status === 0) return finishConnect(spec, { key, offline: true });
15642
+ throw new ConnectError(`${spec.label} rejected this key (HTTP ${result.status}) \u2014 double-check it and try again.`);
15643
+ }
15644
+ const report = await probeProviders(key, detection.candidates);
15645
+ if (report.accepted.length === 1) {
15646
+ const match = report.accepted[0];
15647
+ const spec = getProviderSpec(match.provider);
15648
+ if (opts.callbacks?.confirmDetection) {
15649
+ const ok = await opts.callbacks.confirmDetection(match.provider);
15650
+ if (!ok) throw new ConnectCancelled();
15651
+ }
15652
+ return finishConnect(spec, { key, models: match.models });
15653
+ }
15654
+ if (report.accepted.length > 1) {
15655
+ if (opts.callbacks?.chooseProvider) {
15656
+ const chosen = await opts.callbacks.chooseProvider(report.accepted);
15657
+ if (!chosen) throw new ConnectCancelled();
15658
+ const match = report.accepted.find((a) => a.provider === chosen);
15659
+ return finishConnect(getProviderSpec(chosen), { key, models: match.models });
15660
+ }
15661
+ throw new ConnectError(
15662
+ `Multiple providers accepted this key (${report.accepted.map((a) => a.provider).join(", ")}). Re-run with --provider <id>.`
15663
+ );
15664
+ }
15665
+ if (report.sawNetworkFailure) {
15666
+ throw new ConnectError(
15667
+ `Couldn't reach ${detection.candidates.map(providerLabel).join(" / ")} to identify this key. Check your connection, or force a provider with --provider <id>.`
15668
+ );
15669
+ }
15670
+ throw new ConnectError(
15671
+ `No provider accepted this key (tried ${detection.candidates.map(providerLabel).join(", ")}). If it belongs to an OpenAI-compatible endpoint, run /connect --base-url <url>.`
15672
+ );
15673
+ }
15674
+ async function connectCustomEndpoint(opts) {
15675
+ const id = opts.id.trim().toLowerCase();
15676
+ if (!/^[a-z][a-z0-9_-]*$/.test(id)) {
15677
+ throw new ConnectError(`Invalid provider id "${opts.id}" \u2014 use letters, digits, dashes.`);
15678
+ }
15679
+ const baseUrl = opts.baseUrl.trim().replace(/\/+$/, "");
15680
+ if (!/^https?:\/\//.test(baseUrl)) {
15681
+ throw new ConnectError(`Base URL must start with http:// or https:// (got "${opts.baseUrl}").`);
15682
+ }
15683
+ const builtin = getProviderSpec(id);
15684
+ const spec = builtin ? { ...builtin, base_url: baseUrl } : {
15685
+ id,
15686
+ label: opts.label ?? id,
15687
+ api: "openai-compat",
15688
+ base_url: baseUrl,
15689
+ key_prefixes: [],
15690
+ shared_prefixes: [],
15691
+ key_config_name: `${id}-api-key`,
15692
+ requires_key: !!opts.key,
15693
+ custom: true
15694
+ };
15695
+ const result = await fetchProviderModels(spec, opts.key);
15696
+ if (!result.ok) {
15697
+ if (result.status === 0) {
15698
+ throw new ConnectError(`Couldn't reach ${baseUrl} \u2014 check the URL (expects an OpenAI-compatible /models endpoint).`);
15699
+ }
15700
+ if (result.status === 401 || result.status === 403) {
15701
+ throw new ConnectError(
15702
+ opts.key ? `${spec.label} rejected the key (HTTP ${result.status}).` : `${spec.label} requires an API key (HTTP ${result.status}) \u2014 re-run with a key.`
15703
+ );
15704
+ }
15705
+ throw new ConnectError(`${baseUrl} answered HTTP ${result.status} \u2014 is this an OpenAI-compatible endpoint?`);
15706
+ }
15707
+ if (result.models.length === 0) {
15708
+ throw new ConnectError(`${baseUrl} lists no models \u2014 nothing to connect.`);
15709
+ }
15710
+ saveCustomProvider({
15711
+ id,
15712
+ ...opts.label ? { label: opts.label } : {},
15713
+ base_url: baseUrl,
15714
+ requires_key: !!opts.key,
15715
+ enabled: true
15716
+ });
15717
+ return finishConnect(getProviderSpec(id), { key: opts.key, models: result.models });
15718
+ }
15719
+ async function connectKeyless(providerId, baseUrl) {
15720
+ const builtin = getProviderSpec(providerId);
15721
+ if (!builtin) throw new ConnectError(`Unknown provider "${providerId}".`);
15722
+ const spec = baseUrl ? { ...builtin, base_url: baseUrl.replace(/\/+$/, "") } : builtin;
15723
+ const result = await fetchProviderModels(spec, void 0);
15724
+ if (!result.ok) {
15725
+ throw new ConnectError(
15726
+ `${spec.label} not reachable at ${spec.base_url}. Is it running? (ollama serve, then retry)`
15727
+ );
15728
+ }
15729
+ if (result.models.length === 0) {
15730
+ throw new ConnectError(`${spec.label} is running but has no models \u2014 pull one first (e.g. \`ollama pull llama3.2\`).`);
15731
+ }
15732
+ saveCustomProvider({ id: spec.id, base_url: spec.base_url, enabled: true });
15733
+ return finishConnect(getProviderSpec(spec.id), { models: result.models });
15734
+ }
15735
+ function describeConnectOutcome(outcome) {
15736
+ const lines = [];
15737
+ if (outcome.offline) {
15738
+ lines.push(`${outcome.label} key saved \u2014 provider unreachable right now, models will be discovered on first use.`);
15739
+ } else {
15740
+ lines.push(`Connected ${outcome.label} \u2014 ${outcome.modelCount} chat model${outcome.modelCount === 1 ? "" : "s"} available.`);
15741
+ }
15742
+ if (outcome.stack) {
15743
+ lines.push(`high ${outcome.stack.high}`);
15744
+ lines.push(`medium ${outcome.stack.medium}`);
15745
+ lines.push(`low ${outcome.stack.low}`);
15746
+ }
15747
+ if (outcome.becamePrimary) {
15748
+ lines.push(`Primary engine: ${outcome.provider}`);
15749
+ }
15750
+ return lines;
15751
+ }
15752
+ var ConnectError, ConnectCancelled;
15753
+ var init_connect = __esm({
15754
+ "src/services/connect.ts"() {
15755
+ "use strict";
15756
+ init_detect();
15757
+ init_discovery();
15758
+ init_providers();
15759
+ init_llm_config();
15760
+ init_store();
15761
+ ConnectError = class extends Error {
15762
+ };
15763
+ ConnectCancelled = class extends ConnectError {
15764
+ constructor() {
15765
+ super("Connect cancelled.");
15766
+ }
15767
+ };
15768
+ }
15769
+ });
15770
+
14779
15771
  // src/commands/onboard.ts
14780
15772
  var onboard_exports = {};
14781
15773
  __export(onboard_exports, {
@@ -15103,42 +16095,58 @@ function printIntro() {
15103
16095
  }
15104
16096
  async function ensureLlmKeys(session) {
15105
16097
  if (hasAnyLlmProvider()) return;
16098
+ const { connectWithKey: connectWithKey2, describeConnectOutcome: describeConnectOutcome2, ConnectCancelled: ConnectCancelled2 } = await Promise.resolve().then(() => (init_connect(), connect_exports));
16099
+ const { providerLabel: providerLabel2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
16100
+ const { countAvailableEngines: countAvailableEngines2 } = await Promise.resolve().then(() => (init_session_state(), session_state_exports));
15106
16101
  console.log();
15107
16102
  console.log(" " + chalk19.dim("Onboarding uses AI to draft your profile."));
15108
16103
  console.log(
15109
- " " + chalk19.dim("Or run ") + paint("accent", "/config set api-key") + chalk19.dim(" / ") + paint("accent", "/config set openai-api-key") + chalk19.dim(" anytime.")
16104
+ " " + chalk19.dim("Paste any provider's API key \u2014 Anthropic, OpenAI, Groq, Gemini, Mistral, ...")
15110
16105
  );
15111
- const first = await session.choose("Add your first LLM engine", [
15112
- { value: "anthropic", label: "Anthropic (Claude)", description: "Strong tool-using investigation" },
15113
- { value: "openai", label: "OpenAI (GPT)", description: "Full parity on all surfaces" }
15114
- ]);
15115
- const firstLabel = first === "anthropic" ? "Anthropic API key" : "OpenAI API key";
15116
- const firstKey = await session.askSecret(firstLabel, { confirm: true });
15117
- if (first === "anthropic") setConfigValue("api-key", firstKey);
15118
- else setConfigValue("openai-api-key", firstKey);
15119
- setConfigValue("llm-primary", first);
15120
- setConfigValue("llm-tier", "high");
15121
- const second = first === "anthropic" ? "openai" : "anthropic";
15122
- setConfigValue("llm-failover-order", second);
15123
- setConfigValue("llm-auto-failover", "off");
15124
- const addSecond = await session.confirm(
15125
- `Add a second engine (${second}) for switching in the REPL?`,
15126
- false
16106
+ console.log(
16107
+ " " + chalk19.dim("NTRP detects the provider and discovers its models. Or run ") + paint("accent", "/connect") + chalk19.dim(" anytime.")
15127
16108
  );
15128
- if (addSecond) {
15129
- const secondLabel = second === "anthropic" ? "Anthropic API key" : "OpenAI API key";
15130
- const secondKey = await session.askSecret(secondLabel, { confirm: true });
15131
- if (second === "anthropic") setConfigValue("api-key", secondKey);
15132
- else setConfigValue("openai-api-key", secondKey);
16109
+ for (; ; ) {
16110
+ const key = await session.askSecret("LLM API key (any provider)", { confirm: false });
16111
+ const spinner = ora6({ text: "Identifying provider\u2026", discardStdin: false }).start();
16112
+ try {
16113
+ const outcome = await connectWithKey2(key, {
16114
+ callbacks: {
16115
+ confirmDetection: async (providerId) => {
16116
+ spinner.stop();
16117
+ return session.confirm(`Detected ${providerLabel2(providerId)} \u2014 connect it?`, true);
16118
+ },
16119
+ chooseProvider: async (accepted) => {
16120
+ spinner.stop();
16121
+ return session.choose(
16122
+ "Multiple providers accepted this key \u2014 which is it?",
16123
+ accepted.map((a) => ({ value: a.provider, label: providerLabel2(a.provider) }))
16124
+ );
16125
+ }
16126
+ }
16127
+ });
16128
+ spinner.stop();
16129
+ const [headline, ...rest] = describeConnectOutcome2(outcome);
16130
+ console.log(" " + paint("success", "\u2713") + " " + (headline ?? ""));
16131
+ for (const line of rest) console.log(" " + chalk19.dim(line));
16132
+ } catch (err) {
16133
+ spinner.stop();
16134
+ if (!(err instanceof ConnectCancelled2)) {
16135
+ console.log(" " + chalk19.red(String(err.message ?? err)));
16136
+ }
16137
+ const retry = await session.confirm("Try another key?", true);
16138
+ if (retry) continue;
16139
+ if (!hasAnyLlmProvider()) return;
16140
+ }
16141
+ const addAnother = await session.confirm("Add another engine? (switch anytime with /provider)", false);
16142
+ if (!addAnother) break;
16143
+ }
16144
+ if (countAvailableEngines2() >= 2) {
15133
16145
  const enableFailover = await session.confirm(
15134
16146
  "Enable auto-failover on rate limits? (off = you choose engine with /provider)",
15135
16147
  false
15136
16148
  );
15137
- if (enableFailover) setConfigValue("llm-auto-failover", "on");
15138
- } else {
15139
- console.log(
15140
- " " + chalk19.dim(`Single engine \u2014 add ${second} later via /config set ${second === "anthropic" ? "api-key" : "openai-api-key"}.`)
15141
- );
16149
+ setConfigValue("llm-auto-failover", enableFailover ? "on" : "off");
15142
16150
  }
15143
16151
  console.log(" " + paint("success", "\u2713") + " " + chalk19.dim("LLM engines configured. Use /provider to switch."));
15144
16152
  }
@@ -15208,7 +16216,7 @@ __export(new_exports, {
15208
16216
  handler: () => handler6
15209
16217
  });
15210
16218
  import chalk20 from "chalk";
15211
- import { existsSync as existsSync14 } from "fs";
16219
+ import { existsSync as existsSync16 } from "fs";
15212
16220
  import { basename as basename4 } from "path";
15213
16221
  async function handler6(args, ctx) {
15214
16222
  const { positional, flags } = parseArgs(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
@@ -15230,7 +16238,7 @@ async function handler6(args, ctx) {
15230
16238
  console.error(chalk20.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
15231
16239
  return;
15232
16240
  }
15233
- if (source.kind === "file" && !existsSync14(source.path)) {
16241
+ if (source.kind === "file" && !existsSync16(source.path)) {
15234
16242
  console.error(chalk20.red(` File not found: ${source.path}`));
15235
16243
  return;
15236
16244
  }
@@ -15297,11 +16305,11 @@ async function handler6(args, ctx) {
15297
16305
  return "New empty session";
15298
16306
  }
15299
16307
  if (lens === "revenue_metrics") {
15300
- const ora16 = (await import("ora")).default;
16308
+ const ora19 = (await import("ora")).default;
15301
16309
  const { runMetricsAnalysis: runMetricsAnalysis2 } = await Promise.resolve().then(() => (init_metrics_analysis(), metrics_analysis_exports));
15302
16310
  const { renderMetricsReport: renderMetricsReport2 } = await Promise.resolve().then(() => (init_metrics_report(), metrics_report_exports));
15303
16311
  const structured = isStructuredOutput(ctx.execution);
15304
- const spinner = structured ? null : ora16({ text: "Computing SaaS metrics\u2026", indent: 2, discardStdin: false }).start();
16312
+ const spinner = structured ? null : ora19({ text: "Computing SaaS metrics\u2026", indent: 2, discardStdin: false }).start();
15305
16313
  let result;
15306
16314
  try {
15307
16315
  result = await runMetricsAnalysis2({
@@ -15507,7 +16515,7 @@ __export(session_exports, {
15507
16515
  handler: () => handler8
15508
16516
  });
15509
16517
  import chalk22 from "chalk";
15510
- import { join as join12 } from "path";
16518
+ import { join as join14 } from "path";
15511
16519
  import ora7 from "ora";
15512
16520
  async function handler8(args, ctx) {
15513
16521
  const sub = args[0];
@@ -15596,7 +16604,7 @@ async function pickUp(idArg, ctx) {
15596
16604
  }
15597
16605
  resetContextForSwitch(ctx, {
15598
16606
  sessionId: target.id,
15599
- sessionFile: join12(getSessionsDir(), `${target.id}.json`),
16607
+ sessionFile: join14(getSessionsDir(), `${target.id}.json`),
15600
16608
  sessionName: session.name,
15601
16609
  messages: [...session.messages],
15602
16610
  conversation: session.thread ? [...session.thread] : [],
@@ -15908,7 +16916,7 @@ __export(report_exports, {
15908
16916
  handler: () => handler9
15909
16917
  });
15910
16918
  import chalk23 from "chalk";
15911
- import { writeFileSync as writeFileSync8 } from "fs";
16919
+ import { writeFileSync as writeFileSync10 } from "fs";
15912
16920
  import { dirname as dirname2 } from "path";
15913
16921
  async function handler9(args, ctx) {
15914
16922
  const { flags } = parseArgs(args);
@@ -16004,7 +17012,7 @@ async function handler9(args, ctx) {
16004
17012
  if (!isInsideNtrp(resolvedOutput)) {
16005
17013
  console.warn(chalk23.yellow(` Warning: writing report outside ~/.ntrp (${dirname2(resolvedOutput)})`));
16006
17014
  }
16007
- writeFileSync8(resolvedOutput, rendered);
17015
+ writeFileSync10(resolvedOutput, rendered);
16008
17016
  console.log(chalk23.green(` Report written to ${resolvedOutput}`));
16009
17017
  } else if (rendered) {
16010
17018
  console.log(rendered);
@@ -16035,8 +17043,8 @@ var init_report2 = __esm({
16035
17043
  });
16036
17044
 
16037
17045
  // src/output/notes-export.ts
16038
- import { writeFileSync as writeFileSync9 } from "fs";
16039
- import { join as join13 } from "path";
17046
+ import { writeFileSync as writeFileSync11 } from "fs";
17047
+ import { join as join15 } from "path";
16040
17048
  function exportToNotes(data) {
16041
17049
  const { computeResult, divergences, findings, exchanges } = data;
16042
17050
  const { aggregate, segments } = computeResult;
@@ -16045,7 +17053,7 @@ function exportToNotes(data) {
16045
17053
  const timeStr = formatTime(now2);
16046
17054
  const filename = `${dateStr}-${timeStr}-gtm-health.md`;
16047
17055
  const dir = getExportsDir();
16048
- const filepath = join13(dir, filename);
17056
+ const filepath = join15(dir, filename);
16049
17057
  const severityTags = /* @__PURE__ */ new Set();
16050
17058
  for (const f of findings) severityTags.add(f.severity);
16051
17059
  const tags = ["ntrp", "gtm-health", ...severityTags];
@@ -16134,7 +17142,7 @@ function exportToNotes(data) {
16134
17142
  }
16135
17143
  }
16136
17144
  const content = frontmatter.join("\n") + "\n\n" + body.join("\n") + "\n";
16137
- writeFileSync9(filepath, content);
17145
+ writeFileSync11(filepath, content);
16138
17146
  return filepath;
16139
17147
  }
16140
17148
  function formatDate(d) {
@@ -16274,8 +17282,8 @@ __export(backmeup_exports, {
16274
17282
  });
16275
17283
  import chalk25 from "chalk";
16276
17284
  import Papa5 from "papaparse";
16277
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
16278
- import { join as join14 } from "path";
17285
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
17286
+ import { join as join16 } from "path";
16279
17287
  function sanitizeCsvValue(value) {
16280
17288
  if (typeof value !== "string") return value;
16281
17289
  return CSV_FORMULA_RE.test(value) ? `'${value}` : value;
@@ -16311,7 +17319,7 @@ async function handler11(args, _ctx) {
16311
17319
  if (!isInsideNtrp(baseDir)) {
16312
17320
  console.warn(chalk25.yellow(` Warning: writing backup outside ~/.ntrp (${baseDir})`));
16313
17321
  }
16314
- const folder = join14(baseDir, folderName);
17322
+ const folder = join16(baseDir, folderName);
16315
17323
  mkdirSync9(folder, { recursive: true });
16316
17324
  const generatedAt = now2.toISOString();
16317
17325
  let fileCount = 0;
@@ -16326,7 +17334,7 @@ async function handler11(args, _ctx) {
16326
17334
  "Total At Risk": health.total_value_at_risk != null ? formatCurrency(health.total_value_at_risk) : "N/A",
16327
17335
  "Generated At": generatedAt
16328
17336
  }));
16329
- writeFileSync10(join14(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
17337
+ writeFileSync12(join16(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
16330
17338
  fileCount++;
16331
17339
  if (findings.length > 0) {
16332
17340
  const findingsRows = findings.map((f) => ({
@@ -16336,7 +17344,7 @@ async function handler11(args, _ctx) {
16336
17344
  Finding: f.finding,
16337
17345
  "Recommended Plays": f.recommended_plays ? f.recommended_plays.map((p) => p.play_name).join("; ") : ""
16338
17346
  }));
16339
- writeFileSync10(join14(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
17347
+ writeFileSync12(join16(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
16340
17348
  fileCount++;
16341
17349
  }
16342
17350
  for (const vs of health.vital_signs) {
@@ -16346,7 +17354,7 @@ async function handler11(args, _ctx) {
16346
17354
  ...detail
16347
17355
  }));
16348
17356
  const filename = EVIDENCE_FILENAMES[vs.vital_sign] ?? `${vs.vital_sign}.csv`;
16349
- writeFileSync10(join14(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
17357
+ writeFileSync12(join16(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
16350
17358
  fileCount++;
16351
17359
  }
16352
17360
  console.log(chalk25.green(`
@@ -16540,8 +17548,8 @@ var init_bundle = __esm({
16540
17548
  });
16541
17549
 
16542
17550
  // src/repositories/markdown.ts
16543
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
16544
- import { basename as basename5, dirname as dirname3, join as join15, resolve as resolve6 } from "path";
17551
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
17552
+ import { basename as basename5, dirname as dirname3, join as join17, resolve as resolve6 } from "path";
16545
17553
  import { stringify as stringifyYaml } from "yaml";
16546
17554
  function renderMarkdownFiles(pkg) {
16547
17555
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -16764,9 +17772,9 @@ var init_markdown3 = __esm({
16764
17772
  mkdirSync10(root, { recursive: true });
16765
17773
  const written = [];
16766
17774
  for (const file of files) {
16767
- const absolutePath = join15(root, file.relativePath);
17775
+ const absolutePath = join17(root, file.relativePath);
16768
17776
  mkdirSync10(dirname3(absolutePath), { recursive: true });
16769
- writeFileSync11(absolutePath, file.contents, "utf-8");
17777
+ writeFileSync13(absolutePath, file.contents, "utf-8");
16770
17778
  written.push(absolutePath);
16771
17779
  }
16772
17780
  return {
@@ -17070,8 +18078,8 @@ __export(handoff_exports, {
17070
18078
  handler: () => handler13
17071
18079
  });
17072
18080
  import chalk27 from "chalk";
17073
- import { writeFileSync as writeFileSync12 } from "fs";
17074
- import { join as join16 } from "path";
18081
+ import { writeFileSync as writeFileSync14 } from "fs";
18082
+ import { join as join18 } from "path";
17075
18083
  async function handler13(args, ctx) {
17076
18084
  const sub = args[0];
17077
18085
  if (!sub) {
@@ -17129,7 +18137,7 @@ async function interactiveMenu(ctx) {
17129
18137
  }
17130
18138
  }
17131
18139
  async function runReport(args, ctx) {
17132
- const out = join16(getExportsDir(), `report-${stamp()}.md`);
18140
+ const out = join18(getExportsDir(), `report-${stamp()}.md`);
17133
18141
  const { handler: report } = await Promise.resolve().then(() => (init_report2(), report_exports));
17134
18142
  await report(["--format", "md", "--output", out, ...args], ctx);
17135
18143
  recordDeliverable(ctx, { kind: "report", at: (/* @__PURE__ */ new Date()).toISOString(), path: out });
@@ -17177,8 +18185,8 @@ async function runPrompt(target, ctx) {
17177
18185
  return;
17178
18186
  }
17179
18187
  const prompt = draft.markdown;
17180
- const out = join16(getExportsDir(), `handoff-${target}-${stamp()}.md`);
17181
- writeFileSync12(out, prompt, "utf-8");
18188
+ const out = join18(getExportsDir(), `handoff-${target}-${stamp()}.md`);
18189
+ writeFileSync14(out, prompt, "utf-8");
17182
18190
  recordDeliverable(ctx, { kind: `prompt:${target}`, at: (/* @__PURE__ */ new Date()).toISOString(), path: out });
17183
18191
  console.log();
17184
18192
  console.log(" " + paint("accent", `Agent prompt ready (${target})`));
@@ -18050,24 +19058,24 @@ JSON SHAPE:
18050
19058
 
18051
19059
  // src/strategies/readers.ts
18052
19060
  import { createHash } from "crypto";
18053
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
19061
+ import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
18054
19062
  import { extname, resolve as resolve7 } from "path";
18055
19063
  import { parse as parseYaml } from "yaml";
18056
19064
  import { PDFParse } from "pdf-parse";
18057
19065
  async function readStrategyFile(pathOrDash) {
18058
19066
  if (pathOrDash === "-") {
18059
- const text2 = readFileSync10(0, "utf-8");
19067
+ const text2 = readFileSync13(0, "utf-8");
18060
19068
  return createDocument("stdin", null, text2, {});
18061
19069
  }
18062
19070
  const sourcePath = resolve7(pathOrDash);
18063
- if (!existsSync15(sourcePath)) {
19071
+ if (!existsSync17(sourcePath)) {
18064
19072
  throw new NtrpError("strategy_file_not_found", `Strategy file not found: ${pathOrDash}`, 2 /* Usage */);
18065
19073
  }
18066
19074
  const ext = extname(sourcePath).toLowerCase();
18067
19075
  if (ext === ".pdf") {
18068
19076
  return readPdf(sourcePath);
18069
19077
  }
18070
- const text = readFileSync10(sourcePath, "utf-8");
19078
+ const text = readFileSync13(sourcePath, "utf-8");
18071
19079
  if (ext === ".yaml" || ext === ".yml") {
18072
19080
  const structured = parseStructuredYaml(text);
18073
19081
  return createDocument("yaml", sourcePath, text, structured);
@@ -18082,7 +19090,7 @@ function readStrategyText(text) {
18082
19090
  return createDocument("text", null, text, {});
18083
19091
  }
18084
19092
  async function readPdf(sourcePath) {
18085
- const data = readFileSync10(sourcePath);
19093
+ const data = readFileSync13(sourcePath);
18086
19094
  const parser = new PDFParse({ data });
18087
19095
  try {
18088
19096
  const result = await parser.getText();
@@ -18129,15 +19137,15 @@ var init_readers = __esm({
18129
19137
  });
18130
19138
 
18131
19139
  // src/strategies/library.ts
18132
- import { writeFileSync as writeFileSync13 } from "fs";
18133
- import { join as join17 } from "path";
19140
+ import { writeFileSync as writeFileSync15 } from "fs";
19141
+ import { join as join19 } from "path";
18134
19142
  import { stringify as stringifyYaml2 } from "yaml";
18135
19143
  function strategyLibraryPath(slug) {
18136
- return join17(getStrategiesDir(), `${slug}.md`);
19144
+ return join19(getStrategiesDir(), `${slug}.md`);
18137
19145
  }
18138
19146
  function writeStrategyMarkdown(strategy) {
18139
19147
  const path = strategyLibraryPath(strategy.slug);
18140
- writeFileSync13(path, renderStrategyMarkdown(strategy), "utf-8");
19148
+ writeFileSync15(path, renderStrategyMarkdown(strategy), "utf-8");
18141
19149
  return path;
18142
19150
  }
18143
19151
  function renderStrategyMarkdown(strategy) {
@@ -18211,7 +19219,7 @@ var init_library = __esm({
18211
19219
  // src/strategies/connectors.ts
18212
19220
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
18213
19221
  import { homedir as homedir7 } from "os";
18214
- import { basename as basename6, extname as extname2, join as join18, relative, resolve as resolve8, sep as sep3 } from "path";
19222
+ import { basename as basename6, extname as extname2, join as join20, relative, resolve as resolve8, sep as sep3 } from "path";
18215
19223
  function createLocalFolderConnector(options) {
18216
19224
  const rootPath = resolveUserPath2(options.rootPath);
18217
19225
  const name = options.name ?? (basename6(rootPath) || "local");
@@ -18254,7 +19262,7 @@ function createLocalFolderConnector(options) {
18254
19262
  }
18255
19263
  function walkLocalFolder(rootPath, currentPath, refs, opts) {
18256
19264
  for (const entry of readdirSync2(currentPath, { withFileTypes: true })) {
18257
- const absolutePath = join18(currentPath, entry.name);
19265
+ const absolutePath = join20(currentPath, entry.name);
18258
19266
  const relativePath = normalizePath(relative(rootPath, absolutePath));
18259
19267
  if (entry.isDirectory()) {
18260
19268
  if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
@@ -18318,7 +19326,7 @@ function normalizePath(path) {
18318
19326
  }
18319
19327
  function resolveUserPath2(path) {
18320
19328
  if (path === "~") return homedir7();
18321
- if (path.startsWith("~/")) return join18(homedir7(), path.slice(2));
19329
+ if (path.startsWith("~/")) return join20(homedir7(), path.slice(2));
18322
19330
  return resolve8(path);
18323
19331
  }
18324
19332
  var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
@@ -19442,18 +20450,25 @@ __export(config_exports, {
19442
20450
  handler: () => handler23
19443
20451
  });
19444
20452
  import chalk38 from "chalk";
20453
+ import ora10 from "ora";
20454
+ function secretKeys() {
20455
+ const keys = /* @__PURE__ */ new Set(["license-key", "license-instance-id", "voyage-api-key", "tavily-api-key", "brave-api-key"]);
20456
+ for (const spec of listProviderSpecs()) keys.add(spec.key_config_name);
20457
+ return keys;
20458
+ }
19445
20459
  function display(key, value) {
19446
- return SECRET_KEYS.has(key) ? String(value).slice(0, 10) + "..." : String(value);
20460
+ return secretKeys().has(key) ? String(value).slice(0, 10) + "..." : String(value);
19447
20461
  }
19448
20462
  function secretPromptLabel(key) {
19449
- if (key === "api-key") return "Anthropic API key";
19450
- if (key === "openai-api-key") return "OpenAI API key";
20463
+ const spec = findSpecByConfigKey(key);
20464
+ if (spec) return `${spec.label} API key`;
19451
20465
  if (key === "license-key") return "License key";
19452
20466
  return key;
19453
20467
  }
19454
20468
  function usage() {
19455
20469
  console.log(chalk38.dim(" Usage: /config <get|set|list|delete> [key] [value]"));
19456
20470
  console.log(chalk38.dim(" Tip: ") + paint("accent", "/config set api-key") + chalk38.dim(" opens a hidden prompt (no inline paste)."));
20471
+ console.log(chalk38.dim(" Tip: ") + paint("accent", "/connect") + chalk38.dim(" auto-detects the provider from any pasted key."));
19457
20472
  }
19458
20473
  function fail(message, ctx) {
19459
20474
  console.error(chalk38.red(` ${message}`));
@@ -19485,7 +20500,7 @@ async function handler23(args, ctx) {
19485
20500
  return;
19486
20501
  }
19487
20502
  let value = inlineValue;
19488
- if (!value && SECRET_KEYS.has(key)) {
20503
+ if (!value && secretKeys().has(key)) {
19489
20504
  try {
19490
20505
  value = await promptSecretValue(key, ctx);
19491
20506
  } catch (err) {
@@ -19505,8 +20520,24 @@ async function handler23(args, ctx) {
19505
20520
  }
19506
20521
  console.log();
19507
20522
  console.log(chalk38.green(` \u2713 ${key} saved`) + chalk38.dim(` (${display(key, value)})`));
19508
- if (key === "api-key" || key === "openai-api-key") {
19509
- console.log(" " + chalk38.dim("Keys enable AI after analysis. Add both to switch engines with /provider."));
20523
+ const spec = findSpecByConfigKey(key);
20524
+ if (spec) {
20525
+ const spinner = ora10({ text: `Discovering ${spec.label} models\u2026`, discardStdin: false }).start();
20526
+ try {
20527
+ const { refreshProviderModels: refreshProviderModels2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
20528
+ const entry = await refreshProviderModels2(spec.id, { apiKey: value, force: true });
20529
+ if (entry) {
20530
+ spinner.succeed(`${spec.label}: ${entry.models.length} chat models available.`);
20531
+ console.log(
20532
+ " " + chalk38.dim(`high ${entry.tier_stack.high} \xB7 medium ${entry.tier_stack.medium} \xB7 low ${entry.tier_stack.low}`)
20533
+ );
20534
+ } else {
20535
+ spinner.warn(`${spec.label} unreachable \u2014 models will be discovered on first use.`);
20536
+ }
20537
+ } catch {
20538
+ spinner.warn(`${spec.label} unreachable \u2014 models will be discovered on first use.`);
20539
+ }
20540
+ console.log(" " + chalk38.dim("Switch engines with /provider \xB7 browse models with /model list."));
19510
20541
  }
19511
20542
  console.log();
19512
20543
  return;
@@ -19549,15 +20580,14 @@ async function handler23(args, ctx) {
19549
20580
  }
19550
20581
  }
19551
20582
  }
19552
- var SECRET_KEYS;
19553
20583
  var init_config = __esm({
19554
20584
  "src/commands/config.ts"() {
19555
20585
  "use strict";
19556
20586
  init_store();
20587
+ init_providers();
19557
20588
  init_argparse();
19558
20589
  init_prompts();
19559
20590
  init_theme();
19560
- SECRET_KEYS = /* @__PURE__ */ new Set(["api-key", "openai-api-key", "license-key", "license-instance-id"]);
19561
20591
  }
19562
20592
  });
19563
20593
 
@@ -20246,15 +21276,15 @@ var init_checkout = __esm({
20246
21276
  });
20247
21277
 
20248
21278
  // src/services/setup.ts
20249
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
20250
- import { join as join19 } from "path";
21279
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync16 } from "fs";
21280
+ import { join as join21 } from "path";
20251
21281
  function setupCheck() {
20252
21282
  const home = ntrpHome();
20253
21283
  let writable = false;
20254
21284
  try {
20255
21285
  mkdirSync11(home, { recursive: true });
20256
- const probe = join19(home, ".write-check");
20257
- writeFileSync14(probe, "ok\n");
21286
+ const probe = join21(home, ".write-check");
21287
+ writeFileSync16(probe, "ok\n");
20258
21288
  writable = true;
20259
21289
  } catch {
20260
21290
  writable = false;
@@ -20279,7 +21309,8 @@ function setupCheck() {
20279
21309
  tier: llmCfg.tier,
20280
21310
  auto_failover: llmCfg.autoFailover,
20281
21311
  anthropic: llmReady.anthropic,
20282
- openai: llmReady.openai
21312
+ openai: llmReady.openai,
21313
+ providers: llmReady.providers
20283
21314
  }
20284
21315
  },
20285
21316
  license: {
@@ -20291,7 +21322,7 @@ function setupCheck() {
20291
21322
  };
20292
21323
  }
20293
21324
  function readProfileInput(pathOrDash) {
20294
- const raw = pathOrDash === "-" ? readFileSync11(0, "utf-8") : readFileSync11(pathOrDash, "utf-8");
21325
+ const raw = pathOrDash === "-" ? readFileSync14(0, "utf-8") : readFileSync14(pathOrDash, "utf-8");
20295
21326
  return JSON.parse(raw);
20296
21327
  }
20297
21328
  function writeAgentProfile(input) {
@@ -20316,11 +21347,15 @@ function writeAgentProfile(input) {
20316
21347
  saveProfile(profile);
20317
21348
  return profile;
20318
21349
  }
20319
- function applyAgentConfig(opts) {
21350
+ async function applyAgentConfig(opts) {
20320
21351
  if (opts.defaultFormat) setConfigValue("default-format", opts.defaultFormat);
20321
21352
  if (opts.apiKey) setConfigValue("api-key", opts.apiKey);
20322
21353
  if (opts.openaiApiKey) setConfigValue("openai-api-key", opts.openaiApiKey);
20323
- if (opts.llmPrimary === "anthropic" || opts.llmPrimary === "openai") {
21354
+ if (opts.llmKey) {
21355
+ const { connectWithKey: connectWithKey2 } = await Promise.resolve().then(() => (init_connect(), connect_exports));
21356
+ await connectWithKey2(opts.llmKey, { providerId: opts.llmProvider });
21357
+ }
21358
+ if (opts.llmPrimary && getProviderSpec(opts.llmPrimary)) {
20324
21359
  setConfigValue("llm-primary", opts.llmPrimary);
20325
21360
  }
20326
21361
  if (opts.licenseKey) setConfigValue("license-key", opts.licenseKey);
@@ -20330,6 +21365,7 @@ var init_setup = __esm({
20330
21365
  "src/services/setup.ts"() {
20331
21366
  "use strict";
20332
21367
  init_repl_api();
21368
+ init_providers();
20333
21369
  init_llm_config();
20334
21370
  init_store();
20335
21371
  init_profile();
@@ -20372,13 +21408,12 @@ async function handler27(args, ctx) {
20372
21408
  console.log(` Writable: ${result.writable ? "yes" : "no"}`);
20373
21409
  console.log(` Profile: ${result.profile.exists ? "ready" : "missing"} (${result.profile.path})`);
20374
21410
  const llm = result.config.llm;
20375
- if (llm) {
20376
- const engines = (llm.anthropic ? 1 : 0) + (llm.openai ? 1 : 0);
20377
- console.log(` Engines: ${engines} \xB7 default ${llm.primary} \xB7 tier ${llm.tier}`);
20378
- console.log(` Anthropic: ${llm.anthropic ? "set" : "missing"} \xB7 OpenAI: ${llm.openai ? "set" : "missing"}`);
21411
+ if (llm && llm.providers.length > 0) {
21412
+ console.log(` Engines: ${llm.providers.length} \xB7 default ${llm.primary} \xB7 tier ${llm.tier}`);
21413
+ console.log(` Connected: ${llm.providers.join(", ")}`);
20379
21414
  console.log(` Auto-failover: ${llm.auto_failover ? "on" : "off"}`);
20380
21415
  } else {
20381
- console.log(" Engines: missing");
21416
+ console.log(" Engines: missing \u2014 run /connect with any provider key");
20382
21417
  }
20383
21418
  console.log(` License: ${formatLicenseSetupLine(result.license)}`);
20384
21419
  console.log();
@@ -20399,10 +21434,12 @@ async function handler27(args, ctx) {
20399
21434
  sales_motion: getString(flags, "sales-motion")
20400
21435
  };
20401
21436
  }
20402
- applyAgentConfig({
21437
+ await applyAgentConfig({
20403
21438
  defaultFormat: getString(flags, "default-format"),
20404
21439
  apiKey: getString(flags, "api-key"),
20405
21440
  openaiApiKey: getString(flags, "openai-api-key"),
21441
+ llmKey: getString(flags, "llm-key"),
21442
+ llmProvider: getString(flags, "llm-provider"),
20406
21443
  llmPrimary: getString(flags, "llm-primary"),
20407
21444
  licenseKey: getString(flags, "license-key"),
20408
21445
  exportDir: getString(flags, "export-dir")
@@ -20442,13 +21479,13 @@ var init_setup2 = __esm({
20442
21479
 
20443
21480
  // src/conversation/orchestrator.ts
20444
21481
  import chalk43 from "chalk";
20445
- import { writeFileSync as writeFileSync15 } from "fs";
20446
- import { join as join20 } from "path";
21482
+ import { writeFileSync as writeFileSync17 } from "fs";
21483
+ import { join as join22 } from "path";
20447
21484
  async function handleExploreWithoutKey(ctx) {
20448
21485
  console.log();
20449
21486
  console.log(" " + chalk43.red("AI interpretation needs an LLM API key saved in config."));
20450
21487
  console.log(
20451
- " " + chalk43.dim("Set with: ") + paint("accent", "/config set api-key") + chalk43.dim(" (Anthropic) or ") + paint("accent", "/config set openai-api-key")
21488
+ " " + chalk43.dim("Run ") + paint("accent", "/connect") + chalk43.dim(" and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).")
20452
21489
  );
20453
21490
  console.log(" " + chalk43.dim("Number crunching works without a key \u2014 only Q&A in the REPL needs one."));
20454
21491
  if (ctx.gapAudit) {
@@ -20620,8 +21657,8 @@ async function callProvider(texts) {
20620
21657
  async function embedText(text) {
20621
21658
  const key = text.trim();
20622
21659
  if (!key) return null;
20623
- const cached = cache.get(key);
20624
- if (cached) return cached;
21660
+ const cached2 = cache.get(key);
21661
+ if (cached2) return cached2;
20625
21662
  const result = await callProvider([key]);
20626
21663
  const vec = result?.[0] ?? null;
20627
21664
  if (vec) cache.set(key, vec);
@@ -20631,8 +21668,8 @@ async function embedItems(items) {
20631
21668
  const needing = [];
20632
21669
  const out = items.map((it, index) => {
20633
21670
  if (it.embedding && it.embedding.length > 0) return { ...it };
20634
- const cached = cache.get(it.text.trim());
20635
- if (cached) return { ...it, embedding: cached };
21671
+ const cached2 = cache.get(it.text.trim());
21672
+ if (cached2) return { ...it, embedding: cached2 };
20636
21673
  needing.push({ index, text: it.text });
20637
21674
  return { ...it };
20638
21675
  });
@@ -20791,17 +21828,17 @@ var init_retrieval = __esm({
20791
21828
  });
20792
21829
 
20793
21830
  // src/memory/knowledge.ts
20794
- import { existsSync as existsSync17, readFileSync as readFileSync12, appendFileSync as appendFileSync3, readdirSync as readdirSync3 } from "fs";
20795
- import { join as join21 } from "path";
21831
+ import { existsSync as existsSync19, readFileSync as readFileSync15, appendFileSync as appendFileSync3, readdirSync as readdirSync3 } from "fs";
21832
+ import { join as join23 } from "path";
20796
21833
  import { randomUUID as randomUUID4 } from "crypto";
20797
21834
  function knowledgePath() {
20798
- return join21(getMemoryDir(), KNOWLEDGE_FILE);
21835
+ return join23(getMemoryDir(), KNOWLEDGE_FILE);
20799
21836
  }
20800
21837
  function loadKnowledgeChunks() {
20801
21838
  const path = knowledgePath();
20802
- if (!existsSync17(path)) return [];
21839
+ if (!existsSync19(path)) return [];
20803
21840
  const out = [];
20804
- for (const line of readFileSync12(path, "utf-8").split("\n")) {
21841
+ for (const line of readFileSync15(path, "utf-8").split("\n")) {
20805
21842
  const trimmed = line.trim();
20806
21843
  if (!trimmed) continue;
20807
21844
  try {
@@ -20902,17 +21939,17 @@ __export(store_exports2, {
20902
21939
  rewriteJsonl: () => rewriteJsonl,
20903
21940
  scrubText: () => scrubText
20904
21941
  });
20905
- import { existsSync as existsSync18, readFileSync as readFileSync13, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync16 } from "fs";
20906
- import { join as join22 } from "path";
21942
+ import { existsSync as existsSync20, readFileSync as readFileSync16, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync18 } from "fs";
21943
+ import { join as join24 } from "path";
20907
21944
  import { randomUUID as randomUUID5 } from "crypto";
20908
21945
  function memPath(file) {
20909
- return join22(getMemoryDir(), file);
21946
+ return join24(getMemoryDir(), file);
20910
21947
  }
20911
21948
  function readJsonl(file) {
20912
21949
  const path = memPath(file);
20913
- if (!existsSync18(path)) return [];
21950
+ if (!existsSync20(path)) return [];
20914
21951
  const out = [];
20915
- for (const line of readFileSync13(path, "utf-8").split("\n")) {
21952
+ for (const line of readFileSync16(path, "utf-8").split("\n")) {
20916
21953
  const trimmed = line.trim();
20917
21954
  if (!trimmed) continue;
20918
21955
  try {
@@ -20930,7 +21967,7 @@ function appendJsonl(file, obj) {
20930
21967
  }
20931
21968
  function rewriteJsonl(file, rows) {
20932
21969
  try {
20933
- writeFileSync16(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
21970
+ writeFileSync18(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
20934
21971
  } catch {
20935
21972
  }
20936
21973
  }
@@ -20992,7 +22029,7 @@ function loadWinSnippets() {
20992
22029
  const out = [];
20993
22030
  for (const name of readdirSync4(dir)) {
20994
22031
  if (!name.endsWith(".md") || name.toLowerCase() === "readme.md") continue;
20995
- const raw = readFileSync13(join22(dir, name), "utf-8");
22032
+ const raw = readFileSync16(join24(dir, name), "utf-8");
20996
22033
  const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? name.replace(/\.md$/, "");
20997
22034
  const body = raw.replace(/^#.*$/m, "").replace(/\s+/g, " ").trim().slice(0, 300);
20998
22035
  out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });
@@ -21081,7 +22118,7 @@ var init_store2 = __esm({
21081
22118
  });
21082
22119
 
21083
22120
  // src/services/smoke-protocol.ts
21084
- import { join as join23 } from "path";
22121
+ import { join as join25 } from "path";
21085
22122
  function isSmokeProtocolTrigger(input) {
21086
22123
  return normalize(input).includes(SMOKE_TRIGGER_PHRASE);
21087
22124
  }
@@ -21117,7 +22154,7 @@ async function runSmokeProtocol(_input, ctx) {
21117
22154
  });
21118
22155
  const proposalResult = await proposeRepositoryExport({
21119
22156
  target: "markdown",
21120
- directory: join23(getExportsDir(), "repository-smoke"),
22157
+ directory: join25(getExportsDir(), "repository-smoke"),
21121
22158
  source: "smoke_protocol",
21122
22159
  modelOrFixture: "smoke-protocol-v1"
21123
22160
  });
@@ -21210,13 +22247,13 @@ var init_smoke_protocol = __esm({
21210
22247
  });
21211
22248
 
21212
22249
  // src/cli/nl.ts
21213
- import ora10 from "ora";
22250
+ import ora11 from "ora";
21214
22251
  import chalk44 from "chalk";
21215
22252
  async function runNaturalLanguage(input, ctx) {
21216
22253
  if (isSmokeProtocolTrigger(input)) {
21217
22254
  recordMessage(ctx, "user", input);
21218
22255
  console.log();
21219
- const spinner2 = ora10({ text: "Running smoke protocol\u2026", color: "cyan", discardStdin: false }).start();
22256
+ const spinner2 = ora11({ text: "Running smoke protocol\u2026", color: "cyan", discardStdin: false }).start();
21220
22257
  try {
21221
22258
  const result = await runSmokeProtocol(input, ctx);
21222
22259
  spinner2.succeed("Smoke protocol complete");
@@ -21243,7 +22280,7 @@ async function runNaturalLanguage(input, ctx) {
21243
22280
  let snapshot = ctx.snapshot.computeResult;
21244
22281
  if (!snapshot) {
21245
22282
  const metricsFirst = prefersMetricsFirstContext(ctx);
21246
- const spinner2 = ora10({
22283
+ const spinner2 = ora11({
21247
22284
  text: metricsFirst ? "Loading session context\u2026" : "Computing vital signs\u2026",
21248
22285
  color: "cyan",
21249
22286
  discardStdin: false
@@ -21268,7 +22305,7 @@ async function runNaturalLanguage(input, ctx) {
21268
22305
  }
21269
22306
  console.log();
21270
22307
  const memoryBlock = await buildMemoryBlock(input).catch(() => "");
21271
- const spinner = ora10({ text: "Thinking\u2026", color: "cyan", discardStdin: false }).start();
22308
+ const spinner = ora11({ text: "Thinking\u2026", color: "cyan", discardStdin: false }).start();
21272
22309
  let lastAnswer = "";
21273
22310
  let rawHistory = [];
21274
22311
  const toolsUsed = [];
@@ -21559,7 +22596,7 @@ __export(metrics_exports, {
21559
22596
  handler: () => handler29
21560
22597
  });
21561
22598
  import chalk46 from "chalk";
21562
- import ora11 from "ora";
22599
+ import ora12 from "ora";
21563
22600
  async function handler29(args, ctx) {
21564
22601
  await hydrateAnalysisFromPersistedState(ctx);
21565
22602
  const { flags } = parseArgs(args, ["findings"]);
@@ -21601,7 +22638,7 @@ async function handler29(args, ctx) {
21601
22638
  await initSchema();
21602
22639
  await autoGenerateSegments();
21603
22640
  printCompanionBanner("metrics", ctx.analysis.primary);
21604
- const spinner = ora11({
22641
+ const spinner = ora12({
21605
22642
  text: "Computing SaaS metrics\u2026",
21606
22643
  indent: 2,
21607
22644
  discardStdin: false
@@ -21800,7 +22837,7 @@ __export(feedback_exports, {
21800
22837
  handler: () => handler30
21801
22838
  });
21802
22839
  import chalk47 from "chalk";
21803
- import ora12 from "ora";
22840
+ import ora13 from "ora";
21804
22841
  async function handler30(args, ctx) {
21805
22842
  const feedbackText = args.join(" ").trim();
21806
22843
  if (!feedbackText) {
@@ -21828,7 +22865,7 @@ async function handler30(args, ctx) {
21828
22865
  console.log();
21829
22866
  return;
21830
22867
  }
21831
- const spinner = ora12({ text: "Applying feedback\u2026", discardStdin: false }).start();
22868
+ const spinner = ora13({ text: "Applying feedback\u2026", discardStdin: false }).start();
21832
22869
  try {
21833
22870
  const result = await applyFeedback(profile, feedbackText, ctx);
21834
22871
  spinner.succeed("Feedback applied");
@@ -21860,7 +22897,7 @@ var recap_exports = {};
21860
22897
  __export(recap_exports, {
21861
22898
  handler: () => handler31
21862
22899
  });
21863
- import ora13 from "ora";
22900
+ import ora14 from "ora";
21864
22901
  import chalk48 from "chalk";
21865
22902
  async function handler31(_args, ctx) {
21866
22903
  if (ctx.messages.length === 0) {
@@ -21897,7 +22934,7 @@ ${companyBlock}` : "",
21897
22934
  const prefix = msg.role === "user" ? "USER" : "ASSISTANT";
21898
22935
  conversationLines.push(`[${prefix}]: ${msg.content}`);
21899
22936
  }
21900
- const spinner = ora13({ text: "Summarizing session\u2026", color: "cyan", discardStdin: false }).start();
22937
+ const spinner = ora14({ text: "Summarizing session\u2026", color: "cyan", discardStdin: false }).start();
21901
22938
  try {
21902
22939
  const { text: fullText } = await llmStreamText(
21903
22940
  "recap",
@@ -22025,7 +23062,7 @@ var init_recall = __esm({
22025
23062
 
22026
23063
  // src/memory/feedback.ts
22027
23064
  import { appendFileSync as appendFileSync5 } from "fs";
22028
- import { join as join24 } from "path";
23065
+ import { join as join26 } from "path";
22029
23066
  import { randomUUID as randomUUID6 } from "crypto";
22030
23067
  function summarize(text) {
22031
23068
  return text.replace(/[#*`>_]/g, "").replace(/\s+/g, " ").trim().slice(0, 200);
@@ -22041,7 +23078,7 @@ function recordFeedback(input) {
22041
23078
  created_at: (/* @__PURE__ */ new Date()).toISOString()
22042
23079
  };
22043
23080
  try {
22044
- appendFileSync5(join24(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
23081
+ appendFileSync5(join26(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
22045
23082
  } catch {
22046
23083
  }
22047
23084
  if (input.rating === "positive") {
@@ -22132,7 +23169,7 @@ __export(knowledge_exports, {
22132
23169
  handler: () => handler35
22133
23170
  });
22134
23171
  import chalk52 from "chalk";
22135
- import ora14 from "ora";
23172
+ import ora15 from "ora";
22136
23173
  async function handler35(args, ctx) {
22137
23174
  const sub = (args[0] ?? "list").toLowerCase();
22138
23175
  if (sub === "add") {
@@ -22144,7 +23181,7 @@ async function handler35(args, ctx) {
22144
23181
  console.log();
22145
23182
  return;
22146
23183
  }
22147
- const spin = ctx.execution.progress ? ora14({ text: "Ingesting knowledge\u2026", color: "cyan", discardStdin: false }).start() : null;
23184
+ const spin = ctx.execution.progress ? ora15({ text: "Ingesting knowledge\u2026", color: "cyan", discardStdin: false }).start() : null;
22148
23185
  try {
22149
23186
  const result = await addKnowledgeFile(path);
22150
23187
  spin?.succeed(`Indexed "${result.title}"`);
@@ -22454,8 +23491,8 @@ var switch_exports = {};
22454
23491
  __export(switch_exports, {
22455
23492
  handler: () => handler39
22456
23493
  });
22457
- import { join as join25 } from "path";
22458
- import ora15 from "ora";
23494
+ import { join as join27 } from "path";
23495
+ import ora16 from "ora";
22459
23496
  import chalk56 from "chalk";
22460
23497
  async function handler39(args, ctx) {
22461
23498
  if (args.length === 0) {
@@ -22470,7 +23507,7 @@ async function handler39(args, ctx) {
22470
23507
  }
22471
23508
  const exchangeCount = Math.floor(ctx.messages.length / 2);
22472
23509
  if (exchangeCount > 0) {
22473
- const spinner = ora15({ text: "Saving current session\u2026", color: "cyan", discardStdin: false }).start();
23510
+ const spinner = ora16({ text: "Saving current session\u2026", color: "cyan", discardStdin: false }).start();
22474
23511
  await closeSession(ctx);
22475
23512
  const fromLabel = ctx.sessionName ? `"${ctx.sessionName}"` : ctx.sessionId.slice(-4);
22476
23513
  spinner.succeed(`Saved ${fromLabel}`);
@@ -22485,7 +23522,7 @@ async function handler39(args, ctx) {
22485
23522
  }
22486
23523
  const context = buildSwitchContext(session);
22487
23524
  const newId = makeSessionId();
22488
- const newFile = join25(getSessionsDir(), `${newId}.json`);
23525
+ const newFile = join27(getSessionsDir(), `${newId}.json`);
22489
23526
  resetContextForSwitch(ctx, {
22490
23527
  sessionId: newId,
22491
23528
  sessionFile: newFile,
@@ -22511,7 +23548,7 @@ async function handler39(args, ctx) {
22511
23548
  return `Switched to "${targetName}"`;
22512
23549
  } else {
22513
23550
  const newId = makeSessionId();
22514
- const newFile = join25(getSessionsDir(), `${newId}.json`);
23551
+ const newFile = join27(getSessionsDir(), `${newId}.json`);
22515
23552
  resetContextForSwitch(ctx, {
22516
23553
  sessionId: newId,
22517
23554
  sessionFile: newFile,
@@ -22561,13 +23598,177 @@ var init_switch = __esm({
22561
23598
  }
22562
23599
  });
22563
23600
 
22564
- // src/commands/provider.ts
22565
- var provider_exports = {};
22566
- __export(provider_exports, {
23601
+ // src/commands/connect.ts
23602
+ var connect_exports2 = {};
23603
+ __export(connect_exports2, {
22567
23604
  handler: () => handler40
22568
23605
  });
22569
23606
  import chalk57 from "chalk";
23607
+ import ora17 from "ora";
23608
+ function usage2() {
23609
+ console.log(chalk57.dim(" Usage: /connect paste any provider key"));
23610
+ console.log(chalk57.dim(" /connect <provider> key for a specific provider (or: ollama)"));
23611
+ console.log(chalk57.dim(" /connect --key <key> non-interactive (auto-detects provider)"));
23612
+ console.log(chalk57.dim(" /connect --base-url <url> [--id <name>] [--key <key>] custom endpoint"));
23613
+ }
23614
+ function printOutcome(outcome, ctx) {
23615
+ console.log();
23616
+ const [headline, ...rest] = describeConnectOutcome(outcome);
23617
+ console.log(" " + paint("success", "\u2713") + " " + chalk57.bold(headline ?? ""));
23618
+ for (const line of rest) {
23619
+ console.log(" " + chalk57.dim(line));
23620
+ }
23621
+ console.log();
23622
+ console.log(" " + chalk57.dim(`Active stack: ${formatActiveStack(ctx)}`));
23623
+ console.log(" " + chalk57.dim("/provider to switch engines \xB7 /model list to browse models"));
23624
+ console.log();
23625
+ }
23626
+ function printError(err, ctx) {
23627
+ const message = err instanceof Error ? err.message : String(err);
23628
+ console.log();
23629
+ console.log(" " + chalk57.red(message));
23630
+ console.log();
23631
+ if (ctx.oneShot) process.exit(1);
23632
+ }
23633
+ async function promptKey(session, label) {
23634
+ console.log();
23635
+ console.log(
23636
+ " " + chalk57.dim("Paste once, press Enter. Stored in ") + paint("accent", "~/.ntrp/config.json") + chalk57.dim(" only.")
23637
+ );
23638
+ return session.askSecret(label, { confirm: false });
23639
+ }
22570
23640
  async function handler40(args, ctx) {
23641
+ const { positional, flags } = parseArgs(args);
23642
+ const sub = positional[0]?.toLowerCase();
23643
+ if (sub === "help") {
23644
+ usage2();
23645
+ return;
23646
+ }
23647
+ const inlineKey = getString(flags, "key");
23648
+ const baseUrl = getString(flags, "base-url", "url");
23649
+ const forcedProvider = getString(flags, "provider") ?? (sub && sub !== "help" ? sub : void 0);
23650
+ const customId = getString(flags, "id");
23651
+ const label = getString(flags, "label");
23652
+ const forcedSpec = forcedProvider ? getProviderSpec(forcedProvider) : void 0;
23653
+ if (forcedSpec && !forcedSpec.requires_key && !inlineKey) {
23654
+ const spinner2 = ora17({ text: `Looking for ${forcedSpec.label}\u2026`, discardStdin: false }).start();
23655
+ try {
23656
+ const outcome = await connectKeyless(forcedSpec.id, baseUrl);
23657
+ spinner2.stop();
23658
+ printOutcome(outcome, ctx);
23659
+ } catch (err) {
23660
+ spinner2.stop();
23661
+ printError(err, ctx);
23662
+ }
23663
+ return;
23664
+ }
23665
+ if (baseUrl && !forcedSpec) {
23666
+ const id = customId ?? forcedProvider ?? hostToId(baseUrl);
23667
+ let key2 = inlineKey;
23668
+ if (!key2 && !ctx.oneShot && process.stdin.isTTY) {
23669
+ const session2 = createPromptSession(ctx.rl, ctx);
23670
+ try {
23671
+ const needsKey = await session2.confirm("Does this endpoint need an API key?", false);
23672
+ if (needsKey) key2 = await promptKey(session2, `API key for ${id}`);
23673
+ } finally {
23674
+ session2.close();
23675
+ }
23676
+ }
23677
+ const spinner2 = ora17({ text: `Checking ${baseUrl}\u2026`, discardStdin: false }).start();
23678
+ try {
23679
+ const outcome = await connectCustomEndpoint({ id, baseUrl, key: key2, label });
23680
+ spinner2.stop();
23681
+ printOutcome(outcome, ctx);
23682
+ } catch (err) {
23683
+ spinner2.stop();
23684
+ printError(err, ctx);
23685
+ }
23686
+ return;
23687
+ }
23688
+ if (forcedProvider && !forcedSpec) {
23689
+ console.log();
23690
+ console.log(" " + chalk57.red(`Unknown provider: ${forcedProvider}`));
23691
+ console.log(
23692
+ " " + chalk57.dim("Built-ins: anthropic, openai, google, groq, mistral, deepseek, xai, openrouter, together, fireworks, ollama")
23693
+ );
23694
+ console.log(" " + chalk57.dim(`Custom endpoint: /connect --base-url <url> --id ${forcedProvider}`));
23695
+ console.log();
23696
+ if (ctx.oneShot) process.exit(1);
23697
+ return;
23698
+ }
23699
+ let key = inlineKey;
23700
+ let session;
23701
+ if (!key) {
23702
+ if (ctx.oneShot || !process.stdin.isTTY) {
23703
+ printError(new ConnectError("Non-interactive mode needs --key <key>."), ctx);
23704
+ usage2();
23705
+ return;
23706
+ }
23707
+ session = createPromptSession(ctx.rl, ctx);
23708
+ key = await promptKey(
23709
+ session,
23710
+ forcedSpec ? `${forcedSpec.label} API key` : "LLM API key (any provider)"
23711
+ );
23712
+ }
23713
+ const spinner = ora17({ text: "Identifying provider\u2026", discardStdin: false }).start();
23714
+ try {
23715
+ const outcome = await connectWithKey(key, {
23716
+ providerId: forcedSpec?.id,
23717
+ callbacks: session ? {
23718
+ confirmDetection: async (providerId) => {
23719
+ spinner.stop();
23720
+ return session.confirm(`Detected ${providerLabel(providerId)} \u2014 connect it?`, true);
23721
+ },
23722
+ chooseProvider: async (accepted) => {
23723
+ spinner.stop();
23724
+ return session.choose(
23725
+ "Multiple providers accepted this key \u2014 which is it?",
23726
+ accepted.map((a) => ({ value: a.provider, label: providerLabel(a.provider) }))
23727
+ );
23728
+ }
23729
+ } : void 0
23730
+ });
23731
+ spinner.stop();
23732
+ printOutcome(outcome, ctx);
23733
+ } catch (err) {
23734
+ spinner.stop();
23735
+ if (err instanceof ConnectCancelled) {
23736
+ console.log(" " + chalk57.dim("Cancelled."));
23737
+ console.log();
23738
+ } else {
23739
+ printError(err, ctx);
23740
+ }
23741
+ } finally {
23742
+ session?.close();
23743
+ }
23744
+ }
23745
+ function hostToId(url) {
23746
+ try {
23747
+ const host = new URL(url).hostname;
23748
+ return host.replace(/^www\./, "").split(".")[0] ?? "custom";
23749
+ } catch {
23750
+ return "custom";
23751
+ }
23752
+ }
23753
+ var init_connect2 = __esm({
23754
+ "src/commands/connect.ts"() {
23755
+ "use strict";
23756
+ init_argparse();
23757
+ init_prompts();
23758
+ init_providers();
23759
+ init_session_state();
23760
+ init_connect();
23761
+ init_theme();
23762
+ }
23763
+ });
23764
+
23765
+ // src/commands/provider.ts
23766
+ var provider_exports = {};
23767
+ __export(provider_exports, {
23768
+ handler: () => handler41
23769
+ });
23770
+ import chalk58 from "chalk";
23771
+ async function handler41(args, ctx) {
22571
23772
  const { positional, flags } = parseArgs(args, ["default"]);
22572
23773
  const sub = positional[0]?.toLowerCase();
22573
23774
  if (!sub || sub === "list") {
@@ -22579,7 +23780,7 @@ async function handler40(args, ctx) {
22579
23780
  if (!ctx.oneShot) saveSessionState(ctx);
22580
23781
  console.log();
22581
23782
  console.log(" " + paint("success", "\u2713") + " Session engine reset \u2014 using config defaults.");
22582
- console.log(" " + chalk57.dim(`Default: ${loadLlmConfig().primary}`));
23783
+ console.log(" " + chalk58.dim(`Default: ${loadLlmConfig().primary}`));
22583
23784
  console.log();
22584
23785
  return;
22585
23786
  }
@@ -22592,7 +23793,7 @@ async function handler40(args, ctx) {
22592
23793
  setConfigValue("llm-auto-failover", session.autoFailover ? "on" : "off");
22593
23794
  }
22594
23795
  console.log();
22595
- console.log(" " + paint("success", "\u2713") + ` Saved ${chalk57.bold(active)} as default engine.`);
23796
+ console.log(" " + paint("success", "\u2713") + ` Saved ${chalk58.bold(active)} as default engine.`);
22596
23797
  console.log();
22597
23798
  return;
22598
23799
  }
@@ -22611,36 +23812,40 @@ async function handler40(args, ctx) {
22611
23812
  }
22612
23813
  console.log();
22613
23814
  console.log(
22614
- " " + paint("success", "\u2713") + ` Auto-failover ${session.autoFailover ? chalk57.bold("on") : chalk57.bold("off")} for this session.`
23815
+ " " + paint("success", "\u2713") + ` Auto-failover ${session.autoFailover ? chalk58.bold("on") : chalk58.bold("off")} for this session.`
22615
23816
  );
22616
- if (persist) console.log(" " + chalk57.dim("Also saved as config default."));
23817
+ if (persist) console.log(" " + chalk58.dim("Also saved as config default."));
22617
23818
  console.log();
22618
23819
  return;
22619
23820
  }
22620
- if (!PROVIDERS.includes(sub)) {
23821
+ const spec = getProviderSpec(sub);
23822
+ if (!spec || RESERVED.has(sub)) {
22621
23823
  console.log();
22622
- console.log(" " + chalk57.red(`Unknown engine: ${sub}`));
22623
- console.log(" " + chalk57.dim("Usage: /provider [anthropic|openai|list|reset|save|failover on|off]"));
23824
+ console.log(" " + chalk58.red(`Unknown engine: ${sub}`));
23825
+ console.log(" " + chalk58.dim("Usage: /provider [<id>|list|reset|save|failover on|off]"));
23826
+ console.log(" " + chalk58.dim("Connected: ") + (availableEngineLabels().join(", ") || chalk58.dim("none")));
23827
+ console.log(" " + chalk58.dim("Add one with ") + paint("accent", "/connect"));
22624
23828
  console.log();
22625
23829
  return;
22626
23830
  }
22627
- const provider = sub;
23831
+ const provider = spec.id;
22628
23832
  if (!hasProviderKey(provider)) {
22629
- const keyHint = provider === "anthropic" ? "api-key" : "openai-api-key";
22630
23833
  console.log();
22631
- console.log(" " + chalk57.red(`No ${provider} key configured.`));
22632
- console.log(" " + chalk57.dim(`Run `) + paint("accent", `/config set ${keyHint}`) + chalk57.dim(" to add one."));
23834
+ console.log(" " + chalk58.red(`${spec.label} isn't connected.`));
23835
+ console.log(
23836
+ " " + chalk58.dim("Run ") + paint("accent", `/connect ${provider}`) + chalk58.dim(" (or ") + paint("accent", `/config set ${spec.key_config_name}`) + chalk58.dim(").")
23837
+ );
22633
23838
  console.log();
22634
23839
  return;
22635
23840
  }
22636
23841
  ensureLlmSession(ctx).provider = provider;
22637
23842
  if (!ctx.oneShot) saveSessionState(ctx);
22638
23843
  console.log();
22639
- console.log(" " + paint("success", "\u2713") + ` Active engine: ${chalk57.bold(provider)}`);
22640
- console.log(" " + chalk57.dim(`Stack: ${formatActiveStack(ctx)}`));
23844
+ console.log(" " + paint("success", "\u2713") + ` Active engine: ${chalk58.bold(provider)}`);
23845
+ console.log(" " + chalk58.dim(`Stack: ${formatActiveStack(ctx)}`));
22641
23846
  const others = availableEngineLabels().filter((p) => p !== provider);
22642
23847
  if (others.length > 0) {
22643
- console.log(" " + chalk57.dim(`Also available: ${others.join(", ")}`));
23848
+ console.log(" " + chalk58.dim(`Also available: ${others.join(", ")}`));
22644
23849
  }
22645
23850
  console.log();
22646
23851
  }
@@ -22651,49 +23856,54 @@ function printStatus(ctx) {
22651
23856
  const autoFailover = resolveAutoFailoverEnabled(ctx);
22652
23857
  const engines = countAvailableEngines();
22653
23858
  console.log();
22654
- console.log(chalk57.bold(" LLM engines"));
22655
- console.log(` Available: ${engines} engine${engines === 1 ? "" : "s"}`);
22656
- for (const p of PROVIDERS) {
22657
- const key = hasProviderKey(p) ? paint("success", "\u2713") : chalk57.dim("\xB7");
22658
- const marker2 = p === active ? paint("accent", " \u25BA active") : "";
22659
- console.log(` ${key} ${p}${marker2}`);
23859
+ console.log(chalk58.bold(" LLM engines"));
23860
+ console.log(` Connected: ${engines} engine${engines === 1 ? "" : "s"}`);
23861
+ const configured = listProviderSpecs().filter((s) => hasProviderKey(s.id));
23862
+ for (const s of configured) {
23863
+ const marker2 = s.id === active ? paint("accent", " \u25BA active") : "";
23864
+ console.log(` ${paint("success", "\u2713")} ${s.id}${s.custom ? chalk58.dim(" (custom)") : ""}${marker2}`);
23865
+ }
23866
+ if (configured.length === 0) {
23867
+ console.log(" " + chalk58.dim("none \u2014 run /connect and paste any provider key"));
22660
23868
  }
22661
23869
  console.log();
22662
- console.log(chalk57.bold(" Active stack"));
23870
+ console.log(chalk58.bold(" Active stack"));
22663
23871
  console.log(` ${formatActiveStack(ctx)}`);
22664
23872
  if (sessionOverride) {
22665
- console.log(chalk57.dim(" (session override \u2014 /provider reset to use default)"));
23873
+ console.log(chalk58.dim(" (session override \u2014 /provider reset to use default)"));
22666
23874
  } else {
22667
- console.log(chalk57.dim(` (config default: ${cfg.primary})`));
23875
+ console.log(chalk58.dim(` (config default: ${cfg.primary})`));
22668
23876
  }
22669
23877
  console.log();
22670
- console.log(` Auto-failover: ${autoFailover ? paint("success", "on") : chalk57.dim("off")}`);
22671
- console.log(chalk57.dim(" /provider anthropic|openai \u2014 switch engine"));
22672
- console.log(chalk57.dim(" /provider failover on|off \u2014 rate-limit safety net"));
22673
- console.log(chalk57.dim(" /provider save \u2014 persist active engine to config"));
23878
+ console.log(` Auto-failover: ${autoFailover ? paint("success", "on") : chalk58.dim("off")}`);
23879
+ console.log(chalk58.dim(` /provider <id> \u2014 switch engine (${configured.map((s) => s.id).join(", ") || "none connected"})`));
23880
+ console.log(chalk58.dim(" /provider failover on|off \u2014 rate-limit safety net"));
23881
+ console.log(chalk58.dim(" /provider save \u2014 persist active engine to config"));
23882
+ console.log(chalk58.dim(" /connect \u2014 add another provider (any API key)"));
22674
23883
  console.log();
22675
23884
  }
22676
- var PROVIDERS;
23885
+ var RESERVED;
22677
23886
  var init_provider = __esm({
22678
23887
  "src/commands/provider.ts"() {
22679
23888
  "use strict";
22680
23889
  init_argparse();
22681
23890
  init_session_state();
23891
+ init_providers();
22682
23892
  init_llm_config();
22683
23893
  init_store();
22684
23894
  init_context2();
22685
23895
  init_theme();
22686
- PROVIDERS = ["anthropic", "openai"];
23896
+ RESERVED = /* @__PURE__ */ new Set(["list", "reset", "save", "failover"]);
22687
23897
  }
22688
23898
  });
22689
23899
 
22690
23900
  // src/commands/tier.ts
22691
23901
  var tier_exports = {};
22692
23902
  __export(tier_exports, {
22693
- handler: () => handler41
23903
+ handler: () => handler42
22694
23904
  });
22695
- import chalk58 from "chalk";
22696
- async function handler41(args, ctx) {
23905
+ import chalk59 from "chalk";
23906
+ async function handler42(args, ctx) {
22697
23907
  const { positional, flags } = parseArgs(args, ["default"]);
22698
23908
  const sub = positional[0]?.toLowerCase();
22699
23909
  if (!sub || sub === "list") {
@@ -22702,8 +23912,8 @@ async function handler41(args, ctx) {
22702
23912
  }
22703
23913
  if (!TIERS.includes(sub)) {
22704
23914
  console.log();
22705
- console.log(" " + chalk58.red(`Unknown tier: ${sub}`));
22706
- console.log(" " + chalk58.dim("Usage: /tier [high|medium|low|list] [--default]"));
23915
+ console.log(" " + chalk59.red(`Unknown tier: ${sub}`));
23916
+ console.log(" " + chalk59.dim("Usage: /tier [high|medium|low|list] [--default]"));
22707
23917
  console.log();
22708
23918
  return;
22709
23919
  }
@@ -22717,40 +23927,48 @@ async function handler41(args, ctx) {
22717
23927
  }
22718
23928
  console.log();
22719
23929
  console.log(
22720
- " " + paint("success", "\u2713") + ` Inference tier set to ${chalk58.bold(tier.toUpperCase())}` + (persist ? chalk58.dim(" (saved as default)") : chalk58.dim(" (this session)"))
23930
+ " " + paint("success", "\u2713") + ` Inference tier set to ${chalk59.bold(tier.toUpperCase())}` + (persist ? chalk59.dim(" (saved as default)") : chalk59.dim(" (this session)"))
22721
23931
  );
22722
- console.log(" " + chalk58.dim(`Stack: ${formatActiveStack(ctx)}`));
23932
+ console.log(" " + chalk59.dim(`Stack: ${formatActiveStack(ctx)}`));
22723
23933
  console.log();
22724
23934
  }
22725
23935
  function printCatalog(ctx) {
22726
23936
  const cfg = loadLlmConfig();
22727
23937
  const active = resolveModelForActive(ctx, "agentic_investigation");
22728
23938
  const sessionTier = ctx.llm?.tier;
23939
+ const providers = getAvailableProviders();
22729
23940
  console.log();
22730
- console.log(chalk58.bold(" Inference settings"));
23941
+ console.log(chalk59.bold(" Inference settings"));
22731
23942
  console.log(` Active: ${paint("accent", formatActiveStack(ctx))}`);
22732
23943
  if (sessionTier) {
22733
- console.log(chalk58.dim(" (session tier override)"));
23944
+ console.log(chalk59.dim(" (session tier override)"));
22734
23945
  } else {
22735
- console.log(chalk58.dim(` Config default tier: ${cfg.tier.toUpperCase()}`));
23946
+ console.log(chalk59.dim(` Config default tier: ${cfg.tier.toUpperCase()}`));
22736
23947
  }
22737
23948
  console.log();
23949
+ if (providers.length === 0) {
23950
+ console.log(" " + chalk59.dim("No engines connected \u2014 run /connect and paste any provider key."));
23951
+ console.log();
23952
+ }
22738
23953
  for (const tier of TIERS) {
22739
- console.log(chalk58.bold(` ${tier.toUpperCase()}`));
22740
- for (const provider of ["anthropic", "openai"]) {
22741
- const models = listCatalogEntries(provider).filter((m) => m.tier === tier);
22742
- for (const m of models) {
22743
- const isActive = provider === active.provider && tier === active.tier && m.id === active.modelId;
22744
- const marker2 = isActive ? paint("accent", "\u25BA ") : " ";
22745
- const status = m.status === "active" ? "" : chalk58.yellow(` [${m.status}]`);
22746
- console.log(`${marker2}${provider}/${m.id}${status} \u2014 ${m.display_name}`);
23954
+ console.log(chalk59.bold(` ${tier.toUpperCase()}`));
23955
+ for (const provider of providers) {
23956
+ const modelId = resolveModelSafe(provider, tier);
23957
+ if (!modelId) {
23958
+ console.log(` ${provider}/${chalk59.dim("no models \u2014 /model refresh")}`);
23959
+ continue;
22747
23960
  }
23961
+ const isActive = provider === active.provider && tier === active.tier && modelId === active.modelId;
23962
+ const marker2 = isActive ? paint("accent", "\u25BA ") : " ";
23963
+ const discovered = !!getProviderModels(provider);
23964
+ const source = discovered ? "" : chalk59.dim(" [bundled fallback]");
23965
+ console.log(`${marker2}${provider}/${modelId}${source}`);
22748
23966
  }
22749
23967
  console.log();
22750
23968
  }
22751
- console.log(chalk58.dim(" /tier high|medium|low \u2014 set tier for this session"));
22752
- console.log(chalk58.dim(" /tier high --default \u2014 also save as config default"));
22753
- console.log(chalk58.dim(" /provider anthropic|openai \u2014 switch engine"));
23969
+ console.log(chalk59.dim(" /tier high|medium|low \u2014 set tier for this session"));
23970
+ console.log(chalk59.dim(" /tier high --default \u2014 also save as config default"));
23971
+ console.log(chalk59.dim(" /provider <id> \u2014 switch engine \xB7 /model list \u2014 browse models"));
22754
23972
  console.log();
22755
23973
  }
22756
23974
  var TIERS;
@@ -22759,6 +23977,7 @@ var init_tier = __esm({
22759
23977
  "use strict";
22760
23978
  init_argparse();
22761
23979
  init_catalog();
23980
+ init_models_cache();
22762
23981
  init_session_state();
22763
23982
  init_llm_config();
22764
23983
  init_store();
@@ -22771,12 +23990,21 @@ var init_tier = __esm({
22771
23990
  // src/commands/model.ts
22772
23991
  var model_exports = {};
22773
23992
  __export(model_exports, {
22774
- handler: () => handler42
23993
+ handler: () => handler43
22775
23994
  });
22776
- import chalk59 from "chalk";
22777
- async function handler42(args, ctx) {
22778
- const { positional, flags } = parseArgs(args, ["default"]);
23995
+ import chalk60 from "chalk";
23996
+ import ora18 from "ora";
23997
+ async function handler43(args, ctx) {
23998
+ const { positional, flags } = parseArgs(args, ["default", "all"]);
22779
23999
  const sub = positional[0]?.toLowerCase();
24000
+ if (sub === "list") {
24001
+ printModelList(ctx, getBool(flags, "all"));
24002
+ return;
24003
+ }
24004
+ if (sub === "refresh") {
24005
+ await refreshModels(ctx);
24006
+ return;
24007
+ }
22780
24008
  if (sub === "clear") {
22781
24009
  const persist = getBool(flags, "default");
22782
24010
  if (ctx.llm) ctx.llm.modelOverride = void 0;
@@ -22784,7 +24012,7 @@ async function handler42(args, ctx) {
22784
24012
  if (!ctx.oneShot) saveSessionState(ctx);
22785
24013
  console.log();
22786
24014
  console.log(" " + paint("success", "\u2713") + " Model override cleared \u2014 using tier defaults.");
22787
- console.log(" " + chalk59.dim(`Stack: ${formatActiveStack(ctx)}`));
24015
+ console.log(" " + chalk60.dim(`Stack: ${formatActiveStack(ctx)}`));
22788
24016
  console.log();
22789
24017
  return;
22790
24018
  }
@@ -22792,22 +24020,28 @@ async function handler42(args, ctx) {
22792
24020
  const modelId = positional[1];
22793
24021
  if (!modelId) {
22794
24022
  console.log();
22795
- console.log(" " + chalk59.red("Usage: /model set <model-id> [--default]"));
24023
+ console.log(" " + chalk60.red("Usage: /model set <model-id> [--default]"));
22796
24024
  console.log();
22797
24025
  return;
22798
24026
  }
22799
24027
  const active = resolveActiveProvider(ctx);
22800
24028
  const providerErr = validateModelForProvider(modelId, active);
22801
- const entry = getCatalogEntry(modelId);
22802
24029
  if (providerErr) {
22803
24030
  console.log();
22804
- console.log(" " + chalk59.red(providerErr));
24031
+ console.log(" " + chalk60.red(providerErr));
22805
24032
  console.log();
22806
24033
  return;
22807
24034
  }
22808
- if (!entry) {
24035
+ const cache2 = getProviderModels(active);
24036
+ const known = cache2?.models.some((m) => m.id === modelId);
24037
+ if (cache2 && !known) {
24038
+ console.log();
24039
+ console.log(
24040
+ " " + chalk60.yellow("\u26A0") + ` ${modelId} isn't in ${active}'s discovered list (` + paint("accent", "/model list") + `) \u2014 saving anyway.`
24041
+ );
24042
+ } else if (!cache2) {
22809
24043
  console.log();
22810
- console.log(" " + chalk59.yellow("\u26A0") + ` Unknown model ${modelId} \u2014 saving for active engine anyway.`);
24044
+ console.log(" " + chalk60.yellow("\u26A0") + ` No discovered models for ${active} yet (` + paint("accent", "/model refresh") + `) \u2014 saving anyway.`);
22811
24045
  }
22812
24046
  const persist = getBool(flags, "default");
22813
24047
  if (persist) {
@@ -22818,48 +24052,98 @@ async function handler42(args, ctx) {
22818
24052
  }
22819
24053
  console.log();
22820
24054
  console.log(
22821
- " " + paint("success", "\u2713") + ` Model: ${chalk59.bold(modelId)}` + (persist ? chalk59.dim(" (saved as default)") : chalk59.dim(" (this session)"))
24055
+ " " + paint("success", "\u2713") + ` Model: ${chalk60.bold(modelId)}` + (persist ? chalk60.dim(" (saved as default)") : chalk60.dim(" (this session)"))
22822
24056
  );
22823
- console.log(" " + chalk59.dim(`Stack: ${formatActiveStack(ctx)}`));
24057
+ console.log(" " + chalk60.dim(`Stack: ${formatActiveStack(ctx)}`));
22824
24058
  console.log();
22825
24059
  return;
22826
24060
  }
22827
24061
  const sessionOverride = ctx.llm?.modelOverride;
22828
24062
  const globalOverride = getConfigValue("llm-model-override");
22829
24063
  console.log();
22830
- console.log(chalk59.bold(" Model"));
24064
+ console.log(chalk60.bold(" Model"));
22831
24065
  if (sessionOverride) {
22832
24066
  console.log(` Session override: ${paint("accent", sessionOverride)}`);
22833
24067
  } else if (globalOverride) {
22834
24068
  console.log(` Config default: ${paint("accent", globalOverride)}`);
22835
24069
  } else {
22836
- console.log(" " + chalk59.dim("No override \u2014 tier defaults apply."));
24070
+ console.log(" " + chalk60.dim("No override \u2014 tier defaults apply."));
22837
24071
  }
22838
24072
  console.log(` Active stack: ${formatActiveStack(ctx)}`);
22839
- console.log(chalk59.dim(" /model set <id> \xB7 /model clear \xB7 /tier list \xB7 /provider list"));
24073
+ console.log(chalk60.dim(" /model list \xB7 /model set <id> \xB7 /model refresh \xB7 /model clear"));
22840
24074
  console.log();
22841
24075
  }
24076
+ function tierMarkers(cache2, modelId) {
24077
+ const tiers = Object.entries(cache2.tier_stack).filter(([, id]) => id === modelId).map(([tier]) => tier.toUpperCase());
24078
+ return tiers.length > 0 ? paint("accent", ` \u25C2 ${tiers.join("/")}`) : "";
24079
+ }
24080
+ function printModelList(ctx, showAll) {
24081
+ const active = resolveActiveProvider(ctx);
24082
+ const cache2 = getProviderModels(active);
24083
+ console.log();
24084
+ console.log(chalk60.bold(` Models \u2014 ${active}`));
24085
+ if (!cache2) {
24086
+ console.log(" " + chalk60.dim("Nothing discovered yet."));
24087
+ console.log(" " + chalk60.dim("Run ") + paint("accent", "/model refresh") + chalk60.dim(" (or ") + paint("accent", "/connect") + chalk60.dim(" to add the provider)."));
24088
+ console.log();
24089
+ return;
24090
+ }
24091
+ const fetchedAt = cache2.fetched_at.slice(0, 10);
24092
+ console.log(" " + chalk60.dim(`${cache2.models.length} chat models \xB7 discovered ${fetchedAt} \xB7 /model refresh to update`));
24093
+ console.log();
24094
+ const models = showAll ? cache2.models : cache2.models.slice(0, LIST_LIMIT);
24095
+ const noTools = new Set(cache2.quirks?.no_tools ?? []);
24096
+ for (const m of models) {
24097
+ const name = m.display_name && m.display_name !== m.id ? chalk60.dim(` \u2014 ${m.display_name}`) : "";
24098
+ const quirk = noTools.has(m.id) ? chalk60.yellow(" [no tools]") : "";
24099
+ console.log(` ${m.id}${name}${tierMarkers(cache2, m.id)}${quirk}`);
24100
+ }
24101
+ if (!showAll && cache2.models.length > models.length) {
24102
+ console.log(" " + chalk60.dim(`\u2026 and ${cache2.models.length - models.length} more (/model list --all)`));
24103
+ }
24104
+ console.log();
24105
+ console.log(" " + chalk60.dim("/model set <id> \u2014 pin one for this session (--default to persist)"));
24106
+ console.log();
24107
+ }
24108
+ async function refreshModels(ctx) {
24109
+ const active = resolveActiveProvider(ctx);
24110
+ const spinner = ora18({ text: `Discovering ${active} models\u2026`, discardStdin: false }).start();
24111
+ const entry = await refreshProviderModels(active, { force: true });
24112
+ if (!entry) {
24113
+ spinner.fail(`Couldn't reach ${active} to refresh models.`);
24114
+ console.log(" " + chalk60.dim("Check your connection and key, then retry. Cached models remain in use."));
24115
+ console.log();
24116
+ return;
24117
+ }
24118
+ spinner.succeed(`${active}: ${entry.models.length} chat models discovered.`);
24119
+ console.log(" " + chalk60.dim(`high ${entry.tier_stack.high} \xB7 medium ${entry.tier_stack.medium} \xB7 low ${entry.tier_stack.low}`));
24120
+ console.log(" " + chalk60.dim(`Stack: ${formatActiveStack(ctx)}`));
24121
+ console.log();
24122
+ }
24123
+ var LIST_LIMIT;
22842
24124
  var init_model = __esm({
22843
24125
  "src/commands/model.ts"() {
22844
24126
  "use strict";
22845
24127
  init_argparse();
22846
- init_catalog();
24128
+ init_discovery();
24129
+ init_models_cache();
22847
24130
  init_session_state();
22848
24131
  init_store();
22849
24132
  init_context2();
22850
24133
  init_theme();
24134
+ LIST_LIMIT = 40;
22851
24135
  }
22852
24136
  });
22853
24137
 
22854
24138
  // src/config/update-check.ts
22855
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync14, unlinkSync as unlinkSync5, writeFileSync as writeFileSync17 } from "fs";
22856
- import { join as join26 } from "path";
22857
- function cachePath() {
22858
- return join26(ntrpHome(), "update-check.json");
24139
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync17, unlinkSync as unlinkSync5, writeFileSync as writeFileSync19 } from "fs";
24140
+ import { join as join28 } from "path";
24141
+ function cachePath2() {
24142
+ return join28(ntrpHome(), "update-check.json");
22859
24143
  }
22860
24144
  function invalidateUpdateCheckCache() {
22861
- const path = cachePath();
22862
- if (existsSync19(path)) {
24145
+ const path = cachePath2();
24146
+ if (existsSync21(path)) {
22863
24147
  unlinkSync5(path);
22864
24148
  }
22865
24149
  }
@@ -22871,17 +24155,17 @@ var init_update_check = __esm({
22871
24155
  });
22872
24156
 
22873
24157
  // src/version.ts
22874
- import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
22875
- import { dirname as dirname4, join as join27 } from "path";
24158
+ import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
24159
+ import { dirname as dirname4, join as join29 } from "path";
22876
24160
  import { fileURLToPath } from "url";
22877
24161
  function getInstalledVersion() {
22878
24162
  if (cachedVersion) return cachedVersion;
22879
24163
  const start = dirname4(fileURLToPath(import.meta.url));
22880
24164
  for (const rel of ["../package.json", "../../package.json"]) {
22881
- const path = join27(start, rel);
22882
- if (!existsSync20(path)) continue;
24165
+ const path = join29(start, rel);
24166
+ if (!existsSync22(path)) continue;
22883
24167
  try {
22884
- const pkg = JSON.parse(readFileSync15(path, "utf-8"));
24168
+ const pkg = JSON.parse(readFileSync18(path, "utf-8"));
22885
24169
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
22886
24170
  cachedVersion = pkg.version;
22887
24171
  return cachedVersion;
@@ -22939,10 +24223,10 @@ var init_registry = __esm({
22939
24223
  // src/commands/update.ts
22940
24224
  var update_exports = {};
22941
24225
  __export(update_exports, {
22942
- handler: () => handler43
24226
+ handler: () => handler44
22943
24227
  });
22944
24228
  import { spawnSync } from "child_process";
22945
- import chalk60 from "chalk";
24229
+ import chalk61 from "chalk";
22946
24230
  function tailLines(text, count = 5) {
22947
24231
  return text.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-count).join("\n");
22948
24232
  }
@@ -22958,19 +24242,19 @@ function runGlobalInstall() {
22958
24242
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
22959
24243
  return { ok: result.status === 0, output };
22960
24244
  }
22961
- async function handler43(_args, _ctx) {
24245
+ async function handler44(_args, _ctx) {
22962
24246
  const current = getInstalledVersion();
22963
24247
  const latest = await fetchLatestVersion(1e4);
22964
24248
  if (!latest) {
22965
24249
  console.log();
22966
- console.log(chalk60.yellow(" Could not reach the npm registry."));
22967
- console.log(chalk60.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
24250
+ console.log(chalk61.yellow(" Could not reach the npm registry."));
24251
+ console.log(chalk61.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
22968
24252
  console.log();
22969
24253
  return;
22970
24254
  }
22971
24255
  if (!isNewerVersion(latest, current)) {
22972
24256
  console.log();
22973
- console.log(chalk60.green(` \u2713 You're on the latest version (v${current})`));
24257
+ console.log(chalk61.green(` \u2713 You're on the latest version (v${current})`));
22974
24258
  console.log();
22975
24259
  return;
22976
24260
  }
@@ -22979,24 +24263,24 @@ async function handler43(_args, _ctx) {
22979
24263
  const { ok, output } = runGlobalInstall();
22980
24264
  if (ok) {
22981
24265
  invalidateUpdateCheckCache();
22982
- console.log(chalk60.green(` \u2713 Updated! Restart NTRP to use v${latest}`));
24266
+ console.log(chalk61.green(` \u2713 Updated! Restart NTRP to use v${latest}`));
22983
24267
  console.log();
22984
24268
  return;
22985
24269
  }
22986
24270
  const lower = output.toLowerCase();
22987
24271
  if (lower.includes("eacces") || lower.includes("permission denied") || lower.includes("eperm")) {
22988
- console.log(chalk60.red(` Could not install ${NPM_PACKAGE} (permission denied).`));
22989
- console.log(chalk60.dim(` Try: sudo npm install -g ${NPM_PACKAGE}`));
22990
- console.log(chalk60.dim(` Or fix npm global permissions: ${PERMISSIONS_URL}`));
24272
+ console.log(chalk61.red(` Could not install ${NPM_PACKAGE} (permission denied).`));
24273
+ console.log(chalk61.dim(` Try: sudo npm install -g ${NPM_PACKAGE}`));
24274
+ console.log(chalk61.dim(` Or fix npm global permissions: ${PERMISSIONS_URL}`));
22991
24275
  console.log();
22992
24276
  return;
22993
24277
  }
22994
24278
  const detail = tailLines(output);
22995
- console.log(chalk60.red(` Could not install ${NPM_PACKAGE}.`));
24279
+ console.log(chalk61.red(` Could not install ${NPM_PACKAGE}.`));
22996
24280
  if (detail) {
22997
- console.log(chalk60.dim(` ${detail.split("\n").join("\n ")}`));
24281
+ console.log(chalk61.dim(` ${detail.split("\n").join("\n ")}`));
22998
24282
  }
22999
- console.log(chalk60.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
24283
+ console.log(chalk61.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
23000
24284
  console.log();
23001
24285
  }
23002
24286
  var PERMISSIONS_URL;
@@ -23011,10 +24295,10 @@ var init_update = __esm({
23011
24295
  });
23012
24296
 
23013
24297
  // src/output/progress-report.ts
23014
- import chalk61 from "chalk";
24298
+ import chalk62 from "chalk";
23015
24299
  function printCard(title, rows) {
23016
24300
  const inner = CARD_W - 4;
23017
- const border = chalk61.dim;
24301
+ const border = chalk62.dim;
23018
24302
  console.log();
23019
24303
  console.log(` ${border(`\u256D${"\u2500".repeat(CARD_W - 2)}\u256E`)}`);
23020
24304
  console.log(` ${border("\u2502 ")}${padRight(sectionHeading(title), inner)}${border(" \u2502")}`);
@@ -23030,7 +24314,7 @@ function formatTokens(n) {
23030
24314
  return String(n);
23031
24315
  }
23032
24316
  function sparkline(values) {
23033
- if (values.length === 0) return chalk61.dim("(no activity yet)");
24317
+ if (values.length === 0) return chalk62.dim("(no activity yet)");
23034
24318
  const max = Math.max(...values, 1);
23035
24319
  const blocks = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
23036
24320
  return values.map((v) => {
@@ -23039,7 +24323,7 @@ function sparkline(values) {
23039
24323
  }).join("");
23040
24324
  }
23041
24325
  function formatMemberSince(iso) {
23042
- if (!iso) return chalk61.dim("\u2014");
24326
+ if (!iso) return chalk62.dim("\u2014");
23043
24327
  const d = new Date(iso);
23044
24328
  return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
23045
24329
  }
@@ -23056,49 +24340,49 @@ function renderProgressReport() {
23056
24340
  state.milestones_unlocked.length,
23057
24341
  TIME_MILESTONES.length
23058
24342
  );
23059
- const { usage: usage2 } = summary;
24343
+ const { usage: usage3 } = summary;
23060
24344
  const nextLabel = bank.next_milestone ? `${formatHoursLabel(bank.total_hours)} \u2192 ${formatHoursLabel(bank.next_milestone.hours)}` : `${formatHoursLabel(bank.total_hours)} saved`;
23061
24345
  const bar = inlineBar(bank.progress_pct, 18);
23062
24346
  printCard("Progress", [
23063
- `${chalk61.dim("Hours saved")} ${paint("accent", formatHoursLabel(bank.total_hours))} ${bar}`,
23064
- `${chalk61.dim("Next milestone")} ${bank.next_milestone ? paint("accent", bank.next_milestone.title) : chalk61.dim("top of ladder")}`,
23065
- `${chalk61.dim("Member since")} ${formatMemberSince(usage2.first_active_at)}`,
23066
- `${chalk61.dim("Last active")} ${formatMemberSince(usage2.last_active_at)}`
24347
+ `${chalk62.dim("Hours saved")} ${paint("accent", formatHoursLabel(bank.total_hours))} ${bar}`,
24348
+ `${chalk62.dim("Next milestone")} ${bank.next_milestone ? paint("accent", bank.next_milestone.title) : chalk62.dim("top of ladder")}`,
24349
+ `${chalk62.dim("Member since")} ${formatMemberSince(usage3.first_active_at)}`,
24350
+ `${chalk62.dim("Last active")} ${formatMemberSince(usage3.last_active_at)}`
23067
24351
  ]);
23068
24352
  if (bank.perspective_line) {
23069
- console.log(` ${chalk61.dim.italic(bank.perspective_line)}`);
24353
+ console.log(` ${chalk62.dim.italic(bank.perspective_line)}`);
23070
24354
  }
23071
24355
  printCard("Activity", [
23072
- `${chalk61.dim("Sessions")} ${chalk61.bold(String(summary.total_sessions_on_disk))} total \xB7 ${summary.sessions_with_work} with work \xB7 ${usage2.sessions_closed} closed`,
23073
- `${chalk61.dim("Diagnoses")} ${chalk61.bold(String(usage2.diagnoses))}`,
23074
- `${chalk61.dim("Metrics runs")} ${chalk61.bold(String(usage2.metrics_runs))}`,
23075
- `${chalk61.dim("Deliverables")} ${chalk61.bold(String(usage2.deliverables))}`,
23076
- `${chalk61.dim("AI exchanges")} ${chalk61.bold(String(usage2.nl_exchanges))}`
24356
+ `${chalk62.dim("Sessions")} ${chalk62.bold(String(summary.total_sessions_on_disk))} total \xB7 ${summary.sessions_with_work} with work \xB7 ${usage3.sessions_closed} closed`,
24357
+ `${chalk62.dim("Diagnoses")} ${chalk62.bold(String(usage3.diagnoses))}`,
24358
+ `${chalk62.dim("Metrics runs")} ${chalk62.bold(String(usage3.metrics_runs))}`,
24359
+ `${chalk62.dim("Deliverables")} ${chalk62.bold(String(usage3.deliverables))}`,
24360
+ `${chalk62.dim("AI exchanges")} ${chalk62.bold(String(usage3.nl_exchanges))}`
23077
24361
  ]);
23078
- const totalTokens = usage2.input_tokens + usage2.output_tokens;
24362
+ const totalTokens = usage3.input_tokens + usage3.output_tokens;
23079
24363
  printCard("AI usage", [
23080
- `${chalk61.dim("LLM calls")} ${chalk61.bold(String(usage2.llm_calls))}`,
23081
- `${chalk61.dim("Tokens")} ${chalk61.bold(formatTokens(totalTokens))} in+out (${formatTokens(usage2.input_tokens)} in \xB7 ${formatTokens(usage2.output_tokens)} out)`
24364
+ `${chalk62.dim("LLM calls")} ${chalk62.bold(String(usage3.llm_calls))}`,
24365
+ `${chalk62.dim("Tokens")} ${chalk62.bold(formatTokens(totalTokens))} in+out (${formatTokens(usage3.input_tokens)} in \xB7 ${formatTokens(usage3.output_tokens)} out)`
23082
24366
  ]);
23083
- const weeks = [...usage2.weekly].sort((a, b) => a.week.localeCompare(b.week)).slice(-8);
24367
+ const weeks = [...usage3.weekly].sort((a, b) => a.week.localeCompare(b.week)).slice(-8);
23084
24368
  const weekHours = weeks.map((w) => w.minutes_saved / 60);
23085
24369
  const weekLabels = weeks.map((w) => w.week.replace(/^\d{4}-/, ""));
23086
24370
  console.log();
23087
24371
  console.log(` ${sectionHeading("Weekly hours saved")}`);
23088
24372
  console.log(` ${sparkline(weekHours)}`);
23089
24373
  if (weeks.length > 0) {
23090
- console.log(` ${chalk61.dim(weekLabels.join(" "))}`);
24374
+ console.log(` ${chalk62.dim(weekLabels.join(" "))}`);
23091
24375
  }
23092
24376
  console.log();
23093
24377
  console.log(` ${sectionHeading("Milestone ladder")}`);
23094
24378
  for (const m of TIME_MILESTONES) {
23095
24379
  const unlocked = state.milestones_unlocked.includes(m.id);
23096
24380
  const pct = Math.min(100, bank.total_hours / m.hours * 100);
23097
- const mark = unlocked ? badge("DONE", "success") : bank.total_hours >= m.hours * 0.85 ? badge("NEAR", "warning") : chalk61.dim("\u25CB");
24381
+ const mark = unlocked ? badge("DONE", "success") : bank.total_hours >= m.hours * 0.85 ? badge("NEAR", "warning") : chalk62.dim("\u25CB");
23098
24382
  const barW = 12;
23099
- const mBar = unlocked ? chalk61.hex("#22c55e")("\u2588".repeat(barW)) : scoreBar(pct, bank.total_hours >= m.hours ? "green" : pct >= 50 ? "yellow" : "red", barW);
24383
+ const mBar = unlocked ? chalk62.hex("#22c55e")("\u2588".repeat(barW)) : scoreBar(pct, bank.total_hours >= m.hours ? "green" : pct >= 50 ? "yellow" : "red", barW);
23100
24384
  const label = `${m.title}`.padEnd(16);
23101
- console.log(` ${mark} ${chalk61.dim(label)} ${mBar} ${chalk61.dim(`${m.hours}h`)}`);
24385
+ console.log(` ${mark} ${chalk62.dim(label)} ${mBar} ${chalk62.dim(`${m.hours}h`)}`);
23102
24386
  }
23103
24387
  console.log();
23104
24388
  }
@@ -23120,17 +24404,17 @@ var init_progress_report = __esm({
23120
24404
  // src/commands/progress.ts
23121
24405
  var progress_exports = {};
23122
24406
  __export(progress_exports, {
23123
- handler: () => handler44
24407
+ handler: () => handler45
23124
24408
  });
23125
- import chalk62 from "chalk";
24409
+ import chalk63 from "chalk";
23126
24410
  function printProgressResetPreamble() {
23127
24411
  console.log();
23128
- console.log(" " + chalk62.yellow.bold("This will permanently remove:"));
23129
- console.log(" " + chalk62.dim(" \u2022 Hours saved and milestone unlocks"));
23130
- console.log(" " + chalk62.dim(" \u2022 Usage counters and weekly activity rollups"));
23131
- console.log(" " + chalk62.dim(" \u2022 Credit history used for dedup"));
24412
+ console.log(" " + chalk63.yellow.bold("This will permanently remove:"));
24413
+ console.log(" " + chalk63.dim(" \u2022 Hours saved and milestone unlocks"));
24414
+ console.log(" " + chalk63.dim(" \u2022 Usage counters and weekly activity rollups"));
24415
+ console.log(" " + chalk63.dim(" \u2022 Credit history used for dedup"));
23132
24416
  console.log();
23133
- console.log(" " + chalk62.dim("Preserved: install identity (install.json)"));
24417
+ console.log(" " + chalk63.dim("Preserved: install identity (install.json)"));
23134
24418
  console.log();
23135
24419
  }
23136
24420
  function showProgress() {
@@ -23148,7 +24432,7 @@ async function handleReset(ctx, confirmedFlag) {
23148
24432
  const bank = getTimeBankSummary();
23149
24433
  if (bank.total_minutes <= 0) {
23150
24434
  console.log();
23151
- console.log(" " + chalk62.dim("No progress to reset."));
24435
+ console.log(" " + chalk63.dim("No progress to reset."));
23152
24436
  console.log();
23153
24437
  return "No progress to reset";
23154
24438
  }
@@ -23165,11 +24449,11 @@ async function handleReset(ctx, confirmedFlag) {
23165
24449
  }
23166
24450
  resetProgress();
23167
24451
  console.log();
23168
- console.log(" " + paint("accent", "\u2713 Progress reset") + chalk62.dim(" \u2014 hours and milestones cleared."));
24452
+ console.log(" " + paint("accent", "\u2713 Progress reset") + chalk63.dim(" \u2014 hours and milestones cleared."));
23169
24453
  console.log();
23170
24454
  return "Progress reset";
23171
24455
  }
23172
- async function handler44(args, ctx) {
24456
+ async function handler45(args, ctx) {
23173
24457
  const { positional, flags } = parseArgs(args, ["confirm"]);
23174
24458
  const sub = positional[0]?.toLowerCase();
23175
24459
  if (sub === "reset") {
@@ -23177,7 +24461,7 @@ async function handler44(args, ctx) {
23177
24461
  }
23178
24462
  if (sub && sub !== "reset") {
23179
24463
  console.log();
23180
- console.log(" " + chalk62.dim("Unknown subcommand. Try ") + paint("accent", "/progress") + chalk62.dim(" or ") + paint("accent", "/progress reset") + chalk62.dim("."));
24464
+ console.log(" " + chalk63.dim("Unknown subcommand. Try ") + paint("accent", "/progress") + chalk63.dim(" or ") + paint("accent", "/progress reset") + chalk63.dim("."));
23181
24465
  console.log();
23182
24466
  return;
23183
24467
  }
@@ -23204,8 +24488,8 @@ init_time_milestones();
23204
24488
  init_time_perspectives();
23205
24489
  init_time_bank();
23206
24490
  init_perspective_rotation();
23207
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync18 } from "fs";
23208
- import { join as join28 } from "path";
24491
+ import { existsSync as existsSync23, mkdirSync as mkdirSync13, mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync20 } from "fs";
24492
+ import { join as join30 } from "path";
23209
24493
  import { tmpdir } from "os";
23210
24494
 
23211
24495
  // src/workflows/registry.ts
@@ -23259,10 +24543,10 @@ async function resolveHandler(name) {
23259
24543
  try {
23260
24544
  const mod = await importHandler(runtimePath);
23261
24545
  if (!mod) return null;
23262
- const handler45 = mod.handler;
23263
- if (typeof handler45 !== "function") return null;
23264
- entry.handler = handler45;
23265
- return handler45;
24546
+ const handler46 = mod.handler;
24547
+ if (typeof handler46 !== "function") return null;
24548
+ entry.handler = handler46;
24549
+ return handler46;
23266
24550
  } catch (err) {
23267
24551
  console.error(`Failed to load handler for /${name}:`, err);
23268
24552
  return null;
@@ -23348,6 +24632,8 @@ async function importHandler(runtimePath) {
23348
24632
  return Promise.resolve().then(() => (init_switch(), switch_exports));
23349
24633
  case "../commands/backmeup.js":
23350
24634
  return Promise.resolve().then(() => (init_backmeup(), backmeup_exports));
24635
+ case "../commands/connect.js":
24636
+ return Promise.resolve().then(() => (init_connect2(), connect_exports2));
23351
24637
  case "../commands/provider.js":
23352
24638
  return Promise.resolve().then(() => (init_provider(), provider_exports));
23353
24639
  case "../commands/tier.js":
@@ -23470,7 +24756,9 @@ handler: ../commands/setup.ts
23470
24756
 
23471
24757
  Validate local readiness or configure NTRP non-interactively for automation.
23472
24758
  \`setup check --json\` reports license, profile, API key, database, and writable
23473
- directory state. \`setup agent\` accepts a profile JSON file or direct flags.`
24759
+ directory state. \`setup agent\` accepts a profile JSON file or direct flags \u2014
24760
+ \`--llm-key <key>\` auto-detects the provider from any pasted key
24761
+ (\`--llm-provider <id>\` to force one).`
23474
24762
  },
23475
24763
  {
23476
24764
  name: "update",
@@ -23906,6 +25194,25 @@ handler: ../commands/profile.ts
23906
25194
 
23907
25195
  Choose a sales motion preset (PLG, SMB Velocity, Mid-Market, Enterprise). Each
23908
25196
  preset adjusts the vital-sign thresholds to match your deal cycle.`
25197
+ },
25198
+ {
25199
+ name: "connect",
25200
+ raw: `---
25201
+ name: connect
25202
+ description: Connect an AI provider (paste any key)
25203
+ section: Settings
25204
+ args: [provider] [--key <key>] [--base-url <url> --id <name>]
25205
+ handler: ../commands/connect.ts
25206
+ ---
25207
+
25208
+ Paste any provider's API key \u2014 NTRP identifies the provider from the key
25209
+ format (probing ambiguous ones), validates it, discovers which models the key
25210
+ can use, and builds the HIGH/MEDIUM/LOW tier stack automatically.
25211
+
25212
+ Works with Anthropic, OpenAI, Google Gemini, Groq, Mistral, DeepSeek, xAI,
25213
+ OpenRouter, Together, and Fireworks out of the box. \`/connect ollama\` wires a
25214
+ local Ollama; \`/connect --base-url <url> --id <name>\` registers any other
25215
+ OpenAI-compatible endpoint.`
23909
25216
  },
23910
25217
  {
23911
25218
  name: "config",
@@ -23918,10 +25225,12 @@ handler: ../commands/config.ts
23918
25225
  ---
23919
25226
 
23920
25227
  Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
23921
- \`api-key\` (Anthropic), \`openai-api-key\`, \`llm-primary\` (default engine),
23922
- \`llm-tier\`, \`llm-auto-failover\`, \`default-format\`, \`export-dir\`.
25228
+ \`api-key\` (Anthropic), \`openai-api-key\` (and \`groq-api-key\`, \`google-api-key\`, ...),
25229
+ \`llm-primary\` (default engine), \`llm-tier\`, \`llm-auto-failover\`,
25230
+ \`default-format\`, \`export-dir\`.
23923
25231
 
23924
- \`/config set api-key\` and \`/config set openai-api-key\` open hidden prompts.`
25232
+ Setting a provider key opens a hidden prompt and auto-discovers that
25233
+ provider's models. Prefer \`/connect\` \u2014 it detects the provider for you.`
23925
25234
  },
23926
25235
  {
23927
25236
  name: "provider",
@@ -23929,13 +25238,14 @@ Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
23929
25238
  name: provider
23930
25239
  description: Switch active LLM engine
23931
25240
  section: Settings
23932
- args: [anthropic|openai|list|reset|save|failover on|off]
25241
+ args: [<id>|list|reset|save|failover on|off]
23933
25242
  handler: ../commands/provider.ts
23934
25243
  ---
23935
25244
 
23936
- Choose which engine answers this session \u2014 Anthropic or OpenAI when both keys
23937
- are configured. Session-scoped by default; \`/provider save\` writes the default
23938
- to config. \`/provider failover on\` enables rate-limit auto-failover.`
25245
+ Choose which connected engine answers this session \u2014 any provider added via
25246
+ \`/connect\` (anthropic, openai, groq, google, ollama, custom endpoints, ...).
25247
+ Session-scoped by default; \`/provider save\` writes the default to config.
25248
+ \`/provider failover on\` enables rate-limit auto-failover.`
23939
25249
  },
23940
25250
  {
23941
25251
  name: "tier",
@@ -23957,12 +25267,14 @@ active stack. Add \`--default\` to persist to config.`
23957
25267
  name: model
23958
25268
  description: Override the active LLM model
23959
25269
  section: Settings
23960
- args: [set <id>|clear] [--default]
25270
+ args: [list|set <id>|refresh|clear] [--default]
23961
25271
  handler: ../commands/model.ts
23962
25272
  ---
23963
25273
 
23964
- Pin a catalog model on the **active engine** (e.g. claude-opus-4-6 on anthropic).
23965
- Cross-provider IDs are rejected \u2014 switch with \`/provider\` first.`
25274
+ \`/model list\` shows the models discovered for the active engine with their
25275
+ tier assignments. \`/model refresh\` re-discovers the live list. \`/model set <id>\`
25276
+ pins a model on the **active engine**; cross-provider IDs are rejected \u2014
25277
+ switch with \`/provider\` first.`
23966
25278
  },
23967
25279
  {
23968
25280
  name: "activate",
@@ -24032,7 +25344,7 @@ function mockCtx(sessionId = "2026-06-21-test") {
24032
25344
  };
24033
25345
  }
24034
25346
  function withTempHome(run2) {
24035
- const dir = mkdtempSync(join28(tmpdir(), "ntrp-time-bank-"));
25347
+ const dir = mkdtempSync(join30(tmpdir(), "ntrp-time-bank-"));
24036
25348
  const prev = process.env.NTRP_HOME;
24037
25349
  process.env.NTRP_HOME = dir;
24038
25350
  try {
@@ -24051,7 +25363,7 @@ function withTempHome(run2) {
24051
25363
  }
24052
25364
  }
24053
25365
  async function withTempHomeAsync(run2) {
24054
- const dir = mkdtempSync(join28(tmpdir(), "ntrp-time-bank-"));
25366
+ const dir = mkdtempSync(join30(tmpdir(), "ntrp-time-bank-"));
24055
25367
  const prev = process.env.NTRP_HOME;
24056
25368
  process.env.NTRP_HOME = dir;
24057
25369
  try {
@@ -24172,7 +25484,7 @@ function testUsageBackfillFromCredits() {
24172
25484
  withTempHome(() => {
24173
25485
  const at = "2026-06-01T12:00:00.000Z";
24174
25486
  const installId = loadProgress().install_id;
24175
- writeFileSync18(join28(ntrpHome(), "progress.json"), JSON.stringify({
25487
+ writeFileSync20(join30(ntrpHome(), "progress.json"), JSON.stringify({
24176
25488
  schema_version: 2,
24177
25489
  install_id: installId,
24178
25490
  total_minutes_saved: 30,
@@ -24187,14 +25499,14 @@ function testUsageBackfillFromCredits() {
24187
25499
  function testInstallCreatedOnFirstLoad() {
24188
25500
  withTempHome(() => {
24189
25501
  loadProgress();
24190
- assert(existsSync21(join28(ntrpHome(), "install.json")), "install.json created");
24191
- assert(existsSync21(join28(ntrpHome(), "progress.json")), "progress.json created");
25502
+ assert(existsSync23(join30(ntrpHome(), "install.json")), "install.json created");
25503
+ assert(existsSync23(join30(ntrpHome(), "progress.json")), "progress.json created");
24192
25504
  });
24193
25505
  }
24194
25506
  function testLegacyStateMigration() {
24195
25507
  withTempHome(() => {
24196
25508
  mkdirSync13(ntrpHome(), { recursive: true });
24197
- writeFileSync18(join28(ntrpHome(), "state.json"), JSON.stringify({
25509
+ writeFileSync20(join30(ntrpHome(), "state.json"), JSON.stringify({
24198
25510
  schema_version: 1,
24199
25511
  total_minutes_saved: 45,
24200
25512
  credits: [{ action: "onboard", minutes: 45, at: "2026-06-01T12:00:00.000Z" }],
@@ -24202,8 +25514,8 @@ function testLegacyStateMigration() {
24202
25514
  }));
24203
25515
  const state = loadProgress();
24204
25516
  assert(state.total_minutes_saved === 45, "legacy migration preserves hours");
24205
- assert(existsSync21(join28(ntrpHome(), "progress.json")), "progress.json created from legacy");
24206
- assert(!existsSync21(join28(ntrpHome(), "state.json")), "legacy state.json moved aside");
25517
+ assert(existsSync23(join30(ntrpHome(), "progress.json")), "progress.json created from legacy");
25518
+ assert(!existsSync23(join30(ntrpHome(), "state.json")), "legacy state.json moved aside");
24207
25519
  assert(state.install_id === getInstallId(), "install_id attached on migration");
24208
25520
  });
24209
25521
  }
@@ -24214,7 +25526,7 @@ async function testProgressSurvivesScratchWipe() {
24214
25526
  assert(loadProgress().total_minutes_saved === 180, "pre-scratch credits");
24215
25527
  await performScratchWipe();
24216
25528
  assert(loadProgress().total_minutes_saved === 180, "post-scratch credits preserved");
24217
- assert(existsSync21(join28(ntrpHome(), "install.json")), "install survives scratch");
25529
+ assert(existsSync23(join30(ntrpHome(), "install.json")), "install survives scratch");
24218
25530
  });
24219
25531
  }
24220
25532
  function testInstallIdStable() {
@@ -24234,7 +25546,7 @@ function testResetProgressKeepsInstall() {
24234
25546
  resetProgress();
24235
25547
  assert(loadProgress().total_minutes_saved === 0, "reset should clear hours");
24236
25548
  assert(getInstallId() === installId, "reset should keep install_id");
24237
- assert(existsSync21(join28(ntrpHome(), "install.json")), "install.json should remain");
25549
+ assert(existsSync23(join30(ntrpHome(), "install.json")), "install.json should remain");
24238
25550
  });
24239
25551
  }
24240
25552
  async function testScratchIncludeProgressWipesHours() {
@@ -24243,7 +25555,7 @@ async function testScratchIncludeProgressWipesHours() {
24243
25555
  recordTimeCredit("diagnose", ctx, { silent: true });
24244
25556
  const installId = getInstallId();
24245
25557
  await performScratchWipe({ includeProgress: true });
24246
- assert(!existsSync21(join28(ntrpHome(), "install.json")), "include-progress should remove install.json");
25558
+ assert(!existsSync23(join30(ntrpHome(), "install.json")), "include-progress should remove install.json");
24247
25559
  const state = loadProgress();
24248
25560
  assert(state.total_minutes_saved === 0, "include-progress should clear hours");
24249
25561
  assert(state.install_id !== installId, "include-progress should issue new install_id");
@@ -24267,8 +25579,8 @@ testInstallIdStable();
24267
25579
  testResetProgressKeepsInstall();
24268
25580
  async function testProgressCommandRegistered() {
24269
25581
  assert(hasCommand("progress"), "/progress should be in workflow registry");
24270
- const handler45 = await resolveHandler("progress");
24271
- assert(typeof handler45 === "function", "progress handler should load from importHandler map");
25582
+ const handler46 = await resolveHandler("progress");
25583
+ assert(typeof handler46 === "function", "progress handler should load from importHandler map");
24272
25584
  }
24273
25585
  await testProgressSurvivesScratchWipe();
24274
25586
  await testScratchIncludeProgressWipesHours();