@primitive.ai/prim 0.1.0-alpha.9 → 0.1.0-alpha.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +303 -37
  2. package/SKILL.md +269 -0
  3. package/dist/chunk-4GLVJLEQ.js +55 -0
  4. package/dist/chunk-5AFRMOPX.js +209 -0
  5. package/dist/chunk-5GYQ4J3D.js +126 -0
  6. package/dist/chunk-67VNLLBN.js +789 -0
  7. package/dist/chunk-BTNNEH2W.js +79 -0
  8. package/dist/chunk-EWZMDD44.js +1474 -0
  9. package/dist/chunk-EZXEWFDG.js +210 -0
  10. package/dist/chunk-FBAYCS3X.js +327 -0
  11. package/dist/chunk-FKXFUUWJ.js +36 -0
  12. package/dist/chunk-FOLKHXLJ.js +1529 -0
  13. package/dist/chunk-H4UNYFUC.js +196 -0
  14. package/dist/chunk-HU4PW6I6.js +264 -0
  15. package/dist/chunk-KFXF2V5G.js +155 -0
  16. package/dist/chunk-LFFLHSQO.js +78 -0
  17. package/dist/chunk-M6RIJVRE.js +18358 -0
  18. package/dist/chunk-NAHHFSLH.js +258 -0
  19. package/dist/chunk-OAAP2MY6.js +262 -0
  20. package/dist/chunk-PCVGBE5B.js +659 -0
  21. package/dist/chunk-VW3FDYRT.js +517 -0
  22. package/dist/chunk-WX52U7Z2.js +112 -0
  23. package/dist/chunk-ZB4F75SZ.js +843 -0
  24. package/dist/daemon/server.js +21633 -0
  25. package/dist/hook-runtime/daemon/server.js +21633 -0
  26. package/dist/hook-runtime/hooks/post-commit.js +19258 -0
  27. package/dist/hook-runtime/hooks/post-rewrite.js +19273 -0
  28. package/dist/hook-runtime/hooks/post-tool-use.js +26365 -0
  29. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  30. package/dist/hook-runtime/hooks/pre-tool-use.js +25832 -0
  31. package/dist/hook-runtime/hooks/prim-hook.js +26386 -0
  32. package/dist/hook-runtime/hooks/session-end.js +1078 -0
  33. package/dist/hook-runtime/hooks/session-start.js +28779 -0
  34. package/dist/hook-runtime/index.js +50987 -0
  35. package/dist/hook-runtime/statusline-main.js +809 -0
  36. package/dist/hooks/post-commit.js +190 -0
  37. package/dist/hooks/post-rewrite.js +203 -0
  38. package/dist/hooks/post-tool-use.js +288 -0
  39. package/dist/hooks/pre-commit.js +16 -101
  40. package/dist/hooks/pre-tool-use.js +369 -0
  41. package/dist/hooks/prim-hook.js +229 -0
  42. package/dist/hooks/session-end.js +59 -0
  43. package/dist/hooks/session-start.js +316 -0
  44. package/dist/index.js +8875 -429
  45. package/dist/statusline-main.js +809 -0
  46. package/package.json +35 -13
  47. package/dist/chunk-3APLWTLB.js +0 -165
