infernoflow 0.32.7 → 0.32.9

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 (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -320
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -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};