@primitive.ai/prim 0.1.0-alpha.48 → 0.1.0-alpha.50

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.
package/README.md CHANGED
@@ -11,8 +11,8 @@ presence — from the command line and via session + git hooks.
11
11
  > **Conflict Gates** check each edit against the decision graph and surface any
12
12
  > load-bearing decision it conflicts with; their **Enforcement** tier blocks or pauses
13
13
  > a conflicting edit until you reconcile and retry. Conflict Gates are **not currently
14
- > enabled** — automatic decision capture runs regardless. To enable them for your team,
15
- > contact **support@getprimitive.ai**.
14
+ > enabled** — automatic decision capture in active repos runs regardless. To enable them
15
+ > for your team, contact **support@getprimitive.ai**.
16
16
 
17
17
  ## Installation
18
18
 
@@ -28,6 +28,12 @@ Or run directly without installing:
28
28
  npx @primitive.ai/prim
29
29
  ```
30
30
 
31
+ In commands such as `npx --yes @primitive.ai/prim@latest ...`, the first `--yes`
32
+ belongs to **npm**: it skips npm's package-install confirmation. It does not approve a
33
+ Primitive action. Prim's own global `--yes` comes after the package name, for example
34
+ `npx --yes @primitive.ai/prim@latest --yes decisions create ...`, and applies only to
35
+ that Prim invocation.
36
+
31
37
  ## Quick Start
32
38
 
33
39
  One command does the whole install — auth, session hooks, daemon, git hooks,
@@ -118,6 +124,24 @@ prim codex install # Install OpenAI Codex hooks (project scope)
118
124
  prim hermes install # Install Hermes Agent hooks (global ~/.hermes/config.yaml)
119
125
  ```
120
126
 
127
+ Passive capture is repo-scoped even when these integrations are installed at user
128
+ scope. `prim enable` marks the current Git repo active; `prim disable` makes it
129
+ inactive. A project-scoped install activates its repo automatically. In an inactive
130
+ repo, session content is not passively captured. Here “inactive” means Prim's normal
131
+ effective capture check is false, including an explicit local `prim.active=false`.
132
+
133
+ An agent may still deliberately record one Decision from an inactive repo with
134
+ `prim decisions create`, but each invocation needs its own user approval. An explicit
135
+ request such as “record this Decision” supplies that approval for that one create; for
136
+ a proactive suggestion, the agent must show the proposed Decision and ask first. The
137
+ agent then passes Prim's global `--yes` for that invocation. This one-time approval does
138
+ not activate passive capture, and neither npm's `npx --yes`, setup approval, nor an
139
+ earlier create counts as approval for another Decision.
140
+
141
+ When a person runs `prim decisions create` interactively in an inactive repo, Prim
142
+ asks for that one-time approval itself. In non-interactive use it creates nothing
143
+ unless Prim's global `--yes` is present.
144
+
121
145
  #### Claude decision feedback
122
146
 
123
147
  Claude Code receives eventual, human-visible feedback when automatic capture
@@ -199,6 +223,15 @@ prim decisions unlink <child> --on <parent> # Remove that dependency
199
223
  `<id>` accepts a full decision ID or its short ID. STDOUT is machine-readable
200
224
  JSON; human-readable status goes to STDERR.
201
225
 
226
+ When passive capture is inactive in the current repo, an approved one-time create is:
227
+
228
+ ```bash
229
+ npx --yes @primitive.ai/prim@latest --yes decisions create --intent "…"
230
+ ```
231
+
232
+ Here npm's first `--yes` only permits package resolution; Prim's second `--yes`
233
+ confirms this create. It does not enable the repo or authorize a later create.
234
+
202
235
  `link` / `unlink` curate the dependency edges the automatic linker would otherwise
203
236
  own — `<child>` depends on `<parent>`. Both are idempotent and refuse any link that
204
237
  would create a cycle (exit 2); an unresolved id exits 4.
@@ -245,8 +278,8 @@ prim welcome # Brief orientation to the decision graph (shown after se
245
278
  ### Session & journal
