@primitive.ai/prim 0.1.0-alpha.67 → 0.1.0-alpha.69

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 +39 -1
  2. package/SKILL.md +1 -1
  3. package/dist/{chunk-XPVNH3J6.js → chunk-2KW7DVFJ.js} +121 -130
  4. package/dist/chunk-3EEFL6NJ.js +529 -0
  5. package/dist/chunk-4A2XB2PC.js +310 -0
  6. package/dist/{chunk-G5D3FF3D.js → chunk-BQUJ4AFJ.js} +1 -83
  7. package/dist/chunk-CUE2LKLR.js +155 -0
  8. package/dist/chunk-FI3LIRCB.js +262 -0
  9. package/dist/{chunk-CPSE3Y3T.js → chunk-H64F6QHV.js} +27 -8
  10. package/dist/chunk-KKP2NYZ3.js +505 -0
  11. package/dist/{chunk-JQFN6LBO.js → chunk-LEMX3IOG.js} +6 -37
  12. package/dist/{chunk-U6L2RU67.js → chunk-LMRYZL3D.js} +52 -45
  13. package/dist/{chunk-XUAP572Z.js → chunk-RO3F6JYV.js} +8 -7
  14. package/dist/{chunk-TDRXHBEX.js → chunk-TJTVHDJ5.js} +41 -18
  15. package/dist/{chunk-TCDUH7AN.js → chunk-WB7AUZ4C.js} +17 -0
  16. package/dist/chunk-XGADQPCI.js +26 -0
  17. package/dist/{chunk-UN5O5SYG.js → chunk-YWO7O6QG.js} +14 -53
  18. package/dist/chunk-Z7YPFWAZ.js +917 -0
  19. package/dist/{chunk-BDGQEJA5.js → chunk-ZOSNEO5N.js} +2 -2
  20. package/dist/daemon/server.js +11804 -470
  21. package/dist/hook-runtime/daemon/server.js +13353 -0
  22. package/dist/hook-runtime/hooks/post-commit.js +1128 -0
  23. package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
  24. package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
  25. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  26. package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
  27. package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
  28. package/dist/hook-runtime/hooks/session-end.js +495 -0
  29. package/dist/hook-runtime/hooks/session-start.js +11284 -0
  30. package/dist/hook-runtime/index.js +42775 -0
  31. package/dist/hook-runtime/statusline-main.js +765 -0
  32. package/dist/hooks/post-commit.js +7 -4
  33. package/dist/hooks/post-rewrite.js +7 -4
  34. package/dist/hooks/post-tool-use.js +64 -32
  35. package/dist/hooks/pre-commit.js +4 -3
  36. package/dist/hooks/pre-tool-use.js +118 -38
  37. package/dist/hooks/prim-hook.js +72 -27
  38. package/dist/hooks/session-end.js +5 -4
  39. package/dist/hooks/session-start.js +36 -25
  40. package/dist/index.js +15039 -2555
  41. package/dist/statusline-main.js +488 -96
  42. package/package.json +11 -5
  43. package/dist/chunk-CZ24W5QU.js +0 -123
  44. package/dist/chunk-RTLFE6EZ.js +0 -518
  45. package/dist/chunk-UTKQTZHL.js +0 -88
@@ -3,73 +3,396 @@
3
3
  // src/commands/statusline.ts
4
4
  import { readFileSync as readFileSync5 } from "fs";
5
5
  import { dirname as dirname5, resolve as resolve3 } from "path";
6
- import { fileURLToPath as fileURLToPath2 } from "url";
6
+ import { fileURLToPath } from "url";
7
7
 
8
8
  // src/client.ts
9
- import { createHash, randomBytes as randomBytes2 } from "crypto";
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";
10
15
  import {
11
- chmodSync,
12
- existsSync as existsSync2,
13
- mkdirSync as mkdirSync2,
14
- readFileSync as readFileSync2,
16
+ constants,
17
+ closeSync,
18
+ fchmodSync,
19
+ fsyncSync,
20
+ mkdirSync,
21
+ openSync,
15
22
  renameSync,
16
- rmSync as rmSync2,
17
- writeFileSync as writeFileSync2
23
+ rmSync,
24
+ writeFileSync
18
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
19
34
  import { homedir } from "os";
20
- import { dirname as dirname2, join as join2, resolve } from "path";
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
+ }
21
274
 
