@gitkraken/agent-tools 0.1.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-10-12
10
+
11
+ ### Added
12
+
13
+ - OpenCode MCP layout selection (core-tools#164). `McpInstallRequest.openCode` and `generateConfigEntry`/`clientJSONPath` options take an `OpenCodeMcpPolicy`: `{ mode: 'authoritative', layout: 'v1' | 'v2' }` writes and prunes the other layout, `{ mode: 'infer-existing' }` follows the layout the config already uses (v1 when both exist, the entry OpenCode launches) and never prunes. Omitting it keeps the 0.1.1 behavior (v2, v1 pruned). `inspectMcpInstall` results and `installMcp` results report the selected `openCodeLayout`.
14
+ - `inspectMcpInstall` reports `missing` with the new reason `layout-unknown` when `infer-existing` finds no GitKraken entry in either OpenCode layout (including a missing or blank file), the cases where `installMcp` would throw `OpenCodeMcpLayoutUnknownError`. `McpInstallStatus.openCodeDuplicateLocation` is the JSON pointer of the GitKraken entry in the OpenCode layout that was not compared, present whenever both layouts hold one; it is reported only and never pruned by `infer-existing`.
15
+ - `OpenCodeMcpLayoutUnknownError` for `infer-existing` with no GitKraken entry to follow, or in a read-free helper; `OpenCodeMcpCleanupError`, carrying the committed `result`, when the selected entry was written but removing the other layout's entry failed. Cancellation (`AbortError`) is rethrown as-is.
16
+
17
+ ### Changed
18
+
19
+ - OpenCode migration is two writes: the selected entry first with the other layout retained, then the removal, after re-reading to confirm the file is unchanged. A v2 write refuses a flat server named `servers` and a non-object `mcp` or `mcp.servers`.
20
+ - `inspectMcpInstall` reports a prunable OpenCode duplicate with the selected entry and location (previously the duplicate's), and an entry only in the other layout as `legacy-location` in either direction.
21
+ - `inspectMcpInstall` reports an OpenCode config that `installMcp` refuses (a non-object root, `mcp` or `mcp.servers`, or a flat server named `servers` under a v2 policy) as `missing` with the new reason `unsafe-config`, instead of `no-entry`.
22
+ - Safe OpenCode migration needs the host `FileSystemPort.writeFile` to leave the previous file intact when it rejects. Custom fields inside a replaced GitKraken entry are not carried across layouts.
23
+
24
+ ### Fixed
25
+
26
+ - `uninstallMcp` and `getClientInstallation` no longer treat a `GitKraken` key on a flat OpenCode server named `servers` as a v2 entry, so uninstall leaves it intact.
27
+
9
28
  ## [0.1.1] - 2026-10-02
10
29
 
11
30
  ### Added
package/README.md CHANGED
@@ -33,7 +33,7 @@ const config: AgentHostConfig = {
33
33
  },
34
34
  fs: myFileSystemPort, // @gitkraken/shared-tools FileSystemPort
35
35
  resolveExecutable: (command) => myPathLookup(command), // optional: a client's binary on PATH
36
- openCodeMajor: 2, // optional: the installed OpenCode major version (0/undefined = unknown)
36
+ openCodeMajor: 2, // optional: the installed OpenCode major version (0/undefined = unknown); does not select the MCP layout
37
37
  };
38
38
  ```
39
39
 
@@ -66,7 +66,31 @@ const removal = await uninstallMcp({ client: 'vscode' }, config);
66
66
 
67
67
  `getMcpCliInstallCommand` returns `undefined` when a shortcut cannot do the job: the client has none, a `configPath` is set, the client is Cursor (its config file is always written directly), or Codex's config still holds the legacy `['mcp_servers.GitKraken']` table. `installMcp` and `uninstallMcp` throw `unsupported client` for an unknown id (`installMcp` also for the uninstall-only `gemini`), and throw without writing when a config file does not parse (`inspectMcpInstall` reports `invalid-config` instead). A leading UTF-8 BOM is accepted and kept.
68
68
 
69
- Re-running `installMcp` when the entry is already in place writes nothing and reports `change: 'unchanged'`. `inspectMcpInstall` says why an entry is not current (`reason`, and the drifting `key`) and exposes the on-disk `entry`, so a host can carry user-added args forward before reinstalling. OpenCode entries are written in the v2 shape (`/mcp/servers/GitKraken`); a v1 entry is pruned on install and reported in `removedLocations`.
69
+ Re-running `installMcp` when the entry is already in place writes nothing and reports `change: 'unchanged'`. `inspectMcpInstall` says why an entry is not current (`reason`, and the drifting `key`) and exposes the on-disk `entry`, so a host can carry user-added args forward before reinstalling.
70
+
71
+ ### OpenCode layouts
72
+
73
+ OpenCode reads MCP servers from `/mcp/<name>` (v1) or `/mcp/servers/<name>` (v2). Omitting `openCode` writes v2 and prunes a v1 entry, as in 0.1.1. A host that probed the installed OpenCode should pass a policy on every OpenCode call (inspect before and after repair, and install). The library never runs the probe or compares versions:
74
+
75
+ ```ts
76
+ // Verified earlier-v1 OpenCode -> v1; verified >= 1.18.24, or a v2 client -> v2.
77
+ const policy = { mode: 'authoritative', layout: 'v1' } as const;
78
+ // Probe failed or unparseable: follow what the config already uses, never prune.
79
+ const unknown = { mode: 'infer-existing' } as const;
80
+
81
+ await installMcp({ ...request, openCode: policy }, config);
82
+ generateConfigEntry('opencode', '/usr/local/bin/gk', ['mcp'], { openCode: policy });
83
+ // { type: 'local', command: ['/usr/local/bin/gk', 'mcp'], enabled: true } (v2 uses `disabled: false`)
84
+ clientJSONPath('opencode', { openCode: policy }); // '/mcp/GitKraken'
85
+ ```
86
+
87
+ - `authoritative` writes the selected layout and removes the other layout's GitKraken entry. `infer-existing` follows the layout the config already uses and repairs that entry in place; with both layouts present it selects v1, because OpenCode's compatibility reader retains it over the native entry. It leaves the duplicate alone. It never authorizes pruning, even if you copy the reported `openCodeLayout` elsewhere; pass `infer-existing` unchanged through both inspections and the install.
88
+ - `infer-existing` throws `OpenCodeMcpLayoutUnknownError` when there is no entry to follow, and in the read-free helpers (`generateConfigEntry`, `clientJSONPath`), which cannot read the config. To generate an entry without a probe result, choose a documented fallback yourself, e.g. `{ mode: 'authoritative', layout: 'v1' }`; generating performs no write or cleanup. A non-object GitKraken entry is treated as corruption and refused, not skipped. A malformed policy throws `TypeError`.
89
+ - `inspectMcpInstall` compares the selected layout; an entry found only in the other layout is `drifted` / `legacy-location` with that entry exposed for argv repair, and a current entry with a prunable duplicate is `drifted` / `legacy-location` with the selected entry. With `infer-existing` the compared entry is the v1 one when both layouts are present. `openCodeDuplicateLocation` reports the pointer of the other layout's entry whenever both hold one, in either mode; it is never pruned by `infer-existing`. `infer-existing` with no entry in either layout (including a missing or blank file) is `missing` / `layout-unknown`, the case where `installMcp` would throw `OpenCodeMcpLayoutUnknownError`. Argv merging stays in the host: re-run `inspectMcpInstall` with the effective argv before `installMcp`.
90
+ - A migration writes the selected entry first and the removal second. If the removal fails (or the file changed in between), `installMcp` throws `OpenCodeMcpCleanupError`; `error.result` describes what was committed, with no `removedLocations`. This is not success, since an older reader can still reject a leftover native `servers` object. Run the install again to finish. `AbortError` is rethrown unchanged.
91
+ - Refused without writing: a non-object root, `mcp` or `mcp.servers`; a v2 write where `mcp.servers` is a flat server named `servers` (an object whose `type` or `enabled` is a scalar, array or null). `inspectMcpInstall` reports these same files as `missing` / `unsafe-config`, so a host can tell before installing. Custom fields inside a replaced GitKraken entry are not carried across layouts.
92
+ - Safe migration needs your `FileSystemPort.writeFile` to leave the previous file intact when it rejects (write a temp file, then rename). Your `readFile` must also return exactly the bytes `writeFile` was given (no newline normalization, appended trailing newline, or BOM changes): the migration compares the re-read file against what it wrote before the destructive second write, and a port that does not round-trip turns the migration into `OpenCodeMcpCleanupError` (retrying completes it). The library cannot check this and holds no lock against concurrent writers.
93
+ - `uninstallMcp` ignores the policy and removes both pointers, except that a `GitKraken` key on a flat server named `servers` is not a v2 entry and is left alone; `configPath` confines every read and write to that file.
70
94
 
71
95
  Codex TOML edits preserve surrounding text and comments by splicing table blocks. When an entry needs to change but its valid TOML representation cannot be edited safely this way (for example, a drifted inline or dotted `mcp_servers.GitKraken` assignment), install or uninstall throws an error naming the config path and leaves the file untouched. A semantically current entry remains a no-op even when it uses an unusual representation.
72
96
 
package/dist/config.d.ts CHANGED
@@ -50,7 +50,8 @@ interface AgentHostConfig {
50
50
  * The installed OpenCode major version, or `undefined`/`0` when unknown —
51
51
  * the same sentinel Go's `OpenCodeMajor()` returns. Go probes `opencode
52
52
  * --version`; this library never spawns a process, so the host resolves
53
- * the probe (or reads its own cache) and injects the result.
53
+ * the probe (or reads its own cache) and injects the result. It does not
54
+ * select an MCP layout; pass `McpInstallRequest.openCode` for that.
54
55
  */
55
56
  openCodeMajor?: number;
56
57
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { AgentHostConfig, AgentHostPaths, AgentOrigin, joinPath } from "./config.js";
2
2
  import { HookHost, eventManifest } from "./hooks/events.js";
3
+ import { McpConfigEntry, generateConfigEntry } from "./mcp/config-entry.js";
4
+ import { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp } from "./mcp/installer.js";
5
+ import { McpLayoutOptions, OpenCodeMcpCleanupError, OpenCodeMcpLayout, OpenCodeMcpLayoutUnknownError, OpenCodeMcpPolicy } from "./mcp/opencode-layout.js";
3
6
  import { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds } from "./mcp/clients.js";
4
7
  import { Agent, HookCapability, listAgents, mcpNamesForHook, resolveHookClient } from "./agents.js";
5
8
  import { FileChange, FileChangeKind } from "./file-change.js";
@@ -7,11 +10,9 @@ import { HookInstallResult, HookUninstallResult } from "./hooks/results.js";
7
10
  import { HookConfigError, HookConfigErrorCode } from "./hooks/errors.js";
8
11
  import { HookWriteRequest, installJsonHooks, uninstallJsonHooks } from "./hooks/writers.js";
9
12
  import { InstallHooksRequest, installHooks, uninstallHooks } from "./hooks/install.js";
10
- import { McpConfigEntry, generateConfigEntry } from "./mcp/config-entry.js";
11
- import { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp } from "./mcp/installer.js";
12
13
  import { getMcpCliInstallCommand } from "./mcp/cli-install-command.js";
13
14
  import { McpUninstallFileResult, McpUninstallRequest, McpUninstallResult, uninstallMcp } from "./mcp/uninstaller.js";
14
15
  import { SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, SkillManifest } from "./skills/manifest.js";
15
16
  import { SkillAgent, allSkillAgents, isSkillAgentSupported, skillDir } from "./skills/skill-dirs.js";
16
17
  import { SkillConflictError, SkillFiles, SkillInstallAction, SkillInstallOptions, SkillInstallResult, SkillOperation, SkillReason, SkillState, SkillStatus, SkillUninstallAction, SkillUninstallOptions, SkillUninstallResult, SkillUnsupportedManifestError, inspectSkillStatus, installSkill, uninstallSkill } from "./skills/install.js";
17
- export { type Agent, type AgentHostConfig, type AgentHostPaths, type AgentOrigin, type Client, type ClientDetection, type ClientDetectionSource, type ClientInstallation, type ClientNotDetectedReason, type ClientType, type FileChange, type FileChangeKind, type HookCapability, HookConfigError, type HookConfigErrorCode, type HookHost, type HookInstallResult, type HookUninstallResult, type HookWriteRequest, type InstallHooksRequest, type McpConfigEntry, type McpInstallRequest, type McpInstallResult, type McpInstallState, type McpInstallStateReason, type McpInstallStatus, type McpUninstallFileResult, type McpUninstallRequest, type McpUninstallResult, SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, type SkillAgent, SkillConflictError, type SkillFiles, type SkillInstallAction, type SkillInstallOptions, type SkillInstallResult, type SkillManifest, type SkillOperation, type SkillReason, type SkillState, type SkillStatus, type SkillUninstallAction, type SkillUninstallOptions, type SkillUninstallResult, SkillUnsupportedManifestError, allClients, allSkillAgents, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, eventManifest, generateConfigEntry, getClientInstallation, getMcpCliInstallCommand, hostIdentity, hostOf, inspectMcpInstall, inspectSkillStatus, installHooks, installJsonHooks, installMcp, installSkill, isInstallable, isLegacy, isSkillAgentSupported, isSupported, joinPath, legacyClientIds, listAgents, mcpNamesForHook, resolveHookClient, skillDir, uninstallHooks, uninstallJsonHooks, uninstallMcp, uninstallSkill };
18
+ export { type Agent, type AgentHostConfig, type AgentHostPaths, type AgentOrigin, type Client, type ClientDetection, type ClientDetectionSource, type ClientInstallation, type ClientNotDetectedReason, type ClientType, type FileChange, type FileChangeKind, type HookCapability, HookConfigError, type HookConfigErrorCode, type HookHost, type HookInstallResult, type HookUninstallResult, type HookWriteRequest, type InstallHooksRequest, type McpConfigEntry, type McpInstallRequest, type McpInstallResult, type McpInstallState, type McpInstallStateReason, type McpInstallStatus, type McpLayoutOptions, type McpUninstallFileResult, type McpUninstallRequest, type McpUninstallResult, OpenCodeMcpCleanupError, type OpenCodeMcpLayout, OpenCodeMcpLayoutUnknownError, type OpenCodeMcpPolicy, SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, type SkillAgent, SkillConflictError, type SkillFiles, type SkillInstallAction, type SkillInstallOptions, type SkillInstallResult, type SkillManifest, type SkillOperation, type SkillReason, type SkillState, type SkillStatus, type SkillUninstallAction, type SkillUninstallOptions, type SkillUninstallResult, SkillUnsupportedManifestError, allClients, allSkillAgents, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, eventManifest, generateConfigEntry, getClientInstallation, getMcpCliInstallCommand, hostIdentity, hostOf, inspectMcpInstall, inspectSkillStatus, installHooks, installJsonHooks, installMcp, installSkill, isInstallable, isLegacy, isSkillAgentSupported, isSupported, joinPath, legacyClientIds, listAgents, mcpNamesForHook, resolveHookClient, skillDir, uninstallHooks, uninstallJsonHooks, uninstallMcp, uninstallSkill };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{joinPath as e}from"./config.js";import{allClients as t,antigravityLegacyPath as n,clientCommand as r,clientConfigPath as i,clientConfigPaths as a,clientDisplayName as o,clientExecutablePath as s,clientJSONPath as c,clientType as l,clientsForHost as u,detectClient as d,getClientInstallation as f,hostIdentity as p,hostOf as m,isInstallable as h,isLegacy as g,isSupported as _,legacyClientIds as v}from"./mcp/clients.js";import{listAgents as y,mcpNamesForHook as b,resolveHookClient as x}from"./agents.js";import{HookConfigError as S}from"./hooks/errors.js";import{eventManifest as C}from"./hooks/events.js";import{installJsonHooks as w,uninstallJsonHooks as T}from"./hooks/writers.js";import{installHooks as E,uninstallHooks as D}from"./hooks/install.js";import{generateConfigEntry as O}from"./mcp/config-entry.js";import{inspectMcpInstall as k,installMcp as A}from"./mcp/installer.js";import{getMcpCliInstallCommand as j}from"./mcp/cli-install-command.js";import{uninstallMcp as M}from"./mcp/uninstaller.js";import{SKILL_MANIFEST_FILE_NAME as N,SKILL_MANIFEST_SCHEMA_VERSION as P}from"./skills/manifest.js";import{allSkillAgents as F,isSkillAgentSupported as I,skillDir as L}from"./skills/skill-dirs.js";import{SkillConflictError as R,SkillUnsupportedManifestError as z,inspectSkillStatus as B,installSkill as V,uninstallSkill as H}from"./skills/install.js";export{S as HookConfigError,N as SKILL_MANIFEST_FILE_NAME,P as SKILL_MANIFEST_SCHEMA_VERSION,R as SkillConflictError,z as SkillUnsupportedManifestError,t as allClients,F as allSkillAgents,n as antigravityLegacyPath,r as clientCommand,i as clientConfigPath,a as clientConfigPaths,o as clientDisplayName,s as clientExecutablePath,c as clientJSONPath,l as clientType,u as clientsForHost,d as detectClient,C as eventManifest,O as generateConfigEntry,f as getClientInstallation,j as getMcpCliInstallCommand,p as hostIdentity,m as hostOf,k as inspectMcpInstall,B as inspectSkillStatus,E as installHooks,w as installJsonHooks,A as installMcp,V as installSkill,h as isInstallable,g as isLegacy,I as isSkillAgentSupported,_ as isSupported,e as joinPath,v as legacyClientIds,y as listAgents,b as mcpNamesForHook,x as resolveHookClient,L as skillDir,D as uninstallHooks,T as uninstallJsonHooks,M as uninstallMcp,H as uninstallSkill};
1
+ import{joinPath as e}from"./config.js";import{OpenCodeMcpCleanupError as t,OpenCodeMcpLayoutUnknownError as n}from"./mcp/opencode-layout.js";import{allClients as r,antigravityLegacyPath as i,clientCommand as a,clientConfigPath as o,clientConfigPaths as s,clientDisplayName as c,clientExecutablePath as l,clientJSONPath as u,clientType as d,clientsForHost as f,detectClient as p,getClientInstallation as m,hostIdentity as h,hostOf as g,isInstallable as _,isLegacy as v,isSupported as y,legacyClientIds as b}from"./mcp/clients.js";import{listAgents as x,mcpNamesForHook as S,resolveHookClient as C}from"./agents.js";import{HookConfigError as w}from"./hooks/errors.js";import{eventManifest as T}from"./hooks/events.js";import{installJsonHooks as E,uninstallJsonHooks as D}from"./hooks/writers.js";import{installHooks as O,uninstallHooks as k}from"./hooks/install.js";import{generateConfigEntry as A}from"./mcp/config-entry.js";import{inspectMcpInstall as j,installMcp as M}from"./mcp/installer.js";import{getMcpCliInstallCommand as N}from"./mcp/cli-install-command.js";import{uninstallMcp as P}from"./mcp/uninstaller.js";import{SKILL_MANIFEST_FILE_NAME as F,SKILL_MANIFEST_SCHEMA_VERSION as I}from"./skills/manifest.js";import{allSkillAgents as L,isSkillAgentSupported as R,skillDir as z}from"./skills/skill-dirs.js";import{SkillConflictError as B,SkillUnsupportedManifestError as V,inspectSkillStatus as H,installSkill as U,uninstallSkill as W}from"./skills/install.js";export{w as HookConfigError,t as OpenCodeMcpCleanupError,n as OpenCodeMcpLayoutUnknownError,F as SKILL_MANIFEST_FILE_NAME,I as SKILL_MANIFEST_SCHEMA_VERSION,B as SkillConflictError,V as SkillUnsupportedManifestError,r as allClients,L as allSkillAgents,i as antigravityLegacyPath,a as clientCommand,o as clientConfigPath,s as clientConfigPaths,c as clientDisplayName,l as clientExecutablePath,u as clientJSONPath,d as clientType,f as clientsForHost,p as detectClient,T as eventManifest,A as generateConfigEntry,m as getClientInstallation,N as getMcpCliInstallCommand,h as hostIdentity,g as hostOf,j as inspectMcpInstall,H as inspectSkillStatus,O as installHooks,E as installJsonHooks,M as installMcp,U as installSkill,_ as isInstallable,v as isLegacy,R as isSkillAgentSupported,y as isSupported,e as joinPath,b as legacyClientIds,x as listAgents,S as mcpNamesForHook,C as resolveHookClient,z as skillDir,k as uninstallHooks,D as uninstallJsonHooks,P as uninstallMcp,W as uninstallSkill};
@@ -1,4 +1,5 @@
1
1
  import { AgentHostConfig } from "../config.js";
2
+ import { McpLayoutOptions, OpenCodeMcpLayout } from "./opencode-layout.js";
2
3
 
3
4
  //#region src/mcp/clients.d.ts
4
5
  /**
@@ -17,6 +18,8 @@ declare const OPEN_CODE_V1_JSON_PATH = "/mcp/GitKraken";
17
18
  declare const OPEN_CODE_V2_JSON_PATH = "/mcp/servers/GitKraken";
18
19
  /** Every location an OpenCode GitKraken entry may live at: v2 is written, v1 is only recognised and cleaned up. */
19
20
  declare const OPEN_CODE_JSON_PATHS: readonly string[];
21
+ /** The OpenCode pointers that can hold a GitKraken entry in `text`; the v2 one only counts under a native `servers` envelope, not a flat server named `servers`. */
22
+ declare function openCodeEntryPointers(text: string): string[];
20
23
  declare function isSupported(client: string): boolean;
21
24
  /** Whether `client` is retained only for uninstall cleanup (currently only `gemini`). */
22
25
  declare function isLegacy(client: Client): boolean;
@@ -35,8 +38,14 @@ declare function hostIdentity(client: Client): string;
35
38
  declare function hostOf(client: Client): Client;
36
39
  /** Every non-legacy row sharing `host`'s configuration surface, `host` itself included, in registry order. */
37
40
  declare function clientsForHost(host: Client): Client[];
38
- /** The JSON pointer (or, for Codex, dotted TOML table key) `installMcp` writes a client's GitKraken entry at. */
39
- declare function clientJSONPath(client: Client): string;
41
+ /** The JSON pointer OpenCode reads a GitKraken server from in `layout`. */
42
+ declare function openCodeJSONPath(layout: OpenCodeMcpLayout): string;
43
+ /**
44
+ * The JSON pointer (or, for Codex, dotted TOML table key) for a client's GitKraken entry.
45
+ * OpenCode's returned pointer depends on `options.openCode` (v2 when omitted); `infer-existing` throws
46
+ * {@link OpenCodeMcpLayoutUnknownError}, since the pointer then depends on the config file.
47
+ */
48
+ declare function clientJSONPath(client: Client, options?: McpLayoutOptions): string;
40
49
  /** The single config file path this client's GitKraken entry is written to. */
41
50
  declare function clientConfigPath(client: Client, config: AgentHostConfig): Promise<string | undefined>;
42
51
  /**
@@ -84,4 +93,4 @@ interface ClientInstallation {
84
93
  /** Which of the client's config files already carry a GitKraken entry. Mirrors Go's `mcp.Client.IsInstalled`. */
85
94
  declare function getClientInstallation(client: Client, config: AgentHostConfig): Promise<ClientInstallation>;
86
95
  //#endregion
87
- export { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, OPEN_CODE_JSON_PATHS, OPEN_CODE_V1_JSON_PATH, OPEN_CODE_V2_JSON_PATH, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds };
96
+ export { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, OPEN_CODE_JSON_PATHS, OPEN_CODE_V1_JSON_PATH, OPEN_CODE_V2_JSON_PATH, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds, openCodeEntryPointers, openCodeJSONPath };
@@ -1 +1 @@
1
- import{joinPath as e}from"../config.js";import{findAtPointer as t}from"./jsonc-edit.js";import{parseTomlSafe as n,tomlDottedPath as r,tomlLiteralKey as i}from"./toml-read.js";const a=`/mcp/GitKraken`,o=`/mcp/servers/GitKraken`,s=[o,a];function c(t,...n){return e(t.paths.home,...n)}function l(e){return e.platform===`linux`?e.paths.configHome:e.paths.appSupport}function u(t){return e(t.paths.configHome,`opencode`)}const d=[{id:`vscode`,displayName:`VS Code`,clientType:`gui`,command:`code`,jsonPath:`/servers/GitKraken`,pathFn:t=>e(l(t),`Code`,`User`,`mcp.json`),legacy:!1},{id:`vscode-insiders`,displayName:`VS Code Insiders`,clientType:`gui`,command:`code-insiders`,jsonPath:`/servers/GitKraken`,pathFn:t=>e(l(t),`Code - Insiders`,`User`,`mcp.json`),legacy:!1},{id:`claude`,displayName:`Claude Desktop`,clientType:`gui`,command:`claude`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>e(l(t),`Claude`,`claude_desktop_config.json`),legacy:!1},{id:`claude-cli`,displayName:`Claude Code`,clientType:`cli`,command:`claude`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>t.paths.claudeConfigDir?e(t.paths.claudeConfigDir,`.claude.json`):c(t,`.claude.json`),legacy:!1},{id:`antigravity`,displayName:`Antigravity`,clientType:`gui`,command:`antigravity`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.gemini`,`config`,`mcp_config.json`),legacy:!1,host:`antigravity`},{id:`antigravity-cli`,displayName:`Antigravity CLI`,clientType:`cli`,command:`agy`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.gemini`,`config`,`mcp_config.json`),legacy:!1,host:`antigravity`},{id:`windsurf`,displayName:`Windsurf`,clientType:`gui`,command:`windsurf`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.codeium`,`windsurf`,`mcp_config.json`),legacy:!1},{id:`cursor`,displayName:`Cursor`,clientType:`gui`,command:`cursor`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.cursor`,`mcp.json`),legacy:!1},{id:`zed`,displayName:`Zed`,clientType:`gui`,command:`zed`,jsonPath:`/context_servers/GitKraken`,pathFn:t=>t.platform===`win32`?e(t.paths.appSupport,`Zed`,`settings.json`):t.platform===`darwin`?c(t,`.config`,`zed`,`settings.json`):e(t.paths.configHome,`zed`,`settings.json`),legacy:!1},{id:`trae`,displayName:`Trae`,clientType:`gui`,command:`trae`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>e(l(t),`Trae`,`User`,`mcp.json`),legacy:!1},{id:`copilot`,displayName:`GitHub Copilot CLI`,clientType:`cli`,command:`copilot`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.copilot`,`mcp-config.json`),legacy:!1},{id:`codex`,displayName:`Codex`,clientType:`cli`,command:`codex`,jsonPath:`mcp_servers.GitKraken`,pathFn:e=>c(e,`.codex`,`config.toml`),legacy:!1},{id:`gemini`,displayName:`Gemini CLI`,clientType:`cli`,command:`gemini`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.gemini`,`settings.json`),legacy:!0},{id:`kiro`,displayName:`Kiro`,clientType:`gui`,command:`kiro`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.kiro`,`settings`,`mcp.json`),legacy:!1},{id:`jetbrains-copilot`,displayName:`JetBrains Copilot`,clientType:`gui`,jsonPath:`/servers/GitKraken`,pathFn:t=>t.platform===`win32`?e(t.paths.localAppData??e(t.paths.home,`AppData`,`Local`),`github-copilot`,`intellij`,`mcp.json`):e(t.paths.configHome,`github-copilot`,`intellij`,`mcp.json`),legacy:!1},{id:`opencode`,displayName:`OpenCode`,clientType:`cli`,command:`opencode`,jsonPath:o,pathFn:t=>e(u(t),`opencode.json`),legacy:!1}];function f(e){return d.find(t=>t.id===e)}function p(e){return f(e)!==void 0}function m(e){return f(e)?.legacy===!0}function h(e){let t=f(e);return t!==void 0&&!t.legacy}function g(e){return f(e)?.clientType??`unknown`}function _(e){return f(e)?.displayName??e}function v(e){return f(e)?.command}function y(){return d.filter(e=>!e.legacy).map(e=>e.id)}function b(){return d.filter(e=>e.legacy).map(e=>e.id)}function x(e){return(f(e)?.host??e).toLowerCase()}function S(e){return f(e)?.host??e}function C(e){return d.filter(t=>!t.legacy&&(t.host??t.id)===e).map(e=>e.id)}function w(e){return f(e)?.jsonPath??``}async function T(t){let n=u(t),r=e(n,`opencode.jsonc`);return await t.fs.readFile(r)===null?e(n,`opencode.json`):r}async function E(e,t){let n=f(e);if(n)return e===`opencode`?T(t):n.pathFn(t)}async function D(e,t){let n=await E(e,t),r=n?[n]:[];return e===`antigravity`&&r.push(c(t,`.gemini`,`antigravity`,`mcp_config.json`)),r}function O(e){return c(e,`.gemini`,`antigravity`,`mcp_config.json`)}function k(e,t){let n=v(e);if(!(!n||!t.resolveExecutable))return t.resolveExecutable(n)}function A(e,t){let n=f(e);return n?.host?C(n.host).some(n=>n!==e&&k(n,t)!==void 0):!1}async function j(e,t){if(!t)return;let n=t.lastIndexOf(`/`),r=n>0?t.slice(0,n):``;if(r)return r===e.paths.home.replace(/\/+$/,``)?{matched:await e.fs.readFile(t)!==null,source:`config-file`,path:t}:{matched:await e.fs.readDir(r)!==null,source:`config-dir`,path:r}}async function M(e,t,n,r){for(let i of t){let t=await j(n,i);if(t&&(r.push(t.path),t.matched))return{client:e,detected:!0,source:t.source,path:t.path,checkedPaths:r}}return{client:e,detected:!1,reason:`not-found`,checkedPaths:r}}async function N(e,t){let n=k(e,t),r=[];if(n!==void 0)return{client:e,detected:!0,source:`executable`,executablePath:n,checkedPaths:r};let i=f(e);if(i?.host&&i.host!==e)return{client:e,detected:!1,reason:`shared-host-row`,checkedPaths:r};if(e===`antigravity`){let n=i?.pathFn(t);return n&&!A(e,t)&&(r.push(n),await t.fs.readFile(n)!==null)?{client:e,detected:!0,source:`config-file`,path:n,checkedPaths:r}:M(e,[O(t)],t,r)}return M(e,await D(e,t),t,r)}async function P(e,t){let n=[];for(let r of await D(e,t))await F(e,r,t)&&n.push(r);return{client:e,installed:n.length>0,paths:n}}async function F(e,a,o){let c=await o.fs.readFile(a);if(c===null||c.trim()===``)return!1;if(e===`codex`){let e=n(c);if(!e)return!1;let t=w(`codex`);return i(e,t)!==void 0||r(e,t)!==void 0}return(e===`opencode`?s:[w(e)]).some(e=>t(c,e)!==void 0)}export{s as OPEN_CODE_JSON_PATHS,a as OPEN_CODE_V1_JSON_PATH,o as OPEN_CODE_V2_JSON_PATH,y as allClients,O as antigravityLegacyPath,v as clientCommand,E as clientConfigPath,D as clientConfigPaths,_ as clientDisplayName,k as clientExecutablePath,w as clientJSONPath,g as clientType,C as clientsForHost,N as detectClient,P as getClientInstallation,x as hostIdentity,S as hostOf,h as isInstallable,m as isLegacy,p as isSupported,b as legacyClientIds};
1
+ import{joinPath as e}from"../config.js";import{findAtPointer as t}from"./jsonc-edit.js";import{openCodeLayoutFor as n,openCodeNativeServers as r}from"./opencode-layout.js";import{parseTomlSafe as i,tomlDottedPath as a,tomlLiteralKey as o}from"./toml-read.js";const s=`/mcp/GitKraken`,c=`/mcp/servers/GitKraken`,l=[c,s];function u(e){return l.filter(t=>t!==`/mcp/servers/GitKraken`||r(e)!==void 0)}function d(t,...n){return e(t.paths.home,...n)}function f(e){return e.platform===`linux`?e.paths.configHome:e.paths.appSupport}function p(t){return e(t.paths.configHome,`opencode`)}const m=[{id:`vscode`,displayName:`VS Code`,clientType:`gui`,command:`code`,jsonPath:`/servers/GitKraken`,pathFn:t=>e(f(t),`Code`,`User`,`mcp.json`),legacy:!1},{id:`vscode-insiders`,displayName:`VS Code Insiders`,clientType:`gui`,command:`code-insiders`,jsonPath:`/servers/GitKraken`,pathFn:t=>e(f(t),`Code - Insiders`,`User`,`mcp.json`),legacy:!1},{id:`claude`,displayName:`Claude Desktop`,clientType:`gui`,command:`claude`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>e(f(t),`Claude`,`claude_desktop_config.json`),legacy:!1},{id:`claude-cli`,displayName:`Claude Code`,clientType:`cli`,command:`claude`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>t.paths.claudeConfigDir?e(t.paths.claudeConfigDir,`.claude.json`):d(t,`.claude.json`),legacy:!1},{id:`antigravity`,displayName:`Antigravity`,clientType:`gui`,command:`antigravity`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>d(e,`.gemini`,`config`,`mcp_config.json`),legacy:!1,host:`antigravity`},{id:`antigravity-cli`,displayName:`Antigravity CLI`,clientType:`cli`,command:`agy`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>d(e,`.gemini`,`config`,`mcp_config.json`),legacy:!1,host:`antigravity`},{id:`windsurf`,displayName:`Windsurf`,clientType:`gui`,command:`windsurf`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>d(e,`.codeium`,`windsurf`,`mcp_config.json`),legacy:!1},{id:`cursor`,displayName:`Cursor`,clientType:`gui`,command:`cursor`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>d(e,`.cursor`,`mcp.json`),legacy:!1},{id:`zed`,displayName:`Zed`,clientType:`gui`,command:`zed`,jsonPath:`/context_servers/GitKraken`,pathFn:t=>t.platform===`win32`?e(t.paths.appSupport,`Zed`,`settings.json`):t.platform===`darwin`?d(t,`.config`,`zed`,`settings.json`):e(t.paths.configHome,`zed`,`settings.json`),legacy:!1},{id:`trae`,displayName:`Trae`,clientType:`gui`,command:`trae`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>e(f(t),`Trae`,`User`,`mcp.json`),legacy:!1},{id:`copilot`,displayName:`GitHub Copilot CLI`,clientType:`cli`,command:`copilot`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>d(e,`.copilot`,`mcp-config.json`),legacy:!1},{id:`codex`,displayName:`Codex`,clientType:`cli`,command:`codex`,jsonPath:`mcp_servers.GitKraken`,pathFn:e=>d(e,`.codex`,`config.toml`),legacy:!1},{id:`gemini`,displayName:`Gemini CLI`,clientType:`cli`,command:`gemini`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>d(e,`.gemini`,`settings.json`),legacy:!0},{id:`kiro`,displayName:`Kiro`,clientType:`gui`,command:`kiro`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>d(e,`.kiro`,`settings`,`mcp.json`),legacy:!1},{id:`jetbrains-copilot`,displayName:`JetBrains Copilot`,clientType:`gui`,jsonPath:`/servers/GitKraken`,pathFn:t=>t.platform===`win32`?e(t.paths.localAppData??e(t.paths.home,`AppData`,`Local`),`github-copilot`,`intellij`,`mcp.json`):e(t.paths.configHome,`github-copilot`,`intellij`,`mcp.json`),legacy:!1},{id:`opencode`,displayName:`OpenCode`,clientType:`cli`,command:`opencode`,jsonPath:c,pathFn:t=>e(p(t),`opencode.json`),legacy:!1}];function h(e){return m.find(t=>t.id===e)}function g(e){return h(e)!==void 0}function _(e){return h(e)?.legacy===!0}function v(e){let t=h(e);return t!==void 0&&!t.legacy}function y(e){return h(e)?.clientType??`unknown`}function b(e){return h(e)?.displayName??e}function x(e){return h(e)?.command}function S(){return m.filter(e=>!e.legacy).map(e=>e.id)}function C(){return m.filter(e=>e.legacy).map(e=>e.id)}function w(e){return(h(e)?.host??e).toLowerCase()}function T(e){return h(e)?.host??e}function E(e){return m.filter(t=>!t.legacy&&(t.host??t.id)===e).map(e=>e.id)}function D(e){return e===`v1`?s:c}function O(e,t){return e===`opencode`?D(n(t)):h(e)?.jsonPath??``}async function k(t){let n=p(t),r=e(n,`opencode.jsonc`);return await t.fs.readFile(r)===null?e(n,`opencode.json`):r}async function A(e,t){let n=h(e);if(n)return e===`opencode`?k(t):n.pathFn(t)}async function j(e,t){let n=await A(e,t),r=n?[n]:[];return e===`antigravity`&&r.push(d(t,`.gemini`,`antigravity`,`mcp_config.json`)),r}function M(e){return d(e,`.gemini`,`antigravity`,`mcp_config.json`)}function N(e,t){let n=x(e);if(!(!n||!t.resolveExecutable))return t.resolveExecutable(n)}function P(e,t){let n=h(e);return n?.host?E(n.host).some(n=>n!==e&&N(n,t)!==void 0):!1}async function F(e,t){if(!t)return;let n=t.lastIndexOf(`/`),r=n>0?t.slice(0,n):``;if(r)return r===e.paths.home.replace(/\/+$/,``)?{matched:await e.fs.readFile(t)!==null,source:`config-file`,path:t}:{matched:await e.fs.readDir(r)!==null,source:`config-dir`,path:r}}async function I(e,t,n,r){for(let i of t){let t=await F(n,i);if(t&&(r.push(t.path),t.matched))return{client:e,detected:!0,source:t.source,path:t.path,checkedPaths:r}}return{client:e,detected:!1,reason:`not-found`,checkedPaths:r}}async function L(e,t){let n=N(e,t),r=[];if(n!==void 0)return{client:e,detected:!0,source:`executable`,executablePath:n,checkedPaths:r};let i=h(e);if(i?.host&&i.host!==e)return{client:e,detected:!1,reason:`shared-host-row`,checkedPaths:r};if(e===`antigravity`){let n=i?.pathFn(t);return n&&!P(e,t)&&(r.push(n),await t.fs.readFile(n)!==null)?{client:e,detected:!0,source:`config-file`,path:n,checkedPaths:r}:I(e,[M(t)],t,r)}return I(e,await j(e,t),t,r)}async function R(e,t){let n=[];for(let r of await j(e,t))await z(e,r,t)&&n.push(r);return{client:e,installed:n.length>0,paths:n}}async function z(e,n,r){let s=await r.fs.readFile(n);if(s===null||s.trim()===``)return!1;if(e===`codex`){let e=i(s);if(!e)return!1;let t=O(`codex`);return o(e,t)!==void 0||a(e,t)!==void 0}return(e===`opencode`?u(s):[O(e)]).some(e=>t(s,e)!==void 0)}export{l as OPEN_CODE_JSON_PATHS,s as OPEN_CODE_V1_JSON_PATH,c as OPEN_CODE_V2_JSON_PATH,S as allClients,M as antigravityLegacyPath,x as clientCommand,A as clientConfigPath,j as clientConfigPaths,b as clientDisplayName,N as clientExecutablePath,O as clientJSONPath,y as clientType,E as clientsForHost,L as detectClient,R as getClientInstallation,w as hostIdentity,T as hostOf,v as isInstallable,_ as isLegacy,g as isSupported,C as legacyClientIds,u as openCodeEntryPointers,D as openCodeJSONPath};
@@ -1,3 +1,4 @@
1
+ import { McpLayoutOptions } from "./opencode-layout.js";
1
2
  import { Client } from "./clients.js";
