draftgo-cli 4.0.25 → 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 (87) hide show
  1. package/README.md +21 -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 +19 -41
  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/verify.js +8 -7
  46. package/src/commands/worklog.js +11 -5
  47. package/src/contractCompatibility.js +10 -2
  48. package/src/localRuntime/compose.js +41 -27
  49. package/src/localRuntime/detect.js +6 -6
  50. package/src/localRuntime/index.js +47 -47
  51. package/src/localRuntime/services.js +2 -39
  52. package/src/mcp/client.js +99 -134
  53. package/src/mcp/parallel.js +25 -2
  54. package/src/mcp/protocol.js +38 -9
  55. package/src/mcp/tools.js +10 -19
  56. package/src/projectConfig.js +1 -4
  57. package/src/{workspaceHealth.js → projectHealth.js} +5 -5
  58. package/src/projectMap.js +1 -1
  59. package/src/runtimeFiles.js +2 -1
  60. package/src/worklog.js +3 -2
  61. package/src/worktree/backend.js +127 -15
  62. package/src/worktree/index.js +64 -22
  63. package/src/worktree/locks.js +52 -0
  64. package/src/worktree/manifest.js +18 -4
  65. package/src/worktree/status.js +4 -2
  66. package/resources/custom-service-sdk/ai.go +0 -520
  67. package/resources/custom-service-sdk/ai_test.go +0 -156
  68. package/resources/custom-service-sdk/auth_test.go +0 -56
  69. package/resources/custom-service-sdk/billing.go +0 -596
  70. package/resources/custom-service-sdk/billing_test.go +0 -150
  71. package/resources/custom-service-sdk/go.mod +0 -3
  72. package/resources/custom-service-sdk/manifest.json +0 -77
  73. package/resources/custom-service-sdk/platform.go +0 -352
  74. package/resources/custom-service-sdk/platform_logger_test.go +0 -24
  75. package/resources/custom-service-sdk/registration_test.go +0 -39
  76. package/resources/custom-service-sdk/resources.go +0 -247
  77. package/resources/custom-service-sdk/resources_billing_test.go +0 -115
  78. package/resources/custom-service-sdk/resources_files_test.go +0 -57
  79. package/resources/custom-service-sdk/resources_scope_test.go +0 -92
  80. package/resources/custom-service-sdk/sdk.go +0 -209
  81. package/resources/skill/references/aihub.md +0 -116
  82. package/resources/skill/references/custom-services.md +0 -201
  83. package/src/commands/customService.js +0 -95
  84. package/src/commands/dataRange.js +0 -33
  85. package/src/commands/grant.js +0 -29
  86. package/src/commands/space.js +0 -41
  87. package/src/customServices.js +0 -484
@@ -1,16 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
+ const path = require('path');
4
5
  const log = require('../logger');
5
6
  const { askRequired, askPassword, confirm } = require('../prompt');
6
7
  const { probePort, probeHttp, detectDocker } = require('./detect');
7
8
  const compose = require('./compose');
8
9
  const { ensureDatabase, testConnection, describeClient } = require('./mysqlClient');
9
- const { defaults, portOpen, probeRedis, probeQdrant, startService } = require('./services');
10
+ const { defaults, portOpen, probeQdrant, startService } = require('./services');
10
11
  const { writeProjectConfig } = require('../projectConfig');
11
- const { appendGitignoreLine } = require('../fsx');
12
+ const { appendGitignoreLine, exists, readText } = require('../fsx');
12
13
 
13
- const DEFAULT_APP_PORT = 3000;
14
+ const DEFAULT_APP_PORT = 7777;
14
15
 
