@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.
@@ -1,44 +1,100 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ commitAttributionFromEnvironment,
3
4
  toCommitMove
4
- } from "../chunk-YDBZTFW5.js";
5
+ } from "../chunk-QGVHLP5J.js";
5
6
  import {
6
7
  appendMove,
7
8
  resolveOrg
8
- } from "../chunk-S4LFRVGD.js";
9
+ } from "../chunk-HIMWM526.js";
9
10
  import {
10
- isRepoActiveForCapture
11
- } from "../chunk-LDQZASIF.js";
12
- import "../chunk-I3HIPAIR.js";
11
+ getOrCreateWorkspaceId
12
+ } from "../chunk-UVIWZWM6.js";
13
+ import {
14
+ githubRepositoryFullName,
15
+ isRepoActiveForCapture,
16
+ repoSyncId,
17
+ resolveRepositoryContext
18
+ } from "../chunk-XTNMSS4C.js";
19
+ import "../chunk-CPX3BUOC.js";
13
20
 
14
21
  // src/hooks/post-commit.ts
15
- import { execSync, spawn } from "child_process";
22
+ import { execFileSync, spawn } from "child_process";
16
23
  import { readFileSync } from "fs";
17
24
  import { dirname, join } from "path";
18
25
  import { fileURLToPath } from "url";
19
26
  var here = dirname(fileURLToPath(import.meta.url));
