infernoflow 0.42.0 → 0.42.2
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 +33 -9
- package/README.md +43 -1
- package/dist/bin/infernoflow.mjs +22 -21
- package/dist/lib/commands/amp.mjs +4 -0
- package/dist/templates/cursor/inferno-mcp-server.mjs +516 -470
- package/package.json +1 -1
|
@@ -1,471 +1,517 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import readline from "node:readline";
|
|
5
|
-
|
|
6
|
-
function send(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); }
|
|
7
|
-
function sendResult(id, result) { send({ jsonrpc: "2.0", id, result }); }
|
|
8
|
-
function sendError(id, code, message) { send({ jsonrpc: "2.0", id, error: { code, message } }); }
|
|
9
|
-
|
|
10
|
-
function runCmd(args, env = {}) {
|
|
11
|
-
try { return execSync(`npx infernoflow ${args}`, { encoding: "utf8", cwd: process.cwd(), timeout: 30000, env: { ...process.env, ...env } }); }
|
|
12
|
-
catch (err) { return err.stdout || err.message; }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const TOOLS = [
|
|
16
|
-
{ name: "infernoflow_run", description: "Generate an infernoflow task prompt. Returns the prompt — respond to it with JSON, then call infernoflow_apply.", inputSchema: { type: "object", properties: { task: { type: "string", description: "What to build" } }, required: ["task"] } },
|
|
17
|
-
{ name: "infernoflow_apply", description: "Apply an infernoflow suggestion JSON returned by the agent. Call this after responding to infernoflow_run.", inputSchema: { type: "object", properties: { json: { type: "string", description: "The JSON suggestion from the agent" } }, required: ["json"] } },
|
|
18
|
-
{ name: "infernoflow_check", description: "Validate infernoflow contract and capabilities", inputSchema: { type: "object", properties: {} } },
|
|
19
|
-
{ name: "infernoflow_status", description: "Show contract health at a glance", inputSchema: { type: "object", properties: {} } },
|
|
20
|
-
{ name: "infernoflow_context", description: "Generate AI-ready context", inputSchema: { type: "object", properties: { intent: { type: "string" }, working: { type: "string" } } } },
|
|
21
|
-
{ name: "infernoflow_git_drift", description: "Detect which capabilities may be affected by recent code changes. Compares git-changed files to the capability registry and returns suggestions for contract updates.", inputSchema: { type: "object", properties: { sinceCommits: { type: "number", description: "How many commits back to check (default: 1)" } } } },
|
|
22
|
-
{ name: "infernoflow_implement", description: "Generate a structured code implementation prompt for a task. Uses the contract and stack context to produce step-by-step coding instructions for the agent.", inputSchema: { type: "object", properties: { task: { type: "string", description: "What to implement" }, mode: { type: "string", enum: ["cursor", "generic", "both"], description: "Prompt style (default: both)" } }, required: ["task"] } },
|
|
23
|
-
{ name: "infernoflow_scan_ui", description: "Scan components and styles for UI changes vs the stored contract. Returns new/changed components, design token changes, and suggested contract updates.", inputSchema: { type: "object", properties: {} } },
|
|
24
|
-
{ name: "infernoflow_review", description: "Pre-merge capability drift check. Compares all changed files in the current branch against the capability contract and reports drift risk before you merge.", inputSchema: { type: "object", properties: { branch: { type: "string", description: "Branch to compare against (default: main)" } } } },
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
lines.push("
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
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
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (removedComponents.length) {
|
|
204
|
-
lines.push(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
lines.push(`### New
|
|
210
|
-
|
|
211
|
-
lines.push("");
|
|
212
|
-
}
|
|
213
|
-
if (
|
|
214
|
-
lines.push(`### Removed
|
|
215
|
-
|
|
216
|
-
lines.push("");
|
|
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
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
if (!
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
{ kw: ["
|
|
265
|
-
{ kw: ["
|
|
266
|
-
{ kw: ["
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
lines.push("");
|
|
315
|
-
lines.push(
|
|
316
|
-
lines.push("");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
lines.push(`###
|
|
321
|
-
for (const [id, files] of
|
|
322
|
-
lines.push(` - **${id}** — ${files.slice(0,2).join(", ")}`);
|
|
323
|
-
}
|
|
324
|
-
lines.push("");
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
lines.push(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
lines.push("
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
###
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
${
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
{
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
|
|
6
|
+
function send(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); }
|
|
7
|
+
function sendResult(id, result) { send({ jsonrpc: "2.0", id, result }); }
|
|
8
|
+
function sendError(id, code, message) { send({ jsonrpc: "2.0", id, error: { code, message } }); }
|
|
9
|
+
|
|
10
|
+
function runCmd(args, env = {}) {
|
|
11
|
+
try { return execSync(`npx infernoflow ${args}`, { encoding: "utf8", cwd: process.cwd(), timeout: 30000, env: { ...process.env, ...env } }); }
|
|
12
|
+
catch (err) { return err.stdout || err.message; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TOOLS = [
|
|
16
|
+
{ name: "infernoflow_run", description: "Generate an infernoflow task prompt. Returns the prompt — respond to it with JSON, then call infernoflow_apply.", inputSchema: { type: "object", properties: { task: { type: "string", description: "What to build" } }, required: ["task"] } },
|
|
17
|
+
{ name: "infernoflow_apply", description: "Apply an infernoflow suggestion JSON returned by the agent. Call this after responding to infernoflow_run.", inputSchema: { type: "object", properties: { json: { type: "string", description: "The JSON suggestion from the agent" } }, required: ["json"] } },
|
|
18
|
+
{ name: "infernoflow_check", description: "Validate infernoflow contract and capabilities", inputSchema: { type: "object", properties: {} } },
|
|
19
|
+
{ name: "infernoflow_status", description: "Show contract health at a glance", inputSchema: { type: "object", properties: {} } },
|
|
20
|
+
{ name: "infernoflow_context", description: "Generate AI-ready context", inputSchema: { type: "object", properties: { intent: { type: "string" }, working: { type: "string" } } } },
|
|
21
|
+
{ name: "infernoflow_git_drift", description: "Detect which capabilities may be affected by recent code changes. Compares git-changed files to the capability registry and returns suggestions for contract updates.", inputSchema: { type: "object", properties: { sinceCommits: { type: "number", description: "How many commits back to check (default: 1)" } } } },
|
|
22
|
+
{ name: "infernoflow_implement", description: "Generate a structured code implementation prompt for a task. Uses the contract and stack context to produce step-by-step coding instructions for the agent.", inputSchema: { type: "object", properties: { task: { type: "string", description: "What to implement" }, mode: { type: "string", enum: ["cursor", "generic", "both"], description: "Prompt style (default: both)" } }, required: ["task"] } },
|
|
23
|
+
{ name: "infernoflow_scan_ui", description: "Scan components and styles for UI changes vs the stored contract. Returns new/changed components, design token changes, and suggested contract updates.", inputSchema: { type: "object", properties: {} } },
|
|
24
|
+
{ name: "infernoflow_review", description: "Pre-merge capability drift check. Compares all changed files in the current branch against the capability contract and reports drift risk before you merge.", inputSchema: { type: "object", properties: { branch: { type: "string", description: "Branch to compare against (default: main)" } } } },
|
|
25
|
+
|
|
26
|
+
// ── AMP-spec MCP tools (per docs/protocol/PROTOCOL.md §7.3) ────────────────
|
|
27
|
+
// These are the standard names any AMP-compliant MCP server should expose.
|
|
28
|
+
// They're thin wrappers around the existing infernoflow_* tools so AMP-only
|
|
29
|
+
// clients don't need to know the infernoflow_ vendor prefix.
|
|
30
|
+
{ name: "amp_read", description: "AMP: read session memory entries with optional filters.", inputSchema: { type: "object", properties: { file: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, query: { type: "string" }, limit: { type: "number" } } } },
|
|
31
|
+
{ name: "amp_write", description: "AMP: log a new entry. Required: type + msg. Optional: file, line, tags.", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, msg: { type: "string" }, file: { type: "string" }, line: { type: "number" }, tags: { type: "array", items: { type: "string" } } }, required: ["type","msg"] } },
|
|
32
|
+
{ name: "amp_handoff", description: "AMP: generate the handoff document for the next AI session. format=markdown|json (default: markdown).", inputSchema: { type: "object", properties: { format: { type: "string", enum: ["markdown","json"] } } } },
|
|
33
|
+
{ name: "amp_search", description: "AMP: search entries by keyword. Optional type filter.", inputSchema: { type: "object", properties: { query: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] } }, required: ["query"] } },
|
|
34
|
+
{ name: "amp_health", description: "AMP: get the session health score (0-100, A-F grade).", inputSchema: { type: "object", properties: {} } },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ── git drift detection (inline — no external imports in this template file) ─
|
|
38
|
+
function detectGitDrift(sinceCommits) {
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
41
|
+
|
|
42
|
+
const runGit = (cmd) => {
|
|
43
|
+
try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 10_000 }); }
|
|
44
|
+
catch { return ""; }
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const changedSet = new Set();
|
|
48
|
+
const addLines = (out) => out.split("\n").map(l => l.trim()).filter(Boolean).forEach(f => changedSet.add(f));
|
|
49
|
+
|
|
50
|
+
addLines(runGit("git diff --name-only HEAD"));
|
|
51
|
+
addLines(runGit(`git diff --name-only HEAD~${sinceCommits} HEAD`));
|
|
52
|
+
addLines(runGit("git ls-files --others --exclude-standard"));
|
|
53
|
+
|
|
54
|
+
const changedFiles = Array.from(changedSet).sort();
|
|
55
|
+
if (!changedFiles.length) return "No changed files detected since last commit.";
|
|
56
|
+
|
|
57
|
+
// Load capabilities registry
|
|
58
|
+
let capabilities = [];
|
|
59
|
+
try {
|
|
60
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
61
|
+
if (fs.existsSync(capsPath)) capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")).capabilities || [];
|
|
62
|
+
} catch {}
|
|
63
|
+
|
|
64
|
+
// Load capability-map if present
|
|
65
|
+
let capMap = null;
|
|
66
|
+
try {
|
|
67
|
+
const mapPath = path.join(infernoDir, "capability-map.json");
|
|
68
|
+
if (fs.existsSync(mapPath)) capMap = JSON.parse(fs.readFileSync(mapPath, "utf8"));
|
|
69
|
+
} catch {}
|
|
70
|
+
|
|
71
|
+
const capHits = new Map();
|
|
72
|
+
const mappedFiles = new Set();
|
|
73
|
+
|
|
74
|
+
const addHit = (capId, capTitle, file) => {
|
|
75
|
+
if (!capHits.has(capId)) capHits.set(capId, { id: capId, title: capTitle || capId, files: new Set() });
|
|
76
|
+
capHits.get(capId).files.add(file);
|
|
77
|
+
mappedFiles.add(file);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Strategy 1: capability-map.json
|
|
81
|
+
if (capMap) {
|
|
82
|
+
for (const file of changedFiles) {
|
|
83
|
+
for (const [prefix, capIds] of Object.entries(capMap)) {
|
|
84
|
+
if (file.startsWith(prefix.replace(/\\/g, "/"))) {
|
|
85
|
+
for (const capId of capIds) {
|
|
86
|
+
const cap = capabilities.find(c => c.id === capId);
|
|
87
|
+
addHit(capId, cap?.title, file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Strategy 2: heuristic keyword matching on filename
|
|
95
|
+
const RULES = [
|
|
96
|
+
{ kw: ["search"], id: "SearchItems" }, { kw: ["filter"], id: "FilterItems" },
|
|
97
|
+
{ kw: ["auth", "login", "logout"], id: "Authentication" },
|
|
98
|
+
{ kw: ["create", "add", "new"], id: "CreateItem" },
|
|
99
|
+
{ kw: ["update", "edit"], id: "UpdateItem" },
|
|
100
|
+
{ kw: ["delete", "remove"], id: "DeleteItem" },
|
|
101
|
+
{ kw: ["list", "read", "view"], id: "ReadItems" },
|
|
102
|
+
{ kw: ["due", "deadline"], id: "SetDueDate" },
|
|
103
|
+
{ kw: ["priority"], id: "SetPriority" },
|
|
104
|
+
{ kw: ["complete", "toggle"], id: "ToggleComplete" },
|
|
105
|
+
];
|
|
106
|
+
for (const file of changedFiles) {
|
|
107
|
+
if (mappedFiles.has(file)) continue;
|
|
108
|
+
const lower = file.toLowerCase();
|
|
109
|
+
for (const rule of RULES) {
|
|
110
|
+
if (rule.kw.some(k => lower.includes(k))) {
|
|
111
|
+
const cap = capabilities.find(c => c.id === rule.id);
|
|
112
|
+
addHit(rule.id, cap?.title, file);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const unmapped = changedFiles.filter(f => !mappedFiles.has(f));
|
|
119
|
+
const affected = Array.from(capHits.values());
|
|
120
|
+
|
|
121
|
+
// Format output
|
|
122
|
+
const lines = [
|
|
123
|
+
`## infernoflow git drift report`,
|
|
124
|
+
`Changed files: ${changedFiles.length}`,
|
|
125
|
+
`Affected capabilities: ${affected.length}`,
|
|
126
|
+
"",
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
if (affected.length) {
|
|
130
|
+
lines.push("### Capabilities likely needing contract review:");
|
|
131
|
+
for (const cap of affected) {
|
|
132
|
+
lines.push(`\n**${cap.id}** — ${cap.title}`);
|
|
133
|
+
for (const f of cap.files) lines.push(` - ${f}`);
|
|
134
|
+
}
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("### Suggested action:");
|
|
137
|
+
lines.push(`Call infernoflow_run with task "review changes to ${affected.map(c => c.id).join(", ")}" to update the contract.`);
|
|
138
|
+
} else {
|
|
139
|
+
lines.push("No capability matches found for changed files.");
|
|
140
|
+
lines.push("Consider updating inferno/capability-map.json to map your source paths to capabilities.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (unmapped.length) {
|
|
144
|
+
lines.push(`\n### Unmapped changed files (${unmapped.length}):`);
|
|
145
|
+
for (const f of unmapped.slice(0, 10)) lines.push(` - ${f}`);
|
|
146
|
+
if (unmapped.length > 10) lines.push(` ... +${unmapped.length - 10} more`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lines.join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── infernoflow_scan_ui ────────────────────────────────────────────────────
|
|
153
|
+
function scanUi() {
|
|
154
|
+
const cwd = process.cwd();
|
|
155
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
156
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
157
|
+
if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
|
|
158
|
+
|
|
159
|
+
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
160
|
+
const storedUi = contract.ui || {};
|
|
161
|
+
|
|
162
|
+
// Collect style + component files
|
|
163
|
+
const styleExts = /\.(css|scss|sass|less|ts|tsx|js|jsx|html)$/;
|
|
164
|
+
const SKIP = new Set(["node_modules", ".git", "dist", "build", ".angular", ".next", "vendor", "coverage"]);
|
|
165
|
+
const files = [];
|
|
166
|
+
const walk = (dir) => {
|
|
167
|
+
try {
|
|
168
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
169
|
+
const full = path.join(dir, entry.name);
|
|
170
|
+
if (entry.isDirectory()) { if (!SKIP.has(entry.name)) walk(full); }
|
|
171
|
+
else if (styleExts.test(entry.name) && !entry.name.includes(".min.") && !entry.name.endsWith(".map")) files.push(full);
|
|
172
|
+
}
|
|
173
|
+
} catch {}
|
|
174
|
+
};
|
|
175
|
+
for (const root of ["src", "app", "frontend", "components", "styles"]) {
|
|
176
|
+
const p = path.join(cwd, root);
|
|
177
|
+
if (fs.existsSync(p)) walk(p);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Extract current components from TS/TSX files
|
|
181
|
+
const currentComponents = new Set();
|
|
182
|
+
const currentTokens = new Set();
|
|
183
|
+
|
|
184
|
+
for (const f of files) {
|
|
185
|
+
const text = fs.existsSync(f) ? fs.readFileSync(f, "utf8") : "";
|
|
186
|
+
// Components
|
|
187
|
+
for (const m of text.matchAll(/@Component[\s\S]*?class\s+([A-Z][A-Za-z0-9_]*Component)/g)) currentComponents.add(m[1].replace(/Component$/, ""));
|
|
188
|
+
for (const m of text.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)/g)) currentComponents.add(m[1]);
|
|
189
|
+
// Design tokens
|
|
190
|
+
for (const m of text.matchAll(/--([a-zA-Z][a-zA-Z0-9_-]*)\s*:/g)) currentTokens.add(`--${m[1]}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const storedComponents = new Set(storedUi.components || []);
|
|
194
|
+
const storedTokens = new Set(storedUi.designTokens || []);
|
|
195
|
+
|
|
196
|
+
const newComponents = [...currentComponents].filter(c => !storedComponents.has(c));
|
|
197
|
+
const removedComponents = [...storedComponents].filter(c => !currentComponents.has(c));
|
|
198
|
+
const newTokens = [...currentTokens].filter(t => !storedTokens.has(t));
|
|
199
|
+
const removedTokens = [...storedTokens].filter(t => !currentTokens.has(t));
|
|
200
|
+
|
|
201
|
+
const lines = ["## infernoflow UI scan report", ""];
|
|
202
|
+
|
|
203
|
+
if (!newComponents.length && !removedComponents.length && !newTokens.length && !removedTokens.length) {
|
|
204
|
+
lines.push("✔ No UI changes detected since last scan.");
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (newComponents.length) {
|
|
209
|
+
lines.push(`### New components (${newComponents.length})`);
|
|
210
|
+
newComponents.slice(0, 15).forEach(c => lines.push(` + ${c}`));
|
|
211
|
+
lines.push("");
|
|
212
|
+
}
|
|
213
|
+
if (removedComponents.length) {
|
|
214
|
+
lines.push(`### Removed components (${removedComponents.length})`);
|
|
215
|
+
removedComponents.slice(0, 10).forEach(c => lines.push(` - ${c}`));
|
|
216
|
+
lines.push("");
|
|
217
|
+
}
|
|
218
|
+
if (newTokens.length) {
|
|
219
|
+
lines.push(`### New design tokens (${newTokens.length})`);
|
|
220
|
+
newTokens.slice(0, 10).forEach(t => lines.push(` + ${t}`));
|
|
221
|
+
lines.push("");
|
|
222
|
+
}
|
|
223
|
+
if (removedTokens.length) {
|
|
224
|
+
lines.push(`### Removed design tokens (${removedTokens.length})`);
|
|
225
|
+
removedTokens.slice(0, 10).forEach(t => lines.push(` - ${t}`));
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
lines.push("### Suggested action");
|
|
230
|
+
if (newComponents.length) {
|
|
231
|
+
const newCaps = newComponents.slice(0, 5).map(c => `View${c}`).join(", ");
|
|
232
|
+
lines.push(`Consider adding these capabilities: ${newCaps}`);
|
|
233
|
+
lines.push(`Call infernoflow_run with task "add UI capabilities for new components: ${newComponents.slice(0,3).join(", ")}" to update the contract.`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── infernoflow_review ─────────────────────────────────────────────────────
|
|
240
|
+
function reviewDrift(baseBranch) {
|
|
241
|
+
const cwd = process.cwd();
|
|
242
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
243
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
244
|
+
if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
|
|
245
|
+
|
|
246
|
+
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
247
|
+
|
|
248
|
+
// Get changed files vs base branch
|
|
249
|
+
const runGit = (cmd) => { try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 15_000 }); } catch { return ""; } };
|
|
250
|
+
|
|
251
|
+
const diffOutput = runGit(`git diff --name-only ${baseBranch}...HEAD`);
|
|
252
|
+
const changedFiles = diffOutput.split("\n").map(l => l.trim()).filter(Boolean);
|
|
253
|
+
|
|
254
|
+
if (!changedFiles.length) return `No changes detected vs ${baseBranch}. Safe to merge.`;
|
|
255
|
+
|
|
256
|
+
// Categorise changed files
|
|
257
|
+
const infraFiles = changedFiles.filter(f => /\.(json|yaml|yml|env|config|lock)$/.test(f) || f.includes("inferno/"));
|
|
258
|
+
const sourceFiles = changedFiles.filter(f => /\.(ts|tsx|js|jsx|mjs|cs|py|go|java)$/.test(f));
|
|
259
|
+
const styleFiles = changedFiles.filter(f => /\.(css|scss|sass|less)$/.test(f));
|
|
260
|
+
const contractChanged = changedFiles.some(f => f.startsWith("inferno/"));
|
|
261
|
+
|
|
262
|
+
// Keyword-based drift detection on changed source files
|
|
263
|
+
const HEURISTICS = [
|
|
264
|
+
{ kw: ["search"], id: "SearchItems" }, { kw: ["filter"], id: "FilterItems" },
|
|
265
|
+
{ kw: ["auth", "login", "logout"], id: "Authentication" },
|
|
266
|
+
{ kw: ["create", "add", "new"], id: "CreateItem" },
|
|
267
|
+
{ kw: ["update", "edit", "patch"], id: "UpdateItem" },
|
|
268
|
+
{ kw: ["delete", "remove"], id: "DeleteItem" },
|
|
269
|
+
{ kw: ["list", "read", "fetch", "get"], id: "ReadItems" },
|
|
270
|
+
{ kw: ["due", "deadline"], id: "SetDueDate" },
|
|
271
|
+
{ kw: ["priority"], id: "SetPriority" },
|
|
272
|
+
{ kw: ["complete", "toggle"], id: "ToggleComplete" },
|
|
273
|
+
{ kw: ["export", "download"], id: "ExportData" },
|
|
274
|
+
{ kw: ["import", "upload"], id: "ImportData" },
|
|
275
|
+
{ kw: ["notify", "notification", "email"], id: "SendNotification" },
|
|
276
|
+
{ kw: ["payment", "checkout", "stripe"], id: "ProcessPayment" },
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const capHits = new Map();
|
|
280
|
+
const registeredCaps = new Set(contract.capabilities || []);
|
|
281
|
+
|
|
282
|
+
for (const file of sourceFiles) {
|
|
283
|
+
const lower = file.toLowerCase();
|
|
284
|
+
for (const rule of HEURISTICS) {
|
|
285
|
+
if (rule.kw.some(k => lower.includes(k))) {
|
|
286
|
+
if (!capHits.has(rule.id)) capHits.set(rule.id, []);
|
|
287
|
+
capHits.get(rule.id).push(file);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const newCapSignals = [...capHits.entries()].filter(([id]) => !registeredCaps.has(id));
|
|
293
|
+
const existingCapSignals = [...capHits.entries()].filter(([id]) => registeredCaps.has(id));
|
|
294
|
+
|
|
295
|
+
const lines = [
|
|
296
|
+
`## infernoflow PR review — drift check vs \`${baseBranch}\``,
|
|
297
|
+
`Changed files: ${changedFiles.length} | Source: ${sourceFiles.length} | Styles: ${styleFiles.length} | Infra: ${infraFiles.length}`,
|
|
298
|
+
"",
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
// Risk assessment
|
|
302
|
+
let riskLevel = "LOW";
|
|
303
|
+
if (newCapSignals.length > 0) riskLevel = "MEDIUM";
|
|
304
|
+
if (newCapSignals.length >= 3 || (newCapSignals.length >= 1 && !contractChanged)) riskLevel = "HIGH";
|
|
305
|
+
|
|
306
|
+
const riskEmoji = riskLevel === "HIGH" ? "🔴" : riskLevel === "MEDIUM" ? "🟡" : "🟢";
|
|
307
|
+
lines.push(`### ${riskEmoji} Drift risk: ${riskLevel}`);
|
|
308
|
+
lines.push("");
|
|
309
|
+
|
|
310
|
+
if (contractChanged) {
|
|
311
|
+
lines.push("✔ inferno/ contract files were updated in this PR — good practice.");
|
|
312
|
+
lines.push("");
|
|
313
|
+
} else if (sourceFiles.length > 0) {
|
|
314
|
+
lines.push("⚠ Source files changed but inferno/ contract was NOT updated.");
|
|
315
|
+
lines.push(" Consider running: infernoflow_run to check if capabilities need updating.");
|
|
316
|
+
lines.push("");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (newCapSignals.length > 0) {
|
|
320
|
+
lines.push(`### Possible new capabilities (not in contract):`);
|
|
321
|
+
for (const [id, files] of newCapSignals.slice(0, 6)) {
|
|
322
|
+
lines.push(` - **${id}** — suggested by: ${files.slice(0,2).join(", ")}`);
|
|
323
|
+
}
|
|
324
|
+
lines.push("");
|
|
325
|
+
lines.push(`Suggested action: call infernoflow_run with task "review new capabilities: ${newCapSignals.slice(0,3).map(([id])=>id).join(', ')}"`);
|
|
326
|
+
lines.push("");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (existingCapSignals.length > 0) {
|
|
330
|
+
lines.push(`### Existing capabilities touched:`);
|
|
331
|
+
for (const [id, files] of existingCapSignals.slice(0, 6)) {
|
|
332
|
+
lines.push(` - **${id}** — ${files.slice(0,2).join(", ")}`);
|
|
333
|
+
}
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (styleFiles.length > 0) {
|
|
338
|
+
lines.push(`### Style changes (${styleFiles.length} files) — run infernoflow_scan_ui to check UI contract`);
|
|
339
|
+
styleFiles.slice(0, 5).forEach(f => lines.push(` - ${f}`));
|
|
340
|
+
lines.push("");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (riskLevel === "LOW" && !newCapSignals.length) {
|
|
344
|
+
lines.push("✔ No new capability signals detected. Safe to merge (run infernoflow_check as final gate).");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildImplementPrompt(task, mode) {
|
|
351
|
+
const cwd = process.cwd();
|
|
352
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
353
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
354
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
355
|
+
const profilePath = path.join(infernoDir, "developer-profile.json");
|
|
356
|
+
|
|
357
|
+
if (!fs.existsSync(contractPath)) return "inferno/ not found — run infernoflow init first";
|
|
358
|
+
|
|
359
|
+
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
360
|
+
const caps = fs.existsSync(capsPath) ? JSON.parse(fs.readFileSync(capsPath, "utf8")) : {};
|
|
361
|
+
const profile = fs.existsSync(profilePath) ? JSON.parse(fs.readFileSync(profilePath, "utf8")) : {};
|
|
362
|
+
|
|
363
|
+
const capList = (caps.capabilities || []).map(c => ` - ${c.id}: ${c.title || c.id}`).join("\n");
|
|
364
|
+
const stack = profile.stack || {};
|
|
365
|
+
const stackLine = [stack.framework, stack.language, stack.projectType].filter(Boolean).join(" / ") || "unknown";
|
|
366
|
+
const namingStyle = profile.namingStyle || "PascalCase";
|
|
367
|
+
|
|
368
|
+
const cursorPrompt = `## Cursor Agent Implementation Prompt
|
|
369
|
+
Task: "${task}"
|
|
370
|
+
Project: ${contract.policyId} (${stackLine})
|
|
371
|
+
Naming convention: ${namingStyle}
|
|
372
|
+
|
|
373
|
+
### Current capabilities
|
|
374
|
+
${capList || " (none registered)"}
|
|
375
|
+
|
|
376
|
+
### Implementation instructions
|
|
377
|
+
1. Implement "${task}" following the existing code patterns in this project
|
|
378
|
+
2. Use ${namingStyle} for any new identifiers, matching the existing capability naming
|
|
379
|
+
3. Keep changes minimal — only touch files relevant to this task
|
|
380
|
+
4. After implementing, call \`infernoflow_run\` with task "${task}" to update the contract
|
|
381
|
+
5. Then call \`infernoflow_check\` to validate everything is in sync
|
|
382
|
+
|
|
383
|
+
### Definition of done
|
|
384
|
+
- Feature works as described
|
|
385
|
+
- Contract updated via infernoflow_run → infernoflow_apply
|
|
386
|
+
- infernoflow_check passes`;
|
|
387
|
+
|
|
388
|
+
const genericPrompt = `## Implementation Prompt
|
|
389
|
+
Task: "${task}"
|
|
390
|
+
Project: ${contract.policyId}
|
|
391
|
+
Stack: ${stackLine}
|
|
392
|
+
Capabilities already in contract: ${(contract.capabilities || []).join(", ")}
|
|
393
|
+
|
|
394
|
+
Implement the task above. When done, run:
|
|
395
|
+
infernoflow suggest "${task}"
|
|
396
|
+
infernoflow check`;
|
|
397
|
+
|
|
398
|
+
if (mode === "cursor") return cursorPrompt;
|
|
399
|
+
if (mode === "generic") return genericPrompt;
|
|
400
|
+
return cursorPrompt + "\n\n---\n\n" + genericPrompt;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildPrompt(task) {
|
|
404
|
+
const infernoDir = path.join(process.cwd(), "inferno");
|
|
405
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
406
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
407
|
+
if (!fs.existsSync(contractPath)) return null;
|
|
408
|
+
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
409
|
+
const caps = fs.existsSync(capsPath) ? JSON.parse(fs.readFileSync(capsPath, "utf8")) : {};
|
|
410
|
+
const capList = (caps.capabilities || []).map(c => ` - ${c.id}: ${c.title || c.id}`).join("\n");
|
|
411
|
+
return `You are a developer assistant for the infernoflow CLI tool.
|
|
412
|
+
Analyze this task and suggest updates to the infernoflow contract files.
|
|
413
|
+
|
|
414
|
+
## Current contract
|
|
415
|
+
policyId: ${contract.policyId}
|
|
416
|
+
policyVersion: ${contract.policyVersion}
|
|
417
|
+
capabilities: [${(contract.capabilities || []).join(", ")}]
|
|
418
|
+
|
|
419
|
+
## Capabilities registry
|
|
420
|
+
${capList || " (none)"}
|
|
421
|
+
|
|
422
|
+
## Task
|
|
423
|
+
"${task}"
|
|
424
|
+
|
|
425
|
+
## Instructions
|
|
426
|
+
Respond with ONLY a valid JSON object:
|
|
427
|
+
{
|
|
428
|
+
"summary": "one-line summary of what changed",
|
|
429
|
+
"newCapabilities": [{ "id": "PascalCase", "title": "Human readable title", "reason": "why this is new" }],
|
|
430
|
+
"removedCapabilities": [],
|
|
431
|
+
"updatedScenarios": [],
|
|
432
|
+
"changelogEntry": "- Short description for CHANGELOG.md"
|
|
433
|
+
}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function handleTool(id, name, input) {
|
|
437
|
+
try {
|
|
438
|
+
let text = "";
|
|
439
|
+
if (name === "infernoflow_run") {
|
|
440
|
+
const prompt = buildPrompt(input.task);
|
|
441
|
+
if (!prompt) { sendError(id, -32000, "inferno/ not found — run infernoflow init first"); return; }
|
|
442
|
+
const promptFile = path.join(process.cwd(), "inferno", "agent-prompt.md");
|
|
443
|
+
fs.writeFileSync(promptFile, prompt, "utf8");
|
|
444
|
+
text = `## infernoflow task: "${input.task}"\n\n${prompt}\n\n---\nRespond with the JSON, then call **infernoflow_apply** with your JSON string.`;
|
|
445
|
+
} else if (name === "infernoflow_apply") {
|
|
446
|
+
const responseFile = path.join(process.cwd(), "inferno", "agent-response.json");
|
|
447
|
+
let json = input.json.trim().replace(/^```json?\n?/, "").replace(/\n?```$/, "");
|
|
448
|
+
fs.writeFileSync(responseFile, json, "utf8");
|
|
449
|
+
text = runCmd(`run "apply"`, { INFERNO_AGENT_RESPONSE_FILE: responseFile, INFERNO_AGENT_AVAILABLE: "1" });
|
|
450
|
+
} else if (name === "infernoflow_check") {
|
|
451
|
+
text = runCmd("check");
|
|
452
|
+
} else if (name === "infernoflow_status") {
|
|
453
|
+
text = runCmd("status");
|
|
454
|
+
} else if (name === "infernoflow_context") {
|
|
455
|
+
const parts = [];
|
|
456
|
+
if (input.intent) parts.push(`--intent "${input.intent}"`);
|
|
457
|
+
if (input.working) parts.push(`--working "${input.working}"`);
|
|
458
|
+
text = runCmd("context " + parts.join(" "));
|
|
459
|
+
} else if (name === "infernoflow_git_drift") {
|
|
460
|
+
text = detectGitDrift(input.sinceCommits || 1);
|
|
461
|
+
} else if (name === "infernoflow_implement") {
|
|
462
|
+
text = buildImplementPrompt(input.task, input.mode || "both");
|
|
463
|
+
} else if (name === "infernoflow_scan_ui") {
|
|
464
|
+
text = scanUi();
|
|
465
|
+
} else if (name === "infernoflow_review") {
|
|
466
|
+
text = reviewDrift(input.branch || "main");
|
|
467
|
+
|
|
468
|
+
// ── AMP-spec aliases ───────────────────────────────────────────────────
|
|
469
|
+
} else if (name === "amp_read") {
|
|
470
|
+
const args = [];
|
|
471
|
+
if (input.query) args.push(JSON.stringify(input.query));
|
|
472
|
+
if (input.type) args.push("--type", input.type);
|
|
473
|
+
if (input.limit) args.push("--limit", String(input.limit));
|
|
474
|
+
text = runCmd("ask " + args.join(" "));
|
|
475
|
+
} else if (name === "amp_write") {
|
|
476
|
+
const t = (input.type || "note").replace(/[^a-z]/g, "");
|
|
477
|
+
const m = JSON.stringify(input.msg || "");
|
|
478
|
+
const extras = [];
|
|
479
|
+
if (input.file) extras.push("--source", JSON.stringify(input.file));
|
|
480
|
+
text = runCmd(`log ${m} --type ${t} ${extras.join(" ")}`);
|
|
481
|
+
} else if (name === "amp_handoff") {
|
|
482
|
+
// switch writes a file; we read it back to return the content
|
|
483
|
+
runCmd("switch");
|
|
484
|
+
try {
|
|
485
|
+
const ampPath = path.join(process.cwd(), ".ai-memory", "handoff.md");
|
|
486
|
+
const legacyPath = path.join(process.cwd(), "inferno", "HANDOFF.md");
|
|
487
|
+
const target = fs.existsSync(ampPath) ? ampPath : legacyPath;
|
|
488
|
+
text = fs.readFileSync(target, "utf8");
|
|
489
|
+
if (input.format === "json") {
|
|
490
|
+
// very small markdown-to-json — caller can re-parse if needed
|
|
491
|
+
text = JSON.stringify({ handoff: text });
|
|
492
|
+
}
|
|
493
|
+
} catch (err) {
|
|
494
|
+
text = "(handoff generated; could not read back: " + err.message + ")";
|
|
495
|
+
}
|
|
496
|
+
} else if (name === "amp_search") {
|
|
497
|
+
const args = [JSON.stringify(input.query || "")];
|
|
498
|
+
if (input.type) args.push("--type", input.type);
|
|
499
|
+
text = runCmd("ask " + args.join(" "));
|
|
500
|
+
} else if (name === "amp_health") {
|
|
501
|
+
text = runCmd("recap --json").trim() || runCmd("status");
|
|
502
|
+
|
|
503
|
+
} else { return sendError(id, -32601, `Unknown tool: ${name}`); }
|
|
504
|
+
sendResult(id, { content: [{ type: "text", text: text || "(no output)" }] });
|
|
505
|
+
} catch (err) { sendError(id, -32000, err.message); }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
509
|
+
rl.on("line", (line) => {
|
|
510
|
+
let msg; try { msg = JSON.parse(line); } catch { return; }
|
|
511
|
+
const { id, method, params } = msg;
|
|
512
|
+
if (method === "initialize") { sendResult(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "infernoflow", version: "1.0.0" } }); return; }
|
|
513
|
+
if (method === "tools/list") { sendResult(id, { tools: TOOLS }); return; }
|
|
514
|
+
if (method === "tools/call") { handleTool(id, params.name, params.arguments || {}); return; }
|
|
515
|
+
if (id !== undefined) sendError(id, -32601, `Method not found: ${method}`);
|
|
516
|
+
});
|
|
471
517
|
process.stderr.write("[infernoflow MCP] started\n");
|