@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 +1 -1
- package/dist/{chunk-S5NUHFAD.js → chunk-4GLVJLEQ.js} +5 -5
- package/dist/chunk-UJEQX52M.js +520 -0
- package/dist/hooks/prim-hook.js +1 -1
- package/dist/hooks/session-start.js +94 -68
- package/dist/index.js +22 -317
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prim
|
|
3
|
-
description: Use the prim CLI for Primitive
|
|
3
|
+
description: Use the prim CLI for Primitive’s 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
};
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -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-
|
|
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
|
|
57
|
-
if (agent === "codex") {
|
|
58
|
-
|
|
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
|
|
70
|
-
function
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
return;
|
|
78
|
+
return { output: buildHookOutput({}) };
|
|
111
79
|
}
|
|
112
80
|
if (envelope.hook_event_name !== "SessionStart") {
|
|
113
|
-
|
|
114
|
-
return;
|
|
81
|
+
return { output: buildHookOutput({}) };
|
|
115
82
|
}
|
|
116
83
|
if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
|
|
117
|
-
|
|
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 =
|
|
107
|
+
const notice = reauthNoticeFields(agent);
|
|
128
108
|
if (notice) {
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
143
|
-
buildHookOutput({
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
150
|
-
|
|
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
|
-
|
|
160
|
-
buildHookOutput({
|
|
161
|
-
|
|
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
|
|
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
|
|
67
|
-
import { dirname as
|
|
68
|
-
import { fileURLToPath as
|
|
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((
|
|
472
|
-
settle =
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
|
5170
|
-
import { dirname as
|
|
5171
|
-
import { fileURLToPath
|
|
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 =
|
|
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
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
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(
|
|
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
|
|
5374
|
-
var pkg = JSON.parse(
|
|
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