@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.
- package/README.md +37 -4
- package/SKILL.md +39 -10
- package/dist/{chunk-5K725K35.js → chunk-BOTKPLTI.js} +1 -1
- package/dist/{chunk-TPROV45L.js → chunk-DWDEQRWN.js} +12 -1
- package/dist/{chunk-F7O7V6ZM.js → chunk-HSZPTVKU.js} +12 -88
- package/dist/chunk-R5ZJRSLV.js +82 -0
- package/dist/{chunk-WRPKAPVE.js → chunk-VDLAAUWV.js} +1 -1
- package/dist/{chunk-CK75QBCB.js → chunk-WELBNODJ.js} +1 -1
- package/dist/daemon/server.js +46 -18
- package/dist/hooks/post-commit.js +23 -9
- package/dist/hooks/post-tool-use.js +6 -4
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +5 -3
- package/dist/hooks/prim-hook.js +7 -5
- package/dist/hooks/session-start.js +27 -5
- package/dist/index.js +131 -82
- package/dist/statusline-main.js +76 -9
- package/package.json +1 -1
package/dist/daemon/server.js
CHANGED
|
@@ -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
|
|
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,
|
|
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 =
|
|
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
|
|
844
|
-
var SOCK_PATH =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
1064
|
+
return readLockRecord(join7(configDir, "daemon.lock", "owner.json"));
|
|
1041
1065
|
}
|
|
1042
1066
|
function readDaemonPid(configDir) {
|
|
1043
|
-
const path =
|
|
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 =
|
|
1059
|
-
const ownerPath2 =
|
|
1060
|
-
const pidPath =
|
|
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 =
|
|
1159
|
-
var SOCK_PATH2 =
|
|
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(
|
|
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-
|
|
9
|
-
import
|
|
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
|
-
|
|
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
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
84
|
+
export {
|
|
85
|
+
runPostCommit
|
|
86
|
+
};
|
|
@@ -13,17 +13,19 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
appendMove,
|
|
15
15
|
resolveOrg
|
|
16
|
-
} from "../chunk-
|
|
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-
|
|
22
|
+
} from "../chunk-HSZPTVKU.js";
|
|
23
|
+
import {
|
|
24
|
+
isRepoActiveForCapture
|
|
25
|
+
} from "../chunk-R5ZJRSLV.js";
|
|
24
26
|
import {
|
|
25
27
|
getClient
|
|
26
|
-
} from "../chunk-
|
|
28
|
+
} from "../chunk-DWDEQRWN.js";
|
|
27
29
|
import {
|
|
28
30
|
normalizeEnvelope,
|
|
29
31
|
parseAgent
|
package/dist/hooks/pre-commit.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
isRepoActiveForCapture,
|
|
4
3
|
warmBinCache
|
|
5
|
-
} from "../chunk-
|
|
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-
|
|
11
|
+
} from "../chunk-DWDEQRWN.js";
|
|
10
12
|
import {
|
|
11
13
|
daemonRequest
|
|
12
14
|
} from "../chunk-UTKQTZHL.js";
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
appendMove,
|
|
11
11
|
resolveOrg
|
|
12
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
30
|
-
import
|
|
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-
|
|
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-
|
|
18
|
+
} from "../chunk-HSZPTVKU.js";
|
|
20
19
|
import {
|
|
21
|
-
|
|
22
|
-
} from "../chunk-
|
|
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",
|