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