@gitkraken/agent-tools 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +19 -0
- package/README.md +163 -2
- package/dist/agents.d.ts +52 -0
- package/dist/agents.js +1 -0
- package/dist/config.d.ts +69 -0
- package/dist/config.js +1 -0
- package/dist/file-change.d.ts +28 -0
- package/dist/file-change.js +1 -0
- package/dist/hooks/claude.d.ts +16 -0
- package/dist/hooks/claude.js +3 -0
- package/dist/hooks/errors.d.ts +10 -0
- package/dist/hooks/errors.js +1 -0
- package/dist/hooks/events.d.ts +73 -0
- package/dist/hooks/events.js +1 -0
- package/dist/hooks/install.d.ts +15 -0
- package/dist/hooks/install.js +1 -0
- package/dist/hooks/results.d.ts +30 -0
- package/dist/hooks/results.js +1 -0
- package/dist/hooks/templates.d.ts +15 -0
- package/dist/hooks/templates.js +278 -0
- package/dist/hooks/writers.d.ts +18 -0
- package/dist/hooks/writers.js +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1 -0
- package/dist/mcp/cli-install-command.d.ts +27 -0
- package/dist/mcp/cli-install-command.js +1 -0
- package/dist/mcp/clients.d.ts +96 -0
- package/dist/mcp/clients.js +1 -0
- package/dist/mcp/config-entry.d.ts +15 -0
- package/dist/mcp/config-entry.js +1 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/installer.d.ts +130 -0
- package/dist/mcp/installer.js +3 -0
- package/dist/mcp/jsonc-edit.d.ts +43 -0
- package/dist/mcp/jsonc-edit.js +3 -0
- package/dist/mcp/opencode-layout.d.ts +54 -0
- package/dist/mcp/opencode-layout.js +1 -0
- package/dist/mcp/toml-read.d.ts +11 -0
- package/dist/mcp/toml-read.js +1 -0
- package/dist/mcp/toml-safety.d.ts +7 -0
- package/dist/mcp/toml-safety.js +1 -0
- package/dist/mcp/toml-splice.d.ts +16 -0
- package/dist/mcp/toml-splice.js +11 -0
- package/dist/mcp/uninstaller.d.ts +41 -0
- package/dist/mcp/uninstaller.js +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +1 -0
- package/dist/skills/install.d.ts +126 -0
- package/dist/skills/install.js +1 -0
- package/dist/skills/manifest.d.ts +56 -0
- package/dist/skills/manifest.js +1 -0
- package/dist/skills/skill-dirs.d.ts +22 -0
- package/dist/skills/skill-dirs.js +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/memory-fs.d.ts +22 -0
- package/dist/testing/memory-fs.js +1 -0
- package/package.json +39 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- OpenCode MCP layout selection (core-tools#164). `McpInstallRequest.openCode` and `generateConfigEntry`/`clientJSONPath` options take an `OpenCodeMcpPolicy`: `{ mode: 'authoritative', layout: 'v1' | 'v2' }` writes and prunes the other layout, `{ mode: 'infer-existing' }` follows the layout the config already uses (v1 when both exist, the entry OpenCode launches) and never prunes. Omitting it keeps the 0.1.1 behavior (v2, v1 pruned). `inspectMcpInstall` results and `installMcp` results report the selected `openCodeLayout`.
|
|
14
|
+
- `inspectMcpInstall` reports `missing` with the new reason `layout-unknown` when `infer-existing` finds no GitKraken entry in either OpenCode layout (including a missing or blank file), the cases where `installMcp` would throw `OpenCodeMcpLayoutUnknownError`. `McpInstallStatus.openCodeDuplicateLocation` is the JSON pointer of the GitKraken entry in the OpenCode layout that was not compared, present whenever both layouts hold one; it is reported only and never pruned by `infer-existing`.
|
|
15
|
+
- `OpenCodeMcpLayoutUnknownError` for `infer-existing` with no GitKraken entry to follow, or in a read-free helper; `OpenCodeMcpCleanupError`, carrying the committed `result`, when the selected entry was written but removing the other layout's entry failed. Cancellation (`AbortError`) is rethrown as-is.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- OpenCode migration is two writes: the selected entry first with the other layout retained, then the removal, after re-reading to confirm the file is unchanged. A v2 write refuses a flat server named `servers` and a non-object `mcp` or `mcp.servers`.
|
|
20
|
+
- `inspectMcpInstall` reports a prunable OpenCode duplicate with the selected entry and location (previously the duplicate's), and an entry only in the other layout as `legacy-location` in either direction.
|
|
21
|
+
- `inspectMcpInstall` reports an OpenCode config that `installMcp` refuses (a non-object root, `mcp` or `mcp.servers`, or a flat server named `servers` under a v2 policy) as `missing` with the new reason `unsafe-config`, instead of `no-entry`.
|
|
22
|
+
- Safe OpenCode migration needs the host `FileSystemPort.writeFile` to leave the previous file intact when it rejects. Custom fields inside a replaced GitKraken entry are not carried across layouts.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `uninstallMcp` and `getClientInstallation` no longer treat a `GitKraken` key on a flat OpenCode server named `servers` as a v2 entry, so uninstall leaves it intact.
|
|
27
|
+
|
|
28
|
+
## [0.1.1] - 2026-10-02
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Initial scaffold of `@gitkraken/agent-tools` (P6, gkcli#1726): `AgentHostConfig`, `joinPath`, and the `./testing` subpath with an in-memory `FileSystemPort`.
|
|
33
|
+
- The 16-id MCP client registry (`gemini` retained as legacy, uninstall-only), path resolution over `AgentHostConfig.paths`, `installMcp`/`uninstallMcp` (comment-preserving JSONC via `jsonc-parser`, comment-preserving TOML splicing for Codex), `inspectMcpInstall`, `detectClient`/`getClientInstallation`, and `getMcpCliInstallCommand`.
|
|
34
|
+
- `installMcp` and `uninstallMcp` report each file they touched (created, updated or unchanged), the locations written or removed and why nothing was removed; a re-run of `installMcp` writes nothing. `inspectMcpInstall` reports why an entry is missing or drifted, `detectClient` reports how a client was found, and `getClientInstallation` lists the files that carry an entry. The shared `FileChange` type records per-file changes.
|
|
35
|
+
- OpenCode is installed in its v2 shape; a v1 entry is pruned on install, reported as a legacy location by `inspectMcpInstall`, and still removed by `uninstallMcp`.
|
|
36
|
+
- Per-host hook install/uninstall (`installHooks`, `uninstallHooks`): JSON hook writers, the Claude Code marketplace plugin, and opencode/pi relay plugins, plus the `eventManifest` each host is registered against.
|
|
37
|
+
- `installHooks` and `uninstallHooks` report the hooks file, the events installed, removed or still hooked, and each file created, updated, unchanged or removed, with a `reason` when nothing was removed. Refusals throw `HookConfigError`, carrying a kebab-case `code` and the `path` involved.
|
|
38
|
+
- `listAgents` reports how each MCP row was detected (`detection`) and which config files carry a GitKraken entry (`mcpConfigPaths`).
|
|
39
|
+
- `inspectSkillStatus`/`installSkill`/`uninstallSkill` and per-agent skill directory resolution, with skill ownership recorded in `.gk-skill.json`. Results report the skill directory, each file created, updated, unchanged or removed, and machine-readable status reasons; conflicts throw errors carrying a `code`, the `operation` and the `path`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Core Tools License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Axosoft, LLC dba GitKraken ("GitKraken")
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") may be compiled as part of GitKraken software products, including the gitkraken/vscode-gitlens open source project (collectively, the "GitKraken Products"), to the extent the Software is a required component of the GitKraken Products; provided, however, that the Software and its functionality may only be used if you (and any entity that you represent) have agreed to, and are in compliance with, the GitKraken End User License Agreement, available at https://gitkraken.com/eula (the "EULA"), or other agreement governing the use of the Software, as agreed by you and GitKraken, and otherwise have a valid subscription for the correct number of user seats for the applicable version of the Software (e.g., GitLens Free+, GitLens Pro, GitLens Teams, and GitLens Enterprise).
|
|
6
|
+
|
|
7
|
+
The Software is licensed, not sold. This license only gives you some rights to use the Software. GitKraken reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the Software only as expressly permitted in this license. In doing so, you must comply with any technical limitations in the Software that only allow you to use it in certain ways. You may not (i) work around any technical limitations in the Software, (ii) reverse engineer, decompile or disassemble the Software, or otherwise attempt to derive the source code for the Software, except and to the extent required by third party licensing terms governing use of certain open source components that may be included with the Software, (iii) remove, minimize, block or modify any notices of GitKraken or its suppliers in the Software, (iv) use the software in any way that is against the law, (v) host, share, publish, rent or lease the Software; (vi) distribute the Software as a stand-alone or integrated offering or combine it with any of your applications for others to use, (vii) use the Software other than in connection with validly licensed GitKraken Products or (viii) use any portion of the Software to create software with the same or similar functionality.
|
|
8
|
+
|
|
9
|
+
You agree that GitKraken and/or its licensors (as applicable) retain all right, title and interest in and to the Software and all modifications and/or patches thereto. You are not granted any other rights beyond what is expressly stated herein. Except as set forth above, it is forbidden to copy, merge, publish, distribute, sublicense, modify and/or sell the Software.
|
|
10
|
+
|
|
11
|
+
If you give feedback about the Software to GitKraken, you give to GitKraken, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires GitKraken to license its software or documentation to third parties because we include your feedback in them. These rights survive this License.
|
|
12
|
+
|
|
13
|
+
The full text of this Core Tools License shall be included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
This License and the EULA, if applicable, are the entire agreement for the Software. You must comply with all domestic and international export laws and regulations that apply to the Software, which include restrictions on destinations, end users and end use. This License is governed and construed by the laws of the State of Arizona, United States without regard to any choice or conflict of law principles or rules.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL GITKRAKEN, THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
18
|
+
|
|
19
|
+
For all third party components incorporated into the Software, those components are licensed under the original license provided by the owner of the applicable component.
|
package/README.md
CHANGED
|
@@ -1,3 +1,164 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gitkraken/agent-tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The current scaffold exports `AgentHostConfig`, `AgentHostPaths`, `AgentOrigin`, `joinPath`, and a testing `MemoryFileSystem`. Client detection, MCP and skill configuration, and hook configuration are planned for later layers.
|
|
4
|
+
|
|
5
|
+
The package is **ports & adapters**: it never touches the real filesystem, the environment, or a child process directly. `AgentHostConfig` describes the host-provided platform, paths, filesystem port, and optional process facts. `joinPath` joins path segments using `/` without importing `node:path`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { joinPath } from '@gitkraken/agent-tools';
|
|
9
|
+
|
|
10
|
+
const settingsPath = joinPath('/Users/me', '.config', 'settings.json');
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add @gitkraken/agent-tools
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Host configuration types
|
|
20
|
+
|
|
21
|
+
Import the configuration types when implementing a host adapter:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import type { AgentHostConfig } from '@gitkraken/agent-tools';
|
|
25
|
+
|
|
26
|
+
const config: AgentHostConfig = {
|
|
27
|
+
platform: 'darwin', // 'darwin' | 'linux' | 'win32'
|
|
28
|
+
paths: {
|
|
29
|
+
home: '/Users/me',
|
|
30
|
+
configHome: '/Users/me/.config', // resolved $XDG_CONFIG_HOME
|
|
31
|
+
appSupport: '/Users/me/Library/Application Support',
|
|
32
|
+
// claudeConfigDir, copilotHome, grokHome, piAgentDir: resolved env overrides, when set
|
|
33
|
+
},
|
|
34
|
+
fs: myFileSystemPort, // @gitkraken/shared-tools FileSystemPort
|
|
35
|
+
resolveExecutable: (command) => myPathLookup(command), // optional: a client's binary on PATH
|
|
36
|
+
openCodeMajor: 2, // optional: the installed OpenCode major version (0/undefined = unknown); does not select the MCP layout
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## MCP install/uninstall
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { getMcpCliInstallCommand, inspectMcpInstall, installMcp, uninstallMcp } from '@gitkraken/agent-tools';
|
|
44
|
+
|
|
45
|
+
// 1. Try the client's own CLI first, when it has one. `undefined` means go straight to step 2.
|
|
46
|
+
const request = { client: 'vscode', executablePath: '/usr/local/bin/gk', args: ['mcp', '--host=vscode'] } as const;
|
|
47
|
+
const argv = await getMcpCliInstallCommand(request, config);
|
|
48
|
+
if (argv) {
|
|
49
|
+
const ok = await myHost.run(argv); // your own process spawn
|
|
50
|
+
if (ok) return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Fall back to a direct, comment-preserving config-file write.
|
|
54
|
+
const installed = await installMcp(request, config);
|
|
55
|
+
// { client, path, change: 'created' | 'updated' | 'unchanged', format, location, entry, removedLocations }
|
|
56
|
+
|
|
57
|
+
// Drift check, e.g. for a status command:
|
|
58
|
+
const status = await inspectMcpInstall(request, config);
|
|
59
|
+
// { state: 'missing' | 'drifted' | 'current', reason?, key?, entry?, path, location }
|
|
60
|
+
|
|
61
|
+
const removal = await uninstallMcp({ client: 'vscode' }, config);
|
|
62
|
+
// { client, removed, files: [{ path, change, removedLocations, reason? }] }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`installMcp`/`uninstallMcp`/`inspectMcpInstall` accept an optional `configPath` to target a specific file directly instead of the client's resolved config path.
|
|
66
|
+
|
|
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
|
+
|
|
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.
|
|
94
|
+
|
|
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.
|
|
96
|
+
|
|
97
|
+
JSON/JSONC config files are edited losslessly via `jsonc-parser` (comments and formatting outside the edited range survive); Codex's TOML config is edited by splicing raw text, the same way gkcli does, because a full parse/marshal round trip drops comments and reorders sibling tables.
|
|
98
|
+
|
|
99
|
+
## Hook install/uninstall
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { HookConfigError, installHooks, uninstallHooks } from '@gitkraken/agent-tools';
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const installed = await installHooks({ host: 'codex', executablePath: '/usr/local/bin/gk' }, config);
|
|
106
|
+
// { host, path, events, blockingEvents, files: [{ path, change: 'created' | 'updated' | 'unchanged' | 'removed' }] }
|
|
107
|
+
|
|
108
|
+
const removed = await uninstallHooks('codex', config, ['PreToolUse']);
|
|
109
|
+
// { host, path, removedEvents, remainingEvents, files, reason?: 'file-missing' | 'not-installed' }
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof HookConfigError) {
|
|
112
|
+
console.error(error.code, error.path); // e.g. 'invalid-json', 'wrong-shape', 'unmanaged-file'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
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
|
+
|
|
119
|
+
## Client detection
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { allClients, clientDisplayName, detectClient, getClientInstallation } from '@gitkraken/agent-tools';
|
|
123
|
+
|
|
124
|
+
for (const client of allClients()) {
|
|
125
|
+
// 15 installable clients; 'gemini' is retained separately as legacy, uninstall-only.
|
|
126
|
+
const detection = await detectClient(client, config); // { detected, source?: 'executable' | 'config-file' | 'config-dir', reason?, checkedPaths, ... }
|
|
127
|
+
const installation = await getClientInstallation(client, config); // { installed, paths }: the files that carry a GitKraken entry
|
|
128
|
+
console.log(clientDisplayName(client), detection.detected, installation.installed);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Skills
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { installSkill, SkillConflictError, uninstallSkill } from '@gitkraken/agent-tools';
|
|
136
|
+
|
|
137
|
+
const request = { agent: 'cursor', name: 'commit-composer', files: { 'SKILL.md': skillMarkdown } } as const;
|
|
138
|
+
|
|
139
|
+
// action: 'create' | 'replace' | 'unchanged'. path is the skill directory; files lists each content file
|
|
140
|
+
// and the .gk-skill.json manifest as created, updated or unchanged (empty when nothing needed writing).
|
|
141
|
+
const { action, path, files, status } = await installSkill(request, config);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// action: 'remove' | 'absent'. files lists what was removed, including the directory once it was left empty.
|
|
145
|
+
await uninstallSkill(request, config);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Modified or foreign content requires `force`. Errors carry `code`, `operation` and `path`.
|
|
148
|
+
if (error instanceof SkillConflictError && error.code === 'modified') {
|
|
149
|
+
await uninstallSkill(request, config, { force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`status` is the skill's state before the action: `state` is `'missing' | 'current' | 'older-release' | 'modified'`, `reason` is a kebab-case code (for example `'content-changed'` or `'unowned'`) and is absent when the skill is current and clean, and `path` is the skill directory.
|
|
155
|
+
|
|
156
|
+
The host supplies bundled skill content. The library records ownership in `.gk-skill.json` through the injected filesystem port and protects modified files during install and uninstall; see `ARCHITECTURE.md`.
|
|
157
|
+
|
|
158
|
+
## Testing
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { MemoryFileSystem } from '@gitkraken/agent-tools/testing';
|
|
162
|
+
|
|
163
|
+
const fs = new MemoryFileSystem({ '/Users/me/.cursor/mcp.json': '{}' });
|
|
164
|
+
```
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AgentHostConfig } from "./config.js";
|
|
2
|
+
import { HookHost } from "./hooks/events.js";
|
|
3
|
+
import { Client, ClientDetection, ClientType } from "./mcp/clients.js";
|
|
4
|
+
|
|
5
|
+
//#region src/agents.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A hook client's reported capability, supplied by the host. The host computes
|
|
8
|
+
* it from this package's hook writers and passes it to {@link listAgents}, which
|
|
9
|
+
* merges it with the MCP client registry.
|
|
10
|
+
*/
|
|
11
|
+
interface HookCapability {
|
|
12
|
+
/**
|
|
13
|
+
* The MCP client id(s) this hook capability's row(s) should be merged
|
|
14
|
+
* into (gkcli's `MCPNamesForHook`). A capability reported under more than
|
|
15
|
+
* one id — e.g. Antigravity's shared IDE/CLI hooks.json — does not let
|
|
16
|
+
* either row's `detected`/`executable` clobber the other's.
|
|
17
|
+
*/
|
|
18
|
+
mcpNames: readonly string[];
|
|
19
|
+
displayName: string;
|
|
20
|
+
clientType?: ClientType;
|
|
21
|
+
detected: boolean;
|
|
22
|
+
executable?: string;
|
|
23
|
+
installed: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface Agent {
|
|
26
|
+
name: string;
|
|
27
|
+
type: ClientType | 'unknown';
|
|
28
|
+
displayName: string;
|
|
29
|
+
/** Merged across MCP and hook detection; `detection` holds the MCP-registry result alone. */
|
|
30
|
+
detected: boolean;
|
|
31
|
+
executable?: string;
|
|
32
|
+
/** How the MCP registry found, or failed to find, the client. Absent on hook-only rows. */
|
|
33
|
+
detection?: ClientDetection;
|
|
34
|
+
mcpSupported: boolean;
|
|
35
|
+
mcpInstalled: boolean;
|
|
36
|
+
/** The config files that carry a GitKraken MCP entry; empty on hook-only rows. */
|
|
37
|
+
mcpConfigPaths: string[];
|
|
38
|
+
hooksSupported: boolean;
|
|
39
|
+
hooksInstalled: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The unified list of agents, sorted by name: every MCP client, plus any
|
|
43
|
+
* hook capability the host passes in (see {@link HookCapability}). Mirrors
|
|
44
|
+
* gkcli's `internal/agents.List`.
|
|
45
|
+
*/
|
|
46
|
+
declare function listAgents(config: AgentHostConfig, hookCapabilities?: readonly HookCapability[]): Promise<Agent[]>;
|
|
47
|
+
/** MCP row ids that report one hook host's capability. */
|
|
48
|
+
declare function mcpNamesForHook(host: HookHost): string[];
|
|
49
|
+
/** Resolve aliases accepted by `gk ai hook` to the backing hook registry id. */
|
|
50
|
+
declare function resolveHookClient(client: string): string;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { Agent, type Client, HookCapability, listAgents, mcpNamesForHook, resolveHookClient };
|
package/dist/agents.js
ADDED
|
@@ -0,0 +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};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { FileSystemPort } from "@gitkraken/shared-tools";
|
|
2
|
+
|
|
3
|
+
//#region src/config.d.ts
|
|
4
|
+
/** Origin metadata for the process driving this library, mirroring gkcli's `GITKRAKEN_AGENT_*` env vars. */
|
|
5
|
+
interface AgentOrigin {
|
|
6
|
+
client?: string;
|
|
7
|
+
kind?: string;
|
|
8
|
+
clientRunId?: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Host-resolved filesystem locations this library joins segments onto. The
|
|
13
|
+
* host resolves these (env vars, `os.homedir()`, platform APIs); the library
|
|
14
|
+
* never reads the environment or the real filesystem directly.
|
|
15
|
+
*/
|
|
16
|
+
interface AgentHostPaths {
|
|
17
|
+
/** The user's home directory. */
|
|
18
|
+
home: string;
|
|
19
|
+
/** Resolved `$XDG_CONFIG_HOME`, or its per-platform default (e.g. `~/.config`). */
|
|
20
|
+
configHome: string;
|
|
21
|
+
/** Per-platform "app support" root: `%APPDATA%` on Windows, `~/Library/Application Support` on macOS, and `configHome` on Linux. */
|
|
22
|
+
appSupport: string;
|
|
23
|
+
/** Windows `%LOCALAPPDATA%` (distinct from `appSupport`, which is `%APPDATA%`/Roaming). Only JetBrains Copilot's Windows path uses it; falls back to `home/AppData/Local` when omitted. */
|
|
24
|
+
localAppData?: string;
|
|
25
|
+
/** Resolved `$CLAUDE_CONFIG_DIR`, when set. */
|
|
26
|
+
claudeConfigDir?: string;
|
|
27
|
+
/** Resolved `$COPILOT_HOME`, when set. */
|
|
28
|
+
copilotHome?: string;
|
|
29
|
+
/** Resolved `$GROK_HOME`, when set. */
|
|
30
|
+
grokHome?: string;
|
|
31
|
+
/** Resolved, tilde-expanded `$PI_CODING_AGENT_DIR`, when set. */
|
|
32
|
+
piAgentDir?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Shared configuration for every function in `@gitkraken/agent-tools`. The
|
|
36
|
+
* host injects the filesystem, all paths, and any process/environment facts
|
|
37
|
+
* (origin, insights, the installed OpenCode major) the Go implementation read
|
|
38
|
+
* directly; this library never touches `process.env`, `os.homedir()`, spawns
|
|
39
|
+
* a child process, or writes to stdout.
|
|
40
|
+
*/
|
|
41
|
+
interface AgentHostConfig {
|
|
42
|
+
platform: 'darwin' | 'linux' | 'win32';
|
|
43
|
+
paths: AgentHostPaths;
|
|
44
|
+
fs: FileSystemPort;
|
|
45
|
+
/** Was `GITKRAKEN_AGENT_ORIGIN_CLIENT`/`ORIGIN_KIND`/`CLIENT_RUN_ID`/`ORIGIN_ID`. */
|
|
46
|
+
origin?: AgentOrigin;
|
|
47
|
+
/** Was `GK_CC_INSIGHTS`. */
|
|
48
|
+
insights?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The installed OpenCode major version, or `undefined`/`0` when unknown —
|
|
51
|
+
* the same sentinel Go's `OpenCodeMajor()` returns. Go probes `opencode
|
|
52
|
+
* --version`; this library never spawns a process, so the host resolves
|
|
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.
|
|
55
|
+
*/
|
|
56
|
+
openCodeMajor?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Resolves a command name to its absolute path on `PATH`, or `undefined`
|
|
59
|
+
* when it is not found. Go's client detection calls `exec.LookPath`
|
|
60
|
+
* directly; this library never reads `PATH` itself, so the host injects
|
|
61
|
+
* the lookup (not part of the plan's original `AgentHostConfig` sketch —
|
|
62
|
+
* added because client detection has no other way to learn this).
|
|
63
|
+
*/
|
|
64
|
+
resolveExecutable?: (command: string) => string | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Joins path segments with `/`, matching D7: libraries never use `node:path`. */
|
|
67
|
+
declare function joinPath(...segments: string[]): string;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { AgentHostConfig, AgentHostPaths, AgentOrigin, joinPath };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(...e){let t=[];for(let n of e){if(!n)continue;let e=n.replace(/^\/+/,``).replace(/\/+$/,``);e&&t.push(e)}let n=e[0]??``;return(n.startsWith(`//`)?`//`:n.startsWith(`/`)?`/`:``)+t.join(`/`)}export{e as joinPath};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FileSystemPort } from "@gitkraken/shared-tools";
|
|
2
|
+
|
|
3
|
+
//#region src/file-change.d.ts
|
|
4
|
+
/** What one operation did to one file or directory. */
|
|
5
|
+
type FileChangeKind = 'created' | 'updated' | 'unchanged' | 'removed';
|
|
6
|
+
/** One file or directory an install or uninstall touched, and what happened to it. */
|
|
7
|
+
interface FileChange {
|
|
8
|
+
path: string;
|
|
9
|
+
change: FileChangeKind;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Writes `content` to `path` unless it already holds exactly that, and records
|
|
13
|
+
* the outcome in `changes`. `options.mode` is applied only when the file did
|
|
14
|
+
* not exist, so an existing file keeps the permissions its owner chose.
|
|
15
|
+
*/
|
|
16
|
+
declare function writeFileTracked(fs: FileSystemPort, path: string, content: string, changes: FileChange[], options?: {
|
|
17
|
+
mode?: number;
|
|
18
|
+
}): Promise<Exclude<FileChangeKind, 'removed'>>;
|
|
19
|
+
/**
|
|
20
|
+
* Removes `path` and records `'removed'` in `changes`, or records nothing when
|
|
21
|
+
* it was already absent. A directory (`options.recursive`) is probed with
|
|
22
|
+
* `readDir` rather than `readFile`.
|
|
23
|
+
*/
|
|
24
|
+
declare function removeTracked(fs: FileSystemPort, path: string, changes: FileChange[], options?: {
|
|
25
|
+
recursive?: boolean;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { FileChange, FileChangeKind, removeTracked, writeFileTracked };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async function e(e,t,n,r,i){let a=await e.readFile(t);if(a===n)return r.push({path:t,change:`unchanged`}),`unchanged`;await e.writeFile(t,n,a===null?i:void 0);let o=a===null?`created`:`updated`;return r.push({path:t,change:o}),o}async function t(e,t,n,r){let i=r?.recursive?await e.readDir(t)!==null:await e.readFile(t)!==null;await e.remove(t,r),i&&n.push({path:t,change:`removed`})}export{t as removeTracked,e as writeFileTracked};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { HookInstallResult, HookUninstallResult } from "./results.js";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/claude.d.ts
|
|
5
|
+
interface ClaudeHookRequest {
|
|
6
|
+
executablePath: string;
|
|
7
|
+
events?: readonly string[];
|
|
8
|
+
blockingEvents?: readonly string[];
|
|
9
|
+
version?: string;
|
|
10
|
+
/** Host-supplied timestamp for reproducible installs and tests. */
|
|
11
|
+
now?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function installClaudeHooks(request: ClaudeHookRequest, config: AgentHostConfig): Promise<HookInstallResult>;
|
|
14
|
+
declare function uninstallClaudeHooks(events: readonly string[] | undefined, config: AgentHostConfig): Promise<HookUninstallResult>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ClaudeHookRequest, installClaudeHooks, uninstallClaudeHooks };
|
|
@@ -0,0 +1,3 @@
|
|
|
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};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/hooks/errors.d.ts
|
|
2
|
+
type HookConfigErrorCode = 'invalid-json' | 'wrong-shape' | 'invalid-event' | 'unsupported-host' | 'blocking-unsupported' | 'unmanaged-file';
|
|
3
|
+
/** A hook install or uninstall was refused; `path` names the file involved, when there is one. An existing file is left untouched. */
|
|
4
|
+
declare class HookConfigError extends Error {
|
|
5
|
+
readonly code: HookConfigErrorCode;
|
|
6
|
+
readonly path?: string | undefined;
|
|
7
|
+
constructor(code: HookConfigErrorCode, message: string, path?: string | undefined);
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { HookConfigError, HookConfigErrorCode };
|
|
@@ -0,0 +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};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/hooks/events.d.ts
|
|
2
|
+
declare const eventManifest: {
|
|
3
|
+
readonly 'claude-code': {
|
|
4
|
+
readonly events: readonly ["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"];
|
|
5
|
+
readonly skippedDefaultEvents: readonly ["Setup", "MessageDisplay", "FileChanged", "WorktreeCreate", "WorktreeRemove"];
|
|
6
|
+
readonly backfillEvents: readonly ["PostModelSwitch"];
|
|
7
|
+
readonly sessionEndEvent: "SessionEnd";
|
|
8
|
+
readonly sessionStartEvent: "SessionStart";
|
|
9
|
+
};
|
|
10
|
+
readonly opencode: {
|
|
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"];
|
|
13
|
+
readonly v2EventAliases: {
|
|
14
|
+
readonly 'session.status': readonly ["session.updated"];
|
|
15
|
+
readonly 'session.execution.failed': readonly ["session.error"];
|
|
16
|
+
};
|
|
17
|
+
readonly skippedDefaultEvents: readonly ["message.updated", "message.part.updated", "installation.updated"];
|
|
18
|
+
readonly sessionEndEvent: "session.deleted";
|
|
19
|
+
readonly sessionStartEvent: "session.created";
|
|
20
|
+
};
|
|
21
|
+
readonly cursor: {
|
|
22
|
+
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"];
|
|
24
|
+
readonly sessionEndEvent: "sessionEnd";
|
|
25
|
+
readonly sessionStartEvent: "sessionStart";
|
|
26
|
+
};
|
|
27
|
+
readonly codex: {
|
|
28
|
+
readonly events: readonly ["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact", "SubagentStart", "SubagentStop", "Stop", "Interrupt", "SessionEnd"];
|
|
29
|
+
readonly skippedDefaultEvents: readonly [];
|
|
30
|
+
readonly backfillEvents: readonly ["SessionEnd", "Interrupt"];
|
|
31
|
+
readonly sessionEndEvent: "SessionEnd";
|
|
32
|
+
readonly sessionStartEvent: "SessionStart";
|
|
33
|
+
readonly windowsShell: "powershell";
|
|
34
|
+
};
|
|
35
|
+
readonly copilot: {
|
|
36
|
+
readonly events: readonly ["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Stop", "SubagentStop", "ErrorOccurred", "PreCompact", "Notification", "PermissionRequest"];
|
|
37
|
+
readonly legacyEvents: readonly ["sessionStart", "sessionEnd", "userPromptSubmitted", "preToolUse", "postToolUse", "postToolUseFailure", "agentStop", "subagentStart", "subagentStop", "errorOccurred", "preCompact", "notification", "permissionRequest"];
|
|
38
|
+
readonly skippedDefaultEvents: readonly [];
|
|
39
|
+
readonly sessionEndEvent: "SessionEnd";
|
|
40
|
+
readonly sessionStartEvent: "SessionStart";
|
|
41
|
+
readonly windowsShell: "powershell";
|
|
42
|
+
};
|
|
43
|
+
readonly antigravity: {
|
|
44
|
+
readonly events: readonly ["PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"];
|
|
45
|
+
readonly skippedDefaultEvents: readonly [];
|
|
46
|
+
readonly sessionEndEvent: "Stop";
|
|
47
|
+
readonly sessionStartEvent: "";
|
|
48
|
+
};
|
|
49
|
+
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"];
|
|
52
|
+
readonly sessionEndEvent: "session_shutdown";
|
|
53
|
+
readonly sessionStartEvent: "session_start";
|
|
54
|
+
};
|
|
55
|
+
readonly augment: {
|
|
56
|
+
readonly events: readonly ["SessionStart", "SessionEnd", "PreToolUse", "PostToolUse", "Stop", "Notification", "PromptSubmit"];
|
|
57
|
+
readonly skippedDefaultEvents: readonly [];
|
|
58
|
+
readonly sessionEndEvent: "SessionEnd";
|
|
59
|
+
readonly sessionStartEvent: "SessionStart";
|
|
60
|
+
readonly blockingTimeoutMultiplier: 1000;
|
|
61
|
+
readonly promptSubmitEvent: "PromptSubmit";
|
|
62
|
+
};
|
|
63
|
+
readonly grok: {
|
|
64
|
+
readonly events: readonly ["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionDenied", "Stop", "StopFailure", "StopCancelled", "Notification", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact"];
|
|
65
|
+
readonly skippedDefaultEvents: readonly [];
|
|
66
|
+
readonly sessionEndEvent: "SessionEnd";
|
|
67
|
+
readonly sessionStartEvent: "SessionStart";
|
|
68
|
+
readonly windowsShell: "powershell";
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
type HookHost = keyof typeof eventManifest;
|
|
72
|
+
//#endregion
|
|
73
|
+
export { HookHost, eventManifest };
|
|
@@ -0,0 +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};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { HookHost } from "./events.js";
|
|
3
|
+
import { HookInstallResult, HookUninstallResult } from "./results.js";
|
|
4
|
+
import { HookWriteRequest } from "./writers.js";
|
|
5
|
+
|
|
6
|
+
//#region src/hooks/install.d.ts
|
|
7
|
+
interface InstallHooksRequest extends HookWriteRequest {
|
|
8
|
+
/** Host-supplied build version for Claude's cache directory. */
|
|
9
|
+
version?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Dispatches hook config writes for all supported agent hosts. The host supplies the stable launcher path. */
|
|
12
|
+
declare function installHooks(request: InstallHooksRequest, config: AgentHostConfig): Promise<HookInstallResult>;
|
|
13
|
+
declare function uninstallHooks(host: HookHost, config: AgentHostConfig, events?: readonly string[]): Promise<HookUninstallResult>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { InstallHooksRequest, installHooks, uninstallHooks };
|
|
@@ -0,0 +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};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HookHost } from "./events.js";
|
|
2
|
+
import { FileChange } from "../file-change.js";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/results.d.ts
|
|
5
|
+
interface HookInstallResult {
|
|
6
|
+
host: HookHost;
|
|
7
|
+
/** The hooks file the agent reads. */
|
|
8
|
+
path: string;
|
|
9
|
+
/** The non-blocking events this call installed. */
|
|
10
|
+
events: string[];
|
|
11
|
+
/** The blocking events this call installed. */
|
|
12
|
+
blockingEvents: string[];
|
|
13
|
+
/** Every file written or removed, in order. */
|
|
14
|
+
files: FileChange[];
|
|
15
|
+
}
|
|
16
|
+
interface HookUninstallResult {
|
|
17
|
+
host: HookHost;
|
|
18
|
+
/** The hooks file the agent reads. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Events whose GitKraken hooks this call removed. */
|
|
21
|
+
removedEvents: string[];
|
|
22
|
+
/** Events that still carry a GitKraken hook after this call, as after a partial uninstall. */
|
|
23
|
+
remainingEvents: string[];
|
|
24
|
+
/** Every file written or removed, in order. */
|
|
25
|
+
files: FileChange[];
|
|
26
|
+
/** Set when no GitKraken hook was removed: the hooks file does not exist, or holds none for the requested events. */
|
|
27
|
+
reason?: 'file-missing' | 'not-installed';
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { HookInstallResult, HookUninstallResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { HookInstallResult, HookUninstallResult } from "./results.js";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/templates.d.ts
|
|
5
|
+
type MarkerHost = 'opencode' | 'pi';
|
|
6
|
+
interface MarkerHookRequest {
|
|
7
|
+
host: MarkerHost;
|
|
8
|
+
executablePath: string;
|
|
9
|
+
events?: readonly string[];
|
|
10
|
+
blockingEvents?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
declare function installMarkerHooks(request: MarkerHookRequest, config: AgentHostConfig): Promise<HookInstallResult>;
|
|
13
|
+
declare function uninstallMarkerHooks(host: MarkerHost, events: readonly string[] | undefined, config: AgentHostConfig): Promise<HookUninstallResult>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { MarkerHookRequest, MarkerHost, installMarkerHooks, uninstallMarkerHooks };
|