@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.
@@ -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/preflight-v3.ts
215
- import { Buffer } from "buffer";
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
- PREFLIGHT_PROTOCOL_VERSION,
433
- resolvePreflightTargets,
434
- proposalFor,
435
- requestPreflight,
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 = ["prim", "prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
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");