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.
- package/README.md +21 -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 +19 -41
- 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/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
|
@@ -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,
|
|
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 =
|
|
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 ?
|
|
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}
|
|
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
|
|
185
|
-
:
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
'
|
|
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 };
|