@primitive.ai/prim 0.1.0-alpha.92 → 0.1.0-alpha.94

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 (42) hide show
  1. package/README.md +84 -10
  2. package/SKILL.md +5 -2
  3. package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
  4. package/dist/{chunk-26P57LAR.js → chunk-AXQQMX2U.js} +203 -9
  5. package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
  6. package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
  7. package/dist/chunk-FXF4JWK2.js +162 -0
  8. package/dist/{chunk-PCVGBE5B.js → chunk-G2VMA2QO.js} +2 -1
  9. package/dist/{chunk-NAHHFSLH.js → chunk-ICM2XCXS.js} +2 -2
  10. package/dist/{chunk-5HRR4AOR.js → chunk-IMK4JKTA.js} +1 -1
  11. package/dist/{chunk-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
  12. package/dist/{chunk-ADUTW66W.js → chunk-O7YAW7G6.js} +6 -6
  13. package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
  14. package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
  15. package/dist/chunk-OYHYCK4K.js +13 -0
  16. package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
  17. package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
  18. package/dist/{chunk-LGGMJYY4.js → chunk-WUXFLE7X.js} +720 -369
  19. package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
  20. package/dist/daemon/server.js +372 -123
  21. package/dist/hook-runtime/daemon/server.js +372 -123
  22. package/dist/hook-runtime/hooks/post-commit.js +192 -58
  23. package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
  24. package/dist/hook-runtime/hooks/post-tool-use.js +150 -67
  25. package/dist/hook-runtime/hooks/pre-tool-use.js +87 -15
  26. package/dist/hook-runtime/hooks/prim-hook.js +197 -107
  27. package/dist/hook-runtime/hooks/session-end.js +53 -5
  28. package/dist/hook-runtime/hooks/session-start.js +880 -280
  29. package/dist/hook-runtime/index.js +4008 -2438
  30. package/dist/hook-runtime/statusline-main.js +23 -1
  31. package/dist/hooks/post-commit.js +15 -8
  32. package/dist/hooks/post-rewrite.js +11 -8
  33. package/dist/hooks/post-tool-use.js +13 -15
  34. package/dist/hooks/pre-commit.js +4 -4
  35. package/dist/hooks/pre-tool-use.js +10 -12
  36. package/dist/hooks/prim-hook.js +17 -14
  37. package/dist/hooks/session-end.js +4 -4
  38. package/dist/hooks/session-start.js +35 -45
  39. package/dist/index.js +1401 -620
  40. package/dist/statusline-main.js +23 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-BTNNEH2W.js +0 -79
@@ -1,27 +1,120 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-PCVGBE5B.js";
3
+ } from "./chunk-G2VMA2QO.js";
4
4
  import {
5
+ atomicWriteFile,
6
+ ensureDurableDirectory,
5
7
  jwtOrganizationId,
6
8
  primConfigDirectory,
7
9
  resolveAuthCredential
8
- } from "./chunk-67VNLLBN.js";
10
+ } from "./chunk-2GP5UFV7.js";
9
11
 
10
12
  // src/journal.ts
11
13
  import {
12
14
  appendFileSync,
13
15
  closeSync,
14
- existsSync,
16
+ existsSync as existsSync2,
15
17
  fstatSync,
16
18
  mkdirSync,
17
19
  openSync,
18
20
  opendirSync,
19
- readFileSync,
21
+ readFileSync as readFileSync2,
20
22
  readSync,
21
- readdirSync
23
+ readdirSync as readdirSync2
22
24
  } from "fs";
