@primitive.ai/prim 0.1.0-alpha.16 → 0.1.0-alpha.18
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/SKILL.md +31 -1
- package/dist/{chunk-PTLXSXIY.js → chunk-6LAQVM26.js} +0 -29
- package/dist/chunk-7GHOFNJ2.js +57 -0
- package/dist/chunk-7YRBACIE.js +9 -0
- package/dist/{chunk-S47B4VGC.js → chunk-TPQ3X244.js} +32 -3
- package/dist/daemon/server.js +33 -0
- package/dist/hooks/post-commit.js +71 -0
- package/dist/hooks/post-tool-use.js +12 -4
- package/dist/hooks/pre-commit.js +2 -1
- package/dist/hooks/pre-tool-use.js +47 -4
- package/dist/hooks/prim-hook.js +8 -3
- package/dist/hooks/session-start.js +27 -2
- package/dist/index.js +263 -75
- package/package.json +5 -4
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prim
|
|
3
|
-
description: Use the prim CLI for
|
|
3
|
+
description: Use the prim CLI for Primitive specs, contexts, projects, pre-commit hooks, and the decision graph (passive decision capture, the conflict gate, reconcile, and team presence). TRIGGER when the user mentions Primitive, prim, "specs" or "contexts" (in the Primitive sense), or decisions / the decision graph / a conflict gate / reconcile; when the repo's package.json depends on @primitive.ai/prim; when the user asks to sync, map, update, or auto-map a spec; when an edit is denied or warned by a prior decision; when configuring Primitive hooks. SKIP when "spec" means test specs (vitest, jest, rspec), when "context" means React context or an LLM context window, or for unrelated CLIs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Working with the prim CLI
|
|
@@ -35,6 +35,36 @@ The CLI auto-refreshes expired tokens. On unrecoverable expiry it throws `Authen
|
|
|
35
35
|
2. Every command accepts `--help`. When unsure of flags, run `npx --yes @primitive.ai/prim <cmd> --help` rather than guessing.
|
|
36
36
|
3. The CLI prints API errors as one-liners to stderr and exits non-zero. Treat any non-zero exit as actionable. If a command fails with an unrecognized error, re-run with `--help` to check your flags. If auth-related, re-check `auth status`.
|
|
37
37
|
|
|
38
|
+
## Working with the decision graph
|
|
39
|
+
|
|
40
|
+
Separate from specs, prim passively captures the decisions you make during a coding session -- which library, which pattern, which config value -- into a queryable decision graph, and actively **gates** edits that would conflict with a load-bearing prior decision. Capture and the gate run automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code) or `npx --yes @primitive.ai/prim codex install` (Codex). You never invoke capture; you *respond* to the gate and *read* the graph.
|
|
41
|
+
|
|
42
|
+
### Heed the conflict gate
|
|
43
|
+
Before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch) a PreToolUse hook scores the target file against the graph:
|
|
44
|
+
|
|
45
|
+
- **deny** -- the edit is blocked: it conflicts with a load-bearing prior decision. Don't fight it. Read the reason line; it names the decision id. If you genuinely intend to override that decision, run `npx --yes @primitive.ai/prim reconcile dec_<shortId>`, then retry the edit once. Otherwise choose an approach that respects the decision.
|
|
46
|
+
- **warn / additional context** -- the edit proceeds, but a relevant prior decision is surfaced. Read it. On Codex a would-be `ask` is delivered as allow-plus-context (Codex can't pause mid-tool), so that context is your only signal -- read it before continuing.
|
|
47
|
+
- **"decision check skipped / not verified" or "... partial / truncated"** -- the check could not fully run. Treat constraints as UNKNOWN, not clear; never read silence as approval.
|
|
48
|
+
|
|
49
|
+
The gate fail-opens on its *own* infrastructure errors (no daemon, network blip, org-unbound token) -- a setup problem never blocks your edit. That is exactly why an "unavailable" note matters: it is the honest signal that the check, not your edit, is what failed.
|
|
50
|
+
|
|
51
|
+
### Read the graph before large or load-bearing edits
|
|
52
|
+
- `npx --yes @primitive.ai/prim decisions check --files "src/a.ts,src/b.ts"` -- which active decisions reference the files you're about to touch (comma-separated paths, one `--files` value). Run it before a big change.
|
|
53
|
+
- `npx --yes @primitive.ai/prim decisions recent` -- the team's recent decisions, each row badged by author and agent (`Your Claude Code` / `Your Codex`); `--limit <n>` and `--since <dur>` narrow it.
|
|
54
|
+
- `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.
|
|
55
|
+
|
|
56
|
+
### Reconcile and the verdict footer
|
|
57
|
+
`npx --yes @primitive.ai/prim reconcile <idOrShortId>` mints a single-use bypass for the named decision -- it prints `[prim] reconcile bypass issued for dec_<short> (expires in ...)` to STDERR, with the bypass JSON on STDOUT. Your *next* edit to the governed file then goes through, and on that edit prim prints a verdict footer to STDERR -- confirmation the override was recorded, not silently dropped:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
✓ Conflict caught before merge · N decisions saved · <author>'s intent preserved
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`N` is the reconciled decision's downstream live-dependent count, shown as `N+` when the server caps it.
|
|
64
|
+
|
|
65
|
+
### Presence
|
|
66
|
+
With the daemon running (`npx --yes @primitive.ai/prim daemon start`), `npx --yes @primitive.ai/prim daemon status` includes the live online count in its STDOUT JSON (when presence is fresh); Claude Code surfaces it in the statusline as `team: N online`. Your captured decisions are attributed to your agent automatically -- no flag required.
|
|
67
|
+
|
|
38
68
|
## Common workflows
|
|
39
69
|
|
|
40
70
|
### Read a spec's current text (do this before any partial edit)
|
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
// src/hooks/prim-hook-core.ts
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
|
-
import { platform } from "os";
|
|
4
|
-
|
|
5
|
-
// src/protocol/move.ts
|
|
6
|
-
var ENVELOPE_VERSION = 1;
|
|
7
|
-
|
|
8
|
-
// src/hooks/prim-hook-core.ts
|
|
9
|
-
function toMove(parsed, cliVersion) {
|
|
10
|
-
return {
|
|
11
|
-
moveId: randomUUID(),
|
|
12
|
-
capturedAt: Date.now(),
|
|
13
|
-
sessionId: parsed.session_id ?? "",
|
|
14
|
-
eventType: parsed.hook_event_name ?? "unknown",
|
|
15
|
-
payload: parsed,
|
|
16
|
-
env: {
|
|
17
|
-
cwd: parsed.cwd ?? process.cwd(),
|
|
18
|
-
cliVersion,
|
|
19
|
-
osPlatform: platform()
|
|
20
|
-
},
|
|
21
|
-
envelopeVersion: ENVELOPE_VERSION
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
function shouldFlushAfter(eventType) {
|
|
25
|
-
return eventType === "SessionEnd";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
1
|
// src/hooks/redact.ts
|
|
29
2
|
import { existsSync, readFileSync } from "fs";
|
|
30
3
|
import { join } from "path";
|
|
@@ -105,7 +78,5 @@ function scrubFromCwd(value, cwd) {
|
|
|
105
78
|
}
|
|
106
79
|
|
|
107
80
|
export {
|
|
108
|
-
toMove,
|
|
109
|
-
shouldFlushAfter,
|
|
110
81
|
scrubFromCwd
|
|
111
82
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/hooks/prim-hook-core.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { platform } from "os";
|
|
4
|
+
|
|
5
|
+
// src/protocol/move.ts
|
|
6
|
+
var ENVELOPE_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
// src/hooks/prim-hook-core.ts
|
|
9
|
+
function toMove(parsed, cliVersion, agent = "claude_code") {
|
|
10
|
+
return {
|
|
11
|
+
moveId: randomUUID(),
|
|
12
|
+
capturedAt: Date.now(),
|
|
13
|
+
sessionId: parsed.session_id ?? "",
|
|
14
|
+
eventType: parsed.hook_event_name ?? "unknown",
|
|
15
|
+
payload: parsed,
|
|
16
|
+
env: {
|
|
17
|
+
cwd: parsed.cwd ?? process.cwd(),
|
|
18
|
+
cliVersion,
|
|
19
|
+
osPlatform: platform()
|
|
20
|
+
},
|
|
21
|
+
envelopeVersion: ENVELOPE_VERSION,
|
|
22
|
+
// Stamp the producer only for Codex; Claude Code moves omit it (the
|
|
23
|
+
// backend defaults an absent value to "claude_code"), keeping the
|
|
24
|
+
// Claude wire shape byte-identical.
|
|
25
|
+
...agent === "codex" ? { producer: "codex" } : {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function toCommitMove(commit, cliVersion, cwd) {
|
|
29
|
+
return {
|
|
30
|
+
moveId: `commit:${commit.sha}`,
|
|
31
|
+
capturedAt: Date.now(),
|
|
32
|
+
sessionId: "",
|
|
33
|
+
eventType: "git.commit",
|
|
34
|
+
payload: {
|
|
35
|
+
kind: "git.commit",
|
|
36
|
+
sha: commit.sha,
|
|
37
|
+
parentSha: commit.parentSha,
|
|
38
|
+
branch: commit.branch,
|
|
39
|
+
files: commit.files
|
|
40
|
+
},
|
|
41
|
+
env: {
|
|
42
|
+
cwd,
|
|
43
|
+
cliVersion,
|
|
44
|
+
osPlatform: platform()
|
|
45
|
+
},
|
|
46
|
+
envelopeVersion: ENVELOPE_VERSION
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function shouldFlushAfter(eventType) {
|
|
50
|
+
return eventType === "SessionEnd";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
toMove,
|
|
55
|
+
toCommitMove,
|
|
56
|
+
shouldFlushAfter
|
|
57
|
+
};
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient
|
|
3
3
|
} from "./chunk-6SIEWWUL.js";
|
|
4
|
+
import {
|
|
5
|
+
daemonRequest
|
|
6
|
+
} from "./chunk-UTKQTZHL.js";
|
|
7
|
+
|
|
8
|
+
// src/daemon/proxy.ts
|
|
9
|
+
var DAEMON_HTTP_TIMEOUT_MS = 1e4;
|
|
10
|
+
var DAEMON_PROBE_TIMEOUT_MS = 250;
|
|
11
|
+
async function daemonOrDirect(method, params, direct) {
|
|
12
|
+
const fromDaemon = await daemonRequest(method, params, {
|
|
13
|
+
timeoutMs: DAEMON_PROBE_TIMEOUT_MS
|
|
14
|
+
});
|
|
15
|
+
if (fromDaemon !== null) {
|
|
16
|
+
return fromDaemon;
|
|
17
|
+
}
|
|
18
|
+
return await direct();
|
|
19
|
+
}
|
|
20
|
+
async function daemonOrDirectGet(method, path, client, timeoutMs = DAEMON_HTTP_TIMEOUT_MS) {
|
|
21
|
+
return await daemonOrDirect(
|
|
22
|
+
method,
|
|
23
|
+
{ path },
|
|
24
|
+
async () => await client.get(path, {
|
|
25
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|
|
4
29
|
|
|
5
30
|
// src/hooks/decisions-check.ts
|
|
6
31
|
var DECISIONS_CHECK_TIMEOUT_MS = 1e4;
|
|
@@ -19,9 +44,12 @@ async function fetchAffecting(client, batch) {
|
|
|
19
44
|
params.append("files", file);
|
|
20
45
|
}
|
|
21
46
|
try {
|
|
22
|
-
return await
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
return await daemonOrDirectGet(
|
|
48
|
+
"decisions_affecting",
|
|
49
|
+
`/api/cli/decisions/affecting?${params.toString()}`,
|
|
50
|
+
client,
|
|
51
|
+
DECISIONS_CHECK_TIMEOUT_MS
|
|
52
|
+
);
|
|
25
53
|
} catch (err) {
|
|
26
54
|
const detail = err instanceof Error ? err.message : String(err);
|
|
27
55
|
return { decisions: [], truncated: false, unavailable: `decision check failed: ${detail}` };
|
|
@@ -116,6 +144,7 @@ function getGitContext() {
|
|
|
116
144
|
}
|
|
117
145
|
|
|
118
146
|
export {
|
|
147
|
+
daemonOrDirectGet,
|
|
119
148
|
checkAffectedDecisions,
|
|
120
149
|
formatDecisionsWarning,
|
|
121
150
|
getGitContext
|
package/dist/daemon/server.js
CHANGED
|
@@ -16,6 +16,7 @@ var PID_PATH = join(CONFIG_DIR, "daemon.pid");
|
|
|
16
16
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
17
17
|
var TOKEN_CHECK_INTERVAL_MS = 6e4;
|
|
18
18
|
var TOKEN_REFRESH_THRESHOLD_MS = 9e4;
|
|
19
|
+
var HTTP_PROXY_TIMEOUT_MS = 1e4;
|
|
19
20
|
var PRESENCE_FRESH_WINDOW_MS = 9e4;
|
|
20
21
|
var SOCKET_DIR_MODE = 448;
|
|
21
22
|
var PID_FILE_MODE = 384;
|
|
@@ -98,6 +99,22 @@ async function handleConflictCheck(params) {
|
|
|
98
99
|
}
|
|
99
100
|
return await client.post("/api/cli/decisions/conflict-check", { file: params.file });
|
|
100
101
|
}
|
|
102
|
+
function pathParam(params) {
|
|
103
|
+
if (typeof params.path !== "string" || !params.path.startsWith("/api/cli/")) {
|
|
104
|
+
throw new Error("proxy request requires `path: string` under /api/cli/");
|
|
105
|
+
}
|
|
106
|
+
return params.path;
|
|
107
|
+
}
|
|
108
|
+
function assertEndpointPath(path, endpoint) {
|
|
109
|
+
if (path !== endpoint && !path.startsWith(`${endpoint}?`)) {
|
|
110
|
+
throw new Error(`proxy path must be ${endpoint} or ${endpoint}?...`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function proxyGet(params, allowedPrefix) {
|
|
114
|
+
const path = pathParam(params);
|
|
115
|
+
assertEndpointPath(path, allowedPrefix);
|
|
116
|
+
return await client.get(path, { signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) });
|
|
117
|
+
}
|
|
101
118
|
function handleStatusSnapshot() {
|
|
102
119
|
const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
|
|
103
120
|
const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
|
|
@@ -120,6 +137,22 @@ async function dispatchRequest(req) {
|
|
|
120
137
|
const result = await handleConflictCheck(req.params ?? {});
|
|
121
138
|
return { id, ok: true, result };
|
|
122
139
|
}
|
|
140
|
+
case "decisions_recent": {
|
|
141
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/recent");
|
|
142
|
+
return { id, ok: true, result };
|
|
143
|
+
}
|
|
144
|
+
case "decisions_show": {
|
|
145
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/show");
|
|
146
|
+
return { id, ok: true, result };
|
|
147
|
+
}
|
|
148
|
+
case "decisions_cascade": {
|
|
149
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/cascade");
|
|
150
|
+
return { id, ok: true, result };
|
|
151
|
+
}
|
|
152
|
+
case "decisions_affecting": {
|
|
153
|
+
const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/affecting");
|
|
154
|
+
return { id, ok: true, result };
|
|
155
|
+
}
|
|
123
156
|
case "session_start": {
|
|
124
157
|
const sid = req.params?.sessionId;
|
|
125
158
|
if (typeof sid === "string" && sid.length > 0) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
appendMove,
|
|
4
|
+
resolveOrg
|
|
5
|
+
} from "../chunk-JZGWQDM5.js";
|
|
6
|
+
import {
|
|
7
|
+
toCommitMove
|
|
8
|
+
} from "../chunk-7GHOFNJ2.js";
|
|
9
|
+
|
|
10
|
+
// src/hooks/post-commit.ts
|
|
11
|
+
import { execSync, spawn } from "child_process";
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
function git(args) {
|
|
17
|
+
try {
|
|
18
|
+
return execSync(`git ${args}`, {
|
|
19
|
+
encoding: "utf-8",
|
|
20
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
21
|
+
}).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function readCommit() {
|
|
27
|
+
const sha = git("rev-parse HEAD");
|
|
28
|
+
if (!sha) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const branch = git("rev-parse --abbrev-ref HEAD");
|
|
32
|
+
const files = (git("diff-tree --no-commit-id --name-only -r -m --root HEAD") ?? "").split("\n").filter((f) => f.length > 0);
|
|
33
|
+
return {
|
|
34
|
+
sha,
|
|
35
|
+
parentSha: git("rev-parse --verify --quiet HEAD^") || void 0,
|
|
36
|
+
branch: branch && branch !== "HEAD" ? branch : void 0,
|
|
37
|
+
files
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function resolveCliVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
|
|
43
|
+
return pkg.version ?? "unknown";
|
|
44
|
+
} catch {
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function spawnBackgroundFlush() {
|
|
49
|
+
const entry = join(here, "..", "index.js");
|
|
50
|
+
spawn(process.execPath, [entry, "moves", "flush"], {
|
|
51
|
+
detached: true,
|
|
52
|
+
stdio: "ignore"
|
|
53
|
+
}).unref();
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const commit = readCommit();
|
|
57
|
+
if (commit) {
|
|
58
|
+
const cwd = git("rev-parse --show-toplevel") ?? process.cwd();
|
|
59
|
+
const move = toCommitMove(commit, resolveCliVersion(), cwd);
|
|
60
|
+
const { orgId } = resolveOrg({ sessionId: "", cwd });
|
|
61
|
+
appendMove(move, orgId);
|
|
62
|
+
spawnBackgroundFlush();
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (process.env.PRIM_HOOK_DEBUG) {
|
|
66
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
67
|
+
process.stderr.write(`[prim-post-commit] capture failed: ${detail}
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
process.exit(0);
|
|
@@ -7,9 +7,14 @@ import {
|
|
|
7
7
|
getClient
|
|
8
8
|
} from "../chunk-6SIEWWUL.js";
|
|
9
9
|
import {
|
|
10
|
-
scrubFromCwd
|
|
10
|
+
scrubFromCwd
|
|
11
|
+
} from "../chunk-6LAQVM26.js";
|
|
12
|
+
import {
|
|
11
13
|
toMove
|
|
12
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-7GHOFNJ2.js";
|
|
15
|
+
import {
|
|
16
|
+
parseAgent
|
|
17
|
+
} from "../chunk-7YRBACIE.js";
|
|
13
18
|
|
|
14
19
|
// src/hooks/post-tool-use.ts
|
|
15
20
|
import { readFileSync } from "fs";
|
|
@@ -36,6 +41,7 @@ function isVerdictFooterContext(value) {
|
|
|
36
41
|
var STDIN_TIMEOUT_MS = 1e3;
|
|
37
42
|
var INGEST_TIMEOUT_MS = 4e3;
|
|
38
43
|
var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
44
|
+
var CODEX_EDITING_TOOLS = /* @__PURE__ */ new Set(["apply_patch"]);
|
|
39
45
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
40
46
|
function resolveCliVersion() {
|
|
41
47
|
try {
|
|
@@ -100,7 +106,9 @@ async function main() {
|
|
|
100
106
|
return;
|
|
101
107
|
}
|
|
102
108
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
103
|
-
|
|
109
|
+
const agent = parseAgent(process.argv);
|
|
110
|
+
const editingTools = agent === "codex" ? CODEX_EDITING_TOOLS : EDITING_TOOLS;
|
|
111
|
+
if (!editingTools.has(toolName)) {
|
|
104
112
|
emit();
|
|
105
113
|
return;
|
|
106
114
|
}
|
|
@@ -109,7 +117,7 @@ async function main() {
|
|
|
109
117
|
return;
|
|
110
118
|
}
|
|
111
119
|
const cwd = parsed.cwd ?? process.cwd();
|
|
112
|
-
const base = toMove(parsed, resolveCliVersion());
|
|
120
|
+
const base = toMove(parsed, resolveCliVersion(), agent);
|
|
113
121
|
const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
|
|
114
122
|
try {
|
|
115
123
|
const result = await ingestMove(move);
|
package/dist/hooks/pre-commit.js
CHANGED
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
checkAffectedDecisions,
|
|
4
4
|
formatDecisionsWarning,
|
|
5
5
|
getGitContext
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-TPQ3X244.js";
|
|
7
7
|
import {
|
|
8
8
|
getClient
|
|
9
9
|
} from "../chunk-6SIEWWUL.js";
|
|
10
|
+
import "../chunk-UTKQTZHL.js";
|
|
10
11
|
|
|
11
12
|
// src/hooks/pre-commit.ts
|
|
12
13
|
import { execSync } from "child_process";
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
getClient
|
|
4
4
|
} from "../chunk-6SIEWWUL.js";
|
|
5
|
+
import {
|
|
6
|
+
parseAgent
|
|
7
|
+
} from "../chunk-7YRBACIE.js";
|
|
5
8
|
import {
|
|
6
9
|
daemonRequest
|
|
7
10
|
} from "../chunk-UTKQTZHL.js";
|
|
@@ -43,7 +46,7 @@ function unverifiedNote(results) {
|
|
|
43
46
|
}
|
|
44
47
|
return causes.map((c) => `[primitive] ${c}`).join("\n");
|
|
45
48
|
}
|
|
46
|
-
function buildHookOutput(aggregate, results) {
|
|
49
|
+
function buildHookOutput(aggregate, results, agent = "claude_code") {
|
|
47
50
|
if (aggregate === "deny") {
|
|
48
51
|
const reason = results.filter((r) => r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
|
|
49
52
|
return {
|
|
@@ -54,6 +57,18 @@ function buildHookOutput(aggregate, results) {
|
|
|
54
57
|
}
|
|
55
58
|
};
|
|
56
59
|
}
|
|
60
|
+
if (agent === "codex" && aggregate === "ask") {
|
|
61
|
+
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
62
|
+
const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
|
|
63
|
+
const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
|
|
64
|
+
const out = {
|
|
65
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
|
|
66
|
+
};
|
|
67
|
+
if (merged.length > 0) {
|
|
68
|
+
out.hookSpecificOutput.additionalContext = merged;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
57
72
|
if (aggregate === "ask") {
|
|
58
73
|
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] please confirm this edit";
|
|
59
74
|
const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
|
|
@@ -98,7 +113,32 @@ function failOpenOutput() {
|
|
|
98
113
|
};
|
|
99
114
|
}
|
|
100
115
|
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
101
|
-
|
|
116
|
+
var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
|
|
117
|
+
var LINE_SPLIT_RE = /\r?\n/;
|
|
118
|
+
function parseApplyPatchPaths(command) {
|
|
119
|
+
const paths = /* @__PURE__ */ new Set();
|
|
120
|
+
for (const line of command.split(LINE_SPLIT_RE)) {
|
|
121
|
+
const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path?.trim();
|
|
122
|
+
if (path) {
|
|
123
|
+
paths.add(path);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return Array.from(paths);
|
|
127
|
+
}
|
|
128
|
+
function extractCodexFilePaths(toolName, toolInput) {
|
|
129
|
+
if (toolName !== "apply_patch") {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const command = toolInput.command;
|
|
136
|
+
return typeof command === "string" ? parseApplyPatchPaths(command) : [];
|
|
137
|
+
}
|
|
138
|
+
function extractFilePaths(toolName, toolInput, agent = "claude_code") {
|
|
139
|
+
if (agent === "codex") {
|
|
140
|
+
return extractCodexFilePaths(toolName, toolInput);
|
|
141
|
+
}
|
|
102
142
|
if (!SUPPORTED_TOOLS.has(toolName)) {
|
|
103
143
|
return [];
|
|
104
144
|
}
|
|
@@ -199,7 +239,10 @@ async function main() {
|
|
|
199
239
|
}
|
|
200
240
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
201
241
|
const cwd = typeof envelope.cwd === "string" && envelope.cwd.length > 0 ? envelope.cwd : process.cwd();
|
|
202
|
-
const
|
|
242
|
+
const agent = parseAgent(process.argv);
|
|
243
|
+
const files = extractFilePaths(toolName, envelope.tool_input, agent).map(
|
|
244
|
+
(f) => toRepoRelative(f, cwd)
|
|
245
|
+
);
|
|
203
246
|
if (files.length === 0) {
|
|
204
247
|
emit(failOpenOutput());
|
|
205
248
|
return;
|
|
@@ -213,7 +256,7 @@ async function main() {
|
|
|
213
256
|
}
|
|
214
257
|
const rawAggregate = aggregateCheckResults(results);
|
|
215
258
|
const aggregate = demoteForMode(rawAggregate, mode);
|
|
216
|
-
emit(buildHookOutput(aggregate, results));
|
|
259
|
+
emit(buildHookOutput(aggregate, results, agent));
|
|
217
260
|
}
|
|
218
261
|
main().catch(() => {
|
|
219
262
|
emit(failOpenOutput());
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -4,10 +4,15 @@ import {
|
|
|
4
4
|
resolveOrg
|
|
5
5
|
} from "../chunk-JZGWQDM5.js";
|
|
6
6
|
import {
|
|
7
|
-
scrubFromCwd
|
|
7
|
+
scrubFromCwd
|
|
8
|
+
} from "../chunk-6LAQVM26.js";
|
|
9
|
+
import {
|
|
8
10
|
shouldFlushAfter,
|
|
9
11
|
toMove
|
|
10
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-7GHOFNJ2.js";
|
|
13
|
+
import {
|
|
14
|
+
parseAgent
|
|
15
|
+
} from "../chunk-7YRBACIE.js";
|
|
11
16
|
|
|
12
17
|
// src/hooks/prim-hook.ts
|
|
13
18
|
import { spawn } from "child_process";
|
|
@@ -34,7 +39,7 @@ try {
|
|
|
34
39
|
const raw = readFileSync(0, "utf-8");
|
|
35
40
|
const parsed = JSON.parse(raw);
|
|
36
41
|
const cwd = parsed.cwd ?? process.cwd();
|
|
37
|
-
const base = toMove(parsed, resolveCliVersion());
|
|
42
|
+
const base = toMove(parsed, resolveCliVersion(), parseAgent(process.argv));
|
|
38
43
|
const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
|
|
39
44
|
const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
|
|
40
45
|
appendMove(move, orgId);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseAgent
|
|
4
|
+
} from "../chunk-7YRBACIE.js";
|
|
2
5
|
import {
|
|
3
6
|
daemonRequest
|
|
4
7
|
} from "../chunk-UTKQTZHL.js";
|
|
@@ -23,8 +26,19 @@ function readStdin() {
|
|
|
23
26
|
});
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
|
-
function emit() {
|
|
27
|
-
|
|
29
|
+
function emit(additionalContext) {
|
|
30
|
+
if (!additionalContext) {
|
|
31
|
+
process.stdout.write("{}\n");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const out = {
|
|
35
|
+
hookSpecificOutput: {
|
|
36
|
+
hookEventName: "SessionStart",
|
|
37
|
+
additionalContext
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
process.stdout.write(`${JSON.stringify(out)}
|
|
41
|
+
`);
|
|
28
42
|
}
|
|
29
43
|
async function main() {
|
|
30
44
|
let raw;
|
|
@@ -54,6 +68,17 @@ async function main() {
|
|
|
54
68
|
{ sessionId: envelope.session_id },
|
|
55
69
|
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
56
70
|
);
|
|
71
|
+
if (parseAgent(process.argv) === "codex") {
|
|
72
|
+
const snapshot = await daemonRequest(
|
|
73
|
+
"status_snapshot",
|
|
74
|
+
{},
|
|
75
|
+
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
76
|
+
);
|
|
77
|
+
if (snapshot && !snapshot.presenceStale && typeof snapshot.onlineCount === "number") {
|
|
78
|
+
emit(`[prim] team: ${snapshot.onlineCount} online`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
57
82
|
emit();
|
|
58
83
|
}
|
|
59
84
|
main().catch(() => {
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
} from "./chunk-BEEGFDGU.js";
|
|
7
7
|
import {
|
|
8
8
|
checkAffectedDecisions,
|
|
9
|
+
daemonOrDirectGet,
|
|
9
10
|
formatDecisionsWarning,
|
|
10
11
|
getGitContext
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-TPQ3X244.js";
|
|
12
13
|
import {
|
|
13
14
|
HttpError,
|
|
14
15
|
REFRESH_TOKEN_PATH,
|
|
@@ -532,6 +533,154 @@ ${line("project", result.project)}`);
|
|
|
532
533
|
});
|
|
533
534
|
}
|
|
534
535
|
|
|
536
|
+
// src/commands/codex-install.ts
|
|
537
|
+
import { homedir as homedir2 } from "os";
|
|
538
|
+
import { join as join2 } from "path";
|
|
539
|
+
var CAPTURE_COMMAND2 = "prim-hook --agent codex";
|
|
540
|
+
var GATE_COMMAND2 = "prim-pre-tool-use --agent codex";
|
|
541
|
+
var POST_TOOL_USE_COMMAND2 = "prim-post-tool-use --agent codex";
|
|
542
|
+
var SESSION_START_COMMAND2 = "prim-session-start --agent codex";
|
|
543
|
+
var JSON_INDENT2 = 2;
|
|
544
|
+
var CODEX_CAPTURE_EVENTS = [
|
|
545
|
+
"SessionStart",
|
|
546
|
+
"UserPromptSubmit",
|
|
547
|
+
"PreToolUse",
|
|
548
|
+
"PostToolUse",
|
|
549
|
+
"Stop",
|
|
550
|
+
"SubagentStop"
|
|
551
|
+
];
|
|
552
|
+
var PRIM_COMMANDS2 = /* @__PURE__ */ new Set([
|
|
553
|
+
CAPTURE_COMMAND2,
|
|
554
|
+
GATE_COMMAND2,
|
|
555
|
+
POST_TOOL_USE_COMMAND2,
|
|
556
|
+
SESSION_START_COMMAND2
|
|
557
|
+
]);
|
|
558
|
+
var CODEX_REGISTRATIONS = [
|
|
559
|
+
...CODEX_CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND2 })),
|
|
560
|
+
{ event: "PreToolUse", matcher: "apply_patch", command: GATE_COMMAND2 },
|
|
561
|
+
{ event: "PostToolUse", matcher: "apply_patch", command: POST_TOOL_USE_COMMAND2 },
|
|
562
|
+
{ event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND2 }
|
|
563
|
+
];
|
|
564
|
+
var USER_SCOPE_PATH2 = join2(homedir2(), ".codex", "hooks.json");
|
|
565
|
+
var PROJECT_SCOPE_PATH2 = join2(process.cwd(), ".codex", "hooks.json");
|
|
566
|
+
function settingsPathFor2(scope) {
|
|
567
|
+
return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
|
|
568
|
+
}
|
|
569
|
+
function applyInstall2(settings, options = {}) {
|
|
570
|
+
const hooks = { ...settings.hooks ?? {} };
|
|
571
|
+
for (const reg of CODEX_REGISTRATIONS) {
|
|
572
|
+
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
573
|
+
}
|
|
574
|
+
return { ...settings, hooks };
|
|
575
|
+
}
|
|
576
|
+
function applyUninstall2(settings) {
|
|
577
|
+
const source = settings.hooks ?? {};
|
|
578
|
+
const hooks = {};
|
|
579
|
+
for (const event of Object.keys(source)) {
|
|
580
|
+
let list = source[event] ?? [];
|
|
581
|
+
for (const command of PRIM_COMMANDS2) {
|
|
582
|
+
list = stripCommand(list, command);
|
|
583
|
+
}
|
|
584
|
+
if (list.length > 0) {
|
|
585
|
+
hooks[event] = list;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return { ...settings, hooks };
|
|
589
|
+
}
|
|
590
|
+
function captureInstalled2(settings) {
|
|
591
|
+
return CODEX_CAPTURE_EVENTS.some(
|
|
592
|
+
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND2))
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
function isGateInstalled2(settings) {
|
|
596
|
+
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND2));
|
|
597
|
+
}
|
|
598
|
+
function resultFor(scope, path, after, changed) {
|
|
599
|
+
return {
|
|
600
|
+
scope,
|
|
601
|
+
path,
|
|
602
|
+
gate: isGateInstalled2(after),
|
|
603
|
+
capture: captureInstalled2(after),
|
|
604
|
+
changed
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function performInstall2(scope, force) {
|
|
608
|
+
const path = settingsPathFor2(scope);
|
|
609
|
+
const before = readSettings(path);
|
|
610
|
+
const after = applyInstall2(before, { force });
|
|
611
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
612
|
+
if (changed) {
|
|
613
|
+
atomicWrite(path, after);
|
|
614
|
+
}
|
|
615
|
+
return resultFor(scope, path, after, changed);
|
|
616
|
+
}
|
|
617
|
+
function performUninstall2(scope) {
|
|
618
|
+
const path = settingsPathFor2(scope);
|
|
619
|
+
const before = readSettings(path);
|
|
620
|
+
const after = applyUninstall2(before);
|
|
621
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
622
|
+
if (changed) {
|
|
623
|
+
atomicWrite(path, after);
|
|
624
|
+
}
|
|
625
|
+
return resultFor(scope, path, after, changed);
|
|
626
|
+
}
|
|
627
|
+
function performStatus2() {
|
|
628
|
+
const statusFor = (path) => {
|
|
629
|
+
const settings = readSettings(path);
|
|
630
|
+
return { path, gate: isGateInstalled2(settings), capture: captureInstalled2(settings) };
|
|
631
|
+
};
|
|
632
|
+
return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(PROJECT_SCOPE_PATH2) };
|
|
633
|
+
}
|
|
634
|
+
function resolveScope2(input) {
|
|
635
|
+
if (input === void 0 || input === "user") {
|
|
636
|
+
return "user";
|
|
637
|
+
}
|
|
638
|
+
if (input === "project") {
|
|
639
|
+
return "project";
|
|
640
|
+
}
|
|
641
|
+
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
var TRUST_NOTICE = "[prim] Codex requires hook trust: run `/hooks` in Codex to review and trust these hooks (or start Codex with --dangerously-bypass-hook-trust). Until trusted, the hooks will not fire.";
|
|
645
|
+
function registerCodexCommands(program2) {
|
|
646
|
+
const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
|
|
647
|
+
codex.command("install").description("Register the prim hooks in Codex's ~/.codex/hooks.json").option(
|
|
648
|
+
"--scope <scope>",
|
|
649
|
+
"user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
|
|
650
|
+
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
651
|
+
const scope = resolveScope2(opts.scope);
|
|
652
|
+
const result = performInstall2(scope, opts.force ?? false);
|
|
653
|
+
if (result.changed) {
|
|
654
|
+
console.error(`[prim] Codex integration installed (${scope} scope) at ${result.path}`);
|
|
655
|
+
} else {
|
|
656
|
+
console.error(`[prim] Codex integration already present at ${result.path} (no changes)`);
|
|
657
|
+
}
|
|
658
|
+
console.error(TRUST_NOTICE);
|
|
659
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
660
|
+
});
|
|
661
|
+
codex.command("uninstall").description("Remove all prim hooks from ~/.codex/hooks.json").option(
|
|
662
|
+
"--scope <scope>",
|
|
663
|
+
"user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
|
|
664
|
+
).action((opts) => {
|
|
665
|
+
const scope = resolveScope2(opts.scope);
|
|
666
|
+
const result = performUninstall2(scope);
|
|
667
|
+
if (result.changed) {
|
|
668
|
+
console.error(`[prim] prim hooks removed from ${result.path}`);
|
|
669
|
+
} else {
|
|
670
|
+
console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
|
|
671
|
+
}
|
|
672
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
673
|
+
});
|
|
674
|
+
codex.command("status").description("Report whether each prim surface (gate, capture) is installed per scope").action(() => {
|
|
675
|
+
const result = performStatus2();
|
|
676
|
+
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
677
|
+
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} (${s.path})`;
|
|
678
|
+
console.error(`${line("user", result.user)}
|
|
679
|
+
${line("project", result.project)}`);
|
|
680
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
535
684
|
// src/commands/context.ts
|
|
536
685
|
import { readFileSync as readFileSync3 } from "fs";
|
|
537
686
|
function registerContextCommands(program2) {
|
|
@@ -652,11 +801,11 @@ ${contexts.length} context(s)`);
|
|
|
652
801
|
// src/commands/daemon.ts
|
|
653
802
|
import { spawn } from "child_process";
|
|
654
803
|
import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
655
|
-
import { homedir as
|
|
656
|
-
import { join as
|
|
804
|
+
import { homedir as homedir3 } from "os";
|
|
805
|
+
import { join as join3 } from "path";
|
|
657
806
|
var DAEMON_BIN = "prim-daemon-server";
|
|
658
|
-
var PID_PATH =
|
|
659
|
-
var SOCK_PATH =
|
|
807
|
+
var PID_PATH = join3(homedir3(), ".config", "prim", "daemon.pid");
|
|
808
|
+
var SOCK_PATH = join3(homedir3(), ".config", "prim", "sock");
|
|
660
809
|
var STOP_TIMEOUT_MS = 5e3;
|
|
661
810
|
var STOP_POLL_MS = 100;
|
|
662
811
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
@@ -1036,9 +1185,12 @@ async function fetchCascade(idOrShortId, deps = defaultDeps) {
|
|
|
1036
1185
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
1037
1186
|
const client = deps.getClient();
|
|
1038
1187
|
try {
|
|
1039
|
-
return await
|
|
1040
|
-
|
|
1041
|
-
|
|
1188
|
+
return await daemonOrDirectGet(
|
|
1189
|
+
"decisions_cascade",
|
|
1190
|
+
`/api/cli/decisions/cascade?${params.toString()}`,
|
|
1191
|
+
client,
|
|
1192
|
+
CASCADE_TIMEOUT_MS
|
|
1193
|
+
);
|
|
1042
1194
|
} catch (err) {
|
|
1043
1195
|
if (err instanceof Error && NOT_FOUND_RE.test(err.message)) {
|
|
1044
1196
|
throw new CascadeNotFoundError(idOrShortId);
|
|
@@ -1063,9 +1215,12 @@ async function fetchRecent(args, deps = defaultDeps2) {
|
|
|
1063
1215
|
}
|
|
1064
1216
|
const client = deps.getClient();
|
|
1065
1217
|
try {
|
|
1066
|
-
const res = await
|
|
1067
|
-
|
|
1068
|
-
|
|
1218
|
+
const res = await daemonOrDirectGet(
|
|
1219
|
+
"decisions_recent",
|
|
1220
|
+
`/api/cli/decisions/recent?${params.toString()}`,
|
|
1221
|
+
client,
|
|
1222
|
+
RECENT_TIMEOUT_MS
|
|
1223
|
+
);
|
|
1069
1224
|
const result = { decisions: res.decisions };
|
|
1070
1225
|
if (res.unavailable !== void 0) {
|
|
1071
1226
|
result.unavailable = res.unavailable;
|
|
@@ -1092,6 +1247,8 @@ function authorLabel(row) {
|
|
|
1092
1247
|
switch (row.producerKind) {
|
|
1093
1248
|
case "claude_code":
|
|
1094
1249
|
return "Your Claude Code";
|
|
1250
|
+
case "codex":
|
|
1251
|
+
return "Your Codex";
|
|
1095
1252
|
case "chat":
|
|
1096
1253
|
return "Your chat";
|
|
1097
1254
|
case "spec_edit":
|
|
@@ -1232,9 +1389,12 @@ async function fetchShow(idOrShortId, deps = defaultDeps4) {
|
|
|
1232
1389
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
1233
1390
|
const client = deps.getClient();
|
|
1234
1391
|
try {
|
|
1235
|
-
return await
|
|
1236
|
-
|
|
1237
|
-
|
|
1392
|
+
return await daemonOrDirectGet(
|
|
1393
|
+
"decisions_show",
|
|
1394
|
+
`/api/cli/decisions/show?${params.toString()}`,
|
|
1395
|
+
client,
|
|
1396
|
+
SHOW_TIMEOUT_MS
|
|
1397
|
+
);
|
|
1238
1398
|
} catch (err) {
|
|
1239
1399
|
if (err instanceof Error && NOT_FOUND_RE3.test(err.message)) {
|
|
1240
1400
|
throw new DecisionNotFoundError(idOrShortId);
|
|
@@ -1417,30 +1577,39 @@ import { execSync } from "child_process";
|
|
|
1417
1577
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1418
1578
|
import { resolve } from "path";
|
|
1419
1579
|
import { Option } from "commander";
|
|
1420
|
-
var
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1580
|
+
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
1581
|
+
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
1582
|
+
var HOOKS = [PRE_COMMIT, POST_COMMIT];
|
|
1583
|
+
function blockMarkers(spec) {
|
|
1584
|
+
return {
|
|
1585
|
+
start: `# >>> prim ${spec.hookName} hook >>>`,
|
|
1586
|
+
end: `# <<< prim ${spec.hookName} hook <<<`
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
|
|
1590
|
+
var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
|
|
1591
|
+
function hookShim(binName) {
|
|
1592
|
+
return `if command -v ${binName} >/dev/null 2>&1; then
|
|
1593
|
+
${binName}
|
|
1594
|
+
elif [ -f "./node_modules/.bin/${binName}" ]; then
|
|
1595
|
+
./node_modules/.bin/${binName}
|
|
1429
1596
|
else
|
|
1430
|
-
npx --yes -p @primitive.ai/prim
|
|
1431
|
-
fi
|
|
1597
|
+
npx --yes -p @primitive.ai/prim ${binName} 2>/dev/null || true
|
|
1598
|
+
fi`;
|
|
1599
|
+
}
|
|
1600
|
+
function dotGitScript(spec) {
|
|
1601
|
+
return `#!/bin/sh
|
|
1602
|
+
# prim ${spec.hookName} hook \u2014 installed by: prim hooks install
|
|
1603
|
+
|
|
1604
|
+
${hookShim(spec.binName)}
|
|
1432
1605
|
`;
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
else
|
|
1441
|
-
npx --yes -p @primitive.ai/prim prim-pre-commit 2>/dev/null || true
|
|
1442
|
-
fi
|
|
1443
|
-
${PRIM_BLOCK_END}`;
|
|
1606
|
+
}
|
|
1607
|
+
function huskyBlock(spec) {
|
|
1608
|
+
const { start, end } = blockMarkers(spec);
|
|
1609
|
+
return `${start}
|
|
1610
|
+
${hookShim(spec.binName)}
|
|
1611
|
+
${end}`;
|
|
1612
|
+
}
|
|
1444
1613
|
function getGitRoot() {
|
|
1445
1614
|
return execSync("git rev-parse --show-toplevel", {
|
|
1446
1615
|
encoding: "utf-8"
|
|
@@ -1464,8 +1633,8 @@ function detectHusky(gitRoot) {
|
|
|
1464
1633
|
}
|
|
1465
1634
|
return false;
|
|
1466
1635
|
}
|
|
1467
|
-
function containsPrimHook(content) {
|
|
1468
|
-
return content.includes(
|
|
1636
|
+
function containsPrimHook(content, binName = PRE_COMMIT.binName) {
|
|
1637
|
+
return content.includes(binName);
|
|
1469
1638
|
}
|
|
1470
1639
|
async function askConfirmation(question) {
|
|
1471
1640
|
if (!process.stdin.isTTY) return false;
|
|
@@ -1479,52 +1648,63 @@ async function askConfirmation(question) {
|
|
|
1479
1648
|
rl.close();
|
|
1480
1649
|
}
|
|
1481
1650
|
}
|
|
1482
|
-
function installToHusky(gitRoot) {
|
|
1483
|
-
const hookPath = resolve(gitRoot, ".husky",
|
|
1651
|
+
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
1652
|
+
const hookPath = resolve(gitRoot, ".husky", spec.hookName);
|
|
1484
1653
|
if (existsSync4(hookPath)) {
|
|
1485
1654
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1486
|
-
if (containsPrimHook(existing)) {
|
|
1487
|
-
console.log(
|
|
1655
|
+
if (containsPrimHook(existing, spec.binName)) {
|
|
1656
|
+
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
1488
1657
|
return;
|
|
1489
1658
|
}
|
|
1490
1659
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1491
|
-
writeFileSync3(hookPath, `${existing}${separator}${
|
|
1660
|
+
writeFileSync3(hookPath, `${existing}${separator}${huskyBlock(spec)}
|
|
1492
1661
|
`, {
|
|
1493
1662
|
mode: 493
|
|
1494
1663
|
});
|
|
1495
|
-
console.log(
|
|
1664
|
+
console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
|
|
1496
1665
|
} else {
|
|
1497
1666
|
writeFileSync3(hookPath, `#!/bin/sh
|
|
1498
1667
|
|
|
1499
|
-
${
|
|
1668
|
+
${huskyBlock(spec)}
|
|
1500
1669
|
`, {
|
|
1501
1670
|
mode: 493
|
|
1502
1671
|
});
|
|
1503
|
-
console.log(
|
|
1672
|
+
console.log(`Created .husky/${spec.hookName} with prim hook block.`);
|
|
1504
1673
|
}
|
|
1505
1674
|
}
|
|
1506
|
-
function installToDotGit(gitRoot) {
|
|
1675
|
+
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
1507
1676
|
const hooksDir = resolve(gitRoot, ".git", "hooks");
|
|
1508
|
-
const hookPath = resolve(hooksDir,
|
|
1677
|
+
const hookPath = resolve(hooksDir, spec.hookName);
|
|
1509
1678
|
if (!existsSync4(hooksDir)) {
|
|
1510
1679
|
mkdirSync3(hooksDir, { recursive: true });
|
|
1511
1680
|
}
|
|
1512
1681
|
if (existsSync4(hookPath)) {
|
|
1513
1682
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1514
|
-
if (containsPrimHook(existing)) {
|
|
1515
|
-
console.log(
|
|
1683
|
+
if (containsPrimHook(existing, spec.binName)) {
|
|
1684
|
+
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
1516
1685
|
return;
|
|
1517
1686
|
}
|
|
1518
|
-
console.log(`A
|
|
1687
|
+
console.log(`A ${spec.hookName} hook already exists at ${hookPath}.`);
|
|
1519
1688
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
1520
1689
|
return;
|
|
1521
1690
|
}
|
|
1522
|
-
writeFileSync3(hookPath,
|
|
1523
|
-
console.log(`Installed
|
|
1691
|
+
writeFileSync3(hookPath, dotGitScript(spec), { mode: 493 });
|
|
1692
|
+
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
1693
|
+
}
|
|
1694
|
+
function installHooks(gitRoot, target) {
|
|
1695
|
+
for (const spec of HOOKS) {
|
|
1696
|
+
if (target === "husky") {
|
|
1697
|
+
installToHusky(gitRoot, spec);
|
|
1698
|
+
} else {
|
|
1699
|
+
installToDotGit(gitRoot, spec);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1524
1702
|
}
|
|
1525
1703
|
function registerHooksCommands(program2) {
|
|
1526
1704
|
const hooks = program2.command("hooks").description("Manage git hooks");
|
|
1527
|
-
hooks.command("install").description(
|
|
1705
|
+
hooks.command("install").description(
|
|
1706
|
+
"Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
|
|
1707
|
+
).addOption(
|
|
1528
1708
|
new Option("--target <where>", "install destination; bypasses Husky detection").choices([
|
|
1529
1709
|
"husky",
|
|
1530
1710
|
"git-hooks"
|
|
@@ -1535,10 +1715,10 @@ function registerHooksCommands(program2) {
|
|
|
1535
1715
|
globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
|
|
1536
1716
|
);
|
|
1537
1717
|
const gitRoot = getGitRoot();
|
|
1538
|
-
if (opts.target === "husky") return
|
|
1539
|
-
if (opts.target === "git-hooks") return
|
|
1718
|
+
if (opts.target === "husky") return installHooks(gitRoot, "husky");
|
|
1719
|
+
if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
|
|
1540
1720
|
if (detectHusky(gitRoot)) {
|
|
1541
|
-
if (globals.yes) return
|
|
1721
|
+
if (globals.yes) return installHooks(gitRoot, "husky");
|
|
1542
1722
|
if (nonInteractive) {
|
|
1543
1723
|
throw new Error(
|
|
1544
1724
|
"--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
|
|
@@ -1549,30 +1729,36 @@ function registerHooksCommands(program2) {
|
|
|
1549
1729
|
"Note: Husky detected but stdin is not a TTY \u2014 falling back to .git/hooks. Pass --yes for Husky or --non-interactive to fail fast."
|
|
1550
1730
|
);
|
|
1551
1731
|
} else if (await askConfirmation(
|
|
1552
|
-
"Husky detected. Install prim
|
|
1732
|
+
"Husky detected. Install prim hooks into .husky/ instead of .git/hooks/?"
|
|
1553
1733
|
)) {
|
|
1554
|
-
return
|
|
1734
|
+
return installHooks(gitRoot, "husky");
|
|
1555
1735
|
} else {
|
|
1556
|
-
console.log("Falling back to .git/hooks
|
|
1736
|
+
console.log("Falling back to .git/hooks install.");
|
|
1557
1737
|
}
|
|
1558
1738
|
}
|
|
1559
|
-
|
|
1739
|
+
installHooks(gitRoot, "git-hooks");
|
|
1560
1740
|
});
|
|
1561
|
-
hooks.command("uninstall").description("Remove the prim
|
|
1741
|
+
hooks.command("uninstall").description("Remove the prim git hooks (.git/hooks)").action(() => {
|
|
1562
1742
|
const gitRoot = getGitRoot();
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1743
|
+
for (const spec of HOOKS) {
|
|
1744
|
+
const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
|
|
1745
|
+
if (!existsSync4(hookPath)) {
|
|
1746
|
+
console.log(`No ${spec.hookName} hook found.`);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
|
|
1750
|
+
unlinkSync2(hookPath);
|
|
1751
|
+
console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
|
|
1752
|
+
} else {
|
|
1753
|
+
console.log(`Left ${spec.hookName} hook at ${hookPath} untouched (not a prim hook).`);
|
|
1754
|
+
}
|
|
1567
1755
|
}
|
|
1568
|
-
unlinkSync2(hookPath);
|
|
1569
|
-
console.log(`Removed pre-commit hook at ${hookPath}`);
|
|
1570
1756
|
});
|
|
1571
1757
|
}
|
|
1572
1758
|
|
|
1573
1759
|
// src/commands/moves.ts
|
|
1574
1760
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1575
|
-
import { join as
|
|
1761
|
+
import { join as join4 } from "path";
|
|
1576
1762
|
|
|
1577
1763
|
// src/flusher.ts
|
|
1578
1764
|
import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
@@ -1681,18 +1867,18 @@ function registerMovesCommands(program2) {
|
|
|
1681
1867
|
}
|
|
1682
1868
|
});
|
|
1683
1869
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
1684
|
-
const dir =
|
|
1870
|
+
const dir = join4(process.cwd(), ".prim");
|
|
1685
1871
|
if (!existsSync5(dir)) {
|
|
1686
1872
|
mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
|
|
1687
1873
|
}
|
|
1688
|
-
const file =
|
|
1874
|
+
const file = join4(process.cwd(), WORKSPACE_FILE);
|
|
1689
1875
|
writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
1690
1876
|
mode: FILE_MODE2
|
|
1691
1877
|
});
|
|
1692
1878
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
1693
1879
|
});
|
|
1694
1880
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
1695
|
-
const file =
|
|
1881
|
+
const file = join4(process.cwd(), WORKSPACE_FILE);
|
|
1696
1882
|
if (!existsSync5(file)) {
|
|
1697
1883
|
console.log("[prim] no workspace binding in cwd");
|
|
1698
1884
|
return;
|
|
@@ -1815,7 +2001,7 @@ import {
|
|
|
1815
2001
|
unlinkSync as unlinkSync5,
|
|
1816
2002
|
writeFileSync as writeFileSync5
|
|
1817
2003
|
} from "fs";
|
|
1818
|
-
import { join as
|
|
2004
|
+
import { join as join5 } from "path";
|
|
1819
2005
|
var DIR_MODE2 = 448;
|
|
1820
2006
|
var FILE_MODE3 = 384;
|
|
1821
2007
|
function ensureDir() {
|
|
@@ -1824,7 +2010,7 @@ function ensureDir() {
|
|
|
1824
2010
|
}
|
|
1825
2011
|
}
|
|
1826
2012
|
function markerPath(sessionId) {
|
|
1827
|
-
return
|
|
2013
|
+
return join5(SESSIONS_DIR, `${sessionId}.json`);
|
|
1828
2014
|
}
|
|
1829
2015
|
function registerSessionCommands(program2) {
|
|
1830
2016
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -1852,7 +2038,7 @@ function registerSessionCommands(program2) {
|
|
|
1852
2038
|
for (const f of files) {
|
|
1853
2039
|
const sessionId = f.replace(/\.json$/, "");
|
|
1854
2040
|
try {
|
|
1855
|
-
const m = JSON.parse(readFileSync6(
|
|
2041
|
+
const m = JSON.parse(readFileSync6(join5(SESSIONS_DIR, f), "utf-8"));
|
|
1856
2042
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
1857
2043
|
} catch {
|
|
1858
2044
|
}
|
|
@@ -1887,6 +2073,7 @@ var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
|
1887
2073
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
1888
2074
|
var TARGET_CANDIDATES = [
|
|
1889
2075
|
"CLAUDE.md",
|
|
2076
|
+
"AGENTS.md",
|
|
1890
2077
|
".cursor/rules",
|
|
1891
2078
|
".windsurfrules",
|
|
1892
2079
|
".github/instructions/primitive.md"
|
|
@@ -2366,6 +2553,7 @@ registerMovesCommands(program);
|
|
|
2366
2553
|
registerSessionCommands(program);
|
|
2367
2554
|
registerDecisionsCommands(program);
|
|
2368
2555
|
registerClaudeCommands(program);
|
|
2556
|
+
registerCodexCommands(program);
|
|
2369
2557
|
registerDaemonCommands(program);
|
|
2370
2558
|
registerReconcileCommands(program);
|
|
2371
2559
|
registerStatuslineCommands(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitive.ai/prim",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.18",
|
|
4
4
|
"description": "CLI for managing Primitive specs, contexts, and git hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"bin": {
|
|
31
31
|
"prim": "dist/index.js",
|
|
32
32
|
"prim-pre-commit": "dist/hooks/pre-commit.js",
|
|
33
|
+
"prim-post-commit": "dist/hooks/post-commit.js",
|
|
33
34
|
"prim-hook": "dist/hooks/prim-hook.js",
|
|
34
35
|
"prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
|
|
35
36
|
"prim-post-tool-use": "dist/hooks/post-tool-use.js",
|
|
@@ -45,9 +46,9 @@
|
|
|
45
46
|
"SKILL.md"
|
|
46
47
|
],
|
|
47
48
|
"scripts": {
|
|
48
|
-
"build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --clean",
|
|
49
|
-
"postbuild": "chmod +x ./dist/index.js ./dist/hooks/pre-commit.js ./dist/hooks/prim-hook.js ./dist/hooks/pre-tool-use.js ./dist/hooks/post-tool-use.js ./dist/hooks/session-start.js ./dist/hooks/session-end.js ./dist/daemon/server.js",
|
|
50
|
-
"dev": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --watch --clean",
|
|
49
|
+
"build": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --clean",
|
|
50
|
+
"postbuild": "chmod +x ./dist/index.js ./dist/hooks/pre-commit.js ./dist/hooks/post-commit.js ./dist/hooks/prim-hook.js ./dist/hooks/pre-tool-use.js ./dist/hooks/post-tool-use.js ./dist/hooks/session-start.js ./dist/hooks/session-end.js ./dist/daemon/server.js",
|
|
51
|
+
"dev": "tsup src/index.ts src/hooks/pre-commit.ts src/hooks/post-commit.ts src/hooks/prim-hook.ts src/hooks/pre-tool-use.ts src/hooks/post-tool-use.ts src/hooks/session-start.ts src/hooks/session-end.ts src/daemon/server.ts --format esm --watch --clean",
|
|
51
52
|
"clean": "rm -rf dist coverage",
|
|
52
53
|
"lint": "biome check src/",
|
|
53
54
|
"format": "biome check --fix src/",
|