@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,1314 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/pre-commit.ts
4
+ import { execSync } from "child_process";
5
+
6
+ // src/client.ts
7
+ import { createHash as createHash2 } from "crypto";
8
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3 } from "fs";
9
+ import { dirname as dirname3 } from "path";
10
+
11
+ // src/lib/atomic-file.ts
12
+ import { randomUUID } from "crypto";
13
+ import {
14
+ constants,
15
+ closeSync,
16
+ fchmodSync,
17
+ fsyncSync,
18
+ mkdirSync,
19
+ openSync,
20
+ renameSync,
21
+ rmSync,
22
+ writeFileSync
23
+ } from "fs";
24
+ import { dirname } from "path";
25
+ var DEFAULT_FILE_MODE = 438;
26
+ function syncDirectory(path) {
27
+ const fd = openSync(path, constants.O_RDONLY);
28
+ try {
29
+ fsyncSync(fd);
30
+ } finally {
31
+ closeSync(fd);
32
+ }
33
+ }
34
+ function syncCreatedParentChain(firstCreatedParent, parent) {
35
+ const createdDirectories = [];
36
+ let current = parent;
37
+ while (current !== firstCreatedParent) {
38
+ createdDirectories.push(current);
39
+ const next = dirname(current);
40
+ if (next === current) {
41
+ throw new Error("recursive mkdir returned a path outside the target parent chain");
42
+ }
43
+ current = next;
44
+ }
45
+ createdDirectories.push(firstCreatedParent);
46
+ syncDirectory(dirname(firstCreatedParent));
47
+ for (const directory of createdDirectories.reverse().slice(0, -1)) {
48
+ syncDirectory(directory);
49
+ }
50
+ }
51
+ function ensureDurableDirectory(path, mode) {
52
+ const firstCreatedParent = mkdirSync(path, { recursive: true, mode });
53
+ if (firstCreatedParent) {
54
+ syncCreatedParentChain(firstCreatedParent, path);
55
+ }
56
+ }
57
+ function atomicWriteFile(target, content, options = {}) {
58
+ const parent = dirname(target);
59
+ if (options.ensureParent) {
60
+ ensureDurableDirectory(parent);
61
+ }
62
+ const temporaryPath = `${target}.${randomUUID()}.tmp`;
63
+ let temporaryCreated = false;
64
+ try {
65
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW;
66
+ const fd = openSync(temporaryPath, flags, options.mode ?? DEFAULT_FILE_MODE);
67
+ temporaryCreated = true;
68
+ try {
69
+ if (options.mode !== void 0) fchmodSync(fd, options.mode);
70
+ writeFileSync(fd, content);
71
+ fsyncSync(fd);
72
+ } finally {
73
+ closeSync(fd);
74
+ }
75
+ options.validate?.(temporaryPath);
76
+ renameSync(temporaryPath, target);
77
+ syncDirectory(parent);
78
+ } catch (error) {
79
+ if (temporaryCreated) {
80
+ try {
81
+ rmSync(temporaryPath, { force: true });
82
+ } catch {
83
+ }
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ // src/lib/credentials.ts
90
+ import { createHash } from "crypto";
91
+ import { readFileSync } from "fs";
92
+ import { join as join2 } from "path";
93
+
94
+ // src/lib/paths.ts
95
+ import { homedir } from "os";
96
+ import { isAbsolute, join, normalize } from "path";
97
+ var UNSAFE_CONFIG_PATH_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
98
+ function absolutePath(value, variable) {
99
+ if (value !== void 0 && UNSAFE_CONFIG_PATH_CHARACTERS.test(value)) {
100
+ throw new Error(`${variable} contains unsafe characters`);
101
+ }
102
+ if (!value || value.trim() !== value || !isAbsolute(value)) return;
103
+ const normalized = normalize(value);
104
+ return normalized === value ? value : void 0;
105
+ }
106
+ function resolvePrimConfigDirectory(options = {}) {
107
+ const env = options.env ?? process.env;
108
+ const explicit = absolutePath(env.PRIM_CONFIG_DIR, "PRIM_CONFIG_DIR");
109
+ if (explicit) return { path: explicit, source: "explicit" };
110
+ const xdgConfigHome = absolutePath(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME");
111
+ if (xdgConfigHome) return { path: join(xdgConfigHome, "prim"), source: "xdg" };
112
+ const home = absolutePath(options.homeDir ?? homedir(), "HOME");
113
+ if (!home) {
114
+ throw new Error("cannot resolve Primitive config directory: HOME is not an absolute path");
115
+ }
116
+ return { path: join(home, ".config", "prim"), source: "default" };
117
+ }
118
+ function primConfigDirectory(options = {}) {
119
+ return resolvePrimConfigDirectory(options).path;
120
+ }
121
+
122
+ // src/lib/workos-connect.ts
123
+ var TOKEN_PATH = "/oauth2/token";
124
+ var DISCOVERY_MAX_BYTES = 16 * 1024;
125
+ var WORKOS_CONNECT_RESPONSE_MAX_BYTES = 64 * 1024;
126
+ var MAX_DEVICE_LIFETIME_SECONDS = 60 * 60;
127
+ var MAX_TOKEN_LIFETIME_SECONDS = 24 * 60 * 60;
128
+ var MAX_URL_LENGTH = 2048;
129
+ var MAX_CLIENT_ID_LENGTH = 256;
130
+ var CONNECT_CLIENT_ID_PATTERN = /^client_[A-Za-z0-9_-]+$/u;
131
+ var WorkosConnectProtocolError = class extends Error {
132
+ code;
133
+ constructor(code, message) {
134
+ super(message);
135
+ this.name = "WorkosConnectProtocolError";
136
+ this.code = code;
137
+ }
138
+ };
139
+ function recordFrom(value, label) {
140
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
141
+ throw new WorkosConnectProtocolError("invalid_response", `${label} must be an object`);
142
+ }
143
+ return value;
144
+ }
145
+ function exactString(value, field, maxLength) {
146
+ const containsControl = typeof value === "string" && [...value].some((character) => {
147
+ const code = character.charCodeAt(0);
148
+ return code <= 31 || code === 127;
149
+ });
150
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength || value.trim() !== value || containsControl) {
151
+ throw new WorkosConnectProtocolError("invalid_response", `${field} is invalid`);
152
+ }
153
+ return value;
154
+ }
155
+ function safeInteger(value, field, minimum, maximum) {
156
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
157
+ throw new WorkosConnectProtocolError("invalid_response", `${field} is invalid`);
158
+ }
159
+ return value;
160
+ }
161
+ function canonicalIssuer(value) {
162
+ const issuer = exactString(value, "issuer", MAX_URL_LENGTH);
163
+ let parsed;
164
+ try {
165
+ parsed = new URL(issuer);
166
+ } catch {
167
+ throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
168
+ }
169
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.origin !== issuer) {
170
+ throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
171
+ }
172
+ return issuer;
173
+ }
174
+ function parseWorkosConnectCredentialMetadata(value) {
175
+ const record = recordFrom(value, "credential metadata");
176
+ if (record.version !== 1 || record.family !== "workos_connect") {
177
+ throw new WorkosConnectProtocolError("invalid_metadata", "credential metadata is invalid");
178
+ }
179
+ const issuer = canonicalIssuer(record.issuer);
180
+ const clientId = exactString(record.clientId, "clientId", MAX_CLIENT_ID_LENGTH);
181
+ const accessTokenHash = exactString(record.accessTokenHash, "accessTokenHash", 64);
182
+ const refreshTokenHash = exactString(record.refreshTokenHash, "refreshTokenHash", 64);
183
+ if (!CONNECT_CLIENT_ID_PATTERN.test(clientId)) {
184
+ throw new WorkosConnectProtocolError("invalid_metadata", "credential clientId is invalid");
185
+ }
186
+ if (!/^[a-f0-9]{64}$/u.test(accessTokenHash)) {
187
+ throw new WorkosConnectProtocolError(
188
+ "invalid_metadata",
189
+ "credential accessTokenHash is invalid"
190
+ );
191
+ }
192
+ if (!/^[a-f0-9]{64}$/u.test(refreshTokenHash)) {
193
+ throw new WorkosConnectProtocolError(
194
+ "invalid_metadata",
195
+ "credential refreshTokenHash is invalid"
196
+ );
197
+ }
198
+ return {
199
+ version: 1,
200
+ family: "workos_connect",
201
+ issuer,
202
+ clientId,
203
+ accessTokenHash,
204
+ refreshTokenHash
205
+ };
206
+ }
207
+ async function readBoundedJson(response, maxBytes) {
208
+ const contentLength = response.headers.get("content-length");
209
+ if (contentLength !== null) {
210
+ if (!/^\d+$/u.test(contentLength) || Number(contentLength) > maxBytes) {
211
+ throw new WorkosConnectProtocolError("invalid_response", "response body is too large");
212
+ }
213
+ }
214
+ if (!response.body) {
215
+ throw new WorkosConnectProtocolError("invalid_response", "response body is missing");
216
+ }
217
+ const reader = response.body.getReader();
218
+ const chunks = [];
219
+ let total = 0;
220
+ try {
221
+ while (true) {
222
+ const result = await reader.read();
223
+ if (result.done) break;
224
+ total += result.value.byteLength;
225
+ if (total > maxBytes) {
226
+ await reader.cancel();
227
+ throw new WorkosConnectProtocolError("invalid_response", "response body is too large");
228
+ }
229
+ chunks.push(result.value);
230
+ }
231
+ } finally {
232
+ reader.releaseLock();
233
+ }
234
+ const bytes = new Uint8Array(total);
235
+ let offset = 0;
236
+ for (const chunk2 of chunks) {
237
+ bytes.set(chunk2, offset);
238
+ offset += chunk2.byteLength;
239
+ }
240
+ let text;
241
+ try {
242
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
243
+ return JSON.parse(text);
244
+ } catch {
245
+ throw new WorkosConnectProtocolError("invalid_response", "response body is not valid JSON");
246
+ }
247
+ }
248
+ function workosConnectTokenEndpoint(issuer) {
249
+ return `${issuer}${TOKEN_PATH}`;
250
+ }
251
+ function parseWorkosConnectTokens(value, options = {}) {
252
+ const record = recordFrom(value, "token response");
253
+ const accessToken = exactString(
254
+ record.access_token,
255
+ "access_token",
256
+ WORKOS_CONNECT_RESPONSE_MAX_BYTES
257
+ );
258
+ const refreshToken2 = record.refresh_token === void 0 && options.fallbackRefreshToken !== void 0 ? options.fallbackRefreshToken : exactString(record.refresh_token, "refresh_token", WORKOS_CONNECT_RESPONSE_MAX_BYTES);
259
+ return {
260
+ accessToken,
261
+ refreshToken: refreshToken2,
262
+ expiresIn: record.expires_in === void 0 ? void 0 : safeInteger(record.expires_in, "expires_in", 1, MAX_TOKEN_LIFETIME_SECONDS)
263
+ };
264
+ }
265
+
266
+ // src/lib/credentials.ts
267
+ var CONFIG_DIRECTORY = primConfigDirectory();
268
+ var TOKEN_FILE_PATH = join2(CONFIG_DIRECTORY, "token");
269
+ var REFRESH_TOKEN_PATH = join2(CONFIG_DIRECTORY, "refresh_token");
270
+ var TOKEN_EXPIRES_PATH = join2(CONFIG_DIRECTORY, "token_expires_at");
271
+ var TERMINAL_REFRESH_PATH = join2(CONFIG_DIRECTORY, "refresh_terminal");
272
+ var CREDENTIAL_METADATA_PATH = join2(CONFIG_DIRECTORY, "credential_metadata.json");
273
+ var CREDENTIAL_FAMILY_PATH = join2(CONFIG_DIRECTORY, "credential_family.json");
274
+ var CREDENTIAL_MIGRATION_PATH = join2(CONFIG_DIRECTORY, "credential_migration.json");
275
+ var CREDENTIAL_LOCK_PATH = join2(CONFIG_DIRECTORY, "credentials.lock");
276
+ var MAX_CREDENTIAL_METADATA_BYTES = 4096;
277
+ var CREDENTIAL_MIGRATION_VERSION = 1;
278
+ var CREDENTIAL_MIGRATION_STATE = "family_bound";
279
+ function readTrimmed(path) {
280
+ try {
281
+ const value = readFileSync(path, "utf8").trim();
282
+ return value || void 0;
283
+ } catch {
284
+ return void 0;
285
+ }
286
+ }
287
+ function resolveAuthCredential(options = {}) {
288
+ const envToken = (options.env ?? process.env).PRIM_TOKEN?.trim();
289
+ if (envToken) return { token: envToken, source: "environment" };
290
+ const stored = readTrimmed(options.tokenFilePath ?? TOKEN_FILE_PATH);
291
+ if (!stored) return void 0;
292
+ const metadata = readStoredCredentialMetadata(
293
+ options.metadataPath ?? CREDENTIAL_METADATA_PATH,
294
+ options.familyPath ?? CREDENTIAL_FAMILY_PATH,
295
+ options.migrationPath ?? CREDENTIAL_MIGRATION_PATH
296
+ );
297
+ if (metadata.state === "legacy_broker") {
298
+ if (metadata.metadata) {
299
+ const refreshToken3 = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
300
+ if (!refreshToken3 || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken3)) {
301
+ return void 0;
302
+ }
303
+ }
304
+ return { token: stored, source: "token_file" };
305
+ }
306
+ if (metadata.state === "invalid") return void 0;
307
+ const refreshToken2 = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
308
+ if (!refreshToken2 || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken2)) {
309
+ return void 0;
310
+ }
311
+ return { token: stored, source: "token_file" };
312
+ }
313
+ function credentialFingerprint(value) {
314
+ return createHash("sha256").update(value).digest("hex");
315
+ }
316
+ function parseLegacyBrokerCredentialMetadata(value) {
317
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
318
+ throw new Error("credential family is invalid");
319
+ }
320
+ const record = value;
321
+ 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)) {
322
+ throw new Error("credential family is invalid");
323
+ }
324
+ return {
325
+ version: 1,
326
+ family: "legacy_broker",
327
+ accessTokenHash: record.accessTokenHash,
328
+ refreshTokenHash: record.refreshTokenHash
329
+ };
330
+ }
331
+ function readBoundedCredentialJson(path) {
332
+ let raw;
333
+ try {
334
+ raw = readFileSync(path, "utf8");
335
+ } catch (error) {
336
+ return error.code === "ENOENT" ? void 0 : null;
337
+ }
338
+ if (Buffer.byteLength(raw, "utf8") > MAX_CREDENTIAL_METADATA_BYTES) return null;
339
+ try {
340
+ return JSON.parse(raw);
341
+ } catch {
342
+ return null;
343
+ }
344
+ }
345
+ function readCredentialFamily(path) {
346
+ const parsed = readBoundedCredentialJson(path);
347
+ if (parsed === void 0) return void 0;
348
+ if (parsed === null) return { state: "invalid" };
349
+ try {
350
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.family === "legacy_broker") {
351
+ return { state: "legacy_broker", metadata: parseLegacyBrokerCredentialMetadata(parsed) };
352
+ }
353
+ return {
354
+ state: "workos_connect",
355
+ metadata: parseWorkosConnectCredentialMetadata(parsed)
356
+ };
357
+ } catch {
358
+ return { state: "invalid" };
359
+ }
360
+ }
361
+ function isCredentialMigrationSentinel(value) {
362
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
363
+ const record = value;
364
+ const keys = Object.keys(record).sort();
365
+ return keys.length === 2 && keys[0] === "state" && keys[1] === "version" && record.version === CREDENTIAL_MIGRATION_VERSION && record.state === CREDENTIAL_MIGRATION_STATE;
366
+ }
367
+ function readCredentialMigrationState(path = CREDENTIAL_MIGRATION_PATH) {
368
+ const parsed = readBoundedCredentialJson(path);
369
+ if (parsed === void 0) return "absent";
370
+ return parsed !== null && isCredentialMigrationSentinel(parsed) ? "valid" : "invalid";
371
+ }
372
+ function readStoredCredentialMetadata(metadataPath = CREDENTIAL_METADATA_PATH, familyPath = CREDENTIAL_FAMILY_PATH, migrationPath = CREDENTIAL_MIGRATION_PATH) {
373
+ const family = readCredentialFamily(familyPath);
374
+ if (family) return family;
375
+ const parsed = readBoundedCredentialJson(metadataPath);
376
+ if (parsed === void 0) {
377
+ return readCredentialMigrationState(migrationPath) === "absent" ? { state: "legacy_broker" } : { state: "invalid" };
378
+ }
379
+ if (parsed === null) return { state: "invalid" };
380
+ try {
381
+ return {
382
+ state: "workos_connect",
383
+ metadata: parseWorkosConnectCredentialMetadata(parsed)
384
+ };
385
+ } catch {
386
+ return { state: "invalid" };
387
+ }
388
+ }
389
+ function decodeJwtPayload(token) {
390
+ const parts = token.split(".");
391
+ if (parts.length !== 3) return void 0;
392
+ try {
393
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
394
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return void 0;
395
+ return payload;
396
+ } catch {
397
+ return void 0;
398
+ }
399
+ }
400
+ function jwtExpiresAt(token) {
401
+ const exp = decodeJwtPayload(token)?.exp;
402
+ return typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : void 0;
403
+ }
404
+
405
+ // src/lib/file-lock.ts
406
+ import { randomBytes } from "crypto";
407
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
408
+ import { dirname as dirname2, join as join3 } from "path";
409
+
410
+ // src/lib/process-liveness.ts
411
+ function processIsAlive(pid, probe = process.kill) {
412
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
413
+ try {
414
+ probe(pid, 0);
415
+ return true;
416
+ } catch (error) {
417
+ return error.code === "EPERM";
418
+ }
419
+ }
420
+
421
+ // src/lib/file-lock.ts
422
+ var DEFAULT_TIMEOUT_MS = 3e4;
423
+ var DEFAULT_POLL_MS = 50;
424
+ var DEFAULT_INIT_GRACE_MS = 2e3;
425
+ var DIRECTORY_MODE = 448;
426
+ var FILE_MODE = 384;
427
+ function ownerPath(lockDir) {
428
+ return join3(lockDir, "owner.json");
429
+ }
430
+ function readOwner(lockDir) {
431
+ try {
432
+ const owner = JSON.parse(readFileSync2(ownerPath(lockDir), "utf8"));
433
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || typeof owner.createdAt !== "number") {
434
+ return null;
435
+ }
436
+ return owner;
437
+ } catch {
438
+ return null;
439
+ }
440
+ }
441
+ function sameOwner(a, b) {
442
+ return a?.pid === b.pid && a.nonce === b.nonce && a.createdAt === b.createdAt;
443
+ }
444
+ function tryTakeLock(lockDir, now) {
445
+ try {
446
+ mkdirSync2(lockDir, { mode: DIRECTORY_MODE });
447
+ } catch (error) {
448
+ if (error.code === "EEXIST") return null;
449
+ throw error;
450
+ }
451
+ const owner = {
452
+ pid: process.pid,
453
+ nonce: randomBytes(16).toString("hex"),
454
+ createdAt: now
455
+ };
456
+ try {
457
+ writeFileSync2(ownerPath(lockDir), `${JSON.stringify(owner)}
458
+ `, {
459
+ encoding: "utf8",
460
+ mode: FILE_MODE,
461
+ flag: "wx"
462
+ });
463
+ return owner;
464
+ } catch (error) {
465
+ rmSync2(lockDir, { recursive: true, force: true });
466
+ throw error;
467
+ }
468
+ }
469
+ function recoverStaleLock(lockDir, now, alive, initGraceMs) {
470
+ const owner = readOwner(lockDir);
471
+ if (owner) {
472
+ if (alive(owner.pid)) return false;
473
+ if (!sameOwner(readOwner(lockDir), owner)) return false;
474
+ rmSync2(lockDir, { recursive: true, force: true });
475
+ return true;
476
+ }
477
+ try {
478
+ const before = statSync(lockDir).mtimeMs;
479
+ if (now - before < initGraceMs) return false;
480
+ if (readOwner(lockDir)) return false;
481
+ if (statSync(lockDir).mtimeMs !== before) return false;
482
+ rmSync2(lockDir, { recursive: true, force: true });
483
+ return true;
484
+ } catch {
485
+ return true;
486
+ }
487
+ }
488
+ function abortError(signal) {
489
+ if (signal.reason instanceof Error) return signal.reason;
490
+ const error = new Error("The operation was aborted");
491
+ error.name = "AbortError";
492
+ return error;
493
+ }
494
+ function throwIfAborted(signal) {
495
+ if (signal?.aborted) throw abortError(signal);
496
+ }
497
+ async function defaultSleep(ms, signal) {
498
+ if (!signal) {
499
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
500
+ return;
501
+ }
502
+ throwIfAborted(signal);
503
+ await new Promise((resolve2, reject) => {
504
+ const timer = setTimeout(() => {
505
+ signal.removeEventListener("abort", onAbort);
506
+ resolve2();
507
+ }, ms);
508
+ const onAbort = () => {
509
+ clearTimeout(timer);
510
+ reject(abortError(signal));
511
+ };
512
+ signal.addEventListener("abort", onAbort, { once: true });
513
+ });
514
+ }
515
+ var syncSleeper = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
516
+ async function withFileLock(lockDir, operation, options = {}) {
517
+ mkdirSync2(dirname2(lockDir), { recursive: true, mode: DIRECTORY_MODE });
518
+ const nowMs = options.nowMs ?? Date.now;
519
+ const alive = options.processAlive ?? processIsAlive;
520
+ const deadline = nowMs() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
521
+ let owner = null;
522
+ while (!owner) {
523
+ throwIfAborted(options.signal);
524
+ const now = nowMs();
525
+ owner = tryTakeLock(lockDir, now);
526
+ if (owner) break;
527
+ recoverStaleLock(lockDir, now, alive, options.initGraceMs ?? DEFAULT_INIT_GRACE_MS);
528
+ if (now >= deadline) {
529
+ throw new Error(`timed out waiting for file lock ${lockDir}`);
530
+ }
531
+ if (options.sleep) {
532
+ await options.sleep(options.pollMs ?? DEFAULT_POLL_MS);
533
+ throwIfAborted(options.signal);
534
+ } else {
535
+ await defaultSleep(options.pollMs ?? DEFAULT_POLL_MS, options.signal);
536
+ }
537
+ }
538
+ try {
539
+ throwIfAborted(options.signal);
540
+ return await operation();
541
+ } finally {
542
+ if (sameOwner(readOwner(lockDir), owner)) {
543
+ rmSync2(lockDir, { recursive: true, force: true });
544
+ }
545
+ }
546
+ }
547
+
548
+ // src/lib/terminal-safe.ts
549
+ function replaceIsolatedSurrogates(value) {
550
+ let out = "";
551
+ for (let index = 0; index < value.length; index += 1) {
552
+ const unit = value.charCodeAt(index);
553
+ if (unit >= 55296 && unit <= 56319) {
554
+ const next = value.charCodeAt(index + 1);
555
+ if (next >= 56320 && next <= 57343) {
556
+ out += value[index] + value[index + 1];
557
+ index += 1;
558
+ } else {
559
+ out += "\uFFFD";
560
+ }
561
+ } else if (unit >= 56320 && unit <= 57343) {
562
+ out += "\uFFFD";
563
+ } else {
564
+ out += value[index];
565
+ }
566
+ }
567
+ return out;
568
+ }
569
+ function isUnsafeTerminalCodePoint(point) {
570
+ const c0OrC1 = point <= 31 || point >= 127 && point <= 159;
571
+ const bidiOrZeroWidth = point === 173 || // soft hyphen
572
+ point === 1564 || // Arabic letter mark
573
+ point === 6158 || // Mongolian vowel separator
574
+ point >= 8203 && point <= 8207 || // zero-width + directional marks
575
+ point >= 8234 && point <= 8238 || // bidi embeddings/overrides
576
+ point >= 8288 && point <= 8303 || // word joiner + bidi isolates/controls
577
+ point >= 65529 && point <= 65531 || // interlinear annotation controls
578
+ point === 65279 || // zero-width no-break space / BOM
579
+ point >= 917504 && point <= 917631;
580
+ return c0OrC1 || bidiOrZeroWidth;
581
+ }
582
+ function terminalSafeText(value) {
583
+ return Array.from(replaceIsolatedSurrogates(value)).filter((character) => !isUnsafeTerminalCodePoint(character.codePointAt(0) ?? 0)).join("");
584
+ }
585
+ function terminalSafeLine(value) {
586
+ return terminalSafeText(value.replace(/\s+/gu, " ")).replace(/\s+/gu, " ").trim();
587
+ }
588
+
589
+ // src/client.ts
590
+ var CONFIG_DIR_MODE = 448;
591
+ var CREDENTIAL_FILE_MODE = 384;
592
+ var REFRESH_THRESHOLD_MS = 6e4;
593
+ var DEFAULT_API_URL = "https://api.getprimitive.ai";
594
+ var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
595
+ function readTrimmed2(path) {
596
+ try {
597
+ const value = readFileSync3(path, "utf8").trim();
598
+ return value || void 0;
599
+ } catch {
600
+ return void 0;
601
+ }
602
+ }
603
+ function getSiteUrl() {
604
+ return getSiteUrlForEnvironment(process.env.PRIM_API_URL);
605
+ }
606
+ function getSiteUrlForEnvironment(primApiUrl) {
607
+ return primApiUrl?.trim() || DEFAULT_API_URL;
608
+ }
609
+ function expiresAtFor(token, expiresIn) {
610
+ return expiresIn === void 0 ? jwtExpiresAt(token) : Date.now() + expiresIn * 1e3;
611
+ }
612
+ function ensureConfigDirectory() {
613
+ const directory = dirname3(TOKEN_FILE_PATH);
614
+ mkdirSync3(directory, { recursive: true, mode: CONFIG_DIR_MODE });
615
+ chmodSync(directory, CONFIG_DIR_MODE);
616
+ }
617
+ function atomicWrite(path, content) {
618
+ ensureConfigDirectory();
619
+ atomicWriteFile(path, content, { mode: CREDENTIAL_FILE_MODE });
620
+ }
621
+ function removeCredentialFile(path) {
622
+ if (!existsSync2(path)) return false;
623
+ rmSync3(path, { force: true });
624
+ return true;
625
+ }
626
+ function getTokenExpiresAt() {
627
+ const stored = readTrimmed2(TOKEN_EXPIRES_PATH);
628
+ if (stored === void 0) return void 0;
629
+ const value = Number(stored);
630
+ return Number.isNaN(value) ? void 0 : value;
631
+ }
632
+ function isTokenExpiringSoon(credential) {
633
+ if (credential.source !== "token_file") return false;
634
+ const expiresAt = getTokenExpiresAt();
635
+ return expiresAt !== void 0 && Date.now() >= expiresAt - REFRESH_THRESHOLD_MS;
636
+ }
637
+ function refreshFingerprint(refreshToken2) {
638
+ return createHash2("sha256").update(refreshToken2).digest("hex");
639
+ }
640
+ function legacyBrokerCredentialMetadata(accessToken, refreshToken2) {
641
+ return {
642
+ version: 1,
643
+ family: "legacy_broker",
644
+ accessTokenHash: refreshFingerprint(accessToken),
645
+ refreshTokenHash: refreshFingerprint(refreshToken2)
646
+ };
647
+ }
648
+ function credentialFamilyMetadata(accessToken, refreshToken2, metadata) {
649
+ const hashes = {
650
+ accessTokenHash: refreshFingerprint(accessToken),
651
+ refreshTokenHash: refreshFingerprint(refreshToken2)
652
+ };
653
+ return metadata === void 0 ? legacyBrokerCredentialMetadata(accessToken, refreshToken2) : { ...metadata, ...hashes };
654
+ }
655
+ function writeCredentialMigrationSentinel() {
656
+ atomicWrite(
657
+ CREDENTIAL_MIGRATION_PATH,
658
+ `${JSON.stringify({
659
+ version: CREDENTIAL_MIGRATION_VERSION,
660
+ state: CREDENTIAL_MIGRATION_STATE
661
+ })}
662
+ `
663
+ );
664
+ }
665
+ function bindLegacyBrokerGeneration(accessToken, refreshToken2, metadata) {
666
+ if (metadata.state !== "legacy_broker" || !accessToken || !refreshToken2) {
667
+ return { state: "invalid" };
668
+ }
669
+ const familyMetadata = legacyBrokerCredentialMetadata(accessToken, refreshToken2);
670
+ if (metadata.metadata !== void 0 && (metadata.metadata.accessTokenHash !== familyMetadata.accessTokenHash || metadata.metadata.refreshTokenHash !== familyMetadata.refreshTokenHash)) {
671
+ return { state: "invalid" };
672
+ }
673
+ if (metadata.metadata === void 0) {
674
+ atomicWrite(CREDENTIAL_FAMILY_PATH, `${JSON.stringify(familyMetadata)}
675
+ `);
676
+ }
677
+ const migrationState = readCredentialMigrationState();
678
+ if (migrationState === "invalid") return { state: "invalid" };
679
+ if (migrationState === "absent") writeCredentialMigrationSentinel();
680
+ return { state: "legacy_broker", metadata: familyMetadata };
681
+ }
682
+ function preserveMarkerlessLegacyGeneration() {
683
+ const metadata = readStoredCredentialMetadata();
684
+ if (metadata.state !== "legacy_broker" || metadata.metadata !== void 0) return;
685
+ const accessToken = readTrimmed2(TOKEN_FILE_PATH);
686
+ const refreshToken2 = readTrimmed2(REFRESH_TOKEN_PATH);
687
+ if (!(accessToken && refreshToken2)) return;
688
+ const familyMetadata = legacyBrokerCredentialMetadata(accessToken, refreshToken2);
689
+ atomicWrite(CREDENTIAL_FAMILY_PATH, `${JSON.stringify(familyMetadata)}
690
+ `);
691
+ }
692
+ function terminalFingerprint() {
693
+ return readTrimmed2(TERMINAL_REFRESH_PATH);
694
+ }
695
+ function writeTerminalFingerprint(refreshToken2) {
696
+ atomicWrite(TERMINAL_REFRESH_PATH, `${refreshFingerprint(refreshToken2)}
697
+ `);
698
+ }
699
+ function clearTerminalFingerprint() {
700
+ removeCredentialFile(TERMINAL_REFRESH_PATH);
701
+ }
702
+ function isSessionEnded() {
703
+ if (resolveAuthCredential()?.source !== "token_file") return false;
704
+ const refreshToken2 = readTrimmed2(REFRESH_TOKEN_PATH);
705
+ const ended = terminalFingerprint();
706
+ return Boolean(refreshToken2 && ended && refreshFingerprint(refreshToken2) === ended);
707
+ }
708
+ function withCredentialLock(operation, options = {}) {
709
+ return withFileLock(CREDENTIAL_LOCK_PATH, operation, options);
710
+ }
711
+ function commitCredentialsUnlocked(credentials) {
712
+ const accessToken = credentials.accessToken.trim();
713
+ const refreshToken2 = credentials.refreshToken.trim();
714
+ if (!accessToken || !refreshToken2) {
715
+ throw new Error("OAuth credentials require both access and refresh tokens");
716
+ }
717
+ const familyMetadata = credentialFamilyMetadata(accessToken, refreshToken2, credentials.metadata);
718
+ if (credentials.metadata !== void 0) {
719
+ preserveMarkerlessLegacyGeneration();
720
+ writeCredentialMigrationSentinel();
721
+ atomicWrite(CREDENTIAL_FAMILY_PATH, `${JSON.stringify(familyMetadata)}
722
+ `);
723
+ atomicWrite(CREDENTIAL_METADATA_PATH, `${JSON.stringify(familyMetadata)}
724
+ `);
725
+ atomicWrite(REFRESH_TOKEN_PATH, `${refreshToken2}
726
+ `);
727
+ } else {
728
+ atomicWrite(REFRESH_TOKEN_PATH, `${refreshToken2}
729
+ `);
730
+ atomicWrite(CREDENTIAL_FAMILY_PATH, `${JSON.stringify(familyMetadata)}
731
+ `);
732
+ removeCredentialFile(CREDENTIAL_METADATA_PATH);
733
+ }
734
+ const expiresAt = expiresAtFor(accessToken, credentials.expiresIn);
735
+ if (expiresAt === void 0) removeCredentialFile(TOKEN_EXPIRES_PATH);
736
+ else atomicWrite(TOKEN_EXPIRES_PATH, `${expiresAt}
737
+ `);
738
+ atomicWrite(TOKEN_FILE_PATH, `${accessToken}
739
+ `);
740
+ clearTerminalFingerprint();
741
+ _cachedCredential = { token: accessToken, source: "token_file" };
742
+ }
743
+ function isTerminalRefreshResponse(response, detail) {
744
+ if (detail.includes("invalid_grant") || detail.includes("Session has already ended")) return true;
745
+ try {
746
+ const error = JSON.parse(detail).error;
747
+ return error === "invalid_grant" || response.status === 401 && error === "Invalid or expired refresh token";
748
+ } catch {
749
+ return response.status === 401 && detail.includes("Invalid or expired refresh token");
750
+ }
751
+ }
752
+ function workosConnectRefreshErrorCode(value) {
753
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
754
+ return value.error === "invalid_grant" ? "invalid_grant" : "";
755
+ }
756
+ function refreshDiagnostic(response, detail, quiet, provider) {
757
+ if (quiet) return;
758
+ const statusText = terminalSafeLine(response.statusText).slice(0, 120);
759
+ const safeDetail = terminalSafeLine(detail).slice(0, 200);
760
+ process.stderr.write(
761
+ `[prim] token refresh rejected by ${provider}: ${response.status}${statusText ? ` ${statusText}` : ""}${safeDetail ? ` \u2014 ${safeDetail}` : ""}
762
+ `
763
+ );
764
+ }
765
+ function metadataMatchesCredentialGeneration(expected, accessToken, refreshToken2) {
766
+ const current = readStoredCredentialMetadata();
767
+ if (expected.state === "legacy_broker") {
768
+ if (current.state !== "legacy_broker") return false;
769
+ if (!expected.metadata) return current.metadata === void 0;
770
+ return current.metadata !== void 0 && current.metadata.accessTokenHash === refreshFingerprint(accessToken) && current.metadata.accessTokenHash === expected.metadata.accessTokenHash && current.metadata.refreshTokenHash === refreshFingerprint(refreshToken2) && current.metadata.refreshTokenHash === expected.metadata.refreshTokenHash;
771
+ }
772
+ if (expected.state !== "workos_connect" || current.state !== "workos_connect") return false;
773
+ return current.metadata.issuer === expected.metadata.issuer && current.metadata.clientId === expected.metadata.clientId && current.metadata.accessTokenHash === refreshFingerprint(accessToken) && current.metadata.accessTokenHash === expected.metadata.accessTokenHash && current.metadata.refreshTokenHash === refreshFingerprint(refreshToken2) && current.metadata.refreshTokenHash === expected.metadata.refreshTokenHash;
774
+ }
775
+ function uncoordinatedLegacyWinnerAccessToken(expected, currentGeneration) {
776
+ if (!expected || readCredentialMigrationState() !== "valid") return void 0;
777
+ const stored = readStoredCredentialMetadata();
778
+ if (stored.state !== "legacy_broker" || stored.metadata === void 0 || stored.metadata.accessTokenHash !== expected.accessTokenHash || stored.metadata.refreshTokenHash !== expected.refreshTokenHash) {
779
+ return void 0;
780
+ }
781
+ const replacementRefreshToken = readTrimmed2(REFRESH_TOKEN_PATH);
782
+ if (!replacementRefreshToken || replacementRefreshToken === currentGeneration) return void 0;
783
+ return readTrimmed2(TOKEN_FILE_PATH);
784
+ }
785
+ function winnerAfterRefreshRace(currentGeneration, expectedLegacyGeneration) {
786
+ if (readTrimmed2(REFRESH_TOKEN_PATH) === currentGeneration) return void 0;
787
+ const winner = resolveAuthCredential();
788
+ return (winner?.source === "token_file" ? winner.token : void 0) ?? uncoordinatedLegacyWinnerAccessToken(expectedLegacyGeneration, currentGeneration);
789
+ }
790
+ async function performTokenRefresh(options = {}) {
791
+ const selected = resolveAuthCredential();
792
+ if (selected?.source !== "token_file") return void 0;
793
+ const startingGeneration = readTrimmed2(REFRESH_TOKEN_PATH);
794
+ if (!startingGeneration || isSessionEnded()) return void 0;
795
+ const startingAccessToken = selected.token;
796
+ return withCredentialLock(
797
+ async () => {
798
+ const currentCredential = resolveAuthCredential();
799
+ if (currentCredential?.source !== "token_file") return void 0;
800
+ const currentGeneration = readTrimmed2(REFRESH_TOKEN_PATH);
801
+ if (!currentGeneration) return void 0;
802
+ if (currentGeneration !== startingGeneration) {
803
+ return currentCredential.token;
804
+ }
805
+ if (currentCredential.token !== startingAccessToken) {
806
+ return currentCredential.token;
807
+ }
808
+ if (isSessionEnded()) return void 0;
809
+ let metadata = readStoredCredentialMetadata();
810
+ const markerlessLegacyAtStart = metadata.state === "legacy_broker" && metadata.metadata === void 0;
811
+ if (metadata.state === "invalid" || metadata.state === "workos_connect" && (metadata.metadata.accessTokenHash !== refreshFingerprint(currentCredential.token) || metadata.metadata.refreshTokenHash !== refreshFingerprint(currentGeneration))) {
812
+ return void 0;
813
+ }
814
+ if (metadata.state === "legacy_broker") {
815
+ try {
816
+ metadata = bindLegacyBrokerGeneration(
817
+ currentCredential.token,
818
+ currentGeneration,
819
+ metadata
820
+ );
821
+ } catch {
822
+ return void 0;
823
+ }
824
+ if (metadata.state !== "legacy_broker") return void 0;
825
+ }
826
+ const expectedLegacyGeneration = markerlessLegacyAtStart && metadata.state === "legacy_broker" ? metadata.metadata : void 0;
827
+ const connectContext = metadata.state === "workos_connect" ? {
828
+ version: 1,
829
+ family: "workos_connect",
830
+ issuer: metadata.metadata.issuer,
831
+ clientId: metadata.metadata.clientId
832
+ } : void 0;
833
+ const response = await fetch(
834
+ connectContext ? workosConnectTokenEndpoint(connectContext.issuer) : `${getSiteUrl()}/mcp/broker/refresh`,
835
+ connectContext ? {
836
+ method: "POST",
837
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
838
+ body: new URLSearchParams({
839
+ grant_type: "refresh_token",
840
+ refresh_token: currentGeneration,
841
+ client_id: connectContext.clientId
842
+ }),
843
+ signal: options.signal
844
+ } : {
845
+ method: "POST",
846
+ headers: { "Content-Type": "application/json" },
847
+ body: JSON.stringify({ refresh_token: currentGeneration }),
848
+ signal: options.signal
849
+ }
850
+ );
851
+ let directResponse;
852
+ if (connectContext) {
853
+ try {
854
+ directResponse = await readBoundedJson(response, WORKOS_CONNECT_RESPONSE_MAX_BYTES);
855
+ } catch {
856
+ if (response.ok && readTrimmed2(REFRESH_TOKEN_PATH) === currentGeneration && metadataMatchesCredentialGeneration(
857
+ metadata,
858
+ currentCredential.token,
859
+ currentGeneration
860
+ )) {
861
+ writeTerminalFingerprint(currentGeneration);
862
+ }
863
+ return void 0;
864
+ }
865
+ }
866
+ if (!response.ok) {
867
+ let detail = "";
868
+ if (connectContext) {
869
+ detail = workosConnectRefreshErrorCode(directResponse);
870
+ } else {
871
+ detail = (await response.text().catch(() => "")).slice(0, 200);
872
+ }
873
+ if (isTerminalRefreshResponse(response, detail) && readTrimmed2(REFRESH_TOKEN_PATH) === currentGeneration && metadataMatchesCredentialGeneration(metadata, currentCredential.token, currentGeneration)) {
874
+ writeTerminalFingerprint(currentGeneration);
875
+ }
876
+ refreshDiagnostic(
877
+ response,
878
+ detail,
879
+ options.quiet,
880
+ connectContext ? "WorkOS Connect" : "broker"
881
+ );
882
+ return winnerAfterRefreshRace(currentGeneration, expectedLegacyGeneration);
883
+ }
884
+ let accessToken;
885
+ let replacementRefreshToken;
886
+ let expiresIn;
887
+ if (connectContext) {
888
+ try {
889
+ const tokens = parseWorkosConnectTokens(directResponse, {
890
+ fallbackRefreshToken: currentGeneration
891
+ });
892
+ accessToken = tokens.accessToken;
893
+ replacementRefreshToken = tokens.refreshToken;
894
+ expiresIn = tokens.expiresIn;
895
+ } catch {
896
+ if (readTrimmed2(REFRESH_TOKEN_PATH) === currentGeneration && metadataMatchesCredentialGeneration(
897
+ metadata,
898
+ currentCredential.token,
899
+ currentGeneration
900
+ )) {
901
+ writeTerminalFingerprint(currentGeneration);
902
+ }
903
+ return void 0;
904
+ }
905
+ } else {
906
+ let data;
907
+ try {
908
+ data = await response.json();
909
+ } catch {
910
+ if (readTrimmed2(REFRESH_TOKEN_PATH) === currentGeneration) {
911
+ writeTerminalFingerprint(currentGeneration);
912
+ }
913
+ return void 0;
914
+ }
915
+ const record = typeof data === "object" && data !== null && !Array.isArray(data) ? data : void 0;
916
+ accessToken = typeof record?.access_token === "string" ? record.access_token.trim() : "";
917
+ replacementRefreshToken = typeof record?.refresh_token === "string" ? record.refresh_token.trim() : "";
918
+ expiresIn = typeof record?.expires_in === "number" && Number.isFinite(record.expires_in) ? record.expires_in : void 0;
919
+ if (!(accessToken && replacementRefreshToken)) {
920
+ if (readTrimmed2(REFRESH_TOKEN_PATH) === currentGeneration) {
921
+ writeTerminalFingerprint(currentGeneration);
922
+ }
923
+ return void 0;
924
+ }
925
+ }
926
+ const latestCredential = resolveAuthCredential();
927
+ if (readTrimmed2(REFRESH_TOKEN_PATH) !== currentGeneration || latestCredential?.source !== "token_file" || latestCredential.token !== currentCredential.token || !metadataMatchesCredentialGeneration(metadata, currentCredential.token, currentGeneration)) {
928
+ return latestCredential?.source === "token_file" ? latestCredential.token : winnerAfterRefreshRace(currentGeneration, expectedLegacyGeneration);
929
+ }
930
+ commitCredentialsUnlocked({
931
+ accessToken,
932
+ refreshToken: replacementRefreshToken,
933
+ expiresIn,
934
+ metadata: connectContext
935
+ });
936
+ return accessToken;
937
+ },
938
+ { signal: options.signal }
939
+ );
940
+ }
941
+ var _refreshInFlight;
942
+ function refreshToken(options = {}) {
943
+ if (_refreshInFlight) return _refreshInFlight;
944
+ const attempt = performTokenRefresh(options).then((token) => {
945
+ if (token) _cachedCredential = { token, source: "token_file" };
946
+ return token;
947
+ }).finally(() => {
948
+ if (_refreshInFlight === attempt) _refreshInFlight = void 0;
949
+ });
950
+ _refreshInFlight = attempt;
951
+ return attempt;
952
+ }
953
+ var HttpError = class extends Error {
954
+ status;
955
+ body;
956
+ constructor(status, message, body) {
957
+ super(message);
958
+ this.name = "HttpError";
959
+ this.status = status;
960
+ this.body = body;
961
+ }
962
+ };
963
+ var CLI_MANAGEMENT_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
964
+ var _cachedCredential;
965
+ function selectedCredential() {
966
+ const resolved = resolveAuthCredential();
967
+ if (!_cachedCredential || resolved?.token !== _cachedCredential.token || resolved?.source !== _cachedCredential.source) {
968
+ _cachedCredential = resolved;
969
+ }
970
+ return _cachedCredential;
971
+ }
972
+ function fetchWithToken(method, path, body, options, token, siteUrl) {
973
+ return fetch(`${siteUrl}${path}`, {
974
+ method,
975
+ headers: {
976
+ Authorization: `Bearer ${token}`,
977
+ "Content-Type": "application/json"
978
+ },
979
+ body: body === void 0 ? void 0 : JSON.stringify(body),
980
+ signal: options?.signal
981
+ });
982
+ }
983
+ async function responseValue(response) {
984
+ if (!response.ok) {
985
+ if (response.status === 401) {
986
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
987
+ }
988
+ const errorBody = await response.json().catch(() => null);
989
+ const errorRecord = typeof errorBody === "object" && errorBody !== null && !Array.isArray(errorBody) ? errorBody : void 0;
990
+ const message = typeof errorRecord?.error === "string" ? errorRecord.error : `HTTP ${response.status}`;
991
+ throw new HttpError(response.status, message, errorBody);
992
+ }
993
+ return response.json();
994
+ }
995
+ async function request(method, path, body, options) {
996
+ const siteUrl = getSiteUrl();
997
+ let credential = selectedCredential();
998
+ let refreshAttempted = false;
999
+ if (credential && isTokenExpiringSoon(credential)) {
1000
+ refreshAttempted = true;
1001
+ const token = await refreshToken({
1002
+ signal: options?.signal,
1003
+ quiet: options?.quietRefresh
1004
+ });
1005
+ if (token) credential = { token, source: "token_file" };
1006
+ }
1007
+ if (!credential) {
1008
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
1009
+ }
1010
+ if (isTokenExpiringSoon(credential) && isSessionEnded()) {
1011
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
1012
+ }
1013
+ const tokenUsed = credential.token;
1014
+ let response = await fetchWithToken(method, path, body, options, tokenUsed, siteUrl);
1015
+ if (response.status === 401) {
1016
+ const latest = resolveAuthCredential();
1017
+ let retryToken = latest?.token !== tokenUsed ? latest?.token : void 0;
1018
+ if (!retryToken && latest?.source === "token_file" && !refreshAttempted) {
1019
+ refreshAttempted = true;
1020
+ retryToken = await refreshToken({ signal: options?.signal, quiet: options?.quietRefresh });
1021
+ }
1022
+ if (retryToken) {
1023
+ _cachedCredential = {
1024
+ token: retryToken,
1025
+ source: latest?.source === "token_file" ? "token_file" : latest?.source ?? "token_file"
1026
+ };
1027
+ response = await fetchWithToken(method, path, body, options, retryToken, siteUrl);
1028
+ }
1029
+ }
1030
+ return responseValue(response);
1031
+ }
1032
+ function getClient() {
1033
+ return {
1034
+ get: (path, options) => request("GET", path, void 0, options),
1035
+ post: (path, body, options) => request("POST", path, body, options)
1036
+ };
1037
+ }
1038
+
1039
+ // src/daemon/client.ts
1040
+ import { createConnection } from "net";
1041
+ import { join as join5 } from "path";
1042
+
1043
+ // src/daemon/principal.ts
1044
+ import { createHash as createHash3 } from "crypto";
1045
+ var PRINCIPAL_COMPONENT_MAX_LENGTH = 512;
1046
+ function boundedClaim(value) {
1047
+ return typeof value === "string" && value.length > 0 && value.length <= PRINCIPAL_COMPONENT_MAX_LENGTH ? value : void 0;
1048
+ }
1049
+ function sha256(value) {
1050
+ return createHash3("sha256").update(value).digest("hex");
1051
+ }
1052
+ function resolveDaemonPrincipal(token = resolveAuthCredential()?.token) {
1053
+ if (!token) return void 0;
1054
+ const payload = decodeJwtPayload(token);
1055
+ const subject = boundedClaim(payload?.sub);
1056
+ const organizationId = boundedClaim(payload?.org_id);
1057
+ if (!(subject && organizationId)) return void 0;
1058
+ const issuer = boundedClaim(payload?.iss) ?? "unknown-issuer";
1059
+ return {
1060
+ principalId: sha256(`${issuer}\0${subject}`),
1061
+ organizationId,
1062
+ credentialFingerprint: sha256(token)
1063
+ };
1064
+ }
1065
+
1066
+ // src/daemon/protocol.ts
1067
+ var DAEMON_JSON_REQUEST_MAX_BYTES = 64 * 1024;
1068
+ var DAEMON_JSON_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
1069
+ function isRecord(value) {
1070
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1071
+ }
1072
+ function parseDaemonResponseEnvelope(value) {
1073
+ if (!isRecord(value)) return void 0;
1074
+ if (!(typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0)) {
1075
+ return void 0;
1076
+ }
1077
+ if (typeof value.ok !== "boolean") return void 0;
1078
+ if (value.error !== void 0 && typeof value.error !== "string") return void 0;
1079
+ return {
1080
+ id: value.id,
1081
+ ok: value.ok,
1082
+ ...value.result === void 0 ? {} : { result: value.result },
1083
+ ...value.error === void 0 ? {} : { error: value.error }
1084
+ };
1085
+ }
1086
+
1087
+ // src/daemon/client.ts
1088
+ var SOCK_PATH = join5(primConfigDirectory(), "sock");
1089
+ var DEFAULT_TIMEOUT_MS2 = 250;
1090
+ var nextRequestId = 1;
1091
+ function daemonRequest(method, params = {}, opts = {}) {
1092
+ const id = nextRequestId++;
1093
+ const caller = resolveDaemonPrincipal();
1094
+ const request2 = {
1095
+ id,
1096
+ method,
1097
+ params,
1098
+ ...caller ? { caller } : {}
1099
+ };
1100
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1101
+ return new Promise((resolve2) => {
1102
+ let settled = false;
1103
+ let socket;
1104
+ let buffer = Buffer.alloc(0);
1105
+ const settle = (value) => {
1106
+ if (settled) {
1107
+ return;
1108
+ }
1109
+ settled = true;
1110
+ if (socket) {
1111
+ try {
1112
+ socket.destroy();
1113
+ } catch {
1114
+ }
1115
+ }
1116
+ resolve2(value);
1117
+ };
1118
+ const timer = setTimeout(() => settle(null), timeoutMs);
1119
+ timer.unref();
1120
+ try {
1121
+ socket = createConnection(SOCK_PATH);
1122
+ } catch {
1123
+ clearTimeout(timer);
1124
+ settle(null);
1125
+ return;
1126
+ }
1127
+ socket.on("error", () => {
1128
+ clearTimeout(timer);
1129
+ settle(null);
1130
+ });
1131
+ socket.on("connect", () => {
1132
+ try {
1133
+ socket?.write(`${JSON.stringify(request2)}
1134
+ `);
1135
+ } catch {
1136
+ clearTimeout(timer);
1137
+ settle(null);
1138
+ }
1139
+ });
1140
+ socket.on("data", (chunk2) => {
1141
+ buffer = Buffer.concat([buffer, Buffer.from(chunk2)]);
1142
+ if (buffer.length > DAEMON_JSON_RESPONSE_MAX_BYTES) {
1143
+ clearTimeout(timer);
1144
+ settle(null);
1145
+ return;
1146
+ }
1147
+ const newlineIdx = buffer.indexOf(10);
1148
+ if (newlineIdx === -1) {
1149
+ return;
1150
+ }
1151
+ const line = buffer.subarray(0, newlineIdx).toString("utf8");
1152
+ try {
1153
+ const res = parseDaemonResponseEnvelope(JSON.parse(line));
1154
+ if (!res || res.id !== id) {
1155
+ clearTimeout(timer);
1156
+ settle(null);
1157
+ return;
1158
+ }
1159
+ clearTimeout(timer);
1160
+ settle(res.ok && res.result !== void 0 ? res.result : null);
1161
+ } catch {
1162
+ clearTimeout(timer);
1163
+ settle(null);
1164
+ }
1165
+ });
1166
+ socket.on("end", () => {
1167
+ if (!settled) {
1168
+ clearTimeout(timer);
1169
+ settle(null);
1170
+ }
1171
+ });
1172
+ });
1173
+ }
1174
+
1175
+ // src/daemon/proxy.ts
1176
+ var DAEMON_HTTP_TIMEOUT_MS = 1e4;
1177
+ var DAEMON_PROBE_TIMEOUT_MS = 250;
1178
+ async function daemonOrDirect(method, params, direct) {
1179
+ const fromDaemon = await daemonRequest(
1180
+ method,
1181
+ { ...params, callerEnv: getSiteUrl() },
1182
+ { timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
1183
+ );
1184
+ if (fromDaemon !== null) {
1185
+ return fromDaemon;
1186
+ }
1187
+ return await direct();
1188
+ }
1189
+ async function daemonOrDirectGet(method, path, client, timeoutMs = DAEMON_HTTP_TIMEOUT_MS) {
1190
+ return await daemonOrDirect(
1191
+ method,
1192
+ { path },
1193
+ async () => await client.get(path, {
1194
+ signal: AbortSignal.timeout(timeoutMs)
1195
+ })
1196
+ );
1197
+ }
1198
+
1199
+ // src/hooks/decisions-check.ts
1200
+ var DECISIONS_CHECK_TIMEOUT_MS = 1e4;
1201
+ var MAX_FILES_PER_REQUEST = 25;
1202
+ var defaultDeps = { getClient };
1203
+ function chunk(items, size) {
1204
+ const out = [];
1205
+ for (let i = 0; i < items.length; i += size) {
1206
+ out.push(items.slice(i, i + size));
1207
+ }
1208
+ return out;
1209
+ }
1210
+ async function fetchAffecting(client, batch) {
1211
+ const params = new URLSearchParams();
1212
+ for (const file of batch) {
1213
+ params.append("files", file);
1214
+ }
1215
+ try {
1216
+ return await daemonOrDirectGet(
1217
+ "decisions_affecting",
1218
+ `/api/cli/decisions/affecting?${params.toString()}`,
1219
+ client,
1220
+ DECISIONS_CHECK_TIMEOUT_MS
1221
+ );
1222
+ } catch (err) {
1223
+ const detail = err instanceof Error ? err.message : String(err);
1224
+ return { decisions: [], truncated: false, unavailable: `decision check failed: ${detail}` };
1225
+ }
1226
+ }
1227
+ async function checkAffectedDecisions(filePaths, deps = defaultDeps) {
1228
+ if (filePaths.length === 0) {
1229
+ return { decisions: [], truncated: false };
1230
+ }
1231
+ const client = deps.getClient();
1232
+ const responses = await Promise.all(
1233
+ chunk(filePaths, MAX_FILES_PER_REQUEST).map((batch) => fetchAffecting(client, batch))
1234
+ );
1235
+ const byId = /* @__PURE__ */ new Map();
1236
+ let truncated = false;
1237
+ let unavailable;
1238
+ for (const res of responses) {
1239
+ if (res.unavailable !== void 0 && unavailable === void 0) {
1240
+ unavailable = res.unavailable;
1241
+ }
1242
+ truncated ||= res.truncated === true;
1243
+ for (const d of res.decisions) {
1244
+ const existing = byId.get(d.id);
1245
+ if (existing) {
1246
+ existing.matchedFiles = [.../* @__PURE__ */ new Set([...existing.matchedFiles, ...d.matchedFiles])];
1247
+ } else {
1248
+ byId.set(d.id, { ...d, matchedFiles: [...d.matchedFiles] });
1249
+ }
1250
+ }
1251
+ }
1252
+ const result = { decisions: [...byId.values()], truncated };
1253
+ if (unavailable !== void 0) {
1254
+ result.unavailable = unavailable;
1255
+ }
1256
+ return result;
1257
+ }
1258
+ var FILES_PREVIEW_LIMIT = 3;
1259
+ function formatDecisionsWarning(result) {
1260
+ const lines = [];
1261
+ if (result.unavailable !== void 0) {
1262
+ lines.push(`[prim] decision check not verified \u2014 ${terminalSafeLine(result.unavailable)}`);
1263
+ }
1264
+ if (result.decisions.length > 0) {
1265
+ lines.push(
1266
+ `[prim] ${String(result.decisions.length)} active decision(s) reference staged files:`
1267
+ );
1268
+ for (const d of result.decisions) {
1269
+ const statusMark = d.status === "under_review" ? " (under review)" : "";
1270
+ const preview = d.matchedFiles.slice(0, FILES_PREVIEW_LIMIT).map(terminalSafeLine).join(", ");
1271
+ const overflow = d.matchedFiles.length > FILES_PREVIEW_LIMIT ? ` (+${String(d.matchedFiles.length - FILES_PREVIEW_LIMIT)} more)` : "";
1272
+ lines.push(` \xB7 ${terminalSafeLine(d.intent)}${statusMark}`);
1273
+ lines.push(` files: ${preview}${overflow}`);
1274
+ if (d.rationale) {
1275
+ lines.push(` rationale: ${terminalSafeLine(d.rationale)}`);
1276
+ }
1277
+ }
1278
+ }
1279
+ if (result.truncated) {
1280
+ lines.push(
1281
+ "[prim] result truncated \u2014 more files than the server checks per request; not all decisions shown"
1282
+ );
1283
+ }
1284
+ return lines.join("\n");
1285
+ }
1286
+
1287
+ // src/hooks/pre-commit.ts
1288
+ function getStagedFiles() {
1289
+ const output = execSync("git diff --cached --name-only", {
1290
+ encoding: "utf-8"
1291
+ });
1292
+ return output.trim().split("\n").filter((f) => f.length > 0);
1293
+ }
1294
+ async function runDecisionsCheck() {
1295
+ const stagedFiles = getStagedFiles();
1296
+ if (stagedFiles.length === 0) {
1297
+ return { decisions: [], truncated: false };
1298
+ }
1299
+ return checkAffectedDecisions(stagedFiles);
1300
+ }
1301
+ async function main() {
1302
+ const decisionsResult = await runDecisionsCheck();
1303
+ const warning = formatDecisionsWarning(decisionsResult);
1304
+ if (warning) {
1305
+ console.error(warning);
1306
+ }
1307
+ process.exit(0);
1308
+ }
1309
+ if (!process.env.VITEST) {
1310
+ main().catch((error) => {
1311
+ console.error("[prim] Pre-commit hook error:", error);
1312
+ process.exit(0);
1313
+ });
1314
+ }