@primitive.ai/prim 0.1.0-alpha.31 → 0.1.0-alpha.32
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 +8 -3
- package/SKILL.md +6 -6
- package/dist/{chunk-7GHOFNJ2.js → chunk-S2O4P4A3.js} +4 -4
- package/dist/chunk-TCDUH7AN.js +35 -0
- package/dist/hooks/post-commit.js +1 -1
- package/dist/hooks/post-tool-use.js +18 -7
- package/dist/hooks/pre-tool-use.js +64 -17
- package/dist/hooks/prim-hook.js +7 -5
- package/dist/hooks/session-end.js +9 -1
- package/dist/hooks/session-start.js +10 -5
- package/dist/index.js +450 -79
- package/package.json +3 -2
- package/dist/chunk-7YRBACIE.js +0 -9
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ One command does the whole install — auth, session hooks, daemon, git hooks,
|
|
|
28
28
|
skill, and the welcome:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
prim setup # add --agent codex
|
|
31
|
+
prim setup # add --agent codex or --agent hermes, --no-daemon to skip the daemon
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Or run the steps individually:
|
|
@@ -38,7 +38,7 @@ Or run the steps individually:
|
|
|
38
38
|
prim auth login
|
|
39
39
|
|
|
40
40
|
# 2. Wire the session hooks (decision capture + conflict gate + presence)
|
|
41
|
-
prim claude install # or: prim codex install
|
|
41
|
+
prim claude install # or: prim codex install / prim hermes install
|
|
42
42
|
|
|
43
43
|
# 3. Start the companion daemon (latency + team presence)
|
|
44
44
|
prim daemon start
|
|
@@ -74,6 +74,7 @@ and the manual fallback — live in [`setup.md`](./setup.md).
|
|
|
74
74
|
```bash
|
|
75
75
|
prim setup # Run the whole install in one shot
|
|
76
76
|
prim setup --agent codex # Same, for OpenAI Codex
|
|
77
|
+
prim setup --agent hermes # Same, for Hermes Agent (global-only config)
|
|
77
78
|
prim setup --no-daemon # Skip the companion daemon
|
|
78
79
|
```
|
|
79
80
|
|
|
@@ -99,12 +100,16 @@ self-resolves the CLI at run time (PATH, then a local install, then
|
|
|
99
100
|
|
|
100
101
|
Installs into the current project by default — the repo's `.claude/settings.json`
|
|
101
102
|
/ `.codex/hooks.json`, resolved from the git root (so any subdirectory works);
|
|
102
|
-
pass `--scope user` to install machine-wide.
|
|
103
|
+
pass `--scope user` to install machine-wide. Hermes is the exception: it reads
|
|
104
|
+
shell hooks only from the global `~/.hermes/config.yaml`, so `prim hermes install`
|
|
105
|
+
is always user-scoped — and prim merges in place, leaving the rest of that file
|
|
106
|
+
(providers, models, your own hooks) untouched.
|
|
103
107
|
|
|
104
108
|
```bash
|
|
105
109
|
prim claude install # Install Claude Code hooks (project scope; uninstall / status)
|
|
106
110
|
prim claude install --scope user # Install machine-wide instead
|
|
107
111
|
prim codex install # Install OpenAI Codex hooks (project scope)
|
|
112
|
+
prim hermes install # Install Hermes Agent hooks (global ~/.hermes/config.yaml)
|
|
108
113
|
```
|
|
109
114
|
|
|
110
115
|
### Daemon
|
package/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ description: Use the prim CLI for Primitive's decision graph — passive decisio
|
|
|
11
11
|
|
|
12
12
|
As your team codes, prim passively captures the **decisions** you make -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. When a later change conflicts with a load-bearing prior decision, prim **gates** the edit and surfaces the decision for review.
|
|
13
13
|
|
|
14
|
-
You never invoke capture. It runs automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code)
|
|
14
|
+
You never invoke capture. It 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). Your job is to **respond** to the gate, **read** the graph before load-bearing edits, and **answer** the occasional rationale confirmation.
|
|
15
15
|
|
|
16
16
|
## Auth
|
|
17
17
|
|
|
@@ -33,10 +33,10 @@ The CLI auto-refreshes a still-valid session from the stored refresh token (proa
|
|
|
33
33
|
|
|
34
34
|
## Heed the conflict gate
|
|
35
35
|
|
|
36
|
-
Before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch) a PreToolUse hook scores the target file against the graph:
|
|
36
|
+
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:
|
|
37
37
|
|
|
38
38
|
- **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.
|
|
39
|
-
- **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.
|
|
39
|
+
- **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. Hermes has no soft-confirm tier, so a would-be `ask` arrives as a **deny** carrying the same reconcile directive: reconcile and retry, or set `PRIM_HOOK_MODE=warn` to downgrade it to context-only.
|
|
40
40
|
- **"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.
|
|
41
41
|
|
|
42
42
|
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.
|
|
@@ -44,7 +44,7 @@ The gate fail-opens on its *own* infrastructure errors (no daemon, network blip,
|
|
|
44
44
|
## Read the graph before large or load-bearing edits
|
|
45
45
|
|
|
46
46
|
- `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.
|
|
47
|
-
- `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.
|
|
47
|
+
- `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.
|
|
48
48
|
- `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.
|
|
49
49
|
|
|
50
50
|
## Reconcile and the verdict footer
|
|
@@ -116,7 +116,7 @@ npx --yes @primitive.ai/prim hooks uninstall
|
|
|
116
116
|
|
|
117
117
|
Under `CI=1` (or with `--non-interactive`), `hooks install` fails fast in a Husky repo unless `--yes` or `--target` is set; the error names both escapes. `hooks uninstall` only removes the `.git/hooks` copies — if a hook was installed into `.husky/`, remove the prim block from that file manually. To suppress the hooks for one commit, use `git commit --no-verify`.
|
|
118
118
|
|
|
119
|
-
These git hooks are separate from the **session hooks** (`claude install` / `codex install`) that drive in-session capture and the conflict gate.
|
|
119
|
+
These git hooks are separate from the **session hooks** (`claude install` / `codex install` / `hermes install`) that drive in-session capture and the conflict gate.
|
|
120
120
|
|
|
121
121
|
## Output formats
|
|
122
122
|
|
|
@@ -137,7 +137,7 @@ Examples:
|
|
|
137
137
|
- **An "unavailable" / "not verified" gate or check is not an all-clear.** Treat constraints as UNKNOWN and proceed deliberately; never read the silence as approval.
|
|
138
138
|
- **A `deny` means a real prior decision conflicts.** Reconcile only when you genuinely intend to override it; otherwise pick an approach that respects it.
|
|
139
139
|
- **Reconcile bypasses are single-use and short-lived.** One bypass clears your *next* edit to the governed file; it is not a standing override.
|
|
140
|
-
- **Capture of your coding activity is automatic, never manual.** If decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status`) and the daemon is running — don't try to inject moves by hand. (Deliberately *authoring* a decision the user asks you to record is a separate, supported path — `decisions create`, above.)
|
|
140
|
+
- **Capture of your coding activity is automatic, never manual.** If decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status` / `hermes status`) and the daemon is running — don't try to inject moves by hand. (Deliberately *authoring* a decision the user asks you to record is a separate, supported path — `decisions create`, above.)
|
|
141
141
|
- **Don't fabricate rationale on a confirmation.** If you don't know why a decision was made, say so rather than guessing.
|
|
142
142
|
|
|
143
143
|
## After each task
|
|
@@ -19,10 +19,10 @@ function toMove(parsed, cliVersion, agent = "claude_code") {
|
|
|
19
19
|
osPlatform: platform()
|
|
20
20
|
},
|
|
21
21
|
envelopeVersion: ENVELOPE_VERSION,
|
|
22
|
-
// Stamp the producer
|
|
23
|
-
// backend defaults an absent value to "claude_code"),
|
|
24
|
-
// Claude wire shape byte-identical.
|
|
25
|
-
...agent === "
|
|
22
|
+
// Stamp the producer for non-Claude agents (codex, hermes); Claude Code
|
|
23
|
+
// moves omit it (the backend defaults an absent value to "claude_code"),
|
|
24
|
+
// keeping the Claude wire shape byte-identical.
|
|
25
|
+
...agent === "claude_code" ? {} : { producer: agent }
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
function toCommitMove(commit, cliVersion, cwd) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/hooks/agent.ts
|
|
2
|
+
function parseAgent(argv) {
|
|
3
|
+
const i = argv.indexOf("--agent");
|
|
4
|
+
const value = i !== -1 ? argv[i + 1] : void 0;
|
|
5
|
+
if (value === "codex" || value === "hermes") {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
return "claude_code";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/hooks/normalize.ts
|
|
12
|
+
var HERMES_EVENT_MAP = {
|
|
13
|
+
on_session_start: "SessionStart",
|
|
14
|
+
on_session_end: "SessionEnd",
|
|
15
|
+
pre_llm_call: "UserPromptSubmit",
|
|
16
|
+
post_llm_call: "Stop",
|
|
17
|
+
pre_tool_call: "PreToolUse",
|
|
18
|
+
post_tool_call: "PostToolUse",
|
|
19
|
+
subagent_stop: "SubagentStop"
|
|
20
|
+
};
|
|
21
|
+
function normalizeEnvelope(parsed, agent) {
|
|
22
|
+
if (agent !== "hermes") {
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
const raw = parsed.hook_event_name;
|
|
26
|
+
if (typeof raw === "string" && raw in HERMES_EVENT_MAP) {
|
|
27
|
+
return { ...parsed, hook_event_name: HERMES_EVENT_MAP[raw] };
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
parseAgent,
|
|
34
|
+
normalizeEnvelope
|
|
35
|
+
};
|
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
} from "../chunk-6LAQVM26.js";
|
|
9
9
|
import {
|
|
10
10
|
toMove
|
|
11
|
-
} from "../chunk-
|
|
12
|
-
import {
|
|
13
|
-
parseAgent
|
|
14
|
-
} from "../chunk-7YRBACIE.js";
|
|
11
|
+
} from "../chunk-S2O4P4A3.js";
|
|
15
12
|
import {
|
|
16
13
|
getClient
|
|
17
14
|
} from "../chunk-26VA3ADF.js";
|
|
15
|
+
import {
|
|
16
|
+
normalizeEnvelope,
|
|
17
|
+
parseAgent
|
|
18
|
+
} from "../chunk-TCDUH7AN.js";
|
|
18
19
|
|
|
19
20
|
// src/hooks/post-tool-use.ts
|
|
20
21
|
import { readFileSync } from "fs";
|
|
@@ -42,6 +43,16 @@ var STDIN_TIMEOUT_MS = 1e3;
|
|
|
42
43
|
var INGEST_TIMEOUT_MS = 4e3;
|
|
43
44
|
var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
44
45
|
var CODEX_EDITING_TOOLS = /* @__PURE__ */ new Set(["apply_patch"]);
|
|
46
|
+
var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
|
|
47
|
+
function editingToolsFor(agent) {
|
|
48
|
+
if (agent === "codex") {
|
|
49
|
+
return CODEX_EDITING_TOOLS;
|
|
50
|
+
}
|
|
51
|
+
if (agent === "hermes") {
|
|
52
|
+
return HERMES_EDITING_TOOLS;
|
|
53
|
+
}
|
|
54
|
+
return EDITING_TOOLS;
|
|
55
|
+
}
|
|
45
56
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
46
57
|
function resolveCliVersion() {
|
|
47
58
|
try {
|
|
@@ -86,6 +97,7 @@ async function ingestMove(move) {
|
|
|
86
97
|
);
|
|
87
98
|
}
|
|
88
99
|
async function main() {
|
|
100
|
+
const agent = parseAgent(process.argv);
|
|
89
101
|
let raw;
|
|
90
102
|
try {
|
|
91
103
|
raw = await readStdin();
|
|
@@ -95,7 +107,7 @@ async function main() {
|
|
|
95
107
|
}
|
|
96
108
|
let parsed;
|
|
97
109
|
try {
|
|
98
|
-
parsed = JSON.parse(raw);
|
|
110
|
+
parsed = normalizeEnvelope(JSON.parse(raw), agent);
|
|
99
111
|
} catch {
|
|
100
112
|
emit();
|
|
101
113
|
return;
|
|
@@ -106,8 +118,7 @@ async function main() {
|
|
|
106
118
|
return;
|
|
107
119
|
}
|
|
108
120
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
109
|
-
const
|
|
110
|
-
const editingTools = agent === "codex" ? CODEX_EDITING_TOOLS : EDITING_TOOLS;
|
|
121
|
+
const editingTools = editingToolsFor(agent);
|
|
111
122
|
if (!editingTools.has(toolName)) {
|
|
112
123
|
emit();
|
|
113
124
|
return;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
parseAgent
|
|
4
|
-
} from "../chunk-7YRBACIE.js";
|
|
5
2
|
import {
|
|
6
3
|
getClient,
|
|
7
4
|
getSiteUrl
|
|
@@ -9,6 +6,10 @@ import {
|
|
|
9
6
|
import {
|
|
10
7
|
daemonRequest
|
|
11
8
|
} from "../chunk-UTKQTZHL.js";
|
|
9
|
+
import {
|
|
10
|
+
normalizeEnvelope,
|
|
11
|
+
parseAgent
|
|
12
|
+
} from "../chunk-TCDUH7AN.js";
|
|
12
13
|
|
|
13
14
|
// src/hooks/pre-tool-use-scoring.ts
|
|
14
15
|
import { isAbsolute, relative, sep } from "path";
|
|
@@ -47,7 +48,7 @@ function unverifiedNote(results) {
|
|
|
47
48
|
}
|
|
48
49
|
return causes.map((c) => `[primitive] ${c}`).join("\n");
|
|
49
50
|
}
|
|
50
|
-
function buildHookOutput(aggregate, results,
|
|
51
|
+
function buildHookOutput(aggregate, results, agent2 = "claude_code") {
|
|
51
52
|
if (aggregate === "deny") {
|
|
52
53
|
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)";
|
|
53
54
|
return {
|
|
@@ -58,7 +59,7 @@ function buildHookOutput(aggregate, results, agent = "claude_code") {
|
|
|
58
59
|
}
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
|
-
if (
|
|
62
|
+
if (agent2 === "codex" && aggregate === "ask") {
|
|
62
63
|
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
63
64
|
const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
|
|
64
65
|
const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
|
|
@@ -113,8 +114,21 @@ function failOpenOutput() {
|
|
|
113
114
|
}
|
|
114
115
|
};
|
|
115
116
|
}
|
|
117
|
+
function buildHermesOutput(aggregate, results) {
|
|
118
|
+
if (aggregate !== "deny" && aggregate !== "ask") {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
const reason = results.filter((r) => r.verdict === "deny" || r.verdict === "ask").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
122
|
+
const directive = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
|
|
123
|
+
const message = [reason, directive].filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
|
|
124
|
+
return { action: "block", message };
|
|
125
|
+
}
|
|
126
|
+
function failOpenHermes() {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
116
129
|
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
117
130
|
var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
|
|
131
|
+
var MOVE_FILE_RE = /^\*\*\* Move File:\s*(?<src>.+?)\s*->\s*(?<dst>.+?)\s*$/;
|
|
118
132
|
var LINE_SPLIT_RE = /\r?\n/;
|
|
119
133
|
function parseApplyPatchPaths(command) {
|
|
120
134
|
const paths = /* @__PURE__ */ new Set();
|
|
@@ -122,6 +136,14 @@ function parseApplyPatchPaths(command) {
|
|
|
122
136
|
const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path?.trim();
|
|
123
137
|
if (path) {
|
|
124
138
|
paths.add(path);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const move = MOVE_FILE_RE.exec(line)?.groups;
|
|
142
|
+
if (move?.src) {
|
|
143
|
+
paths.add(move.src.trim());
|
|
144
|
+
if (move.dst) {
|
|
145
|
+
paths.add(move.dst.trim());
|
|
146
|
+
}
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
149
|
return Array.from(paths);
|
|
@@ -136,10 +158,27 @@ function extractCodexFilePaths(toolName, toolInput) {
|
|
|
136
158
|
const command = toolInput.command;
|
|
137
159
|
return typeof command === "string" ? parseApplyPatchPaths(command) : [];
|
|
138
160
|
}
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
|
|
162
|
+
function extractHermesFilePaths(toolName, toolInput) {
|
|
163
|
+
if (!HERMES_EDITING_TOOLS.has(toolName)) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const input = toolInput;
|
|
170
|
+
if (toolName === "patch" && input.mode === "patch") {
|
|
171
|
+
return typeof input.patch === "string" ? parseApplyPatchPaths(input.patch) : [];
|
|
172
|
+
}
|
|
173
|
+
return typeof input.path === "string" && input.path.length > 0 ? [input.path] : [];
|
|
174
|
+
}
|
|
175
|
+
function extractFilePaths(toolName, toolInput, agent2 = "claude_code") {
|
|
176
|
+
if (agent2 === "codex") {
|
|
141
177
|
return extractCodexFilePaths(toolName, toolInput);
|
|
142
178
|
}
|
|
179
|
+
if (agent2 === "hermes") {
|
|
180
|
+
return extractHermesFilePaths(toolName, toolInput);
|
|
181
|
+
}
|
|
143
182
|
if (!SUPPORTED_TOOLS.has(toolName)) {
|
|
144
183
|
return [];
|
|
145
184
|
}
|
|
@@ -193,10 +232,14 @@ async function readStdin() {
|
|
|
193
232
|
});
|
|
194
233
|
});
|
|
195
234
|
}
|
|
235
|
+
var agent = parseAgent(process.argv);
|
|
196
236
|
function emit(output) {
|
|
197
237
|
process.stdout.write(`${JSON.stringify(output)}
|
|
198
238
|
`);
|
|
199
239
|
}
|
|
240
|
+
function failOpen() {
|
|
241
|
+
return agent === "hermes" ? failOpenHermes() : failOpenOutput();
|
|
242
|
+
}
|
|
200
243
|
async function checkOneFile(file) {
|
|
201
244
|
const fromDaemon = await daemonRequest(
|
|
202
245
|
"conflict_check",
|
|
@@ -220,47 +263,51 @@ async function main() {
|
|
|
220
263
|
try {
|
|
221
264
|
raw = await readStdin();
|
|
222
265
|
} catch {
|
|
223
|
-
emit(
|
|
266
|
+
emit(failOpen());
|
|
224
267
|
return;
|
|
225
268
|
}
|
|
226
269
|
let envelope;
|
|
227
270
|
try {
|
|
228
|
-
envelope =
|
|
271
|
+
envelope = normalizeEnvelope(
|
|
272
|
+
JSON.parse(raw),
|
|
273
|
+
agent
|
|
274
|
+
);
|
|
229
275
|
} catch {
|
|
230
|
-
emit(
|
|
276
|
+
emit(failOpen());
|
|
231
277
|
return;
|
|
232
278
|
}
|
|
233
279
|
if (envelope.hook_event_name !== "PreToolUse") {
|
|
234
|
-
emit(
|
|
280
|
+
emit(failOpen());
|
|
235
281
|
return;
|
|
236
282
|
}
|
|
237
283
|
const env = process.env;
|
|
238
284
|
const mode = readHookMode(env);
|
|
239
285
|
if (mode === "off") {
|
|
240
|
-
emit(
|
|
286
|
+
emit(failOpen());
|
|
241
287
|
return;
|
|
242
288
|
}
|
|
243
289
|
const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
|
|
244
290
|
const cwd = typeof envelope.cwd === "string" && envelope.cwd.length > 0 ? envelope.cwd : process.cwd();
|
|
245
|
-
const agent = parseAgent(process.argv);
|
|
246
291
|
const files = extractFilePaths(toolName, envelope.tool_input, agent).map(
|
|
247
292
|
(f) => toRepoRelative(f, cwd)
|
|
248
293
|
);
|
|
249
294
|
if (files.length === 0) {
|
|
250
|
-
emit(
|
|
295
|
+
emit(failOpen());
|
|
251
296
|
return;
|
|
252
297
|
}
|
|
253
298
|
let results;
|
|
254
299
|
try {
|
|
255
300
|
results = await Promise.all(files.map((f) => checkOneFile(f)));
|
|
256
301
|
} catch {
|
|
257
|
-
emit(
|
|
302
|
+
emit(failOpen());
|
|
258
303
|
return;
|
|
259
304
|
}
|
|
260
305
|
const rawAggregate = aggregateCheckResults(results);
|
|
261
306
|
const aggregate = demoteForMode(rawAggregate, mode);
|
|
262
|
-
emit(
|
|
307
|
+
emit(
|
|
308
|
+
agent === "hermes" ? buildHermesOutput(aggregate, results) : buildHookOutput(aggregate, results, agent)
|
|
309
|
+
);
|
|
263
310
|
}
|
|
264
311
|
main().catch(() => {
|
|
265
|
-
emit(
|
|
312
|
+
emit(failOpen());
|
|
266
313
|
});
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
shouldFlushAfter,
|
|
11
11
|
toMove
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-S2O4P4A3.js";
|
|
13
|
+
import "../chunk-26VA3ADF.js";
|
|
13
14
|
import {
|
|
15
|
+
normalizeEnvelope,
|
|
14
16
|
parseAgent
|
|
15
|
-
} from "../chunk-
|
|
16
|
-
import "../chunk-26VA3ADF.js";
|
|
17
|
+
} from "../chunk-TCDUH7AN.js";
|
|
17
18
|
|
|
18
19
|
// src/hooks/prim-hook.ts
|
|
19
20
|
import { spawn } from "child_process";
|
|
@@ -37,10 +38,11 @@ function spawnBackgroundFlush() {
|
|
|
37
38
|
}).unref();
|
|
38
39
|
}
|
|
39
40
|
try {
|
|
41
|
+
const agent = parseAgent(process.argv);
|
|
40
42
|
const raw = readFileSync(0, "utf-8");
|
|
41
|
-
const parsed = JSON.parse(raw);
|
|
43
|
+
const parsed = normalizeEnvelope(JSON.parse(raw), agent);
|
|
42
44
|
const cwd = parsed.cwd ?? process.cwd();
|
|
43
|
-
const base = toMove(parsed, resolveCliVersion(),
|
|
45
|
+
const base = toMove(parsed, resolveCliVersion(), agent);
|
|
44
46
|
const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
|
|
45
47
|
const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
|
|
46
48
|
appendMove(move, orgId);
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
daemonRequest
|
|
4
4
|
} from "../chunk-UTKQTZHL.js";
|
|
5
|
+
import {
|
|
6
|
+
normalizeEnvelope,
|
|
7
|
+
parseAgent
|
|
8
|
+
} from "../chunk-TCDUH7AN.js";
|
|
5
9
|
|
|
6
10
|
// src/hooks/session-end.ts
|
|
7
11
|
var STDIN_TIMEOUT_MS = 1e3;
|
|
@@ -27,6 +31,7 @@ function emit() {
|
|
|
27
31
|
process.stdout.write("{}\n");
|
|
28
32
|
}
|
|
29
33
|
async function main() {
|
|
34
|
+
const agent = parseAgent(process.argv);
|
|
30
35
|
let raw;
|
|
31
36
|
try {
|
|
32
37
|
raw = await readStdin();
|
|
@@ -36,7 +41,10 @@ async function main() {
|
|
|
36
41
|
}
|
|
37
42
|
let envelope;
|
|
38
43
|
try {
|
|
39
|
-
envelope =
|
|
44
|
+
envelope = normalizeEnvelope(
|
|
45
|
+
JSON.parse(raw),
|
|
46
|
+
agent
|
|
47
|
+
);
|
|
40
48
|
} catch {
|
|
41
49
|
emit();
|
|
42
50
|
return;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
parseAgent
|
|
4
|
-
} from "../chunk-7YRBACIE.js";
|
|
5
2
|
import {
|
|
6
3
|
getSiteUrl
|
|
7
4
|
} from "../chunk-26VA3ADF.js";
|
|
8
5
|
import {
|
|
9
6
|
daemonRequest
|
|
10
7
|
} from "../chunk-UTKQTZHL.js";
|
|
8
|
+
import {
|
|
9
|
+
normalizeEnvelope,
|
|
10
|
+
parseAgent
|
|
11
|
+
} from "../chunk-TCDUH7AN.js";
|
|
11
12
|
|
|
12
13
|
// src/hooks/session-start.ts
|
|
13
14
|
var STDIN_TIMEOUT_MS = 1e3;
|
|
@@ -44,6 +45,7 @@ function emit(additionalContext) {
|
|
|
44
45
|
`);
|
|
45
46
|
}
|
|
46
47
|
async function main() {
|
|
48
|
+
const agent = parseAgent(process.argv);
|
|
47
49
|
let raw;
|
|
48
50
|
try {
|
|
49
51
|
raw = await readStdin();
|
|
@@ -53,7 +55,10 @@ async function main() {
|
|
|
53
55
|
}
|
|
54
56
|
let envelope;
|
|
55
57
|
try {
|
|
56
|
-
envelope =
|
|
58
|
+
envelope = normalizeEnvelope(
|
|
59
|
+
JSON.parse(raw),
|
|
60
|
+
agent
|
|
61
|
+
);
|
|
57
62
|
} catch {
|
|
58
63
|
emit();
|
|
59
64
|
return;
|
|
@@ -71,7 +76,7 @@ async function main() {
|
|
|
71
76
|
{ sessionId: envelope.session_id },
|
|
72
77
|
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
73
78
|
);
|
|
74
|
-
if (
|
|
79
|
+
if (agent === "codex") {
|
|
75
80
|
const snapshot = await daemonRequest(
|
|
76
81
|
"status_snapshot",
|
|
77
82
|
// callerEnv: a cross-env daemon withholds onlineCount, so a prod Codex
|
package/dist/index.js
CHANGED
|
@@ -36,8 +36,8 @@ import {
|
|
|
36
36
|
} from "./chunk-UTKQTZHL.js";
|
|
37
37
|
|
|
38
38
|
// src/index.ts
|
|
39
|
-
import { readFileSync as
|
|
40
|
-
import { dirname as
|
|
39
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
40
|
+
import { dirname as dirname7, resolve as resolve4 } from "path";
|
|
41
41
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
42
42
|
import { Command } from "commander";
|
|
43
43
|
import updateNotifier from "update-notifier";
|
|
@@ -1371,6 +1371,8 @@ function authorLabel(row) {
|
|
|
1371
1371
|
return "Your Claude Code";
|
|
1372
1372
|
case "codex":
|
|
1373
1373
|
return "Your Codex";
|
|
1374
|
+
case "hermes":
|
|
1375
|
+
return "Your Hermes";
|
|
1374
1376
|
case "chat":
|
|
1375
1377
|
return "Your chat";
|
|
1376
1378
|
case "spec_edit":
|
|
@@ -1998,9 +2000,351 @@ function registerDoctorCommands(program2) {
|
|
|
1998
2000
|
});
|
|
1999
2001
|
}
|
|
2000
2002
|
|
|
2003
|
+
// src/commands/hermes-install.ts
|
|
2004
|
+
import {
|
|
2005
|
+
chmodSync,
|
|
2006
|
+
closeSync as closeSync3,
|
|
2007
|
+
existsSync as existsSync6,
|
|
2008
|
+
fsyncSync as fsyncSync2,
|
|
2009
|
+
mkdirSync as mkdirSync4,
|
|
2010
|
+
openSync as openSync3,
|
|
2011
|
+
readFileSync as readFileSync5,
|
|
2012
|
+
renameSync as renameSync2,
|
|
2013
|
+
rmSync as rmSync2,
|
|
2014
|
+
writeFileSync as writeFileSync3
|
|
2015
|
+
} from "fs";
|
|
2016
|
+
import { homedir as homedir4 } from "os";
|
|
2017
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
2018
|
+
import { Document, parseDocument, stringify } from "yaml";
|
|
2019
|
+
var CAPTURE_BIN3 = "prim-hook";
|
|
2020
|
+
var GATE_BIN3 = "prim-pre-tool-use";
|
|
2021
|
+
var POST_TOOL_USE_BIN3 = "prim-post-tool-use";
|
|
2022
|
+
var SESSION_START_BIN3 = "prim-session-start";
|
|
2023
|
+
var SESSION_END_BIN2 = "prim-session-end";
|
|
2024
|
+
var HERMES_ARGS = "--agent hermes";
|
|
2025
|
+
var EDIT_MATCHER = "write_file|patch";
|
|
2026
|
+
var JSON_INDENT3 = 2;
|
|
2027
|
+
var SHIM_MODE = 493;
|
|
2028
|
+
var PRIM_BINS3 = [
|
|
2029
|
+
CAPTURE_BIN3,
|
|
2030
|
+
GATE_BIN3,
|
|
2031
|
+
POST_TOOL_USE_BIN3,
|
|
2032
|
+
SESSION_START_BIN3,
|
|
2033
|
+
SESSION_END_BIN2
|
|
2034
|
+
];
|
|
2035
|
+
function hermesHome() {
|
|
2036
|
+
return process.env.HERMES_HOME ?? join5(homedir4(), ".hermes");
|
|
2037
|
+
}
|
|
2038
|
+
function configPath() {
|
|
2039
|
+
return join5(hermesHome(), "config.yaml");
|
|
2040
|
+
}
|
|
2041
|
+
function shimPath() {
|
|
2042
|
+
return join5(hermesHome(), "agent-hooks", "prim-shim.sh");
|
|
2043
|
+
}
|
|
2044
|
+
var SHIM_SCRIPT = `#!/bin/sh
|
|
2045
|
+
# prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
|
|
2046
|
+
# with shell=False, so the PATH \u2192 local node_modules \u2192 npx @latest resolution a
|
|
2047
|
+
# shell does for the Claude Code / Codex installs lives here instead.
|
|
2048
|
+
bin="$1"
|
|
2049
|
+
shift
|
|
2050
|
+
if command -v "$bin" >/dev/null 2>&1; then
|
|
2051
|
+
exec "$bin" "$@"
|
|
2052
|
+
fi
|
|
2053
|
+
if [ -x "./node_modules/.bin/$bin" ]; then
|
|
2054
|
+
exec "./node_modules/.bin/$bin" "$@"
|
|
2055
|
+
fi
|
|
2056
|
+
exec npx --yes -p @primitive.ai/prim@latest "$bin" "$@"
|
|
2057
|
+
`;
|
|
2058
|
+
function commandFor(bin) {
|
|
2059
|
+
return `"${shimPath()}" ${bin} ${HERMES_ARGS}`;
|
|
2060
|
+
}
|
|
2061
|
+
function commandUsesBin(command, bin) {
|
|
2062
|
+
return new RegExp(`prim-shim\\.sh"?\\s+${bin}\\s`).test(command);
|
|
2063
|
+
}
|
|
2064
|
+
var CAPTURE_EVENTS2 = [
|
|
2065
|
+
"on_session_start",
|
|
2066
|
+
"pre_llm_call",
|
|
2067
|
+
"pre_tool_call",
|
|
2068
|
+
"post_tool_call",
|
|
2069
|
+
"post_llm_call",
|
|
2070
|
+
"on_session_end",
|
|
2071
|
+
"subagent_stop"
|
|
2072
|
+
];
|
|
2073
|
+
var GATE_TIMEOUT_SECONDS = 10;
|
|
2074
|
+
var REGISTRATIONS2 = [
|
|
2075
|
+
...CAPTURE_EVENTS2.map((event) => ({ event, bin: CAPTURE_BIN3 })),
|
|
2076
|
+
{ event: "pre_tool_call", matcher: EDIT_MATCHER, bin: GATE_BIN3, timeout: GATE_TIMEOUT_SECONDS },
|
|
2077
|
+
{ event: "post_tool_call", matcher: EDIT_MATCHER, bin: POST_TOOL_USE_BIN3 },
|
|
2078
|
+
{ event: "on_session_start", bin: SESSION_START_BIN3 },
|
|
2079
|
+
{ event: "on_session_end", bin: SESSION_END_BIN2 }
|
|
2080
|
+
];
|
|
2081
|
+
function entryFor(reg) {
|
|
2082
|
+
const command = commandFor(reg.bin);
|
|
2083
|
+
const entry = reg.matcher ? { matcher: reg.matcher, command } : { command };
|
|
2084
|
+
if (reg.timeout !== void 0) {
|
|
2085
|
+
entry.timeout = reg.timeout;
|
|
2086
|
+
}
|
|
2087
|
+
return entry;
|
|
2088
|
+
}
|
|
2089
|
+
function stripBin(list, bin) {
|
|
2090
|
+
return list.filter((e) => !commandUsesBin(e.command, bin));
|
|
2091
|
+
}
|
|
2092
|
+
function ensureReg(list, reg, force) {
|
|
2093
|
+
const entry = entryFor(reg);
|
|
2094
|
+
const present = list.some(
|
|
2095
|
+
(e) => e.command === entry.command && (e.matcher ?? "") === (entry.matcher ?? "") && (e.timeout ?? null) === (entry.timeout ?? null)
|
|
2096
|
+
);
|
|
2097
|
+
if (present && !force) {
|
|
2098
|
+
return list;
|
|
2099
|
+
}
|
|
2100
|
+
return [...stripBin(list, reg.bin), entry];
|
|
2101
|
+
}
|
|
2102
|
+
function applyInstall3(hooks, force) {
|
|
2103
|
+
const next = { ...hooks };
|
|
2104
|
+
for (const reg of REGISTRATIONS2) {
|
|
2105
|
+
next[reg.event] = ensureReg(next[reg.event] ?? [], reg, force);
|
|
2106
|
+
}
|
|
2107
|
+
return next;
|
|
2108
|
+
}
|
|
2109
|
+
function applyUninstall3(hooks) {
|
|
2110
|
+
const next = {};
|
|
2111
|
+
for (const [event, list] of Object.entries(hooks)) {
|
|
2112
|
+
let kept = list;
|
|
2113
|
+
for (const bin of PRIM_BINS3) {
|
|
2114
|
+
kept = stripBin(kept, bin);
|
|
2115
|
+
}
|
|
2116
|
+
if (kept.length > 0) {
|
|
2117
|
+
next[event] = kept;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return next;
|
|
2121
|
+
}
|
|
2122
|
+
function isGateInstalled3(hooks) {
|
|
2123
|
+
return (hooks.pre_tool_call ?? []).some((e) => commandUsesBin(e.command, GATE_BIN3));
|
|
2124
|
+
}
|
|
2125
|
+
function isCaptureInstalled(hooks) {
|
|
2126
|
+
return CAPTURE_EVENTS2.some(
|
|
2127
|
+
(event) => (hooks[event] ?? []).some((e) => commandUsesBin(e.command, CAPTURE_BIN3))
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
function readDoc(path) {
|
|
2131
|
+
if (!existsSync6(path)) {
|
|
2132
|
+
return new Document();
|
|
2133
|
+
}
|
|
2134
|
+
const raw = readFileSync5(path, "utf-8");
|
|
2135
|
+
return raw.trim().length === 0 ? new Document() : parseDocument(raw);
|
|
2136
|
+
}
|
|
2137
|
+
function readHooks(doc) {
|
|
2138
|
+
const root = doc.toJS();
|
|
2139
|
+
const hooks = root?.hooks;
|
|
2140
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
2141
|
+
return {};
|
|
2142
|
+
}
|
|
2143
|
+
const out = {};
|
|
2144
|
+
for (const [event, list] of Object.entries(hooks)) {
|
|
2145
|
+
if (!Array.isArray(list)) {
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
const entries = list.filter(
|
|
2149
|
+
(e) => typeof e === "object" && e !== null && typeof e.command === "string"
|
|
2150
|
+
);
|
|
2151
|
+
if (entries.length > 0) {
|
|
2152
|
+
out[event] = entries;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
function locateHooksBlock(raw) {
|
|
2158
|
+
const lines = raw.split("\n");
|
|
2159
|
+
let offset = 0;
|
|
2160
|
+
let start = -1;
|
|
2161
|
+
let end = raw.length;
|
|
2162
|
+
for (const line of lines) {
|
|
2163
|
+
const lineStart = offset;
|
|
2164
|
+
offset += line.length + 1;
|
|
2165
|
+
if (start === -1) {
|
|
2166
|
+
if (/^hooks:(\s|$)/.test(line)) {
|
|
2167
|
+
start = lineStart;
|
|
2168
|
+
}
|
|
2169
|
+
} else if (/^\S/.test(line) && !line.startsWith("#")) {
|
|
2170
|
+
end = lineStart;
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
return start === -1 ? null : { start, end };
|
|
2175
|
+
}
|
|
2176
|
+
function serializeHooks(hooks) {
|
|
2177
|
+
if (Object.keys(hooks).length === 0) {
|
|
2178
|
+
return "";
|
|
2179
|
+
}
|
|
2180
|
+
return stringify({ hooks }, { lineWidth: 0 });
|
|
2181
|
+
}
|
|
2182
|
+
function spliceHooks(raw, hooks) {
|
|
2183
|
+
const block = serializeHooks(hooks);
|
|
2184
|
+
const loc = locateHooksBlock(raw);
|
|
2185
|
+
if (loc) {
|
|
2186
|
+
return raw.slice(0, loc.start) + block + raw.slice(loc.end);
|
|
2187
|
+
}
|
|
2188
|
+
if (block.length === 0) {
|
|
2189
|
+
return raw;
|
|
2190
|
+
}
|
|
2191
|
+
const sep = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
|
|
2192
|
+
return `${raw}${sep}${block}`;
|
|
2193
|
+
}
|
|
2194
|
+
function hasAutoAccept(raw) {
|
|
2195
|
+
return /^hooks_auto_accept:/m.test(raw);
|
|
2196
|
+
}
|
|
2197
|
+
function setAutoAccept(raw) {
|
|
2198
|
+
if (hasAutoAccept(raw)) {
|
|
2199
|
+
return raw.replace(/^hooks_auto_accept:.*$/m, "hooks_auto_accept: true");
|
|
2200
|
+
}
|
|
2201
|
+
const sep = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
|
|
2202
|
+
return `${raw}${sep}hooks_auto_accept: true
|
|
2203
|
+
`;
|
|
2204
|
+
}
|
|
2205
|
+
function atomicWriteFile(path, content) {
|
|
2206
|
+
const dir = dirname4(path);
|
|
2207
|
+
if (!existsSync6(dir)) {
|
|
2208
|
+
mkdirSync4(dir, { recursive: true });
|
|
2209
|
+
}
|
|
2210
|
+
const tmp = `${path}.tmp.${String(process.pid)}`;
|
|
2211
|
+
writeFileSync3(tmp, content, "utf-8");
|
|
2212
|
+
const fd = openSync3(tmp, "r+");
|
|
2213
|
+
try {
|
|
2214
|
+
fsyncSync2(fd);
|
|
2215
|
+
} finally {
|
|
2216
|
+
closeSync3(fd);
|
|
2217
|
+
}
|
|
2218
|
+
renameSync2(tmp, path);
|
|
2219
|
+
}
|
|
2220
|
+
function mergeKeepsYamlValid(before, after) {
|
|
2221
|
+
return parseDocument(after).errors.length <= parseDocument(before).errors.length;
|
|
2222
|
+
}
|
|
2223
|
+
function assertMergeValid(before, after) {
|
|
2224
|
+
if (!mergeKeepsYamlValid(before, after)) {
|
|
2225
|
+
console.error("[prim] aborted: the merge would introduce invalid YAML; config left unchanged");
|
|
2226
|
+
process.exit(1);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function writeShim() {
|
|
2230
|
+
const path = shimPath();
|
|
2231
|
+
const dir = dirname4(path);
|
|
2232
|
+
if (!existsSync6(dir)) {
|
|
2233
|
+
mkdirSync4(dir, { recursive: true });
|
|
2234
|
+
}
|
|
2235
|
+
writeFileSync3(path, SHIM_SCRIPT, "utf-8");
|
|
2236
|
+
chmodSync(path, SHIM_MODE);
|
|
2237
|
+
}
|
|
2238
|
+
function removeShim() {
|
|
2239
|
+
rmSync2(shimPath(), { force: true });
|
|
2240
|
+
}
|
|
2241
|
+
function autoAcceptOf(raw) {
|
|
2242
|
+
if (raw.trim().length === 0) {
|
|
2243
|
+
return false;
|
|
2244
|
+
}
|
|
2245
|
+
return parseDocument(raw).toJS()?.hooks_auto_accept === true;
|
|
2246
|
+
}
|
|
2247
|
+
function readHooksFromRaw(raw) {
|
|
2248
|
+
return readHooks(raw.trim().length > 0 ? parseDocument(raw) : new Document());
|
|
2249
|
+
}
|
|
2250
|
+
function performInstall3(opts) {
|
|
2251
|
+
const path = configPath();
|
|
2252
|
+
const raw = existsSync6(path) ? readFileSync5(path, "utf-8") : "";
|
|
2253
|
+
const existing = readHooksFromRaw(raw);
|
|
2254
|
+
const desired = applyInstall3(existing, opts.force);
|
|
2255
|
+
let next = raw;
|
|
2256
|
+
if (JSON.stringify(existing) !== JSON.stringify(desired)) {
|
|
2257
|
+
next = spliceHooks(next, desired);
|
|
2258
|
+
}
|
|
2259
|
+
if (opts.autoAccept) {
|
|
2260
|
+
next = setAutoAccept(next);
|
|
2261
|
+
}
|
|
2262
|
+
writeShim();
|
|
2263
|
+
const changed = next !== raw;
|
|
2264
|
+
if (changed) {
|
|
2265
|
+
assertMergeValid(raw, next);
|
|
2266
|
+
atomicWriteFile(path, next);
|
|
2267
|
+
}
|
|
2268
|
+
return {
|
|
2269
|
+
path,
|
|
2270
|
+
gate: isGateInstalled3(desired),
|
|
2271
|
+
capture: isCaptureInstalled(desired),
|
|
2272
|
+
autoAccept: autoAcceptOf(next),
|
|
2273
|
+
changed
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
function performUninstall3() {
|
|
2277
|
+
const path = configPath();
|
|
2278
|
+
if (!existsSync6(path)) {
|
|
2279
|
+
removeShim();
|
|
2280
|
+
return { path, gate: false, capture: false, autoAccept: false, changed: false };
|
|
2281
|
+
}
|
|
2282
|
+
const raw = readFileSync5(path, "utf-8");
|
|
2283
|
+
const existing = readHooksFromRaw(raw);
|
|
2284
|
+
const remaining = applyUninstall3(existing);
|
|
2285
|
+
const next = JSON.stringify(existing) === JSON.stringify(remaining) ? raw : spliceHooks(raw, remaining);
|
|
2286
|
+
const changed = next !== raw;
|
|
2287
|
+
if (changed) {
|
|
2288
|
+
assertMergeValid(raw, next);
|
|
2289
|
+
atomicWriteFile(path, next);
|
|
2290
|
+
}
|
|
2291
|
+
removeShim();
|
|
2292
|
+
return {
|
|
2293
|
+
path,
|
|
2294
|
+
gate: isGateInstalled3(remaining),
|
|
2295
|
+
capture: isCaptureInstalled(remaining),
|
|
2296
|
+
autoAccept: autoAcceptOf(next),
|
|
2297
|
+
changed
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
function performStatus3() {
|
|
2301
|
+
const path = configPath();
|
|
2302
|
+
const hooks = existsSync6(path) ? readHooks(readDoc(path)) : {};
|
|
2303
|
+
return { path, gate: isGateInstalled3(hooks), capture: isCaptureInstalled(hooks) };
|
|
2304
|
+
}
|
|
2305
|
+
var TRUST_NOTICE2 = "[prim] Hermes requires hook consent: it prompts once per (event, command) on a TTY and records approval in ~/.hermes/shell-hooks-allowlist.json. To pre-approve non-interactively, re-run with --auto-accept (sets hooks_auto_accept: true), export HERMES_ACCEPT_HOOKS=1, or start Hermes with `hermes --accept-hooks chat`. Until approved, the hooks will not fire.";
|
|
2306
|
+
function rejectProjectScope(scope) {
|
|
2307
|
+
if (scope === "project") {
|
|
2308
|
+
console.error(
|
|
2309
|
+
"[prim] Hermes config is global-only (~/.hermes/config.yaml); --scope project is not supported."
|
|
2310
|
+
);
|
|
2311
|
+
process.exit(2);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
function registerHermesCommands(program2) {
|
|
2315
|
+
const hermes = program2.command("hermes").description("Manage the prim Hermes Agent integration (capture, gate, ingest, presence)");
|
|
2316
|
+
hermes.command("install").description("Register the prim hooks in Hermes's ~/.hermes/config.yaml").option("--scope <scope>", "user (default; Hermes config is global-only)").option("--force", "Replace any drifted prim hook entries").option("--auto-accept", "Also set hooks_auto_accept: true so the hooks need no TTY consent").action((opts) => {
|
|
2317
|
+
rejectProjectScope(opts.scope);
|
|
2318
|
+
const result = performInstall3({
|
|
2319
|
+
force: opts.force ?? false,
|
|
2320
|
+
autoAccept: opts.autoAccept ?? false
|
|
2321
|
+
});
|
|
2322
|
+
console.error(
|
|
2323
|
+
result.changed ? `[prim] Hermes integration installed at ${result.path}` : `[prim] Hermes integration already present at ${result.path} (no changes)`
|
|
2324
|
+
);
|
|
2325
|
+
console.error(TRUST_NOTICE2);
|
|
2326
|
+
console.log(JSON.stringify(result, null, JSON_INDENT3));
|
|
2327
|
+
});
|
|
2328
|
+
hermes.command("uninstall").description("Remove all prim hooks from Hermes's ~/.hermes/config.yaml").action(() => {
|
|
2329
|
+
const result = performUninstall3();
|
|
2330
|
+
console.error(
|
|
2331
|
+
result.changed ? `[prim] prim hooks removed from ${result.path}` : `[prim] no prim hooks to remove at ${result.path} (nothing changed)`
|
|
2332
|
+
);
|
|
2333
|
+
console.log(JSON.stringify(result, null, JSON_INDENT3));
|
|
2334
|
+
});
|
|
2335
|
+
hermes.command("status").description("Report whether each prim surface (gate, capture) is installed").action(() => {
|
|
2336
|
+
const result = performStatus3();
|
|
2337
|
+
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
2338
|
+
console.error(
|
|
2339
|
+
`[prim] hermes: gate ${mark(result.gate)} \xB7 capture ${mark(result.capture)} (${result.path})`
|
|
2340
|
+
);
|
|
2341
|
+
console.log(JSON.stringify(result, null, JSON_INDENT3));
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2001
2345
|
// src/commands/hooks.ts
|
|
2002
2346
|
import { execSync as execSync2 } from "child_process";
|
|
2003
|
-
import { existsSync as
|
|
2347
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2004
2348
|
import { resolve } from "path";
|
|
2005
2349
|
import { Option } from "commander";
|
|
2006
2350
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
@@ -2043,13 +2387,13 @@ function getGitRoot() {
|
|
|
2043
2387
|
}
|
|
2044
2388
|
function detectHusky(gitRoot) {
|
|
2045
2389
|
const huskyDir = resolve(gitRoot, ".husky");
|
|
2046
|
-
if (!
|
|
2047
|
-
if (
|
|
2048
|
-
if (
|
|
2390
|
+
if (!existsSync7(huskyDir)) return false;
|
|
2391
|
+
if (existsSync7(resolve(huskyDir, "_"))) return true;
|
|
2392
|
+
if (existsSync7(resolve(huskyDir, "pre-commit"))) return true;
|
|
2049
2393
|
const pkgPath = resolve(gitRoot, "package.json");
|
|
2050
|
-
if (
|
|
2394
|
+
if (existsSync7(pkgPath)) {
|
|
2051
2395
|
try {
|
|
2052
|
-
const pkg2 = JSON.parse(
|
|
2396
|
+
const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
2053
2397
|
const scripts = pkg2.scripts ?? {};
|
|
2054
2398
|
if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
|
|
2055
2399
|
return true;
|
|
@@ -2076,20 +2420,20 @@ async function askConfirmation(question) {
|
|
|
2076
2420
|
}
|
|
2077
2421
|
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
2078
2422
|
const hookPath = resolve(gitRoot, ".husky", spec.hookName);
|
|
2079
|
-
if (
|
|
2080
|
-
const existing =
|
|
2423
|
+
if (existsSync7(hookPath)) {
|
|
2424
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2081
2425
|
if (containsPrimHook(existing, spec.binName)) {
|
|
2082
2426
|
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
2083
2427
|
return;
|
|
2084
2428
|
}
|
|
2085
2429
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
2086
|
-
|
|
2430
|
+
writeFileSync4(hookPath, `${existing}${separator}${huskyBlock(spec)}
|
|
2087
2431
|
`, {
|
|
2088
2432
|
mode: 493
|
|
2089
2433
|
});
|
|
2090
2434
|
console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
|
|
2091
2435
|
} else {
|
|
2092
|
-
|
|
2436
|
+
writeFileSync4(hookPath, `#!/bin/sh
|
|
2093
2437
|
|
|
2094
2438
|
${huskyBlock(spec)}
|
|
2095
2439
|
`, {
|
|
@@ -2101,11 +2445,11 @@ ${huskyBlock(spec)}
|
|
|
2101
2445
|
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
2102
2446
|
const hooksDir = resolve(gitRoot, ".git", "hooks");
|
|
2103
2447
|
const hookPath = resolve(hooksDir, spec.hookName);
|
|
2104
|
-
if (!
|
|
2105
|
-
|
|
2448
|
+
if (!existsSync7(hooksDir)) {
|
|
2449
|
+
mkdirSync5(hooksDir, { recursive: true });
|
|
2106
2450
|
}
|
|
2107
|
-
if (
|
|
2108
|
-
const existing =
|
|
2451
|
+
if (existsSync7(hookPath)) {
|
|
2452
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2109
2453
|
if (containsPrimHook(existing, spec.binName)) {
|
|
2110
2454
|
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
2111
2455
|
return;
|
|
@@ -2114,7 +2458,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
|
2114
2458
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
2115
2459
|
return;
|
|
2116
2460
|
}
|
|
2117
|
-
|
|
2461
|
+
writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
|
|
2118
2462
|
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
2119
2463
|
}
|
|
2120
2464
|
function installHooks(gitRoot, target) {
|
|
@@ -2168,11 +2512,11 @@ function registerHooksCommands(program2) {
|
|
|
2168
2512
|
const gitRoot = getGitRoot();
|
|
2169
2513
|
for (const spec of HOOKS) {
|
|
2170
2514
|
const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
|
|
2171
|
-
if (!
|
|
2515
|
+
if (!existsSync7(hookPath)) {
|
|
2172
2516
|
console.log(`No ${spec.hookName} hook found.`);
|
|
2173
2517
|
continue;
|
|
2174
2518
|
}
|
|
2175
|
-
if (containsPrimHook(
|
|
2519
|
+
if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
|
|
2176
2520
|
unlinkSync2(hookPath);
|
|
2177
2521
|
console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
|
|
2178
2522
|
} else {
|
|
@@ -2183,11 +2527,11 @@ function registerHooksCommands(program2) {
|
|
|
2183
2527
|
}
|
|
2184
2528
|
|
|
2185
2529
|
// src/commands/moves.ts
|
|
2186
|
-
import { existsSync as
|
|
2187
|
-
import { join as
|
|
2530
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
2531
|
+
import { join as join6 } from "path";
|
|
2188
2532
|
|
|
2189
2533
|
// src/flusher.ts
|
|
2190
|
-
import { renameSync as
|
|
2534
|
+
import { renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
2191
2535
|
var BATCH_SIZE = 500;
|
|
2192
2536
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
2193
2537
|
var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
|
|
@@ -2219,7 +2563,7 @@ async function drainFlushingPath(flushingPath) {
|
|
|
2219
2563
|
async function drainPath(path) {
|
|
2220
2564
|
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
2221
2565
|
try {
|
|
2222
|
-
|
|
2566
|
+
renameSync3(path, tmpPath);
|
|
2223
2567
|
} catch (err) {
|
|
2224
2568
|
if (err.code === "ENOENT") {
|
|
2225
2569
|
return 0;
|
|
@@ -2343,19 +2687,19 @@ function registerMovesCommands(program2) {
|
|
|
2343
2687
|
}
|
|
2344
2688
|
});
|
|
2345
2689
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
2346
|
-
const dir =
|
|
2347
|
-
if (!
|
|
2348
|
-
|
|
2690
|
+
const dir = join6(process.cwd(), ".prim");
|
|
2691
|
+
if (!existsSync8(dir)) {
|
|
2692
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
|
|
2349
2693
|
}
|
|
2350
|
-
const file =
|
|
2351
|
-
|
|
2694
|
+
const file = join6(process.cwd(), WORKSPACE_FILE);
|
|
2695
|
+
writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
2352
2696
|
mode: FILE_MODE2
|
|
2353
2697
|
});
|
|
2354
2698
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
2355
2699
|
});
|
|
2356
2700
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
2357
|
-
const file =
|
|
2358
|
-
if (!
|
|
2701
|
+
const file = join6(process.cwd(), WORKSPACE_FILE);
|
|
2702
|
+
if (!existsSync8(file)) {
|
|
2359
2703
|
console.log("[prim] no workspace binding in cwd");
|
|
2360
2704
|
return;
|
|
2361
2705
|
}
|
|
@@ -2448,23 +2792,23 @@ function registerReconcileCommands(program2) {
|
|
|
2448
2792
|
|
|
2449
2793
|
// src/commands/session.ts
|
|
2450
2794
|
import {
|
|
2451
|
-
existsSync as
|
|
2452
|
-
mkdirSync as
|
|
2453
|
-
readFileSync as
|
|
2795
|
+
existsSync as existsSync9,
|
|
2796
|
+
mkdirSync as mkdirSync7,
|
|
2797
|
+
readFileSync as readFileSync7,
|
|
2454
2798
|
readdirSync,
|
|
2455
2799
|
unlinkSync as unlinkSync5,
|
|
2456
|
-
writeFileSync as
|
|
2800
|
+
writeFileSync as writeFileSync6
|
|
2457
2801
|
} from "fs";
|
|
2458
|
-
import { join as
|
|
2802
|
+
import { join as join7 } from "path";
|
|
2459
2803
|
var DIR_MODE2 = 448;
|
|
2460
2804
|
var FILE_MODE3 = 384;
|
|
2461
2805
|
function ensureDir() {
|
|
2462
|
-
if (!
|
|
2463
|
-
|
|
2806
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
2807
|
+
mkdirSync7(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
2464
2808
|
}
|
|
2465
2809
|
}
|
|
2466
2810
|
function markerPath(sessionId) {
|
|
2467
|
-
return
|
|
2811
|
+
return join7(SESSIONS_DIR, `${sessionId}.json`);
|
|
2468
2812
|
}
|
|
2469
2813
|
function registerSessionCommands(program2) {
|
|
2470
2814
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -2474,13 +2818,13 @@ function registerSessionCommands(program2) {
|
|
|
2474
2818
|
orgId: opts.orgId,
|
|
2475
2819
|
startedAt: Date.now()
|
|
2476
2820
|
};
|
|
2477
|
-
|
|
2821
|
+
writeFileSync6(markerPath(sessionId), JSON.stringify(marker, null, 2), {
|
|
2478
2822
|
mode: FILE_MODE3
|
|
2479
2823
|
});
|
|
2480
2824
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
2481
2825
|
});
|
|
2482
2826
|
session.command("list").description("List active session markers").action(() => {
|
|
2483
|
-
if (!
|
|
2827
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
2484
2828
|
console.log("[prim] no session markers");
|
|
2485
2829
|
return;
|
|
2486
2830
|
}
|
|
@@ -2492,7 +2836,7 @@ function registerSessionCommands(program2) {
|
|
|
2492
2836
|
for (const f of files) {
|
|
2493
2837
|
const sessionId = f.replace(/\.json$/, "");
|
|
2494
2838
|
try {
|
|
2495
|
-
const m = JSON.parse(
|
|
2839
|
+
const m = JSON.parse(readFileSync7(join7(SESSIONS_DIR, f), "utf-8"));
|
|
2496
2840
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
2497
2841
|
} catch {
|
|
2498
2842
|
}
|
|
@@ -2500,7 +2844,7 @@ function registerSessionCommands(program2) {
|
|
|
2500
2844
|
});
|
|
2501
2845
|
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
2502
2846
|
const p = markerPath(sessionId);
|
|
2503
|
-
if (!
|
|
2847
|
+
if (!existsSync9(p)) {
|
|
2504
2848
|
console.log(`[prim] no marker for session ${sessionId}`);
|
|
2505
2849
|
return;
|
|
2506
2850
|
}
|
|
@@ -2513,13 +2857,19 @@ function registerSessionCommands(program2) {
|
|
|
2513
2857
|
import { spawnSync } from "child_process";
|
|
2514
2858
|
var EXIT_INCOMPLETE = 1;
|
|
2515
2859
|
var EXIT_USAGE3 = 2;
|
|
2860
|
+
var SESSION_LABELS = {
|
|
2861
|
+
claude: "Claude Code integration",
|
|
2862
|
+
codex: "Codex integration",
|
|
2863
|
+
hermes: "Hermes integration"
|
|
2864
|
+
};
|
|
2516
2865
|
function planSetupSteps(opts) {
|
|
2517
2866
|
const scopeArgs = opts.scope === "user" ? ["--scope", "user"] : [];
|
|
2867
|
+
const sessionArgs = opts.agent === "hermes" ? [opts.agent, "install"] : [opts.agent, "install", ...scopeArgs];
|
|
2518
2868
|
const steps = [
|
|
2519
2869
|
{
|
|
2520
2870
|
key: "session",
|
|
2521
|
-
label: opts.agent
|
|
2522
|
-
args:
|
|
2871
|
+
label: SESSION_LABELS[opts.agent],
|
|
2872
|
+
args: sessionArgs,
|
|
2523
2873
|
required: true
|
|
2524
2874
|
}
|
|
2525
2875
|
];
|
|
@@ -2532,16 +2882,32 @@ function planSetupSteps(opts) {
|
|
|
2532
2882
|
});
|
|
2533
2883
|
}
|
|
2534
2884
|
steps.push({ key: "hooks", label: "Git hooks", args: ["hooks", "install"], required: true });
|
|
2535
|
-
|
|
2885
|
+
const skillArgs = opts.agent === "hermes" ? ["skill", "install", "--target", ".hermes.md"] : ["skill", "install"];
|
|
2886
|
+
steps.push({ key: "skill", label: "Agent skill", args: skillArgs, required: true });
|
|
2536
2887
|
return steps;
|
|
2537
2888
|
}
|
|
2889
|
+
function detectAgent(env) {
|
|
2890
|
+
if (env.HERMES_INTERACTIVE) {
|
|
2891
|
+
return "hermes";
|
|
2892
|
+
}
|
|
2893
|
+
return "claude";
|
|
2894
|
+
}
|
|
2895
|
+
function resolveAgent(agentFlag, env) {
|
|
2896
|
+
if (agentFlag !== void 0) {
|
|
2897
|
+
return { agent: agentFlag, detected: false };
|
|
2898
|
+
}
|
|
2899
|
+
return { agent: detectAgent(env), detected: true };
|
|
2900
|
+
}
|
|
2538
2901
|
function registerSetupCommand(program2) {
|
|
2539
2902
|
program2.command("setup").description(
|
|
2540
2903
|
"Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
|
|
2541
|
-
).option("--agent <agent>", "claude
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2904
|
+
).option("--agent <agent>", "claude, codex, or hermes (auto-detected when omitted)").option("--scope <scope>", "project or user (session integration)", "project").option("--no-daemon", "skip starting the companion daemon").action((opts) => {
|
|
2905
|
+
const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
|
|
2906
|
+
if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
|
|
2907
|
+
process.stderr.write(
|
|
2908
|
+
`[prim] unknown --agent "${agentInput}" (expected claude, codex, or hermes)
|
|
2909
|
+
`
|
|
2910
|
+
);
|
|
2545
2911
|
process.exit(EXIT_USAGE3);
|
|
2546
2912
|
}
|
|
2547
2913
|
if (opts.scope !== "project" && opts.scope !== "user") {
|
|
@@ -2549,7 +2915,7 @@ function registerSetupCommand(program2) {
|
|
|
2549
2915
|
`);
|
|
2550
2916
|
process.exit(EXIT_USAGE3);
|
|
2551
2917
|
}
|
|
2552
|
-
const agent =
|
|
2918
|
+
const agent = agentInput;
|
|
2553
2919
|
const scope = opts.scope;
|
|
2554
2920
|
const self = process.argv[1];
|
|
2555
2921
|
const run = (args, capture = false) => {
|
|
@@ -2571,6 +2937,9 @@ function registerSetupCommand(program2) {
|
|
|
2571
2937
|
return false;
|
|
2572
2938
|
}
|
|
2573
2939
|
};
|
|
2940
|
+
if (detected && agent !== "claude") {
|
|
2941
|
+
note(`agent \xB7 detected ${agent} session (override with --agent <agent>)`);
|
|
2942
|
+
}
|
|
2574
2943
|
if (agent === "claude") {
|
|
2575
2944
|
note("pre-authorize \xB7 writing prim allow-rule (user scope)\u2026");
|
|
2576
2945
|
results.preauth = run(["claude", "preauth", "--scope", "user"]).code === 0 ? "ok" : "skipped";
|
|
@@ -2601,23 +2970,24 @@ function registerSetupCommand(program2) {
|
|
|
2601
2970
|
|
|
2602
2971
|
// src/commands/skill.ts
|
|
2603
2972
|
import {
|
|
2604
|
-
closeSync as
|
|
2605
|
-
existsSync as
|
|
2606
|
-
fsyncSync as
|
|
2607
|
-
openSync as
|
|
2608
|
-
readFileSync as
|
|
2609
|
-
renameSync as
|
|
2610
|
-
writeFileSync as
|
|
2973
|
+
closeSync as closeSync4,
|
|
2974
|
+
existsSync as existsSync10,
|
|
2975
|
+
fsyncSync as fsyncSync3,
|
|
2976
|
+
openSync as openSync4,
|
|
2977
|
+
readFileSync as readFileSync8,
|
|
2978
|
+
renameSync as renameSync4,
|
|
2979
|
+
writeFileSync as writeFileSync7
|
|
2611
2980
|
} from "fs";
|
|
2612
|
-
import { dirname as
|
|
2981
|
+
import { dirname as dirname5, resolve as resolve2 } from "path";
|
|
2613
2982
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2614
2983
|
import { createPatch } from "diff";
|
|
2615
|
-
var __dirname =
|
|
2984
|
+
var __dirname = dirname5(fileURLToPath2(import.meta.url));
|
|
2616
2985
|
var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
2617
2986
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
2618
2987
|
var TARGET_CANDIDATES = [
|
|
2619
2988
|
"CLAUDE.md",
|
|
2620
2989
|
"AGENTS.md",
|
|
2990
|
+
".hermes.md",
|
|
2621
2991
|
".cursor/rules",
|
|
2622
2992
|
".windsurfrules",
|
|
2623
2993
|
".github/instructions/primitive.md"
|
|
@@ -2625,15 +2995,15 @@ var TARGET_CANDIDATES = [
|
|
|
2625
2995
|
var DEFAULT_TARGET = "CLAUDE.md";
|
|
2626
2996
|
function loadSkill() {
|
|
2627
2997
|
let dir = __dirname;
|
|
2628
|
-
while (dir !==
|
|
2998
|
+
while (dir !== dirname5(dir)) {
|
|
2629
2999
|
const p = resolve2(dir, "SKILL.md");
|
|
2630
|
-
if (
|
|
2631
|
-
dir =
|
|
3000
|
+
if (existsSync10(p)) return readFileSync8(p, "utf-8");
|
|
3001
|
+
dir = dirname5(dir);
|
|
2632
3002
|
}
|
|
2633
3003
|
throw new Error("SKILL.md not found in package");
|
|
2634
3004
|
}
|
|
2635
3005
|
function detectTargets(cwd) {
|
|
2636
|
-
return TARGET_CANDIDATES.filter((p) =>
|
|
3006
|
+
return TARGET_CANDIDATES.filter((p) => existsSync10(resolve2(cwd, p)));
|
|
2637
3007
|
}
|
|
2638
3008
|
function detectNewline(content) {
|
|
2639
3009
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -2661,14 +3031,14 @@ function removeBlock(existing) {
|
|
|
2661
3031
|
}
|
|
2662
3032
|
function atomicWrite2(target, content) {
|
|
2663
3033
|
const tmp = `${target}.tmp`;
|
|
2664
|
-
|
|
2665
|
-
const fd =
|
|
3034
|
+
writeFileSync7(tmp, content);
|
|
3035
|
+
const fd = openSync4(tmp, "r+");
|
|
2666
3036
|
try {
|
|
2667
|
-
|
|
3037
|
+
fsyncSync3(fd);
|
|
2668
3038
|
} finally {
|
|
2669
|
-
|
|
3039
|
+
closeSync4(fd);
|
|
2670
3040
|
}
|
|
2671
|
-
|
|
3041
|
+
renameSync4(tmp, target);
|
|
2672
3042
|
}
|
|
2673
3043
|
function resolveTarget(cwd, override) {
|
|
2674
3044
|
if (override) return resolve2(cwd, override);
|
|
@@ -2682,7 +3052,7 @@ function resolveTarget(cwd, override) {
|
|
|
2682
3052
|
function runInstall(cwd, opts) {
|
|
2683
3053
|
const target = resolveTarget(cwd, opts.target);
|
|
2684
3054
|
if (target === null) return 1;
|
|
2685
|
-
const existing =
|
|
3055
|
+
const existing = existsSync10(target) ? readFileSync8(target, "utf-8") : "";
|
|
2686
3056
|
const eol = existing ? detectNewline(existing) : "\n";
|
|
2687
3057
|
const block = composeBlock(loadSkill(), eol);
|
|
2688
3058
|
const next = applyBlock(existing, block, eol);
|
|
@@ -2701,11 +3071,11 @@ function runInstall(cwd, opts) {
|
|
|
2701
3071
|
function runUninstall(cwd, opts) {
|
|
2702
3072
|
const target = resolveTarget(cwd, opts.target);
|
|
2703
3073
|
if (target === null) return 1;
|
|
2704
|
-
if (!
|
|
3074
|
+
if (!existsSync10(target)) {
|
|
2705
3075
|
console.log(`Skill block not present at ${target}`);
|
|
2706
3076
|
return 0;
|
|
2707
3077
|
}
|
|
2708
|
-
const existing =
|
|
3078
|
+
const existing = readFileSync8(target, "utf-8");
|
|
2709
3079
|
const next = removeBlock(existing);
|
|
2710
3080
|
if (next === null) {
|
|
2711
3081
|
console.log(`Skill block not present at ${target}`);
|
|
@@ -2718,10 +3088,10 @@ function runUninstall(cwd, opts) {
|
|
|
2718
3088
|
function runStatus(cwd, opts) {
|
|
2719
3089
|
const target = resolveTarget(cwd, opts.target);
|
|
2720
3090
|
if (target === null) return 1;
|
|
2721
|
-
const fileExists =
|
|
3091
|
+
const fileExists = existsSync10(target);
|
|
2722
3092
|
let installed = false;
|
|
2723
3093
|
if (fileExists) {
|
|
2724
|
-
const content =
|
|
3094
|
+
const content = readFileSync8(target, "utf-8");
|
|
2725
3095
|
installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
|
|
2726
3096
|
}
|
|
2727
3097
|
if (opts.json) {
|
|
@@ -2758,18 +3128,18 @@ function registerSkillCommands(program2) {
|
|
|
2758
3128
|
}
|
|
2759
3129
|
|
|
2760
3130
|
// src/commands/statusline.ts
|
|
2761
|
-
import { readFileSync as
|
|
2762
|
-
import { dirname as
|
|
3131
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3132
|
+
import { dirname as dirname6, resolve as resolve3 } from "path";
|
|
2763
3133
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2764
3134
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
2765
3135
|
var STATUSLINE_NAME_CAP = 3;
|
|
2766
3136
|
function readPackageVersion() {
|
|
2767
3137
|
try {
|
|
2768
|
-
const here =
|
|
3138
|
+
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
2769
3139
|
const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
|
|
2770
3140
|
for (const path of candidates) {
|
|
2771
3141
|
try {
|
|
2772
|
-
const pkg2 = JSON.parse(
|
|
3142
|
+
const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
|
|
2773
3143
|
if (pkg2.version) {
|
|
2774
3144
|
return pkg2.version;
|
|
2775
3145
|
}
|
|
@@ -2936,8 +3306,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
|
2936
3306
|
}
|
|
2937
3307
|
|
|
2938
3308
|
// src/index.ts
|
|
2939
|
-
var __dirname2 =
|
|
2940
|
-
var pkg = JSON.parse(
|
|
3309
|
+
var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
|
|
3310
|
+
var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
|
|
2941
3311
|
updateNotifier({ pkg }).notify();
|
|
2942
3312
|
var program = new Command();
|
|
2943
3313
|
program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
|
|
@@ -2952,6 +3322,7 @@ registerSessionCommands(program);
|
|
|
2952
3322
|
registerDecisionsCommands(program);
|
|
2953
3323
|
registerClaudeCommands(program);
|
|
2954
3324
|
registerCodexCommands(program);
|
|
3325
|
+
registerHermesCommands(program);
|
|
2955
3326
|
registerDaemonCommands(program);
|
|
2956
3327
|
registerDoctorCommands(program);
|
|
2957
3328
|
registerReconcileCommands(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.32",
|
|
4
4
|
"description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"commander": "^12.1.0",
|
|
63
63
|
"diff": "^5.2.0",
|
|
64
|
-
"update-notifier": "^7.3.1"
|
|
64
|
+
"update-notifier": "^7.3.1",
|
|
65
|
+
"yaml": "^2.9.0"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@biomejs/biome": "^1.9.0",
|