@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.
@@ -1,10 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ PREFLIGHT_PROTOCOL_VERSION,
4
+ buildHermesOutput,
5
+ buildHookOutput,
6
+ demoteForMode,
7
+ failOpenHermes,
8
+ failOpenOutput,
9
+ parsePreflightResponse,
10
+ proposalFor,
11
+ readHookMode,
12
+ resolvePreflightTargets,
13
+ resultForPreflight,
14
+ unverifiedResult
15
+ } from "../chunk-SWWBK4MP.js";
16
+ import "../chunk-GUXBUBQ7.js";
2
17
  import {
3
18
  warmBinCache
4
- } from "../chunk-HSZPTVKU.js";
19
+ } from "../chunk-YIMUIPMD.js";
5
20
  import {
6
- isRepoActiveForCapture
7
- } from "../chunk-R5ZJRSLV.js";
21
+ isRepoActive,
22
+ repoSyncId
23
+ } from "../chunk-LDQZASIF.js";
8
24
  import {
9
25
  getClient,
10
26
  getSiteUrl
@@ -17,206 +33,6 @@ import {
17
33
  parseAgent
18
34
  } from "../chunk-TCDUH7AN.js";
19
35
 
20
- // src/hooks/pre-tool-use-scoring.ts
21
- import { isAbsolute, relative, sep } from "path";
22
- var VERDICT_SEVERITY = {
23
- allow: 0,
24
- warn: 1,
25
- ask: 2,
26
- deny: 3
27
- };
28
- function toRepoRelative(filePath, cwd) {
29
- const rel = isAbsolute(filePath) ? relative(cwd, filePath) : filePath;
30
- return sep === "/" ? rel : rel.split(sep).join("/");
31
- }
32
- function aggregateCheckResults(results) {
33
- let worst = "allow";
34
- for (const r of results) {
35
- if (r.verdict !== "unavailable" && VERDICT_SEVERITY[r.verdict] > VERDICT_SEVERITY[worst]) {
36
- worst = r.verdict;
37
- }
38
- }
39
- return worst;
40
- }
41
- function anyUnverified(results) {
42
- return results.some((r) => r.verdict === "unavailable" || r.truncated);
43
- }
44
- function unverifiedNote(results) {
45
- const causes = [];
46
- const unavailable = results.find((r) => r.verdict === "unavailable");
47
- if (unavailable) {
48
- causes.push(
49
- unavailable.unavailable ? `decision check skipped \u2014 ${unavailable.unavailable}` : "decision check skipped \u2014 not verified"
50
- );
51
- }
52
- if (results.some((r) => r.truncated)) {
53
- causes.push("decision check partial \u2014 conflict set truncated (per-file cap hit)");
54
- }
55
- return causes.map((c) => `[primitive] ${c}`).join("\n");
56
- }
57
- function buildHookOutput(aggregate, results, agent2 = "claude_code") {
58
- if (aggregate === "deny") {
59
- 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)";
60
- return {
61
- hookSpecificOutput: {
62
- hookEventName: "PreToolUse",
63
- permissionDecision: "deny",
64
- permissionDecisionReason: reason
65
- }
66
- };
67
- }
68
- if (agent2 === "codex" && aggregate === "ask") {
69
- const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
70
- const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
71
- const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
72
- const out = {
73
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
74
- };
75
- if (merged.length > 0) {
76
- out.hookSpecificOutput.additionalContext = merged;
77
- }
78
- return out;
79
- }
80
- if (aggregate === "ask") {
81
- 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";
82
- const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
83
- const out = {
84
- hookSpecificOutput: {
85
- hookEventName: "PreToolUse",
86
- permissionDecision: "ask",
87
- permissionDecisionReason: reason
88
- }
89
- };
90
- if (additionalContext.length > 0) {
91
- out.hookSpecificOutput.additionalContext = additionalContext;
92
- }
93
- return out;
94
- }
95
- const notes = [
96
- ...results.map((r) => r.additionalContext).filter((s) => s.length > 0),
97
- anyUnverified(results) ? unverifiedNote(results) : ""
98
- ].filter((s) => s.length > 0).join("\n");
99
- if (notes.length > 0) {
100
- return {
101
- hookSpecificOutput: {
102
- hookEventName: "PreToolUse",
103
- permissionDecision: "allow",
104
- additionalContext: notes
105
- }
106
- };
107
- }
108
- return {
109
- hookSpecificOutput: {
110
- hookEventName: "PreToolUse",
111
- permissionDecision: "allow"
112
- }
113
- };
114
- }
115
- function failOpenOutput() {
116
- return {
117
- hookSpecificOutput: {
118
- hookEventName: "PreToolUse",
119
- permissionDecision: "allow"
120
- }
121
- };
122
- }
123
- function buildHermesOutput(aggregate, results) {
124
- if (aggregate !== "deny" && aggregate !== "ask") {
125
- return {};
126
- }
127
- const reason = results.filter((r) => r.verdict === "deny" || r.verdict === "ask").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
128
- const directive = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
129
- const message = [reason, directive].filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
130
- return { action: "block", message };
131
- }
132
- function failOpenHermes() {
133
- return {};
134
- }
135
- var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
136
- var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
137
- var MOVE_FILE_RE = /^\*\*\* Move File:\s*(?<src>.+?)\s*->\s*(?<dst>.+?)\s*$/;
138
- var LINE_SPLIT_RE = /\r?\n/;
139
- function parseApplyPatchPaths(command) {
140
- const paths = /* @__PURE__ */ new Set();
141
- for (const line of command.split(LINE_SPLIT_RE)) {
142
- const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path?.trim();
143
- if (path) {
144
- paths.add(path);
145
- continue;
146
- }
147
- const move = MOVE_FILE_RE.exec(line)?.groups;
148
- if (move?.src) {
149
- paths.add(move.src.trim());
150
- if (move.dst) {
151
- paths.add(move.dst.trim());
152
- }
153
- }
154
- }
155
- return Array.from(paths);
156
- }
157
- function extractCodexFilePaths(toolName, toolInput) {
158
- if (toolName !== "apply_patch") {
159
- return [];
160
- }
161
- if (!toolInput || typeof toolInput !== "object") {
162
- return [];
163
- }
164
- const command = toolInput.command;
165
- return typeof command === "string" ? parseApplyPatchPaths(command) : [];
166
- }
167
- var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
168
- function extractHermesFilePaths(toolName, toolInput) {
169
- if (!HERMES_EDITING_TOOLS.has(toolName)) {
170
- return [];
171
- }
172
- if (!toolInput || typeof toolInput !== "object") {
173
- return [];
174
- }
175
- const input = toolInput;
176
- if (toolName === "patch" && input.mode === "patch") {
177
- return typeof input.patch === "string" ? parseApplyPatchPaths(input.patch) : [];
178
- }
179
- return typeof input.path === "string" && input.path.length > 0 ? [input.path] : [];
180
- }
181
- function extractFilePaths(toolName, toolInput, agent2 = "claude_code") {
182
- if (agent2 === "codex") {
183
- return extractCodexFilePaths(toolName, toolInput);
184
- }
185
- if (agent2 === "hermes") {
186
- return extractHermesFilePaths(toolName, toolInput);
187
- }
188
- if (!SUPPORTED_TOOLS.has(toolName)) {
189
- return [];
190
- }
191
- if (!toolInput || typeof toolInput !== "object") {
192
- return [];
193
- }
194
- const input = toolInput;
195
- if (typeof input.file_path === "string" && input.file_path.length > 0) {
196
- return [input.file_path];
197
- }
198
- return [];
199
- }
200
- function readHookMode(env) {
201
- if (env.PRIM_BYPASS === "1" || env.PRIM_BYPASS === "true") {
202
- return "off";
203
- }
204
- const mode = env.PRIM_HOOK_MODE;
205
- if (mode === "off" || mode === "warn") {
206
- return mode;
207
- }
208
- return "block";
209
- }
210
- function demoteForMode(verdict, mode) {
211
- if (mode === "off") {
212
- return "allow";
213
- }
214
- if (mode === "warn" && (verdict === "ask" || verdict === "deny")) {
215
- return "warn";
216
- }
217
- return verdict;
218
- }
219
-
220
36
  // src/hooks/pre-tool-use.ts
