From 71a6352f0ed27358b19120b747a365dbdc279af8 Mon Sep 17 00:00:00 2001 From: Nivedit Jain Date: Thu, 16 Jul 2026 13:17:23 +0000 Subject: [PATCH 1/3] fix(docs): convert HTML comments to MDX comments in translated READMEs The Mintlify deployment failed on every `docs/i18n/README.*.md`: Mintlify parses each page as MDX, where the README's top-level HTML comment (``) is a hard syntax error ("Unexpected character `!` ... to create a comment in MDX, use the brace-slash-star form"), so all 14 locale READMEs failed to publish. Add a fence-aware `convertHtmlComments` sanitizer (a sibling of the existing `stripStrayTrailingFence` / `sanitizeJsxAttributes` MDX sanitizers) that rewrites HTML comments to MDX comments on translator output, leaving `` inside code fences literal (e.g. the AgentEye collector plist sample). Wire it into both `readme-translator.ts` and `translateMdxPage`, regenerate the 14 committed translations to match, and unit-test the helper. The root `README.md` keeps HTML-comment syntax (GitHub renders it invisibly); only the Mintlify-served copies under docs/i18n/ are rewritten. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Eg5Bgx3ucKNJNZ2f2Ng5dQ --- CHANGELOG.md | 1 + .../translate-docs/mdx-translator.test.ts | 48 +++++++++++++++++ docs/i18n/README.ar.md | 4 +- docs/i18n/README.de.md | 4 +- docs/i18n/README.es.md | 4 +- docs/i18n/README.fr.md | 4 +- docs/i18n/README.he.md | 4 +- docs/i18n/README.hi.md | 4 +- docs/i18n/README.it.md | 4 +- docs/i18n/README.ja.md | 4 +- docs/i18n/README.ko.md | 4 +- docs/i18n/README.pt-br.md | 4 +- docs/i18n/README.ru.md | 4 +- docs/i18n/README.tr.md | 4 +- docs/i18n/README.vi.md | 4 +- docs/i18n/README.zh.md | 4 +- scripts/translate-docs/mdx-translator.ts | 54 +++++++++++++++++-- scripts/translate-docs/readme-translator.ts | 7 +-- 18 files changed, 132 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea9dd2c5..479c4b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Surface OpenClaw sessions in the dashboard's audit + history browser: reads the real JSONL transcripts at `~/.openclaw/agents//sessions/.jsonl` (skipping the heavy `.trajectory.jsonl` OTel traces) via `lib/openclaw-sessions.ts` (a pure, unit-tested type-discriminated parser that pairs assistant `toolCall` blocks with their `toolResult` by `toolCallId`) and `lib/openclaw-projects.ts` (reads the `sessions.json` index, groups by agentId into `openclaw-` projects, parses channel metadata from the sessionKey). Wired through `src/audit/cli-adapters/openclaw.ts`, `lib/cli-registry.ts` (teal badge), `lib/projects.ts`, `lib/download-session.ts` (real-file download — no synthesis needed), and the `app/project/[name]` routes. (#489) ### Fixes +- Fix the Mintlify docs deployment failing on every `docs/i18n/README.*.md` translation. Mintlify parses each page as MDX, where the README's top-level HTML comment (``) is a hard syntax error (`Unexpected character `!` … to create a comment in MDX, use {/* text */}`), so all 14 locale READMEs failed to publish. The README translator now converts HTML comments to MDX comments (`{/* … */}`) on output via a new fence-aware `convertHtmlComments` sanitizer — applied in both `readme-translator.ts` and `translateMdxPage`, and leaving `` inside code fences literal (e.g. the AgentEye collector plist sample) — and the 14 committed translations are regenerated to match. (#535) - Fix Copilot CLI file policies silently allowing file access (re-verified live against Copilot CLI 1.0.71, whose hook contract drifted since the 1.0.41 verification). Copilot's snake_case hook events deliver canonical tool names but its own input keys — Read `{path}`, Write `{path, file_text}`, Edit `{path, old_str, new_str}` — so path/content builtins like `block-env-files` never fired (a live `.env` read was observed passing). Adds `COPILOT_TOOL_INPUT_MAP` mapping them to `file_path`/`content`/`old_string`/`new_string`. Also normalizes the `permissionRequest` event's camelCase payload (`toolName`/`toolInput`/`sessionId`, lowercase tool names) in the handler so PermissionRequest-matched policies (e.g. `block-sudo`'s escalation guard) fire instead of seeing a null tool name. Verified end-to-end: the previously-passing `.env` read replay is now denied, and Copilot 1.0.71 honors the deny (`code:"denied"`, tool never runs). (#516) - Make documentation translation publishing atomic: require every locale and cache artifact, pin the Mintlify validator, validate the final overlaid PR tree, and emit the canonical `pt-BR` Mintlify locale while preserving `pt-br` paths. - Make the dashboard header responsive. On narrow windows the single-row header crushed its children: the active nav tab clipped mid-word ("policie…"), the version string wrapped mid-token ("V0.0.14-/BETA.1"), and "Reach Us" broke onto two lines. The version + section cluster now never wraps mid-token and sheds entirely below 900px (it duplicates the active-tab highlight), tab/header padding tightens, and below 620px the actions row wraps under the nav right-aligned instead of crushing it. Verified headless at 1500/760/480px. (#516) diff --git a/__tests__/scripts/translate-docs/mdx-translator.test.ts b/__tests__/scripts/translate-docs/mdx-translator.test.ts index 1546f58d..3d5be87f 100644 --- a/__tests__/scripts/translate-docs/mdx-translator.test.ts +++ b/__tests__/scripts/translate-docs/mdx-translator.test.ts @@ -4,6 +4,7 @@ import { rewriteInternalLinks, sanitizeJsxAttributes, stripStrayTrailingFence, + convertHtmlComments, getEnglishMdxPages, } from "@/scripts/translate-docs/mdx-translator"; @@ -187,3 +188,50 @@ describe("stripStrayTrailingFence", () => { expect(stripStrayTrailingFence(input)).toBe(input); }); }); + +describe("convertHtmlComments", () => { + it("converts a single-line HTML comment to an MDX comment", () => { + expect(convertHtmlComments("")).toBe("{/* hello */}"); + }); + + it("converts a multi-line HTML comment, preserving inner content", () => { + const input = + "## Heading\n\n\n
\n"; + const expected = + "## Heading\n\n{/* A note about the\n table layout below. */}\n
\n"; + expect(convertHtmlComments(input)).toBe(expected); + }); + + it("keeps HTML that only looks like a comment inside inline text intact", () => { + // Regression guard for the real README note that mentions ``. + const input = ""; + expect(convertHtmlComments(input)).toBe( + "{/* prefer a table over inline runs */}", + ); + }); + + it("leaves HTML comments inside fenced code blocks untouched", () => { + // A plist sample in a ```xml fence must stay literal — MDX does not parse + // fenced content, so the collector-installation docs are already valid. + const input = + "```xml\n\n\n```\n"; + expect(convertHtmlComments(input)).toBe(input); + }); + + it("converts a top-level comment but not one nested in a later fence", () => { + const input = + "\n\n```html\n\n```\n"; + const expected = + "{/* top note */}\n\n```html\n\n```\n"; + expect(convertHtmlComments(input)).toBe(expected); + }); + + it("neutralizes a `*/` inside the body so the MDX comment can't close early", () => { + expect(convertHtmlComments("")).toBe("{/* a * / b */}"); + }); + + it("returns content with no comments unchanged", () => { + const input = "# Title\n\nJust prose, no comments here.\n"; + expect(convertHtmlComments(input)).toBe(input); + }); +}); diff --git a/docs/i18n/README.ar.md b/docs/i18n/README.ar.md index c159d96f..c60d2ba2 100644 --- a/docs/i18n/README.ar.md +++ b/docs/i18n/README.ar.md @@ -33,9 +33,9 @@ ## واجهات سطر الأوامر للعملاء المدعومة - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.de.md b/docs/i18n/README.de.md index f647e879..90979bda 100644 --- a/docs/i18n/README.de.md +++ b/docs/i18n/README.de.md @@ -31,9 +31,9 @@ bevor sie zu Vorfällen werden. Nulllatenz. Läuft lokal. ## Unterstützte Agenten-CLIs - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.es.md b/docs/i18n/README.es.md index dd9c7bc1..6ff54a1d 100644 --- a/docs/i18n/README.es.md +++ b/docs/i18n/README.es.md @@ -31,9 +31,9 @@ antes de que se conviertan en incidentes. Latencia cero. Se ejecuta localmente. ## CLIs de agentes compatibles - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.fr.md b/docs/i18n/README.fr.md index 1396260f..cc58d227 100644 --- a/docs/i18n/README.fr.md +++ b/docs/i18n/README.fr.md @@ -31,9 +31,9 @@ avant qu'ils ne deviennent des incidents. Zéro latence. Fonctionne en local. ## CLI d'agents pris en charge - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.he.md b/docs/i18n/README.he.md index 80d2838f..38f926de 100644 --- a/docs/i18n/README.he.md +++ b/docs/i18n/README.he.md @@ -33,9 +33,9 @@ ## ממשקי CLI של סוכנים נתמכים - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.hi.md b/docs/i18n/README.hi.md index cfd9e882..a7afe8d3 100644 --- a/docs/i18n/README.hi.md +++ b/docs/i18n/README.hi.md @@ -31,9 +31,9 @@ Claude Code और Codex में हुक करता है। लूप् ## समर्थित एजेंट CLIs - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.it.md b/docs/i18n/README.it.md index 368861c2..2f0587c3 100644 --- a/docs/i18n/README.it.md +++ b/docs/i18n/README.it.md @@ -31,9 +31,9 @@ prima che diventino incidenti. Zero latenza. Eseguito localmente. ## CLI agenti supportati - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.ja.md b/docs/i18n/README.ja.md index 8e08c2e9..dfd0f31c 100644 --- a/docs/i18n/README.ja.md +++ b/docs/i18n/README.ja.md @@ -31,9 +31,9 @@ Claude Code および Codex にフックし、ループ・危険な操作・シ ## 対応エージェント CLI - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.ko.md b/docs/i18n/README.ko.md index 7ddf52c3..d37dd616 100644 --- a/docs/i18n/README.ko.md +++ b/docs/i18n/README.ko.md @@ -31,9 +31,9 @@ Claude Code 및 Codex에 훅으로 연결됩니다. 루프, 위험한 작업, ## 지원되는 에이전트 CLI - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.pt-br.md b/docs/i18n/README.pt-br.md index 5039de03..4061533a 100644 --- a/docs/i18n/README.pt-br.md +++ b/docs/i18n/README.pt-br.md @@ -31,9 +31,9 @@ antes que se tornem incidentes. Latência zero. Executa localmente. ## CLIs de agentes suportados - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.ru.md b/docs/i18n/README.ru.md index b7d7356e..ba5c573d 100644 --- a/docs/i18n/README.ru.md +++ b/docs/i18n/README.ru.md @@ -31,9 +31,9 @@ ## Поддерживаемые CLI агентов - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.tr.md b/docs/i18n/README.tr.md index 0ebdf104..d6b62cf5 100644 --- a/docs/i18n/README.tr.md +++ b/docs/i18n/README.tr.md @@ -31,9 +31,9 @@ olay haline gelmeden yakalar. Sıfır gecikme. Yerel olarak çalışır. ## Desteklenen ajan CLIleri - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.vi.md b/docs/i18n/README.vi.md index 2ba5c51c..360cc3d6 100644 --- a/docs/i18n/README.vi.md +++ b/docs/i18n/README.vi.md @@ -31,9 +31,9 @@ trước khi chúng trở thành sự cố. Không độ trễ. Chạy cục b ## Các CLI agent được hỗ trợ - + instead of collapsing into ragged orphan rows). */}
diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index 83ae5fdd..30a57549 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -30,9 +30,9 @@ ## 支持的 Agent CLI - + instead of collapsing into ragged orphan rows). */}
diff --git a/scripts/translate-docs/mdx-translator.ts b/scripts/translate-docs/mdx-translator.ts index 69f71128..f283c040 100644 --- a/scripts/translate-docs/mdx-translator.ts +++ b/scripts/translate-docs/mdx-translator.ts @@ -94,6 +94,52 @@ export function stripStrayTrailingFence(content: string): string { return lines.join("\n"); } +/** + * Convert HTML comments (``) into MDX brace-slash-star comments. + * + * Mintlify parses every page as MDX, where a top-level HTML comment is a hard + * syntax error ("Unexpected character `!` (U+0021) before name …") that fails + * the whole deployment — the remedy Mintlify itself suggests is switching to + * MDX's own comment form. The root README keeps HTML-comment syntax (GitHub + * renders it invisibly), so its translated copies under docs/i18n/ have to be + * rewritten to the MDX form or the docs deploy breaks. + * + * Comments inside fenced code blocks are left untouched — there `` is + * literal sample text (e.g. the plist snippet in the AgentEye collector docs), + * not a comment to convert. Any `*`+`/` sequence inside a body is broken up so + * the generated JS block comment can't be terminated early. + */ +export function convertHtmlComments(content: string): string { + // Record the offset of every fenced-code marker (```… or ~~~…) at line start + // so we can tell whether a given comment sits inside a code block. An odd + // number of markers before an offset means it is inside an open fence. + const fenceOffsets: number[] = []; + const fenceRe = /^[ \t]*(?:`{3,}|~{3,})/gm; + let fenceMatch: RegExpExecArray | null; + while ((fenceMatch = fenceRe.exec(content)) !== null) { + fenceOffsets.push(fenceMatch.index); + } + + const isInsideFence = (offset: number): boolean => { + let markers = 0; + for (const pos of fenceOffsets) { + if (pos >= offset) break; + markers++; + } + return markers % 2 === 1; + }; + + return content.replace( + //g, + (match: string, body: string, offset: number) => { + if (isInsideFence(offset)) return match; + // Neutralize any `*/` so it can't close the JS block comment early. + const safeBody = body.replace(/\*\//g, "* /"); + return `{/*${safeBody}*/}`; + }, + ); +} + /** * Rewrite internal doc links to include the language prefix. * e.g. href="/built-in-policies" -> href="/es/built-in-policies" @@ -175,9 +221,11 @@ export async function translateMdxPage( ); // Strip stray quote artifacts from JSX attribute values, drop any - // unmatched trailing code fence the model sometimes hallucinates, then - // rewrite links. - const sanitized = stripStrayTrailingFence(sanitizeJsxAttributes(translated)); + // unmatched trailing code fence the model sometimes hallucinates, convert + // any HTML comments to MDX comments, then rewrite links. + const sanitized = convertHtmlComments( + stripStrayTrailingFence(sanitizeJsxAttributes(translated)), + ); const withLinks = rewriteInternalLinks(sanitized, lang); // Write output diff --git a/scripts/translate-docs/readme-translator.ts b/scripts/translate-docs/readme-translator.ts index 38bc955d..40810df2 100644 --- a/scripts/translate-docs/readme-translator.ts +++ b/scripts/translate-docs/readme-translator.ts @@ -3,7 +3,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { LANGUAGES, getLanguageByCode } from "./config"; import { translateContent } from "./translator"; -import { stripStrayTrailingFence } from "./mdx-translator"; +import { stripStrayTrailingFence, convertHtmlComments } from "./mdx-translator"; import { readCache, writeCache, isCached, setCacheEntry } from "./cache"; import type { TranslationResult, TranslationCache } from "./types"; @@ -105,8 +105,9 @@ export async function translateReadme( // Drop any stray trailing fence the model hallucinated — would otherwise // open an unclosed code block that swallows the wrapping `` for RTL - // pages and break Mintlify's MDX parser. - const cleaned = stripStrayTrailingFence(translated); + // pages and break Mintlify's MDX parser — and convert the README's HTML + // comments to MDX comments, which Mintlify parses as MDX and would reject. + const cleaned = convertHtmlComments(stripStrayTrailingFence(translated)); const output = `${disclaimer}\n\n${langSelector}\n\n---\n${rtlOpen}\n${cleaned}\n${rtlClose}`; From 7ae1d995d81b7670abd6e049800f643344c3e703 Mon Sep 17 00:00:00 2001 From: Nivedit Jain Date: Thu, 16 Jul 2026 13:31:55 +0000 Subject: [PATCH 2/3] fix(docs): validate .md pages in validate:mdx and sanitize README JSX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `validate:mdx` CI safety net only walked `.mdx` files, but Mintlify parses `.md` pages (e.g. the docs/i18n/README translations) as MDX too — so their breakage sailed past CI and only failed at the post-merge deploy, which is exactly how the HTML-comment failure reached `main`. `collectMdxFiles` now collects `.md` as well, so this whole class of translation breakage fails on the PR instead of the deploy. Also align `readme-translator.ts` with `translateMdxPage`: the README emits JSX (the logo table), so run `sanitizeJsxAttributes` over its output too. Tests: cover `collectMdxFiles` (.md + .mdx collected, others ignored) and add `findMdxParseError` regressions asserting a top-level `` fails to parse while the `{/* */}` form and a fenced `` parse cleanly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Eg5Bgx3ucKNJNZ2f2Ng5dQ --- CHANGELOG.md | 3 +- __tests__/scripts/validate-mdx.test.ts | 46 +++++++++++++++++++++ scripts/translate-docs/readme-translator.ts | 19 ++++++--- scripts/validate-mdx.ts | 16 +++++-- 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 479c4b75..c68b6ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ - Surface OpenClaw sessions in the dashboard's audit + history browser: reads the real JSONL transcripts at `~/.openclaw/agents//sessions/.jsonl` (skipping the heavy `.trajectory.jsonl` OTel traces) via `lib/openclaw-sessions.ts` (a pure, unit-tested type-discriminated parser that pairs assistant `toolCall` blocks with their `toolResult` by `toolCallId`) and `lib/openclaw-projects.ts` (reads the `sessions.json` index, groups by agentId into `openclaw-` projects, parses channel metadata from the sessionKey). Wired through `src/audit/cli-adapters/openclaw.ts`, `lib/cli-registry.ts` (teal badge), `lib/projects.ts`, `lib/download-session.ts` (real-file download — no synthesis needed), and the `app/project/[name]` routes. (#489) ### Fixes -- Fix the Mintlify docs deployment failing on every `docs/i18n/README.*.md` translation. Mintlify parses each page as MDX, where the README's top-level HTML comment (``) is a hard syntax error (`Unexpected character `!` … to create a comment in MDX, use {/* text */}`), so all 14 locale READMEs failed to publish. The README translator now converts HTML comments to MDX comments (`{/* … */}`) on output via a new fence-aware `convertHtmlComments` sanitizer — applied in both `readme-translator.ts` and `translateMdxPage`, and leaving `` inside code fences literal (e.g. the AgentEye collector plist sample) — and the 14 committed translations are regenerated to match. (#535) +- Fix the Mintlify docs deployment failing on every `docs/i18n/README.*.md` translation. Mintlify parses each page as MDX, where the README's top-level HTML comment (``) is a hard syntax error (`Unexpected character `!` … to create a comment in MDX, use {/* text */}`), so all 14 locale READMEs failed to publish. The README translator now converts HTML comments to MDX comments (`{/* … */}`) on output via a new fence-aware `convertHtmlComments` sanitizer — applied in both `readme-translator.ts` and `translateMdxPage`, and leaving `` inside code fences literal (e.g. the AgentEye collector plist sample) — and the 14 committed translations are regenerated to match. Also aligns `readme-translator.ts` with `translateMdxPage` by running `sanitizeJsxAttributes` over its JSX-bearing output. (#535) +- Close the gap that let the above reach `main`: the `validate:mdx` CI safety net (`scripts/validate-mdx.ts`) only walked `.mdx` files, but Mintlify parses `.md` pages as MDX too — so the `docs/i18n/README.*.md` breakage sailed past CI and only failed at the post-merge deploy. `collectMdxFiles` now validates `.md` pages as well, so this whole class of translation breakage fails on the PR instead. (#535) - Fix Copilot CLI file policies silently allowing file access (re-verified live against Copilot CLI 1.0.71, whose hook contract drifted since the 1.0.41 verification). Copilot's snake_case hook events deliver canonical tool names but its own input keys — Read `{path}`, Write `{path, file_text}`, Edit `{path, old_str, new_str}` — so path/content builtins like `block-env-files` never fired (a live `.env` read was observed passing). Adds `COPILOT_TOOL_INPUT_MAP` mapping them to `file_path`/`content`/`old_string`/`new_string`. Also normalizes the `permissionRequest` event's camelCase payload (`toolName`/`toolInput`/`sessionId`, lowercase tool names) in the handler so PermissionRequest-matched policies (e.g. `block-sudo`'s escalation guard) fire instead of seeing a null tool name. Verified end-to-end: the previously-passing `.env` read replay is now denied, and Copilot 1.0.71 honors the deny (`code:"denied"`, tool never runs). (#516) - Make documentation translation publishing atomic: require every locale and cache artifact, pin the Mintlify validator, validate the final overlaid PR tree, and emit the canonical `pt-BR` Mintlify locale while preserving `pt-br` paths. - Make the dashboard header responsive. On narrow windows the single-row header crushed its children: the active nav tab clipped mid-word ("policie…"), the version string wrapped mid-token ("V0.0.14-/BETA.1"), and "Reach Us" broke onto two lines. The version + section cluster now never wraps mid-token and sheds entirely below 900px (it duplicates the active-tab highlight), tab/header padding tightens, and below 620px the actions row wraps under the nav right-aligned instead of crushing it. Verified headless at 1500/760/480px. (#516) diff --git a/__tests__/scripts/validate-mdx.test.ts b/__tests__/scripts/validate-mdx.test.ts index 671f020e..081c85d4 100644 --- a/__tests__/scripts/validate-mdx.test.ts +++ b/__tests__/scripts/validate-mdx.test.ts @@ -1,6 +1,10 @@ // @vitest-environment node import { describe, it, expect } from "vitest"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { join, relative } from "node:path"; +import { tmpdir } from "node:os"; import { + collectMdxFiles, encodeAnnotation, findMdxParseError, stripFrontmatter, @@ -84,4 +88,46 @@ describe("findMdxParseError", () => { expect(error).not.toBeNull(); expect(error?.line).toBe(5); }); + + it("flags a top-level HTML comment (the docs/i18n README deploy failure)", async () => { + // Mintlify parses .md/.mdx as MDX, where `` is a hard syntax error + // ("Unexpected character `!` … use the MDX comment form"). This is exactly + // what broke every docs/i18n/README.*.md translation at deploy time. + const error = await findMdxParseError("# Title\n\n\n"); + expect(error).not.toBeNull(); + expect(error?.message).toMatch(/character/i); + }); + + it("accepts the MDX comment form the translator now emits", async () => { + expect(await findMdxParseError("# Title\n\n{/* a note */}\n")).toBeNull(); + }); + + it("leaves an HTML comment inside a code fence alone", async () => { + // Fenced content is literal in MDX, so the AgentEye collector plist sample + // keeps its `` — which is why convertHtmlComments skips fences. + const src = "```xml\n\n```\n"; + expect(await findMdxParseError(src)).toBeNull(); + }); +}); + +describe("collectMdxFiles", () => { + it("collects .md and .mdx pages recursively but ignores other files", () => { + // Regression guard: Mintlify parses .md pages (e.g. the docs/i18n READMEs) + // as MDX too, so the walk must not be restricted to .mdx. + const dir = mkdtempSync(join(tmpdir(), "validate-mdx-collect-")); + try { + writeFileSync(join(dir, "page.mdx"), "# mdx\n"); + writeFileSync(join(dir, "readme.md"), "# md\n"); + writeFileSync(join(dir, "notes.txt"), "ignore me\n"); + mkdirSync(join(dir, "sub")); + writeFileSync(join(dir, "sub", "nested.md"), "# nested\n"); + + const found = collectMdxFiles(dir) + .map((f) => relative(dir, f)) + .sort(); + expect(found).toEqual(["page.mdx", "readme.md", "sub/nested.md"].sort()); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); }); diff --git a/scripts/translate-docs/readme-translator.ts b/scripts/translate-docs/readme-translator.ts index 40810df2..0214d045 100644 --- a/scripts/translate-docs/readme-translator.ts +++ b/scripts/translate-docs/readme-translator.ts @@ -3,7 +3,11 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { LANGUAGES, getLanguageByCode } from "./config"; import { translateContent } from "./translator"; -import { stripStrayTrailingFence, convertHtmlComments } from "./mdx-translator"; +import { + stripStrayTrailingFence, + convertHtmlComments, + sanitizeJsxAttributes, +} from "./mdx-translator"; import { readCache, writeCache, isCached, setCacheEntry } from "./cache"; import type { TranslationResult, TranslationCache } from "./types"; @@ -103,11 +107,14 @@ export async function translateReadme( const rtlOpen = langConfig.rtl ? `
\n\n` : ""; const rtlClose = langConfig.rtl ? `\n\n
` : ""; - // Drop any stray trailing fence the model hallucinated — would otherwise - // open an unclosed code block that swallows the wrapping `` for RTL - // pages and break Mintlify's MDX parser — and convert the README's HTML - // comments to MDX comments, which Mintlify parses as MDX and would reject. - const cleaned = convertHtmlComments(stripStrayTrailingFence(translated)); + // Run the same MDX sanitizers as translateMdxPage — the README emits JSX + // (the logo table), so its output has to satisfy Mintlify's MDX parser too: + // strip stray quote artifacts from JSX attributes, drop any unmatched + // trailing code fence the model hallucinates (which would swallow the RTL + // `` wrapper), and convert HTML comments to MDX comments. + const cleaned = convertHtmlComments( + stripStrayTrailingFence(sanitizeJsxAttributes(translated)), + ); const output = `${disclaimer}\n\n${langSelector}\n\n---\n${rtlOpen}\n${cleaned}\n${rtlClose}`; diff --git a/scripts/validate-mdx.ts b/scripts/validate-mdx.ts index 22b0341b..ff4074ee 100644 --- a/scripts/validate-mdx.ts +++ b/scripts/validate-mdx.ts @@ -1,6 +1,6 @@ /** - * Validate that every docs MDX page parses with the same MDX engine Mintlify - * runs at deploy time. + * Validate that every docs page (`.mdx` and `.md` — Mintlify parses both as + * MDX) parses with the same MDX engine Mintlify runs at deploy time. * * Why this exists: `mintlify validate` (the existing `docs` CI job) only checks * `docs.json` structure and nav-link resolution — it does NOT parse page @@ -91,12 +91,20 @@ export function encodeAnnotation(value: string): string { .replace(/\n/g, "%0A"); } -function collectMdxFiles(dir: string): string[] { +/** + * Collect every Mintlify content page under `dir`. Mintlify runs BOTH `.mdx` + * and `.md` files through the same MDX pipeline at deploy time, so a `.md` page + * with an MDX syntax error (e.g. an HTML `` comment, which MDX rejects) + * fails the deploy exactly like a broken `.mdx` would. The docs/i18n README + * translations are `.md`, so restricting this walk to `.mdx` let their breakage + * sail past this safety net and reach the post-merge deploy — collect both. + */ +export function collectMdxFiles(dir: string): string[] { const out: string[] = []; for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) out.push(...collectMdxFiles(full)); - else if (entry.endsWith(".mdx")) out.push(full); + else if (entry.endsWith(".mdx") || entry.endsWith(".md")) out.push(full); } return out; } From 1d72e4ba8c16fb5c4ad2c9be86efb6fcb66e9267 Mon Sep 17 00:00:00 2001 From: Nivedit Jain Date: Thu, 16 Jul 2026 13:41:09 +0000 Subject: [PATCH 3/3] fix(docs): robust fence tracking in convertHtmlComments (CodeRabbit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit review on PR #535: - Critical: the fence detection counted "any ``` / ~~~ toggles", which desyncs on a ```` block embedding ``` or on mixed ```/~~~ fences — a real top-level comment after such a block would be left unconverted and break the deploy. Replace with CommonMark-style tracking: a fence closes only on a later line with the SAME character and length >= the opener; different-char or shorter markers while open are inner content; an unterminated fence runs to EOF. Add regressions for quad-tick-embeds-triple, tilde-inside-backtick, and unterminated-fence. - Minor: fix a nested-backtick Markdown code span in the CHANGELOG entry that would render broken on GitHub. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Eg5Bgx3ucKNJNZ2f2Ng5dQ --- CHANGELOG.md | 2 +- .../translate-docs/mdx-translator.test.ts | 23 ++++++++++++ scripts/translate-docs/mdx-translator.ts | 36 +++++++++++-------- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c68b6ec5..a2450f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Surface OpenClaw sessions in the dashboard's audit + history browser: reads the real JSONL transcripts at `~/.openclaw/agents//sessions/.jsonl` (skipping the heavy `.trajectory.jsonl` OTel traces) via `lib/openclaw-sessions.ts` (a pure, unit-tested type-discriminated parser that pairs assistant `toolCall` blocks with their `toolResult` by `toolCallId`) and `lib/openclaw-projects.ts` (reads the `sessions.json` index, groups by agentId into `openclaw-` projects, parses channel metadata from the sessionKey). Wired through `src/audit/cli-adapters/openclaw.ts`, `lib/cli-registry.ts` (teal badge), `lib/projects.ts`, `lib/download-session.ts` (real-file download — no synthesis needed), and the `app/project/[name]` routes. (#489) ### Fixes -- Fix the Mintlify docs deployment failing on every `docs/i18n/README.*.md` translation. Mintlify parses each page as MDX, where the README's top-level HTML comment (``) is a hard syntax error (`Unexpected character `!` … to create a comment in MDX, use {/* text */}`), so all 14 locale READMEs failed to publish. The README translator now converts HTML comments to MDX comments (`{/* … */}`) on output via a new fence-aware `convertHtmlComments` sanitizer — applied in both `readme-translator.ts` and `translateMdxPage`, and leaving `` inside code fences literal (e.g. the AgentEye collector plist sample) — and the 14 committed translations are regenerated to match. Also aligns `readme-translator.ts` with `translateMdxPage` by running `sanitizeJsxAttributes` over its JSX-bearing output. (#535) +- Fix the Mintlify docs deployment failing on every `docs/i18n/README.*.md` translation. Mintlify parses each page as MDX, where the README's top-level HTML comment (``) is a hard syntax error — it rejects the leading `!` of `` inside code fences literal (e.g. the AgentEye collector plist sample) — and the 14 committed translations are regenerated to match. Also aligns `readme-translator.ts` with `translateMdxPage` by running `sanitizeJsxAttributes` over its JSX-bearing output. (#535) - Close the gap that let the above reach `main`: the `validate:mdx` CI safety net (`scripts/validate-mdx.ts`) only walked `.mdx` files, but Mintlify parses `.md` pages as MDX too — so the `docs/i18n/README.*.md` breakage sailed past CI and only failed at the post-merge deploy. `collectMdxFiles` now validates `.md` pages as well, so this whole class of translation breakage fails on the PR instead. (#535) - Fix Copilot CLI file policies silently allowing file access (re-verified live against Copilot CLI 1.0.71, whose hook contract drifted since the 1.0.41 verification). Copilot's snake_case hook events deliver canonical tool names but its own input keys — Read `{path}`, Write `{path, file_text}`, Edit `{path, old_str, new_str}` — so path/content builtins like `block-env-files` never fired (a live `.env` read was observed passing). Adds `COPILOT_TOOL_INPUT_MAP` mapping them to `file_path`/`content`/`old_string`/`new_string`. Also normalizes the `permissionRequest` event's camelCase payload (`toolName`/`toolInput`/`sessionId`, lowercase tool names) in the handler so PermissionRequest-matched policies (e.g. `block-sudo`'s escalation guard) fire instead of seeing a null tool name. Verified end-to-end: the previously-passing `.env` read replay is now denied, and Copilot 1.0.71 honors the deny (`code:"denied"`, tool never runs). (#516) - Make documentation translation publishing atomic: require every locale and cache artifact, pin the Mintlify validator, validate the final overlaid PR tree, and emit the canonical `pt-BR` Mintlify locale while preserving `pt-br` paths. diff --git a/__tests__/scripts/translate-docs/mdx-translator.test.ts b/__tests__/scripts/translate-docs/mdx-translator.test.ts index 3d5be87f..85606516 100644 --- a/__tests__/scripts/translate-docs/mdx-translator.test.ts +++ b/__tests__/scripts/translate-docs/mdx-translator.test.ts @@ -234,4 +234,27 @@ describe("convertHtmlComments", () => { const input = "# Title\n\nJust prose, no comments here.\n"; expect(convertHtmlComments(input)).toBe(input); }); + + it("converts a top-level comment after a ```` block that embeds ```", () => { + // A generic fence toggle would count the inner ``` as a boundary and leave + // this comment mis-flagged as inside a fence, breaking the deploy. + const input = "````\ninner ``` fence\n````\n\n\n"; + const expected = "````\ninner ``` fence\n````\n\n{/* note */}\n"; + expect(convertHtmlComments(input)).toBe(expected); + }); + + it("does not treat a ~~~ line inside a ``` block as a fence boundary", () => { + // The tilde line is inner content of the backtick fence, so the comment on + // the next line stays literal; only the comment after the fence converts. + const input = + "```\n~~~ still inside\n\n```\n\n\n"; + const expected = + "```\n~~~ still inside\n\n```\n\n{/* outside */}\n"; + expect(convertHtmlComments(input)).toBe(expected); + }); + + it("treats an unterminated fence as running to end of document", () => { + const input = "```\n\n"; + expect(convertHtmlComments(input)).toBe(input); + }); }); diff --git a/scripts/translate-docs/mdx-translator.ts b/scripts/translate-docs/mdx-translator.ts index f283c040..e83dc7be 100644 --- a/scripts/translate-docs/mdx-translator.ts +++ b/scripts/translate-docs/mdx-translator.ts @@ -110,24 +110,32 @@ export function stripStrayTrailingFence(content: string): string { * the generated JS block comment can't be terminated early. */ export function convertHtmlComments(content: string): string { - // Record the offset of every fenced-code marker (```… or ~~~…) at line start - // so we can tell whether a given comment sits inside a code block. An odd - // number of markers before an offset means it is inside an open fence. - const fenceOffsets: number[] = []; - const fenceRe = /^[ \t]*(?:`{3,}|~{3,})/gm; + // Map out fenced-code ranges so comments inside them stay literal. Per + // CommonMark, a fence opens with ≥3 backticks or tildes and closes only on a + // later line using the SAME character and at least the same length. A naive + // "any ``` toggles" counter misfires on a ```` block that embeds ``` or on + // mixed ```/~~~ fences — the toggle desyncs and a real top-level comment + // after the block would be left unconverted (breaking the deploy we fix here). + const fenceRanges: Array<[number, number]> = []; + const fenceRe = /^[ \t]*(`{3,}|~{3,})/gm; let fenceMatch: RegExpExecArray | null; + let open: { char: string; length: number; start: number } | null = null; while ((fenceMatch = fenceRe.exec(content)) !== null) { - fenceOffsets.push(fenceMatch.index); + const marker = fenceMatch[1]; + if (!open) { + open = { char: marker[0], length: marker.length, start: fenceMatch.index }; + } else if (marker[0] === open.char && marker.length >= open.length) { + const lineEnd = content.indexOf("\n", fenceRe.lastIndex); + fenceRanges.push([open.start, lineEnd === -1 ? content.length : lineEnd]); + open = null; + } + // A different char or shorter marker while a fence is open is inner content. } + // An unterminated fence runs to the end of the document. + if (open) fenceRanges.push([open.start, content.length]); - const isInsideFence = (offset: number): boolean => { - let markers = 0; - for (const pos of fenceOffsets) { - if (pos >= offset) break; - markers++; - } - return markers % 2 === 1; - }; + const isInsideFence = (offset: number): boolean => + fenceRanges.some(([start, end]) => offset >= start && offset < end); return content.replace( //g,