@primitive.ai/prim 0.1.0-alpha.31 → 0.1.0-alpha.33

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/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 readFileSync9 } from "fs";
40
- import { dirname as dirname6, resolve as resolve4 } from "path";
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";
@@ -342,6 +342,9 @@ function hookShimCommand(bin, args = "") {
342
342
  const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
343
343
  return `if command -v ${bin} >/dev/null 2>&1; then ${invoke(bin)}; elif [ -f "./node_modules/.bin/${bin}" ]; then ${invoke(`./node_modules/.bin/${bin}`)}; else ${invoke(`${NPX_FALLBACK} ${bin}`)}; fi`;
344
344
  }
345
+ function detachedHookShimCommand(bin, args = "") {
346
+ return `payload=$(cat); { trap '' HUP; export npm_config_fetch_retries=2 npm_config_fetch_retry_mintimeout=10000 npm_config_fetch_retry_maxtimeout=10000 npm_config_fetch_timeout=60000; printf '%s' "$payload" | { ${hookShimCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
347
+ }
345
348
  function commandMatchesBin(command, bin) {
346
349
  if (!command) {
347
350
  return false;
@@ -395,12 +398,17 @@ var CAPTURE_EVENTS = [
395
398
  function makeRegistration(event, matcher, bin, args = "") {
396
399
  return { event, matcher, bin, command: hookShimCommand(bin, args) };
397
400
  }
401
+ function makeDetachedRegistration(event, matcher, bin, args = "") {
402
+ return { event, matcher, bin, command: detachedHookShimCommand(bin, args) };
403
+ }
398
404
  var REGISTRATIONS = [
399
- ...CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN)),
405
+ ...CAPTURE_EVENTS.map(
406
+ (event) => event === "SessionEnd" ? makeDetachedRegistration(event, "*", CAPTURE_BIN) : makeRegistration(event, "*", CAPTURE_BIN)
407
+ ),
400
408
  makeRegistration("PreToolUse", "Edit|Write|MultiEdit", GATE_BIN),
401
409
  makeRegistration("PostToolUse", "Edit|Write|MultiEdit", POST_TOOL_USE_BIN),
402
410
  makeRegistration("SessionStart", "*", SESSION_START_BIN),
403
- makeRegistration("SessionEnd", "*", SESSION_END_BIN)
411
+ makeDetachedRegistration("SessionEnd", "*", SESSION_END_BIN)
404
412
  ];
405
413
  var PRIM_PERMISSION_RULE = "Bash(npx --yes @primitive.ai/prim*)";
406
414
  var LEGACY_PRIM_PERMISSION_RULES = [
@@ -445,10 +453,9 @@ function stripCommand(list, bin) {
445
453
  return out;
446
454
  }
447
455
  function ensureRegistration(list, reg, force) {
448
- const hasCanonical = list.some(
449
- (e) => e.matcher === reg.matcher && e.hooks?.length === 1 && e.hooks[0].command === reg.command
450
- );
451
- if (hasCanonical && !force) {
456
+ const isCanonical = (e) => e.matcher === reg.matcher && e.hooks?.length === 1 && e.hooks[0].command === reg.command;
457
+ const hasStray = (e) => !isCanonical(e) && (e.hooks ?? []).some((h) => commandMatchesBin(h.command, reg.bin));
458
+ if (list.some(isCanonical) && !list.some(hasStray) && !force) {
452
459
  return list;
453
460
  }
454
461
  return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
@@ -1371,6 +1378,8 @@ function authorLabel(row) {
1371
1378
  return "Your Claude Code";
1372
1379
  case "codex":
1373
1380
  return "Your Codex";
1381
+ case "hermes":
1382
+ return "Your Hermes";
1374
1383
  case "chat":
1375
1384
  return "Your chat";
1376
1385
  case "spec_edit":
@@ -1998,9 +2007,351 @@ function registerDoctorCommands(program2) {
1998
2007
  });
1999
2008
  }
2000
2009
 
2010
+ // src/commands/hermes-install.ts
2011
+ import {
2012
+ chmodSync,
2013
+ closeSync as closeSync3,
2014
+ existsSync as existsSync6,
2015
+ fsyncSync as fsyncSync2,
2016
+ mkdirSync as mkdirSync4,
2017
+ openSync as openSync3,
2018
+ readFileSync as readFileSync5,
2019
+ renameSync as renameSync2,
2020
+ rmSync as rmSync2,
2021
+ writeFileSync as writeFileSync3
2022
+ } from "fs";
2023
+ import { homedir as homedir4 } from "os";
2024
+ import { dirname as dirname4, join as join5 } from "path";
2025
+ import { Document, parseDocument, stringify } from "yaml";
2026
+ var CAPTURE_BIN3 = "prim-hook";
2027
+ var GATE_BIN3 = "prim-pre-tool-use";
2028
+ var POST_TOOL_USE_BIN3 = "prim-post-tool-use";
2029
+ var SESSION_START_BIN3 = "prim-session-start";
2030
+ var SESSION_END_BIN2 = "prim-session-end";
2031
+ var HERMES_ARGS = "--agent hermes";
2032
+ var EDIT_MATCHER = "write_file|patch";
2033
+ var JSON_INDENT3 = 2;
2034
+ var SHIM_MODE = 493;
2035
+ var PRIM_BINS3 = [
2036
+ CAPTURE_BIN3,
2037
+ GATE_BIN3,
2038
+ POST_TOOL_USE_BIN3,
2039
+ SESSION_START_BIN3,
2040
+ SESSION_END_BIN2
2041
+ ];
2042
+ function hermesHome() {
2043
+ return process.env.HERMES_HOME ?? join5(homedir4(), ".hermes");
2044
+ }
2045
+ function configPath() {
2046
+ return join5(hermesHome(), "config.yaml");
2047
+ }
2048
+ function shimPath() {
2049
+ return join5(hermesHome(), "agent-hooks", "prim-shim.sh");
2050
+ }
2051
+ var SHIM_SCRIPT = `#!/bin/sh
2052
+ # prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
2053
+ # with shell=False, so the PATH \u2192 local node_modules \u2192 npx @latest resolution a
2054
+ # shell does for the Claude Code / Codex installs lives here instead.
2055
+ bin="$1"
2056
+ shift
2057
+ if command -v "$bin" >/dev/null 2>&1; then
2058
+ exec "$bin" "$@"
2059
+ fi
2060
+ if [ -x "./node_modules/.bin/$bin" ]; then
2061
+ exec "./node_modules/.bin/$bin" "$@"
2062
+ fi
2063
+ exec npx --yes -p @primitive.ai/prim@latest "$bin" "$@"
2064
+ `;
2065
+ function commandFor(bin) {
2066
+ return `"${shimPath()}" ${bin} ${HERMES_ARGS}`;
2067
+ }
2068
+ function commandUsesBin(command, bin) {
2069
+ return new RegExp(`prim-shim\\.sh"?\\s+${bin}\\s`).test(command);
2070
+ }
2071
+ var CAPTURE_EVENTS2 = [
2072
+ "on_session_start",
2073
+ "pre_llm_call",
2074
+ "pre_tool_call",
2075
+ "post_tool_call",
2076
+ "post_llm_call",
2077
+ "on_session_end",
2078
+ "subagent_stop"
2079
+ ];
2080
+ var GATE_TIMEOUT_SECONDS = 10;
2081
+ var REGISTRATIONS2 = [
2082
+ ...CAPTURE_EVENTS2.map((event) => ({ event, bin: CAPTURE_BIN3 })),
2083
+ { event: "pre_tool_call", matcher: EDIT_MATCHER, bin: GATE_BIN3, timeout: GATE_TIMEOUT_SECONDS },
2084
+ { event: "post_tool_call", matcher: EDIT_MATCHER, bin: POST_TOOL_USE_BIN3 },
2085
+ { event: "on_session_start", bin: SESSION_START_BIN3 },
2086
+ { event: "on_session_end", bin: SESSION_END_BIN2 }
2087
+ ];
2088
+ function entryFor(reg) {
2089
+ const command = commandFor(reg.bin);
2090
+ const entry = reg.matcher ? { matcher: reg.matcher, command } : { command };
2091
+ if (reg.timeout !== void 0) {
2092
+ entry.timeout = reg.timeout;
2093
+ }
2094
+ return entry;
2095
+ }
2096
+ function stripBin(list, bin) {
2097
+ return list.filter((e) => !commandUsesBin(e.command, bin));
2098
+ }
2099
+ function ensureReg(list, reg, force) {
2100
+ const entry = entryFor(reg);
2101
+ const present = list.some(
2102
+ (e) => e.command === entry.command && (e.matcher ?? "") === (entry.matcher ?? "") && (e.timeout ?? null) === (entry.timeout ?? null)
2103
+ );
2104
+ if (present && !force) {
2105
+ return list;
2106
+ }
2107
+ return [...stripBin(list, reg.bin), entry];
2108
+ }
2109
+ function applyInstall3(hooks, force) {
2110
+ const next = { ...hooks };
2111
+ for (const reg of REGISTRATIONS2) {
2112
+ next[reg.event] = ensureReg(next[reg.event] ?? [], reg, force);
2113
+ }
2114
+ return next;
2115
+ }
2116
+ function applyUninstall3(hooks) {
2117
+ const next = {};
2118
+ for (const [event, list] of Object.entries(hooks)) {
2119
+ let kept = list;
2120
+ for (const bin of PRIM_BINS3) {
2121
+ kept = stripBin(kept, bin);
2122
+ }
2123
+ if (kept.length > 0) {
2124
+ next[event] = kept;
2125
+ }
2126
+ }
2127
+ return next;
2128
+ }
2129
+ function isGateInstalled3(hooks) {
2130
+ return (hooks.pre_tool_call ?? []).some((e) => commandUsesBin(e.command, GATE_BIN3));
2131
+ }
2132
+ function isCaptureInstalled(hooks) {
2133
+ return CAPTURE_EVENTS2.some(
2134
+ (event) => (hooks[event] ?? []).some((e) => commandUsesBin(e.command, CAPTURE_BIN3))
2135
+ );
2136
+ }
2137
+ function readDoc(path) {
2138
+ if (!existsSync6(path)) {
2139
+ return new Document();
2140
+ }
2141
+ const raw = readFileSync5(path, "utf-8");
2142
+ return raw.trim().length === 0 ? new Document() : parseDocument(raw);
2143
+ }
2144
+ function readHooks(doc) {
2145
+ const root = doc.toJS();
2146
+ const hooks = root?.hooks;
2147
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
2148
+ return {};
2149
+ }
2150
+ const out = {};
2151
+ for (const [event, list] of Object.entries(hooks)) {
2152
+ if (!Array.isArray(list)) {
2153
+ continue;
2154
+ }
2155
+ const entries = list.filter(
2156
+ (e) => typeof e === "object" && e !== null && typeof e.command === "string"
2157
+ );
2158
+ if (entries.length > 0) {
2159
+ out[event] = entries;
2160
+ }
2161
+ }
2162
+ return out;
2163
+ }
2164
+ function locateHooksBlock(raw) {
2165
+ const lines = raw.split("\n");
2166
+ let offset = 0;
2167
+ let start = -1;
2168
+ let end = raw.length;
2169
+ for (const line of lines) {
2170
+ const lineStart = offset;
2171
+ offset += line.length + 1;
2172
+ if (start === -1) {
2173
+ if (/^hooks:(\s|$)/.test(line)) {
2174
+ start = lineStart;
2175
+ }
2176
+ } else if (/^\S/.test(line) && !line.startsWith("#")) {
2177
+ end = lineStart;
2178
+ break;
2179
+ }
2180
+ }
2181
+ return start === -1 ? null : { start, end };
2182
+ }
2183
+ function serializeHooks(hooks) {
2184
+ if (Object.keys(hooks).length === 0) {
2185
+ return "";
2186
+ }
2187
+ return stringify({ hooks }, { lineWidth: 0 });
2188
+ }
2189
+ function spliceHooks(raw, hooks) {
2190
+ const block = serializeHooks(hooks);
2191
+ const loc = locateHooksBlock(raw);
2192
+ if (loc) {
2193
+ return raw.slice(0, loc.start) + block + raw.slice(loc.end);
2194
+ }
2195
+ if (block.length === 0) {
2196
+ return raw;
2197
+ }
2198
+ const sep = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
2199
+ return `${raw}${sep}${block}`;
2200
+ }
2201
+ function hasAutoAccept(raw) {
2202
+ return /^hooks_auto_accept:/m.test(raw);
2203
+ }
2204
+ function setAutoAccept(raw) {
2205
+ if (hasAutoAccept(raw)) {
2206
+ return raw.replace(/^hooks_auto_accept:.*$/m, "hooks_auto_accept: true");
2207
+ }
2208
+ const sep = raw.length === 0 || raw.endsWith("\n") ? "" : "\n";
2209
+ return `${raw}${sep}hooks_auto_accept: true
2210
+ `;
2211
+ }
2212
+ function atomicWriteFile(path, content) {
2213
+ const dir = dirname4(path);
2214
+ if (!existsSync6(dir)) {
2215
+ mkdirSync4(dir, { recursive: true });
2216
+ }
2217
+ const tmp = `${path}.tmp.${String(process.pid)}`;
2218
+ writeFileSync3(tmp, content, "utf-8");
2219
+ const fd = openSync3(tmp, "r+");
2220
+ try {
2221
+ fsyncSync2(fd);
2222
+ } finally {
2223
+ closeSync3(fd);
2224
+ }
2225
+ renameSync2(tmp, path);
2226
+ }
2227
+ function mergeKeepsYamlValid(before, after) {
2228
+ return parseDocument(after).errors.length <= parseDocument(before).errors.length;
2229
+ }
2230
+ function assertMergeValid(before, after) {
2231
+ if (!mergeKeepsYamlValid(before, after)) {
2232
+ console.error("[prim] aborted: the merge would introduce invalid YAML; config left unchanged");
2233
+ process.exit(1);
2234
+ }
2235
+ }
2236
+ function writeShim() {
2237
+ const path = shimPath();
2238
+ const dir = dirname4(path);
2239
+ if (!existsSync6(dir)) {
2240
+ mkdirSync4(dir, { recursive: true });
2241
+ }
2242
+ writeFileSync3(path, SHIM_SCRIPT, "utf-8");
2243
+ chmodSync(path, SHIM_MODE);
2244
+ }
2245
+ function removeShim() {
2246
+ rmSync2(shimPath(), { force: true });
2247
+ }
2248
+ function autoAcceptOf(raw) {
2249
+ if (raw.trim().length === 0) {
2250
+ return false;
2251
+ }
2252
+ return parseDocument(raw).toJS()?.hooks_auto_accept === true;
2253
+ }
2254
+ function readHooksFromRaw(raw) {
2255
+ return readHooks(raw.trim().length > 0 ? parseDocument(raw) : new Document());
2256
+ }
2257
+ function performInstall3(opts) {
2258
+ const path = configPath();
2259
+ const raw = existsSync6(path) ? readFileSync5(path, "utf-8") : "";
2260
+ const existing = readHooksFromRaw(raw);
2261
+ const desired = applyInstall3(existing, opts.force);
2262
+ let next = raw;
2263
+ if (JSON.stringify(existing) !== JSON.stringify(desired)) {
2264
+ next = spliceHooks(next, desired);
2265
+ }
2266
+ if (opts.autoAccept) {
2267
+ next = setAutoAccept(next);
2268
+ }
2269
+ writeShim();
2270
+ const changed = next !== raw;
2271
+ if (changed) {
2272
+ assertMergeValid(raw, next);
2273
+ atomicWriteFile(path, next);
2274
+ }
2275
+ return {
2276
+ path,
2277
+ gate: isGateInstalled3(desired),
2278
+ capture: isCaptureInstalled(desired),
2279
+ autoAccept: autoAcceptOf(next),
2280
+ changed
2281
+ };
2282
+ }
2283
+ function performUninstall3() {
2284
+ const path = configPath();
2285
+ if (!existsSync6(path)) {
2286
+ removeShim();
2287
+ return { path, gate: false, capture: false, autoAccept: false, changed: false };
2288
+ }
2289
+ const raw = readFileSync5(path, "utf-8");
2290
+ const existing = readHooksFromRaw(raw);
2291
+ const remaining = applyUninstall3(existing);
2292
+ const next = JSON.stringify(existing) === JSON.stringify(remaining) ? raw : spliceHooks(raw, remaining);
2293
+ const changed = next !== raw;
2294
+ if (changed) {
2295
+ assertMergeValid(raw, next);
2296
+ atomicWriteFile(path, next);
2297
+ }
2298
+ removeShim();
2299
+ return {
2300
+ path,
2301
+ gate: isGateInstalled3(remaining),
2302
+ capture: isCaptureInstalled(remaining),
2303
+ autoAccept: autoAcceptOf(next),
2304
+ changed
2305
+ };
2306
+ }
2307
+ function performStatus3() {
2308
+ const path = configPath();
2309
+ const hooks = existsSync6(path) ? readHooks(readDoc(path)) : {};
2310
+ return { path, gate: isGateInstalled3(hooks), capture: isCaptureInstalled(hooks) };
2311
+ }
2312
+ 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.";
2313
+ function rejectProjectScope(scope) {
2314
+ if (scope === "project") {
2315
+ console.error(
2316
+ "[prim] Hermes config is global-only (~/.hermes/config.yaml); --scope project is not supported."
2317
+ );
2318
+ process.exit(2);
2319
+ }
2320
+ }
2321
+ function registerHermesCommands(program2) {
2322
+ const hermes = program2.command("hermes").description("Manage the prim Hermes Agent integration (capture, gate, ingest, presence)");
2323
+ 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) => {
2324
+ rejectProjectScope(opts.scope);
2325
+ const result = performInstall3({
2326
+ force: opts.force ?? false,
2327
+ autoAccept: opts.autoAccept ?? false
2328
+ });
2329
+ console.error(
2330
+ result.changed ? `[prim] Hermes integration installed at ${result.path}` : `[prim] Hermes integration already present at ${result.path} (no changes)`
2331
+ );
2332
+ console.error(TRUST_NOTICE2);
2333
+ console.log(JSON.stringify(result, null, JSON_INDENT3));
2334
+ });
2335
+ hermes.command("uninstall").description("Remove all prim hooks from Hermes's ~/.hermes/config.yaml").action(() => {
2336
+ const result = performUninstall3();
2337
+ console.error(
2338
+ result.changed ? `[prim] prim hooks removed from ${result.path}` : `[prim] no prim hooks to remove at ${result.path} (nothing changed)`
2339
+ );
2340
+ console.log(JSON.stringify(result, null, JSON_INDENT3));
2341
+ });
2342
+ hermes.command("status").description("Report whether each prim surface (gate, capture) is installed").action(() => {
2343
+ const result = performStatus3();
2344
+ const mark = (b) => b ? "\u2713" : "\u2717";
2345
+ console.error(
2346
+ `[prim] hermes: gate ${mark(result.gate)} \xB7 capture ${mark(result.capture)} (${result.path})`
2347
+ );
2348
+ console.log(JSON.stringify(result, null, JSON_INDENT3));
2349
+ });
2350
+ }
2351
+
2001
2352
  // src/commands/hooks.ts
