@primitive.ai/prim 0.1.0-alpha.30 → 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/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  listBuckets,
19
19
  listFlushing,
20
20
  readMovesFromPath
21
- } from "./chunk-C4UYMR7X.js";
21
+ } from "./chunk-H4OR42TJ.js";
22
22
  import {
23
23
  HttpError,
24
24
  REFRESH_TOKEN_PATH,
@@ -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";
@@ -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 existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
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 (!existsSync6(huskyDir)) return false;
2047
- if (existsSync6(resolve(huskyDir, "_"))) return true;
2048
- if (existsSync6(resolve(huskyDir, "pre-commit"))) return true;
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 (existsSync6(pkgPath)) {
2394
+ if (existsSync7(pkgPath)) {
2051
2395
  try {
2052
- const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
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 (existsSync6(hookPath)) {
2080
- const existing = readFileSync5(hookPath, "utf-8");
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
- writeFileSync3(hookPath, `${existing}${separator}${huskyBlock(spec)}
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
- writeFileSync3(hookPath, `#!/bin/sh
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 (!existsSync6(hooksDir)) {
2105
- mkdirSync4(hooksDir, { recursive: true });
2448
+ if (!existsSync7(hooksDir)) {
2449
+ mkdirSync5(hooksDir, { recursive: true });
2106
2450
  }
2107
- if (existsSync6(hookPath)) {
2108
- const existing = readFileSync5(hookPath, "utf-8");
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
- writeFileSync3(hookPath, dotGitScript(spec), { mode: 493 });
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 (!existsSync6(hookPath)) {
2515
+ if (!existsSync7(hookPath)) {
2172
2516
  console.log(`No ${spec.hookName} hook found.`);
2173
2517
  continue;
2174
2518
  }
2175
- if (containsPrimHook(readFileSync5(hookPath, "utf-8"), spec.binName)) {
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 existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
2187
- import { join as join5 } from "path";
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 renameSync2, unlinkSync as unlinkSync3 } from "fs";
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
- renameSync2(path, tmpPath);
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 = join5(process.cwd(), ".prim");
2347
- if (!existsSync7(dir)) {
2348
- mkdirSync5(dir, { recursive: true, mode: DIR_MODE });
2690
+ const dir = join6(process.cwd(), ".prim");
2691
+ if (!existsSync8(dir)) {
2692
+ mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
2349
2693
  }
2350
- const file = join5(process.cwd(), WORKSPACE_FILE);
2351
- writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
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 = join5(process.cwd(), WORKSPACE_FILE);
2358
- if (!existsSync7(file)) {
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 existsSync8,
2452
- mkdirSync as mkdirSync6,
2453
- readFileSync as readFileSync6,
2795
+ existsSync as existsSync9,
2796
+ mkdirSync as mkdirSync7,
2797
+ readFileSync as readFileSync7,
2454
2798
  readdirSync,
2455
2799
  unlinkSync as unlinkSync5,
2456
- writeFileSync as writeFileSync5
2800
+ writeFileSync as writeFileSync6
2457
2801
  } from "fs";
2458
- import { join as join6 } from "path";
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 (!existsSync8(SESSIONS_DIR)) {
2463
- mkdirSync6(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
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 join6(SESSIONS_DIR, `${sessionId}.json`);
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
- writeFileSync5(markerPath(sessionId), JSON.stringify(marker, null, 2), {
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 (!existsSync8(SESSIONS_DIR)) {
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(readFileSync6(join6(SESSIONS_DIR, f), "utf-8"));
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 (!existsSync8(p)) {
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 === "codex" ? "Codex integration" : "Claude Code integration",
2522
- args: [opts.agent, "install", ...scopeArgs],
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
- steps.push({ key: "skill", label: "Agent skill", args: ["skill", "install"], required: true });
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 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
- `);
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 = opts.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 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
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 dirname4, resolve as resolve2 } from "path";
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 = dirname4(fileURLToPath2(import.meta.url));
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 !== dirname4(dir)) {
2998
+ while (dir !== dirname5(dir)) {
2629
2999
  const p = resolve2(dir, "SKILL.md");
2630
- if (existsSync9(p)) return readFileSync7(p, "utf-8");
2631
- dir = dirname4(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) => existsSync9(resolve2(cwd, 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
- writeFileSync6(tmp, content);
2665
- const fd = openSync3(tmp, "r+");
3034
+ writeFileSync7(tmp, content);
3035
+ const fd = openSync4(tmp, "r+");
2666
3036
  try {
2667
- fsyncSync2(fd);
3037
+ fsyncSync3(fd);
2668
3038
  } finally {
2669
- closeSync3(fd);
3039
+ closeSync4(fd);
2670
3040
  }
2671
- renameSync3(tmp, target);
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 = existsSync9(target) ? readFileSync7(target, "utf-8") : "";
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 (!existsSync9(target)) {
3074
+ if (!existsSync10(target)) {
2705
3075
  console.log(`Skill block not present at ${target}`);
2706
3076
  return 0;
2707
3077
  }
2708
- const existing = readFileSync7(target, "utf-8");
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 = existsSync9(target);
3091
+ const fileExists = existsSync10(target);
2722
3092
  let installed = false;
2723
3093
  if (fileExists) {
2724
- const content = readFileSync7(target, "utf-8");
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 readFileSync8 } from "fs";
2762
- import { dirname as dirname5, resolve as resolve3 } from "path";
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 = dirname5(fileURLToPath3(import.meta.url));
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(readFileSync8(path, "utf-8"));
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 = dirname6(fileURLToPath4(import.meta.url));
2940
- var pkg = JSON.parse(readFileSync9(resolve4(__dirname2, "../package.json"), "utf-8"));
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);