@primitive.ai/prim 0.1.0-alpha.50 → 0.1.0-alpha.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prim
3
- description: Use the prim CLI for Primitive's decision graph passive capture of coding decisions, deliberate recording of higher-order forks in the road, the conflict gate, reconcile, rationale confirmations, and team presence. TRIGGER when the user mentions Primitive, prim, decisions / the decision graph / a conflict gate / reconcile; when a durable decision emerges during coding, planning, review, or connected-context work; when an edit is denied or warned by a prior decision; when the repo's package.json depends on @primitive.ai/prim; when onboarding to or configuring Primitive session or git hooks. SKIP when "decision" is unrelated to Primitive, or for unrelated CLIs.
3
+ description: Use the prim CLI for Primitives decision graph. MUST INVOKE before finishing any coding, planning, specification, or review task where the user or agent chose between plausible approaches or established or changed a lasting goal, priority, constraint, invariant, default, commitment, tradeoff, exception, or shared instruction—even when Primitive was not mentioned. Also invoke for Primitive setup, reading decisions, conflict gates, reconcile, rationale confirmations, linking, and team presence. SKIP routine implementation that merely follows an existing decision, temporary tactics, and unrelated uses of “decision.”
4
4
  ---
5
5
 
6
6
  # Working with the prim CLI
