draftgo-cli 3.0.41 → 3.0.44
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 +8 -2
- package/package.json +1 -1
- package/resources/skill/SKILL.md +1 -1
- package/resources/skill/manifest.json +1 -1
- package/resources/skill/references/mcp.md +2 -2
- package/src/cli.js +1 -0
- package/src/commands/connect.js +21 -6
- package/src/commands/context.js +1 -15
- package/src/commands/help.js +6 -1
- package/src/commands/map.js +64 -15
- package/src/commands/mcp.js +3 -2
- package/src/commands/verifyUi.js +47 -5
- package/src/context/index.js +241 -33
- package/src/mcp/client.js +170 -14
- package/src/mcp/parallel.js +31 -0
- package/src/mcp/protocol.js +2 -1
- package/src/projectConfig.js +39 -5
- package/src/timeout.js +18 -0
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ DraftGo Skill 不能被 MCP 替代。根 `SKILL.md` 会在 Skill 触发时自动
|
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
`context` 先验证并逐字提取当前 CLI 随包 Reference 的固定任务章节,再用同一 MCP 会话并行查询项目、资源、API 和 `db_meta` 数据结构;它不做摘要,也不限制后续 MCP 调用。输出中的 `reference_bundle` 会报告 CLI bundle 与项目已安装 Skill 的版本及同步状态,不同步时先运行 `draftgo update`。页面或导航任务使用 `frontend`,数据、自定义服务、AIHub、正文和项目清点分别可用 `data`、`custom-service`、`aihub`、`content`、`project`。`mcp test` 是连接诊断,不是每次资源查询的前置步骤。
|
|
51
|
+
当前 context JSON 使用 `schema_version: "1.1"`;`live.resources` 和 `live.api` 以带来源的 `pages` 序列保留批量原始响应,`live.data_structures` 保留 db_meta 的 registry、describe 契约及完整分页读取结果。
|
|
51
52
|
|
|
52
53
|
只回答无需实时状态的本地规则问题,或需要 context 路由之外的专项资料时,Agent 继续按根 Skill 表格手工补读。静态资源须先区分:平台内置目录由 `frontend.md` 说明,指定页面依赖经 MCP 定位、checkout 后本地搜索,服务器全量文件不能凭现有 Skill、MCP 摘要或 checkout 声称已枚举。
|
|
53
54
|
|
|
@@ -69,6 +70,8 @@ draftgo connect codex # 输入 server 和 SAT,验证 MCP,并配置 Co
|
|
|
69
70
|
draftgo connect codex --server https://draftgo.example --token "$DRAFTGO_SAT"
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
`--server` 始终表示 DraftGo 基础地址。直接连接 MCP endpoint 时使用 `--mcp-url https://draftgo.example/mcp`;CLI 会显式保存 `mcp_url`,若未同时传 `--server`,则从标准 `/mcp` 后缀推导基础地址,避免猜测已有 base URL 的语义。
|
|
74
|
+
|
|
72
75
|
`connect` 会先验证 SAT、MCP `initialize`、`tools/list` 和关键 `tools/call`,成功后写入项目私有配置 `.draftgo/config.json`,并尝试为指定或自动检测到的宿主执行 MCP setup。它不会 pull 页面、导航、文档或任何其他业务资源。
|
|
73
76
|
|
|
74
77
|
远端暂不可用时,只有显式传入 `--allow-offline` 才会保存配置;恢复后运行 `draftgo mcp test`。使用 `--no-mcp-setup` 可跳过宿主配置。
|
|
@@ -89,7 +92,7 @@ SAT 只从 `.draftgo/config.json` 读取。宿主 MCP 配置、命令输出和
|
|
|
89
92
|
```bash
|
|
90
93
|
draftgo mcp setup [target...] # 写入或合并项目级 MCP 配置
|
|
91
94
|
draftgo mcp status [target...] # 检查配置是否存在且不含敏感连接参数
|
|
92
|
-
draftgo mcp test # 验证 initialize、tools/list
|
|
95
|
+
draftgo mcp test # 验证 initialize、tools/list、项目/正文/API 三条调用路径
|
|
93
96
|
draftgo mcp serve # 启动 stdio -> 远端 Streamable HTTP bridge
|
|
94
97
|
```
|
|
95
98
|
|
|
@@ -198,7 +201,7 @@ draftgo changelog add "完成文档管理系统及角色权限"
|
|
|
198
201
|
| 命令 | 说明 |
|
|
199
202
|
|---|---|
|
|
200
203
|
| `draftgo context --task <task> --output json` | 精确读取任务 Reference 章节,并复用一个 MCP 会话并行查询项目、资源、API 和数据结构;保留原始结果及来源。 |
|
|
201
|
-
| `draftgo map` |
|
|
204
|
+
| `draftgo map` | 并行读取 pages、navigations、docs/articles 元数据,再叠加本地 checkout 状态;`--type` 可限制一种类型。 |
|
|
202
205
|
| `draftgo checkout <pages|nav|docs> <id...>` | 下载完整正文和 base,校验 hash 后原子落盘;`--force` 可显式覆盖已修改的本地正文。 |
|
|
203
206
|
| `draftgo check` | 只检查 checkout manifest、worktree 文件、base/hash、扩展名、HTML 结构和已 checkout 导航引用。 |
|
|
204
207
|
| `draftgo diff <pages|nav|docs> <id>` | 显示 checkout base 与当前本地正文的差异。 |
|
|
@@ -338,6 +341,7 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
|
|
|
338
341
|
- `--project <dir>`:操作指定项目目录,默认当前目录。
|
|
339
342
|
- `--output json`:为 context、map、check、checkout、commit、diff、conflicts、conflict、api 或 delete 输出 JSON。
|
|
340
343
|
- `--task <task>`:选择 context 路由;支持 frontend、data、custom-service、aihub、content、project。
|
|
344
|
+
- `--type <type>`:限制 `map` 查询一种正文资源类型;支持 pages、nav/navigations、docs/articles 及其别名。
|
|
341
345
|
- `--strict`:check/deploy/auto-push 将 warnings 视为失败。
|
|
342
346
|
- `--force`:init 覆盖已有 Skill;checkout 显式覆盖已修改的本地正文。update 始终刷新目标 Skill。
|
|
343
347
|
- `--purge`:uninstall 时同时删除整个 `.draftgo/` 运行时目录。
|
|
@@ -345,9 +349,11 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
|
|
|
345
349
|
- `--connect` / `--local-dev` / `--no-setup`:控制 init 安装 Skill 后的基座处理流程。
|
|
346
350
|
- `--target <name,...>`:为 MCP setup/status 指定一个或多个宿主。
|
|
347
351
|
- `--server <url>` / `--token <sat>`:非交互 connect;不传时安全提示输入。
|
|
352
|
+
- `--mcp-url <url>`:为 connect 显式指定完整 MCP endpoint;标准 `/mcp` 可自动推导基础地址,自定义路径需同时传 `--server`。
|
|
348
353
|
- `--allow-offline` / `--no-mcp-setup`:允许 connect 在 MCP 暂不可用时保存,或跳过宿主配置。
|
|
349
354
|
- `--yes`:跳过支持该选项的交互确认。
|
|
350
355
|
- `--mobile-check auto|always|never`:控制 `verify-ui` 是否执行。
|
|
356
|
+
- `--token auto|never`:`verify-ui` 默认读取 `.draftgo/config.json`,对与 `server` 同源的地址自动附加 `token=<SAT>`;配置无 SAT、跨源地址或 `never` 模式均不附加。
|
|
351
357
|
- `--screenshot on-failure|always|never`:控制 UI 截图。
|
|
352
358
|
- `--delivery local|preview|deploy`:控制 deploy 行为。
|
|
353
359
|
- `--timeout <ms>`:控制 connect、MCP test 或 context 的远端请求超时。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "draftgo-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.44",
|
|
4
4
|
"description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro).",
|
|
5
5
|
"bin": {
|
|
6
6
|
"draftgo": "bin/draftgo.js"
|
package/resources/skill/SKILL.md
CHANGED
|
@@ -79,7 +79,7 @@ context 一次准备信息
|
|
|
79
79
|
### 页面、导航和文档正文
|
|
80
80
|
|
|
81
81
|
1. 运行 frontend 或 content context,并按需继续通过 MCP 定位资源、取得元数据。
|
|
82
|
-
2. Agent
|
|
82
|
+
2. Agent 根据任务自主设计;页面开发优先考虑 Tailwind CSS 作为基础样式方案,并按任务需要搭配现有组件库;对已确定的 pages/nav/docs 批量 checkout。
|
|
83
83
|
3. 按唯一资源 owner 读取、搜索和编辑 `.draftgo/worktree/` 正文;独立资源可并行。
|
|
84
84
|
4. 主 Agent 汇总回读后统一运行一次 `draftgo check`,页面布局或交互变化时再统一运行 `draftgo verify-ui`。
|
|
85
85
|
5. 用 `draftgo diff <type> <id>` 检查全部 base/local 差异,再运行对应的 `draftgo commit ...`。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": "1.0",
|
|
3
3
|
"id": "draftgo",
|
|
4
4
|
"name": "DraftGo 开发助手",
|
|
5
|
-
"version": "3.0.
|
|
5
|
+
"version": "3.0.44",
|
|
6
6
|
"entry": "SKILL.md",
|
|
7
7
|
"description": "以 context 聚合编排、Skill/reference 原文、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
|
|
8
8
|
"license": "MIT",
|
|
@@ -38,10 +38,10 @@ draftgo mcp serve
|
|
|
38
38
|
- `context` 是开发任务的一次性准备入口;不保存业务镜像,不传输完整正文。
|
|
39
39
|
- `setup` 写入项目级宿主配置,只替换 `draftgo` MCP 条目并保留其他配置。
|
|
40
40
|
- `status` 检查配置是否存在、格式是否有效,以及是否误写了凭据或远端 URL。
|
|
41
|
-
- `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list
|
|
41
|
+
- `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list、三类正文 resource_list,以及 db_meta 的 api_search、api_describe 和只读 api_call。
|
|
42
42
|
- `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。
|
|
43
43
|
|
|
44
|
-
`draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup
|
|
44
|
+
`draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;`--server` 表示基础地址,完整 MCP endpoint 使用 `--mcp-url` 显式传入。它不下载业务资源,
|
|
45
45
|
也不创建 pages、navigation、docs、db_meta 等本地镜像。底座仍在开发或暂不可达时,报告诊断结果即可,
|
|
46
46
|
不得回退到 `pull --all`。
|
|
47
47
|
|
package/src/cli.js
CHANGED
package/src/commands/connect.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../logger');
|
|
4
4
|
const { ask, askPassword } = require('../prompt');
|
|
5
|
-
const { normalizeServer, writeProjectConfig } = require('../projectConfig');
|
|
5
|
+
const { normalizeConnection, normalizeServer, writeProjectConfig } = require('../projectConfig');
|
|
6
6
|
const { testConnection } = require('../mcp/client');
|
|
7
7
|
const { redactText } = require('../mcp/protocol');
|
|
8
|
+
const { parseTimeout } = require('../timeout');
|
|
8
9
|
|
|
9
10
|
async function promptServer(defaultValue) {
|
|
10
11
|
while (true) {
|
|
@@ -28,19 +29,32 @@ async function promptToken() {
|
|
|
28
29
|
|
|
29
30
|
async function connect(projectDir, positional, flags = {}) {
|
|
30
31
|
log.title('draftgo connect');
|
|
31
|
-
const
|
|
32
|
+
const endpointConnection = flags['mcp-url']
|
|
33
|
+
? normalizeConnection(flags['mcp-url'])
|
|
34
|
+
: null;
|
|
35
|
+
if (endpointConnection && !endpointConnection.server && !flags.server) {
|
|
36
|
+
throw new Error('--server is required when --mcp-url does not use the standard /mcp path.');
|
|
37
|
+
}
|
|
38
|
+
const connection = {
|
|
39
|
+
server: flags.server
|
|
40
|
+
? normalizeServer(flags.server)
|
|
41
|
+
: endpointConnection && endpointConnection.server || await promptServer('https://'),
|
|
42
|
+
mcp_url: endpointConnection && endpointConnection.mcp_url || '',
|
|
43
|
+
};
|
|
44
|
+
const server = connection.server;
|
|
32
45
|
const token = flags.token ? String(flags.token).trim() : await promptToken();
|
|
33
46
|
if (!token) {
|
|
34
47
|
log.err('A non-empty SAT is required.');
|
|
35
48
|
return 1;
|
|
36
49
|
}
|
|
50
|
+
const timeoutMs = parseTimeout(flags.timeout);
|
|
37
51
|
|
|
38
52
|
log.step('Validating SAT and DraftGo MCP capabilities...');
|
|
39
53
|
try {
|
|
40
|
-
const diagnostic = await testConnection({ server, token }, {
|
|
41
|
-
timeoutMs
|
|
54
|
+
const diagnostic = await testConnection({ ...connection, server, token }, {
|
|
55
|
+
timeoutMs,
|
|
42
56
|
});
|
|
43
|
-
log.ok(`MCP ready (${diagnostic.tools.length} tools; tested ${diagnostic.
|
|
57
|
+
log.ok(`MCP ready (${diagnostic.tools.length} tools; tested ${diagnostic.testedCalls.join(', ')}).`);
|
|
44
58
|
} catch (error) {
|
|
45
59
|
const message = redactText(error && error.message ? error.message : error, [token]);
|
|
46
60
|
if (!flags['allow-offline']) {
|
|
@@ -52,7 +66,7 @@ async function connect(projectDir, positional, flags = {}) {
|
|
|
52
66
|
log.warn('Continuing only because --allow-offline was explicitly supplied.');
|
|
53
67
|
}
|
|
54
68
|
|
|
55
|
-
const configFile = writeProjectConfig(projectDir, server, token);
|
|
69
|
+
const configFile = writeProjectConfig(projectDir, server, token, { mcp_url: connection.mcp_url });
|
|
56
70
|
log.ok(`Project configuration written: ${configFile}`);
|
|
57
71
|
|
|
58
72
|
if (!flags['no-mcp-setup']) {
|
|
@@ -66,6 +80,7 @@ async function connect(projectDir, positional, flags = {}) {
|
|
|
66
80
|
|
|
67
81
|
log.title('Connected');
|
|
68
82
|
log.plain(` server: ${server}`);
|
|
83
|
+
if (connection.mcp_url) log.plain(` mcp endpoint: ${connection.mcp_url}`);
|
|
69
84
|
log.plain(` config: ${configFile}`);
|
|
70
85
|
log.dim(' No DraftGo business resources were downloaded. Use MCP discovery or checkout long content explicitly.');
|
|
71
86
|
return 0;
|
package/src/commands/context.js
CHANGED
|
@@ -2,21 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../logger');
|
|
4
4
|
const { collectContext, TASK_PROFILES } = require('../context');
|
|
5
|
-
|
|
6
|
-
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
7
|
-
|
|
8
|
-
function parseTimeout(value) {
|
|
9
|
-
if (value == null) return undefined;
|
|
10
|
-
const text = String(value).trim();
|
|
11
|
-
if (!/^\d+$/.test(text)) {
|
|
12
|
-
throw new TypeError('--timeout must be a positive integer in milliseconds.');
|
|
13
|
-
}
|
|
14
|
-
const timeoutMs = Number(text);
|
|
15
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
16
|
-
throw new TypeError(`--timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds.`);
|
|
17
|
-
}
|
|
18
|
-
return timeoutMs;
|
|
19
|
-
}
|
|
5
|
+
const { parseTimeout } = require('../timeout');
|
|
20
6
|
|
|
21
7
|
function printUsage() {
|
|
22
8
|
log.err('Usage: draftgo context --task <task> --output json');
|
package/src/commands/help.js
CHANGED
|
@@ -34,7 +34,7 @@ Usage:
|
|
|
34
34
|
downloads DraftGo business resources.
|
|
35
35
|
draftgo mcp setup [<target>...] Merge project-level stdio MCP configuration.
|
|
36
36
|
draftgo mcp status [<target>...] Check host configuration and secret safety.
|
|
37
|
-
draftgo mcp test Test
|
|
37
|
+
draftgo mcp test Test project, resource, and API MCP calls.
|
|
38
38
|
draftgo mcp serve Bridge local stdio to the remote /mcp endpoint.
|
|
39
39
|
|
|
40
40
|
draftgo context --task <task> Read exact task Reference sections, then reuse
|
|
@@ -88,6 +88,8 @@ Important flags:
|
|
|
88
88
|
--project <dir> Operate on <dir> instead of the current directory.
|
|
89
89
|
--target <name,...> Select one or more MCP setup/status targets.
|
|
90
90
|
--server <url> (connect) DraftGo base URL.
|
|
91
|
+
--mcp-url <url> (connect) Explicit endpoint; standard /mcp derives the
|
|
92
|
+
base URL; custom paths also need --server.
|
|
91
93
|
--token <sat> (connect) SAT for non-interactive use.
|
|
92
94
|
--timeout <ms> (connect/mcp test/context) Network timeout.
|
|
93
95
|
--allow-offline (connect) Save only after explicitly accepting a
|
|
@@ -102,6 +104,7 @@ Important flags:
|
|
|
102
104
|
--output json Print machine-readable output where supported.
|
|
103
105
|
--task <task> (context) frontend | data | custom-service | aihub |
|
|
104
106
|
content | project.
|
|
107
|
+
--type <type> (map) pages | nav/navigations | docs/articles.
|
|
105
108
|
--strict Treat check warnings as failures.
|
|
106
109
|
--yes Skip supported confirmation prompts.
|
|
107
110
|
--operation-id <id> (delete) Select an operation explicitly.
|
|
@@ -110,6 +113,8 @@ Important flags:
|
|
|
110
113
|
--delivery <mode> (deploy) local | preview | deploy.
|
|
111
114
|
--dry-run (push) Show diffs without committing.
|
|
112
115
|
--mobile-check <mode> (verify-ui) auto | always | never.
|
|
116
|
+
--token <mode> (verify-ui) auto | never; auto appends the configured
|
|
117
|
+
SAT to same-origin URLs as the token query parameter.
|
|
113
118
|
--screenshot <mode> (verify-ui) on-failure | always | never.
|
|
114
119
|
--browser <name> (verify-ui) chromium | chrome | msedge.
|
|
115
120
|
--selector <css> (verify-ui) Require a visible key element.
|
package/src/commands/map.js
CHANGED
|
@@ -6,7 +6,16 @@ const log = require('../logger');
|
|
|
6
6
|
const { loadProjectConfig } = require('../projectConfig');
|
|
7
7
|
const { loadManifest, absolutePath } = require('../worktree/manifest');
|
|
8
8
|
const { hashFile } = require('../worktree/streams');
|
|
9
|
+
const { canonicalResourceType } = require('../worktree/types');
|
|
9
10
|
const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
|
|
11
|
+
const { allWithAbort } = require('../mcp/parallel');
|
|
12
|
+
|
|
13
|
+
const REMOTE_RESOURCE_TYPES = Object.freeze({
|
|
14
|
+
pages: 'pages',
|
|
15
|
+
navigations: 'navigations',
|
|
16
|
+
docs: 'docs/articles',
|
|
17
|
+
});
|
|
18
|
+
const DEFAULT_REMOTE_RESOURCE_TYPES = Object.freeze(Object.values(REMOTE_RESOURCE_TYPES));
|
|
10
19
|
|
|
11
20
|
function itemsFrom(value) {
|
|
12
21
|
if (Array.isArray(value)) return value;
|
|
@@ -18,26 +27,59 @@ function itemsFrom(value) {
|
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
function nextCursor(value) {
|
|
21
|
-
|
|
30
|
+
if (!value || typeof value !== 'object') return null;
|
|
31
|
+
if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) return value.next_cursor;
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) return value.nextCursor;
|
|
33
|
+
return (value.has_more === true || value.hasMore === true)
|
|
34
|
+
&& Object.prototype.hasOwnProperty.call(value, 'cursor')
|
|
35
|
+
? value.cursor
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function claimsMorePages(value) {
|
|
40
|
+
return !!value && typeof value === 'object'
|
|
41
|
+
&& (value.has_more === true || value.hasMore === true);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeMapResourceType(value) {
|
|
45
|
+
return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
|
|
48
|
+
function requestedResourceTypes(flags = {}) {
|
|
49
|
+
return flags.type == null
|
|
50
|
+
? DEFAULT_REMOTE_RESOURCE_TYPES
|
|
51
|
+
: [normalizeMapResourceType(flags.type)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function listRemoteResourceType(session, resourceType, options = {}) {
|
|
25
55
|
const resources = [];
|
|
26
56
|
const seen = new Set();
|
|
27
57
|
let cursor = null;
|
|
28
58
|
for (let page = 0; page < 100; page += 1) {
|
|
29
|
-
const args = {};
|
|
30
|
-
if (cursor) args.cursor = cursor;
|
|
31
|
-
|
|
32
|
-
const payload = await callStructured(session, TOOL_NAMES.resourceList, args);
|
|
59
|
+
const args = { resource_type: resourceType, limit: 100 };
|
|
60
|
+
if (cursor != null && cursor !== '') args.cursor = cursor;
|
|
61
|
+
const payload = await callStructured(session, TOOL_NAMES.resourceList, args, options);
|
|
33
62
|
resources.push(...itemsFrom(payload));
|
|
34
63
|
cursor = nextCursor(payload);
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
64
|
+
if (cursor == null || cursor === '') {
|
|
65
|
+
if (claimsMorePages(payload)) {
|
|
66
|
+
throw new Error(`DraftGo resource_list (${resourceType}) reported more pages without a cursor.`);
|
|
67
|
+
}
|
|
68
|
+
return resources;
|
|
69
|
+
}
|
|
70
|
+
const cursorKey = String(cursor);
|
|
71
|
+
if (seen.has(cursorKey)) {
|
|
72
|
+
throw new Error(`DraftGo resource_list (${resourceType}) repeated a cursor.`);
|
|
73
|
+
}
|
|
74
|
+
seen.add(cursorKey);
|
|
38
75
|
}
|
|
39
|
-
|
|
40
|
-
|
|
76
|
+
throw new Error(`DraftGo resource_list (${resourceType}) exceeded 100 pages.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOURCE_TYPES, options = {}) {
|
|
80
|
+
const groups = await allWithAbort(resourceTypes.map((resourceType) =>
|
|
81
|
+
(queryOptions) => listRemoteResourceType(session, resourceType, queryOptions)), options);
|
|
82
|
+
return groups.flat();
|
|
41
83
|
}
|
|
42
84
|
|
|
43
85
|
async function localCheckouts(projectDir) {
|
|
@@ -63,13 +105,17 @@ function legacyCaches(projectDir) {
|
|
|
63
105
|
}
|
|
64
106
|
|
|
65
107
|
async function mapCommand(projectDir, flags = {}) {
|
|
108
|
+
const resourceTypes = requestedResourceTypes(flags);
|
|
66
109
|
const config = loadProjectConfig(projectDir);
|
|
67
110
|
const session = await openToolSession(config, [TOOL_NAMES.projectOverview, TOOL_NAMES.resourceList]);
|
|
68
|
-
const [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
111
|
+
const [remote, checkouts] = await allWithAbort([
|
|
112
|
+
(options) => allWithAbort([
|
|
113
|
+
(options) => callStructured(session, TOOL_NAMES.projectOverview, {}, options),
|
|
114
|
+
(options) => listRemoteResources(session, resourceTypes, options),
|
|
115
|
+
], options),
|
|
116
|
+
() => localCheckouts(projectDir),
|
|
72
117
|
]);
|
|
118
|
+
const [overview, resources] = remote;
|
|
73
119
|
const result = {
|
|
74
120
|
server: config.server,
|
|
75
121
|
overview,
|
|
@@ -103,3 +149,6 @@ async function mapCommand(projectDir, flags = {}) {
|
|
|
103
149
|
|
|
104
150
|
module.exports = mapCommand;
|
|
105
151
|
module.exports.itemsFrom = itemsFrom;
|
|
152
|
+
module.exports.normalizeMapResourceType = normalizeMapResourceType;
|
|
153
|
+
module.exports.requestedResourceTypes = requestedResourceTypes;
|
|
154
|
+
module.exports.listRemoteResources = listRemoteResources;
|
package/src/commands/mcp.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
statusHosts,
|
|
12
12
|
} = require('../mcp/hosts');
|
|
13
13
|
const { serveStdio } = require('../mcp/stdio');
|
|
14
|
+
const { parseTimeout } = require('../timeout');
|
|
14
15
|
|
|
15
16
|
function targetArgs(positional, flags) {
|
|
16
17
|
const values = positional.slice();
|
|
@@ -89,11 +90,11 @@ async function test(projectDir, _positional = [], flags = {}) {
|
|
|
89
90
|
try {
|
|
90
91
|
config = loadProjectConfig(projectDir);
|
|
91
92
|
const result = await testConnection(config, {
|
|
92
|
-
timeoutMs:
|
|
93
|
+
timeoutMs: parseTimeout(flags.timeout),
|
|
93
94
|
});
|
|
94
95
|
log.ok('DraftGo MCP initialize succeeded.');
|
|
95
96
|
log.ok(`tools/list returned ${result.tools.length} tools.`);
|
|
96
|
-
log.ok(`tools/call succeeded: ${result.
|
|
97
|
+
log.ok(`tools/call succeeded: ${result.testedCalls.join(', ')}`);
|
|
97
98
|
log.dim(` protocol: ${result.protocolVersion}`);
|
|
98
99
|
return 0;
|
|
99
100
|
} catch (error) {
|
package/src/commands/verifyUi.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
6
|
const log = require('../logger');
|
|
7
|
+
const { configPath, loadProjectConfig } = require('../projectConfig');
|
|
7
8
|
|
|
8
9
|
const UI_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less', '.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte']);
|
|
9
10
|
|
|
@@ -50,6 +51,31 @@ function numberFlag(value, fallback, min, max) {
|
|
|
50
51
|
return Number.isFinite(n) ? Math.max(min, Math.min(max, Math.round(n))) : fallback;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function configuredUiUrl(projectDir, rawUrl, tokenMode = 'auto') {
|
|
55
|
+
if (tokenMode === 'never') return rawUrl;
|
|
56
|
+
if (!fs.existsSync(configPath(projectDir))) return rawUrl;
|
|
57
|
+
|
|
58
|
+
const config = loadProjectConfig(projectDir, { requireToken: false });
|
|
59
|
+
if (!config.token) return rawUrl;
|
|
60
|
+
|
|
61
|
+
let target;
|
|
62
|
+
try {
|
|
63
|
+
target = new URL(rawUrl);
|
|
64
|
+
} catch {
|
|
65
|
+
return rawUrl;
|
|
66
|
+
}
|
|
67
|
+
const server = new URL(config.server);
|
|
68
|
+
if (target.origin !== server.origin) return rawUrl;
|
|
69
|
+
target.searchParams.set('token', config.token);
|
|
70
|
+
return target.toString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function redactUrlTokens(message) {
|
|
74
|
+
return String(message || '')
|
|
75
|
+
.replace(/([?&]token=)[^&#\s]+/gi, '$1<redacted>')
|
|
76
|
+
.replace(/\bsat_[A-Za-z0-9._~+/-]+/g, '<redacted>');
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
function executableCandidates() {
|
|
54
80
|
if (process.platform === 'win32') {
|
|
55
81
|
const roots = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
|
|
@@ -93,12 +119,18 @@ async function launchBrowser(chromium, requested) {
|
|
|
93
119
|
}
|
|
94
120
|
|
|
95
121
|
async function verifyUi(projectDir, positional, flags = {}) {
|
|
96
|
-
const
|
|
97
|
-
if (!/^https?:\/\//i.test(
|
|
122
|
+
const rawUrl = String(flags.url || positional[0] || '').trim();
|
|
123
|
+
if (!/^https?:\/\//i.test(rawUrl)) {
|
|
98
124
|
log.err('用法:draftgo verify-ui <http://localhost:port/path>');
|
|
99
125
|
return 1;
|
|
100
126
|
}
|
|
101
127
|
|
|
128
|
+
const tokenMode = String(flags.token || 'auto').toLowerCase();
|
|
129
|
+
if (!['auto', 'never'].includes(tokenMode)) {
|
|
130
|
+
log.err('--token 在 verify-ui 中只支持 auto、never。');
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
const mode = String(flags['mobile-check'] || 'auto').toLowerCase();
|
|
103
135
|
if (!['auto', 'always', 'never'].includes(mode)) {
|
|
104
136
|
log.err('--mobile-check 只支持 auto、always、never。');
|
|
@@ -110,6 +142,14 @@ async function verifyUi(projectDir, positional, flags = {}) {
|
|
|
110
142
|
return 0;
|
|
111
143
|
}
|
|
112
144
|
|
|
145
|
+
let url;
|
|
146
|
+
try {
|
|
147
|
+
url = configuredUiUrl(projectDir, rawUrl, tokenMode);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
log.err(`无法读取 verify-ui 的项目配置:${err.message}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
let chromium;
|
|
114
154
|
try {
|
|
115
155
|
({ chromium } = require('playwright-core'));
|
|
@@ -133,8 +173,8 @@ async function verifyUi(projectDir, positional, flags = {}) {
|
|
|
133
173
|
const page = await browser.newPage({ viewport: { width, height } });
|
|
134
174
|
const consoleErrors = [];
|
|
135
175
|
const pageErrors = [];
|
|
136
|
-
page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
|
|
137
|
-
page.on('pageerror', (err) => pageErrors.push(err.message));
|
|
176
|
+
page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(redactUrlTokens(msg.text())); });
|
|
177
|
+
page.on('pageerror', (err) => pageErrors.push(redactUrlTokens(err.message)));
|
|
138
178
|
|
|
139
179
|
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
140
180
|
if (waitMs) await page.waitForTimeout(waitMs);
|
|
@@ -187,7 +227,7 @@ async function verifyUi(projectDir, positional, flags = {}) {
|
|
|
187
227
|
if (screenshotPath) log.info(`截图:${screenshotPath}`);
|
|
188
228
|
return 0;
|
|
189
229
|
} catch (err) {
|
|
190
|
-
log.err(`UI smoke check 失败:${err.message}`);
|
|
230
|
+
log.err(`UI smoke check 失败:${redactUrlTokens(err.message)}`);
|
|
191
231
|
return 1;
|
|
192
232
|
} finally {
|
|
193
233
|
if (browser) await browser.close().catch(() => {});
|
|
@@ -198,3 +238,5 @@ module.exports = verifyUi;
|
|
|
198
238
|
module.exports.decideMobileCheck = decideMobileCheck;
|
|
199
239
|
module.exports.gitChangedFiles = gitChangedFiles;
|
|
200
240
|
module.exports.isUiFile = isUiFile;
|
|
241
|
+
module.exports.configuredUiUrl = configuredUiUrl;
|
|
242
|
+
module.exports.redactUrlTokens = redactUrlTokens;
|
package/src/context/index.js
CHANGED
|
@@ -11,6 +11,13 @@ const { readInstalledVersion } = require('../skill');
|
|
|
11
11
|
|
|
12
12
|
const SKILL_ROOT = path.join(RESOURCES_DIR, 'skill');
|
|
13
13
|
const MAX_STRUCTURED_TEXT_BYTES = 1024 * 1024;
|
|
14
|
+
const LONG_CONTENT_RESOURCE_TYPES = Object.freeze([
|
|
15
|
+
'pages',
|
|
16
|
+
'navigations',
|
|
17
|
+
'docs/articles',
|
|
18
|
+
]);
|
|
19
|
+
const DB_META_LIST_PATH = '/api/db-meta';
|
|
20
|
+
const DB_META_PAGE_SIZE = 100;
|
|
14
21
|
const ROOT_SECTIONS = [
|
|
15
22
|
'核心边界',
|
|
16
23
|
'强制预读:Reference 优先于 MCP',
|
|
@@ -22,7 +29,11 @@ const ROOT_SECTIONS = [
|
|
|
22
29
|
|
|
23
30
|
const TASK_PROFILES = Object.freeze({
|
|
24
31
|
frontend: {
|
|
25
|
-
|
|
32
|
+
apiQueries: [
|
|
33
|
+
{ resource_type: 'pages' },
|
|
34
|
+
{ resource_type: 'navigations' },
|
|
35
|
+
{ resource_type: 'db' },
|
|
36
|
+
],
|
|
26
37
|
references: [
|
|
27
38
|
{ file: 'SKILL.md', headings: ROOT_SECTIONS },
|
|
28
39
|
{ file: 'references/architecture.md' },
|
|
@@ -36,7 +47,7 @@ const TASK_PROFILES = Object.freeze({
|
|
|
36
47
|
],
|
|
37
48
|
},
|
|
38
49
|
data: {
|
|
39
|
-
|
|
50
|
+
apiQueries: [{ resource_type: 'db' }],
|
|
40
51
|
references: [
|
|
41
52
|
{ file: 'SKILL.md', headings: ROOT_SECTIONS },
|
|
42
53
|
{ file: 'references/modules.md' },
|
|
@@ -48,7 +59,7 @@ const TASK_PROFILES = Object.freeze({
|
|
|
48
59
|
],
|
|
49
60
|
},
|
|
50
61
|
'custom-service': {
|
|
51
|
-
|
|
62
|
+
apiQueries: [{ resource_type: 'custom_scripts' }],
|
|
52
63
|
references: [
|
|
53
64
|
{ file: 'SKILL.md', headings: ROOT_SECTIONS },
|
|
54
65
|
{ file: 'references/modules.md' },
|
|
@@ -59,7 +70,10 @@ const TASK_PROFILES = Object.freeze({
|
|
|
59
70
|
],
|
|
60
71
|
},
|
|
61
72
|
aihub: {
|
|
62
|
-
|
|
73
|
+
apiQueries: [
|
|
74
|
+
{ resource_type: 'aihub' },
|
|
75
|
+
{ resource_type: 'agents' },
|
|
76
|
+
],
|
|
63
77
|
references: [
|
|
64
78
|
{ file: 'SKILL.md', headings: ROOT_SECTIONS },
|
|
65
79
|
{ file: 'references/aihub.md' },
|
|
@@ -69,7 +83,11 @@ const TASK_PROFILES = Object.freeze({
|
|
|
69
83
|
],
|
|
70
84
|
},
|
|
71
85
|
content: {
|
|
72
|
-
|
|
86
|
+
apiQueries: [
|
|
87
|
+
{ resource_type: 'pages' },
|
|
88
|
+
{ resource_type: 'navigations' },
|
|
89
|
+
{ resource_type: 'docs/articles' },
|
|
90
|
+
],
|
|
73
91
|
references: [
|
|
74
92
|
{ file: 'SKILL.md', headings: ROOT_SECTIONS },
|
|
75
93
|
{ file: 'references/architecture.md' },
|
|
@@ -80,7 +98,12 @@ const TASK_PROFILES = Object.freeze({
|
|
|
80
98
|
],
|
|
81
99
|
},
|
|
82
100
|
project: {
|
|
83
|
-
|
|
101
|
+
apiQueries: [
|
|
102
|
+
{ resource_type: 'pages' },
|
|
103
|
+
{ resource_type: 'navigations' },
|
|
104
|
+
{ resource_type: 'docs/articles' },
|
|
105
|
+
{ resource_type: 'db' },
|
|
106
|
+
],
|
|
84
107
|
references: [
|
|
85
108
|
{ file: 'SKILL.md', headings: ROOT_SECTIONS },
|
|
86
109
|
{ file: 'references/architecture.md' },
|
|
@@ -204,7 +227,7 @@ function rawQuery(session, expectedName, args, server, options = {}) {
|
|
|
204
227
|
}));
|
|
205
228
|
}
|
|
206
229
|
|
|
207
|
-
function
|
|
230
|
+
function structuredValueFromRaw(result) {
|
|
208
231
|
let value = result;
|
|
209
232
|
if (value && value.result) value = value.result;
|
|
210
233
|
if (value && value.structuredContent) value = value.structuredContent;
|
|
@@ -225,12 +248,112 @@ function nextCursorFromRaw(result) {
|
|
|
225
248
|
}
|
|
226
249
|
if (value && value.data) value = value.data;
|
|
227
250
|
if (value && Object.prototype.hasOwnProperty.call(value, 'value')) value = value.value;
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function nextCursorFromRaw(result) {
|
|
255
|
+
const value = structuredValueFromRaw(result);
|
|
228
256
|
if (!value || typeof value !== 'object') return null;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
const hasMore = value.has_more === true || value.hasMore === true;
|
|
258
|
+
let cursor = null;
|
|
259
|
+
let hasCursor = false;
|
|
260
|
+
if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
|
|
261
|
+
cursor = value.next_cursor;
|
|
262
|
+
hasCursor = true;
|
|
263
|
+
} else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
|
|
264
|
+
cursor = value.nextCursor;
|
|
265
|
+
hasCursor = true;
|
|
266
|
+
} else if (hasMore && Object.prototype.hasOwnProperty.call(value, 'cursor')) {
|
|
267
|
+
cursor = value.cursor;
|
|
268
|
+
hasCursor = true;
|
|
269
|
+
}
|
|
270
|
+
if (hasMore && (!hasCursor || cursor == null || cursor === '')) {
|
|
271
|
+
throw new Error('DraftGo MCP pagination reported more results without a cursor.');
|
|
272
|
+
}
|
|
273
|
+
return hasCursor ? cursor : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function collectLongContentResources(session, server, options = {}) {
|
|
277
|
+
const sequences = await Promise.all(LONG_CONTENT_RESOURCE_TYPES.map((resourceType) =>
|
|
278
|
+
rawPagedQuery(
|
|
279
|
+
session,
|
|
280
|
+
TOOL_NAMES.resourceList,
|
|
281
|
+
{ resource_type: resourceType, limit: 100 },
|
|
282
|
+
server,
|
|
283
|
+
options,
|
|
284
|
+
)));
|
|
285
|
+
return {
|
|
286
|
+
source: {
|
|
287
|
+
kind: 'mcp_tool_batch',
|
|
288
|
+
server,
|
|
289
|
+
canonical_tool: TOOL_NAMES.resourceList,
|
|
290
|
+
arguments: LONG_CONTENT_RESOURCE_TYPES.map((resourceType) => ({
|
|
291
|
+
resource_type: resourceType,
|
|
292
|
+
limit: 100,
|
|
293
|
+
})),
|
|
294
|
+
},
|
|
295
|
+
pages: sequences.flatMap((sequence) => sequence.pages),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function collectTaskAPIs(session, queries, server, options = {}) {
|
|
300
|
+
const argumentsList = queries.map((query) => ({ ...query, limit: 100 }));
|
|
301
|
+
const sequences = await Promise.all(argumentsList.map((args) =>
|
|
302
|
+
rawPagedQuery(session, TOOL_NAMES.apiSearch, args, server, options)));
|
|
303
|
+
return {
|
|
304
|
+
source: {
|
|
305
|
+
kind: 'mcp_tool_batch',
|
|
306
|
+
server,
|
|
307
|
+
canonical_tool: TOOL_NAMES.apiSearch,
|
|
308
|
+
arguments: argumentsList,
|
|
309
|
+
},
|
|
310
|
+
pages: sequences.flatMap((sequence) => sequence.pages),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function itemsFromRawSequence(sequence) {
|
|
315
|
+
if (!sequence || !Array.isArray(sequence.pages)) return [];
|
|
316
|
+
return sequence.pages.flatMap((page) => {
|
|
317
|
+
const value = structuredValueFromRaw(page);
|
|
318
|
+
if (Array.isArray(value)) return value;
|
|
319
|
+
if (value && Array.isArray(value.items)) return value.items;
|
|
320
|
+
if (value && Array.isArray(value.operations)) return value.operations;
|
|
321
|
+
return [];
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function selectDBMetaListOperation(sequence) {
|
|
326
|
+
return itemsFromRawSequence(sequence).find((operation) => {
|
|
327
|
+
if (!operation || typeof operation !== 'object') return false;
|
|
328
|
+
const method = String(operation.method || '').trim().toUpperCase();
|
|
329
|
+
const resourceType = String(operation.resource_type || '').trim().toLowerCase();
|
|
330
|
+
const operationPath = String(operation.path || operation.path_template || '').trim();
|
|
331
|
+
return method === 'GET'
|
|
332
|
+
&& resourceType === 'db_meta'
|
|
333
|
+
&& operationPath === DB_META_LIST_PATH
|
|
334
|
+
&& operation.destructive !== true;
|
|
335
|
+
}) || null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function optionalPageInteger(value, field, minimum) {
|
|
339
|
+
if (value == null) return null;
|
|
340
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
341
|
+
throw new Error(`DraftGo db_meta API returned invalid ${field}.`);
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function dbMetaPageInfo(response) {
|
|
347
|
+
let value = structuredValueFromRaw(response);
|
|
348
|
+
if (value && value.response) value = value.response;
|
|
349
|
+
if (value && value.data) value = value.data;
|
|
350
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.items)) return null;
|
|
351
|
+
return {
|
|
352
|
+
items: value.items,
|
|
353
|
+
total: optionalPageInteger(value.total, 'total', 0),
|
|
354
|
+
page: optionalPageInteger(value.page, 'page', 1),
|
|
355
|
+
pageSize: optionalPageInteger(value.page_size, 'page_size', 1),
|
|
356
|
+
};
|
|
234
357
|
}
|
|
235
358
|
|
|
236
359
|
async function rawPagedQuery(session, expectedName, baseArgs, server, options = {}) {
|
|
@@ -266,24 +389,102 @@ async function rawPagedQuery(session, expectedName, baseArgs, server, options =
|
|
|
266
389
|
throw new Error(expectedName + ' exceeded ' + maxPages + ' pages.');
|
|
267
390
|
}
|
|
268
391
|
|
|
392
|
+
async function collectDBMetaResources(session, operationID, server, options = {}) {
|
|
393
|
+
const pages = [];
|
|
394
|
+
let fetchedItems = 0;
|
|
395
|
+
let reportedTotal = null;
|
|
396
|
+
const maxPages = options.maxPages == null ? 100 : Number(options.maxPages);
|
|
397
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
|
|
398
|
+
throw new TypeError('maxPages must be a positive integer.');
|
|
399
|
+
}
|
|
400
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
401
|
+
const args = {
|
|
402
|
+
operation_id: operationID,
|
|
403
|
+
query: { page, page_size: DB_META_PAGE_SIZE },
|
|
404
|
+
};
|
|
405
|
+
const response = await rawQuery(session, TOOL_NAMES.apiCall, args, server, options);
|
|
406
|
+
pages.push(response);
|
|
407
|
+
const info = dbMetaPageInfo(response);
|
|
408
|
+
if (!info) {
|
|
409
|
+
return {
|
|
410
|
+
source: {
|
|
411
|
+
kind: 'mcp_tool_sequence',
|
|
412
|
+
server,
|
|
413
|
+
canonical_tool: TOOL_NAMES.apiCall,
|
|
414
|
+
operation_id: operationID,
|
|
415
|
+
},
|
|
416
|
+
pages,
|
|
417
|
+
pagination: { complete: false, reason: 'response_shape_unavailable' },
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const responsePage = info.page || page;
|
|
421
|
+
const responsePageSize = info.pageSize || DB_META_PAGE_SIZE;
|
|
422
|
+
if (responsePage !== page) {
|
|
423
|
+
throw new Error(`DraftGo db_meta API returned page ${responsePage} while page ${page} was requested.`);
|
|
424
|
+
}
|
|
425
|
+
fetchedItems += info.items.length;
|
|
426
|
+
if (info.total != null) {
|
|
427
|
+
if (reportedTotal != null && info.total !== reportedTotal) {
|
|
428
|
+
throw new Error(`DraftGo db_meta API changed total from ${reportedTotal} to ${info.total} during pagination.`);
|
|
429
|
+
}
|
|
430
|
+
reportedTotal = info.total;
|
|
431
|
+
}
|
|
432
|
+
if (reportedTotal != null) {
|
|
433
|
+
if (fetchedItems >= reportedTotal) {
|
|
434
|
+
return {
|
|
435
|
+
source: {
|
|
436
|
+
kind: 'mcp_tool_sequence',
|
|
437
|
+
server,
|
|
438
|
+
canonical_tool: TOOL_NAMES.apiCall,
|
|
439
|
+
operation_id: operationID,
|
|
440
|
+
},
|
|
441
|
+
pages,
|
|
442
|
+
pagination: { complete: true, total: reportedTotal },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (!info.items.length) {
|
|
446
|
+
throw new Error('DraftGo db_meta API ended before the reported total was reached.');
|
|
447
|
+
}
|
|
448
|
+
} else if (info.items.length < responsePageSize) {
|
|
449
|
+
return {
|
|
450
|
+
source: {
|
|
451
|
+
kind: 'mcp_tool_sequence',
|
|
452
|
+
server,
|
|
453
|
+
canonical_tool: TOOL_NAMES.apiCall,
|
|
454
|
+
operation_id: operationID,
|
|
455
|
+
},
|
|
456
|
+
pages,
|
|
457
|
+
pagination: { complete: true, total: null },
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
throw new Error(`DraftGo db_meta API exceeded ${maxPages} pages.`);
|
|
462
|
+
}
|
|
463
|
+
|
|
269
464
|
async function collectDataStructures(session, server, options = {}) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
465
|
+
const api = await rawPagedQuery(
|
|
466
|
+
session,
|
|
467
|
+
TOOL_NAMES.apiSearch,
|
|
468
|
+
{ resource_type: 'db_meta', limit: 100 },
|
|
469
|
+
server,
|
|
470
|
+
options,
|
|
471
|
+
);
|
|
472
|
+
const operation = selectDBMetaListOperation(api);
|
|
473
|
+
if (!operation || !operation.operation_id) {
|
|
474
|
+
throw new Error('DraftGo MCP does not expose a read-only GET /api/db-meta operation.');
|
|
475
|
+
}
|
|
476
|
+
const operationID = String(operation.operation_id);
|
|
477
|
+
const [contract, resources] = await Promise.all([
|
|
278
478
|
rawQuery(
|
|
279
479
|
session,
|
|
280
|
-
TOOL_NAMES.
|
|
281
|
-
{
|
|
480
|
+
TOOL_NAMES.apiDescribe,
|
|
481
|
+
{ operation_id: operationID },
|
|
282
482
|
server,
|
|
283
483
|
options,
|
|
284
484
|
),
|
|
485
|
+
collectDBMetaResources(session, operationID, server, options),
|
|
285
486
|
]);
|
|
286
|
-
return {
|
|
487
|
+
return { api, contract, resources };
|
|
287
488
|
}
|
|
288
489
|
|
|
289
490
|
async function collectContext(projectDir, taskValue, options = {}) {
|
|
@@ -292,7 +493,13 @@ async function collectContext(projectDir, taskValue, options = {}) {
|
|
|
292
493
|
const references = readTaskReferences(task, options);
|
|
293
494
|
const installedSkillVersion = readInstalledSkillVersion(projectDir);
|
|
294
495
|
const config = options.config || loadProjectConfig(projectDir);
|
|
295
|
-
const expected = [
|
|
496
|
+
const expected = [
|
|
497
|
+
TOOL_NAMES.projectOverview,
|
|
498
|
+
TOOL_NAMES.resourceList,
|
|
499
|
+
TOOL_NAMES.apiSearch,
|
|
500
|
+
TOOL_NAMES.apiDescribe,
|
|
501
|
+
TOOL_NAMES.apiCall,
|
|
502
|
+
];
|
|
296
503
|
const openSession = options.openToolSession || openToolSession;
|
|
297
504
|
const session = options.session || await openSession(config, expected, options);
|
|
298
505
|
const controller = new AbortController();
|
|
@@ -311,14 +518,8 @@ async function collectContext(projectDir, taskValue, options = {}) {
|
|
|
311
518
|
const queryOptions = { ...options, signal: controller.signal };
|
|
312
519
|
[project, resources, api, dataStructures] = await Promise.all([
|
|
313
520
|
rawQuery(session, TOOL_NAMES.projectOverview, {}, config.server, queryOptions),
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
session,
|
|
317
|
-
TOOL_NAMES.apiSearch,
|
|
318
|
-
{ query: profile.apiQuery },
|
|
319
|
-
config.server,
|
|
320
|
-
queryOptions,
|
|
321
|
-
),
|
|
521
|
+
collectLongContentResources(session, config.server, queryOptions),
|
|
522
|
+
collectTaskAPIs(session, profile.apiQueries, config.server, queryOptions),
|
|
322
523
|
collectDataStructures(session, config.server, queryOptions),
|
|
323
524
|
]);
|
|
324
525
|
} catch (error) {
|
|
@@ -329,7 +530,7 @@ async function collectContext(projectDir, taskValue, options = {}) {
|
|
|
329
530
|
}
|
|
330
531
|
|
|
331
532
|
return {
|
|
332
|
-
schema_version: '1.
|
|
533
|
+
schema_version: '1.1',
|
|
333
534
|
task,
|
|
334
535
|
reference_bundle: {
|
|
335
536
|
source: 'draftgo-cli',
|
|
@@ -360,8 +561,15 @@ module.exports = {
|
|
|
360
561
|
normalizeTask,
|
|
361
562
|
extractSections,
|
|
362
563
|
readTaskReferences,
|
|
564
|
+
structuredValueFromRaw,
|
|
363
565
|
nextCursorFromRaw,
|
|
364
566
|
rawPagedQuery,
|
|
567
|
+
collectLongContentResources,
|
|
568
|
+
collectTaskAPIs,
|
|
569
|
+
itemsFromRawSequence,
|
|
570
|
+
selectDBMetaListOperation,
|
|
571
|
+
dbMetaPageInfo,
|
|
572
|
+
collectDBMetaResources,
|
|
365
573
|
collectDataStructures,
|
|
366
574
|
readInstalledSkillVersion,
|
|
367
575
|
collectContext,
|
package/src/mcp/client.js
CHANGED
|
@@ -7,11 +7,14 @@ const {
|
|
|
7
7
|
postJsonRpc,
|
|
8
8
|
redactText,
|
|
9
9
|
} = require('./protocol');
|
|
10
|
+
const { allWithAbort } = require('./parallel');
|
|
10
11
|
|
|
11
12
|
const SAFE_TEST_TOOLS = [
|
|
12
13
|
'draftgo_project_overview',
|
|
13
14
|
'draftgo_resource_list',
|
|
14
15
|
'draftgo_api_search',
|
|
16
|
+
'draftgo_api_describe',
|
|
17
|
+
'draftgo_api_call',
|
|
15
18
|
];
|
|
16
19
|
|
|
17
20
|
const REQUIRED_DRAFTGO_TOOLS = [
|
|
@@ -25,6 +28,33 @@ const REQUIRED_DRAFTGO_TOOLS = [
|
|
|
25
28
|
'draftgo_api_call',
|
|
26
29
|
];
|
|
27
30
|
|
|
31
|
+
function diagnosticArguments(canonicalName) {
|
|
32
|
+
if (canonicalName === 'draftgo_resource_list') {
|
|
33
|
+
return { resource_type: 'pages', limit: 1 };
|
|
34
|
+
}
|
|
35
|
+
if (canonicalName === 'draftgo_api_search') return { query: 'project', limit: 1 };
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function diagnosticData(result) {
|
|
40
|
+
let value = result;
|
|
41
|
+
if (value && value.structuredContent) value = value.structuredContent;
|
|
42
|
+
else if (value && value.structured_content) value = value.structured_content;
|
|
43
|
+
if (value && value.data) value = value.data;
|
|
44
|
+
if (value && Object.prototype.hasOwnProperty.call(value, 'value')) value = value.value;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
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
|
+
}
|
|
57
|
+
|
|
28
58
|
class McpRpcError extends Error {
|
|
29
59
|
constructor(message, { code = -32000, data, id = null } = {}) {
|
|
30
60
|
super(message);
|
|
@@ -35,6 +65,40 @@ class McpRpcError extends Error {
|
|
|
35
65
|
}
|
|
36
66
|
}
|
|
37
67
|
|
|
68
|
+
function diagnosticNextCursor(result) {
|
|
69
|
+
const value = diagnosticData(result);
|
|
70
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
71
|
+
const hasMore = value.has_more === true || value.hasMore === true;
|
|
72
|
+
|
|
73
|
+
let cursor;
|
|
74
|
+
let present = false;
|
|
75
|
+
if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
|
|
76
|
+
cursor = value.next_cursor;
|
|
77
|
+
present = true;
|
|
78
|
+
} else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
|
|
79
|
+
cursor = value.nextCursor;
|
|
80
|
+
present = true;
|
|
81
|
+
} else if (hasMore) {
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'cursor')) {
|
|
83
|
+
throw new McpRpcError('DraftGo api_search reported more pages without a cursor.');
|
|
84
|
+
}
|
|
85
|
+
cursor = value.cursor;
|
|
86
|
+
present = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!present) return null;
|
|
90
|
+
if (cursor == null || cursor === '') {
|
|
91
|
+
if (hasMore) {
|
|
92
|
+
throw new McpRpcError('DraftGo api_search reported more pages with an empty cursor.');
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (typeof cursor !== 'string' || !cursor.trim()) {
|
|
97
|
+
throw new McpRpcError('DraftGo api_search returned an invalid cursor.');
|
|
98
|
+
}
|
|
99
|
+
return cursor;
|
|
100
|
+
}
|
|
101
|
+
|
|
38
102
|
function redactValue(value, secrets = [], seen = new WeakMap()) {
|
|
39
103
|
if (typeof value === 'string') return redactText(value, secrets);
|
|
40
104
|
if (!value || typeof value !== 'object') return value;
|
|
@@ -222,30 +286,118 @@ class DraftGoMcpClient {
|
|
|
222
286
|
if (missing.length) {
|
|
223
287
|
throw new McpRpcError(`DraftGo MCP is missing required tools: ${missing.join(', ')}.`, { code: -32601 });
|
|
224
288
|
}
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
289
|
+
const safeTools = options.safeTools || SAFE_TEST_TOOLS;
|
|
290
|
+
const hasDiagnosticOverrides = Boolean(options.requiredTools || options.safeTools);
|
|
291
|
+
const firstSafeTool = tools.find((tool) => tool && typeof tool.name === 'string'
|
|
292
|
+
&& safeTools.some((canonical) => toolMatches(tool.name, canonical)));
|
|
293
|
+
if (!firstSafeTool) {
|
|
229
294
|
throw new McpRpcError(
|
|
230
|
-
`DraftGo MCP exposes no safe diagnostic tool (${
|
|
295
|
+
`DraftGo MCP exposes no safe diagnostic tool (${safeTools.join(', ')}).`,
|
|
231
296
|
{ code: -32601 },
|
|
232
297
|
);
|
|
233
298
|
}
|
|
234
299
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
300
|
+
const selected = safeTools.map((canonical) => ({
|
|
301
|
+
canonical,
|
|
302
|
+
tool: tools.find((candidate) => candidate && typeof candidate.name === 'string'
|
|
303
|
+
&& toolMatches(candidate.name, canonical)),
|
|
304
|
+
})).filter((entry) => entry.tool);
|
|
305
|
+
const byCanonical = Object.fromEntries(selected.map((entry) => [entry.canonical, entry.tool]));
|
|
306
|
+
const overrideCanonical = safeTools.find((canonical) => toolMatches(firstSafeTool.name, canonical));
|
|
307
|
+
const defaultPlan = hasDiagnosticOverrides ? [{
|
|
308
|
+
label: overrideCanonical,
|
|
309
|
+
canonical: overrideCanonical,
|
|
310
|
+
tool: firstSafeTool,
|
|
311
|
+
args: diagnosticArguments(overrideCanonical),
|
|
312
|
+
}] : [
|
|
313
|
+
{ label: 'project', canonical: 'draftgo_project_overview', tool: byCanonical.draftgo_project_overview, args: {} },
|
|
314
|
+
...['pages', 'navigations', 'docs/articles'].map((resourceType) => ({
|
|
315
|
+
label: `resource:${resourceType}`,
|
|
316
|
+
canonical: 'draftgo_resource_list',
|
|
317
|
+
tool: byCanonical.draftgo_resource_list,
|
|
318
|
+
args: { resource_type: resourceType, limit: 1 },
|
|
319
|
+
})),
|
|
320
|
+
{
|
|
321
|
+
label: 'api:db_meta',
|
|
322
|
+
canonical: 'draftgo_api_search',
|
|
323
|
+
tool: byCanonical.draftgo_api_search,
|
|
324
|
+
args: { resource_type: 'db_meta', limit: 100 },
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
const runPlan = (plan, startIndex = 0) => allWithAbort(plan.map((entry, offset) =>
|
|
328
|
+
async (queryOptions) => ({
|
|
329
|
+
label: entry.label,
|
|
330
|
+
canonical: entry.canonical,
|
|
331
|
+
name: entry.tool.name,
|
|
332
|
+
arguments: options.toolArguments && startIndex + offset === 0
|
|
333
|
+
? options.toolArguments
|
|
334
|
+
: entry.args,
|
|
335
|
+
result: await this.toolsCall(
|
|
336
|
+
entry.tool.name,
|
|
337
|
+
options.toolArguments && startIndex + offset === 0
|
|
338
|
+
? options.toolArguments
|
|
339
|
+
: entry.args,
|
|
340
|
+
queryOptions,
|
|
341
|
+
),
|
|
342
|
+
})), options);
|
|
343
|
+
const tested = await runPlan(defaultPlan);
|
|
344
|
+
if (!hasDiagnosticOverrides) {
|
|
345
|
+
let discovery = tested.find((entry) => entry.label === 'api:db_meta');
|
|
346
|
+
let operation = discovery && findDBMetaListOperation(discovery.result);
|
|
347
|
+
const seenCursors = new Set();
|
|
348
|
+
const maxPages = Number(options.maxPages || 100);
|
|
349
|
+
let page = 1;
|
|
350
|
+
while (!operation) {
|
|
351
|
+
const cursor = diagnosticNextCursor(discovery && discovery.result);
|
|
352
|
+
if (cursor == null) break;
|
|
353
|
+
if (seenCursors.has(cursor)) {
|
|
354
|
+
throw new McpRpcError('DraftGo api_search repeated a db_meta cursor.');
|
|
355
|
+
}
|
|
356
|
+
seenCursors.add(cursor);
|
|
357
|
+
if (page >= maxPages) {
|
|
358
|
+
throw new McpRpcError(`DraftGo api_search exceeded ${maxPages} db_meta pages.`);
|
|
359
|
+
}
|
|
360
|
+
page += 1;
|
|
361
|
+
[discovery] = await runPlan([{
|
|
362
|
+
label: `api:db_meta:page-${page}`,
|
|
363
|
+
canonical: 'draftgo_api_search',
|
|
364
|
+
tool: byCanonical.draftgo_api_search,
|
|
365
|
+
args: { resource_type: 'db_meta', limit: 100, cursor },
|
|
366
|
+
}], tested.length);
|
|
367
|
+
tested.push(discovery);
|
|
368
|
+
operation = findDBMetaListOperation(discovery.result);
|
|
369
|
+
}
|
|
370
|
+
if (!operation || !operation.operation_id) {
|
|
371
|
+
throw new McpRpcError('DraftGo MCP exposes no read-only GET /api/db-meta operation.', {
|
|
372
|
+
code: -32601,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const operationID = String(operation.operation_id);
|
|
376
|
+
tested.push(...await runPlan([
|
|
377
|
+
{
|
|
378
|
+
label: 'api:describe-db_meta',
|
|
379
|
+
canonical: 'draftgo_api_describe',
|
|
380
|
+
tool: byCanonical.draftgo_api_describe,
|
|
381
|
+
args: { operation_id: operationID },
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
label: 'api:call-db_meta',
|
|
385
|
+
canonical: 'draftgo_api_call',
|
|
386
|
+
tool: byCanonical.draftgo_api_call,
|
|
387
|
+
args: { operation_id: operationID, query: { page: 1, page_size: 1 } },
|
|
388
|
+
},
|
|
389
|
+
], tested.length));
|
|
390
|
+
}
|
|
242
391
|
return {
|
|
243
392
|
initialized,
|
|
244
393
|
protocolVersion: this.protocolVersion,
|
|
245
394
|
sessionId: this.sessionId,
|
|
246
395
|
tools,
|
|
247
|
-
testedTool:
|
|
248
|
-
toolResult,
|
|
396
|
+
testedTool: tested[0].name,
|
|
397
|
+
toolResult: tested[0].result,
|
|
398
|
+
testedTools: [...new Set(tested.map((entry) => entry.name))],
|
|
399
|
+
testedCalls: tested.map((entry) => entry.label),
|
|
400
|
+
diagnosticResults: tested,
|
|
249
401
|
};
|
|
250
402
|
}
|
|
251
403
|
}
|
|
@@ -268,6 +420,10 @@ module.exports = {
|
|
|
268
420
|
McpHttpError,
|
|
269
421
|
SAFE_TEST_TOOLS,
|
|
270
422
|
REQUIRED_DRAFTGO_TOOLS,
|
|
423
|
+
diagnosticArguments,
|
|
424
|
+
diagnosticData,
|
|
425
|
+
diagnosticNextCursor,
|
|
426
|
+
findDBMetaListOperation,
|
|
271
427
|
redactValue,
|
|
272
428
|
testConnection,
|
|
273
429
|
callTool,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function allWithAbort(taskFactories, options = {}) {
|
|
4
|
+
if (!Array.isArray(taskFactories) || taskFactories.some((task) => typeof task !== 'function')) {
|
|
5
|
+
throw new TypeError('Parallel MCP tasks must be functions.');
|
|
6
|
+
}
|
|
7
|
+
const externalSignal = options.signal;
|
|
8
|
+
if (externalSignal && externalSignal.aborted) {
|
|
9
|
+
if (typeof externalSignal.throwIfAborted === 'function') externalSignal.throwIfAborted();
|
|
10
|
+
const error = new Error('Parallel MCP tasks were aborted before they started.');
|
|
11
|
+
error.name = 'AbortError';
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const forwardAbort = () => controller.abort(externalSignal.reason);
|
|
17
|
+
if (externalSignal) {
|
|
18
|
+
externalSignal.addEventListener('abort', forwardAbort, { once: true });
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const taskOptions = { ...options, signal: controller.signal };
|
|
22
|
+
return await Promise.all(taskFactories.map((task) => task(taskOptions)));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!controller.signal.aborted) controller.abort(error);
|
|
25
|
+
throw error;
|
|
26
|
+
} finally {
|
|
27
|
+
if (externalSignal) externalSignal.removeEventListener('abort', forwardAbort);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { allWithAbort };
|
package/src/mcp/protocol.js
CHANGED
|
@@ -22,9 +22,10 @@ function redactText(value, secrets = []) {
|
|
|
22
22
|
|
|
23
23
|
function endpointFor(config) {
|
|
24
24
|
const server = new URL(String(config.server || config.mcp_url));
|
|
25
|
+
const serverPath = server.pathname.replace(/\/+$/, '');
|
|
25
26
|
const endpoint = config.mcp_url
|
|
26
27
|
? new URL(String(config.mcp_url), server)
|
|
27
|
-
: new URL(`${
|
|
28
|
+
: new URL(`${serverPath}/mcp`, server);
|
|
28
29
|
if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) {
|
|
29
30
|
throw new McpHttpError('DraftGo MCP endpoint must be a credential-free HTTP(S) URL.');
|
|
30
31
|
}
|
package/src/projectConfig.js
CHANGED
|
@@ -11,16 +11,42 @@ const IGNORE_ENTRIES = [
|
|
|
11
11
|
'.draftgo/conflicts/',
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
function
|
|
14
|
+
function parseHttpUrl(raw) {
|
|
15
15
|
let value = String(raw || '').trim();
|
|
16
|
-
if (!value) return
|
|
16
|
+
if (!value) return null;
|
|
17
17
|
if (!/^https?:\/\//i.test(value)) value = `http://${value}`;
|
|
18
18
|
const parsed = new URL(value);
|
|
19
19
|
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password
|
|
20
20
|
|| parsed.search || parsed.hash) {
|
|
21
21
|
throw new Error('DraftGo server must be an absolute HTTP(S) URL without credentials, query, or fragment.');
|
|
22
22
|
}
|
|
23
|
-
return
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeServer(raw) {
|
|
27
|
+
const parsed = parseHttpUrl(raw);
|
|
28
|
+
if (!parsed) return '';
|
|
29
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeMcpEndpoint(raw) {
|
|
33
|
+
const parsed = parseHttpUrl(raw);
|
|
34
|
+
return parsed ? parsed.toString() : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeConnection(raw) {
|
|
38
|
+
const value = normalizeMcpEndpoint(raw);
|
|
39
|
+
if (!value) return { server: '', mcp_url: '' };
|
|
40
|
+
const parsed = new URL(value);
|
|
41
|
+
const pathname = parsed.pathname.replace(/\/+$/, '');
|
|
42
|
+
if (!pathname.toLowerCase().endsWith('/mcp')) {
|
|
43
|
+
return { server: '', mcp_url: value };
|
|
44
|
+
}
|
|
45
|
+
parsed.pathname = pathname.slice(0, -4) || '/';
|
|
46
|
+
return {
|
|
47
|
+
server: parsed.toString().replace(/\/+$/, ''),
|
|
48
|
+
mcp_url: value,
|
|
49
|
+
};
|
|
24
50
|
}
|
|
25
51
|
|
|
26
52
|
function configPath(projectDir) {
|
|
@@ -65,7 +91,7 @@ function writePrivateJson(file, value) {
|
|
|
65
91
|
try { fs.chmodSync(file, 0o600); } catch { /* Windows ACLs are inherited. */ }
|
|
66
92
|
}
|
|
67
93
|
|
|
68
|
-
function writeProjectConfig(projectDir, server, token) {
|
|
94
|
+
function writeProjectConfig(projectDir, server, token, options = {}) {
|
|
69
95
|
const normalizedServer = normalizeServer(server);
|
|
70
96
|
const sat = String(token || '').trim();
|
|
71
97
|
if (!normalizedServer) throw new Error('DraftGo server is required.');
|
|
@@ -77,6 +103,8 @@ function writeProjectConfig(projectDir, server, token) {
|
|
|
77
103
|
token: sat,
|
|
78
104
|
auto_push: existing.auto_push === true,
|
|
79
105
|
};
|
|
106
|
+
if (options.mcp_url) config.mcp_url = normalizeMcpEndpoint(options.mcp_url);
|
|
107
|
+
else delete config.mcp_url;
|
|
80
108
|
const file = configPath(projectDir);
|
|
81
109
|
writePrivateJson(file, config);
|
|
82
110
|
ensureProjectIgnores(projectDir);
|
|
@@ -88,16 +116,22 @@ function loadProjectConfig(projectDir, { requireToken = true } = {}) {
|
|
|
88
116
|
if (!exists(file)) throw new Error('Missing .draftgo/config.json; run `draftgo connect` first.');
|
|
89
117
|
const config = readExisting(projectDir);
|
|
90
118
|
const server = normalizeServer(config.server);
|
|
119
|
+
const mcpUrl = config.mcp_url ? normalizeMcpEndpoint(config.mcp_url) : '';
|
|
91
120
|
const token = String(config.token || config.sat || '').trim();
|
|
92
121
|
if (!server) throw new Error('DraftGo project config is missing server.');
|
|
93
122
|
if (requireToken && !token) throw new Error('DraftGo project config is missing SAT.');
|
|
94
|
-
|
|
123
|
+
const result = { ...config, server, token, path: file };
|
|
124
|
+
if (mcpUrl) result.mcp_url = mcpUrl;
|
|
125
|
+
else delete result.mcp_url;
|
|
126
|
+
return result;
|
|
95
127
|
}
|
|
96
128
|
|
|
97
129
|
module.exports = {
|
|
98
130
|
IGNORE_ENTRIES,
|
|
99
131
|
configPath,
|
|
100
132
|
normalizeServer,
|
|
133
|
+
normalizeMcpEndpoint,
|
|
134
|
+
normalizeConnection,
|
|
101
135
|
ensureProjectIgnores,
|
|
102
136
|
writeProjectConfig,
|
|
103
137
|
loadProjectConfig,
|
package/src/timeout.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
4
|
+
|
|
5
|
+
function parseTimeout(value) {
|
|
6
|
+
if (value == null) return undefined;
|
|
7
|
+
const text = String(value).trim();
|
|
8
|
+
if (!/^\d+$/.test(text)) {
|
|
9
|
+
throw new TypeError('--timeout must be a positive integer in milliseconds.');
|
|
10
|
+
}
|
|
11
|
+
const timeoutMs = Number(text);
|
|
12
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
13
|
+
throw new TypeError(`--timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds.`);
|
|
14
|
+
}
|
|
15
|
+
return timeoutMs;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { MAX_TIMEOUT_MS, parseTimeout };
|