20
- function git(args) {
27
+ var GIT_TIMEOUT_MS = 1e3;
28
+ var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
29
+ function gitText(args) {
21
30
  try {
22
- return execSync(`git ${args}`, {
31
+ return execFileSync("git", args, {
23
32
  encoding: "utf-8",
24
- stdio: ["ignore", "pipe", "ignore"]
33
+ stdio: ["ignore", "pipe", "ignore"],
34
+ timeout: GIT_TIMEOUT_MS
25
35
  }).trim();
26
36
  } catch {
27
37
  return;
28
38
  }
29
39
  }
40
+ function gitBytes(args) {
41
+ try {
42
+ return execFileSync("git", args, {
43
+ encoding: "buffer",
44
+ stdio: ["ignore", "pipe", "ignore"],
45
+ timeout: GIT_TIMEOUT_MS
46
+ });
47
+ } catch {
48
+ return;
49
+ }
50
+ }
51
+ function parseNulPaths(value) {
52
+ if (value.length === 0) return { files: [], complete: true };
53
+ const chunks = [];
54
+ let start = 0;
55
+ for (let i = 0; i < value.length; i += 1) {
56
+ if (value[i] !== 0) continue;
57
+ chunks.push(value.subarray(start, i));
58
+ start = i + 1;
59
+ }
60
+ if (start !== value.length || chunks.some((chunk) => chunk.length === 0)) {
61
+ return { files: [], complete: false };
62
+ }
63
+ const files = [];
64
+ for (const chunk of chunks) {
65
+ const file = chunk.toString("utf8");
66
+ if (!Buffer.from(file, "utf8").equals(chunk)) {
67
+ return { files: [], complete: false };
68
+ }
69
+ files.push(file);
70
+ }
71
+ return { files: [...new Set(files)], complete: true };
72
+ }
30
73
  function readCommit() {
31
- const sha = git("rev-parse HEAD");
32
- if (!sha) {
74
+ const snapshotProvided = process.env.PRIM_COMMIT_SHA !== void 0;
75
+ const sha = snapshotProvided ? process.env.PRIM_COMMIT_SHA : gitText(["rev-parse", "HEAD"]);
76
+ if (!sha || !FULL_SHA_RE.test(sha)) {
33
77
  return null;
34
78
  }
35
- const branch = git("rev-parse --abbrev-ref HEAD");
36
- const files = (git("diff-tree --no-commit-id --name-only -r -m --root HEAD") ?? "").split("\n").filter((f) => f.length > 0);
79
+ const rawBranch = process.env.PRIM_COMMIT_BRANCH !== void 0 ? process.env.PRIM_COMMIT_BRANCH : gitText(["rev-parse", "--abbrev-ref", "HEAD"]);
80
+ const branch = rawBranch && rawBranch === rawBranch.trim() && ![...rawBranch].some((char) => char < " " || char === "\x7F") ? rawBranch : void 0;
81
+ const rawFiles = gitBytes([
82
+ "diff-tree",
83
+ "--no-commit-id",
84
+ "--name-only",
85
+ "-z",
86
+ "-r",
87
+ "-m",
88
+ "--root",
89
+ sha
90
+ ]);
91
+ const { files, complete } = rawFiles === void 0 ? { files: [], complete: false } : parseNulPaths(rawFiles);
37
92
  return {
38
93
  sha,
39
- parentSha: git("rev-parse --verify --quiet HEAD^") || void 0,
94
+ parentSha: gitText(["rev-parse", "--verify", "--quiet", `${sha}^`]) || void 0,
40
95
  branch: branch && branch !== "HEAD" ? branch : void 0,
41
- files
96
+ files,
97
+ filesComplete: complete
42
98
  };
43
99
  }
44
100
  function resolveCliVersion() {
@@ -57,14 +113,24 @@ function spawnBackgroundFlush() {
57
113
  }).unref();
58
114
  }
59
115
  function runPostCommit() {
60
- const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
116
+ const cwd = gitText(["rev-parse", "--show-toplevel"]) ?? process.cwd();
61
117
  if (!isRepoActiveForCapture(cwd)) {
62
118
  return;
63
119
  }
64
120
  const commit = readCommit();
65
121
  if (commit) {
66
- const move = toCommitMove(commit, resolveCliVersion(), cwd);
67
- const { orgId } = resolveOrg({ sessionId: "", cwd });
122
+ const repository = resolveRepositoryContext(cwd);
123
+ const identity = getOrCreateWorkspaceId(cwd);
124
+ const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
125
+ const attribution = commitAttributionFromEnvironment(process.env, workspaceId);
126
+ const move = toCommitMove(commit, resolveCliVersion(), cwd, {
127
+ repository,
128
+ repoFullName: githubRepositoryFullName(cwd) ?? void 0,
129
+ repoSyncId: repoSyncId(cwd),
130
+ workspaceId,
131
+ attribution
132
+ });
133
+ const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd });
68
134
  appendMove(move, orgId);
69
135
  spawnBackgroundFlush();
70
136
  }
@@ -5,32 +5,34 @@ import {
5
5
  requireDurableIngestAcknowledgement
6
6
  } from "../chunk-JQFN6LBO.js";
7
7
  import {
8
- resolvePreflightTargets
9
- } from "../chunk-B4SHJYIN.js";
8
+ enrichHookPayloadWithFileRefs,
9
+ preserveHookFileMetadata
10
+ } from "../chunk-DA4BMHEB.js";
10
11
  import {
11
12
  postToolInvocationId,
12
13
  toMove
13
- } from "../chunk-YDBZTFW5.js";
14
+ } from "../chunk-QGVHLP5J.js";
14
15
  import {
15
16
  appendMove,
16
17
  resolveOrg
17
- } from "../chunk-S4LFRVGD.js";
18
+ } from "../chunk-HIMWM526.js";
18
19
  import {
19
20
  scrubFromCwd
20
- } from "../chunk-GUXBUBQ7.js";
21
+ } from "../chunk-GF5QII7T.js";
21
22
  import {
22
23
  getOrCreateWorkspaceId
23
- } from "../chunk-IMAIBPUC.js";
24
+ } from "../chunk-UVIWZWM6.js";
24
25
  import {
25
26
  warmBinCache
26
- } from "../chunk-YIMUIPMD.js";
27
+ } from "../chunk-KBFFVYBC.js";
27
28
  import {
28
- isRepoActive,
29
- repoSyncId
30
- } from "../chunk-LDQZASIF.js";
29
+ isRepoActiveForCapture,
30
+ repoSyncId,
31
+ resolveRepositoryContext
32
+ } from "../chunk-XTNMSS4C.js";
31
33
  import {
32
34
  getClient
33
- } from "../chunk-I3HIPAIR.js";
35
+ } from "../chunk-CPX3BUOC.js";
34
36
  import {
35
37
  normalizeEnvelope,
36
38
  parseAgent
@@ -151,27 +153,41 @@ async function main() {
151
153
  return;
152
154
  }
153
155
  const cwd = parsed.cwd ?? process.cwd();
154
- if (!isRepoActive(cwd) || !repoSyncId(cwd)) {
156
+ if (!isRepoActiveForCapture(cwd)) {
155
157
  emit();
156
158
  return;
157
159
  }
158
- if (agent === "claude_code") {
159
- const targets = resolvePreflightTargets({
160
- toolName,
161
- toolInput: envelope.tool_input,
162
- agent,
163
- cwd
164
- });
165
- if (targets.mutation === "none") {
166
- emit();
167
- return;
168
- }
160
+ const resolvedRepository = resolveRepositoryContext(cwd);
161
+ if (!resolvedRepository) {
162
+ emit();
163
+ return;
164
+ }
165
+ const repository = { ...resolvedRepository, repoSyncId: repoSyncId(cwd) };
166
+ const enrichment = enrichHookPayloadWithFileRefs({
167
+ parsed,
168
+ agent,
169
+ cwd,
170
+ repository
171
+ });
172
+ const { resolution } = enrichment;
173
+ if (resolution.shellMutation === "none") {
174
+ emit();
175
+ return;
176
+ }
177
+ if (resolution.shellMutation === void 0 && resolution.fileRefs.length === 0) {
178
+ emit();
179
+ return;
169
180
  }
181
+ const enriched = enrichment.parsed;
170
182
  const identity = getOrCreateWorkspaceId(cwd);
171
183
  const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
172
- const invocationId = postToolInvocationId(parsed, agent);
173
- const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
174
- const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
184
+ const invocationId = postToolInvocationId(enriched, agent);
185
+ const base = toMove(enriched, resolveCliVersion(), agent, workspaceId, repository, invocationId);
186
+ const scrubbed = scrubFromCwd(enriched, cwd);
187
+ const move = {
188
+ ...base,
189
+ payload: preserveHookFileMetadata(scrubbed, resolution)
190
+ };
175
191
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
176
192
  try {
177
193
  const result = await deliverPostToolMove(move, orgId);
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  checkAffectedDecisions,
4
4
  formatDecisionsWarning
5
- } from "../chunk-YITJ2H3B.js";
6
- import "../chunk-I3HIPAIR.js";
5
+ } from "../chunk-WS3MCNFD.js";
6
+ import "../chunk-CPX3BUOC.js";
7
7
  import "../chunk-UTKQTZHL.js";
8
8
 
9
9
  // src/hooks/pre-commit.ts
@@ -1,34 +1,126 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- PREFLIGHT_PROTOCOL_VERSION,
3
+ DEFAULT_RULES,
4
+ analyzeShellTargets,
4
5
  buildCodexOutput,
5
6
  buildHermesOutput,
6
7
  buildHookOutput,
7
8
  demoteForMode,
9
+ extractFileTargets,
8
10
  failOpenCodex,
9
11
  failOpenHermes,
10
12
  failOpenOutput,
11
- proposalFor,
12
13
  readHookMode,
13
- requestPreflight,
14
- resolvePreflightTargets,
15
- resultForPreflight,
16
- unverifiedResult
17
- } from "../chunk-B4SHJYIN.js";
18
- import "../chunk-GUXBUBQ7.js";
14
+ scrub
15
+ } from "../chunk-GF5QII7T.js";
19
16
  import {
20
17
  warmBinCache
21
- } from "../chunk-YIMUIPMD.js";
18
+ } from "../chunk-KBFFVYBC.js";
22
19
  import {
20
+ canonicalGitRoot,
21
+ canonicalRepositoryPath,
23
22
  isRepoActive,
24
23
  repoSyncId
25
- } from "../chunk-LDQZASIF.js";
26
- import "../chunk-I3HIPAIR.js";
24
+ } from "../chunk-XTNMSS4C.js";
25
+ import {
26
+ getClient
27
+ } from "../chunk-CPX3BUOC.js";
27
28
  import {
28
29
  normalizeEnvelope,
29
30
  parseAgent
30
31
  } from "../chunk-TCDUH7AN.js";