22
275
  // src/lib/file-lock.ts
23
276
  import { randomBytes } from "crypto";
24
- import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
25
- import { dirname, join } from "path";
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));
26
280
 
27
- // src/client.ts
28
- var DEFAULT_API_URL = "https://api.getprimitive.ai";
29
- function loadEnvFile(cwd = process.cwd()) {
30
- const envVars = {};
31
- for (const file of [".env.local", ".env"]) {
32
- const filePath = resolve(cwd, file);
33
- if (!existsSync2(filePath)) continue;
34
- const content = readFileSync2(filePath, "utf-8");
35
- for (const line of content.split("\n")) {
36
- const trimmed = line.trim();
37
- if (!trimmed || trimmed.startsWith("#")) continue;
38
- const eqIdx = trimmed.indexOf("=");
39
- if (eqIdx === -1) continue;
40
- envVars[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
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];
41
298
  }
42
299
  }
43
- return envVars;
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("");
44
317
  }
45
- var TOKEN_FILE_PATH = join2(homedir(), ".config", "prim", "token");
46
- var REFRESH_TOKEN_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_token");
47
- var TOKEN_EXPIRES_PATH = join2(dirname2(TOKEN_FILE_PATH), "token_expires_at");
48
- var TERMINAL_REFRESH_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_terminal");
49
- var CREDENTIAL_LOCK_PATH = join2(dirname2(TOKEN_FILE_PATH), "credentials.lock");
318
+ var stripControlChars = terminalSafeText;
319
+
320
+ // src/client.ts
321
+ var DEFAULT_API_URL = "https://api.getprimitive.ai";
50
322
  function getSiteUrl() {
51
- return getSiteUrlForCwd(process.cwd(), process.env.PRIM_API_URL);
323
+ return getSiteUrlForEnvironment(process.env.PRIM_API_URL);
52
324
  }
