infernoflow 0.32.2 → 0.32.4
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/lib/ai/providerRouter.mjs +59 -0
- package/dist/lib/commands/ai.mjs +72 -35
- package/package.json +1 -1
|
@@ -234,3 +234,62 @@ export function detectAvailableProviders(cwd) {
|
|
|
234
234
|
ollama: false, // checked async — doctor runs its own check
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve which provider + IDE is available for the `run` command.
|
|
240
|
+
* Returns a structured object that run.mjs uses to decide how to proceed.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} providerRequested - "auto"|"anthropic"|"openai"|etc.
|
|
243
|
+
* @param {string} ideRequested - "auto"|"vscode"|"cursor"|etc.
|
|
244
|
+
* @returns {{ providerResolved: string, ideDetected: string, agentAvailable: boolean, reasonCodes: string[], error?: string }}
|
|
245
|
+
*/
|
|
246
|
+
export async function resolveProvider(providerRequested = "auto", ideRequested = "auto") {
|
|
247
|
+
const cwd = process.cwd();
|
|
248
|
+
const config = readAiConfig(cwd);
|
|
249
|
+
const reasons = [];
|
|
250
|
+
|
|
251
|
+
// Detect IDE
|
|
252
|
+
const inVsCode = !!process.env.VSCODE_PID || !!process.env.TERM_PROGRAM?.includes("vscode");
|
|
253
|
+
const inCursor = !!process.env.CURSOR_TRACE_ID || !!process.env.CURSOR_CHANNEL;
|
|
254
|
+
const ideDetected = inCursor ? "cursor" : inVsCode ? "vscode" : "terminal";
|
|
255
|
+
|
|
256
|
+
// Detect available providers
|
|
257
|
+
const available = {
|
|
258
|
+
anthropic: !!(process.env.ANTHROPIC_API_KEY || config.anthropic?.apiKey),
|
|
259
|
+
openai: !!(process.env.OPENAI_API_KEY || config.openai?.apiKey),
|
|
260
|
+
gemini: !!(process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY || config.gemini?.apiKey),
|
|
261
|
+
openrouter: !!(process.env.OPENROUTER_API_KEY || config.openrouter?.apiKey),
|
|
262
|
+
ollama: false,
|
|
263
|
+
vscode: inVsCode || inCursor,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Check Ollama quickly (sync port probe via env hint)
|
|
267
|
+
if (process.env.OLLAMA_HOST || config.ollama?.host) {
|
|
268
|
+
available.ollama = true;
|
|
269
|
+
reasons.push("ollama_env");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Resolve which provider to use
|
|
273
|
+
let providerResolved = "none";
|
|
274
|
+
const forced = (providerRequested || "auto").toLowerCase();
|
|
275
|
+
|
|
276
|
+
if (forced !== "auto" && forced !== "prompt" && available[forced]) {
|
|
277
|
+
providerResolved = forced;
|
|
278
|
+
reasons.push(`forced_${forced}`);
|
|
279
|
+
} else {
|
|
280
|
+
// Priority order: vscode/cursor IDE → anthropic → openai → gemini → openrouter → ollama
|
|
281
|
+
const priority = ["vscode", "anthropic", "openai", "gemini", "openrouter", "ollama"];
|
|
282
|
+
for (const p of priority) {
|
|
283
|
+
if (available[p]) { providerResolved = p; reasons.push(`auto_${p}`); break; }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const agentAvailable = providerResolved !== "none";
|
|
288
|
+
|
|
289
|
+
if (!agentAvailable) {
|
|
290
|
+
reasons.push("no_provider");
|
|
291
|
+
return { providerResolved: "none", ideDetected, agentAvailable: false, reasonCodes: reasons, error: "agent_unavailable" };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { providerResolved, ideDetected, agentAvailable: true, reasonCodes: reasons };
|
|
295
|
+
}
|
package/dist/lib/commands/ai.mjs
CHANGED
|
@@ -196,35 +196,52 @@ async function cmdStatus(cwd) {
|
|
|
196
196
|
async function cmdSetup(cwd) {
|
|
197
197
|
const config = loadConfig(cwd);
|
|
198
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
|
+
|
|
199
207
|
console.log();
|
|
200
208
|
console.log(` ${bold("🔥 infernoflow ai setup")}`);
|
|
201
209
|
console.log(` ${gray("Connect an AI provider for explain, why, review, and changelog.")}`);
|
|
202
210
|
console.log();
|
|
203
|
-
|
|
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
|
+
|
|
204
223
|
console.log();
|
|
205
224
|
|
|
206
225
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
207
226
|
|
|
208
227
|
try {
|
|
209
|
-
//
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const provider = PROVIDERS.find(p => p.id === providerId);
|
|
228
|
+
// Numbered selection
|
|
229
|
+
const choice = await prompt(rl, ` Select provider [1]: `);
|
|
230
|
+
const idx = (parseInt(choice.trim()) || 1) - 1;
|
|
213
231
|
|
|
214
|
-
if (
|
|
215
|
-
console.log(red(`
|
|
232
|
+
if (idx < 0 || idx >= PROVIDERS.length) {
|
|
233
|
+
console.log(red(` Invalid choice. Enter a number 1–${PROVIDERS.length}.`));
|
|
216
234
|
return;
|
|
217
235
|
}
|
|
218
236
|
|
|
237
|
+
const provider = PROVIDERS[idx];
|
|
238
|
+
const providerId = provider.id;
|
|
239
|
+
|
|
219
240
|
console.log();
|
|
220
241
|
console.log(` ${bold(provider.name)}`);
|
|
221
242
|
|
|
222
|
-
if (provider.docsUrl) {
|
|
223
|
-
console.log(` ${gray("Get API key:")} ${cyan(provider.docsUrl)}`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
243
|
if (providerId === "ollama") {
|
|
227
|
-
// Ollama — no key
|
|
244
|
+
// Ollama — no key needed
|
|
228
245
|
const hostInput = await prompt(rl, ` Ollama host [http://localhost:11434]: `);
|
|
229
246
|
const modelInput = await prompt(rl, ` Model [${provider.default}]: `);
|
|
230
247
|
|
|
@@ -235,49 +252,70 @@ async function cmdSetup(cwd) {
|
|
|
235
252
|
saveConfig(cwd, config);
|
|
236
253
|
|
|
237
254
|
console.log();
|
|
238
|
-
|
|
255
|
+
process.stdout.write(` ${green("✓")} Saved. Testing connection… `);
|
|
239
256
|
const probe = await httpGet(`${config.ollama.host}/api/tags`).catch(() => null);
|
|
240
257
|
if (probe?.status === 200) {
|
|
241
|
-
console.log(
|
|
258
|
+
console.log(green("OK"));
|
|
242
259
|
} else {
|
|
243
|
-
console.log(
|
|
260
|
+
console.log(yellow("not reachable"));
|
|
261
|
+
console.log(` ${yellow("⚠")} Start Ollama first: ${cyan("ollama serve")}`);
|
|
244
262
|
}
|
|
263
|
+
|
|
245
264
|
} else {
|
|
246
265
|
// API key provider
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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 };
|
|
255
293
|
}
|
|
256
294
|
|
|
257
|
-
|
|
258
|
-
const
|
|
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;
|
|
259
301
|
|
|
260
|
-
config[providerId] = { apiKey, model };
|
|
261
302
|
saveConfig(cwd, config);
|
|
262
303
|
|
|
263
304
|
console.log();
|
|
264
|
-
|
|
265
|
-
console.log();
|
|
266
|
-
process.stdout.write(` Testing connection… `);
|
|
305
|
+
process.stdout.write(` ${green("✓")} Saved. Testing connection… `);
|
|
267
306
|
|
|
268
307
|
const result = await testProvider(providerId, config, cwd);
|
|
269
308
|
if (result?.text) {
|
|
270
|
-
console.log(green("OK"));
|
|
271
|
-
console.log(` ${gray(result.text.trim().slice(0, 80))}`);
|
|
309
|
+
console.log(green("OK") + gray(` (${config[providerId].model})`));
|
|
272
310
|
} else {
|
|
273
311
|
console.log(yellow("no response"));
|
|
274
|
-
console.log(` ${yellow("⚠")}
|
|
312
|
+
console.log(` ${yellow("⚠")} Connection failed — double-check your API key.`);
|
|
275
313
|
}
|
|
276
314
|
}
|
|
277
315
|
|
|
278
316
|
console.log();
|
|
279
317
|
console.log(` ${green("✓")} ${bold(provider.name)} is ready.`);
|
|
280
|
-
console.log(` ${gray("
|
|
318
|
+
console.log(` ${gray("AI-powered commands:")} explain why review changelog`);
|
|
281
319
|
console.log();
|
|
282
320
|
|
|
283
321
|
// gitignore reminder
|
|
@@ -285,8 +323,7 @@ async function cmdSetup(cwd) {
|
|
|
285
323
|
if (fs.existsSync(gitignorePath)) {
|
|
286
324
|
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
287
325
|
if (!content.includes("integrations.json")) {
|
|
288
|
-
console.log(` ${yellow("⚠")}
|
|
289
|
-
console.log(` ${gray("Add")} ${cyan("inferno/integrations.json")} ${gray("to .gitignore to avoid committing it.")}`);
|
|
326
|
+
console.log(` ${yellow("⚠")} Add ${cyan("inferno/integrations.json")} to your .gitignore to avoid committing your API key.`);
|
|
290
327
|
console.log();
|
|
291
328
|
}
|
|
292
329
|
}
|