@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/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 +482 -93
- package/package.json +3 -2
- package/dist/chunk-7YRBACIE.js +0 -9
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";
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
449
|
-
|
|
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
|
|
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 (!
|
|
2047
|
-
if (
|
|
2048
|
-
if (
|
|
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 (
|
|
2401
|
+
if (existsSync7(pkgPath)) {
|
|
2051
2402
|
try {
|
|
2052
|
-
const pkg2 = JSON.parse(
|
|
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 (
|
|
2080
|
-
const existing =
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
2105
|
-
|
|
2455
|
+
if (!existsSync7(hooksDir)) {
|
|
2456
|
+
mkdirSync5(hooksDir, { recursive: true });
|
|
2106
2457
|
}
|
|
2107
|
-
if (
|
|
2108
|
-
const existing =
|
|
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
|
-
|
|
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 (!
|
|
2522
|
+
if (!existsSync7(hookPath)) {
|
|
2172
2523
|
console.log(`No ${spec.hookName} hook found.`);
|
|
2173
2524
|
continue;
|
|
2174
2525
|
}
|
|
2175
|
-
if (containsPrimHook(
|
|
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
|
|
2187
|
-
import { join as
|
|
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
|
|
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
|
-
|
|
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 =
|
|
2347
|
-
if (!
|
|
2348
|
-
|
|
2697
|
+
const dir = join6(process.cwd(), ".prim");
|
|
2698
|
+
if (!existsSync8(dir)) {
|
|
2699
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
|
|
2349
2700
|
}
|
|
2350
|
-
const file =
|
|
2351
|
-
|
|
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 =
|
|
2358
|
-
if (!
|
|
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
|
|
2452
|
-
mkdirSync as
|
|
2453
|
-
readFileSync as
|
|
2802
|
+
existsSync as existsSync9,
|
|
2803
|
+
mkdirSync as mkdirSync7,
|
|
2804
|
+
readFileSync as readFileSync7,
|
|
2454
2805
|
readdirSync,
|
|
2455
2806
|
unlinkSync as unlinkSync5,
|
|
2456
|
-
writeFileSync as
|
|
2807
|
+
writeFileSync as writeFileSync6
|
|
2457
2808
|
} from "fs";
|
|
2458
|
-
import { join as
|
|
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 (!
|
|
2463
|
-
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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
|
|
2522
|
-
args:
|
|
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
|
-
|
|
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
|
|
2542
|
-
|
|
2543
|
-
|
|
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 =
|
|
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
|
|
2605
|
-
existsSync as
|
|
2606
|
-
fsyncSync as
|
|
2607
|
-
openSync as
|
|
2608
|
-
readFileSync as
|
|
2609
|
-
renameSync as
|
|
2610
|
-
writeFileSync as
|
|
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
|
|
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 =
|
|
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 !==
|
|
3010
|
+
while (dir !== dirname5(dir)) {
|
|
2629
3011
|
const p = resolve2(dir, "SKILL.md");
|
|
2630
|
-
if (
|
|
2631
|
-
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) =>
|
|
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
|
-
|
|
2665
|
-
const fd =
|
|
3046
|
+
writeFileSync7(tmp, content);
|
|
3047
|
+
const fd = openSync4(tmp, "r+");
|
|
2666
3048
|
try {
|
|
2667
|
-
|
|
3049
|
+
fsyncSync3(fd);
|
|
2668
3050
|
} finally {
|
|
2669
|
-
|
|
3051
|
+
closeSync4(fd);
|
|
2670
3052
|
}
|
|
2671
|
-
|
|
3053
|
+
renameSync4(tmp, target);
|
|
2672
3054
|
}
|
|
2673
|
-
function resolveTarget(cwd,
|
|
2674
|
-
if (
|
|
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
|
|
3071
|
+
const target = resolveTarget(cwd, opts);
|
|
2684
3072
|
if (target === null) return 1;
|
|
2685
|
-
const existing =
|
|
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
|
|
3090
|
+
const target = resolveTarget(cwd, opts);
|
|
2703
3091
|
if (target === null) return 1;
|
|
2704
|
-
if (!
|
|
3092
|
+
if (!existsSync10(target)) {
|
|
2705
3093
|
console.log(`Skill block not present at ${target}`);
|
|
2706
3094
|
return 0;
|
|
2707
3095
|
}
|
|
2708
|
-
const existing =
|
|
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
|
|
3107
|
+
const target = resolveTarget(cwd, opts);
|
|
2720
3108
|
if (target === null) return 1;
|
|
2721
|
-
const fileExists =
|
|
3109
|
+
const fileExists = existsSync10(target);
|
|
2722
3110
|
let installed = false;
|
|
2723
3111
|
if (fileExists) {
|
|
2724
|
-
const content =
|
|
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
|
|
2762
|
-
import { dirname as
|
|
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 =
|
|
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(
|
|
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 =
|
|
2940
|
-
var pkg = JSON.parse(
|
|
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);
|