infernoflow 0.10.9 → 0.10.11
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/README.md +40 -0
- package/bin/infernoflow.mjs +18 -1
- package/lib/commands/init.mjs +55 -1
- package/lib/commands/installCursorHooks.mjs +36 -0
- package/lib/commands/installVsCodeCopilotHooks.mjs +37 -0
- package/lib/cursorHooksInstall.mjs +39 -0
- package/lib/draftToolingInstall.mjs +69 -0
- package/lib/vsCodeCopilotHooksInstall.mjs +42 -0
- package/package.json +1 -1
- package/templates/cursor/hooks/inferno-session-draft.mjs +112 -0
- package/templates/cursor/hooks.json +15 -0
- package/templates/github-hooks/infernoflow-drafts.json +17 -0
- package/templates/scripts/inferno-promote-draft.mjs +114 -0
- package/templates/scripts/inferno-vscode-copilot-hook.mjs +184 -0
package/README.md
CHANGED
|
@@ -28,6 +28,17 @@ npx infernoflow init
|
|
|
28
28
|
# 1. Scaffold in your project root:
|
|
29
29
|
npx infernoflow init
|
|
30
30
|
|
|
31
|
+
# Optional: same as init, plus Cursor hooks that append each Agent reply
|
|
32
|
+
# to inferno/CONTEXT.draft.md (gitignored). Promote with npm run inferno:promote-draft.
|
|
33
|
+
npx infernoflow init --yes --cursor-hooks
|
|
34
|
+
|
|
35
|
+
# Optional: VS Code + GitHub Copilot agent hooks (Preview) — same draft file + promote flow
|
|
36
|
+
npx infernoflow init --yes --vscode-copilot-hooks
|
|
37
|
+
|
|
38
|
+
# Or add hooks to an existing infernoflow project:
|
|
39
|
+
npx infernoflow install-cursor-hooks
|
|
40
|
+
npx infernoflow install-vscode-copilot-hooks
|
|
41
|
+
|
|
31
42
|
# 2. See your contract health:
|
|
32
43
|
infernoflow status
|
|
33
44
|
|
|
@@ -172,11 +183,38 @@ infernoflow doc-gate --json
|
|
|
172
183
|
| `infernoflow check` | Full validation: contract, capabilities, scenarios, changelog |
|
|
173
184
|
| `infernoflow doc-gate` | Fails if code changed but docs weren't updated |
|
|
174
185
|
| `infernoflow context` | Build/persist AI session context for this project |
|
|
186
|
+
| `infernoflow install-cursor-hooks` | Install `.cursor/hooks` + `scripts/inferno-promote-draft.mjs` (Agent → draft → promote) |
|
|
187
|
+
| `infernoflow install-vscode-copilot-hooks` | Install `.github/hooks` for VS Code + Copilot (Preview) + same promote script |
|
|
188
|
+
|
|
189
|
+
### Cursor hooks (draft → promote)
|
|
190
|
+
|
|
191
|
+
[Cursor hooks](https://cursor.com/docs/agent/hooks) can run a small Node script after each assistant message. infernoflow can install:
|
|
192
|
+
|
|
193
|
+
- **`.cursor/hooks.json`** — `afterAgentResponse` and `stop` events
|
|
194
|
+
- **`.cursor/hooks/inferno-session-draft.mjs`** — appends assistant `text` to **`inferno/CONTEXT.draft.md`** (never overwrites `CONTEXT.md` automatically)
|
|
195
|
+
- **`scripts/inferno-promote-draft.mjs`** + **`npm run inferno:promote-draft`** — preview, `--append-notes` (merge under `## Decisions & notes` in `inferno/CONTEXT.md`), or `--clear`
|
|
196
|
+
- **`.gitignore`** entry for `inferno/CONTEXT.draft.md` when possible
|
|
197
|
+
|
|
198
|
+
Install with **`infernoflow install-cursor-hooks`** or **`infernoflow init --cursor-hooks`**. Restart Cursor after install. Review the draft before promoting; treat chat as **input**, not product truth.
|
|
199
|
+
|
|
200
|
+
### VS Code + GitHub Copilot hooks (draft → promote, Preview)
|
|
201
|
+
|
|
202
|
+
VS Code can run [Agent hooks](https://code.visualstudio.com/docs/copilot/customization/hooks) from **`.github/hooks/*.json`**. infernoflow installs:
|
|
203
|
+
|
|
204
|
+
- **`.github/hooks/infernoflow-drafts.json`** — wires **`UserPromptSubmit`** (your prompt) and **`Stop`** (end of agent turn)
|
|
205
|
+
- **`scripts/inferno-vscode-copilot-hook.mjs`** — appends the user prompt on submit; on **Stop**, reads **`transcript_path`** from stdin (when present), parses **JSONL** or session **JSON**, and appends the **last assistant text** it can infer (format varies by VS Code / Copilot version — if parsing fails, a short marker is still appended)
|
|
206
|
+
- The same **`inferno:promote-draft`** script and **`.gitignore`** entry for **`inferno/CONTEXT.draft.md`** as the Cursor flow
|
|
207
|
+
|
|
208
|
+
Install with **`infernoflow install-vscode-copilot-hooks`** or **`infernoflow init --vscode-copilot-hooks`**. Restart VS Code, confirm your org allows hooks, and use the **GitHub Copilot Chat Hooks** output channel for diagnostics.
|
|
209
|
+
|
|
210
|
+
**Limitations:** Hooks are **Preview**; `transcript_path` / JSONL shape may differ by build; some hook events omit `transcript_path` ([vscode#300583](https://github.com/microsoft/vscode/issues/300583)). You still have the full **`infernoflow`** CLI in the terminal when hooks are not enough.
|
|
175
211
|
|
|
176
212
|
### Options
|
|
177
213
|
|
|
178
214
|
```bash
|
|
179
215
|
infernoflow init --force # overwrite existing files
|
|
216
|
+
infernoflow init --cursor-hooks # with init: install Cursor draft hooks (see above)
|
|
217
|
+
infernoflow init --vscode-copilot-hooks # with init: install VS Code + Copilot draft hooks (Preview)
|
|
180
218
|
infernoflow init --yes # skip prompts, use defaults
|
|
181
219
|
infernoflow init --adopt # infer baseline from existing project
|
|
182
220
|
infernoflow init --adopt --lang ts --framework react --project-type frontend
|
|
@@ -194,6 +232,8 @@ infernoflow pr-impact --json
|
|
|
194
232
|
infernoflow sync --auto
|
|
195
233
|
infernoflow sync --auto --json
|
|
196
234
|
npm run inferno:hooks # install local git hooks (after init)
|
|
235
|
+
infernoflow install-cursor-hooks --force # overwrite hook files if present
|
|
236
|
+
infernoflow install-vscode-copilot-hooks --force
|
|
197
237
|
infernoflow check --json # machine-readable output for CI
|
|
198
238
|
infernoflow check --skip-doc-gate
|
|
199
239
|
infernoflow status --json # machine-readable status summary
|
package/bin/infernoflow.mjs
CHANGED
|
@@ -9,6 +9,9 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8
|
|
|
9
9
|
const VERSION = pkg.version || "0.0.0";
|
|
10
10
|
const COMMAND_DESCRIPTIONS = {
|
|
11
11
|
init: "Scaffold inferno/ in your project (or adopt existing project)",
|
|
12
|
+
"install-cursor-hooks": "Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md",
|
|
13
|
+
"install-vscode-copilot-hooks":
|
|
14
|
+
"Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",
|
|
12
15
|
check: "Validate contract, capabilities, scenarios, changelog",
|
|
13
16
|
status: "Show contract health at a glance",
|
|
14
17
|
"pr-impact": "Summarize PR impact on capabilities and docs",
|
|
@@ -22,6 +25,10 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
22
25
|
|
|
23
26
|
const COMMAND_HANDLERS = {
|
|
24
27
|
init: async (args) => (await import("../lib/commands/init.mjs")).initCommand(args),
|
|
28
|
+
"install-cursor-hooks": async (args) =>
|
|
29
|
+
(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(args),
|
|
30
|
+
"install-vscode-copilot-hooks": async (args) =>
|
|
31
|
+
(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(args),
|
|
25
32
|
check: async (args) => (await import("../lib/commands/check.mjs")).checkCommand(args),
|
|
26
33
|
status: async (args) => (await import("../lib/commands/status.mjs")).statusCommand(args),
|
|
27
34
|
"pr-impact": async (args) => (await import("../lib/commands/prImpact.mjs")).prImpactCommand(args),
|
|
@@ -34,8 +41,10 @@ const COMMAND_HANDLERS = {
|
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
function formatCommandsHelp() {
|
|
44
|
+
const names = Object.keys(COMMAND_DESCRIPTIONS);
|
|
45
|
+
const w = Math.max(...names.map((n) => n.length), 8) + 1;
|
|
37
46
|
return Object.entries(COMMAND_DESCRIPTIONS)
|
|
38
|
-
.map(([name, desc]) => ` ${name.padEnd(
|
|
47
|
+
.map(([name, desc]) => ` ${name.padEnd(w, " ")}${desc}`)
|
|
39
48
|
.join("\n");
|
|
40
49
|
}
|
|
41
50
|
|
|
@@ -50,6 +59,8 @@ const HELP = `
|
|
|
50
59
|
${formatCommandsHelp()}
|
|
51
60
|
|
|
52
61
|
${bold("init options:")}
|
|
62
|
+
--cursor-hooks Also install Cursor hooks (draft → inferno/CONTEXT.draft.md)
|
|
63
|
+
--vscode-copilot-hooks Also install VS Code + Copilot hooks (.github/hooks — Preview)
|
|
53
64
|
--adopt Infer capabilities from an existing codebase
|
|
54
65
|
--lang <name> Override detected language (e.g. ts, js, py)
|
|
55
66
|
--framework <name> Override detected framework (e.g. react, angular, express)
|
|
@@ -60,6 +71,12 @@ ${formatCommandsHelp()}
|
|
|
60
71
|
--yes, -y Skip prompts and accept inferred/default values
|
|
61
72
|
--force, -f Overwrite existing inferno/ files
|
|
62
73
|
|
|
74
|
+
${bold("install-cursor-hooks options:")}
|
|
75
|
+
--force, -f Overwrite .cursor/hooks.json and hook scripts if they exist
|
|
76
|
+
|
|
77
|
+
${bold("install-vscode-copilot-hooks options:")}
|
|
78
|
+
--force, -f Overwrite .github/hooks/infernoflow-drafts.json and scripts if they exist
|
|
79
|
+
|
|
63
80
|
${bold("context options:")}
|
|
64
81
|
--intent "..." What you plan to build next
|
|
65
82
|
--working "..." What you are building right now
|
package/lib/commands/init.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
summarizeCapabilities,
|
|
12
12
|
buildSignalsReport,
|
|
13
13
|
} from "./adopt.mjs";
|
|
14
|
+
import { installCursorHooksArtifacts } from "../cursorHooksInstall.mjs";
|
|
15
|
+
import { installVsCodeCopilotHooksArtifacts } from "../vsCodeCopilotHooksInstall.mjs";
|
|
14
16
|
|
|
15
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
|
|
@@ -153,6 +155,8 @@ export async function initCommand(args) {
|
|
|
153
155
|
const force = args.includes("--force") || args.includes("-f");
|
|
154
156
|
const yes = args.includes("--yes") || args.includes("-y");
|
|
155
157
|
const adopt = args.includes("--adopt");
|
|
158
|
+
const cursorHooks = args.includes("--cursor-hooks");
|
|
159
|
+
const vscodeCopilotHooks = args.includes("--vscode-copilot-hooks");
|
|
156
160
|
const reportJson = args.includes("--report-json");
|
|
157
161
|
const reportJsonOnly = args.includes("--report-json-only");
|
|
158
162
|
const reportHumanOnly = args.includes("--report-human-only");
|
|
@@ -317,6 +321,35 @@ export async function initCommand(args) {
|
|
|
317
321
|
|
|
318
322
|
upsertScripts(cwd, silent);
|
|
319
323
|
|
|
324
|
+
if (cursorHooks) {
|
|
325
|
+
installCursorHooksArtifacts({
|
|
326
|
+
cwd,
|
|
327
|
+
templatesRoot: templates,
|
|
328
|
+
force,
|
|
329
|
+
silent,
|
|
330
|
+
logOk: (msg) => {
|
|
331
|
+
if (!silent) ok(msg);
|
|
332
|
+
},
|
|
333
|
+
logWarn: (msg) => {
|
|
334
|
+
if (!silent) warn(msg);
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (vscodeCopilotHooks) {
|
|
339
|
+
installVsCodeCopilotHooksArtifacts({
|
|
340
|
+
cwd,
|
|
341
|
+
templatesRoot: templates,
|
|
342
|
+
force,
|
|
343
|
+
silent,
|
|
344
|
+
logOk: (msg) => {
|
|
345
|
+
if (!silent) ok(msg);
|
|
346
|
+
},
|
|
347
|
+
logWarn: (msg) => {
|
|
348
|
+
if (!silent) warn(msg);
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
320
353
|
if (adopt) {
|
|
321
354
|
const statePath = path.join(infernoDir, "context-state.json");
|
|
322
355
|
let state = {};
|
|
@@ -341,7 +374,28 @@ export async function initCommand(args) {
|
|
|
341
374
|
cyan("infernoflow check") + " — validate everything",
|
|
342
375
|
(adopt ? "Review inferred baseline in " : "Edit ") + yellow("inferno/capabilities.json") + (adopt ? " and refine IDs/titles" : " to describe each capability in detail"),
|
|
343
376
|
"Add more " + yellow("inferno/scenarios/*.json") + " files for edge cases",
|
|
344
|
-
"Add " + cyan("inferno:check") + " to your CI pipeline"
|
|
377
|
+
"Add " + cyan("inferno:check") + " to your CI pipeline",
|
|
378
|
+
...(cursorHooks
|
|
379
|
+
? [
|
|
380
|
+
"Restart Cursor — hooks write assistant text to " + yellow("inferno/CONTEXT.draft.md"),
|
|
381
|
+
"Promote when ready: " + cyan("npm run inferno:promote-draft -- --append-notes"),
|
|
382
|
+
]
|
|
383
|
+
: []),
|
|
384
|
+
...(vscodeCopilotHooks
|
|
385
|
+
? [
|
|
386
|
+
"Restart VS Code — Copilot hooks append prompts + assistant (from transcript) to " +
|
|
387
|
+
yellow("inferno/CONTEXT.draft.md"),
|
|
388
|
+
"Promote when ready: " + cyan("npm run inferno:promote-draft -- --append-notes"),
|
|
389
|
+
]
|
|
390
|
+
: []),
|
|
391
|
+
...(!cursorHooks && !vscodeCopilotHooks
|
|
392
|
+
? [
|
|
393
|
+
"Optional: " +
|
|
394
|
+
cyan("infernoflow install-cursor-hooks") +
|
|
395
|
+
" or " +
|
|
396
|
+
cyan("infernoflow install-vscode-copilot-hooks"),
|
|
397
|
+
]
|
|
398
|
+
: []),
|
|
345
399
|
]);
|
|
346
400
|
}
|
|
347
401
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { header, ok, warn, done, nextSteps, cyan, yellow } from "../ui/output.mjs";
|
|
4
|
+
import { installCursorHooksArtifacts } from "../cursorHooksInstall.mjs";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
function getTemplatesRoot() {
|
|
9
|
+
return path.resolve(__dirname, "../../templates");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function installCursorHooksCommand(args) {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
15
|
+
|
|
16
|
+
header("install-cursor-hooks");
|
|
17
|
+
|
|
18
|
+
installCursorHooksArtifacts({
|
|
19
|
+
cwd,
|
|
20
|
+
templatesRoot: getTemplatesRoot(),
|
|
21
|
+
force,
|
|
22
|
+
silent: false,
|
|
23
|
+
logOk: (msg) => ok(msg),
|
|
24
|
+
logWarn: (msg) => warn(msg),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
done("Cursor draft hooks installed");
|
|
28
|
+
|
|
29
|
+
nextSteps([
|
|
30
|
+
"Restart Cursor (or reload window) so " + yellow(".cursor/hooks.json") + " is picked up",
|
|
31
|
+
"Use Agent chat — each assistant reply appends to " + yellow("inferno/CONTEXT.draft.md") + " (gitignored)",
|
|
32
|
+
cyan("npm run inferno:promote-draft") + " — preview draft",
|
|
33
|
+
cyan("npm run inferno:promote-draft -- --append-notes") + " — merge into inferno/CONTEXT.md under Decisions",
|
|
34
|
+
cyan("npm run inferno:promote-draft -- --clear") + " — discard draft",
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { header, ok, warn, done, nextSteps, cyan, yellow } from "../ui/output.mjs";
|
|
4
|
+
import { installVsCodeCopilotHooksArtifacts } from "../vsCodeCopilotHooksInstall.mjs";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
function getTemplatesRoot() {
|
|
9
|
+
return path.resolve(__dirname, "../../templates");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function installVsCodeCopilotHooksCommand(args) {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
15
|
+
|
|
16
|
+
header("install-vscode-copilot-hooks");
|
|
17
|
+
|
|
18
|
+
installVsCodeCopilotHooksArtifacts({
|
|
19
|
+
cwd,
|
|
20
|
+
templatesRoot: getTemplatesRoot(),
|
|
21
|
+
force,
|
|
22
|
+
silent: false,
|
|
23
|
+
logOk: (msg) => ok(msg),
|
|
24
|
+
logWarn: (msg) => warn(msg),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
done("VS Code / Copilot draft hooks installed");
|
|
28
|
+
|
|
29
|
+
nextSteps([
|
|
30
|
+
"Requires VS Code + GitHub Copilot and **Agent hooks (Preview)** — see " +
|
|
31
|
+
yellow("https://code.visualstudio.com/docs/copilot/customization/hooks"),
|
|
32
|
+
"Hooks load from " + yellow(".github/hooks/*.json") + " — restart VS Code or reload window after first install",
|
|
33
|
+
"Check the **GitHub Copilot Chat Hooks** output channel if nothing runs",
|
|
34
|
+
cyan("npm run inferno:promote-draft") + " — preview draft",
|
|
35
|
+
cyan("npm run inferno:promote-draft -- --append-notes") + " — merge into inferno/CONTEXT.md",
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { installInfernoDraftTooling } from "./draftToolingInstall.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {object} opts
|
|
7
|
+
* @param {string} opts.cwd
|
|
8
|
+
* @param {string} opts.templatesRoot
|
|
9
|
+
* @param {boolean} opts.force
|
|
10
|
+
* @param {boolean} opts.silent
|
|
11
|
+
* @param {(msg: string) => void} [opts.logOk]
|
|
12
|
+
* @param {(msg: string) => void} [opts.logWarn]
|
|
13
|
+
*/
|
|
14
|
+
export function installCursorHooksArtifacts(opts) {
|
|
15
|
+
const { cwd, templatesRoot, force, silent } = opts;
|
|
16
|
+
const logOk = opts.logOk || (() => {});
|
|
17
|
+
const logWarn = opts.logWarn || (() => {});
|
|
18
|
+
|
|
19
|
+
function copyFile(src, dst) {
|
|
20
|
+
if (fs.existsSync(dst) && !force) {
|
|
21
|
+
if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
25
|
+
fs.copyFileSync(src, dst);
|
|
26
|
+
if (!silent) logOk("Created: " + path.relative(cwd, dst));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
installInfernoDraftTooling({ cwd, templatesRoot, force, silent, logOk, logWarn });
|
|
31
|
+
|
|
32
|
+
const srcHooksJson = path.join(templatesRoot, "cursor", "hooks.json");
|
|
33
|
+
const dstHooksJson = path.join(cwd, ".cursor", "hooks.json");
|
|
34
|
+
const srcHook = path.join(templatesRoot, "cursor", "hooks", "inferno-session-draft.mjs");
|
|
35
|
+
const dstHook = path.join(cwd, ".cursor", "hooks", "inferno-session-draft.mjs");
|
|
36
|
+
|
|
37
|
+
copyFile(srcHooksJson, dstHooksJson);
|
|
38
|
+
copyFile(srcHook, dstHook);
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
const GITIGNORE_SNIPPET = `
|
|
5
|
+
# infernoflow: agent draft (IDE hooks — review before commit)
|
|
6
|
+
inferno/CONTEXT.draft.md
|
|
7
|
+
`.trimStart();
|
|
8
|
+
|
|
9
|
+
function upsertPromoteScript(cwd, silent, logOk) {
|
|
10
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
11
|
+
if (!fs.existsSync(pkgPath)) {
|
|
12
|
+
if (!silent) logOk("No package.json — add script manually: inferno:promote-draft");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
16
|
+
pkg.scripts = pkg.scripts || {};
|
|
17
|
+
if (!pkg.scripts["inferno:promote-draft"]) {
|
|
18
|
+
pkg.scripts["inferno:promote-draft"] = "node scripts/inferno-promote-draft.mjs";
|
|
19
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
20
|
+
if (!silent) logOk("Updated package.json script: inferno:promote-draft");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* inferno/CONTEXT.draft.md gitignore + promote script (shared by Cursor and VS Code installers).
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
* @param {string} opts.cwd
|
|
28
|
+
* @param {string} opts.templatesRoot
|
|
29
|
+
* @param {boolean} opts.force
|
|
30
|
+
* @param {boolean} opts.silent
|
|
31
|
+
* @param {(msg: string) => void} [opts.logOk]
|
|
32
|
+
* @param {(msg: string) => void} [opts.logWarn]
|
|
33
|
+
*/
|
|
34
|
+
export function installInfernoDraftTooling(opts) {
|
|
35
|
+
const { cwd, templatesRoot, force, silent } = opts;
|
|
36
|
+
const logOk = opts.logOk || (() => {});
|
|
37
|
+
const logWarn = opts.logWarn || (() => {});
|
|
38
|
+
|
|
39
|
+
function copyFile(src, dst) {
|
|
40
|
+
if (fs.existsSync(dst) && !force) {
|
|
41
|
+
if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
45
|
+
fs.copyFileSync(src, dst);
|
|
46
|
+
if (!silent) logOk("Created: " + path.relative(cwd, dst));
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const srcPromote = path.join(templatesRoot, "scripts", "inferno-promote-draft.mjs");
|
|
51
|
+
const dstPromote = path.join(cwd, "scripts", "inferno-promote-draft.mjs");
|
|
52
|
+
copyFile(srcPromote, dstPromote);
|
|
53
|
+
|
|
54
|
+
upsertPromoteScript(cwd, silent, logOk);
|
|
55
|
+
|
|
56
|
+
const gi = path.join(cwd, ".gitignore");
|
|
57
|
+
if (fs.existsSync(gi)) {
|
|
58
|
+
const cur = fs.readFileSync(gi, "utf8");
|
|
59
|
+
if (cur.includes("CONTEXT.draft.md")) {
|
|
60
|
+
if (!silent) logOk(".gitignore already mentions CONTEXT.draft.md");
|
|
61
|
+
} else {
|
|
62
|
+
fs.appendFileSync(gi, `\n${GITIGNORE_SNIPPET}\n`, "utf8");
|
|
63
|
+
if (!silent) logOk("Updated: " + path.relative(cwd, gi));
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
fs.writeFileSync(gi, `${GITIGNORE_SNIPPET}\n`, "utf8");
|
|
67
|
+
if (!silent) logOk("Created: " + path.relative(cwd, gi));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { installInfernoDraftTooling } from "./draftToolingInstall.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* VS Code + GitHub Copilot agent hooks (Preview). See:
|
|
7
|
+
* https://code.visualstudio.com/docs/copilot/customization/hooks
|
|
8
|
+
*
|
|
9
|
+
* @param {object} opts
|
|
10
|
+
* @param {string} opts.cwd
|
|
11
|
+
* @param {string} opts.templatesRoot
|
|
12
|
+
* @param {boolean} opts.force
|
|
13
|
+
* @param {boolean} opts.silent
|
|
14
|
+
* @param {(msg: string) => void} [opts.logOk]
|
|
15
|
+
* @param {(msg: string) => void} [opts.logWarn]
|
|
16
|
+
*/
|
|
17
|
+
export function installVsCodeCopilotHooksArtifacts(opts) {
|
|
18
|
+
const { cwd, templatesRoot, force, silent } = opts;
|
|
19
|
+
const logOk = opts.logOk || (() => {});
|
|
20
|
+
const logWarn = opts.logWarn || (() => {});
|
|
21
|
+
|
|
22
|
+
function copyFile(src, dst) {
|
|
23
|
+
if (fs.existsSync(dst) && !force) {
|
|
24
|
+
if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
28
|
+
fs.copyFileSync(src, dst);
|
|
29
|
+
if (!silent) logOk("Created: " + path.relative(cwd, dst));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
installInfernoDraftTooling({ cwd, templatesRoot, force, silent, logOk, logWarn });
|
|
34
|
+
|
|
35
|
+
const srcHooks = path.join(templatesRoot, "github-hooks", "infernoflow-drafts.json");
|
|
36
|
+
const dstHooks = path.join(cwd, ".github", "hooks", "infernoflow-drafts.json");
|
|
37
|
+
const srcHookScript = path.join(templatesRoot, "scripts", "inferno-vscode-copilot-hook.mjs");
|
|
38
|
+
const dstHookScript = path.join(cwd, "scripts", "inferno-vscode-copilot-hook.mjs");
|
|
39
|
+
|
|
40
|
+
copyFile(srcHooks, dstHooks);
|
|
41
|
+
copyFile(srcHookScript, dstHookScript);
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
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();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"UserPromptSubmit": [
|
|
5
|
+
{
|
|
6
|
+
"type": "command",
|
|
7
|
+
"command": "node scripts/inferno-vscode-copilot-hook.mjs"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"Stop": [
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "node scripts/inferno-vscode-copilot-hook.mjs"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Merge inferno/CONTEXT.draft.md into inferno/CONTEXT.md under ## Decisions & notes,
|
|
4
|
+
* or clear the draft. Draft is gitignored; CONTEXT.md is the promoted source of truth.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
const draftFile = path.join(root, "inferno", "CONTEXT.draft.md");
|
|
11
|
+
const contextFile = path.join(root, "inferno", "CONTEXT.md");
|
|
12
|
+
|
|
13
|
+
/** Keep in sync with .cursor/hooks/inferno-session-draft.mjs (getDraftBody splits at first \\n---\\n). */
|
|
14
|
+
const DRAFT_HEADER = `# CONTEXT draft (gitignored)
|
|
15
|
+
|
|
16
|
+
Auto-captured by IDE hooks (Cursor and/or VS Code + Copilot). **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const DECISIONS_ANCHOR = "## Decisions & notes";
|
|
22
|
+
const PASTE_FOOTER_ANCHOR = "\n---\n_Paste this block at the start of any new AI session._";
|
|
23
|
+
|
|
24
|
+
function read(p) {
|
|
25
|
+
return fs.readFileSync(p, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function write(p, s) {
|
|
29
|
+
fs.writeFileSync(p, s, "utf8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getDraftBody() {
|
|
33
|
+
if (!fs.existsSync(draftFile)) return "";
|
|
34
|
+
const full = read(draftFile);
|
|
35
|
+
const sep = full.indexOf("\n---\n");
|
|
36
|
+
if (sep === -1) return full.trim();
|
|
37
|
+
const after = full.slice(sep + "\n---\n".length).trim();
|
|
38
|
+
return after;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clearDraft() {
|
|
42
|
+
fs.mkdirSync(path.dirname(draftFile), { recursive: true });
|
|
43
|
+
write(draftFile, DRAFT_HEADER);
|
|
44
|
+
console.log("Cleared inferno/CONTEXT.draft.md (header only).");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function appendNotes() {
|
|
48
|
+
if (!fs.existsSync(contextFile)) {
|
|
49
|
+
console.error("Missing inferno/CONTEXT.md");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const body = getDraftBody();
|
|
53
|
+
if (!body) {
|
|
54
|
+
console.error("Nothing to promote: inferno/CONTEXT.draft.md is empty after the header.");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ctx = read(contextFile);
|
|
59
|
+
const i = ctx.indexOf(DECISIONS_ANCHOR);
|
|
60
|
+
if (i === -1) {
|
|
61
|
+
console.error(`Could not find "${DECISIONS_ANCHOR}" in inferno/CONTEXT.md`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const j = ctx.indexOf(PASTE_FOOTER_ANCHOR, i);
|
|
65
|
+
if (j === -1) {
|
|
66
|
+
console.error("Could not find paste footer block in inferno/CONTEXT.md");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const before = ctx.slice(0, i + DECISIONS_ANCHOR.length);
|
|
71
|
+
const decisionsAndFooter = ctx.slice(i + DECISIONS_ANCHOR.length, j);
|
|
72
|
+
const after = ctx.slice(j);
|
|
73
|
+
|
|
74
|
+
let middle = decisionsAndFooter;
|
|
75
|
+
if (middle.includes("_No decisions recorded_")) {
|
|
76
|
+
middle = middle.replace("_No decisions recorded_", "").replace(/\n\n\n+/g, "\n\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const stamp = new Date().toISOString().slice(0, 19);
|
|
80
|
+
const indented = body.split("\n").map((line) => ` ${line}`).join("\n");
|
|
81
|
+
const block = `\n\n### Captured from agent draft (${stamp})\n\n${indented}\n`;
|
|
82
|
+
|
|
83
|
+
write(contextFile, `${before}${middle}${block}${after}`);
|
|
84
|
+
clearDraft();
|
|
85
|
+
console.log("Appended draft under ## Decisions & notes in inferno/CONTEXT.md and cleared the draft.");
|
|
86
|
+
console.log("Next: edit wording if needed, then run infernoflow check when contract/changelog should match.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const args = process.argv.slice(2);
|
|
90
|
+
if (args.includes("--clear")) {
|
|
91
|
+
clearDraft();
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
if (args.includes("--append-notes")) {
|
|
95
|
+
appendNotes();
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const body = getDraftBody();
|
|
100
|
+
if (!body) {
|
|
101
|
+
console.log("inferno/CONTEXT.draft.md has no captured content yet (after the header).");
|
|
102
|
+
console.log("Use the Agent chat; each assistant reply appends via the afterAgentResponse hook.");
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log("--- inferno/CONTEXT.draft.md (excerpt, first 2000 chars) ---\n");
|
|
107
|
+
console.log(body.slice(0, 2000) + (body.length > 2000 ? "\n…" : ""));
|
|
108
|
+
console.log("\n---");
|
|
109
|
+
console.log("Promote into CONTEXT.md under Decisions:");
|
|
110
|
+
console.log(" npm run inferno:promote-draft -- --append-notes");
|
|
111
|
+
console.log("Or set working/intent via CLI:");
|
|
112
|
+
console.log(' npm exec -- infernoflow context --working "..." --intent "..."');
|
|
113
|
+
console.log("Clear draft without merging:");
|
|
114
|
+
console.log(" npm run inferno:promote-draft -- --clear");
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Copilot / VS Code agent hook (Preview): stdin JSON per
|
|
4
|
+
* https://code.visualstudio.com/docs/copilot/customization/hooks
|
|
5
|
+
*
|
|
6
|
+
* - UserPromptSubmit: appends the user's prompt to inferno/CONTEXT.draft.md
|
|
7
|
+
* - Stop: reads transcript_path (JSONL or session JSON), appends last assistant text if found
|
|
8
|
+
*
|
|
9
|
+
* Always prints {"continue":true} so the agent is never blocked. Errors → stderr only.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
|
|
14
|
+
/** Keep in sync with inferno-promote-draft.mjs / inferno-session-draft.mjs */
|
|
15
|
+
const DRAFT_HEADER = `# CONTEXT draft (gitignored)
|
|
16
|
+
|
|
17
|
+
Auto-captured by VS Code / Copilot hooks. **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const MAX_APPEND = 120_000;
|
|
23
|
+
const MAX_FILE_BYTES = 280_000;
|
|
24
|
+
|
|
25
|
+
function draftPath(root) {
|
|
26
|
+
return path.join(root, "inferno", "CONTEXT.draft.md");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureDraft(file) {
|
|
30
|
+
if (!fs.existsSync(file)) {
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
fs.writeFileSync(file, DRAFT_HEADER, "utf8");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function trimFile(file) {
|
|
37
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
38
|
+
if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
|
|
39
|
+
const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
|
|
40
|
+
const idx = keep.indexOf("\n### ");
|
|
41
|
+
const body = idx === -1 ? keep : keep.slice(idx);
|
|
42
|
+
fs.writeFileSync(file, `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function append(root, block) {
|
|
46
|
+
const file = draftPath(root);
|
|
47
|
+
ensureDraft(file);
|
|
48
|
+
fs.appendFileSync(file, block, "utf8");
|
|
49
|
+
trimFile(file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readStdinSync() {
|
|
53
|
+
/** Prefer readSync: on some Windows shells, readFileSync(0) returns empty for piped hook stdin. */
|
|
54
|
+
const buf = Buffer.alloc(16 * 1024 * 1024);
|
|
55
|
+
let n = 0;
|
|
56
|
+
try {
|
|
57
|
+
n = fs.readSync(0, buf, 0, buf.length, null);
|
|
58
|
+
} catch {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
return buf.slice(0, n).toString("utf8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function flattenResponse(resp) {
|
|
65
|
+
if (!resp) return "";
|
|
66
|
+
if (typeof resp === "string") return resp.slice(0, MAX_APPEND);
|
|
67
|
+
if (typeof resp.markdown === "string") return resp.markdown;
|
|
68
|
+
if (typeof resp.text === "string") return resp.text;
|
|
69
|
+
if (Array.isArray(resp.parts)) {
|
|
70
|
+
const bits = resp.parts
|
|
71
|
+
.map((p) => (typeof p === "string" ? p : p?.text || p?.content || p?.value || ""))
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
if (bits.length) return bits.join("\n").slice(0, MAX_APPEND);
|
|
74
|
+
}
|
|
75
|
+
if (resp.message && typeof resp.message.text === "string") return resp.message.text;
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function lastAssistantFromSessionJson(data) {
|
|
80
|
+
const reqs = data.requests;
|
|
81
|
+
if (!Array.isArray(reqs)) return "";
|
|
82
|
+
for (let i = reqs.length - 1; i >= 0; i--) {
|
|
83
|
+
const t = flattenResponse(reqs[i]?.response);
|
|
84
|
+
if (t && t.trim()) return t.slice(0, MAX_APPEND);
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function extractFromJsonlLine(obj) {
|
|
90
|
+
if (!obj || typeof obj !== "object") return "";
|
|
91
|
+
const a = obj.assistant;
|
|
92
|
+
if (a && typeof a.message === "string" && a.message.trim()) return a.message.slice(0, MAX_APPEND);
|
|
93
|
+
if (a && typeof a.text === "string" && a.text.trim()) return a.text.slice(0, MAX_APPEND);
|
|
94
|
+
if ((obj.role === "assistant" || obj.type === "assistant") && typeof obj.message === "string")
|
|
95
|
+
return obj.message.slice(0, MAX_APPEND);
|
|
96
|
+
if ((obj.role === "assistant" || obj.type === "assistant") && typeof obj.content === "string")
|
|
97
|
+
return obj.content.slice(0, MAX_APPEND);
|
|
98
|
+
if (obj.assistantMessage && typeof obj.assistantMessage === "string")
|
|
99
|
+
return obj.assistantMessage.slice(0, MAX_APPEND);
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function lastAssistantFromTranscriptFile(transcriptPath) {
|
|
104
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return "";
|
|
105
|
+
const raw = fs.readFileSync(transcriptPath, "utf8").trim();
|
|
106
|
+
if (!raw) return "";
|
|
107
|
+
|
|
108
|
+
if (raw.startsWith("{")) {
|
|
109
|
+
try {
|
|
110
|
+
const data = JSON.parse(raw);
|
|
111
|
+
const fromReq = lastAssistantFromSessionJson(data);
|
|
112
|
+
if (fromReq) return fromReq;
|
|
113
|
+
} catch {
|
|
114
|
+
/* fall through */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let last = "";
|
|
119
|
+
for (const line of raw.split("\n")) {
|
|
120
|
+
const t = line.trim();
|
|
121
|
+
if (!t || t[0] !== "{") continue;
|
|
122
|
+
try {
|
|
123
|
+
const v = extractFromJsonlLine(JSON.parse(t));
|
|
124
|
+
if (v) last = v;
|
|
125
|
+
} catch {
|
|
126
|
+
/* skip line */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return last;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function main() {
|
|
133
|
+
let data = {};
|
|
134
|
+
try {
|
|
135
|
+
const s = readStdinSync().trim();
|
|
136
|
+
if (s) data = JSON.parse(s);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error("[inferno-vscode-copilot-hook] stdin JSON:", e.message);
|
|
139
|
+
console.log(JSON.stringify({ continue: true }));
|
|
140
|
+
process.exit(0);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const root = data.cwd || process.cwd();
|
|
145
|
+
const hook = String(data.hookEventName || data.hook_event_name || "")
|
|
146
|
+
.replace(/\s+/g, "")
|
|
147
|
+
.toLowerCase();
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
if (hook === "userpromptsubmit") {
|
|
151
|
+
const prompt = data.prompt || data.Prompt || "";
|
|
152
|
+
if (typeof prompt === "string" && prompt.trim()) {
|
|
153
|
+
append(
|
|
154
|
+
root,
|
|
155
|
+
`\n### User prompt (${new Date().toISOString()})\n\n${prompt.slice(0, MAX_APPEND)}\n\n---\n`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
} else if (hook === "stop") {
|
|
159
|
+
const tp = data.transcript_path || data.transcriptPath;
|
|
160
|
+
const assistant = lastAssistantFromTranscriptFile(tp);
|
|
161
|
+
const stopActive = data.stop_hook_active ?? data.stopHookActive;
|
|
162
|
+
if (assistant) {
|
|
163
|
+
append(
|
|
164
|
+
root,
|
|
165
|
+
`\n### Assistant (from transcript) (${new Date().toISOString()})\n\n${assistant}\n\n---\n`
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
append(
|
|
169
|
+
root,
|
|
170
|
+
`\n### _Copilot Stop_ (${new Date().toISOString()})\n\nstop_hook_active: ${Boolean(stopActive)}${
|
|
171
|
+
tp ? ` · transcript: ${tp}` : " · (no transcript_path or empty parse)"
|
|
172
|
+
}\n\n---\n`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error("[inferno-vscode-copilot-hook]", e);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(JSON.stringify({ continue: true }));
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main();
|