2
3
 
3
4
  //#region src/mcp/config-entry.d.ts
@@ -6,8 +7,9 @@ type McpConfigEntry = Record<string, unknown>;
6
7
  /**
7
8
  * Builds the entry a client's GitKraken MCP server config carries. Port of
8
9
  * gkcli's `installer.generateConfigs`/`installConfig`: four shapes dispatched
9
- * by client family. OpenCode always gets its v2 shape.
10
+ * by client family. OpenCode gets its v2 shape unless `options.openCode` selects v1;
11
+ * `infer-existing` throws {@link OpenCodeMcpLayoutUnknownError} because choosing needs the config file.
10
12
  */
11
- declare function generateConfigEntry(client: Client, executablePath: string, args: readonly string[]): McpConfigEntry;
13
+ declare function generateConfigEntry(client: Client, executablePath: string, args: readonly string[], options?: McpLayoutOptions): McpConfigEntry;
12
14
  //#endregion
13
15
  export { McpConfigEntry, generateConfigEntry };
@@ -1 +1 @@
1
- function e(e,t,n){switch(e){case`zed`:return{source:`custom`,enabled:!0,command:t,args:[...n]};case`copilot`:return{type:`local`,command:t,tools:[`*`],args:[...n]};case`opencode`:return{type:`local`,command:[t,...n],disabled:!1};default:return{type:`stdio`,command:t,args:[...n]}}}export{e as generateConfigEntry};
1
+ import{openCodeLayoutFor as e}from"./opencode-layout.js";function t(t,n,r,i){switch(t){case`zed`:return{source:`custom`,enabled:!0,command:n,args:[...r]};case`copilot`:return{type:`local`,command:n,tools:[`*`],args:[...r]};case`opencode`:return e(i)===`v1`?{type:`local`,command:[n,...r],enabled:!0}:{type:`local`,command:[n,...r],disabled:!1};default:return{type:`stdio`,command:n,args:[...r]}}}export{t as generateConfigEntry};
@@ -1,6 +1,7 @@
1
- import { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds } from "./clients.js";
2
1
  import { McpConfigEntry, generateConfigEntry } from "./config-entry.js";