23
- import { dirname, join } from "path";
24
- var JOURNAL_DIR = join(primConfigDirectory(), "moves");
25
+ import { dirname as dirname2, join as join2 } from "path";
26
+
27
+ // src/drain-progress.ts
28
+ import {
29
+ chmodSync,
30
+ existsSync,
31
+ readFileSync,
32
+ readdirSync,
33
+ rmSync,
34
+ rmdirSync
35
+ } from "fs";
36
+ import { basename, dirname, join } from "path";
37
+ var DIRECTORY_MODE = 448;
38
+ var FILE_MODE = 384;
39
+ var DRAIN_PROGRESS_DIRNAME = "drain-progress";
40
+ var CHECKPOINT_NAME = /^(.+)\.json(?:\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp)?$/;
41
+ function rotationIdentity(stat) {
42
+ return { size: stat.size, ino: stat.ino, dev: stat.dev };
43
+ }
44
+ function drainProgressDirectory(flushingPath) {
45
+ return join(dirname(flushingPath), DRAIN_PROGRESS_DIRNAME);
46
+ }
47
+ function drainProgressPath(flushingPath) {
48
+ return join(drainProgressDirectory(flushingPath), `${basename(flushingPath)}.json`);
49
+ }
50
+ function isByteCount(value) {
51
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
52
+ }
53
+ function drainResumeOffset(flushingPath, rotation) {
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(readFileSync(drainProgressPath(flushingPath), "utf8"));
57
+ } catch {
58
+ return 0;
59
+ }
60
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
61
+ return 0;
62
+ }
63
+ const { v, offset, size, ino, dev } = parsed;
64
+ if (v !== 2 || ino !== rotation.ino || dev !== rotation.dev || size !== rotation.size || !isByteCount(offset) || offset > rotation.size) {
65
+ return 0;
66
+ }
67
+ return offset;
68
+ }
69
+ function writeDrainCheckpoint(flushingPath, checkpoint) {
70
+ const directory = drainProgressDirectory(flushingPath);
71
+ ensureDurableDirectory(directory, DIRECTORY_MODE);
72
+ chmodSync(directory, DIRECTORY_MODE);
73
+ atomicWriteFile(drainProgressPath(flushingPath), `${JSON.stringify(checkpoint)}
74
+ `, {
75
+ mode: FILE_MODE
76
+ });
77
+ }
78
+ function removeEmptyDirectory(directory) {
79
+ try {
80
+ rmdirSync(directory);
81
+ } catch (error) {
82
+ const code = error.code;
83
+ if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST") {
84
+ throw error;
85
+ }
86
+ }
87
+ }
88
+ function removeDrainCheckpoint(flushingPath) {
89
+ rmSync(drainProgressPath(flushingPath), { force: true });
90
+ removeEmptyDirectory(drainProgressDirectory(flushingPath));
91
+ }
92
+ function sweepOrphanedDrainCheckpoints(bucketDirectory) {
93
+ const directory = join(bucketDirectory, DRAIN_PROGRESS_DIRNAME);
94
+ let names;
95
+ try {
96
+ names = readdirSync(directory);
97
+ } catch {
98
+ return;
99
+ }
100
+ for (const name of names) {
101
+ const rotation = CHECKPOINT_NAME.exec(name)?.[1];
102
+ if (rotation === void 0 || existsSync(join(bucketDirectory, rotation))) {
103
+ continue;
104
+ }
105
+ try {
106
+ rmSync(join(directory, name), { force: true });
107
+ } catch {
108
+ }
109
+ }
110
+ try {
111
+ removeEmptyDirectory(directory);
112
+ } catch {
113
+ }
114
+ }
115
+
116
+ // src/journal.ts
117
+ var JOURNAL_DIR = join2(primConfigDirectory(), "moves");
25
118
  var UNBOUND_BUCKET = "_unbound";
26
119
  var JOURNAL_BASENAME = "journal.ndjson";
27
120
  var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
@@ -34,26 +127,26 @@ function envSlug(apiUrl) {
34
127
  return slug;
35
128
  }
36
129
  function currentEnvDir() {
37
- return join(JOURNAL_DIR, envSlug(getSiteUrl()));
130
+ return join2(JOURNAL_DIR, envSlug(getSiteUrl()));
38
131
  }
39
132
  var DIR_MODE = 448;
