draftgo-cli 3.0.33 → 3.0.38

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 (64) hide show
  1. package/README.md +220 -269
  2. package/package.json +6 -2
  3. package/resources/skill/SKILL.md +114 -55
  4. package/resources/skill/init/SKILL.md +29 -15
  5. package/resources/skill/manifest.json +13 -5
  6. package/resources/skill/push/SKILL.md +41 -29
  7. package/resources/skill/references/aihub.md +8 -5
  8. package/resources/skill/references/api-endpoints.md +5 -3
  9. package/resources/skill/references/architecture.md +1 -1
  10. package/resources/skill/references/checkout.md +116 -0
  11. package/resources/skill/references/custom-services.md +9 -10
  12. package/resources/skill/references/data.md +4 -2
  13. package/resources/skill/references/frontend.md +99 -23
  14. package/resources/skill/references/mcp.md +101 -0
  15. package/resources/skill/references/modules.md +8 -8
  16. package/resources/skill/references/parallel.md +6 -3
  17. package/resources/skill/references/runtime.md +7 -10
  18. package/resources/skill/scripts/README.md +8 -0
  19. package/resources/skill/story/SKILL.md +8 -8
  20. package/src/cli.js +5 -0
  21. package/src/commandRegistry.js +7 -1
  22. package/src/commands/api.js +24 -187
  23. package/src/commands/autoPush.js +48 -17
  24. package/src/commands/check.js +17 -47
  25. package/src/commands/checkout.js +18 -0
  26. package/src/commands/commit.js +21 -0
  27. package/src/commands/conflict.js +30 -0
  28. package/src/commands/conflicts.js +16 -0
  29. package/src/commands/connect.js +60 -48
  30. package/src/commands/delete.js +79 -64
  31. package/src/commands/deploy.js +18 -10
  32. package/src/commands/diff.js +23 -0
  33. package/src/commands/help.js +99 -75
  34. package/src/commands/init.js +4 -10
  35. package/src/commands/local.js +23 -6
  36. package/src/commands/map.js +89 -89
  37. package/src/commands/mcp.js +126 -0
  38. package/src/commands/sync.js +28 -43
  39. package/src/commands/verifyUi.js +3 -2
  40. package/src/localdev/index.js +37 -7
  41. package/src/localdev/mysqlClient.js +1 -1
  42. package/src/mcp/client.js +275 -0
  43. package/src/mcp/hosts.js +520 -0
  44. package/src/mcp/protocol.js +173 -0
  45. package/src/mcp/stdio.js +300 -0
  46. package/src/mcp/tools.js +37 -0
  47. package/src/platforms.js +3 -4
  48. package/src/projectConfig.js +91 -49
  49. package/src/projectMap.js +123 -460
  50. package/src/skill.js +6 -28
  51. package/src/worktree/backend.js +250 -0
  52. package/src/worktree/errors.js +28 -0
  53. package/src/worktree/index.js +461 -0
  54. package/src/worktree/manifest.js +75 -0
  55. package/src/worktree/streams.js +200 -0
  56. package/src/worktree/types.js +103 -0
  57. package/src/worktree/validate.js +37 -0
  58. package/resources/skill/pull/SKILL.md +0 -33
  59. package/resources/skill/references/api.json +0 -20248
  60. package/resources/skill/scripts/draftgo_delete.py +0 -149
  61. package/resources/skill/scripts/draftgo_init.py +0 -80
  62. package/resources/skill/scripts/draftgo_pull.py +0 -427
  63. package/resources/skill/scripts/draftgo_push.py +0 -1022
  64. package/src/python.js +0 -27
