@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
package/README.md CHANGED
@@ -94,6 +94,23 @@ Orchestrates auth → session hooks → supervised daemon → capture-health gat
94
94
  re-running each underlying command so every step behaves exactly as if run by
95
95
  hand (including the browser login). Idempotent — safe to re-run.
96
96
 
97
+ ### Uninstall
98
+
99
+ ```bash
100
+ prim uninstall
101
+ ```
102
+
103
+ Stops the companion daemon, removes the current repository's Prim-owned agent
104
+ and Git-hook surfaces when run inside a Git repository, removes the user-scoped
105
+ Claude, Codex, Hermes, and Git-hook surfaces, then deletes only schema-valid
106
+ Prim runtime bytes. Foreign configuration is retained; ambiguous ownership
107
+ makes the command fail closed and retain the runtimes. If a removal races or
108
+ fails after one runtime changes, its JSON result reports each runtime's exact
109
+ state so rerunning can safely finish cleanup. Authentication,
110
+ undelivered journals, repository bindings, and agent skill guidance are kept.
111
+ Use `prim skill uninstall --agent <claude|codex|hermes> --scope <project|user>`
112
+ when you also want to remove a known skill target.
113
+
97
114
  ### Auth
98
115
 
99
116
  ```bash
@@ -101,8 +118,23 @@ prim auth login # Authenticate via browser
101
118
  prim auth set-token <token> # Save a bearer token (e.g. for CI)
102
119
  prim auth clear # Remove saved tokens
103
120
  prim auth status # Check authentication status
121
+ prim auth api-keys mint --name <name> [--expires-at <epoch-ms>] # Mint a user key; secret prints once
122
+ prim auth api-keys list [--limit <count>] [--after <api-key-id>] # List user-key metadata
123
+ prim auth api-keys revoke <api-key-id> # Revoke a user key
124
+ ```
125
+
126
+ ### GitHub
127
+
128
+ ```bash
129
+ prim github connect # Install or reuse the Primitive GitHub App for this checkout
130
+ prim github connect --no-browser # Print the installation URL without opening it
104
131
  ```
105
132
 
133
+ The command first reuses existing repository access. When access is missing, it creates a
134
+ server-owned GitHub App installation intent, opens its installation URL unless browser opening is
135
+ suppressed, polls the same intent, then verifies and persists this checkout's binding. A completed
136
+ installation without admin access to this repository remains truthfully unbound.
137
+
106
138
  ### Session integration
107
139
 
108
140
  Wires the agent's session hooks so the decisions you make are captured into the
@@ -145,13 +177,19 @@ unless Prim's global `--yes` is present.
145
177
  #### Claude decision feedback
146
178
 
147
179
  Claude Code receives eventual, human-visible feedback when automatic capture
148
- creates an accepted `change` Decision. A later Claude `Stop` or fresh
180
+ creates a `change` Decision. A later Claude `Stop` or fresh
149
181
  `SessionStart` in the same Git worktree can display:
150
182
 
151
183
  ```text
152
184
  [prim] response → created Decision (dec_a1b2c3d4): Use the stable API (https://app.getprimitive.ai/decisions/r571n1dqjdrtyxxpf0fnzee4gn8aed6q)
153
185
  ```
154
186
 
187
+ An author-private draft uses an actionable publish prompt instead:
188
+
189
+ ```text
190
+ [prim] publish this Decision draft (dec_a1b2c3d4)? Use the stable API (https://app.getprimitive.ai/decisions/r571n1dqjdrtyxxpf0fnzee4gn8aed6q) Run `prim decisions publish decision-full-1` to share it with your team.
191
+ ```
192
+
155
193
  This is a hook `systemMessage` for the person using Claude Code; it is not
156
194
  injected into the model's context. Delivery is at-least-once at the stdout
157
195
  handoff boundary: prim acknowledges only after writing the hook response, so a
package/SKILL.md CHANGED
@@ -20,7 +20,7 @@ Run `npx --yes @primitive.ai/prim auth status` first. It exits **0 if authentica
20
20
  Three ways to authenticate, in priority order:
21
21
 
22
22
  1. **`PRIM_TOKEN` environment variable** -- preferred for agents and CI. Set it before invoking prim and you're done; no interactive flow, no token files.
23
- 2. **`npx --yes @primitive.ai/prim auth set-token <token>`** -- saves a bearer token to `~/.config/prim/token`. Use when the user has a long-lived token in hand.
23
+ 2. **`npx --yes @primitive.ai/prim auth set-token <token>`** -- saves a bearer token in Primitive's config directory (`PRIM_CONFIG_DIR`, then `$XDG_CONFIG_HOME/prim`, default `~/.config/prim`). Use when the user has a long-lived token in hand.
24
24
  3. **`npx --yes @primitive.ai/prim auth login`** -- opens a browser via WorkOS OAuth. **Drive this yourself; do not hand it to the user.** It blocks up to 2 minutes waiting for approval -- that wait is expected, not a failure. The user's only action is clicking "Authorize". Run it in the background, surface the authorize URL it prints on STDERR so the user can click it if the browser didn't open, then poll `auth status` until it exits 0. If it times out before they click, run it again -- never fall back to asking them to run it.
25
25
 
26
26
  The CLI auto-refreshes a still-valid session from the stored refresh token (proactively ~60s before expiry, and again on a 401), so a short access-token "expires in 2m" is normal -- not a reason to re-authenticate or warn the user. Only an absent refresh token, or an explicit `Authentication expired. Run prim auth login to re-authenticate.`, warrants a re-login -- which you then drive yourself, per the above. Relay that message if it appears.
@@ -1,34 +1,68 @@
1
1
  import {
2
- clearRepoSyncId,
2
+ binFile,
3
+ compareSemver,
4
+ pinnedNpxCommand
5
+ } from "./chunk-4A2XB2PC.js";
6
+ import {
3
7
  gitToplevel,
4
8
  githubRepositoryFullName,
5
9
  isValidRepoSyncId,
6
- setRepoSyncId
7
- } from "./chunk-TDRXHBEX.js";
10
+ setRepoSyncId,
11
+ setRepositoryBindingState
12
+ } from "./chunk-TJTVHDJ5.js";
8
13
  import {
9
14
  HttpError,
15
+ atomicWriteFile,
10
16
  getClient,
11
17
  withFileLock
12
- } from "./chunk-RTLFE6EZ.js";
18
+ } from "./chunk-Z7YPFWAZ.js";
19
+
20
+ // src/lib/bin-cache.ts
21
+ import { chmodSync, mkdirSync } from "fs";
22
+ import { homedir } from "os";
23
+ import { join } from "path";
24
+ var GIT_HOOK_CACHE_SHELL_DIR = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
25
+ var GIT_HOOK_CACHE_TTL_MINUTES = 1440;
26
+ var GIT_HOOK_CACHED_BINS = ["prim-post-commit", "prim-post-rewrite"];
27
+ function binCacheDir() {
28
+ const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
29
+ return join(base, "prim", "bin");
30
+ }
31
+ function writeAtomic(path, content) {
32
+ atomicWriteFile(path, content, { mode: 384 });
33
+ }
34
+ function warmBinCache() {
35
+ try {
36
+ if (process.env.PRIM_BIN_CACHE === "0") {
37
+ return;
38
+ }
39
+ const dir = binCacheDir();
40
+ mkdirSync(dir, { recursive: true, mode: 448 });
41
+ chmodSync(dir, 448);
42
+ writeAtomic(join(dir, "node"), process.execPath);
43
+ for (const bin of GIT_HOOK_CACHED_BINS) {
44
+ const file = binFile(bin);
45
+ if (file) {
46
+ writeAtomic(join(dir, bin), file);
47
+ }
48
+ }
49
+ } catch {
50
+ }
51
+ }
13
52
 
14
53
  // src/commands/skill.ts