3
2
  import { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp } from "./installer.js";
3
+ import { McpLayoutOptions, OpenCodeMcpCleanupError, OpenCodeMcpLayout, OpenCodeMcpLayoutUnknownError, OpenCodeMcpPolicy } from "./opencode-layout.js";
4
+ import { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds } from "./clients.js";
4
5
  import { getMcpCliInstallCommand } from "./cli-install-command.js";
5
6
  import { McpUninstallFileResult, McpUninstallRequest, McpUninstallResult, uninstallMcp } from "./uninstaller.js";
6
- export { type Client, type ClientDetection, type ClientDetectionSource, type ClientInstallation, type ClientNotDetectedReason, type ClientType, type McpConfigEntry, type McpInstallRequest, type McpInstallResult, type McpInstallState, type McpInstallStateReason, type McpInstallStatus, type McpUninstallFileResult, type McpUninstallRequest, type McpUninstallResult, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, generateConfigEntry, getClientInstallation, getMcpCliInstallCommand, hostIdentity, hostOf, inspectMcpInstall, installMcp, isInstallable, isLegacy, isSupported, legacyClientIds, uninstallMcp };
7
+ export { type Client, type ClientDetection, type ClientDetectionSource, type ClientInstallation, type ClientNotDetectedReason, type ClientType, type McpConfigEntry, type McpInstallRequest, type McpInstallResult, type McpInstallState, type McpInstallStateReason, type McpInstallStatus, type McpLayoutOptions, type McpUninstallFileResult, type McpUninstallRequest, type McpUninstallResult, OpenCodeMcpCleanupError, type OpenCodeMcpLayout, OpenCodeMcpLayoutUnknownError, type OpenCodeMcpPolicy, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, generateConfigEntry, getClientInstallation, getMcpCliInstallCommand, hostIdentity, hostOf, inspectMcpInstall, installMcp, isInstallable, isLegacy, isSupported, legacyClientIds, uninstallMcp };
package/dist/mcp/index.js CHANGED
@@ -1 +1 @@
1
- import{allClients as e,antigravityLegacyPath as t,clientCommand as n,clientConfigPath as r,clientConfigPaths as i,clientDisplayName as a,clientExecutablePath as o,clientJSONPath as s,clientType as c,clientsForHost as l,detectClient as u,getClientInstallation as d,hostIdentity as f,hostOf as p,isInstallable as m,isLegacy as h,isSupported as g,legacyClientIds as _}from"./clients.js";import{generateConfigEntry as v}from"./config-entry.js";import{inspectMcpInstall as y,installMcp as b}from"./installer.js";import{getMcpCliInstallCommand as x}from"./cli-install-command.js";import{uninstallMcp as S}from"./uninstaller.js";export{e as allClients,t as antigravityLegacyPath,n as clientCommand,r as clientConfigPath,i as clientConfigPaths,a as clientDisplayName,o as clientExecutablePath,s as clientJSONPath,c as clientType,l as clientsForHost,u as detectClient,v as generateConfigEntry,d as getClientInstallation,x as getMcpCliInstallCommand,f as hostIdentity,p as hostOf,y as inspectMcpInstall,b as installMcp,m as isInstallable,h as isLegacy,g as isSupported,_ as legacyClientIds,S as uninstallMcp};
1
+ import{OpenCodeMcpCleanupError as e,OpenCodeMcpLayoutUnknownError as t}from"./opencode-layout.js";import{allClients as n,antigravityLegacyPath as r,clientCommand as i,clientConfigPath as a,clientConfigPaths as o,clientDisplayName as s,clientExecutablePath as c,clientJSONPath as l,clientType as u,clientsForHost as d,detectClient as f,getClientInstallation as p,hostIdentity as m,hostOf as h,isInstallable as g,isLegacy as _,isSupported as v,legacyClientIds as y}from"./clients.js";import{generateConfigEntry as b}from"./config-entry.js";import{inspectMcpInstall as x,installMcp as S}from"./installer.js";import{getMcpCliInstallCommand as C}from"./cli-install-command.js";import{uninstallMcp as w}from"./uninstaller.js";export{e as OpenCodeMcpCleanupError,t as OpenCodeMcpLayoutUnknownError,n as allClients,r as antigravityLegacyPath,i as clientCommand,a as clientConfigPath,o as clientConfigPaths,s as clientDisplayName,c as clientExecutablePath,l as clientJSONPath,u as clientType,d as clientsForHost,f as detectClient,b as generateConfigEntry,p as getClientInstallation,C as getMcpCliInstallCommand,m as hostIdentity,h as hostOf,x as inspectMcpInstall,S as installMcp,g as isInstallable,_ as isLegacy,v as isSupported,y as legacyClientIds,w as uninstallMcp};
@@ -1,6 +1,7 @@
1
1
  import { AgentHostConfig } from "../config.js";
