@primitive.ai/prim 0.1.0-alpha.91 → 0.1.0-alpha.93

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 (40) hide show
  1. package/README.md +34 -2
  2. package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
  3. package/dist/{chunk-VW3FDYRT.js → chunk-36TWVNBE.js} +6 -6
  4. package/dist/{chunk-HU4PW6I6.js → chunk-C3VAA5CG.js} +1 -1
  5. package/dist/{chunk-FOLKHXLJ.js → chunk-DCJN6LZH.js} +37 -5
  6. package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
  7. package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
  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-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
  11. package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
  12. package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
  13. package/dist/chunk-OYHYCK4K.js +13 -0
  14. package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
  15. package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
  16. package/dist/{chunk-EWZMDD44.js → chunk-X64HTHUF.js} +22 -3
  17. package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
  18. package/dist/daemon/server.js +382 -124
  19. package/dist/hook-runtime/daemon/server.js +382 -124
  20. package/dist/hook-runtime/hooks/post-commit.js +57 -39
  21. package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
  22. package/dist/hook-runtime/hooks/post-tool-use.js +111 -78
  23. package/dist/hook-runtime/hooks/pre-tool-use.js +48 -26
  24. package/dist/hook-runtime/hooks/prim-hook.js +157 -117
  25. package/dist/hook-runtime/hooks/session-end.js +1 -1
  26. package/dist/hook-runtime/hooks/session-start.js +38 -4
  27. package/dist/hook-runtime/index.js +1293 -526
  28. package/dist/hook-runtime/statusline-main.js +23 -1
  29. package/dist/hooks/post-commit.js +11 -8
  30. package/dist/hooks/post-rewrite.js +11 -8
  31. package/dist/hooks/post-tool-use.js +13 -15
  32. package/dist/hooks/pre-commit.js +4 -4
  33. package/dist/hooks/pre-tool-use.js +10 -12
  34. package/dist/hooks/prim-hook.js +17 -14
  35. package/dist/hooks/session-end.js +4 -4
  36. package/dist/hooks/session-start.js +12 -36
  37. package/dist/index.js +867 -291
  38. package/dist/statusline-main.js +23 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-BTNNEH2W.js +0 -79
package/README.md CHANGED
@@ -238,13 +238,39 @@ installation.
238
238
 
239
239
  A supervised long-lived companion process that continuously drains captured
240
240
  Moves, accelerates decision-graph reads, and powers the "team: N online"
241
- presence count. `prim setup` requires it to become healthy unless the explicit
242
- `--no-daemon` opt-out is supplied; hooks still fail soft if it later degrades.
241
+ presence count. `prim setup` requires it to be live on the current version with
242
+ an authenticated, healthy heartbeat unless the explicit `--no-daemon` opt-out is
243
+ supplied. Hooks still fail soft if it later degrades.
244
+
245
+ `prim daemon start` (and `restart`/`ensure`) does not gate on delivery. While
246
+ Moves are past the 30s delivery SLA it reports them as draining in the
247
+ background when the daemon's last sweep recorded no failure, or when the drains
248
+ that failed acknowledged Moves before failing; another bucket delivering in the
249
+ same sweep does not count. Journal buckets the daemon holds back (unbound,
250
+ another organization, identity unavailable) read as delivery held back, and a
251
+ failure with no progress reads as delivery failing; both are a ⚠ warning,
252
+ still exit 0. Its JSON adds `draining`, `deliveryRetained`, `deliveryFailing`,
253
+ and `ingestion` in those states. Start's JSON `healthy` means the start
254
+ succeeded, so it can differ from `daemon status --json`, which stays degraded
255
+ until the backlog drains. Setup's own health check reports a backlog the
256
+ daemon is draining as a warning and fails on held-back buckets and on failures
257
+ recorded without progress; standalone `prim doctor` still fails a missed SLA.
243
258
 
244
259
  ```bash
245
260
  prim daemon start # start (stop / restart / status)
246
261
  ```
247
262
 
