draftgo-cli 3.0.55 → 4.0.1

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 (50) hide show
  1. package/README.md +112 -316
  2. package/package.json +5 -5
  3. package/resources/skill/SKILL.md +25 -24
  4. package/resources/skill/init/SKILL.md +5 -10
  5. package/resources/skill/manifest.json +2 -2
  6. package/resources/skill/references/aihub.md +10 -5
  7. package/resources/skill/references/chat-sdk.md +10 -0
  8. package/resources/skill/references/checkout.md +4 -4
  9. package/resources/skill/references/custom-services.md +65 -226
  10. package/resources/skill/references/data.md +3 -2
  11. package/resources/skill/references/frontend.md +96 -490
  12. package/resources/skill/references/mcp.md +39 -103
  13. package/resources/skill/references/runtime.md +3 -2
  14. package/resources/skill/story/SKILL.md +1 -2
  15. package/src/apiContractCache.js +112 -0
  16. package/src/cli.js +1 -21
  17. package/src/commandRegistry.js +6 -11
  18. package/src/commands/api.js +28 -8
  19. package/src/commands/check.js +1 -10
  20. package/src/commands/customService.js +2 -4
  21. package/src/commands/delete.js +23 -46
  22. package/src/commands/deploy.js +1 -1
  23. package/src/commands/help.js +16 -31
  24. package/src/commands/init.js +4 -10
  25. package/src/commands/listTargets.js +1 -1
  26. package/src/commands/local.js +2 -6
  27. package/src/commands/map.js +0 -11
  28. package/src/commands/status.js +1 -1
  29. package/src/commands/uninstall.js +3 -3
  30. package/src/commands/update.js +1 -1
  31. package/src/commands/verify.js +43 -21
  32. package/src/commands/{verifyUi.js → visualVerify.js} +28 -116
  33. package/src/commands/worklog.js +86 -0
  34. package/src/customServices.js +150 -33
  35. package/src/{localdev → localRuntime}/detect.js +1 -1
  36. package/src/{localdev → localRuntime}/mysqlClient.js +1 -1
  37. package/src/{localdev → localRuntime}/services.js +1 -1
  38. package/src/projectConfig.js +2 -0
  39. package/src/{installers/index.js → targets.js} +3 -5
  40. package/src/worklog.js +274 -0
  41. package/src/workspaceHealth.js +1 -1
  42. package/src/worktree/index.js +81 -51
  43. package/src/changelog.js +0 -276
  44. package/src/commands/changelog.js +0 -24
  45. package/src/commands/localDev.js +0 -9
  46. package/src/commands/sync.js +0 -46
  47. package/src/commands/task.js +0 -408
  48. package/src/commands/verifyUiCompat.js +0 -16
  49. /package/src/{localdev → localRuntime}/compose.js +0 -0
  50. /package/src/{localdev → localRuntime}/index.js +0 -0
@@ -4,62 +4,44 @@ read_when: 配置或诊断 DraftGo MCP 时 · 查询实时资源或 API 契约
4
4
 
5
5
  # DraftGo MCP
6
6
 
7
- > 根 `SKILL.md` Skill 触发时会自动加载。使用本文件前,先完成根 Skill 的“强制预读:Reference 优先于 MCP”任务路由。本文件只说明实时 MCP 的边界和调用顺序,不能替代页面、前端、运行时、安全或模块资料。
7
+ 根 `SKILL.md` 已负责任务路由。本文件只保留 MCP 的实时契约、调用优化和安全边界,不重复页面、前端或交付规则。
8
8
 
9
9
  ## 边界
10
10
 
11
- DraftGo MCP 用于实时发现、结构化查询和普通 API 操作。完整 pages、navigations、docs/articles 正文不通过
12
- MCP tool result 或参数传输;需要全文时使用 `draftgo checkout`,提交时使用 `draftgo commit`。MCP 返回
13
- `artifact` / `omitted` 时保留该语义,不要尝试重新展开长内容。
11
+ MCP 用于实时发现、结构化查询和普通 API 操作。pages、navigations、docs/articles、custom_services 的完整正文走 `draftgo checkout` / `draftgo commit`;工具返回 `artifact` 或 `omitted` 时保留该语义,不要求模型展开长内容。
14
12
 
15
- 不要在 CLI MCP 服务端增加聚合上下文工具。自然语言任务的语义筛选由已加载根 Skill 的 Agent 完成:先读取任务路由指定的最少必要 Reference,再调用下面的精确 MCP 工具,并根据结果逐步缩小范围。
13
+ 不要增加聚合上下文工具或静态 API 路径表。按任务读取最少必要的 Reference,再调用精确工具:
16
14
 
17
- 标准发现顺序:
15
+ | 需要 | 工具 |
16
+ |---|---|
17
+ | 项目能力、registry 覆盖和 checkout 类型 | `draftgo_project_overview` |
18
+ | 定位资源 | `draftgo_resource_search` / `draftgo_resource_list` |
19
+ | 元数据或短片段 | `draftgo_resource_get_metadata` / `draftgo_resource_read_fragment` |
20
+ | 定位 API operation | `draftgo_api_search` |
21
+ | 读取 operation schema | `draftgo_api_describe` |
22
+ | 结构化读写 | `draftgo_api_call` |
18
23
 
