@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.
- package/dist/{chunk-QGVHLP5J.js → chunk-7X7SFOPJ.js} +54 -5
- package/dist/{chunk-I3HIPAIR.js → chunk-CPX3BUOC.js} +7 -4
- package/dist/chunk-G5D3FF3D.js +853 -0
- package/dist/{chunk-S4LFRVGD.js → chunk-HIMWM526.js} +1 -1
- package/dist/{chunk-R4CUX6FY.js → chunk-KHZNYEUV.js} +1 -1
- package/dist/{chunk-KBFFVYBC.js → chunk-LNU44OJV.js} +3 -5
- package/dist/{chunk-LOGW54W5.js → chunk-MCPHIB4W.js} +18 -7
- package/dist/{chunk-DA4BMHEB.js → chunk-VYGY3OOX.js} +2 -2
- package/dist/{chunk-YITJ2H3B.js → chunk-WS3MCNFD.js} +1 -1
- package/dist/daemon/server.js +343 -44
- package/dist/hooks/post-commit.js +30 -6
- package/dist/hooks/post-tool-use.js +32 -20
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +11 -3
- package/dist/hooks/prim-hook.js +7 -7
- package/dist/hooks/session-start.js +15 -14
- package/dist/index.js +236 -139
- package/dist/statusline-main.js +50 -40
- package/package.json +1 -1
- package/dist/chunk-GF5QII7T.js +0 -411
package/dist/daemon/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/daemon/server.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
5
5
|
import { createServer } from "net";
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { join as
|
|
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((
|
|
113
|
+
await new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
throwIfAborted(signal);
|
|
117
|
-
await new Promise((
|
|
117
|
+
await new Promise((resolve3, reject) => {
|
|
118
118
|
const timer = setTimeout(() => {
|
|
119
119
|
signal.removeEventListener("abort", onAbort);
|
|
120
|
-
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
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/
|
|
865
|
-
import {
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
962
|
-
import { homedir as
|
|
963
|
-
import { dirname as
|
|
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 =
|
|
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 =
|
|
1010
|
-
if (!
|
|
1211
|
+
const dir = dirname6(path);
|
|
1212
|
+
if (!existsSync5(dir)) {
|
|
1011
1213
|
mkdirSync4(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
|
|
1012
1214
|
}
|
|
1013
|
-
const 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
|
|
1226
|
+
existsSync as existsSync6,
|
|
1025
1227
|
mkdirSync as mkdirSync5,
|
|
1026
|
-
readFileSync as
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
1266
|
+
return readLockRecord(join8(configDir, "daemon.lock", "owner.json"));
|
|
1065
1267
|
}
|
|
1066
1268
|
function readDaemonPid(configDir) {
|
|
1067
|
-
const path =
|
|
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 =
|
|
1083
|
-
const ownerPath2 =
|
|
1084
|
-
const pidPath =
|
|
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 (!
|
|
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 =
|
|
1183
|
-
var SOCK_PATH2 =
|
|
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(
|
|
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 (
|
|
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
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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 =
|
|
1601
|
-
|
|
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 =
|
|
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-
|
|
5
|
+
} from "../chunk-7X7SFOPJ.js";
|
|
6
6
|
import {
|
|
7
7
|
appendMove,
|
|
8
8
|
resolveOrg
|
|
9
|
-
} from "../chunk-
|
|
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-
|
|
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
|
};
|