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