@primitive.ai/prim 0.1.0-alpha.8 → 0.1.0-alpha.81

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.
Files changed (45) hide show
  1. package/README.md +295 -37
  2. package/SKILL.md +269 -0
  3. package/dist/chunk-3EEFL6NJ.js +529 -0
  4. package/dist/chunk-4CZTNXIS.js +505 -0
  5. package/dist/chunk-4GLVJLEQ.js +55 -0
  6. package/dist/chunk-BQUJ4AFJ.js +771 -0
  7. package/dist/chunk-CUE2LKLR.js +155 -0
  8. package/dist/chunk-EZXEWFDG.js +210 -0
  9. package/dist/chunk-FI3LIRCB.js +262 -0
  10. package/dist/chunk-HWSS4BV5.js +1400 -0
  11. package/dist/chunk-LEMX3IOG.js +79 -0
  12. package/dist/chunk-LMRYZL3D.js +209 -0
  13. package/dist/chunk-OUXH3UEP.js +247 -0
  14. package/dist/chunk-PMRDE2DA.js +310 -0
  15. package/dist/chunk-RO3F6JYV.js +126 -0
  16. package/dist/chunk-TJTVHDJ5.js +321 -0
  17. package/dist/chunk-WB7AUZ4C.js +52 -0
  18. package/dist/chunk-XGADQPCI.js +26 -0
  19. package/dist/chunk-YWO7O6QG.js +327 -0
  20. package/dist/chunk-Z7YPFWAZ.js +917 -0
  21. package/dist/chunk-ZOSNEO5N.js +77 -0
  22. package/dist/daemon/server.js +15329 -0
  23. package/dist/hook-runtime/daemon/server.js +15329 -0
  24. package/dist/hook-runtime/hooks/post-commit.js +1128 -0
  25. package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
  26. package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
  27. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  28. package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
  29. package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
  30. package/dist/hook-runtime/hooks/session-end.js +495 -0
  31. package/dist/hook-runtime/hooks/session-start.js +11313 -0
  32. package/dist/hook-runtime/index.js +44696 -0
  33. package/dist/hook-runtime/statusline-main.js +765 -0
  34. package/dist/hooks/post-commit.js +179 -0
  35. package/dist/hooks/post-rewrite.js +197 -0
  36. package/dist/hooks/post-tool-use.js +250 -0
  37. package/dist/hooks/pre-commit.js +21 -74
  38. package/dist/hooks/pre-tool-use.js +348 -0
  39. package/dist/hooks/prim-hook.js +195 -0
  40. package/dist/hooks/session-end.js +70 -0
  41. package/dist/hooks/session-start.js +299 -0
  42. package/dist/index.js +20981 -519
  43. package/dist/statusline-main.js +765 -0
  44. package/package.json +35 -13
  45. package/dist/chunk-3APLWTLB.js +0 -165
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ commitAttributionFromEnvironment,
4
+ toCommitMove
5
+ } from "../chunk-OUXH3UEP.js";
6
+ import {
7
+ getOrCreateWorkspaceId
8
+ } from "../chunk-EZXEWFDG.js";
9
+ import {
10
+ appendMove,
11
+ resolveOrg
12
+ } from "../chunk-YWO7O6QG.js";
13
+ import "../chunk-FI3LIRCB.js";
14
+ import "../chunk-LEMX3IOG.js";
15
+ import {
16
+ githubRepositoryFullName,
17
+ isRepoActiveForCapture,
18
+ repoSyncId,
19
+ resolveRepositoryContext
20
+ } from "../chunk-TJTVHDJ5.js";
21
+ import "../chunk-Z7YPFWAZ.js";
22
+ import "../chunk-3EEFL6NJ.js";
23
+
24
+ // src/hooks/post-commit.ts
25
+ import { execFileSync, spawn } from "child_process";
26
+ import { lstatSync, readFileSync } from "fs";
27
+ import { basename, dirname, isAbsolute, join } from "path";
28
+ import { fileURLToPath } from "url";
29
+ var here = dirname(fileURLToPath(import.meta.url));
30
+ var GIT_TIMEOUT_MS = 1e3;
31
+ var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
32
+ var OBSERVATION_MARKER_RE = /^prim-post-commit-observed\.[A-Za-z0-9]+$/u;
33
+ var MAX_OBSERVATION_MARKER_AGE_MS = 10 * 60 * 1e3;
34
+ var MAX_OBSERVATION_MARKER_FUTURE_SKEW_MS = 5e3;
35
+ function capturedAtFromLauncher(env = process.env, now = Date.now()) {
36
+ const path = env.PRIM_COMMIT_OBSERVED_FILE;
37
+ if (!path || !isAbsolute(path) || !OBSERVATION_MARKER_RE.test(basename(path))) return;
38
+ try {
39
+ const stat = lstatSync(path);
40
+ const capturedAt = Math.trunc(stat.mtimeMs);
41
+ const uid = process.getuid?.();
42
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== 0 || stat.nlink !== 1 || (stat.mode & 63) !== 0 || uid !== void 0 && stat.uid !== uid || !Number.isSafeInteger(capturedAt) || capturedAt <= 0 || capturedAt < now - MAX_OBSERVATION_MARKER_AGE_MS || capturedAt > now + MAX_OBSERVATION_MARKER_FUTURE_SKEW_MS) {
43
+ return;
44
+ }
45
+ return capturedAt;
46
+ } catch {
47
+ return;
48
+ }
49
+ }
50
+ function gitText(args) {
51
+ try {
52
+ return execFileSync("git", args, {
53
+ encoding: "utf-8",
54
+ stdio: ["ignore", "pipe", "ignore"],
55
+ timeout: GIT_TIMEOUT_MS
56
+ }).trim();
57
+ } catch {
58
+ return;
59
+ }
60
+ }
61
+ function gitBytes(args) {
62
+ try {
63
+ return execFileSync("git", args, {
64
+ encoding: "buffer",
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ timeout: GIT_TIMEOUT_MS
67
+ });
68
+ } catch {
69
+ return;
70
+ }
71
+ }
72
+ function parseNulPaths(value) {
73
+ if (value.length === 0) return { files: [], complete: true };
74
+ const chunks = [];
75
+ let start = 0;
76
+ for (let i = 0; i < value.length; i += 1) {
77
+ if (value[i] !== 0) continue;
78
+ chunks.push(value.subarray(start, i));
79
+ start = i + 1;
80
+ }
81
+ if (start !== value.length || chunks.some((chunk) => chunk.length === 0)) {
82
+ return { files: [], complete: false };
83
+ }
84
+ const files = [];
85
+ for (const chunk of chunks) {
86
+ const file = chunk.toString("utf8");
87
+ if (!Buffer.from(file, "utf8").equals(chunk)) {
88
+ return { files: [], complete: false };
89
+ }
90
+ files.push(file);
91
+ }
92
+ return { files: [...new Set(files)], complete: true };
93
+ }
94
+ function readCommit() {
95
+ const snapshotProvided = process.env.PRIM_COMMIT_SHA !== void 0;
96
+ const sha = snapshotProvided ? process.env.PRIM_COMMIT_SHA : gitText(["rev-parse", "HEAD"]);
97
+ if (!sha || !FULL_SHA_RE.test(sha)) {
98
+ return null;
99
+ }
100
+ const rawBranch = process.env.PRIM_COMMIT_BRANCH !== void 0 ? process.env.PRIM_COMMIT_BRANCH : gitText(["rev-parse", "--abbrev-ref", "HEAD"]);
101
+ const branch = rawBranch && rawBranch === rawBranch.trim() && ![...rawBranch].some((char) => char < " " || char === "\x7F") ? rawBranch : void 0;
102
+ const rawFiles = gitBytes([
103
+ "diff-tree",
104
+ "--no-commit-id",
105
+ "--name-only",
106
+ "-z",
107
+ "-r",
108
+ "-m",
109
+ "--root",
110
+ sha
111
+ ]);
112
+ const { files, complete } = rawFiles === void 0 ? { files: [], complete: false } : parseNulPaths(rawFiles);
113
+ return {
114
+ sha,
115
+ parentSha: gitText(["rev-parse", "--verify", "--quiet", `${sha}^`]) || void 0,
116
+ branch: branch && branch !== "HEAD" ? branch : void 0,
117
+ files,
118
+ filesComplete: complete
119
+ };
120
+ }
121
+ function resolveCliVersion() {
122
+ try {
123
+ const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
124
+ return pkg.version ?? "unknown";
125
+ } catch {
126
+ return "unknown";
127
+ }
128
+ }
129
+ function spawnBackgroundFlush() {
130
+ const entry = join(here, "..", "index.js");
131
+ spawn(process.execPath, [entry, "moves", "flush"], {
132
+ detached: true,
133
+ stdio: "ignore"
134
+ }).unref();
135
+ }
136
+ function runPostCommit() {
137
+ const cwd = gitText(["rev-parse", "--show-toplevel"]) ?? process.cwd();
138
+ if (!isRepoActiveForCapture(cwd)) {
139
+ return;
140
+ }
141
+ const capturedAt = capturedAtFromLauncher();
142
+ if (process.env.PRIM_COMMIT_OBSERVED_FILE !== void 0 && capturedAt === void 0) {
143
+ return;
144
+ }
145
+ const commit = readCommit();
146
+ if (commit) {
147
+ const repository = resolveRepositoryContext(cwd);
148
+ const identity = getOrCreateWorkspaceId(cwd);
149
+ const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
150
+ const attribution = commitAttributionFromEnvironment(process.env, workspaceId);
151
+ const move = toCommitMove(commit, resolveCliVersion(), cwd, {
152
+ repository,
153
+ repoFullName: githubRepositoryFullName(cwd) ?? void 0,
154
+ repoSyncId: repoSyncId(cwd),
155
+ workspaceId,
156
+ attribution,
157
+ ...capturedAt === void 0 ? {} : { capturedAt }
158
+ });
159
+ const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd });
160
+ appendMove(move, orgId);
161
+ spawnBackgroundFlush();
162
+ }
163
+ }
164
+ if (!process.env.VITEST) {
165
+ try {
166
+ runPostCommit();
167
+ } catch (err) {
168
+ if (process.env.PRIM_HOOK_DEBUG) {
169
+ const detail = err instanceof Error ? err.message : String(err);
170
+ process.stderr.write(`[prim-post-commit] capture failed: ${detail}
171
+ `);
172
+ }
173
+ }
174
+ process.exit(0);
175
+ }
176
+ export {
177
+ capturedAtFromLauncher,
178
+ runPostCommit
179
+ };
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ canonicalRewritePairs,
4
+ toRewriteMove
5
+ } from "../chunk-OUXH3UEP.js";
6
+ import {
7
+ getOrCreateWorkspaceId
8
+ } from "../chunk-EZXEWFDG.js";
9
+ import {
10
+ appendMove,
11
+ resolveOrg
12
+ } from "../chunk-YWO7O6QG.js";
13
+ import "../chunk-FI3LIRCB.js";
14
+ import "../chunk-LEMX3IOG.js";
15
+ import {
16
+ githubRepositoryFullName,
17
+ isRepoActiveForCapture,
18
+ repoSyncId,
19
+ resolveRepositoryContext
20
+ } from "../chunk-TJTVHDJ5.js";
21
+ import "../chunk-Z7YPFWAZ.js";
22
+ import "../chunk-3EEFL6NJ.js";
23
+
24
+ // src/hooks/post-rewrite.ts
25
+ import { execFileSync, spawn } from "child_process";
26
+ import {
27
+ constants,
28
+ closeSync,
29
+ fstatSync,
30
+ lstatSync,
31
+ openSync,
32
+ readFileSync,
33
+ unlinkSync
34
+ } from "fs";
35
+ import { basename, dirname, isAbsolute, join } from "path";
36
+ import { fileURLToPath } from "url";
37
+ var here = dirname(fileURLToPath(import.meta.url));
38
+ var GIT_TIMEOUT_MS = 1e3;
39
+ var MAX_PAIRS_FILE_BYTES = 1048576;
40
+ var MAX_PAIRS_FILE_AGE_MS = 10 * 60 * 1e3;
41
+ var MAX_PAIRS_FILE_FUTURE_SKEW_MS = 5e3;
42
+ var PAIRS_FILE_RE = /^prim-post-rewrite-pairs\.[A-Za-z0-9]+$/u;
43
+ var FULL_SHA_SOURCE = "(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})";
44
+ var REWRITE_PAIR_RE = new RegExp(
45
+ `^(${FULL_SHA_SOURCE}) (${FULL_SHA_SOURCE})(?: ([^\\s]+))?$`,
46
+ "u"
47
+ );
48
+ function parseRewritePairs(value) {
49
+ if (value.length === 0) return [];
50
+ const body = value.endsWith("\n") ? value.slice(0, -1) : value;
51
+ if (body.length === 0) return [];
52
+ const parsed = [];
53
+ for (const line of body.split("\n")) {
54
+ const match = REWRITE_PAIR_RE.exec(line);
55
+ if (!match?.[1] || !match[2]) return;
56
+ parsed.push({ oldSha: match[1], newSha: match[2] });
57
+ }
58
+ return canonicalRewritePairs(parsed) ?? void 0;
59
+ }
60
+ function privatePairsFileStat(stat, now) {
61
+ const capturedAt = Math.trunc(stat.mtimeMs);
62
+ const uid = process.getuid?.();
63
+ return stat.isFile() && !stat.isSymbolicLink() && stat.size <= MAX_PAIRS_FILE_BYTES && stat.nlink === 1 && (stat.mode & 511) === 384 && (uid === void 0 || stat.uid === uid) && Number.isSafeInteger(capturedAt) && capturedAt > 0 && capturedAt >= now - MAX_PAIRS_FILE_AGE_MS && capturedAt <= now + MAX_PAIRS_FILE_FUTURE_SKEW_MS;
64
+ }
65
+ function rewritePairsFromLauncher(env = process.env, now = Date.now()) {
66
+ const path = env.PRIM_REWRITE_PAIRS_FILE;
67
+ if (!path || !isAbsolute(path) || !PAIRS_FILE_RE.test(basename(path))) return;
68
+ let fd;
69
+ try {
70
+ const pathStat = lstatSync(path);
71
+ if (!privatePairsFileStat(pathStat, now)) return;
72
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
73
+ const openStat = fstatSync(fd);
74
+ if (!privatePairsFileStat(openStat, now) || openStat.dev !== pathStat.dev || openStat.ino !== pathStat.ino) {
75
+ return;
76
+ }
77
+ const bytes = readFileSync(fd);
78
+ if (bytes.length !== openStat.size) return;
79
+ let text;
80
+ try {
81
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
82
+ } catch {
83
+ return;
84
+ }
85
+ const pairs = parseRewritePairs(text);
86
+ if (!pairs) return;
87
+ return {
88
+ path,
89
+ dev: openStat.dev,
90
+ ino: openStat.ino,
91
+ capturedAt: Math.trunc(openStat.mtimeMs),
92
+ pairs
93
+ };
94
+ } catch {
95
+ return;
96
+ } finally {
97
+ if (fd !== void 0) {
98
+ try {
99
+ closeSync(fd);
100
+ } catch {
101
+ }
102
+ }
103
+ }
104
+ }
105
+ function unlinkSnapshot(snapshot) {
106
+ try {
107
+ const current = lstatSync(snapshot.path);
108
+ if (current.isFile() && !current.isSymbolicLink() && current.dev === snapshot.dev && current.ino === snapshot.ino) {
109
+ unlinkSync(snapshot.path);
110
+ }
111
+ } catch {
112
+ }
113
+ }
114
+ function gitText(args) {
115
+ try {
116
+ return execFileSync("git", args, {
117
+ encoding: "utf-8",
118
+ stdio: ["ignore", "pipe", "ignore"],
119
+ timeout: GIT_TIMEOUT_MS
120
+ }).trim();
121
+ } catch {
122
+ return;
123
+ }
124
+ }
125
+ function validSource(value) {
126
+ return value === "amend" || value === "rebase";
127
+ }
128
+ function validBranch(value) {
129
+ return value && value === value.trim() && ![...value].some((char) => char < " " || char === "\x7F") ? value : void 0;
130
+ }
131
+ function resolveCliVersion() {
132
+ try {
133
+ const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
134
+ return pkg.version ?? "unknown";
135
+ } catch {
136
+ return "unknown";
137
+ }
138
+ }
139
+ function spawnBackgroundFlush() {
140
+ const entry = join(here, "..", "index.js");
141
+ spawn(process.execPath, [entry, "moves", "flush"], {
142
+ detached: true,
143
+ stdio: "ignore"
144
+ }).unref();
145
+ }
146
+ function runPostRewrite() {
147
+ const snapshot = rewritePairsFromLauncher();
148
+ if (!snapshot) return;
149
+ try {
150
+ const source = process.env.PRIM_REWRITE_SOURCE;
151
+ if (!validSource(source) || snapshot.pairs.length === 0) return;
152
+ const cwd = gitText(["rev-parse", "--show-toplevel"]) ?? process.cwd();
153
+ if (!isRepoActiveForCapture(cwd)) return;
154
+ const repository = resolveRepositoryContext(cwd);
155
+ const identity = getOrCreateWorkspaceId(cwd);
156
+ const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
157
+ const moves = toRewriteMove(
158
+ {
159
+ source,
160
+ pairs: snapshot.pairs,
161
+ branch: validBranch(process.env.PRIM_REWRITE_BRANCH)
162
+ },
163
+ resolveCliVersion(),
164
+ cwd,
165
+ {
166
+ repository,
167
+ repoFullName: githubRepositoryFullName(cwd) ?? void 0,
168
+ repoSyncId: repoSyncId(cwd),
169
+ workspaceId,
170
+ capturedAt: snapshot.capturedAt
171
+ }
172
+ );
173
+ if (moves.length === 0) return;
174
+ const { orgId } = resolveOrg({ sessionId: "", cwd });
175
+ for (const move of moves) appendMove(move, orgId);
176
+ spawnBackgroundFlush();
177
+ } finally {
178
+ unlinkSnapshot(snapshot);
179
+ }
180
+ }
181
+ if (!process.env.VITEST) {
182
+ try {
183
+ runPostRewrite();
184
+ } catch (error) {
185
+ if (process.env.PRIM_HOOK_DEBUG) {
186
+ const detail = error instanceof Error ? error.message : String(error);
187
+ process.stderr.write(`[prim-post-rewrite] capture failed: ${detail}
188
+ `);
189
+ }
190
+ }
191
+ process.exit(0);
192
+ }
193
+ export {
194
+ parseRewritePairs,
195
+ rewritePairsFromLauncher,
196
+ runPostRewrite
197
+ };
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ requireDurableIngestAcknowledgement
4
+ } from "../chunk-XGADQPCI.js";
5
+ import {
6
+ enrichHookPayloadWithFileRefs,
7
+ preserveHookFileMetadata
8
+ } from "../chunk-ZOSNEO5N.js";
9
+ import {
10
+ postToolInvocationId,
11
+ toMove,
12
+ toolOutcomeFor
13
+ } from "../chunk-OUXH3UEP.js";
14
+ import {
15
+ getOrCreateWorkspaceId
16
+ } from "../chunk-EZXEWFDG.js";
17
+ import {
18
+ appendMove,
19
+ resolveOrg
20
+ } from "../chunk-YWO7O6QG.js";
21
+ import "../chunk-BQUJ4AFJ.js";
22
+ import {
23
+ appendCodexContext,
24
+ prepareCodexContext
25
+ } from "../chunk-4CZTNXIS.js";
26
+ import "../chunk-PMRDE2DA.js";
27
+ import {
28
+ scrubFromCwd
29
+ } from "../chunk-FI3LIRCB.js";
30
+ import {
31
+ bold,
32
+ color
33
+ } from "../chunk-LEMX3IOG.js";
34
+ import {
35
+ isRepoActiveForCapture,
36
+ repoSyncId,
37
+ resolveRepositoryContext
38
+ } from "../chunk-TJTVHDJ5.js";
39
+ import {
40
+ getClient,
41
+ terminalSafeLine
42
+ } from "../chunk-Z7YPFWAZ.js";
43
+ import {
44
+ normalizeEnvelope,
45
+ parseAgent
46
+ } from "../chunk-WB7AUZ4C.js";
47
+ import "../chunk-CUE2LKLR.js";
48
+ import "../chunk-3EEFL6NJ.js";
49
+
50
+ // src/hooks/post-tool-use.ts
51
+ import { readFileSync } from "fs";
52
+ import { dirname, join } from "path";
53
+ import { fileURLToPath } from "url";
54
+
55
+ // src/hooks/post-tool-delivery.ts
56
+ var INGEST_TIMEOUT_MS = 4e3;
57
+ async function deliverPostToolMove(move, orgId) {
58
+ appendMove(move, orgId);
59
+ const response = await getClient().post(
60
+ "/api/cli/moves/ingest",
61
+ { batch: [move] },
62
+ { signal: AbortSignal.timeout(INGEST_TIMEOUT_MS) }
63
+ );
64
+ return requireDurableIngestAcknowledgement(response, 1);
65
+ }
66
+
67
+ // src/hooks/verdict-footer.ts
68
+ function renderVerdictFooter(ctx) {
69
+ const successPrefix = color("\u2713 Conflict caught before merge", "green");
70
+ const savedCount = `${String(ctx.decisionsSaved)}${ctx.decisionsSavedTruncated ? "+" : ""}`;
71
+ const savedFragment = `${savedCount} decisions saved`;
72
+ const intentFragment = `${bold(terminalSafeLine(ctx.author))}'s intent preserved`;
73
+ return `${successPrefix} \xB7 ${savedFragment} \xB7 ${intentFragment}`;
74
+ }
75
+ function isVerdictFooterContext(value) {
76
+ if (typeof value !== "object" || value === null) {
77
+ return false;
78
+ }
79
+ const v = value;
80
+ return typeof v.author === "string" && typeof v.intent === "string" && typeof v.decisionsSaved === "number" && typeof v.decisionsSavedTruncated === "boolean" && typeof v.decisionId === "string";
81
+ }
82
+
83
+ // src/hooks/post-tool-use.ts
84
+ var STDIN_TIMEOUT_MS = 1e3;
85
+ var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
86
+ var CODEX_EDITING_TOOLS = /* @__PURE__ */ new Set(["apply_patch", "Bash"]);
87
+ var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
88
+ function editingToolsFor(agent) {
89
+ if (agent === "codex") {
90
+ return CODEX_EDITING_TOOLS;
91
+ }
92
+ if (agent === "hermes") {
93
+ return HERMES_EDITING_TOOLS;
94
+ }
95
+ return EDITING_TOOLS;
96
+ }
97
+ var here = dirname(fileURLToPath(import.meta.url));
98
+ function resolveCliVersion() {
99
+ try {
100
+ const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
101
+ return pkg.version ?? "unknown";
102
+ } catch {
103
+ return "unknown";
104
+ }
105
+ }
106
+ function readStdin() {
107
+ return new Promise((resolve, reject) => {
108
+ const chunks = [];
109
+ const timer = setTimeout(() => {
110
+ reject(new Error("stdin read timeout"));
111
+ }, STDIN_TIMEOUT_MS);
112
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
113
+ process.stdin.on("end", () => {
114
+ clearTimeout(timer);
115
+ resolve(Buffer.concat(chunks).toString("utf-8"));
116
+ });
117
+ process.stdin.on("error", (err) => {
118
+ clearTimeout(timer);
119
+ reject(err);
120
+ });
121
+ });
122
+ }
123
+ async function emit(output = {}) {
124
+ return await new Promise((resolve) => {
125
+ try {
126
+ process.stdout.write(`${JSON.stringify(output)}
127
+ `, (error) => resolve(!error));
128
+ } catch {
129
+ resolve(false);
130
+ }
131
+ });
132
+ }
133
+ async function emitWithAcknowledgment(output, acknowledge) {
134
+ const handedOff = await emit(output);
135
+ await acknowledge?.(handedOff);
136
+ }
137
+ function debug(msg) {
138
+ if (process.env.PRIM_HOOK_VERBOSE === "1") {
139
+ process.stderr.write(`[prim-post-tool-use] ${msg}
140
+ `);
141
+ }
142
+ }
143
+ async function main() {
144
+ const agent = parseAgent(process.argv);
145
+ let raw;
146
+ try {
147
+ raw = await readStdin();
148
+ } catch {
149
+ await emit();
150
+ return;
151
+ }
152
+ let parsed;
153
+ try {
154
+ parsed = normalizeEnvelope(JSON.parse(raw), agent);
155
+ } catch {
156
+ await emit();
157
+ return;
158
+ }
159
+ let envelope = parsed;
160
+ const isToolResult = envelope.hook_event_name === "PostToolUse" || agent === "claude_code" && envelope.hook_event_name === "PostToolUseFailure";
161
+ const isHermesDenial = agent === "hermes" && envelope.hook_event_name === "post_approval_response" && toolOutcomeFor(parsed, agent) === "prevented";
162
+ if (!isToolResult && !isHermesDenial) {
163
+ await emit();
164
+ return;
165
+ }
166
+ const invocationId = postToolInvocationId(parsed, agent);
167
+ if (isHermesDenial && !invocationId) {
168
+ await emit();
169
+ return;
170
+ }
171
+ const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
172
+ if (!isHermesDenial && !editingToolsFor(agent).has(toolName)) {
173
+ await emit();
174
+ return;
175
+ }
176
+ if (isHermesDenial && (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) && typeof envelope.extra?.session_key === "string" && envelope.extra.session_key.length > 0) {
177
+ parsed = { ...parsed, session_id: envelope.extra.session_key };
178
+ envelope = parsed;
179
+ }
180
+ if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
181
+ await emit();
182
+ return;
183
+ }
184
+ const cwd = parsed.cwd ?? process.cwd();
185
+ if (!isRepoActiveForCapture(cwd)) {
186
+ await emit();
187
+ return;
188
+ }
189
+ const resolvedRepository = resolveRepositoryContext(cwd);
190
+ if (!resolvedRepository) {
191
+ await emit();
192
+ return;
193
+ }
194
+ const repository = { ...resolvedRepository, repoSyncId: repoSyncId(cwd) };
195
+ const enrichment = enrichHookPayloadWithFileRefs({
196
+ parsed,
197
+ agent,
198
+ cwd,
199
+ repository
200
+ });
201
+ const { resolution } = enrichment;
202
+ if (!isHermesDenial) {
203
+ if (resolution.shellMutation === "none") {
204
+ await emit();
205
+ return;
206
+ }
207
+ if (resolution.shellMutation === void 0 && resolution.fileRefs.length === 0) {
208
+ await emit();
209
+ return;
210
+ }
211
+ }
212
+ const enriched = enrichment.parsed;
213
+ const identity = getOrCreateWorkspaceId(cwd);
214
+ const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
215
+ const base = toMove(enriched, resolveCliVersion(), agent, workspaceId, repository, invocationId);
216
+ const scrubbed = await scrubFromCwd(enriched, cwd);
217
+ const move = {
218
+ ...base,
219
+ payload: preserveHookFileMetadata(scrubbed, resolution)
220
+ };
221
+ const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd });
222
+ let verdictFooter = false;
223
+ try {
224
+ const result = await deliverPostToolMove(move, orgId);
225
+ debug(`durably ingested ${move.moveId} (${toolName})`);
226
+ if (isVerdictFooterContext(result.verdictFooter)) {
227
+ verdictFooter = true;
228
+ process.stderr.write(`${renderVerdictFooter(result.verdictFooter)}
229
+ `);
230
+ }
231
+ } catch (err) {
232
+ debug(`ingest failed: ${err instanceof Error ? err.message : String(err)}`);
233
+ }
234
+ if (agent === "codex" && verdictFooter) {
235
+ try {
236
+ const context = await prepareCodexContext({
237
+ cwd,
238
+ sessionId: envelope.session_id,
239
+ includeDigest: false
240
+ });
241
+ await emitWithAcknowledgment(appendCodexContext({}, context.context), context.acknowledge);
242
+ return;
243
+ } catch {
244
+ }
245
+ }
246
+ await emit();
247
+ }
248
+ main().catch(async () => {
249
+ await emit();
250
+ });