@@ -2,11 +2,11 @@
2
2
  function buildHookOutput(options) {
3
3
  const output = {};
4
4
  if (options.systemMessage) output.systemMessage = options.systemMessage;
5
- if (options.additionalContext) {
6
- output.hookSpecificOutput = {
7
- hookEventName: "SessionStart",
8
- additionalContext: options.additionalContext
9
- };
5
+ const fields = {};
6
+ if (options.additionalContext) fields.additionalContext = options.additionalContext;
7
+ if (options.reloadSkills) fields.reloadSkills = true;
8
+ if (Object.keys(fields).length > 0) {
9
+ output.hookSpecificOutput = { hookEventName: "SessionStart", ...fields };
10
10
  }
11
11
  return output;
12
12
  }
@@ -0,0 +1,520 @@
1
+ import {
2
+ gitToplevel
3
+ } from "./chunk-R5ZJRSLV.js";
4
+ import {
5
+ withFileLock
6
+ } from "./chunk-DWDEQRWN.js";
7
+
8
+ // src/commands/skill.ts
9
+ import { randomUUID } from "crypto";
10
+ import {
11
+ closeSync as closeSync2,
12
+ existsSync as existsSync2,
13
+ fsyncSync,
14
+ openSync as openSync2,
15
+ readFileSync as readFileSync2,
16
+ renameSync,
17
+ rmSync as rmSync2,
18
+ writeFileSync
19
+ } from "fs";
20
+ import { homedir as homedir2 } from "os";
21
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
22
+ import { fileURLToPath as fileURLToPath2 } from "url";
23
+ import { createPatch } from "diff";
24
+
25
+ // src/output.ts
26
+ function printJson(data) {
27
+ console.log(JSON.stringify(data, null, 2));
28
+ }
29
+
30
+ // src/commands/claude-plugin.ts
31
+ import { createHash } from "crypto";
32
+ import {
33
+ constants,
34
+ closeSync,
35
+ existsSync,
36
+ fstatSync,
37
+ lstatSync,
38
+ mkdirSync,
39
+ openSync,
40
+ readFileSync,
41
+ readSync,
42
+ readdirSync,
43
+ realpathSync,
44
+ rmSync,
45
+ rmdirSync
46
+ } from "fs";
47
+ import { homedir } from "os";
48
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
49
+ import { fileURLToPath } from "url";
50
+ import { parse as parseYaml } from "yaml";
51
+ var __dirname = dirname(fileURLToPath(import.meta.url));
52
+ var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
53
+ var MAX_MANIFEST_BYTES = 64 * 1024;
54
+ var MAX_SKILL_BYTES = 1024 * 1024;
55
+ var REFRESH_LOCK_TIMEOUT_MS = 150;
56
+ var REFRESH_LOCK_POLL_MS = 10;
57
+ function pluginDirFor(cwd, scope) {
58
+ const base = scope === "user" ? join(homedir(), ".claude") : join(gitToplevel(cwd) ?? cwd, ".claude");
59
+ return join(base, "skills", "prim");
60
+ }
61
+ function resolvePluginDir(cwd, scope) {
62
+ if (scope && scope !== "user" && scope !== "project") {
63
+ console.error(`Unknown --scope "${scope}" (expected user or project)`);
64
+ return null;
65
+ }
66
+ return pluginDirFor(cwd, scope === "user" ? "user" : "project");
67
+ }
68
+ function packageVersion() {
69
+ let dir = __dirname;
70
+ while (dir !== dirname(dir)) {
71
+ const p = resolve(dir, "package.json");
72
+ if (existsSync(p)) return JSON.parse(readFileSync(p, "utf-8")).version;
73
+ dir = dirname(dir);
74
+ }
75
+ return "0.0.0";
76
+ }
77
+ function pluginManifest(version) {
78
+ const manifest = { name: "prim", description: PLUGIN_DESCRIPTION, version };
79
+ return `${JSON.stringify(manifest, null, 2)}
80
+ `;
81
+ }
82
+ function renderClaudePlugin() {
83
+ const version = packageVersion();
84
+ return { manifest: pluginManifest(version), skill: loadSkill(), version };
85
+ }
86
+ function removeDirIfEmpty(dir) {
87
+ if (existsSync(dir) && readdirSync(dir).length === 0) rmdirSync(dir);
88
+ }
89
+ function pluginPaths(dir) {
90
+ return {
91
+ manifestPath: join(dir, ".claude-plugin", "plugin.json"),
92
+ skillPath: join(dir, "SKILL.md")
93
+ };
94
+ }
95
+ function parseSemver(value) {
96
+ if (typeof value !== "string" || value.length > 256) return;
97
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
98
+ value
99
+ );
100
+ if (!match) return;
101
+ const prerelease = match[4]?.split(".") ?? [];
102
+ if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
103
+ return;
104
+ }
105
+ return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
106
+ }
107
+ function compareSemver(left, right) {
108
+ const a = parseSemver(left);
109
+ const b = parseSemver(right);
110
+ if (!a || !b) return;
111
+ for (let i = 0; i < a.core.length; i += 1) {
112
+ if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
113
+ }
114
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
115
+ return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
116
+ }
117
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
118
+ for (let i = 0; i < length; i += 1) {
119
+ const x = a.prerelease[i];
120
+ const y = b.prerelease[i];
121
+ if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
122
+ if (x === y) continue;
123
+ const xNumeric = /^\d+$/u.test(x);
124
+ const yNumeric = /^\d+$/u.test(y);
125
+ if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
126
+ if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
127
+ return x < y ? -1 : 1;
128
+ }
129
+ return 0;
130
+ }
131
+ function pathIsInside(root, path) {
132
+ const rel = relative(root, path);
133
+ return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
134
+ }
135
+ function managedPathsAreSafe(paths, projectRoot) {
136
+ try {
137
+ const managed = [paths.manifestPath, paths.skillPath];
138
+ if (managed.some((path) => !lstatSync(path).isFile())) return false;
139
+ if (projectRoot === void 0) return true;
140
+ const realRoot = realpathSync(projectRoot);
141
+ return managed.every((path) => pathIsInside(realRoot, realpathSync(path)));
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+ function readBoundedRegularFile(path, maxBytes) {
147
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
148
+ try {
149
+ const stat = fstatSync(fd);
150
+ if (!stat.isFile() || stat.size > maxBytes) throw new Error("unsafe managed file");
151
+ const chunks = [];
152
+ let total = 0;
153
+ while (total <= maxBytes) {
154
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
155
+ const bytesRead = readSync(fd, chunk, 0, chunk.length, null);
156
+ if (bytesRead === 0) break;
157
+ chunks.push(chunk.subarray(0, bytesRead));
158
+ total += bytesRead;
159
+ }
160
+ if (total > maxBytes) throw new Error("unsafe managed file");
161
+ return Buffer.concat(chunks, total).toString("utf8");
162
+ } finally {
163
+ closeSync(fd);
164
+ }
165
+ }
166
+ function hasUsablePrimFrontmatter(skill) {
167
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/u.exec(skill);
168
+ if (!match) return false;
169
+ try {
170
+ const parsed = parseYaml(match[1]);
171
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
172
+ const frontmatter = parsed;
173
+ return frontmatter.name === "prim" && typeof frontmatter.description === "string" && frontmatter.description.trim().length > 0 && match[2].trim().length > 0;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+ function inspectRecognizedInstallation(paths, projectRoot) {
179
+ if (!existsSync(paths.manifestPath) || !existsSync(paths.skillPath)) return;
180
+ if (!managedPathsAreSafe(paths, projectRoot)) return;
181
+ const manifest = readBoundedRegularFile(paths.manifestPath, MAX_MANIFEST_BYTES);
182
+ const parsed = JSON.parse(manifest);
183
+ if (parsed.name !== "prim") return;
184
+ const skill = readBoundedRegularFile(paths.skillPath, MAX_SKILL_BYTES);
185
+ return {
186
+ manifest,
187
+ manifestVersion: parsed.version,
188
+ skill,
189
+ usable: hasUsablePrimFrontmatter(skill)
190
+ };
191
+ }
192
+ function refreshLockDir(physicalPluginDir) {
193
+ const cacheRoot = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
194
+ const identity = createHash("sha256").update(physicalPluginDir).digest("hex");
195
+ return join(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
196
+ }
197
+ async function refreshClaudePlugins(cwd, options = {}) {
198
+ const result = { installed: 0, refreshed: 0 };
199
+ const candidates = [];
200
+ if (options.includeProject !== false) {
201
+ const projectRoot = options.projectRoot ?? gitToplevel(cwd);
202
+ if (projectRoot !== null && isAbsolute(projectRoot)) {
203
+ candidates.push({ dir: join(projectRoot, ".claude", "skills", "prim"), projectRoot });
204
+ }
205
+ }
206
+ candidates.push({ dir: pluginDirFor(cwd, "user") });
207
+ const seen = /* @__PURE__ */ new Set();
208
+ let desired;
209
+ for (const { dir, projectRoot } of candidates) {
210
+ let counted = false;
211
+ const markInstalled = () => {
212
+ if (!counted) {
213
+ result.installed += 1;
214
+ counted = true;
215
+ }
216
+ };
217
+ const refreshCandidate = () => {
218
+ const { manifestPath, skillPath } = pluginPaths(dir);
219
+ const paths = { manifestPath, skillPath };
220
+ try {
221
+ let changed = false;
222
+ const current = inspectRecognizedInstallation(paths, projectRoot);
223
+ if (!current) return;
224
+ desired ??= renderClaudePlugin();
225
+ if (projectRoot !== void 0 && current.usable && compareSemver(current.manifestVersion, desired.version) === 1) {
226
+ markInstalled();
227
+ return;
228
+ }
229
+ if (current.manifest !== desired.manifest) {
230
+ if (!managedPathsAreSafe(paths, projectRoot)) throw new Error("unsafe managed path");
231
+ atomicWrite(manifestPath, desired.manifest);
232
+ if (!changed) result.refreshed += 1;
233
+ changed = true;
234
+ }
235
+ if (current.skill !== desired.skill) {
236
+ if (!managedPathsAreSafe(paths, projectRoot)) throw new Error("unsafe managed path");
237
+ atomicWrite(skillPath, desired.skill);
238
+ if (!changed) result.refreshed += 1;
239
+ changed = true;
240
+ }
241
+ markInstalled();
242
+ } catch {
243
+ try {
244
+ if (inspectRecognizedInstallation(paths, projectRoot)?.usable) markInstalled();
245
+ } catch {
246
+ }
247
+ }
248
+ };
249
+ try {
250
+ const paths = pluginPaths(dir);
251
+ if (!existsSync(paths.manifestPath) || !existsSync(paths.skillPath)) continue;
252
+ const physicalPluginDir = realpathSync(dir);
253
+ if (seen.has(physicalPluginDir)) continue;
254
+ seen.add(physicalPluginDir);
255
+ await withFileLock(refreshLockDir(physicalPluginDir), refreshCandidate, {
256
+ timeoutMs: REFRESH_LOCK_TIMEOUT_MS,
257
+ pollMs: REFRESH_LOCK_POLL_MS
258
+ });
259
+ } catch {
260
+ }
261
+ }
262
+ return result;
263
+ }
264
+ function installClaudePlugin(cwd, opts) {
265
+ const dir = resolvePluginDir(cwd, opts.scope);
266
+ if (dir === null) return 1;
267
+ const { manifestPath, skillPath } = pluginPaths(dir);
268
+ const { manifest, skill } = renderClaudePlugin();
269
+ const manifestCurrent = existsSync(manifestPath) ? readFileSync(manifestPath, "utf-8") : null;
270
+ const skillCurrent = existsSync(skillPath) ? readFileSync(skillPath, "utf-8") : null;
271
+ if (manifestCurrent === manifest && skillCurrent === skill) {
272
+ console.log("No changes \u2014 prim skill plugin already up to date.");
273
+ return 0;
274
+ }
275
+ if (opts.dryRun) {
276
+ console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
277
+ return 0;
278
+ }
279
+ mkdirSync(join(dir, ".claude-plugin"), { recursive: true });
280
+ atomicWrite(manifestPath, manifest);
281
+ atomicWrite(skillPath, skill);
282
+ console.log(`Installed prim skill plugin at ${dir}`);
283
+ console.log("Restart Claude Code or run /reload-plugins to load it.");
284
+ if (opts.scope !== "user") {
285
+ console.log("Project-scope plugins load only when Claude Code launches from this directory.");
286
+ }
287
+ return 0;
288
+ }
289
+ function uninstallClaudePlugin(cwd, opts) {
290
+ const dir = resolvePluginDir(cwd, opts.scope);
291
+ if (dir === null) return 1;
292
+ const { manifestPath, skillPath } = pluginPaths(dir);
293
+ if (!existsSync(manifestPath) && !existsSync(skillPath)) {
294
+ console.log(`prim skill plugin not present at ${dir}`);
295
+ return 0;
296
+ }
297
+ rmSync(manifestPath, { force: true });
298
+ rmSync(skillPath, { force: true });
299
+ removeDirIfEmpty(join(dir, ".claude-plugin"));
300
+ removeDirIfEmpty(dir);
301
+ console.log(`Removed prim skill plugin from ${dir}`);
302
+ return 0;
303
+ }
304
+ function statusClaudePlugin(cwd, opts) {
305
+ const dir = resolvePluginDir(cwd, opts.scope);
306
+ if (dir === null) return 1;
307
+ const { skillPath } = pluginPaths(dir);
308
+ const installed = existsSync(skillPath);
309
+ if (opts.json) {
310
+ printJson({ installed, target: dir });
311
+ return installed ? 0 : 1;
312
+ }
313
+ if (installed) {
314
+ console.log(`prim skill plugin installed at ${dir}`);
315
+ return 0;
316
+ }
317
+ console.log(`No prim skill plugin at ${dir}`);
318
+ return 1;
319
+ }
320
+
321
+ // src/commands/skill.ts
322
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
323
+ var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
324
+ var SKILL_END = "<!-- END PRIM SKILL v1 -->";
325
+ var TARGET_CANDIDATES = [
326
+ "CLAUDE.md",
327
+ "AGENTS.md",
328
+ ".hermes.md",
329
+ ".cursor/rules",
330
+ ".windsurfrules",
331
+ ".github/instructions/primitive.md"
332
+ ];
333
+ var DEFAULT_TARGET = "CLAUDE.md";
334
+ var AGENT_TARGET = {
335
+ claude: "CLAUDE.md",
336
+ codex: "AGENTS.md",
337
+ hermes: ".hermes.md"
338
+ };
339
+ function userTargetFor(agent) {
340
+ if (agent === "claude") return join2(homedir2(), ".claude", "CLAUDE.md");
341
+ if (agent === "codex") return join2(homedir2(), ".codex", "AGENTS.md");
342
+ if (agent === "hermes") {
343
+ return join2(process.env.HERMES_HOME ?? join2(homedir2(), ".hermes"), ".hermes.md");
344
+ }
345
+ return null;
346
+ }
347
+ function loadSkill() {
348
+ let dir = __dirname2;
349
+ while (dir !== dirname2(dir)) {
350
+ const p = resolve2(dir, "SKILL.md");
351
+ if (existsSync2(p)) return readFileSync2(p, "utf-8");
352
+ dir = dirname2(dir);
353
+ }
354
+ throw new Error("SKILL.md not found in package");
355
+ }
356
+ function detectTargets(cwd) {
357
+ return TARGET_CANDIDATES.filter((p) => existsSync2(resolve2(cwd, p)));
358
+ }
359
+ function detectNewline(content) {
360
+ return content.includes("\r\n") ? "\r\n" : "\n";
361
+ }
362
+ function composeBlock(skill, eol) {
363
+ const body = skill.replace(/\r?\n/g, eol);
364
+ return `${SKILL_BEGIN}${eol}${body}${eol}${SKILL_END}`;
365
+ }
366
+ function applyBlock(existing, block, eol) {
367
+ const b = existing.indexOf(SKILL_BEGIN);
368
+ const e = existing.indexOf(SKILL_END);
369
+ if (b !== -1 && e !== -1) {
370
+ return existing.slice(0, b) + block + existing.slice(e + SKILL_END.length);
371
+ }
372
+ if (existing.length === 0) return `${block}${eol}`;
373
+ const sep2 = existing.endsWith(eol) ? "" : eol;
374
+ return `${existing}${sep2}${block}${eol}`;
375
+ }
376
+ function removeBlock(existing) {
377
+ const b = existing.indexOf(SKILL_BEGIN);
378
+ const e = existing.indexOf(SKILL_END);
379
+ if (b === -1 || e === -1) return null;
380
+ const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
381
+ return out.replace(/(\r?\n){2,}$/, "$1");
382
+ }
383
+ function atomicWrite(target, content) {
384
+ const tmp = `${target}.${randomUUID()}.tmp`;
385
+ let temporaryCreated = false;
386
+ try {
387
+ const fd = openSync2(tmp, "wx");
388
+ temporaryCreated = true;
389
+ try {
390
+ writeFileSync(fd, content);
391
+ fsyncSync(fd);
392
+ } finally {
393
+ closeSync2(fd);
394
+ }
395
+ renameSync(tmp, target);
396
+ } catch (error) {
397
+ if (temporaryCreated) {
398
+ try {
399
+ rmSync2(tmp, { force: true });
400
+ } catch {
401
+ }
402
+ }
403
+ throw error;
404
+ }
405
+ }
406
+ function resolveTarget(cwd, opts) {
407
+ if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
408
+ console.error(`Unknown --scope "${opts.scope}" (expected user or project)`);
409
+ return null;
410
+ }
411
+ if (opts.target) return resolve2(cwd, opts.target);
412
+ if (opts.scope === "user") {
413
+ if (!opts.agent) {
414
+ console.error("--scope user requires --agent (claude, codex, or hermes)");
415
+ return null;
416
+ }
417
+ const userTarget = userTargetFor(opts.agent);
418
+ if (userTarget) return userTarget;
419
+ console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
420
+ return null;
421
+ }
422
+ if (opts.agent) {
423
+ const mapped = AGENT_TARGET[opts.agent];
424
+ if (typeof mapped === "string") return resolve2(cwd, mapped);
425
+ console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
426
+ return null;
427
+ }
428
+ const matches = detectTargets(cwd);
429
+ if (matches.length === 0) return resolve2(cwd, DEFAULT_TARGET);
430
+ if (matches.length === 1) return resolve2(cwd, matches[0]);
431
+ console.error("Multiple rules files detected. Use --target to disambiguate:");
432
+ for (const m of matches) console.error(` ${m}`);
433
+ return null;
434
+ }
435
+ function runInstall(cwd, opts) {
436
+ if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
437
+ const target = resolveTarget(cwd, opts);
438
+ if (target === null) return 1;
439
+ const existing = existsSync2(target) ? readFileSync2(target, "utf-8") : "";
440
+ const eol = existing ? detectNewline(existing) : "\n";
441
+ const block = composeBlock(loadSkill(), eol);
442
+ const next = applyBlock(existing, block, eol);
443
+ if (next === existing) {
444
+ console.log("No changes \u2014 skill block already up to date.");
445
+ return 0;
446
+ }
447
+ if (opts.dryRun) {
448
+ process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
449
+ return 0;
450
+ }
451
+ atomicWrite(target, next);
452
+ console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
453
+ return 0;
454
+ }
455
+ function runUninstall(cwd, opts) {
456
+ if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
457
+ const target = resolveTarget(cwd, opts);
458
+ if (target === null) return 1;
459
+ if (!existsSync2(target)) {
460
+ console.log(`Skill block not present at ${target}`);
461
+ return 0;
462
+ }
463
+ const existing = readFileSync2(target, "utf-8");
464
+ const next = removeBlock(existing);
465
+ if (next === null) {
466
+ console.log(`Skill block not present at ${target}`);
467
+ return 0;
468
+ }
469
+ atomicWrite(target, next);
470
+ console.log(`Removed skill block from ${target}`);
471
+ return 0;
472
+ }
473
+ function runStatus(cwd, opts) {
474
+ if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
475
+ const target = resolveTarget(cwd, opts);
476
+ if (target === null) return 1;
477
+ const fileExists = existsSync2(target);
478
+ let installed = false;
479
+ if (fileExists) {
480
+ const content = readFileSync2(target, "utf-8");
481
+ installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
482
+ }
483
+ if (opts.json) {
484
+ printJson({ installed, target });
485
+ return installed ? 0 : 1;
486
+ }
487
+ if (!fileExists) {
488
+ console.log(`No rules file at ${target}`);
489
+ return 1;
490
+ }
491
+ if (installed) {
492
+ console.log(`PRIM SKILL v1 installed at ${target}`);
493
+ return 0;
494
+ }
495
+ console.log(`No PRIM SKILL block at ${target}`);
496
+ return 1;
497
+ }
498
+ function registerSkillCommands(program) {
499
+ const skill = program.command("skill").description("Manage the prim skill in your project rules file");
500
+ 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("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
501
+ try {
502
+ process.exit(runInstall(process.cwd(), opts));
503
+ } catch (err) {
504
+ console.error(err instanceof Error ? err.message : String(err));
505
+ process.exit(2);
506
+ }
507
+ });
508
+ 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)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").action((opts) => {
509
+ process.exit(runUninstall(process.cwd(), opts));
510
+ });
511
+ 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("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--json", "Output as JSON").action((opts) => {
512
+ process.exit(runStatus(process.cwd(), opts));
513
+ });
514
+ }
515
+
516
+ export {
517
+ printJson,
518
+ refreshClaudePlugins,
519
+ registerSkillCommands
520
+ };
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  buildHookOutput,
15
15
  handoffHookOutput
