@primitive.ai/prim 0.1.0-alpha.67 → 0.1.0-alpha.69

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.
Files changed (45) hide show
  1. package/README.md +39 -1
  2. package/SKILL.md +1 -1
  3. package/dist/{chunk-XPVNH3J6.js → chunk-2KW7DVFJ.js} +121 -130
  4. package/dist/chunk-3EEFL6NJ.js +529 -0
  5. package/dist/chunk-4A2XB2PC.js +310 -0
  6. package/dist/{chunk-G5D3FF3D.js → chunk-BQUJ4AFJ.js} +1 -83
  7. package/dist/chunk-CUE2LKLR.js +155 -0
  8. package/dist/chunk-FI3LIRCB.js +262 -0
  9. package/dist/{chunk-CPSE3Y3T.js → chunk-H64F6QHV.js} +27 -8
  10. package/dist/chunk-KKP2NYZ3.js +505 -0
  11. package/dist/{chunk-JQFN6LBO.js → chunk-LEMX3IOG.js} +6 -37
  12. package/dist/{chunk-U6L2RU67.js → chunk-LMRYZL3D.js} +52 -45
  13. package/dist/{chunk-XUAP572Z.js → chunk-RO3F6JYV.js} +8 -7
  14. package/dist/{chunk-TDRXHBEX.js → chunk-TJTVHDJ5.js} +41 -18
  15. package/dist/{chunk-TCDUH7AN.js → chunk-WB7AUZ4C.js} +17 -0
  16. package/dist/chunk-XGADQPCI.js +26 -0
  17. package/dist/{chunk-UN5O5SYG.js → chunk-YWO7O6QG.js} +14 -53
  18. package/dist/chunk-Z7YPFWAZ.js +917 -0
  19. package/dist/{chunk-BDGQEJA5.js → chunk-ZOSNEO5N.js} +2 -2
  20. package/dist/daemon/server.js +11804 -470
  21. package/dist/hook-runtime/daemon/server.js +13353 -0
  22. package/dist/hook-runtime/hooks/post-commit.js +1128 -0
  23. package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
  24. package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
  25. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  26. package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
  27. package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
  28. package/dist/hook-runtime/hooks/session-end.js +495 -0
  29. package/dist/hook-runtime/hooks/session-start.js +11284 -0
  30. package/dist/hook-runtime/index.js +42775 -0
  31. package/dist/hook-runtime/statusline-main.js +765 -0
  32. package/dist/hooks/post-commit.js +7 -4
  33. package/dist/hooks/post-rewrite.js +7 -4
  34. package/dist/hooks/post-tool-use.js +64 -32
  35. package/dist/hooks/pre-commit.js +4 -3
  36. package/dist/hooks/pre-tool-use.js +118 -38
  37. package/dist/hooks/prim-hook.js +72 -27
  38. package/dist/hooks/session-end.js +5 -4
  39. package/dist/hooks/session-start.js +36 -25
  40. package/dist/index.js +15039 -2555
  41. package/dist/statusline-main.js +488 -96
  42. package/package.json +11 -5
  43. package/dist/chunk-CZ24W5QU.js +0 -123
  44. package/dist/chunk-RTLFE6EZ.js +0 -518
  45. package/dist/chunk-UTKQTZHL.js +0 -88
@@ -1,9 +1,13 @@
1
1
  import {
2
- getClient
3
- } from "./chunk-RTLFE6EZ.js";
2
+ getClient,
3
+ isTerminalSafeText,
4
+ terminalSafeLine,
5
+ terminalSafeText
6
+ } from "./chunk-Z7YPFWAZ.js";
4
7
 
5
8
  // src/decisions/feedback.ts
6
- var FEEDBACK_PROTOCOL_VERSION = 1;
9
+ var FEEDBACK_PROTOCOL_VERSION = 2;
10
+ var FEEDBACK_MIN_PROTOCOL_VERSION = 1;
7
11
  var FEEDBACK_DEADLINE_MS = 3e3;
