@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 +37 -4
- package/SKILL.md +39 -10
- package/dist/{chunk-5K725K35.js → chunk-BOTKPLTI.js} +1 -1
- package/dist/{chunk-TPROV45L.js → chunk-DWDEQRWN.js} +12 -1
- package/dist/{chunk-F7O7V6ZM.js → chunk-HSZPTVKU.js} +12 -88
- package/dist/chunk-R5ZJRSLV.js +82 -0
- package/dist/{chunk-WRPKAPVE.js → chunk-VDLAAUWV.js} +1 -1
- package/dist/{chunk-CK75QBCB.js → chunk-WELBNODJ.js} +1 -1
- package/dist/daemon/server.js +46 -18
- package/dist/hooks/post-commit.js +23 -9
- package/dist/hooks/post-tool-use.js +6 -4
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +5 -3
- package/dist/hooks/prim-hook.js +7 -5
- package/dist/hooks/session-start.js +27 -5
- package/dist/index.js +131 -82
- package/dist/statusline-main.js +76 -9
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -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,
|
|
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
|
|
76
|
-
import { join as
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join as join2 } from "path";
|
|
77
5
|
|
|
78
6
|
// src/lib/bin-path.ts
|
|
79
|
-
import { existsSync
|
|
80
|
-
import { dirname, isAbsolute, join
|
|
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 =
|
|
95
|
-
if (
|
|
22
|
+
const manifestPath = join(dir, "package.json");
|
|
23
|
+
if (existsSync(manifestPath)) {
|
|
96
24
|
try {
|
|
97
|
-
const manifest = JSON.parse(
|
|
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 :
|
|
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 ||
|
|
150
|
-
return
|
|
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(
|
|
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(
|
|
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
|
+
};
|