@modootoday/pmcp 0.16.1 → 0.17.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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +7 -1
- package/dist/cli.js +417 -256
- package/dist/http.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +233 -85
- package/dist/stdio.js +1 -1
- package/docs/project-configuration.md +35 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/plugins/pmcp/.claude-plugin/plugin.json +1 -1
- package/plugins/pmcp/gemini-extension.json +1 -1
- package/plugins/pmcp/plugin.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1548,7 +1548,8 @@ function nativeOptions(path, where, value) {
|
|
|
1548
1548
|
codex: ["startup_timeout_sec", "tool_timeout_sec"],
|
|
1549
1549
|
gemini: ["timeout"],
|
|
1550
1550
|
grok: ["startup_timeout_sec", "tool_timeout_sec"],
|
|
1551
|
-
antigravity: []
|
|
1551
|
+
antigravity: [],
|
|
1552
|
+
opencode: ["timeout"]
|
|
1552
1553
|
};
|
|
1553
1554
|
const result = {};
|
|
1554
1555
|
for (const [tool, raw] of Object.entries(
|
|
@@ -1980,7 +1981,8 @@ var TOOLS = [
|
|
|
1980
1981
|
"codex",
|
|
1981
1982
|
"gemini",
|
|
1982
1983
|
"grok",
|
|
1983
|
-
"antigravity"
|
|
1984
|
+
"antigravity",
|
|
1985
|
+
"opencode"
|
|
1984
1986
|
];
|
|
1985
1987
|
var NAME = /^[a-z0-9][a-z0-9-]*$/u;
|
|
1986
1988
|
function table(path, where, value) {
|
|
@@ -2079,8 +2081,8 @@ function readSpec(config) {
|
|
|
2079
2081
|
}
|
|
2080
2082
|
|
|
2081
2083
|
// src/project/plan.ts
|
|
2082
|
-
import { existsSync as
|
|
2083
|
-
import { join as
|
|
2084
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
|
|
2085
|
+
import { join as join8 } from "path";
|
|
2084
2086
|
|
|
2085
2087
|
// src/project/frontmatter.ts
|
|
2086
2088
|
import { parse as parse3 } from "yaml";
|
|
@@ -2228,6 +2230,56 @@ function packageRuleOutputs(root, spec, owned) {
|
|
|
2228
2230
|
|
|
2229
2231
|
// src/project/mcp.ts
|
|
2230
2232
|
import { isAbsolute as isAbsolute4, relative as relative4 } from "path";
|
|
2233
|
+
|
|
2234
|
+
// src/project/adapters/opencode.ts
|
|
2235
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2236
|
+
import { join as join6 } from "path";
|
|
2237
|
+
var OPENCODE_CONFIG = "opencode.json";
|
|
2238
|
+
function checkOpenCodeConfig(root) {
|
|
2239
|
+
if (existsSync8(join6(root, "opencode.jsonc"))) {
|
|
2240
|
+
throw new ProjectError(
|
|
2241
|
+
"OpenCode JSONC projection is not supported; reconcile opencode.jsonc before projecting opencode.json"
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
function opencodeMcpEntry(server, started) {
|
|
2246
|
+
if (server.tools !== void 0) {
|
|
2247
|
+
throw new ProjectError(
|
|
2248
|
+
"OpenCode per-server tool filtering is not qualified; limit this server's targets"
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
const timeout = server.native?.opencode;
|
|
2252
|
+
if (server.url !== void 0) {
|
|
2253
|
+
const headers = { ...server.headers };
|
|
2254
|
+
for (const [key, name] of Object.entries(server.headerEnv ?? {})) {
|
|
2255
|
+
headers[key] = `{env:${name}}`;
|
|
2256
|
+
}
|
|
2257
|
+
if (server.bearerTokenEnvVar) {
|
|
2258
|
+
headers["Authorization"] = `Bearer {env:${server.bearerTokenEnvVar}}`;
|
|
2259
|
+
}
|
|
2260
|
+
return {
|
|
2261
|
+
type: "remote",
|
|
2262
|
+
url: server.url,
|
|
2263
|
+
enabled: true,
|
|
2264
|
+
...Object.keys(headers).length > 0 ? { headers } : {},
|
|
2265
|
+
...timeout
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
if (!started) {
|
|
2269
|
+
throw new ProjectError(
|
|
2270
|
+
"OpenCode local MCP projection requires a launch specification"
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
return {
|
|
2274
|
+
type: "local",
|
|
2275
|
+
command: [started.command, ...started.args],
|
|
2276
|
+
enabled: true,
|
|
2277
|
+
...Object.keys(started.env).length > 0 ? { environment: { ...started.env } } : {},
|
|
2278
|
+
...timeout
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// src/project/mcp.ts
|
|
2231
2283
|
var runtimeByTool = {
|
|
2232
2284
|
claude: "claude-code",
|
|
2233
2285
|
codex: "codex-cli",
|
|
@@ -2235,6 +2287,15 @@ var runtimeByTool = {
|
|
|
2235
2287
|
grok: "grok-cli",
|
|
2236
2288
|
antigravity: "antigravity"
|
|
2237
2289
|
};
|
|
2290
|
+
function mailboxRuntime(tool) {
|
|
2291
|
+
const runtime = runtimeByTool[tool];
|
|
2292
|
+
if (!runtime) {
|
|
2293
|
+
throw new ProjectError(
|
|
2294
|
+
"OpenCode mailbox runtime binding is not qualified; limit this server's targets"
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
return runtime;
|
|
2298
|
+
}
|
|
2238
2299
|
var ROOT_VAR = "${PROJECT_ROOT}";
|
|
2239
2300
|
var ANTIGRAVITY_MCP_PLUGIN = ".agents/plugins/pmcp-mcp";
|
|
2240
2301
|
var VARIABLE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu;
|
|
@@ -2370,18 +2431,19 @@ function httpEntry(server, tool) {
|
|
|
2370
2431
|
function nativeEntry(spec, alias, server, tool) {
|
|
2371
2432
|
let result;
|
|
2372
2433
|
if (server.url !== void 0) {
|
|
2434
|
+
if (tool === "opencode") return opencodeMcpEntry(server);
|
|
2373
2435
|
result = httpEntry(server, tool);
|
|
2374
2436
|
} else {
|
|
2375
2437
|
const env = { ...server.env };
|
|
2376
2438
|
if (server.mailbox) {
|
|
2377
|
-
env["PMCP_MAILBOX_RUNTIME"] =
|
|
2439
|
+
env["PMCP_MAILBOX_RUNTIME"] = mailboxRuntime(tool);
|
|
2378
2440
|
}
|
|
2379
2441
|
let started;
|
|
2380
2442
|
if (Object.keys(server.envFrom ?? {}).length > 0) {
|
|
2381
2443
|
const entry = relative4(spec.root, process.argv[1] ?? "");
|
|
2382
2444
|
const driverEnv = {};
|
|
2383
2445
|
if (server.mailbox) {
|
|
2384
|
-
driverEnv["PMCP_MAILBOX_RUNTIME"] =
|
|
2446
|
+
driverEnv["PMCP_MAILBOX_RUNTIME"] = mailboxRuntime(tool);
|
|
2385
2447
|
}
|
|
2386
2448
|
if (!isAbsolute4(entry) && !entry.startsWith("..")) {
|
|
2387
2449
|
started = launch({
|
|
@@ -2399,6 +2461,7 @@ function nativeEntry(spec, alias, server, tool) {
|
|
|
2399
2461
|
} else {
|
|
2400
2462
|
started = launch({ ...server, env });
|
|
2401
2463
|
}
|
|
2464
|
+
if (tool === "opencode") return opencodeMcpEntry(server, started);
|
|
2402
2465
|
result = { command: started.command, args: [...started.args] };
|
|
2403
2466
|
if (Object.keys(started.env).length > 0) {
|
|
2404
2467
|
result["env"] = { ...started.env };
|
|
@@ -2455,9 +2518,11 @@ function mcpOutputs(spec) {
|
|
|
2455
2518
|
const jsonFiles = {
|
|
2456
2519
|
claude: ".mcp.json",
|
|
2457
2520
|
gemini: ".gemini/settings.json",
|
|
2458
|
-
antigravity: `${ANTIGRAVITY_MCP_PLUGIN}/mcp_config.json
|
|
2521
|
+
antigravity: `${ANTIGRAVITY_MCP_PLUGIN}/mcp_config.json`,
|
|
2522
|
+
opencode: OPENCODE_CONFIG
|
|
2459
2523
|
};
|
|
2460
2524
|
for (const tool of spec.tools) {
|
|
2525
|
+
if (tool === "opencode") checkOpenCodeConfig(spec.root);
|
|
2461
2526
|
const servers = serversForTool(spec, tool);
|
|
2462
2527
|
if (servers.length === 0) {
|
|
2463
2528
|
continue;
|
|
@@ -2476,7 +2541,7 @@ function mcpOutputs(spec) {
|
|
|
2476
2541
|
outputs.push(
|
|
2477
2542
|
jsonOutput(
|
|
2478
2543
|
file,
|
|
2479
|
-
["mcpServers", alias],
|
|
2544
|
+
[tool === "opencode" ? "mcp" : "mcpServers", alias],
|
|
2480
2545
|
nativeEntry(spec, alias, server, tool)
|
|
2481
2546
|
)
|
|
2482
2547
|
);
|
|
@@ -2492,8 +2557,8 @@ function mcpOutputs(spec) {
|
|
|
2492
2557
|
|
|
2493
2558
|
// src/project/hooks.ts
|
|
2494
2559
|
import { createHash as createHash4 } from "crypto";
|
|
2495
|
-
import { existsSync as
|
|
2496
|
-
import { join as
|
|
2560
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
2561
|
+
import { join as join7 } from "path";
|
|
2497
2562
|
import { fileURLToPath } from "url";
|
|
2498
2563
|
var HOOK_DIRECTORY = ".agents/pmcp-hooks";
|
|
2499
2564
|
var ADAPTER = `${HOOK_DIRECTORY}/adapter.mjs`;
|
|
@@ -2523,8 +2588,8 @@ function object4(value) {
|
|
|
2523
2588
|
return value;
|
|
2524
2589
|
}
|
|
2525
2590
|
function groups(root, file, key) {
|
|
2526
|
-
const absolute =
|
|
2527
|
-
if (!
|
|
2591
|
+
const absolute = join7(root, file);
|
|
2592
|
+
if (!existsSync9(absolute)) {
|
|
2528
2593
|
return [];
|
|
2529
2594
|
}
|
|
2530
2595
|
let value;
|
|
@@ -2553,15 +2618,15 @@ function adapterSource() {
|
|
|
2553
2618
|
const sourceBuild = fileURLToPath(
|
|
2554
2619
|
new URL("../../dist/project/hook-adapter.js", import.meta.url)
|
|
2555
2620
|
);
|
|
2556
|
-
const file =
|
|
2557
|
-
if (!
|
|
2621
|
+
const file = existsSync9(published) ? published : sourceBuild;
|
|
2622
|
+
if (!existsSync9(file)) {
|
|
2558
2623
|
throw new ProjectError("Build pmcp before projecting shell hook adapters");
|
|
2559
2624
|
}
|
|
2560
2625
|
return readFileSync6(file, "utf8");
|
|
2561
2626
|
}
|
|
2562
2627
|
function hookOutputs(spec) {
|
|
2563
|
-
const lockFile =
|
|
2564
|
-
const lock =
|
|
2628
|
+
const lockFile = join7(spec.root, "pmcp.lock");
|
|
2629
|
+
const lock = existsSync9(lockFile) ? JSON.parse(readFileSync6(lockFile, "utf8")) : {};
|
|
2565
2630
|
const previous = new Map(
|
|
2566
2631
|
(lock.outputs ?? []).filter((entry) => entry.managed !== void 0).map((entry) => [entry.path, entry.managed])
|
|
2567
2632
|
);
|
|
@@ -2641,6 +2706,14 @@ function hookOutputs(spec) {
|
|
|
2641
2706
|
return outputs;
|
|
2642
2707
|
}
|
|
2643
2708
|
|
|
2709
|
+
// src/project/adapters/opencode-skills.ts
|
|
2710
|
+
function opencodeSkillOutputs(spec, names) {
|
|
2711
|
+
if (!spec.tools.has("opencode")) return [];
|
|
2712
|
+
return names.filter((name) => spec.skills[name]?.invocation === "user").map(
|
|
2713
|
+
(name) => jsonOutput(OPENCODE_CONFIG, ["permission", "skill", name], "deny")
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2644
2717
|
// src/project/plan.ts
|
|
2645
2718
|
var AGENT_FIELDS = {
|
|
2646
2719
|
claude: ["name", "description", "tools", "model", "effort"],
|
|
@@ -2648,25 +2721,26 @@ var AGENT_FIELDS = {
|
|
|
2648
2721
|
};
|
|
2649
2722
|
var SETTINGS = ".claude/settings.json";
|
|
2650
2723
|
function listDirs(dir) {
|
|
2651
|
-
if (!
|
|
2724
|
+
if (!existsSync10(dir)) return [];
|
|
2652
2725
|
return readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
2653
2726
|
}
|
|
2654
2727
|
var tomlString = (value) => JSON.stringify(typeof value === "string" ? value : String(value ?? ""));
|
|
2655
2728
|
function skillNames(root) {
|
|
2656
|
-
const dir =
|
|
2729
|
+
const dir = join8(root, ".agents/skills");
|
|
2657
2730
|
return listDirs(dir).filter(
|
|
2658
|
-
(name) =>
|
|
2731
|
+
(name) => existsSync10(join8(dir, name, "SKILL.md"))
|
|
2659
2732
|
);
|
|
2660
2733
|
}
|
|
2661
2734
|
function agentNames(root) {
|
|
2662
|
-
const dir =
|
|
2735
|
+
const dir = join8(root, ".agents/agents");
|
|
2663
2736
|
return listDirs(dir).filter(
|
|
2664
|
-
(name) =>
|
|
2737
|
+
(name) => existsSync10(join8(dir, name, "agent.md"))
|
|
2665
2738
|
);
|
|
2666
2739
|
}
|
|
2667
2740
|
function skillOutputs(spec) {
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2741
|
+
const names = skillNames(spec.root);
|
|
2742
|
+
const outputs = opencodeSkillOutputs(spec, names);
|
|
2743
|
+
for (const name of names) {
|
|
2670
2744
|
const target = `../../.agents/skills/${name}`;
|
|
2671
2745
|
if (spec.tools.has("claude")) {
|
|
2672
2746
|
outputs.push({ kind: "link", path: `.claude/skills/${name}`, target });
|
|
@@ -2695,13 +2769,13 @@ policy:
|
|
|
2695
2769
|
}
|
|
2696
2770
|
function ruleOutputs(spec) {
|
|
2697
2771
|
if (!spec.tools.has("claude")) return [];
|
|
2698
|
-
const dir =
|
|
2699
|
-
if (!
|
|
2772
|
+
const dir = join8(spec.root, ".agents/rules");
|
|
2773
|
+
if (!existsSync10(dir)) return [];
|
|
2700
2774
|
const outputs = [];
|
|
2701
2775
|
for (const file of readdirSync4(dir).filter((f) => f.endsWith(".md")).sort()) {
|
|
2702
2776
|
const rel = `.agents/rules/${file}`;
|
|
2703
2777
|
const { meta, body } = splitFrontmatter(
|
|
2704
|
-
readFileSync7(
|
|
2778
|
+
readFileSync7(join8(spec.root, rel), "utf8")
|
|
2705
2779
|
);
|
|
2706
2780
|
const globs = [meta["globs"] ?? []].flat().filter((g) => typeof g === "string" && g !== "");
|
|
2707
2781
|
const head = globs.length > 0 ? renderFrontmatter({ paths: globs }) : "";
|
|
@@ -2720,7 +2794,7 @@ function agentOutputs(spec) {
|
|
|
2720
2794
|
for (const name of agentNames(spec.root)) {
|
|
2721
2795
|
const rel = `.agents/agents/${name}/agent.md`;
|
|
2722
2796
|
const { meta, body } = splitFrontmatter(
|
|
2723
|
-
readFileSync7(
|
|
2797
|
+
readFileSync7(join8(spec.root, rel), "utf8")
|
|
2724
2798
|
);
|
|
2725
2799
|
const overrides = spec.agents[name] ?? {};
|
|
2726
2800
|
const note = `<!-- ${MARK} from ${rel} -->
|
|
@@ -2792,7 +2866,7 @@ function censusDirs(spec) {
|
|
|
2792
2866
|
// src/project/apply.ts
|
|
2793
2867
|
import { createHash as createHash6 } from "crypto";
|
|
2794
2868
|
import {
|
|
2795
|
-
existsSync as
|
|
2869
|
+
existsSync as existsSync12,
|
|
2796
2870
|
lstatSync as lstatSync4,
|
|
2797
2871
|
mkdirSync as mkdirSync2,
|
|
2798
2872
|
readFileSync as readFileSync9,
|
|
@@ -2800,13 +2874,13 @@ import {
|
|
|
2800
2874
|
readlinkSync as readlinkSync2,
|
|
2801
2875
|
rmSync as rmSync2
|
|
2802
2876
|
} from "fs";
|
|
2803
|
-
import { dirname as dirname8, join as
|
|
2877
|
+
import { dirname as dirname8, join as join10, posix as posix2 } from "path";
|
|
2804
2878
|
import { parse as parse4 } from "smol-toml";
|
|
2805
2879
|
|
|
2806
2880
|
// src/project/backup.ts
|
|
2807
2881
|
import { createHash as createHash5 } from "crypto";
|
|
2808
2882
|
import {
|
|
2809
|
-
existsSync as
|
|
2883
|
+
existsSync as existsSync11,
|
|
2810
2884
|
lstatSync as lstatSync2,
|
|
2811
2885
|
mkdirSync,
|
|
2812
2886
|
mkdtempSync,
|
|
@@ -2814,9 +2888,9 @@ import {
|
|
|
2814
2888
|
writeFileSync
|
|
2815
2889
|
} from "fs";
|
|
2816
2890
|
import { homedir } from "os";
|
|
2817
|
-
import { dirname as dirname6, isAbsolute as isAbsolute5, join as
|
|
2891
|
+
import { dirname as dirname6, isAbsolute as isAbsolute5, join as join9, relative as relative5, resolve as resolve6 } from "path";
|
|
2818
2892
|
function backupFiles(root, paths) {
|
|
2819
|
-
const base =
|
|
2893
|
+
const base = join9(homedir(), ".cache/pmcp/project-backups");
|
|
2820
2894
|
const inside = relative5(resolve6(root), resolve6(base));
|
|
2821
2895
|
if (!isAbsolute5(inside) && inside !== ".." && !inside.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
2822
2896
|
throw new ProjectError("Project backups must be outside the project");
|
|
@@ -2837,7 +2911,7 @@ function backupFiles(root, paths) {
|
|
|
2837
2911
|
if (isAbsolute5(within) || within === ".." || within.startsWith("../") || within.startsWith("..\\")) {
|
|
2838
2912
|
throw new ProjectError("Backup paths must stay inside the project");
|
|
2839
2913
|
}
|
|
2840
|
-
if (!
|
|
2914
|
+
if (!existsSync11(absolute)) {
|
|
2841
2915
|
return { path, contents: null };
|
|
2842
2916
|
}
|
|
2843
2917
|
const stat = lstatSync2(absolute);
|
|
@@ -2848,12 +2922,12 @@ function backupFiles(root, paths) {
|
|
|
2848
2922
|
}
|
|
2849
2923
|
return { path, contents: readFileSync8(absolute) };
|
|
2850
2924
|
});
|
|
2851
|
-
const destination = mkdtempSync(
|
|
2925
|
+
const destination = mkdtempSync(join9(base, "project-"));
|
|
2852
2926
|
const entries = snapshots.map(({ path, contents }) => {
|
|
2853
2927
|
if (contents === null) {
|
|
2854
2928
|
return { path, present: false };
|
|
2855
2929
|
}
|
|
2856
|
-
const output =
|
|
2930
|
+
const output = join9(destination, "files", path);
|
|
2857
2931
|
mkdirSync(dirname6(output), { recursive: true, mode: 448 });
|
|
2858
2932
|
writeFileSync(output, contents, { mode: 384 });
|
|
2859
2933
|
return {
|
|
@@ -2863,7 +2937,7 @@ function backupFiles(root, paths) {
|
|
|
2863
2937
|
};
|
|
2864
2938
|
});
|
|
2865
2939
|
writeFileSync(
|
|
2866
|
-
|
|
2940
|
+
join9(destination, "receipt.json"),
|
|
2867
2941
|
JSON.stringify(
|
|
2868
2942
|
{
|
|
2869
2943
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2949,8 +3023,8 @@ var PREDECESSORS = [
|
|
|
2949
3023
|
];
|
|
2950
3024
|
var sha = (text) => createHash6("sha256").update(text).digest("hex");
|
|
2951
3025
|
function readLock(root) {
|
|
2952
|
-
const path =
|
|
2953
|
-
if (!
|
|
3026
|
+
const path = join10(root, LOCK_FILE);
|
|
3027
|
+
if (!existsSync12(path)) return [];
|
|
2954
3028
|
const lock = JSON.parse(readFileSync9(path, "utf8"));
|
|
2955
3029
|
return lock.outputs ?? [];
|
|
2956
3030
|
}
|
|
@@ -2961,9 +3035,9 @@ function linkOf(abs) {
|
|
|
2961
3035
|
return null;
|
|
2962
3036
|
}
|
|
2963
3037
|
}
|
|
2964
|
-
var exists = (abs) =>
|
|
3038
|
+
var exists = (abs) => existsSync12(abs) || linkOf(abs) !== null;
|
|
2965
3039
|
function readJson2(abs) {
|
|
2966
|
-
if (!
|
|
3040
|
+
if (!existsSync12(abs)) return {};
|
|
2967
3041
|
let value;
|
|
2968
3042
|
try {
|
|
2969
3043
|
value = JSON.parse(readFileSync9(abs, "utf8"));
|
|
@@ -3018,32 +3092,44 @@ function withoutBlock(text) {
|
|
|
3018
3092
|
return `${text.slice(0, start).replace(/\n+$/u, "\n")}${after}`;
|
|
3019
3093
|
}
|
|
3020
3094
|
function present(root, output) {
|
|
3021
|
-
const abs =
|
|
3095
|
+
const abs = join10(root, "file" in output ? output.file : output.path);
|
|
3022
3096
|
switch (output.kind) {
|
|
3023
3097
|
case "link":
|
|
3024
3098
|
return linkOf(abs) === output.target;
|
|
3025
3099
|
case "file":
|
|
3026
|
-
return
|
|
3100
|
+
return existsSync12(abs) && linkOf(abs) === null && readFileSync9(abs, "utf8") === output.content;
|
|
3027
3101
|
case "json":
|
|
3028
3102
|
if (output.managed && Array.isArray(output.value) && output.value.length === 0) {
|
|
3029
3103
|
return getKey(readJson2(abs), output.key) === void 0;
|
|
3030
3104
|
}
|
|
3031
3105
|
return same(getKey(readJson2(abs), output.key), output.value);
|
|
3032
3106
|
case "block":
|
|
3033
|
-
return
|
|
3107
|
+
return existsSync12(abs) && readFileSync9(abs, "utf8").includes(block(output.content));
|
|
3034
3108
|
}
|
|
3035
3109
|
}
|
|
3036
3110
|
function replaceable(root, output, owned) {
|
|
3037
3111
|
if (output.kind === "json") {
|
|
3038
|
-
if (output.
|
|
3112
|
+
if (output.file === "opencode.json") {
|
|
3113
|
+
let parent = readJson2(join10(root, output.file));
|
|
3114
|
+
for (const part of output.key.slice(0, -1)) {
|
|
3115
|
+
if (parent === void 0) break;
|
|
3116
|
+
if (parent === null || typeof parent !== "object" || Array.isArray(parent))
|
|
3117
|
+
return false;
|
|
3118
|
+
parent = parent[part];
|
|
3119
|
+
}
|
|
3120
|
+
if (parent !== void 0 && (parent === null || typeof parent !== "object" || Array.isArray(parent)))
|
|
3121
|
+
return false;
|
|
3122
|
+
}
|
|
3123
|
+
const guarded = output.key[0] === "mcpServers" || output.file === "opencode.json";
|
|
3124
|
+
if (!guarded || owned.has(output.path)) {
|
|
3039
3125
|
return true;
|
|
3040
3126
|
}
|
|
3041
|
-
const existing = getKey(readJson2(
|
|
3127
|
+
const existing = getKey(readJson2(join10(root, output.file)), output.key);
|
|
3042
3128
|
return existing === void 0 || same(existing, output.value);
|
|
3043
3129
|
}
|
|
3044
3130
|
if (output.kind === "block") {
|
|
3045
|
-
const absolute =
|
|
3046
|
-
const text2 =
|
|
3131
|
+
const absolute = join10(root, output.file);
|
|
3132
|
+
const text2 = existsSync12(absolute) ? readFileSync9(absolute, "utf8") : "";
|
|
3047
3133
|
const outside = parse4(withoutBlock(text2));
|
|
3048
3134
|
const proposed = parse4(output.content);
|
|
3049
3135
|
const existing = objectKeys(outside["mcp_servers"]);
|
|
@@ -3052,7 +3138,7 @@ function replaceable(root, output, owned) {
|
|
|
3052
3138
|
);
|
|
3053
3139
|
}
|
|
3054
3140
|
if (owned.has(output.path)) return true;
|
|
3055
|
-
const abs =
|
|
3141
|
+
const abs = join10(root, output.path);
|
|
3056
3142
|
if (!exists(abs)) return true;
|
|
3057
3143
|
const target = linkOf(abs);
|
|
3058
3144
|
if (target !== null) {
|
|
@@ -3073,15 +3159,15 @@ function undeclared(spec, outputs) {
|
|
|
3073
3159
|
const declared = new Set(outputs.map((o) => o.path));
|
|
3074
3160
|
const found = [];
|
|
3075
3161
|
for (const dir of censusDirs(spec)) {
|
|
3076
|
-
const abs =
|
|
3077
|
-
if (!
|
|
3162
|
+
const abs = join10(spec.root, dir);
|
|
3163
|
+
if (!existsSync12(abs)) continue;
|
|
3078
3164
|
for (const entry of readdirSync5(abs)) {
|
|
3079
3165
|
const path = `${dir}/${entry}`;
|
|
3080
3166
|
if (!declared.has(path)) found.push(path);
|
|
3081
3167
|
}
|
|
3082
3168
|
}
|
|
3083
|
-
const rules =
|
|
3084
|
-
if (spec.tools.has("grok") &&
|
|
3169
|
+
const rules = join10(spec.root, RULES_DIR);
|
|
3170
|
+
if (spec.tools.has("grok") && existsSync12(rules)) {
|
|
3085
3171
|
for (const entry of readdirSync5(rules, { withFileTypes: true })) {
|
|
3086
3172
|
if (entry.isDirectory()) continue;
|
|
3087
3173
|
found.push(`${RULES_DIR}/${entry.name}`);
|
|
@@ -3108,8 +3194,8 @@ function deadGlobs(root, outputs) {
|
|
|
3108
3194
|
if (rule.kind === "file") {
|
|
3109
3195
|
text = rule.content;
|
|
3110
3196
|
} else if (rule.kind === "link") {
|
|
3111
|
-
const abs =
|
|
3112
|
-
if (!
|
|
3197
|
+
const abs = join10(root, posix2.join(posix2.dirname(rule.path), rule.target));
|
|
3198
|
+
if (!existsSync12(abs)) continue;
|
|
3113
3199
|
text = readFileSync9(abs, "utf8");
|
|
3114
3200
|
} else {
|
|
3115
3201
|
continue;
|
|
@@ -3160,7 +3246,7 @@ function project(spec, options) {
|
|
|
3160
3246
|
}
|
|
3161
3247
|
}
|
|
3162
3248
|
for (const path of jsonPaths) {
|
|
3163
|
-
readJson2(
|
|
3249
|
+
readJson2(join10(spec.root, path));
|
|
3164
3250
|
}
|
|
3165
3251
|
if (options.check) {
|
|
3166
3252
|
const problems = [
|
|
@@ -3181,6 +3267,7 @@ function project(spec, options) {
|
|
|
3181
3267
|
return { code: 3, problems: refused, outputs: outputs.length, pruned: 0 };
|
|
3182
3268
|
}
|
|
3183
3269
|
const mcpPaths = [
|
|
3270
|
+
"opencode.json",
|
|
3184
3271
|
".mcp.json",
|
|
3185
3272
|
".gemini/settings.json",
|
|
3186
3273
|
".agents/mcp_config.json",
|
|
@@ -3222,7 +3309,7 @@ function project(spec, options) {
|
|
|
3222
3309
|
for (const output of outputs) {
|
|
3223
3310
|
if (output.kind !== "link" && output.kind !== "file") continue;
|
|
3224
3311
|
if (present(spec.root, output)) continue;
|
|
3225
|
-
const abs =
|
|
3312
|
+
const abs = join10(spec.root, output.path);
|
|
3226
3313
|
mkdirSync2(dirname8(abs), { recursive: true });
|
|
3227
3314
|
if (output.kind === "link") {
|
|
3228
3315
|
replaceLink(abs, output.target);
|
|
@@ -3250,12 +3337,12 @@ function project(spec, options) {
|
|
|
3250
3337
|
group.drops.push(keyed.key);
|
|
3251
3338
|
jsonFiles.set(keyed.file, group);
|
|
3252
3339
|
} else {
|
|
3253
|
-
rmSync2(
|
|
3340
|
+
rmSync2(join10(spec.root, entry.path), { force: true, recursive: true });
|
|
3254
3341
|
}
|
|
3255
3342
|
pruned += 1;
|
|
3256
3343
|
}
|
|
3257
3344
|
for (const [file, { sets, drops }] of jsonFiles) {
|
|
3258
|
-
const abs =
|
|
3345
|
+
const abs = join10(spec.root, file);
|
|
3259
3346
|
const before = readJson2(abs);
|
|
3260
3347
|
const after = structuredClone(before);
|
|
3261
3348
|
for (const key of drops) deleteKey(after, key);
|
|
@@ -3270,14 +3357,14 @@ function project(spec, options) {
|
|
|
3270
3357
|
}
|
|
3271
3358
|
}
|
|
3272
3359
|
if (same(before, after)) continue;
|
|
3273
|
-
if (!
|
|
3360
|
+
if (!existsSync12(abs) && Object.keys(after).length === 0) continue;
|
|
3274
3361
|
mkdirSync2(dirname8(abs), { recursive: true });
|
|
3275
3362
|
replaceFile(abs, `${JSON.stringify(after, null, 2)}
|
|
3276
3363
|
`);
|
|
3277
3364
|
}
|
|
3278
3365
|
for (const [file, content] of blockFiles) {
|
|
3279
|
-
const abs =
|
|
3280
|
-
const before =
|
|
3366
|
+
const abs = join10(spec.root, file);
|
|
3367
|
+
const before = existsSync12(abs) ? readFileSync9(abs, "utf8") : "";
|
|
3281
3368
|
let after = withoutBlock(before);
|
|
3282
3369
|
if (content !== null) {
|
|
3283
3370
|
const sep5 = after === "" || after.endsWith("\n\n") ? "" : after.endsWith("\n") ? "\n" : "\n\n";
|
|
@@ -3296,15 +3383,15 @@ function project(spec, options) {
|
|
|
3296
3383
|
)
|
|
3297
3384
|
);
|
|
3298
3385
|
for (const [name, target] of legacyLinks(spec.root)) {
|
|
3299
|
-
const abs =
|
|
3300
|
-
const broken = !
|
|
3386
|
+
const abs = join10(spec.root, RULES_DIR, `${name}.md`);
|
|
3387
|
+
const broken = !existsSync12(abs);
|
|
3301
3388
|
if (!broken && !linkedDirs.has(posix2.dirname(target))) continue;
|
|
3302
3389
|
rmSync2(abs, { force: true });
|
|
3303
3390
|
pruned += 1;
|
|
3304
3391
|
}
|
|
3305
3392
|
}
|
|
3306
3393
|
replaceFile(
|
|
3307
|
-
|
|
3394
|
+
join10(spec.root, LOCK_FILE),
|
|
3308
3395
|
`${JSON.stringify({ version: 1, outputs: outputs.filter((output) => output.kind !== "json" || output.managed === void 0 || output.managed.length > 0).map(lockEntry) }, null, 2)}
|
|
3309
3396
|
`
|
|
3310
3397
|
);
|
|
@@ -3319,9 +3406,68 @@ function project(spec, options) {
|
|
|
3319
3406
|
|
|
3320
3407
|
// src/project/doctor.ts
|
|
3321
3408
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3322
|
-
import { existsSync as
|
|
3323
|
-
import { join as
|
|
3409
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
|
|
3410
|
+
import { join as join12 } from "path";
|
|
3324
3411
|
import { parse as parse5 } from "smol-toml";
|
|
3412
|
+
|
|
3413
|
+
// src/project/adapters/opencode-doctor.ts
|
|
3414
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
3415
|
+
import { join as join11 } from "path";
|
|
3416
|
+
function connected(out, alias) {
|
|
3417
|
+
const ansi = new RegExp(
|
|
3418
|
+
`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`,
|
|
3419
|
+
"gu"
|
|
3420
|
+
);
|
|
3421
|
+
const pattern = new RegExp(
|
|
3422
|
+
`^[^a-zA-Z0-9]*${alias}\\s+connected(?:\\s|$)`,
|
|
3423
|
+
"u"
|
|
3424
|
+
);
|
|
3425
|
+
return out.replace(ansi, "").split("\n").some((line) => pattern.test(line.trim()));
|
|
3426
|
+
}
|
|
3427
|
+
function projectSkill(out, root, name) {
|
|
3428
|
+
try {
|
|
3429
|
+
const entries = JSON.parse(out);
|
|
3430
|
+
if (!Array.isArray(entries)) return false;
|
|
3431
|
+
const expected = realpathSync3(
|
|
3432
|
+
join11(root, ".agents/skills", name, "SKILL.md")
|
|
3433
|
+
);
|
|
3434
|
+
return entries.some(
|
|
3435
|
+
(entry) => entry?.name === name && typeof entry.location === "string" && realpathSync3(entry.location) === expected
|
|
3436
|
+
);
|
|
3437
|
+
} catch {
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
function opencodeDoctor(spec, options, listed2) {
|
|
3442
|
+
const findings = [];
|
|
3443
|
+
const names = skillNames(spec.root);
|
|
3444
|
+
if (names.length > 0) {
|
|
3445
|
+
findings.push(
|
|
3446
|
+
listed2(
|
|
3447
|
+
"opencode",
|
|
3448
|
+
"project skills discovered",
|
|
3449
|
+
options.run("opencode", ["debug", "skill"], spec.root, options.env),
|
|
3450
|
+
names,
|
|
3451
|
+
(out, name) => projectSkill(out, spec.root, name)
|
|
3452
|
+
)
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
const aliases = serversForTool(spec, "opencode").map(([alias]) => alias);
|
|
3456
|
+
if (aliases.length > 0) {
|
|
3457
|
+
findings.push(
|
|
3458
|
+
listed2(
|
|
3459
|
+
"opencode",
|
|
3460
|
+
"mcp servers connected",
|
|
3461
|
+
options.run("opencode", ["mcp", "list"], spec.root, options.env),
|
|
3462
|
+
aliases,
|
|
3463
|
+
connected
|
|
3464
|
+
)
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
3467
|
+
return findings;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// src/project/doctor.ts
|
|
3325
3471
|
var missingFrom = (names, present2) => names.filter((name) => !present2(name));
|
|
3326
3472
|
function verdict(tool, check, missing) {
|
|
3327
3473
|
if (missing.length === 0) return { tool, check, status: "ok" };
|
|
@@ -3382,9 +3528,9 @@ function claude(spec, o) {
|
|
|
3382
3528
|
}
|
|
3383
3529
|
function codex(spec, o) {
|
|
3384
3530
|
const findings = [];
|
|
3385
|
-
const config =
|
|
3531
|
+
const config = join12(o.home, ".codex/config.toml");
|
|
3386
3532
|
let trusted = false;
|
|
3387
|
-
if (
|
|
3533
|
+
if (existsSync13(config)) {
|
|
3388
3534
|
const projects = parse5(readFileSync10(config, "utf8")).projects ?? {};
|
|
3389
3535
|
const root = spec.root.replace(/\/$/u, "");
|
|
3390
3536
|
trusted = projects[root]?.trust_level === "trusted" || projects[`${root}/`]?.trust_level === "trusted";
|
|
@@ -3512,7 +3658,7 @@ function grok(spec, o) {
|
|
|
3512
3658
|
function antigravity(spec, options) {
|
|
3513
3659
|
const foreign = agentNames(spec.root).filter((name) => {
|
|
3514
3660
|
const text = readFileSync10(
|
|
3515
|
-
|
|
3661
|
+
join12(spec.root, ".agents/agents", name, "agent.md"),
|
|
3516
3662
|
"utf8"
|
|
3517
3663
|
);
|
|
3518
3664
|
return splitFrontmatter(text).meta["tools"] !== void 0;
|
|
@@ -3532,7 +3678,7 @@ function antigravity(spec, options) {
|
|
|
3532
3678
|
if (serversForTool(spec, "antigravity").length > 0) {
|
|
3533
3679
|
const result = options.run(
|
|
3534
3680
|
"agy",
|
|
3535
|
-
["plugin", "validate",
|
|
3681
|
+
["plugin", "validate", join12(spec.root, ANTIGRAVITY_MCP_PLUGIN)],
|
|
3536
3682
|
spec.root
|
|
3537
3683
|
);
|
|
3538
3684
|
findings.push(
|
|
@@ -3556,6 +3702,8 @@ function doctor(spec, options) {
|
|
|
3556
3702
|
if (wanted("codex")) findings.push(...codex(spec, options));
|
|
3557
3703
|
if (wanted("gemini")) findings.push(...gemini(spec, options));
|
|
3558
3704
|
if (wanted("grok")) findings.push(...grok(spec, options));
|
|
3705
|
+
if (wanted("opencode"))
|
|
3706
|
+
findings.push(...opencodeDoctor(spec, options, listed));
|
|
3559
3707
|
if (wanted("antigravity")) {
|
|
3560
3708
|
findings.push(...antigravity(spec, options));
|
|
3561
3709
|
}
|
|
@@ -3567,7 +3715,7 @@ import { readFileSync as readFileSync14 } from "fs";
|
|
|
3567
3715
|
|
|
3568
3716
|
// src/files/bundle.ts
|
|
3569
3717
|
import { readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
3570
|
-
import { dirname as dirname9, join as
|
|
3718
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
3571
3719
|
|
|
3572
3720
|
// src/mime.ts
|
|
3573
3721
|
var TYPES = {
|
|
@@ -3675,7 +3823,7 @@ function classify(path, bytes) {
|
|
|
3675
3823
|
// src/files/list.ts
|
|
3676
3824
|
import { createHash as createHash7 } from "crypto";
|
|
3677
3825
|
import { readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
|
|
3678
|
-
import { basename as basename4, join as
|
|
3826
|
+
import { basename as basename4, join as join13, relative as relative6, sep as sep2 } from "path";
|
|
3679
3827
|
|
|
3680
3828
|
// src/files/redact.ts
|
|
3681
3829
|
import { basename as basename3 } from "path";
|
|
@@ -3725,7 +3873,7 @@ function listSkillFiles(root, limit = FILE_LIST_LIMIT) {
|
|
|
3725
3873
|
const dir = stack.pop();
|
|
3726
3874
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
3727
3875
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
3728
|
-
const full =
|
|
3876
|
+
const full = join13(dir, entry.name);
|
|
3729
3877
|
if (entry.isDirectory()) {
|
|
3730
3878
|
stack.push(full);
|
|
3731
3879
|
continue;
|
|
@@ -3767,7 +3915,7 @@ function bundleSkill(entry) {
|
|
|
3767
3915
|
files: listing2.files.map((file) => ({
|
|
3768
3916
|
...file,
|
|
3769
3917
|
uri: skillFileUri(entry, file.path),
|
|
3770
|
-
mimeType: classify(file.path, readFileSync12(
|
|
3918
|
+
mimeType: classify(file.path, readFileSync12(join14(base, file.path))).mimeType
|
|
3771
3919
|
}))
|
|
3772
3920
|
};
|
|
3773
3921
|
}
|
|
@@ -3776,19 +3924,19 @@ function bundleSkill(entry) {
|
|
|
3776
3924
|
import { readFileSync as readFileSync13, statSync as statSync6 } from "fs";
|
|
3777
3925
|
|
|
3778
3926
|
// src/files/resolve.ts
|
|
3779
|
-
import { realpathSync as
|
|
3927
|
+
import { realpathSync as realpathSync4, statSync as statSync5 } from "fs";
|
|
3780
3928
|
import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative7, resolve as resolve7, sep as sep3 } from "path";
|
|
3781
3929
|
var ownFile = (entry) => relative7(assetRoot(entry), entry.path).split(sep3).join("/");
|
|
3782
3930
|
function resolveSkillFile(entry, wanted = ownFile(entry)) {
|
|
3783
3931
|
if (isAbsolute6(wanted) || wanted.split(/[\\/]/u).includes("..")) {
|
|
3784
3932
|
return { ok: false, error: "path_outside_skill", detail: wanted };
|
|
3785
3933
|
}
|
|
3786
|
-
const scope =
|
|
3934
|
+
const scope = realpathSync4(assetRoot(entry));
|
|
3787
3935
|
const single = statSync5(scope).isFile();
|
|
3788
3936
|
const root = single ? dirname10(scope) : scope;
|
|
3789
3937
|
let target;
|
|
3790
3938
|
try {
|
|
3791
|
-
target =
|
|
3939
|
+
target = realpathSync4(
|
|
3792
3940
|
single && wanted === "" ? scope : resolve7(root, wanted)
|
|
3793
3941
|
);
|
|
3794
3942
|
} catch {
|
|
@@ -4151,7 +4299,7 @@ function registerSkillResources(server, skills) {
|
|
|
4151
4299
|
// src/extension/entry.ts
|
|
4152
4300
|
import { createHash as createHash8 } from "crypto";
|
|
4153
4301
|
import { readFileSync as readFileSync15, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
|
|
4154
|
-
import { basename as basename5, dirname as dirname11, join as
|
|
4302
|
+
import { basename as basename5, dirname as dirname11, join as join15, relative as relative8, sep as sep4 } from "path";
|
|
4155
4303
|
var MAX_RESOURCES = 512;
|
|
4156
4304
|
var MAX_SKILL_BYTES = 16 * 1024 * 1024;
|
|
4157
4305
|
var digests = /* @__PURE__ */ new Map();
|
|
@@ -4173,7 +4321,7 @@ function filesUnder(root) {
|
|
|
4173
4321
|
const dir = stack.pop();
|
|
4174
4322
|
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
4175
4323
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
4176
|
-
const full =
|
|
4324
|
+
const full = join15(dir, entry.name);
|
|
4177
4325
|
if (entry.isDirectory()) stack.push(full);
|
|
4178
4326
|
else if (entry.isFile()) found.push(full);
|
|
4179
4327
|
if (found.length > MAX_RESOURCES) return null;
|
|
@@ -4679,7 +4827,7 @@ function registerMailboxTools(server, service) {
|
|
|
4679
4827
|
|
|
4680
4828
|
// src/mcp.ts
|
|
4681
4829
|
var SERVER_NAME = "pmcp";
|
|
4682
|
-
var SERVER_VERSION = "0.
|
|
4830
|
+
var SERVER_VERSION = "0.17.0";
|
|
4683
4831
|
function registerSkillTools(server, tools) {
|
|
4684
4832
|
registerCatalogTool(server, tools);
|
|
4685
4833
|
registerFindTool(server, tools);
|
|
@@ -4700,16 +4848,16 @@ function createSkillServer(options) {
|
|
|
4700
4848
|
}
|
|
4701
4849
|
|
|
4702
4850
|
// src/validate.ts
|
|
4703
|
-
import { existsSync as
|
|
4704
|
-
import { basename as basename6, join as
|
|
4851
|
+
import { existsSync as existsSync14, readFileSync as readFileSync16, statSync as statSync9 } from "fs";
|
|
4852
|
+
import { basename as basename6, join as join16, resolve as resolve8 } from "path";
|
|
4705
4853
|
function validateSkillDir(dir) {
|
|
4706
4854
|
const root = resolve8(dir);
|
|
4707
|
-
const skillMd = basename6(root) === "SKILL.md" ? root :
|
|
4708
|
-
if (!
|
|
4855
|
+
const skillMd = basename6(root) === "SKILL.md" ? root : join16(root, "SKILL.md");
|
|
4856
|
+
if (!existsSync14(skillMd) || !statSync9(skillMd).isFile()) {
|
|
4709
4857
|
return [{ path: root, reason: "no SKILL.md", level: "error" }];
|
|
4710
4858
|
}
|
|
4711
4859
|
const front = frontmatterObject(readFileSync16(skillMd, "utf8"));
|
|
4712
|
-
const directory = basename6(skillMd === root ?
|
|
4860
|
+
const directory = basename6(skillMd === root ? join16(root, "..") : root);
|
|
4713
4861
|
return specProblems(front, directory).map((reason) => ({
|
|
4714
4862
|
path: skillMd,
|
|
4715
4863
|
reason,
|