@primitive.ai/prim 0.1.0-alpha.61 → 0.1.0-alpha.63

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.
@@ -153,10 +153,10 @@ var CREDENTIAL_FILE_MODE = 384;
153
153
  var REFRESH_THRESHOLD_MS = 6e4;
154
154
  var DEFAULT_API_URL = "https://api.getprimitive.ai";
155
155
  var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
156
- function loadEnvFile() {
156
+ function loadEnvFile(cwd = process.cwd()) {
157
157
  const envVars = {};
158
158
  for (const file of [".env.local", ".env"]) {
159
- const filePath = resolve(process.cwd(), file);
159
+ const filePath = resolve(cwd, file);
160
160
  if (!existsSync2(filePath)) continue;
161
161
  const content = readFileSync2(filePath, "utf-8");
162
162
  for (const line of content.split("\n")) {
@@ -194,8 +194,11 @@ function resolveAuthCredential() {
194
194
  return envToken ? { token: envToken, source: "env_file" } : void 0;
195
195
  }
196
196
  function getSiteUrl() {
197
- if (process.env.PRIM_API_URL) return process.env.PRIM_API_URL;
198
- return loadEnvFile().PRIM_API_URL ?? DEFAULT_API_URL;
197
+ return getSiteUrlForCwd(process.cwd(), process.env.PRIM_API_URL);
198
+ }
199
+ function getSiteUrlForCwd(cwd, primApiUrl) {
200
+ if (primApiUrl) return primApiUrl;
201
+ return loadEnvFile(cwd).PRIM_API_URL ?? DEFAULT_API_URL;
199
202
  }
200
203
  function getJwtExpiry(token) {
201
204
  const parts = token.split(".");
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-I3HIPAIR.js";
3
+ } from "./chunk-CPX3BUOC.js";
4
4
 
5
5
  // src/journal.ts
6
6
  import {
@@ -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");
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-I3HIPAIR.js";
3
+ } from "./chunk-CPX3BUOC.js";
4
4
 
5
5
  // src/decisions/feedback.ts
6
6
  var FEEDBACK_PROTOCOL_VERSION = 1;
@@ -0,0 +1,117 @@
1
+ import {
2
+ isCanonicalWorkspaceId
3
+ } from "./chunk-UVIWZWM6.js";
4
+ import {
5
+ isValidRepoSyncId
6
+ } from "./chunk-XTNMSS4C.js";
7
+
8
+ // src/hooks/prim-hook-core.ts
9
+ import { createHash, randomUUID } from "crypto";
10
+ import { platform } from "os";
11
+
12
+ // src/protocol/move.ts
13
+ var LEGACY_ENVELOPE_VERSION = 1;
14
+ var AGENT_ENVELOPE_VERSION = 3;
15
+ var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
16
+
17
+ // src/hooks/prim-hook-core.ts
18
+ function postToolInvocationId(parsed, agent) {
19
+ if (parsed.hook_event_name !== "PostToolUse") return void 0;
20
+ const raw = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
21
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
22
+ }
23
+ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, repository, invocationId) {
24
+ const sessionId = parsed.session_id ?? "";
25
+ const eventType = parsed.hook_event_name ?? "unknown";
26
+ const moveId = eventType === "PostToolUse" && sessionId && invocationId ? `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}` : randomUUID();
27
+ return {
28
+ moveId,
29
+ capturedAt: Date.now(),
30
+ sessionId,
31
+ eventType,
32
+ payload: parsed,
33
+ env: {
34
+ cwd: parsed.cwd ?? process.cwd(),
35
+ cliVersion,
36
+ osPlatform: platform(),
37
+ ...repository ? {
38
+ repoRoot: repository.repoRoot,
39
+ repoKey: repository.repoKey,
40
+ gitRoot: repository.repoRoot,
41
+ repoFullName: repository.repoFullName,
42
+ repoSyncId: repository.repoSyncId
43
+ } : {},
44
+ ...workspaceId ? { workspaceId } : {}
45
+ },
46
+ envelopeVersion: AGENT_ENVELOPE_VERSION,
47
+ producer: agent,
48
+ ...invocationId ? { invocationId } : {}
49
+ };
50
+ }
51
+ var MAX_CLASSIFIER_ID_CHARS = 256;
52
+ var INVALID_CLASSIFIER_ID_RE = /[\u0000-\u001f\u007f\uD800-\uDFFF]/u;
53
+ function validSessionHint(value) {
54
+ return typeof value === "string" && value.trim().length > 0 && value === value.trim() && value.length <= MAX_CLASSIFIER_ID_CHARS && !INVALID_CLASSIFIER_ID_RE.test(value);
55
+ }
56
+ function commitAttributionFromEnvironment(env, workspaceId) {
57
+ if (env.HERMES_INTERACTIVE) return void 0;
58
+ if (!workspaceId || !isCanonicalWorkspaceId(workspaceId)) return void 0;
59
+ const candidates = [
60
+ validSessionHint(env.CLAUDE_CODE_SESSION_ID) ? { sessionId: env.CLAUDE_CODE_SESSION_ID, producer: "claude_code" } : void 0,
61
+ validSessionHint(env.CODEX_THREAD_ID) ? { sessionId: env.CODEX_THREAD_ID, producer: "codex" } : void 0
62
+ ].filter((candidate) => candidate !== void 0);
63
+ return candidates.length === 1 ? candidates[0] : void 0;
64
+ }
65
+ function toCommitMove(commit, cliVersion, cwd, context = {}) {
66
+ const { repository, repoFullName, repoSyncId, workspaceId, attribution } = context;
67
+ const validatedRepoSyncId = isValidRepoSyncId(repoSyncId) ? repoSyncId : void 0;
68
+ const validatedWorkspaceId = workspaceId && isCanonicalWorkspaceId(workspaceId) ? workspaceId : void 0;
69
+ const validatedAttribution = validatedWorkspaceId ? attribution : void 0;
70
+ const fullSha = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(commit.sha);
71
+ const moveId = validatedRepoSyncId && validatedWorkspaceId && fullSha ? `commit:v2:${createHash("sha256").update(
72
+ JSON.stringify(["commit:v2", validatedRepoSyncId, validatedWorkspaceId, commit.sha])
73
+ ).digest("hex")}` : `commit:v2:unscoped:${randomUUID()}`;
74
+ const base = {
75
+ moveId,
76
+ capturedAt: Date.now(),
77
+ sessionId: validatedAttribution?.sessionId ?? "",
78
+ eventType: "git.commit",
79
+ payload: {
80
+ kind: "git.commit",
81
+ sha: commit.sha,
82
+ parentSha: commit.parentSha,
83
+ branch: commit.branch,
84
+ changedFiles: commit.files,
85
+ changedFilesComplete: commit.filesComplete
86
+ },
87
+ env: {
88
+ cwd,
89
+ cliVersion,
90
+ osPlatform: platform(),
91
+ ...repository ? {
92
+ repoRoot: repository.repoRoot,
93
+ repoKey: repository.repoKey,
94
+ gitRoot: repository.repoRoot
95
+ } : {},
96
+ ...repoFullName ? { repoFullName } : {},
97
+ ...validatedRepoSyncId ? { repoSyncId: validatedRepoSyncId } : {},
98
+ ...validatedWorkspaceId ? { workspaceId: validatedWorkspaceId } : {}
99
+ }
100
+ };
101
+ return validatedAttribution ? {
102
+ ...base,
103
+ envelopeVersion: AGENT_ENVELOPE_VERSION,
104
+ producer: validatedAttribution.producer
105
+ } : { ...base, envelopeVersion: ENVELOPE_VERSION };
106
+ }
107
+ function shouldFlushAfter(eventType) {
108
+ return eventType === "SessionEnd";
109
+ }
110
+
111
+ export {
112
+ postToolInvocationId,
113
+ toMove,
114
+ commitAttributionFromEnvironment,
115
+ toCommitMove,
116
+ shouldFlushAfter
117
+ };
@@ -205,6 +205,7 @@ function errorCode(error) {
205
205
  }
206
206
 
207
207
  export {
208
+ isCanonicalWorkspaceId,
208
209
  inspectWorkspaceId,
209
210
  getOrCreateWorkspaceId
210
211
  };