@primitive.ai/prim 0.1.0-alpha.49 → 0.1.0-alpha.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-TPROV45L.js";
3
+ } from "./chunk-DWDEQRWN.js";
4
4
 
5
5
  // src/journal.ts
6
6
  import {
@@ -154,6 +154,7 @@ var CONFIG_DIR_MODE = 448;
154
154
  var CREDENTIAL_FILE_MODE = 384;
155
155
  var REFRESH_THRESHOLD_MS = 6e4;
156
156
  var DEFAULT_API_URL = "https://api.getprimitive.ai";
157
+ var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
157
158
  function loadEnvFile() {
158
159
  const envVars = {};
159
160
  for (const file of [".env.local", ".env"]) {
@@ -349,6 +350,7 @@ async function performTokenRefresh(options = {}) {
349
350
  if (selected?.source !== "token_file") return void 0;
350
351
  const startingGeneration = readTrimmed(REFRESH_TOKEN_PATH);
351
352
  if (!startingGeneration || isSessionEnded()) return void 0;
353
+ const startingAccessToken = selected.token;
352
354
  return withCredentialLock(
353
355
  async () => {
354
356
  const currentCredential = resolveAuthCredential();
@@ -358,6 +360,9 @@ async function performTokenRefresh(options = {}) {
358
360
  if (currentGeneration !== startingGeneration) {
359
361
  return currentCredential.token;
360
362
  }
363
+ if (currentCredential.token !== startingAccessToken) {
364
+ return currentCredential.token;
365
+ }
361
366
  if (isSessionEnded()) return void 0;
362
367
  const response = await fetch(`${getSiteUrl()}/mcp/broker/refresh`, {
363
368
  method: "POST",
@@ -442,6 +447,12 @@ async function request(method, path, body, options) {
442
447
  });
443
448
  if (token) credential = { token, source: "token_file" };
444
449
  }
450
+ if (!credential) {
451
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
452
+ }
453
+ if (isTokenExpiringSoon(credential) && isSessionEnded()) {
454
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
455
+ }
445
456
  const doFetch = (token) => {
446
457
  const headers = { "Content-Type": "application/json" };
447
458
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -471,7 +482,7 @@ async function request(method, path, body, options) {
471
482
  }
472
483
  if (!response.ok) {
473
484
  if (response.status === 401) {
474
- throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
485
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
475
486
  }
476
487
  const errorBody = await response.json().catch(() => null);
477
488
  throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getClient,
3
3
  getSiteUrl
4
- } from "./chunk-TPROV45L.js";
4
+ } from "./chunk-DWDEQRWN.js";
5
5
  import {
6
6
  daemonRequest
7
7
  } from "./chunk-UTKQTZHL.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-TPROV45L.js";
3
+ } from "./chunk-DWDEQRWN.js";
4
4
 
5
5
  // src/decisions/feedback.ts
6
6
  var FEEDBACK_PROTOCOL_VERSION = 1;
@@ -4,7 +4,7 @@
4
4
  import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
5
5
  import { createServer } from "net";
6
6
  import { homedir as homedir5 } from "os";
7
- import { join as join7 } from "path";
7
+ import { join as join8 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
10
10
  // src/client.ts
@@ -163,6 +163,7 @@ var CONFIG_DIR_MODE = 448;
163
163
  var CREDENTIAL_FILE_MODE = 384;
164
164
  var REFRESH_THRESHOLD_MS = 6e4;
165
165
  var DEFAULT_API_URL = "https://api.getprimitive.ai";
166
+ var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
166
167
  function loadEnvFile() {
167
168
  const envVars = {};
168
169
  for (const file of [".env.local", ".env"]) {
@@ -319,6 +320,7 @@ async function performTokenRefresh(options = {}) {
319
320
  if (selected?.source !== "token_file") return void 0;
320
321
  const startingGeneration = readTrimmed(REFRESH_TOKEN_PATH);
321
322
  if (!startingGeneration || isSessionEnded()) return void 0;
323
+ const startingAccessToken = selected.token;
322
324
  return withCredentialLock(
323
325
  async () => {
324
326
  const currentCredential = resolveAuthCredential();
@@ -328,6 +330,9 @@ async function performTokenRefresh(options = {}) {
328
330
  if (currentGeneration !== startingGeneration) {
329
331
  return currentCredential.token;
330
332
  }
333
+ if (currentCredential.token !== startingAccessToken) {
334
+ return currentCredential.token;
335
+ }
331
336
  if (isSessionEnded()) return void 0;
332
337
  const response = await fetch(`${getSiteUrl()}/mcp/broker/refresh`, {
333
338
  method: "POST",
@@ -412,6 +417,12 @@ async function request(method, path, body, options) {
412
417
  });
413
418
  if (token) credential = { token, source: "token_file" };
414
419
  }
420
+ if (!credential) {
421
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
422
+ }
423
+ if (isTokenExpiringSoon(credential) && isSessionEnded()) {
424
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
425
+ }
415
426
  const doFetch = (token) => {
416
427
  const headers = { "Content-Type": "application/json" };
417
428
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -441,7 +452,7 @@ async function request(method, path, body, options) {
441
452
  }
442
453
  if (!response.ok) {
443
454
  if (response.status === 401) {
444
- throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
455
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
445
456
  }
446
457
  const errorBody = await response.json().catch(() => null);
447
458
  throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
@@ -457,6 +468,7 @@ function getClient() {
457
468
 
458
469
  // src/flusher.ts
459
470
  import { createReadStream, renameSync as renameSync2, unlinkSync } from "fs";
471
+ import { dirname as dirname4, join as join4 } from "path";
460
472
  import { createInterface } from "readline";
461
473
 
462
474
  // src/ingest-response.ts
@@ -824,11 +836,23 @@ async function flushOnce() {
824
836
  return { flushed: total };
825
837
  }
826
838
  var flushInFlight;
839
+ var FLUSH_LOCK_PATH = join4(dirname4(JOURNAL_DIR), ".flush.lock");
840
+ var FLUSH_LOCK_TIMEOUT_MS = 250;
841
+ function isFlushLockContended(error) {
842
+ return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
843
+ }
827
844
  function flush() {
828
845
  if (flushInFlight) {
829
846
  return flushInFlight;
830
847
  }
831
- const attempt = flushOnce().finally(() => {
848
+ const attempt = withFileLock(FLUSH_LOCK_PATH, flushOnce, {
849
+ timeoutMs: FLUSH_LOCK_TIMEOUT_MS
850
+ }).catch((error) => {
851
+ if (isFlushLockContended(error)) {
852
+ return { flushed: 0, skipped: true };
853
+ }
854
+ throw error;
855
+ }).finally(() => {
832
856
  if (flushInFlight === attempt) {
833
857
  flushInFlight = void 0;
834
858
  }
@@ -840,8 +864,8 @@ function flush() {
840
864
  // src/daemon/client.ts
841
865
  import { createConnection } from "net";
842
866
  import { homedir as homedir3 } from "os";
843
- import { join as join4 } from "path";
844
- var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
867
+ import { join as join5 } from "path";
868
+ var SOCK_PATH = join5(homedir3(), ".config", "prim", "sock");
845
869
  var DEFAULT_TIMEOUT_MS2 = 250;
846
870
  var nextRequestId = 1;
847
871
  function daemonRequest(method, params = {}, opts = {}) {
@@ -936,7 +960,7 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
936
960
  // src/daemon/health.ts
937
961
  import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
938
962
  import { homedir as homedir4 } from "os";
939
- import { dirname as dirname4, join as join5 } from "path";
963
+ import { dirname as dirname5, join as join6 } from "path";
940
964
  var CONFIG_DIR_MODE2 = 448;
941
965
  var HEALTH_FILE_MODE = 384;
942
966
  var INGESTION_SLA_MS = 3e4;
@@ -944,7 +968,7 @@ var HEARTBEAT_FRESH_MS = 9e4;
944
968
  var HEARTBEAT_RETRY_CAP_MS = 3e4;
945
969
  var INGESTION_RETRY_BASE_MS = 5e3;
946
970
  var INGESTION_RETRY_CAP_MS = 15e3;
947
- var DAEMON_HEALTH_PATH = join5(homedir4(), ".config", "prim", "daemon-health.json");
971
+ var DAEMON_HEALTH_PATH = join6(homedir4(), ".config", "prim", "daemon-health.json");
948
972
  function createDaemonHealthState(version, pid, startedAt2) {
949
973
  return {
950
974
  schemaVersion: 1,
@@ -982,11 +1006,11 @@ function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
982
1006
  return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
983
1007
  }
984
1008
  function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
985
- const dir = dirname4(path);
1009
+ const dir = dirname5(path);
986
1010
  if (!existsSync4(dir)) {
987
1011
  mkdirSync4(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
988
1012
  }
989
- const tmp = join5(dir, `.${process.pid}.daemon-health.tmp`);
1013
+ const tmp = join6(dir, `.${process.pid}.daemon-health.tmp`);
990
1014
  writeFileSync3(tmp, `${JSON.stringify(state, null, 2)}
991
1015
  `, { mode: HEALTH_FILE_MODE });
992
1016
  chmodSync2(tmp, HEALTH_FILE_MODE);
@@ -1006,7 +1030,7 @@ import {
1006
1030
  unlinkSync as unlinkSync2,
1007
1031
  writeFileSync as writeFileSync4
1008
1032
  } from "fs";
1009
- import { join as join6 } from "path";
1033
+ import { join as join7 } from "path";
1010
1034
  var DIR_MODE = 448;
1011
1035
  var FILE_MODE2 = 384;
1012
1036
  var DAEMON_STARTUP_GRACE_MS = 5e3;
@@ -1037,10 +1061,10 @@ function daemonProcessIsAlive(pid) {
1037
1061
  }
1038
1062
  }
1039
1063
  function readDaemonOwner(configDir) {
1040
- return readLockRecord(join6(configDir, "daemon.lock", "owner.json"));
1064
+ return readLockRecord(join7(configDir, "daemon.lock", "owner.json"));
1041
1065
  }
1042
1066
  function readDaemonPid(configDir) {
1043
- const path = join6(configDir, "daemon.pid");
1067
+ const path = join7(configDir, "daemon.pid");
1044
1068
  const pid = numericPid(path);
1045
1069
  if (pid === void 0) {
1046
1070
  return;
@@ -1055,9 +1079,9 @@ function acquireDaemonOwnership(configDir, opts = {}) {
1055
1079
  const pid = opts.pid ?? process.pid;
1056
1080
  const now = opts.now ?? Date.now();
1057
1081
  const isAlive = opts.isAlive ?? daemonProcessIsAlive;
1058
- const lockDir = join6(configDir, "daemon.lock");
1059
- const ownerPath2 = join6(lockDir, "owner.json");
1060
- const pidPath = join6(configDir, "daemon.pid");
1082
+ const lockDir = join7(configDir, "daemon.lock");
1083
+ const ownerPath2 = join7(lockDir, "owner.json");
1084
+ const pidPath = join7(configDir, "daemon.pid");
1061
1085
  const instanceId = randomUUID();
1062
1086
  if (!existsSync5(configDir)) {
1063
1087
  mkdirSync5(configDir, { recursive: true, mode: DIR_MODE });
@@ -1155,8 +1179,8 @@ function releaseDaemonOwnership(ownership2, socketPath) {
1155
1179
  }
1156
1180
 
1157
1181
  // src/daemon/server.ts
1158
- var CONFIG_DIR = join7(homedir5(), ".config", "prim");
1159
- var SOCK_PATH2 = join7(CONFIG_DIR, "sock");
1182
+ var CONFIG_DIR = join8(homedir5(), ".config", "prim");
1183
+ var SOCK_PATH2 = join8(CONFIG_DIR, "sock");
1160
1184
  var HEARTBEAT_INTERVAL_MS = 3e4;
1161
1185
  var INGESTION_POLL_INTERVAL_MS = 15e3;
1162
1186
  var TOKEN_CHECK_INTERVAL_MS = 6e4;
@@ -1190,7 +1214,7 @@ function resolveRuntimeVersion() {
1190
1214
  }
1191
1215
  try {
1192
1216
  const here = fileURLToPath(new URL(".", import.meta.url));
1193
- const pkg = JSON.parse(readFileSync5(join7(here, "..", "..", "package.json"), "utf-8"));
1217
+ const pkg = JSON.parse(readFileSync5(join8(here, "..", "..", "package.json"), "utf-8"));
1194
1218
  return pkg.version ?? "unknown";
1195
1219
  } catch {
1196
1220
  return "unknown";
@@ -1430,6 +1454,10 @@ async function runIngestionLoop() {
1430
1454
  persistHealth();
1431
1455
  try {
1432
1456
  const result = await flush();
1457
+ if (result.skipped) {
1458
+ scheduleIngestion(INGESTION_POLL_INTERVAL_MS);
1459
+ return;
1460
+ }
1433
1461
  daemonHealth.ingestion.lastSuccessAt = Date.now();
1434
1462
  daemonHealth.ingestion.lastAcknowledgedCount = result.flushed;
1435
1463
  daemonHealth.ingestion.consecutiveFailures = 0;
@@ -5,11 +5,11 @@ import {
5
5
  import {
6
6
  appendMove,
7
7
  resolveOrg
8
- } from "../chunk-5K725K35.js";
8
+ } from "../chunk-BOTKPLTI.js";
9
9
  import {
10
10
  isRepoActiveForCapture
11
11
  } from "../chunk-R5ZJRSLV.js";
12
- import "../chunk-TPROV45L.js";
12
+ import "../chunk-DWDEQRWN.js";
13
13
 
14
14
  // src/hooks/post-commit.ts
15
15
  import { execSync, spawn } from "child_process";
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  appendMove,
15
15
  resolveOrg
16
- } from "../chunk-5K725K35.js";
16
+ } from "../chunk-BOTKPLTI.js";
17
17
  import {
18
18
  getOrCreateWorkspaceId
19
19
  } from "../chunk-IMAIBPUC.js";
@@ -25,7 +25,7 @@ import {
25
25
  } from "../chunk-R5ZJRSLV.js";
26
26
  import {
27
27
  getClient
28
- } from "../chunk-TPROV45L.js";
28
+ } from "../chunk-DWDEQRWN.js";
29
29
  import {
30
30
  normalizeEnvelope,
31
31
  parseAgent
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  checkAffectedDecisions,
4
4
  formatDecisionsWarning
5
- } from "../chunk-WRPKAPVE.js";
6
- import "../chunk-TPROV45L.js";
5
+ } from "../chunk-VDLAAUWV.js";
6
+ import "../chunk-DWDEQRWN.js";
7
7
  import "../chunk-UTKQTZHL.js";
8
8
 
9
9
  // src/hooks/pre-commit.ts
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  getClient,
10
10
  getSiteUrl
11
- } from "../chunk-TPROV45L.js";
11
+ } from "../chunk-DWDEQRWN.js";
12
12
  import {
13
13
  daemonRequest
14
14
  } from "../chunk-UTKQTZHL.js";
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  appendMove,
11
11
  resolveOrg
12
- } from "../chunk-5K725K35.js";
12
+ } from "../chunk-BOTKPLTI.js";
13
13
  import {
14
14
  buildHookOutput,
15
15
  handoffHookOutput
@@ -19,7 +19,7 @@ import {
19
19
  acknowledgeDecisionFeedback,
20
20
  leaseDecisionFeedback,
21
21
  renderFeedback
22
- } from "../chunk-CK75QBCB.js";
22
+ } from "../chunk-WELBNODJ.js";
23
23
  import {
24
24
  getOrCreateWorkspaceId
25
25
  } from "../chunk-IMAIBPUC.js";
@@ -29,7 +29,7 @@ import {
29
29
  import {
30
30
  isRepoActiveForCapture
31
31
  } from "../chunk-R5ZJRSLV.js";
32
- import "../chunk-TPROV45L.js";
32
+ import "../chunk-DWDEQRWN.js";
33
33
  import {
34
34
  normalizeEnvelope,
35
35
  parseAgent
@@ -8,7 +8,7 @@ import {
8
8
  acknowledgeDecisionFeedback,
9
9
  leaseDecisionFeedback,
10
10
  renderFeedback
11
- } from "../chunk-CK75QBCB.js";
11
+ } from "../chunk-WELBNODJ.js";
12
12
  import {
13
13
  getOrCreateWorkspaceId
14
14
  } from "../chunk-IMAIBPUC.js";
@@ -20,8 +20,9 @@ import {
20
20
  isRepoActiveForCapture
21
21
  } from "../chunk-R5ZJRSLV.js";
22
22
  import {
23
- getSiteUrl
24
- } from "../chunk-TPROV45L.js";
23
+ getSiteUrl,
24
+ isSessionEnded
25
+ } from "../chunk-DWDEQRWN.js";
25
26
  import {
26
27
  daemonRequest
27
28
  } from "../chunk-UTKQTZHL.js";
@@ -50,6 +51,18 @@ function kickDaemonEnsure(options = {}) {
50
51
  }
51
52
  }
52
53
 
54
+ // src/hooks/reauth-notice.ts
55
+ var REAUTH_NOTICE = "[prim] authentication ended \u2014 run `prim auth login` to resume decision capture";
56
+ function reauthNoticeOutput(agent) {
57
+ if (agent === "codex") {
58
+ return buildHookOutput({ additionalContext: REAUTH_NOTICE });
59
+ }
60
+ if (agent === "claude_code") {
61
+ return buildHookOutput({ systemMessage: REAUTH_NOTICE });
62
+ }
63
+ return;
64
+ }
65
+
53
66
  // src/hooks/session-start.ts
54
67
  var STDIN_TIMEOUT_MS = 1e3;
55
68
  var DAEMON_TIMEOUT_MS = 250;
@@ -110,6 +123,13 @@ async function main() {
110
123
  { sessionId: envelope.session_id },
111
124
  { timeoutMs: DAEMON_TIMEOUT_MS }
112
125
  );
126
+ if (isSessionEnded()) {
127
+ const notice = reauthNoticeOutput(agent);
128
+ if (notice) {
129
+ await emitOutput(notice);
130
+ return;
131
+ }
132
+ }
113
133
  if (agent === "codex") {
114
134
  const snapshot = await daemonRequest(
115
135
  "status_snapshot",
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  checkAffectedDecisions,
13
13
  daemonOrDirectGet,
14
14
  formatDecisionsWarning
15
- } from "./chunk-WRPKAPVE.js";
15
+ } from "./chunk-VDLAAUWV.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-5K725K35.js";
24
+ } from "./chunk-BOTKPLTI.js";
25
25
  import {
26
26
  fetchFeedbackCapability
27
- } from "./chunk-CK75QBCB.js";
27
+ } from "./chunk-WELBNODJ.js";
28
28
  import {
29
29
  inspectWorkspaceId
30
30
  } from "./chunk-IMAIBPUC.js";
@@ -56,7 +56,7 @@ import {
56
56
  resolveAuthCredential,
57
57
  setStoredToken,
58
58
  withFileLock
59
- } from "./chunk-TPROV45L.js";
59
+ } from "./chunk-DWDEQRWN.js";
60
60
  import {
61
61
  daemonIsLive,
62
62
  daemonRequest
@@ -64,7 +64,7 @@ import {
64
64
 
65
65
  // src/index.ts
66
66
  import { readFileSync as readFileSync12 } from "fs";
67
- import { dirname as dirname8, resolve as resolve6 } from "path";
67
+ import { dirname as dirname9, resolve as resolve6 } from "path";
68
68
  import { fileURLToPath as fileURLToPath4 } from "url";
69
69
  import { Command } from "commander";
70
70
  import updateNotifier from "update-notifier";
@@ -4241,10 +4241,11 @@ function registerHooksCommands(program2) {
4241
4241
 
4242
4242
  // src/commands/moves.ts
4243
4243
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
4244
- import { join as join7 } from "path";
4244
+ import { join as join8 } from "path";
4245
4245
 
4246
4246
  // src/flusher.ts
4247
4247
  import { createReadStream, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
4248
+ import { dirname as dirname5, join as join7 } from "path";
4248
4249
  import { createInterface } from "readline";
4249
4250
  var BATCH_SIZE = 500;
4250
4251
  var HTTP_TIMEOUT_MS = 1e4;
@@ -4368,11 +4369,23 @@ async function flushOnce() {
4368
4369
  return { flushed: total };
4369
4370
  }
4370
4371
  var flushInFlight;
4372
+ var FLUSH_LOCK_PATH = join7(dirname5(JOURNAL_DIR), ".flush.lock");
4373
+ var FLUSH_LOCK_TIMEOUT_MS = 250;
4374
+ function isFlushLockContended(error) {
4375
+ return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
4376
+ }
4371
4377
  function flush() {
4372
4378
  if (flushInFlight) {
4373
4379
  return flushInFlight;
4374
4380
  }
4375
- const attempt = flushOnce().finally(() => {
4381
+ const attempt = withFileLock(FLUSH_LOCK_PATH, flushOnce, {
4382
+ timeoutMs: FLUSH_LOCK_TIMEOUT_MS
4383
+ }).catch((error) => {
4384
+ if (isFlushLockContended(error)) {
4385
+ return { flushed: 0, skipped: true };
4386
+ }
4387
+ throw error;
4388
+ }).finally(() => {
4376
4389
  if (flushInFlight === attempt) {
4377
4390
  flushInFlight = void 0;
4378
4391
  }
@@ -4410,7 +4423,11 @@ var WORKSPACE_FILE = ".prim/workspace.json";
4410
4423
  function registerMovesCommands(program2) {
4411
4424
  const moves = program2.command("moves").description("Decision Event Pipeline \u2014 local journal");
4412
4425
  moves.command("flush").description("Drain all local move journals to the server").action(async () => {
4413
- const { flushed } = await flush();
4426
+ const { flushed, skipped } = await flush();
4427
+ if (skipped) {
4428
+ console.log("[prim] another flush is already draining; moves left journaled");
4429
+ return;
4430
+ }
4414
4431
  console.log(`[prim] flushed ${String(flushed)} move${flushed === 1 ? "" : "s"}`);
4415
4432
  });
4416
4433
  moves.command("status").description("Show per-bucket pending stats").action(() => {
@@ -4469,18 +4486,18 @@ function registerMovesCommands(program2) {
4469
4486
  }
4470
4487
  });
4471
4488
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
4472
- const dir = join7(process.cwd(), ".prim");
4489
+ const dir = join8(process.cwd(), ".prim");
4473
4490
  if (!existsSync8(dir)) {
4474
4491
  mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
4475
4492
  }
4476
- const file = join7(process.cwd(), WORKSPACE_FILE);
4493
+ const file = join8(process.cwd(), WORKSPACE_FILE);
4477
4494
  writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
4478
4495
  mode: FILE_MODE
4479
4496
  });
4480
4497
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
4481
4498
  });
4482
4499
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
4483
- const file = join7(process.cwd(), WORKSPACE_FILE);
4500
+ const file = join8(process.cwd(), WORKSPACE_FILE);
4484
4501
  if (!existsSync8(file)) {
4485
4502
  console.log("[prim] no workspace binding in cwd");
4486
4503
  return;
@@ -4581,7 +4598,7 @@ import {
4581
4598
  unlinkSync as unlinkSync5,
4582
4599
  writeFileSync as writeFileSync6
4583
4600
  } from "fs";
4584
- import { join as join8 } from "path";
4601
+ import { join as join9 } from "path";
4585
4602
  var DIR_MODE2 = 448;
4586
4603
  var FILE_MODE2 = 384;
4587
4604
  function ensureDir() {
@@ -4590,7 +4607,7 @@ function ensureDir() {
4590
4607
  }
4591
4608
  }
4592
4609
  function markerPath(sessionId) {
4593
- return join8(SESSIONS_DIR, `${sessionId}.json`);
4610
+ return join9(SESSIONS_DIR, `${sessionId}.json`);
4594
4611
  }
4595
4612
  function registerSessionCommands(program2) {
4596
4613
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -4618,7 +4635,7 @@ function registerSessionCommands(program2) {
4618
4635
  for (const f of files) {
4619
4636
  const sessionId = f.replace(/\.json$/, "");
4620
4637
  try {
4621
- const m = JSON.parse(readFileSync7(join8(SESSIONS_DIR, f), "utf-8"));
4638
+ const m = JSON.parse(readFileSync7(join9(SESSIONS_DIR, f), "utf-8"));
4622
4639
  console.log(`${sessionId} org=${m.orgId}`);
4623
4640
  } catch {
4624
4641
  }
@@ -4638,7 +4655,7 @@ function registerSessionCommands(program2) {
4638
4655
  // src/commands/setup.ts
4639
4656
  import { spawnSync as spawnSync2 } from "child_process";
4640
4657
  import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
4641
- import { join as join9 } from "path";
4658
+ import { join as join10 } from "path";
4642
4659
  var EXIT_INCOMPLETE = 1;
4643
4660
  var EXIT_USAGE3 = 2;
4644
4661
  var SESSION_LABELS = {
@@ -4714,7 +4731,7 @@ function detectProjectConflicts(agent, run) {
4714
4731
  }
4715
4732
  try {
4716
4733
  const root = gitToplevel();
4717
- const preCommit = root && join9(root, ".git", "hooks", "pre-commit");
4734
+ const preCommit = root && join10(root, ".git", "hooks", "pre-commit");
4718
4735
  if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
4719
4736
  conflicts.push(CONFLICT_HOOKS);
4720
4737
  }
@@ -4865,31 +4882,31 @@ import {
4865
4882
  writeFileSync as writeFileSync7
4866
4883
  } from "fs";
4867
4884
  import { homedir as homedir8 } from "os";
4868
- import { dirname as dirname6, join as join11, resolve as resolve4 } from "path";
4885
+ import { dirname as dirname7, join as join12, resolve as resolve4 } from "path";
4869
4886
  import { fileURLToPath as fileURLToPath2 } from "url";
4870
4887
  import { createPatch } from "diff";
4871
4888
 
4872
4889
  // src/commands/claude-plugin.ts
4873
4890
  import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync3, rmdirSync } from "fs";
4874
4891
  import { homedir as homedir7 } from "os";
4875
- import { dirname as dirname5, join as join10, resolve as resolve3 } from "path";
4892
+ import { dirname as dirname6, join as join11, resolve as resolve3 } from "path";
4876
4893
  import { fileURLToPath } from "url";
4877
- var __dirname = dirname5(fileURLToPath(import.meta.url));
4894
+ var __dirname = dirname6(fileURLToPath(import.meta.url));
4878
4895
  var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
4879
4896
  function resolvePluginDir(cwd, scope) {
4880
4897
  if (scope && scope !== "user" && scope !== "project") {
4881
4898
  console.error(`Unknown --scope "${scope}" (expected user or project)`);
4882
4899
  return null;
4883
4900
  }
4884
- const base = scope === "user" ? join10(homedir7(), ".claude") : join10(gitToplevel(cwd) ?? cwd, ".claude");
4885
- return join10(base, "skills", "prim");
4901
+ const base = scope === "user" ? join11(homedir7(), ".claude") : join11(gitToplevel(cwd) ?? cwd, ".claude");
4902
+ return join11(base, "skills", "prim");
4886
4903
  }
4887
4904
  function packageVersion() {
4888
4905
  let dir = __dirname;
4889
- while (dir !== dirname5(dir)) {
4906
+ while (dir !== dirname6(dir)) {
4890
4907
  const p = resolve3(dir, "package.json");
4891
4908
  if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
4892
- dir = dirname5(dir);
4909
+ dir = dirname6(dir);
4893
4910
  }
4894
4911
  return "0.0.0";
4895
4912
  }
@@ -4903,8 +4920,8 @@ function removeDirIfEmpty(dir) {
4903
4920
  }
4904
4921
  function pluginPaths(dir) {
4905
4922
  return {
4906
- manifestPath: join10(dir, ".claude-plugin", "plugin.json"),
4907
- skillPath: join10(dir, "SKILL.md")
4923
+ manifestPath: join11(dir, ".claude-plugin", "plugin.json"),
4924
+ skillPath: join11(dir, "SKILL.md")
4908
4925
  };
4909
4926
  }
4910
4927
  function installClaudePlugin(cwd, opts) {
@@ -4923,7 +4940,7 @@ function installClaudePlugin(cwd, opts) {
4923
4940
  console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
4924
4941
  return 0;
4925
4942
  }
4926
- mkdirSync8(join10(dir, ".claude-plugin"), { recursive: true });
4943
+ mkdirSync8(join11(dir, ".claude-plugin"), { recursive: true });
4927
4944
  atomicWrite3(manifestPath, manifest);
4928
4945
  atomicWrite3(skillPath, skill);
4929
4946
  console.log(`Installed prim skill plugin at ${dir}`);
@@ -4943,7 +4960,7 @@ function uninstallClaudePlugin(cwd, opts) {
4943
4960
  }
4944
4961
  rmSync3(manifestPath, { force: true });
4945
4962
  rmSync3(skillPath, { force: true });
4946
- removeDirIfEmpty(join10(dir, ".claude-plugin"));
4963
+ removeDirIfEmpty(join11(dir, ".claude-plugin"));
4947
4964
  removeDirIfEmpty(dir);
4948
4965
  console.log(`Removed prim skill plugin from ${dir}`);
4949
4966
  return 0;
@@ -4966,7 +4983,7 @@ function statusClaudePlugin(cwd, opts) {
4966
4983
  }
4967
4984
 
4968
4985
  // src/commands/skill.ts
4969
- var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
4986
+ var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
4970
4987
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
4971
4988
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
4972
4989
  var TARGET_CANDIDATES = [
@@ -4984,19 +5001,19 @@ var AGENT_TARGET = {
4984
5001
  hermes: ".hermes.md"
4985
5002
  };
4986
5003
  function userTargetFor(agent) {
4987
- if (agent === "claude") return join11(homedir8(), ".claude", "CLAUDE.md");
4988
- if (agent === "codex") return join11(homedir8(), ".codex", "AGENTS.md");
5004
+ if (agent === "claude") return join12(homedir8(), ".claude", "CLAUDE.md");
5005
+ if (agent === "codex") return join12(homedir8(), ".codex", "AGENTS.md");
4989
5006
  if (agent === "hermes") {
4990
- return join11(process.env.HERMES_HOME ?? join11(homedir8(), ".hermes"), ".hermes.md");
5007
+ return join12(process.env.HERMES_HOME ?? join12(homedir8(), ".hermes"), ".hermes.md");
4991
5008
  }
4992
5009
  return null;
4993
5010
  }
4994
5011
  function loadSkill() {
4995
5012
  let dir = __dirname2;
4996
- while (dir !== dirname6(dir)) {
5013
+ while (dir !== dirname7(dir)) {
4997
5014
  const p = resolve4(dir, "SKILL.md");
4998
5015
  if (existsSync12(p)) return readFileSync10(p, "utf-8");
4999
- dir = dirname6(dir);
5016
+ dir = dirname7(dir);
5000
5017
  }
5001
5018
  throw new Error("SKILL.md not found in package");
5002
5019
  }
@@ -5150,13 +5167,13 @@ function registerSkillCommands(program2) {
5150
5167
 
5151
5168
  // src/commands/statusline.ts
5152
5169
  import { readFileSync as readFileSync11 } from "fs";
5153
- import { dirname as dirname7, resolve as resolve5 } from "path";
5170
+ import { dirname as dirname8, resolve as resolve5 } from "path";
5154
5171
  import { fileURLToPath as fileURLToPath3 } from "url";
5155
5172
  var STATUSLINE_TIMEOUT_MS = 200;
5156
5173
  var STATUSLINE_NAME_CAP = 3;
5157
5174
  function readPackageVersion() {
5158
5175
  try {
5159
- const here = dirname7(fileURLToPath3(import.meta.url));
5176
+ const here = dirname8(fileURLToPath3(import.meta.url));
5160
5177
  const candidates = [
5161
5178
  // The supervised runtime stages a tiny manifest beside the standalone
5162
5179
  // statusline bundle, so it remains versioned without loading the package.
@@ -5197,6 +5214,9 @@ async function renderStatusline() {
5197
5214
  return `primitive ${version} (daemon: down)`;
5198
5215
  }
5199
5216
  if (snapshot.healthy === false) {
5217
+ if (snapshot.needsReauth) {
5218
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
5219
+ }
5200
5220
  if (snapshot.ingestion?.healthy === false) {
5201
5221
  const pending = snapshot.ingestion.pendingCount;
5202
5222
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
@@ -5350,7 +5370,7 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
5350
5370
  }
5351
5371
 
5352
5372
  // src/index.ts
5353
- var __dirname3 = dirname8(fileURLToPath4(import.meta.url));
5373
+ var __dirname3 = dirname9(fileURLToPath4(import.meta.url));
5354
5374
  var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
5355
5375
  updateNotifier({ pkg }).notify();
5356
5376
  var program = new Command();
@@ -312,6 +312,9 @@ async function renderStatusline() {
312
312
  return `primitive ${version} (daemon: down)`;
313
313
  }
314
314
  if (snapshot.healthy === false) {
315
+ if (snapshot.needsReauth) {
316
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
317
+ }
315
318
  if (snapshot.ingestion?.healthy === false) {
316
319
  const pending = snapshot.ingestion.pendingCount;
317
320
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.49",
3
+ "version": "0.1.0-alpha.50",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",