@gitkraken/agent-tools 0.0.0-stage → 0.1.1
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 +20 -0
- package/LICENSE +19 -0
- package/README.md +139 -2
- package/dist/agents.d.ts +52 -0
- package/dist/agents.js +1 -0
- package/dist/config.d.ts +68 -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 +17 -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 +87 -0
- package/dist/mcp/clients.js +1 -0
- package/dist/mcp/config-entry.d.ts +13 -0
- package/dist/mcp/config-entry.js +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/installer.d.ts +69 -0
- package/dist/mcp/installer.js +3 -0
- package/dist/mcp/jsonc-edit.d.ts +41 -0
- package/dist/mcp/jsonc-edit.js +3 -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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{HookConfigError as r}from"./errors.js";const i=`// gk-managed:`;function a(t,n){return e(t===`opencode`?e(n.paths.configHome,`opencode`,`plugins`):e(n.paths.piAgentDir||e(n.paths.home,`.pi`,`agent`),`extensions`),`gk-hooks.js`)}function o(e){return e?.split(`
|
|
2
|
+
`,1)[0].startsWith(i)??!1}function s(e){if(!o(e))return;let t=e.split(`
|
|
3
|
+
`,1)[0];try{let e=JSON.parse(t.slice(14));if(e&&typeof e==`object`&&Array.isArray(e.events)){let t=e;return{events:t.events.filter(e=>typeof e==`string`),executable:typeof t.executable==`string`?t.executable:``,hostMajor:typeof t.hostMajor==`number`?t.hostMajor:0}}}catch{}}function c(e,t,n,r){let a={events:t,executable:n,version:e===`opencode`?5:1};e===`opencode`&&r&&(a.hostMajor=r);let o=e===`opencode`?`import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const EVENTS = new Set({{.EventsJSON}});
|
|
6
|
+
const EXECUTABLE = {{.ExecJSON}};
|
|
7
|
+
const HOST = {{.HostJSON}};
|
|
8
|
+
const MAX_HOOK_INPUT_BYTES = {{.MaxInputBytesJSON}};
|
|
9
|
+
|
|
10
|
+
function getString(value) {
|
|
11
|
+
return typeof value === "string" && value.length > 0 ? value : "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// extractToolName is the one piece of the relay every host must supply: only
|
|
15
|
+
// the host knows where its payloads carry a tool name. The default below
|
|
16
|
+
// returns null so a host that forgets it still forwards events with
|
|
17
|
+
// tool_name: null, instead of throwing inside runHook's catch-all and
|
|
18
|
+
// silently forwarding nothing at all. Hosts override it with
|
|
19
|
+
// {{define "extractToolName"}} — see hook_pi.js.tmpl and
|
|
20
|
+
// hook_opencode.js.tmpl.
|
|
21
|
+
function extractToolName(payload) {
|
|
22
|
+
return (
|
|
23
|
+
getString(payload?.input?.tool) ||
|
|
24
|
+
getString(payload?.event?.properties?.part?.tool) ||
|
|
25
|
+
getString(payload?.event?.properties?.tool) ||
|
|
26
|
+
// v2's tool.hook("execute.before"/"execute.after") callbacks pass { event },
|
|
27
|
+
// where event.tool is the tool name directly (e.g. "read").
|
|
28
|
+
getString(payload?.event?.tool) ||
|
|
29
|
+
""
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const V1_ONLY_EVENTS = new Set({{.V1OnlyEventsJSON}});
|
|
34
|
+
const V2_EVENT_ALIASES = new Map(Object.entries({{.V2EventAliasesJSON}}));
|
|
35
|
+
|
|
36
|
+
function extractSessionID(event) {
|
|
37
|
+
return (
|
|
38
|
+
getString(event?.properties?.sessionID) ||
|
|
39
|
+
getString(event?.properties?.sessionId) ||
|
|
40
|
+
getString(event?.properties?.session_id) ||
|
|
41
|
+
// A message or part names its session in its own sessionID; its id names
|
|
42
|
+
// the message or part. Only a session's own info carries the session id
|
|
43
|
+
// as id, so that fallback comes after every explicit session field.
|
|
44
|
+
getString(event?.properties?.info?.sessionID) ||
|
|
45
|
+
getString(event?.properties?.part?.sessionID) ||
|
|
46
|
+
getString(event?.properties?.info?.id) ||
|
|
47
|
+
getString(event?.properties?.id) ||
|
|
48
|
+
getString(event?.sessionID) ||
|
|
49
|
+
getString(event?.sessionId) ||
|
|
50
|
+
getString(event?.session_id) ||
|
|
51
|
+
// v2's event bus nests the id under \`data\` instead of \`properties\`. The
|
|
52
|
+
// event's own top-level id is never a fallback: on v2 it names the event,
|
|
53
|
+
// and relaying it would report a session that does not exist.
|
|
54
|
+
getString(event?.data?.sessionID)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// server is the OpenCode v1 plugin entrypoint (returns a Hooks object). v1
|
|
61
|
+
// calls this directly off the default export below.
|
|
62
|
+
const server = async (ctx) => {
|
|
63
|
+
// No process.cwd() fallback: that is the host's launch directory, which the
|
|
64
|
+
// relay cannot tell apart from a real per-session cwd and would use to
|
|
65
|
+
// attribute the session to the wrong repo. Each hook below drops its event
|
|
66
|
+
// instead when cwd is empty, so the spawn is skipped entirely rather than
|
|
67
|
+
// paid for and discarded by the relay's own missing-cwd guard.
|
|
68
|
+
const cwd = getString(ctx.directory) || getString(ctx.worktree);
|
|
69
|
+
const hooks = {};
|
|
70
|
+
|
|
71
|
+
// General event hook — forwards native dot names as-is.
|
|
72
|
+
hooks.event = async ({ event }) => {
|
|
73
|
+
if (!cwd) return;
|
|
74
|
+
const type = event?.type ?? "";
|
|
75
|
+
if (type === "tool.execute.before" || type === "tool.execute.after") return;
|
|
76
|
+
const sessionID = extractSessionID(event);
|
|
77
|
+
if (!sessionID) return;
|
|
78
|
+
runHook(type, sessionID, cwd, { event });
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Dedicated tool hooks — provide structured sessionID and tool name.
|
|
82
|
+
if (EVENTS.has("tool.execute.before")) {
|
|
83
|
+
hooks["tool.execute.before"] = async (input, output) => {
|
|
84
|
+
if (!cwd) return;
|
|
85
|
+
const sessionID =
|
|
86
|
+
getString(input?.sessionID) || getString(input?.sessionId) || getString(input?.session_id);
|
|
87
|
+
if (!sessionID) return;
|
|
88
|
+
runHook("tool.execute.before", sessionID, cwd, { input, output });
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (EVENTS.has("tool.execute.after")) {
|
|
92
|
+
hooks["tool.execute.after"] = async (input, output) => {
|
|
93
|
+
if (!cwd) return;
|
|
94
|
+
const sessionID =
|
|
95
|
+
getString(input?.sessionID) || getString(input?.sessionId) || getString(input?.session_id);
|
|
96
|
+
if (!sessionID) return;
|
|
97
|
+
runHook("tool.execute.after", sessionID, cwd, { input, output });
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return hooks;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// setup is the OpenCode v2 plugin entrypoint. v2 replaced the returned-Hooks
|
|
105
|
+
// shape with named registration APIs (ctx.tool.hook, ctx.event.subscribe). It
|
|
106
|
+
// dropped the v1 names ctx.directory/ctx.worktree, but the directory itself is
|
|
107
|
+
// still on the context as ctx.location.directory, so that is the base cwd.
|
|
108
|
+
const setup = async (ctx) => {
|
|
109
|
+
// Per-session directories arrive only on the subset of events that carry a
|
|
110
|
+
// location, and the subscription only yields from plugin load onward, so a
|
|
111
|
+
// session predating this plugin instance (server restart, resumed session)
|
|
112
|
+
// never populates the cache. Treat the cache as an override on top of the
|
|
113
|
+
// context directory rather than the only source, and never guess: with no
|
|
114
|
+
// directory at all, send "" so the relay drops the event instead of
|
|
115
|
+
// attributing the session to the host's launch directory.
|
|
116
|
+
const baseCwd =
|
|
117
|
+
getString(ctx?.location?.directory) || getString(ctx?.directory) || getString(ctx?.worktree);
|
|
118
|
+
// session.deleted is an explicit, rare user action, so eviction on it alone
|
|
119
|
+
// would let a host left running for days accumulate one entry per session it
|
|
120
|
+
// ever saw. Cap the map the way runner.go caps its cwd timeline and drop the
|
|
121
|
+
// oldest entry first (Map iterates in insertion order); a session evicted
|
|
122
|
+
// this way falls back to baseCwd, same as one that never carried a location.
|
|
123
|
+
const MAX_TRACKED_SESSIONS = 50;
|
|
124
|
+
const cwdBySession = new Map();
|
|
125
|
+
const rememberCwd = (sessionID, directory) => {
|
|
126
|
+
// Re-insert so a session still in use is treated as the newest entry.
|
|
127
|
+
cwdBySession.delete(sessionID);
|
|
128
|
+
cwdBySession.set(sessionID, directory);
|
|
129
|
+
while (cwdBySession.size > MAX_TRACKED_SESSIONS) {
|
|
130
|
+
cwdBySession.delete(cwdBySession.keys().next().value);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const resolveCwd = (sessionID) => (sessionID && cwdBySession.get(sessionID)) || baseCwd;
|
|
134
|
+
|
|
135
|
+
if (EVENTS.has("tool.execute.before")) {
|
|
136
|
+
await ctx.tool.hook("execute.before", (event) => {
|
|
137
|
+
const sessionID = getString(event?.sessionID);
|
|
138
|
+
if (!sessionID) return;
|
|
139
|
+
runHook("tool.execute.before", sessionID, resolveCwd(sessionID), { event });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (EVENTS.has("tool.execute.after")) {
|
|
143
|
+
await ctx.tool.hook("execute.after", (event) => {
|
|
144
|
+
const sessionID = getString(event?.sessionID);
|
|
145
|
+
if (!sessionID) return;
|
|
146
|
+
runHook("tool.execute.after", sessionID, resolveCwd(sessionID), { event });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
(async () => {
|
|
152
|
+
try {
|
|
153
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
154
|
+
const sessionID = extractSessionID(event);
|
|
155
|
+
const directory =
|
|
156
|
+
getString(event?.location?.directory) || getString(event?.data?.location?.directory);
|
|
157
|
+
if (sessionID && directory) rememberCwd(sessionID, directory);
|
|
158
|
+
|
|
159
|
+
const type = event?.type ?? "";
|
|
160
|
+
if (type === "tool.execute.before" || type === "tool.execute.after") continue;
|
|
161
|
+
if (V1_ONLY_EVENTS.has(type)) continue;
|
|
162
|
+
// File edits, branch changes and todo updates can arrive without a
|
|
163
|
+
// session; there is no conversation to attribute them to.
|
|
164
|
+
if (!sessionID) continue;
|
|
165
|
+
runHook(type, sessionID, resolveCwd(sessionID), { event }, V2_EVENT_ALIASES.get(type));
|
|
166
|
+
// Evict regardless of whether session.deleted is in the installed
|
|
167
|
+
// EVENTS set, so an explicitly deleted session is released immediately
|
|
168
|
+
// rather than waiting to age out of the cap above.
|
|
169
|
+
if (type === "session.deleted") cwdBySession.delete(sessionID);
|
|
170
|
+
}
|
|
171
|
+
} catch (_) {
|
|
172
|
+
// Subscription tears down when the plugin is disposed; must never throw.
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
|
|
176
|
+
return () => controller.abort();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// OpenCode v2 validates the default export as an object before reading setup;
|
|
180
|
+
// callable objects fail that schema. v1.4+ reads server from this same shape.
|
|
181
|
+
// Older v1 hosts cannot share one module shape with v2 and are unsupported by
|
|
182
|
+
// this v2-capable template.
|
|
183
|
+
export default { id: "gk-hooks", server, setup };
|
|
184
|
+
`:`import { spawn } from "node:child_process";
|
|
185
|
+
|
|
186
|
+
const EVENTS = new Set({{.EventsJSON}});
|
|
187
|
+
const EXECUTABLE = {{.ExecJSON}};
|
|
188
|
+
const HOST = {{.HostJSON}};
|
|
189
|
+
const MAX_HOOK_INPUT_BYTES = {{.MaxInputBytesJSON}};
|
|
190
|
+
|
|
191
|
+
function getString(value) {
|
|
192
|
+
return typeof value === "string" && value.length > 0 ? value : "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// extractToolName is the one piece of the relay every host must supply: only
|
|
196
|
+
// the host knows where its payloads carry a tool name. The default below
|
|
197
|
+
// returns null so a host that forgets it still forwards events with
|
|
198
|
+
// tool_name: null, instead of throwing inside runHook's catch-all and
|
|
199
|
+
// silently forwarding nothing at all. Hosts override it with
|
|
200
|
+
// {{define "extractToolName"}} — see hook_pi.js.tmpl and
|
|
201
|
+
// hook_opencode.js.tmpl.
|
|
202
|
+
function extractToolName(payload) {
|
|
203
|
+
return getString(payload?.event?.toolName);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
function sessionIdOf(ctx) {
|
|
209
|
+
return getString(ctx?.sessionManager?.getSessionId?.());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default function (pi) {
|
|
213
|
+
for (const name of EVENTS) {
|
|
214
|
+
pi.on(name, async (event, ctx) => {
|
|
215
|
+
// A reload is not a session ending; forwarding it would kill a live session.
|
|
216
|
+
if (name === "session_shutdown" && event?.reason === "reload") return;
|
|
217
|
+
// No process.cwd() fallback: that is pi's launch directory, which the
|
|
218
|
+
// relay cannot tell apart from a real per-session cwd and would use to
|
|
219
|
+
// attribute the session to the wrong repo. Dropping the event lets the
|
|
220
|
+
// relay's missing-cwd guard stay meaningful.
|
|
221
|
+
const cwd = getString(ctx?.cwd);
|
|
222
|
+
if (!cwd) return;
|
|
223
|
+
runHook(name, sessionIdOf(ctx), cwd, { event });
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
`,s={EventsJSON:JSON.stringify(t),ExecJSON:JSON.stringify(n),HostJSON:JSON.stringify(e),MaxInputBytesJSON:`1048576`,V1OnlyEventsJSON:JSON.stringify([`session.updated`,`session.error`]),V2EventAliasesJSON:JSON.stringify({"session.execution.failed":[`session.error`],"session.status":[`session.updated`]})},c=e===`opencode`?`
|
|
228
|
+
|
|
229
|
+
const V1_ONLY_EVENTS`:`
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
function sessionIdOf`,l=o.indexOf(c);if(l<0)throw Error(`missing ${e} relay insertion point`);let u=o.slice(0,l)+`
|
|
234
|
+
|
|
235
|
+
function buildHookInput(eventName, sessionID, cwd, payload) {
|
|
236
|
+
const toolName = extractToolName(payload);
|
|
237
|
+
const hookInput = {
|
|
238
|
+
hook_event_name: eventName,
|
|
239
|
+
session_id: sessionID || "",
|
|
240
|
+
cwd,
|
|
241
|
+
tool_name: toolName || null,
|
|
242
|
+
source: null,
|
|
243
|
+
model: null,
|
|
244
|
+
reason: null,
|
|
245
|
+
agent_id: null,
|
|
246
|
+
agent_type: null,
|
|
247
|
+
hook_payload: payload || null,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const serializedInput = JSON.stringify(hookInput);
|
|
251
|
+
const payloadBytes = Buffer.byteLength(serializedInput, "utf8");
|
|
252
|
+
if (payloadBytes <= MAX_HOOK_INPUT_BYTES) {
|
|
253
|
+
return serializedInput;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return JSON.stringify({
|
|
257
|
+
...hookInput,
|
|
258
|
+
hook_payload: { truncated: true, originalBytes: payloadBytes },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function runHook(eventName, sessionID, cwd, payload, aliases = []) {
|
|
263
|
+
if (!EVENTS.has(eventName) && !aliases.some((alias) => EVENTS.has(alias))) return;
|
|
264
|
+
try {
|
|
265
|
+
const input = buildHookInput(eventName, sessionID, cwd, payload);
|
|
266
|
+
const child = spawn(EXECUTABLE, ["ai", "hook", "run", "--host", HOST], {
|
|
267
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
268
|
+
windowsHide: true,
|
|
269
|
+
});
|
|
270
|
+
child.on("error", () => {});
|
|
271
|
+
child.stdin.on("error", () => {});
|
|
272
|
+
child.stdin.end(input);
|
|
273
|
+
child.unref();
|
|
274
|
+
} catch (_) {
|
|
275
|
+
// Hooks must never block the host
|
|
276
|
+
}
|
|
277
|
+
}`+o.slice(l);return i+JSON.stringify(a)+`
|
|
278
|
+
`+u.replace(/{{\.(\w+)}}/g,(e,t)=>s[t]??``)}async function l(n,r,i){let s=a(n,r),c=s.slice(0,s.lastIndexOf(`/`));for(let n of await r.fs.readDir(c)??[]){if(n===`gk-hooks.js`||!n.startsWith(`gk-hooks.`))continue;let a=e(c,n);o(await r.fs.readFile(a))&&await t(r.fs,a,i)}}async function u(e,t){let i=a(e.host,t);if(e.blockingEvents?.length)throw new r(`blocking-unsupported`,`${e.host} does not support blocking hooks`,i);let u=await t.fs.readFile(i),d=s(u);if(u!==null&&!o(u))throw new r(`unmanaged-file`,`${i} already exists and was not written by GitKraken`,i);let f=[...new Set([...d?.events??[],...e.events??[]])].sort(),p=e.host===`opencode`?t.openCodeMajor??0:0,m=[];return await n(t.fs,i,c(e.host,f,e.executablePath.replace(/\\/g,`/`),p),m,{mode:384}),await l(e.host,t,m),{host:e.host,path:i,events:[...e.events??[]],blockingEvents:[],files:m}}async function d(e,r,i){let o=a(e,i),u=[],d=await i.fs.readFile(o),f=s(d),p=[],m=[];f&&(p=f.events.filter(e=>(r??f.events).includes(e)),m=f.events.filter(e=>!p.includes(e)).sort(),m.length?await n(i.fs,o,c(e,m,f.executable,f.hostMajor),u,{mode:384}):await t(i.fs,o,u)),await l(e,i,u);let h={host:e,path:o,removedEvents:p,remainingEvents:m,files:u};return p.length||(h.reason=d===null?`file-missing`:`not-installed`),h}export{u as installMarkerHooks,d as uninstallMarkerHooks};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { HookHost } from "./events.js";
|
|
3
|
+
import { HookInstallResult, HookUninstallResult } from "./results.js";
|
|
4
|
+
|
|
5
|
+
//#region src/hooks/writers.d.ts
|
|
6
|
+
type JsonHookHost = Extract<HookHost, 'cursor' | 'codex' | 'copilot' | 'antigravity' | 'augment' | 'grok'>;
|
|
7
|
+
interface HookWriteRequest {
|
|
8
|
+
host: HookHost;
|
|
9
|
+
executablePath: string;
|
|
10
|
+
events?: readonly string[];
|
|
11
|
+
blockingEvents?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
declare function command(executablePath: string, host: HookHost, config: AgentHostConfig, blocking: boolean): string;
|
|
14
|
+
/** Merges only GitKraken-owned hooks, preserving unrelated entries and document keys. */
|
|
15
|
+
declare function installJsonHooks(request: HookWriteRequest, config: AgentHostConfig): Promise<HookInstallResult>;
|
|
16
|
+
declare function uninstallJsonHooks(host: JsonHookHost, config: AgentHostConfig, events?: readonly string[]): Promise<HookUninstallResult>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { HookWriteRequest, command, installJsonHooks, uninstallJsonHooks };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{joinPath as e}from"../config.js";import{removeTracked as t,writeFileTracked as n}from"../file-change.js";import{HookConfigError as r}from"./errors.js";import{eventManifest as i}from"./events.js";const a={cursor:{path:t=>e(t.paths.home,`.cursor`,`hooks.json`),key:`hooks`,version:!0,matcher:!1},codex:{path:t=>e(t.paths.home,`.codex`,`hooks.json`),key:`hooks`,version:!1,matcher:!0},copilot:{path:t=>e(t.paths.home,`.copilot`,`hooks`,`gitkraken-hooks.json`),key:`hooks`,version:!0,matcher:!1},antigravity:{path:t=>e(t.paths.home,`.gemini`,`config`,`hooks.json`),key:`gitkraken`,version:!1,matcher:!1},augment:{path:t=>e(t.paths.home,`.augment`,`settings.json`),key:`hooks`,version:!1,matcher:!0,neverDelete:!0},grok:{path:t=>e(t.paths.grokHome??e(t.paths.home,`.grok`),`hooks`,`gitkraken-hooks.json`),key:`hooks`,version:!1,matcher:!0}};function o(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function s(e){return typeof e==`string`&&e.includes(` ai hook run --host `)}function c(e,t,n,r){return`${n.platform===`win32`&&`windowsShell`in i[t]&&i[t].windowsShell===`powershell`?`& `:``}"${e.replaceAll(`\\`,`/`)}" ai hook run --host ${t}${r?` --blocking`:``}`}function l(e){let t=i[e.host],n=e.events?.length||e.blockingEvents?.length?e.events??[]:t.events.filter(e=>!t.skippedDefaultEvents.includes(e)),a=e.blockingEvents??[];for(let i of[...n,...a])if(!t.events.includes(i))throw new r(`invalid-event`,`${i} is not valid for ${e.host}`);return[n,a]}function u(e,t){return e===`antigravity`?t===`PreToolUse`||t===`PostToolUse`:a[e].matcher}function d(e,t,n){if(!Array.isArray(e)||!e.every(o))throw new r(`wrong-shape`,`${n} has the wrong event shape`,n);if(t&&e.some(e=>`command`in e||`hooks`in e&&(!Array.isArray(e.hooks)||!e.hooks.every(o))))throw new r(`wrong-shape`,`${n} has the wrong matcher shape`,n);if(!t&&e.some(e=>`hooks`in e))throw new r(`wrong-shape`,`${n} has the wrong flat shape`,n)}function f(e,t){return Array.isArray(e)?e.some(e=>t&&o(e)&&Array.isArray(e.hooks)?e.hooks.some(e=>o(e)&&s(e.command)):o(e)&&s(e.command)):!1}function p(e,t){return Object.keys(t).filter(n=>f(t[n],u(e,n)))}function m(e,t){return t?e.flatMap(e=>{if(!Array.isArray(e.hooks))return[e];let t=e.hooks.filter(e=>!o(e)||!s(e.command));return t.length===e.hooks.length?[e]:t.length?[{...e,hooks:t}]:[]}):e.filter(e=>!s(e.command))}function h(e,t,n,r,a){let o=86400*(`blockingTimeoutMultiplier`in i[n]?Number(i[n].blockingTimeoutMultiplier):1),c={type:`command`,command:r,...a?{timeout:o}:{}};if(!t)return[...m(e,!1),c];let l=m(e,!0),u=l.find(e=>!e.matcher&&Array.isArray(e.hooks)&&e.hooks.length>0);u||(u=n===`augment`?{hooks:[]}:{matcher:``,hooks:[]},l.push(u));let d=Array.isArray(u.hooks)?u.hooks:[];return u.hooks=[...d.filter(e=>!s(e.command)),c],l}function g(e,t){let n;try{n=JSON.parse(e.replace(/^\uFEFF/,``))}catch{throw new r(`invalid-json`,`Invalid JSON at ${t}`,t)}if(!o(n))throw new r(`wrong-shape`,`Invalid hooks document at ${t}`,t);return n}function _(e,t,n){let i=e[t];if(i!=null){if(!o(i))throw new r(`wrong-shape`,`Invalid hooks wrapper at ${n}`,n);return i}}async function v(e,t){if(!(e.host in a))throw new r(`unsupported-host`,`No JSON writer for ${e.host}`);let o=e.host,s=a[o],f=s.path(t),p=await t.fs.readFile(f),v=p?.trim()?g(p,f):{},y={..._(v,s.key,f)};for(let[e,t]of Object.entries(y))d(t,u(o,e),f);let[b,x]=l(e);for(let[n,r]of[[b,!1],[x,!0]])for(let i of n){let n=u(o,i);y[i]=h(y[i]??[],n,o,c(e.executablePath,o,t,r),r)}if(`legacyEvents`in i[o])for(let e of i[o].legacyEvents){let t=y[e];if(t){let n=m(t,u(o,e));n.length?y[e]=n:delete y[e]}}Object.keys(y).length?v[s.key]=y:delete v[s.key],s.version&&v.version==null&&(v.version=1);let S=[];return await n(t.fs,f,`${JSON.stringify(v,null,2)}\n`,S),{host:o,path:f,events:[...b],blockingEvents:[...x],files:S}}async function y(e,r,o){let s=a[e],c=s.path(r),l=[],f=(t,n=[])=>({host:e,path:c,removedEvents:[],remainingEvents:n,files:l,reason:t}),h=await r.fs.readFile(c);if(h===null)return f(`file-missing`);if(!h.trim())return f(`not-installed`);let v=g(h,c),y=_(v,s.key,c);if(!y)return f(`not-installed`);let b={...y};for(let[t,n]of Object.entries(b))d(n,u(e,t),c);let x=[],S=o?.length?o:i[e].events;for(let t of[...S,...`legacyEvents`in i[e]?i[e].legacyEvents:[]]){let n=b[t];if(!n)continue;let r=m(n,u(e,t));JSON.stringify(r)!==JSON.stringify(n)&&(x.push(t),r.length?b[t]=r:delete b[t])}let C=p(e,b);if(!x.length)return f(`not-installed`,C);Object.keys(b).length?v[s.key]=b:delete v[s.key];let w=Object.keys(v).some(e=>e!==s.key&&!(e===`version`&&s.version&&v.version===1));return!Object.keys(b).length&&!w&&!s.neverDelete?await t(r.fs,c,l):await n(r.fs,c,`${JSON.stringify(v,null,2)}\n`,l),{host:e,path:c,removedEvents:x,remainingEvents:C,files:l}}export{c as command,v as installJsonHooks,y as uninstallJsonHooks};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AgentHostConfig, AgentHostPaths, AgentOrigin, joinPath } from "./config.js";
|
|
2
|
+
import { HookHost, eventManifest } from "./hooks/events.js";
|
|
3
|
+
import { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds } from "./mcp/clients.js";
|
|
4
|
+
import { Agent, HookCapability, listAgents, mcpNamesForHook, resolveHookClient } from "./agents.js";
|
|
5
|
+
import { FileChange, FileChangeKind } from "./file-change.js";
|
|
6
|
+
import { HookInstallResult, HookUninstallResult } from "./hooks/results.js";
|
|
7
|
+
import { HookConfigError, HookConfigErrorCode } from "./hooks/errors.js";
|
|
8
|
+
import { HookWriteRequest, installJsonHooks, uninstallJsonHooks } from "./hooks/writers.js";
|
|
9
|
+
import { InstallHooksRequest, installHooks, uninstallHooks } from "./hooks/install.js";
|
|
10
|
+
import { McpConfigEntry, generateConfigEntry } from "./mcp/config-entry.js";
|
|
11
|
+
import { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp } from "./mcp/installer.js";
|
|
12
|
+
import { getMcpCliInstallCommand } from "./mcp/cli-install-command.js";
|
|
13
|
+
import { McpUninstallFileResult, McpUninstallRequest, McpUninstallResult, uninstallMcp } from "./mcp/uninstaller.js";
|
|
14
|
+
import { SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, SkillManifest } from "./skills/manifest.js";
|
|
15
|
+
import { SkillAgent, allSkillAgents, isSkillAgentSupported, skillDir } from "./skills/skill-dirs.js";
|
|
16
|
+
import { SkillConflictError, SkillFiles, SkillInstallAction, SkillInstallOptions, SkillInstallResult, SkillOperation, SkillReason, SkillState, SkillStatus, SkillUninstallAction, SkillUninstallOptions, SkillUninstallResult, SkillUnsupportedManifestError, inspectSkillStatus, installSkill, uninstallSkill } from "./skills/install.js";
|
|
17
|
+
export { type Agent, type AgentHostConfig, type AgentHostPaths, type AgentOrigin, type Client, type ClientDetection, type ClientDetectionSource, type ClientInstallation, type ClientNotDetectedReason, type ClientType, type FileChange, type FileChangeKind, type HookCapability, HookConfigError, type HookConfigErrorCode, type HookHost, type HookInstallResult, type HookUninstallResult, type HookWriteRequest, type InstallHooksRequest, type McpConfigEntry, type McpInstallRequest, type McpInstallResult, type McpInstallState, type McpInstallStateReason, type McpInstallStatus, type McpUninstallFileResult, type McpUninstallRequest, type McpUninstallResult, SKILL_MANIFEST_FILE_NAME, SKILL_MANIFEST_SCHEMA_VERSION, type SkillAgent, SkillConflictError, type SkillFiles, type SkillInstallAction, type SkillInstallOptions, type SkillInstallResult, type SkillManifest, type SkillOperation, type SkillReason, type SkillState, type SkillStatus, type SkillUninstallAction, type SkillUninstallOptions, type SkillUninstallResult, SkillUnsupportedManifestError, allClients, allSkillAgents, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, eventManifest, generateConfigEntry, getClientInstallation, getMcpCliInstallCommand, hostIdentity, hostOf, inspectMcpInstall, inspectSkillStatus, installHooks, installJsonHooks, installMcp, installSkill, isInstallable, isLegacy, isSkillAgentSupported, isSupported, joinPath, legacyClientIds, listAgents, mcpNamesForHook, resolveHookClient, skillDir, uninstallHooks, uninstallJsonHooks, uninstallMcp, uninstallSkill };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{joinPath as e}from"./config.js";import{allClients as t,antigravityLegacyPath as n,clientCommand as r,clientConfigPath as i,clientConfigPaths as a,clientDisplayName as o,clientExecutablePath as s,clientJSONPath as c,clientType as l,clientsForHost as u,detectClient as d,getClientInstallation as f,hostIdentity as p,hostOf as m,isInstallable as h,isLegacy as g,isSupported as _,legacyClientIds as v}from"./mcp/clients.js";import{listAgents as y,mcpNamesForHook as b,resolveHookClient as x}from"./agents.js";import{HookConfigError as S}from"./hooks/errors.js";import{eventManifest as C}from"./hooks/events.js";import{installJsonHooks as w,uninstallJsonHooks as T}from"./hooks/writers.js";import{installHooks as E,uninstallHooks as D}from"./hooks/install.js";import{generateConfigEntry as O}from"./mcp/config-entry.js";import{inspectMcpInstall as k,installMcp as A}from"./mcp/installer.js";import{getMcpCliInstallCommand as j}from"./mcp/cli-install-command.js";import{uninstallMcp as M}from"./mcp/uninstaller.js";import{SKILL_MANIFEST_FILE_NAME as N,SKILL_MANIFEST_SCHEMA_VERSION as P}from"./skills/manifest.js";import{allSkillAgents as F,isSkillAgentSupported as I,skillDir as L}from"./skills/skill-dirs.js";import{SkillConflictError as R,SkillUnsupportedManifestError as z,inspectSkillStatus as B,installSkill as V,uninstallSkill as H}from"./skills/install.js";export{S as HookConfigError,N as SKILL_MANIFEST_FILE_NAME,P as SKILL_MANIFEST_SCHEMA_VERSION,R as SkillConflictError,z as SkillUnsupportedManifestError,t as allClients,F as allSkillAgents,n as antigravityLegacyPath,r as clientCommand,i as clientConfigPath,a as clientConfigPaths,o as clientDisplayName,s as clientExecutablePath,c as clientJSONPath,l as clientType,u as clientsForHost,d as detectClient,C as eventManifest,O as generateConfigEntry,f as getClientInstallation,j as getMcpCliInstallCommand,p as hostIdentity,m as hostOf,k as inspectMcpInstall,B as inspectSkillStatus,E as installHooks,w as installJsonHooks,A as installMcp,V as installSkill,h as isInstallable,g as isLegacy,I as isSkillAgentSupported,_ as isSupported,e as joinPath,v as legacyClientIds,y as listAgents,b as mcpNamesForHook,x as resolveHookClient,L as skillDir,D as uninstallHooks,T as uninstallJsonHooks,M as uninstallMcp,H as uninstallSkill};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { McpInstallRequest } from "./installer.js";
|
|
3
|
+
|
|
4
|
+
//#region src/mcp/cli-install-command.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Returns the argv of the client's own CLI shortcut for installing the
|
|
7
|
+
* GitKraken MCP server, or `undefined` when the host should go straight to
|
|
8
|
+
* {@link installMcp}. The host runs the argv and falls back to
|
|
9
|
+
* {@link installMcp} on failure, preserving Go's order (`installVSCode`,
|
|
10
|
+
* `installClaudeCLI`, `installCodex`, …).
|
|
11
|
+
*
|
|
12
|
+
* `undefined` when the client has no shortcut, and also when the shortcut
|
|
13
|
+
* would not do the job:
|
|
14
|
+
* - `request.configPath` is set, because no shortcut can target a specific file.
|
|
15
|
+
* - Cursor: gkcli ignores the outcome of `cursor --add-mcp` and always writes
|
|
16
|
+
* `~/.cursor/mcp.json`, so the direct write is the install.
|
|
17
|
+
* - Codex, when its config still holds the legacy `['mcp_servers.GitKraken']`
|
|
18
|
+
* table: `codex mcp add` would leave it behind, so the direct write must
|
|
19
|
+
* replace it.
|
|
20
|
+
*
|
|
21
|
+
* Excludes gkcli's GitLens deep-link URL scheme (`vscode:mcp/install?…`,
|
|
22
|
+
* `cursor://…`), which is a browser-launch side effect outside this
|
|
23
|
+
* library's scope.
|
|
24
|
+
*/
|
|
25
|
+
declare function getMcpCliInstallCommand(request: McpInstallRequest, config: AgentHostConfig): Promise<string[] | undefined>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { getMcpCliInstallCommand };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{inspectMcpInstall as e}from"./installer.js";function t(e,t,n){return JSON.stringify({command:t,type:`stdio`,name:e,args:[...n]})}async function n(n,r){if(n.configPath)return;let{client:i,executablePath:a,args:o}=n;switch(i){case`vscode`:return[`code`,`--add-mcp`,t(`GitKraken`,a,o)];case`vscode-insiders`:return[`code-insiders`,`--add-mcp`,t(`GitKraken`,a,o)];case`claude-cli`:return[`claude`,`mcp`,`add`,`GitKraken`,`-s`,`user`,`--`,a,...o];case`trae`:return[`trae`,`--add-mcp`,t(``,a,o)];case`codex`:{let{reason:t}=await e(n,r);return t===`legacy-location`?void 0:[`codex`,`mcp`,`add`,`GitKraken`,a,...o]}default:return}}export{n as getMcpCliInstallCommand};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
|
|
3
|
+
//#region src/mcp/clients.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The 16 MCP client ids gkcli knows about. `gemini` is legacy: retained only
|
|
6
|
+
* so an uninstall can clear a stale GitKraken entry, and excluded from
|
|
7
|
+
* {@link allClients}.
|
|
8
|
+
*/
|
|
9
|
+
type Client = 'vscode' | 'vscode-insiders' | 'claude' | 'claude-cli' | 'antigravity' | 'antigravity-cli' | 'windsurf' | 'cursor' | 'zed' | 'trae' | 'copilot' | 'codex' | 'gemini' | 'kiro' | 'jetbrains-copilot' | 'opencode';
|
|
10
|
+
/**
|
|
11
|
+
* How a client runs. Installing into a GUI client may launch its window (VS
|
|
12
|
+
* Code and Cursor are configured through their own binary); callers that must
|
|
13
|
+
* not disturb the desktop select `'cli'` clients.
|
|
14
|
+
*/
|
|
15
|
+
type ClientType = 'cli' | 'gui';
|
|
16
|
+
declare const OPEN_CODE_V1_JSON_PATH = "/mcp/GitKraken";
|
|
17
|
+
declare const OPEN_CODE_V2_JSON_PATH = "/mcp/servers/GitKraken";
|
|
18
|
+
/** Every location an OpenCode GitKraken entry may live at: v2 is written, v1 is only recognised and cleaned up. */
|
|
19
|
+
declare const OPEN_CODE_JSON_PATHS: readonly string[];
|
|
20
|
+
declare function isSupported(client: string): boolean;
|
|
21
|
+
/** Whether `client` is retained only for uninstall cleanup (currently only `gemini`). */
|
|
22
|
+
declare function isLegacy(client: Client): boolean;
|
|
23
|
+
/** Safe default for any client-name input: a supported, non-legacy client. */
|
|
24
|
+
declare function isInstallable(client: Client): boolean;
|
|
25
|
+
declare function clientType(client: Client): ClientType | 'unknown';
|
|
26
|
+
declare function clientDisplayName(client: Client): string;
|
|
27
|
+
declare function clientCommand(client: Client): string | undefined;
|
|
28
|
+
/** Every non-legacy client id, in registry order. */
|
|
29
|
+
declare function allClients(): Client[];
|
|
30
|
+
/** Every legacy client id, e.g. `gemini`. */
|
|
31
|
+
declare function legacyClientIds(): Client[];
|
|
32
|
+
/** The `--host=` value the installer bakes into a client's generated args. Rows sharing a host must emit the same value. */
|
|
33
|
+
declare function hostIdentity(client: Client): string;
|
|
34
|
+
/** The client whose configuration surface `client` shares (itself, for every row but the Antigravity pair). */
|
|
35
|
+
declare function hostOf(client: Client): Client;
|
|
36
|
+
/** Every non-legacy row sharing `host`'s configuration surface, `host` itself included, in registry order. */
|
|
37
|
+
declare function clientsForHost(host: Client): Client[];
|
|
38
|
+
/** The JSON pointer (or, for Codex, dotted TOML table key) `installMcp` writes a client's GitKraken entry at. */
|
|
39
|
+
declare function clientJSONPath(client: Client): string;
|
|
40
|
+
/** The single config file path this client's GitKraken entry is written to. */
|
|
41
|
+
declare function clientConfigPath(client: Client, config: AgentHostConfig): Promise<string | undefined>;
|
|
42
|
+
/**
|
|
43
|
+
* Every config file a client's GitKraken entry may live in: write-and-delete
|
|
44
|
+
* authority. `uninstallMcp` clears the entry from every path.
|
|
45
|
+
*/
|
|
46
|
+
declare function clientConfigPaths(client: Client, config: AgentHostConfig): Promise<string[]>;
|
|
47
|
+
/** The pre-shared-config location Antigravity's IDE used to read; uninstall still covers it for back-compat. */
|
|
48
|
+
declare function antigravityLegacyPath(config: AgentHostConfig): string;
|
|
49
|
+
/**
|
|
50
|
+
* The resolved path to the client's binary on `PATH`, or `undefined`. Go's
|
|
51
|
+
* `exec.LookPath` reads `PATH` and stats candidate directories directly; this
|
|
52
|
+
* library never reads the environment, so the host injects the lookup.
|
|
53
|
+
*/
|
|
54
|
+
declare function clientExecutablePath(client: Client, config: AgentHostConfig): string | undefined;
|
|
55
|
+
/** How {@link detectClient} found a client. */
|
|
56
|
+
type ClientDetectionSource = 'executable' | 'config-file' | 'config-dir';
|
|
57
|
+
/** Why {@link detectClient} did not find a client. `shared-host-row` is a row such as `antigravity-cli`, which has no config signal of its own and no binary on `PATH`. */
|
|
58
|
+
type ClientNotDetectedReason = 'not-found' | 'shared-host-row';
|
|
59
|
+
interface ClientDetection {
|
|
60
|
+
client: Client;
|
|
61
|
+
detected: boolean;
|
|
62
|
+
/** Set when `detected`. */
|
|
63
|
+
source?: ClientDetectionSource;
|
|
64
|
+
/** The client's binary on `PATH`, when `resolveExecutable` found it. */
|
|
65
|
+
executablePath?: string;
|
|
66
|
+
/** The config file or directory that matched. */
|
|
67
|
+
path?: string;
|
|
68
|
+
/** Set when not `detected`. */
|
|
69
|
+
reason?: ClientNotDetectedReason;
|
|
70
|
+
/** Every config file or directory probed, in order. */
|
|
71
|
+
checkedPaths: string[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether the client's binary or config directory is present, and how it was
|
|
75
|
+
* found. Mirrors Go's `mcp.Client.IsDetected`.
|
|
76
|
+
*/
|
|
77
|
+
declare function detectClient(client: Client, config: AgentHostConfig): Promise<ClientDetection>;
|
|
78
|
+
interface ClientInstallation {
|
|
79
|
+
client: Client;
|
|
80
|
+
installed: boolean;
|
|
81
|
+
/** The config files that carry a GitKraken entry, so a host can tell when the entry sits in a legacy file. */
|
|
82
|
+
paths: string[];
|
|
83
|
+
}
|
|
84
|
+
/** Which of the client's config files already carry a GitKraken entry. Mirrors Go's `mcp.Client.IsInstalled`. */
|
|
85
|
+
declare function getClientInstallation(client: Client, config: AgentHostConfig): Promise<ClientInstallation>;
|
|
86
|
+
//#endregion
|
|
87
|
+
export { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, OPEN_CODE_JSON_PATHS, OPEN_CODE_V1_JSON_PATH, OPEN_CODE_V2_JSON_PATH, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{joinPath as e}from"../config.js";import{findAtPointer as t}from"./jsonc-edit.js";import{parseTomlSafe as n,tomlDottedPath as r,tomlLiteralKey as i}from"./toml-read.js";const a=`/mcp/GitKraken`,o=`/mcp/servers/GitKraken`,s=[o,a];function c(t,...n){return e(t.paths.home,...n)}function l(e){return e.platform===`linux`?e.paths.configHome:e.paths.appSupport}function u(t){return e(t.paths.configHome,`opencode`)}const d=[{id:`vscode`,displayName:`VS Code`,clientType:`gui`,command:`code`,jsonPath:`/servers/GitKraken`,pathFn:t=>e(l(t),`Code`,`User`,`mcp.json`),legacy:!1},{id:`vscode-insiders`,displayName:`VS Code Insiders`,clientType:`gui`,command:`code-insiders`,jsonPath:`/servers/GitKraken`,pathFn:t=>e(l(t),`Code - Insiders`,`User`,`mcp.json`),legacy:!1},{id:`claude`,displayName:`Claude Desktop`,clientType:`gui`,command:`claude`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>e(l(t),`Claude`,`claude_desktop_config.json`),legacy:!1},{id:`claude-cli`,displayName:`Claude Code`,clientType:`cli`,command:`claude`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>t.paths.claudeConfigDir?e(t.paths.claudeConfigDir,`.claude.json`):c(t,`.claude.json`),legacy:!1},{id:`antigravity`,displayName:`Antigravity`,clientType:`gui`,command:`antigravity`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.gemini`,`config`,`mcp_config.json`),legacy:!1,host:`antigravity`},{id:`antigravity-cli`,displayName:`Antigravity CLI`,clientType:`cli`,command:`agy`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.gemini`,`config`,`mcp_config.json`),legacy:!1,host:`antigravity`},{id:`windsurf`,displayName:`Windsurf`,clientType:`gui`,command:`windsurf`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.codeium`,`windsurf`,`mcp_config.json`),legacy:!1},{id:`cursor`,displayName:`Cursor`,clientType:`gui`,command:`cursor`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.cursor`,`mcp.json`),legacy:!1},{id:`zed`,displayName:`Zed`,clientType:`gui`,command:`zed`,jsonPath:`/context_servers/GitKraken`,pathFn:t=>t.platform===`win32`?e(t.paths.appSupport,`Zed`,`settings.json`):t.platform===`darwin`?c(t,`.config`,`zed`,`settings.json`):e(t.paths.configHome,`zed`,`settings.json`),legacy:!1},{id:`trae`,displayName:`Trae`,clientType:`gui`,command:`trae`,jsonPath:`/mcpServers/GitKraken`,pathFn:t=>e(l(t),`Trae`,`User`,`mcp.json`),legacy:!1},{id:`copilot`,displayName:`GitHub Copilot CLI`,clientType:`cli`,command:`copilot`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.copilot`,`mcp-config.json`),legacy:!1},{id:`codex`,displayName:`Codex`,clientType:`cli`,command:`codex`,jsonPath:`mcp_servers.GitKraken`,pathFn:e=>c(e,`.codex`,`config.toml`),legacy:!1},{id:`gemini`,displayName:`Gemini CLI`,clientType:`cli`,command:`gemini`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.gemini`,`settings.json`),legacy:!0},{id:`kiro`,displayName:`Kiro`,clientType:`gui`,command:`kiro`,jsonPath:`/mcpServers/GitKraken`,pathFn:e=>c(e,`.kiro`,`settings`,`mcp.json`),legacy:!1},{id:`jetbrains-copilot`,displayName:`JetBrains Copilot`,clientType:`gui`,jsonPath:`/servers/GitKraken`,pathFn:t=>t.platform===`win32`?e(t.paths.localAppData??e(t.paths.home,`AppData`,`Local`),`github-copilot`,`intellij`,`mcp.json`):e(t.paths.configHome,`github-copilot`,`intellij`,`mcp.json`),legacy:!1},{id:`opencode`,displayName:`OpenCode`,clientType:`cli`,command:`opencode`,jsonPath:o,pathFn:t=>e(u(t),`opencode.json`),legacy:!1}];function f(e){return d.find(t=>t.id===e)}function p(e){return f(e)!==void 0}function m(e){return f(e)?.legacy===!0}function h(e){let t=f(e);return t!==void 0&&!t.legacy}function g(e){return f(e)?.clientType??`unknown`}function _(e){return f(e)?.displayName??e}function v(e){return f(e)?.command}function y(){return d.filter(e=>!e.legacy).map(e=>e.id)}function b(){return d.filter(e=>e.legacy).map(e=>e.id)}function x(e){return(f(e)?.host??e).toLowerCase()}function S(e){return f(e)?.host??e}function C(e){return d.filter(t=>!t.legacy&&(t.host??t.id)===e).map(e=>e.id)}function w(e){return f(e)?.jsonPath??``}async function T(t){let n=u(t),r=e(n,`opencode.jsonc`);return await t.fs.readFile(r)===null?e(n,`opencode.json`):r}async function E(e,t){let n=f(e);if(n)return e===`opencode`?T(t):n.pathFn(t)}async function D(e,t){let n=await E(e,t),r=n?[n]:[];return e===`antigravity`&&r.push(c(t,`.gemini`,`antigravity`,`mcp_config.json`)),r}function O(e){return c(e,`.gemini`,`antigravity`,`mcp_config.json`)}function k(e,t){let n=v(e);if(!(!n||!t.resolveExecutable))return t.resolveExecutable(n)}function A(e,t){let n=f(e);return n?.host?C(n.host).some(n=>n!==e&&k(n,t)!==void 0):!1}async function j(e,t){if(!t)return;let n=t.lastIndexOf(`/`),r=n>0?t.slice(0,n):``;if(r)return r===e.paths.home.replace(/\/+$/,``)?{matched:await e.fs.readFile(t)!==null,source:`config-file`,path:t}:{matched:await e.fs.readDir(r)!==null,source:`config-dir`,path:r}}async function M(e,t,n,r){for(let i of t){let t=await j(n,i);if(t&&(r.push(t.path),t.matched))return{client:e,detected:!0,source:t.source,path:t.path,checkedPaths:r}}return{client:e,detected:!1,reason:`not-found`,checkedPaths:r}}async function N(e,t){let n=k(e,t),r=[];if(n!==void 0)return{client:e,detected:!0,source:`executable`,executablePath:n,checkedPaths:r};let i=f(e);if(i?.host&&i.host!==e)return{client:e,detected:!1,reason:`shared-host-row`,checkedPaths:r};if(e===`antigravity`){let n=i?.pathFn(t);return n&&!A(e,t)&&(r.push(n),await t.fs.readFile(n)!==null)?{client:e,detected:!0,source:`config-file`,path:n,checkedPaths:r}:M(e,[O(t)],t,r)}return M(e,await D(e,t),t,r)}async function P(e,t){let n=[];for(let r of await D(e,t))await F(e,r,t)&&n.push(r);return{client:e,installed:n.length>0,paths:n}}async function F(e,a,o){let c=await o.fs.readFile(a);if(c===null||c.trim()===``)return!1;if(e===`codex`){let e=n(c);if(!e)return!1;let t=w(`codex`);return i(e,t)!==void 0||r(e,t)!==void 0}return(e===`opencode`?s:[w(e)]).some(e=>t(c,e)!==void 0)}export{s as OPEN_CODE_JSON_PATHS,a as OPEN_CODE_V1_JSON_PATH,o as OPEN_CODE_V2_JSON_PATH,y as allClients,O as antigravityLegacyPath,v as clientCommand,E as clientConfigPath,D as clientConfigPaths,_ as clientDisplayName,k as clientExecutablePath,w as clientJSONPath,g as clientType,C as clientsForHost,N as detectClient,P as getClientInstallation,x as hostIdentity,S as hostOf,h as isInstallable,m as isLegacy,p as isSupported,b as legacyClientIds};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Client } from "./clients.js";
|
|
2
|
+
|
|
3
|
+
//#region src/mcp/config-entry.d.ts
|
|
4
|
+
/** The shape of an entry `installMcp` writes: keys vary per client family (see {@link generateConfigEntry}). */
|
|
5
|
+
type McpConfigEntry = Record<string, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Builds the entry a client's GitKraken MCP server config carries. Port of
|
|
8
|
+
* gkcli's `installer.generateConfigs`/`installConfig`: four shapes dispatched
|
|
9
|
+
* by client family. OpenCode always gets its v2 shape.
|
|
10
|
+
*/
|
|
11
|
+
declare function generateConfigEntry(client: Client, executablePath: string, args: readonly string[]): McpConfigEntry;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { McpConfigEntry, generateConfigEntry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,t,n){switch(e){case`zed`:return{source:`custom`,enabled:!0,command:t,args:[...n]};case`copilot`:return{type:`local`,command:t,tools:[`*`],args:[...n]};case`opencode`:return{type:`local`,command:[t,...n],disabled:!1};default:return{type:`stdio`,command:t,args:[...n]}}}export{e as generateConfigEntry};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Client, ClientDetection, ClientDetectionSource, ClientInstallation, ClientNotDetectedReason, ClientType, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, getClientInstallation, hostIdentity, hostOf, isInstallable, isLegacy, isSupported, legacyClientIds } from "./clients.js";
|
|
2
|
+
import { McpConfigEntry, generateConfigEntry } from "./config-entry.js";
|
|
3
|
+
import { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp } from "./installer.js";
|
|
4
|
+
import { getMcpCliInstallCommand } from "./cli-install-command.js";
|
|
5
|
+
import { McpUninstallFileResult, McpUninstallRequest, McpUninstallResult, uninstallMcp } from "./uninstaller.js";
|
|
6
|
+
export { type Client, type ClientDetection, type ClientDetectionSource, type ClientInstallation, type ClientNotDetectedReason, type ClientType, type McpConfigEntry, type McpInstallRequest, type McpInstallResult, type McpInstallState, type McpInstallStateReason, type McpInstallStatus, type McpUninstallFileResult, type McpUninstallRequest, type McpUninstallResult, allClients, antigravityLegacyPath, clientCommand, clientConfigPath, clientConfigPaths, clientDisplayName, clientExecutablePath, clientJSONPath, clientType, clientsForHost, detectClient, generateConfigEntry, getClientInstallation, getMcpCliInstallCommand, hostIdentity, hostOf, inspectMcpInstall, installMcp, isInstallable, isLegacy, isSupported, legacyClientIds, uninstallMcp };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{allClients as e,antigravityLegacyPath as t,clientCommand as n,clientConfigPath as r,clientConfigPaths as i,clientDisplayName as a,clientExecutablePath as o,clientJSONPath as s,clientType as c,clientsForHost as l,detectClient as u,getClientInstallation as d,hostIdentity as f,hostOf as p,isInstallable as m,isLegacy as h,isSupported as g,legacyClientIds as _}from"./clients.js";import{generateConfigEntry as v}from"./config-entry.js";import{inspectMcpInstall as y,installMcp as b}from"./installer.js";import{getMcpCliInstallCommand as x}from"./cli-install-command.js";import{uninstallMcp as S}from"./uninstaller.js";export{e as allClients,t as antigravityLegacyPath,n as clientCommand,r as clientConfigPath,i as clientConfigPaths,a as clientDisplayName,o as clientExecutablePath,s as clientJSONPath,c as clientType,l as clientsForHost,u as detectClient,v as generateConfigEntry,d as getClientInstallation,x as getMcpCliInstallCommand,f as hostIdentity,p as hostOf,y as inspectMcpInstall,b as installMcp,m as isInstallable,h as isLegacy,g as isSupported,_ as legacyClientIds,S as uninstallMcp};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { AgentHostConfig } from "../config.js";
|
|
2
|
+
import { Client } from "./clients.js";
|
|
3
|
+
import { McpConfigEntry } from "./config-entry.js";
|
|
4
|
+
|
|
5
|
+
//#region src/mcp/installer.d.ts
|
|
6
|
+
interface McpInstallRequest {
|
|
7
|
+
client: Client;
|
|
8
|
+
executablePath: string;
|
|
9
|
+
/** The full MCP server argv the host computed (e.g. `['mcp', '--host=cursor']`); this library never derives it. */
|
|
10
|
+
args: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Targets this path instead of the client's resolved config path. Lets a
|
|
13
|
+
* host aim at a specific file, such as a workspace-scoped config.
|
|
14
|
+
*/
|
|
15
|
+
configPath?: string;
|
|
16
|
+
}
|
|
17
|
+
/** How the client's config compares to what `installMcp` would write. */
|
|
18
|
+
type McpInstallState = 'missing' | 'drifted' | 'current';
|
|
19
|
+
/** Why {@link inspectMcpInstall} reported its state. Absent when the state is `current`. */
|
|
20
|
+
type McpInstallStateReason = /** `missing`: the client's config file does not exist. */'file-missing' /** `missing`: the config file does not parse, so no entry can be read. */ | 'invalid-config' /** `missing`: the config has no GitKraken entry. */ | 'no-entry' /** `missing`: the GitKraken key exists but is not an object. */ | 'entry-not-object' /** `missing`: the client id is unknown or uninstall-only, so it has no config to inspect. */ | 'unsupported-client' /** `drifted`: the entry sits where an older install wrote it (Codex's literal-key table, OpenCode's v1 shape). */ | 'legacy-location' /** `drifted`: the entry lacks `command` or `args`. */ | 'pinned-key-missing' /** `drifted`: a key `installMcp` writes holds a different value. */ | 'value-mismatch';
|
|
21
|
+
interface McpInstallStatus {
|
|
22
|
+
client: Client;
|
|
23
|
+
state: McpInstallState;
|
|
24
|
+
/** The config file inspected; empty when the client has none. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Where the entry was looked up, or found when it is a legacy location. */
|
|
27
|
+
location: string;
|
|
28
|
+
reason?: McpInstallStateReason;
|
|
29
|
+
/** The key that differs, for `pinned-key-missing` and `value-mismatch`. */
|
|
30
|
+
key?: string;
|
|
31
|
+
/** The entry on disk, when one exists. A host can read it to carry user-added args forward before reinstalling. */
|
|
32
|
+
entry?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
interface McpInstallResult {
|
|
35
|
+
client: Client;
|
|
36
|
+
/** The config file written. */
|
|
37
|
+
path: string;
|
|
38
|
+
change: 'created' | 'updated' | 'unchanged';
|
|
39
|
+
format: 'json' | 'toml';
|
|
40
|
+
/** A JSON pointer, or the Codex dotted TOML table key. */
|
|
41
|
+
location: string;
|
|
42
|
+
/** The entry written, or already present when `change` is `unchanged`. */
|
|
43
|
+
entry: McpConfigEntry;
|
|
44
|
+
/** Stale locations pruned from the file, such as OpenCode's v1 pointer; empty otherwise. */
|
|
45
|
+
removedLocations: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Writes the GitKraken entry in the shape each client reads. The shapes are
|
|
49
|
+
* compatible with entries gkcli writes, so either tool recognises and repairs
|
|
50
|
+
* the other's. A re-run that finds the entry already in place writes nothing.
|
|
51
|
+
* Port of gkcli's installer.
|
|
52
|
+
*
|
|
53
|
+
* OpenCode gets its v2 shape and any v1 entry is pruned, which a v1-only
|
|
54
|
+
* OpenCode will no longer read. Run the client's own CLI shortcut first when
|
|
55
|
+
* it has one (see {@link getMcpCliInstallCommand}); this is the direct
|
|
56
|
+
* config-file write.
|
|
57
|
+
*
|
|
58
|
+
* Throws, without writing, for a client that is unknown or uninstall-only and
|
|
59
|
+
* for an existing config file that does not parse.
|
|
60
|
+
*/
|
|
61
|
+
declare function installMcp(request: McpInstallRequest, config: AgentHostConfig): Promise<McpInstallResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Reports whether the client's config already holds an entry equivalent to
|
|
64
|
+
* the one `installMcp` would write, and if not, why. Mirrors Go's
|
|
65
|
+
* `installer.State`.
|
|
66
|
+
*/
|
|
67
|
+
declare function inspectMcpInstall(request: McpInstallRequest, config: AgentHostConfig): Promise<McpInstallStatus>;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { McpInstallRequest, McpInstallResult, McpInstallState, McpInstallStateReason, McpInstallStatus, inspectMcpInstall, installMcp };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{addAtPointer as e,findAtPointer as t,isValidJsonc as n,removeAtPointerAndEmptyParent as r}from"./jsonc-edit.js";import{assertValidToml as i,parseTomlSafe as a,tomlDottedPath as o,tomlLiteralKey as s}from"./toml-read.js";import{OPEN_CODE_V1_JSON_PATH as c,clientConfigPath as l,clientJSONPath as u,isInstallable as d}from"./clients.js";import{writeFileTracked as f}from"../file-change.js";import{generateConfigEntry as p}from"./config-entry.js";import{tomlSemanticEqual as m,withoutCodexEntry as h}from"./toml-safety.js";import{removeBlock as g,replaceOrAppendBlock as _}from"./toml-splice.js";import{stringify as v}from"smol-toml";const y=[`command`,`args`];async function b(n,i){let{client:a}=n,o=d(a)?n.configPath??await l(a,i):void 0;if(!o)throw Error(`unsupported client: ${a}`);let s=p(a,n.executablePath,n.args);if(a===`codex`)return x(n,i,o,s);let m=await i.fs.readFile(o),h=m===null||m.trim()===``?`{}`:m,g=u(a),_=t(h,g),v=m!==null&&C(_)&&k(_,s,{client:a,path:o,location:g}).state===`current`,y=v?h:e(h,g,s,o),b=[];if(a===`opencode`){let e=r(v?h:y,c,o);e.removed&&(y=e.text,b.push(c))}return{client:a,path:o,change:v&&b.length===0?`unchanged`:await f(i.fs,o,y,[],{mode:420}),format:`json`,location:g,entry:v?_:s,removedLocations:b}}async function x(e,t,n,r){let c=await t.fs.readFile(n);c!==null&&c.trim()!==``&&i(c,n);let l=u(`codex`),d=c??``,p=a(d),v=o(p,l),y=s(p,l);if(c!==null&&y===void 0&&C(v)&&k(v,r,{client:e.client,path:n,location:l}).state===`current`)return{client:e.client,path:n,change:`unchanged`,format:`toml`,location:l,entry:v,removedLocations:[]};let b,x,w=[];if(y!==void 0){if(!C(y)||v!==void 0&&!C(v))throw Error(`cannot safely migrate Codex MCP entry in ${n}: ambiguous legacy and nested tables`);let e=C(v)&&v.env!==void 0?v.env:y.env;if(e!==void 0&&!C(e))throw Error(`cannot safely migrate Codex MCP entry in ${n}: legacy env is not a table`);if(Object.entries(y).some(([e,t])=>e!==`env`&&C(t))||e&&Object.values(e).some(C))throw Error(`cannot safely migrate Codex MCP entry in ${n}: unsupported legacy subtable`);x=e;let t=g(d,l).text;b=_(v===void 0?t:g(t,l).text,l,S(l,r,e)),w.push(`["${l}"]`)}else b=_(d,l,S(l,r));let T=a(b),E=o(T,l);if(!C(E)||k(E,r,{client:e.client,path:n,location:l}).state!==`current`||s(T,l)!==void 0||y!==void 0&&!m(E.env,x)||!m(h(T),h(p)))throw Error(`cannot safely update Codex MCP entry in ${n}: unsupported TOML representation`);let D=await f(t.fs,n,b,[],{mode:384});return{client:e.client,path:n,change:D,format:`toml`,location:l,entry:E,removedLocations:w}}function S(e,t,n){let r=v(t),i=r.endsWith(`
|
|
2
|
+
`)?r:`${r}\n`,a=n===void 0?``:v(n);return`[${e}]\n${i}${n===void 0?``:`\n[${e}.env]\n${a.endsWith(`
|
|
3
|
+
`)?a:`${a}\n`}`}`}function C(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function w(e,t){return{...e,state:`missing`,reason:t}}function T(e,t){return{...e,state:`drifted`,reason:`legacy-location`,...C(t)&&{entry:t}}}async function E(e,r){let{client:i}=e,a=u(i),o=d(i)?e.configPath??await l(i,r):void 0;if(!o)return w({client:i,path:``,location:a},`unsupported-client`);let s={client:i,path:o,location:a},c=await r.fs.readFile(o);if(c===null)return w(s,`file-missing`);let f=p(i,e.executablePath,e.args);if(i===`codex`)return D(c,f,s);if(c.trim()===``)return w(s,`no-entry`);if(!n(c))return w(s,`invalid-config`);if(i===`opencode`)return O(c,f,s);let m=t(c,a);return m===void 0?w(s,`no-entry`):C(m)?k(m,f,s):w(s,`entry-not-object`)}function D(e,t,n){if(e.trim()===``)return w(n,`no-entry`);let r=a(e);if(!r)return w(n,`invalid-config`);let i=s(r,n.location);if(i!==void 0)return T(n,i);let c=o(r,n.location);return c===void 0?w(n,`no-entry`):C(c)?k(c,t,n):w(n,`entry-not-object`)}function O(e,n,r){let i=t(e,r.location),a=t(e,c),o={...r,location:c};if(i===void 0)return a===void 0?w(r,`no-entry`):T(o,a);if(!C(i))return w(r,`entry-not-object`);let s=k(i,n,r);return s.state===`current`&&a!==void 0?T(o,a):s}function k(e,t,n){for(let[r,i]of Object.entries(t)){if(!(r in e)){if(y.includes(r))return{...n,state:`drifted`,reason:`pinned-key-missing`,key:r,entry:e};continue}if(!A(e[r],i))return{...n,state:`drifted`,reason:`value-mismatch`,key:r,entry:e}}return{...n,state:`current`,entry:e}}function A(e,t){if(e===t)return!0;if(typeof e!=typeof t||e===null||t===null||typeof e!=`object`)return!1;if(Array.isArray(e)||Array.isArray(t))return!Array.isArray(e)||!Array.isArray(t)||e.length!==t.length?!1:e.every((e,n)=>A(e,t[n]));let n=Object.keys(e),r=Object.keys(t);return n.length===r.length?n.every(n=>A(e[n],t[n])):!1}export{E as inspectMcpInstall,b as installMcp};
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
/** Reads the value at `pointer` in JSONC `text`, or `undefined` when absent or the document is unparsable. */
|
|
5
|
+
declare function findAtPointer(text: string, pointer: string): unknown;
|
|
6
|
+
/**
|
|
7
|
+
* Adds `value` at `pointer`, creating any missing parent objects along the
|
|
8
|
+
* way (mirrors gkcli's `installer.writeConfig`, which patches in each
|
|
9
|
+
* missing parent as `{}` before adding the leaf so a JSON-patch "add" never
|
|
10
|
+
* fails on a missing container). Comment-preserving: only the edited range
|
|
11
|
+
* is touched. `sourcePath` names `text`'s file for the error thrown when it
|
|
12
|
+
* isn't valid JSONC; no edit is made in that case.
|
|
13
|
+
*/
|
|
14
|
+
declare function addAtPointer(text: string, pointer: string, value: unknown, sourcePath: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Removes the value at `pointer`, leaving its parent container in place even
|
|
17
|
+
* if that empties it. Mirrors gkcli's `uninstaller.writeConfig`, the shape
|
|
18
|
+
* every plain client's `UninstallMCP` uses. `sourcePath` names `text`'s file
|
|
19
|
+
* for the error thrown when it isn't valid JSONC; no edit is made in that case.
|
|
20
|
+
*
|
|
21
|
+
* Returns `removed: false` when there was nothing at `pointer` to begin with.
|
|
22
|
+
*/
|
|
23
|
+
declare function removeAtPointer(text: string, pointer: string, sourcePath: string): {
|
|
24
|
+
text: string;
|
|
25
|
+
removed: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Removes the value at `pointer`, and then its parent container when that
|
|
29
|
+
* removal left it an empty object. Mirrors gkcli's
|
|
30
|
+
* `uninstaller.removeJSONPointer`, used only for OpenCode: v1 reads every key
|
|
31
|
+
* under `/mcp` as a server definition, so an empty `servers` husk would parse
|
|
32
|
+
* as one with no command. `sourcePath` is as for {@link removeAtPointer}.
|
|
33
|
+
*
|
|
34
|
+
* Returns `removed: false` when there was nothing at `pointer` to begin with.
|
|
35
|
+
*/
|
|
36
|
+
declare function removeAtPointerAndEmptyParent(text: string, pointer: string, sourcePath: string): {
|
|
37
|
+
text: string;
|
|
38
|
+
removed: boolean;
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { addAtPointer, findAtPointer, isValidJsonc, removeAtPointer, removeAtPointerAndEmptyParent };
|