draftgo-cli 4.0.23 → 4.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/bin/draftgo.js +8 -8
  3. package/package.json +72 -72
  4. package/resources/custom-service-sdk/auth_test.go +1 -1
  5. package/resources/custom-service-sdk/manifest.json +11 -11
  6. package/resources/custom-service-sdk/platform.go +10 -3
  7. package/resources/custom-service-sdk/resources.go +1 -0
  8. package/resources/custom-service-sdk/resources_scope_test.go +10 -5
  9. package/resources/custom-service-sdk/sdk.go +4 -3
  10. package/resources/skill/SKILL.md +1 -1
  11. package/resources/skill/manifest.json +1 -1
  12. package/resources/skill/references/aihub.md +74 -74
  13. package/resources/skill/references/app-api.md +78 -78
  14. package/resources/skill/references/architecture.md +40 -40
  15. package/resources/skill/references/checkout.md +105 -105
  16. package/resources/skill/references/custom-services.md +4 -4
  17. package/resources/skill/references/data.md +168 -168
  18. package/resources/skill/references/methods.md +3 -0
  19. package/resources/skill/references/modules.md +47 -47
  20. package/resources/skill/references/runtime.md +95 -96
  21. package/resources/skill/story/SKILL.md +264 -264
  22. package/src/commands/help.js +72 -72
  23. package/src/commands/listTargets.js +12 -12
  24. package/src/commands/status.js +2 -2
  25. package/src/commands/uninstall.js +45 -45
  26. package/src/commands/update.js +20 -20
  27. package/src/customServices.js +5 -4
  28. package/src/detect.js +14 -14
  29. package/src/fsx.js +67 -67
  30. package/src/index.js +25 -25
  31. package/src/localRuntime/detect.js +76 -76
  32. package/src/localRuntime/mysqlClient.js +138 -138
  33. package/src/logger.js +37 -37
  34. package/src/mcp/client.js +586 -595
  35. package/src/mcp/hosts.js +520 -520
  36. package/src/mcp/protocol.js +167 -167
  37. package/src/prompt.js +94 -94
  38. package/src/updateCheck.js +16 -16