@@ -0,0 +1,809 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/statusline.ts
4
+ import { readFileSync as readFileSync5 } from "fs";
5
+ import { dirname as dirname5, resolve as resolve3 } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/client.ts
9
+ import { createHash as createHash2 } from "crypto";
10
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3 } from "fs";
11
+ import { dirname as dirname3 } from "path";
12
+
13
+ // src/lib/atomic-file.ts
14
+ import { randomUUID } from "crypto";
15
+ import {
16
+ constants,
17
+ closeSync,
18
+ fchmodSync,
19
+ fsyncSync,
20
+ mkdirSync,
21
+ openSync,
22
+ renameSync,
23
+ rmSync,
24
+ writeFileSync
25
+ } from "fs";
26
+ import { dirname } from "path";
27
+
28
+ // src/lib/credentials.ts
29
+ import { createHash } from "crypto";
30
+ import { readFileSync } from "fs";
31
+ import { join as join2 } from "path";
32
+
33
+ // src/lib/paths.ts
34
+ import { homedir } from "os";
35
+ import { isAbsolute, join, normalize } from "path";
36
+ var UNSAFE_CONFIG_PATH_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
37
+ function absolutePath(value, variable) {
38
+ if (value !== void 0 && UNSAFE_CONFIG_PATH_CHARACTERS.test(value)) {
39
+ throw new Error(`${variable} contains unsafe characters`);
40
+ }
41
+ if (!value || value.trim() !== value || !isAbsolute(value)) return;
42
+ const normalized = normalize(value);
43
+ return normalized === value ? value : void 0;
44
+ }
45
+ function resolvePrimConfigDirectory(options = {}) {
46
+ const env = options.env ?? process.env;
47
+ const explicit = absolutePath(env.PRIM_CONFIG_DIR, "PRIM_CONFIG_DIR");
48
+ if (explicit) return { path: explicit, source: "explicit" };
49
+ const xdgConfigHome = absolutePath(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME");
50
+ if (xdgConfigHome) return { path: join(xdgConfigHome, "prim"), source: "xdg" };
51
+ const home = absolutePath(options.homeDir ?? homedir(), "HOME");
52
+ if (!home) {
53
+ throw new Error("cannot resolve Primitive config directory: HOME is not an absolute path");
54
+ }
55
+ return { path: join(home, ".config", "prim"), source: "default" };
56
+ }
57
+ function primConfigDirectory(options = {}) {
58
+ return resolvePrimConfigDirectory(options).path;
59
+ }
60
+
61
+ // src/lib/workos-connect.ts
62
+ var DISCOVERY_MAX_BYTES = 16 * 1024;
63
+ var WORKOS_CONNECT_RESPONSE_MAX_BYTES = 64 * 1024;
64
+ var MAX_DEVICE_LIFETIME_SECONDS = 60 * 60;
65
+ var MAX_TOKEN_LIFETIME_SECONDS = 24 * 60 * 60;
66
+ var MAX_URL_LENGTH = 2048;
67
+ var MAX_CLIENT_ID_LENGTH = 256;
68
+ var CONNECT_CLIENT_ID_PATTERN = /^client_[A-Za-z0-9_-]+$/u;
69
+ var WorkosConnectProtocolError = class extends Error {
70
+ code;
71
+ constructor(code, message) {
72
+ super(message);
73
+ this.name = "WorkosConnectProtocolError";
74
+ this.code = code;
75
+ }
76
+ };
77
+ function recordFrom(value, label) {
78
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
79
+ throw new WorkosConnectProtocolError("invalid_response", `${label} must be an object`);
80
+ }
81
+ return value;
82
+ }
83
+ function exactString(value, field, maxLength) {
84
+ const containsControl = typeof value === "string" && [...value].some((character) => {
85
+ const code = character.charCodeAt(0);
86
+ return code <= 31 || code === 127;
87
+ });
88
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength || value.trim() !== value || containsControl) {
89
+ throw new WorkosConnectProtocolError("invalid_response", `${field} is invalid`);
90
+ }
91
+ return value;
92
+ }
93
+ function canonicalIssuer(value) {
94
+ const issuer = exactString(value, "issuer", MAX_URL_LENGTH);
95
+ let parsed;
96
+ try {
97
+ parsed = new URL(issuer);
98
+ } catch {
99
+ throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
100
+ }
101
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.origin !== issuer) {
102
+ throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
103
+ }
104
+ return issuer;
105
+ }
106
+ function parseWorkosConnectCredentialMetadata(value) {
107
+ const record = recordFrom(value, "credential metadata");
108
+ if (record.version !== 1 || record.family !== "workos_connect") {
109
+ throw new WorkosConnectProtocolError("invalid_metadata", "credential metadata is invalid");
110
+ }
111
+ const issuer = canonicalIssuer(record.issuer);
112
+ const clientId = exactString(record.clientId, "clientId", MAX_CLIENT_ID_LENGTH);
113
+ const accessTokenHash = exactString(record.accessTokenHash, "accessTokenHash", 64);
114
+ const refreshTokenHash = exactString(record.refreshTokenHash, "refreshTokenHash", 64);
115
+ if (!CONNECT_CLIENT_ID_PATTERN.test(clientId)) {
116
+ throw new WorkosConnectProtocolError("invalid_metadata", "credential clientId is invalid");
117
+ }
118
+ if (!/^[a-f0-9]{64}$/u.test(accessTokenHash)) {
119
+ throw new WorkosConnectProtocolError(
120
+ "invalid_metadata",
121
+ "credential accessTokenHash is invalid"
122
+ );
123
+ }
124
+ if (!/^[a-f0-9]{64}$/u.test(refreshTokenHash)) {
125
+ throw new WorkosConnectProtocolError(
126
+ "invalid_metadata",
127
+ "credential refreshTokenHash is invalid"
128
+ );
129
+ }
130
+ return {
131
+ version: 1,
132
+ family: "workos_connect",
133
+ issuer,
134
+ clientId,
135
+ accessTokenHash,
136
+ refreshTokenHash
137
+ };
138
+ }
139
+
140
+ // src/lib/credentials.ts
141
+ var CONFIG_DIRECTORY = primConfigDirectory();
142
+ var TOKEN_FILE_PATH = join2(CONFIG_DIRECTORY, "token");
143
+ var REFRESH_TOKEN_PATH = join2(CONFIG_DIRECTORY, "refresh_token");
144
+ var TOKEN_EXPIRES_PATH = join2(CONFIG_DIRECTORY, "token_expires_at");
145
+ var TERMINAL_REFRESH_PATH = join2(CONFIG_DIRECTORY, "refresh_terminal");
146
+ var CREDENTIAL_METADATA_PATH = join2(CONFIG_DIRECTORY, "credential_metadata.json");
147
+ var CREDENTIAL_FAMILY_PATH = join2(CONFIG_DIRECTORY, "credential_family.json");
148
+ var CREDENTIAL_MIGRATION_PATH = join2(CONFIG_DIRECTORY, "credential_migration.json");
149
+ var CREDENTIAL_LOCK_PATH = join2(CONFIG_DIRECTORY, "credentials.lock");
150
+ var MAX_CREDENTIAL_METADATA_BYTES = 4096;
151
+ var CREDENTIAL_MIGRATION_VERSION = 1;
152
+ var CREDENTIAL_MIGRATION_STATE = "family_bound";
153
+ function readTrimmed(path) {
154
+ try {
155
+ const value = readFileSync(path, "utf8").trim();
156
+ return value || void 0;
157
+ } catch {
158
+ return void 0;
159
+ }
160
+ }
161
+ function resolveAuthCredential(options = {}) {
162
+ const envToken = (options.env ?? process.env).PRIM_TOKEN?.trim();
163
+ if (envToken) return { token: envToken, source: "environment" };
164
+ const stored = readTrimmed(options.tokenFilePath ?? TOKEN_FILE_PATH);
165
+ if (!stored) return void 0;
166
+ const metadata = readStoredCredentialMetadata(
167
+ options.metadataPath ?? CREDENTIAL_METADATA_PATH,
168
+ options.familyPath ?? CREDENTIAL_FAMILY_PATH,
169
+ options.migrationPath ?? CREDENTIAL_MIGRATION_PATH
170
+ );
171
+ if (metadata.state === "legacy_broker") {
172
+ if (metadata.metadata) {
173
+ const refreshToken2 = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
174
+ if (!refreshToken2 || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken2)) {
175
+ return void 0;
176
+ }
177
+ }
178
+ return { token: stored, source: "token_file" };
179
+ }
180
+ if (metadata.state === "invalid") return void 0;
181
+ const refreshToken = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
182
+ if (!refreshToken || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken)) {
183
+ return void 0;
184
+ }
185
+ return { token: stored, source: "token_file" };
186
+ }
187
+ function credentialFingerprint(value) {
188
+ return createHash("sha256").update(value).digest("hex");
189
+ }
190
+ function parseLegacyBrokerCredentialMetadata(value) {
191
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
192
+ throw new Error("credential family is invalid");
193
+ }
194
+ const record = value;
195
+ 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)) {
196
+ throw new Error("credential family is invalid");
197
+ }
198
+ return {
199
+ version: 1,
200
+ family: "legacy_broker",
201
+ accessTokenHash: record.accessTokenHash,
202
+ refreshTokenHash: record.refreshTokenHash
203
+ };
204
+ }
205
+ function readBoundedCredentialJson(path) {
206
+ let raw;
207
+ try {
208
+ raw = readFileSync(path, "utf8");
209
+ } catch (error) {
210
+ return error.code === "ENOENT" ? void 0 : null;
211
+ }
212
+ if (Buffer.byteLength(raw, "utf8") > MAX_CREDENTIAL_METADATA_BYTES) return null;
213
+ try {
214
+ return JSON.parse(raw);
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ function readCredentialFamily(path) {
220
+ const parsed = readBoundedCredentialJson(path);
221
+ if (parsed === void 0) return void 0;
222
+ if (parsed === null) return { state: "invalid" };
223
+ try {
224
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.family === "legacy_broker") {
225
+ return { state: "legacy_broker", metadata: parseLegacyBrokerCredentialMetadata(parsed) };
226
+ }
227
+ return {
228
+ state: "workos_connect",
229
+ metadata: parseWorkosConnectCredentialMetadata(parsed)
230
+ };
231
+ } catch {
232
+ return { state: "invalid" };
233
+ }
234
+ }
235
+ function isCredentialMigrationSentinel(value) {
236
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
237
+ const record = value;
238
+ const keys = Object.keys(record).sort();
239
+ return keys.length === 2 && keys[0] === "state" && keys[1] === "version" && record.version === CREDENTIAL_MIGRATION_VERSION && record.state === CREDENTIAL_MIGRATION_STATE;
240
+ }
241
+ function readCredentialMigrationState(path = CREDENTIAL_MIGRATION_PATH) {
242
+ const parsed = readBoundedCredentialJson(path);
243
+ if (parsed === void 0) return "absent";
244
+ return parsed !== null && isCredentialMigrationSentinel(parsed) ? "valid" : "invalid";
245
+ }
246
+ function readStoredCredentialMetadata(metadataPath = CREDENTIAL_METADATA_PATH, familyPath = CREDENTIAL_FAMILY_PATH, migrationPath = CREDENTIAL_MIGRATION_PATH) {
247
+ const family = readCredentialFamily(familyPath);
248
+ if (family) return family;
249
+ const parsed = readBoundedCredentialJson(metadataPath);
250
+ if (parsed === void 0) {
251
+ return readCredentialMigrationState(migrationPath) === "absent" ? { state: "legacy_broker" } : { state: "invalid" };
252
+ }
253
+ if (parsed === null) return { state: "invalid" };
254
+ try {
255
+ return {
256
+ state: "workos_connect",
257
+ metadata: parseWorkosConnectCredentialMetadata(parsed)
258
+ };
259
+ } catch {
260
+ return { state: "invalid" };
261
+ }
262
+ }
263
+ function decodeJwtPayload(token) {
264
+ const parts = token.split(".");
265
+ if (parts.length !== 3) return void 0;
266
+ try {
267
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
268
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return void 0;
269
+ return payload;
270
+ } catch {
271
+ return void 0;
272
+ }
273
+ }
274
+
275
+ // src/lib/file-lock.ts
276
+ import { randomBytes } from "crypto";
277
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
278
+ import { dirname as dirname2, join as join3 } from "path";
279
+ var syncSleeper = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
280
+
281
+ // src/lib/terminal-safe.ts
282
+ function replaceIsolatedSurrogates(value) {
283
+ let out = "";
284
+ for (let index = 0; index < value.length; index += 1) {
285
+ const unit = value.charCodeAt(index);
286
+ if (unit >= 55296 && unit <= 56319) {
287
+ const next = value.charCodeAt(index + 1);
288
+ if (next >= 56320 && next <= 57343) {
289
+ out += value[index] + value[index + 1];
290
+ index += 1;
291
+ } else {
292
+ out += "\uFFFD";
293
+ }
294
+ } else if (unit >= 56320 && unit <= 57343) {
295
+ out += "\uFFFD";
296
+ } else {
297
+ out += value[index];
298
+ }
299
+ }
300
+ return out;
301
+ }
302
+ function isUnsafeTerminalCodePoint(point) {
303
+ const c0OrC1 = point <= 31 || point >= 127 && point <= 159;
304
+ const bidiOrZeroWidth = point === 173 || // soft hyphen
305
+ point === 1564 || // Arabic letter mark
306
+ point === 6158 || // Mongolian vowel separator
307
+ point >= 8203 && point <= 8207 || // zero-width + directional marks
308
+ point >= 8234 && point <= 8238 || // bidi embeddings/overrides
309
+ point >= 8288 && point <= 8303 || // word joiner + bidi isolates/controls
310
+ point >= 65529 && point <= 65531 || // interlinear annotation controls
311
+ point === 65279 || // zero-width no-break space / BOM
312
+ point >= 917504 && point <= 917631;
313
+ return c0OrC1 || bidiOrZeroWidth;
314
+ }
315
+ function terminalSafeText(value) {
316
+ return Array.from(replaceIsolatedSurrogates(value)).filter((character) => !isUnsafeTerminalCodePoint(character.codePointAt(0) ?? 0)).join("");
317
+ }
318
+ var stripControlChars = terminalSafeText;
319
+
320
+ // src/client.ts
321
+ var DEFAULT_API_URL = "https://api.getprimitive.ai";
322
+ function getSiteUrl() {
323
+ return getSiteUrlForEnvironment(process.env.PRIM_API_URL);
324
+ }
325
+ function getSiteUrlForEnvironment(primApiUrl) {
326
+ return primApiUrl?.trim() || DEFAULT_API_URL;
327
+ }
328
+ var CLI_MANAGEMENT_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
329
+
330
+ // src/daemon/client.ts
331
+ import { createConnection } from "net";
332
+ import { join as join5 } from "path";
333
+
334
+ // src/daemon/principal.ts
335
+ import { createHash as createHash3 } from "crypto";
336
+ var PRINCIPAL_COMPONENT_MAX_LENGTH = 512;
337
+ function boundedClaim(value) {
338
+ return typeof value === "string" && value.length > 0 && value.length <= PRINCIPAL_COMPONENT_MAX_LENGTH ? value : void 0;
339
+ }
340
+ function sha256(value) {
341
+ return createHash3("sha256").update(value).digest("hex");
342
+ }
343
+ function resolveDaemonPrincipal(token = resolveAuthCredential()?.token) {
344
+ if (!token) return void 0;
345
+ const payload = decodeJwtPayload(token);
346
+ const subject = boundedClaim(payload?.sub);
347
+ const organizationId = boundedClaim(payload?.org_id);
348
+ if (!(subject && organizationId)) return void 0;
349
+ const issuer = boundedClaim(payload?.iss) ?? "unknown-issuer";
350
+ return {
351
+ principalId: sha256(`${issuer}\0${subject}`),
352
+ organizationId,
353
+ credentialFingerprint: sha256(token)
354
+ };
355
+ }
356
+
357
+ // src/daemon/protocol.ts
358
+ var DAEMON_JSON_REQUEST_MAX_BYTES = 64 * 1024;
359
+ var DAEMON_JSON_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
360
+ function isRecord(value) {
361
+ return typeof value === "object" && value !== null && !Array.isArray(value);
362
+ }
363
+ function parseDaemonResponseEnvelope(value) {
364
+ if (!isRecord(value)) return void 0;
365
+ if (!(typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0)) {
366
+ return void 0;
367
+ }
368
+ if (typeof value.ok !== "boolean") return void 0;
369
+ if (value.error !== void 0 && typeof value.error !== "string") return void 0;
370
+ return {
371
+ id: value.id,
372
+ ok: value.ok,
373
+ ...value.result === void 0 ? {} : { result: value.result },
374
+ ...value.error === void 0 ? {} : { error: value.error }
375
+ };
376
+ }
377
+
378
+ // src/daemon/client.ts
379
+ var SOCK_PATH = join5(primConfigDirectory(), "sock");
380
+ var DEFAULT_TIMEOUT_MS = 250;
381
+ var nextRequestId = 1;
382
+ function daemonRequest(method, params = {}, opts = {}) {
383
+ const id = nextRequestId++;
384
+ const caller = resolveDaemonPrincipal();
385
+ const request = {
386
+ id,
387
+ method,
388
+ params,
389
+ ...caller ? { caller } : {}
390
+ };
391
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
392
+ return new Promise((resolve4) => {
393
+ let settled = false;
394
+ let socket;
395
+ let buffer = Buffer.alloc(0);
396
+ const settle = (value) => {
397
+ if (settled) {
398
+ return;
399
+ }
400
+ settled = true;
401
+ if (socket) {
402
+ try {
403
+ socket.destroy();
404
+ } catch {
405
+ }
406
+ }
407
+ resolve4(value);
408
+ };
409
+ const timer = setTimeout(() => settle(null), timeoutMs);
410
+ timer.unref();
411
+ try {
412
+ socket = createConnection(SOCK_PATH);
413
+ } catch {
414
+ clearTimeout(timer);
415
+ settle(null);
416
+ return;
417
+ }
418
+ socket.on("error", () => {
419
+ clearTimeout(timer);
420
+ settle(null);
421
+ });
422
+ socket.on("connect", () => {
423
+ try {
424
+ socket?.write(`${JSON.stringify(request)}
425
+ `);
426
+ } catch {
427
+ clearTimeout(timer);
428
+ settle(null);
429
+ }
430
+ });
431
+ socket.on("data", (chunk) => {
432
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
433
+ if (buffer.length > DAEMON_JSON_RESPONSE_MAX_BYTES) {
434
+ clearTimeout(timer);
435
+ settle(null);
436
+ return;
437
+ }
438
+ const newlineIdx = buffer.indexOf(10);
439
+ if (newlineIdx === -1) {
440
+ return;
441
+ }
442
+ const line = buffer.subarray(0, newlineIdx).toString("utf8");
443
+ try {
444
+ const res = parseDaemonResponseEnvelope(JSON.parse(line));
445
+ if (!res || res.id !== id) {
446
+ clearTimeout(timer);
447
+ settle(null);
448
+ return;
449
+ }
450
+ clearTimeout(timer);
451
+ settle(res.ok && res.result !== void 0 ? res.result : null);
452
+ } catch {
453
+ clearTimeout(timer);
454
+ settle(null);
455
+ }
456
+ });
457
+ socket.on("end", () => {
458
+ if (!settled) {
459
+ clearTimeout(timer);
460
+ settle(null);
461
+ }
462
+ });
463
+ });
464
+ }
465
+
466
+ // src/lib/activation.ts
467
+ import { execFileSync as execFileSync2 } from "child_process";
468
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
469
+ import { homedir as homedir2 } from "os";
470
+ import { join as join6 } from "path";
471
+
472
+ // src/lib/git.ts
473
+ import { execFileSync } from "child_process";
474
+ import { createHash as createHash4 } from "crypto";
475
+ import { lstatSync, realpathSync } from "fs";
476
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "path";
477
+ var GIT_TIMEOUT_MS = 1e3;
478
+ var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
479
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
480
+ function gitToplevel(cwd) {
481
+ try {
482
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
483
+ cwd,
484
+ encoding: "utf-8",
485
+ stdio: ["ignore", "pipe", "ignore"],
486
+ timeout: GIT_TIMEOUT_MS
487
+ }).trim();
488
+ } catch {
489
+ return null;
490
+ }
491
+ }
492
+ function gitValue(cwd, args) {
493
+ try {
494
+ const value = execFileSync("git", args, {
495
+ cwd,
496
+ encoding: "utf-8",
497
+ stdio: ["ignore", "pipe", "ignore"],
498
+ timeout: GIT_TIMEOUT_MS
499
+ }).trim();
500
+ return value || void 0;
501
+ } catch {
502
+ return void 0;
503
+ }
504
+ }
505
+ function githubRepositoryFullName(cwd) {
506
+ const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
507
+ if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
508
+ const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
509
+ remote
510
+ );
511
+ const path = match?.[1] ?? match?.[2];
512
+ if (!path) return null;
513
+ const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
514
+ if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
515
+ return null;
516
+ }
517
+ return `${parts[0]}/${parts[1]}`;
518
+ }
519
+
520
+ // src/lib/activation.ts
521
+ var PRIM_ACTIVE_KEY = "prim.active";
522
+ var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
523
+ var PRIM_REPO_SYNC_REPOSITORY_KEY = "prim.repoSyncRepository";
524
+ var PRIM_REPO_BINDING_STATE_KEY = "prim.repoBindingState";
525
+ var GIT_TIMEOUT_MS2 = 1e3;
526
+ var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
527
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json", ".cursor/hooks.json"];
528
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
529
+ function repoActiveFlag(cwd) {
530
+ try {
531
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
532
+ cwd,
533
+ encoding: "utf-8",
534
+ stdio: ["ignore", "pipe", "ignore"],
535
+ timeout: GIT_TIMEOUT_MS2
536
+ }).trim();
537
+ if (value === "true") return "true";
538
+ if (value === "false") return "false";
539
+ return void 0;
540
+ } catch {
541
+ return void 0;
542
+ }
543
+ }
544
+ function isValidRepoSyncId(value) {
545
+ return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
546
+ }
547
+ function localGitConfigValue(cwd, key) {
548
+ try {
549
+ const value = execFileSync2("git", ["config", "--local", "--get", key], {
550
+ cwd,
551
+ encoding: "utf-8",
552
+ stdio: ["ignore", "pipe", "ignore"],
553
+ timeout: GIT_TIMEOUT_MS2
554
+ }).trim();
555
+ return value || void 0;
556
+ } catch {
557
+ return void 0;
558
+ }
559
+ }
560
+ function repoSyncId(cwd) {
561
+ const value = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
562
+ if (!isValidRepoSyncId(value)) return void 0;
563
+ const repositoryFullName = localGitConfigValue(cwd, PRIM_REPO_SYNC_REPOSITORY_KEY);
564
+ return repositoryFullName === githubRepositoryFullName(cwd) ? value : void 0;
565
+ }
566
+ function repositoryBindingState(cwd) {
567
+ const rawId = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
568
+ const marker = localGitConfigValue(cwd, PRIM_REPO_BINDING_STATE_KEY);
569
+ if (rawId !== void 0 && !isValidRepoSyncId(rawId)) return "invalid";
570
+ if (marker === "unbound" || marker === "invalid") return marker;
571
+ if (marker === "connected") return repoSyncId(cwd) ? "connected" : "invalid";
572
+ if (marker !== void 0) return "invalid";
573
+ return rawId === void 0 ? "unbound" : repoSyncId(cwd) ? void 0 : "unbound";
574
+ }
575
+ function hasProjectPrimInstall(cwd) {
576
+ const root = gitToplevel(cwd);
577
+ if (root === null || root === homedir2()) return false;
578
+ for (const rel of PROJECT_INSTALL_FILES) {
579
+ const path = join6(root, rel);
580
+ if (existsSync3(path)) {
581
+ try {
582
+ const content = readFileSync4(path, "utf-8");
583
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
584
+ } catch {
585
+ }
586
+ }
587
+ }
588
+ return false;
589
+ }
590
+ function isRepoActiveForCapture(cwd) {
591
+ const flag = repoActiveFlag(cwd);
592
+ if (flag === "true") return true;
593
+ if (flag === "false") return false;
594
+ return hasProjectPrimInstall(cwd);
595
+ }
596
+ function decisionIngestionStatus(cwd) {
597
+ return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
598
+ }
599
+
600
+ // src/lib/presence.ts
601
+ var DECISION_ORIGIN = "https://app.getprimitive.ai";
602
+ function validDecisionUrl(value) {
603
+ if (typeof value !== "string" || stripControlChars(value) !== value) {
604
+ return void 0;
605
+ }
606
+ try {
607
+ const url = new URL(value);
608
+ if (url.origin !== DECISION_ORIGIN || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || !/^\/decisions\/[^/]+$/u.test(url.pathname)) {
609
+ return void 0;
610
+ }
611
+ return url.href;
612
+ } catch {
613
+ return void 0;
614
+ }
615
+ }
616
+ function decisionLink(label, decisionUrl, plain) {
617
+ const url = validDecisionUrl(decisionUrl);
618
+ if (plain || !url) {
619
+ return label;
620
+ }
621
+ return `\x1B]8;;${url}\x07\x1B[34;4m${label}\x1B[0m\x1B]8;;\x07`;
622
+ }
623
+ function formatLabeled(labels, cap) {
624
+ if (labels === void 0) {
625
+ return "\u2014";
626
+ }
627
+ if (labels.length === 0) {
628
+ return "just you";
629
+ }
630
+ if (labels.length <= cap) {
631
+ return labels.join(", ");
632
+ }
633
+ return `${labels.slice(0, cap).join(", ")} +${String(labels.length - cap)}`;
634
+ }
635
+ function formatTeammates(names, cap) {
636
+ return formatLabeled(names?.map(stripControlChars), cap);
637
+ }
638
+ function formatTeammatesWithArea(teammates, cap, plainLinks = false) {
639
+ return formatLabeled(
640
+ teammates?.map((t) => {
641
+ const name = stripControlChars(t.name);
642
+ const area = stripControlChars(t.area ?? "").trim();
643
+ const label = area ? `${name} - ${area}` : name;
644
+ return decisionLink(label, t.decisionUrl, plainLinks);
645
+ }),
646
+ cap
647
+ );
648
+ }
649
+
650
+ // src/lib/statusline-render.ts
651
+ function repositoryBindingDiagnosticLabel(state) {
652
+ if (state === "unbound") return "GitHub repo connection: required (run `prim github connect`)";
653
+ if (state === "invalid") return "GitHub repo connection: invalid (run `prim doctor`)";
654
+ return void 0;
655
+ }
656
+ var STATUSLINE_NAME_CAP = 3;
657
+ function formatStatusline(version, snapshot, resolveIngestionStatus, options = {}) {
658
+ const ingestionSuffix = (status) => status === void 0 ? "" : ` \xB7 Decision ingestion ${status}`;
659
+ const ingestionStatus = snapshot === null || snapshot.healthy === false ? options.includeIngestionWhenUnavailable ? resolveIngestionStatus() : void 0 : resolveIngestionStatus();
660
+ if (snapshot?.healthy === false && snapshot.needsReauth) {
661
+ return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`${ingestionSuffix(ingestionStatus)})`;
662
+ }
663
+ const repositoryLabel = repositoryBindingDiagnosticLabel(
664
+ snapshot !== null && snapshot.healthy !== false || options.includeIngestionWhenUnavailable === true ? options.resolveRepositoryBindingState?.() : void 0
665
+ );
666
+ const repositorySuffix = repositoryLabel ? ` \xB7 ${repositoryLabel}` : "";
667
+ if (!snapshot) {
668
+ return `primitive ${version} (daemon: down${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
669
+ }
670
+ if (snapshot.healthy === false) {
671
+ if (snapshot.ingestion?.healthy === false) {
672
+ const pending = snapshot.ingestion.pendingCount;
673
+ const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
674
+ return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
675
+ }
676
+ if (snapshot.heartbeat?.healthy === false) {
677
+ return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
678
+ }
679
+ return `primitive ${version} (daemon: starting${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
680
+ }
681
+ if (snapshot.envMismatch) {
682
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: other env)`;
683
+ }
684
+ if (snapshot.principalMismatch) {
685
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: other account)`;
686
+ }
687
+ if (snapshot.presenceStale) {
688
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: stale)`;
689
+ }
690
+ let team;
691
+ if (snapshot.onlineTeammates !== void 0) {
692
+ team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP, options.plainLinks === true)}`;
693
+ } else if (snapshot.onlineNames !== void 0) {
694
+ team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
695
+ } else if (typeof snapshot.onlineCount === "number") {
696
+ team = `team: ${String(snapshot.onlineCount)} online`;
697
+ } else {
698
+ team = "team: \u2014";
699
+ }
700
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 ${team})`;
701
+ }
702
+
703
+ // src/commands/statusline.ts
704
+ var STATUSLINE_TIMEOUT_MS = 200;
705
+ function readPackageVersion() {
706
+ try {
707
+ const here = dirname5(fileURLToPath(import.meta.url));
708
+ const candidates = [
709
+ // Schema-v2 runtimes staged a manifest beside this standalone bundle.
710
+ // Keep resolving it while those legacy launchers transition to schema v3.
711
+ resolve3(here, "manifest.json"),
712
+ resolve3(here, "../../package.json"),
713
+ resolve3(here, "../package.json")
714
+ ];
715
+ for (const path of candidates) {
716
+ try {
717
+ const pkg = JSON.parse(readFileSync5(path, "utf-8"));
718
+ if (pkg.version) {
719
+ return pkg.version;
720
+ }
721
+ } catch {
722
+ }
723
+ }
724
+ } catch {
725
+ }
726
+ return "0.0.0";
727
+ }
728
+ function debug(msg) {
729
+ if (process.env.PRIM_STATUSLINE_DEBUG === "1") {
730
+ process.stderr.write(`[prim-statusline] ${msg}
731
+ `);
732
+ }
733
+ }
734
+ async function renderStatusline(cwd = process.cwd()) {
735
+ const version = readPackageVersion();
736
+ let ingestion;
737
+ const resolveIngestion = () => {
738
+ ingestion ??= decisionIngestionStatus(cwd);
739
+ return ingestion;
740
+ };
741
+ const snapshot = await daemonRequest(
742
+ "status_snapshot",
743
+ // callerEnv lets the daemon withhold presence when it is bound to a different
744
+ // deployment than this statusline targets.
745
+ { callerEnv: getSiteUrl() },
746
+ { timeoutMs: STATUSLINE_TIMEOUT_MS }
747
+ );
748
+ if (!snapshot) {
749
+ debug("daemon snapshot missing");
750
+ }
751
+ return formatStatusline(version, snapshot, resolveIngestion, {
752
+ resolveRepositoryBindingState: () => resolveIngestion() === "enabled" ? repositoryBindingState(cwd) : void 0
753
+ });
754
+ }
755
+
756
+ // src/hooks/hook-stdin.ts
757
+ var DEFAULT_MAX_BYTES = 1024 * 1024;
758
+ function readHookStdin(timeoutMs = 1e3, maxBytes = DEFAULT_MAX_BYTES) {
759
+ return new Promise((resolve4, reject) => {
760
+ const chunks = [];
761
+ let bytes = 0;
762
+ let settled = false;
763
+ const finish = (error) => {
764
+ if (settled) return;
765
+ settled = true;
766
+ clearTimeout(timer);
767
+ process.stdin.removeAllListeners("data");
768
+ process.stdin.removeAllListeners("end");
769
+ process.stdin.removeAllListeners("error");
770
+ if (error) reject(error);
771
+ else resolve4(Buffer.concat(chunks, bytes).toString("utf-8"));
772
+ };
773
+ const timer = setTimeout(() => finish(new Error("stdin read timeout")), timeoutMs);
774
+ process.stdin.on("data", (chunk) => {
775
+ bytes += chunk.length;
776
+ if (bytes > maxBytes) {
777
+ finish(new Error("stdin envelope exceeds size limit"));
778
+ return;
779
+ }
780
+ chunks.push(chunk);
781
+ });
782
+ process.stdin.on("end", () => finish());
783
+ process.stdin.on("error", (error) => finish(error));
784
+ });
785
+ }
786
+
787
+ // src/statusline-main.ts
788
+ async function inputCwd() {
789
+ try {
790
+ const raw = await readHookStdin(200, 64 * 1024);
791
+ const parsed = JSON.parse(raw);
792
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
793
+ const input = parsed;
794
+ if (typeof input.cwd === "string" && input.cwd.length > 0) return input.cwd;
795
+ if (input.workspace && typeof input.workspace === "object" && !Array.isArray(input.workspace)) {
796
+ const current = input.workspace.current_dir;
797
+ if (typeof current === "string" && current.length > 0) return current;
798
+ }
799
+ } catch {
800
+ }
801
+ }
802
+ async function main() {
803
+ try {
804
+ process.stdout.write(await renderStatusline(await inputCwd() ?? process.cwd()));
805
+ } catch {
806
+ process.stdout.write(`primitive ${readPackageVersion()} (daemon: unavailable)`);
807
+ }
808
+ }
809
+ void main();