@gitkraken/agent-tools 0.2.0 → 0.3.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
@@ -1,10 +1,40 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
3
+ ## 0.3.0
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).
5
+ ### Minor Changes
6
6
 
7
- ## [Unreleased]
7
+ - [#194](https://github.com/gitkraken/core-tools/pull/194) [`6a05082`](https://github.com/gitkraken/core-tools/commit/6a05082e9efb35203bb209c51845539c4be25a00) Thanks [@JBUinfo](https://github.com/JBUinfo)! - A default `installJsonHooks` or `installHooks` install removes the GitKraken-owned entries for every event in the host's `unsafeEvents` already on disk (for Cursor, a stale `afterAgentThought` entry written by 0.1.x or 0.2.0), keeping other entries and events, so Cursor stops aborting every turn. A request that names `events` or `blockingEvents` does not prune.
8
+
9
+ - [#194](https://github.com/gitkraken/core-tools/pull/194) [`6a05082`](https://github.com/gitkraken/core-tools/commit/6a05082e9efb35203bb209c51845539c4be25a00) Thanks [@JBUinfo](https://github.com/JBUinfo)! - Cursor refuses blocking events like OpenCode and Pi: `installHooks` and `installJsonHooks` throw `HookConfigError` `blocking-unsupported` (`cursor does not support blocking hooks`) before touching the hooks file, because Cursor ignores a blocking decision. The check reads `noBlocking` from `hookClientInfo` for every host. A default Cursor install no longer writes `afterAgentThought`, which aborts every Cursor turn; requesting it with `events` still writes it.
10
+
11
+ - [#194](https://github.com/gitkraken/core-tools/pull/194) [`6a05082`](https://github.com/gitkraken/core-tools/commit/6a05082e9efb35203bb209c51845539c4be25a00) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `eventManifest` is regenerated from gkcli's `events.json`. Entries gain `promptSubmitEvent`, `eventFlag` and `blockingWait`; Antigravity lists `SessionStart` as an event and as its session start, and has no session-end event (`sessionStartEvent` was empty and `sessionEndEvent` was `Stop`); Cursor skips `afterAgentThought` in a default install and has `promptSubmitEvent`; Pi and OpenCode event lists match gkcli, and OpenCode's `v1OnlyEvents` also lists `session.idle`, `session.diff`, `file.edited` and `permission.updated` while `v2EventAliases` no longer maps `session.status`. `hookHosts` is derived from the manifest, in gkcli's order.
12
+
13
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - Hook registry: `hookHosts`, `hookClientInfo(host)` (`{ displayName, binaryNames, clientType?, noBlocking, unsafeEvents, blockingWait? }`, the values of gkcli's hook registry for the nine hosts) and `executableForHook(host, config)`, which returns the first of the host's `binaryNames` that `config.resolveExecutable` finds. `resolveHookClient` is backed by the registry with unchanged results.
14
+
15
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `hookCapabilities(config)` builds the `HookCapability[]` that `listAgents` merges, from the hook registry, `inspectHooks` and `config.resolveExecutable`, so hosts no longer assemble it. Its `detected` is PATH-only, as in gkcli.
16
+
17
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `HookConfigErrorCode` gains `unreadable`, reported by `inspectHooks` when the filesystem fails.
18
+
19
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `inspectHooks(host, config)` reads whether GitKraken hooks are registered without writing or removing anything, and returns `{ host, installed, usable, path, layout, events, blockingEvents, needsMigration, error? }`. A corrupt or unreadable file never throws: `error` carries the code. `installed` mirrors gkcli's `IsInstalled`, so Claude Code counts an enabled plugin even when its hooks file is corrupt or missing; `usable` is true only when `installed`, there is no `error`, at least one GitKraken event is registered and, for Claude Code, no plugin files are missing (a legacy plugin's own files are not checked, so a legacy install is usable only with `settings.json` hooks). For Claude Code, `installed` is true for any layout gkcli counts: a GitKraken entry in `settings.json` `hooks`, the legacy `gitkraken@local` or `gk@gitkraken` plugin enabled, a legacy record in `installed_plugins.json`, or `gitkraken-hooks@gitkraken` enabled; `needsMigration` is set for legacy state and for an enabled current plugin whose cached manifest, cached or marketplace hooks file, marketplace manifest or marketplace path is missing or wrong. `layout` is `file`, `plugin`, `legacy` or `none`, and `events` includes the blocking ones listed in `blockingEvents`.
20
+
21
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `installHooks` and `uninstallHooks` reject an invalid event with the `validateHookEvents` message, which now quotes the event and lists the valid ones (previously `<event> is not valid for <host>`).
22
+
23
+ - [#194](https://github.com/gitkraken/core-tools/pull/194) [`6a05082`](https://github.com/gitkraken/core-tools/commit/6a05082e9efb35203bb209c51845539c4be25a00) Thanks [@JBUinfo](https://github.com/JBUinfo)! - JSON hook entries match what gkcli writes. Antigravity entries carry `--event <name>` (before any blocking tail), Copilot blocking entries end with `--timeout 5s --blocking`, and a non-blocking Codex `SessionEnd` entry gets `timeout: 3`. Entries already on disk are rewritten on the next install.
24
+
25
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `listAgents` takes a third argument, `{ hookDetection?: 'merged' | 'path-only' }`. `path-only` makes a hook capability replace `detected` on every row that is not a shared multi-row host (so the MCP config-directory fallback no longer marks a hook row detected), matching gkcli's `internal/agents.List`. The default, `merged`, is the 0.2.0 behavior.
26
+
27
+ - [#245](https://github.com/gitkraken/core-tools/pull/245) [`6770dec`](https://github.com/gitkraken/core-tools/commit/6770dec81ff6a849ecdd9cb454a8e558b311943b) Thanks [@JBUinfo](https://github.com/JBUinfo)! - The OpenCode relay plugin is now template version 8 and the Pi extension version 3, so installed files regenerate on the next install. Relayed events carry `relay_instance`, `relay_seq` and `host_pid`, and the OpenCode plugin's v1-only list now includes `session.idle`, `session.diff`, `file.edited` and `permission.updated`, while `session.status` no longer aliases `session.updated`.
28
+
29
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `validateHookEvents(host, events)` throws `HookConfigError` (`invalid-event`) for the first event the host does not list, with the message `'<event>' is not a valid event for <host>. Valid events: <list>` that `gk ai hook` prints.
30
+
31
+ ### Patch Changes
32
+
33
+ - [#254](https://github.com/gitkraken/core-tools/pull/254) [`a1cc87d`](https://github.com/gitkraken/core-tools/commit/a1cc87d942b2c66f6353b94aeb058280dc6e9225) Thanks [@JBUinfo](https://github.com/JBUinfo)! - Claude Code hook install and uninstall now edit only the GitKraken entries in `settings.json`, `known_marketplaces.json` and `installed_plugins.json`, keeping the formatting and comments of every other entry.
34
+
35
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `installJsonHooks` throws `HookConfigError` (`unsupported-host`) for a prototype key such as `constructor`, instead of a raw `TypeError`.
36
+
37
+ - [#193](https://github.com/gitkraken/core-tools/pull/193) [`92a5945`](https://github.com/gitkraken/core-tools/commit/92a594597d013ca901ce47e8d023b0c0da1642b5) Thanks [@JBUinfo](https://github.com/JBUinfo)! - `uninstallHooks` for Claude Code removes the legacy `gitkraken@local` or `gk@gitkraken` `enabledPlugins` entry when it is the only GitKraken state left, instead of returning without changes.
8
38
 
9
39
  ## [0.2.0] - 2026-10-12
10
40
 
package/README.md CHANGED
@@ -116,6 +116,35 @@ try {
116
116
 
117
117
  Only GitKraken-owned hooks are added or removed; other entries and keys in the hooks file are preserved, and a file that is not valid JSON (or, for the OpenCode and Pi relays, not written by GitKraken) is refused without being modified.
118
118
 
119
+ Inspect, list and validate without writing anything:
120
+
121
+ ```ts
122
+ import {
123
+ executableForHook,
124
+ hookCapabilities,
125
+ hookClientInfo,
126
+ hookHosts,
127
+ inspectHooks,
128
+ listAgents,
129
+ validateHookEvents,
130
+ } from '@gitkraken/agent-tools';
131
+
132
+ const inspection = await inspectHooks('claude-code', config);
133
+ // { host, installed, usable, path, layout: 'file' | 'plugin' | 'legacy' | 'none', events, blockingEvents, needsMigration, error? }
134
+
135
+ // Never throws on a corrupt or unreadable file: `error` carries the code instead.
136
+ const agents = await listAgents(config, await hookCapabilities(config), { hookDetection: 'path-only' });
137
+
138
+ for (const host of hookHosts) {
139
+ hookClientInfo(host); // { displayName, binaryNames, clientType?, noBlocking, unsafeEvents, blockingWait? }
140
+ executableForHook(host, config); // the first of binaryNames that config.resolveExecutable finds, or undefined
141
+ }
142
+
143
+ validateHookEvents('codex', ['PreToolUse']); // throws HookConfigError ('invalid-event') for the first unknown event
144
+ ```
145
+
146
+ `installed` mirrors gkcli's `IsInstalled`: hooks are registered, even if the Claude plugin's files are broken. `usable` additionally requires no `error`, at least one event and, for Claude Code, intact plugin files. `needsMigration` is true for legacy Claude state or plugin drift that a reinstall would fix. `hookCapabilities` builds the hook list for `listAgents` (`hooksInstalled` follows `installed`); `hookDetection: 'path-only'` makes a hook's PATH lookup replace `detected` on rows that are not shared multi-row hosts, as gkcli does.
147
+
119
148
  ## Client detection
120
149
 
121
150
  ```ts
package/dist/agents.d.ts CHANGED
@@ -38,15 +38,24 @@ interface Agent {
38
38
  hooksSupported: boolean;
39
39
  hooksInstalled: boolean;
40
40
  }
41
+ interface ListAgentsOptions {
42
+ /** `merged` (default) ORs a hook capability's `detected` into the MCP result; `path-only` replaces it, like gkcli. */
43
+ hookDetection?: 'merged' | 'path-only';
44
+ }
41
45
  /**
42
46
  * The unified list of agents, sorted by name: every MCP client, plus any
43
47
  * hook capability the host passes in (see {@link HookCapability}). Mirrors
44
48
  * gkcli's `internal/agents.List`.
45
49
  */
46
- declare function listAgents(config: AgentHostConfig, hookCapabilities?: readonly HookCapability[]): Promise<Agent[]>;
50
+ declare function listAgents(config: AgentHostConfig, hookCapabilities?: readonly HookCapability[], options?: ListAgentsOptions): Promise<Agent[]>;
47
51
  /** MCP row ids that report one hook host's capability. */
48
52
  declare function mcpNamesForHook(host: HookHost): string[];
49
53
  /** Resolve aliases accepted by `gk ai hook` to the backing hook registry id. */
50
54
  declare function resolveHookClient(client: string): string;
55
+ /**
56
+ * One {@link HookCapability} per hook host, with PATH-only `detected`, for {@link listAgents}.
57
+ * `installed` is `inspectHooks().installed`, not `usable`, so a broken Claude plugin still prompts a repair.
58
+ */
59
+ declare function hookCapabilities(config: AgentHostConfig): Promise<HookCapability[]>;
51
60
  //#endregion
52
- export { Agent, type Client, HookCapability, listAgents, mcpNamesForHook, resolveHookClient };
61
+ export { Agent, type Client, HookCapability, ListAgentsOptions, hookCapabilities, listAgents, mcpNamesForHook, resolveHookClient };
package/dist/agents.js CHANGED
@@ -1 +1 @@
1
- import{allClients as e,clientDisplayName as t,clientType as n,clientsForHost as r,detectClient as i,getClientInstallation as a}from"./mcp/clients.js";async function o(r,o=[]){let s=new Map;for(let o of e()){let e=await i(o,r),c=await a(o,r);s.set(o,{name:o,type:n(o),displayName:t(o),detected:e.detected,executable:e.executablePath,detection:e,mcpSupported:!0,mcpInstalled:c.installed,mcpConfigPaths:c.paths,hooksSupported:!1,hooksInstalled:!1})}for(let e of o)for(let t of e.mcpNames){let n=s.get(t);n||(n={name:t,type:e.clientType??`unknown`,displayName:e.displayName,detected:!1,mcpSupported:!1,mcpInstalled:!1,mcpConfigPaths:[],hooksSupported:!1,hooksInstalled:!1},s.set(t,n)),n.hooksSupported=!0,n.hooksInstalled=e.installed,!(e.mcpNames.length>1)&&(n.detected=n.detected||e.detected,n.executable||=e.executable)}return[...s.values()].sort((e,t)=>e.name.localeCompare(t.name))}function s(e){if(e===`claude-code`)return[`claude-cli`];let t=r(e);return t.length?t:[e]}function c(e){for(let t of[`claude-code`,`opencode`,`cursor`,`codex`,`copilot`,`antigravity`,`pi`,`augment`,`grok`])if(s(t).includes(e))return t;return e}export{o as listAgents,s as mcpNamesForHook,c as resolveHookClient};
1
+ import{executableForHook as e,hookClientInfo as t,hookHosts as n}from"./hooks/registry.js";import{inspectHooks as r}from"./hooks/inspect.js";import{allClients as i,clientDisplayName as a,clientType as o,clientsForHost as s,detectClient as c,getClientInstallation as l}from"./mcp/clients.js";async function u(e,t=[],n={}){let r=new Map;for(let t of i()){let n=await c(t,e),i=await l(t,e);r.set(t,{name:t,type:o(t),displayName:a(t),detected:n.detected,executable:n.executablePath,detection:n,mcpSupported:!0,mcpInstalled:i.installed,mcpConfigPaths:i.paths,hooksSupported:!1,hooksInstalled:!1})}for(let e of t)for(let t of e.mcpNames){let i=r.get(t);i||(i={name:t,type:e.clientType??`unknown`,displayName:e.displayName,detected:!1,mcpSupported:!1,mcpInstalled:!1,mcpConfigPaths:[],hooksSupported:!1,hooksInstalled:!1},r.set(t,i)),i.hooksSupported=!0,i.hooksInstalled=e.installed,!(e.mcpNames.length>1)&&(i.detected=n.hookDetection===`path-only`?e.detected:i.detected||e.detected,i.executable||=e.executable)}return[...r.values()].sort((e,t)=>e.name.localeCompare(t.name))}function d(e){if(e===`claude-code`)return[`claude-cli`];let t=s(e);return t.length?t:[e]}function f(e){for(let t of n)if(d(t).includes(e))return t;return e}async function p(i){return Promise.all(n.map(async n=>{let a=t(n),o=e(n,i);return{mcpNames:d(n),displayName:a.displayName,clientType:a.clientType,detected:o!==void 0,executable:o,installed:(await r(n,i)).installed}}))}export{p as hookCapabilities,u as listAgents,d as mcpNamesForHook,f as resolveHookClient};
@@ -1,5 +1,5 @@
1
1
  import { AgentHostConfig } from "../config.js";
2
- import { HookInstallResult, HookUninstallResult } from "./results.js";
2
+ import { HookInspection, HookInstallResult, HookUninstallResult } from "./results.js";
3
3
 
4
4
  //#region src/hooks/claude.d.ts
5
5
  interface ClaudeHookRequest {
@@ -12,5 +12,6 @@ interface ClaudeHookRequest {
12
12
  }
13
13
  declare function installClaudeHooks(request: ClaudeHookRequest, config: AgentHostConfig): Promise<HookInstallResult>;
14
14
  declare function uninstallClaudeHooks(events: readonly string[] | undefined, config: AgentHostConfig): Promise<HookUninstallResult>;
15
+ declare function inspectClaudeHooks(config: AgentHostConfig): Promise<HookInspection>;
15
16
  //#endregion
16
- export { ClaudeHookRequest, installClaudeHooks, uninstallClaudeHooks };
17
+ export { ClaudeHookRequest, inspectClaudeHooks, installClaudeHooks, uninstallClaudeHooks };
@@ -1,3 +1,2 @@
1
- import{joinPath as e}from"../config.js";import{addAtPointer as t,removeAtPointer as n}from"../mcp/jsonc-edit.js";import{removeTracked as r,writeFileTracked as i}from"../file-change.js";import{HookConfigError as a}from"./errors.js";import{eventManifest as o}from"./events.js";import{command as s}from"./writers.js";import{parse as c}from"jsonc-parser";const l=`gitkraken`,u=`gitkraken-hooks`,d=`${u}@${l}`,f=`Live AI session tracking for GitKraken products. See what Claude is doing in real time — current tool, active prompt, subagents — and optionally approve or deny tool calls from the UI.`,p=[`gitkraken@local`,`gk@gitkraken`];function m(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:{}}function h(e,t){if(e===null||e.trim()===``)return{};let n=[],r=c(e,n,{allowTrailingComma:!0});if(n.length||!r||typeof r!=`object`||Array.isArray(r))throw new a(`invalid-json`,`${t} is not valid JSON; refusing to overwrite shared configuration`,t);return r}function g(e,t){if(e.hooks===void 0)return{};if(!e.hooks||typeof e.hooks!=`object`||Array.isArray(e.hooks))throw new a(`wrong-shape`,`${t} has an invalid hooks wrapper`,t);let n=e.hooks;for(let[e,r]of Object.entries(n))if(!Array.isArray(r)||!r.every(e=>{if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e.hooks;return t===void 0||Array.isArray(t)&&t.every(e=>!!e&&typeof e==`object`&&!Array.isArray(e))}))throw new a(`wrong-shape`,`${t} has an invalid ${e} hook group`,t);return n}function _(e,t){for(let n of[`enabledPlugins`,`extraKnownMarketplaces`]){let r=e[n];if(r!==void 0&&(!r||typeof r!=`object`||Array.isArray(r)))throw new a(`wrong-shape`,`${t} has an invalid ${n} map`,t)}}function v(t){return t.paths.claudeConfigDir||e(t.paths.home,`.claude`)}function y(t,n){let r=v(t),i=e(r,`plugins`,`marketplaces`,l);return{base:r,marketplace:i,source:e(i,`plugins`,u),cache:e(r,`plugins`,`cache`,l,u,n),settings:e(r,`settings.json`),known:e(r,`plugins`,`known_marketplaces.json`)}}function b(){return{name:l,description:`Plugins from GitKraken to enhance AI coding workflows in GitKraken Desktop, GitLens, and other GitKraken products.`,owner:{name:`GitKraken`},plugins:[{name:u,source:`./plugins/${u}`,description:f}]}}function x(e){return typeof m(e).command==`string`&&m(e).command.includes(`ai hook run`)}function S(e,t,n,r){C(e,[t]);let i=Array.isArray(e[t])?[...e[t]]:[],a=i.findIndex(e=>(typeof e.matcher!=`string`||e.matcher===``)&&Array.isArray(e.hooks)&&e.hooks.length>0);a<0&&(a=i.length,i.push({matcher:``,hooks:[]}));let o={...i[a]};o.hooks=[...(Array.isArray(o.hooks)?o.hooks:[]).filter(e=>!x(e)),{type:`command`,command:r?`${n} --blocking`:n,...r?{timeout:86400}:{}}],i[a]=o,e[t]=i}function C(e,t){for(let n of t){let t=Array.isArray(e[n])?e[n]:[],r=!1,i=t.flatMap(e=>{if(!Array.isArray(e.hooks))return[e];let t=e.hooks.filter(e=>!x(e));return t.length===e.hooks.length?[e]:(r=!0,t.length?[{...e,hooks:t}]:[])});r&&(i.length?e[n]=i:delete e[n])}}function w(e){return Object.keys(e).filter(t=>{let n=e[t];return Array.isArray(n)&&n.some(e=>Array.isArray(m(e).hooks)&&m(e).hooks.some(x))})}function T(e,t){let n=e.hooks;if(!n||typeof n!=`object`||Array.isArray(n))return;let r={...n};return C(r,t),Object.keys(n).every(e=>r[e]===n[e])?void 0:r}function E(e,r,i,a){let o=T(r,i);if(!o)return e;for(let[i,s]of Object.entries(r.hooks)){if(o[i]===s)continue;let r=`/hooks/${i.replace(/~/g,`~0`).replace(/\//g,`~1`)}`;e=o[i]===void 0?n(e,r,a).text:t(e,r,o[i],a)}return e}async function D(e,t,n,r){await i(n.fs,e,`${JSON.stringify(t,null,2)}\n`,r,{mode:384})}async function O(e,r,a,o,s,c){let u=await s.fs.readFile(e),f=h(u,e);_(f,e);let g={...m(f.enabledPlugins)};for(let e of p)delete g[e];a?g[d]=!0:delete g[d];let v={...m(f.extraKnownMarketplaces)};if(a?v[l]={...m(v[l]),source:{source:`directory`,path:r}}:delete v[l],!a&&u===null)return;let y=u===null||u.trim()===``;if(!a&&y){c.push({path:e,change:`unchanged`});return}let b=y?`{}`:E(u,f,o,e);for(let[r,i]of[[`enabledPlugins`,g],[`extraKnownMarketplaces`,v]])b=Object.keys(i).length?t(b,`/${r}`,i,e):n(b,`/${r}`,e).text;b===u?c.push({path:e,change:`unchanged`}):await i(s.fs,e,b.endsWith(`
2
- `)?b:`${b}\n`,c,{mode:384})}async function k(e,r,a,o){let s=await a.fs.readFile(e);if(s===null)return;let c=h(s,e);_(c,e);let l={...m(c.enabledPlugins)};for(let e of[...p,d])delete l[e];if(s.trim()===``){o.push({path:e,change:`unchanged`});return}let u=E(s,c,r,e),f=Object.keys(l).length?t(u,`/enabledPlugins`,l,e):n(u,`/enabledPlugins`,e).text;f===s?o.push({path:e,change:`unchanged`}):await i(a.fs,e,f.endsWith(`
3
- `)?f:`${f}\n`,o,{mode:384})}function A(t){return e(t,`plugins`,`installed_plugins.json`)}function j(e,t){return t===e||t.startsWith(`${e}/`)}function M(e,t){let n=e.replaceAll(`\\`,`/`);return t===`win32`?n.toLowerCase():n}function N(e,t,n,r=``){let i=t.filter(t=>t in e).map(e=>`/${e}`),a=m(e.plugins),o=M(r,n),s=o.slice(0,o.lastIndexOf(`/`));for(let e of t){if(!(e in a))continue;if(e!==d||!o){i.push(`/plugins/${e}`);continue}let t=a[e];if(!Array.isArray(t))continue;let r=t.flatMap((e,t)=>{let r=m(e).installPath;if(typeof r!=`string`)return[];let i=M(r,n);return i!==s&&j(s,i)&&!j(o,i)?[t]:[]});r.length&&r.length===t.length?i.push(`/plugins/${e}`):i.push(...r.reverse().map(t=>`/plugins/${e}/${t}`))}return i}async function P(e,t){let n=A(e),r=await t.fs.readFile(n);if(r!==null)try{return[r,h(r,n)]}catch{return}}async function F(e,t,r,a){let o=await P(t,e);if(!o)return;let s=A(t),[c,l]=o,u=c;for(let t of N(l,[...p,d],e.platform,a))u=n(u,t,s).text;u!==c&&await i(e.fs,s,u,r,{mode:384})}async function I(t,n){for(let r of[e(n,`.claude-plugin`,`plugin.json`),e(n,`plugin.json`)]){let e=await t.fs.readFile(r);if(e!==null)try{let t=m(JSON.parse(e)).name;return t===u||t===`gk`}catch{return!1}}return!1}async function L(t,n,i,a){let o=e(n,`plugins`,`cache`,l,u);for(let n of await t.fs.readDir(o)??[]){let s=e(o,n);n!==i&&await I(t,s)&&await r(t.fs,s,a,{recursive:!0})}}async function R(t,n,i,a=!1){let o=[e(n.source,`.claude-plugin`,`plugin.json`),e(n.source,`hooks`,`hooks.json`)];for(let e of o)await r(t.fs,e,i);for(let a of[e(n.source,`.claude-plugin`),e(n.source,`hooks`)])(await t.fs.readDir(a))?.length||await r(t.fs,a,i,{recursive:!0});(await t.fs.readDir(n.source))?.length||await r(t.fs,n.source,i,{recursive:!0});let s=e(n.marketplace,`plugins`);(await t.fs.readDir(s))?.length||await r(t.fs,s,i,{recursive:!0});let c=e(n.base,`plugins`,`cache`,l,u);for(let n of await t.fs.readDir(c)??[]){let a=e(c,n);await r(t.fs,e(a,`.claude-plugin`,`plugin.json`),i),await r(t.fs,e(a,`hooks`,`hooks.json`),i);for(let n of[e(a,`.claude-plugin`),e(a,`hooks`)])(await t.fs.readDir(n))?.length||await r(t.fs,n,i,{recursive:!0});(await t.fs.readDir(a))?.length||await r(t.fs,a,i,{recursive:!0})}(await t.fs.readDir(c))?.length||await r(t.fs,c,i,{recursive:!0});let d=e(n.marketplace,`.claude-plugin`,`marketplace.json`),f=await t.fs.readFile(d);!a&&f?.trimEnd()===JSON.stringify(b(),null,2)&&await r(t.fs,d,i);let p=e(n.marketplace,`.claude-plugin`);!a&&!(await t.fs.readDir(p))?.length&&await r(t.fs,p,i,{recursive:!0});let m=await t.fs.readDir(n.marketplace);!a&&!m?.length&&(m===null&&i.some(e=>e.path.startsWith(`${n.marketplace}/`))?(await t.fs.remove(n.marketplace,{recursive:!0}),i.push({path:n.marketplace,change:`removed`})):await r(t.fs,n.marketplace,i,{recursive:!0}))}async function z(t,n){return(await t.fs.readDir(e(n.marketplace,`plugins`))??[]).some(e=>e!==u)}function B(e,t){return Object.keys(e).some(e=>e!==`hooks`)||Object.values(t).some(Array.isArray)}async function V(t,n){let i=[],a=t.version||`0.0.0-dev`,c=y(n,a),d=e(c.source,`hooks`,`hooks.json`),p=h(await n.fs.readFile(d),d),v=g(p,d),x=h(await n.fs.readFile(c.known),c.known),C=c.settings;_(h(await n.fs.readFile(C),C),C);let w=s(t.executablePath,`claude-code`,n,!1);for(let e of t.events??[])S(v,e,w,!1);for(let e of t.blockingEvents??[])S(v,e,w,!0);p.hooks=v,await D(e(c.marketplace,`.claude-plugin`,`marketplace.json`),b(),n,i);for(let t of[c.source,c.cache])await D(e(t,`.claude-plugin`,`plugin.json`),{name:u,version:a,description:f},n,i),await D(e(t,`hooks`,`hooks.json`),p,n,i);return x[l]={...m(x[l]),source:{source:`directory`,path:c.marketplace},installLocation:c.marketplace,lastUpdated:t.now??new Date().toISOString().replace(/\.\d{3}Z$/,`Z`)},await D(c.known,x,n,i),await O(c.settings,c.marketplace,!0,o[`claude-code`].events,n,i),await F(n,c.base,i,c.cache),await r(n.fs,e(c.marketplace,`plugins`,`gk`),i,{recursive:!0}),await r(n.fs,e(c.base,`plugins`,`cache`,l,`gk`),i,{recursive:!0}),await L(n,c.base,a,i),{host:`claude-code`,path:d,events:[...t.events??[]],blockingEvents:[...t.blockingEvents??[]],files:i}}async function H(t,n){let i=[],a=y(n,`0.0.0-dev`),s=e(a.source,`hooks`,`hooks.json`),c=await n.fs.readFile(s),f=h(c,s),v=g(f,s),b=h(await n.fs.readFile(a.settings),a.settings);_(b,a.settings);let x=await n.fs.readFile(a.known),S={};try{x!==null&&(S=h(x,a.known))}catch{}let E=t??Object.keys(v),A=t??o[`claude-code`].events,j=w(v).filter(e=>E.includes(e));C(v,E),f.hooks=v;let M=w(v),I={host:`claude-code`,path:s,removedEvents:j,remainingEvents:M,files:i};j.length||(I.reason=c===null?`file-missing`:`not-installed`);let L=B(f,v);if(!j.length){if(t!==void 0||L)return I;let r=await P(a.base,n),i=!!r&&N(r[1],p,n.platform).length>0,o=e(a.base,`plugins`,`cache`,l,u);if(!(d in m(b.enabledPlugins)||l in S||i||T(b,A)||(await n.fs.readDir(o))?.length))return I}else if(M.length||L){await D(s,f,n,i);let t=e(a.base,`plugins`,`cache`,l,u);for(let r of await n.fs.readDir(t)??[])await D(e(t,r,`hooks`,`hooks.json`),f,n,i);return I}let V=await z(n,a);return V?await k(a.settings,A,n,i):await O(a.settings,a.marketplace,!1,A,n,i),l in S&&!V&&(delete S[l],Object.keys(S).length?await D(a.known,S,n,i):await r(n.fs,a.known,i)),await R(n,a,i,V),await F(n,a.base,i),I}export{V as installClaudeHooks,H as uninstallClaudeHooks};
1
+ import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{addAtPointer as r,removeAtPointer as i,removeAtPointerAndEmptyParent as a}from"../mcp/jsonc-edit.js";import{HookConfigError as o,hookErrorCode as s}from"./errors.js";import{eventManifest as c}from"./events.js";import{command as l}from"./writers.js";import{parse as u}from"jsonc-parser";const d=`gitkraken`,f=`gitkraken-hooks`,p=`${f}@${d}`,m=`Live AI session tracking for GitKraken products. See what Claude is doing in real time — current tool, active prompt, subagents — and optionally approve or deny tool calls from the UI.`,h=[`gitkraken@local`,`gk@gitkraken`];function g(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:{}}function _(e,t){if(e===null||e.trim()===``)return{};let n=[],r=u(e,n,{allowTrailingComma:!0});if(n.length||!r||typeof r!=`object`||Array.isArray(r))throw new o(`invalid-json`,`${t} is not valid JSON; refusing to overwrite shared configuration`,t);return r}function v(e,t){if(e.hooks===void 0)return{};if(!e.hooks||typeof e.hooks!=`object`||Array.isArray(e.hooks))throw new o(`wrong-shape`,`${t} has an invalid hooks wrapper`,t);let n=e.hooks;for(let[e,r]of Object.entries(n))if(!Array.isArray(r)||!r.every(e=>{if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e.hooks;return t===void 0||Array.isArray(t)&&t.every(e=>!!e&&typeof e==`object`&&!Array.isArray(e))}))throw new o(`wrong-shape`,`${t} has an invalid ${e} hook group`,t);return n}function y(e,t){for(let n of[`enabledPlugins`,`extraKnownMarketplaces`]){let r=e[n];if(r!==void 0&&(!r||typeof r!=`object`||Array.isArray(r)))throw new o(`wrong-shape`,`${t} has an invalid ${n} map`,t)}}function b(t){return t.paths.claudeConfigDir||e(t.paths.home,`.claude`)}function x(t,n){let r=b(t),i=e(r,`plugins`,`marketplaces`,d);return{base:r,marketplace:i,source:e(i,`plugins`,f),cache:e(r,`plugins`,`cache`,d,f,n),settings:e(r,`settings.json`),known:e(r,`plugins`,`known_marketplaces.json`)}}function S(){return{name:d,description:`Plugins from GitKraken to enhance AI coding workflows in GitKraken Desktop, GitLens, and other GitKraken products.`,owner:{name:`GitKraken`},plugins:[{name:f,source:`./plugins/${f}`,description:m}]}}function C(e){return typeof g(e).command==`string`&&g(e).command.includes(`ai hook run`)}function w(e,t,n,r){T(e,[t]);let i=Array.isArray(e[t])?[...e[t]]:[],a=i.findIndex(e=>(typeof e.matcher!=`string`||e.matcher===``)&&Array.isArray(e.hooks)&&e.hooks.length>0);a<0&&(a=i.length,i.push({matcher:``,hooks:[]}));let o={...i[a]};o.hooks=[...(Array.isArray(o.hooks)?o.hooks:[]).filter(e=>!C(e)),{type:`command`,command:n,...r?{timeout:86400}:{}}],i[a]=o,e[t]=i}function T(e,t){for(let n of t){let t=Array.isArray(e[n])?e[n]:[],r=!1,i=t.flatMap(e=>{if(!Array.isArray(e.hooks))return[e];let t=e.hooks.filter(e=>!C(e));return t.length===e.hooks.length?[e]:(r=!0,t.length?[{...e,hooks:t}]:[])});r&&(i.length?e[n]=i:delete e[n])}}function E(e,t=!1){let n=e=>C(e)&&(!t||g(e).command.endsWith(` --blocking`));return Object.keys(e).filter(t=>{let r=e[t];return Array.isArray(r)&&r.some(e=>Array.isArray(g(e).hooks)&&g(e).hooks.some(n))})}function D(e,t){let n=e.hooks;if(!n||typeof n!=`object`||Array.isArray(n))return;let r={...n};return T(r,t),Object.keys(n).every(e=>r[e]===n[e])?void 0:r}function O(e,t,n,a){let o=D(t,n);if(!o)return e;for(let[n,s]of Object.entries(t.hooks)){if(o[n]===s)continue;let t=`/hooks/${n.replace(/~/g,`~0`).replace(/\//g,`~1`)}`;e=o[n]===void 0?i(e,t,a).text:r(e,t,o[n],a)}return e}async function k(e,t,r,i){await n(r.fs,e,`${JSON.stringify(t,null,2)}\n`,i,{mode:384})}async function A(e,t,r,i){await n(e.fs,t,r.endsWith(`
2
+ `)?r:`${r}\n`,i,{mode:384})}const j={preserveSiblings:!0};function M(e,t,n,r){for(let i of n)e=a(e,`${t}/${i}`,r,j).text;return e}async function N(e,t,n,i,a,o){let s=await a.fs.readFile(e),c=_(s,e);if(y(c,e),!n&&s===null)return;let l=s===null||s.trim()===``;if(!n&&l){o.push({path:e,change:`unchanged`});return}let u=l?`{}`:O(s,c,i,e),f=`/extraKnownMarketplaces/${d}`;if(n){u=r(u,`/enabledPlugins/${p}`,!0,e,j),u=M(u,`/enabledPlugins`,h,e);let n={source:`directory`,path:t},i=g(c.extraKnownMarketplaces)[d];u=i&&typeof i==`object`&&!Array.isArray(i)?r(u,`${f}/source`,n,e,j):r(u,f,{source:n},e,j)}else u=M(u,`/enabledPlugins`,[...h,p],e),u=M(u,`/extraKnownMarketplaces`,[d],e);u===s?o.push({path:e,change:`unchanged`}):await A(a,e,u,o)}async function P(e,t,n,r){let i=await n.fs.readFile(e);if(i===null)return;let a=_(i,e);if(y(a,e),i.trim()===``){r.push({path:e,change:`unchanged`});return}let o=M(O(i,a,t,e),`/enabledPlugins`,[...h,p],e);o===i?r.push({path:e,change:`unchanged`}):await A(n,e,o,r)}function F(t){return e(t,`plugins`,`installed_plugins.json`)}function I(e,t){return t===e||t.startsWith(`${e}/`)}function L(e,t){let n=e.replaceAll(`\\`,`/`);return t===`win32`?n.toLowerCase():n}function R(e,t,n,r=``){let i=t.filter(t=>t in e).map(e=>`/${e}`),a=g(e.plugins),o=L(r,n),s=o.slice(0,o.lastIndexOf(`/`));for(let e of t){if(!(e in a))continue;if(e!==p||!o){i.push(`/plugins/${e}`);continue}let t=a[e];if(!Array.isArray(t))continue;let r=t.flatMap((e,t)=>{let r=g(e).installPath;if(typeof r!=`string`)return[];let i=L(r,n);return i!==s&&I(s,i)&&!I(o,i)?[t]:[]});r.length&&r.length===t.length?i.push(`/plugins/${e}`):i.push(...r.reverse().map(t=>`/plugins/${e}/${t}`))}return i}async function z(e,t){let n=F(e),r=await t.fs.readFile(n);if(r!==null)try{return[r,_(r,n)]}catch{return}}async function B(e,t,r,a){let o=await z(t,e);if(!o)return;let s=F(t),[c,l]=o,u=c;for(let t of R(l,[...h,p],e.platform,a))u=i(u,t,s,j).text;u!==c&&await n(e.fs,s,u,r,{mode:384})}async function V(t,n){for(let r of[e(n,`.claude-plugin`,`plugin.json`),e(n,`plugin.json`)]){let e=await t.fs.readFile(r);if(e!==null)try{let t=g(JSON.parse(e)).name;return t===f||t===`gk`}catch{return!1}}return!1}async function H(n,r,i,a){let o=e(r,`plugins`,`cache`,d,f);for(let r of await n.fs.readDir(o)??[]){let s=e(o,r);r!==i&&await V(n,s)&&await t(n.fs,s,a,{recursive:!0})}}async function U(n,r,i,a=!1){let o=[e(r.source,`.claude-plugin`,`plugin.json`),e(r.source,`hooks`,`hooks.json`)];for(let e of o)await t(n.fs,e,i);for(let a of[e(r.source,`.claude-plugin`),e(r.source,`hooks`)])(await n.fs.readDir(a))?.length||await t(n.fs,a,i,{recursive:!0});(await n.fs.readDir(r.source))?.length||await t(n.fs,r.source,i,{recursive:!0});let s=e(r.marketplace,`plugins`);(await n.fs.readDir(s))?.length||await t(n.fs,s,i,{recursive:!0});let c=e(r.base,`plugins`,`cache`,d,f);for(let r of await n.fs.readDir(c)??[]){let a=e(c,r);await t(n.fs,e(a,`.claude-plugin`,`plugin.json`),i),await t(n.fs,e(a,`hooks`,`hooks.json`),i);for(let r of[e(a,`.claude-plugin`),e(a,`hooks`)])(await n.fs.readDir(r))?.length||await t(n.fs,r,i,{recursive:!0});(await n.fs.readDir(a))?.length||await t(n.fs,a,i,{recursive:!0})}(await n.fs.readDir(c))?.length||await t(n.fs,c,i,{recursive:!0});let l=e(r.marketplace,`.claude-plugin`,`marketplace.json`),u=await n.fs.readFile(l);!a&&u?.trimEnd()===JSON.stringify(S(),null,2)&&await t(n.fs,l,i);let p=e(r.marketplace,`.claude-plugin`);!a&&!(await n.fs.readDir(p))?.length&&await t(n.fs,p,i,{recursive:!0});let m=await n.fs.readDir(r.marketplace);!a&&!m?.length&&(m===null&&i.some(e=>e.path.startsWith(`${r.marketplace}/`))?(await n.fs.remove(r.marketplace,{recursive:!0}),i.push({path:r.marketplace,change:`removed`})):await t(n.fs,r.marketplace,i,{recursive:!0}))}async function W(t,n){return(await t.fs.readDir(e(n.marketplace,`plugins`))??[]).some(e=>e!==f)}function G(e,t){return Object.keys(e).some(e=>e!==`hooks`)||Object.values(t).some(Array.isArray)}async function K(n,i){let a=[],o=n.version||`0.0.0-dev`,s=x(i,o),u=e(s.source,`hooks`,`hooks.json`),p=_(await i.fs.readFile(u),u),h=v(p,u),b=await i.fs.readFile(s.known),C=_(b,s.known),T=s.settings;y(_(await i.fs.readFile(T),T),T);let E=l(n.executablePath,`claude-code`,i,!1),D=l(n.executablePath,`claude-code`,i,!0);for(let e of n.events??[])w(h,e,E,!1);for(let e of n.blockingEvents??[])w(h,e,D,!0);p.hooks=h,await k(e(s.marketplace,`.claude-plugin`,`marketplace.json`),S(),i,a);for(let t of[s.source,s.cache])await k(e(t,`.claude-plugin`,`plugin.json`),{name:f,version:o,description:m},i,a),await k(e(t,`hooks`,`hooks.json`),p,i,a);let O={...g(C[d]),source:{source:`directory`,path:s.marketplace},installLocation:s.marketplace,lastUpdated:n.now??new Date().toISOString().replace(/\.\d{3}Z$/,`Z`)};if(b===null||b.trim()===``)await k(s.known,{[d]:O},i,a);else{let e=r(b,`/${d}`,O,s.known,j);await A(i,s.known,e,a)}return await N(s.settings,s.marketplace,!0,c[`claude-code`].events,i,a),await B(i,s.base,a,s.cache),await t(i.fs,e(s.marketplace,`plugins`,`gk`),a,{recursive:!0}),await t(i.fs,e(s.base,`plugins`,`cache`,d,`gk`),a,{recursive:!0}),await H(i,s.base,o,a),{host:`claude-code`,path:u,events:[...n.events??[]],blockingEvents:[...n.blockingEvents??[]],files:a}}async function q(r,a){let o=[],s=x(a,`0.0.0-dev`),l=e(s.source,`hooks`,`hooks.json`),u=await a.fs.readFile(l),m=_(u,l),b=v(m,l),S=_(await a.fs.readFile(s.settings),s.settings);y(S,s.settings);let C=await a.fs.readFile(s.known),w={};try{C!==null&&(w=_(C,s.known))}catch{}let O=r??Object.keys(b),A=r??c[`claude-code`].events,M=E(b).filter(e=>O.includes(e));T(b,O),m.hooks=b;let F=E(b),I={host:`claude-code`,path:l,removedEvents:M,remainingEvents:F,files:o};M.length||(I.reason=u===null?`file-missing`:`not-installed`);let L=G(m,b);if(!M.length){if(r!==void 0||L)return I;let t=await z(s.base,a),n=!!t&&R(t[1],h,a.platform).length>0,i=e(s.base,`plugins`,`cache`,d,f);if(!(p in g(S.enabledPlugins)||h.some(e=>g(S.enabledPlugins)[e]===!0)||d in w||n||D(S,A)||(await a.fs.readDir(i))?.length))return I}else if(F.length||L){await k(l,m,a,o);let t=e(s.base,`plugins`,`cache`,d,f);for(let n of await a.fs.readDir(t)??[])await k(e(t,n,`hooks`,`hooks.json`),m,a,o);return I}let V=await W(a,s);if(V?await P(s.settings,A,a,o):await N(s.settings,s.marketplace,!1,A,a,o),d in w&&!V){let e=i(C??``,`/${d}`,s.known,j).text;Object.keys(w).length>1?await n(a.fs,s.known,e,o,{mode:384}):await t(a.fs,s.known,o)}return await U(a,s,o,V),await B(a,s.base,o),I}async function J(t,n,r){let i=e(n.base,`plugins`,`cache`,d,f),a=!1;for(let n of await t.fs.readDir(i)??[]){let r=await t.fs.readFile(e(i,n,`.claude-plugin`,`plugin.json`)),o=await t.fs.readFile(e(i,n,`hooks`,`hooks.json`));r!==null&&o!==null&&(a=!0)}let o=await t.fs.readFile(e(n.source,`hooks`,`hooks.json`)),s=await t.fs.readFile(e(n.marketplace,`.claude-plugin`,`marketplace.json`)),c=g(g(g(r.extraKnownMarketplaces)[d]).source).path;return!a||o===null||s===null||typeof c!=`string`||L(c,t.platform).replace(/\/+$/,``)!==L(n.marketplace,t.platform)}async function Y(t){let n=x(t,`0.0.0-dev`),r=e(n.source,`hooks`,`hooks.json`),i,a=async(e,t)=>{try{return await t()}catch(t){return i??=s(t),e}},o=e=>a({},async()=>_(await t.fs.readFile(e),e)),c=await o(r),l=await a({},()=>v(c,r)),u=await o(n.settings),d=g(u.hooks),f=g(u.enabledPlugins),m=E(d),y=await a(void 0,()=>z(n.base,t)),b=m.length>0||h.some(e=>f[e]===!0)||!!y&&R(y[1],h,t.platform).length>0,S=f[p]===!0,C=S&&await a(!1,()=>J(t,n,u)),w=e=>[...new Set(e)],T=w([...S?E(l):[],...b?m:[]]);return{host:`claude-code`,installed:b||S,usable:(b||S)&&!i&&!C&&T.length>0,path:r,layout:b?`legacy`:S?`plugin`:`none`,events:T,blockingEvents:w([...S?E(l,!0):[],...b?E(d,!0):[]]),needsMigration:b||C,...i?{error:i}:{}}}export{Y as inspectClaudeHooks,K as installClaudeHooks,q as uninstallClaudeHooks};
@@ -1,10 +1,12 @@
1
1
  //#region src/hooks/errors.d.ts
2
- type HookConfigErrorCode = 'invalid-json' | 'wrong-shape' | 'invalid-event' | 'unsupported-host' | 'blocking-unsupported' | 'unmanaged-file';
2
+ type HookConfigErrorCode = 'invalid-json' | 'wrong-shape' | 'invalid-event' | 'unsupported-host' | 'blocking-unsupported' | 'unmanaged-file' | 'unreadable';
3
3
  /** A hook install or uninstall was refused; `path` names the file involved, when there is one. An existing file is left untouched. */
4
4
  declare class HookConfigError extends Error {
5
5
  readonly code: HookConfigErrorCode;
6
6
  readonly path?: string | undefined;
7
7
  constructor(code: HookConfigErrorCode, message: string, path?: string | undefined);
8
8
  }
9
+ /** The code `inspectHooks` reports for a failed read: the refusal's own code, or `unreadable` for anything else. */
10
+ declare function hookErrorCode(error: unknown): HookConfigErrorCode;
9
11
  //#endregion
10
- export { HookConfigError, HookConfigErrorCode };
12
+ export { HookConfigError, HookConfigErrorCode, hookErrorCode };
@@ -1 +1 @@
1
- var e=class extends Error{constructor(e,t,n){super(t),this.code=e,this.path=n,this.name=`HookConfigError`}};export{e as HookConfigError};
1
+ var e=class extends Error{constructor(e,t,n){super(t),this.code=e,this.path=n,this.name=`HookConfigError`}};function t(t){return t instanceof e?t.code:`unreadable`}export{e as HookConfigError,t as hookErrorCode};
@@ -9,9 +9,8 @@ declare const eventManifest: {
9
9
  };
10
10
  readonly opencode: {
11
11
  readonly events: readonly ["session.created", "session.updated", "session.deleted", "session.idle", "session.error", "session.status", "session.diff", "session.execution.failed", "session.compacted", "permission.asked", "permission.updated", "permission.replied", "message.updated", "message.part.updated", "tool.execute.before", "tool.execute.after", "file.edited", "todo.updated", "command.executed", "vcs.branch.updated", "installation.updated"];
12
- readonly v1OnlyEvents: readonly ["session.updated", "session.error"];
12
+ readonly v1OnlyEvents: readonly ["session.updated", "session.error", "session.idle", "session.diff", "file.edited", "permission.updated"];
13
13
  readonly v2EventAliases: {
14
- readonly 'session.status': readonly ["session.updated"];
15
14
  readonly 'session.execution.failed': readonly ["session.error"];
16
15
  };
17
16
  readonly skippedDefaultEvents: readonly ["message.updated", "message.part.updated", "installation.updated"];
@@ -20,7 +19,8 @@ declare const eventManifest: {
20
19
  };
21
20
  readonly cursor: {
22
21
  readonly events: readonly ["sessionStart", "sessionEnd", "preToolUse", "postToolUse", "postToolUseFailure", "subagentStart", "subagentStop", "beforeShellExecution", "afterShellExecution", "beforeMCPExecution", "afterMCPExecution", "beforeReadFile", "afterFileEdit", "beforeSubmitPrompt", "preCompact", "stop", "afterAgentResponse", "afterAgentThought", "beforeTabFileRead", "afterTabFileEdit", "workspaceOpen"];
23
- readonly skippedDefaultEvents: readonly ["beforeTabFileRead", "afterTabFileEdit", "workspaceOpen"];
22
+ readonly skippedDefaultEvents: readonly ["afterAgentThought", "beforeTabFileRead", "afterTabFileEdit", "workspaceOpen"];
23
+ readonly promptSubmitEvent: "beforeSubmitPrompt";
24
24
  readonly sessionEndEvent: "sessionEnd";
25
25
  readonly sessionStartEvent: "sessionStart";
26
26
  };
@@ -39,16 +39,19 @@ declare const eventManifest: {
39
39
  readonly sessionEndEvent: "SessionEnd";
40
40
  readonly sessionStartEvent: "SessionStart";
41
41
  readonly windowsShell: "powershell";
42
+ readonly blockingWait: "5s";
42
43
  };
43
44
  readonly antigravity: {
44
- readonly events: readonly ["PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"];
45
+ readonly events: readonly ["SessionStart", "PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"];
45
46
  readonly skippedDefaultEvents: readonly [];
46
- readonly sessionEndEvent: "Stop";
47
- readonly sessionStartEvent: "";
47
+ readonly backfillEvents: readonly ["SessionStart"];
48
+ readonly sessionEndEvent: "";
49
+ readonly sessionStartEvent: "SessionStart";
50
+ readonly eventFlag: true;
48
51
  };
49
52
  readonly pi: {
50
- readonly events: readonly ["project_trust", "resources_discover", "session_start", "session_info_changed", "session_before_switch", "session_before_fork", "session_before_compact", "session_compact", "session_compact_failed", "session_before_tree", "session_tree", "session_shutdown", "before_agent_start", "agent_start", "agent_end", "agent_settled", "ui_prompt_start", "ui_prompt_end", "turn_start", "turn_end", "message_start", "message_update", "message_end", "tool_call", "tool_result", "tool_execution_start", "tool_execution_update", "tool_execution_end", "context", "before_provider_headers", "before_provider_request", "after_provider_response", "model_select", "thinking_level_select", "user_bash", "input"];
51
- readonly skippedDefaultEvents: readonly ["resources_discover", "message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "context", "before_provider_headers", "before_provider_request", "after_provider_response", "input"];
53
+ readonly events: readonly ["project_trust", "resources_discover", "session_start", "session_info_changed", "session_before_switch", "session_before_fork", "session_before_compact", "session_compact", "session_compact_failed", "session_before_tree", "session_tree", "session_shutdown", "before_agent_start", "agent_start", "agent_end", "agent_before_settle", "agent_settled", "ui_prompt_start", "ui_prompt_end", "turn_start", "turn_end", "message_start", "message_update", "message_end", "tool_call", "tool_result", "tool_execution_start", "tool_execution_update", "tool_execution_end", "context", "cache_warming_decision", "context_with_system", "before_provider_headers", "before_provider_request", "after_provider_response", "provider_stream_event", "model_select", "thinking_level_select", "user_bash", "input"];
54
+ readonly skippedDefaultEvents: readonly ["resources_discover", "message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "context", "cache_warming_decision", "context_with_system", "before_provider_headers", "before_provider_request", "after_provider_response", "provider_stream_event", "input"];
52
55
  readonly sessionEndEvent: "session_shutdown";
53
56
  readonly sessionStartEvent: "session_start";
54
57
  };
@@ -1 +1 @@
1
- const e={"claude-code":{events:`SessionStart.Setup.InstructionsLoaded.UserPromptSubmit.UserPromptExpansion.MessageDisplay.PreToolUse.PermissionRequest.PermissionDenied.PostToolUse.PostToolUseFailure.PostToolBatch.Notification.SubagentStart.SubagentStop.TaskCreated.TaskCompleted.Stop.StopFailure.TeammateIdle.ConfigChange.CwdChanged.DirectoryAdded.FileChanged.WorktreeCreate.WorktreeRemove.PreCompact.PostCompact.PreModelSwitch.PostModelSwitch.Elicitation.ElicitationResult.SessionEnd`.split(`.`),skippedDefaultEvents:[`Setup`,`MessageDisplay`,`FileChanged`,`WorktreeCreate`,`WorktreeRemove`],backfillEvents:[`PostModelSwitch`],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`},opencode:{events:[`session.created`,`session.updated`,`session.deleted`,`session.idle`,`session.error`,`session.status`,`session.diff`,`session.execution.failed`,`session.compacted`,`permission.asked`,`permission.updated`,`permission.replied`,`message.updated`,`message.part.updated`,`tool.execute.before`,`tool.execute.after`,`file.edited`,`todo.updated`,`command.executed`,`vcs.branch.updated`,`installation.updated`],v1OnlyEvents:[`session.updated`,`session.error`],v2EventAliases:{"session.status":[`session.updated`],"session.execution.failed":[`session.error`]},skippedDefaultEvents:[`message.updated`,`message.part.updated`,`installation.updated`],sessionEndEvent:`session.deleted`,sessionStartEvent:`session.created`},cursor:{events:[`sessionStart`,`sessionEnd`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`beforeSubmitPrompt`,`preCompact`,`stop`,`afterAgentResponse`,`afterAgentThought`,`beforeTabFileRead`,`afterTabFileEdit`,`workspaceOpen`],skippedDefaultEvents:[`beforeTabFileRead`,`afterTabFileEdit`,`workspaceOpen`],sessionEndEvent:`sessionEnd`,sessionStartEvent:`sessionStart`},codex:{events:[`SessionStart`,`UserPromptSubmit`,`PreToolUse`,`PermissionRequest`,`PostToolUse`,`PreCompact`,`PostCompact`,`SubagentStart`,`SubagentStop`,`Stop`,`Interrupt`,`SessionEnd`],skippedDefaultEvents:[],backfillEvents:[`SessionEnd`,`Interrupt`],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,windowsShell:`powershell`},copilot:{events:[`SessionStart`,`SessionEnd`,`UserPromptSubmit`,`PreToolUse`,`PostToolUse`,`PostToolUseFailure`,`Stop`,`SubagentStop`,`ErrorOccurred`,`PreCompact`,`Notification`,`PermissionRequest`],legacyEvents:[`sessionStart`,`sessionEnd`,`userPromptSubmitted`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`agentStop`,`subagentStart`,`subagentStop`,`errorOccurred`,`preCompact`,`notification`,`permissionRequest`],skippedDefaultEvents:[],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,windowsShell:`powershell`},antigravity:{events:[`PreToolUse`,`PostToolUse`,`PreInvocation`,`PostInvocation`,`Stop`],skippedDefaultEvents:[],sessionEndEvent:`Stop`,sessionStartEvent:``},pi:{events:`project_trust.resources_discover.session_start.session_info_changed.session_before_switch.session_before_fork.session_before_compact.session_compact.session_compact_failed.session_before_tree.session_tree.session_shutdown.before_agent_start.agent_start.agent_end.agent_settled.ui_prompt_start.ui_prompt_end.turn_start.turn_end.message_start.message_update.message_end.tool_call.tool_result.tool_execution_start.tool_execution_update.tool_execution_end.context.before_provider_headers.before_provider_request.after_provider_response.model_select.thinking_level_select.user_bash.input`.split(`.`),skippedDefaultEvents:[`resources_discover`,`message_start`,`message_update`,`message_end`,`tool_execution_start`,`tool_execution_update`,`tool_execution_end`,`context`,`before_provider_headers`,`before_provider_request`,`after_provider_response`,`input`],sessionEndEvent:`session_shutdown`,sessionStartEvent:`session_start`},augment:{events:[`SessionStart`,`SessionEnd`,`PreToolUse`,`PostToolUse`,`Stop`,`Notification`,`PromptSubmit`],skippedDefaultEvents:[],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,blockingTimeoutMultiplier:1e3,promptSubmitEvent:`PromptSubmit`},grok:{events:[`SessionStart`,`SessionEnd`,`UserPromptSubmit`,`PreToolUse`,`PostToolUse`,`PostToolUseFailure`,`PermissionDenied`,`Stop`,`StopFailure`,`StopCancelled`,`Notification`,`SubagentStart`,`SubagentStop`,`PreCompact`,`PostCompact`],skippedDefaultEvents:[],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,windowsShell:`powershell`}};export{e as eventManifest};
1
+ const e={"claude-code":{events:`SessionStart.Setup.InstructionsLoaded.UserPromptSubmit.UserPromptExpansion.MessageDisplay.PreToolUse.PermissionRequest.PermissionDenied.PostToolUse.PostToolUseFailure.PostToolBatch.Notification.SubagentStart.SubagentStop.TaskCreated.TaskCompleted.Stop.StopFailure.TeammateIdle.ConfigChange.CwdChanged.DirectoryAdded.FileChanged.WorktreeCreate.WorktreeRemove.PreCompact.PostCompact.PreModelSwitch.PostModelSwitch.Elicitation.ElicitationResult.SessionEnd`.split(`.`),skippedDefaultEvents:[`Setup`,`MessageDisplay`,`FileChanged`,`WorktreeCreate`,`WorktreeRemove`],backfillEvents:[`PostModelSwitch`],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`},opencode:{events:[`session.created`,`session.updated`,`session.deleted`,`session.idle`,`session.error`,`session.status`,`session.diff`,`session.execution.failed`,`session.compacted`,`permission.asked`,`permission.updated`,`permission.replied`,`message.updated`,`message.part.updated`,`tool.execute.before`,`tool.execute.after`,`file.edited`,`todo.updated`,`command.executed`,`vcs.branch.updated`,`installation.updated`],v1OnlyEvents:[`session.updated`,`session.error`,`session.idle`,`session.diff`,`file.edited`,`permission.updated`],v2EventAliases:{"session.execution.failed":[`session.error`]},skippedDefaultEvents:[`message.updated`,`message.part.updated`,`installation.updated`],sessionEndEvent:`session.deleted`,sessionStartEvent:`session.created`},cursor:{events:[`sessionStart`,`sessionEnd`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`subagentStart`,`subagentStop`,`beforeShellExecution`,`afterShellExecution`,`beforeMCPExecution`,`afterMCPExecution`,`beforeReadFile`,`afterFileEdit`,`beforeSubmitPrompt`,`preCompact`,`stop`,`afterAgentResponse`,`afterAgentThought`,`beforeTabFileRead`,`afterTabFileEdit`,`workspaceOpen`],skippedDefaultEvents:[`afterAgentThought`,`beforeTabFileRead`,`afterTabFileEdit`,`workspaceOpen`],promptSubmitEvent:`beforeSubmitPrompt`,sessionEndEvent:`sessionEnd`,sessionStartEvent:`sessionStart`},codex:{events:[`SessionStart`,`UserPromptSubmit`,`PreToolUse`,`PermissionRequest`,`PostToolUse`,`PreCompact`,`PostCompact`,`SubagentStart`,`SubagentStop`,`Stop`,`Interrupt`,`SessionEnd`],skippedDefaultEvents:[],backfillEvents:[`SessionEnd`,`Interrupt`],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,windowsShell:`powershell`},copilot:{events:[`SessionStart`,`SessionEnd`,`UserPromptSubmit`,`PreToolUse`,`PostToolUse`,`PostToolUseFailure`,`Stop`,`SubagentStop`,`ErrorOccurred`,`PreCompact`,`Notification`,`PermissionRequest`],legacyEvents:[`sessionStart`,`sessionEnd`,`userPromptSubmitted`,`preToolUse`,`postToolUse`,`postToolUseFailure`,`agentStop`,`subagentStart`,`subagentStop`,`errorOccurred`,`preCompact`,`notification`,`permissionRequest`],skippedDefaultEvents:[],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,windowsShell:`powershell`,blockingWait:`5s`},antigravity:{events:[`SessionStart`,`PreToolUse`,`PostToolUse`,`PreInvocation`,`PostInvocation`,`Stop`],skippedDefaultEvents:[],backfillEvents:[`SessionStart`],sessionEndEvent:``,sessionStartEvent:`SessionStart`,eventFlag:!0},pi:{events:`project_trust.resources_discover.session_start.session_info_changed.session_before_switch.session_before_fork.session_before_compact.session_compact.session_compact_failed.session_before_tree.session_tree.session_shutdown.before_agent_start.agent_start.agent_end.agent_before_settle.agent_settled.ui_prompt_start.ui_prompt_end.turn_start.turn_end.message_start.message_update.message_end.tool_call.tool_result.tool_execution_start.tool_execution_update.tool_execution_end.context.cache_warming_decision.context_with_system.before_provider_headers.before_provider_request.after_provider_response.provider_stream_event.model_select.thinking_level_select.user_bash.input`.split(`.`),skippedDefaultEvents:[`resources_discover`,`message_start`,`message_update`,`message_end`,`tool_execution_start`,`tool_execution_update`,`tool_execution_end`,`context`,`cache_warming_decision`,`context_with_system`,`before_provider_headers`,`before_provider_request`,`after_provider_response`,`provider_stream_event`,`input`],sessionEndEvent:`session_shutdown`,sessionStartEvent:`session_start`},augment:{events:[`SessionStart`,`SessionEnd`,`PreToolUse`,`PostToolUse`,`Stop`,`Notification`,`PromptSubmit`],skippedDefaultEvents:[],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,blockingTimeoutMultiplier:1e3,promptSubmitEvent:`PromptSubmit`},grok:{events:[`SessionStart`,`SessionEnd`,`UserPromptSubmit`,`PreToolUse`,`PostToolUse`,`PostToolUseFailure`,`PermissionDenied`,`Stop`,`StopFailure`,`StopCancelled`,`Notification`,`SubagentStart`,`SubagentStop`,`PreCompact`,`PostCompact`],skippedDefaultEvents:[],sessionEndEvent:`SessionEnd`,sessionStartEvent:`SessionStart`,windowsShell:`powershell`}};export{e as eventManifest};
@@ -0,0 +1,9 @@
1
+ import { AgentHostConfig } from "../config.js";
2
+ import { HookHost } from "./events.js";
3
+ import { HookInspection } from "./results.js";
4
+
5
+ //#region src/hooks/inspect.d.ts
6
+ /** Read-only: whether GitKraken hooks are registered for `host`. A corrupt or unreadable file sets an `error` code, never a throw. */
7
+ declare function inspectHooks(host: HookHost, config: AgentHostConfig): Promise<HookInspection>;
8
+ //#endregion
9
+ export { inspectHooks };
@@ -0,0 +1 @@
1
+ import{eventManifest as e}from"./events.js";import{inspectJsonHooks as t}from"./writers.js";import{inspectClaudeHooks as n}from"./claude.js";import{inspectMarkerHooks as r}from"./templates.js";async function i(i,a){return Object.hasOwn(e,i)?i===`claude-code`?n(a):i===`opencode`||i===`pi`?r(i,a):t(i,a):{host:i,installed:!1,usable:!1,path:``,layout:`none`,events:[],blockingEvents:[],needsMigration:!1,error:`unsupported-host`}}export{i as inspectHooks};
@@ -1 +1 @@
1
- import{HookConfigError as e}from"./errors.js";import{eventManifest as t}from"./events.js";import{installJsonHooks as n,uninstallJsonHooks as r}from"./writers.js";import{installClaudeHooks as i,uninstallClaudeHooks as a}from"./claude.js";import{installMarkerHooks as o,uninstallMarkerHooks as s}from"./templates.js";function c(n){let r=t[n.host],i=n.events?.length||n.blockingEvents?.length?[...n.events??[]]:r.events.filter(e=>!r.skippedDefaultEvents.includes(e)),a=[...n.blockingEvents??[]];for(let t of[...i,...a])if(!r.events.includes(t))throw new e(`invalid-event`,`${t} is not valid for ${n.host}`);return{events:i,blockingEvents:a}}async function l(e,t){let{events:r,blockingEvents:a}=c(e);return e.host===`claude-code`?i({executablePath:e.executablePath,events:r,blockingEvents:a,version:e.version},t):e.host===`opencode`||e.host===`pi`?o({host:e.host,executablePath:e.executablePath,events:r,blockingEvents:a},t):n({...e,events:r,blockingEvents:a},t)}async function u(n,i,o){if(o){for(let r of o)if(!t[n].events.includes(r))throw new e(`invalid-event`,`${r} is not valid for ${n}`)}let c=o?.length?o:t[n].events;return n===`claude-code`?a(o?.length?c:void 0,i):n===`opencode`||n===`pi`?s(n,c,i):r(n,i,c)}export{l as installHooks,u as uninstallHooks};
1
+ import{eventManifest as e}from"./events.js";import{selectHookEvents as t,validateHookEvents as n}from"./validate.js";import{installJsonHooks as r,uninstallJsonHooks as i}from"./writers.js";import{installClaudeHooks as a,uninstallClaudeHooks as o}from"./claude.js";import{installMarkerHooks as s,uninstallMarkerHooks as c}from"./templates.js";async function l(e,n){if(e.host===`claude-code`){let{events:r,blockingEvents:i}=t(e);return a({executablePath:e.executablePath,events:r,blockingEvents:i,version:e.version},n)}if(e.host===`opencode`||e.host===`pi`){let{events:r,blockingEvents:i}=t(e);return s({host:e.host,executablePath:e.executablePath,events:r,blockingEvents:i},n)}return r(e,n)}async function u(t,r,a){a&&n(t,a);let s=a?.length?a:e[t].events;return t===`claude-code`?o(a?.length?s:void 0,r):t===`opencode`||t===`pi`?c(t,s,r):i(t,r,s)}export{l as installHooks,u as uninstallHooks};
@@ -0,0 +1,26 @@
1
+ import { AgentHostConfig } from "../config.js";
2
+ import { HookHost } from "./events.js";
3
+ import { ClientType } from "../mcp/clients.js";
4
+
5
+ //#region src/hooks/registry.d.ts
6
+ /** Per-host hook registry data, mirroring gkcli's `aihook` client registry. */
7
+ interface HookClientInfo {
8
+ displayName: string;
9
+ /** Binary names looked up on `PATH`, in order; the first one found is the executable. */
10
+ binaryNames: readonly string[];
11
+ /** Set only for hosts with no MCP registry row to take a type from. */
12
+ clientType?: ClientType;
13
+ /** The host cannot honor a blocking decision, so blocking events are refused at install. */
14
+ noBlocking: boolean;
15
+ /** Events where any GitKraken entry breaks the host; the manifest lists them in `skippedDefaultEvents`, so a default install omits them. */
16
+ unsafeEvents: readonly string[];
17
+ /** How long a blocking entry waits for a decision (a Go duration) before the host's own UI takes over. */
18
+ blockingWait?: string;
19
+ }
20
+ /** Every hook host, in the manifest's order (gkcli's registry order). */
21
+ declare const hookHosts: readonly HookHost[];
22
+ declare function hookClientInfo(host: HookHost): HookClientInfo;
23
+ /** The first of the host's binary names that `config.resolveExecutable` finds, like Go's `ExecutablePath`. */
24
+ declare function executableForHook(host: HookHost, config: AgentHostConfig): string | undefined;
25
+ //#endregion
26
+ export { HookClientInfo, executableForHook, hookClientInfo, hookHosts };
@@ -0,0 +1 @@
1
+ import{eventManifest as e}from"./events.js";const t=Object.keys(e),n={"claude-code":{displayName:`Claude Code`,binaryNames:[`claude`,`claude-code`],noBlocking:!1,unsafeEvents:[]},opencode:{displayName:`OpenCode`,binaryNames:[`opencode`],noBlocking:!0,unsafeEvents:[]},cursor:{displayName:`Cursor`,binaryNames:[`cursor`,`cursor-agent`],noBlocking:!0,unsafeEvents:[`afterAgentThought`]},codex:{displayName:`Codex`,binaryNames:[`codex`],noBlocking:!1,unsafeEvents:[]},copilot:{displayName:`GitHub Copilot CLI`,binaryNames:[`copilot`],noBlocking:!1,unsafeEvents:[],blockingWait:`5s`},antigravity:{displayName:`Antigravity`,binaryNames:[`antigravity`,`agy`],noBlocking:!1,unsafeEvents:[]},pi:{displayName:`Pi`,binaryNames:[`pi`],clientType:`cli`,noBlocking:!0,unsafeEvents:[]},augment:{displayName:`Augment`,binaryNames:[`auggie`],clientType:`cli`,noBlocking:!1,unsafeEvents:[]},grok:{displayName:`Grok Build`,binaryNames:[`grok`],clientType:`cli`,noBlocking:!1,unsafeEvents:[]}};function r(e){if(!Object.hasOwn(n,e))throw Error(`Unknown hook host: ${e}`);return n[e]}function i(e,t){if(Object.hasOwn(n,e))for(let r of n[e].binaryNames){let e=t.resolveExecutable?.(r);if(e)return e}}export{i as executableForHook,r as hookClientInfo,t as hookHosts};
@@ -1,5 +1,6 @@
1
1
  import { HookHost } from "./events.js";
2
2
  import { FileChange } from "../file-change.js";
3
+ import { HookConfigErrorCode } from "./errors.js";
3
4
 
4
5
  //#region src/hooks/results.d.ts
5
6
  interface HookInstallResult {
@@ -26,5 +27,26 @@ interface HookUninstallResult {
26
27
  /** Set when no GitKraken hook was removed: the hooks file does not exist, or holds none for the requested events. */
27
28
  reason?: 'file-missing' | 'not-installed';
28
29
  }
30
+ type HookLayout = 'none' | 'file' | 'plugin' | 'legacy';
31
+ interface HookInspection {
32
+ host: HookHost;
33
+ /** Whether GitKraken hooks are registered, like gkcli's `IsInstalled`; Claude counts an enabled plugin even when its files are broken. */
34
+ installed: boolean;
35
+ /** `installed`, readable and intact (no `error`, no missing Claude plugin files) and carrying at least one event.
36
+ * A legacy Claude plugin's own files are not checked, so legacy installs are usable only with settings.json hooks. */
37
+ usable: boolean;
38
+ /** The hooks file the agent reads (Claude: the marketplace plugin's hooks.json). */
39
+ path: string;
40
+ /** `file` for a dedicated hooks or relay file, `plugin` for Claude's current plugin, `legacy` for any older Claude layout, `none` when not installed. */
41
+ layout: HookLayout;
42
+ /** Every event that carries a GitKraken hook, blocking ones included. */
43
+ events: string[];
44
+ /** The subset of `events` registered as blocking. */
45
+ blockingEvents: string[];
46
+ /** Legacy Claude state or drift, or Copilot entries under replaced event names, that a reinstall would move. */
47
+ needsMigration: boolean;
48
+ /** Set when a file could not be read or parsed; the rest of the result then reflects what could still be read. */
49
+ error?: HookConfigErrorCode;
50
+ }
29
51
  //#endregion
30
- export { HookInstallResult, HookUninstallResult };
52
+ export { HookInspection, HookInstallResult, HookLayout, HookUninstallResult };
@@ -1,5 +1,5 @@
1
1
  import { AgentHostConfig } from "../config.js";
2
- import { HookInstallResult, HookUninstallResult } from "./results.js";
2
+ import { HookInspection, HookInstallResult, HookUninstallResult } from "./results.js";
3
3
 
4
4
  //#region src/hooks/templates.d.ts
5
5
  type MarkerHost = 'opencode' | 'pi';
@@ -11,5 +11,7 @@ interface MarkerHookRequest {
11
11
  }
12
12
  declare function installMarkerHooks(request: MarkerHookRequest, config: AgentHostConfig): Promise<HookInstallResult>;
13
13
  declare function uninstallMarkerHooks(host: MarkerHost, events: readonly string[] | undefined, config: AgentHostConfig): Promise<HookUninstallResult>;
14
+ /** Read-only: reports the events recorded in a managed relay file's header without ever writing it. */
15
+ declare function inspectMarkerHooks(host: MarkerHost, config: AgentHostConfig): Promise<HookInspection>;
14
16
  //#endregion
15
- export { MarkerHookRequest, MarkerHost, installMarkerHooks, uninstallMarkerHooks };
17
+ export { MarkerHookRequest, MarkerHost, inspectMarkerHooks, installMarkerHooks, uninstallMarkerHooks };
@@ -1,11 +1,14 @@
1
- import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{HookConfigError as r}from"./errors.js";const i=`// gk-managed:`;function a(t,n){return e(t===`opencode`?e(n.paths.configHome,`opencode`,`plugins`):e(n.paths.piAgentDir||e(n.paths.home,`.pi`,`agent`),`extensions`),`gk-hooks.js`)}function o(e){return e?.split(`
2
- `,1)[0].startsWith(i)??!1}function s(e){if(!o(e))return;let t=e.split(`
3
- `,1)[0];try{let e=JSON.parse(t.slice(14));if(e&&typeof e==`object`&&Array.isArray(e.events)){let t=e;return{events:t.events.filter(e=>typeof e==`string`),executable:typeof t.executable==`string`?t.executable:``,hostMajor:typeof t.hostMajor==`number`?t.hostMajor:0}}}catch{}}function c(e,t,n,r){let a={events:t,executable:n,version:e===`opencode`?5:1};e===`opencode`&&r&&(a.hostMajor=r);let o=e===`opencode`?`import { spawn } from "node:child_process";
1
+ import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{HookConfigError as r,hookErrorCode as i}from"./errors.js";import{eventManifest as a}from"./events.js";import{assertBlockingSupported as o}from"./validate.js";const s=`// gk-managed:`;function c(t,n){return e(t===`opencode`?e(n.paths.configHome,`opencode`,`plugins`):e(n.paths.piAgentDir||e(n.paths.home,`.pi`,`agent`),`extensions`),`gk-hooks.js`)}function l(e){return e?.split(`
2
+ `,1)[0].startsWith(s)??!1}function u(e){if(!l(e))return;let t=e.split(`
3
+ `,1)[0];try{let e=JSON.parse(t.slice(14));if(e&&typeof e==`object`&&Array.isArray(e.events)){let t=e;return{events:t.events.filter(e=>typeof e==`string`),executable:typeof t.executable==`string`?t.executable:``,hostMajor:typeof t.hostMajor==`number`?t.hostMajor:0}}}catch{}}function d(e,t,n,r){let i={events:t,executable:n,version:e===`opencode`?8:3};e===`opencode`&&r&&(i.hostMajor=r);let o=e===`opencode`?`import { spawn } from "node:child_process";
4
+ import { randomBytes } from "node:crypto";
4
5
 
5
6
  const EVENTS = new Set({{.EventsJSON}});
6
7
  const EXECUTABLE = {{.ExecJSON}};
7
8
  const HOST = {{.HostJSON}};
8
9
  const MAX_HOOK_INPUT_BYTES = {{.MaxInputBytesJSON}};
10
+ const RELAY_INSTANCE = process.pid + "-" + randomBytes(8).toString("hex");
11
+ let relaySeq = 0;
9
12
 
10
13
  function getString(value) {
11
14
  return typeof value === "string" && value.length > 0 ? value : "";
@@ -182,11 +185,14 @@ const setup = async (ctx) => {
182
185
  // this v2-capable template.
183
186
  export default { id: "gk-hooks", server, setup };
184
187
  `:`import { spawn } from "node:child_process";
188
+ import { randomBytes } from "node:crypto";
185
189
 
186
190
  const EVENTS = new Set({{.EventsJSON}});
187
191
  const EXECUTABLE = {{.ExecJSON}};
188
192
  const HOST = {{.HostJSON}};
189
193
  const MAX_HOOK_INPUT_BYTES = {{.MaxInputBytesJSON}};
194
+ const RELAY_INSTANCE = process.pid + "-" + randomBytes(8).toString("hex");
195
+ let relaySeq = 0;
190
196
 
191
197
  function getString(value) {
192
198
  return typeof value === "string" && value.length > 0 ? value : "";
@@ -224,18 +230,20 @@ export default function (pi) {
224
230
  });
225
231
  }
226
232
  }
227
- `,s={EventsJSON:JSON.stringify(t),ExecJSON:JSON.stringify(n),HostJSON:JSON.stringify(e),MaxInputBytesJSON:`1048576`,V1OnlyEventsJSON:JSON.stringify([`session.updated`,`session.error`]),V2EventAliasesJSON:JSON.stringify({"session.execution.failed":[`session.error`],"session.status":[`session.updated`]})},c=e===`opencode`?`
233
+ `,c={EventsJSON:JSON.stringify(t),ExecJSON:JSON.stringify(n),HostJSON:JSON.stringify(e),MaxInputBytesJSON:`1048576`,V1OnlyEventsJSON:JSON.stringify(a.opencode.v1OnlyEvents),V2EventAliasesJSON:JSON.stringify(a.opencode.v2EventAliases)},l=e===`opencode`?`
228
234
 
229
235
  const V1_ONLY_EVENTS`:`
230
236
 
231
237
 
232
238
 
233
- function sessionIdOf`,l=o.indexOf(c);if(l<0)throw Error(`missing ${e} relay insertion point`);let u=o.slice(0,l)+`
239
+ function sessionIdOf`,u=o.indexOf(l);if(u<0)throw Error(`missing ${e} relay insertion point`);let d=o.slice(0,u)+`
234
240
 
235
- function buildHookInput(eventName, sessionID, cwd, payload) {
241
+ function buildHookInput(eventName, sessionID, cwd, payload, seq) {
236
242
  const toolName = extractToolName(payload);
237
243
  const hookInput = {
238
244
  hook_event_name: eventName,
245
+ relay_instance: RELAY_INSTANCE,
246
+ relay_seq: seq,
239
247
  session_id: sessionID || "",
240
248
  cwd,
241
249
  tool_name: toolName || null,
@@ -244,6 +252,7 @@ function buildHookInput(eventName, sessionID, cwd, payload) {
244
252
  reason: null,
245
253
  agent_id: null,
246
254
  agent_type: null,
255
+ host_pid: process.pid,
247
256
  hook_payload: payload || null,
248
257
  };
249
258
 
@@ -261,8 +270,10 @@ function buildHookInput(eventName, sessionID, cwd, payload) {
261
270
 
262
271
  function runHook(eventName, sessionID, cwd, payload, aliases = []) {
263
272
  if (!EVENTS.has(eventName) && !aliases.some((alias) => EVENTS.has(alias))) return;
273
+ relaySeq += 1;
274
+ const seq = relaySeq;
264
275
  try {
265
- const input = buildHookInput(eventName, sessionID, cwd, payload);
276
+ const input = buildHookInput(eventName, sessionID, cwd, payload, seq);
266
277
  const child = spawn(EXECUTABLE, ["ai", "hook", "run", "--host", HOST], {
267
278
  stdio: ["pipe", "ignore", "ignore"],
268
279
  windowsHide: true,
@@ -274,5 +285,5 @@ function runHook(eventName, sessionID, cwd, payload, aliases = []) {
274
285
  } catch (_) {
275
286
  // Hooks must never block the host
276
287
  }
277
- }`+o.slice(l);return i+JSON.stringify(a)+`
278
- `+u.replace(/{{\.(\w+)}}/g,(e,t)=>s[t]??``)}async function l(n,r,i){let s=a(n,r),c=s.slice(0,s.lastIndexOf(`/`));for(let n of await r.fs.readDir(c)??[]){if(n===`gk-hooks.js`||!n.startsWith(`gk-hooks.`))continue;let a=e(c,n);o(await r.fs.readFile(a))&&await t(r.fs,a,i)}}async function u(e,t){let i=a(e.host,t);if(e.blockingEvents?.length)throw new r(`blocking-unsupported`,`${e.host} does not support blocking hooks`,i);let u=await t.fs.readFile(i),d=s(u);if(u!==null&&!o(u))throw new r(`unmanaged-file`,`${i} already exists and was not written by GitKraken`,i);let f=[...new Set([...d?.events??[],...e.events??[]])].sort(),p=e.host===`opencode`?t.openCodeMajor??0:0,m=[];return await n(t.fs,i,c(e.host,f,e.executablePath.replace(/\\/g,`/`),p),m,{mode:384}),await l(e.host,t,m),{host:e.host,path:i,events:[...e.events??[]],blockingEvents:[],files:m}}async function d(e,r,i){let o=a(e,i),u=[],d=await i.fs.readFile(o),f=s(d),p=[],m=[];f&&(p=f.events.filter(e=>(r??f.events).includes(e)),m=f.events.filter(e=>!p.includes(e)).sort(),m.length?await n(i.fs,o,c(e,m,f.executable,f.hostMajor),u,{mode:384}):await t(i.fs,o,u)),await l(e,i,u);let h={host:e,path:o,removedEvents:p,remainingEvents:m,files:u};return p.length||(h.reason=d===null?`file-missing`:`not-installed`),h}export{u as installMarkerHooks,d as uninstallMarkerHooks};
288
+ }`+o.slice(u);return s+JSON.stringify(i)+`
289
+ `+d.replace(/{{\.(\w+)}}/g,(e,t)=>c[t]??``)}async function f(n,r,i){let a=c(n,r),o=a.slice(0,a.lastIndexOf(`/`));for(let n of await r.fs.readDir(o)??[]){if(n===`gk-hooks.js`||!n.startsWith(`gk-hooks.`))continue;let a=e(o,n);l(await r.fs.readFile(a))&&await t(r.fs,a,i)}}async function p(e,t){let i=c(e.host,t);o(e.host,e.blockingEvents,i);let a=await t.fs.readFile(i),s=u(a);if(a!==null&&!l(a))throw new r(`unmanaged-file`,`${i} already exists and was not written by GitKraken`,i);let p=[...new Set([...s?.events??[],...e.events??[]])].sort(),m=e.host===`opencode`?t.openCodeMajor??0:0,h=[];return await n(t.fs,i,d(e.host,p,e.executablePath.replace(/\\/g,`/`),m),h,{mode:384}),await f(e.host,t,h),{host:e.host,path:i,events:[...e.events??[]],blockingEvents:[],files:h}}async function m(e,r,i){let a=c(e,i),o=[],s=await i.fs.readFile(a),l=u(s),p=[],m=[];l&&(p=l.events.filter(e=>(r??l.events).includes(e)),m=l.events.filter(e=>!p.includes(e)).sort(),m.length?await n(i.fs,a,d(e,m,l.executable,l.hostMajor),o,{mode:384}):await t(i.fs,a,o)),await f(e,i,o);let h={host:e,path:a,removedEvents:p,remainingEvents:m,files:o};return p.length||(h.reason=s===null?`file-missing`:`not-installed`),h}async function h(e,t){let n=c(e,t),r={host:e,installed:!1,usable:!1,path:n,layout:`none`,events:[],blockingEvents:[],needsMigration:!1};try{let e=await t.fs.readFile(n);if(!e?.trim())return r;let i=u(e);return i?{...r,installed:!0,usable:i.events.length>0,layout:`file`,events:i.events}:{...r,error:l(e)?`invalid-json`:`unmanaged-file`}}catch(e){return{...r,error:i(e)}}}export{h as inspectMarkerHooks,p as installMarkerHooks,m as uninstallMarkerHooks};
@@ -0,0 +1,19 @@
1
+ import { HookHost } from "./events.js";
2
+
3
+ //#region src/hooks/validate.d.ts
4
+ /** Throws `invalid-event` for the first event the host's manifest does not list, with gkcli's wording. */
5
+ declare function validateHookEvents(host: HookHost, events: readonly string[]): void;
6
+ /** Throws `blocking-unsupported` when the host cannot honor a decision (`noBlocking` in the registry) and `blockingEvents` is not empty. */
7
+ declare function assertBlockingSupported(host: HookHost, blockingEvents: readonly string[] | undefined, path?: string): void;
8
+ /** Resolves the events to write: the explicit ones, or the manifest's minus `skippedDefaultEvents` when none are requested. */
9
+ declare function selectHookEvents(request: {
10
+ host: HookHost;
11
+ events?: readonly string[];
12
+ blockingEvents?: readonly string[];
13
+ }): {
14
+ events: string[];
15
+ blockingEvents: string[];
16
+ isDefault: boolean;
17
+ };
18
+ //#endregion
19
+ export { assertBlockingSupported, selectHookEvents, validateHookEvents };
@@ -0,0 +1 @@
1
+ import{HookConfigError as e}from"./errors.js";import{eventManifest as t}from"./events.js";import{hookClientInfo as n}from"./registry.js";function r(n,r){let i=t[n].events;for(let t of r)if(!i.includes(t))throw new e(`invalid-event`,`'${t}' is not a valid event for ${n}. Valid events: ${i.join(`, `)}`)}function i(t,r,i){if(r?.length&&n(t).noBlocking)throw new e(`blocking-unsupported`,`${t} does not support blocking hooks`,i)}function a(e){let n=t[e.host],i=!e.events?.length&&!e.blockingEvents?.length,a=i?n.events.filter(e=>!n.skippedDefaultEvents.includes(e)):[...e.events??[]],o=[...e.blockingEvents??[]];return r(e.host,[...a,...o]),{events:a,blockingEvents:o,isDefault:i}}export{i as assertBlockingSupported,a as selectHookEvents,r as validateHookEvents};
@@ -1,6 +1,6 @@
1
1
  import { AgentHostConfig } from "../config.js";
2
2
  import { HookHost } from "./events.js";
3
- import { HookInstallResult, HookUninstallResult } from "./results.js";
3
+ import { HookInspection, HookInstallResult, HookUninstallResult } from "./results.js";
4
4
 
5
5
  //#region src/hooks/writers.d.ts
6
6
  type JsonHookHost = Extract<HookHost, 'cursor' | 'codex' | 'copilot' | 'antigravity' | 'augment' | 'grok'>;
@@ -10,9 +10,14 @@ interface HookWriteRequest {
10
10
  events?: readonly string[];
11
11
  blockingEvents?: readonly string[];
12
12
  }
13
- declare function command(executablePath: string, host: HookHost, config: AgentHostConfig, blocking: boolean): string;
13
+ declare const BLOCKING_SUFFIX = " --blocking";
14
+ declare const HOOK_RUN_MARKER = " ai hook run --host ";
15
+ /** `event` is appended as `--event` for hosts whose payload omits the event name; a blocking entry ends with `--blocking`. */
16
+ declare function command(executablePath: string, host: HookHost, config: AgentHostConfig, blocking: boolean, event?: string): string;
14
17
  /** Merges only GitKraken-owned hooks, preserving unrelated entries and document keys. */
15
18
  declare function installJsonHooks(request: HookWriteRequest, config: AgentHostConfig): Promise<HookInstallResult>;
16
19
  declare function uninstallJsonHooks(host: JsonHookHost, config: AgentHostConfig, events?: readonly string[]): Promise<HookUninstallResult>;
20
+ /** Read-only: reports the GitKraken events in `host`'s hooks file without ever writing it. */
21
+ declare function inspectJsonHooks(host: HookHost, config: AgentHostConfig): Promise<HookInspection>;
17
22
  //#endregion
18
- export { HookWriteRequest, command, installJsonHooks, uninstallJsonHooks };
23
+ export { BLOCKING_SUFFIX, HOOK_RUN_MARKER, HookWriteRequest, command, inspectJsonHooks, installJsonHooks, uninstallJsonHooks };
@@ -1 +1 @@
1
- import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{HookConfigError as r}from"./errors.js";import{eventManifest as i}from"./events.js";const a={cursor:{path:t=>e(t.paths.home,`.cursor`,`hooks.json`),key:`hooks`,version:!0,matcher:!1},codex:{path:t=>e(t.paths.home,`.codex`,`hooks.json`),key:`hooks`,version:!1,matcher:!0},copilot:{path:t=>e(t.paths.home,`.copilot`,`hooks`,`gitkraken-hooks.json`),key:`hooks`,version:!0,matcher:!1},antigravity:{path:t=>e(t.paths.home,`.gemini`,`config`,`hooks.json`),key:`gitkraken`,version:!1,matcher:!1},augment:{path:t=>e(t.paths.home,`.augment`,`settings.json`),key:`hooks`,version:!1,matcher:!0,neverDelete:!0},grok:{path:t=>e(t.paths.grokHome??e(t.paths.home,`.grok`),`hooks`,`gitkraken-hooks.json`),key:`hooks`,version:!1,matcher:!0}};function o(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function s(e){return typeof e==`string`&&e.includes(` ai hook run --host `)}function c(e,t,n,r){return`${n.platform===`win32`&&`windowsShell`in i[t]&&i[t].windowsShell===`powershell`?`& `:``}"${e.replaceAll(`\\`,`/`)}" ai hook run --host ${t}${r?` --blocking`:``}`}function l(e){let t=i[e.host],n=e.events?.length||e.blockingEvents?.length?e.events??[]:t.events.filter(e=>!t.skippedDefaultEvents.includes(e)),a=e.blockingEvents??[];for(let i of[...n,...a])if(!t.events.includes(i))throw new r(`invalid-event`,`${i} is not valid for ${e.host}`);return[n,a]}function u(e,t){return e===`antigravity`?t===`PreToolUse`||t===`PostToolUse`:a[e].matcher}function d(e,t,n){if(!Array.isArray(e)||!e.every(o))throw new r(`wrong-shape`,`${n} has the wrong event shape`,n);if(t&&e.some(e=>`command`in e||`hooks`in e&&(!Array.isArray(e.hooks)||!e.hooks.every(o))))throw new r(`wrong-shape`,`${n} has the wrong matcher shape`,n);if(!t&&e.some(e=>`hooks`in e))throw new r(`wrong-shape`,`${n} has the wrong flat shape`,n)}function f(e,t){return Array.isArray(e)?e.some(e=>t&&o(e)&&Array.isArray(e.hooks)?e.hooks.some(e=>o(e)&&s(e.command)):o(e)&&s(e.command)):!1}function p(e,t){return Object.keys(t).filter(n=>f(t[n],u(e,n)))}function m(e,t){return t?e.flatMap(e=>{if(!Array.isArray(e.hooks))return[e];let t=e.hooks.filter(e=>!o(e)||!s(e.command));return t.length===e.hooks.length?[e]:t.length?[{...e,hooks:t}]:[]}):e.filter(e=>!s(e.command))}function h(e,t,n,r,a){let o=86400*(`blockingTimeoutMultiplier`in i[n]?Number(i[n].blockingTimeoutMultiplier):1),c={type:`command`,command:r,...a?{timeout:o}:{}};if(!t)return[...m(e,!1),c];let l=m(e,!0),u=l.find(e=>!e.matcher&&Array.isArray(e.hooks)&&e.hooks.length>0);u||(u=n===`augment`?{hooks:[]}:{matcher:``,hooks:[]},l.push(u));let d=Array.isArray(u.hooks)?u.hooks:[];return u.hooks=[...d.filter(e=>!s(e.command)),c],l}function g(e,t){let n;try{n=JSON.parse(e.replace(/^\uFEFF/,``))}catch{throw new r(`invalid-json`,`Invalid JSON at ${t}`,t)}if(!o(n))throw new r(`wrong-shape`,`Invalid hooks document at ${t}`,t);return n}function _(e,t,n){let i=e[t];if(i!=null){if(!o(i))throw new r(`wrong-shape`,`Invalid hooks wrapper at ${n}`,n);return i}}async function v(e,t){if(!(e.host in a))throw new r(`unsupported-host`,`No JSON writer for ${e.host}`);let o=e.host,s=a[o],f=s.path(t),p=await t.fs.readFile(f),v=p?.trim()?g(p,f):{},y={..._(v,s.key,f)};for(let[e,t]of Object.entries(y))d(t,u(o,e),f);let[b,x]=l(e);for(let[n,r]of[[b,!1],[x,!0]])for(let i of n){let n=u(o,i);y[i]=h(y[i]??[],n,o,c(e.executablePath,o,t,r),r)}if(`legacyEvents`in i[o])for(let e of i[o].legacyEvents){let t=y[e];if(t){let n=m(t,u(o,e));n.length?y[e]=n:delete y[e]}}Object.keys(y).length?v[s.key]=y:delete v[s.key],s.version&&v.version==null&&(v.version=1);let S=[];return await n(t.fs,f,`${JSON.stringify(v,null,2)}\n`,S),{host:o,path:f,events:[...b],blockingEvents:[...x],files:S}}async function y(e,r,o){let s=a[e],c=s.path(r),l=[],f=(t,n=[])=>({host:e,path:c,removedEvents:[],remainingEvents:n,files:l,reason:t}),h=await r.fs.readFile(c);if(h===null)return f(`file-missing`);if(!h.trim())return f(`not-installed`);let v=g(h,c),y=_(v,s.key,c);if(!y)return f(`not-installed`);let b={...y};for(let[t,n]of Object.entries(b))d(n,u(e,t),c);let x=[],S=o?.length?o:i[e].events;for(let t of[...S,...`legacyEvents`in i[e]?i[e].legacyEvents:[]]){let n=b[t];if(!n)continue;let r=m(n,u(e,t));JSON.stringify(r)!==JSON.stringify(n)&&(x.push(t),r.length?b[t]=r:delete b[t])}let C=p(e,b);if(!x.length)return f(`not-installed`,C);Object.keys(b).length?v[s.key]=b:delete v[s.key];let w=Object.keys(v).some(e=>e!==s.key&&!(e===`version`&&s.version&&v.version===1));return!Object.keys(b).length&&!w&&!s.neverDelete?await t(r.fs,c,l):await n(r.fs,c,`${JSON.stringify(v,null,2)}\n`,l),{host:e,path:c,removedEvents:x,remainingEvents:C,files:l}}export{c as command,v as installJsonHooks,y as uninstallJsonHooks};
1
+ import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{HookConfigError as r,hookErrorCode as i}from"./errors.js";import{eventManifest as a}from"./events.js";import{hookClientInfo as o}from"./registry.js";import{assertBlockingSupported as s,selectHookEvents as c}from"./validate.js";const l={cursor:{path:t=>e(t.paths.home,`.cursor`,`hooks.json`),key:`hooks`,version:!0,matcher:!1},codex:{path:t=>e(t.paths.home,`.codex`,`hooks.json`),key:`hooks`,version:!1,matcher:!0},copilot:{path:t=>e(t.paths.home,`.copilot`,`hooks`,`gitkraken-hooks.json`),key:`hooks`,version:!0,matcher:!1},antigravity:{path:t=>e(t.paths.home,`.gemini`,`config`,`hooks.json`),key:`gitkraken`,version:!1,matcher:!1},augment:{path:t=>e(t.paths.home,`.augment`,`settings.json`),key:`hooks`,version:!1,matcher:!0,neverDelete:!0},grok:{path:t=>e(t.paths.grokHome??e(t.paths.home,`.grok`),`hooks`,`gitkraken-hooks.json`),key:`hooks`,version:!1,matcher:!0}},u=` --blocking`,d=` ai hook run --host `;function f(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function p(e){return typeof e==`string`&&e.includes(` ai hook run --host `)}function m(e,t,n,r,i){let s=a[t],c=n.platform===`win32`&&`windowsShell`in s&&s.windowsShell===`powershell`?`& `:``,l=i&&`eventFlag`in s&&s.eventFlag?` --event ${i}`:``,f=o(t).blockingWait,p=r?(f?` --timeout ${f}`:``)+u:``;return`${c}"${e.replaceAll(`\\`,`/`)}"${d}${t}${l}${p}`}function h(e,t){return e===`antigravity`?t===`PreToolUse`||t===`PostToolUse`:l[e].matcher}function g(e,t,n){if(!Array.isArray(e)||!e.every(f))throw new r(`wrong-shape`,`${n} has the wrong event shape`,n);if(t&&e.some(e=>`command`in e||`hooks`in e&&(!Array.isArray(e.hooks)||!e.hooks.every(f))))throw new r(`wrong-shape`,`${n} has the wrong matcher shape`,n);if(!t&&e.some(e=>`hooks`in e))throw new r(`wrong-shape`,`${n} has the wrong flat shape`,n)}function _(e,t){return Array.isArray(e)?e.flatMap(e=>t&&f(e)&&Array.isArray(e.hooks)?e.hooks.map(e=>f(e)?e.command:void 0):[f(e)?e.command:void 0]).filter(e=>p(e)):[]}function v(e,t){return Object.keys(t).filter(n=>_(t[n],h(e,n)).length>0)}function y(e,t){return t?e.flatMap(e=>{if(!Array.isArray(e.hooks))return[e];let t=e.hooks.filter(e=>!f(e)||!p(e.command));return t.length===e.hooks.length?[e]:t.length?[{...e,hooks:t}]:[]}):e.filter(e=>!p(e.command))}function b(e,t,n){return n?86400*(`blockingTimeoutMultiplier`in a[e]?Number(a[e].blockingTimeoutMultiplier):1):e===`codex`&&t===`SessionEnd`?3:void 0}function x(e,t,n,r,i){let a={type:`command`,command:r,...i?{timeout:i}:{}};if(!t)return[...y(e,!1),a];let o=y(e,!0),s=o.find(e=>!e.matcher&&Array.isArray(e.hooks)&&e.hooks.length>0);s||(s=n===`augment`?{hooks:[]}:{matcher:``,hooks:[]},o.push(s));let c=Array.isArray(s.hooks)?s.hooks:[];return s.hooks=[...c.filter(e=>!p(e.command)),a],o}function S(e,t){let n;try{n=JSON.parse(e.replace(/^\uFEFF/,``))}catch{throw new r(`invalid-json`,`Invalid JSON at ${t}`,t)}if(!f(n))throw new r(`wrong-shape`,`Invalid hooks document at ${t}`,t);return n}function C(e,t,n){let i=e[t];if(i!=null){if(!f(i))throw new r(`wrong-shape`,`Invalid hooks wrapper at ${n}`,n);return i}}async function w(e,t){if(!Object.hasOwn(l,e.host))throw new r(`unsupported-host`,`No JSON writer for ${e.host}`);let i=e.host,u=l[i],d=u.path(t),{events:f,blockingEvents:p,isDefault:_}=c(e);s(i,p,d);let v=await t.fs.readFile(d),w=v?.trim()?S(v,d):{},T={...C(w,u.key,d)};for(let[e,t]of Object.entries(T))g(t,h(i,e),d);for(let[n,r]of[[f,!1],[p,!0]])for(let a of n){let n=h(i,a);T[a]=x(T[a]??[],n,i,m(e.executablePath,i,t,r,a),b(i,a,r))}if(`legacyEvents`in a[i])for(let e of a[i].legacyEvents){let t=T[e];if(t){let n=y(t,h(i,e));n.length?T[e]=n:delete T[e]}}if(_)for(let e of o(i).unsafeEvents){let t=T[e];if(!t)continue;let n=y(t,h(i,e));n.length?T[e]=n:delete T[e]}Object.keys(T).length?w[u.key]=T:delete w[u.key],u.version&&w.version==null&&(w.version=1);let E=[];return await n(t.fs,d,`${JSON.stringify(w,null,2)}\n`,E),{host:i,path:d,events:[...f],blockingEvents:[...p],files:E}}async function T(e,r,i){let o=l[e],s=o.path(r),c=[],u=(t,n=[])=>({host:e,path:s,removedEvents:[],remainingEvents:n,files:c,reason:t}),d=await r.fs.readFile(s);if(d===null)return u(`file-missing`);if(!d.trim())return u(`not-installed`);let f=S(d,s),p=C(f,o.key,s);if(!p)return u(`not-installed`);let m={...p};for(let[t,n]of Object.entries(m))g(n,h(e,t),s);let _=[],b=i?.length?i:a[e].events;for(let t of[...b,...`legacyEvents`in a[e]?a[e].legacyEvents:[]]){let n=m[t];if(!n)continue;let r=y(n,h(e,t));JSON.stringify(r)!==JSON.stringify(n)&&(_.push(t),r.length?m[t]=r:delete m[t])}let x=v(e,m);if(!_.length)return u(`not-installed`,x);Object.keys(m).length?f[o.key]=m:delete f[o.key];let w=Object.keys(f).some(e=>e!==o.key&&!(e===`version`&&o.version&&f.version===1));return!Object.keys(m).length&&!w&&!o.neverDelete?await t(r.fs,s,c):await n(r.fs,s,`${JSON.stringify(f,null,2)}\n`,c),{host:e,path:s,removedEvents:_,remainingEvents:x,files:c}}async function E(e,t){let n=e,r=l[n],o=r.path(t),s={host:e,installed:!1,usable:!1,path:o,layout:`none`,events:[],blockingEvents:[],needsMigration:!1};try{let i=await t.fs.readFile(o);if(!i?.trim())return s;let c=C(S(i,o),r.key,o)??{};for(let[e,t]of Object.entries(c))g(t,h(n,e),o);let l=v(n,c),d=`legacyEvents`in a[e]?a[e].legacyEvents:[];return{...s,installed:l.length>0,usable:l.length>0,layout:l.length?`file`:`none`,events:l,blockingEvents:l.filter(e=>_(c[e],h(n,e)).some(e=>e.endsWith(u))),needsMigration:l.some(e=>d.includes(e))}}catch(e){return{...s,error:i(e)}}}export{u as BLOCKING_SUFFIX,d as HOOK_RUN_MARKER,m as command,E as inspectJsonHooks,w as installJsonHooks,T as uninstallJsonHooks};
package/dist/index.d.ts CHANGED
@@ -4,15 +4,18 @@ import { McpConfigEntry, generateConfigEntry } from "./mcp/config-entry.js";
4
4
  import { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp } from "./mcp/installer.js";
5
5
  import { McpLayoutOptions, OpenCodeMcpCleanupError, OpenCodeMcpLayout, OpenCodeMcpLayoutUnknownError, OpenCodeMcpPolicy } from "./mcp/opencode-layout.js";
6
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";
7
- import { Agent, HookCapability, listAgents, mcpNamesForHook, resolveHookClient } from "./agents.js";
7
+ import { Agent, HookCapability, ListAgentsOptions, hookCapabilities, listAgents, mcpNamesForHook, resolveHookClient } from "./agents.js";
8
8
  import { FileChange, FileChangeKind } from "./file-change.js";
9
- import { HookInstallResult, HookUninstallResult } from "./hooks/results.js";
10
9
  import { HookConfigError, HookConfigErrorCode } from "./hooks/errors.js";
10
+ import { HookInspection, HookInstallResult, HookLayout, HookUninstallResult } from "./hooks/results.js";
11
+ import { inspectHooks } from "./hooks/inspect.js";
11
12
  import { HookWriteRequest, installJsonHooks, uninstallJsonHooks } from "./hooks/writers.js";
12
13
  import { InstallHooksRequest, installHooks, uninstallHooks } from "./hooks/install.js";
14
+ import { HookClientInfo, executableForHook, hookClientInfo, hookHosts } from "./hooks/registry.js";
15
+ import { validateHookEvents } from "./hooks/validate.js";
13
16
  import { getMcpCliInstallCommand } from "./mcp/cli-install-command.js";
14
17
  import { McpUninstallFileResult, McpUninstallRequest, McpUninstallResult, uninstallMcp } from "./mcp/uninstaller.js";
15
18
  import { SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, SkillManifest } from "./skills/manifest.js";
16
19
  import { SkillAgent, allSkillAgents, isSkillAgentSupported, skillDir } from "./skills/skill-dirs.js";
17
20
  import { SkillConflictError, SkillFiles, SkillInstallAction, SkillInstallOptions, SkillInstallResult, SkillOperation, SkillReason, SkillState, SkillStatus, SkillUninstallAction, SkillUninstallOptions, SkillUninstallResult, SkillUnsupportedManifestError, inspectSkillStatus, installSkill, uninstallSkill } from "./skills/install.js";
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 };
21
+ 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, type HookClientInfo, HookConfigError, type HookConfigErrorCode, type HookHost, type HookInspection, type HookInstallResult, type HookLayout, type HookUninstallResult, type HookWriteRequest, type InstallHooksRequest, type ListAgentsOptions, 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, executableForHook, generateConfigEntry, getClientInstallation, getMcpCliInstallCommand, hookCapabilities, hookClientInfo, hookHosts, hostIdentity, hostOf, inspectHooks, inspectMcpInstall, inspectSkillStatus, installHooks, installJsonHooks, installMcp, installSkill, isInstallable, isLegacy, isSkillAgentSupported, isSupported, joinPath, legacyClientIds, listAgents, mcpNamesForHook, resolveHookClient, skillDir, uninstallHooks, uninstallJsonHooks, uninstallMcp, uninstallSkill, validateHookEvents };
package/dist/index.js CHANGED
@@ -1 +1 @@
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
+ import{joinPath as e}from"./config.js";import{HookConfigError as t}from"./hooks/errors.js";import{eventManifest as n}from"./hooks/events.js";import{executableForHook as r,hookClientInfo as i,hookHosts as a}from"./hooks/registry.js";import{validateHookEvents as o}from"./hooks/validate.js";import{installJsonHooks as s,uninstallJsonHooks as c}from"./hooks/writers.js";import{inspectHooks as l}from"./hooks/inspect.js";import{OpenCodeMcpCleanupError as u,OpenCodeMcpLayoutUnknownError as d}from"./mcp/opencode-layout.js";import{allClients as f,antigravityLegacyPath as p,clientCommand as m,clientConfigPath as h,clientConfigPaths as g,clientDisplayName as _,clientExecutablePath as v,clientJSONPath as y,clientType as b,clientsForHost as x,detectClient as S,getClientInstallation as C,hostIdentity as w,hostOf as T,isInstallable as E,isLegacy as D,isSupported as O,legacyClientIds as k}from"./mcp/clients.js";import{hookCapabilities as A,listAgents as j,mcpNamesForHook as M,resolveHookClient as N}from"./agents.js";import{installHooks as P,uninstallHooks as F}from"./hooks/install.js";import{generateConfigEntry as I}from"./mcp/config-entry.js";import{inspectMcpInstall as L,installMcp as R}from"./mcp/installer.js";import{getMcpCliInstallCommand as z}from"./mcp/cli-install-command.js";import{uninstallMcp as B}from"./mcp/uninstaller.js";import{SKILL_MANIFEST_FILE_NAME as V,SKILL_MANIFEST_SCHEMA_VERSION as H}from"./skills/manifest.js";import{allSkillAgents as U,isSkillAgentSupported as W,skillDir as G}from"./skills/skill-dirs.js";import{SkillConflictError as K,SkillUnsupportedManifestError as q,inspectSkillStatus as J,installSkill as Y,uninstallSkill as X}from"./skills/install.js";export{t as HookConfigError,u as OpenCodeMcpCleanupError,d as OpenCodeMcpLayoutUnknownError,V as SKILL_MANIFEST_FILE_NAME,H as SKILL_MANIFEST_SCHEMA_VERSION,K as SkillConflictError,q as SkillUnsupportedManifestError,f as allClients,U as allSkillAgents,p as antigravityLegacyPath,m as clientCommand,h as clientConfigPath,g as clientConfigPaths,_ as clientDisplayName,v as clientExecutablePath,y as clientJSONPath,b as clientType,x as clientsForHost,S as detectClient,n as eventManifest,r as executableForHook,I as generateConfigEntry,C as getClientInstallation,z as getMcpCliInstallCommand,A as hookCapabilities,i as hookClientInfo,a as hookHosts,w as hostIdentity,T as hostOf,l as inspectHooks,L as inspectMcpInstall,J as inspectSkillStatus,P as installHooks,s as installJsonHooks,R as installMcp,Y as installSkill,E as isInstallable,D as isLegacy,W as isSkillAgentSupported,O as isSupported,e as joinPath,k as legacyClientIds,j as listAgents,M as mcpNamesForHook,N as resolveHookClient,G as skillDir,F as uninstallHooks,c as uninstallJsonHooks,B as uninstallMcp,X as uninstallSkill,o as validateHookEvents};
@@ -1,3 +1,3 @@
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(`
1
+ import{writeFileTracked as e}from"../file-change.js";import{addAtPointer as t,assertValidJsonc as n,findAtPointer as r,isValidJsonc as i,removeAtPointerAndEmptyParent as a}from"./jsonc-edit.js";import{OpenCodeMcpCleanupError as o,OpenCodeMcpLayoutUnknownError as s,openCodeNativeServers as c,openCodeRefusal as l,resolveOpenCodePolicy as u}from"./opencode-layout.js";import{assertValidToml as d,parseTomlSafe as f,tomlDottedPath as p,tomlLiteralKey as m}from"./toml-read.js";import{clientConfigPath as h,clientJSONPath as g,isInstallable as _,openCodeJSONPath as v}from"./clients.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(n,i){let{client:a}=n,o=_(a)?n.configPath??await h(a,i):void 0;if(!o)throw Error(`unsupported client: ${a}`);if(a===`opencode`)return D(n,i,o);let s=y(a,n.executablePath,n.args);if(a===`codex`)return P(n,i,o,s);let c=await i.fs.readFile(o),l=c===null||c.trim()===``?`{}`:c,u=g(a),d=r(l,u),f=c!==null&&I(d)&&H(d,s,{client:a,path:o,location:u}).state===`current`;return{client:a,path:o,change:f?`unchanged`:await e(i.fs,o,t(l,u,s,o),[],{mode:420}),format:`json`,location:u,entry:f?d:s,removedLocations:[]}}async function D(r,i,s){let c=u(r.openCode),d=await i.fs.readFile(s),f=d===null||d.trim()===``?`{}`:d;n(f,s);let p=l(f,k(c),s);if(p!==void 0)throw Error(p);let m=c.mode===`authoritative`?c.layout:O(f,s),h=j(m),g=v(m),_=v(h),b=y(`opencode`,r.executablePath,r.args,{openCode:{mode:`authoritative`,layout:m}}),x=M(f,m),S=d!==null&&I(x)&&H(x,b,{client:`opencode`,path:s,location:g}).state===`current`,C=c.mode===`authoritative`?M(f,h):void 0,w=S?f:t(f,g,b,s),T=C===void 0?void 0:a(w,_,s),E=S?`unchanged`:await e(i.fs,s,w,[],{mode:420}),D=e=>({client:`opencode`,path:s,change:E,format:`json`,location:g,entry:S?x:b,removedLocations:e,openCodeLayout:m});if(T===void 0||!T.removed)return D([]);try{if(await i.fs.readFile(s)!==w)throw Error(`${s} changed while the OpenCode MCP entry was being installed`);let t=await e(i.fs,s,T.text,[],{mode:420});E===`unchanged`&&(E=t)}catch(e){throw A(e)?e:new o(D([]),e)}return D([_])}function O(e,t){let n=N(e);if(!n)throw new s;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`&&c(e)===void 0))return r(e,v(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(t,n,r,i){let a=await n.fs.readFile(r);a!==null&&a.trim()!==``&&d(a,r);let o=g(`codex`),s=a??``,c=f(s),l=p(c,o),u=m(c,o);if(a!==null&&u===void 0&&I(l)&&H(l,i,{client:t.client,path:r,location:o}).state===`current`)return{client:t.client,path:r,change:`unchanged`,format:`toml`,location:o,entry:l,removedLocations:[]};let h,_,v=[];if(u!==void 0){if(!I(u)||l!==void 0&&!I(l))throw Error(`cannot safely migrate Codex MCP entry in ${r}: ambiguous legacy and nested tables`);let e=I(l)&&l.env!==void 0?l.env:u.env;if(e!==void 0&&!I(e))throw Error(`cannot safely migrate Codex MCP entry in ${r}: legacy env is not a table`);if(Object.entries(u).some(([e,t])=>e!==`env`&&I(t))||e&&Object.values(e).some(I))throw Error(`cannot safely migrate Codex MCP entry in ${r}: unsupported legacy subtable`);_=e;let t=S(s,o).text;h=C(l===void 0?t:S(t,o).text,o,F(o,i,e)),v.push(`["${o}"]`)}else h=C(s,o,F(o,i));let y=f(h),w=p(y,o);if(!I(w)||H(w,i,{client:t.client,path:r,location:o}).state!==`current`||m(y,o)!==void 0||u!==void 0&&!b(w.env,_)||!b(x(y),x(c)))throw Error(`cannot safely update Codex MCP entry in ${r}: unsupported TOML representation`);let T=await e(n.fs,r,h,[],{mode:384});return{client:t.client,path:r,change:T,format:`toml`,location:o,entry:w,removedLocations:v}}function F(e,t,n){let r=w(t),i=r.endsWith(`
2
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};
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:n}=e,a=n===`opencode`?u(e.openCode):void 0,o=a?.mode===`infer-existing`?void 0:a?.layout,s=n===`opencode`?v(o??`v2`):g(n),c=_(n)?e.configPath??await h(n,t):void 0;if(!c)return L({client:n,path:``,location:s},`unsupported-client`);let l={client:n,path:c,location:s,...o&&{openCodeLayout:o}},d=await t.fs.readFile(c);if(d===null)return L(l,a?.mode===`infer-existing`?`layout-unknown`:`file-missing`);if(n===`codex`)return B(d,y(n,e.executablePath,e.args),l);if(d.trim()===``)return L(l,a?.mode===`infer-existing`?`layout-unknown`:`no-entry`);if(!i(d))return L(l,`invalid-config`);if(a)return V(d,e,a,l);let f=r(d,s);if(f===void 0)return L(l,`no-entry`);let p=y(n,e.executablePath,e.args);return I(f)?H(f,p,l):L(l,`entry-not-object`)}function B(e,t,n){if(e.trim()===``)return L(n,`no-entry`);let r=f(e);if(!r)return L(n,`invalid-config`);let i=m(r,n.location);if(i!==void 0)return R(n,i);let a=p(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(l(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:v(n),openCodeLayout:n,...M(e,o)!==void 0&&{openCodeDuplicateLocation:v(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:v(o)},s);let c=s===void 0?r:{...r,openCodeDuplicateLocation:v(o)};if(!I(a))return L(c,`entry-not-object`);let u=H(a,i(n.layout),c);return u.state===`current`&&s!==void 0?R(c,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};
@@ -3,6 +3,10 @@
3
3
  declare function isValidJsonc(text: string): boolean;
4
4
  /** Throws when `text` isn't valid JSONC; never touches `text`. */
5
5
  declare function assertValidJsonc(text: string, path: string): void;
6
+ interface EditOptions {
7
+ /** Edit only the targeted member's text; the formatting of its siblings is left byte-for-byte. */
8
+ preserveSiblings?: boolean;
9
+ }
6
10
  /** Reads the value at `pointer` in JSONC `text`, or `undefined` when absent or the document is unparsable. */
7
11
  declare function findAtPointer(text: string, pointer: string): unknown;
8
12
  /**
@@ -13,7 +17,7 @@ declare function findAtPointer(text: string, pointer: string): unknown;
13
17
  * is touched. `sourcePath` names `text`'s file for the error thrown when it
14
18
  * isn't valid JSONC; no edit is made in that case.
15
19
  */
16
- declare function addAtPointer(text: string, pointer: string, value: unknown, sourcePath: string): string;
20
+ declare function addAtPointer(text: string, pointer: string, value: unknown, sourcePath: string, options?: EditOptions): string;
17
21
  /**
18
22
  * Removes the value at `pointer`, leaving its parent container in place even
19
23
  * if that empties it. Mirrors gkcli's `uninstaller.writeConfig`, the shape
@@ -22,7 +26,7 @@ declare function addAtPointer(text: string, pointer: string, value: unknown, sou
22
26
  *
23
27
  * Returns `removed: false` when there was nothing at `pointer` to begin with.
24
28
  */
25
- declare function removeAtPointer(text: string, pointer: string, sourcePath: string): {
29
+ declare function removeAtPointer(text: string, pointer: string, sourcePath: string, options?: EditOptions): {
26
30
  text: string;
27
31
  removed: boolean;
28
32
  };
@@ -35,9 +39,9 @@ declare function removeAtPointer(text: string, pointer: string, sourcePath: stri
35
39
  *
36
40
  * Returns `removed: false` when there was nothing at `pointer` to begin with.
37
41
  */
38
- declare function removeAtPointerAndEmptyParent(text: string, pointer: string, sourcePath: string): {
42
+ declare function removeAtPointerAndEmptyParent(text: string, pointer: string, sourcePath: string, options?: EditOptions): {
39
43
  text: string;
40
44
  removed: boolean;
41
45
  };
42
46
  //#endregion
43
- export { addAtPointer, assertValidJsonc, findAtPointer, isValidJsonc, removeAtPointer, removeAtPointerAndEmptyParent };
47
+ export { EditOptions, addAtPointer, assertValidJsonc, findAtPointer, isValidJsonc, removeAtPointer, removeAtPointerAndEmptyParent };
@@ -1,3 +1,9 @@
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
- `},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,u as assertValidJsonc,p as findAtPointer,l as isValidJsonc,y as removeAtPointer,b as removeAtPointerAndEmptyParent};
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,t){return e.lastIndexOf(`
3
+ `,t-1)+1}function f(e,t){let n=e.indexOf(`
4
+ `,t);return n<0?e.length:n}function p(e,t){let n=f(e,t);return n>t&&e[n-1]===`\r`?n-1:n}function m(e,t,n=`
5
+ `){return JSON.stringify(e,null,2).replace(/\n/g,`${n}${t}`)}function h(t,n,r,i){let a=n.children??[],o=a[a.length-1],s=o.offset+o.length,c=n.offset+n.length-1,l=E(t,s,c),u=t.slice(d(t,o.offset),o.offset),f=p(t,l??s),h=t.includes(`\r
6
+ `)?`\r
7
+ `:`
8
+ `,g=`${JSON.stringify(r)}: `;if(!/^\s*$/.test(u)||f>=c)return e(t,[{offset:s,length:0,content:`, ${g}${JSON.stringify(i)}`}]);let _=`${u}${g}${m(i,u,h)}`;return e(t,l===void 0?[{offset:s,length:0,content:`,`},{offset:f,length:0,content:`${h}${_}`}]:[{offset:f,length:0,content:`${h}${_},`}])}function g(t,r,i){if(JSON.stringify(n(r))===JSON.stringify(i))return t;let a=t.slice(d(t,r.offset),r.offset).match(/^\s*/)?.[0]??``;return e(t,[{offset:r.offset,length:r.length,content:m(i,a)}])}function _(e,n,r){let a=i(e,void 0,s);if(!a)return;let o=x(a,n),c=a,l=0;for(;l<o.length;l++){let e=t(c,[o[l]]);if(!e)break;c=e}if(l===o.length)return g(e,c,r);if(c.type!==`object`||!c.children?.length)return;let u=o.slice(l+1).reduceRight((e,t)=>({[t]:e}),r);return v(h(e,c,String(o[l]),u),n,r)}function v(e,r,a){let o=i(e,void 0,s),c=o?t(o,x(o,r)):void 0;return c&&JSON.stringify(n(c))===JSON.stringify(a)?e:void 0}function y(t,n){let r=n.parent,i=r?.children??[],a=i.indexOf(n),o=n.offset+n.length,s=r?r.offset+r.length-1:t.length,c=E(t,o,a<i.length-1?i[a+1].offset:s),l=[];if(c===void 0&&a>0){let e=i[a-1],r=E(t,e.offset+e.length,n.offset);r!==void 0&&l.push({offset:r,length:1,content:``})}let u=c===void 0?o:c+1,p=d(t,n.offset),m=f(t,u),h=/^\s*$/.test(t.slice(p,n.offset))&&/^\s*(\/\/[^\n]*)?$/.test(t.slice(u,m))&&m<s;return l.push(h?{offset:p,length:m+1-p,content:``}:{offset:n.offset,length:u-n.offset,content:``}),e(t,l.sort((e,t)=>e.offset-t.offset))}function b(e){return e?e.split(`/`).slice(1).map(e=>e.replace(/~1/g,`/`).replace(/~0/g,`~`)):[]}function x(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 S(e,r){let a=[],o=i(c(e).body,a,s);if(!o||a.length>0)return;let l=t(o,x(o,b(r)));return l?n(l):void 0}function C(n,a,s,l,d={}){u(n,l);let{bom:f,body:p}=c(n),m=b(a);if(d.preserveSiblings){let e=_(p,m,s);if(e!==void 0)return f+e}let h=p;for(let n=1;n<m.length;n++){let a=i(h),s=a?x(a,m.slice(0,n)):m.slice(0,n);a&&t(a,s)||(h=e(h,r(h,s,{},{formattingOptions:o})))}let g=i(h),v=g?x(g,m):m;return f+e(h,r(h,v,s,{formattingOptions:o}))}function w(n,a,c={}){let l=i(n,void 0,s),u=l?x(l,a):a,d=l?t(l,u):void 0;if(!(a.length===0||!l||!d))return d.parent?.type===`array`&&typeof u.at(-1)==`number`?T(n,d.parent,d):c.preserveSiblings&&d.parent?.type===`property`?y(n,d.parent):D(e(n,r(n,u,void 0,{formattingOptions:o})))}function T(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=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=E(t,e.offset+e.length,r.offset);n!==void 0&&o.push({offset:n,length:1,content:``})}else{let e=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(e,t,n){for(let r=t;r<n;r++)if(e.startsWith(`//`,r))for(;r<n&&e[r]!==`
9
+ `;)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 D(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 O(e,t,n,r={}){u(e,n);let{bom:i,body:a}=c(e),o=w(a,b(t),r);return o===void 0?{text:e,removed:!1}:{text:i+o,removed:!0}}function k(e,t,n,r={}){u(e,n);let{bom:i,body:a}=c(e),o=b(t),s=w(a,o,r);if(s===void 0)return{text:e,removed:!1};let l=o.slice(0,-1);return l.length>0&&A(s,l)&&(s=w(s,l,r)??s),{text:i+s,removed:!0}}function A(e,n){let r=i(e,void 0,s);if(!r)return!1;let a=t(r,x(r,n));return!a||a.type!==`object`?!1:(a.children?.length??0)===0}export{C as addAtPointer,u as assertValidJsonc,S as findAtPointer,l as isValidJsonc,O as removeAtPointer,k as removeAtPointerAndEmptyParent};
@@ -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{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};
1
+ import{writeFileTracked as e}from"../file-change.js";import{removeAtPointer as t,removeAtPointerAndEmptyParent as n}from"./jsonc-edit.js";import{assertValidToml as r,parseTomlSafe as i,tomlDottedPath as a,tomlLiteralKey as o}from"./toml-read.js";import{clientConfigPaths as s,clientJSONPath as c,isSupported as l,openCodeEntryPointers as u}from"./clients.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(t,n){let{client:r}=t;if(!l(r))throw Error(`unsupported client: ${r}`);let i=t.configPath?[t.configPath]:await s(r,n),a=[];for(let e of i)a.push(await _(r,e,n));let o=[];for(let{file:t,text:r}of a)r!==void 0&&await e(n.fs,t.path,r,[]),o.push(t);return{client:r,removed:o.some(e=>e.removedLocations.length>0),files:o}}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(e,r,i){let a=r===`opencode`,o=a?u(e):[c(r)],s=e,l=[];for(let e of o){let r=a?n(s,e,i):t(s,e,i);r.removed&&(s=r.text,l.push(e))}return{text:s,removedLocations:l}}function y(e,t){r(e,t);let n=c(`codex`),s=i(e);if(o(s,n)===void 0&&a(s,n)===void 0)return{text:e,removedLocations:[]};let{text:l,removed:u}=p(e,n);if(!u){if(o(s,n)!==void 0||a(s,n)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: unsupported TOML representation`);return{text:e,removedLocations:[]}}let h=m(l),g=i(h);if(o(g,n)!==void 0||a(g,n)!==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:[n]}}export{g as uninstallMcp};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/agent-tools",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",