@primitive.ai/prim 0.1.0-alpha.48 → 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.
@@ -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,8 +5,11 @@ import {
5
5
  import {
6
6
  appendMove,
7
7
  resolveOrg
8
- } from "../chunk-5K725K35.js";
9
- import "../chunk-TPROV45L.js";
8
+ } from "../chunk-BOTKPLTI.js";
9
+ import {
10
+ isRepoActiveForCapture
11
+ } from "../chunk-R5ZJRSLV.js";
12
+ import "../chunk-DWDEQRWN.js";
10
13
 
11
14
  // src/hooks/post-commit.ts
12
15
  import { execSync, spawn } from "child_process";
@@ -53,20 +56,31 @@ function spawnBackgroundFlush() {
53
56
  stdio: "ignore"
54
57
  }).unref();
55
58
  }
56
- try {
59
+ function runPostCommit() {
60
+ const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
61
+ if (!isRepoActiveForCapture(cwd)) {
62
+ return;
63
+ }
57
64
  const commit = readCommit();
58
65
  if (commit) {
59
- const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
60
66
  const move = toCommitMove(commit, resolveCliVersion(), cwd);
61
67
  const { orgId } = resolveOrg({ sessionId: "", cwd });
62
68
  appendMove(move, orgId);
63
69
  spawnBackgroundFlush();
64
70
  }
65
- } catch (err) {
66
- if (process.env.PRIM_HOOK_DEBUG) {
67
- const detail = err instanceof Error ? err.message : String(err);
68
- process.stderr.write(`[prim-post-commit] capture failed: ${detail}
71
+ }
72
+ if (!process.env.VITEST) {
73
+ try {
74
+ runPostCommit();
75
+ } catch (err) {
76
+ if (process.env.PRIM_HOOK_DEBUG) {
77
+ const detail = err instanceof Error ? err.message : String(err);
78
+ process.stderr.write(`[prim-post-commit] capture failed: ${detail}
69
79
  `);
80
+ }
70
81
  }
82
+ process.exit(0);
71
83
  }
72
- process.exit(0);
84
+ export {
85
+ runPostCommit
86
+ };
@@ -13,17 +13,19 @@ 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";
20
20
  import {
21
- isRepoActiveForCapture,
22
21
  warmBinCache
23
- } from "../chunk-F7O7V6ZM.js";
22
+ } from "../chunk-HSZPTVKU.js";
23
+ import {
24
+ isRepoActiveForCapture
25
+ } from "../chunk-R5ZJRSLV.js";
24
26
  import {
25
27
  getClient
26
- } from "../chunk-TPROV45L.js";
28
+ } from "../chunk-DWDEQRWN.js";
27
29
  import {
28
30
  normalizeEnvelope,
29
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
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- isRepoActiveForCapture,
4
3
  warmBinCache
5
- } from "../chunk-F7O7V6ZM.js";
4
+ } from "../chunk-HSZPTVKU.js";
5
+ import {
6
+ isRepoActiveForCapture
7
+ } from "../chunk-R5ZJRSLV.js";
6
8
  import {
7
9
  getClient,
8
10
  getSiteUrl
9
- } from "../chunk-TPROV45L.js";
11
+ } from "../chunk-DWDEQRWN.js";
10
12
  import {
11
13
  daemonRequest
12
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,15 +19,17 @@ 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";
26
26
  import {
27
- isRepoActiveForCapture,
28
27
  warmBinCache
29
- } from "../chunk-F7O7V6ZM.js";
30
- import "../chunk-TPROV45L.js";
28
+ } from "../chunk-HSZPTVKU.js";
29
+ import {
30
+ isRepoActiveForCapture
31
+ } from "../chunk-R5ZJRSLV.js";
32
+ import "../chunk-DWDEQRWN.js";
31
33
  import {
32
34
  normalizeEnvelope,
33
35
  parseAgent
@@ -8,18 +8,21 @@ 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";
15
15
  import {
16
16
  binFile,
17
- isRepoActiveForCapture,
18
17
  warmBinCache
19
- } from "../chunk-F7O7V6ZM.js";
18
+ } from "../chunk-HSZPTVKU.js";
20
19
  import {
21
- getSiteUrl
22
- } from "../chunk-TPROV45L.js";
20
+ isRepoActiveForCapture
21
+ } from "../chunk-R5ZJRSLV.js";
22
+ import {
23
+ getSiteUrl,
24
+ isSessionEnded
25
+ } from "../chunk-DWDEQRWN.js";
23
26
  import {
24
27
  daemonRequest
25
28
  } from "../chunk-UTKQTZHL.js";
@@ -48,6 +51,18 @@ function kickDaemonEnsure(options = {}) {
48
51
  }
49
52
  }
50
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
+
51
66
  // src/hooks/session-start.ts
52
67
  var STDIN_TIMEOUT_MS = 1e3;
53
68
  var DAEMON_TIMEOUT_MS = 250;
@@ -108,6 +123,13 @@ async function main() {
108
123
  { sessionId: envelope.session_id },
109
124
  { timeoutMs: DAEMON_TIMEOUT_MS }
110
125
  );
126
+ if (isSessionEnded()) {
127
+ const notice = reauthNoticeOutput(agent);
128
+ if (notice) {
129
+ await emitOutput(notice);
130
+ return;
131
+ }
132
+ }
111
133
  if (agent === "codex") {
112
134
  const snapshot = await daemonRequest(
113
135
  "status_snapshot",