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.
Files changed (88) hide show
  1. package/README.md +23 -37
  2. package/package.json +3 -5
  3. package/resources/skill/SKILL.md +9 -5
  4. package/resources/skill/manifest.json +2 -5
  5. package/resources/skill/references/ai.md +41 -0
  6. package/resources/skill/references/app-api.md +2 -50
  7. package/resources/skill/references/architecture.md +1 -1
  8. package/resources/skill/references/chat-sdk.md +29 -37
  9. package/resources/skill/references/checkout.md +4 -4
  10. package/resources/skill/references/data.md +0 -46
  11. package/resources/skill/references/delivery.md +3 -3
  12. package/resources/skill/references/diagnostics.md +10 -11
  13. package/resources/skill/references/frontend.md +23 -20
  14. package/resources/skill/references/mcp.md +4 -14
  15. package/resources/skill/references/methods.md +15 -68
  16. package/resources/skill/references/modules.md +23 -44
  17. package/resources/skill/references/runtime.md +3 -20
  18. package/resources/skill/story/SKILL.md +2 -2
  19. package/src/apiContractCache.js +14 -6
  20. package/src/cli.js +0 -7
  21. package/src/commandRegistry.js +0 -6
  22. package/src/commands/api.js +87 -17
  23. package/src/commands/apiKey.js +2 -6
  24. package/src/commands/autoPush.js +15 -51
  25. package/src/commands/capabilities.js +22 -15
  26. package/src/commands/check.js +19 -53
  27. package/src/commands/checkout.js +1 -4
  28. package/src/commands/clean.js +1 -1
  29. package/src/commands/commit.js +1 -4
  30. package/src/commands/components.js +12 -8
  31. package/src/commands/conflict.js +4 -6
  32. package/src/commands/conflicts.js +1 -2
  33. package/src/commands/connect.js +0 -8
  34. package/src/commands/delete.js +15 -11
  35. package/src/commands/deploy.js +64 -26
  36. package/src/commands/diff.js +1 -4
  37. package/src/commands/group.js +2 -3
  38. package/src/commands/help.js +22 -43
  39. package/src/commands/init.js +13 -6
  40. package/src/commands/local.js +4 -1
  41. package/src/commands/map.js +138 -23
  42. package/src/commands/reconcile.js +1 -15
  43. package/src/commands/role.js +1 -2
  44. package/src/commands/status.js +12 -40
  45. package/src/commands/update.js +18 -24
  46. package/src/commands/verify.js +8 -7
  47. package/src/commands/worklog.js +11 -5
  48. package/src/contractCompatibility.js +10 -2
  49. package/src/localRuntime/compose.js +41 -27
  50. package/src/localRuntime/detect.js +6 -6
  51. package/src/localRuntime/index.js +47 -47
  52. package/src/localRuntime/services.js +2 -39
  53. package/src/mcp/client.js +99 -134
  54. package/src/mcp/parallel.js +25 -2
  55. package/src/mcp/protocol.js +38 -9
  56. package/src/mcp/tools.js +10 -19
  57. package/src/projectConfig.js +1 -4
  58. package/src/{workspaceHealth.js → projectHealth.js} +5 -5
  59. package/src/projectMap.js +1 -1
  60. package/src/runtimeFiles.js +2 -1
  61. package/src/worklog.js +3 -2
  62. package/src/worktree/backend.js +127 -15
  63. package/src/worktree/index.js +64 -22
  64. package/src/worktree/locks.js +52 -0
  65. package/src/worktree/manifest.js +18 -4
  66. package/src/worktree/status.js +4 -2
  67. package/resources/custom-service-sdk/ai.go +0 -520
  68. package/resources/custom-service-sdk/ai_test.go +0 -156
  69. package/resources/custom-service-sdk/auth_test.go +0 -56
  70. package/resources/custom-service-sdk/billing.go +0 -596
  71. package/resources/custom-service-sdk/billing_test.go +0 -150
  72. package/resources/custom-service-sdk/go.mod +0 -3
  73. package/resources/custom-service-sdk/manifest.json +0 -77
  74. package/resources/custom-service-sdk/platform.go +0 -352
  75. package/resources/custom-service-sdk/platform_logger_test.go +0 -24
  76. package/resources/custom-service-sdk/registration_test.go +0 -39
  77. package/resources/custom-service-sdk/resources.go +0 -247
  78. package/resources/custom-service-sdk/resources_billing_test.go +0 -115
  79. package/resources/custom-service-sdk/resources_files_test.go +0 -57
  80. package/resources/custom-service-sdk/resources_scope_test.go +0 -92
  81. package/resources/custom-service-sdk/sdk.go +0 -209
  82. package/resources/skill/references/aihub.md +0 -116
  83. package/resources/skill/references/custom-services.md +0 -201
  84. package/src/commands/customService.js +0 -95
  85. package/src/commands/dataRange.js +0 -33
  86. package/src/commands/grant.js +0 -29
  87. package/src/commands/space.js +0 -41
  88. 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
