@modootoday/pmcp 0.10.0 → 0.11.0

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.
@@ -1,1191 +0,0 @@
1
- import {
2
- loadEmbedder,
3
- openEntryVectors
4
- } from "./chunk-XEH4ZGGJ.js";
5
- import {
6
- frontmatterObject,
7
- readCatalog,
8
- specProblems
9
- } from "./chunk-VRZFM2M5.js";
10
- import {
11
- ConfigError
12
- } from "./chunk-MSTKY33C.js";
13
- import {
14
- DEFAULT_MODEL_ID,
15
- resolveEncoder
16
- } from "./chunk-XOH2CW6P.js";
17
-
18
- // src/search.ts
19
- import { existsSync } from "fs";
20
- async function prepareSearch(catalog, options) {
21
- const lexical = (reason) => ({
22
- ranking: "lexical",
23
- reason,
24
- options: {}
25
- });
26
- if (!existsSync(options.indexPath)) return lexical("index_missing");
27
- let store;
28
- try {
29
- store = await openEntryVectors(options.indexPath, { readOnly: true });
30
- } catch {
31
- return lexical("index_unavailable");
32
- }
33
- if (!store) return lexical("index_unavailable");
34
- try {
35
- const entries = (options.loadCatalog ?? readCatalog)(catalog);
36
- const selected = resolveEncoder(
37
- options.modelId ?? options.embedder?.modelId ?? DEFAULT_MODEL_ID,
38
- options
39
- );
40
- const modelId = selected.modelId;
41
- const key = selected.indexKey;
42
- if (options.embedder?.indexKey !== void 0 && options.embedder.indexKey !== key)
43
- return lexical("vector_mismatch");
44
- const descriptions = new Map(
45
- entries.map((entry2) => [entry2.name, entry2.description])
46
- );
47
- if (entries.length === 0 || store.stale(key, descriptions).length > 0)
48
- return lexical("index_stale");
49
- const vectors = store.read(key);
50
- const embedder = options.embedder ?? await loadEmbedder(selected.modelId, {
51
- ...options,
52
- localOnly: options.localOnly ?? true
53
- });
54
- if (!embedder || embedder.modelId !== modelId)
55
- return lexical("encoder_unavailable");
56
- if (embedder.indexKey !== void 0 && embedder.indexKey !== key)
57
- return lexical("vector_mismatch");
58
- const probe = await embedder.embed("pmcp");
59
- if (probe.length === 0 || !entries.every(
60
- (entry2) => vectors.get(entry2.name)?.length === probe.length
61
- ))
62
- return lexical("vector_mismatch");
63
- return {
64
- ranking: "semantic",
65
- options: { embedder, vectors, loadCatalog: () => entries }
66
- };
67
- } catch {
68
- return lexical("encoder_unavailable");
69
- } finally {
70
- store.close();
71
- }
72
- }
73
-
74
- // src/project/package-rules.ts
75
- import { execFileSync } from "child_process";
76
- import { existsSync as existsSync2, lstatSync, readdirSync, readlinkSync } from "fs";
77
- import { dirname, join, posix } from "path";
78
- var ProjectError = class extends Error {
79
- constructor(message) {
80
- super(message);
81
- this.name = "ProjectError";
82
- }
83
- };
84
- var RULES_DIR = ".claude/rules";
85
- function trackedFiles(root, recurse = false) {
86
- try {
87
- const out = execFileSync(
88
- "git",
89
- ["ls-files", "-z", ...recurse ? ["--recurse-submodules"] : []],
90
- { cwd: root, encoding: "utf8", maxBuffer: 1 << 28 }
91
- );
92
- return out.split("\0").filter(Boolean);
93
- } catch {
94
- return [];
95
- }
96
- }
97
- function deriveName(dir, strip) {
98
- const segments = dir.split("/");
99
- if (segments.length > 1 && strip.includes(segments[0] ?? "")) {
100
- segments.shift();
101
- }
102
- const tokens = [];
103
- for (const segment of segments) {
104
- if (segment === "packages") continue;
105
- for (const token of segment.split("-")) {
106
- if (token === tokens[tokens.length - 1]) continue;
107
- tokens.push(token);
108
- }
109
- }
110
- return tokens.join("-");
111
- }
112
- function linkTarget(abs) {
113
- try {
114
- if (!lstatSync(abs).isSymbolicLink()) return null;
115
- return readlinkSync(abs);
116
- } catch {
117
- return null;
118
- }
119
- }
120
- function legacyLinks(root) {
121
- const found = /* @__PURE__ */ new Map();
122
- const dir = join(root, RULES_DIR);
123
- if (!existsSync2(dir)) return found;
124
- for (const entry2 of readdirSync(dir).sort()) {
125
- if (!entry2.endsWith(".md")) continue;
126
- const target = linkTarget(join(dir, entry2));
127
- if (target === null || !target.startsWith("../../")) continue;
128
- found.set(entry2.slice(0, -3), target.slice("../../".length));
129
- }
130
- return found;
131
- }
132
- function packageRuleOutputs(root, spec, owned) {
133
- const suffix = `/${spec.file}`;
134
- const files = trackedFiles(root).filter((file) => file.endsWith(suffix)).sort();
135
- const prefix = `${RULES_DIR}/${spec.dir}/`;
136
- const byTarget = /* @__PURE__ */ new Map();
137
- for (const [path, target] of owned) {
138
- if (!path.startsWith(prefix)) continue;
139
- const rel = posix.normalize(posix.join(posix.dirname(path), target));
140
- byTarget.set(rel, path.slice(prefix.length, -3));
141
- }
142
- const legacy = legacyLinks(root);
143
- const legacyByDir = /* @__PURE__ */ new Map();
144
- for (const [name, target] of legacy) {
145
- if (!byTarget.has(target)) byTarget.set(target, name);
146
- const dir = dirname(target);
147
- if (!legacyByDir.has(dir)) legacyByDir.set(dir, name);
148
- }
149
- const outputs = [];
150
- const taken = /* @__PURE__ */ new Map();
151
- for (const file of files) {
152
- const pkg = file.slice(0, -suffix.length);
153
- const name = byTarget.get(file) ?? legacyByDir.get(dirname(file)) ?? deriveName(pkg, spec.strip);
154
- const other = taken.get(name);
155
- if (other !== void 0) {
156
- throw new ProjectError(
157
- `two packages want rule link ${prefix}${name}.md: ${other} and ${pkg}`
158
- );
159
- }
160
- taken.set(name, pkg);
161
- outputs.push({
162
- kind: "link",
163
- path: `${prefix}${name}.md`,
164
- target: `../../../${file}`
165
- });
166
- }
167
- return outputs;
168
- }
169
-
170
- // src/project/plan.ts
171
- import { existsSync as existsSync3, readFileSync, readdirSync as readdirSync2 } from "fs";
172
- import { join as join2 } from "path";
173
-
174
- // src/project/frontmatter.ts
175
- import { parse } from "yaml";
176
- function splitFrontmatter(source) {
177
- const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/u.exec(source);
178
- if (!match) return { meta: {}, body: source };
179
- const meta = parse(match[1] ?? "");
180
- const object = meta !== null && typeof meta === "object" && !Array.isArray(meta) ? meta : {};
181
- return { meta: object, body: match[2] ?? "" };
182
- }
183
- function scalar(value) {
184
- if (typeof value === "string") return value;
185
- return JSON.stringify(value);
186
- }
187
- function renderFrontmatter(meta) {
188
- const lines2 = [];
189
- for (const [key, value] of Object.entries(meta)) {
190
- if (value === void 0 || value === null) continue;
191
- if (typeof value === "string" && value.includes("\n")) {
192
- const block2 = value.replace(/\n$/u, "").split("\n").map((line) => ` ${line}`);
193
- lines2.push(`${key}: |`, ...block2);
194
- continue;
195
- }
196
- if (Array.isArray(value)) {
197
- lines2.push(`${key}: [${value.map((v) => JSON.stringify(v)).join(", ")}]`);
198
- continue;
199
- }
200
- lines2.push(`${key}: ${scalar(value)}`);
201
- }
202
- return `---
203
- ${lines2.join("\n")}
204
- ---
205
- `;
206
- }
207
-
208
- // src/project/outputs.ts
209
- var MARK = "AUTO-GENERATED by pmcp project";
210
- function jsonOutput(file, key, value) {
211
- return { kind: "json", path: `${file}#${key.join(".")}`, file, key, value };
212
- }
213
- function blockOutput(file, content) {
214
- return { kind: "block", path: `${file}#pmcp`, file, content };
215
- }
216
- function splitKeyed(path) {
217
- const at = path.indexOf("#");
218
- if (at === -1) return null;
219
- return { file: path.slice(0, at), key: path.slice(at + 1).split(".") };
220
- }
221
-
222
- // src/project/mcp.ts
223
- var runtimeByTool = {
224
- claude: "claude-code",
225
- codex: "codex-cli",
226
- gemini: "gemini-cli",
227
- grok: "grok-cli",
228
- antigravity: "antigravity"
229
- };
230
- function launchForTool(server, tool) {
231
- if (!server.mailbox) return launch(server);
232
- return launch({
233
- ...server,
234
- env: { ...server.env, PMCP_MAILBOX_RUNTIME: runtimeByTool[tool] }
235
- });
236
- }
237
- var ROOT_VAR = "${PROJECT_ROOT}";
238
- var quote = (text) => `'${text.replaceAll("'", "'\\''")}'`;
239
- function shellWord(text) {
240
- return text.split(ROOT_VAR).map((piece) => piece === "" ? "" : quote(piece)).join('"$d"');
241
- }
242
- function launch(spec) {
243
- const words = [spec.command, ...spec.args, ...Object.values(spec.env)];
244
- if (!words.some((word) => word.includes(ROOT_VAR))) {
245
- return { command: spec.command, args: spec.args, env: spec.env };
246
- }
247
- const exports = Object.entries(spec.env).map(
248
- ([key, value]) => `export ${key}=${shellWord(value)}; `
249
- );
250
- const script = 'd=$PWD; while [ ! -f "$d/pmcp.toml" ]; do [ "$d" = / ] && exit 1; d=$(dirname "$d"); done; ' + exports.join("") + `exec ${[spec.command, ...spec.args].map(shellWord).join(" ")}`;
251
- return { command: "sh", args: ["-c", script], env: {} };
252
- }
253
- function entry(server, extra = {}) {
254
- return {
255
- command: server.command,
256
- args: [...server.args],
257
- ...Object.keys(server.env).length > 0 ? { env: { ...server.env } } : {},
258
- ...extra
259
- };
260
- }
261
- var tomlValue = (value) => {
262
- if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
263
- if (value !== null && typeof value === "object") {
264
- const parts = Object.entries(value).map(
265
- ([k, v]) => `${JSON.stringify(k)} = ${tomlValue(v)}`
266
- );
267
- return `{ ${parts.join(", ")} }`;
268
- }
269
- return JSON.stringify(value);
270
- };
271
- function tomlServers(servers) {
272
- const lines2 = [`# ${MARK} from pmcp.toml [mcp]`];
273
- for (const [alias, server] of servers) {
274
- lines2.push(
275
- "",
276
- `[mcp_servers.${alias}]`,
277
- `command = ${tomlValue(server.command)}`,
278
- `args = ${tomlValue(server.args)}`
279
- );
280
- if (Object.keys(server.env).length > 0) {
281
- lines2.push(`env = ${tomlValue(server.env)}`);
282
- }
283
- }
284
- return `${lines2.join("\n")}
285
- `;
286
- }
287
- function mcpOutputs(spec) {
288
- const servers = Object.entries(spec.mcp);
289
- if (servers.length === 0) return [];
290
- const outputs = [];
291
- for (const [alias, server] of servers) {
292
- const tools = spec.mcp[alias]?.tools;
293
- if (spec.tools.has("claude")) {
294
- outputs.push(
295
- jsonOutput(
296
- ".mcp.json",
297
- ["mcpServers", alias],
298
- entry(launchForTool(server, "claude"))
299
- )
300
- );
301
- }
302
- if (spec.tools.has("gemini")) {
303
- const extra = tools ? { includeTools: [...tools] } : {};
304
- outputs.push(
305
- jsonOutput(
306
- ".gemini/settings.json",
307
- ["mcpServers", alias],
308
- entry(launchForTool(server, "gemini"), extra)
309
- )
310
- );
311
- }
312
- if (spec.tools.has("antigravity")) {
313
- outputs.push(
314
- jsonOutput(
315
- ".agents/mcp_config.json",
316
- ["mcpServers", alias],
317
- entry(launchForTool(server, "antigravity"))
318
- )
319
- );
320
- }
321
- }
322
- if (spec.tools.has("codex")) {
323
- outputs.push(
324
- blockOutput(
325
- ".codex/config.toml",
326
- tomlServers(
327
- servers.map(([alias, server]) => [
328
- alias,
329
- launchForTool(server, "codex")
330
- ])
331
- )
332
- )
333
- );
334
- }
335
- if (spec.tools.has("grok")) {
336
- outputs.push(
337
- blockOutput(
338
- ".grok/config.toml",
339
- tomlServers(
340
- servers.map(([alias, server]) => [
341
- alias,
342
- launchForTool(server, "grok")
343
- ])
344
- )
345
- )
346
- );
347
- }
348
- return outputs;
349
- }
350
-
351
- // src/project/plan.ts
352
- var AGENT_FIELDS = {
353
- claude: ["name", "description", "tools", "model", "effort"],
354
- gemini: ["name", "description"]
355
- };
356
- var SETTINGS = ".claude/settings.json";
357
- function listDirs(dir) {
358
- if (!existsSync3(dir)) return [];
359
- return readdirSync2(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
360
- }
361
- var tomlString = (value) => JSON.stringify(typeof value === "string" ? value : String(value ?? ""));
362
- function skillNames(root) {
363
- const dir = join2(root, ".agents/skills");
364
- return listDirs(dir).filter(
365
- (name) => existsSync3(join2(dir, name, "SKILL.md"))
366
- );
367
- }
368
- function agentNames(root) {
369
- const dir = join2(root, ".agents/agents");
370
- return listDirs(dir).filter(
371
- (name) => existsSync3(join2(dir, name, "agent.md"))
372
- );
373
- }
374
- function skillOutputs(spec) {
375
- const outputs = [];
376
- for (const name of skillNames(spec.root)) {
377
- const target = `../../.agents/skills/${name}`;
378
- if (spec.tools.has("claude")) {
379
- outputs.push({ kind: "link", path: `.claude/skills/${name}`, target });
380
- }
381
- if (spec.tools.has("grok")) {
382
- outputs.push({ kind: "link", path: `.grok/skills/${name}`, target });
383
- }
384
- if (spec.skills[name]?.invocation !== "user") continue;
385
- if (spec.tools.has("claude")) {
386
- outputs.push(
387
- jsonOutput(SETTINGS, ["skillOverrides", name], "user-invocable-only")
388
- );
389
- }
390
- if (spec.tools.has("codex")) {
391
- outputs.push({
392
- kind: "file",
393
- path: `.agents/skills/${name}/agents/openai.yaml`,
394
- content: `# ${MARK}
395
- policy:
396
- allow_implicit_invocation: false
397
- `
398
- });
399
- }
400
- }
401
- return outputs;
402
- }
403
- function ruleOutputs(spec) {
404
- if (!spec.tools.has("claude")) return [];
405
- const dir = join2(spec.root, ".agents/rules");
406
- if (!existsSync3(dir)) return [];
407
- const outputs = [];
408
- for (const file of readdirSync2(dir).filter((f) => f.endsWith(".md")).sort()) {
409
- const rel = `.agents/rules/${file}`;
410
- const { meta, body } = splitFrontmatter(
411
- readFileSync(join2(spec.root, rel), "utf8")
412
- );
413
- const globs = [meta["globs"] ?? []].flat().filter((g) => typeof g === "string" && g !== "");
414
- const head = globs.length > 0 ? renderFrontmatter({ paths: globs }) : "";
415
- outputs.push({
416
- kind: "file",
417
- path: `${RULES_DIR}/pmcp/${file}`,
418
- content: `${head}
419
- <!-- ${MARK} from ${rel} -->
420
- ${body}`
421
- });
422
- }
423
- return outputs;
424
- }
425
- function agentOutputs(spec) {
426
- const outputs = [];
427
- for (const name of agentNames(spec.root)) {
428
- const rel = `.agents/agents/${name}/agent.md`;
429
- const { meta, body } = splitFrontmatter(
430
- readFileSync(join2(spec.root, rel), "utf8")
431
- );
432
- const overrides = spec.agents[name] ?? {};
433
- const note = `<!-- ${MARK} from ${rel} -->
434
- `;
435
- const pick = (tool) => {
436
- const merged = {
437
- ...meta,
438
- ...overrides[tool] ?? {}
439
- };
440
- const kept = {};
441
- for (const key of AGENT_FIELDS[tool] ?? []) {
442
- if (merged[key] !== void 0) kept[key] = merged[key];
443
- }
444
- return kept;
445
- };
446
- for (const tool of ["claude", "gemini"]) {
447
- if (!spec.tools.has(tool)) continue;
448
- outputs.push({
449
- kind: "file",
450
- path: `.${tool}/agents/${name}.md`,
451
- content: `${renderFrontmatter(pick(tool))}
452
- ${note}${body}`
453
- });
454
- }
455
- if (spec.tools.has("codex")) {
456
- const toml = [
457
- `# ${MARK} from ${rel}`,
458
- `name = ${tomlString(meta["name"])}`,
459
- `description = ${tomlString(meta["description"])}`,
460
- `developer_instructions = ${tomlString(body.trim())}`,
461
- ""
462
- ].join("\n");
463
- outputs.push({
464
- kind: "file",
465
- path: `.codex/agents/${name}.toml`,
466
- content: toml
467
- });
468
- }
469
- }
470
- return outputs;
471
- }
472
- function plan(spec, owned = /* @__PURE__ */ new Map()) {
473
- const outputs = [
474
- ...skillOutputs(spec),
475
- ...ruleOutputs(spec),
476
- ...agentOutputs(spec),
477
- ...mcpOutputs(spec)
478
- ];
479
- if (spec.packageRules && spec.tools.has("claude")) {
480
- outputs.push(...packageRuleOutputs(spec.root, spec.packageRules, owned));
481
- }
482
- return outputs;
483
- }
484
- function censusDirs(spec) {
485
- const dirs = [
486
- ".claude/skills",
487
- ".claude/agents",
488
- ".claude/commands",
489
- ".gemini/agents",
490
- ".codex/agents",
491
- ".grok/skills",
492
- `${RULES_DIR}/pmcp`
493
- ];
494
- if (spec.packageRules) dirs.push(`${RULES_DIR}/${spec.packageRules.dir}`);
495
- return dirs;
496
- }
497
-
498
- // src/project/apply.ts
499
- import { createHash } from "crypto";
500
- import {
501
- existsSync as existsSync4,
502
- lstatSync as lstatSync2,
503
- mkdirSync,
504
- readFileSync as readFileSync2,
505
- readdirSync as readdirSync3,
506
- readlinkSync as readlinkSync2,
507
- rmSync,
508
- symlinkSync,
509
- writeFileSync
510
- } from "fs";
511
- import { dirname as dirname2, join as join3, posix as posix2 } from "path";
512
- var LOCK_FILE = "pmcp.lock";
513
- var BEGIN = "# >>> pmcp project (edit pmcp.toml, not this block) >>>";
514
- var END = "# <<< pmcp project <<<";
515
- var PREDECESSORS = [
516
- "by sync-claude-projection.sh",
517
- "AUTO-GENERATED by scripts/agent/project.mjs"
518
- ];
519
- var sha = (text) => createHash("sha256").update(text).digest("hex");
520
- function readLock(root) {
521
- const path = join3(root, LOCK_FILE);
522
- if (!existsSync4(path)) return [];
523
- const lock = JSON.parse(readFileSync2(path, "utf8"));
524
- return lock.outputs ?? [];
525
- }
526
- function linkOf(abs) {
527
- try {
528
- return lstatSync2(abs).isSymbolicLink() ? readlinkSync2(abs) : null;
529
- } catch {
530
- return null;
531
- }
532
- }
533
- var exists = (abs) => existsSync4(abs) || linkOf(abs) !== null;
534
- function readJson(abs) {
535
- if (!existsSync4(abs)) return {};
536
- const value = JSON.parse(readFileSync2(abs, "utf8"));
537
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
538
- }
539
- function getKey(object, key) {
540
- let node = object;
541
- for (const part of key) {
542
- if (node === null || typeof node !== "object") return void 0;
543
- node = node[part];
544
- }
545
- return node;
546
- }
547
- function setKey(object, key, value) {
548
- let node = object;
549
- for (const part of key.slice(0, -1)) {
550
- const next = node[part];
551
- if (next === null || typeof next !== "object" || Array.isArray(next)) {
552
- node[part] = {};
553
- }
554
- node = node[part];
555
- }
556
- node[key[key.length - 1] ?? ""] = value;
557
- }
558
- function deleteKey(object, key) {
559
- const [head, ...rest] = key;
560
- if (head === void 0 || !(head in object)) return;
561
- if (rest.length === 0) {
562
- delete object[head];
563
- return;
564
- }
565
- const child = object[head];
566
- if (child === null || typeof child !== "object") return;
567
- deleteKey(child, rest);
568
- if (Object.keys(child).length === 0) delete object[head];
569
- }
570
- var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
571
- var block = (content) => `${BEGIN}
572
- ${content}${END}
573
- `;
574
- function withoutBlock(text) {
575
- const start = text.indexOf(BEGIN);
576
- if (start === -1) return text;
577
- const end = text.indexOf(END, start);
578
- if (end === -1) return text;
579
- const after = text.slice(end + END.length).replace(/^\n/u, "");
580
- return `${text.slice(0, start).replace(/\n+$/u, "\n")}${after}`;
581
- }
582
- function present(root, output) {
583
- const abs = join3(root, "file" in output ? output.file : output.path);
584
- switch (output.kind) {
585
- case "link":
586
- return linkOf(abs) === output.target;
587
- case "file":
588
- return existsSync4(abs) && linkOf(abs) === null && readFileSync2(abs, "utf8") === output.content;
589
- case "json":
590
- return same(getKey(readJson(abs), output.key), output.value);
591
- case "block":
592
- return existsSync4(abs) && readFileSync2(abs, "utf8").includes(block(output.content));
593
- }
594
- }
595
- function replaceable(root, output, owned) {
596
- if (output.kind === "json" || output.kind === "block") return true;
597
- if (owned.has(output.path)) return true;
598
- const abs = join3(root, output.path);
599
- if (!exists(abs)) return true;
600
- const target = linkOf(abs);
601
- if (target !== null) {
602
- return output.kind === "link" && target === output.target;
603
- }
604
- if (lstatSync2(abs).isDirectory()) return false;
605
- const text = readFileSync2(abs, "utf8");
606
- if (output.kind === "file" && text === output.content) return true;
607
- return PREDECESSORS.some((mark) => text.includes(mark));
608
- }
609
- function undeclared(spec, outputs) {
610
- const declared = new Set(outputs.map((o) => o.path));
611
- const found = [];
612
- for (const dir of censusDirs(spec)) {
613
- const abs = join3(spec.root, dir);
614
- if (!existsSync4(abs)) continue;
615
- for (const entry2 of readdirSync3(abs)) {
616
- const path = `${dir}/${entry2}`;
617
- if (!declared.has(path)) found.push(path);
618
- }
619
- }
620
- const rules = join3(spec.root, RULES_DIR);
621
- if (spec.tools.has("grok") && existsSync4(rules)) {
622
- for (const entry2 of readdirSync3(rules, { withFileTypes: true })) {
623
- if (entry2.isDirectory()) continue;
624
- found.push(`${RULES_DIR}/${entry2.name}`);
625
- }
626
- }
627
- return found.sort();
628
- }
629
- function deadGlobs(root, outputs) {
630
- const rules = outputs.filter(
631
- (o) => (o.kind === "link" || o.kind === "file") && o.path.startsWith(`${RULES_DIR}/`)
632
- );
633
- if (rules.length === 0) return [];
634
- const tracked = trackedFiles(root, true);
635
- if (tracked.length === 0) return [];
636
- const seen = /* @__PURE__ */ new Set();
637
- for (const file of tracked) {
638
- const parts = file.split("/");
639
- for (let i = 1; i <= parts.length; i += 1)
640
- seen.add(parts.slice(0, i).join("/"));
641
- }
642
- const dead = [];
643
- for (const rule of rules) {
644
- let text;
645
- if (rule.kind === "file") {
646
- text = rule.content;
647
- } else if (rule.kind === "link") {
648
- const abs = join3(root, posix2.join(posix2.dirname(rule.path), rule.target));
649
- if (!existsSync4(abs)) continue;
650
- text = readFileSync2(abs, "utf8");
651
- } else {
652
- continue;
653
- }
654
- const paths = splitFrontmatter(text).meta["paths"];
655
- for (const glob of [paths ?? []].flat()) {
656
- if (typeof glob !== "string") continue;
657
- const prefix = glob.replace(/\*.*$/u, "").replace(/\/$/u, "");
658
- if (prefix !== "" && !seen.has(prefix))
659
- dead.push(`${rule.path}: ${glob}`);
660
- }
661
- }
662
- return dead;
663
- }
664
- function lockEntry(output) {
665
- switch (output.kind) {
666
- case "link":
667
- return { path: output.path, kind: "link", target: output.target };
668
- case "file":
669
- return { path: output.path, kind: "file", sha256: sha(output.content) };
670
- case "json":
671
- return {
672
- path: output.path,
673
- kind: "json",
674
- sha256: sha(JSON.stringify(output.value))
675
- };
676
- case "block":
677
- return { path: output.path, kind: "block", sha256: sha(output.content) };
678
- }
679
- }
680
- function project(spec, options) {
681
- const lock = readLock(spec.root);
682
- const ownedTargets = new Map(
683
- lock.filter((e) => e.kind === "link" && e.target !== void 0).map((e) => [e.path, e.target])
684
- );
685
- const outputs = plan(spec, ownedTargets);
686
- const owned = new Set(lock.map((e) => e.path));
687
- const planned = new Set(outputs.map((o) => o.path));
688
- const stale = lock.filter((e) => !planned.has(e.path));
689
- if (options.check) {
690
- const problems = [
691
- ...outputs.filter((o) => !present(spec.root, o)).map((o) => `drift: ${o.path}`),
692
- ...stale.map((e) => `stale: ${e.path}`),
693
- ...undeclared(spec, outputs).map((p) => `undeclared: ${p}`),
694
- ...deadGlobs(spec.root, outputs).map((p) => `dead glob: ${p}`)
695
- ];
696
- return {
697
- code: problems.length > 0 ? 2 : 0,
698
- problems,
699
- outputs: outputs.length,
700
- pruned: 0
701
- };
702
- }
703
- const refused = outputs.filter((o) => !replaceable(spec.root, o, owned)).map((o) => `refusing to overwrite a file it did not write: ${o.path}`);
704
- if (refused.length > 0) {
705
- return { code: 3, problems: refused, outputs: outputs.length, pruned: 0 };
706
- }
707
- for (const output of outputs) {
708
- if (output.kind !== "link" && output.kind !== "file") continue;
709
- if (present(spec.root, output)) continue;
710
- const abs = join3(spec.root, output.path);
711
- mkdirSync(dirname2(abs), { recursive: true });
712
- rmSync(abs, { force: true, recursive: true });
713
- if (output.kind === "link") symlinkSync(output.target, abs);
714
- else writeFileSync(abs, output.content);
715
- }
716
- let pruned = 0;
717
- const jsonFiles = /* @__PURE__ */ new Map();
718
- const blockFiles = /* @__PURE__ */ new Map();
719
- for (const output of outputs) {
720
- if (output.kind === "json") {
721
- const entry2 = jsonFiles.get(output.file) ?? { sets: [], drops: [] };
722
- entry2.sets.push(output);
723
- jsonFiles.set(output.file, entry2);
724
- }
725
- if (output.kind === "block") blockFiles.set(output.file, output.content);
726
- }
727
- for (const entry2 of stale) {
728
- const keyed = splitKeyed(entry2.path);
729
- if (keyed && keyed.key.length === 1 && keyed.key[0] === "pmcp") {
730
- if (!blockFiles.has(keyed.file)) blockFiles.set(keyed.file, null);
731
- } else if (keyed) {
732
- const group = jsonFiles.get(keyed.file) ?? { sets: [], drops: [] };
733
- group.drops.push(keyed.key);
734
- jsonFiles.set(keyed.file, group);
735
- } else {
736
- rmSync(join3(spec.root, entry2.path), { force: true, recursive: true });
737
- }
738
- pruned += 1;
739
- }
740
- for (const [file, { sets, drops }] of jsonFiles) {
741
- const abs = join3(spec.root, file);
742
- const before = readJson(abs);
743
- const after = structuredClone(before);
744
- for (const key of drops) deleteKey(after, key);
745
- for (const output of sets) {
746
- if (output.kind === "json") setKey(after, output.key, output.value);
747
- }
748
- if (same(before, after)) continue;
749
- if (!existsSync4(abs) && Object.keys(after).length === 0) continue;
750
- mkdirSync(dirname2(abs), { recursive: true });
751
- writeFileSync(abs, `${JSON.stringify(after, null, 2)}
752
- `);
753
- }
754
- for (const [file, content] of blockFiles) {
755
- const abs = join3(spec.root, file);
756
- const before = existsSync4(abs) ? readFileSync2(abs, "utf8") : "";
757
- let after = withoutBlock(before);
758
- if (content !== null) {
759
- const sep = after === "" || after.endsWith("\n\n") ? "" : after.endsWith("\n") ? "\n" : "\n\n";
760
- after = `${after}${sep}${block(content)}`;
761
- }
762
- if (after === before) continue;
763
- mkdirSync(dirname2(abs), { recursive: true });
764
- writeFileSync(abs, after);
765
- }
766
- if (spec.packageRules) {
767
- const linkedDirs = new Set(
768
- outputs.filter(
769
- (o) => o.kind === "link" && o.path.startsWith(`${RULES_DIR}/${spec.packageRules?.dir}/`)
770
- ).map(
771
- (o) => o.kind === "link" ? posix2.dirname(o.target.replace(/^(\.\.\/)+/u, "")) : ""
772
- )
773
- );
774
- for (const [name, target] of legacyLinks(spec.root)) {
775
- const abs = join3(spec.root, RULES_DIR, `${name}.md`);
776
- const broken = !existsSync4(abs);
777
- if (!broken && !linkedDirs.has(posix2.dirname(target))) continue;
778
- rmSync(abs, { force: true });
779
- pruned += 1;
780
- }
781
- }
782
- writeFileSync(
783
- join3(spec.root, LOCK_FILE),
784
- `${JSON.stringify({ version: 1, outputs: outputs.map(lockEntry) }, null, 2)}
785
- `
786
- );
787
- return { code: 0, problems: [], outputs: outputs.length, pruned };
788
- }
789
-
790
- // src/project/doctor.ts
791
- import { spawnSync } from "child_process";
792
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
793
- import { join as join4 } from "path";
794
- import { parse as parse2 } from "smol-toml";
795
- var spawnRunner = (command, args, cwd) => {
796
- const result = spawnSync(command, [...args], {
797
- cwd,
798
- encoding: "utf8",
799
- timeout: 18e4,
800
- maxBuffer: 1 << 26
801
- });
802
- const code = result.error?.code;
803
- return {
804
- missing: code === "ENOENT",
805
- status: result.status,
806
- stdout: `${result.stdout ?? ""}${result.stderr ?? ""}`
807
- };
808
- };
809
- var missingFrom = (names, present2) => names.filter((name) => !present2(name));
810
- function verdict(tool, check, missing) {
811
- if (missing.length === 0) return { tool, check, status: "ok" };
812
- return {
813
- tool,
814
- check,
815
- status: "fail",
816
- detail: `missing: ${missing.join(", ")}`
817
- };
818
- }
819
- function listed(tool, check, result, names, present2) {
820
- if (result.missing)
821
- return { tool, check, status: "skip", detail: "not installed" };
822
- if (result.status !== 0) {
823
- return {
824
- tool,
825
- check,
826
- status: "fail",
827
- detail: `exit ${result.status}: ${result.stdout.trim().split("\n").pop() ?? ""}`
828
- };
829
- }
830
- return verdict(
831
- tool,
832
- check,
833
- missingFrom(names, (name) => present2(result.stdout, name))
834
- );
835
- }
836
- var lines = (out) => out.split("\n").map((l) => l.trim());
837
- function claude(spec, o) {
838
- const aliases = Object.keys(spec.mcp);
839
- if (aliases.length === 0) return [];
840
- const result = o.run("claude", ["mcp", "list"], spec.root);
841
- return [
842
- listed(
843
- "claude",
844
- "mcp servers connected",
845
- result,
846
- aliases,
847
- (out, alias) => lines(out).some(
848
- (l) => l.startsWith(`${alias}:`) && /connected/iu.test(l) && !/failed/iu.test(l)
849
- )
850
- )
851
- ];
852
- }
853
- function codex(spec, o) {
854
- const findings = [];
855
- const config = join4(o.home, ".codex/config.toml");
856
- let trusted = false;
857
- if (existsSync5(config)) {
858
- const projects = parse2(readFileSync3(config, "utf8")).projects ?? {};
859
- const root = spec.root.replace(/\/$/u, "");
860
- trusted = projects[root]?.trust_level === "trusted" || projects[`${root}/`]?.trust_level === "trusted";
861
- }
862
- findings.push(
863
- trusted ? { tool: "codex", check: "project trusted", status: "ok" } : {
864
- tool: "codex",
865
- check: "project trusted",
866
- status: "fail",
867
- detail: "untrusted projects skip .codex/ and .agents/"
868
- }
869
- );
870
- const aliases = Object.keys(spec.mcp);
871
- if (aliases.length > 0) {
872
- const result = o.run("codex", ["mcp", "list"], spec.root);
873
- findings.push(
874
- listed(
875
- "codex",
876
- "mcp servers listed",
877
- result,
878
- aliases,
879
- (out, alias) => lines(out).some((l) => l.split(/\s+/u)[0] === alias)
880
- )
881
- );
882
- }
883
- return findings;
884
- }
885
- function gemini(spec, o) {
886
- const findings = [];
887
- const skills = skillNames(spec.root);
888
- if (skills.length > 0) {
889
- const result = o.run("gemini", ["skills", "list"], spec.root);
890
- findings.push(
891
- listed(
892
- "gemini",
893
- "project skills listed",
894
- result,
895
- skills,
896
- (out, name) => lines(out).some((l) => l.startsWith(`${name} [`))
897
- )
898
- );
899
- }
900
- const aliases = Object.keys(spec.mcp);
901
- if (aliases.length > 0) {
902
- const result = o.run("gemini", ["mcp", "list"], spec.root);
903
- findings.push(
904
- listed(
905
- "gemini",
906
- "mcp servers connected",
907
- result,
908
- aliases,
909
- (out, alias) => lines(out).some(
910
- (l) => l.includes(`${alias}:`) && /connected/iu.test(l) && !/disconnected/iu.test(l)
911
- )
912
- )
913
- );
914
- }
915
- return findings;
916
- }
917
- function grok(spec, o) {
918
- const result = o.run("grok", ["inspect", "--json"], spec.root);
919
- if (result.missing)
920
- return [
921
- {
922
- tool: "grok",
923
- check: "inspect",
924
- status: "skip",
925
- detail: "not installed"
926
- }
927
- ];
928
- let report;
929
- try {
930
- report = JSON.parse(
931
- result.stdout.slice(result.stdout.indexOf("{"))
932
- );
933
- } catch {
934
- return [
935
- {
936
- tool: "grok",
937
- check: "inspect",
938
- status: "fail",
939
- detail: "grok inspect --json did not return JSON"
940
- }
941
- ];
942
- }
943
- const findings = [];
944
- findings.push(
945
- report.projectTrusted ? { tool: "grok", check: "project trusted", status: "ok" } : {
946
- tool: "grok",
947
- check: "project trusted",
948
- status: "fail",
949
- detail: "untrusted projects skip .grok/ MCP and hooks"
950
- }
951
- );
952
- const skills = new Set((report.skills ?? []).map((s) => s.name));
953
- findings.push(
954
- verdict(
955
- "grok",
956
- "project skills listed",
957
- missingFrom(skillNames(spec.root), (n) => skills.has(n))
958
- )
959
- );
960
- const leaked = (report.projectInstructions ?? []).filter(
961
- (i) => i.path.includes("/.claude/rules/")
962
- );
963
- findings.push(
964
- leaked.length === 0 ? { tool: "grok", check: "no .claude/rules loaded", status: "ok" } : {
965
- tool: "grok",
966
- check: "no .claude/rules loaded",
967
- status: "fail",
968
- detail: `${leaked.length} rule files load unscoped`
969
- }
970
- );
971
- const servers = new Set((report.mcpServers ?? []).map((s) => s.name));
972
- if (Object.keys(spec.mcp).length > 0) {
973
- findings.push(
974
- verdict(
975
- "grok",
976
- "mcp servers listed",
977
- missingFrom(Object.keys(spec.mcp), (n) => servers.has(n))
978
- )
979
- );
980
- }
981
- return findings;
982
- }
983
- function antigravity(spec) {
984
- const foreign = agentNames(spec.root).filter((name) => {
985
- const text = readFileSync3(
986
- join4(spec.root, ".agents/agents", name, "agent.md"),
987
- "utf8"
988
- );
989
- return splitFrontmatter(text).meta["tools"] !== void 0;
990
- });
991
- return [
992
- foreign.length === 0 ? {
993
- tool: "antigravity",
994
- check: "agents carry no tool lists",
995
- status: "ok"
996
- } : {
997
- tool: "antigravity",
998
- check: "agents carry no tool lists",
999
- status: "fail",
1000
- detail: `move tools to pmcp.toml [agents.<name>.<tool>]: ${foreign.join(", ")}`
1001
- }
1002
- ];
1003
- }
1004
- function doctor(spec, options) {
1005
- const check = project(spec, { check: true });
1006
- const findings = [
1007
- check.code === 0 ? { tool: "project", check: "outputs match sources", status: "ok" } : {
1008
- tool: "project",
1009
- check: "outputs match sources",
1010
- status: "fail",
1011
- detail: check.problems.slice(0, 5).join("; ")
1012
- }
1013
- ];
1014
- const wanted = (tool) => spec.tools.has(tool) && (options.only === void 0 || options.only.has(tool));
1015
- if (wanted("claude")) findings.push(...claude(spec, options));
1016
- if (wanted("codex")) findings.push(...codex(spec, options));
1017
- if (wanted("gemini")) findings.push(...gemini(spec, options));
1018
- if (wanted("grok")) findings.push(...grok(spec, options));
1019
- if (wanted("antigravity")) findings.push(...antigravity(spec));
1020
- return findings;
1021
- }
1022
-
1023
- // src/project/spec.ts
1024
- var TOOLS = [
1025
- "claude",
1026
- "codex",
1027
- "gemini",
1028
- "grok",
1029
- "antigravity"
1030
- ];
1031
- var NAME = /^[a-z0-9][a-z0-9-]*$/u;
1032
- function table(path, where, value) {
1033
- if (value === void 0) return {};
1034
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
1035
- throw new ConfigError(path, `${where} must be a table`);
1036
- }
1037
- return value;
1038
- }
1039
- function strings(path, where, value) {
1040
- if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
1041
- throw new ConfigError(path, `${where} must be a list of strings`);
1042
- }
1043
- return value;
1044
- }
1045
- function named(path, where, name) {
1046
- if (!NAME.test(name)) {
1047
- throw new ConfigError(
1048
- path,
1049
- `${where} ${name}: use lowercase, digits and -`
1050
- );
1051
- }
1052
- return name;
1053
- }
1054
- function readSpec(config) {
1055
- const path = config.path;
1056
- const raw = config.raw;
1057
- const targets = table(path, "[targets]", raw["targets"]);
1058
- const tools = /* @__PURE__ */ new Set();
1059
- if (targets["tools"] !== void 0) {
1060
- for (const tool of strings(path, "[targets] tools", targets["tools"])) {
1061
- if (!TOOLS.includes(tool)) {
1062
- throw new ConfigError(path, `[targets] unknown tool ${tool}`);
1063
- }
1064
- tools.add(tool);
1065
- }
1066
- }
1067
- const skills = {};
1068
- for (const [name, value] of Object.entries(
1069
- table(path, "[skills]", raw["skills"])
1070
- )) {
1071
- const entry2 = table(path, `[skills.${name}]`, value);
1072
- const invocation = entry2["invocation"] ?? "any";
1073
- if (invocation !== "any" && invocation !== "user") {
1074
- throw new ConfigError(
1075
- path,
1076
- `[skills.${name}] invocation must be "any" or "user"`
1077
- );
1078
- }
1079
- skills[named(path, "[skills]", name)] = { invocation };
1080
- }
1081
- const agents = {};
1082
- for (const [name, value] of Object.entries(
1083
- table(path, "[agents]", raw["agents"])
1084
- )) {
1085
- const perTool = {};
1086
- for (const [tool, fields] of Object.entries(
1087
- table(path, `[agents.${name}]`, value)
1088
- )) {
1089
- if (!TOOLS.includes(tool)) {
1090
- throw new ConfigError(path, `[agents.${name}] unknown tool ${tool}`);
1091
- }
1092
- perTool[tool] = table(path, `[agents.${name}.${tool}]`, fields);
1093
- }
1094
- agents[named(path, "[agents]", name)] = perTool;
1095
- }
1096
- const rules = table(path, "[rules]", raw["rules"]);
1097
- let packageRules = null;
1098
- if (rules["packages"] !== void 0) {
1099
- const pkg = table(path, "[rules.packages]", rules["packages"]);
1100
- const file = pkg["file"];
1101
- if (typeof file !== "string" || file === "") {
1102
- throw new ConfigError(path, "[rules.packages] file is required");
1103
- }
1104
- const dir = pkg["dir"] ?? "pkg";
1105
- if (typeof dir !== "string" || !NAME.test(dir) || dir === "pmcp") {
1106
- throw new ConfigError(
1107
- path,
1108
- '[rules.packages] dir must be a plain name other than "pmcp"'
1109
- );
1110
- }
1111
- const strip = pkg["strip"] === void 0 ? [] : strings(path, "[rules.packages] strip", pkg["strip"]);
1112
- packageRules = { file, dir, strip };
1113
- }
1114
- const mcp = {};
1115
- for (const [alias, value] of Object.entries(
1116
- table(path, "[mcp]", raw["mcp"])
1117
- )) {
1118
- const where = `[mcp.${alias}]`;
1119
- const entry2 = table(path, where, value);
1120
- const command = entry2["command"];
1121
- if (typeof command !== "string" || command === "") {
1122
- throw new ConfigError(path, `${where} command is required`);
1123
- }
1124
- const env = {};
1125
- if (entry2["mailbox"] !== void 0 && typeof entry2["mailbox"] !== "boolean") {
1126
- throw new ConfigError(path, `${where} mailbox must be a boolean`);
1127
- }
1128
- for (const [key, v] of Object.entries(
1129
- table(path, `${where} env`, entry2["env"])
1130
- )) {
1131
- if (typeof v !== "string") {
1132
- throw new ConfigError(path, `${where} env ${key} must be a string`);
1133
- }
1134
- env[key] = v;
1135
- }
1136
- mcp[named(path, "[mcp]", alias)] = {
1137
- ...entry2["mailbox"] === true ? { mailbox: true } : {},
1138
- command,
1139
- args: entry2["args"] === void 0 ? [] : strings(path, `${where} args`, entry2["args"]),
1140
- env,
1141
- ...entry2["tools"] === void 0 ? {} : { tools: strings(path, `${where} tools`, entry2["tools"]) }
1142
- };
1143
- }
1144
- return { root: config.dir, tools, skills, agents, packageRules, mcp };
1145
- }
1146
-
1147
- // src/validate.ts
1148
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync } from "fs";
1149
- import { basename, join as join5, resolve } from "path";
1150
- function validateSkillDir(dir) {
1151
- const root = resolve(dir);
1152
- const skillMd = basename(root) === "SKILL.md" ? root : join5(root, "SKILL.md");
1153
- if (!existsSync6(skillMd) || !statSync(skillMd).isFile()) {
1154
- return [{ path: root, reason: "no SKILL.md", level: "error" }];
1155
- }
1156
- const front = frontmatterObject(readFileSync4(skillMd, "utf8"));
1157
- const directory = basename(skillMd === root ? join5(root, "..") : root);
1158
- return specProblems(front, directory).map((reason) => ({
1159
- path: skillMd,
1160
- reason,
1161
- level: "error"
1162
- }));
1163
- }
1164
- function validateCatalog(options) {
1165
- const findings = [];
1166
- readCatalog({
1167
- ...options,
1168
- onReject: (r) => findings.push({ ...r, level: "error" }),
1169
- onWarning: (w) => findings.push({ ...w, level: "warning" })
1170
- });
1171
- return findings;
1172
- }
1173
-
1174
- export {
1175
- prepareSearch,
1176
- splitFrontmatter,
1177
- renderFrontmatter,
1178
- ProjectError,
1179
- deriveName,
1180
- plan,
1181
- LOCK_FILE,
1182
- deadGlobs,
1183
- project,
1184
- spawnRunner,
1185
- doctor,
1186
- TOOLS,
1187
- readSpec,
1188
- validateSkillDir,
1189
- validateCatalog
1190
- };
1191
- //# sourceMappingURL=chunk-ZUFFVIHQ.js.map