@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +19 -0
  3. package/README.md +163 -2
  4. package/dist/agents.d.ts +52 -0
  5. package/dist/agents.js +1 -0
  6. package/dist/config.d.ts +69 -0
  7. package/dist/config.js +1 -0
  8. package/dist/file-change.d.ts +28 -0
  9. package/dist/file-change.js +1 -0
  10. package/dist/hooks/claude.d.ts +16 -0
  11. package/dist/hooks/claude.js +3 -0
  12. package/dist/hooks/errors.d.ts +10 -0
  13. package/dist/hooks/errors.js +1 -0
  14. package/dist/hooks/events.d.ts +73 -0
  15. package/dist/hooks/events.js +1 -0
  16. package/dist/hooks/install.d.ts +15 -0
  17. package/dist/hooks/install.js +1 -0
  18. package/dist/hooks/results.d.ts +30 -0
  19. package/dist/hooks/results.js +1 -0
  20. package/dist/hooks/templates.d.ts +15 -0
  21. package/dist/hooks/templates.js +278 -0
  22. package/dist/hooks/writers.d.ts +18 -0
  23. package/dist/hooks/writers.js +1 -0
  24. package/dist/index.d.ts +18 -0
  25. package/dist/index.js +1 -0
  26. package/dist/mcp/cli-install-command.d.ts +27 -0
  27. package/dist/mcp/cli-install-command.js +1 -0
  28. package/dist/mcp/clients.d.ts +96 -0
  29. package/dist/mcp/clients.js +1 -0
  30. package/dist/mcp/config-entry.d.ts +15 -0
  31. package/dist/mcp/config-entry.js +1 -0
  32. package/dist/mcp/index.d.ts +7 -0
  33. package/dist/mcp/index.js +1 -0
  34. package/dist/mcp/installer.d.ts +130 -0
  35. package/dist/mcp/installer.js +3 -0
  36. package/dist/mcp/jsonc-edit.d.ts +43 -0
  37. package/dist/mcp/jsonc-edit.js +3 -0
  38. package/dist/mcp/opencode-layout.d.ts +54 -0
  39. package/dist/mcp/opencode-layout.js +1 -0
  40. package/dist/mcp/toml-read.d.ts +11 -0
  41. package/dist/mcp/toml-read.js +1 -0
  42. package/dist/mcp/toml-safety.d.ts +7 -0
  43. package/dist/mcp/toml-safety.js +1 -0
  44. package/dist/mcp/toml-splice.d.ts +16 -0
  45. package/dist/mcp/toml-splice.js +11 -0
  46. package/dist/mcp/uninstaller.d.ts +41 -0
  47. package/dist/mcp/uninstaller.js +1 -0
  48. package/dist/skills/index.d.ts +4 -0
  49. package/dist/skills/index.js +1 -0
  50. package/dist/skills/install.d.ts +126 -0
  51. package/dist/skills/install.js +1 -0
  52. package/dist/skills/manifest.d.ts +56 -0
  53. package/dist/skills/manifest.js +1 -0
  54. package/dist/skills/skill-dirs.d.ts +22 -0
  55. package/dist/skills/skill-dirs.js +1 -0
  56. package/dist/testing/index.d.ts +2 -0
  57. package/dist/testing/index.js +1 -0
  58. package/dist/testing/memory-fs.d.ts +22 -0
  59. package/dist/testing/memory-fs.js +1 -0
  60. package/package.json +39 -3
