@primitive.ai/prim 0.1.0-alpha.68 → 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 (44) 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-I5BHJQH2.js → chunk-KKP2NYZ3.js} +206 -30
  11. package/dist/chunk-LEMX3IOG.js +79 -0
  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-UN5O5SYG.js → chunk-YWO7O6QG.js} +14 -53
  17. package/dist/chunk-Z7YPFWAZ.js +917 -0
  18. package/dist/{chunk-BDGQEJA5.js → chunk-ZOSNEO5N.js} +2 -2
  19. package/dist/daemon/server.js +11634 -391
  20. package/dist/hook-runtime/daemon/server.js +13353 -0
  21. package/dist/hook-runtime/hooks/post-commit.js +1128 -0
  22. package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
  23. package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
  24. package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
  25. package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
  26. package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
  27. package/dist/hook-runtime/hooks/session-end.js +495 -0
  28. package/dist/hook-runtime/hooks/session-start.js +11284 -0
  29. package/dist/hook-runtime/index.js +42775 -0
  30. package/dist/hook-runtime/statusline-main.js +765 -0
  31. package/dist/hooks/post-commit.js +7 -4
  32. package/dist/hooks/post-rewrite.js +7 -4
  33. package/dist/hooks/post-tool-use.js +21 -19
  34. package/dist/hooks/pre-commit.js +4 -3
  35. package/dist/hooks/pre-tool-use.js +59 -18
  36. package/dist/hooks/prim-hook.js +39 -56
  37. package/dist/hooks/session-end.js +3 -2
  38. package/dist/hooks/session-start.js +15 -12
  39. package/dist/index.js +15050 -2479
  40. package/dist/statusline-main.js +477 -89
  41. package/package.json +11 -5
  42. package/dist/chunk-K3AYW6VZ.js +0 -300
  43. package/dist/chunk-RTLFE6EZ.js +0 -518
  44. 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;
44
314
  }
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");
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";
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) {
@@ -280,32 +648,44 @@ function formatTeammatesWithArea(teammates, cap, plainLinks = false) {
280
648
  }
281
649
 
282
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
+ }
283
656
  var STATUSLINE_NAME_CAP = 3;
284
657
  function formatStatusline(version, snapshot, resolveIngestionStatus, options = {}) {
285
658
  const ingestionSuffix = (status) => status === void 0 ? "" : ` \xB7 Decision ingestion ${status}`;
286
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}` : "";
287
667
  if (!snapshot) {
288
- return `primitive ${version} (daemon: down${ingestionSuffix(ingestionStatus)})`;
668
+ return `primitive ${version} (daemon: down${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
289
669
  }
290
670
  if (snapshot.healthy === false) {
291
- if (snapshot.needsReauth) {
292
- return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`${ingestionSuffix(ingestionStatus)})`;
293
- }
294
671
  if (snapshot.ingestion?.healthy === false) {
295
672
  const pending = snapshot.ingestion.pendingCount;
296
673
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
297
- return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${ingestionSuffix(ingestionStatus)})`;
674
+ return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
298
675
  }
299
676
  if (snapshot.heartbeat?.healthy === false) {
300
- return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${ingestionSuffix(ingestionStatus)})`;
677
+ return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
301
678
  }
302
- return `primitive ${version} (daemon: starting${ingestionSuffix(ingestionStatus)})`;
679
+ return `primitive ${version} (daemon: starting${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
303
680
  }
304
681
  if (snapshot.envMismatch) {
305
- 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)`;
306
686
  }
307
687
  if (snapshot.presenceStale) {
308
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
688
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 presence: stale)`;
309
689
  }
310
690
  let team;
311
691
  if (snapshot.onlineTeammates !== void 0) {
@@ -317,14 +697,14 @@ function formatStatusline(version, snapshot, resolveIngestionStatus, options = {
317
697
  } else {
318
698
  team = "team: \u2014";
319
699
  }
320
- return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
700
+ return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus}${repositorySuffix} \xB7 ${team})`;
321
701
  }
322
702
 
323
703
  // src/commands/statusline.ts
324
704
  var STATUSLINE_TIMEOUT_MS = 200;
325
705
  function readPackageVersion() {
326
706
  try {
327
- const here = dirname5(fileURLToPath2(import.meta.url));
707
+ const here = dirname5(fileURLToPath(import.meta.url));
328
708
  const candidates = [
329
709
  // Schema-v2 runtimes staged a manifest beside this standalone bundle.
330
710
  // Keep resolving it while those legacy launchers transition to schema v3.
@@ -353,6 +733,12 @@ function debug(msg) {
353
733
  }
354
734
  async function renderStatusline() {
355
735
  const version = readPackageVersion();
736
+ const cwd = process.cwd();
737
+ let ingestion;
738
+ const resolveIngestion = () => {
739
+ ingestion ??= decisionIngestionStatus(cwd);
740
+ return ingestion;
741
+ };
356
742
  const snapshot = await daemonRequest(
357
743
  "status_snapshot",
358
744
  // callerEnv lets the daemon withhold presence when it is bound to a different
@@ -363,7 +749,9 @@ async function renderStatusline() {
363
749
  if (!snapshot) {
364
750
  debug("daemon snapshot missing");
365
751
  }
366
- return formatStatusline(version, snapshot, () => decisionIngestionStatus(process.cwd()));
752
+ return formatStatusline(version, snapshot, resolveIngestion, {
753
+ resolveRepositoryBindingState: () => resolveIngestion() === "enabled" ? repositoryBindingState(cwd) : void 0
754
+ });
367
755
  }
368
756
 
369
757
  // src/statusline-main.ts