19
- 1. `draftgo_project_overview`
20
- 2. `draftgo_resource_search` 或 `draftgo_resource_list`
21
- 3. 按需调用 `draftgo_resource_get_metadata` 或 `draftgo_resource_read_fragment`
22
- 4. 需要实时 API 契约时调用 `draftgo_api_search`、`draftgo_api_describe`
23
- 5. 结构化资源读写使用 `draftgo_api_call`
24
+ 只在需要对应信息时调用工具,不把 `project_overview` 作为每个任务的固定前置步骤。独立资源或 operation 可并发调用;同一资源的依赖步骤保持顺序。非幂等写入失败后先读状态,不自动重试。
24
25
 
25
- 结构化资源包括 db_meta、AIHub、system_config、roles、users、doc_categories 和自定义服务元数据。
26
- 这些资源不 checkout,也不生成 `.draftgo/*/index.json` 镜像。
26
+ ## API 契约缓存
27
27
 
28
- ## API 发现与调用
28
+ 动态 schema 不写入 Skill。Skill 只保存下面的缓存规则:
29
29
 
30
- 静态 API 路径表不是事实来源。每次调用都按 `draftgo_api_search` `draftgo_api_describe` `draftgo_api_call` 执行,不能从旧文档、经验或相似 operation 猜参数。
30
+ 1. operation 未知时才 search;已有精确 `operation_id` 时跳过 search。
31
+ 2. 第一次使用 operation 时 describe,检查 method、path、parameters、request body、responses、permission、risk、`destructive`、`idempotent`、`input_schema` 和 `response_policy`。
32
+ 3. 同一 server 的 operation 描述按 `registry_revision` 复用。CLI 将缓存写在私有 `.draftgo/api-contract-cache.json`,不把 schema 注入 Skill 或对话上下文。
33
+ 4. call 携带缓存的 `registry_revision`。服务返回 `CONTRACT_CHANGED` 时重新 describe 并重试一次;其他工具错误不触发自动重试。
31
34
 
32
- ### 1. 搜索 operation
35
+ describe 缺少本次调用需要的 schema、权限、风险或响应契约时停止并报告,不猜字段、不拼路径、不绕过 MCP。多个 operation 仍可能匹配时继续缩小 search,而不是任选一个。
33
36
 
34
- ```json
35
- {
36
- "query": "需要查找的能力或路径片段",
37
- "module": "可选模块",
38
- "resource_type": "可选资源类型",
39
- "limit": 100,
40
- "cursor": "下一页游标"
41
- }
42
- ```
37
+ 动态 Go 自定义服务 Route 也在 registry 中:module 为 `scripts_dynamic`,resource type 为 `custom_scripts`。Route 的 SDK、身份和运行限制见 `custom-services.md`。
43
38
 
44
- `query`、`module`、`resource_type` 可组合筛选。结果提供 `operation_id`、method、path、module、resource type、permission、risk、`destructive` 和 `idempotent`。需要完整候选集时持续读取 `next_cursor`,直到没有下一页;不要因首批搜索结果为空或同名 operation 较多就猜测端点。
45
-
46
- 动态 Go 自定义服务也在同一 registry:有效的 `app.Route` 会实时物化为实际 method 与 `/api/x/{slug}/{path}` operation,module 为 `scripts_dynamic`、resource type 为 `custom_scripts`。SDK、身份继承、精确匹配和运行限制仍以 `references/custom-services.md` 为准。
47
-
48
- ### 2. 描述唯一 operation
49
-
50
- ```json
51
- { "operation_id": "search 返回的精确 operation_id" }
52
- ```
53
-
54
- 调用前必须检查 describe 返回的 method、path、parameters、request body、responses、examples、permission、risk、`destructive`、`idempotent`、完整 `input_schema` 和 `response_policy`。路径参数放 `path`,查询参数放 `query`,JSON 请求体放 `body`,文件或表单字段放 `multipart`;只发送 schema 允许的字段。
55
-
56
- describe 缺少与本次调用有关的参数、请求体、responses、permission、risk、`input_schema` 或 `response_policy` 时停止并报告契约不完整,不得自行补字段、改走猜测路径或绕过 MCP。若多个 operation 仍可能匹配,继续缩小搜索范围,而不是任选一个。
57
-
58
- ### 3. 调用
39
+ ## 调用形状
59
40
 
60
41
  ```json
61
42
  {
62
43
  "operation_id": "精确 operation_id",
44
+ "registry_revision": "describe 返回的 revision",
63
45
  "path": {},
64
46
  "query": {},
65
47
  "body": null,
@@ -68,11 +50,11 @@ describe 缺少与本次调用有关的参数、请求体、responses、permissi
68
50
  }
69
51
  ```
70
52
 
