@primitive.ai/prim 0.1.0-alpha.44 → 0.1.0-alpha.46

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.
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  checkAffectedDecisions,
13
13
  daemonOrDirectGet,
14
14
  formatDecisionsWarning
15
- } from "./chunk-BSZGI5RL.js";
15
+ } from "./chunk-Z4VI6TNX.js";
16
16
  import {
17
17
  JOURNAL_DIR,
18
18
  SESSIONS_DIR,
@@ -21,10 +21,10 @@ import {
21
21
  listFlushing,
22
22
  pendingJournalStats,
23
23
  readMovesFromPath
24
- } from "./chunk-OKUXEBPT.js";
24
+ } from "./chunk-ESLX7ISX.js";
25
25
  import {
26
26
  fetchFeedbackCapability
27
- } from "./chunk-UISU3A7W.js";
27
+ } from "./chunk-JT4Q333I.js";
28
28
  import {
29
29
  inspectWorkspaceId
30
30
  } from "./chunk-IMAIBPUC.js";
@@ -41,14 +41,18 @@ import {
41
41
  import {
42
42
  HttpError,
43
43
  REFRESH_TOKEN_PATH,
44
- TOKEN_EXPIRES_PATH,
45
44
  TOKEN_FILE_PATH,
46
- getAuthToken,
45
+ clearStoredCredentials,
46
+ commitCredentials,
47
47
  getClient,
48
48
  getSiteUrl,
49
49
  getTokenExpiresAt,
50
- saveTokenExpiry
51
- } from "./chunk-W6PAKEDO.js";
50
+ isSessionEnded,
51
+ refreshToken,
52
+ resolveAuthCredential,
53
+ setStoredToken,
54
+ withFileLock
55
+ } from "./chunk-VQ2ZV2D5.js";
52
56
  import {
53
57
  daemonIsLive,
54
58
  daemonRequest
@@ -56,7 +60,7 @@ import {
56
60
 
57
61
  // src/index.ts
58
62
  import { readFileSync as readFileSync12 } from "fs";
59
- import { dirname as dirname9, resolve as resolve6 } from "path";
63
+ import { dirname as dirname8, resolve as resolve6 } from "path";
60
64
  import { fileURLToPath as fileURLToPath4 } from "url";
61
65
  import { Command } from "commander";
62
66
  import updateNotifier from "update-notifier";
@@ -96,10 +100,9 @@ function registerActivationCommands(program2) {
96
100
  // src/commands/auth.ts
97
101
  import { exec } from "child_process";
98
102
  import { createHash, randomBytes } from "crypto";
99
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
103
+ import { existsSync, readFileSync } from "fs";
100
104
  import { createServer } from "http";
101
105
  import { platform } from "os";
102
- import { dirname } from "path";
103
106
 
104
107
  // src/commands/auth-pages.ts
105
108
  var FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="99.77 99.96 400.85 400.85" fill="currentColor">
@@ -248,12 +251,13 @@ var STATE_MISMATCH_HTML = renderPage({
248
251
  });
249
252
 
250
253
  // src/commands/auth.ts
251
- var FILE_MODE = 384;
252
254
  var LOCALHOST = "127.0.0.1";
253
255
  var CALLBACK_PORT = 19876;
254
256
  var CALLBACK_TIMEOUT_MS = 12e4;
255
257
  var EXIT_OK = 0;
256
258
  var EXIT_FAIL = 1;
259
+ var EXIT_UNREACHABLE = 2;
260
+ var AUTH_PROBE_TIMEOUT_MS = 1e4;
257
261
  var HTML_HEADERS = { "Content-Type": "text/html; charset=utf-8" };
258
262
  var BASE64_PLUS_RE = /\+/g;
259
263
  var BASE64_SLASH_RE = /\//g;
@@ -271,13 +275,6 @@ function openBrowser(url) {
271
275
  const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
272
276
  exec(`${cmd} "${url}"`);
273
277
  }
274
- function saveToken(token) {
275
- const dir = dirname(TOKEN_FILE_PATH);
276
- if (!existsSync(dir)) {
277
- mkdirSync(dir, { recursive: true });
278
- }
279
- writeFileSync(TOKEN_FILE_PATH, token, { mode: FILE_MODE });
280
- }
281
278
  function resolveCallbackPage(params, expectedState) {
282
279
  if (params.get("state") !== expectedState) {
283
280
  return {
@@ -327,6 +324,123 @@ function reportSuccess() {
327
324
  console.log(JSON.stringify({ authenticated: true, tokenFile: TOKEN_FILE_PATH }));
328
325
  process.exitCode = EXIT_OK;
329
326
  }
327
+ function hasStoredRefreshToken() {
328
+ if (!existsSync(REFRESH_TOKEN_PATH)) return false;
329
+ try {
330
+ return readFileSync(REFRESH_TOKEN_PATH, "utf-8").trim().length > 0;
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+ function authStatusResult(status, reason, source, refreshTokenPresent) {
336
+ const expiresAt = source === "token_file" ? getTokenExpiresAt() : void 0;
337
+ const expiresInMs = expiresAt === void 0 ? null : expiresAt - Date.now();
338
+ return {
339
+ authenticated: status === "valid",
340
+ status,
341
+ reason,
342
+ tokenFile: source === "token_file" ? TOKEN_FILE_PATH : null,
343
+ accessTokenExpiresInMs: expiresInMs,
344
+ accessTokenExpired: expiresInMs !== null && expiresInMs <= 0,
345
+ refreshTokenPresent,
346
+ warnings: source === "token_file" && !refreshTokenPresent ? ["no refresh token"] : []
347
+ };
348
+ }
349
+ async function verifyAuthStatus() {
350
+ const credential = resolveAuthCredential();
351
+ if (!credential) {
352
+ return authStatusResult("invalid", "missing_credentials", void 0, false);
353
+ }
354
+ const storedCredential = credential.source === "token_file";
355
+ const refreshPresent = storedCredential && hasStoredRefreshToken();
356
+ let accessToken = credential.token;
357
+ if (refreshPresent) {
358
+ try {
359
+ const refreshed = await refreshToken({
360
+ force: true,
361
+ signal: AbortSignal.timeout(AUTH_PROBE_TIMEOUT_MS)
362
+ });
363
+ if (!refreshed) {
364
+ const rejected = isSessionEnded();
365
+ return authStatusResult(
366
+ rejected ? "invalid" : "unreachable",
367
+ rejected ? "refresh_rejected" : "verification_unavailable",
368
+ credential.source,
369
+ true
370
+ );
371
+ }
372
+ accessToken = refreshed;
373
+ } catch {
374
+ const rejected = isSessionEnded();
375
+ return authStatusResult(
376
+ rejected ? "invalid" : "unreachable",
377
+ rejected ? "refresh_rejected" : "verification_unavailable",
378
+ credential.source,
379
+ true
380
+ );
381
+ }
382
+ }
383
+ try {
384
+ const response = await fetch(`${getSiteUrl()}/api/cli/auth/status`, {
385
+ method: "GET",
386
+ headers: { Authorization: `Bearer ${accessToken}` },
387
+ signal: AbortSignal.timeout(AUTH_PROBE_TIMEOUT_MS)
388
+ });
389
+ if (response.ok) {
390
+ return authStatusResult("valid", "verified", credential.source, refreshPresent);
391
+ }
392
+ if (response.status === 401 || response.status === 403) {
393
+ return authStatusResult("invalid", "access_rejected", credential.source, refreshPresent);
394
+ }
395
+ return authStatusResult(
396
+ "unreachable",
397
+ "verification_unavailable",
398
+ credential.source,
399
+ refreshPresent
400
+ );
401
+ } catch {
402
+ return authStatusResult(
403
+ "unreachable",
404
+ "verification_unavailable",
405
+ credential.source,
406
+ refreshPresent
407
+ );
408
+ }
409
+ }
410
+ function authStatusExitCode(status) {
411
+ if (status === "valid") return EXIT_OK;
412
+ return status === "invalid" ? EXIT_FAIL : EXIT_UNREACHABLE;
413
+ }
414
+ function writeHumanAuthStatus(result) {
415
+ if (result.status === "invalid") {
416
+ const detail = result.reason === "refresh_rejected" ? "The saved session was rejected." : result.reason === "access_rejected" ? "The selected access token was rejected." : "No credentials were found.";
417
+ console.log(`${detail} Run \`prim auth login\` to authenticate.`);
418
+ return;
419
+ }
420
+ if (result.status === "unreachable") {
421
+ console.log("Authentication could not be verified. Credentials were retained.");
422
+ return;
423
+ }
424
+ console.log("Authenticated (verified).");
425
+ if (result.tokenFile) {
426
+ console.log(`Token file: ${result.tokenFile}`);
427
+ } else {
428
+ console.log("Token source: fixed environment credential");
429
+ }
430
+ if (result.accessTokenExpiresInMs === null) {
431
+ console.log("Access token expiry: unknown (no metadata)");
432
+ } else if (result.accessTokenExpiresInMs <= 0) {
433
+ console.log("Access token: expired");
434
+ } else {
435
+ const minutes = Math.floor(result.accessTokenExpiresInMs / 6e4);
436
+ const seconds = Math.floor(result.accessTokenExpiresInMs % 6e4 / 1e3);
437
+ console.log(`Access token expires in: ${minutes}m ${seconds}s`);
438
+ }
439
+ console.log(
440
+ `Refresh token: ${result.tokenFile ? result.refreshTokenPresent ? "present" : "missing" : "not applicable"}`
441
+ );
442
+ for (const warning of result.warnings) console.log(`Warning: ${warning}`);
443
+ }
330
444
  function registerAuthCommands(program2) {
331
445
  const auth = program2.command("auth").description("Manage CLI authentication");
332
446
  auth.command("login").description("Authenticate via browser (WorkOS OAuth)").action(async () => {
@@ -417,27 +531,28 @@ ${authUrl.toString()}
417
531
  return;
418
532
  }
419
533
  try {
420
- const token = await exchangeCode(siteUrl, result.code, verifier, redirectUri);
421
- saveToken(token);
534
+ const tokens = await exchangeCode(siteUrl, result.code, verifier, redirectUri);
535
+ await commitCredentials(tokens);
422
536
  reportSuccess();
423
537
  } catch (err) {
424
538
  reportFailure("token_exchange_failed", err instanceof Error ? err.message : String(err));
425
539
  }
426
540
  });
427
- auth.command("set-token <token>").description("Save a bearer token for authenticated CLI calls").action((token) => {
428
- saveToken(token);
541
+ auth.command("set-token <token>").description("Save a bearer token for authenticated CLI calls").action(async (token) => {
542
+ await setStoredToken(token);
429
543
  console.log(`Token saved to ${TOKEN_FILE_PATH}`);
430
544
  });
431
545
  auth.command("clear").description("Remove the saved authentication token").action(async () => {
432
- if (existsSync(REFRESH_TOKEN_PATH)) {
433
- const refreshTokenValue = readFileSync(REFRESH_TOKEN_PATH, "utf-8").trim();
434
- if (refreshTokenValue) {
546
+ const removed = await clearStoredCredentials({
547
+ beforeClear: async (refreshTokenValue) => {
548
+ if (!refreshTokenValue) return;
435
549
  try {
436
550
  const siteUrl = getSiteUrl();
437
551
  const res = await fetch(`${siteUrl}/mcp/broker/revoke`, {
438
552
  method: "POST",
439
553
  headers: { "Content-Type": "application/json" },
440
- body: JSON.stringify({ refresh_token: refreshTokenValue })
554
+ body: JSON.stringify({ refresh_token: refreshTokenValue }),
555
+ signal: AbortSignal.timeout(AUTH_PROBE_TIMEOUT_MS)
441
556
  });
442
557
  if (res.ok) {
443
558
  console.log("Server token revoked.");
@@ -451,62 +566,18 @@ ${authUrl.toString()}
451
566
  console.warn("Could not reach server for revocation \u2014 clearing local files anyway.");
452
567
  }
453
568
  }
454
- }
455
- let removed = false;
456
- for (const filePath of [TOKEN_FILE_PATH, REFRESH_TOKEN_PATH, TOKEN_EXPIRES_PATH]) {
457
- if (existsSync(filePath)) {
458
- rmSync(filePath);
459
- removed = true;
460
- }
461
- }
569
+ });
462
570
  if (removed) {
463
571
  console.log("Local tokens removed.");
464
572
  } else {
465
573
  console.log("No saved tokens found.");
466
574
  }
467
575
  });
468
- auth.command("status").description("Check authentication status and token expiry").option("--json", "Output as JSON").action((opts) => {
469
- const token = getAuthToken();
470
- if (opts.json) {
471
- const expiresAt2 = getTokenExpiresAt();
472
- const expiresInMs = expiresAt2 ? expiresAt2 - Date.now() : null;
473
- const refreshPresent = existsSync(REFRESH_TOKEN_PATH);
474
- printJson({
475
- authenticated: !!token,
476
- tokenFile: token ? TOKEN_FILE_PATH : null,
477
- accessTokenExpiresInMs: expiresInMs,
478
- accessTokenExpired: expiresInMs !== null && expiresInMs <= 0,
479
- refreshTokenPresent: refreshPresent,
480
- warnings: !token || refreshPresent ? [] : ["no refresh token"]
481
- });
482
- process.exit(token ? 0 : 1);
483
- }
484
- if (!token) {
485
- console.log("Not authenticated. Run `prim auth login` to authenticate.");
486
- process.exit(1);
487
- }
488
- console.log("Authenticated.");
489
- console.log(`Token file: ${TOKEN_FILE_PATH}`);
490
- const expiresAt = getTokenExpiresAt();
491
- if (expiresAt) {
492
- const remaining = expiresAt - Date.now();
493
- if (remaining <= 0) {
494
- console.log("Access token: expired");
495
- } else {
496
- const minutes = Math.floor(remaining / 6e4);
497
- const seconds = Math.floor(remaining % 6e4 / 1e3);
498
- console.log(`Access token expires in: ${minutes}m ${seconds}s`);
499
- }
500
- } else {
501
- console.log("Access token expiry: unknown (no metadata)");
502
- }
503
- const hasRefresh = existsSync(REFRESH_TOKEN_PATH);
504
- console.log(`Refresh token: ${hasRefresh ? "present" : "missing"}`);
505
- if (!hasRefresh) {
506
- console.log(
507
- "Warning: No refresh token. Re-run `prim auth login` when access token expires."
508
- );
509
- }
576
+ auth.command("status").description("Check authentication status and token expiry").option("--json", "Output as JSON").action(async (opts) => {
577
+ const result = await verifyAuthStatus();
578
+ if (opts.json) printJson(result);
579
+ else writeHumanAuthStatus(result);
580
+ process.exit(authStatusExitCode(result.status));
510
581
  });
511
582
  }
512
583
  async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
@@ -524,19 +595,14 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
524
595
  throw new Error(`Token exchange failed (${response.status}): ${body}`);
525
596
  }
526
597
  const data = await response.json();
527
- if (!data.access_token) {
528
- throw new Error("No access token in response");
598
+ if (!data.access_token || !data.refresh_token) {
599
+ throw new Error("Token exchange did not return a complete credential pair");
529
600
  }
530
- if (data.refresh_token) {
531
- const refreshPath = TOKEN_FILE_PATH.replace("/token", "/refresh_token");
532
- const dir = dirname(refreshPath);
533
- if (!existsSync(dir)) {
534
- mkdirSync(dir, { recursive: true });
535
- }
536
- writeFileSync(refreshPath, data.refresh_token, { mode: FILE_MODE });
537
- }
538
- saveTokenExpiry(data.access_token, data.expires_in);
539
- return data.access_token;
601
+ return {
602
+ accessToken: data.access_token,
603
+ refreshToken: data.refresh_token,
604
+ expiresIn: data.expires_in
605
+ };
540
606
  }
541
607
 
542
608
  // src/commands/claude-install.ts
@@ -544,14 +610,14 @@ import {
544
610
  closeSync,
545
611
  existsSync as existsSync3,
546
612
  fsyncSync,
547
- mkdirSync as mkdirSync3,
613
+ mkdirSync as mkdirSync2,
548
614
  openSync,
549
615
  readFileSync as readFileSync3,
550
616
  renameSync as renameSync2,
551
- writeFileSync as writeFileSync3
617
+ writeFileSync as writeFileSync2
552
618
  } from "fs";
553
619
  import { homedir as homedir2 } from "os";
554
- import { dirname as dirname3, join as join2 } from "path";
620
+ import { dirname as dirname2, join as join2 } from "path";
555
621
 
556
622
  // src/daemon/launchd.ts
557
623
  import { spawnSync } from "child_process";
@@ -560,17 +626,16 @@ import {
560
626
  chmodSync,
561
627
  copyFileSync,
562
628
  existsSync as existsSync2,
563
- mkdirSync as mkdirSync2,
629
+ mkdirSync,
564
630
  mkdtempSync,
565
631
  readFileSync as readFileSync2,
566
632
  renameSync,
567
- rmSync as rmSync2,
568
- statSync,
633
+ rmSync,
569
634
  symlinkSync,
570
- writeFileSync as writeFileSync2
635
+ writeFileSync
571
636
  } from "fs";
572
637
  import { homedir } from "os";
573
- import { dirname as dirname2, isAbsolute, join, resolve } from "path";
638
+ import { dirname, isAbsolute, join, resolve } from "path";
574
639
  var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
575
640
  var RUNTIME_SCHEMA_VERSION = 1;
576
641
  var RUNTIME_DIR_MODE = 448;
@@ -580,9 +645,6 @@ var PLIST_FILE_MODE = 384;
580
645
  var READY_TIMEOUT_MS = 15e3;
581
646
  var READY_POLL_MS = 100;
582
647
  var READY_PROBE_TIMEOUT_MS = 250;
583
- var LIFECYCLE_LOCK_TIMEOUT_MS = 3e4;
584
- var LIFECYCLE_LOCK_POLL_MS = 50;
585
- var LIFECYCLE_LOCK_INIT_GRACE_MS = 2e3;
586
648
  function xdgDataHome(options) {
587
649
  const home = options.homeDir ?? homedir();
588
650
  const configured = (options.env ?? process.env).XDG_DATA_HOME;
@@ -606,7 +668,7 @@ function runtimePaths(options = {}) {
606
668
  };
607
669
  }
608
670
  function findPackageVersion(entryFile) {
609
- let dir = dirname2(resolve(entryFile));
671
+ let dir = dirname(resolve(entryFile));
610
672
  for (let depth = 0; depth < 6; depth++) {
611
673
  const packagePath = join(dir, "package.json");
612
674
  if (existsSync2(packagePath)) {
@@ -618,7 +680,7 @@ function findPackageVersion(entryFile) {
618
680
  } catch {
619
681
  }
620
682
  }
621
- const parent = dirname2(dir);
683
+ const parent = dirname(dir);
622
684
  if (parent === dir) break;
623
685
  dir = parent;
624
686
  }
@@ -646,7 +708,7 @@ function atomicSymlink(target, linkPath) {
646
708
  try {
647
709
  renameSync(tempLink, linkPath);
648
710
  } catch (error) {
649
- rmSync2(tempLink, { force: true });
711
+ rmSync(tempLink, { force: true });
650
712
  throw error;
651
713
  }
652
714
  }
@@ -677,11 +739,11 @@ function stageRuntime(options = {}) {
677
739
  RUNTIME_LAUNCHER_MODE
678
740
  );
679
741
  } else {
680
- rmSync2(paths.statuslineLauncher, { force: true });
742
+ rmSync(paths.statuslineLauncher, { force: true });
681
743
  }
682
744
  return { changed: false, manifest: current, paths };
683
745
  }
684
- mkdirSync2(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
746
+ mkdirSync(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
685
747
  chmodSync(paths.runtimeDir, RUNTIME_DIR_MODE);
686
748
  chmodSync(paths.releasesDir, RUNTIME_DIR_MODE);
687
749
  const stagingDir = mkdtempSync(join(paths.releasesDir, ".stage-"));
@@ -696,7 +758,7 @@ function stageRuntime(options = {}) {
696
758
  chmodSync(statuslineTarget, RUNTIME_FILE_MODE);
697
759
  }
698
760
  const manifestTarget = join(stagingDir, "manifest.json");
699
- writeFileSync2(manifestTarget, `${JSON.stringify(desired, null, 2)}
761
+ writeFileSync(manifestTarget, `${JSON.stringify(desired, null, 2)}
700
762
  `, {
701
763
  encoding: "utf8",
702
764
  mode: RUNTIME_FILE_MODE,
@@ -715,10 +777,10 @@ function stageRuntime(options = {}) {
715
777
  RUNTIME_LAUNCHER_MODE
716
778
  );
717
779
  } else {
718
- rmSync2(paths.statuslineLauncher, { force: true });
780
+ rmSync(paths.statuslineLauncher, { force: true });
719
781
  }
720
782
  } catch (error) {
721
- rmSync2(stagingDir, { recursive: true, force: true });
783
+ rmSync(stagingDir, { recursive: true, force: true });
722
784
  throw error;
723
785
  }
724
786
  return { changed: true, manifest: desired, paths };
@@ -815,13 +877,13 @@ function atomicWrite(path, content, mode) {
815
877
  }
816
878
  } catch {
817
879
  }
818
- mkdirSync2(dirname2(path), { recursive: true });
880
+ mkdirSync(dirname(path), { recursive: true });
819
881
  const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
820
- writeFileSync2(temp, content, { encoding: "utf8", mode, flag: "wx" });
882
+ writeFileSync(temp, content, { encoding: "utf8", mode, flag: "wx" });
821
883
  try {
822
884
  renameSync(temp, path);
823
885
  } catch (error) {
824
- rmSync2(temp, { force: true });
886
+ rmSync(temp, { force: true });
825
887
  throw error;
826
888
  }
827
889
  return true;
@@ -849,95 +911,9 @@ function processIsAlive(pid) {
849
911
  return error.code === "EPERM";
850
912
  }
851
913
  }
852
- function lifecycleOwnerPath(lockDir) {
853
- return join(lockDir, "owner.json");
854
- }
855
- function readLifecycleOwner(lockDir) {
856
- try {
857
- const owner = JSON.parse(
858
- readFileSync2(lifecycleOwnerPath(lockDir), "utf8")
859
- );
860
- if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || typeof owner.createdAt !== "number") {
861
- return null;
862
- }
863
- return owner;
864
- } catch {
865
- return null;
866
- }
867
- }
868
- function sameLifecycleOwner(a, b) {
869
- return a?.pid === b.pid && a.nonce === b.nonce && a.createdAt === b.createdAt;
870
- }
871
- function tryTakeLifecycleLock(lockDir, now) {
872
- try {
873
- mkdirSync2(lockDir, { mode: RUNTIME_DIR_MODE });
874
- } catch (error) {
875
- if (error.code === "EEXIST") return null;
876
- throw error;
877
- }
878
- const owner = {
879
- pid: process.pid,
880
- nonce: createHash2("sha256").update(`${process.pid}\0${now}\0${Math.random()}`).digest("hex"),
881
- createdAt: now
882
- };
883
- try {
884
- writeFileSync2(lifecycleOwnerPath(lockDir), `${JSON.stringify(owner)}
885
- `, {
886
- encoding: "utf8",
887
- mode: RUNTIME_FILE_MODE,
888
- flag: "wx"
889
- });
890
- return owner;
891
- } catch (error) {
892
- rmSync2(lockDir, { recursive: true, force: true });
893
- throw error;
894
- }
895
- }
896
- function recoverStaleLifecycleLock(lockDir, now, alive) {
897
- const owner = readLifecycleOwner(lockDir);
898
- if (owner) {
899
- if (alive(owner.pid)) return false;
900
- if (!sameLifecycleOwner(readLifecycleOwner(lockDir), owner)) return false;
901
- rmSync2(lockDir, { recursive: true, force: true });
902
- return true;
903
- }
904
- try {
905
- const before = statSync(lockDir).mtimeMs;
906
- if (now - before < LIFECYCLE_LOCK_INIT_GRACE_MS) return false;
907
- if (readLifecycleOwner(lockDir)) return false;
908
- if (statSync(lockDir).mtimeMs !== before) return false;
909
- rmSync2(lockDir, { recursive: true, force: true });
910
- return true;
911
- } catch {
912
- return true;
913
- }
914
- }
915
914
  async function withDaemonLifecycleLock(operation, options = {}) {
916
915
  const lockDir = runtimePaths(options).lifecycleLockDir;
917
- mkdirSync2(dirname2(lockDir), { recursive: true, mode: RUNTIME_DIR_MODE });
918
- const control = options.lifecycleLock;
919
- const nowMs = control?.nowMs ?? Date.now;
920
- const sleepFor = control?.sleep ?? defaultSleep;
921
- const alive = control?.processAlive ?? processIsAlive;
922
- const deadline = nowMs() + (control?.timeoutMs ?? LIFECYCLE_LOCK_TIMEOUT_MS);
923
- let owner = null;
924
- while (!owner) {
925
- const now = nowMs();
926
- owner = tryTakeLifecycleLock(lockDir, now);
927
- if (owner) break;
928
- recoverStaleLifecycleLock(lockDir, now, alive);
929
- if (now >= deadline) {
930
- throw new Error(`timed out waiting for daemon lifecycle lock ${lockDir}`);
931
- }
932
- await sleepFor(control?.pollMs ?? LIFECYCLE_LOCK_POLL_MS);
933
- }
934
- try {
935
- return await operation();
936
- } finally {
937
- if (sameLifecycleOwner(readLifecycleOwner(lockDir), owner)) {
938
- rmSync2(lockDir, { recursive: true, force: true });
939
- }
940
- }
916
+ return withFileLock(lockDir, operation, options.lifecycleLock);
941
917
  }
942
918
  async function stopVerifiedLegacyDaemon(homeDir) {
943
919
  const pidPath = join(homeDir, ".config", "prim", "daemon.pid");
@@ -975,11 +951,11 @@ function daemonExplicitlyDisabled(options = {}) {
975
951
  function setDaemonExplicitlyDisabled(disabled, options = {}) {
976
952
  const marker = runtimePaths(options).disabledMarker;
977
953
  if (!disabled) {
978
- rmSync2(marker, { force: true });
954
+ rmSync(marker, { force: true });
979
955
  return;
980
956
  }
981
- mkdirSync2(dirname2(marker), { recursive: true, mode: RUNTIME_DIR_MODE });
982
- writeFileSync2(marker, "disabled by `prim daemon stop`\n", {
957
+ mkdirSync(dirname(marker), { recursive: true, mode: RUNTIME_DIR_MODE });
958
+ writeFileSync(marker, "disabled by `prim daemon stop`\n", {
983
959
  encoding: "utf8",
984
960
  mode: RUNTIME_FILE_MODE
985
961
  });
@@ -1002,8 +978,8 @@ async function ensureMacDaemonLocked(options) {
1002
978
  if (options.explicitlyStarted) setDaemonExplicitlyDisabled(false, options);
1003
979
  const runtime = stageRuntime(options);
1004
980
  const servicePaths = launchdPaths(options);
1005
- mkdirSync2(dirname2(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
1006
- writeFileSync2(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
981
+ mkdirSync(dirname(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
982
+ writeFileSync(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
1007
983
  chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
1008
984
  const plist = generateLaunchAgentPlist({
1009
985
  nodePath: runtime.manifest.nodePath,
@@ -1318,12 +1294,12 @@ function isGateInstalled(settings) {
1318
1294
  return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
1319
1295
  }
1320
1296
  function atomicWrite2(path, content) {
1321
- const dir = dirname3(path);
1297
+ const dir = dirname2(path);
1322
1298
  if (!existsSync3(dir)) {
1323
- mkdirSync3(dir, { recursive: true });
1299
+ mkdirSync2(dir, { recursive: true });
1324
1300
  }
1325
1301
  const tmp = `${path}.tmp.${String(Date.now())}`;
1326
- writeFileSync3(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
1302
+ writeFileSync2(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
1327
1303
  `, "utf-8");
1328
1304
  const fd = openSync(tmp, "r+");
1329
1305
  try {
@@ -1657,7 +1633,7 @@ ${line("project", result.project)}`);
1657
1633
 
1658
1634
  // src/commands/daemon.ts
1659
1635
  import { spawn } from "child_process";
1660
- import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
1636
+ import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
1661
1637
  import { homedir as homedir4 } from "os";
1662
1638
  import { join as join4 } from "path";
1663
1639
 
@@ -1727,6 +1703,30 @@ var HEALTHY_POLL_MS = 250;
1727
1703
  var EXIT_OK2 = 0;
1728
1704
  var EXIT_NOT_RUNNING = 2;
1729
1705
  var EXIT_BOOTING = 3;
1706
+ var MAX_HEALTH_ERROR_LENGTH = 240;
1707
+ function boundedHealthError(value) {
1708
+ if (!value) return void 0;
1709
+ const clean = stripControlChars(value).replace(/\s+/g, " ").trim();
1710
+ if (!clean) return void 0;
1711
+ return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
1712
+ }
1713
+ function daemonDegradedReason(snapshot) {
1714
+ if (!snapshot || snapshot.healthy !== false) return void 0;
1715
+ if (snapshot.needsReauth) {
1716
+ return "authentication requires `prim auth login`";
1717
+ }
1718
+ if (snapshot.heartbeat?.healthy === false) {
1719
+ const detail = boundedHealthError(snapshot.heartbeat.lastError);
1720
+ return `heartbeat unhealthy${detail ? `: ${detail}` : ""}`;
1721
+ }
1722
+ if (snapshot.ingestion?.healthy === false) {
1723
+ const detail = boundedHealthError(snapshot.ingestion.lastError);
1724
+ const pending = snapshot.ingestion.pendingCount;
1725
+ const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
1726
+ return `ingestion unhealthy${typeof pending === "number" ? ` (${qualifier}${String(pending)} pending)` : ""}${detail ? `: ${detail}` : ""}`;
1727
+ }
1728
+ return "health checks have not recovered";
1729
+ }
1730
1730
  function readPidfile() {
1731
1731
  if (!existsSync4(PID_PATH)) {
1732
1732
  return null;
@@ -1765,7 +1765,7 @@ function spawnDaemon(options) {
1765
1765
  }
1766
1766
  function openDaemonLog(configDir = CONFIG_DIR) {
1767
1767
  if (!existsSync4(configDir)) {
1768
- mkdirSync4(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
1768
+ mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
1769
1769
  }
1770
1770
  return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
1771
1771
  }
@@ -1805,6 +1805,15 @@ async function verifiedPid(existing) {
1805
1805
  function daemonStartIsHealthy(serviceReady, snapshot, expectedVersion) {
1806
1806
  return serviceReady && snapshot?.healthy === true && expectedVersion !== void 0 && snapshot.version === expectedVersion;
1807
1807
  }
1808
+ function daemonStartHealthFields(healthy, snapshot) {
1809
+ if (healthy) return {};
1810
+ return {
1811
+ state: "degraded",
1812
+ needsReauth: snapshot?.needsReauth === true,
1813
+ heartbeat: snapshot?.heartbeat ?? null,
1814
+ ingestion: snapshot?.ingestion ?? null
1815
+ };
1816
+ }
1808
1817
  async function detachedDaemonStart(opts) {
1809
1818
  const existing = readPidfile();
1810
1819
  if (existing?.alive) {
@@ -1961,8 +1970,9 @@ async function macDaemonStart(forceRestart = false) {
1961
1970
  `
1962
1971
  );
1963
1972
  } else {
1973
+ const reason = daemonDegradedReason(snapshot);
1964
1974
  process.stderr.write(
1965
- `[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state (see ${LOG_PATH})
1975
+ `[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state${reason ? ` \xB7 ${reason}` : ""} (see ${LOG_PATH})
1966
1976
  `
1967
1977
  );
1968
1978
  if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
@@ -1977,6 +1987,7 @@ async function macDaemonStart(forceRestart = false) {
1977
1987
  loaded: result.service.loaded,
1978
1988
  responding: result.responding,
1979
1989
  healthy,
1990
+ ...daemonStartHealthFields(healthy, snapshot),
1980
1991
  version: snapshot?.version,
1981
1992
  expectedVersion
1982
1993
  },
@@ -2138,8 +2149,9 @@ function writeLiveSnapshot(snapshot, supervised = false) {
2138
2149
  }
2139
2150
  const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
2140
2151
  if (snapshot.healthy === false) {
2152
+ const reason = daemonDegradedReason(snapshot);
2141
2153
  process.stderr.write(
2142
- `[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}
2154
+ `[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}${reason ? ` \xB7 ${reason}` : ""}
2143
2155
  `
2144
2156
  );
2145
2157
  return;
@@ -3147,12 +3159,13 @@ function classifyDoctor(checks) {
3147
3159
  exitCode: status === "fail" ? EXIT_UNHEALTHY : 0
3148
3160
  };
3149
3161
  }
3150
- function checkAuth() {
3151
- if (!getAuthToken()) {
3162
+ function classifyAuthCredential(credential, expiresAt, hasRefresh) {
3163
+ if (!credential) {
3152
3164
  return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
3153
3165
  }
3154
- const expiresAt = getTokenExpiresAt();
3155
- const hasRefresh = existsSync5(REFRESH_TOKEN_PATH);
3166
+ if (credential.source !== "token_file") {
3167
+ return { name: "auth", status: "ok", detail: "valid fixed bearer credential" };
3168
+ }
3156
3169
  if (expiresAt !== void 0 && Date.now() >= expiresAt) {
3157
3170
  return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
3158
3171
  name: "auth",
@@ -3166,6 +3179,15 @@ function checkAuth() {
3166
3179
  const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
3167
3180
  return { name: "auth", status: "ok", detail };
3168
3181
  }
3182
+ function checkAuth() {
3183
+ const credential = resolveAuthCredential();
3184
+ const storedCredential = credential?.source === "token_file";
3185
+ return classifyAuthCredential(
3186
+ credential,
3187
+ storedCredential ? getTokenExpiresAt() : void 0,
3188
+ storedCredential && existsSync5(REFRESH_TOKEN_PATH)
3189
+ );
3190
+ }
3169
3191
  function classifyDaemonHealth(snapshot, options = {}) {
3170
3192
  if (options.disabled) {
3171
3193
  return {
@@ -3195,6 +3217,13 @@ function classifyDaemonHealth(snapshot, options = {}) {
3195
3217
  detail: `launchd does not own the daemon socket (launchd ${String(options.service.pid ?? "none")} \xB7 socket ${String(snapshot.pid ?? "none")})`
3196
3218
  };
3197
3219
  }
3220
+ if (snapshot.needsReauth) {
3221
+ return {
3222
+ name: "daemon",
3223
+ status: "fail",
3224
+ detail: "authentication ended \u2014 run `prim auth login`"
3225
+ };
3226
+ }
3198
3227
  if (!snapshot.heartbeat?.healthy) {
3199
3228
  return {
3200
3229
  name: "daemon",
@@ -3463,15 +3492,15 @@ import {
3463
3492
  closeSync as closeSync3,
3464
3493
  existsSync as existsSync6,
3465
3494
  fsyncSync as fsyncSync2,
3466
- mkdirSync as mkdirSync5,
3495
+ mkdirSync as mkdirSync4,
3467
3496
  openSync as openSync3,
3468
3497
  readFileSync as readFileSync5,
3469
3498
  renameSync as renameSync3,
3470
- rmSync as rmSync3,
3471
- writeFileSync as writeFileSync4
3499
+ rmSync as rmSync2,
3500
+ writeFileSync as writeFileSync3
3472
3501
  } from "fs";
3473
3502
  import { homedir as homedir5 } from "os";
3474
- import { dirname as dirname4, join as join5 } from "path";
3503
+ import { dirname as dirname3, join as join5 } from "path";
3475
3504
  import { Document, parseDocument, stringify } from "yaml";
3476
3505
  var CAPTURE_BIN3 = "prim-hook";
3477
3506
  var GATE_BIN3 = "prim-pre-tool-use";
@@ -3660,12 +3689,12 @@ function setAutoAccept(raw) {
3660
3689
  `;
3661
3690
  }
3662
3691
  function atomicWriteFile(path, content) {
3663
- const dir = dirname4(path);
3692
+ const dir = dirname3(path);
3664
3693
  if (!existsSync6(dir)) {
3665
- mkdirSync5(dir, { recursive: true });
3694
+ mkdirSync4(dir, { recursive: true });
3666
3695
  }
3667
3696
  const tmp = `${path}.tmp.${String(process.pid)}`;
3668
- writeFileSync4(tmp, content, "utf-8");
3697
+ writeFileSync3(tmp, content, "utf-8");
3669
3698
  const fd = openSync3(tmp, "r+");
3670
3699
  try {
3671
3700
  fsyncSync2(fd);
@@ -3685,15 +3714,15 @@ function assertMergeValid(before, after) {
3685
3714
  }
3686
3715
  function writeShim() {
3687
3716
  const path = shimPath();
3688
- const dir = dirname4(path);
3717
+ const dir = dirname3(path);
3689
3718
  if (!existsSync6(dir)) {
3690
- mkdirSync5(dir, { recursive: true });
3719
+ mkdirSync4(dir, { recursive: true });
3691
3720
  }
3692
- writeFileSync4(path, SHIM_SCRIPT, "utf-8");
3721
+ writeFileSync3(path, SHIM_SCRIPT, "utf-8");
3693
3722
  chmodSync2(path, SHIM_MODE);
3694
3723
  }
3695
3724
  function removeShim() {
3696
- rmSync3(shimPath(), { force: true });
3725
+ rmSync2(shimPath(), { force: true });
3697
3726
  }
3698
3727
  function autoAcceptOf(raw) {
3699
3728
  if (raw.trim().length === 0) {
@@ -3801,9 +3830,9 @@ function registerHermesCommands(program2) {
3801
3830
 
3802
3831
  // src/commands/hooks.ts
3803
3832
  import { execFileSync } from "child_process";
3804
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
3833
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
3805
3834
  import { homedir as homedir6 } from "os";
3806
- import { dirname as dirname5, join as join6, resolve as resolve2 } from "path";
3835
+ import { dirname as dirname4, join as join6, resolve as resolve2 } from "path";
3807
3836
  import { Option } from "commander";
3808
3837
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
3809
3838
  var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
@@ -3856,12 +3885,12 @@ function mergePrimBlock(hookPath, block, binName) {
3856
3885
  const existing = readFileSync6(hookPath, "utf-8");
3857
3886
  if (containsPrimHook(existing, binName)) return false;
3858
3887
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
3859
- writeFileSync5(hookPath, `${existing}${separator}${block}
3888
+ writeFileSync4(hookPath, `${existing}${separator}${block}
3860
3889
  `, { mode: 493 });
3861
3890
  return true;
3862
3891
  }
3863
- mkdirSync6(dirname5(hookPath), { recursive: true });
3864
- writeFileSync5(hookPath, `#!/bin/sh
3892
+ mkdirSync5(dirname4(hookPath), { recursive: true });
3893
+ writeFileSync4(hookPath, `#!/bin/sh
3865
3894
  # ${PRIM_CREATED_MARK}
3866
3895
 
3867
3896
  ${block}
@@ -3924,7 +3953,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
3924
3953
  const hooksDir = resolve2(gitRoot, ".git", "hooks");
3925
3954
  const hookPath = resolve2(hooksDir, spec.hookName);
3926
3955
  if (!existsSync7(hooksDir)) {
3927
- mkdirSync6(hooksDir, { recursive: true });
3956
+ mkdirSync5(hooksDir, { recursive: true });
3928
3957
  }
3929
3958
  if (existsSync7(hookPath)) {
3930
3959
  const existing = readFileSync6(hookPath, "utf-8");
@@ -3936,7 +3965,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
3936
3965
  console.log("To replace it, run: prim hooks uninstall && prim hooks install");
3937
3966
  return;
3938
3967
  }
3939
- writeFileSync5(hookPath, dotGitScript(spec), { mode: 493 });
3968
+ writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
3940
3969
  console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
3941
3970
  }
3942
3971
  var PRIM_GIT_HOOKS_DIR = join6(homedir6(), ".config", "prim", "git-hooks");
@@ -4007,15 +4036,15 @@ function ownedHookNames() {
4007
4036
  }
4008
4037
  function writeOwnHooks() {
4009
4038
  if (!existsSync7(PRIM_GIT_HOOKS_DIR)) {
4010
- mkdirSync6(PRIM_GIT_HOOKS_DIR, { recursive: true });
4039
+ mkdirSync5(PRIM_GIT_HOOKS_DIR, { recursive: true });
4011
4040
  }
4012
4041
  for (const spec of HOOKS) {
4013
- writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
4042
+ writeFileSync4(resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
4014
4043
  mode: 493
4015
4044
  });
4016
4045
  }
4017
4046
  for (const name of PASSTHROUGH_HOOKS) {
4018
- writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
4047
+ writeFileSync4(resolve2(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
4019
4048
  }
4020
4049
  }
4021
4050
  function appendPrimBlock(hookPath, spec) {
@@ -4035,7 +4064,7 @@ function stripPrimBlock(hookPath, spec) {
4035
4064
  unlinkSync2(hookPath);
4036
4065
  return;
4037
4066
  }
4038
- writeFileSync5(hookPath, out, { mode: 493 });
4067
+ writeFileSync4(hookPath, out, { mode: 493 });
4039
4068
  }
4040
4069
  function installGlobalHooks(opts = {}) {
4041
4070
  const global = gitConfigGet("--global");
@@ -4183,7 +4212,7 @@ function registerHooksCommands(program2) {
4183
4212
  }
4184
4213
 
4185
4214
  // src/commands/moves.ts
4186
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4, writeFileSync as writeFileSync6 } from "fs";
4215
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
4187
4216
  import { join as join7 } from "path";
4188
4217
 
4189
4218
  // src/flusher.ts
@@ -4348,7 +4377,7 @@ var EVENT_COL_WIDTH = 20;
4348
4377
  var BUCKET_COL_WIDTH = 20;
4349
4378
  var TAIL_BUCKET_COL_WIDTH = 12;
4350
4379
  var DIR_MODE = 448;
4351
- var FILE_MODE2 = 384;
4380
+ var FILE_MODE = 384;
4352
4381
  var WORKSPACE_FILE = ".prim/workspace.json";
4353
4382
  function registerMovesCommands(program2) {
4354
4383
  const moves = program2.command("moves").description("Decision Event Pipeline \u2014 local journal");
@@ -4414,11 +4443,11 @@ function registerMovesCommands(program2) {
4414
4443
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
4415
4444
  const dir = join7(process.cwd(), ".prim");
4416
4445
  if (!existsSync8(dir)) {
4417
- mkdirSync7(dir, { recursive: true, mode: DIR_MODE });
4446
+ mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
4418
4447
  }
4419
4448
  const file = join7(process.cwd(), WORKSPACE_FILE);
4420
- writeFileSync6(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
4421
- mode: FILE_MODE2
4449
+ writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
4450
+ mode: FILE_MODE
4422
4451
  });
4423
4452
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
4424
4453
  });
@@ -4518,18 +4547,18 @@ function registerReconcileCommands(program2) {
4518
4547
  // src/commands/session.ts
4519
4548
  import {
4520
4549
  existsSync as existsSync9,
4521
- mkdirSync as mkdirSync8,
4550
+ mkdirSync as mkdirSync7,
4522
4551
  readFileSync as readFileSync7,
4523
4552
  readdirSync,
4524
4553
  unlinkSync as unlinkSync5,
4525
- writeFileSync as writeFileSync7
4554
+ writeFileSync as writeFileSync6
4526
4555
  } from "fs";
4527
4556
  import { join as join8 } from "path";
4528
4557
  var DIR_MODE2 = 448;
4529
- var FILE_MODE3 = 384;
4558
+ var FILE_MODE2 = 384;
4530
4559
  function ensureDir() {
4531
4560
  if (!existsSync9(SESSIONS_DIR)) {
4532
- mkdirSync8(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
4561
+ mkdirSync7(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
4533
4562
  }
4534
4563
  }
4535
4564
  function markerPath(sessionId) {
@@ -4543,8 +4572,8 @@ function registerSessionCommands(program2) {
4543
4572
  orgId: opts.orgId,
4544
4573
  startedAt: Date.now()
4545
4574
  };
4546
- writeFileSync7(markerPath(sessionId), JSON.stringify(marker, null, 2), {
4547
- mode: FILE_MODE3
4575
+ writeFileSync6(markerPath(sessionId), JSON.stringify(marker, null, 2), {
4576
+ mode: FILE_MODE2
4548
4577
  });
4549
4578
  console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
4550
4579
  });
@@ -4684,7 +4713,17 @@ function resolveAgent(agentFlag, env) {
4684
4713
  }
4685
4714
  return { agent: detectAgent(env), detected: true };
4686
4715
  }
4687
- function registerSetupCommand(program2) {
4716
+ function parseSetupAuthStatus(result) {
4717
+ try {
4718
+ const parsed = JSON.parse(result.stdout || "{}");
4719
+ if (parsed.status === "valid" || parsed.authenticated === true) return "valid";
4720
+ if (parsed.status === "unreachable") return "unreachable";
4721
+ if (parsed.status === "invalid") return "invalid";
4722
+ } catch {
4723
+ }
4724
+ return result.code === EXIT_USAGE3 ? "unreachable" : "invalid";
4725
+ }
4726
+ function registerSetupCommand(program2, dependencies = {}) {
4688
4727
  program2.command("setup").description(
4689
4728
  "Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
4690
4729
  ).option("--agent <agent>", "claude, codex, or hermes (auto-detected when omitted)").option(
@@ -4701,50 +4740,64 @@ function registerSetupCommand(program2) {
4701
4740
  `[prim] unknown --agent "${agentInput}" (expected claude, codex, or hermes)
4702
4741
  `
4703
4742
  );
4704
- process.exit(EXIT_USAGE3);
4743
+ (dependencies.exit ?? process.exit)(EXIT_USAGE3);
4744
+ return;
4705
4745
  }
4706
4746
  if (opts.scope !== "project" && opts.scope !== "user") {
4707
4747
  process.stderr.write(`[prim] unknown --scope "${opts.scope}" (expected project or user)
4708
4748
  `);
4709
- process.exit(EXIT_USAGE3);
4749
+ (dependencies.exit ?? process.exit)(EXIT_USAGE3);
4750
+ return;
4710
4751
  }
4711
4752
  const agent = agentInput;
4712
4753
  const scope = opts.scope;
4713
4754
  const self = process.argv[1];
4714
- const run = (args, capture = false) => {
4755
+ const run = dependencies.run ?? ((args, capture = false) => {
4715
4756
  const r = spawnSync2(process.execPath, [self, ...args], {
4716
4757
  stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
4717
4758
  encoding: "utf-8"
4718
4759
  });
4719
4760
  return { code: r.status ?? 1, stdout: capture ? r.stdout ?? "" : "" };
4720
- };
4761
+ });
4721
4762
  const results = {};
4722
- const note = (msg) => {
4763
+ const note = dependencies.note ?? ((msg) => {
4723
4764
  process.stderr.write(`[prim] ${msg}
4724
4765
  `);
4725
- };
4726
- const isAuthed = (json) => {
4727
- try {
4728
- return JSON.parse(json || "{}").authenticated === true;
4729
- } catch {
4730
- return false;
4731
- }
4732
- };
4766
+ });
4767
+ const exit = dependencies.exit ?? process.exit;
4733
4768
  if (detected && agent !== "claude") {
4734
4769
  note(`agent \xB7 detected ${agent} session (override with --agent <agent>)`);
4735
4770
  }
4771
+ let authStatus = parseSetupAuthStatus(run(["auth", "status", "--json"], true));
4772
+ if (authStatus === "unreachable") {
4773
+ note("auth \xB7 verification unavailable; no setup changes were made");
4774
+ exit(EXIT_USAGE3);
4775
+ return;
4776
+ }
4777
+ if (authStatus === "invalid") {
4778
+ note("auth \xB7 opening browser to authenticate\u2026");
4779
+ const login = run(["auth", "login"]);
4780
+ if (login.code !== 0) {
4781
+ note("auth \xB7 login failed; no integration changes were made");
4782
+ exit(EXIT_INCOMPLETE);
4783
+ return;
4784
+ }
4785
+ authStatus = parseSetupAuthStatus(run(["auth", "status", "--json"], true));
4786
+ if (authStatus !== "valid") {
4787
+ note(
4788
+ authStatus === "unreachable" ? "auth \xB7 login completed but verification is unavailable; no integration changes were made" : "auth \xB7 login did not produce valid credentials; no integration changes were made"
4789
+ );
4790
+ exit(authStatus === "unreachable" ? EXIT_USAGE3 : EXIT_INCOMPLETE);
4791
+ return;
4792
+ }
4793
+ } else {
4794
+ note("auth \xB7 already authenticated");
4795
+ }
4796
+ results.auth = "ok";
4736
4797
  if (agent === "claude") {
4737
4798
  note("pre-authorize \xB7 writing prim allow-rule (user scope)\u2026");
4738
4799
  results.preauth = run(["claude", "preauth", "--scope", "user"]).code === 0 ? "ok" : "skipped";
4739
4800
  }
4740
- if (isAuthed(run(["auth", "status", "--json"], true).stdout)) {
4741
- note("auth \xB7 already authenticated");
4742
- results.auth = "ok";
4743
- } else {
4744
- note("auth \xB7 opening browser to authenticate\u2026");
4745
- run(["auth", "login"]);
4746
- results.auth = isAuthed(run(["auth", "status", "--json"], true).stdout) ? "ok" : "failed";
4747
- }
4748
4801
  for (const step of planSetupSteps({ agent, daemon: opts.daemon, scope })) {
4749
4802
  note(`${step.label} \xB7 installing\u2026`);
4750
4803
  const { code } = run(step.args);
@@ -4769,7 +4822,7 @@ function registerSetupCommand(program2) {
4769
4822
  note(
4770
4823
  `setup ${failed.length === 0 ? "complete" : `incomplete (failed: ${failed.join(", ")})`} \u2014 ${trail}`
4771
4824
  );
4772
- process.exit(failed.length === 0 ? 0 : EXIT_INCOMPLETE);
4825
+ exit(failed.length === 0 ? 0 : EXIT_INCOMPLETE);
4773
4826
  });
4774
4827
  }
4775
4828
 
@@ -4781,19 +4834,19 @@ import {
4781
4834
  openSync as openSync4,
4782
4835
  readFileSync as readFileSync10,
4783
4836
  renameSync as renameSync5,
4784
- writeFileSync as writeFileSync8
4837
+ writeFileSync as writeFileSync7
4785
4838
  } from "fs";
4786
4839
  import { homedir as homedir8 } from "os";
4787
- import { dirname as dirname7, join as join11, resolve as resolve4 } from "path";
4840
+ import { dirname as dirname6, join as join11, resolve as resolve4 } from "path";
4788
4841
  import { fileURLToPath as fileURLToPath2 } from "url";
4789
4842
  import { createPatch } from "diff";
4790
4843
 
4791
4844
  // src/commands/claude-plugin.ts
4792
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync4, rmdirSync } from "fs";
4845
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync3, rmdirSync } from "fs";
4793
4846
  import { homedir as homedir7 } from "os";
4794
- import { dirname as dirname6, join as join10, resolve as resolve3 } from "path";
4847
+ import { dirname as dirname5, join as join10, resolve as resolve3 } from "path";
4795
4848
  import { fileURLToPath } from "url";
4796
- var __dirname = dirname6(fileURLToPath(import.meta.url));
4849
+ var __dirname = dirname5(fileURLToPath(import.meta.url));
4797
4850
  var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
4798
4851
  function resolvePluginDir(cwd, scope) {
4799
4852
  if (scope && scope !== "user" && scope !== "project") {
@@ -4805,10 +4858,10 @@ function resolvePluginDir(cwd, scope) {
4805
4858
  }
4806
4859
  function packageVersion() {
4807
4860
  let dir = __dirname;
4808
- while (dir !== dirname6(dir)) {
4861
+ while (dir !== dirname5(dir)) {
4809
4862
  const p = resolve3(dir, "package.json");
4810
4863
  if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
4811
- dir = dirname6(dir);
4864
+ dir = dirname5(dir);
4812
4865
  }
4813
4866
  return "0.0.0";
4814
4867
  }
@@ -4842,7 +4895,7 @@ function installClaudePlugin(cwd, opts) {
4842
4895
  console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
4843
4896
  return 0;
4844
4897
  }
4845
- mkdirSync9(join10(dir, ".claude-plugin"), { recursive: true });
4898
+ mkdirSync8(join10(dir, ".claude-plugin"), { recursive: true });
4846
4899
  atomicWrite3(manifestPath, manifest);
4847
4900
  atomicWrite3(skillPath, skill);
4848
4901
  console.log(`Installed prim skill plugin at ${dir}`);
@@ -4860,8 +4913,8 @@ function uninstallClaudePlugin(cwd, opts) {
4860
4913
  console.log(`prim skill plugin not present at ${dir}`);
4861
4914
  return 0;
4862
4915
  }
4863
- rmSync4(manifestPath, { force: true });
4864
- rmSync4(skillPath, { force: true });
4916
+ rmSync3(manifestPath, { force: true });
4917
+ rmSync3(skillPath, { force: true });
4865
4918
  removeDirIfEmpty(join10(dir, ".claude-plugin"));
4866
4919
  removeDirIfEmpty(dir);
4867
4920
  console.log(`Removed prim skill plugin from ${dir}`);
@@ -4885,7 +4938,7 @@ function statusClaudePlugin(cwd, opts) {
4885
4938
  }
4886
4939
 
4887
4940
  // src/commands/skill.ts
4888
- var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
4941
+ var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
4889
4942
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
4890
4943
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
4891
4944
  var TARGET_CANDIDATES = [
@@ -4912,10 +4965,10 @@ function userTargetFor(agent) {
4912
4965
  }
4913
4966
  function loadSkill() {
4914
4967
  let dir = __dirname2;
4915
- while (dir !== dirname7(dir)) {
4968
+ while (dir !== dirname6(dir)) {
4916
4969
  const p = resolve4(dir, "SKILL.md");
4917
4970
  if (existsSync12(p)) return readFileSync10(p, "utf-8");
4918
- dir = dirname7(dir);
4971
+ dir = dirname6(dir);
4919
4972
  }
4920
4973
  throw new Error("SKILL.md not found in package");
4921
4974
  }
@@ -4948,7 +5001,7 @@ function removeBlock(existing) {
4948
5001
  }
4949
5002
  function atomicWrite3(target, content) {
4950
5003
  const tmp = `${target}.tmp`;
4951
- writeFileSync8(tmp, content);
5004
+ writeFileSync7(tmp, content);
4952
5005
  const fd = openSync4(tmp, "r+");
4953
5006
  try {
4954
5007
  fsyncSync3(fd);
@@ -5069,13 +5122,13 @@ function registerSkillCommands(program2) {
5069
5122
 
5070
5123
  // src/commands/statusline.ts
5071
5124
  import { readFileSync as readFileSync11 } from "fs";
5072
- import { dirname as dirname8, resolve as resolve5 } from "path";
5125
+ import { dirname as dirname7, resolve as resolve5 } from "path";
5073
5126
  import { fileURLToPath as fileURLToPath3 } from "url";
5074
5127
  var STATUSLINE_TIMEOUT_MS = 200;
5075
5128
  var STATUSLINE_NAME_CAP = 3;
5076
5129
  function readPackageVersion() {
5077
5130
  try {
5078
- const here = dirname8(fileURLToPath3(import.meta.url));
5131
+ const here = dirname7(fileURLToPath3(import.meta.url));
5079
5132
  const candidates = [
5080
5133
  // The supervised runtime stages a tiny manifest beside the standalone
5081
5134
  // statusline bundle, so it remains versioned without loading the package.
@@ -5268,7 +5321,7 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
5268
5321
  }
5269
5322
 
5270
5323
  // src/index.ts
5271
- var __dirname3 = dirname9(fileURLToPath4(import.meta.url));
5324
+ var __dirname3 = dirname8(fileURLToPath4(import.meta.url));
5272
5325
  var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
5273
5326
  updateNotifier({ pkg }).notify();
5274
5327
  var program = new Command();