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.
- package/dist/bin/infernoflow.mjs +46 -0
- package/dist/lib/ai/providerRouter.mjs +227 -64
- package/dist/lib/commands/adoptWizard.mjs +320 -0
- package/dist/lib/commands/coverage.mjs +282 -0
- package/dist/lib/commands/doctor.mjs +284 -0
- package/dist/lib/commands/init.mjs +70 -0
- package/dist/lib/commands/review.mjs +238 -0
- package/dist/lib/commands/vibe.mjs +365 -0
- package/dist/lib/templates/index.mjs +131 -0
- package/package.json +3 -2
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|