diff --git a/CHANGELOG.md b/CHANGELOG.md index ea9dd2c5..a2450f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +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 — 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. - 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..85606516 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,73 @@ 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); + }); + + 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/__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/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..e83dc7be 100644 --- a/scripts/translate-docs/mdx-translator.ts +++ b/scripts/translate-docs/mdx-translator.ts @@ -94,6 +94,60 @@ 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 { + // 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) { + 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 => + fenceRanges.some(([start, end]) => offset >= start && offset < end); + + 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 +229,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..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 } from "./mdx-translator"; +import { + stripStrayTrailingFence, + convertHtmlComments, + sanitizeJsxAttributes, +} from "./mdx-translator"; import { readCache, writeCache, isCached, setCacheEntry } from "./cache"; import type { TranslationResult, TranslationCache } from "./types"; @@ -103,10 +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. - const cleaned = 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; }