263
+ Ordinary commands may upgrade an older daemon in the background. When an
264
+ installed CLI runs an attended, allowlisted command (for example `auth`,
265
+ `decisions`, `moves`, `skill`, or an agent's `install`/`status`) on macOS and
266
+ finds the launchd daemon on an older version, or on its own version but unable
267
+ to run (say, after a Node upgrade deleted the binary it pinned), it starts a
268
+ detached `prim daemon ensure`, at most once an hour whatever version it
269
+ targets. It never downgrades, never repoints the daemon at another deployment
270
+ or config root, and never runs from hooks or their background drains, `daemon`,
271
+ `doctor`, `setup` or its steps, `auth clear`, removal commands, a development
272
+ checkout, or as root. Set `PRIM_DAEMON_DRIFT_HEAL=0` to turn it off.
273
+
248
274
  ### Decisions
249
275
 
250
276
  Read and respond to the decision graph.
@@ -338,6 +364,12 @@ prim moves tail # Pretty-print recent journal entries
338
364
  prim moves flush # Drain the local journals to the server (also runs from hooks)
339
365
  ```
340
366
 
367
+ Any other command except `uninstall` and a `setup` that starts the daemon (or
368
+ one of its steps) that finds Moves waiting over a minute (or cannot tell how
369
+ long) hands them to a detached background `prim moves flush` and never waits for
370
+ it. It starts none while a live daemon on the same version and deployment, with
371
+ a healthy heartbeat and no recorded ingestion failure, already owns the drain.
372
+
341
373
  ### Skill
342
374
 
343
375
  ```bash
@@ -782,6 +782,7 @@ export {
782
782
  jwtOrganizationId,
783
783
  syncDirectory,
784
784
  syncFile,
785
+ ensureDurableDirectory,
785
786
  atomicWriteFile,
786
787
  processIsAlive,
787
788
  withFileLockSync,
@@ -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-EWZMDD44.js";
15
+ } from "./chunk-X64HTHUF.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";
@@ -2,7 +2,7 @@ import {
2
2
  cursorHooksPath,
3
3
  hasExactCursorHandler,
4
4
  readCursorHooks
5
- } from "./chunk-EWZMDD44.js";
5
+ } from "./chunk-X64HTHUF.js";
6
6
  import {
7
7
  resolveRepositoryContext
8
8
  } from "./chunk-H4UNYFUC.js";
