@gitkraken/agent-tools 0.1.1 → 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 +52 -3
- package/README.md +55 -2
- package/dist/agents.d.ts +11 -2
- package/dist/agents.js +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/hooks/claude.d.ts +3 -2
- package/dist/hooks/claude.js +2 -3
- package/dist/hooks/errors.d.ts +4 -2
- package/dist/hooks/errors.js +1 -1
- package/dist/hooks/events.d.ts +11 -8
- package/dist/hooks/events.js +1 -1
- package/dist/hooks/inspect.d.ts +9 -0
- package/dist/hooks/inspect.js +1 -0
- package/dist/hooks/install.js +1 -1
- package/dist/hooks/registry.d.ts +26 -0
- package/dist/hooks/registry.js +1 -0
- package/dist/hooks/results.d.ts +23 -1
- package/dist/hooks/templates.d.ts +4 -2
- package/dist/hooks/templates.js +20 -9
- package/dist/hooks/validate.d.ts +19 -0
- package/dist/hooks/validate.js +1 -0
- package/dist/hooks/writers.d.ts +8 -3
- package/dist/hooks/writers.js +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +1 -1
- package/dist/mcp/clients.d.ts +12 -3
- package/dist/mcp/clients.js +1 -1
- package/dist/mcp/config-entry.d.ts +4 -2
- package/dist/mcp/config-entry.js +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/installer.d.ts +70 -9
- package/dist/mcp/installer.js +3 -3
- package/dist/mcp/jsonc-edit.d.ts +10 -4
- package/dist/mcp/jsonc-edit.js +8 -2
- package/dist/mcp/opencode-layout.d.ts +54 -0
- package/dist/mcp/opencode-layout.js +1 -0
- package/dist/mcp/uninstaller.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 0.3.0
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Minor Changes
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
38
|
+
|
|
39
|
+
## [0.2.0] - 2026-10-12
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- OpenCode MCP layout selection (core-tools#164). `McpInstallRequest.openCode` and `generateConfigEntry`/`clientJSONPath` options take an `OpenCodeMcpPolicy`: `{ mode: 'authoritative', layout: 'v1' | 'v2' }` writes and prunes the other layout, `{ mode: 'infer-existing' }` follows the layout the config already uses (v1 when both exist, the entry OpenCode launches) and never prunes. Omitting it keeps the 0.1.1 behavior (v2, v1 pruned). `inspectMcpInstall` results and `installMcp` results report the selected `openCodeLayout`.
|
|
44
|
+
- `inspectMcpInstall` reports `missing` with the new reason `layout-unknown` when `infer-existing` finds no GitKraken entry in either OpenCode layout (including a missing or blank file), the cases where `installMcp` would throw `OpenCodeMcpLayoutUnknownError`. `McpInstallStatus.openCodeDuplicateLocation` is the JSON pointer of the GitKraken entry in the OpenCode layout that was not compared, present whenever both layouts hold one; it is reported only and never pruned by `infer-existing`.
|
|
45
|
+
- `OpenCodeMcpLayoutUnknownError` for `infer-existing` with no GitKraken entry to follow, or in a read-free helper; `OpenCodeMcpCleanupError`, carrying the committed `result`, when the selected entry was written but removing the other layout's entry failed. Cancellation (`AbortError`) is rethrown as-is.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- OpenCode migration is two writes: the selected entry first with the other layout retained, then the removal, after re-reading to confirm the file is unchanged. A v2 write refuses a flat server named `servers` and a non-object `mcp` or `mcp.servers`.
|
|
50
|
+
- `inspectMcpInstall` reports a prunable OpenCode duplicate with the selected entry and location (previously the duplicate's), and an entry only in the other layout as `legacy-location` in either direction.
|
|
51
|
+
- `inspectMcpInstall` reports an OpenCode config that `installMcp` refuses (a non-object root, `mcp` or `mcp.servers`, or a flat server named `servers` under a v2 policy) as `missing` with the new reason `unsafe-config`, instead of `no-entry`.
|
|
52
|
+
- Safe OpenCode migration needs the host `FileSystemPort.writeFile` to leave the previous file intact when it rejects. Custom fields inside a replaced GitKraken entry are not carried across layouts.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- `uninstallMcp` and `getClientInstallation` no longer treat a `GitKraken` key on a flat OpenCode server named `servers` as a v2 entry, so uninstall leaves it intact.
|
|
8
57
|
|
|
9
58
|
## [0.1.1] - 2026-10-02
|
|
10
59
|
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ const config: AgentHostConfig = {
|
|
|
33
33
|
},
|
|
34
34
|
fs: myFileSystemPort, // @gitkraken/shared-tools FileSystemPort
|
|
35
35
|
resolveExecutable: (command) => myPathLookup(command), // optional: a client's binary on PATH
|
|
36
|
-
openCodeMajor: 2, // optional: the installed OpenCode major version (0/undefined = unknown)
|
|
36
|
+
openCodeMajor: 2, // optional: the installed OpenCode major version (0/undefined = unknown); does not select the MCP layout
|
|
37
37
|
};
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -66,7 +66,31 @@ const removal = await uninstallMcp({ client: 'vscode' }, config);
|
|
|
66
66
|
|
|
67
67
|
`getMcpCliInstallCommand` returns `undefined` when a shortcut cannot do the job: the client has none, a `configPath` is set, the client is Cursor (its config file is always written directly), or Codex's config still holds the legacy `['mcp_servers.GitKraken']` table. `installMcp` and `uninstallMcp` throw `unsupported client` for an unknown id (`installMcp` also for the uninstall-only `gemini`), and throw without writing when a config file does not parse (`inspectMcpInstall` reports `invalid-config` instead). A leading UTF-8 BOM is accepted and kept.
|
|
68
68
|
|
|
69
|
-
Re-running `installMcp` when the entry is already in place writes nothing and reports `change: 'unchanged'`. `inspectMcpInstall` says why an entry is not current (`reason`, and the drifting `key`) and exposes the on-disk `entry`, so a host can carry user-added args forward before reinstalling.
|
|
69
|
+
Re-running `installMcp` when the entry is already in place writes nothing and reports `change: 'unchanged'`. `inspectMcpInstall` says why an entry is not current (`reason`, and the drifting `key`) and exposes the on-disk `entry`, so a host can carry user-added args forward before reinstalling.
|
|
70
|
+
|
|
71
|
+
### OpenCode layouts
|
|
72
|
+
|
|
73
|
+
OpenCode reads MCP servers from `/mcp/<name>` (v1) or `/mcp/servers/<name>` (v2). Omitting `openCode` writes v2 and prunes a v1 entry, as in 0.1.1. A host that probed the installed OpenCode should pass a policy on every OpenCode call (inspect before and after repair, and install). The library never runs the probe or compares versions:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// Verified earlier-v1 OpenCode -> v1; verified >= 1.18.24, or a v2 client -> v2.
|
|
77
|
+
const policy = { mode: 'authoritative', layout: 'v1' } as const;
|
|
78
|
+
// Probe failed or unparseable: follow what the config already uses, never prune.
|
|
79
|
+
const unknown = { mode: 'infer-existing' } as const;
|
|
80
|
+
|
|
81
|
+
await installMcp({ ...request, openCode: policy }, config);
|
|
82
|
+
generateConfigEntry('opencode', '/usr/local/bin/gk', ['mcp'], { openCode: policy });
|
|
83
|
+
// { type: 'local', command: ['/usr/local/bin/gk', 'mcp'], enabled: true } (v2 uses `disabled: false`)
|
|
84
|
+
clientJSONPath('opencode', { openCode: policy }); // '/mcp/GitKraken'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `authoritative` writes the selected layout and removes the other layout's GitKraken entry. `infer-existing` follows the layout the config already uses and repairs that entry in place; with both layouts present it selects v1, because OpenCode's compatibility reader retains it over the native entry. It leaves the duplicate alone. It never authorizes pruning, even if you copy the reported `openCodeLayout` elsewhere; pass `infer-existing` unchanged through both inspections and the install.
|
|
88
|
+
- `infer-existing` throws `OpenCodeMcpLayoutUnknownError` when there is no entry to follow, and in the read-free helpers (`generateConfigEntry`, `clientJSONPath`), which cannot read the config. To generate an entry without a probe result, choose a documented fallback yourself, e.g. `{ mode: 'authoritative', layout: 'v1' }`; generating performs no write or cleanup. A non-object GitKraken entry is treated as corruption and refused, not skipped. A malformed policy throws `TypeError`.
|
|
89
|
+
- `inspectMcpInstall` compares the selected layout; an entry found only in the other layout is `drifted` / `legacy-location` with that entry exposed for argv repair, and a current entry with a prunable duplicate is `drifted` / `legacy-location` with the selected entry. With `infer-existing` the compared entry is the v1 one when both layouts are present. `openCodeDuplicateLocation` reports the pointer of the other layout's entry whenever both hold one, in either mode; it is never pruned by `infer-existing`. `infer-existing` with no entry in either layout (including a missing or blank file) is `missing` / `layout-unknown`, the case where `installMcp` would throw `OpenCodeMcpLayoutUnknownError`. Argv merging stays in the host: re-run `inspectMcpInstall` with the effective argv before `installMcp`.
|
|
90
|
+
- A migration writes the selected entry first and the removal second. If the removal fails (or the file changed in between), `installMcp` throws `OpenCodeMcpCleanupError`; `error.result` describes what was committed, with no `removedLocations`. This is not success, since an older reader can still reject a leftover native `servers` object. Run the install again to finish. `AbortError` is rethrown unchanged.
|
|
91
|
+
- Refused without writing: a non-object root, `mcp` or `mcp.servers`; a v2 write where `mcp.servers` is a flat server named `servers` (an object whose `type` or `enabled` is a scalar, array or null). `inspectMcpInstall` reports these same files as `missing` / `unsafe-config`, so a host can tell before installing. Custom fields inside a replaced GitKraken entry are not carried across layouts.
|
|
92
|
+
- Safe migration needs your `FileSystemPort.writeFile` to leave the previous file intact when it rejects (write a temp file, then rename). Your `readFile` must also return exactly the bytes `writeFile` was given (no newline normalization, appended trailing newline, or BOM changes): the migration compares the re-read file against what it wrote before the destructive second write, and a port that does not round-trip turns the migration into `OpenCodeMcpCleanupError` (retrying completes it). The library cannot check this and holds no lock against concurrent writers.
|
|
93
|
+
- `uninstallMcp` ignores the policy and removes both pointers, except that a `GitKraken` key on a flat server named `servers` is not a v2 entry and is left alone; `configPath` confines every read and write to that file.
|
|
70
94
|
|
|
71
95
|
Codex TOML edits preserve surrounding text and comments by splicing table blocks. When an entry needs to change but its valid TOML representation cannot be edited safely this way (for example, a drifted inline or dotted `mcp_servers.GitKraken` assignment), install or uninstall throws an error naming the config path and leaves the file untouched. A semantically current entry remains a no-op even when it uses an unusual representation.
|
|
72
96
|
|
|
@@ -92,6 +116,35 @@ try {
|
|
|
92
116
|
|
|
93
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.
|
|
94
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
|
+
|
|
95
148
|
## Client detection
|
|
96
149
|
|
|
97
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{
|
|
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};
|
package/dist/config.d.ts
CHANGED
|
@@ -50,7 +50,8 @@ interface AgentHostConfig {
|
|
|
50
50
|
* The installed OpenCode major version, or `undefined`/`0` when unknown —
|
|
51
51
|
* the same sentinel Go's `OpenCodeMajor()` returns. Go probes `opencode
|
|
52
52
|
* --version`; this library never spawns a process, so the host resolves
|
|
53
|
-
* the probe (or reads its own cache) and injects the result.
|
|
53
|
+
* the probe (or reads its own cache) and injects the result. It does not
|
|
54
|
+
* select an MCP layout; pass `McpInstallRequest.openCode` for that.
|
|
54
55
|
*/
|
|
55
56
|
openCodeMajor?: number;
|
|
56
57
|
/**
|
package/dist/hooks/claude.d.ts
CHANGED
|
@@ -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 };
|
package/dist/hooks/claude.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import{joinPath as e}from"../config.js";import{
|
|
2
|
-
`)?
|
|
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};
|
package/dist/hooks/errors.d.ts
CHANGED
|
@@ -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 };
|
package/dist/hooks/errors.js
CHANGED
|
@@ -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};
|
package/dist/hooks/events.d.ts
CHANGED
|
@@ -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
|
|
47
|
-
readonly
|
|
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
|
};
|
package/dist/hooks/events.js
CHANGED
|
@@ -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
|
|
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};
|
package/dist/hooks/install.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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};
|
package/dist/hooks/results.d.ts
CHANGED
|
@@ -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 };
|
package/dist/hooks/templates.js
CHANGED
|
@@ -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
|
|
2
|
-
`,1)[0].startsWith(
|
|
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
|
|
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
|
-
`,
|
|
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`,
|
|
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(
|
|
278
|
-
`+
|
|
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};
|