@primitive.ai/prim 0.1.0-alpha.55 → 0.1.0-alpha.57
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/SKILL.md +23 -18
- package/dist/{chunk-ROL7SDXX.js → chunk-GQURYENF.js} +1 -1
- package/dist/{chunk-6LAQVM26.js → chunk-GUXBUBQ7.js} +2 -0
- package/dist/chunk-LDQZASIF.js +166 -0
- package/dist/chunk-SWWBK4MP.js +424 -0
- package/dist/{chunk-F5QCFBJ6.js → chunk-TNNHR3EZ.js} +8 -18
- package/dist/{chunk-HSZPTVKU.js → chunk-YIMUIPMD.js} +20 -15
- package/dist/daemon/server.js +7 -0
- package/dist/hooks/post-commit.js +2 -2
- package/dist/hooks/post-tool-use.js +26 -9
- package/dist/hooks/pre-tool-use.js +89 -230
- package/dist/hooks/prim-hook.js +10 -7
- package/dist/hooks/session-start.js +3 -3
- package/dist/index.js +99 -63
- package/dist/statusline-main.js +10 -8
- package/package.json +2 -1
- package/dist/chunk-R5ZJRSLV.js +0 -82
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RULES,
|
|
3
|
+
scrub
|
|
4
|
+
} from "./chunk-GUXBUBQ7.js";
|
|
5
|
+
import {
|
|
6
|
+
canonicalGitRoot,
|
|
7
|
+
canonicalRepositoryPath
|
|
8
|
+
} from "./chunk-LDQZASIF.js";
|
|
9
|
+
|
|
10
|
+
// src/hooks/pre-tool-use-scoring.ts
|
|
11
|
+
import { isAbsolute, relative, sep } from "path";
|
|
12
|
+
function anyUnverified(results) {
|
|
13
|
+
return results.some((r) => r.verdict === "unavailable" || r.truncated);
|
|
14
|
+
}
|
|
15
|
+
function unverifiedNote(results) {
|
|
16
|
+
const causes = [];
|
|
17
|
+
const unavailable = results.find((r) => r.verdict === "unavailable");
|
|
18
|
+
if (unavailable) {
|
|
19
|
+
causes.push(
|
|
20
|
+
unavailable.unavailable ? `decision check skipped \u2014 ${unavailable.unavailable}` : "decision check skipped \u2014 not verified"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (results.some((r) => r.truncated)) {
|
|
24
|
+
causes.push("decision check partial \u2014 conflict set truncated (per-file cap hit)");
|
|
25
|
+
}
|
|
26
|
+
return causes.map((c) => `[primitive] ${c}`).join("\n");
|
|
27
|
+
}
|
|
28
|
+
function buildHookOutput(aggregate, results, agent = "claude_code") {
|
|
29
|
+
if (aggregate === "deny") {
|
|
30
|
+
const reason = results.filter((r) => r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
|
|
31
|
+
return {
|
|
32
|
+
hookSpecificOutput: {
|
|
33
|
+
hookEventName: "PreToolUse",
|
|
34
|
+
permissionDecision: "deny",
|
|
35
|
+
permissionDecisionReason: reason
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (agent === "codex" && aggregate === "ask") {
|
|
40
|
+
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
41
|
+
const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
|
|
42
|
+
const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
|
|
43
|
+
const out = {
|
|
44
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
|
|
45
|
+
};
|
|
46
|
+
if (merged.length > 0) {
|
|
47
|
+
out.hookSpecificOutput.additionalContext = merged;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
if (aggregate === "ask") {
|
|
52
|
+
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] please confirm this edit";
|
|
53
|
+
const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
|
|
54
|
+
const out = {
|
|
55
|
+
hookSpecificOutput: {
|
|
56
|
+
hookEventName: "PreToolUse",
|
|
57
|
+
permissionDecision: "ask",
|
|
58
|
+
permissionDecisionReason: reason
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
if (additionalContext.length > 0) {
|
|
62
|
+
out.hookSpecificOutput.additionalContext = additionalContext;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
const notes = [
|
|
67
|
+
...results.map((r) => r.additionalContext).filter((s) => s.length > 0),
|
|
68
|
+
anyUnverified(results) ? unverifiedNote(results) : ""
|
|
69
|
+
].filter((s) => s.length > 0).join("\n");
|
|
70
|
+
if (notes.length > 0) {
|
|
71
|
+
return {
|
|
72
|
+
hookSpecificOutput: {
|
|
73
|
+
hookEventName: "PreToolUse",
|
|
74
|
+
permissionDecision: "allow",
|
|
75
|
+
additionalContext: notes
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
hookSpecificOutput: {
|
|
81
|
+
hookEventName: "PreToolUse",
|
|
82
|
+
permissionDecision: "allow"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function failOpenOutput() {
|
|
87
|
+
return {
|
|
88
|
+
hookSpecificOutput: {
|
|
89
|
+
hookEventName: "PreToolUse",
|
|
90
|
+
permissionDecision: "allow"
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function buildHermesOutput(aggregate, results) {
|
|
95
|
+
if (aggregate !== "deny" && aggregate !== "ask") {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
const reason = results.filter((r) => r.verdict === "deny" || r.verdict === "ask").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
99
|
+
const directive = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
|
|
100
|
+
const message = [reason, directive].filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
|
|
101
|
+
return { action: "block", message };
|
|
102
|
+
}
|
|
103
|
+
function failOpenHermes() {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
107
|
+
var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
|
|
108
|
+
var MOVE_FILE_RE = /^\*\*\* Move File:\s*(?<src>.+?)\s*->\s*(?<dst>.+?)\s*$/;
|
|
109
|
+
var MOVE_TO_RE = /^\*\*\* Move to: (?<dst>.+)$/;
|
|
110
|
+
var LINE_SPLIT_RE = /\r?\n/;
|
|
111
|
+
function parseApplyPatchTargets(command) {
|
|
112
|
+
const paths = /* @__PURE__ */ new Set();
|
|
113
|
+
let activeUpdate = false;
|
|
114
|
+
let complete = true;
|
|
115
|
+
for (const line of command.split(LINE_SPLIT_RE)) {
|
|
116
|
+
const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path;
|
|
117
|
+
if (path) {
|
|
118
|
+
paths.add(path);
|
|
119
|
+
activeUpdate = line.startsWith("*** Update File:");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const moveTo = MOVE_TO_RE.exec(line)?.groups?.dst;
|
|
123
|
+
if (moveTo) {
|
|
124
|
+
if (activeUpdate) paths.add(moveTo);
|
|
125
|
+
else complete = false;
|
|
126
|
+
activeUpdate = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const move = MOVE_FILE_RE.exec(line)?.groups;
|
|
130
|
+
if (move?.src) {
|
|
131
|
+
paths.add(move.src);
|
|
132
|
+
if (move.dst) {
|
|
133
|
+
paths.add(move.dst);
|
|
134
|
+
}
|
|
135
|
+
activeUpdate = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (line === "*** Begin Patch" || line === "*** End Patch") activeUpdate = false;
|
|
139
|
+
else if (line.startsWith("*** ")) complete = false;
|
|
140
|
+
}
|
|
141
|
+
return { paths: [...paths], complete: complete && paths.size > 0 };
|
|
142
|
+
}
|
|
143
|
+
function extractCodexFileTargets(toolName, toolInput) {
|
|
144
|
+
if (toolName !== "apply_patch") {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
148
|
+
return { paths: [], complete: false };
|
|
149
|
+
}
|
|
150
|
+
const command = toolInput.command;
|
|
151
|
+
return typeof command === "string" ? parseApplyPatchTargets(command) : { paths: [], complete: false };
|
|
152
|
+
}
|
|
153
|
+
var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
|
|
154
|
+
function extractHermesFileTargets(toolName, toolInput) {
|
|
155
|
+
if (!HERMES_EDITING_TOOLS.has(toolName)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
159
|
+
return { paths: [], complete: false };
|
|
160
|
+
}
|
|
161
|
+
const input = toolInput;
|
|
162
|
+
if (toolName === "patch" && input.mode === "patch") {
|
|
163
|
+
return typeof input.patch === "string" ? parseApplyPatchTargets(input.patch) : { paths: [], complete: false };
|
|
164
|
+
}
|
|
165
|
+
const paths = typeof input.path === "string" && input.path.length > 0 ? [input.path] : [];
|
|
166
|
+
const mode = input.mode;
|
|
167
|
+
return { paths, complete: paths.length > 0 && (mode === void 0 || mode === "replace") };
|
|
168
|
+
}
|
|
169
|
+
function extractFileTargets(toolName, toolInput, agent = "claude_code") {
|
|
170
|
+
if (agent === "codex") {
|
|
171
|
+
return extractCodexFileTargets(toolName, toolInput);
|
|
172
|
+
}
|
|
173
|
+
if (agent === "hermes") {
|
|
174
|
+
return extractHermesFileTargets(toolName, toolInput);
|
|
175
|
+
}
|
|
176
|
+
if (!SUPPORTED_TOOLS.has(toolName)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
180
|
+
return { paths: [], complete: false };
|
|
181
|
+
}
|
|
182
|
+
const input = toolInput;
|
|
183
|
+
if (toolName === "NotebookEdit") {
|
|
184
|
+
const paths2 = typeof input.notebook_path === "string" && input.notebook_path.length > 0 ? [input.notebook_path] : [];
|
|
185
|
+
return { paths: paths2, complete: paths2.length > 0 };
|
|
186
|
+
}
|
|
187
|
+
const paths = typeof input.file_path === "string" && input.file_path.length > 0 ? [input.file_path] : [];
|
|
188
|
+
return { paths, complete: paths.length > 0 };
|
|
189
|
+
}
|
|
190
|
+
function readHookMode(env) {
|
|
191
|
+
if (env.PRIM_BYPASS === "1" || env.PRIM_BYPASS === "true") {
|
|
192
|
+
return "off";
|
|
193
|
+
}
|
|
194
|
+
const mode = env.PRIM_HOOK_MODE;
|
|
195
|
+
if (mode === "off" || mode === "warn") {
|
|
196
|
+
return mode;
|
|
197
|
+
}
|
|
198
|
+
return "block";
|
|
199
|
+
}
|
|
200
|
+
function demoteForMode(verdict, mode) {
|
|
201
|
+
if (mode === "off") {
|
|
202
|
+
return "allow";
|
|
203
|
+
}
|
|
204
|
+
if (mode === "warn" && (verdict === "ask" || verdict === "deny")) {
|
|
205
|
+
return "warn";
|
|
206
|
+
}
|
|
207
|
+
return verdict;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/hooks/preflight-v3.ts
|
|
211
|
+
import { Buffer } from "buffer";
|
|
212
|
+
|
|
213
|
+
// src/hooks/shell-targets.ts
|
|
214
|
+
import { parse } from "unbash";
|
|
215
|
+
var OUTPUT_REDIRECTS = /* @__PURE__ */ new Set([">", ">>", ">|", "&>", "&>>"]);
|
|
216
|
+
var READ_ONLY_COMMANDS = new Set(": cat echo false printf pwd test true [".split(" "));
|
|
217
|
+
var SINKS = /* @__PURE__ */ new Set(["/dev/null", "/dev/stdout", "/dev/stderr"]);
|
|
218
|
+
var STATIC_PARTS = /* @__PURE__ */ new Set(["Literal", "SingleQuoted", "AnsiCQuoted"]);
|
|
219
|
+
function staticPart(part) {
|
|
220
|
+
return STATIC_PARTS.has(part.type) || part.type === "DoubleQuoted" && part.parts.every(staticPart);
|
|
221
|
+
}
|
|
222
|
+
function staticWord(word) {
|
|
223
|
+
if (!word || !word.value || word.value.includes("\0") || word.value.includes("\n")) return;
|
|
224
|
+
if (word.parts ? !word.parts.every(staticPart) : word.value.startsWith("~") || /[*?\[$`]/.test(word.text))
|
|
225
|
+
return;
|
|
226
|
+
return word.value;
|
|
227
|
+
}
|
|
228
|
+
function unknown(state) {
|
|
229
|
+
state.complete = false;
|
|
230
|
+
state.mutation = true;
|
|
231
|
+
}
|
|
232
|
+
function recordTarget(word, state) {
|
|
233
|
+
const path = staticWord(word);
|
|
234
|
+
if (path === "-" || path && (SINKS.has(path) || path.startsWith("/dev/fd/"))) return;
|
|
235
|
+
state.mutation = true;
|
|
236
|
+
if (path) state.paths.add(path);
|
|
237
|
+
else state.complete = false;
|
|
238
|
+
}
|
|
239
|
+
function visitRedirect(redirect, state) {
|
|
240
|
+
const target = staticWord(redirect.target);
|
|
241
|
+
if (redirect.operator === ">&" && (target === "-" || /^\d+$/.test(target ?? ""))) return;
|
|
242
|
+
if (OUTPUT_REDIRECTS.has(redirect.operator) || redirect.operator === ">&")
|
|
243
|
+
recordTarget(redirect.target, state);
|
|
244
|
+
else unknown(state);
|
|
245
|
+
}
|
|
246
|
+
function operands(words, short = "", long = /* @__PURE__ */ new Set()) {
|
|
247
|
+
const result = [];
|
|
248
|
+
let optionsEnded = false;
|
|
249
|
+
for (const word of words) {
|
|
250
|
+
const value = staticWord(word);
|
|
251
|
+
if (value === void 0) return;
|
|
252
|
+
if (optionsEnded || value === "-" || !value.startsWith("-")) result.push(value);
|
|
253
|
+
else if (value === "--") optionsEnded = true;
|
|
254
|
+
else if (value.startsWith("--") ? !long.has(value) : [...value.slice(1)].some((c) => !short.includes(c)))
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
function visitCommand(command, state) {
|
|
260
|
+
for (const redirect of command.redirects) visitRedirect(redirect, state);
|
|
261
|
+
const name = staticWord(command.name);
|
|
262
|
+
if (!name || name.includes("/") || command.suffix.some((word) => !staticWord(word))) {
|
|
263
|
+
unknown(state);
|
|
264
|
+
} else if (!READ_ONLY_COMMANDS.has(name)) {
|
|
265
|
+
state.mutation = true;
|
|
266
|
+
let targets;
|
|
267
|
+
if (name === "tee") {
|
|
268
|
+
targets = operands(command.suffix, "ai", /* @__PURE__ */ new Set(["--append", "--ignore-interrupts"]));
|
|
269
|
+
} else if (name === "touch") {
|
|
270
|
+
targets = operands(command.suffix);
|
|
271
|
+
} else if (name === "rm") {
|
|
272
|
+
targets = operands(
|
|
273
|
+
command.suffix,
|
|
274
|
+
"fivI",
|
|
275
|
+
/* @__PURE__ */ new Set(["--force", "--interactive", "--interactive=once", "--verbose"])
|
|
276
|
+
);
|
|
277
|
+
} else if (name === "cp" || name === "mv") {
|
|
278
|
+
const pair = operands(command.suffix);
|
|
279
|
+
if (pair?.length === 2) targets = name === "cp" ? [pair[1]] : pair;
|
|
280
|
+
}
|
|
281
|
+
if (!targets?.length) state.complete = false;
|
|
282
|
+
else for (const target of targets) state.paths.add(target);
|
|
283
|
+
}
|
|
284
|
+
visitNested([command.name, command.prefix, command.suffix, command.redirects], state);
|
|
285
|
+
}
|
|
286
|
+
var CONTAINERS = /* @__PURE__ */ new Set(["Script", "Pipeline", "AndOr", "CompoundList"]);
|
|
287
|
+
function visitNested(value, state) {
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
for (const child of value) visitNested(child, state);
|
|
290
|
+
} else if (value && typeof value === "object") {
|
|
291
|
+
const child = value;
|
|
292
|
+
if (child.type === "Command") {
|
|
293
|
+
visitCommand(child, state);
|
|
294
|
+
} else if (child.type === "Statement") {
|
|
295
|
+
visitStatement(child, state);
|
|
296
|
+
} else if ("parts" in child) {
|
|
297
|
+
for (const part of value.parts ?? []) visitNested(part, state);
|
|
298
|
+
} else {
|
|
299
|
+
const node = typeof child.type === "string" && "pos" in child && "end" in child;
|
|
300
|
+
if (node && !CONTAINERS.has(child.type)) unknown(state);
|
|
301
|
+
if (child.type === "Function") return;
|
|
302
|
+
for (const nested of Object.values(child)) visitNested(nested, state);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function visitStatement(statement, state) {
|
|
307
|
+
if (statement.background) unknown(state);
|
|
308
|
+
for (const redirect of statement.redirects) visitRedirect(redirect, state);
|
|
309
|
+
visitNested([statement.redirects, statement.command], state);
|
|
310
|
+
}
|
|
311
|
+
function analyzeShellTargets(source) {
|
|
312
|
+
if (!source.trim())
|
|
313
|
+
return { paths: [], coverage: "complete", mutation: "none" };
|
|
314
|
+
try {
|
|
315
|
+
const script = parse(source);
|
|
316
|
+
const state = { paths: /* @__PURE__ */ new Set(), complete: !script.errors?.length, mutation: false };
|
|
317
|
+
for (const statement of script.commands) visitStatement(statement, state);
|
|
318
|
+
if (script.errors?.length) state.mutation = true;
|
|
319
|
+
return {
|
|
320
|
+
paths: [...state.paths],
|
|
321
|
+
coverage: state.complete ? "complete" : "unverified",
|
|
322
|
+
mutation: state.mutation ? "present" : "none"
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return { paths: [], coverage: "unverified", mutation: "present" };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/hooks/preflight-v3.ts
|
|
330
|
+
var PREFLIGHT_PROTOCOL_VERSION = 3;
|
|
331
|
+
var MAX_PREFLIGHT_PATHS = 32;
|
|
332
|
+
var MAX_PROPOSAL_BYTES = 6144;
|
|
333
|
+
function resolvePreflightTargets(args) {
|
|
334
|
+
let rawPaths;
|
|
335
|
+
let coverage = "complete";
|
|
336
|
+
let mutation = "present";
|
|
337
|
+
if (args.agent === "claude_code" && args.toolName === "Bash") {
|
|
338
|
+
const command = args.toolInput && typeof args.toolInput === "object" ? args.toolInput.command : void 0;
|
|
339
|
+
if (typeof command !== "string") {
|
|
340
|
+
return { paths: [], coverage: "unverified", mutation: "present" };
|
|
341
|
+
}
|
|
342
|
+
const shell = analyzeShellTargets(command);
|
|
343
|
+
rawPaths = shell.paths;
|
|
344
|
+
coverage = shell.coverage;
|
|
345
|
+
mutation = shell.mutation;
|
|
346
|
+
} else {
|
|
347
|
+
const extracted = extractFileTargets(args.toolName, args.toolInput, args.agent);
|
|
348
|
+
if (!extracted) return { paths: [], coverage: "complete", mutation: "none" };
|
|
349
|
+
rawPaths = extracted.paths;
|
|
350
|
+
if (!extracted.complete) coverage = "unverified";
|
|
351
|
+
}
|
|
352
|
+
const root = canonicalGitRoot(args.cwd);
|
|
353
|
+
const paths = /* @__PURE__ */ new Set();
|
|
354
|
+
for (const rawPath of rawPaths) {
|
|
355
|
+
const canonical = canonicalRepositoryPath(rawPath, args.cwd, root);
|
|
356
|
+
if (canonical) paths.add(canonical);
|
|
357
|
+
else coverage = "unverified";
|
|
358
|
+
}
|
|
359
|
+
const bounded = [...paths];
|
|
360
|
+
if (bounded.length > MAX_PREFLIGHT_PATHS) coverage = "unverified";
|
|
361
|
+
return { paths: bounded.slice(0, MAX_PREFLIGHT_PATHS), coverage, mutation };
|
|
362
|
+
}
|
|
363
|
+
function truncateUtf8(value, maxBytes) {
|
|
364
|
+
const encoded = Buffer.from(value);
|
|
365
|
+
if (encoded.byteLength <= maxBytes) return value;
|
|
366
|
+
return encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
|
|
367
|
+
}
|
|
368
|
+
function proposalFor(toolInput) {
|
|
369
|
+
const redacted = scrub(toolInput, DEFAULT_RULES);
|
|
370
|
+
return truncateUtf8(JSON.stringify(redacted) ?? "", MAX_PROPOSAL_BYTES);
|
|
371
|
+
}
|
|
372
|
+
function parsePreflightResponse(value) {
|
|
373
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
374
|
+
const response = value;
|
|
375
|
+
const verdicts = /* @__PURE__ */ new Set(["allow", "warn", "ask", "block", "unavailable"]);
|
|
376
|
+
if (response.protocolVersion !== PREFLIGHT_PROTOCOL_VERSION || typeof response.verdict !== "string" || !verdicts.has(response.verdict) || typeof response.reasonCode !== "string" || typeof response.message !== "string" || !Array.isArray(response.conflicts) || !Array.isArray(response.bypassed)) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
return response;
|
|
380
|
+
}
|
|
381
|
+
function resultForPreflight(response) {
|
|
382
|
+
const verdict = response.verdict === "block" ? "deny" : response.verdict;
|
|
383
|
+
const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
384
|
+
const directives = response.conflicts.flatMap((value) => {
|
|
385
|
+
if (!value || typeof value !== "object") return [];
|
|
386
|
+
const { decisionId, shortId } = value;
|
|
387
|
+
const id = typeof shortId === "string" && safe.test(shortId) ? `dec_${shortId}` : typeof decisionId === "string" && safe.test(decisionId) ? decisionId : void 0;
|
|
388
|
+
return id ? [`To reconcile, run: prim reconcile ${id}`] : [];
|
|
389
|
+
});
|
|
390
|
+
const reason = [response.message, ...new Set(directives)].filter(Boolean).join("\n");
|
|
391
|
+
return {
|
|
392
|
+
verdict,
|
|
393
|
+
conflicts: response.conflicts,
|
|
394
|
+
reason,
|
|
395
|
+
additionalContext: ["warn", "ask", "block"].includes(response.verdict) ? reason : "",
|
|
396
|
+
truncated: false,
|
|
397
|
+
unavailable: response.verdict === "unavailable" ? response.message || response.reasonCode : void 0
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function unverifiedResult(message) {
|
|
401
|
+
return {
|
|
402
|
+
verdict: "unavailable",
|
|
403
|
+
conflicts: [],
|
|
404
|
+
reason: "",
|
|
405
|
+
additionalContext: "",
|
|
406
|
+
truncated: false,
|
|
407
|
+
unavailable: message
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export {
|
|
412
|
+
buildHookOutput,
|
|
413
|
+
failOpenOutput,
|
|
414
|
+
buildHermesOutput,
|
|
415
|
+
failOpenHermes,
|
|
416
|
+
readHookMode,
|
|
417
|
+
demoteForMode,
|
|
418
|
+
PREFLIGHT_PROTOCOL_VERSION,
|
|
419
|
+
resolvePreflightTargets,
|
|
420
|
+
proposalFor,
|
|
421
|
+
parsePreflightResponse,
|
|
422
|
+
resultForPreflight,
|
|
423
|
+
unverifiedResult
|
|
424
|
+
};
|
|
@@ -4,20 +4,12 @@ import { platform } from "os";
|
|
|
4
4
|
|
|
5
5
|
// src/protocol/move.ts
|
|
6
6
|
var LEGACY_ENVELOPE_VERSION = 1;
|
|
7
|
-
var AGENT_ENVELOPE_VERSION =
|
|
7
|
+
var AGENT_ENVELOPE_VERSION = 3;
|
|
8
8
|
var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
|
|
9
|
-
function withAgentProvenance(move, producer, workspaceId) {
|
|
10
|
-
return {
|
|
11
|
-
...move,
|
|
12
|
-
env: { ...move.env, workspaceId },
|
|
13
|
-
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
14
|
-
producer
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
9
|
|
|
18
10
|
// src/hooks/prim-hook-core.ts
|
|
19
|
-
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
|
|
20
|
-
|
|
11
|
+
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, invocationId) {
|
|
12
|
+
return {
|
|
21
13
|
moveId: randomUUID(),
|
|
22
14
|
capturedAt: Date.now(),
|
|
23
15
|
sessionId: parsed.session_id ?? "",
|
|
@@ -26,15 +18,13 @@ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
|
|
|
26
18
|
env: {
|
|
27
19
|
cwd: parsed.cwd ?? process.cwd(),
|
|
28
20
|
cliVersion,
|
|
29
|
-
osPlatform: platform()
|
|
21
|
+
osPlatform: platform(),
|
|
22
|
+
...workspaceId ? { workspaceId } : {}
|
|
30
23
|
},
|
|
31
|
-
envelopeVersion:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// keeping the Claude wire shape byte-identical.
|
|
35
|
-
...agent === "claude_code" ? {} : { producer: agent }
|
|
24
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
25
|
+
producer: agent,
|
|
26
|
+
...invocationId ? { invocationId } : {}
|
|
36
27
|
};
|
|
37
|
-
return workspaceId ? withAgentProvenance(move, agent, workspaceId) : move;
|
|
38
28
|
}
|
|
39
29
|
function toCommitMove(commit, cliVersion, cwd) {
|
|
40
30
|
return {
|
|
@@ -10,8 +10,6 @@ import { fileURLToPath } from "url";
|
|
|
10
10
|
var PKG_NAME = "@primitive.ai/prim";
|
|
11
11
|
var ROOT_WALK_LIMIT = 6;
|
|
12
12
|
var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
|
|
13
|
-
var BIN_CACHE_DIR_SH = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
|
|
14
|
-
var BIN_CACHE_TTL_MIN_DEFAULT = 1440;
|
|
15
13
|
var resolvedRoot;
|
|
16
14
|
function locateRoot() {
|
|
17
15
|
if (resolvedRoot !== void 0) {
|
|
@@ -24,7 +22,7 @@ function locateRoot() {
|
|
|
24
22
|
try {
|
|
25
23
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
26
24
|
if (manifest.name === PKG_NAME && manifest.bin) {
|
|
27
|
-
resolvedRoot = { dir, bin: manifest.bin };
|
|
25
|
+
resolvedRoot = { dir, version: manifest.version, bin: manifest.bin };
|
|
28
26
|
return resolvedRoot;
|
|
29
27
|
}
|
|
30
28
|
} catch {
|
|
@@ -47,18 +45,23 @@ function binFile(bin) {
|
|
|
47
45
|
}
|
|
48
46
|
return isAbsolute(rel) ? rel : join(root.dir, rel);
|
|
49
47
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
48
|
+
function packageVersion() {
|
|
49
|
+
return locateRoot()?.version ?? null;
|
|
50
|
+
}
|
|
51
|
+
function shellQuote(value) {
|
|
52
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
53
|
+
}
|
|
54
|
+
function pinnedHookCommand(bin, args = "") {
|
|
55
|
+
const version = packageVersion();
|
|
56
|
+
if (!version) throw new Error("cannot determine Primitive package version");
|
|
57
|
+
const suffix = args ? ` ${args}` : "";
|
|
58
|
+
const fallback = `npx --yes -p ${PKG_NAME}@${version} ${bin}${suffix}`;
|
|
59
|
+
const file = binFile(bin);
|
|
60
|
+
if (!file) return fallback;
|
|
61
|
+
return `if [ -x ${shellQuote(process.execPath)} ] && [ -f ${shellQuote(file)} ]; then ${shellQuote(process.execPath)} ${shellQuote(file)}${suffix}; else ${fallback}; fi`;
|
|
59
62
|
}
|
|
60
63
|
function detachedHookShimCommand(bin, args = "") {
|
|
61
|
-
return `payload=$(cat); { trap '' HUP; export npm_config_fetch_retries=2 npm_config_fetch_retry_mintimeout=10000 npm_config_fetch_retry_maxtimeout=10000 npm_config_fetch_timeout=60000; printf '%s' "$payload" | { ${
|
|
64
|
+
return `payload=$(cat); { trap '' HUP; export npm_config_fetch_retries=2 npm_config_fetch_retry_mintimeout=10000 npm_config_fetch_retry_maxtimeout=10000 npm_config_fetch_timeout=60000; printf '%s' "$payload" | { ${pinnedHookCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
|
|
62
65
|
}
|
|
63
66
|
function commandMatchesBin(command, bin) {
|
|
64
67
|
if (!command) {
|
|
@@ -68,7 +71,8 @@ function commandMatchesBin(command, bin) {
|
|
|
68
71
|
if (c === bin || c.startsWith(`${bin} `)) {
|
|
69
72
|
return true;
|
|
70
73
|
}
|
|
71
|
-
|
|
74
|
+
const exactBin = new RegExp(`(?:^|\\s)${bin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|;|$)`);
|
|
75
|
+
return c.includes(`command -v ${bin} `) || c.includes(`-p ${PKG_NAME}@`) && exactBin.test(c);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
// src/lib/bin-cache.ts
|
|
@@ -106,7 +110,8 @@ function warmBinCache() {
|
|
|
106
110
|
|
|
107
111
|
export {
|
|
108
112
|
binFile,
|
|
109
|
-
|
|
113
|
+
packageVersion,
|
|
114
|
+
pinnedHookCommand,
|
|
110
115
|
detachedHookShimCommand,
|
|
111
116
|
commandMatchesBin,
|
|
112
117
|
warmBinCache
|
package/dist/daemon/server.js
CHANGED
|
@@ -1488,6 +1488,13 @@ async function runIngestionLoop() {
|
|
|
1488
1488
|
}
|
|
1489
1489
|
async function handleConflictCheck(params) {
|
|
1490
1490
|
assertCallerEnvMatches(params.callerEnv, getSiteUrl());
|
|
1491
|
+
if (params.protocolVersion === 3) {
|
|
1492
|
+
const { callerEnv: _callerEnv, ...body } = params;
|
|
1493
|
+
return await client.post("/api/cli/decisions/conflict-check", body, {
|
|
1494
|
+
signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS),
|
|
1495
|
+
quietRefresh: true
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1491
1498
|
if (typeof params.file !== "string") {
|
|
1492
1499
|
throw new Error("conflict_check requires `file: string`");
|
|
1493
1500
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
toCommitMove
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-TNNHR3EZ.js";
|
|
5
5
|
import {
|
|
6
6
|
appendMove,
|
|
7
7
|
resolveOrg
|
|
8
8
|
} from "../chunk-BOTKPLTI.js";
|
|
9
9
|
import {
|
|
10
10
|
isRepoActiveForCapture
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-LDQZASIF.js";
|
|
12
12
|
import "../chunk-DWDEQRWN.js";
|
|
13
13
|
|
|
14
14
|
// src/hooks/post-commit.ts
|
|
@@ -5,24 +5,28 @@ import {
|
|
|
5
5
|
requireDurableIngestAcknowledgement
|
|
6
6
|
} from "../chunk-QY75BQMF.js";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
} from "../chunk-
|
|
8
|
+
resolvePreflightTargets
|
|
9
|
+
} from "../chunk-SWWBK4MP.js";
|
|
10
10
|
import {
|
|
11
11
|
toMove
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-TNNHR3EZ.js";
|
|
13
13
|
import {
|
|
14
14
|
appendMove,
|
|
15
15
|
resolveOrg
|
|
16
16
|
} from "../chunk-BOTKPLTI.js";
|
|
17
|
+
import {
|
|
18
|
+
scrubFromCwd
|
|
19
|
+
} from "../chunk-GUXBUBQ7.js";
|
|
17
20
|
import {
|
|
18
21
|
getOrCreateWorkspaceId
|
|
19
22
|
} from "../chunk-IMAIBPUC.js";
|
|
20
23
|
import {
|
|
21
24
|
warmBinCache
|
|
22
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-YIMUIPMD.js";
|
|
23
26
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
isRepoActive,
|
|
28
|
+
repoSyncId
|
|
29
|
+
} from "../chunk-LDQZASIF.js";
|
|
26
30
|
import {
|
|
27
31
|
getClient
|
|
28
32
|
} from "../chunk-DWDEQRWN.js";
|
|
@@ -66,7 +70,7 @@ function isVerdictFooterContext(value) {
|
|
|
66
70
|
|
|
67
71
|
// src/hooks/post-tool-use.ts
|
|
68
72
|
var STDIN_TIMEOUT_MS = 1e3;
|
|
69
|
-
var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
73
|
+
var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
|
|
70
74
|
var CODEX_EDITING_TOOLS = /* @__PURE__ */ new Set(["apply_patch"]);
|
|
71
75
|
var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
|
|
72
76
|
function editingToolsFor(agent) {
|
|
@@ -146,13 +150,26 @@ async function main() {
|
|
|
146
150
|
return;
|
|
147
151
|
}
|
|
148
152
|
const cwd = parsed.cwd ?? process.cwd();
|
|
149
|
-
if (!
|
|
153
|
+
if (!isRepoActive(cwd) || !repoSyncId(cwd)) {
|
|
150
154
|
emit();
|
|
151
155
|
return;
|
|
152
156
|
}
|
|
157
|
+
if (agent === "claude_code") {
|
|
158
|
+
const targets = resolvePreflightTargets({
|
|
159
|
+
toolName,
|
|
160
|
+
toolInput: envelope.tool_input,
|
|
161
|
+
agent,
|
|
162
|
+
cwd
|
|
163
|
+
});
|
|
164
|
+
if (targets.mutation === "none") {
|
|
165
|
+
emit();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
153
169
|
const identity = getOrCreateWorkspaceId(cwd);
|
|
154
170
|
const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
|
|
155
|
-
const
|
|
171
|
+
const invocationId = agent === "claude_code" && typeof envelope.tool_use_id === "string" ? envelope.tool_use_id : void 0;
|
|
172
|
+
const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
|
|
156
173
|
const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
|
|
157
174
|
const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
|
|
158
175
|
try {
|