infernoflow 0.44.13 → 0.44.14

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.
@@ -1,337 +1,285 @@
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
- import { spawnSync } from "node:child_process";
20
-
21
- /** Keep in sync with templates/scripts/inferno-promote-draft.mjs */
22
- const DRAFT_HEADER = `# CONTEXT draft (gitignored)
23
- 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\`.
24
- ---
25
- `;
26
-
27
- const MAX_MESSAGE_CHARS = 120_000;
28
- const MAX_FILE_BYTES = 280_000;
29
-
30
- // ── paths ──────────────────────────────────────────────────────────────────
31
-
32
- function projectRoot() {
33
- return process.cwd();
34
- }
35
-
36
- function draftPath() {
37
- return path.join(projectRoot(), "inferno", "CONTEXT.draft.md");
38
- }
39
-
40
- function agentPromptPath() {
41
- return path.join(projectRoot(), "inferno", "agent-prompt.md");
42
- }
43
-
44
- function agentResponsePath() {
45
- return path.join(projectRoot(), "inferno", "agent-response.json");
46
- }
47
-
48
- // ── CONTEXT.draft.md helpers ───────────────────────────────────────────────
49
-
50
- function ensureDraftFile(file) {
51
- if (!fs.existsSync(file)) {
52
- fs.mkdirSync(path.dirname(file), { recursive: true });
53
- fs.writeFileSync(file, DRAFT_HEADER, "utf8");
54
- }
55
- }
56
-
57
- function trimFile(file) {
58
- const raw = fs.readFileSync(file, "utf8");
59
- if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
60
- const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
61
- const idx = keep.indexOf("\n### ");
62
- const body = idx === -1 ? keep : keep.slice(idx);
63
- fs.writeFileSync(
64
- file,
65
- `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`,
66
- "utf8",
67
- );
68
- }
69
-
70
- function appendBlock(file, block) {
71
- ensureDraftFile(file);
72
- fs.appendFileSync(file, block, "utf8");
73
- trimFile(file);
74
- }
75
-
76
- // ── JSON extraction ────────────────────────────────────────────────────────
77
-
78
- function extractJsonFromText(text) {
79
- // 1. fenced code block
80
- const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
81
- if (fenceMatch) {
82
- const candidate = fenceMatch[1].trim();
83
- try {
84
- JSON.parse(candidate);
85
- return candidate;
86
- } catch {}
87
- }
88
-
89
- // 2. largest bare JSON object in the text
90
- const jsonMatch = text.match(/\{[\s\S]*\}/);
91
- if (jsonMatch) {
92
- try {
93
- JSON.parse(jsonMatch[0]);
94
- return jsonMatch[0];
95
- } catch {}
96
- }
97
-
98
- return null;
99
- }
100
-
101
- // ── bridge: write agent-response.json if infernoflow is waiting ────────────
102
-
103
- function maybeWriteAgentResponse(text) {
104
- const promptFile = agentPromptPath();
105
- const responseFile = agentResponsePath();
106
-
107
- if (!fs.existsSync(promptFile)) return false;
108
-
109
- const json = extractJsonFromText(text);
110
- if (!json) {
111
- process.stderr.write(
112
- "[inferno-session-draft] infernoflow waiting but no JSON found in agent reply\n",
113
- );
114
- return false;
115
- }
116
-
117
- fs.writeFileSync(responseFile, json, "utf8");
118
- try {
119
- fs.unlinkSync(promptFile);
120
- } catch {}
121
-
122
- process.stderr.write(
123
- "[inferno-session-draft] ✔ agent-response.json written — infernoflow will continue\n",
124
- );
125
- return true;
126
- }
127
-
128
- // ── stdin ──────────────────────────────────────────────────────────────────
129
-
130
- async function readStdin() {
131
- const chunks = [];
132
- for await (const c of process.stdin) chunks.push(c);
133
- return Buffer.concat(chunks).toString("utf8");
134
- }
135
-
136
- // ── Deterministic trigger capture (beforeSubmitPrompt) ──────────────────────
137
- // Cursor's beforeSubmitPrompt hook hands us the USER's prompt text before it
138
- // goes to the model. The Memory-protocol block already asks the AI to log on
139
- // these signals, but the AI doesn't always obey — so this is a deterministic
140
- // backstop: if the prompt itself contains a trouble signal (!!, retry, "not
141
- // working", …) we write an `attempt` entry ourselves. Bounded hard against
142
- // noise: a 90s cooldown + identical-prompt dedupe, so a frustrated burst of
143
- // "still broken!! retry!!" produces ONE entry, not ten.
144
- const TRIGGER_RES = [
145
- /(?:^|\s)!!+/,
146
- /\bretry(?:ing)?\b/i,
147
- /\bnot working\b/i,
148
- /\bstill (?:broken|failing|not working|doesn['’]?t)\b/i,
149
- /\bsame (?:error|issue|problem)\b/i,
150
- /\bno change\b/i,
151
- /\bdoesn['’]?t work\b/i,
152
- ];
153
-
154
- // Deterministic BOOKMARK triggers — an explicit "bookmark this" is an intentional
155
- // resume point (not a trouble signal), so it takes precedence and drops a real
156
- // bookmark (which auto-captures the session transcript). No AI cooperation needed.
157
- const BOOKMARK_RES = [
158
- /\bbookmark (?:this|it|here)(?: point)?\b/i,
159
- /\bmark this (?:point|spot|moment|here)\b/i,
160
- /\bsave (?:this )?(?:point|checkpoint|resume point)\b/i,
161
- ];
162
-
163
- /** Strip the trigger phrase to reuse the rest of the prompt as the bookmark label. */
164
- function deriveBookmarkLabel(prompt) {
165
- const label = prompt
166
- .replace(/\bbookmark (?:this|it|here)(?: point)?\b/ig, "")
167
- .replace(/\bmark this (?:point|spot|moment|here)\b/ig, "")
168
- .replace(/\bsave (?:this )?(?:point|checkpoint|resume point)\b/ig, "")
169
- .replace(/[\s:.!,–—-]+/g, " ")
170
- .replace(/\s+/g, " ")
171
- .trim();
172
- return (label.slice(0, 80).trim()) || "Session bookmark";
173
- }
174
-
175
- function memoryRootExists() {
176
- return fs.existsSync(path.join(projectRoot(), ".ai-memory")) ||
177
- fs.existsSync(path.join(projectRoot(), "inferno"));
178
- }
179
-
180
- function triggerStatePath() {
181
- return path.join(projectRoot(), ".ai-memory", ".trigger-state.json");
182
- }
183
-
184
- function cheapHash(s) {
185
- let h = 0;
186
- for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
187
- return String(h);
188
- }
189
-
190
- function handleBookmarkTrigger(prompt) {
191
- const now = Date.now();
192
- const stateFile = triggerStatePath();
193
- let state = {};
194
- try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
195
- const h = cheapHash(prompt.slice(0, 200));
196
- if (state.lastBmHash === h) return; // same prompt — don't double-fire
197
-
198
- const label = deriveBookmarkLabel(prompt);
199
- let wrote = false;
200
- try {
201
- // The `bookmark` command (no --marker) auto-captures the session transcript
202
- // as the resume point. Needs infernoflow >= 0.44.10 on PATH; if the global
203
- // CLI is older / missing, this no-ops and the AI's amp_bookmark path covers it.
204
- const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
205
- const r = spawnSync(bin, ["bookmark", label], {
206
- cwd: projectRoot(), encoding: "utf8", timeout: 12000, shell: process.platform === "win32",
207
- });
208
- wrote = r.status === 0;
209
- } catch { /* CLI unavailable — skip */ }
210
-
211
- if (wrote) {
212
- try { fs.writeFileSync(stateFile, JSON.stringify({ ...state, lastBmHash: h }), "utf8"); } catch {}
213
- process.stderr.write("[inferno-session-draft] auto-bookmarked resume point\n");
214
- }
215
- }
216
-
217
- function handleUserPrompt(text) {
218
- const trimmed = (text || "").trim();
219
- if (!trimmed) return;
220
- if (!memoryRootExists()) return; // only inside infernoflow projects
221
-
222
- // Bookmark trigger takes precedence — "bookmark this" is intentional, not trouble.
223
- if (BOOKMARK_RES.some((re) => re.test(trimmed))) { handleBookmarkTrigger(trimmed); return; }
224
-
225
- if (!TRIGGER_RES.some((re) => re.test(trimmed))) return;
226
-
227
- const now = Date.now();
228
- const stateFile = triggerStatePath();
229
- let state = {};
230
- try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
231
- const h = cheapHash(trimmed.slice(0, 200));
232
- const COOLDOWN_MS = 90_000;
233
- if (state.lastHash === h) return; // exact same prompt — skip
234
- if (state.lastTs && now - state.lastTs < COOLDOWN_MS) return; // rate-limit
235
-
236
- const msg = "Auto-trigger — user signalled trouble: " +
237
- trimmed.replace(/\s+/g, " ").slice(0, 180);
238
-
239
- // Prefer the CLI (correct id / branch routing / AMP shape); fall back to a
240
- // direct sessions.jsonl append so capture still works without a global CLI.
241
- let wrote = false;
242
- try {
243
- const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
244
- const r = spawnSync(bin, ["log", msg, "--type", "attempt", "--source", "cursor-trigger", "--tags", "auto-trigger"], {
245
- cwd: projectRoot(), encoding: "utf8", timeout: 8000, shell: process.platform === "win32",
246
- });
247
- wrote = r.status === 0;
248
- } catch { /* fall through to direct write */ }
249
- if (!wrote) {
250
- try {
251
- const sess = path.join(projectRoot(), ".ai-memory", "sessions.jsonl");
252
- fs.mkdirSync(path.dirname(sess), { recursive: true });
253
- const entry = { type: "attempt", msg, ts: now, id: "amp_hook_" + now.toString(36), source: "cursor-trigger", tags: ["auto-trigger"], meta: { agent: "cursor-hook" } };
254
- fs.appendFileSync(sess, JSON.stringify(entry) + "\n", "utf8");
255
- wrote = true;
256
- } catch { /* best effort */ }
257
- }
258
-
259
- if (wrote) {
260
- try { fs.writeFileSync(stateFile, JSON.stringify({ ...state, lastTs: now, lastHash: h }), "utf8"); } catch {}
261
- process.stderr.write("[inferno-session-draft] auto-captured trigger to memory\n");
262
- }
263
- }
264
-
265
- // ── main ───────────────────────────────────────────────────────────────────
266
-
267
- function main() {
268
- const agentStop = process.argv.includes("--agent-stop");
269
-
270
- readStdin()
271
- .then((raw) => {
272
- let data = {};
273
- try {
274
- data = raw.trim() ? JSON.parse(raw) : {};
275
- } catch (e) {
276
- console.error("[inferno-session-draft] stdin JSON parse:", e.message);
277
- console.log("{}");
278
- process.exit(0);
279
- return;
280
- }
281
-
282
- // beforeSubmitPrompt: deterministic trigger capture on the USER's prompt.
283
- if (process.argv.includes("--user-prompt")) {
284
- const t = typeof data.prompt === "string" ? data.prompt
285
- : typeof data.text === "string" ? data.text : "";
286
- try { handleUserPrompt(t); } catch (e) { console.error("[inferno-session-draft] trigger:", e?.message); }
287
- console.log("{}");
288
- process.exit(0);
289
- return;
290
- }
291
-
292
- const file = draftPath();
293
-
294
- if (agentStop) {
295
- const status = data.status ?? "unknown";
296
- const loop = data.loop_count ?? 0;
297
- appendBlock(
298
- file,
299
- `\n### _agent stop_ (${new Date().toISOString()})\n\nstatus: \`${status}\` · loop_count: ${loop}\n\n---\n`,
300
- );
301
- console.log("{}");
302
- process.exit(0);
303
- return;
304
- }
305
-
306
- const text = typeof data.text === "string" ? data.text : "";
307
- if (!text.trim()) {
308
- console.log("{}");
309
- process.exit(0);
310
- return;
311
- }
312
-
313
- // Job 2: feed infernoflow's file-based bridge if it is waiting
314
- maybeWriteAgentResponse(text);
315
-
316
- // Job 1: always append to CONTEXT.draft.md
317
- const clipped =
318
- text.length > MAX_MESSAGE_CHARS
319
- ? `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n_…trimmed (${text.length - MAX_MESSAGE_CHARS} chars omitted)_\n`
320
- : text;
321
-
322
- appendBlock(
323
- file,
324
- `\n### Assistant message (${new Date().toISOString()})\n\n${clipped}\n\n---\n`,
325
- );
326
-
327
- console.log("{}");
328
- process.exit(0);
329
- })
330
- .catch((e) => {
331
- console.error("[inferno-session-draft]", e);
332
- console.log("{}");
333
- process.exit(0);
334
- });
335
- }
336
-
337
- 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
+ import { spawnSync } from "node:child_process";
20
+
21
+ /** Keep in sync with templates/scripts/inferno-promote-draft.mjs */
22
+ const DRAFT_HEADER = `# CONTEXT draft (gitignored)
23
+ 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\`.
24
+ ---
25
+ `;
26
+
27
+ const MAX_MESSAGE_CHARS = 120_000;
28
+ const MAX_FILE_BYTES = 280_000;
29
+
30
+ // ── paths ──────────────────────────────────────────────────────────────────
31
+
32
+ function projectRoot() {
33
+ return process.cwd();
34
+ }
35
+
36
+ function draftPath() {
37
+ return path.join(projectRoot(), "inferno", "CONTEXT.draft.md");
38
+ }
39
+
40
+ function agentPromptPath() {
41
+ return path.join(projectRoot(), "inferno", "agent-prompt.md");
42
+ }
43
+
44
+ function agentResponsePath() {
45
+ return path.join(projectRoot(), "inferno", "agent-response.json");
46
+ }
47
+
48
+ // ── CONTEXT.draft.md helpers ───────────────────────────────────────────────
49
+
50
+ function ensureDraftFile(file) {
51
+ if (!fs.existsSync(file)) {
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ fs.writeFileSync(file, DRAFT_HEADER, "utf8");
54
+ }
55
+ }
56
+
57
+ function trimFile(file) {
58
+ const raw = fs.readFileSync(file, "utf8");
59
+ if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
60
+ const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
61
+ const idx = keep.indexOf("\n### ");
62
+ const body = idx === -1 ? keep : keep.slice(idx);
63
+ fs.writeFileSync(
64
+ file,
65
+ `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`,
66
+ "utf8",
67
+ );
68
+ }
69
+
70
+ function appendBlock(file, block) {
71
+ ensureDraftFile(file);
72
+ fs.appendFileSync(file, block, "utf8");
73
+ trimFile(file);
74
+ }
75
+
76
+ // ── JSON extraction ────────────────────────────────────────────────────────
77
+
78
+ function extractJsonFromText(text) {
79
+ // 1. fenced code block
80
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
81
+ if (fenceMatch) {
82
+ const candidate = fenceMatch[1].trim();
83
+ try {
84
+ JSON.parse(candidate);
85
+ return candidate;
86
+ } catch {}
87
+ }
88
+
89
+ // 2. largest bare JSON object in the text
90
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
91
+ if (jsonMatch) {
92
+ try {
93
+ JSON.parse(jsonMatch[0]);
94
+ return jsonMatch[0];
95
+ } catch {}
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ // ── bridge: write agent-response.json if infernoflow is waiting ────────────
102
+
103
+ function maybeWriteAgentResponse(text) {
104
+ const promptFile = agentPromptPath();
105
+ const responseFile = agentResponsePath();
106
+
107
+ if (!fs.existsSync(promptFile)) return false;
108
+
109
+ const json = extractJsonFromText(text);
110
+ if (!json) {
111
+ process.stderr.write(
112
+ "[inferno-session-draft] infernoflow waiting but no JSON found in agent reply\n",
113
+ );
114
+ return false;
115
+ }
116
+
117
+ fs.writeFileSync(responseFile, json, "utf8");
118
+ try {
119
+ fs.unlinkSync(promptFile);
120
+ } catch {}
121
+
122
+ process.stderr.write(
123
+ "[inferno-session-draft] ✔ agent-response.json written — infernoflow will continue\n",
124
+ );
125
+ return true;
126
+ }
127
+
128
+ // ── stdin ──────────────────────────────────────────────────────────────────
129
+
130
+ async function readStdin() {
131
+ const chunks = [];
132
+ for await (const c of process.stdin) chunks.push(c);
133
+ return Buffer.concat(chunks).toString("utf8");
134
+ }
135
+
136
+ // ── Deterministic trigger capture (beforeSubmitPrompt) ──────────────────────
137
+ // Cursor's beforeSubmitPrompt hook hands us the USER's prompt text before it
138
+ // goes to the model. The Memory-protocol block already asks the AI to log on
139
+ // these signals, but the AI doesn't always obey — so this is a deterministic
140
+ // backstop: if the prompt itself contains a trouble signal (!!, retry, "not
141
+ // working", …) we write an `attempt` entry ourselves. Bounded hard against
142
+ // noise: a 90s cooldown + identical-prompt dedupe, so a frustrated burst of
143
+ // "still broken!! retry!!" produces ONE entry, not ten.
144
+ const TRIGGER_RES = [
145
+ /(?:^|\s)!!+/,
146
+ /\bretry(?:ing)?\b/i,
147
+ /\bnot working\b/i,
148
+ /\bstill (?:broken|failing|not working|doesn['’]?t)\b/i,
149
+ /\bsame (?:error|issue|problem)\b/i,
150
+ /\bno change\b/i,
151
+ /\bdoesn['’]?t work\b/i,
152
+ ];
153
+
154
+ function memoryRootExists() {
155
+ return fs.existsSync(path.join(projectRoot(), ".ai-memory")) ||
156
+ fs.existsSync(path.join(projectRoot(), "inferno"));
157
+ }
158
+
159
+ function triggerStatePath() {
160
+ return path.join(projectRoot(), ".ai-memory", ".trigger-state.json");
161
+ }
162
+
163
+ function cheapHash(s) {
164
+ let h = 0;
165
+ for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
166
+ return String(h);
167
+ }
168
+
169
+ function handleUserPrompt(text) {
170
+ const trimmed = (text || "").trim();
171
+ if (!trimmed) return;
172
+ if (!memoryRootExists()) return; // only inside infernoflow projects
173
+ if (!TRIGGER_RES.some((re) => re.test(trimmed))) return;
174
+
175
+ const now = Date.now();
176
+ const stateFile = triggerStatePath();
177
+ let state = {};
178
+ try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
179
+ const h = cheapHash(trimmed.slice(0, 200));
180
+ const COOLDOWN_MS = 90_000;
181
+ if (state.lastHash === h) return; // exact same prompt — skip
182
+ if (state.lastTs && now - state.lastTs < COOLDOWN_MS) return; // rate-limit
183
+
184
+ const msg = "Auto-trigger — user signalled trouble: " +
185
+ trimmed.replace(/\s+/g, " ").slice(0, 180);
186
+
187
+ // Prefer the CLI (correct id / branch routing / AMP shape); fall back to a
188
+ // direct sessions.jsonl append so capture still works without a global CLI.
189
+ let wrote = false;
190
+ try {
191
+ const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
192
+ const r = spawnSync(bin, ["log", msg, "--type", "attempt", "--source", "cursor-trigger", "--tags", "auto-trigger"], {
193
+ cwd: projectRoot(), encoding: "utf8", timeout: 8000, shell: process.platform === "win32",
194
+ });
195
+ wrote = r.status === 0;
196
+ } catch { /* fall through to direct write */ }
197
+ if (!wrote) {
198
+ try {
199
+ const sess = path.join(projectRoot(), ".ai-memory", "sessions.jsonl");
200
+ fs.mkdirSync(path.dirname(sess), { recursive: true });
201
+ const entry = { type: "attempt", msg, ts: now, id: "amp_hook_" + now.toString(36), source: "cursor-trigger", tags: ["auto-trigger"], meta: { agent: "cursor-hook" } };
202
+ fs.appendFileSync(sess, JSON.stringify(entry) + "\n", "utf8");
203
+ wrote = true;
204
+ } catch { /* best effort */ }
205
+ }
206
+
207
+ if (wrote) {
208
+ try { fs.writeFileSync(stateFile, JSON.stringify({ lastTs: now, lastHash: h }), "utf8"); } catch {}
209
+ process.stderr.write("[inferno-session-draft] auto-captured trigger to memory\n");
210
+ }
211
+ }
212
+
213
+ // ── main ───────────────────────────────────────────────────────────────────
214
+
215
+ function main() {
216
+ const agentStop = process.argv.includes("--agent-stop");
217
+
218
+ readStdin()
219
+ .then((raw) => {
220
+ let data = {};
221
+ try {
222
+ data = raw.trim() ? JSON.parse(raw) : {};
223
+ } catch (e) {
224
+ console.error("[inferno-session-draft] stdin JSON parse:", e.message);
225
+ console.log("{}");
226
+ process.exit(0);
227
+ return;
228
+ }
229
+
230
+ // beforeSubmitPrompt: deterministic trigger capture on the USER's prompt.
231
+ if (process.argv.includes("--user-prompt")) {
232
+ const t = typeof data.prompt === "string" ? data.prompt
233
+ : typeof data.text === "string" ? data.text : "";
234
+ try { handleUserPrompt(t); } catch (e) { console.error("[inferno-session-draft] trigger:", e?.message); }
235
+ console.log("{}");
236
+ process.exit(0);
237
+ return;
238
+ }
239
+
240
+ const file = draftPath();
241
+
242
+ if (agentStop) {
243
+ const status = data.status ?? "unknown";
244
+ const loop = data.loop_count ?? 0;
245
+ appendBlock(
246
+ file,
247
+ `\n### _agent stop_ (${new Date().toISOString()})\n\nstatus: \`${status}\` · loop_count: ${loop}\n\n---\n`,
248
+ );
249
+ console.log("{}");
250
+ process.exit(0);
251
+ return;
252
+ }
253
+
254
+ const text = typeof data.text === "string" ? data.text : "";
255
+ if (!text.trim()) {
256
+ console.log("{}");
257
+ process.exit(0);
258
+ return;
259
+ }
260
+
261
+ // Job 2: feed infernoflow's file-based bridge if it is waiting
262
+ maybeWriteAgentResponse(text);
263
+
264
+ // Job 1: always append to CONTEXT.draft.md
265
+ const clipped =
266
+ text.length > MAX_MESSAGE_CHARS
267
+ ? `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n_…trimmed (${text.length - MAX_MESSAGE_CHARS} chars omitted)_\n`
268
+ : text;
269
+
270
+ appendBlock(
271
+ file,
272
+ `\n### Assistant message (${new Date().toISOString()})\n\n${clipped}\n\n---\n`,
273
+ );
274
+
275
+ console.log("{}");
276
+ process.exit(0);
277
+ })
278
+ .catch((e) => {
279
+ console.error("[inferno-session-draft]", e);
280
+ console.log("{}");
281
+ process.exit(0);
282
+ });
283
+ }
284
+
285
+ main();