40
- var FILE_MODE = 384;
133
+ var FILE_MODE2 = 384;
41
134
  var SAFE_BUCKET = /^[A-Za-z0-9_-]+$/;
42
135
  var RESERVED_BUCKETS = /* @__PURE__ */ new Set([UNBOUND_BUCKET, "_legacy"]);
43
136
  function appendMoveToPath(path, move) {
44
- const dir = dirname(path);
45
- if (!existsSync(dir)) {
137
+ const dir = dirname2(path);
138
+ if (!existsSync2(dir)) {
46
139
  mkdirSync(dir, { recursive: true, mode: DIR_MODE });
47
140
  }
48
141
  appendFileSync(path, `${JSON.stringify(move)}
49
- `, { mode: FILE_MODE });
142
+ `, { mode: FILE_MODE2 });
50
143
  }
51
144
  function bucketDir(orgId) {
52
145
  const safe = orgId !== void 0 && SAFE_BUCKET.test(orgId) && !RESERVED_BUCKETS.has(orgId);
53
- return join(currentEnvDir(), safe ? orgId : UNBOUND_BUCKET);
146
+ return join2(currentEnvDir(), safe ? orgId : UNBOUND_BUCKET);
54
147
  }
55
148
  function journalPath(orgId) {
56
- return join(bucketDir(orgId), JOURNAL_BASENAME);
149
+ return join2(bucketDir(orgId), JOURNAL_BASENAME);
57
150
  }
58
151
  function appendMove(move, orgId) {
59
152
  appendMoveToPath(journalPath(orgId), move);
@@ -72,23 +165,23 @@ function parseMoves(content) {
72
165
  return moves;
73
166
  }
74
167
  function readMovesFromPath(path) {
75
- if (!existsSync(path)) {
168
+ if (!existsSync2(path)) {
76
169
  return [];
77
170
  }
78
- return parseMoves(readFileSync(path, "utf-8"));
171
+ return parseMoves(readFileSync2(path, "utf-8"));
79
172
  }
80
173
  function listBuckets() {
81
174
  const out = [];
82
175
  const envDir = currentEnvDir();
83
- if (!existsSync(envDir)) {
176
+ if (!existsSync2(envDir)) {
84
177
  return out;
85
178
  }
86
- for (const entry of readdirSync(envDir, { withFileTypes: true })) {
179
+ for (const entry of readdirSync2(envDir, { withFileTypes: true })) {
87
180
  if (!entry.isDirectory()) {
88
181
  continue;
89
182
  }
90
- const path = join(envDir, entry.name, JOURNAL_BASENAME);
91
- if (existsSync(path)) {
183
+ const path = join2(envDir, entry.name, JOURNAL_BASENAME);
184
+ if (existsSync2(path)) {
92
185
  out.push({ bucket: entry.name, path });
93
186
  }
94
187
  }
@@ -105,22 +198,23 @@ function oldestCapturedAt(moves) {
105
198
  }
106
199
  return oldest;
107
200
  }
108
- function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
201
+ function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES, options = {}) {
109
202
  const fd = openSync(path, "r");
110
203
  try {
111
204
  const initial = fstatSync(fd);
112
- const target = Math.min(initial.size, Math.max(0, maxBytes));
205
+ const start = options.fromDrainCheckpoint ? drainResumeOffset(path, rotationIdentity(initial)) : 0;
206
+ const target = Math.min(initial.size - start, Math.max(0, maxBytes));
113
207
  const buffer = Buffer.allocUnsafe(target);
114
208
  let sampledBytes = 0;
115
209
  while (sampledBytes < target) {
116
- const count = readSync(fd, buffer, sampledBytes, target - sampledBytes, sampledBytes);
210
+ const count = readSync(fd, buffer, sampledBytes, target - sampledBytes, start + sampledBytes);
117
211
  if (count === 0) {
118
212
  break;
119
213
  }
120
214
  sampledBytes += count;
121
215
  }
122
216
  const stat = fstatSync(fd);
123
- const sampled = stat.size > sampledBytes;
217
+ const sampled = stat.size - start > sampledBytes;
124
218
  let content = buffer.subarray(0, sampledBytes).toString("utf-8");
125
219
  if (sampled) {
126
220
  const lastNewline = content.lastIndexOf("\n");
@@ -130,6 +224,7 @@ function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
130
224
  const moves = parseMoves(content);
131
225
  return {
132
226
  sizeBytes: stat.size,
227
+ pendingBytes: Math.max(0, stat.size - start),
133
228
  mtimeMs: stat.mtimeMs,
134
229
  lineCount: sampled && stat.size > 0 ? Math.max(1, lines.length) : lines.length,
135
230
  oldestCapturedAt: oldestCapturedAt(moves),
@@ -148,7 +243,7 @@ function parseFlushingPid(name) {
148
243
  return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
149
244
  }
150
245
  function listFlushingInDir(dir, bucket, options = {}) {
151
- if (!existsSync(dir)) {
246
+ if (!existsSync2(dir)) {
152
247
  return [];
153
248
  }
154
249
  const out = [];
@@ -162,10 +257,10 @@ function listFlushingInDir(dir, bucket, options = {}) {
162
257
  entry = entries.readSync();
163
258
  continue;
164
259
  }
165
- const path = join(dir, entry.name);
260
+ const path = join2(dir, entry.name);
166
261
  let sample;
167
262
  try {
168
- sample = sampleJournalFile(path, remainingBytes);
263
+ sample = sampleJournalFile(path, remainingBytes, { fromDrainCheckpoint: true });
169
264
  } catch (err) {
170
265
  if (err.code === "ENOENT") {
171
266
  entry = entries.readSync();
@@ -189,18 +284,18 @@ function listFlushingInDir(dir, bucket, options = {}) {
189
284
  }
190
285
  function listFlushing(options = {}) {
191
286
  const envDir = currentEnvDir();
192
- if (!existsSync(envDir)) {
287
+ if (!existsSync2(envDir)) {
193
288
  return [];
194
289
  }
195
290
  const out = [];
196
291
  let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
197
292
  const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
198
- for (const entry of readdirSync(envDir, { withFileTypes: true })) {
293
+ for (const entry of readdirSync2(envDir, { withFileTypes: true })) {
199
294
  if (out.length >= maxFiles) {
200
295
  break;
201
296
  }
202
297
  if (entry.isDirectory()) {
203
- const found = listFlushingInDir(join(envDir, entry.name), entry.name, {
298
+ const found = listFlushingInDir(join2(envDir, entry.name), entry.name, {
204
299
  sampleBytes: remainingBytes,
205
300
  maxFiles: maxFiles - out.length
206
301
  });
@@ -213,6 +308,17 @@ function listFlushing(options = {}) {
213
308
  }
214
309
  return out;
215
310
  }
311
+ function sweepOrphanedDrainProgress() {
312
+ const envDir = currentEnvDir();
313
+ if (!existsSync2(envDir)) {
314
+ return;
315
+ }
316
+ for (const entry of readdirSync2(envDir, { withFileTypes: true })) {
317
+ if (entry.isDirectory()) {
318
+ sweepOrphanedDrainCheckpoints(join2(envDir, entry.name));
319
+ }
320
+ }
321
+ }
216
322
  function bucketStats(options = {}) {
217
323
  const out = [];
218
324
  let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
@@ -258,17 +364,17 @@ function pendingJournalStats() {
258
364
  }
259
365
 
260
366
  // src/binding.ts
261
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
262
- import { dirname as dirname2, join as join2 } from "path";
367
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
368
+ import { dirname as dirname3, join as join3 } from "path";
263
369
  var PRIM_CONFIG_DIR = primConfigDirectory();
264
- var SESSIONS_DIR = join2(PRIM_CONFIG_DIR, "sessions");
370
+ var SESSIONS_DIR = join3(PRIM_CONFIG_DIR, "sessions");
265
371
  var WORKSPACE_FILE = ".prim/workspace.json";
266
372
  function readJsonSafe(path) {
267
- if (!existsSync2(path)) {
373
+ if (!existsSync3(path)) {
268
374
  return;
269
375
  }
270
376
  try {
271
- return JSON.parse(readFileSync2(path, "utf-8"));
377
+ return JSON.parse(readFileSync3(path, "utf-8"));
272
378
  } catch {
273
379
  return;
274
380
  }
@@ -277,17 +383,17 @@ function fromSessionMarker(sessionId) {
277
383
  if (!sessionId) {
278
384
  return;
279
385
  }
280
- const marker = readJsonSafe(join2(SESSIONS_DIR, `${sessionId}.json`));
386
+ const marker = readJsonSafe(join3(SESSIONS_DIR, `${sessionId}.json`));
281
387
  return marker?.orgId;
282
388
  }
283
389
  function fromWorkspaceFile(startDir) {
284
390
  let dir = startDir;
285
391
  while (true) {
286
- const config = readJsonSafe(join2(dir, WORKSPACE_FILE));
392
+ const config = readJsonSafe(join3(dir, WORKSPACE_FILE));
287
393
  if (config?.orgId) {
288
394
  return config.orgId;
289
395
  }
290
- const parent = dirname2(dir);
396
+ const parent = dirname3(dir);
291
397
  if (parent === dir) {
292
398
  return;
293
399
  }
@@ -315,11 +421,17 @@ function resolveOrg(args) {
315
421
  }
316
422
 
317
423
  export {
424
+ rotationIdentity,
425
+ drainResumeOffset,
426
+ writeDrainCheckpoint,
427
+ removeDrainCheckpoint,
318
428
  JOURNAL_DIR,
429
+ envSlug,
319
430
  appendMove,
320
431
  readMovesFromPath,
321
432
  listBuckets,
322
433
  listFlushing,
434
+ sweepOrphanedDrainProgress,
323
435
  bucketStats,
324
436
  pendingJournalStats,
325
437
  SESSIONS_DIR,
@@ -0,0 +1,162 @@
1
+ import {
2
+ repoActiveFlag
3
+ } from "./chunk-DSWT6BCC.js";
4
+ import {
5
+ atomicWriteFile
6
+ } from "./chunk-2GP5UFV7.js";
7
+
8
+ // src/lib/commit-heartbeat.ts
9
+ import { execFileSync } from "child_process";
10
+ import { mkdirSync, readFileSync, readdirSync, unlinkSync } from "fs";
11
+ import { dirname, join, resolve } from "path";
12
+ var FIRED_DIR = "prim/post-commit-fired";
13
+ var WIRED_PATH = "prim/git-hooks-wired";
14
+ var GIT_TIMEOUT_MS = 1e3;
15
+ var MAX_STAMP_BYTES = 1024;
16
+ var REFLOG_SCAN_LIMIT = 50;
17
+ var MAX_FIRED_STAMPS = 256;
18
+ var PRUNE_SLACK = 64;
19
+ var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
20
+ var REFLOG_LINE_RE = /^([0-9a-f]{40}|[0-9a-f]{64})\tHEAD@\{([0-9]+)\}\t(.*)$/u;
21
+ var COMMIT_SUBJECT_RE = /^commit(?: \([^)]*\))?:/u;
22
+ var REFLOG_RESOLUTION_MS = 1e3;
23
+ var POST_COMMIT_GRACE_MS = 6e4;
24
+ function commonPath(cwd, relative) {
25
+ const value = execFileSync("git", ["rev-parse", "--git-common-dir"], {
26
+ cwd,
27
+ encoding: "utf8",
28
+ stdio: ["ignore", "pipe", "ignore"],
29
+ timeout: GIT_TIMEOUT_MS
30
+ }).replace(/\r?\n$/u, "");
31
+ if (value === "" || /[\0\r\n]/u.test(value)) throw new Error("Git returned an unsafe path");
32
+ return join(resolve(cwd, value), relative);
33
+ }
34
+ function gitPath(cwd, relative) {
35
+ const value = execFileSync("git", ["rev-parse", "--git-path", relative], {
36
+ cwd,
37
+ encoding: "utf8",
38
+ stdio: ["ignore", "pipe", "ignore"],
39
+ timeout: GIT_TIMEOUT_MS
40
+ }).replace(/\r?\n$/u, "");
41
+ if (value === "" || /[\0\r\n]/u.test(value)) throw new Error("Git returned an unsafe path");
42
+ return resolve(cwd, value);
43
+ }
44
+ function ensurePrivateParent(path) {
45
+ mkdirSync(dirname(dirname(path)), { recursive: true, mode: 448 });
46
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
47
+ }
48
+ function writeStamp(path, at) {
49
+ ensurePrivateParent(path);
50
+ atomicWriteFile(path, `${JSON.stringify({ at })}
51
+ `, { mode: 384 });
52
+ }
53
+ function readStampAt(path) {
54
+ try {
55
+ const raw = readFileSync(path, "utf8");
56
+ if (raw.length > MAX_STAMP_BYTES) return void 0;
57
+ const parsed = JSON.parse(raw);
58
+ return typeof parsed.at === "number" && Number.isSafeInteger(parsed.at) && parsed.at > 0 ? parsed.at : void 0;
59
+ } catch {
60
+ return void 0;
61
+ }
62
+ }
63
+ function readFiredStamps(cwd) {
64
+ try {
65
+ const dir = gitPath(cwd, FIRED_DIR);
66
+ return readdirSync(dir).flatMap((sha) => {
67
+ if (!FULL_SHA_RE.test(sha)) return [];
68
+ const at = readStampAt(join(dir, sha));
69
+ return at === void 0 ? [] : [{ sha, at }];
70
+ });
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+ function recordPostCommitFired(cwd, sha, now = Date.now()) {
76
+ if (!sha || !FULL_SHA_RE.test(sha)) return;
77
+ try {
78
+ const dir = gitPath(cwd, FIRED_DIR);
79
+ writeStamp(join(dir, sha), now);
80
+ const names = readdirSync(dir).filter((name) => FULL_SHA_RE.test(name));
81
+ if (names.length <= MAX_FIRED_STAMPS + PRUNE_SLACK) return;
82
+ const stale = names.map((name) => ({ name, at: readStampAt(join(dir, name)) ?? 0 })).sort((left, right) => right.at - left.at || left.name.localeCompare(right.name)).slice(MAX_FIRED_STAMPS);
83
+ for (const { name } of stale) unlinkSync(join(dir, name));
84
+ } catch {
85
+ }
86
+ }
87
+ function recordHooksWired(cwd, options = {}) {
88
+ try {
89
+ const path = commonPath(cwd, WIRED_PATH);
90
+ if (options.onlyIfAbsent && readStampAt(path) !== void 0) return;
91
+ writeStamp(path, options.now ?? Date.now());
92
+ } catch {
93
+ }
94
+ }
95
+ function clearHooksWired(cwd) {
96
+ try {
97
+ unlinkSync(commonPath(cwd, WIRED_PATH));
98
+ } catch {
99
+ }
100
+ }
101
+ function latestLocalCommit(cwd) {
102
+ let output;
103
+ try {
104
+ output = execFileSync(
105
+ "git",
106
+ [
107
+ "log",
108
+ "-g",
109
+ "--no-show-signature",
110
+ "-n",
111
+ String(REFLOG_SCAN_LIMIT),
112
+ "--date=unix",
113
+ "--format=%H%x09%gd%x09%gs",
114
+ "HEAD"
115
+ ],
116
+ { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: GIT_TIMEOUT_MS }
117
+ );
118
+ } catch {
119
+ return void 0;
120
+ }
121
+ for (const line of output.split("\n")) {
122
+ const match = REFLOG_LINE_RE.exec(line);
123
+ if (match?.[1] && match[2] && COMMIT_SUBJECT_RE.test(match[3] ?? "")) {
124
+ return { sha: match[1], at: Number(match[2]) * 1e3 };
125
+ }
126
+ }
127
+ return void 0;
128
+ }
129
+ function inspectPostCommitFiring(cwd, now = Date.now()) {
130
+ if (repoActiveFlag(cwd) !== "true") return { state: "inactive" };
131
+ const commit = latestLocalCommit(cwd);
132
+ if (!commit) return { state: "unverified" };
133
+ const stamps = readFiredStamps(cwd);
134
+ const own = stamps.find((stamp) => stamp.sha === commit.sha);
135
+ if (own) return { state: "fired", commitAt: commit.at, firedAt: own.at };
136
+ if (now - commit.at < POST_COMMIT_GRACE_MS) return { state: "pending", commitAt: commit.at };
137
+ const lastFiredAt = stamps.reduce(
138
+ (latest, stamp) => latest === void 0 || stamp.at > latest ? stamp.at : latest,
139
+ void 0
140
+ );
141
+ let wiredAt;
142
+ try {
143
+ wiredAt = readStampAt(commonPath(cwd, WIRED_PATH));
144
+ } catch {
145
+ wiredAt = void 0;
146
+ }
147
+ if (wiredAt === void 0) return { state: "unverified" };
148
+ const firedSinceWired = lastFiredAt !== void 0 && lastFiredAt >= wiredAt;
149
+ const judged = commit.at > wiredAt || firedSinceWired && commit.at + REFLOG_RESOLUTION_MS > lastFiredAt;
150
+ if (judged) {
151
+ return lastFiredAt === void 0 ? { state: "not_firing", commitAt: commit.at } : { state: "not_firing", commitAt: commit.at, firedAt: lastFiredAt };
152
+ }
153
+ return { state: "unverified" };
154
+ }
155
+
156
+ export {
157
+ POST_COMMIT_GRACE_MS,
158
+ recordPostCommitFired,
159
+ recordHooksWired,
160
+ clearHooksWired,
161
+ inspectPostCommitFiring
162
+ };
@@ -19,7 +19,7 @@ import {
19
19
  resolveAuthCredential,
20
20
  withFileLock,
21
21
  workosConnectTokenEndpoint
22
- } from "./chunk-67VNLLBN.js";
22
+ } from "./chunk-2GP5UFV7.js";
23
23
 
24
24
  // src/lib/terminal-safe.ts
25
25
  function replaceIsolatedSurrogates(value) {
@@ -645,6 +645,7 @@ export {
645
645
  isTerminalSafeText,
646
646
  stripControlChars,
647
647
  getSiteUrl,
648
+ getSiteUrlForEnvironment,
648
649
  getTokenExpiresAt,
649
650
  isSessionEnded,
650
651
  commitCredentials,
@@ -3,10 +3,10 @@ import {
3
3
  } from "./chunk-EZXEWFDG.js";
4
4
  import {
5
5
  scrubEnvironmentPaths
6
- } from "./chunk-OAAP2MY6.js";
6
+ } from "./chunk-K7D2EYBS.js";
7
7
  import {
8
8
  isValidRepoSyncId
9
- } from "./chunk-M6RIJVRE.js";
9
+ } from "./chunk-DSWT6BCC.js";
10
10
 
11
11
  // src/hooks/prim-hook-core.ts
12
12
  import { createHash, randomUUID } from "crypto";
@@ -2,7 +2,7 @@ import {
2
2
  cursorHooksPath,
3
3
  hasExactCursorHandler,
4
4
  readCursorHooks
5
- } from "./chunk-26P57LAR.js";
5
+ } from "./chunk-AXQQMX2U.js";
6
6
  import {
7
7
  resolveRepositoryContext
8
8
  } from "./chunk-H4UNYFUC.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  boundedHealthError
3
- } from "./chunk-BTNNEH2W.js";
3
+ } from "./chunk-DSWT6BCC.js";
4
4
  import {
5
5
  terminalSafeLine
6
- } from "./chunk-PCVGBE5B.js";
6
+ } from "./chunk-G2VMA2QO.js";
7
7
 
8
8
  // src/hooks/redact.ts
9
9
  import { existsSync, readFileSync, statSync } from "fs";
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  formatStatusline
3
- } from "./chunk-WX52U7Z2.js";
3
+ } from "./chunk-ZU563PQI.js";
4
4
  import {
5
5
  decisionIngestionStatus,
6
6
  repositoryBindingState
7
- } from "./chunk-M6RIJVRE.js";
7
+ } from "./chunk-DSWT6BCC.js";
8
8
  import {
9
9
  getSiteUrl,
10
10
  isSessionEnded,
11
11
  terminalSafeText
12
- } from "./chunk-PCVGBE5B.js";
12
+ } from "./chunk-G2VMA2QO.js";
13
13
  import {
14
14
  packageVersion
15
- } from "./chunk-26P57LAR.js";
15
+ } from "./chunk-AXQQMX2U.js";
16
16
  import {
17
17
  gitToplevel
18
18
  } from "./chunk-H4UNYFUC.js";
@@ -20,11 +20,11 @@ import {
20
20
  daemonPrincipalsMatch,
21
21
  daemonRequest,
22
22
  resolveDaemonPrincipal
23
- } from "./chunk-KFXF2V5G.js";
23
+ } from "./chunk-OTHKG76J.js";
24
24
  import {
25
25
  primConfigDirectory,
26
26
  withFileLock
27
- } from "./chunk-67VNLLBN.js";
27
+ } from "./chunk-2GP5UFV7.js";
28
28
 
29
29
  // src/hooks/codex-context.ts
30
30
  import { createHash, randomBytes as randomBytes2 } from "crypto";
@@ -3,7 +3,7 @@ import {
3
3
  isTerminalSafeText,
4
4
  terminalSafeLine,
5
5
  terminalSafeText
6
- } from "./chunk-PCVGBE5B.js";
6
+ } from "./chunk-G2VMA2QO.js";
7
7
 
8
8
  // src/decisions/feedback.ts
9
9
  var FEEDBACK_PROTOCOL_VERSION = 2;
@@ -2,7 +2,7 @@ import {
2
2
  decodeJwtPayload,
3
3
  primConfigDirectory,
4
4
  resolveAuthCredential
5
- } from "./chunk-67VNLLBN.js";
5
+ } from "./chunk-2GP5UFV7.js";
6
6
 
7
7
  // src/daemon/client.ts
8
8
  import { createConnection } from "net";
@@ -0,0 +1,13 @@
1
+ // src/lib/unattended.ts
2
+ var UNATTENDED_ENV = "PRIM_UNATTENDED";
3
+ function unattendedEnv(env = process.env) {
4
+ return { ...env, [UNATTENDED_ENV]: "1" };
5
+ }
6
+ function isUnattended(env = process.env) {
7
+ return env[UNATTENDED_ENV] === "1";
8
+ }
9
+
10
+ export {
11
+ unattendedEnv,
12
+ isUnattended
13
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scheduleCollectScopeRefresh
3
- } from "./chunk-M6RIJVRE.js";
3
+ } from "./chunk-DSWT6BCC.js";
4
4
 
5
5
  // src/ingest-response.ts
6
6
  var IngestAcknowledgementError = class extends Error {
@@ -2,10 +2,10 @@ import {
2
2
  getClient,
3
3
  getSiteUrl,
4
4
  terminalSafeLine
5
- } from "./chunk-PCVGBE5B.js";
5
+ } from "./chunk-G2VMA2QO.js";
6
6
  import {
7
7
  daemonRequest
8
- } from "./chunk-KFXF2V5G.js";
8
+ } from "./chunk-OTHKG76J.js";
9
9
 
10
10
  // src/daemon/proxy.ts
11
11
  var DAEMON_HTTP_TIMEOUT_MS = 1e4;