2002
2353
  import { execSync as execSync2 } from "child_process";
2003
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
2354
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2004
2355
  import { resolve } from "path";
2005
2356
  import { Option } from "commander";
2006
2357
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
@@ -2043,13 +2394,13 @@ function getGitRoot() {
2043
2394
  }
2044
2395
  function detectHusky(gitRoot) {
2045
2396
  const huskyDir = resolve(gitRoot, ".husky");
2046
- if (!existsSync6(huskyDir)) return false;
2047
- if (existsSync6(resolve(huskyDir, "_"))) return true;
2048
- if (existsSync6(resolve(huskyDir, "pre-commit"))) return true;
2397
+ if (!existsSync7(huskyDir)) return false;
2398
+ if (existsSync7(resolve(huskyDir, "_"))) return true;
2399
+ if (existsSync7(resolve(huskyDir, "pre-commit"))) return true;
2049
2400
  const pkgPath = resolve(gitRoot, "package.json");
2050
- if (existsSync6(pkgPath)) {
2401
+ if (existsSync7(pkgPath)) {
2051
2402
  try {
2052
- const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
2403
+ const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
2053
2404
  const scripts = pkg2.scripts ?? {};
2054
2405
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
2055
2406
  return true;
@@ -2076,20 +2427,20 @@ async function askConfirmation(question) {
2076
2427
  }
2077
2428
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
2078
2429
  const hookPath = resolve(gitRoot, ".husky", spec.hookName);
2079
- if (existsSync6(hookPath)) {
2080
- const existing = readFileSync5(hookPath, "utf-8");
2430
+ if (existsSync7(hookPath)) {
2431
+ const existing = readFileSync6(hookPath, "utf-8");
2081
2432
  if (containsPrimHook(existing, spec.binName)) {
2082
2433
  console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
2083
2434
  return;
2084
2435
  }
2085
2436
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
2086
- writeFileSync3(hookPath, `${existing}${separator}${huskyBlock(spec)}
2437
+ writeFileSync4(hookPath, `${existing}${separator}${huskyBlock(spec)}
2087
2438
  `, {
2088
2439
  mode: 493
2089
2440
  });
2090
2441
  console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
2091
2442
  } else {
2092
- writeFileSync3(hookPath, `#!/bin/sh
2443
+ writeFileSync4(hookPath, `#!/bin/sh
2093
2444
 
2094
2445
  ${huskyBlock(spec)}
2095
2446
  `, {
@@ -2101,11 +2452,11 @@ ${huskyBlock(spec)}
2101
2452
  function installToDotGit(gitRoot, spec = PRE_COMMIT) {
2102
2453
  const hooksDir = resolve(gitRoot, ".git", "hooks");
2103
2454
  const hookPath = resolve(hooksDir, spec.hookName);
2104
- if (!existsSync6(hooksDir)) {
2105
- mkdirSync4(hooksDir, { recursive: true });
2455
+ if (!existsSync7(hooksDir)) {
2456
+ mkdirSync5(hooksDir, { recursive: true });
2106
2457
  }
2107
- if (existsSync6(hookPath)) {
2108
- const existing = readFileSync5(hookPath, "utf-8");
2458
+ if (existsSync7(hookPath)) {
2459
+ const existing = readFileSync6(hookPath, "utf-8");
2109
2460
  if (containsPrimHook(existing, spec.binName)) {
2110
2461
  console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
2111
2462
  return;
@@ -2114,7 +2465,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
2114
2465
  console.log("To replace it, run: prim hooks uninstall && prim hooks install");
2115
2466
  return;
2116
2467
  }
2117
- writeFileSync3(hookPath, dotGitScript(spec), { mode: 493 });
2468
+ writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
2118
2469
  console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
2119
2470
  }
2120
2471
  function installHooks(gitRoot, target) {
@@ -2168,11 +2519,11 @@ function registerHooksCommands(program2) {
2168
2519
  const gitRoot = getGitRoot();
2169
2520
  for (const spec of HOOKS) {
2170
2521
  const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
2171
- if (!existsSync6(hookPath)) {
2522
+ if (!existsSync7(hookPath)) {
2172
2523
  console.log(`No ${spec.hookName} hook found.`);
2173
2524
  continue;
2174
2525
  }
2175
- if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
2526
+ if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
2176
2527
  unlinkSync2(hookPath);
2177
2528
  console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
2178
2529
  } else {
@@ -2183,11 +2534,11 @@ function registerHooksCommands(program2) {
2183
2534
  }
2184
2535
 
2185
2536
  // src/commands/moves.ts
2186
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
2187
- import { join as join5 } from "path";
2537
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
2538
+ import { join as join6 } from "path";
2188
2539
 
2189
2540
  // src/flusher.ts
2190
- import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
2541
+ import { renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
2191
2542
  var BATCH_SIZE = 500;
2192
2543
  var HTTP_TIMEOUT_MS = 1e4;
2193
2544
  var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
@@ -2219,7 +2570,7 @@ async function drainFlushingPath(flushingPath) {
2219
2570
  async function drainPath(path) {
2220
2571
  const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
2221
2572
  try {
2222
- renameSync2(path, tmpPath);
2573
+ renameSync3(path, tmpPath);
2223
2574
  } catch (err) {
2224
2575
  if (err.code === "ENOENT") {
2225
2576
  return 0;
@@ -2343,19 +2694,19 @@ function registerMovesCommands(program2) {
2343
2694
  }
2344
2695
  });
2345
2696
  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 = join5(process.cwd(), ".prim");
2347
- if (!existsSync7(dir)) {
2348
- mkdirSync5(dir, { recursive: true, mode: DIR_MODE });
2697
+ const dir = join6(process.cwd(), ".prim");
2698
+ if (!existsSync8(dir)) {
2699
+ mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
2349
2700
  }
2350
- const file = join5(process.cwd(), WORKSPACE_FILE);
2351
- writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
2701
+ const file = join6(process.cwd(), WORKSPACE_FILE);
2702
+ writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
2352
2703
  mode: FILE_MODE2
2353
2704
  });
2354
2705
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
2355
2706
  });
2356
2707
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
2357
- const file = join5(process.cwd(), WORKSPACE_FILE);
2358
- if (!existsSync7(file)) {
2708
+ const file = join6(process.cwd(), WORKSPACE_FILE);
2709
+ if (!existsSync8(file)) {
2359
2710
  console.log("[prim] no workspace binding in cwd");
2360
2711
  return;
2361
2712
  }
@@ -2448,23 +2799,23 @@ function registerReconcileCommands(program2) {
2448
2799
 
2449
2800
  // src/commands/session.ts
2450
2801
  import {
2451
- existsSync as existsSync8,
2452
- mkdirSync as mkdirSync6,
2453
- readFileSync as readFileSync6,
2802
+ existsSync as existsSync9,
2803
+ mkdirSync as mkdirSync7,
2804
+ readFileSync as readFileSync7,
2454
2805
  readdirSync,
2455
2806
  unlinkSync as unlinkSync5,
2456
- writeFileSync as writeFileSync5
2807
+ writeFileSync as writeFileSync6
2457
2808
  } from "fs";
2458
- import { join as join6 } from "path";
2809
+ import { join as join7 } from "path";
2459
2810
  var DIR_MODE2 = 448;
2460
2811
  var FILE_MODE3 = 384;
2461
2812
  function ensureDir() {
2462
- if (!existsSync8(SESSIONS_DIR)) {
2463
- mkdirSync6(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
2813
+ if (!existsSync9(SESSIONS_DIR)) {
2814
+ mkdirSync7(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
2464
2815
  }
2465
2816
  }
2466
2817
  function markerPath(sessionId) {
2467
- return join6(SESSIONS_DIR, `${sessionId}.json`);
2818
+ return join7(SESSIONS_DIR, `${sessionId}.json`);
2468
2819
  }
2469
2820
  function registerSessionCommands(program2) {
2470
2821
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -2474,13 +2825,13 @@ function registerSessionCommands(program2) {
2474
2825
  orgId: opts.orgId,
2475
2826
  startedAt: Date.now()
2476
2827
  };
2477
- writeFileSync5(markerPath(sessionId), JSON.stringify(marker, null, 2), {
2828
+ writeFileSync6(markerPath(sessionId), JSON.stringify(marker, null, 2), {
2478
2829
  mode: FILE_MODE3
2479
2830
  });
2480
2831
  console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
2481
2832
  });
2482
2833
  session.command("list").description("List active session markers").action(() => {
2483
- if (!existsSync8(SESSIONS_DIR)) {
2834
+ if (!existsSync9(SESSIONS_DIR)) {
2484
2835
  console.log("[prim] no session markers");
2485
2836
  return;
2486
2837
  }
@@ -2492,7 +2843,7 @@ function registerSessionCommands(program2) {
2492
2843
  for (const f of files) {
2493
2844
  const sessionId = f.replace(/\.json$/, "");
2494
2845
  try {
2495
- const m = JSON.parse(readFileSync6(join6(SESSIONS_DIR, f), "utf-8"));
2846
+ const m = JSON.parse(readFileSync7(join7(SESSIONS_DIR, f), "utf-8"));
2496
2847
  console.log(`${sessionId} org=${m.orgId}`);
2497
2848
  } catch {
2498
2849
  }
@@ -2500,7 +2851,7 @@ function registerSessionCommands(program2) {
2500
2851
  });
2501
2852
  session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
2502
2853
  const p = markerPath(sessionId);
2503
- if (!existsSync8(p)) {
2854
+ if (!existsSync9(p)) {
2504
2855
  console.log(`[prim] no marker for session ${sessionId}`);
2505
2856
  return;
2506
2857
  }
@@ -2513,13 +2864,19 @@ function registerSessionCommands(program2) {
2513
2864
  import { spawnSync } from "child_process";
2514
2865
  var EXIT_INCOMPLETE = 1;
2515
2866
  var EXIT_USAGE3 = 2;
2867
+ var SESSION_LABELS = {
2868
+ claude: "Claude Code integration",
2869
+ codex: "Codex integration",
2870
+ hermes: "Hermes integration"
2871
+ };
2516
2872
  function planSetupSteps(opts) {
2517
2873
  const scopeArgs = opts.scope === "user" ? ["--scope", "user"] : [];
2874
+ const sessionArgs = opts.agent === "hermes" ? [opts.agent, "install"] : [opts.agent, "install", ...scopeArgs];
2518
2875
  const steps = [
2519
2876
  {
2520
2877
  key: "session",
2521
- label: opts.agent === "codex" ? "Codex integration" : "Claude Code integration",
2522
- args: [opts.agent, "install", ...scopeArgs],
2878
+ label: SESSION_LABELS[opts.agent],
2879
+ args: sessionArgs,
2523
2880
  required: true
2524
2881
  }
2525
2882
  ];
@@ -2532,16 +2889,32 @@ function planSetupSteps(opts) {
2532
2889
  });
2533
2890
  }
2534
2891
  steps.push({ key: "hooks", label: "Git hooks", args: ["hooks", "install"], required: true });
2535
- steps.push({ key: "skill", label: "Agent skill", args: ["skill", "install"], required: true });
2892
+ const skillArgs = ["skill", "install", "--agent", opts.agent];
2893
+ steps.push({ key: "skill", label: "Agent skill", args: skillArgs, required: true });
2536
2894
  return steps;
2537
2895
  }
2896
+ function detectAgent(env) {
2897
+ if (env.HERMES_INTERACTIVE) {
2898
+ return "hermes";
2899
+ }
2900
+ return "claude";
2901
+ }
2902
+ function resolveAgent(agentFlag, env) {
2903
+ if (agentFlag !== void 0) {
2904
+ return { agent: agentFlag, detected: false };
2905
+ }
2906
+ return { agent: detectAgent(env), detected: true };
2907
+ }
2538
2908
  function registerSetupCommand(program2) {
2539
2909
  program2.command("setup").description(
2540
2910
  "Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
2541
- ).option("--agent <agent>", "claude or codex", "claude").option("--scope <scope>", "project or user (session integration)", "project").option("--no-daemon", "skip starting the companion daemon").action((opts) => {
2542
- if (opts.agent !== "claude" && opts.agent !== "codex") {
2543
- process.stderr.write(`[prim] unknown --agent "${opts.agent}" (expected claude or codex)
2544
- `);
2911
+ ).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) => {
2912
+ const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
2913
+ if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
2914
+ process.stderr.write(
2915
+ `[prim] unknown --agent "${agentInput}" (expected claude, codex, or hermes)
2916
+ `
2917
+ );
2545
2918
  process.exit(EXIT_USAGE3);
2546
2919
  }
2547
2920
  if (opts.scope !== "project" && opts.scope !== "user") {
@@ -2549,7 +2922,7 @@ function registerSetupCommand(program2) {
2549
2922
  `);
2550
2923
  process.exit(EXIT_USAGE3);
2551
2924
  }
2552
- const agent = opts.agent;
2925
+ const agent = agentInput;
2553
2926
  const scope = opts.scope;
2554
2927
  const self = process.argv[1];
2555
2928
  const run = (args, capture = false) => {
@@ -2571,6 +2944,9 @@ function registerSetupCommand(program2) {
2571
2944
  return false;
2572
2945
  }
2573
2946
  };
2947
+ if (detected && agent !== "claude") {
2948
+ note(`agent \xB7 detected ${agent} session (override with --agent <agent>)`);
2949
+ }
2574
2950
  if (agent === "claude") {
2575
2951
  note("pre-authorize \xB7 writing prim allow-rule (user scope)\u2026");
2576
2952
  results.preauth = run(["claude", "preauth", "--scope", "user"]).code === 0 ? "ok" : "skipped";
@@ -2601,39 +2977,45 @@ function registerSetupCommand(program2) {
2601
2977
 
2602
2978
  // src/commands/skill.ts
2603
2979
  import {
2604
- closeSync as closeSync3,
2605
- existsSync as existsSync9,
2606
- fsyncSync as fsyncSync2,
2607
- openSync as openSync3,
2608
- readFileSync as readFileSync7,
2609
- renameSync as renameSync3,
2610
- writeFileSync as writeFileSync6
2980
+ closeSync as closeSync4,
2981
+ existsSync as existsSync10,
2982
+ fsyncSync as fsyncSync3,
2983
+ openSync as openSync4,
2984
+ readFileSync as readFileSync8,
2985
+ renameSync as renameSync4,
2986
+ writeFileSync as writeFileSync7
2611
2987
  } from "fs";
2612
- import { dirname as dirname4, resolve as resolve2 } from "path";
2988
+ import { dirname as dirname5, resolve as resolve2 } from "path";
2613
2989
  import { fileURLToPath as fileURLToPath2 } from "url";
2614
2990
  import { createPatch } from "diff";
2615
- var __dirname = dirname4(fileURLToPath2(import.meta.url));
2991
+ var __dirname = dirname5(fileURLToPath2(import.meta.url));
2616
2992
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
2617
2993
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
2618
2994
  var TARGET_CANDIDATES = [
2619
2995
  "CLAUDE.md",
2620
2996
  "AGENTS.md",
2997
+ ".hermes.md",
2621
2998
  ".cursor/rules",
2622
2999
  ".windsurfrules",
2623
3000
  ".github/instructions/primitive.md"
2624
3001
  ];
2625
3002
  var DEFAULT_TARGET = "CLAUDE.md";
3003
+ var AGENT_TARGET = {
3004
+ claude: "CLAUDE.md",
3005
+ codex: "AGENTS.md",
3006
+ hermes: ".hermes.md"
3007
+ };
2626
3008
  function loadSkill() {
2627
3009
  let dir = __dirname;
2628
- while (dir !== dirname4(dir)) {
3010
+ while (dir !== dirname5(dir)) {
2629
3011
  const p = resolve2(dir, "SKILL.md");
2630
- if (existsSync9(p)) return readFileSync7(p, "utf-8");
2631
- dir = dirname4(dir);
3012
+ if (existsSync10(p)) return readFileSync8(p, "utf-8");
3013
+ dir = dirname5(dir);
2632
3014
  }
2633
3015
  throw new Error("SKILL.md not found in package");
2634
3016
  }
2635
3017
  function detectTargets(cwd) {
2636
- return TARGET_CANDIDATES.filter((p) => existsSync9(resolve2(cwd, p)));
3018
+ return TARGET_CANDIDATES.filter((p) => existsSync10(resolve2(cwd, p)));
2637
3019
  }
2638
3020
  function detectNewline(content) {
2639
3021
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -2661,17 +3043,23 @@ function removeBlock(existing) {
2661
3043
  }
2662
3044
  function atomicWrite2(target, content) {
2663
3045
  const tmp = `${target}.tmp`;
2664
- writeFileSync6(tmp, content);
2665
- const fd = openSync3(tmp, "r+");
3046
+ writeFileSync7(tmp, content);
3047
+ const fd = openSync4(tmp, "r+");
2666
3048
  try {
2667
- fsyncSync2(fd);
3049
+ fsyncSync3(fd);
2668
3050
  } finally {
2669
- closeSync3(fd);
3051
+ closeSync4(fd);
2670
3052
  }
2671
- renameSync3(tmp, target);
3053
+ renameSync4(tmp, target);
2672
3054
  }
2673
- function resolveTarget(cwd, override) {
2674
- if (override) return resolve2(cwd, override);
3055
+ function resolveTarget(cwd, opts) {
3056
+ if (opts.target) return resolve2(cwd, opts.target);
3057
+ if (opts.agent) {
3058
+ const mapped = AGENT_TARGET[opts.agent];
3059
+ if (typeof mapped === "string") return resolve2(cwd, mapped);
3060
+ console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
3061
+ return null;
3062
+ }
2675
3063
  const matches = detectTargets(cwd);
2676
3064
  if (matches.length === 0) return resolve2(cwd, DEFAULT_TARGET);
2677
3065
  if (matches.length === 1) return resolve2(cwd, matches[0]);
@@ -2680,9 +3068,9 @@ function resolveTarget(cwd, override) {
2680
3068
  return null;
2681
3069
  }
2682
3070
  function runInstall(cwd, opts) {
2683
- const target = resolveTarget(cwd, opts.target);
3071
+ const target = resolveTarget(cwd, opts);
2684
3072
  if (target === null) return 1;
2685
- const existing = existsSync9(target) ? readFileSync7(target, "utf-8") : "";
3073
+ const existing = existsSync10(target) ? readFileSync8(target, "utf-8") : "";
2686
3074
  const eol = existing ? detectNewline(existing) : "\n";
2687
3075
  const block = composeBlock(loadSkill(), eol);
2688
3076
  const next = applyBlock(existing, block, eol);
@@ -2699,13 +3087,13 @@ function runInstall(cwd, opts) {
2699
3087
  return 0;
2700
3088
  }
2701
3089
  function runUninstall(cwd, opts) {
2702
- const target = resolveTarget(cwd, opts.target);
3090
+ const target = resolveTarget(cwd, opts);
2703
3091
  if (target === null) return 1;
2704
- if (!existsSync9(target)) {
3092
+ if (!existsSync10(target)) {
2705
3093
  console.log(`Skill block not present at ${target}`);
2706
3094
  return 0;
2707
3095
  }
2708
- const existing = readFileSync7(target, "utf-8");
3096
+ const existing = readFileSync8(target, "utf-8");
2709
3097
  const next = removeBlock(existing);
2710
3098
  if (next === null) {
2711
3099
  console.log(`Skill block not present at ${target}`);
@@ -2716,12 +3104,12 @@ function runUninstall(cwd, opts) {
2716
3104
  return 0;
2717
3105
  }
2718
3106
  function runStatus(cwd, opts) {
2719
- const target = resolveTarget(cwd, opts.target);
3107
+ const target = resolveTarget(cwd, opts);
2720
3108
  if (target === null) return 1;
2721
- const fileExists = existsSync9(target);
3109
+ const fileExists = existsSync10(target);
2722
3110
  let installed = false;
2723
3111
  if (fileExists) {
2724
- const content = readFileSync7(target, "utf-8");
3112
+ const content = readFileSync8(target, "utf-8");
2725
3113
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
2726
3114
  }
2727
3115
  if (opts.json) {
@@ -2741,7 +3129,7 @@ function runStatus(cwd, opts) {
2741
3129
  }
2742
3130
  function registerSkillCommands(program2) {
2743
3131
  const skill = program2.command("skill").description("Manage the prim skill in your project rules file");
2744
- skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
3132
+ skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
2745
3133
  try {
2746
3134
  process.exit(runInstall(process.cwd(), opts));
2747
3135
  } catch (err) {
@@ -2749,27 +3137,27 @@ function registerSkillCommands(program2) {
2749
3137
  process.exit(2);
2750
3138
  }
2751
3139
  });
2752
- skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").action((opts) => {
3140
+ skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").action((opts) => {
2753
3141
  process.exit(runUninstall(process.cwd(), opts));
2754
3142
  });
2755
- skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--json", "Output as JSON").action((opts) => {
3143
+ skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--json", "Output as JSON").action((opts) => {
2756
3144
  process.exit(runStatus(process.cwd(), opts));
2757
3145
  });
2758
3146
  }
2759
3147
 
2760
3148
  // src/commands/statusline.ts
2761
- import { readFileSync as readFileSync8 } from "fs";
2762
- import { dirname as dirname5, resolve as resolve3 } from "path";
3149
+ import { readFileSync as readFileSync9 } from "fs";
3150
+ import { dirname as dirname6, resolve as resolve3 } from "path";
2763
3151
  import { fileURLToPath as fileURLToPath3 } from "url";
2764
3152
  var STATUSLINE_TIMEOUT_MS = 200;
2765
3153
  var STATUSLINE_NAME_CAP = 3;
2766
3154
  function readPackageVersion() {
2767
3155
  try {
2768
- const here = dirname5(fileURLToPath3(import.meta.url));
3156
+ const here = dirname6(fileURLToPath3(import.meta.url));
2769
3157
  const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
2770
3158
  for (const path of candidates) {
2771
3159
  try {
2772
- const pkg2 = JSON.parse(readFileSync8(path, "utf-8"));
3160
+ const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
2773
3161
  if (pkg2.version) {
2774
3162
  return pkg2.version;
2775
3163
  }
@@ -2936,8 +3324,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
2936
3324
  }
2937
3325
 
2938
3326
  // src/index.ts
2939
- var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
2940
- var pkg = JSON.parse(readFileSync9(resolve4(__dirname2, "../package.json"), "utf-8"));
3327
+ var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
3328
+ var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
2941
3329
  updateNotifier({ pkg }).notify();
2942
3330
  var program = new Command();
2943
3331
  program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
@@ -2952,6 +3340,7 @@ registerSessionCommands(program);
2952
3340
  registerDecisionsCommands(program);
2953
3341
  registerClaudeCommands(program);
2954
3342
  registerCodexCommands(program);
3343
+ registerHermesCommands(program);
2955
3344
  registerDaemonCommands(program);
2956
3345
  registerDoctorCommands(program);
2957
3346
  registerReconcileCommands(program);