@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.
@@ -4002,10 +4002,198 @@ var init_health_score = __esm({
4002
4002
  }
4003
4003
  });
4004
4004
 
4005
+ // src/ai/llm/providers.ts
4006
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
4007
+ import { join as join7 } from "path";
4008
+ function providersPath() {
4009
+ return join7(ntrpHome(), "providers.json");
4010
+ }
4011
+ function loadCustomProviders() {
4012
+ if (cachedEntries) return cachedEntries;
4013
+ const path = providersPath();
4014
+ if (!existsSync7(path)) {
4015
+ cachedEntries = [];
4016
+ return cachedEntries;
4017
+ }
4018
+ try {
4019
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
4020
+ cachedEntries = Array.isArray(parsed.providers) ? parsed.providers : [];
4021
+ } catch {
4022
+ cachedEntries = [];
4023
+ }
4024
+ return cachedEntries;
4025
+ }
4026
+ function customEntryToSpec(entry) {
4027
+ return {
4028
+ id: entry.id,
4029
+ label: entry.label ?? entry.id,
4030
+ api: "openai-compat",
4031
+ base_url: entry.base_url.replace(/\/+$/, ""),
4032
+ key_prefixes: [],
4033
+ shared_prefixes: [],
4034
+ key_config_name: keyConfigNameFor(entry.id),
4035
+ requires_key: entry.requires_key ?? false,
4036
+ custom: true
4037
+ };
4038
+ }
4039
+ function keyConfigNameFor(providerId) {
4040
+ return providerId === "anthropic" ? "api-key" : `${providerId}-api-key`;
4041
+ }
4042
+ function listProviderSpecs() {
4043
+ const customs = loadCustomProviders();
4044
+ const customById = new Map(customs.map((e) => [e.id, e]));
4045
+ const specs = BUILTIN_SPECS.map((spec) => {
4046
+ const override = customById.get(spec.id);
4047
+ if (override?.base_url) {
4048
+ return { ...spec, base_url: override.base_url.replace(/\/+$/, "") };
4049
+ }
4050
+ return spec;
4051
+ });
4052
+ for (const entry of customs) {
4053
+ if (!BUILTIN_SPECS.some((s) => s.id === entry.id)) {
4054
+ specs.push(customEntryToSpec(entry));
4055
+ }
4056
+ }
4057
+ return specs;
4058
+ }
4059
+ function getProviderSpec(id) {
4060
+ return listProviderSpecs().find((s) => s.id === id);
4061
+ }
4062
+ function isEndpointEnabled(id) {
4063
+ const entry = loadCustomProviders().find((e) => e.id === id);
4064
+ return !!entry && entry.enabled !== false;
4065
+ }
4066
+ function modelsUrl(spec) {
4067
+ if (spec.api === "anthropic") return `${spec.base_url}/v1/models?limit=100`;
4068
+ return `${spec.base_url}/models`;
4069
+ }
4070
+ var BUILTIN_SPECS, cachedEntries;
4071
+ var init_providers = __esm({
4072
+ "src/ai/llm/providers.ts"() {
4073
+ "use strict";
4074
+ init_store();
4075
+ BUILTIN_SPECS = [
4076
+ {
4077
+ id: "anthropic",
4078
+ label: "Anthropic",
4079
+ api: "anthropic",
4080
+ base_url: "https://api.anthropic.com",
4081
+ key_prefixes: ["sk-ant-"],
4082
+ shared_prefixes: [],
4083
+ key_config_name: "api-key",
4084
+ requires_key: true
4085
+ },
4086
+ {
4087
+ id: "openai",
4088
+ label: "OpenAI",
4089
+ api: "openai-compat",
4090
+ base_url: "https://api.openai.com/v1",
4091
+ key_prefixes: ["sk-proj-", "sk-svcacct-", "sk-admin-"],
4092
+ shared_prefixes: ["sk-"],
4093
+ key_config_name: "openai-api-key",
4094
+ env_var: "OPENAI_API_KEY",
4095
+ requires_key: true
4096
+ },
4097
+ {
4098
+ id: "google",
4099
+ label: "Google Gemini",
4100
+ api: "openai-compat",
4101
+ base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
4102
+ key_prefixes: ["AIza"],
4103
+ shared_prefixes: [],
4104
+ key_config_name: "google-api-key",
4105
+ requires_key: true
4106
+ },
4107
+ {
4108
+ id: "groq",
4109
+ label: "Groq",
4110
+ api: "openai-compat",
4111
+ base_url: "https://api.groq.com/openai/v1",
4112
+ key_prefixes: ["gsk_"],
4113
+ shared_prefixes: [],
4114
+ key_config_name: "groq-api-key",
4115
+ requires_key: true
4116
+ },
4117
+ {
4118
+ id: "mistral",
4119
+ label: "Mistral",
4120
+ api: "openai-compat",
4121
+ base_url: "https://api.mistral.ai/v1",
4122
+ key_prefixes: [],
4123
+ shared_prefixes: [],
4124
+ key_config_name: "mistral-api-key",
4125
+ requires_key: true
4126
+ },
4127
+ {
4128
+ id: "deepseek",
4129
+ label: "DeepSeek",
4130
+ api: "openai-compat",
4131
+ base_url: "https://api.deepseek.com/v1",
4132
+ key_prefixes: [],
4133
+ shared_prefixes: ["sk-"],
4134
+ key_config_name: "deepseek-api-key",
4135
+ requires_key: true
4136
+ },
4137
+ {
4138
+ id: "xai",
4139
+ label: "xAI",
4140
+ api: "openai-compat",
4141
+ base_url: "https://api.x.ai/v1",
4142
+ key_prefixes: ["xai-"],
4143
+ shared_prefixes: [],
4144
+ key_config_name: "xai-api-key",
4145
+ requires_key: true
4146
+ },
4147
+ {
4148
+ id: "openrouter",
4149
+ label: "OpenRouter",
4150
+ api: "openai-compat",
4151
+ base_url: "https://openrouter.ai/api/v1",
4152
+ key_prefixes: ["sk-or-"],
4153
+ shared_prefixes: [],
4154
+ key_config_name: "openrouter-api-key",
4155
+ requires_key: true
4156
+ },
4157
+ {
4158
+ id: "together",
4159
+ label: "Together AI",
4160
+ api: "openai-compat",
4161
+ base_url: "https://api.together.xyz/v1",
4162
+ key_prefixes: [],
4163
+ shared_prefixes: [],
4164
+ key_config_name: "together-api-key",
4165
+ requires_key: true
4166
+ },
4167
+ {
4168
+ id: "fireworks",
4169
+ label: "Fireworks AI",
4170
+ api: "openai-compat",
4171
+ base_url: "https://api.fireworks.ai/inference/v1",
4172
+ key_prefixes: ["fw_"],
4173
+ shared_prefixes: [],
4174
+ key_config_name: "fireworks-api-key",
4175
+ requires_key: true
4176
+ },
4177
+ {
4178
+ id: "ollama",
4179
+ label: "Ollama (local)",
4180
+ api: "openai-compat",
4181
+ base_url: "http://localhost:11434/v1",
4182
+ key_prefixes: [],
4183
+ shared_prefixes: [],
4184
+ key_config_name: "ollama-api-key",
4185
+ requires_key: false
4186
+ }
4187
+ ];
4188
+ cachedEntries = null;
4189
+ }
4190
+ });
4191
+
4005
4192
  // src/config/llm-config.ts
4006
4193
  function parseProvider(raw) {
4007
- if (raw === "anthropic" || raw === "openai") return raw;
4008
- return void 0;
4194
+ if (!raw?.trim()) return void 0;
4195
+ const id = raw.trim();
4196
+ return getProviderSpec(id) ? id : void 0;
4009
4197
  }
4010
4198
  function parseTier(raw) {
4011
4199
  if (raw === "high" || raw === "medium" || raw === "low") return raw;
@@ -4013,7 +4201,7 @@ function parseTier(raw) {
4013
4201
  }
4014
4202
  function parseFailoverOrder(raw) {
4015
4203
  if (!raw?.trim()) return ["openai"];
4016
- return raw.split(",").map((s) => s.trim()).filter((s) => s === "anthropic" || s === "openai");
4204
+ return raw.split(",").map((s) => s.trim()).filter((s) => !!s && !!getProviderSpec(s));
4017
4205
  }
4018
4206
  function parseAutoFailover(raw) {
4019
4207
  if (!raw) return false;
@@ -4028,30 +4216,42 @@ function getOpenAiApiKey() {
4028
4216
  if (fromConfig) return fromConfig;
4029
4217
  return process.env.OPENAI_API_KEY?.trim() || void 0;
4030
4218
  }
4219
+ function getProviderApiKey(provider) {
4220
+ const spec = getProviderSpec(provider);
4221
+ if (!spec) return void 0;
4222
+ const record = loadConfig();
4223
+ const fromConfig = record[spec.key_config_name]?.trim();
4224
+ if (fromConfig) return fromConfig;
4225
+ if (spec.env_var) {
4226
+ const fromEnv = process.env[spec.env_var]?.trim();
4227
+ if (fromEnv) return fromEnv;
4228
+ }
4229
+ return void 0;
4230
+ }
4031
4231
  function hasProviderKey(provider) {
4032
- if (provider === "anthropic") return !!getAnthropicApiKey();
4033
- return !!getOpenAiApiKey();
4232
+ const spec = getProviderSpec(provider);
4233
+ if (!spec) return false;
4234
+ if (!spec.requires_key) return isEndpointEnabled(spec.id) || !!getProviderApiKey(provider);
4235
+ return !!getProviderApiKey(provider);
4034
4236
  }
4035
4237
  function getAvailableProviders() {
4036
- const out = [];
4037
- if (hasProviderKey("anthropic")) out.push("anthropic");
4038
- if (hasProviderKey("openai")) out.push("openai");
4039
- return out;
4238
+ return listProviderSpecs().filter((s) => hasProviderKey(s.id)).map((s) => s.id);
4040
4239
  }
4041
4240
  function hasAnyLlmProvider() {
4042
4241
  return getAvailableProviders().length > 0;
4043
4242
  }
4243
+ function hasKeylessConfiguredProvider() {
4244
+ return listProviderSpecs().some((s) => !s.requires_key && hasProviderKey(s.id));
4245
+ }
4044
4246
  function applyLazyMigration(config) {
4045
4247
  if (migrated) return;
4046
4248
  migrated = true;
4047
4249
  let changed = false;
4048
4250
  const record = config;
4049
4251
  if (!record["llm-primary"]) {
4050
- if (record["api-key"]) {
4051
- record["llm-primary"] = "anthropic";
4052
- changed = true;
4053
- } else if (record["openai-api-key"] || process.env.OPENAI_API_KEY) {
4054
- record["llm-primary"] = "openai";
4252
+ const available = getAvailableProviders();
4253
+ if (available.length > 0) {
4254
+ record["llm-primary"] = available[0];
4055
4255
  changed = true;
4056
4256
  }
4057
4257
  }
@@ -4091,20 +4291,20 @@ function loadLlmConfig() {
4091
4291
  openaiKey: getOpenAiApiKey()
4092
4292
  };
4093
4293
  }
4094
- function getProviderApiKey(provider) {
4095
- if (provider === "anthropic") return getAnthropicApiKey();
4096
- return getOpenAiApiKey();
4097
- }
4098
4294
  function getInvestigationApiKey(provider) {
4099
4295
  if (provider === "anthropic") {
4100
4296
  return process.env.NTRP_INVESTIGATION_API_KEY?.trim() || getAnthropicApiKey();
4101
4297
  }
4102
- return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
4298
+ if (provider === "openai") {
4299
+ return process.env.NTRP_INVESTIGATION_OPENAI_KEY?.trim() || getOpenAiApiKey();
4300
+ }
4301
+ return getProviderApiKey(provider);
4103
4302
  }
4104
4303
  var migrated;
4105
4304
  var init_llm_config = __esm({
4106
4305
  "src/config/llm-config.ts"() {
4107
4306
  "use strict";
4307
+ init_providers();
4108
4308
  init_store();
4109
4309
  migrated = false;
4110
4310
  }
@@ -4122,7 +4322,13 @@ function resolvePrimaryApiKey(ctx) {
4122
4322
  if (isInvestigationMode(ctx)) {
4123
4323
  return getInvestigationApiKey(primary) ?? getInvestigationApiKey("anthropic") ?? getInvestigationApiKey("openai");
4124
4324
  }
4125
- return getProviderApiKey(primary) ?? getProviderApiKey("anthropic") ?? getProviderApiKey("openai");
4325
+ const primaryKey = getProviderApiKey(primary);
4326
+ if (primaryKey) return primaryKey;
4327
+ for (const provider of getAvailableProviders()) {
4328
+ const key = getProviderApiKey(provider);
4329
+ if (key) return key;
4330
+ }
4331
+ return void 0;
4126
4332
  }
4127
4333
  function canUseReplAi(ctx) {
4128
4334
  if (!ctx) return false;
@@ -4133,25 +4339,21 @@ function canUseReplAi(ctx) {
4133
4339
  }
4134
4340
  function assertReplAi(ctx) {
4135
4341
  if (!ctx) {
4136
- throw new Error(
4137
- "AI features require stored API keys. Run `ntrp`, then /config set api-key or /config set openai-api-key."
4138
- );
4342
+ throw new Error(`AI features require stored API keys. Run \`ntrp\`, then /connect.`);
4139
4343
  }
4140
4344
  if (!canUseReplAi(ctx)) {
4141
4345
  if (!hasAnyLlmProvider()) {
4142
- throw new Error(
4143
- "No LLM API key configured. Run: /config set api-key (Anthropic) and/or /config set openai-api-key"
4144
- );
4346
+ throw new Error(NO_KEY_MESSAGE);
4145
4347
  }
4146
4348
  throw new Error(
4147
4349
  "AI features run only in the interactive REPL or headless mode with stored keys."
4148
4350
  );
4149
4351
  }
4150
4352
  const key = resolvePrimaryApiKey(ctx);
4151
- if (!key) {
4152
- throw new Error("No LLM API key configured. Run: /config set api-key or /config set openai-api-key");
4353
+ if (!key && !hasKeylessConfiguredProvider()) {
4354
+ throw new Error(NO_KEY_MESSAGE);
4153
4355
  }
4154
- return key;
4356
+ return key ?? "";
4155
4357
  }
4156
4358
  function hasEnvApiKeyHint() {
4157
4359
  return !!(process.env.ANTHROPIC_API_KEY ?? process.env.NTRP_API_KEY ?? process.env.OPENAI_API_KEY);
@@ -4164,10 +4366,12 @@ function describeLlmReadiness() {
4164
4366
  openai: providers.includes("openai")
4165
4367
  };
4166
4368
  }
4369
+ var NO_KEY_MESSAGE;
4167
4370
  var init_gate = __esm({
4168
4371
  "src/ai/llm/gate.ts"() {
4169
4372
  "use strict";
4170
4373
  init_llm_config();
4374
+ NO_KEY_MESSAGE = "No LLM API key configured. Run /connect and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).";
4171
4375
  }
4172
4376
  });
4173
4377
 
@@ -4212,6 +4416,12 @@ var init_types = __esm({
4212
4416
  });
4213
4417
 
4214
4418
  // src/ai/llm/errors.ts
4419
+ function isToolsUnsupportedMessage(message) {
4420
+ const msg = message.toLowerCase();
4421
+ const mentionsTools = msg.includes("tool") || msg.includes("function");
4422
+ const mentionsUnsupported = msg.includes("not support") || msg.includes("unsupported") || msg.includes("no support") || msg.includes("not available") || msg.includes("not enabled");
4423
+ return mentionsTools && mentionsUnsupported;
4424
+ }
4215
4425
  function mapAnthropicError(err, provider) {
4216
4426
  const e = err;
4217
4427
  const status = e.status;
@@ -4229,6 +4439,9 @@ function mapAnthropicError(err, provider) {
4229
4439
  if (status === 503) {
4230
4440
  return new LlmError("OVERLOADED", message, provider, status);
4231
4441
  }
4442
+ if (isToolsUnsupportedMessage(message)) {
4443
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
4444
+ }
4232
4445
  if (status === 404 || message.toLowerCase().includes("model")) {
4233
4446
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
4234
4447
  }
@@ -4237,6 +4450,11 @@ function mapAnthropicError(err, provider) {
4237
4450
  }
4238
4451
  return new LlmError("UNKNOWN", message, provider, status);
4239
4452
  }
4453
+ function isModelNotFoundMessage(message) {
4454
+ const msg = message.toLowerCase();
4455
+ if (!msg.includes("model")) return false;
4456
+ 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");
4457
+ }
4240
4458
  function mapOpenAiError(err, provider) {
4241
4459
  const e = err;
4242
4460
  const status = e.status;
@@ -4251,7 +4469,10 @@ function mapOpenAiError(err, provider) {
4251
4469
  if (status === 503 || code === "server_error") {
4252
4470
  return new LlmError("OVERLOADED", message, provider, status);
4253
4471
  }
4254
- if (status === 404 || code === "model_not_found") {
4472
+ if (isToolsUnsupportedMessage(message)) {
4473
+ return new LlmError("TOOLS_UNSUPPORTED", message, provider, status);
4474
+ }
4475
+ if (status === 404 || code === "model_not_found" || code === "model_decommissioned" || isModelNotFoundMessage(message)) {
4255
4476
  return new LlmError("MODEL_NOT_FOUND", message, provider, status);
4256
4477
  }
4257
4478
  if (code === "context_length_exceeded") {
@@ -4387,8 +4608,15 @@ var init_anthropic = __esm({
4387
4608
  }
4388
4609
  });
4389
4610
 
4390
- // src/ai/llm/adapters/openai.ts
4611
+ // src/ai/llm/adapters/openai-compat.ts
4391
4612
  import OpenAI from "openai";
4613
+ function makeClient(apiKey, baseUrl) {
4614
+ return new OpenAI({
4615
+ // Keyless endpoints (Ollama) still need a non-empty string for the SDK.
4616
+ apiKey: apiKey || "local",
4617
+ ...baseUrl ? { baseURL: baseUrl } : {}
4618
+ });
4619
+ }
4392
4620
  function toOpenAiTools(tools2) {
4393
4621
  return tools2.map((t) => ({
4394
4622
  type: "function",
@@ -4457,9 +4685,8 @@ function parseResponse2(message) {
4457
4685
  assistant_message: { role: "assistant", content: text, tool_calls }
4458
4686
  };
4459
4687
  }
4460
- async function openaiComplete(apiKey, model, req) {
4461
- const provider = "openai";
4462
- const client = new OpenAI({ apiKey });
4688
+ async function openaiCompatComplete(provider, baseUrl, apiKey, model, req) {
4689
+ const client = makeClient(apiKey, baseUrl);
4463
4690
  try {
4464
4691
  const response = await client.chat.completions.create({
4465
4692
  model,
@@ -4469,7 +4696,7 @@ async function openaiComplete(apiKey, model, req) {
4469
4696
  });
4470
4697
  const choice = response.choices[0];
4471
4698
  if (!choice?.message) {
4472
- throw new Error("OpenAI returned no message");
4699
+ throw new Error(`${provider} returned no message`);
4473
4700
  }
4474
4701
  const parsed = parseResponse2(choice.message);
4475
4702
  if (response.usage) {
@@ -4480,15 +4707,11 @@ async function openaiComplete(apiKey, model, req) {
4480
4707
  }
4481
4708
  return parsed;
4482
4709
  } catch (err) {
4483
- if (err instanceof OpenAI.APIError) {
4484
- throw mapOpenAiError(err, provider);
4485
- }
4486
4710
  throw mapOpenAiError(err, provider);
4487
4711
  }
4488
4712
  }
4489
- async function* openaiStream(apiKey, model, req) {
4490
- const provider = "openai";
4491
- const client = new OpenAI({ apiKey });
4713
+ async function* openaiCompatStream(provider, baseUrl, apiKey, model, req) {
4714
+ const client = makeClient(apiKey, baseUrl);
4492
4715
  try {
4493
4716
  const stream = await client.chat.completions.create({
4494
4717
  model,
@@ -4501,64 +4724,118 @@ async function* openaiStream(apiKey, model, req) {
4501
4724
  if (delta) yield { type: "text_delta", text: delta };
4502
4725
  }
4503
4726
  } catch (err) {
4504
- if (err instanceof OpenAI.APIError) {
4505
- throw mapOpenAiError(err, provider);
4506
- }
4507
4727
  throw mapOpenAiError(err, provider);
4508
4728
  }
4509
4729
  }
4510
- var init_openai = __esm({
4511
- "src/ai/llm/adapters/openai.ts"() {
4730
+ var init_openai_compat = __esm({
4731
+ "src/ai/llm/adapters/openai-compat.ts"() {
4512
4732
  "use strict";
4513
4733
  init_errors();
4514
4734
  }
4515
4735
  });
4516
4736
 
4517
- // src/ai/llm/catalog.ts
4518
- function getCatalogEntry(id) {
4519
- return byId.get(id);
4520
- }
4521
- function resolveRetiredModel(id) {
4522
- const seen = /* @__PURE__ */ new Set();
4523
- let current = id;
4524
- while (true) {
4525
- if (seen.has(current)) break;
4526
- seen.add(current);
4527
- const entry = byId.get(current);
4528
- if (!entry) return current;
4529
- if (entry.status === "active") return entry.id;
4530
- if (!entry.successor_id) {
4531
- const fallback = cheapestActiveInTier(entry.provider, entry.tier);
4532
- return fallback?.id ?? current;
4533
- }
4534
- current = entry.successor_id;
4737
+ // src/ai/llm/models-cache.ts
4738
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
4739
+ import { join as join8 } from "path";
4740
+ function cachePath() {
4741
+ return join8(ntrpHome(), "models.json");
4742
+ }
4743
+ function loadFile() {
4744
+ if (cached) return cached;
4745
+ const path = cachePath();
4746
+ if (!existsSync8(path)) {
4747
+ cached = { version: 1, providers: {} };
4748
+ return cached;
4535
4749
  }
4536
- return id;
4750
+ try {
4751
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
4752
+ cached = { version: 1, providers: parsed.providers ?? {} };
4753
+ } catch {
4754
+ cached = { version: 1, providers: {} };
4755
+ }
4756
+ return cached;
4757
+ }
4758
+ function saveFile(file) {
4759
+ writeFileSync7(cachePath(), JSON.stringify(file, null, 2) + "\n");
4760
+ cached = file;
4761
+ }
4762
+ function getProviderModels(provider) {
4763
+ return loadFile().providers[provider];
4764
+ }
4765
+ function setProviderModels(provider, entry) {
4766
+ const file = loadFile();
4767
+ file.providers[provider] = entry;
4768
+ saveFile(file);
4769
+ }
4770
+ function getCachedTierModel(provider, tier) {
4771
+ return getProviderModels(provider)?.tier_stack?.[tier];
4772
+ }
4773
+ function findCachedModel(provider, modelId) {
4774
+ return getProviderModels(provider)?.models.find((m) => m.id === modelId);
4775
+ }
4776
+ function cachedModelProvider(modelId) {
4777
+ const file = loadFile();
4778
+ for (const [provider, entry] of Object.entries(file.providers)) {
4779
+ if (entry.models.some((m) => m.id === modelId)) return provider;
4780
+ }
4781
+ return void 0;
4537
4782
  }
4538
- function cheapestActiveInTier(provider, tier) {
4783
+ function markModelNoTools(provider, modelId) {
4784
+ const file = loadFile();
4785
+ const entry = file.providers[provider];
4786
+ if (!entry) return;
4787
+ const noTools = new Set(entry.quirks?.no_tools ?? []);
4788
+ if (noTools.has(modelId)) return;
4789
+ noTools.add(modelId);
4790
+ entry.quirks = { ...entry.quirks, no_tools: [...noTools] };
4791
+ saveFile(file);
4792
+ }
4793
+ function modelHasNoToolsQuirk(provider, modelId) {
4794
+ return !!getProviderModels(provider)?.quirks?.no_tools?.includes(modelId);
4795
+ }
4796
+ function isProviderCacheStale(provider, ttlMs = CACHE_TTL_MS) {
4797
+ const entry = getProviderModels(provider);
4798
+ if (!entry) return true;
4799
+ const fetched = Date.parse(entry.fetched_at);
4800
+ if (Number.isNaN(fetched)) return true;
4801
+ return Date.now() - fetched > ttlMs;
4802
+ }
4803
+ var CACHE_TTL_MS, cached;
4804
+ var init_models_cache = __esm({
4805
+ "src/ai/llm/models-cache.ts"() {
4806
+ "use strict";
4807
+ init_store();
4808
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4809
+ cached = null;
4810
+ }
4811
+ });
4812
+
4813
+ // src/ai/llm/catalog.ts
4814
+ function catalogTierDefault(provider, tier) {
4539
4815
  const candidates = ENTRIES.filter(
4540
4816
  (e) => e.provider === provider && e.tier === tier && e.status === "active"
4541
4817
  );
4542
4818
  if (candidates.length === 0) return void 0;
4543
4819
  return candidates.sort((a, b) => a.relative_cost - b.relative_cost)[0];
4544
4820
  }
4545
- function getTierDefault(provider, tier) {
4546
- const entry = cheapestActiveInTier(provider, tier);
4547
- if (!entry) {
4548
- throw new Error(`No active ${tier}-tier model for provider ${provider} in catalog`);
4549
- }
4550
- return entry;
4821
+ function modelProviderHint(modelId) {
4822
+ return cachedModelProvider(modelId) ?? byId.get(modelId)?.provider;
4551
4823
  }
4552
- function resolveModel(provider, tier, override) {
4553
- if (override) {
4554
- const entry = byId.get(override);
4555
- if (!entry) return resolveRetiredModel(override);
4556
- if (entry.status === "retired") return resolveRetiredModel(override);
4557
- return entry.id;
4558
- }
4559
- return getTierDefault(provider, tier).id;
4824
+ function overrideForProvider(override, provider, activeProvider) {
4825
+ if (!override) return void 0;
4826
+ const hint = modelProviderHint(override);
4827
+ if (hint) return hint === provider ? override : void 0;
4828
+ return provider === activeProvider ? override : void 0;
4829
+ }
4830
+ function resolveModelSafe(provider, tier, override) {
4831
+ if (override) return override;
4832
+ const discovered = getCachedTierModel(provider, tier);
4833
+ if (discovered) return discovered;
4834
+ return catalogTierDefault(provider, tier)?.id;
4560
4835
  }
4561
4836
  function formatModelLabel(provider, modelId) {
4837
+ const cachedName = findCachedModel(provider, modelId)?.display_name;
4838
+ if (cachedName) return `${provider}/${cachedName}`;
4562
4839
  const entry = byId.get(modelId);
4563
4840
  return entry ? `${provider}/${entry.display_name}` : `${provider}/${modelId}`;
4564
4841
  }
@@ -4566,6 +4843,7 @@ var ENTRIES, byId;
4566
4843
  var init_catalog = __esm({
4567
4844
  "src/ai/llm/catalog.ts"() {
4568
4845
  "use strict";
4846
+ init_models_cache();
4569
4847
  ENTRIES = [
4570
4848
  {
4571
4849
  id: "claude-opus-4-6",
@@ -4638,6 +4916,292 @@ var init_catalog = __esm({
4638
4916
  }
4639
4917
  });
4640
4918
 
4919
+ // src/ai/llm/http.ts
4920
+ import { readFileSync as readFileSync8 } from "fs";
4921
+ function fixtureResponse(url, headers) {
4922
+ try {
4923
+ const raw = readFileSync8(process.env.NTRP_LLM_HTTP_FIXTURE, "utf-8");
4924
+ const entries = JSON.parse(raw);
4925
+ const headerValues = Object.values(headers).join(" ");
4926
+ for (const entry of entries) {
4927
+ if (!url.includes(entry.url_includes)) continue;
4928
+ if (entry.auth_includes && !headerValues.includes(entry.auth_includes)) continue;
4929
+ return { status: entry.status, ok: entry.status >= 200 && entry.status < 300, body: entry.body };
4930
+ }
4931
+ } catch {
4932
+ }
4933
+ return { status: 0, ok: false, body: void 0 };
4934
+ }
4935
+ async function llmHttpGetJson(url, headers, timeoutMs = 6e3) {
4936
+ if (process.env.NTRP_LLM_HTTP_FIXTURE) {
4937
+ return fixtureResponse(url, headers);
4938
+ }
4939
+ const controller = new AbortController();
4940
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4941
+ try {
4942
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
4943
+ let body;
4944
+ try {
4945
+ body = await res.json();
4946
+ } catch {
4947
+ body = void 0;
4948
+ }
4949
+ return { status: res.status, ok: res.ok, body };
4950
+ } catch {
4951
+ return { status: 0, ok: false, body: void 0 };
4952
+ } finally {
4953
+ clearTimeout(timer);
4954
+ }
4955
+ }
4956
+ var init_http = __esm({
4957
+ "src/ai/llm/http.ts"() {
4958
+ "use strict";
4959
+ }
4960
+ });
4961
+
4962
+ // src/ai/llm/ranking.ts
4963
+ function compareModels(a, b) {
4964
+ const createdA = a.created ?? 0;
4965
+ const createdB = b.created ?? 0;
4966
+ if (createdA !== createdB) return createdB - createdA;
4967
+ const versionA = extractVersion(a.id);
4968
+ const versionB = extractVersion(b.id);
4969
+ if (versionA !== versionB) return versionB - versionA;
4970
+ if (a.id.length !== b.id.length) return a.id.length - b.id.length;
4971
+ return a.id.localeCompare(b.id);
4972
+ }
4973
+ function extractVersion(id) {
4974
+ const match = id.match(/(\d+(?:\.\d+)?)/);
4975
+ return match ? Number(match[1]) : 0;
4976
+ }
4977
+ function pickByPatterns(models, patterns) {
4978
+ for (const pattern of patterns) {
4979
+ const matches = models.filter((m) => pattern.test(m.id));
4980
+ if (matches.length > 0) return [...matches].sort(compareModels)[0];
4981
+ }
4982
+ return void 0;
4983
+ }
4984
+ function genericBucket(model) {
4985
+ if (GENERIC_HIGH.test(model.id)) return "high";
4986
+ if (GENERIC_LOW.test(model.id)) return "low";
4987
+ return "medium";
4988
+ }
4989
+ function genericPick(models, tier) {
4990
+ const bucket = models.filter((m) => genericBucket(m) === tier);
4991
+ if (bucket.length > 0) return [...bucket].sort(compareModels)[0];
4992
+ return void 0;
4993
+ }
4994
+ function rankModels(providerId, models) {
4995
+ if (models.length === 0) return null;
4996
+ const preferences = PROVIDER_PREFERENCES[providerId];
4997
+ const picks = {};
4998
+ for (const tier of ["high", "medium", "low"]) {
4999
+ const preferred = preferences ? pickByPatterns(models, preferences[tier]) : void 0;
5000
+ const generic = preferred ?? genericPick(models, tier);
5001
+ if (generic) picks[tier] = generic.id;
5002
+ }
5003
+ const anyModel = [...models].sort(compareModels)[0].id;
5004
+ const high = picks.high ?? picks.medium ?? picks.low ?? anyModel;
5005
+ const medium = picks.medium ?? picks.high ?? picks.low ?? anyModel;
5006
+ const low = picks.low ?? picks.medium ?? picks.high ?? anyModel;
5007
+ return { high, medium, low };
5008
+ }
5009
+ var PROVIDER_PREFERENCES, GENERIC_LOW, GENERIC_HIGH;
5010
+ var init_ranking = __esm({
5011
+ "src/ai/llm/ranking.ts"() {
5012
+ "use strict";
5013
+ PROVIDER_PREFERENCES = {
5014
+ anthropic: {
5015
+ high: [/^claude-opus/i, /^claude-sonnet/i],
5016
+ medium: [/^claude-sonnet/i, /^claude-haiku/i],
5017
+ low: [/^claude-haiku/i, /^claude-sonnet/i]
5018
+ },
5019
+ openai: {
5020
+ high: [/^gpt-5(?!.*(mini|nano|chat))/i, /^gpt-4\.1(?!.*(mini|nano))/i, /^gpt-4o(?!.*mini)/i, /^o3(?!.*mini)/i],
5021
+ medium: [/^gpt-5.*mini/i, /^gpt-4\.1-mini/i, /^gpt-4o-mini/i, /^o4-mini/i],
5022
+ low: [/^gpt-5.*nano/i, /^gpt-4\.1-nano/i, /^gpt-4o-mini/i]
5023
+ },
5024
+ google: {
5025
+ high: [/^gemini-[\d.]+-pro/i, /^gemini-[\d.]+-flash(?!-lite)/i],
5026
+ medium: [/^gemini-[\d.]+-flash(?!-lite|-8b)/i, /^gemini-[\d.]+-pro/i],
5027
+ low: [/^gemini-[\d.]+-flash-lite/i, /flash-8b/i, /^gemini-[\d.]+-flash(?!-lite)/i]
5028
+ },
5029
+ groq: {
5030
+ high: [/llama-3\.3-70b/i, /gpt-oss-120b/i, /70b/i, /deepseek-r1/i],
5031
+ medium: [/llama-3\.1-8b-instant/i, /gpt-oss-20b/i, /llama.*8b/i],
5032
+ low: [/8b-instant/i, /llama.*8b/i, /gemma/i]
5033
+ },
5034
+ deepseek: {
5035
+ high: [/reasoner/i, /chat/i],
5036
+ medium: [/chat/i],
5037
+ low: [/chat/i]
5038
+ },
5039
+ mistral: {
5040
+ high: [/large/i, /medium/i],
5041
+ medium: [/medium/i, /^mistral-small/i],
5042
+ low: [/ministral/i, /small/i, /tiny/i]
5043
+ },
5044
+ xai: {
5045
+ high: [/^grok-\d+(?!.*(mini|fast))/i, /^grok(?!.*(mini|fast))/i],
5046
+ medium: [/^grok.*mini(?!.*fast)/i, /^grok.*fast/i],
5047
+ low: [/^grok.*mini.*fast/i, /^grok.*mini/i]
5048
+ },
5049
+ openrouter: {
5050
+ high: [/^openrouter\/auto$/i, /claude.*opus/i, /^openai\/gpt-5(?!.*(mini|nano))/i, /gemini.*pro/i],
5051
+ medium: [/claude.*sonnet/i, /gpt-5.*mini/i, /gpt-4\.1-mini/i, /gemini.*flash(?!-lite)/i],
5052
+ low: [/claude.*haiku/i, /nano/i, /flash-lite/i, /mini/i]
5053
+ }
5054
+ };
5055
+ GENERIC_LOW = /(mini|nano|lite|tiny|micro|small|haiku|instant|flash|turbo|\b0?\.?5b\b|\b[1-8]b\b)/i;
5056
+ GENERIC_HIGH = /(opus|ultra|large|max\b|\bpro\b|405b|253b|235b|120b|72b|70b|reason|-r1\b|think|deep)/i;
5057
+ }
5058
+ });
5059
+
5060
+ // src/ai/llm/discovery.ts
5061
+ function authHeaders(spec, apiKey) {
5062
+ if (spec.api === "anthropic") {
5063
+ return { "x-api-key": apiKey ?? "", "anthropic-version": "2023-06-01" };
5064
+ }
5065
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
5066
+ }
5067
+ function normalizeItem(spec, item) {
5068
+ let id = item.id ?? "";
5069
+ if (!id) return null;
5070
+ if (id.startsWith("models/")) id = id.slice("models/".length);
5071
+ const model = { id };
5072
+ const display = item.display_name ?? item.name;
5073
+ if (display && display !== id) model.display_name = display;
5074
+ if (typeof item.created === "number") model.created = item.created;
5075
+ else if (item.created_at) {
5076
+ const parsed = Date.parse(item.created_at);
5077
+ if (!Number.isNaN(parsed)) model.created = Math.floor(parsed / 1e3);
5078
+ }
5079
+ if (typeof item.context_length === "number") model.context_length = item.context_length;
5080
+ if (Array.isArray(item.supported_parameters)) {
5081
+ model.supports_tools = item.supported_parameters.includes("tools");
5082
+ }
5083
+ return model;
5084
+ }
5085
+ async function fetchProviderModels(spec, apiKey, timeoutMs = 6e3) {
5086
+ const headers = authHeaders(spec, apiKey);
5087
+ if (spec.api === "anthropic") {
5088
+ const models2 = [];
5089
+ let url = modelsUrl(spec);
5090
+ for (let page = 0; page < 5 && url; page++) {
5091
+ const res2 = await llmHttpGetJson(url, headers, timeoutMs);
5092
+ if (!res2.ok) return models2.length > 0 ? { ok: true, models: models2 } : { ok: false, status: res2.status };
5093
+ const body2 = res2.body;
5094
+ for (const item of body2?.data ?? []) {
5095
+ const model = normalizeItem(spec, item);
5096
+ if (model) models2.push(model);
5097
+ }
5098
+ url = body2?.has_more && body2.last_id ? `${spec.base_url}/v1/models?limit=100&after_id=${encodeURIComponent(body2.last_id)}` : null;
5099
+ }
5100
+ return { ok: true, models: models2 };
5101
+ }
5102
+ const res = await llmHttpGetJson(modelsUrl(spec), headers, timeoutMs);
5103
+ if (!res.ok) return { ok: false, status: res.status };
5104
+ const body = res.body;
5105
+ const list = Array.isArray(body) ? body : body?.data ?? [];
5106
+ const models = [];
5107
+ for (const item of list) {
5108
+ const model = normalizeItem(spec, item);
5109
+ if (model) models.push(model);
5110
+ }
5111
+ return { ok: true, models };
5112
+ }
5113
+ function filterChatModels(spec, models) {
5114
+ const extra = PROVIDER_EXCLUDE[spec.id];
5115
+ return models.filter((m) => !NON_CHAT.test(m.id) && !(extra && extra.test(m.id)));
5116
+ }
5117
+ function storeDiscoveredModels(providerId, rawModels) {
5118
+ const spec = getProviderSpec(providerId);
5119
+ if (!spec || rawModels.length === 0) return null;
5120
+ const chat = filterChatModels(spec, rawModels);
5121
+ const usable = chat.length > 0 ? chat : rawModels;
5122
+ const stack = rankModels(providerId, usable);
5123
+ if (!stack) return null;
5124
+ const prior = getProviderModels(providerId);
5125
+ const entry = {
5126
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString(),
5127
+ models: usable,
5128
+ tier_stack: stack,
5129
+ ...prior?.quirks ? { quirks: prior.quirks } : {}
5130
+ };
5131
+ setProviderModels(providerId, entry);
5132
+ return entry;
5133
+ }
5134
+ async function refreshProviderModels(providerId, opts = {}) {
5135
+ const spec = getProviderSpec(providerId);
5136
+ if (!spec) return null;
5137
+ if (!opts.force && !isProviderCacheStale(providerId)) {
5138
+ return getProviderModels(providerId) ?? null;
5139
+ }
5140
+ const apiKey = opts.apiKey ?? getProviderApiKey(providerId);
5141
+ if (spec.requires_key && !apiKey) return null;
5142
+ const result = await fetchProviderModels(spec, apiKey);
5143
+ if (!result.ok) return null;
5144
+ return storeDiscoveredModels(providerId, result.models);
5145
+ }
5146
+ function rerankExcluding(providerId, deadModelId) {
5147
+ const prior = getProviderModels(providerId);
5148
+ if (!prior) return null;
5149
+ const survivors = prior.models.filter((m) => m.id !== deadModelId);
5150
+ const stack = rankModels(providerId, survivors);
5151
+ if (!stack) return null;
5152
+ const entry = { ...prior, models: survivors, tier_stack: stack };
5153
+ setProviderModels(providerId, entry);
5154
+ return entry;
5155
+ }
5156
+ var NON_CHAT, PROVIDER_EXCLUDE;
5157
+ var init_discovery = __esm({
5158
+ "src/ai/llm/discovery.ts"() {
5159
+ "use strict";
5160
+ init_llm_config();
5161
+ init_http();
5162
+ init_models_cache();
5163
+ init_providers();
5164
+ init_ranking();
5165
+ 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;
5166
+ PROVIDER_EXCLUDE = {
5167
+ openai: /(chatgpt|-search|deep-research|-pro\b|computer-use|codex-mini|-instruct\b)/i
5168
+ };
5169
+ }
5170
+ });
5171
+
5172
+ // src/ai/llm/heal.ts
5173
+ async function healModelNotFound(opts) {
5174
+ const { provider, tier, deadModel } = opts;
5175
+ const refreshed = await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true });
5176
+ let candidate = refreshed?.tier_stack?.[tier] ?? getCachedTierModel(provider, tier);
5177
+ if (!candidate || candidate === deadModel) {
5178
+ const reranked = rerankExcluding(provider, deadModel);
5179
+ candidate = reranked?.tier_stack?.[tier];
5180
+ }
5181
+ if (!candidate || candidate === deadModel) return null;
5182
+ clearDeadOverride(deadModel, opts.ctx);
5183
+ return {
5184
+ model: candidate,
5185
+ notice: `model ${deadModel} is no longer available \u2014 switched to ${candidate}`
5186
+ };
5187
+ }
5188
+ function clearDeadOverride(deadModel, ctx) {
5189
+ if (getConfigValue("llm-model-override")?.trim() === deadModel) {
5190
+ deleteConfigValue("llm-model-override");
5191
+ }
5192
+ if (ctx?.llm?.modelOverride === deadModel) {
5193
+ ctx.llm.modelOverride = void 0;
5194
+ }
5195
+ }
5196
+ var init_heal = __esm({
5197
+ "src/ai/llm/heal.ts"() {
5198
+ "use strict";
5199
+ init_store();
5200
+ init_discovery();
5201
+ init_models_cache();
5202
+ }
5203
+ });
5204
+
4641
5205
  // src/ai/llm/surfaces.ts
4642
5206
  function tierForSurface(surface, userTier) {
4643
5207
  const spec = SURFACE_SPECS[surface];
@@ -4682,7 +5246,7 @@ function resolveActiveProvider(ctx) {
4682
5246
  if (session && hasProviderKey(session)) return session;
4683
5247
  const cfg = loadLlmConfig();
4684
5248
  if (hasProviderKey(cfg.primary)) return cfg.primary;
4685
- const available = ["anthropic", "openai"].filter(hasProviderKey);
5249
+ const available = getAvailableProviders();
4686
5250
  if (available.length > 0) return available[0];
4687
5251
  return cfg.primary;
4688
5252
  }
@@ -4708,8 +5272,8 @@ function resolveProviderOrder(ctx) {
4708
5272
  for (const p of cfg.failoverOrder) {
4709
5273
  if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
4710
5274
  }
4711
- for (const p of ["anthropic", "openai"]) {
4712
- if (p !== active && hasProviderKey(p) && !order.includes(p)) order.push(p);
5275
+ for (const p of getAvailableProviders()) {
5276
+ if (p !== active && !order.includes(p)) order.push(p);
4713
5277
  }
4714
5278
  return order;
4715
5279
  }
@@ -4723,23 +5287,16 @@ var init_session_state = __esm({
4723
5287
  });
4724
5288
 
4725
5289
  // src/ai/llm/resolver.ts
4726
- function getProviderOrder(config, ctx) {
4727
- void config;
4728
- return resolveProviderOrder(ctx);
4729
- }
4730
5290
  function resolveCompletionContext(surface, opts = {}) {
4731
5291
  const activeProvider = resolveActiveProvider(opts.ctx);
4732
5292
  const tier = opts.tier ?? resolveEffectiveTier(opts.ctx, surface);
4733
5293
  const override = opts.modelOverride ?? resolveEffectiveModelOverride(opts.ctx);
4734
5294
  const providerOrder = resolveProviderOrder(opts.ctx);
4735
5295
  const modelByProvider = {};
4736
- for (const provider of providerOrder) {
4737
- const providerOverride = override && getCatalogEntry(override)?.provider === provider ? override : void 0;
4738
- modelByProvider[provider] = resolveModel(provider, tier, providerOverride);
4739
- }
4740
- if (!modelByProvider[activeProvider]) {
4741
- const activeOverride = override && getCatalogEntry(override)?.provider === activeProvider ? override : void 0;
4742
- modelByProvider[activeProvider] = resolveModel(activeProvider, tier, activeOverride);
5296
+ for (const provider of /* @__PURE__ */ new Set([...providerOrder, activeProvider])) {
5297
+ const providerOverride = overrideForProvider(override, provider, activeProvider);
5298
+ const model = resolveModelSafe(provider, tier, providerOverride);
5299
+ if (model) modelByProvider[provider] = model;
4743
5300
  }
4744
5301
  return {
4745
5302
  providerOrder,
@@ -4765,70 +5322,142 @@ var init_resolver = __esm({
4765
5322
 
4766
5323
  // src/ai/llm/failover.ts
4767
5324
  async function completeOnProvider(provider, model, apiKey, req) {
4768
- if (provider === "anthropic") return anthropicComplete(apiKey, model, req);
4769
- return openaiComplete(apiKey, model, req);
5325
+ const spec = getProviderSpec(provider);
5326
+ if (!spec) {
5327
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
5328
+ }
5329
+ if (spec.api === "anthropic") {
5330
+ return anthropicComplete(apiKey ?? "", model, req);
5331
+ }
5332
+ return openaiCompatComplete(provider, spec.base_url, apiKey, model, req);
5333
+ }
5334
+ function usableKey(provider, ctx) {
5335
+ const spec = getProviderSpec(provider);
5336
+ if (!spec) return { ok: false };
5337
+ const apiKey = getApiKeyForProvider(provider, ctx);
5338
+ if (spec.requires_key && !apiKey) return { ok: false };
5339
+ return { ok: true, apiKey };
5340
+ }
5341
+ async function resolveModelWithDiscovery(provider, cfg, opts) {
5342
+ const known = cfg.modelByProvider[provider];
5343
+ if (known) return known;
5344
+ const override = overrideForProvider(opts.modelOverride, provider, cfg.activeProvider);
5345
+ const direct = resolveModelSafe(provider, cfg.tier, override);
5346
+ if (direct) return direct;
5347
+ await refreshProviderModels(provider, { apiKey: opts.apiKey, force: true }).catch(() => null);
5348
+ return resolveModelSafe(provider, cfg.tier, override);
5349
+ }
5350
+ function stripTools(req) {
5351
+ const { tools: _tools, ...rest } = req;
5352
+ return rest;
4770
5353
  }
4771
5354
  async function completeWithFailover(req, opts = {}) {
4772
- const ctx = resolveCompletionContext(req.surface, {
5355
+ const cfg = resolveCompletionContext(req.surface, {
4773
5356
  max_tokens: req.max_tokens,
4774
5357
  tier: opts.tier,
4775
5358
  modelOverride: opts.modelOverride,
4776
5359
  ctx: opts.ctx
4777
5360
  });
4778
- const providers = ctx.providerOrder;
5361
+ const providers = cfg.providerOrder;
4779
5362
  if (providers.length === 0) {
4780
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
5363
+ throw new Error(NO_PROVIDER_MESSAGE);
4781
5364
  }
5365
+ const notices = [];
4782
5366
  let lastError;
4783
5367
  let failoverFrom;
5368
+ const buildMeta = (provider, model, response) => ({
5369
+ provider_used: provider,
5370
+ model_used: model,
5371
+ ...response.token_usage ?? {},
5372
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
5373
+ ...notices.length > 0 ? { notices: [...notices] } : {}
5374
+ });
4784
5375
  for (let i = 0; i < providers.length; i++) {
4785
5376
  const provider = providers[i];
4786
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
4787
- if (!apiKey) continue;
4788
- let model = ctx.modelByProvider[provider] ?? resolveModel(provider, ctx.tier, opts.modelOverride);
5377
+ const key = usableKey(provider, opts.ctx);
5378
+ if (!key.ok) continue;
5379
+ let model = await resolveModelWithDiscovery(provider, cfg, {
5380
+ modelOverride: opts.modelOverride,
5381
+ apiKey: key.apiKey
5382
+ });
5383
+ if (!model) {
5384
+ lastError = new LlmError(
5385
+ "MODEL_NOT_FOUND",
5386
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
5387
+ provider
5388
+ );
5389
+ continue;
5390
+ }
5391
+ let effectiveReq = req;
5392
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
5393
+ effectiveReq = stripTools(req);
5394
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
5395
+ }
4789
5396
  try {
4790
- const response = await completeOnProvider(provider, model, apiKey, req);
4791
- const meta = {
4792
- provider_used: provider,
4793
- model_used: model,
4794
- ...response.token_usage ?? {},
4795
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
4796
- };
5397
+ const response = await completeOnProvider(provider, model, key.apiKey, effectiveReq);
5398
+ const meta = buildMeta(provider, model, response);
4797
5399
  recordLlmUsage(response.token_usage);
4798
5400
  return { response, meta };
4799
5401
  } catch (err) {
4800
- const llmErr = err;
5402
+ let llmErr = err;
4801
5403
  if (llmErr.name !== "LlmError") throw err;
4802
5404
  lastError = llmErr;
4803
- if (llmErr.code === "MODEL_NOT_FOUND") {
4804
- model = resolveModel(provider, ctx.tier);
5405
+ if (llmErr.code === "TOOLS_UNSUPPORTED" && effectiveReq.tools?.length) {
5406
+ markModelNoTools(provider, model);
5407
+ notices.push(`${model} doesn't support tool calling \u2014 retrying without live data tools`);
4805
5408
  try {
4806
- const response = await completeOnProvider(provider, model, apiKey, req);
4807
- const meta = {
4808
- provider_used: provider,
4809
- model_used: model,
4810
- ...response.token_usage ?? {},
4811
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
4812
- };
5409
+ const response = await completeOnProvider(provider, model, key.apiKey, stripTools(effectiveReq));
5410
+ const meta = buildMeta(provider, model, response);
4813
5411
  recordLlmUsage(response.token_usage);
4814
5412
  return { response, meta };
4815
5413
  } catch (retryErr) {
4816
5414
  const retryLlm = retryErr;
4817
- if (retryLlm.name === "LlmError") lastError = retryLlm;
4818
- else throw retryErr;
5415
+ if (retryLlm.name !== "LlmError") throw retryErr;
5416
+ lastError = retryLlm;
5417
+ llmErr = retryLlm;
5418
+ }
5419
+ }
5420
+ if (llmErr.code === "MODEL_NOT_FOUND") {
5421
+ const healed = await healModelNotFound({
5422
+ provider,
5423
+ tier: cfg.tier,
5424
+ deadModel: model,
5425
+ apiKey: key.apiKey,
5426
+ ctx: opts.ctx
5427
+ }).catch(() => null);
5428
+ if (healed) {
5429
+ notices.push(healed.notice);
5430
+ model = healed.model;
5431
+ let retryReq = req;
5432
+ if (req.tools?.length && modelHasNoToolsQuirk(provider, model)) {
5433
+ retryReq = stripTools(req);
5434
+ notices.push(`${model} doesn't support tool calling \u2014 answering without live data tools`);
5435
+ }
5436
+ try {
5437
+ const response = await completeOnProvider(provider, model, key.apiKey, retryReq);
5438
+ const meta = buildMeta(provider, model, response);
5439
+ recordLlmUsage(response.token_usage);
5440
+ return { response, meta };
5441
+ } catch (retryErr) {
5442
+ const retryLlm = retryErr;
5443
+ if (retryLlm.name !== "LlmError") throw retryErr;
5444
+ lastError = retryLlm;
5445
+ llmErr = retryLlm;
5446
+ }
4819
5447
  }
4820
5448
  }
4821
5449
  if (!isFailoverEligible(llmErr.code)) throw llmErr;
4822
5450
  const next = providers[i + 1];
4823
5451
  if (next) {
4824
5452
  failoverFrom = failoverFrom ?? provider;
5453
+ notices.push(`${provider} unavailable (${llmErr.code.toLowerCase()}) \u2014 trying ${next}`);
4825
5454
  opts.onFailover?.(provider, next, llmErr.code);
4826
5455
  continue;
4827
5456
  }
4828
5457
  throw llmErr;
4829
5458
  }
4830
5459
  }
4831
- throw lastError ?? new Error("No LLM provider available");
5460
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
4832
5461
  }
4833
5462
  async function* streamWithFailover(req, opts = {}) {
4834
5463
  const cfg = resolveCompletionContext(req.surface, {
@@ -4837,23 +5466,47 @@ async function* streamWithFailover(req, opts = {}) {
4837
5466
  modelOverride: opts.modelOverride,
4838
5467
  ctx: opts.ctx
4839
5468
  });
4840
- const providers = getProviderOrder();
5469
+ const providers = cfg.providerOrder;
4841
5470
  if (providers.length === 0) {
4842
- throw new Error("No LLM provider configured. Set api-key and/or openai-api-key via /config.");
5471
+ throw new Error(NO_PROVIDER_MESSAGE);
4843
5472
  }
5473
+ const notices = [];
4844
5474
  let lastError;
4845
5475
  let failoverFrom;
5476
+ async function* streamOnProvider(provider, model, apiKey) {
5477
+ const spec = getProviderSpec(provider);
5478
+ if (!spec) {
5479
+ throw new LlmError("UNKNOWN", `Unknown provider "${provider}" \u2014 run /connect to register it.`, provider);
5480
+ }
5481
+ if (spec.api === "anthropic") {
5482
+ yield* anthropicStream(apiKey ?? "", model, req);
5483
+ return;
5484
+ }
5485
+ yield* openaiCompatStream(provider, spec.base_url, apiKey, model, req);
5486
+ }
4846
5487
  for (let i = 0; i < providers.length; i++) {
4847
5488
  const provider = providers[i];
4848
- const apiKey = getApiKeyForProvider(provider, opts.ctx);
4849
- if (!apiKey) continue;
4850
- const model = cfg.modelByProvider[provider] ?? resolveModel(provider, cfg.tier, opts.modelOverride);
4851
- try {
5489
+ const key = usableKey(provider, opts.ctx);
5490
+ if (!key.ok) continue;
5491
+ let model = await resolveModelWithDiscovery(provider, cfg, {
5492
+ modelOverride: opts.modelOverride,
5493
+ apiKey: key.apiKey
5494
+ });
5495
+ if (!model) {
5496
+ lastError = new LlmError(
5497
+ "MODEL_NOT_FOUND",
5498
+ `No models known for provider "${provider}". Run /connect or /model refresh.`,
5499
+ provider
5500
+ );
5501
+ continue;
5502
+ }
5503
+ let yieldedAny = false;
5504
+ const attempt = async function* (attemptModel) {
4852
5505
  let fullText = "";
4853
- const streamFn = provider === "anthropic" ? anthropicStream : openaiStream;
4854
- for await (const event of streamFn(apiKey, model, req)) {
5506
+ for await (const event of streamOnProvider(provider, attemptModel, key.apiKey)) {
4855
5507
  if (event.type === "text_delta") {
4856
5508
  fullText += event.text;
5509
+ yieldedAny = true;
4857
5510
  yield event;
4858
5511
  }
4859
5512
  }
@@ -4861,10 +5514,11 @@ async function* streamWithFailover(req, opts = {}) {
4861
5514
  recordLlmUsage({ input_tokens: 0, output_tokens: estimatedOut });
4862
5515
  const meta = {
4863
5516
  provider_used: provider,
4864
- model_used: model,
5517
+ model_used: attemptModel,
4865
5518
  input_tokens: 0,
4866
5519
  output_tokens: estimatedOut,
4867
- ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {}
5520
+ ...failoverFrom ? { failover: true, failover_from: failoverFrom } : {},
5521
+ ...notices.length > 0 ? { notices: [...notices] } : {}
4868
5522
  };
4869
5523
  yield {
4870
5524
  type: "done",
@@ -4876,52 +5530,86 @@ async function* streamWithFailover(req, opts = {}) {
4876
5530
  },
4877
5531
  meta
4878
5532
  };
5533
+ };
5534
+ try {
5535
+ yield* attempt(model);
4879
5536
  return;
4880
5537
  } catch (err) {
4881
5538
  const llmErr = err;
4882
5539
  if (llmErr.name !== "LlmError") throw err;
4883
5540
  lastError = llmErr;
4884
- if (!isFailoverEligible(llmErr.code)) throw llmErr;
5541
+ if (yieldedAny) throw llmErr;
5542
+ if (llmErr.code === "MODEL_NOT_FOUND") {
5543
+ const healed = await healModelNotFound({
5544
+ provider,
5545
+ tier: cfg.tier,
5546
+ deadModel: model,
5547
+ apiKey: key.apiKey,
5548
+ ctx: opts.ctx
5549
+ }).catch(() => null);
5550
+ if (healed) {
5551
+ notices.push(healed.notice);
5552
+ model = healed.model;
5553
+ try {
5554
+ yield* attempt(model);
5555
+ return;
5556
+ } catch (retryErr) {
5557
+ const retryLlm = retryErr;
5558
+ if (retryLlm.name !== "LlmError") throw retryErr;
5559
+ lastError = retryLlm;
5560
+ if (yieldedAny) throw retryLlm;
5561
+ }
5562
+ }
5563
+ }
5564
+ if (!isFailoverEligible(lastError.code)) throw lastError;
4885
5565
  const next = providers[i + 1];
4886
5566
  if (next) {
4887
5567
  failoverFrom = failoverFrom ?? provider;
4888
- opts.onFailover?.(provider, next, llmErr.code);
5568
+ notices.push(`${provider} unavailable (${lastError.code.toLowerCase()}) \u2014 trying ${next}`);
5569
+ opts.onFailover?.(provider, next, lastError.code);
4889
5570
  continue;
4890
5571
  }
4891
- throw llmErr;
5572
+ throw lastError;
4892
5573
  }
4893
5574
  }
4894
- throw lastError ?? new Error("No LLM provider available");
5575
+ throw lastError ?? new Error(NO_PROVIDER_MESSAGE);
4895
5576
  }
5577
+ var NO_PROVIDER_MESSAGE;
4896
5578
  var init_failover = __esm({
4897
5579
  "src/ai/llm/failover.ts"() {
4898
5580
  "use strict";
4899
5581
  init_usage_stats();
4900
5582
  init_anthropic();
4901
- init_openai();
5583
+ init_openai_compat();
4902
5584
  init_catalog();
5585
+ init_discovery();
4903
5586
  init_errors();
5587
+ init_heal();
5588
+ init_models_cache();
5589
+ init_providers();
5590
+ init_types();
4904
5591
  init_resolver();
5592
+ NO_PROVIDER_MESSAGE = "No LLM provider configured. Run /connect and paste any API key (Anthropic, OpenAI, Groq, Gemini, ...).";
4905
5593
  }
4906
5594
  });
4907
5595
 
4908
5596
  // src/config/profile.ts
4909
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
4910
- import { join as join7 } from "path";
5597
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
5598
+ import { join as join9 } from "path";
4911
5599
  function profilePath() {
4912
5600
  return PROFILE_PATH;
4913
5601
  }
4914
5602
  function profileExists() {
4915
- return existsSync7(PROFILE_PATH);
5603
+ return existsSync9(PROFILE_PATH);
4916
5604
  }
4917
5605
  function isProfileConfigured(profile = loadProfile()) {
4918
5606
  if (!profile) return false;
4919
5607
  return profile.company_name.trim().length > 0;
4920
5608
  }
4921
5609
  function loadProfile() {
4922
- if (!existsSync7(PROFILE_PATH)) return null;
5610
+ if (!existsSync9(PROFILE_PATH)) return null;
4923
5611
  try {
4924
- const parsed = JSON.parse(readFileSync6(PROFILE_PATH, "utf-8"));
5612
+ const parsed = JSON.parse(readFileSync9(PROFILE_PATH, "utf-8"));
4925
5613
  if (!parsed || typeof parsed !== "object") return null;
4926
5614
  return parsed;
4927
5615
  } catch {
@@ -4934,21 +5622,21 @@ var init_profile = __esm({
4934
5622
  "use strict";
4935
5623
  init_store();
4936
5624
  NTRP_DIR3 = ntrpHome();
4937
- PROFILE_PATH = join7(NTRP_DIR3, "profile.json");
5625
+ PROFILE_PATH = join9(NTRP_DIR3, "profile.json");
4938
5626
  }
4939
5627
  });
4940
5628
 
4941
5629
  // src/data/playbook.ts
4942
- import { existsSync as existsSync8, readFileSync as readFileSync7, appendFileSync } from "fs";
4943
- import { join as join8 } from "path";
5630
+ import { existsSync as existsSync10, readFileSync as readFileSync10, appendFileSync } from "fs";
5631
+ import { join as join10 } from "path";
4944
5632
  function playsPath() {
4945
- return join8(getMemoryDir(), PLAYS_FILE);
5633
+ return join10(getMemoryDir(), PLAYS_FILE);
4946
5634
  }
4947
5635
  function getCustomPlays() {
4948
5636
  const path = playsPath();
4949
- if (!existsSync8(path)) return [];
5637
+ if (!existsSync10(path)) return [];
4950
5638
  const out = [];
4951
- for (const line of readFileSync7(path, "utf-8").split("\n")) {
5639
+ for (const line of readFileSync10(path, "utf-8").split("\n")) {
4952
5640
  const trimmed = line.trim();
4953
5641
  if (!trimmed) continue;
4954
5642
  try {
@@ -5395,7 +6083,8 @@ async function* streamFindings(input, ctx) {
5395
6083
  findings,
5396
6084
  model_used: meta.model_used,
5397
6085
  provider_used: meta.provider_used,
5398
- raw_prompt: userMessage
6086
+ raw_prompt: userMessage,
6087
+ usage: meta
5399
6088
  };
5400
6089
  }
5401
6090
  }
@@ -5696,9 +6385,9 @@ var init_tool_schemas = __esm({
5696
6385
  });
5697
6386
 
5698
6387
  // src/ai/privacy.ts
5699
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, appendFileSync as appendFileSync2 } from "fs";
6388
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, appendFileSync as appendFileSync2 } from "fs";
5700
6389
  import { homedir as homedir3 } from "os";
5701
- import { join as join9 } from "path";
6390
+ import { join as join11 } from "path";
5702
6391
  function stripPII(obj) {
5703
6392
  if (obj === null || obj === void 0) return obj;
5704
6393
  if (typeof obj !== "object") return obj;
@@ -5713,14 +6402,14 @@ function stripPII(obj) {
5713
6402
  return out;
5714
6403
  }
5715
6404
  function ensureAuditDir() {
5716
- if (!existsSync9(AUDIT_DIR)) {
6405
+ if (!existsSync11(AUDIT_DIR)) {
5717
6406
  mkdirSync7(AUDIT_DIR, { recursive: true });
5718
6407
  }
5719
6408
  }
5720
6409
  function logToolCall(entry) {
5721
6410
  ensureAuditDir();
5722
6411
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5723
- const path = join9(AUDIT_DIR, `agentic-${date}.jsonl`);
6412
+ const path = join11(AUDIT_DIR, `agentic-${date}.jsonl`);
5724
6413
  appendFileSync2(path, JSON.stringify(entry) + "\n");
5725
6414
  }
5726
6415
  var PII_FIELDS, AUDIT_DIR;
@@ -5744,7 +6433,7 @@ var init_privacy = __esm({
5744
6433
  "raw_data",
5745
6434
  "metadata"
5746
6435
  ]);
5747
- AUDIT_DIR = join9(homedir3(), ".ntrp", "audit");
6436
+ AUDIT_DIR = join11(homedir3(), ".ntrp", "audit");
5748
6437
  }
5749
6438
  });
5750
6439
 
@@ -7982,7 +8671,7 @@ async function runMetricsAnalysis(options = {}) {
7982
8671
  if (options.findings) {
7983
8672
  if (!canUseReplAi(options.ctx)) {
7984
8673
  throw new Error(
7985
- "AI metrics findings require stored API keys. Run `ntrp`, set api-key and/or openai-api-key, then /metrics --findings."
8674
+ "AI metrics findings require stored API keys. Run `ntrp`, then /connect (any provider key), then /metrics --findings."
7986
8675
  );
7987
8676
  }
7988
8677
  options.onProgress?.("findings");
@@ -8337,6 +9026,9 @@ function formatLlmAttribution(meta) {
8337
9026
  return line;
8338
9027
  }
8339
9028
  function printLlmAttribution(meta) {
9029
+ for (const notice of meta.notices ?? []) {
9030
+ console.log(chalk8.dim(` ${notice}`));
9031
+ }
8340
9032
  const line = formatLlmAttribution(meta);
8341
9033
  if (line) console.log(chalk8.dim(` ${line}`));
8342
9034
  }
@@ -8609,6 +9301,7 @@ async function renderDiagnoseStream(options) {
8609
9301
  let modelUsed = "";
8610
9302
  let providerUsed;
8611
9303
  let failover;
9304
+ let notices;
8612
9305
  let rawPrompt = "";
8613
9306
  try {
8614
9307
  for await (const event of runFindings(fullResult)) {
@@ -8624,6 +9317,7 @@ async function renderDiagnoseStream(options) {
8624
9317
  modelUsed = event.model_used;
8625
9318
  providerUsed = event.provider_used;
8626
9319
  failover = event.failover;
9320
+ notices = event.usage?.notices;
8627
9321
  rawPrompt = event.raw_prompt;
8628
9322
  }
8629
9323
  }
@@ -8647,7 +9341,8 @@ async function renderDiagnoseStream(options) {
8647
9341
  printLlmAttribution({
8648
9342
  model_used: modelUsed,
8649
9343
  provider_used: providerUsed,
8650
- failover
9344
+ failover,
9345
+ notices
8651
9346
  });
8652
9347
  } catch (err) {
8653
9348
  findingsSpinner.fail(deep ? "Agentic investigation failed" : "AI findings failed");
@@ -9070,7 +9765,7 @@ async function handler(args, ctx) {
9070
9765
  console.log();
9071
9766
  console.log(" " + chalk11.red("AI findings run only in the interactive REPL."));
9072
9767
  console.log(" " + chalk11.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
9073
- 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."));
9768
+ console.log(" " + chalk11.dim("Start with ") + paint("accent", "ntrp") + chalk11.dim(", run ") + paint("accent", "/connect") + chalk11.dim(" (any provider key), then /diagnose --findings."));
9074
9769
  console.log();
9075
9770
  return;
9076
9771
  }
@@ -12075,18 +12770,18 @@ var init_generator = __esm({
12075
12770
  });
12076
12771
 
12077
12772
  // src/demo/taxonomy-cache.ts
12078
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync10, mkdirSync as mkdirSync8, unlinkSync as unlinkSync3 } from "fs";
12773
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync8, unlinkSync as unlinkSync3 } from "fs";
12079
12774
  import { homedir as homedir4 } from "os";
12080
- import { join as join10 } from "path";
12775
+ import { join as join12 } from "path";
12081
12776
  function ensureDir5() {
12082
- if (!existsSync10(NTRP_DIR4)) {
12777
+ if (!existsSync12(NTRP_DIR4)) {
12083
12778
  mkdirSync8(NTRP_DIR4, { recursive: true });
12084
12779
  }
12085
12780
  }
12086
12781
  function loadCachedTaxonomy(profile) {
12087
- if (!existsSync10(TAXONOMY_PATH)) return null;
12782
+ if (!existsSync12(TAXONOMY_PATH)) return null;
12088
12783
  try {
12089
- const parsed = JSON.parse(readFileSync8(TAXONOMY_PATH, "utf-8"));
12784
+ const parsed = JSON.parse(readFileSync11(TAXONOMY_PATH, "utf-8"));
12090
12785
  if (!parsed || typeof parsed !== "object") return null;
12091
12786
  if (parsed.profile_updated_at !== profile.updated_at) return null;
12092
12787
  return parsed;
@@ -12096,14 +12791,14 @@ function loadCachedTaxonomy(profile) {
12096
12791
  }
12097
12792
  function saveCachedTaxonomy(taxonomy) {
12098
12793
  ensureDir5();
12099
- writeFileSync7(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
12794
+ writeFileSync9(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
12100
12795
  }
12101
12796
  var NTRP_DIR4, TAXONOMY_PATH;
12102
12797
  var init_taxonomy_cache = __esm({
12103
12798
  "src/demo/taxonomy-cache.ts"() {
12104
12799
  "use strict";
12105
- NTRP_DIR4 = join10(homedir4(), ".ntrp");
12106
- TAXONOMY_PATH = join10(NTRP_DIR4, "demo-taxonomy.json");
12800
+ NTRP_DIR4 = join12(homedir4(), ".ntrp");
12801
+ TAXONOMY_PATH = join12(NTRP_DIR4, "demo-taxonomy.json");
12107
12802
  }
12108
12803
  });
12109
12804
 
@@ -12432,8 +13127,8 @@ function markFailure(ctx) {
12432
13127
  }
12433
13128
  async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
12434
13129
  if (!forceRegen) {
12435
- const cached = loadCachedTaxonomy(profile);
12436
- if (cached) return cached;
13130
+ const cached2 = loadCachedTaxonomy(profile);
13131
+ if (cached2) return cached2;
12437
13132
  }
12438
13133
  const spinnerText = forceRegen ? "Rebuilding market taxonomy\u2026" : "Researching your market taxonomy\u2026";
12439
13134
  const spinner = ora4({ text: spinnerText, discardStdin: false }).start();
@@ -12536,7 +13231,7 @@ __export(ingest_exports, {
12536
13231
  });
12537
13232
  import chalk15 from "chalk";
12538
13233
  import ora5 from "ora";
12539
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
13234
+ import { readFileSync as readFileSync12, existsSync as existsSync13 } from "fs";
12540
13235
  import { basename as basename2 } from "path";
12541
13236
  async function handler3(args, ctx) {
12542
13237
  const { positional, flags } = parseArgs(args, [
@@ -12560,7 +13255,7 @@ async function handler3(args, ctx) {
12560
13255
  console.error(chalk15.dim(" /ingest --demo [--scenario <name>]"));
12561
13256
  process.exit(1);
12562
13257
  }
12563
- if (!existsSync11(file)) {
13258
+ if (!existsSync13(file)) {
12564
13259
  console.error(chalk15.red(` File not found: ${file}`));
12565
13260
  process.exit(1);
12566
13261
  }
@@ -12578,7 +13273,7 @@ async function handler3(args, ctx) {
12578
13273
  try {
12579
13274
  await initSchema();
12580
13275
  spinner.text = "Parsing CSV...";
12581
- const content = readFileSync9(file, "utf-8");
13276
+ const content = readFileSync12(file, "utf-8");
12582
13277
  const { rows, headers } = parseCSV(content);
12583
13278
  if (rows.length === 0) {
12584
13279
  spinner.fail("CSV is empty");
@@ -12694,7 +13389,7 @@ __export(ingest_chat_exports, {
12694
13389
  loadDemoFromChat: () => loadDemoFromChat,
12695
13390
  looksLikeFilePath: () => looksLikeFilePath
12696
13391
  });
12697
- import { existsSync as existsSync12 } from "fs";
13392
+ import { existsSync as existsSync14 } from "fs";
12698
13393
  import { basename as basename3, resolve as resolve4 } from "path";
12699
13394
  import { homedir as homedir5 } from "os";
12700
13395
  import chalk16 from "chalk";
@@ -12714,11 +13409,11 @@ function extractFilePath(input) {
12714
13409
  const m = trimmed.match(re);
12715
13410
  if (m?.[1]) {
12716
13411
  const p = expandPath(m[1]);
12717
- if (existsSync12(p)) return p;
13412
+ if (existsSync14(p)) return p;
12718
13413
  }
12719
13414
  if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
12720
13415
  const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
12721
- if (existsSync12(p)) return p;
13416
+ if (existsSync14(p)) return p;
12722
13417
  }
12723
13418
  }
12724
13419
  return null;
@@ -12748,12 +13443,12 @@ async function ingestFromChat(ctx, filePath) {
12748
13443
  }
12749
13444
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
12750
13445
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
12751
- const { readFileSync: readFileSync15 } = await import("fs");
13446
+ const { readFileSync: readFileSync18 } = await import("fs");
12752
13447
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
12753
13448
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
12754
13449
  let headerCheckFailed = false;
12755
13450
  try {
12756
- const raw = readFileSync15(filePath, "utf-8");
13451
+ const raw = readFileSync18(filePath, "utf-8");
12757
13452
  const { headers } = parseCSV2(raw);
12758
13453
  const detected = detectEntityType2(headers, "unknown");
12759
13454
  if (!detected) headerCheckFailed = true;
@@ -13892,7 +14587,7 @@ async function runDiagnosis(options = {}) {
13892
14587
  if (options.findings) {
13893
14588
  if (!canUseReplAi(options.ctx)) {
13894
14589
  throw new Error(
13895
- "AI findings require stored API keys. Run `ntrp`, then /config set api-key and/or /config set openai-api-key, and use /diagnose --findings."
14590
+ "AI findings require stored API keys. Run `ntrp`, then /connect (any provider key), and use /diagnose --findings."
13896
14591
  );
13897
14592
  }
13898
14593
  if (options.deep) {
@@ -14100,24 +14795,24 @@ JSON SHAPE:
14100
14795
 
14101
14796
  // src/strategies/readers.ts
14102
14797
  import { createHash } from "crypto";
14103
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
14798
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
14104
14799
  import { extname, resolve as resolve5 } from "path";
14105
14800
  import { parse as parseYaml } from "yaml";
14106
14801
  import { PDFParse } from "pdf-parse";
14107
14802
  async function readStrategyFile(pathOrDash) {
14108
14803
  if (pathOrDash === "-") {
14109
- const text2 = readFileSync10(0, "utf-8");
14804
+ const text2 = readFileSync13(0, "utf-8");
14110
14805
  return createDocument("stdin", null, text2, {});
14111
14806
  }
14112
14807
  const sourcePath = resolve5(pathOrDash);
14113
- if (!existsSync13(sourcePath)) {
14808
+ if (!existsSync15(sourcePath)) {
14114
14809
  throw new NtrpError("strategy_file_not_found", `Strategy file not found: ${pathOrDash}`, 2 /* Usage */);
14115
14810
  }
14116
14811
  const ext = extname(sourcePath).toLowerCase();
14117
14812
  if (ext === ".pdf") {
14118
14813
  return readPdf(sourcePath);
14119
14814
  }
14120
- const text = readFileSync10(sourcePath, "utf-8");
14815
+ const text = readFileSync13(sourcePath, "utf-8");
14121
14816
  if (ext === ".yaml" || ext === ".yml") {
14122
14817
  const structured = parseStructuredYaml(text);
14123
14818
  return createDocument("yaml", sourcePath, text, structured);
@@ -14132,7 +14827,7 @@ function readStrategyText(text) {
14132
14827
  return createDocument("text", null, text, {});
14133
14828
  }
14134
14829
  async function readPdf(sourcePath) {
14135
- const data = readFileSync10(sourcePath);
14830
+ const data = readFileSync13(sourcePath);
14136
14831
  const parser = new PDFParse({ data });
14137
14832
  try {
14138
14833
  const result = await parser.getText();
@@ -14179,15 +14874,15 @@ var init_readers = __esm({
14179
14874
  });
14180
14875
 
14181
14876
  // src/strategies/library.ts
14182
- import { writeFileSync as writeFileSync8 } from "fs";
14183
- import { join as join11 } from "path";
14877
+ import { writeFileSync as writeFileSync10 } from "fs";
14878
+ import { join as join13 } from "path";
14184
14879
  import { stringify as stringifyYaml } from "yaml";
14185
14880
  function strategyLibraryPath(slug) {
14186
- return join11(getStrategiesDir(), `${slug}.md`);
14881
+ return join13(getStrategiesDir(), `${slug}.md`);
14187
14882
  }
14188
14883
  function writeStrategyMarkdown(strategy) {
14189
14884
  const path = strategyLibraryPath(strategy.slug);
14190
- writeFileSync8(path, renderStrategyMarkdown(strategy), "utf-8");
14885
+ writeFileSync10(path, renderStrategyMarkdown(strategy), "utf-8");
14191
14886
  return path;
14192
14887
  }
14193
14888
  function renderStrategyMarkdown(strategy) {
@@ -14261,7 +14956,7 @@ var init_library = __esm({
14261
14956
  // src/strategies/connectors.ts
14262
14957
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
14263
14958
  import { homedir as homedir6 } from "os";
14264
- import { basename as basename4, extname as extname2, join as join12, relative, resolve as resolve6, sep as sep2 } from "path";
14959
+ import { basename as basename4, extname as extname2, join as join14, relative, resolve as resolve6, sep as sep2 } from "path";
14265
14960
  function createLocalFolderConnector(options) {
14266
14961
  const rootPath = resolveUserPath(options.rootPath);
14267
14962
  const name = options.name ?? (basename4(rootPath) || "local");
@@ -14304,7 +14999,7 @@ function createLocalFolderConnector(options) {
14304
14999
  }
14305
15000
  function walkLocalFolder(rootPath, currentPath, refs, opts) {
14306
15001
  for (const entry of readdirSync2(currentPath, { withFileTypes: true })) {
14307
- const absolutePath = join12(currentPath, entry.name);
15002
+ const absolutePath = join14(currentPath, entry.name);
14308
15003
  const relativePath = normalizePath(relative(rootPath, absolutePath));
14309
15004
  if (entry.isDirectory()) {
14310
15005
  if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
@@ -14368,7 +15063,7 @@ function normalizePath(path) {
14368
15063
  }
14369
15064
  function resolveUserPath(path) {
14370
15065
  if (path === "~") return homedir6();
14371
- if (path.startsWith("~/")) return join12(homedir6(), path.slice(2));
15066
+ if (path.startsWith("~/")) return join14(homedir6(), path.slice(2));
14372
15067
  return resolve6(path);
14373
15068
  }
14374
15069
  var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
@@ -14613,8 +15308,8 @@ async function callProvider(texts) {
14613
15308
  async function embedText(text) {
14614
15309
  const key = text.trim();
14615
15310
  if (!key) return null;
14616
- const cached = cache.get(key);
14617
- if (cached) return cached;
15311
+ const cached2 = cache.get(key);
15312
+ if (cached2) return cached2;
14618
15313
  const result = await callProvider([key]);
14619
15314
  const vec = result?.[0] ?? null;
14620
15315
  if (vec) cache.set(key, vec);
@@ -14624,8 +15319,8 @@ async function embedItems(items) {
14624
15319
  const needing = [];
14625
15320
  const out = items.map((it, index) => {
14626
15321
  if (it.embedding && it.embedding.length > 0) return { ...it };
14627
- const cached = cache.get(it.text.trim());
14628
- if (cached) return { ...it, embedding: cached };
15322
+ const cached2 = cache.get(it.text.trim());
15323
+ if (cached2) return { ...it, embedding: cached2 };
14629
15324
  needing.push({ index, text: it.text });
14630
15325
  return { ...it };
14631
15326
  });
@@ -14784,17 +15479,17 @@ var init_retrieval = __esm({
14784
15479
  });
14785
15480
 
14786
15481
  // src/memory/knowledge.ts
14787
- import { existsSync as existsSync14, readFileSync as readFileSync11, appendFileSync as appendFileSync3, readdirSync as readdirSync3 } from "fs";
14788
- import { join as join15 } from "path";
15482
+ import { existsSync as existsSync16, readFileSync as readFileSync14, appendFileSync as appendFileSync3, readdirSync as readdirSync3 } from "fs";
15483
+ import { join as join17 } from "path";
14789
15484
  import { randomUUID as randomUUID4 } from "crypto";
14790
15485
  function knowledgePath() {
14791
- return join15(getMemoryDir(), KNOWLEDGE_FILE);
15486
+ return join17(getMemoryDir(), KNOWLEDGE_FILE);
14792
15487
  }
14793
15488
  function loadKnowledgeChunks() {
14794
15489
  const path = knowledgePath();
14795
- if (!existsSync14(path)) return [];
15490
+ if (!existsSync16(path)) return [];
14796
15491
  const out = [];
14797
- for (const line of readFileSync11(path, "utf-8").split("\n")) {
15492
+ for (const line of readFileSync14(path, "utf-8").split("\n")) {
14798
15493
  const trimmed = line.trim();
14799
15494
  if (!trimmed) continue;
14800
15495
  try {
@@ -14827,17 +15522,17 @@ __export(store_exports2, {
14827
15522
  rewriteJsonl: () => rewriteJsonl,
14828
15523
  scrubText: () => scrubText
14829
15524
  });
14830
- import { existsSync as existsSync15, readFileSync as readFileSync12, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync10 } from "fs";
14831
- import { join as join16 } from "path";
15525
+ import { existsSync as existsSync17, readFileSync as readFileSync15, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync12 } from "fs";
15526
+ import { join as join18 } from "path";
14832
15527
  import { randomUUID as randomUUID5 } from "crypto";
14833
15528
  function memPath(file) {
14834
- return join16(getMemoryDir(), file);
15529
+ return join18(getMemoryDir(), file);
14835
15530
  }
14836
15531
  function readJsonl(file) {
14837
15532
  const path = memPath(file);
14838
- if (!existsSync15(path)) return [];
15533
+ if (!existsSync17(path)) return [];
14839
15534
  const out = [];
14840
- for (const line of readFileSync12(path, "utf-8").split("\n")) {
15535
+ for (const line of readFileSync15(path, "utf-8").split("\n")) {
14841
15536
  const trimmed = line.trim();
14842
15537
  if (!trimmed) continue;
14843
15538
  try {
@@ -14855,7 +15550,7 @@ function appendJsonl(file, obj) {
14855
15550
  }
14856
15551
  function rewriteJsonl(file, rows) {
14857
15552
  try {
14858
- writeFileSync10(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
15553
+ writeFileSync12(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
14859
15554
  } catch {
14860
15555
  }
14861
15556
  }
@@ -14917,7 +15612,7 @@ function loadWinSnippets() {
14917
15612
  const out = [];
14918
15613
  for (const name of readdirSync4(dir)) {
14919
15614
  if (!name.endsWith(".md") || name.toLowerCase() === "readme.md") continue;
14920
- const raw = readFileSync12(join16(dir, name), "utf-8");
15615
+ const raw = readFileSync15(join18(dir, name), "utf-8");
14921
15616
  const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? name.replace(/\.md$/, "");
14922
15617
  const body = raw.replace(/^#.*$/m, "").replace(/\s+/g, " ").trim().slice(0, 300);
14923
15618
  out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });
@@ -15091,7 +15786,7 @@ init_session_analysis();
15091
15786
  init_queries();
15092
15787
  init_diagnosis();
15093
15788
  init_strategy();
15094
- import { join as join14 } from "path";
15789
+ import { join as join16 } from "path";
15095
15790
 
15096
15791
  // src/services/publish.ts
15097
15792
  init_queries();
@@ -15256,8 +15951,8 @@ function buildSections(input) {
15256
15951
 
15257
15952
  // src/repositories/markdown.ts
15258
15953
  init_formatters();
15259
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
15260
- import { basename as basename5, dirname as dirname2, join as join13, resolve as resolve7 } from "path";
15954
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
15955
+ import { basename as basename5, dirname as dirname2, join as join15, resolve as resolve7 } from "path";
15261
15956
  import { stringify as stringifyYaml2 } from "yaml";
15262
15957
  var markdownRepositoryAdapter = {
15263
15958
  kind: "markdown",
@@ -15282,9 +15977,9 @@ var markdownRepositoryAdapter = {
15282
15977
  mkdirSync9(root, { recursive: true });
15283
15978
  const written = [];
15284
15979
  for (const file of files) {
15285
- const absolutePath = join13(root, file.relativePath);
15980
+ const absolutePath = join15(root, file.relativePath);
15286
15981
  mkdirSync9(dirname2(absolutePath), { recursive: true });
15287
- writeFileSync9(absolutePath, file.contents, "utf-8");
15982
+ writeFileSync11(absolutePath, file.contents, "utf-8");
15288
15983
  written.push(absolutePath);
15289
15984
  }
15290
15985
  return {
@@ -15600,7 +16295,7 @@ async function runSmokeProtocol(_input, ctx) {
15600
16295
  });
15601
16296
  const proposalResult = await proposeRepositoryExport({
15602
16297
  target: "markdown",
15603
- directory: join14(getExportsDir(), "repository-smoke"),
16298
+ directory: join16(getExportsDir(), "repository-smoke"),
15604
16299
  source: "smoke_protocol",
15605
16300
  modelOrFixture: "smoke-protocol-v1"
15606
16301
  });
@@ -15764,11 +16459,12 @@ async function runAsk(question, ctx) {
15764
16459
 
15765
16460
  // src/services/setup.ts
15766
16461
  init_repl_api();
16462
+ init_providers();
15767
16463
  init_llm_config();
15768
16464
  init_store();
15769
16465
  init_profile();
15770
- import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
15771
- import { join as join17 } from "path";
16466
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
16467
+ import { join as join19 } from "path";
15772
16468
 
15773
16469
  // src/license/verify.ts
15774
16470
  init_store();
@@ -15965,8 +16661,8 @@ function setupCheck() {
15965
16661
  let writable = false;
15966
16662
  try {
15967
16663
  mkdirSync10(home, { recursive: true });
15968
- const probe = join17(home, ".write-check");
15969
- writeFileSync11(probe, "ok\n");
16664
+ const probe = join19(home, ".write-check");
16665
+ writeFileSync13(probe, "ok\n");
15970
16666
  writable = true;
15971
16667
  } catch {
15972
16668
  writable = false;
@@ -15991,7 +16687,8 @@ function setupCheck() {
15991
16687
  tier: llmCfg.tier,
15992
16688
  auto_failover: llmCfg.autoFailover,
15993
16689
  anthropic: llmReady.anthropic,
15994
- openai: llmReady.openai
16690
+ openai: llmReady.openai,
16691
+ providers: llmReady.providers
15995
16692
  }
15996
16693
  },
15997
16694
  license: {
@@ -16010,18 +16707,18 @@ init_serialize();
16010
16707
  init_errors2();
16011
16708
 
16012
16709
  // src/version.ts
16013
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
16014
- import { dirname as dirname3, join as join18 } from "path";
16710
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
16711
+ import { dirname as dirname3, join as join20 } from "path";
16015
16712
  import { fileURLToPath } from "url";
16016
16713
  var cachedVersion;
16017
16714
  function getInstalledVersion() {
16018
16715
  if (cachedVersion) return cachedVersion;
16019
16716
  const start = dirname3(fileURLToPath(import.meta.url));
16020
16717
  for (const rel of ["../package.json", "../../package.json"]) {
16021
- const path = join18(start, rel);
16022
- if (!existsSync17(path)) continue;
16718
+ const path = join20(start, rel);
16719
+ if (!existsSync19(path)) continue;
16023
16720
  try {
16024
- const pkg = JSON.parse(readFileSync14(path, "utf-8"));
16721
+ const pkg = JSON.parse(readFileSync17(path, "utf-8"));
16025
16722
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
16026
16723
  cachedVersion = pkg.version;
16027
16724
  return cachedVersion;
@@ -16097,10 +16794,13 @@ async function callTool(name, input) {
16097
16794
  case "ntrp_setup_check":
16098
16795
  return toolContent(setupCheck());
16099
16796
  case "ntrp_diagnose": {
16797
+ const wantsFindings = Boolean(input.findings || input.deep);
16100
16798
  const result = await runDiagnosis({
16101
- findings: Boolean(input.findings || input.deep),
16799
+ findings: wantsFindings,
16102
16800
  deep: Boolean(input.deep),
16103
- segment: typeof input.segment === "string" ? input.segment : void 0
16801
+ segment: typeof input.segment === "string" ? input.segment : void 0,
16802
+ // Without a ctx, canUseReplAi() rejects AI findings even with stored keys.
16803
+ ...wantsFindings ? { ctx: await initHeadlessAgentContext() } : {}
16104
16804
  });
16105
16805
  return toolContent({
16106
16806
  health: serializeHealth(result.health),