infernoflow 0.10.12 → 0.10.13
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/lib/ai/ideDetection.mjs
CHANGED
|
@@ -2,7 +2,7 @@ export function detectIdeContext(preferredIde = "auto") {
|
|
|
2
2
|
const env = process.env;
|
|
3
3
|
const lowerPreferred = String(preferredIde || "auto").toLowerCase();
|
|
4
4
|
|
|
5
|
-
const hasCursor = !!(env.CURSOR_TRACE_ID || env.CURSOR_AGENT || env.CURSOR_SESSION_ID);
|
|
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
6
|
const hasVscode = !!(env.VSCODE_PID || env.VSCODE_CWD || env.GITHUB_COPILOT_AGENT);
|
|
7
7
|
const hasWindsurf = !!(env.WINDSURF || env.CODEIUM || env.WINDSURF_SESSION_ID);
|
|
8
8
|
|
package/lib/commands/run.mjs
CHANGED
|
@@ -126,10 +126,33 @@ function buildPromptFallbackSuggestion(task, contract) {
|
|
|
126
126
|
|
|
127
127
|
async function generateWithIdeAgent(prompt) {
|
|
128
128
|
if (process.env.INFERNO_AGENT_MOCK_RESPONSE) return process.env.INFERNO_AGENT_MOCK_RESPONSE;
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const responseFile = process.env.INFERNO_AGENT_RESPONSE_FILE
|
|
130
|
+
? process.env.INFERNO_AGENT_RESPONSE_FILE
|
|
131
|
+
: path.join(process.cwd(), "inferno", "agent-response.json");
|
|
132
|
+
if (fs.existsSync(responseFile)) {
|
|
133
|
+
const data = fs.readFileSync(responseFile, "utf8");
|
|
134
|
+
fs.unlinkSync(responseFile);
|
|
135
|
+
return data;
|
|
131
136
|
}
|
|
132
|
-
|
|
137
|
+
const infernoDir = path.join(process.cwd(), "inferno");
|
|
138
|
+
const promptFile = path.join(infernoDir, "agent-prompt.md");
|
|
139
|
+
if (fs.existsSync(promptFile)) fs.unlinkSync(promptFile);
|
|
140
|
+
fs.writeFileSync(promptFile, prompt, "utf8");
|
|
141
|
+
process.stderr.write("\n \u2139 Prompt written to inferno/agent-prompt.md\n");
|
|
142
|
+
process.stderr.write(" \u2192 Open it, paste into Cursor or Claude\n");
|
|
143
|
+
process.stderr.write(" \u2192 Save the JSON reply to: inferno/agent-response.json\n");
|
|
144
|
+
process.stderr.write(" Waiting up to 5 minutes...\n\n");
|
|
145
|
+
const deadline = Date.now() + 300_000;
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
148
|
+
if (fs.existsSync(responseFile)) {
|
|
149
|
+
const data = fs.readFileSync(responseFile, "utf8");
|
|
150
|
+
fs.unlinkSync(responseFile);
|
|
151
|
+
process.stderr.write(" \u2714 Response received\n\n");
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw new Error("ide_agent_bridge_timeout");
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
export async function runCommand(args = []) {
|
package/package.json
CHANGED
|
@@ -1,112 +1,197 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Cursor hook:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor hook: capture agent output for infernoflow.
|
|
4
|
+
*
|
|
5
|
+
* Two jobs in one:
|
|
6
|
+
* 1. Always append agent text to inferno/CONTEXT.draft.md (existing behaviour)
|
|
7
|
+
* 2. When infernoflow is waiting (inferno/agent-prompt.md exists),
|
|
8
|
+
* extract the JSON block from the agent reply and write it to
|
|
9
|
+
* inferno/agent-response.json so infernoflow picks it up automatically.
|
|
10
|
+
*
|
|
11
|
+
* Trigger in .cursor/hooks.json:
|
|
12
|
+
* afterAgentResponse → { text }
|
|
13
|
+
* stop → { status, loop_count, ... } (--agent-stop flag)
|
|
14
|
+
*
|
|
15
|
+
* Never fail closed: errors go to stderr; stdout is {} for Cursor.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
|
|
20
|
+
/** Keep in sync with templates/scripts/inferno-promote-draft.mjs */
|
|
21
|
+
const DRAFT_HEADER = `# CONTEXT draft (gitignored)
|
|
22
|
+
Auto-captured by Cursor hooks (\`.cursor/hooks/inferno-session-draft.mjs\`). **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
|
|
23
|
+
---
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const MAX_MESSAGE_CHARS = 120_000;
|
|
27
|
+
const MAX_FILE_BYTES = 280_000;
|
|
28
|
+
|
|
29
|
+
// ── paths ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function projectRoot() {
|
|
32
|
+
return process.cwd();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function draftPath() {
|
|
36
|
+
return path.join(projectRoot(), "inferno", "CONTEXT.draft.md");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function agentPromptPath() {
|
|
40
|
+
return path.join(projectRoot(), "inferno", "agent-prompt.md");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function agentResponsePath() {
|
|
44
|
+
return path.join(projectRoot(), "inferno", "agent-response.json");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── CONTEXT.draft.md helpers ───────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function ensureDraftFile(file) {
|
|
50
|
+
if (!fs.existsSync(file)) {
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
fs.writeFileSync(file, DRAFT_HEADER, "utf8");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function trimFile(file) {
|
|
57
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
58
|
+
if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
|
|
59
|
+
const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
|
|
60
|
+
const idx = keep.indexOf("\n### ");
|
|
61
|
+
const body = idx === -1 ? keep : keep.slice(idx);
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
file,
|
|
64
|
+
`${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`,
|
|
65
|
+
"utf8",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function appendBlock(file, block) {
|
|
70
|
+
ensureDraftFile(file);
|
|
71
|
+
fs.appendFileSync(file, block, "utf8");
|
|
72
|
+
trimFile(file);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── JSON extraction ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function extractJsonFromText(text) {
|
|
78
|
+
// 1. fenced code block
|
|
79
|
+
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
80
|
+
if (fenceMatch) {
|
|
81
|
+
const candidate = fenceMatch[1].trim();
|
|
82
|
+
try {
|
|
83
|
+
JSON.parse(candidate);
|
|
84
|
+
return candidate;
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. largest bare JSON object in the text
|
|
89
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
90
|
+
if (jsonMatch) {
|
|
91
|
+
try {
|
|
92
|
+
JSON.parse(jsonMatch[0]);
|
|
93
|
+
return jsonMatch[0];
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── bridge: write agent-response.json if infernoflow is waiting ────────────
|
|
101
|
+
|
|
102
|
+
function maybeWriteAgentResponse(text) {
|
|
103
|
+
const promptFile = agentPromptPath();
|
|
104
|
+
const responseFile = agentResponsePath();
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(promptFile)) return false;
|
|
107
|
+
|
|
108
|
+
const json = extractJsonFromText(text);
|
|
109
|
+
if (!json) {
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
"[inferno-session-draft] infernoflow waiting but no JSON found in agent reply\n",
|
|
112
|
+
);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(responseFile, json, "utf8");
|
|
117
|
+
try {
|
|
118
|
+
fs.unlinkSync(promptFile);
|
|
119
|
+
} catch {}
|
|
120
|
+
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
"[inferno-session-draft] ✔ agent-response.json written — infernoflow will continue\n",
|
|
123
|
+
);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── stdin ──────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function readStdin() {
|
|
130
|
+
const chunks = [];
|
|
131
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
132
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── main ───────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function main() {
|
|
138
|
+
const agentStop = process.argv.includes("--agent-stop");
|
|
139
|
+
|
|
140
|
+
readStdin()
|
|
141
|
+
.then((raw) => {
|
|
142
|
+
let data = {};
|
|
143
|
+
try {
|
|
144
|
+
data = raw.trim() ? JSON.parse(raw) : {};
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.error("[inferno-session-draft] stdin JSON parse:", e.message);
|
|
147
|
+
console.log("{}");
|
|
148
|
+
process.exit(0);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const file = draftPath();
|
|
153
|
+
|
|
154
|
+
if (agentStop) {
|
|
155
|
+
const status = data.status ?? "unknown";
|
|
156
|
+
const loop = data.loop_count ?? 0;
|
|
157
|
+
appendBlock(
|
|
158
|
+
file,
|
|
159
|
+
`\n### _agent stop_ (${new Date().toISOString()})\n\nstatus: \`${status}\` · loop_count: ${loop}\n\n---\n`,
|
|
160
|
+
);
|
|
161
|
+
console.log("{}");
|
|
162
|
+
process.exit(0);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const text = typeof data.text === "string" ? data.text : "";
|
|
167
|
+
if (!text.trim()) {
|
|
168
|
+
console.log("{}");
|
|
169
|
+
process.exit(0);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Job 2: feed infernoflow's file-based bridge if it is waiting
|
|
174
|
+
maybeWriteAgentResponse(text);
|
|
175
|
+
|
|
176
|
+
// Job 1: always append to CONTEXT.draft.md
|
|
177
|
+
const clipped =
|
|
178
|
+
text.length > MAX_MESSAGE_CHARS
|
|
179
|
+
? `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n_…trimmed (${text.length - MAX_MESSAGE_CHARS} chars omitted)_\n`
|
|
180
|
+
: text;
|
|
181
|
+
|
|
182
|
+
appendBlock(
|
|
183
|
+
file,
|
|
184
|
+
`\n### Assistant message (${new Date().toISOString()})\n\n${clipped}\n\n---\n`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
console.log("{}");
|
|
188
|
+
process.exit(0);
|
|
189
|
+
})
|
|
190
|
+
.catch((e) => {
|
|
191
|
+
console.error("[inferno-session-draft]", e);
|
|
192
|
+
console.log("{}");
|
|
193
|
+
process.exit(0);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main();
|