@primitive.ai/prim 0.1.0-alpha.9 → 0.1.0-alpha.91
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 +303 -37
- package/SKILL.md +269 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-5AFRMOPX.js +209 -0
- package/dist/chunk-5GYQ4J3D.js +126 -0
- package/dist/chunk-67VNLLBN.js +789 -0
- package/dist/chunk-BTNNEH2W.js +79 -0
- package/dist/chunk-EWZMDD44.js +1474 -0
- package/dist/chunk-EZXEWFDG.js +210 -0
- package/dist/chunk-FBAYCS3X.js +327 -0
- package/dist/chunk-FKXFUUWJ.js +36 -0
- package/dist/chunk-FOLKHXLJ.js +1529 -0
- package/dist/chunk-H4UNYFUC.js +196 -0
- package/dist/chunk-HU4PW6I6.js +264 -0
- package/dist/chunk-KFXF2V5G.js +155 -0
- package/dist/chunk-LFFLHSQO.js +78 -0
- package/dist/chunk-M6RIJVRE.js +18358 -0
- package/dist/chunk-NAHHFSLH.js +258 -0
- package/dist/chunk-OAAP2MY6.js +262 -0
- package/dist/chunk-PCVGBE5B.js +659 -0
- package/dist/chunk-VW3FDYRT.js +517 -0
- package/dist/chunk-WX52U7Z2.js +112 -0
- package/dist/chunk-ZB4F75SZ.js +843 -0
- package/dist/daemon/server.js +21633 -0
- package/dist/hook-runtime/daemon/server.js +21633 -0
- package/dist/hook-runtime/hooks/post-commit.js +19258 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +19273 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +26365 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +25832 -0
- package/dist/hook-runtime/hooks/prim-hook.js +26386 -0
- package/dist/hook-runtime/hooks/session-end.js +1078 -0
- package/dist/hook-runtime/hooks/session-start.js +28779 -0
- package/dist/hook-runtime/index.js +50987 -0
- package/dist/hook-runtime/statusline-main.js +809 -0
- package/dist/hooks/post-commit.js +190 -0
- package/dist/hooks/post-rewrite.js +203 -0
- package/dist/hooks/post-tool-use.js +288 -0
- package/dist/hooks/pre-commit.js +16 -101
- package/dist/hooks/pre-tool-use.js +369 -0
- package/dist/hooks/prim-hook.js +229 -0
- package/dist/hooks/session-end.js +59 -0
- package/dist/hooks/session-start.js +316 -0
- package/dist/index.js +8875 -429
- package/dist/statusline-main.js +809 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isCanonicalWorkspaceId
|
|
3
|
+
} from "./chunk-EZXEWFDG.js";
|
|
4
|
+
import {
|
|
5
|
+
scrubEnvironmentPaths
|
|
6
|
+
} from "./chunk-OAAP2MY6.js";
|
|
7
|
+
import {
|
|
8
|
+
isValidRepoSyncId
|
|
9
|
+
} from "./chunk-M6RIJVRE.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
|
+
if (agent === "cursor") {
|
|
49
|
+
if (event === "PostToolUse") return "succeeded";
|
|
50
|
+
if (event === "PostToolUseFailure") {
|
|
51
|
+
if (parsed.is_interrupt === true) return "interrupted";
|
|
52
|
+
const failureType = lowerString(parsed.failure_type);
|
|
53
|
+
if (failureType === "permission_denied") return "prevented";
|
|
54
|
+
if (failureType === "error" || failureType === "timeout") return "failed";
|
|
55
|
+
return "unknown";
|
|
56
|
+
}
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
const extra = parsed.extra;
|
|
60
|
+
if (event === "post_approval_response") {
|
|
61
|
+
const choice = lowerString(extra?.choice);
|
|
62
|
+
return (choice === "deny" || choice === "smart_deny" || choice === "timeout") && postToolInvocationId(parsed, agent) ? "prevented" : void 0;
|
|
63
|
+
}
|
|
64
|
+
if (event !== "PostToolUse" && event !== "post_tool_call") {
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
const status = lowerString(extra?.status);
|
|
68
|
+
if (status === "ok" || status === "success") {
|
|
69
|
+
return "succeeded";
|
|
70
|
+
}
|
|
71
|
+
if (status === "error" || status === "timeout") {
|
|
72
|
+
return "failed";
|
|
73
|
+
}
|
|
74
|
+
if (status === "cancelled") {
|
|
75
|
+
return "interrupted";
|
|
76
|
+
}
|
|
77
|
+
if (status === "blocked") {
|
|
78
|
+
return "prevented";
|
|
79
|
+
}
|
|
80
|
+
return "unknown";
|
|
81
|
+
}
|
|
82
|
+
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, repository, invocationId) {
|
|
83
|
+
const sessionId = parsed.session_id ?? "";
|
|
84
|
+
const eventType = parsed.hook_event_name ?? "unknown";
|
|
85
|
+
const toolOutcome = toolOutcomeFor(parsed, agent);
|
|
86
|
+
let moveId;
|
|
87
|
+
if (CORRELATED_TOOL_EVENTS.has(eventType) && sessionId && invocationId) {
|
|
88
|
+
moveId = `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}`;
|
|
89
|
+
} else if (CORRELATED_TOOL_EVENTS.has(eventType) && sessionId && agent !== "cursor") {
|
|
90
|
+
moveId = `posttool:fallback:v1:${createHash("sha256").update(
|
|
91
|
+
JSON.stringify([
|
|
92
|
+
agent,
|
|
93
|
+
sessionId,
|
|
94
|
+
eventType,
|
|
95
|
+
parsed.turn_id ?? null,
|
|
96
|
+
parsed.tool_name ?? null,
|
|
97
|
+
parsed.tool_input ?? null
|
|
98
|
+
])
|
|
99
|
+
).digest("hex")}`;
|
|
100
|
+
} else {
|
|
101
|
+
moveId = randomUUID();
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
moveId,
|
|
105
|
+
capturedAt: Date.now(),
|
|
106
|
+
sessionId,
|
|
107
|
+
eventType,
|
|
108
|
+
payload: parsed,
|
|
109
|
+
env: scrubEnvironmentPaths({
|
|
110
|
+
cwd: parsed.cwd ?? process.cwd(),
|
|
111
|
+
cliVersion,
|
|
112
|
+
osPlatform: platform(),
|
|
113
|
+
...repository ? {
|
|
114
|
+
repoRoot: repository.repoRoot,
|
|
115
|
+
repoKey: repository.repoKey,
|
|
116
|
+
gitRoot: repository.repoRoot,
|
|
117
|
+
repoFullName: repository.repoFullName,
|
|
118
|
+
repoSyncId: repository.repoSyncId
|
|
119
|
+
} : {},
|
|
120
|
+
...workspaceId ? { workspaceId } : {}
|
|
121
|
+
}),
|
|
122
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
123
|
+
producer: agent,
|
|
124
|
+
...invocationId ? { invocationId } : {},
|
|
125
|
+
...toolOutcome ? { toolOutcome } : {}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
var MAX_REWRITE_PAIRS_PER_MOVE = 40;
|
|
129
|
+
var MAX_REWRITE_PAIRS_PER_HOOK = 2e3;
|
|
130
|
+
var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
|
|
131
|
+
var MAX_CLASSIFIER_ID_CHARS = 256;
|
|
132
|
+
var INVALID_CLASSIFIER_ID_RE = /[\u0000-\u001f\u007f\uD800-\uDFFF]/u;
|
|
133
|
+
function validSessionHint(value) {
|
|
134
|
+
return typeof value === "string" && value.trim().length > 0 && value === value.trim() && value.length <= MAX_CLASSIFIER_ID_CHARS && !INVALID_CLASSIFIER_ID_RE.test(value);
|
|
135
|
+
}
|
|
136
|
+
function commitAttributionFromEnvironment(env, workspaceId) {
|
|
137
|
+
if (env.HERMES_INTERACTIVE) return void 0;
|
|
138
|
+
if (!workspaceId || !isCanonicalWorkspaceId(workspaceId)) return void 0;
|
|
139
|
+
const candidates = [
|
|
140
|
+
validSessionHint(env.CLAUDE_CODE_SESSION_ID) ? { sessionId: env.CLAUDE_CODE_SESSION_ID, producer: "claude_code" } : void 0,
|
|
141
|
+
validSessionHint(env.CODEX_THREAD_ID) ? { sessionId: env.CODEX_THREAD_ID, producer: "codex" } : void 0
|
|
142
|
+
].filter((candidate) => candidate !== void 0);
|
|
143
|
+
return candidates.length === 1 ? candidates[0] : void 0;
|
|
144
|
+
}
|
|
145
|
+
function toCommitMove(commit, cliVersion, cwd, context = {}) {
|
|
146
|
+
const { repository, repoFullName, repoSyncId, workspaceId, attribution, capturedAt } = context;
|
|
147
|
+
const validatedRepoSyncId = isValidRepoSyncId(repoSyncId) ? repoSyncId : void 0;
|
|
148
|
+
const validatedWorkspaceId = workspaceId && isCanonicalWorkspaceId(workspaceId) ? workspaceId : void 0;
|
|
149
|
+
const validatedAttribution = validatedWorkspaceId ? attribution : void 0;
|
|
150
|
+
const validatedCapturedAt = typeof capturedAt === "number" && Number.isSafeInteger(capturedAt) && capturedAt > 0 ? capturedAt : Date.now();
|
|
151
|
+
const fullSha = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(commit.sha);
|
|
152
|
+
const moveId = validatedRepoSyncId && validatedWorkspaceId && fullSha ? `commit:v2:${createHash("sha256").update(
|
|
153
|
+
JSON.stringify(["commit:v2", validatedRepoSyncId, validatedWorkspaceId, commit.sha])
|
|
154
|
+
).digest("hex")}` : `commit:v2:unscoped:${randomUUID()}`;
|
|
155
|
+
const base = {
|
|
156
|
+
moveId,
|
|
157
|
+
capturedAt: validatedCapturedAt,
|
|
158
|
+
sessionId: validatedAttribution?.sessionId ?? "",
|
|
159
|
+
eventType: "git.commit",
|
|
160
|
+
payload: {
|
|
161
|
+
kind: "git.commit",
|
|
162
|
+
sha: commit.sha,
|
|
163
|
+
parentSha: commit.parentSha,
|
|
164
|
+
branch: commit.branch,
|
|
165
|
+
changedFiles: commit.files,
|
|
166
|
+
changedFilesComplete: commit.filesComplete
|
|
167
|
+
},
|
|
168
|
+
env: scrubEnvironmentPaths({
|
|
169
|
+
cwd,
|
|
170
|
+
cliVersion,
|
|
171
|
+
osPlatform: platform(),
|
|
172
|
+
...repository ? {
|
|
173
|
+
repoRoot: repository.repoRoot,
|
|
174
|
+
repoKey: repository.repoKey,
|
|
175
|
+
gitRoot: repository.repoRoot
|
|
176
|
+
} : {},
|
|
177
|
+
...repoFullName ? { repoFullName } : {},
|
|
178
|
+
...validatedRepoSyncId ? { repoSyncId: validatedRepoSyncId } : {},
|
|
179
|
+
...validatedWorkspaceId ? { workspaceId: validatedWorkspaceId } : {}
|
|
180
|
+
})
|
|
181
|
+
};
|
|
182
|
+
return validatedAttribution ? {
|
|
183
|
+
...base,
|
|
184
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
185
|
+
producer: validatedAttribution.producer
|
|
186
|
+
} : { ...base, envelopeVersion: ENVELOPE_VERSION };
|
|
187
|
+
}
|
|
188
|
+
function canonicalRewritePairs(pairs) {
|
|
189
|
+
const unique = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const pair of pairs) {
|
|
191
|
+
const oldSha = pair.oldSha.toLowerCase();
|
|
192
|
+
const newSha = pair.newSha.toLowerCase();
|
|
193
|
+
if (!FULL_SHA_RE.test(oldSha) || !FULL_SHA_RE.test(newSha)) return null;
|
|
194
|
+
if (oldSha === newSha) continue;
|
|
195
|
+
unique.set(`${oldSha}\0${newSha}`, { oldSha, newSha });
|
|
196
|
+
}
|
|
197
|
+
return [...unique.values()].sort((left, right) => {
|
|
198
|
+
const leftKey = `${left.oldSha}\0${left.newSha}`;
|
|
199
|
+
const rightKey = `${right.oldSha}\0${right.newSha}`;
|
|
200
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
201
|
+
}).slice(0, MAX_REWRITE_PAIRS_PER_HOOK);
|
|
202
|
+
}
|
|
203
|
+
function toRewriteMove(rewrite, cliVersion, cwd, context) {
|
|
204
|
+
const { repository, repoFullName, repoSyncId, workspaceId, capturedAt } = context;
|
|
205
|
+
if (rewrite.source !== "amend" && rewrite.source !== "rebase" || !isValidRepoSyncId(repoSyncId) || !workspaceId || !isCanonicalWorkspaceId(workspaceId)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
const pairs = canonicalRewritePairs(rewrite.pairs);
|
|
209
|
+
if (!pairs || pairs.length === 0) return [];
|
|
210
|
+
const branch = rewrite.branch && rewrite.branch === rewrite.branch.trim() && ![...rewrite.branch].some((char) => char < " " || char === "\x7F") ? rewrite.branch : void 0;
|
|
211
|
+
const observedAt = typeof capturedAt === "number" && Number.isSafeInteger(capturedAt) && capturedAt > 0 ? capturedAt : Date.now();
|
|
212
|
+
const moves = [];
|
|
213
|
+
for (let offset = 0; offset < pairs.length; offset += MAX_REWRITE_PAIRS_PER_MOVE) {
|
|
214
|
+
const sortedPairs = pairs.slice(offset, offset + MAX_REWRITE_PAIRS_PER_MOVE);
|
|
215
|
+
const moveId = `rewrite:v1:${createHash("sha256").update(JSON.stringify(["rewrite:v1", repoSyncId, workspaceId, rewrite.source, sortedPairs])).digest("hex")}`;
|
|
216
|
+
moves.push({
|
|
217
|
+
moveId,
|
|
218
|
+
capturedAt: observedAt,
|
|
219
|
+
sessionId: "",
|
|
220
|
+
eventType: "git.rewrite",
|
|
221
|
+
payload: {
|
|
222
|
+
kind: "git.rewrite",
|
|
223
|
+
source: rewrite.source,
|
|
224
|
+
pairs: sortedPairs,
|
|
225
|
+
...branch ? { branch } : {}
|
|
226
|
+
},
|
|
227
|
+
env: scrubEnvironmentPaths({
|
|
228
|
+
cwd,
|
|
229
|
+
cliVersion,
|
|
230
|
+
osPlatform: platform(),
|
|
231
|
+
...repository ? {
|
|
232
|
+
repoRoot: repository.repoRoot,
|
|
233
|
+
repoKey: repository.repoKey,
|
|
234
|
+
gitRoot: repository.repoRoot
|
|
235
|
+
} : {},
|
|
236
|
+
...repoFullName ? { repoFullName } : {},
|
|
237
|
+
repoSyncId,
|
|
238
|
+
workspaceId
|
|
239
|
+
}),
|
|
240
|
+
envelopeVersion: ENVELOPE_VERSION
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return moves;
|
|
244
|
+
}
|
|
245
|
+
function shouldFlushAfter(eventType, agent = "claude_code") {
|
|
246
|
+
return eventType === "SessionEnd" || agent === "cursor" && eventType === "Stop";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export {
|
|
250
|
+
postToolInvocationId,
|
|
251
|
+
toolOutcomeFor,
|
|
252
|
+
toMove,
|
|
253
|
+
commitAttributionFromEnvironment,
|
|
254
|
+
toCommitMove,
|
|
255
|
+
canonicalRewritePairs,
|
|
256
|
+
toRewriteMove,
|
|
257
|
+
shouldFlushAfter
|
|
258
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boundedHealthError
|
|
3
|
+
} from "./chunk-BTNNEH2W.js";
|
|
4
|
+
import {
|
|
5
|
+
terminalSafeLine
|
|
6
|
+
} from "./chunk-PCVGBE5B.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
|
+
};
|