draftgo-cli 4.0.24 → 4.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -37
- package/package.json +3 -5
- package/resources/skill/SKILL.md +9 -5
- package/resources/skill/manifest.json +2 -5
- package/resources/skill/references/ai.md +41 -0
- package/resources/skill/references/app-api.md +2 -50
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/chat-sdk.md +29 -37
- package/resources/skill/references/checkout.md +4 -4
- package/resources/skill/references/data.md +0 -46
- package/resources/skill/references/delivery.md +3 -3
- package/resources/skill/references/diagnostics.md +10 -11
- package/resources/skill/references/frontend.md +23 -20
- package/resources/skill/references/mcp.md +4 -14
- package/resources/skill/references/methods.md +15 -68
- package/resources/skill/references/modules.md +23 -44
- package/resources/skill/references/runtime.md +3 -20
- package/resources/skill/story/SKILL.md +2 -2
- package/src/apiContractCache.js +14 -6
- package/src/cli.js +0 -7
- package/src/commandRegistry.js +0 -6
- package/src/commands/api.js +87 -17
- package/src/commands/apiKey.js +2 -6
- package/src/commands/autoPush.js +15 -51
- package/src/commands/capabilities.js +22 -15
- package/src/commands/check.js +19 -53
- package/src/commands/checkout.js +1 -4
- package/src/commands/clean.js +1 -1
- package/src/commands/commit.js +1 -4
- package/src/commands/components.js +12 -8
- package/src/commands/conflict.js +4 -6
- package/src/commands/conflicts.js +1 -2
- package/src/commands/connect.js +0 -8
- package/src/commands/delete.js +15 -11
- package/src/commands/deploy.js +64 -26
- package/src/commands/diff.js +1 -4
- package/src/commands/group.js +2 -3
- package/src/commands/help.js +22 -43
- package/src/commands/init.js +13 -6
- package/src/commands/local.js +4 -1
- package/src/commands/map.js +138 -23
- package/src/commands/reconcile.js +1 -15
- package/src/commands/role.js +1 -2
- package/src/commands/status.js +12 -40
- package/src/commands/update.js +18 -24
- package/src/commands/verify.js +8 -7
- package/src/commands/worklog.js +11 -5
- package/src/contractCompatibility.js +10 -2
- package/src/localRuntime/compose.js +41 -27
- package/src/localRuntime/detect.js +6 -6
- package/src/localRuntime/index.js +47 -47
- package/src/localRuntime/services.js +2 -39
- package/src/mcp/client.js +99 -134
- package/src/mcp/parallel.js +25 -2
- package/src/mcp/protocol.js +38 -9
- package/src/mcp/tools.js +10 -19
- package/src/projectConfig.js +1 -4
- package/src/{workspaceHealth.js → projectHealth.js} +5 -5
- package/src/projectMap.js +1 -1
- package/src/runtimeFiles.js +2 -1
- package/src/worklog.js +3 -2
- package/src/worktree/backend.js +127 -15
- package/src/worktree/index.js +64 -22
- package/src/worktree/locks.js +52 -0
- package/src/worktree/manifest.js +18 -4
- package/src/worktree/status.js +4 -2
- package/resources/custom-service-sdk/ai.go +0 -520
- package/resources/custom-service-sdk/ai_test.go +0 -156
- package/resources/custom-service-sdk/auth_test.go +0 -56
- package/resources/custom-service-sdk/billing.go +0 -596
- package/resources/custom-service-sdk/billing_test.go +0 -150
- package/resources/custom-service-sdk/go.mod +0 -3
- package/resources/custom-service-sdk/manifest.json +0 -77
- package/resources/custom-service-sdk/platform.go +0 -352
- package/resources/custom-service-sdk/platform_logger_test.go +0 -24
- package/resources/custom-service-sdk/registration_test.go +0 -39
- package/resources/custom-service-sdk/resources.go +0 -247
- package/resources/custom-service-sdk/resources_billing_test.go +0 -115
- package/resources/custom-service-sdk/resources_files_test.go +0 -57
- package/resources/custom-service-sdk/resources_scope_test.go +0 -92
- package/resources/custom-service-sdk/sdk.go +0 -209
- package/resources/skill/references/aihub.md +0 -116
- package/resources/skill/references/custom-services.md +0 -201
- package/src/commands/customService.js +0 -95
- package/src/commands/dataRange.js +0 -33
- package/src/commands/grant.js +0 -29
- package/src/commands/space.js +0 -41
- package/src/customServices.js +0 -484
package/src/mcp/client.js
CHANGED
|
@@ -9,30 +9,37 @@ const {
|
|
|
9
9
|
} = require('./protocol');
|
|
10
10
|
const { allWithAbort } = require('./parallel');
|
|
11
11
|
|
|
12
|
-
const SAFE_TEST_TOOLS = [
|
|
13
|
-
'draftgo_project_overview',
|
|
14
|
-
'draftgo_resource_list',
|
|
15
|
-
'draftgo_api_search',
|
|
16
|
-
'draftgo_api_describe',
|
|
17
|
-
'draftgo_api_call',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const REQUIRED_DRAFTGO_TOOLS = [
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
]
|
|
12
|
+
const SAFE_TEST_TOOLS = [
|
|
13
|
+
'draftgo_project_overview',
|
|
14
|
+
'draftgo_resource_list',
|
|
15
|
+
'draftgo_api_search',
|
|
16
|
+
'draftgo_api_describe',
|
|
17
|
+
'draftgo_api_call',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const REQUIRED_DRAFTGO_TOOLS = [
|
|
21
|
+
'draftgo_api_search',
|
|
22
|
+
'draftgo_api_describe',
|
|
23
|
+
'draftgo_api_call',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const TOOL_ALIASES = Object.freeze({
|
|
27
|
+
draftgo_api_search: Object.freeze([
|
|
28
|
+
'draftgo_api_search', 'draftgo_operation_search', 'search_operations', 'search',
|
|
29
|
+
]),
|
|
30
|
+
draftgo_api_describe: Object.freeze([
|
|
31
|
+
'draftgo_api_describe', 'draftgo_operation_describe', 'describe_operation', 'describe',
|
|
32
|
+
]),
|
|
33
|
+
draftgo_api_call: Object.freeze([
|
|
34
|
+
'draftgo_api_invoke', 'draftgo_api_call', 'draftgo_operation_invoke', 'invoke_operation', 'invoke', 'call',
|
|
35
|
+
]),
|
|
36
|
+
});
|
|
30
37
|
|
|
31
38
|
function diagnosticArguments(canonicalName) {
|
|
32
39
|
if (canonicalName === 'draftgo_resource_list') {
|
|
33
40
|
return { resource_type: 'pages', limit: 1 };
|
|
34
41
|
}
|
|
35
|
-
if (canonicalName === 'draftgo_api_search') return { query: '
|
|
42
|
+
if (canonicalName === 'draftgo_api_search') return { query: 'listDbMeta', limit: 1 };
|
|
36
43
|
return {};
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -45,15 +52,35 @@ function diagnosticData(result) {
|
|
|
45
52
|
return value;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
&&
|
|
55
|
-
&& operation.
|
|
56
|
-
|
|
55
|
+
const CONNECTION_PROBE_OPERATION = 'getCurrentUserAPIKey';
|
|
56
|
+
|
|
57
|
+
function findConnectionProbeOperation(result) {
|
|
58
|
+
const value = diagnosticData(result);
|
|
59
|
+
const items = value && Array.isArray(value.items) ? value.items : [];
|
|
60
|
+
return items.find((operation) => operation
|
|
61
|
+
&& operation.operation_id === CONNECTION_PROBE_OPERATION
|
|
62
|
+
&& String(operation.method || '').toUpperCase() === 'GET'
|
|
63
|
+
&& String(operation.risk || '').toLowerCase() === 'low'
|
|
64
|
+
&& operation.destructive !== true) || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertSafeConnectionProbe(description) {
|
|
68
|
+
const value = diagnosticData(description);
|
|
69
|
+
const operation = value && value.operation && typeof value.operation === 'object'
|
|
70
|
+
? value.operation : value;
|
|
71
|
+
const properties = operation && operation.input_schema && operation.input_schema.properties || {};
|
|
72
|
+
const required = operation && operation.input_schema && operation.input_schema.required || [];
|
|
73
|
+
const inputKeys = Object.keys(properties).filter((key) => key !== 'confirm');
|
|
74
|
+
if (!operation || operation.operation_id !== CONNECTION_PROBE_OPERATION
|
|
75
|
+
|| String(operation.method || '').toUpperCase() !== 'GET'
|
|
76
|
+
|| String(operation.risk || '').toLowerCase() !== 'low'
|
|
77
|
+
|| operation.destructive === true || required.length > 0 || inputKeys.length > 0) {
|
|
78
|
+
throw new McpRpcError(`DraftGo Registry does not expose a safe ${CONNECTION_PROBE_OPERATION} probe.`, {
|
|
79
|
+
code: -32601,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return operation;
|
|
83
|
+
}
|
|
57
84
|
|
|
58
85
|
class McpRpcError extends Error {
|
|
59
86
|
constructor(message, { code = -32000, data, id = null } = {}) {
|
|
@@ -103,40 +130,6 @@ function sessionInvalidResponse(message, expectedIds) {
|
|
|
103
130
|
&& isSessionInvalidError(item.error)) || null;
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
function diagnosticNextCursor(result) {
|
|
107
|
-
const value = diagnosticData(result);
|
|
108
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
109
|
-
const hasMore = value.has_more === true || value.hasMore === true;
|
|
110
|
-
|
|
111
|
-
let cursor;
|
|
112
|
-
let present = false;
|
|
113
|
-
if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
|
|
114
|
-
cursor = value.next_cursor;
|
|
115
|
-
present = true;
|
|
116
|
-
} else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
|
|
117
|
-
cursor = value.nextCursor;
|
|
118
|
-
present = true;
|
|
119
|
-
} else if (hasMore) {
|
|
120
|
-
if (!Object.prototype.hasOwnProperty.call(value, 'cursor')) {
|
|
121
|
-
throw new McpRpcError('DraftGo api_search reported more pages without a cursor.');
|
|
122
|
-
}
|
|
123
|
-
cursor = value.cursor;
|
|
124
|
-
present = true;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!present) return null;
|
|
128
|
-
if (cursor == null || cursor === '') {
|
|
129
|
-
if (hasMore) {
|
|
130
|
-
throw new McpRpcError('DraftGo api_search reported more pages with an empty cursor.');
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
if (typeof cursor !== 'string' || !cursor.trim()) {
|
|
135
|
-
throw new McpRpcError('DraftGo api_search returned an invalid cursor.');
|
|
136
|
-
}
|
|
137
|
-
return cursor;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
133
|
function redactValue(value, secrets = [], seen = new WeakMap()) {
|
|
141
134
|
if (typeof value === 'string') return redactText(value, secrets);
|
|
142
135
|
if (!value || typeof value !== 'object') return value;
|
|
@@ -175,9 +168,10 @@ function findResponse(messages, id) {
|
|
|
175
168
|
&& !message.method);
|
|
176
169
|
}
|
|
177
170
|
|
|
178
|
-
function toolMatches(name, candidate) {
|
|
179
|
-
|
|
180
|
-
|
|
171
|
+
function toolMatches(name, candidate) {
|
|
172
|
+
const aliases = TOOL_ALIASES[candidate] || [candidate];
|
|
173
|
+
return aliases.some((alias) => name === alias || name.endsWith(`.${alias}`)
|
|
174
|
+
|| name.endsWith(`/${alias}`) || name.endsWith(`:${alias}`));
|
|
181
175
|
}
|
|
182
176
|
|
|
183
177
|
class DraftGoMcpClient {
|
|
@@ -446,32 +440,20 @@ class DraftGoMcpClient {
|
|
|
446
440
|
);
|
|
447
441
|
}
|
|
448
442
|
|
|
449
|
-
const selected = safeTools.map((canonical) => ({
|
|
443
|
+
const selected = safeTools.map((canonical) => ({
|
|
450
444
|
canonical,
|
|
451
445
|
tool: tools.find((candidate) => candidate && typeof candidate.name === 'string'
|
|
452
446
|
&& toolMatches(candidate.name, canonical)),
|
|
453
447
|
})).filter((entry) => entry.tool);
|
|
454
448
|
const byCanonical = Object.fromEntries(selected.map((entry) => [entry.canonical, entry.tool]));
|
|
455
449
|
const overrideCanonical = safeTools.find((canonical) => toolMatches(firstSafeTool.name, canonical));
|
|
456
|
-
|
|
457
|
-
label: overrideCanonical,
|
|
458
|
-
canonical: overrideCanonical,
|
|
459
|
-
tool: firstSafeTool,
|
|
460
|
-
args: diagnosticArguments(overrideCanonical),
|
|
461
|
-
}] : [
|
|
462
|
-
{ label: '
|
|
463
|
-
...['pages', 'navigations', 'docs/articles'].map((resourceType) => ({
|
|
464
|
-
label: `resource:${resourceType}`,
|
|
465
|
-
canonical: 'draftgo_resource_list',
|
|
466
|
-
tool: byCanonical.draftgo_resource_list,
|
|
467
|
-
args: { resource_type: resourceType, limit: 1 },
|
|
468
|
-
})),
|
|
469
|
-
{
|
|
470
|
-
label: 'api:db_meta',
|
|
471
|
-
canonical: 'draftgo_api_search',
|
|
472
|
-
tool: byCanonical.draftgo_api_search,
|
|
473
|
-
args: { resource_type: 'db_meta', limit: 100 },
|
|
474
|
-
},
|
|
450
|
+
const defaultPlan = hasDiagnosticOverrides ? [{
|
|
451
|
+
label: overrideCanonical,
|
|
452
|
+
canonical: overrideCanonical,
|
|
453
|
+
tool: firstSafeTool,
|
|
454
|
+
args: diagnosticArguments(overrideCanonical),
|
|
455
|
+
}] : [
|
|
456
|
+
{ label: 'api:catalog', canonical: 'draftgo_api_search', tool: byCanonical.draftgo_api_search, args: { query: CONNECTION_PROBE_OPERATION, limit: 100 } },
|
|
475
457
|
];
|
|
476
458
|
const runPlan = (plan, startIndex = 0) => allWithAbort(plan.map((entry, offset) =>
|
|
477
459
|
async (queryOptions) => {
|
|
@@ -501,52 +483,34 @@ class DraftGoMcpClient {
|
|
|
501
483
|
});
|
|
502
484
|
}), options);
|
|
503
485
|
const tested = await runPlan(defaultPlan);
|
|
504
|
-
if (!hasDiagnosticOverrides) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
code: -32601,
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
const operationID = String(operation.operation_id);
|
|
536
|
-
tested.push(...await runPlan([
|
|
537
|
-
{
|
|
538
|
-
label: 'api:describe-db_meta',
|
|
539
|
-
canonical: 'draftgo_api_describe',
|
|
540
|
-
tool: byCanonical.draftgo_api_describe,
|
|
541
|
-
args: { operation_id: operationID },
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
label: 'api:call-db_meta',
|
|
545
|
-
canonical: 'draftgo_api_call',
|
|
546
|
-
tool: byCanonical.draftgo_api_call,
|
|
547
|
-
args: { operation_id: operationID, query: { page: 1, page_size: 1 } },
|
|
548
|
-
},
|
|
549
|
-
], tested.length));
|
|
486
|
+
if (!hasDiagnosticOverrides) {
|
|
487
|
+
const discovery = tested.find((entry) => entry.label === 'api:catalog');
|
|
488
|
+
const catalog = diagnosticData(discovery && discovery.result);
|
|
489
|
+
const operation = findConnectionProbeOperation(discovery && discovery.result);
|
|
490
|
+
if (!operation || !operation.operation_id) {
|
|
491
|
+
throw new McpRpcError(`DraftGo Registry returned no safe ${CONNECTION_PROBE_OPERATION} operation.`, {
|
|
492
|
+
code: -32601,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const operationID = String(operation.operation_id);
|
|
496
|
+
const revision = String(catalog.registry_revision || '').trim();
|
|
497
|
+
if (!revision) {
|
|
498
|
+
throw new McpRpcError('DraftGo api_search did not return registry_revision.');
|
|
499
|
+
}
|
|
500
|
+
const [described] = await runPlan([{
|
|
501
|
+
label: 'api:describe',
|
|
502
|
+
canonical: 'draftgo_api_describe',
|
|
503
|
+
tool: byCanonical.draftgo_api_describe,
|
|
504
|
+
args: { operation_id: operationID, registry_revision: revision },
|
|
505
|
+
}], tested.length);
|
|
506
|
+
tested.push(described);
|
|
507
|
+
assertSafeConnectionProbe(described.result);
|
|
508
|
+
tested.push(...await runPlan([{
|
|
509
|
+
label: 'api:invoke',
|
|
510
|
+
canonical: 'draftgo_api_call',
|
|
511
|
+
tool: byCanonical.draftgo_api_call,
|
|
512
|
+
args: { operation_id: operationID, registry_revision: revision },
|
|
513
|
+
}], tested.length));
|
|
550
514
|
}
|
|
551
515
|
return {
|
|
552
516
|
initialized,
|
|
@@ -580,11 +544,12 @@ module.exports = {
|
|
|
580
544
|
McpRpcError,
|
|
581
545
|
McpHttpError,
|
|
582
546
|
SAFE_TEST_TOOLS,
|
|
583
|
-
REQUIRED_DRAFTGO_TOOLS,
|
|
547
|
+
REQUIRED_DRAFTGO_TOOLS,
|
|
548
|
+
TOOL_ALIASES,
|
|
584
549
|
diagnosticArguments,
|
|
585
550
|
diagnosticData,
|
|
586
|
-
|
|
587
|
-
|
|
551
|
+
findConnectionProbeOperation,
|
|
552
|
+
assertSafeConnectionProbe,
|
|
588
553
|
isSessionInvalidError,
|
|
589
554
|
redactValue,
|
|
590
555
|
testConnection,
|
package/src/mcp/parallel.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
async function mapBounded(items, mapper, options = {}) {
|
|
4
|
+
const limit = options.concurrency ?? 8;
|
|
5
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 64) throw new TypeError('Concurrency must be between 1 and 64.');
|
|
6
|
+
const results = new Array(items.length);
|
|
7
|
+
let cursor = 0;
|
|
8
|
+
async function worker() {
|
|
9
|
+
while (cursor < items.length) {
|
|
10
|
+
const index = cursor++;
|
|
11
|
+
try { results[index] = { status: 'fulfilled', value: await mapper(items[index], index) }; }
|
|
12
|
+
catch (reason) { results[index] = { status: 'rejected', reason }; }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
16
|
+
if (options.settled) return results;
|
|
17
|
+
const failure = results.find((item) => item.status === 'rejected');
|
|
18
|
+
if (failure) throw failure.reason;
|
|
19
|
+
return results.map((item) => item.value);
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
async function allWithAbort(taskFactories, options = {}) {
|
|
4
23
|
if (!Array.isArray(taskFactories) || taskFactories.some((task) => typeof task !== 'function')) {
|
|
5
24
|
throw new TypeError('Parallel MCP tasks must be functions.');
|
|
@@ -19,7 +38,11 @@ async function allWithAbort(taskFactories, options = {}) {
|
|
|
19
38
|
}
|
|
20
39
|
try {
|
|
21
40
|
const taskOptions = { ...options, signal: controller.signal };
|
|
22
|
-
return await
|
|
41
|
+
return await mapBounded(taskFactories, async (task) => {
|
|
42
|
+
controller.signal.throwIfAborted();
|
|
43
|
+
try { return await task(taskOptions); }
|
|
44
|
+
catch (error) { controller.abort(error); throw error; }
|
|
45
|
+
}, options);
|
|
23
46
|
} catch (error) {
|
|
24
47
|
if (!controller.signal.aborted) controller.abort(error);
|
|
25
48
|
throw error;
|
|
@@ -28,4 +51,4 @@ async function allWithAbort(taskFactories, options = {}) {
|
|
|
28
51
|
}
|
|
29
52
|
}
|
|
30
53
|
|
|
31
|
-
module.exports = { allWithAbort };
|
|
54
|
+
module.exports = { allWithAbort, mapBounded };
|
package/src/mcp/protocol.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const DEFAULT_PROTOCOL_VERSION = '2026-07-28';
|
|
4
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
4
5
|
|
|
5
6
|
class McpHttpError extends Error {
|
|
6
7
|
constructor(message, { status = 0, rpc = null, code = null } = {}) {
|
|
@@ -62,13 +63,14 @@ function routingHeaders(message) {
|
|
|
62
63
|
return headers;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
async function parseEventStream(body, onMessage) {
|
|
66
|
+
async function parseEventStream(body, onMessage, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
66
67
|
if (!body) return [];
|
|
67
68
|
const reader = body.getReader();
|
|
68
69
|
const decoder = new TextDecoder();
|
|
69
70
|
let pending = '';
|
|
70
71
|
let dataLines = [];
|
|
71
|
-
const messages = [];
|
|
72
|
+
const messages = [];
|
|
73
|
+
let size = 0;
|
|
72
74
|
const consume = () => {
|
|
73
75
|
if (!dataLines.length) return;
|
|
74
76
|
const data = dataLines.join('\n');
|
|
@@ -84,7 +86,12 @@ async function parseEventStream(body, onMessage) {
|
|
|
84
86
|
}
|
|
85
87
|
};
|
|
86
88
|
while (true) {
|
|
87
|
-
const { done, value } = await reader.read();
|
|
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
|
+
}
|
|
88
95
|
pending += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
89
96
|
const lines = pending.split(/\r?\n/);
|
|
90
97
|
pending = lines.pop() || '';
|
|
@@ -99,7 +106,7 @@ async function parseEventStream(body, onMessage) {
|
|
|
99
106
|
return messages;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
async function readLimitedText(response, maxBytes = 65536) {
|
|
109
|
+
async function readLimitedText(response, maxBytes = 65536) {
|
|
103
110
|
if (!response.body) return '';
|
|
104
111
|
const reader = response.body.getReader();
|
|
105
112
|
const chunks = [];
|
|
@@ -112,7 +119,26 @@ async function readLimitedText(response, maxBytes = 65536) {
|
|
|
112
119
|
size += chunk.length;
|
|
113
120
|
}
|
|
114
121
|
return Buffer.concat(chunks).toString('utf8');
|
|
115
|
-
}
|
|
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
|
+
}
|
|
116
142
|
|
|
117
143
|
async function postJsonRpc(config, message, options = {}) {
|
|
118
144
|
const token = String(config.token || config.sat || '');
|
|
@@ -163,9 +189,11 @@ async function postJsonRpc(config, message, options = {}) {
|
|
|
163
189
|
}
|
|
164
190
|
|
|
165
191
|
if (contentType.includes('text/event-stream')) {
|
|
166
|
-
return await parseEventStream(response.body, options.onMessage
|
|
167
|
-
|
|
168
|
-
|
|
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));
|
|
169
197
|
if (!text.trim()) return [];
|
|
170
198
|
let parsed;
|
|
171
199
|
try { parsed = JSON.parse(text); } catch {
|
|
@@ -183,7 +211,8 @@ async function postJsonRpc(config, message, options = {}) {
|
|
|
183
211
|
}
|
|
184
212
|
}
|
|
185
213
|
|
|
186
|
-
module.exports = {
|
|
214
|
+
module.exports = {
|
|
215
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
187
216
|
DEFAULT_PROTOCOL_VERSION,
|
|
188
217
|
McpHttpError,
|
|
189
218
|
redactText,
|
package/src/mcp/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { DraftGoMcpClient } = require('./client');
|
|
3
|
+
const { DraftGoMcpClient, TOOL_ALIASES } = require('./client');
|
|
4
4
|
const { WorktreeError } = require('../worktree/errors');
|
|
5
5
|
const { unwrapToolResult, unwrapProtectedResult, toolNameMatches } = require('../worktree/backend');
|
|
6
6
|
|
|
@@ -15,6 +15,8 @@ const TOOL_NAMES = Object.freeze({
|
|
|
15
15
|
apiCall: 'draftgo_api_call',
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
// Keep server-generation naming compatibility at the protocol boundary so
|
|
19
|
+
// commands only deal with one canonical Registry contract.
|
|
18
20
|
const PROTECTED_RESOURCE_TOOLS = new Set([
|
|
19
21
|
TOOL_NAMES.resourceList,
|
|
20
22
|
TOOL_NAMES.resourceSearch,
|
|
@@ -24,28 +26,17 @@ const PROTECTED_RESOURCE_TOOLS = new Set([
|
|
|
24
26
|
|
|
25
27
|
async function openToolSession(config, expected = [], options = {}) {
|
|
26
28
|
const client = options.client || new DraftGoMcpClient(config);
|
|
27
|
-
await client.initialize(options);
|
|
28
|
-
const tools = await client.listAllTools(options);
|
|
29
|
+
const initialized = options.clientInitialized ? null : await client.initialize(options);
|
|
30
|
+
const tools = options.tools || await client.listAllTools(options);
|
|
29
31
|
const names = {};
|
|
30
32
|
for (const expectedName of expected) {
|
|
31
|
-
const
|
|
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);
|
|
32
36
|
if (!found) throw new WorktreeError('MCP_TOOL_UNAVAILABLE', `${expectedName} is not available.`);
|
|
33
37
|
names[expectedName] = found.name;
|
|
34
38
|
}
|
|
35
|
-
return { client, tools, names };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function configuredScope(config, description = null) {
|
|
39
|
-
const operation = description && (description.operation || description) || {};
|
|
40
|
-
if (String(operation.ownership_mode || '').toLowerCase() === 'platform_only') return {};
|
|
41
|
-
const supported = Array.isArray(operation.supported_scopes)
|
|
42
|
-
? operation.supported_scopes.map(value => String(value).toLowerCase()) : [];
|
|
43
|
-
const needsSpace = String(operation.ownership_mode || '').toLowerCase() === 'workspace_required'
|
|
44
|
-
&& supported.length > 0 && !supported.includes('platform');
|
|
45
|
-
if (config && config.scope_type === 'space' && Number(config.space_id) > 0 && (needsSpace || supported.includes('space'))) {
|
|
46
|
-
return { scope_type: 'space', space_id: Number(config.space_id) };
|
|
47
|
-
}
|
|
48
|
-
return {};
|
|
39
|
+
return { client, initialized, tools, names };
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
async function callStructured(session, expectedName, args = {}, options = {}) {
|
|
@@ -57,4 +48,4 @@ async function callStructured(session, expectedName, args = {}, options = {}) {
|
|
|
57
48
|
: data;
|
|
58
49
|
}
|
|
59
50
|
|
|
60
|
-
module.exports = { TOOL_NAMES, openToolSession, callStructured
|
|
51
|
+
module.exports = { TOOL_NAMES, TOOL_ALIASES, openToolSession, callStructured };
|
package/src/projectConfig.js
CHANGED
|
@@ -100,13 +100,10 @@ function writeProjectConfig(projectDir, server, token, options = {}) {
|
|
|
100
100
|
if (!apiKey) throw new Error('DraftGo API Key is required.');
|
|
101
101
|
const existing = readExisting(projectDir);
|
|
102
102
|
const config = {
|
|
103
|
-
...existing,
|
|
104
103
|
server: normalizedServer,
|
|
105
104
|
token: apiKey,
|
|
106
105
|
auto_push: existing.auto_push === true,
|
|
107
106
|
};
|
|
108
|
-
if (options.scope_type) config.scope_type = String(options.scope_type).trim().toLowerCase();
|
|
109
|
-
if (options.space_id) config.space_id = Number(options.space_id);
|
|
110
107
|
if (options.mcp_url) config.mcp_url = normalizeMcpEndpoint(options.mcp_url);
|
|
111
108
|
else delete config.mcp_url;
|
|
112
109
|
const file = configPath(projectDir);
|
|
@@ -124,7 +121,7 @@ function loadProjectConfig(projectDir, { requireToken = true } = {}) {
|
|
|
124
121
|
const token = String(config.token || config.sat || '').trim();
|
|
125
122
|
if (!server) throw new Error('DraftGo project config is missing server.');
|
|
126
123
|
if (requireToken && !token) throw new Error('DraftGo project config is missing API Key.');
|
|
127
|
-
const result = {
|
|
124
|
+
const result = { server, token, auto_push: config.auto_push === true, path: file };
|
|
128
125
|
if (mcpUrl) result.mcp_url = mcpUrl;
|
|
129
126
|
else delete result.mcp_url;
|
|
130
127
|
return result;
|
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const runtime = require('./runtimeFiles');
|
|
6
6
|
|
|
7
7
|
const PROTECTED_PREFIXES = ['worktree/', 'conflicts/', 'lessons/', 'tmp/', 'config.json', 'api-contract-cache.json', 'api-contract-cache.json.lock', 'worklog.md', 'story.yaml', '.version', 'runtime-manifest.json'];
|
|
8
|
-
function
|
|
8
|
+
function projectHealth(projectDir) {
|
|
9
9
|
const root = runtime.draftgoRoot(projectDir); const registered = new Set(runtime.load(projectDir).entries.map((entry) => String(entry.path).replace(/\\/g, '/')));
|
|
10
10
|
const summary = { managed_files: 0, unknown_files: 0, temporary_files: 0, artifact_files: 0, total_bytes: 0, reclaimable_bytes: 0, warnings: [] };
|
|
11
11
|
if (!fs.existsSync(root)) return summary;
|
|
@@ -13,7 +13,7 @@ function workspaceHealth(projectDir) {
|
|
|
13
13
|
for (const item of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
14
14
|
const absolute = path.join(directory, item.name); const relative = path.relative(root, absolute).replace(/\\/g, '/');
|
|
15
15
|
if (item.isDirectory()) {
|
|
16
|
-
if ((/^(.+\/)?(build|dist|node_modules|tmp-build|build-)/i.test(relative)) && directorySize(absolute) > 50 * 1024 * 1024) summary.warnings.push({ code: 'DG-
|
|
16
|
+
if ((/^(.+\/)?(build|dist|node_modules|tmp-build|build-)/i.test(relative)) && directorySize(absolute) > 50 * 1024 * 1024) summary.warnings.push({ code: 'DG-PROJECT-LARGE-BUILD', path: relative });
|
|
17
17
|
visit(absolute); continue;
|
|
18
18
|
}
|
|
19
19
|
if (!item.isFile()) continue;
|
|
@@ -23,11 +23,11 @@ function workspaceHealth(projectDir) {
|
|
|
23
23
|
if (relative.startsWith('tmp/')) summary.temporary_files += 1;
|
|
24
24
|
if (relative.startsWith('artifacts/')) summary.artifact_files += 1;
|
|
25
25
|
if (registered.has(relative) || relative.startsWith('tmp/')) summary.reclaimable_bytes += size;
|
|
26
|
-
if (!managed && (/\.exe$/i.test(relative) || (/^[^/]+\.(go|json)$/i.test(relative) && !['config.json', 'runtime-manifest.json'].includes(relative)))) summary.warnings.push({ code: 'DG-
|
|
27
|
-
if (relative.startsWith('artifacts/') && !registered.has(relative)) summary.warnings.push({ code: 'DG-
|
|
26
|
+
if (!managed && (/\.exe$/i.test(relative) || (/^[^/]+\.(go|json)$/i.test(relative) && !['config.json', 'runtime-manifest.json'].includes(relative)))) summary.warnings.push({ code: 'DG-PROJECT-UNKNOWN-FILE', path: relative, size });
|
|
27
|
+
if (relative.startsWith('artifacts/') && !registered.has(relative)) summary.warnings.push({ code: 'DG-PROJECT-UNREGISTERED-ARTIFACT', path: relative, size });
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
visit(root); return summary;
|
|
31
31
|
}
|
|
32
32
|
function directorySize(directory) { let total = 0; const visit = (current) => { for (const item of fs.readdirSync(current, { withFileTypes: true })) { const absolute = path.join(current, item.name); if (item.isDirectory()) visit(absolute); else if (item.isFile()) total += fs.statSync(absolute).size; if (total > 50 * 1024 * 1024) return; } }; visit(directory); return total; }
|
|
33
|
-
module.exports = {
|
|
33
|
+
module.exports = { projectHealth };
|
package/src/projectMap.js
CHANGED
|
@@ -110,7 +110,7 @@ function buildProjectMap(projectDir) {
|
|
|
110
110
|
docs,
|
|
111
111
|
pageGroups: collectGroups(pages),
|
|
112
112
|
routeRefs: Object.fromEntries([...routeRefs].map(([route, sources]) => [route, [...sources]])),
|
|
113
|
-
remote_only: ['db_meta', '
|
|
113
|
+
remote_only: ['db_meta', 'models', 'prompts', 'plugins', 'knowledge', 'memory', 'agents', 'custom_services', 'system_config', 'roles', 'users', 'doc_categories'],
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
package/src/runtimeFiles.js
CHANGED
|
@@ -8,7 +8,7 @@ const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
|
8
8
|
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
9
9
|
function draftgoRoot(projectDir) { return path.resolve(projectDir, '.draftgo'); }
|
|
10
10
|
function manifestPath(projectDir) { return path.join(draftgoRoot(projectDir), 'runtime-manifest.json'); }
|
|
11
|
-
function inside(root, target) { const relation = path.relative(root, target); return relation && !relation.startsWith('..') && !path.isAbsolute(relation); }
|
|
11
|
+
function inside(root, target) { const relation = path.relative(path.resolve(root), path.resolve(target)); return relation && !relation.startsWith('..') && !path.isAbsolute(relation); }
|
|
12
12
|
function load(projectDir) {
|
|
13
13
|
const file = manifestPath(projectDir);
|
|
14
14
|
if (!fs.existsSync(file)) return { schema_version: SCHEMA_VERSION, entries: [] };
|
|
@@ -36,6 +36,7 @@ function save(projectDir, value) {
|
|
|
36
36
|
function register(projectDir, file, type, command, options = {}) {
|
|
37
37
|
const root = draftgoRoot(projectDir); const absolute = path.resolve(file);
|
|
38
38
|
if (!inside(root, absolute)) throw new Error('Managed runtime files must stay inside .draftgo.');
|
|
39
|
+
if (fs.existsSync(absolute) && !inside(fs.realpathSync(root), fs.realpathSync(absolute))) throw new Error('Managed runtime file escapes .draftgo.');
|
|
39
40
|
const relative = path.relative(root, absolute).replace(/\\/g, '/'); const value = load(projectDir);
|
|
40
41
|
const entry = { path: relative, type: String(type), command: String(command), created_at: new Date().toISOString(), cleanable: options.cleanable !== false };
|
|
41
42
|
value.entries = value.entries.filter((item) => item.path !== relative); value.entries.push(entry); save(projectDir, value); return entry;
|
package/src/worklog.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { dgDir } = require('./paths');
|
|
7
7
|
|
|
8
|
-
const STATUS = Object.freeze({ pending: '', active: '●', completed: '√' });
|
|
8
|
+
const STATUS = Object.freeze({ pending: '', active: '●', waiting: '?', completed: '√' });
|
|
9
9
|
const LOCK_WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
|
|
10
10
|
|
|
11
11
|
function formatLocalDate(value = new Date()) {
|
|
@@ -47,6 +47,7 @@ function normalizeDate(value, now = new Date()) {
|
|
|
47
47
|
function parseStatus(marker) {
|
|
48
48
|
if (marker === '●') return 'active';
|
|
49
49
|
if (marker === '√') return 'completed';
|
|
50
|
+
if (marker === '?') return 'waiting';
|
|
50
51
|
if (marker === '') return 'pending';
|
|
51
52
|
throw invalid(`unsupported status marker ${JSON.stringify(marker)}.`);
|
|
52
53
|
}
|
|
@@ -72,7 +73,7 @@ function parseWorklog(source) {
|
|
|
72
73
|
cursor += 1;
|
|
73
74
|
const entries = [];
|
|
74
75
|
while (cursor < lines.length && lines[cursor] !== '') {
|
|
75
|
-
const itemMatch = /^([1-9]\d*)\.\s+\[\s*(
|
|
76
|
+
const itemMatch = /^([1-9]\d*)\.\s+\[\s*(●|√|\?)?\s*\]\s+(.+)$/.exec(lines[cursor]);
|
|
76
77
|
if (!itemMatch) throw invalid(`invalid item at line ${cursor + 1}.`);
|
|
77
78
|
const number = Number(itemMatch[1]);
|
|
78
79
|
if (!Number.isSafeInteger(number) || number !== entries.length + 1) {
|