@@ -1,18 +1,21 @@
1
+ import {
2
+ unattendedEnv
3
+ } from "./chunk-OYHYCK4K.js";
1
4
  import {
2
5
  isValidRepoSyncId,
3
6
  setRepoSyncId,
4
7
  setRepositoryBindingState
5
- } from "./chunk-M6RIJVRE.js";
8
+ } from "./chunk-DSWT6BCC.js";
6
9
  import {
7
10
  HttpError,
8
11
  getClient
9
- } from "./chunk-PCVGBE5B.js";
12
+ } from "./chunk-G2VMA2QO.js";
10
13
  import {
11
14
  binFile,
12
15
  compareSemver,
13
16
  cursorConfigDirectory,
14
17
  pinnedNpxCommand
15
- } from "./chunk-EWZMDD44.js";
18
+ } from "./chunk-X64HTHUF.js";
16
19
  import {
17
20
  gitToplevel,
18
21
  githubRepositoryFullName
@@ -20,7 +23,7 @@ import {
20
23
  import {
21
24
  atomicWriteFile,
22
25
  withFileLock
23
- } from "./chunk-67VNLLBN.js";
26
+ } from "./chunk-2GP5UFV7.js";
24
27
 
25
28
  // src/lib/bin-cache.ts
26
29
  import { chmodSync, mkdirSync } from "fs";
@@ -1504,6 +1507,34 @@ async function bindRepository(root, options) {
1504
1507
  return bindRepositoryWithClient(root, getClient(), options);
1505
1508
  }
1506
1509
 
1510
+ // src/daemon/self-heal.ts
1511
+ import { spawn } from "child_process";
1512
+ function kickDaemonEnsure(options = {}) {
1513
+ const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
1514
+ if (!primEntry) {
1515
+ return false;
1516
+ }
1517
+ try {
1518
+ const args = [primEntry, "daemon", "ensure"];
1519
+ if (options.latestBootstrap ?? (options.platform ?? process.platform) === "darwin") {
1520
+ args.push("--latest-bootstrap");
1521
+ }
1522
+ const child = (options.spawnProcess ?? spawn)(
1523
+ options.nodeEntry ?? process.execPath,
1524
+ args,
1525
+ // Unattended: neither SessionStart's repair nor a drift heal is a person
1526
+ // running prim, so the child (and its descendants) never heal in turn.
1527
+ { detached: true, stdio: "ignore", env: unattendedEnv() }
1528
+ );
1529
+ child.once("error", () => {
1530
+ });
1531
+ child.unref();
1532
+ return true;
1533
+ } catch {
1534
+ return false;
1535
+ }
1536
+ }
1537
+
1507
1538
  export {
1508
1539
  warmBinCache,
1509
1540
  postCommitHookBlock,
@@ -1525,5 +1556,6 @@ export {
1525
1556
  refreshClaudePlugins,
1526
1557
  hasUsableCodexGuidance,
1527
1558
  hasUsableCursorSkill,
1528
- registerSkillCommands
1559
+ registerSkillCommands,
1560
+ kickDaemonEnsure
1529
1561
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
- getClient
3
- } from "./chunk-PCVGBE5B.js";
2
+ getClient,
3
+ terminalSafeLine
4
+ } from "./chunk-G2VMA2QO.js";
4
5
  import {
5
6
  gitToplevel,
6
7
  githubRepositoryFullName
@@ -8,7 +9,7 @@ import {
8
9
  import {
9
10
  __commonJS,
10
11
  __toESM
11
- } from "./chunk-67VNLLBN.js";
12
+ } from "./chunk-2GP5UFV7.js";
12
13
 
13
14
  // node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/runtime/ucs2length.js
14
15
  var require_ucs2length = __commonJS({
@@ -18331,6 +18332,73 @@ function scheduleCollectScopeRefresh(cwd, collectScopeVersion, dependencies = de
18331
18332
  scheduledRefreshes.set(cwd, refresh);
18332
18333
  }
18333
18334
 
18335
+ // src/lib/ansi.ts
18336
+ var ANSI_CODES = {
18337
+ yellow: "\x1B[33m",
18338
+ magenta: "\x1B[35m",
18339
+ blue: "\x1B[34m",
18340
+ cyan: "\x1B[36m",
18341
+ green: "\x1B[32m",
18342
+ red: "\x1B[31m",
18343
+ // 256-color approximation of the orange trigger marker (xterm 208).
18344
+ orange: "\x1B[38;5;208m",
18345
+ gray: "\x1B[90m"
18346
+ };
18347
+ var ANSI_RESET = "\x1B[0m";
18348
+ var ANSI_DIM = "\x1B[2m";
18349
+ var ANSI_BOLD = "\x1B[1m";
18350
+ function supportsColor() {
18351
+ if (process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "") {
18352
+ return false;
18353
+ }
18354
+ return process.stderr.isTTY === true;
18355
+ }
18356
+ function color(text, c) {
18357
+ if (!supportsColor()) {
18358
+ return text;
18359
+ }
18360
+ return `${ANSI_CODES[c]}${text}${ANSI_RESET}`;
18361
+ }
18362
+ function dim(text) {
18363
+ if (!supportsColor()) {
18364
+ return text;
18365
+ }
18366
+ return `${ANSI_DIM}${text}${ANSI_RESET}`;
18367
+ }
18368
+ function bold(text) {
18369
+ if (!supportsColor()) {
18370
+ return text;
18371
+ }
18372
+ return `${ANSI_BOLD}${text}${ANSI_RESET}`;
18373
+ }
18374
+ var AREA_COLORS = {
18375
+ auth: "yellow",
18376
+ data: "magenta",
18377
+ mobile: "blue",
18378
+ infra: "cyan",
18379
+ ui: "green",
18380
+ billing: "orange",
18381
+ api: "blue",
18382
+ docs: "gray",
18383
+ testing: "gray"
18384
+ };
18385
+ function colorForArea(area) {
18386
+ if (!area) {
18387
+ return "gray";
18388
+ }
18389
+ return AREA_COLORS[area] ?? "gray";
18390
+ }
18391
+ var MAX_HEALTH_ERROR_LENGTH = 240;
18392
+ function boundedHealthError(value) {
18393
+ if (!value) return void 0;
18394
+ const clean = terminalSafeLine(value);
18395
+ if (!clean) return void 0;
18396
+ return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
18397
+ }
18398
+ function stripAnsi(text) {
18399
+ return text.replace(/\u001b\[[0-9;]*m/g, "");
18400
+ }
18401
+
18334
18402
  export {
18335
18403
  repoActiveFlag,
18336
18404
  isRepoActive,
@@ -18354,5 +18422,11 @@ export {
18354
18422
  isGitHubInstallIntentStatusResponse,
18355
18423
  cachedCollectScopeAdmits,
18356
18424
  fetchAndCacheCollectScope,
18357
- scheduleCollectScopeRefresh
18425
+ scheduleCollectScopeRefresh,
18426
+ color,
18427
+ dim,
18428
+ bold,
18429
+ colorForArea,
18430
+ boundedHealthError,
18431
+ stripAnsi
18358
18432
  };
@@ -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,
@@ -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";
@@ -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";