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.
- package/LICENSE +21 -0
- package/README.md +20 -12
- package/dist/bin/infernoflow.mjs +5 -3
- package/dist/lib/amp/io.mjs +13 -13
- package/dist/lib/commands/ask.mjs +3 -3
- package/dist/lib/commands/doctor.mjs +2 -2
- package/dist/lib/commands/log.mjs +14 -14
- package/dist/lib/commands/refresh.mjs +4 -5
- package/dist/lib/commands/setup.mjs +35 -6
- package/dist/lib/commands/switch.mjs +10 -10
- package/dist/lib/projectRoot.mjs +1 -1
- package/dist/lib/ruleFiles.mjs +10 -11
- package/dist/lib/ui/output.mjs +2 -2
- package/dist/templates/cursor/hooks/inferno-session-draft.mjs +285 -337
- package/dist/templates/cursor/inferno-mcp-server.mjs +2 -46
- package/package.json +69 -69
- package/dist/lib/commands/bookmark.mjs +0 -28
- package/dist/lib/transcript.mjs +0 -4
|
@@ -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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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();
|