infernoflow 0.10.11 → 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/CHANGELOG.md +26 -0
- package/lib/ai/ideDetection.mjs +1 -1
- package/lib/commands/run.mjs +26 -3
- package/package.json +3 -2
- package/templates/cursor/hooks/inferno-session-draft.mjs +197 -112
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog — infernoflow
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.10.12 — 2026-04-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `infernoflow install-cursor-hooks` — Cursor Agent hooks append assistant replies to `inferno/CONTEXT.draft.md`; `infernoflow init --cursor-hooks`.
|
|
9
|
+
- `infernoflow install-vscode-copilot-hooks` — VS Code + GitHub Copilot agent hooks (Preview) via `.github/hooks/`; `infernoflow init --vscode-copilot-hooks`.
|
|
10
|
+
- Shared draft tooling: `scripts/inferno-promote-draft.mjs`, `.gitignore` entry for `inferno/CONTEXT.draft.md`.
|
|
11
|
+
- `lib/draftToolingInstall.mjs` — shared installer logic for promote script and gitignore.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- CLI help widens command column for long names (e.g. `install-vscode-copilot-hooks`).
|
|
15
|
+
|
|
16
|
+
## 0.1.0 — 2026-02-26
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `infernoflow init` — interactive scaffold with prompts
|
|
20
|
+
- `infernoflow check` — full validation with clear error messages
|
|
21
|
+
- `infernoflow status` — at-a-glance dashboard
|
|
22
|
+
- `infernoflow doc-gate` — CI hook for keeping docs in sync
|
|
23
|
+
- Zero npm dependencies — works with Node.js 18+ out of the box
|
|
24
|
+
- `--json` flag on check for CI pipelines
|
|
25
|
+
- Auto-detect project name from package.json
|
|
26
|
+
- Auto-add npm scripts to package.json on init
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.13",
|
|
4
4
|
"description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"bin",
|
|
14
14
|
"lib",
|
|
15
15
|
"templates",
|
|
16
|
-
"README.md"
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"test": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs && node scripts/implement-smoke.mjs && node scripts/adopt-smoke.mjs && node scripts/pr-impact-smoke.mjs && node scripts/sync-smoke.mjs && node scripts/run-smoke.mjs",
|
|
@@ -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();
|