@modootoday/pmcp 0.16.1 → 0.17.1

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/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 existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
2083
- import { join as join7 } from "path";
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"] = runtimeByTool[tool];
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"] = runtimeByTool[tool];
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 existsSync8, readFileSync as readFileSync6 } from "fs";
2496
- import { join as join6 } from "path";
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 = join6(root, file);
2527
- if (!existsSync8(absolute)) {
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 = existsSync8(published) ? published : sourceBuild;
2557
- if (!existsSync8(file)) {
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 = join6(spec.root, "pmcp.lock");
2564
- const lock = existsSync8(lockFile) ? JSON.parse(readFileSync6(lockFile, "utf8")) : {};
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 (!existsSync9(dir)) return [];
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 = join7(root, ".agents/skills");
2729
+ const dir = join8(root, ".agents/skills");
2657
2730
  return listDirs(dir).filter(
2658
- (name) => existsSync9(join7(dir, name, "SKILL.md"))
2731
+ (name) => existsSync10(join8(dir, name, "SKILL.md"))
2659
2732
  );
2660
2733
  }
2661
2734
  function agentNames(root) {
2662
- const dir = join7(root, ".agents/agents");
2735
+ const dir = join8(root, ".agents/agents");
2663
2736
  return listDirs(dir).filter(
2664
- (name) => existsSync9(join7(dir, name, "agent.md"))
2737
+ (name) => existsSync10(join8(dir, name, "agent.md"))
2665
2738
  );
2666
2739
  }
2667
2740
  function skillOutputs(spec) {
2668
- const outputs = [];
2669
- for (const name of skillNames(spec.root)) {
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 = join7(spec.root, ".agents/rules");
2699
- if (!existsSync9(dir)) return [];
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(join7(spec.root, rel), "utf8")
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(join7(spec.root, rel), "utf8")
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 existsSync11,
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 join9, posix as posix2 } from "path";
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 existsSync10,
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 join8, relative as relative5, resolve as resolve6 } from "path";
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 = join8(homedir(), ".cache/pmcp/project-backups");
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 (!existsSync10(absolute)) {
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(join8(base, "project-"));
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 = join8(destination, "files", path);
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
- join8(destination, "receipt.json"),
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 = join9(root, LOCK_FILE);
2953
- if (!existsSync11(path)) return [];
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) => existsSync11(abs) || linkOf(abs) !== null;
3038
+ var exists = (abs) => existsSync12(abs) || linkOf(abs) !== null;
2965
3039
  function readJson2(abs) {
2966
- if (!existsSync11(abs)) return {};
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 = join9(root, "file" in output ? output.file : output.path);
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 existsSync11(abs) && linkOf(abs) === null && readFileSync9(abs, "utf8") === output.content;
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 existsSync11(abs) && readFileSync9(abs, "utf8").includes(block(output.content));
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.key[0] !== "mcpServers" || owned.has(output.path)) {
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(join9(root, output.file)), output.key);
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 = join9(root, output.file);
3046
- const text2 = existsSync11(absolute) ? readFileSync9(absolute, "utf8") : "";
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 = join9(root, output.path);
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 = join9(spec.root, dir);
3077
- if (!existsSync11(abs)) continue;
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 = join9(spec.root, RULES_DIR);
3084
- if (spec.tools.has("grok") && existsSync11(rules)) {
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 = join9(root, posix2.join(posix2.dirname(rule.path), rule.target));
3112
- if (!existsSync11(abs)) continue;
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(join9(spec.root, path));
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 = join9(spec.root, output.path);
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(join9(spec.root, entry.path), { force: true, recursive: true });
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 = join9(spec.root, file);
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 (!existsSync11(abs) && Object.keys(after).length === 0) continue;
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 = join9(spec.root, file);
3280
- const before = existsSync11(abs) ? readFileSync9(abs, "utf8") : "";
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 = join9(spec.root, RULES_DIR, `${name}.md`);
3300
- const broken = !existsSync11(abs);
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
- join9(spec.root, LOCK_FILE),
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
  );
@@ -3318,10 +3405,83 @@ function project(spec, options) {
3318
3405
  }
3319
3406
 
3320
3407
  // src/project/doctor.ts
3321
- import { spawnSync as spawnSync2 } from "child_process";
3322
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
3323
- import { join as join10 } from "path";
3408
+ import { spawnSync as spawnSync3 } from "child_process";
3409
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
3410
+ import { join as join13 } 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/diagnostics/native-json.ts
3471
+ import { spawnSync as spawnSync2 } from "child_process";
3472
+ import {
3473
+ closeSync as closeSync2,
3474
+ mkdtempSync as mkdtempSync2,
3475
+ openSync as openSync2,
3476
+ readFileSync as readFileSync10,
3477
+ rmSync as rmSync3,
3478
+ statSync as statSync3
3479
+ } from "fs";
3480
+ import { tmpdir } from "os";
3481
+ import { join as join12 } from "path";
3482
+ var MAX_OUTPUT = 1 << 26;
3483
+
3484
+ // src/project/doctor.ts
3325
3485
  var missingFrom = (names, present2) => names.filter((name) => !present2(name));
3326
3486
  function verdict(tool, check, missing) {
3327
3487
  if (missing.length === 0) return { tool, check, status: "ok" };
@@ -3382,10 +3542,10 @@ function claude(spec, o) {
3382
3542
  }
3383
3543
  function codex(spec, o) {
3384
3544
  const findings = [];
3385
- const config = join10(o.home, ".codex/config.toml");
3545
+ const config = join13(o.home, ".codex/config.toml");
3386
3546
  let trusted = false;
3387
- if (existsSync12(config)) {
3388
- const projects = parse5(readFileSync10(config, "utf8")).projects ?? {};
3547
+ if (existsSync13(config)) {
3548
+ const projects = parse5(readFileSync11(config, "utf8")).projects ?? {};
3389
3549
  const root = spec.root.replace(/\/$/u, "");
3390
3550
  trusted = projects[root]?.trust_level === "trusted" || projects[`${root}/`]?.trust_level === "trusted";
3391
3551
  }
@@ -3511,8 +3671,8 @@ function grok(spec, o) {
3511
3671
  }
3512
3672
  function antigravity(spec, options) {
3513
3673
  const foreign = agentNames(spec.root).filter((name) => {
3514
- const text = readFileSync10(
3515
- join10(spec.root, ".agents/agents", name, "agent.md"),
3674
+ const text = readFileSync11(
3675
+ join13(spec.root, ".agents/agents", name, "agent.md"),
3516
3676
  "utf8"
3517
3677
  );
3518
3678
  return splitFrontmatter(text).meta["tools"] !== void 0;
@@ -3532,7 +3692,7 @@ function antigravity(spec, options) {
3532
3692
  if (serversForTool(spec, "antigravity").length > 0) {
3533
3693
  const result = options.run(
3534
3694
  "agy",
3535
- ["plugin", "validate", join10(spec.root, ANTIGRAVITY_MCP_PLUGIN)],
3695
+ ["plugin", "validate", join13(spec.root, ANTIGRAVITY_MCP_PLUGIN)],
3536
3696
  spec.root
3537
3697
  );
3538
3698
  findings.push(
@@ -3556,6 +3716,8 @@ function doctor(spec, options) {
3556
3716
  if (wanted("codex")) findings.push(...codex(spec, options));
3557
3717
  if (wanted("gemini")) findings.push(...gemini(spec, options));
3558
3718
  if (wanted("grok")) findings.push(...grok(spec, options));
3719
+ if (wanted("opencode"))
3720
+ findings.push(...opencodeDoctor(spec, options, listed));
3559
3721
  if (wanted("antigravity")) {
3560
3722
  findings.push(...antigravity(spec, options));
3561
3723
  }
@@ -3563,11 +3725,11 @@ function doctor(spec, options) {
3563
3725
  }
3564
3726
 
3565
3727
  // src/server.ts
3566
- import { readFileSync as readFileSync14 } from "fs";
3728
+ import { readFileSync as readFileSync15 } from "fs";
3567
3729
 
3568
3730
  // src/files/bundle.ts
3569
- import { readFileSync as readFileSync12, statSync as statSync4 } from "fs";
3570
- import { dirname as dirname9, join as join12 } from "path";
3731
+ import { readFileSync as readFileSync13, statSync as statSync5 } from "fs";
3732
+ import { dirname as dirname9, join as join15 } from "path";
3571
3733
 
3572
3734
  // src/mime.ts
3573
3735
  var TYPES = {
@@ -3674,8 +3836,8 @@ function classify(path, bytes) {
3674
3836
 
3675
3837
  // src/files/list.ts
3676
3838
  import { createHash as createHash7 } from "crypto";
3677
- import { readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
3678
- import { basename as basename4, join as join11, relative as relative6, sep as sep2 } from "path";
3839
+ import { readFileSync as readFileSync12, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
3840
+ import { basename as basename4, join as join14, relative as relative6, sep as sep2 } from "path";
3679
3841
 
3680
3842
  // src/files/redact.ts
3681
3843
  import { basename as basename3 } from "path";
@@ -3712,11 +3874,11 @@ function servedBytes(path, raw) {
3712
3874
  var FILE_LIST_LIMIT = 200;
3713
3875
  var sha256 = (bytes) => createHash7("sha256").update(bytes).digest("hex");
3714
3876
  function describe(path, full) {
3715
- const bytes = servedBytes(full, readFileSync11(full));
3877
+ const bytes = servedBytes(full, readFileSync12(full));
3716
3878
  return { path, bytes: bytes.length, sha256: sha256(bytes) };
3717
3879
  }
3718
3880
  function listSkillFiles(root, limit = FILE_LIST_LIMIT) {
3719
- if (statSync3(root).isFile()) {
3881
+ if (statSync4(root).isFile()) {
3720
3882
  return { files: [describe(basename4(root), root)], truncated: false };
3721
3883
  }
3722
3884
  const files = [];
@@ -3725,7 +3887,7 @@ function listSkillFiles(root, limit = FILE_LIST_LIMIT) {
3725
3887
  const dir = stack.pop();
3726
3888
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
3727
3889
  if (entry.name === "node_modules" || entry.name === ".git") continue;
3728
- const full = join11(dir, entry.name);
3890
+ const full = join14(dir, entry.name);
3729
3891
  if (entry.isDirectory()) {
3730
3892
  stack.push(full);
3731
3893
  continue;
@@ -3759,7 +3921,7 @@ function resolveSkillFileUri(uri, entries) {
3759
3921
  // src/files/bundle.ts
3760
3922
  function bundleSkill(entry) {
3761
3923
  const root = assetRoot(entry);
3762
- const base = statSync4(root).isFile() ? dirname9(root) : root;
3924
+ const base = statSync5(root).isFile() ? dirname9(root) : root;
3763
3925
  const listing2 = listSkillFiles(root);
3764
3926
  return {
3765
3927
  name: entry.name,
@@ -3767,28 +3929,28 @@ function bundleSkill(entry) {
3767
3929
  files: listing2.files.map((file) => ({
3768
3930
  ...file,
3769
3931
  uri: skillFileUri(entry, file.path),
3770
- mimeType: classify(file.path, readFileSync12(join12(base, file.path))).mimeType
3932
+ mimeType: classify(file.path, readFileSync13(join15(base, file.path))).mimeType
3771
3933
  }))
3772
3934
  };
3773
3935
  }
3774
3936
 
3775
3937
  // src/files/read.ts
3776
- import { readFileSync as readFileSync13, statSync as statSync6 } from "fs";
3938
+ import { readFileSync as readFileSync14, statSync as statSync7 } from "fs";
3777
3939
 
3778
3940
  // src/files/resolve.ts
3779
- import { realpathSync as realpathSync3, statSync as statSync5 } from "fs";
3941
+ import { realpathSync as realpathSync4, statSync as statSync6 } from "fs";
3780
3942
  import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative7, resolve as resolve7, sep as sep3 } from "path";
3781
3943
  var ownFile = (entry) => relative7(assetRoot(entry), entry.path).split(sep3).join("/");
3782
3944
  function resolveSkillFile(entry, wanted = ownFile(entry)) {
3783
3945
  if (isAbsolute6(wanted) || wanted.split(/[\\/]/u).includes("..")) {
3784
3946
  return { ok: false, error: "path_outside_skill", detail: wanted };
3785
3947
  }
3786
- const scope = realpathSync3(assetRoot(entry));
3787
- const single = statSync5(scope).isFile();
3948
+ const scope = realpathSync4(assetRoot(entry));
3949
+ const single = statSync6(scope).isFile();
3788
3950
  const root = single ? dirname10(scope) : scope;
3789
3951
  let target;
3790
3952
  try {
3791
- target = realpathSync3(
3953
+ target = realpathSync4(
3792
3954
  single && wanted === "" ? scope : resolve7(root, wanted)
3793
3955
  );
3794
3956
  } catch {
@@ -3809,7 +3971,7 @@ var READ_BUDGET_BYTES = 256 * 1024;
3809
3971
  function readSkillFile(entry, wanted, budget = READ_BUDGET_BYTES) {
3810
3972
  const resolved = resolveSkillFile(entry, wanted);
3811
3973
  if (!resolved.ok) return resolved;
3812
- const size = statSync6(resolved.target).size;
3974
+ const size = statSync7(resolved.target).size;
3813
3975
  if (size > budget) {
3814
3976
  return {
3815
3977
  ok: false,
@@ -3817,7 +3979,7 @@ function readSkillFile(entry, wanted, budget = READ_BUDGET_BYTES) {
3817
3979
  detail: `${size} bytes > ${budget}`
3818
3980
  };
3819
3981
  }
3820
- const bytes = servedBytes(resolved.target, readFileSync13(resolved.target));
3982
+ const bytes = servedBytes(resolved.target, readFileSync14(resolved.target));
3821
3983
  const { kind: kind2, mimeType } = classify(resolved.path, bytes);
3822
3984
  return {
3823
3985
  ok: true,
@@ -3993,7 +4155,7 @@ function createSkillTools(options) {
3993
4155
  describe(name) {
3994
4156
  const entry = byName(name);
3995
4157
  if (!entry) return null;
3996
- const source = readFileSync14(entry.path, "utf8");
4158
+ const source = readFileSync15(entry.path, "utf8");
3997
4159
  const listing2 = listSkillFiles(assetRoot(entry));
3998
4160
  return {
3999
4161
  name: entry.name,
@@ -4030,7 +4192,7 @@ function createSkillTools(options) {
4030
4192
  return {
4031
4193
  name: entry.name,
4032
4194
  body: readBody(
4033
- servedBytes(entry.path, readFileSync14(entry.path)).toString("utf8")
4195
+ servedBytes(entry.path, readFileSync15(entry.path)).toString("utf8")
4034
4196
  ),
4035
4197
  files: bundle.files.filter((file) => file.path !== own),
4036
4198
  filesTruncated: bundle.truncated
@@ -4099,7 +4261,7 @@ function registerSkillPrompts(server, tools) {
4099
4261
  }
4100
4262
 
4101
4263
  // src/resources.ts
4102
- import { statSync as statSync7 } from "fs";
4264
+ import { statSync as statSync8 } from "fs";
4103
4265
  import {
4104
4266
  ResourceNotFoundError,
4105
4267
  ResourceTemplate
@@ -4112,7 +4274,7 @@ function listing(entries) {
4112
4274
  name: entry.name,
4113
4275
  description: entry.description,
4114
4276
  mimeType: "text/markdown",
4115
- size: statSync7(entry.path).size
4277
+ size: statSync8(entry.path).size
4116
4278
  }));
4117
4279
  }
4118
4280
  function complete(value, entries) {
@@ -4150,17 +4312,17 @@ function registerSkillResources(server, skills) {
4150
4312
 
4151
4313
  // src/extension/entry.ts
4152
4314
  import { createHash as createHash8 } from "crypto";
4153
- import { readFileSync as readFileSync15, readdirSync as readdirSync7, statSync as statSync8 } from "fs";
4154
- import { basename as basename5, dirname as dirname11, join as join13, relative as relative8, sep as sep4 } from "path";
4315
+ import { readFileSync as readFileSync16, readdirSync as readdirSync7, statSync as statSync9 } from "fs";
4316
+ import { basename as basename5, dirname as dirname11, join as join16, relative as relative8, sep as sep4 } from "path";
4155
4317
  var MAX_RESOURCES = 512;
4156
4318
  var MAX_SKILL_BYTES = 16 * 1024 * 1024;
4157
4319
  var digests = /* @__PURE__ */ new Map();
4158
4320
  function facts(path) {
4159
- const stat = statSync8(path);
4321
+ const stat = statSync9(path);
4160
4322
  const key = `${stat.mtimeMs}:${stat.size}`;
4161
4323
  const cached = digests.get(path);
4162
4324
  if (cached?.key === key) return cached;
4163
- const bytes = servedBytes(path, readFileSync15(path));
4325
+ const bytes = servedBytes(path, readFileSync16(path));
4164
4326
  const digest = `sha256:${createHash8("sha256").update(bytes).digest("hex")}`;
4165
4327
  const fresh = { key, digest, size: bytes.length };
4166
4328
  digests.set(path, fresh);
@@ -4173,7 +4335,7 @@ function filesUnder(root) {
4173
4335
  const dir = stack.pop();
4174
4336
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
4175
4337
  if (entry.name === "node_modules" || entry.name === ".git") continue;
4176
- const full = join13(dir, entry.name);
4338
+ const full = join16(dir, entry.name);
4177
4339
  if (entry.isDirectory()) stack.push(full);
4178
4340
  else if (entry.isFile()) found.push(full);
4179
4341
  if (found.length > MAX_RESOURCES) return null;
@@ -4184,7 +4346,7 @@ function filesUnder(root) {
4184
4346
  function extensionEntry(entry) {
4185
4347
  if (kindOf(entry) !== "skill" || basename5(entry.path) !== "SKILL.md")
4186
4348
  return null;
4187
- const frontmatter = frontmatterObject(readFileSync15(entry.path, "utf8"));
4349
+ const frontmatter = frontmatterObject(readFileSync16(entry.path, "utf8"));
4188
4350
  const root = dirname11(entry.path);
4189
4351
  if (specProblems(frontmatter, basename5(root)).length > 0) return null;
4190
4352
  if (frontmatter?.["name"] !== entry.slug) return null;
@@ -4679,7 +4841,7 @@ function registerMailboxTools(server, service) {
4679
4841
 
4680
4842
  // src/mcp.ts
4681
4843
  var SERVER_NAME = "pmcp";
4682
- var SERVER_VERSION = "0.16.1";
4844
+ var SERVER_VERSION = "0.17.1";
4683
4845
  function registerSkillTools(server, tools) {
4684
4846
  registerCatalogTool(server, tools);
4685
4847
  registerFindTool(server, tools);
@@ -4700,16 +4862,16 @@ function createSkillServer(options) {
4700
4862
  }
4701
4863
 
4702
4864
  // src/validate.ts
4703
- import { existsSync as existsSync13, readFileSync as readFileSync16, statSync as statSync9 } from "fs";
4704
- import { basename as basename6, join as join14, resolve as resolve8 } from "path";
4865
+ import { existsSync as existsSync14, readFileSync as readFileSync17, statSync as statSync10 } from "fs";
4866
+ import { basename as basename6, join as join17, resolve as resolve8 } from "path";
4705
4867
  function validateSkillDir(dir) {
4706
4868
  const root = resolve8(dir);
4707
- const skillMd = basename6(root) === "SKILL.md" ? root : join14(root, "SKILL.md");
4708
- if (!existsSync13(skillMd) || !statSync9(skillMd).isFile()) {
4869
+ const skillMd = basename6(root) === "SKILL.md" ? root : join17(root, "SKILL.md");
4870
+ if (!existsSync14(skillMd) || !statSync10(skillMd).isFile()) {
4709
4871
  return [{ path: root, reason: "no SKILL.md", level: "error" }];
4710
4872
  }
4711
- const front = frontmatterObject(readFileSync16(skillMd, "utf8"));
4712
- const directory = basename6(skillMd === root ? join14(root, "..") : root);
4873
+ const front = frontmatterObject(readFileSync17(skillMd, "utf8"));
4874
+ const directory = basename6(skillMd === root ? join17(root, "..") : root);
4713
4875
  return specProblems(front, directory).map((reason) => ({
4714
4876
  path: skillMd,
4715
4877
  reason,