@leeguoo/yapi-mcp 0.4.0 → 0.4.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.
package/README.md CHANGED
@@ -54,27 +54,46 @@ Yapi Auto MCP Server 是一个基于 [Model Context Protocol](https://modelconte
54
54
 
55
55
  ### 推荐方式:用 Cross Request Master 一键安装 Skill(免手动找 Token)
56
56
 
57
- 如果你日常就在浏览器里使用 YApi,推荐安装 Chrome 扩展 [cross-request-master](https://github.com/leeguooooo/cross-request-master)。它会在 YApi 接口详情页(基本信息区域右上角)提供 **「YApi 工具箱」** 按钮,包含 Skill 一键安装(推荐)/MCP 配置(兼容)/CLI docs-sync 说明;另外保留 **「复制给 AI」** 一键复制接口 Markdown:
57
+ 如果你日常就在浏览器里使用 YApi,推荐安装 Chrome 扩展 [cross-request-master](https://github.com/leeguooooo/cross-request-master)。它会在 YApi 接口详情页(基本信息区域右上角)提供 **「YApi 工具箱」** 按钮,包含 Skill 一键安装(推荐,支持 `npx skills add`)/MCP 配置(兼容)/CLI docs-sync 说明;另外保留 **「复制给 AI」** 一键复制接口 Markdown:
58
58
 
59
- - Skill 一键安装(推荐):生成 Codex/Claude Skill,并写入全局配置 `~/.yapi/config.toml`
59
+ - Skill 一键安装(推荐):优先生成 `npx skills add` 命令安装仓库导出的 Skill,再配合 `yapi config init` 初始化全局配置;如需一步写入配置,保留 `yapi install-skill` 兼容路径
60
60
  - MCP 配置(兼容):使用 `--yapi-auth-mode=global`(账号密码),默认会自动懒登录;也可手动调用一次 `yapi_update_token` 预热缓存
61
61
  - CLI 使用与 docs-sync:提供本地 CLI 安装命令和文档同步示例
62
62
 
63
63
  ### Skill 一键安装与 CLI
64
64
 
65
- 一条命令把 Skill 安装到 Codex / Claude Code / Cursor,并写入 `~/.yapi/config.toml`(缺省会提示输入):
65
+ 推荐先用 `skills` 安装仓库导出的 Skill:
66
66
 
67
67
  ```bash
68
- # 推荐:全局安装后使用 yapi 命令
68
+ # 推荐:把仓库里的 yapi Skill 装到全局 agent 目录
69
+ npx skills add leeguooooo/cross-request-master -y -g
70
+ ```
71
+
72
+ 这条命令只负责安装 Skill 文件,不会写入 `~/.yapi/config.toml`。如果你还没有本地配置,继续执行下面任一方式:
73
+
74
+ ```bash
75
+ # 方式一:推荐单独初始化配置
69
76
  npm install -g @leeguoo/yapi-mcp
77
+ yapi config init \
78
+ --base-url=https://your-yapi-domain.com \
79
+ --auth-mode=global \
80
+ --email=your_email@example.com
70
81
 
82
+ # 如未保存密码,首次再同步一次浏览器登录态
83
+ yapi login --base-url=https://your-yapi-domain.com --browser
84
+ ```
85
+
86
+ 也可以继续使用兼容命令,在安装 Skill 的同时写入 `~/.yapi/config.toml`:
87
+
88
+ ```bash
89
+ npm install -g @leeguoo/yapi-mcp
71
90
  yapi install-skill \
72
91
  --yapi-base-url=https://your-yapi-domain.com \
73
92
  --yapi-email=your_email@example.com \
74
93
  --yapi-password=your_password
75
94
  ```
76
95
 
77
- 也可以用 npx 临时运行(不全局安装):
96
+ 也可以用 npx 临时运行兼容命令(不全局安装):
78
97
 
79
98
  ```bash
80
99
  npx -y -p @leeguoo/yapi-mcp yapi install-skill \
@@ -83,22 +102,20 @@ npx -y -p @leeguoo/yapi-mcp yapi install-skill \
83
102
  --yapi-password=your_password
84
103
  ```
85
104
 
86
- Skill 安装目录:
87
- - Codex: `~/.codex/skills/yapi/`
88
- - Claude Code: `~/.claude/skills/yapi/`
89
- - Cursor: `~/.cursor/skills/yapi/`
105
+ `skills` CLI 会维护一份规范化安装副本,并按目标 agent 建立链接/映射。当前全局安装通常可在 `~/.agents/skills/yapi/` 看到 canonical copy,具体 agent 侧落点由 `skills` CLI 决定。
90
106
 
91
- Skill 模板来源:`packages/yapi-mcp/skill-template/SKILL.md`(发布后从包内复制到技能目录)。
107
+ 仓库导出的 Skill 来源:`skills/yapi/SKILL.md`(供 `npx skills add` 发现)。
108
+ npm 包内安装模板来源:`packages/yapi-mcp/skill-template/SKILL.md`(供 `yapi install-skill` 复制到技能目录)。
92
109
  后续当 CLI 升级而本地 Skill 仍是旧版本时,`yapi` 会自动提示:
93
110
 
94
111
  ```bash
95
- skill update available: installed Codex@0.3.24, current 0.3.25. Run: yapi install-skill --force
112
+ skill update available: installed Codex@0.3.24, current 0.3.25. Run: npx skills add leeguooooo/cross-request-master -y -g
96
113
  ```
97
114
 
98
115
  也可以手动重装 Skill:
99
116
 
100
117
  ```bash
101
- yapi install-skill --force
118
+ npx skills add leeguooooo/cross-request-master -y -g
102
119
  ```
103
120
 
104
121
  ### CLI 使用
@@ -106,13 +123,14 @@ yapi install-skill --force
106
123
  推荐全局安装后直接使用 `yapi` 命令(走同一份 `~/.yapi/config.toml`):
107
124
 
108
125
  当前 CLI 能力补充:
126
+ - 支持 `yapi config init`:单独初始化/更新 `~/.yapi/config.toml`,适合和 `npx skills add` 组合使用
109
127
  - 支持 `yapi login --browser`:通过 `agent-browser-stealth` 打开页面,登录后自动同步 `_yapi_token/_yapi_uid` 到本地缓存
110
128
  - 默认打开 `base-url` 首页(不强制 `/login`),适配“已登录可直接拿 Cookie”的场景
111
129
  - 支持 `yapi login --login-url <url>` 指定登录页
112
130
  - 支持 `yapi logout` 清理当前 `base_url` 对应的全局会话缓存
113
131
  - 适用于 SSO/额外验证体系:无法使用账号密码时可只走浏览器登录
114
132
  - 支持 `yapi self-update` 升级全局 CLI
115
- - 当已安装的 Skill 版本落后于当前 CLI 时,会自动提示重新执行 `yapi install-skill --force`
133
+ - 当已安装的 Skill 版本落后于当前 CLI 时,会自动提示重新执行 `npx skills add leeguooooo/cross-request-master -y -g`
116
134
 
117
135
  ```bash
118
136
  # 检查版本
@@ -194,12 +212,14 @@ yapi docs-sync
194
212
  - 绑定模式同步后会写入 `.yapi/docs-sync.deployments.json`(本地文档 → 已部署 URL)
195
213
  - 兼容旧方式:`--dir` 读取目录内 `.yapi.json` 的 `project_id/catid` 与 `source_files`
196
214
  - 管理绑定:`yapi docs-sync bind list|get|add|update|remove`
215
+ - 也可以在运行时临时过滤文件:`yapi docs-sync --binding projectA --source-file architecture.md`;优先级是 `--source-file` > 绑定里的 `source_files` > 目录全量扫描
197
216
  - `--query` 支持像 curl 一样写成单个字符串:`--query "catid=4631&limit=50&page=1"`
198
217
  - 可用 `--dry-run` 只做预览不更新;现在会输出每个文件的 Markdown/HTML/请求体大小,并提前暴露超大文档风险
199
218
  - 默认只同步内容变更的文件,如需全量更新使用 `--force`
200
219
  - 普通同步命中相同 `file_hashes` 时会在渲染前直接跳过,不再重复渲染 Mermaid / PlantUML / Graphviz / D2;`--dry-run` 仍会保留预览渲染
201
- - 如果上传返回 `413 Payload Too Large`,CLI 会显示当前请求大小、解析出的服务端限制值(如果响应里有)、以及最大的 Mermaid 块大小,并建议先拆分文档
220
+ - 如果上传返回 `413 Payload Too Large`,CLI 会按 `默认 Mermaid -> --mermaid-classic -> --no-mermaid` 自动降级;某个文件一旦降级成功,会把该模式记住,后续同步优先直接使用,避免每次先撞一次 413
202
221
  - Mermaid 预渲染依赖 `mmdc`(默认手绘风格;安装时会尝试拉取,失败不影响同步)
222
+ - 如果 `mmdc` 已安装但提示缺少 `chrome-headless-shell` / Puppeteer 浏览器,执行:`npx puppeteer browsers install chrome-headless-shell`
203
223
  - PlantUML 预渲染依赖 `plantuml`(需要本机 Java 环境)
204
224
  - Graphviz 预渲染依赖 `dot`(graphviz)
205
225
  - D2 预渲染依赖 `d2`(默认手绘风格输出)
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const REQUIRED_DEPS = ["yargs", "axios"];
5
+ const missing = [];
6
+ for (const dep of REQUIRED_DEPS) {
7
+ try {
8
+ require.resolve(dep);
9
+ }
10
+ catch {
11
+ missing.push(dep);
12
+ }
13
+ }
14
+ if (missing.length > 0) {
15
+ const path = require("path");
16
+ const pkgDir = path.resolve(__dirname, "..");
17
+ const lines = [
18
+ `[yapi-cli] Missing runtime dependencies: ${missing.join(", ")}`,
19
+ "",
20
+ "This usually means the package directory has no node_modules installed.",
21
+ "",
22
+ "To fix, run one of the following in the package directory:",
23
+ ` cd "${pkgDir}"`,
24
+ " pnpm install # if using pnpm (recommended for this repo)",
25
+ " # or",
26
+ " npm install # if using npm",
27
+ "",
28
+ "If you installed @leeguoo/yapi-mcp globally, try reinstalling:",
29
+ " npm i -g @leeguoo/yapi-mcp",
30
+ ];
31
+ console.error(lines.join("\n"));
32
+ process.exit(1);
33
+ }
34
+ require("./yapi-cli");
35
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";;;AAQA,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAEzC,MAAM,OAAO,GAAa,EAAE,CAAC;AAC7B,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;IAChC,IAAI,CAAC;QACH,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACvB,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAA0B,CAAC;IACtD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG;QACZ,4CAA4C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAChE,EAAE;QACF,yEAAyE;QACzE,EAAE;QACF,4DAA4D;QAC5D,SAAS,MAAM,GAAG;QAClB,sEAAsE;QACtE,QAAQ;QACR,yCAAyC;QACzC,EAAE;QACF,gEAAgE;QAChE,8BAA8B;KAC/B,CAAC;IAEF,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAChC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAGD,OAAO,CAAC,YAAY,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Options } from "../types";
2
+ export declare function runConfig(action: string, options: Options): Promise<number>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runConfig = runConfig;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const utils_1 = require("../utils");
9
+ function resolveAuthMode(explicit, existing, merged) {
10
+ const normalized = String(explicit || existing.auth_mode || "").trim().toLowerCase();
11
+ if (!normalized) {
12
+ return merged.token ? "token" : "global";
13
+ }
14
+ if (normalized === "token" || normalized === "global")
15
+ return normalized;
16
+ return null;
17
+ }
18
+ async function runConfig(action, options) {
19
+ const normalizedAction = String(action || "init").trim().toLowerCase() || "init";
20
+ if (normalizedAction !== "init") {
21
+ console.error(`unknown config action: ${normalizedAction}`);
22
+ return 2;
23
+ }
24
+ const configPath = options.config || (0, utils_1.globalConfigPath)();
25
+ const existing = fs_1.default.existsSync(configPath)
26
+ ? (0, utils_1.parseSimpleToml)(fs_1.default.readFileSync(configPath, "utf8"))
27
+ : {};
28
+ const merged = { ...existing };
29
+ if (options.baseUrl !== undefined)
30
+ merged.base_url = options.baseUrl;
31
+ if (options.email !== undefined)
32
+ merged.email = options.email;
33
+ if (options.password !== undefined)
34
+ merged.password = options.password;
35
+ if (options.token !== undefined)
36
+ merged.token = options.token;
37
+ if (options.projectId !== undefined)
38
+ merged.project_id = options.projectId;
39
+ if (!merged.base_url) {
40
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
41
+ console.error("missing --base-url");
42
+ return 2;
43
+ }
44
+ merged.base_url = await (0, utils_1.promptRequired)("YApi base URL: ", false);
45
+ }
46
+ const authMode = resolveAuthMode(options.authMode || "", existing, merged);
47
+ if (!authMode) {
48
+ console.error("invalid --auth-mode (use token or global)");
49
+ return 2;
50
+ }
51
+ merged.auth_mode = authMode;
52
+ merged.email = merged.email || "";
53
+ merged.password = merged.password || "";
54
+ merged.token = merged.token || "";
55
+ merged.project_id = merged.project_id || "";
56
+ (0, utils_1.writeConfig)(configPath, merged);
57
+ console.log(`Config written to: ${configPath}`);
58
+ if (authMode === "global" && !merged.password) {
59
+ console.log("Global auth configured without saved password. Run `yapi login --base-url <url> --browser` once to sync cookie, or rerun with --password to enable password relogin.");
60
+ }
61
+ if (authMode === "token" && !merged.token) {
62
+ console.log("Token mode configured without token. Pass --token now or set it later before requests.");
63
+ }
64
+ return 0;
65
+ }
66
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/cli/commands/config.ts"],"names":[],"mappings":"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"}
@@ -149,20 +149,6 @@ function saveDocsSyncDeployments(homeDir, config) {
149
149
  const configPath = docsSyncDeploymentsPath(homeDir);
150
150
  fs_1.default.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
151
151
  }
152
- function resolveBindingDir(rootDir, bindingDir) {
153
- if (!bindingDir)
154
- return rootDir;
155
- return path_1.default.isAbsolute(bindingDir) ? bindingDir : path_1.default.resolve(rootDir, bindingDir);
156
- }
157
- function normalizeBindingDir(rootDir, bindingDir) {
158
- const resolved = resolveBindingDir(rootDir, bindingDir);
159
- const relative = path_1.default.relative(rootDir, resolved);
160
- if (!relative || relative === ".")
161
- return ".";
162
- if (relative.startsWith("..") || path_1.default.isAbsolute(relative))
163
- return resolved;
164
- return relative;
165
- }
166
152
  function getBindingBaseDir(homeDir, rootDir, cwd) {
167
153
  if (!isGlobalDocsSyncHome(homeDir)) {
168
154
  return { baseDir: rootDir, gitRoot: (0, utils_1.findGitRoot)(cwd), usedGitRoot: false };
@@ -241,6 +227,26 @@ function resolveSourceFiles(dirPath, mapping) {
241
227
  return resolved;
242
228
  });
243
229
  }
230
+ function cloneMappingForSync(mapping, sourceFiles) {
231
+ const next = {
232
+ ...mapping,
233
+ files: mapping.files ? { ...mapping.files } : {},
234
+ file_hashes: mapping.file_hashes ? { ...mapping.file_hashes } : {},
235
+ file_render_modes: mapping.file_render_modes ? { ...mapping.file_render_modes } : {},
236
+ };
237
+ if (sourceFiles?.length) {
238
+ next.source_files = [...sourceFiles];
239
+ }
240
+ return next;
241
+ }
242
+ function mergeSyncState(target, source) {
243
+ target.project_id = source.project_id;
244
+ target.catid = source.catid;
245
+ target.template_id = source.template_id;
246
+ target.files = source.files ? { ...source.files } : {};
247
+ target.file_hashes = source.file_hashes ? { ...source.file_hashes } : {};
248
+ target.file_render_modes = source.file_render_modes ? { ...source.file_render_modes } : {};
249
+ }
244
250
  async function listExistingInterfaces(catId, request) {
245
251
  const resp = await request("/api/interface/list_cat", "GET", {
246
252
  catid: catId,
@@ -319,7 +325,7 @@ function buildDocsSyncPreviewLine(item) {
319
325
  }
320
326
  return `preview ${parts.join(" ")}`;
321
327
  }
322
- function buildDocsSyncPayloadTooLargeMessage(fileName, preview, error) {
328
+ function buildDocsSyncPayloadTooLargeMessage(fileName, preview, error, attempts = []) {
323
329
  const lines = [
324
330
  `413 Payload Too Large while syncing ${fileName}`,
325
331
  `- request payload: ${(0, utils_1.formatBytes)(preview.payloadBytes)}`,
@@ -339,10 +345,85 @@ function buildDocsSyncPayloadTooLargeMessage(fileName, preview, error) {
339
345
  else {
340
346
  lines.push("- largest Mermaid block: none");
341
347
  }
348
+ attempts.forEach((attempt) => {
349
+ if (attempt.mode === "default")
350
+ return;
351
+ const label = attempt.mode === "classic" ? "--mermaid-classic" : "--no-mermaid";
352
+ if (attempt.payloadBytes && attempt.htmlBytes) {
353
+ lines.push(`- retry with ${label}: payload=${(0, utils_1.formatBytes)(attempt.payloadBytes)} html=${(0, utils_1.formatBytes)(attempt.htmlBytes)}`);
354
+ }
355
+ if (attempt.message) {
356
+ lines.push(`- ${label} retry result: ${attempt.message}`);
357
+ }
358
+ });
342
359
  lines.push("- suggestion: run `yapi docs-sync --dry-run ...` to preview all files before upload");
343
360
  lines.push("- suggestion: split oversized Mermaid diagrams or move them into separate docs");
361
+ lines.push("- suggestion: if even `--no-mermaid` still hits 413, the remaining limit is on the YApi server/proxy side");
344
362
  return lines.join("\n");
345
363
  }
364
+ function resolveRenderModeLabel(mode) {
365
+ if (mode === "classic")
366
+ return "--mermaid-classic";
367
+ if (mode === "no-mermaid")
368
+ return "--no-mermaid";
369
+ return "default Mermaid rendering";
370
+ }
371
+ function resolveDocsSyncOptionsForMode(options, mode) {
372
+ if (mode === "classic") {
373
+ return {
374
+ ...options,
375
+ noMermaid: false,
376
+ mermaidLook: "classic",
377
+ };
378
+ }
379
+ if (mode === "no-mermaid") {
380
+ return {
381
+ ...options,
382
+ noMermaid: true,
383
+ };
384
+ }
385
+ return { ...options };
386
+ }
387
+ function resolveInitialRenderMode(options, rememberedMode) {
388
+ if (options.noMermaid)
389
+ return "no-mermaid";
390
+ if (options.mermaidLook === "classic")
391
+ return "classic";
392
+ if (rememberedMode)
393
+ return rememberedMode;
394
+ return "default";
395
+ }
396
+ function resolveRetryModes(mode, hasMermaidBlocks) {
397
+ if (!hasMermaidBlocks)
398
+ return [];
399
+ if (mode === "default")
400
+ return ["classic", "no-mermaid"];
401
+ if (mode === "classic")
402
+ return ["no-mermaid"];
403
+ return [];
404
+ }
405
+ function renderDocsSyncHtml(markdown, options, logPrefix) {
406
+ let mermaidFailed = false;
407
+ let diagramFailed = false;
408
+ const diagramMetrics = [];
409
+ const html = (0, markdown_1.renderMarkdownToHtml)(markdown, {
410
+ noMermaid: options.noMermaid,
411
+ logMermaid: true,
412
+ mermaidLook: options.mermaidLook,
413
+ mermaidHandDrawnSeed: options.mermaidHandDrawnSeed,
414
+ logger: (message) => console.log(`${logPrefix} ${message}`),
415
+ onDiagramRendered: (metric) => {
416
+ diagramMetrics.push(metric);
417
+ },
418
+ onMermaidError: () => {
419
+ mermaidFailed = true;
420
+ },
421
+ onDiagramError: () => {
422
+ diagramFailed = true;
423
+ },
424
+ });
425
+ return { html, mermaidFailed, diagramFailed, diagramMetrics };
426
+ }
346
427
  async function addInterface(title, apiPath, mapping, request) {
347
428
  const projectId = Number(mapping.project_id || 0);
348
429
  const catId = Number(mapping.catid || 0);
@@ -432,7 +513,17 @@ async function syncDocsDir(dirPath, mapping, options, request) {
432
513
  const resolvedPath = byId[String(docId)]?.path || apiPath;
433
514
  fileInfos[relName] = { docId: Number(docId), apiPath: resolvedPath };
434
515
  }
435
- const contentHash = (0, utils_1.buildDocsSyncHash)(markdown, options);
516
+ const logPrefix = `[docs-sync:${relName}]`;
517
+ const hasMermaidBlocks = /```mermaid\s*\r?\n/i.test(markdown);
518
+ const rememberedMode = !options.noMermaid && options.mermaidLook === "handDrawn"
519
+ ? mapping.file_render_modes?.[relName]
520
+ : undefined;
521
+ let appliedMode = resolveInitialRenderMode(options, rememberedMode);
522
+ let effectiveOptions = resolveDocsSyncOptionsForMode(options, appliedMode);
523
+ if (rememberedMode) {
524
+ console.log(`${logPrefix} using remembered fallback: ${resolveRenderModeLabel(appliedMode)}.`);
525
+ }
526
+ let contentHash = (0, utils_1.buildDocsSyncHash)(markdown, effectiveOptions);
436
527
  const previousHash = mapping.file_hashes[relName];
437
528
  const currentTitle = docId ? byId[String(docId)]?.title : "";
438
529
  const titleToUpdate = !docId
@@ -450,26 +541,7 @@ async function syncDocsDir(dirPath, mapping, options, request) {
450
541
  skipped += 1;
451
542
  continue;
452
543
  }
453
- const logPrefix = `[docs-sync:${relName}]`;
454
- let mermaidFailed = false;
455
- let diagramFailed = false;
456
- const diagramMetrics = [];
457
- const html = (0, markdown_1.renderMarkdownToHtml)(markdown, {
458
- noMermaid: options.noMermaid,
459
- logMermaid: true,
460
- mermaidLook: options.mermaidLook,
461
- mermaidHandDrawnSeed: options.mermaidHandDrawnSeed,
462
- logger: (message) => console.log(`${logPrefix} ${message}`),
463
- onDiagramRendered: (metric) => {
464
- diagramMetrics.push(metric);
465
- },
466
- onMermaidError: () => {
467
- mermaidFailed = true;
468
- },
469
- onDiagramError: () => {
470
- diagramFailed = true;
471
- },
472
- });
544
+ let { html, mermaidFailed, diagramFailed, diagramMetrics } = renderDocsSyncHtml(markdown, effectiveOptions, logPrefix);
473
545
  if (shouldSkipUnchanged) {
474
546
  action = "skip";
475
547
  skipped += 1;
@@ -494,11 +566,54 @@ async function syncDocsDir(dirPath, mapping, options, request) {
494
566
  }
495
567
  catch (error) {
496
568
  if (error instanceof http_1.HttpStatusError && error.status === 413) {
497
- throw new Error(buildDocsSyncPayloadTooLargeMessage(relName, preview, error));
569
+ const attempts = [];
570
+ let resolved = false;
571
+ for (const retryMode of resolveRetryModes(appliedMode, hasMermaidBlocks)) {
572
+ console.warn(`${logPrefix} 413 received, retrying with ${resolveRenderModeLabel(retryMode)} for this file.`);
573
+ const retryAttempt = { mode: retryMode };
574
+ attempts.push(retryAttempt);
575
+ try {
576
+ const retryOptions = resolveDocsSyncOptionsForMode(options, retryMode);
577
+ const retryResult = renderDocsSyncHtml(markdown, retryOptions, logPrefix);
578
+ const retryPayload = buildUpdatePayload(docId, titleToUpdate, markdown, retryResult.html);
579
+ retryAttempt.payloadBytes = Buffer.byteLength(JSON.stringify(retryPayload), "utf8");
580
+ retryAttempt.htmlBytes = Buffer.byteLength(retryResult.html, "utf8");
581
+ await updateInterface(docId, titleToUpdate, markdown, retryResult.html, request);
582
+ html = retryResult.html;
583
+ mermaidFailed = retryResult.mermaidFailed;
584
+ diagramFailed = retryResult.diagramFailed;
585
+ diagramMetrics = retryResult.diagramMetrics;
586
+ appliedMode = retryMode;
587
+ effectiveOptions = retryOptions;
588
+ contentHash = (0, utils_1.buildDocsSyncHash)(markdown, effectiveOptions);
589
+ resolved = true;
590
+ break;
591
+ }
592
+ catch (retryError) {
593
+ retryAttempt.message =
594
+ retryError instanceof Error ? retryError.message : String(retryError);
595
+ }
596
+ }
597
+ if (!resolved) {
598
+ throw new Error(buildDocsSyncPayloadTooLargeMessage(relName, preview, error, attempts));
599
+ }
600
+ }
601
+ else {
602
+ throw error;
498
603
  }
499
- throw error;
500
604
  }
501
605
  }
606
+ if (appliedMode === "default") {
607
+ if (mapping.file_render_modes) {
608
+ delete mapping.file_render_modes[relName];
609
+ }
610
+ }
611
+ else {
612
+ if (!mapping.file_render_modes || typeof mapping.file_render_modes !== "object") {
613
+ mapping.file_render_modes = {};
614
+ }
615
+ mapping.file_render_modes[relName] = appliedMode;
616
+ }
502
617
  if (docId && !mermaidFailed && !diagramFailed) {
503
618
  mapping.file_hashes[relName] = contentHash;
504
619
  }
@@ -740,6 +855,15 @@ async function runDocsSync(options) {
740
855
  console.warn("mmdc not found, Mermaid blocks will stay as code.");
741
856
  console.warn("Install mermaid-cli: npm i -g @mermaid-js/mermaid-cli");
742
857
  }
858
+ else if (!options.noMermaid) {
859
+ const mermaidRuntime = (0, markdown_1.probeMermaidRuntime)({
860
+ look: options.mermaidLook,
861
+ handDrawnSeed: options.mermaidHandDrawnSeed,
862
+ });
863
+ if (!mermaidRuntime.ok) {
864
+ console.warn(`mermaid runtime check failed: ${mermaidRuntime.message}`);
865
+ }
866
+ }
743
867
  if (!(0, markdown_1.isPlantUmlAvailable)()) {
744
868
  console.warn("plantuml not found, PlantUML blocks will be removed from HTML.");
745
869
  console.warn("Install PlantUML (macOS): brew install plantuml");
@@ -970,12 +1094,14 @@ async function runDocsSync(options) {
970
1094
  if (!fs_1.default.existsSync(dirPath) || !fs_1.default.statSync(dirPath).isDirectory()) {
971
1095
  throw new Error(`dir not found for binding ${name}: ${dirPath}`);
972
1096
  }
973
- const result = await syncDocsDir(dirPath, binding, options, request);
1097
+ const runtimeBinding = cloneMappingForSync(binding, options.sourceFiles);
1098
+ const result = await syncDocsDir(dirPath, runtimeBinding, options, request);
974
1099
  if (options.dryRun) {
975
1100
  console.log(`dry-run preview binding=${name}`);
976
1101
  result.previews.forEach((item) => console.log(buildDocsSyncPreviewLine(item)));
977
1102
  }
978
1103
  console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} preview_only=${result.previewOnly} binding=${name} dir=${dirPath}`);
1104
+ mergeSyncState(binding, runtimeBinding);
979
1105
  bindingResults[name] = { binding, files: result.files };
980
1106
  }
981
1107
  if (!options.dryRun) {
@@ -990,12 +1116,14 @@ async function runDocsSync(options) {
990
1116
  throw new Error(`dir not found: ${dirPath}`);
991
1117
  }
992
1118
  const { mapping, mappingPath } = loadMapping(dirPath);
993
- const result = await syncDocsDir(dirPath, mapping, options, request);
1119
+ const runtimeMapping = cloneMappingForSync(mapping, options.sourceFiles);
1120
+ const result = await syncDocsDir(dirPath, runtimeMapping, options, request);
994
1121
  if (options.dryRun) {
995
1122
  console.log(`dry-run preview dir=${dirPath}`);
996
1123
  result.previews.forEach((item) => console.log(buildDocsSyncPreviewLine(item)));
997
1124
  }
998
1125
  if (!options.dryRun) {
1126
+ mergeSyncState(mapping, runtimeMapping);
999
1127
  saveMapping(mapping, mappingPath);
1000
1128
  }
1001
1129
  console.log(`synced=${result.updated} created=${result.created} skipped=${result.skipped} preview_only=${result.previewOnly} dir=${dirPath}`);