@primitive.ai/prim 0.1.0-alpha.61 → 0.1.0-alpha.63
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-I3HIPAIR.js → chunk-CPX3BUOC.js} +7 -4
- package/dist/chunk-DA4BMHEB.js +77 -0
- package/dist/{chunk-B4SHJYIN.js → chunk-GF5QII7T.js} +83 -110
- package/dist/{chunk-S4LFRVGD.js → chunk-HIMWM526.js} +1 -1
- package/dist/{chunk-YIMUIPMD.js → chunk-KBFFVYBC.js} +7 -1
- package/dist/{chunk-R4CUX6FY.js → chunk-KHZNYEUV.js} +1 -1
- package/dist/chunk-QGVHLP5J.js +117 -0
- package/dist/{chunk-IMAIBPUC.js → chunk-UVIWZWM6.js} +1 -0
- package/dist/chunk-WNIY6Z6N.js +1244 -0
- package/dist/{chunk-YITJ2H3B.js → chunk-WS3MCNFD.js} +1 -1
- package/dist/chunk-XTNMSS4C.js +283 -0
- package/dist/daemon/server.js +343 -44
- package/dist/hooks/post-commit.js +84 -18
- package/dist/hooks/post-tool-use.js +42 -26
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +103 -11
- package/dist/hooks/prim-hook.js +33 -12
- package/dist/hooks/session-start.js +29 -8
- package/dist/index.js +361 -155
- package/dist/statusline-main.js +57 -42
- package/package.json +1 -1
- package/dist/chunk-GUXBUBQ7.js +0 -84
- package/dist/chunk-LDQZASIF.js +0 -166
- package/dist/chunk-N4R7EXZT.js +0 -620
- package/dist/chunk-YDBZTFW5.js +0 -67
|
@@ -153,10 +153,10 @@ var CREDENTIAL_FILE_MODE = 384;
|
|
|
153
153
|
var REFRESH_THRESHOLD_MS = 6e4;
|
|
154
154
|
var DEFAULT_API_URL = "https://api.getprimitive.ai";
|
|
155
155
|
var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
|
|
156
|
-
function loadEnvFile() {
|
|
156
|
+
function loadEnvFile(cwd = process.cwd()) {
|
|
157
157
|
const envVars = {};
|
|
158
158
|
for (const file of [".env.local", ".env"]) {
|
|
159
|
-
const filePath = resolve(
|
|
159
|
+
const filePath = resolve(cwd, file);
|
|
160
160
|
if (!existsSync2(filePath)) continue;
|
|
161
161
|
const content = readFileSync2(filePath, "utf-8");
|
|
162
162
|
for (const line of content.split("\n")) {
|
|
@@ -194,8 +194,11 @@ function resolveAuthCredential() {
|
|
|
194
194
|
return envToken ? { token: envToken, source: "env_file" } : void 0;
|
|
195
195
|
}
|
|
196
196
|
function getSiteUrl() {
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
return getSiteUrlForCwd(process.cwd(), process.env.PRIM_API_URL);
|
|
198
|
+
}
|
|
199
|
+
function getSiteUrlForCwd(cwd, primApiUrl) {
|
|
200
|
+
if (primApiUrl) return primApiUrl;
|
|
201
|
+
return loadEnvFile(cwd).PRIM_API_URL ?? DEFAULT_API_URL;
|
|
199
202
|
}
|
|
200
203
|
function getJwtExpiry(token) {
|
|
201
204
|
const parts = token.split(".");
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeShellTargets,
|
|
3
|
+
extractFileTargets
|
|
4
|
+
} from "./chunk-GF5QII7T.js";
|
|
5
|
+
import {
|
|
6
|
+
canonicalRepositoryPath
|
|
7
|
+
} from "./chunk-XTNMSS4C.js";
|
|
8
|
+
|
|
9
|
+
// src/hooks/file-refs.ts
|
|
10
|
+
var MAX_PREFLIGHT_FILE_TARGETS = 25;
|
|
11
|
+
function primitiveMetadataForResolution(resolution) {
|
|
12
|
+
const shouldAttach = resolution.fileRefs.length > 0 || resolution.rejected.length > 0 || resolution.targetsIncomplete || resolution.targetsTruncated || resolution.shellMutation !== void 0;
|
|
13
|
+
if (!shouldAttach) return;
|
|
14
|
+
return {
|
|
15
|
+
fileRefs: [...resolution.fileRefs],
|
|
16
|
+
...resolution.rejected.length > 0 || resolution.targetsIncomplete ? { fileRefsIncomplete: true } : {},
|
|
17
|
+
...resolution.targetsTruncated ? { fileRefsTruncated: true } : {},
|
|
18
|
+
...resolution.shellMutation !== void 0 ? { shellMutation: resolution.shellMutation } : {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function preserveHookFileMetadata(payload, resolution) {
|
|
22
|
+
const primitive = primitiveMetadataForResolution(resolution);
|
|
23
|
+
if (!primitive || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
24
|
+
return payload;
|
|
25
|
+
}
|
|
26
|
+
return { ...payload, primitive };
|
|
27
|
+
}
|
|
28
|
+
function resolveHookFileRefs(args) {
|
|
29
|
+
const command = args.agent === "claude_code" && args.toolName === "Bash" && args.toolInput && typeof args.toolInput === "object" && typeof args.toolInput.command === "string" ? args.toolInput.command : void 0;
|
|
30
|
+
const shell = command === void 0 ? void 0 : analyzeShellTargets(command);
|
|
31
|
+
const shellMutation = shell?.mutation === "none" ? "none" : shell?.coverage === "complete" ? "resolved" : shell ? "unresolved" : void 0;
|
|
32
|
+
const readPath = args.agent === "claude_code" && args.toolName === "Read" && args.toolInput && typeof args.toolInput === "object" && typeof args.toolInput.file_path === "string" ? [args.toolInput.file_path] : void 0;
|
|
33
|
+
const nativeTargets = shell ? null : readPath ? { paths: readPath, complete: true } : extractFileTargets(args.toolName, args.toolInput, args.agent);
|
|
34
|
+
const rawPaths = shell ? shellMutation === "unresolved" ? [] : shell.paths : nativeTargets?.paths ?? [];
|
|
35
|
+
const uniqueRawPaths = Array.from(new Set(rawPaths));
|
|
36
|
+
const targetsTruncated = uniqueRawPaths.length > MAX_PREFLIGHT_FILE_TARGETS;
|
|
37
|
+
const fileRefs = /* @__PURE__ */ new Set();
|
|
38
|
+
const rejected = [];
|
|
39
|
+
for (const path of uniqueRawPaths.slice(0, MAX_PREFLIGHT_FILE_TARGETS)) {
|
|
40
|
+
const canonical = canonicalRepositoryPath(path, args.repository, args.cwd);
|
|
41
|
+
if (canonical.ok) fileRefs.add(canonical.file);
|
|
42
|
+
else rejected.push({ path, reason: canonical.reason });
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
fileRefs: [...fileRefs],
|
|
46
|
+
rejected,
|
|
47
|
+
targetsIncomplete: nativeTargets?.complete === false,
|
|
48
|
+
targetsTruncated,
|
|
49
|
+
...shellMutation ? { shellMutation: targetsTruncated ? "unresolved" : shellMutation } : {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function enrichHookPayloadWithFileRefs(args) {
|
|
53
|
+
const toolName = typeof args.parsed.tool_name === "string" ? args.parsed.tool_name : "";
|
|
54
|
+
const resolution = resolveHookFileRefs({
|
|
55
|
+
toolName,
|
|
56
|
+
toolInput: args.parsed.tool_input,
|
|
57
|
+
agent: args.agent,
|
|
58
|
+
cwd: args.cwd,
|
|
59
|
+
repository: args.repository
|
|
60
|
+
});
|
|
61
|
+
const primitive = primitiveMetadataForResolution(resolution);
|
|
62
|
+
if (!primitive) return { parsed: args.parsed, resolution };
|
|
63
|
+
return {
|
|
64
|
+
parsed: {
|
|
65
|
+
...args.parsed,
|
|
66
|
+
// An explicit empty list is authoritative when every exposed target was
|
|
67
|
+
// rejected. It prevents backend fallback to unsafe raw tool input.
|
|
68
|
+
primitive
|
|
69
|
+
},
|
|
70
|
+
resolution
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
preserveHookFileMetadata,
|
|
76
|
+
enrichHookPayloadWithFileRefs
|
|
77
|
+
};
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_RULES,
|
|
3
|
-
scrub
|
|
4
|
-
} from "./chunk-GUXBUBQ7.js";
|
|
5
|
-
import {
|
|
6
|
-
canonicalGitRoot,
|
|
7
|
-
canonicalRepositoryPath
|
|
8
|
-
} from "./chunk-LDQZASIF.js";
|
|
9
|
-
import {
|
|
10
|
-
getClient
|
|
11
|
-
} from "./chunk-I3HIPAIR.js";
|
|
12
|
-
|
|
13
1
|
// src/hooks/pre-tool-use-scoring.ts
|
|
14
2
|
import { isAbsolute, relative, sep } from "path";
|
|
15
3
|
function anyUnverified(results) {
|
|
@@ -211,8 +199,84 @@ function demoteForMode(verdict, mode) {
|
|
|
211
199
|
return verdict;
|
|
212
200
|
}
|
|
213
201
|
|
|
214
|
-
// src/hooks/
|
|
215
|
-
import {
|
|
202
|
+
// src/hooks/redact.ts
|
|
203
|
+
import { existsSync, readFileSync } from "fs";
|
|
204
|
+
import { join } from "path";
|
|
205
|
+
var DEFAULT_RULES = [
|
|
206
|
+
{ pattern: /Bearer\s+[A-Za-z0-9._\-]+/g, reason: "bearer-token" },
|
|
207
|
+
{ pattern: /sk-[A-Za-z0-9]{32,}/g, reason: "sk-api-key" },
|
|
208
|
+
{
|
|
209
|
+
pattern: /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g,
|
|
210
|
+
reason: "private-key"
|
|
211
|
+
},
|
|
212
|
+
{ pattern: /xox[abprs]-[A-Za-z0-9-]{10,}/g, reason: "slack-token" },
|
|
213
|
+
{ pattern: /ghp_[A-Za-z0-9]{36,}/g, reason: "github-pat" }
|
|
214
|
+
];
|
|
215
|
+
var MAX_SCRUB_LEN = 256e3;
|
|
216
|
+
var WORKSPACE_FILE = ".prim/redaction.json";
|
|
217
|
+
function debugRedaction(message, err) {
|
|
218
|
+
if (!process.env.PRIM_HOOK_DEBUG) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
222
|
+
process.stderr.write(`[prim-hook] redaction: ${message}: ${detail}
|
|
223
|
+
`);
|
|
224
|
+
}
|
|
225
|
+
function loadWorkspaceRules(cwd) {
|
|
226
|
+
const path = join(cwd, WORKSPACE_FILE);
|
|
227
|
+
if (!existsSync(path)) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
let cfg;
|
|
231
|
+
try {
|
|
232
|
+
cfg = JSON.parse(readFileSync(path, "utf-8"));
|
|
233
|
+
} catch (err) {
|
|
234
|
+
debugRedaction(`ignoring unparseable ${WORKSPACE_FILE}`, err);
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
if (!cfg.rules) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const compiled = [];
|
|
241
|
+
for (const r of cfg.rules) {
|
|
242
|
+
try {
|
|
243
|
+
compiled.push({ pattern: new RegExp(r.pattern, r.flags ?? "g"), reason: r.reason });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
debugRedaction(`skipping invalid redaction rule "${r.reason}"`, err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return compiled;
|
|
249
|
+
}
|
|
250
|
+
function applyRules(input, rules) {
|
|
251
|
+
if (input.length > MAX_SCRUB_LEN) {
|
|
252
|
+
return "<REDACTED:oversized>";
|
|
253
|
+
}
|
|
254
|
+
let out = input;
|
|
255
|
+
for (const rule of rules) {
|
|
256
|
+
out = out.replace(rule.pattern, `<REDACTED:${rule.reason}>`);
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function scrub(value, rules = DEFAULT_RULES) {
|
|
261
|
+
if (typeof value === "string") {
|
|
262
|
+
return applyRules(value, rules);
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
return value.map((v) => scrub(v, rules));
|
|
266
|
+
}
|
|
267
|
+
if (value && typeof value === "object") {
|
|
268
|
+
const out = {};
|
|
269
|
+
for (const [k, v] of Object.entries(value)) {
|
|
270
|
+
out[k] = scrub(v, rules);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
function scrubFromCwd(value, cwd) {
|
|
277
|
+
const rules = [...DEFAULT_RULES, ...loadWorkspaceRules(cwd)];
|
|
278
|
+
return scrub(value, rules);
|
|
279
|
+
}
|
|
216
280
|
|
|
217
281
|
// src/hooks/shell-targets.ts
|
|
218
282
|
import { parse } from "unbash";
|
|
@@ -330,96 +394,6 @@ function analyzeShellTargets(source) {
|
|
|
330
394
|
}
|
|
331
395
|
}
|
|
332
396
|
|
|
333
|
-
// src/hooks/preflight-v3.ts
|
|
334
|
-
var PREFLIGHT_PROTOCOL_VERSION = 3;
|
|
335
|
-
var PREFLIGHT_TIMEOUT_MS = 6500;
|
|
336
|
-
var MAX_PREFLIGHT_PATHS = 32;
|
|
337
|
-
var MAX_PROPOSAL_BYTES = 6144;
|
|
338
|
-
function resolvePreflightTargets(args) {
|
|
339
|
-
let rawPaths;
|
|
340
|
-
let coverage = "complete";
|
|
341
|
-
let mutation = "present";
|
|
342
|
-
if (args.agent === "claude_code" && args.toolName === "Bash") {
|
|
343
|
-
const command = args.toolInput && typeof args.toolInput === "object" ? args.toolInput.command : void 0;
|
|
344
|
-
if (typeof command !== "string") {
|
|
345
|
-
return { paths: [], coverage: "unverified", mutation: "present" };
|
|
346
|
-
}
|
|
347
|
-
const shell = analyzeShellTargets(command);
|
|
348
|
-
rawPaths = shell.paths;
|
|
349
|
-
coverage = shell.coverage;
|
|
350
|
-
mutation = shell.mutation;
|
|
351
|
-
} else {
|
|
352
|
-
const extracted = extractFileTargets(args.toolName, args.toolInput, args.agent);
|
|
353
|
-
if (!extracted) return { paths: [], coverage: "complete", mutation: "none" };
|
|
354
|
-
rawPaths = extracted.paths;
|
|
355
|
-
if (!extracted.complete) coverage = "unverified";
|
|
356
|
-
}
|
|
357
|
-
const root = canonicalGitRoot(args.cwd);
|
|
358
|
-
const paths = /* @__PURE__ */ new Set();
|
|
359
|
-
for (const rawPath of rawPaths) {
|
|
360
|
-
const canonical = canonicalRepositoryPath(rawPath, args.cwd, root);
|
|
361
|
-
if (canonical) paths.add(canonical);
|
|
362
|
-
else coverage = "unverified";
|
|
363
|
-
}
|
|
364
|
-
const bounded = [...paths];
|
|
365
|
-
if (bounded.length > MAX_PREFLIGHT_PATHS) coverage = "unverified";
|
|
366
|
-
return { paths: bounded.slice(0, MAX_PREFLIGHT_PATHS), coverage, mutation };
|
|
367
|
-
}
|
|
368
|
-
function truncateUtf8(value, maxBytes) {
|
|
369
|
-
const encoded = Buffer.from(value);
|
|
370
|
-
if (encoded.byteLength <= maxBytes) return value;
|
|
371
|
-
return encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
|
|
372
|
-
}
|
|
373
|
-
function proposalFor(toolInput) {
|
|
374
|
-
const redacted = scrub(toolInput, DEFAULT_RULES);
|
|
375
|
-
return truncateUtf8(JSON.stringify(redacted) ?? "", MAX_PROPOSAL_BYTES);
|
|
376
|
-
}
|
|
377
|
-
function parsePreflightResponse(value) {
|
|
378
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
379
|
-
const response = value;
|
|
380
|
-
const verdicts = /* @__PURE__ */ new Set(["allow", "warn", "ask", "block", "unavailable"]);
|
|
381
|
-
if (response.protocolVersion !== PREFLIGHT_PROTOCOL_VERSION || typeof response.verdict !== "string" || !verdicts.has(response.verdict) || typeof response.reasonCode !== "string" || typeof response.message !== "string" || !Array.isArray(response.conflicts) || !Array.isArray(response.bypassed)) {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
return response;
|
|
385
|
-
}
|
|
386
|
-
async function requestPreflight(request, signal = AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS)) {
|
|
387
|
-
const response = await getClient().post("/api/cli/decisions/conflict-check", request, {
|
|
388
|
-
signal,
|
|
389
|
-
quietRefresh: true
|
|
390
|
-
});
|
|
391
|
-
return parsePreflightResponse(response);
|
|
392
|
-
}
|
|
393
|
-
function resultForPreflight(response) {
|
|
394
|
-
const verdict = response.verdict === "block" ? "deny" : response.verdict;
|
|
395
|
-
const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
396
|
-
const directives = response.conflicts.flatMap((value) => {
|
|
397
|
-
if (!value || typeof value !== "object") return [];
|
|
398
|
-
const { decisionId, shortId } = value;
|
|
399
|
-
const id = typeof shortId === "string" && safe.test(shortId) ? `dec_${shortId}` : typeof decisionId === "string" && safe.test(decisionId) ? decisionId : void 0;
|
|
400
|
-
return id ? [`To reconcile, run: prim reconcile ${id}`] : [];
|
|
401
|
-
});
|
|
402
|
-
const reason = [response.message, ...new Set(directives)].filter(Boolean).join("\n");
|
|
403
|
-
return {
|
|
404
|
-
verdict,
|
|
405
|
-
conflicts: response.conflicts,
|
|
406
|
-
reason,
|
|
407
|
-
additionalContext: ["warn", "ask", "block"].includes(response.verdict) ? reason : "",
|
|
408
|
-
truncated: false,
|
|
409
|
-
unavailable: response.verdict === "unavailable" ? response.message || response.reasonCode : void 0
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
function unverifiedResult(message) {
|
|
413
|
-
return {
|
|
414
|
-
verdict: "unavailable",
|
|
415
|
-
conflicts: [],
|
|
416
|
-
reason: "",
|
|
417
|
-
additionalContext: "",
|
|
418
|
-
truncated: false,
|
|
419
|
-
unavailable: message
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
397
|
export {
|
|
424
398
|
buildHookOutput,
|
|
425
399
|
buildCodexOutput,
|
|
@@ -427,12 +401,11 @@ export {
|
|
|
427
401
|
failOpenCodex,
|
|
428
402
|
buildHermesOutput,
|
|
429
403
|
failOpenHermes,
|
|
404
|
+
extractFileTargets,
|
|
430
405
|
readHookMode,
|
|
431
406
|
demoteForMode,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
resultForPreflight,
|
|
437
|
-
unverifiedResult
|
|
407
|
+
analyzeShellTargets,
|
|
408
|
+
DEFAULT_RULES,
|
|
409
|
+
scrub,
|
|
410
|
+
scrubFromCwd
|
|
438
411
|
};
|
|
@@ -76,7 +76,13 @@ function commandMatchesBin(command, bin) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// src/lib/bin-cache.ts
|
|
79
|
-
var CACHED_BINS = [
|
|
79
|
+
var CACHED_BINS = [
|
|
80
|
+
"prim",
|
|
81
|
+
"prim-hook",
|
|
82
|
+
"prim-pre-tool-use",
|
|
83
|
+
"prim-post-tool-use",
|
|
84
|
+
"prim-post-commit"
|
|
85
|
+
];
|
|
80
86
|
function binCacheDir() {
|
|
81
87
|
const base = process.env.XDG_CACHE_HOME || join2(homedir(), ".cache");
|
|
82
88
|
return join2(base, "prim", "bin");
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isCanonicalWorkspaceId
|
|
3
|
+
} from "./chunk-UVIWZWM6.js";
|
|
4
|
+
import {
|
|
5
|
+
isValidRepoSyncId
|
|
6
|
+
} from "./chunk-XTNMSS4C.js";
|
|
7
|
+
|
|
8
|
+
// src/hooks/prim-hook-core.ts
|
|
9
|
+
import { createHash, randomUUID } from "crypto";
|
|
10
|
+
import { platform } from "os";
|
|
11
|
+
|
|
12
|
+
// src/protocol/move.ts
|
|
13
|
+
var LEGACY_ENVELOPE_VERSION = 1;
|
|
14
|
+
var AGENT_ENVELOPE_VERSION = 3;
|
|
15
|
+
var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
|
|
16
|
+
|
|
17
|
+
// src/hooks/prim-hook-core.ts
|
|
18
|
+
function postToolInvocationId(parsed, agent) {
|
|
19
|
+
if (parsed.hook_event_name !== "PostToolUse") return void 0;
|
|
20
|
+
const raw = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
|
|
21
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
22
|
+
}
|
|
23
|
+
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, repository, invocationId) {
|
|
24
|
+
const sessionId = parsed.session_id ?? "";
|
|
25
|
+
const eventType = parsed.hook_event_name ?? "unknown";
|
|
26
|
+
const moveId = eventType === "PostToolUse" && sessionId && invocationId ? `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}` : randomUUID();
|
|
27
|
+
return {
|
|
28
|
+
moveId,
|
|
29
|
+
capturedAt: Date.now(),
|
|
30
|
+
sessionId,
|
|
31
|
+
eventType,
|
|
32
|
+
payload: parsed,
|
|
33
|
+
env: {
|
|
34
|
+
cwd: parsed.cwd ?? process.cwd(),
|
|
35
|
+
cliVersion,
|
|
36
|
+
osPlatform: platform(),
|
|
37
|
+
...repository ? {
|
|
38
|
+
repoRoot: repository.repoRoot,
|
|
39
|
+
repoKey: repository.repoKey,
|
|
40
|
+
gitRoot: repository.repoRoot,
|
|
41
|
+
repoFullName: repository.repoFullName,
|
|
42
|
+
repoSyncId: repository.repoSyncId
|
|
43
|
+
} : {},
|
|
44
|
+
...workspaceId ? { workspaceId } : {}
|
|
45
|
+
},
|
|
46
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
47
|
+
producer: agent,
|
|
48
|
+
...invocationId ? { invocationId } : {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
var MAX_CLASSIFIER_ID_CHARS = 256;
|
|
52
|
+
var INVALID_CLASSIFIER_ID_RE = /[\u0000-\u001f\u007f\uD800-\uDFFF]/u;
|
|
53
|
+
function validSessionHint(value) {
|
|
54
|
+
return typeof value === "string" && value.trim().length > 0 && value === value.trim() && value.length <= MAX_CLASSIFIER_ID_CHARS && !INVALID_CLASSIFIER_ID_RE.test(value);
|
|
55
|
+
}
|
|
56
|
+
function commitAttributionFromEnvironment(env, workspaceId) {
|
|
57
|
+
if (env.HERMES_INTERACTIVE) return void 0;
|
|
58
|
+
if (!workspaceId || !isCanonicalWorkspaceId(workspaceId)) return void 0;
|
|
59
|
+
const candidates = [
|
|
60
|
+
validSessionHint(env.CLAUDE_CODE_SESSION_ID) ? { sessionId: env.CLAUDE_CODE_SESSION_ID, producer: "claude_code" } : void 0,
|
|
61
|
+
validSessionHint(env.CODEX_THREAD_ID) ? { sessionId: env.CODEX_THREAD_ID, producer: "codex" } : void 0
|
|
62
|
+
].filter((candidate) => candidate !== void 0);
|
|
63
|
+
return candidates.length === 1 ? candidates[0] : void 0;
|
|
64
|
+
}
|
|
65
|
+
function toCommitMove(commit, cliVersion, cwd, context = {}) {
|
|
66
|
+
const { repository, repoFullName, repoSyncId, workspaceId, attribution } = context;
|
|
67
|
+
const validatedRepoSyncId = isValidRepoSyncId(repoSyncId) ? repoSyncId : void 0;
|
|
68
|
+
const validatedWorkspaceId = workspaceId && isCanonicalWorkspaceId(workspaceId) ? workspaceId : void 0;
|
|
69
|
+
const validatedAttribution = validatedWorkspaceId ? attribution : void 0;
|
|
70
|
+
const fullSha = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(commit.sha);
|
|
71
|
+
const moveId = validatedRepoSyncId && validatedWorkspaceId && fullSha ? `commit:v2:${createHash("sha256").update(
|
|
72
|
+
JSON.stringify(["commit:v2", validatedRepoSyncId, validatedWorkspaceId, commit.sha])
|
|
73
|
+
).digest("hex")}` : `commit:v2:unscoped:${randomUUID()}`;
|
|
74
|
+
const base = {
|
|
75
|
+
moveId,
|
|
76
|
+
capturedAt: Date.now(),
|
|
77
|
+
sessionId: validatedAttribution?.sessionId ?? "",
|
|
78
|
+
eventType: "git.commit",
|
|
79
|
+
payload: {
|
|
80
|
+
kind: "git.commit",
|
|
81
|
+
sha: commit.sha,
|
|
82
|
+
parentSha: commit.parentSha,
|
|
83
|
+
branch: commit.branch,
|
|
84
|
+
changedFiles: commit.files,
|
|
85
|
+
changedFilesComplete: commit.filesComplete
|
|
86
|
+
},
|
|
87
|
+
env: {
|
|
88
|
+
cwd,
|
|
89
|
+
cliVersion,
|
|
90
|
+
osPlatform: platform(),
|
|
91
|
+
...repository ? {
|
|
92
|
+
repoRoot: repository.repoRoot,
|
|
93
|
+
repoKey: repository.repoKey,
|
|
94
|
+
gitRoot: repository.repoRoot
|
|
95
|
+
} : {},
|
|
96
|
+
...repoFullName ? { repoFullName } : {},
|
|
97
|
+
...validatedRepoSyncId ? { repoSyncId: validatedRepoSyncId } : {},
|
|
98
|
+
...validatedWorkspaceId ? { workspaceId: validatedWorkspaceId } : {}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return validatedAttribution ? {
|
|
102
|
+
...base,
|
|
103
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
104
|
+
producer: validatedAttribution.producer
|
|
105
|
+
} : { ...base, envelopeVersion: ENVELOPE_VERSION };
|
|
106
|
+
}
|
|
107
|
+
function shouldFlushAfter(eventType) {
|
|
108
|
+
return eventType === "SessionEnd";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
postToolInvocationId,
|
|
113
|
+
toMove,
|
|
114
|
+
commitAttributionFromEnvironment,
|
|
115
|
+
toCommitMove,
|
|
116
|
+
shouldFlushAfter
|
|
117
|
+
};
|