15
- import { randomUUID } from "crypto";
16
54
  import {
17
55
  constants as constants2,
18
56
  closeSync as closeSync2,
19
57
  existsSync as existsSync2,
20
58
  fstatSync as fstatSync2,
21
- fsyncSync,
22
59
  lstatSync as lstatSync2,
23
60
  openSync as openSync2,
24
61
  readFileSync as readFileSync2,
25
- readSync as readSync2,
26
- renameSync,
27
- rmSync as rmSync2,
28
- writeFileSync
62
+ readSync as readSync2
29
63
  } from "fs";
30
- import { homedir as homedir2 } from "os";
31
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
64
+ import { homedir as homedir3 } from "os";
65
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
32
66
  import { fileURLToPath as fileURLToPath2 } from "url";
33
67
  import { createPatch } from "diff";
34
68
  import { parse as parseYaml2 } from "yaml";
@@ -46,7 +80,7 @@ import {
46
80
  existsSync,
47
81
  fstatSync,
48
82
  lstatSync,
49
- mkdirSync,
83
+ mkdirSync as mkdirSync2,
50
84
  openSync,
51
85
  readFileSync,
52
86
  readSync,
@@ -55,49 +89,10 @@ import {
55
89
  rmSync,
56
90
  rmdirSync
57
91
  } from "fs";
58
- import { homedir } from "os";
59
- import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
92
+ import { homedir as homedir2 } from "os";
93
+ import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
60
94
  import { fileURLToPath } from "url";
61
95
  import { parse as parseYaml } from "yaml";
62
-
63
- // src/lib/semver.ts
64
- function parseSemver(value) {
65
- if (typeof value !== "string" || value.length > 256) return;
66
- const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
67
- value
68
- );
69
- if (!match) return;
70
- const prerelease = match[4]?.split(".") ?? [];
71
- if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
72
- return;
73
- }
74
- return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
75
- }
76
- function compareSemver(left, right) {
77
- const a = parseSemver(left);
78
- const b = parseSemver(right);
79
- if (!a || !b) return;
80
- for (let i = 0; i < a.core.length; i += 1) {
81
- if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
82
- }
83
- if (a.prerelease.length === 0 || b.prerelease.length === 0) {
84
- return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
85
- }
86
- for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i += 1) {
87
- const x = a.prerelease[i];
88
- const y = b.prerelease[i];
89
- if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
90
- if (x === y) continue;
91
- const xNumeric = /^\d+$/u.test(x);
92
- const yNumeric = /^\d+$/u.test(y);
93
- if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
94
- if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
95
- return x < y ? -1 : 1;
96
- }
97
- return 0;
98
- }
99
-
100
- // src/commands/claude-plugin.ts
101
96
  var __dirname = dirname(fileURLToPath(import.meta.url));
102
97
  var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
103
98
  var MAX_MANIFEST_BYTES = 64 * 1024;
@@ -105,8 +100,8 @@ var MAX_SKILL_BYTES = 1024 * 1024;
105
100
  var REFRESH_LOCK_TIMEOUT_MS = 150;
106
101
  var REFRESH_LOCK_POLL_MS = 10;
107
102
  function pluginDirFor(cwd, scope) {
108
- const base = scope === "user" ? join(homedir(), ".claude") : join(gitToplevel(cwd) ?? cwd, ".claude");
109
- return join(base, "skills", "prim");
103
+ const base = scope === "user" ? join2(homedir2(), ".claude") : join2(gitToplevel(cwd) ?? cwd, ".claude");
104
+ return join2(base, "skills", "prim");
110
105
  }