31
32
 
33
+ // src/hooks/preflight-v3.ts
34
+ import { Buffer as Buffer2 } from "buffer";
35
+ var PREFLIGHT_PROTOCOL_VERSION = 3;
36
+ var PREFLIGHT_TIMEOUT_MS = 6500;
37
+ var MAX_PREFLIGHT_PATHS = 32;
38
+ var MAX_PROPOSAL_BYTES = 6144;
39
+ function resolvePreflightTargets(args) {
40
+ let rawPaths;
41
+ let coverage = "complete";
42
+ let mutation = "present";
43
+ if (args.agent === "claude_code" && args.toolName === "Bash") {
44
+ const command = args.toolInput && typeof args.toolInput === "object" ? args.toolInput.command : void 0;
45
+ if (typeof command !== "string") {
46
+ return { paths: [], coverage: "unverified", mutation: "present" };
47
+ }
48
+ const shell = analyzeShellTargets(command);
49
+ rawPaths = shell.paths;
50
+ coverage = shell.coverage;
51
+ mutation = shell.mutation;
52
+ } else {
53
+ const extracted = extractFileTargets(args.toolName, args.toolInput, args.agent);
54
+ if (!extracted) return { paths: [], coverage: "complete", mutation: "none" };
55
+ rawPaths = extracted.paths;
56
+ if (!extracted.complete) coverage = "unverified";
57
+ }
58
+ const root = canonicalGitRoot(args.cwd);
59
+ const paths = /* @__PURE__ */ new Set();
60
+ for (const rawPath of rawPaths) {
61
+ const canonical = canonicalRepositoryPath(rawPath, args.cwd, root);
62
+ if (canonical) paths.add(canonical);
63
+ else coverage = "unverified";
64
+ }
65
+ const bounded = [...paths];
66
+ if (bounded.length > MAX_PREFLIGHT_PATHS) coverage = "unverified";
67
+ return { paths: bounded.slice(0, MAX_PREFLIGHT_PATHS), coverage, mutation };
68
+ }
69
+ function truncateUtf8(value, maxBytes) {
70
+ const encoded = Buffer2.from(value);
71
+ if (encoded.byteLength <= maxBytes) return value;
72
+ return encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
73
+ }
74
+ function proposalFor(toolInput) {
75
+ const redacted = scrub(toolInput, DEFAULT_RULES);
76
+ return truncateUtf8(JSON.stringify(redacted) ?? "", MAX_PROPOSAL_BYTES);
77
+ }
78
+ function parsePreflightResponse(value) {
79
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
80
+ const response = value;
81
+ const verdicts = /* @__PURE__ */ new Set(["allow", "warn", "ask", "block", "unavailable"]);
82
+ 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)) {
83
+ return null;
84
+ }
85
+ return response;
86
+ }
87
+ async function requestPreflight(request, signal = AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS)) {
88
+ const response = await getClient().post("/api/cli/decisions/conflict-check", request, {
89
+ signal,
90
+ quietRefresh: true
91
+ });
92
+ return parsePreflightResponse(response);
93
+ }
94
+ function resultForPreflight(response) {
95
+ const verdict = response.verdict === "block" ? "deny" : response.verdict;
96
+ const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
97
+ const directives = response.conflicts.flatMap((value) => {
98
+ if (!value || typeof value !== "object") return [];
99
+ const { decisionId, shortId } = value;
100
+ const id = typeof shortId === "string" && safe.test(shortId) ? `dec_${shortId}` : typeof decisionId === "string" && safe.test(decisionId) ? decisionId : void 0;
101
+ return id ? [`To reconcile, run: prim reconcile ${id}`] : [];
102
+ });
103
+ const reason = [response.message, ...new Set(directives)].filter(Boolean).join("\n");
104
+ return {
105
+ verdict,
106
+ conflicts: response.conflicts,
107
+ reason,
108
+ additionalContext: ["warn", "ask", "block"].includes(response.verdict) ? reason : "",
109
+ truncated: false,
110
+ unavailable: response.verdict === "unavailable" ? response.message || response.reasonCode : void 0
111
+ };
112
+ }
113
+ function unverifiedResult(message) {
114
+ return {
115
+ verdict: "unavailable",
116
+ conflicts: [],
117
+ reason: "",
118
+ additionalContext: "",
119
+ truncated: false,
120
+ unavailable: message
121
+ };
122
+ }
123
+
32
124
  // src/hooks/pre-tool-use.ts
