@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/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
  );
@@ -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 existsSync12, readFileSync as readFileSync10 } from "fs";
3323
- import { join as join10 } from "path";
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 = join10(o.home, ".codex/config.toml");
3531
+ const config = join12(o.home, ".codex/config.toml");
3386
3532
  let trusted = false;
3387
- if (existsSync12(config)) {
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
- join10(spec.root, ".agents/agents", name, "agent.md"),
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", join10(spec.root, ANTIGRAVITY_MCP_PLUGIN)],
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 join12 } from "path";
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 join11, relative as relative6, sep as sep2 } from "path";
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 = join11(dir, entry.name);
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(join12(base, file.path))).mimeType
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 realpathSync3, statSync as statSync5 } from "fs";
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 = realpathSync3(assetRoot(entry));
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 = realpathSync3(
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 join13, relative as relative8, sep as sep4 } from "path";
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 = join13(dir, entry.name);
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.16.1";
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 existsSync13, readFileSync as readFileSync16, statSync as statSync9 } from "fs";
4704
- import { basename as basename6, join as join14, resolve as resolve8 } from "path";
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 : join14(root, "SKILL.md");
4708
- if (!existsSync13(skillMd) || !statSync9(skillMd).isFile()) {
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 ? join14(root, "..") : 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,