sticky-note-cli 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +456 -0
- package/bin/cli.js +1941 -0
- package/package.json +34 -0
- package/templates/CLAUDE.md +208 -0
- package/templates/copilot-instructions.md +185 -0
- package/templates/gitignore-additions.txt +18 -0
- package/templates/hooks/inject-context.js +525 -0
- package/templates/hooks/on-error.js +135 -0
- package/templates/hooks/on-stop.js +150 -0
- package/templates/hooks/parse-transcript.js +67 -0
- package/templates/hooks/post-rewrite.js +63 -0
- package/templates/hooks/pre-tool-use.js +219 -0
- package/templates/hooks/session-end.js +953 -0
- package/templates/hooks/session-start.js +394 -0
- package/templates/hooks/sticky-attribution.js +534 -0
- package/templates/hooks/sticky-codex.ps1 +98 -0
- package/templates/hooks/sticky-codex.sh +71 -0
- package/templates/hooks/sticky-git-notes.js +337 -0
- package/templates/hooks/sticky-utils.js +647 -0
- package/templates/hooks/track-work.js +321 -0
- package/templates/hooks.json +47 -0
- package/templates/settings.json +81 -0
- package/templates/sticky-note-config.json +8 -0
- package/templates/sticky-note.json +5 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* on-stop.js — Handoff Summary Generation (V2)
|
|
5
|
+
*
|
|
6
|
+
* Hook: Stop (Claude Code)
|
|
7
|
+
* Generates a structured handoff_summary on the current thread.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
|
|
12
|
+
function safeExit() {
|
|
13
|
+
try {
|
|
14
|
+
process.stdout.write(JSON.stringify({ output: "" }) + "\n");
|
|
15
|
+
} catch (_) {
|
|
16
|
+
process.stdout.write('{"output":""}\n');
|
|
17
|
+
}
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let utils;
|
|
22
|
+
try {
|
|
23
|
+
utils = require("./sticky-utils.js");
|
|
24
|
+
} catch (_) {
|
|
25
|
+
safeExit();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
getMemoryPath,
|
|
30
|
+
loadJson,
|
|
31
|
+
saveJson,
|
|
32
|
+
appendAuditLine,
|
|
33
|
+
getUser,
|
|
34
|
+
getBranch,
|
|
35
|
+
getSessionId,
|
|
36
|
+
detectTool,
|
|
37
|
+
} = utils;
|
|
38
|
+
|
|
39
|
+
function buildHandoffSummary(thread, reason) {
|
|
40
|
+
const parts = [];
|
|
41
|
+
const files = thread.files_touched || [];
|
|
42
|
+
const workType = thread.work_type || "general";
|
|
43
|
+
const note = thread.last_note || "";
|
|
44
|
+
|
|
45
|
+
if (files.length) {
|
|
46
|
+
parts.push(`What done: ${workType} on ${files.slice(0, 5).join(", ")}`);
|
|
47
|
+
} else if (note) {
|
|
48
|
+
parts.push(`What done: ${note}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const failed = thread.failed_approaches || [];
|
|
52
|
+
if (failed.length) {
|
|
53
|
+
const descs = failed.slice(0, 3).map((a) => (a.description || "").substring(0, 60));
|
|
54
|
+
parts.push(`What failed: ${descs.join("; ")}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const narrative = thread.narrative || "";
|
|
58
|
+
if (narrative) {
|
|
59
|
+
parts.push(`Status: ${narrative.substring(0, 150)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (reason) {
|
|
63
|
+
parts.push(`Next: ${reason.substring(0, 100)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return parts.length ? parts.join(" | ") : "Session stopped -- no summary available";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function main() {
|
|
70
|
+
let hookInput = {};
|
|
71
|
+
try {
|
|
72
|
+
if (!process.stdin.isTTY) {
|
|
73
|
+
const raw = require("fs").readFileSync(0, "utf-8").trim();
|
|
74
|
+
if (raw) {
|
|
75
|
+
hookInput = JSON.parse(raw);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (_) {
|
|
79
|
+
hookInput = {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sessionId = getSessionId(hookInput);
|
|
83
|
+
const user = getUser();
|
|
84
|
+
const now = new Date().toISOString();
|
|
85
|
+
const reason = hookInput.reason || "";
|
|
86
|
+
|
|
87
|
+
const memoryPath = getMemoryPath();
|
|
88
|
+
const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
|
|
89
|
+
|
|
90
|
+
const threads = memory.threads || [];
|
|
91
|
+
let found = false;
|
|
92
|
+
|
|
93
|
+
for (const thread of threads) {
|
|
94
|
+
if (thread.session_id === sessionId && (thread.status === "open" || thread.status === "stuck")) {
|
|
95
|
+
thread.last_activity_at = now;
|
|
96
|
+
thread.handoff_summary = buildHandoffSummary(thread, reason);
|
|
97
|
+
if (reason) {
|
|
98
|
+
thread.last_note = reason.substring(0, 200);
|
|
99
|
+
}
|
|
100
|
+
found = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!found && sessionId !== "unknown") {
|
|
106
|
+
const thread = {
|
|
107
|
+
id: crypto.randomUUID(),
|
|
108
|
+
user,
|
|
109
|
+
project: memory.project || "",
|
|
110
|
+
status: "closed",
|
|
111
|
+
branch: getBranch(),
|
|
112
|
+
created_at: now,
|
|
113
|
+
closed_at: now,
|
|
114
|
+
last_activity_at: now,
|
|
115
|
+
files_touched: [],
|
|
116
|
+
last_note: reason ? reason.substring(0, 200) : "Session stopped",
|
|
117
|
+
narrative: "",
|
|
118
|
+
failed_approaches: [],
|
|
119
|
+
handoff_summary: buildHandoffSummary({}, reason),
|
|
120
|
+
related_session_ids: [],
|
|
121
|
+
tool: detectTool(hookInput),
|
|
122
|
+
session_id: sessionId,
|
|
123
|
+
};
|
|
124
|
+
threads.push(thread);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
appendAuditLine({
|
|
128
|
+
type: "stop",
|
|
129
|
+
user,
|
|
130
|
+
ts: now,
|
|
131
|
+
session_id: sessionId,
|
|
132
|
+
reason: reason ? reason.substring(0, 200) : "stop_checkpoint",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
saveJson(memoryPath, memory);
|
|
136
|
+
const reasonStr = reason ? reason.substring(0, 60) : "checkpoint";
|
|
137
|
+
const statusMsg = `[STICKY-NOTE] Session stopped - handoff saved (${reasonStr})`;
|
|
138
|
+
try {
|
|
139
|
+
process.stdout.write(JSON.stringify({ output: statusMsg }) + "\n");
|
|
140
|
+
} catch (_) {
|
|
141
|
+
process.stdout.write('{"output":""}\n');
|
|
142
|
+
}
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
main();
|
|
148
|
+
} catch (_) {
|
|
149
|
+
safeExit();
|
|
150
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* parse-transcript.js — Standalone Transcript Parser (V2)
|
|
5
|
+
*
|
|
6
|
+
* Extracts narrative + failed_approaches from a session transcript file.
|
|
7
|
+
* Used by sticky-codex.sh to process Codex stdout/stderr logs.
|
|
8
|
+
* Also usable standalone: node parse-transcript.js <transcript_path>
|
|
9
|
+
*
|
|
10
|
+
* Outputs JSON to stdout: {"narrative": "...", "failed_approaches": [...]}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const {
|
|
15
|
+
parseJsonlFile,
|
|
16
|
+
extractNarrativeFromEntries,
|
|
17
|
+
extractNarrativeFromText,
|
|
18
|
+
extractFailedFromEntries,
|
|
19
|
+
extractFailedFromText,
|
|
20
|
+
} = require("./sticky-utils.js");
|
|
21
|
+
|
|
22
|
+
function parsePlaintextTranscript(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
return fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
25
|
+
} catch (_) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseTranscript(filePath) {
|
|
31
|
+
const entries = parseJsonlFile(filePath);
|
|
32
|
+
if (entries.length) {
|
|
33
|
+
return {
|
|
34
|
+
narrative: extractNarrativeFromEntries(entries),
|
|
35
|
+
failed_approaches: extractFailedFromEntries(entries),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lines = parsePlaintextTranscript(filePath);
|
|
40
|
+
if (lines.length) {
|
|
41
|
+
return {
|
|
42
|
+
narrative: extractNarrativeFromText(lines),
|
|
43
|
+
failed_approaches: extractFailedFromText(lines),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { narrative: "", failed_approaches: [] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function main() {
|
|
51
|
+
if (process.argv.length < 3) {
|
|
52
|
+
process.stderr.write("Usage: parse-transcript.js <transcript_path>\n");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const transcriptPath = process.argv[2];
|
|
57
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
58
|
+
process.stderr.write(`Warning: transcript file not found: ${transcriptPath}\n`);
|
|
59
|
+
console.log(JSON.stringify({ narrative: "", failed_approaches: [] }));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = parseTranscript(transcriptPath);
|
|
64
|
+
console.log(JSON.stringify(result));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* post-rewrite.js — Git post-rewrite hook for Sticky Note
|
|
5
|
+
*
|
|
6
|
+
* Copies Git Notes from old commit SHAs to new SHAs when commits
|
|
7
|
+
* are rewritten via rebase or amend. This keeps session attribution
|
|
8
|
+
* alive through history rewrites.
|
|
9
|
+
*
|
|
10
|
+
* Install: place in .git/hooks/post-rewrite (or use core.hooksPath)
|
|
11
|
+
* Git calls this with: post-rewrite <command>
|
|
12
|
+
* Stdin receives: <old-sha> <new-sha>\n per rewritten commit
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
|
|
18
|
+
let gitNotes;
|
|
19
|
+
try {
|
|
20
|
+
// Try hooks dir first (deployed)
|
|
21
|
+
gitNotes = require(path.join(__dirname, "..", ".claude", "hooks", "sticky-git-notes.js"));
|
|
22
|
+
} catch (_) {
|
|
23
|
+
try {
|
|
24
|
+
// Try relative to .git/hooks/
|
|
25
|
+
gitNotes = require(path.join(__dirname, "..", "..", ".claude", "hooks", "sticky-git-notes.js"));
|
|
26
|
+
} catch (_) {
|
|
27
|
+
try {
|
|
28
|
+
// Try templates
|
|
29
|
+
gitNotes = require(path.join(__dirname, "..", "..", "templates", "hooks", "sticky-git-notes.js"));
|
|
30
|
+
} catch (_) {
|
|
31
|
+
// Can't load git-notes module — exit silently
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function main() {
|
|
38
|
+
// Read stdin: "old_sha new_sha\n" pairs
|
|
39
|
+
let stdin = "";
|
|
40
|
+
try {
|
|
41
|
+
stdin = fs.readFileSync(0, "utf-8");
|
|
42
|
+
} catch (_) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!stdin.trim()) process.exit(0);
|
|
47
|
+
|
|
48
|
+
const pairs = gitNotes.parsePostRewriteInput(stdin);
|
|
49
|
+
if (pairs.length === 0) process.exit(0);
|
|
50
|
+
|
|
51
|
+
const copied = gitNotes.copyNotesForRewrite(pairs);
|
|
52
|
+
|
|
53
|
+
if (copied > 0 && process.env.STICKY_NOTE_DEBUG) {
|
|
54
|
+
process.stderr.write(`[sticky-note] Copied ${copied} note(s) to rewritten commits\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
main();
|
|
60
|
+
} catch (_) {
|
|
61
|
+
// Never fail a git hook
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* pre-tool-use.js — Lazy Injection via PreToolUse Hook (V2.5)
|
|
5
|
+
*
|
|
6
|
+
* Hook: PreToolUse (Claude Code) / preToolUse (Copilot CLI)
|
|
7
|
+
* Fires BEFORE each tool call. If the tool targets a file, runs
|
|
8
|
+
* git blame → attribution engine to find threads that authored
|
|
9
|
+
* lines in that file, and injects them as system context with
|
|
10
|
+
* line-range detail.
|
|
11
|
+
*
|
|
12
|
+
* Dedup: each thread is only injected once per session via the
|
|
13
|
+
* .sticky-injected tracking file.
|
|
14
|
+
*
|
|
15
|
+
* No external dependencies — uses built-in git blame.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── Output helpers ────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function _isCopilotCli() {
|
|
21
|
+
return process.argv.includes("--copilot-cli") || !!process.env.COPILOT_CLI;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _emit(text) {
|
|
25
|
+
if (text === undefined) text = "";
|
|
26
|
+
if (_isCopilotCli()) {
|
|
27
|
+
process.stdout.write(JSON.stringify({ additionalContext: text }) + "\n");
|
|
28
|
+
} else {
|
|
29
|
+
process.stdout.write(JSON.stringify({ output: text }) + "\n");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _safeExit() {
|
|
34
|
+
try {
|
|
35
|
+
_emit("");
|
|
36
|
+
} catch (_) {
|
|
37
|
+
process.stdout.write('{"output": ""}\n');
|
|
38
|
+
}
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Import dependencies ───────────────────────────────────
|
|
43
|
+
|
|
44
|
+
let utils, attribution;
|
|
45
|
+
try {
|
|
46
|
+
utils = require("./sticky-utils.js");
|
|
47
|
+
} catch (_) {
|
|
48
|
+
_safeExit();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
attribution = require("./sticky-attribution.js");
|
|
53
|
+
} catch (_) {
|
|
54
|
+
// Attribution engine not available — skip lazy injection
|
|
55
|
+
_safeExit();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
getSessionId,
|
|
60
|
+
getUser,
|
|
61
|
+
appendAuditLine,
|
|
62
|
+
isThreadInjected,
|
|
63
|
+
markThreadInjected,
|
|
64
|
+
} = utils;
|
|
65
|
+
|
|
66
|
+
// ── File path extraction from tool input ──────────────────
|
|
67
|
+
|
|
68
|
+
function extractFilePath(hookInput) {
|
|
69
|
+
const toolInput = hookInput.tool_input || hookInput.input || hookInput.toolArgs || {};
|
|
70
|
+
|
|
71
|
+
if (typeof toolInput === "object" && toolInput !== null) {
|
|
72
|
+
for (const key of ["file_path", "filePath", "path", "file", "filename"]) {
|
|
73
|
+
if (toolInput[key] && typeof toolInput[key] === "string") {
|
|
74
|
+
return toolInput[key];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (toolInput.command && typeof toolInput.command === "string") {
|
|
78
|
+
return extractFileFromCommand(toolInput.command);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractFileFromCommand(command) {
|
|
86
|
+
const tokens = command.split(/\s+/);
|
|
87
|
+
for (const token of tokens) {
|
|
88
|
+
if (token.includes("/") || token.includes("\\")) {
|
|
89
|
+
if (/\.\w{1,10}$/.test(token)) {
|
|
90
|
+
return token;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Thread formatting for injection ───────────────────────
|
|
98
|
+
|
|
99
|
+
function formatThreadForInjection(threadData, file) {
|
|
100
|
+
const thread = threadData.thread || threadData;
|
|
101
|
+
const lineRanges = threadData.line_ranges || threadData._line_ranges || [];
|
|
102
|
+
const tier = threadData.tier || threadData._tier || "";
|
|
103
|
+
|
|
104
|
+
const user = thread.user || thread.author || "unknown";
|
|
105
|
+
const status = thread.status || "open";
|
|
106
|
+
const statusTag = status === "stuck" ? "[STUCK]" : status === "open" ? "[OPEN]" : "[CLOSED]";
|
|
107
|
+
const branch = thread.branch || "";
|
|
108
|
+
const branchStr = branch ? ` (${branch})` : "";
|
|
109
|
+
|
|
110
|
+
const lineStr = lineRanges.length > 0 ? ` [lines ${lineRanges.join(", ")}]` : "";
|
|
111
|
+
|
|
112
|
+
const lines = [
|
|
113
|
+
`[STICKY-NOTE] ${statusTag} ${user}'s thread on ${file}${lineStr}${branchStr}:`,
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
if (thread.narrative) {
|
|
117
|
+
lines.push(thread.narrative.substring(0, 200));
|
|
118
|
+
} else if (thread.last_note) {
|
|
119
|
+
lines.push(thread.last_note.substring(0, 150));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const failed = thread.failed_approaches || [];
|
|
123
|
+
if (failed.length > 0) {
|
|
124
|
+
lines.push(`[!] ${failed.length} failed approach(es) - check thread ${(thread.id || "").substring(0, 8)} for details`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (thread.handoff_summary) {
|
|
128
|
+
lines.push(`Handoff: ${thread.handoff_summary.substring(0, 150)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Main ──────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function main() {
|
|
137
|
+
let hookInput = {};
|
|
138
|
+
try {
|
|
139
|
+
const raw = require("fs").readFileSync(0, "utf-8");
|
|
140
|
+
if (raw.trim()) {
|
|
141
|
+
hookInput = JSON.parse(raw);
|
|
142
|
+
}
|
|
143
|
+
} catch (_) {
|
|
144
|
+
hookInput = {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sessionId = getSessionId(hookInput);
|
|
148
|
+
|
|
149
|
+
// Extract file path from tool input
|
|
150
|
+
const filePath = extractFilePath(hookInput);
|
|
151
|
+
if (!filePath) {
|
|
152
|
+
_emit("");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Run attribution engine: get threads for this file with line ranges
|
|
157
|
+
let fileAttr;
|
|
158
|
+
try {
|
|
159
|
+
fileAttr = attribution.getFileAttribution(filePath);
|
|
160
|
+
} catch (_) {
|
|
161
|
+
_emit("");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!fileAttr || !fileAttr.threads || fileAttr.threads.length === 0) {
|
|
166
|
+
_emit("");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Filter out already-injected threads
|
|
171
|
+
const newThreads = fileAttr.threads.filter(
|
|
172
|
+
(t) => !isThreadInjected(t.thread ? t.thread.id : t.id)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (newThreads.length === 0) {
|
|
176
|
+
_emit("");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Build injection output with line-range detail
|
|
181
|
+
const outputParts = [];
|
|
182
|
+
for (const threadData of newThreads) {
|
|
183
|
+
const threadId = threadData.thread ? threadData.thread.id : threadData.id;
|
|
184
|
+
outputParts.push(formatThreadForInjection(threadData, filePath));
|
|
185
|
+
markThreadInjected(threadId, sessionId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const output = outputParts.join("\n\n");
|
|
189
|
+
|
|
190
|
+
// Audit the injection
|
|
191
|
+
try {
|
|
192
|
+
appendAuditLine({
|
|
193
|
+
type: "lazy_inject",
|
|
194
|
+
user: getUser(),
|
|
195
|
+
ts: new Date().toISOString(),
|
|
196
|
+
session_id: sessionId,
|
|
197
|
+
file: filePath,
|
|
198
|
+
threads_injected: newThreads.length,
|
|
199
|
+
thread_ids: newThreads.map((t) => {
|
|
200
|
+
const id = t.thread ? t.thread.id : t.id;
|
|
201
|
+
return (id || "").substring(0, 8);
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
} catch (_) {
|
|
205
|
+
// ignore
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
_emit(output);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Entry point ───────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
if (require.main === module) {
|
|
214
|
+
try {
|
|
215
|
+
main();
|
|
216
|
+
} catch (_) {
|
|
217
|
+
_safeExit();
|
|
218
|
+
}
|
|
219
|
+
}
|