draftgo-cli 1.0.4
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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/draftgo.js +9 -0
- package/package.json +70 -0
- package/resources/project-design/README.md +42 -0
- package/resources/skill/SKILL.md +62 -0
- package/resources/skill/init/SKILL.md +41 -0
- package/resources/skill/manifest.json +35 -0
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +97 -0
- package/resources/skill/references/architecture.md +13 -0
- package/resources/skill/references/chat-sdk.md +205 -0
- package/resources/skill/references/checkout.md +140 -0
- package/resources/skill/references/data.md +49 -0
- package/resources/skill/references/db-relations.md +29 -0
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/development.md +41 -0
- package/resources/skill/references/diagnostics.md +50 -0
- package/resources/skill/references/frontend.md +158 -0
- package/resources/skill/references/mcp.md +110 -0
- package/resources/skill/references/methods.md +143 -0
- package/resources/skill/references/modules.md +75 -0
- package/resources/skill/references/runtime.md +109 -0
- package/resources/skill/references/services.md +32 -0
- package/src/apiContractCache.js +120 -0
- package/src/cli.js +100 -0
- package/src/commandRegistry.js +46 -0
- package/src/commands/api.js +244 -0
- package/src/commands/apiKey.js +30 -0
- package/src/commands/autoPush.js +36 -0
- package/src/commands/capabilities.js +100 -0
- package/src/commands/check.js +82 -0
- package/src/commands/checkout.js +18 -0
- package/src/commands/clean.js +72 -0
- package/src/commands/commit.js +47 -0
- package/src/commands/components.js +554 -0
- package/src/commands/conflict.js +30 -0
- package/src/commands/conflicts.js +16 -0
- package/src/commands/connect.js +91 -0
- package/src/commands/delete.js +95 -0
- package/src/commands/deploy.js +77 -0
- package/src/commands/diff.js +39 -0
- package/src/commands/group.js +37 -0
- package/src/commands/help.js +190 -0
- package/src/commands/init.js +126 -0
- package/src/commands/listTargets.js +13 -0
- package/src/commands/local.js +79 -0
- package/src/commands/map.js +395 -0
- package/src/commands/mcp.js +150 -0
- package/src/commands/reconcile.js +20 -0
- package/src/commands/role.js +31 -0
- package/src/commands/status.js +98 -0
- package/src/commands/uninstall.js +52 -0
- package/src/commands/update.js +79 -0
- package/src/commands/verify.js +188 -0
- package/src/commands/visualVerify.js +281 -0
- package/src/commands/worklog.js +117 -0
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +65 -0
- package/src/detect.js +25 -0
- package/src/diffReport.js +106 -0
- package/src/fsx.js +67 -0
- package/src/index.js +46 -0
- package/src/localRuntime/compose.js +119 -0
- package/src/localRuntime/detect.js +77 -0
- package/src/localRuntime/index.js +211 -0
- package/src/localRuntime/mysqlClient.js +155 -0
- package/src/localRuntime/services.js +117 -0
- package/src/logger.js +37 -0
- package/src/mcp/client.js +558 -0
- package/src/mcp/hosts.js +520 -0
- package/src/mcp/parallel.js +54 -0
- package/src/mcp/protocol.js +223 -0
- package/src/mcp/stdio.js +300 -0
- package/src/mcp/tools.js +51 -0
- package/src/paths.js +32 -0
- package/src/platforms.js +110 -0
- package/src/projectConfig.js +139 -0
- package/src/projectDesign.js +19 -0
- package/src/projectHealth.js +33 -0
- package/src/projectMap.js +220 -0
- package/src/prompt.js +94 -0
- package/src/releaseInstall.js +105 -0
- package/src/runtimeFiles.js +45 -0
- package/src/skill.js +295 -0
- package/src/targets.js +43 -0
- package/src/timeout.js +18 -0
- package/src/updateCheck.js +100 -0
- package/src/worklog.js +276 -0
- package/src/worktree/backend.js +438 -0
- package/src/worktree/errors.js +28 -0
- package/src/worktree/index.js +751 -0
- package/src/worktree/inlineScripts.js +99 -0
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +89 -0
- package/src/worktree/status.js +124 -0
- package/src/worktree/streams.js +200 -0
- package/src/worktree/types.js +103 -0
- package/src/worktree/validate.js +37 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PROTOCOL_VERSION = '2026-07-28';
|
|
4
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
class McpHttpError extends Error {
|
|
7
|
+
constructor(message, { status = 0, rpc = null, code = null } = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'McpHttpError';
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.rpc = rpc;
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function redactText(value, secrets = []) {
|
|
17
|
+
let text = String(value == null ? '' : value);
|
|
18
|
+
for (const secret of secrets.filter(Boolean)) text = text.split(String(secret)).join('[REDACTED]');
|
|
19
|
+
text = text.replace(/(authorization\s*[:=]\s*bearer\s+)[^\s,;"']+/ig, '$1[REDACTED]');
|
|
20
|
+
text = text.replace(/(\"?(?:token|sat)\"?\s*[:=]\s*\"?)[^\s,;"'}]+/ig, '$1[REDACTED]');
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function endpointFor(config) {
|
|
25
|
+
const server = new URL(String(config.server || config.mcp_url));
|
|
26
|
+
const serverPath = server.pathname.replace(/\/+$/, '');
|
|
27
|
+
const endpoint = config.mcp_url
|
|
28
|
+
? new URL(String(config.mcp_url), server)
|
|
29
|
+
: new URL(`${serverPath}/mcp`, server);
|
|
30
|
+
if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) {
|
|
31
|
+
throw new McpHttpError('DraftGo MCP endpoint must be a credential-free HTTP(S) URL.');
|
|
32
|
+
}
|
|
33
|
+
const allowedOrigins = new Set([
|
|
34
|
+
server.origin,
|
|
35
|
+
...((config.mcp_allowed_origins || []).map((value) => new URL(value).origin)),
|
|
36
|
+
]);
|
|
37
|
+
if (!allowedOrigins.has(endpoint.origin)) {
|
|
38
|
+
throw new McpHttpError('DraftGo MCP endpoint origin is not allowlisted; refusing to send the API Key.');
|
|
39
|
+
}
|
|
40
|
+
return endpoint.toString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function jsonRpcMessages(value) {
|
|
44
|
+
if (Array.isArray(value)) return value.filter((item) => item && typeof item === 'object');
|
|
45
|
+
return value && typeof value === 'object' ? [value] : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function routingHeaders(message) {
|
|
49
|
+
const messages = jsonRpcMessages(message);
|
|
50
|
+
if (messages.length !== 1 || typeof messages[0].method !== 'string' || !messages[0].method) return {};
|
|
51
|
+
const request = messages[0];
|
|
52
|
+
const headers = { 'Mcp-Method': request.method };
|
|
53
|
+
const params = request.params && typeof request.params === 'object' && !Array.isArray(request.params)
|
|
54
|
+
? request.params
|
|
55
|
+
: {};
|
|
56
|
+
if (request.method === 'tools/call' && typeof params.name === 'string' && params.name) {
|
|
57
|
+
headers['Mcp-Name'] = params.name;
|
|
58
|
+
} else if (request.method === 'prompts/get' && typeof params.name === 'string' && params.name) {
|
|
59
|
+
headers['Mcp-Name'] = params.name;
|
|
60
|
+
} else if (request.method === 'resources/read' && typeof params.uri === 'string' && params.uri) {
|
|
61
|
+
headers['Mcp-Name'] = params.uri;
|
|
62
|
+
}
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function parseEventStream(body, onMessage, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
67
|
+
if (!body) return [];
|
|
68
|
+
const reader = body.getReader();
|
|
69
|
+
const decoder = new TextDecoder();
|
|
70
|
+
let pending = '';
|
|
71
|
+
let dataLines = [];
|
|
72
|
+
const messages = [];
|
|
73
|
+
let size = 0;
|
|
74
|
+
const consume = () => {
|
|
75
|
+
if (!dataLines.length) return;
|
|
76
|
+
const data = dataLines.join('\n');
|
|
77
|
+
dataLines = [];
|
|
78
|
+
if (!data || data === '[DONE]') return;
|
|
79
|
+
let parsed;
|
|
80
|
+
try { parsed = JSON.parse(data); } catch {
|
|
81
|
+
throw new McpHttpError('DraftGo MCP returned invalid JSON in an event stream.');
|
|
82
|
+
}
|
|
83
|
+
for (const message of jsonRpcMessages(parsed)) {
|
|
84
|
+
messages.push(message);
|
|
85
|
+
if (onMessage) onMessage(message);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
while (true) {
|
|
89
|
+
const { done, value } = await reader.read();
|
|
90
|
+
size += value ? value.byteLength : 0;
|
|
91
|
+
if (size > maxBytes) {
|
|
92
|
+
await reader.cancel().catch(() => {});
|
|
93
|
+
throw new McpHttpError(`DraftGo MCP response exceeds ${maxBytes} bytes.`);
|
|
94
|
+
}
|
|
95
|
+
pending += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
96
|
+
const lines = pending.split(/\r?\n/);
|
|
97
|
+
pending = lines.pop() || '';
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (!line) consume();
|
|
100
|
+
else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
101
|
+
}
|
|
102
|
+
if (done) break;
|
|
103
|
+
}
|
|
104
|
+
if (pending.startsWith('data:')) dataLines.push(pending.slice(5).replace(/^ /, ''));
|
|
105
|
+
consume();
|
|
106
|
+
return messages;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readLimitedText(response, maxBytes = 65536) {
|
|
110
|
+
if (!response.body) return '';
|
|
111
|
+
const reader = response.body.getReader();
|
|
112
|
+
const chunks = [];
|
|
113
|
+
let size = 0;
|
|
114
|
+
while (size < maxBytes) {
|
|
115
|
+
const { done, value } = await reader.read();
|
|
116
|
+
if (done) break;
|
|
117
|
+
const chunk = Buffer.from(value);
|
|
118
|
+
chunks.push(chunk.subarray(0, Math.max(0, maxBytes - size)));
|
|
119
|
+
size += chunk.length;
|
|
120
|
+
}
|
|
121
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function readBoundedText(response, maxBytes) {
|
|
125
|
+
if (!response.body) return '';
|
|
126
|
+
const reader = response.body.getReader();
|
|
127
|
+
const chunks = [];
|
|
128
|
+
let size = 0;
|
|
129
|
+
while (true) {
|
|
130
|
+
const { done, value } = await reader.read();
|
|
131
|
+
if (done) break;
|
|
132
|
+
const chunk = Buffer.from(value);
|
|
133
|
+
size += chunk.length;
|
|
134
|
+
if (size > maxBytes) {
|
|
135
|
+
await reader.cancel().catch(() => {});
|
|
136
|
+
throw new McpHttpError(`DraftGo MCP response exceeds ${maxBytes} bytes.`);
|
|
137
|
+
}
|
|
138
|
+
chunks.push(chunk);
|
|
139
|
+
}
|
|
140
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function postJsonRpc(config, message, options = {}) {
|
|
144
|
+
const token = String(config.token || config.sat || '');
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeoutMs = Number(options.timeoutMs || config.mcp_timeout_ms || 60000);
|
|
147
|
+
const timer = setTimeout(() => controller.abort(new Error('MCP request timed out')), timeoutMs);
|
|
148
|
+
const externalSignal = options.signal;
|
|
149
|
+
const abort = () => controller.abort(externalSignal.reason);
|
|
150
|
+
if (externalSignal) {
|
|
151
|
+
if (externalSignal.aborted) abort();
|
|
152
|
+
else externalSignal.addEventListener('abort', abort, { once: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const headers = {
|
|
156
|
+
Authorization: `Bearer ${token}`,
|
|
157
|
+
Accept: 'application/json, text/event-stream',
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
'MCP-Protocol-Version': options.protocolVersion || DEFAULT_PROTOCOL_VERSION,
|
|
160
|
+
...routingHeaders(message),
|
|
161
|
+
};
|
|
162
|
+
if (options.sessionId) headers['Mcp-Session-Id'] = options.sessionId;
|
|
163
|
+
|
|
164
|
+
let response;
|
|
165
|
+
try {
|
|
166
|
+
response = await fetch(endpointFor(config), {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers,
|
|
169
|
+
body: JSON.stringify(message),
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
});
|
|
172
|
+
const sessionId = response.headers.get('mcp-session-id');
|
|
173
|
+
if (sessionId && options.onSession) options.onSession(sessionId);
|
|
174
|
+
if (response.status === 202 || response.status === 204) return [];
|
|
175
|
+
|
|
176
|
+
const contentType = String(response.headers.get('content-type') || '').toLowerCase();
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
const bodyText = await readLimitedText(response);
|
|
179
|
+
let rpc = null;
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(bodyText);
|
|
182
|
+
rpc = jsonRpcMessages(parsed)[0] || null;
|
|
183
|
+
} catch { /* retain a bounded text error */ }
|
|
184
|
+
const structured = rpc && rpc.error && (rpc.error.message || rpc.error.code);
|
|
185
|
+
throw new McpHttpError(
|
|
186
|
+
redactText(structured || bodyText || `MCP HTTP ${response.status}`, [token]),
|
|
187
|
+
{ status: response.status, rpc, code: rpc && rpc.error && rpc.error.code },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (contentType.includes('text/event-stream')) {
|
|
192
|
+
return await parseEventStream(response.body, options.onMessage,
|
|
193
|
+
Number(options.maxResponseBytes || config.mcp_max_response_bytes || DEFAULT_MAX_RESPONSE_BYTES));
|
|
194
|
+
}
|
|
195
|
+
const text = await readBoundedText(response,
|
|
196
|
+
Number(options.maxResponseBytes || config.mcp_max_response_bytes || DEFAULT_MAX_RESPONSE_BYTES));
|
|
197
|
+
if (!text.trim()) return [];
|
|
198
|
+
let parsed;
|
|
199
|
+
try { parsed = JSON.parse(text); } catch {
|
|
200
|
+
throw new McpHttpError('DraftGo MCP returned invalid JSON.', { status: response.status });
|
|
201
|
+
}
|
|
202
|
+
const messages = jsonRpcMessages(parsed);
|
|
203
|
+
if (options.onMessage) messages.forEach(options.onMessage);
|
|
204
|
+
return messages;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error instanceof McpHttpError || controller.signal.aborted) throw error;
|
|
207
|
+
throw new McpHttpError(redactText(error.message || error, [token]), { status: response && response.status });
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
if (externalSignal) externalSignal.removeEventListener('abort', abort);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
216
|
+
DEFAULT_PROTOCOL_VERSION,
|
|
217
|
+
McpHttpError,
|
|
218
|
+
redactText,
|
|
219
|
+
endpointFor,
|
|
220
|
+
routingHeaders,
|
|
221
|
+
postJsonRpc,
|
|
222
|
+
parseEventStream,
|
|
223
|
+
};
|
package/src/mcp/stdio.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadProjectConfig } = require('../projectConfig');
|
|
4
|
+
const { DraftGoMcpClient, redactValue } = require('./client');
|
|
5
|
+
const { redactText } = require('./protocol');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function indexOfHeaderEnd(buffer, start = 0) {
|
|
10
|
+
const crlf = buffer.indexOf('\r\n\r\n', start, 'ascii');
|
|
11
|
+
const lf = buffer.indexOf('\n\n', start, 'ascii');
|
|
12
|
+
if (crlf < 0) return lf < 0 ? null : { index: lf, length: 2 };
|
|
13
|
+
if (lf < 0 || crlf <= lf) return { index: crlf, length: 4 };
|
|
14
|
+
return { index: lf, length: 2 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function leadingWhitespaceLength(buffer) {
|
|
18
|
+
let offset = 0;
|
|
19
|
+
while (offset < buffer.length) {
|
|
20
|
+
const byte = buffer[offset];
|
|
21
|
+
if (byte !== 0x20 && byte !== 0x09 && byte !== 0x0d && byte !== 0x0a) break;
|
|
22
|
+
offset += 1;
|
|
23
|
+
}
|
|
24
|
+
return offset;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function headerState(buffer, offset) {
|
|
28
|
+
const target = 'content-length:';
|
|
29
|
+
const available = buffer.subarray(offset, Math.min(buffer.length, offset + target.length))
|
|
30
|
+
.toString('ascii')
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
if (target.startsWith(available)) return available.length === target.length ? 'header' : 'partial';
|
|
33
|
+
return available.startsWith(target) ? 'header' : 'json';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class StdioFrameParser {
|
|
37
|
+
constructor(onMessage, onError, options = {}) {
|
|
38
|
+
this.onMessage = onMessage;
|
|
39
|
+
this.onError = onError;
|
|
40
|
+
this.maxFrameBytes = Number(options.maxFrameBytes || DEFAULT_MAX_FRAME_BYTES);
|
|
41
|
+
this.buffer = Buffer.alloc(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
push(chunk) {
|
|
45
|
+
if (chunk == null || chunk.length === 0) return;
|
|
46
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
47
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, value]) : value;
|
|
48
|
+
this._consume(false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
end() {
|
|
52
|
+
this._consume(true);
|
|
53
|
+
if (this.buffer.length && this.buffer.toString('utf8').trim()) {
|
|
54
|
+
this._emitError(new Error('Incomplete JSON-RPC frame.'), 'newline');
|
|
55
|
+
}
|
|
56
|
+
this.buffer = Buffer.alloc(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_emitError(error, framing) {
|
|
60
|
+
if (typeof this.onError === 'function') this.onError(error, framing);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_emitBody(body, framing) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(body.toString('utf8'));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this._emitError(error, framing);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.onMessage(parsed, framing);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_consume(flush) {
|
|
75
|
+
while (this.buffer.length) {
|
|
76
|
+
const whitespace = leadingWhitespaceLength(this.buffer);
|
|
77
|
+
if (whitespace === this.buffer.length) {
|
|
78
|
+
if (flush) this.buffer = Buffer.alloc(0);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (whitespace) this.buffer = this.buffer.subarray(whitespace);
|
|
83
|
+
const state = headerState(this.buffer, 0);
|
|
84
|
+
if (state === 'partial' && !flush) return;
|
|
85
|
+
|
|
86
|
+
if (state === 'header') {
|
|
87
|
+
const end = indexOfHeaderEnd(this.buffer);
|
|
88
|
+
if (!end) {
|
|
89
|
+
if (flush) {
|
|
90
|
+
this._emitError(new Error('Incomplete Content-Length header.'), 'content-length');
|
|
91
|
+
this.buffer = Buffer.alloc(0);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const rawHeaders = this.buffer.subarray(0, end.index).toString('ascii');
|
|
96
|
+
const headers = rawHeaders.split(/\r?\n/);
|
|
97
|
+
const lengthHeader = headers.find((line) => /^\s*content-length\s*:/i.test(line));
|
|
98
|
+
const rawLength = lengthHeader && lengthHeader.replace(/^[^:]*:/, '').trim();
|
|
99
|
+
const length = rawLength && /^\d+$/.test(rawLength) ? Number(rawLength) : NaN;
|
|
100
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > this.maxFrameBytes) {
|
|
101
|
+
this._emitError(new Error('Invalid Content-Length header.'), 'content-length');
|
|
102
|
+
this.buffer = this.buffer.subarray(end.index + end.length);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const bodyStart = end.index + end.length;
|
|
106
|
+
if (this.buffer.length - bodyStart < length) {
|
|
107
|
+
if (flush) {
|
|
108
|
+
this._emitError(new Error('Incomplete Content-Length body.'), 'content-length');
|
|
109
|
+
this.buffer = Buffer.alloc(0);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length);
|
|
114
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
115
|
+
this._emitBody(body, 'content-length');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const newline = this.buffer.indexOf(0x0a);
|
|
120
|
+
if (newline < 0) {
|
|
121
|
+
if (!flush) return;
|
|
122
|
+
const body = this.buffer;
|
|
123
|
+
this.buffer = Buffer.alloc(0);
|
|
124
|
+
if (body.toString('utf8').trim()) this._emitBody(body, 'newline');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const body = this.buffer.subarray(0, newline);
|
|
128
|
+
this.buffer = this.buffer.subarray(newline + 1);
|
|
129
|
+
if (body.toString('utf8').trim()) this._emitBody(body, 'newline');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createFrameWriter(output, options = {}) {
|
|
135
|
+
const secrets = options.secrets || [];
|
|
136
|
+
return function writeFrame(message, framing = 'newline') {
|
|
137
|
+
const body = Buffer.from(JSON.stringify(redactValue(message, secrets)), 'utf8');
|
|
138
|
+
if (framing === 'content-length') {
|
|
139
|
+
output.write(Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'ascii'));
|
|
140
|
+
output.write(body);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
output.write(body);
|
|
144
|
+
output.write('\n');
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rpcError(id, code, message, data) {
|
|
149
|
+
const error = { code, message };
|
|
150
|
+
if (data !== undefined) error.data = data;
|
|
151
|
+
return { jsonrpc: '2.0', id: id == null ? null : id, error };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function requestIds(message) {
|
|
155
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
156
|
+
return messages
|
|
157
|
+
.filter((item) => item && typeof item === 'object'
|
|
158
|
+
&& typeof item.method === 'string'
|
|
159
|
+
&& Object.prototype.hasOwnProperty.call(item, 'id'))
|
|
160
|
+
.map((item) => item.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function idKey(id) {
|
|
164
|
+
return `${typeof id}:${JSON.stringify(id)}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validRpcMessage(message) {
|
|
168
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) return false;
|
|
169
|
+
if (message.jsonrpc !== '2.0') return false;
|
|
170
|
+
if (typeof message.method === 'string') return true;
|
|
171
|
+
return Object.prototype.hasOwnProperty.call(message, 'id')
|
|
172
|
+
&& (Object.prototype.hasOwnProperty.call(message, 'result')
|
|
173
|
+
|| Object.prototype.hasOwnProperty.call(message, 'error'));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validRpcPayload(payload) {
|
|
177
|
+
return Array.isArray(payload)
|
|
178
|
+
? payload.length > 0 && payload.every(validRpcMessage)
|
|
179
|
+
: validRpcMessage(payload);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isCancellation(payload) {
|
|
183
|
+
const messages = Array.isArray(payload) ? payload : [payload];
|
|
184
|
+
return messages.some((message) => message
|
|
185
|
+
&& message.method === 'notifications/cancelled');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function serveStdio(projectDir, options = {}) {
|
|
189
|
+
const input = options.input || process.stdin;
|
|
190
|
+
const output = options.output || process.stdout;
|
|
191
|
+
const errorOutput = options.error || process.stderr;
|
|
192
|
+
const config = options.config || loadProjectConfig(projectDir);
|
|
193
|
+
const token = String(config.token || config.sat || '');
|
|
194
|
+
const secrets = [token].filter(Boolean);
|
|
195
|
+
const client = options.client || new DraftGoMcpClient(config);
|
|
196
|
+
const writeFrame = createFrameWriter(output, { secrets });
|
|
197
|
+
const pending = new Set();
|
|
198
|
+
let initializePending = null;
|
|
199
|
+
let ended = false;
|
|
200
|
+
|
|
201
|
+
const reportStreamError = (error) => {
|
|
202
|
+
const text = redactText(error && error.message ? error.message : error, secrets);
|
|
203
|
+
errorOutput.write(`draftgo mcp serve: ${text}\n`);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const dispatch = async (message, framing) => {
|
|
207
|
+
if (!validRpcPayload(message)) {
|
|
208
|
+
writeFrame(rpcError(null, -32600, 'Invalid Request'), framing);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const responded = new Set();
|
|
212
|
+
try {
|
|
213
|
+
await client.forward(message, {
|
|
214
|
+
onMessage(remoteMessage) {
|
|
215
|
+
if (remoteMessage && typeof remoteMessage === 'object' && !remoteMessage.method
|
|
216
|
+
&& Object.prototype.hasOwnProperty.call(remoteMessage, 'id')) {
|
|
217
|
+
responded.add(idKey(remoteMessage.id));
|
|
218
|
+
}
|
|
219
|
+
writeFrame(remoteMessage, framing);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
const missing = requestIds(message).filter((id) => !responded.has(idKey(id)));
|
|
223
|
+
if (missing.length) {
|
|
224
|
+
const errors = missing.map((id) => rpcError(id, -32000, 'DraftGo MCP returned no response.'));
|
|
225
|
+
writeFrame(errors.length === 1 ? errors[0] : errors, framing);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
let missing = requestIds(message).filter((id) => !responded.has(idKey(id)));
|
|
229
|
+
if (!missing.length) {
|
|
230
|
+
reportStreamError(error);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (error && error.rpc) {
|
|
234
|
+
const rpc = redactValue(error.rpc, secrets);
|
|
235
|
+
writeFrame(rpc, framing);
|
|
236
|
+
for (const response of Array.isArray(rpc) ? rpc : [rpc]) {
|
|
237
|
+
if (response && Object.prototype.hasOwnProperty.call(response, 'id')) {
|
|
238
|
+
responded.add(idKey(response.id));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
missing = missing.filter((id) => !responded.has(idKey(id)));
|
|
242
|
+
if (!missing.length) return;
|
|
243
|
+
}
|
|
244
|
+
const code = Number.isInteger(error && error.code) ? error.code : -32000;
|
|
245
|
+
const safeMessage = redactText(error && error.message ? error.message : error, secrets);
|
|
246
|
+
const errors = missing.map((id) => rpcError(id, code, safeMessage || 'DraftGo MCP bridge error.'));
|
|
247
|
+
writeFrame(errors.length === 1 ? errors[0] : errors, framing);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const track = (promise) => {
|
|
252
|
+
pending.add(promise);
|
|
253
|
+
promise.finally(() => pending.delete(promise));
|
|
254
|
+
return promise;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const parser = new StdioFrameParser((message, framing) => {
|
|
258
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
259
|
+
const hasInitialize = messages.some((item) => item && item.method === 'initialize');
|
|
260
|
+
let task;
|
|
261
|
+
if (hasInitialize) {
|
|
262
|
+
task = dispatch(message, framing);
|
|
263
|
+
initializePending = task.finally(() => { initializePending = null; });
|
|
264
|
+
} else if (initializePending && !isCancellation(message)) {
|
|
265
|
+
task = initializePending.then(() => dispatch(message, framing));
|
|
266
|
+
} else {
|
|
267
|
+
task = dispatch(message, framing);
|
|
268
|
+
}
|
|
269
|
+
track(Promise.resolve(task).catch(reportStreamError));
|
|
270
|
+
}, (error, framing) => {
|
|
271
|
+
writeFrame(rpcError(null, -32700, 'Parse error'), framing);
|
|
272
|
+
reportStreamError(error);
|
|
273
|
+
}, options);
|
|
274
|
+
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const finish = async () => {
|
|
277
|
+
if (ended) return;
|
|
278
|
+
ended = true;
|
|
279
|
+
parser.end();
|
|
280
|
+
await Promise.allSettled([...pending]);
|
|
281
|
+
resolve(0);
|
|
282
|
+
};
|
|
283
|
+
input.on('data', (chunk) => parser.push(chunk));
|
|
284
|
+
input.once('end', finish);
|
|
285
|
+
input.once('error', (error) => {
|
|
286
|
+
reportStreamError(error);
|
|
287
|
+
reject(error);
|
|
288
|
+
});
|
|
289
|
+
if (input.readableEnded || input.destroyed) queueMicrotask(finish);
|
|
290
|
+
else if (typeof input.resume === 'function') input.resume();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
DEFAULT_MAX_FRAME_BYTES,
|
|
296
|
+
StdioFrameParser,
|
|
297
|
+
createFrameWriter,
|
|
298
|
+
rpcError,
|
|
299
|
+
serveStdio,
|
|
300
|
+
};
|
package/src/mcp/tools.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { DraftGoMcpClient, TOOL_ALIASES } = require('./client');
|
|
4
|
+
const { WorktreeError } = require('../worktree/errors');
|
|
5
|
+
const { unwrapToolResult, unwrapProtectedResult, toolNameMatches } = require('../worktree/backend');
|
|
6
|
+
|
|
7
|
+
const TOOL_NAMES = Object.freeze({
|
|
8
|
+
projectOverview: 'draftgo_project_overview',
|
|
9
|
+
resourceList: 'draftgo_resource_list',
|
|
10
|
+
resourceSearch: 'draftgo_resource_search',
|
|
11
|
+
resourceMetadata: 'draftgo_resource_get_metadata',
|
|
12
|
+
resourceFragment: 'draftgo_resource_read_fragment',
|
|
13
|
+
apiSearch: 'draftgo_api_search',
|
|
14
|
+
apiDescribe: 'draftgo_api_describe',
|
|
15
|
+
apiCall: 'draftgo_api_call',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Keep server-generation naming compatibility at the protocol boundary so
|
|
19
|
+
// commands only deal with one canonical Registry contract.
|
|
20
|
+
const PROTECTED_RESOURCE_TOOLS = new Set([
|
|
21
|
+
TOOL_NAMES.resourceList,
|
|
22
|
+
TOOL_NAMES.resourceSearch,
|
|
23
|
+
TOOL_NAMES.resourceMetadata,
|
|
24
|
+
TOOL_NAMES.resourceFragment,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
async function openToolSession(config, expected = [], options = {}) {
|
|
28
|
+
const client = options.client || new DraftGoMcpClient(config);
|
|
29
|
+
const initialized = options.clientInitialized ? null : await client.initialize(options);
|
|
30
|
+
const tools = options.tools || await client.listAllTools(options);
|
|
31
|
+
const names = {};
|
|
32
|
+
for (const expectedName of expected) {
|
|
33
|
+
const aliases = TOOL_ALIASES[expectedName] || [expectedName];
|
|
34
|
+
const found = aliases.map((alias) => tools.find((tool) => tool
|
|
35
|
+
&& toolNameMatches(String(tool.name || ''), alias))).find(Boolean);
|
|
36
|
+
if (!found) throw new WorktreeError('MCP_TOOL_UNAVAILABLE', `${expectedName} is not available.`);
|
|
37
|
+
names[expectedName] = found.name;
|
|
38
|
+
}
|
|
39
|
+
return { client, initialized, tools, names };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function callStructured(session, expectedName, args = {}, options = {}) {
|
|
43
|
+
const name = session.names[expectedName] || expectedName;
|
|
44
|
+
const result = await session.client.toolsCall(name, args, options);
|
|
45
|
+
const data = unwrapToolResult(result);
|
|
46
|
+
return PROTECTED_RESOURCE_TOOLS.has(expectedName)
|
|
47
|
+
? unwrapProtectedResult(data, expectedName)
|
|
48
|
+
: data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { TOOL_NAMES, TOOL_ALIASES, openToolSession, callStructured };
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Resource dir inside the package (bundled with npm publish)
|
|
6
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
const RESOURCES_DIR = path.join(PACKAGE_ROOT, 'resources');
|
|
8
|
+
|
|
9
|
+
// In-project runtime data root: <project>/.draftgo
|
|
10
|
+
// The skill body itself NO LONGER lives here; it is rendered into each
|
|
11
|
+
// AI tool's own directory (see src/platforms.js). Only runtime data and
|
|
12
|
+
// CLI config stay under .draftgo/.
|
|
13
|
+
const DG_DIR_NAME = '.draftgo';
|
|
14
|
+
const VERSION_FILE = '.version';
|
|
15
|
+
|
|
16
|
+
function dgDir(projectDir) {
|
|
17
|
+
return path.join(projectDir, DG_DIR_NAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// CLI version marker now lives at <project>/.draftgo/.version
|
|
21
|
+
function versionFile(projectDir) {
|
|
22
|
+
return path.join(dgDir(projectDir), VERSION_FILE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
PACKAGE_ROOT,
|
|
27
|
+
RESOURCES_DIR,
|
|
28
|
+
DG_DIR_NAME,
|
|
29
|
+
VERSION_FILE,
|
|
30
|
+
dgDir,
|
|
31
|
+
versionFile,
|
|
32
|
+
};
|