71
- 仅传需要的容器。`draftgo_api_call` 会按该 operation `input_schema` 拒绝未知字段,并以当前 MCP principal 执行权限检查;不要传 tenant override、SAT、Authorization 或其他凭据。高风险 operation 只有在用户意图明确、影响范围已核对时才把 `confirm` 设为 `true`;`confirm` 是 MCP 的风险确认字段,不要自行拼接 `X-Confirm-Token` 或调用旧式 reauth 流程。
53
+ 只传 operation schema 需要的容器。路径参数放 `path`,查询参数放 `query`,JSON `body`,文件或表单字段放 `multipart`。不得传 tenant override、SAT、Authorization 或其他凭据。
72
54
 
73
- describe 的 responses 与实际 `status_code` 解析结果。标准管理 API 通常使用 `{code, data, message}`,但 OpenAI 兼容流、模型列表和自定义 Route 等响应可不同;不得对所有 operation 强套统一信封。`response_policy.checkout_required=true` 或长正文资源的完整内容必须转用 `draftgo checkout` / `draftgo commit`,不能塞进 `body`、`multipart` 或工具结果。
55
+ 高风险 operation 仅在用户意图和影响范围明确时设置 `confirm=true`;不要拼 `X-Confirm-Token` 或调用旧 reauth 流程。按 describe 的 responses 与实际 `status_code` 解释结果,不对自定义 Route、OpenAI 兼容流等强套 `{code,data,message}`。`response_policy.checkout_required=true` 时改走 checkout/commit
74
56
 
75
- ## CLI
57
+ ## CLI 与宿主
76
58
 
77
59
  ```bash
78
60
  draftgo mcp setup [target...]
@@ -81,72 +63,26 @@ draftgo mcp test
81
63
  draftgo mcp serve
82
64
  ```
83
65
 
84
- - `setup` 写入项目级宿主配置,只替换 `draftgo` MCP 条目并保留其他配置。
85
- - `status` 检查配置是否存在、格式是否有效,以及是否误写了凭据或远端 URL。
86
- - `test` `.draftgo/config.json` 读取连接,验证 initialize、tools/list、三类正文 resource_list,以及 db_meta 的 api_search、api_describe 和只读 api_call。
87
- - `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。远端重启或 session 过期后,bridge 会清除旧 `Mcp-Session-Id`、重新 initialize,并对被明确拒绝为 session 无效的当前请求重试一次;普通超时、服务端错误和工具错误不会自动重试。
88
-
89
- `draftgo mcp test` 每次使用独立的新连接,因此它证明当前 server、SAT 和协议可以建立新 session,不代表宿主已持有的旧 session 仍然有效。宿主工具报告 session 失效但 `mcp test` 成功时,bridge 应自动恢复;若恢复仍失败,再检查远端日志、代理头传递和服务可用性。
66
+ - `setup` 写项目级宿主配置,只替换 `draftgo` 条目。
67
+ - `status` 检查配置与凭据泄漏风险。
68
+ - `test` 建立独立新连接并验证关键工具;它不证明宿主持有的旧 session 仍有效。
69
+ - `serve` stdio bridge。远端 session 明确失效时重新 initialize 并重放当前请求一次;普通超时、服务错误和工具错误不重试。
90
70
 
91
- `draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;`--server` 表示基础地址,完整 MCP endpoint 使用 `--mcp-url` 显式传入。它不下载业务资源,
92
- 也不创建 pages、navigation、docs、db_meta 等本地镜像。底座仍在开发或暂不可达时,报告诊断结果即可,
93
- 不得回退到 `pull --all`。
71
+ 支持项目级 setup:Codex、Claude Code、Cursor、Gemini CLI、Kiro、GitHub Copilot。Windsurf Antigravity 没有可靠项目级 MCP 配置,CLI 应明确提示不支持。
94
72
 
95
- ## 宿主配置
96
-
97
- | 宿主 | 项目配置 | MCP setup |
98
- |---|---|---|
99
- | Codex | `.codex/config.toml` | 支持 |
100
- | Claude Code | `.mcp.json` | 支持 |
101
- | Cursor | `.cursor/mcp.json` | 支持 |
102
- | Gemini CLI | `.gemini/settings.json` | 支持 |
103
- | Kiro | `.kiro/settings/mcp.json` | 支持 |
104
- | GitHub Copilot | `.vscode/mcp.json` | 支持 |
105
- | Windsurf | 无可靠的项目级适配 | 不支持,CLI 必须明确提示 |
106
- | Antigravity | 无可靠的项目级适配 | 不支持,CLI 必须明确提示 |
107
-
108
- Codex 配置:
109
-
110
- ```toml
111
- [mcp_servers.draftgo]
112
- command = "draftgo"
113
- args = ["mcp", "serve"]
114
- ```
115
-
116
- Claude Code、Cursor、Gemini CLI 和 Kiro 使用各自文件中的 `mcpServers`:
73
+ 宿主配置只运行:
117
74
 
118
75
  ```json
119
- {
120
- "mcpServers": {
121
- "draftgo": {
122
- "command": "draftgo",
123
- "args": ["mcp", "serve"]
124
- }
125
- }
126
- }
76
+ {"command":"draftgo","args":["mcp","serve"]}
127
77
  ```
128
78
 