package/src/mcp/hosts.js CHANGED
@@ -1,520 +1,520 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { exists, ensureDir } = require('../fsx');
6
-
7
- const JSON_ENTRY = Object.freeze({
8
- command: 'draftgo',
9
- args: ['mcp', 'serve'],
10
- });
11
-
12
- const VSCODE_ENTRY = Object.freeze({
13
- type: 'stdio',
14
- command: 'draftgo',
15
- args: ['mcp', 'serve'],
16
- });
17
-
18
- const HOSTS = Object.freeze([
19
- { name: 'codex', displayName: 'Codex CLI', path: '.codex/config.toml', format: 'toml', signals: ['.codex', 'AGENTS.md'] },
20
- { name: 'claudecode', aliases: ['claude'], displayName: 'Claude Code', path: '.mcp.json', format: 'jsonc', registry: 'mcpServers', signals: ['.claude', 'CLAUDE.md', '.mcp.json'] },
21
- { name: 'cursor', displayName: 'Cursor', path: '.cursor/mcp.json', format: 'jsonc', registry: 'mcpServers', signals: ['.cursor'] },
22
- { name: 'gemini', displayName: 'Gemini CLI', path: '.gemini/settings.json', format: 'jsonc', registry: 'mcpServers', signals: ['.gemini', 'GEMINI.md'] },
23
- { name: 'kiro', displayName: 'Kiro', path: '.kiro/settings/mcp.json', format: 'jsonc', registry: 'mcpServers', signals: ['.kiro'] },
24
- { name: 'copilot', aliases: ['github-copilot'], displayName: 'GitHub Copilot', path: '.vscode/mcp.json', format: 'jsonc', registry: 'servers', vscode: true, signals: ['.vscode', '.github/prompts', '.github/copilot-instructions.md'] },
25
- { name: 'windsurf', displayName: 'Windsurf', supported: false, reason: 'Windsurf has no supported project-level MCP configuration adapter.', signals: ['.windsurf'] },
26
- { name: 'antigravity', displayName: 'Antigravity', supported: false, reason: 'Antigravity has no supported project-level MCP configuration adapter.', signals: ['.agent'] },
27
- ]);
28
-
29
- const byName = new Map();
30
- for (const host of HOSTS) {
31
- byName.set(host.name, host);
32
- for (const alias of host.aliases || []) byName.set(alias, host);
33
- }
34
-
35
- function stripJsonc(source) {
36
- const chars = [...source];
37
- let inString = false;
38
- let escaped = false;
39
- for (let i = 0; i < chars.length; i += 1) {
40
- const char = chars[i];
41
- const next = chars[i + 1];
42
- if (inString) {
43
- if (escaped) escaped = false;
44
- else if (char === '\\') escaped = true;
45
- else if (char === '"') inString = false;
46
- continue;
47
- }
48
- if (char === '"') {
49
- inString = true;
50
- continue;
51
- }
52
- if (char === '/' && next === '/') {
53
- chars[i] = ' ';
54
- chars[i + 1] = ' ';
55
- i += 2;
56
- while (i < chars.length && chars[i] !== '\n' && chars[i] !== '\r') {
57
- chars[i] = ' ';
58
- i += 1;
59
- }
60
- i -= 1;
61
- continue;
62
- }
63
- if (char === '/' && next === '*') {
64
- chars[i] = ' ';
65
- chars[i + 1] = ' ';
66
- i += 2;
67
- let closed = false;
68
- while (i < chars.length) {
69
- if (chars[i] === '*' && chars[i + 1] === '/') {
70
- chars[i] = ' ';
71
- chars[i + 1] = ' ';
72
- i += 1;
73
- closed = true;
74
- break;
75
- }
76
- if (chars[i] !== '\n' && chars[i] !== '\r') chars[i] = ' ';
77
- i += 1;
78
- }
79
- if (!closed) throw new Error('Invalid JSONC: unterminated block comment.');
80
- }
81
- }
82
- if (inString) throw new Error('Invalid JSONC: unterminated string.');
83
-
84
- for (let i = 0; i < chars.length; i += 1) {
85
- if (chars[i] !== ',') continue;
86
- let next = i + 1;
87
- while (next < chars.length && /\s/.test(chars[next])) next += 1;
88
- if (chars[next] === '}' || chars[next] === ']') chars[i] = ' ';
89
- }
90
- return chars.join('').replace(/^\ufeff/, '');
91
- }
92
-
93
- function parseJsonc(source, file = 'MCP config') {
94
- const text = source.trim() ? source : '{}';
95
- let value;
96
- try {
97
- value = JSON.parse(stripJsonc(text));
98
- } catch (error) {
99
- throw new Error(`Invalid ${file}: ${error.message}`);
100
- }
101
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
102
- throw new Error(`Invalid ${file}: root must be an object.`);
103
- }
104
- return value;
105
- }
106
-
107
- function skipTrivia(source, offset, limit = source.length) {
108
- let index = offset;
109
- while (index < limit) {
110
- if (/\s/.test(source[index])) {
111
- index += 1;
112
- continue;
113
- }
114
- if (source[index] === '/' && source[index + 1] === '/') {
115
- index += 2;
116
- while (index < limit && source[index] !== '\n' && source[index] !== '\r') index += 1;
117
- continue;
118
- }
119
- if (source[index] === '/' && source[index + 1] === '*') {
120
- const end = source.indexOf('*/', index + 2);
121
- if (end < 0 || end >= limit) throw new Error('Invalid JSONC block comment.');
122
- index = end + 2;
123
- continue;
124
- }
125
- break;
126
- }
127
- return index;
128
- }
129
-
130
- function scanString(source, offset) {
131
- if (source[offset] !== '"') throw new Error('Expected a JSON string.');
132
- let escaped = false;
133
- for (let index = offset + 1; index < source.length; index += 1) {
134
- const char = source[index];
135
- if (escaped) escaped = false;
136
- else if (char === '\\') escaped = true;
137
- else if (char === '"') return index + 1;
138
- }
139
- throw new Error('Unterminated JSON string.');
140
- }
141
-
142
- function scanComposite(source, offset) {
143
- const stack = [source[offset] === '{' ? '}' : ']'];
144
- let index = offset + 1;
145
- while (index < source.length && stack.length) {
146
- const char = source[index];
147
- if (char === '"') {
148
- index = scanString(source, index);
149
- continue;
150
- }
151
- if (char === '/' && source[index + 1] === '/') {
152
- index = skipTrivia(source, index);
153
- continue;
154
- }
155
- if (char === '/' && source[index + 1] === '*') {
156
- index = skipTrivia(source, index);
157
- continue;
158
- }
159
- if (char === '{') stack.push('}');
160
- else if (char === '[') stack.push(']');
161
- else if (char === stack[stack.length - 1]) stack.pop();
162
- index += 1;
163
- }
164
- if (stack.length) throw new Error('Unterminated JSON object or array.');
165
- return index;
166
- }
167
-
168
- function scanValue(source, offset, limit = source.length) {
169
- const char = source[offset];
170
- if (char === '"') return scanString(source, offset);
171
- if (char === '{' || char === '[') return scanComposite(source, offset);
172
- let index = offset;
173
- while (index < limit && source[index] !== ',' && source[index] !== '}' && source[index] !== ']') {
174
- if (source[index] === '/' && (source[index + 1] === '/' || source[index + 1] === '*')) break;
175
- index += 1;
176
- }
177
- while (index > offset && /\s/.test(source[index - 1])) index -= 1;
178
- return index;
179
- }
180
-
181
- function objectProperties(source, open, close) {
182
- const properties = [];
183
- let index = open + 1;
184
- while (index < close) {
185
- index = skipTrivia(source, index, close);
186
- if (source[index] === ',') {
187
- index += 1;
188
- continue;
189
- }
190
- if (index >= close) break;
191
- const propertyStart = index;
192
- const keyEnd = scanString(source, index);
193
- const key = JSON.parse(source.slice(index, keyEnd));
194
- index = skipTrivia(source, keyEnd, close);
195
- if (source[index] !== ':') throw new Error('Invalid JSONC object property.');
196
- const valueStart = skipTrivia(source, index + 1, close);
197
- const valueEnd = scanValue(source, valueStart, close);
198
- index = skipTrivia(source, valueEnd, close);
199
- const comma = source[index] === ',' ? index : null;
200
- properties.push({ key, propertyStart, valueStart, valueEnd, comma });
201
- if (comma != null) index = comma + 1;
202
- else if (index < close) throw new Error('Invalid JSONC object separator.');
203
- }
204
- return properties;
205
- }
206
-
207
- function lineIndent(source, offset) {
208
- const start = source.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
209
- const prefix = source.slice(start, offset);
210
- return /^\s*$/.test(prefix) ? prefix.replace(/\r/g, '') : '';
211
- }
212
-
213
- function formatJsonValue(value, propertyIndent, newline) {
214
- return JSON.stringify(value, null, 2).replace(/\n/g, `${newline}${propertyIndent}`);
215
- }
216
-
217
- function replaceRange(source, start, end, replacement) {
218
- return source.slice(0, start) + replacement + source.slice(end);
219
- }
220
-
221
- function insertObjectProperty(source, open, close, key, value, newline) {
222
- const properties = objectProperties(source, open, close);
223
- const openingIndent = lineIndent(source, open);
224
- const propertyIndent = properties.length
225
- ? (lineIndent(source, properties[0].propertyStart) || `${openingIndent} `)
226
- : `${openingIndent} `;
227
- const property = `${JSON.stringify(key)}: ${formatJsonValue(value, propertyIndent, newline)}`;
228
- let updated = source;
229
- let adjustedClose = close;
230
-
231
- if (properties.length) {
232
- const last = properties[properties.length - 1];
233
- if (last.comma == null) {
234
- updated = replaceRange(updated, last.valueEnd, last.valueEnd, ',');
235
- adjustedClose += 1;
236
- }
237
- }
238
-
239
- const closeLineStart = updated.lastIndexOf('\n', Math.max(0, adjustedClose - 1)) + 1;
240
- const closePrefix = updated.slice(closeLineStart, adjustedClose);
241
- if (closeLineStart > open && /^\s*$/.test(closePrefix)) {
242
- return replaceRange(updated, closeLineStart, closeLineStart, `${propertyIndent}${property}${newline}`);
243
- }
244
- return replaceRange(
245
- updated,
246
- adjustedClose,
247
- adjustedClose,
248
- `${newline}${propertyIndent}${property}${newline}${openingIndent}`,
249
- );
250
- }
251
-
252
- function mergeJsoncConfig(source, registry, entry, file = 'MCP config') {
253
- const initial = source.trim() ? source : '{\n}\n';
254
- const parsed = parseJsonc(initial, file);
255
- if (parsed[registry] !== undefined
256
- && (!parsed[registry] || typeof parsed[registry] !== 'object' || Array.isArray(parsed[registry]))) {
257
- throw new Error(`Invalid ${file}: ${registry} must be an object.`);
258
- }
259
-
260
- const newline = initial.includes('\r\n') ? '\r\n' : '\n';
261
- const rootOffset = initial.charCodeAt(0) === 0xfeff ? 1 : 0;
262
- const rootOpen = skipTrivia(initial, rootOffset);
263
- const rootEnd = scanComposite(initial, rootOpen);
264
- const rootClose = rootEnd - 1;
265
- const rootProperties = objectProperties(initial, rootOpen, rootClose);
266
- const registryProperty = rootProperties.find((item) => item.key === registry);
267
- let result;
268
-
269
- if (!registryProperty) {
270
- result = insertObjectProperty(initial, rootOpen, rootClose, registry, { draftgo: entry }, newline);
271
- } else {
272
- const registryOpen = registryProperty.valueStart;
273
- const registryClose = scanComposite(initial, registryOpen) - 1;
274
- const serverProperties = objectProperties(initial, registryOpen, registryClose);
275
- const draftgo = serverProperties.find((item) => item.key === 'draftgo');
276
- if (draftgo) {
277
- const indent = lineIndent(initial, draftgo.propertyStart);
278
- result = replaceRange(
279
- initial,
280
- draftgo.valueStart,
281
- draftgo.valueEnd,
282
- formatJsonValue(entry, indent, newline),
283
- );
284
- } else {
285
- result = insertObjectProperty(initial, registryOpen, registryClose, 'draftgo', entry, newline);
286
- }
287
- }
288
-
289
- parseJsonc(result, file);
290
- return result.endsWith('\n') || result.endsWith('\r') ? result : `${result}${newline}`;
291
- }
292
-
293
- function canonicalTomlPath(raw) {
294
- const parts = [];
295
- let current = '';
296
- let quote = null;
297
- for (const char of raw.trim()) {
298
- if (quote) {
299
- if (char === quote) quote = null;
300
- else current += char;
301
- } else if (char === '"' || char === "'") {
302
- quote = char;
303
- } else if (char === '.') {
304
- parts.push(current.trim());
305
- current = '';
306
- } else {
307
- current += char;
308
- }
309
- }
310
- parts.push(current.trim());
311
- return parts.join('.');
312
- }
313
-
314
- function tomlHeaderPath(line) {
315
- const match = line.match(/^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/);
316
- return match ? canonicalTomlPath(match[1]) : null;
317
- }
318
-
319
- function isDraftGoTomlPath(value) {
320
- return value === 'mcp_servers.draftgo' || value.startsWith('mcp_servers.draftgo.');
321
- }
322
-
323
- function assertNoConflictingToml(source, file) {
324
- for (const line of source.split(/\r?\n/)) {
325
- const code = line.replace(/#.*$/, '').trim();
326
- if (!code) continue;
327
- if (/^mcp_servers\s*=/.test(code)
328
- || /^mcp_servers\s*\.\s*(?:draftgo|"draftgo"|'draftgo')\s*=/.test(code)) {
329
- throw new Error(`Invalid ${file}: inline or dotted draftgo MCP definitions are not supported.`);
330
- }
331
- }
332
- }
333
-
334
- function mergeCodexToml(source, file = '.codex/config.toml') {
335
- assertNoConflictingToml(source, file);
336
- const newline = source.includes('\r\n') ? '\r\n' : '\n';
337
- const output = [];
338
- let skip = false;
339
- for (const line of source.split(/\r?\n/)) {
340
- const header = tomlHeaderPath(line);
341
- if (header != null) skip = isDraftGoTomlPath(header);
342
- if (!skip) output.push(line);
343
- }
344
- while (output.length && output[output.length - 1].trim() === '') output.pop();
345
- if (output.length) output.push('');
346
- output.push(
347
- '[mcp_servers.draftgo]',
348
- 'command = "draftgo"',
349
- 'args = ["mcp", "serve"]',
350
- '',
351
- );
352
- return output.join(newline);
353
- }
354
-
355
- function writeAtomic(file, content) {
356
- ensureDir(path.dirname(file));
357
- const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
358
- const temporary = `${file}.tmp-${suffix}`;
359
- const backup = `${file}.bak-${suffix}`;
360
- let handle;
361
- let backedUp = false;
362
- let installed = false;
363
- try {
364
- handle = fs.openSync(temporary, 'wx', 0o600);
365
- fs.writeFileSync(handle, content, 'utf8');
366
- fs.fsyncSync(handle);
367
- fs.closeSync(handle);
368
- handle = null;
369
- if (exists(file)) {
370
- fs.renameSync(file, backup);
371
- backedUp = true;
372
- }
373
- fs.renameSync(temporary, file);
374
- installed = true;
375
- } catch (error) {
376
- if (installed && exists(file)) fs.rmSync(file, { force: true });
377
- if (backedUp && exists(backup)) fs.renameSync(backup, file);
378
- throw error;
379
- } finally {
380
- if (handle != null) fs.closeSync(handle);
381
- if (exists(temporary)) fs.rmSync(temporary, { force: true });
382
- }
383
- if (exists(backup)) fs.rmSync(backup, { force: true });
384
- }
385
-
386
- function resolveHost(name) {
387
- return byName.get(String(name || '').toLowerCase()) || null;
388
- }
389
-
390
- function resolveHostTargets(input) {
391
- const names = (Array.isArray(input) ? input : [input])
392
- .filter(Boolean)
393
- .flatMap((item) => String(item).split(','))
394
- .map((item) => item.trim().toLowerCase())
395
- .filter(Boolean);
396
- if (names.includes('all')) return { hosts: [...HOSTS], unknown: [] };
397
- const hosts = [];
398
- const unknown = [];
399
- const seen = new Set();
400
- for (const name of names) {
401
- const host = resolveHost(name);
402
- if (!host) {
403
- unknown.push(name);
404
- } else if (!seen.has(host.name)) {
405
- seen.add(host.name);
406
- hosts.push(host);
407
- }
408
- }
409
- return { hosts, unknown };
410
- }
411
-
412
- function detectHostTargets(projectDir) {
413
- return HOSTS.filter((host) => (host.signals || []).some((signal) => exists(path.join(projectDir, signal))));
414
- }
415
-
416
- function setupHost(projectDir, target) {
417
- const host = typeof target === 'string' ? resolveHost(target) : target;
418
- if (!host) throw new Error(`Unknown MCP target: ${target}`);
419
- if (host.supported === false) {
420
- return { host, supported: false, configured: false, reason: host.reason };
421
- }
422
- const file = path.join(projectDir, host.path);
423
- const original = exists(file) ? fs.readFileSync(file, 'utf8') : '';
424
- const entry = host.vscode ? VSCODE_ENTRY : JSON_ENTRY;
425
- const content = host.format === 'toml'
426
- ? mergeCodexToml(original, host.path)
427
- : mergeJsoncConfig(original, host.registry, entry, host.path);
428
- if (content !== original) writeAtomic(file, content);
429
- return { host, supported: true, configured: true, changed: content !== original, path: file };
430
- }
431
-
432
- function setupHosts(projectDir, targets) {
433
- const selected = targets && targets.length ? resolveHostTargets(targets) : { hosts: detectHostTargets(projectDir), unknown: [] };
434
- if (selected.unknown.length) throw new Error(`Unknown MCP target: ${selected.unknown.join(', ')}`);
435
- return selected.hosts.map((host) => setupHost(projectDir, host));
436
- }
437
-
438
- function exactEntry(actual, expected) {
439
- if (!actual || typeof actual !== 'object' || Array.isArray(actual)) return false;
440
- const actualKeys = Object.keys(actual).sort();
441
- const expectedKeys = Object.keys(expected).sort();
442
- return JSON.stringify(actualKeys) === JSON.stringify(expectedKeys)
443
- && actual.command === expected.command
444
- && JSON.stringify(actual.args) === JSON.stringify(expected.args)
445
- && (expected.type === undefined || actual.type === expected.type);
446
- }
447
-
448
- function codexStatus(source) {
449
- try {
450
- assertNoConflictingToml(source, '.codex/config.toml');
451
- } catch (error) {
452
- return { configured: false, secure: false, error: error.message };
453
- }
454
- const lines = source.split(/\r?\n/);
455
- const blocks = [];
456
- let active = null;
457
- for (const line of lines) {
458
- const header = tomlHeaderPath(line);
459
- if (header != null) {
460
- active = isDraftGoTomlPath(header) ? [] : null;
461
- if (active) blocks.push(active);
462
- } else if (active) {
463
- active.push(line);
464
- }
465
- }
466
- const body = blocks.flat().join('\n');
467
- const command = /^\s*command\s*=\s*"draftgo"\s*(?:#.*)?$/m.test(body);
468
- const args = /^\s*args\s*=\s*\[\s*"mcp"\s*,\s*"serve"\s*\]\s*(?:#.*)?$/m.test(body);
469
- const forbidden = /^\s*(?:env|cwd|url|server|sat|token|headers?)\b/im.test(body);
470
- return { configured: blocks.length === 1 && command && args && !forbidden, secure: !forbidden };
471
- }
472
-
473
- function statusHost(projectDir, target) {
474
- const host = typeof target === 'string' ? resolveHost(target) : target;
475
- if (!host) throw new Error(`Unknown MCP target: ${target}`);
476
- if (host.supported === false) {
477
- return { host, supported: false, configured: false, reason: host.reason };
478
- }
479
- const file = path.join(projectDir, host.path);
480
- if (!exists(file)) return { host, supported: true, configured: false, secure: true, path: file };
481
- const source = fs.readFileSync(file, 'utf8');
482
- if (host.format === 'toml') return { host, supported: true, path: file, ...codexStatus(source) };
483
- try {
484
- const value = parseJsonc(source, host.path);
485
- const registry = value[host.registry];
486
- const entry = registry && typeof registry === 'object' && !Array.isArray(registry)
487
- ? registry.draftgo
488
- : undefined;
489
- const expected = host.vscode ? VSCODE_ENTRY : JSON_ENTRY;
490
- const configured = exactEntry(entry, expected);
491
- const text = entry === undefined ? '' : JSON.stringify(entry).toLowerCase();
492
- const secure = !/(?:token|sat|authorization|bearer|server|https?:|headers|env|cwd)/.test(text);
493
- return { host, supported: true, configured, secure, path: file };
494
- } catch (error) {
495
- return { host, supported: true, configured: false, secure: false, path: file, error: error.message };
496
- }
497
- }
498
-
499
- function statusHosts(projectDir, targets) {
500
- const selected = targets && targets.length ? resolveHostTargets(targets) : { hosts: [...HOSTS], unknown: [] };
501
- if (selected.unknown.length) throw new Error(`Unknown MCP target: ${selected.unknown.join(', ')}`);
502
- return selected.hosts.map((host) => statusHost(projectDir, host));
503
- }
504
-
505
- module.exports = {
506
- HOSTS,
507
- JSON_ENTRY,
508
- VSCODE_ENTRY,
509
- detectHostTargets,
510
- mergeCodexToml,
511
- mergeJsoncConfig,
512
- parseJsonc,
513
- resolveHost,
514
- resolveHostTargets,
515
- setupHost,
516
- setupHosts,
517
- statusHost,
518
- statusHosts,
519
- writeAtomic,
520
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { exists, ensureDir } = require('../fsx');
6
+
7
+ const JSON_ENTRY = Object.freeze({
8
+ command: 'draftgo',
9
+ args: ['mcp', 'serve'],
10
+ });
11
+
12
+ const VSCODE_ENTRY = Object.freeze({
13
+ type: 'stdio',
14
+ command: 'draftgo',
15
+ args: ['mcp', 'serve'],
16
+ });
17
+
18
+ const HOSTS = Object.freeze([
19
+ { name: 'codex', displayName: 'Codex CLI', path: '.codex/config.toml', format: 'toml', signals: ['.codex', 'AGENTS.md'] },
20
+ { name: 'claudecode', aliases: ['claude'], displayName: 'Claude Code', path: '.mcp.json', format: 'jsonc', registry: 'mcpServers', signals: ['.claude', 'CLAUDE.md', '.mcp.json'] },
21
+ { name: 'cursor', displayName: 'Cursor', path: '.cursor/mcp.json', format: 'jsonc', registry: 'mcpServers', signals: ['.cursor'] },
22
+ { name: 'gemini', displayName: 'Gemini CLI', path: '.gemini/settings.json', format: 'jsonc', registry: 'mcpServers', signals: ['.gemini', 'GEMINI.md'] },
23
+ { name: 'kiro', displayName: 'Kiro', path: '.kiro/settings/mcp.json', format: 'jsonc', registry: 'mcpServers', signals: ['.kiro'] },
24
+ { name: 'copilot', aliases: ['github-copilot'], displayName: 'GitHub Copilot', path: '.vscode/mcp.json', format: 'jsonc', registry: 'servers', vscode: true, signals: ['.vscode', '.github/prompts', '.github/copilot-instructions.md'] },
25
+ { name: 'windsurf', displayName: 'Windsurf', supported: false, reason: 'Windsurf has no supported project-level MCP configuration adapter.', signals: ['.windsurf'] },
26
+ { name: 'antigravity', displayName: 'Antigravity', supported: false, reason: 'Antigravity has no supported project-level MCP configuration adapter.', signals: ['.agent'] },
27
+ ]);
28
+
29
+ const byName = new Map();
30
+ for (const host of HOSTS) {
31
+ byName.set(host.name, host);
32
+ for (const alias of host.aliases || []) byName.set(alias, host);
33
+ }
34
+
35
+ function stripJsonc(source) {
36
+ const chars = [...source];
37
+ let inString = false;
38
+ let escaped = false;
39
+ for (let i = 0; i < chars.length; i += 1) {
40
+ const char = chars[i];
41
+ const next = chars[i + 1];
42
+ if (inString) {
43
+ if (escaped) escaped = false;
44
+ else if (char === '\\') escaped = true;
45
+ else if (char === '"') inString = false;
46
+ continue;
47
+ }
48
+ if (char === '"') {
49
+ inString = true;
50
+ continue;
51
+ }
52
+ if (char === '/' && next === '/') {
53
+ chars[i] = ' ';
54
+ chars[i + 1] = ' ';
55
+ i += 2;
56
+ while (i < chars.length && chars[i] !== '\n' && chars[i] !== '\r') {
57
+ chars[i] = ' ';
58
+ i += 1;
59
+ }
60
+ i -= 1;
61
+ continue;
62
+ }
63
+ if (char === '/' && next === '*') {
64
+ chars[i] = ' ';
65
+ chars[i + 1] = ' ';
66
+ i += 2;
67
+ let closed = false;
68
+ while (i < chars.length) {
69
+ if (chars[i] === '*' && chars[i + 1] === '/') {
70
+ chars[i] = ' ';
71
+ chars[i + 1] = ' ';
72
+ i += 1;
73
+ closed = true;
74
+ break;
75
+ }
76
+ if (chars[i] !== '\n' && chars[i] !== '\r') chars[i] = ' ';
77
+ i += 1;
78
+ }
79
+ if (!closed) throw new Error('Invalid JSONC: unterminated block comment.');
80
+ }
81
+ }
82
+ if (inString) throw new Error('Invalid JSONC: unterminated string.');
83
+
84
+ for (let i = 0; i < chars.length; i += 1) {
85
+ if (chars[i] !== ',') continue;
86
+ let next = i + 1;
87
+ while (next < chars.length && /\s/.test(chars[next])) next += 1;
88
+ if (chars[next] === '}' || chars[next] === ']') chars[i] = ' ';
89
+ }
90
+ return chars.join('').replace(/^\ufeff/, '');
91
+ }
92
+
93
+ function parseJsonc(source, file = 'MCP config') {
94
+ const text = source.trim() ? source : '{}';
95
+ let value;
96
+ try {
97
+ value = JSON.parse(stripJsonc(text));
98
+ } catch (error) {
99
+ throw new Error(`Invalid ${file}: ${error.message}`);
100
+ }
101
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
102
+ throw new Error(`Invalid ${file}: root must be an object.`);
103
+ }
104
+ return value;
105
+ }
106
+
107
+ function skipTrivia(source, offset, limit = source.length) {
108
+ let index = offset;
109
+ while (index < limit) {
110
+ if (/\s/.test(source[index])) {
111
+ index += 1;
112
+ continue;
113
+ }
114
+ if (source[index] === '/' && source[index + 1] === '/') {
115
+ index += 2;
116
+ while (index < limit && source[index] !== '\n' && source[index] !== '\r') index += 1;
117
+ continue;
118
+ }
119
+ if (source[index] === '/' && source[index + 1] === '*') {
120
+ const end = source.indexOf('*/', index + 2);
121
+ if (end < 0 || end >= limit) throw new Error('Invalid JSONC block comment.');
122
+ index = end + 2;
123
+ continue;
124
+ }
125
+ break;
126
+ }
127
+ return index;
128
+ }
129
+
130
+ function scanString(source, offset) {
131
+ if (source[offset] !== '"') throw new Error('Expected a JSON string.');
132
+ let escaped = false;
133
+ for (let index = offset + 1; index < source.length; index += 1) {
134
+ const char = source[index];
135
+ if (escaped) escaped = false;
136
+ else if (char === '\\') escaped = true;
137
+ else if (char === '"') return index + 1;
138
+ }
139
+ throw new Error('Unterminated JSON string.');
140
+ }
141
+
142
+ function scanComposite(source, offset) {
143
+ const stack = [source[offset] === '{' ? '}' : ']'];
144
+ let index = offset + 1;
145
+ while (index < source.length && stack.length) {
146
+ const char = source[index];
147
+ if (char === '"') {
148
+ index = scanString(source, index);
149
+ continue;
150
+ }
151
+ if (char === '/' && source[index + 1] === '/') {
152
+ index = skipTrivia(source, index);
153
+ continue;
154
+ }
155
+ if (char === '/' && source[index + 1] === '*') {
156
+ index = skipTrivia(source, index);
157
+ continue;
158
+ }
159
+ if (char === '{') stack.push('}');
160
+ else if (char === '[') stack.push(']');
161
+ else if (char === stack[stack.length - 1]) stack.pop();
162
+ index += 1;
163
+ }
164
+ if (stack.length) throw new Error('Unterminated JSON object or array.');
165
+ return index;
166
+ }
167
+
168
+ function scanValue(source, offset, limit = source.length) {
169
+ const char = source[offset];
170
+ if (char === '"') return scanString(source, offset);
171
+ if (char === '{' || char === '[') return scanComposite(source, offset);
172
+ let index = offset;
173
+ while (index < limit && source[index] !== ',' && source[index] !== '}' && source[index] !== ']') {
174
+ if (source[index] === '/' && (source[index + 1] === '/' || source[index + 1] === '*')) break;
175
+ index += 1;
176
+ }
177
+ while (index > offset && /\s/.test(source[index - 1])) index -= 1;
178
+ return index;
179
+ }
180
+
181
+ function objectProperties(source, open, close) {
182
+ const properties = [];
183
+ let index = open + 1;
184
+ while (index < close) {
185
+ index = skipTrivia(source, index, close);
186
+ if (source[index] === ',') {
187
+ index += 1;
188
+ continue;
189
+ }
190
+ if (index >= close) break;
191
+ const propertyStart = index;
192
+ const keyEnd = scanString(source, index);
193
+ const key = JSON.parse(source.slice(index, keyEnd));
194
+ index = skipTrivia(source, keyEnd, close);
195
+ if (source[index] !== ':') throw new Error('Invalid JSONC object property.');
196
+ const valueStart = skipTrivia(source, index + 1, close);
197
+ const valueEnd = scanValue(source, valueStart, close);
198
+ index = skipTrivia(source, valueEnd, close);
199
+ const comma = source[index] === ',' ? index : null;
200
+ properties.push({ key, propertyStart, valueStart, valueEnd, comma });
201
+ if (comma != null) index = comma + 1;
202
+ else if (index < close) throw new Error('Invalid JSONC object separator.');
203
+ }
204
+ return properties;
205
+ }
206
+
207
+ function lineIndent(source, offset) {
208
+ const start = source.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
209
+ const prefix = source.slice(start, offset);
210
+ return /^\s*$/.test(prefix) ? prefix.replace(/\r/g, '') : '';
211
+ }
212
+
213
+ function formatJsonValue(value, propertyIndent, newline) {
214
+ return JSON.stringify(value, null, 2).replace(/\n/g, `${newline}${propertyIndent}`);
215
+ }
216
+
217
+ function replaceRange(source, start, end, replacement) {
218
+ return source.slice(0, start) + replacement + source.slice(end);
219
+ }
220
+
221
+ function insertObjectProperty(source, open, close, key, value, newline) {
222
+ const properties = objectProperties(source, open, close);
223
+ const openingIndent = lineIndent(source, open);
224
+ const propertyIndent = properties.length
225
+ ? (lineIndent(source, properties[0].propertyStart) || `${openingIndent} `)
226
+ : `${openingIndent} `;
227
+ const property = `${JSON.stringify(key)}: ${formatJsonValue(value, propertyIndent, newline)}`;
228
+ let updated = source;
229
+ let adjustedClose = close;
230
+
231
+ if (properties.length) {
232
+ const last = properties[properties.length - 1];
233
+ if (last.comma == null) {
234
+ updated = replaceRange(updated, last.valueEnd, last.valueEnd, ',');
235
+ adjustedClose += 1;
236
+ }
237
+ }
238
+
239
+ const closeLineStart = updated.lastIndexOf('\n', Math.max(0, adjustedClose - 1)) + 1;
240
+ const closePrefix = updated.slice(closeLineStart, adjustedClose);
241
+ if (closeLineStart > open && /^\s*$/.test(closePrefix)) {
242
+ return replaceRange(updated, closeLineStart, closeLineStart, `${propertyIndent}${property}${newline}`);
243
+ }
244
+ return replaceRange(
245
+ updated,
246
+ adjustedClose,
247
+ adjustedClose,
248
+ `${newline}${propertyIndent}${property}${newline}${openingIndent}`,
249
+ );
250
+ }
251
+
252
+ function mergeJsoncConfig(source, registry, entry, file = 'MCP config') {
253
+ const initial = source.trim() ? source : '{\n}\n';
254
+ const parsed = parseJsonc(initial, file);
255
+ if (parsed[registry] !== undefined
256
+ && (!parsed[registry] || typeof parsed[registry] !== 'object' || Array.isArray(parsed[registry]))) {
257
+ throw new Error(`Invalid ${file}: ${registry} must be an object.`);
258
+ }
259
+
260
+ const newline = initial.includes('\r\n') ? '\r\n' : '\n';
261
+ const rootOffset = initial.charCodeAt(0) === 0xfeff ? 1 : 0;
262
+ const rootOpen = skipTrivia(initial, rootOffset);
263
+ const rootEnd = scanComposite(initial, rootOpen);
264
+ const rootClose = rootEnd - 1;
265
+ const rootProperties = objectProperties(initial, rootOpen, rootClose);
266
+ const registryProperty = rootProperties.find((item) => item.key === registry);
267
+ let result;
268
+
269
+ if (!registryProperty) {
270
+ result = insertObjectProperty(initial, rootOpen, rootClose, registry, { draftgo: entry }, newline);
271
+ } else {
272
+ const registryOpen = registryProperty.valueStart;
273
+ const registryClose = scanComposite(initial, registryOpen) - 1;
274
+ const serverProperties = objectProperties(initial, registryOpen, registryClose);
275
+ const draftgo = serverProperties.find((item) => item.key === 'draftgo');
276
+ if (draftgo) {
277
+ const indent = lineIndent(initial, draftgo.propertyStart);
278
+ result = replaceRange(
279
+ initial,
280
+ draftgo.valueStart,
281
+ draftgo.valueEnd,
282
+ formatJsonValue(entry, indent, newline),
283
+ );
284
+ } else {
285
+ result = insertObjectProperty(initial, registryOpen, registryClose, 'draftgo', entry, newline);
286
+ }
287
+ }
288
+
289
+ parseJsonc(result, file);
290
+ return result.endsWith('\n') || result.endsWith('\r') ? result : `${result}${newline}`;
291
+ }
292
+
293
+ function canonicalTomlPath(raw) {
294
+ const parts = [];
295
+ let current = '';
296
+ let quote = null;
297
+ for (const char of raw.trim()) {
298
+ if (quote) {
299
+ if (char === quote) quote = null;
300
+ else current += char;
301
+ } else if (char === '"' || char === "'") {
302
+ quote = char;
303
+ } else if (char === '.') {
304
+ parts.push(current.trim());
305
+ current = '';
306
+ } else {
307
+ current += char;
308
+ }
309
+ }
310
+ parts.push(current.trim());
311
+ return parts.join('.');
312
+ }
313
+
314
+ function tomlHeaderPath(line) {
315
+ const match = line.match(/^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/);
316
+ return match ? canonicalTomlPath(match[1]) : null;
317
+ }
318
+
319
+ function isDraftGoTomlPath(value) {
320
+ return value === 'mcp_servers.draftgo' || value.startsWith('mcp_servers.draftgo.');
321
+ }
322
+
323
+ function assertNoConflictingToml(source, file) {
324
+ for (const line of source.split(/\r?\n/)) {
325
+ const code = line.replace(/#.*$/, '').trim();
326
+ if (!code) continue;
327
+ if (/^mcp_servers\s*=/.test(code)
328
+ || /^mcp_servers\s*\.\s*(?:draftgo|"draftgo"|'draftgo')\s*=/.test(code)) {
329
+ throw new Error(`Invalid ${file}: inline or dotted draftgo MCP definitions are not supported.`);
330
+ }
331
+ }
332
+ }
333
+
334
+ function mergeCodexToml(source, file = '.codex/config.toml') {
335
+ assertNoConflictingToml(source, file);
336
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
337
+ const output = [];
338
+ let skip = false;
339
+ for (const line of source.split(/\r?\n/)) {
340
+ const header = tomlHeaderPath(line);
341
+ if (header != null) skip = isDraftGoTomlPath(header);
342
+ if (!skip) output.push(line);
343
+ }
344
+ while (output.length && output[output.length - 1].trim() === '') output.pop();
345
+ if (output.length) output.push('');
346
+ output.push(
347
+ '[mcp_servers.draftgo]',
348
+ 'command = "draftgo"',
349
+ 'args = ["mcp", "serve"]',
350
+ '',
351
+ );
352
+ return output.join(newline);
353
+ }
354
+
355
+ function writeAtomic(file, content) {
356
+ ensureDir(path.dirname(file));
357
+ const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
358
+ const temporary = `${file}.tmp-${suffix}`;
359
+ const backup = `${file}.bak-${suffix}`;
360
+ let handle;
361
+ let backedUp = false;
362
+ let installed = false;
363
+ try {
364
+ handle = fs.openSync(temporary, 'wx', 0o600);
365
+ fs.writeFileSync(handle, content, 'utf8');
366
+ fs.fsyncSync(handle);
367
+ fs.closeSync(handle);
368
+ handle = null;
369
+ if (exists(file)) {
370
+ fs.renameSync(file, backup);
371
+ backedUp = true;
372
+ }
373
+ fs.renameSync(temporary, file);
374
+ installed = true;
375
+ } catch (error) {
376
+ if (installed && exists(file)) fs.rmSync(file, { force: true });
377
+ if (backedUp && exists(backup)) fs.renameSync(backup, file);
378
+ throw error;
379
+ } finally {
380
+ if (handle != null) fs.closeSync(handle);
381
+ if (exists(temporary)) fs.rmSync(temporary, { force: true });
382
+ }
383
+ if (exists(backup)) fs.rmSync(backup, { force: true });
384
+ }
385
+
386
+ function resolveHost(name) {
387
+ return byName.get(String(name || '').toLowerCase()) || null;
388
+ }
389
+
390
+ function resolveHostTargets(input) {
391
+ const names = (Array.isArray(input) ? input : [input])
392
+ .filter(Boolean)
393
+ .flatMap((item) => String(item).split(','))
394
+ .map((item) => item.trim().toLowerCase())
395
+ .filter(Boolean);
396
+ if (names.includes('all')) return { hosts: [...HOSTS], unknown: [] };
397
+ const hosts = [];
398
+ const unknown = [];
399
+ const seen = new Set();
400
+ for (const name of names) {
401
+ const host = resolveHost(name);
402
+ if (!host) {
403
+ unknown.push(name);
404
+ } else if (!seen.has(host.name)) {
405
+ seen.add(host.name);
406
+ hosts.push(host);
407
+ }
408
+ }
409
+ return { hosts, unknown };
410
+ }
411
+
412
+ function detectHostTargets(projectDir) {
413
+ return HOSTS.filter((host) => (host.signals || []).some((signal) => exists(path.join(projectDir, signal))));
414
+ }
415
+
416
+ function setupHost(projectDir, target) {
417
+ const host = typeof target === 'string' ? resolveHost(target) : target;
418
+ if (!host) throw new Error(`Unknown MCP target: ${target}`);
419
+ if (host.supported === false) {
420
+ return { host, supported: false, configured: false, reason: host.reason };
421
+ }
422
+ const file = path.join(projectDir, host.path);
423
+ const original = exists(file) ? fs.readFileSync(file, 'utf8') : '';
424
+ const entry = host.vscode ? VSCODE_ENTRY : JSON_ENTRY;
425
+ const content = host.format === 'toml'
426
+ ? mergeCodexToml(original, host.path)
427
+ : mergeJsoncConfig(original, host.registry, entry, host.path);
428
+ if (content !== original) writeAtomic(file, content);
429
+ return { host, supported: true, configured: true, changed: content !== original, path: file };
430
+ }
431
+
432
+ function setupHosts(projectDir, targets) {
433
+ const selected = targets && targets.length ? resolveHostTargets(targets) : { hosts: detectHostTargets(projectDir), unknown: [] };
434
+ if (selected.unknown.length) throw new Error(`Unknown MCP target: ${selected.unknown.join(', ')}`);
435
+ return selected.hosts.map((host) => setupHost(projectDir, host));
436
+ }
437
+
438
+ function exactEntry(actual, expected) {
439
+ if (!actual || typeof actual !== 'object' || Array.isArray(actual)) return false;
440
+ const actualKeys = Object.keys(actual).sort();
441
+ const expectedKeys = Object.keys(expected).sort();
442
+ return JSON.stringify(actualKeys) === JSON.stringify(expectedKeys)
443
+ && actual.command === expected.command
444
+ && JSON.stringify(actual.args) === JSON.stringify(expected.args)
445
+ && (expected.type === undefined || actual.type === expected.type);
446
+ }
447
+
448
+ function codexStatus(source) {
449
+ try {
450
+ assertNoConflictingToml(source, '.codex/config.toml');
451
+ } catch (error) {
452
+ return { configured: false, secure: false, error: error.message };
453
+ }
454
+ const lines = source.split(/\r?\n/);
455
+ const blocks = [];
456
+ let active = null;
457
+ for (const line of lines) {
458
+ const header = tomlHeaderPath(line);
459
+ if (header != null) {
460
+ active = isDraftGoTomlPath(header) ? [] : null;
461
+ if (active) blocks.push(active);
462
+ } else if (active) {
463
+ active.push(line);
464
+ }
465
+ }
466
+ const body = blocks.flat().join('\n');
467
+ const command = /^\s*command\s*=\s*"draftgo"\s*(?:#.*)?$/m.test(body);
468
+ const args = /^\s*args\s*=\s*\[\s*"mcp"\s*,\s*"serve"\s*\]\s*(?:#.*)?$/m.test(body);
469
+ const forbidden = /^\s*(?:env|cwd|url|server|sat|token|headers?)\b/im.test(body);
470
+ return { configured: blocks.length === 1 && command && args && !forbidden, secure: !forbidden };
471
+ }
472
+
473
+ function statusHost(projectDir, target) {
474
+ const host = typeof target === 'string' ? resolveHost(target) : target;
475
+ if (!host) throw new Error(`Unknown MCP target: ${target}`);
476
+ if (host.supported === false) {
477
+ return { host, supported: false, configured: false, reason: host.reason };
478
+ }
479
+ const file = path.join(projectDir, host.path);
480
+ if (!exists(file)) return { host, supported: true, configured: false, secure: true, path: file };
481
+ const source = fs.readFileSync(file, 'utf8');
482
+ if (host.format === 'toml') return { host, supported: true, path: file, ...codexStatus(source) };
483
+ try {
484
+ const value = parseJsonc(source, host.path);
485
+ const registry = value[host.registry];
486
+ const entry = registry && typeof registry === 'object' && !Array.isArray(registry)
487
+ ? registry.draftgo
488
+ : undefined;
489
+ const expected = host.vscode ? VSCODE_ENTRY : JSON_ENTRY;
490
+ const configured = exactEntry(entry, expected);
491
+ const text = entry === undefined ? '' : JSON.stringify(entry).toLowerCase();
492
+ const secure = !/(?:token|sat|authorization|bearer|server|https?:|headers|env|cwd)/.test(text);
493
+ return { host, supported: true, configured, secure, path: file };
494
+ } catch (error) {
495
+ return { host, supported: true, configured: false, secure: false, path: file, error: error.message };
496
+ }
497
+ }
498
+
499
+ function statusHosts(projectDir, targets) {
500
+ const selected = targets && targets.length ? resolveHostTargets(targets) : { hosts: [...HOSTS], unknown: [] };
501
+ if (selected.unknown.length) throw new Error(`Unknown MCP target: ${selected.unknown.join(', ')}`);
502
+ return selected.hosts.map((host) => statusHost(projectDir, host));
503
+ }
504
+
505
+ module.exports = {
506
+ HOSTS,
507
+ JSON_ENTRY,
508
+ VSCODE_ENTRY,
509
+ detectHostTargets,
510
+ mergeCodexToml,
511
+ mergeJsoncConfig,
512
+ parseJsonc,
513
+ resolveHost,
514
+ resolveHostTargets,
515
+ setupHost,
516
+ setupHosts,
517
+ statusHost,
518
+ statusHosts,
519
+ writeAtomic,
520
+ };