15
16
  async function promptMysqlConnection() {
16
17
  while (true) {
@@ -25,10 +26,11 @@ async function promptMysqlConnection() {
25
26
  }
26
27
  }
27
28
 
28
- async function ensureProjectDatabase(conn, database) {
29
+ async function ensureProjectDatabase(conn, database, interactive = true) {
29
30
  while (true) {
30
31
  const result = await ensureDatabase({ ...conn, database }, {
31
32
  rootPromptFn: async () => {
33
+ if (!interactive) return null;
32
34
  log.warn(` ${conn.user} cannot create database ${database}.`);
33
35
  if (!await confirm(' Create it with another MySQL account?', { default: true })) return null;
34
36
  return {
@@ -39,13 +41,14 @@ async function ensureProjectDatabase(conn, database) {
39
41
  });
40
42
  if (result.ok) return { ...conn, database };
41
43
  log.err(` Database setup failed: ${(result.detail || result.reason || 'unknown error').split('\n')[0]}`);
44
+ if (!interactive) throw new Error('Database setup needs credentials. Run `draftgo local setup` in a terminal to continue.');
42
45
  const retry = await confirm(' Re-enter MySQL connection settings?', { default: true });
43
46
  if (!retry) throw new Error('MySQL database was not prepared.');
44
47
  conn = await promptMysqlConnection();
45
48
  }
46
49
  }
47
50
 
48
- async function planMysql(docker, defaultDatabase) {
51
+ async function planMysql(docker, defaultDatabase, interactive = true) {
49
52
  log.step('Checking MySQL');
50
53
  let conn = { ...defaults.mysql };
51
54
  if (!await portOpen(conn.host, conn.port)) {
@@ -54,40 +57,15 @@ async function planMysql(docker, defaultDatabase) {
54
57
  }
55
58
  if (!testConnection(conn).ok) {
56
59
  log.warn(' Default MySQL credentials were not accepted.');
60
+ if (!interactive) throw new Error('MySQL needs credentials. Run `draftgo local setup` in a terminal to continue.');
57
61
  conn = await promptMysqlConnection();
58
62
  }
59
- const database = await askRequired(' Project database name', { default: defaultDatabase });
60
- const out = await ensureProjectDatabase(conn, database);
63
+ const database = interactive ? await askRequired(' Project database name', { default: defaultDatabase }) : defaultDatabase;
64
+ const out = await ensureProjectDatabase(conn, database, interactive);
61
65
  log.ok(` MySQL ready: ${out.host}:${out.port}/${out.database} (${describeClient()})`);
62
66
  return out;
63
67
  }
64
68
 
65
- async function promptRedisConnection() {
66
- while (true) {
67
- const host = await askRequired(' Redis host', { default: '127.0.0.1' });
68
- const port = Number(await askRequired(' Redis port', { default: '6379' }));
69
- const password = await askPassword(' Redis password (empty for none)', { default: '' });
70
- const conn = { host, port, password };
71
- if (await probeRedis(conn)) return conn;
72
- log.err(' Redis PING failed. Check the address and password.');
73
- }
74
- }
75
-
76
- async function planRedis(docker) {
77
- log.step('Checking Redis');
78
- let conn = { ...defaults.redis };
79
- if (!await portOpen(conn.host, conn.port)) {
80
- log.dim(' Redis is not running locally; starting the shared local service.');
81
- if (!startService(docker, 'redis')) throw new Error('Unable to start shared Redis.');
82
- }
83
- if (!await probeRedis(conn)) {
84
- log.warn(' Default passwordless Redis PING failed.');
85
- conn = await promptRedisConnection();
86
- }
87
- log.ok(` Redis ready: ${conn.host}:${conn.port}`);
88
- return conn;
89
- }
90
-
91
69
  async function promptQdrantConnection() {
92
70
  while (true) {
93
71
  const host = await askRequired(' Qdrant host', { default: '127.0.0.1' });
@@ -99,7 +77,7 @@ async function promptQdrantConnection() {
99
77
  }
100
78
  }
101
79
 
102
- async function planQdrant(docker) {
80
+ async function planQdrant(docker, interactive = true) {
103
81
  log.step('Checking Qdrant');
104
82
  let conn = { ...defaults.qdrant };
105
83
  if (!await portOpen(conn.host, conn.port)) {
@@ -108,15 +86,22 @@ async function planQdrant(docker) {
108
86
  }
109
87
  if (!await probeQdrant(conn)) {
110
88
  log.warn(' Default unauthenticated Qdrant REST health check failed.');
89
+ if (!interactive) throw new Error('Qdrant needs credentials. Run `draftgo local setup` in a terminal to continue.');
111
90
  conn = await promptQdrantConnection();
112
91
  }
113
92
  log.ok(` Qdrant ready: ${conn.host}:${conn.port}`);
114
93
  return conn;
115
94
  }
116
95
 
117
- async function pickAppPort() {
96
+ async function pickAppPort(interactive = true) {
97
+ if (!interactive) {
98
+ for (let port = DEFAULT_APP_PORT; port < DEFAULT_APP_PORT + 100; port += 1) {
99
+ if (!await probePort('127.0.0.1', port)) return port;
100
+ }
101
+ throw new Error('No free local port found. Run `draftgo local setup` in a terminal to select a port.');
102
+ }
118
103
  const taken = await probePort('127.0.0.1', DEFAULT_APP_PORT);
119
- const suggested = taken ? '3001' : String(DEFAULT_APP_PORT);
104
+ const suggested = String(taken ? DEFAULT_APP_PORT + 1 : DEFAULT_APP_PORT);
120
105
  while (true) {
121
106
  const port = Number(await askRequired(' DraftGo host port', { default: suggested }));
122
107
  if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
@@ -141,7 +126,7 @@ function composeUp(docker, dir) {
141
126
  }
142
127
 
143
128
  async function waitForApp(appPort) {
144
- const ok = await probeHttp(`http://127.0.0.1:${appPort}/`, { totalMs: 120000, intervalMs: 2000 });
129
+ const ok = await probeHttp(`http://127.0.0.1:${appPort}/api/system/health`, { totalMs: 120000, intervalMs: 2000 });
145
130
  if (!ok) log.err(' Timed out waiting for DraftGo. Run `draftgo local logs` to inspect the app.');
146
131
  return ok;
147
132
  }
@@ -174,35 +159,50 @@ async function finishMcpSetup(projectDir, mcpCommands) {
174
159
  return setupOk && testOk;
175
160
  }
176
161
 
177
- async function runWizard(projectDir, { yes = false, mcpCommands } = {}) {
162
+ async function runWizard(projectDir, { yes = false, mcpCommands, interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !yes, params = {} } = {}) {
178
163
  log.title('draftgo local setup');
164
+ if (!yes && !interactive) throw new Error('Run `draftgo local setup --yes` for unattended setup, or use a terminal for guided setup.');
179
165
  if (!yes && !await confirm('Continue with local DraftGo setup?', { default: true })) return 0;
180
166
 
181
167
  const docker = detectDocker();
182
168
  if (!docker.ok) {
183
169
  throw new Error(docker.reason === 'compose-missing'
184
- ? 'Docker is installed but Docker Compose is unavailable.'
185
- : 'Docker is required to start missing local dependencies.');
170
+ ? 'Install Docker Compose: https://docs.docker.com/compose/install/ then retry `draftgo local setup`.'
171
+ : docker.reason === 'docker-not-running'
172
+ ? 'Start Docker Desktop or the Docker daemon, then retry `draftgo local setup`. Installed Skills are preserved.'
173
+ : 'Install Docker: https://docs.docker.com/get-started/get-docker/ then start it and retry `draftgo local setup`. Installed Skills are preserved.');
186
174
  }
187
175
 
188
- const appPort = await pickAppPort();
189
- const projectName = await pickProjectName();
190
- const mysql = await planMysql(docker, projectName);
191
- const redis = await planRedis(docker);
192
- const qdrant = await planQdrant(docker);
176
+ if (exists(compose.composePath(projectDir))) {
177
+ if (readText(compose.composePath(projectDir)).includes('./data/storage:/app/data/storage')) {
178
+ throw new Error('Existing runtime uses legacy storage. Back up and migrate it to final DraftGo before setup; Compose and data were preserved.');
179
+ }
180
+ log.dim(' Existing project runtime preserved. Use `draftgo local start` to resume it, then `draftgo connect` to bind your API Key.');
181
+ return 0;
182
+ }
183
+ const appPort = params.appPort || await pickAppPort(interactive);
184
+ const projectName = params.projectName || (interactive ? await pickProjectName() : compose.sanitizeProjectName(path.basename(projectDir)));
185
+ const mysql = params.mysql || await planMysql(docker, projectName, interactive);
186
+ const qdrant = params.qdrant === false ? null : (params.qdrant || await planQdrant(docker, interactive));
193
187
 
194
- const out = compose.generate(projectDir, { projectName, appPort, mysql, redis, qdrant });
188
+ const out = compose.generate(projectDir, { projectName, appPort, mysql, qdrant, image: params.image });
195
189
  appendGitignoreLine(projectDir, '.draftgo/docker/');
196
190
  if (!composeUp(docker, out.dir) || !await waitForApp(appPort)) return 1;
197
191
 
198
192
  const url = `http://localhost:${appPort}`;
193
+ log.ok(`DraftGo runtime is ready at ${url}`);
194
+ log.dim(' Open the site, register or sign in, and obtain your personal API Key.');
195
+ if (!interactive) {
196
+ log.dim(` Connection pending. Run \`draftgo connect --server ${url}\` in a terminal to enter your API Key.`);
197
+ return 0;
198
+ }
199
199
  let token = '';
200
200
  while (token.length < 10) {
201
201
  token = String(await askPassword('Paste a DraftGo user API Key')).trim();
202
202
  if (token.length < 10) log.err(' API Key appears too short. Please try again.');
203
203
  }
204
204
  const cfgPath = writeProjectConfig(projectDir, url, token);
205
- await finishMcpSetup(projectDir, mcpCommands);
205
+ if (!await finishMcpSetup(projectDir, mcpCommands)) return 1;
206
206
  log.ok(`DraftGo is ready at ${url}`);
207
207
  log.dim(`Project config: ${cfgPath}`);
208
208
  return 0;
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // Shared local runtime dependencies live outside individual projects. Project Compose
4
- // files only start DraftGo itself and connect back to these loopback services.
4
+ // files run DraftGo and isolated Redis, then connect back to MySQL and Qdrant.
5
5
  const net = require('net');
6
6
  const os = require('os');
7
7
  const path = require('path');
@@ -16,17 +16,15 @@ const MYSQL_INIT_FILE = path.join(SERVICES_DIR, 'mysql-init.sql');
16
16
 
17
17
  const defaults = {
18
18
  mysql: { host: '127.0.0.1', port: 3306, user: 'draftgo', password: 'draftgo' },
19
- redis: { host: '127.0.0.1', port: 6379, password: '' },
20
19
  qdrant: { host: '127.0.0.1', port: 26333, apiKey: '' },
21
20
  };
22
21
 
23
22
  function writeServiceFiles() {
24
23
  ensureDir(SERVICES_DIR);
25
24
  ensureDir(path.join(SERVICES_DIR, 'data', 'mysql'));
26
- ensureDir(path.join(SERVICES_DIR, 'data', 'redis'));
27
25
  ensureDir(path.join(SERVICES_DIR, 'data', 'qdrant'));
28
26
  writeText(MYSQL_INIT_FILE, [
29
- "GRANT ALL PRIVILEGES ON *.* TO 'draftgo'@'%' WITH GRANT OPTION;",
27
+ "GRANT ALL PRIVILEGES ON `draftgo\\_%`.* TO 'draftgo'@'%';",
30
28
  'FLUSH PRIVILEGES;',
31
29
  '',
32
30
  ].join('\n'));
@@ -54,20 +52,6 @@ function writeServiceFiles() {
54
52
  ' timeout: 5s',
55
53
  ' retries: 24',
56
54
  ' start_period: 20s',
57
- ' redis:',
58
- ' image: redis:7.4-alpine',
59
- ' container_name: draftgo-local-redis',
60
- ' restart: unless-stopped',
61
- ' command: ["redis-server", "--appendonly", "yes"]',
62
- ' ports:',
63
- ' - "127.0.0.1:6379:6379"',
64
- ' volumes:',
65
- ' - ./data/redis:/data',
66
- ' healthcheck:',
67
- ' test: ["CMD", "redis-cli", "ping"]',
68
- ' interval: 5s',
69
- ' timeout: 5s',
70
- ' retries: 20',
71
55
  ' qdrant:',
72
56
  ' image: qdrant/qdrant:v1.19.0',
73
57
  ' container_name: draftgo-local-qdrant',
@@ -97,26 +81,6 @@ function portOpen(host, port, timeoutMs = 800) {
97
81
  });
98
82
  }
99
83
 
100
- function probeRedis({ host, port, password = '' }) {
101
- return new Promise((resolve) => {
102
- const socket = net.createConnection({ host, port });
103
- let response = '';
104
- const finish = (ok) => { socket.destroy(); resolve(ok); };
105
- socket.setTimeout(2000);
106
- socket.once('connect', () => {
107
- const auth = password ? `*2\r\n$4\r\nAUTH\r\n$${Buffer.byteLength(password)}\r\n${password}\r\n` : '';
108
- socket.write(`${auth}*1\r\n$4\r\nPING\r\n`);
109
- });
110
- socket.on('data', (chunk) => {
111
- response += chunk.toString('utf8');
112
- if (response.includes('+PONG')) finish(true);
113
- if (response.includes('-NOAUTH') || response.includes('-WRONGPASS') || response.includes('-ERR')) finish(false);
114
- });
115
- socket.once('timeout', () => finish(false));
116
- socket.once('error', () => finish(false));
117
- });
118
- }
119
-
120
84
  function probeQdrant({ host, port, apiKey = '' }) {
121
85
  return new Promise((resolve) => {
122
86
  const client = String(host || '').startsWith('https://') ? https : http;
@@ -148,7 +112,6 @@ module.exports = {
148
112
  SERVICES_DIR,
149
113
  defaults,
150
114
  portOpen,
151
- probeRedis,
152
115
  probeQdrant,
153
116
  startService,
154
117
  };
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 };