129
- GitHub Copilot 使用 `servers`,并声明 stdio
130
-
131
- ```json
132
- {
133
- "servers": {
134
- "draftgo": {
135
- "type": "stdio",
136
- "command": "draftgo",
137
- "args": ["mcp", "serve"]
138
- }
139
- }
140
- }
141
- ```
79
+ Codex 使用 `[mcp_servers.draftgo]`;GitHub Copilot 使用 `servers` 且声明 `type="stdio"`;其他支持宿主使用各自 `mcpServers`。配置不得包含 SAT、token、Authorization、headers、远端 `/mcp` URL、`env` 或带凭据命令。bridge 从当前项目 `.draftgo/config.json` 读取 server/SAT。
142
80
 
143
- 配置中不得出现 SAT、token、Authorization、headers、远端 `/mcp` URL、`env` 或带凭据的命令行。
144
- bridge 必须从当前工作目录的 `.draftgo/config.json` 读取 server/SAT。
81
+ `draftgo connect` 只保存并验证连接,不下载业务资源或创建本地镜像。MCP 不可用时运行 `draftgo mcp test` 收集证据。
145
82
 
146
- ## 协议与安全
83
+ ## 安全
147
84
 
148
- - stdio 只输出 MCP JSON-RPC 帧;普通日志写入 stderr,且不得包含 SAT。
149
- - 代理 initialize、tools/list、tools/call、通知、取消、错误和流式响应,不改写底座 schema 或结构化错误。
150
- - HTTP 401/403、协议错误和工具缺失必须清晰失败;输出前对 SAT Authorization 值做脱敏。
151
- - `.draftgo/config.json`、`.draftgo/worktree/``.draftgo/conflicts/` 必须加入 `.gitignore`。
152
- - MCP 不可用时运行 `draftgo mcp test` 收集证据;不要读取旧索引作为替代事实来源。
85
+ - stdio stdout 只输出 MCP JSON-RPC;日志写 stderr,且不得包含 SAT。
86
+ - 代理 initialize、tools/list、tools/call、通知、取消、错误和流式响应,不改写底座结果。
87
+ - HTTP 401/403、协议错误和工具缺失必须清晰失败;输出前脱敏 SAT Authorization
88
+ - `.draftgo/config.json`、`.draftgo/api-contract-cache.json`、worktree 和 conflicts 都是私有运行时状态并应 gitignore
@@ -94,8 +94,9 @@ const orderId = query.orderId; // "42"
94
94
 
95
95
  ### 修改方法
96
96
 
97
- system_config 是结构化远端资源。修改前通过 MCP `draftgo_api_search` / `draftgo_api_describe` 获取实时契约,
98
- 再用 `draftgo_api_call` 读取并更新目标键;不要 checkout、pull 或维护本地索引。
97
+ system_config 是结构化远端资源。未知 operation 才通过 MCP `draftgo_api_search` 定位;首次使用或
98
+ registry revision 变化时 `draftgo_api_describe`,再用 `draftgo_api_call` 读取并更新目标键;不要 checkout
99
+ 或维护本地索引。
99
100
 
100
101
  前端全局层是系统默认配置。更新现有全局配置时只发送 `config_value`;不要带 `description`、`category`、
101
102
  `value_type`、`status` 等元信息,避免触发“系统默认字段不允许修改字段描述/分类/状态”。
@@ -110,9 +110,8 @@ now:
110
110
  - MCP resource_search/get_metadata → 页面与导航元数据(跳过 tag="系统" 的内置页)
111
111
  - MCP api_search/api_describe/api_call → db_meta 与自定义服务结构
112
112
  - 只有必须分析完整 pages/nav/docs 正文时才 checkout;不要要求 MCP 返回全文
113
- - .draftgo/changelog.md → 更新日志
113
+ - .draftgo/worklog.md → 工作进度与完成记录
114
114
  - .draftgo/lessons/ → 开发经验记录
115
- - .draftgo/Task/ → 历史任务文档
116
115
  - 用户提供的设计文档(PRD / 原型说明 / 需求文档)→ 提取产品意图
117
116
 