246
279
 
247
280
  Lower-level plumbing for the capture pipeline — org binding and the local move
248
- journal. Capture works automatically once the session hooks are installed; these
249
- are for inspecting and steering it (e.g. multi-org machines).
281
+ journal. Capture works automatically once the session hooks are installed **and the
282
+ repo is active**; these are for inspecting and steering it (e.g. multi-org machines).
250
283
 
251
284
  ```bash
252
285
  prim session start <id> # Pin a Claude Code session to an org (list / drop <id>)
package/SKILL.md CHANGED
@@ -9,9 +9,9 @@ description: Use the prim CLI for Primitive's decision graph — passive capture
9
9
 
10
10
  ## Mental model
11
11
 
12
- As your team codes, prim passively captures the **decisions** you make -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. **Conflict Gates** can check a later change against that graph and surface any load-bearing decision it conflicts with; with **Enforcement**, prim blocks the edit until you reconcile the decision and retry. Conflict Gates are **not currently enabled** — capture runs regardless. To enable them for your team, contact support@getprimitive.ai.
12
+ In an active repo, prim passively captures the **decisions** your team makes -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. **Conflict Gates** can check a later change against that graph and surface any load-bearing decision it conflicts with; with **Enforcement**, prim blocks the edit until you reconcile the decision and retry. Conflict Gates are **not currently enabled** — capture in active repos runs regardless. To enable them for your team, contact support@getprimitive.ai.
13
13
 
14
- Low-level capture runs automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code), `npx --yes @primitive.ai/prim codex install` (Codex), or `npx --yes @primitive.ai/prim hermes install` (Hermes). Deliberately record higher-order forks in the road through `prim decisions create` as described below. Your job is also to **read** the graph before load-bearing edits and **answer** the occasional rationale confirmation. (Responding to Conflict Gates applies only once Enforcement is enabled — see below.)
14
+ Low-level capture runs automatically in active repos through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code), `npx --yes @primitive.ai/prim codex install` (Codex), or `npx --yes @primitive.ai/prim hermes install` (Hermes). An inactive repo is not passively captured. Deliberately record higher-order forks in the road through `prim decisions create` as described below. Your job is also to **read** the graph before load-bearing edits and **answer** the occasional rationale confirmation. (Responding to Conflict Gates applies only once Enforcement is enabled — see below.)
15
15
 
16
16
  ## Auth
17
17
 
@@ -30,10 +30,11 @@ The CLI auto-refreshes a still-valid session from the stored refresh token (proa
30
30
  1. Every command accepts `--help`. When unsure of flags, run `npx --yes @primitive.ai/prim <cmd> --help` rather than guessing.
31
31
  2. The CLI prints API errors as one-liners to stderr and exits non-zero. Treat any non-zero exit as actionable. If auth-related, re-check `auth status`.
32
32
  3. `<idOrShortId>` arguments accept either a full decision ID or the short ID shown in feeds (and gate reasons, when Conflict Gates are enabled).
33
+ 4. In `npx --yes @primitive.ai/prim ...`, `npx --yes` is npm's flag and only skips npm's package-install confirmation. It is not user consent for a Primitive action. Prim's global flag appears after the package name: `npx --yes @primitive.ai/prim --yes ...`.
33
34
 
34
35
  ## Conflict Gates & Enforcement (not currently enabled)
35
36
 
36
- **Conflict Gates** check each edit against the decision graph before it lands and surface any load-bearing decision it conflicts with. Their **Enforcement** tier goes further -- a conflicting edit is blocked (or paused for confirmation) until you reconcile the decision and retry. **Conflict Gates are not currently enabled** — automatic decision capture (above) runs regardless. To enable Conflict Gates and Enforcement for your team, contact support@getprimitive.ai.
37
+ **Conflict Gates** check each edit against the decision graph before it lands and surface any load-bearing decision it conflicts with. Their **Enforcement** tier goes further -- a conflicting edit is blocked (or paused for confirmation) until you reconcile the decision and retry. **Conflict Gates are not currently enabled** — automatic decision capture in active repos (above) runs regardless. To enable Conflict Gates and Enforcement for your team, contact support@getprimitive.ai.
37
38
 
38
39
  When Enforcement is enabled, before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch; Hermes: write_file/patch) a PreToolUse hook scores the target file against the graph:
39
40
 
@@ -77,7 +78,7 @@ Confirmations are author-targeted and rare by design; answering keeps the graph'
77
78
 
78
79
  ## Preserve durable user decisions
79
80
 
80
- Capture is automatic for low-level choices made while coding. Use the deliberate CLI path for higher-order decisions that emerge in conversation: goals, priorities, principles, invariants, constraints, defaults, commitments, durable tradeoffs, and exceptions.
81
+ Capture is automatic for low-level choices made while coding in an active repo. Use the deliberate CLI path for higher-order decisions that emerge in conversation: goals, priorities, principles, invariants, constraints, defaults, commitments, durable tradeoffs, and exceptions.
81
82
 
82
83
  A decision worth deliberately recording is a genuine **fork in the road**: the user or agent encountered multiple plausible paths, selected one, and that selection should inform future work. Record the chosen behavior, direction, constraint, or tradeoff—not routine implementation needed to finish the task or follow an existing convention. A teammate working elsewhere should benefit from knowing it.
83
84
 
@@ -87,7 +88,7 @@ Before deliberately recording a decision, actively gather the real context behin
87
88
 
88
89
  Record only rationale supported by those sources. Do not mistake the implementation method, the task request, or a restatement of the decision for its rationale. If the rationale remains unclear or the relevant source is unavailable, omit `--rationale` rather than inventing one.
89
90
 
90
- For proactively identified decisions, let confidence in the rationale determine the interaction:
91
+ For proactively identified decisions in an active repo, let confidence in the rationale determine the interaction:
91
92
 
92
93
  - **Clear and well-supported** — record the decision and rationale silently at the natural task boundary.
93
94
  - **Plausible but uncertain** — at the task boundary, state the proposed rationale and ask for lightweight confirmation: “I understand the reason for choosing X to be Y. Is that right?” Record after confirmation or correction.
@@ -95,13 +96,35 @@ For proactively identified decisions, let confidence in the rationale determine
95
96
 
96
97
  These questions share the interruption budget below; never ask separate questions for the decision and its rationale. If both are uncertain, combine them into one concise prompt. An explicit request to “add this decision to Primitive” still records immediately with the information supplied—do not delay it to demand rationale.
97
98
 
99
+ ### Inactive repos: approve each deliberate create
100
+
101
+ When passive capture is inactive for the current repo, do not silently turn a durable
102
+ decision into a write. “Inactive” means Prim's effective repo-capture check is false;
103
+ an explicit local `prim.active=false` is always inactive. Every
104
+ `prim decisions create` invocation needs fresh user approval:
105
+
106
+ - An explicit request to record or create that Decision is approval for that one
107
+ invocation; do not ask redundantly.
108
+ - For a proactively identified Decision, present the proposed intent and supported
109
+ rationale, then wait for approval before creating it. A previous approval never
110
+ carries forward to another Decision.
111
+ - After approval, pass Prim's global `--yes` for that invocation:
112
+ `npx --yes @primitive.ai/prim --yes decisions create ...`. The first `--yes` is
113
+ npm's package-install flag; the second is Prim's one-time confirmation.
114
+ - Do not run `prim enable` merely to bypass this boundary or infer permission to
115
+ activate the repo. Activation requires a separate user request (a requested
116
+ `prim setup` counts because setup explicitly activates its current repo). A
117
+ one-time create leaves passive capture inactive; auth approval, npm's
118
+ `npx --yes`, setup's shell-permission grant, and earlier creates do not
119
+ independently change that state.
120
+
98
121
  ### Direct requests: record immediately
99
122
 
100
- When the user asks to record a decision—for example, “add this decision to Primitive”—author it directly. Do not ask for another confirmation.
123
+ When the user asks to record a decision—for example, “add this decision to Primitive”—author it directly. Do not ask for another confirmation. In an inactive repo, the request is the one-time approval: use Prim's `--yes` for that create as described above.
101
124
 
102
125
  ### Clear decisions: record without interrupting
103
126
 
104
- When the user clearly makes a durable fork-in-the-road decision without explicitly asking to record it, record it at the next natural task boundary without asking a redundant confirmation question. Preserve the user's meaning and stated rationale; do not strengthen, broaden, or embellish it. Prefer the governing position over the implementation activity that revealed it.
127
+ When the user clearly makes a durable fork-in-the-road decision without explicitly asking to record it, record it at the next natural task boundary without asking a redundant confirmation question **when passive capture is active**. In an inactive repo, present it and obtain the per-create approval above. Preserve the user's meaning and stated rationale; do not strengthen, broaden, or embellish it. Prefer the governing position over the implementation activity that revealed it.
105
128
 
106
129
  Examples that qualify:
107
130
 
@@ -140,12 +163,18 @@ Outside onboarding, before creating a proactively identified decision, read rece
140
163
  npx --yes @primitive.ai/prim decisions recent --limit 20
141
164
  ```