- 'draftgo_project_overview',
22
- 'draftgo_resource_list',
23
- 'draftgo_resource_search',
24
- 'draftgo_resource_get_metadata',
25
- 'draftgo_resource_read_fragment',
26
- 'draftgo_api_search',
27
- 'draftgo_api_describe',
28
- 'draftgo_api_call',
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: 'project', limit: 1 };
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
- function findDBMetaListOperation(result) {
49
- const value = diagnosticData(result);
50
- const items = value && Array.isArray(value.items) ? value.items : [];
51
- return items.find((operation) => operation
52
- && String(operation.method || '').toUpperCase() === 'GET'
53
- && String(operation.resource_type || '').toLowerCase() === 'db_meta'
54
- && String(operation.path || operation.path_template || '') === '/api/db-meta'
55
- && operation.destructive !== true) || null;
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
- return name === candidate || name.endsWith(`.${candidate}`)
180
- || name.endsWith(`/${candidate}`) || name.endsWith(`:${candidate}`);
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
- let defaultPlan = hasDiagnosticOverrides ? [{
457
- label: overrideCanonical,
458
- canonical: overrideCanonical,
459
- tool: firstSafeTool,
460
- args: diagnosticArguments(overrideCanonical),
461
- }] : [
462
- { label: 'project', canonical: 'draftgo_project_overview', tool: byCanonical.draftgo_project_overview, args: {} },
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
- let discovery = tested.find((entry) => entry.label === 'api:db_meta');
506
- let operation = discovery && findDBMetaListOperation(discovery.result);
507
- const seenCursors = new Set();
508
- const maxPages = Number(options.maxPages || 100);
509
- let page = 1;
510
- while (!operation) {
511
- const cursor = diagnosticNextCursor(discovery && discovery.result);
512
- if (cursor == null) break;
513
- if (seenCursors.has(cursor)) {
514
- throw new McpRpcError('DraftGo api_search repeated a db_meta cursor.');
515
- }
516
- seenCursors.add(cursor);
517
- if (page >= maxPages) {
518
- throw new McpRpcError(`DraftGo api_search exceeded ${maxPages} db_meta pages.`);
519
- }
520
- page += 1;
521
- [discovery] = await runPlan([{
522
- label: `api:db_meta:page-${page}`,
523
- canonical: 'draftgo_api_search',
524
- tool: byCanonical.draftgo_api_search,
525
- args: { resource_type: 'db_meta', limit: 100, cursor },
526
- }], tested.length);
527
- tested.push(discovery);
528
- operation = findDBMetaListOperation(discovery.result);
529
- }
530
- if (!operation || !operation.operation_id) {
531
- throw new McpRpcError('DraftGo MCP exposes no read-only GET /api/db-meta operation.', {
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
- diagnosticNextCursor,
587
- findDBMetaListOperation,
551
+ findConnectionProbeOperation,
552
+ assertSafeConnectionProbe,
588
553
  isSessionInvalidError,
589
554
  redactValue,
590
555
  testConnection,
@@ -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 Promise.all(taskFactories.map((task) => task(taskOptions)));
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 };
@@ -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
- const text = await response.text();
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 found = tools.find((tool) => tool && toolNameMatches(String(tool.name || ''), expectedName));
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, configuredScope };
51
+ module.exports = { TOOL_NAMES, TOOL_ALIASES, openToolSession, callStructured };
@@ -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 = { ...config, server, token, path: file };
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 workspaceHealth(projectDir) {
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-WORKSPACE-LARGE-BUILD', path: relative });
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-WORKSPACE-UNKNOWN-FILE', path: relative, size });
27
- if (relative.startsWith('artifacts/') && !registered.has(relative)) summary.warnings.push({ code: 'DG-WORKSPACE-UNREGISTERED-ARTIFACT', path: relative, 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-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 = { workspaceHealth };
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', 'aihub', 'system_config', 'roles', 'users', 'doc_categories', 'custom_services'],
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
 
@@ -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*(●|√)?\s*\]\s+(.+)$/.exec(lines[cursor]);
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) {