221
37
  var HOOK_TIMEOUT_MS = 4500;
222
38
  var STDIN_TIMEOUT_MS = 1e3;
@@ -246,23 +62,19 @@ function emit(output) {
246
62
  function failOpen() {
247
63
  return agent === "hermes" ? failOpenHermes() : failOpenOutput();
248
64
  }
249
- async function checkOneFile(file) {
250
- const fromDaemon = await daemonRequest(
251
- "conflict_check",
252
- // callerEnv lets a staging-bound daemon refuse a prod-context gate check
253
- // (and vice versa) so we fall through to a direct call against our own env.
254
- { file, callerEnv: getSiteUrl() },
255
- { timeoutMs: DAEMON_TIMEOUT_MS }
256
- );
257
- if (fromDaemon) {
258
- return fromDaemon;
65
+ function invocationId(envelope) {
66
+ const value = agent === "hermes" ? envelope.extra?.tool_call_id : envelope.tool_use_id;
67
+ return typeof value === "string" && value.length > 0 ? value : void 0;
68
+ }
69
+ function emitUnverified(message) {
70
+ const result = unverifiedResult(message);
71
+ if (agent === "hermes") {
72
+ process.stderr.write(`[primitive] ${message}
73
+ `);
74
+ emit(failOpenHermes());
75
+ } else {
76
+ emit(buildHookOutput("allow", [result], agent));
259
77
  }
260
- const client = getClient();
261
- return await client.post(
262
- "/api/cli/decisions/conflict-check",
263
- { file },
264
- { signal: AbortSignal.timeout(HOOK_TIMEOUT_MS) }
265
- );
266
78
  }
267
79
  async function main() {
268
80
  warmBinCache();
@@ -295,28 +107,75 @@ async function main() {
295
107
  }
296
108
  const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
297
109
  const cwd = typeof envelope.cwd === "string" && envelope.cwd.length > 0 ? envelope.cwd : process.cwd();
298
- const files = extractFilePaths(toolName, envelope.tool_input, agent).map(
299
- (f) => toRepoRelative(f, cwd)
300
- );
301
- if (files.length === 0) {
110
+ const targets = resolvePreflightTargets({
111
+ toolName,
112
+ toolInput: envelope.tool_input,
113
+ agent,
114
+ cwd
115
+ });
116
+ if (targets.mutation === "none") {
302
117
  emit(failOpen());
303
118
  return;
304
119
  }
305
- if (!isRepoActiveForCapture(cwd)) {
120
+ if (!isRepoActive(cwd)) {
306
121
  emit(failOpen());
307
122
  return;
308
123
  }
309
- let results;
124
+ if (targets.paths.length === 0) {
125
+ emitUnverified("mutation targets could not be determined; enforcement not verified");
126
+ return;
127
+ }
128
+ const binding = repoSyncId(cwd);
129
+ const sessionId = envelope.session_id;
130
+ const callId = invocationId(envelope);
131
+ if (!binding || typeof sessionId !== "string" || !sessionId || !callId) {
132
+ emitUnverified("repository binding or tool invocation identity is unavailable");
133
+ return;
134
+ }
135
+ const request = {
136
+ protocolVersion: PREFLIGHT_PROTOCOL_VERSION,
137
+ agent,
138
+ sessionId,
139
+ invocationId: callId,
140
+ repoSyncId: binding,
141
+ paths: targets.paths,
142
+ coverage: targets.coverage,
143
+ proposal: proposalFor(envelope.tool_input)
144
+ };
145
+ let result;
310
146
  try {
311
- results = await Promise.all(files.map((f) => checkOneFile(f)));
147
+ const fromDaemon = await daemonRequest(
148
+ "conflict_check",
149
+ { ...request, callerEnv: getSiteUrl() },
150
+ { timeoutMs: DAEMON_TIMEOUT_MS }
151
+ );
152
+ let response = parsePreflightResponse(fromDaemon);
153
+ if (!response) {
154
+ const direct = await getClient().post("/api/cli/decisions/conflict-check", request, {
155
+ signal: AbortSignal.timeout(HOOK_TIMEOUT_MS),
156
+ quietRefresh: true
157
+ });
158
+ response = parsePreflightResponse(direct);
159
+ }
160
+ if (!response) {
161
+ emitUnverified("enforcement service returned an incompatible response");
162
+ return;
163
+ }
164
+ result = resultForPreflight(response);
312
165
  } catch {
313
- emit(failOpen());
166
+ emitUnverified("enforcement service unavailable; change was not verified");
314
167
  return;
315
168
  }
316
- const rawAggregate = aggregateCheckResults(results);
317
- const aggregate = demoteForMode(rawAggregate, mode);
169
+ const aggregate = demoteForMode(
170
+ result.verdict === "unavailable" ? "allow" : result.verdict,
171
+ mode
172
+ );
173
+ if (agent === "hermes" && (aggregate === "warn" || result.verdict === "unavailable")) {
174
+ process.stderr.write(`[primitive] ${result.reason || result.unavailable}
175
+ `);
176
+ }
318
177
  emit(
319
- agent === "hermes" ? buildHermesOutput(aggregate, results) : buildHookOutput(aggregate, results, agent)
178
+ agent === "hermes" ? buildHermesOutput(aggregate, [result]) : buildHookOutput(aggregate, [result], agent)
320
179
  );
321
180
  }
322
181
  main().catch(() => {
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- scrubFromCwd
4
- } from "../chunk-6LAQVM26.js";
5
2
  import {
6
3
  shouldFlushAfter,
7
4
  toMove
8
- } from "../chunk-F5QCFBJ6.js";
5
+ } from "../chunk-TNNHR3EZ.js";
9
6
  import {
10
7
  appendMove,
11
8
  resolveOrg
12
9
  } from "../chunk-BOTKPLTI.js";
10
+ import {
11
+ scrubFromCwd
12
+ } from "../chunk-GUXBUBQ7.js";
13
13
  import {
14
14
  buildHookOutput,
15
15
  handoffHookOutput
@@ -25,10 +25,10 @@ import {
25
25
  } from "../chunk-IMAIBPUC.js";
26
26
  import {
27
27
  warmBinCache
28
- } from "../chunk-HSZPTVKU.js";
28
+ } from "../chunk-YIMUIPMD.js";
29
29
  import {
30
30
  isRepoActiveForCapture
31
- } from "../chunk-R5ZJRSLV.js";
31
+ } from "../chunk-LDQZASIF.js";
32
32
  import "../chunk-DWDEQRWN.js";
33
33
  import {
34
34
  normalizeEnvelope,
@@ -90,7 +90,10 @@ async function main() {
90
90
  const identity = getOrCreateWorkspaceId(cwd);
91
91
  const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
92
92
  try {
93
- const base = toMove(parsed, resolveCliVersion(), agent, workspaceId);
93
+ const successfulPost = parsed.hook_event_name === "PostToolUse";
94
+ const rawInvocationId = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
95
+ const invocationId = successfulPost && typeof rawInvocationId === "string" ? rawInvocationId : void 0;
96
+ const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
94
97
  const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
95
98
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
96
99
  appendMove(move, orgId);
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  hasUsableCodexGuidance,
4
4
  refreshClaudePlugins
5
- } from "../chunk-ROL7SDXX.js";
5
+ } from "../chunk-GQURYENF.js";
6
6
  import {
7
7
  buildHookOutput,
8
8
  handoffHookOutput
@@ -19,11 +19,11 @@ import {
19
19
  import {
20
20
  binFile,
21
21
  warmBinCache
22
- } from "../chunk-HSZPTVKU.js";
22
+ } from "../chunk-YIMUIPMD.js";
23
23
  import {
24
24
  gitToplevel,
25
25
  isRepoActiveForCapture
26
- } from "../chunk-R5ZJRSLV.js";
26
+ } from "../chunk-LDQZASIF.js";
27
27
  import {
28
28
  getSiteUrl,
29
29
  isSessionEnded