@primitive.ai/prim 0.1.0-alpha.8 → 0.1.0-alpha.81

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.
Files changed (45) hide show
  1. package/README.md +295 -37
  2. package/SKILL.md +269 -0
  3. package/dist/chunk-3EEFL6NJ.js +529 -0
  4. package/dist/chunk-4CZTNXIS.js +505 -0
  5. package/dist/chunk-4GLVJLEQ.js +55 -0
  6. package/dist/chunk-BQUJ4AFJ.js +771 -0
  7. package/dist/chunk-CUE2LKLR.js +155 -0
  8. package/dist/chunk-EZXEWFDG.js +210 -0
  9. package/dist/chunk-FI3LIRCB.js +262 -0
  10. package/dist/chunk-HWSS4BV5.js +1400 -0
  11. package/dist/chunk-LEMX3IOG.js +79 -0
  12. package/dist/chunk-LMRYZL3D.js +209 -0
  13. package/dist/chunk-OUXH3UEP.js +247 -0
  14. package/dist/chunk-PMRDE2DA.js +310 -0
  15. package/dist/chunk-RO3F6JYV.js +126 -0
  16. package/dist/chunk-TJTVHDJ5.js +321 -0
  17. package/dist/chunk-WB7AUZ4C.js +52 -0
  18. package/dist/chunk-XGADQPCI.js +26 -0
  19. package/dist/chunk-YWO7O6QG.js +327 -0
  20. package/dist/chunk-Z7YPFWAZ.js +917 -0
  21. package/dist/chunk-ZOSNEO5N.js +77 -0
  22. package/dist/daemon/server.js +15329 -0
  23. package/dist/hook-runtime/daemon/server.js +15329 -0
  24. package/dist/hook-runtime/hooks/post-commit.js +1128 -0
  25. package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
  26. package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
  27. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  28. package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
  29. package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
  30. package/dist/hook-runtime/hooks/session-end.js +495 -0
  31. package/dist/hook-runtime/hooks/session-start.js +11313 -0
  32. package/dist/hook-runtime/index.js +44696 -0
  33. package/dist/hook-runtime/statusline-main.js +765 -0
  34. package/dist/hooks/post-commit.js +179 -0
  35. package/dist/hooks/post-rewrite.js +197 -0
  36. package/dist/hooks/post-tool-use.js +250 -0
  37. package/dist/hooks/pre-commit.js +21 -74
  38. package/dist/hooks/pre-tool-use.js +348 -0
  39. package/dist/hooks/prim-hook.js +195 -0
  40. package/dist/hooks/session-end.js +70 -0
  41. package/dist/hooks/session-start.js +299 -0
  42. package/dist/index.js +20981 -519
  43. package/dist/statusline-main.js +765 -0
  44. package/package.json +35 -13
  45. package/dist/chunk-3APLWTLB.js +0 -165
