@primitive.ai/prim 0.1.0-alpha.94 → 0.1.0-alpha.97

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 (40) hide show
  1. package/README.md +7 -4
  2. package/dist/{chunk-UKUAAAXE.js → chunk-27ADT6FQ.js} +2 -2
  3. package/dist/{chunk-TYAHC25F.js → chunk-2MBV7ZMV.js} +1 -1
  4. package/dist/{chunk-OTHKG76J.js → chunk-35FMRWJH.js} +1 -1
  5. package/dist/{chunk-O7YAW7G6.js → chunk-3I4GEWXQ.js} +9 -11
  6. package/dist/{chunk-DXJQR4QI.js → chunk-4GHANPWQ.js} +2 -2
  7. package/dist/{chunk-WUXFLE7X.js → chunk-7KABYYWG.js} +64 -66
  8. package/dist/{chunk-LFFLHSQO.js → chunk-AJYZB5PF.js} +63 -6
  9. package/dist/{chunk-H4UNYFUC.js → chunk-DHOQGHKQ.js} +184 -1
  10. package/dist/{chunk-2GP5UFV7.js → chunk-DWRSEC34.js} +5 -3
  11. package/dist/{chunk-ZU563PQI.js → chunk-FBPZ4YUD.js} +2 -2
  12. package/dist/{chunk-G2VMA2QO.js → chunk-LY6NJ7KL.js} +6 -4
  13. package/dist/{chunk-ZB4F75SZ.js → chunk-MXVCP33E.js} +5 -1
  14. package/dist/{chunk-ICM2XCXS.js → chunk-OW4GJBLW.js} +2 -2
  15. package/dist/{chunk-K7D2EYBS.js → chunk-QNPTWTBG.js} +2 -2
  16. package/dist/{chunk-IMK4JKTA.js → chunk-RK4ZS3HG.js} +2 -2
  17. package/dist/{chunk-OH3EZXYX.js → chunk-UO2U6IKF.js} +1 -1
  18. package/dist/{chunk-FXF4JWK2.js → chunk-XEDJ5N3M.js} +17 -2
  19. package/dist/{chunk-AXQQMX2U.js → chunk-Y5W4TZM7.js} +2 -2
  20. package/dist/{chunk-DSWT6BCC.js → chunk-ZYK3TBEW.js} +2542 -2250
  21. package/dist/daemon/server.js +2547 -2119
  22. package/dist/hook-runtime/daemon/server.js +2547 -2119
  23. package/dist/hook-runtime/hooks/post-commit.js +2547 -2119
  24. package/dist/hook-runtime/hooks/post-rewrite.js +2547 -2119
  25. package/dist/hook-runtime/hooks/post-tool-use.js +2874 -2373
  26. package/dist/hook-runtime/hooks/pre-tool-use.js +3766 -3325
  27. package/dist/hook-runtime/hooks/prim-hook.js +2973 -2440
  28. package/dist/hook-runtime/hooks/session-end.js +179 -129
  29. package/dist/hook-runtime/hooks/session-start.js +2651 -2221
  30. package/dist/hook-runtime/index.js +2596 -2126
  31. package/dist/hooks/post-commit.js +11 -11
  32. package/dist/hooks/post-rewrite.js +10 -10
  33. package/dist/hooks/post-tool-use.js +20 -20
  34. package/dist/hooks/pre-commit.js +4 -4
  35. package/dist/hooks/pre-tool-use.js +29 -17
  36. package/dist/hooks/prim-hook.js +22 -18
  37. package/dist/hooks/session-end.js +11 -5
  38. package/dist/hooks/session-start.js +26 -22
  39. package/dist/index.js +79 -60
  40. package/package.json +1 -1
package/README.md CHANGED
@@ -148,9 +148,12 @@ graph, and presence is reported. Each hook
148
148
  self-resolves the CLI at run time (PATH, then a local install, then
149
149
  `npx --yes @latest`), so it keeps working with no global install.
150
150
 
151
- Installs into the current project by default — the repo's `.claude/settings.json`,
152
- `.cursor/hooks.json`, or `.codex/hooks.json`, resolved from the git root (so any subdirectory works);
153
- pass `--scope user` to install machine-wide. Hermes is the exception: it reads
151
+ Installs into the current project by default — the repo's `.claude/settings.json`
152
+ or `.cursor/hooks.json`, resolved from the git root (so any subdirectory works);
153
+ pass `--scope user` to install machine-wide. Codex defaults to user scope
154
+ (`~/.codex/hooks.json`): Codex reads a project `.codex/hooks.json` only from the
155
+ checkout it lives in and trusts it by absolute path, so a project install covers
156
+ that checkout only, not the repo's other worktrees. Hermes is the exception: it reads
154
157
  shell hooks only from the global `~/.hermes/config.yaml`, so `prim hermes install`