142
165
 
143
- If an equivalent decision is already present, do not create or suggest it again. After this duplicate check—or after the user confirms an onboarding proposal—author the decision directly:
166
+ If an equivalent decision is already present, do not create or suggest it again. After this duplicate check—or after the user confirms an onboarding proposal—author the decision directly. If the repo is inactive, that confirmation authorizes Prim's `--yes` only for the approved create:
144
167
 
145
168
  ```
146
169
  npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab over Yjs" --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge"
147
170
  ```
148
171
 
172
+ Inactive-repo form after approval:
173
+
174
+ ```
175
+ npx --yes @primitive.ai/prim --yes decisions create --intent "Adopt prosemirror-collab over Yjs" --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge"
176
+ ```
177
+
149
178
  Only `--intent` is required. Optional: `--kind` (change|exploration|task_execution|unclear, default change), `--rationale`, `--area`, `--decided`, `--alternatives` (comma-separated), `--confidence` (high|medium|low, default high), `--reversibility` (high|low, default high), and `--files` (comma-separated repo-relative paths the decision governs — these are the files Conflict Gates would check on later edits, same path form as `decisions check`; Conflict Gates are not currently enabled). Omit `--files` for broad directions that should not immediately participate in file-based Conflict Gates. STDOUT is the created identity `{ decisionId, shortId, createdAt }`; STDERR prints `[prim] created dec_<short>.` — pass that `dec_<short>` straight into `decisions show` / `cascade` / `confirm`.