@@ -0,0 +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
+ };
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
4
+
5
+ class McpHttpError extends Error {
6
+ constructor(message, { status = 0, rpc = null, code = null } = {}) {
7
+ super(message);
8
+ this.name = 'McpHttpError';
9
+ this.status = status;
10
+ this.rpc = rpc;
11
+ this.code = code;
12
+ }
13
+ }
14
+
15
+ function redactText(value, secrets = []) {
16
+ let text = String(value == null ? '' : value);
17
+ for (const secret of secrets.filter(Boolean)) text = text.split(String(secret)).join('[REDACTED]');
18
+ text = text.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s,;"']+/ig, '$1[REDACTED]');
19
+ text = text.replace(/(\"?(?:token|sat)\"?\s*[:=]\s*\"?)[^\s,;"'}]+/ig, '$1[REDACTED]');
20
+ return text;
21
+ }
22
+
23
+ function endpointFor(config) {
24
+ const server = new URL(String(config.server || config.mcp_url));
25
+ const endpoint = config.mcp_url
26
+ ? new URL(String(config.mcp_url), server)
27
+ : new URL(`${server.pathname.replace(/\/+$/, '')}/mcp`, server);
28
+ if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) {
29
+ throw new McpHttpError('DraftGo MCP endpoint must be a credential-free HTTP(S) URL.');
30
+ }
31
+ const allowedOrigins = new Set([
32
+ server.origin,
33
+ ...((config.mcp_allowed_origins || []).map((value) => new URL(value).origin)),
34
+ ]);
35
+ if (!allowedOrigins.has(endpoint.origin)) {
36
+ throw new McpHttpError('DraftGo MCP endpoint origin is not allowlisted; refusing to send the SAT.');
37
+ }
38
+ return endpoint.toString();
39
+ }
40
+
41
+ function jsonRpcMessages(value) {
42
+ if (Array.isArray(value)) return value.filter((item) => item && typeof item === 'object');
43
+ return value && typeof value === 'object' ? [value] : [];
44
+ }
45
+
46
+ async function parseEventStream(body, onMessage) {
47
+ if (!body) return [];
48
+ const reader = body.getReader();
49
+ const decoder = new TextDecoder();
50
+ let pending = '';
51
+ let dataLines = [];
52
+ const messages = [];
53
+ const consume = () => {
54
+ if (!dataLines.length) return;
55
+ const data = dataLines.join('\n');
56
+ dataLines = [];
57
+ if (!data || data === '[DONE]') return;
58
+ let parsed;
59
+ try { parsed = JSON.parse(data); } catch {
60
+ throw new McpHttpError('DraftGo MCP returned invalid JSON in an event stream.');
61
+ }
62
+ for (const message of jsonRpcMessages(parsed)) {
63
+ messages.push(message);
64
+ if (onMessage) onMessage(message);
65
+ }
66
+ };
67
+ while (true) {
68
+ const { done, value } = await reader.read();
69
+ pending += decoder.decode(value || new Uint8Array(), { stream: !done });
70
+ const lines = pending.split(/\r?\n/);
71
+ pending = lines.pop() || '';
72
+ for (const line of lines) {
73
+ if (!line) consume();
74
+ else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
75
+ }
76
+ if (done) break;
77
+ }
78
+ if (pending.startsWith('data:')) dataLines.push(pending.slice(5).replace(/^ /, ''));
79
+ consume();
80
+ return messages;
81
+ }
82
+
83
+ async function readLimitedText(response, maxBytes = 65536) {
84
+ if (!response.body) return '';
85
+ const reader = response.body.getReader();
86
+ const chunks = [];
87
+ let size = 0;
88
+ while (size < maxBytes) {
89
+ const { done, value } = await reader.read();
90
+ if (done) break;
91
+ const chunk = Buffer.from(value);
92
+ chunks.push(chunk.subarray(0, Math.max(0, maxBytes - size)));
93
+ size += chunk.length;
94
+ }
95
+ return Buffer.concat(chunks).toString('utf8');
96
+ }
97
+
98
+ async function postJsonRpc(config, message, options = {}) {
99
+ const token = String(config.token || config.sat || '');
100
+ const controller = new AbortController();
101
+ const timeoutMs = Number(options.timeoutMs || config.mcp_timeout_ms || 60000);
102
+ const timer = setTimeout(() => controller.abort(new Error('MCP request timed out')), timeoutMs);
103
+ const externalSignal = options.signal;
104
+ const abort = () => controller.abort(externalSignal.reason);
105
+ if (externalSignal) {
106
+ if (externalSignal.aborted) abort();
107
+ else externalSignal.addEventListener('abort', abort, { once: true });
108
+ }
109
+
110
+ const headers = {
111
+ Authorization: `Bearer ${token}`,
112
+ Accept: 'application/json, text/event-stream',
113
+ 'Content-Type': 'application/json',
114
+ 'MCP-Protocol-Version': options.protocolVersion || DEFAULT_PROTOCOL_VERSION,
115
+ };
116
+ if (options.sessionId) headers['Mcp-Session-Id'] = options.sessionId;
117
+
118
+ let response;
119
+ try {
120
+ response = await fetch(endpointFor(config), {
121
+ method: 'POST',
122
+ headers,
123
+ body: JSON.stringify(message),
124
+ signal: controller.signal,
125
+ });
126
+ const sessionId = response.headers.get('mcp-session-id');
127
+ if (sessionId && options.onSession) options.onSession(sessionId);
128
+ if (response.status === 202 || response.status === 204) return [];
129
+
130
+ const contentType = String(response.headers.get('content-type') || '').toLowerCase();
131
+ if (!response.ok) {
132
+ const bodyText = await readLimitedText(response);
133
+ let rpc = null;
134
+ try {
135
+ const parsed = JSON.parse(bodyText);
136
+ rpc = jsonRpcMessages(parsed)[0] || null;
137
+ } catch { /* retain a bounded text error */ }
138
+ const structured = rpc && rpc.error && (rpc.error.message || rpc.error.code);
139
+ throw new McpHttpError(
140
+ redactText(structured || bodyText || `MCP HTTP ${response.status}`, [token]),
141
+ { status: response.status, rpc, code: rpc && rpc.error && rpc.error.code },
142
+ );
143
+ }
144
+
145
+ if (contentType.includes('text/event-stream')) {
146
+ return await parseEventStream(response.body, options.onMessage);
147
+ }
148
+ const text = await response.text();
149
+ if (!text.trim()) return [];
150
+ let parsed;
151
+ try { parsed = JSON.parse(text); } catch {
152
+ throw new McpHttpError('DraftGo MCP returned invalid JSON.', { status: response.status });
153
+ }
154
+ const messages = jsonRpcMessages(parsed);
155
+ if (options.onMessage) messages.forEach(options.onMessage);
156
+ return messages;
157
+ } catch (error) {
158
+ if (error instanceof McpHttpError || controller.signal.aborted) throw error;
159
+ throw new McpHttpError(redactText(error.message || error, [token]), { status: response && response.status });
160
+ } finally {
161
+ clearTimeout(timer);
162
+ if (externalSignal) externalSignal.removeEventListener('abort', abort);
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ DEFAULT_PROTOCOL_VERSION,
168
+ McpHttpError,
169
+ redactText,
170
+ endpointFor,
171
+ postJsonRpc,
172
+ parseEventStream,
173
+ };