buddy-agent-harness 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "author": {
6
6
  "name": "repobuddy"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "author": {
6
6
  "name": "repobuddy"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "author": {
6
6
  "name": "repobuddy"
package/dist/cli.mjs CHANGED
@@ -17876,6 +17876,13 @@ function transportOf(entry) {
17876
17876
  * is a string is not a timeout, and letting it into the model would report a divergence whose real
17877
17877
  * cause is a typo the finding does not name.
17878
17878
  */
17879
+ /**
17880
+ * Exported for `mcp-inventory.ts`, which reads a handful of sources this module's own `McpConfig`
17881
+ * shape cannot describe — a plugin's flat `.mcp.json`, a project entry nested inside `~/.claude.json`,
17882
+ * and hosts whose `command` is an array or an object rather than a string. Each of those normalizes
17883
+ * its raw entry into this same field shape before calling this function, so one reader still decides
17884
+ * what counts as `transport`, `args`, and the rest.
17885
+ */
17879
17886
  function serverFrom(entry) {
17880
17887
  const args = Array.isArray(entry["args"]) && entry["args"].every((item) => typeof item === "string");
17881
17888
  const transport = transportOf(entry);
package/dist/index.d.mts CHANGED
@@ -110,7 +110,7 @@ type NonstandardArtifact = {
110
110
  };
111
111
  //#endregion
112
112
  //#region src/harness-registry/harness-registry.d.ts
113
- type HarnessName = "claude-code" | "cursor" | "codex" | "copilot-cli" | "gemini-cli" | "devin-desktop" | "windsurf";
113
+ type HarnessName = "claude-code" | "cursor" | "codex" | "copilot-cli" | "gemini-cli" | "devin-desktop" | "windsurf" | "vscode" | "opencode" | "zed";
114
114
  /** The two scopes a harness reads configuration at. Their roots differ; their shape does not. */
115
115
  type HarnessScopeName = "project" | "user";
116
116
  /**
@@ -598,5 +598,83 @@ type InitializeResult = {
598
598
  };
599
599
  export declare function initializeHarnesses({ root, harnesses: preferred, copy, force }: InitializeOptions): InitializeResult;
600
600
  //#endregion
601
- export type { BridgeFinding, BridgeKind, BridgeProblem, BridgeReport, BridgeStatus, ConfigurationFault, ConfigurationProblem, DiagnoseMcpOptions, DiagnoseOptions, DiagnoseResult, DivergenceDirection, DivergenceReport, DoctorProblem, DoctorReport, Harness, HarnessName, HarnessScope, HarnessScopeName, InitializeOptions, InitializeResult, InstructionBridge, InstructionKind, InstructionProblem, InstructionReport, InstructionStatus, McpConfig, McpDirection, McpField, McpProblem, McpServer, McpTransport, Repair };
601
+ //#region src/mcp-inventory/mcp-inventory.d.ts
602
+ /**
603
+ * A public, redaction-safe inventory of configured MCP servers: what `diagnoseMcp` already knows how
604
+ * to read, reshaped for a consumer that wants a list rather than a drift diagnosis.
605
+ *
606
+ * This is a **different question** than `doctor` answers, not a wider version of it. `doctor`
607
+ * compares project-scope configuration against a golden set the user authors, and stays
608
+ * project-scope only by design — see `apps/web/src/content/docs/agent-configuration/mcp-servers.md`
609
+ * for why reading a user's home directory is a wider blast radius than diagnosis needs. An inventory
610
+ * has no golden set to compare against and nothing to diagnose; it answers "what MCP servers does
611
+ * this project, and this machine, already have configured", which is a question a consumer can only
612
+ * answer by reading both scopes. Adding user scope here changes nothing about what `doctor` reads:
613
+ * `diagnoseMcp` still touches `harness.project.mcpConfig` alone.
614
+ *
615
+ * Beyond `harnessRegistry`'s own harnesses this module also covers sources `doctor` never diagnoses
616
+ * at all and that carry no `harnessRegistry` entry: VS Code, Windsurf/Devin Desktop's legacy config
617
+ * path, OpenCode, and Zed at both scopes; Claude Code's project-local servers nested inside
618
+ * `~/.claude.json`; and the MCP servers an installed Claude Code plugin ships. None of this changes
619
+ * `selectHarnesses`, `diagnoseMcp`, or `doctor`'s project-scope-only policy — this module reads none
620
+ * of it through those functions.
621
+ *
622
+ * Every field that could carry a credential is reduced before it leaves this module, the same
623
+ * discipline `mcp-secrets.ts` documents for `doctor`'s own findings: `command` is trimmed to its
624
+ * basename, `args` drops any `--flag=value` pair `nonSecretArgs` flags, and `url` is trimmed to its
625
+ * origin and path. `env` and `headers` are not returned at all — a name alone does not say whether
626
+ * the value behind it is a literal or a reference, and the safe answer is not to carry the map.
627
+ */
628
+ /**
629
+ * The two scopes `harnessRegistry` knows, plus the two this module adds on top of it.
630
+ *
631
+ * `local` is Claude Code's project-scoped entry inside the user-level `~/.claude.json` — a server
632
+ * declared for one project only, not visible to any other. `plugin` is a server an installed Claude
633
+ * Code plugin ships; `McpServerEntry.plugin` names which one. Neither fits `project` (there is no
634
+ * project-scope file) or `user` (the entry does not apply to every project), so both get their own
635
+ * name rather than being folded into the nearer of the two and read wrong.
636
+ */
637
+ type McpServerScope = HarnessScopeName | "local" | "plugin";
638
+ /** One configured MCP server, redacted of everything that could carry a credential. */
639
+ type McpServerEntry = {
640
+ harness: HarnessName;
641
+ scope: McpServerScope;
642
+ /** The absolute path of the config file this entry was read from. */
643
+ configFile: string;
644
+ /** The name the server is declared under. */
645
+ name: string;
646
+ transport: McpTransport | undefined;
647
+ /** The command's basename, never its full path. */
648
+ command: string | undefined;
649
+ /** `args`, with any credential-bearing `--flag=value` pair dropped. */
650
+ args: readonly string[];
651
+ /** The URL's origin and path, never its query string. */
652
+ url: string | undefined;
653
+ enabled: boolean;
654
+ /** The plugin id this server came from, set only when `scope` is `'plugin'`. */
655
+ plugin?: string;
656
+ };
657
+ type ListMcpServersOptions = {
658
+ /** The project directory to read project-scope configuration from. */
659
+ projectDir: string;
660
+ /** Overrides `os.homedir()`, for a test that must not touch the real one. */
661
+ homeDir?: string;
662
+ /** Overrides `process.env`, for a test — read only for `CODEX_HOME`, `COPILOT_HOME`, and `XDG_CONFIG_HOME`. */
663
+ env?: Readonly<Record<string, string | undefined>>;
664
+ /** Overrides `os.platform()`, so the VS Code user directory is testable on every OS. */
665
+ platform?: NodeJS.Platform;
666
+ };
667
+ /**
668
+ * Every MCP server configured for a project, across every supported harness and every scope that
669
+ * harness documents — without ever surfacing a credential.
670
+ *
671
+ * Reuses `diagnoseMcp`'s own parsing (`parseTarget`, `serverFrom`), model (`McpServer`), and
672
+ * redaction (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading
673
+ * or re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one
674
+ * whose file does not parse, contributes no entries; this function reports an inventory, not a
675
+ * diagnosis, so it has nothing to say about why a file is missing or broken.
676
+ */
677
+ export declare function listMcpServers(options: ListMcpServersOptions): McpServerEntry[];
678
+ //#endregion
679
+ export type { BridgeFinding, BridgeKind, BridgeProblem, BridgeReport, BridgeStatus, ConfigurationFault, ConfigurationProblem, DiagnoseMcpOptions, DiagnoseOptions, DiagnoseResult, DivergenceDirection, DivergenceReport, DoctorProblem, DoctorReport, Harness, HarnessName, HarnessScope, HarnessScopeName, InitializeOptions, InitializeResult, InstructionBridge, InstructionKind, InstructionProblem, InstructionReport, InstructionStatus, ListMcpServersOptions, McpConfig, McpDirection, McpField, McpProblem, McpServer, McpServerEntry, McpTransport, Repair };
602
680
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/cli.ts","../src/harness-registry/instruction-bridge.ts","../src/harness-registry/mcp-config.ts","../src/harness-registry/nonstandard-artifact.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/locator.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-configuration/diagnose-configuration.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-mcp/diagnose-mcp.ts","../src/diagnose-mcp/mcp-model.ts","../src/diagnose-mcp/mcp-baseline.ts","../src/diagnose-mcp/mcp-sources.ts","../src/initialize-harnesses/init.command.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts"],"mappings":";;;;;;;;;;;;;wBAiDsB,IAAI,iBAAiB;;;;;;;;;;;;;;;KCrC/B;;;;;;EAOT;;EAEA;;;EAIA;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;KCbS;;EAEX;;;;;;EAMA;;;;;EAKA;;;;;;EAMA;;;;;;;;;;;;;;;;;KCtBW;KAgBA;;;;;EAKX;;EAEA;EACA,MAAM;;;;KC/BK;;KAUA;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA;EACX;EACA;EACA,oBAAoB;EACpB,YAAY;EACZ,uBAAuB;;KAGZ;EACX,MAAM;;EAEN,SAAS;;;;;EAKT,OAAO;;EAEP,aAAa;;;;;;;;;;;;;;;;;;;;;;qBAuBD,0BAA0B;;;;KCzE3B;;;;;;KAMA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;;;;;;;;;wBAkFO,qBACf,cACA,oBAAoB,WACpB;EACI,cAAc;EAAqB,UAAU;;;;;;;;;;;;;;;;;;;;KC5FtC;EAAa;EAAc;;KAE3B;;EAEX;;EAEA;;EAEA;;EAEA,WAAW;;;;;;;;;;KCbA;;;;;;;;;;;KAqBA;;;;;;;;;KAUA;;;;;;;;;KAoBA;KAOA;;KAOA,gBAAgB,gBAAgB,qBAAqB,qBAAqB,aAAa;;KAGvF,uBAAuB;;;;;;;;;;;;;;;;;;KAmBvB;;EAEX;;EAEA;;KAGW;EACX,SAAS;;EAET;;;;;;;EAOA,OAAO,IAAI,SAAS,cAAc;;;;;;EAMlC,YAAY,IAAI;;qBAyLJ,wBAAwB;qBA4CxB,6BAA6B;qBAuM7B,wBAAwB;qBAOxB;WACZ;WACA;;;;;;;;;;wBAmCe,kBAAkB;;;;KC/kBtB;;KAGA;;;;;;cAwBC;mBAIA;;mBAFK;EAEjB,YAAY;;;;;;EASZ,WAAW,eAAe;;;;;;;EAa1B,UAAU;;;;;;;;;EAaV,gBAAgB;;EAMhB,UAAU,gBAAgB;;;;;EAS1B,YAAY,gBAAgB,oBAAoB;;UAYxC;;;;;UAmBA;;;;KC5GG;KACA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;KAGG;EACX;;;;;EAKA,SAAS;EACT;;EAEA,QAAQ;;KAGG;EACX;EACA,WAAW;;KAGA;EACX;EACA,YAAY;;EAEZ;;KAGW;EACX,SAAS;;;;;EAKT,cAAc;EACd,YAAY;EACZ,UAAU;;;;;;;wBA2EK,kBAAkB,MAAM,WAAW,WAAgB,OAAO,kBAAkB;;;;;;;;;;;;;;;;;KC1GhF;;EAEX;EACA,SAAS;EACT;;EAEA,QAAQ;;;;KCfG;EACX;EACA,SAAS;EACT,cAAc;EACd,aAAa;;;;;EAKb;IAAY;IAAc,SAAS;IAAe;;;;;;;;;;EASlD,OAAO;;;;;;wBAOQ,kBACf,aACA,QAAQ,gBACR,gBAAe,yBACb;qBAgCU,eAAe,MAAI;;;;;;;;;;;;;;;;;;KC7CpB;EACX;EACA,KAAK;;EAEL;;wBAgCe,cAAc,MAAM,KAAK,OAAO,qBAAqB;;;;;;;;;;;;;;;;KClDzD;KAEA;EACX,YAAY;EACZ;EACA;EACA,MAAM,SAAS;EACf;EACA,UAAU,SAAS;EACnB;EACA;EACA;EACA;;;cAIY;KAaD,mBAAmB;;;;KCnCnB;;;;qBCAC;;;qBCAA,aAAa,MAAI;qBAoDjB,gBAAgB,MAAI;wBAMjB,WAAW;EAAgB,WAAW,gBAAgB;;;;;;;;;KCV1D;;;KCpDA;EACX;EACA,YAAY;EACZ;EACA,QAAQ;;KAEG;EACX;EACA,WAAW;EACX,QAAQ;EACR,QAAQ;;EAER,SAAS;;EAET;IAAc,MAAM;IAAa,YAAY;;EAC7C;EACA;;wBAGe,sBACf,MACA,WAAW,WACX,MACA,SACE,oBAAoB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/cli.ts","../src/harness-registry/instruction-bridge.ts","../src/harness-registry/mcp-config.ts","../src/harness-registry/nonstandard-artifact.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/locator.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-configuration/diagnose-configuration.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-mcp/diagnose-mcp.ts","../src/diagnose-mcp/mcp-model.ts","../src/diagnose-mcp/mcp-baseline.ts","../src/diagnose-mcp/mcp-sources.ts","../src/initialize-harnesses/init.command.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts","../src/mcp-inventory/mcp-inventory.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
3
  import { cli, command, exitCodes, z } from "clibuilder";
4
4
  import { homedir, platform } from "node:os";
5
- import { dirname, join, posix, relative, resolve, sep } from "node:path";
5
+ import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
6
6
  import { encode } from "@toon-format/toon";
7
7
  import { parse } from "smol-toml";
8
8
  import { execFileSync } from "node:child_process";
@@ -1564,6 +1564,13 @@ function transportOf(entry) {
1564
1564
  * is a string is not a timeout, and letting it into the model would report a divergence whose real
1565
1565
  * cause is a typo the finding does not name.
1566
1566
  */
1567
+ /**
1568
+ * Exported for `mcp-inventory.ts`, which reads a handful of sources this module's own `McpConfig`
1569
+ * shape cannot describe — a plugin's flat `.mcp.json`, a project entry nested inside `~/.claude.json`,
1570
+ * and hosts whose `command` is an array or an object rather than a string. Each of those normalizes
1571
+ * its raw entry into this same field shape before calling this function, so one reader still decides
1572
+ * what counts as `transport`, `args`, and the rest.
1573
+ */
1567
1574
  function serverFrom(entry) {
1568
1575
  const args = Array.isArray(entry["args"]) && entry["args"].every((item) => typeof item === "string");
1569
1576
  const transport = transportOf(entry);
@@ -1831,9 +1838,25 @@ function credentialFields(server) {
1831
1838
  ...server.url && urlHoldsCredential(server.url) ? ["url"] : []
1832
1839
  ];
1833
1840
  }
1841
+ /**
1842
+ * `args` with any `--flag=value` pair dropped whose flag names a credential and whose value is a
1843
+ * literal rather than a reference — the same test `credentialKey` and `holdsLiteral` apply to `env`
1844
+ * and `headers`, applied here to the one field that carries names and values in a single string
1845
+ * instead of a map.
1846
+ *
1847
+ * A positional argument, or a flag with no `=`, passes through unfiltered: `-y` and a package name
1848
+ * are not a place a credential's value is written, and there is no key in either to test.
1849
+ */
1850
+ function nonSecretArgs(args) {
1851
+ return (args ?? []).filter((arg) => {
1852
+ const at = arg.indexOf("=");
1853
+ if (at === -1) return true;
1854
+ return !(credentialKey(arg.slice(0, at)) && holdsLiteral(arg.slice(at + 1)));
1855
+ });
1856
+ }
1834
1857
  //#endregion
1835
1858
  //#region src/diagnose-mcp/diagnose-mcp.ts
1836
- function read(root, path) {
1859
+ function read$1(root, path) {
1837
1860
  const absolute = join(root, path);
1838
1861
  return existsSync(absolute) ? readFileSync(absolute, "utf8") : void 0;
1839
1862
  }
@@ -1884,7 +1907,7 @@ function diagnoseMcp({ root, git, cli }) {
1884
1907
  const targets = /* @__PURE__ */ new Map();
1885
1908
  const configs = [];
1886
1909
  for (const config of targetsOf(root)) {
1887
- const parsed = parseTarget(config, read(root, config.path));
1910
+ const parsed = parseTarget(config, read$1(root, config.path));
1888
1911
  if (parsed.kind === "unreadable") {
1889
1912
  add({ file: config.path }, "mcp-target-unreadable");
1890
1913
  continue;
@@ -1894,7 +1917,7 @@ function diagnoseMcp({ root, git, cli }) {
1894
1917
  targets.set(config.path, parsed.servers);
1895
1918
  configs.push(config);
1896
1919
  }
1897
- const golden = parseGoldenSet(read(root, goldenSetPath));
1920
+ const golden = parseGoldenSet(read$1(root, goldenSetPath));
1898
1921
  if (golden.kind === "unreadable") {
1899
1922
  add({
1900
1923
  file: goldenSetPath,
@@ -1906,7 +1929,7 @@ function diagnoseMcp({ root, git, cli }) {
1906
1929
  reportSecrets(goldenSetPath, golden.servers);
1907
1930
  const baseline = new McpBaseline({
1908
1931
  git,
1909
- record: parseProjectionRecord(read(root, projectionRecordPath))
1932
+ record: parseProjectionRecord(read$1(root, projectionRecordPath))
1910
1933
  });
1911
1934
  for (const config of configs) {
1912
1935
  const servers = targets.get(config.path);
@@ -2723,6 +2746,303 @@ async function run(argv) {
2723
2746
  }
2724
2747
  }
2725
2748
  //#endregion
2726
- export { activate, bridgeRepairs, buildDoctorReport, diagnoseBridges, diagnoseInstructions, diagnoseMcp, doctorCommand, doctorRepairs, doctorSkill, goldenSetPath, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, renderDoctorSkill, run };
2749
+ //#region src/mcp-inventory/mcp-inventory.ts
2750
+ /** Where a harness keeps its **user-scope** MCP configuration, as that harness documents it.
2751
+ *
2752
+ * Unlike `harnessRegistry`, this table is read only from here: it is not wired into `doctor`, and
2753
+ * adding a harness to it never changes what `doctor` reads. Codex and Copilot CLI each let their
2754
+ * whole configuration directory move via an environment variable — `CODEX_HOME` and `COPILOT_HOME`
2755
+ * — so those two resolve through `env` before falling back to the home directory; the rest do not
2756
+ * document one.
2757
+ */
2758
+ function userScopeConfig(harness, home, env) {
2759
+ switch (harness) {
2760
+ case "claude-code": return {
2761
+ file: join(home, ".claude.json"),
2762
+ config: {
2763
+ path: ".claude.json",
2764
+ key: "mcpServers",
2765
+ format: "json"
2766
+ }
2767
+ };
2768
+ case "cursor": return {
2769
+ file: join(home, ".cursor", "mcp.json"),
2770
+ config: {
2771
+ path: ".cursor/mcp.json",
2772
+ key: "mcpServers",
2773
+ format: "json"
2774
+ }
2775
+ };
2776
+ case "codex": return {
2777
+ file: join(env["CODEX_HOME"] || join(home, ".codex"), "config.toml"),
2778
+ config: {
2779
+ path: "config.toml",
2780
+ key: "mcp_servers",
2781
+ format: "toml"
2782
+ }
2783
+ };
2784
+ case "copilot-cli": return {
2785
+ file: join(env["COPILOT_HOME"] || join(home, ".copilot"), "mcp-config.json"),
2786
+ config: {
2787
+ path: "mcp-config.json",
2788
+ key: "mcpServers",
2789
+ format: "json"
2790
+ }
2791
+ };
2792
+ case "gemini-cli": return {
2793
+ file: join(home, ".gemini", "settings.json"),
2794
+ config: {
2795
+ path: ".gemini/settings.json",
2796
+ key: "mcpServers",
2797
+ format: "json",
2798
+ shared: true
2799
+ }
2800
+ };
2801
+ default: return;
2802
+ }
2803
+ }
2804
+ function read(file) {
2805
+ return existsSync(file) ? readFileSync(file, "utf8") : void 0;
2806
+ }
2807
+ /** A URL trimmed to its origin and path, dropping any query string a token could ride in on. */
2808
+ function safeUrl(value) {
2809
+ if (!value) return void 0;
2810
+ try {
2811
+ const url = new URL(value);
2812
+ return `${url.origin}${url.pathname}`;
2813
+ } catch {
2814
+ return;
2815
+ }
2816
+ }
2817
+ function toEntry(harness, scope, file, name, server, plugin) {
2818
+ return {
2819
+ harness,
2820
+ scope,
2821
+ configFile: file,
2822
+ name,
2823
+ transport: server.transport,
2824
+ command: server.command ? basename(server.command) : void 0,
2825
+ args: nonSecretArgs(server.args),
2826
+ url: safeUrl(server.url),
2827
+ enabled: server.enabled !== false,
2828
+ ...plugin ? { plugin } : {}
2829
+ };
2830
+ }
2831
+ function entriesFrom(harness, scope, file, config) {
2832
+ const parsed = parseTarget(config, read(file));
2833
+ if (parsed.kind !== "servers") return [];
2834
+ return [...parsed.servers].map(([name, server]) => toEntry(harness, scope, file, name, server));
2835
+ }
2836
+ /**
2837
+ * A raw entry reshaped so `serverFrom` recognizes fields the registry-driven harnesses never write:
2838
+ * OpenCode's `command` as an array (its first element is the command, the rest are args), Zed's
2839
+ * `command` as `{ path, args }`, and Windsurf's `serverUrl` as an alias for `url`. A field none of
2840
+ * those apply to is passed through unchanged.
2841
+ */
2842
+ function normalizeRawEntry(raw) {
2843
+ const command = raw["command"];
2844
+ if (Array.isArray(command)) {
2845
+ const [first, ...rest] = command;
2846
+ return {
2847
+ ...raw,
2848
+ command: first,
2849
+ args: raw["args"] ?? rest
2850
+ };
2851
+ }
2852
+ if (isRecord(command)) return {
2853
+ ...raw,
2854
+ command: command["path"],
2855
+ args: raw["args"] ?? command["args"]
2856
+ };
2857
+ if (raw["url"] === void 0 && typeof raw["serverUrl"] === "string") return {
2858
+ ...raw,
2859
+ url: raw["serverUrl"]
2860
+ };
2861
+ return raw;
2862
+ }
2863
+ /** Every server under `key` in an already-parsed JSON(C) document, normalized and converted. */
2864
+ function serversFromDocument(document, key) {
2865
+ const table = isRecord(document) ? document[key] : void 0;
2866
+ if (!isRecord(table)) return /* @__PURE__ */ new Map();
2867
+ return new Map(Object.entries(table).filter((pair) => isRecord(pair[1])).map(([name, entry]) => [name, serverFrom(normalizeRawEntry(entry))]));
2868
+ }
2869
+ function entriesFromDocument(harness, scope, file, document, key, options = {}) {
2870
+ return [...serversFromDocument(document, key)].map(([name, server]) => {
2871
+ const entry = toEntry(harness, scope, file, name, server, options.plugin);
2872
+ return options.forceDisabled ? {
2873
+ ...entry,
2874
+ enabled: false
2875
+ } : entry;
2876
+ });
2877
+ }
2878
+ /** Where VS Code keeps its user-scope settings, per platform — mirrors what VS Code itself documents. */
2879
+ function vscodeUserDir(env, home, plat) {
2880
+ if (plat === "win32") return join(env["APPDATA"] || join(home, "AppData", "Roaming"), "Code", "User");
2881
+ if (plat === "darwin") return join(home, "Library", "Application Support", "Code", "User");
2882
+ return join(env["XDG_CONFIG_HOME"] || join(home, ".config"), "Code", "User");
2883
+ }
2884
+ /**
2885
+ * Sources `harnessRegistry` carries no entry for at all, so they cannot be read through
2886
+ * `userScopeConfig` or a harness's `project.mcpConfig`. Each of these harnesses is either not one
2887
+ * `doctor` diagnoses (VS Code, OpenCode, Zed have no `HarnessScope.mcpConfig`) or is diagnosed under
2888
+ * a different name than the one its own vendor uses for this file: Windsurf was rebranded to Devin
2889
+ * Desktop (`.research/agentic-configuration-standards/evidence.md`, E-WS-02), and
2890
+ * `~/.codeium/windsurf/mcp_config.json` is that product's legacy config path, read here under
2891
+ * `devin-desktop` rather than under a separate `windsurf` MCP source. This is a deliberate departure
2892
+ * from `repobuddy`'s scanner, which has no such mapping and reports the file under a standalone
2893
+ * `windsurf` harness name.
2894
+ */
2895
+ function extraSources(projectDir, home, env, plat) {
2896
+ const xdg = env["XDG_CONFIG_HOME"] || join(home, ".config");
2897
+ return [
2898
+ {
2899
+ harness: "vscode",
2900
+ scope: "user",
2901
+ file: join(vscodeUserDir(env, home, plat), "mcp.json"),
2902
+ key: "servers"
2903
+ },
2904
+ {
2905
+ harness: "vscode",
2906
+ scope: "project",
2907
+ file: join(projectDir, ".vscode", "mcp.json"),
2908
+ key: "servers"
2909
+ },
2910
+ {
2911
+ harness: "devin-desktop",
2912
+ scope: "user",
2913
+ file: join(home, ".codeium", "windsurf", "mcp_config.json"),
2914
+ key: "mcpServers"
2915
+ },
2916
+ {
2917
+ harness: "opencode",
2918
+ scope: "user",
2919
+ file: join(xdg, "opencode", "opencode.json"),
2920
+ key: "mcp"
2921
+ },
2922
+ {
2923
+ harness: "opencode",
2924
+ scope: "project",
2925
+ file: join(projectDir, "opencode.json"),
2926
+ key: "mcp"
2927
+ },
2928
+ {
2929
+ harness: "zed",
2930
+ scope: "user",
2931
+ file: join(xdg, "zed", "settings.json"),
2932
+ key: "context_servers"
2933
+ },
2934
+ {
2935
+ harness: "zed",
2936
+ scope: "project",
2937
+ file: join(projectDir, ".zed", "settings.json"),
2938
+ key: "context_servers"
2939
+ }
2940
+ ];
2941
+ }
2942
+ /**
2943
+ * Claude Code's project-local servers: `~/.claude.json`'s `projects["<absolute project dir>"]
2944
+ * .mcpServers`, a server declared for this one project rather than every project the way the file's
2945
+ * top-level `mcpServers` (read as `'user'` scope, above) is.
2946
+ */
2947
+ function claudeLocalEntries(home, projectDir) {
2948
+ const file = join(home, ".claude.json");
2949
+ const text = read(file);
2950
+ if (text === void 0) return [];
2951
+ const document = parseJsonWithComments(text);
2952
+ const projects = isRecord(document) ? document["projects"] : void 0;
2953
+ const project = isRecord(projects) ? projects[resolve(projectDir)] : void 0;
2954
+ if (!isRecord(project)) return [];
2955
+ return entriesFromDocument("claude-code", "local", file, project, "mcpServers");
2956
+ }
2957
+ /** `enabledPlugins`, merged from global settings up through project-local settings, last write wins. */
2958
+ function enabledPlugins(home, projectDir) {
2959
+ const enabled = {};
2960
+ for (const file of [
2961
+ join(home, ".claude", "settings.json"),
2962
+ join(projectDir, ".claude", "settings.json"),
2963
+ join(projectDir, ".claude", "settings.local.json")
2964
+ ]) {
2965
+ const text = read(file);
2966
+ if (text === void 0) continue;
2967
+ const document = parseJsonWithComments(text);
2968
+ const table = isRecord(document) ? document["enabledPlugins"] : void 0;
2969
+ if (!isRecord(table)) continue;
2970
+ for (const [id, value] of Object.entries(table)) if (typeof value === "boolean") enabled[id] = value;
2971
+ }
2972
+ return enabled;
2973
+ }
2974
+ /**
2975
+ * MCP servers shipped by installed Claude Code plugins, with `enabled` reflecting whether the plugin
2976
+ * itself is enabled for this project — a plugin not set to `true` in the merged `enabledPlugins` is
2977
+ * disabled, and every server it ships is reported disabled regardless of that server's own field. An
2978
+ * install whose `projectPath` names a different project contributes nothing: that install is not
2979
+ * this project's.
2980
+ */
2981
+ function pluginEntries(home, projectDir) {
2982
+ const text = read(join(home, ".claude", "plugins", "installed_plugins.json"));
2983
+ if (text === void 0) return [];
2984
+ const document = parseJsonWithComments(text);
2985
+ const installed = isRecord(document) ? document["plugins"] : void 0;
2986
+ if (!isRecord(installed)) return [];
2987
+ const enabled = enabledPlugins(home, projectDir);
2988
+ const absoluteProject = resolve(projectDir);
2989
+ const entries = [];
2990
+ for (const [id, list] of Object.entries(installed)) {
2991
+ if (!Array.isArray(list)) continue;
2992
+ for (const install of list) {
2993
+ if (!isRecord(install)) continue;
2994
+ const projectPath = install["projectPath"];
2995
+ if (typeof projectPath === "string" && resolve(projectPath) !== absoluteProject) continue;
2996
+ const installPath = install["installPath"];
2997
+ if (typeof installPath !== "string") continue;
2998
+ const mcpFile = join(installPath, ".mcp.json");
2999
+ const mcpText = read(mcpFile);
3000
+ if (mcpText === void 0) continue;
3001
+ const mcpDocument = parseJsonWithComments(mcpText);
3002
+ if (!isRecord(mcpDocument)) continue;
3003
+ const document2 = isRecord(mcpDocument["mcpServers"]) ? mcpDocument : { mcpServers: mcpDocument };
3004
+ entries.push(...entriesFromDocument("claude-code", "plugin", mcpFile, document2, "mcpServers", {
3005
+ plugin: id,
3006
+ forceDisabled: enabled[id] !== true
3007
+ }));
3008
+ }
3009
+ }
3010
+ return entries;
3011
+ }
3012
+ /**
3013
+ * Every MCP server configured for a project, across every supported harness and every scope that
3014
+ * harness documents — without ever surfacing a credential.
3015
+ *
3016
+ * Reuses `diagnoseMcp`'s own parsing (`parseTarget`, `serverFrom`), model (`McpServer`), and
3017
+ * redaction (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading
3018
+ * or re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one
3019
+ * whose file does not parse, contributes no entries; this function reports an inventory, not a
3020
+ * diagnosis, so it has nothing to say about why a file is missing or broken.
3021
+ */
3022
+ function listMcpServers(options) {
3023
+ const home = options.homeDir ?? homedir();
3024
+ const env = options.env ?? process.env;
3025
+ const plat = options.platform ?? platform();
3026
+ const entries = [];
3027
+ for (const harness of harnessRegistry) {
3028
+ if (harness.deprecated) continue;
3029
+ const project = harness.project.mcpConfig;
3030
+ if (project) entries.push(...entriesFrom(harness.name, "project", join(options.projectDir, project.path), project));
3031
+ const user = userScopeConfig(harness.name, home, env);
3032
+ if (user) entries.push(...entriesFrom(harness.name, "user", user.file, user.config));
3033
+ }
3034
+ entries.push(...claudeLocalEntries(home, options.projectDir));
3035
+ entries.push(...pluginEntries(home, options.projectDir));
3036
+ for (const source of extraSources(options.projectDir, home, env, plat)) {
3037
+ const text = read(source.file);
3038
+ if (text === void 0) continue;
3039
+ const document = parseJsonWithComments(text);
3040
+ if (document === void 0) continue;
3041
+ entries.push(...entriesFromDocument(source.harness, source.scope, source.file, document, source.key));
3042
+ }
3043
+ return entries;
3044
+ }
3045
+ //#endregion
3046
+ export { activate, bridgeRepairs, buildDoctorReport, diagnoseBridges, diagnoseInstructions, diagnoseMcp, doctorCommand, doctorRepairs, doctorSkill, goldenSetPath, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, listMcpServers, renderDoctorSkill, run };
2727
3047
 
2728
3048
  //# sourceMappingURL=index.mjs.map