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,111 +1 @@
1
- /**
2
- * lib/adopters/css.mjs
3
- * CSS / SCSS / design token scanner for --adopt.
4
- * Extracts design tokens, component class names, and UI patterns.
5
- */
6
-
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
-
10
- function safeRead(filePath) {
11
- try { return fs.readFileSync(filePath, "utf8"); } catch { return ""; }
12
- }
13
-
14
- /**
15
- * Scan CSS/SCSS/style files for design tokens and UI signals.
16
- *
17
- * Returns:
18
- * {
19
- * designTokens: string[], // CSS custom properties (--var-name)
20
- * colorTokens: string[], // tokens that look like colors
21
- * spacingTokens: string[], // tokens that look like spacing
22
- * componentClasses: string[], // BEM-style or component-level class names
23
- * themeVars: string[], // theme-related variables
24
- * }
25
- */
26
- export function scanCSS(cwd, files) {
27
- const allTokens = new Set();
28
- const colorTokens = new Set();
29
- const spacingTokens = new Set();
30
- const componentClasses = new Set();
31
- const themeVars = new Set();
32
-
33
- const styleFiles = files.filter(f =>
34
- /\.(css|scss|sass|less|styl)$/.test(f) ||
35
- // Also scan JS/TS files for CSS-in-JS (styled-components, emotion)
36
- (/\.(ts|tsx|js|jsx)$/.test(f) && !f.includes("node_modules"))
37
- );
38
-
39
- for (const filePath of styleFiles) {
40
- const text = safeRead(filePath);
41
- if (!text) continue;
42
-
43
- // ── CSS custom properties (design tokens) ─────────────────────────────
44
- const tokenMatches = text.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);
45
- for (const m of tokenMatches) {
46
- const token = `--${m[1]}`;
47
- allTokens.add(token);
48
-
49
- // Classify by name
50
- if (/color|colour|bg|background|text|border|shadow|fill|stroke/i.test(m[1])) {
51
- colorTokens.add(token);
52
- } else if (/space|spacing|gap|padding|margin|size|radius|width|height/i.test(m[1])) {
53
- spacingTokens.add(token);
54
- } else if (/theme|primary|secondary|accent|brand|dark|light/i.test(m[1])) {
55
- themeVars.add(token);
56
- }
57
- }
58
-
59
- // ── CSS class names → component hints ────────────────────────────────
60
- if (/\.(css|scss|sass|less)$/.test(filePath)) {
61
- // BEM block names: .my-component { }
62
- const classMatches = text.matchAll(/^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)[\s{,]/gm);
63
- for (const m of classMatches) {
64
- const cls = m[1];
65
- // Skip utility classes (short names, numbers, state classes)
66
- if (cls.length < 4) continue;
67
- if (/^(flex|grid|block|hidden|text|font|bg|border|p-|m-|w-|h-)/.test(cls)) continue;
68
- if (/^(active|disabled|hover|focus|error|success|warning)$/.test(cls)) continue;
69
- componentClasses.add(cls);
70
- }
71
- }
72
-
73
- // ── CSS-in-JS: styled-components / emotion ────────────────────────────
74
- if (/\.(ts|tsx|js|jsx)$/.test(filePath)) {
75
- // styled.div`...` or styled(Component)`...`
76
- const styledMatches = text.matchAll(/(?:styled|css)`[^`]*--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);
77
- for (const m of styledMatches) allTokens.add(`--${m[1]}`);
78
-
79
- // Tailwind arbitrary values referencing CSS vars: bg-[--color-primary]
80
- const tailwindVars = text.matchAll(/\[--([a-zA-Z][a-zA-Z0-9_-]*)\]/g);
81
- for (const m of tailwindVars) allTokens.add(`--${m[1]}`);
82
- }
83
- }
84
-
85
- return {
86
- designTokens: Array.from(allTokens).sort().slice(0, 40),
87
- colorTokens: Array.from(colorTokens).sort().slice(0, 20),
88
- spacingTokens: Array.from(spacingTokens).sort().slice(0, 15),
89
- componentClasses: Array.from(componentClasses).sort().slice(0, 30),
90
- themeVars: Array.from(themeVars).sort().slice(0, 15),
91
- };
92
- }
93
-
94
- /**
95
- * Detect which CSS framework is in use from class names and package deps.
96
- */
97
- export function detectCSSFramework(text, externalLibraries = []) {
98
- const hasDep = (name) => externalLibraries.includes(name);
99
- const hasClass = (pattern) => pattern.test(text);
100
-
101
- if (hasDep("tailwindcss") || hasClass(/\b(?:flex|grid|px-\d|py-\d|text-\w+|bg-\w+|rounded)/)) return "tailwind";
102
- if (hasDep("bootstrap") || hasClass(/\b(?:container|row|col-|btn btn-|navbar|card)/)) return "bootstrap";
103
- if (externalLibraries.some(d => d.startsWith("@angular/material"))) return "angular-material";
104
- if (hasDep("antd") || hasClass(/\bant-/)) return "ant-design";
105
- if (hasDep("@mui/material") || hasDep("@material-ui/core")) return "mui";
106
- if (hasDep("styled-components")) return "styled-components";
107
- if (hasDep("@emotion/react") || hasDep("@emotion/styled")) return "emotion";
108
- if (hasDep("@chakra-ui/react")) return "chakra-ui";
109
- if (hasDep("@radix-ui/react-primitive")) return "radix-ui";
110
- return "unknown";
111
- }
1
+ import*as h from"node:fs";import"node:path";function g(c){try{return h.readFileSync(c,"utf8")}catch{return""}}function w(c,l){const t=new Set,a=new Set,s=new Set,d=new Set,f=new Set,m=l.filter(o=>/\.(css|scss|sass|less|styl)$/.test(o)||/\.(ts|tsx|js|jsx)$/.test(o)&&!o.includes("node_modules"));for(const o of m){const i=g(o);if(!i)continue;const u=i.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);for(const e of u){const r=`--${e[1]}`;t.add(r),/color|colour|bg|background|text|border|shadow|fill|stroke/i.test(e[1])?a.add(r):/space|spacing|gap|padding|margin|size|radius|width|height/i.test(e[1])?s.add(r):/theme|primary|secondary|accent|brand|dark|light/i.test(e[1])&&f.add(r)}if(/\.(css|scss|sass|less)$/.test(o)){const e=i.matchAll(/^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)[\s{,]/gm);for(const r of e){const n=r[1];n.length<4||/^(flex|grid|block|hidden|text|font|bg|border|p-|m-|w-|h-)/.test(n)||/^(active|disabled|hover|focus|error|success|warning)$/.test(n)||d.add(n)}}if(/\.(ts|tsx|js|jsx)$/.test(o)){const e=i.matchAll(/(?:styled|css)`[^`]*--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g);for(const n of e)t.add(`--${n[1]}`);const r=i.matchAll(/\[--([a-zA-Z][a-zA-Z0-9_-]*)\]/g);for(const n of r)t.add(`--${n[1]}`)}}return{designTokens:Array.from(t).sort().slice(0,40),colorTokens:Array.from(a).sort().slice(0,20),spacingTokens:Array.from(s).sort().slice(0,15),componentClasses:Array.from(d).sort().slice(0,30),themeVars:Array.from(f).sort().slice(0,15)}}function y(c,l=[]){const t=s=>l.includes(s),a=s=>s.test(c);return t("tailwindcss")||a(/\b(?:flex|grid|px-\d|py-\d|text-\w+|bg-\w+|rounded)/)?"tailwind":t("bootstrap")||a(/\b(?:container|row|col-|btn btn-|navbar|card)/)?"bootstrap":l.some(s=>s.startsWith("@angular/material"))?"angular-material":t("antd")||a(/\bant-/)?"ant-design":t("@mui/material")||t("@material-ui/core")?"mui":t("styled-components")?"styled-components":t("@emotion/react")||t("@emotion/styled")?"emotion":t("@chakra-ui/react")?"chakra-ui":t("@radix-ui/react-primitive")?"radix-ui":"unknown"}export{y as detectCSSFramework,w as scanCSS};
@@ -1,104 +1 @@
1
- /**
2
- * lib/adopters/react.mjs
3
- * React-specific scanner for --adopt.
4
- * Detects components, hooks, routes, and UI capabilities from React projects.
5
- */
6
-
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
-
10
- function safeRead(filePath) {
11
- try { return fs.readFileSync(filePath, "utf8"); } catch { return ""; }
12
- }
13
-
14
- /**
15
- * Scan a React project's source files for UI signals.
16
- *
17
- * Returns:
18
- * {
19
- * components: string[],
20
- * customHooks: string[],
21
- * routes: string[],
22
- * capabilities: { id, title, reason, sourceFiles }[]
23
- * }
24
- */
25
- export function scanReact(cwd, files) {
26
- const components = new Set();
27
- const customHooks = new Set();
28
- const routes = new Set();
29
- const capabilityMap = new Map();
30
-
31
- const addCap = (id, title, reason, filePath) => {
32
- if (!capabilityMap.has(id)) {
33
- capabilityMap.set(id, { id, title, reason, sourceFiles: new Set() });
34
- }
35
- capabilityMap.get(id).sourceFiles.add(path.relative(cwd, filePath));
36
- };
37
-
38
- for (const filePath of files) {
39
- if (!/\.(tsx?|jsx?)$/.test(filePath)) continue;
40
- const text = safeRead(filePath);
41
- if (!text) continue;
42
-
43
- // ── Component detection ───────────────────────────────────────────────
44
- // export default function MyComponent / export function MyComponent
45
- const exportFn = text.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);
46
- for (const m of exportFn) {
47
- components.add(m[1]);
48
- // Page/View/Screen/Dashboard components → ViewXxx capability
49
- if (/Page|View|Screen|Dashboard|Panel|Modal|Dialog/i.test(m[1])) {
50
- const capId = "View" + m[1].replace(/(Page|View|Screen|Dashboard|Panel|Modal|Dialog)$/, "");
51
- addCap(capId, `View ${m[1].replace(/([A-Z])/g, " $1").trim()}`, `React component: ${m[1]}`, filePath);
52
- }
53
- }
54
-
55
- // Arrow function components: const MyComponent = () =>
56
- const arrowComp = text.matchAll(/(?:export\s+)?const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:React\.memo\()?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/g);
57
- for (const m of arrowComp) {
58
- components.add(m[1]);
59
- }
60
-
61
- // ── Custom hooks ─────────────────────────────────────────────────────
62
- const hookMatches = text.matchAll(/export\s+(?:default\s+)?function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/g);
63
- for (const m of hookMatches) customHooks.add(m[1]);
64
-
65
- const hookArrow = text.matchAll(/(?:export\s+)?const\s+(use[A-Z][A-Za-z0-9_]*)\s*=/g);
66
- for (const m of hookArrow) customHooks.add(m[1]);
67
-
68
- // ── Route detection (react-router) ────────────────────────────────────
69
- // <Route path="/some/path" or path: "/some/path"
70
- const routeJsx = text.matchAll(/<Route[^>]+path\s*=\s*["'`]([^"'`]+)["'`]/g);
71
- for (const m of routeJsx) {
72
- const p = m[1].replace(/^\//, "").replace(/:[\w]+/g, "{id}");
73
- if (p) routes.add(p);
74
- }
75
-
76
- const routeObj = text.matchAll(/path\s*:\s*["'`]([^"'`]+)["'`]/g);
77
- for (const m of routeObj) {
78
- const p = m[1].replace(/^\//, "").replace(/:[\w]+/g, "{id}");
79
- if (p && p !== "*" && p.length < 60) routes.add(p);
80
- }
81
-
82
- // ── Button / action detection ─────────────────────────────────────────
83
- const onClicks = text.matchAll(/onClick\s*=\s*\{(?:[^}]*\b(delete|remove|create|add|submit|save|search|filter|toggle|update|edit)\b[^}]*)\}/gi);
84
- for (const m of onClicks) {
85
- const action = m[1].toLowerCase();
86
- if (action === "delete" || action === "remove") addCap("DeleteItem", "Delete Item", `onClick handler contains "${action}"`, filePath);
87
- if (action === "create" || action === "add") addCap("CreateItem", "Create Item", `onClick handler contains "${action}"`, filePath);
88
- if (action === "submit" || action === "save" || action === "update" || action === "edit") addCap("UpdateItem", "Update Item", `onClick handler contains "${action}"`, filePath);
89
- if (action === "search") addCap("SearchItems", "Search Items", `onClick handler contains "search"`, filePath);
90
- if (action === "filter") addCap("FilterItems", "Filter Items", `onClick handler contains "filter"`, filePath);
91
- if (action === "toggle") addCap("ToggleComplete", "Toggle Complete", `onClick handler contains "toggle"`, filePath);
92
- }
93
- }
94
-
95
- return {
96
- components: Array.from(components).sort(),
97
- customHooks: Array.from(customHooks).sort(),
98
- routes: Array.from(routes).sort(),
99
- capabilities: Array.from(capabilityMap.values()).map(c => ({
100
- ...c,
101
- sourceFiles: Array.from(c.sourceFiles),
102
- })),
103
- };
104
- }
1
+ import*as w from"node:fs";import*as C from"node:path";function k(c){try{return w.readFileSync(c,"utf8")}catch{return""}}function I(c,d){const n=new Set,l=new Set,i=new Set,r=new Map,a=(t,s,m,f)=>{r.has(t)||r.set(t,{id:t,title:s,reason:m,sourceFiles:new Set}),r.get(t).sourceFiles.add(C.relative(c,f))};for(const t of d){if(!/\.(tsx?|jsx?)$/.test(t))continue;const s=k(t);if(!s)continue;const m=s.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)\s*\(/g);for(const o of m)if(n.add(o[1]),/Page|View|Screen|Dashboard|Panel|Modal|Dialog/i.test(o[1])){const e="View"+o[1].replace(/(Page|View|Screen|Dashboard|Panel|Modal|Dialog)$/,"");a(e,`View ${o[1].replace(/([A-Z])/g," $1").trim()}`,`React component: ${o[1]}`,t)}const f=s.matchAll(/(?:export\s+)?const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:React\.memo\()?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/g);for(const o of f)n.add(o[1]);const p=s.matchAll(/export\s+(?:default\s+)?function\s+(use[A-Z][A-Za-z0-9_]*)\s*\(/g);for(const o of p)l.add(o[1]);const h=s.matchAll(/(?:export\s+)?const\s+(use[A-Z][A-Za-z0-9_]*)\s*=/g);for(const o of h)l.add(o[1]);const u=s.matchAll(/<Route[^>]+path\s*=\s*["'`]([^"'`]+)["'`]/g);for(const o of u){const e=o[1].replace(/^\//,"").replace(/:[\w]+/g,"{id}");e&&i.add(e)}const g=s.matchAll(/path\s*:\s*["'`]([^"'`]+)["'`]/g);for(const o of g){const e=o[1].replace(/^\//,"").replace(/:[\w]+/g,"{id}");e&&e!=="*"&&e.length<60&&i.add(e)}const A=s.matchAll(/onClick\s*=\s*\{(?:[^}]*\b(delete|remove|create|add|submit|save|search|filter|toggle|update|edit)\b[^}]*)\}/gi);for(const o of A){const e=o[1].toLowerCase();(e==="delete"||e==="remove")&&a("DeleteItem","Delete Item",`onClick handler contains "${e}"`,t),(e==="create"||e==="add")&&a("CreateItem","Create Item",`onClick handler contains "${e}"`,t),(e==="submit"||e==="save"||e==="update"||e==="edit")&&a("UpdateItem","Update Item",`onClick handler contains "${e}"`,t),e==="search"&&a("SearchItems","Search Items",'onClick handler contains "search"',t),e==="filter"&&a("FilterItems","Filter Items",'onClick handler contains "filter"',t),e==="toggle"&&a("ToggleComplete","Toggle Complete",'onClick handler contains "toggle"',t)}}return{components:Array.from(n).sort(),customHooks:Array.from(l).sort(),routes:Array.from(i).sort(),capabilities:Array.from(r.values()).map(t=>({...t,sourceFiles:Array.from(t.sourceFiles)}))}}export{I as scanReact};
@@ -1,31 +1 @@
1
- export function detectIdeContext(preferredIde = "auto") {
2
- const env = process.env;
3
- const lowerPreferred = String(preferredIde || "auto").toLowerCase();
4
-
5
- const hasCursor = !!(env.CURSOR_TRACE_ID || env.CURSOR_AGENT || env.CURSOR_SESSION_ID || (env.VSCODE_GIT_ASKPASS_NODE || "").toLowerCase().includes("cursor") || (env.VSCODE_GIT_ASKPASS_MAIN || "").toLowerCase().includes("cursor"));
6
- const hasVscode = !!(env.VSCODE_PID || env.VSCODE_CWD || env.GITHUB_COPILOT_AGENT);
7
- const hasWindsurf = !!(env.WINDSURF || env.CODEIUM || env.WINDSURF_SESSION_ID);
8
-
9
- let ideDetected = "unknown";
10
- if (hasCursor) ideDetected = "cursor";
11
- else if (hasVscode) ideDetected = "vscode";
12
- else if (hasWindsurf) ideDetected = "windsurf";
13
-
14
- if (lowerPreferred !== "auto" && ["cursor", "vscode", "windsurf"].includes(lowerPreferred)) {
15
- ideDetected = lowerPreferred;
16
- }
17
-
18
- const explicitAgentAvailability = env.INFERNO_AGENT_AVAILABLE;
19
- const agentAvailable = explicitAgentAvailability != null
20
- ? explicitAgentAvailability === "1" || explicitAgentAvailability === "true"
21
- : ideDetected !== "unknown";
22
-
23
- const reasonCodes = [];
24
- if (ideDetected !== "unknown") reasonCodes.push(`IDE_${ideDetected.toUpperCase()}_DETECTED`);
25
- else reasonCodes.push("IDE_UNKNOWN");
26
- if (agentAvailable) reasonCodes.push("IDE_AGENT_AVAILABLE");
27
- else reasonCodes.push("IDE_AGENT_UNAVAILABLE");
28
-
29
- return { ideDetected, agentAvailable, reasonCodes };
30
- }
31
-
1
+ function A(_="auto"){const e=process.env,n=String(_||"auto").toLowerCase(),u=!!(e.CURSOR_TRACE_ID||e.CURSOR_AGENT||e.CURSOR_SESSION_ID||(e.VSCODE_GIT_ASKPASS_NODE||"").toLowerCase().includes("cursor")||(e.VSCODE_GIT_ASKPASS_MAIN||"").toLowerCase().includes("cursor")),E=!!(e.VSCODE_PID||e.VSCODE_CWD||e.GITHUB_COPILOT_AGENT),c=!!(e.WINDSURF||e.CODEIUM||e.WINDSURF_SESSION_ID);let s="unknown";u?s="cursor":E?s="vscode":c&&(s="windsurf"),n!=="auto"&&["cursor","vscode","windsurf"].includes(n)&&(s=n);const t=e.INFERNO_AGENT_AVAILABLE,r=t!=null?t==="1"||t==="true":s!=="unknown",o=[];return s!=="unknown"?o.push(`IDE_${s.toUpperCase()}_DETECTED`):o.push("IDE_UNKNOWN"),r?o.push("IDE_AGENT_AVAILABLE"):o.push("IDE_AGENT_UNAVAILABLE"),{ideDetected:s,agentAvailable:r,reasonCodes:o}}export{A as detectIdeContext};
@@ -1,88 +1 @@
1
- const DEFAULT_TIMEOUT_MS = 45000;
2
-
3
- function withTimeout(ms) {
4
- const controller = new AbortController();
5
- const timer = setTimeout(() => controller.abort(), ms);
6
- return { controller, timer };
7
- }
8
-
9
- async function callOllama(prompt, timeoutMs) {
10
- const endpoint = process.env.INFERNO_LOCAL_ENDPOINT || "http://127.0.0.1:11434/api/generate";
11
- const model = process.env.INFERNO_LOCAL_MODEL || "llama3.1:8b";
12
- const { controller, timer } = withTimeout(timeoutMs);
13
- try {
14
- const res = await fetch(endpoint, {
15
- method: "POST",
16
- headers: { "Content-Type": "application/json" },
17
- signal: controller.signal,
18
- body: JSON.stringify({
19
- model,
20
- prompt,
21
- stream: false,
22
- }),
23
- });
24
- if (!res.ok) {
25
- const body = await res.text();
26
- throw new Error(`local_model_http_${res.status}: ${body.slice(0, 240)}`);
27
- }
28
- const data = await res.json();
29
- if (!data?.response || typeof data.response !== "string") {
30
- throw new Error("local_model_invalid_response");
31
- }
32
- return data.response.trim();
33
- } finally {
34
- clearTimeout(timer);
35
- }
36
- }
37
-
38
- async function callOpenAICompat(prompt, timeoutMs) {
39
- const endpoint = process.env.INFERNO_LOCAL_ENDPOINT || "http://127.0.0.1:1234/v1/chat/completions";
40
- const model = process.env.INFERNO_LOCAL_MODEL || "local-model";
41
- const apiKey = process.env.INFERNO_LOCAL_API_KEY || "local";
42
- const { controller, timer } = withTimeout(timeoutMs);
43
- try {
44
- const res = await fetch(endpoint, {
45
- method: "POST",
46
- headers: {
47
- "Content-Type": "application/json",
48
- Authorization: `Bearer ${apiKey}`,
49
- },
50
- signal: controller.signal,
51
- body: JSON.stringify({
52
- model,
53
- temperature: 0.1,
54
- messages: [
55
- { role: "system", content: "Return JSON only." },
56
- { role: "user", content: prompt },
57
- ],
58
- }),
59
- });
60
- if (!res.ok) {
61
- const body = await res.text();
62
- throw new Error(`local_model_http_${res.status}: ${body.slice(0, 240)}`);
63
- }
64
- const data = await res.json();
65
- const text = data?.choices?.[0]?.message?.content;
66
- if (!text || typeof text !== "string") {
67
- throw new Error("local_model_invalid_response");
68
- }
69
- return text.trim();
70
- } finally {
71
- clearTimeout(timer);
72
- }
73
- }
74
-
75
- export async function generateWithLocalModel(prompt, options = {}) {
76
- if (process.env.INFERNO_LOCAL_MOCK_RESPONSE) {
77
- return process.env.INFERNO_LOCAL_MOCK_RESPONSE;
78
- }
79
-
80
- const provider = (process.env.INFERNO_LOCAL_PROVIDER || "ollama").toLowerCase();
81
- const timeoutMs = Number(options.timeoutMs || process.env.INFERNO_LOCAL_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
82
-
83
- if (provider === "openai") {
84
- return callOpenAICompat(prompt, timeoutMs);
85
- }
86
- return callOllama(prompt, timeoutMs);
87
- }
88
-
1
+ const E=45e3;function _(e){const t=new AbortController,n=setTimeout(()=>t.abort(),e);return{controller:t,timer:n}}async function p(e,t){const n=process.env.INFERNO_LOCAL_ENDPOINT||"http://127.0.0.1:11434/api/generate",s=process.env.INFERNO_LOCAL_MODEL||"llama3.1:8b",{controller:a,timer:c}=_(t);try{const r=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},signal:a.signal,body:JSON.stringify({model:s,prompt:e,stream:!1})});if(!r.ok){const l=await r.text();throw new Error(`local_model_http_${r.status}: ${l.slice(0,240)}`)}const o=await r.json();if(!o?.response||typeof o.response!="string")throw new Error("local_model_invalid_response");return o.response.trim()}finally{clearTimeout(c)}}async function m(e,t){const n=process.env.INFERNO_LOCAL_ENDPOINT||"http://127.0.0.1:1234/v1/chat/completions",s=process.env.INFERNO_LOCAL_MODEL||"local-model",a=process.env.INFERNO_LOCAL_API_KEY||"local",{controller:c,timer:r}=_(t);try{const o=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},signal:c.signal,body:JSON.stringify({model:s,temperature:.1,messages:[{role:"system",content:"Return JSON only."},{role:"user",content:e}]})});if(!o.ok){const O=await o.text();throw new Error(`local_model_http_${o.status}: ${O.slice(0,240)}`)}const i=(await o.json())?.choices?.[0]?.message?.content;if(!i||typeof i!="string")throw new Error("local_model_invalid_response");return i.trim()}finally{clearTimeout(r)}}async function N(e,t={}){if(process.env.INFERNO_LOCAL_MOCK_RESPONSE)return process.env.INFERNO_LOCAL_MOCK_RESPONSE;const n=(process.env.INFERNO_LOCAL_PROVIDER||"ollama").toLowerCase(),s=Number(t.timeoutMs||process.env.INFERNO_LOCAL_TIMEOUT_MS||45e3);return n==="openai"?m(e,s):p(e,s)}export{N as generateWithLocalModel};
@@ -1,295 +1,2 @@
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
- */
15
-
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;
62
-
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 };
77
- }
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";
111
-
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 {}
219
- }
220
-
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);
229
- return {
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
235
- };
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
- }
1
+ import*as v from"node:fs";import*as y from"node:path";import*as A from"node:https";import*as _ from"node:http";function m(n){const t=y.join(n,"inferno","integrations.json");if(!v.existsSync(t))return{};try{return JSON.parse(v.readFileSync(t,"utf8"))}catch{return{}}}function u(n,t,s){return new Promise((o,e)=>{const r=new URL(n),p=r.protocol==="https:"?A:_,l=JSON.stringify(s),c=p.request({hostname:r.hostname,port:r.port||(r.protocol==="https:"?443:80),path:r.pathname+(r.search||""),method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(l),...t}},i=>{let a="";i.on("data",h=>a+=h),i.on("end",()=>{try{o({status:i.statusCode,body:JSON.parse(a)})}catch{o({status:i.statusCode,body:a})}})});c.on("error",e),c.write(l),c.end()})}async function E(n,t){const s=process.env.ANTHROPIC_API_KEY||t.anthropic?.apiKey;if(!s)return null;const o=t.anthropic?.model||process.env.ANTHROPIC_MODEL||"claude-sonnet-4-6";try{const e=await u("https://api.anthropic.com/v1/messages",{"x-api-key":s,"anthropic-version":"2023-06-01"},{model:o,max_tokens:1024,messages:[{role:"user",content:n}]});if(e.status===200&&e.body?.content?.[0]?.text)return{text:e.body.content[0].text,provider:"anthropic",model:o}}catch{}return null}async function O(n,t){const s=process.env.OPENAI_API_KEY||t.openai?.apiKey,o=process.env.OPENAI_ENDPOINT||t.openai?.endpoint||"https://api.openai.com/v1/chat/completions";if(!s)return null;const e=t.openai?.model||process.env.OPENAI_MODEL||"gpt-4o";try{const r=await u(o,{Authorization:`Bearer ${s}`},{model:e,max_tokens:1024,messages:[{role:"user",content:n}]});if(r.status===200&&r.body?.choices?.[0]?.message?.content)return{text:r.body.choices[0].message.content,provider:"openai",model:e}}catch{}return null}async function I(n,t){const s=process.env.GOOGLE_AI_API_KEY||process.env.GEMINI_API_KEY||t.gemini?.apiKey;if(!s)return null;const o=t.gemini?.model||process.env.GEMINI_MODEL||"gemini-2.0-flash";try{const e=await u(`https://generativelanguage.googleapis.com/v1beta/models/${o}:generateContent?key=${s}`,{},{contents:[{parts:[{text:n}]}]}),r=e.body?.candidates?.[0]?.content?.parts?.[0]?.text;if(e.status===200&&r)return{text:r,provider:"gemini",model:o}}catch{}return null}async function P(n,t){const s=process.env.OPENROUTER_API_KEY||t.openrouter?.apiKey;if(!s)return null;const o=t.openrouter?.model||process.env.OPENROUTER_MODEL||"anthropic/claude-sonnet-4-6";try{const e=await u("https://openrouter.ai/api/v1/chat/completions",{Authorization:`Bearer ${s}`,"HTTP-Referer":"https://infernoflow.dev"},{model:o,messages:[{role:"user",content:n}],max_tokens:1024});if(e.status===200&&e.body?.choices?.[0]?.message?.content)return{text:e.body.choices[0].message.content,provider:"openrouter",model:o}}catch{}return null}async function g(n,t){const s=process.env.OLLAMA_HOST||t.ollama?.host||"http://localhost:11434",o=process.env.OLLAMA_MODEL||t.ollama?.model||"llama3";try{await new Promise((e,r)=>{const p=new URL(s);_.get({hostname:p.hostname,port:p.port||11434,path:"/api/tags",timeout:1500},l=>e(l)).on("error",r)})}catch{return null}try{const e=await u(`${s}/api/generate`,{},{model:o,prompt:n,stream:!1});if(e.status===200&&e.body?.response)return{text:e.body.response,provider:"ollama",model:o}}catch{}return null}async function K(n,t={}){const s=t.cwd||process.cwd(),o=m(s),e=(t.provider||"auto").toLowerCase(),r=t.silent??!0,p=[["anthropic",()=>E(n,o)],["openai",()=>O(n,o)],["gemini",()=>I(n,o)],["openrouter",()=>P(n,o)],["ollama",()=>g(n,o)]],l=e==="auto"||e==="prompt"?p:p.filter(([c])=>c===e);for(const[c,i]of l)try{const a=await i();if(a)return r||process.stderr.write(` [infernoflow ai] using ${c}:${a.model}
2
+ `),a}catch{}return null}function R(n){const t=m(n);return{anthropic:!!(process.env.ANTHROPIC_API_KEY||t.anthropic?.apiKey),openai:!!(process.env.OPENAI_API_KEY||t.openai?.apiKey),gemini:!!(process.env.GOOGLE_AI_API_KEY||process.env.GEMINI_API_KEY||t.gemini?.apiKey),openrouter:!!(process.env.OPENROUTER_API_KEY||t.openrouter?.apiKey),ollama:!1}}async function N(n="auto",t="auto"){const s=process.cwd(),o=m(s),e=[],r=!!process.env.VSCODE_PID||!!process.env.TERM_PROGRAM?.includes("vscode"),p=!!process.env.CURSOR_TRACE_ID||!!process.env.CURSOR_CHANNEL,l=p?"cursor":r?"vscode":"terminal",c={anthropic:!!(process.env.ANTHROPIC_API_KEY||o.anthropic?.apiKey),openai:!!(process.env.OPENAI_API_KEY||o.openai?.apiKey),gemini:!!(process.env.GOOGLE_AI_API_KEY||process.env.GEMINI_API_KEY||o.gemini?.apiKey),openrouter:!!(process.env.OPENROUTER_API_KEY||o.openrouter?.apiKey),ollama:!1,vscode:r||p};(process.env.OLLAMA_HOST||o.ollama?.host)&&(c.ollama=!0,e.push("ollama_env"));let i="none";const a=(n||"auto").toLowerCase();if(a!=="auto"&&a!=="prompt"&&c[a])i=a,e.push(`forced_${a}`);else{const f=["vscode","anthropic","openai","gemini","openrouter","ollama"];for(const d of f)if(c[d]){i=d,e.push(`auto_${d}`);break}}return i!=="none"?{providerResolved:i,ideDetected:l,agentAvailable:!0,reasonCodes:e}:(e.push("no_provider"),{providerResolved:"none",ideDetected:l,agentAvailable:!1,reasonCodes:e,error:"agent_unavailable"})}export{K as callAI,R as detectAvailableProviders,m as readAiConfig,N as resolveProvider};