@primitive.ai/prim 0.1.0-alpha.60 → 0.1.0-alpha.62
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/dist/chunk-DA4BMHEB.js +77 -0
- package/dist/{chunk-B4SHJYIN.js → chunk-GF5QII7T.js} +83 -110
- package/dist/{chunk-QY75BQMF.js → chunk-JQFN6LBO.js} +8 -0
- package/dist/{chunk-YIMUIPMD.js → chunk-KBFFVYBC.js} +7 -1
- package/dist/chunk-LOGW54W5.js +1244 -0
- package/dist/chunk-QGVHLP5J.js +117 -0
- package/dist/{chunk-IMAIBPUC.js → chunk-UVIWZWM6.js} +1 -0
- package/dist/chunk-XTNMSS4C.js +283 -0
- package/dist/daemon/server.js +1 -0
- package/dist/hooks/post-commit.js +82 -16
- package/dist/hooks/post-tool-use.js +41 -25
- package/dist/hooks/pre-tool-use.js +103 -11
- package/dist/hooks/prim-hook.js +30 -9
- package/dist/hooks/session-start.js +22 -5
- package/dist/index.js +730 -179
- package/dist/statusline-main.js +7 -2
- package/package.json +1 -1
- package/dist/chunk-GUXBUBQ7.js +0 -84
- package/dist/chunk-LDQZASIF.js +0 -166
- package/dist/chunk-MZQO7K2P.js +0 -616
- package/dist/chunk-YDBZTFW5.js +0 -67
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeShellTargets,
|
|
3
|
+
extractFileTargets
|
|
4
|
+
} from "./chunk-GF5QII7T.js";
|
|
5
|
+
import {
|
|
6
|
+
canonicalRepositoryPath
|
|
7
|
+
} from "./chunk-XTNMSS4C.js";
|
|
8
|
+
|
|
9
|
+
// src/hooks/file-refs.ts
|
|
10
|
+
var MAX_PREFLIGHT_FILE_TARGETS = 25;
|
|
11
|
+
function primitiveMetadataForResolution(resolution) {
|
|
12
|
+
const shouldAttach = resolution.fileRefs.length > 0 || resolution.rejected.length > 0 || resolution.targetsIncomplete || resolution.targetsTruncated || resolution.shellMutation !== void 0;
|
|
13
|
+
if (!shouldAttach) return;
|
|
14
|
+
return {
|
|
15
|
+
fileRefs: [...resolution.fileRefs],
|
|
16
|
+
...resolution.rejected.length > 0 || resolution.targetsIncomplete ? { fileRefsIncomplete: true } : {},
|
|
17
|
+
...resolution.targetsTruncated ? { fileRefsTruncated: true } : {},
|
|
18
|
+
...resolution.shellMutation !== void 0 ? { shellMutation: resolution.shellMutation } : {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function preserveHookFileMetadata(payload, resolution) {
|
|
22
|
+
const primitive = primitiveMetadataForResolution(resolution);
|
|
23
|
+
if (!primitive || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
24
|
+
return payload;
|
|
25
|
+
}
|
|
26
|
+
return { ...payload, primitive };
|
|
27
|
+
}
|
|
28
|
+
function resolveHookFileRefs(args) {
|
|
29
|
+
const command = args.agent === "claude_code" && args.toolName === "Bash" && args.toolInput && typeof args.toolInput === "object" && typeof args.toolInput.command === "string" ? args.toolInput.command : void 0;
|
|
30
|
+
const shell = command === void 0 ? void 0 : analyzeShellTargets(command);
|
|
31
|
+
const shellMutation = shell?.mutation === "none" ? "none" : shell?.coverage === "complete" ? "resolved" : shell ? "unresolved" : void 0;
|
|
32
|
+
const readPath = args.agent === "claude_code" && args.toolName === "Read" && args.toolInput && typeof args.toolInput === "object" && typeof args.toolInput.file_path === "string" ? [args.toolInput.file_path] : void 0;
|
|
33
|
+
const nativeTargets = shell ? null : readPath ? { paths: readPath, complete: true } : extractFileTargets(args.toolName, args.toolInput, args.agent);
|
|
34
|
+
const rawPaths = shell ? shellMutation === "unresolved" ? [] : shell.paths : nativeTargets?.paths ?? [];
|
|
35
|
+
const uniqueRawPaths = Array.from(new Set(rawPaths));
|
|
36
|
+
const targetsTruncated = uniqueRawPaths.length > MAX_PREFLIGHT_FILE_TARGETS;
|
|
37
|
+
const fileRefs = /* @__PURE__ */ new Set();
|
|
38
|
+
const rejected = [];
|
|
39
|
+
for (const path of uniqueRawPaths.slice(0, MAX_PREFLIGHT_FILE_TARGETS)) {
|
|
40
|
+
const canonical = canonicalRepositoryPath(path, args.repository, args.cwd);
|
|
41
|
+
if (canonical.ok) fileRefs.add(canonical.file);
|
|
42
|
+
else rejected.push({ path, reason: canonical.reason });
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
fileRefs: [...fileRefs],
|
|
46
|
+
rejected,
|
|
47
|
+
targetsIncomplete: nativeTargets?.complete === false,
|
|
48
|
+
targetsTruncated,
|
|
49
|
+
...shellMutation ? { shellMutation: targetsTruncated ? "unresolved" : shellMutation } : {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function enrichHookPayloadWithFileRefs(args) {
|
|
53
|
+
const toolName = typeof args.parsed.tool_name === "string" ? args.parsed.tool_name : "";
|
|
54
|
+
const resolution = resolveHookFileRefs({
|
|
55
|
+
toolName,
|
|
56
|
+
toolInput: args.parsed.tool_input,
|
|
57
|
+
agent: args.agent,
|
|
58
|
+
cwd: args.cwd,
|
|
59
|
+
repository: args.repository
|
|
60
|
+
});
|
|
61
|
+
const primitive = primitiveMetadataForResolution(resolution);
|
|
62
|
+
if (!primitive) return { parsed: args.parsed, resolution };
|
|
63
|
+
return {
|
|
64
|
+
parsed: {
|
|
65
|
+
...args.parsed,
|
|
66
|
+
// An explicit empty list is authoritative when every exposed target was
|
|
67
|
+
// rejected. It prevents backend fallback to unsafe raw tool input.
|
|
68
|
+
primitive
|
|
69
|
+
},
|
|
70
|
+
resolution
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
preserveHookFileMetadata,
|
|
76
|
+
enrichHookPayloadWithFileRefs
|
|
77
|
+
};
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_RULES,
|
|
3
|
-
scrub
|
|
4
|
-
} from "./chunk-GUXBUBQ7.js";
|
|
5
|
-
import {
|
|
6
|
-
canonicalGitRoot,
|
|
7
|
-
canonicalRepositoryPath
|
|
8
|
-
} from "./chunk-LDQZASIF.js";
|
|
9
|
-
import {
|
|
10
|
-
getClient
|
|
11
|
-
} from "./chunk-I3HIPAIR.js";
|
|
12
|
-
|
|
13
1
|
// src/hooks/pre-tool-use-scoring.ts
|
|
14
2
|
import { isAbsolute, relative, sep } from "path";
|
|
15
3
|
function anyUnverified(results) {
|
|
@@ -211,8 +199,84 @@ function demoteForMode(verdict, mode) {
|
|
|
211
199
|
return verdict;
|
|
212
200
|
}
|
|
213
201
|
|
|
214
|
-
// src/hooks/
|
|
215
|
-
import {
|
|
202
|
+
// src/hooks/redact.ts
|
|
203
|
+
import { existsSync, readFileSync } from "fs";
|
|
204
|
+
import { join } from "path";
|
|
205
|
+
var DEFAULT_RULES = [
|
|
206
|
+
{ pattern: /Bearer\s+[A-Za-z0-9._\-]+/g, reason: "bearer-token" },
|
|
207
|
+
{ pattern: /sk-[A-Za-z0-9]{32,}/g, reason: "sk-api-key" },
|
|
208
|
+
{
|
|
209
|
+
pattern: /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g,
|
|
210
|
+
reason: "private-key"
|
|
211
|
+
},
|
|
212
|
+
{ pattern: /xox[abprs]-[A-Za-z0-9-]{10,}/g, reason: "slack-token" },
|
|
213
|
+
{ pattern: /ghp_[A-Za-z0-9]{36,}/g, reason: "github-pat" }
|
|
214
|
+
];
|
|
215
|
+
var MAX_SCRUB_LEN = 256e3;
|
|
216
|
+
var WORKSPACE_FILE = ".prim/redaction.json";
|
|
217
|
+
function debugRedaction(message, err) {
|
|
218
|
+
if (!process.env.PRIM_HOOK_DEBUG) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
222
|
+
process.stderr.write(`[prim-hook] redaction: ${message}: ${detail}
|
|
223
|
+
`);
|
|
224
|
+
}
|
|
225
|
+
function loadWorkspaceRules(cwd) {
|
|
226
|
+
const path = join(cwd, WORKSPACE_FILE);
|
|
227
|
+
if (!existsSync(path)) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
let cfg;
|
|
231
|
+
try {
|
|
232
|
+
cfg = JSON.parse(readFileSync(path, "utf-8"));
|
|
233
|
+
} catch (err) {
|
|
234
|
+
debugRedaction(`ignoring unparseable ${WORKSPACE_FILE}`, err);
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
if (!cfg.rules) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const compiled = [];
|
|
241
|
+
for (const r of cfg.rules) {
|
|
242
|
+
try {
|
|
243
|
+
compiled.push({ pattern: new RegExp(r.pattern, r.flags ?? "g"), reason: r.reason });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
debugRedaction(`skipping invalid redaction rule "${r.reason}"`, err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return compiled;
|
|
249
|
+
}
|
|
250
|
+
function applyRules(input, rules) {
|
|
251
|
+
if (input.length > MAX_SCRUB_LEN) {
|
|
252
|
+
return "<REDACTED:oversized>";
|
|
253
|
+
}
|
|
254
|
+
let out = input;
|
|
255
|
+
for (const rule of rules) {
|
|
256
|
+
out = out.replace(rule.pattern, `<REDACTED:${rule.reason}>`);
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function scrub(value, rules = DEFAULT_RULES) {
|
|
261
|
+
if (typeof value === "string") {
|
|
262
|
+
return applyRules(value, rules);
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
return value.map((v) => scrub(v, rules));
|
|
266
|
+
}
|
|
267
|
+
if (value && typeof value === "object") {
|
|
268
|
+
const out = {};
|
|
269
|
+
for (const [k, v] of Object.entries(value)) {
|
|
270
|
+
out[k] = scrub(v, rules);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
function scrubFromCwd(value, cwd) {
|
|
277
|
+
const rules = [...DEFAULT_RULES, ...loadWorkspaceRules(cwd)];
|
|
278
|
+
return scrub(value, rules);
|
|
279
|
+
}
|
|
216
280
|
|
|
217
281
|
// src/hooks/shell-targets.ts
|
|
218
282
|
import { parse } from "unbash";
|
|
@@ -330,96 +394,6 @@ function analyzeShellTargets(source) {
|
|
|
330
394
|
}
|
|
331
395
|
}
|
|
332
396
|
|
|
333
|
-
// src/hooks/preflight-v3.ts
|
|
334
|
-
var PREFLIGHT_PROTOCOL_VERSION = 3;
|
|
335
|
-
var PREFLIGHT_TIMEOUT_MS = 6500;
|
|
336
|
-
var MAX_PREFLIGHT_PATHS = 32;
|
|
337
|
-
var MAX_PROPOSAL_BYTES = 6144;
|
|
338
|
-
function resolvePreflightTargets(args) {
|
|
339
|
-
let rawPaths;
|
|
340
|
-
let coverage = "complete";
|
|
341
|
-
let mutation = "present";
|
|
342
|
-
if (args.agent === "claude_code" && args.toolName === "Bash") {
|
|
343
|
-
const command = args.toolInput && typeof args.toolInput === "object" ? args.toolInput.command : void 0;
|
|
344
|
-
if (typeof command !== "string") {
|
|
345
|
-
return { paths: [], coverage: "unverified", mutation: "present" };
|
|
346
|
-
}
|
|
347
|
-
const shell = analyzeShellTargets(command);
|
|
348
|
-
rawPaths = shell.paths;
|
|
349
|
-
coverage = shell.coverage;
|
|
350
|
-
mutation = shell.mutation;
|
|
351
|
-
} else {
|
|
352
|
-
const extracted = extractFileTargets(args.toolName, args.toolInput, args.agent);
|
|
353
|
-
if (!extracted) return { paths: [], coverage: "complete", mutation: "none" };
|
|
354
|
-
rawPaths = extracted.paths;
|
|
355
|
-
if (!extracted.complete) coverage = "unverified";
|
|
356
|
-
}
|
|
357
|
-
const root = canonicalGitRoot(args.cwd);
|
|
358
|
-
const paths = /* @__PURE__ */ new Set();
|
|
359
|
-
for (const rawPath of rawPaths) {
|
|
360
|
-
const canonical = canonicalRepositoryPath(rawPath, args.cwd, root);
|
|
361
|
-
if (canonical) paths.add(canonical);
|
|
362
|
-
else coverage = "unverified";
|
|
363
|
-
}
|
|
364
|
-
const bounded = [...paths];
|
|
365
|
-
if (bounded.length > MAX_PREFLIGHT_PATHS) coverage = "unverified";
|
|
366
|
-
return { paths: bounded.slice(0, MAX_PREFLIGHT_PATHS), coverage, mutation };
|
|
367
|
-
}
|
|
368
|
-
function truncateUtf8(value, maxBytes) {
|
|
369
|
-
const encoded = Buffer.from(value);
|
|
370
|
-
if (encoded.byteLength <= maxBytes) return value;
|
|
371
|
-
return encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
|
|
372
|
-
}
|
|
373
|
-
function proposalFor(toolInput) {
|
|
374
|
-
const redacted = scrub(toolInput, DEFAULT_RULES);
|
|
375
|
-
return truncateUtf8(JSON.stringify(redacted) ?? "", MAX_PROPOSAL_BYTES);
|
|
376
|
-
}
|
|
377
|
-
function parsePreflightResponse(value) {
|
|
378
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
379
|
-
const response = value;
|
|
380
|
-
const verdicts = /* @__PURE__ */ new Set(["allow", "warn", "ask", "block", "unavailable"]);
|
|
381
|
-
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)) {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
return response;
|
|
385
|
-
}
|
|
386
|
-
async function requestPreflight(request, signal = AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS)) {
|
|
387
|
-
const response = await getClient().post("/api/cli/decisions/conflict-check", request, {
|
|
388
|
-
signal,
|
|
389
|
-
quietRefresh: true
|
|
390
|
-
});
|
|
391
|
-
return parsePreflightResponse(response);
|
|
392
|
-
}
|
|
393
|
-
function resultForPreflight(response) {
|
|
394
|
-
const verdict = response.verdict === "block" ? "deny" : response.verdict;
|
|
395
|
-
const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
396
|
-
const directives = response.conflicts.flatMap((value) => {
|
|
397
|
-
if (!value || typeof value !== "object") return [];
|
|
398
|
-
const { decisionId, shortId } = value;
|
|
399
|
-
const id = typeof shortId === "string" && safe.test(shortId) ? `dec_${shortId}` : typeof decisionId === "string" && safe.test(decisionId) ? decisionId : void 0;
|
|
400
|
-
return id ? [`To reconcile, run: prim reconcile ${id}`] : [];
|
|
401
|
-
});
|
|
402
|
-
const reason = [response.message, ...new Set(directives)].filter(Boolean).join("\n");
|
|
403
|
-
return {
|
|
404
|
-
verdict,
|
|
405
|
-
conflicts: response.conflicts,
|
|
406
|
-
reason,
|
|
407
|
-
additionalContext: ["warn", "ask", "block"].includes(response.verdict) ? reason : "",
|
|
408
|
-
truncated: false,
|
|
409
|
-
unavailable: response.verdict === "unavailable" ? response.message || response.reasonCode : void 0
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
function unverifiedResult(message) {
|
|
413
|
-
return {
|
|
414
|
-
verdict: "unavailable",
|
|
415
|
-
conflicts: [],
|
|
416
|
-
reason: "",
|
|
417
|
-
additionalContext: "",
|
|
418
|
-
truncated: false,
|
|
419
|
-
unavailable: message
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
397
|
export {
|
|
424
398
|
buildHookOutput,
|
|
425
399
|
buildCodexOutput,
|
|
@@ -427,12 +401,11 @@ export {
|
|
|
427
401
|
failOpenCodex,
|
|
428
402
|
buildHermesOutput,
|
|
429
403
|
failOpenHermes,
|
|
404
|
+
extractFileTargets,
|
|
430
405
|
readHookMode,
|
|
431
406
|
demoteForMode,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
resultForPreflight,
|
|
437
|
-
unverifiedResult
|
|
407
|
+
analyzeShellTargets,
|
|
408
|
+
DEFAULT_RULES,
|
|
409
|
+
scrub,
|
|
410
|
+
scrubFromCwd
|
|
438
411
|
};
|
|
@@ -64,6 +64,13 @@ function stripControlChars(text) {
|
|
|
64
64
|
}
|
|
65
65
|
return out;
|
|
66
66
|
}
|
|
67
|
+
var MAX_HEALTH_ERROR_LENGTH = 240;
|
|
68
|
+
function boundedHealthError(value) {
|
|
69
|
+
if (!value) return void 0;
|
|
70
|
+
const clean = stripControlChars(value).replace(/\s+/g, " ").trim();
|
|
71
|
+
if (!clean) return void 0;
|
|
72
|
+
return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
|
|
73
|
+
}
|
|
67
74
|
function stripAnsi(text) {
|
|
68
75
|
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
69
76
|
}
|
|
@@ -97,6 +104,7 @@ export {
|
|
|
97
104
|
bold,
|
|
98
105
|
colorForArea,
|
|
99
106
|
stripControlChars,
|
|
107
|
+
boundedHealthError,
|
|
100
108
|
stripAnsi,
|
|
101
109
|
requireDurableIngestAcknowledgement
|
|
102
110
|
};
|
|
@@ -76,7 +76,13 @@ function commandMatchesBin(command, bin) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// src/lib/bin-cache.ts
|
|
79
|
-
var CACHED_BINS = [
|
|
79
|
+
var CACHED_BINS = [
|
|
80
|
+
"prim",
|
|
81
|
+
"prim-hook",
|
|
82
|
+
"prim-pre-tool-use",
|
|
83
|
+
"prim-post-tool-use",
|
|
84
|
+
"prim-post-commit"
|
|
85
|
+
];
|
|
80
86
|
function binCacheDir() {
|
|
81
87
|
const base = process.env.XDG_CACHE_HOME || join2(homedir(), ".cache");
|
|
82
88
|
return join2(base, "prim", "bin");
|