118
117
  2. 从已有数据推断系统画像,生成推断版 Story(含 design.overview 和 modules)
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const SCHEMA_VERSION = 1;
8
+ const WAIT = new Int32Array(new SharedArrayBuffer(4));
9
+
10
+ function cachePath(projectDir) {
11
+ return path.join(projectDir, '.draftgo', 'api-contract-cache.json');
12
+ }
13
+
14
+ function emptyCache(server) {
15
+ return { schema_version: SCHEMA_VERSION, server, operations: {} };
16
+ }
17
+
18
+ function readCache(projectDir, server) {
19
+ const file = cachePath(projectDir);
20
+ if (!fs.existsSync(file)) return emptyCache(server);
21
+ try {
22
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
23
+ if (!value || value.schema_version !== SCHEMA_VERSION || value.server !== server
24
+ || !value.operations || Array.isArray(value.operations)) return emptyCache(server);
25
+ return value;
26
+ } catch {
27
+ return emptyCache(server);
28
+ }
29
+ }
30
+
31
+ function normalizeDescription(value) {
32
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
33
+ const operation = value.operation && typeof value.operation === 'object' ? value.operation : value;
34
+ const registryRevision = String(value.registry_revision || operation.registry_revision || '').trim();
35
+ const contractHash = String(value.contract_hash || operation.contract_hash || '').trim();
36
+ const operationId = String(operation.operation_id || '').trim();
37
+ if (!operationId || !registryRevision || !contractHash) return null;
38
+ const normalized = { ...operation };
39
+ delete normalized.registry_revision;
40
+ delete normalized.contract_hash;
41
+ return { operation: normalized, registry_revision: registryRevision, contract_hash: contractHash };
42
+ }
43
+
44
+ function cachedDescription(projectDir, server, operationId) {
45
+ const cached = readCache(projectDir, server).operations[String(operationId)];
46
+ return normalizeDescription(cached);
47
+ }
48
+
49
+ function acquire(file) {
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ const lock = `${file}.lock`;
52
+ const started = Date.now();
53
+ while (true) {
54
+ try {
55
+ const descriptor = fs.openSync(lock, 'wx', 0o600);
56
+ fs.closeSync(descriptor);
57
+ return lock;
58
+ } catch (error) {
59
+ if (!error || error.code !== 'EEXIST') throw error;
60
+ }
61
+ try {
62
+ if (Date.now() - fs.statSync(lock).mtimeMs > 30_000) {
63
+ fs.rmSync(lock, { force: true });
64
+ continue;
65
+ }
66
+ } catch (error) {
67
+ if (!error || error.code !== 'ENOENT') throw error;
68
+ continue;
69
+ }
70
+ if (Date.now() - started >= 2000) return null;
71
+ Atomics.wait(WAIT, 0, 0, 10);
72
+ }
73
+ }
74
+
75
+ function writeAtomic(file, value) {
76
+ const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
77
+ try {
78
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
79
+ fs.renameSync(temporary, file);
80
+ } finally {
81
+ try { fs.rmSync(temporary, { force: true }); } catch { /* Best-effort cleanup. */ }
82
+ }
83
+ }
84
+
85
+ function storeDescription(projectDir, server, value) {
86
+ const normalized = normalizeDescription(value);
87
+ if (!normalized) return null;
88
+ const file = cachePath(projectDir);
89
+ const lock = acquire(file);
90
+ if (!lock) return normalized;
91
+ try {
92
+ const cache = readCache(projectDir, server);
93
+ cache.operations[normalized.operation.operation_id] = {
94
+ ...normalized,
95
+ described_at: new Date().toISOString(),
96
+ };
97
+ cache.updated_at = new Date().toISOString();
98
+ writeAtomic(file, cache);
99
+ } finally {
100
+ try { fs.rmSync(lock, { force: true }); } catch { /* Cache writes are best effort. */ }
101
+ }
102
+ return normalized;
103
+ }
104
+
105
+ module.exports = {
106
+ SCHEMA_VERSION,
107
+ cachePath,
108
+ readCache,
109
+ normalizeDescription,
110
+ cachedDescription,
111
+ storeDescription,
112
+ };
package/src/cli.js CHANGED
@@ -13,8 +13,6 @@ const VALUE_FLAGS = new Set([
13
13
  'url',
14
14
  'browser',
15
15
  'browser-path',
16
- 'resource',
17
- 'mobile-check',
18
16
  'ui',
19
17
  'frame',
20
18
  'viewport',
@@ -31,30 +29,12 @@ const VALUE_FLAGS = new Set([
31
29
  'params',
32
30
  'input',
33
31
  'type',
34
- 'title',
35
- 'description',
36
- 'item-id',
37
- 'id',
38
- 'owner',
39
- 'original',
40
- 'original-requirement',
41
- 'clarified',
42
- 'clarified-requirement',
43
- 'expected-effect',
44
- 'effect',
45
- 'scope',
46
- 'out-of-scope',
47
- 'content',
48
- 'acceptance',
49
- 'module-logic',
50
- 'user-journey',
51
- 'item',
52
32
  'date',
53
33
  'handler',
54
34
  'headers',
55
35
  'user',
56
36
  'side-effect-policy',
57
- 'evidence',
37
+ 'note',
58
38
  ]);
59
39
 
60
40
  function parse(argv) {
@@ -5,15 +5,13 @@
5
5
  const commands = [
6
6
  { name: 'init', run: (dir, args, flags) => require('./commands/init')(dir, args, flags) },
7
7
  { name: 'update', run: (dir, args, flags) => require('./commands/update')(dir, args, flags) },
8
- { name: 'uninstall', aliases: ['remove'], run: (dir, args, flags) => require('./commands/uninstall')(dir, args, flags) },
8
+ { name: 'uninstall', run: (dir, args, flags) => require('./commands/uninstall')(dir, args, flags) },
9
9
  { name: 'status', run: (dir) => require('./commands/status')(dir) },
10
- { name: 'changelog', run: (dir, args, flags) => require('./commands/changelog')(dir, args, flags) },
11
- { name: 'task', run: (dir, args, flags) => require('./commands/task')(dir, args, flags) },
10
+ { name: 'work', run: (dir, args, flags) => require('./commands/worklog')(dir, args, flags) },
12
11
  { name: 'clean', run: (dir, args, flags) => require('./commands/clean')(dir, args, flags) },
13
12
  { name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
14
13
  { name: 'check', run: (dir, args, flags) => require('./commands/check')(dir, args, flags) },
15
14
  { name: 'verify', run: (dir, args, flags) => require('./commands/verify')(dir, args, flags) },
16
- { name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUiCompat')(dir, args, flags) },
17
15
  { name: 'mcp', run: (dir, args, flags) => require('./commands/mcp')(dir, args, flags) },
18
16
  { name: 'checkout', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
19
17
  { name: 'refresh', run: (dir, args, flags) => require('./commands/checkout')(dir, args, flags) },
@@ -26,15 +24,12 @@ const commands = [
26
24
  { name: 'conflicts', run: (dir, args, flags) => require('./commands/conflicts')(dir, args, flags) },
27
25
  { name: 'conflict', run: (dir, args, flags) => require('./commands/conflict')(dir, args, flags) },
28
26
  { name: 'api', run: (dir, args, flags) => require('./commands/api')(dir, args, flags) },
29
- { name: 'delete', aliases: ['del', 'rm'], run: (dir, args, flags) => require('./commands/delete')(dir, args, flags) },
27
+ { name: 'delete', run: (dir, args, flags) => require('./commands/delete')(dir, args, flags) },
30
28
  { name: 'deploy', run: (dir, args, flags) => require('./commands/deploy')(dir, args, flags) },
31
- { name: 'auto-push', aliases: ['autopush'], run: (dir, args, flags) => require('./commands/autoPush')(dir, args, flags) },
32
- { name: 'pull', run: (dir, args, flags) => require('./commands/sync')(dir, 'pull', args, flags) },
33
- { name: 'push', run: (dir, args, flags) => require('./commands/sync')(dir, 'push', args, flags) },
29
+ { name: 'auto-push', run: (dir, args, flags) => require('./commands/autoPush')(dir, args, flags) },
34
30
  { name: 'local', run: (dir, args, flags) => require('./commands/local')(dir, args, flags) },
35
- { name: 'list-targets', aliases: ['targets'], run: () => require('./commands/listTargets')() },
36
- { name: 'local-dev', aliases: ['localdev'], run: (dir, args, flags) => require('./commands/localDev')(dir, args, flags) },
37
- { name: 'connect', aliases: ['login'], run: (dir, args, flags) => require('./commands/connect')(dir, args, flags) },
31
+ { name: 'list-targets', run: () => require('./commands/listTargets')() },
32
+ { name: 'connect', run: (dir, args, flags) => require('./commands/connect')(dir, args, flags) },
38
33
  ];
39
34
 
40
35
  const aliases = new Map();
@@ -6,6 +6,7 @@ const log = require('../logger');
6
6
  const { confirm } = require('../prompt');
7
7
  const { loadProjectConfig } = require('../projectConfig');
8
8
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
9
+ const { cachedDescription, normalizeDescription, storeDescription } = require('../apiContractCache');
9
10
 
10
11
  function readCallInput(projectDir, flags) {
11
12
  const raw = flags.input
@@ -66,27 +67,45 @@ async function callOperation(projectDir, operationId, flags = {}) {
66
67
  let session;
67
68
  let description;
68
69
  let input;
70
+ let config;
69
71
  try {
70
- const config = loadProjectConfig(projectDir);
72
+ config = loadProjectConfig(projectDir);
71
73
  session = await openToolSession(config, [TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall]);
72
- description = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: operationId });
74
+ description = cachedDescription(projectDir, config.server, operationId);
75
+ if (!description) {
76
+ const described = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: operationId });
77
+ description = storeDescription(projectDir, config.server, described) || normalizeDescription(described);
78
+ }
79
+ if (!description) throw new Error('DraftGo API description is missing contract revision metadata.');
73
80
  input = readCallInput(projectDir, flags);
74
81
  } catch (error) {
75
82
  return printCallError(operationId, error, flags);
76
83
  }
77
- const destructive = description && (description.destructive === true
78
- || description.operation && description.operation.destructive === true);
84
+ const destructive = description.operation.destructive === true;
79
85
  if (destructive && !flags.yes && !flags.y) {
80
86
  const approved = await confirm(`Call destructive DraftGo operation ${operationId}?`, { default: false });
81
87
  if (!approved) return 0;
82
88
  }
83
89
  let result;
84
90
  try {
85
- result = await callStructured(session, TOOL_NAMES.apiCall, {
86
- ...input,
87
- operation_id: operationId,
88
- ...(destructive ? { confirm: true } : {}),
91
+ const invoke = (contract, confirmed) => callStructured(session, TOOL_NAMES.apiCall, {
92
+ ...input, operation_id: operationId, registry_revision: contract.registry_revision,
93
+ ...(confirmed ? { confirm: true } : {}),
89
94
  });
95
+ try {
96
+ result = await invoke(description, destructive);
97
+ } catch (error) {
98
+ if (error.code !== 'CONTRACT_CHANGED') throw error;
99
+ const described = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: operationId });
100
+ description = storeDescription(projectDir, config.server, described) || normalizeDescription(described);
101
+ if (!description) throw new Error('DraftGo API description is missing contract revision metadata.');
102
+ const changedDestructive = description.operation.destructive === true;
103
+ if (changedDestructive && !destructive && !flags.yes && !flags.y) {
104
+ const approved = await confirm(`Call destructive DraftGo operation ${operationId}?`, { default: false });
105
+ if (!approved) return 0;
106
+ }
107
+ result = await invoke(description, changedDestructive);
108
+ }
90
109
  } catch (error) {
91
110
  return printCallError(operationId, error, flags);
92
111
  }
@@ -130,6 +149,7 @@ async function apiCommand(projectDir, positional, flags = {}) {
130
149
  const result = await callStructured(session, expected, describe
131
150
  ? { operation_id: query }
132
151
  : { query });
152
+ if (describe) storeDescription(projectDir, config.server, result);
133
153
  if (flags.output === 'json') console.log(JSON.stringify(result, null, 2));
134
154
  else printResult(describe ? `DraftGo API ${query}` : `DraftGo API search: ${query}`, result);
135
155
  return 0;
@@ -1,18 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
3
  const log = require('../logger');
6
4
  const { analyzeProject } = require('../projectMap');
7
5
  const { inspectRemoteCheckouts } = require('../worktree/status');
8
6
  const customServices = require('../customServices');
9
7
  const { workspaceHealth } = require('../workspaceHealth');
10
8
 
11
- function legacyCacheDetected(projectDir) {
12
- return ['pages', 'navigations', 'docs', 'db_meta', 'custom_scripts', 'aihub', 'roles', 'users', 'system_config']
13
- .some((name) => fs.existsSync(path.join(projectDir, '.draftgo', name, 'index.json')));
14
- }
15
-
16
9
  async function check(projectDir, positional = [], flags = {}) {
17
10
  if (!Array.isArray(positional)) {
18
11
  flags = positional || {};
@@ -94,7 +87,7 @@ async function check(projectDir, positional = [], flags = {}) {
94
87
  workspace_hygiene: hygiene,
95
88
  };
96
89
  if (flags.output === 'json') {
97
- console.log(JSON.stringify({ ...output, legacy_cache_ignored: legacyCacheDetected(projectDir) }, null, 2));
90
+ console.log(JSON.stringify(output, null, 2));
98
91
  return result.errors.length || (flags.strict && result.warnings.length) ? 1 : 0;
99
92
  }
100
93
 
@@ -111,8 +104,6 @@ async function check(projectDir, positional = [], flags = {}) {
111
104
  if (remote) log.info(`Remote checkout comparison: ${remote.length} resource(s)`);
112
105
  for (const message of result.errors) log.err(message);
113
106
  for (const detail of result.warningDetails) log.warn(`[${detail.code}/${detail.confidence}] ${detail.message}`);
114
- if (legacyCacheDetected(projectDir)) log.dim(' Legacy .draftgo index caches were detected and ignored.');
115
-
116
107
  if (result.errors.length || (flags.strict && result.warnings.length)) return 1;
117
108
  log.ok(result.warnings.length ? 'Local checkout validation completed with warnings.' : 'Local checkout validation passed.');
118
109
  return 0;
@@ -5,14 +5,12 @@ const log = require('../logger');
5
5
  const services = require('../customServices');
6
6
 
7
7
  function ids(positional) { return positional.map(String).filter(Boolean); }
8
- const TYPE_ALIASES = new Set(['custom-services', 'custom-service', 'custom_services', 'custom_service', 'services', 'scripts', 'custom_scripts']);
9
8
  function isServiceType(value) {
10
- return TYPE_ALIASES.has(String(value || '').toLowerCase());
9
+ return String(value || '').toLowerCase() === 'custom-services';
11
10
  }
12
11
  function serviceIds(positional) {
13
12
  const values = ids(positional);
14
- if (isServiceType(values[0])) values.shift();
15
- return values;
13
+ return isServiceType(values[0]) ? values.slice(1) : [];
16
14
  }
17
15
  function output(flags, value) {
18
16
  if (flags.output === 'json') console.log(JSON.stringify(value, null, 2));
@@ -6,23 +6,7 @@ const log = require('../logger');
6
6
  const { confirm } = require('../prompt');
7
7
  const { loadProjectConfig } = require('../projectConfig');
8
8
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
9
-
10
- const LEGACY_TYPES = new Set([
11
- 'page', 'pages', 'nav', 'navigation', 'navigations', 'doc', 'docs', 'article',
12
- 'db_meta', 'custom_scripts', 'doc_categories', 'aihub', 'roles', 'users', 'system_config',
13
- ]);
14
-
15
- function resultItems(value) {
16
- if (Array.isArray(value)) return value;
17
- if (!value || typeof value !== 'object') return [];
18
- if (Array.isArray(value.items)) return value.items;
19
- if (Array.isArray(value.operations)) return value.operations;
20
- return value.data ? resultItems(value.data) : [];
21
- }
22
-
23
- function operationId(value) {
24
- return value && (value.operation_id || value.operationId || value.id);
25
- }
9
+ const { cachedDescription, normalizeDescription, storeDescription } = require('../apiContractCache');
26
10
 
27
11
  function readParams(projectDir, flags) {
28
12
  const raw = flags.params ? String(flags.params)
@@ -45,40 +29,25 @@ function pathParameterName(description) {
45
29
  async function deleteResource(projectDir, positional, flags = {}) {
46
30
  if (!positional.length && !flags['operation-id']) {
47
31
  log.err('Usage: draftgo delete <operation_id> [id] [--params JSON]');
48
- log.dim(' Legacy form is supported when discovery is unambiguous: draftgo delete <type> <id>.');
49
32
  return 1;
50
33
  }
51
34
  const config = loadProjectConfig(projectDir);
52
35
  const session = await openToolSession(config, [
53
- TOOL_NAMES.apiSearch, TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
36
+ TOOL_NAMES.apiDescribe, TOOL_NAMES.apiCall,
54
37
  ]);
55
38
 
56
- const first = String(positional[0] || '');
57
- const legacy = LEGACY_TYPES.has(first.toLowerCase()) && !flags['operation-id'];
58
- let selectedOperation = flags['operation-id'] ? String(flags['operation-id']) : (legacy ? null : first);
59
- const resourceId = legacy ? positional[1] : positional[1];
60
- if (legacy) {
61
- if (!resourceId) {
62
- log.err('Legacy delete form requires a resource id.');
63
- return 1;
64
- }
65
- const search = await callStructured(session, TOOL_NAMES.apiSearch, { query: `delete ${first}` });
66
- const candidates = resultItems(search).filter((item) => operationId(item)
67
- && (!item.method || String(item.method).toUpperCase() === 'DELETE'));
68
- if (candidates.length !== 1) {
69
- log.err(`Delete operation discovery was ambiguous (${candidates.length} matches).`);
70
- log.dim(` Run \`draftgo api delete ${first}\`, then pass --operation-id explicitly.`);
71
- return 1;
72
- }
73
- selectedOperation = operationId(candidates[0]);
74
- }
39
+ const selectedOperation = flags['operation-id'] ? String(flags['operation-id']) : String(positional[0]);
40
+ const resourceId = positional[1];
75
41
 
76
- const description = await callStructured(session, TOOL_NAMES.apiDescribe, {
77
- operation_id: selectedOperation,
78
- });
42
+ let contract = cachedDescription(projectDir, config.server, selectedOperation);
43
+ if (!contract) {
44
+ const described = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: selectedOperation });
45
+ contract = storeDescription(projectDir, config.server, described) || normalizeDescription(described);
46
+ }
47
+ if (!contract) throw new Error('DraftGo API description is missing contract revision metadata.');
79
48
  const params = readParams(projectDir, flags);
80
49
  if (resourceId && Object.keys(params).length === 0) {
81
- const parameterName = pathParameterName(description);
50
+ const parameterName = pathParameterName(contract.operation);
82
51
  if (!parameterName) {
83
52
  log.err('The operation does not expose exactly one path parameter; provide --params JSON explicitly.');
84
53
  return 1;
@@ -89,11 +58,19 @@ async function deleteResource(projectDir, positional, flags = {}) {
89
58
  const approved = await confirm(`Call destructive DraftGo operation ${selectedOperation}?`, { default: false });
90
59
  if (!approved) return 0;
91
60
  }
92
- const result = await callStructured(session, TOOL_NAMES.apiCall, {
93
- ...params,
94
- operation_id: selectedOperation,
95
- confirm: true,
61
+ const invoke = () => callStructured(session, TOOL_NAMES.apiCall, {
62
+ ...params, operation_id: selectedOperation, registry_revision: contract.registry_revision, confirm: true,
96
63
  });
64
+ let result;
65
+ try {
66
+ result = await invoke();
67
+ } catch (error) {
68
+ if (error.code !== 'CONTRACT_CHANGED') throw error;
69
+ const described = await callStructured(session, TOOL_NAMES.apiDescribe, { operation_id: selectedOperation });
70
+ contract = storeDescription(projectDir, config.server, described) || normalizeDescription(described);
71
+ if (!contract) throw new Error('DraftGo API description is missing contract revision metadata.');
72
+ result = await invoke();
73
+ }
97
74
  if (flags.output === 'json') console.log(JSON.stringify(result, null, 2));
98
75
  else log.ok(`DraftGo operation completed: ${selectedOperation}`);
99
76
  return 0;
@@ -23,7 +23,7 @@ async function deploy(projectDir, positional, flags = {}) {
23
23
 
24
24
  const [resourceType, ...ids] = positional;
25
25
  if (!resourceType || !ids.length) {
26
- log.err('Deploy no longer commits every legacy index. Specify <pages|nav|docs> <id...>.');
26
+ log.err('Usage: draftgo deploy <pages|nav|docs> <id...> --delivery preview|deploy');
27
27
  return 1;
28
28
  }
29
29
  if (delivery === 'preview') {