@suzumiyaaoba/mdxr 0.7.0 → 0.8.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/dist/cli.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $g as REPORT_SPECS, A_ as parseDiff, Bv as parseLineRange, Dh as RESEARCH_CLAIM_SCHEMA, Eh as PERFORMANCE_TARGET_SCHEMA, Ev as numOf, Gv as isComponent, Ha as shadcnModules, Hv as SKIP_LANGS, Iv as parseProps, Jv as own, Kv as isRecord, Nm as PlanHeader, Nv as takeUsedIcons, Oh as performanceDetails, Pv as DocContext, Rv as fenceFilename, Th as PERFORMANCE_LABELS, Tv as attrTrue, Vv as splitPathLines, Wv as langForPath, Xg as calculate, Xv as urlScheme, Yv as safeHref, __ as parseJson, b_ as tableRecords, bm as plotModel, dp as dependencyMatrixModel, e_ as reportModel, fp as queryPlanModel, g_ as numberValue, h_ as isDataRecord, hv as textList, kh as performanceResult, m_ as display, mp as flameModel, mv as numList, n_ as schemaRows, p_ as columnsOf, pp as diagramModel, qv as nonEmpty, rv as isStatus, t as builtinComponents, t_ as comparisonModel, tv as STATUSES, up as bundleModel, v_ as readRecords, wh as CLAIM_LABELS, y_ as records, ym as PLOT_NAMES, zv as fenceLang } from "./ui-BhvyxJNU.mjs";
2
+ import { $g as REPORT_SPECS, A_ as parseDiff, Bv as parseLineRange, Dh as RESEARCH_CLAIM_SCHEMA, Eh as PERFORMANCE_TARGET_SCHEMA, Ev as numOf, Gv as isComponent, Ha as shadcnModules, Hv as SKIP_LANGS, Iv as parseProps, Jv as own, Kv as isRecord$1, Nm as PlanHeader, Nv as takeUsedIcons, Oh as performanceDetails, Pv as DocContext, Rv as fenceFilename, Th as PERFORMANCE_LABELS, Tv as attrTrue, Vv as splitPathLines, Wv as langForPath, Xg as calculate, Xv as urlScheme, Yv as safeHref, __ as parseJson, b_ as tableRecords, bm as plotModel, dp as dependencyMatrixModel, e_ as reportModel, fp as queryPlanModel, g_ as numberValue, h_ as isDataRecord, hv as textList, kh as performanceResult, m_ as display, mp as flameModel, mv as numList, n_ as schemaRows, p_ as columnsOf, pp as diagramModel, qv as nonEmpty, rv as isStatus, t as builtinComponents, t_ as comparisonModel, tv as STATUSES, up as bundleModel, v_ as readRecords, wh as CLAIM_LABELS, y_ as records, ym as PLOT_NAMES, zv as fenceLang } from "./ui-BhvyxJNU.mjs";
3
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-HGfE6JlG.mjs";
4
4
  import module from "node:module";
