@primitive.ai/prim 0.1.0-alpha.49 → 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
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-TPROV45L.js";
3
+ } from "./chunk-DWDEQRWN.js";
4
4
 
5
5
  // src/journal.ts
6
6
  import {
@@ -154,6 +154,7 @@ var CONFIG_DIR_MODE = 448;
154
154
  var CREDENTIAL_FILE_MODE = 384;
155
155
  var REFRESH_THRESHOLD_MS = 6e4;
156
156
  var DEFAULT_API_URL = "https://api.getprimitive.ai";
157
+ var AUTH_EXPIRED_MESSAGE = "Authentication expired. Run `prim auth login` to re-authenticate.";
157
158
  function loadEnvFile() {
158
159
  const envVars = {};
159
160
  for (const file of [".env.local", ".env"]) {
@@ -349,6 +350,7 @@ async function performTokenRefresh(options = {}) {
349
350
  if (selected?.source !== "token_file") return void 0;
350
351
  const startingGeneration = readTrimmed(REFRESH_TOKEN_PATH);
351
352
  if (!startingGeneration || isSessionEnded()) return void 0;
353
+ const startingAccessToken = selected.token;
352
354
  return withCredentialLock(
353
355
  async () => {
354
356
  const currentCredential = resolveAuthCredential();
@@ -358,6 +360,9 @@ async function performTokenRefresh(options = {}) {
358
360
  if (currentGeneration !== startingGeneration) {
359
361
  return currentCredential.token;
360
362
  }
363
+ if (currentCredential.token !== startingAccessToken) {
364
+ return currentCredential.token;
365
+ }
361
366
  if (isSessionEnded()) return void 0;
362
367
  const response = await fetch(`${getSiteUrl()}/mcp/broker/refresh`, {
363
368
  method: "POST",
@@ -442,6 +447,12 @@ async function request(method, path, body, options) {
442
447
  });
443
448
  if (token) credential = { token, source: "token_file" };
444
449
  }
450
+ if (!credential) {
451
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
452
+ }
453
+ if (isTokenExpiringSoon(credential) && isSessionEnded()) {
454
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
455
+ }
445
456
  const doFetch = (token) => {
446
457
  const headers = { "Content-Type": "application/json" };
447
458
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -471,7 +482,7 @@ async function request(method, path, body, options) {
471
482
  }
472
483
  if (!response.ok) {
473
484
  if (response.status === 401) {
474
- throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
485
+ throw new HttpError(401, AUTH_EXPIRED_MESSAGE);
475
486
  }
476
487
  const errorBody = await response.json().catch(() => null);
477
488
  throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
@@ -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
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getClient,
3
3
  getSiteUrl
4
- } from "./chunk-TPROV45L.js";
4
+ } from "./chunk-DWDEQRWN.js";
5
5
  import {
6
6
  daemonRequest
7
7
  } from "./chunk-UTKQTZHL.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-TPROV45L.js";
3
+ } from "./chunk-DWDEQRWN.js";
4
4
 
5
5
  // src/decisions/feedback.ts
6
6
  var FEEDBACK_PROTOCOL_VERSION = 1;