16
- } from "../chunk-S5NUHFAD.js";
16
+ } from "../chunk-4GLVJLEQ.js";
17
17
  import {
18
18
  FEEDBACK_DEADLINE_MS,
19
19
  acknowledgeDecisionFeedback,
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ refreshClaudePlugins
4
+ } from "../chunk-UJEQX52M.js";
2
5
  import {
3
6
  buildHookOutput,
4
7
  handoffHookOutput
5
- } from "../chunk-S5NUHFAD.js";
8
+ } from "../chunk-4GLVJLEQ.js";
6
9
  import {
7
10
  FEEDBACK_DEADLINE_MS,
8
11
  acknowledgeDecisionFeedback,
@@ -17,6 +20,7 @@ import {
17
20
  warmBinCache
18
21
  } from "../chunk-HSZPTVKU.js";
19
22
  import {
23
+ gitToplevel,
20
24
  isRepoActiveForCapture
21
25
  } from "../chunk-R5ZJRSLV.js";
22
26
  import {
@@ -53,69 +57,31 @@ function kickDaemonEnsure(options = {}) {
53
57
 
54
58
  // src/hooks/reauth-notice.ts
55
59
  var REAUTH_NOTICE = "[prim] authentication ended \u2014 run `prim auth login` to resume decision capture";
56
- function reauthNoticeOutput(agent) {
57
- if (agent === "codex") {
58
- return buildHookOutput({ additionalContext: REAUTH_NOTICE });
59
- }
60
- if (agent === "claude_code") {
61
- return buildHookOutput({ systemMessage: REAUTH_NOTICE });
62
- }
60
+ function reauthNoticeFields(agent) {
61
+ if (agent === "codex") return { additionalContext: REAUTH_NOTICE };
62
+ if (agent === "claude_code") return { systemMessage: REAUTH_NOTICE };
63
63
  return;
64
64
  }
65
65
 
66
- // src/hooks/session-start.ts
67
- var STDIN_TIMEOUT_MS = 1e3;
66
+ // src/hooks/session-start-core.ts
68
67
  var DAEMON_TIMEOUT_MS = 250;
69
- var outputAttempted = false;
70
- function emitOutput(output, acknowledge) {
71
- outputAttempted = true;
72
- return handoffHookOutput(output, acknowledge);
73
- }
74
- function readStdin() {
75
- return new Promise((resolve, reject) => {
76
- const chunks = [];
77
- const timer = setTimeout(() => {
78
- reject(new Error("stdin read timeout"));
79
- }, STDIN_TIMEOUT_MS);
80
- process.stdin.on("data", (chunk) => chunks.push(chunk));
81
- process.stdin.on("end", () => {
82
- clearTimeout(timer);
83
- resolve(Buffer.concat(chunks).toString("utf-8"));
84
- });
85
- process.stdin.on("error", (err) => {
86
- clearTimeout(timer);
87
- reject(err);
88
- });
89
- });
90
- }
91
- async function main() {
92
- const feedbackSignal = AbortSignal.timeout(FEEDBACK_DEADLINE_MS);
93
- warmBinCache();
94
- const agent = parseAgent(process.argv);
95
- let raw;
96
- try {
97
- raw = await readStdin();
98
- } catch {
99
- await emitOutput(buildHookOutput({}));
100
- return;
101
- }
68
+ var PRIM_SKILL_REMINDER = "Primitive is active in this repository. When this task chooses between plausible approaches or establishes or changes a lasting goal, priority, constraint, invariant, default, commitment, tradeoff, exception, or shared instruction, invoke the `prim` skill before finishing. Never invoke `prim` for routine implementation that merely follows an existing decision made before this task or for temporary tactics; they never qualify, including for evaluation. When a direct request replaces one lasting default with another but supplies no rationale, complete the work, invoke the skill, and ask one concise rationale question at the task boundary, even if the user requested only implementation or recording fails.";
69
+ async function processSessionStart(raw, agent, feedbackSignal = AbortSignal.timeout(FEEDBACK_DEADLINE_MS)) {
102
70
  let envelope;
103
71
  try {
104
- envelope = normalizeEnvelope(
105
- JSON.parse(raw),
106
- agent
107
- );
72
+ const parsed = JSON.parse(raw);
73
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
74
+ return { output: buildHookOutput({}) };
75
+ }
76
+ envelope = normalizeEnvelope(parsed, agent);
108
77
  } catch {
109
- await emitOutput(buildHookOutput({}));
110
- return;
78
+ return { output: buildHookOutput({}) };
111
79
  }
112
80
  if (envelope.hook_event_name !== "SessionStart") {
113
- await emitOutput(buildHookOutput({}));
114
- return;
81
+ return { output: buildHookOutput({}) };
115
82
  }
116
83
  if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
117
- await emitOutput(buildHookOutput({}));
118
- return;
84
+ return { output: buildHookOutput({}) };
119
85
  }
120
86
  kickDaemonEnsure();
121
87
  await daemonRequest(
@@ -123,11 +89,26 @@ async function main() {
123
89
  { sessionId: envelope.session_id },
124
90
  { timeoutMs: DAEMON_TIMEOUT_MS }
125
91
  );
92
+ const cwd = envelope.cwd ?? process.cwd();
93
+ let active = false;
94
+ let skillState = { installed: 0, refreshed: 0 };
95
+ if (agent === "claude_code") {
96
+ try {
97
+ const projectRoot = gitToplevel(cwd);
98
+ active = projectRoot !== null && isRepoActiveForCapture(cwd);
99
+ skillState = await refreshClaudePlugins(
100
+ cwd,
101
+ active && projectRoot !== null ? { includeProject: true, projectRoot } : { includeProject: false }
102
+ );
103
+ } catch {
104
+ }
105
+ }
126
106
  if (isSessionEnded()) {
127
- const notice = reauthNoticeOutput(agent);
107
+ const notice = reauthNoticeFields(agent);
128
108
  if (notice) {
129
- await emitOutput(notice);
130
- return;
109
+ return {
110
+ output: buildHookOutput({ ...notice, reloadSkills: skillState.refreshed > 0 })
111
+ };
131
112
  }
132
113
  }
133
114
  if (agent === "codex") {
@@ -139,15 +120,17 @@ async function main() {
139
120
  { timeoutMs: DAEMON_TIMEOUT_MS }
140
121
  );
141
122
  if (snapshot && !snapshot.presenceStale && typeof snapshot.onlineCount === "number") {
142
- await emitOutput(
143
- buildHookOutput({ additionalContext: `[prim] team: ${snapshot.onlineCount} online` })
144
- );
145
- return;
123
+ return {
124
+ output: buildHookOutput({
125
+ additionalContext: `[prim] team: ${snapshot.onlineCount} online`
126
+ })
127
+ };
146
128
  }
147
129
  }
148
130
  if (agent === "claude_code") {
149
- const cwd = envelope.cwd ?? process.cwd();
150
- if (isRepoActiveForCapture(cwd)) {
131
+ const additionalContext = active && skillState.installed > 0 ? PRIM_SKILL_REMINDER : void 0;
132
+ const reloadSkills = skillState.refreshed > 0;
133
+ if (active) {
151
134
  const identity = getOrCreateWorkspaceId(cwd);
152
135
  if (identity.status === "ready") {
153
136
  const lease = await leaseDecisionFeedback({
@@ -156,21 +139,64 @@ async function main() {
156
139
  signal: feedbackSignal
157
140
  });
158
141
  const rendered = lease ? renderFeedback(lease) : void 0;
159
- await emitOutput(
160
- buildHookOutput({ systemMessage: rendered?.systemMessage }),
161
- rendered ? async () => {
142
+ return {
143
+ output: buildHookOutput({
144
+ systemMessage: rendered?.systemMessage,
145
+ additionalContext,
146
+ reloadSkills
147
+ }),
148
+ acknowledge: rendered ? async () => {
162
149
  await acknowledgeDecisionFeedback({
163
150
  workspaceId: identity.workspaceId,
164
151
  deliveries: rendered.deliveries,
165
152
  signal: feedbackSignal
166
153
  });
167
154
  } : void 0
168
- );
169
- return;
155
+ };
170
156
  }
171
157
  }
158
+ return { output: buildHookOutput({ additionalContext, reloadSkills }) };
159
+ }
160
+ return { output: buildHookOutput({}) };
161
+ }
162
+
163
+ // src/hooks/session-start.ts
164
+ var STDIN_TIMEOUT_MS = 1e3;
165
+ var outputAttempted = false;
166
+ function emitOutput(output, acknowledge) {
167
+ outputAttempted = true;
168
+ return handoffHookOutput(output, acknowledge);
169
+ }
170
+ function readStdin() {
171
+ return new Promise((resolve, reject) => {
172
+ const chunks = [];
173
+ const timer = setTimeout(() => {
174
+ reject(new Error("stdin read timeout"));
175
+ }, STDIN_TIMEOUT_MS);
176
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
177
+ process.stdin.on("end", () => {
178
+ clearTimeout(timer);
179
+ resolve(Buffer.concat(chunks).toString("utf-8"));
180
+ });
181
+ process.stdin.on("error", (err) => {
182
+ clearTimeout(timer);
183
+ reject(err);
184
+ });
185
+ });
186
+ }
187
+ async function main() {
188
+ const feedbackSignal = AbortSignal.timeout(FEEDBACK_DEADLINE_MS);
189
+ warmBinCache();
190
+ const agent = parseAgent(process.argv);
191
+ let raw;
192
+ try {
193
+ raw = await readStdin();
194
+ } catch {
195
+ await emitOutput(buildHookOutput({}));
196
+ return;
172
197
  }
173
- await emitOutput(buildHookOutput({}));
198
+ const result = await processSessionStart(raw, agent, feedbackSignal);
199
+ await emitOutput(result.output, result.acknowledge);
174
200
  }
175
201
  void main().catch(async () => {
176
202
  if (!outputAttempted) await emitOutput(buildHookOutput({}));
package/dist/index.js CHANGED
@@ -22,6 +22,10 @@ import {
22
22
  pendingJournalStats,
23
23
  readMovesFromPath
24
24
  } from "./chunk-BOTKPLTI.js";
25
+ import {
26
+ printJson,
27
+ registerSkillCommands
28
+ } from "./chunk-UJEQX52M.js";
25
29
  import {
26
30
  fetchFeedbackCapability
27
31
  } from "./chunk-WELBNODJ.js";
@@ -63,17 +67,12 @@ import {
63
67
  } from "./chunk-UTKQTZHL.js";
64
68
 
65
69
  // src/index.ts
66
- import { readFileSync as readFileSync12 } from "fs";
67
- import { dirname as dirname9, resolve as resolve6 } from "path";
68
- import { fileURLToPath as fileURLToPath4 } from "url";
70
+ import { readFileSync as readFileSync10 } from "fs";
71
+ import { dirname as dirname7, resolve as resolve4 } from "path";
72
+ import { fileURLToPath as fileURLToPath2 } from "url";
69
73
  import { Command } from "commander";
70
74
  import updateNotifier from "update-notifier";
71
75
 
72
- // src/output.ts
73
- function printJson(data) {
74
- console.log(JSON.stringify(data, null, 2));
75
- }
76
-
77
76
  // src/commands/activation.ts
78
77
  function applyActivation(active) {
79
78
  const root = gitToplevel();
@@ -468,8 +467,8 @@ function registerAuthCommands(program2) {
468
467
  const state = base64url(randomBytes(16));
469
468
  const redirectUri = `http://${LOCALHOST}:${CALLBACK_PORT}/callback`;
470
469
  let settle;
471
- const outcome = new Promise((resolve7) => {
472
- settle = resolve7;
470
+ const outcome = new Promise((resolve5) => {
471
+ settle = resolve5;
473
472
  });
474
473
  const server = createServer((req, res) => {
475
474
  const url = new URL(req.url ?? "/", `http://${LOCALHOST}`);
@@ -483,12 +482,12 @@ function registerAuthCommands(program2) {
483
482
  res.end(page.html, () => settle(page.result));
484
483
  });
485
484
  try {
486
- await new Promise((resolve7, reject) => {
485
+ await new Promise((resolve5, reject) => {
487
486
  const onError = (err) => reject(err);
488
487
  server.once("error", onError);
489
488
  server.listen(CALLBACK_PORT, LOCALHOST, () => {
490
489
  server.removeListener("error", onError);
491
- resolve7();
490
+ resolve5();
492
491
  });
493
492
  });
494
493
  } catch (err) {
@@ -1767,7 +1766,7 @@ function clearStaleArtifacts() {
1767
1766
  }
1768
1767
  }
1769
1768
  function sleep(ms) {
1770
- return new Promise((resolve7) => setTimeout(resolve7, ms));
1769
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
1771
1770
  }
1772
1771
  function spawnDaemon(options) {
1773
1772
  const file = binFile(DAEMON_BIN);
@@ -4871,319 +4870,25 @@ function registerSetupCommand(program2, dependencies = {}) {
4871
4870
  });
4872
4871
  }
4873
4872
 
4874
- // src/commands/skill.ts
4875
- import {
4876
- closeSync as closeSync4,
4877
- existsSync as existsSync12,
4878
- fsyncSync as fsyncSync3,
4879
- openSync as openSync4,
4880
- readFileSync as readFileSync10,
4881
- renameSync as renameSync5,
4882
- writeFileSync as writeFileSync7
4883
- } from "fs";
4884
- import { homedir as homedir8 } from "os";
4885
- import { dirname as dirname7, join as join12, resolve as resolve4 } from "path";
4886
- import { fileURLToPath as fileURLToPath2 } from "url";
4887
- import { createPatch } from "diff";
4888
-
4889
- // src/commands/claude-plugin.ts
4890
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync3, rmdirSync } from "fs";
4891
- import { homedir as homedir7 } from "os";
4892
- import { dirname as dirname6, join as join11, resolve as resolve3 } from "path";
4893
- import { fileURLToPath } from "url";
4894
- var __dirname = dirname6(fileURLToPath(import.meta.url));
4895
- var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
4896
- function resolvePluginDir(cwd, scope) {
4897
- if (scope && scope !== "user" && scope !== "project") {
4898
- console.error(`Unknown --scope "${scope}" (expected user or project)`);
4899
- return null;
4900
- }
4901
- const base = scope === "user" ? join11(homedir7(), ".claude") : join11(gitToplevel(cwd) ?? cwd, ".claude");
4902
- return join11(base, "skills", "prim");
4903
- }
4904
- function packageVersion() {
4905
- let dir = __dirname;
4906
- while (dir !== dirname6(dir)) {
4907
- const p = resolve3(dir, "package.json");
4908
- if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
4909
- dir = dirname6(dir);
4910
- }
4911
- return "0.0.0";
4912
- }
4913
- function pluginManifest() {
4914
- const manifest = { name: "prim", description: PLUGIN_DESCRIPTION, version: packageVersion() };
4915
- return `${JSON.stringify(manifest, null, 2)}
4916
- `;
4917
- }
4918
- function removeDirIfEmpty(dir) {
4919
- if (existsSync11(dir) && readdirSync2(dir).length === 0) rmdirSync(dir);
4920
- }
4921
- function pluginPaths(dir) {
4922
- return {
4923
- manifestPath: join11(dir, ".claude-plugin", "plugin.json"),
4924
- skillPath: join11(dir, "SKILL.md")
4925
- };
4926
- }
4927
- function installClaudePlugin(cwd, opts) {
4928
- const dir = resolvePluginDir(cwd, opts.scope);
4929
- if (dir === null) return 1;
4930
- const { manifestPath, skillPath } = pluginPaths(dir);
4931
- const manifest = pluginManifest();
4932
- const skill = loadSkill();
4933
- const manifestCurrent = existsSync11(manifestPath) ? readFileSync9(manifestPath, "utf-8") : null;
4934
- const skillCurrent = existsSync11(skillPath) ? readFileSync9(skillPath, "utf-8") : null;
4935
- if (manifestCurrent === manifest && skillCurrent === skill) {
4936
- console.log("No changes \u2014 prim skill plugin already up to date.");
4937
- return 0;
4938
- }
4939
- if (opts.dryRun) {
4940
- console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
4941
- return 0;
4942
- }
4943
- mkdirSync8(join11(dir, ".claude-plugin"), { recursive: true });
4944
- atomicWrite3(manifestPath, manifest);
4945
- atomicWrite3(skillPath, skill);
4946
- console.log(`Installed prim skill plugin at ${dir}`);
4947
- console.log("Restart Claude Code or run /reload-plugins to load it.");
4948
- if (opts.scope !== "user") {
4949
- console.log("Project-scope plugins load only when Claude Code launches from this directory.");
4950
- }
4951
- return 0;
4952
- }
4953
- function uninstallClaudePlugin(cwd, opts) {
4954
- const dir = resolvePluginDir(cwd, opts.scope);
4955
- if (dir === null) return 1;
4956
- const { manifestPath, skillPath } = pluginPaths(dir);
4957
- if (!existsSync11(manifestPath) && !existsSync11(skillPath)) {
4958
- console.log(`prim skill plugin not present at ${dir}`);
4959
- return 0;
4960
- }
4961
- rmSync3(manifestPath, { force: true });
4962
- rmSync3(skillPath, { force: true });
4963
- removeDirIfEmpty(join11(dir, ".claude-plugin"));
4964
- removeDirIfEmpty(dir);
4965
- console.log(`Removed prim skill plugin from ${dir}`);
4966
- return 0;
4967
- }
4968
- function statusClaudePlugin(cwd, opts) {
4969
- const dir = resolvePluginDir(cwd, opts.scope);
4970
- if (dir === null) return 1;
4971
- const { skillPath } = pluginPaths(dir);
4972
- const installed = existsSync11(skillPath);
4973
- if (opts.json) {
4974
- printJson({ installed, target: dir });
4975
- return installed ? 0 : 1;
4976
- }
4977
- if (installed) {
4978
- console.log(`prim skill plugin installed at ${dir}`);
4979
- return 0;
4980
- }
4981
- console.log(`No prim skill plugin at ${dir}`);
4982
- return 1;
4983
- }
4984
-
4985
- // src/commands/skill.ts
4986
- var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
4987
- var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
4988
- var SKILL_END = "<!-- END PRIM SKILL v1 -->";
4989
- var TARGET_CANDIDATES = [
4990
- "CLAUDE.md",
4991
- "AGENTS.md",
4992
- ".hermes.md",
4993
- ".cursor/rules",
4994
- ".windsurfrules",
4995
- ".github/instructions/primitive.md"
4996
- ];
4997
- var DEFAULT_TARGET = "CLAUDE.md";
4998
- var AGENT_TARGET = {
4999
- claude: "CLAUDE.md",
5000
- codex: "AGENTS.md",
5001
- hermes: ".hermes.md"
5002
- };
5003
- function userTargetFor(agent) {
5004
- if (agent === "claude") return join12(homedir8(), ".claude", "CLAUDE.md");
5005
- if (agent === "codex") return join12(homedir8(), ".codex", "AGENTS.md");
5006
- if (agent === "hermes") {
5007
- return join12(process.env.HERMES_HOME ?? join12(homedir8(), ".hermes"), ".hermes.md");
5008
- }
5009
- return null;
5010
- }
5011
- function loadSkill() {
5012
- let dir = __dirname2;
5013
- while (dir !== dirname7(dir)) {
5014
- const p = resolve4(dir, "SKILL.md");
5015
- if (existsSync12(p)) return readFileSync10(p, "utf-8");
5016
- dir = dirname7(dir);
5017
- }
5018
- throw new Error("SKILL.md not found in package");
5019
- }
5020
- function detectTargets(cwd) {
5021
- return TARGET_CANDIDATES.filter((p) => existsSync12(resolve4(cwd, p)));
5022
- }
5023
- function detectNewline(content) {
5024
- return content.includes("\r\n") ? "\r\n" : "\n";
5025
- }
5026
- function composeBlock(skill, eol) {
5027
- const body = skill.replace(/\r?\n/g, eol);
5028
- return `${SKILL_BEGIN}${eol}${body}${eol}${SKILL_END}`;
5029
- }
5030
- function applyBlock(existing, block, eol) {
5031
- const b = existing.indexOf(SKILL_BEGIN);
5032
- const e = existing.indexOf(SKILL_END);
5033
- if (b !== -1 && e !== -1) {
5034
- return existing.slice(0, b) + block + existing.slice(e + SKILL_END.length);
5035
- }
5036
- if (existing.length === 0) return `${block}${eol}`;
5037
- const sep = existing.endsWith(eol) ? "" : eol;
5038
- return `${existing}${sep}${block}${eol}`;
5039
- }
5040
- function removeBlock(existing) {
5041
- const b = existing.indexOf(SKILL_BEGIN);
5042
- const e = existing.indexOf(SKILL_END);
5043
- if (b === -1 || e === -1) return null;
5044
- const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
5045
- return out.replace(/(\r?\n){2,}$/, "$1");
5046
- }
5047
- function atomicWrite3(target, content) {
5048
- const tmp = `${target}.tmp`;
5049
- writeFileSync7(tmp, content);
5050
- const fd = openSync4(tmp, "r+");
5051
- try {
5052
- fsyncSync3(fd);
5053
- } finally {
5054
- closeSync4(fd);
5055
- }
5056
- renameSync5(tmp, target);
5057
- }
5058
- function resolveTarget(cwd, opts) {
5059
- if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
5060
- console.error(`Unknown --scope "${opts.scope}" (expected user or project)`);
5061
- return null;
5062
- }
5063
- if (opts.target) return resolve4(cwd, opts.target);
5064
- if (opts.scope === "user") {
5065
- if (!opts.agent) {
5066
- console.error("--scope user requires --agent (claude, codex, or hermes)");
5067
- return null;
5068
- }
5069
- const userTarget = userTargetFor(opts.agent);
5070
- if (userTarget) return userTarget;
5071
- console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
5072
- return null;
5073
- }
5074
- if (opts.agent) {
5075
- const mapped = AGENT_TARGET[opts.agent];
5076
- if (typeof mapped === "string") return resolve4(cwd, mapped);
5077
- console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
5078
- return null;
5079
- }
5080
- const matches = detectTargets(cwd);
5081
- if (matches.length === 0) return resolve4(cwd, DEFAULT_TARGET);
5082
- if (matches.length === 1) return resolve4(cwd, matches[0]);
5083
- console.error("Multiple rules files detected. Use --target to disambiguate:");
5084
- for (const m of matches) console.error(` ${m}`);
5085
- return null;
5086
- }
5087
- function runInstall(cwd, opts) {
5088
- if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
5089
- const target = resolveTarget(cwd, opts);
5090
- if (target === null) return 1;
5091
- const existing = existsSync12(target) ? readFileSync10(target, "utf-8") : "";
5092
- const eol = existing ? detectNewline(existing) : "\n";
5093
- const block = composeBlock(loadSkill(), eol);
5094
- const next = applyBlock(existing, block, eol);
5095
- if (next === existing) {
5096
- console.log("No changes \u2014 skill block already up to date.");
5097
- return 0;
5098
- }
5099
- if (opts.dryRun) {
5100
- process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
5101
- return 0;
5102
- }
5103
- atomicWrite3(target, next);
5104
- console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
5105
- return 0;
5106
- }
5107
- function runUninstall(cwd, opts) {
5108
- if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
5109
- const target = resolveTarget(cwd, opts);
5110
- if (target === null) return 1;
5111
- if (!existsSync12(target)) {
5112
- console.log(`Skill block not present at ${target}`);
5113
- return 0;
5114
- }
5115
- const existing = readFileSync10(target, "utf-8");
5116
- const next = removeBlock(existing);
5117
- if (next === null) {
5118
- console.log(`Skill block not present at ${target}`);
5119
- return 0;
5120
- }
5121
- atomicWrite3(target, next);
5122
- console.log(`Removed skill block from ${target}`);
5123
- return 0;
5124
- }
5125
- function runStatus(cwd, opts) {
5126
- if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
5127
- const target = resolveTarget(cwd, opts);
5128
- if (target === null) return 1;
5129
- const fileExists = existsSync12(target);
5130
- let installed = false;
5131
- if (fileExists) {
5132
- const content = readFileSync10(target, "utf-8");
5133
- installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
5134
- }
5135
- if (opts.json) {
5136
- printJson({ installed, target });
5137
- return installed ? 0 : 1;
5138
- }
5139
- if (!fileExists) {
5140
- console.log(`No rules file at ${target}`);
5141
- return 1;
5142
- }
5143
- if (installed) {
5144
- console.log(`PRIM SKILL v1 installed at ${target}`);
5145
- return 0;
5146
- }
5147
- console.log(`No PRIM SKILL block at ${target}`);
5148
- return 1;
5149
- }
5150
- function registerSkillCommands(program2) {
5151
- const skill = program2.command("skill").description("Manage the prim skill in your project rules file");
5152
- 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("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
5153
- try {
5154
- process.exit(runInstall(process.cwd(), opts));
5155
- } catch (err) {
5156
- console.error(err instanceof Error ? err.message : String(err));
5157
- process.exit(2);
5158
- }
5159
- });
5160
- 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)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").action((opts) => {
5161
- process.exit(runUninstall(process.cwd(), opts));
5162
- });
5163
- 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("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--json", "Output as JSON").action((opts) => {
5164
- process.exit(runStatus(process.cwd(), opts));
5165
- });
5166
- }
5167
-
5168
4873
  // src/commands/statusline.ts
5169
- import { readFileSync as readFileSync11 } from "fs";
5170
- import { dirname as dirname8, resolve as resolve5 } from "path";
5171
- import { fileURLToPath as fileURLToPath3 } from "url";
4874
+ import { readFileSync as readFileSync9 } from "fs";
4875
+ import { dirname as dirname6, resolve as resolve3 } from "path";
4876
+ import { fileURLToPath } from "url";
5172
4877
  var STATUSLINE_TIMEOUT_MS = 200;
5173
4878
  var STATUSLINE_NAME_CAP = 3;
5174
4879
  function readPackageVersion() {
5175
4880
  try {
5176
- const here = dirname8(fileURLToPath3(import.meta.url));
4881
+ const here = dirname6(fileURLToPath(import.meta.url));
5177
4882
  const candidates = [
5178
4883
  // The supervised runtime stages a tiny manifest beside the standalone
5179
4884
  // statusline bundle, so it remains versioned without loading the package.
5180
- resolve5(here, "manifest.json"),
5181
- resolve5(here, "../../package.json"),
5182
- resolve5(here, "../package.json")
4885
+ resolve3(here, "manifest.json"),
4886
+ resolve3(here, "../../package.json"),
4887
+ resolve3(here, "../package.json")
5183
4888
  ];
5184
4889
  for (const path of candidates) {
5185
4890
  try {
5186
- const pkg2 = JSON.parse(readFileSync11(path, "utf-8"));
4891
+ const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
5187
4892
  if (pkg2.version) {
5188
4893
  return pkg2.version;
5189
4894
  }
@@ -5370,8 +5075,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
5370
5075
  }
5371
5076
 
5372
5077
  // src/index.ts
5373
- var __dirname3 = dirname9(fileURLToPath4(import.meta.url));
5374
- var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
5078
+ var __dirname = dirname7(fileURLToPath2(import.meta.url));
5079
+ var pkg = JSON.parse(readFileSync10(resolve4(__dirname, "../package.json"), "utf-8"));
5375
5080
  updateNotifier({ pkg }).notify();
5376
5081
  var program = new Command();
5377
5082
  program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.50",
3
+ "version": "0.1.0-alpha.51",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",