@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
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  getClient,
3
- getSiteUrl
4
- } from "./chunk-RTLFE6EZ.js";
3
+ getSiteUrl,
4
+ terminalSafeLine
5
+ } from "./chunk-Z7YPFWAZ.js";
5
6
  import {
6
7
  daemonRequest
7
- } from "./chunk-UTKQTZHL.js";
8
+ } from "./chunk-CUE2LKLR.js";
8
9
 
9
10
  // src/daemon/proxy.ts
10
11
  var DAEMON_HTTP_TIMEOUT_MS = 1e4;
@@ -93,7 +94,7 @@ var FILES_PREVIEW_LIMIT = 3;
93
94
  function formatDecisionsWarning(result) {
94
95
  const lines = [];
95
96
  if (result.unavailable !== void 0) {
96
- lines.push(`[prim] decision check not verified \u2014 ${result.unavailable}`);
97
+ lines.push(`[prim] decision check not verified \u2014 ${terminalSafeLine(result.unavailable)}`);
97
98
  }
98
99
  if (result.decisions.length > 0) {
99
100
  lines.push(
@@ -101,12 +102,12 @@ function formatDecisionsWarning(result) {
101
102
  );
102
103
  for (const d of result.decisions) {
103
104
  const statusMark = d.status === "under_review" ? " (under review)" : "";
104
- const preview = d.matchedFiles.slice(0, FILES_PREVIEW_LIMIT).join(", ");
105
+ const preview = d.matchedFiles.slice(0, FILES_PREVIEW_LIMIT).map(terminalSafeLine).join(", ");
105
106
  const overflow = d.matchedFiles.length > FILES_PREVIEW_LIMIT ? ` (+${String(d.matchedFiles.length - FILES_PREVIEW_LIMIT)} more)` : "";
106
- lines.push(` \xB7 ${d.intent}${statusMark}`);
107
+ lines.push(` \xB7 ${terminalSafeLine(d.intent)}${statusMark}`);
107
108
  lines.push(` files: ${preview}${overflow}`);
108
109
  if (d.rationale) {
109
- lines.push(` rationale: ${d.rationale}`);
110
+ lines.push(` rationale: ${terminalSafeLine(d.rationale)}`);
110
111
  }
111
112
  }
112
113
  }
@@ -4,6 +4,8 @@ import { createHash } from "crypto";
4
4
  import { lstatSync, realpathSync } from "fs";
5
5
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
6
6
  var GIT_TIMEOUT_MS = 1e3;
7
+ var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
8
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
7
9
  function gitToplevel(cwd) {
8
10
  try {
9
11
  return execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -38,7 +40,9 @@ function githubRepositoryFullName(cwd) {
38
40
  const path = match?.[1] ?? match?.[2];
39
41
  if (!path) return null;
40
42
  const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
41
- if (parts.length !== 2 || parts.some((part) => !part || part !== part.trim())) return null;
43
+ if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
44
+ return null;
45
+ }
42
46
  return `${parts[0]}/${parts[1]}`;
43
47
  }
44
48
  function canonicalGitRoot(cwd) {
@@ -186,6 +190,8 @@ import { homedir } from "os";
186
190
  import { join } from "path";
187
191
  var PRIM_ACTIVE_KEY = "prim.active";
188
192
  var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
193
+ var PRIM_REPO_SYNC_REPOSITORY_KEY = "prim.repoSyncRepository";
194
+ var PRIM_REPO_BINDING_STATE_KEY = "prim.repoBindingState";
189
195
  var GIT_TIMEOUT_MS2 = 1e3;
190
196
  var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
191
197
  var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
@@ -218,20 +224,26 @@ function setRepoActive(cwd, active) {
218
224
  function isValidRepoSyncId(value) {
219
225
  return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
220
226
  }
221
- function repoSyncId(cwd) {
227
+ function localGitConfigValue(cwd, key) {
222
228
  try {
223
- const value = execFileSync2("git", ["config", "--local", "--get", PRIM_REPO_SYNC_ID_KEY], {
229
+ const value = execFileSync2("git", ["config", "--local", "--get", key], {
224
230
  cwd,
225
231
  encoding: "utf-8",
226
232
  stdio: ["ignore", "pipe", "ignore"],
227
233
  timeout: GIT_TIMEOUT_MS2
228
234
  }).trim();
229
- return isValidRepoSyncId(value) ? value : void 0;
235
+ return value || void 0;
230
236
  } catch {
231
237
  return void 0;
232
238
  }
233
239
  }
234
- function setRepoSyncId(cwd, value) {
240
+ function repoSyncId(cwd) {
241
+ const value = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
242
+ if (!isValidRepoSyncId(value)) return void 0;
243
+ const repositoryFullName = localGitConfigValue(cwd, PRIM_REPO_SYNC_REPOSITORY_KEY);
244
+ return repositoryFullName === githubRepositoryFullName(cwd) ? value : void 0;
245
+ }
246
+ function setRepoSyncId(cwd, value, repositoryFullName) {
235
247
  if (!isValidRepoSyncId(value)) {
236
248
  throw new Error("invalid repository binding returned by server");
237
249
  }
@@ -240,20 +252,30 @@ function setRepoSyncId(cwd, value) {
240
252
  stdio: ["ignore", "ignore", "pipe"],
241
253
  timeout: GIT_TIMEOUT_MS2
242
254
  });
255
+ execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_REPOSITORY_KEY, repositoryFullName], {
256
+ cwd,
257
+ stdio: ["ignore", "ignore", "pipe"],
258
+ timeout: GIT_TIMEOUT_MS2
259
+ });
243
260
  }
244
- function clearRepoSyncId(cwd) {
245
- try {
246
- execFileSync2("git", ["config", "--local", "--unset-all", PRIM_REPO_SYNC_ID_KEY], {
247
- cwd,
248
- stdio: ["ignore", "ignore", "pipe"],
249
- timeout: GIT_TIMEOUT_MS2
250
- });
251
- } catch (error) {
252
- if (typeof error === "object" && error !== null && "status" in error && error.status === 5) {
253
- return;
254
- }
255
- throw error;
261
+ function setRepositoryBindingState(cwd, state) {
262
+ if (state !== "connected" && state !== "unbound" && state !== "invalid") {
263
+ throw new Error("invalid repository binding state");
256
264
  }
265
+ execFileSync2("git", ["config", "--local", PRIM_REPO_BINDING_STATE_KEY, state], {
266
+ cwd,
267
+ stdio: ["ignore", "ignore", "pipe"],
268
+ timeout: GIT_TIMEOUT_MS2
269
+ });
270
+ }
271
+ function repositoryBindingState(cwd) {
272
+ const rawId = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
273
+ const marker = localGitConfigValue(cwd, PRIM_REPO_BINDING_STATE_KEY);
274
+ if (rawId !== void 0 && !isValidRepoSyncId(rawId)) return "invalid";
275
+ if (marker === "unbound" || marker === "invalid") return marker;
276
+ if (marker === "connected") return repoSyncId(cwd) ? "connected" : "invalid";
277
+ if (marker !== void 0) return "invalid";
278
+ return rawId === void 0 ? "unbound" : repoSyncId(cwd) ? void 0 : "unbound";
257
279
  }
258
280
  function hasProjectPrimInstall(cwd) {
259
281
  const root = gitToplevel(cwd);
@@ -292,7 +314,8 @@ export {
292
314
  isValidRepoSyncId,
293
315
  repoSyncId,
294
316
  setRepoSyncId,
295
- clearRepoSyncId,
317
+ setRepositoryBindingState,
318
+ repositoryBindingState,
296
319
  isRepoActiveForCapture,
297
320
  decisionIngestionStatus
298
321
  };
@@ -9,6 +9,7 @@ function parseAgent(argv) {
9
9
  }
10
10
 
11
11
  // src/hooks/normalize.ts
12
+ import { createHash } from "crypto";
12
13
  var HERMES_EVENT_MAP = {
13
14
  on_session_start: "SessionStart",
14
15
  on_session_end: "SessionEnd",
@@ -19,6 +20,22 @@ var HERMES_EVENT_MAP = {
19
20
  subagent_stop: "SubagentStop"
20
21
  };
21
22
  function normalizeEnvelope(parsed, agent) {
23
+ if (agent === "codex") {
24
+ const event = parsed.hook_event_name;
25
+ if ((event === "PreToolUse" || event === "PostToolUse") && (typeof parsed.tool_use_id !== "string" || parsed.tool_use_id.length === 0)) {
26
+ const stableToolIdentity = JSON.stringify([
27
+ parsed.session_id ?? null,
28
+ parsed.turn_id ?? null,
29
+ parsed.tool_name ?? null,
30
+ parsed.tool_input ?? null
31
+ ]);
32
+ return {
33
+ ...parsed,
34
+ tool_use_id: `codex:fallback:v1:${createHash("sha256").update(stableToolIdentity).digest("hex")}`
35
+ };
36
+ }
37
+ return parsed;
38
+ }
22
39
  if (agent !== "hermes") {
23
40
  return parsed;
24
41
  }
@@ -1,6 +1,11 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-RTLFE6EZ.js";
3
+ } from "./chunk-Z7YPFWAZ.js";
4
+ import {
5
+ jwtOrganizationId,
6
+ primConfigDirectory,
7
+ resolveAuthCredential
8
+ } from "./chunk-3EEFL6NJ.js";
4
9
 
5
10
  // src/journal.ts
6
11
  import {
@@ -15,9 +20,8 @@ import {
15
20
  readSync,
16
21
  readdirSync
17
22
  } from "fs";
18
- import { homedir } from "os";
19
23
  import { dirname, join } from "path";
20
- var JOURNAL_DIR = join(homedir(), ".config", "prim", "moves");
24
+ var JOURNAL_DIR = join(primConfigDirectory(), "moves");
21
25
  var UNBOUND_BUCKET = "_unbound";
22
26
  var JOURNAL_BASENAME = "journal.ndjson";
23
27
  var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
@@ -139,7 +143,9 @@ function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
139
143
  function parseFlushingPid(name) {
140
144
  const segments = name.slice(FLUSHING_PREFIX.length).split(".");
141
145
  const last = segments.length >= 2 ? segments[segments.length - 1] : void 0;
142
- return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
146
+ if (last === void 0 || !/^[0-9]+$/.test(last)) return void 0;
147
+ const pid = Number(last);
148
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
143
149
  }
144
150
  function listFlushingInDir(dir, bucket, options = {}) {
145
151
  if (!existsSync(dir)) {
@@ -253,18 +259,10 @@ function pendingJournalStats() {
253
259
 
254
260
  // src/binding.ts
255
261
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
256
- import { homedir as homedir2 } from "os";
257
262
  import { dirname as dirname2, join as join2 } from "path";
258
- var PRIM_CONFIG_DIR = join2(homedir2(), ".config", "prim");
259
- var TOKEN_PATH = join2(PRIM_CONFIG_DIR, "token");
263
+ var PRIM_CONFIG_DIR = primConfigDirectory();
260
264
  var SESSIONS_DIR = join2(PRIM_CONFIG_DIR, "sessions");
261
265
  var WORKSPACE_FILE = ".prim/workspace.json";
262
- var JWT_PARTS = 3;
263
- var BASE64_PAD_4 = 4;
264
- var ENV_TOKEN_LINE = /^\s*PRIM_TOKEN\s*=\s*(.*)$/m;
265
- var SURROUNDING_QUOTES = /^["']|["']$/g;
266
- var BASE64URL_MINUS = /-/g;
267
- var BASE64URL_UNDERSCORE = /_/g;
268
266
  function readJsonSafe(path) {
269
267
  if (!existsSync2(path)) {
270
268
  return;
@@ -296,46 +294,9 @@ function fromWorkspaceFile(startDir) {
296
294
  dir = parent;
297
295
  }
298
296
  }
299
- function readToken(cwd) {
300
- const fromEnv = process.env.PRIM_TOKEN;
301
- if (fromEnv) {
302
- return fromEnv.trim();
303
- }
304
- if (existsSync2(TOKEN_PATH)) {
305
- return readFileSync2(TOKEN_PATH, "utf-8").trim();
306
- }
307
- const envLocal = join2(cwd, ".env.local");
308
- if (existsSync2(envLocal)) {
309
- const match = readFileSync2(envLocal, "utf-8").match(ENV_TOKEN_LINE);
310
- if (match) {
311
- return match[1].trim().replace(SURROUNDING_QUOTES, "");
312
- }
313
- }
314
- return;
315
- }
316
- function base64UrlDecode(s) {
317
- const padded = s.padEnd(
318
- s.length + (BASE64_PAD_4 - s.length % BASE64_PAD_4) % BASE64_PAD_4,
319
- "="
320
- );
321
- const normalized = padded.replace(BASE64URL_MINUS, "+").replace(BASE64URL_UNDERSCORE, "/");
322
- return Buffer.from(normalized, "base64").toString("utf-8");
323
- }
324
- function fromDefaultOrg(cwd) {
325
- const token = readToken(cwd);
326
- if (!token) {
327
- return;
328
- }
329
- const parts = token.split(".");
330
- if (parts.length !== JWT_PARTS) {
331
- return;
332
- }
333
- try {
334
- const claims = JSON.parse(base64UrlDecode(parts[1]));
335
- return claims.org_id;
336
- } catch {
337
- return;
338
- }
297
+ function fromDefaultOrg(_cwd) {
298
+ const credential = resolveAuthCredential();
299
+ return credential ? jwtOrganizationId(credential.token) : void 0;
339
300
  }
340
301
  function resolveOrg(args) {
341
302
  const fromSession = fromSessionMarker(args.sessionId);