@suzumiyaaoba/mdxr 0.12.0 → 0.14.0

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
@@ -10,6 +10,7 @@ Run directly with `npx` — no install required:
10
10
  npx @suzumiyaaoba/mdxr render plan.mdx # writes plan.html
11
11
  npx @suzumiyaaoba/mdxr serve plan.mdx # live preview at http://localhost:3737
12
12
  npx @suzumiyaaoba/mdxr catalog # list available components
13
+ npx @suzumiyaaoba/mdxr --version # print the package version
13
14
  npx @suzumiyaaoba/mdxr init # install mdxr instructions for Codex and Claude
14
15
  ```
15
16
 
@@ -76,6 +77,8 @@ mdxr init --skill # install the Agent Skill instead
76
77
 
77
78
  The global install creates `$CODEX_HOME/MDXR.md` (or `~/.codex/MDXR.md` when `CODEX_HOME` is unset) and its `mdxr/references/`, plus matching files under `~/.claude/`. It adds a read instruction to `$CODEX_HOME/AGENTS.md`; Claude's `~/.claude/CLAUDE.md` imports `@./MDXR.md`. Use `--local` to create `.codex/MDXR.md` and `.claude/MDXR.md` in the project, with references from the root `AGENTS.md` and `CLAUDE.md`. A local install also adds `.mdxr/` to `.gitignore` for agent-authored documents and rendered HTML. `--force` refreshes generated instruction files and references while preserving the rest of your config files.
78
79
 
80
+ The bundled prompts tell the agent to compare their recorded version with `--version` from the CLI used for the task. If they differ, the agent reports both versions and recommends refreshing with `init --force` from that same CLI, preserving the original tool and install scope; Skill installs also keep `--skill`. See [Agent setup](docs/docs/agent-skill.mdx) for details.
81
+
79
82
  ## Wireframes
80
83
 
81
84
  Build screen mockups with the built-in `Wireframe*` components, adapted from [wireframe-ui](https://wireframe-ui.vercel.app/components). All 45 upstream UI families and nine blocks are included, with their compound parts and wireframe helpers. No imports or extra installation are needed in MDX:
@@ -107,7 +110,7 @@ Once the instructions or skill are installed, ask your agent, for example:
107
110
 
108
111
  > Write an implementation plan for the auth feature and render it to HTML with mdxr.
109
112
 
110
- Plans, reports, reviews, investigation summaries, release notes, and postmortems all work. By default, new documents use `.mdxr/yyyyMMddhhmmss-<name>.mdx`, with a zero-padded 14-digit local creation time in 24-hour format (for example, `.mdxr/20260929140530-auth-plan.mdx`). A filename or file path you provide takes priority. If you specify only a directory, the document gets a timestamped filename there.
113
+ Plans, reports, reviews, investigation summaries, release notes, and postmortems all work. By default, new documents use `.mdxr/yyyyMMddhhmmss-<name>/index.mdx`, with a zero-padded 14-digit local creation time in 24-hour format (for example, `.mdxr/20260929140530-auth-plan/index.mdx`). Keep images and related MDX files in the document's directory. A file path you provide takes priority, and existing documents keep their current paths. If you specify only a directory, create a timestamped subdirectory there and save the document as `index.mdx`. Preview it with `mdxr serve .mdxr/20260929140530-auth-plan/index.mdx`.
111
114
 
112
115
  ## License
113
116
 
package/dist/cli.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { Bv as fenceLang, Dh as RESEARCH_CLAIM_SCHEMA, Dv as numOf, Eh as PERFORMANCE_TARGET_SCHEMA, Ev as attrTrue, Fv as DocContext, Gv as langForPath, Ha as shadcnModules, Hv as splitPathLines, Jv as nonEmpty, Kv as isComponent, Lv as parseProps, Nm as PlanHeader, Oh as performanceDetails, Pv as takeUsedIcons, Th as PERFORMANCE_LABELS, Uv as SKIP_LANGS, Vv as parseLineRange, Xv as safeHref, Yv as own, Zg as calculate, Zv as urlScheme, __ as numberValue, b_ as records, bm as plotModel, dp as dependencyMatrixModel, e_ as REPORT_SPECS, fp as queryPlanModel, g_ as isDataRecord, gv as textList, h_ as display, hv as numList, iv as isStatus, j_ as parseDiff, kh as performanceResult, m_ as columnsOf, mp as flameModel, n_ as comparisonModel, nv as STATUSES, pp as diagramModel, qv as isRecord, r_ as schemaRows, t as builtinComponents, t_ as reportModel, up as bundleModel, v_ as parseJson, wh as CLAIM_LABELS, x_ as tableRecords, y_ as readRecords, ym as PLOT_NAMES, zv as fenceFilename } from "./ui-DkizsCxS.mjs";
3
- import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-BeI2zo52.mjs";
2
+ import { Bv as fenceFilename, Dv as attrTrue, Fv as takeUsedIcons, Ha as shadcnModules, Hv as parseLineRange, Iv as DocContext, Jf as dependencyMatrixModel, Jv as isRecord, Kv as langForPath, M_ as parseDiff, Mh as parseSyntaxLines, Ov as numOf, Qg as calculate, Qv as urlScheme, Rv as parseProps, S_ as tableRecords, Uv as splitPathLines, Vv as fenceLang, Wv as SKIP_LANGS, Xf as diagramModel, Xv as own, Yf as queryPlanModel, Yv as nonEmpty, Zf as flameModel, Zv as safeHref, __ as isDataRecord, _v as textList, av as isStatus, b_ as readRecords, ch as PERFORMANCE_LABELS, dh as performanceDetails, fh as performanceResult, g_ as display, gm as PlanHeader, gv as numList, h_ as columnsOf, i_ as schemaRows, lh as PERFORMANCE_TARGET_SCHEMA, n_ as reportModel, nm as PLOT_NAMES, qf as bundleModel, qv as isComponent, r_ as comparisonModel, rm as plotModel, rv as STATUSES, sh as CLAIM_LABELS, t as builtinComponents, t_ as REPORT_SPECS, uh as RESEARCH_CLAIM_SCHEMA, v_ as numberValue, x_ as records, y_ as parseJson } from "./ui-DuJK74P7.mjs";
3
+ import { a as loadUserModule, c as srcDir, i as importBundledCode, n as loadConfig, o as resolveModuleEntry, r as SHARED_PACKAGES, s as pkgRoot } from "./config-DssZ4uXC.mjs";
4
4
  import module from "node:module";
5
- import fsp, { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
5
+ import fsp, { access, mkdir, open, readFile, readdir, realpath, rename, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { cac } from "cac";
8
8
  import { directiveToMarkdown } from "mdast-util-directive";
@@ -20,7 +20,7 @@ import { unified } from "unified";
20
20
  import { VFile } from "vfile";
21
21
  import { matter } from "vfile-matter";
22
22
  import { visit } from "unist-util-visit";
23
- import fs, { existsSync, readFileSync, realpathSync } from "node:fs";
23
+ import fs, { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
24
24
  import { visitParents } from "unist-util-visit-parents";
25
25
  import * as valibot from "valibot";
26
26
  import { Fragment, createElement } from "react";
@@ -45,6 +45,9 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
45
45
  import { connect } from "node:net";
46
46
  import { WebSocket } from "ws";
47
47
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
48
+ //#region package.json
49
+ var version = "0.14.0";
50
+ //#endregion
48
51
  //#region src/format-error.ts
49
52
  /**
50
53
  * Validate the `--format` flag: omitted and "text" mean human-readable
@@ -5582,7 +5585,7 @@ const CONVENTIONS = [
5582
5585
  syntax: "```lang title=\"file.ts\""