33
125
  var STDIN_TIMEOUT_MS = 1e3;
34
126
  async function readStdin() {
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ enrichHookPayloadWithFileRefs,
4
+ preserveHookFileMetadata
5
+ } from "../chunk-DA4BMHEB.js";
2
6
  import {
3
7
  postToolInvocationId,
4
8
  shouldFlushAfter,
5
9
  toMove
6
- } from "../chunk-YDBZTFW5.js";
10
+ } from "../chunk-QGVHLP5J.js";
7
11
  import {
8
12
  appendMove,
9
13
  resolveOrg
10
- } from "../chunk-S4LFRVGD.js";
14
+ } from "../chunk-HIMWM526.js";
11
15
  import {
12
16
  scrubFromCwd
13
- } from "../chunk-GUXBUBQ7.js";
17
+ } from "../chunk-GF5QII7T.js";
14
18
  import {
15
19
  buildHookOutput,
16
20
  handoffHookOutput
@@ -20,17 +24,19 @@ import {
20
24
  acknowledgeDecisionFeedback,
21
25
  leaseDecisionFeedback,
22
26
  renderFeedback
23
- } from "../chunk-R4CUX6FY.js";
27
+ } from "../chunk-KHZNYEUV.js";
24
28
  import {
25
29
  getOrCreateWorkspaceId
26
- } from "../chunk-IMAIBPUC.js";
30
+ } from "../chunk-UVIWZWM6.js";
27
31
  import {
28
32
  warmBinCache
29
- } from "../chunk-YIMUIPMD.js";
33
+ } from "../chunk-KBFFVYBC.js";
30
34
  import {
31
- isRepoActiveForCapture
32
- } from "../chunk-LDQZASIF.js";
33
- import "../chunk-I3HIPAIR.js";
35
+ isRepoActiveForCapture,
36
+ repoSyncId,
37
+ resolveRepositoryContext
38
+ } from "../chunk-XTNMSS4C.js";
39
+ import "../chunk-CPX3BUOC.js";
34
40
  import {
35
41
  normalizeEnvelope,
36
42
  parseAgent
@@ -90,10 +96,25 @@ async function main() {
90
96
  }
91
97
  const identity = getOrCreateWorkspaceId(cwd);
92
98
  const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
99
+ const resolvedRepository = resolveRepositoryContext(cwd);
100
+ const repository = resolvedRepository ? { ...resolvedRepository, repoSyncId: repoSyncId(cwd) } : null;
93
101
  try {
94
- const invocationId = postToolInvocationId(parsed, agent);
95
- const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
96
- const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
102
+ const enrichment = repository ? enrichHookPayloadWithFileRefs({ parsed, agent, cwd, repository }) : void 0;
103
+ const enriched = enrichment?.parsed ?? parsed;
104
+ const invocationId = postToolInvocationId(enriched, agent);
105
+ const base = toMove(
106
+ enriched,
107
+ resolveCliVersion(),
108
+ agent,
109
+ workspaceId,
110
+ repository,
111
+ invocationId
112
+ );
113
+ const scrubbed = scrubFromCwd(enriched, cwd);
114
+ const move = {
115
+ ...base,
116
+ payload: enrichment ? preserveHookFileMetadata(scrubbed, enrichment.resolution) : scrubbed
117
+ };
97
118
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
98
119
  appendMove(move, orgId);
99
120
  if (shouldFlushAfter(move.eventType)) {
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  bindRepository,
4
+ ensureEffectivePostCommitHook,
4
5
  hasUsableCodexGuidance,
5
6
  refreshClaudePlugins
6
- } from "../chunk-N4R7EXZT.js";
7
+ } from "../chunk-WNIY6Z6N.js";
7
8
  import {
8
9
  buildHookOutput,
9
10
  handoffHookOutput
@@ -13,23 +14,25 @@ import {
13
14
  acknowledgeDecisionFeedback,
14
15
  leaseDecisionFeedback,
15
16
  renderFeedback
16
- } from "../chunk-R4CUX6FY.js";
17
+ } from "../chunk-KHZNYEUV.js";
17
18
  import {
18
19
  getOrCreateWorkspaceId
19
- } from "../chunk-IMAIBPUC.js";
20
+ } from "../chunk-UVIWZWM6.js";
20
21
  import {
21
22
  binFile,
22
23
  warmBinCache
23
- } from "../chunk-YIMUIPMD.js";
24
+ } from "../chunk-KBFFVYBC.js";
24
25
  import {
25
26
  gitToplevel,
26
27
  isRepoActiveForCapture,
27
- repoSyncId
28
- } from "../chunk-LDQZASIF.js";
28
+ repoActiveFlag,
29
+ repoSyncId,
30
+ setRepoActive
31
+ } from "../chunk-XTNMSS4C.js";
29
32
  import {
30
33
  getSiteUrl,
31
34
  isSessionEnded
32
- } from "../chunk-I3HIPAIR.js";
35
+ } from "../chunk-CPX3BUOC.js";
33
36
  import {
34
37
  daemonRequest
35
38
  } from "../chunk-UTKQTZHL.js";