111
106
  function resolvePluginDir(cwd, scope) {
112
107
  if (scope && scope !== "user" && scope !== "project") {
@@ -138,8 +133,8 @@ function removeDirIfEmpty(dir) {
138
133
  }
139
134
  function pluginPaths(dir) {
140
135
  return {
141
- manifestPath: join(dir, ".claude-plugin", "plugin.json"),
142
- skillPath: join(dir, "SKILL.md")
136
+ manifestPath: join2(dir, ".claude-plugin", "plugin.json"),
137
+ skillPath: join2(dir, "SKILL.md")
143
138
  };
144
139
  }
145
140
  function pathIsInside(root, path) {
@@ -204,9 +199,9 @@ function inspectRecognizedInstallation(paths, projectRoot) {
204
199
  };
205
200
  }
206
201
  function refreshLockDir(physicalPluginDir) {
207
- const cacheRoot = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
202
+ const cacheRoot = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
208
203
  const identity = createHash("sha256").update(physicalPluginDir).digest("hex");
209
- return join(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
204
+ return join2(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
210
205
  }
211
206
  async function refreshClaudePlugins(cwd, options = {}) {
212
207
  const result = { installed: 0, refreshed: 0 };
@@ -214,7 +209,7 @@ async function refreshClaudePlugins(cwd, options = {}) {
214
209
  if (options.includeProject !== false) {
215
210
  const projectRoot = options.projectRoot ?? gitToplevel(cwd);
216
211
  if (projectRoot !== null && isAbsolute(projectRoot)) {
217
- candidates.push({ dir: join(projectRoot, ".claude", "skills", "prim"), projectRoot });
212
+ candidates.push({ dir: join2(projectRoot, ".claude", "skills", "prim"), projectRoot });
218
213
  }
219
214
  }
220
215
  candidates.push({ dir: pluginDirFor(cwd, "user") });
@@ -290,7 +285,7 @@ function installClaudePlugin(cwd, opts) {
290
285
  console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
291
286
  return 0;
292
287
  }
293
- mkdirSync(join(dir, ".claude-plugin"), { recursive: true });
288
+ mkdirSync2(join2(dir, ".claude-plugin"), { recursive: true });
294
289
  atomicWrite(manifestPath, manifest);
295
290
  atomicWrite(skillPath, skill);
296
291
  console.log(`Installed prim skill plugin at ${dir}`);
@@ -310,7 +305,7 @@ function uninstallClaudePlugin(cwd, opts) {
310
305
  }
311
306
  rmSync(manifestPath, { force: true });
312
307
  rmSync(skillPath, { force: true });
313
- removeDirIfEmpty(join(dir, ".claude-plugin"));
308
+ removeDirIfEmpty(join2(dir, ".claude-plugin"));
314
309
  removeDirIfEmpty(dir);
315
310
  console.log(`Removed prim skill plugin from ${dir}`);
316
311
  return 0;
@@ -365,10 +360,10 @@ function readCodexGuidance(path) {
365
360
  }
366
361
  }
367
362
  function effectiveCodexGuidance(scope) {
368
- const override = readCodexGuidance(join2(scope, "AGENTS.override.md"));
363
+ const override = readCodexGuidance(join3(scope, "AGENTS.override.md"));
369
364
  if (override === null) return null;
370
365
  if (override !== void 0 && override.toString("utf8").trim().length > 0) return override;
371
- const base = readCodexGuidance(join2(scope, "AGENTS.md"));
366
+ const base = readCodexGuidance(join3(scope, "AGENTS.md"));
372
367
  return base ?? null;
373
368
  }
374
369
  function hasUsablePrimBlock(content, visibleBytes) {
@@ -391,7 +386,7 @@ function hasUsablePrimBlock(content, visibleBytes) {
391
386
  }
392
387
  function hasUsableCodexGuidance(projectRoot) {
393
388
  try {
394
- const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join2(homedir2(), ".codex");
389
+ const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir3(), ".codex");
395
390
  const scopes = [
396
391
  { path: codexHome, visibleBytes: CODEX_GUIDANCE_LIMIT_BYTES },
397
392
  { path: projectRoot, visibleBytes: CODEX_DEFAULT_PROJECT_GUIDANCE_BYTES }
@@ -419,10 +414,10 @@ var AGENT_TARGET = {
419
414
  hermes: ".hermes.md"
420
415
  };
421
416
  function userTargetFor(agent) {
422
- if (agent === "claude") return join2(homedir2(), ".claude", "CLAUDE.md");
423
- if (agent === "codex") return join2(homedir2(), ".codex", "AGENTS.md");
417
+ if (agent === "claude") return join3(homedir3(), ".claude", "CLAUDE.md");
418
+ if (agent === "codex") return join3(homedir3(), ".codex", "AGENTS.md");
424
419
  if (agent === "hermes") {
425
- return join2(process.env.HERMES_HOME ?? join2(homedir2(), ".hermes"), ".hermes.md");
420
+ return join3(process.env.HERMES_HOME ?? join3(homedir3(), ".hermes"), ".hermes.md");
426
421
  }
427
422
  return null;
428
423
  }
@@ -463,27 +458,7 @@ function removeBlock(existing) {
463
458
  return out.replace(/(\r?\n){2,}$/, "$1");
464
459
  }
465
460
  function atomicWrite(target, content) {
466
- const tmp = `${target}.${randomUUID()}.tmp`;
467
- let temporaryCreated = false;
468
- try {
469
- const fd = openSync2(tmp, "wx");
470
- temporaryCreated = true;
471
- try {
472
- writeFileSync(fd, content);
473
- fsyncSync(fd);
474
- } finally {
475
- closeSync2(fd);
476
- }
477
- renameSync(tmp, target);
478
- } catch (error) {
479
- if (temporaryCreated) {
480
- try {
481
- rmSync2(tmp, { force: true });
482
- } catch {
483
- }
484
- }
485
- throw error;
486
- }
461
+ atomicWriteFile(target, content);
487
462
  }
488
463
  function resolveTarget(cwd, opts) {
489
464
  if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
@@ -597,20 +572,20 @@ function registerSkillCommands(program) {
597
572
 
598
573
  // src/lib/post-commit-hook.ts
599
574
  import { execFileSync } from "child_process";
600
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
575
+ import { createHash as createHash2, randomUUID } from "crypto";
601
576
  import {
602
577
  constants as constants3,
603
578
  closeSync as closeSync3,
604
579
  existsSync as existsSync3,
605
580
  fchmodSync,
606
- fsyncSync as fsyncSync2,
581
+ fsyncSync,
607
582
  lstatSync as lstatSync3,
608
- mkdirSync as mkdirSync2,
583
+ mkdirSync as mkdirSync3,
609
584
  openSync as openSync3,
610
585
  readFileSync as readFileSync3,
611
- renameSync as renameSync2,
586
+ renameSync,
612
587
  unlinkSync,
613
- writeFileSync as writeFileSync2
588
+ writeFileSync
614
589
  } from "fs";
615
590
  import { basename, dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
616
591
  var PRIM_POST_COMMIT_BLOCK_START = "# >>> prim post-commit hook >>>";
@@ -656,9 +631,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
656
631
  if [ -n "$prim_commit_sha" ]; then
657
632
  prim_commit_observed_file=$(mktemp "\${TMPDIR:-/tmp}/prim-post-commit-observed.XXXXXXXX" 2>/dev/null) || prim_commit_observed_file=
658
633
  prim_commit_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_commit_branch=
659
- prim_cache_dir="\${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin"
634
+ prim_cache_dir="${GIT_HOOK_CACHE_SHELL_DIR}"
660
635
  prim_post_commit_ran=0
661
- if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-commit" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-commit" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-1440}" 2>/dev/null)" ]; then
636
+ if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-commit" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-commit" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-${GIT_HOOK_CACHE_TTL_MINUTES}}" 2>/dev/null)" ]; then
662
637
  prim_node=$(cat "$prim_cache_dir/node")
663
638
  prim_entry=$(cat "$prim_cache_dir/prim-post-commit")
664
639
  if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
@@ -666,14 +641,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
666
641
  prim_post_commit_ran=1
667
642
  fi
668
643
  fi
669
- if [ "$prim_post_commit_ran" -eq 0 ]; then
670
- if [ -x "./node_modules/.bin/prim-post-commit" ]; then
671
- ( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; ./node_modules/.bin/prim-post-commit ) </dev/null >/dev/null 2>&1 &
672
- prim_post_commit_ran=1
673
- elif command -v npx >/dev/null 2>&1; then
674
- ( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; npx --yes -p @primitive.ai/prim@latest prim-post-commit ) </dev/null >/dev/null 2>&1 &
675
- prim_post_commit_ran=1
676
- fi
644
+ if [ "$prim_post_commit_ran" -eq 0 ] && command -v npx >/dev/null 2>&1; then
645
+ ( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; ${pinnedNpxCommand("prim-post-commit")} ) </dev/null >/dev/null 2>&1 &
646
+ prim_post_commit_ran=1
677
647
  fi
678
648
  if [ "$prim_post_commit_ran" -eq 0 ] && [ -n "$prim_commit_observed_file" ]; then
679
649
  rm -f "$prim_commit_observed_file"
@@ -701,9 +671,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
701
671
  if cat > "$prim_rewrite_pairs_file"; then
702
672
  exec < "$prim_rewrite_pairs_file"
703
673
  prim_rewrite_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_rewrite_branch=
704
- prim_cache_dir="\${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin"
674
+ prim_cache_dir="${GIT_HOOK_CACHE_SHELL_DIR}"
705
675
  prim_post_rewrite_ran=0
706
- if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-rewrite" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-rewrite" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-1440}" 2>/dev/null)" ]; then
676
+ if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-rewrite" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-rewrite" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-${GIT_HOOK_CACHE_TTL_MINUTES}}" 2>/dev/null)" ]; then
707
677
  prim_node=$(cat "$prim_cache_dir/node")
708
678
  prim_entry=$(cat "$prim_cache_dir/prim-post-rewrite")
709
679
  if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
@@ -711,14 +681,9 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
711
681
  prim_post_rewrite_ran=1
712
682
  fi
713
683
  fi
714
- if [ "$prim_post_rewrite_ran" -eq 0 ]; then
715
- if [ -x "./node_modules/.bin/prim-post-rewrite" ]; then
716
- ( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; ./node_modules/.bin/prim-post-rewrite ) </dev/null >/dev/null 2>&1 &
717
- prim_post_rewrite_ran=1
718
- elif command -v npx >/dev/null 2>&1; then
719
- ( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; npx --yes -p @primitive.ai/prim@latest prim-post-rewrite ) </dev/null >/dev/null 2>&1 &
720
- prim_post_rewrite_ran=1
721
- fi
684
+ if [ "$prim_post_rewrite_ran" -eq 0 ] && command -v npx >/dev/null 2>&1; then
685
+ ( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; ${pinnedNpxCommand("prim-post-rewrite")} ) </dev/null >/dev/null 2>&1 &
686
+ prim_post_rewrite_ran=1
722
687
  fi
723
688
  if [ "$prim_post_rewrite_ran" -eq 0 ]; then
724
689
  rm -f "$prim_rewrite_pairs_file"
@@ -1026,10 +991,10 @@ function assertTargetUnchanged(path, expected, spec, stat) {
1026
991
  }
1027
992
  function rewriteHookAtomically(path, next, expected, spec, stat) {
1028
993
  const parent = dirname3(path);
1029
- mkdirSync2(parent, { recursive: true });
994
+ mkdirSync3(parent, { recursive: true });
1030
995
  const temporary = resolve3(
1031
996
  parent,
1032
- `.${basename(path)}.prim-${String(process.pid)}-${randomUUID2()}.tmp`
997
+ `.${basename(path)}.prim-${String(process.pid)}-${randomUUID()}.tmp`
1033
998
  );
1034
999
  let fd;
1035
1000
  try {
@@ -1038,13 +1003,13 @@ function rewriteHookAtomically(path, next, expected, spec, stat) {
1038
1003
  constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW,
1039
1004
  stat?.mode ?? 493
1040
1005
  );
1041
- writeFileSync2(fd, next);
1006
+ writeFileSync(fd, next);
1042
1007
  fchmodSync(fd, (stat?.mode ?? 493) & 4095);
1043
- fsyncSync2(fd);
1008
+ fsyncSync(fd);
1044
1009
  closeSync3(fd);
1045
1010
  fd = void 0;
1046
1011
  assertTargetUnchanged(path, expected, spec, stat);
1047
- renameSync2(temporary, path);
1012
+ renameSync(temporary, path);
1048
1013
  } finally {
1049
1014
  if (fd !== void 0) {
1050
1015
  try {
@@ -1353,40 +1318,66 @@ function uninstallPostRewriteHookAtPath(hookPath) {
1353
1318
  }
1354
1319
 
1355
1320
  // src/lib/repository-binding.ts
1356
- async function resolveRepositoryBinding(root, options) {
1321
+ var InvalidRepositoryBindingResponseError = class extends Error {
1322
+ };
1323
+ async function resolveRepositoryBindingWithClient(root, client, options) {
1357
1324
  const repositoryFullName = githubRepositoryFullName(root);
1358
1325
  if (!repositoryFullName) {
1359
1326
  throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
1360
1327
  }
1361
1328
  let response;
1362
1329
  try {
1363
- response = await getClient().post(
1330
+ response = await client.post(
1364
1331
  "/api/cli/repositories/bind",
1365
1332
  { repositoryFullName },
1366
1333
  options
1367
1334
  );
1368
1335
  } catch (error) {
1369
1336
  if (error instanceof HttpError && error.status === 404) {
1370
- return { status: "pending", repositoryFullName };
1337
+ return { status: "unbound", repositoryFullName };
1371
1338
  }
1372
1339
  throw error;
1373
1340
  }
1374
1341
  if (!isValidRepoSyncId(response.repoSyncId)) {
1375
- throw new Error("server returned no repository binding");
1342
+ throw new InvalidRepositoryBindingResponseError(
1343
+ "server returned an invalid repository binding"
1344
+ );
1376
1345
  }
1377
1346
  return { status: "connected", repoSyncId: response.repoSyncId, repositoryFullName };
1378
1347
  }
1379
- async function bindRepository(root, options) {
1380
- const binding = await resolveRepositoryBinding(root, options);
1348
+ async function resolveRepositoryBinding(root, options) {
1349
+ return resolveRepositoryBindingWithClient(root, getClient(), options);
1350
+ }
1351
+ function persistRepositoryBinding(root, binding) {
1381
1352
  if (binding.status === "connected") {
1382
- setRepoSyncId(root, binding.repoSyncId);
1383
- } else {
1384
- clearRepoSyncId(root);
1353
+ setRepoSyncId(root, binding.repoSyncId, binding.repositoryFullName);
1354
+ setRepositoryBindingState(root, "connected");
1355
+ return;
1385
1356
  }
1357
+ setRepositoryBindingState(root, "unbound");
1358
+ }
1359
+ async function bindRepositoryWithClient(root, client, options) {
1360
+ let binding;
1361
+ try {
1362
+ binding = await resolveRepositoryBindingWithClient(root, client, options);
1363
+ } catch (error) {
1364
+ if (error instanceof InvalidRepositoryBindingResponseError) {
1365
+ try {
1366
+ setRepositoryBindingState(root, "invalid");
1367
+ } catch {
1368
+ }
1369
+ }
1370
+ throw error;
1371
+ }
1372
+ persistRepositoryBinding(root, binding);
1386
1373
  return binding;
1387
1374
  }
1375
+ async function bindRepository(root, options) {
1376
+ return bindRepositoryWithClient(root, getClient(), options);
1377
+ }
1388
1378
 
1389
1379
  export {
1380
+ warmBinCache,
1390
1381
  postCommitHookBlock,
1391
1382
  postRewriteHookBlock,
1392
1383
  ensureEffectivePostCommitHook,
@@ -1400,9 +1391,9 @@ export {
1400
1391
  uninstallPostCommitHookAtPath,
1401
1392
  uninstallPostRewriteHookAtPath,
1402
1393
  resolveRepositoryBinding,
1394
+ bindRepositoryWithClient,
1403
1395
  bindRepository,
1404
1396
  printJson,
1405
- compareSemver,
1406
1397
  refreshClaudePlugins,
1407
1398
  hasUsableCodexGuidance,
1408
1399
  registerSkillCommands