150
179
 
151
180
  ### Inferred decisions: finish first, then optionally ask once
@@ -227,11 +256,11 @@ Examples:
227
256
  - **An "unavailable" / "not verified" decision check is not an all-clear.** Treat constraints as UNKNOWN and proceed deliberately; never read the silence as approval — the same holds for Conflict Gates once enabled.
228
257
  - **When Enforcement is enabled, a `deny` means a real prior decision conflicts.** Reconcile only when you genuinely intend to override it; otherwise pick an approach that respects it.
229
258
  - **Reconcile bypasses are single-use and short-lived.** One bypass clears your *next* edit to the governed file; it is not a standing override.
230
- - **Capture of coding activity is automatic; never inject moves by hand.** If decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status` / `hermes status`) and the daemon is running. Deliberately authoring a higher-order fork—either at the user's request or under the conservative proactive policy above—is a separate, supported path through `decisions create`.
259
+ - **Capture of coding activity is automatic only in active repos; never inject moves by hand.** If an active repo's decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status` / `hermes status`) and the daemon is running. Deliberately authoring a higher-order fork—either at the user's request or under the conservative proactive policy above—is a separate, supported path through `decisions create`; in an inactive repo, honor the per-invocation approval boundary above.
231
260
  - **Don't fabricate rationale on a confirmation.** If you don't know why a decision was made, say so rather than guessing.