53
- function getSiteUrlForCwd(cwd, primApiUrl) {
54
- if (primApiUrl) return primApiUrl;
55
- return loadEnvFile(cwd).PRIM_API_URL ?? DEFAULT_API_URL;
325
+ function getSiteUrlForEnvironment(primApiUrl) {
326
+ return primApiUrl?.trim() || DEFAULT_API_URL;
56
327
  }
328
+ var CLI_MANAGEMENT_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
57
329
 
58
330
  // src/daemon/client.ts
59
331
  import { createConnection } from "net";
60
- import { homedir as homedir2 } from "os";
61
- import { join as join3 } from "path";
62
- var SOCK_PATH = join3(homedir2(), ".config", "prim", "sock");
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");
63
380
  var DEFAULT_TIMEOUT_MS = 250;
64
381
  var nextRequestId = 1;
65
382
  function daemonRequest(method, params = {}, opts = {}) {
66
383
  const id = nextRequestId++;
67
- const request = { id, method, params };
384
+ const caller = resolveDaemonPrincipal();
385
+ const request = {
386
+ id,
387
+ method,
388
+ params,
389
+ ...caller ? { caller } : {}
390
+ };
68
391
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
69
392
  return new Promise((resolve4) => {
70
393
  let settled = false;
71
394
  let socket;
72
- let buffer = "";
395
+ let buffer = Buffer.alloc(0);
73
396
  const settle = (value) => {
74
397
  if (settled) {
75
398
  return;
@@ -106,15 +429,20 @@ function daemonRequest(method, params = {}, opts = {}) {
106
429
  }
107
430
  });
108
431
  socket.on("data", (chunk) => {
109
- buffer += chunk.toString("utf-8");
110
- const newlineIdx = buffer.indexOf("\n");
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);
111
439
  if (newlineIdx === -1) {
112
440
  return;
113
441
  }
114
- const line = buffer.slice(0, newlineIdx);
442
+ const line = buffer.subarray(0, newlineIdx).toString("utf8");
115
443
  try {
116
- const res = JSON.parse(line);
117
- if (res.id !== id) {
444
+ const res = parseDaemonResponseEnvelope(JSON.parse(line));
445
+ if (!res || res.id !== id) {
118
446
  clearTimeout(timer);
119
447
  settle(null);
120
448
  return;
@@ -137,16 +465,18 @@ function daemonRequest(method, params = {}, opts = {}) {
137
465
 
138
466
  // src/lib/activation.ts
139
467
  import { execFileSync as execFileSync2 } from "child_process";
140
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
141
- import { homedir as homedir3 } from "os";
142
- import { join as join4 } from "path";
468
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
469
+ import { homedir as homedir2 } from "os";
470
+ import { join as join6 } from "path";
143
471
 
144
472
  // src/lib/git.ts
145
473
  import { execFileSync } from "child_process";
146
- import { createHash as createHash2 } from "crypto";
474
+ import { createHash as createHash4 } from "crypto";
147
475
  import { lstatSync, realpathSync } from "fs";
148
- import { basename, dirname as dirname3, isAbsolute, relative, resolve as resolve2, sep } from "path";
476
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "path";
149
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;
150
480
  function gitToplevel(cwd) {
151
481
  try {
152
482
  return execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -159,10 +489,41 @@ function gitToplevel(cwd) {
159
489
  return null;
160
490
  }
161
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
+ }
162
519
 
163
520
  // src/lib/activation.ts
164
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";
165
525
  var GIT_TIMEOUT_MS2 = 1e3;
526
+ var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
166
527
  var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
167
528
  var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
168
529
  function repoActiveFlag(cwd) {
@@ -180,14 +541,45 @@ function repoActiveFlag(cwd) {
180
541
  return void 0;
181
542
  }
182
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
+ }
183
575
  function hasProjectPrimInstall(cwd) {
184
576
  const root = gitToplevel(cwd);
185
- if (root === null || root === homedir3()) return false;
577
+ if (root === null || root === homedir2()) return false;
186
578
  for (const rel of PROJECT_INSTALL_FILES) {
187
- const path = join4(root, rel);
579
+ const path = join6(root, rel);
188
580
  if (existsSync3(path)) {
189
581
  try {
190
- const content = readFileSync3(path, "utf-8");
582
+ const content = readFileSync4(path, "utf-8");
191
583
  if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
192
584
  } catch {
193
585
  }
@@ -205,30 +597,6 @@ function decisionIngestionStatus(cwd) {
205
597
  return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
206
598
  }
207
599
 
208
- // src/lib/bin-cache.ts
209
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
210
- import { homedir as homedir4 } from "os";
211
- import { join as join6 } from "path";
212
-
213
- // src/lib/bin-path.ts
214
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
215
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5 } from "path";
216
- import { fileURLToPath } from "url";
217
- var PKG_NAME = "@primitive.ai/prim";
218
- var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
219
-
220
- // src/lib/ansi.ts
221
- function stripControlChars(text) {
222
- let out = "";
223
- for (const ch of text) {
224
- const code = ch.codePointAt(0) ?? 0;
225
- if (code > 31 && !(code >= 127 && code <= 159)) {
226
- out += ch;
227
- }
228
- }
229
- return out;
230
- }
231
-
232
600
  // src/lib/presence.ts
233
601
  var DECISION_ORIGIN = "https://app.getprimitive.ai";
234
602
  function validDecisionUrl(value) {
@@ -245,9 +613,12 @@ function validDecisionUrl(value) {
245
613
  return void 0;
246
614
  }
247
615
  }
248
- function decisionLink(label, decisionUrl) {
616
+ function decisionLink(label, decisionUrl, plain) {
249
617
  const url = validDecisionUrl(decisionUrl);
250
- return url ? `\x1B]8;;${url}\x07\x1B[34;4m${label}\x1B[0m\x1B]8;;\x07` : label;
618
+ if (plain || !url) {
619
+ return label;
620
+ }
621
+ return `\x1B]8;;${url}\x07\x1B[34;4m${label}\x1B[0m\x1B]8;;\x07`;
251
622
  }
252
623
  function formatLabeled(labels, cap) {
253
624
  if (labels === void 0) {
@@ -264,48 +635,61 @@ function formatLabeled(labels, cap) {
264
635
  function formatTeammates(names, cap) {
265
636
  return formatLabeled(names?.map(stripControlChars), cap);
266
637
  }
267
- function formatTeammatesWithArea(teammates, cap) {
638
+ function formatTeammatesWithArea(teammates, cap, plainLinks = false) {
268
639
  return formatLabeled(
269
640
  teammates?.map((t) => {
270
641
  const name = stripControlChars(t.name);
271
642
  const area = stripControlChars(t.area ?? "").trim();
272
643
  const label = area ? `${name} - ${area}` : name;
273
- return decisionLink(label, t.decisionUrl);
644
+ return decisionLink(label, t.decisionUrl, plainLinks);
274
645
  }),
275
646
  cap
276
647
  );
277
648
  }
278
649
 
279
650
  // src/lib/statusline-render.ts
651
+ function repositoryBindingDiagnosticLabel(state) {
652
+ if (state === "unbound") return "repository: unbound (enforcement not evaluating)";
653
+ if (state === "invalid") return "repository binding: invalid (run `prim doctor`)";
654
+ return void 0;
655
+ }
280
656
  var STATUSLINE_NAME_CAP = 3;
281
- function formatStatusline(version, snapshot, resolveIngestionStatus) {
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}` : "";
282
667
  if (!snapshot) {
283
- return `primitive ${version} (daemon: down)`;
668
+ return `primitive ${version} (daemon: down${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
284
669
  }
285
670
  if (snapshot.healthy === false) {
286
- if (snapshot.needsReauth) {
287
- return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
288
- }
289
671
  if (snapshot.ingestion?.healthy === false) {
290
672
  const pending = snapshot.ingestion.pendingCount;
291
673
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
292
- return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
674
+ return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
293
675
  }
294
676
  if (snapshot.heartbeat?.healthy === false) {
295
- return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
677
+ return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
296
678
  }
297
- return `primitive ${version} (daemon: starting)`;
679
+ return `primitive ${version} (daemon: starting${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
298
680
  }
299
- const ingestionStatus = resolveIngestionStatus();
300
681
  if (snapshot.envMismatch) {
301
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
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)`;
302
686
  }
303
687
  if (snapshot.presenceStale) {
304
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
688
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: stale)`;
305
689
  }
306
690
  let team;
307
691
  if (snapshot.onlineTeammates !== void 0) {
308
- team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
692
+ team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP, options.plainLinks === true)}`;
309
693
  } else if (snapshot.onlineNames !== void 0) {
310
694
  team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
311
695
  } else if (typeof snapshot.onlineCount === "number") {
@@ -313,14 +697,14 @@ function formatStatusline(version, snapshot, resolveIngestionStatus) {
313
697
  } else {
314
698
  team = "team: \u2014";
315
699
  }
316
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
700
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 ${team})`;
317
701
  }
318
702
 
319
703
  // src/commands/statusline.ts
320
704
  var STATUSLINE_TIMEOUT_MS = 200;
321
705
  function readPackageVersion() {
322
706
  try {
323
- const here = dirname5(fileURLToPath2(import.meta.url));
707
+ const here = dirname5(fileURLToPath(import.meta.url));
324
708
  const candidates = [
325
709
  // Schema-v2 runtimes staged a manifest beside this standalone bundle.
326
710
  // Keep resolving it while those legacy launchers transition to schema v3.
@@ -349,6 +733,12 @@ function debug(msg) {
349
733
  }
350
734
  async function renderStatusline() {
351
735
  const version = readPackageVersion();
736
+ const cwd = process.cwd();
737
+ let ingestion;
738
+ const resolveIngestion = () => {
739
+ ingestion ??= decisionIngestionStatus(cwd);
740
+ return ingestion;
741
+ };
352
742
  const snapshot = await daemonRequest(
353
743
  "status_snapshot",
354
744
  // callerEnv lets the daemon withhold presence when it is bound to a different
@@ -359,7 +749,9 @@ async function renderStatusline() {
359
749
  if (!snapshot) {
360
750
  debug("daemon snapshot missing");
361
751
  }
362
- return formatStatusline(version, snapshot, () => decisionIngestionStatus(process.cwd()));
752
+ return formatStatusline(version, snapshot, resolveIngestion, {
753
+ resolveRepositoryBindingState: () => resolveIngestion() === "enabled" ? repositoryBindingState(cwd) : void 0
754
+ });
363
755
  }
364
756
 
365
757
  // src/statusline-main.ts