@primitive.ai/prim 0.1.0-alpha.62 → 0.1.0-alpha.64

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,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/daemon/server.ts
4
- import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
4
+ import { existsSync as existsSync7, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
5
5
  import { createServer } from "net";
6
- import { homedir as homedir5 } from "os";
7
- import { join as join8 } from "path";
6
+ import { homedir as homedir6 } from "os";
7
+ import { join as join9 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
10
10
  // src/client.ts
@@ -110,14 +110,14 @@ function throwIfAborted(signal) {
110
110
  }
111
111
  async function defaultSleep(ms, signal) {
112
112
  if (!signal) {
113
- await new Promise((resolve2) => setTimeout(resolve2, ms));
113
+ await new Promise((resolve3) => setTimeout(resolve3, ms));
114
114
  return;
115
115
  }
116
116
  throwIfAborted(signal);
117
- await new Promise((resolve2, reject) => {
117
+ await new Promise((resolve3, reject) => {
118
118
  const timer = setTimeout(() => {
119
119
  signal.removeEventListener("abort", onAbort);
120
- resolve2();
120
+ resolve3();
121
121
  }, ms);
122
122
  const onAbort = () => {
123
123
  clearTimeout(timer);
@@ -164,10 +164,10 @@ var CREDENTIAL_FILE_MODE = 384;
164
164
  var REFRESH_THRESHOLD_MS = 6e4;
165
165
  var DEFAULT_API_URL = "https://api.getprimitive.ai";
166
166
  var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
167
- function loadEnvFile() {
167
+ function loadEnvFile(cwd = process.cwd()) {
168
168
  const envVars = {};
169
169
  for (const file of [".env.local", ".env"]) {
170
- const filePath = resolve(process.cwd(), file);
170
+ const filePath = resolve(cwd, file);
171
171
  if (!existsSync2(filePath)) continue;
172
172
  const content = readFileSync2(filePath, "utf-8");
173
173
  for (const line of content.split("\n")) {
@@ -205,8 +205,11 @@ function resolveAuthCredential() {
205
205
  return envToken ? { token: envToken, source: "env_file" } : void 0;
206
206
  }
207
207
  function getSiteUrl() {
208
- if (process.env.PRIM_API_URL) return process.env.PRIM_API_URL;
209
- return loadEnvFile().PRIM_API_URL ?? DEFAULT_API_URL;
208
+ return getSiteUrlForCwd(process.cwd(), process.env.PRIM_API_URL);
209
+ }
210
+ function getSiteUrlForCwd(cwd, primApiUrl) {
211
+ if (primApiUrl) return primApiUrl;
212
+ return loadEnvFile(cwd).PRIM_API_URL ?? DEFAULT_API_URL;
210
213
  }
211
214
  function getJwtExpiry(token) {
212
215
  const parts = token.split(".");
@@ -861,18 +864,217 @@ function flush() {
861
864
  return attempt;
862
865
  }
863
866
 
864
- // src/daemon/client.ts
865
- import { createConnection } from "net";
867
+ // src/lib/activation.ts
868
+ import { execFileSync as execFileSync2 } from "child_process";
869
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
866
870
  import { homedir as homedir3 } from "os";
867
871
  import { join as join5 } from "path";
868
- var SOCK_PATH = join5(homedir3(), ".config", "prim", "sock");
872
+
873
+ // src/lib/git.ts
874
+ import { execFileSync } from "child_process";
875
+ import { createHash as createHash2 } from "crypto";
876
+ import { lstatSync, realpathSync } from "fs";
877
+ import { basename, dirname as dirname5, isAbsolute, relative, resolve as resolve2, sep } from "path";
878
+ var GIT_TIMEOUT_MS = 1e3;
879
+ function gitToplevel(cwd) {
880
+ try {
881
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
882
+ cwd,
883
+ encoding: "utf-8",
884
+ stdio: ["ignore", "pipe", "ignore"],
885
+ timeout: GIT_TIMEOUT_MS
886
+ }).trim();
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+
892
+ // src/lib/activation.ts
893
+ var PRIM_ACTIVE_KEY = "prim.active";
894
+ var GIT_TIMEOUT_MS2 = 1e3;
895
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
896
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
897
+ function repoActiveFlag(cwd) {
898
+ try {
899
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
900
+ cwd,
901
+ encoding: "utf-8",
902
+ stdio: ["ignore", "pipe", "ignore"],
903
+ timeout: GIT_TIMEOUT_MS2
904
+ }).trim();
905
+ if (value === "true") return "true";
906
+ if (value === "false") return "false";
907
+ return void 0;
908
+ } catch {
909
+ return void 0;
910
+ }
911
+ }
912
+ function hasProjectPrimInstall(cwd) {
913
+ const root = gitToplevel(cwd);
914
+ if (root === null || root === homedir3()) return false;
915
+ for (const rel of PROJECT_INSTALL_FILES) {
916
+ const path = join5(root, rel);
917
+ if (existsSync4(path)) {
918
+ try {
919
+ const content = readFileSync4(path, "utf-8");
920
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
921
+ } catch {
922
+ }
923
+ }
924
+ }
925
+ return false;
926
+ }
927
+ function isRepoActiveForCapture(cwd) {
928
+ const flag = repoActiveFlag(cwd);
929
+ if (flag === "true") return true;
930
+ if (flag === "false") return false;
931
+ return hasProjectPrimInstall(cwd);
932
+ }
933
+ function decisionIngestionStatus(cwd) {
934
+ return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
935
+ }
936
+
937
+ // src/lib/ansi.ts
938
+ function stripControlChars(text) {
939
+ let out = "";
940
+ for (const ch of text) {
941
+ const code = ch.codePointAt(0) ?? 0;
942
+ if (code > 31 && !(code >= 127 && code <= 159)) {
943
+ out += ch;
944
+ }
945
+ }
946
+ return out;
947
+ }
948
+
949
+ // src/lib/presence.ts
950
+ var DECISION_ORIGIN = "https://app.getprimitive.ai";
951
+ function validDecisionUrl(value) {
952
+ if (typeof value !== "string" || stripControlChars(value) !== value) {
953
+ return void 0;
954
+ }
955
+ try {
956
+ const url = new URL(value);
957
+ if (url.origin !== DECISION_ORIGIN || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || !/^\/decisions\/[^/]+$/u.test(url.pathname)) {
958
+ return void 0;
959
+ }
960
+ return url.href;
961
+ } catch {
962
+ return void 0;
963
+ }
964
+ }
965
+ function decisionLink(label, decisionUrl) {
966
+ const url = validDecisionUrl(decisionUrl);
967
+ return url ? `\x1B]8;;${url}\x07\x1B[34;4m${label}\x1B[0m\x1B]8;;\x07` : label;
968
+ }
969
+ function formatLabeled(labels, cap) {
970
+ if (labels === void 0) {
971
+ return "\u2014";
972
+ }
973
+ if (labels.length === 0) {
974
+ return "just you";
975
+ }
976
+ if (labels.length <= cap) {
977
+ return labels.join(", ");
978
+ }
979
+ return `${labels.slice(0, cap).join(", ")} +${String(labels.length - cap)}`;
980
+ }
981
+ function formatTeammates(names, cap) {
982
+ return formatLabeled(names?.map(stripControlChars), cap);
983
+ }
984
+ function formatTeammatesWithArea(teammates, cap) {
985
+ return formatLabeled(
986
+ teammates?.map((t) => {
987
+ const name = stripControlChars(t.name);
988
+ const area = stripControlChars(t.area ?? "").trim();
989
+ const label = area ? `${name} - ${area}` : name;
990
+ return decisionLink(label, t.decisionUrl);
991
+ }),
992
+ cap
993
+ );
994
+ }
995
+
996
+ // src/lib/statusline-render.ts
997
+ var STATUSLINE_NAME_CAP = 3;
998
+ function formatStatusline(version, snapshot, resolveIngestionStatus) {
999
+ if (!snapshot) {
1000
+ return `primitive ${version} (daemon: down)`;
1001
+ }
1002
+ if (snapshot.healthy === false) {
1003
+ if (snapshot.needsReauth) {
1004
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
1005
+ }
1006
+ if (snapshot.ingestion?.healthy === false) {
1007
+ const pending = snapshot.ingestion.pendingCount;
1008
+ const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
1009
+ return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
1010
+ }
1011
+ if (snapshot.heartbeat?.healthy === false) {
1012
+ return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
1013
+ }
1014
+ return `primitive ${version} (daemon: starting)`;
1015
+ }
1016
+ const ingestionStatus = resolveIngestionStatus();
1017
+ if (snapshot.envMismatch) {
1018
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
1019
+ }
1020
+ if (snapshot.presenceStale) {
1021
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
1022
+ }
1023
+ let team;
1024
+ if (snapshot.onlineTeammates !== void 0) {
1025
+ team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
1026
+ } else if (snapshot.onlineNames !== void 0) {
1027
+ team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
1028
+ } else if (typeof snapshot.onlineCount === "number") {
1029
+ team = `team: ${String(snapshot.onlineCount)} online`;
1030
+ } else {
1031
+ team = "team: \u2014";
1032
+ }
1033
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
1034
+ }
1035
+ var STATUSLINE_INGESTION_CACHE_TTL_MS = 3e4;
1036
+ var STATUSLINE_INGESTION_CACHE_MAX_ENTRIES = 256;
1037
+ var StatuslineIngestionCache = class {
1038
+ constructor(resolve3, now = Date.now, ttlMs = STATUSLINE_INGESTION_CACHE_TTL_MS, maxEntries = STATUSLINE_INGESTION_CACHE_MAX_ENTRIES) {
1039
+ this.resolve = resolve3;
1040
+ this.now = now;
1041
+ this.ttlMs = ttlMs;
1042
+ this.maxEntries = maxEntries;
1043
+ }
1044
+ #entries = /* @__PURE__ */ new Map();
1045
+ get(cwd) {
1046
+ const now = this.now();
1047
+ const cached = this.#entries.get(cwd);
1048
+ if (cached && cached.expiresAt > now) {
1049
+ return cached.value;
1050
+ }
1051
+ const value = this.resolve(cwd);
1052
+ this.#entries.delete(cwd);
1053
+ this.#entries.set(cwd, { expiresAt: now + this.ttlMs, value });
1054
+ while (this.#entries.size > this.maxEntries) {
1055
+ const oldest = this.#entries.keys().next().value;
1056
+ if (oldest === void 0) break;
1057
+ this.#entries.delete(oldest);
1058
+ }
1059
+ return value;
1060
+ }
1061
+ clear() {
1062
+ this.#entries.clear();
1063
+ }
1064
+ };
1065
+
1066
+ // src/daemon/client.ts
1067
+ import { createConnection } from "net";
1068
+ import { homedir as homedir4 } from "os";
1069
+ import { join as join6 } from "path";
1070
+ var SOCK_PATH = join6(homedir4(), ".config", "prim", "sock");
869
1071
  var DEFAULT_TIMEOUT_MS2 = 250;
870
1072
  var nextRequestId = 1;
871
1073
  function daemonRequest(method, params = {}, opts = {}) {
872
1074
  const id = nextRequestId++;
873
1075
  const request2 = { id, method, params };
874
1076
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
875
- return new Promise((resolve2) => {
1077
+ return new Promise((resolve3) => {
876
1078
  let settled = false;
877
1079
  let socket;
878
1080
  let buffer = "";
@@ -887,7 +1089,7 @@ function daemonRequest(method, params = {}, opts = {}) {
887
1089
  } catch {
888
1090
  }
889
1091
  }
890
- resolve2(value);
1092
+ resolve3(value);
891
1093
  };
892
1094
  const timer = setTimeout(() => settle(null), timeoutMs);
893
1095
  timer.unref();
@@ -958,9 +1160,9 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
958
1160
  }
959
1161
 
960
1162
  // src/daemon/health.ts
961
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
962
- import { homedir as homedir4 } from "os";
963
- import { dirname as dirname5, join as join6 } from "path";
1163
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
1164
+ import { homedir as homedir5 } from "os";
1165
+ import { dirname as dirname6, join as join7 } from "path";
964
1166
  var CONFIG_DIR_MODE2 = 448;
965
1167
  var HEALTH_FILE_MODE = 384;
966
1168
  var INGESTION_SLA_MS = 3e4;
@@ -968,7 +1170,7 @@ var HEARTBEAT_FRESH_MS = 9e4;
968
1170
  var HEARTBEAT_RETRY_CAP_MS = 3e4;
969
1171
  var INGESTION_RETRY_BASE_MS = 5e3;
970
1172
  var INGESTION_RETRY_CAP_MS = 15e3;
971
- var DAEMON_HEALTH_PATH = join6(homedir4(), ".config", "prim", "daemon-health.json");
1173
+ var DAEMON_HEALTH_PATH = join7(homedir5(), ".config", "prim", "daemon-health.json");
972
1174
  function createDaemonHealthState(version, pid, startedAt2) {
973
1175
  return {
974
1176
  schemaVersion: 1,
@@ -1006,11 +1208,11 @@ function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
1006
1208
  return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
1007
1209
  }
1008
1210
  function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
1009
- const dir = dirname5(path);
1010
- if (!existsSync4(dir)) {
1211
+ const dir = dirname6(path);
1212
+ if (!existsSync5(dir)) {
1011
1213
  mkdirSync4(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
1012
1214
  }
1013
- const tmp = join6(dir, `.${process.pid}.daemon-health.tmp`);
1215
+ const tmp = join7(dir, `.${process.pid}.daemon-health.tmp`);
1014
1216
  writeFileSync3(tmp, `${JSON.stringify(state, null, 2)}
1015
1217
  `, { mode: HEALTH_FILE_MODE });
1016
1218
  chmodSync2(tmp, HEALTH_FILE_MODE);
@@ -1021,22 +1223,22 @@ function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
1021
1223
  import { randomUUID } from "crypto";
1022
1224
  import {
1023
1225
  chmodSync as chmodSync3,
1024
- existsSync as existsSync5,
1226
+ existsSync as existsSync6,
1025
1227
  mkdirSync as mkdirSync5,
1026
- readFileSync as readFileSync4,
1228
+ readFileSync as readFileSync5,
1027
1229
  renameSync as renameSync4,
1028
1230
  rmSync as rmSync3,
1029
1231
  statSync as statSync2,
1030
1232
  unlinkSync as unlinkSync2,
1031
1233
  writeFileSync as writeFileSync4
1032
1234
  } from "fs";
1033
- import { join as join7 } from "path";
1235
+ import { join as join8 } from "path";
1034
1236
  var DIR_MODE = 448;
1035
1237
  var FILE_MODE2 = 384;
1036
1238
  var DAEMON_STARTUP_GRACE_MS = 5e3;
1037
1239
  function readLockRecord(path) {
1038
1240
  try {
1039
- const value = JSON.parse(readFileSync4(path, "utf-8"));
1241
+ const value = JSON.parse(readFileSync5(path, "utf-8"));
1040
1242
  if (Number.isInteger(value.pid) && typeof value.instanceId === "string" && typeof value.startedAt === "number") {
1041
1243
  return value;
1042
1244
  }
@@ -1046,7 +1248,7 @@ function readLockRecord(path) {
1046
1248
  }
1047
1249
  function numericPid(path) {
1048
1250
  try {
1049
- const pid = Number(readFileSync4(path, "utf-8").trim());
1251
+ const pid = Number(readFileSync5(path, "utf-8").trim());
1050
1252
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
1051
1253
  } catch {
1052
1254
  return;
@@ -1061,10 +1263,10 @@ function daemonProcessIsAlive(pid) {
1061
1263
  }
1062
1264
  }
1063
1265
  function readDaemonOwner(configDir) {
1064
- return readLockRecord(join7(configDir, "daemon.lock", "owner.json"));
1266
+ return readLockRecord(join8(configDir, "daemon.lock", "owner.json"));
1065
1267
  }
1066
1268
  function readDaemonPid(configDir) {
1067
- const path = join7(configDir, "daemon.pid");
1269
+ const path = join8(configDir, "daemon.pid");
1068
1270
  const pid = numericPid(path);
1069
1271
  if (pid === void 0) {
1070
1272
  return;
@@ -1079,11 +1281,11 @@ function acquireDaemonOwnership(configDir, opts = {}) {
1079
1281
  const pid = opts.pid ?? process.pid;
1080
1282
  const now = opts.now ?? Date.now();
1081
1283
  const isAlive = opts.isAlive ?? daemonProcessIsAlive;
1082
- const lockDir = join7(configDir, "daemon.lock");
1083
- const ownerPath2 = join7(lockDir, "owner.json");
1084
- const pidPath = join7(configDir, "daemon.pid");
1284
+ const lockDir = join8(configDir, "daemon.lock");
1285
+ const ownerPath2 = join8(lockDir, "owner.json");
1286
+ const pidPath = join8(configDir, "daemon.pid");
1085
1287
  const instanceId = randomUUID();
1086
- if (!existsSync5(configDir)) {
1288
+ if (!existsSync6(configDir)) {
1087
1289
  mkdirSync5(configDir, { recursive: true, mode: DIR_MODE });
1088
1290
  }
1089
1291
  for (; ; ) {
@@ -1178,9 +1380,48 @@ function releaseDaemonOwnership(ownership2, socketPath) {
1178
1380
  return true;
1179
1381
  }
1180
1382
 
1383
+ // src/daemon/statusline-protocol.ts
1384
+ import { isAbsolute as isAbsolute2 } from "path";
1385
+ var STATUSLINE_PROTOCOL_PREFIX = Buffer.from("prim-statusline-v1\0", "utf8");
1386
+ var STATUSLINE_REQUEST_MAX_BYTES = 64 * 1024;
1387
+ var utf8Decoder = new TextDecoder("utf-8", { fatal: true });
1388
+ function isStatuslineRequestPrefix(buffer) {
1389
+ const compared = Math.min(buffer.length, STATUSLINE_PROTOCOL_PREFIX.length);
1390
+ return buffer.subarray(0, compared).equals(STATUSLINE_PROTOCOL_PREFIX.subarray(0, compared));
1391
+ }
1392
+ function parseStatuslineRequest(buffer) {
1393
+ if (buffer.length > STATUSLINE_REQUEST_MAX_BYTES || !isStatuslineRequestPrefix(buffer)) {
1394
+ return { state: "invalid" };
1395
+ }
1396
+ if (buffer.length < STATUSLINE_PROTOCOL_PREFIX.length) {
1397
+ return { state: "incomplete" };
1398
+ }
1399
+ const cwdEnd = buffer.indexOf(0, STATUSLINE_PROTOCOL_PREFIX.length);
1400
+ if (cwdEnd === -1) {
1401
+ return { state: "incomplete" };
1402
+ }
1403
+ const apiUrlEnd = buffer.indexOf(0, cwdEnd + 1);
1404
+ if (apiUrlEnd === -1) {
1405
+ return { state: "incomplete" };
1406
+ }
1407
+ if (apiUrlEnd !== buffer.length - 1) {
1408
+ return { state: "invalid" };
1409
+ }
1410
+ try {
1411
+ const cwd = utf8Decoder.decode(buffer.subarray(STATUSLINE_PROTOCOL_PREFIX.length, cwdEnd));
1412
+ const primApiUrl = utf8Decoder.decode(buffer.subarray(cwdEnd + 1, apiUrlEnd));
1413
+ if (!cwd || !isAbsolute2(cwd)) {
1414
+ return { state: "invalid" };
1415
+ }
1416
+ return { state: "complete", request: { cwd, primApiUrl } };
1417
+ } catch {
1418
+ return { state: "invalid" };
1419
+ }
1420
+ }
1421
+
1181
1422
  // src/daemon/server.ts
1182
- var CONFIG_DIR = join8(homedir5(), ".config", "prim");
1183
- var SOCK_PATH2 = join8(CONFIG_DIR, "sock");
1423
+ var CONFIG_DIR = join9(homedir6(), ".config", "prim");
1424
+ var SOCK_PATH2 = join9(CONFIG_DIR, "sock");
1184
1425
  var HEARTBEAT_INTERVAL_MS = 3e4;
1185
1426
  var INGESTION_POLL_INTERVAL_MS = 15e3;
1186
1427
  var TOKEN_CHECK_INTERVAL_MS = 6e4;
@@ -1193,6 +1434,7 @@ var startedAt = Date.now();
1193
1434
  var client = getClient();
1194
1435
  var runtimeVersion = resolveRuntimeVersion();
1195
1436
  var daemonHealth = createDaemonHealthState(runtimeVersion, process.pid, startedAt);
1437
+ var statuslineIngestionCache = new StatuslineIngestionCache(decisionIngestionStatus);
1196
1438
  var activeSessionId = process.env.PRIM_DAEMON_SESSION_ID ?? `daemon-${process.pid}`;
1197
1439
  var lastHeartbeatAt;
1198
1440
  var lastOnlineCount;
@@ -1214,7 +1456,7 @@ function resolveRuntimeVersion() {
1214
1456
  }
1215
1457
  try {
1216
1458
  const here = fileURLToPath(new URL(".", import.meta.url));
1217
- const pkg = JSON.parse(readFileSync5(join8(here, "..", "..", "package.json"), "utf-8"));
1459
+ const pkg = JSON.parse(readFileSync6(join9(here, "..", "..", "package.json"), "utf-8"));
1218
1460
  return pkg.version ?? "unknown";
1219
1461
  } catch {
1220
1462
  return "unknown";
@@ -1285,7 +1527,7 @@ async function takeOwnership() {
1285
1527
  (pid) => pid !== void 0 && daemonProcessIsAlive(pid)
1286
1528
  )
1287
1529
  );
1288
- if (existsSync6(SOCK_PATH2) || candidatePids.size > 0) {
1530
+ if (existsSync7(SOCK_PATH2) || candidatePids.size > 0) {
1289
1531
  const snapshot = await daemonRequest(
1290
1532
  "status_snapshot",
1291
1533
  {},
@@ -1566,6 +1808,7 @@ async function dispatchRequest(req) {
1566
1808
  return { id, ok: true, result };
1567
1809
  }
1568
1810
  case "session_start": {
1811
+ statuslineIngestionCache.clear();
1569
1812
  const sid = req.params?.sessionId;
1570
1813
  if (typeof sid === "string" && sid.length > 0) {
1571
1814
  activeSessionId = sid;
@@ -1578,6 +1821,9 @@ async function dispatchRequest(req) {
1578
1821
  }
1579
1822
  case "status_snapshot":
1580
1823
  return { id, ok: true, result: handleStatusSnapshot(req.params ?? {}) };
1824
+ case "statusline_invalidate":
1825
+ statuslineIngestionCache.clear();
1826
+ return { id, ok: true, result: { ack: true } };
1581
1827
  case "ping":
1582
1828
  return { id, ok: true, result: { pong: true } };
1583
1829
  default:
@@ -1592,13 +1838,15 @@ async function dispatchRequest(req) {
1592
1838
  }
1593
1839
  }
1594
1840
  function handleConnection(conn) {
1595
- let buffer = "";
1596
- conn.on("data", (chunk) => {
1597
- buffer += chunk.toString("utf-8");
1598
- let newlineIdx = buffer.indexOf("\n");
1841
+ let mode = "undecided";
1842
+ let rawBuffer = Buffer.alloc(0);
1843
+ let jsonBuffer = "";
1844
+ const dispatchJson = (chunk) => {
1845
+ jsonBuffer += chunk;
1846
+ let newlineIdx = jsonBuffer.indexOf("\n");
1599
1847
  while (newlineIdx !== -1) {
1600
- const line = buffer.slice(0, newlineIdx);
1601
- buffer = buffer.slice(newlineIdx + 1);
1848
+ const line = jsonBuffer.slice(0, newlineIdx);
1849
+ jsonBuffer = jsonBuffer.slice(newlineIdx + 1);
1602
1850
  if (line.length > 0) {
1603
1851
  try {
1604
1852
  const req = JSON.parse(line);
@@ -1613,8 +1861,59 @@ function handleConnection(conn) {
1613
1861
  } catch {
1614
1862
  }
1615
1863
  }
1616
- newlineIdx = buffer.indexOf("\n");
1864
+ newlineIdx = jsonBuffer.indexOf("\n");
1865
+ }
1866
+ };
1867
+ const dispatchStatusline = () => {
1868
+ const parsed = parseStatuslineRequest(rawBuffer);
1869
+ if (parsed.state === "incomplete") {
1870
+ return;
1871
+ }
1872
+ if (parsed.state === "invalid") {
1873
+ conn.end();
1874
+ return;
1875
+ }
1876
+ try {
1877
+ const callerEnv = getSiteUrlForCwd(
1878
+ parsed.request.cwd,
1879
+ parsed.request.primApiUrl || void 0
1880
+ );
1881
+ const snapshot = handleStatusSnapshot({ callerEnv });
1882
+ const line = formatStatusline(
1883
+ runtimeVersion,
1884
+ snapshot,
1885
+ () => statuslineIngestionCache.get(parsed.request.cwd)
1886
+ );
1887
+ conn.end(line);
1888
+ } catch {
1889
+ conn.end();
1890
+ }
1891
+ };
1892
+ conn.on("data", (chunk) => {
1893
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1894
+ if (mode === "json") {
1895
+ dispatchJson(bytes.toString("utf8"));
1896
+ return;
1897
+ }
1898
+ rawBuffer = Buffer.concat([rawBuffer, bytes]);
1899
+ if (mode === "statusline") {
1900
+ if (rawBuffer.length > STATUSLINE_REQUEST_MAX_BYTES) {
1901
+ conn.end();
1902
+ } else {
1903
+ dispatchStatusline();
1904
+ }
1905
+ return;
1906
+ }
1907
+ if (isStatuslineRequestPrefix(rawBuffer)) {
1908
+ if (rawBuffer.length >= STATUSLINE_PROTOCOL_PREFIX.length) {
1909
+ mode = "statusline";
1910
+ dispatchStatusline();
1911
+ }
1912
+ return;
1617
1913
  }
1914
+ mode = "json";
1915
+ dispatchJson(rawBuffer.toString("utf8"));
1916
+ rawBuffer = Buffer.alloc(0);
1618
1917
  });
1619
1918
  conn.on("error", () => {
1620
1919
  });
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  commitAttributionFromEnvironment,
4
4
  toCommitMove
5
- } from "../chunk-QGVHLP5J.js";
5
+ } from "../chunk-7X7SFOPJ.js";
6
6
  import {
7
7
  appendMove,
8
8
  resolveOrg
9
- } from "../chunk-S4LFRVGD.js";
9
+ } from "../chunk-HIMWM526.js";
10
10
  import {
11
11
  getOrCreateWorkspaceId
12
12
  } from "../chunk-UVIWZWM6.js";
@@ -16,16 +16,34 @@ import {
16
16
  repoSyncId,
17
17
  resolveRepositoryContext
18
18
  } from "../chunk-XTNMSS4C.js";
19
- import "../chunk-I3HIPAIR.js";
19
+ import "../chunk-CPX3BUOC.js";
20
20
 
21
21
  // src/hooks/post-commit.ts
22
22
  import { execFileSync, spawn } from "child_process";
23
- import { readFileSync } from "fs";
24
- import { dirname, join } from "path";
23
+ import { lstatSync, readFileSync } from "fs";
24
+ import { basename, dirname, isAbsolute, join } from "path";
25
25
  import { fileURLToPath } from "url";
26
26
  var here = dirname(fileURLToPath(import.meta.url));
27
27
  var GIT_TIMEOUT_MS = 1e3;
28
28
  var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
29
+ var OBSERVATION_MARKER_RE = /^prim-post-commit-observed\.[A-Za-z0-9]+$/u;
30
+ var MAX_OBSERVATION_MARKER_AGE_MS = 10 * 60 * 1e3;
31
+ var MAX_OBSERVATION_MARKER_FUTURE_SKEW_MS = 5e3;
32
+ function capturedAtFromLauncher(env = process.env, now = Date.now()) {
33
+ const path = env.PRIM_COMMIT_OBSERVED_FILE;
34
+ if (!path || !isAbsolute(path) || !OBSERVATION_MARKER_RE.test(basename(path))) return;
35
+ try {
36
+ const stat = lstatSync(path);
37
+ const capturedAt = Math.trunc(stat.mtimeMs);
38
+ const uid = process.getuid?.();
39
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== 0 || stat.nlink !== 1 || (stat.mode & 63) !== 0 || uid !== void 0 && stat.uid !== uid || !Number.isSafeInteger(capturedAt) || capturedAt <= 0 || capturedAt < now - MAX_OBSERVATION_MARKER_AGE_MS || capturedAt > now + MAX_OBSERVATION_MARKER_FUTURE_SKEW_MS) {
40
+ return;
41
+ }
42
+ return capturedAt;
43
+ } catch {
44
+ return;
45
+ }
46
+ }
29
47
  function gitText(args) {
30
48
  try {
31
49
  return execFileSync("git", args, {
@@ -117,6 +135,10 @@ function runPostCommit() {
117
135
  if (!isRepoActiveForCapture(cwd)) {
118
136
  return;
119
137
  }
138
+ const capturedAt = capturedAtFromLauncher();
139
+ if (process.env.PRIM_COMMIT_OBSERVED_FILE !== void 0 && capturedAt === void 0) {
140
+ return;
141
+ }
120
142
  const commit = readCommit();
121
143
  if (commit) {
122
144
  const repository = resolveRepositoryContext(cwd);
@@ -128,7 +150,8 @@ function runPostCommit() {
128
150
  repoFullName: githubRepositoryFullName(cwd) ?? void 0,
129
151
  repoSyncId: repoSyncId(cwd),
130
152
  workspaceId,
131
- attribution
153
+ attribution,
154
+ ...capturedAt === void 0 ? {} : { capturedAt }
132
155
  });
133
156
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd });
134
157
  appendMove(move, orgId);
@@ -148,5 +171,6 @@ if (!process.env.VITEST) {
148
171
  process.exit(0);
149
172
  }
150
173
  export {
174
+ capturedAtFromLauncher,
151
175
  runPostCommit
152
176
  };