232
261
 
233
262
  ## After each task
234
263
 
235
- If the user clearly made a durable fork-in-the-road decision, check recent decisions and record it now if it is not already present. If a strong higher-order decision was only inferred, decide whether it clears the interruption budget; if so, ask the single concise confirmation question after reporting the completed work. Otherwise say nothing about Primitive.
264
+ If the user clearly made a durable fork-in-the-road decision, check recent decisions and record it now if it is not already present when capture is active. When capture is inactive, record it only if the user explicitly requested that create; otherwise present it for the per-invocation approval above. If a strong higher-order decision was only inferred, decide whether it clears the interruption budget; if so, ask the single concise confirmation question after reporting the completed work. Otherwise say nothing about Primitive.
236
265
 
237
266
  If Conflict Gates are enabled and one denied or warned you, report which decision(s) it named and whether you reconciled. If you read the graph before a load-bearing change, note what you found so the user can verify in the dashboard.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-TPROV45L.js";
3
+ } from "./chunk-DWDEQRWN.js";
4
4
 
5
5
  // src/journal.ts
6
6
  import {
@@ -154,6 +154,7 @@ var CONFIG_DIR_MODE = 448;
154
154
  var CREDENTIAL_FILE_MODE = 384;
155
155
  var REFRESH_THRESHOLD_MS = 6e4;
156
156
  var DEFAULT_API_URL = "https://api.getprimitive.ai";
157
+ var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
157
158
  function loadEnvFile() {
158
159
  const envVars = {};
159
160
  for (const file of [".env.local", ".env"]) {
@@ -349,6 +350,7 @@ async function performTokenRefresh(options = {}) {
349
350
  if (selected?.source !== "token_file") return void 0;
350
351
  const startingGeneration = readTrimmed(REFRESH_TOKEN_PATH);
351
352
  if (!startingGeneration || isSessionEnded()) return void 0;
353
+ const startingAccessToken = selected.token;
352
354
  return withCredentialLock(
353
355
  async () => {
354
356
  const currentCredential = resolveAuthCredential();
@@ -358,6 +360,9 @@ async function performTokenRefresh(options = {}) {
358
360
  if (currentGeneration !== startingGeneration) {
359
361
  return currentCredential.token;
360
362
  }
363
+ if (currentCredential.token !== startingAccessToken) {
364
+ return currentCredential.token;
365
+ }
361
366
  if (isSessionEnded()) return void 0;
362
367
  const response = await fetch(`${getSiteUrl()}/mcp/broker/refresh`, {
363
368
  method: "POST",
@@ -442,6 +447,12 @@ async function request(method, path, body, options) {
442
447
  });
443
448
  if (token) credential = { token, source: "token_file" };
444
449
  }
450
+ if (!credential) {
451
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
452
+ }
453
+ if (isTokenExpiringSoon(credential) && isSessionEnded()) {
454
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
455
+ }
445
456
  const doFetch = (token) => {
446
457
  const headers = { "Content-Type": "application/json" };
447
458
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -471,7 +482,7 @@ async function request(method, path, body, options) {
471
482
  }
472
483
  if (!response.ok) {
473
484
  if (response.status === 401) {
474
- throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
485
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
475
486
  }
476
487
  const errorBody = await response.json().catch(() => null);
477
488
  throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
@@ -1,83 +1,11 @@
1
- // src/lib/activation.ts
2
- import { execFileSync as execFileSync2 } from "child_process";
3
- import { existsSync, readFileSync } from "fs";
4
- import { homedir } from "os";
5
- import { join } from "path";
6
-
7
- // src/lib/git.ts
8
- import { execFileSync } from "child_process";
9
- function gitToplevel(cwd) {
10
- try {
11
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
12
- cwd,
13
- encoding: "utf-8",
14
- stdio: ["ignore", "pipe", "ignore"]
15
- }).trim();
16
- } catch {
17
- return null;
18
- }
19
- }
20
-
21
- // src/lib/activation.ts
22
- var PRIM_ACTIVE_KEY = "prim.active";
23
- var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
24
- var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
25
- function repoActiveFlag(cwd) {
26
- try {
27
- const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
28
- cwd,
29
- encoding: "utf-8",
30
- stdio: ["ignore", "pipe", "ignore"]
31
- }).trim();
32
- if (value === "true") return "true";
33
- if (value === "false") return "false";
34
- return void 0;
35
- } catch {
36
- return void 0;
37
- }
38
- }
39
- function setRepoActive(cwd, active) {
40
- execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
41
- cwd,
42
- stdio: ["ignore", "ignore", "pipe"]
43
- });
44
- }
45
- function activateRepoBestEffort(cwd) {
46
- try {
47
- setRepoActive(cwd, true);
48
- } catch {
49
- }
50
- }
51
- function hasProjectPrimInstall(cwd) {
52
- const root = gitToplevel(cwd);
53
- if (root === null || root === homedir()) return false;
54
- for (const rel of PROJECT_INSTALL_FILES) {
55
- const path = join(root, rel);
56
- if (existsSync(path)) {
57
- try {
58
- const content = readFileSync(path, "utf-8");
59
- if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
60
- } catch {
61
- }
62
- }
63
- }
64
- return false;
65
- }
66
- function isRepoActiveForCapture(cwd) {
67
- const flag = repoActiveFlag(cwd);
68
- if (flag === "true") return true;
69
- if (flag === "false") return false;
70
- return hasProjectPrimInstall(cwd);
71
- }
72
-
73
1
  // src/lib/bin-cache.ts
74
2
  import { chmodSync, mkdirSync, renameSync, writeFileSync } from "fs";
75
- import { homedir as homedir2 } from "os";
76
- import { join as join3 } from "path";
3
+ import { homedir } from "os";
4
+ import { join as join2 } from "path";
77
5
 
78
6
  // src/lib/bin-path.ts
79
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
80
- import { dirname, isAbsolute, join as join2 } from "path";
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { dirname, isAbsolute, join } from "path";
81
9
  import { fileURLToPath } from "url";
82
10
  var PKG_NAME = "@primitive.ai/prim";
83
11
  var ROOT_WALK_LIMIT = 6;
@@ -91,10 +19,10 @@ function locateRoot() {
91
19
  }
92
20
  let dir = dirname(fileURLToPath(import.meta.url));
93
21
  for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
94
- const manifestPath = join2(dir, "package.json");
95
- if (existsSync2(manifestPath)) {
22
+ const manifestPath = join(dir, "package.json");
23
+ if (existsSync(manifestPath)) {
96
24
  try {
97
- const manifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
25
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
98
26
  if (manifest.name === PKG_NAME && manifest.bin) {
99
27
  resolvedRoot = { dir, bin: manifest.bin };
100
28
  return resolvedRoot;
@@ -117,7 +45,7 @@ function binFile(bin) {
117
45
  if (!root || !rel) {
118
46
  return null;
119
47
  }
120
- return isAbsolute(rel) ? rel : join2(root.dir, rel);
48
+ return isAbsolute(rel) ? rel : join(root.dir, rel);
121
49
  }
122
50
  function hookShimCommand(bin, args = "", opts = {}) {
123
51
  const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
@@ -146,8 +74,8 @@ function commandMatchesBin(command, bin) {
146
74
  // src/lib/bin-cache.ts
147
75
  var CACHED_BINS = ["prim", "prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
148
76
  function binCacheDir() {
149
- const base = process.env.XDG_CACHE_HOME || join3(homedir2(), ".cache");
150
- return join3(base, "prim", "bin");
77
+ const base = process.env.XDG_CACHE_HOME || join2(homedir(), ".cache");
78
+ return join2(base, "prim", "bin");
151
79
  }
152
80
  function writeAtomic(path, content) {
153
81
  const tmp = `${path}.${process.pid}.tmp`;
@@ -165,11 +93,11 @@ function warmBinCache() {
165
93
  const dir = binCacheDir();
166
94
  mkdirSync(dir, { recursive: true, mode: 448 });
167
95
  chmodSync(dir, 448);
168
- writeAtomic(join3(dir, "node"), process.execPath);
96
+ writeAtomic(join2(dir, "node"), process.execPath);
169
97
  for (const bin of CACHED_BINS) {
170
98
  const file = binFile(bin);
171
99
  if (file) {
172
- writeAtomic(join3(dir, bin), file);
100
+ writeAtomic(join2(dir, bin), file);
173
101
  }
174
102
  }
175
103
  } catch {
@@ -177,10 +105,6 @@ function warmBinCache() {
177
105
  }
178
106
 
179
107
  export {
180
- gitToplevel,
181
- setRepoActive,
182
- activateRepoBestEffort,
183
- isRepoActiveForCapture,
184
108
  binFile,
185
109
  hookShimCommand,
186
110
  detachedHookShimCommand,
@@ -0,0 +1,82 @@
1
+ // src/lib/activation.ts
2
+ import { execFileSync as execFileSync2 } from "child_process";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+
7
+ // src/lib/git.ts
8
+ import { execFileSync } from "child_process";
9
+ function gitToplevel(cwd) {
10
+ try {
11
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
12
+ cwd,
13
+ encoding: "utf-8",
14
+ stdio: ["ignore", "pipe", "ignore"]
15
+ }).trim();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ // src/lib/activation.ts
22
+ var PRIM_ACTIVE_KEY = "prim.active";
23
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
24
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
25
+ function repoActiveFlag(cwd) {
26
+ try {
27
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
28
+ cwd,
29
+ encoding: "utf-8",
30
+ stdio: ["ignore", "pipe", "ignore"]
31
+ }).trim();
32
+ if (value === "true") return "true";
33
+ if (value === "false") return "false";
34
+ return void 0;
35
+ } catch {
36
+ return void 0;
37
+ }
38
+ }
39
+ function setRepoActive(cwd, active) {
40
+ execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
41
+ cwd,
42
+ stdio: ["ignore", "ignore", "pipe"]
43
+ });
44
+ }
45
+ function activateRepoBestEffort(cwd) {
46
+ try {
47
+ setRepoActive(cwd, true);
48
+ } catch {
49
+ }
50
+ }
51
+ function hasProjectPrimInstall(cwd) {
52
+ const root = gitToplevel(cwd);
53
+ if (root === null || root === homedir()) return false;
54
+ for (const rel of PROJECT_INSTALL_FILES) {
55
+ const path = join(root, rel);
56
+ if (existsSync(path)) {
57
+ try {
58
+ const content = readFileSync(path, "utf-8");
59
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
60
+ } catch {
61
+ }
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ function isRepoActiveForCapture(cwd) {
67
+ const flag = repoActiveFlag(cwd);
68
+ if (flag === "true") return true;
69
+ if (flag === "false") return false;
70
+ return hasProjectPrimInstall(cwd);
71
+ }
72
+ function decisionIngestionStatus(cwd) {
73
+ return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
74
+ }
75
+
76
+ export {
77
+ gitToplevel,
78
+ setRepoActive,
79
+ activateRepoBestEffort,
80
+ isRepoActiveForCapture,
81
+ decisionIngestionStatus
82
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getClient,
3
3
  getSiteUrl
4
- } from "./chunk-TPROV45L.js";
4
+ } from "./chunk-DWDEQRWN.js";
5
5
  import {
6
6
  daemonRequest
7
7
  } from "./chunk-UTKQTZHL.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-TPROV45L.js";
3
+ } from "./chunk-DWDEQRWN.js";
4
4
 
5
5
  // src/decisions/feedback.ts
6
6
  var FEEDBACK_PROTOCOL_VERSION = 1;