@@ -46,9 +49,13 @@ function kickDaemonEnsure(options = {}) {
46
49
  return false;
47
50
  }
48
51
  try {
52
+ const args = [primEntry, "daemon", "ensure"];
53
+ if ((options.platform ?? process.platform) === "darwin") {
54
+ args.push("--latest-bootstrap");
55
+ }
49
56
  const child = (options.spawnProcess ?? spawn)(
50
57
  options.nodeEntry ?? process.execPath,
51
- [primEntry, "daemon", "ensure"],
58
+ args,
52
59
  { detached: true, stdio: "ignore" }
53
60
  );
54
61
  child.unref();
@@ -81,6 +88,20 @@ async function activeProjectRoot(cwd) {
81
88
  try {
82
89
  const root = gitToplevel(cwd);
83
90
  if (!root || !isRepoActiveForCapture(cwd)) return null;
91
+ let shellGateActive = repoActiveFlag(root) === "true";
92
+ if (!shellGateActive) {
93
+ try {
94
+ setRepoActive(root, true);
95
+ shellGateActive = true;
96
+ } catch {
97
+ }
98
+ }
99
+ if (shellGateActive) {
100
+ try {
101
+ ensureEffectivePostCommitHook(root);
102
+ } catch {
103
+ }
104
+ }
84
105
  if (!repoSyncId(root)) {
85
106
  try {
86
107
  await bindRepository(root, {