155
158
  is always user-scoped — and prim merges in place, leaving the rest of that file
156
159
  (providers, models, your own hooks) untouched.
@@ -160,7 +163,7 @@ prim claude install # Install Claude Code hooks (project scope; u
160
163
  prim claude install --scope user # Install machine-wide instead
161
164
  prim cursor install # Install native Cursor hooks (project scope)
162
165
  prim cursor install --scope user # Install hooks, skill, and CLI footer machine-wide
163
- prim codex install # Install OpenAI Codex hooks (project scope)
166
+ prim codex install # Install OpenAI Codex hooks (user scope; --scope project for this checkout only)
164
167
  prim hermes install # Install Hermes Agent hooks (global ~/.hermes/config.yaml)
165
168
  ```
166
169
 
@@ -2,10 +2,10 @@ import {
2
2
  getClient,
3
3
  getSiteUrl,
4
4
  terminalSafeLine
5
- } from "./chunk-G2VMA2QO.js";
5
+ } from "./chunk-LY6NJ7KL.js";
6
6
  import {
7
7
  daemonRequest
8
- } from "./chunk-OTHKG76J.js";
8
+ } from "./chunk-35FMRWJH.js";
9
9
 
10
10
  // src/daemon/proxy.ts
11
11
  var DAEMON_HTTP_TIMEOUT_MS = 1e4;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scheduleCollectScopeRefresh
3
- } from "./chunk-DSWT6BCC.js";
3
+ } from "./chunk-ZYK3TBEW.js";
4
4
 
5
5
  // src/ingest-response.ts
6
6
  var IngestAcknowledgementError = class extends Error {
@@ -2,7 +2,7 @@ import {
2
2
  decodeJwtPayload,
3
3
  primConfigDirectory,
4
4
  resolveAuthCredential
5
- } from "./chunk-2GP5UFV7.js";
5
+ } from "./chunk-DWRSEC34.js";
6
6
 
7
7
  // src/daemon/client.ts
8
8
  import { createConnection } from "net";
@@ -1,30 +1,28 @@
1
1
  import {
2
2
  formatStatusline
3
- } from "./chunk-ZU563PQI.js";
4
- import {
5
- decisionIngestionStatus,
6
- repositoryBindingState
7
- } from "./chunk-DSWT6BCC.js";
3
+ } from "./chunk-FBPZ4YUD.js";
8
4
  import {
9
5
  getSiteUrl,
10
6
  isSessionEnded,
11
7
  terminalSafeText
12
- } from "./chunk-G2VMA2QO.js";
8
+ } from "./chunk-LY6NJ7KL.js";
13
9
  import {
14
10
  packageVersion
15
- } from "./chunk-AXQQMX2U.js";
11
+ } from "./chunk-Y5W4TZM7.js";
16
12
  import {
17
- gitToplevel
18
- } from "./chunk-H4UNYFUC.js";
13
+ decisionIngestionStatus,
14
+ gitToplevel,
15
+ repositoryBindingState
16
+ } from "./chunk-DHOQGHKQ.js";
19
17
  import {
20
18
  daemonPrincipalsMatch,
21
19
  daemonRequest,
22
20
  resolveDaemonPrincipal
23
- } from "./chunk-OTHKG76J.js";
21
+ } from "./chunk-35FMRWJH.js";
24
22
  import {
25
23
  primConfigDirectory,
26
24
  withFileLock
27
- } from "./chunk-2GP5UFV7.js";
25
+ } from "./chunk-DWRSEC34.js";
28
26
 
29
27
  // src/hooks/codex-context.ts
30
28
  import { createHash, randomBytes as randomBytes2 } from "crypto";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-G2VMA2QO.js";
3
+ } from "./chunk-LY6NJ7KL.js";
4
4
  import {
5
5
  atomicWriteFile,
6
6
  ensureDurableDirectory,
7
7
  jwtOrganizationId,
8
8
  primConfigDirectory,
9
9
  resolveAuthCredential
10
- } from "./chunk-2GP5UFV7.js";
10
+ } from "./chunk-DWRSEC34.js";
11
11
 
12
12
  // src/journal.ts
13
13
  import {
@@ -1,15 +1,10 @@
1
1
  import {
2
2
  unattendedEnv
3
3
  } from "./chunk-OYHYCK4K.js";
4
- import {
5
- isValidRepoSyncId,
6
- setRepoSyncId,
7
- setRepositoryBindingState
8
- } from "./chunk-DSWT6BCC.js";
9
4
  import {
10
5
  HttpError,
11
6
  getClient
12
- } from "./chunk-G2VMA2QO.js";
7
+ } from "./chunk-LY6NJ7KL.js";
13
8
  import {
14
9
  GIT_HOOK_CONTRACT_VERSION,
15
10
  binFile,
@@ -22,49 +17,19 @@ import {
22
17
  managedHookBlock,
23
18
  pinnedHookCommand,
24
19
  pinnedNpxCommand
25
- } from "./chunk-AXQQMX2U.js";
20
+ } from "./chunk-Y5W4TZM7.js";
26
21
  import {
27
22
  gitToplevel,
28
- githubRepositoryFullName
29
- } from "./chunk-H4UNYFUC.js";
23
+ githubRepositoryFullName,
24
+ isValidRepoSyncId,
25
+ setRepoSyncId,
26
+ setRepositoryBindingState
27
+ } from "./chunk-DHOQGHKQ.js";
30
28
  import {
31
29
  atomicWriteFile,
32
30
  primConfigDirectory,
33
31
  withFileLock
34
- } from "./chunk-2GP5UFV7.js";
35
-
36
- // src/lib/bin-cache.ts
37
- import { chmodSync, mkdirSync } from "fs";
38
- import { homedir } from "os";
39
- import { join } from "path";
40
- var GIT_HOOK_CACHE_SHELL_DIR = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
41
- var GIT_HOOK_CACHE_TTL_MINUTES = 1440;
42
- var GIT_HOOK_CACHED_BINS = ["prim-post-commit", "prim-post-rewrite"];
43
- function binCacheDir() {
44
- const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
45
- return join(base, "prim", "bin");
46
- }
47
- function writeAtomic(path, content) {
48
- atomicWriteFile(path, content, { mode: 384 });
49
- }
50
- function warmBinCache() {
51
- try {
52
- if (process.env.PRIM_BIN_CACHE === "0") {
53
- return;
54
- }
55
- const dir = binCacheDir();
56
- mkdirSync(dir, { recursive: true, mode: 448 });
57
- chmodSync(dir, 448);
58
- writeAtomic(join(dir, "node"), process.execPath);
59
- for (const bin of GIT_HOOK_CACHED_BINS) {
60
- const file = binFile(bin);
61
- if (file) {
62
- writeAtomic(join(dir, bin), file);
63
- }
64
- }
65
- } catch {
66
- }
67
- }
32
+ } from "./chunk-DWRSEC34.js";
68
33
 
69
34
  // src/commands/skill.ts
70
35
  import { createHash as createHash2 } from "crypto";
@@ -81,8 +46,8 @@ import {
81
46
  rmdirSync as rmdirSync2,
82
47
  unlinkSync
83
48
  } from "fs";
84
- import { homedir as homedir3 } from "os";
85
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
49
+ import { homedir as homedir2 } from "os";
50
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
86
51
  import { fileURLToPath as fileURLToPath2 } from "url";
87
52
  import { createPatch } from "diff";
88
53
  import { parse as parseYaml2 } from "yaml";
@@ -100,7 +65,7 @@ import {
100
65
  existsSync,
101
66
  fstatSync,
102
67
  lstatSync,
103
- mkdirSync as mkdirSync2,
68
+ mkdirSync,
104
69
  openSync,
105
70
  readFileSync,
106
71
  readSync,
@@ -109,8 +74,8 @@ import {
109
74
  rmSync,
110
75
  rmdirSync
111
76
  } from "fs";
112
- import { homedir as homedir2 } from "os";
113
- import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
77
+ import { homedir } from "os";
78
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
114
79
  import { fileURLToPath } from "url";
115
80
  import { parse as parseYaml } from "yaml";
116
81
  var __dirname = dirname(fileURLToPath(import.meta.url));
@@ -120,8 +85,8 @@ var MAX_SKILL_BYTES = 1024 * 1024;
120
85
  var REFRESH_LOCK_TIMEOUT_MS = 150;
121
86
  var REFRESH_LOCK_POLL_MS = 10;
122
87
  function pluginDirFor(cwd, scope) {
123
- const base = scope === "user" ? join2(homedir2(), ".claude") : join2(gitToplevel(cwd) ?? cwd, ".claude");
124
- return join2(base, "skills", "prim");
88
+ const base = scope === "user" ? join(homedir(), ".claude") : join(gitToplevel(cwd) ?? cwd, ".claude");
89
+ return join(base, "skills", "prim");
125
90
  }
126
91
  function resolvePluginDir(cwd, scope) {
127
92
  if (scope && scope !== "user" && scope !== "project") {
@@ -153,8 +118,8 @@ function removeDirIfEmpty(dir) {
153
118
  }
154
119
  function pluginPaths(dir) {
155
120
  return {
156
- manifestPath: join2(dir, ".claude-plugin", "plugin.json"),
157
- skillPath: join2(dir, "SKILL.md")
121
+ manifestPath: join(dir, ".claude-plugin", "plugin.json"),
122
+ skillPath: join(dir, "SKILL.md")
158
123
  };
159
124
  }
160
125
  function pathIsInside(root, path) {
@@ -219,9 +184,9 @@ function inspectRecognizedInstallation(paths, projectRoot) {
219
184
  };
220
185
  }
221
186
  function refreshLockDir(physicalPluginDir) {
222
- const cacheRoot = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
187
+ const cacheRoot = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
223
188
  const identity = createHash("sha256").update(physicalPluginDir).digest("hex");
224
- return join2(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
189
+ return join(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
225
190
  }
226
191
  async function refreshClaudePlugins(cwd, options = {}) {
227
192
  const result = { installed: 0, refreshed: 0 };
@@ -229,7 +194,7 @@ async function refreshClaudePlugins(cwd, options = {}) {
229
194
  if (options.includeProject !== false) {
230
195
  const projectRoot = options.projectRoot ?? gitToplevel(cwd);
231
196
  if (projectRoot !== null && isAbsolute(projectRoot)) {
232
- candidates.push({ dir: join2(projectRoot, ".claude", "skills", "prim"), projectRoot });
197
+ candidates.push({ dir: join(projectRoot, ".claude", "skills", "prim"), projectRoot });
233
198
  }
234
199
  }
235
200
  candidates.push({ dir: pluginDirFor(cwd, "user") });
@@ -305,7 +270,7 @@ function installClaudePlugin(cwd, opts) {
305
270
  console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
306
271
  return 0;
307
272
  }
308
- mkdirSync2(join2(dir, ".claude-plugin"), { recursive: true });
273
+ mkdirSync(join(dir, ".claude-plugin"), { recursive: true });
309
274
  atomicWrite(manifestPath, manifest);
310
275
  atomicWrite(skillPath, skill);
311
276
  console.log(`Installed prim skill plugin at ${dir}`);
@@ -325,7 +290,7 @@ function uninstallClaudePlugin(cwd, opts) {
325
290
  }
326
291
  rmSync(manifestPath, { force: true });
327
292
  rmSync(skillPath, { force: true });
328
- removeDirIfEmpty(join2(dir, ".claude-plugin"));
293
+ removeDirIfEmpty(join(dir, ".claude-plugin"));
329
294
  removeDirIfEmpty(dir);
330
295
  console.log(`Removed prim skill plugin from ${dir}`);
331
296
  return 0;
@@ -380,10 +345,10 @@ function readCodexGuidance(path) {
380
345
  }
381
346
  }
382
347
  function effectiveCodexGuidance(scope) {
383
- const override = readCodexGuidance(join3(scope, "AGENTS.override.md"));
348
+ const override = readCodexGuidance(join2(scope, "AGENTS.override.md"));
384
349
  if (override === null) return null;
385
350
  if (override !== void 0 && override.toString("utf8").trim().length > 0) return override;
386
- const base = readCodexGuidance(join3(scope, "AGENTS.md"));
351
+ const base = readCodexGuidance(join2(scope, "AGENTS.md"));
387
352
  return base ?? null;
388
353
  }
389
354
  function hasUsablePrimBlock(content, visibleBytes) {
@@ -406,7 +371,7 @@ function hasUsablePrimBlock(content, visibleBytes) {
406
371
  }
407
372
  function hasUsableCodexGuidance(projectRoot) {
408
373
  try {
409
- const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir3(), ".codex");
374
+ const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join2(homedir2(), ".codex");
410
375
  const scopes = [
411
376
  { path: codexHome, visibleBytes: CODEX_GUIDANCE_LIMIT_BYTES },
412
377
  { path: projectRoot, visibleBytes: CODEX_DEFAULT_PROJECT_GUIDANCE_BYTES }
@@ -440,14 +405,14 @@ function sha256(content) {
440
405
  }
441
406
  function cursorSkillDirectory(cwd, scope) {
442
407
  const base = scope === "user" ? cursorConfigDirectory() : gitToplevel(cwd) ?? cwd;
443
- return join3(base, scope === "user" ? "skills" : ".cursor/skills", "prim");
408
+ return join2(base, scope === "user" ? "skills" : ".cursor/skills", "prim");
444
409
  }
445
410
  function cursorSkillPaths(cwd, scope) {
446
411
  const directory = cursorSkillDirectory(cwd, scope);
447
412
  return {
448
413
  directory,
449
- skill: join3(directory, "SKILL.md"),
450
- manifest: join3(directory, CURSOR_SKILL_MANIFEST)
414
+ skill: join2(directory, "SKILL.md"),
415
+ manifest: join2(directory, CURSOR_SKILL_MANIFEST)
451
416
  };
452
417
  }
453
418
  function readCursorSkillManifest(path) {
@@ -535,10 +500,10 @@ function hasUsableCursorSkill(projectRoot) {
535
500
  return cursorSkillStatus(projectRoot, "project").installed || cursorSkillStatus(projectRoot, "user").installed;
536
501
  }
537
502
  function userTargetFor(agent) {
538
- if (agent === "claude") return join3(homedir3(), ".claude", "CLAUDE.md");
539
- if (agent === "codex") return join3(homedir3(), ".codex", "AGENTS.md");
503
+ if (agent === "claude") return join2(homedir2(), ".claude", "CLAUDE.md");
504
+ if (agent === "codex") return join2(homedir2(), ".codex", "AGENTS.md");
540
505
  if (agent === "hermes") {
541
- return join3(process.env.HERMES_HOME ?? join3(homedir3(), ".hermes"), ".hermes.md");
506
+ return join2(process.env.HERMES_HOME ?? join2(homedir2(), ".hermes"), ".hermes.md");
542
507
  }
543
508
  return null;
544
509
  }
@@ -709,6 +674,39 @@ function registerSkillCommands(program) {
709
674
  });
710
675
  }
711
676
 
677
+ // src/lib/bin-cache.ts
678
+ import { chmodSync, mkdirSync as mkdirSync2 } from "fs";
679
+ import { homedir as homedir3 } from "os";
680
+ import { join as join3 } from "path";
681
+ var GIT_HOOK_CACHE_SHELL_DIR = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
682
+ var GIT_HOOK_CACHE_TTL_MINUTES = 1440;
683
+ var GIT_HOOK_CACHED_BINS = ["prim-post-commit", "prim-post-rewrite"];
684
+ function binCacheDir() {
685
+ const base = process.env.XDG_CACHE_HOME || join3(homedir3(), ".cache");
686
+ return join3(base, "prim", "bin");
687
+ }
688
+ function writeAtomic(path, content) {
689
+ atomicWriteFile(path, content, { mode: 384 });
690
+ }
691
+ function warmBinCache() {
692
+ try {
693
+ if (process.env.PRIM_BIN_CACHE === "0") {
694
+ return;
695
+ }
696
+ const dir = binCacheDir();
697
+ mkdirSync2(dir, { recursive: true, mode: 448 });
698
+ chmodSync(dir, 448);
699
+ writeAtomic(join3(dir, "node"), process.execPath);
700
+ for (const bin of GIT_HOOK_CACHED_BINS) {
701
+ const file = binFile(bin);
702
+ if (file) {
703
+ writeAtomic(join3(dir, bin), file);
704
+ }
705
+ }
706
+ } catch {
707
+ }
708
+ }
709
+
712
710
  // src/lib/git-hook-legacy.ts
713
711
  import { isAbsolute as isAbsolute2 } from "path";
714
712
  var PRIM_MANAGED_MARK = "prim-managed-hook";
@@ -1,18 +1,32 @@
1
+ import {
2
+ getOrCreateWorkspaceId
3
+ } from "./chunk-EZXEWFDG.js";
1
4
  import {
2
5
  analyzeShellTargets,
3
6
  extractFileTargets
4
- } from "./chunk-ZB4F75SZ.js";
7
+ } from "./chunk-MXVCP33E.js";
8
+ import {
9
+ scrubEnvironmentPaths
10
+ } from "./chunk-QNPTWTBG.js";
5
11
  import {
6
- canonicalRepositoryPath
7
- } from "./chunk-H4UNYFUC.js";
12
+ cachedCollectScopeAdmits
13
+ } from "./chunk-ZYK3TBEW.js";
14
+ import {
15
+ canonicalRepositoryPath,
16
+ currentBranch,
17
+ isRepoActiveForCapture,
18
+ relatedWorktreeTarget,
19
+ repoSyncId
20
+ } from "./chunk-DHOQGHKQ.js";
8
21
 
9
22
  // src/hooks/file-refs.ts
10
23
  var MAX_PREFLIGHT_FILE_TARGETS = 25;
11
24
  function primitiveMetadataForResolution(resolution) {
12
- const shouldAttach = resolution.fileRefs.length > 0 || resolution.rejected.length > 0 || resolution.targetsIncomplete || resolution.targetsTruncated || resolution.shellMutation !== void 0;
25
+ const shouldAttach = resolution.fileRefs.length > 0 || (resolution.targetCheckouts?.length ?? 0) > 0 || resolution.rejected.length > 0 || resolution.targetsIncomplete || resolution.targetsTruncated || resolution.shellMutation !== void 0;
13
26
  if (!shouldAttach) return;
14
27
  return {
15
28
  fileRefs: [...resolution.fileRefs],
29
+ ...resolution.targetCheckouts ? { targetCheckouts: resolution.targetCheckouts } : {},
16
30
  ...resolution.rejected.length > 0 || resolution.targetsIncomplete ? { fileRefsIncomplete: true } : {},
17
31
  ...resolution.targetsTruncated ? { fileRefsTruncated: true } : {},
18
32
  ...resolution.shellMutation !== void 0 ? { shellMutation: resolution.shellMutation } : {}
@@ -37,13 +51,55 @@ function resolveHookFileRefs(args) {
37
51
  const targetsTruncated = uniqueRawPaths.length > MAX_PREFLIGHT_FILE_TARGETS;
38
52
  const fileRefs = /* @__PURE__ */ new Set();
39
53
  const rejected = [];
54
+ const targets = /* @__PURE__ */ new Map();
40
55
  for (const path of uniqueRawPaths.slice(0, MAX_PREFLIGHT_FILE_TARGETS)) {
41
56
  const canonical = canonicalRepositoryPath(path, args.repository, args.cwd);
42
57
  if (canonical.ok) fileRefs.add(canonical.file);
43
- else rejected.push({ path, reason: canonical.reason });
58
+ else {
59
+ const target = args.captureTargets && canonical.reason === "outside_repository" ? relatedWorktreeTarget(path, args.cwd, args.repository) : null;
60
+ const resolved = target ? canonicalRepositoryPath(path, target, args.cwd) : null;
61
+ if (target && resolved?.ok && isRepoActiveForCapture(target.repoRoot) && repoSyncId(target.repoRoot) === args.repository.repoSyncId && args.repository.repoSyncId && cachedCollectScopeAdmits(target.repoRoot, {
62
+ repository: target.repoFullName,
63
+ branch: currentBranch(target.repoRoot),
64
+ agent: args.agent,
65
+ paths: [resolved.file],
66
+ pathsComplete: true
67
+ })) {
68
+ const group = targets.get(target.repoRoot) ?? { repository: target, fileRefs: [] };
69
+ group.fileRefs.push(resolved.file);
70
+ targets.set(target.repoRoot, group);
71
+ } else rejected.push({ path, reason: canonical.reason });
72
+ }
73
+ }
74
+ const targetCheckouts = [];
75
+ if (targets.size > 0) {
76
+ if (fileRefs.size > 0)
77
+ targets.set(args.repository.repoRoot, {
78
+ repository: args.repository,
79
+ fileRefs: [...fileRefs]
80
+ });
81
+ for (const { repository, fileRefs: paths } of targets.values()) {
82
+ const identity = getOrCreateWorkspaceId(repository.repoRoot);
83
+ const binding = repoSyncId(repository.repoRoot);
84
+ if (identity.status !== "ready" || !binding) {
85
+ rejected.push({ path: repository.repoRoot, reason: "invalid_path" });
86
+ continue;
87
+ }
88
+ const root = scrubEnvironmentPaths({
89
+ cwd: repository.repoRoot,
90
+ gitRoot: repository.repoRoot
91
+ }).gitRoot;
92
+ targetCheckouts.push({
93
+ gitRoot: root,
94
+ workspaceId: identity.workspaceId,
95
+ repoSyncId: binding,
96
+ fileRefs: paths
97
+ });
98
+ }
44
99
  }
45
100
  return {
46
101
  fileRefs: [...fileRefs],
102
+ ...targetCheckouts.length > 0 ? { targetCheckouts } : {},
47
103
  rejected,
48
104
  targetsIncomplete: nativeTargets?.complete === false,
49
105
  targetsTruncated,
@@ -57,7 +113,8 @@ function enrichHookPayloadWithFileRefs(args) {
57
113
  toolInput: args.parsed.tool_input,
58
114
  agent: args.agent,
59
115
  cwd: args.cwd,
60
- repository: args.repository
116
+ repository: args.repository,
117
+ captureTargets: true
61
118
  });
62
119
  const primitive = primitiveMetadataForResolution(resolution);
63
120
  if (!primitive) return { parsed: args.parsed, resolution };
@@ -18,6 +18,24 @@ function gitToplevel(cwd) {
18
18
  return null;
19
19
  }
20
20
  }
21
+ function mainWorktreeRoot(cwd) {
22
+ try {
23
+ const [gitDir, commonDir] = execFileSync(
24
+ "git",
25
+ ["rev-parse", "--git-dir", "--git-common-dir"],
26
+ {
27
+ cwd,
28
+ encoding: "utf-8",
29
+ stdio: ["ignore", "pipe", "ignore"],
30
+ timeout: GIT_TIMEOUT_MS
31
+ }
32
+ ).trim().split("\n").map((value) => resolve(cwd ?? process.cwd(), value));
33
+ if (gitDir === void 0 || commonDir === void 0 || gitDir === commonDir) return null;
34
+ return basename(commonDir) === ".git" ? dirname(commonDir) : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
21
39
  function gitValue(cwd, args) {
22
40
  try {
23
41
  const value = execFileSync("git", args, {
@@ -56,6 +74,31 @@ function canonicalGitRoot(cwd) {
56
74
  return null;
57
75
  }
58
76
  }
77
+ function relatedWorktreeTarget(path, cwd, origin) {
78
+ let parent = dirname(resolve(cwd, path));
79
+ while (true) {
80
+ try {
81
+ realpathSync.native(parent);
82
+ break;
83
+ } catch {
84
+ const next = dirname(parent);
85
+ if (next === parent) return null;
86
+ parent = next;
87
+ }
88
+ }
89
+ const target = resolveRepositoryContext(parent);
90
+ if (!target || !target.repoKey || target.repoKey !== origin.repoKey) return null;
91
+ const common = (root) => {
92
+ const value = gitValue(root, ["rev-parse", "--git-common-dir"]);
93
+ try {
94
+ return value ? realpathSync.native(resolve(root, value)) : void 0;
95
+ } catch {
96
+ return void 0;
97
+ }
98
+ };
99
+ const originCommon = common(origin.repoRoot);
100
+ return originCommon && originCommon === common(target.repoRoot) ? target : null;
101
+ }
59
102
  function normalizeOriginRemote(remote) {
60
103
  const value = remote.trim();
61
104
  if (!value) return void 0;
@@ -186,11 +229,151 @@ function canonicalRepositoryPath(filePath, contextOrCwd, baseDirOrRoot) {
186
229
  );
187
230
  }
188
231
 
232
+ // src/lib/activation.ts
233
+ import { execFileSync as execFileSync2 } from "child_process";
234
+ import { existsSync, readFileSync } from "fs";
235
+ import { homedir } from "os";
236
+ import { join } from "path";
237
+ var PRIM_ACTIVE_KEY = "prim.active";
238
+ var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
239
+ var PRIM_REPO_SYNC_REPOSITORY_KEY = "prim.repoSyncRepository";
240
+ var PRIM_REPO_BINDING_STATE_KEY = "prim.repoBindingState";
241
+ var GIT_TIMEOUT_MS2 = 1e3;
242
+ var REPO_SYNC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
243
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json", ".cursor/hooks.json"];
244
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
245
+ function repoActiveFlag(cwd) {
246
+ try {
247
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
248
+ cwd,
249
+ encoding: "utf-8",
250
+ stdio: ["ignore", "pipe", "ignore"],
251
+ timeout: GIT_TIMEOUT_MS2
252
+ }).trim();
253
+ if (value === "true") return "true";
254
+ if (value === "false") return "false";
255
+ return void 0;
256
+ } catch {
257
+ return void 0;
258
+ }
259
+ }
260
+ function isRepoActive(cwd) {
261
+ return repoActiveFlag(cwd) === "true";
262
+ }
263
+ function setRepoActive(cwd, active) {
264
+ execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
265
+ cwd,
266
+ stdio: ["ignore", "ignore", "pipe"],
267
+ timeout: GIT_TIMEOUT_MS2
268
+ });
269
+ }
270
+ function isValidRepoSyncId(value) {
271
+ return typeof value === "string" && REPO_SYNC_ID_RE.test(value);
272
+ }
273
+ function localGitConfigValue(cwd, key) {
274
+ try {
275
+ const value = execFileSync2("git", ["config", "--local", "--get", key], {
276
+ cwd,
277
+ encoding: "utf-8",
278
+ stdio: ["ignore", "pipe", "ignore"],
279
+ timeout: GIT_TIMEOUT_MS2
280
+ }).trim();
281
+ return value || void 0;
282
+ } catch {
283
+ return void 0;
284
+ }
285
+ }
286
+ function setLocalGitConfigValue(cwd, key, value) {
287
+ execFileSync2("git", ["config", "--local", key, value], {
288
+ cwd,
289
+ stdio: ["ignore", "ignore", "pipe"],
290
+ timeout: GIT_TIMEOUT_MS2
291
+ });
292
+ }
293
+ function repoSyncId(cwd) {
294
+ const value = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
295
+ if (!isValidRepoSyncId(value)) return void 0;
296
+ const repositoryFullName = localGitConfigValue(cwd, PRIM_REPO_SYNC_REPOSITORY_KEY);
297
+ return repositoryFullName === githubRepositoryFullName(cwd) ? value : void 0;
298
+ }
299
+ function setRepoSyncId(cwd, value, repositoryFullName) {
300
+ if (!isValidRepoSyncId(value)) {
301
+ throw new Error("invalid repository binding returned by server");
302
+ }
303
+ execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_ID_KEY, value], {
304
+ cwd,
305
+ stdio: ["ignore", "ignore", "pipe"],
306
+ timeout: GIT_TIMEOUT_MS2
307
+ });
308
+ execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_REPOSITORY_KEY, repositoryFullName], {
309
+ cwd,
310
+ stdio: ["ignore", "ignore", "pipe"],
311
+ timeout: GIT_TIMEOUT_MS2
312
+ });
313
+ }
314
+ function setRepositoryBindingState(cwd, state) {
315
+ if (state !== "connected" && state !== "unbound" && state !== "invalid") {
316
+ throw new Error("invalid repository binding state");
317
+ }
318
+ execFileSync2("git", ["config", "--local", PRIM_REPO_BINDING_STATE_KEY, state], {
319
+ cwd,
320
+ stdio: ["ignore", "ignore", "pipe"],
321
+ timeout: GIT_TIMEOUT_MS2
322
+ });
323
+ }
324
+ function repositoryBindingState(cwd) {
325
+ const rawId = localGitConfigValue(cwd, PRIM_REPO_SYNC_ID_KEY);
326
+ const marker = localGitConfigValue(cwd, PRIM_REPO_BINDING_STATE_KEY);
327
+ if (rawId !== void 0 && !isValidRepoSyncId(rawId)) return "invalid";
328
+ if (marker === "unbound" || marker === "invalid") return marker;
329
+ if (marker === "connected") return repoSyncId(cwd) ? "connected" : "invalid";
330
+ if (marker !== void 0) return "invalid";
331
+ return rawId === void 0 ? "unbound" : repoSyncId(cwd) ? void 0 : "unbound";
332
+ }
333
+ function hasProjectPrimInstall(cwd) {
334
+ const root = gitToplevel(cwd);
335
+ if (root === null || root === homedir()) return false;
336
+ for (const rel of PROJECT_INSTALL_FILES) {
337
+ const path = join(root, rel);
338
+ if (existsSync(path)) {
339
+ try {
340
+ const content = readFileSync(path, "utf-8");
341
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
342
+ } catch {
343
+ }
344
+ }
345
+ }
346
+ return false;
347
+ }
348
+ function isRepoActiveForCapture(cwd) {
349
+ const flag = repoActiveFlag(cwd);
350
+ if (flag === "true") return true;
351
+ if (flag === "false") return false;
352
+ return hasProjectPrimInstall(cwd);
353
+ }
354
+ function decisionIngestionStatus(cwd) {
355
+ return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
356
+ }
357
+
189
358
  export {
190
359
  gitToplevel,
360
+ mainWorktreeRoot,
191
361
  currentBranch,
192
362
  githubRepositoryFullName,
193
363
  canonicalGitRoot,
364
+ relatedWorktreeTarget,
194
365
  resolveRepositoryContext,
195
- canonicalRepositoryPath
366
+ canonicalRepositoryPath,
367
+ repoActiveFlag,
368
+ isRepoActive,
369
+ setRepoActive,
370
+ isValidRepoSyncId,
371
+ localGitConfigValue,
372
+ setLocalGitConfigValue,
373
+ repoSyncId,
374
+ setRepoSyncId,
375
+ setRepositoryBindingState,
376
+ repositoryBindingState,
377
+ isRepoActiveForCapture,
378
+ decisionIngestionStatus
196
379
  };