infernoflow 0.10.8 → 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 +63 -11
- package/bin/infernoflow.mjs +20 -1
- package/lib/ai/ideDetection.mjs +31 -0
- package/lib/ai/providerRouter.mjs +73 -0
- package/lib/commands/init.mjs +56 -2
- package/lib/commands/installCursorHooks.mjs +36 -0
- package/lib/commands/installVsCodeCopilotHooks.mjs +37 -0
- package/lib/commands/run.mjs +95 -7
- 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/ci/github-inferno-check.yml +2 -5
- 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-install-hooks.mjs +4 -4
- 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
|
|
@@ -240,10 +280,11 @@ Works with any AI — Claude, ChatGPT, GitHub Copilot, Cursor, or your own setup
|
|
|
240
280
|
|
|
241
281
|
Run one command and infernoflow will:
|
|
242
282
|
1. Detect drift (`pr-impact`)
|
|
243
|
-
2.
|
|
244
|
-
3.
|
|
245
|
-
4.
|
|
246
|
-
5.
|
|
283
|
+
2. Resolve provider (`auto` defaults to IDE agent)
|
|
284
|
+
3. Generate suggestion
|
|
285
|
+
4. Apply inferno updates
|
|
286
|
+
5. Validate with `check`
|
|
287
|
+
6. Roll back automatically if validation fails
|
|
247
288
|
|
|
248
289
|
```bash
|
|
249
290
|
infernoflow run "add favorite badge to tasks and filter by favorite"
|
|
@@ -255,10 +296,24 @@ Machine mode:
|
|
|
255
296
|
infernoflow run "sync check" --json
|
|
256
297
|
```
|
|
257
298
|
|
|
258
|
-
|
|
299
|
+
Provider options:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
infernoflow run "task" --provider auto # default (IDE agent first)
|
|
303
|
+
infernoflow run "task" --provider agent --ide cursor # require IDE agent
|
|
304
|
+
infernoflow run "task" --provider local # explicit local model
|
|
305
|
+
infernoflow run "task" --provider prompt # deterministic prompt fallback
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
IDE routing behavior:
|
|
309
|
+
- `auto` + agent available -> uses IDE agent
|
|
310
|
+
- `auto` + no agent -> falls back to prompt mode (`FALLBACK_PROMPT_MODE`)
|
|
311
|
+
- `agent` + no agent -> exits with `EXPLICIT_AGENT_REQUIRED`
|
|
312
|
+
|
|
313
|
+
Local model configuration (optional):
|
|
259
314
|
|
|
260
315
|
```bash
|
|
261
|
-
#
|
|
316
|
+
# local provider example: ollama
|
|
262
317
|
set INFERNO_LOCAL_PROVIDER=ollama
|
|
263
318
|
set INFERNO_LOCAL_ENDPOINT=http://127.0.0.1:11434/api/generate
|
|
264
319
|
set INFERNO_LOCAL_MODEL=llama3.1:8b
|
|
@@ -320,12 +375,9 @@ Recommended chain:
|
|
|
320
375
|
|
|
321
376
|
```yaml
|
|
322
377
|
# .github/workflows/ci.yml
|
|
323
|
-
- name: infernoflow run
|
|
324
|
-
run: npx infernoflow run "sync check" --json
|
|
378
|
+
- name: infernoflow run (headless)
|
|
379
|
+
run: npx infernoflow run "sync check" --provider prompt --json
|
|
325
380
|
env:
|
|
326
|
-
INFERNO_LOCAL_PROVIDER: ollama
|
|
327
|
-
INFERNO_LOCAL_ENDPOINT: http://127.0.0.1:11434/api/generate
|
|
328
|
-
INFERNO_LOCAL_MODEL: llama3.1:8b
|
|
329
381
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
330
382
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
331
383
|
```
|
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
|
|
@@ -76,6 +93,8 @@ ${formatCommandsHelp()}
|
|
|
76
93
|
--dry-run Execute full flow without writing files
|
|
77
94
|
--json Emit machine-readable events and result payload
|
|
78
95
|
--no-rollback Keep changes even if validation fails
|
|
96
|
+
--provider <type> auto | agent | local | prompt (default: auto)
|
|
97
|
+
--ide <name> auto | cursor | vscode | windsurf (default: auto)
|
|
79
98
|
|
|
80
99
|
${bold("Typical workflow:")}
|
|
81
100
|
${gray('1. infernoflow context --intent "what I want to build"')}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function detectIdeContext(preferredIde = "auto") {
|
|
2
|
+
const env = process.env;
|
|
3
|
+
const lowerPreferred = String(preferredIde || "auto").toLowerCase();
|
|
4
|
+
|
|
5
|
+
const hasCursor = !!(env.CURSOR_TRACE_ID || env.CURSOR_AGENT || env.CURSOR_SESSION_ID);
|
|
6
|
+
const hasVscode = !!(env.VSCODE_PID || env.VSCODE_CWD || env.GITHUB_COPILOT_AGENT);
|
|
7
|
+
const hasWindsurf = !!(env.WINDSURF || env.CODEIUM || env.WINDSURF_SESSION_ID);
|
|
8
|
+
|
|
9
|
+
let ideDetected = "unknown";
|
|
10
|
+
if (hasCursor) ideDetected = "cursor";
|
|
11
|
+
else if (hasVscode) ideDetected = "vscode";
|
|
12
|
+
else if (hasWindsurf) ideDetected = "windsurf";
|
|
13
|
+
|
|
14
|
+
if (lowerPreferred !== "auto" && ["cursor", "vscode", "windsurf"].includes(lowerPreferred)) {
|
|
15
|
+
ideDetected = lowerPreferred;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const explicitAgentAvailability = env.INFERNO_AGENT_AVAILABLE;
|
|
19
|
+
const agentAvailable = explicitAgentAvailability != null
|
|
20
|
+
? explicitAgentAvailability === "1" || explicitAgentAvailability === "true"
|
|
21
|
+
: ideDetected !== "unknown";
|
|
22
|
+
|
|
23
|
+
const reasonCodes = [];
|
|
24
|
+
if (ideDetected !== "unknown") reasonCodes.push(`IDE_${ideDetected.toUpperCase()}_DETECTED`);
|
|
25
|
+
else reasonCodes.push("IDE_UNKNOWN");
|
|
26
|
+
if (agentAvailable) reasonCodes.push("IDE_AGENT_AVAILABLE");
|
|
27
|
+
else reasonCodes.push("IDE_AGENT_UNAVAILABLE");
|
|
28
|
+
|
|
29
|
+
return { ideDetected, agentAvailable, reasonCodes };
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { detectIdeContext } from "./ideDetection.mjs";
|
|
2
|
+
|
|
3
|
+
export async function resolveProvider(requestedProvider = "auto", preferredIde = "auto") {
|
|
4
|
+
const providerRequested = String(requestedProvider || "auto").toLowerCase();
|
|
5
|
+
const ide = detectIdeContext(preferredIde);
|
|
6
|
+
const reasonCodes = [...ide.reasonCodes];
|
|
7
|
+
|
|
8
|
+
if (providerRequested === "local") {
|
|
9
|
+
reasonCodes.push("LOCAL_PROVIDER_SELECTED");
|
|
10
|
+
return {
|
|
11
|
+
providerRequested,
|
|
12
|
+
providerResolved: "local",
|
|
13
|
+
ideDetected: ide.ideDetected,
|
|
14
|
+
agentAvailable: ide.agentAvailable,
|
|
15
|
+
reasonCodes,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (providerRequested === "prompt") {
|
|
20
|
+
reasonCodes.push("PROMPT_PROVIDER_SELECTED");
|
|
21
|
+
return {
|
|
22
|
+
providerRequested,
|
|
23
|
+
providerResolved: "prompt",
|
|
24
|
+
ideDetected: ide.ideDetected,
|
|
25
|
+
agentAvailable: ide.agentAvailable,
|
|
26
|
+
reasonCodes,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (providerRequested === "agent") {
|
|
31
|
+
if (!ide.agentAvailable) {
|
|
32
|
+
reasonCodes.push("EXPLICIT_AGENT_REQUIRED");
|
|
33
|
+
return {
|
|
34
|
+
providerRequested,
|
|
35
|
+
providerResolved: "none",
|
|
36
|
+
ideDetected: ide.ideDetected,
|
|
37
|
+
agentAvailable: ide.agentAvailable,
|
|
38
|
+
reasonCodes,
|
|
39
|
+
error: "agent_unavailable",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
reasonCodes.push("IDE_AGENT_SELECTED");
|
|
43
|
+
return {
|
|
44
|
+
providerRequested,
|
|
45
|
+
providerResolved: "agent",
|
|
46
|
+
ideDetected: ide.ideDetected,
|
|
47
|
+
agentAvailable: ide.agentAvailable,
|
|
48
|
+
reasonCodes,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// auto
|
|
53
|
+
if (ide.agentAvailable) {
|
|
54
|
+
reasonCodes.push("IDE_AGENT_SELECTED");
|
|
55
|
+
return {
|
|
56
|
+
providerRequested: "auto",
|
|
57
|
+
providerResolved: "agent",
|
|
58
|
+
ideDetected: ide.ideDetected,
|
|
59
|
+
agentAvailable: ide.agentAvailable,
|
|
60
|
+
reasonCodes,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
reasonCodes.push("FALLBACK_PROMPT_MODE");
|
|
65
|
+
return {
|
|
66
|
+
providerRequested: "auto",
|
|
67
|
+
providerResolved: "prompt",
|
|
68
|
+
ideDetected: ide.ideDetected,
|
|
69
|
+
agentAvailable: ide.agentAvailable,
|
|
70
|
+
reasonCodes,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
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
|
|
|
@@ -68,7 +70,7 @@ function upsertScripts(cwd, silent = false) {
|
|
|
68
70
|
"inferno:gate": "infernoflow doc-gate",
|
|
69
71
|
"inferno:impact": "infernoflow pr-impact --json",
|
|
70
72
|
"inferno:sync": "infernoflow sync --auto --json",
|
|
71
|
-
"inferno:run": "infernoflow run \"sync check\" --json",
|
|
73
|
+
"inferno:run": "infernoflow run \"sync check\" --provider auto --json",
|
|
72
74
|
"inferno:hooks": "node scripts/inferno-install-hooks.mjs"
|
|
73
75
|
};
|
|
74
76
|
for (const [k, v] of Object.entries(toAdd)) {
|
|
@@ -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
|
+
}
|
package/lib/commands/run.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { generateWithLocalModel } from "../ai/localProvider.mjs";
|
|
6
|
+
import { resolveProvider } from "../ai/providerRouter.mjs";
|
|
6
7
|
import {
|
|
7
8
|
buildPrompt,
|
|
8
9
|
loadSuggestContext,
|
|
@@ -88,13 +89,59 @@ function writeRunArtifact(cwd, artifact) {
|
|
|
88
89
|
return path.relative(cwd, filePath);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function getOptionValue(args, flag, fallback = null) {
|
|
93
|
+
const idx = args.indexOf(flag);
|
|
94
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractTask(args) {
|
|
99
|
+
const takesValue = new Set(["--provider", "--ide"]);
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = 1; i < args.length; i++) {
|
|
102
|
+
const token = args[i];
|
|
103
|
+
if (token.startsWith("-")) {
|
|
104
|
+
if (takesValue.has(token)) i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
out.push(token);
|
|
108
|
+
}
|
|
109
|
+
return out.join(" ").trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildPromptFallbackSuggestion(task, contract) {
|
|
113
|
+
return {
|
|
114
|
+
summary: `Prompt fallback only: ${task}`,
|
|
115
|
+
newCapabilities: [],
|
|
116
|
+
removedCapabilities: [],
|
|
117
|
+
updatedScenarios: [],
|
|
118
|
+
changelogEntry: `- Prompt fallback mode for task: ${task} (no automatic contract mutation).`,
|
|
119
|
+
_meta: {
|
|
120
|
+
actionRequired: true,
|
|
121
|
+
nextStep: "Run infernoflow suggest or provide an agent bridge for automatic apply.",
|
|
122
|
+
capabilitiesCount: (contract?.capabilities || []).length,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function generateWithIdeAgent(prompt) {
|
|
128
|
+
if (process.env.INFERNO_AGENT_MOCK_RESPONSE) return process.env.INFERNO_AGENT_MOCK_RESPONSE;
|
|
129
|
+
if (process.env.INFERNO_AGENT_RESPONSE_FILE && fs.existsSync(process.env.INFERNO_AGENT_RESPONSE_FILE)) {
|
|
130
|
+
return fs.readFileSync(process.env.INFERNO_AGENT_RESPONSE_FILE, "utf8");
|
|
131
|
+
}
|
|
132
|
+
throw new Error("ide_agent_bridge_not_configured");
|
|
133
|
+
}
|
|
134
|
+
|
|
91
135
|
export async function runCommand(args = []) {
|
|
92
136
|
const asJson = args.includes("--json");
|
|
93
137
|
const dryRun = args.includes("--dry-run");
|
|
94
138
|
const noRollback = args.includes("--no-rollback");
|
|
95
|
-
const
|
|
139
|
+
const providerRequested = (getOptionValue(args, "--provider", "auto") || "auto").toLowerCase();
|
|
140
|
+
const ideRequested = (getOptionValue(args, "--ide", "auto") || "auto").toLowerCase();
|
|
141
|
+
const task = extractTask(args) || "sync check";
|
|
96
142
|
const cwd = process.cwd();
|
|
97
143
|
const events = [];
|
|
144
|
+
const reasonCodes = [];
|
|
98
145
|
|
|
99
146
|
if (!asJson) header("run");
|
|
100
147
|
stageEvent(asJson, events, "init", "info", { task, dryRun, noRollback });
|
|
@@ -103,6 +150,30 @@ export async function runCommand(args = []) {
|
|
|
103
150
|
const impact = runCliJson(["pr-impact", "--json"]);
|
|
104
151
|
stageEvent(asJson, events, "detect", impact.data?.ok ? "ok" : "warn", { confidence: impact.data?.confidence || "low" });
|
|
105
152
|
|
|
153
|
+
const routed = await resolveProvider(providerRequested, ideRequested);
|
|
154
|
+
reasonCodes.push(...(routed.reasonCodes || []));
|
|
155
|
+
if (routed.error === "agent_unavailable") {
|
|
156
|
+
const payload = {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: "agent_unavailable",
|
|
159
|
+
providerRequested,
|
|
160
|
+
providerResolved: routed.providerResolved,
|
|
161
|
+
ideDetected: routed.ideDetected,
|
|
162
|
+
agentAvailable: routed.agentAvailable,
|
|
163
|
+
reasonCodes,
|
|
164
|
+
events,
|
|
165
|
+
};
|
|
166
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
167
|
+
else fail("provider agent unavailable", "Use --provider auto|local|prompt");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
stageEvent(asJson, events, "route", "ok", {
|
|
171
|
+
providerRequested,
|
|
172
|
+
providerResolved: routed.providerResolved,
|
|
173
|
+
ideDetected: routed.ideDetected,
|
|
174
|
+
agentAvailable: routed.agentAvailable,
|
|
175
|
+
});
|
|
176
|
+
|
|
106
177
|
const ctx = loadSuggestContext(cwd);
|
|
107
178
|
if (!ctx?.contract) {
|
|
108
179
|
const payload = { ok: false, error: "inferno_missing", events };
|
|
@@ -120,12 +191,19 @@ export async function runCommand(args = []) {
|
|
|
120
191
|
});
|
|
121
192
|
let suggestion;
|
|
122
193
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
194
|
+
if (routed.providerResolved === "local") {
|
|
195
|
+
const raw = await generateWithLocalModel(prompt);
|
|
196
|
+
suggestion = parseSuggestionJson(raw);
|
|
197
|
+
} else if (routed.providerResolved === "agent") {
|
|
198
|
+
const raw = await generateWithIdeAgent(prompt);
|
|
199
|
+
suggestion = parseSuggestionJson(raw);
|
|
200
|
+
} else {
|
|
201
|
+
suggestion = buildPromptFallbackSuggestion(task, ctx.contract);
|
|
202
|
+
}
|
|
125
203
|
} catch (err) {
|
|
126
|
-
const payload = { ok: false, error: "
|
|
204
|
+
const payload = { ok: false, error: "proposal_failed", reason: String(err.message || err), reasonCodes, events };
|
|
127
205
|
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
128
|
-
else fail(
|
|
206
|
+
else fail("proposal generation failed", err.message);
|
|
129
207
|
process.exit(1);
|
|
130
208
|
}
|
|
131
209
|
stageEvent(asJson, events, "propose", "ok", {
|
|
@@ -133,8 +211,8 @@ export async function runCommand(args = []) {
|
|
|
133
211
|
removedCapabilities: (suggestion.removedCapabilities || []).length,
|
|
134
212
|
});
|
|
135
213
|
|
|
136
|
-
const schemaErrors = validateSuggestion(suggestion);
|
|
137
|
-
const conflictErrors = detectSuggestionConflicts(ctx.contract, suggestion);
|
|
214
|
+
const schemaErrors = routed.providerResolved === "prompt" ? [] : validateSuggestion(suggestion);
|
|
215
|
+
const conflictErrors = routed.providerResolved === "prompt" ? [] : detectSuggestionConflicts(ctx.contract, suggestion);
|
|
138
216
|
if (schemaErrors.length || conflictErrors.length) {
|
|
139
217
|
const payload = {
|
|
140
218
|
ok: false,
|
|
@@ -155,6 +233,11 @@ export async function runCommand(args = []) {
|
|
|
155
233
|
try {
|
|
156
234
|
if (dryRun) {
|
|
157
235
|
stageEvent(asJson, events, "apply", "info", { dryRun: true });
|
|
236
|
+
} else if (routed.providerResolved === "prompt") {
|
|
237
|
+
stageEvent(asJson, events, "apply", "warn", {
|
|
238
|
+
skipped: true,
|
|
239
|
+
reason: "prompt_fallback_requires_manual_step",
|
|
240
|
+
});
|
|
158
241
|
} else {
|
|
159
242
|
applyChanged = applyChanges({
|
|
160
243
|
cwd,
|
|
@@ -205,6 +288,11 @@ export async function runCommand(args = []) {
|
|
|
205
288
|
mode: "run",
|
|
206
289
|
task,
|
|
207
290
|
dryRun,
|
|
291
|
+
providerRequested,
|
|
292
|
+
providerResolved: routed.providerResolved,
|
|
293
|
+
ideDetected: routed.ideDetected,
|
|
294
|
+
agentAvailable: routed.agentAvailable,
|
|
295
|
+
reasonCodes: Array.from(new Set(reasonCodes)),
|
|
208
296
|
rolledBack,
|
|
209
297
|
applyChanged,
|
|
210
298
|
artifactPath,
|
|
@@ -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
|
@@ -22,12 +22,9 @@ jobs:
|
|
|
22
22
|
- name: Install dependencies
|
|
23
23
|
run: npm ci --ignore-scripts || npm install --ignore-scripts
|
|
24
24
|
|
|
25
|
-
- name: Inferno one-command run
|
|
26
|
-
run: npx infernoflow run "sync check" --json
|
|
25
|
+
- name: Inferno one-command run (headless prompt mode)
|
|
26
|
+
run: npx infernoflow run "sync check" --provider prompt --json
|
|
27
27
|
env:
|
|
28
|
-
INFERNO_LOCAL_PROVIDER: ollama
|
|
29
|
-
INFERNO_LOCAL_ENDPOINT: http://127.0.0.1:11434/api/generate
|
|
30
|
-
INFERNO_LOCAL_MODEL: llama3.1:8b
|
|
31
28
|
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
32
29
|
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
33
30
|
|
|
@@ -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
|
+
}
|
|
@@ -14,13 +14,13 @@ if (!fs.existsSync(gitDir)) {
|
|
|
14
14
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
15
15
|
|
|
16
16
|
const preCommit = `#!/bin/sh
|
|
17
|
-
echo "[inferno hooks] pre-commit: infernoflow run --dry-run"
|
|
18
|
-
npx infernoflow run "sync check" --dry-run
|
|
17
|
+
echo "[inferno hooks] pre-commit: infernoflow run --provider auto --dry-run"
|
|
18
|
+
npx infernoflow run "sync check" --provider auto --dry-run
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
21
|
const prePush = `#!/bin/sh
|
|
22
|
-
echo "[inferno hooks] pre-push: infernoflow run --json"
|
|
23
|
-
npx infernoflow run "sync check" --json
|
|
22
|
+
echo "[inferno hooks] pre-push: infernoflow run --provider auto --json"
|
|
23
|
+
npx infernoflow run "sync check" --provider auto --json
|
|
24
24
|
`;
|
|
25
25
|
|
|
26
26
|
const writeHook = (name, content) => {
|
|
@@ -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();
|