@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.
package/dist/index.js CHANGED
@@ -1755,7 +1755,7 @@ function migrateUsageIfNeeded(state) {
1755
1755
  if (state.usage?.first_active_at) return { state, changed: false };
1756
1756
  const fromCredits = rebuildFromCredits(state.credits);
1757
1757
  const prior = state.usage;
1758
- const usage2 = {
1758
+ const usage3 = {
1759
1759
  sessions_closed: prior?.sessions_closed ?? 0,
1760
1760
  llm_calls: prior?.llm_calls ?? 0,
1761
1761
  input_tokens: prior?.input_tokens ?? 0,
@@ -1763,7 +1763,7 @@ function migrateUsageIfNeeded(state) {
1763
1763
  ...fromCredits,
1764
1764
  weekly: mergeWeekly(prior?.weekly ?? [], fromCredits.weekly)
1765
1765
  };
1766
- return { state: { ...state, usage: usage2 }, changed: true };
1766
+ return { state: { ...state, usage: usage3 }, changed: true };
1767
1767
  }
1768
1768
  var init_usage_backfill = __esm({
1769
1769
  "src/whimsy/usage-backfill.ts"() {
@@ -2287,48 +2287,48 @@ function bumpWeekly2(weekly, patch) {
2287
2287
  }
2288
2288
  function touchUsage(state, patch) {
2289
2289
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
2290
- const usage2 = ensureUsage(state);
2290
+ const usage3 = ensureUsage(state);
2291
2291
  return {
2292
2292
  ...state,
2293
2293
  usage: {
2294
- ...usage2,
2294
+ ...usage3,
2295
2295
  ...patch,
2296
- first_active_at: usage2.first_active_at ?? now2,
2296
+ first_active_at: usage3.first_active_at ?? now2,
2297
2297
  last_active_at: now2,
2298
- weekly: patch.weekly ?? usage2.weekly
2298
+ weekly: patch.weekly ?? usage3.weekly
2299
2299
  }
2300
2300
  };
2301
2301
  }
2302
2302
  function recordUsageFromCredit(action, minutes) {
2303
2303
  if (minutes <= 0) return;
2304
2304
  let state = loadProgress();
2305
- const usage2 = ensureUsage(state);
2306
- const weekly = bumpWeekly2(usage2.weekly, { minutes_saved: minutes, actions: 1 });
2305
+ const usage3 = ensureUsage(state);
2306
+ const weekly = bumpWeekly2(usage3.weekly, { minutes_saved: minutes, actions: 1 });
2307
2307
  const counters = { weekly };
2308
- if (action === "diagnose" || action === "diagnose_findings") counters.diagnoses = usage2.diagnoses + 1;
2309
- if (action === "metrics" || action === "metrics_findings") counters.metrics_runs = usage2.metrics_runs + 1;
2310
- if (action === "deliverable" || action === "deliverable_deck") counters.deliverables = usage2.deliverables + 1;
2311
- if (action === "nl_answer") counters.nl_exchanges = usage2.nl_exchanges + 1;
2308
+ if (action === "diagnose" || action === "diagnose_findings") counters.diagnoses = usage3.diagnoses + 1;
2309
+ if (action === "metrics" || action === "metrics_findings") counters.metrics_runs = usage3.metrics_runs + 1;
2310
+ if (action === "deliverable" || action === "deliverable_deck") counters.deliverables = usage3.deliverables + 1;
2311
+ if (action === "nl_answer") counters.nl_exchanges = usage3.nl_exchanges + 1;
2312
2312
  state = touchUsage(state, counters);
2313
2313
  saveProgress(state);
2314
2314
  }
2315
2315
  function recordSessionClosed() {
2316
2316
  let state = loadProgress();
2317
- const usage2 = ensureUsage(state);
2317
+ const usage3 = ensureUsage(state);
2318
2318
  state = touchUsage(state, {
2319
- sessions_closed: usage2.sessions_closed + 1,
2320
- weekly: bumpWeekly2(usage2.weekly, { actions: 1 })
2319
+ sessions_closed: usage3.sessions_closed + 1,
2320
+ weekly: bumpWeekly2(usage3.weekly, { actions: 1 })
2321
2321
  });
2322
2322
  saveProgress(state);
2323
2323
  }
2324
2324
  function recordLlmUsage(tokenUsage) {
2325
2325
  let state = loadProgress();
2326
- const usage2 = ensureUsage(state);
2327
- const weekly = bumpWeekly2(usage2.weekly, { llm_calls: 1 });
2326
+ const usage3 = ensureUsage(state);
2327
+ const weekly = bumpWeekly2(usage3.weekly, { llm_calls: 1 });
2328
2328
  state = touchUsage(state, {
2329
- llm_calls: usage2.llm_calls + 1,
2330
- input_tokens: usage2.input_tokens + (tokenUsage?.input_tokens ?? 0),
2331
- output_tokens: usage2.output_tokens + (tokenUsage?.output_tokens ?? 0),
2329
+ llm_calls: usage3.llm_calls + 1,
2330
+ input_tokens: usage3.input_tokens + (tokenUsage?.input_tokens ?? 0),
2331
+ output_tokens: usage3.output_tokens + (tokenUsage?.output_tokens ?? 0),
2332
2332
  weekly
2333
2333
  });
2334
2334
  saveProgress(state);
@@ -3109,10 +3109,232 @@ var init_context2 = __esm({
3109
3109
  }
3110
3110
  });
3111
3111
 
3112
+ // src/ai/llm/providers.ts
3113
+ var providers_exports = {};
3114
+ __export(providers_exports, {
3115
+ findSpecByConfigKey: () => findSpecByConfigKey,
3116
+ getProviderSpec: () => getProviderSpec,
3117
+ isEndpointEnabled: () => isEndpointEnabled,
3118
+ keyConfigNameFor: () => keyConfigNameFor,
3119
+ listProviderSpecs: () => listProviderSpecs,
3120
+ loadCustomProviders: () => loadCustomProviders,
3121
+ modelsUrl: () => modelsUrl,
3122
+ providerLabel: () => providerLabel,
3123
+ removeCustomProvider: () => removeCustomProvider,
3124
+ resetProvidersCache: () => resetProvidersCache,
3125
+ saveCustomProvider: () => saveCustomProvider
3126
+ });
3127
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
3128
+ import { join as join7 } from "path";
3129
+ function providersPath() {
3130
+ return join7(ntrpHome(), "providers.json");
3131
+ }
3132
+ function loadCustomProviders() {
3133
+ if (cachedEntries) return cachedEntries;
3134
+ const path = providersPath();
3135
+ if (!existsSync7(path)) {
3136
+ cachedEntries = [];
3137
+ return cachedEntries;
3138
+ }
3139
+ try {
3140
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
3141
+ cachedEntries = Array.isArray(parsed.providers) ? parsed.providers : [];
3142
+ } catch {
3143
+ cachedEntries = [];
3144
+ }
3145
+ return cachedEntries;
3146
+ }
3147
+ function saveCustomProvider(entry) {
3148
+ const entries = loadCustomProviders().filter((e) => e.id !== entry.id);
3149
+ entries.push(entry);
3150
+ writeFileSync6(providersPath(), JSON.stringify({ version: 1, providers: entries }, null, 2) + "\n");
3151
+ cachedEntries = entries;
3152
+ }
3153
+ function removeCustomProvider(id) {
3154
+ const entries = loadCustomProviders().filter((e) => e.id !== id);
3155
+ writeFileSync6(providersPath(), JSON.stringify({ version: 1, providers: entries }, null, 2) + "\n");
3156
+ cachedEntries = entries;
3157
+ }
3158
+ function resetProvidersCache() {
3159
+ cachedEntries = null;
3160
+ }
3161
+ function customEntryToSpec(entry) {
3162
+ return {
3163
+ id: entry.id,
3164
+ label: entry.label ?? entry.id,
3165
+ api: "openai-compat",
3166
+ base_url: entry.base_url.replace(/\/+$/, ""),
3167
+ key_prefixes: [],
3168
+ shared_prefixes: [],
3169
+ key_config_name: keyConfigNameFor(entry.id),
3170
+ requires_key: entry.requires_key ?? false,
3171
+ custom: true
3172
+ };
3173
+ }
3174
+ function keyConfigNameFor(providerId) {
3175
+ return providerId === "anthropic" ? "api-key" : `${providerId}-api-key`;
3176
+ }
3177
+ function listProviderSpecs() {
3178
+ const customs = loadCustomProviders();
3179
+ const customById = new Map(customs.map((e) => [e.id, e]));
3180
+ const specs = BUILTIN_SPECS.map((spec) => {
3181
+ const override = customById.get(spec.id);
3182
+ if (override?.base_url) {
3183
+ return { ...spec, base_url: override.base_url.replace(/\/+$/, "") };
3184
+ }
3185
+ return spec;
3186
+ });
3187
+ for (const entry of customs) {
3188
+ if (!BUILTIN_SPECS.some((s) => s.id === entry.id)) {
3189
+ specs.push(customEntryToSpec(entry));
3190
+ }
3191
+ }
3192
+ return specs;
3193
+ }
3194
+ function getProviderSpec(id) {
3195
+ return listProviderSpecs().find((s) => s.id === id);
3196
+ }
3197
+ function findSpecByConfigKey(configKey) {
3198
+ return listProviderSpecs().find((s) => s.key_config_name === configKey);
3199
+ }
3200
+ function isEndpointEnabled(id) {
3201
+ const entry = loadCustomProviders().find((e) => e.id === id);
3202
+ return !!entry && entry.enabled !== false;
3203
+ }
3204
+ function modelsUrl(spec) {
3205
+ if (spec.api === "anthropic") return `${spec.base_url}/v1/models?limit=100`;
3206
+ return `${spec.base_url}/models`;
3207
+ }
3208
+ function providerLabel(id) {
3209
+ return getProviderSpec(id)?.label ?? id;
3210
+ }
3211
+ var BUILTIN_SPECS, cachedEntries;
3212
+ var init_providers = __esm({
3213
+ "src/ai/llm/providers.ts"() {
3214
+ "use strict";
3215
+ init_store();
3216
+ BUILTIN_SPECS = [
3217
+ {
3218
+ id: "anthropic",
3219
+ label: "Anthropic",
3220
+ api: "anthropic",
3221
+ base_url: "https://api.anthropic.com",
3222
+ key_prefixes: ["sk-ant-"],
3223
+ shared_prefixes: [],
3224
+ key_config_name: "api-key",
3225
+ requires_key: true
3226
+ },
3227
+ {
3228
+ id: "openai",
3229
+ label: "OpenAI",
3230
+ api: "openai-compat",
3231
+ base_url: "https://api.openai.com/v1",
3232
+ key_prefixes: ["sk-proj-", "sk-svcacct-", "sk-admin-"],
3233
+ shared_prefixes: ["sk-"],
3234
+ key_config_name: "openai-api-key",
3235
+ env_var: "OPENAI_API_KEY",
3236
+ requires_key: true
3237
+ },
3238
+ {
3239
+ id: "google",
3240
+ label: "Google Gemini",
3241
+ api: "openai-compat",
3242
+ base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
3243
+ key_prefixes: ["AIza"],
3244
+ shared_prefixes: [],
3245
+ key_config_name: "google-api-key",
3246
+ requires_key: true
3247
+ },
3248
+ {
3249
+ id: "groq",
3250
+ label: "Groq",
3251
+ api: "openai-compat",
3252
+ base_url: "https://api.groq.com/openai/v1",
3253
+ key_prefixes: ["gsk_"],
3254
+ shared_prefixes: [],
3255
+ key_config_name: "groq-api-key",
3256
+ requires_key: true
3257
+ },
3258
+ {
3259
+ id: "mistral",
3260
+ label: "Mistral",
3261
+ api: "openai-compat",
3262
+ base_url: "https://api.mistral.ai/v1",
3263
+ key_prefixes: [],
3264
+ shared_prefixes: [],
3265
+ key_config_name: "mistral-api-key",
3266
+ requires_key: true
3267
+ },
3268
+ {
3269
+ id: "deepseek",
3270
+ label: "DeepSeek",
3271
+ api: "openai-compat",
3272
+ base_url: "https://api.deepseek.com/v1",
3273
+ key_prefixes: [],
3274
+ shared_prefixes: ["sk-"],
3275
+ key_config_name: "deepseek-api-key",
3276
+ requires_key: true
3277
+ },
3278
+ {
3279
+ id: "xai",
3280
+ label: "xAI",
3281
+ api: "openai-compat",
3282
+ base_url: "https://api.x.ai/v1",
3283
+ key_prefixes: ["xai-"],
3284
+ shared_prefixes: [],
3285
+ key_config_name: "xai-api-key",
3286
+ requires_key: true
3287
+ },
3288
+ {
3289
+ id: "openrouter",
3290
+ label: "OpenRouter",
3291
+ api: "openai-compat",
3292
+ base_url: "https://openrouter.ai/api/v1",
3293
+ key_prefixes: ["sk-or-"],
3294
+ shared_prefixes: [],
3295
+ key_config_name: "openrouter-api-key",
3296
+ requires_key: true
3297
+ },
3298
+ {
3299
+ id: "together",
3300
+ label: "Together AI",
3301
+ api: "openai-compat",
3302
+ base_url: "https://api.together.xyz/v1",
3303
+ key_prefixes: [],
3304
+ shared_prefixes: [],
3305
+ key_config_name: "together-api-key",
3306
+ requires_key: true
3307
+ },
3308
+ {
3309
+ id: "fireworks",
3310
+ label: "Fireworks AI",
3311
+ api: "openai-compat",
3312
+ base_url: "https://api.fireworks.ai/inference/v1",
3313
+ key_prefixes: ["fw_"],
3314
+ shared_prefixes: [],
3315
+ key_config_name: "fireworks-api-key",
3316
+ requires_key: true
3317
+ },
3318
+ {
3319
+ id: "ollama",
3320
+ label: "Ollama (local)",
3321
+ api: "openai-compat",
3322
+ base_url: "http://localhost:11434/v1",
3323
+ key_prefixes: [],
3324
+ shared_prefixes: [],
3325
+ key_config_name: "ollama-api-key",
3326
+ requires_key: false
3327
+ }
3328
+ ];
3329
+ cachedEntries = null;
3330
+ }
3331
+ });
3332
+
3112
3333
  // src/config/llm-config.ts
3113
3334
  function parseProvider(raw) {
3114
- if (raw === "anthropic" || raw === "openai") return raw;
3115
- return void 0;
3335
+ if (!raw?.trim()) return void 0;
3336
+ const id = raw.trim();
3337
+ return getProviderSpec(id) ? id : void 0;
3116
3338
  }
3117
3339
  function parseTier(raw) {
3118
3340
  if (raw === "high" || raw === "medium" || raw === "low") return raw;
@@ -3120,7 +3342,7 @@ function parseTier(raw) {
3120
3342
  }
3121
3343
  function parseFailoverOrder(raw) {
3122
3344
  if (!raw?.trim()) return ["openai"];
3123
- return raw.split(",").map((s) => s.trim()).filter((s) => s === "anthropic" || s === "openai");
3345
+ return raw.split(",").map((s) => s.trim()).filter((s) => !!s && !!getProviderSpec(s));
3124
3346
  }
3125
3347
  function parseAutoFailover(raw) {
3126
3348
  if (!raw) return false;
@@ -3135,30 +3357,42 @@ function getOpenAiApiKey() {
3135
3357
  if (fromConfig) return fromConfig;
3136
3358
  return process.env.OPENAI_API_KEY?.trim() || void 0;
3137
3359
  }
3360
+ function getProviderApiKey(provider) {
3361
+ const spec = getProviderSpec(provider);
3362
+ if (!spec) return void 0;
3363
+ const record = loadConfig();
3364
+ const fromConfig = record[spec.key_config_name]?.trim();
3365
+ if (fromConfig) return fromConfig;
3366
+ if (spec.env_var) {
3367
+ const fromEnv = process.env[spec.env_var]?.trim();
3368
+ if (fromEnv) return fromEnv;
3369
+ }
3370
+ return void 0;
3371
+ }
3138
3372
  function hasProviderKey(provider) {
3139
- if (provider === "anthropic") return !!getAnthropicApiKey();
3140
- return !!getOpenAiApiKey();
3373
+ const spec = getProviderSpec(provider);
3374
+ if (!spec) return false;
3375
+ if (!spec.requires_key) return isEndpointEnabled(spec.id) || !!getProviderApiKey(provider);
3376
+ return !!getProviderApiKey(provider);
3141
3377
  }
3142
3378
  function getAvailableProviders() {
3143
- const out = [];
3144
- if (hasProviderKey("anthropic")) out.push("anthropic");
3145
- if (hasProviderKey("openai")) out.push("openai");
3146
- return out;
3379
+ return listProviderSpecs().filter((s) => hasProviderKey(s.id)).map((s) => s.id);
3147
3380
  }
3148
3381
  function hasAnyLlmProvider() {
3149
3382
  return getAvailableProviders().length > 0;
3150
3383
  }
3384
+ function hasKeylessConfiguredProvider() {
3385
+ return listProviderSpecs().some((s) => !s.requires_key && hasProviderKey(s.id));
3386
+ }
3151
3387
  function applyLazyMigration(config) {
3152
3388
  if (migrated) return;
3153
3389
  migrated = true;
3154
3390
  let changed = false;
3155
3391
  const record = config;
3156
3392
  if (!record["llm-primary"]) {
3157
- if (record["api-key"]) {
3158
- record["llm-primary"] = "anthropic";
3159
- changed = true;
3160
- } else if (record["openai-api-key"] || process.env.OPENAI_API_KEY) {
3161
- record["llm-primary"] = "openai";
3393
+ const available = getAvailableProviders();
3394
+ if (available.length > 0) {
3395
+ record["llm-primary"] = available[0];
3162
3396
  changed = true;
3163
3397
  }
3164
3398
  }
@@ -3201,20 +3435,20 @@ function loadLlmConfig() {
3201
3435
  openaiKey: getOpenAiApiKey()
3202
3436
  };
3203
3437
  }
3204
- function getProviderApiKey(provider) {
3205
- if (provider === "anthropic") return getAnthropicApiKey();
3206
- return getOpenAiApiKey();
3207
- }
3208
3438
  function getInvestigationApiKey(provider) {
3209
3439
  if (provider === "anthropic") {
3210
3440
  return process.env.NTRP_INVESTIGATION_API_KEY?.trim() || getAnthropicApiKey();
3211
3441
  }
3212
- return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
3442
+ if (provider === "openai") {
3443
+ return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
3444
+ }
3445
+ return getProviderApiKey(provider);
3213
3446
  }
3214
3447
  var migrated;
3215
3448
  var init_llm_config = __esm({
3216
3449
  "src/config/llm-config.ts"() {
3217
3450
  "use strict";
3451
+ init_providers();
3218
3452
  init_store();
3219
3453
  migrated = false;
3220
3454
  }
@@ -3644,8 +3878,8 @@ var init_admin_confirm = __esm({
3644
3878
  });
3645
3879
 
3646
3880
  // src/services/scratch-wipe.ts
3647
- import { existsSync as existsSync7, rmSync as rmSync3, unlinkSync as unlinkSync3 } from "fs";
3648
- import { join as join7 } from "path";
3881
+ import { existsSync as existsSync8, rmSync as rmSync3, unlinkSync as unlinkSync3 } from "fs";
3882
+ import { join as join8 } from "path";
3649
3883
  async function performScratchWipe(opts = {}) {
3650
3884
  const home = ntrpHome();
3651
3885
  const removed = [];
@@ -3655,25 +3889,25 @@ async function performScratchWipe(opts = {}) {
3655
3889
  } catch {
3656
3890
  }
3657
3891
  const targets = [
3658
- { path: join7(home, "config.json"), kind: "file" },
3659
- { path: join7(home, "profile.json"), kind: "file" },
3660
- { path: join7(home, "demo-taxonomy.json"), kind: "file" },
3661
- { path: join7(home, "update-check.json"), kind: "file" },
3662
- { path: join7(home, "state.json.bak"), kind: "file" },
3663
- { path: join7(home, "ntrp.duckdb"), kind: "file" },
3664
- { path: join7(home, "ntrp.duckdb.wal"), kind: "file" },
3665
- { path: join7(home, "sessions"), kind: "dir" },
3666
- { path: join7(home, "datasets"), kind: "dir" }
3892
+ { path: join8(home, "config.json"), kind: "file" },
3893
+ { path: join8(home, "profile.json"), kind: "file" },
3894
+ { path: join8(home, "demo-taxonomy.json"), kind: "file" },
3895
+ { path: join8(home, "update-check.json"), kind: "file" },
3896
+ { path: join8(home, "state.json.bak"), kind: "file" },
3897
+ { path: join8(home, "ntrp.duckdb"), kind: "file" },
3898
+ { path: join8(home, "ntrp.duckdb.wal"), kind: "file" },
3899
+ { path: join8(home, "sessions"), kind: "dir" },
3900
+ { path: join8(home, "datasets"), kind: "dir" }
3667
3901
  ];
3668
3902
  if (opts.includeProgress) {
3669
3903
  targets.push(
3670
- { path: join7(home, "install.json"), kind: "file" },
3671
- { path: join7(home, "progress.json"), kind: "file" },
3672
- { path: join7(home, "state.json"), kind: "file" }
3904
+ { path: join8(home, "install.json"), kind: "file" },
3905
+ { path: join8(home, "progress.json"), kind: "file" },
3906
+ { path: join8(home, "state.json"), kind: "file" }
3673
3907
  );
3674
3908
  }
3675
3909
  for (const { path, kind } of targets) {
3676
- if (!existsSync7(path)) continue;
3910
+ if (!existsSync8(path)) continue;
3677
3911
  try {
3678
3912
  if (kind === "dir") {
3679
3913
  rmSync3(path, { recursive: true, force: true });
@@ -3701,10 +3935,10 @@ var init_scratch_wipe = __esm({
3701
3935
  });
3702
3936
 
3703
3937
  // src/config/profile.ts
3704
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
3705
- import { join as join8 } from "path";
3938
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
3939
+ import { join as join9 } from "path";
3706
3940
  function ensureDir5() {
3707
- if (!existsSync8(NTRP_DIR3)) {
3941
+ if (!existsSync9(NTRP_DIR3)) {
3708
3942
  mkdirSync6(NTRP_DIR3, { recursive: true });
3709
3943
  }
3710
3944
  }
@@ -3712,16 +3946,16 @@ function profilePath() {
3712
3946
  return PROFILE_PATH;
3713
3947
  }
3714
3948
  function profileExists() {
3715
- return existsSync8(PROFILE_PATH);
3949
+ return existsSync9(PROFILE_PATH);
3716
3950
  }
3717
3951
  function isProfileConfigured(profile = loadProfile()) {
3718
3952
  if (!profile) return false;
3719
3953
  return profile.company_name.trim().length > 0;
3720
3954
  }
3721
3955
  function loadProfile() {
3722
- if (!existsSync8(PROFILE_PATH)) return null;
3956
+ if (!existsSync9(PROFILE_PATH)) return null;
3723
3957
  try {
3724
- const parsed = JSON.parse(readFileSync6(PROFILE_PATH, "utf-8"));
3958
+ const parsed = JSON.parse(readFileSync7(PROFILE_PATH, "utf-8"));
3725
3959
  if (!parsed || typeof parsed !== "object") return null;
3726
3960
  return parsed;
3727
3961
  } catch {
@@ -3737,7 +3971,7 @@ function saveProfile(profile) {
3737
3971
  created_at: profile.created_at || now2,
3738
3972
  updated_at: now2
3739
3973
  };
3740
- writeFileSync6(PROFILE_PATH, JSON.stringify(toWrite, null, 2) + "\n");
3974
+ writeFileSync7(PROFILE_PATH, JSON.stringify(toWrite, null, 2) + "\n");
3741
3975
  }
3742
3976
  function updateProfile(patch) {
3743
3977
  const existing = loadProfile();
@@ -3763,7 +3997,7 @@ var init_profile = __esm({
3763
3997
  "use strict";
3764
3998
  init_store();
3765
3999
  NTRP_DIR3 = ntrpHome();
3766
- PROFILE_PATH = join8(NTRP_DIR3, "profile.json");
4000
+ PROFILE_PATH = join9(NTRP_DIR3, "profile.json");
3767
4001
  }
3768
4002
  });
3769
4003
 
@@ -4115,7 +4349,13 @@ function resolvePrimaryApiKey(ctx) {
4115
4349
  if (isInvestigationMode(ctx)) {
4116
4350
  return getInvestigationApiKey(primary) ?? getInvestigationApiKey("anthropic") ?? getInvestigationApiKey("openai");
4117
4351
  }
4118
- return getProviderApiKey(primary) ?? getProviderApiKey("anthropic") ?? getProviderApiKey("openai");
4352
+ const primaryKey = getProviderApiKey(primary);
4353
+ if (primaryKey) return primaryKey;
4354
+ for (const provider of getAvailableProviders()) {
4355
+ const key = getProviderApiKey(provider);
4356
+ if (key) return key;
4357
+ }
4358
+ return void 0;
4119
4359
  }
4120
4360
  function canUseReplAi(ctx) {
4121
4361
  if (!ctx) return false;
@@ -4126,25 +4366,21 @@ function canUseReplAi(ctx) {
4126
4366
  }
4127
4367
  function assertReplAi(ctx) {
4128
4368
  if (!ctx) {
4129
- throw new Error(
4130
- "AI features require stored API keys. Run `ntrp`, then /config set api-key or /config set openai-api-key."
4131
- );
4369
+ throw new Error(`AI features require stored API keys. Run \`ntrp\`, then /connect.`);
4132
4370
  }
4133
4371
  if (!canUseReplAi(ctx)) {
4134
4372
  if (!hasAnyLlmProvider()) {
4135
- throw new Error(
4136
- "No LLM API key configured. Run: /config set api-key (Anthropic) and/or /config set openai-api-key"
4137
- );
4373
+ throw new Error(NO_KEY_MESSAGE);
4138
4374
  }
4139
4375
  throw new Error(
4140
4376
  "AI features run only in the interactive REPL or headless mode with stored keys."
4141
4377
  );
4142
4378
  }
4143
4379
  const key = resolvePrimaryApiKey(ctx);
4144
- if (!key) {
4145
- throw new Error("No LLM API key configured. Run: /config set api-key or /config set openai-api-key");
4380
+ if (!key && !hasKeylessConfiguredProvider()) {
4381
+ throw new Error(NO_KEY_MESSAGE);
4146
4382
  }
4147
- return key;
4383
+ return key ?? "";
4148
4384
  }
4149
4385
  function hasEnvApiKeyHint() {
4150
4386
  return !!(process.env.ANTHROPIC_API_KEY ?? process.env.NTRP_API_KEY ?? process.env.OPENAI_API_KEY);
@@ -4157,10 +4393,12 @@ function describeLlmReadiness() {
4157
4393
  openai: providers.includes("openai")
4158
4394
  };
4159
4395
  }
4396
+ var NO_KEY_MESSAGE;
4160
4397
  var init_gate = __esm({
4161
4398
  "src/ai/llm/gate.ts"() {
4162
4399
  "use strict";
4163
4400
  init_llm_config();
4401
+ NO_KEY_MESSAGE = "No LLM API key configured. Run /connect and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).";
4164
4402
  }
4165
4403
  });
4166
4404
 
@@ -4185,18 +4423,18 @@ var init_repl_api = __esm({
4185
4423
  });
4186
4424
 
4187
4425
  // src/demo/taxonomy-cache.ts
4188
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync9, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
4426
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync10, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
4189
4427
  import { homedir as homedir3 } from "os";
4190
- import { join as join9 } from "path";
4428
+ import { join as join10 } from "path";
4191
4429
  function ensureDir6() {
4192
- if (!existsSync9(NTRP_DIR4)) {
4430
+ if (!existsSync10(NTRP_DIR4)) {
4193
4431
  mkdirSync7(NTRP_DIR4, { recursive: true });
4194
4432
  }
4195
4433
  }
4196
4434
  function loadCachedTaxonomy(profile) {
4197
- if (!existsSync9(TAXONOMY_PATH)) return null;
4435
+ if (!existsSync10(TAXONOMY_PATH)) return null;
4198
4436
  try {
4199
- const parsed = JSON.parse(readFileSync7(TAXONOMY_PATH, "utf-8"));
4437
+ const parsed = JSON.parse(readFileSync8(TAXONOMY_PATH, "utf-8"));
4200
4438
  if (!parsed || typeof parsed !== "object") return null;
4201
4439
  if (parsed.profile_updated_at !== profile.updated_at) return null;
4202
4440
  return parsed;
@@ -4206,10 +4444,10 @@ function loadCachedTaxonomy(profile) {
4206
4444
  }
4207
4445
  function saveCachedTaxonomy(taxonomy) {
4208
4446
  ensureDir6();
4209
- writeFileSync7(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
4447
+ writeFileSync8(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
4210
4448
  }
4211
4449
  function invalidateTaxonomy() {
4212
- if (existsSync9(TAXONOMY_PATH)) {
4450
+ if (existsSync10(TAXONOMY_PATH)) {
4213
4451
  try {
4214
4452
  unlinkSync4(TAXONOMY_PATH);
4215
4453
  } catch {
@@ -4220,8 +4458,8 @@ var NTRP_DIR4, TAXONOMY_PATH;
4220
4458
  var init_taxonomy_cache = __esm({
4221
4459
  "src/demo/taxonomy-cache.ts"() {
4222
4460
  "use strict";
4223
- NTRP_DIR4 = join9(homedir3(), ".ntrp");
4224
- TAXONOMY_PATH = join9(NTRP_DIR4, "demo-taxonomy.json");
4461
+ NTRP_DIR4 = join10(homedir3(), ".ntrp");
4462
+ TAXONOMY_PATH = join10(NTRP_DIR4, "demo-taxonomy.json");
4225
4463
  }
4226
4464
  });
4227
4465
 
@@ -4246,6 +4484,12 @@ var init_types = __esm({
4246
4484
  });
4247
4485
 
4248
4486
  // src/ai/llm/errors.ts
4487
+ function isToolsUnsupportedMessage(message) {
4488
+ const msg = message.toLowerCase();
4489
+ const mentionsTools = msg.includes("tool") || msg.includes("function");
4490
+ const mentionsUnsupported = msg.includes("not support") || msg.includes("unsupported") || msg.includes("no support") || msg.includes("not available") || msg.includes("not enabled");
4491
+ return mentionsTools && mentionsUnsupported;
4492
+ }
4249
4493
  function mapAnthropicError(err, provider) {
4250
4494
  const e = err;
4251
4495
  const status = e.status;
@@ -4263,6 +4507,9 @@ function mapAnthropicError(err, provider) {
4263
4507
  if (status === 503) {
4264
4508
  return new LlmError("OVERLOADED", message, provider, status);
4265
4509
  }
4510
+ if (isToolsUnsupportedMessage(message)) {
4511
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
4512
+ }
4266
4513
  if (status === 404 || message.toLowerCase().includes("model")) {
4267
4514
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
4268
4515
  }
@@ -4271,6 +4518,11 @@ function mapAnthropicError(err, provider) {
4271
4518
  }
4272
4519
  return new LlmError("UNKNOWN", message, provider, status);
4273
4520
  }
4521
+ function isModelNotFoundMessage(message) {
4522
+ const msg = message.toLowerCase();
4523
+ if (!msg.includes("model")) return false;
4524
+ 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");
4525
+ }
4274
4526
  function mapOpenAiError(err, provider) {
4275
4527
  const e = err;
4276
4528
  const status = e.status;
@@ -4285,7 +4537,10 @@ function mapOpenAiError(err, provider) {
4285
4537
  if (status === 503 || code === "server_error") {
4286
4538
  return new LlmError("OVERLOADED", message, provider, status);
4287
4539
  }
4288
- if (status === 404 || code === "model_not_found") {
4540
+ if (isToolsUnsupportedMessage(message)) {
4541
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
4542
+ }
4543
+ if (status === 404 || code === "model_not_found" || code === "model_decommissioned" || isModelNotFoundMessage(message)) {
4289
4544
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
4290
4545
  }
4291
4546
  if (code === "context_length_exceeded") {
@@ -4421,8 +4676,15 @@ var init_anthropic = __esm({
4421
4676
  }
4422
4677
  });
4423
4678
 
4424
- // src/ai/llm/adapters/openai.ts
4679
+ // src/ai/llm/adapters/openai-compat.ts
4425
4680
  import OpenAI from "openai";
4681
+ function makeClient(apiKey, baseUrl) {
4682
+ return new OpenAI({
4683
+ // Keyless endpoints (Ollama) still need a non-empty string for the SDK.
4684
+ apiKey: apiKey || "local",
4685
+ ...baseUrl ? { baseURL: baseUrl } : {}
4686
+ });
4687
+ }
4426
4688
  function toOpenAiTools(tools) {
4427
4689
  return tools.map((t) => ({
4428
4690
  type: "function",
@@ -4491,9 +4753,8 @@ function parseResponse2(message) {
4491
4753
  assistant_message: { role: "assistant", content: text, tool_calls }
4492
4754
  };
4493
4755
  }
4494
- async function openaiComplete(apiKey, model, req) {
4495
- const provider = "openai";
4496
- const client = new OpenAI({ apiKey });
4756
+ async function openaiCompatComplete(provider, baseUrl, apiKey, model, req) {
4757
+ const client = makeClient(apiKey, baseUrl);
4497
4758
  try {
4498
4759
  const response = await client.chat.completions.create({
4499
4760
  model,
@@ -4503,7 +4764,7 @@ async function openaiComplete(apiKey, model, req) {
4503
4764
  });
4504
4765
  const choice = response.choices[0];
4505
4766
  if (!choice?.message) {
4506
- throw new Error("OpenAI returned no message");
4767
+ throw new Error(`${provider} returned no message`);
4507
4768
  }
4508
4769
  const parsed = parseResponse2(choice.message);
4509
4770
  if (response.usage) {
@@ -4514,15 +4775,11 @@ async function openaiComplete(apiKey, model, req) {
4514
4775
  }
4515
4776
  return parsed;
4516
4777
  } catch (err) {
4517
- if (err instanceof OpenAI.APIError) {
4518
- throw mapOpenAiError(err, provider);
4519
- }
4520
4778
  throw mapOpenAiError(err, provider);
4521
4779
  }
4522
4780
  }
4523
- async function* openaiStream(apiKey, model, req) {
4524
- const provider = "openai";
4525
- const client = new OpenAI({ apiKey });
4781
+ async function* openaiCompatStream(provider, baseUrl, apiKey, model, req) {
4782
+ const client = makeClient(apiKey, baseUrl);
4526
4783
  try {
4527
4784
  const stream = await client.chat.completions.create({
4528
4785
  model,
@@ -4535,68 +4792,118 @@ async function* openaiStream(apiKey, model, req) {
4535
4792
  if (delta) yield { type: "text_delta", text: delta };
4536
4793
  }
4537
4794
  } catch (err) {
4538
- if (err instanceof OpenAI.APIError) {
4539
- throw mapOpenAiError(err, provider);
4540
- }
4541
4795
  throw mapOpenAiError(err, provider);
4542
4796
  }
4543
4797
  }
4544
- var init_openai = __esm({
4545
- "src/ai/llm/adapters/openai.ts"() {
4798
+ var init_openai_compat = __esm({
4799
+ "src/ai/llm/adapters/openai-compat.ts"() {
4546
4800
  "use strict";
4547
4801
  init_errors();
4548
4802
  }
4549
4803
  });
4550
4804
 
4551
- // src/ai/llm/catalog.ts
4552
- function getCatalogEntry(id) {
4553
- return byId.get(id);
4554
- }
4555
- function listCatalogEntries(provider) {
4556
- if (!provider) return [...ENTRIES];
4557
- return ENTRIES.filter((e) => e.provider === provider);
4558
- }
4559
- function resolveRetiredModel(id) {
4560
- const seen = /* @__PURE__ */ new Set();
4561
- let current = id;
4562
- while (true) {
4563
- if (seen.has(current)) break;
4564
- seen.add(current);
4565
- const entry = byId.get(current);
4566
- if (!entry) return current;
4567
- if (entry.status === "active") return entry.id;
4568
- if (!entry.successor_id) {
4569
- const fallback = cheapestActiveInTier(entry.provider, entry.tier);
4570
- return fallback?.id ?? current;
4571
- }
4572
- current = entry.successor_id;
4805
+ // src/ai/llm/models-cache.ts
4806
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
4807
+ import { join as join11 } from "path";
4808
+ function cachePath() {
4809
+ return join11(ntrpHome(), "models.json");
4810
+ }
4811
+ function loadFile() {
4812
+ if (cached) return cached;
4813
+ const path = cachePath();
4814
+ if (!existsSync11(path)) {
4815
+ cached = { version: 1, providers: {} };
4816
+ return cached;
4573
4817
  }
4574
- return id;
4818
+ try {
4819
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
4820
+ cached = { version: 1, providers: parsed.providers ?? {} };
4821
+ } catch {
4822
+ cached = { version: 1, providers: {} };
4823
+ }
4824
+ return cached;
4825
+ }
4826
+ function saveFile(file) {
4827
+ writeFileSync9(cachePath(), JSON.stringify(file, null, 2) + "\n");
4828
+ cached = file;
4829
+ }
4830
+ function getProviderModels(provider) {
4831
+ return loadFile().providers[provider];
4832
+ }
4833
+ function setProviderModels(provider, entry) {
4834
+ const file = loadFile();
4835
+ file.providers[provider] = entry;
4836
+ saveFile(file);
4837
+ }
4838
+ function getCachedTierModel(provider, tier) {
4839
+ return getProviderModels(provider)?.tier_stack?.[tier];
4840
+ }
4841
+ function findCachedModel(provider, modelId) {
4842
+ return getProviderModels(provider)?.models.find((m) => m.id === modelId);
4575
4843
  }
4576
- function cheapestActiveInTier(provider, tier) {
4844
+ function cachedModelProvider(modelId) {
4845
+ const file = loadFile();
4846
+ for (const [provider, entry] of Object.entries(file.providers)) {
4847
+ if (entry.models.some((m) => m.id === modelId)) return provider;
4848
+ }
4849
+ return void 0;
4850
+ }
4851
+ function markModelNoTools(provider, modelId) {
4852
+ const file = loadFile();
4853
+ const entry = file.providers[provider];
4854
+ if (!entry) return;
4855
+ const noTools = new Set(entry.quirks?.no_tools ?? []);
4856
+ if (noTools.has(modelId)) return;
4857
+ noTools.add(modelId);
4858
+ entry.quirks = { ...entry.quirks, no_tools: [...noTools] };
4859
+ saveFile(file);
4860
+ }
4861
+ function modelHasNoToolsQuirk(provider, modelId) {
4862
+ return !!getProviderModels(provider)?.quirks?.no_tools?.includes(modelId);
4863
+ }
4864
+ function isProviderCacheStale(provider, ttlMs = CACHE_TTL_MS) {
4865
+ const entry = getProviderModels(provider);
4866
+ if (!entry) return true;
4867
+ const fetched = Date.parse(entry.fetched_at);
4868
+ if (Number.isNaN(fetched)) return true;
4869
+ return Date.now() - fetched > ttlMs;
4870
+ }
4871
+ var CACHE_TTL_MS, cached;
4872
+ var init_models_cache = __esm({
4873
+ "src/ai/llm/models-cache.ts"() {
4874
+ "use strict";
4875
+ init_store();
4876
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4877
+ cached = null;
4878
+ }
4879
+ });
4880
+
4881
+ // src/ai/llm/catalog.ts
4882
+ function catalogTierDefault(provider, tier) {
4577
4883
  const candidates = ENTRIES.filter(
4578
4884
  (e) => e.provider === provider && e.tier === tier && e.status === "active"
4579
4885
  );
4580
4886
  if (candidates.length === 0) return void 0;
4581
4887
  return candidates.sort((a, b) => a.relative_cost - b.relative_cost)[0];
4582
4888
  }
4583
- function getTierDefault(provider, tier) {
4584
- const entry = cheapestActiveInTier(provider, tier);
4585
- if (!entry) {
4586
- throw new Error(`No active ${tier}-tier model for provider ${provider} in catalog`);
4587
- }
4588
- return entry;
4889
+ function modelProviderHint(modelId) {
4890
+ return cachedModelProvider(modelId) ?? byId.get(modelId)?.provider;
4589
4891
  }
4590
- function resolveModel(provider, tier, override) {
4591
- if (override) {
4592
- const entry = byId.get(override);
4593
- if (!entry) return resolveRetiredModel(override);
4594
- if (entry.status === "retired") return resolveRetiredModel(override);
4595
- return entry.id;
4596
- }
4597
- return getTierDefault(provider, tier).id;
4892
+ function overrideForProvider(override, provider, activeProvider) {
4893
+ if (!override) return void 0;
4894
+ const hint = modelProviderHint(override);
4895
+ if (hint) return hint === provider ? override : void 0;
4896
+ return provider === activeProvider ? override : void 0;
4897
+ }
4898
+ function resolveModelSafe(provider, tier, override) {
4899
+ if (override) return override;
4900
+ const discovered = getCachedTierModel(provider, tier);
4901
+ if (discovered) return discovered;
4902
+ return catalogTierDefault(provider, tier)?.id;
4598
4903
  }
4599
4904
  function formatModelLabel(provider, modelId) {
4905
+ const cachedName = findCachedModel(provider, modelId)?.display_name;
4906
+ if (cachedName) return `${provider}/${cachedName}`;
4600
4907
  const entry = byId.get(modelId);
4601
4908
  return entry ? `${provider}/${entry.display_name}` : `${provider}/${modelId}`;
4602
4909
  }
@@ -4604,6 +4911,7 @@ var ENTRIES, byId;
4604
4911
  var init_catalog = __esm({
4605
4912
  "src/ai/llm/catalog.ts"() {
4606
4913
  "use strict";
4914
+ init_models_cache();
4607
4915
  ENTRIES = [
4608
4916
  {
4609
4917
  id: "claude-opus-4-6",
@@ -4676,6 +4984,306 @@ var init_catalog = __esm({
4676
4984
  }
4677
4985
  });
4678
4986
 
4987
+ // src/ai/llm/http.ts
4988
+ import { readFileSync as readFileSync10 } from "fs";
4989
+ function fixtureResponse(url, headers) {
4990
+ try {
4991
+ const raw = readFileSync10(process.env.NTRP_LLM_HTTP_FIXTURE, "utf-8");
4992
+ const entries = JSON.parse(raw);
4993
+ const headerValues = Object.values(headers).join(" ");
4994
+ for (const entry of entries) {
4995
+ if (!url.includes(entry.url_includes)) continue;
4996
+ if (entry.auth_includes && !headerValues.includes(entry.auth_includes)) continue;
4997
+ return { status: entry.status, ok: entry.status >= 200 && entry.status < 300, body: entry.body };
4998
+ }
4999
+ } catch {
5000
+ }
5001
+ return { status: 0, ok: false, body: void 0 };
5002
+ }
5003
+ async function llmHttpGetJson(url, headers, timeoutMs = 6e3) {
5004
+ if (process.env.NTRP_LLM_HTTP_FIXTURE) {
5005
+ return fixtureResponse(url, headers);
5006
+ }
5007
+ const controller = new AbortController();
5008
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
5009
+ try {
5010
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
5011
+ let body;
5012
+ try {
5013
+ body = await res.json();
5014
+ } catch {
5015
+ body = void 0;
5016
+ }
5017
+ return { status: res.status, ok: res.ok, body };
5018
+ } catch {
5019
+ return { status: 0, ok: false, body: void 0 };
5020
+ } finally {
5021
+ clearTimeout(timer);
5022
+ }
5023
+ }
5024
+ var init_http = __esm({
5025
+ "src/ai/llm/http.ts"() {
5026
+ "use strict";
5027
+ }
5028
+ });
5029
+
5030
+ // src/ai/llm/ranking.ts
5031
+ function compareModels(a, b) {
5032
+ const createdA = a.created ?? 0;
5033
+ const createdB = b.created ?? 0;
5034
+ if (createdA !== createdB) return createdB - createdA;
5035
+ const versionA = extractVersion(a.id);
5036
+ const versionB = extractVersion(b.id);
5037
+ if (versionA !== versionB) return versionB - versionA;
5038
+ if (a.id.length !== b.id.length) return a.id.length - b.id.length;
5039
+ return a.id.localeCompare(b.id);
5040
+ }
5041
+ function extractVersion(id) {
5042
+ const match = id.match(/(\d+(?:\.\d+)?)/);
5043
+ return match ? Number(match[1]) : 0;
5044
+ }
5045
+ function pickByPatterns(models, patterns) {
5046
+ for (const pattern of patterns) {
5047
+ const matches = models.filter((m) => pattern.test(m.id));
5048
+ if (matches.length > 0) return [...matches].sort(compareModels)[0];
5049
+ }
5050
+ return void 0;
5051
+ }
5052
+ function genericBucket(model) {
5053
+ if (GENERIC_HIGH.test(model.id)) return "high";
5054
+ if (GENERIC_LOW.test(model.id)) return "low";
5055
+ return "medium";
5056
+ }
5057
+ function genericPick(models, tier) {
5058
+ const bucket = models.filter((m) => genericBucket(m) === tier);
5059
+ if (bucket.length > 0) return [...bucket].sort(compareModels)[0];
5060
+ return void 0;
5061
+ }
5062
+ function rankModels(providerId, models) {
5063
+ if (models.length === 0) return null;
5064
+ const preferences = PROVIDER_PREFERENCES[providerId];
5065
+ const picks = {};
5066
+ for (const tier of ["high", "medium", "low"]) {
5067
+ const preferred = preferences ? pickByPatterns(models, preferences[tier]) : void 0;
5068
+ const generic = preferred ?? genericPick(models, tier);
5069
+ if (generic) picks[tier] = generic.id;
5070
+ }
5071
+ const anyModel = [...models].sort(compareModels)[0].id;
5072
+ const high = picks.high ?? picks.medium ?? picks.low ?? anyModel;
5073
+ const medium = picks.medium ?? picks.high ?? picks.low ?? anyModel;
5074
+ const low = picks.low ?? picks.medium ?? picks.high ?? anyModel;
5075
+ return { high, medium, low };
5076
+ }
5077
+ var PROVIDER_PREFERENCES, GENERIC_LOW, GENERIC_HIGH;
5078
+ var init_ranking = __esm({
5079
+ "src/ai/llm/ranking.ts"() {
5080
+ "use strict";
5081
+ PROVIDER_PREFERENCES = {
5082
+ anthropic: {
5083
+ high: [/^claude-opus/i, /^claude-sonnet/i],
5084
+ medium: [/^claude-sonnet/i, /^claude-haiku/i],
5085
+ low: [/^claude-haiku/i, /^claude-sonnet/i]
5086
+ },
5087
+ openai: {
5088
+ high: [/^gpt-5(?!.*(mini|nano|chat))/i, /^gpt-4\.1(?!.*(mini|nano))/i, /^gpt-4o(?!.*mini)/i, /^o3(?!.*mini)/i],
5089
+ medium: [/^gpt-5.*mini/i, /^gpt-4\.1-mini/i, /^gpt-4o-mini/i, /^o4-mini/i],
5090
+ low: [/^gpt-5.*nano/i, /^gpt-4\.1-nano/i, /^gpt-4o-mini/i]
5091
+ },
5092
+ google: {
5093
+ high: [/^gemini-[\d.]+-pro/i, /^gemini-[\d.]+-flash(?!-lite)/i],
5094
+ medium: [/^gemini-[\d.]+-flash(?!-lite|-8b)/i, /^gemini-[\d.]+-pro/i],
5095
+ low: [/^gemini-[\d.]+-flash-lite/i, /flash-8b/i, /^gemini-[\d.]+-flash(?!-lite)/i]
5096
+ },
5097
+ groq: {
5098
+ high: [/llama-3\.3-70b/i, /gpt-oss-120b/i, /70b/i, /deepseek-r1/i],
5099
+ medium: [/llama-3\.1-8b-instant/i, /gpt-oss-20b/i, /llama.*8b/i],
5100
+ low: [/8b-instant/i, /llama.*8b/i, /gemma/i]
5101
+ },
5102
+ deepseek: {
5103
+ high: [/reasoner/i, /chat/i],
5104
+ medium: [/chat/i],
5105
+ low: [/chat/i]
5106
+ },
5107
+ mistral: {
5108
+ high: [/large/i, /medium/i],
5109
+ medium: [/medium/i, /^mistral-small/i],
5110
+ low: [/ministral/i, /small/i, /tiny/i]
5111
+ },
5112
+ xai: {
5113
+ high: [/^grok-\d+(?!.*(mini|fast))/i, /^grok(?!.*(mini|fast))/i],
5114
+ medium: [/^grok.*mini(?!.*fast)/i, /^grok.*fast/i],
5115
+ low: [/^grok.*mini.*fast/i, /^grok.*mini/i]
5116
+ },
5117
+ openrouter: {
5118
+ high: [/^openrouter\/auto$/i, /claude.*opus/i, /^openai\/gpt-5(?!.*(mini|nano))/i, /gemini.*pro/i],
5119
+ medium: [/claude.*sonnet/i, /gpt-5.*mini/i, /gpt-4\.1-mini/i, /gemini.*flash(?!-lite)/i],
5120
+ low: [/claude.*haiku/i, /nano/i, /flash-lite/i, /mini/i]
5121
+ }
5122
+ };
5123
+ GENERIC_LOW = /(mini|nano|lite|tiny|micro|small|haiku|instant|flash|turbo|\b0?\.?5b\b|\b[1-8]b\b)/i;
5124
+ GENERIC_HIGH = /(opus|ultra|large|max\b|\bpro\b|405b|253b|235b|120b|72b|70b|reason|-r1\b|think|deep)/i;
5125
+ }
5126
+ });
5127
+
5128
+ // src/ai/llm/discovery.ts
5129
+ var discovery_exports = {};
5130
+ __export(discovery_exports, {
5131
+ fetchProviderModels: () => fetchProviderModels,
5132
+ filterChatModels: () => filterChatModels,
5133
+ refreshProviderModels: () => refreshProviderModels,
5134
+ refreshStaleProviderCaches: () => refreshStaleProviderCaches,
5135
+ rerankExcluding: () => rerankExcluding,
5136
+ storeDiscoveredModels: () => storeDiscoveredModels
5137
+ });
5138
+ function authHeaders(spec, apiKey) {
5139
+ if (spec.api === "anthropic") {
5140
+ return { "x-api-key": apiKey ?? "", "anthropic-version": "2023-06-01" };
5141
+ }
5142
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
5143
+ }
5144
+ function normalizeItem(spec, item) {
5145
+ let id = item.id ?? "";
5146
+ if (!id) return null;
5147
+ if (id.startsWith("models/")) id = id.slice("models/".length);
5148
+ const model = { id };
5149
+ const display2 = item.display_name ?? item.name;
5150
+ if (display2 && display2 !== id) model.display_name = display2;
5151
+ if (typeof item.created === "number") model.created = item.created;
5152
+ else if (item.created_at) {
5153
+ const parsed = Date.parse(item.created_at);
5154
+ if (!Number.isNaN(parsed)) model.created = Math.floor(parsed / 1e3);
5155
+ }
5156
+ if (typeof item.context_length === "number") model.context_length = item.context_length;
5157
+ if (Array.isArray(item.supported_parameters)) {
5158
+ model.supports_tools = item.supported_parameters.includes("tools");
5159
+ }
5160
+ return model;
5161
+ }
5162
+ async function fetchProviderModels(spec, apiKey, timeoutMs = 6e3) {
5163
+ const headers = authHeaders(spec, apiKey);
5164
+ if (spec.api === "anthropic") {
5165
+ const models2 = [];
5166
+ let url = modelsUrl(spec);
5167
+ for (let page = 0; page < 5 && url; page++) {
5168
+ const res2 = await llmHttpGetJson(url, headers, timeoutMs);
5169
+ if (!res2.ok) return models2.length > 0 ? { ok: true, models: models2 } : { ok: false, status: res2.status };
5170
+ const body2 = res2.body;
5171
+ for (const item of body2?.data ?? []) {
5172
+ const model = normalizeItem(spec, item);
5173
+ if (model) models2.push(model);
5174
+ }
5175
+ url = body2?.has_more && body2.last_id ? `${spec.base_url}/v1/models?limit=100&after_id=${encodeURIComponent(body2.last_id)}` : null;
5176
+ }
5177
+ return { ok: true, models: models2 };
5178
+ }
5179
+ const res = await llmHttpGetJson(modelsUrl(spec), headers, timeoutMs);
5180
+ if (!res.ok) return { ok: false, status: res.status };
5181
+ const body = res.body;
5182
+ const list = Array.isArray(body) ? body : body?.data ?? [];
5183
+ const models = [];
5184
+ for (const item of list) {
5185
+ const model = normalizeItem(spec, item);
5186
+ if (model) models.push(model);
5187
+ }
5188
+ return { ok: true, models };
5189
+ }
5190
+ function filterChatModels(spec, models) {
5191
+ const extra = PROVIDER_EXCLUDE[spec.id];
5192
+ return models.filter((m) => !NON_CHAT.test(m.id) && !(extra && extra.test(m.id)));
5193
+ }
5194
+ function storeDiscoveredModels(providerId, rawModels) {
5195
+ const spec = getProviderSpec(providerId);
5196
+ if (!spec || rawModels.length === 0) return null;
5197
+ const chat = filterChatModels(spec, rawModels);
5198
+ const usable = chat.length > 0 ? chat : rawModels;
5199
+ const stack = rankModels(providerId, usable);
5200
+ if (!stack) return null;
5201
+ const prior = getProviderModels(providerId);
5202
+ const entry = {
5203
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
5204
+ models: usable,
5205
+ tier_stack: stack,
5206
+ ...prior?.quirks ? { quirks: prior.quirks } : {}
5207
+ };
5208
+ setProviderModels(providerId, entry);
5209
+ return entry;
5210
+ }
5211
+ async function refreshProviderModels(providerId, opts = {}) {
5212
+ const spec = getProviderSpec(providerId);
5213
+ if (!spec) return null;
5214
+ if (!opts.force && !isProviderCacheStale(providerId)) {
5215
+ return getProviderModels(providerId) ?? null;
5216
+ }
5217
+ const apiKey = opts.apiKey ?? getProviderApiKey(providerId);
5218
+ if (spec.requires_key && !apiKey) return null;
5219
+ const result = await fetchProviderModels(spec, apiKey);
5220
+ if (!result.ok) return null;
5221
+ return storeDiscoveredModels(providerId, result.models);
5222
+ }
5223
+ function rerankExcluding(providerId, deadModelId) {
5224
+ const prior = getProviderModels(providerId);
5225
+ if (!prior) return null;
5226
+ const survivors = prior.models.filter((m) => m.id !== deadModelId);
5227
+ const stack = rankModels(providerId, survivors);
5228
+ if (!stack) return null;
5229
+ const entry = { ...prior, models: survivors, tier_stack: stack };
5230
+ setProviderModels(providerId, entry);
5231
+ return entry;
5232
+ }
5233
+ async function refreshStaleProviderCaches() {
5234
+ await Promise.allSettled(
5235
+ getAvailableProviders().filter((p) => isProviderCacheStale(p)).map((p) => refreshProviderModels(p))
5236
+ );
5237
+ }
5238
+ var NON_CHAT, PROVIDER_EXCLUDE;
5239
+ var init_discovery = __esm({
5240
+ "src/ai/llm/discovery.ts"() {
5241
+ "use strict";
5242
+ init_llm_config();
5243
+ init_http();
5244
+ init_models_cache();
5245
+ init_providers();
5246
+ init_ranking();
5247
+ 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;
5248
+ PROVIDER_EXCLUDE = {
5249
+ openai: /(chatgpt|-search|deep-research|-pro\b|computer-use|codex-mini|-instruct\b)/i
5250
+ };
5251
+ }
5252
+ });
5253
+
5254
+ // src/ai/llm/heal.ts
5255
+ async function healModelNotFound(opts) {
5256
+ const { provider, tier, deadModel } = opts;
5257
+ const refreshed = await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true });
5258
+ let candidate = refreshed?.tier_stack?.[tier] ?? getCachedTierModel(provider, tier);
5259
+ if (!candidate || candidate === deadModel) {
5260
+ const reranked = rerankExcluding(provider, deadModel);
5261
+ candidate = reranked?.tier_stack?.[tier];
5262
+ }
5263
+ if (!candidate || candidate === deadModel) return null;
5264
+ clearDeadOverride(deadModel, opts.ctx);
5265
+ return {
5266
+ model: candidate,
5267
+ notice: `model ${deadModel} is no longer available \u2014 switched to ${candidate}`
5268
+ };
5269
+ }
5270
+ function clearDeadOverride(deadModel, ctx) {
5271
+ if (getConfigValue("llm-model-override")?.trim() === deadModel) {
5272
+ deleteConfigValue("llm-model-override");
5273
+ }
5274
+ if (ctx?.llm?.modelOverride === deadModel) {
5275
+ ctx.llm.modelOverride = void 0;
5276
+ }
5277
+ }
5278
+ var init_heal = __esm({
5279
+ "src/ai/llm/heal.ts"() {
5280
+ "use strict";
5281
+ init_store();
5282
+ init_discovery();
5283
+ init_models_cache();
5284
+ }
5285
+ });
5286
+
4679
5287
  // src/ai/llm/surfaces.ts
4680
5288
  function tierForSurface(surface, userTier) {
4681
5289
  const spec = SURFACE_SPECS[surface];
@@ -4747,7 +5355,7 @@ function resolveActiveProvider(ctx) {
4747
5355
  if (session && hasProviderKey(session)) return session;
4748
5356
  const cfg = loadLlmConfig();
4749
5357
  if (hasProviderKey(cfg.primary)) return cfg.primary;
4750
- const available = ["anthropic", "openai"].filter(hasProviderKey);
5358
+ const available = getAvailableProviders();
4751
5359
  if (available.length > 0) return available[0];
4752
5360
  return cfg.primary;
4753
5361
  }
@@ -4769,8 +5377,8 @@ function resolveModelForActive(ctx, surface) {
4769
5377
  const provider = resolveActiveProvider(ctx);
4770
5378
  const tier = resolveEffectiveTier(ctx, surface);
4771
5379
  const override = resolveEffectiveModelOverride(ctx);
4772
- const providerOverride = override && getCatalogEntry(override)?.provider === provider ? override : void 0;
4773
- const modelId = resolveModel(provider, tier, providerOverride);
5380
+ const providerOverride = overrideForProvider(override, provider, provider);
5381
+ const modelId = resolveModelSafe(provider, tier, providerOverride);
4774
5382
  return { provider, tier, modelId };
4775
5383
  }
4776
5384
  function resolveProviderOrder(ctx) {
@@ -4781,30 +5389,30 @@ function resolveProviderOrder(ctx) {
4781
5389
  for (const p of cfg.failoverOrder) {
4782
5390
  if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
4783
5391
  }
4784
- for (const p of ["anthropic", "openai"]) {
4785
- if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
5392
+ for (const p of getAvailableProviders()) {
5393
+ if (p !== active && !order.includes(p)) order.push(p);
4786
5394
  }
4787
5395
  return order;
4788
5396
  }
4789
5397
  function formatActiveStack(ctx, surface = "agentic_investigation") {
4790
5398
  const { provider, tier, modelId } = resolveModelForActive(ctx, surface);
4791
- return `${provider} \xB7 ${tier} \xB7 ${modelId}`;
5399
+ return `${provider} \xB7 ${tier} \xB7 ${modelId ?? "no models yet (run /connect)"}`;
4792
5400
  }
4793
5401
  function formatActiveStackShort(ctx, surface = "agentic_investigation") {
4794
5402
  const { provider, tier } = resolveModelForActive(ctx, surface);
4795
5403
  return `${provider} \xB7 ${tier}`;
4796
5404
  }
4797
5405
  function countAvailableEngines() {
4798
- return ["anthropic", "openai"].filter(hasProviderKey).length;
5406
+ return getAvailableProviders().length;
4799
5407
  }
4800
5408
  function availableEngineLabels() {
4801
- return ["anthropic", "openai"].filter(hasProviderKey);
5409
+ return getAvailableProviders();
4802
5410
  }
4803
5411
  function validateModelForProvider(modelId, provider) {
4804
- const entry = getCatalogEntry(modelId);
4805
- if (!entry) return null;
4806
- if (entry.provider !== provider) {
4807
- return `Model ${modelId} belongs to ${entry.provider}. Run /provider ${entry.provider} first.`;
5412
+ const hint = modelProviderHint(modelId);
5413
+ if (!hint) return null;
5414
+ if (hint !== provider) {
5415
+ return `Model ${modelId} belongs to ${hint}. Run /provider ${hint} first.`;
4808
5416
  }
4809
5417
  return null;
4810
5418
  }
@@ -4818,23 +5426,16 @@ var init_session_state = __esm({
4818
5426
  });
4819
5427
 
4820
5428
  // src/ai/llm/resolver.ts
4821
- function getProviderOrder(config, ctx) {
4822
- void config;
4823
- return resolveProviderOrder(ctx);
4824
- }
4825
5429
  function resolveCompletionContext(surface, opts = {}) {
4826
5430
  const activeProvider = resolveActiveProvider(opts.ctx);
4827
5431
  const tier = opts.tier ?? resolveEffectiveTier(opts.ctx, surface);
4828
5432
  const override = opts.modelOverride ?? resolveEffectiveModelOverride(opts.ctx);
4829
5433
  const providerOrder = resolveProviderOrder(opts.ctx);
4830
5434
  const modelByProvider = {};
4831
- for (const provider of providerOrder) {
4832
- const providerOverride = override && getCatalogEntry(override)?.provider === provider ? override : void 0;
4833
- modelByProvider[provider] = resolveModel(provider, tier, providerOverride);
4834
- }
4835
- if (!modelByProvider[activeProvider]) {
4836
- const activeOverride = override && getCatalogEntry(override)?.provider === activeProvider ? override : void 0;
4837
- modelByProvider[activeProvider] = resolveModel(activeProvider, tier, activeOverride);
5435
+ for (const provider of /* @__PURE__ */ new Set([...providerOrder, activeProvider])) {
5436
+ const providerOverride = overrideForProvider(override, provider, activeProvider);
5437
+ const model = resolveModelSafe(provider, tier, providerOverride);
5438
+ if (model) modelByProvider[provider] = model;
4838
5439
  }
4839
5440
  return {
4840
5441
  providerOrder,
@@ -4860,70 +5461,142 @@ var init_resolver = __esm({
4860
5461
 
4861
5462
  // src/ai/llm/failover.ts
4862
5463
  async function completeOnProvider(provider, model, apiKey, req) {
4863
- if (provider === "anthropic") return anthropicComplete(apiKey, model, req);
4864
- return openaiComplete(apiKey, model, req);
5464
+ const spec = getProviderSpec(provider);
5465
+ if (!spec) {
5466
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
5467
+ }
5468
+ if (spec.api === "anthropic") {
5469
+ return anthropicComplete(apiKey ?? "", model, req);
5470
+ }
5471
+ return openaiCompatComplete(provider, spec.base_url, apiKey, model, req);
5472
+ }
5473
+ function usableKey(provider, ctx) {
5474
+ const spec = getProviderSpec(provider);
5475
+ if (!spec) return { ok: false };
5476
+ const apiKey = getApiKeyForProvider(provider, ctx);
5477
+ if (spec.requires_key && !apiKey) return { ok: false };
5478
+ return { ok: true, apiKey };
5479
+ }
5480
+ async function resolveModelWithDiscovery(provider, cfg, opts) {
5481
+ const known = cfg.modelByProvider[provider];
5482
+ if (known) return known;
5483
+ const override = overrideForProvider(opts.modelOverride, provider, cfg.activeProvider);
5484
+ const direct = resolveModelSafe(provider, cfg.tier, override);
5485
+ if (direct) return direct;
5486
+ await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true }).catch(() => null);
5487
+ return resolveModelSafe(provider, cfg.tier, override);
5488
+ }
5489
+ function stripTools(req) {
5490
+ const { tools: _tools, ...rest } = req;
5491
+ return rest;
4865
5492
  }
4866
5493
  async function completeWithFailover(req, opts = {}) {
4867
- const ctx = resolveCompletionContext(req.surface, {
5494
+ const cfg = resolveCompletionContext(req.surface, {
4868
5495
  max_tokens: req.max_tokens,
4869
5496
  tier: opts.tier,
4870
5497
  modelOverride: opts.modelOverride,
4871
5498
  ctx: opts.ctx
4872
5499
  });
4873
- const providers = ctx.providerOrder;
5500
+ const providers = cfg.providerOrder;
4874
5501
  if (providers.length === 0) {
4875
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
5502
+ throw new Error(NO_PROVIDER_MESSAGE);
4876
5503
  }
5504
+ const notices = [];
4877
5505
  let lastError;
4878
5506
  let failoverFrom;
5507
+ const buildMeta = (provider, model, response) => ({
5508
+ provider_used: provider,
5509
+ model_used: model,
5510
+ ...response.token_usage ?? {},
5511
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
5512
+ ...notices.length > 0 ? { notices: [...notices] } : {}
5513
+ });
4879
5514
  for (let i = 0; i < providers.length; i++) {
4880
5515
  const provider = providers[i];
4881
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
4882
- if (!apiKey) continue;
4883
- let model = ctx.modelByProvider[provider] ?? resolveModel(provider, ctx.tier, opts.modelOverride);
5516
+ const key = usableKey(provider, opts.ctx);
5517
+ if (!key.ok) continue;
5518
+ let model = await resolveModelWithDiscovery(provider, cfg, {
5519
+ modelOverride: opts.modelOverride,
5520
+ apiKey: key.apiKey
5521
+ });
5522
+ if (!model) {
5523
+ lastError = new LlmError(
5524
+ "MODEL_NOT_FOUND",
5525
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
5526
+ provider
5527
+ );
5528
+ continue;
5529
+ }
5530
+ let effectiveReq = req;
5531
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
5532
+ effectiveReq = stripTools(req);
5533
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
5534
+ }
4884
5535
  try {
4885
- const response = await completeOnProvider(provider, model, apiKey, req);
4886
- const meta = {
4887
- provider_used: provider,
4888
- model_used: model,
4889
- ...response.token_usage ?? {},
4890
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
4891
- };
5536
+ const response = await completeOnProvider(provider, model, key.apiKey, effectiveReq);
5537
+ const meta = buildMeta(provider, model, response);
4892
5538
  recordLlmUsage(response.token_usage);
4893
5539
  return { response, meta };
4894
5540
  } catch (err) {
4895
- const llmErr = err;
5541
+ let llmErr = err;
4896
5542
  if (llmErr.name !== "LlmError") throw err;
4897
5543
  lastError = llmErr;
4898
- if (llmErr.code === "MODEL_NOT_FOUND") {
4899
- model = resolveModel(provider, ctx.tier);
5544
+ if (llmErr.code === "TOOLS_UNSUPPORTED" && effectiveReq.tools?.length) {
5545
+ markModelNoTools(provider, model);
5546
+ notices.push(`${model} doesn't support tool calling \u2014 retrying without live data tools`);
4900
5547
  try {
4901
- const response = await completeOnProvider(provider, model, apiKey, req);
4902
- const meta = {
4903
- provider_used: provider,
4904
- model_used: model,
4905
- ...response.token_usage ?? {},
4906
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
4907
- };
5548
+ const response = await completeOnProvider(provider, model, key.apiKey, stripTools(effectiveReq));
5549
+ const meta = buildMeta(provider, model, response);
4908
5550
  recordLlmUsage(response.token_usage);
4909
5551
  return { response, meta };
4910
5552
  } catch (retryErr) {
4911
5553
  const retryLlm = retryErr;
4912
- if (retryLlm.name === "LlmError") lastError = retryLlm;
4913
- else throw retryErr;
5554
+ if (retryLlm.name !== "LlmError") throw retryErr;
5555
+ lastError = retryLlm;
5556
+ llmErr = retryLlm;
5557
+ }
5558
+ }
5559
+ if (llmErr.code === "MODEL_NOT_FOUND") {
5560
+ const healed = await healModelNotFound({
5561
+ provider,
5562
+ tier: cfg.tier,
5563
+ deadModel: model,
5564
+ apiKey: key.apiKey,
5565
+ ctx: opts.ctx
5566
+ }).catch(() => null);
5567
+ if (healed) {
5568
+ notices.push(healed.notice);
5569
+ model = healed.model;
5570
+ let retryReq = req;
5571
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
5572
+ retryReq = stripTools(req);
5573
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
5574
+ }
5575
+ try {
5576
+ const response = await completeOnProvider(provider, model, key.apiKey, retryReq);
5577
+ const meta = buildMeta(provider, model, response);
5578
+ recordLlmUsage(response.token_usage);
5579
+ return { response, meta };
5580
+ } catch (retryErr) {
5581
+ const retryLlm = retryErr;
5582
+ if (retryLlm.name !== "LlmError") throw retryErr;
5583
+ lastError = retryLlm;
5584
+ llmErr = retryLlm;
5585
+ }
4914
5586
  }
4915
5587
  }
4916
5588
  if (!isFailoverEligible(llmErr.code)) throw llmErr;
4917
5589
  const next = providers[i + 1];
4918
5590
  if (next) {
4919
5591
  failoverFrom = failoverFrom ?? provider;
5592
+ notices.push(`${provider} unavailable (${llmErr.code.toLowerCase()}) \u2014 trying ${next}`);
4920
5593
  opts.onFailover?.(provider, next, llmErr.code);
4921
5594
  continue;
4922
5595
  }
4923
5596
  throw llmErr;
4924
5597
  }
4925
5598
  }
4926
- throw lastError ?? new Error("No LLM provider available");
5599
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
4927
5600
  }
4928
5601
  async function* streamWithFailover(req, opts = {}) {
4929
5602
  const cfg = resolveCompletionContext(req.surface, {
@@ -4932,23 +5605,47 @@ async function* streamWithFailover(req, opts = {}) {
4932
5605
  modelOverride: opts.modelOverride,
4933
5606
  ctx: opts.ctx
4934
5607
  });
4935
- const providers = getProviderOrder();
5608
+ const providers = cfg.providerOrder;
4936
5609
  if (providers.length === 0) {
4937
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
5610
+ throw new Error(NO_PROVIDER_MESSAGE);
4938
5611
  }
5612
+ const notices = [];
4939
5613
  let lastError;
4940
5614
  let failoverFrom;
5615
+ async function* streamOnProvider(provider, model, apiKey) {
5616
+ const spec = getProviderSpec(provider);
5617
+ if (!spec) {
5618
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
5619
+ }
5620
+ if (spec.api === "anthropic") {
5621
+ yield* anthropicStream(apiKey ?? "", model, req);
5622
+ return;
5623
+ }
5624
+ yield* openaiCompatStream(provider, spec.base_url, apiKey, model, req);
5625
+ }
4941
5626
  for (let i = 0; i < providers.length; i++) {
4942
5627
  const provider = providers[i];
4943
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
4944
- if (!apiKey) continue;
4945
- const model = cfg.modelByProvider[provider] ?? resolveModel(provider, cfg.tier, opts.modelOverride);
4946
- try {
5628
+ const key = usableKey(provider, opts.ctx);
5629
+ if (!key.ok) continue;
5630
+ let model = await resolveModelWithDiscovery(provider, cfg, {
5631
+ modelOverride: opts.modelOverride,
5632
+ apiKey: key.apiKey
5633
+ });
5634
+ if (!model) {
5635
+ lastError = new LlmError(
5636
+ "MODEL_NOT_FOUND",
5637
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
5638
+ provider
5639
+ );
5640
+ continue;
5641
+ }
5642
+ let yieldedAny = false;
5643
+ const attempt = async function* (attemptModel) {
4947
5644
  let fullText = "";
4948
- const streamFn = provider === "anthropic" ? anthropicStream : openaiStream;
4949
- for await (const event of streamFn(apiKey, model, req)) {
5645
+ for await (const event of streamOnProvider(provider, attemptModel, key.apiKey)) {
4950
5646
  if (event.type === "text_delta") {
4951
5647
  fullText += event.text;
5648
+ yieldedAny = true;
4952
5649
  yield event;
4953
5650
  }
4954
5651
  }
@@ -4956,10 +5653,11 @@ async function* streamWithFailover(req, opts = {}) {
4956
5653
  recordLlmUsage({ input_tokens: 0, output_tokens: estimatedOut });
4957
5654
  const meta = {
4958
5655
  provider_used: provider,
4959
- model_used: model,
5656
+ model_used: attemptModel,
4960
5657
  input_tokens: 0,
4961
5658
  output_tokens: estimatedOut,
4962
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
5659
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
5660
+ ...notices.length > 0 ? { notices: [...notices] } : {}
4963
5661
  };
4964
5662
  yield {
4965
5663
  type: "done",
@@ -4971,32 +5669,66 @@ async function* streamWithFailover(req, opts = {}) {
4971
5669
  },
4972
5670
  meta
4973
5671
  };
5672
+ };
5673
+ try {
5674
+ yield* attempt(model);
4974
5675
  return;
4975
5676
  } catch (err) {
4976
5677
  const llmErr = err;
4977
5678
  if (llmErr.name !== "LlmError") throw err;
4978
5679
  lastError = llmErr;
4979
- if (!isFailoverEligible(llmErr.code)) throw llmErr;
5680
+ if (yieldedAny) throw llmErr;
5681
+ if (llmErr.code === "MODEL_NOT_FOUND") {
5682
+ const healed = await healModelNotFound({
5683
+ provider,
5684
+ tier: cfg.tier,
5685
+ deadModel: model,
5686
+ apiKey: key.apiKey,
5687
+ ctx: opts.ctx
5688
+ }).catch(() => null);
5689
+ if (healed) {
5690
+ notices.push(healed.notice);
5691
+ model = healed.model;
5692
+ try {
5693
+ yield* attempt(model);
5694
+ return;
5695
+ } catch (retryErr) {
5696
+ const retryLlm = retryErr;
5697
+ if (retryLlm.name !== "LlmError") throw retryErr;
5698
+ lastError = retryLlm;
5699
+ if (yieldedAny) throw retryLlm;
5700
+ }
5701
+ }
5702
+ }
5703
+ if (!isFailoverEligible(lastError.code)) throw lastError;
4980
5704
  const next = providers[i + 1];
4981
5705
  if (next) {
4982
5706
  failoverFrom = failoverFrom ?? provider;
4983
- opts.onFailover?.(provider, next, llmErr.code);
5707
+ notices.push(`${provider} unavailable (${lastError.code.toLowerCase()}) \u2014 trying ${next}`);
5708
+ opts.onFailover?.(provider, next, lastError.code);
4984
5709
  continue;
4985
5710
  }
4986
- throw llmErr;
5711
+ throw lastError;
4987
5712
  }
4988
5713
  }
4989
- throw lastError ?? new Error("No LLM provider available");
5714
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
4990
5715
  }
5716
+ var NO_PROVIDER_MESSAGE;
4991
5717
  var init_failover = __esm({
4992
5718
  "src/ai/llm/failover.ts"() {
4993
5719
  "use strict";
4994
5720
  init_usage_stats();
4995
5721
  init_anthropic();
4996
- init_openai();
5722
+ init_openai_compat();
4997
5723
  init_catalog();
5724
+ init_discovery();
4998
5725
  init_errors();
5726
+ init_heal();
5727
+ init_models_cache();
5728
+ init_providers();
5729
+ init_types();
4999
5730
  init_resolver();
5731
+ NO_PROVIDER_MESSAGE = "No LLM provider configured. Run /connect and paste any API key (Anthropic, OpenAI, Groq, Gemini, ...).";
5000
5732
  }
5001
5733
  });
5002
5734
 
@@ -5657,6 +6389,235 @@ var init_profile2 = __esm({
5657
6389
  }
5658
6390
  });
5659
6391
 
6392
+ // src/ai/llm/detect.ts
6393
+ function detectProviderByKey(key) {
6394
+ const k = key.trim();
6395
+ const specs = listProviderSpecs();
6396
+ let best;
6397
+ for (const spec of specs) {
6398
+ for (const prefix of spec.key_prefixes) {
6399
+ if (k.startsWith(prefix) && (!best || prefix.length > best.length)) {
6400
+ best = { id: spec.id, length: prefix.length };
6401
+ }
6402
+ }
6403
+ }
6404
+ if (best) return { certain: best.id, candidates: [best.id] };
6405
+ const shared = specs.filter((s) => s.shared_prefixes.some((p) => k.startsWith(p)));
6406
+ if (shared.length > 0) return { candidates: shared.map((s) => s.id) };
6407
+ const noPrefix = specs.filter(
6408
+ (s) => s.requires_key && !s.custom && s.key_prefixes.length === 0 && s.shared_prefixes.length === 0
6409
+ );
6410
+ return { candidates: noPrefix.map((s) => s.id) };
6411
+ }
6412
+ async function probeProviders(key, candidateIds, timeoutMs = 5e3) {
6413
+ const results = await Promise.all(
6414
+ candidateIds.map(async (id) => {
6415
+ const spec = getProviderSpec(id);
6416
+ if (!spec) return { id, ok: false, status: 0 };
6417
+ const result = await fetchProviderModels(spec, key, timeoutMs);
6418
+ if (result.ok) return { id, ok: true, models: result.models };
6419
+ return { id, ok: false, status: result.status };
6420
+ })
6421
+ );
6422
+ const accepted = [];
6423
+ let sawNetworkFailure = false;
6424
+ for (const r of results) {
6425
+ if (r.ok && r.models.length > 0) accepted.push({ provider: r.id, models: r.models });
6426
+ else if (!r.ok && r.status === 0) sawNetworkFailure = true;
6427
+ }
6428
+ return { accepted, sawNetworkFailure };
6429
+ }
6430
+ var init_detect = __esm({
6431
+ "src/ai/llm/detect.ts"() {
6432
+ "use strict";
6433
+ init_discovery();
6434
+ init_providers();
6435
+ }
6436
+ });
6437
+
6438
+ // src/services/connect.ts
6439
+ var connect_exports = {};
6440
+ __export(connect_exports, {
6441
+ ConnectCancelled: () => ConnectCancelled,
6442
+ ConnectError: () => ConnectError,
6443
+ connectCustomEndpoint: () => connectCustomEndpoint,
6444
+ connectKeyless: () => connectKeyless,
6445
+ connectWithKey: () => connectWithKey,
6446
+ describeConnectOutcome: () => describeConnectOutcome
6447
+ });
6448
+ function finishConnect(spec, opts) {
6449
+ const before = getAvailableProviders();
6450
+ if (opts.key) {
6451
+ setConfigValue(spec.key_config_name, opts.key);
6452
+ }
6453
+ const entry = opts.models && opts.models.length > 0 ? storeDiscoveredModels(spec.id, opts.models) : null;
6454
+ const cfg = loadLlmConfig();
6455
+ let becamePrimary = false;
6456
+ if (cfg.primary !== spec.id && (before.length === 0 || !hasProviderKey(cfg.primary))) {
6457
+ setConfigValue("llm-primary", spec.id);
6458
+ becamePrimary = true;
6459
+ }
6460
+ return {
6461
+ provider: spec.id,
6462
+ label: spec.label,
6463
+ modelCount: entry?.models.length ?? 0,
6464
+ ...entry ? { stack: entry.tier_stack } : {},
6465
+ becamePrimary,
6466
+ offline: !!opts.offline
6467
+ };
6468
+ }
6469
+ async function connectWithKey(rawKey, opts = {}) {
6470
+ const key = rawKey.trim();
6471
+ if (!key) throw new ConnectError("Empty key.");
6472
+ if (opts.providerId) {
6473
+ const spec = getProviderSpec(opts.providerId);
6474
+ if (!spec) {
6475
+ throw new ConnectError(
6476
+ `Unknown provider "${opts.providerId}". Use a built-in id or /connect --base-url <url> --id ${opts.providerId} for a custom endpoint.`
6477
+ );
6478
+ }
6479
+ const result = await fetchProviderModels(spec, key);
6480
+ if (result.ok) return finishConnect(spec, { key, models: result.models });
6481
+ if (result.status === 0) {
6482
+ return finishConnect(spec, { key, offline: true });
6483
+ }
6484
+ throw new ConnectError(`${spec.label} rejected this key (HTTP ${result.status}) \u2014 double-check it and try again.`);
6485
+ }
6486
+ const detection = detectProviderByKey(key);
6487
+ if (detection.certain) {
6488
+ const spec = getProviderSpec(detection.certain);
6489
+ const result = await fetchProviderModels(spec, key);
6490
+ if (result.ok) return finishConnect(spec, { key, models: result.models });
6491
+ if (result.status === 0) return finishConnect(spec, { key, offline: true });
6492
+ throw new ConnectError(`${spec.label} rejected this key (HTTP ${result.status}) \u2014 double-check it and try again.`);
6493
+ }
6494
+ const report = await probeProviders(key, detection.candidates);
6495
+ if (report.accepted.length === 1) {
6496
+ const match = report.accepted[0];
6497
+ const spec = getProviderSpec(match.provider);
6498
+ if (opts.callbacks?.confirmDetection) {
6499
+ const ok = await opts.callbacks.confirmDetection(match.provider);
6500
+ if (!ok) throw new ConnectCancelled();
6501
+ }
6502
+ return finishConnect(spec, { key, models: match.models });
6503
+ }
6504
+ if (report.accepted.length > 1) {
6505
+ if (opts.callbacks?.chooseProvider) {
6506
+ const chosen = await opts.callbacks.chooseProvider(report.accepted);
6507
+ if (!chosen) throw new ConnectCancelled();
6508
+ const match = report.accepted.find((a) => a.provider === chosen);
6509
+ return finishConnect(getProviderSpec(chosen), { key, models: match.models });
6510
+ }
6511
+ throw new ConnectError(
6512
+ `Multiple providers accepted this key (${report.accepted.map((a) => a.provider).join(", ")}). Re-run with --provider <id>.`
6513
+ );
6514
+ }
6515
+ if (report.sawNetworkFailure) {
6516
+ throw new ConnectError(
6517
+ `Couldn't reach ${detection.candidates.map(providerLabel).join(" / ")} to identify this key. Check your connection, or force a provider with --provider <id>.`
6518
+ );
6519
+ }
6520
+ throw new ConnectError(
6521
+ `No provider accepted this key (tried ${detection.candidates.map(providerLabel).join(", ")}). If it belongs to an OpenAI-compatible endpoint, run /connect --base-url <url>.`
6522
+ );
6523
+ }
6524
+ async function connectCustomEndpoint(opts) {
6525
+ const id = opts.id.trim().toLowerCase();
6526
+ if (!/^[a-z][a-z0-9_-]*$/.test(id)) {
6527
+ throw new ConnectError(`Invalid provider id "${opts.id}" \u2014 use letters, digits, dashes.`);
6528
+ }
6529
+ const baseUrl = opts.baseUrl.trim().replace(/\/+$/, "");
6530
+ if (!/^https?:\/\//.test(baseUrl)) {
6531
+ throw new ConnectError(`Base URL must start with http:// or https:// (got "${opts.baseUrl}").`);
6532
+ }
6533
+ const builtin = getProviderSpec(id);
6534
+ const spec = builtin ? { ...builtin, base_url: baseUrl } : {
6535
+ id,
6536
+ label: opts.label ?? id,
6537
+ api: "openai-compat",
6538
+ base_url: baseUrl,
6539
+ key_prefixes: [],
6540
+ shared_prefixes: [],
6541
+ key_config_name: `${id}-api-key`,
6542
+ requires_key: !!opts.key,
6543
+ custom: true
6544
+ };
6545
+ const result = await fetchProviderModels(spec, opts.key);
6546
+ if (!result.ok) {
6547
+ if (result.status === 0) {
6548
+ throw new ConnectError(`Couldn't reach ${baseUrl} \u2014 check the URL (expects an OpenAI-compatible /models endpoint).`);
6549
+ }
6550
+ if (result.status === 401 || result.status === 403) {
6551
+ throw new ConnectError(
6552
+ 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.`
6553
+ );
6554
+ }
6555
+ throw new ConnectError(`${baseUrl} answered HTTP ${result.status} \u2014 is this an OpenAI-compatible endpoint?`);
6556
+ }
6557
+ if (result.models.length === 0) {
6558
+ throw new ConnectError(`${baseUrl} lists no models \u2014 nothing to connect.`);
6559
+ }
6560
+ saveCustomProvider({
6561
+ id,
6562
+ ...opts.label ? { label: opts.label } : {},
6563
+ base_url: baseUrl,
6564
+ requires_key: !!opts.key,
6565
+ enabled: true
6566
+ });
6567
+ return finishConnect(getProviderSpec(id), { key: opts.key, models: result.models });
6568
+ }
6569
+ async function connectKeyless(providerId, baseUrl) {
6570
+ const builtin = getProviderSpec(providerId);
6571
+ if (!builtin) throw new ConnectError(`Unknown provider "${providerId}".`);
6572
+ const spec = baseUrl ? { ...builtin, base_url: baseUrl.replace(/\/+$/, "") } : builtin;
6573
+ const result = await fetchProviderModels(spec, void 0);
6574
+ if (!result.ok) {
6575
+ throw new ConnectError(
6576
+ `${spec.label} not reachable at ${spec.base_url}. Is it running? (ollama serve, then retry)`
6577
+ );
6578
+ }
6579
+ if (result.models.length === 0) {
6580
+ throw new ConnectError(`${spec.label} is running but has no models \u2014 pull one first (e.g. \`ollama pull llama3.2\`).`);
6581
+ }
6582
+ saveCustomProvider({ id: spec.id, base_url: spec.base_url, enabled: true });
6583
+ return finishConnect(getProviderSpec(spec.id), { models: result.models });
6584
+ }
6585
+ function describeConnectOutcome(outcome) {
6586
+ const lines = [];
6587
+ if (outcome.offline) {
6588
+ lines.push(`${outcome.label} key saved \u2014 provider unreachable right now, models will be discovered on first use.`);
6589
+ } else {
6590
+ lines.push(`Connected ${outcome.label} \u2014 ${outcome.modelCount} chat model${outcome.modelCount === 1 ? "" : "s"} available.`);
6591
+ }
6592
+ if (outcome.stack) {
6593
+ lines.push(`high ${outcome.stack.high}`);
6594
+ lines.push(`medium ${outcome.stack.medium}`);
6595
+ lines.push(`low ${outcome.stack.low}`);
6596
+ }
6597
+ if (outcome.becamePrimary) {
6598
+ lines.push(`Primary engine: ${outcome.provider}`);
6599
+ }
6600
+ return lines;
6601
+ }
6602
+ var ConnectError, ConnectCancelled;
6603
+ var init_connect = __esm({
6604
+ "src/services/connect.ts"() {
6605
+ "use strict";
6606
+ init_detect();
6607
+ init_discovery();
6608
+ init_providers();
6609
+ init_llm_config();
6610
+ init_store();
6611
+ ConnectError = class extends Error {
6612
+ };
6613
+ ConnectCancelled = class extends ConnectError {
6614
+ constructor() {
6615
+ super("Connect cancelled.");
6616
+ }
6617
+ };
6618
+ }
6619
+ });
6620
+
5660
6621
  // src/commands/onboard.ts
5661
6622
  var onboard_exports = {};
5662
6623
  __export(onboard_exports, {
@@ -5984,42 +6945,58 @@ function printIntro() {
5984
6945
  }
5985
6946
  async function ensureLlmKeys(session) {
5986
6947
  if (hasAnyLlmProvider()) return;
6948
+ const { connectWithKey: connectWithKey2, describeConnectOutcome: describeConnectOutcome2, ConnectCancelled: ConnectCancelled2 } = await Promise.resolve().then(() => (init_connect(), connect_exports));
6949
+ const { providerLabel: providerLabel2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
6950
+ const { countAvailableEngines: countAvailableEngines2 } = await Promise.resolve().then(() => (init_session_state(), session_state_exports));
5987
6951
  console.log();
5988
6952
  console.log(" " + chalk8.dim("Onboarding uses AI to draft your profile."));
5989
6953
  console.log(
5990
- " " + chalk8.dim("Or run ") + paint("accent", "/config set api-key") + chalk8.dim(" / ") + paint("accent", "/config set openai-api-key") + chalk8.dim(" anytime.")
6954
+ " " + chalk8.dim("Paste any provider's API key \u2014 Anthropic, OpenAI, Groq, Gemini, Mistral, ...")
5991
6955
  );
5992
- const first = await session.choose("Add your first LLM engine", [
5993
- { value: "anthropic", label: "Anthropic (Claude)", description: "Strong tool-using investigation" },
5994
- { value: "openai", label: "OpenAI (GPT)", description: "Full parity on all surfaces" }
5995
- ]);
5996
- const firstLabel = first === "anthropic" ? "Anthropic API key" : "OpenAI API key";
5997
- const firstKey = await session.askSecret(firstLabel, { confirm: true });
5998
- if (first === "anthropic") setConfigValue("api-key", firstKey);
5999
- else setConfigValue("openai-api-key", firstKey);
6000
- setConfigValue("llm-primary", first);
6001
- setConfigValue("llm-tier", "high");
6002
- const second = first === "anthropic" ? "openai" : "anthropic";
6003
- setConfigValue("llm-failover-order", second);
6004
- setConfigValue("llm-auto-failover", "off");
6005
- const addSecond = await session.confirm(
6006
- `Add a second engine (${second}) for switching in the REPL?`,
6007
- false
6956
+ console.log(
6957
+ " " + chalk8.dim("NTRP detects the provider and discovers its models. Or run ") + paint("accent", "/connect") + chalk8.dim(" anytime.")
6008
6958
  );
6009
- if (addSecond) {
6010
- const secondLabel = second === "anthropic" ? "Anthropic API key" : "OpenAI API key";
6011
- const secondKey = await session.askSecret(secondLabel, { confirm: true });
6012
- if (second === "anthropic") setConfigValue("api-key", secondKey);
6013
- else setConfigValue("openai-api-key", secondKey);
6959
+ for (; ; ) {
6960
+ const key = await session.askSecret("LLM API key (any provider)", { confirm: false });
6961
+ const spinner = ora({ text: "Identifying provider\u2026", discardStdin: false }).start();
6962
+ try {
6963
+ const outcome = await connectWithKey2(key, {
6964
+ callbacks: {
6965
+ confirmDetection: async (providerId) => {
6966
+ spinner.stop();
6967
+ return session.confirm(`Detected ${providerLabel2(providerId)} \u2014 connect it?`, true);
6968
+ },
6969
+ chooseProvider: async (accepted) => {
6970
+ spinner.stop();
6971
+ return session.choose(
6972
+ "Multiple providers accepted this key \u2014 which is it?",
6973
+ accepted.map((a) => ({ value: a.provider, label: providerLabel2(a.provider) }))
6974
+ );
6975
+ }
6976
+ }
6977
+ });
6978
+ spinner.stop();
6979
+ const [headline, ...rest] = describeConnectOutcome2(outcome);
6980
+ console.log(" " + paint("success", "\u2713") + " " + (headline ?? ""));
6981
+ for (const line of rest) console.log(" " + chalk8.dim(line));
6982
+ } catch (err) {
6983
+ spinner.stop();
6984
+ if (!(err instanceof ConnectCancelled2)) {
6985
+ console.log(" " + chalk8.red(String(err.message ?? err)));
6986
+ }
6987
+ const retry = await session.confirm("Try another key?", true);
6988
+ if (retry) continue;
6989
+ if (!hasAnyLlmProvider()) return;
6990
+ }
6991
+ const addAnother = await session.confirm("Add another engine? (switch anytime with /provider)", false);
6992
+ if (!addAnother) break;
6993
+ }
6994
+ if (countAvailableEngines2() >= 2) {
6014
6995
  const enableFailover = await session.confirm(
6015
6996
  "Enable auto-failover on rate limits? (off = you choose engine with /provider)",
6016
6997
  false
6017
6998
  );
6018
- if (enableFailover) setConfigValue("llm-auto-failover", "on");
6019
- } else {
6020
- console.log(
6021
- " " + chalk8.dim(`Single engine \u2014 add ${second} later via /config set ${second === "anthropic" ? "api-key" : "openai-api-key"}.`)
6022
- );
6999
+ setConfigValue("llm-auto-failover", enableFailover ? "on" : "off");
6023
7000
  }
6024
7001
  console.log(" " + paint("success", "\u2713") + " " + chalk8.dim("LLM engines configured. Use /provider to switch."));
6025
7002
  }
@@ -6318,16 +7295,16 @@ async function runGlobalAdminCommand(command, line, ctx) {
6318
7295
  const args = tokens.slice(1);
6319
7296
  switch (command) {
6320
7297
  case "scratch": {
6321
- const { handler: handler45 } = await Promise.resolve().then(() => (init_scratch(), scratch_exports));
6322
- return handler45(args, ctx);
7298
+ const { handler: handler46 } = await Promise.resolve().then(() => (init_scratch(), scratch_exports));
7299
+ return handler46(args, ctx);
6323
7300
  }
6324
7301
  case "cleanup": {
6325
- const { handler: handler45 } = await Promise.resolve().then(() => (init_cleanup(), cleanup_exports));
6326
- return handler45(args, ctx);
7302
+ const { handler: handler46 } = await Promise.resolve().then(() => (init_cleanup(), cleanup_exports));
7303
+ return handler46(args, ctx);
6327
7304
  }
6328
7305
  case "deactivate-demo": {
6329
- const { handler: handler45 } = await Promise.resolve().then(() => (init_deactivate_demo(), deactivate_demo_exports));
6330
- return handler45(args, ctx);
7306
+ const { handler: handler46 } = await Promise.resolve().then(() => (init_deactivate_demo(), deactivate_demo_exports));
7307
+ return handler46(args, ctx);
6331
7308
  }
6332
7309
  default:
6333
7310
  return void 0;
@@ -9218,6 +10195,9 @@ function formatLlmAttribution(meta) {
9218
10195
  return line;
9219
10196
  }
9220
10197
  function printLlmAttribution(meta) {
10198
+ for (const notice of meta.notices ?? []) {
10199
+ console.log(chalk13.dim(` ${notice}`));
10200
+ }
9221
10201
  const line = formatLlmAttribution(meta);
9222
10202
  if (line) console.log(chalk13.dim(` ${line}`));
9223
10203
  }
@@ -9535,6 +10515,7 @@ async function renderDiagnoseStream(options) {
9535
10515
  let modelUsed = "";
9536
10516
  let providerUsed;
9537
10517
  let failover;
10518
+ let notices;
9538
10519
  let rawPrompt = "";
9539
10520
  try {
9540
10521
  for await (const event of runFindings(fullResult)) {
@@ -9550,6 +10531,7 @@ async function renderDiagnoseStream(options) {
9550
10531
  modelUsed = event.model_used;
9551
10532
  providerUsed = event.provider_used;
9552
10533
  failover = event.failover;
10534
+ notices = event.usage?.notices;
9553
10535
  rawPrompt = event.raw_prompt;
9554
10536
  }
9555
10537
  }
@@ -9573,7 +10555,8 @@ async function renderDiagnoseStream(options) {
9573
10555
  printLlmAttribution({
9574
10556
  model_used: modelUsed,
9575
10557
  provider_used: providerUsed,
9576
- failover
10558
+ failover,
10559
+ notices
9577
10560
  });
9578
10561
  } catch (err) {
9579
10562
  findingsSpinner.fail(deep ? "Agentic investigation failed" : "AI findings failed");
@@ -9953,8 +10936,8 @@ function markFailure(ctx) {
9953
10936
  }
9954
10937
  async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
9955
10938
  if (!forceRegen) {
9956
- const cached = loadCachedTaxonomy(profile);
9957
- if (cached) return cached;
10939
+ const cached2 = loadCachedTaxonomy(profile);
10940
+ if (cached2) return cached2;
9958
10941
  }
9959
10942
  const spinnerText = forceRegen ? "Rebuilding market taxonomy\u2026" : "Researching your market taxonomy\u2026";
9960
10943
  const spinner = ora3({ text: spinnerText, discardStdin: false }).start();
@@ -10112,7 +11095,7 @@ __export(ingest_exports, {
10112
11095
  });
10113
11096
  import chalk16 from "chalk";
10114
11097
  import ora4 from "ora";
10115
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
11098
+ import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
10116
11099
  import { basename as basename3 } from "path";
10117
11100
  async function handler7(args, ctx) {
10118
11101
  const { positional, flags } = parseArgs2(args, [
@@ -10136,7 +11119,7 @@ async function handler7(args, ctx) {
10136
11119
  console.error(chalk16.dim(" /ingest --demo [--scenario <name>]"));
10137
11120
  process.exit(1);
10138
11121
  }
10139
- if (!existsSync10(file)) {
11122
+ if (!existsSync12(file)) {
10140
11123
  console.error(chalk16.red(` File not found: ${file}`));
10141
11124
  process.exit(1);
10142
11125
  }
@@ -10154,7 +11137,7 @@ async function handler7(args, ctx) {
10154
11137
  try {
10155
11138
  await initSchema();
10156
11139
  spinner.text = "Parsing CSV...";
10157
- const content = readFileSync8(file, "utf-8");
11140
+ const content = readFileSync11(file, "utf-8");
10158
11141
  const { rows, headers } = parseCSV(content);
10159
11142
  if (rows.length === 0) {
10160
11143
  spinner.fail("CSV is empty");
@@ -12317,16 +13300,16 @@ var init_compute = __esm({
12317
13300
  });
12318
13301
 
12319
13302
  // src/data/playbook.ts
12320
- import { existsSync as existsSync11, readFileSync as readFileSync9, appendFileSync } from "fs";
12321
- import { join as join10 } from "path";
13303
+ import { existsSync as existsSync13, readFileSync as readFileSync12, appendFileSync } from "fs";
13304
+ import { join as join12 } from "path";
12322
13305
  function playsPath() {
12323
- return join10(getMemoryDir(), PLAYS_FILE);
13306
+ return join12(getMemoryDir(), PLAYS_FILE);
12324
13307
  }
12325
13308
  function getCustomPlays() {
12326
13309
  const path = playsPath();
12327
- if (!existsSync11(path)) return [];
13310
+ if (!existsSync13(path)) return [];
12328
13311
  const out = [];
12329
- for (const line of readFileSync9(path, "utf-8").split("\n")) {
13312
+ for (const line of readFileSync12(path, "utf-8").split("\n")) {
12330
13313
  const trimmed = line.trim();
12331
13314
  if (!trimmed) continue;
12332
13315
  try {
@@ -12918,7 +13901,7 @@ async function runMetricsAnalysis(options = {}) {
12918
13901
  if (options.findings) {
12919
13902
  if (!canUseReplAi(options.ctx)) {
12920
13903
  throw new Error(
12921
- "AI metrics findings require stored API keys. Run `ntrp`, set api-key and/or openai-api-key, then /metrics --findings."
13904
+ "AI metrics findings require stored API keys. Run `ntrp`, then /connect (any provider key), then /metrics --findings."
12922
13905
  );
12923
13906
  }
12924
13907
  options.onProgress?.("findings");
@@ -13327,7 +14310,8 @@ async function* streamFindings(input, ctx) {
13327
14310
  findings,
13328
14311
  model_used: meta.model_used,
13329
14312
  provider_used: meta.provider_used,
13330
- raw_prompt: userMessage
14313
+ raw_prompt: userMessage,
14314
+ usage: meta
13331
14315
  };
13332
14316
  }
13333
14317
  }
@@ -13628,9 +14612,9 @@ var init_tool_schemas = __esm({
13628
14612
  });
13629
14613
 
13630
14614
  // src/ai/privacy.ts
13631
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
14615
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
13632
14616
  import { homedir as homedir4 } from "os";
13633
- import { join as join11 } from "path";
14617
+ import { join as join13 } from "path";
13634
14618
  function stripPII(obj) {
13635
14619
  if (obj === null || obj === void 0) return obj;
13636
14620
  if (typeof obj !== "object") return obj;
@@ -13645,14 +14629,14 @@ function stripPII(obj) {
13645
14629
  return out;
13646
14630
  }
13647
14631
  function ensureAuditDir() {
13648
- if (!existsSync12(AUDIT_DIR)) {
14632
+ if (!existsSync14(AUDIT_DIR)) {
13649
14633
  mkdirSync8(AUDIT_DIR, { recursive: true });
13650
14634
  }
13651
14635
  }
13652
14636
  function logToolCall(entry) {
13653
14637
  ensureAuditDir();
13654
14638
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
13655
- const path = join11(AUDIT_DIR, `agentic-${date}.jsonl`);
14639
+ const path = join13(AUDIT_DIR, `agentic-${date}.jsonl`);
13656
14640
  appendFileSync2(path, JSON.stringify(entry) + "\n");
13657
14641
  }
13658
14642
  var PII_FIELDS, AUDIT_DIR;
@@ -13676,7 +14660,7 @@ var init_privacy = __esm({
13676
14660
  "raw_data",
13677
14661
  "metadata"
13678
14662
  ]);
13679
- AUDIT_DIR = join11(homedir4(), ".ntrp", "audit");
14663
+ AUDIT_DIR = join13(homedir4(), ".ntrp", "audit");
13680
14664
  }
13681
14665
  });
13682
14666
 
@@ -14182,7 +15166,7 @@ __export(ingest_chat_exports, {
14182
15166
  loadDemoFromChat: () => loadDemoFromChat,
14183
15167
  looksLikeFilePath: () => looksLikeFilePath
14184
15168
  });
14185
- import { existsSync as existsSync13 } from "fs";
15169
+ import { existsSync as existsSync15 } from "fs";
14186
15170
  import { basename as basename4, resolve as resolve4 } from "path";
14187
15171
  import { homedir as homedir5 } from "os";
14188
15172
  import chalk22 from "chalk";
@@ -14202,11 +15186,11 @@ function extractFilePath(input) {
14202
15186
  const m = trimmed.match(re);
14203
15187
  if (m?.[1]) {
14204
15188
  const p = expandPath(m[1]);
14205
- if (existsSync13(p)) return p;
15189
+ if (existsSync15(p)) return p;
14206
15190
  }
14207
15191
  if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
14208
15192
  const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
14209
- if (existsSync13(p)) return p;
15193
+ if (existsSync15(p)) return p;
14210
15194
  }
14211
15195
  }
14212
15196
  return null;
@@ -14236,12 +15220,12 @@ async function ingestFromChat(ctx, filePath) {
14236
15220
  }
14237
15221
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
14238
15222
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
14239
- const { readFileSync: readFileSync16 } = await import("fs");
15223
+ const { readFileSync: readFileSync19 } = await import("fs");
14240
15224
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
14241
15225
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
14242
15226
  let headerCheckFailed = false;
14243
15227
  try {
14244
- const raw = readFileSync16(filePath, "utf-8");
15228
+ const raw = readFileSync19(filePath, "utf-8");
14245
15229
  const { headers } = parseCSV2(raw);
14246
15230
  const detected = detectEntityType2(headers, "unknown");
14247
15231
  if (!detected) headerCheckFailed = true;
@@ -14989,12 +15973,12 @@ async function handleDraftHandoff(input) {
14989
15973
  };
14990
15974
  }
14991
15975
  async function executeToolCall(name, input, ctx) {
14992
- const handler45 = HANDLERS[name];
14993
- if (!handler45) {
15976
+ const handler46 = HANDLERS[name];
15977
+ if (!handler46) {
14994
15978
  return JSON.stringify({ error: `Unknown tool '${name}'` });
14995
15979
  }
14996
15980
  const start = Date.now();
14997
- const rawResult = await handler45(input, ctx);
15981
+ const rawResult = await handler46(input, ctx);
14998
15982
  const safeResult = stripPII(rawResult);
14999
15983
  const resultJson = JSON.stringify(safeResult);
15000
15984
  const duration = Date.now() - start;
@@ -15387,7 +16371,7 @@ async function runDiagnosis(options = {}) {
15387
16371
  if (options.findings) {
15388
16372
  if (!canUseReplAi(options.ctx)) {
15389
16373
  throw new Error(
15390
- "AI findings require stored API keys. Run `ntrp`, then /config set api-key and/or /config set openai-api-key, and use /diagnose --findings."
16374
+ "AI findings require stored API keys. Run `ntrp`, then /connect (any provider key), and use /diagnose --findings."
15391
16375
  );
15392
16376
  }
15393
16377
  if (options.deep) {
@@ -15530,7 +16514,7 @@ async function handler8(args, ctx) {
15530
16514
  console.log();
15531
16515
  console.log(" " + chalk23.red("AI findings run only in the interactive REPL."));
15532
16516
  console.log(" " + chalk23.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
15533
- console.log(" " + chalk23.dim("Start with ") + paint("accent", "ntrp") + chalk23.dim(", set ") + paint("accent", "/config set api-key") + chalk23.dim(" or openai-api-key, then /diagnose --findings."));
16517
+ console.log(" " + chalk23.dim("Start with ") + paint("accent", "ntrp") + chalk23.dim(", run ") + paint("accent", "/connect") + chalk23.dim(" (any provider key), then /diagnose --findings."));
15534
16518
  console.log();
15535
16519
  return;
15536
16520
  }
@@ -15710,7 +16694,7 @@ __export(new_exports, {
15710
16694
  handler: () => handler9
15711
16695
  });
15712
16696
  import chalk24 from "chalk";
15713
- import { existsSync as existsSync14 } from "fs";
16697
+ import { existsSync as existsSync16 } from "fs";
15714
16698
  import { basename as basename5 } from "path";
15715
16699
  async function handler9(args, ctx) {
15716
16700
  const { positional, flags } = parseArgs2(args, ["demo", "empty", "list-scenarios", "regen-taxonomy"]);
@@ -15732,7 +16716,7 @@ async function handler9(args, ctx) {
15732
16716
  console.error(chalk24.red(" Usage: /new <file.csv> | --demo [--scenario <name>] | --empty [--lens health|metrics]"));
15733
16717
  return;
15734
16718
  }
15735
- if (source.kind === "file" && !existsSync14(source.path)) {
16719
+ if (source.kind === "file" && !existsSync16(source.path)) {
15736
16720
  console.error(chalk24.red(` File not found: ${source.path}`));
15737
16721
  return;
15738
16722
  }
@@ -15799,11 +16783,11 @@ async function handler9(args, ctx) {
15799
16783
  return "New empty session";
15800
16784
  }
15801
16785
  if (lens === "revenue_metrics") {
15802
- const ora17 = (await import("ora")).default;
16786
+ const ora20 = (await import("ora")).default;
15803
16787
  const { runMetricsAnalysis: runMetricsAnalysis2 } = await Promise.resolve().then(() => (init_metrics_analysis(), metrics_analysis_exports));
15804
16788
  const { renderMetricsReport: renderMetricsReport2 } = await Promise.resolve().then(() => (init_metrics_report(), metrics_report_exports));
15805
16789
  const structured = isStructuredOutput(ctx.execution);
15806
- const spinner = structured ? null : ora17({ text: "Computing SaaS metrics\u2026", indent: 2, discardStdin: false }).start();
16790
+ const spinner = structured ? null : ora20({ text: "Computing SaaS metrics\u2026", indent: 2, discardStdin: false }).start();
15807
16791
  let result;
15808
16792
  try {
15809
16793
  result = await runMetricsAnalysis2({
@@ -16009,7 +16993,7 @@ __export(session_exports, {
16009
16993
  handler: () => handler11
16010
16994
  });
16011
16995
  import chalk26 from "chalk";
16012
- import { join as join12 } from "path";
16996
+ import { join as join14 } from "path";
16013
16997
  import ora7 from "ora";
16014
16998
  async function handler11(args, ctx) {
16015
16999
  const sub = args[0];
@@ -16098,7 +17082,7 @@ async function pickUp(idArg, ctx) {
16098
17082
  }
16099
17083
  resetContextForSwitch(ctx, {
16100
17084
  sessionId: target.id,
16101
- sessionFile: join12(getSessionsDir(), `${target.id}.json`),
17085
+ sessionFile: join14(getSessionsDir(), `${target.id}.json`),
16102
17086
  sessionName: session.name,
16103
17087
  messages: [...session.messages],
16104
17088
  conversation: session.thread ? [...session.thread] : [],
@@ -16410,7 +17394,7 @@ __export(report_exports, {
16410
17394
  handler: () => handler12
16411
17395
  });
16412
17396
  import chalk27 from "chalk";
16413
- import { writeFileSync as writeFileSync8 } from "fs";
17397
+ import { writeFileSync as writeFileSync10 } from "fs";
16414
17398
  import { dirname as dirname2 } from "path";
16415
17399
  async function handler12(args, ctx) {
16416
17400
  const { flags } = parseArgs2(args);
@@ -16506,7 +17490,7 @@ async function handler12(args, ctx) {
16506
17490
  if (!isInsideNtrp(resolvedOutput)) {
16507
17491
  console.warn(chalk27.yellow(` Warning: writing report outside ~/.ntrp (${dirname2(resolvedOutput)})`));
16508
17492
  }
16509
- writeFileSync8(resolvedOutput, rendered);
17493
+ writeFileSync10(resolvedOutput, rendered);
16510
17494
  console.log(chalk27.green(` Report written to ${resolvedOutput}`));
16511
17495
  } else if (rendered) {
16512
17496
  console.log(rendered);
@@ -16537,8 +17521,8 @@ var init_report2 = __esm({
16537
17521
  });
16538
17522
 
16539
17523
  // src/output/notes-export.ts
16540
- import { writeFileSync as writeFileSync9 } from "fs";
16541
- import { join as join13 } from "path";
17524
+ import { writeFileSync as writeFileSync11 } from "fs";
17525
+ import { join as join15 } from "path";
16542
17526
  function exportToNotes(data) {
16543
17527
  const { computeResult, divergences, findings, exchanges } = data;
16544
17528
  const { aggregate, segments } = computeResult;
@@ -16547,7 +17531,7 @@ function exportToNotes(data) {
16547
17531
  const timeStr = formatTime(now2);
16548
17532
  const filename = `${dateStr}-${timeStr}-gtm-health.md`;
16549
17533
  const dir = getExportsDir();
16550
- const filepath = join13(dir, filename);
17534
+ const filepath = join15(dir, filename);
16551
17535
  const severityTags = /* @__PURE__ */ new Set();
16552
17536
  for (const f of findings) severityTags.add(f.severity);
16553
17537
  const tags = ["ntrp", "gtm-health", ...severityTags];
@@ -16636,7 +17620,7 @@ function exportToNotes(data) {
16636
17620
  }
16637
17621
  }
16638
17622
  const content = frontmatter.join("\n") + "\n\n" + body.join("\n") + "\n";
16639
- writeFileSync9(filepath, content);
17623
+ writeFileSync11(filepath, content);
16640
17624
  return filepath;
16641
17625
  }
16642
17626
  function formatDate(d) {
@@ -16776,8 +17760,8 @@ __export(backmeup_exports, {
16776
17760
  });
16777
17761
  import chalk29 from "chalk";
16778
17762
  import Papa5 from "papaparse";
16779
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
16780
- import { join as join14 } from "path";
17763
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
17764
+ import { join as join16 } from "path";
16781
17765
  function sanitizeCsvValue(value) {
16782
17766
  if (typeof value !== "string") return value;
16783
17767
  return CSV_FORMULA_RE.test(value) ? `'${value}` : value;
@@ -16813,7 +17797,7 @@ async function handler14(args, _ctx) {
16813
17797
  if (!isInsideNtrp(baseDir)) {
16814
17798
  console.warn(chalk29.yellow(` Warning: writing backup outside ~/.ntrp (${baseDir})`));
16815
17799
  }
16816
- const folder = join14(baseDir, folderName);
17800
+ const folder = join16(baseDir, folderName);
16817
17801
  mkdirSync9(folder, { recursive: true });
16818
17802
  const generatedAt = now2.toISOString();
16819
17803
  let fileCount = 0;
@@ -16828,7 +17812,7 @@ async function handler14(args, _ctx) {
16828
17812
  "Total At Risk": health.total_value_at_risk != null ? formatCurrency(health.total_value_at_risk) : "N/A",
16829
17813
  "Generated At": generatedAt
16830
17814
  }));
16831
- writeFileSync10(join14(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
17815
+ writeFileSync12(join16(folder, "cover-sheet.csv"), Papa5.unparse(sanitizeCsvRows(coverRows)), "utf-8");
16832
17816
  fileCount++;
16833
17817
  if (findings.length > 0) {
16834
17818
  const findingsRows = findings.map((f) => ({
@@ -16838,7 +17822,7 @@ async function handler14(args, _ctx) {
16838
17822
  Finding: f.finding,
16839
17823
  "Recommended Plays": f.recommended_plays ? f.recommended_plays.map((p) => p.play_name).join("; ") : ""
16840
17824
  }));
16841
- writeFileSync10(join14(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
17825
+ writeFileSync12(join16(folder, "findings.csv"), Papa5.unparse(sanitizeCsvRows(findingsRows)), "utf-8");
16842
17826
  fileCount++;
16843
17827
  }
16844
17828
  for (const vs of health.vital_signs) {
@@ -16848,7 +17832,7 @@ async function handler14(args, _ctx) {
16848
17832
  ...detail
16849
17833
  }));
16850
17834
  const filename = EVIDENCE_FILENAMES[vs.vital_sign] ?? `${vs.vital_sign}.csv`;
16851
- writeFileSync10(join14(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
17835
+ writeFileSync12(join16(folder, filename), Papa5.unparse(sanitizeCsvRows(rows)), "utf-8");
16852
17836
  fileCount++;
16853
17837
  }
16854
17838
  console.log(chalk29.green(`
@@ -17042,8 +18026,8 @@ var init_bundle = __esm({
17042
18026
  });
17043
18027
 
17044
18028
  // src/repositories/markdown.ts
17045
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync11 } from "fs";
17046
- import { basename as basename6, dirname as dirname3, join as join15, resolve as resolve6 } from "path";
18029
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
18030
+ import { basename as basename6, dirname as dirname3, join as join17, resolve as resolve6 } from "path";
17047
18031
  import { stringify as stringifyYaml } from "yaml";
17048
18032
  function renderMarkdownFiles(pkg) {
17049
18033
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -17266,9 +18250,9 @@ var init_markdown3 = __esm({
17266
18250
  mkdirSync10(root, { recursive: true });
17267
18251
  const written = [];
17268
18252
  for (const file of files) {
17269
- const absolutePath = join15(root, file.relativePath);
18253
+ const absolutePath = join17(root, file.relativePath);
17270
18254
  mkdirSync10(dirname3(absolutePath), { recursive: true });
17271
- writeFileSync11(absolutePath, file.contents, "utf-8");
18255
+ writeFileSync13(absolutePath, file.contents, "utf-8");
17272
18256
  written.push(absolutePath);
17273
18257
  }
17274
18258
  return {
@@ -17572,8 +18556,8 @@ __export(handoff_exports, {
17572
18556
  handler: () => handler16
17573
18557
  });
17574
18558
  import chalk31 from "chalk";
17575
- import { writeFileSync as writeFileSync12 } from "fs";
17576
- import { join as join16 } from "path";
18559
+ import { writeFileSync as writeFileSync14 } from "fs";
18560
+ import { join as join18 } from "path";
17577
18561
  async function handler16(args, ctx) {
17578
18562
  const sub = args[0];
17579
18563
  if (!sub) {
@@ -17631,7 +18615,7 @@ async function interactiveMenu(ctx) {
17631
18615
  }
17632
18616
  }
17633
18617
  async function runReport(args, ctx) {
17634
- const out = join16(getExportsDir(), `report-${stamp()}.md`);
18618
+ const out = join18(getExportsDir(), `report-${stamp()}.md`);
17635
18619
  const { handler: report } = await Promise.resolve().then(() => (init_report2(), report_exports));
17636
18620
  await report(["--format", "md", "--output", out, ...args], ctx);
17637
18621
  recordDeliverable(ctx, { kind: "report", at: (/* @__PURE__ */ new Date()).toISOString(), path: out });
@@ -17679,8 +18663,8 @@ async function runPrompt(target, ctx) {
17679
18663
  return;
17680
18664
  }
17681
18665
  const prompt = draft.markdown;
17682
- const out = join16(getExportsDir(), `handoff-${target}-${stamp()}.md`);
17683
- writeFileSync12(out, prompt, "utf-8");
18666
+ const out = join18(getExportsDir(), `handoff-${target}-${stamp()}.md`);
18667
+ writeFileSync14(out, prompt, "utf-8");
17684
18668
  recordDeliverable(ctx, { kind: `prompt:${target}`, at: (/* @__PURE__ */ new Date()).toISOString(), path: out });
17685
18669
  console.log();
17686
18670
  console.log(" " + paint("accent", `Agent prompt ready (${target})`));
@@ -18552,24 +19536,24 @@ JSON SHAPE:
18552
19536
 
18553
19537
  // src/strategies/readers.ts
18554
19538
  import { createHash } from "crypto";
18555
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
19539
+ import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
18556
19540
  import { extname, resolve as resolve7 } from "path";
18557
19541
  import { parse as parseYaml } from "yaml";
18558
19542
  import { PDFParse } from "pdf-parse";
18559
19543
  async function readStrategyFile(pathOrDash) {
18560
19544
  if (pathOrDash === "-") {
18561
- const text2 = readFileSync10(0, "utf-8");
19545
+ const text2 = readFileSync13(0, "utf-8");
18562
19546
  return createDocument("stdin", null, text2, {});
18563
19547
  }
18564
19548
  const sourcePath = resolve7(pathOrDash);
18565
- if (!existsSync15(sourcePath)) {
19549
+ if (!existsSync17(sourcePath)) {
18566
19550
  throw new NtrpError("strategy_file_not_found", `Strategy file not found: ${pathOrDash}`, 2 /* Usage */);
18567
19551
  }
18568
19552
  const ext = extname(sourcePath).toLowerCase();
18569
19553
  if (ext === ".pdf") {
18570
19554
  return readPdf(sourcePath);
18571
19555
  }
18572
- const text = readFileSync10(sourcePath, "utf-8");
19556
+ const text = readFileSync13(sourcePath, "utf-8");
18573
19557
  if (ext === ".yaml" || ext === ".yml") {
18574
19558
  const structured = parseStructuredYaml(text);
18575
19559
  return createDocument("yaml", sourcePath, text, structured);
@@ -18584,7 +19568,7 @@ function readStrategyText(text) {
18584
19568
  return createDocument("text", null, text, {});
18585
19569
  }
18586
19570
  async function readPdf(sourcePath) {
18587
- const data = readFileSync10(sourcePath);
19571
+ const data = readFileSync13(sourcePath);
18588
19572
  const parser = new PDFParse({ data });
18589
19573
  try {
18590
19574
  const result = await parser.getText();
@@ -18631,15 +19615,15 @@ var init_readers = __esm({
18631
19615
  });
18632
19616
 
18633
19617
  // src/strategies/library.ts
18634
- import { writeFileSync as writeFileSync13 } from "fs";
18635
- import { join as join17 } from "path";
19618
+ import { writeFileSync as writeFileSync15 } from "fs";
19619
+ import { join as join19 } from "path";
18636
19620
  import { stringify as stringifyYaml2 } from "yaml";
18637
19621
  function strategyLibraryPath(slug) {
18638
- return join17(getStrategiesDir(), `${slug}.md`);
19622
+ return join19(getStrategiesDir(), `${slug}.md`);
18639
19623
  }
18640
19624
  function writeStrategyMarkdown(strategy) {
18641
19625
  const path = strategyLibraryPath(strategy.slug);
18642
- writeFileSync13(path, renderStrategyMarkdown(strategy), "utf-8");
19626
+ writeFileSync15(path, renderStrategyMarkdown(strategy), "utf-8");
18643
19627
  return path;
18644
19628
  }
18645
19629
  function renderStrategyMarkdown(strategy) {
@@ -18713,7 +19697,7 @@ var init_library = __esm({
18713
19697
  // src/strategies/connectors.ts
18714
19698
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
18715
19699
  import { homedir as homedir7 } from "os";
18716
- import { basename as basename7, extname as extname2, join as join18, relative, resolve as resolve8, sep as sep3 } from "path";
19700
+ import { basename as basename7, extname as extname2, join as join20, relative, resolve as resolve8, sep as sep3 } from "path";
18717
19701
  function createLocalFolderConnector(options) {
18718
19702
  const rootPath = resolveUserPath2(options.rootPath);
18719
19703
  const name = options.name ?? (basename7(rootPath) || "local");
@@ -18756,7 +19740,7 @@ function createLocalFolderConnector(options) {
18756
19740
  }
18757
19741
  function walkLocalFolder(rootPath, currentPath, refs, opts) {
18758
19742
  for (const entry of readdirSync2(currentPath, { withFileTypes: true })) {
18759
- const absolutePath = join18(currentPath, entry.name);
19743
+ const absolutePath = join20(currentPath, entry.name);
18760
19744
  const relativePath = normalizePath(relative(rootPath, absolutePath));
18761
19745
  if (entry.isDirectory()) {
18762
19746
  if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
@@ -18820,7 +19804,7 @@ function normalizePath(path) {
18820
19804
  }
18821
19805
  function resolveUserPath2(path) {
18822
19806
  if (path === "~") return homedir7();
18823
- if (path.startsWith("~/")) return join18(homedir7(), path.slice(2));
19807
+ if (path.startsWith("~/")) return join20(homedir7(), path.slice(2));
18824
19808
  return resolve8(path);
18825
19809
  }
18826
19810
  var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
@@ -19674,18 +20658,25 @@ __export(config_exports, {
19674
20658
  handler: () => handler23
19675
20659
  });
19676
20660
  import chalk38 from "chalk";
20661
+ import ora10 from "ora";
20662
+ function secretKeys() {
20663
+ const keys = /* @__PURE__ */ new Set(["license-key", "license-instance-id", "voyage-api-key", "tavily-api-key", "brave-api-key"]);
20664
+ for (const spec of listProviderSpecs()) keys.add(spec.key_config_name);
20665
+ return keys;
20666
+ }
19677
20667
  function display(key, value) {
19678
- return SECRET_KEYS.has(key) ? String(value).slice(0, 10) + "..." : String(value);
20668
+ return secretKeys().has(key) ? String(value).slice(0, 10) + "..." : String(value);
19679
20669
  }
19680
20670
  function secretPromptLabel(key) {
19681
- if (key === "api-key") return "Anthropic API key";
19682
- if (key === "openai-api-key") return "OpenAI API key";
20671
+ const spec = findSpecByConfigKey(key);
20672
+ if (spec) return `${spec.label} API key`;
19683
20673
  if (key === "license-key") return "License key";
19684
20674
  return key;
19685
20675
  }
19686
20676
  function usage() {
19687
20677
  console.log(chalk38.dim(" Usage: /config <get|set|list|delete> [key] [value]"));
19688
20678
  console.log(chalk38.dim(" Tip: ") + paint("accent", "/config set api-key") + chalk38.dim(" opens a hidden prompt (no inline paste)."));
20679
+ console.log(chalk38.dim(" Tip: ") + paint("accent", "/connect") + chalk38.dim(" auto-detects the provider from any pasted key."));
19689
20680
  }
19690
20681
  function fail(message, ctx) {
19691
20682
  console.error(chalk38.red(` ${message}`));
@@ -19717,7 +20708,7 @@ async function handler23(args, ctx) {
19717
20708
  return;
19718
20709
  }
19719
20710
  let value = inlineValue;
19720
- if (!value && SECRET_KEYS.has(key)) {
20711
+ if (!value && secretKeys().has(key)) {
19721
20712
  try {
19722
20713
  value = await promptSecretValue(key, ctx);
19723
20714
  } catch (err) {
@@ -19737,8 +20728,24 @@ async function handler23(args, ctx) {
19737
20728
  }
19738
20729
  console.log();
19739
20730
  console.log(chalk38.green(` \u2713 ${key} saved`) + chalk38.dim(` (${display(key, value)})`));
19740
- if (key === "api-key" || key === "openai-api-key") {
19741
- console.log(" " + chalk38.dim("Keys enable AI after analysis. Add both to switch engines with /provider."));
20731
+ const spec = findSpecByConfigKey(key);
20732
+ if (spec) {
20733
+ const spinner = ora10({ text: `Discovering ${spec.label} models\u2026`, discardStdin: false }).start();
20734
+ try {
20735
+ const { refreshProviderModels: refreshProviderModels2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
20736
+ const entry = await refreshProviderModels2(spec.id, { apiKey: value, force: true });
20737
+ if (entry) {
20738
+ spinner.succeed(`${spec.label}: ${entry.models.length} chat models available.`);
20739
+ console.log(
20740
+ " " + chalk38.dim(`high ${entry.tier_stack.high} \xB7 medium ${entry.tier_stack.medium} \xB7 low ${entry.tier_stack.low}`)
20741
+ );
20742
+ } else {
20743
+ spinner.warn(`${spec.label} unreachable \u2014 models will be discovered on first use.`);
20744
+ }
20745
+ } catch {
20746
+ spinner.warn(`${spec.label} unreachable \u2014 models will be discovered on first use.`);
20747
+ }
20748
+ console.log(" " + chalk38.dim("Switch engines with /provider \xB7 browse models with /model list."));
19742
20749
  }
19743
20750
  console.log();
19744
20751
  return;
@@ -19781,15 +20788,14 @@ async function handler23(args, ctx) {
19781
20788
  }
19782
20789
  }
19783
20790
  }
19784
- var SECRET_KEYS;
19785
20791
  var init_config = __esm({
19786
20792
  "src/commands/config.ts"() {
19787
20793
  "use strict";
19788
20794
  init_store();
20795
+ init_providers();
19789
20796
  init_argparse();
19790
20797
  init_prompts();
19791
20798
  init_theme();
19792
- SECRET_KEYS = /* @__PURE__ */ new Set(["api-key", "openai-api-key", "license-key", "license-instance-id"]);
19793
20799
  }
19794
20800
  });
19795
20801
 
@@ -20571,15 +21577,15 @@ var init_checkout = __esm({
20571
21577
  });
20572
21578
 
20573
21579
  // src/services/setup.ts
20574
- import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync14 } from "fs";
20575
- import { join as join19 } from "path";
21580
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync16 } from "fs";
21581
+ import { join as join21 } from "path";
20576
21582
  function setupCheck() {
20577
21583
  const home = ntrpHome();
20578
21584
  let writable = false;
20579
21585
  try {
20580
21586
  mkdirSync11(home, { recursive: true });
20581
- const probe = join19(home, ".write-check");
20582
- writeFileSync14(probe, "ok\n");
21587
+ const probe = join21(home, ".write-check");
21588
+ writeFileSync16(probe, "ok\n");
20583
21589
  writable = true;
20584
21590
  } catch {
20585
21591
  writable = false;
@@ -20604,7 +21610,8 @@ function setupCheck() {
20604
21610
  tier: llmCfg.tier,
20605
21611
  auto_failover: llmCfg.autoFailover,
20606
21612
  anthropic: llmReady.anthropic,
20607
- openai: llmReady.openai
21613
+ openai: llmReady.openai,
21614
+ providers: llmReady.providers
20608
21615
  }
20609
21616
  },
20610
21617
  license: {
@@ -20616,7 +21623,7 @@ function setupCheck() {
20616
21623
  };
20617
21624
  }
20618
21625
  function readProfileInput(pathOrDash) {
20619
- const raw = pathOrDash === "-" ? readFileSync11(0, "utf-8") : readFileSync11(pathOrDash, "utf-8");
21626
+ const raw = pathOrDash === "-" ? readFileSync14(0, "utf-8") : readFileSync14(pathOrDash, "utf-8");
20620
21627
  return JSON.parse(raw);
20621
21628
  }
20622
21629
  function writeAgentProfile(input) {
@@ -20641,11 +21648,15 @@ function writeAgentProfile(input) {
20641
21648
  saveProfile(profile);
20642
21649
  return profile;
20643
21650
  }
20644
- function applyAgentConfig(opts) {
21651
+ async function applyAgentConfig(opts) {
20645
21652
  if (opts.defaultFormat) setConfigValue("default-format", opts.defaultFormat);
20646
21653
  if (opts.apiKey) setConfigValue("api-key", opts.apiKey);
20647
21654
  if (opts.openaiApiKey) setConfigValue("openai-api-key", opts.openaiApiKey);
20648
- if (opts.llmPrimary === "anthropic" || opts.llmPrimary === "openai") {
21655
+ if (opts.llmKey) {
21656
+ const { connectWithKey: connectWithKey2 } = await Promise.resolve().then(() => (init_connect(), connect_exports));
21657
+ await connectWithKey2(opts.llmKey, { providerId: opts.llmProvider });
21658
+ }
21659
+ if (opts.llmPrimary && getProviderSpec(opts.llmPrimary)) {
20649
21660
  setConfigValue("llm-primary", opts.llmPrimary);
20650
21661
  }
20651
21662
  if (opts.licenseKey) setConfigValue("license-key", opts.licenseKey);
@@ -20655,6 +21666,7 @@ var init_setup = __esm({
20655
21666
  "src/services/setup.ts"() {
20656
21667
  "use strict";
20657
21668
  init_repl_api();
21669
+ init_providers();
20658
21670
  init_llm_config();
20659
21671
  init_store();
20660
21672
  init_profile();
@@ -20697,13 +21709,12 @@ async function handler27(args, ctx) {
20697
21709
  console.log(` Writable: ${result.writable ? "yes" : "no"}`);
20698
21710
  console.log(` Profile: ${result.profile.exists ? "ready" : "missing"} (${result.profile.path})`);
20699
21711
  const llm = result.config.llm;
20700
- if (llm) {
20701
- const engines = (llm.anthropic ? 1 : 0) + (llm.openai ? 1 : 0);
20702
- console.log(` Engines: ${engines} \xB7 default ${llm.primary} \xB7 tier ${llm.tier}`);
20703
- console.log(` Anthropic: ${llm.anthropic ? "set" : "missing"} \xB7 OpenAI: ${llm.openai ? "set" : "missing"}`);
21712
+ if (llm && llm.providers.length > 0) {
21713
+ console.log(` Engines: ${llm.providers.length} \xB7 default ${llm.primary} \xB7 tier ${llm.tier}`);
21714
+ console.log(` Connected: ${llm.providers.join(", ")}`);
20704
21715
  console.log(` Auto-failover: ${llm.auto_failover ? "on" : "off"}`);
20705
21716
  } else {
20706
- console.log(" Engines: missing");
21717
+ console.log(" Engines: missing \u2014 run /connect with any provider key");
20707
21718
  }
20708
21719
  console.log(` License: ${formatLicenseSetupLine(result.license)}`);
20709
21720
  console.log();
@@ -20724,10 +21735,12 @@ async function handler27(args, ctx) {
20724
21735
  sales_motion: getString(flags, "sales-motion")
20725
21736
  };
20726
21737
  }
20727
- applyAgentConfig({
21738
+ await applyAgentConfig({
20728
21739
  defaultFormat: getString(flags, "default-format"),
20729
21740
  apiKey: getString(flags, "api-key"),
20730
21741
  openaiApiKey: getString(flags, "openai-api-key"),
21742
+ llmKey: getString(flags, "llm-key"),
21743
+ llmProvider: getString(flags, "llm-provider"),
20731
21744
  llmPrimary: getString(flags, "llm-primary"),
20732
21745
  licenseKey: getString(flags, "license-key"),
20733
21746
  exportDir: getString(flags, "export-dir")
@@ -20767,8 +21780,8 @@ var init_setup2 = __esm({
20767
21780
 
20768
21781
  // src/conversation/orchestrator.ts
20769
21782
  import chalk43 from "chalk";
20770
- import { writeFileSync as writeFileSync15 } from "fs";
20771
- import { join as join20 } from "path";
21783
+ import { writeFileSync as writeFileSync17 } from "fs";
21784
+ import { join as join22 } from "path";
20772
21785
  function printScopeProposal(ctx) {
20773
21786
  if (!ctx.scope) return;
20774
21787
  const lens = ctx.scope.primary_lens === "revenue_metrics" ? "SaaS metrics" : "pipeline health";
@@ -20926,8 +21939,8 @@ async function handleDeliverFlow(input, ctx) {
20926
21939
  prompts.close();
20927
21940
  }
20928
21941
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/T/, "-").replace(/:/g, "").slice(0, 15);
20929
- const out = join20(getExportsDir(), `handoff-${target}-${stamp2}.md`);
20930
- writeFileSync15(out, draft.markdown, "utf-8");
21942
+ const out = join22(getExportsDir(), `handoff-${target}-${stamp2}.md`);
21943
+ writeFileSync17(out, draft.markdown, "utf-8");
20931
21944
  const d = {
20932
21945
  kind: `prompt:${target}`,
20933
21946
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -20951,7 +21964,7 @@ async function handleExploreWithoutKey(ctx) {
20951
21964
  console.log();
20952
21965
  console.log(" " + chalk43.red("AI interpretation needs an LLM API key saved in config."));
20953
21966
  console.log(
20954
- " " + chalk43.dim("Set with: ") + paint("accent", "/config set api-key") + chalk43.dim(" (Anthropic) or ") + paint("accent", "/config set openai-api-key")
21967
+ " " + chalk43.dim("Run ") + paint("accent", "/connect") + chalk43.dim(" and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).")
20955
21968
  );
20956
21969
  console.log(" " + chalk43.dim("Number crunching works without a key \u2014 only Q&A in the REPL needs one."));
20957
21970
  if (ctx.gapAudit) {
@@ -21126,8 +22139,8 @@ async function callProvider(texts) {
21126
22139
  async function embedText(text) {
21127
22140
  const key = text.trim();
21128
22141
  if (!key) return null;
21129
- const cached = cache.get(key);
21130
- if (cached) return cached;
22142
+ const cached2 = cache.get(key);
22143
+ if (cached2) return cached2;
21131
22144
  const result = await callProvider([key]);
21132
22145
  const vec = result?.[0] ?? null;
21133
22146
  if (vec) cache.set(key, vec);
@@ -21137,8 +22150,8 @@ async function embedItems(items) {
21137
22150
  const needing = [];
21138
22151
  const out = items.map((it, index) => {
21139
22152
  if (it.embedding && it.embedding.length > 0) return { ...it };
21140
- const cached = cache.get(it.text.trim());
21141
- if (cached) return { ...it, embedding: cached };
22153
+ const cached2 = cache.get(it.text.trim());
22154
+ if (cached2) return { ...it, embedding: cached2 };
21142
22155
  needing.push({ index, text: it.text });
21143
22156
  return { ...it };
21144
22157
  });
@@ -21297,17 +22310,17 @@ var init_retrieval = __esm({
21297
22310
  });
21298
22311
 
21299
22312
  // src/memory/knowledge.ts
21300
- import { existsSync as existsSync17, readFileSync as readFileSync12, appendFileSync as appendFileSync3, readdirSync as readdirSync3 } from "fs";
21301
- import { join as join21 } from "path";
22313
+ import { existsSync as existsSync19, readFileSync as readFileSync15, appendFileSync as appendFileSync3, readdirSync as readdirSync3 } from "fs";
22314
+ import { join as join23 } from "path";
21302
22315
  import { randomUUID as randomUUID4 } from "crypto";
21303
22316
  function knowledgePath() {
21304
- return join21(getMemoryDir(), KNOWLEDGE_FILE);
22317
+ return join23(getMemoryDir(), KNOWLEDGE_FILE);
21305
22318
  }
21306
22319
  function loadKnowledgeChunks() {
21307
22320
  const path = knowledgePath();
21308
- if (!existsSync17(path)) return [];
22321
+ if (!existsSync19(path)) return [];
21309
22322
  const out = [];
21310
- for (const line of readFileSync12(path, "utf-8").split("\n")) {
22323
+ for (const line of readFileSync15(path, "utf-8").split("\n")) {
21311
22324
  const trimmed = line.trim();
21312
22325
  if (!trimmed) continue;
21313
22326
  try {
@@ -21408,17 +22421,17 @@ __export(store_exports2, {
21408
22421
  rewriteJsonl: () => rewriteJsonl,
21409
22422
  scrubText: () => scrubText
21410
22423
  });
21411
- import { existsSync as existsSync18, readFileSync as readFileSync13, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync16 } from "fs";
21412
- import { join as join22 } from "path";
22424
+ import { existsSync as existsSync20, readFileSync as readFileSync16, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync18 } from "fs";
22425
+ import { join as join24 } from "path";
21413
22426
  import { randomUUID as randomUUID5 } from "crypto";
21414
22427
  function memPath(file) {
21415
- return join22(getMemoryDir(), file);
22428
+ return join24(getMemoryDir(), file);
21416
22429
  }
21417
22430
  function readJsonl(file) {
21418
22431
  const path = memPath(file);
21419
- if (!existsSync18(path)) return [];
22432
+ if (!existsSync20(path)) return [];
21420
22433
  const out = [];
21421
- for (const line of readFileSync13(path, "utf-8").split("\n")) {
22434
+ for (const line of readFileSync16(path, "utf-8").split("\n")) {
21422
22435
  const trimmed = line.trim();
21423
22436
  if (!trimmed) continue;
21424
22437
  try {
@@ -21436,7 +22449,7 @@ function appendJsonl(file, obj) {
21436
22449
  }
21437
22450
  function rewriteJsonl(file, rows) {
21438
22451
  try {
21439
- writeFileSync16(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
22452
+ writeFileSync18(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
21440
22453
  } catch {
21441
22454
  }
21442
22455
  }
@@ -21498,7 +22511,7 @@ function loadWinSnippets() {
21498
22511
  const out = [];
21499
22512
  for (const name of readdirSync4(dir)) {
21500
22513
  if (!name.endsWith(".md") || name.toLowerCase() === "readme.md") continue;
21501
- const raw = readFileSync13(join22(dir, name), "utf-8");
22514
+ const raw = readFileSync16(join24(dir, name), "utf-8");
21502
22515
  const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? name.replace(/\.md$/, "");
21503
22516
  const body = raw.replace(/^#.*$/m, "").replace(/\s+/g, " ").trim().slice(0, 300);
21504
22517
  out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });
@@ -21587,7 +22600,7 @@ var init_store2 = __esm({
21587
22600
  });
21588
22601
 
21589
22602
  // src/services/smoke-protocol.ts
21590
- import { join as join23 } from "path";
22603
+ import { join as join25 } from "path";
21591
22604
  function isSmokeProtocolTrigger(input) {
21592
22605
  return normalize(input).includes(SMOKE_TRIGGER_PHRASE);
21593
22606
  }
@@ -21623,7 +22636,7 @@ async function runSmokeProtocol(_input, ctx) {
21623
22636
  });
21624
22637
  const proposalResult = await proposeRepositoryExport({
21625
22638
  target: "markdown",
21626
- directory: join23(getExportsDir(), "repository-smoke"),
22639
+ directory: join25(getExportsDir(), "repository-smoke"),
21627
22640
  source: "smoke_protocol",
21628
22641
  modelOrFixture: "smoke-protocol-v1"
21629
22642
  });
@@ -21720,13 +22733,13 @@ var nl_exports = {};
21720
22733
  __export(nl_exports, {
21721
22734
  runNaturalLanguage: () => runNaturalLanguage
21722
22735
  });
21723
- import ora10 from "ora";
22736
+ import ora11 from "ora";
21724
22737
  import chalk44 from "chalk";
21725
22738
  async function runNaturalLanguage(input, ctx) {
21726
22739
  if (isSmokeProtocolTrigger(input)) {
21727
22740
  recordMessage(ctx, "user", input);
21728
22741
  console.log();
21729
- const spinner2 = ora10({ text: "Running smoke protocol\u2026", color: "cyan", discardStdin: false }).start();
22742
+ const spinner2 = ora11({ text: "Running smoke protocol\u2026", color: "cyan", discardStdin: false }).start();
21730
22743
  try {
21731
22744
  const result = await runSmokeProtocol(input, ctx);
21732
22745
  spinner2.succeed("Smoke protocol complete");
@@ -21753,7 +22766,7 @@ async function runNaturalLanguage(input, ctx) {
21753
22766
  let snapshot = ctx.snapshot.computeResult;
21754
22767
  if (!snapshot) {
21755
22768
  const metricsFirst = prefersMetricsFirstContext(ctx);
21756
- const spinner2 = ora10({
22769
+ const spinner2 = ora11({
21757
22770
  text: metricsFirst ? "Loading session context\u2026" : "Computing vital signs\u2026",
21758
22771
  color: "cyan",
21759
22772
  discardStdin: false
@@ -21778,7 +22791,7 @@ async function runNaturalLanguage(input, ctx) {
21778
22791
  }
21779
22792
  console.log();
21780
22793
  const memoryBlock = await buildMemoryBlock(input).catch(() => "");
21781
- const spinner = ora10({ text: "Thinking\u2026", color: "cyan", discardStdin: false }).start();
22794
+ const spinner = ora11({ text: "Thinking\u2026", color: "cyan", discardStdin: false }).start();
21782
22795
  let lastAnswer = "";
21783
22796
  let rawHistory = [];
21784
22797
  const toolsUsed = [];
@@ -22069,7 +23082,7 @@ __export(metrics_exports, {
22069
23082
  handler: () => handler29
22070
23083
  });
22071
23084
  import chalk46 from "chalk";
22072
- import ora11 from "ora";
23085
+ import ora12 from "ora";
22073
23086
  async function handler29(args, ctx) {
22074
23087
  await hydrateAnalysisFromPersistedState(ctx);
22075
23088
  const { flags } = parseArgs2(args, ["findings"]);
@@ -22111,7 +23124,7 @@ async function handler29(args, ctx) {
22111
23124
  await initSchema();
22112
23125
  await autoGenerateSegments();
22113
23126
  printCompanionBanner("metrics", ctx.analysis.primary);
22114
- const spinner = ora11({
23127
+ const spinner = ora12({
22115
23128
  text: "Computing SaaS metrics\u2026",
22116
23129
  indent: 2,
22117
23130
  discardStdin: false
@@ -22310,7 +23323,7 @@ __export(feedback_exports, {
22310
23323
  handler: () => handler30
22311
23324
  });
22312
23325
  import chalk47 from "chalk";
22313
- import ora12 from "ora";
23326
+ import ora13 from "ora";
22314
23327
  async function handler30(args, ctx) {
22315
23328
  const feedbackText = args.join(" ").trim();
22316
23329
  if (!feedbackText) {
@@ -22338,7 +23351,7 @@ async function handler30(args, ctx) {
22338
23351
  console.log();
22339
23352
  return;
22340
23353
  }
22341
- const spinner = ora12({ text: "Applying feedback\u2026", discardStdin: false }).start();
23354
+ const spinner = ora13({ text: "Applying feedback\u2026", discardStdin: false }).start();
22342
23355
  try {
22343
23356
  const result = await applyFeedback(profile, feedbackText, ctx);
22344
23357
  spinner.succeed("Feedback applied");
@@ -22370,7 +23383,7 @@ var recap_exports = {};
22370
23383
  __export(recap_exports, {
22371
23384
  handler: () => handler31
22372
23385
  });
22373
- import ora13 from "ora";
23386
+ import ora14 from "ora";
22374
23387
  import chalk48 from "chalk";
22375
23388
  async function handler31(_args, ctx) {
22376
23389
  if (ctx.messages.length === 0) {
@@ -22407,7 +23420,7 @@ ${companyBlock}` : "",
22407
23420
  const prefix = msg.role === "user" ? "USER" : "ASSISTANT";
22408
23421
  conversationLines.push(`[${prefix}]: ${msg.content}`);
22409
23422
  }
22410
- const spinner = ora13({ text: "Summarizing session\u2026", color: "cyan", discardStdin: false }).start();
23423
+ const spinner = ora14({ text: "Summarizing session\u2026", color: "cyan", discardStdin: false }).start();
22411
23424
  try {
22412
23425
  const { text: fullText } = await llmStreamText(
22413
23426
  "recap",
@@ -22535,7 +23548,7 @@ var init_recall = __esm({
22535
23548
 
22536
23549
  // src/memory/feedback.ts
22537
23550
  import { appendFileSync as appendFileSync5 } from "fs";
22538
- import { join as join24 } from "path";
23551
+ import { join as join26 } from "path";
22539
23552
  import { randomUUID as randomUUID6 } from "crypto";
22540
23553
  function summarize(text) {
22541
23554
  return text.replace(/[#*`>_]/g, "").replace(/\s+/g, " ").trim().slice(0, 200);
@@ -22551,7 +23564,7 @@ function recordFeedback(input) {
22551
23564
  created_at: (/* @__PURE__ */ new Date()).toISOString()
22552
23565
  };
22553
23566
  try {
22554
- appendFileSync5(join24(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
23567
+ appendFileSync5(join26(getMemoryDir(), "feedback.jsonl"), JSON.stringify(entry) + "\n");
22555
23568
  } catch {
22556
23569
  }
22557
23570
  if (input.rating === "positive") {
@@ -22642,7 +23655,7 @@ __export(knowledge_exports, {
22642
23655
  handler: () => handler35
22643
23656
  });
22644
23657
  import chalk52 from "chalk";
22645
- import ora14 from "ora";
23658
+ import ora15 from "ora";
22646
23659
  async function handler35(args, ctx) {
22647
23660
  const sub = (args[0] ?? "list").toLowerCase();
22648
23661
  if (sub === "add") {
@@ -22654,7 +23667,7 @@ async function handler35(args, ctx) {
22654
23667
  console.log();
22655
23668
  return;
22656
23669
  }
22657
- const spin = ctx.execution.progress ? ora14({ text: "Ingesting knowledge\u2026", color: "cyan", discardStdin: false }).start() : null;
23670
+ const spin = ctx.execution.progress ? ora15({ text: "Ingesting knowledge\u2026", color: "cyan", discardStdin: false }).start() : null;
22658
23671
  try {
22659
23672
  const result = await addKnowledgeFile(path);
22660
23673
  spin?.succeed(`Indexed "${result.title}"`);
@@ -22964,8 +23977,8 @@ var switch_exports = {};
22964
23977
  __export(switch_exports, {
22965
23978
  handler: () => handler39
22966
23979
  });
22967
- import { join as join25 } from "path";
22968
- import ora15 from "ora";
23980
+ import { join as join27 } from "path";
23981
+ import ora16 from "ora";
22969
23982
  import chalk56 from "chalk";
22970
23983
  async function handler39(args, ctx) {
22971
23984
  if (args.length === 0) {
@@ -22980,7 +23993,7 @@ async function handler39(args, ctx) {
22980
23993
  }
22981
23994
  const exchangeCount = Math.floor(ctx.messages.length / 2);
22982
23995
  if (exchangeCount > 0) {
22983
- const spinner = ora15({ text: "Saving current session\u2026", color: "cyan", discardStdin: false }).start();
23996
+ const spinner = ora16({ text: "Saving current session\u2026", color: "cyan", discardStdin: false }).start();
22984
23997
  await closeSession(ctx);
22985
23998
  const fromLabel = ctx.sessionName ? `"${ctx.sessionName}"` : ctx.sessionId.slice(-4);
22986
23999
  spinner.succeed(`Saved ${fromLabel}`);
@@ -22995,7 +24008,7 @@ async function handler39(args, ctx) {
22995
24008
  }
22996
24009
  const context = buildSwitchContext(session);
22997
24010
  const newId = makeSessionId();
22998
- const newFile = join25(getSessionsDir(), `${newId}.json`);
24011
+ const newFile = join27(getSessionsDir(), `${newId}.json`);
22999
24012
  resetContextForSwitch(ctx, {
23000
24013
  sessionId: newId,
23001
24014
  sessionFile: newFile,
@@ -23021,7 +24034,7 @@ async function handler39(args, ctx) {
23021
24034
  return `Switched to "${targetName}"`;
23022
24035
  } else {
23023
24036
  const newId = makeSessionId();
23024
- const newFile = join25(getSessionsDir(), `${newId}.json`);
24037
+ const newFile = join27(getSessionsDir(), `${newId}.json`);
23025
24038
  resetContextForSwitch(ctx, {
23026
24039
  sessionId: newId,
23027
24040
  sessionFile: newFile,
@@ -23071,13 +24084,177 @@ var init_switch = __esm({
23071
24084
  }
23072
24085
  });
23073
24086
 
23074
- // src/commands/provider.ts
23075
- var provider_exports = {};
23076
- __export(provider_exports, {
24087
+ // src/commands/connect.ts
24088
+ var connect_exports2 = {};
24089
+ __export(connect_exports2, {
23077
24090
  handler: () => handler40
23078
24091
  });
23079
24092
  import chalk57 from "chalk";
24093
+ import ora17 from "ora";
24094
+ function usage2() {
24095
+ console.log(chalk57.dim(" Usage: /connect paste any provider key"));
24096
+ console.log(chalk57.dim(" /connect <provider> key for a specific provider (or: ollama)"));
24097
+ console.log(chalk57.dim(" /connect --key <key> non-interactive (auto-detects provider)"));
24098
+ console.log(chalk57.dim(" /connect --base-url <url> [--id <name>] [--key <key>] custom endpoint"));
24099
+ }
24100
+ function printOutcome(outcome, ctx) {
24101
+ console.log();
24102
+ const [headline, ...rest] = describeConnectOutcome(outcome);
24103
+ console.log(" " + paint("success", "\u2713") + " " + chalk57.bold(headline ?? ""));
24104
+ for (const line of rest) {
24105
+ console.log(" " + chalk57.dim(line));
24106
+ }
24107
+ console.log();
24108
+ console.log(" " + chalk57.dim(`Active stack: ${formatActiveStack(ctx)}`));
24109
+ console.log(" " + chalk57.dim("/provider to switch engines \xB7 /model list to browse models"));
24110
+ console.log();
24111
+ }
24112
+ function printError(err, ctx) {
24113
+ const message = err instanceof Error ? err.message : String(err);
24114
+ console.log();
24115
+ console.log(" " + chalk57.red(message));
24116
+ console.log();
24117
+ if (ctx.oneShot) process.exit(1);
24118
+ }
24119
+ async function promptKey(session, label) {
24120
+ console.log();
24121
+ console.log(
24122
+ " " + chalk57.dim("Paste once, press Enter. Stored in ") + paint("accent", "~/.ntrp/config.json") + chalk57.dim(" only.")
24123
+ );
24124
+ return session.askSecret(label, { confirm: false });
24125
+ }
23080
24126
  async function handler40(args, ctx) {
24127
+ const { positional, flags } = parseArgs2(args);
24128
+ const sub = positional[0]?.toLowerCase();
24129
+ if (sub === "help") {
24130
+ usage2();
24131
+ return;
24132
+ }
24133
+ const inlineKey = getString(flags, "key");
24134
+ const baseUrl = getString(flags, "base-url", "url");
24135
+ const forcedProvider = getString(flags, "provider") ?? (sub && sub !== "help" ? sub : void 0);
24136
+ const customId = getString(flags, "id");
24137
+ const label = getString(flags, "label");
24138
+ const forcedSpec = forcedProvider ? getProviderSpec(forcedProvider) : void 0;
24139
+ if (forcedSpec && !forcedSpec.requires_key && !inlineKey) {
24140
+ const spinner2 = ora17({ text: `Looking for ${forcedSpec.label}\u2026`, discardStdin: false }).start();
24141
+ try {
24142
+ const outcome = await connectKeyless(forcedSpec.id, baseUrl);
24143
+ spinner2.stop();
24144
+ printOutcome(outcome, ctx);
24145
+ } catch (err) {
24146
+ spinner2.stop();
24147
+ printError(err, ctx);
24148
+ }
24149
+ return;
24150
+ }
24151
+ if (baseUrl && !forcedSpec) {
24152
+ const id = customId ?? forcedProvider ?? hostToId(baseUrl);
24153
+ let key2 = inlineKey;
24154
+ if (!key2 && !ctx.oneShot && process.stdin.isTTY) {
24155
+ const session2 = createPromptSession(ctx.rl, ctx);
24156
+ try {
24157
+ const needsKey = await session2.confirm("Does this endpoint need an API key?", false);
24158
+ if (needsKey) key2 = await promptKey(session2, `API key for ${id}`);
24159
+ } finally {
24160
+ session2.close();
24161
+ }
24162
+ }
24163
+ const spinner2 = ora17({ text: `Checking ${baseUrl}\u2026`, discardStdin: false }).start();
24164
+ try {
24165
+ const outcome = await connectCustomEndpoint({ id, baseUrl, key: key2, label });
24166
+ spinner2.stop();
24167
+ printOutcome(outcome, ctx);
24168
+ } catch (err) {
24169
+ spinner2.stop();
24170
+ printError(err, ctx);
24171
+ }
24172
+ return;
24173
+ }
24174
+ if (forcedProvider && !forcedSpec) {
24175
+ console.log();
24176
+ console.log(" " + chalk57.red(`Unknown provider: ${forcedProvider}`));
24177
+ console.log(
24178
+ " " + chalk57.dim("Built-ins: anthropic, openai, google, groq, mistral, deepseek, xai, openrouter, together, fireworks, ollama")
24179
+ );
24180
+ console.log(" " + chalk57.dim(`Custom endpoint: /connect --base-url <url> --id ${forcedProvider}`));
24181
+ console.log();
24182
+ if (ctx.oneShot) process.exit(1);
24183
+ return;
24184
+ }
24185
+ let key = inlineKey;
24186
+ let session;
24187
+ if (!key) {
24188
+ if (ctx.oneShot || !process.stdin.isTTY) {
24189
+ printError(new ConnectError("Non-interactive mode needs --key <key>."), ctx);
24190
+ usage2();
24191
+ return;
24192
+ }
24193
+ session = createPromptSession(ctx.rl, ctx);
24194
+ key = await promptKey(
24195
+ session,
24196
+ forcedSpec ? `${forcedSpec.label} API key` : "LLM API key (any provider)"
24197
+ );
24198
+ }
24199
+ const spinner = ora17({ text: "Identifying provider\u2026", discardStdin: false }).start();
24200
+ try {
24201
+ const outcome = await connectWithKey(key, {
24202
+ providerId: forcedSpec?.id,
24203
+ callbacks: session ? {
24204
+ confirmDetection: async (providerId) => {
24205
+ spinner.stop();
24206
+ return session.confirm(`Detected ${providerLabel(providerId)} \u2014 connect it?`, true);
24207
+ },
24208
+ chooseProvider: async (accepted) => {
24209
+ spinner.stop();
24210
+ return session.choose(
24211
+ "Multiple providers accepted this key \u2014 which is it?",
24212
+ accepted.map((a) => ({ value: a.provider, label: providerLabel(a.provider) }))
24213
+ );
24214
+ }
24215
+ } : void 0
24216
+ });
24217
+ spinner.stop();
24218
+ printOutcome(outcome, ctx);
24219
+ } catch (err) {
24220
+ spinner.stop();
24221
+ if (err instanceof ConnectCancelled) {
24222
+ console.log(" " + chalk57.dim("Cancelled."));
24223
+ console.log();
24224
+ } else {
24225
+ printError(err, ctx);
24226
+ }
24227
+ } finally {
24228
+ session?.close();
24229
+ }
24230
+ }
24231
+ function hostToId(url) {
24232
+ try {
24233
+ const host = new URL(url).hostname;
24234
+ return host.replace(/^www\./, "").split(".")[0] ?? "custom";
24235
+ } catch {
24236
+ return "custom";
24237
+ }
24238
+ }
24239
+ var init_connect2 = __esm({
24240
+ "src/commands/connect.ts"() {
24241
+ "use strict";
24242
+ init_argparse();
24243
+ init_prompts();
24244
+ init_providers();
24245
+ init_session_state();
24246
+ init_connect();
24247
+ init_theme();
24248
+ }
24249
+ });
24250
+
24251
+ // src/commands/provider.ts
24252
+ var provider_exports = {};
24253
+ __export(provider_exports, {
24254
+ handler: () => handler41
24255
+ });
24256
+ import chalk58 from "chalk";
24257
+ async function handler41(args, ctx) {
23081
24258
  const { positional, flags } = parseArgs2(args, ["default"]);
23082
24259
  const sub = positional[0]?.toLowerCase();
23083
24260
  if (!sub || sub === "list") {
@@ -23089,7 +24266,7 @@ async function handler40(args, ctx) {
23089
24266
  if (!ctx.oneShot) saveSessionState(ctx);
23090
24267
  console.log();
23091
24268
  console.log(" " + paint("success", "\u2713") + " Session engine reset \u2014 using config defaults.");
23092
- console.log(" " + chalk57.dim(`Default: ${loadLlmConfig().primary}`));
24269
+ console.log(" " + chalk58.dim(`Default: ${loadLlmConfig().primary}`));
23093
24270
  console.log();
23094
24271
  return;
23095
24272
  }
@@ -23102,7 +24279,7 @@ async function handler40(args, ctx) {
23102
24279
  setConfigValue("llm-auto-failover", session.autoFailover ? "on" : "off");
23103
24280
  }
23104
24281
  console.log();
23105
- console.log(" " + paint("success", "\u2713") + ` Saved ${chalk57.bold(active)} as default engine.`);
24282
+ console.log(" " + paint("success", "\u2713") + ` Saved ${chalk58.bold(active)} as default engine.`);
23106
24283
  console.log();
23107
24284
  return;
23108
24285
  }
@@ -23121,36 +24298,40 @@ async function handler40(args, ctx) {
23121
24298
  }
23122
24299
  console.log();
23123
24300
  console.log(
23124
- " " + paint("success", "\u2713") + ` Auto-failover ${session.autoFailover ? chalk57.bold("on") : chalk57.bold("off")} for this session.`
24301
+ " " + paint("success", "\u2713") + ` Auto-failover ${session.autoFailover ? chalk58.bold("on") : chalk58.bold("off")} for this session.`
23125
24302
  );
23126
- if (persist) console.log(" " + chalk57.dim("Also saved as config default."));
24303
+ if (persist) console.log(" " + chalk58.dim("Also saved as config default."));
23127
24304
  console.log();
23128
24305
  return;
23129
24306
  }
23130
- if (!PROVIDERS.includes(sub)) {
24307
+ const spec = getProviderSpec(sub);
24308
+ if (!spec || RESERVED.has(sub)) {
23131
24309
  console.log();
23132
- console.log(" " + chalk57.red(`Unknown engine: ${sub}`));
23133
- console.log(" " + chalk57.dim("Usage: /provider [anthropic|openai|list|reset|save|failover on|off]"));
24310
+ console.log(" " + chalk58.red(`Unknown engine: ${sub}`));
24311
+ console.log(" " + chalk58.dim("Usage: /provider [<id>|list|reset|save|failover on|off]"));
24312
+ console.log(" " + chalk58.dim("Connected: ") + (availableEngineLabels().join(", ") || chalk58.dim("none")));
24313
+ console.log(" " + chalk58.dim("Add one with ") + paint("accent", "/connect"));
23134
24314
  console.log();
23135
24315
  return;
23136
24316
  }
23137
- const provider = sub;
24317
+ const provider = spec.id;
23138
24318
  if (!hasProviderKey(provider)) {
23139
- const keyHint = provider === "anthropic" ? "api-key" : "openai-api-key";
23140
24319
  console.log();
23141
- console.log(" " + chalk57.red(`No ${provider} key configured.`));
23142
- console.log(" " + chalk57.dim(`Run `) + paint("accent", `/config set ${keyHint}`) + chalk57.dim(" to add one."));
24320
+ console.log(" " + chalk58.red(`${spec.label} isn't connected.`));
24321
+ console.log(
24322
+ " " + chalk58.dim("Run ") + paint("accent", `/connect ${provider}`) + chalk58.dim(" (or ") + paint("accent", `/config set ${spec.key_config_name}`) + chalk58.dim(").")
24323
+ );
23143
24324
  console.log();
23144
24325
  return;
23145
24326
  }
23146
24327
  ensureLlmSession(ctx).provider = provider;
23147
24328
  if (!ctx.oneShot) saveSessionState(ctx);
23148
24329
  console.log();
23149
- console.log(" " + paint("success", "\u2713") + ` Active engine: ${chalk57.bold(provider)}`);
23150
- console.log(" " + chalk57.dim(`Stack: ${formatActiveStack(ctx)}`));
24330
+ console.log(" " + paint("success", "\u2713") + ` Active engine: ${chalk58.bold(provider)}`);
24331
+ console.log(" " + chalk58.dim(`Stack: ${formatActiveStack(ctx)}`));
23151
24332
  const others = availableEngineLabels().filter((p) => p !== provider);
23152
24333
  if (others.length > 0) {
23153
- console.log(" " + chalk57.dim(`Also available: ${others.join(", ")}`));
24334
+ console.log(" " + chalk58.dim(`Also available: ${others.join(", ")}`));
23154
24335
  }
23155
24336
  console.log();
23156
24337
  }
@@ -23161,49 +24342,54 @@ function printStatus(ctx) {
23161
24342
  const autoFailover = resolveAutoFailoverEnabled(ctx);
23162
24343
  const engines = countAvailableEngines();
23163
24344
  console.log();
23164
- console.log(chalk57.bold(" LLM engines"));
23165
- console.log(` Available: ${engines} engine${engines === 1 ? "" : "s"}`);
23166
- for (const p of PROVIDERS) {
23167
- const key = hasProviderKey(p) ? paint("success", "\u2713") : chalk57.dim("\xB7");
23168
- const marker2 = p === active ? paint("accent", " \u25BA active") : "";
23169
- console.log(` ${key} ${p}${marker2}`);
24345
+ console.log(chalk58.bold(" LLM engines"));
24346
+ console.log(` Connected: ${engines} engine${engines === 1 ? "" : "s"}`);
24347
+ const configured = listProviderSpecs().filter((s) => hasProviderKey(s.id));
24348
+ for (const s of configured) {
24349
+ const marker2 = s.id === active ? paint("accent", " \u25BA active") : "";
24350
+ console.log(` ${paint("success", "\u2713")} ${s.id}${s.custom ? chalk58.dim(" (custom)") : ""}${marker2}`);
24351
+ }
24352
+ if (configured.length === 0) {
24353
+ console.log(" " + chalk58.dim("none \u2014 run /connect and paste any provider key"));
23170
24354
  }
23171
24355
  console.log();
23172
- console.log(chalk57.bold(" Active stack"));
24356
+ console.log(chalk58.bold(" Active stack"));
23173
24357
  console.log(` ${formatActiveStack(ctx)}`);
23174
24358
  if (sessionOverride) {
23175
- console.log(chalk57.dim(" (session override \u2014 /provider reset to use default)"));
24359
+ console.log(chalk58.dim(" (session override \u2014 /provider reset to use default)"));
23176
24360
  } else {
23177
- console.log(chalk57.dim(` (config default: ${cfg.primary})`));
24361
+ console.log(chalk58.dim(` (config default: ${cfg.primary})`));
23178
24362
  }
23179
24363
  console.log();
23180
- console.log(` Auto-failover: ${autoFailover ? paint("success", "on") : chalk57.dim("off")}`);
23181
- console.log(chalk57.dim(" /provider anthropic|openai \u2014 switch engine"));
23182
- console.log(chalk57.dim(" /provider failover on|off \u2014 rate-limit safety net"));
23183
- console.log(chalk57.dim(" /provider save \u2014 persist active engine to config"));
24364
+ console.log(` Auto-failover: ${autoFailover ? paint("success", "on") : chalk58.dim("off")}`);
24365
+ console.log(chalk58.dim(` /provider <id> \u2014 switch engine (${configured.map((s) => s.id).join(", ") || "none connected"})`));
24366
+ console.log(chalk58.dim(" /provider failover on|off \u2014 rate-limit safety net"));
24367
+ console.log(chalk58.dim(" /provider save \u2014 persist active engine to config"));
24368
+ console.log(chalk58.dim(" /connect \u2014 add another provider (any API key)"));
23184
24369
  console.log();
23185
24370
  }
23186
- var PROVIDERS;
24371
+ var RESERVED;
23187
24372
  var init_provider = __esm({
23188
24373
  "src/commands/provider.ts"() {
23189
24374
  "use strict";
23190
24375
  init_argparse();
23191
24376
  init_session_state();
24377
+ init_providers();
23192
24378
  init_llm_config();
23193
24379
  init_store();
23194
24380
  init_context2();
23195
24381
  init_theme();
23196
- PROVIDERS = ["anthropic", "openai"];
24382
+ RESERVED = /* @__PURE__ */ new Set(["list", "reset", "save", "failover"]);
23197
24383
  }
23198
24384
  });
23199
24385
 
23200
24386
  // src/commands/tier.ts
23201
24387
  var tier_exports = {};
23202
24388
  __export(tier_exports, {
23203
- handler: () => handler41
24389
+ handler: () => handler42
23204
24390
  });
23205
- import chalk58 from "chalk";
23206
- async function handler41(args, ctx) {
24391
+ import chalk59 from "chalk";
24392
+ async function handler42(args, ctx) {
23207
24393
  const { positional, flags } = parseArgs2(args, ["default"]);
23208
24394
  const sub = positional[0]?.toLowerCase();
23209
24395
  if (!sub || sub === "list") {
@@ -23212,8 +24398,8 @@ async function handler41(args, ctx) {
23212
24398
  }
23213
24399
  if (!TIERS.includes(sub)) {
23214
24400
  console.log();
23215
- console.log(" " + chalk58.red(`Unknown tier: ${sub}`));
23216
- console.log(" " + chalk58.dim("Usage: /tier [high|medium|low|list] [--default]"));
24401
+ console.log(" " + chalk59.red(`Unknown tier: ${sub}`));
24402
+ console.log(" " + chalk59.dim("Usage: /tier [high|medium|low|list] [--default]"));
23217
24403
  console.log();
23218
24404
  return;
23219
24405
  }
@@ -23227,40 +24413,48 @@ async function handler41(args, ctx) {
23227
24413
  }
23228
24414
  console.log();
23229
24415
  console.log(
23230
- " " + paint("success", "\u2713") + ` Inference tier set to ${chalk58.bold(tier.toUpperCase())}` + (persist ? chalk58.dim(" (saved as default)") : chalk58.dim(" (this session)"))
24416
+ " " + paint("success", "\u2713") + ` Inference tier set to ${chalk59.bold(tier.toUpperCase())}` + (persist ? chalk59.dim(" (saved as default)") : chalk59.dim(" (this session)"))
23231
24417
  );
23232
- console.log(" " + chalk58.dim(`Stack: ${formatActiveStack(ctx)}`));
24418
+ console.log(" " + chalk59.dim(`Stack: ${formatActiveStack(ctx)}`));
23233
24419
  console.log();
23234
24420
  }
23235
24421
  function printCatalog(ctx) {
23236
24422
  const cfg = loadLlmConfig();
23237
24423
  const active = resolveModelForActive(ctx, "agentic_investigation");
23238
24424
  const sessionTier = ctx.llm?.tier;
24425
+ const providers = getAvailableProviders();
23239
24426
  console.log();
23240
- console.log(chalk58.bold(" Inference settings"));
24427
+ console.log(chalk59.bold(" Inference settings"));
23241
24428
  console.log(` Active: ${paint("accent", formatActiveStack(ctx))}`);
23242
24429
  if (sessionTier) {
23243
- console.log(chalk58.dim(" (session tier override)"));
24430
+ console.log(chalk59.dim(" (session tier override)"));
23244
24431
  } else {
23245
- console.log(chalk58.dim(` Config default tier: ${cfg.tier.toUpperCase()}`));
24432
+ console.log(chalk59.dim(` Config default tier: ${cfg.tier.toUpperCase()}`));
23246
24433
  }
23247
24434
  console.log();
24435
+ if (providers.length === 0) {
24436
+ console.log(" " + chalk59.dim("No engines connected \u2014 run /connect and paste any provider key."));
24437
+ console.log();
24438
+ }
23248
24439
  for (const tier of TIERS) {
23249
- console.log(chalk58.bold(` ${tier.toUpperCase()}`));
23250
- for (const provider of ["anthropic", "openai"]) {
23251
- const models = listCatalogEntries(provider).filter((m) => m.tier === tier);
23252
- for (const m of models) {
23253
- const isActive = provider === active.provider && tier === active.tier && m.id === active.modelId;
23254
- const marker2 = isActive ? paint("accent", "\u25BA ") : " ";
23255
- const status = m.status === "active" ? "" : chalk58.yellow(` [${m.status}]`);
23256
- console.log(`${marker2}${provider}/${m.id}${status} \u2014 ${m.display_name}`);
24440
+ console.log(chalk59.bold(` ${tier.toUpperCase()}`));
24441
+ for (const provider of providers) {
24442
+ const modelId = resolveModelSafe(provider, tier);
24443
+ if (!modelId) {
24444
+ console.log(` ${provider}/${chalk59.dim("no models \u2014 /model refresh")}`);
24445
+ continue;
23257
24446
  }
24447
+ const isActive = provider === active.provider && tier === active.tier && modelId === active.modelId;
24448
+ const marker2 = isActive ? paint("accent", "\u25BA ") : " ";
24449
+ const discovered = !!getProviderModels(provider);
24450
+ const source = discovered ? "" : chalk59.dim(" [bundled fallback]");
24451
+ console.log(`${marker2}${provider}/${modelId}${source}`);
23258
24452
  }
23259
24453
  console.log();
23260
24454
  }
23261
- console.log(chalk58.dim(" /tier high|medium|low \u2014 set tier for this session"));
23262
- console.log(chalk58.dim(" /tier high --default \u2014 also save as config default"));
23263
- console.log(chalk58.dim(" /provider anthropic|openai \u2014 switch engine"));
24455
+ console.log(chalk59.dim(" /tier high|medium|low \u2014 set tier for this session"));
24456
+ console.log(chalk59.dim(" /tier high --default \u2014 also save as config default"));
24457
+ console.log(chalk59.dim(" /provider <id> \u2014 switch engine \xB7 /model list \u2014 browse models"));
23264
24458
  console.log();
23265
24459
  }
23266
24460
  var TIERS;
@@ -23269,6 +24463,7 @@ var init_tier = __esm({
23269
24463
  "use strict";
23270
24464
  init_argparse();
23271
24465
  init_catalog();
24466
+ init_models_cache();
23272
24467
  init_session_state();
23273
24468
  init_llm_config();
23274
24469
  init_store();
@@ -23281,12 +24476,21 @@ var init_tier = __esm({
23281
24476
  // src/commands/model.ts
23282
24477
  var model_exports = {};
23283
24478
  __export(model_exports, {
23284
- handler: () => handler42
24479
+ handler: () => handler43
23285
24480
  });
23286
- import chalk59 from "chalk";
23287
- async function handler42(args, ctx) {
23288
- const { positional, flags } = parseArgs2(args, ["default"]);
24481
+ import chalk60 from "chalk";
24482
+ import ora18 from "ora";
24483
+ async function handler43(args, ctx) {
24484
+ const { positional, flags } = parseArgs2(args, ["default", "all"]);
23289
24485
  const sub = positional[0]?.toLowerCase();
24486
+ if (sub === "list") {
24487
+ printModelList(ctx, getBool(flags, "all"));
24488
+ return;
24489
+ }
24490
+ if (sub === "refresh") {
24491
+ await refreshModels(ctx);
24492
+ return;
24493
+ }
23290
24494
  if (sub === "clear") {
23291
24495
  const persist = getBool(flags, "default");
23292
24496
  if (ctx.llm) ctx.llm.modelOverride = void 0;
@@ -23294,7 +24498,7 @@ async function handler42(args, ctx) {
23294
24498
  if (!ctx.oneShot) saveSessionState(ctx);
23295
24499
  console.log();
23296
24500
  console.log(" " + paint("success", "\u2713") + " Model override cleared \u2014 using tier defaults.");
23297
- console.log(" " + chalk59.dim(`Stack: ${formatActiveStack(ctx)}`));
24501
+ console.log(" " + chalk60.dim(`Stack: ${formatActiveStack(ctx)}`));
23298
24502
  console.log();
23299
24503
  return;
23300
24504
  }
@@ -23302,22 +24506,28 @@ async function handler42(args, ctx) {
23302
24506
  const modelId = positional[1];
23303
24507
  if (!modelId) {
23304
24508
  console.log();
23305
- console.log(" " + chalk59.red("Usage: /model set <model-id> [--default]"));
24509
+ console.log(" " + chalk60.red("Usage: /model set <model-id> [--default]"));
23306
24510
  console.log();
23307
24511
  return;
23308
24512
  }
23309
24513
  const active = resolveActiveProvider(ctx);
23310
24514
  const providerErr = validateModelForProvider(modelId, active);
23311
- const entry = getCatalogEntry(modelId);
23312
24515
  if (providerErr) {
23313
24516
  console.log();
23314
- console.log(" " + chalk59.red(providerErr));
24517
+ console.log(" " + chalk60.red(providerErr));
23315
24518
  console.log();
23316
24519
  return;
23317
24520
  }
23318
- if (!entry) {
24521
+ const cache2 = getProviderModels(active);
24522
+ const known = cache2?.models.some((m) => m.id === modelId);
24523
+ if (cache2 && !known) {
23319
24524
  console.log();
23320
- console.log(" " + chalk59.yellow("\u26A0") + ` Unknown model ${modelId} \u2014 saving for active engine anyway.`);
24525
+ console.log(
24526
+ " " + chalk60.yellow("\u26A0") + ` ${modelId} isn't in ${active}'s discovered list (` + paint("accent", "/model list") + `) \u2014 saving anyway.`
24527
+ );
24528
+ } else if (!cache2) {
24529
+ console.log();
24530
+ console.log(" " + chalk60.yellow("\u26A0") + ` No discovered models for ${active} yet (` + paint("accent", "/model refresh") + `) \u2014 saving anyway.`);
23321
24531
  }
23322
24532
  const persist = getBool(flags, "default");
23323
24533
  if (persist) {
@@ -23328,36 +24538,86 @@ async function handler42(args, ctx) {
23328
24538
  }
23329
24539
  console.log();
23330
24540
  console.log(
23331
- " " + paint("success", "\u2713") + ` Model: ${chalk59.bold(modelId)}` + (persist ? chalk59.dim(" (saved as default)") : chalk59.dim(" (this session)"))
24541
+ " " + paint("success", "\u2713") + ` Model: ${chalk60.bold(modelId)}` + (persist ? chalk60.dim(" (saved as default)") : chalk60.dim(" (this session)"))
23332
24542
  );
23333
- console.log(" " + chalk59.dim(`Stack: ${formatActiveStack(ctx)}`));
24543
+ console.log(" " + chalk60.dim(`Stack: ${formatActiveStack(ctx)}`));
23334
24544
  console.log();
23335
24545
  return;
23336
24546
  }
23337
24547
  const sessionOverride = ctx.llm?.modelOverride;
23338
24548
  const globalOverride = getConfigValue("llm-model-override");
23339
24549
  console.log();
23340
- console.log(chalk59.bold(" Model"));
24550
+ console.log(chalk60.bold(" Model"));
23341
24551
  if (sessionOverride) {
23342
24552
  console.log(` Session override: ${paint("accent", sessionOverride)}`);
23343
24553
  } else if (globalOverride) {
23344
24554
  console.log(` Config default: ${paint("accent", globalOverride)}`);
23345
24555
  } else {
23346
- console.log(" " + chalk59.dim("No override \u2014 tier defaults apply."));
24556
+ console.log(" " + chalk60.dim("No override \u2014 tier defaults apply."));
23347
24557
  }
23348
24558
  console.log(` Active stack: ${formatActiveStack(ctx)}`);
23349
- console.log(chalk59.dim(" /model set <id> \xB7 /model clear \xB7 /tier list \xB7 /provider list"));
24559
+ console.log(chalk60.dim(" /model list \xB7 /model set <id> \xB7 /model refresh \xB7 /model clear"));
24560
+ console.log();
24561
+ }
24562
+ function tierMarkers(cache2, modelId) {
24563
+ const tiers = Object.entries(cache2.tier_stack).filter(([, id]) => id === modelId).map(([tier]) => tier.toUpperCase());
24564
+ return tiers.length > 0 ? paint("accent", ` \u25C2 ${tiers.join("/")}`) : "";
24565
+ }
24566
+ function printModelList(ctx, showAll) {
24567
+ const active = resolveActiveProvider(ctx);
24568
+ const cache2 = getProviderModels(active);
24569
+ console.log();
24570
+ console.log(chalk60.bold(` Models \u2014 ${active}`));
24571
+ if (!cache2) {
24572
+ console.log(" " + chalk60.dim("Nothing discovered yet."));
24573
+ console.log(" " + chalk60.dim("Run ") + paint("accent", "/model refresh") + chalk60.dim(" (or ") + paint("accent", "/connect") + chalk60.dim(" to add the provider)."));
24574
+ console.log();
24575
+ return;
24576
+ }
24577
+ const fetchedAt = cache2.fetched_at.slice(0, 10);
24578
+ console.log(" " + chalk60.dim(`${cache2.models.length} chat models \xB7 discovered ${fetchedAt} \xB7 /model refresh to update`));
24579
+ console.log();
24580
+ const models = showAll ? cache2.models : cache2.models.slice(0, LIST_LIMIT);
24581
+ const noTools = new Set(cache2.quirks?.no_tools ?? []);
24582
+ for (const m of models) {
24583
+ const name = m.display_name && m.display_name !== m.id ? chalk60.dim(` \u2014 ${m.display_name}`) : "";
24584
+ const quirk = noTools.has(m.id) ? chalk60.yellow(" [no tools]") : "";
24585
+ console.log(` ${m.id}${name}${tierMarkers(cache2, m.id)}${quirk}`);
24586
+ }
24587
+ if (!showAll && cache2.models.length > models.length) {
24588
+ console.log(" " + chalk60.dim(`\u2026 and ${cache2.models.length - models.length} more (/model list --all)`));
24589
+ }
24590
+ console.log();
24591
+ console.log(" " + chalk60.dim("/model set <id> \u2014 pin one for this session (--default to persist)"));
24592
+ console.log();
24593
+ }
24594
+ async function refreshModels(ctx) {
24595
+ const active = resolveActiveProvider(ctx);
24596
+ const spinner = ora18({ text: `Discovering ${active} models\u2026`, discardStdin: false }).start();
24597
+ const entry = await refreshProviderModels(active, { force: true });
24598
+ if (!entry) {
24599
+ spinner.fail(`Couldn't reach ${active} to refresh models.`);
24600
+ console.log(" " + chalk60.dim("Check your connection and key, then retry. Cached models remain in use."));
24601
+ console.log();
24602
+ return;
24603
+ }
24604
+ spinner.succeed(`${active}: ${entry.models.length} chat models discovered.`);
24605
+ console.log(" " + chalk60.dim(`high ${entry.tier_stack.high} \xB7 medium ${entry.tier_stack.medium} \xB7 low ${entry.tier_stack.low}`));
24606
+ console.log(" " + chalk60.dim(`Stack: ${formatActiveStack(ctx)}`));
23350
24607
  console.log();
23351
24608
  }
24609
+ var LIST_LIMIT;
23352
24610
  var init_model = __esm({
23353
24611
  "src/commands/model.ts"() {
23354
24612
  "use strict";
23355
24613
  init_argparse();
23356
- init_catalog();
24614
+ init_discovery();
24615
+ init_models_cache();
23357
24616
  init_session_state();
23358
24617
  init_store();
23359
24618
  init_context2();
23360
24619
  init_theme();
24620
+ LIST_LIMIT = 40;
23361
24621
  }
23362
24622
  });
23363
24623
 
@@ -23369,22 +24629,22 @@ __export(update_check_exports, {
23369
24629
  loadUpdateCheckCache: () => loadUpdateCheckCache,
23370
24630
  saveUpdateCheckCache: () => saveUpdateCheckCache
23371
24631
  });
23372
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync14, unlinkSync as unlinkSync5, writeFileSync as writeFileSync17 } from "fs";
23373
- import { join as join26 } from "path";
23374
- function cachePath() {
23375
- return join26(ntrpHome(), "update-check.json");
24632
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync17, unlinkSync as unlinkSync5, writeFileSync as writeFileSync19 } from "fs";
24633
+ import { join as join28 } from "path";
24634
+ function cachePath2() {
24635
+ return join28(ntrpHome(), "update-check.json");
23376
24636
  }
23377
24637
  function ensureDir7() {
23378
24638
  const dir = ntrpHome();
23379
- if (!existsSync19(dir)) {
24639
+ if (!existsSync21(dir)) {
23380
24640
  mkdirSync12(dir, { recursive: true });
23381
24641
  }
23382
24642
  }
23383
24643
  function loadUpdateCheckCache() {
23384
- const path = cachePath();
23385
- if (!existsSync19(path)) return null;
24644
+ const path = cachePath2();
24645
+ if (!existsSync21(path)) return null;
23386
24646
  try {
23387
- const parsed = JSON.parse(readFileSync14(path, "utf-8"));
24647
+ const parsed = JSON.parse(readFileSync17(path, "utf-8"));
23388
24648
  if (!parsed || typeof parsed !== "object" || typeof parsed.lastCheck !== "number" || typeof parsed.latestVersion !== "string") {
23389
24649
  return null;
23390
24650
  }
@@ -23395,39 +24655,39 @@ function loadUpdateCheckCache() {
23395
24655
  }
23396
24656
  function saveUpdateCheckCache(cache2) {
23397
24657
  ensureDir7();
23398
- writeFileSync17(cachePath(), JSON.stringify(cache2, null, 2) + "\n");
24658
+ writeFileSync19(cachePath2(), JSON.stringify(cache2, null, 2) + "\n");
23399
24659
  }
23400
- function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS) {
24660
+ function isCacheFresh(cache2, ttlMs = CACHE_TTL_MS2) {
23401
24661
  if (!cache2) return false;
23402
24662
  return Date.now() - cache2.lastCheck < ttlMs;
23403
24663
  }
23404
24664
  function invalidateUpdateCheckCache() {
23405
- const path = cachePath();
23406
- if (existsSync19(path)) {
24665
+ const path = cachePath2();
24666
+ if (existsSync21(path)) {
23407
24667
  unlinkSync5(path);
23408
24668
  }
23409
24669
  }
23410
- var CACHE_TTL_MS;
24670
+ var CACHE_TTL_MS2;
23411
24671
  var init_update_check = __esm({
23412
24672
  "src/config/update-check.ts"() {
23413
24673
  "use strict";
23414
24674
  init_store();
23415
- CACHE_TTL_MS = 864e5;
24675
+ CACHE_TTL_MS2 = 864e5;
23416
24676
  }
23417
24677
  });
23418
24678
 
23419
24679
  // src/version.ts
23420
- import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
23421
- import { dirname as dirname4, join as join27 } from "path";
24680
+ import { existsSync as existsSync22, readFileSync as readFileSync18 } from "fs";
24681
+ import { dirname as dirname4, join as join29 } from "path";
23422
24682
  import { fileURLToPath } from "url";
23423
24683
  function getInstalledVersion() {
23424
24684
  if (cachedVersion) return cachedVersion;
23425
24685
  const start = dirname4(fileURLToPath(import.meta.url));
23426
24686
  for (const rel of ["../package.json", "../../package.json"]) {
23427
- const path = join27(start, rel);
23428
- if (!existsSync20(path)) continue;
24687
+ const path = join29(start, rel);
24688
+ if (!existsSync22(path)) continue;
23429
24689
  try {
23430
- const pkg = JSON.parse(readFileSync15(path, "utf-8"));
24690
+ const pkg = JSON.parse(readFileSync18(path, "utf-8"));
23431
24691
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
23432
24692
  cachedVersion = pkg.version;
23433
24693
  return cachedVersion;
@@ -23493,14 +24753,14 @@ function buildResult(current, latest) {
23493
24753
  async function checkForUpdate(options) {
23494
24754
  const current = getInstalledVersion();
23495
24755
  const timeoutMs = options?.timeoutMs ?? 5e3;
23496
- const cached = loadUpdateCheckCache();
23497
- if (!options?.force && isCacheFresh(cached)) {
23498
- return buildResult(current, cached.latestVersion);
24756
+ const cached2 = loadUpdateCheckCache();
24757
+ if (!options?.force && isCacheFresh(cached2)) {
24758
+ return buildResult(current, cached2.latestVersion);
23499
24759
  }
23500
24760
  const latest = await fetchLatestVersion(timeoutMs);
23501
24761
  if (!latest) {
23502
- if (cached?.latestVersion) {
23503
- return buildResult(current, cached.latestVersion);
24762
+ if (cached2?.latestVersion) {
24763
+ return buildResult(current, cached2.latestVersion);
23504
24764
  }
23505
24765
  return null;
23506
24766
  }
@@ -23521,10 +24781,10 @@ var init_registry = __esm({
23521
24781
  // src/commands/update.ts
23522
24782
  var update_exports = {};
23523
24783
  __export(update_exports, {
23524
- handler: () => handler43
24784
+ handler: () => handler44
23525
24785
  });
23526
24786
  import { spawnSync } from "child_process";
23527
- import chalk60 from "chalk";
24787
+ import chalk61 from "chalk";
23528
24788
  function tailLines(text, count = 5) {
23529
24789
  return text.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).slice(-count).join("\n");
23530
24790
  }
@@ -23540,19 +24800,19 @@ function runGlobalInstall() {
23540
24800
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
23541
24801
  return { ok: result.status === 0, output };
23542
24802
  }
23543
- async function handler43(_args, _ctx) {
24803
+ async function handler44(_args, _ctx) {
23544
24804
  const current = getInstalledVersion();
23545
24805
  const latest = await fetchLatestVersion(1e4);
23546
24806
  if (!latest) {
23547
24807
  console.log();
23548
- console.log(chalk60.yellow(" Could not reach the npm registry."));
23549
- console.log(chalk60.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
24808
+ console.log(chalk61.yellow(" Could not reach the npm registry."));
24809
+ console.log(chalk61.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
23550
24810
  console.log();
23551
24811
  return;
23552
24812
  }
23553
24813
  if (!isNewerVersion(latest, current)) {
23554
24814
  console.log();
23555
- console.log(chalk60.green(` \u2713 You're on the latest version (v${current})`));
24815
+ console.log(chalk61.green(` \u2713 You're on the latest version (v${current})`));
23556
24816
  console.log();
23557
24817
  return;
23558
24818
  }
@@ -23561,24 +24821,24 @@ async function handler43(_args, _ctx) {
23561
24821
  const { ok, output } = runGlobalInstall();
23562
24822
  if (ok) {
23563
24823
  invalidateUpdateCheckCache();
23564
- console.log(chalk60.green(` \u2713 Updated! Restart NTRP to use v${latest}`));
24824
+ console.log(chalk61.green(` \u2713 Updated! Restart NTRP to use v${latest}`));
23565
24825
  console.log();
23566
24826
  return;
23567
24827
  }
23568
24828
  const lower = output.toLowerCase();
23569
24829
  if (lower.includes("eacces") || lower.includes("permission denied") || lower.includes("eperm")) {
23570
- console.log(chalk60.red(` Could not install ${NPM_PACKAGE} (permission denied).`));
23571
- console.log(chalk60.dim(` Try: sudo npm install -g ${NPM_PACKAGE}`));
23572
- console.log(chalk60.dim(` Or fix npm global permissions: ${PERMISSIONS_URL}`));
24830
+ console.log(chalk61.red(` Could not install ${NPM_PACKAGE} (permission denied).`));
24831
+ console.log(chalk61.dim(` Try: sudo npm install -g ${NPM_PACKAGE}`));
24832
+ console.log(chalk61.dim(` Or fix npm global permissions: ${PERMISSIONS_URL}`));
23573
24833
  console.log();
23574
24834
  return;
23575
24835
  }
23576
24836
  const detail = tailLines(output);
23577
- console.log(chalk60.red(` Could not install ${NPM_PACKAGE}.`));
24837
+ console.log(chalk61.red(` Could not install ${NPM_PACKAGE}.`));
23578
24838
  if (detail) {
23579
- console.log(chalk60.dim(` ${detail.split("\n").join("\n ")}`));
24839
+ console.log(chalk61.dim(` ${detail.split("\n").join("\n ")}`));
23580
24840
  }
23581
- console.log(chalk60.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
24841
+ console.log(chalk61.dim(` Try manually: npm install -g ${NPM_PACKAGE}`));
23582
24842
  console.log();
23583
24843
  }
23584
24844
  var PERMISSIONS_URL;
@@ -23593,10 +24853,10 @@ var init_update = __esm({
23593
24853
  });
23594
24854
 
23595
24855
  // src/output/progress-report.ts
23596
- import chalk61 from "chalk";
24856
+ import chalk62 from "chalk";
23597
24857
  function printCard(title, rows) {
23598
24858
  const inner = CARD_W - 4;
23599
- const border = chalk61.dim;
24859
+ const border = chalk62.dim;
23600
24860
  console.log();
23601
24861
  console.log(` ${border(`\u256D${"\u2500".repeat(CARD_W - 2)}\u256E`)}`);
23602
24862
  console.log(` ${border("\u2502 ")}${padRight(sectionHeading(title), inner)}${border(" \u2502")}`);
@@ -23612,7 +24872,7 @@ function formatTokens(n) {
23612
24872
  return String(n);
23613
24873
  }
23614
24874
  function sparkline(values) {
23615
- if (values.length === 0) return chalk61.dim("(no activity yet)");
24875
+ if (values.length === 0) return chalk62.dim("(no activity yet)");
23616
24876
  const max = Math.max(...values, 1);
23617
24877
  const blocks = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
23618
24878
  return values.map((v) => {
@@ -23621,7 +24881,7 @@ function sparkline(values) {
23621
24881
  }).join("");
23622
24882
  }
23623
24883
  function formatMemberSince(iso) {
23624
- if (!iso) return chalk61.dim("\u2014");
24884
+ if (!iso) return chalk62.dim("\u2014");
23625
24885
  const d = new Date(iso);
23626
24886
  return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
23627
24887
  }
@@ -23638,49 +24898,49 @@ function renderProgressReport() {
23638
24898
  state.milestones_unlocked.length,
23639
24899
  TIME_MILESTONES.length
23640
24900
  );
23641
- const { usage: usage2 } = summary;
24901
+ const { usage: usage3 } = summary;
23642
24902
  const nextLabel = bank.next_milestone ? `${formatHoursLabel(bank.total_hours)} \u2192 ${formatHoursLabel(bank.next_milestone.hours)}` : `${formatHoursLabel(bank.total_hours)} saved`;
23643
24903
  const bar = inlineBar(bank.progress_pct, 18);
23644
24904
  printCard("Progress", [
23645
- `${chalk61.dim("Hours saved")} ${paint("accent", formatHoursLabel(bank.total_hours))} ${bar}`,
23646
- `${chalk61.dim("Next milestone")} ${bank.next_milestone ? paint("accent", bank.next_milestone.title) : chalk61.dim("top of ladder")}`,
23647
- `${chalk61.dim("Member since")} ${formatMemberSince(usage2.first_active_at)}`,
23648
- `${chalk61.dim("Last active")} ${formatMemberSince(usage2.last_active_at)}`
24905
+ `${chalk62.dim("Hours saved")} ${paint("accent", formatHoursLabel(bank.total_hours))} ${bar}`,
24906
+ `${chalk62.dim("Next milestone")} ${bank.next_milestone ? paint("accent", bank.next_milestone.title) : chalk62.dim("top of ladder")}`,
24907
+ `${chalk62.dim("Member since")} ${formatMemberSince(usage3.first_active_at)}`,
24908
+ `${chalk62.dim("Last active")} ${formatMemberSince(usage3.last_active_at)}`
23649
24909
  ]);
23650
24910
  if (bank.perspective_line) {
23651
- console.log(` ${chalk61.dim.italic(bank.perspective_line)}`);
24911
+ console.log(` ${chalk62.dim.italic(bank.perspective_line)}`);
23652
24912
  }
23653
24913
  printCard("Activity", [
23654
- `${chalk61.dim("Sessions")} ${chalk61.bold(String(summary.total_sessions_on_disk))} total \xB7 ${summary.sessions_with_work} with work \xB7 ${usage2.sessions_closed} closed`,
23655
- `${chalk61.dim("Diagnoses")} ${chalk61.bold(String(usage2.diagnoses))}`,
23656
- `${chalk61.dim("Metrics runs")} ${chalk61.bold(String(usage2.metrics_runs))}`,
23657
- `${chalk61.dim("Deliverables")} ${chalk61.bold(String(usage2.deliverables))}`,
23658
- `${chalk61.dim("AI exchanges")} ${chalk61.bold(String(usage2.nl_exchanges))}`
24914
+ `${chalk62.dim("Sessions")} ${chalk62.bold(String(summary.total_sessions_on_disk))} total \xB7 ${summary.sessions_with_work} with work \xB7 ${usage3.sessions_closed} closed`,
24915
+ `${chalk62.dim("Diagnoses")} ${chalk62.bold(String(usage3.diagnoses))}`,
24916
+ `${chalk62.dim("Metrics runs")} ${chalk62.bold(String(usage3.metrics_runs))}`,
24917
+ `${chalk62.dim("Deliverables")} ${chalk62.bold(String(usage3.deliverables))}`,
24918
+ `${chalk62.dim("AI exchanges")} ${chalk62.bold(String(usage3.nl_exchanges))}`
23659
24919
  ]);
23660
- const totalTokens = usage2.input_tokens + usage2.output_tokens;
24920
+ const totalTokens = usage3.input_tokens + usage3.output_tokens;
23661
24921
  printCard("AI usage", [
23662
- `${chalk61.dim("LLM calls")} ${chalk61.bold(String(usage2.llm_calls))}`,
23663
- `${chalk61.dim("Tokens")} ${chalk61.bold(formatTokens(totalTokens))} in+out (${formatTokens(usage2.input_tokens)} in \xB7 ${formatTokens(usage2.output_tokens)} out)`
24922
+ `${chalk62.dim("LLM calls")} ${chalk62.bold(String(usage3.llm_calls))}`,
24923
+ `${chalk62.dim("Tokens")} ${chalk62.bold(formatTokens(totalTokens))} in+out (${formatTokens(usage3.input_tokens)} in \xB7 ${formatTokens(usage3.output_tokens)} out)`
23664
24924
  ]);
23665
- const weeks = [...usage2.weekly].sort((a, b) => a.week.localeCompare(b.week)).slice(-8);
24925
+ const weeks = [...usage3.weekly].sort((a, b) => a.week.localeCompare(b.week)).slice(-8);
23666
24926
  const weekHours = weeks.map((w) => w.minutes_saved / 60);
23667
24927
  const weekLabels = weeks.map((w) => w.week.replace(/^\d{4}-/, ""));
23668
24928
  console.log();
23669
24929
  console.log(` ${sectionHeading("Weekly hours saved")}`);
23670
24930
  console.log(` ${sparkline(weekHours)}`);
23671
24931
  if (weeks.length > 0) {
23672
- console.log(` ${chalk61.dim(weekLabels.join(" "))}`);
24932
+ console.log(` ${chalk62.dim(weekLabels.join(" "))}`);
23673
24933
  }
23674
24934
  console.log();
23675
24935
  console.log(` ${sectionHeading("Milestone ladder")}`);
23676
24936
  for (const m of TIME_MILESTONES) {
23677
24937
  const unlocked = state.milestones_unlocked.includes(m.id);
23678
24938
  const pct = Math.min(100, bank.total_hours / m.hours * 100);
23679
- const mark = unlocked ? badge("DONE", "success") : bank.total_hours >= m.hours * 0.85 ? badge("NEAR", "warning") : chalk61.dim("\u25CB");
24939
+ const mark = unlocked ? badge("DONE", "success") : bank.total_hours >= m.hours * 0.85 ? badge("NEAR", "warning") : chalk62.dim("\u25CB");
23680
24940
  const barW = 12;
23681
- const mBar = unlocked ? chalk61.hex("#22c55e")("\u2588".repeat(barW)) : scoreBar(pct, bank.total_hours >= m.hours ? "green" : pct >= 50 ? "yellow" : "red", barW);
24941
+ const mBar = unlocked ? chalk62.hex("#22c55e")("\u2588".repeat(barW)) : scoreBar(pct, bank.total_hours >= m.hours ? "green" : pct >= 50 ? "yellow" : "red", barW);
23682
24942
  const label = `${m.title}`.padEnd(16);
23683
- console.log(` ${mark} ${chalk61.dim(label)} ${mBar} ${chalk61.dim(`${m.hours}h`)}`);
24943
+ console.log(` ${mark} ${chalk62.dim(label)} ${mBar} ${chalk62.dim(`${m.hours}h`)}`);
23684
24944
  }
23685
24945
  console.log();
23686
24946
  }
@@ -23702,17 +24962,17 @@ var init_progress_report = __esm({
23702
24962
  // src/commands/progress.ts
23703
24963
  var progress_exports = {};
23704
24964
  __export(progress_exports, {
23705
- handler: () => handler44
24965
+ handler: () => handler45
23706
24966
  });
23707
- import chalk62 from "chalk";
24967
+ import chalk63 from "chalk";
23708
24968
  function printProgressResetPreamble() {
23709
24969
  console.log();
23710
- console.log(" " + chalk62.yellow.bold("This will permanently remove:"));
23711
- console.log(" " + chalk62.dim(" \u2022 Hours saved and milestone unlocks"));
23712
- console.log(" " + chalk62.dim(" \u2022 Usage counters and weekly activity rollups"));
23713
- console.log(" " + chalk62.dim(" \u2022 Credit history used for dedup"));
24970
+ console.log(" " + chalk63.yellow.bold("This will permanently remove:"));
24971
+ console.log(" " + chalk63.dim(" \u2022 Hours saved and milestone unlocks"));
24972
+ console.log(" " + chalk63.dim(" \u2022 Usage counters and weekly activity rollups"));
24973
+ console.log(" " + chalk63.dim(" \u2022 Credit history used for dedup"));
23714
24974
  console.log();
23715
- console.log(" " + chalk62.dim("Preserved: install identity (install.json)"));
24975
+ console.log(" " + chalk63.dim("Preserved: install identity (install.json)"));
23716
24976
  console.log();
23717
24977
  }
23718
24978
  function showProgress() {
@@ -23730,7 +24990,7 @@ async function handleReset(ctx, confirmedFlag) {
23730
24990
  const bank = getTimeBankSummary();
23731
24991
  if (bank.total_minutes <= 0) {
23732
24992
  console.log();
23733
- console.log(" " + chalk62.dim("No progress to reset."));
24993
+ console.log(" " + chalk63.dim("No progress to reset."));
23734
24994
  console.log();
23735
24995
  return "No progress to reset";
23736
24996
  }
@@ -23747,11 +25007,11 @@ async function handleReset(ctx, confirmedFlag) {
23747
25007
  }
23748
25008
  resetProgress();
23749
25009
  console.log();
23750
- console.log(" " + paint("accent", "\u2713 Progress reset") + chalk62.dim(" \u2014 hours and milestones cleared."));
25010
+ console.log(" " + paint("accent", "\u2713 Progress reset") + chalk63.dim(" \u2014 hours and milestones cleared."));
23751
25011
  console.log();
23752
25012
  return "Progress reset";
23753
25013
  }
23754
- async function handler44(args, ctx) {
25014
+ async function handler45(args, ctx) {
23755
25015
  const { positional, flags } = parseArgs2(args, ["confirm"]);
23756
25016
  const sub = positional[0]?.toLowerCase();
23757
25017
  if (sub === "reset") {
@@ -23759,7 +25019,7 @@ async function handler44(args, ctx) {
23759
25019
  }
23760
25020
  if (sub && sub !== "reset") {
23761
25021
  console.log();
23762
- console.log(" " + chalk62.dim("Unknown subcommand. Try ") + paint("accent", "/progress") + chalk62.dim(" or ") + paint("accent", "/progress reset") + chalk62.dim("."));
25022
+ console.log(" " + chalk63.dim("Unknown subcommand. Try ") + paint("accent", "/progress") + chalk63.dim(" or ") + paint("accent", "/progress reset") + chalk63.dim("."));
23763
25023
  console.log();
23764
25024
  return;
23765
25025
  }
@@ -23866,10 +25126,10 @@ async function resolveHandler(name) {
23866
25126
  try {
23867
25127
  const mod = await importHandler(runtimePath);
23868
25128
  if (!mod) return null;
23869
- const handler45 = mod.handler;
23870
- if (typeof handler45 !== "function") return null;
23871
- entry.handler = handler45;
23872
- return handler45;
25129
+ const handler46 = mod.handler;
25130
+ if (typeof handler46 !== "function") return null;
25131
+ entry.handler = handler46;
25132
+ return handler46;
23873
25133
  } catch (err) {
23874
25134
  console.error(`Failed to load handler for /${name}:`, err);
23875
25135
  return null;
@@ -23955,6 +25215,8 @@ async function importHandler(runtimePath) {
23955
25215
  return Promise.resolve().then(() => (init_switch(), switch_exports));
23956
25216
  case "../commands/backmeup.js":
23957
25217
  return Promise.resolve().then(() => (init_backmeup(), backmeup_exports));
25218
+ case "../commands/connect.js":
25219
+ return Promise.resolve().then(() => (init_connect2(), connect_exports2));
23958
25220
  case "../commands/provider.js":
23959
25221
  return Promise.resolve().then(() => (init_provider(), provider_exports));
23960
25222
  case "../commands/tier.js":
@@ -24082,7 +25344,9 @@ handler: ../commands/setup.ts
24082
25344
 
24083
25345
  Validate local readiness or configure NTRP non-interactively for automation.
24084
25346
  \`setup check --json\` reports license, profile, API key, database, and writable
24085
- directory state. \`setup agent\` accepts a profile JSON file or direct flags.`
25347
+ directory state. \`setup agent\` accepts a profile JSON file or direct flags \u2014
25348
+ \`--llm-key <key>\` auto-detects the provider from any pasted key
25349
+ (\`--llm-provider <id>\` to force one).`
24086
25350
  },
24087
25351
  {
24088
25352
  name: "update",
@@ -24518,6 +25782,25 @@ handler: ../commands/profile.ts
24518
25782
 
24519
25783
  Choose a sales motion preset (PLG, SMB Velocity, Mid-Market, Enterprise). Each
24520
25784
  preset adjusts the vital-sign thresholds to match your deal cycle.`
25785
+ },
25786
+ {
25787
+ name: "connect",
25788
+ raw: `---
25789
+ name: connect
25790
+ description: Connect an AI provider (paste any key)
25791
+ section: Settings
25792
+ args: [provider] [--key <key>] [--base-url <url> --id <name>]
25793
+ handler: ../commands/connect.ts
25794
+ ---
25795
+
25796
+ Paste any provider's API key \u2014 NTRP identifies the provider from the key
25797
+ format (probing ambiguous ones), validates it, discovers which models the key
25798
+ can use, and builds the HIGH/MEDIUM/LOW tier stack automatically.
25799
+
25800
+ Works with Anthropic, OpenAI, Google Gemini, Groq, Mistral, DeepSeek, xAI,
25801
+ OpenRouter, Together, and Fireworks out of the box. \`/connect ollama\` wires a
25802
+ local Ollama; \`/connect --base-url <url> --id <name>\` registers any other
25803
+ OpenAI-compatible endpoint.`
24521
25804
  },
24522
25805
  {
24523
25806
  name: "config",
@@ -24530,10 +25813,12 @@ handler: ../commands/config.ts
24530
25813
  ---
24531
25814
 
24532
25815
  Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
24533
- \`api-key\` (Anthropic), \`openai-api-key\`, \`llm-primary\` (default engine),
24534
- \`llm-tier\`, \`llm-auto-failover\`, \`default-format\`, \`export-dir\`.
25816
+ \`api-key\` (Anthropic), \`openai-api-key\` (and \`groq-api-key\`, \`google-api-key\`, ...),
25817
+ \`llm-primary\` (default engine), \`llm-tier\`, \`llm-auto-failover\`,
25818
+ \`default-format\`, \`export-dir\`.
24535
25819
 
24536
- \`/config set api-key\` and \`/config set openai-api-key\` open hidden prompts.`
25820
+ Setting a provider key opens a hidden prompt and auto-discovers that
25821
+ provider's models. Prefer \`/connect\` \u2014 it detects the provider for you.`
24537
25822
  },
24538
25823
  {
24539
25824
  name: "provider",
@@ -24541,13 +25826,14 @@ Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
24541
25826
  name: provider
24542
25827
  description: Switch active LLM engine
24543
25828
  section: Settings
24544
- args: [anthropic|openai|list|reset|save|failover on|off]
25829
+ args: [<id>|list|reset|save|failover on|off]
24545
25830
  handler: ../commands/provider.ts
24546
25831
  ---
24547
25832
 
24548
- Choose which engine answers this session \u2014 Anthropic or OpenAI when both keys
24549
- are configured. Session-scoped by default; \`/provider save\` writes the default
24550
- to config. \`/provider failover on\` enables rate-limit auto-failover.`
25833
+ Choose which connected engine answers this session \u2014 any provider added via
25834
+ \`/connect\` (anthropic, openai, groq, google, ollama, custom endpoints, ...).
25835
+ Session-scoped by default; \`/provider save\` writes the default to config.
25836
+ \`/provider failover on\` enables rate-limit auto-failover.`
24551
25837
  },
24552
25838
  {
24553
25839
  name: "tier",
@@ -24569,12 +25855,14 @@ active stack. Add \`--default\` to persist to config.`
24569
25855
  name: model
24570
25856
  description: Override the active LLM model
24571
25857
  section: Settings
24572
- args: [set <id>|clear] [--default]
25858
+ args: [list|set <id>|refresh|clear] [--default]
24573
25859
  handler: ../commands/model.ts
24574
25860
  ---
24575
25861
 
24576
- Pin a catalog model on the **active engine** (e.g. claude-opus-4-6 on anthropic).
24577
- Cross-provider IDs are rejected \u2014 switch with \`/provider\` first.`
25862
+ \`/model list\` shows the models discovered for the active engine with their
25863
+ tier assignments. \`/model refresh\` re-discovers the live list. \`/model set <id>\`
25864
+ pins a model on the **active engine**; cross-provider IDs are rejected \u2014
25865
+ switch with \`/provider\` first.`
24578
25866
  },
24579
25867
  {
24580
25868
  name: "activate",
@@ -24633,7 +25921,7 @@ paragraph that flows into all AI surfaces.`
24633
25921
  });
24634
25922
 
24635
25923
  // src/license/activation.ts
24636
- import chalk63 from "chalk";
25924
+ import chalk64 from "chalk";
24637
25925
  function hasValidLicense() {
24638
25926
  return checkLicense().valid;
24639
25927
  }
@@ -24641,10 +25929,10 @@ async function ensureLicenseActivated(ctx) {
24641
25929
  if (hasValidLicense()) return false;
24642
25930
  if (!process.stdin.isTTY) {
24643
25931
  console.error();
24644
- console.error(chalk63.red(" A license key is required."));
24645
- console.error(chalk63.dim(` Sign up: ${getCheckoutUrl()}`));
24646
- console.error(chalk63.dim(" Then run: ntrp activate <key>"));
24647
- console.error(chalk63.dim(" Or set NTRP_LICENSE_KEY for headless use."));
25932
+ console.error(chalk64.red(" A license key is required."));
25933
+ console.error(chalk64.dim(` Sign up: ${getCheckoutUrl()}`));
25934
+ console.error(chalk64.dim(" Then run: ntrp activate <key>"));
25935
+ console.error(chalk64.dim(" Or set NTRP_LICENSE_KEY for headless use."));
24648
25936
  console.error();
24649
25937
  process.exit(1);
24650
25938
  }
@@ -24654,7 +25942,7 @@ async function ensureLicenseActivated(ctx) {
24654
25942
  }
24655
25943
  printCenteredLogo();
24656
25944
  console.log(" " + bold("Activate your license"));
24657
- console.log(" " + chalk63.dim("Don't have a key yet? Sign up (free trial or Pro), then paste it below."));
25945
+ console.log(" " + chalk64.dim("Don't have a key yet? Sign up (free trial or Pro), then paste it below."));
24658
25946
  console.log();
24659
25947
  await promptOpenCheckout(ctx);
24660
25948
  return promptForLicenseKey(ctx);
@@ -24681,6 +25969,7 @@ var init_gate2 = __esm({
24681
25969
  UNGATED_COMMANDS = /* @__PURE__ */ new Set([
24682
25970
  "activate",
24683
25971
  "config",
25972
+ "connect",
24684
25973
  "profile",
24685
25974
  "onboard",
24686
25975
  "setup",
@@ -24705,7 +25994,7 @@ var router_exports = {};
24705
25994
  __export(router_exports, {
24706
25995
  conversationRouter: () => conversationRouter
24707
25996
  });
24708
- import chalk64 from "chalk";
25997
+ import chalk65 from "chalk";
24709
25998
  async function conversationRouter(input, ctx) {
24710
25999
  if (ctx.oneShot || (ctx.wizardDepth ?? 0) > 0) {
24711
26000
  return { handled: false };
@@ -24715,7 +26004,7 @@ async function conversationRouter(input, ctx) {
24715
26004
  if (FRESH_START_RE.test(line)) {
24716
26005
  console.log();
24717
26006
  console.log(
24718
- " " + chalk64.dim("Start a fresh analysis? This keeps prior sessions \u2014 say ") + chalk64.cyan("yes") + chalk64.dim(" to confirm or ") + chalk64.cyan("/home") + chalk64.dim(" for the dashboard.")
26007
+ " " + chalk65.dim("Start a fresh analysis? This keeps prior sessions \u2014 say ") + chalk65.cyan("yes") + chalk65.dim(" to confirm or ") + chalk65.cyan("/home") + chalk65.dim(" for the dashboard.")
24719
26008
  );
24720
26009
  console.log();
24721
26010
  return { handled: true };
@@ -24749,7 +26038,7 @@ async function conversationRouter(input, ctx) {
24749
26038
  }
24750
26039
  if (phase === "compute") {
24751
26040
  console.log();
24752
- console.log(" " + chalk64.dim("Analysis running \u2014 wait for it to finish before typing another question."));
26041
+ console.log(" " + chalk65.dim("Analysis running \u2014 wait for it to finish before typing another question."));
24753
26042
  console.log();
24754
26043
  return { handled: true };
24755
26044
  }
@@ -24789,7 +26078,7 @@ var init_router = __esm({
24789
26078
  });
24790
26079
 
24791
26080
  // src/cli/dispatch.ts
24792
- import chalk65 from "chalk";
26081
+ import chalk66 from "chalk";
24793
26082
  function printLicenseRequired(command) {
24794
26083
  printLicenseBlocked(command);
24795
26084
  }
@@ -24855,7 +26144,7 @@ async function dispatch(input, ctx) {
24855
26144
  if (tokens.length === 1) {
24856
26145
  if (/^\d$/.test(first)) {
24857
26146
  console.log(
24858
- " " + chalk65.dim("Looks like a menu pick \u2014 run ") + paint("accent", "/new") + chalk65.dim(" to start (pick Demo, then choose your analysis type).")
26147
+ " " + chalk66.dim("Looks like a menu pick \u2014 run ") + paint("accent", "/new") + chalk66.dim(" to start (pick Demo, then choose your analysis type).")
24859
26148
  );
24860
26149
  return { kind: "handled" };
24861
26150
  }
@@ -24878,22 +26167,22 @@ async function dispatch(input, ctx) {
24878
26167
  return { kind: "handled", summary };
24879
26168
  }
24880
26169
  console.log(
24881
- " " + chalk65.dim("Not in Q&A yet \u2014 confirm scope, load data, and run analysis first. Type ") + paint("accent", "/home") + chalk65.dim(" for status.")
26170
+ " " + chalk66.dim("Not in Q&A yet \u2014 confirm scope, load data, and run analysis first. Type ") + paint("accent", "/home") + chalk66.dim(" for status.")
24882
26171
  );
24883
26172
  return { kind: "handled" };
24884
26173
  }
24885
26174
  console.log(
24886
- " " + chalk65.dim("Natural-language questions run in the interactive REPL. Start with ") + paint("accent", "ntrp") + chalk65.dim(" and ask after analysis.")
26175
+ " " + chalk66.dim("Natural-language questions run in the interactive REPL. Start with ") + paint("accent", "ntrp") + chalk66.dim(" and ask after analysis.")
24887
26176
  );
24888
26177
  return { kind: "handled" };
24889
26178
  }
24890
26179
  async function runSlashCommand(name, args, ctx) {
24891
- const handler45 = await resolveHandler(name);
24892
- if (!handler45) {
24893
- console.error(chalk65.red(` Unknown command: /${name}`));
26180
+ const handler46 = await resolveHandler(name);
26181
+ if (!handler46) {
26182
+ console.error(chalk66.red(` Unknown command: /${name}`));
24894
26183
  return void 0;
24895
26184
  }
24896
- const result = await handler45(args, ctx);
26185
+ const result = await handler46(args, ctx);
24897
26186
  return result ?? void 0;
24898
26187
  }
24899
26188
  async function runNaturalLanguage2(input, ctx) {
@@ -24922,7 +26211,7 @@ __export(welcome_exports, {
24922
26211
  GRADIENT: () => GRADIENT,
24923
26212
  printWelcome: () => printWelcome
24924
26213
  });
24925
- import chalk66 from "chalk";
26214
+ import chalk67 from "chalk";
24926
26215
  function resolveSessionSummary(input) {
24927
26216
  if (input.scope?.intent_summary?.trim()) return input.scope.intent_summary.trim();
24928
26217
  if (input.summary?.trim()) return input.summary.trim();
@@ -24956,19 +26245,19 @@ function sessionSummaryText(s) {
24956
26245
  summary: s.summary,
24957
26246
  dataset: s.dataset
24958
26247
  });
24959
- return summary === NO_SUMMARY ? chalk66.dim(summary) : summary;
26248
+ return summary === NO_SUMMARY ? chalk67.dim(summary) : summary;
24960
26249
  }
24961
26250
  function formatLastSessionLine(s, colW, ctx, opts) {
24962
26251
  const phase = sessionPhaseLabel(s, ctx);
24963
- const current = opts?.markCurrent && s.id === ctx?.sessionId ? chalk66.dim(" \xB7 current") : "";
24964
- const meta = `${formatSessionId(s.id, s.name)} ${chalk66.dim("\xB7")} ${chalk66.dim(lensBadgeLabel(s.analysis))} ${chalk66.dim("\xB7")} ${paint("accent", phase)} ${chalk66.dim("\xB7")} ${sessionSummaryText(s)}${current}`;
26252
+ const current = opts?.markCurrent && s.id === ctx?.sessionId ? chalk67.dim(" \xB7 current") : "";
26253
+ const meta = `${formatSessionId(s.id, s.name)} ${chalk67.dim("\xB7")} ${chalk67.dim(lensBadgeLabel(s.analysis))} ${chalk67.dim("\xB7")} ${paint("accent", phase)} ${chalk67.dim("\xB7")} ${sessionSummaryText(s)}${current}`;
24965
26254
  return truncateVisible(` ${meta}`, colW);
24966
26255
  }
24967
26256
  function formatActiveSessionLine(s, colW, ctx, opts) {
24968
26257
  const indent = " ";
24969
26258
  const idPart = formatSessionId(s.id, s.name);
24970
- const status = chalk66.dim(` \xB7 ${sessionStatusSuffix(s)}`);
24971
- const current = opts?.markCurrent && s.id === ctx?.sessionId ? chalk66.dim(" \xB7 current") : "";
26259
+ const status = chalk67.dim(` \xB7 ${sessionStatusSuffix(s)}`);
26260
+ const current = opts?.markCurrent && s.id === ctx?.sessionId ? chalk67.dim(" \xB7 current") : "";
24972
26261
  const suffix = `${status}${current}`;
24973
26262
  const summaryBudget = Math.max(8, colW - visibleWidth(indent) - visibleWidth(idPart) - visibleWidth(suffix) - 2);
24974
26263
  const summaryPart = truncateVisible(sessionSummaryText(s), summaryBudget);
@@ -25005,13 +26294,13 @@ function buildSystemLines(colW, statusRows, recent) {
25005
26294
  const lines = [""];
25006
26295
  lines.push(sectionHeading("System"));
25007
26296
  for (const item of statusRows) {
25008
- const label = chalk66.dim(padRight(item.label, 8));
26297
+ const label = chalk67.dim(padRight(item.label, 8));
25009
26298
  const state = padRight(item.state, 10);
25010
26299
  const detailW = Math.max(1, colW - 21);
25011
- lines.push(`${label} ${state} ${chalk66.dim(truncateVisible(item.detail, detailW))}`);
26300
+ lines.push(`${label} ${state} ${chalk67.dim(truncateVisible(item.detail, detailW))}`);
25012
26301
  }
25013
26302
  if (recent) {
25014
- lines.push(`${chalk66.dim(padRight("last used", 8))} ${chalk66.dim(recent)}`);
26303
+ lines.push(`${chalk67.dim(padRight("last used", 8))} ${chalk67.dim(recent)}`);
25015
26304
  }
25016
26305
  return lines;
25017
26306
  }
@@ -25021,7 +26310,7 @@ function buildHelpLines(colW, unfinishedCount) {
25021
26310
  lines.push(sectionHeading(section.heading));
25022
26311
  for (const entry of section.entries) {
25023
26312
  const desc = entry.dynamicDescription ? entry.dynamicDescription(unfinishedCount) : entry.description;
25024
- const text = ` ${paint("accent", entry.command)} ${chalk66.dim(desc)}`;
26313
+ const text = ` ${paint("accent", entry.command)} ${chalk67.dim(desc)}`;
25025
26314
  lines.push(truncateVisible(text, colW));
25026
26315
  }
25027
26316
  }
@@ -25031,10 +26320,10 @@ function buildLastSessionLines(colW, ctx, lastSession, nextAction, emptyDataHint
25031
26320
  const lines = [""];
25032
26321
  lines.push(sectionHeading("Last Session"));
25033
26322
  if (!lastSession) {
25034
- lines.push(` ${chalk66.dim("(none yet)")}`);
26323
+ lines.push(` ${chalk67.dim("(none yet)")}`);
25035
26324
  lines.push(
25036
26325
  truncateVisible(
25037
- ` ${nextAction.command ? actionHint(nextAction.label, nextAction.command, nextAction.detail) : `${chalk66.dim(nextAction.label)} ${chalk66.dim(nextAction.detail)}`}`,
26326
+ ` ${nextAction.command ? actionHint(nextAction.label, nextAction.command, nextAction.detail) : `${chalk67.dim(nextAction.label)} ${chalk67.dim(nextAction.detail)}`}`,
25038
26327
  colW
25039
26328
  )
25040
26329
  );
@@ -25045,7 +26334,7 @@ function buildLastSessionLines(colW, ctx, lastSession, nextAction, emptyDataHint
25045
26334
  if (!isCurrent) {
25046
26335
  lines.push(
25047
26336
  truncateVisible(
25048
- ` ${chalk66.dim("Resume:")} ${paint("accent", `/session ${lastSession.id.slice(-4)}`)}`,
26337
+ ` ${chalk67.dim("Resume:")} ${paint("accent", `/session ${lastSession.id.slice(-4)}`)}`,
25049
26338
  colW
25050
26339
  )
25051
26340
  );
@@ -25054,7 +26343,7 @@ function buildLastSessionLines(colW, ctx, lastSession, nextAction, emptyDataHint
25054
26343
  truncateVisible(` ${actionHint(nextAction.label, nextAction.command, nextAction.detail)}`, colW)
25055
26344
  );
25056
26345
  } else {
25057
- lines.push(truncateVisible(` ${chalk66.dim(nextAction.label)} ${chalk66.dim(nextAction.detail)}`, colW));
26346
+ lines.push(truncateVisible(` ${chalk67.dim(nextAction.label)} ${chalk67.dim(nextAction.detail)}`, colW));
25058
26347
  }
25059
26348
  if (isCurrent && emptyDataHint) {
25060
26349
  lines.push(truncateVisible(` ${emptyDataHint}`, colW));
@@ -25065,14 +26354,14 @@ function buildActiveSessionsLines(colW, ctx, activeSessions) {
25065
26354
  const lines = [""];
25066
26355
  lines.push(sectionHeading("Active Sessions"));
25067
26356
  if (activeSessions.length === 0) {
25068
- lines.push(` ${chalk66.dim("(none in progress)")}`);
26357
+ lines.push(` ${chalk67.dim("(none in progress)")}`);
25069
26358
  return lines;
25070
26359
  }
25071
26360
  for (const s of activeSessions.slice(0, 5)) {
25072
26361
  lines.push(formatActiveSessionLine(s, colW, ctx, { markCurrent: true }));
25073
26362
  }
25074
26363
  if (activeSessions.length > 5) {
25075
- lines.push(` ${chalk66.dim(`+${activeSessions.length - 5} more \xB7 `)}${paint("accent", "/session")}`);
26364
+ lines.push(` ${chalk67.dim(`+${activeSessions.length - 5} more \xB7 `)}${paint("accent", "/session")}`);
25076
26365
  }
25077
26366
  return lines;
25078
26367
  }
@@ -25080,7 +26369,7 @@ function buildProgressLines(colW) {
25080
26369
  const summary = getTimeBankSummary();
25081
26370
  const lines = [""];
25082
26371
  const heading = sectionHeading("Progress");
25083
- const hint = `${paint("accent", "/progress")}${chalk66.dim(" for usage metrics")}`;
26372
+ const hint = `${paint("accent", "/progress")}${chalk67.dim(" for usage metrics")}`;
25084
26373
  const gap = colW - visibleWidth(heading) - visibleWidth(hint);
25085
26374
  if (gap > 2) {
25086
26375
  lines.push(truncateVisible(`${heading}${" ".repeat(gap)}${hint}`, colW));
@@ -25090,7 +26379,7 @@ function buildProgressLines(colW) {
25090
26379
  if (summary.total_minutes <= 0) {
25091
26380
  lines.push(
25092
26381
  truncateVisible(
25093
- ` ${chalk66.dim("Run /diagnose or ask a question to start banking hours.")}`,
26382
+ ` ${chalk67.dim("Run /diagnose or ask a question to start banking hours.")}`,
25094
26383
  colW
25095
26384
  )
25096
26385
  );
@@ -25101,7 +26390,7 @@ function buildProgressLines(colW) {
25101
26390
  const bar = inlineBar(summary.progress_pct, 16);
25102
26391
  lines.push(truncateVisible(` ${bar} ${nextLabel}`, colW));
25103
26392
  if (summary.perspective_line) {
25104
- lines.push(truncateVisible(` ${chalk66.dim.italic(summary.perspective_line)}`, colW));
26393
+ lines.push(truncateVisible(` ${chalk67.dim.italic(summary.perspective_line)}`, colW));
25105
26394
  }
25106
26395
  return lines;
25107
26396
  }
@@ -25111,7 +26400,7 @@ async function printWelcome(ctx, version) {
25111
26400
  const innerW = cardW - 2;
25112
26401
  const contentW = innerW - 2;
25113
26402
  const outerPad = " ".repeat(Math.max(0, Math.floor((width - cardW) / 2)));
25114
- const border = (ch) => chalk66.dim(ch);
26403
+ const border = (ch) => chalk67.dim(ch);
25115
26404
  const push = (line) => console.log(outerPad + line);
25116
26405
  const fitCell = (content, width2) => {
25117
26406
  if (visibleWidth(content) > width2) return truncateVisible(content, width2);
@@ -25149,11 +26438,9 @@ async function printWelcome(ctx, version) {
25149
26438
  (s) => s.id !== ctx.sessionId && (s.exchange_count > 0 || s.stage === "analyzed" || s.stage === "delivered" || !!s.dataset?.label)
25150
26439
  );
25151
26440
  const datasetDetail = hasData ? ctx.dataset?.label ?? countStr : savedSessions.length > 0 ? `none loaded \xB7 ${savedSessions.length} saved` : "none loaded";
25152
- const { describeLlmReadiness: describeLlmReadiness2 } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
25153
26441
  const { countAvailableEngines: countAvailableEngines2, formatActiveStack: formatActiveStack2, availableEngineLabels: availableEngineLabels2 } = await Promise.resolve().then(() => (init_session_state(), session_state_exports));
25154
- const llmReady = describeLlmReadiness2();
25155
26442
  const engineCount = countAvailableEngines2();
25156
- const llmDetail = engineCount === 0 ? "needs key" : engineCount === 1 ? `1 engine \xB7 ${availableEngineLabels2()[0]}` : `${engineCount} engines`;
26443
+ const llmDetail = engineCount === 0 ? "run /connect (any key)" : engineCount === 1 ? `1 engine \xB7 ${availableEngineLabels2()[0]}` : `${engineCount} engines \xB7 ${availableEngineLabels2().slice(0, 3).join(", ")}${engineCount > 3 ? ", \u2026" : ""}`;
25157
26444
  const llmState = engineCount >= 2 ? badge("READY", "success") : engineCount === 1 ? badge("READY", "success") : badge("MISSING", "warning");
25158
26445
  const inferenceDetail = engineCount > 0 ? `active: ${formatActiveStack2(ctx)}` : "not configured";
25159
26446
  const license = checkLicense();
@@ -25209,7 +26496,7 @@ async function printWelcome(ctx, version) {
25209
26496
  ctx,
25210
26497
  unfinishedCount: unfinishedSessions.length
25211
26498
  });
25212
- const emptyDataHint = !hasData ? savedSessions.length > 0 ? chalk66.dim("Run ") + paint("accent", "/session") + chalk66.dim(" to resume a saved analysis, or ") + paint("accent", "/new") + chalk66.dim(" for a fresh start") : chalk66.dim("Run ") + paint("accent", "/new") + chalk66.dim(" \u2192 pick Demo to explore sample data") : null;
26499
+ const emptyDataHint = !hasData ? savedSessions.length > 0 ? chalk67.dim("Run ") + paint("accent", "/session") + chalk67.dim(" to resume a saved analysis, or ") + paint("accent", "/new") + chalk67.dim(" for a fresh start") : chalk67.dim("Run ") + paint("accent", "/new") + chalk67.dim(" \u2192 pick Demo to explore sample data") : null;
25213
26500
  const colW = useWideLayout ? leftW : contentW;
25214
26501
  const rightColW = useWideLayout ? rightW : contentW;
25215
26502
  const systemLines = buildSystemLines(colW, statusRows, recent);
@@ -25225,14 +26512,14 @@ async function printWelcome(ctx, version) {
25225
26512
  const logoOffset = " ".repeat(Math.max(0, Math.floor((cardW - maxLogoW) / 2)));
25226
26513
  for (const line of logo) push(logoOffset + line);
25227
26514
  const taglineOffset = " ".repeat(Math.max(0, Math.floor((cardW - visibleWidth(TAGLINE)) / 2)));
25228
- push(taglineOffset + chalk66.dim(TAGLINE));
26515
+ push(taglineOffset + chalk67.dim(TAGLINE));
25229
26516
  push("");
25230
26517
  }
25231
26518
  const versionTag = ` v${version} `;
25232
26519
  const gap = Math.max(0, innerW - versionTag.length);
25233
26520
  const gapL = Math.floor(gap / 2);
25234
26521
  push(
25235
- border(`\u256D${"\u2500".repeat(gapL)}`) + chalk66.dim(versionTag) + border(`${"\u2500".repeat(gap - gapL)}\u256E`)
26522
+ border(`\u256D${"\u2500".repeat(gapL)}`) + chalk67.dim(versionTag) + border(`${"\u2500".repeat(gap - gapL)}\u256E`)
25236
26523
  );
25237
26524
  if (useWideLayout) {
25238
26525
  const leftLines = [...systemLines, ...helpLines];
@@ -25332,8 +26619,8 @@ __export(repl_exports, {
25332
26619
  });
25333
26620
  import { createInterface as createInterface2 } from "readline/promises";
25334
26621
  import { clearLine as clearLine2, cursorTo as cursorTo2 } from "readline";
25335
- import ora16 from "ora";
25336
- import chalk67 from "chalk";
26622
+ import ora19 from "ora";
26623
+ import chalk68 from "chalk";
25337
26624
  function buildPrompt(ctx) {
25338
26625
  return buildConversationPrompt(ctx);
25339
26626
  }
@@ -25370,12 +26657,12 @@ function renderInlineSuggestion(rl, prompt) {
25370
26657
  const suffix = cursor === line.length ? inlineCommandSuggestion(line) : null;
25371
26658
  clearLine2(process.stdout, 0);
25372
26659
  cursorTo2(process.stdout, 0);
25373
- process.stdout.write(prompt + line + (suffix ? chalk67.dim(suffix) : ""));
26660
+ process.stdout.write(prompt + line + (suffix ? chalk68.dim(suffix) : ""));
25374
26661
  cursorTo2(process.stdout, visibleLength(prompt) + cursor);
25375
26662
  }
25376
26663
  function appendTurnLine(current, promptLabel, currentSummary) {
25377
- const currentLine = currentSummary ? `${promptLabel} ${current} ${chalk67.white("\u2192")} ${currentSummary}` : `${promptLabel} ${current}`;
25378
- console.log(" " + chalk67.dim(currentLine));
26664
+ const currentLine = currentSummary ? `${promptLabel} ${current} ${chalk68.white("\u2192")} ${currentSummary}` : `${promptLabel} ${current}`;
26665
+ console.log(" " + chalk68.dim(currentLine));
25379
26666
  console.log();
25380
26667
  }
25381
26668
  async function goHome(ctx, version, history, opts) {
@@ -25385,7 +26672,7 @@ async function goHome(ctx, version, history, opts) {
25385
26672
  process.stdout.write("\x1B[2J\x1B[H");
25386
26673
  if (opts?.banner) {
25387
26674
  console.log();
25388
- console.log(" " + paint("accent", "\u2713") + " " + chalk67.dim(opts.banner));
26675
+ console.log(" " + paint("accent", "\u2713") + " " + chalk68.dim(opts.banner));
25389
26676
  }
25390
26677
  await printWelcome(ctx, version);
25391
26678
  }
@@ -25408,11 +26695,11 @@ async function handleDispatchResult(result, ctx, version, history) {
25408
26695
  case "unknown":
25409
26696
  if (result.suggestion) {
25410
26697
  console.log(
25411
- " " + chalk67.red(`Unknown command: ${result.token}.`) + chalk67.dim(" Did you mean ") + paint("accent", result.suggestion) + chalk67.dim("?")
26698
+ " " + chalk68.red(`Unknown command: ${result.token}.`) + chalk68.dim(" Did you mean ") + paint("accent", result.suggestion) + chalk68.dim("?")
25412
26699
  );
25413
26700
  } else {
25414
26701
  console.log(
25415
- " " + chalk67.red(`Unknown command: ${result.token}`) + chalk67.dim(" Type ") + paint("accent", "/help") + chalk67.dim(" to see available commands.")
26702
+ " " + chalk68.red(`Unknown command: ${result.token}`) + chalk68.dim(" Type ") + paint("accent", "/help") + chalk68.dim(" to see available commands.")
25416
26703
  );
25417
26704
  }
25418
26705
  break;
@@ -25436,7 +26723,7 @@ async function runRepl(ctx, version) {
25436
26723
  ctx.rl = rl;
25437
26724
  console.log();
25438
26725
  console.log(
25439
- " " + chalk67.dim("What do you want to look at? ") + chalk67.dim('(e.g. "pipeline health", "is NRR real?", "board deck on Q3")')
26726
+ " " + chalk68.dim("What do you want to look at? ") + chalk68.dim('(e.g. "pipeline health", "is NRR real?", "board deck on Q3")')
25440
26727
  );
25441
26728
  console.log();
25442
26729
  if (ctx.pendingUpdateCheck) {
@@ -25466,7 +26753,7 @@ async function runRepl(ctx, version) {
25466
26753
  return;
25467
26754
  }
25468
26755
  sigintPrimed = true;
25469
- console.log("\n " + chalk67.dim("Type /exit to quit, or press Ctrl+C again."));
26756
+ console.log("\n " + chalk68.dim("Type /exit to quit, or press Ctrl+C again."));
25470
26757
  };
25471
26758
  rl.on("SIGINT", sigintHandler);
25472
26759
  function shutdownRepl() {
@@ -25537,7 +26824,7 @@ async function runRepl(ctx, version) {
25537
26824
  }
25538
26825
  }
25539
26826
  } else {
25540
- console.error(" " + chalk67.red("Error: " + String(err.message ?? err)));
26827
+ console.error(" " + chalk68.red("Error: " + String(err.message ?? err)));
25541
26828
  }
25542
26829
  }
25543
26830
  history.push({ input: line, summary });
@@ -25545,7 +26832,7 @@ async function runRepl(ctx, version) {
25545
26832
  shutdownRepl();
25546
26833
  const exchangeCount = Math.floor(ctx.messages.length / 2);
25547
26834
  if (exchangeCount > 0) {
25548
- const spinner = ora16({ text: "Saving session\u2026", color: "cyan", discardStdin: false }).start();
26835
+ const spinner = ora19({ text: "Saving session\u2026", color: "cyan", discardStdin: false }).start();
25549
26836
  const summary = await closeSession(ctx);
25550
26837
  if (summary) {
25551
26838
  spinner.succeed(`Session saved (${summary})`);
@@ -25555,7 +26842,7 @@ async function runRepl(ctx, version) {
25555
26842
  } else {
25556
26843
  await closeSession(ctx);
25557
26844
  }
25558
- console.log(" " + chalk67.dim(randomGoodbye()));
26845
+ console.log(" " + chalk68.dim(randomGoodbye()));
25559
26846
  }
25560
26847
  function printHelpOneShot() {
25561
26848
  printHelp();
@@ -25563,10 +26850,10 @@ function printHelpOneShot() {
25563
26850
  function printHelp() {
25564
26851
  console.log();
25565
26852
  console.log(" " + sectionHeading("Conversation"));
25566
- console.log(" " + chalk67.dim("Type what you want to investigate \u2014 no slash needed."));
25567
- console.log(" " + chalk67.dim("Paste a CSV path or say ") + paint("accent", '"use demo data"') + chalk67.dim(" to load data."));
25568
- console.log(" " + chalk67.dim("After analysis, ask questions in plain English."));
25569
- console.log(" " + chalk67.dim("Say ") + paint("accent", '"ship a board deck"') + chalk67.dim(" to draft a handoff prompt."));
26853
+ console.log(" " + chalk68.dim("Type what you want to investigate \u2014 no slash needed."));
26854
+ console.log(" " + chalk68.dim("Paste a CSV path or say ") + paint("accent", '"use demo data"') + chalk68.dim(" to load data."));
26855
+ console.log(" " + chalk68.dim("After analysis, ask questions in plain English."));
26856
+ console.log(" " + chalk68.dim("Say ") + paint("accent", '"ship a board deck"') + chalk68.dim(" to draft a handoff prompt."));
25570
26857
  console.log();
25571
26858
  console.log(" " + sectionHeading("Shortcuts"));
25572
26859
  const shortcuts = [
@@ -25581,7 +26868,7 @@ function printHelp() {
25581
26868
  ];
25582
26869
  const maxW = Math.max(...shortcuts.map(([c]) => c.length)) + 2;
25583
26870
  for (const [cmd, desc] of shortcuts) {
25584
- console.log(` ${paint("accent", padRight(cmd, maxW))} ${chalk67.dim(desc)}`);
26871
+ console.log(` ${paint("accent", padRight(cmd, maxW))} ${chalk68.dim(desc)}`);
25585
26872
  }
25586
26873
  console.log();
25587
26874
  console.log(" " + sectionHeading("Admin"));
@@ -25592,10 +26879,10 @@ function printHelp() {
25592
26879
  ];
25593
26880
  const adminMaxW = Math.max(...admin.map(([c]) => c.length)) + 2;
25594
26881
  for (const [cmd, desc] of admin) {
25595
- console.log(` ${paint("accent", padRight(cmd, adminMaxW))} ${chalk67.dim(desc)}`);
26882
+ console.log(` ${paint("accent", padRight(cmd, adminMaxW))} ${chalk68.dim(desc)}`);
25596
26883
  }
25597
26884
  console.log();
25598
- console.log(" " + chalk67.dim("Power-user commands (") + paint("accent", "/new") + chalk67.dim(", ") + paint("accent", "/diagnose") + chalk67.dim(", ") + paint("accent", "/metrics") + chalk67.dim(") remain available."));
26885
+ console.log(" " + chalk68.dim("Power-user commands (") + paint("accent", "/new") + chalk68.dim(", ") + paint("accent", "/diagnose") + chalk68.dim(", ") + paint("accent", "/metrics") + chalk68.dim(") remain available."));
25599
26886
  console.log();
25600
26887
  }
25601
26888
  var REPL_BUILTINS, ANSI_PATTERN, GOODBYES;
@@ -25678,7 +26965,7 @@ init_emit();
25678
26965
  init_errors2();
25679
26966
  init_types2();
25680
26967
  init_version();
25681
- import chalk68 from "chalk";
26968
+ import chalk69 from "chalk";
25682
26969
  var VERSION = getInstalledVersion();
25683
26970
  var UNGATED = UNGATED_COMMANDS;
25684
26971
  var DB_COMMANDS = /* @__PURE__ */ new Set(["actions", "ask", "backmeup", "demo", "diagnose", "export", "handoff", "ingest", "metrics", "new", "playbook", "publish", "report", "reset", "segment", "session", "status", "strategy"]);
@@ -25705,7 +26992,7 @@ async function main() {
25705
26992
  quiet: args.globals.quiet
25706
26993
  });
25707
26994
  if (!ctx.execution.color) {
25708
- chalk68.level = 0;
26995
+ chalk69.level = 0;
25709
26996
  }
25710
26997
  if (args.globals.stdin) {
25711
26998
  args.input = (await readStdin()).trim();
@@ -25718,16 +27005,16 @@ async function main() {
25718
27005
  if (isStructuredOutput(ctx.execution)) {
25719
27006
  emitError(cmd || "ntrp", new NtrpError("license_invalid", lic2.message, 3 /* Auth */));
25720
27007
  }
25721
- console.error(chalk68.red(`
27008
+ console.error(chalk69.red(`
25722
27009
  ${lic2.message}`));
25723
- console.error(chalk68.dim(" Trial's over \u2014 /upgrade and paste your key.\n"));
27010
+ console.error(chalk69.dim(" Trial's over \u2014 /upgrade and paste your key.\n"));
25724
27011
  process.exit(1);
25725
27012
  }
25726
27013
  }
25727
- const PROFILE_HINT_SKIP = /* @__PURE__ */ new Set(["onboard", "setup", "config", "activate", "help", "home", "exit", "quit", "clear", "profile", "progress"]);
27014
+ const PROFILE_HINT_SKIP = /* @__PURE__ */ new Set(["onboard", "setup", "config", "connect", "activate", "help", "home", "exit", "quit", "clear", "profile", "progress"]);
25728
27015
  if (!isProfileConfigured() && !PROFILE_HINT_SKIP.has(cmd) && !ctx.execution.quiet) {
25729
27016
  console.error(
25730
- " " + chalk68.dim("Tip: run ") + paint("accent", "ntrp") + chalk68.dim(" interactively to set up your company profile for richer answers.")
27017
+ " " + chalk69.dim("Tip: run ") + paint("accent", "ntrp") + chalk69.dim(" interactively to set up your company profile for richer answers.")
25731
27018
  );
25732
27019
  }
25733
27020
  const result = await dispatch(args.input, ctx);
@@ -25739,12 +27026,12 @@ async function main() {
25739
27026
  }
25740
27027
  if (result.suggestion) {
25741
27028
  console.error(
25742
- chalk68.red(` Unknown command: ${result.token}.`) + chalk68.dim(" Did you mean ") + paint("accent", result.suggestion) + chalk68.dim("?")
27029
+ chalk69.red(` Unknown command: ${result.token}.`) + chalk69.dim(" Did you mean ") + paint("accent", result.suggestion) + chalk69.dim("?")
25743
27030
  );
25744
27031
  } else {
25745
- console.error(chalk68.red(` Unknown command: ${result.token}`));
27032
+ console.error(chalk69.red(` Unknown command: ${result.token}`));
25746
27033
  }
25747
- console.error(chalk68.dim(" Run 'ntrp' for the interactive prompt."));
27034
+ console.error(chalk69.dim(" Run 'ntrp' for the interactive prompt."));
25748
27035
  process.exit(1);
25749
27036
  break;
25750
27037
  case "help":
@@ -25771,6 +27058,7 @@ async function main() {
25771
27058
  const { printTrialNudge: printTrialNudge2 } = await Promise.resolve().then(() => (init_upgrade(), upgrade_exports));
25772
27059
  printTrialNudge2(lic);
25773
27060
  }
27061
+ void Promise.resolve().then(() => (init_discovery(), discovery_exports)).then((m) => m.refreshStaleProviderCaches()).catch(() => void 0);
25774
27062
  if (!isProfileConfigured()) {
25775
27063
  try {
25776
27064
  const { handler: onboard } = await Promise.resolve().then(() => (init_onboard(), onboard_exports));