@primitive.ai/prim 0.1.0-alpha.7 → 0.1.0-alpha.70
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 +296 -37
- package/SKILL.md +269 -0
- package/dist/chunk-2KW7DVFJ.js +1400 -0
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4A2XB2PC.js +310 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-BQUJ4AFJ.js +771 -0
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/chunk-H64F6QHV.js +247 -0
- package/dist/chunk-KKP2NYZ3.js +505 -0
- package/dist/chunk-LEMX3IOG.js +79 -0
- package/dist/chunk-LMRYZL3D.js +209 -0
- package/dist/chunk-RO3F6JYV.js +126 -0
- package/dist/chunk-TJTVHDJ5.js +321 -0
- package/dist/chunk-UVIWZWM6.js +211 -0
- package/dist/chunk-WB7AUZ4C.js +52 -0
- package/dist/chunk-XGADQPCI.js +26 -0
- package/dist/chunk-YWO7O6QG.js +327 -0
- package/dist/chunk-Z7YPFWAZ.js +917 -0
- package/dist/chunk-ZOSNEO5N.js +77 -0
- package/dist/daemon/server.js +13353 -0
- package/dist/hook-runtime/daemon/server.js +13353 -0
- package/dist/hook-runtime/hooks/post-commit.js +1128 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
- package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
- package/dist/hook-runtime/hooks/session-end.js +495 -0
- package/dist/hook-runtime/hooks/session-start.js +11284 -0
- package/dist/hook-runtime/index.js +42775 -0
- package/dist/hook-runtime/statusline-main.js +765 -0
- package/dist/hooks/post-commit.js +179 -0
- package/dist/hooks/post-rewrite.js +197 -0
- package/dist/hooks/post-tool-use.js +250 -0
- package/dist/hooks/pre-commit.js +21 -74
- package/dist/hooks/pre-tool-use.js +348 -0
- package/dist/hooks/prim-hook.js +195 -0
- package/dist/hooks/session-end.js +70 -0
- package/dist/hooks/session-start.js +271 -0
- package/dist/index.js +18920 -518
- package/dist/statusline-main.js +765 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeJwtPayload,
|
|
3
|
+
primConfigDirectory,
|
|
4
|
+
resolveAuthCredential
|
|
5
|
+
} from "./chunk-3EEFL6NJ.js";
|
|
6
|
+
|
|
7
|
+
// src/daemon/client.ts
|
|
8
|
+
import { createConnection } from "net";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
|
|
11
|
+
// src/daemon/principal.ts
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
var PRINCIPAL_COMPONENT_MAX_LENGTH = 512;
|
|
14
|
+
function boundedClaim(value) {
|
|
15
|
+
return typeof value === "string" && value.length > 0 && value.length <= PRINCIPAL_COMPONENT_MAX_LENGTH ? value : void 0;
|
|
16
|
+
}
|
|
17
|
+
function sha256(value) {
|
|
18
|
+
return createHash("sha256").update(value).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
function resolveDaemonPrincipal(token = resolveAuthCredential()?.token) {
|
|
21
|
+
if (!token) return void 0;
|
|
22
|
+
const payload = decodeJwtPayload(token);
|
|
23
|
+
const subject = boundedClaim(payload?.sub);
|
|
24
|
+
const organizationId = boundedClaim(payload?.org_id);
|
|
25
|
+
if (!(subject && organizationId)) return void 0;
|
|
26
|
+
const issuer = boundedClaim(payload?.iss) ?? "unknown-issuer";
|
|
27
|
+
return {
|
|
28
|
+
principalId: sha256(`${issuer}\0${subject}`),
|
|
29
|
+
organizationId,
|
|
30
|
+
credentialFingerprint: sha256(token)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function daemonPrincipalsMatch(caller, daemon) {
|
|
34
|
+
return caller !== void 0 && daemon !== void 0 && caller.principalId === daemon.principalId && caller.organizationId === daemon.organizationId && caller.credentialFingerprint === daemon.credentialFingerprint;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/daemon/protocol.ts
|
|
38
|
+
var DAEMON_JSON_REQUEST_MAX_BYTES = 64 * 1024;
|
|
39
|
+
var DAEMON_JSON_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function parseDaemonResponseEnvelope(value) {
|
|
44
|
+
if (!isRecord(value)) return void 0;
|
|
45
|
+
if (!(typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0)) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value.ok !== "boolean") return void 0;
|
|
49
|
+
if (value.error !== void 0 && typeof value.error !== "string") return void 0;
|
|
50
|
+
return {
|
|
51
|
+
id: value.id,
|
|
52
|
+
ok: value.ok,
|
|
53
|
+
...value.result === void 0 ? {} : { result: value.result },
|
|
54
|
+
...value.error === void 0 ? {} : { error: value.error }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/daemon/client.ts
|
|
59
|
+
var SOCK_PATH = join(primConfigDirectory(), "sock");
|
|
60
|
+
var DEFAULT_TIMEOUT_MS = 250;
|
|
61
|
+
var nextRequestId = 1;
|
|
62
|
+
function daemonRequest(method, params = {}, opts = {}) {
|
|
63
|
+
const id = nextRequestId++;
|
|
64
|
+
const caller = resolveDaemonPrincipal();
|
|
65
|
+
const request = {
|
|
66
|
+
id,
|
|
67
|
+
method,
|
|
68
|
+
params,
|
|
69
|
+
...caller ? { caller } : {}
|
|
70
|
+
};
|
|
71
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
let settled = false;
|
|
74
|
+
let socket;
|
|
75
|
+
let buffer = Buffer.alloc(0);
|
|
76
|
+
const settle = (value) => {
|
|
77
|
+
if (settled) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
settled = true;
|
|
81
|
+
if (socket) {
|
|
82
|
+
try {
|
|
83
|
+
socket.destroy();
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
resolve(value);
|
|
88
|
+
};
|
|
89
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
90
|
+
timer.unref();
|
|
91
|
+
try {
|
|
92
|
+
socket = createConnection(SOCK_PATH);
|
|
93
|
+
} catch {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
settle(null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
socket.on("error", () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
settle(null);
|
|
101
|
+
});
|
|
102
|
+
socket.on("connect", () => {
|
|
103
|
+
try {
|
|
104
|
+
socket?.write(`${JSON.stringify(request)}
|
|
105
|
+
`);
|
|
106
|
+
} catch {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
settle(null);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
socket.on("data", (chunk) => {
|
|
112
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
113
|
+
if (buffer.length > DAEMON_JSON_RESPONSE_MAX_BYTES) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
settle(null);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const newlineIdx = buffer.indexOf(10);
|
|
119
|
+
if (newlineIdx === -1) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const line = buffer.subarray(0, newlineIdx).toString("utf8");
|
|
123
|
+
try {
|
|
124
|
+
const res = parseDaemonResponseEnvelope(JSON.parse(line));
|
|
125
|
+
if (!res || res.id !== id) {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
settle(null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
settle(res.ok && res.result !== void 0 ? res.result : null);
|
|
132
|
+
} catch {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
settle(null);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
socket.on("end", () => {
|
|
138
|
+
if (!settled) {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
settle(null);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async function daemonIsLive(timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
146
|
+
const result = await daemonRequest("ping", {}, { timeoutMs });
|
|
147
|
+
return result?.pong === true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
resolveDaemonPrincipal,
|
|
152
|
+
daemonPrincipalsMatch,
|
|
153
|
+
daemonRequest,
|
|
154
|
+
daemonIsLive
|
|
155
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boundedHealthError
|
|
3
|
+
} from "./chunk-LEMX3IOG.js";
|
|
4
|
+
import {
|
|
5
|
+
terminalSafeLine
|
|
6
|
+
} from "./chunk-Z7YPFWAZ.js";
|
|
7
|
+
|
|
8
|
+
// src/hooks/redact.ts
|
|
9
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { Worker } from "worker_threads";
|
|
12
|
+
var USER_HOME_RULES = [
|
|
13
|
+
{
|
|
14
|
+
pattern: /\/Users\/[^/\\\s"'`]+/g,
|
|
15
|
+
reason: "user-home-path",
|
|
16
|
+
replacement: "/Users/__redacted_user__"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pattern: /\/home\/[^/\\\s"'`]+/g,
|
|
20
|
+
reason: "user-home-path",
|
|
21
|
+
replacement: "/home/__redacted_user__"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
pattern: /([A-Za-z]:\\Users\\)[^/\\\s"'`]+/gi,
|
|
25
|
+
reason: "user-home-path",
|
|
26
|
+
replacement: "$1__redacted_user__"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /\/root(?=\/|$)/g,
|
|
30
|
+
reason: "user-home-path",
|
|
31
|
+
replacement: "/__redacted_user__"
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
var DEFAULT_RULES = [
|
|
35
|
+
{ pattern: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi, reason: "bearer-token" },
|
|
36
|
+
{
|
|
37
|
+
pattern: /\bsk-(?:(?:ant|proj)-)?[A-Za-z0-9_-]{16,}(?![A-Za-z0-9_-])/g,
|
|
38
|
+
reason: "sk-api-key"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g,
|
|
42
|
+
reason: "private-key"
|
|
43
|
+
},
|
|
44
|
+
{ pattern: /xox[abprs]-[A-Za-z0-9-]{10,}/g, reason: "slack-token" },
|
|
45
|
+
{
|
|
46
|
+
pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{50,})\b/g,
|
|
47
|
+
reason: "github-pat"
|
|
48
|
+
},
|
|
49
|
+
{ pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, reason: "aws-access-key" },
|
|
50
|
+
{
|
|
51
|
+
pattern: /\bAWS_(?:SECRET_ACCESS_KEY|SESSION_TOKEN)"?\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}["']?/gi,
|
|
52
|
+
reason: "aws-secret"
|
|
53
|
+
},
|
|
54
|
+
{ pattern: /\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, reason: "gcp-api-key" },
|
|
55
|
+
{
|
|
56
|
+
pattern: /\bya29\.[0-9A-Za-z_-]{20,}(?![0-9A-Za-z_-])/g,
|
|
57
|
+
reason: "gcp-oauth-token"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
pattern: /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*(?![A-Za-z0-9_-])/g,
|
|
61
|
+
reason: "jwt"
|
|
62
|
+
},
|
|
63
|
+
{ pattern: /\bBasic\s+[A-Za-z0-9+/]+={0,2}/gi, reason: "basic-auth" },
|
|
64
|
+
{ pattern: /\bhttps?:\/\/[^/\s:@]+:[^/\s@]+@/gi, reason: "basic-auth" },
|
|
65
|
+
...USER_HOME_RULES
|
|
66
|
+
];
|
|
67
|
+
var MAX_SCRUB_LEN = 256e3;
|
|
68
|
+
var MAX_WORKSPACE_CONFIG_BYTES = 64 * 1024;
|
|
69
|
+
var MAX_WORKSPACE_RULES = 32;
|
|
70
|
+
var MAX_WORKSPACE_PATTERN_LENGTH = 512;
|
|
71
|
+
var MAX_WORKSPACE_REASON_LENGTH = 64;
|
|
72
|
+
var WORKSPACE_RULE_TIMEOUT_MS = 250;
|
|
73
|
+
var WORKSPACE_FILE = ".prim/redaction.json";
|
|
74
|
+
var SAFE_REASON_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
|
|
75
|
+
var SAFE_WORKSPACE_FLAGS_RE = /^[gimsu]*$/u;
|
|
76
|
+
function debugRedaction(message) {
|
|
77
|
+
writeHookDebug(`redaction: ${message}`);
|
|
78
|
+
}
|
|
79
|
+
function normalizedWorkspaceFlags(value) {
|
|
80
|
+
if (value !== void 0 && typeof value !== "string") return null;
|
|
81
|
+
const requested = value ?? "";
|
|
82
|
+
if (!SAFE_WORKSPACE_FLAGS_RE.test(requested)) return null;
|
|
83
|
+
return [...new Set(`g${requested}`)].sort().join("");
|
|
84
|
+
}
|
|
85
|
+
function loadWorkspaceRules(cwd) {
|
|
86
|
+
const path = join(cwd, WORKSPACE_FILE);
|
|
87
|
+
if (!existsSync(path)) return [];
|
|
88
|
+
let cfg;
|
|
89
|
+
try {
|
|
90
|
+
if (statSync(path).size > MAX_WORKSPACE_CONFIG_BYTES) {
|
|
91
|
+
debugRedaction(`ignoring oversized ${WORKSPACE_FILE}`);
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
cfg = JSON.parse(readFileSync(path, "utf-8"));
|
|
95
|
+
} catch {
|
|
96
|
+
debugRedaction(`ignoring unparseable ${WORKSPACE_FILE}`);
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(cfg.rules)) return [];
|
|
100
|
+
const compiled = [];
|
|
101
|
+
for (const candidate of cfg.rules.slice(0, MAX_WORKSPACE_RULES)) {
|
|
102
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
103
|
+
const { pattern, reason } = candidate;
|
|
104
|
+
const flags = normalizedWorkspaceFlags(candidate.flags);
|
|
105
|
+
if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > MAX_WORKSPACE_PATTERN_LENGTH || typeof reason !== "string" || reason.length === 0 || reason.length > MAX_WORKSPACE_REASON_LENGTH || !SAFE_REASON_RE.test(reason) || flags === null) {
|
|
106
|
+
debugRedaction("skipping invalid workspace rule metadata");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
compiled.push({ pattern: new RegExp(pattern, flags), reason });
|
|
111
|
+
} catch {
|
|
112
|
+
debugRedaction(`skipping invalid workspace rule ${reason}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return compiled;
|
|
116
|
+
}
|
|
117
|
+
function applyRules(input, rules) {
|
|
118
|
+
if (input.length > MAX_SCRUB_LEN) return "<REDACTED:oversized>";
|
|
119
|
+
let out = input;
|
|
120
|
+
for (const rule of rules) {
|
|
121
|
+
out = out.replace(rule.pattern, rule.replacement ?? `<REDACTED:${rule.reason}>`);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
var REDACTED_OBJECT_KEY = "<REDACTED:object-key>";
|
|
126
|
+
function redactedObjectKey(input, transform) {
|
|
127
|
+
return transform(input) === input ? input : REDACTED_OBJECT_KEY;
|
|
128
|
+
}
|
|
129
|
+
function uniqueObjectKey(output, key) {
|
|
130
|
+
let candidate = key;
|
|
131
|
+
let suffix = 2;
|
|
132
|
+
while (Object.hasOwn(output, candidate)) candidate = `${key}_${suffix++}`;
|
|
133
|
+
return candidate;
|
|
134
|
+
}
|
|
135
|
+
function mapStrings(value, transform) {
|
|
136
|
+
if (typeof value === "string") return transform(value);
|
|
137
|
+
if (Array.isArray(value)) return value.map((item) => mapStrings(item, transform));
|
|
138
|
+
if (value && typeof value === "object") {
|
|
139
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
140
|
+
for (const [key, item] of Object.entries(value)) {
|
|
141
|
+
out[uniqueObjectKey(out, redactedObjectKey(key, transform))] = mapStrings(item, transform);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
function scrub(value, rules = DEFAULT_RULES) {
|
|
148
|
+
return mapStrings(value, (input) => applyRules(input, rules));
|
|
149
|
+
}
|
|
150
|
+
function debugDetail(value) {
|
|
151
|
+
try {
|
|
152
|
+
return String(value instanceof Error ? value.message : value);
|
|
153
|
+
} catch {
|
|
154
|
+
return "unprintable detail";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function writeHookDebug(context, ...details) {
|
|
158
|
+
if (!process.env.PRIM_HOOK_DEBUG) return;
|
|
159
|
+
const raw = details.length > 0 ? `${context}: ${details.map(debugDetail).join(": ")}` : context;
|
|
160
|
+
const normalized = terminalSafeLine(raw);
|
|
161
|
+
const message = boundedHealthError(scrub(normalized, DEFAULT_RULES)) ?? context;
|
|
162
|
+
process.stderr.write(`[prim-hook] ${message === `${context}:` ? context : message}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
function redactEveryString(value, reason) {
|
|
166
|
+
return mapStrings(value, () => `<REDACTED:${reason}>`);
|
|
167
|
+
}
|
|
168
|
+
var WORKSPACE_REDACTION_WORKER = String.raw`
|
|
169
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
170
|
+
const REDACTED_OBJECT_KEY = "<REDACTED:object-key>";
|
|
171
|
+
const rules = workerData.rules.map((rule) => ({
|
|
172
|
+
pattern: new RegExp(rule.source, rule.flags),
|
|
173
|
+
reason: rule.reason,
|
|
174
|
+
}));
|
|
175
|
+
function redact(input) {
|
|
176
|
+
let output = input;
|
|
177
|
+
for (const rule of rules) {
|
|
178
|
+
output = output.replace(rule.pattern, "<REDACTED:" + rule.reason + ">");
|
|
179
|
+
}
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
function objectKey(input) {
|
|
183
|
+
return redact(input) === input ? input : REDACTED_OBJECT_KEY;
|
|
184
|
+
}
|
|
185
|
+
function uniqueKey(output, key) {
|
|
186
|
+
let candidate = key;
|
|
187
|
+
let suffix = 2;
|
|
188
|
+
while (Object.hasOwn(output, candidate)) candidate = key + "_" + suffix++;
|
|
189
|
+
return candidate;
|
|
190
|
+
}
|
|
191
|
+
function walk(value) {
|
|
192
|
+
if (typeof value === "string") {
|
|
193
|
+
return redact(value);
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(value)) return value.map(walk);
|
|
196
|
+
if (value && typeof value === "object") {
|
|
197
|
+
const output = Object.create(null);
|
|
198
|
+
for (const [key, item] of Object.entries(value)) {
|
|
199
|
+
output[uniqueKey(output, objectKey(key))] = walk(item);
|
|
200
|
+
}
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
parentPort.postMessage(walk(workerData.value));
|
|
206
|
+
`;
|
|
207
|
+
function applyWorkspaceRules(value, rules) {
|
|
208
|
+
const serialized = rules.map(({ pattern, reason }) => ({
|
|
209
|
+
source: pattern.source,
|
|
210
|
+
flags: pattern.flags,
|
|
211
|
+
reason
|
|
212
|
+
}));
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
let worker;
|
|
215
|
+
try {
|
|
216
|
+
worker = new Worker(WORKSPACE_REDACTION_WORKER, {
|
|
217
|
+
eval: true,
|
|
218
|
+
workerData: { value, rules: serialized }
|
|
219
|
+
});
|
|
220
|
+
} catch {
|
|
221
|
+
resolve(redactEveryString(value, "workspace-rule-failed"));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let settled = false;
|
|
225
|
+
const finish = (result) => {
|
|
226
|
+
if (settled) return;
|
|
227
|
+
settled = true;
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
void worker.terminate();
|
|
230
|
+
resolve(result);
|
|
231
|
+
};
|
|
232
|
+
const failClosed = () => finish(redactEveryString(value, "workspace-rule-failed"));
|
|
233
|
+
const timer = setTimeout(failClosed, WORKSPACE_RULE_TIMEOUT_MS);
|
|
234
|
+
worker.once("message", finish);
|
|
235
|
+
worker.once("error", failClosed);
|
|
236
|
+
worker.once("exit", (code) => {
|
|
237
|
+
if (code !== 0) failClosed();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async function scrubFromCwd(value, cwd) {
|
|
242
|
+
const defaultScrubbed = scrub(value, DEFAULT_RULES);
|
|
243
|
+
const workspaceRules = loadWorkspaceRules(cwd);
|
|
244
|
+
return workspaceRules.length === 0 ? defaultScrubbed : await applyWorkspaceRules(defaultScrubbed, workspaceRules);
|
|
245
|
+
}
|
|
246
|
+
function scrubEnvironmentPaths(environment) {
|
|
247
|
+
const scrubPath = (value) => value === void 0 ? void 0 : applyRules(value, USER_HOME_RULES);
|
|
248
|
+
return {
|
|
249
|
+
...environment,
|
|
250
|
+
cwd: scrubPath(environment.cwd) ?? environment.cwd,
|
|
251
|
+
...environment.repoRoot === void 0 ? {} : { repoRoot: scrubPath(environment.repoRoot) },
|
|
252
|
+
...environment.gitRoot === void 0 ? {} : { gitRoot: scrubPath(environment.gitRoot) }
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export {
|
|
257
|
+
DEFAULT_RULES,
|
|
258
|
+
scrub,
|
|
259
|
+
writeHookDebug,
|
|
260
|
+
scrubFromCwd,
|
|
261
|
+
scrubEnvironmentPaths
|
|
262
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isCanonicalWorkspaceId
|
|
3
|
+
} from "./chunk-UVIWZWM6.js";
|
|
4
|
+
import {
|
|
5
|
+
scrubEnvironmentPaths
|
|
6
|
+
} from "./chunk-FI3LIRCB.js";
|
|
7
|
+
import {
|
|
8
|
+
isValidRepoSyncId
|
|
9
|
+
} from "./chunk-TJTVHDJ5.js";
|
|
10
|
+
|
|
11
|
+
// src/hooks/prim-hook-core.ts
|
|
12
|
+
import { createHash, randomUUID } from "crypto";
|
|
13
|
+
import { platform } from "os";
|
|
14
|
+
|
|
15
|
+
// src/protocol/move.ts
|
|
16
|
+
var LEGACY_ENVELOPE_VERSION = 1;
|
|
17
|
+
var AGENT_ENVELOPE_VERSION = 3;
|
|
18
|
+
var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
|
|
19
|
+
|
|
20
|
+
// src/hooks/prim-hook-core.ts
|
|
21
|
+
var CORRELATED_TOOL_EVENTS = /* @__PURE__ */ new Set([
|
|
22
|
+
"PostToolUse",
|
|
23
|
+
"PostToolUseFailure",
|
|
24
|
+
"post_tool_call",
|
|
25
|
+
"post_approval_response"
|
|
26
|
+
]);
|
|
27
|
+
function postToolInvocationId(parsed, agent) {
|
|
28
|
+
if (typeof parsed.hook_event_name !== "string" || !CORRELATED_TOOL_EVENTS.has(parsed.hook_event_name)) {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
const raw = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
|
|
32
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
33
|
+
}
|
|
34
|
+
function lowerString(value) {
|
|
35
|
+
return typeof value === "string" ? value.toLowerCase() : void 0;
|
|
36
|
+
}
|
|
37
|
+
function toolOutcomeFor(parsed, agent) {
|
|
38
|
+
const event = parsed.hook_event_name;
|
|
39
|
+
if (agent === "claude_code") {
|
|
40
|
+
if (event === "PostToolUseFailure") {
|
|
41
|
+
return parsed.is_interrupt === true ? "interrupted" : "failed";
|
|
42
|
+
}
|
|
43
|
+
return event === "PostToolUse" ? "succeeded" : void 0;
|
|
44
|
+
}
|
|
45
|
+
if (agent === "codex") {
|
|
46
|
+
return event === "PostToolUse" ? "returned" : void 0;
|
|
47
|
+
}
|
|
48
|
+
const extra = parsed.extra;
|
|
49
|
+
if (event === "post_approval_response") {
|
|
50
|
+
const choice = lowerString(extra?.choice);
|
|
51
|
+
return (choice === "deny" || choice === "smart_deny" || choice === "timeout") && postToolInvocationId(parsed, agent) ? "prevented" : void 0;
|
|
52
|
+
}
|
|
53
|
+
if (event !== "PostToolUse" && event !== "post_tool_call") {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
const status = lowerString(extra?.status);
|
|
57
|
+
if (status === "ok" || status === "success") {
|
|
58
|
+
return "succeeded";
|
|
59
|
+
}
|
|
60
|
+
if (status === "error" || status === "timeout") {
|
|
61
|
+
return "failed";
|
|
62
|
+
}
|
|
63
|
+
if (status === "cancelled") {
|
|
64
|
+
return "interrupted";
|
|
65
|
+
}
|
|
66
|
+
if (status === "blocked") {
|
|
67
|
+
return "prevented";
|
|
68
|
+
}
|
|
69
|
+
return "unknown";
|
|
70
|
+
}
|
|
71
|
+
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, repository, invocationId) {
|
|
72
|
+
const sessionId = parsed.session_id ?? "";
|
|
73
|
+
const eventType = parsed.hook_event_name ?? "unknown";
|
|
74
|
+
const toolOutcome = toolOutcomeFor(parsed, agent);
|
|
75
|
+
let moveId;
|
|
76
|
+
if (CORRELATED_TOOL_EVENTS.has(eventType) && sessionId && invocationId) {
|
|
77
|
+
moveId = `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}`;
|
|
78
|
+
} else if (CORRELATED_TOOL_EVENTS.has(eventType) && sessionId) {
|
|
79
|
+
moveId = `posttool:fallback:v1:${createHash("sha256").update(
|
|
80
|
+
JSON.stringify([
|
|
81
|
+
agent,
|
|
82
|
+
sessionId,
|
|
83
|
+
eventType,
|
|
84
|
+
parsed.turn_id ?? null,
|
|
85
|
+
parsed.tool_name ?? null,
|
|
86
|
+
parsed.tool_input ?? null
|
|
87
|
+
])
|
|
88
|
+
).digest("hex")}`;
|
|
89
|
+
} else {
|
|
90
|
+
moveId = randomUUID();
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
moveId,
|
|
94
|
+
capturedAt: Date.now(),
|
|
95
|
+
sessionId,
|
|
96
|
+
eventType,
|
|
97
|
+
payload: parsed,
|
|
98
|
+
env: scrubEnvironmentPaths({
|
|
99
|
+
cwd: parsed.cwd ?? process.cwd(),
|
|
100
|
+
cliVersion,
|
|
101
|
+
osPlatform: platform(),
|
|
102
|
+
...repository ? {
|
|
103
|
+
repoRoot: repository.repoRoot,
|
|
104
|
+
repoKey: repository.repoKey,
|
|
105
|
+
gitRoot: repository.repoRoot,
|
|
106
|
+
repoFullName: repository.repoFullName,
|
|
107
|
+
repoSyncId: repository.repoSyncId
|
|
108
|
+
} : {},
|
|
109
|
+
...workspaceId ? { workspaceId } : {}
|
|
110
|
+
}),
|
|
111
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
112
|
+
producer: agent,
|
|
113
|
+
...invocationId ? { invocationId } : {},
|
|
114
|
+
...toolOutcome ? { toolOutcome } : {}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
var MAX_REWRITE_PAIRS_PER_MOVE = 40;
|
|
118
|
+
var MAX_REWRITE_PAIRS_PER_HOOK = 2e3;
|
|
119
|
+
var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
|
|
120
|
+
var MAX_CLASSIFIER_ID_CHARS = 256;
|
|
121
|
+
var INVALID_CLASSIFIER_ID_RE = /[\u0000-\u001f\u007f\uD800-\uDFFF]/u;
|
|
122
|
+
function validSessionHint(value) {
|
|
123
|
+
return typeof value === "string" && value.trim().length > 0 && value === value.trim() && value.length <= MAX_CLASSIFIER_ID_CHARS && !INVALID_CLASSIFIER_ID_RE.test(value);
|
|
124
|
+
}
|
|
125
|
+
function commitAttributionFromEnvironment(env, workspaceId) {
|
|
126
|
+
if (env.HERMES_INTERACTIVE) return void 0;
|
|
127
|
+
if (!workspaceId || !isCanonicalWorkspaceId(workspaceId)) return void 0;
|
|
128
|
+
const candidates = [
|
|
129
|
+
validSessionHint(env.CLAUDE_CODE_SESSION_ID) ? { sessionId: env.CLAUDE_CODE_SESSION_ID, producer: "claude_code" } : void 0,
|
|
130
|
+
validSessionHint(env.CODEX_THREAD_ID) ? { sessionId: env.CODEX_THREAD_ID, producer: "codex" } : void 0
|
|
131
|
+
].filter((candidate) => candidate !== void 0);
|
|
132
|
+
return candidates.length === 1 ? candidates[0] : void 0;
|
|
133
|
+
}
|
|
134
|
+
function toCommitMove(commit, cliVersion, cwd, context = {}) {
|
|
135
|
+
const { repository, repoFullName, repoSyncId, workspaceId, attribution, capturedAt } = context;
|
|
136
|
+
const validatedRepoSyncId = isValidRepoSyncId(repoSyncId) ? repoSyncId : void 0;
|
|
137
|
+
const validatedWorkspaceId = workspaceId && isCanonicalWorkspaceId(workspaceId) ? workspaceId : void 0;
|
|
138
|
+
const validatedAttribution = validatedWorkspaceId ? attribution : void 0;
|
|
139
|
+
const validatedCapturedAt = typeof capturedAt === "number" && Number.isSafeInteger(capturedAt) && capturedAt > 0 ? capturedAt : Date.now();
|
|
140
|
+
const fullSha = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(commit.sha);
|
|
141
|
+
const moveId = validatedRepoSyncId && validatedWorkspaceId && fullSha ? `commit:v2:${createHash("sha256").update(
|
|
142
|
+
JSON.stringify(["commit:v2", validatedRepoSyncId, validatedWorkspaceId, commit.sha])
|
|
143
|
+
).digest("hex")}` : `commit:v2:unscoped:${randomUUID()}`;
|
|
144
|
+
const base = {
|
|
145
|
+
moveId,
|
|
146
|
+
capturedAt: validatedCapturedAt,
|
|
147
|
+
sessionId: validatedAttribution?.sessionId ?? "",
|
|
148
|
+
eventType: "git.commit",
|
|
149
|
+
payload: {
|
|
150
|
+
kind: "git.commit",
|
|
151
|
+
sha: commit.sha,
|
|
152
|
+
parentSha: commit.parentSha,
|
|
153
|
+
branch: commit.branch,
|
|
154
|
+
changedFiles: commit.files,
|
|
155
|
+
changedFilesComplete: commit.filesComplete
|
|
156
|
+
},
|
|
157
|
+
env: scrubEnvironmentPaths({
|
|
158
|
+
cwd,
|
|
159
|
+
cliVersion,
|
|
160
|
+
osPlatform: platform(),
|
|
161
|
+
...repository ? {
|
|
162
|
+
repoRoot: repository.repoRoot,
|
|
163
|
+
repoKey: repository.repoKey,
|
|
164
|
+
gitRoot: repository.repoRoot
|
|
165
|
+
} : {},
|
|
166
|
+
...repoFullName ? { repoFullName } : {},
|
|
167
|
+
...validatedRepoSyncId ? { repoSyncId: validatedRepoSyncId } : {},
|
|
168
|
+
...validatedWorkspaceId ? { workspaceId: validatedWorkspaceId } : {}
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
return validatedAttribution ? {
|
|
172
|
+
...base,
|
|
173
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
174
|
+
producer: validatedAttribution.producer
|
|
175
|
+
} : { ...base, envelopeVersion: ENVELOPE_VERSION };
|
|
176
|
+
}
|
|
177
|
+
function canonicalRewritePairs(pairs) {
|
|
178
|
+
const unique = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const pair of pairs) {
|
|
180
|
+
const oldSha = pair.oldSha.toLowerCase();
|
|
181
|
+
const newSha = pair.newSha.toLowerCase();
|
|
182
|
+
if (!FULL_SHA_RE.test(oldSha) || !FULL_SHA_RE.test(newSha)) return null;
|
|
183
|
+
if (oldSha === newSha) continue;
|
|
184
|
+
unique.set(`${oldSha}\0${newSha}`, { oldSha, newSha });
|
|
185
|
+
}
|
|
186
|
+
return [...unique.values()].sort((left, right) => {
|
|
187
|
+
const leftKey = `${left.oldSha}\0${left.newSha}`;
|
|
188
|
+
const rightKey = `${right.oldSha}\0${right.newSha}`;
|
|
189
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
190
|
+
}).slice(0, MAX_REWRITE_PAIRS_PER_HOOK);
|
|
191
|
+
}
|
|
192
|
+
function toRewriteMove(rewrite, cliVersion, cwd, context) {
|
|
193
|
+
const { repository, repoFullName, repoSyncId, workspaceId, capturedAt } = context;
|
|
194
|
+
if (rewrite.source !== "amend" && rewrite.source !== "rebase" || !isValidRepoSyncId(repoSyncId) || !workspaceId || !isCanonicalWorkspaceId(workspaceId)) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const pairs = canonicalRewritePairs(rewrite.pairs);
|
|
198
|
+
if (!pairs || pairs.length === 0) return [];
|
|
199
|
+
const branch = rewrite.branch && rewrite.branch === rewrite.branch.trim() && ![...rewrite.branch].some((char) => char < " " || char === "\x7F") ? rewrite.branch : void 0;
|
|
200
|
+
const observedAt = typeof capturedAt === "number" && Number.isSafeInteger(capturedAt) && capturedAt > 0 ? capturedAt : Date.now();
|
|
201
|
+
const moves = [];
|
|
202
|
+
for (let offset = 0; offset < pairs.length; offset += MAX_REWRITE_PAIRS_PER_MOVE) {
|
|
203
|
+
const sortedPairs = pairs.slice(offset, offset + MAX_REWRITE_PAIRS_PER_MOVE);
|
|
204
|
+
const moveId = `rewrite:v1:${createHash("sha256").update(JSON.stringify(["rewrite:v1", repoSyncId, workspaceId, rewrite.source, sortedPairs])).digest("hex")}`;
|
|
205
|
+
moves.push({
|
|
206
|
+
moveId,
|
|
207
|
+
capturedAt: observedAt,
|
|
208
|
+
sessionId: "",
|
|
209
|
+
eventType: "git.rewrite",
|
|
210
|
+
payload: {
|
|
211
|
+
kind: "git.rewrite",
|
|
212
|
+
source: rewrite.source,
|
|
213
|
+
pairs: sortedPairs,
|
|
214
|
+
...branch ? { branch } : {}
|
|
215
|
+
},
|
|
216
|
+
env: scrubEnvironmentPaths({
|
|
217
|
+
cwd,
|
|
218
|
+
cliVersion,
|
|
219
|
+
osPlatform: platform(),
|
|
220
|
+
...repository ? {
|
|
221
|
+
repoRoot: repository.repoRoot,
|
|
222
|
+
repoKey: repository.repoKey,
|
|
223
|
+
gitRoot: repository.repoRoot
|
|
224
|
+
} : {},
|
|
225
|
+
...repoFullName ? { repoFullName } : {},
|
|
226
|
+
repoSyncId,
|
|
227
|
+
workspaceId
|
|
228
|
+
}),
|
|
229
|
+
envelopeVersion: ENVELOPE_VERSION
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return moves;
|
|
233
|
+
}
|
|
234
|
+
function shouldFlushAfter(eventType) {
|
|
235
|
+
return eventType === "SessionEnd";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
postToolInvocationId,
|
|
240
|
+
toolOutcomeFor,
|
|
241
|
+
toMove,
|
|
242
|
+
commitAttributionFromEnvironment,
|
|
243
|
+
toCommitMove,
|
|
244
|
+
canonicalRewritePairs,
|
|
245
|
+
toRewriteMove,
|
|
246
|
+
shouldFlushAfter
|
|
247
|
+
};
|