@primitive.ai/prim 0.1.0-alpha.67 → 0.1.0-alpha.68
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-I5BHJQH2.js +329 -0
- package/dist/chunk-K3AYW6VZ.js +300 -0
- package/dist/chunk-XGADQPCI.js +26 -0
- package/dist/daemon/server.js +115 -24
- package/dist/hooks/post-tool-use.js +49 -19
- package/dist/hooks/pre-tool-use.js +63 -24
- package/dist/hooks/prim-hook.js +64 -2
- package/dist/hooks/session-end.js +3 -3
- package/dist/hooks/session-start.js +25 -17
- package/dist/index.js +13 -100
- package/dist/statusline-main.js +16 -12
- package/package.json +1 -1
- package/dist/chunk-CZ24W5QU.js +0 -123
- package/dist/chunk-JQFN6LBO.js +0 -110
package/dist/daemon/server.js
CHANGED
|
@@ -966,9 +966,12 @@ function validDecisionUrl(value) {
|
|
|
966
966
|
return void 0;
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
|
-
function decisionLink(label, decisionUrl) {
|
|
969
|
+
function decisionLink(label, decisionUrl, plain) {
|
|
970
970
|
const url = validDecisionUrl(decisionUrl);
|
|
971
|
-
|
|
971
|
+
if (plain || !url) {
|
|
972
|
+
return label;
|
|
973
|
+
}
|
|
974
|
+
return `\x1B]8;;${url}\x07\x1B[34;4m${label}\x1B[0m\x1B]8;;\x07`;
|
|
972
975
|
}
|
|
973
976
|
function formatLabeled(labels, cap) {
|
|
974
977
|
if (labels === void 0) {
|
|
@@ -985,13 +988,13 @@ function formatLabeled(labels, cap) {
|
|
|
985
988
|
function formatTeammates(names, cap) {
|
|
986
989
|
return formatLabeled(names?.map(stripControlChars), cap);
|
|
987
990
|
}
|
|
988
|
-
function formatTeammatesWithArea(teammates, cap) {
|
|
991
|
+
function formatTeammatesWithArea(teammates, cap, plainLinks = false) {
|
|
989
992
|
return formatLabeled(
|
|
990
993
|
teammates?.map((t) => {
|
|
991
994
|
const name = stripControlChars(t.name);
|
|
992
995
|
const area = stripControlChars(t.area ?? "").trim();
|
|
993
996
|
const label = area ? `${name} - ${area}` : name;
|
|
994
|
-
return decisionLink(label, t.decisionUrl);
|
|
997
|
+
return decisionLink(label, t.decisionUrl, plainLinks);
|
|
995
998
|
}),
|
|
996
999
|
cap
|
|
997
1000
|
);
|
|
@@ -999,25 +1002,26 @@ function formatTeammatesWithArea(teammates, cap) {
|
|
|
999
1002
|
|
|
1000
1003
|
// src/lib/statusline-render.ts
|
|
1001
1004
|
var STATUSLINE_NAME_CAP = 3;
|
|
1002
|
-
function formatStatusline(version, snapshot, resolveIngestionStatus) {
|
|
1005
|
+
function formatStatusline(version, snapshot, resolveIngestionStatus, options = {}) {
|
|
1006
|
+
const ingestionSuffix = (status) => status === void 0 ? "" : ` \xB7 Decision ingestion ${status}`;
|
|
1007
|
+
const ingestionStatus = snapshot === null || snapshot.healthy === false ? options.includeIngestionWhenUnavailable ? resolveIngestionStatus() : void 0 : resolveIngestionStatus();
|
|
1003
1008
|
if (!snapshot) {
|
|
1004
|
-
return `primitive ${version} (daemon: down)`;
|
|
1009
|
+
return `primitive ${version} (daemon: down${ingestionSuffix(ingestionStatus)})`;
|
|
1005
1010
|
}
|
|
1006
1011
|
if (snapshot.healthy === false) {
|
|
1007
1012
|
if (snapshot.needsReauth) {
|
|
1008
|
-
return `primitive ${version} (daemon: paused \xB7 run \`prim auth login
|
|
1013
|
+
return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`${ingestionSuffix(ingestionStatus)})`;
|
|
1009
1014
|
}
|
|
1010
1015
|
if (snapshot.ingestion?.healthy === false) {
|
|
1011
1016
|
const pending = snapshot.ingestion.pendingCount;
|
|
1012
1017
|
const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
|
|
1013
|
-
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
|
|
1018
|
+
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${ingestionSuffix(ingestionStatus)})`;
|
|
1014
1019
|
}
|
|
1015
1020
|
if (snapshot.heartbeat?.healthy === false) {
|
|
1016
|
-
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
|
|
1021
|
+
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${ingestionSuffix(ingestionStatus)})`;
|
|
1017
1022
|
}
|
|
1018
|
-
return `primitive ${version} (daemon: starting)`;
|
|
1023
|
+
return `primitive ${version} (daemon: starting${ingestionSuffix(ingestionStatus)})`;
|
|
1019
1024
|
}
|
|
1020
|
-
const ingestionStatus = resolveIngestionStatus();
|
|
1021
1025
|
if (snapshot.envMismatch) {
|
|
1022
1026
|
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
|
|
1023
1027
|
}
|
|
@@ -1026,7 +1030,7 @@ function formatStatusline(version, snapshot, resolveIngestionStatus) {
|
|
|
1026
1030
|
}
|
|
1027
1031
|
let team;
|
|
1028
1032
|
if (snapshot.onlineTeammates !== void 0) {
|
|
1029
|
-
team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
|
|
1033
|
+
team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP, options.plainLinks === true)}`;
|
|
1030
1034
|
} else if (snapshot.onlineNames !== void 0) {
|
|
1031
1035
|
team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
|
|
1032
1036
|
} else if (typeof snapshot.onlineCount === "number") {
|
|
@@ -1147,6 +1151,56 @@ function daemonRequest(method, params = {}, opts = {}) {
|
|
|
1147
1151
|
});
|
|
1148
1152
|
}
|
|
1149
1153
|
|
|
1154
|
+
// src/daemon/decision-digest-cache.ts
|
|
1155
|
+
var DECISION_DIGEST_CACHE_WARMING = "daemon Decision cache warming";
|
|
1156
|
+
var DECISION_DIGEST_CACHE_LIMIT = 100;
|
|
1157
|
+
var DECISION_DIGEST_CACHE_WINDOW = "24h";
|
|
1158
|
+
var DECISION_DIGEST_CACHE_PATH = `/api/cli/decisions/recent?limit=${String(DECISION_DIGEST_CACHE_LIMIT)}&since=${DECISION_DIGEST_CACHE_WINDOW}`;
|
|
1159
|
+
function errorMessage(error) {
|
|
1160
|
+
return error instanceof Error ? error.message : String(error);
|
|
1161
|
+
}
|
|
1162
|
+
var DecisionDigestCache = class {
|
|
1163
|
+
constructor(load, now = Date.now) {
|
|
1164
|
+
this.load = load;
|
|
1165
|
+
this.now = now;
|
|
1166
|
+
}
|
|
1167
|
+
snapshot = {
|
|
1168
|
+
decisions: [],
|
|
1169
|
+
unavailable: DECISION_DIGEST_CACHE_WARMING
|
|
1170
|
+
};
|
|
1171
|
+
refreshInFlight;
|
|
1172
|
+
read() {
|
|
1173
|
+
return { ...this.snapshot, decisions: [...this.snapshot.decisions] };
|
|
1174
|
+
}
|
|
1175
|
+
refresh() {
|
|
1176
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
1177
|
+
this.refreshInFlight = this.performRefresh().finally(() => {
|
|
1178
|
+
this.refreshInFlight = void 0;
|
|
1179
|
+
});
|
|
1180
|
+
return this.refreshInFlight;
|
|
1181
|
+
}
|
|
1182
|
+
async performRefresh() {
|
|
1183
|
+
try {
|
|
1184
|
+
const value = await this.load();
|
|
1185
|
+
if (typeof value !== "object" || value === null || !Array.isArray(value.decisions)) {
|
|
1186
|
+
throw new Error("malformed Decision feed response");
|
|
1187
|
+
}
|
|
1188
|
+
this.snapshot = {
|
|
1189
|
+
decisions: value.decisions,
|
|
1190
|
+
cachedAt: this.now(),
|
|
1191
|
+
unavailable: typeof value.unavailable === "string" ? value.unavailable : void 0
|
|
1192
|
+
};
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (this.snapshot.cachedAt === void 0) {
|
|
1195
|
+
this.snapshot = {
|
|
1196
|
+
decisions: [],
|
|
1197
|
+
unavailable: `daemon Decision cache unavailable: ${errorMessage(error)}`
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1150
1204
|
// src/daemon/env-binding.ts
|
|
1151
1205
|
function normalizeApiUrl(url) {
|
|
1152
1206
|
return url.trim().replace(/\/+$/, "");
|
|
@@ -1428,6 +1482,7 @@ var CONFIG_DIR = join9(homedir6(), ".config", "prim");
|
|
|
1428
1482
|
var SOCK_PATH2 = join9(CONFIG_DIR, "sock");
|
|
1429
1483
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1430
1484
|
var INGESTION_POLL_INTERVAL_MS = 15e3;
|
|
1485
|
+
var DECISION_DIGEST_POLL_INTERVAL_MS = 5e3;
|
|
1431
1486
|
var TOKEN_CHECK_INTERVAL_MS = 6e4;
|
|
1432
1487
|
var TOKEN_REFRESH_THRESHOLD_MS = 9e4;
|
|
1433
1488
|
var HTTP_PROXY_TIMEOUT_MS = 1e4;
|
|
@@ -1439,6 +1494,11 @@ var client = getClient();
|
|
|
1439
1494
|
var runtimeVersion = resolveRuntimeVersion();
|
|
1440
1495
|
var daemonHealth = createDaemonHealthState(runtimeVersion, process.pid, startedAt);
|
|
1441
1496
|
var statuslineIngestionCache = new StatuslineIngestionCache(decisionIngestionStatus);
|
|
1497
|
+
var decisionDigestCache = new DecisionDigestCache(
|
|
1498
|
+
async () => await client.get(DECISION_DIGEST_CACHE_PATH, {
|
|
1499
|
+
signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS)
|
|
1500
|
+
})
|
|
1501
|
+
);
|
|
1442
1502
|
var activeSessionId = process.env.PRIM_DAEMON_SESSION_ID ?? `daemon-${process.pid}`;
|
|
1443
1503
|
var lastHeartbeatAt;
|
|
1444
1504
|
var lastOnlineCount;
|
|
@@ -1447,6 +1507,7 @@ var lastOnlineTeammates;
|
|
|
1447
1507
|
var lastOkAtLocal;
|
|
1448
1508
|
var heartbeatTimer;
|
|
1449
1509
|
var ingestionTimer;
|
|
1510
|
+
var decisionDigestTimer;
|
|
1450
1511
|
var tokenCheckTimer;
|
|
1451
1512
|
var heartbeatInFlight;
|
|
1452
1513
|
var heartbeatRerunRequested = false;
|
|
@@ -1466,7 +1527,7 @@ function resolveRuntimeVersion() {
|
|
|
1466
1527
|
return "unknown";
|
|
1467
1528
|
}
|
|
1468
1529
|
}
|
|
1469
|
-
function
|
|
1530
|
+
function errorMessage2(err) {
|
|
1470
1531
|
return err instanceof Error ? err.message : String(err);
|
|
1471
1532
|
}
|
|
1472
1533
|
function persistHealth() {
|
|
@@ -1475,7 +1536,7 @@ function persistHealth() {
|
|
|
1475
1536
|
try {
|
|
1476
1537
|
writeDaemonHealthState(daemonHealth);
|
|
1477
1538
|
} catch (err) {
|
|
1478
|
-
process.stderr.write(`[prim-daemon] health-state error: ${
|
|
1539
|
+
process.stderr.write(`[prim-daemon] health-state error: ${errorMessage2(err)}
|
|
1479
1540
|
`);
|
|
1480
1541
|
}
|
|
1481
1542
|
}
|
|
@@ -1500,9 +1561,13 @@ function enterReauthHold() {
|
|
|
1500
1561
|
clearTimeout(ingestionTimer);
|
|
1501
1562
|
ingestionTimer = void 0;
|
|
1502
1563
|
}
|
|
1564
|
+
if (decisionDigestTimer) {
|
|
1565
|
+
clearTimeout(decisionDigestTimer);
|
|
1566
|
+
decisionDigestTimer = void 0;
|
|
1567
|
+
}
|
|
1503
1568
|
persistHealth();
|
|
1504
1569
|
process.stderr.write(
|
|
1505
|
-
"[prim-daemon] authentication ended \u2014 halting heartbeat + ingestion until `prim auth login` (captures continue to the journal)\n"
|
|
1570
|
+
"[prim-daemon] authentication ended \u2014 halting heartbeat + ingestion + Decision cache until `prim auth login` (captures continue to the journal)\n"
|
|
1506
1571
|
);
|
|
1507
1572
|
}
|
|
1508
1573
|
function exitReauthHold() {
|
|
@@ -1517,10 +1582,11 @@ function exitReauthHold() {
|
|
|
1517
1582
|
daemonHealth.ingestion.lastError = void 0;
|
|
1518
1583
|
persistHealth();
|
|
1519
1584
|
process.stderr.write(
|
|
1520
|
-
"[prim-daemon] re-authentication detected \u2014 resuming heartbeat + ingestion\n"
|
|
1585
|
+
"[prim-daemon] re-authentication detected \u2014 resuming heartbeat + ingestion + Decision cache\n"
|
|
1521
1586
|
);
|
|
1522
1587
|
void sendHeartbeat();
|
|
1523
1588
|
void runIngestionLoop();
|
|
1589
|
+
void runDecisionDigestLoop();
|
|
1524
1590
|
}
|
|
1525
1591
|
async function takeOwnership() {
|
|
1526
1592
|
const now = Date.now();
|
|
@@ -1570,7 +1636,7 @@ function cleanup() {
|
|
|
1570
1636
|
try {
|
|
1571
1637
|
releaseDaemonOwnership(ownership, SOCK_PATH2);
|
|
1572
1638
|
} catch (err) {
|
|
1573
|
-
process.stderr.write(`[prim-daemon] cleanup error: ${
|
|
1639
|
+
process.stderr.write(`[prim-daemon] cleanup error: ${errorMessage2(err)}
|
|
1574
1640
|
`);
|
|
1575
1641
|
}
|
|
1576
1642
|
}
|
|
@@ -1603,14 +1669,14 @@ async function performHeartbeat() {
|
|
|
1603
1669
|
}
|
|
1604
1670
|
persistHealth();
|
|
1605
1671
|
} catch (err) {
|
|
1606
|
-
daemonHealth.heartbeat.lastError =
|
|
1672
|
+
daemonHealth.heartbeat.lastError = errorMessage2(err);
|
|
1607
1673
|
daemonHealth.heartbeat.consecutiveFailures += 1;
|
|
1608
1674
|
persistHealth();
|
|
1609
1675
|
if (isSessionEnded()) {
|
|
1610
1676
|
enterReauthHold();
|
|
1611
1677
|
return;
|
|
1612
1678
|
}
|
|
1613
|
-
process.stderr.write(`[prim-daemon] heartbeat error: ${
|
|
1679
|
+
process.stderr.write(`[prim-daemon] heartbeat error: ${errorMessage2(err)}
|
|
1614
1680
|
`);
|
|
1615
1681
|
}
|
|
1616
1682
|
}
|
|
@@ -1681,7 +1747,7 @@ async function runIngestionLoop() {
|
|
|
1681
1747
|
const failures = daemonHealth.ingestion.consecutiveFailures + 1;
|
|
1682
1748
|
const delay = ingestionRetryDelayMs(failures);
|
|
1683
1749
|
daemonHealth.ingestion.consecutiveFailures = failures;
|
|
1684
|
-
daemonHealth.ingestion.lastError = `journal scan failed: ${
|
|
1750
|
+
daemonHealth.ingestion.lastError = `journal scan failed: ${errorMessage2(err)}`;
|
|
1685
1751
|
daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
|
|
1686
1752
|
persistHealth();
|
|
1687
1753
|
scheduleIngestion(delay);
|
|
@@ -1716,7 +1782,7 @@ async function runIngestionLoop() {
|
|
|
1716
1782
|
const delay = ingestionRetryDelayMs(failures);
|
|
1717
1783
|
daemonHealth.ingestion.lastAcknowledgedCount = err instanceof FlushError ? err.flushed : 0;
|
|
1718
1784
|
daemonHealth.ingestion.consecutiveFailures = failures;
|
|
1719
|
-
daemonHealth.ingestion.lastError =
|
|
1785
|
+
daemonHealth.ingestion.lastError = errorMessage2(err);
|
|
1720
1786
|
daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
|
|
1721
1787
|
try {
|
|
1722
1788
|
updatePendingHealth();
|
|
@@ -1727,11 +1793,25 @@ async function runIngestionLoop() {
|
|
|
1727
1793
|
enterReauthHold();
|
|
1728
1794
|
return;
|
|
1729
1795
|
}
|
|
1730
|
-
process.stderr.write(`[prim-daemon] ingestion error: ${
|
|
1796
|
+
process.stderr.write(`[prim-daemon] ingestion error: ${errorMessage2(err)}
|
|
1731
1797
|
`);
|
|
1732
1798
|
scheduleIngestion(delay);
|
|
1733
1799
|
}
|
|
1734
1800
|
}
|
|
1801
|
+
function scheduleDecisionDigestRefresh() {
|
|
1802
|
+
if (shuttingDown || reauthHold) return;
|
|
1803
|
+
decisionDigestTimer = setTimeout(() => {
|
|
1804
|
+
decisionDigestTimer = void 0;
|
|
1805
|
+
void runDecisionDigestLoop();
|
|
1806
|
+
}, DECISION_DIGEST_POLL_INTERVAL_MS);
|
|
1807
|
+
}
|
|
1808
|
+
function refreshDecisionDigest() {
|
|
1809
|
+
return shuttingDown || reauthHold ? Promise.resolve() : decisionDigestCache.refresh();
|
|
1810
|
+
}
|
|
1811
|
+
async function runDecisionDigestLoop() {
|
|
1812
|
+
await refreshDecisionDigest();
|
|
1813
|
+
scheduleDecisionDigestRefresh();
|
|
1814
|
+
}
|
|
1735
1815
|
function pathParam(params) {
|
|
1736
1816
|
if (typeof params.path !== "string" || !params.path.startsWith("/api/cli/")) {
|
|
1737
1817
|
throw new Error("proxy request requires `path: string` under /api/cli/");
|
|
@@ -1818,6 +1898,7 @@ async function dispatchRequest(req) {
|
|
|
1818
1898
|
activeSessionId = sid;
|
|
1819
1899
|
}
|
|
1820
1900
|
await sendHeartbeat();
|
|
1901
|
+
void refreshDecisionDigest();
|
|
1821
1902
|
return { id, ok: true, result: { sessionId: activeSessionId } };
|
|
1822
1903
|
}
|
|
1823
1904
|
case "session_end": {
|
|
@@ -1825,6 +1906,12 @@ async function dispatchRequest(req) {
|
|
|
1825
1906
|
}
|
|
1826
1907
|
case "status_snapshot":
|
|
1827
1908
|
return { id, ok: true, result: handleStatusSnapshot(req.params ?? {}) };
|
|
1909
|
+
case "decision_digest_snapshot": {
|
|
1910
|
+
assertCallerEnvMatches(req.params?.callerEnv, getSiteUrl());
|
|
1911
|
+
const result = decisionDigestCache.read();
|
|
1912
|
+
void refreshDecisionDigest();
|
|
1913
|
+
return { id, ok: true, result };
|
|
1914
|
+
}
|
|
1828
1915
|
case "statusline_invalidate":
|
|
1829
1916
|
statuslineIngestionCache.clear();
|
|
1830
1917
|
return { id, ok: true, result: { ack: true } };
|
|
@@ -1959,6 +2046,7 @@ function startTimers() {
|
|
|
1959
2046
|
persistHealth();
|
|
1960
2047
|
void sendHeartbeat();
|
|
1961
2048
|
void runIngestionLoop();
|
|
2049
|
+
void runDecisionDigestLoop();
|
|
1962
2050
|
}
|
|
1963
2051
|
void runTokenCheckLoop();
|
|
1964
2052
|
}
|
|
@@ -1987,6 +2075,9 @@ function stopTimers() {
|
|
|
1987
2075
|
if (ingestionTimer) {
|
|
1988
2076
|
clearTimeout(ingestionTimer);
|
|
1989
2077
|
}
|
|
2078
|
+
if (decisionDigestTimer) {
|
|
2079
|
+
clearTimeout(decisionDigestTimer);
|
|
2080
|
+
}
|
|
1990
2081
|
if (tokenCheckTimer) {
|
|
1991
2082
|
clearTimeout(tokenCheckTimer);
|
|
1992
2083
|
}
|
|
@@ -2001,14 +2092,14 @@ function installSignalHandlers() {
|
|
|
2001
2092
|
shutdown(EXIT_CRASH, `[prim-daemon] uncaught: ${err.message}`);
|
|
2002
2093
|
});
|
|
2003
2094
|
process.on("unhandledRejection", (err) => {
|
|
2004
|
-
shutdown(EXIT_CRASH, `[prim-daemon] unhandled rejection: ${
|
|
2095
|
+
shutdown(EXIT_CRASH, `[prim-daemon] unhandled rejection: ${errorMessage2(err)}`);
|
|
2005
2096
|
});
|
|
2006
2097
|
}
|
|
2007
2098
|
async function main() {
|
|
2008
2099
|
try {
|
|
2009
2100
|
await takeOwnership();
|
|
2010
2101
|
} catch (err) {
|
|
2011
|
-
process.stderr.write(`[prim-daemon] ${
|
|
2102
|
+
process.stderr.write(`[prim-daemon] ${errorMessage2(err)}
|
|
2012
2103
|
`);
|
|
2013
2104
|
cleanup();
|
|
2014
2105
|
process.exit(EXIT_CRASH);
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
bold,
|
|
4
|
-
color,
|
|
5
3
|
requireDurableIngestAcknowledgement
|
|
6
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-XGADQPCI.js";
|
|
7
5
|
import {
|
|
8
6
|
enrichHookPayloadWithFileRefs,
|
|
9
7
|
preserveHookFileMetadata
|
|
@@ -24,8 +22,14 @@ import {
|
|
|
24
22
|
scrubFromCwd
|
|
25
23
|
} from "../chunk-G5D3FF3D.js";
|
|
26
24
|
import {
|
|
25
|
+
appendCodexContext,
|
|
26
|
+
prepareCodexContext
|
|
27
|
+
} from "../chunk-I5BHJQH2.js";
|
|
28
|
+
import {
|
|
29
|
+
bold,
|
|
30
|
+
color,
|
|
27
31
|
warmBinCache
|
|
28
|
-
} from "../chunk-
|
|
32
|
+
} from "../chunk-K3AYW6VZ.js";
|
|
29
33
|
import {
|
|
30
34
|
isRepoActiveForCapture,
|
|
31
35
|
repoSyncId,
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
normalizeEnvelope,
|
|
39
43
|
parseAgent
|
|
40
44
|
} from "../chunk-TCDUH7AN.js";
|
|
45
|
+
import "../chunk-UTKQTZHL.js";
|
|
41
46
|
|
|
42
47
|
// src/hooks/post-tool-use.ts
|
|
43
48
|
import { readFileSync } from "fs";
|
|
@@ -112,8 +117,19 @@ function readStdin() {
|
|
|
112
117
|
});
|
|
113
118
|
});
|
|
114
119
|
}
|
|
115
|
-
function emit() {
|
|
116
|
-
|
|
120
|
+
async function emit(output = {}) {
|
|
121
|
+
return await new Promise((resolve) => {
|
|
122
|
+
try {
|
|
123
|
+
process.stdout.write(`${JSON.stringify(output)}
|
|
124
|
+
`, (error) => resolve(!error));
|
|
125
|
+
} catch {
|
|
126
|
+
resolve(false);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function emitWithAcknowledgment(output, acknowledge) {
|
|
131
|
+
const handedOff = await emit(output);
|
|
132
|
+
await acknowledge?.(handedOff);
|
|
117
133
|
}
|
|
118
134
|
function debug(msg) {
|
|
119
135
|
if (process.env.PRIM_HOOK_VERBOSE === "1") {
|
|
@@ -128,31 +144,31 @@ async function main() {
|
|
|
128
144
|
try {
|
|
129
145
|
raw = await readStdin();
|
|
130
146
|
} catch {
|
|
131
|
-
emit();
|
|
147
|
+
await emit();
|
|
132
148
|
return;
|
|
133
149
|
}
|
|
134
150
|
let parsed;
|
|
135
151
|
try {
|
|
136
152
|
parsed = normalizeEnvelope(JSON.parse(raw), agent);
|
|
137
153
|
} catch {
|
|
138
|
-
emit();
|
|
154
|
+
await emit();
|
|
139
155
|
return;
|
|
140
156
|
}
|
|
141
157
|
let envelope = parsed;
|
|
142
158
|
const isToolResult = envelope.hook_event_name === "PostToolUse" || agent === "claude_code" && envelope.hook_event_name === "PostToolUseFailure";
|
|
143
159
|
const isHermesDenial = agent === "hermes" && envelope.hook_event_name === "post_approval_response" && toolOutcomeFor(parsed, agent) === "prevented";
|
|
144
160
|
if (!isToolResult && !isHermesDenial) {
|
|
145
|
-
emit();
|
|
161
|
+
await emit();
|
|
146
162
|
return;
|
|
147
163
|
}
|
|
148
164
|
const invocationId = postToolInvocationId(parsed, agent);
|
|
149
165
|
if (isHermesDenial && !invocationId) {
|
|
150
|
-
emit();
|
|
166
|
+
await emit();
|
|
151
167
|
return;
|
|
152
168
|
}
|
|
153
169
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
154
170
|
if (!isHermesDenial && !editingToolsFor(agent).has(toolName)) {
|
|
155
|
-
emit();
|
|
171
|
+
await emit();
|
|
156
172
|
return;
|
|
157
173
|
}
|
|
158
174
|
if (isHermesDenial && (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) && typeof envelope.extra?.session_key === "string" && envelope.extra.session_key.length > 0) {
|
|
@@ -160,17 +176,17 @@ async function main() {
|
|
|
160
176
|
envelope = parsed;
|
|
161
177
|
}
|
|
162
178
|
if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
|
|
163
|
-
emit();
|
|
179
|
+
await emit();
|
|
164
180
|
return;
|
|
165
181
|
}
|
|
166
182
|
const cwd = parsed.cwd ?? process.cwd();
|
|
167
183
|
if (!isRepoActiveForCapture(cwd)) {
|
|
168
|
-
emit();
|
|
184
|
+
await emit();
|
|
169
185
|
return;
|
|
170
186
|
}
|
|
171
187
|
const resolvedRepository = resolveRepositoryContext(cwd);
|
|
172
188
|
if (!resolvedRepository) {
|
|
173
|
-
emit();
|
|
189
|
+
await emit();
|
|
174
190
|
return;
|
|
175
191
|
}
|
|
176
192
|
const repository = { ...resolvedRepository, repoSyncId: repoSyncId(cwd) };
|
|
@@ -183,11 +199,11 @@ async function main() {
|
|
|
183
199
|
const { resolution } = enrichment;
|
|
184
200
|
if (!isHermesDenial) {
|
|
185
201
|
if (resolution.shellMutation === "none") {
|
|
186
|
-
emit();
|
|
202
|
+
await emit();
|
|
187
203
|
return;
|
|
188
204
|
}
|
|
189
205
|
if (resolution.shellMutation === void 0 && resolution.fileRefs.length === 0) {
|
|
190
|
-
emit();
|
|
206
|
+
await emit();
|
|
191
207
|
return;
|
|
192
208
|
}
|
|
193
209
|
}
|
|
@@ -201,18 +217,32 @@ async function main() {
|
|
|
201
217
|
payload: preserveHookFileMetadata(scrubbed, resolution)
|
|
202
218
|
};
|
|
203
219
|
const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
|
|
220
|
+
let verdictFooter = false;
|
|
204
221
|
try {
|
|
205
222
|
const result = await deliverPostToolMove(move, orgId);
|
|
206
223
|
debug(`durably ingested ${move.moveId} (${toolName})`);
|
|
207
224
|
if (isVerdictFooterContext(result.verdictFooter)) {
|
|
225
|
+
verdictFooter = true;
|
|
208
226
|
process.stderr.write(`${renderVerdictFooter(result.verdictFooter)}
|
|
209
227
|
`);
|
|
210
228
|
}
|
|
211
229
|
} catch (err) {
|
|
212
230
|
debug(`ingest failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
213
231
|
}
|
|
214
|
-
|
|
232
|
+
if (agent === "codex" && verdictFooter) {
|
|
233
|
+
try {
|
|
234
|
+
const context = await prepareCodexContext({
|
|
235
|
+
cwd,
|
|
236
|
+
sessionId: envelope.session_id,
|
|
237
|
+
includeDigest: false
|
|
238
|
+
});
|
|
239
|
+
await emitWithAcknowledgment(appendCodexContext({}, context.context), context.acknowledge);
|
|
240
|
+
return;
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
await emit();
|
|
215
245
|
}
|
|
216
|
-
main().catch(() => {
|
|
217
|
-
emit();
|
|
246
|
+
main().catch(async () => {
|
|
247
|
+
await emit();
|
|
218
248
|
});
|
|
@@ -13,10 +13,15 @@ import {
|
|
|
13
13
|
readHookMode,
|
|
14
14
|
scrub
|
|
15
15
|
} from "../chunk-G5D3FF3D.js";
|
|
16
|
+
import {
|
|
17
|
+
appendCodexContext,
|
|
18
|
+
hasVisibleCodexMessage,
|
|
19
|
+
prepareCodexContext
|
|
20
|
+
} from "../chunk-I5BHJQH2.js";
|
|
16
21
|
import {
|
|
17
22
|
packageVersion,
|
|
18
23
|
warmBinCache
|
|
19
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-K3AYW6VZ.js";
|
|
20
25
|
import {
|
|
21
26
|
canonicalGitRoot,
|
|
22
27
|
canonicalRepositoryPath,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
normalizeEnvelope,
|
|
31
36
|
parseAgent
|
|
32
37
|
} from "../chunk-TCDUH7AN.js";
|
|
38
|
+
import "../chunk-UTKQTZHL.js";
|
|
33
39
|
|
|
34
40
|
// src/hooks/preflight-v3.ts
|
|
35
41
|
import { Buffer as Buffer2 } from "buffer";
|
|
@@ -147,9 +153,19 @@ async function readStdin() {
|
|
|
147
153
|
});
|
|
148
154
|
}
|
|
149
155
|
var agent = parseAgent(process.argv);
|
|
150
|
-
function emit(output) {
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
async function emit(output) {
|
|
157
|
+
return await new Promise((resolve) => {
|
|
158
|
+
try {
|
|
159
|
+
process.stdout.write(`${JSON.stringify(output)}
|
|
160
|
+
`, (error) => resolve(!error));
|
|
161
|
+
} catch {
|
|
162
|
+
resolve(false);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function emitWithAcknowledgment(output, acknowledge) {
|
|
167
|
+
const handedOff = await emit(output);
|
|
168
|
+
await acknowledge?.(handedOff);
|
|
153
169
|
}
|
|
154
170
|
function failOpen() {
|
|
155
171
|
if (agent === "hermes") return failOpenHermes();
|
|
@@ -159,16 +175,28 @@ function invocationId(envelope) {
|
|
|
159
175
|
const value = agent === "hermes" ? envelope.extra?.tool_call_id : envelope.tool_use_id;
|
|
160
176
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
161
177
|
}
|
|
162
|
-
function emitUnverified(message) {
|
|
178
|
+
async function emitUnverified(message, envelope) {
|
|
163
179
|
const result = unverifiedResult(message);
|
|
164
180
|
if (agent === "hermes") {
|
|
165
181
|
process.stderr.write(`[primitive] ${message}
|
|
166
182
|
`);
|
|
167
|
-
emit(failOpenHermes());
|
|
183
|
+
await emit(failOpenHermes());
|
|
168
184
|
} else {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
let output = agent === "codex" ? buildCodexOutput("allow", [result]) : buildHookOutput("allow", [result]);
|
|
186
|
+
if (agent === "codex" && typeof envelope?.session_id === "string" && envelope.session_id) {
|
|
187
|
+
try {
|
|
188
|
+
const context = await prepareCodexContext({
|
|
189
|
+
cwd: envelope.cwd ?? process.cwd(),
|
|
190
|
+
sessionId: envelope.session_id,
|
|
191
|
+
includeDigest: false
|
|
192
|
+
});
|
|
193
|
+
output = appendCodexContext(output, context.context);
|
|
194
|
+
await emitWithAcknowledgment(output, context.acknowledge);
|
|
195
|
+
return;
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await emit(output);
|
|
172
200
|
}
|
|
173
201
|
}
|
|
174
202
|
async function main() {
|
|
@@ -177,7 +205,7 @@ async function main() {
|
|
|
177
205
|
try {
|
|
178
206
|
raw = await readStdin();
|
|
179
207
|
} catch {
|
|
180
|
-
emit(failOpen());
|
|
208
|
+
await emit(failOpen());
|
|
181
209
|
return;
|
|
182
210
|
}
|
|
183
211
|
let envelope;
|
|
@@ -187,17 +215,17 @@ async function main() {
|
|
|
187
215
|
agent
|
|
188
216
|
);
|
|
189
217
|
} catch {
|
|
190
|
-
emit(failOpen());
|
|
218
|
+
await emit(failOpen());
|
|
191
219
|
return;
|
|
192
220
|
}
|
|
193
221
|
if (envelope.hook_event_name !== "PreToolUse") {
|
|
194
|
-
emit(failOpen());
|
|
222
|
+
await emit(failOpen());
|
|
195
223
|
return;
|
|
196
224
|
}
|
|
197
225
|
const env = process.env;
|
|
198
226
|
const mode = readHookMode(env);
|
|
199
227
|
if (mode === "off") {
|
|
200
|
-
emit(failOpen());
|
|
228
|
+
await emit(failOpen());
|
|
201
229
|
return;
|
|
202
230
|
}
|
|
203
231
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
@@ -209,22 +237,25 @@ async function main() {
|
|
|
209
237
|
cwd
|
|
210
238
|
});
|
|
211
239
|
if (targets.mutation === "none") {
|
|
212
|
-
emit(failOpen());
|
|
240
|
+
await emit(failOpen());
|
|
213
241
|
return;
|
|
214
242
|
}
|
|
215
243
|
if (!isRepoActive(cwd)) {
|
|
216
|
-
emit(failOpen());
|
|
244
|
+
await emit(failOpen());
|
|
217
245
|
return;
|
|
218
246
|
}
|
|
219
247
|
if (targets.paths.length === 0) {
|
|
220
|
-
emitUnverified(
|
|
248
|
+
await emitUnverified(
|
|
249
|
+
"mutation targets could not be determined; enforcement not verified",
|
|
250
|
+
envelope
|
|
251
|
+
);
|
|
221
252
|
return;
|
|
222
253
|
}
|
|
223
254
|
const binding = repoSyncId(cwd);
|
|
224
255
|
const sessionId = envelope.session_id;
|
|
225
256
|
const callId = invocationId(envelope);
|
|
226
257
|
if (!binding || typeof sessionId !== "string" || !sessionId || !callId) {
|
|
227
|
-
emitUnverified("repository binding or tool invocation identity is unavailable");
|
|
258
|
+
await emitUnverified("repository binding or tool invocation identity is unavailable", envelope);
|
|
228
259
|
return;
|
|
229
260
|
}
|
|
230
261
|
const request = {
|
|
@@ -243,12 +274,12 @@ async function main() {
|
|
|
243
274
|
try {
|
|
244
275
|
const response = await requestPreflight(request);
|
|
245
276
|
if (!response) {
|
|
246
|
-
emitUnverified("enforcement service returned an incompatible response");
|
|
277
|
+
await emitUnverified("enforcement service returned an incompatible response", envelope);
|
|
247
278
|
return;
|
|
248
279
|
}
|
|
249
280
|
result = resultForPreflight(response);
|
|
250
281
|
} catch {
|
|
251
|
-
emitUnverified("enforcement service unavailable; change was not verified");
|
|
282
|
+
await emitUnverified("enforcement service unavailable; change was not verified", envelope);
|
|
252
283
|
return;
|
|
253
284
|
}
|
|
254
285
|
const aggregate = demoteForMode(
|
|
@@ -259,10 +290,18 @@ async function main() {
|
|
|
259
290
|
process.stderr.write(`[primitive] ${result.reason || result.unavailable}
|
|
260
291
|
`);
|
|
261
292
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
293
|
+
let output = agent === "hermes" ? buildHermesOutput(aggregate, [result]) : agent === "codex" ? buildCodexOutput(aggregate, [result]) : buildHookOutput(aggregate, [result]);
|
|
294
|
+
if (agent === "codex" && hasVisibleCodexMessage(output) && typeof sessionId === "string" && sessionId.length > 0) {
|
|
295
|
+
try {
|
|
296
|
+
const context = await prepareCodexContext({ cwd, sessionId, includeDigest: false });
|
|
297
|
+
output = appendCodexContext(output, context.context);
|
|
298
|
+
await emitWithAcknowledgment(output, context.acknowledge);
|
|
299
|
+
return;
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
await emit(output);
|
|
265
304
|
}
|
|
266
|
-
main().catch(() => {
|
|
267
|
-
emit(failOpen());
|
|
305
|
+
main().catch(async () => {
|
|
306
|
+
await emit(failOpen());
|
|
268
307
|
});
|