@sonnechasser/ntrp 0.1.7 → 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.
@@ -2954,6 +2954,12 @@ var init_types = __esm({
2954
2954
  });
2955
2955
 
2956
2956
  // src/ai/llm/errors.ts
2957
+ function isToolsUnsupportedMessage(message) {
2958
+ const msg = message.toLowerCase();
2959
+ const mentionsTools = msg.includes("tool") || msg.includes("function");
2960
+ const mentionsUnsupported = msg.includes("not support") || msg.includes("unsupported") || msg.includes("no support") || msg.includes("not available") || msg.includes("not enabled");
2961
+ return mentionsTools && mentionsUnsupported;
2962
+ }
2957
2963
  function mapAnthropicError(err, provider) {
2958
2964
  const e = err;
2959
2965
  const status = e.status;
@@ -2971,6 +2977,9 @@ function mapAnthropicError(err, provider) {
2971
2977
  if (status === 503) {
2972
2978
  return new LlmError("OVERLOADED", message, provider, status);
2973
2979
  }
2980
+ if (isToolsUnsupportedMessage(message)) {
2981
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
2982
+ }
2974
2983
  if (status === 404 || message.toLowerCase().includes("model")) {
2975
2984
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
2976
2985
  }
@@ -2979,6 +2988,11 @@ function mapAnthropicError(err, provider) {
2979
2988
  }
2980
2989
  return new LlmError("UNKNOWN", message, provider, status);
2981
2990
  }
2991
+ function isModelNotFoundMessage(message) {
2992
+ const msg = message.toLowerCase();
2993
+ if (!msg.includes("model")) return false;
2994
+ 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");
2995
+ }
2982
2996
  function mapOpenAiError(err, provider) {
2983
2997
  const e = err;
2984
2998
  const status = e.status;
@@ -2993,7 +3007,10 @@ function mapOpenAiError(err, provider) {
2993
3007
  if (status === 503 || code === "server_error") {
2994
3008
  return new LlmError("OVERLOADED", message, provider, status);
2995
3009
  }
2996
- if (status === 404 || code === "model_not_found") {
3010
+ if (isToolsUnsupportedMessage(message)) {
3011
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
3012
+ }
3013
+ if (status === 404 || code === "model_not_found" || code === "model_decommissioned" || isModelNotFoundMessage(message)) {
2997
3014
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
2998
3015
  }
2999
3016
  if (code === "context_length_exceeded") {
@@ -3129,8 +3146,15 @@ var init_anthropic = __esm({
3129
3146
  }
3130
3147
  });
3131
3148
 
3132
- // src/ai/llm/adapters/openai.ts
3149
+ // src/ai/llm/adapters/openai-compat.ts
3133
3150
  import OpenAI from "openai";
3151
+ function makeClient(apiKey, baseUrl) {
3152
+ return new OpenAI({
3153
+ // Keyless endpoints (Ollama) still need a non-empty string for the SDK.
3154
+ apiKey: apiKey || "local",
3155
+ ...baseUrl ? { baseURL: baseUrl } : {}
3156
+ });
3157
+ }
3134
3158
  function toOpenAiTools(tools) {
3135
3159
  return tools.map((t) => ({
3136
3160
  type: "function",
@@ -3199,9 +3223,8 @@ function parseResponse2(message) {
3199
3223
  assistant_message: { role: "assistant", content: text, tool_calls }
3200
3224
  };
3201
3225
  }
3202
- async function openaiComplete(apiKey, model, req) {
3203
- const provider = "openai";
3204
- const client = new OpenAI({ apiKey });
3226
+ async function openaiCompatComplete(provider, baseUrl, apiKey, model, req) {
3227
+ const client = makeClient(apiKey, baseUrl);
3205
3228
  try {
3206
3229
  const response = await client.chat.completions.create({
3207
3230
  model,
@@ -3211,7 +3234,7 @@ async function openaiComplete(apiKey, model, req) {
3211
3234
  });
3212
3235
  const choice = response.choices[0];
3213
3236
  if (!choice?.message) {
3214
- throw new Error("OpenAI returned no message");
3237
+ throw new Error(`${provider} returned no message`);
3215
3238
  }
3216
3239
  const parsed = parseResponse2(choice.message);
3217
3240
  if (response.usage) {
@@ -3222,15 +3245,11 @@ async function openaiComplete(apiKey, model, req) {
3222
3245
  }
3223
3246
  return parsed;
3224
3247
  } catch (err) {
3225
- if (err instanceof OpenAI.APIError) {
3226
- throw mapOpenAiError(err, provider);
3227
- }
3228
3248
  throw mapOpenAiError(err, provider);
3229
3249
  }
3230
3250
  }
3231
- async function* openaiStream(apiKey, model, req) {
3232
- const provider = "openai";
3233
- const client = new OpenAI({ apiKey });
3251
+ async function* openaiCompatStream(provider, baseUrl, apiKey, model, req) {
3252
+ const client = makeClient(apiKey, baseUrl);
3234
3253
  try {
3235
3254
  const stream = await client.chat.completions.create({
3236
3255
  model,
@@ -3243,64 +3262,118 @@ async function* openaiStream(apiKey, model, req) {
3243
3262
  if (delta) yield { type: "text_delta", text: delta };
3244
3263
  }
3245
3264
  } catch (err) {
3246
- if (err instanceof OpenAI.APIError) {
3247
- throw mapOpenAiError(err, provider);
3248
- }
3249
3265
  throw mapOpenAiError(err, provider);
3250
3266
  }
3251
3267
  }
3252
- var init_openai = __esm({
3253
- "src/ai/llm/adapters/openai.ts"() {
3268
+ var init_openai_compat = __esm({
3269
+ "src/ai/llm/adapters/openai-compat.ts"() {
3254
3270
  "use strict";
3255
3271
  init_errors();
3256
3272
  }
3257
3273
  });
3258
3274
 
3259
- // src/ai/llm/catalog.ts
3260
- function getCatalogEntry(id) {
3261
- return byId.get(id);
3262
- }
3263
- function resolveRetiredModel(id) {
3264
- const seen = /* @__PURE__ */ new Set();
3265
- let current = id;
3266
- while (true) {
3267
- if (seen.has(current)) break;
3268
- seen.add(current);
3269
- const entry = byId.get(current);
3270
- if (!entry) return current;
3271
- if (entry.status === "active") return entry.id;
3272
- if (!entry.successor_id) {
3273
- const fallback = cheapestActiveInTier(entry.provider, entry.tier);
3274
- return fallback?.id ?? current;
3275
- }
3276
- current = entry.successor_id;
3275
+ // src/ai/llm/models-cache.ts
3276
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
3277
+ import { join as join7 } from "path";
3278
+ function cachePath() {
3279
+ return join7(ntrpHome(), "models.json");
3280
+ }
3281
+ function loadFile() {
3282
+ if (cached) return cached;
3283
+ const path = cachePath();
3284
+ if (!existsSync7(path)) {
3285
+ cached = { version: 1, providers: {} };
3286
+ return cached;
3277
3287
  }
3278
- return id;
3288
+ try {
3289
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
3290
+ cached = { version: 1, providers: parsed.providers ?? {} };
3291
+ } catch {
3292
+ cached = { version: 1, providers: {} };
3293
+ }
3294
+ return cached;
3295
+ }
3296
+ function saveFile(file) {
3297
+ writeFileSync6(cachePath(), JSON.stringify(file, null, 2) + "\n");
3298
+ cached = file;
3299
+ }
3300
+ function getProviderModels(provider) {
3301
+ return loadFile().providers[provider];
3302
+ }
3303
+ function setProviderModels(provider, entry) {
3304
+ const file = loadFile();
3305
+ file.providers[provider] = entry;
3306
+ saveFile(file);
3307
+ }
3308
+ function getCachedTierModel(provider, tier) {
3309
+ return getProviderModels(provider)?.tier_stack?.[tier];
3310
+ }
3311
+ function findCachedModel(provider, modelId) {
3312
+ return getProviderModels(provider)?.models.find((m) => m.id === modelId);
3279
3313
  }
3280
- function cheapestActiveInTier(provider, tier) {
3314
+ function cachedModelProvider(modelId) {
3315
+ const file = loadFile();
3316
+ for (const [provider, entry] of Object.entries(file.providers)) {
3317
+ if (entry.models.some((m) => m.id === modelId)) return provider;
3318
+ }
3319
+ return void 0;
3320
+ }
3321
+ function markModelNoTools(provider, modelId) {
3322
+ const file = loadFile();
3323
+ const entry = file.providers[provider];
3324
+ if (!entry) return;
3325
+ const noTools = new Set(entry.quirks?.no_tools ?? []);
3326
+ if (noTools.has(modelId)) return;
3327
+ noTools.add(modelId);
3328
+ entry.quirks = { ...entry.quirks, no_tools: [...noTools] };
3329
+ saveFile(file);
3330
+ }
3331
+ function modelHasNoToolsQuirk(provider, modelId) {
3332
+ return !!getProviderModels(provider)?.quirks?.no_tools?.includes(modelId);
3333
+ }
3334
+ function isProviderCacheStale(provider, ttlMs = CACHE_TTL_MS) {
3335
+ const entry = getProviderModels(provider);
3336
+ if (!entry) return true;
3337
+ const fetched = Date.parse(entry.fetched_at);
3338
+ if (Number.isNaN(fetched)) return true;
3339
+ return Date.now() - fetched > ttlMs;
3340
+ }
3341
+ var CACHE_TTL_MS, cached;
3342
+ var init_models_cache = __esm({
3343
+ "src/ai/llm/models-cache.ts"() {
3344
+ "use strict";
3345
+ init_store();
3346
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
3347
+ cached = null;
3348
+ }
3349
+ });
3350
+
3351
+ // src/ai/llm/catalog.ts
3352
+ function catalogTierDefault(provider, tier) {
3281
3353
  const candidates = ENTRIES.filter(
3282
3354
  (e) => e.provider === provider && e.tier === tier && e.status === "active"
3283
3355
  );
3284
3356
  if (candidates.length === 0) return void 0;
3285
3357
  return candidates.sort((a, b) => a.relative_cost - b.relative_cost)[0];
3286
3358
  }
3287
- function getTierDefault(provider, tier) {
3288
- const entry = cheapestActiveInTier(provider, tier);
3289
- if (!entry) {
3290
- throw new Error(`No active ${tier}-tier model for provider ${provider} in catalog`);
3291
- }
3292
- return entry;
3359
+ function modelProviderHint(modelId) {
3360
+ return cachedModelProvider(modelId) ?? byId.get(modelId)?.provider;
3293
3361
  }
3294
- function resolveModel(provider, tier, override) {
3295
- if (override) {
3296
- const entry = byId.get(override);
3297
- if (!entry) return resolveRetiredModel(override);
3298
- if (entry.status === "retired") return resolveRetiredModel(override);
3299
- return entry.id;
3300
- }
3301
- return getTierDefault(provider, tier).id;
3362
+ function overrideForProvider(override, provider, activeProvider) {
3363
+ if (!override) return void 0;
3364
+ const hint = modelProviderHint(override);
3365
+ if (hint) return hint === provider ? override : void 0;
3366
+ return provider === activeProvider ? override : void 0;
3367
+ }
3368
+ function resolveModelSafe(provider, tier, override) {
3369
+ if (override) return override;
3370
+ const discovered = getCachedTierModel(provider, tier);
3371
+ if (discovered) return discovered;
3372
+ return catalogTierDefault(provider, tier)?.id;
3302
3373
  }
3303
3374
  function formatModelLabel(provider, modelId) {
3375
+ const cachedName = findCachedModel(provider, modelId)?.display_name;
3376
+ if (cachedName) return `${provider}/${cachedName}`;
3304
3377
  const entry = byId.get(modelId);
3305
3378
  return entry ? `${provider}/${entry.display_name}` : `${provider}/${modelId}`;
3306
3379
  }
@@ -3308,6 +3381,7 @@ var ENTRIES, byId;
3308
3381
  var init_catalog = __esm({
3309
3382
  "src/ai/llm/catalog.ts"() {
3310
3383
  "use strict";
3384
+ init_models_cache();
3311
3385
  ENTRIES = [
3312
3386
  {
3313
3387
  id: "claude-opus-4-6",
@@ -3380,10 +3454,198 @@ var init_catalog = __esm({
3380
3454
  }
3381
3455
  });
3382
3456
 
3457
+ // src/ai/llm/providers.ts
3458
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
3459
+ import { join as join8 } from "path";
3460
+ function providersPath() {
3461
+ return join8(ntrpHome(), "providers.json");
3462
+ }
3463
+ function loadCustomProviders() {
3464
+ if (cachedEntries) return cachedEntries;
3465
+ const path = providersPath();
3466
+ if (!existsSync8(path)) {
3467
+ cachedEntries = [];
3468
+ return cachedEntries;
3469
+ }
3470
+ try {
3471
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
3472
+ cachedEntries = Array.isArray(parsed.providers) ? parsed.providers : [];
3473
+ } catch {
3474
+ cachedEntries = [];
3475
+ }
3476
+ return cachedEntries;
3477
+ }
3478
+ function customEntryToSpec(entry) {
3479
+ return {
3480
+ id: entry.id,
3481
+ label: entry.label ?? entry.id,
3482
+ api: "openai-compat",
3483
+ base_url: entry.base_url.replace(/\/+$/, ""),
3484
+ key_prefixes: [],
3485
+ shared_prefixes: [],
3486
+ key_config_name: keyConfigNameFor(entry.id),
3487
+ requires_key: entry.requires_key ?? false,
3488
+ custom: true
3489
+ };
3490
+ }
3491
+ function keyConfigNameFor(providerId) {
3492
+ return providerId === "anthropic" ? "api-key" : `${providerId}-api-key`;
3493
+ }
3494
+ function listProviderSpecs() {
3495
+ const customs = loadCustomProviders();
3496
+ const customById = new Map(customs.map((e) => [e.id, e]));
3497
+ const specs = BUILTIN_SPECS.map((spec) => {
3498
+ const override = customById.get(spec.id);
3499
+ if (override?.base_url) {
3500
+ return { ...spec, base_url: override.base_url.replace(/\/+$/, "") };
3501
+ }
3502
+ return spec;
3503
+ });
3504
+ for (const entry of customs) {
3505
+ if (!BUILTIN_SPECS.some((s) => s.id === entry.id)) {
3506
+ specs.push(customEntryToSpec(entry));
3507
+ }
3508
+ }
3509
+ return specs;
3510
+ }
3511
+ function getProviderSpec(id) {
3512
+ return listProviderSpecs().find((s) => s.id === id);
3513
+ }
3514
+ function isEndpointEnabled(id) {
3515
+ const entry = loadCustomProviders().find((e) => e.id === id);
3516
+ return !!entry && entry.enabled !== false;
3517
+ }
3518
+ function modelsUrl(spec) {
3519
+ if (spec.api === "anthropic") return `${spec.base_url}/v1/models?limit=100`;
3520
+ return `${spec.base_url}/models`;
3521
+ }
3522
+ var BUILTIN_SPECS, cachedEntries;
3523
+ var init_providers = __esm({
3524
+ "src/ai/llm/providers.ts"() {
3525
+ "use strict";
3526
+ init_store();
3527
+ BUILTIN_SPECS = [
3528
+ {
3529
+ id: "anthropic",
3530
+ label: "Anthropic",
3531
+ api: "anthropic",
3532
+ base_url: "https://api.anthropic.com",
3533
+ key_prefixes: ["sk-ant-"],
3534
+ shared_prefixes: [],
3535
+ key_config_name: "api-key",
3536
+ requires_key: true
3537
+ },
3538
+ {
3539
+ id: "openai",
3540
+ label: "OpenAI",
3541
+ api: "openai-compat",
3542
+ base_url: "https://api.openai.com/v1",
3543
+ key_prefixes: ["sk-proj-", "sk-svcacct-", "sk-admin-"],
3544
+ shared_prefixes: ["sk-"],
3545
+ key_config_name: "openai-api-key",
3546
+ env_var: "OPENAI_API_KEY",
3547
+ requires_key: true
3548
+ },
3549
+ {
3550
+ id: "google",
3551
+ label: "Google Gemini",
3552
+ api: "openai-compat",
3553
+ base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
3554
+ key_prefixes: ["AIza"],
3555
+ shared_prefixes: [],
3556
+ key_config_name: "google-api-key",
3557
+ requires_key: true
3558
+ },
3559
+ {
3560
+ id: "groq",
3561
+ label: "Groq",
3562
+ api: "openai-compat",
3563
+ base_url: "https://api.groq.com/openai/v1",
3564
+ key_prefixes: ["gsk_"],
3565
+ shared_prefixes: [],
3566
+ key_config_name: "groq-api-key",
3567
+ requires_key: true
3568
+ },
3569
+ {
3570
+ id: "mistral",
3571
+ label: "Mistral",
3572
+ api: "openai-compat",
3573
+ base_url: "https://api.mistral.ai/v1",
3574
+ key_prefixes: [],
3575
+ shared_prefixes: [],
3576
+ key_config_name: "mistral-api-key",
3577
+ requires_key: true
3578
+ },
3579
+ {
3580
+ id: "deepseek",
3581
+ label: "DeepSeek",
3582
+ api: "openai-compat",
3583
+ base_url: "https://api.deepseek.com/v1",
3584
+ key_prefixes: [],
3585
+ shared_prefixes: ["sk-"],
3586
+ key_config_name: "deepseek-api-key",
3587
+ requires_key: true
3588
+ },
3589
+ {
3590
+ id: "xai",
3591
+ label: "xAI",
3592
+ api: "openai-compat",
3593
+ base_url: "https://api.x.ai/v1",
3594
+ key_prefixes: ["xai-"],
3595
+ shared_prefixes: [],
3596
+ key_config_name: "xai-api-key",
3597
+ requires_key: true
3598
+ },
3599
+ {
3600
+ id: "openrouter",
3601
+ label: "OpenRouter",
3602
+ api: "openai-compat",
3603
+ base_url: "https://openrouter.ai/api/v1",
3604
+ key_prefixes: ["sk-or-"],
3605
+ shared_prefixes: [],
3606
+ key_config_name: "openrouter-api-key",
3607
+ requires_key: true
3608
+ },
3609
+ {
3610
+ id: "together",
3611
+ label: "Together AI",
3612
+ api: "openai-compat",
3613
+ base_url: "https://api.together.xyz/v1",
3614
+ key_prefixes: [],
3615
+ shared_prefixes: [],
3616
+ key_config_name: "together-api-key",
3617
+ requires_key: true
3618
+ },
3619
+ {
3620
+ id: "fireworks",
3621
+ label: "Fireworks AI",
3622
+ api: "openai-compat",
3623
+ base_url: "https://api.fireworks.ai/inference/v1",
3624
+ key_prefixes: ["fw_"],
3625
+ shared_prefixes: [],
3626
+ key_config_name: "fireworks-api-key",
3627
+ requires_key: true
3628
+ },
3629
+ {
3630
+ id: "ollama",
3631
+ label: "Ollama (local)",
3632
+ api: "openai-compat",
3633
+ base_url: "http://localhost:11434/v1",
3634
+ key_prefixes: [],
3635
+ shared_prefixes: [],
3636
+ key_config_name: "ollama-api-key",
3637
+ requires_key: false
3638
+ }
3639
+ ];
3640
+ cachedEntries = null;
3641
+ }
3642
+ });
3643
+
3383
3644
  // src/config/llm-config.ts
3384
3645
  function parseProvider(raw) {
3385
- if (raw === "anthropic" || raw === "openai") return raw;
3386
- return void 0;
3646
+ if (!raw?.trim()) return void 0;
3647
+ const id = raw.trim();
3648
+ return getProviderSpec(id) ? id : void 0;
3387
3649
  }
3388
3650
  function parseTier(raw) {
3389
3651
  if (raw === "high" || raw === "medium" || raw === "low") return raw;
@@ -3391,7 +3653,7 @@ function parseTier(raw) {
3391
3653
  }
3392
3654
  function parseFailoverOrder(raw) {
3393
3655
  if (!raw?.trim()) return ["openai"];
3394
- return raw.split(",").map((s) => s.trim()).filter((s) => s === "anthropic" || s === "openai");
3656
+ return raw.split(",").map((s) => s.trim()).filter((s) => !!s && !!getProviderSpec(s));
3395
3657
  }
3396
3658
  function parseAutoFailover(raw) {
3397
3659
  if (!raw) return false;
@@ -3406,30 +3668,42 @@ function getOpenAiApiKey() {
3406
3668
  if (fromConfig) return fromConfig;
3407
3669
  return process.env.OPENAI_API_KEY?.trim() || void 0;
3408
3670
  }
3671
+ function getProviderApiKey(provider) {
3672
+ const spec = getProviderSpec(provider);
3673
+ if (!spec) return void 0;
3674
+ const record = loadConfig();
3675
+ const fromConfig = record[spec.key_config_name]?.trim();
3676
+ if (fromConfig) return fromConfig;
3677
+ if (spec.env_var) {
3678
+ const fromEnv = process.env[spec.env_var]?.trim();
3679
+ if (fromEnv) return fromEnv;
3680
+ }
3681
+ return void 0;
3682
+ }
3409
3683
  function hasProviderKey(provider) {
3410
- if (provider === "anthropic") return !!getAnthropicApiKey();
3411
- return !!getOpenAiApiKey();
3684
+ const spec = getProviderSpec(provider);
3685
+ if (!spec) return false;
3686
+ if (!spec.requires_key) return isEndpointEnabled(spec.id) || !!getProviderApiKey(provider);
3687
+ return !!getProviderApiKey(provider);
3412
3688
  }
3413
3689
  function getAvailableProviders() {
3414
- const out = [];
3415
- if (hasProviderKey("anthropic")) out.push("anthropic");
3416
- if (hasProviderKey("openai")) out.push("openai");
3417
- return out;
3690
+ return listProviderSpecs().filter((s) => hasProviderKey(s.id)).map((s) => s.id);
3418
3691
  }
3419
3692
  function hasAnyLlmProvider() {
3420
3693
  return getAvailableProviders().length > 0;
3421
3694
  }
3695
+ function hasKeylessConfiguredProvider() {
3696
+ return listProviderSpecs().some((s) => !s.requires_key && hasProviderKey(s.id));
3697
+ }
3422
3698
  function applyLazyMigration(config) {
3423
3699
  if (migrated) return;
3424
3700
  migrated = true;
3425
3701
  let changed = false;
3426
3702
  const record = config;
3427
3703
  if (!record["llm-primary"]) {
3428
- if (record["api-key"]) {
3429
- record["llm-primary"] = "anthropic";
3430
- changed = true;
3431
- } else if (record["openai-api-key"] || process.env.OPENAI_API_KEY) {
3432
- record["llm-primary"] = "openai";
3704
+ const available = getAvailableProviders();
3705
+ if (available.length > 0) {
3706
+ record["llm-primary"] = available[0];
3433
3707
  changed = true;
3434
3708
  }
3435
3709
  }
@@ -3469,25 +3743,311 @@ function loadLlmConfig() {
3469
3743
  openaiKey: getOpenAiApiKey()
3470
3744
  };
3471
3745
  }
3472
- function getProviderApiKey(provider) {
3473
- if (provider === "anthropic") return getAnthropicApiKey();
3474
- return getOpenAiApiKey();
3475
- }
3476
3746
  function getInvestigationApiKey(provider) {
3477
3747
  if (provider === "anthropic") {
3478
3748
  return process.env.NTRP_INVESTIGATION_API_KEY?.trim() || getAnthropicApiKey();
3479
3749
  }
3480
- return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
3750
+ if (provider === "openai") {
3751
+ return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
3752
+ }
3753
+ return getProviderApiKey(provider);
3481
3754
  }
3482
3755
  var migrated;
3483
3756
  var init_llm_config = __esm({
3484
3757
  "src/config/llm-config.ts"() {
3485
3758
  "use strict";
3759
+ init_providers();
3486
3760
  init_store();
3487
3761
  migrated = false;
3488
3762
  }
3489
3763
  });
3490
3764
 
3765
+ // src/ai/llm/http.ts
3766
+ import { readFileSync as readFileSync8 } from "fs";
3767
+ function fixtureResponse(url, headers) {
3768
+ try {
3769
+ const raw = readFileSync8(process.env.NTRP_LLM_HTTP_FIXTURE, "utf-8");
3770
+ const entries = JSON.parse(raw);
3771
+ const headerValues = Object.values(headers).join(" ");
3772
+ for (const entry of entries) {
3773
+ if (!url.includes(entry.url_includes)) continue;
3774
+ if (entry.auth_includes && !headerValues.includes(entry.auth_includes)) continue;
3775
+ return { status: entry.status, ok: entry.status >= 200 && entry.status < 300, body: entry.body };
3776
+ }
3777
+ } catch {
3778
+ }
3779
+ return { status: 0, ok: false, body: void 0 };
3780
+ }
3781
+ async function llmHttpGetJson(url, headers, timeoutMs = 6e3) {
3782
+ if (process.env.NTRP_LLM_HTTP_FIXTURE) {
3783
+ return fixtureResponse(url, headers);
3784
+ }
3785
+ const controller = new AbortController();
3786
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3787
+ try {
3788
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
3789
+ let body;
3790
+ try {
3791
+ body = await res.json();
3792
+ } catch {
3793
+ body = void 0;
3794
+ }
3795
+ return { status: res.status, ok: res.ok, body };
3796
+ } catch {
3797
+ return { status: 0, ok: false, body: void 0 };
3798
+ } finally {
3799
+ clearTimeout(timer);
3800
+ }
3801
+ }
3802
+ var init_http = __esm({
3803
+ "src/ai/llm/http.ts"() {
3804
+ "use strict";
3805
+ }
3806
+ });
3807
+
3808
+ // src/ai/llm/ranking.ts
3809
+ function compareModels(a, b) {
3810
+ const createdA = a.created ?? 0;
3811
+ const createdB = b.created ?? 0;
3812
+ if (createdA !== createdB) return createdB - createdA;
3813
+ const versionA = extractVersion(a.id);
3814
+ const versionB = extractVersion(b.id);
3815
+ if (versionA !== versionB) return versionB - versionA;
3816
+ if (a.id.length !== b.id.length) return a.id.length - b.id.length;
3817
+ return a.id.localeCompare(b.id);
3818
+ }
3819
+ function extractVersion(id) {
3820
+ const match = id.match(/(\d+(?:\.\d+)?)/);
3821
+ return match ? Number(match[1]) : 0;
3822
+ }
3823
+ function pickByPatterns(models, patterns) {
3824
+ for (const pattern of patterns) {
3825
+ const matches = models.filter((m) => pattern.test(m.id));
3826
+ if (matches.length > 0) return [...matches].sort(compareModels)[0];
3827
+ }
3828
+ return void 0;
3829
+ }
3830
+ function genericBucket(model) {
3831
+ if (GENERIC_HIGH.test(model.id)) return "high";
3832
+ if (GENERIC_LOW.test(model.id)) return "low";
3833
+ return "medium";
3834
+ }
3835
+ function genericPick(models, tier) {
3836
+ const bucket = models.filter((m) => genericBucket(m) === tier);
3837
+ if (bucket.length > 0) return [...bucket].sort(compareModels)[0];
3838
+ return void 0;
3839
+ }
3840
+ function rankModels(providerId, models) {
3841
+ if (models.length === 0) return null;
3842
+ const preferences = PROVIDER_PREFERENCES[providerId];
3843
+ const picks = {};
3844
+ for (const tier of ["high", "medium", "low"]) {
3845
+ const preferred = preferences ? pickByPatterns(models, preferences[tier]) : void 0;
3846
+ const generic = preferred ?? genericPick(models, tier);
3847
+ if (generic) picks[tier] = generic.id;
3848
+ }
3849
+ const anyModel = [...models].sort(compareModels)[0].id;
3850
+ const high = picks.high ?? picks.medium ?? picks.low ?? anyModel;
3851
+ const medium = picks.medium ?? picks.high ?? picks.low ?? anyModel;
3852
+ const low = picks.low ?? picks.medium ?? picks.high ?? anyModel;
3853
+ return { high, medium, low };
3854
+ }
3855
+ var PROVIDER_PREFERENCES, GENERIC_LOW, GENERIC_HIGH;
3856
+ var init_ranking = __esm({
3857
+ "src/ai/llm/ranking.ts"() {
3858
+ "use strict";
3859
+ PROVIDER_PREFERENCES = {
3860
+ anthropic: {
3861
+ high: [/^claude-opus/i, /^claude-sonnet/i],
3862
+ medium: [/^claude-sonnet/i, /^claude-haiku/i],
3863
+ low: [/^claude-haiku/i, /^claude-sonnet/i]
3864
+ },
3865
+ openai: {
3866
+ high: [/^gpt-5(?!.*(mini|nano|chat))/i, /^gpt-4\.1(?!.*(mini|nano))/i, /^gpt-4o(?!.*mini)/i, /^o3(?!.*mini)/i],
3867
+ medium: [/^gpt-5.*mini/i, /^gpt-4\.1-mini/i, /^gpt-4o-mini/i, /^o4-mini/i],
3868
+ low: [/^gpt-5.*nano/i, /^gpt-4\.1-nano/i, /^gpt-4o-mini/i]
3869
+ },
3870
+ google: {
3871
+ high: [/^gemini-[\d.]+-pro/i, /^gemini-[\d.]+-flash(?!-lite)/i],
3872
+ medium: [/^gemini-[\d.]+-flash(?!-lite|-8b)/i, /^gemini-[\d.]+-pro/i],
3873
+ low: [/^gemini-[\d.]+-flash-lite/i, /flash-8b/i, /^gemini-[\d.]+-flash(?!-lite)/i]
3874
+ },
3875
+ groq: {
3876
+ high: [/llama-3\.3-70b/i, /gpt-oss-120b/i, /70b/i, /deepseek-r1/i],
3877
+ medium: [/llama-3\.1-8b-instant/i, /gpt-oss-20b/i, /llama.*8b/i],
3878
+ low: [/8b-instant/i, /llama.*8b/i, /gemma/i]
3879
+ },
3880
+ deepseek: {
3881
+ high: [/reasoner/i, /chat/i],
3882
+ medium: [/chat/i],
3883
+ low: [/chat/i]
3884
+ },
3885
+ mistral: {
3886
+ high: [/large/i, /medium/i],
3887
+ medium: [/medium/i, /^mistral-small/i],
3888
+ low: [/ministral/i, /small/i, /tiny/i]
3889
+ },
3890
+ xai: {
3891
+ high: [/^grok-\d+(?!.*(mini|fast))/i, /^grok(?!.*(mini|fast))/i],
3892
+ medium: [/^grok.*mini(?!.*fast)/i, /^grok.*fast/i],
3893
+ low: [/^grok.*mini.*fast/i, /^grok.*mini/i]
3894
+ },
3895
+ openrouter: {
3896
+ high: [/^openrouter\/auto$/i, /claude.*opus/i, /^openai\/gpt-5(?!.*(mini|nano))/i, /gemini.*pro/i],
3897
+ medium: [/claude.*sonnet/i, /gpt-5.*mini/i, /gpt-4\.1-mini/i, /gemini.*flash(?!-lite)/i],
3898
+ low: [/claude.*haiku/i, /nano/i, /flash-lite/i, /mini/i]
3899
+ }
3900
+ };
3901
+ GENERIC_LOW = /(mini|nano|lite|tiny|micro|small|haiku|instant|flash|turbo|\b0?\.?5b\b|\b[1-8]b\b)/i;
3902
+ GENERIC_HIGH = /(opus|ultra|large|max\b|\bpro\b|405b|253b|235b|120b|72b|70b|reason|-r1\b|think|deep)/i;
3903
+ }
3904
+ });
3905
+
3906
+ // src/ai/llm/discovery.ts
3907
+ function authHeaders(spec, apiKey) {
3908
+ if (spec.api === "anthropic") {
3909
+ return { "x-api-key": apiKey ?? "", "anthropic-version": "2023-06-01" };
3910
+ }
3911
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
3912
+ }
3913
+ function normalizeItem(spec, item) {
3914
+ let id = item.id ?? "";
3915
+ if (!id) return null;
3916
+ if (id.startsWith("models/")) id = id.slice("models/".length);
3917
+ const model = { id };
3918
+ const display = item.display_name ?? item.name;
3919
+ if (display && display !== id) model.display_name = display;
3920
+ if (typeof item.created === "number") model.created = item.created;
3921
+ else if (item.created_at) {
3922
+ const parsed = Date.parse(item.created_at);
3923
+ if (!Number.isNaN(parsed)) model.created = Math.floor(parsed / 1e3);
3924
+ }
3925
+ if (typeof item.context_length === "number") model.context_length = item.context_length;
3926
+ if (Array.isArray(item.supported_parameters)) {
3927
+ model.supports_tools = item.supported_parameters.includes("tools");
3928
+ }
3929
+ return model;
3930
+ }
3931
+ async function fetchProviderModels(spec, apiKey, timeoutMs = 6e3) {
3932
+ const headers = authHeaders(spec, apiKey);
3933
+ if (spec.api === "anthropic") {
3934
+ const models2 = [];
3935
+ let url = modelsUrl(spec);
3936
+ for (let page = 0; page < 5 && url; page++) {
3937
+ const res2 = await llmHttpGetJson(url, headers, timeoutMs);
3938
+ if (!res2.ok) return models2.length > 0 ? { ok: true, models: models2 } : { ok: false, status: res2.status };
3939
+ const body2 = res2.body;
3940
+ for (const item of body2?.data ?? []) {
3941
+ const model = normalizeItem(spec, item);
3942
+ if (model) models2.push(model);
3943
+ }
3944
+ url = body2?.has_more && body2.last_id ? `${spec.base_url}/v1/models?limit=100&after_id=${encodeURIComponent(body2.last_id)}` : null;
3945
+ }
3946
+ return { ok: true, models: models2 };
3947
+ }
3948
+ const res = await llmHttpGetJson(modelsUrl(spec), headers, timeoutMs);
3949
+ if (!res.ok) return { ok: false, status: res.status };
3950
+ const body = res.body;
3951
+ const list = Array.isArray(body) ? body : body?.data ?? [];
3952
+ const models = [];
3953
+ for (const item of list) {
3954
+ const model = normalizeItem(spec, item);
3955
+ if (model) models.push(model);
3956
+ }
3957
+ return { ok: true, models };
3958
+ }
3959
+ function filterChatModels(spec, models) {
3960
+ const extra = PROVIDER_EXCLUDE[spec.id];
3961
+ return models.filter((m) => !NON_CHAT.test(m.id) && !(extra && extra.test(m.id)));
3962
+ }
3963
+ function storeDiscoveredModels(providerId, rawModels) {
3964
+ const spec = getProviderSpec(providerId);
3965
+ if (!spec || rawModels.length === 0) return null;
3966
+ const chat = filterChatModels(spec, rawModels);
3967
+ const usable = chat.length > 0 ? chat : rawModels;
3968
+ const stack = rankModels(providerId, usable);
3969
+ if (!stack) return null;
3970
+ const prior = getProviderModels(providerId);
3971
+ const entry = {
3972
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
3973
+ models: usable,
3974
+ tier_stack: stack,
3975
+ ...prior?.quirks ? { quirks: prior.quirks } : {}
3976
+ };
3977
+ setProviderModels(providerId, entry);
3978
+ return entry;
3979
+ }
3980
+ async function refreshProviderModels(providerId, opts = {}) {
3981
+ const spec = getProviderSpec(providerId);
3982
+ if (!spec) return null;
3983
+ if (!opts.force && !isProviderCacheStale(providerId)) {
3984
+ return getProviderModels(providerId) ?? null;
3985
+ }
3986
+ const apiKey = opts.apiKey ?? getProviderApiKey(providerId);
3987
+ if (spec.requires_key && !apiKey) return null;
3988
+ const result = await fetchProviderModels(spec, apiKey);
3989
+ if (!result.ok) return null;
3990
+ return storeDiscoveredModels(providerId, result.models);
3991
+ }
3992
+ function rerankExcluding(providerId, deadModelId) {
3993
+ const prior = getProviderModels(providerId);
3994
+ if (!prior) return null;
3995
+ const survivors = prior.models.filter((m) => m.id !== deadModelId);
3996
+ const stack = rankModels(providerId, survivors);
3997
+ if (!stack) return null;
3998
+ const entry = { ...prior, models: survivors, tier_stack: stack };
3999
+ setProviderModels(providerId, entry);
4000
+ return entry;
4001
+ }
4002
+ var NON_CHAT, PROVIDER_EXCLUDE;
4003
+ var init_discovery = __esm({
4004
+ "src/ai/llm/discovery.ts"() {
4005
+ "use strict";
4006
+ init_llm_config();
4007
+ init_http();
4008
+ init_models_cache();
4009
+ init_providers();
4010
+ init_ranking();
4011
+ 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;
4012
+ PROVIDER_EXCLUDE = {
4013
+ openai: /(chatgpt|-search|deep-research|-pro\b|computer-use|codex-mini|-instruct\b)/i
4014
+ };
4015
+ }
4016
+ });
4017
+
4018
+ // src/ai/llm/heal.ts
4019
+ async function healModelNotFound(opts) {
4020
+ const { provider, tier, deadModel } = opts;
4021
+ const refreshed = await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true });
4022
+ let candidate = refreshed?.tier_stack?.[tier] ?? getCachedTierModel(provider, tier);
4023
+ if (!candidate || candidate === deadModel) {
4024
+ const reranked = rerankExcluding(provider, deadModel);
4025
+ candidate = reranked?.tier_stack?.[tier];
4026
+ }
4027
+ if (!candidate || candidate === deadModel) return null;
4028
+ clearDeadOverride(deadModel, opts.ctx);
4029
+ return {
4030
+ model: candidate,
4031
+ notice: `model ${deadModel} is no longer available \u2014 switched to ${candidate}`
4032
+ };
4033
+ }
4034
+ function clearDeadOverride(deadModel, ctx) {
4035
+ if (getConfigValue("llm-model-override")?.trim() === deadModel) {
4036
+ deleteConfigValue("llm-model-override");
4037
+ }
4038
+ if (ctx?.llm?.modelOverride === deadModel) {
4039
+ ctx.llm.modelOverride = void 0;
4040
+ }
4041
+ }
4042
+ var init_heal = __esm({
4043
+ "src/ai/llm/heal.ts"() {
4044
+ "use strict";
4045
+ init_store();
4046
+ init_discovery();
4047
+ init_models_cache();
4048
+ }
4049
+ });
4050
+
3491
4051
  // src/ai/llm/surfaces.ts
3492
4052
  function tierForSurface(surface, userTier) {
3493
4053
  const spec = SURFACE_SPECS[surface];
@@ -3532,7 +4092,7 @@ function resolveActiveProvider(ctx) {
3532
4092
  if (session && hasProviderKey(session)) return session;
3533
4093
  const cfg = loadLlmConfig();
3534
4094
  if (hasProviderKey(cfg.primary)) return cfg.primary;
3535
- const available = ["anthropic", "openai"].filter(hasProviderKey);
4095
+ const available = getAvailableProviders();
3536
4096
  if (available.length > 0) return available[0];
3537
4097
  return cfg.primary;
3538
4098
  }
@@ -3558,8 +4118,8 @@ function resolveProviderOrder(ctx) {
3558
4118
  for (const p of cfg.failoverOrder) {
3559
4119
  if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
3560
4120
  }
3561
- for (const p of ["anthropic", "openai"]) {
3562
- if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
4121
+ for (const p of getAvailableProviders()) {
4122
+ if (p !== active && !order.includes(p)) order.push(p);
3563
4123
  }
3564
4124
  return order;
3565
4125
  }
@@ -3573,23 +4133,16 @@ var init_session_state = __esm({
3573
4133
  });
3574
4134
 
3575
4135
  // src/ai/llm/resolver.ts
3576
- function getProviderOrder(config, ctx) {
3577
- void config;
3578
- return resolveProviderOrder(ctx);
3579
- }
3580
4136
  function resolveCompletionContext(surface, opts = {}) {
3581
4137
  const activeProvider = resolveActiveProvider(opts.ctx);
3582
4138
  const tier = opts.tier ?? resolveEffectiveTier(opts.ctx, surface);
3583
4139
  const override = opts.modelOverride ?? resolveEffectiveModelOverride(opts.ctx);
3584
4140
  const providerOrder = resolveProviderOrder(opts.ctx);
3585
4141
  const modelByProvider = {};
3586
- for (const provider of providerOrder) {
3587
- const providerOverride = override && getCatalogEntry(override)?.provider === provider ? override : void 0;
3588
- modelByProvider[provider] = resolveModel(provider, tier, providerOverride);
3589
- }
3590
- if (!modelByProvider[activeProvider]) {
3591
- const activeOverride = override && getCatalogEntry(override)?.provider === activeProvider ? override : void 0;
3592
- modelByProvider[activeProvider] = resolveModel(activeProvider, tier, activeOverride);
4142
+ for (const provider of /* @__PURE__ */ new Set([...providerOrder, activeProvider])) {
4143
+ const providerOverride = overrideForProvider(override, provider, activeProvider);
4144
+ const model = resolveModelSafe(provider, tier, providerOverride);
4145
+ if (model) modelByProvider[provider] = model;
3593
4146
  }
3594
4147
  return {
3595
4148
  providerOrder,
@@ -3615,70 +4168,142 @@ var init_resolver = __esm({
3615
4168
 
3616
4169
  // src/ai/llm/failover.ts
3617
4170
  async function completeOnProvider(provider, model, apiKey, req) {
3618
- if (provider === "anthropic") return anthropicComplete(apiKey, model, req);
3619
- return openaiComplete(apiKey, model, req);
4171
+ const spec = getProviderSpec(provider);
4172
+ if (!spec) {
4173
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
4174
+ }
4175
+ if (spec.api === "anthropic") {
4176
+ return anthropicComplete(apiKey ?? "", model, req);
4177
+ }
4178
+ return openaiCompatComplete(provider, spec.base_url, apiKey, model, req);
4179
+ }
4180
+ function usableKey(provider, ctx) {
4181
+ const spec = getProviderSpec(provider);
4182
+ if (!spec) return { ok: false };
4183
+ const apiKey = getApiKeyForProvider(provider, ctx);
4184
+ if (spec.requires_key && !apiKey) return { ok: false };
4185
+ return { ok: true, apiKey };
4186
+ }
4187
+ async function resolveModelWithDiscovery(provider, cfg, opts) {
4188
+ const known = cfg.modelByProvider[provider];
4189
+ if (known) return known;
4190
+ const override = overrideForProvider(opts.modelOverride, provider, cfg.activeProvider);
4191
+ const direct = resolveModelSafe(provider, cfg.tier, override);
4192
+ if (direct) return direct;
4193
+ await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true }).catch(() => null);
4194
+ return resolveModelSafe(provider, cfg.tier, override);
4195
+ }
4196
+ function stripTools(req) {
4197
+ const { tools: _tools, ...rest } = req;
4198
+ return rest;
3620
4199
  }
3621
4200
  async function completeWithFailover(req, opts = {}) {
3622
- const ctx = resolveCompletionContext(req.surface, {
4201
+ const cfg = resolveCompletionContext(req.surface, {
3623
4202
  max_tokens: req.max_tokens,
3624
4203
  tier: opts.tier,
3625
4204
  modelOverride: opts.modelOverride,
3626
4205
  ctx: opts.ctx
3627
4206
  });
3628
- const providers = ctx.providerOrder;
4207
+ const providers = cfg.providerOrder;
3629
4208
  if (providers.length === 0) {
3630
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
4209
+ throw new Error(NO_PROVIDER_MESSAGE);
3631
4210
  }
4211
+ const notices = [];
3632
4212
  let lastError;
3633
4213
  let failoverFrom;
4214
+ const buildMeta = (provider, model, response) => ({
4215
+ provider_used: provider,
4216
+ model_used: model,
4217
+ ...response.token_usage ?? {},
4218
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
4219
+ ...notices.length > 0 ? { notices: [...notices] } : {}
4220
+ });
3634
4221
  for (let i = 0; i < providers.length; i++) {
3635
4222
  const provider = providers[i];
3636
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
3637
- if (!apiKey) continue;
3638
- let model = ctx.modelByProvider[provider] ?? resolveModel(provider, ctx.tier, opts.modelOverride);
4223
+ const key = usableKey(provider, opts.ctx);
4224
+ if (!key.ok) continue;
4225
+ let model = await resolveModelWithDiscovery(provider, cfg, {
4226
+ modelOverride: opts.modelOverride,
4227
+ apiKey: key.apiKey
4228
+ });
4229
+ if (!model) {
4230
+ lastError = new LlmError(
4231
+ "MODEL_NOT_FOUND",
4232
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
4233
+ provider
4234
+ );
4235
+ continue;
4236
+ }
4237
+ let effectiveReq = req;
4238
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
4239
+ effectiveReq = stripTools(req);
4240
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
4241
+ }
3639
4242
  try {
3640
- const response = await completeOnProvider(provider, model, apiKey, req);
3641
- const meta = {
3642
- provider_used: provider,
3643
- model_used: model,
3644
- ...response.token_usage ?? {},
3645
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
3646
- };
4243
+ const response = await completeOnProvider(provider, model, key.apiKey, effectiveReq);
4244
+ const meta = buildMeta(provider, model, response);
3647
4245
  recordLlmUsage(response.token_usage);
3648
4246
  return { response, meta };
3649
4247
  } catch (err) {
3650
- const llmErr = err;
4248
+ let llmErr = err;
3651
4249
  if (llmErr.name !== "LlmError") throw err;
3652
4250
  lastError = llmErr;
3653
- if (llmErr.code === "MODEL_NOT_FOUND") {
3654
- model = resolveModel(provider, ctx.tier);
4251
+ if (llmErr.code === "TOOLS_UNSUPPORTED" && effectiveReq.tools?.length) {
4252
+ markModelNoTools(provider, model);
4253
+ notices.push(`${model} doesn't support tool calling \u2014 retrying without live data tools`);
3655
4254
  try {
3656
- const response = await completeOnProvider(provider, model, apiKey, req);
3657
- const meta = {
3658
- provider_used: provider,
3659
- model_used: model,
3660
- ...response.token_usage ?? {},
3661
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
3662
- };
4255
+ const response = await completeOnProvider(provider, model, key.apiKey, stripTools(effectiveReq));
4256
+ const meta = buildMeta(provider, model, response);
3663
4257
  recordLlmUsage(response.token_usage);
3664
4258
  return { response, meta };
3665
4259
  } catch (retryErr) {
3666
4260
  const retryLlm = retryErr;
3667
- if (retryLlm.name === "LlmError") lastError = retryLlm;
3668
- else throw retryErr;
4261
+ if (retryLlm.name !== "LlmError") throw retryErr;
4262
+ lastError = retryLlm;
4263
+ llmErr = retryLlm;
4264
+ }
4265
+ }
4266
+ if (llmErr.code === "MODEL_NOT_FOUND") {
4267
+ const healed = await healModelNotFound({
4268
+ provider,
4269
+ tier: cfg.tier,
4270
+ deadModel: model,
4271
+ apiKey: key.apiKey,
4272
+ ctx: opts.ctx
4273
+ }).catch(() => null);
4274
+ if (healed) {
4275
+ notices.push(healed.notice);
4276
+ model = healed.model;
4277
+ let retryReq = req;
4278
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
4279
+ retryReq = stripTools(req);
4280
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
4281
+ }
4282
+ try {
4283
+ const response = await completeOnProvider(provider, model, key.apiKey, retryReq);
4284
+ const meta = buildMeta(provider, model, response);
4285
+ recordLlmUsage(response.token_usage);
4286
+ return { response, meta };
4287
+ } catch (retryErr) {
4288
+ const retryLlm = retryErr;
4289
+ if (retryLlm.name !== "LlmError") throw retryErr;
4290
+ lastError = retryLlm;
4291
+ llmErr = retryLlm;
4292
+ }
3669
4293
  }
3670
4294
  }
3671
4295
  if (!isFailoverEligible(llmErr.code)) throw llmErr;
3672
4296
  const next = providers[i + 1];
3673
4297
  if (next) {
3674
4298
  failoverFrom = failoverFrom ?? provider;
4299
+ notices.push(`${provider} unavailable (${llmErr.code.toLowerCase()}) \u2014 trying ${next}`);
3675
4300
  opts.onFailover?.(provider, next, llmErr.code);
3676
4301
  continue;
3677
4302
  }
3678
4303
  throw llmErr;
3679
4304
  }
3680
4305
  }
3681
- throw lastError ?? new Error("No LLM provider available");
4306
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
3682
4307
  }
3683
4308
  async function* streamWithFailover(req, opts = {}) {
3684
4309
  const cfg = resolveCompletionContext(req.surface, {
@@ -3687,23 +4312,47 @@ async function* streamWithFailover(req, opts = {}) {
3687
4312
  modelOverride: opts.modelOverride,
3688
4313
  ctx: opts.ctx
3689
4314
  });
3690
- const providers = getProviderOrder();
4315
+ const providers = cfg.providerOrder;
3691
4316
  if (providers.length === 0) {
3692
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
4317
+ throw new Error(NO_PROVIDER_MESSAGE);
3693
4318
  }
4319
+ const notices = [];
3694
4320
  let lastError;
3695
4321
  let failoverFrom;
4322
+ async function* streamOnProvider(provider, model, apiKey) {
4323
+ const spec = getProviderSpec(provider);
4324
+ if (!spec) {
4325
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
4326
+ }
4327
+ if (spec.api === "anthropic") {
4328
+ yield* anthropicStream(apiKey ?? "", model, req);
4329
+ return;
4330
+ }
4331
+ yield* openaiCompatStream(provider, spec.base_url, apiKey, model, req);
4332
+ }
3696
4333
  for (let i = 0; i < providers.length; i++) {
3697
4334
  const provider = providers[i];
3698
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
3699
- if (!apiKey) continue;
3700
- const model = cfg.modelByProvider[provider] ?? resolveModel(provider, cfg.tier, opts.modelOverride);
3701
- try {
4335
+ const key = usableKey(provider, opts.ctx);
4336
+ if (!key.ok) continue;
4337
+ let model = await resolveModelWithDiscovery(provider, cfg, {
4338
+ modelOverride: opts.modelOverride,
4339
+ apiKey: key.apiKey
4340
+ });
4341
+ if (!model) {
4342
+ lastError = new LlmError(
4343
+ "MODEL_NOT_FOUND",
4344
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
4345
+ provider
4346
+ );
4347
+ continue;
4348
+ }
4349
+ let yieldedAny = false;
4350
+ const attempt = async function* (attemptModel) {
3702
4351
  let fullText = "";
3703
- const streamFn = provider === "anthropic" ? anthropicStream : openaiStream;
3704
- for await (const event of streamFn(apiKey, model, req)) {
4352
+ for await (const event of streamOnProvider(provider, attemptModel, key.apiKey)) {
3705
4353
  if (event.type === "text_delta") {
3706
4354
  fullText += event.text;
4355
+ yieldedAny = true;
3707
4356
  yield event;
3708
4357
  }
3709
4358
  }
@@ -3711,10 +4360,11 @@ async function* streamWithFailover(req, opts = {}) {
3711
4360
  recordLlmUsage({ input_tokens: 0, output_tokens: estimatedOut });
3712
4361
  const meta = {
3713
4362
  provider_used: provider,
3714
- model_used: model,
4363
+ model_used: attemptModel,
3715
4364
  input_tokens: 0,
3716
4365
  output_tokens: estimatedOut,
3717
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
4366
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
4367
+ ...notices.length > 0 ? { notices: [...notices] } : {}
3718
4368
  };
3719
4369
  yield {
3720
4370
  type: "done",
@@ -3726,32 +4376,66 @@ async function* streamWithFailover(req, opts = {}) {
3726
4376
  },
3727
4377
  meta
3728
4378
  };
4379
+ };
4380
+ try {
4381
+ yield* attempt(model);
3729
4382
  return;
3730
4383
  } catch (err) {
3731
4384
  const llmErr = err;
3732
4385
  if (llmErr.name !== "LlmError") throw err;
3733
4386
  lastError = llmErr;
3734
- if (!isFailoverEligible(llmErr.code)) throw llmErr;
4387
+ if (yieldedAny) throw llmErr;
4388
+ if (llmErr.code === "MODEL_NOT_FOUND") {
4389
+ const healed = await healModelNotFound({
4390
+ provider,
4391
+ tier: cfg.tier,
4392
+ deadModel: model,
4393
+ apiKey: key.apiKey,
4394
+ ctx: opts.ctx
4395
+ }).catch(() => null);
4396
+ if (healed) {
4397
+ notices.push(healed.notice);
4398
+ model = healed.model;
4399
+ try {
4400
+ yield* attempt(model);
4401
+ return;
4402
+ } catch (retryErr) {
4403
+ const retryLlm = retryErr;
4404
+ if (retryLlm.name !== "LlmError") throw retryErr;
4405
+ lastError = retryLlm;
4406
+ if (yieldedAny) throw retryLlm;
4407
+ }
4408
+ }
4409
+ }
4410
+ if (!isFailoverEligible(lastError.code)) throw lastError;
3735
4411
  const next = providers[i + 1];
3736
4412
  if (next) {
3737
4413
  failoverFrom = failoverFrom ?? provider;
3738
- opts.onFailover?.(provider, next, llmErr.code);
4414
+ notices.push(`${provider} unavailable (${lastError.code.toLowerCase()}) \u2014 trying ${next}`);
4415
+ opts.onFailover?.(provider, next, lastError.code);
3739
4416
  continue;
3740
4417
  }
3741
- throw llmErr;
4418
+ throw lastError;
3742
4419
  }
3743
4420
  }
3744
- throw lastError ?? new Error("No LLM provider available");
4421
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
3745
4422
  }
4423
+ var NO_PROVIDER_MESSAGE;
3746
4424
  var init_failover = __esm({
3747
4425
  "src/ai/llm/failover.ts"() {
3748
4426
  "use strict";
3749
4427
  init_usage_stats();
3750
4428
  init_anthropic();
3751
- init_openai();
4429
+ init_openai_compat();
3752
4430
  init_catalog();
4431
+ init_discovery();
3753
4432
  init_errors();
4433
+ init_heal();
4434
+ init_models_cache();
4435
+ init_providers();
4436
+ init_types();
3754
4437
  init_resolver();
4438
+ NO_PROVIDER_MESSAGE = "No LLM provider configured. Run /connect and paste any API key (Anthropic, OpenAI, Groq, Gemini, ...).";
3755
4439
  }
3756
4440
  });
3757
4441
 
@@ -3804,7 +4488,13 @@ function resolvePrimaryApiKey(ctx) {
3804
4488
  if (isInvestigationMode(ctx)) {
3805
4489
  return getInvestigationApiKey(primary) ?? getInvestigationApiKey("anthropic") ?? getInvestigationApiKey("openai");
3806
4490
  }
3807
- return getProviderApiKey(primary) ?? getProviderApiKey("anthropic") ?? getProviderApiKey("openai");
4491
+ const primaryKey = getProviderApiKey(primary);
4492
+ if (primaryKey) return primaryKey;
4493
+ for (const provider of getAvailableProviders()) {
4494
+ const key = getProviderApiKey(provider);
4495
+ if (key) return key;
4496
+ }
4497
+ return void 0;
3808
4498
  }
3809
4499
  function canUseReplAi(ctx) {
3810
4500
  if (!ctx) return false;
@@ -3815,25 +4505,21 @@ function canUseReplAi(ctx) {
3815
4505
  }
3816
4506
  function assertReplAi(ctx) {
3817
4507
  if (!ctx) {
3818
- throw new Error(
3819
- "AI features require stored API keys. Run `ntrp`, then /config set api-key or /config set openai-api-key."
3820
- );
4508
+ throw new Error(`AI features require stored API keys. Run \`ntrp\`, then /connect.`);
3821
4509
  }
3822
4510
  if (!canUseReplAi(ctx)) {
3823
4511
  if (!hasAnyLlmProvider()) {
3824
- throw new Error(
3825
- "No LLM API key configured. Run: /config set api-key (Anthropic) and/or /config set openai-api-key"
3826
- );
4512
+ throw new Error(NO_KEY_MESSAGE);
3827
4513
  }
3828
4514
  throw new Error(
3829
4515
  "AI features run only in the interactive REPL or headless mode with stored keys."
3830
4516
  );
3831
4517
  }
3832
4518
  const key = resolvePrimaryApiKey(ctx);
3833
- if (!key) {
3834
- throw new Error("No LLM API key configured. Run: /config set api-key or /config set openai-api-key");
4519
+ if (!key && !hasKeylessConfiguredProvider()) {
4520
+ throw new Error(NO_KEY_MESSAGE);
3835
4521
  }
3836
- return key;
4522
+ return key ?? "";
3837
4523
  }
3838
4524
  function hasEnvApiKeyHint() {
3839
4525
  return !!(process.env.ANTHROPIC_API_KEY ?? process.env.NTRP_API_KEY ?? process.env.OPENAI_API_KEY);
@@ -3846,10 +4532,12 @@ function describeLlmReadiness() {
3846
4532
  openai: providers.includes("openai")
3847
4533
  };
3848
4534
  }
4535
+ var NO_KEY_MESSAGE;
3849
4536
  var init_gate = __esm({
3850
4537
  "src/ai/llm/gate.ts"() {
3851
4538
  "use strict";
3852
4539
  init_llm_config();
4540
+ NO_KEY_MESSAGE = "No LLM API key configured. Run /connect and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).";
3853
4541
  }
3854
4542
  });
3855
4543
 
@@ -4159,9 +4847,9 @@ var init_tool_schemas = __esm({
4159
4847
  });
4160
4848
 
4161
4849
  // src/ai/privacy.ts
4162
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, appendFileSync } from "fs";
4850
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, appendFileSync } from "fs";
4163
4851
  import { homedir as homedir3 } from "os";
4164
- import { join as join7 } from "path";
4852
+ import { join as join9 } from "path";
4165
4853
  function stripPII(obj) {
4166
4854
  if (obj === null || obj === void 0) return obj;
4167
4855
  if (typeof obj !== "object") return obj;
@@ -4176,14 +4864,14 @@ function stripPII(obj) {
4176
4864
  return out;
4177
4865
  }
4178
4866
  function ensureAuditDir() {
4179
- if (!existsSync7(AUDIT_DIR)) {
4867
+ if (!existsSync9(AUDIT_DIR)) {
4180
4868
  mkdirSync6(AUDIT_DIR, { recursive: true });
4181
4869
  }
4182
4870
  }
4183
4871
  function logToolCall(entry) {
4184
4872
  ensureAuditDir();
4185
4873
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4186
- const path = join7(AUDIT_DIR, `agentic-${date}.jsonl`);
4874
+ const path = join9(AUDIT_DIR, `agentic-${date}.jsonl`);
4187
4875
  appendFileSync(path, JSON.stringify(entry) + "\n");
4188
4876
  }
4189
4877
  var PII_FIELDS, AUDIT_DIR;
@@ -4207,7 +4895,7 @@ var init_privacy = __esm({
4207
4895
  "raw_data",
4208
4896
  "metadata"
4209
4897
  ]);
4210
- AUDIT_DIR = join7(homedir3(), ".ntrp", "audit");
4898
+ AUDIT_DIR = join9(homedir3(), ".ntrp", "audit");
4211
4899
  }
4212
4900
  });
4213
4901
 
@@ -4434,16 +5122,16 @@ var init_metrics_benchmarks = __esm({
4434
5122
  });
4435
5123
 
4436
5124
  // src/config/profile.ts
4437
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
4438
- import { join as join8 } from "path";
5125
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
5126
+ import { join as join10 } from "path";
4439
5127
  function isProfileConfigured(profile = loadProfile()) {
4440
5128
  if (!profile) return false;
4441
5129
  return profile.company_name.trim().length > 0;
4442
5130
  }
4443
5131
  function loadProfile() {
4444
- if (!existsSync8(PROFILE_PATH)) return null;
5132
+ if (!existsSync10(PROFILE_PATH)) return null;
4445
5133
  try {
4446
- const parsed = JSON.parse(readFileSync6(PROFILE_PATH, "utf-8"));
5134
+ const parsed = JSON.parse(readFileSync9(PROFILE_PATH, "utf-8"));
4447
5135
  if (!parsed || typeof parsed !== "object") return null;
4448
5136
  return parsed;
4449
5137
  } catch {
@@ -4456,7 +5144,7 @@ var init_profile = __esm({
4456
5144
  "use strict";
4457
5145
  init_store();
4458
5146
  NTRP_DIR3 = ntrpHome();
4459
- PROFILE_PATH = join8(NTRP_DIR3, "profile.json");
5147
+ PROFILE_PATH = join10(NTRP_DIR3, "profile.json");
4460
5148
  }
4461
5149
  });
4462
5150
 
@@ -7369,16 +8057,16 @@ var init_complete = __esm({
7369
8057
  });
7370
8058
 
7371
8059
  // src/data/playbook.ts
7372
- import { existsSync as existsSync9, readFileSync as readFileSync7, appendFileSync as appendFileSync2 } from "fs";
7373
- import { join as join9 } from "path";
8060
+ import { existsSync as existsSync11, readFileSync as readFileSync10, appendFileSync as appendFileSync2 } from "fs";
8061
+ import { join as join11 } from "path";
7374
8062
  function playsPath() {
7375
- return join9(getMemoryDir(), PLAYS_FILE);
8063
+ return join11(getMemoryDir(), PLAYS_FILE);
7376
8064
  }
7377
8065
  function getCustomPlays() {
7378
8066
  const path = playsPath();
7379
- if (!existsSync9(path)) return [];
8067
+ if (!existsSync11(path)) return [];
7380
8068
  const out = [];
7381
- for (const line of readFileSync7(path, "utf-8").split("\n")) {
8069
+ for (const line of readFileSync10(path, "utf-8").split("\n")) {
7382
8070
  const trimmed = line.trim();
7383
8071
  if (!trimmed) continue;
7384
8072
  try {
@@ -7805,7 +8493,7 @@ async function runMetricsAnalysis(options = {}) {
7805
8493
  if (options.findings) {
7806
8494
  if (!canUseReplAi(options.ctx)) {
7807
8495
  throw new Error(
7808
- "AI metrics findings require stored API keys. Run `ntrp`, set api-key and/or openai-api-key, then /metrics --findings."
8496
+ "AI metrics findings require stored API keys. Run `ntrp`, then /connect (any provider key), then /metrics --findings."
7809
8497
  );
7810
8498
  }
7811
8499
  options.onProgress?.("findings");
@@ -8160,6 +8848,9 @@ function formatLlmAttribution(meta) {
8160
8848
  return line;
8161
8849
  }
8162
8850
  function printLlmAttribution(meta) {
8851
+ for (const notice of meta.notices ?? []) {
8852
+ console.log(chalk8.dim(` ${notice}`));
8853
+ }
8163
8854
  const line = formatLlmAttribution(meta);
8164
8855
  if (line) console.log(chalk8.dim(` ${line}`));
8165
8856
  }
@@ -8432,6 +9123,7 @@ async function renderDiagnoseStream(options) {
8432
9123
  let modelUsed = "";
8433
9124
  let providerUsed;
8434
9125
  let failover;
9126
+ let notices;
8435
9127
  let rawPrompt = "";
8436
9128
  try {
8437
9129
  for await (const event of runFindings(fullResult)) {
@@ -8447,6 +9139,7 @@ async function renderDiagnoseStream(options) {
8447
9139
  modelUsed = event.model_used;
8448
9140
  providerUsed = event.provider_used;
8449
9141
  failover = event.failover;
9142
+ notices = event.usage?.notices;
8450
9143
  rawPrompt = event.raw_prompt;
8451
9144
  }
8452
9145
  }
@@ -8470,7 +9163,8 @@ async function renderDiagnoseStream(options) {
8470
9163
  printLlmAttribution({
8471
9164
  model_used: modelUsed,
8472
9165
  provider_used: providerUsed,
8473
- failover
9166
+ failover,
9167
+ notices
8474
9168
  });
8475
9169
  } catch (err) {
8476
9170
  findingsSpinner.fail(deep ? "Agentic investigation failed" : "AI findings failed");
@@ -8782,7 +9476,8 @@ async function* streamFindings(input, ctx) {
8782
9476
  findings,
8783
9477
  model_used: meta.model_used,
8784
9478
  provider_used: meta.provider_used,
8785
- raw_prompt: userMessage
9479
+ raw_prompt: userMessage,
9480
+ usage: meta
8786
9481
  };
8787
9482
  }
8788
9483
  }
@@ -9000,7 +9695,7 @@ async function runDiagnosis(options = {}) {
9000
9695
  if (options.findings) {
9001
9696
  if (!canUseReplAi(options.ctx)) {
9002
9697
  throw new Error(
9003
- "AI findings require stored API keys. Run `ntrp`, then /config set api-key and/or /config set openai-api-key, and use /diagnose --findings."
9698
+ "AI findings require stored API keys. Run `ntrp`, then /connect (any provider key), and use /diagnose --findings."
9004
9699
  );
9005
9700
  }
9006
9701
  if (options.deep) {
@@ -9143,7 +9838,7 @@ async function handler(args, ctx) {
9143
9838
  console.log();
9144
9839
  console.log(" " + chalk11.red("AI findings run only in the interactive REPL."));
9145
9840
  console.log(" " + chalk11.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
9146
- console.log(" " + chalk11.dim("Start with ") + paint("accent", "ntrp") + chalk11.dim(", set ") + paint("accent", "/config set api-key") + chalk11.dim(" or openai-api-key, then /diagnose --findings."));
9841
+ console.log(" " + chalk11.dim("Start with ") + paint("accent", "ntrp") + chalk11.dim(", run ") + paint("accent", "/connect") + chalk11.dim(" (any provider key), then /diagnose --findings."));
9147
9842
  console.log();
9148
9843
  return;
9149
9844
  }
@@ -12148,18 +12843,18 @@ var init_generator = __esm({
12148
12843
  });
12149
12844
 
12150
12845
  // src/demo/taxonomy-cache.ts
12151
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync10, mkdirSync as mkdirSync8, unlinkSync as unlinkSync3 } from "fs";
12846
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync8, unlinkSync as unlinkSync3 } from "fs";
12152
12847
  import { homedir as homedir4 } from "os";
12153
- import { join as join10 } from "path";
12848
+ import { join as join12 } from "path";
12154
12849
  function ensureDir5() {
12155
- if (!existsSync10(NTRP_DIR4)) {
12850
+ if (!existsSync12(NTRP_DIR4)) {
12156
12851
  mkdirSync8(NTRP_DIR4, { recursive: true });
12157
12852
  }
12158
12853
  }
12159
12854
  function loadCachedTaxonomy(profile) {
12160
- if (!existsSync10(TAXONOMY_PATH)) return null;
12855
+ if (!existsSync12(TAXONOMY_PATH)) return null;
12161
12856
  try {
12162
- const parsed = JSON.parse(readFileSync8(TAXONOMY_PATH, "utf-8"));
12857
+ const parsed = JSON.parse(readFileSync11(TAXONOMY_PATH, "utf-8"));
12163
12858
  if (!parsed || typeof parsed !== "object") return null;
12164
12859
  if (parsed.profile_updated_at !== profile.updated_at) return null;
12165
12860
  return parsed;
@@ -12169,14 +12864,14 @@ function loadCachedTaxonomy(profile) {
12169
12864
  }
12170
12865
  function saveCachedTaxonomy(taxonomy) {
12171
12866
  ensureDir5();
12172
- writeFileSync7(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
12867
+ writeFileSync9(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
12173
12868
  }
12174
12869
  var NTRP_DIR4, TAXONOMY_PATH;
12175
12870
  var init_taxonomy_cache = __esm({
12176
12871
  "src/demo/taxonomy-cache.ts"() {
12177
12872
  "use strict";
12178
- NTRP_DIR4 = join10(homedir4(), ".ntrp");
12179
- TAXONOMY_PATH = join10(NTRP_DIR4, "demo-taxonomy.json");
12873
+ NTRP_DIR4 = join12(homedir4(), ".ntrp");
12874
+ TAXONOMY_PATH = join12(NTRP_DIR4, "demo-taxonomy.json");
12180
12875
  }
12181
12876
  });
12182
12877
 
@@ -12505,8 +13200,8 @@ function markFailure(ctx) {
12505
13200
  }
12506
13201
  async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
12507
13202
  if (!forceRegen) {
12508
- const cached = loadCachedTaxonomy(profile);
12509
- if (cached) return cached;
13203
+ const cached2 = loadCachedTaxonomy(profile);
13204
+ if (cached2) return cached2;
12510
13205
  }
12511
13206
  const spinnerText = forceRegen ? "Rebuilding market taxonomy\u2026" : "Researching your market taxonomy\u2026";
12512
13207
  const spinner = ora4({ text: spinnerText, discardStdin: false }).start();
@@ -12609,7 +13304,7 @@ __export(ingest_exports, {
12609
13304
  });
12610
13305
  import chalk15 from "chalk";
12611
13306
  import ora5 from "ora";
12612
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
13307
+ import { readFileSync as readFileSync12, existsSync as existsSync13 } from "fs";
12613
13308
  import { basename as basename2 } from "path";
12614
13309
  async function handler3(args, ctx) {
12615
13310
  const { positional, flags } = parseArgs(args, [
@@ -12633,7 +13328,7 @@ async function handler3(args, ctx) {
12633
13328
  console.error(chalk15.dim(" /ingest --demo [--scenario <name>]"));
12634
13329
  process.exit(1);
12635
13330
  }
12636
- if (!existsSync11(file)) {
13331
+ if (!existsSync13(file)) {
12637
13332
  console.error(chalk15.red(` File not found: ${file}`));
12638
13333
  process.exit(1);
12639
13334
  }
@@ -12651,7 +13346,7 @@ async function handler3(args, ctx) {
12651
13346
  try {
12652
13347
  await initSchema();
12653
13348
  spinner.text = "Parsing CSV...";
12654
- const content = readFileSync9(file, "utf-8");
13349
+ const content = readFileSync12(file, "utf-8");
12655
13350
  const { rows, headers } = parseCSV(content);
12656
13351
  if (rows.length === 0) {
12657
13352
  spinner.fail("CSV is empty");
@@ -12767,7 +13462,7 @@ __export(ingest_chat_exports, {
12767
13462
  loadDemoFromChat: () => loadDemoFromChat,
12768
13463
  looksLikeFilePath: () => looksLikeFilePath
12769
13464
  });
12770
- import { existsSync as existsSync12 } from "fs";
13465
+ import { existsSync as existsSync14 } from "fs";
12771
13466
  import { basename as basename3, resolve as resolve4 } from "path";
12772
13467
  import { homedir as homedir5 } from "os";
12773
13468
  import chalk16 from "chalk";
@@ -12787,11 +13482,11 @@ function extractFilePath(input) {
12787
13482
  const m = trimmed.match(re);
12788
13483
  if (m?.[1]) {
12789
13484
  const p = expandPath(m[1]);
12790
- if (existsSync12(p)) return p;
13485
+ if (existsSync14(p)) return p;
12791
13486
  }
12792
13487
  if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
12793
13488
  const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
12794
- if (existsSync12(p)) return p;
13489
+ if (existsSync14(p)) return p;
12795
13490
  }
12796
13491
  }
12797
13492
  return null;
@@ -12821,12 +13516,12 @@ async function ingestFromChat(ctx, filePath) {
12821
13516
  }
12822
13517
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
12823
13518
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
12824
- const { readFileSync: readFileSync13 } = await import("fs");
13519
+ const { readFileSync: readFileSync16 } = await import("fs");
12825
13520
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
12826
13521
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
12827
13522
  let headerCheckFailed = false;
12828
13523
  try {
12829
- const raw = readFileSync13(filePath, "utf-8");
13524
+ const raw = readFileSync16(filePath, "utf-8");
12830
13525
  const { headers } = parseCSV2(raw);
12831
13526
  const detected = detectEntityType2(headers, "unknown");
12832
13527
  if (!detected) headerCheckFailed = true;
@@ -14007,8 +14702,8 @@ async function callProvider(texts) {
14007
14702
  async function embedText(text) {
14008
14703
  const key = text.trim();
14009
14704
  if (!key) return null;
14010
- const cached = cache.get(key);
14011
- if (cached) return cached;
14705
+ const cached2 = cache.get(key);
14706
+ if (cached2) return cached2;
14012
14707
  const result = await callProvider([key]);
14013
14708
  const vec = result?.[0] ?? null;
14014
14709
  if (vec) cache.set(key, vec);
@@ -14018,8 +14713,8 @@ async function embedItems(items) {
14018
14713
  const needing = [];
14019
14714
  const out = items.map((it, index) => {
14020
14715
  if (it.embedding && it.embedding.length > 0) return { ...it };
14021
- const cached = cache.get(it.text.trim());
14022
- if (cached) return { ...it, embedding: cached };
14716
+ const cached2 = cache.get(it.text.trim());
14717
+ if (cached2) return { ...it, embedding: cached2 };
14023
14718
  needing.push({ index, text: it.text });
14024
14719
  return { ...it };
14025
14720
  });
@@ -14049,24 +14744,24 @@ var init_embeddings = __esm({
14049
14744
 
14050
14745
  // src/strategies/readers.ts
14051
14746
  import { createHash } from "crypto";
14052
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
14747
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
14053
14748
  import { extname, resolve as resolve5 } from "path";
14054
14749
  import { parse as parseYaml } from "yaml";
14055
14750
  import { PDFParse } from "pdf-parse";
14056
14751
  async function readStrategyFile(pathOrDash) {
14057
14752
  if (pathOrDash === "-") {
14058
- const text2 = readFileSync10(0, "utf-8");
14753
+ const text2 = readFileSync13(0, "utf-8");
14059
14754
  return createDocument("stdin", null, text2, {});
14060
14755
  }
14061
14756
  const sourcePath = resolve5(pathOrDash);
14062
- if (!existsSync13(sourcePath)) {
14757
+ if (!existsSync15(sourcePath)) {
14063
14758
  throw new NtrpError("strategy_file_not_found", `Strategy file not found: ${pathOrDash}`, 2 /* Usage */);
14064
14759
  }
14065
14760
  const ext = extname(sourcePath).toLowerCase();
14066
14761
  if (ext === ".pdf") {
14067
14762
  return readPdf(sourcePath);
14068
14763
  }
14069
- const text = readFileSync10(sourcePath, "utf-8");
14764
+ const text = readFileSync13(sourcePath, "utf-8");
14070
14765
  if (ext === ".yaml" || ext === ".yml") {
14071
14766
  const structured = parseStructuredYaml(text);
14072
14767
  return createDocument("yaml", sourcePath, text, structured);
@@ -14081,7 +14776,7 @@ function readStrategyText(text) {
14081
14776
  return createDocument("text", null, text, {});
14082
14777
  }
14083
14778
  async function readPdf(sourcePath) {
14084
- const data = readFileSync10(sourcePath);
14779
+ const data = readFileSync13(sourcePath);
14085
14780
  const parser = new PDFParse({ data });
14086
14781
  try {
14087
14782
  const result = await parser.getText();
@@ -14270,15 +14965,15 @@ JSON SHAPE:
14270
14965
  });
14271
14966
 
14272
14967
  // src/strategies/library.ts
14273
- import { writeFileSync as writeFileSync8 } from "fs";
14274
- import { join as join12 } from "path";
14968
+ import { writeFileSync as writeFileSync10 } from "fs";
14969
+ import { join as join14 } from "path";
14275
14970
  import { stringify as stringifyYaml } from "yaml";
14276
14971
  function strategyLibraryPath(slug) {
14277
- return join12(getStrategiesDir(), `${slug}.md`);
14972
+ return join14(getStrategiesDir(), `${slug}.md`);
14278
14973
  }
14279
14974
  function writeStrategyMarkdown(strategy) {
14280
14975
  const path = strategyLibraryPath(strategy.slug);
14281
- writeFileSync8(path, renderStrategyMarkdown(strategy), "utf-8");
14976
+ writeFileSync10(path, renderStrategyMarkdown(strategy), "utf-8");
14282
14977
  return path;
14283
14978
  }
14284
14979
  function renderStrategyMarkdown(strategy) {
@@ -14352,7 +15047,7 @@ var init_library = __esm({
14352
15047
  // src/strategies/connectors.ts
14353
15048
  import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
14354
15049
  import { homedir as homedir6 } from "os";
14355
- import { basename as basename4, extname as extname2, join as join13, relative, resolve as resolve6, sep as sep2 } from "path";
15050
+ import { basename as basename4, extname as extname2, join as join15, relative, resolve as resolve6, sep as sep2 } from "path";
14356
15051
  function createLocalFolderConnector(options) {
14357
15052
  const rootPath = resolveUserPath(options.rootPath);
14358
15053
  const name = options.name ?? (basename4(rootPath) || "local");
@@ -14395,7 +15090,7 @@ function createLocalFolderConnector(options) {
14395
15090
  }
14396
15091
  function walkLocalFolder(rootPath, currentPath, refs, opts) {
14397
15092
  for (const entry of readdirSync3(currentPath, { withFileTypes: true })) {
14398
- const absolutePath = join13(currentPath, entry.name);
15093
+ const absolutePath = join15(currentPath, entry.name);
14399
15094
  const relativePath = normalizePath(relative(rootPath, absolutePath));
14400
15095
  if (entry.isDirectory()) {
14401
15096
  if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
@@ -14459,7 +15154,7 @@ function normalizePath(path) {
14459
15154
  }
14460
15155
  function resolveUserPath(path) {
14461
15156
  if (path === "~") return homedir6();
14462
- if (path.startsWith("~/")) return join13(homedir6(), path.slice(2));
15157
+ if (path.startsWith("~/")) return join15(homedir6(), path.slice(2));
14463
15158
  return resolve6(path);
14464
15159
  }
14465
15160
  var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
@@ -14670,8 +15365,8 @@ init_divergence();
14670
15365
 
14671
15366
  // src/memory/store.ts
14672
15367
  init_store();
14673
- import { existsSync as existsSync15, readFileSync as readFileSync12, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync9 } from "fs";
14674
- import { join as join14 } from "path";
15368
+ import { existsSync as existsSync17, readFileSync as readFileSync15, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync11 } from "fs";
15369
+ import { join as join16 } from "path";
14675
15370
  import { randomUUID as randomUUID5 } from "crypto";
14676
15371
 
14677
15372
  // src/memory/retrieval.ts
@@ -14801,18 +15496,18 @@ async function rankByRelevance(query, items, topK) {
14801
15496
  // src/memory/knowledge.ts
14802
15497
  init_store();
14803
15498
  init_readers();
14804
- import { existsSync as existsSync14, readFileSync as readFileSync11, appendFileSync as appendFileSync3, readdirSync as readdirSync2 } from "fs";
14805
- import { join as join11 } from "path";
15499
+ import { existsSync as existsSync16, readFileSync as readFileSync14, appendFileSync as appendFileSync3, readdirSync as readdirSync2 } from "fs";
15500
+ import { join as join13 } from "path";
14806
15501
  import { randomUUID as randomUUID4 } from "crypto";
14807
15502
  var KNOWLEDGE_FILE = "knowledge.jsonl";
14808
15503
  function knowledgePath() {
14809
- return join11(getMemoryDir(), KNOWLEDGE_FILE);
15504
+ return join13(getMemoryDir(), KNOWLEDGE_FILE);
14810
15505
  }
14811
15506
  function loadKnowledgeChunks() {
14812
15507
  const path = knowledgePath();
14813
- if (!existsSync14(path)) return [];
15508
+ if (!existsSync16(path)) return [];
14814
15509
  const out = [];
14815
- for (const line of readFileSync11(path, "utf-8").split("\n")) {
15510
+ for (const line of readFileSync14(path, "utf-8").split("\n")) {
14816
15511
  const trimmed = line.trim();
14817
15512
  if (!trimmed) continue;
14818
15513
  try {
@@ -14827,13 +15522,13 @@ function loadKnowledgeChunks() {
14827
15522
  var FACTS_FILE = "facts.jsonl";
14828
15523
  var LEDGER_FILE = "ledger.jsonl";
14829
15524
  function memPath(file) {
14830
- return join14(getMemoryDir(), file);
15525
+ return join16(getMemoryDir(), file);
14831
15526
  }
14832
15527
  function readJsonl(file) {
14833
15528
  const path = memPath(file);
14834
- if (!existsSync15(path)) return [];
15529
+ if (!existsSync17(path)) return [];
14835
15530
  const out = [];
14836
- for (const line of readFileSync12(path, "utf-8").split("\n")) {
15531
+ for (const line of readFileSync15(path, "utf-8").split("\n")) {
14837
15532
  const trimmed = line.trim();
14838
15533
  if (!trimmed) continue;
14839
15534
  try {
@@ -14868,7 +15563,7 @@ function loadWinSnippets() {
14868
15563
  const out = [];
14869
15564
  for (const name of readdirSync4(dir)) {
14870
15565
  if (!name.endsWith(".md") || name.toLowerCase() === "readme.md") continue;
14871
- const raw = readFileSync12(join14(dir, name), "utf-8");
15566
+ const raw = readFileSync15(join16(dir, name), "utf-8");
14872
15567
  const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? name.replace(/\.md$/, "");
14873
15568
  const body = raw.replace(/^#.*$/m, "").replace(/\s+/g, " ").trim().slice(0, 300);
14874
15569
  out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });