infernoflow 0.37.1 → 0.37.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,407 +1,2 @@
1
- /**
2
- * infernoflow ai
3
- *
4
- * Manage AI provider configuration for infernoflow commands
5
- * (explain, why, review, changelog, etc.)
6
- *
7
- * Subcommands:
8
- * infernoflow ai setup Interactive guided setup
9
- * infernoflow ai status Show configured providers and which is active
10
- * infernoflow ai test [provider] Send a test prompt and show response
11
- * infernoflow ai clear [provider] Remove a provider's API key from config
12
- *
13
- * Config is stored in inferno/integrations.json (project-scoped).
14
- * API keys can also come from environment variables (checked first).
15
- *
16
- * Supported providers:
17
- * anthropic ANTHROPIC_API_KEY claude-sonnet-4-6
18
- * openai OPENAI_API_KEY gpt-4o
19
- * gemini GOOGLE_AI_API_KEY gemini-2.0-flash
20
- * openrouter OPENROUTER_API_KEY (any model)
21
- * ollama (local, no key) llama3.2
22
- */
23
-
24
- import * as fs from "node:fs";
25
- import * as path from "node:path";
26
- import * as https from "node:https";
27
- import * as http from "node:http";
28
- import * as readline from "node:readline";
29
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
30
-
31
- // ── config helpers ────────────────────────────────────────────────────────────
32
-
33
- function infernoDir(cwd) { return path.join(cwd, "inferno"); }
34
-
35
- function loadConfig(cwd) {
36
- const p = path.join(infernoDir(cwd), "integrations.json");
37
- if (!fs.existsSync(p)) return {};
38
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
39
- }
40
-
41
- function saveConfig(cwd, config) {
42
- const dir = infernoDir(cwd);
43
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
44
- fs.writeFileSync(path.join(dir, "integrations.json"), JSON.stringify(config, null, 2) + "\n");
45
- }
46
-
47
- // ── provider definitions ──────────────────────────────────────────────────────
48
-
49
- const PROVIDERS = [
50
- {
51
- id: "anthropic",
52
- name: "Anthropic (Claude)",
53
- envKey: "ANTHROPIC_API_KEY",
54
- models: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"],
55
- default: "claude-sonnet-4-6",
56
- keyHint: "sk-ant-api03-…",
57
- docsUrl: "https://console.anthropic.com/settings/keys",
58
- },
59
- {
60
- id: "openai",
61
- name: "OpenAI (GPT)",
62
- envKey: "OPENAI_API_KEY",
63
- models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
64
- default: "gpt-4o",
65
- keyHint: "sk-…",
66
- docsUrl: "https://platform.openai.com/api-keys",
67
- },
68
- {
69
- id: "gemini",
70
- name: "Google Gemini",
71
- envKey: "GOOGLE_AI_API_KEY",
72
- models: ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"],
73
- default: "gemini-2.0-flash",
74
- keyHint: "AIza…",
75
- docsUrl: "https://aistudio.google.com/app/apikey",
76
- },
77
- {
78
- id: "openrouter",
79
- name: "OpenRouter",
80
- envKey: "OPENROUTER_API_KEY",
81
- models: ["anthropic/claude-sonnet-4-6", "openai/gpt-4o", "meta-llama/llama-3.1-8b-instruct:free"],
82
- default: "anthropic/claude-sonnet-4-6",
83
- keyHint: "sk-or-…",
84
- docsUrl: "https://openrouter.ai/keys",
85
- },
86
- {
87
- id: "ollama",
88
- name: "Ollama (local)",
89
- envKey: null,
90
- models: ["llama3.2", "mistral", "codellama", "phi3"],
91
- default: "llama3.2",
92
- keyHint: null,
93
- docsUrl: "https://ollama.com",
94
- },
95
- ];
96
-
97
- // ── HTTP probe ────────────────────────────────────────────────────────────────
98
-
99
- function httpGet(url) {
100
- return new Promise((resolve) => {
101
- const parsed = new URL(url);
102
- const lib = parsed.protocol === "https:" ? https : http;
103
- const req = lib.request({ hostname: parsed.hostname, port: parsed.port || (parsed.protocol === "https:" ? 443 : 80), path: parsed.pathname + (parsed.search || ""), method: "GET", timeout: 5000 }, (res) => {
104
- let raw = "";
105
- res.on("data", d => (raw += d));
106
- res.on("end", () => { try { resolve({ status: res.statusCode, body: JSON.parse(raw) }); } catch { resolve({ status: res.statusCode, body: raw }); } });
107
- });
108
- req.on("error", () => resolve(null));
109
- req.on("timeout", () => { req.destroy(); resolve(null); });
110
- req.end();
111
- });
112
- }
113
-
114
- // ── provider status check ─────────────────────────────────────────────────────
115
-
116
- async function checkProviderStatus(providerId, config) {
117
- const envMap = {
118
- anthropic: process.env.ANTHROPIC_API_KEY,
119
- openai: process.env.OPENAI_API_KEY,
120
- gemini: process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY,
121
- openrouter: process.env.OPENROUTER_API_KEY,
122
- };
123
-
124
- const fromEnv = envMap[providerId];
125
- const fromConfig = config[providerId]?.apiKey;
126
- const key = fromEnv || fromConfig;
127
- const source = fromEnv ? "env" : fromConfig ? "integrations.json" : null;
128
- const model = config[providerId]?.model || PROVIDERS.find(p => p.id === providerId)?.default;
129
-
130
- if (providerId === "ollama") {
131
- // Check if Ollama is running
132
- const probe = await httpGet("http://localhost:11434/api/tags").catch(() => null);
133
- if (probe?.status === 200) {
134
- const models = probe.body?.models?.map(m => m.name) || [];
135
- return { configured: true, source: "local", model: config.ollama?.model || "llama3.2", available: true, models };
136
- }
137
- return { configured: false, source: null, model: null, available: false };
138
- }
139
-
140
- return { configured: !!key, source, model, available: null, masked: key ? key.slice(0, 8) + "…" : null };
141
- }
142
-
143
- // ── ai test prompt ────────────────────────────────────────────────────────────
144
-
145
- async function testProvider(providerId, config, cwd) {
146
- try {
147
- const { callAI } = await import("../ai/providerRouter.mjs");
148
- const testPrompt = `Reply with exactly: "infernoflow AI test OK — ${providerId}"`;
149
- const result = await callAI(testPrompt, cwd, providerId);
150
- return result;
151
- } catch {
152
- return null;
153
- }
154
- }
155
-
156
- // ── readline helper ───────────────────────────────────────────────────────────
157
-
158
- function prompt(rl, question) {
159
- return new Promise(resolve => rl.question(question, resolve));
160
- }
161
-
162
- // ── subcommands ───────────────────────────────────────────────────────────────
163
-
164
- async function cmdStatus(cwd) {
165
- const config = loadConfig(cwd);
166
-
167
- console.log();
168
- console.log(` ${bold("infernoflow ai")} ${gray("— provider status")}`);
169
- console.log();
170
-
171
- let anyConfigured = false;
172
-
173
- for (const p of PROVIDERS) {
174
- const status = await checkProviderStatus(p.id, config);
175
- if (status.configured) anyConfigured = true;
176
-
177
- const icon = status.configured ? green("✓") : gray("○");
178
- const label = bold(p.name.padEnd(22));
179
- const info = status.configured
180
- ? `${green("configured")} ${gray(status.source)} ${gray("model: " + status.model)}${status.masked ? " " + gray(status.masked) : ""}`
181
- : gray("not configured");
182
- console.log(` ${icon} ${label} ${info}`);
183
- }
184
-
185
- console.log();
186
-
187
- if (!anyConfigured) {
188
- console.log(` ${yellow("No AI providers configured.")} Run: ${cyan("infernoflow ai setup")}`);
189
- console.log(` ${gray("Without a provider, explain/why/review use structural fallbacks.")}`);
190
- } else {
191
- console.log(` ${gray("Run")} ${cyan("infernoflow ai test")} ${gray("to verify the active provider.")}`);
192
- }
193
- console.log();
194
- }
195
-
196
- async function cmdSetup(cwd) {
197
- const config = loadConfig(cwd);
198
-
199
- // Check which providers already have keys (env vars or saved config)
200
- const envKeys = {
201
- anthropic: process.env.ANTHROPIC_API_KEY,
202
- openai: process.env.OPENAI_API_KEY,
203
- gemini: process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY,
204
- openrouter: process.env.OPENROUTER_API_KEY,
205
- };
206
-
207
- console.log();
208
- console.log(` ${bold("🔥 infernoflow ai setup")}`);
209
- console.log(` ${gray("Connect an AI provider for explain, why, review, and changelog.")}`);
210
- console.log();
211
-
212
- // Numbered menu
213
- PROVIDERS.forEach((p, i) => {
214
- const envKey = envKeys[p.id];
215
- const savedKey = config[p.id]?.apiKey;
216
- const detected = envKey ? green(" ✓ key detected in environment") :
217
- savedKey ? green(" ✓ key already saved") : "";
218
- const num = bold(String(i + 1));
219
- const local = p.id === "ollama" ? gray(" (local, no key needed)") : "";
220
- console.log(` ${num}) ${bold(p.name.padEnd(22))}${local}${detected}`);
221
- });
222
-
223
- console.log();
224
-
225
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
226
-
227
- try {
228
- // Numbered selection
229
- const choice = await prompt(rl, ` Select provider [1]: `);
230
- const idx = (parseInt(choice.trim()) || 1) - 1;
231
-
232
- if (idx < 0 || idx >= PROVIDERS.length) {
233
- console.log(red(` Invalid choice. Enter a number 1–${PROVIDERS.length}.`));
234
- return;
235
- }
236
-
237
- const provider = PROVIDERS[idx];
238
- const providerId = provider.id;
239
-
240
- console.log();
241
- console.log(` ${bold(provider.name)}`);
242
-
243
- if (providerId === "ollama") {
244
- // Ollama — no key needed
245
- const hostInput = await prompt(rl, ` Ollama host [http://localhost:11434]: `);
246
- const modelInput = await prompt(rl, ` Model [${provider.default}]: `);
247
-
248
- config.ollama = {
249
- host: hostInput.trim() || "http://localhost:11434",
250
- model: modelInput.trim() || provider.default,
251
- };
252
- saveConfig(cwd, config);
253
-
254
- console.log();
255
- process.stdout.write(` ${green("✓")} Saved. Testing connection… `);
256
- const probe = await httpGet(`${config.ollama.host}/api/tags`).catch(() => null);
257
- if (probe?.status === 200) {
258
- console.log(green("OK"));
259
- } else {
260
- console.log(yellow("not reachable"));
261
- console.log(` ${yellow("⚠")} Start Ollama first: ${cyan("ollama serve")}`);
262
- }
263
-
264
- } else {
265
- // API key provider
266
- const envKey = envKeys[providerId];
267
- const savedKey = config[providerId]?.apiKey;
268
- const existing = envKey || savedKey;
269
-
270
- if (existing) {
271
- // Key already detected — confirm or replace
272
- const source = envKey ? "environment variable" : "saved config";
273
- console.log(` ${green("✓")} API key detected from ${source}: ${gray(existing.slice(0, 12) + "…")}`);
274
- const useIt = await prompt(rl, ` Use this key? [Y/n]: `);
275
- if (useIt.trim().toLowerCase() === "n") {
276
- console.log();
277
- if (provider.docsUrl) console.log(` ${gray("Get a key at:")} ${cyan(provider.docsUrl)}`);
278
- const keyInput = await prompt(rl, ` Paste new API key: `);
279
- if (!keyInput.trim()) { console.log(red(" No key provided. Exiting.")); return; }
280
- config[providerId] = { apiKey: keyInput.trim(), model: config[providerId]?.model || provider.default };
281
- } else {
282
- // Use existing key — just confirm/update model
283
- config[providerId] = { apiKey: existing, model: config[providerId]?.model || provider.default };
284
- }
285
- } else {
286
- // No key found — ask for it
287
- console.log(` ${gray("Get your API key at:")} ${cyan(provider.docsUrl)}`);
288
- console.log(` ${gray("Tip: paste the key below — it starts with")} ${gray(provider.keyHint)}`);
289
- console.log();
290
- const keyInput = await prompt(rl, ` Paste API key: `);
291
- if (!keyInput.trim()) { console.log(red(" No key provided. Exiting.")); return; }
292
- config[providerId] = { apiKey: keyInput.trim(), model: provider.default };
293
- }
294
-
295
- // Model selection (just press Enter to keep default)
296
- const currentModel = config[providerId].model;
297
- console.log();
298
- console.log(` ${gray("Available models:")} ${provider.models.join(" ")}`);
299
- const modelInput = await prompt(rl, ` Model [${currentModel}]: `);
300
- config[providerId].model = modelInput.trim() || currentModel;
301
-
302
- saveConfig(cwd, config);
303
-
304
- console.log();
305
- process.stdout.write(` ${green("✓")} Saved. Testing connection… `);
306
-
307
- const result = await testProvider(providerId, config, cwd);
308
- if (result?.text) {
309
- console.log(green("OK") + gray(` (${config[providerId].model})`));
310
- } else {
311
- console.log(yellow("no response"));
312
- console.log(` ${yellow("⚠")} Connection failed — double-check your API key.`);
313
- }
314
- }
315
-
316
- console.log();
317
- console.log(` ${green("✓")} ${bold(provider.name)} is ready.`);
318
- console.log(` ${gray("AI-powered commands:")} explain why review changelog`);
319
- console.log();
320
-
321
- // gitignore reminder
322
- const gitignorePath = path.join(cwd, ".gitignore");
323
- if (fs.existsSync(gitignorePath)) {
324
- const content = fs.readFileSync(gitignorePath, "utf8");
325
- if (!content.includes("integrations.json")) {
326
- console.log(` ${yellow("⚠")} Add ${cyan("inferno/integrations.json")} to your .gitignore to avoid committing your API key.`);
327
- console.log();
328
- }
329
- }
330
-
331
- } finally {
332
- rl.close();
333
- }
334
- }
335
-
336
- async function cmdTest(args, cwd) {
337
- const config = loadConfig(cwd);
338
- const providerId = args.find(a => !a.startsWith("--")) || null;
339
-
340
- console.log();
341
- console.log(` ${bold("infernoflow ai test")}`);
342
- console.log();
343
-
344
- const toTest = providerId
345
- ? PROVIDERS.filter(p => p.id === providerId)
346
- : PROVIDERS;
347
-
348
- for (const p of toTest) {
349
- const status = await checkProviderStatus(p.id, config);
350
- if (!status.configured) {
351
- console.log(` ${gray("○")} ${bold(p.name.padEnd(22))} ${gray("not configured — skipping")}`);
352
- continue;
353
- }
354
-
355
- process.stdout.write(` ${yellow("…")} ${bold(p.name.padEnd(22))} testing… `);
356
- const result = await testProvider(p.id, config, cwd);
357
- if (result?.text) {
358
- console.log(green("OK") + gray(` (${result.model || p.id})`));
359
- console.log(` ${gray(result.text.trim().slice(0, 80))}`);
360
- } else {
361
- console.log(red("FAIL"));
362
- console.log(` ${red("No response — check API key or model name")}`);
363
- }
364
- }
365
-
366
- console.log();
367
- }
368
-
369
- async function cmdClear(args, cwd) {
370
- const config = loadConfig(cwd);
371
- const providerId = args.find(a => !a.startsWith("--"));
372
-
373
- if (!providerId) {
374
- console.error(red("✗ Usage: infernoflow ai clear <provider>"));
375
- console.error(gray(" Example: infernoflow ai clear openai"));
376
- process.exit(1);
377
- }
378
-
379
- if (!config[providerId]) {
380
- console.log(gray(` No config found for "${providerId}"`));
381
- return;
382
- }
383
-
384
- delete config[providerId];
385
- saveConfig(cwd, config);
386
- console.log(green(` ✓ Cleared config for ${providerId}`));
387
- }
388
-
389
- // ── entry point ───────────────────────────────────────────────────────────────
390
-
391
- export async function aiCommand(rawArgs) {
392
- const args = (rawArgs || []).slice(1);
393
- const sub = args.find(a => !a.startsWith("--")) || "status";
394
- const subArgs = args.filter(a => a !== sub);
395
- const cwd = process.cwd();
396
-
397
- switch (sub) {
398
- case "setup": return cmdSetup(cwd);
399
- case "status": return cmdStatus(cwd);
400
- case "test": return cmdTest(subArgs, cwd);
401
- case "clear": return cmdClear(subArgs, cwd);
402
- default:
403
- console.error(red(`✗ Unknown subcommand: "${sub}"`));
404
- console.error(gray(" Usage: infernoflow ai <setup|status|test|clear>"));
405
- process.exit(1);
406
- }
407
- }
1
+ import*as g from"node:fs";import*as k from"node:path";import*as x from"node:https";import*as C from"node:http";import*as R from"node:readline";import{bold as d,cyan as v,gray as i,green as r,yellow as y,red as h}from"../ui/output.mjs";function O(n){return k.join(n,"inferno")}function A(n){const o=k.join(O(n),"integrations.json");if(!g.existsSync(o))return{};try{return JSON.parse(g.readFileSync(o,"utf8"))}catch{return{}}}function E(n,o){const t=O(n);g.existsSync(t)||g.mkdirSync(t,{recursive:!0}),g.writeFileSync(k.join(t,"integrations.json"),JSON.stringify(o,null,2)+`
2
+ `)}const m=[{id:"anthropic",name:"Anthropic (Claude)",envKey:"ANTHROPIC_API_KEY",models:["claude-sonnet-4-6","claude-opus-4-6","claude-haiku-4-5-20251001"],default:"claude-sonnet-4-6",keyHint:"sk-ant-api03-\u2026",docsUrl:"https://console.anthropic.com/settings/keys"},{id:"openai",name:"OpenAI (GPT)",envKey:"OPENAI_API_KEY",models:["gpt-4o","gpt-4o-mini","gpt-4-turbo"],default:"gpt-4o",keyHint:"sk-\u2026",docsUrl:"https://platform.openai.com/api-keys"},{id:"gemini",name:"Google Gemini",envKey:"GOOGLE_AI_API_KEY",models:["gemini-2.0-flash","gemini-1.5-pro","gemini-1.5-flash"],default:"gemini-2.0-flash",keyHint:"AIza\u2026",docsUrl:"https://aistudio.google.com/app/apikey"},{id:"openrouter",name:"OpenRouter",envKey:"OPENROUTER_API_KEY",models:["anthropic/claude-sonnet-4-6","openai/gpt-4o","meta-llama/llama-3.1-8b-instruct:free"],default:"anthropic/claude-sonnet-4-6",keyHint:"sk-or-\u2026",docsUrl:"https://openrouter.ai/keys"},{id:"ollama",name:"Ollama (local)",envKey:null,models:["llama3.2","mistral","codellama","phi3"],default:"llama3.2",keyHint:null,docsUrl:"https://ollama.com"}];function _(n){return new Promise(o=>{const t=new URL(n),e=(t.protocol==="https:"?x:C).request({hostname:t.hostname,port:t.port||(t.protocol==="https:"?443:80),path:t.pathname+(t.search||""),method:"GET",timeout:5e3},s=>{let a="";s.on("data",c=>a+=c),s.on("end",()=>{try{o({status:s.statusCode,body:JSON.parse(a)})}catch{o({status:s.statusCode,body:a})}})});e.on("error",()=>o(null)),e.on("timeout",()=>{e.destroy(),o(null)}),e.end()})}async function b(n,o){const l={anthropic:process.env.ANTHROPIC_API_KEY,openai:process.env.OPENAI_API_KEY,gemini:process.env.GOOGLE_AI_API_KEY||process.env.GEMINI_API_KEY,openrouter:process.env.OPENROUTER_API_KEY}[n],e=o[n]?.apiKey,s=l||e,a=l?"env":e?"integrations.json":null,c=o[n]?.model||m.find(u=>u.id===n)?.default;if(n==="ollama"){const u=await _("http://localhost:11434/api/tags").catch(()=>null);if(u?.status===200){const p=u.body?.models?.map(f=>f.name)||[];return{configured:!0,source:"local",model:o.ollama?.model||"llama3.2",available:!0,models:p}}return{configured:!1,source:null,model:null,available:!1}}return{configured:!!s,source:a,model:c,available:null,masked:s?s.slice(0,8)+"\u2026":null}}async function N(n,o,t){try{const{callAI:l}=await import("../ai/providerRouter.mjs"),e=`Reply with exactly: "infernoflow AI test OK \u2014 ${n}"`;return await l(e,t,n)}catch{return null}}function $(n,o){return new Promise(t=>n.question(o,t))}async function U(n){const o=A(n);console.log(),console.log(` ${d("infernoflow ai")} ${i("\u2014 provider status")}`),console.log();let t=!1;for(const l of m){const e=await b(l.id,o);e.configured&&(t=!0);const s=e.configured?r("\u2713"):i("\u25CB"),a=d(l.name.padEnd(22)),c=e.configured?`${r("configured")} ${i(e.source)} ${i("model: "+e.model)}${e.masked?" "+i(e.masked):""}`:i("not configured");console.log(` ${s} ${a} ${c}`)}console.log(),t?console.log(` ${i("Run")} ${v("infernoflow ai test")} ${i("to verify the active provider.")}`):(console.log(` ${y("No AI providers configured.")} Run: ${v("infernoflow ai setup")}`),console.log(` ${i("Without a provider, explain/why/review use structural fallbacks.")}`)),console.log()}async function G(n){const o=A(n),t={anthropic:process.env.ANTHROPIC_API_KEY,openai:process.env.OPENAI_API_KEY,gemini:process.env.GOOGLE_AI_API_KEY||process.env.GEMINI_API_KEY,openrouter:process.env.OPENROUTER_API_KEY};console.log(),console.log(` ${d("\u{1F525} infernoflow ai setup")}`),console.log(` ${i("Connect an AI provider for explain, why, review, and changelog.")}`),console.log(),m.forEach((e,s)=>{const a=t[e.id],c=o[e.id]?.apiKey,u=a?r(" \u2713 key detected in environment"):c?r(" \u2713 key already saved"):"",p=d(String(s+1)),f=e.id==="ollama"?i(" (local, no key needed)"):"";console.log(` ${p}) ${d(e.name.padEnd(22))}${f}${u}`)}),console.log();const l=R.createInterface({input:process.stdin,output:process.stdout});try{const e=await $(l," Select provider [1]: "),s=(parseInt(e.trim())||1)-1;if(s<0||s>=m.length){console.log(h(` Invalid choice. Enter a number 1\u2013${m.length}.`));return}const a=m[s],c=a.id;if(console.log(),console.log(` ${d(a.name)}`),c==="ollama"){const p=await $(l," Ollama host [http://localhost:11434]: "),f=await $(l,` Model [${a.default}]: `);o.ollama={host:p.trim()||"http://localhost:11434",model:f.trim()||a.default},E(n,o),console.log(),process.stdout.write(` ${r("\u2713")} Saved. Testing connection\u2026 `),(await _(`${o.ollama.host}/api/tags`).catch(()=>null))?.status===200?console.log(r("OK")):(console.log(y("not reachable")),console.log(` ${y("\u26A0")} Start Ollama first: ${v("ollama serve")}`))}else{const p=t[c],f=o[c]?.apiKey,I=p||f;if(I){const w=p?"environment variable":"saved config";if(console.log(` ${r("\u2713")} API key detected from ${w}: ${i(I.slice(0,12)+"\u2026")}`),(await $(l," Use this key? [Y/n]: ")).trim().toLowerCase()==="n"){console.log(),a.docsUrl&&console.log(` ${i("Get a key at:")} ${v(a.docsUrl)}`);const K=await $(l," Paste new API key: ");if(!K.trim()){console.log(h(" No key provided. Exiting."));return}o[c]={apiKey:K.trim(),model:o[c]?.model||a.default}}else o[c]={apiKey:I,model:o[c]?.model||a.default}}else{console.log(` ${i("Get your API key at:")} ${v(a.docsUrl)}`),console.log(` ${i("Tip: paste the key below \u2014 it starts with")} ${i(a.keyHint)}`),console.log();const w=await $(l," Paste API key: ");if(!w.trim()){console.log(h(" No key provided. Exiting."));return}o[c]={apiKey:w.trim(),model:a.default}}const P=o[c].model;console.log(),console.log(` ${i("Available models:")} ${a.models.join(" ")}`);const S=await $(l,` Model [${P}]: `);o[c].model=S.trim()||P,E(n,o),console.log(),process.stdout.write(` ${r("\u2713")} Saved. Testing connection\u2026 `),(await N(c,o,n))?.text?console.log(r("OK")+i(` (${o[c].model})`)):(console.log(y("no response")),console.log(` ${y("\u26A0")} Connection failed \u2014 double-check your API key.`))}console.log(),console.log(` ${r("\u2713")} ${d(a.name)} is ready.`),console.log(` ${i("AI-powered commands:")} explain why review changelog`),console.log();const u=k.join(n,".gitignore");g.existsSync(u)&&(g.readFileSync(u,"utf8").includes("integrations.json")||(console.log(` ${y("\u26A0")} Add ${v("inferno/integrations.json")} to your .gitignore to avoid committing your API key.`),console.log()))}finally{l.close()}}async function Y(n,o){const t=A(o),l=n.find(s=>!s.startsWith("--"))||null;console.log(),console.log(` ${d("infernoflow ai test")}`),console.log();const e=l?m.filter(s=>s.id===l):m;for(const s of e){if(!(await b(s.id,t)).configured){console.log(` ${i("\u25CB")} ${d(s.name.padEnd(22))} ${i("not configured \u2014 skipping")}`);continue}process.stdout.write(` ${y("\u2026")} ${d(s.name.padEnd(22))} testing\u2026 `);const c=await N(s.id,t,o);c?.text?(console.log(r("OK")+i(` (${c.model||s.id})`)),console.log(` ${i(c.text.trim().slice(0,80))}`)):(console.log(h("FAIL")),console.log(` ${h("No response \u2014 check API key or model name")}`))}console.log()}async function T(n,o){const t=A(o),l=n.find(e=>!e.startsWith("--"));if(l||(console.error(h("\u2717 Usage: infernoflow ai clear <provider>")),console.error(i(" Example: infernoflow ai clear openai")),process.exit(1)),!t[l]){console.log(i(` No config found for "${l}"`));return}delete t[l],E(o,t),console.log(r(` \u2713 Cleared config for ${l}`))}async function L(n){const o=(n||[]).slice(1),t=o.find(s=>!s.startsWith("--"))||"status",l=o.filter(s=>s!==t),e=process.cwd();switch(t){case"setup":return G(e);case"status":return U(e);case"test":return Y(l,e);case"clear":return T(l,e);default:console.error(h(`\u2717 Unknown subcommand: "${t}"`)),console.error(i(" Usage: infernoflow ai <setup|status|test|clear>")),process.exit(1)}}export{L as aiCommand};