@@ -0,0 +1,1148 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/post-rewrite.ts
4
+ import { execFileSync as execFileSync4, spawn } from "child_process";
5
+ import {
6
+ constants as constants3,
7
+ closeSync as closeSync4,
8
+ fstatSync as fstatSync2,
9
+ lstatSync as lstatSync3,
10
+ openSync as openSync4,
11
+ readFileSync as readFileSync9,
12
+ unlinkSync as unlinkSync2
13
+ } from "fs";
14
+ import { basename as basename2, dirname as dirname8, isAbsolute as isAbsolute4, join as join10 } from "path";
15
+ import { fileURLToPath } from "url";
16
+
17
+ // src/binding.ts
18
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
19
+ import { dirname, join as join3 } from "path";
20
+
21
+ // src/lib/credentials.ts
22
+ import { createHash } from "crypto";
23
+ import { readFileSync } from "fs";
24
+ import { join as join2 } from "path";
25
+
26
+ // src/lib/paths.ts
27
+ import { homedir } from "os";
28
+ import { isAbsolute, join, normalize } from "path";
29
+ var UNSAFE_CONFIG_PATH_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
30
+ function absolutePath(value, variable) {
31
+ if (value !== void 0 && UNSAFE_CONFIG_PATH_CHARACTERS.test(value)) {
32
+ throw new Error(`${variable} contains unsafe characters`);
33
+ }
34
+ if (!value || value.trim() !== value || !isAbsolute(value)) return;
35
+ const normalized = normalize(value);
36
+ return normalized === value ? value : void 0;
37
+ }
38
+ function resolvePrimConfigDirectory(options = {}) {
39
+ const env = options.env ?? process.env;
40
+ const explicit = absolutePath(env.PRIM_CONFIG_DIR, "PRIM_CONFIG_DIR");
41
+ if (explicit) return { path: explicit, source: "explicit" };
42
+ const xdgConfigHome = absolutePath(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME");
43
+ if (xdgConfigHome) return { path: join(xdgConfigHome, "prim"), source: "xdg" };
44
+ const home = absolutePath(options.homeDir ?? homedir(), "HOME");
45
+ if (!home) {
46
+ throw new Error("cannot resolve Primitive config directory: HOME is not an absolute path");
47
+ }
48
+ return { path: join(home, ".config", "prim"), source: "default" };
49
+ }
50
+ function primConfigDirectory(options = {}) {
51
+ return resolvePrimConfigDirectory(options).path;
52
+ }
53
+
54
+ // src/lib/workos-connect.ts
55
+ var DISCOVERY_MAX_BYTES = 16 * 1024;
56
+ var WORKOS_CONNECT_RESPONSE_MAX_BYTES = 64 * 1024;
57
+ var MAX_DEVICE_LIFETIME_SECONDS = 60 * 60;
58
+ var MAX_TOKEN_LIFETIME_SECONDS = 24 * 60 * 60;
59
+ var MAX_URL_LENGTH = 2048;
60
+ var MAX_CLIENT_ID_LENGTH = 256;
61
+ var CONNECT_CLIENT_ID_PATTERN = /^client_[A-Za-z0-9_-]+$/u;
62
+ var WorkosConnectProtocolError = class extends Error {
63
+ code;
64
+ constructor(code, message) {
65
+ super(message);
66
+ this.name = "WorkosConnectProtocolError";
67
+ this.code = code;
68
+ }
69
+ };
70
+ function recordFrom(value, label) {
71
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
72
+ throw new WorkosConnectProtocolError("invalid_response", `${label} must be an object`);
73
+ }
74
+ return value;
75
+ }
76
+ function exactString(value, field, maxLength) {
77
+ const containsControl = typeof value === "string" && [...value].some((character) => {
78
+ const code = character.charCodeAt(0);
79
+ return code <= 31 || code === 127;
80
+ });
81
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength || value.trim() !== value || containsControl) {
82
+ throw new WorkosConnectProtocolError("invalid_response", `${field} is invalid`);
83
+ }
84
+ return value;
85
+ }
86
+ function canonicalIssuer(value) {
87
+ const issuer = exactString(value, "issuer", MAX_URL_LENGTH);
88
+ let parsed;
89
+ try {
90
+ parsed = new URL(issuer);
91
+ } catch {
92
+ throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
93
+ }
94
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.origin !== issuer) {
95
+ throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
96
+ }
97
+ return issuer;
98
+ }
99
+ function parseWorkosConnectCredentialMetadata(value) {
100
+ const record = recordFrom(value, "credential metadata");
101
+ if (record.version !== 1 || record.family !== "workos_connect") {
102
+ throw new WorkosConnectProtocolError("invalid_metadata", "credential metadata is invalid");
103
+ }
104
+ const issuer = canonicalIssuer(record.issuer);
105
+ const clientId = exactString(record.clientId, "clientId", MAX_CLIENT_ID_LENGTH);
106
+ const accessTokenHash = exactString(record.accessTokenHash, "accessTokenHash", 64);
107
+ const refreshTokenHash = exactString(record.refreshTokenHash, "refreshTokenHash", 64);
108
+ if (!CONNECT_CLIENT_ID_PATTERN.test(clientId)) {
109
+ throw new WorkosConnectProtocolError("invalid_metadata", "credential clientId is invalid");
110
+ }
111
+ if (!/^[a-f0-9]{64}$/u.test(accessTokenHash)) {
112
+ throw new WorkosConnectProtocolError(
113
+ "invalid_metadata",
114
+ "credential accessTokenHash is invalid"
115
+ );
116
+ }
117
+ if (!/^[a-f0-9]{64}$/u.test(refreshTokenHash)) {
118
+ throw new WorkosConnectProtocolError(
119
+ "invalid_metadata",
120
+ "credential refreshTokenHash is invalid"
121
+ );
122
+ }
123
+ return {
124
+ version: 1,
125
+ family: "workos_connect",
126
+ issuer,
127
+ clientId,
128
+ accessTokenHash,
129
+ refreshTokenHash
130
+ };
131
+ }
132
+
133
+ // src/lib/credentials.ts
134
+ var CONFIG_DIRECTORY = primConfigDirectory();
135
+ var TOKEN_FILE_PATH = join2(CONFIG_DIRECTORY, "token");
136
+ var REFRESH_TOKEN_PATH = join2(CONFIG_DIRECTORY, "refresh_token");
137
+ var TOKEN_EXPIRES_PATH = join2(CONFIG_DIRECTORY, "token_expires_at");
138
+ var TERMINAL_REFRESH_PATH = join2(CONFIG_DIRECTORY, "refresh_terminal");
139
+ var CREDENTIAL_METADATA_PATH = join2(CONFIG_DIRECTORY, "credential_metadata.json");
140
+ var CREDENTIAL_FAMILY_PATH = join2(CONFIG_DIRECTORY, "credential_family.json");
141
+ var CREDENTIAL_MIGRATION_PATH = join2(CONFIG_DIRECTORY, "credential_migration.json");
142
+ var CREDENTIAL_LOCK_PATH = join2(CONFIG_DIRECTORY, "credentials.lock");
143
+ var MAX_CREDENTIAL_METADATA_BYTES = 4096;
144
+ var CREDENTIAL_MIGRATION_VERSION = 1;
145
+ var CREDENTIAL_MIGRATION_STATE = "family_bound";
146
+ function readTrimmed(path) {
147
+ try {
148
+ const value = readFileSync(path, "utf8").trim();
149
+ return value || void 0;
150
+ } catch {
151
+ return void 0;
152
+ }
153
+ }
154
+ function resolveAuthCredential(options = {}) {
155
+ const envToken = (options.env ?? process.env).PRIM_TOKEN?.trim();
156
+ if (envToken) return { token: envToken, source: "environment" };
157
+ const stored = readTrimmed(options.tokenFilePath ?? TOKEN_FILE_PATH);
158
+ if (!stored) return void 0;
159
+ const metadata = readStoredCredentialMetadata(
160
+ options.metadataPath ?? CREDENTIAL_METADATA_PATH,
161
+ options.familyPath ?? CREDENTIAL_FAMILY_PATH,
162
+ options.migrationPath ?? CREDENTIAL_MIGRATION_PATH
163
+ );
164
+ if (metadata.state === "legacy_broker") {
165
+ if (metadata.metadata) {
166
+ const refreshToken2 = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
167
+ if (!refreshToken2 || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken2)) {
168
+ return void 0;
169
+ }
170
+ }
171
+ return { token: stored, source: "token_file" };
172
+ }
173
+ if (metadata.state === "invalid") return void 0;
174
+ const refreshToken = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
175
+ if (!refreshToken || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken)) {
176
+ return void 0;
177
+ }
178
+ return { token: stored, source: "token_file" };
179
+ }
180
+ function credentialFingerprint(value) {
181
+ return createHash("sha256").update(value).digest("hex");
182
+ }
183
+ function parseLegacyBrokerCredentialMetadata(value) {
184
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
185
+ throw new Error("credential family is invalid");
186
+ }
187
+ const record = value;
188
+ if (record.version !== 1 || record.family !== "legacy_broker" || typeof record.accessTokenHash !== "string" || typeof record.refreshTokenHash !== "string" || !/^[a-f0-9]{64}$/u.test(record.accessTokenHash) || !/^[a-f0-9]{64}$/u.test(record.refreshTokenHash)) {
189
+ throw new Error("credential family is invalid");
190
+ }
191
+ return {
192
+ version: 1,
193
+ family: "legacy_broker",
194
+ accessTokenHash: record.accessTokenHash,
195
+ refreshTokenHash: record.refreshTokenHash
196
+ };
197
+ }
198
+ function readBoundedCredentialJson(path) {
199
+ let raw;
200
+ try {
201
+ raw = readFileSync(path, "utf8");
202
+ } catch (error) {
203
+ return error.code === "ENOENT" ? void 0 : null;
204
+ }
205
+ if (Buffer.byteLength(raw, "utf8") > MAX_CREDENTIAL_METADATA_BYTES) return null;
206
+ try {
207
+ return JSON.parse(raw);
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ function readCredentialFamily(path) {
213
+ const parsed = readBoundedCredentialJson(path);
214
+ if (parsed === void 0) return void 0;
215
+ if (parsed === null) return { state: "invalid" };
216
+ try {
217
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.family === "legacy_broker") {
218
+ return { state: "legacy_broker", metadata: parseLegacyBrokerCredentialMetadata(parsed) };
219
+ }
220
+ return {
221
+ state: "workos_connect",
222
+ metadata: parseWorkosConnectCredentialMetadata(parsed)
223
+ };
224
+ } catch {
225
+ return { state: "invalid" };
226
+ }
227
+ }
228
+ function isCredentialMigrationSentinel(value) {
229
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
230
+ const record = value;
231
+ const keys = Object.keys(record).sort();
232
+ return keys.length === 2 && keys[0] === "state" && keys[1] === "version" && record.version === CREDENTIAL_MIGRATION_VERSION && record.state === CREDENTIAL_MIGRATION_STATE;
233
+ }
234
+ function readCredentialMigrationState(path = CREDENTIAL_MIGRATION_PATH) {
235
+ const parsed = readBoundedCredentialJson(path);
236
+ if (parsed === void 0) return "absent";
237
+ return parsed !== null && isCredentialMigrationSentinel(parsed) ? "valid" : "invalid";
238
+ }
239
+ function readStoredCredentialMetadata(metadataPath = CREDENTIAL_METADATA_PATH, familyPath = CREDENTIAL_FAMILY_PATH, migrationPath = CREDENTIAL_MIGRATION_PATH) {
240
+ const family = readCredentialFamily(familyPath);
241
+ if (family) return family;
242
+ const parsed = readBoundedCredentialJson(metadataPath);
243
+ if (parsed === void 0) {
244
+ return readCredentialMigrationState(migrationPath) === "absent" ? { state: "legacy_broker" } : { state: "invalid" };
245
+ }
246
+ if (parsed === null) return { state: "invalid" };
247
+ try {
248
+ return {
249
+ state: "workos_connect",
250
+ metadata: parseWorkosConnectCredentialMetadata(parsed)
251
+ };
252
+ } catch {
253
+ return { state: "invalid" };
254
+ }
255
+ }
256
+ function decodeJwtPayload(token) {
257
+ const parts = token.split(".");
258
+ if (parts.length !== 3) return void 0;
259
+ try {
260
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
261
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return void 0;
262
+ return payload;
263
+ } catch {
264
+ return void 0;
265
+ }
266
+ }
267
+ function jwtOrganizationId(token) {
268
+ const orgId = decodeJwtPayload(token)?.org_id;
269
+ return typeof orgId === "string" && orgId.length > 0 ? orgId : void 0;
270
+ }
271
+
272
+ // src/binding.ts
273
+ var PRIM_CONFIG_DIR = primConfigDirectory();
274
+ var SESSIONS_DIR = join3(PRIM_CONFIG_DIR, "sessions");
275
+ var WORKSPACE_FILE = ".prim/workspace.json";
276
+ function readJsonSafe(path) {
277
+ if (!existsSync(path)) {
278
+ return;
279
+ }
280
+ try {
281
+ return JSON.parse(readFileSync2(path, "utf-8"));
282
+ } catch {
283
+ return;
284
+ }
285
+ }
286
+ function fromSessionMarker(sessionId) {
287
+ if (!sessionId) {
288
+ return;
289
+ }
290
+ const marker = readJsonSafe(join3(SESSIONS_DIR, `${sessionId}.json`));
291
+ return marker?.orgId;
292
+ }
293
+ function fromWorkspaceFile(startDir) {
294
+ let dir = startDir;
295
+ while (true) {
296
+ const config = readJsonSafe(join3(dir, WORKSPACE_FILE));
297
+ if (config?.orgId) {
298
+ return config.orgId;
299
+ }
300
+ const parent = dirname(dir);
301
+ if (parent === dir) {
302
+ return;
303
+ }
304
+ dir = parent;
305
+ }
306
+ }
307
+ function fromDefaultOrg(_cwd) {
308
+ const credential = resolveAuthCredential();
309
+ return credential ? jwtOrganizationId(credential.token) : void 0;
310
+ }
311
+ function resolveOrg(args) {
312
+ const fromSession = fromSessionMarker(args.sessionId);
313
+ if (fromSession) {
314
+ return { orgId: fromSession, source: "session" };
315
+ }
316
+ const fromWorkspace = fromWorkspaceFile(args.cwd);
317
+ if (fromWorkspace) {
318
+ return { orgId: fromWorkspace, source: "workspace" };
319
+ }
320
+ const fromDefault = fromDefaultOrg(args.cwd);
321
+ if (fromDefault) {
322
+ return { orgId: fromDefault, source: "defaultOrg" };
323
+ }
324
+ return { orgId: void 0, source: "unbound" };
325
+ }
326
+
327
+ // src/journal.ts
328
+ import {
329
+ appendFileSync,
330
+ closeSync as closeSync2,
331
+ existsSync as existsSync4,
332
+ fstatSync,
333
+ mkdirSync as mkdirSync4,
334
+ openSync as openSync2,
335
+ opendirSync,
336
+ readFileSync as readFileSync5,
337
+ readSync,
338
+ readdirSync
339
+ } from "fs";
340
+ import { dirname as dirname5, join as join6 } from "path";
341
+
342
+ // src/client.ts
343
+ import { createHash as createHash2 } from "crypto";
344
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3 } from "fs";
345
+ import { dirname as dirname4 } from "path";
346
+
347
+ // src/lib/atomic-file.ts
348
+ import { randomUUID } from "crypto";
349
+ import {
350
+ constants,
351
+ closeSync,
352
+ fchmodSync,
353
+ fsyncSync,
354
+ mkdirSync,
355
+ openSync,
356
+ renameSync,
357
+ rmSync,
358
+ writeFileSync
359
+ } from "fs";
360
+ import { dirname as dirname2 } from "path";
361
+
362
+ // src/lib/file-lock.ts
363
+ import { randomBytes } from "crypto";
364
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
365
+ import { dirname as dirname3, join as join4 } from "path";
366
+ var syncSleeper = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
367
+
368
+ // src/client.ts
369
+ var DEFAULT_API_URL = "https://api.getprimitive.ai";
370
+ function getSiteUrl() {
371
+ return getSiteUrlForEnvironment(process.env.PRIM_API_URL);
372
+ }
373
+ function getSiteUrlForEnvironment(primApiUrl) {
374
+ return primApiUrl?.trim() || DEFAULT_API_URL;
375
+ }
376
+ var CLI_MANAGEMENT_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
377
+
378
+ // src/journal.ts
379
+ var JOURNAL_DIR = join6(primConfigDirectory(), "moves");
380
+ var UNBOUND_BUCKET = "_unbound";
381
+ var JOURNAL_BASENAME = "journal.ndjson";
382
+ var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
383
+ function envSlug(apiUrl) {
384
+ const slug = apiUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
385
+ if (slug.length === 0 || slug === "." || slug === "..") {
386
+ return "default";
387
+ }
388
+ return slug;
389
+ }
390
+ function currentEnvDir() {
391
+ return join6(JOURNAL_DIR, envSlug(getSiteUrl()));
392
+ }
393
+ var DIR_MODE = 448;
394
+ var FILE_MODE = 384;
395
+ var SAFE_BUCKET = /^[A-Za-z0-9_-]+$/;
396
+ var RESERVED_BUCKETS = /* @__PURE__ */ new Set([UNBOUND_BUCKET, "_legacy"]);
397
+ function appendMoveToPath(path, move) {
398
+ const dir = dirname5(path);
399
+ if (!existsSync4(dir)) {
400
+ mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
401
+ }
402
+ appendFileSync(path, `${JSON.stringify(move)}
403
+ `, { mode: FILE_MODE });
404
+ }
405
+ function bucketDir(orgId) {
406
+ const safe = orgId !== void 0 && SAFE_BUCKET.test(orgId) && !RESERVED_BUCKETS.has(orgId);
407
+ return join6(currentEnvDir(), safe ? orgId : UNBOUND_BUCKET);
408
+ }
409
+ function journalPath(orgId) {
410
+ return join6(bucketDir(orgId), JOURNAL_BASENAME);
411
+ }
412
+ function appendMove(move, orgId) {
413
+ appendMoveToPath(journalPath(orgId), move);
414
+ }
415
+ var FLUSHING_PREFIX = `${JOURNAL_BASENAME}.flushing.`;
416
+
417
+ // src/lib/activation.ts
418
+ import { execFileSync as execFileSync2 } from "child_process";
419
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
420
+ import { homedir as homedir2 } from "os";
421
+ import { join as join7 } from "path";
422
+
423
+ // src/lib/git.ts
424
+ import { execFileSync } from "child_process";
425
+ import { createHash as createHash3 } from "crypto";
426
+ import { lstatSync, realpathSync } from "fs";
427
+ import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "path";
428
+ var GIT_TIMEOUT_MS = 1e3;
429
+ var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
430
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
431
+ function gitToplevel(cwd) {
432
+ try {
433
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
434
+ cwd,
435
+ encoding: "utf-8",
436
+ stdio: ["ignore", "pipe", "ignore"],
437
+ timeout: GIT_TIMEOUT_MS
438
+ }).trim();
439
+ } catch {
440
+ return null;
441
+ }
442
+ }
443
+ function gitValue(cwd, args) {
444
+ try {
445
+ const value = execFileSync("git", args, {
446
+ cwd,
447
+ encoding: "utf-8",
448
+ stdio: ["ignore", "pipe", "ignore"],
449
+ timeout: GIT_TIMEOUT_MS
450
+ }).trim();
451
+ return value || void 0;
452
+ } catch {
453
+ return void 0;
454
+ }
455
+ }
456
+ function githubRepositoryFullName(cwd) {
457
+ const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
458
+ if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
459
+ const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
460
+ remote
461
+ );
462
+ const path = match?.[1] ?? match?.[2];
463
+ if (!path) return null;
464
+ const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
465
+ if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
466
+ return null;
467
+ }
468
+ return `${parts[0]}/${parts[1]}`;
469
+ }
470
+ function normalizeOriginRemote(remote) {
471
+ const value = remote.trim();
472
+ if (!value) return void 0;
473
+ const scp = !value.includes("://") ? /^(?:[^@/\s]+@)?(?<host>[^:/\s]+):(?<path>.+)$/.exec(value)?.groups : void 0;
474
+ if (scp?.host && scp.path) {
475
+ const path = scp.path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
476
+ return path ? `${scp.host.toLowerCase()}/${path}` : void 0;
477
+ }
478
+ try {
479
+ const url = new URL(value);
480
+ url.username = "";
481
+ url.password = "";
482
+ url.search = "";
483
+ url.hash = "";
484
+ const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
485
+ if (url.host && path) return `${url.host.toLowerCase()}/${path}`;
486
+ return void 0;
487
+ } catch {
488
+ return void 0;
489
+ }
490
+ }
491
+ function opaqueRepoKey(source, value) {
492
+ return `repo_v1_${createHash3("sha256").update(`${source}\0${value}`).digest("hex")}`;
493
+ }
494
+ function resolveRepositoryContext(cwd = process.cwd()) {
495
+ const top = gitToplevel(cwd);
496
+ if (!top) return null;
497
+ let repoRoot;
498
+ try {
499
+ repoRoot = realpathSync.native(top);
500
+ } catch {
501
+ repoRoot = resolve2(top);
502
+ }
503
+ const origin = gitValue(repoRoot, ["config", "--get", "remote.origin.url"]);
504
+ const repoFullName = githubRepositoryFullName(repoRoot) ?? void 0;
505
+ const normalizedOrigin = origin ? normalizeOriginRemote(origin) : void 0;
506
+ if (normalizedOrigin) {
507
+ return {
508
+ repoRoot,
509
+ repoKey: opaqueRepoKey("origin", normalizedOrigin),
510
+ identitySource: "origin",
511
+ repoFullName
512
+ };
513
+ }
514
+ const roots = gitValue(repoRoot, ["rev-list", "--max-parents=0", "HEAD"])?.split(/\s+/).filter(Boolean).sort();
515
+ if (roots && roots.length > 0) {
516
+ return {
517
+ repoRoot,
518
+ repoKey: opaqueRepoKey("root_commit", roots.join("\n")),
519
+ identitySource: "root_commit",
520
+ repoFullName
521
+ };
522
+ }
523
+ return { repoRoot, repoFullName };
524
+ }
525
+
526
+ // src/lib/activation.ts
527
+ var PRIM_ACTIVE_KEY = "prim.active";
528
+ var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
529
+ var PRIM_REPO_SYNC_REPOSITORY_KEY = "prim.repoSyncRepository";
530
+ var GIT_TIMEOUT_MS2 = 1e3;
531
+ var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
532
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
533
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
534
+ function repoActiveFlag(cwd) {
535
+ try {
536
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
537
+ cwd,
538
+ encoding: "utf-8",
539
+ stdio: ["ignore", "pipe", "ignore"],
540
+ timeout: GIT_TIMEOUT_MS2
541
+ }).trim();
542
+ if (value === "true") return "true";
543
+ if (value === "false") return "false";
544
+ return void 0;
545
+ } catch {
546
+ return void 0;
547
+ }
548
+ }
549
+ function isValidRepoSyncId(value) {
550
+ return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
551
+ }
552
+ function localGitConfigValue(cwd, key) {
553
+ try {
554
+ const value = execFileSync2("git", ["config", "--local", "--get", key], {
555
+ cwd,
556
+ encoding: "utf-8",
557
+ stdio: ["ignore", "pipe", "ignore"],
558
+ timeout: GIT_TIMEOUT_MS2
559
+ }).trim();
560
+ return value || void 0;
561
+ } catch {
562
+ return void 0;
563
+ }
564
+ }
565
+ function repoSyncId(cwd) {
566
+ const value = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
567
+ if (!isValidRepoSyncId(value)) return void 0;
568
+ const repositoryFullName = localGitConfigValue(cwd, PRIM_REPO_SYNC_REPOSITORY_KEY);
569
+ return repositoryFullName === githubRepositoryFullName(cwd) ? value : void 0;
570
+ }
571
+ function hasProjectPrimInstall(cwd) {
572
+ const root = gitToplevel(cwd);
573
+ if (root === null || root === homedir2()) return false;
574
+ for (const rel of PROJECT_INSTALL_FILES) {
575
+ const path = join7(root, rel);
576
+ if (existsSync5(path)) {
577
+ try {
578
+ const content = readFileSync6(path, "utf-8");
579
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
580
+ } catch {
581
+ }
582
+ }
583
+ }
584
+ return false;
585
+ }
586
+ function isRepoActiveForCapture(cwd) {
587
+ const flag = repoActiveFlag(cwd);
588
+ if (flag === "true") return true;
589
+ if (flag === "false") return false;
590
+ return hasProjectPrimInstall(cwd);
591
+ }
592
+
593
+ // src/lib/workspace-id.ts
594
+ import { execFileSync as execFileSync3 } from "child_process";
595
+ import { randomUUID as randomUUID2 } from "crypto";
596
+ import {
597
+ constants as constants2,
598
+ chmodSync as chmodSync2,
599
+ closeSync as closeSync3,
600
+ fchmodSync as fchmodSync2,
601
+ fsyncSync as fsyncSync2,
602
+ linkSync,
603
+ lstatSync as lstatSync2,
604
+ mkdirSync as mkdirSync5,
605
+ openSync as openSync3,
606
+ readFileSync as readFileSync7,
607
+ statSync as statSync2,
608
+ unlinkSync,
609
+ writeFileSync as writeFileSync3
610
+ } from "fs";
611
+ import { dirname as dirname7, isAbsolute as isAbsolute3, join as join8, resolve as resolve3 } from "path";
612
+ var WORKSPACE_ID_RELATIVE_PATH = join8("prim", "workspace-id");
613
+ var MAX_ANCESTORS = 64;
614
+ var MAX_GIT_FILE_BYTES = 4096;
615
+ var MAX_ID_FILE_BYTES = 128;
616
+ var GIT_FALLBACK_TIMEOUT_MS = 250;
617
+ var RACE_READ_ATTEMPTS = 5;
618
+ var RACE_READ_DELAY_MS = 5;
619
+ var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
620
+ var raceDelay = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
621
+ function isCanonicalWorkspaceId(value) {
622
+ return CANONICAL_UUID.test(value);
623
+ }
624
+ function inspectWorkspaceId(cwd = process.cwd()) {
625
+ const resolved = resolveWorkspaceIdPath(cwd);
626
+ if (resolved.status !== "resolved") return resolved;
627
+ return inspectPath(resolved.path);
628
+ }
629
+ function getOrCreateWorkspaceId(cwd = process.cwd()) {
630
+ const inspection = inspectWorkspaceId(cwd);
631
+ if (inspection.status !== "missing") return inspection;
632
+ return createWorkspaceId(inspection.path);
633
+ }
634
+ function resolveWorkspaceIdPath(cwd) {
635
+ if (!process.env.GIT_DIR && !process.env.GIT_WORK_TREE) {
636
+ const fromFilesystem = resolveFromFilesystem(cwd);
637
+ if (fromFilesystem) return { status: "resolved", path: fromFilesystem };
638
+ }
639
+ try {
640
+ const output = execFileSync3(
641
+ "git",
642
+ ["rev-parse", "--is-inside-work-tree", "--git-path", WORKSPACE_ID_RELATIVE_PATH],
643
+ {
644
+ cwd,
645
+ encoding: "utf8",
646
+ maxBuffer: 4096,
647
+ stdio: ["ignore", "pipe", "ignore"],
648
+ timeout: GIT_FALLBACK_TIMEOUT_MS,
649
+ windowsHide: true
650
+ }
651
+ );
652
+ const [insideWorktree, gitPath, ...extra] = output.trim().split(/\r?\n/);
653
+ if (insideWorktree !== "true" || !gitPath || extra.length > 0 || gitPath.includes("\0")) {
654
+ return { status: "not_git" };
655
+ }
656
+ return {
657
+ status: "resolved",
658
+ path: isAbsolute3(gitPath) ? resolve3(gitPath) : resolve3(cwd, gitPath)
659
+ };
660
+ } catch (error) {
661
+ const code = errorCode(error);
662
+ if (code === "ETIMEDOUT" || code === "EACCES" || code === "EPERM") {
663
+ return { status: "unavailable", operation: "resolve", errorCode: code };
664
+ }
665
+ return { status: "not_git" };
666
+ }
667
+ }
668
+ function resolveFromFilesystem(cwd) {
669
+ let current = resolve3(cwd);
670
+ for (let depth = 0; depth < MAX_ANCESTORS; depth += 1) {
671
+ const dotGit = join8(current, ".git");
672
+ try {
673
+ const stat = lstatSync2(dotGit);
674
+ if (stat.isDirectory() && isGitDirectory(dotGit)) {
675
+ return join8(dotGit, WORKSPACE_ID_RELATIVE_PATH);
676
+ }
677
+ if (stat.isFile() && stat.size <= MAX_GIT_FILE_BYTES) {
678
+ const contents = readFileSync7(dotGit, "utf8");
679
+ const match = /^gitdir: (.+?)(?:\r?\n)?$/.exec(contents);
680
+ if (match?.[1]) {
681
+ const gitDirectory = isAbsolute3(match[1]) ? resolve3(match[1]) : resolve3(current, match[1]);
682
+ if (isGitDirectory(gitDirectory)) {
683
+ return join8(gitDirectory, WORKSPACE_ID_RELATIVE_PATH);
684
+ }
685
+ }
686
+ }
687
+ return null;
688
+ } catch (error) {
689
+ const code = errorCode(error);
690
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
691
+ return null;
692
+ }
693
+ }
694
+ const parent = dirname7(current);
695
+ if (parent === current) break;
696
+ current = parent;
697
+ }
698
+ return null;
699
+ }
700
+ function isGitDirectory(path) {
701
+ try {
702
+ return statSync2(join8(path, "HEAD")).isFile();
703
+ } catch {
704
+ return false;
705
+ }
706
+ }
707
+ function inspectPath(path) {
708
+ let stat;
709
+ try {
710
+ stat = lstatSync2(path);
711
+ } catch (error) {
712
+ const code = errorCode(error);
713
+ if (code === "ENOENT" || code === "ENOTDIR") return { status: "missing", path };
714
+ return { status: "unavailable", path, operation: "read", errorCode: code };
715
+ }
716
+ if (!stat.isFile()) return { status: "corrupt", path, reason: "not_file" };
717
+ if (stat.size > MAX_ID_FILE_BYTES) {
718
+ return { status: "corrupt", path, reason: "invalid_content" };
719
+ }
720
+ try {
721
+ const contents = readFileSync7(path, "utf8");
722
+ const workspaceId = contents.endsWith("\n") ? contents.slice(0, -1) : contents;
723
+ if ((contents === workspaceId || contents === `${workspaceId}
724
+ `) && isCanonicalWorkspaceId(workspaceId)) {
725
+ return { status: "ready", workspaceId, path, created: false };
726
+ }
727
+ return { status: "corrupt", path, reason: "invalid_content" };
728
+ } catch (error) {
729
+ return {
730
+ status: "unavailable",
731
+ path,
732
+ operation: "read",
733
+ errorCode: errorCode(error)
734
+ };
735
+ }
736
+ }
737
+ function createWorkspaceId(path) {
738
+ const parent = dirname7(path);
739
+ const workspaceId = randomUUID2();
740
+ const temporaryPath = join8(parent, `.workspace-id-${process.pid}-${randomUUID2()}.tmp`);
741
+ let temporaryCreated = false;
742
+ let publishing = false;
743
+ try {
744
+ mkdirSync5(parent, { recursive: true, mode: 448 });
745
+ chmodSync2(parent, 448);
746
+ const fd = openSync3(
747
+ temporaryPath,
748
+ constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
749
+ 384
750
+ );
751
+ temporaryCreated = true;
752
+ try {
753
+ writeFileSync3(fd, `${workspaceId}
754
+ `, "utf8");
755
+ fchmodSync2(fd, 384);
756
+ fsyncSync2(fd);
757
+ } finally {
758
+ closeSync3(fd);
759
+ }
760
+ publishing = true;
761
+ linkSync(temporaryPath, path);
762
+ return { status: "ready", workspaceId, path, created: true };
763
+ } catch (error) {
764
+ if (publishing && errorCode(error) === "EEXIST") return inspectAfterRace(path);
765
+ return {
766
+ status: "unavailable",
767
+ path,
768
+ operation: "create",
769
+ errorCode: errorCode(error)
770
+ };
771
+ } finally {
772
+ if (temporaryCreated) {
773
+ try {
774
+ unlinkSync(temporaryPath);
775
+ } catch {
776
+ }
777
+ }
778
+ }
779
+ }
780
+ function inspectAfterRace(path) {
781
+ let result = { status: "missing", path };
782
+ for (let attempt = 0; attempt < RACE_READ_ATTEMPTS; attempt += 1) {
783
+ result = inspectPath(path);
784
+ if (result.status === "ready") return result;
785
+ if (attempt + 1 < RACE_READ_ATTEMPTS) {
786
+ Atomics.wait(raceDelay, 0, 0, RACE_READ_DELAY_MS);
787
+ }
788
+ }
789
+ if (result.status === "missing") {
790
+ return { status: "unavailable", path, operation: "create", errorCode: "EEXIST" };
791
+ }
792
+ return result;
793
+ }
794
+ function errorCode(error) {
795
+ if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
796
+ return typeof error.code === "string" ? error.code : void 0;
797
+ }
798
+
799
+ // src/hooks/prim-hook-core.ts
800
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
801
+ import { platform } from "os";
802
+
803
+ // src/protocol/move.ts
804
+ var LEGACY_ENVELOPE_VERSION = 1;
805
+ var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
806
+
807
+ // src/hooks/redact.ts
808
+ import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
809
+ import { join as join9 } from "path";
810
+ import { Worker } from "worker_threads";
811
+ var USER_HOME_RULES = [
812
+ {
813
+ pattern: /\/Users\/[^/\\\s"'`]+/g,
814
+ reason: "user-home-path",
815
+ replacement: "/Users/__redacted_user__"
816
+ },
817
+ {
818
+ pattern: /\/home\/[^/\\\s"'`]+/g,
819
+ reason: "user-home-path",
820
+ replacement: "/home/__redacted_user__"
821
+ },
822
+ {
823
+ pattern: /([A-Za-z]:\\Users\\)[^/\\\s"'`]+/gi,
824
+ reason: "user-home-path",
825
+ replacement: "$1__redacted_user__"
826
+ },
827
+ {
828
+ pattern: /\/root(?=\/|$)/g,
829
+ reason: "user-home-path",
830
+ replacement: "/__redacted_user__"
831
+ }
832
+ ];
833
+ var DEFAULT_RULES = [
834
+ { pattern: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi, reason: "bearer-token" },
835
+ {
836
+ pattern: /\bsk-(?:(?:ant|proj)-)?[A-Za-z0-9_-]{16,}(?![A-Za-z0-9_-])/g,
837
+ reason: "sk-api-key"
838
+ },
839
+ {
840
+ pattern: /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g,
841
+ reason: "private-key"
842
+ },
843
+ { pattern: /xox[abprs]-[A-Za-z0-9-]{10,}/g, reason: "slack-token" },
844
+ {
845
+ pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{50,})\b/g,
846
+ reason: "github-pat"
847
+ },
848
+ { pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, reason: "aws-access-key" },
849
+ {
850
+ pattern: /\bAWS_(?:SECRET_ACCESS_KEY|SESSION_TOKEN)"?\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}["']?/gi,
851
+ reason: "aws-secret"
852
+ },
853
+ { pattern: /\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, reason: "gcp-api-key" },
854
+ {
855
+ pattern: /\bya29\.[0-9A-Za-z_-]{20,}(?![0-9A-Za-z_-])/g,
856
+ reason: "gcp-oauth-token"
857
+ },
858
+ {
859
+ pattern: /\beyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*(?![A-Za-z0-9_-])/g,
860
+ reason: "jwt"
861
+ },
862
+ { pattern: /\bBasic\s+[A-Za-z0-9+/]+={0,2}/gi, reason: "basic-auth" },
863
+ { pattern: /\bhttps?:\/\/[^/\s:@]+:[^/\s@]+@/gi, reason: "basic-auth" },
864
+ ...USER_HOME_RULES
865
+ ];
866
+ var MAX_SCRUB_LEN = 256e3;
867
+ var MAX_WORKSPACE_CONFIG_BYTES = 64 * 1024;
868
+ function applyRules(input, rules) {
869
+ if (input.length > MAX_SCRUB_LEN) return "<REDACTED:oversized>";
870
+ let out = input;
871
+ for (const rule of rules) {
872
+ out = out.replace(rule.pattern, rule.replacement ?? `<REDACTED:${rule.reason}>`);
873
+ }
874
+ return out;
875
+ }
876
+ var WORKSPACE_REDACTION_WORKER = String.raw`
877
+ const { parentPort, workerData } = require("node:worker_threads");
878
+ const REDACTED_OBJECT_KEY = "<REDACTED:object-key>";
879
+ const rules = workerData.rules.map((rule) => ({
880
+ pattern: new RegExp(rule.source, rule.flags),
881
+ reason: rule.reason,
882
+ }));
883
+ function redact(input) {
884
+ let output = input;
885
+ for (const rule of rules) {
886
+ output = output.replace(rule.pattern, "<REDACTED:" + rule.reason + ">");
887
+ }
888
+ return output;
889
+ }
890
+ function objectKey(input) {
891
+ return redact(input) === input ? input : REDACTED_OBJECT_KEY;
892
+ }
893
+ function uniqueKey(output, key) {
894
+ let candidate = key;
895
+ let suffix = 2;
896
+ while (Object.hasOwn(output, candidate)) candidate = key + "_" + suffix++;
897
+ return candidate;
898
+ }
899
+ function walk(value) {
900
+ if (typeof value === "string") {
901
+ return redact(value);
902
+ }
903
+ if (Array.isArray(value)) return value.map(walk);
904
+ if (value && typeof value === "object") {
905
+ const output = Object.create(null);
906
+ for (const [key, item] of Object.entries(value)) {
907
+ output[uniqueKey(output, objectKey(key))] = walk(item);
908
+ }
909
+ return output;
910
+ }
911
+ return value;
912
+ }
913
+ parentPort.postMessage(walk(workerData.value));
914
+ `;
915
+ function scrubEnvironmentPaths(environment) {
916
+ const scrubPath = (value) => value === void 0 ? void 0 : applyRules(value, USER_HOME_RULES);
917
+ return {
918
+ ...environment,
919
+ cwd: scrubPath(environment.cwd) ?? environment.cwd,
920
+ ...environment.repoRoot === void 0 ? {} : { repoRoot: scrubPath(environment.repoRoot) },
921
+ ...environment.gitRoot === void 0 ? {} : { gitRoot: scrubPath(environment.gitRoot) }
922
+ };
923
+ }
924
+
925
+ // src/hooks/prim-hook-core.ts
926
+ var MAX_REWRITE_PAIRS_PER_MOVE = 40;
927
+ var MAX_REWRITE_PAIRS_PER_HOOK = 2e3;
928
+ var FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
929
+ function canonicalRewritePairs(pairs) {
930
+ const unique = /* @__PURE__ */ new Map();
931
+ for (const pair of pairs) {
932
+ const oldSha = pair.oldSha.toLowerCase();
933
+ const newSha = pair.newSha.toLowerCase();
934
+ if (!FULL_SHA_RE.test(oldSha) || !FULL_SHA_RE.test(newSha)) return null;
935
+ if (oldSha === newSha) continue;
936
+ unique.set(`${oldSha}\0${newSha}`, { oldSha, newSha });
937
+ }
938
+ return [...unique.values()].sort((left, right) => {
939
+ const leftKey = `${left.oldSha}\0${left.newSha}`;
940
+ const rightKey = `${right.oldSha}\0${right.newSha}`;
941
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
942
+ }).slice(0, MAX_REWRITE_PAIRS_PER_HOOK);
943
+ }
944
+ function toRewriteMove(rewrite, cliVersion, cwd, context) {
945
+ const { repository, repoFullName, repoSyncId: repoSyncId2, workspaceId, capturedAt } = context;
946
+ if (rewrite.source !== "amend" && rewrite.source !== "rebase" || !isValidRepoSyncId(repoSyncId2) || !workspaceId || !isCanonicalWorkspaceId(workspaceId)) {
947
+ return [];
948
+ }
949
+ const pairs = canonicalRewritePairs(rewrite.pairs);
950
+ if (!pairs || pairs.length === 0) return [];
951
+ const branch = rewrite.branch && rewrite.branch === rewrite.branch.trim() && ![...rewrite.branch].some((char) => char < " " || char === "\x7F") ? rewrite.branch : void 0;
952
+ const observedAt = typeof capturedAt === "number" && Number.isSafeInteger(capturedAt) && capturedAt > 0 ? capturedAt : Date.now();
953
+ const moves = [];
954
+ for (let offset = 0; offset < pairs.length; offset += MAX_REWRITE_PAIRS_PER_MOVE) {
955
+ const sortedPairs = pairs.slice(offset, offset + MAX_REWRITE_PAIRS_PER_MOVE);
956
+ const moveId = `rewrite:v1:${createHash4("sha256").update(JSON.stringify(["rewrite:v1", repoSyncId2, workspaceId, rewrite.source, sortedPairs])).digest("hex")}`;
957
+ moves.push({
958
+ moveId,
959
+ capturedAt: observedAt,
960
+ sessionId: "",
961
+ eventType: "git.rewrite",
962
+ payload: {
963
+ kind: "git.rewrite",
964
+ source: rewrite.source,
965
+ pairs: sortedPairs,
966
+ ...branch ? { branch } : {}
967
+ },
968
+ env: scrubEnvironmentPaths({
969
+ cwd,
970
+ cliVersion,
971
+ osPlatform: platform(),
972
+ ...repository ? {
973
+ repoRoot: repository.repoRoot,
974
+ repoKey: repository.repoKey,
975
+ gitRoot: repository.repoRoot
976
+ } : {},
977
+ ...repoFullName ? { repoFullName } : {},
978
+ repoSyncId: repoSyncId2,
979
+ workspaceId
980
+ }),
981
+ envelopeVersion: ENVELOPE_VERSION
982
+ });
983
+ }
984
+ return moves;
985
+ }
986
+
987
+ // src/hooks/post-rewrite.ts
988
+ var here = dirname8(fileURLToPath(import.meta.url));
989
+ var GIT_TIMEOUT_MS3 = 1e3;
990
+ var MAX_PAIRS_FILE_BYTES = 1048576;
991
+ var MAX_PAIRS_FILE_AGE_MS = 10 * 60 * 1e3;
992
+ var MAX_PAIRS_FILE_FUTURE_SKEW_MS = 5e3;
993
+ var PAIRS_FILE_RE = /^prim-post-rewrite-pairs\.[A-Za-z0-9]+$/u;
994
+ var FULL_SHA_SOURCE = "(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})";
995
+ var REWRITE_PAIR_RE = new RegExp(
996
+ `^(${FULL_SHA_SOURCE}) (${FULL_SHA_SOURCE})(?: ([^\\s]+))?$`,
997
+ "u"
998
+ );
999
+ function parseRewritePairs(value) {
1000
+ if (value.length === 0) return [];
1001
+ const body = value.endsWith("\n") ? value.slice(0, -1) : value;
1002
+ if (body.length === 0) return [];
1003
+ const parsed = [];
1004
+ for (const line of body.split("\n")) {
1005
+ const match = REWRITE_PAIR_RE.exec(line);
1006
+ if (!match?.[1] || !match[2]) return;
1007
+ parsed.push({ oldSha: match[1], newSha: match[2] });
1008
+ }
1009
+ return canonicalRewritePairs(parsed) ?? void 0;
1010
+ }
1011
+ function privatePairsFileStat(stat, now) {
1012
+ const capturedAt = Math.trunc(stat.mtimeMs);
1013
+ const uid = process.getuid?.();
1014
+ return stat.isFile() && !stat.isSymbolicLink() && stat.size <= MAX_PAIRS_FILE_BYTES && stat.nlink === 1 && (stat.mode & 511) === 384 && (uid === void 0 || stat.uid === uid) && Number.isSafeInteger(capturedAt) && capturedAt > 0 && capturedAt >= now - MAX_PAIRS_FILE_AGE_MS && capturedAt <= now + MAX_PAIRS_FILE_FUTURE_SKEW_MS;
1015
+ }
1016
+ function rewritePairsFromLauncher(env = process.env, now = Date.now()) {
1017
+ const path = env.PRIM_REWRITE_PAIRS_FILE;
1018
+ if (!path || !isAbsolute4(path) || !PAIRS_FILE_RE.test(basename2(path))) return;
1019
+ let fd;
1020
+ try {
1021
+ const pathStat = lstatSync3(path);
1022
+ if (!privatePairsFileStat(pathStat, now)) return;
1023
+ fd = openSync4(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
1024
+ const openStat = fstatSync2(fd);
1025
+ if (!privatePairsFileStat(openStat, now) || openStat.dev !== pathStat.dev || openStat.ino !== pathStat.ino) {
1026
+ return;
1027
+ }
1028
+ const bytes = readFileSync9(fd);
1029
+ if (bytes.length !== openStat.size) return;
1030
+ let text;
1031
+ try {
1032
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1033
+ } catch {
1034
+ return;
1035
+ }
1036
+ const pairs = parseRewritePairs(text);
1037
+ if (!pairs) return;
1038
+ return {
1039
+ path,
1040
+ dev: openStat.dev,
1041
+ ino: openStat.ino,
1042
+ capturedAt: Math.trunc(openStat.mtimeMs),
1043
+ pairs
1044
+ };
1045
+ } catch {
1046
+ return;
1047
+ } finally {
1048
+ if (fd !== void 0) {
1049
+ try {
1050
+ closeSync4(fd);
1051
+ } catch {
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+ function unlinkSnapshot(snapshot) {
1057
+ try {
1058
+ const current = lstatSync3(snapshot.path);
1059
+ if (current.isFile() && !current.isSymbolicLink() && current.dev === snapshot.dev && current.ino === snapshot.ino) {
1060
+ unlinkSync2(snapshot.path);
1061
+ }
1062
+ } catch {
1063
+ }
1064
+ }
1065
+ function gitText(args) {
1066
+ try {
1067
+ return execFileSync4("git", args, {
1068
+ encoding: "utf-8",
1069
+ stdio: ["ignore", "pipe", "ignore"],
1070
+ timeout: GIT_TIMEOUT_MS3
1071
+ }).trim();
1072
+ } catch {
1073
+ return;
1074
+ }
1075
+ }
1076
+ function validSource(value) {
1077
+ return value === "amend" || value === "rebase";
1078
+ }
1079
+ function validBranch(value) {
1080
+ return value && value === value.trim() && ![...value].some((char) => char < " " || char === "\x7F") ? value : void 0;
1081
+ }
1082
+ function resolveCliVersion() {
1083
+ try {
1084
+ const pkg = JSON.parse(readFileSync9(join10(here, "..", "..", "package.json"), "utf-8"));
1085
+ return pkg.version ?? "unknown";
1086
+ } catch {
1087
+ return "unknown";
1088
+ }
1089
+ }
1090
+ function spawnBackgroundFlush() {
1091
+ const entry = join10(here, "..", "index.js");
1092
+ spawn(process.execPath, [entry, "moves", "flush"], {
1093
+ detached: true,
1094
+ stdio: "ignore"
1095
+ }).unref();
1096
+ }
1097
+ function runPostRewrite() {
1098
+ const snapshot = rewritePairsFromLauncher();
1099
+ if (!snapshot) return;
1100
+ try {
1101
+ const source = process.env.PRIM_REWRITE_SOURCE;
1102
+ if (!validSource(source) || snapshot.pairs.length === 0) return;
1103
+ const cwd = gitText(["rev-parse", "--show-toplevel"]) ?? process.cwd();
1104
+ if (!isRepoActiveForCapture(cwd)) return;
1105
+ const repository = resolveRepositoryContext(cwd);
1106
+ const identity = getOrCreateWorkspaceId(cwd);
1107
+ const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
1108
+ const moves = toRewriteMove(
1109
+ {
1110
+ source,
1111
+ pairs: snapshot.pairs,
1112
+ branch: validBranch(process.env.PRIM_REWRITE_BRANCH)
1113
+ },
1114
+ resolveCliVersion(),
1115
+ cwd,
1116
+ {
1117
+ repository,
1118
+ repoFullName: githubRepositoryFullName(cwd) ?? void 0,
1119
+ repoSyncId: repoSyncId(cwd),
1120
+ workspaceId,
1121
+ capturedAt: snapshot.capturedAt
1122
+ }
1123
+ );
1124
+ if (moves.length === 0) return;
1125
+ const { orgId } = resolveOrg({ sessionId: "", cwd });
1126
+ for (const move of moves) appendMove(move, orgId);
1127
+ spawnBackgroundFlush();
1128
+ } finally {
1129
+ unlinkSnapshot(snapshot);
1130
+ }
1131
+ }
1132
+ if (!process.env.VITEST) {
1133
+ try {
1134
+ runPostRewrite();
1135
+ } catch (error) {
1136
+ if (process.env.PRIM_HOOK_DEBUG) {
1137
+ const detail = error instanceof Error ? error.message : String(error);
1138
+ process.stderr.write(`[prim-post-rewrite] capture failed: ${detail}
1139
+ `);
1140
+ }
1141
+ }
1142
+ process.exit(0);
1143
+ }
1144
+ export {
1145
+ parseRewritePairs,
1146
+ rewritePairsFromLauncher,
1147
+ runPostRewrite
1148
+ };