@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 +1569 -35
- package/dist/components.d.mts +1 -1
- package/package.json +5 -2
- package/src/agent-http.ts +131 -0
- package/src/agent-markdown.ts +90 -0
- package/src/agent-preview.ts +23 -0
- package/src/agent-session.ts +238 -0
- package/src/ai-elements/LICENSE +201 -0
- package/src/ai-elements/NOTICE.md +7 -0
- package/src/ai-elements/conversation.tsx +195 -0
- package/src/ai-elements/message.tsx +92 -0
- package/src/ai-elements/prompt-input.tsx +260 -0
- package/src/annotation-html.ts +8 -1
- package/src/annotations.ts +55 -4
- package/src/assets/annotations.css +139 -0
- package/src/assets/workspace.css +1098 -0
- package/src/claude-stream-session.ts +267 -0
- package/src/cli.ts +48 -2
- package/src/client/annotation-view.ts +140 -2
- package/src/client/annotations.ts +168 -17
- package/src/codex-app-server.ts +926 -0
- package/src/document-history-http.ts +118 -0
- package/src/document-history.ts +455 -0
- package/src/rehype/shiki.ts +39 -0
- package/src/serve.ts +126 -4
- package/src/workspace-app.tsx +787 -0
- package/src/workspace-chat.tsx +256 -0
- package/src/workspace-code.tsx +117 -0
- package/src/workspace-diff-model.ts +471 -0
- package/src/workspace-diff.tsx +184 -0
- package/src/workspace-js.ts +36 -0
- package/src/workspace-syntax.ts +66 -0
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
|
-
<
|
|
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
|
"\"": """,
|
|
5864
5875
|
"&": "&",
|
|
5865
5876
|
"'": "'",
|
|
5866
5877
|
"<": "<",
|
|
5867
5878
|
">": ">"
|
|
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
|
+
"\"": """,
|
|
7823
|
+
"&": "&",
|
|
7824
|
+
"'": "'",
|
|
7825
|
+
"<": "<",
|
|
7826
|
+
">": ">"
|
|
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("&", "&").replaceAll("<", "<")}</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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
}
|