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 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
@@ -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
 
@@ -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
- if (process.env.INFERNO_AGENT_RESPONSE_FILE && fs.existsSync(process.env.INFERNO_AGENT_RESPONSE_FILE)) {
130
- return fs.readFileSync(process.env.INFERNO_AGENT_RESPONSE_FILE, "utf8");
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
- throw new Error("ide_agent_bridge_not_configured");
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.11",
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: append agent output to inferno/CONTEXT.draft.md (gitignored).
4
- * - Default stdin: afterAgentResponse → { text }
5
- * - --agent-stop stdin: stop → { status, loop_count, ... }
6
- * Never fail closed: errors go to stderr; stdout is {} for Cursor.
7
- */
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
10
-
11
- /** Keep in sync with templates/scripts/inferno-promote-draft.mjs (split at first \\n---\\n). */
12
- const DRAFT_HEADER = `# CONTEXT draft (gitignored)
13
-
14
- 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\`.
15
-
16
- ---
17
- `;
18
-
19
- const MAX_MESSAGE_CHARS = 120_000;
20
- const MAX_FILE_BYTES = 280_000;
21
-
22
- function projectRoot() {
23
- return process.cwd();
24
- }
25
-
26
- function draftPath() {
27
- return path.join(projectRoot(), "inferno", "CONTEXT.draft.md");
28
- }
29
-
30
- function ensureDraftFile(file) {
31
- if (!fs.existsSync(file)) {
32
- fs.mkdirSync(path.dirname(file), { recursive: true });
33
- fs.writeFileSync(file, DRAFT_HEADER, "utf8");
34
- }
35
- }
36
-
37
- function trimFile(file) {
38
- const raw = fs.readFileSync(file, "utf8");
39
- if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
40
- const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
41
- const idx = keep.indexOf("\n### ");
42
- const body = idx === -1 ? keep : keep.slice(idx);
43
- fs.writeFileSync(file, `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`, "utf8");
44
- }
45
-
46
- function appendBlock(file, block) {
47
- ensureDraftFile(file);
48
- fs.appendFileSync(file, block, "utf8");
49
- trimFile(file);
50
- }
51
-
52
- async function readStdin() {
53
- const chunks = [];
54
- for await (const c of process.stdin) chunks.push(c);
55
- return Buffer.concat(chunks).toString("utf8");
56
- }
57
-
58
- function main() {
59
- const agentStop = process.argv.includes("--agent-stop");
60
-
61
- readStdin()
62
- .then((raw) => {
63
- let data = {};
64
- try {
65
- data = raw.trim() ? JSON.parse(raw) : {};
66
- } catch (e) {
67
- console.error("[inferno-session-draft] stdin JSON parse:", e.message);
68
- console.log("{}");
69
- process.exit(0);
70
- return;
71
- }
72
-
73
- const file = draftPath();
74
- if (agentStop) {
75
- const status = data.status ?? "unknown";
76
- const loop = data.loop_count ?? 0;
77
- appendBlock(
78
- file,
79
- `\n### _agent stop_ (${new Date().toISOString()})\n\nstatus: \`${status}\` · loop_count: ${loop}\n\n---\n`
80
- );
81
- console.log("{}");
82
- process.exit(0);
83
- return;
84
- }
85
-
86
- const text = typeof data.text === "string" ? data.text : "";
87
- if (!text.trim()) {
88
- console.log("{}");
89
- process.exit(0);
90
- return;
91
- }
92
-
93
- const clipped =
94
- text.length > MAX_MESSAGE_CHARS
95
- ? `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n_…trimmed (${text.length - MAX_MESSAGE_CHARS} chars omitted)_\n`
96
- : text;
97
-
98
- appendBlock(
99
- file,
100
- `\n### Assistant message (${new Date().toISOString()})\n\n${clipped}\n\n---\n`
101
- );
102
- console.log("{}");
103
- process.exit(0);
104
- })
105
- .catch((e) => {
106
- console.error("[inferno-session-draft]", e);
107
- console.log("{}");
108
- process.exit(0);
109
- });
110
- }
111
-
112
- main();
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();