@@ -0,0 +1,130 @@
1
+ import { AgentHostConfig } from "../config.js";
2
+ import { McpConfigEntry } from "./config-entry.js";
3
+ import { OpenCodeMcpLayout, OpenCodeMcpPolicy } from "./opencode-layout.js";
4
+ import { Client } from "./clients.js";
5
+
6
+ //#region src/mcp/installer.d.ts
7
+ interface McpInstallRequest {
8
+ client: Client;
9
+ executablePath: string;
10
+ /** The full MCP server argv the host computed (e.g. `['mcp', '--host=cursor']`); this library never derives it. */
11
+ args: readonly string[];
12
+ /**
13
+ * Targets this path instead of the client's resolved config path. Lets a
14
+ * host aim at a specific file, such as a workspace-scoped config.
15
+ */
16
+ configPath?: string;
17
+ /**
18
+ * Which OpenCode MCP layout to use; ignored for other clients. Omitted keeps the 0.1.1 behavior: v2 with the
19
+ * v1 entry pruned. A host that probed OpenCode should always pass a policy; see {@link OpenCodeMcpPolicy}.
20
+ */
21
+ openCode?: OpenCodeMcpPolicy;
22
+ }
23
+ /** How the client's config compares to what `installMcp` would write. */
24
+ type McpInstallState = 'missing' | 'drifted' | 'current';
25
+ /** Why {@link inspectMcpInstall} reported its state. Absent when the state is `current`. */
26
+ type McpInstallStateReason = /** `missing`: the client's config file does not exist. */'file-missing' /** `missing`: the config file does not parse, so no entry can be read. */ | 'invalid-config' /** `missing`: the config has no GitKraken entry. */ | 'no-entry'
27
+ /**
28
+ * `missing`: `infer-existing` found no GitKraken entry in either OpenCode layout, so `installMcp` cannot choose
29
+ * one and would throw {@link OpenCodeMcpLayoutUnknownError}; the host must supply an authoritative layout.
30
+ */
31
+ | 'layout-unknown'
32
+ /**
33
+ * `missing`: the GitKraken key exists but is not an object. Under `infer-existing` this is also fatal to
34
+ * `installMcp`, which throws instead of overwriting it; an authoritative install overwrites it.
35
+ */
36
+ | 'entry-not-object' /** `missing`: the OpenCode config's root, `mcp` or `mcp.servers` cannot take the entry without displacing other data, so `installMcp` refuses it. */ | 'unsafe-config' /** `missing`: the client id is unknown or uninstall-only, so it has no config to inspect. */ | 'unsupported-client'
37
+ /**
38
+ * `drifted`: the entry sits where an older install wrote it (Codex's literal-key table, OpenCode's other
39
+ * layout), or the selected OpenCode entry is current but a prunable duplicate remains in the other layout.
40
+ */
41
+ | 'legacy-location' /** `drifted`: the entry lacks `command` or `args`. */ | 'pinned-key-missing' /** `drifted`: a key `installMcp` writes holds a different value. */ | 'value-mismatch';
42
+ interface McpInstallStatus {
43
+ client: Client;
44
+ state: McpInstallState;
45
+ /** The config file inspected; empty when the client has none. */
46
+ path: string;
47
+ /** Where the entry was looked up, or found when it is a legacy location. */
48
+ location: string;
49
+ reason?: McpInstallStateReason;
50
+ /** The key that differs, for `pinned-key-missing` and `value-mismatch`. */
51
+ key?: string;
52
+ /**
53
+ * The entry on disk, when one exists. A host can read it to carry user-added args forward before reinstalling.
54
+ * For a prunable OpenCode duplicate this is the selected entry, not the duplicate.
55
+ */
56
+ entry?: Record<string, unknown>;
57
+ /**
58
+ * The OpenCode layout the inspection compared against; absent for other clients and when
59
+ * `infer-existing` found no entry (`layout-unknown`). Selection evidence only: it never authorizes pruning.
60
+ */
61
+ openCodeLayout?: OpenCodeMcpLayout;
62
+ /**
63
+ * The JSON pointer of the GitKraken entry in the OpenCode layout that was not compared, present whenever both
64
+ * layouts hold one. Reported only: `infer-existing` never prunes it. OpenCode can emit a conflict diagnostic
65
+ * when the two differ.
66
+ */
67
+ openCodeDuplicateLocation?: string;
68
+ }
69
+ interface McpInstallResult {
70
+ client: Client;
71
+ /** The config file written. */
72
+ path: string;
73
+ change: 'created' | 'updated' | 'unchanged';
74
+ format: 'json' | 'toml';
75
+ /** A JSON pointer, or the Codex dotted TOML table key. */
76
+ location: string;
77
+ /** The entry written, or already present when `change` is `unchanged`. */
78
+ entry: McpConfigEntry;
79
+ /** Stale locations pruned from the file, such as OpenCode's other-layout pointer; empty otherwise. */
80
+ removedLocations: string[];
81
+ /**
82
+ * The OpenCode layout written or found; absent for other clients. For an inferred layout this is selection
83
+ * evidence only and must not be copied into an authoritative policy.
84
+ */
85
+ openCodeLayout?: OpenCodeMcpLayout;
86
+ }
87
+ /**
88
+ * Writes the GitKraken entry in the shape each client reads. The shapes are
89
+ * compatible with entries gkcli writes, so either tool recognises and repairs
90
+ * the other's. A re-run that finds the entry already in place writes nothing.
91
+ * Port of gkcli's installer.
92
+ *
93
+ * OpenCode gets the layout `request.openCode` selects, v2 when omitted. An
94
+ * `authoritative` layout is written first and the other layout's GitKraken
95
+ * entry is removed in a second write; `infer-existing` follows the layout the
96
+ * config already uses, v1 when both are present because OpenCode launches that
97
+ * entry, and prunes nothing. If that removal fails, the selected entry stays
98
+ * and {@link OpenCodeMcpCleanupError} reports it; running the install again
99
+ * finishes the cleanup. Run the client's own CLI shortcut first
100
+ * when it has one (see {@link getMcpCliInstallCommand}); this is the direct
101
+ * config-file write.
102
+ *
103
+ * Safe OpenCode migration needs the host's `FileSystemPort.writeFile` to leave
104
+ * the previous file intact when it rejects (e.g. write a temp file, then
105
+ * rename); the library cannot verify that and adds no cross-process lock.
106
+ *
107
+ * Throws, without writing, for a client that is unknown or uninstall-only, for
108
+ * an existing config file that does not parse, for an OpenCode config whose
109
+ * `mcp` containers cannot take the entry without displacing other data, and
110
+ * for `infer-existing` with no GitKraken entry ({@link OpenCodeMcpLayoutUnknownError}).
111
+ */
112
+ declare function installMcp(request: McpInstallRequest, config: AgentHostConfig): Promise<McpInstallResult>;
113
+ /**
114
+ * Reports whether the client's config already holds an entry equivalent to
115
+ * the one `installMcp` would write, and if not, why. Mirrors Go's
116
+ * `installer.State`.
117
+ *
118
+ * For OpenCode with an `authoritative` (or omitted) policy, the selected
119
+ * layout is compared; when only the other layout holds an entry, or the
120
+ * selected entry is current but a prunable duplicate remains, the state is
121
+ * `drifted` with reason `legacy-location`. With `infer-existing`, only the
122
+ * entry the config already uses is compared. With both layouts present that is the v1 entry, which OpenCode's
123
+ * compatibility reader retains over the native one, and the other entry is only reported in
124
+ * `openCodeDuplicateLocation`. No entry, a missing file or an empty file reports `layout-unknown`, because
125
+ * `installMcp` would throw {@link OpenCodeMcpLayoutUnknownError}; there is no `openCodeLayout` and `location`
126
+ * is the v2 pointer.
127
+ */
128
+ declare function inspectMcpInstall(request: McpInstallRequest, config: AgentHostConfig): Promise<McpInstallStatus>;
129
+ //#endregion
130
+ export { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp };
@@ -0,0 +1,3 @@
1
+ import{addAtPointer as e,assertValidJsonc as t,findAtPointer as n,isValidJsonc as r,removeAtPointerAndEmptyParent as i}from"./jsonc-edit.js";import{OpenCodeMcpCleanupError as a,OpenCodeMcpLayoutUnknownError as o,openCodeNativeServers as s,openCodeRefusal as c,resolveOpenCodePolicy as l}from"./opencode-layout.js";import{assertValidToml as u,parseTomlSafe as d,tomlDottedPath as f,tomlLiteralKey as p}from"./toml-read.js";import{clientConfigPath as m,clientJSONPath as h,isInstallable as g,openCodeJSONPath as _}from"./clients.js";import{writeFileTracked as v}from"../file-change.js";import{generateConfigEntry as y}from"./config-entry.js";import{tomlSemanticEqual as b,withoutCodexEntry as x}from"./toml-safety.js";import{removeBlock as S,replaceOrAppendBlock as C}from"./toml-splice.js";import{stringify as w}from"smol-toml";const T=[`command`,`args`];async function E(t,r){let{client:i}=t,a=g(i)?t.configPath??await m(i,r):void 0;if(!a)throw Error(`unsupported client: ${i}`);if(i===`opencode`)return D(t,r,a);let o=y(i,t.executablePath,t.args);if(i===`codex`)return P(t,r,a,o);let s=await r.fs.readFile(a),c=s===null||s.trim()===``?`{}`:s,l=h(i),u=n(c,l),d=s!==null&&I(u)&&H(u,o,{client:i,path:a,location:l}).state===`current`;return{client:i,path:a,change:d?`unchanged`:await v(r.fs,a,e(c,l,o,a),[],{mode:420}),format:`json`,location:l,entry:d?u:o,removedLocations:[]}}async function D(n,r,o){let s=l(n.openCode),u=await r.fs.readFile(o),d=u===null||u.trim()===``?`{}`:u;t(d,o);let f=c(d,k(s),o);if(f!==void 0)throw Error(f);let p=s.mode===`authoritative`?s.layout:O(d,o),m=j(p),h=_(p),g=_(m),b=y(`opencode`,n.executablePath,n.args,{openCode:{mode:`authoritative`,layout:p}}),x=M(d,p),S=u!==null&&I(x)&&H(x,b,{client:`opencode`,path:o,location:h}).state===`current`,C=s.mode===`authoritative`?M(d,m):void 0,w=S?d:e(d,h,b,o),T=C===void 0?void 0:i(w,g,o),E=S?`unchanged`:await v(r.fs,o,w,[],{mode:420}),D=e=>({client:`opencode`,path:o,change:E,format:`json`,location:h,entry:S?x:b,removedLocations:e,openCodeLayout:p});if(T===void 0||!T.removed)return D([]);try{if(await r.fs.readFile(o)!==w)throw Error(`${o} changed while the OpenCode MCP entry was being installed`);let e=await v(r.fs,o,T.text,[],{mode:420});E===`unchanged`&&(E=e)}catch(e){throw A(e)?e:new a(D([]),e)}return D([g])}function O(e,t){let n=N(e);if(!n)throw new o;if(!I(n.found))throw Error(`cannot safely update OpenCode MCP entry in ${t}: the GitKraken entry is not an object`);return n.layout}function k(e){return e.mode===`authoritative`?e.layout:void 0}function A(e){return e instanceof Error&&e.name===`AbortError`}function j(e){return e===`v1`?`v2`:`v1`}function M(e,t){if(!(t===`v2`&&s(e)===void 0))return n(e,_(t))}function N(e){for(let t of[`v1`,`v2`]){let n=M(e,t);if(n!==void 0)return{layout:t,found:n}}}async function P(e,t,n,r){let i=await t.fs.readFile(n);i!==null&&i.trim()!==``&&u(i,n);let a=h(`codex`),o=i??``,s=d(o),c=f(s,a),l=p(s,a);if(i!==null&&l===void 0&&I(c)&&H(c,r,{client:e.client,path:n,location:a}).state===`current`)return{client:e.client,path:n,change:`unchanged`,format:`toml`,location:a,entry:c,removedLocations:[]};let m,g,_=[];if(l!==void 0){if(!I(l)||c!==void 0&&!I(c))throw Error(`cannot safely migrate Codex MCP entry in ${n}: ambiguous legacy and nested tables`);let e=I(c)&&c.env!==void 0?c.env:l.env;if(e!==void 0&&!I(e))throw Error(`cannot safely migrate Codex MCP entry in ${n}: legacy env is not a table`);if(Object.entries(l).some(([e,t])=>e!==`env`&&I(t))||e&&Object.values(e).some(I))throw Error(`cannot safely migrate Codex MCP entry in ${n}: unsupported legacy subtable`);g=e;let t=S(o,a).text;m=C(c===void 0?t:S(t,a).text,a,F(a,r,e)),_.push(`["${a}"]`)}else m=C(o,a,F(a,r));let y=d(m),w=f(y,a);if(!I(w)||H(w,r,{client:e.client,path:n,location:a}).state!==`current`||p(y,a)!==void 0||l!==void 0&&!b(w.env,g)||!b(x(y),x(s)))throw Error(`cannot safely update Codex MCP entry in ${n}: unsupported TOML representation`);let T=await v(t.fs,n,m,[],{mode:384});return{client:e.client,path:n,change:T,format:`toml`,location:a,entry:w,removedLocations:_}}function F(e,t,n){let r=w(t),i=r.endsWith(`
2
+ `)?r:`${r}\n`,a=n===void 0?``:w(n);return`[${e}]\n${i}${n===void 0?``:`\n[${e}.env]\n${a.endsWith(`
3
+ `)?a:`${a}\n`}`}`}function I(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function L(e,t){return{...e,state:`missing`,reason:t}}function R(e,t){return{...e,state:`drifted`,reason:`legacy-location`,...I(t)&&{entry:t}}}async function z(e,t){let{client:i}=e,a=i===`opencode`?l(e.openCode):void 0,o=a?.mode===`infer-existing`?void 0:a?.layout,s=i===`opencode`?_(o??`v2`):h(i),c=g(i)?e.configPath??await m(i,t):void 0;if(!c)return L({client:i,path:``,location:s},`unsupported-client`);let u={client:i,path:c,location:s,...o&&{openCodeLayout:o}},d=await t.fs.readFile(c);if(d===null)return L(u,a?.mode===`infer-existing`?`layout-unknown`:`file-missing`);if(i===`codex`)return B(d,y(i,e.executablePath,e.args),u);if(d.trim()===``)return L(u,a?.mode===`infer-existing`?`layout-unknown`:`no-entry`);if(!r(d))return L(u,`invalid-config`);if(a)return V(d,e,a,u);let f=n(d,s);if(f===void 0)return L(u,`no-entry`);let p=y(i,e.executablePath,e.args);return I(f)?H(f,p,u):L(u,`entry-not-object`)}function B(e,t,n){if(e.trim()===``)return L(n,`no-entry`);let r=d(e);if(!r)return L(n,`invalid-config`);let i=p(r,n.location);if(i!==void 0)return R(n,i);let a=f(r,n.location);return a===void 0?L(n,`no-entry`):I(a)?H(a,t,n):L(n,`entry-not-object`)}function V(e,t,n,r){let i=e=>y(`opencode`,t.executablePath,t.args,{openCode:{mode:`authoritative`,layout:e}});if(c(e,k(n),r.path)!==void 0)return L(r,`unsafe-config`);if(n.mode===`infer-existing`){let t=N(e);if(!t)return L(r,`layout-unknown`);let{layout:n,found:a}=t,o=j(n),s={...r,location:_(n),openCodeLayout:n,...M(e,o)!==void 0&&{openCodeDuplicateLocation:_(o)}};return I(a)?H(a,i(n),s):L(s,`entry-not-object`)}let a=M(e,n.layout),o=j(n.layout),s=M(e,o);if(a===void 0)return s===void 0?L(r,`no-entry`):R({...r,location:_(o)},s);let l=s===void 0?r:{...r,openCodeDuplicateLocation:_(o)};if(!I(a))return L(l,`entry-not-object`);let u=H(a,i(n.layout),l);return u.state===`current`&&s!==void 0?R(l,a):u}function H(e,t,n){for(let[r,i]of Object.entries(t)){if(!(r in e)){if(T.includes(r))return{...n,state:`drifted`,reason:`pinned-key-missing`,key:r,entry:e};continue}if(!U(e[r],i))return{...n,state:`drifted`,reason:`value-mismatch`,key:r,entry:e}}return{...n,state:`current`,entry:e}}function U(e,t){if(e===t)return!0;if(typeof e!=typeof t||e===null||t===null||typeof e!=`object`)return!1;if(Array.isArray(e)||Array.isArray(t))return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((e,n)=>U(e,t[n]));let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?n.every(n=>U(e[n],t[n])):!1}export{z as inspectMcpInstall,E as installMcp};
@@ -0,0 +1,43 @@
1
+ //#region src/mcp/jsonc-edit.d.ts
2
+ /** Whether `text` is valid JSONC: comments, a trailing comma and a leading UTF-8 BOM are all fine. */
3
+ declare function isValidJsonc(text: string): boolean;
4
+ /** Throws when `text` isn't valid JSONC; never touches `text`. */
5
+ declare function assertValidJsonc(text: string, path: string): void;
6
+ /** Reads the value at `pointer` in JSONC `text`, or `undefined` when absent or the document is unparsable. */
7
+ declare function findAtPointer(text: string, pointer: string): unknown;
8
+ /**
9
+ * Adds `value` at `pointer`, creating any missing parent objects along the
10
+ * way (mirrors gkcli's `installer.writeConfig`, which patches in each
11
+ * missing parent as `{}` before adding the leaf so a JSON-patch "add" never
12
+ * fails on a missing container). Comment-preserving: only the edited range
13
+ * is touched. `sourcePath` names `text`'s file for the error thrown when it
14
+ * isn't valid JSONC; no edit is made in that case.
15
+ */
16
+ declare function addAtPointer(text: string, pointer: string, value: unknown, sourcePath: string): string;
17
+ /**
18
+ * Removes the value at `pointer`, leaving its parent container in place even
19
+ * if that empties it. Mirrors gkcli's `uninstaller.writeConfig`, the shape
20
+ * every plain client's `UninstallMCP` uses. `sourcePath` names `text`'s file
21
+ * for the error thrown when it isn't valid JSONC; no edit is made in that case.
22
+ *
23
+ * Returns `removed: false` when there was nothing at `pointer` to begin with.
24
+ */
25
+ declare function removeAtPointer(text: string, pointer: string, sourcePath: string): {
26
+ text: string;
27
+ removed: boolean;
28
+ };
29
+ /**
30
+ * Removes the value at `pointer`, and then its parent container when that
31
+ * removal left it an empty object. Mirrors gkcli's
32
+ * `uninstaller.removeJSONPointer`, used only for OpenCode: v1 reads every key
33
+ * under `/mcp` as a server definition, so an empty `servers` husk would parse
34
+ * as one with no command. `sourcePath` is as for {@link removeAtPointer}.
35
+ *
36
+ * Returns `removed: false` when there was nothing at `pointer` to begin with.
37
+ */
38
+ declare function removeAtPointerAndEmptyParent(text: string, pointer: string, sourcePath: string): {
39
+ text: string;
40
+ removed: boolean;
41
+ };
42
+ //#endregion
43
+ export { addAtPointer, assertValidJsonc, findAtPointer, isValidJsonc, removeAtPointer, removeAtPointerAndEmptyParent };
@@ -0,0 +1,3 @@
1
+ import{applyEdits as e,findNodeAtLocation as t,getNodeValue as n,modify as r,parseTree as i,visit as a}from"jsonc-parser";const o={tabSize:2,insertSpaces:!0,eol:`
2
+ `},s={allowTrailingComma:!0};function c(e){return e.startsWith(``)?{bom:``,body:e.slice(1)}:{bom:``,body:e}}function l(e){let t=[];return i(c(e).body,t,s),t.length===0}function u(e,t){if(!l(e))throw Error(`invalid JSON in ${t}`)}function d(e){return e?e.split(`/`).slice(1).map(e=>e.replace(/~1/g,`/`).replace(/~0/g,`~`)):[]}function f(e,n){let r=[],i=e;for(let e of n){let n=i?.type===`array`&&/^(0|[1-9]\d*)$/.test(e)?Number(e):e;r.push(n),i=i?t(i,[n]):void 0}return r}function p(e,r){let a=[],o=i(c(e).body,a,s);if(!o||a.length>0)return;let l=t(o,f(o,d(r)));return l?n(l):void 0}function m(n,a,s,l){u(n,l);let{bom:p,body:m}=c(n),h=d(a),g=m;for(let n=1;n<h.length;n++){let a=i(g),s=a?f(a,h.slice(0,n)):h.slice(0,n);a&&t(a,s)||(g=e(g,r(g,s,{},{formattingOptions:o})))}let _=i(g),v=_?f(_,h):h;return p+e(g,r(g,v,s,{formattingOptions:o}))}function h(n,a){let c=i(n,void 0,s),l=c?f(c,a):a,u=c?t(c,l):void 0;if(!(a.length===0||!c||!u))return u.parent?.type===`array`&&typeof l.at(-1)==`number`?g(n,u.parent,u):v(e(n,r(n,l,void 0,{formattingOptions:o})))}function g(t,n,r){let i=n.children??[],a=i.indexOf(r),o=[{offset:r.offset,length:r.length,content:``}];if(a<i.length-1){let e=_(t,r.offset+r.length,i[a+1].offset);e!==void 0&&o.push({offset:e,length:1,content:``})}else if(a>0){let e=i[a-1],n=_(t,e.offset+e.length,r.offset);n!==void 0&&o.push({offset:n,length:1,content:``})}else{let e=_(t,r.offset+r.length,n.offset+n.length-1);e!==void 0&&o.push({offset:e,length:1,content:``})}return e(t,o.sort((e,t)=>e.offset-t.offset))}function _(e,t,n){for(let r=t;r<n;r++)if(e.startsWith(`//`,r))for(;r<n&&e[r]!==`
3
+ `;)r++;else if(e.startsWith(`/*`,r)){let t=e.indexOf(`*/`,r+2);if(t<0||t>=n)return;r=t+1}else if(e[r]===`,`)return r}function v(e){let t={justOpened:!1},n=()=>{t.justOpened=!1};a(e,{onObjectBegin:()=>{t.justOpened=!0},onSeparator:(e,r)=>{t.justOpened&&e===`,`&&(t.commaOffset??=r),n()},onObjectProperty:n,onObjectEnd:n},s);let{commaOffset:r}=t;return r===void 0?e:e.slice(0,r)+e.slice(r+1)}function y(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=h(i,d(t));return a===void 0?{text:e,removed:!1}:{text:r+a,removed:!0}}function b(e,t,n){u(e,n);let{bom:r,body:i}=c(e),a=d(t),o=h(i,a);if(o===void 0)return{text:e,removed:!1};let s=a.slice(0,-1);return s.length>0&&x(o,s)&&(o=h(o,s)??o),{text:r+o,removed:!0}}function x(e,n){let r=i(e,void 0,s);if(!r)return!1;let a=t(r,f(r,n));return!a||a.type!==`object`?!1:(a.children?.length??0)===0}export{m as addAtPointer,u as assertValidJsonc,p as findAtPointer,l as isValidJsonc,y as removeAtPointer,b as removeAtPointerAndEmptyParent};
@@ -0,0 +1,54 @@
1
+ import { McpInstallResult } from "./installer.js";
2
+
3
+ //#region src/mcp/opencode-layout.d.ts
4
+ /** Where OpenCode reads MCP servers: `/mcp/<name>` (v1) or `/mcp/servers/<name>` (v2). */
5
+ type OpenCodeMcpLayout = 'v1' | 'v2';
6
+ /**
7
+ * How a host tells this library which OpenCode MCP layout to use.
8
+ *
9
+ * - `authoritative`: the host verified the installed OpenCode and asserts the layout. Install writes it and
10
+ * prunes the other layout's GitKraken entry.
11
+ * - `infer-existing`: the host could not tell. Install and inspection follow the layout the config already
12
+ * uses and never prune. With both layouts present they follow v1, the entry OpenCode's compatibility reader
13
+ * retains over the native one. It needs the config file, so read-free helpers reject it.
14
+ *
15
+ * Omitting the policy keeps the native-v2 behavior of 0.1.1: v2 generation and inspection, v1 pruning.
16
+ */
17
+ type OpenCodeMcpPolicy = {
18
+ mode: 'authoritative';
19
+ layout: OpenCodeMcpLayout;
20
+ } | {
21
+ mode: 'infer-existing';
22
+ };
23
+ interface McpLayoutOptions {
24
+ /** Ignored for every client but OpenCode. */
25
+ openCode?: OpenCodeMcpPolicy;
26
+ }
27
+ /** `infer-existing` found no GitKraken entry to follow, or was passed to a helper that cannot read the config. */
28
+ declare class OpenCodeMcpLayoutUnknownError extends Error {
29
+ constructor();
30
+ }
31
+ /**
32
+ * The selected OpenCode entry is in place, but removing the other layout's entry did not complete. Not a
33
+ * success: an older reader can still reject a leftover native `servers` envelope, and a v1-compatible reader
34
+ * can pick the older duplicate. Re-running the install finishes the cleanup.
35
+ */
36
+ declare class OpenCodeMcpCleanupError extends Error {
37
+ /** What was committed before the cleanup failed: the selected location and entry, and no removed locations. */
38
+ readonly result: McpInstallResult;
39
+ constructor(result: McpInstallResult, cause: unknown);
40
+ }
41
+ /** Applies the omitted-policy default and rejects a value that is not an {@link OpenCodeMcpPolicy}. */
42
+ declare function resolveOpenCodePolicy(policy: OpenCodeMcpPolicy | undefined): OpenCodeMcpPolicy;
43
+ /** The layout a read-free helper emits for `options`; `infer-existing` cannot be resolved without reading the config. */
44
+ declare function openCodeLayoutFor(options: McpLayoutOptions | undefined): OpenCodeMcpLayout;
45
+ /** The v2 `servers` envelope, or `undefined` when `/mcp/servers` is absent or is a flat v1 server that happens to be named `servers`. */
46
+ declare function openCodeNativeServers(text: string): Record<string, unknown> | undefined;
47
+ /**
48
+ * Why writing the GitKraken entry in `layout` would displace other data in the OpenCode config `text`, or
49
+ * `undefined` when it is safe. Shared by install, which throws it, and inspect, which reports it. An undefined
50
+ * `layout` (`infer-existing`) checks only the root and `mcp`: an inferred v2 entry already sits in a native envelope.
51
+ */
52
+ declare function openCodeRefusal(text: string, layout: OpenCodeMcpLayout | undefined, path: string): string | undefined;
53
+ //#endregion
54
+ export { McpLayoutOptions, OpenCodeMcpCleanupError, OpenCodeMcpLayout, OpenCodeMcpLayoutUnknownError, OpenCodeMcpPolicy, openCodeLayoutFor, openCodeNativeServers, openCodeRefusal, resolveOpenCodePolicy };
@@ -0,0 +1 @@
1
+ import{findAtPointer as e}from"./jsonc-edit.js";var t=class extends Error{constructor(){super(`cannot resolve the OpenCode MCP layout for infer-existing: there is no GitKraken entry to follow, or the caller cannot read the config; pass an authoritative layout instead`),this.name=`OpenCodeMcpLayoutUnknownError`}},n=class extends Error{result;constructor(e,t){super(`OpenCode MCP entry was written but alternate-layout cleanup did not complete; run the install again`,{cause:t}),this.name=`OpenCodeMcpCleanupError`,this.result=e}};function r(e){if(e===void 0)return{mode:`authoritative`,layout:`v2`};let t=e;if(t?.mode===`infer-existing`&&!(`layout`in t))return{mode:`infer-existing`};if(t?.mode===`authoritative`&&(t.layout===`v1`||t.layout===`v2`))return{mode:`authoritative`,layout:t.layout};throw TypeError(`invalid OpenCode MCP policy`)}function i(e){let n=r(e?.openCode);if(n.mode===`infer-existing`)throw new t;return n.layout}const a=`/mcp/servers`;function o(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function s(e){return[`type`,`enabled`].some(t=>Object.hasOwn(e,t)&&(e[t]===null||typeof e[t]!=`object`||Array.isArray(e[t])))}function c(t){let n=e(t,a);return o(n)&&!s(n)?n:void 0}function l(t,n,r){let i=e(t,`/mcp`);if(!o(e(t,``))||i!==void 0&&!o(i))return`cannot safely update OpenCode MCP entry in ${r}: the config or its "mcp" is not an object`;if(n!==`v2`)return;let c=e(t,a);if(c!==void 0&&!o(c))return`cannot safely update OpenCode MCP entry in ${r}: "mcp.servers" is not an object`;if(o(c)&&s(c))return`cannot safely write the OpenCode v2 MCP layout in ${r}: "mcp.servers" is a server definition`}export{n as OpenCodeMcpCleanupError,t as OpenCodeMcpLayoutUnknownError,i as openCodeLayoutFor,c as openCodeNativeServers,l as openCodeRefusal,r as resolveOpenCodePolicy};
@@ -0,0 +1,11 @@
1
+ //#region src/mcp/toml-read.d.ts
2
+ /** Parses TOML `text`, or returns `undefined` on any parse error (mirrors `toml.Unmarshal`'s error check in gkcli). */
3
+ declare function parseTomlSafe(text: string): Record<string, unknown> | undefined;
4
+ /** Throws when `text` isn't valid TOML; `path` names its file in the message. */
5
+ declare function assertValidToml(text: string, path: string): void;
6
+ /** Reads Codex's legacy literal-key table (`['mcp_servers.GitKraken']`), which decodes to a single flat top-level key. */
7
+ declare function tomlLiteralKey(table: Record<string, unknown> | undefined, key: string): unknown;
8
+ /** Walks a dotted key (`a.b.c`) through nested TOML tables. */
9
+ declare function tomlDottedPath(table: Record<string, unknown> | undefined, dottedKey: string): unknown;
10
+ //#endregion
11
+ export { assertValidToml, parseTomlSafe, tomlDottedPath, tomlLiteralKey };
@@ -0,0 +1 @@
1
+ import{parse as e}from"smol-toml";function t(t){try{return e(t)}catch{return}}function n(e,n){if(!t(e))throw Error(`invalid TOML in ${n}`)}function r(e,t){return e?e[t]:void 0}function i(e,t){let n=e;for(let e of t.split(`.`))if(typeof n!=`object`||!n||Array.isArray(n)||(n=n[e],n===void 0))return;return n}export{n as assertValidToml,t as parseTomlSafe,i as tomlDottedPath,r as tomlLiteralKey};
@@ -0,0 +1,7 @@
1
+ //#region src/mcp/toml-safety.d.ts
2
+ /** Removes Codex's entry from a parsed document and gives an absent parent the same shape as an empty table. */
3
+ declare function withoutCodexEntry(table: Record<string, unknown> | undefined): Record<string, unknown>;
4
+ /** Compares parsed TOML values without treating distinct Date values as equal empty objects. */
5
+ declare function tomlSemanticEqual(a: unknown, b: unknown): boolean;
6
+ //#endregion
7
+ export { tomlSemanticEqual, withoutCodexEntry };
@@ -0,0 +1 @@
1
+ function e(e){let t={...e??{}};delete t[`mcp_servers.GitKraken`];let r=t.mcp_servers;if(n(r)){let e={...r};delete e.GitKraken,t.mcp_servers=e}else r===void 0&&(t.mcp_servers={});return t}function t(e,n){if(e===n)return!0;if(typeof e!=typeof n||e===null||n===null||typeof e!=`object`)return!1;if(e instanceof Date||n instanceof Date)return e instanceof Date&&n instanceof Date&&e.valueOf()===n.valueOf();if(Array.isArray(e)||Array.isArray(n))return Array.isArray(e)&&Array.isArray(n)&&e.length===n.length&&e.every((e,r)=>t(e,n[r]));let r=e,i=n,a=Object.keys(r);return a.length===Object.keys(i).length&&a.every(e=>e in i&&t(r[e],i[e]))}function n(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&!(e instanceof Date)}export{t as tomlSemanticEqual,e as withoutCodexEntry};
@@ -0,0 +1,16 @@
1
+ //#region src/mcp/toml-splice.d.ts
2
+ /**
3
+ * Splices `block` over the existing `[tableName]` block (replacing only the
4
+ * leaf, preserving any `[tableName.*]` subtables) or appends it. `block` must
5
+ * start with its own header line.
6
+ */
7
+ declare function replaceOrAppendBlock(original: string, tableName: string, block: string): string;
8
+ /** Removes every `[tableName]` block and any `[tableName.*]` subtables, so hand-added subtables don't become orphans. */
9
+ declare function removeBlock(original: string, tableName: string): {
10
+ text: string;
11
+ removed: boolean;
12
+ };
13
+ /** Collapses trailing blank lines to a single newline, so removing an end-of-file block doesn't leave an extra blank line behind. */
14
+ declare function trimTrailingBlankLines(text: string): string;
15
+ //#endregion
16
+ export { removeBlock, replaceOrAppendBlock, trimTrailingBlankLines };
@@ -0,0 +1,11 @@
1
+ import{parse as e}from"smol-toml";function t(e,t,n){let{bom:c,body:l}=r(e),u=i(l),d=a(o(n),u)+u,f=s(l,t,!1);if(f.length===0)return w(c,l,d,u);let p=c,m=0;return f.forEach((e,t)=>{p+=l.slice(m,e.start),t===0&&(p+=d),m=e.end}),p+=l.slice(m),p}function n(e,t){let{bom:n,body:i}=r(e),a=s(i,t,!0);if(a.length===0)return{text:e,removed:!1};let o=``,c=0;for(let e of a)o+=i.slice(c,e.start),c=e.end;return o+=i.slice(c),o.length===0?{text:``,removed:!0}:{text:n+o,removed:!0}}function r(e){return e.startsWith(``)?{bom:``,body:e.slice(1)}:{bom:``,body:e}}function i(e){return e.includes(`\r
2
+ `)?`\r
3
+ `:`
4
+ `}function a(e,t){let n=e.replace(/\r\n/g,`
5
+ `);return t===`\r
6
+ `?n.replace(/\n/g,`\r
7
+ `):n}function o(e){return e.replace(/[\r\n]+$/,``)}function s(e,t,n){let r=y(e);if(r.length===0)return[];let i=d(t),{headerKey:a,isHeader:o,lineStartsInString:s}=l(r),u=[],f=-1,_=e=>{if(f<0)return;let t=e;for(;t>f+1&&b(r[t-1]);)t--;u.push({start:c(r,f),end:c(r,t)}),f=-1};for(let e=0;e<r.length;e++){if(s[e]||!o[e])continue;let r=a[e],c=p(r.segments,i)||m(r.segments,t),l=n&&(h(r.segments,i)||g(r.segments,t));if(f>=0){if(c||l){!n&&c&&(_(e),f=e);continue}_(e)}(c||l)&&(f=e)}return _(r.length),u}function c(e,t){let n=0;for(let r=0;r<t&&r<e.length;r++)n+=e[r].length;return n}function l(e){let t=Array(e.length).fill({segments:[]}),n=Array(e.length).fill(!1),r=Array(e.length).fill(!1),i=new x;for(let a=0;a<e.length;a++){if(r[a]=i.inString(),!r[a]){let r=u(e[a]);r!==void 0&&(t[a]=r,n[a]=!0)}i.advance(v(e[a]))}return{headerKey:t,isHeader:n,lineStartsInString:r}}function u(t){let n=v(t).trim();if(!n.startsWith(`[`))return;let r=_(n);if(!(r<0)&&!(n.startsWith(`[[`)&&!n.slice(r+1).startsWith(`]`)))try{return{segments:f(e(`${n}\n`))}}catch{return}}function d(t){try{return f(e(`[${t}]\n`))}catch{return t.split(`.`)}}function f(e){let t=[],n=e;for(;n&&typeof n==`object`&&!Array.isArray(n);){let e=Object.entries(n);if(e.length!==1)break;let[r,i]=e[0];if(t.push(r),n=Array.isArray(i)?i[i.length-1]:i,n&&typeof n==`object`&&!Array.isArray(n)&&Object.keys(n).length===0)break}return t}function p(e,t){return e.length===t.length&&e.every((e,n)=>e===t[n])}function m(e,t){return e.length===1&&e[0]===t}function h(e,t){return e.length>t.length&&t.every((t,n)=>e[n]===t)}function g(e,t){return e.length>1&&e[0]===t}function _(e){let t=!1,n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(t)i===`'`&&(t=!1);else if(n)i===`\\`?r++:i===`"`&&(n=!1);else if(i===`'`)t=!0;else if(i===`"`)n=!0;else if(i===`]`)return r}return-1}function v(e){return e.replace(/\r?\n$/,``)}function y(e){if(e.length===0)return[];let t=[],n=0;for(let r=0;r<e.length;r++)e[r]===`
8
+ `&&(t.push(e.slice(n,r+1)),n=r+1);return n<e.length&&t.push(e.slice(n)),t}function b(e){let t=v(e).trim();return t===``||t.startsWith(`#`)}var x=class{inMultiBasic=!1;inMultiLiteral=!1;inString(){return this.inMultiBasic||this.inMultiLiteral}advance(e){let t=0;for(;t<e.length;)if(this.inMultiBasic){if(e[t]===`\\`&&t+1<e.length){t+=2;continue}if(e.startsWith(`"""`,t)){this.inMultiBasic=!1,t+=3;continue}t++}else if(this.inMultiLiteral){if(e.startsWith(`'''`,t)){this.inMultiLiteral=!1,t+=3;continue}t++}else if(e[t]===`#`)return;else e.startsWith(`"""`,t)?(this.inMultiBasic=!0,t+=3):e.startsWith(`'''`,t)?(this.inMultiLiteral=!0,t+=3):e[t]===`"`?t=S(e,t+1):e[t]===`'`?t=C(e,t+1):t++}};function S(e,t){let n=t;for(;n<e.length;){if(e[n]===`\\`&&n+1<e.length){n+=2;continue}if(e[n]===`"`)return n+1;n++}return n}function C(e,t){let n=t;for(;n<e.length;){if(e[n]===`'`)return n+1;n++}return n}function w(e,t,n,r){let i=e+t;return t.length===0?i+n:(t.endsWith(`
9
+ `)||(i+=r),T(t)||(i+=r),i+n)}function T(e){if(!e.endsWith(`
10
+ `))return!1;let t=e.slice(0,-1);return t.endsWith(`\r`)&&(t=t.slice(0,-1)),t.endsWith(`
11
+ `)}function E(e){let t=e.replace(/[ \t\r\n]+$/,``);return t===``?``:`${t}\n`}export{n as removeBlock,t as replaceOrAppendBlock,E as trimTrailingBlankLines};
@@ -0,0 +1,41 @@
1
+ import { AgentHostConfig } from "../config.js";
2
+ import { Client } from "./clients.js";
3
+
4
+ //#region src/mcp/uninstaller.d.ts
5
+ interface McpUninstallRequest {
6
+ client: Client;
7
+ /**
8
+ * Clears this path instead of every path {@link clientConfigPaths} resolves
9
+ * (Go's `--file-path`). When set, only this one file is touched — e.g. the
10
+ * antigravity legacy path is not additionally swept.
11
+ */
12
+ configPath?: string;
13
+ }
14
+ interface McpUninstallFileResult {
15
+ path: string;
16
+ /** MCP uninstall edits a config file but never deletes it. */
17
+ change: 'updated' | 'unchanged';
18
+ /** The pointers or table keys removed from this file. */
19
+ removedLocations: string[];
20
+ /** Why nothing was removed; set when `change` is `unchanged`. */
21
+ reason?: 'file-missing' | 'file-empty' | 'no-entry';
22
+ }
23
+ interface McpUninstallResult {
24
+ client: Client;
25
+ /** Whether a GitKraken entry was actually removed from at least one config path. */
26
+ removed: boolean;
27
+ /** One entry per path swept, in sweep order. */
28
+ files: McpUninstallFileResult[];
29
+ }
30
+ /**
31
+ * Clears the client's GitKraken MCP entry from every config path it may live
32
+ * in (mirrors gkcli's uninstall sweep across `mcp.Client.Paths()`), including
33
+ * OpenCode's v1/v2 pointer pair and Codex's comment-preserving TOML splice.
34
+ * A missing or empty config is a no-op, matching Go's "already uninstalled".
35
+ *
36
+ * Throws, before writing anything, for an unknown client or a config file that
37
+ * does not parse, so a sweep never leaves some files edited and others not.
38
+ */
39
+ declare function uninstallMcp(request: McpUninstallRequest, config: AgentHostConfig): Promise<McpUninstallResult>;
40
+ //#endregion
41
+ export { McpUninstallFileResult, McpUninstallRequest, McpUninstallResult, uninstallMcp };
@@ -0,0 +1 @@
1
+ import{removeAtPointer as e,removeAtPointerAndEmptyParent as t}from"./jsonc-edit.js";import{assertValidToml as n,parseTomlSafe as r,tomlDottedPath as i,tomlLiteralKey as a}from"./toml-read.js";import{clientConfigPaths as o,clientJSONPath as s,isSupported as c,openCodeEntryPointers as l}from"./clients.js";import{writeFileTracked as u}from"../file-change.js";import{tomlSemanticEqual as d,withoutCodexEntry as f}from"./toml-safety.js";import{removeBlock as p,trimTrailingBlankLines as m}from"./toml-splice.js";function h(e,t){return{file:{path:e,change:`unchanged`,removedLocations:[],reason:t}}}async function g(e,t){let{client:n}=e;if(!c(n))throw Error(`unsupported client: ${n}`);let r=e.configPath?[e.configPath]:await o(n,t),i=[];for(let e of r)i.push(await _(n,e,t));let a=[];for(let{file:e,text:n}of i)n!==void 0&&await u(t.fs,e.path,n,[]),a.push(e);return{client:n,removed:a.some(e=>e.removedLocations.length>0),files:a}}async function _(e,t,n){let r=await n.fs.readFile(t);if(r===null)return h(t,`file-missing`);if(r.trim()===``)return h(t,`file-empty`);let{text:i,removedLocations:a}=e===`codex`?y(r,t):v(r,e,t);return a.length===0?h(t,`no-entry`):{file:{path:t,change:`updated`,removedLocations:a},text:i}}function v(n,r,i){let a=r===`opencode`,o=a?l(n):[s(r)],c=n,u=[];for(let n of o){let r=a?t(c,n,i):e(c,n,i);r.removed&&(c=r.text,u.push(n))}return{text:c,removedLocations:u}}function y(e,t){n(e,t);let o=s(`codex`),c=r(e);if(a(c,o)===void 0&&i(c,o)===void 0)return{text:e,removedLocations:[]};let{text:l,removed:u}=p(e,o);if(!u){if(a(c,o)!==void 0||i(c,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: unsupported TOML representation`);return{text:e,removedLocations:[]}}let h=m(l),g=r(h);if(a(g,o)!==void 0||i(g,o)!==void 0)throw Error(`cannot safely remove Codex MCP entry in ${t}: target remains after splice`);if(!g||!d(f(g),f(c)))throw Error(`cannot safely remove Codex MCP entry in ${t}: splice would change another TOML value`);return{text:h,removedLocations:[o]}}export{g as uninstallMcp};
@@ -0,0 +1,4 @@
1
+ import { SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, SkillManifest } from "./manifest.js";
2
+ import { SkillAgent, allSkillAgents, isSkillAgentSupported, skillDir } from "./skill-dirs.js";
3
+ import { SkillConflictError, SkillFiles, SkillInstallAction, SkillInstallOptions, SkillInstallResult, SkillOperation, SkillReason, SkillState, SkillStatus, SkillUninstallAction, SkillUninstallOptions, SkillUninstallResult, SkillUnsupportedManifestError, inspectSkillStatus, installSkill, uninstallSkill } from "./install.js";
4
+ export { SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, type SkillAgent, SkillConflictError, type SkillFiles, type SkillInstallAction, type SkillInstallOptions, type SkillInstallResult, type SkillManifest, type SkillOperation, type SkillReason, type SkillState, type SkillStatus, type SkillUninstallAction, type SkillUninstallOptions, type SkillUninstallResult, SkillUnsupportedManifestError, allSkillAgents, inspectSkillStatus, installSkill, isSkillAgentSupported, skillDir, uninstallSkill };
@@ -0,0 +1 @@
1
+ import{SKILL_MANIFEST_FILE_NAME as e,SKILL_MANIFEST_SCHEMA_VERSION as t}from"./manifest.js";import{allSkillAgents as n,isSkillAgentSupported as r,skillDir as i}from"./skill-dirs.js";import{SkillConflictError as a,SkillUnsupportedManifestError as o,inspectSkillStatus as s,installSkill as c,uninstallSkill as l}from"./install.js";export{e as SKILL_MANIFEST_FILE_NAME,t as SKILL_MANIFEST_SCHEMA_VERSION,a as SkillConflictError,o as SkillUnsupportedManifestError,n as allSkillAgents,s as inspectSkillStatus,c as installSkill,r as isSkillAgentSupported,i as skillDir,l as uninstallSkill};
@@ -0,0 +1,126 @@
1
+ import { AgentHostConfig } from "../config.js";
2
+ import { FileChange } from "../file-change.js";
3
+ import { SkillManifest } from "./manifest.js";
4
+ import { SkillAgent } from "./skill-dirs.js";
5
+
6
+ //#region src/skills/install.d.ts
7
+ interface SkillFiles {
8
+ agent: SkillAgent;
9
+ /** A nonempty portable directory name (one path segment, not `.`/`..`). */
10
+ name: string;
11
+ /** Safe relative slash paths (e.g. `'SKILL.md'`) -> file content. Must be nonempty and exclude the reserved manifest path. */
12
+ files: Record<string, string>;
13
+ }
14
+ /**
15
+ * How the skill's on-disk state compares to the bundle passed in. Mirrors
16
+ * gkcli's `SkillStatus`/`InstallState`/reason constants
17
+ * (`internal/actions/aiskill/skill.go`), minus the fields this library has no
18
+ * way to observe: `removalBlocked`'s `symlink` half (no `FileSystemPort`
19
+ * primitive reports a symlink) and project-scope provenance (`AgentHostConfig`
20
+ * has no git-worktree concept, so every install/uninstall is Go's "global"
21
+ * scope).
22
+ */
23
+ type SkillState = 'missing' | 'current' | 'older-release' | 'modified';
24
+ type SkillReason = 'absent' | 'unowned' | 'invalid-manifest' | 'unsupported-manifest' | 'content-changed' | 'different-bundle' | 'stale-stamp';
25
+ interface SkillStatus {
26
+ state: SkillState;
27
+ /** The skill's directory, `<skillDir>/<name>`. */
28
+ path: string;
29
+ /** Why the skill is not plainly current or missing. Absent when it is current and clean. */
30
+ reason?: SkillReason;
31
+ /** Whether `force` could repair this status — always `false` for `'unsupported-manifest'`. */
32
+ requiresForce: boolean;
33
+ /** Whether a valid, matching manifest vouches for this skill (mirrors `SkillStatus.Owned`). */
34
+ owned: boolean;
35
+ /** Whether the on-disk files are byte-identical to the bundle passed in. */
36
+ matchesBundle: boolean;
37
+ manifestVersion?: number;
38
+ installedAt?: string;
39
+ updatedAt?: string;
40
+ }
41
+ /** Inspects the on-disk state against a bundle without writing any files. */
42
+ declare function inspectSkillStatus(request: SkillFiles, config: AgentHostConfig): Promise<SkillStatus>;
43
+ type SkillInstallAction = 'create' | 'replace' | 'unchanged';
44
+ type SkillUninstallAction = 'remove' | 'absent';
45
+ type SkillOperation = 'install' | 'uninstall';
46
+ /** A skill's on-disk content conflicts with the bundle, and `force` was not set (or cannot repair it). */
47
+ declare class SkillConflictError extends Error {
48
+ readonly code: 'conflict' | 'invalid-manifest' | 'modified';
49
+ readonly operation: SkillOperation;
50
+ /** The skill's directory. */
51
+ readonly path: string;
52
+ readonly status: SkillStatus;
53
+ constructor(status: SkillStatus, operation: SkillOperation);
54
+ private static codeFor;
55
+ private static buildMessage;
56
+ }
57
+ /** A skill's manifest was written by a newer release. Neither install nor uninstall may discard state this version cannot interpret, even with `force`. */
58
+ declare class SkillUnsupportedManifestError extends Error {
59
+ readonly code = "unsupported-manifest";
60
+ readonly operation: SkillOperation;
61
+ /** The skill's directory. */
62
+ readonly path: string;
63
+ readonly status: SkillStatus;
64
+ constructor(status: SkillStatus, operation: SkillOperation);
65
+ }
66
+ interface SkillInstallOptions {
67
+ /** Overwrite content this library cannot otherwise prove it owns. Never lifts a `SkillUnsupportedManifestError`. */
68
+ force?: boolean;
69
+ /** Recorded as the manifest's `gkVersion`. Defaults to `'dev'`, the value gkcli dev builds record. */
70
+ version?: string;
71
+ /** Injectable for tests; defaults to `new Date()`. */
72
+ now?: Date;
73
+ }
74
+ interface SkillInstallResult {
75
+ action: SkillInstallAction;
76
+ /** The skill's status before the action. */
77
+ status: SkillStatus;
78
+ /** The skill's directory. */
79
+ path: string;
80
+ /** What the install did to each content file and the manifest. Empty when `action` is `'unchanged'` and nothing needed repair. */
81
+ files: FileChange[];
82
+ /** The manifest now on disk. Absent when nothing was written. */
83
+ manifest?: SkillManifest;
84
+ }
85
+ /**
86
+ * Writes a bundled skill's files under `<skillDir>/<name>/` for `agent`, and
87
+ * the `.gk-skill.json` ownership manifest beside them. Mirrors gkcli's
88
+ * `aiskill.Install`: a fresh install (`'create'`), a no-op when the bundle is
89
+ * already current (`'unchanged'`, still repairing a missing or stale manifest
90
+ * when applicable), an automatic upgrade over an owned older release
91
+ * (`'replace'`), and — with `force` — a repair over content this library does not
92
+ * otherwise recognize. Without `force`, content that conflicts with the
93
+ * bundle throws {@link SkillConflictError}; a manifest from a newer release
94
+ * always throws {@link SkillUnsupportedManifestError}. `name` must be one
95
+ * portable directory segment; `files` must be a nonempty map of safe relative
96
+ * slash paths without traversal, collisions, or the reserved manifest path.
97
+ * The result lists each file created, updated or left unchanged.
98
+ */
99
+ declare function installSkill(request: SkillFiles, config: AgentHostConfig, options?: SkillInstallOptions): Promise<SkillInstallResult>;
100
+ interface SkillUninstallOptions {
101
+ /** Remove content this library cannot otherwise prove it owns. Never lifts a `SkillUnsupportedManifestError`. */
102
+ force?: boolean;
103
+ }
104
+ interface SkillUninstallResult {
105
+ action: SkillUninstallAction;
106
+ /** The skill's status before the action. */
107
+ status: SkillStatus;
108
+ /** The skill's directory. */
109
+ path: string;
110
+ /** The content files removed, the manifest when it was owned and removable, and the directory once it was left empty. Empty when `action` is `'absent'`. */
111
+ files: FileChange[];
112
+ }
113
+ /**
114
+ * Removes a bundled skill's files for `agent`, mirroring gkcli's
115
+ * `aiskill.Uninstall`. A skill whose content is byte-identical to the bundle
116
+ * passed in is always removed; an owned older release is removed without
117
+ * `force`; other content requires `force`. The manifest is removed alongside
118
+ * the content only when it is owned (a valid, matching manifest) and either
119
+ * `force` was set or the content wasn't hand-edited after install (mirrors
120
+ * `removeSkillFile`'s `removeManifest` rule: a foreign or mismatched-identity
121
+ * manifest is left in place even though the content beside it is removable).
122
+ * The skill's directory is removed too, but only once it is left empty.
123
+ */
124
+ declare function uninstallSkill(request: SkillFiles, config: AgentHostConfig, options?: SkillUninstallOptions): Promise<SkillUninstallResult>;
125
+ //#endregion
126
+ export { SkillConflictError, SkillFiles, SkillInstallAction, SkillInstallOptions, SkillInstallResult, SkillOperation, SkillReason, SkillState, SkillStatus, SkillUninstallAction, SkillUninstallOptions, SkillUninstallResult, SkillUnsupportedManifestError, inspectSkillStatus, installSkill, uninstallSkill };
@@ -0,0 +1 @@
1
+ import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{SKILL_MANIFEST_FILE_NAME as r,buildSkillManifest as i,digestFiles as a,readSkillManifest as o,serializeSkillManifest as s}from"./manifest.js";import{skillDir as c}from"./skill-dirs.js";function l(e){let t=e.name;if(typeof t!=`string`||t.length===0||t===`.`||t===`..`||/[\\/:<>"|?*]/.test(t)||u(t)||/[ .]$/.test(t)||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(t))throw TypeError(`skill name must be a nonempty portable directory name`);if(typeof e.files!=`object`||e.files===null||Array.isArray(e.files))throw TypeError(`skill files must be a nonempty file map`);let n=Object.keys(e.files);if(n.length===0)throw TypeError(`skill files must be a nonempty file map`);let i=new Set;for(let t of n){if(typeof e.files[t]!=`string`||t.length===0||t.startsWith(`/`)||t.includes(`\\`)||t.includes(`:`)||t.split(`/`).some(e=>e===``||e===`.`||e===`..`||/[<>"|?*]/.test(e)||u(e)||/[ .]$/.test(e)||/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(e)))throw TypeError(`skill file path must be a safe relative slash path: ${t}`);let n=t.toLowerCase(),a=r.toLowerCase();if(n===a||n.startsWith(`${a}/`))throw TypeError(`skill files cannot include ${r}`);if(i.has(n))throw TypeError(`skill files contain a case-insensitive path collision: ${t}`);i.add(n)}for(let e of i){let t=e.split(`/`);for(let n=1;n<t.length;n++)if(i.has(t.slice(0,n).join(`/`)))throw TypeError(`skill files contain a file and parent path collision: ${e}`)}}function u(e){return[...e].some(e=>e.charCodeAt(0)<32||e.charCodeAt(0)===127)}function d(e){return e!==`unsupported-manifest`}function f(e){return e===`unowned`||e===`stale-stamp`}async function p(t,n,r){let i=Object.create(null);for(let a of n){let n=await r.readFile(e(t,a));n!==null&&(i[a]=n)}return i}async function m(t,n){let r=Object.create(null),i=[],a=async(t,o)=>{let s;try{s=await n.readFile(t)}catch(e){if(e?.code!==`EISDIR`)throw e;s=null}if(s!==null){o!==`.gk-skill.json`&&(r[o]=s);return}let c;try{c=await n.readDir(t)}catch(e){if(e?.code===`ENOENT`&&o===``)return;throw e}if(c!==null){i.push(t);for(let n of c)await a(e(t,n),o?`${o}/${n}`:n)}};return await a(t,``),{files:r,directories:i}}function h(t,n){let r=new Set([t]);for(let i of n){let n=i.split(`/`);for(let i=1;i<n.length;i++)r.add(e(t,n.slice(0,i).join(`/`)))}return[...r]}function g(e,t){let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?n.every(n=>e[n]===t[n]):!1}async function _(t,n){let i=e(c(t.agent,n),t.name),s=e(i,r),l=Object.keys(t.files),{manifest:u,reason:f}=await o(s,t.name,t.agent,`global`,n.fs.readFile.bind(n.fs)),_,v=async()=>(_??=await m(i,n.fs),_),y=(e,t=[])=>({status:e,ownedPaths:t,directories:_?.directories??h(i,t)});if(f===`unsupported-manifest`)return y({state:`modified`,path:i,reason:`unsupported-manifest`,requiresForce:!1,owned:!1,matchesBundle:!1});let b=await p(i,l,n.fs),x=Object.keys(b),S=x.length>0,C=x.length===l.length,w=C&&g(b,t.files);if(!S){if(u===void 0){if(f!==`unowned`){let e=f??`absent`;return y({state:`missing`,path:i,reason:e,requiresForce:d(e),owned:!1,matchesBundle:!1})}return y({state:`missing`,path:i,reason:`absent`,requiresForce:!1,owned:!1,matchesBundle:!1})}let e=await v(),t=Object.keys(e.files);return(t.length>0?await a(e.files):void 0)===u.digest?y({state:`older-release`,path:i,reason:`different-bundle`,requiresForce:!1,owned:!0,matchesBundle:!1,manifestVersion:u.schemaVersion,installedAt:u.installedAt,updatedAt:u.updatedAt},t):t.length>0?y({state:`modified`,path:i,reason:`content-changed`,requiresForce:!0,owned:!0,matchesBundle:!1,manifestVersion:u.schemaVersion,installedAt:u.installedAt,updatedAt:u.updatedAt}):y({state:`missing`,path:i,reason:`absent`,requiresForce:!1,owned:!1,matchesBundle:!1})}if(u===void 0){let e=f??`invalid-manifest`;return e===`unowned`&&w?y({state:`current`,path:i,reason:`unowned`,requiresForce:!1,owned:!1,matchesBundle:w},l):y({state:`modified`,path:i,reason:e,requiresForce:d(e),owned:!1,matchesBundle:w},x)}let T={owned:!0,manifestVersion:u.schemaVersion,installedAt:u.installedAt,updatedAt:u.updatedAt};if((C?await a(b):void 0)===u.digest)return y(w?{state:`current`,path:i,requiresForce:!1,matchesBundle:w,...T}:{state:`older-release`,path:i,reason:`different-bundle`,requiresForce:!1,matchesBundle:w,...T},l);let E=await v(),D=Object.keys(E.files);return(D.length>0?await a(E.files):void 0)===u.digest?y({state:`older-release`,path:i,reason:`different-bundle`,requiresForce:!1,matchesBundle:w,...T},D):w&&g(E.files,t.files)?y({state:`current`,path:i,reason:`stale-stamp`,requiresForce:!1,matchesBundle:w,...T},l):y({state:`modified`,path:i,reason:`content-changed`,requiresForce:d(`content-changed`),matchesBundle:w,...T},x)}async function v(e,t,n,r){for(let i of[...t].sort((e,t)=>t.length-e.length))i!==r&&(await e.readDir(i))?.length===0&&(await e.remove(i),n.push({path:i,change:`removed`}))}async function y(e,t){return l(e),(await _(e,t)).status}function b(e,t){switch(e.state){case`missing`:switch(e.reason){case void 0:case`absent`:case`unowned`:return`create`;default:return t&&d(e.reason)?`replace`:`conflict`}case`current`:return t?`replace`:`unchanged`;case`older-release`:return`replace`;default:return t&&d(e.reason)?`replace`:`conflict`}}function x(e,t){return e.state===`missing`?`absent`:e.reason===`unsupported-manifest`?`unsupported-manifest`:e.matchesBundle||t||e.state===`older-release`?`remove`:`modified`}var S=class e extends Error{code;operation;path;status;constructor(t,n){let r=e.codeFor(t,n);super(e.buildMessage(r,t,n)),this.name=`SkillConflictError`,this.code=r,this.operation=n,this.path=t.path,this.status=t}static codeFor(e,t){return t===`uninstall`?`modified`:e.reason===`invalid-manifest`?`invalid-manifest`:`conflict`}static buildMessage(e,t,n){let r=n===`install`?`replace`:`remove`;return e===`invalid-manifest`?`skill at ${t.path} has invalid ownership metadata; pass \`force\` to ${r} it`:`skill at ${t.path} differs from the bundle (${t.reason}); pass \`force\` to ${r} it`}},C=class extends Error{code=`unsupported-manifest`;operation;path;status;constructor(e,t){super(`skill metadata at ${e.path} was written by a newer release and can't be interpreted; nothing was changed`),this.name=`SkillUnsupportedManifestError`,this.operation=t,this.path=e.path,this.status=e}};async function w(a,o,c={}){l(a);let u=await _(a,o),{status:d}=u,p=b(d,c.force??!1);if(p===`conflict`)throw d.reason===`unsupported-manifest`?new C(d,`install`):new S(d,`install`);let m=d.path,h=e(m,r),g=p===`unchanged`&&f(d.reason);if(p===`unchanged`&&!g)return{action:p,status:d,path:m,files:[]};let y=[];if(!g){for(let[t,r]of Object.entries(a.files))await n(o.fs,e(m,t),r,y,{mode:420});if(d.state===`older-release`){let n=new Set(Object.keys(a.files));for(let r of u.ownedPaths)n.has(r)||await t(o.fs,e(m,r),y);await v(o.fs,u.directories,y,m)}}let x=await i(a.name,a.agent,a.files,d.installedAt,c.version,c.now??new Date);return await n(o.fs,h,s(x),y,{mode:420}),{action:p,status:d,path:m,files:y,manifest:x}}async function T(n,i,a={}){l(n);let o=await _(n,i),{status:s}=o,c=a.force??!1,u=x(s,c);if(u===`unsupported-manifest`)throw new C(s,`uninstall`);if(u===`modified`)throw new S(s,`uninstall`);if(u===`absent`)return{action:u,status:s,path:s.path,files:[]};let d=s.path,f=[];for(let n of o.ownedPaths)await t(i.fs,e(d,n),f);return s.owned&&(c||s.reason!==`content-changed`)&&await t(i.fs,e(d,r),f),await v(i.fs,o.directories,f,void 0),{action:u,status:s,path:d,files:f}}export{S as SkillConflictError,C as SkillUnsupportedManifestError,y as inspectSkillStatus,w as installSkill,T as uninstallSkill};
@@ -0,0 +1,56 @@
1
+ //#region src/skills/manifest.d.ts
2
+ /**
3
+ * The ownership manifest gkcli's `aiskill` package writes beside a skill's
4
+ * content (`.gk-skill.json`), and the read/validate/digest logic that backs
5
+ * `installSkill`/`uninstallSkill`'s state machine. Direct port of
6
+ * `internal/actions/aiskill/manifest.go`.
7
+ *
8
+ * This is deliberately local bookkeeping, not a publisher signature: it lets
9
+ * `installSkill`/`uninstallSkill` tell "gk installed this, byte-identical to
10
+ * what it wrote" apart from "someone else's file happens to be here."
11
+ */
12
+ declare const SKILL_MANIFEST_FILE_NAME = ".gk-skill.json";
13
+ declare const SKILL_MANIFEST_SCHEMA_VERSION = 1;
14
+ /** Persisted shape of `.gk-skill.json`. See `## Persisted data` in `ARCHITECTURE.md`. */
15
+ interface SkillManifest {
16
+ schemaVersion: number;
17
+ owner: string;
18
+ skill: string;
19
+ client: string;
20
+ scope: string;
21
+ /** `sha256:<64 lowercase hex chars>`. */
22
+ digest: string;
23
+ gkVersion?: string;
24
+ /** ISO 8601 / RFC 3339. */
25
+ installedAt?: string;
26
+ /** ISO 8601 / RFC 3339. */
27
+ updatedAt?: string;
28
+ }
29
+ /** `sha256:<hex>` of a single string, compatible with gkcli's manifest digest so either tool can verify ownership. */
30
+ declare function digestContent(content: string): Promise<string>;
31
+ /**
32
+ * `sha256:<hex>` over every file in a skill (sorted by path), for a skill
33
+ * bundle with more than one file. gkcli's bundled skills are always exactly
34
+ * one file (`SKILL.md`), where this reduces to {@link digestContent} of that
35
+ * file's bytes; multi-file skills have no Go equivalent to stay byte-compatible
36
+ * with.
37
+ */
38
+ declare function digestFiles(files: Readonly<Record<string, string>>): Promise<string>;
39
+ /** Why a manifest read didn't return a valid, matching manifest — see `SkillReason` in `install.ts`. */
40
+ type ManifestReadReason = 'unowned' | 'invalid-manifest' | 'unsupported-manifest';
41
+ interface ManifestReadResult {
42
+ manifest?: SkillManifest;
43
+ /** Set only when `manifest` is `undefined`. */
44
+ reason?: ManifestReadReason;
45
+ }
46
+ /**
47
+ * Reads and validates the manifest beside `target`. The host filesystem port
48
+ * does not expose symlink identity, so symlink handling is the host's
49
+ * responsibility (see `ARCHITECTURE.md`).
50
+ */
51
+ declare function readSkillManifest(manifestPath: string, name: string, client: string, scope: string, readFile: (path: string) => Promise<string | null>): Promise<ManifestReadResult>;
52
+ /** Builds the manifest `installSkill` writes. Mirrors `marshalManifest`; `scope` is always `'global'` (see `readSkillManifest`'s doc comment). */
53
+ declare function buildSkillManifest(name: string, client: string, files: Readonly<Record<string, string>>, previousInstalledAt: string | undefined, version: string | undefined, now: Date): Promise<SkillManifest>;
54
+ declare function serializeSkillManifest(manifest: SkillManifest): string;
55
+ //#endregion
56
+ export { ManifestReadReason, ManifestReadResult, SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, SkillManifest, buildSkillManifest, digestContent, digestFiles, readSkillManifest, serializeSkillManifest };
@@ -0,0 +1 @@
1
+ const e=`.gk-skill.json`,t=1,n=`gitkraken`;async function r(e){let t=new TextEncoder().encode(e),n=await globalThis.crypto.subtle.digest(`SHA-256`,t);return Array.from(new Uint8Array(n)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function i(e){return`sha256:${await r(e)}`}async function a(e){let t=Object.keys(e).sort();return t.length===1?i(e[t[0]]):i(t.map(t=>`${t}\u0000${e[t]}`).join(`\0`))}const o=/^sha256:[0-9a-f]{64}$/;function s(e){return typeof e==`string`&&o.test(e)}function c(e){if(typeof e!=`string`)return!1;let t=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(e);if(!t)return!1;let[,n,r,i,a,o,s,c]=t,l=Number(n),u=Number(r),d=Number(i),f=Number(a),p=Number(o),m=Number(s),h=[31,l%4==0&&(l%100!=0||l%400==0)?29:28,31,30,31,30,31,31,30,31,30,31][u-1];if(u<1||u>12||d<1||d>h||f>23||p>59||m>59)return!1;if(c!==`Z`){let e=/[+-](\d{2}):(\d{2})$/.exec(c);if(!e||Number(e[1])>23||Number(e[2])>59)return!1}return!0}function l(e,t,r,i){return e.schemaVersion!==1||e.owner!==n||e.skill!==t||e.client!==r||e.scope!==i||!s(e.digest)?!1:typeof e.installedAt==`string`&&c(e.installedAt)&&typeof e.updatedAt==`string`&&c(e.updatedAt)&&(e.gkVersion===void 0||typeof e.gkVersion==`string`)}async function u(e,t,n,r,i){let a=await i(e);if(a===null)return{reason:`unowned`};let o;try{o=JSON.parse(a)}catch{return{reason:`invalid-manifest`}}if(typeof o!=`object`||!o)return{reason:`invalid-manifest`};let s=o;return s.schemaVersion===1?l(s,t,n,r)?{manifest:s}:{reason:`invalid-manifest`}:{reason:`unsupported-manifest`}}function d(e){return e.toISOString().replace(/\.\d{3}Z$/,`Z`)}async function f(e,t,r,i,o,s){let c=d(s);return{schemaVersion:1,owner:n,skill:e,client:t,scope:`global`,digest:await a(r),gkVersion:o||`dev`,installedAt:i||c,updatedAt:c}}function p(e){return`${JSON.stringify(e)}\n`}export{e as SKILL_MANIFEST_FILE_NAME,t as SKILL_MANIFEST_SCHEMA_VERSION,f as buildSkillManifest,i as digestContent,a as digestFiles,u as readSkillManifest,p as serializeSkillManifest};
@@ -0,0 +1,22 @@
1
+ import { AgentHostConfig } from "../config.js";
2
+
3
+ //#region src/skills/skill-dirs.d.ts
4
+ /**
5
+ * Hook-registry client ids gkcli can install bundled skills for (a subset of
6
+ * the full aihook registry — every client that has a skill install
7
+ * directory). Ids match gkcli's `aihook.Client` values, which differ from
8
+ * the MCP registry's (`'claude-code'` here vs. `'claude-cli'` in
9
+ * `src/mcp`); the two registries are unified only by gkcli's own
10
+ * `internal/agents` package, which is out of this lane's P6.1/P6.2 scope.
11
+ */
12
+ type SkillAgent = 'claude-code' | 'opencode' | 'cursor' | 'codex' | 'copilot' | 'antigravity' | 'pi' | 'augment' | 'grok';
13
+ declare function allSkillAgents(): SkillAgent[];
14
+ declare function isSkillAgentSupported(agent: string): agent is SkillAgent;
15
+ /**
16
+ * The global skill install directory for `agent` (bundled skills are written
17
+ * one subdirectory per skill name beneath it). Direct port of gkcli's
18
+ * `aiskill.skillDirs`.
19
+ */
20
+ declare function skillDir(agent: SkillAgent, config: AgentHostConfig): string;
21
+ //#endregion
22
+ export { SkillAgent, allSkillAgents, isSkillAgentSupported, skillDir };