infernoflow 0.20.0 → 0.22.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.
@@ -49,6 +49,11 @@ const COMMAND_DESCRIPTIONS = {
49
49
  export: "Export contract to OpenAPI, Backstage catalog-info.yaml, CSV, or Markdown",
50
50
  snapshot: "Save/diff/restore named snapshots of the capability contract",
51
51
  health: "Compute a 0–100 health score across coverage, docs, freshness, completeness, drift",
52
+ vibe: "Vibe coding mode — watches files, auto-syncs contract, regenerates context on every save",
53
+ adopt: "Interactive wizard to adopt infernoflow in an existing project (detect → review → wire up)",
54
+ doctor: "Diagnose your infernoflow setup — checks Node, git, contract, AI providers, MCP, hooks",
55
+ coverage: "Map test files to capabilities — show which caps have test coverage and which don't",
56
+ review: "AI-powered capability impact review for staged or recent git changes",
52
57
  };
53
58
 
54
59
  const COMMAND_HANDLERS = {
@@ -91,6 +96,11 @@ const COMMAND_HANDLERS = {
91
96
  export: async (args) => (await import("../lib/commands/export.mjs")).exportCommand(args),
92
97
  snapshot: async (args) => (await import("../lib/commands/snapshot.mjs")).snapshotCommand(args),
93
98
  health: async (args) => (await import("../lib/commands/health.mjs")).healthCommand(args),
99
+ vibe: async (args) => (await import("../lib/commands/vibe.mjs")).vibeCommand(args),
100
+ adopt: async (args) => (await import("../lib/commands/adoptWizard.mjs")).adoptWizardCommand(args),
101
+ doctor: async (args) => (await import("../lib/commands/doctor.mjs")).doctorCommand(args),
102
+ coverage: async (args) => (await import("../lib/commands/coverage.mjs")).coverageCommand(args),
103
+ review: async (args) => (await import("../lib/commands/review.mjs")).reviewCommand(args),
94
104
  };
95
105
 
96
106
  function formatCommandsHelp() {
@@ -287,6 +297,26 @@ ${formatCommandsHelp()}
287
297
  --fail-on high|medium Exit 1 if unreviewed caps at given severity exist
288
298
  --json Machine-readable output
289
299
 
300
+ ${bold("vibe options:")}
301
+ --dir <dirs> Comma-separated directories to watch (default: auto-detected)
302
+ --no-suggest Disable automatic contract sync on file save
303
+ --no-context Disable CONTEXT.md regeneration
304
+ --interval <secs> Debounce seconds between saves (default: 4)
305
+ --port <n> Also run a mini status dashboard on localhost:<n>
306
+ --silent Suppress all terminal output (pure background mode)
307
+
308
+ ${bold("adopt options:")}
309
+ --dir <dirs> Source directories to scan (default: src,lib,app,api,routes,controllers)
310
+ --yes, -y Auto-approve all candidates (non-interactive)
311
+ --json Machine-readable output, implies --yes
312
+
313
+ ${bold("init --template options:")}
314
+ --template rest-api REST API (Express/Fastify/Hono) starter
315
+ --template nextjs Next.js fullstack app starter
316
+ --template cli CLI tool (Node.js/Python) starter
317
+ --template graphql GraphQL API (Apollo/Pothos) starter
318
+ --template monorepo Monorepo workspace starter
319
+
290
320
  ${bold("scout options:")}
291
321
  --dir <dirs> Comma-separated directories to scan (default: src,lib,app,api,routes)
292
322
  --apply Write discovered capabilities to the contract file
@@ -315,6 +345,22 @@ ${formatCommandsHelp()}
315
345
  --interval <secs> Watch interval in seconds (default: 30)
316
346
  --json Machine-readable score + breakdown
317
347
 
348
+ ${bold("doctor options:")}
349
+ --fix Auto-fix common issues (installs hooks, runs init, etc.)
350
+ --json Machine-readable list of pass/warn/fail results
351
+
352
+ ${bold("coverage options:")}
353
+ --dir <path> Extra directory to scan for test files (repeatable)
354
+ --threshold <0-1> Minimum fuzzy-match score to count a test (default: 0.25)
355
+ --fail-below <pct> Exit 1 if coverage percentage is below this value (CI gate)
356
+ --json Machine-readable coverage breakdown
357
+
358
+ ${bold("review options:")}
359
+ --unstaged Review all working-tree changes (not just staged)
360
+ --last Review last commit (git diff HEAD~1)
361
+ --dry-run Print the AI prompt only — no API call made
362
+ --json Machine-readable output (affectedCaps, summary, provider)
363
+
318
364
  ${bold("Machine output:")}
319
365
  ${gray("status --json")}
320
366
  ${gray("check --json")}
@@ -1,73 +1,236 @@
1
- import { detectIdeContext } from "./ideDetection.mjs";
2
-
3
- export async function resolveProvider(requestedProvider = "auto", preferredIde = "auto") {
4
- const providerRequested = String(requestedProvider || "auto").toLowerCase();
5
- const ide = detectIdeContext(preferredIde);
6
- const reasonCodes = [...ide.reasonCodes];
7
-
8
- if (providerRequested === "local") {
9
- reasonCodes.push("LOCAL_PROVIDER_SELECTED");
10
- return {
11
- providerRequested,
12
- providerResolved: "local",
13
- ideDetected: ide.ideDetected,
14
- agentAvailable: ide.agentAvailable,
15
- reasonCodes,
16
- };
17
- }
1
+ /**
2
+ * infernoflow AI provider router
3
+ *
4
+ * Tries providers in order until one works:
5
+ * Tier 1 — VS Code Language Model API (vscode.lm — any Copilot model: Gemini, Claude, GPT)
6
+ * Tier 2 — Direct API: Anthropic, OpenAI, Google AI (Gemini), OpenRouter
7
+ * Tier 3 — Ollama (local, free, offline)
8
+ * Tier 4 — Prompt fallback (print prompt, no AI call)
9
+ *
10
+ * Config sources (in priority order):
11
+ * 1. Environment variables
12
+ * 2. inferno/integrations.json
13
+ * 3. Auto-detection (Ollama running locally, etc.)
14
+ */
18
15
 
19
- if (providerRequested === "prompt") {
20
- reasonCodes.push("PROMPT_PROVIDER_SELECTED");
21
- return {
22
- providerRequested,
23
- providerResolved: "prompt",
24
- ideDetected: ide.ideDetected,
25
- agentAvailable: ide.agentAvailable,
26
- reasonCodes,
27
- };
28
- }
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import * as https from "node:https";
19
+ import * as http from "node:http";
20
+
21
+ // ── Config reader ─────────────────────────────────────────────────────────────
22
+
23
+ export function readAiConfig(cwd) {
24
+ const p = path.join(cwd, "inferno", "integrations.json");
25
+ if (!fs.existsSync(p)) return {};
26
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
27
+ }
28
+
29
+ // ── HTTP helpers ──────────────────────────────────────────────────────────────
30
+
31
+ function post(url, headers, body) {
32
+ return new Promise((resolve, reject) => {
33
+ const parsed = new URL(url);
34
+ const lib = parsed.protocol === "https:" ? https : http;
35
+ const data = JSON.stringify(body);
36
+
37
+ const req = lib.request({
38
+ hostname: parsed.hostname,
39
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
40
+ path: parsed.pathname + (parsed.search || ""),
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data), ...headers },
43
+ }, (res) => {
44
+ let raw = "";
45
+ res.on("data", d => (raw += d));
46
+ res.on("end", () => {
47
+ try { resolve({ status: res.statusCode, body: JSON.parse(raw) }); }
48
+ catch { resolve({ status: res.statusCode, body: raw }); }
49
+ });
50
+ });
51
+ req.on("error", reject);
52
+ req.write(data);
53
+ req.end();
54
+ });
55
+ }
56
+
57
+ // ── Tier 2: Direct API providers ─────────────────────────────────────────────
58
+
59
+ async function callAnthropic(prompt, config) {
60
+ const apiKey = process.env.ANTHROPIC_API_KEY || config.anthropic?.apiKey;
61
+ if (!apiKey) return null;
29
62
 
30
- if (providerRequested === "agent") {
31
- if (!ide.agentAvailable) {
32
- reasonCodes.push("EXPLICIT_AGENT_REQUIRED");
33
- return {
34
- providerRequested,
35
- providerResolved: "none",
36
- ideDetected: ide.ideDetected,
37
- agentAvailable: ide.agentAvailable,
38
- reasonCodes,
39
- error: "agent_unavailable",
40
- };
63
+ const model = config.anthropic?.model || process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6";
64
+
65
+ try {
66
+ const res = await post(
67
+ "https://api.anthropic.com/v1/messages",
68
+ { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
69
+ {
70
+ model,
71
+ max_tokens: 1024,
72
+ messages: [{ role: "user", content: prompt }],
73
+ }
74
+ );
75
+ if (res.status === 200 && res.body?.content?.[0]?.text) {
76
+ return { text: res.body.content[0].text, provider: "anthropic", model };
41
77
  }
42
- reasonCodes.push("IDE_AGENT_SELECTED");
43
- return {
44
- providerRequested,
45
- providerResolved: "agent",
46
- ideDetected: ide.ideDetected,
47
- agentAvailable: ide.agentAvailable,
48
- reasonCodes,
49
- };
50
- }
78
+ } catch {}
79
+ return null;
80
+ }
81
+
82
+ async function callOpenAI(prompt, config) {
83
+ const apiKey = process.env.OPENAI_API_KEY || config.openai?.apiKey;
84
+ const endpoint = process.env.OPENAI_ENDPOINT || config.openai?.endpoint || "https://api.openai.com/v1/chat/completions";
85
+ if (!apiKey) return null;
86
+
87
+ const model = config.openai?.model || process.env.OPENAI_MODEL || "gpt-4o";
88
+
89
+ try {
90
+ const res = await post(
91
+ endpoint,
92
+ { "Authorization": `Bearer ${apiKey}` },
93
+ {
94
+ model,
95
+ max_tokens: 1024,
96
+ messages: [{ role: "user", content: prompt }],
97
+ }
98
+ );
99
+ if (res.status === 200 && res.body?.choices?.[0]?.message?.content) {
100
+ return { text: res.body.choices[0].message.content, provider: "openai", model };
101
+ }
102
+ } catch {}
103
+ return null;
104
+ }
105
+
106
+ async function callGemini(prompt, config) {
107
+ const apiKey = process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY || config.gemini?.apiKey;
108
+ if (!apiKey) return null;
109
+
110
+ const model = config.gemini?.model || process.env.GEMINI_MODEL || "gemini-2.0-flash";
51
111
 
52
- // auto
53
- if (ide.agentAvailable) {
54
- reasonCodes.push("IDE_AGENT_SELECTED");
55
- return {
56
- providerRequested: "auto",
57
- providerResolved: "agent",
58
- ideDetected: ide.ideDetected,
59
- agentAvailable: ide.agentAvailable,
60
- reasonCodes,
61
- };
112
+ try {
113
+ const res = await post(
114
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
115
+ {},
116
+ { contents: [{ parts: [{ text: prompt }] }] }
117
+ );
118
+ const text = res.body?.candidates?.[0]?.content?.parts?.[0]?.text;
119
+ if (res.status === 200 && text) {
120
+ return { text, provider: "gemini", model };
121
+ }
122
+ } catch {}
123
+ return null;
124
+ }
125
+
126
+ async function callOpenRouter(prompt, config) {
127
+ const apiKey = process.env.OPENROUTER_API_KEY || config.openrouter?.apiKey;
128
+ if (!apiKey) return null;
129
+
130
+ const model = config.openrouter?.model || process.env.OPENROUTER_MODEL || "anthropic/claude-sonnet-4-6";
131
+
132
+ try {
133
+ const res = await post(
134
+ "https://openrouter.ai/api/v1/chat/completions",
135
+ { "Authorization": `Bearer ${apiKey}`, "HTTP-Referer": "https://infernoflow.dev" },
136
+ {
137
+ model,
138
+ messages: [{ role: "user", content: prompt }],
139
+ max_tokens: 1024,
140
+ }
141
+ );
142
+ if (res.status === 200 && res.body?.choices?.[0]?.message?.content) {
143
+ return { text: res.body.choices[0].message.content, provider: "openrouter", model };
144
+ }
145
+ } catch {}
146
+ return null;
147
+ }
148
+
149
+ // ── Tier 3: Ollama (local) ────────────────────────────────────────────────────
150
+
151
+ async function callOllama(prompt, config) {
152
+ const host = process.env.OLLAMA_HOST || config.ollama?.host || "http://localhost:11434";
153
+ const model = process.env.OLLAMA_MODEL || config.ollama?.model || "llama3";
154
+
155
+ // Quick liveness check
156
+ try {
157
+ await new Promise((res, rej) => {
158
+ const u = new URL(host);
159
+ http.get({ hostname: u.hostname, port: u.port || 11434, path: "/api/tags", timeout: 1500 },
160
+ r => res(r)).on("error", rej);
161
+ });
162
+ } catch { return null; }
163
+
164
+ try {
165
+ const res = await post(
166
+ `${host}/api/generate`,
167
+ {},
168
+ { model, prompt, stream: false }
169
+ );
170
+ if (res.status === 200 && res.body?.response) {
171
+ return { text: res.body.response, provider: "ollama", model };
172
+ }
173
+ } catch {}
174
+ return null;
175
+ }
176
+
177
+ // ── Main router ───────────────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Call the best available AI provider with a prompt.
181
+ *
182
+ * @param {string} prompt - The full prompt text
183
+ * @param {object} opts
184
+ * opts.cwd - Project root (for reading integrations.json)
185
+ * opts.provider - Force a specific provider: anthropic|openai|gemini|openrouter|ollama|prompt
186
+ * opts.silent - Don't print "using provider X" message
187
+ * @returns {{ text: string, provider: string, model: string } | null}
188
+ * null means no provider was available → caller should use prompt fallback
189
+ */
190
+ export async function callAI(prompt, opts = {}) {
191
+ const cwd = opts.cwd || process.cwd();
192
+ const config = readAiConfig(cwd);
193
+ const forced = (opts.provider || "auto").toLowerCase();
194
+ const silent = opts.silent ?? true;
195
+
196
+ const providers = [
197
+ // Tier 2 — direct API (Tier 1 vscode.lm is handled in the VS Code extension)
198
+ ["anthropic", () => callAnthropic(prompt, config)],
199
+ ["openai", () => callOpenAI(prompt, config)],
200
+ ["gemini", () => callGemini(prompt, config)],
201
+ ["openrouter", () => callOpenRouter(prompt, config)],
202
+ // Tier 3 — local
203
+ ["ollama", () => callOllama(prompt, config)],
204
+ ];
205
+
206
+ // If a specific provider is forced, only try that one
207
+ const toTry = forced === "auto" || forced === "prompt"
208
+ ? providers
209
+ : providers.filter(([name]) => name === forced);
210
+
211
+ for (const [name, fn] of toTry) {
212
+ try {
213
+ const result = await fn();
214
+ if (result) {
215
+ if (!silent) process.stderr.write(` [infernoflow ai] using ${name}:${result.model}\n`);
216
+ return result;
217
+ }
218
+ } catch {}
62
219
  }
63
220
 
64
- reasonCodes.push("FALLBACK_PROMPT_MODE");
221
+ return null; // No provider → fallback to prompt output
222
+ }
223
+
224
+ /**
225
+ * Detect which providers are configured (for doctor command).
226
+ */
227
+ export function detectAvailableProviders(cwd) {
228
+ const config = readAiConfig(cwd);
65
229
  return {
66
- providerRequested: "auto",
67
- providerResolved: "prompt",
68
- ideDetected: ide.ideDetected,
69
- agentAvailable: ide.agentAvailable,
70
- reasonCodes,
230
+ anthropic: !!(process.env.ANTHROPIC_API_KEY || config.anthropic?.apiKey),
231
+ openai: !!(process.env.OPENAI_API_KEY || config.openai?.apiKey),
232
+ gemini: !!(process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY || config.gemini?.apiKey),
233
+ openrouter: !!(process.env.OPENROUTER_API_KEY || config.openrouter?.apiKey),
234
+ ollama: false, // checked async — doctor runs its own check
71
235
  };
72
236
  }
73
-