5583
5586
  },
5584
5587
  {
5585
- result: "FileRef chip (editor link when the file exists)",
5588
+ result: "FileRef chip (local preview in serve; editor link in static HTML)",
5586
5589
  syntax: "`src/x.ts` inline code naming a real file"
5587
5590
  },
5588
5591
  {
@@ -6110,19 +6113,21 @@ const iconSvg = (name) => {
6110
6113
  * `handleDocEvent`); THEME_JS restores the stored choice before paint.
6111
6114
  */
6112
6115
  const THEME_TOGGLE_HTML = `<button type="button" class="mdxr-theme" data-mdxr-theme data-mode="auto" title="Theme: auto" aria-label="Switch theme (current: auto)"><span class="mdxr-theme-i mdxr-theme-i-auto">${iconSvg("sun-moon")}</span><span class="mdxr-theme-i mdxr-theme-i-light">${iconSvg("sun")}</span><span class="mdxr-theme-i mdxr-theme-i-dark">${iconSvg("moon")}</span></button>`;
6113
- const htmlDocument = (o) => `<!doctype html>
6116
+ const htmlDocument = (o) => {
6117
+ const annotations = o.documentControls === false ? void 0 : o.annotations;
6118
+ return `<!doctype html>
6114
6119
  <html lang="en"${o.initialTheme === void 0 ? "" : ` class="${o.initialTheme}" style="color-scheme: ${o.initialTheme}"`}>
6115
6120
  <head>
6116
6121
  <meta charset="utf-8">
6117
6122
  <meta name="viewport" content="width=device-width, initial-scale=1">
6118
6123
  <meta name="generator" content="mdxr">
6119
6124
  <title>${escapeHtml(o.title)}</title>
6120
- <script>${inlineScript(THEME_JS)}<\/script>
6125
+ ${o.documentControls === false ? "" : `<script>${inlineScript(THEME_JS)}<\/script>`}
6121
6126
  ${o.needsKatex === true ? `<link rel="stylesheet" href="${KATEX_CDN_URL}">` : ""}
6122
- <style>${inlineStyle(o.css)}${o.annotations === void 0 ? "" : ANNOTATION_HIGHLIGHT_CSS}</style>
6127
+ <style>${inlineStyle(o.css)}${annotations === void 0 ? "" : ANNOTATION_HIGHLIGHT_CSS}</style>
6123
6128
  </head>
6124
6129
  <body class="bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
6125
- ${THEME_TOGGLE_HTML}
6130
+ ${o.documentControls === false ? "" : `${THEME_TOGGLE_HTML}
6126
6131
  <div class="mdxr-view-controls" role="group" aria-label="Document view" hidden>
6127
6132
  <button type="button" data-mdxr-view="document" aria-pressed="true">${iconSvg("file-text")}<span>Document</span></button>
6128
6133
  <button type="button" data-mdxr-view="pages" aria-pressed="false">${iconSvg("panel-left")}<span>Pages</span></button>
@@ -6131,16 +6136,16 @@ ${THEME_TOGGLE_HTML}
6131
6136
  <p class="mdxr-pages-title">${escapeHtml(o.title)}</p>
6132
6137
  <p data-section-review-summary role="status" hidden></p>
6133
6138
  <div data-mdxr-page-tabs role="tablist" aria-label="Sections" aria-orientation="vertical"></div>
6134
- </aside>
6139
+ </aside>`}
6135
6140
  <div id="mdxr-content">
6136
6141
  <main id="mdxr-root" class="prose prose-neutral dark:prose-invert mx-auto max-w-3xl px-6 py-10">${o.body}</main>
6137
- <nav class="mdxr-page-navigation" aria-label="Section navigation" hidden>
6142
+ ${o.documentControls === false ? "" : `<nav class="mdxr-page-navigation" aria-label="Section navigation" hidden>
6138
6143
  <button type="button" data-mdxr-page-previous>${iconSvg("chevron-left")}<span class="mdxr-page-navigation-labels"><span>Previous</span><span data-mdxr-page-previous-title hidden></span></span></button>
6139
6144
  <button type="button" data-mdxr-page-next><span class="mdxr-page-navigation-labels"><span>Next</span><span data-mdxr-page-next-title hidden></span></span>${iconSvg("chevron-right")}</button>
6140
- </nav>
6145
+ </nav>`}
6141
6146
  </div>
6142
- ${o.annotations === void 0 ? "" : `${annotationHtml(iconSvg)}<script type="application/json" id="mdxr-annotation-document">${JSON.stringify(o.annotations).replaceAll("<", "\\u003c")}<\/script>`}
6143
- ${o.annotations === void 0 ? "" : sectionReviewHtml(iconSvg)}
6147
+ ${annotations === void 0 ? "" : `${annotationHtml(iconSvg)}<script type="application/json" id="mdxr-annotation-document">${JSON.stringify(annotations).replaceAll("<", "\\u003c")}<\/script>`}
6148
+ ${annotations === void 0 ? "" : sectionReviewHtml(iconSvg)}
6144
6149
  ${o.linkedDocuments === void 0 || Object.keys(o.linkedDocuments).length === 0 ? "" : `<script type="application/json" id="mdxr-linked-documents">${JSON.stringify(o.linkedDocuments).replaceAll("<", "\\u003c")}<\/script>`}
6145
6150
  <script>${inlineScript(o.clientJs)}<\/script>
6146
6151
  ${o.needsMermaid ? `<script type="module">${inlineScript(MERMAID_JS)}<\/script>` : ""}
@@ -6149,6 +6154,7 @@ ${o.hydrateJs === void 0 ? "" : `<script>${inlineScript(o.hydrateJs)}<\/script>`
6149
6154
  </body>
6150
6155
  </html>
6151
6156
  `;
6157
+ };
6152
6158
  //#endregion
6153
6159
  //#region src/hydrate/export-index.ts
6154
6160
  /**
@@ -6617,6 +6623,8 @@ ${mountDocument}({
6617
6623
  components: { ${entries.join(", ")} },
6618
6624
  doc: __mdxrDoc,
6619
6625
  fileLinks: ${JSON.stringify(spec.fileLinks)},
6626
+ filePreviews: ${JSON.stringify(spec.filePreviews)},
6627
+ filePreviewEnabled: ${JSON.stringify(spec.filePreviewEnabled)},
6620
6628
  headerProps: ${JSON.stringify(spec.header)},
6621
6629
  now: ${JSON.stringify(spec.now)},
6622
6630
  planHeader: ${planHeader},
@@ -6808,47 +6816,6 @@ const rehypeLinkedAssets = (opts = {}) => (tree, file) => {
6808
6816
  });
6809
6817
  };
6810
6818
  //#endregion
6811
- //#region src/workspace-syntax.ts
6812
- const ALLOWED_STYLE_PROPERTIES = [
6813
- "--shiki-light",
6814
- "--shiki-dark",
6815
- "--shiki-light-font-style",
6816
- "--shiki-dark-font-style",
6817
- "--shiki-light-font-weight",
6818
- "--shiki-dark-font-weight",
6819
- "--shiki-light-text-decoration",
6820
- "--shiki-dark-text-decoration"
6821
- ];
6822
- const parseToken = (value) => {
6823
- if (!isRecord(value) || typeof value.text !== "string" || !isRecord(value.style) || Array.isArray(value.style)) return;
6824
- const style = {};
6825
- for (const property of ALLOWED_STYLE_PROPERTIES) {
6826
- if (!Object.hasOwn(value.style, property)) continue;
6827
- const propertyValue = value.style[property];
6828
- if (typeof propertyValue === "string") style[property] = propertyValue;
6829
- }
6830
- return {
6831
- style,
6832
- text: value.text
6833
- };
6834
- };
6835
- /** Validate and narrow syntax tokens received through a workspace API. */
6836
- const parseSyntaxLines = (value) => {
6837
- if (!Array.isArray(value)) return;
6838
- const lines = [];
6839
- for (const lineValue of value) {
6840
- if (!Array.isArray(lineValue)) return;
6841
- const line = [];
6842
- for (const tokenValue of lineValue) {
6843
- const token = parseToken(tokenValue);
6844
- if (token === void 0) return;
6845
- line.push(token);
6846
- }
6847
- lines.push(line);
6848
- }
6849
- return lines;
6850
- };
6851
- //#endregion
6852
6819
  //#region src/rehype/shiki.ts
6853
6820
  /**
6854
6821
  * Fenced code blocks are syntax-highlighted with shiki at render time.
@@ -6993,16 +6960,17 @@ const plainSyntaxLines = (source) => source.split("\n").map((text) => [{
6993
6960
  style: {},
6994
6961
  text
6995
6962
  }]);
6996
- /** Tokenize a complete MDX document for the workspace source and diff views. */
6997
- const highlightMdxSource = async (source) => {
6963
+ const isBundledLanguage = (highlighter, lang) => Object.hasOwn(highlighter.getBundledLanguages(), lang);
6964
+ /** Tokenize source without interpreting Shiki's code-fence annotations. */
6965
+ const highlightSource = async (source, lang) => {
6998
6966
  const normalizedSource = source.replaceAll("\r\n", "\n");
6999
6967
  if (normalizedSource.includes("\r")) return plainSyntaxLines(normalizedSource);
7000
- const highlighter = await readyHighlighter("mdx");
7001
- if (highlighter === void 0) return plainSyntaxLines(normalizedSource);
6968
+ const highlighter = await readyHighlighter(lang);
6969
+ if (highlighter === void 0 || !isBundledLanguage(highlighter, lang)) return plainSyntaxLines(normalizedSource);
7002
6970
  try {
7003
6971
  const result = highlighter.codeToTokens(normalizedSource, {
7004
6972
  defaultColor: false,
7005
- lang: "mdx",
6973
+ lang,
7006
6974
  themes: THEMES
7007
6975
  });
7008
6976
  return parseSyntaxLines(result.tokens.map((line) => line.map((token) => ({
@@ -7013,6 +6981,8 @@ const highlightMdxSource = async (source) => {
7013
6981
  return plainSyntaxLines(normalizedSource);
7014
6982
  }
7015
6983
  };
6984
+ /** Tokenize a complete MDX document for the workspace source and diff views. */
6985
+ const highlightMdxSource = async (source) => await highlightSource(source, "mdx");
7016
6986
  /**
7017
6987
  * One side of a hunk, highlighted as the file's own language: `ctx`/`add`
7018
6988
  * rows read from the new side, `ctx`/`del` rows from the old side. Shiki's
@@ -7371,6 +7341,14 @@ const extractUsedComponents = (code, components) => {
7371
7341
  if (catalogKeys.has("wrapper")) used.add("wrapper");
7372
7342
  return [...used];
7373
7343
  };
7344
+ const createFilePreview = (dir, resolver, filePreviews) => {
7345
+ if (resolver === void 0) return;
7346
+ return (relPath) => {
7347
+ const url = resolver(path.resolve(dir, relPath));
7348
+ if (url !== void 0) filePreviews.set(relPath, url);
7349
+ return url;
7350
+ };
7351
+ };
7374
7352
  const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {}) => {
7375
7353
  const file = new VFile({
7376
7354
  path: filePath,
@@ -7382,12 +7360,14 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
7382
7360
  const editor = typeof frontmatter.editor === "string" && frontmatter.editor !== "" ? frontmatter.editor : opts.editor;
7383
7361
  const dir = file.dirname ?? ".";
7384
7362
  const fileLinks = /* @__PURE__ */ new Map();
7363
+ const filePreviews = /* @__PURE__ */ new Map();
7385
7364
  const fileLink = (rel, line) => {
7386
7365
  const abs = path.resolve(dir, rel);
7387
7366
  const url = existsSync(abs) ? editorUrl(editor, abs, line) : void 0;
7388
7367
  if (url !== void 0) fileLinks.set(`${rel}\0${line ?? ""}`, url);
7389
7368
  return url;
7390
7369
  };
7370
+ const filePreview = createFilePreview(dir, opts.filePreview, filePreviews);
7391
7371
  const compiled = await compile(file, {
7392
7372
  baseUrl: import.meta.url,
7393
7373
  format: "mdx",
@@ -7426,6 +7406,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
7426
7406
  const renderedAt = /* @__PURE__ */ new Date();
7427
7407
  const context = {
7428
7408
  fileLink,
7409
+ filePreview,
7429
7410
  now: renderedAt
7430
7411
  };
7431
7412
  const hydrated = (opts.hydrate ?? true) && used.length > 0;
@@ -7444,6 +7425,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
7444
7425
  context,
7445
7426
  dependencies: [...new Set(includeDependencies)],
7446
7427
  fileLinks: Object.fromEntries(fileLinks),
7428
+ filePreviews: Object.fromEntries(filePreviews),
7447
7429
  frontmatter,
7448
7430
  hydrated,
7449
7431
  linkedDocuments,
@@ -7452,6 +7434,7 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
7452
7434
  const html = renderDocument(header);
7453
7435
  return {
7454
7436
  fileLinks: Object.fromEntries(fileLinks),
7437
+ filePreviews: Object.fromEntries(filePreviews),
7455
7438
  html,
7456
7439
  usedIcons: takeUsedIcons()
7457
7440
  };
@@ -8082,6 +8065,8 @@ const buildHydrateBundle = async (args) => {
8082
8065
  code: args.code,
8083
8066
  componentsPath: args.config.componentsPath === void 0 ? void 0 : resolveModuleEntry(args.config.componentsPath),
8084
8067
  fileLinks: args.fileLinks,
8068
+ filePreviewEnabled: args.filePreviewEnabled,
8069
+ filePreviews: args.filePreviews,
8085
8070
  header: args.headerProps,
8086
8071
  now: args.now,
8087
8072
  usedComponents: args.usedComponents,
@@ -8131,8 +8116,9 @@ const renderDocument = async (source, opts, include = {}) => {
8131
8116
  ...builtinComponents,
8132
8117
  ...user.components
8133
8118
  };
8134
- const { annotationSources, body, code, dependencies: includeDependencies, fileLinks, frontmatter, linkedDocuments, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
8119
+ const { annotationSources, body, code, dependencies: includeDependencies, fileLinks, filePreviews, frontmatter, linkedDocuments, renderedAt, renderWithHeader, usedComponents, usedIcons } = await mdxToHtml(source, components, filePath, {
8135
8120
  editor: config.editor,
8121
+ filePreview: opts.filePreview,
8136
8122
  hydrate: opts.hydrate,
8137
8123
  include
8138
8124
  });
@@ -8148,11 +8134,13 @@ const renderDocument = async (source, opts, include = {}) => {
8148
8134
  const headerProps = frontmatterHeader(fmStr, body);
8149
8135
  let docBody = body;
8150
8136
  let docFileLinks = fileLinks;
8137
+ let docFilePreviews = filePreviews;
8151
8138
  let docIcons = usedIcons;
8152
8139
  if (headerProps !== void 0) {
8153
8140
  const pass = renderWithHeader(createElement(PlanHeader, headerProps));
8154
8141
  docBody = pass.html;
8155
8142
  docFileLinks = pass.fileLinks;
8143
+ docFilePreviews = pass.filePreviews;
8156
8144
  docIcons = pass.usedIcons;
8157
8145
  }
8158
8146
  const themeCss = config.themePath === void 0 ? void 0 : await readFile(config.themePath, "utf-8");
@@ -8185,6 +8173,8 @@ const renderDocument = async (source, opts, include = {}) => {
8185
8173
  code,
8186
8174
  config,
8187
8175
  fileLinks: docFileLinks,
8176
+ filePreviewEnabled: opts.filePreview !== void 0,
8177
+ filePreviews: docFilePreviews,
8188
8178
  headerProps,
8189
8179
  hydrate: opts.hydrate,
8190
8180
  now: renderedAt,
@@ -8225,6 +8215,7 @@ const renderDocument = async (source, opts, include = {}) => {
8225
8215
  body: docBody,
8226
8216
  clientJs: js,
8227
8217
  css,
8218
+ documentControls: opts.documentControls,
8228
8219
  hydrateJs,
8229
8220
  initialTheme: opts.initialTheme,
8230
8221
  linkedDocuments: documents,
@@ -8235,7 +8226,7 @@ const renderDocument = async (source, opts, include = {}) => {
8235
8226
  });
8236
8227
  };
8237
8228
  /** Render MDX source text to a standalone HTML document. */
8238
- const render = async (source, opts = {}) => await renderDocument(source, opts);
8229
+ const render = async (source, opts = {}) => await renderDocument(source, opts, { inlineAssets: opts.inlineAssets });
8239
8230
  /** Read `mdxPath` and render it to a standalone HTML document. */
8240
8231
  const renderFile = async (mdxPath, opts = {}) => {
8241
8232
  const abs = path.resolve(mdxPath);
@@ -8322,7 +8313,7 @@ const reply = (res, status, body, headers = {}) => {
8322
8313
  });
8323
8314
  res.end(JSON.stringify(body));
8324
8315
  };
8325
- const sameOrigin$2 = (req) => {
8316
+ const sameOrigin$3 = (req) => {
8326
8317
  const port = req.socket.localPort;
8327
8318
  const host = req.headers.host ?? "";
8328
8319
  return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (req.headers.origin === void 0 || req.headers.origin === `http://${host}`);
@@ -8358,7 +8349,7 @@ const handleAgentRequest = async (req, res, agent, notify, beforeSend) => {
8358
8349
  reply(res, 404, { error: "Agent chat is disabled" });
8359
8350
  return;
8360
8351
  }
8361
- if (!sameOrigin$2(req)) {
8352
+ if (!sameOrigin$3(req)) {
8362
8353
  reply(res, 403, { error: "Request origin is not allowed" });
8363
8354
  return;
8364
8355
  }
@@ -9567,7 +9558,7 @@ const parsePreviewTheme = (value) => {
9567
9558
  if (value === null) return;
9568
9559
  return value === "light" || value === "dark" ? value : null;
9569
9560
  };
9570
- const replyJson$1 = (response, status, body, headers = {}) => {
9561
+ const replyJson$2 = (response, status, body, headers = {}) => {
9571
9562
  response.writeHead(status, {
9572
9563
  "cache-control": "no-store",
9573
9564
  "content-type": "application/json; charset=utf-8",
@@ -9576,7 +9567,7 @@ const replyJson$1 = (response, status, body, headers = {}) => {
9576
9567
  });
9577
9568
  response.end(JSON.stringify(body));
9578
9569
  };
9579
- const sameOrigin$1 = (request) => {
9570
+ const sameOrigin$2 = (request) => {
9580
9571
  const port = request.socket.localPort;
9581
9572
  const host = request.headers.host ?? "";
9582
9573
  return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`);
@@ -9591,7 +9582,7 @@ const replyDiff = async (response, history, from, to) => {
9591
9582
  if (line.type !== "remove") afterSource.push(line.text);
9592
9583
  }
9593
9584
  const [before, after] = await Promise.all([highlightMdxSource(beforeSource.join("\n")), highlightMdxSource(afterSource.join("\n"))]);
9594
- replyJson$1(response, 200, {
9585
+ replyJson$2(response, 200, {
9595
9586
  from,
9596
9587
  lines,
9597
9588
  syntax: {
@@ -9604,7 +9595,7 @@ const replyDiff = async (response, history, from, to) => {
9604
9595
  const replyPreview = async (response, history, filePath, id, requestedTheme) => {
9605
9596
  const theme = parsePreviewTheme(requestedTheme);
9606
9597
  if (theme === null) {
9607
- replyJson$1(response, 400, { error: "Invalid preview theme" });
9598
+ replyJson$2(response, 400, { error: "Invalid preview theme" });
9608
9599
  return;
9609
9600
  }
9610
9601
  const source = await history.read(id);
@@ -9624,14 +9615,14 @@ const replyPreview = async (response, history, filePath, id, requestedTheme) =>
9624
9615
  const replyHistoryView = async (response, history, filePath, params) => {
9625
9616
  const view = params.get("view");
9626
9617
  if (view === null) {
9627
- replyJson$1(response, 200, await history.list());
9618
+ replyJson$2(response, 200, await history.list());
9628
9619
  return;
9629
9620
  }
9630
9621
  const id = params.get("id");
9631
9622
  if (view === "source" && id !== null) {
9632
9623
  const source = await history.read(id);
9633
9624
  const syntax = await highlightMdxSource(source);
9634
- replyJson$1(response, 200, {
9625
+ replyJson$2(response, 200, {
9635
9626
  id,
9636
9627
  source,
9637
9628
  syntax
@@ -9650,20 +9641,20 @@ const replyHistoryView = async (response, history, filePath, params) => {
9650
9641
  await replyPreview(response, history, filePath, id, params.get("theme"));
9651
9642
  return;
9652
9643
  }
9653
- replyJson$1(response, 400, { error: "Invalid history view or version" });
9644
+ replyJson$2(response, 400, { error: "Invalid history view or version" });
9654
9645
  };
9655
9646
  /** Read-only endpoints for the local, token-free MDX version store. */
9656
9647
  const handleDocumentHistoryRequest = async (request, response, history, filePath) => {
9657
9648
  if (history === void 0 || filePath === void 0) {
9658
- replyJson$1(response, 404, { error: "Document history is unavailable" });
9649
+ replyJson$2(response, 404, { error: "Document history is unavailable" });
9659
9650
  return;
9660
9651
  }
9661
- if (!sameOrigin$1(request)) {
9662
- replyJson$1(response, 403, { error: "Request origin is not allowed" });
9652
+ if (!sameOrigin$2(request)) {
9653
+ replyJson$2(response, 403, { error: "Request origin is not allowed" });
9663
9654
  return;
9664
9655
  }
9665
9656
  if (request.method !== "GET") {
9666
- replyJson$1(response, 405, { error: "Method not allowed" }, { allow: "GET" });
9657
+ replyJson$2(response, 405, { error: "Method not allowed" }, { allow: "GET" });
9667
9658
  return;
9668
9659
  }
9669
9660
  const url = new URL(request.url ?? "", "http://localhost");
@@ -9672,10 +9663,157 @@ const handleDocumentHistoryRequest = async (request, response, history, filePath
9672
9663
  } catch (error) {
9673
9664
  const detail = formatError(error);
9674
9665
  const missingVersion = error instanceof DocumentHistoryError && (error.kind === "invalid-version" || error.kind === "unknown-version");
9675
- replyJson$1(response, missingVersion ? 404 : 500, { error: detail });
9666
+ replyJson$2(response, missingVersion ? 404 : 500, { error: detail });
9676
9667
  }
9677
9668
  };
9678
9669
  //#endregion
9670
+ //#region src/file-preview-http.ts
9671
+ const MAX_TEXT_BYTES = 2097152;
9672
+ const MAX_IMAGE_BYTES = 33554432;
9673
+ const MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([
9674
+ ".md",
9675
+ ".markdown",
9676
+ ".mdx",
9677
+ ".mdown"
9678
+ ]);
9679
+ var FilePreviewError = class extends Error {
9680
+ status;
9681
+ constructor(status, message) {
9682
+ super(message);
9683
+ this.name = "FilePreviewError";
9684
+ this.status = status;
9685
+ }
9686
+ };
9687
+ const replyJson$1 = (response, status, body) => {
9688
+ response.writeHead(status, {
9689
+ "cache-control": "no-store",
9690
+ "content-type": "application/json; charset=utf-8",
9691
+ "x-content-type-options": "nosniff",
9692
+ ...status === 405 ? { allow: "GET" } : {}
9693
+ });
9694
+ response.end(JSON.stringify(body));
9695
+ };
9696
+ const sameOrigin$1 = (request) => {
9697
+ const port = request.socket.localPort;
9698
+ const host = request.headers.host ?? "";
9699
+ return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`) && request.headers["sec-fetch-site"] !== "cross-site";
9700
+ };
9701
+ const readPreviewFile = async (file, limit) => {
9702
+ if (await realpath(file.path) !== file.canonical) throw new FilePreviewError(403, "The referenced file has changed location");
9703
+ const handle = await open(file.canonical, "r");
9704
+ try {
9705
+ const stat = await handle.stat();
9706
+ if (!stat.isFile()) throw new FilePreviewError(415, "Only regular files can be previewed");
9707
+ if (stat.size > limit) throw new FilePreviewError(413, "This file is too large to preview. Open it in your editor.");
9708
+ return await handle.readFile();
9709
+ } finally {
9710
+ await handle.close();
9711
+ }
9712
+ };
9713
+ const decodeText = (contents) => {
9714
+ try {
9715
+ const source = new TextDecoder("utf-8", { fatal: true }).decode(contents);
9716
+ if (source.includes("\0")) throw new Error("Binary content");
9717
+ return source;
9718
+ } catch {
9719
+ throw new FilePreviewError(415, "This file is not UTF-8 text. Open it in your editor.");
9720
+ }
9721
+ };
9722
+ const previewData = async (file, theme) => {
9723
+ const extension = path.extname(file.path).toLowerCase();
9724
+ const mime = IMAGE_MIME_TYPES[extension];
9725
+ const contents = await readPreviewFile(file, mime === void 0 ? MAX_TEXT_BYTES : MAX_IMAGE_BYTES);
9726
+ if (mime !== void 0) return {
9727
+ kind: "image",
9728
+ src: `data:${mime};base64,${contents.toString("base64")}`
9729
+ };
9730
+ const source = decodeText(contents);
9731
+ const markdown = MARKDOWN_EXTENSIONS.has(extension);
9732
+ const lang = extension === ".mdx" ? "mdx" : langForPath(file.path);
9733
+ const syntax = await highlightSource(source, lang ?? (markdown ? "markdown" : "text"));
9734
+ if (!markdown) return {
9735
+ kind: "text",
9736
+ source,
9737
+ syntax
9738
+ };
9739
+ try {
9740
+ return {
9741
+ html: await render(source, {
9742
+ dir: path.dirname(file.path),
9743
+ documentControls: false,
9744
+ filePath: file.path,
9745
+ initialTheme: theme,
9746
+ inlineAssets: true
9747
+ }),
9748
+ kind: "markdown",
9749
+ source,
9750
+ syntax
9751
+ };
9752
+ } catch (error) {
9753
+ return {
9754
+ kind: "markdown",
9755
+ renderError: formatError(error),
9756
+ source,
9757
+ syntax
9758
+ };
9759
+ }
9760
+ };
9761
+ const replyError = (response, error) => {
9762
+ if (isRecord(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
9763
+ replyJson$1(response, 404, { error: "The referenced file no longer exists" });
9764
+ return;
9765
+ }
9766
+ const status = error instanceof FilePreviewError ? error.status : 500;
9767
+ replyJson$1(response, status, { error: formatError(error) });
9768
+ };
9769
+ /** Per-server capabilities, issued only for files a rendered FileRef uses. */
9770
+ const createFilePreviews = () => {
9771
+ const paths = /* @__PURE__ */ new Map();
9772
+ const files = /* @__PURE__ */ new Map();
9773
+ return {
9774
+ async handle(request, response) {
9775
+ if (!sameOrigin$1(request)) {
9776
+ replyJson$1(response, 403, { error: "Request origin is not allowed" });
9777
+ return;
9778
+ }
9779
+ if (request.method !== "GET") {
9780
+ replyJson$1(response, 405, { error: "Method not allowed" });
9781
+ return;
9782
+ }
9783
+ const params = new URL(request.url ?? "", "http://localhost").searchParams;
9784
+ const file = files.get(params.get("id") ?? "");
9785
+ if (file === void 0) {
9786
+ replyJson$1(response, 404, { error: "The referenced file is unavailable" });
9787
+ return;
9788
+ }
9789
+ try {
9790
+ replyJson$1(response, 200, await previewData(file, params.get("theme") === "dark" ? "dark" : "light"));
9791
+ } catch (error) {
9792
+ replyError(response, error);
9793
+ }
9794
+ },
9795
+ register: (filePath) => {
9796
+ try {
9797
+ const canonical = realpathSync(filePath);
9798
+ if (!statSync(canonical).isFile()) return;
9799
+ let file = paths.get(filePath);
9800
+ if (file === void 0 || file.canonical !== canonical) {
9801
+ file = {
9802
+ canonical,
9803
+ id: randomUUID(),
9804
+ path: filePath
9805
+ };
9806
+ paths.set(filePath, file);
9807
+ files.set(file.id, file);
9808
+ }
9809
+ return `/__mdxr_file?id=${file.id}`;
9810
+ } catch {
9811
+ return;
9812
+ }
9813
+ }
9814
+ };
9815
+ };
9816
+ //#endregion
9679
9817
  //#region src/workspace-export-data.ts
9680
9818
  /** Collect a complete, internally consistent archive of one served document. */
9681
9819
  const createWorkspaceExportData = async (filePath, history, agent) => {
@@ -9894,6 +10032,10 @@ const servePreview = async (target, port) => {
9894
10032
  };
9895
10033
  target.agent?.setOnUpdate(notifyAgent);
9896
10034
  const server = http.createServer((req, res) => {
10035
+ if (req.url?.split("?")[0] === "/__mdxr_file") {
10036
+ target.filePreviews.handle(req, res);
10037
+ return;
10038
+ }
9897
10039
  if (req.url === "/__mdxr_workspace.js") {
9898
10040
  if (target.history === void 0) {
9899
10041
  res.writeHead(404);
@@ -10051,13 +10193,16 @@ const serve = async (mdxPath, port, opts = {}) => {
10051
10193
  if (opts.server !== void 0 && opts.agent !== "codex") throw new Error("--server requires --agent codex");
10052
10194
  const abs = path.resolve(mdxPath);
10053
10195
  const history = createDocumentHistory(abs);
10196
+ const filePreviews = createFilePreviews();
10054
10197
  const server = await servePreview({
10055
10198
  agent: opts.agent === void 0 ? void 0 : createAgentSession(abs, opts.agent, opts.session, opts.server),
10056
10199
  agentProvider: opts.agent,
10200
+ filePreviews,
10057
10201
  history,
10058
10202
  historyFile: abs,
10059
10203
  label: mdxPath,
10060
10204
  ...trackDeps(async (onDeps) => await renderFile(abs, {
10205
+ filePreview: filePreviews.register,
10061
10206
  liveReload: true,
10062
10207
  onDependencies: onDeps
10063
10208
  })),
@@ -10071,13 +10216,18 @@ const serve = async (mdxPath, port, opts = {}) => {
10071
10216
  const serveSource = async (source, port, opts = {}) => {
10072
10217
  if (opts.agent !== void 0 || opts.session !== void 0 || opts.server !== void 0) throw new Error("Agent chat requires an MDX file");
10073
10218
  const dir = path.resolve(opts.dir ?? process.cwd());
10219
+ const filePreviews = createFilePreviews();
10074
10220
  const server = await servePreview({
10221
+ filePreviews,
10075
10222
  label: opts.filePath ?? "stdin",
10076
10223
  ...trackDeps(async (onDeps) => await render(source, {
10077
10224
  dir,
10225
+ documentControls: opts.documentControls,
10078
10226
  filePath: opts.filePath,
10227
+ filePreview: filePreviews.register,
10079
10228
  hydrate: opts.hydrate,
10080
10229
  initialTheme: opts.initialTheme,
10230
+ inlineAssets: opts.inlineAssets,
10081
10231
  liveReload: true,
10082
10232
  onDependencies: onDeps
10083
10233
  })),
@@ -10124,7 +10274,7 @@ const validateAgentOptions = (agent, session, server) => {
10124
10274
  if (session !== void 0 && session.trim() === "") throw new Error("--session requires a non-empty ID");
10125
10275
  return agent;
10126
10276
  };
10127
- const cli = cac("mdxr");
10277
+ const cli = cac("mdxr").version(version);
10128
10278
  cli.command("render [file]", "Render an .mdx document to a standalone HTML file").option("-o, --out <path>", "Output path (default: <file>.html; stdout for stdin input or '-')").option("--format <format>", "Error output: text | json").option("--no-hydrate", "Emit static HTML without the client hydration bundle").option("--open", "Open the rendered file in the default browser").action(async (file, opts) => {
10129
10279
  const json = opts.format === "json";
10130
10280
  try {