@zlnap/astrolabe 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/bin/astrolabe.js +24 -10
- package/lib/setup/args.js +85 -0
- package/lib/setup/clients.js +912 -0
- package/lib/setup/codebuddy.js +93 -0
- package/lib/setup/dsh.js +137 -0
- package/lib/setup/grok.js +81 -0
- package/lib/setup/index.js +16 -0
- package/lib/setup/jsonc.js +334 -0
- package/lib/setup/kimicode.js +94 -0
- package/lib/setup/opencode.js +159 -0
- package/lib/setup/prompt.js +69 -0
- package/lib/setup/run.js +305 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/README.md +93 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/cordis.patch.yml +11 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/lib/index.js +145 -0
- package/lib/setup/vendor/astrolabe-dsh-guard/package.json +38 -0
- package/lib/setup/zcode.js +135 -0
- package/lib/setup.test.js +539 -0
- package/package.json +7 -7
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const opencodeMod = require('./opencode');
|
|
7
|
+
const dshMod = require('./dsh');
|
|
8
|
+
const codebuddyMod = require('./codebuddy');
|
|
9
|
+
const grokMod = require('./grok');
|
|
10
|
+
const kimicodeMod = require('./kimicode');
|
|
11
|
+
const zcodeMod = require('./zcode');
|
|
12
|
+
const PACKAGE_NAME = '@zlnap/astrolabe';
|
|
13
|
+
const SERVER_NAME = 'astrolabe';
|
|
14
|
+
|
|
15
|
+
/** Keep in sync with crates/astrolabe-mcp/src/lib.rs CATALOG_TOOL_NAMES. */
|
|
16
|
+
const CATALOG_TOOL_NAMES = [
|
|
17
|
+
'search_code',
|
|
18
|
+
'find_symbol',
|
|
19
|
+
'resolve_context',
|
|
20
|
+
'get_diagnostics',
|
|
21
|
+
'find_references',
|
|
22
|
+
'goto_definition',
|
|
23
|
+
'get_languages',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function which(cmd, env = process.env) {
|
|
27
|
+
const pathVar = env.PATH || env.Path || '';
|
|
28
|
+
const parts = pathVar.split(path.delimiter).filter(Boolean);
|
|
29
|
+
const exts =
|
|
30
|
+
process.platform === 'win32'
|
|
31
|
+
? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
|
|
32
|
+
: [''];
|
|
33
|
+
for (const dir of parts) {
|
|
34
|
+
for (const ext of exts) {
|
|
35
|
+
const candidate = path.join(dir, cmd + (ext && !cmd.toLowerCase().endsWith(ext.toLowerCase()) ? ext : ''));
|
|
36
|
+
try {
|
|
37
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function exists(p) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.existsSync(p);
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appSupport(...parts) {
|
|
55
|
+
if (process.platform === 'darwin') {
|
|
56
|
+
return path.join(os.homedir(), 'Library', 'Application Support', ...parts);
|
|
57
|
+
}
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
60
|
+
return path.join(base, ...parts);
|
|
61
|
+
}
|
|
62
|
+
return path.join(os.homedir(), '.config', ...parts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveLaunch({ context, native, commandOverride }) {
|
|
66
|
+
if (commandOverride) {
|
|
67
|
+
const args = ['.'];
|
|
68
|
+
if (context) args.push(`--context=${context}`);
|
|
69
|
+
return { command: commandOverride, args };
|
|
70
|
+
}
|
|
71
|
+
if (native) {
|
|
72
|
+
const args = ['.'];
|
|
73
|
+
if (context) args.push(`--context=${context}`);
|
|
74
|
+
return { command: 'astrolabe', args };
|
|
75
|
+
}
|
|
76
|
+
const args = ['-y', PACKAGE_NAME, '.'];
|
|
77
|
+
if (context) args.push(`--context=${context}`);
|
|
78
|
+
return { command: 'npx', args };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function jsonServerEntry(launch, { typed = false } = {}) {
|
|
82
|
+
const entry = { command: launch.command, args: launch.args };
|
|
83
|
+
if (typed) entry.type = 'stdio';
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mergeJsonMcpFile(filePath, entry, { create = true } = {}) {
|
|
88
|
+
let doc = {};
|
|
89
|
+
let created = false;
|
|
90
|
+
if (exists(filePath)) {
|
|
91
|
+
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
|
92
|
+
doc = JSON.parse(raw);
|
|
93
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
94
|
+
throw new Error(`${filePath} root must be a JSON object`);
|
|
95
|
+
}
|
|
96
|
+
} else if (!create) {
|
|
97
|
+
return { skipped: true, reason: `missing ${filePath} (will not create)` };
|
|
98
|
+
} else {
|
|
99
|
+
created = true;
|
|
100
|
+
}
|
|
101
|
+
if (!doc.mcpServers || typeof doc.mcpServers !== 'object' || Array.isArray(doc.mcpServers)) {
|
|
102
|
+
doc.mcpServers = {};
|
|
103
|
+
}
|
|
104
|
+
const had = Object.prototype.hasOwnProperty.call(doc.mcpServers, SERVER_NAME);
|
|
105
|
+
doc.mcpServers[SERVER_NAME] = entry;
|
|
106
|
+
return { doc, created, replaced: had, filePath };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mergeClaudeJson(filePath, entry) {
|
|
110
|
+
// ~/.claude.json is a large user settings file; only touch mcpServers.astrolabe.
|
|
111
|
+
return mergeJsonMcpFile(filePath, entry, { create: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Append or replace [mcp_servers.astrolabe] in Codex config.toml without a
|
|
116
|
+
* TOML dependency. Preserves the rest of the file byte-for-byte aside from
|
|
117
|
+
* the astrolabe table.
|
|
118
|
+
*/
|
|
119
|
+
function mergeCodexToml(filePath, launch) {
|
|
120
|
+
const block = [
|
|
121
|
+
`[mcp_servers.${SERVER_NAME}]`,
|
|
122
|
+
`command = ${tomlString(launch.command)}`,
|
|
123
|
+
`args = ${tomlStringArray(launch.args)}`,
|
|
124
|
+
'',
|
|
125
|
+
].join('\n');
|
|
126
|
+
|
|
127
|
+
let created = false;
|
|
128
|
+
let text = '';
|
|
129
|
+
if (exists(filePath)) {
|
|
130
|
+
text = fs.readFileSync(filePath, 'utf8');
|
|
131
|
+
} else {
|
|
132
|
+
created = true;
|
|
133
|
+
text = '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const headerRe = /^\[mcp_servers\.astrolabe\]\s*$/m;
|
|
137
|
+
const match = headerRe.exec(text);
|
|
138
|
+
let next;
|
|
139
|
+
let replaced = false;
|
|
140
|
+
if (match) {
|
|
141
|
+
replaced = true;
|
|
142
|
+
const start = match.index;
|
|
143
|
+
const after = text.slice(start + match[0].length);
|
|
144
|
+
const nextHeader = after.search(/^\[/m);
|
|
145
|
+
const end = nextHeader === -1 ? text.length : start + match[0].length + nextHeader;
|
|
146
|
+
const before = text.slice(0, start).replace(/\s*$/, '\n\n');
|
|
147
|
+
const rest = text.slice(end).replace(/^\s*/, '');
|
|
148
|
+
next = `${before}${block}${rest ? `\n${rest}` : ''}`;
|
|
149
|
+
} else {
|
|
150
|
+
const trimmed = text.replace(/\s*$/, '');
|
|
151
|
+
next = trimmed ? `${trimmed}\n\n${block}` : block;
|
|
152
|
+
}
|
|
153
|
+
return { text: next, created, replaced, filePath };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function tomlString(value) {
|
|
157
|
+
return JSON.stringify(String(value));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function tomlStringArray(values) {
|
|
161
|
+
return `[${values.map((v) => tomlString(v)).join(', ')}]`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function backupFile(filePath) {
|
|
165
|
+
if (!exists(filePath)) return null;
|
|
166
|
+
const real = fs.realpathSync(filePath);
|
|
167
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
168
|
+
const backup = `${real}.bak-${stamp}`;
|
|
169
|
+
fs.copyFileSync(real, backup);
|
|
170
|
+
return backup;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function writeTextAtomic(filePath, contents) {
|
|
174
|
+
let dest = filePath;
|
|
175
|
+
let mode = null;
|
|
176
|
+
if (exists(filePath)) {
|
|
177
|
+
dest = fs.realpathSync(filePath);
|
|
178
|
+
mode = fs.statSync(dest).mode & 0o7777;
|
|
179
|
+
}
|
|
180
|
+
const dir = path.dirname(dest);
|
|
181
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
182
|
+
const tmp = path.join(dir, `.${path.basename(dest)}.${process.pid}.${Date.now()}.tmp`);
|
|
183
|
+
fs.writeFileSync(tmp, contents.endsWith('\n') ? contents : `${contents}\n`, {
|
|
184
|
+
encoding: 'utf8',
|
|
185
|
+
mode: mode ?? 0o600,
|
|
186
|
+
});
|
|
187
|
+
if (mode !== null) {
|
|
188
|
+
try {
|
|
189
|
+
fs.chmodSync(tmp, mode);
|
|
190
|
+
} catch {
|
|
191
|
+
// best effort
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
fs.renameSync(tmp, dest);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeJsonAtomic(filePath, value) {
|
|
198
|
+
writeTextAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function mergeCursorPermissions(home, { dryRun = false, yes = false } = {}) {
|
|
202
|
+
const permissionsPath = path.join(home, '.cursor', 'permissions.json');
|
|
203
|
+
const extras = CATALOG_TOOL_NAMES.map((name) => `${SERVER_NAME}:${name}`);
|
|
204
|
+
let doc = {};
|
|
205
|
+
let created = false;
|
|
206
|
+
if (exists(permissionsPath)) {
|
|
207
|
+
doc = JSON.parse(fs.readFileSync(permissionsPath, 'utf8').replace(/^\uFEFF/, ''));
|
|
208
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
209
|
+
throw new Error(`${permissionsPath} root must be a JSON object`);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
created = true;
|
|
213
|
+
}
|
|
214
|
+
const existing = Array.isArray(doc.mcpAllowlist) ? doc.mcpAllowlist : [];
|
|
215
|
+
const seen = new Set(existing.filter((x) => typeof x === 'string'));
|
|
216
|
+
const list = existing.slice();
|
|
217
|
+
let added = 0;
|
|
218
|
+
for (const item of extras) {
|
|
219
|
+
if (seen.has(item)) continue;
|
|
220
|
+
seen.add(item);
|
|
221
|
+
list.push(item);
|
|
222
|
+
added += 1;
|
|
223
|
+
}
|
|
224
|
+
doc.mcpAllowlist = list;
|
|
225
|
+
return {
|
|
226
|
+
filePath: permissionsPath,
|
|
227
|
+
doc,
|
|
228
|
+
created,
|
|
229
|
+
added,
|
|
230
|
+
total: list.length,
|
|
231
|
+
dryRun,
|
|
232
|
+
yes,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function clineSettingsPath(home) {
|
|
237
|
+
if (process.platform === 'darwin') {
|
|
238
|
+
return path.join(
|
|
239
|
+
home,
|
|
240
|
+
'Library',
|
|
241
|
+
'Application Support',
|
|
242
|
+
'Code',
|
|
243
|
+
'User',
|
|
244
|
+
'globalStorage',
|
|
245
|
+
'saoudrizwan.claude-dev',
|
|
246
|
+
'settings',
|
|
247
|
+
'cline_mcp_settings.json',
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (process.platform === 'win32') {
|
|
251
|
+
const base = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
252
|
+
return path.join(
|
|
253
|
+
base,
|
|
254
|
+
'Code',
|
|
255
|
+
'User',
|
|
256
|
+
'globalStorage',
|
|
257
|
+
'saoudrizwan.claude-dev',
|
|
258
|
+
'settings',
|
|
259
|
+
'cline_mcp_settings.json',
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return path.join(
|
|
263
|
+
home,
|
|
264
|
+
'.config',
|
|
265
|
+
'Code',
|
|
266
|
+
'User',
|
|
267
|
+
'globalStorage',
|
|
268
|
+
'saoudrizwan.claude-dev',
|
|
269
|
+
'settings',
|
|
270
|
+
'cline_mcp_settings.json',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function rooSettingsPath(home) {
|
|
275
|
+
if (process.platform === 'darwin') {
|
|
276
|
+
return path.join(
|
|
277
|
+
home,
|
|
278
|
+
'Library',
|
|
279
|
+
'Application Support',
|
|
280
|
+
'Code',
|
|
281
|
+
'User',
|
|
282
|
+
'globalStorage',
|
|
283
|
+
'rooveterinaryinc.roo-cline',
|
|
284
|
+
'settings',
|
|
285
|
+
'mcp_settings.json',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (process.platform === 'win32') {
|
|
289
|
+
const base = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
290
|
+
return path.join(
|
|
291
|
+
base,
|
|
292
|
+
'Code',
|
|
293
|
+
'User',
|
|
294
|
+
'globalStorage',
|
|
295
|
+
'rooveterinaryinc.roo-cline',
|
|
296
|
+
'settings',
|
|
297
|
+
'mcp_settings.json',
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return path.join(
|
|
301
|
+
home,
|
|
302
|
+
'.config',
|
|
303
|
+
'Code',
|
|
304
|
+
'User',
|
|
305
|
+
'globalStorage',
|
|
306
|
+
'rooveterinaryinc.roo-cline',
|
|
307
|
+
'settings',
|
|
308
|
+
'mcp_settings.json',
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @typedef {object} ClientDef
|
|
314
|
+
* @property {string} id
|
|
315
|
+
* @property {string} label
|
|
316
|
+
* @property {string|null} context
|
|
317
|
+
* @property {(home: string, env?: NodeJS.ProcessEnv) => { detected: boolean, evidence: string[] }} detect
|
|
318
|
+
* @property {(ctx: object) => object} plan
|
|
319
|
+
*/
|
|
320
|
+
|
|
321
|
+
/** @type {ClientDef[]} */
|
|
322
|
+
const CLIENTS = [
|
|
323
|
+
{
|
|
324
|
+
id: 'claude-code',
|
|
325
|
+
label: 'Claude Code',
|
|
326
|
+
context: 'claude-code',
|
|
327
|
+
detect(home, env = process.env) {
|
|
328
|
+
const evidence = [];
|
|
329
|
+
if (which('claude', env)) evidence.push('claude on PATH');
|
|
330
|
+
if (exists(path.join(home, '.claude.json'))) evidence.push('~/.claude.json');
|
|
331
|
+
if (exists(path.join(home, '.claude'))) evidence.push('~/.claude/');
|
|
332
|
+
return { detected: evidence.length > 0, evidence };
|
|
333
|
+
},
|
|
334
|
+
plan({ home, launch }) {
|
|
335
|
+
const filePath = path.join(home, '.claude.json');
|
|
336
|
+
const entry = jsonServerEntry(launch, { typed: true });
|
|
337
|
+
return {
|
|
338
|
+
kind: 'json-mcp',
|
|
339
|
+
filePath,
|
|
340
|
+
entry,
|
|
341
|
+
summary: `merge mcpServers.astrolabe into ${filePath}`,
|
|
342
|
+
apply() {
|
|
343
|
+
const result = mergeClaudeJson(filePath, entry);
|
|
344
|
+
if (result.skipped) return result;
|
|
345
|
+
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: 'cursor',
|
|
352
|
+
label: 'Cursor',
|
|
353
|
+
context: 'cursor',
|
|
354
|
+
detect(home, env = process.env) {
|
|
355
|
+
const evidence = [];
|
|
356
|
+
if (which('cursor', env)) evidence.push('cursor on PATH');
|
|
357
|
+
if (exists(path.join(home, '.cursor'))) evidence.push('~/.cursor/');
|
|
358
|
+
if (process.platform === 'darwin' && exists('/Applications/Cursor.app')) {
|
|
359
|
+
evidence.push('/Applications/Cursor.app');
|
|
360
|
+
}
|
|
361
|
+
return { detected: evidence.length > 0, evidence };
|
|
362
|
+
},
|
|
363
|
+
plan({ home, launch }) {
|
|
364
|
+
const filePath = path.join(home, '.cursor', 'mcp.json');
|
|
365
|
+
const entry = jsonServerEntry(launch);
|
|
366
|
+
return {
|
|
367
|
+
kind: 'json-mcp',
|
|
368
|
+
filePath,
|
|
369
|
+
entry,
|
|
370
|
+
summary: `merge mcpServers.astrolabe into ${filePath}`,
|
|
371
|
+
apply() {
|
|
372
|
+
const result = mergeJsonMcpFile(filePath, entry, { create: true });
|
|
373
|
+
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: 'codex',
|
|
380
|
+
label: 'Codex',
|
|
381
|
+
context: 'codex',
|
|
382
|
+
detect(home, env = process.env) {
|
|
383
|
+
const evidence = [];
|
|
384
|
+
if (which('codex', env)) evidence.push('codex on PATH');
|
|
385
|
+
if (exists(path.join(home, '.codex'))) evidence.push('~/.codex/');
|
|
386
|
+
return { detected: evidence.length > 0, evidence };
|
|
387
|
+
},
|
|
388
|
+
plan({ home, launch }) {
|
|
389
|
+
const filePath = path.join(home, '.codex', 'config.toml');
|
|
390
|
+
return {
|
|
391
|
+
kind: 'codex-toml',
|
|
392
|
+
filePath,
|
|
393
|
+
summary: `merge [mcp_servers.astrolabe] into ${filePath}`,
|
|
394
|
+
apply() {
|
|
395
|
+
const result = mergeCodexToml(filePath, launch);
|
|
396
|
+
return { ...result, write: () => writeTextAtomic(filePath, result.text) };
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: 'windsurf',
|
|
403
|
+
label: 'Windsurf',
|
|
404
|
+
context: null,
|
|
405
|
+
detect(home, env = process.env) {
|
|
406
|
+
const evidence = [];
|
|
407
|
+
if (which('windsurf', env)) evidence.push('windsurf on PATH');
|
|
408
|
+
if (exists(path.join(home, '.codeium', 'windsurf'))) evidence.push('~/.codeium/windsurf/');
|
|
409
|
+
if (process.platform === 'darwin' && exists('/Applications/Windsurf.app')) {
|
|
410
|
+
evidence.push('/Applications/Windsurf.app');
|
|
411
|
+
}
|
|
412
|
+
return { detected: evidence.length > 0, evidence };
|
|
413
|
+
},
|
|
414
|
+
plan({ home, launch }) {
|
|
415
|
+
const filePath = path.join(home, '.codeium', 'windsurf', 'mcp_config.json');
|
|
416
|
+
const entry = jsonServerEntry(launch);
|
|
417
|
+
return {
|
|
418
|
+
kind: 'json-mcp',
|
|
419
|
+
filePath,
|
|
420
|
+
entry,
|
|
421
|
+
summary: `merge mcpServers.astrolabe into ${filePath}`,
|
|
422
|
+
apply() {
|
|
423
|
+
const result = mergeJsonMcpFile(filePath, entry, { create: true });
|
|
424
|
+
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: 'cline',
|
|
431
|
+
label: 'Cline (VS Code)',
|
|
432
|
+
context: null,
|
|
433
|
+
detect(home) {
|
|
434
|
+
const evidence = [];
|
|
435
|
+
const settings = clineSettingsPath(home);
|
|
436
|
+
if (exists(settings)) evidence.push(settings);
|
|
437
|
+
else if (exists(path.dirname(settings))) evidence.push(path.dirname(settings));
|
|
438
|
+
return { detected: evidence.length > 0, evidence };
|
|
439
|
+
},
|
|
440
|
+
plan({ home, launch }) {
|
|
441
|
+
const filePath = clineSettingsPath(home);
|
|
442
|
+
const entry = jsonServerEntry(launch);
|
|
443
|
+
return {
|
|
444
|
+
kind: 'json-mcp',
|
|
445
|
+
filePath,
|
|
446
|
+
entry,
|
|
447
|
+
summary: `merge mcpServers.astrolabe into ${filePath}`,
|
|
448
|
+
apply() {
|
|
449
|
+
// Only create when the extension settings dir already exists.
|
|
450
|
+
const create = exists(path.dirname(filePath)) || exists(filePath);
|
|
451
|
+
const result = mergeJsonMcpFile(filePath, entry, { create });
|
|
452
|
+
if (result.skipped) return result;
|
|
453
|
+
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: 'roo',
|
|
460
|
+
label: 'Roo Code (VS Code)',
|
|
461
|
+
context: null,
|
|
462
|
+
detect(home) {
|
|
463
|
+
const evidence = [];
|
|
464
|
+
const settings = rooSettingsPath(home);
|
|
465
|
+
if (exists(settings)) evidence.push(settings);
|
|
466
|
+
else if (exists(path.dirname(settings))) evidence.push(path.dirname(settings));
|
|
467
|
+
return { detected: evidence.length > 0, evidence };
|
|
468
|
+
},
|
|
469
|
+
plan({ home, launch }) {
|
|
470
|
+
const filePath = rooSettingsPath(home);
|
|
471
|
+
const entry = jsonServerEntry(launch);
|
|
472
|
+
return {
|
|
473
|
+
kind: 'json-mcp',
|
|
474
|
+
filePath,
|
|
475
|
+
entry,
|
|
476
|
+
summary: `merge mcpServers.astrolabe into ${filePath}`,
|
|
477
|
+
apply() {
|
|
478
|
+
const create = exists(path.dirname(filePath)) || exists(filePath);
|
|
479
|
+
const result = mergeJsonMcpFile(filePath, entry, { create });
|
|
480
|
+
if (result.skipped) return result;
|
|
481
|
+
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: 'continue',
|
|
488
|
+
label: 'Continue',
|
|
489
|
+
context: null,
|
|
490
|
+
detect(home) {
|
|
491
|
+
const evidence = [];
|
|
492
|
+
if (exists(path.join(home, '.continue'))) evidence.push('~/.continue/');
|
|
493
|
+
return { detected: evidence.length > 0, evidence };
|
|
494
|
+
},
|
|
495
|
+
plan({ home, launch }) {
|
|
496
|
+
// Continue's primary config is YAML; MCP servers for the IDE extension
|
|
497
|
+
// commonly live in ~/.continue/mcpServers/astrolabe.yaml or mcp.json.
|
|
498
|
+
// We write the documented JSON mcpServers shape to ~/.continue/mcp.json
|
|
499
|
+
// (merge-only) which matches INSTALL_GUIDE examples.
|
|
500
|
+
const filePath = path.join(home, '.continue', 'mcp.json');
|
|
501
|
+
const entry = jsonServerEntry(launch);
|
|
502
|
+
return {
|
|
503
|
+
kind: 'json-mcp',
|
|
504
|
+
filePath,
|
|
505
|
+
entry,
|
|
506
|
+
summary: `merge mcpServers.astrolabe into ${filePath}`,
|
|
507
|
+
apply() {
|
|
508
|
+
const create = exists(path.join(home, '.continue')) || exists(filePath);
|
|
509
|
+
const result = mergeJsonMcpFile(filePath, entry, { create });
|
|
510
|
+
if (result.skipped) return result;
|
|
511
|
+
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: 'opencode',
|
|
518
|
+
label: 'OpenCode',
|
|
519
|
+
aliases: [],
|
|
520
|
+
context: null,
|
|
521
|
+
detect(home, env = process.env) {
|
|
522
|
+
return opencodeMod.detectOpenCode(home, env, { whichFn: which, existsFn: exists });
|
|
523
|
+
},
|
|
524
|
+
plan({ home, launch, withHooks = false }) {
|
|
525
|
+
const filePath = opencodeMod.resolveOpenCodeConfigPath(home, exists);
|
|
526
|
+
const steps = [];
|
|
527
|
+
const mcpPlan = {
|
|
528
|
+
kind: 'opencode-mcp',
|
|
529
|
+
filePath,
|
|
530
|
+
summary: `merge mcp.astrolabe into ${filePath} (OpenCode V1/V2/mcpServers auto-detect)`,
|
|
531
|
+
apply() {
|
|
532
|
+
const result = opencodeMod.mergeOpenCodeMcpFile(filePath, launch, { create: true, existsFn: exists });
|
|
533
|
+
if (result.skipped) return result;
|
|
534
|
+
return {
|
|
535
|
+
...result,
|
|
536
|
+
write: () => writeTextAtomic(filePath, result.text),
|
|
537
|
+
};
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
steps.push(mcpPlan);
|
|
541
|
+
if (withHooks) {
|
|
542
|
+
steps.push(opencodeMod.planOpenCodeGuardPlugin(home, exists));
|
|
543
|
+
}
|
|
544
|
+
// Primary plan surface remains the MCP file; hooks are optional extras applied by run.js
|
|
545
|
+
return {
|
|
546
|
+
kind: 'opencode',
|
|
547
|
+
filePath,
|
|
548
|
+
summary: mcpPlan.summary + (withHooks ? '; optional guard plugin' : ''),
|
|
549
|
+
steps,
|
|
550
|
+
apply() {
|
|
551
|
+
const results = [];
|
|
552
|
+
for (const step of steps) {
|
|
553
|
+
const applied = step.apply();
|
|
554
|
+
if (applied.skipped) {
|
|
555
|
+
results.push(applied);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (!applied.write) {
|
|
559
|
+
const fp = applied.filePath;
|
|
560
|
+
const contents = applied.text;
|
|
561
|
+
applied.write = () => writeTextAtomic(fp, contents);
|
|
562
|
+
}
|
|
563
|
+
results.push(applied);
|
|
564
|
+
}
|
|
565
|
+
const primary = results.find((r) => !r.skipped) || results[0];
|
|
566
|
+
return {
|
|
567
|
+
...primary,
|
|
568
|
+
results,
|
|
569
|
+
write: () => {
|
|
570
|
+
for (const r of results) {
|
|
571
|
+
if (!r.skipped && r.write) r.write();
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: 'deepseek-harness',
|
|
581
|
+
label: 'DeepSeek Harness (DSH)',
|
|
582
|
+
aliases: ['dsh'],
|
|
583
|
+
context: null,
|
|
584
|
+
detect(home, env = process.env) {
|
|
585
|
+
return dshMod.detectDsh(home, env, { whichFn: which, existsFn: exists });
|
|
586
|
+
},
|
|
587
|
+
plan({ home }) {
|
|
588
|
+
const patchPath = path.join(home, '.dsh', 'cordis.patch.yml');
|
|
589
|
+
const source = dshMod.resolveDshPluginSource(exists);
|
|
590
|
+
const installDirs = dshMod.resolveDshPluginInstallDirs(home, exists);
|
|
591
|
+
return {
|
|
592
|
+
kind: 'dsh-guard',
|
|
593
|
+
filePath: patchPath,
|
|
594
|
+
summary:
|
|
595
|
+
`install astrolabe-dsh-guard into DSH profile node_modules and merge ${patchPath}` +
|
|
596
|
+
' (MCP servers are per-repo in the DSH UI — setup does not write them)',
|
|
597
|
+
apply() {
|
|
598
|
+
if (!source) {
|
|
599
|
+
return {
|
|
600
|
+
skipped: true,
|
|
601
|
+
reason:
|
|
602
|
+
'dsh-plugin sources not found (expected vendor/astrolabe-dsh-guard next to setup or repo dsh-plugin/)',
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const patch = dshMod.mergeDshCordisPatch(patchPath, {
|
|
606
|
+
create: exists(path.join(home, '.dsh')) || exists(patchPath),
|
|
607
|
+
existsFn: exists,
|
|
608
|
+
});
|
|
609
|
+
if (patch.skipped) return patch;
|
|
610
|
+
return {
|
|
611
|
+
filePath: patchPath,
|
|
612
|
+
created: patch.created,
|
|
613
|
+
replaced: patch.replaced,
|
|
614
|
+
text: patch.text,
|
|
615
|
+
source,
|
|
616
|
+
installDirs,
|
|
617
|
+
entryPreview: dshMod.MANAGED_BLOCK.trim(),
|
|
618
|
+
write: () => {
|
|
619
|
+
for (const dest of installDirs) {
|
|
620
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
621
|
+
dshMod.copyDirRecursive(source, dest);
|
|
622
|
+
}
|
|
623
|
+
writeTextAtomic(patchPath, patch.text);
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: 'codebuddy',
|
|
632
|
+
label: 'Codebuddy',
|
|
633
|
+
aliases: [],
|
|
634
|
+
context: null,
|
|
635
|
+
detect(home, env = process.env) {
|
|
636
|
+
return codebuddyMod.detectCodebuddy(home, env, { whichFn: which, existsFn: exists });
|
|
637
|
+
},
|
|
638
|
+
plan({ home, launch, withHooks = false }) {
|
|
639
|
+
const mcpPath = codebuddyMod.resolveCodebuddyMcpPath(home, exists);
|
|
640
|
+
const settingsPath = path.join(home, '.codebuddy', 'settings.json');
|
|
641
|
+
const entry = jsonServerEntry(launch, { typed: true });
|
|
642
|
+
const steps = [];
|
|
643
|
+
steps.push({
|
|
644
|
+
kind: 'json-mcp',
|
|
645
|
+
filePath: mcpPath,
|
|
646
|
+
summary: `merge mcpServers.astrolabe into ${mcpPath}`,
|
|
647
|
+
apply() {
|
|
648
|
+
const create =
|
|
649
|
+
exists(path.join(home, '.codebuddy')) ||
|
|
650
|
+
exists(mcpPath) ||
|
|
651
|
+
exists(path.join(home, '.codebuddy.json'));
|
|
652
|
+
const result = mergeJsonMcpFile(mcpPath, entry, { create });
|
|
653
|
+
if (result.skipped) return result;
|
|
654
|
+
return { ...result, write: () => writeJsonAtomic(mcpPath, result.doc) };
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
if (withHooks) {
|
|
658
|
+
steps.push({
|
|
659
|
+
kind: 'codebuddy-hooks',
|
|
660
|
+
filePath: settingsPath,
|
|
661
|
+
summary: `merge PreToolUse/SessionEnd into ${settingsPath}`,
|
|
662
|
+
apply() {
|
|
663
|
+
const create = exists(path.join(home, '.codebuddy')) || exists(settingsPath);
|
|
664
|
+
const result = codebuddyMod.mergeCodebuddyHooksSettings(settingsPath, {
|
|
665
|
+
create,
|
|
666
|
+
existsFn: exists,
|
|
667
|
+
});
|
|
668
|
+
if (result.skipped) return result;
|
|
669
|
+
return { ...result, write: () => writeJsonAtomic(settingsPath, result.doc) };
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
kind: 'codebuddy',
|
|
675
|
+
filePath: mcpPath,
|
|
676
|
+
summary:
|
|
677
|
+
steps[0].summary + (withHooks ? `; hooks → ${settingsPath}` : ''),
|
|
678
|
+
steps,
|
|
679
|
+
apply() {
|
|
680
|
+
const results = [];
|
|
681
|
+
for (const step of steps) {
|
|
682
|
+
const applied = step.apply();
|
|
683
|
+
if (!applied.skipped && !applied.write && applied.doc) {
|
|
684
|
+
const fp = applied.filePath;
|
|
685
|
+
applied.write = () => writeJsonAtomic(fp, applied.doc);
|
|
686
|
+
}
|
|
687
|
+
results.push(applied);
|
|
688
|
+
}
|
|
689
|
+
const primary = results.find((r) => !r.skipped) || results[0];
|
|
690
|
+
return {
|
|
691
|
+
...primary,
|
|
692
|
+
results,
|
|
693
|
+
write: () => {
|
|
694
|
+
for (const r of results) {
|
|
695
|
+
if (!r.skipped && r.write) r.write();
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: 'grok',
|
|
705
|
+
label: 'Grok Build',
|
|
706
|
+
aliases: ['grokbuild', 'grok-build'],
|
|
707
|
+
context: null,
|
|
708
|
+
detect(home, env = process.env) {
|
|
709
|
+
return grokMod.detectGrok(home, env, { whichFn: which, existsFn: exists });
|
|
710
|
+
},
|
|
711
|
+
plan({ home, launch, withHooks = false }) {
|
|
712
|
+
const tomlPath = grokMod.resolveGrokConfigToml(home);
|
|
713
|
+
const hooksPath = grokMod.resolveGrokHooksPath(home);
|
|
714
|
+
const steps = [];
|
|
715
|
+
steps.push({
|
|
716
|
+
kind: 'grok-toml',
|
|
717
|
+
filePath: tomlPath,
|
|
718
|
+
summary: `merge [mcp_servers.astrolabe] into ${tomlPath}`,
|
|
719
|
+
apply() {
|
|
720
|
+
const create = exists(path.join(home, '.grok')) || exists(tomlPath);
|
|
721
|
+
if (!create) {
|
|
722
|
+
return { skipped: true, reason: `missing ${tomlPath} (will not create)` };
|
|
723
|
+
}
|
|
724
|
+
const result = mergeCodexToml(tomlPath, launch);
|
|
725
|
+
return { ...result, write: () => writeTextAtomic(tomlPath, result.text) };
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
if (withHooks) {
|
|
729
|
+
steps.push({
|
|
730
|
+
kind: 'grok-hooks',
|
|
731
|
+
filePath: hooksPath,
|
|
732
|
+
summary: `write ${hooksPath}`,
|
|
733
|
+
apply() {
|
|
734
|
+
const create = exists(path.join(home, '.grok')) || exists(hooksPath);
|
|
735
|
+
const result = grokMod.mergeGrokHooksFile(hooksPath, { create, existsFn: exists });
|
|
736
|
+
if (result.skipped) return result;
|
|
737
|
+
return { ...result, write: () => writeJsonAtomic(hooksPath, result.doc) };
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
kind: 'grok',
|
|
743
|
+
filePath: tomlPath,
|
|
744
|
+
summary: steps[0].summary + (withHooks ? `; hooks → ${hooksPath}` : ''),
|
|
745
|
+
steps,
|
|
746
|
+
apply() {
|
|
747
|
+
const results = [];
|
|
748
|
+
for (const step of steps) {
|
|
749
|
+
const applied = step.apply();
|
|
750
|
+
results.push(applied);
|
|
751
|
+
}
|
|
752
|
+
const primary = results.find((r) => !r.skipped) || results[0];
|
|
753
|
+
return {
|
|
754
|
+
...primary,
|
|
755
|
+
results,
|
|
756
|
+
write: () => {
|
|
757
|
+
for (const r of results) {
|
|
758
|
+
if (!r.skipped && r.write) r.write();
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
id: 'kimicode',
|
|
768
|
+
label: 'Kimi Code',
|
|
769
|
+
aliases: ['kimi-code', 'kimi_code', 'kimi'],
|
|
770
|
+
context: null,
|
|
771
|
+
detect(home, env = process.env) {
|
|
772
|
+
return kimicodeMod.detectKimiCode(home, env, { whichFn: which, existsFn: exists });
|
|
773
|
+
},
|
|
774
|
+
plan({ home, launch, withHooks = false }) {
|
|
775
|
+
const mcpPath = kimicodeMod.resolveKimiMcpPath(home);
|
|
776
|
+
const tomlPath = kimicodeMod.resolveKimiConfigToml(home);
|
|
777
|
+
const entry = jsonServerEntry(launch);
|
|
778
|
+
const steps = [];
|
|
779
|
+
steps.push({
|
|
780
|
+
kind: 'json-mcp',
|
|
781
|
+
filePath: mcpPath,
|
|
782
|
+
summary: `merge mcpServers.astrolabe into ${mcpPath}`,
|
|
783
|
+
apply() {
|
|
784
|
+
const create = exists(path.join(home, '.kimi-code')) || exists(mcpPath);
|
|
785
|
+
const result = mergeJsonMcpFile(mcpPath, entry, { create });
|
|
786
|
+
if (result.skipped) return result;
|
|
787
|
+
return { ...result, write: () => writeJsonAtomic(mcpPath, result.doc) };
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
if (withHooks) {
|
|
791
|
+
steps.push({
|
|
792
|
+
kind: 'kimi-hooks',
|
|
793
|
+
filePath: tomlPath,
|
|
794
|
+
summary: `merge [[hooks]] into ${tomlPath}`,
|
|
795
|
+
apply() {
|
|
796
|
+
const create = exists(path.join(home, '.kimi-code')) || exists(tomlPath);
|
|
797
|
+
const result = kimicodeMod.mergeKimiHooksToml(tomlPath, { create, existsFn: exists });
|
|
798
|
+
if (result.skipped) return result;
|
|
799
|
+
return { ...result, write: () => writeTextAtomic(tomlPath, result.text) };
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
kind: 'kimicode',
|
|
805
|
+
filePath: mcpPath,
|
|
806
|
+
summary: steps[0].summary + (withHooks ? `; hooks → ${tomlPath}` : ''),
|
|
807
|
+
steps,
|
|
808
|
+
apply() {
|
|
809
|
+
const results = [];
|
|
810
|
+
for (const step of steps) {
|
|
811
|
+
results.push(step.apply());
|
|
812
|
+
}
|
|
813
|
+
const primary = results.find((r) => !r.skipped) || results[0];
|
|
814
|
+
return {
|
|
815
|
+
...primary,
|
|
816
|
+
results,
|
|
817
|
+
write: () => {
|
|
818
|
+
for (const r of results) {
|
|
819
|
+
if (!r.skipped && r.write) r.write();
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
id: 'zcode',
|
|
829
|
+
label: 'ZCode',
|
|
830
|
+
aliases: ['z-code', 'zai'],
|
|
831
|
+
context: null,
|
|
832
|
+
detect(home, env = process.env) {
|
|
833
|
+
return zcodeMod.detectZCode(home, env, { whichFn: which, existsFn: exists });
|
|
834
|
+
},
|
|
835
|
+
plan({ home, launch, withHooks = false }) {
|
|
836
|
+
const filePath = zcodeMod.resolveZcodeConfigPath(home, exists);
|
|
837
|
+
return {
|
|
838
|
+
kind: 'zcode',
|
|
839
|
+
filePath,
|
|
840
|
+
summary:
|
|
841
|
+
`merge mcp.servers.astrolabe into ${filePath}` +
|
|
842
|
+
(withHooks ? ' (+ hooks.events.PreToolUse, no SessionEnd)' : ''),
|
|
843
|
+
apply() {
|
|
844
|
+
const create =
|
|
845
|
+
exists(path.join(home, '.zcode')) ||
|
|
846
|
+
exists(path.join(home, '.zcode', 'cli')) ||
|
|
847
|
+
exists(filePath);
|
|
848
|
+
const result = zcodeMod.mergeZcodeConfigFile(filePath, launch, {
|
|
849
|
+
create,
|
|
850
|
+
withHooks,
|
|
851
|
+
existsFn: exists,
|
|
852
|
+
});
|
|
853
|
+
if (result.skipped) return result;
|
|
854
|
+
return { ...result, write: () => writeJsonAtomic(filePath, result.doc) };
|
|
855
|
+
},
|
|
856
|
+
};
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
];
|
|
861
|
+
|
|
862
|
+
function getClient(id) {
|
|
863
|
+
return (
|
|
864
|
+
CLIENTS.find(
|
|
865
|
+
(c) => c && (c.id === id || (Array.isArray(c.aliases) && c.aliases.includes(id))),
|
|
866
|
+
) || null
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function canonicalClientId(id) {
|
|
871
|
+
const c = getClient(id);
|
|
872
|
+
return c ? c.id : id;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
module.exports = {
|
|
876
|
+
CLIENTS,
|
|
877
|
+
CATALOG_TOOL_NAMES,
|
|
878
|
+
PACKAGE_NAME,
|
|
879
|
+
SERVER_NAME,
|
|
880
|
+
which,
|
|
881
|
+
resolveLaunch,
|
|
882
|
+
jsonServerEntry,
|
|
883
|
+
mergeJsonMcpFile,
|
|
884
|
+
mergeCodexToml,
|
|
885
|
+
mergeCursorPermissions,
|
|
886
|
+
backupFile,
|
|
887
|
+
writeJsonAtomic,
|
|
888
|
+
writeTextAtomic,
|
|
889
|
+
getClient,
|
|
890
|
+
canonicalClientId,
|
|
891
|
+
appSupport,
|
|
892
|
+
// OpenCode / DSH helpers (tests)
|
|
893
|
+
mergeOpenCodeMcpFile: opencodeMod.mergeOpenCodeMcpFile,
|
|
894
|
+
detectOpenCodeMcpFormat: opencodeMod.detectOpenCodeMcpFormat,
|
|
895
|
+
resolveOpenCodeConfigPath: opencodeMod.resolveOpenCodeConfigPath,
|
|
896
|
+
mergeDshCordisPatch: dshMod.mergeDshCordisPatch,
|
|
897
|
+
resolveDshPluginSource: dshMod.resolveDshPluginSource,
|
|
898
|
+
resolveDshPluginInstallDirs: dshMod.resolveDshPluginInstallDirs,
|
|
899
|
+
OPENCODE_GUARD_TS: opencodeMod.OPENCODE_GUARD_TS,
|
|
900
|
+
parseJsonc: require('./jsonc').parseJsonc,
|
|
901
|
+
// Codebuddy / Grok / Kimi / ZCode helpers (tests)
|
|
902
|
+
resolveCodebuddyMcpPath: codebuddyMod.resolveCodebuddyMcpPath,
|
|
903
|
+
mergeCodebuddyHooksSettings: codebuddyMod.mergeCodebuddyHooksSettings,
|
|
904
|
+
resolveGrokConfigToml: grokMod.resolveGrokConfigToml,
|
|
905
|
+
resolveGrokHooksPath: grokMod.resolveGrokHooksPath,
|
|
906
|
+
mergeGrokHooksFile: grokMod.mergeGrokHooksFile,
|
|
907
|
+
resolveKimiMcpPath: kimicodeMod.resolveKimiMcpPath,
|
|
908
|
+
resolveKimiConfigToml: kimicodeMod.resolveKimiConfigToml,
|
|
909
|
+
mergeKimiHooksToml: kimicodeMod.mergeKimiHooksToml,
|
|
910
|
+
resolveZcodeConfigPath: zcodeMod.resolveZcodeConfigPath,
|
|
911
|
+
mergeZcodeConfigFile: zcodeMod.mergeZcodeConfigFile,
|
|
912
|
+
};
|