2
- import { Client } from "./clients.js";
3
2
  import { McpConfigEntry } from "./config-entry.js";
3
+ import { OpenCodeMcpLayout, OpenCodeMcpPolicy } from "./opencode-layout.js";
4
+ import { Client } from "./clients.js";
4
5
 
5
6
  //#region src/mcp/installer.d.ts
6
7
  interface McpInstallRequest {
@@ -13,11 +14,31 @@ interface McpInstallRequest {
13
14
  * host aim at a specific file, such as a workspace-scoped config.
14
15
  */
15
16
  configPath?: string;
17
+ /**
18
+ * Which OpenCode MCP layout to use; ignored for other clients. Omitted keeps the 0.1.1 behavior: v2 with the
19
+ * v1 entry pruned. A host that probed OpenCode should always pass a policy; see {@link OpenCodeMcpPolicy}.
20
+ */
21
+ openCode?: OpenCodeMcpPolicy;
16
22
  }
17
23
  /** How the client's config compares to what `installMcp` would write. */
18
24
  type McpInstallState = 'missing' | 'drifted' | 'current';
19
25
  /** Why {@link inspectMcpInstall} reported its state. Absent when the state is `current`. */
20
- type McpInstallStateReason = /** `missing`: the client's config file does not exist. */'file-missing' /** `missing`: the config file does not parse, so no entry can be read. */ | 'invalid-config' /** `missing`: the config has no GitKraken entry. */ | 'no-entry' /** `missing`: the GitKraken key exists but is not an object. */ | 'entry-not-object' /** `missing`: the client id is unknown or uninstall-only, so it has no config to inspect. */ | 'unsupported-client' /** `drifted`: the entry sits where an older install wrote it (Codex's literal-key table, OpenCode's v1 shape). */ | 'legacy-location' /** `drifted`: the entry lacks `command` or `args`. */ | 'pinned-key-missing' /** `drifted`: a key `installMcp` writes holds a different value. */ | 'value-mismatch';
26
+ type McpInstallStateReason = /** `missing`: the client's config file does not exist. */'file-missing' /** `missing`: the config file does not parse, so no entry can be read. */ | 'invalid-config' /** `missing`: the config has no GitKraken entry. */ | 'no-entry'
27
+ /**
28
+ * `missing`: `infer-existing` found no GitKraken entry in either OpenCode layout, so `installMcp` cannot choose
29
+ * one and would throw {@link OpenCodeMcpLayoutUnknownError}; the host must supply an authoritative layout.
30
+ */
31
+ | 'layout-unknown'
32
+ /**
33
+ * `missing`: the GitKraken key exists but is not an object. Under `infer-existing` this is also fatal to
34
+ * `installMcp`, which throws instead of overwriting it; an authoritative install overwrites it.
35
+ */
36
+ | 'entry-not-object' /** `missing`: the OpenCode config's root, `mcp` or `mcp.servers` cannot take the entry without displacing other data, so `installMcp` refuses it. */ | 'unsafe-config' /** `missing`: the client id is unknown or uninstall-only, so it has no config to inspect. */ | 'unsupported-client'
37
+ /**
38
+ * `drifted`: the entry sits where an older install wrote it (Codex's literal-key table, OpenCode's other
39
+ * layout), or the selected OpenCode entry is current but a prunable duplicate remains in the other layout.
40
+ */
41
+ | 'legacy-location' /** `drifted`: the entry lacks `command` or `args`. */ | 'pinned-key-missing' /** `drifted`: a key `installMcp` writes holds a different value. */ | 'value-mismatch';
21
42
  interface McpInstallStatus {
22
43
  client: Client;
23
44
  state: McpInstallState;
@@ -28,8 +49,22 @@ interface McpInstallStatus {
28
49
  reason?: McpInstallStateReason;
29
50
  /** The key that differs, for `pinned-key-missing` and `value-mismatch`. */
30
51
  key?: string;
31
- /** The entry on disk, when one exists. A host can read it to carry user-added args forward before reinstalling. */
52
+ /**
53
+ * The entry on disk, when one exists. A host can read it to carry user-added args forward before reinstalling.
54
+ * For a prunable OpenCode duplicate this is the selected entry, not the duplicate.
55
+ */
32
56
  entry?: Record<string, unknown>;
57
+ /**
58
+ * The OpenCode layout the inspection compared against; absent for other clients and when
59
+ * `infer-existing` found no entry (`layout-unknown`). Selection evidence only: it never authorizes pruning.
60
+ */
61
+ openCodeLayout?: OpenCodeMcpLayout;
62
+ /**
63
+ * The JSON pointer of the GitKraken entry in the OpenCode layout that was not compared, present whenever both
64
+ * layouts hold one. Reported only: `infer-existing` never prunes it. OpenCode can emit a conflict diagnostic
65
+ * when the two differ.
66
+ */
67
+ openCodeDuplicateLocation?: string;
33
68
  }
34
69
  interface McpInstallResult {
35
70
  client: Client;
@@ -41,8 +76,13 @@ interface McpInstallResult {
41
76
  location: string;
42
77
  /** The entry written, or already present when `change` is `unchanged`. */
43
78
  entry: McpConfigEntry;
44
- /** Stale locations pruned from the file, such as OpenCode's v1 pointer; empty otherwise. */
79
+ /** Stale locations pruned from the file, such as OpenCode's other-layout pointer; empty otherwise. */
45
80
  removedLocations: string[];
81
+ /**
82
+ * The OpenCode layout written or found; absent for other clients. For an inferred layout this is selection
83
+ * evidence only and must not be copied into an authoritative policy.
84
+ */
85
+ openCodeLayout?: OpenCodeMcpLayout;
46
86
  }
47
87
  /**
48
88
  * Writes the GitKraken entry in the shape each client reads. The shapes are
@@ -50,19 +90,40 @@ interface McpInstallResult {
50
90
  * the other's. A re-run that finds the entry already in place writes nothing.
51
91
  * Port of gkcli's installer.
52
92
  *
53
- * OpenCode gets its v2 shape and any v1 entry is pruned, which a v1-only
54
- * OpenCode will no longer read. Run the client's own CLI shortcut first when
55
- * it has one (see {@link getMcpCliInstallCommand}); this is the direct
93
+ * OpenCode gets the layout `request.openCode` selects, v2 when omitted. An
94
+ * `authoritative` layout is written first and the other layout's GitKraken
95
+ * entry is removed in a second write; `infer-existing` follows the layout the
96
+ * config already uses, v1 when both are present because OpenCode launches that
97
+ * entry, and prunes nothing. If that removal fails, the selected entry stays
98
+ * and {@link OpenCodeMcpCleanupError} reports it; running the install again
99
+ * finishes the cleanup. Run the client's own CLI shortcut first
100
+ * when it has one (see {@link getMcpCliInstallCommand}); this is the direct
56
101
  * config-file write.
57
102
  *
58
- * Throws, without writing, for a client that is unknown or uninstall-only and
59
- * for an existing config file that does not parse.
103
+ * Safe OpenCode migration needs the host's `FileSystemPort.writeFile` to leave
104
+ * the previous file intact when it rejects (e.g. write a temp file, then
105
+ * rename); the library cannot verify that and adds no cross-process lock.
106
+ *
107
+ * Throws, without writing, for a client that is unknown or uninstall-only, for
108
+ * an existing config file that does not parse, for an OpenCode config whose
109
+ * `mcp` containers cannot take the entry without displacing other data, and
110
+ * for `infer-existing` with no GitKraken entry ({@link OpenCodeMcpLayoutUnknownError}).
60
111
  */
61
112
  declare function installMcp(request: McpInstallRequest, config: AgentHostConfig): Promise<McpInstallResult>;
62
113
  /**
63
114
  * Reports whether the client's config already holds an entry equivalent to
64
115
  * the one `installMcp` would write, and if not, why. Mirrors Go's
65
116
  * `installer.State`.
117
+ *
118
+ * For OpenCode with an `authoritative` (or omitted) policy, the selected
119
+ * layout is compared; when only the other layout holds an entry, or the
120
+ * selected entry is current but a prunable duplicate remains, the state is
121
+ * `drifted` with reason `legacy-location`. With `infer-existing`, only the
122
+ * entry the config already uses is compared. With both layouts present that is the v1 entry, which OpenCode's
123
+ * compatibility reader retains over the native one, and the other entry is only reported in
124
+ * `openCodeDuplicateLocation`. No entry, a missing file or an empty file reports `layout-unknown`, because
125
+ * `installMcp` would throw {@link OpenCodeMcpLayoutUnknownError}; there is no `openCodeLayout` and `location`
126
+ * is the v2 pointer.
66
127
  */
67
128
  declare function inspectMcpInstall(request: McpInstallRequest, config: AgentHostConfig): Promise<McpInstallStatus>;
68
129
  //#endregion
@@ -1,3 +1,3 @@
1
- import{addAtPointer as e,findAtPointer as t,isValidJsonc as n,removeAtPointerAndEmptyParent as r}from"./jsonc-edit.js";import{assertValidToml as i,parseTomlSafe as a,tomlDottedPath as o,tomlLiteralKey as s}from"./toml-read.js";import{OPEN_CODE_V1_JSON_PATH as c,clientConfigPath as l,clientJSONPath as u,isInstallable as d}from"./clients.js";import{writeFileTracked as f}from"../file-change.js";import{generateConfigEntry as p}from"./config-entry.js";import{tomlSemanticEqual as m,withoutCodexEntry as h}from"./toml-safety.js";import{removeBlock as g,replaceOrAppendBlock as _}from"./toml-splice.js";import{stringify as v}from"smol-toml";const y=[`command`,`args`];async function b(n,i){let{client:a}=n,o=d(a)?n.configPath??await l(a,i):void 0;if(!o)throw Error(`unsupported client: ${a}`);let s=p(a,n.executablePath,n.args);if(a===`codex`)return x(n,i,o,s);let m=await i.fs.readFile(o),h=m===null||m.trim()===``?`{}`:m,g=u(a),_=t(h,g),v=m!==null&&C(_)&&k(_,s,{client:a,path:o,location:g}).state===`current`,y=v?h:e(h,g,s,o),b=[];if(a===`opencode`){let e=r(v?h:y,c,o);e.removed&&(y=e.text,b.push(c))}return{client:a,path:o,change:v&&b.length===0?`unchanged`:await f(i.fs,o,y,[],{mode:420}),format:`json`,location:g,entry:v?_:s,removedLocations:b}}async function x(e,t,n,r){let c=await t.fs.readFile(n);c!==null&&c.trim()!==``&&i(c,n);let l=u(`codex`),d=c??``,p=a(d),v=o(p,l),y=s(p,l);if(c!==null&&y===void 0&&C(v)&&k(v,r,{client:e.client,path:n,location:l}).state===`current`)return{client:e.client,path:n,change:`unchanged`,format:`toml`,location:l,entry:v,removedLocations:[]};let b,x,w=[];if(y!==void 0){if(!C(y)||v!==void 0&&!C(v))throw Error(`cannot safely migrate Codex MCP entry in ${n}: ambiguous legacy and nested tables`);let e=C(v)&&v.env!==void 0?v.env:y.env;if(e!==void 0&&!C(e))throw Error(`cannot safely migrate Codex MCP entry in ${n}: legacy env is not a table`);if(Object.entries(y).some(([e,t])=>e!==`env`&&C(t))||e&&Object.values(e).some(C))throw Error(`cannot safely migrate Codex MCP entry in ${n}: unsupported legacy subtable`);x=e;let t=g(d,l).text;b=_(v===void 0?t:g(t,l).text,l,S(l,r,e)),w.push(`["${l}"]`)}else b=_(d,l,S(l,r));let T=a(b),E=o(T,l);if(!C(E)||k(E,r,{client:e.client,path:n,location:l}).state!==`current`||s(T,l)!==void 0||y!==void 0&&!m(E.env,x)||!m(h(T),h(p)))throw Error(`cannot safely update Codex MCP entry in ${n}: unsupported TOML representation`);let D=await f(t.fs,n,b,[],{mode:384});return{client:e.client,path:n,change:D,format:`toml`,location:l,entry:E,removedLocations:w}}function S(e,t,n){let r=v(t),i=r.endsWith(`
2
- `)?r:`${r}\n`,a=n===void 0?``:v(n);return`[${e}]\n${i}${n===void 0?``:`\n[${e}.env]\n${a.endsWith(`
3
- `)?a:`${a}\n`}`}`}function C(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function w(e,t){return{...e,state:`missing`,reason:t}}function T(e,t){return{...e,state:`drifted`,reason:`legacy-location`,...C(t)&&{entry:t}}}async function E(e,r){let{client:i}=e,a=u(i),o=d(i)?e.configPath??await l(i,r):void 0;if(!o)return w({client:i,path:``,location:a},`unsupported-client`);let s={client:i,path:o,location:a},c=await r.fs.readFile(o);if(c===null)return w(s,`file-missing`);let f=p(i,e.executablePath,e.args);if(i===`codex`)return D(c,f,s);if(c.trim()===``)return w(s,`no-entry`);if(!n(c))return w(s,`invalid-config`);if(i===`opencode`)return O(c,f,s);let m=t(c,a);return m===void 0?w(s,`no-entry`):C(m)?k(m,f,s):w(s,`entry-not-object`)}function D(e,t,n){if(e.trim()===``)return w(n,`no-entry`);let r=a(e);if(!r)return w(n,`invalid-config`);let i=s(r,n.location);if(i!==void 0)return T(n,i);let c=o(r,n.location);return c===void 0?w(n,`no-entry`):C(c)?k(c,t,n):w(n,`entry-not-object`)}function O(e,n,r){let i=t(e,r.location),a=t(e,c),o={...r,location:c};if(i===void 0)return a===void 0?w(r,`no-entry`):T(o,a);if(!C(i))return w(r,`entry-not-object`);let s=k(i,n,r);return s.state===`current`&&a!==void 0?T(o,a):s}function k(e,t,n){for(let[r,i]of Object.entries(t)){if(!(r in e)){if(y.includes(r))return{...n,state:`drifted`,reason:`pinned-key-missing`,key:r,entry:e};continue}if(!A(e[r],i))return{...n,state:`drifted`,reason:`value-mismatch`,key:r,entry:e}}return{...n,state:`current`,entry:e}}function A(e,t){if(e===t)return!0;if(typeof e!=typeof t||e===null||t===null||typeof e!=`object`)return!1;if(Array.isArray(e)||Array.isArray(t))return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((e,n)=>A(e,t[n]));let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?n.every(n=>A(e[n],t[n])):!1}export{E as inspectMcpInstall,b as installMcp};
1
+ import{addAtPointer as e,assertValidJsonc as t,findAtPointer as n,isValidJsonc as r,removeAtPointerAndEmptyParent as i}from"./jsonc-edit.js";import{OpenCodeMcpCleanupError as a,OpenCodeMcpLayoutUnknownError as o,openCodeNativeServers as s,openCodeRefusal as c,resolveOpenCodePolicy as l}from"./opencode-layout.js";import{assertValidToml as u,parseTomlSafe as d,tomlDottedPath as f,tomlLiteralKey as p}from"./toml-read.js";import{clientConfigPath as m,clientJSONPath as h,isInstallable as g,openCodeJSONPath as _}from"./clients.js";import{writeFileTracked as v}from"../file-change.js";import{generateConfigEntry as y}from"./config-entry.js";import{tomlSemanticEqual as b,withoutCodexEntry as x}from"./toml-safety.js";import{removeBlock as S,replaceOrAppendBlock as C}from"./toml-splice.js";import{stringify as w}from"smol-toml";const T=[`command`,`args`];async function E(t,r){let{client:i}=t,a=g(i)?t.configPath??await m(i,r):void 0;if(!a)throw Error(`unsupported client: ${i}`);if(i===`opencode`)return D(t,r,a);let o=y(i,t.executablePath,t.args);if(i===`codex`)return P(t,r,a,o);let s=await r.fs.readFile(a),c=s===null||s.trim()===``?`{}`:s,l=h(i),u=n(c,l),d=s!==null&&I(u)&&H(u,o,{client:i,path:a,location:l}).state===`current`;return{client:i,path:a,change:d?`unchanged`:await v(r.fs,a,e(c,l,o,a),[],{mode:420}),format:`json`,location:l,entry:d?u:o,removedLocations:[]}}async function D(n,r,o){let s=l(n.openCode),u=await r.fs.readFile(o),d=u===null||u.trim()===``?`{}`:u;t(d,o);let f=c(d,k(s),o);if(f!==void 0)throw Error(f);let p=s.mode===`authoritative`?s.layout:O(d,o),m=j(p),h=_(p),g=_(m),b=y(`opencode`,n.executablePath,n.args,{openCode:{mode:`authoritative`,layout:p}}),x=M(d,p),S=u!==null&&I(x)&&H(x,b,{client:`opencode`,path:o,location:h}).state===`current`,C=s.mode===`authoritative`?M(d,m):void 0,w=S?d:e(d,h,b,o),T=C===void 0?void 0:i(w,g,o),E=S?`unchanged`:await v(r.fs,o,w,[],{mode:420}),D=e=>({client:`opencode`,path:o,change:E,format:`json`,location:h,entry:S?x:b,removedLocations:e,openCodeLayout:p});if(T===void 0||!T.removed)return D([]);try{if(await r.fs.readFile(o)!==w)throw Error(`${o} changed while the OpenCode MCP entry was being installed`);let e=await v(r.fs,o,T.text,[],{mode:420});E===`unchanged`&&(E=e)}catch(e){throw A(e)?e:new a(D([]),e)}return D([g])}function O(e,t){let n=N(e);if(!n)throw new o;if(!I(n.found))throw Error(`cannot safely update OpenCode MCP entry in ${t}: the GitKraken entry is not an object`);return n.layout}function k(e){return e.mode===`authoritative`?e.layout:void 0}function A(e){return e instanceof Error&&e.name===`AbortError`}function j(e){return e===`v1`?`v2`:`v1`}function M(e,t){if(!(t===`v2`&&s(e)===void 0))return n(e,_(t))}function N(e){for(let t of[`v1`,`v2`]){let n=M(e,t);if(n!==void 0)return{layout:t,found:n}}}async function P(e,t,n,r){let i=await t.fs.readFile(n);i!==null&&i.trim()!==``&&u(i,n);let a=h(`codex`),o=i??``,s=d(o),c=f(s,a),l=p(s,a);if(i!==null&&l===void 0&&I(c)&&H(c,r,{client:e.client,path:n,location:a}).state===`current`)return{client:e.client,path:n,change:`unchanged`,format:`toml`,location:a,entry:c,removedLocations:[]};let m,g,_=[];if(l!==void 0){if(!I(l)||c!==void 0&&!I(c))throw Error(`cannot safely migrate Codex MCP entry in ${n}: ambiguous legacy and nested tables`);let e=I(c)&&c.env!==void 0?c.env:l.env;if(e!==void 0&&!I(e))throw Error(`cannot safely migrate Codex MCP entry in ${n}: legacy env is not a table`);if(Object.entries(l).some(([e,t])=>e!==`env`&&I(t))||e&&Object.values(e).some(I))throw Error(`cannot safely migrate Codex MCP entry in ${n}: unsupported legacy subtable`);g=e;let t=S(o,a).text;m=C(c===void 0?t:S(t,a).text,a,F(a,r,e)),_.push(`["${a}"]`)}else m=C(o,a,F(a,r));let y=d(m),w=f(y,a);if(!I(w)||H(w,r,{client:e.client,path:n,location:a}).state!==`current`||p(y,a)!==void 0||l!==void 0&&!b(w.env,g)||!b(x(y),x(s)))throw Error(`cannot safely update Codex MCP entry in ${n}: unsupported TOML representation`);let T=await v(t.fs,n,m,[],{mode:384});return{client:e.client,path:n,change:T,format:`toml`,location:a,entry:w,removedLocations:_}}function F(e,t,n){let r=w(t),i=r.endsWith(`
2
+ `)?r:`${r}\n`,a=n===void 0?``:w(n);return`[${e}]\n${i}${n===void 0?``:`\n[${e}.env]\n${a.endsWith(`
3
+ `)?a:`${a}\n`}`}`}function I(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function L(e,t){return{...e,state:`missing`,reason:t}}function R(e,t){return{...e,state:`drifted`,reason:`legacy-location`,...I(t)&&{entry:t}}}async function z(e,t){let{client:i}=e,a=i===`opencode`?l(e.openCode):void 0,o=a?.mode===`infer-existing`?void 0:a?.layout,s=i===`opencode`?_(o??`v2`):h(i),c=g(i)?e.configPath??await m(i,t):void 0;if(!c)return L({client:i,path:``,location:s},`unsupported-client`);let u={client:i,path:c,location:s,...o&&{openCodeLayout:o}},d=await t.fs.readFile(c);if(d===null)return L(u,a?.mode===`infer-existing`?`layout-unknown`:`file-missing`);if(i===`codex`)return B(d,y(i,e.executablePath,e.args),u);if(d.trim()===``)return L(u,a?.mode===`infer-existing`?`layout-unknown`:`no-entry`);if(!r(d))return L(u,`invalid-config`);if(a)return V(d,e,a,u);let f=n(d,s);if(f===void 0)return L(u,`no-entry`);let p=y(i,e.executablePath,e.args);return I(f)?H(f,p,u):L(u,`entry-not-object`)}function B(e,t,n){if(e.trim()===``)return L(n,`no-entry`);let r=d(e);if(!r)return L(n,`invalid-config`);let i=p(r,n.location);if(i!==void 0)return R(n,i);let a=f(r,n.location);return a===void 0?L(n,`no-entry`):I(a)?H(a,t,n):L(n,`entry-not-object`)}function V(e,t,n,r){let i=e=>y(`opencode`,t.executablePath,t.args,{openCode:{mode:`authoritative`,layout:e}});if(c(e,k(n),r.path)!==void 0)return L(r,`unsafe-config`);if(n.mode===`infer-existing`){let t=N(e);if(!t)return L(r,`layout-unknown`);let{layout:n,found:a}=t,o=j(n),s={...r,location:_(n),openCodeLayout:n,...M(e,o)!==void 0&&{openCodeDuplicateLocation:_(o)}};return I(a)?H(a,i(n),s):L(s,`entry-not-object`)}let a=M(e,n.layout),o=j(n.layout),s=M(e,o);if(a===void 0)return s===void 0?L(r,`no-entry`):R({...r,location:_(o)},s);let l=s===void 0?r:{...r,openCodeDuplicateLocation:_(o)};if(!I(a))return L(l,`entry-not-object`);let u=H(a,i(n.layout),l);return u.state===`current`&&s!==void 0?R(l,a):u}function H(e,t,n){for(let[r,i]of Object.entries(t)){if(!(r in e)){if(T.includes(r))return{...n,state:`drifted`,reason:`pinned-key-missing`,key:r,entry:e};continue}if(!U(e[r],i))return{...n,state:`drifted`,reason:`value-mismatch`,key:r,entry:e}}return{...n,state:`current`,entry:e}}function U(e,t){if(e===t)return!0;if(typeof e!=typeof t||e===null||t===null||typeof e!=`object`)return!1;if(Array.isArray(e)||Array.isArray(t))return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((e,n)=>U(e,t[n]));let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?n.every(n=>U(e[n],t[n])):!1}export{z as inspectMcpInstall,E as installMcp};
@@ -1,6 +1,8 @@
1
1
  //#region src/mcp/jsonc-edit.d.ts
2
2
  /** Whether `text` is valid JSONC: comments, a trailing comma and a leading UTF-8 BOM are all fine. */
3
3
  declare function isValidJsonc(text: string): boolean;
4
+ /** Throws when `text` isn't valid JSONC; never touches `text`. */
5
+ declare function assertValidJsonc(text: string, path: string): void;
4
6
  /** Reads the value at `pointer` in JSONC `text`, or `undefined` when absent or the document is unparsable. */
5
7
  declare function findAtPointer(text: string, pointer: string): unknown;
6
8
  /**
@@ -38,4 +40,4 @@ declare function removeAtPointerAndEmptyParent(text: string, pointer: string, so
38
40
  removed: boolean;
39
41
  };
40
42
  //#endregion
41
- export { addAtPointer, findAtPointer, isValidJsonc, removeAtPointer, removeAtPointerAndEmptyParent };
43
+ export { addAtPointer, assertValidJsonc, findAtPointer, isValidJsonc, removeAtPointer, removeAtPointerAndEmptyParent };
@@ -1,3 +1,3 @@
1
1
  import{applyEdits as e,findNodeAtLocation as t,getNodeValue as n,modify as r,parseTree as i,visit as a}from"jsonc-parser";const o={tabSize:2,insertSpaces:!0,eol:`
2
2
  `},s={allowTrailingComma:!0};function c(e){return e.startsWith(``)?{bom:``,body:e.slice(1)}:{bom:``,body:e}}function l(e){let t=[];return i(c(e).body,t,s),t.length===0}function u(e,t){if(!l(e))throw Error(`invalid JSON in ${t}`)}function d(e){return e?e.split(`/`).slice(1).map(e=>e.replace(/~1/g,`/`).replace(/~0/g,`~`)):[]}function f(e,n){let r=[],i=e;for(let e of n){let n=i?.type===`array`&&/^(0|[1-9]\d*)$/.test(e)?Number(e):e;r.push(n),i=i?t(i,[n]):void 0}return r}function p(e,r){let a=[],o=i(c(e).body,a,s);if(!o||a.length>0)return;let l=t(o,f(o,d(r)));return l?n(l):void 0}function m(n,a,s,l){u(n,l);let{bom:p,body:m}=c(n),h=d(a),g=m;for(let n=1;n<h.length;n++){let a=i(g),s=a?f(a,h.slice(0,n)):h.slice(0,n);a&&t(a,s)||(g=e(g,r(g,s,{},{formattingOptions:o})))}let _=i(g),v=_?f(_,h):h;return p+e(g,r(g,v,s,{formattingOptions:o}))}function h(n,a){let c=i(n,void 0,s),l=c?f(c,a):a,u=c?t(c,l):void 0;if(!(a.length===0||!c||!u))return u.parent?.type===`array`&&typeof l.at(-1)==`number`?g(n,u.parent,u):v(e(n,r(n,l,void 0,{formattingOptions:o})))}function g(t,n,r){let i=n.children??[],a=i.indexOf(r),o=[{offset:r.offset,length:r.length,content:``}];if(a<i.length-1){let e=_(t,r.offset+r.length,i[a+1].offset);e!==void 0&&o.push({offset:e,length:1,content:``})}else if(a>0){let e=i[a-1],n=_(t,e.offset+e.length,r.offset);n!==void 0&&o.push({offset:n,length:1,content:``})}else{let e=_(t,r.offset+r.length,n.offset+n.length-1);e!==void 0&&o.push({offset:e,length:1,content:``})}return e(t,o.sort((e,t)=>e.offset-t.offset))}function _(e,t,n){for(let r=t;r<n;r++)if(e.startsWith(`//`,r))for(;r<n&&e[r]!==`
3
- `;)r++;else if(e.startsWith(`/*`,r)){let t=e.indexOf(`*/`,r+2);if(t<0||t>=n)return;r=t+1}else if(e[r]===`,`)return r}function v(e){let t={justOpened:!1},n=()=>{t.justOpened=!1};a(e,{onObjectBegin:()=>{t.justOpened=!0},onSeparator:(e,r)=>{t.justOpened&&e===`,`&&(t.commaOffset??=r),n()},onObjectProperty:n,onObjectEnd:n},s);let{commaOffset:r}=t;return r===void 0?e:e.slice(0,r)+e.slice(r+1)}function y(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=h(i,d(t));return a===void 0?{text:e,removed:!1}:{text:r+a,removed:!0}}function b(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=d(t),o=h(i,a);if(o===void 0)return{text:e,removed:!1};let s=a.slice(0,-1);return s.length>0&&x(o,s)&&(o=h(o,s)??o),{text:r+o,removed:!0}}function x(e,n){let r=i(e,void 0,s);if(!r)return!1;let a=t(r,f(r,n));return!a||a.type!==`object`?!1:(a.children?.length??0)===0}export{m as addAtPointer,p as findAtPointer,l as isValidJsonc,y as removeAtPointer,b as removeAtPointerAndEmptyParent};
3
+ `;)r++;else if(e.startsWith(`/*`,r)){let t=e.indexOf(`*/`,r+2);if(t<0||t>=n)return;r=t+1}else if(e[r]===`,`)return r}function v(e){let t={justOpened:!1},n=()=>{t.justOpened=!1};a(e,{onObjectBegin:()=>{t.justOpened=!0},onSeparator:(e,r)=>{t.justOpened&&e===`,`&&(t.commaOffset??=r),n()},onObjectProperty:n,onObjectEnd:n},s);let{commaOffset:r}=t;return r===void 0?e:e.slice(0,r)+e.slice(r+1)}function y(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=h(i,d(t));return a===void 0?{text:e,removed:!1}:{text:r+a,removed:!0}}function b(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=d(t),o=h(i,a);if(o===void 0)return{text:e,removed:!1};let s=a.slice(0,-1);return s.length>0&&x(o,s)&&(o=h(o,s)??o),{text:r+o,removed:!0}}function x(e,n){let r=i(e,void 0,s);if(!r)return!1;let a=t(r,f(r,n));return!a||a.type!==`object`?!1:(a.children?.length??0)===0}export{m as addAtPointer,u as assertValidJsonc,p as findAtPointer,l as isValidJsonc,y as removeAtPointer,b as removeAtPointerAndEmptyParent};
@@ -0,0 +1,54 @@
1
+ import { McpInstallResult } from "./installer.js";
2
+
3
+ //#region src/mcp/opencode-layout.d.ts
4
+ /** Where OpenCode reads MCP servers: `/mcp/<name>` (v1) or `/mcp/servers/<name>` (v2). */
5
+ type OpenCodeMcpLayout = 'v1' | 'v2';
6
+ /**
7
+ * How a host tells this library which OpenCode MCP layout to use.
8
+ *
9
+ * - `authoritative`: the host verified the installed OpenCode and asserts the layout. Install writes it and
10
+ * prunes the other layout's GitKraken entry.
11
+ * - `infer-existing`: the host could not tell. Install and inspection follow the layout the config already
12
+ * uses and never prune. With both layouts present they follow v1, the entry OpenCode's compatibility reader
13
+ * retains over the native one. It needs the config file, so read-free helpers reject it.
14
+ *
15
+ * Omitting the policy keeps the native-v2 behavior of 0.1.1: v2 generation and inspection, v1 pruning.
16
+ */
17
+ type OpenCodeMcpPolicy = {
18
+ mode: 'authoritative';
19
+ layout: OpenCodeMcpLayout;
20
+ } | {
21
+ mode: 'infer-existing';
22
+ };
23
+ interface McpLayoutOptions {
24
+ /** Ignored for every client but OpenCode. */
25
+ openCode?: OpenCodeMcpPolicy;
26
+ }
27
+ /** `infer-existing` found no GitKraken entry to follow, or was passed to a helper that cannot read the config. */
28
+ declare class OpenCodeMcpLayoutUnknownError extends Error {
29
+ constructor();
30
+ }
31
+ /**
32
+ * The selected OpenCode entry is in place, but removing the other layout's entry did not complete. Not a
33
+ * success: an older reader can still reject a leftover native `servers` envelope, and a v1-compatible reader
34
+ * can pick the older duplicate. Re-running the install finishes the cleanup.
35
+ */
36
+ declare class OpenCodeMcpCleanupError extends Error {
37
+ /** What was committed before the cleanup failed: the selected location and entry, and no removed locations. */
38
+ readonly result: McpInstallResult;
39
+ constructor(result: McpInstallResult, cause: unknown);
40
+ }
41
+ /** Applies the omitted-policy default and rejects a value that is not an {@link OpenCodeMcpPolicy}. */
42
+ declare function resolveOpenCodePolicy(policy: OpenCodeMcpPolicy | undefined): OpenCodeMcpPolicy;
43
+ /** The layout a read-free helper emits for `options`; `infer-existing` cannot be resolved without reading the config. */
44
+ declare function openCodeLayoutFor(options: McpLayoutOptions | undefined): OpenCodeMcpLayout;
45
+ /** The v2 `servers` envelope, or `undefined` when `/mcp/servers` is absent or is a flat v1 server that happens to be named `servers`. */
46
+ declare function openCodeNativeServers(text: string): Record<string, unknown> | undefined;
47
+ /**
48
+ * Why writing the GitKraken entry in `layout` would displace other data in the OpenCode config `text`, or
49
+ * `undefined` when it is safe. Shared by install, which throws it, and inspect, which reports it. An undefined
50
+ * `layout` (`infer-existing`) checks only the root and `mcp`: an inferred v2 entry already sits in a native envelope.
51
+ */
52
+ declare function openCodeRefusal(text: string, layout: OpenCodeMcpLayout | undefined, path: string): string | undefined;
53
+ //#endregion
54
+ export { McpLayoutOptions, OpenCodeMcpCleanupError, OpenCodeMcpLayout, OpenCodeMcpLayoutUnknownError, OpenCodeMcpPolicy, openCodeLayoutFor, openCodeNativeServers, openCodeRefusal, resolveOpenCodePolicy };
@@ -0,0 +1 @@
1
+ import{findAtPointer as e}from"./jsonc-edit.js";var t=class extends Error{constructor(){super(`cannot resolve the OpenCode MCP layout for infer-existing: there is no GitKraken entry to follow, or the caller cannot read the config; pass an authoritative layout instead`),this.name=`OpenCodeMcpLayoutUnknownError`}},n=class extends Error{result;constructor(e,t){super(`OpenCode MCP entry was written but alternate-layout cleanup did not complete; run the install again`,{cause:t}),this.name=`OpenCodeMcpCleanupError`,this.result=e}};function r(e){if(e===void 0)return{mode:`authoritative`,layout:`v2`};let t=e;if(t?.mode===`infer-existing`&&!(`layout`in t))return{mode:`infer-existing`};if(t?.mode===`authoritative`&&(t.layout===`v1`||t.layout===`v2`))return{mode:`authoritative`,layout:t.layout};throw TypeError(`invalid OpenCode MCP policy`)}function i(e){let n=r(e?.openCode);if(n.mode===`infer-existing`)throw new t;return n.layout}const a=`/mcp/servers`;function o(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function s(e){return[`type`,`enabled`].some(t=>Object.hasOwn(e,t)&&(e[t]===null||typeof e[t]!=`object`||Array.isArray(e[t])))}function c(t){let n=e(t,a);return o(n)&&!s(n)?n:void 0}function l(t,n,r){let i=e(t,`/mcp`);if(!o(e(t,``))||i!==void 0&&!o(i))return`cannot safely update OpenCode MCP entry in ${r}: the config or its "mcp" is not an object`;if(n!==`v2`)return;let c=e(t,a);if(c!==void 0&&!o(c))return`cannot safely update OpenCode MCP entry in ${r}: "mcp.servers" is not an object`;if(o(c)&&s(c))return`cannot safely write the OpenCode v2 MCP layout in ${r}: "mcp.servers" is a server definition`}export{n as OpenCodeMcpCleanupError,t as OpenCodeMcpLayoutUnknownError,i as openCodeLayoutFor,c as openCodeNativeServers,l as openCodeRefusal,r as resolveOpenCodePolicy};
@@ -1 +1 @@
1
- import{removeAtPointer as e,removeAtPointerAndEmptyParent as t}from"./jsonc-edit.js";import{assertValidToml as n,parseTomlSafe as r,tomlDottedPath as i,tomlLiteralKey as a}from"./toml-read.js";import{OPEN_CODE_JSON_PATHS as o,clientConfigPaths as s,clientJSONPath as c,isSupported as l}from"./clients.js";import{writeFileTracked as u}from"../file-change.js";import{tomlSemanticEqual as d,withoutCodexEntry as f}from"./toml-safety.js";import{removeBlock as p,trimTrailingBlankLines as m}from"./toml-splice.js";function h(e,t){return{file:{path:e,change:`unchanged`,removedLocations:[],reason:t}}}async function g(e,t){let{client:n}=e;if(!l(n))throw Error(`unsupported client: ${n}`);let r=e.configPath?[e.configPath]:await s(n,t),i=[];for(let e of r)i.push(await _(n,e,t));let a=[];for(let{file:e,text:n}of i)n!==void 0&&await u(t.fs,e.path,n,[]),a.push(e);return{client:n,removed:a.some(e=>e.removedLocations.length>0),files:a}}async function _(e,t,n){let r=await n.fs.readFile(t);if(r===null)return h(t,`file-missing`);if(r.trim()===``)return h(t,`file-empty`);let{text:i,removedLocations:a}=e===`codex`?y(r,t):v(r,e,t);return a.length===0?h(t,`no-entry`):{file:{path:t,change:`updated`,removedLocations:a},text:i}}function v(n,r,i){let a=r===`opencode`,s=a?o:[c(r)],l=n,u=[];for(let n of s){let r=a?t(l,n,i):e(l,n,i);r.removed&&(l=r.text,u.push(n))}return{text:l,removedLocations:u}}function y(e,t){n(e,t);let o=c(`codex`),s=r(e);if(a(s,o)===void 0&&i(s,o)===void 0)return{text:e,removedLocations:[]};let{text:l,removed:u}=p(e,o);if(!u){if(a(s,o)!==void 0||i(s,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: unsupported TOML representation`);return{text:e,removedLocations:[]}}let h=m(l),g=r(h);if(a(g,o)!==void 0||i(g,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: target remains after splice`);if(!g||!d(f(g),f(s)))throw Error(`cannot safely remove Codex MCP entry in ${t}: splice would change another TOML value`);return{text:h,removedLocations:[o]}}export{g as uninstallMcp};
1
+ import{removeAtPointer as e,removeAtPointerAndEmptyParent as t}from"./jsonc-edit.js";import{assertValidToml as n,parseTomlSafe as r,tomlDottedPath as i,tomlLiteralKey as a}from"./toml-read.js";import{clientConfigPaths as o,clientJSONPath as s,isSupported as c,openCodeEntryPointers as l}from"./clients.js";import{writeFileTracked as u}from"../file-change.js";import{tomlSemanticEqual as d,withoutCodexEntry as f}from"./toml-safety.js";import{removeBlock as p,trimTrailingBlankLines as m}from"./toml-splice.js";function h(e,t){return{file:{path:e,change:`unchanged`,removedLocations:[],reason:t}}}async function g(e,t){let{client:n}=e;if(!c(n))throw Error(`unsupported client: ${n}`);let r=e.configPath?[e.configPath]:await o(n,t),i=[];for(let e of r)i.push(await _(n,e,t));let a=[];for(let{file:e,text:n}of i)n!==void 0&&await u(t.fs,e.path,n,[]),a.push(e);return{client:n,removed:a.some(e=>e.removedLocations.length>0),files:a}}async function _(e,t,n){let r=await n.fs.readFile(t);if(r===null)return h(t,`file-missing`);if(r.trim()===``)return h(t,`file-empty`);let{text:i,removedLocations:a}=e===`codex`?y(r,t):v(r,e,t);return a.length===0?h(t,`no-entry`):{file:{path:t,change:`updated`,removedLocations:a},text:i}}function v(n,r,i){let a=r===`opencode`,o=a?l(n):[s(r)],c=n,u=[];for(let n of o){let r=a?t(c,n,i):e(c,n,i);r.removed&&(c=r.text,u.push(n))}return{text:c,removedLocations:u}}function y(e,t){n(e,t);let o=s(`codex`),c=r(e);if(a(c,o)===void 0&&i(c,o)===void 0)return{text:e,removedLocations:[]};let{text:l,removed:u}=p(e,o);if(!u){if(a(c,o)!==void 0||i(c,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: unsupported TOML representation`);return{text:e,removedLocations:[]}}let h=m(l),g=r(h);if(a(g,o)!==void 0||i(g,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: target remains after splice`);if(!g||!d(f(g),f(c)))throw Error(`cannot safely remove Codex MCP entry in ${t}: splice would change another TOML value`);return{text:h,removedLocations:[o]}}export{g as uninstallMcp};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/agent-tools",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Host configuration and path helpers for AI coding agent integrations, with an in-memory filesystem for testing.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",