@primitive.ai/prim 0.1.0-alpha.47 → 0.1.0-alpha.49
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 +44 -13
- package/dist/{chunk-F7O7V6ZM.js → chunk-HSZPTVKU.js} +12 -88
- package/dist/chunk-R5ZJRSLV.js +82 -0
- package/dist/hooks/post-commit.js +21 -7
- package/dist/hooks/post-tool-use.js +4 -2
- package/dist/hooks/pre-tool-use.js +4 -2
- package/dist/hooks/prim-hook.js +4 -2
- package/dist/hooks/session-start.js +4 -2
- package/dist/index.js +75 -46
- package/dist/statusline-main.js +73 -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
|
|
|
@@ -49,7 +50,9 @@ When enabled, the gate fail-opens on its *own* infrastructure errors (no daemon,
|
|
|
49
50
|
- `npx --yes @primitive.ai/prim decisions recent` -- the team's recent decisions, each row badged by author and agent (`Your Claude Code` / `Your Codex` / `Your Hermes`); `--limit <n>` and `--since <dur>` narrow it. `--author "<name>"` filters to one teammate (feed name, `"First Last"`, last name, username, email, or email local-part) -- the way to answer "what has X decided?"; an unknown or ambiguous name comes back as `unavailable` with the reason, and `authorHasDecisions` in the JSON distinguishes "no feed-visible decisions" (false) from "has decisions, none in this window" (true). The page defaults to the 10 most recent, so it can hide older ones: on an author query the JSON's `windowTotal` is how many that teammate has in the window. When `windowTotal` exceeds the rows returned, don't present the page as complete -- tell the user you're showing the most recent N of `windowTotal` and offer to pull the rest, which is a re-run with `--limit <windowTotal>` (capped at 100; `windowTotalCapped` means the count is a floor rendered `N+`, and a window past 100 can't be fetched whole).
|
|
50
51
|
- `npx --yes @primitive.ai/prim decisions show <idOrShortId>` and `npx --yes @primitive.ai/prim decisions cascade <idOrShortId>` -- full detail, and the downstream blast radius a change would disturb.
|
|
51
52
|
|
|
52
|
-
Before presenting decision reads, use the current conversation, task, and available memory to understand what matters to the requester. Do not dump the API's chronological rows unchanged. Group related decisions around goals or workstreams, lead with decisions that affect the requester's current work, and surface conflicts, supersessions, or consequential tradeoffs before background activity. Explain why an item is relevant when the connection is supported; do not invent relevance or rationale. Preserve the response's `unavailable`, truncation, and count semantics when reshaping it.
|
|
53
|
+
Before presenting decision reads, use the current conversation, task, and available memory to understand what matters to the requester and how they want it delivered. Do not dump the API's chronological rows unchanged. Group related decisions around goals or workstreams, lead with decisions that affect the requester's current work, and surface conflicts, supersessions, or consequential tradeoffs before background activity. Explain why an item is relevant when the connection is supported; do not invent relevance or rationale. Preserve the response's `unavailable`, truncation, and count semantics when reshaping it.
|
|
54
|
+
|
|
55
|
+
Then tailor the delivery to this requester, not a generic reader. Take your cues from the signals actually present — durable preferences in memory, the format and depth they've used or asked for earlier in the conversation, and the shape of their current goal — and match the form (a one-line answer, a table, grouped headers, or prose), the depth (a bare intent versus full rationale, alternatives, and tradeoffs), the altitude (strategic direction versus specific ids and files), the fields they actually track, and the register of the exchange. Where a preference isn't evidenced, default to a concise, skimmable summary — read taste from signal the way you read rationale, never conjuring a persona from thin air. Tailoring changes how the facts land, never which facts: keep every conflict, supersession, and consequential tradeoff in view even when the requester wants it terse, and never let style override the `unavailable`, truncation, and count semantics above.
|
|
53
56
|
|
|
54
57
|
## Reconcile and the verdict footer
|
|
55
58
|
|
|
@@ -75,17 +78,17 @@ Confirmations are author-targeted and rare by design; answering keeps the graph'
|
|
|
75
78
|
|
|
76
79
|
## Preserve durable user decisions
|
|
77
80
|
|
|
78
|
-
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.
|
|
79
82
|
|
|
80
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.
|
|
81
84
|
|
|
82
|
-
### Ground the rationale in
|
|
85
|
+
### Ground the rationale in real sources
|
|
83
86
|
|
|
84
|
-
Before deliberately recording a decision,
|
|
87
|
+
Before deliberately recording a decision, actively gather the real context behind **why this path was chosen** — aim to populate the decision with as much genuine, source-grounded rationale as you can find, never a plausible-sounding guess. Start with the current conversation, then reach for every tool, connector, and MCP server available to you to pull from the actual sources the decision or task points to: Slack threads, Granola or other meeting notes, Linear issues, Zoom transcripts, email, and repository docs or skills. Don't limit yourself to that list — use whatever integrations you have. Read the source directly instead of inferring from memory, and target the specific thread, meeting, ticket, or document the user referenced rather than a broad, scattershot search.
|
|
85
88
|
|
|
86
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.
|
|
87
90
|
|
|
88
|
-
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:
|
|
89
92
|
|
|
90
93
|
- **Clear and well-supported** — record the decision and rationale silently at the natural task boundary.
|
|
91
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.
|
|
@@ -93,13 +96,35 @@ For proactively identified decisions, let confidence in the rationale determine
|
|
|
93
96
|
|
|
94
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.
|
|
95
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
|
+
|
|
96
121
|
### Direct requests: record immediately
|
|
97
122
|
|
|
98
|
-
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.
|
|
99
124
|
|
|
100
125
|
### Clear decisions: record without interrupting
|
|
101
126
|
|
|
102
|
-
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.
|
|
103
128
|
|
|
104
129
|
Examples that qualify:
|
|
105
130
|
|
|
@@ -138,12 +163,18 @@ Outside onboarding, before creating a proactively identified decision, read rece
|
|
|
138
163
|
npx --yes @primitive.ai/prim decisions recent --limit 20
|
|
139
164
|
```
|
|
140
165
|
|
|
141
|
-
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:
|
|
142
167
|
|
|
143
168
|
```
|
|
144
169
|
npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab over Yjs" --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge"
|
|
145
170
|
```
|
|
146
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
|
+
|
|
147
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`.
|
|
148
179
|
|
|
149
180
|
### Inferred decisions: finish first, then optionally ask once
|
|
@@ -225,11 +256,11 @@ Examples:
|
|
|
225
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.
|
|
226
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.
|
|
227
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.
|
|
228
|
-
- **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.
|
|
229
260
|
- **Don't fabricate rationale on a confirmation.** If you don't know why a decision was made, say so rather than guessing.
|
|
230
261
|
|
|
231
262
|
## After each task
|
|
232
263
|
|
|
233
|
-
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.
|
|
234
265
|
|
|
235
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,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
|
+
};
|
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
appendMove,
|
|
7
7
|
resolveOrg
|
|
8
8
|
} from "../chunk-5K725K35.js";
|
|
9
|
+
import {
|
|
10
|
+
isRepoActiveForCapture
|
|
11
|
+
} from "../chunk-R5ZJRSLV.js";
|
|
9
12
|
import "../chunk-TPROV45L.js";
|
|
10
13
|
|
|
11
14
|
// src/hooks/post-commit.ts
|
|
@@ -53,20 +56,31 @@ function spawnBackgroundFlush() {
|
|
|
53
56
|
stdio: "ignore"
|
|
54
57
|
}).unref();
|
|
55
58
|
}
|
|
56
|
-
|
|
59
|
+
function runPostCommit() {
|
|
60
|
+
const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
|
|
61
|
+
if (!isRepoActiveForCapture(cwd)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
57
64
|
const commit = readCommit();
|
|
58
65
|
if (commit) {
|
|
59
|
-
const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
|
|
60
66
|
const move = toCommitMove(commit, resolveCliVersion(), cwd);
|
|
61
67
|
const { orgId } = resolveOrg({ sessionId: "", cwd });
|
|
62
68
|
appendMove(move, orgId);
|
|
63
69
|
spawnBackgroundFlush();
|
|
64
70
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
}
|
|
72
|
+
if (!process.env.VITEST) {
|
|
73
|
+
try {
|
|
74
|
+
runPostCommit();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (process.env.PRIM_HOOK_DEBUG) {
|
|
77
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
78
|
+
process.stderr.write(`[prim-post-commit] capture failed: ${detail}
|
|
69
79
|
`);
|
|
80
|
+
}
|
|
70
81
|
}
|
|
82
|
+
process.exit(0);
|
|
71
83
|
}
|
|
72
|
-
|
|
84
|
+
export {
|
|
85
|
+
runPostCommit
|
|
86
|
+
};
|
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
getOrCreateWorkspaceId
|
|
19
19
|
} from "../chunk-IMAIBPUC.js";
|
|
20
20
|
import {
|
|
21
|
-
isRepoActiveForCapture,
|
|
22
21
|
warmBinCache
|
|
23
|
-
} from "../chunk-
|
|
22
|
+
} from "../chunk-HSZPTVKU.js";
|
|
23
|
+
import {
|
|
24
|
+
isRepoActiveForCapture
|
|
25
|
+
} from "../chunk-R5ZJRSLV.js";
|
|
24
26
|
import {
|
|
25
27
|
getClient
|
|
26
28
|
} from "../chunk-TPROV45L.js";
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
getOrCreateWorkspaceId
|
|
25
25
|
} from "../chunk-IMAIBPUC.js";
|
|
26
26
|
import {
|
|
27
|
-
isRepoActiveForCapture,
|
|
28
27
|
warmBinCache
|
|
29
|
-
} from "../chunk-
|
|
28
|
+
} from "../chunk-HSZPTVKU.js";
|
|
29
|
+
import {
|
|
30
|
+
isRepoActiveForCapture
|
|
31
|
+
} from "../chunk-R5ZJRSLV.js";
|
|
30
32
|
import "../chunk-TPROV45L.js";
|
|
31
33
|
import {
|
|
32
34
|
normalizeEnvelope,
|
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
} from "../chunk-IMAIBPUC.js";
|
|
15
15
|
import {
|
|
16
16
|
binFile,
|
|
17
|
-
isRepoActiveForCapture,
|
|
18
17
|
warmBinCache
|
|
19
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-HSZPTVKU.js";
|
|
19
|
+
import {
|
|
20
|
+
isRepoActiveForCapture
|
|
21
|
+
} from "../chunk-R5ZJRSLV.js";
|
|
20
22
|
import {
|
|
21
23
|
getSiteUrl
|
|
22
24
|
} from "../chunk-TPROV45L.js";
|
package/dist/index.js
CHANGED
|
@@ -29,15 +29,19 @@ import {
|
|
|
29
29
|
inspectWorkspaceId
|
|
30
30
|
} from "./chunk-IMAIBPUC.js";
|
|
31
31
|
import {
|
|
32
|
-
activateRepoBestEffort,
|
|
33
32
|
binFile,
|
|
34
33
|
commandMatchesBin,
|
|
35
34
|
detachedHookShimCommand,
|
|
36
|
-
gitToplevel,
|
|
37
35
|
hookShimCommand,
|
|
38
|
-
setRepoActive,
|
|
39
36
|
warmBinCache
|
|
40
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-HSZPTVKU.js";
|
|
38
|
+
import {
|
|
39
|
+
activateRepoBestEffort,
|
|
40
|
+
decisionIngestionStatus,
|
|
41
|
+
gitToplevel,
|
|
42
|
+
isRepoActiveForCapture,
|
|
43
|
+
setRepoActive
|
|
44
|
+
} from "./chunk-R5ZJRSLV.js";
|
|
41
45
|
import {
|
|
42
46
|
HttpError,
|
|
43
47
|
REFRESH_TOKEN_PATH,
|
|
@@ -1703,6 +1707,12 @@ var HEALTHY_POLL_MS = 250;
|
|
|
1703
1707
|
var EXIT_OK2 = 0;
|
|
1704
1708
|
var EXIT_NOT_RUNNING = 2;
|
|
1705
1709
|
var EXIT_BOOTING = 3;
|
|
1710
|
+
function formatDaemonLifecycleMessage(message, decisionIngestion) {
|
|
1711
|
+
return `${message} \xB7 Decision ingestion ${decisionIngestion}`;
|
|
1712
|
+
}
|
|
1713
|
+
function formatCurrentDaemonLifecycleMessage(message) {
|
|
1714
|
+
return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()));
|
|
1715
|
+
}
|
|
1706
1716
|
var MAX_HEALTH_ERROR_LENGTH = 240;
|
|
1707
1717
|
function boundedHealthError(value) {
|
|
1708
1718
|
if (!value) return void 0;
|
|
@@ -1819,8 +1829,10 @@ async function detachedDaemonStart(opts) {
|
|
|
1819
1829
|
if (existing?.alive) {
|
|
1820
1830
|
const snapshot = await verifiedPid(existing);
|
|
1821
1831
|
if (snapshot) {
|
|
1822
|
-
process.stderr.write(
|
|
1823
|
-
`)
|
|
1832
|
+
process.stderr.write(
|
|
1833
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] daemon already running (pid=${existing.pid})`)}
|
|
1834
|
+
`
|
|
1835
|
+
);
|
|
1824
1836
|
console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
|
|
1825
1837
|
return;
|
|
1826
1838
|
}
|
|
@@ -1876,7 +1888,7 @@ async function detachedDaemonStart(opts) {
|
|
|
1876
1888
|
if (live) {
|
|
1877
1889
|
const after = readPidfile();
|
|
1878
1890
|
process.stderr.write(
|
|
1879
|
-
`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})
|
|
1891
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon started (pid=${after?.pid ?? "?"}, socket=${SOCK_PATH})`)}
|
|
1880
1892
|
`
|
|
1881
1893
|
);
|
|
1882
1894
|
console.log(JSON.stringify({ started: true, pid: after?.pid }, null, 2));
|
|
@@ -1966,7 +1978,7 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
1966
1978
|
if (healthy) {
|
|
1967
1979
|
const verb = result.action === "none" ? "already running" : "started";
|
|
1968
1980
|
process.stderr.write(
|
|
1969
|
-
`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? result.service.pid ?? "?"})
|
|
1981
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? result.service.pid ?? "?"})`)}
|
|
1970
1982
|
`
|
|
1971
1983
|
);
|
|
1972
1984
|
} else {
|
|
@@ -2140,28 +2152,23 @@ function classifyLaunchdStatus(service, responding, snapshot, disabled) {
|
|
|
2140
2152
|
exitCode: EXIT_OK2
|
|
2141
2153
|
};
|
|
2142
2154
|
}
|
|
2143
|
-
function
|
|
2155
|
+
function formatDaemonSnapshotMessage(snapshot, supervised, decisionIngestion) {
|
|
2144
2156
|
if (!snapshot) {
|
|
2145
|
-
|
|
2146
|
-
supervised ? "[prim] \u2713 daemon live under launchd (no snapshot)\n" : "[prim] \u2713 daemon live\n"
|
|
2147
|
-
);
|
|
2148
|
-
return;
|
|
2157
|
+
return supervised ? `[prim] \u2713 daemon live, Decision ingestion ${decisionIngestion} under launchd (no snapshot)` : `[prim] \u2713 daemon live, Decision ingestion ${decisionIngestion}`;
|
|
2149
2158
|
}
|
|
2150
2159
|
const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
|
|
2151
2160
|
if (snapshot.healthy === false) {
|
|
2152
2161
|
const reason = daemonDegradedReason(snapshot);
|
|
2153
|
-
|
|
2154
|
-
`[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}${reason ? ` \xB7 ${reason}` : ""}
|
|
2155
|
-
`
|
|
2156
|
-
);
|
|
2157
|
-
return;
|
|
2162
|
+
return `[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}${reason ? ` \xB7 ${reason}` : ""}`;
|
|
2158
2163
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
);
|
|
2164
|
+
return `[prim] \u2713 daemon live, Decision ingestion ${decisionIngestion}${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(
|
|
2165
|
+
snapshot.uptimeMs / 1e3
|
|
2166
|
+
)}s \xB7 session=${snapshot.sessionId}${team}`;
|
|
2167
|
+
}
|
|
2168
|
+
function writeLiveSnapshot(snapshot, supervised = false) {
|
|
2169
|
+
const decisionIngestion = snapshot?.healthy === false ? "disabled" : decisionIngestionStatus(process.cwd());
|
|
2170
|
+
process.stderr.write(`${formatDaemonSnapshotMessage(snapshot, supervised, decisionIngestion)}
|
|
2171
|
+
`);
|
|
2165
2172
|
}
|
|
2166
2173
|
async function detachedDaemonStatus() {
|
|
2167
2174
|
const pid = readPidfile();
|
|
@@ -2262,8 +2269,10 @@ async function daemonEnsure() {
|
|
|
2262
2269
|
if (result.state === "disabled") {
|
|
2263
2270
|
process.stderr.write("[prim] daemon remains explicitly disabled\n");
|
|
2264
2271
|
} else if (result.state === "running") {
|
|
2265
|
-
process.stderr.write(
|
|
2266
|
-
`)
|
|
2272
|
+
process.stderr.write(
|
|
2273
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ensured under launchd (${result.action})`)}
|
|
2274
|
+
`
|
|
2275
|
+
);
|
|
2267
2276
|
} else {
|
|
2268
2277
|
process.stderr.write(`[prim] \u2717 daemon ensure failed; see ${LOG_PATH}
|
|
2269
2278
|
`);
|
|
@@ -2999,9 +3008,29 @@ function formatShowJson(result) {
|
|
|
2999
3008
|
return JSON.stringify(result, null, 2);
|
|
3000
3009
|
}
|
|
3001
3010
|
|
|
3011
|
+
// src/lib/confirmation.ts
|
|
3012
|
+
function isNonInteractive(globals) {
|
|
3013
|
+
return Boolean(globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE);
|
|
3014
|
+
}
|
|
3015
|
+
async function askConfirmation(question, output = process.stdout) {
|
|
3016
|
+
if (!process.stdin.isTTY) return false;
|
|
3017
|
+
const { createInterface: createInterface2 } = await import("readline/promises");
|
|
3018
|
+
const rl = createInterface2({ input: process.stdin, output });
|
|
3019
|
+
try {
|
|
3020
|
+
const answer = await rl.question(`${question} [y/N] `);
|
|
3021
|
+
const normalized = answer.trim().toLowerCase();
|
|
3022
|
+
return normalized === "y" || normalized === "yes";
|
|
3023
|
+
} finally {
|
|
3024
|
+
rl.close();
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3002
3028
|
// src/commands/decisions.ts
|
|
3003
3029
|
var EXIT_NOT_FOUND = 4;
|
|
3004
3030
|
var EXIT_USAGE = 2;
|
|
3031
|
+
var CREATE_INACTIVE_PROMPT = "[prim] Decision ingestion is disabled here. Create this one Decision without enabling passive ingestion?";
|
|
3032
|
+
var CREATE_INACTIVE_APPROVED = "[prim] one-time Decision creation approved; passive ingestion remains disabled here";
|
|
3033
|
+
var CREATE_INACTIVE_REJECTED = "[prim] decision not created: Decision ingestion is disabled here; rerun with prim's --yes to approve this one Decision, or run `prim enable` in a Git project";
|
|
3005
3034
|
var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
3006
3035
|
function registerDecisionsCommands(program2) {
|
|
3007
3036
|
const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
|
|
@@ -3081,7 +3110,19 @@ function registerDecisionsCommands(program2) {
|
|
|
3081
3110
|
).option("--decided <items>", "Comma-separated option(s) chosen").option("--alternatives <items>", "Comma-separated options considered but rejected").option("--confidence <level>", "high | medium | low (default high)").option("--reversibility <level>", "high | low (default high)").option(
|
|
3082
3111
|
"--files <paths>",
|
|
3083
3112
|
"Comma-separated repo-relative paths this decision governs (the files Conflict Gates would check \u2014 not currently enabled)"
|
|
3084
|
-
).action(async (opts) => {
|
|
3113
|
+
).action(async (opts, command) => {
|
|
3114
|
+
if (!isRepoActiveForCapture(process.cwd())) {
|
|
3115
|
+
const globals = command.optsWithGlobals();
|
|
3116
|
+
const nonInteractive = isNonInteractive(globals);
|
|
3117
|
+
const approved = Boolean(globals.yes) || !nonInteractive && await askConfirmation(CREATE_INACTIVE_PROMPT, process.stderr);
|
|
3118
|
+
if (!approved) {
|
|
3119
|
+
console.error(CREATE_INACTIVE_REJECTED);
|
|
3120
|
+
console.log(JSON.stringify({ ok: false, error: "prim_inactive" }, null, 2));
|
|
3121
|
+
process.exitCode = EXIT_USAGE;
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
console.error(CREATE_INACTIVE_APPROVED);
|
|
3125
|
+
}
|
|
3085
3126
|
const request = {
|
|
3086
3127
|
intent: opts.intent,
|
|
3087
3128
|
kind: opts.kind,
|
|
@@ -3243,10 +3284,11 @@ function classifyDaemonHealth(snapshot, options = {}) {
|
|
|
3243
3284
|
if (snapshot.healthy !== true) {
|
|
3244
3285
|
return { name: "daemon", status: "fail", detail: "health state is not ready" };
|
|
3245
3286
|
}
|
|
3287
|
+
const ingestionStatus = options.ingestionStatus ?? decisionIngestionStatus(process.cwd());
|
|
3246
3288
|
return {
|
|
3247
3289
|
name: "daemon",
|
|
3248
3290
|
status: "ok",
|
|
3249
|
-
detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""}`
|
|
3291
|
+
detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""} \xB7 Decision ingestion ${ingestionStatus}`
|
|
3250
3292
|
};
|
|
3251
3293
|
}
|
|
3252
3294
|
async function checkDaemon() {
|
|
@@ -3925,18 +3967,6 @@ function detectHusky(gitRoot) {
|
|
|
3925
3967
|
function containsPrimHook(content, binName = PRE_COMMIT.binName) {
|
|
3926
3968
|
return content.includes(binName);
|
|
3927
3969
|
}
|
|
3928
|
-
async function askConfirmation(question) {
|
|
3929
|
-
if (!process.stdin.isTTY) return false;
|
|
3930
|
-
const { createInterface: createInterface2 } = await import("readline/promises");
|
|
3931
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3932
|
-
try {
|
|
3933
|
-
const answer = await rl.question(`${question} [y/N] `);
|
|
3934
|
-
const normalized = answer.trim().toLowerCase();
|
|
3935
|
-
return normalized === "y" || normalized === "yes";
|
|
3936
|
-
} finally {
|
|
3937
|
-
rl.close();
|
|
3938
|
-
}
|
|
3939
|
-
}
|
|
3940
3970
|
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
3941
3971
|
const hookPath = resolve2(gitRoot, ".husky", spec.hookName);
|
|
3942
3972
|
const existed = existsSync7(hookPath);
|
|
@@ -4154,9 +4184,7 @@ function registerHooksCommands(program2) {
|
|
|
4154
4184
|
return;
|
|
4155
4185
|
}
|
|
4156
4186
|
const globals = command.optsWithGlobals();
|
|
4157
|
-
const nonInteractive =
|
|
4158
|
-
globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
|
|
4159
|
-
);
|
|
4187
|
+
const nonInteractive = isNonInteractive(globals);
|
|
4160
4188
|
const gitRoot = getGitRoot();
|
|
4161
4189
|
if (opts.target === "husky") return installHooks(gitRoot, "husky");
|
|
4162
4190
|
if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
|
|
@@ -5179,11 +5207,12 @@ async function renderStatusline() {
|
|
|
5179
5207
|
}
|
|
5180
5208
|
return `primitive ${version} (daemon: starting)`;
|
|
5181
5209
|
}
|
|
5210
|
+
const ingestionStatus = decisionIngestionStatus(process.cwd());
|
|
5182
5211
|
if (snapshot.envMismatch) {
|
|
5183
|
-
return `primitive ${version} (daemon: live \xB7 presence: other env)`;
|
|
5212
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
|
|
5184
5213
|
}
|
|
5185
5214
|
if (snapshot.presenceStale) {
|
|
5186
|
-
return `primitive ${version} (daemon: live \xB7 presence: stale)`;
|
|
5215
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
|
|
5187
5216
|
}
|
|
5188
5217
|
let team;
|
|
5189
5218
|
if (snapshot.onlineTeammates !== void 0) {
|
|
@@ -5195,7 +5224,7 @@ async function renderStatusline() {
|
|
|
5195
5224
|
} else {
|
|
5196
5225
|
team = "team: \u2014";
|
|
5197
5226
|
}
|
|
5198
|
-
return `primitive ${version} (daemon: live \xB7 ${team})`;
|
|
5227
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
|
|
5199
5228
|
}
|
|
5200
5229
|
function registerStatuslineCommands(program2) {
|
|
5201
5230
|
program2.command("statusline").description("Render the Claude Code statusLine for the prim companion daemon").action(async () => {
|
package/dist/statusline-main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands/statusline.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5
5
|
import { dirname as dirname4, resolve as resolve2 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
@@ -132,14 +132,77 @@ function daemonRequest(method, params = {}, opts = {}) {
|
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// src/lib/activation.ts
|
|
136
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
137
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
138
|
+
import { homedir as homedir3 } from "os";
|
|
139
|
+
import { join as join4 } from "path";
|
|
140
|
+
|
|
141
|
+
// src/lib/git.ts
|
|
142
|
+
import { execFileSync } from "child_process";
|
|
143
|
+
function gitToplevel(cwd) {
|
|
144
|
+
try {
|
|
145
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
146
|
+
cwd,
|
|
147
|
+
encoding: "utf-8",
|
|
148
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
149
|
+
}).trim();
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/lib/activation.ts
|
|
156
|
+
var PRIM_ACTIVE_KEY = "prim.active";
|
|
157
|
+
var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
|
|
158
|
+
var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
|
|
159
|
+
function repoActiveFlag(cwd) {
|
|
160
|
+
try {
|
|
161
|
+
const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
|
|
162
|
+
cwd,
|
|
163
|
+
encoding: "utf-8",
|
|
164
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
165
|
+
}).trim();
|
|
166
|
+
if (value === "true") return "true";
|
|
167
|
+
if (value === "false") return "false";
|
|
168
|
+
return void 0;
|
|
169
|
+
} catch {
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function hasProjectPrimInstall(cwd) {
|
|
174
|
+
const root = gitToplevel(cwd);
|
|
175
|
+
if (root === null || root === homedir3()) return false;
|
|
176
|
+
for (const rel of PROJECT_INSTALL_FILES) {
|
|
177
|
+
const path = join4(root, rel);
|
|
178
|
+
if (existsSync3(path)) {
|
|
179
|
+
try {
|
|
180
|
+
const content = readFileSync3(path, "utf-8");
|
|
181
|
+
if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function isRepoActiveForCapture(cwd) {
|
|
189
|
+
const flag = repoActiveFlag(cwd);
|
|
190
|
+
if (flag === "true") return true;
|
|
191
|
+
if (flag === "false") return false;
|
|
192
|
+
return hasProjectPrimInstall(cwd);
|
|
193
|
+
}
|
|
194
|
+
function decisionIngestionStatus(cwd) {
|
|
195
|
+
return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
|
|
196
|
+
}
|
|
197
|
+
|
|
135
198
|
// src/lib/bin-cache.ts
|
|
136
199
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
137
|
-
import { homedir as
|
|
138
|
-
import { join as
|
|
200
|
+
import { homedir as homedir4 } from "os";
|
|
201
|
+
import { join as join6 } from "path";
|
|
139
202
|
|
|
140
203
|
// src/lib/bin-path.ts
|
|
141
|
-
import { existsSync as
|
|
142
|
-
import { dirname as dirname3, isAbsolute, join as
|
|
204
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
205
|
+
import { dirname as dirname3, isAbsolute, join as join5 } from "path";
|
|
143
206
|
import { fileURLToPath } from "url";
|
|
144
207
|
var PKG_NAME = "@primitive.ai/prim";
|
|
145
208
|
var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
|
|
@@ -218,7 +281,7 @@ function readPackageVersion() {
|
|
|
218
281
|
];
|
|
219
282
|
for (const path of candidates) {
|
|
220
283
|
try {
|
|
221
|
-
const pkg = JSON.parse(
|
|
284
|
+
const pkg = JSON.parse(readFileSync5(path, "utf-8"));
|
|
222
285
|
if (pkg.version) {
|
|
223
286
|
return pkg.version;
|
|
224
287
|
}
|
|
@@ -259,11 +322,12 @@ async function renderStatusline() {
|
|
|
259
322
|
}
|
|
260
323
|
return `primitive ${version} (daemon: starting)`;
|
|
261
324
|
}
|
|
325
|
+
const ingestionStatus = decisionIngestionStatus(process.cwd());
|
|
262
326
|
if (snapshot.envMismatch) {
|
|
263
|
-
return `primitive ${version} (daemon: live \xB7 presence: other env)`;
|
|
327
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
|
|
264
328
|
}
|
|
265
329
|
if (snapshot.presenceStale) {
|
|
266
|
-
return `primitive ${version} (daemon: live \xB7 presence: stale)`;
|
|
330
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
|
|
267
331
|
}
|
|
268
332
|
let team;
|
|
269
333
|
if (snapshot.onlineTeammates !== void 0) {
|
|
@@ -275,7 +339,7 @@ async function renderStatusline() {
|
|
|
275
339
|
} else {
|
|
276
340
|
team = "team: \u2014";
|
|
277
341
|
}
|
|
278
|
-
return `primitive ${version} (daemon: live \xB7 ${team})`;
|
|
342
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
|
|
279
343
|
}
|
|
280
344
|
|
|
281
345
|
// src/statusline-main.ts
|
package/package.json
CHANGED