5
- import fsp, { access, readFile, readdir, writeFile } from "node:fs/promises";
5
+ import fsp, { access, mkdir, readFile, readdir, 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";
@@ -27,9 +27,9 @@ import { Fragment, createElement } from "react";
27
27
  import { icons } from "@iconify-json/lucide";
28
28
  import { icons as icons$1 } from "@iconify-json/vscode-icons";
29
29
  import { differenceInCalendarDays, isValid, parseISO } from "date-fns";
30
- import { createHash } from "node:crypto";
30
+ import { createHash, randomUUID } from "node:crypto";
31
31
  import { build } from "esbuild";
32
- import os from "node:os";
32
+ import os, { homedir } from "node:os";
33
33
  import { spawn } from "node:child_process";
34
34
  import { once } from "node:events";
35
35
  import { compile } from "@mdx-js/mdx";
@@ -40,6 +40,10 @@ import { createHighlighter } from "shiki";
40
40
  import { compile as compile$1, optimize } from "@tailwindcss/node";
41
41
  import { Scanner } from "@tailwindcss/oxide";
42
42
  import http from "node:http";
43
+ import { query } from "@anthropic-ai/claude-agent-sdk";
44
+ import { connect } from "node:net";
45
+ import { WebSocket } from "ws";
46
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
43
47
  //#region src/format-error.ts
44
48
  /**
45
49
  * Validate the `--format` flag: omitted and "text" mean human-readable
@@ -98,7 +102,7 @@ const textContent$1 = (node) => {
98
102
  /** A JSX attribute's string value, or undefined when absent/empty/non-string. */
99
103
  const jsxAttr = (node, name) => {
100
104
  if (!Array.isArray(node.attributes)) return;
101
- for (const a of node.attributes) if (isRecord(a) && a.name === name && typeof a.value === "string" && a.value !== "") return a.value;
105
+ for (const a of node.attributes) if (isRecord$1(a) && a.name === name && typeof a.value === "string" && a.value !== "") return a.value;
102
106
  };
103
107
  /**
104
108
  * `Record` → `mdxJsxAttribute[]`. null/undefined/"" values are dropped —
@@ -120,10 +124,10 @@ const toMdxElement = (node, kind, name, attrs) => {
120
124
  * The mdast→hast bridge turns hProperties into element props.
121
125
  */
122
126
  const setHProperty = (node, key, value) => {
123
- const data = isRecord(node.data) ? node.data : {};
127
+ const data = isRecord$1(node.data) ? node.data : {};
124
128
  node.data = data;
125
129
  data.hProperties = {
126
- ...isRecord(data.hProperties) ? data.hProperties : {},
130
+ ...isRecord$1(data.hProperties) ? data.hProperties : {},
127
131
  [key]: value
128
132
  };
129
133
  };
@@ -1033,7 +1037,7 @@ const CONTAINER_COMPONENTS = {
1033
1037
  vuln: "Vuln",
1034
1038
  waterfall: "Waterfall"
1035
1039
  };
1036
- const isDirective = (n) => "name" in n && typeof n.name === "string" && "attributes" in n && isRecord(n.attributes) && isParent(n);
1040
+ const isDirective = (n) => "name" in n && typeof n.name === "string" && "attributes" in n && isRecord$1(n.attributes) && isParent(n);
1037
1041
  /**
1038
1042
  * remark-directive containers become mdxr components:
1039
1043
  * :::note[Optional label] → <Callout kind="note" title="Optional label">
@@ -1083,7 +1087,7 @@ const remarkMdxrDirectives = () => (tree, file) => {
1083
1087
  }
1084
1088
  const attrs = { ...directive.attributes };
1085
1089
  const [first] = directive.children;
1086
- if (first !== void 0 && isRecord(first.data) && first.data.directiveLabel === true) {
1090
+ if (first !== void 0 && isRecord$1(first.data) && first.data.directiveLabel === true) {
1087
1091
  attrs.title ??= textContent$1(first).trim();
1088
1092
  directive.children.shift();
1089
1093
  }
@@ -1236,7 +1240,7 @@ const remarkNoJs = () => (tree, file) => {
1236
1240
  });
1237
1241
  visit(tree, JSX_ELEMENTS, (node) => {
1238
1242
  if (!("attributes" in node) || !Array.isArray(node.attributes)) return;
1239
- for (const attr of node.attributes) if (isRecord(attr) && (attr.type !== "mdxJsxAttribute" || attr.value !== null && typeof attr.value !== "string")) file.fail(MESSAGE, node, "mdxr:no-js");
1243
+ for (const attr of node.attributes) if (isRecord$1(attr) && (attr.type !== "mdxJsxAttribute" || attr.value !== null && typeof attr.value !== "string")) file.fail(MESSAGE, node, "mdxr:no-js");
1240
1244
  });
1241
1245
  };
1242
1246
  //#endregion
@@ -1262,7 +1266,7 @@ const rebase = (node, origin, root) => {
1262
1266
  const relative = (value) => REMOTE.test(value) ? value : path.relative(root, path.resolve(origin, value)).split(path.sep).join("/");
1263
1267
  if ("url" in node && typeof node.url === "string") node.url = relative(node.url);
1264
1268
  if (!("attributes" in node) || !Array.isArray(node.attributes)) return;
1265
- for (const attr of node.attributes) if (isRecord(attr) && [
1269
+ for (const attr of node.attributes) if (isRecord$1(attr) && [
1266
1270
  "path",
1267
1271
  "src",
1268
1272
  "poster",
@@ -1319,7 +1323,7 @@ const remarkInclude = () => (tree, file) => {
1319
1323
  //#region src/remark/references.ts
1320
1324
  const isElement$1 = (node) => node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement";
1321
1325
  const set = (node, name, value) => {
1322
- node.attributes = [...Array.isArray(node.attributes) ? node.attributes.filter((attr) => !(isRecord(attr) && attr.name === name)) : [], {
1326
+ node.attributes = [...Array.isArray(node.attributes) ? node.attributes.filter((attr) => !(isRecord$1(attr) && attr.name === name)) : [], {
1323
1327
  name,
1324
1328
  type: "mdxJsxAttribute",
1325
1329
  value
@@ -1511,7 +1515,7 @@ const attr = (node, name) => jsxAttr(node, name);
1511
1515
  */
1512
1516
  const flag = (node, name) => {
1513
1517
  if (!Array.isArray(node.attributes)) return false;
1514
- for (const a of node.attributes) if (isRecord(a) && a.name === name) return a.value === null || a.value === true || attrTrue(a.value);
1518
+ for (const a of node.attributes) if (isRecord$1(a) && a.name === name) return a.value === null || a.value === true || attrTrue(a.value);
1515
1519
  return false;
1516
1520
  };
1517
1521
  /** Attribute explicitly set to the string `"false"` (open="false" opt-outs). */
@@ -1794,7 +1798,7 @@ const transformAscii = (root, registry, serialize, warn) => {
1794
1798
  return [n];
1795
1799
  };
1796
1800
  const transformNodes = (nodes, ctx) => nodes.flatMap((node) => {
1797
- const id = isRecord(node.data) ? node.data.mdxrReferenceId : void 0;
1801
+ const id = isRecord$1(node.data) ? node.data.mdxrReferenceId : void 0;
1798
1802
  const output = transformElement(node, ctx);
1799
1803
  if (typeof id !== "string") return output;
1800
1804
  return [{
@@ -5220,8 +5224,8 @@ const mdxToAscii = async (source, filePath = "document.mdx") => {
5220
5224
  value: source
5221
5225
  });
5222
5226
  matter(file);
5223
- const fmRaw = isRecord(file.data) ? file.data.matter : void 0;
5224
- const fm = isRecord(fmRaw) ? fmRaw : {};
5227
+ const fmRaw = isRecord$1(file.data) ? file.data.matter : void 0;
5228
+ const fm = isRecord$1(fmRaw) ? fmRaw : {};
5225
5229
  const tree = processor.parse(file);
5226
5230
  await processor.run(tree, file);
5227
5231
  const hasPlan = contains(tree.children, "Plan");
@@ -5258,11 +5262,11 @@ const describeLiteral = (value) => {
5258
5262
  };
5259
5263
  const pipeConstraint = (schema) => {
5260
5264
  if (!Array.isArray(schema.pipe)) return;
5261
- return schema.pipe.find((s) => isRecord(s) && (s.type === "picklist" || s.type === "union" || s.type === "literal"));
5265
+ return schema.pipe.find((s) => isRecord$1(s) && (s.type === "picklist" || s.type === "union" || s.type === "literal"));
5262
5266
  };
5263
5267
  /** Structural view of a valibot schema — enough for catalog introspection. */
5264
5268
  const describeSchema = (schema) => {
5265
- if (!isRecord(schema)) return "unknown";
5269
+ if (!isRecord$1(schema)) return "unknown";
5266
5270
  switch (schema.type) {
5267
5271
  case "nullable":
5268
5272
  case "nullish": return `${describeSchema(schema.wrapped)} | null`;
@@ -5284,10 +5288,10 @@ const describeSchema = (schema) => {
5284
5288
  return typeof schema.type === "string" ? schema.type : "unknown";
5285
5289
  };
5286
5290
  const propsOf = (schema) => {
5287
- if (!isRecord(schema) || !isRecord(schema.entries)) return {};
5291
+ if (!isRecord$1(schema) || !isRecord$1(schema.entries)) return {};
5288
5292
  const out = {};
5289
5293
  for (const [key, raw] of Object.entries(schema.entries)) {
5290
- const optional = isRecord(raw) && typeof raw.type === "string" && OPTIONAL_TYPES.has(raw.type);
5294
+ const optional = isRecord$1(raw) && typeof raw.type === "string" && OPTIONAL_TYPES.has(raw.type);
5291
5295
  const def = optional ? raw.default : void 0;
5292
5296
  Object.defineProperty(out, key, {
5293
5297
  configurable: true,
@@ -5663,7 +5667,7 @@ const put = (into, k, v) => {
5663
5667
  };
5664
5668
  /** A default-exported object is treated as a `{ Name: Component }` map. */
5665
5669
  const mergeDefaultMap = (val, into) => {
5666
- if (!isRecord(val)) return;
5670
+ if (!isRecord$1(val)) return;
5667
5671
  for (const [k, v] of Object.entries(val)) if (isComponent(v)) put(into, k, v);
5668
5672
  };
5669
5673
  /**
@@ -5718,9 +5722,16 @@ const annotationHtml = (icon) => `
5718
5722
  </form>
5719
5723
  <p class="mdxr-annotation-empty" data-annotation-empty>No comments</p>
5720
5724
  <ol class="mdxr-annotation-list" data-annotation-list></ol>
5725
+ <details class="mdxr-annotation-history" data-annotation-history hidden>
5726
+ <summary>History <span data-annotation-history-count>0</span></summary>
5727
+ <div class="mdxr-annotation-history-list" data-annotation-history-list></div>
5728
+ </details>
5721
5729
  </div>
5722
5730
  <footer class="mdxr-annotation-footer">
5723
- <button type="button" data-annotation-copy aria-label="Copy Markdown" title="Copy Markdown" disabled><span class="mdxr-annotation-copy-idle">${icon("copy")}</span><span class="mdxr-annotation-copy-done">${icon("check")}</span><span data-annotation-copy-label>Markdown</span></button>
5731
+ <div class="mdxr-annotation-footer-actions">
5732
+ <button type="button" data-annotation-copy aria-label="Copy Markdown" title="Copy Markdown" disabled><span class="mdxr-annotation-copy-idle">${icon("copy")}</span><span class="mdxr-annotation-copy-done">${icon("check")}</span><span data-annotation-copy-label>Markdown</span></button>
5733
+ <button type="button" data-annotation-send aria-label="Send to chat" title="Send to chat" disabled hidden>${icon("send")}<span>Send to chat</span></button>
5734
+ </div>
5724
5735
  <div class="mdxr-annotation-save-status" data-annotation-status-row data-state="saved"><p role="status" aria-live="polite" data-annotation-status></p></div>
5725
5736
  <div data-annotation-export hidden>
5726
5737
  <label class="mdxr-annotation-sr-only" for="mdxr-annotation-markdown">Markdown feedback</label>
@@ -5859,14 +5870,14 @@ const inlineScript = (js) => js.replaceAll(/<\/script/giu, "\\u003C/script").rep
5859
5870
  * reads identically inside CSS strings/comments, so it's safe to emit.
5860
5871
  */
5861
5872
  const inlineStyle = (css) => css.replaceAll(/<\/style/giu, "<\\/style");
5862
- const ESCAPES = {
5873
+ const ESCAPES$1 = {
5863
5874
  "\"": "&quot;",
5864
5875
  "&": "&amp;",
5865
5876
  "'": "&#39;",
5866
5877
  "<": "&lt;",
5867
5878
  ">": "&gt;"
5868
5879
  };
5869
- const escapeHtml = (s) => s.replaceAll(/[&<>"']/gu, (c) => ESCAPES[c] ?? c);
5880
+ const escapeHtml = (s) => s.replaceAll(/[&<>"']/gu, (c) => ESCAPES$1[c] ?? c);
5870
5881
  /** Inline SVG from the bundled lucide set (bodies carry stroke attrs). */
5871
5882
  const iconSvg = (name) => {
5872
5883
  const icon = icons.icons[name];
@@ -6476,6 +6487,47 @@ const editorUrl = (editor, absPath, line) => {
6476
6487
  return scheme !== void 0 && SCRIPTABLE.has(scheme) ? void 0 : url;
6477
6488
  };
6478
6489
  //#endregion
6490
+ //#region src/workspace-syntax.ts
6491
+ const ALLOWED_STYLE_PROPERTIES = [
6492
+ "--shiki-light",
6493
+ "--shiki-dark",
6494
+ "--shiki-light-font-style",
6495
+ "--shiki-dark-font-style",
6496
+ "--shiki-light-font-weight",
6497
+ "--shiki-dark-font-weight",
6498
+ "--shiki-light-text-decoration",
6499
+ "--shiki-dark-text-decoration"
6500
+ ];
6501
+ const parseToken = (value) => {
6502
+ if (!isRecord$1(value) || typeof value.text !== "string" || !isRecord$1(value.style) || Array.isArray(value.style)) return;
6503
+ const style = {};
6504
+ for (const property of ALLOWED_STYLE_PROPERTIES) {
6505
+ if (!Object.hasOwn(value.style, property)) continue;
6506
+ const propertyValue = value.style[property];
6507
+ if (typeof propertyValue === "string") style[property] = propertyValue;
6508
+ }
6509
+ return {
6510
+ style,
6511
+ text: value.text
6512
+ };
6513
+ };
6514
+ /** Validate and narrow syntax tokens received through a workspace API. */
6515
+ const parseSyntaxLines = (value) => {
6516
+ if (!Array.isArray(value)) return;
6517
+ const lines = [];
6518
+ for (const lineValue of value) {
6519
+ if (!Array.isArray(lineValue)) return;
6520
+ const line = [];
6521
+ for (const tokenValue of lineValue) {
6522
+ const token = parseToken(tokenValue);
6523
+ if (token === void 0) return;
6524
+ line.push(token);
6525
+ }
6526
+ lines.push(line);
6527
+ }
6528
+ return lines;
6529
+ };
6530
+ //#endregion
6479
6531
  //#region src/rehype/shiki.ts
6480
6532
  /**
6481
6533
  * Fenced code blocks are syntax-highlighted with shiki at render time.
@@ -6512,6 +6564,7 @@ const PRELOADED_LANGS = [
6512
6564
  "jsx",
6513
6565
  "kotlin",
6514
6566
  "lua",
6567
+ "mdx",
6515
6568
  "markdown",
6516
6569
  "php",
6517
6570
  "python",
@@ -6569,7 +6622,7 @@ const getHighlighter = async () => {
6569
6622
  }
6570
6623
  };
6571
6624
  const DIFF_FENCE_LANGS = /* @__PURE__ */ new Set(["diff", "patch"]);
6572
- const isElement = (node) => isRecord(node) && node.type === "element" && typeof node.tagName === "string";
6625
+ const isElement = (node) => isRecord$1(node) && node.type === "element" && typeof node.tagName === "string";
6573
6626
  /** mdast-sourced nodes use `className`; shiki's hast uses `class`. Read both. */
6574
6627
  const classNames = (el) => {
6575
6628
  const c = el?.properties?.className ?? el?.properties?.class;
@@ -6615,6 +6668,30 @@ const readyHighlighter = async (lang) => {
6615
6668
  return;
6616
6669
  }
6617
6670
  };
6671
+ const plainSyntaxLines = (source) => source.split("\n").map((text) => [{
6672
+ style: {},
6673
+ text
6674
+ }]);
6675
+ /** Tokenize a complete MDX document for the workspace source and diff views. */
6676
+ const highlightMdxSource = async (source) => {
6677
+ const normalizedSource = source.replaceAll("\r\n", "\n");
6678
+ if (normalizedSource.includes("\r")) return plainSyntaxLines(normalizedSource);
6679
+ const highlighter = await readyHighlighter("mdx");
6680
+ if (highlighter === void 0) return plainSyntaxLines(normalizedSource);
6681
+ try {
6682
+ const result = highlighter.codeToTokens(normalizedSource, {
6683
+ defaultColor: false,
6684
+ lang: "mdx",
6685
+ themes: THEMES
6686
+ });
6687
+ return parseSyntaxLines(result.tokens.map((line) => line.map((token) => ({
6688
+ style: token.htmlStyle ?? {},
6689
+ text: token.content
6690
+ })))) ?? plainSyntaxLines(normalizedSource);
6691
+ } catch {
6692
+ return plainSyntaxLines(normalizedSource);
6693
+ }
6694
+ };
6618
6695
  /**
6619
6696
  * One side of a hunk, highlighted as the file's own language: `ctx`/`add`
6620
6697
  * rows read from the new side, `ctx`/`del` rows from the old side. Shiki's
@@ -6814,7 +6891,7 @@ const remarkAnnotationSources = () => (tree, file) => {
6814
6891
  "mdxJsxFlowElement",
6815
6892
  "mdxJsxTextElement"
6816
6893
  ].includes(node.type)) return;
6817
- const origin = isRecord(node.data) ? node.data.mdxrSourceFile : void 0;
6894
+ const origin = isRecord$1(node.data) ? node.data.mdxrSourceFile : void 0;
6818
6895
  const label = jsxAttr(node, "caption") ?? jsxAttr(node, "title") ?? jsxAttr(node, "aria-label") ?? jsxAttr(node, "alt") ?? "";
6819
6896
  const image = "url" in node && typeof node.url === "string" ? node.url : jsxAttr(node, "src") ?? "";
6820
6897
  const text = typeof node.value === "string" ? node.value : textContent$1(node);
@@ -6928,8 +7005,8 @@ const remarkSectionReviews = () => (tree) => {
6928
7005
  if (!isFlowElement(node) || jsxAttr(node, "data-mdxr-page") === void 0) return;
6929
7006
  const [heading] = node.children ?? [];
6930
7007
  const data = heading?.data;
6931
- const properties = isRecord(data) ? data.hProperties : void 0;
6932
- if (heading?.type !== "heading" || !("depth" in heading) || heading.depth !== 2 || !isRecord(properties) || typeof properties.id !== "string" || node.children === void 0) return;
7008
+ const properties = isRecord$1(data) ? data.hProperties : void 0;
7009
+ if (heading?.type !== "heading" || !("depth" in heading) || heading.depth !== 2 || !isRecord$1(properties) || typeof properties.id !== "string" || node.children === void 0) return;
6933
7010
  const control = {
6934
7011
  attributes: jsxAttrs({
6935
7012
  "data-section-id": properties.id,
@@ -6979,8 +7056,8 @@ const mdxToHtml = async (source, components, filePath = "document.mdx", opts = {
6979
7056
  value: source
6980
7057
  });
6981
7058
  matter(file);
6982
- const fmRaw = isRecord(file.data) ? file.data.matter : void 0;
6983
- const frontmatter = isRecord(fmRaw) ? fmRaw : {};
7059
+ const fmRaw = isRecord$1(file.data) ? file.data.matter : void 0;
7060
+ const frontmatter = isRecord$1(fmRaw) ? fmRaw : {};
6984
7061
  const editor = typeof frontmatter.editor === "string" && frontmatter.editor !== "" ? frontmatter.editor : opts.editor;
6985
7062
  const dir = file.dirname ?? ".";
6986
7063
  const fileLinks = /* @__PURE__ */ new Map();
@@ -7740,6 +7817,1384 @@ const renderFile = async (mdxPath, opts = {}) => {
7740
7817
  });
7741
7818
  };
7742
7819
  //#endregion
7820
+ //#region src/agent-markdown.ts
7821
+ const ESCAPES = {
7822
+ "\"": "&quot;",
7823
+ "&": "&amp;",
7824
+ "'": "&#39;",
7825
+ "<": "&lt;",
7826
+ ">": "&gt;"
7827
+ };
7828
+ const WRAPPERS = {
7829
+ blockquote: ["<blockquote>", "</blockquote>"],
7830
+ delete: ["<del>", "</del>"],
7831
+ emphasis: ["<em>", "</em>"],
7832
+ listItem: ["<li>", "</li>"],
7833
+ paragraph: ["<p>", "</p>"],
7834
+ strong: ["<strong>", "</strong>"],
7835
+ table: ["<table>", "</table>"],
7836
+ tableCell: ["<td>", "</td>"],
7837
+ tableRow: ["<tr>", "</tr>"]
7838
+ };
7839
+ const escape = (value) => value.replaceAll(/[&<>"']/gu, (char) => ESCAPES[char] ?? char);
7840
+ const safeUrl = (value) => {
7841
+ const trimmed = value.trim();
7842
+ return /^(?:https?:|mailto:|\/|#)/iu.test(trimmed) ? trimmed : void 0;
7843
+ };
7844
+ const renderLiteral = (node) => {
7845
+ if (node.type === "code") return `<pre><code>${escape(node.value ?? "")}</code></pre>`;
7846
+ if (node.type === "inlineCode") return `<code>${escape(node.value ?? "")}</code>`;
7847
+ if (node.type === "break") return "<br>";
7848
+ if (node.type === "thematicBreak") return "<hr>";
7849
+ };
7850
+ const renderNode = (node) => {
7851
+ const literal = renderLiteral(node);
7852
+ if (literal !== void 0) return literal;
7853
+ const children = (node.children ?? []).map(renderNode).join("");
7854
+ const wrapper = WRAPPERS[node.type];
7855
+ if (wrapper !== void 0) return `${wrapper[0]}${children}${wrapper[1]}`;
7856
+ if (node.type === "heading") {
7857
+ const depth = Math.min(Math.max(node.depth ?? 1, 1), 6);
7858
+ return `<h${depth}>${children}</h${depth}>`;
7859
+ }
7860
+ if (node.type === "list") return node.ordered === true ? `<ol>${children}</ol>` : `<ul>${children}</ul>`;
7861
+ if (node.type === "link") {
7862
+ const url = safeUrl(node.url ?? "");
7863
+ return url === void 0 ? children : `<a href="${escape(url)}" rel="noopener noreferrer" target="_blank">${children}</a>`;
7864
+ }
7865
+ return children || escape(node.value ?? "");
7866
+ };
7867
+ /** Render agent text as inert Markdown; MDX JSX and raw HTML never execute. */
7868
+ const renderAgentMarkdown = (source) => {
7869
+ const tree = unified().use(remarkParse).use(remarkGfm).parse(source);
7870
+ return renderNode(tree);
7871
+ };
7872
+ //#endregion
7873
+ //#region src/agent-http.ts
7874
+ const view = (conversation) => ({
7875
+ ...conversation,
7876
+ messages: conversation.messages.map((message) => ({
7877
+ ...message,
7878
+ html: renderAgentMarkdown(message.content)
7879
+ })),
7880
+ sessionId: conversation.sessionId ?? null
7881
+ });
7882
+ const reply = (res, status, body) => {
7883
+ res.writeHead(status, {
7884
+ "cache-control": "no-store",
7885
+ "content-type": "application/json; charset=utf-8",
7886
+ "x-content-type-options": "nosniff"
7887
+ });
7888
+ res.end(JSON.stringify(body));
7889
+ };
7890
+ const sameOrigin$1 = (req) => {
7891
+ const port = req.socket.localPort;
7892
+ const host = req.headers.host ?? "";
7893
+ return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (req.headers.origin === void 0 || req.headers.origin === `http://${host}`);
7894
+ };
7895
+ const readMessage = async (req) => {
7896
+ if (req.headers["content-type"]?.startsWith("application/json") !== true) throw new Error("Expected application/json");
7897
+ let source = "";
7898
+ for await (const chunk of req) {
7899
+ source += Buffer.isBuffer(chunk) ? chunk.toString("utf-8") : String(chunk);
7900
+ if (source.length > 65536) throw new Error("Message is too large");
7901
+ }
7902
+ let value;
7903
+ try {
7904
+ value = JSON.parse(source);
7905
+ } catch {
7906
+ throw new Error("Invalid JSON");
7907
+ }
7908
+ const message = typeof value === "object" && value !== null && "message" in value ? value.message : void 0;
7909
+ if (typeof message !== "string" || message.trim() === "" || message.length > 65536) throw new Error("Expected a non-empty message");
7910
+ return message.trim();
7911
+ };
7912
+ const errorStatus = (detail) => {
7913
+ if (detail === "Agent is already responding") return 409;
7914
+ if (detail === "Message is too large") return 413;
7915
+ if (detail === "Expected application/json" || detail === "Invalid JSON" || detail === "Expected a non-empty message") return 400;
7916
+ return 500;
7917
+ };
7918
+ /** Local-only JSON endpoint for one served document's agent conversation. */
7919
+ const handleAgentRequest = async (req, res, agent, notify, beforeSend) => {
7920
+ let started = false;
7921
+ try {
7922
+ if (agent === void 0) {
7923
+ reply(res, 404, { error: "Agent chat is disabled" });
7924
+ return;
7925
+ }
7926
+ if (!sameOrigin$1(req)) {
7927
+ reply(res, 403, { error: "Request origin is not allowed" });
7928
+ return;
7929
+ }
7930
+ if (req.method === "GET") {
7931
+ reply(res, 200, view(await agent.current()));
7932
+ return;
7933
+ }
7934
+ if (req.method !== "POST") {
7935
+ reply(res, 405, { error: "Method not allowed" });
7936
+ return;
7937
+ }
7938
+ const message = await readMessage(req);
7939
+ await beforeSend?.();
7940
+ started = true;
7941
+ reply(res, 200, view(await agent.send(message)));
7942
+ notify();
7943
+ } catch (error) {
7944
+ if (started) notify();
7945
+ if (res.headersSent) return;
7946
+ const detail = formatError(error);
7947
+ reply(res, errorStatus(detail), { error: detail });
7948
+ }
7949
+ };
7950
+ //#endregion
7951
+ //#region src/agent-preview.ts
7952
+ const css = readFileSync(path.join(srcDir, "assets", "workspace.css"), "utf-8");
7953
+ /** Add the preview workspace without modifying the rendered MDX tree. */
7954
+ const injectAgentPreview = (html, provider) => {
7955
+ const agentAttributes = provider === void 0 ? "" : ` data-mdxr-agent data-agent-provider="${provider}"`;
7956
+ const workspace = `<style>${inlineStyle(css)}</style><div id="mdxr-workspace-root"${agentAttributes}></div><script src="/__mdxr_workspace.js" defer><\/script>`;
7957
+ return html.includes("</body>") ? html.replace("</body>", `${workspace}</body>`) : `${html}${workspace}`;
7958
+ };
7959
+ //#endregion
7960
+ //#region src/claude-stream-session.ts
7961
+ var UserMessageQueue = class {
7962
+ messages = [];
7963
+ readers = [];
7964
+ closed = false;
7965
+ [Symbol.asyncIterator]() {
7966
+ return this;
7967
+ }
7968
+ close() {
7969
+ this.closed = true;
7970
+ for (const reader of this.readers.splice(0)) reader({
7971
+ done: true,
7972
+ value: void 0
7973
+ });
7974
+ }
7975
+ async next() {
7976
+ const message = this.messages.shift();
7977
+ if (message !== void 0) return {
7978
+ done: false,
7979
+ value: message
7980
+ };
7981
+ if (this.closed) return {
7982
+ done: true,
7983
+ value: void 0
7984
+ };
7985
+ return await new Promise((resolve) => {
7986
+ this.readers.push(resolve);
7987
+ });
7988
+ }
7989
+ push(message) {
7990
+ if (this.closed) throw new Error("Claude Agent SDK input is closed");
7991
+ const reader = this.readers.shift();
7992
+ if (reader === void 0) {
7993
+ this.messages.push(message);
7994
+ return;
7995
+ }
7996
+ reader({
7997
+ done: false,
7998
+ value: message
7999
+ });
8000
+ }
8001
+ };
8002
+ const asError = (error) => error instanceof Error ? error : new Error(String(error));
8003
+ const noApprovalError = (tools) => /* @__PURE__ */ new Error(`Claude Code denied ${tools.join(", ")} because the MDX preview has no tool approval UI.`);
8004
+ /**
8005
+ * Create an MDX-owned, long-lived Claude Agent SDK session for `cwd`.
8006
+ * This starts a new session; it cannot attach to an already-running Claude CLI.
8007
+ */
8008
+ const createClaudeStreamSession = (cwd) => {
8009
+ const absoluteCwd = path.resolve(cwd);
8010
+ const input = new UserMessageQueue();
8011
+ let activeTurn;
8012
+ let failure;
8013
+ let closed = false;
8014
+ let sessionId;
8015
+ let abortController;
8016
+ let runner;
8017
+ const finishTurn = (turn, result) => {
8018
+ if (activeTurn === turn) activeTurn = void 0;
8019
+ turn.resolve(result);
8020
+ };
8021
+ const failTurn = (turn, error) => {
8022
+ if (activeTurn === turn) activeTurn = void 0;
8023
+ turn.reject(error);
8024
+ };
8025
+ const failSession = (error) => {
8026
+ if (failure !== void 0 || closed) return;
8027
+ failure = asError(error);
8028
+ input.close();
8029
+ abortController?.abort();
8030
+ if (activeTurn !== void 0) failTurn(activeTurn, failure);
8031
+ };
8032
+ const handleMessage = (message) => {
8033
+ if (message.type === "system" && message.subtype === "init") {
8034
+ sessionId = message.session_id;
8035
+ return;
8036
+ }
8037
+ if (message.type !== "result") return;
8038
+ sessionId = message.session_id;
8039
+ const turn = activeTurn;
8040
+ if (turn === void 0) return;
8041
+ const resultUserIds = message.user_message_uuids ?? [];
8042
+ if (!(message.user_message_uuid === void 0 || message.user_message_uuid === turn.uuid || resultUserIds.includes(turn.uuid))) return;
8043
+ if (message.permission_denials.length > 0) {
8044
+ const deniedTools = [...new Set(message.permission_denials.map((denial) => denial.tool_name))];
8045
+ failTurn(turn, noApprovalError(deniedTools));
8046
+ return;
8047
+ }
8048
+ if (message.subtype !== "success") {
8049
+ failTurn(turn, /* @__PURE__ */ new Error(`Claude Agent SDK turn failed (${message.subtype}): ${message.errors.join("; ")}`));
8050
+ return;
8051
+ }
8052
+ if (message.is_error) {
8053
+ failTurn(turn, /* @__PURE__ */ new Error(`Claude Code returned an error: ${message.result}`));
8054
+ return;
8055
+ }
8056
+ if (sessionId === void 0 || sessionId === "") {
8057
+ failTurn(turn, /* @__PURE__ */ new Error("Claude Agent SDK did not provide a session ID"));
8058
+ return;
8059
+ }
8060
+ finishTurn(turn, {
8061
+ answer: message.result,
8062
+ sessionId
8063
+ });
8064
+ };
8065
+ const start = () => {
8066
+ if (runner !== void 0 || failure !== void 0 || closed) return;
8067
+ abortController = new AbortController();
8068
+ try {
8069
+ const stream = query({
8070
+ options: {
8071
+ abortController,
8072
+ cwd: absoluteCwd,
8073
+ permissionMode: "dontAsk"
8074
+ },
8075
+ prompt: input
8076
+ });
8077
+ runner = (async () => {
8078
+ try {
8079
+ for await (const message of stream) handleMessage(message);
8080
+ if (!closed) throw new Error("Claude Agent SDK session ended unexpectedly");
8081
+ } catch (error) {
8082
+ failSession(error);
8083
+ }
8084
+ })();
8085
+ } catch (error) {
8086
+ failSession(error);
8087
+ }
8088
+ };
8089
+ const sendOne = async (message) => {
8090
+ if (closed) throw new Error("Claude Agent SDK session is closed");
8091
+ if (failure !== void 0) throw failure;
8092
+ if (activeTurn !== void 0) throw new Error("Claude Agent SDK is already responding");
8093
+ if (message.trim() === "") throw new Error("Message must not be empty");
8094
+ const uuid = randomUUID();
8095
+ return await new Promise((resolve, reject) => {
8096
+ const turn = {
8097
+ reject,
8098
+ resolve,
8099
+ uuid
8100
+ };
8101
+ activeTurn = turn;
8102
+ start();
8103
+ if (failure === void 0) try {
8104
+ input.push({
8105
+ message: {
8106
+ content: message,
8107
+ role: "user"
8108
+ },
8109
+ parent_tool_use_id: null,
8110
+ type: "user",
8111
+ uuid
8112
+ });
8113
+ } catch (error) {
8114
+ failTurn(turn, asError(error));
8115
+ }
8116
+ });
8117
+ };
8118
+ const close = async () => {
8119
+ if (!closed) {
8120
+ closed = true;
8121
+ input.close();
8122
+ abortController?.abort();
8123
+ if (activeTurn !== void 0) failTurn(activeTurn, /* @__PURE__ */ new Error("Claude Agent SDK session was closed"));
8124
+ }
8125
+ await runner;
8126
+ };
8127
+ return {
8128
+ close,
8129
+ send: sendOne
8130
+ };
8131
+ };
8132
+ //#endregion
8133
+ //#region src/codex-app-server.ts
8134
+ const DEFAULT_ENDPOINT = "ws://127.0.0.1:4500";
8135
+ const DEFAULT_DAEMON_WS_ENDPOINT = "ws://localhost/";
8136
+ const RPC_TIMEOUT_MS = 3e4;
8137
+ const MAX_BUFFERED_TURNS = 8;
8138
+ var CodexAppServerError = class extends Error {
8139
+ kind;
8140
+ constructor(message, kind = "general", options) {
8141
+ super(message, options);
8142
+ this.kind = kind;
8143
+ this.name = "CodexAppServerError";
8144
+ }
8145
+ };
8146
+ const isRecord = (value) => typeof value === "object" && value !== null;
8147
+ const isRpcId = (value) => typeof value === "number" || typeof value === "string";
8148
+ const decodeFrame = (data) => {
8149
+ if (Array.isArray(data)) return Buffer.concat(data).toString("utf-8");
8150
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf-8");
8151
+ return data.toString("utf-8");
8152
+ };
8153
+ const defaultDaemonSocketPath = () => {
8154
+ const configuredCodexHome = process.env.CODEX_HOME;
8155
+ const codexHome = configuredCodexHome === void 0 || configuredCodexHome.length === 0 ? path.join(homedir(), ".codex") : configuredCodexHome;
8156
+ return path.join(codexHome, "app-server-control", "app-server-control.sock");
8157
+ };
8158
+ const resolveEndpoint = (endpoint, useDefaultDaemon) => {
8159
+ const resolved = endpoint ?? process.env.CODEX_APP_SERVER_URL ?? (useDefaultDaemon ? `unix://${defaultDaemonSocketPath()}` : DEFAULT_ENDPOINT);
8160
+ let parsed;
8161
+ try {
8162
+ parsed = new URL(resolved);
8163
+ } catch (error) {
8164
+ throw new CodexAppServerError("Invalid Codex App Server endpoint.", "general", { cause: error });
8165
+ }
8166
+ if (parsed.protocol === "unix:") {
8167
+ let socketPath;
8168
+ try {
8169
+ socketPath = decodeURIComponent(parsed.pathname);
8170
+ } catch (error) {
8171
+ throw new CodexAppServerError("Invalid Codex App Server Unix socket path.", "general", { cause: error });
8172
+ }
8173
+ if (parsed.host !== "" || !path.isAbsolute(socketPath) || parsed.search !== "" || parsed.hash !== "") throw new CodexAppServerError("Codex App Server Unix endpoint must be unix:///absolute/path without query or fragment.");
8174
+ return {
8175
+ socketPath,
8176
+ url: DEFAULT_DAEMON_WS_ENDPOINT
8177
+ };
8178
+ }
8179
+ if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") throw new CodexAppServerError("Codex App Server endpoint must use ws://, wss://, or unix:///absolute/path.");
8180
+ if (parsed.username !== "" || parsed.password !== "") throw new CodexAppServerError("Codex App Server endpoint must not contain credentials; configure authentication separately.");
8181
+ return { url: resolved };
8182
+ };
8183
+ const isActiveWriterConflict = (message) => {
8184
+ const normalized = message.toLowerCase();
8185
+ return normalized.includes("thread-store conflict") || normalized.includes("already has an active writer") || normalized.includes("active writer conflict");
8186
+ };
8187
+ const makeTurnState = () => ({
8188
+ messageOrder: [],
8189
+ messages: /* @__PURE__ */ new Map()
8190
+ });
8191
+ const getAgentMessage = (state, itemId) => {
8192
+ let item = state.messages.get(itemId);
8193
+ if (item === void 0) {
8194
+ item = { text: "" };
8195
+ state.messages.set(itemId, item);
8196
+ state.messageOrder.push(itemId);
8197
+ }
8198
+ return item;
8199
+ };
8200
+ const getNotificationTurnId = (params) => {
8201
+ const { turnId, turn } = params;
8202
+ if (typeof turnId === "string") return turnId;
8203
+ if (!isRecord(turn)) return;
8204
+ return typeof turn.id === "string" ? turn.id : void 0;
8205
+ };
8206
+ const createDeferred = () => {
8207
+ let handlers;
8208
+ const promise = new Promise((resolve, reject) => {
8209
+ handlers = {
8210
+ reject,
8211
+ resolve
8212
+ };
8213
+ });
8214
+ if (handlers === void 0) throw new Error("Could not create a Codex turn completion promise.");
8215
+ return {
8216
+ promise,
8217
+ reject: handlers.reject,
8218
+ resolve: handlers.resolve
8219
+ };
8220
+ };
8221
+ const settleQueuedMessage = (queuedMessage, outcome) => {
8222
+ if (!queuedMessage.acknowledged) {
8223
+ queuedMessage.outcome = outcome;
8224
+ return;
8225
+ }
8226
+ if (outcome.type === "resolve") queuedMessage.completion.resolve(outcome.answer);
8227
+ else queuedMessage.completion.reject(outcome.error);
8228
+ };
8229
+ const acknowledgeQueuedMessage = (queuedMessage) => {
8230
+ queuedMessage.acknowledged = true;
8231
+ if (queuedMessage.outcome !== void 0) settleQueuedMessage(queuedMessage, queuedMessage.outcome);
8232
+ };
8233
+ /**
8234
+ * Create a long-lived JSON-RPC client for the same Codex App Server used by
8235
+ * `codex --remote`. Calls reuse one socket and one thread until `close()`.
8236
+ */
8237
+ const createCodexAppServerSession = (options = {}) => {
8238
+ const endpoint = resolveEndpoint(options.endpoint, options.initialSessionId !== void 0);
8239
+ let socket;
8240
+ let connectionPromise;
8241
+ let nextRequestId = 1;
8242
+ let threadId;
8243
+ let activeTurn;
8244
+ let isClosed = false;
8245
+ let isClosing = false;
8246
+ let closePromise;
8247
+ let busy = false;
8248
+ let attachedToExistingThread = options.initialSessionId !== void 0;
8249
+ const pendingRequests = /* @__PURE__ */ new Map();
8250
+ const turns = /* @__PURE__ */ new Map();
8251
+ const queuedMessagesByClientId = /* @__PURE__ */ new Map();
8252
+ const queuedMessagesByTurnId = /* @__PURE__ */ new Map();
8253
+ const makeTransportError = (reason, cause) => new CodexAppServerError(`Codex App Server connection ${reason}${cause instanceof Error ? `: ${cause.message}` : ""}`, "general", cause instanceof Error ? { cause } : void 0);
8254
+ const rejectPendingRequests = (error) => {
8255
+ for (const [id, pending] of pendingRequests) {
8256
+ clearTimeout(pending.timeout);
8257
+ pendingRequests.delete(id);
8258
+ pending.reject(error);
8259
+ }
8260
+ };
8261
+ const rejectQueuedMessages = (error) => {
8262
+ for (const queuedMessage of queuedMessagesByClientId.values()) settleQueuedMessage(queuedMessage, {
8263
+ error,
8264
+ type: "reject"
8265
+ });
8266
+ queuedMessagesByClientId.clear();
8267
+ queuedMessagesByTurnId.clear();
8268
+ };
8269
+ const sendFrame = (value) => {
8270
+ if (socket?.readyState !== WebSocket.OPEN) throw makeTransportError("is not open.");
8271
+ socket.send(JSON.stringify(value), (error) => {
8272
+ if (error instanceof Error) {
8273
+ const wrapped = makeTransportError("failed while sending a message.", error);
8274
+ rejectPendingRequests(wrapped);
8275
+ activeTurn?.reject(wrapped);
8276
+ rejectQueuedMessages(wrapped);
8277
+ }
8278
+ });
8279
+ };
8280
+ const replyToServerRequest = (id, method) => {
8281
+ if (attachedToExistingThread) return;
8282
+ if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
8283
+ sendFrame({
8284
+ id,
8285
+ result: { decision: "decline" }
8286
+ });
8287
+ return;
8288
+ }
8289
+ if (method === "item/tool/requestUserInput" || method === "mcpServer/elicitation/request") {
8290
+ sendFrame({
8291
+ id,
8292
+ result: {
8293
+ action: "decline",
8294
+ content: null
8295
+ }
8296
+ });
8297
+ return;
8298
+ }
8299
+ sendFrame({
8300
+ error: {
8301
+ code: -32601,
8302
+ message: `mdxr does not support the server request ${method}`
8303
+ },
8304
+ id
8305
+ });
8306
+ };
8307
+ const finishActiveTurnIfReady = () => {
8308
+ const currentTurn = activeTurn;
8309
+ if (currentTurn === void 0 || currentTurn.turnId === void 0) return;
8310
+ const activeTurnId = currentTurn.turnId;
8311
+ const turn = turns.get(activeTurnId);
8312
+ const completedTurn = turn?.completedTurn;
8313
+ if (turn === void 0 || completedTurn === void 0) return;
8314
+ const { status } = completedTurn;
8315
+ if (status !== "completed") {
8316
+ const { error } = completedTurn;
8317
+ const detail = isRecord(error) ? error.message : void 0;
8318
+ const message = typeof detail === "string" ? `Codex turn ${String(status)}: ${detail}` : `Codex turn finished with status ${String(status)}.`;
8319
+ currentTurn.reject(new CodexAppServerError(message));
8320
+ return;
8321
+ }
8322
+ const messages = turn.messageOrder.flatMap((itemId) => {
8323
+ const item = turn.messages.get(itemId);
8324
+ return item === void 0 ? [] : [item];
8325
+ });
8326
+ if (Array.isArray(completedTurn.items)) {
8327
+ for (const item of completedTurn.items) if (isRecord(item) && item.type === "agentMessage" && typeof item.id === "string" && typeof item.text === "string") {
8328
+ const message = getAgentMessage(turn, item.id);
8329
+ message.text = item.text;
8330
+ if (typeof item.phase === "string") message.phase = item.phase;
8331
+ }
8332
+ }
8333
+ const completedMessages = turn.messageOrder.flatMap((itemId) => {
8334
+ const item = turn.messages.get(itemId);
8335
+ return item === void 0 ? [] : [item];
8336
+ });
8337
+ const answer = completedMessages.toReversed().find((item) => item.phase === "final_answer") ?? completedMessages.at(-1) ?? messages.at(-1);
8338
+ if (threadId === void 0 || answer === void 0 || answer.text.trim() === "") {
8339
+ currentTurn.reject(new CodexAppServerError("Codex returned no answer."));
8340
+ return;
8341
+ }
8342
+ currentTurn.resolve({
8343
+ answer: answer.text,
8344
+ sessionId: threadId
8345
+ });
8346
+ };
8347
+ const finishQueuedMessageIfReady = (turnId, turn) => {
8348
+ const queuedMessage = queuedMessagesByTurnId.get(turnId);
8349
+ const { completedTurn } = turn;
8350
+ if (queuedMessage === void 0 || completedTurn === void 0) return;
8351
+ queuedMessagesByTurnId.delete(turnId);
8352
+ queuedMessagesByClientId.delete(queuedMessage.clientUserMessageId);
8353
+ const { status, error } = completedTurn;
8354
+ if (status !== "completed") {
8355
+ const detail = isRecord(error) ? error.message : void 0;
8356
+ settleQueuedMessage(queuedMessage, {
8357
+ error: new CodexAppServerError(typeof detail === "string" ? `Codex turn ${String(status)}: ${detail}` : `Codex turn finished with status ${String(status)}.`),
8358
+ type: "reject"
8359
+ });
8360
+ return;
8361
+ }
8362
+ const messages = turn.messageOrder.flatMap((itemId) => {
8363
+ const item = turn.messages.get(itemId);
8364
+ return item === void 0 ? [] : [item];
8365
+ });
8366
+ if (Array.isArray(completedTurn.items)) {
8367
+ for (const item of completedTurn.items) if (isRecord(item) && item.type === "agentMessage" && typeof item.id === "string" && typeof item.text === "string") {
8368
+ const message = getAgentMessage(turn, item.id);
8369
+ message.text = item.text;
8370
+ if (typeof item.phase === "string") message.phase = item.phase;
8371
+ }
8372
+ }
8373
+ const completedMessages = turn.messageOrder.flatMap((itemId) => {
8374
+ const item = turn.messages.get(itemId);
8375
+ return item === void 0 ? [] : [item];
8376
+ });
8377
+ const answer = completedMessages.toReversed().find((item) => item.phase === "final_answer") ?? completedMessages.at(-1) ?? messages.at(-1);
8378
+ if (answer === void 0 || answer.text.trim() === "") {
8379
+ settleQueuedMessage(queuedMessage, {
8380
+ error: new CodexAppServerError("Codex returned no answer for the queued message."),
8381
+ type: "reject"
8382
+ });
8383
+ return;
8384
+ }
8385
+ settleQueuedMessage(queuedMessage, {
8386
+ answer: answer.text,
8387
+ type: "resolve"
8388
+ });
8389
+ };
8390
+ const getTurnState = (turnId) => {
8391
+ const existing = turns.get(turnId);
8392
+ if (existing !== void 0) return existing;
8393
+ const created = makeTurnState();
8394
+ turns.set(turnId, created);
8395
+ return created;
8396
+ };
8397
+ const handleAgentMessageDelta = (turn, params) => {
8398
+ const { itemId, delta } = params;
8399
+ if (typeof itemId !== "string" || typeof delta !== "string") return;
8400
+ getAgentMessage(turn, itemId).text += delta;
8401
+ };
8402
+ const handleCompletedItem = (turn, params) => {
8403
+ const { item } = params;
8404
+ if (!isRecord(item) || item.type !== "agentMessage" || typeof item.id !== "string" || typeof item.text !== "string") return;
8405
+ const agentMessage = getAgentMessage(turn, item.id);
8406
+ agentMessage.text = item.text;
8407
+ if (typeof item.phase === "string") agentMessage.phase = item.phase;
8408
+ };
8409
+ const handleStartedItem = (turnId, params) => {
8410
+ const { item } = params;
8411
+ if (!isRecord(item) || item.type !== "userMessage") return;
8412
+ const { clientId } = item;
8413
+ if (typeof clientId !== "string") return;
8414
+ const queuedMessage = queuedMessagesByClientId.get(clientId);
8415
+ if (queuedMessage === void 0) return;
8416
+ queuedMessage.turnId = turnId;
8417
+ queuedMessagesByTurnId.set(turnId, queuedMessage);
8418
+ };
8419
+ const pruneTurnBuffer = (activeTurnId) => {
8420
+ if (turns.size <= MAX_BUFFERED_TURNS) return;
8421
+ const oldestTurnId = turns.keys().next().value;
8422
+ if (oldestTurnId !== void 0 && oldestTurnId !== activeTurnId) turns.delete(oldestTurnId);
8423
+ };
8424
+ const handleNotification = (message) => {
8425
+ const { method, params } = message;
8426
+ if (typeof method !== "string" || !isRecord(params) || params.threadId !== threadId) return;
8427
+ const turnId = getNotificationTurnId(params);
8428
+ if (turnId === void 0) return;
8429
+ const turn = getTurnState(turnId);
8430
+ switch (method) {
8431
+ case "item/started":
8432
+ handleStartedItem(turnId, params);
8433
+ break;
8434
+ case "item/agentMessage/delta":
8435
+ handleAgentMessageDelta(turn, params);
8436
+ break;
8437
+ case "item/completed":
8438
+ handleCompletedItem(turn, params);
8439
+ break;
8440
+ case "turn/completed": if (isRecord(params.turn)) {
8441
+ turn.completedTurn = params.turn;
8442
+ finishActiveTurnIfReady();
8443
+ finishQueuedMessageIfReady(turnId, turn);
8444
+ }
8445
+ }
8446
+ pruneTurnBuffer(activeTurn?.turnId ?? "");
8447
+ };
8448
+ const handleResponse = (message) => {
8449
+ const { id } = message;
8450
+ if (!isRpcId(id)) return;
8451
+ const pending = pendingRequests.get(id);
8452
+ if (pending === void 0) return;
8453
+ pendingRequests.delete(id);
8454
+ clearTimeout(pending.timeout);
8455
+ const { error } = message;
8456
+ if (isRecord(error)) {
8457
+ const messageText = typeof error.message === "string" ? error.message : "Unknown JSON-RPC error";
8458
+ if (pending.method === "thread/resume" && options.initialSessionId !== void 0 && isActiveWriterConflict(messageText) && options.initialSessionId !== void 0) {
8459
+ pending.reject(new CodexAppServerError(`Cannot resume Codex session ${options.initialSessionId}: another active Codex App Server owns this session. Connect mdxr and the Codex TUI to the same --remote endpoint. The session ID was not changed.`, "session-conflict"));
8460
+ return;
8461
+ }
8462
+ pending.reject(new CodexAppServerError(`${pending.method} failed: ${messageText}`));
8463
+ return;
8464
+ }
8465
+ pending.resolve(message.result);
8466
+ };
8467
+ const handleMessage = (data) => {
8468
+ let parsed;
8469
+ try {
8470
+ parsed = JSON.parse(decodeFrame(data));
8471
+ } catch {
8472
+ return;
8473
+ }
8474
+ if (!isRecord(parsed)) return;
8475
+ const { id, method } = parsed;
8476
+ if (isRpcId(id) && typeof method === "string") replyToServerRequest(id, method);
8477
+ else if (isRpcId(id)) handleResponse(parsed);
8478
+ else handleNotification(parsed);
8479
+ };
8480
+ const failConnection = (error) => {
8481
+ rejectPendingRequests(error);
8482
+ activeTurn?.reject(error);
8483
+ rejectQueuedMessages(error);
8484
+ };
8485
+ const request = async (method, params) => {
8486
+ const id = nextRequestId;
8487
+ nextRequestId += 1;
8488
+ const response = createDeferred();
8489
+ const timeout = setTimeout(() => {
8490
+ pendingRequests.delete(id);
8491
+ response.reject(new CodexAppServerError(`${method} timed out after ${RPC_TIMEOUT_MS / 1e3} seconds.`));
8492
+ }, RPC_TIMEOUT_MS);
8493
+ timeout.unref?.();
8494
+ pendingRequests.set(id, {
8495
+ method,
8496
+ reject: response.reject,
8497
+ resolve: response.resolve,
8498
+ timeout
8499
+ });
8500
+ try {
8501
+ sendFrame({
8502
+ id,
8503
+ method,
8504
+ params
8505
+ });
8506
+ } catch (error) {
8507
+ pendingRequests.delete(id);
8508
+ clearTimeout(timeout);
8509
+ response.reject(error instanceof Error ? error : new Error(String(error)));
8510
+ }
8511
+ return await response.promise;
8512
+ };
8513
+ const notify = (method, params) => {
8514
+ sendFrame({
8515
+ method,
8516
+ params
8517
+ });
8518
+ };
8519
+ const connect$1 = async () => {
8520
+ if (isClosed) throw makeTransportError("was closed by mdxr.");
8521
+ if (connectionPromise !== void 0) {
8522
+ await connectionPromise;
8523
+ return;
8524
+ }
8525
+ const { socketPath } = endpoint;
8526
+ const nextSocket = new WebSocket(endpoint.url, {
8527
+ ...socketPath === void 0 ? {} : { createConnection: () => connect(socketPath) },
8528
+ perMessageDeflate: false
8529
+ });
8530
+ socket = nextSocket;
8531
+ nextSocket.on("message", handleMessage);
8532
+ nextSocket.on("error", (error) => {
8533
+ failConnection(makeTransportError("failed.", error));
8534
+ });
8535
+ nextSocket.on("close", (code, reason) => {
8536
+ if (!isClosing) {
8537
+ const detail = reason.toString("utf-8");
8538
+ const suffix = detail === "" ? "" : `: ${detail}`;
8539
+ failConnection(makeTransportError(`closed unexpectedly (${code}${suffix}).`));
8540
+ }
8541
+ });
8542
+ connectionPromise = (async () => {
8543
+ await once(nextSocket, "open");
8544
+ await request("initialize", {
8545
+ capabilities: { experimentalApi: true },
8546
+ clientInfo: {
8547
+ name: "mdxr",
8548
+ title: "mdxr",
8549
+ version: "0.7.0"
8550
+ }
8551
+ });
8552
+ notify("initialized", {});
8553
+ })();
8554
+ await connectionPromise;
8555
+ };
8556
+ const ensureThread = async () => {
8557
+ if (threadId !== void 0) return threadId;
8558
+ if (options.initialSessionId !== void 0) {
8559
+ const result = await request("thread/resume", {
8560
+ excludeTurns: true,
8561
+ threadId: options.initialSessionId
8562
+ });
8563
+ const thread = isRecord(result) ? result.thread : void 0;
8564
+ const resumedId = isRecord(thread) ? thread.id : void 0;
8565
+ if (typeof resumedId !== "string") throw new CodexAppServerError(`Codex App Server did not resume session ${options.initialSessionId}.`);
8566
+ if (resumedId !== options.initialSessionId) throw new CodexAppServerError(`Codex App Server returned thread ${resumedId} while resuming ${options.initialSessionId}; refusing to switch sessions.`);
8567
+ threadId = resumedId;
8568
+ attachedToExistingThread = true;
8569
+ return threadId;
8570
+ }
8571
+ const params = {};
8572
+ if (options.cwd !== void 0) params.cwd = options.cwd;
8573
+ const result = await request("thread/start", params);
8574
+ const thread = isRecord(result) ? result.thread : void 0;
8575
+ const startedId = isRecord(thread) ? thread.id : void 0;
8576
+ if (typeof startedId !== "string") throw new CodexAppServerError("Codex App Server did not start a thread.");
8577
+ threadId = startedId;
8578
+ return threadId;
8579
+ };
8580
+ const send = async (message) => {
8581
+ if (message.trim() === "") throw new CodexAppServerError("Codex message must not be empty.");
8582
+ if (busy) throw new CodexAppServerError("Codex is already responding in this session.");
8583
+ busy = true;
8584
+ try {
8585
+ await connect$1();
8586
+ const currentThreadId = await ensureThread();
8587
+ const result = await request("turn/start", {
8588
+ input: [{
8589
+ text: message,
8590
+ type: "text"
8591
+ }],
8592
+ threadId: currentThreadId
8593
+ });
8594
+ const turn = isRecord(result) ? result.turn : void 0;
8595
+ const startedTurnId = isRecord(turn) ? turn.id : void 0;
8596
+ if (typeof startedTurnId !== "string") throw new CodexAppServerError("Codex App Server did not return a turn ID.");
8597
+ const completed = createDeferred();
8598
+ const waiter = {
8599
+ reject: completed.reject,
8600
+ resolve: completed.resolve,
8601
+ turnId: startedTurnId
8602
+ };
8603
+ activeTurn = waiter;
8604
+ try {
8605
+ if (isRecord(turn) && turn.status !== "inProgress") {
8606
+ const turnState = turns.get(startedTurnId) ?? makeTurnState();
8607
+ turnState.completedTurn = turn;
8608
+ turns.set(startedTurnId, turnState);
8609
+ }
8610
+ finishActiveTurnIfReady();
8611
+ return await completed.promise;
8612
+ } catch (error) {
8613
+ if (activeTurn === waiter) activeTurn = void 0;
8614
+ throw error instanceof Error ? error : new Error(String(error));
8615
+ } finally {
8616
+ if (activeTurn === waiter) activeTurn = void 0;
8617
+ if (waiter.turnId !== void 0) turns.delete(waiter.turnId);
8618
+ }
8619
+ } finally {
8620
+ busy = false;
8621
+ }
8622
+ };
8623
+ const enqueue = async (message) => {
8624
+ if (message.trim() === "") throw new CodexAppServerError("Codex message must not be empty.");
8625
+ await connect$1();
8626
+ const currentThreadId = await ensureThread();
8627
+ const clientUserMessageId = randomUUID();
8628
+ const completion = createDeferred();
8629
+ const queuedMessage = {
8630
+ acknowledged: false,
8631
+ clientUserMessageId,
8632
+ completion
8633
+ };
8634
+ queuedMessagesByClientId.set(clientUserMessageId, queuedMessage);
8635
+ try {
8636
+ const result = await request("thread/queue/add", {
8637
+ clientUserMessageId,
8638
+ input: [{
8639
+ text: message,
8640
+ type: "text"
8641
+ }],
8642
+ threadId: currentThreadId
8643
+ });
8644
+ const queuedSubmission = isRecord(result) ? result.queuedSubmission : void 0;
8645
+ if (!isRecord(queuedSubmission) || typeof queuedSubmission.id !== "string" || queuedSubmission.clientUserMessageId !== clientUserMessageId) throw new CodexAppServerError("Codex App Server did not acknowledge the queued message with its client ID.");
8646
+ acknowledgeQueuedMessage(queuedMessage);
8647
+ return {
8648
+ completion: completion.promise,
8649
+ sessionId: currentThreadId
8650
+ };
8651
+ } catch (error) {
8652
+ queuedMessagesByClientId.delete(clientUserMessageId);
8653
+ if (queuedMessage.turnId !== void 0) queuedMessagesByTurnId.delete(queuedMessage.turnId);
8654
+ throw error instanceof Error ? error : new Error(String(error));
8655
+ }
8656
+ };
8657
+ const close = async () => {
8658
+ if (closePromise !== void 0) {
8659
+ await closePromise;
8660
+ return;
8661
+ }
8662
+ isClosed = true;
8663
+ isClosing = true;
8664
+ closePromise = (async () => {
8665
+ const currentSocket = socket;
8666
+ if (currentSocket === void 0 || currentSocket.readyState === WebSocket.CLOSED) {
8667
+ failConnection(makeTransportError("was closed by mdxr."));
8668
+ return;
8669
+ }
8670
+ const closed = once(currentSocket, "close");
8671
+ if (currentSocket.readyState === WebSocket.OPEN) currentSocket.close(1e3, "mdxr session closed");
8672
+ else currentSocket.terminate();
8673
+ await closed;
8674
+ failConnection(makeTransportError("was closed by mdxr."));
8675
+ })();
8676
+ await closePromise;
8677
+ };
8678
+ return {
8679
+ close,
8680
+ enqueue,
8681
+ send
8682
+ };
8683
+ };
8684
+ //#endregion
8685
+ //#region src/agent-session.ts
8686
+ const emptyStore = () => ({
8687
+ documents: {},
8688
+ version: 1
8689
+ });
8690
+ const isConversation = (value) => {
8691
+ if (typeof value !== "object" || value === null) return false;
8692
+ const item = value;
8693
+ return (item.provider === "codex" || item.provider === "claude") && (item.sessionId === void 0 || typeof item.sessionId === "string") && Array.isArray(item.messages) && item.messages.every((message) => (message.role === "user" || message.role === "assistant") && typeof message.content === "string");
8694
+ };
8695
+ const isSessionStore = (value) => {
8696
+ if (typeof value !== "object" || value === null) return false;
8697
+ const store = value;
8698
+ return store.version === 1 && typeof store.documents === "object" && store.documents !== null && Object.values(store.documents).every(isConversation);
8699
+ };
8700
+ const readStore = async (file) => {
8701
+ let raw;
8702
+ try {
8703
+ raw = await readFile(file, "utf-8");
8704
+ } catch (error) {
8705
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return emptyStore();
8706
+ throw error;
8707
+ }
8708
+ const parsed = JSON.parse(raw);
8709
+ if (!isSessionStore(parsed)) throw new Error("Invalid mdxr session store");
8710
+ return parsed;
8711
+ };
8712
+ const writeStore = async (file, store) => {
8713
+ await mkdir(path.dirname(file), { recursive: true });
8714
+ const temp = path.join(path.dirname(file), `.sessions-${randomUUID()}.tmp`);
8715
+ try {
8716
+ await writeFile(temp, `${JSON.stringify(store, null, 2)}\n`, { mode: 384 });
8717
+ await rename(temp, file);
8718
+ } catch (error) {
8719
+ const { rm } = await import("node:fs/promises");
8720
+ await rm(temp, { force: true });
8721
+ throw error;
8722
+ }
8723
+ };
8724
+ /** Keep one live provider connection for the lifetime of the MDX preview. */
8725
+ const createAgentSession = (filePath, provider, initialSessionId, serverEndpoint) => {
8726
+ const document = path.resolve(filePath);
8727
+ const cwd = process.cwd();
8728
+ const storePath = path.join(cwd, ".mdxr", "sessions.json");
8729
+ let busy = false;
8730
+ let lastError;
8731
+ let onUpdate;
8732
+ let connection;
8733
+ let claudeConversation = {
8734
+ messages: [],
8735
+ provider
8736
+ };
8737
+ const savedConversation = async () => {
8738
+ if (provider === "claude") return claudeConversation;
8739
+ const saved = (await readStore(storePath)).documents[document];
8740
+ if (saved?.provider === provider && (initialSessionId === void 0 || saved.sessionId === initialSessionId)) return saved;
8741
+ return {
8742
+ messages: [],
8743
+ provider,
8744
+ sessionId: initialSessionId
8745
+ };
8746
+ };
8747
+ const current = async () => ({
8748
+ ...await savedConversation(),
8749
+ busy,
8750
+ error: lastError
8751
+ });
8752
+ const saveConversation = async (conversation) => {
8753
+ if (provider === "claude") {
8754
+ claudeConversation = conversation;
8755
+ return;
8756
+ }
8757
+ const store = await readStore(storePath);
8758
+ store.documents[document] = conversation;
8759
+ await writeStore(storePath, store);
8760
+ };
8761
+ const send = async (message) => {
8762
+ if (busy) throw new Error("Agent is already responding");
8763
+ busy = true;
8764
+ lastError = void 0;
8765
+ let awaitingQueuedAnswer = false;
8766
+ try {
8767
+ const conversation = await savedConversation();
8768
+ connection ??= provider === "codex" ? createCodexAppServerSession({
8769
+ cwd,
8770
+ endpoint: serverEndpoint,
8771
+ initialSessionId: conversation.sessionId
8772
+ }) : createClaudeStreamSession(cwd);
8773
+ const prompt = conversation.sessionId === void 0 ? `This message was sent from the preview of ${document}.\n\n${message}` : message;
8774
+ if (provider === "codex" && conversation.sessionId !== void 0 && connection.enqueue !== void 0) {
8775
+ const queued = await connection.enqueue(prompt);
8776
+ const updated = {
8777
+ messages: [...conversation.messages, {
8778
+ content: message,
8779
+ role: "user"
8780
+ }],
8781
+ provider,
8782
+ sessionId: queued.sessionId
8783
+ };
8784
+ await saveConversation(updated);
8785
+ (async () => {
8786
+ try {
8787
+ const answer = await queued.completion;
8788
+ await saveConversation({
8789
+ ...updated,
8790
+ messages: [...updated.messages, {
8791
+ content: answer,
8792
+ role: "assistant"
8793
+ }]
8794
+ });
8795
+ } catch (error) {
8796
+ lastError = error instanceof Error ? error.message : String(error);
8797
+ } finally {
8798
+ busy = false;
8799
+ onUpdate?.();
8800
+ }
8801
+ })();
8802
+ awaitingQueuedAnswer = true;
8803
+ return {
8804
+ ...updated,
8805
+ busy: true
8806
+ };
8807
+ }
8808
+ const response = await connection.send(prompt);
8809
+ const updated = {
8810
+ messages: [
8811
+ ...conversation.messages,
8812
+ {
8813
+ content: message,
8814
+ role: "user"
8815
+ },
8816
+ {
8817
+ content: response.answer,
8818
+ role: "assistant"
8819
+ }
8820
+ ],
8821
+ provider,
8822
+ sessionId: response.sessionId
8823
+ };
8824
+ await saveConversation(updated);
8825
+ return {
8826
+ ...updated,
8827
+ busy: false
8828
+ };
8829
+ } finally {
8830
+ if (!awaitingQueuedAnswer) busy = false;
8831
+ }
8832
+ };
8833
+ const close = async () => {
8834
+ await connection?.close();
8835
+ };
8836
+ const setOnUpdate = (callback) => {
8837
+ onUpdate = callback;
8838
+ };
8839
+ return {
8840
+ close,
8841
+ current,
8842
+ send,
8843
+ setOnUpdate
8844
+ };
8845
+ };
8846
+ //#endregion
8847
+ //#region src/document-history.ts
8848
+ var DocumentHistoryError = class extends Error {
8849
+ kind;
8850
+ constructor(kind, message) {
8851
+ super(message);
8852
+ this.name = "DocumentHistoryError";
8853
+ this.kind = kind;
8854
+ }
8855
+ };
8856
+ const HASH_PATTERN = /^[\da-f]{64}$/u;
8857
+ const EVENT_PATTERN = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu;
8858
+ const LOCK_STALE_AFTER_MS = 6e5;
8859
+ const LOCK_RETRY_MS = 20;
8860
+ const MAX_LOCK_WAIT_MS = 3e4;
8861
+ const MAX_STABLE_READS = 6;
8862
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
8863
+ const isNodeError = (error) => error instanceof Error && "code" in error;
8864
+ const sameFileState = (left, right) => left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
8865
+ const readStableFile = async (filePath) => {
8866
+ for (let attempt = 0; attempt < MAX_STABLE_READS; attempt += 1) {
8867
+ try {
8868
+ const before = await fsp.stat(filePath, { bigint: true });
8869
+ const content = await fsp.readFile(filePath);
8870
+ const after = await fsp.stat(filePath, { bigint: true });
8871
+ if (sameFileState(before, after) && BigInt(content.byteLength) === after.size) return content;
8872
+ } catch (error) {
8873
+ if (!isNodeError(error) || error.code !== "ENOENT" && error.code !== "EBUSY" || attempt === 5) throw error;
8874
+ }
8875
+ await setTimeout$1(12 * (attempt + 1));
8876
+ }
8877
+ throw new Error(`Could not read a stable MDX version: ${filePath}`);
8878
+ };
8879
+ /** Write a file through a temporary name, then link it without replacing an
8880
+ * existing immutable file. This is safe when two preview processes race. */
8881
+ const writeImmutable = async (filePath, content) => {
8882
+ const temporary = `${filePath}.${randomUUID()}.tmp`;
8883
+ try {
8884
+ await fsp.writeFile(temporary, content, { flag: "wx" });
8885
+ try {
8886
+ await fsp.link(temporary, filePath);
8887
+ } catch (error) {
8888
+ if (!isNodeError(error) || error.code !== "EEXIST") throw error;
8889
+ }
8890
+ } finally {
8891
+ await fsp.rm(temporary, { force: true });
8892
+ }
8893
+ };
8894
+ const parseVersionKind = (value) => value === "initial" || value === "before-instruction" || value === "change";
8895
+ const readVersionEvent = async (eventPath, expectedId) => {
8896
+ let raw;
8897
+ try {
8898
+ raw = JSON.parse(await fsp.readFile(eventPath, "utf-8"));
8899
+ } catch (error) {
8900
+ if (isNodeError(error) && error.code === "ENOENT") throw error;
8901
+ throw new DocumentHistoryError("corrupt-version", `Could not read document history event: ${expectedId}: ${String(error)}`);
8902
+ }
8903
+ if (typeof raw !== "object" || raw === null || !("id" in raw) || raw.id !== expectedId || !("contentHash" in raw) || typeof raw.contentHash !== "string" || !HASH_PATTERN.test(raw.contentHash) || !("kind" in raw) || !parseVersionKind(raw.kind) || !("createdAt" in raw) || typeof raw.createdAt !== "string" || Number.isNaN(Date.parse(raw.createdAt)) || !("size" in raw) || typeof raw.size !== "number" || !Number.isSafeInteger(raw.size) || raw.size < 0 || !("sequence" in raw) || typeof raw.sequence !== "number" || !Number.isSafeInteger(raw.sequence) || raw.sequence < 1) throw new DocumentHistoryError("corrupt-version", `Invalid document history event: ${expectedId}`);
8904
+ return {
8905
+ contentHash: raw.contentHash,
8906
+ createdAt: raw.createdAt,
8907
+ id: raw.id,
8908
+ kind: raw.kind,
8909
+ sequence: raw.sequence,
8910
+ size: raw.size
8911
+ };
8912
+ };
8913
+ const acquireLock = async (lockPath) => {
8914
+ const startedAt = Date.now();
8915
+ while (Date.now() - startedAt < MAX_LOCK_WAIT_MS) try {
8916
+ const handle = await fsp.open(lockPath, "wx", 384);
8917
+ await handle.writeFile(`${process.pid}\n${Date.now()}\n`);
8918
+ return async () => {
8919
+ await handle.close();
8920
+ await fsp.rm(lockPath, { force: true });
8921
+ };
8922
+ } catch (error) {
8923
+ if (!isNodeError(error) || error.code !== "EEXIST") throw error;
8924
+ try {
8925
+ const stat = await fsp.stat(lockPath);
8926
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_AFTER_MS) {
8927
+ await fsp.rm(lockPath, { force: true });
8928
+ continue;
8929
+ }
8930
+ } catch (statError) {
8931
+ if (!isNodeError(statError) || statError.code !== "ENOENT") throw statError;
8932
+ }
8933
+ await setTimeout$1(LOCK_RETRY_MS);
8934
+ }
8935
+ throw new Error("Timed out waiting for the MDX history lock");
8936
+ };
8937
+ const lines = (source) => source.replaceAll("\r\n", "\n").split("\n");
8938
+ /** Myers shortest edit script with O((N + M)D) time and trace backtracking. */
8939
+ const diffLines = (before, after) => {
8940
+ const left = lines(before);
8941
+ const right = lines(after);
8942
+ const max = left.length + right.length;
8943
+ const trace = [];
8944
+ const frontier = /* @__PURE__ */ new Map([[1, 0]]);
8945
+ let found = false;
8946
+ for (let distance = 0; distance <= max; distance += 1) {
8947
+ trace.push(new Map(frontier));
8948
+ for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
8949
+ let x = diagonal === -distance || diagonal !== distance && (frontier.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY) < (frontier.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY) ? frontier.get(diagonal + 1) ?? 0 : (frontier.get(diagonal - 1) ?? 0) + 1;
8950
+ let y = x - diagonal;
8951
+ while (x < left.length && y < right.length && left[x] === right[y]) {
8952
+ x += 1;
8953
+ y += 1;
8954
+ }
8955
+ frontier.set(diagonal, x);
8956
+ if (x >= left.length && y >= right.length) {
8957
+ found = true;
8958
+ break;
8959
+ }
8960
+ }
8961
+ if (found) break;
8962
+ }
8963
+ let x = left.length;
8964
+ let y = right.length;
8965
+ const reversed = [];
8966
+ for (let distance = trace.length - 1; distance >= 0; distance -= 1) {
8967
+ const previous = trace[distance];
8968
+ if (previous === void 0) continue;
8969
+ const diagonal = x - y;
8970
+ const previousDiagonal = diagonal === -distance || diagonal !== distance && (previous.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY) < (previous.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY) ? diagonal + 1 : diagonal - 1;
8971
+ const previousX = previous.get(previousDiagonal) ?? 0;
8972
+ const previousY = previousX - previousDiagonal;
8973
+ while (x > previousX && y > previousY) {
8974
+ reversed.push({
8975
+ text: left[x - 1] ?? "",
8976
+ type: "context"
8977
+ });
8978
+ x -= 1;
8979
+ y -= 1;
8980
+ }
8981
+ if (distance === 0) break;
8982
+ if (x === previousX) {
8983
+ reversed.push({
8984
+ text: right[y - 1] ?? "",
8985
+ type: "add"
8986
+ });
8987
+ y -= 1;
8988
+ } else {
8989
+ reversed.push({
8990
+ text: left[x - 1] ?? "",
8991
+ type: "remove"
8992
+ });
8993
+ x -= 1;
8994
+ }
8995
+ }
8996
+ return reversed.toReversed();
8997
+ };
8998
+ const eventCompare = (left, right) => left.sequence - right.sequence;
8999
+ /** Local, token-free snapshots and line diffs for one MDX source file. */
9000
+ const createDocumentHistory = (filePath, rootDir = process.cwd()) => {
9001
+ const absoluteFilePath = path.resolve(filePath);
9002
+ const historyPath = path.join(path.resolve(rootDir), ".mdxr", "history", sha256(absoluteFilePath));
9003
+ const versionsPath = path.join(historyPath, "versions");
9004
+ const eventsPath = path.join(historyPath, "events");
9005
+ const lockPath = path.join(historyPath, ".capture.lock");
9006
+ const listUnlocked = async () => {
9007
+ await fsp.mkdir(eventsPath, { recursive: true });
9008
+ const eventEntries = (await fsp.readdir(eventsPath, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && EVENT_PATTERN.test(entry.name.replace(/\.json$/u, "")));
9009
+ const versions = await Promise.all(eventEntries.map(async (entry) => {
9010
+ const eventId = entry.name.slice(0, -5);
9011
+ const version = await readVersionEvent(path.join(eventsPath, entry.name), eventId);
9012
+ return {
9013
+ contentHash: version.contentHash,
9014
+ createdAt: version.createdAt,
9015
+ id: version.id,
9016
+ kind: version.kind,
9017
+ sequence: version.sequence,
9018
+ size: version.size
9019
+ };
9020
+ }));
9021
+ versions.sort(eventCompare);
9022
+ return {
9023
+ latestId: versions.at(-1)?.id ?? "",
9024
+ versions
9025
+ };
9026
+ };
9027
+ const findVersion = async (id) => {
9028
+ if (!EVENT_PATTERN.test(id)) throw new DocumentHistoryError("invalid-version", `Invalid document history version ID: ${id}`);
9029
+ const eventPath = path.join(eventsPath, `${id}.json`);
9030
+ try {
9031
+ return await readVersionEvent(eventPath, id);
9032
+ } catch (error) {
9033
+ if (isNodeError(error) && error.code === "ENOENT") throw new DocumentHistoryError("unknown-version", `Unknown document history version: ${id}`);
9034
+ throw error;
9035
+ }
9036
+ };
9037
+ return {
9038
+ capture: async (kind) => {
9039
+ await fsp.mkdir(versionsPath, { recursive: true });
9040
+ const release = await acquireLock(lockPath);
9041
+ try {
9042
+ const content = await readStableFile(absoluteFilePath);
9043
+ const contentHash = sha256(content);
9044
+ const latest = (await listUnlocked()).versions.at(-1);
9045
+ if (kind !== "before-instruction" && latest?.contentHash === contentHash) return latest;
9046
+ const blobPath = path.join(versionsPath, `${contentHash}.mdx`);
9047
+ await writeImmutable(blobPath, content);
9048
+ const version = {
9049
+ contentHash,
9050
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9051
+ id: randomUUID(),
9052
+ kind,
9053
+ sequence: (latest?.sequence ?? 0) + 1,
9054
+ size: content.byteLength
9055
+ };
9056
+ const eventPath = path.join(eventsPath, `${version.id}.json`);
9057
+ await writeImmutable(eventPath, JSON.stringify(version));
9058
+ return version;
9059
+ } finally {
9060
+ await release();
9061
+ }
9062
+ },
9063
+ diff: async (fromId, toId) => {
9064
+ const [from, to] = await Promise.all([findVersion(fromId), findVersion(toId)]);
9065
+ const [before, after] = await Promise.all([fsp.readFile(path.join(versionsPath, `${from.contentHash}.mdx`)), fsp.readFile(path.join(versionsPath, `${to.contentHash}.mdx`))]);
9066
+ if (sha256(before) !== from.contentHash) throw new DocumentHistoryError("corrupt-version", `Corrupt document history version: ${fromId}`);
9067
+ if (sha256(after) !== to.contentHash) throw new DocumentHistoryError("corrupt-version", `Corrupt document history version: ${toId}`);
9068
+ return { lines: diffLines(before.toString("utf-8"), after.toString("utf-8")) };
9069
+ },
9070
+ list: async () => await listUnlocked(),
9071
+ read: async (id) => {
9072
+ const version = await findVersion(id);
9073
+ const content = await fsp.readFile(path.join(versionsPath, `${version.contentHash}.mdx`));
9074
+ if (sha256(content) !== version.contentHash) throw new DocumentHistoryError("corrupt-version", `Corrupt document history version: ${id}`);
9075
+ return content.toString("utf-8");
9076
+ }
9077
+ };
9078
+ };
9079
+ //#endregion
9080
+ //#region src/document-history-http.ts
9081
+ const replyJson = (response, status, body) => {
9082
+ response.writeHead(status, {
9083
+ "cache-control": "no-store",
9084
+ "content-type": "application/json; charset=utf-8",
9085
+ "x-content-type-options": "nosniff"
9086
+ });
9087
+ response.end(JSON.stringify(body));
9088
+ };
9089
+ const sameOrigin = (request) => {
9090
+ const port = request.socket.localPort;
9091
+ const host = request.headers.host ?? "";
9092
+ return (host === `localhost:${port}` || host === `127.0.0.1:${port}`) && (request.headers.origin === void 0 || request.headers.origin === `http://${host}`);
9093
+ };
9094
+ /** Read-only endpoints for the local, token-free MDX version store. */
9095
+ const handleDocumentHistoryRequest = async (request, response, history, filePath) => {
9096
+ if (history === void 0 || filePath === void 0) {
9097
+ replyJson(response, 404, { error: "Document history is unavailable" });
9098
+ return;
9099
+ }
9100
+ if (!sameOrigin(request)) {
9101
+ replyJson(response, 403, { error: "Request origin is not allowed" });
9102
+ return;
9103
+ }
9104
+ if (request.method !== "GET") {
9105
+ replyJson(response, 405, { error: "Method not allowed" });
9106
+ return;
9107
+ }
9108
+ const url = new URL(request.url ?? "", "http://localhost");
9109
+ try {
9110
+ const view = url.searchParams.get("view");
9111
+ if (view === null) {
9112
+ replyJson(response, 200, await history.list());
9113
+ return;
9114
+ }
9115
+ const id = url.searchParams.get("id");
9116
+ if (view === "source" && id !== null) {
9117
+ const source = await history.read(id);
9118
+ const syntax = await highlightMdxSource(source);
9119
+ replyJson(response, 200, {
9120
+ id,
9121
+ source,
9122
+ syntax
9123
+ });
9124
+ return;
9125
+ }
9126
+ if (view === "diff") {
9127
+ const from = url.searchParams.get("from");
9128
+ const to = url.searchParams.get("to");
9129
+ if (from !== null && to !== null) {
9130
+ const { lines } = await history.diff(from, to);
9131
+ const beforeSource = lines.filter((line) => line.type !== "add").map((line) => line.text).join("\n");
9132
+ const afterSource = lines.filter((line) => line.type !== "remove").map((line) => line.text).join("\n");
9133
+ const [before, after] = await Promise.all([highlightMdxSource(beforeSource), highlightMdxSource(afterSource)]);
9134
+ replyJson(response, 200, {
9135
+ from,
9136
+ lines,
9137
+ syntax: {
9138
+ after,
9139
+ before
9140
+ },
9141
+ to
9142
+ });
9143
+ return;
9144
+ }
9145
+ }
9146
+ if (view === "preview" && id !== null) {
9147
+ const source = await history.read(id);
9148
+ const html = await render(source, {
9149
+ dir: path.dirname(filePath),
9150
+ filePath,
9151
+ liveReload: false
9152
+ });
9153
+ response.writeHead(200, {
9154
+ "cache-control": "no-store",
9155
+ "content-type": "text/html; charset=utf-8",
9156
+ "x-content-type-options": "nosniff"
9157
+ });
9158
+ response.end(html);
9159
+ return;
9160
+ }
9161
+ replyJson(response, 400, { error: "Invalid history view or version" });
9162
+ } catch (error) {
9163
+ const detail = formatError(error);
9164
+ const missingVersion = error instanceof DocumentHistoryError && (error.kind === "invalid-version" || error.kind === "unknown-version");
9165
+ replyJson(response, missingVersion ? 404 : 500, { error: detail });
9166
+ }
9167
+ };
9168
+ //#endregion
9169
+ //#region src/workspace-js.ts
9170
+ let cached;
9171
+ const buildWorkspaceJs = async () => {
9172
+ return (await build({
9173
+ absWorkingDir: pkgRoot,
9174
+ bundle: true,
9175
+ define: { "process.env.NODE_ENV": "\"production\"" },
9176
+ entryPoints: [path.join(srcDir, "workspace-app.tsx")],
9177
+ format: "iife",
9178
+ jsx: "automatic",
9179
+ jsxImportSource: "react",
9180
+ logLevel: "silent",
9181
+ minify: true,
9182
+ platform: "browser",
9183
+ target: "es2022",
9184
+ write: false
9185
+ })).outputFiles[0]?.text ?? "";
9186
+ };
9187
+ /** Browser-only React workspace used while serving a file. */
9188
+ const workspaceJs = async () => {
9189
+ cached ??= buildWorkspaceJs();
9190
+ try {
9191
+ return await cached;
9192
+ } catch (error) {
9193
+ cached = void 0;
9194
+ throw error;
9195
+ }
9196
+ };
9197
+ //#endregion
7743
9198
  //#region src/serve.ts
7744
9199
  const errorPage = (err) => `<!doctype html><meta charset="utf-8"><body style="font-family:monospace;background:#1c1917;color:#fca5a5;padding:2rem"><h1>mdxr render error</h1><pre>${formatError(err).replaceAll("&", "&amp;").replaceAll("<", "&lt;")}</pre></body>`;
7745
9200
  /** Dependency/build output dirs — never worth a watch fd. */
@@ -7785,7 +9240,7 @@ const createWatchSet = () => {
7785
9240
  * picked up on the next rebuild (armDeps re-arms the tree for this).
7786
9241
  */
7787
9242
  const arm = (dir, onEvent) => {
7788
- if (SKIP_DIRS.has(path.basename(dir))) return;
9243
+ if (SKIP_DIRS.has(path.basename(dir)) || path.basename(dir) === "history" && path.basename(path.dirname(dir)) === ".mdxr") return;
7789
9244
  if (!armed.has(dir)) try {
7790
9245
  track(fs.watch(dir, onEvent), dir);
7791
9246
  } catch {}
@@ -7812,8 +9267,49 @@ const createWatchSet = () => {
7812
9267
  const servePreview = async (target, port) => {
7813
9268
  let html = "";
7814
9269
  let closed = false;
9270
+ let capturedInitialVersion = false;
7815
9271
  const clients = /* @__PURE__ */ new Set();
9272
+ const notifyAgent = () => {
9273
+ for (const client of clients) try {
9274
+ client.write("event: agent\ndata: {}\n\n");
9275
+ } catch {
9276
+ clients.delete(client);
9277
+ }
9278
+ };
9279
+ target.agent?.setOnUpdate(notifyAgent);
7816
9280
  const server = http.createServer((req, res) => {
9281
+ if (req.url === "/__mdxr_workspace.js") {
9282
+ if (target.history === void 0) {
9283
+ res.writeHead(404);
9284
+ res.end();
9285
+ return;
9286
+ }
9287
+ (async () => {
9288
+ try {
9289
+ const script = await workspaceJs();
9290
+ res.writeHead(200, {
9291
+ "cache-control": "no-store",
9292
+ "content-type": "text/javascript; charset=utf-8",
9293
+ "x-content-type-options": "nosniff"
9294
+ });
9295
+ res.end(script);
9296
+ } catch (error) {
9297
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
9298
+ res.end(formatError(error));
9299
+ }
9300
+ })();
9301
+ return;
9302
+ }
9303
+ if (req.url === "/__mdxr_agent") {
9304
+ handleAgentRequest(req, res, target.agent, notifyAgent, async () => {
9305
+ await target.history?.capture("before-instruction");
9306
+ });
9307
+ return;
9308
+ }
9309
+ if (req.url?.startsWith("/__mdxr_history") === true) {
9310
+ handleDocumentHistoryRequest(req, res, target.history, target.historyFile);
9311
+ return;
9312
+ }
7817
9313
  if (req.url === "/__mdxr_events") {
7818
9314
  res.writeHead(200, {
7819
9315
  "cache-control": "no-cache",
@@ -7853,7 +9349,12 @@ const servePreview = async (target, port) => {
7853
9349
  };
7854
9350
  const rebuild = async (onEvent) => {
7855
9351
  try {
7856
- html = await target.renderDoc();
9352
+ if (target.history !== void 0) {
9353
+ await target.history.capture(capturedInitialVersion ? "change" : "initial");
9354
+ capturedInitialVersion = true;
9355
+ }
9356
+ const rendered = await target.renderDoc();
9357
+ html = target.history === void 0 ? rendered : injectAgentPreview(rendered, target.agentProvider);
7857
9358
  } catch (error) {
7858
9359
  html = errorPage(error);
7859
9360
  }
@@ -7876,7 +9377,10 @@ const servePreview = async (target, port) => {
7876
9377
  })();
7877
9378
  };
7878
9379
  const notify = (_event, filename) => {
7879
- if (closed || filename?.split(path.sep).some((part) => SKIP_DIRS.has(part)) === true) return;
9380
+ const parts = filename?.split(path.sep) ?? [];
9381
+ const isHistoryWrite = parts.includes("history") && (parts.includes(".mdxr") || path.basename(target.watchDir) === ".mdxr");
9382
+ const isInternalMdxrWrite = path.basename(target.watchDir) !== ".mdxr" && parts.includes(".mdxr");
9383
+ if (closed || parts.some((part) => SKIP_DIRS.has(part)) || isHistoryWrite || isInternalMdxrWrite || parts.some((part) => part === "sessions.json" || /^\.sessions-.*\.tmp$/u.test(part))) return;
7880
9384
  clearTimeout(timer);
7881
9385
  timer = setTimeout(() => {
7882
9386
  reload(notify);
@@ -7891,10 +9395,16 @@ const servePreview = async (target, port) => {
7891
9395
  if (watch.size === 0 && target.watchFile !== void 0) try {
7892
9396
  watch.track(fs.watch(target.watchFile, notify));
7893
9397
  } catch {}
9398
+ const closeAgent = async () => {
9399
+ try {
9400
+ await target.agent?.close();
9401
+ } catch {}
9402
+ };
7894
9403
  server.on("close", () => {
7895
9404
  closed = true;
7896
9405
  clearTimeout(timer);
7897
9406
  watch.close();
9407
+ closeAgent();
7898
9408
  });
7899
9409
  reloading = rebuild(notify);
7900
9410
  await reloading;
@@ -7938,8 +9448,16 @@ const trackDeps = (renderDoc) => {
7938
9448
  };
7939
9449
  /** Serve an .mdx file, rebuilding + live-reloading on changes in its directory. */
7940
9450
  const serve = async (mdxPath, port, opts = {}) => {
9451
+ if (opts.session !== void 0 && opts.agent === void 0) throw new Error("--session requires --agent");
9452
+ if (opts.session !== void 0 && opts.agent !== "codex") throw new Error("--session is only supported for codex");
9453
+ if (opts.server !== void 0 && opts.agent !== "codex") throw new Error("--server requires --agent codex");
7941
9454
  const abs = path.resolve(mdxPath);
9455
+ const history = createDocumentHistory(abs);
7942
9456
  const server = await servePreview({
9457
+ agent: opts.agent === void 0 ? void 0 : createAgentSession(abs, opts.agent, opts.session, opts.server),
9458
+ agentProvider: opts.agent,
9459
+ history,
9460
+ historyFile: abs,
7943
9461
  label: mdxPath,
7944
9462
  ...trackDeps(async (onDeps) => await renderFile(abs, {
7945
9463
  liveReload: true,
@@ -7953,6 +9471,7 @@ const serve = async (mdxPath, port, opts = {}) => {
7953
9471
  };
7954
9472
  /** Serve MDX source passed directly (e.g. piped via stdin). */
7955
9473
  const serveSource = async (source, port, opts = {}) => {
9474
+ if (opts.agent !== void 0 || opts.session !== void 0 || opts.server !== void 0) throw new Error("Agent chat requires an MDX file");
7956
9475
  const dir = path.resolve(opts.dir ?? process.cwd());
7957
9476
  const server = await servePreview({
7958
9477
  label: opts.filePath ?? "stdin",
@@ -7998,6 +9517,14 @@ const fail = (err, json) => {
7998
9517
  else console.error(`mdxr: error: ${formatError(err)}`);
7999
9518
  process.exit(1);
8000
9519
  };
9520
+ const validateAgentOptions = (agent, session, server) => {
9521
+ if (agent !== void 0 && agent !== "codex" && agent !== "claude") throw new Error(`invalid --agent: ${agent} (expected codex or claude)`);
9522
+ if (session !== void 0 && agent === void 0) throw new Error("--session requires --agent");
9523
+ if (session !== void 0 && agent !== "codex") throw new Error("--session is only supported for codex");
9524
+ if (server !== void 0 && agent !== "codex") throw new Error("--server requires --agent codex");
9525
+ if (session !== void 0 && session.trim() === "") throw new Error("--session requires a non-empty ID");
9526
+ return agent;
9527
+ };
8001
9528
  const cli = cac("mdxr");
8002
9529
  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) => {
8003
9530
  const json = opts.format === "json";
@@ -8027,12 +9554,14 @@ cli.command("render [file]", "Render an .mdx document to a standalone HTML file"
8027
9554
  fail(error, json);
8028
9555
  }
8029
9556
  });
8030
- cli.command("serve [file]", "Preview a document in the browser with live reload").option("-p, --port <port>", "Port", { default: 3737 }).option("--open", "Open the preview in the default browser once serving").action(async (file, opts) => {
9557
+ cli.command("serve [file]", "Preview a document in the browser with live reload").option("-p, --port <port>", "Port", { default: 3737 }).option("--open", "Open the preview in the default browser once serving").option("--agent <agent>", "Chat with codex or claude in the preview").option("--session <id>", "Send to an existing Codex thread").option("--server <url>", "Codex App Server ws:// or unix:// endpoint").action(async (file, opts) => {
8031
9558
  try {
8032
9559
  const port = Number(opts.port);
8033
9560
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`invalid --port: ${opts.port}`);
8034
9561
  const open = opts.open === true;
9562
+ const agent = validateAgentOptions(opts.agent, opts.session, opts.server);
8035
9563
  if (file === void 0 || file === "-") {
9564
+ if (agent !== void 0) throw new Error("--agent requires an MDX file");
8036
9565
  requireStdinSource();
8037
9566
  await serveSource(await readStdin(), port, {
8038
9567
  dir: process.cwd(),
@@ -8041,7 +9570,12 @@ cli.command("serve [file]", "Preview a document in the browser with live reload"
8041
9570
  });
8042
9571
  return;
8043
9572
  }
8044
- await serve(file, port, { open });
9573
+ await serve(file, port, {
9574
+ agent,
9575
+ open,
9576
+ server: opts.server,
9577
+ session: opts.session
9578
+ });
8045
9579
  } catch (error) {
8046
9580
  fail(error, false);
8047
9581
  }