8
12
  var MAX_FEEDBACK_EVENTS = 40;
9
13
  var MAX_FEEDBACK_MESSAGE_CODE_POINTS = 8e3;
@@ -15,38 +19,15 @@ var SHORT_ID = /^[0-9a-f]{8}$/u;
15
19
  var MAX_EVENT_ID_CHARS = 128;
16
20
  var MAX_RAW_INTENT_CODE_UNITS = 512;
17
21
  var MAX_FEEDBACK_WEB_URL_CHARS = 2048;
22
+ var SAFE_PUBLISH_DECISION_ID = /^[A-Za-z0-9_-]+$/u;
18
23
  function isRecord(value) {
19
24
  return typeof value === "object" && value !== null && !Array.isArray(value);
20
25
  }
21
- function isUnsafeControl(point) {
22
- const c0OrC1 = point <= 31 || point >= 127 && point <= 159;
23
- const bidiControl = point === 1564 || point === 8206 || point === 8207 || point >= 8234 && point <= 8238 || point >= 8294 && point <= 8297;
24
- return c0OrC1 || bidiControl;
25
- }
26
- function replaceIsolatedSurrogates(value) {
27
- let out = "";
28
- for (let index = 0; index < value.length; index += 1) {
29
- const unit = value.charCodeAt(index);
30
- if (unit >= 55296 && unit <= 56319) {
31
- const next = value.charCodeAt(index + 1);
32
- if (next >= 56320 && next <= 57343) {
33
- out += value[index] + value[index + 1];
34
- index += 1;
35
- } else {
36
- out += "\uFFFD";
37
- }
38
- } else if (unit >= 56320 && unit <= 57343) {
39
- out += "\uFFFD";
40
- } else {
41
- out += value[index];
42
- }
43
- }
44
- return out;
26
+ function isSupportedProtocolVersion(value) {
27
+ return Number.isSafeInteger(value) && Number(value) >= FEEDBACK_MIN_PROTOCOL_VERSION && Number(value) <= FEEDBACK_PROTOCOL_VERSION;
45
28
  }
46
29
  function normalizeFeedbackIntent(value) {
47
- const safeUnicode = replaceIsolatedSurrogates(value);
48
- const whitespace = safeUnicode.replace(/\s+/gu, " ");
49
- const withoutControls = Array.from(whitespace).filter((character) => !isUnsafeControl(character.codePointAt(0) ?? 0)).join("").replace(/\s+/gu, " ").trim();
30
+ const withoutControls = terminalSafeLine(value);
50
31
  const points = Array.from(withoutControls);
51
32
  if (points.length <= MAX_FEEDBACK_INTENT_CODE_POINTS) {
52
33
  return withoutControls;
@@ -54,7 +35,7 @@ function normalizeFeedbackIntent(value) {
54
35
  return `${points.slice(0, MAX_FEEDBACK_INTENT_CODE_POINTS - 1).join("")}\u2026`;
55
36
  }
56
37
  function parseFeedbackWebUrl(value) {
57
- if (typeof value !== "string" || value.length === 0 || value.length > MAX_FEEDBACK_WEB_URL_CHARS || !/^https:\/\//iu.test(value) || /\s/u.test(value) || replaceIsolatedSurrogates(value) !== value || Array.from(value).some((character) => isUnsafeControl(character.codePointAt(0) ?? 0))) {
38
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_FEEDBACK_WEB_URL_CHARS || !/^https:\/\//iu.test(value) || /\s/u.test(value) || !isTerminalSafeText(value)) {
58
39
  return void 0;
59
40
  }
60
41
  try {
@@ -67,9 +48,15 @@ function parseFeedbackWebUrl(value) {
67
48
  }
68
49
  return value;
69
50
  }
70
- function parseEvent(value) {
51
+ function parseFeedbackDecisionId(value) {
52
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_ID_CHARS || terminalSafeText(value) !== value || !SAFE_PUBLISH_DECISION_ID.test(value)) {
53
+ return void 0;
54
+ }
55
+ return value;
56
+ }
57
+ function parseEvent(value, protocolVersion) {
71
58
  if (!isRecord(value)) return void 0;
72
- if (typeof value.eventId !== "string" || value.eventId.length === 0 || value.eventId.length > MAX_EVENT_ID_CHARS || Array.from(value.eventId).some((character) => isUnsafeControl(character.codePointAt(0) ?? 0))) {
59
+ if (typeof value.eventId !== "string" || value.eventId.length === 0 || value.eventId.length > MAX_EVENT_ID_CHARS || terminalSafeText(value.eventId) !== value.eventId) {
73
60
  return void 0;
74
61
  }
75
62
  if (!Number.isSafeInteger(value.leaseVersion) || Number(value.leaseVersion) < 1) {
@@ -85,40 +72,58 @@ function parseEvent(value) {
85
72
  if (!intent) return void 0;
86
73
  const webUrl = value.webUrl === void 0 ? void 0 : parseFeedbackWebUrl(value.webUrl);
87
74
  if (value.webUrl !== void 0 && webUrl === void 0) return void 0;
75
+ const kind = value.kind === void 0 && protocolVersion === 1 ? "confirm_prompt" : value.kind;
76
+ if (kind !== "confirm_prompt" && kind !== "publish_prompt") return void 0;
77
+ if (protocolVersion === 1 && kind === "publish_prompt") return void 0;
78
+ const decisionId = value.decisionId === void 0 ? void 0 : parseFeedbackDecisionId(value.decisionId);
79
+ if (value.decisionId !== void 0 && decisionId === void 0) return void 0;
80
+ if (decisionId !== void 0 && (protocolVersion !== 2 || kind !== "publish_prompt")) {
81
+ return void 0;
82
+ }
83
+ if (protocolVersion === 2 && kind === "publish_prompt" && decisionId === void 0) {
84
+ return void 0;
85
+ }
88
86
  return {
89
87
  eventId: value.eventId,
90
88
  leaseVersion: Number(value.leaseVersion),
91
89
  shortId: value.shortId,
92
90
  intent,
93
- ...webUrl === void 0 ? {} : { webUrl }
91
+ ...webUrl === void 0 ? {} : { webUrl },
92
+ ...decisionId === void 0 ? {} : { decisionId },
93
+ kind
94
94
  };
95
95
  }
96
96
  function parseFeedbackLease(value) {
97
- if (!isRecord(value) || value.protocolVersion !== FEEDBACK_PROTOCOL_VERSION) return void 0;
97
+ if (!isRecord(value) || !isSupportedProtocolVersion(value.protocolVersion)) return void 0;
98
+ const protocolVersion = value.protocolVersion;
98
99
  if (value.status === "empty") {
99
- return value.hasMore === false ? { events: [], hasMore: false } : void 0;
100
+ return value.hasMore === false ? { protocolVersion, events: [], hasMore: false } : void 0;
100
101
  }
101
102
  if (value.status === "unavailable") {
102
- return value.reason === "organization_unbound" ? { events: [], hasMore: false } : void 0;
103
+ return value.reason === "organization_unbound" ? { protocolVersion, events: [], hasMore: false } : void 0;
103
104
  }
104
105
  if (value.status !== "leased" || typeof value.hasMore !== "boolean") return void 0;
105
106
  if (!Array.isArray(value.events) || value.events.length === 0 || value.events.length > MAX_FEEDBACK_EVENTS) {
106
107
  return void 0;
107
108
  }
108
- const events = value.events.map(parseEvent);
109
+ const events = value.events.map((event) => parseEvent(event, protocolVersion));
109
110
  if (events.some((event) => event === void 0)) return void 0;
110
111
  const parsedEvents = events;
111
112
  if (new Set(parsedEvents.map((event) => event.eventId)).size !== parsedEvents.length) {
112
113
  return void 0;
113
114
  }
114
- return { events: parsedEvents, hasMore: value.hasMore };
115
+ return { protocolVersion, events: parsedEvents, hasMore: value.hasMore };
115
116
  }
116
117
  function renderFeedback(lease) {
117
118
  const lines = [];
118
119
  const deliveries = [];
119
120
  let pointCount = 0;
120
121
  for (const event of lease.events) {
121
- const line = `[prim] response \u2192 created Decision (dec_${event.shortId}): ${event.intent}${event.webUrl ? ` (${event.webUrl})` : ""}`;
122
+ const decisionId = event.kind === "publish_prompt" ? parseFeedbackDecisionId(event.decisionId) : void 0;
123
+ if (event.kind === "publish_prompt" && decisionId === void 0) continue;
124
+ const identifier = `dec_${event.shortId}`;
125
+ const detail = `${event.intent}${event.webUrl ? ` (${event.webUrl})` : ""}`;
126
+ const line = event.kind === "publish_prompt" ? `[prim] publish this Decision draft (${identifier})? ${detail} Run \`prim decisions publish ${decisionId}\` to share it with your team.` : `[prim] response \u2192 created Decision (${identifier}): ${detail}`;
122
127
  const extra = Array.from(line).length + (lines.length === 0 ? 0 : 1);
123
128
  if (pointCount + extra > MAX_FEEDBACK_MESSAGE_CODE_POINTS) break;
124
129
  lines.push(line);
@@ -126,7 +131,7 @@ function renderFeedback(lease) {
126
131
  pointCount += extra;
127
132
  }
128
133
  if (lines.length === 0) return void 0;
129
- return { systemMessage: lines.join("\n"), deliveries };
134
+ return { protocolVersion: lease.protocolVersion, systemMessage: lines.join("\n"), deliveries };
130
135
  }
131
136
  async function leaseDecisionFeedback(input, dependencies = {}) {
132
137
  try {
@@ -152,12 +157,14 @@ async function leaseDecisionFeedback(input, dependencies = {}) {
152
157
  }
153
158
  }
154
159
  async function acknowledgeDecisionFeedback(input, dependencies = {}) {
155
- if (input.deliveries.length === 0 || input.deliveries.length > MAX_FEEDBACK_EVENTS) return false;
160
+ if (!isSupportedProtocolVersion(input.protocolVersion) || input.deliveries.length === 0 || input.deliveries.length > MAX_FEEDBACK_EVENTS) {
161
+ return false;
162
+ }
156
163
  try {
157
164
  const response = await (dependencies.client ?? getClient()).post(
158
165
  ACK_PATH,
159
166
  {
160
- protocolVersion: FEEDBACK_PROTOCOL_VERSION,
167
+ protocolVersion: input.protocolVersion,
161
168
  workspaceId: input.workspaceId,
162
169
  deliveries: input.deliveries
163
170
  },
@@ -165,7 +172,7 @@ async function acknowledgeDecisionFeedback(input, dependencies = {}) {
165
172
  );
166
173
  const acknowledgedEventIds = isRecord(response) ? response.acknowledgedEventIds : void 0;
167
174
  const expectedEventIds = new Set(input.deliveries.map((delivery) => delivery.eventId));
168
- const valid = isRecord(response) && response.protocolVersion === FEEDBACK_PROTOCOL_VERSION && response.status === "acked" && Array.isArray(acknowledgedEventIds) && acknowledgedEventIds.length <= input.deliveries.length && acknowledgedEventIds.every(
175
+ const valid = isRecord(response) && response.protocolVersion === input.protocolVersion && response.status === "acked" && Array.isArray(acknowledgedEventIds) && acknowledgedEventIds.length <= input.deliveries.length && acknowledgedEventIds.every(
169
176
  (id) => typeof id === "string" && id.length > 0 && id.length <= MAX_EVENT_ID_CHARS && expectedEventIds.has(id)
170
177
  ) && new Set(acknowledgedEventIds).size === acknowledgedEventIds.length;
171
178
  if (!valid) {
@@ -180,7 +187,7 @@ async function acknowledgeDecisionFeedback(input, dependencies = {}) {
180
187
  }
181
188
  }
182
189
  function parseFeedbackCapability(value) {
183
- if (!isRecord(value) || value.protocolVersion !== FEEDBACK_PROTOCOL_VERSION) return void 0;
190
+ if (!isRecord(value) || !isSupportedProtocolVersion(value.protocolVersion)) return void 0;
184
191
  if (value.status === "available") return { status: "available" };
185
192
  if (value.status === "unavailable" && value.reason === "organization_unbound") {
186
193
  return { status: "unavailable", reason: "organization_unbound" };
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  getClient,
3
- getSiteUrl
4
- } from "./chunk-RTLFE6EZ.js";
3
+ getSiteUrl,
4
+ terminalSafeLine
5
+ } from "./chunk-Z7YPFWAZ.js";
5
6
  import {
6
7
  daemonRequest
7
- } from "./chunk-UTKQTZHL.js";
8
+ } from "./chunk-CUE2LKLR.js";
8
9
 
9
10
  // src/daemon/proxy.ts
10
11
  var DAEMON_HTTP_TIMEOUT_MS = 1e4;
@@ -93,7 +94,7 @@ var FILES_PREVIEW_LIMIT = 3;
93
94
  function formatDecisionsWarning(result) {
94
95
  const lines = [];
95
96
  if (result.unavailable !== void 0) {
96
- lines.push(`[prim] decision check not verified \u2014 ${result.unavailable}`);
97
+ lines.push(`[prim] decision check not verified \u2014 ${terminalSafeLine(result.unavailable)}`);
97
98
  }
98
99
  if (result.decisions.length > 0) {
99
100
  lines.push(
@@ -101,12 +102,12 @@ function formatDecisionsWarning(result) {
101
102
  );
102
103
  for (const d of result.decisions) {
103
104
  const statusMark = d.status === "under_review" ? " (under review)" : "";
104
- const preview = d.matchedFiles.slice(0, FILES_PREVIEW_LIMIT).join(", ");
105
+ const preview = d.matchedFiles.slice(0, FILES_PREVIEW_LIMIT).map(terminalSafeLine).join(", ");
105
106
  const overflow = d.matchedFiles.length > FILES_PREVIEW_LIMIT ? ` (+${String(d.matchedFiles.length - FILES_PREVIEW_LIMIT)} more)` : "";
106
- lines.push(` \xB7 ${d.intent}${statusMark}`);
107
+ lines.push(` \xB7 ${terminalSafeLine(d.intent)}${statusMark}`);
107
108
  lines.push(` files: ${preview}${overflow}`);
108
109
  if (d.rationale) {
109
- lines.push(` rationale: ${d.rationale}`);
110
+ lines.push(` rationale: ${terminalSafeLine(d.rationale)}`);
110
111
  }
111
112
  }
112
113
  }
@@ -4,6 +4,8 @@ import { createHash } from "crypto";
4
4
  import { lstatSync, realpathSync } from "fs";
5
5
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
6
6
  var GIT_TIMEOUT_MS = 1e3;
7
+ var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
8
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
7
9
  function gitToplevel(cwd) {
8
10
  try {
9
11
  return execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -38,7 +40,9 @@ function githubRepositoryFullName(cwd) {
38
40
  const path = match?.[1] ?? match?.[2];
39
41
  if (!path) return null;
40
42
  const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
41
- if (parts.length !== 2 || parts.some((part) => !part || part !== part.trim())) return null;
43
+ if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
44
+ return null;
45
+ }
42
46
  return `${parts[0]}/${parts[1]}`;
43
47
  }
44
48
  function canonicalGitRoot(cwd) {
@@ -186,6 +190,8 @@ import { homedir } from "os";
186
190
  import { join } from "path";
187
191
  var PRIM_ACTIVE_KEY = "prim.active";
188
192
  var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
193
+ var PRIM_REPO_SYNC_REPOSITORY_KEY = "prim.repoSyncRepository";
194
+ var PRIM_REPO_BINDING_STATE_KEY = "prim.repoBindingState";
189
195
  var GIT_TIMEOUT_MS2 = 1e3;
190
196
  var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
191
197
  var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
@@ -218,20 +224,26 @@ function setRepoActive(cwd, active) {
218
224
  function isValidRepoSyncId(value) {
219
225
  return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
220
226
  }
221
- function repoSyncId(cwd) {
227
+ function localGitConfigValue(cwd, key) {
222
228
  try {
223
- const value = execFileSync2("git", ["config", "--local", "--get", PRIM_REPO_SYNC_ID_KEY], {
229
+ const value = execFileSync2("git", ["config", "--local", "--get", key], {
224
230
  cwd,
225
231
  encoding: "utf-8",
226
232
  stdio: ["ignore", "pipe", "ignore"],
227
233
  timeout: GIT_TIMEOUT_MS2
228
234
  }).trim();
229
- return isValidRepoSyncId(value) ? value : void 0;
235
+ return value || void 0;
230
236
  } catch {
231
237
  return void 0;
232
238
  }
233
239
  }
234
- function setRepoSyncId(cwd, value) {
240
+ function repoSyncId(cwd) {
241
+ const value = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
242
+ if (!isValidRepoSyncId(value)) return void 0;
243
+ const repositoryFullName = localGitConfigValue(cwd, PRIM_REPO_SYNC_REPOSITORY_KEY);
244
+ return repositoryFullName === githubRepositoryFullName(cwd) ? value : void 0;
245
+ }
246
+ function setRepoSyncId(cwd, value, repositoryFullName) {
235
247
  if (!isValidRepoSyncId(value)) {
236
248
  throw new Error("invalid repository binding returned by server");
237
249
  }
@@ -240,20 +252,30 @@ function setRepoSyncId(cwd, value) {
240
252
  stdio: ["ignore", "ignore", "pipe"],
241
253
  timeout: GIT_TIMEOUT_MS2
242
254
  });
255
+ execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_REPOSITORY_KEY, repositoryFullName], {
256
+ cwd,
257
+ stdio: ["ignore", "ignore", "pipe"],
258
+ timeout: GIT_TIMEOUT_MS2
259
+ });
243
260
  }
244
- function clearRepoSyncId(cwd) {
245
- try {
246
- execFileSync2("git", ["config", "--local", "--unset-all", PRIM_REPO_SYNC_ID_KEY], {
247
- cwd,
248
- stdio: ["ignore", "ignore", "pipe"],
249
- timeout: GIT_TIMEOUT_MS2
250
- });
251
- } catch (error) {
252
- if (typeof error === "object" && error !== null && "status" in error && error.status === 5) {
253
- return;
254
- }
255
- throw error;
261
+ function setRepositoryBindingState(cwd, state) {
262
+ if (state !== "connected" && state !== "unbound" && state !== "invalid") {
263
+ throw new Error("invalid repository binding state");
256
264
  }
265
+ execFileSync2("git", ["config", "--local", PRIM_REPO_BINDING_STATE_KEY, state], {
266
+ cwd,
267
+ stdio: ["ignore", "ignore", "pipe"],
268
+ timeout: GIT_TIMEOUT_MS2
269
+ });
270
+ }
271
+ function repositoryBindingState(cwd) {
272
+ const rawId = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
273
+ const marker = localGitConfigValue(cwd, PRIM_REPO_BINDING_STATE_KEY);
274
+ if (rawId !== void 0 && !isValidRepoSyncId(rawId)) return "invalid";
275
+ if (marker === "unbound" || marker === "invalid") return marker;
276
+ if (marker === "connected") return repoSyncId(cwd) ? "connected" : "invalid";
277
+ if (marker !== void 0) return "invalid";
278
+ return rawId === void 0 ? "unbound" : repoSyncId(cwd) ? void 0 : "unbound";
257
279
  }
258
280
  function hasProjectPrimInstall(cwd) {
259
281
  const root = gitToplevel(cwd);
@@ -292,7 +314,8 @@ export {
292
314
  isValidRepoSyncId,
293
315
  repoSyncId,
294
316
  setRepoSyncId,
295
- clearRepoSyncId,
317
+ setRepositoryBindingState,
318
+ repositoryBindingState,
296
319
  isRepoActiveForCapture,
297
320
  decisionIngestionStatus
298
321
  };
@@ -9,6 +9,7 @@ function parseAgent(argv) {
9
9
  }
10
10
 
11
11
  // src/hooks/normalize.ts
12
+ import { createHash } from "crypto";
12
13
  var HERMES_EVENT_MAP = {
13
14
  on_session_start: "SessionStart",
14
15
  on_session_end: "SessionEnd",
@@ -19,6 +20,22 @@ var HERMES_EVENT_MAP = {
19
20
  subagent_stop: "SubagentStop"
20
21
  };
21
22
  function normalizeEnvelope(parsed, agent) {
23
+ if (agent === "codex") {
24
+ const event = parsed.hook_event_name;
25
+ if ((event === "PreToolUse" || event === "PostToolUse") && (typeof parsed.tool_use_id !== "string" || parsed.tool_use_id.length === 0)) {
26
+ const stableToolIdentity = JSON.stringify([
27
+ parsed.session_id ?? null,
28
+ parsed.turn_id ?? null,
29
+ parsed.tool_name ?? null,
30
+ parsed.tool_input ?? null
31
+ ]);
32
+ return {
33
+ ...parsed,
34
+ tool_use_id: `codex:fallback:v1:${createHash("sha256").update(stableToolIdentity).digest("hex")}`
35
+ };
36
+ }
37
+ return parsed;
38
+ }
22
39
  if (agent !== "hermes") {
23
40
  return parsed;
24
41
  }
@@ -0,0 +1,26 @@
1
+ // src/ingest-response.ts
2
+ var IngestAcknowledgementError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "IngestAcknowledgementError";
6
+ }
7
+ };
8
+ function requireDurableIngestAcknowledgement(value, expected) {
9
+ if (typeof value !== "object" || value === null) {
10
+ throw new IngestAcknowledgementError("ingest response was not an object");
11
+ }
12
+ const response = value;
13
+ if (response.disposition !== "persisted") {
14
+ throw new IngestAcknowledgementError("ingest response did not confirm persistence");
15
+ }
16
+ if (response.acknowledged !== expected) {
17
+ throw new IngestAcknowledgementError(
18
+ `ingest acknowledged ${String(response.acknowledged)} of ${String(expected)} moves`
19
+ );
20
+ }
21
+ return value;
22
+ }
23
+
24
+ export {
25
+ requireDurableIngestAcknowledgement
26
+ };
@@ -1,6 +1,11 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-RTLFE6EZ.js";
3
+ } from "./chunk-Z7YPFWAZ.js";
4
+ import {
5
+ jwtOrganizationId,
6
+ primConfigDirectory,
7
+ resolveAuthCredential
8
+ } from "./chunk-3EEFL6NJ.js";
4
9
 
5
10
  // src/journal.ts
6
11
  import {
@@ -15,9 +20,8 @@ import {
15
20
  readSync,
16
21
  readdirSync
17
22
  } from "fs";
18
- import { homedir } from "os";
19
23
  import { dirname, join } from "path";
20
- var JOURNAL_DIR = join(homedir(), ".config", "prim", "moves");
24
+ var JOURNAL_DIR = join(primConfigDirectory(), "moves");
21
25
  var UNBOUND_BUCKET = "_unbound";
22
26
  var JOURNAL_BASENAME = "journal.ndjson";
23
27
  var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
@@ -139,7 +143,9 @@ function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
139
143
  function parseFlushingPid(name) {
140
144
  const segments = name.slice(FLUSHING_PREFIX.length).split(".");
141
145
  const last = segments.length >= 2 ? segments[segments.length - 1] : void 0;
142
- return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
146
+ if (last === void 0 || !/^[0-9]+$/.test(last)) return void 0;
147
+ const pid = Number(last);
148
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
143
149
  }
144
150
  function listFlushingInDir(dir, bucket, options = {}) {
145
151
  if (!existsSync(dir)) {
@@ -253,18 +259,10 @@ function pendingJournalStats() {
253
259
 
254
260
  // src/binding.ts
255
261
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
256
- import { homedir as homedir2 } from "os";
257
262
  import { dirname as dirname2, join as join2 } from "path";
258
- var PRIM_CONFIG_DIR = join2(homedir2(), ".config", "prim");
259
- var TOKEN_PATH = join2(PRIM_CONFIG_DIR, "token");
263
+ var PRIM_CONFIG_DIR = primConfigDirectory();
260
264
  var SESSIONS_DIR = join2(PRIM_CONFIG_DIR, "sessions");
261
265
  var WORKSPACE_FILE = ".prim/workspace.json";
262
- var JWT_PARTS = 3;
263
- var BASE64_PAD_4 = 4;
264
- var ENV_TOKEN_LINE = /^\s*PRIM_TOKEN\s*=\s*(.*)$/m;
265
- var SURROUNDING_QUOTES = /^["']|["']$/g;
266
- var BASE64URL_MINUS = /-/g;
267
- var BASE64URL_UNDERSCORE = /_/g;
268
266
  function readJsonSafe(path) {
269
267
  if (!existsSync2(path)) {
270
268
  return;
@@ -296,46 +294,9 @@ function fromWorkspaceFile(startDir) {
296
294
  dir = parent;
297
295
  }
298
296
  }
299
- function readToken(cwd) {
300
- const fromEnv = process.env.PRIM_TOKEN;
301
- if (fromEnv) {
302
- return fromEnv.trim();
303
- }
304
- if (existsSync2(TOKEN_PATH)) {
305
- return readFileSync2(TOKEN_PATH, "utf-8").trim();
306
- }
307
- const envLocal = join2(cwd, ".env.local");
308
- if (existsSync2(envLocal)) {
309
- const match = readFileSync2(envLocal, "utf-8").match(ENV_TOKEN_LINE);
310
- if (match) {
311
- return match[1].trim().replace(SURROUNDING_QUOTES, "");
312
- }
313
- }
314
- return;
315
- }
316
- function base64UrlDecode(s) {
317
- const padded = s.padEnd(
318
- s.length + (BASE64_PAD_4 - s.length % BASE64_PAD_4) % BASE64_PAD_4,
319
- "="
320
- );
321
- const normalized = padded.replace(BASE64URL_MINUS, "+").replace(BASE64URL_UNDERSCORE, "/");
322
- return Buffer.from(normalized, "base64").toString("utf-8");
323
- }
324
- function fromDefaultOrg(cwd) {
325
- const token = readToken(cwd);
326
- if (!token) {
327
- return;
328
- }
329
- const parts = token.split(".");
330
- if (parts.length !== JWT_PARTS) {
331
- return;
332
- }
333
- try {
334
- const claims = JSON.parse(base64UrlDecode(parts[1]));
335
- return claims.org_id;
336
- } catch {
337
- return;
338
- }
297
+ function fromDefaultOrg(_cwd) {
298
+ const credential = resolveAuthCredential();
299
+ return credential ? jwtOrganizationId(credential.token) : void 0;
339
300
  }
340
301
  function resolveOrg(args) {
341
302
  const fromSession = fromSessionMarker(args.sessionId);