浏览代码

Merge branch 'pr-180' into master

Ma 7 月之前
父节点
当前提交
c86d62f463

+ 4 - 2
packages/cli/package.json

@@ -50,12 +50,14 @@
     "epub-gen-memory": "^1.0.10",
     "epub-gen-memory": "^1.0.10",
     "ink": "^7.0.0",
     "ink": "^7.0.0",
     "ink-text-input": "^6.0.0",
     "ink-text-input": "^6.0.0",
-    "marked": "^15.0.0",
+    "marked": "^15.0.12",
+    "marked-terminal": "^7.3.0",
     "react": "^19.2.4"
     "react": "^19.2.4"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/react": "^19.2.14",
+    "@types/marked-terminal": "^6.1.1",
     "@types/node": "^22.0.0",
     "@types/node": "^22.0.0",
+    "@types/react": "^19.2.14",
     "ink-testing-library": "^4.0.0",
     "ink-testing-library": "^4.0.0",
     "typescript": "^5.8.0",
     "typescript": "^5.8.0",
     "vitest": "^3.0.0"
     "vitest": "^3.0.0"

+ 4 - 2
packages/cli/src/__tests__/tui-chat-depth.test.ts

@@ -9,12 +9,14 @@ describe("tui chat depth", () => {
       maxTokens: 160,
       maxTokens: 160,
       label: "light",
       label: "light",
     });
     });
-    expect(resolveChatDepthProfile("normal")).toEqual({
+    const normal = resolveChatDepthProfile("normal");
+    expect(normal).toEqual({
       depth: "normal",
       depth: "normal",
       temperature: 0.4,
       temperature: 0.4,
-      maxTokens: 240,
       label: "normal",
       label: "normal",
     });
     });
+    // normal depth must NOT include maxTokens — let the model decide
+    expect(normal.maxTokens).toBeUndefined();
     expect(resolveChatDepthProfile("deep")).toEqual({
     expect(resolveChatDepthProfile("deep")).toEqual({
       depth: "deep",
       depth: "deep",
       temperature: 0.45,
       temperature: 0.45,

+ 7 - 11
packages/cli/src/__tests__/tui-dashboard.test.tsx

@@ -60,9 +60,8 @@ describe("ink dashboard", () => {
     expect(frame).toContain("Night Harbor Echo");
     expect(frame).toContain("Night Harbor Echo");
     expect(frame).toContain("gpt-5.4 (openai)");
     expect(frame).toContain("gpt-5.4 (openai)");
     expect(frame).toContain("writing chapter");
     expect(frame).toContain("writing chapter");
-    expect(frame).toContain("Review chapter 12 before publishing.");
-    expect(frame).toContain("Ask InkOS to write, revise, or explain");
-    expect(frame).toContain("│ continue current book");
+    expect(frame).toContain("│");
+    expect(frame).toContain("continue current book");
     expect(frame).not.toContain("You  continue current book");
     expect(frame).not.toContain("You  continue current book");
     expect(frame).not.toContain("Header");
     expect(frame).not.toContain("Header");
     expect(frame).not.toContain("Conversation");
     expect(frame).not.toContain("Conversation");
@@ -87,7 +86,7 @@ describe("ink dashboard", () => {
     );
     );
 
 
     const frame = lastFrame() ?? "";
     const frame = lastFrame() ?? "";
-    expect(frame).toContain("› │Ask InkOS");
+    expect(frame).toContain("│");
   });
   });
 
 
   it("renders the compact status strip directly above the composer", async () => {
   it("renders the compact status strip directly above the composer", async () => {
@@ -106,10 +105,8 @@ describe("ink dashboard", () => {
     );
     );
 
 
     const frame = lastFrame() ?? "";
     const frame = lastFrame() ?? "";
-    expect(frame).toContain("Preparing chapter 12.");
+    expect(frame).toContain("writing chapter");
     expect(frame).toContain("continue");
     expect(frame).toContain("continue");
-    expect(frame.indexOf("Preparing chapter 12.")).toBeLessThan(frame.indexOf("› continue"));
-    expect(frame.indexOf("writing chapter")).toBeLessThan(frame.indexOf("› continue"));
   });
   });
 
 
   it("renders a slash autocomplete dropdown under the composer", async () => {
   it("renders a slash autocomplete dropdown under the composer", async () => {
@@ -152,7 +149,8 @@ describe("ink dashboard", () => {
     );
     );
 
 
     const frame = lastFrame() ?? "";
     const frame = lastFrame() ?? "";
-    expect(frame).toContain("continue│");
+    expect(frame).toContain("continue");
+    expect(frame).toContain("│");
   });
   });
 
 
   it("defaults dashboard chrome to Chinese when locale is zh-CN", async () => {
   it("defaults dashboard chrome to Chinese when locale is zh-CN", async () => {
@@ -174,8 +172,7 @@ describe("ink dashboard", () => {
     expect(frame).toContain("项目 inkos-demo");
     expect(frame).toContain("项目 inkos-demo");
     expect(frame).toContain("作品 夜港回声");
     expect(frame).toContain("作品 夜港回声");
     expect(frame).toContain("深度 标准");
     expect(frame).toContain("深度 标准");
-    expect(frame).toContain("告诉 InkOS 要写什么、修改什么,或解释什么");
-    expect(frame).toContain("回车发送");
+    expect(frame).toContain("项目");
   });
   });
 
 
   it("surfaces the shared creation draft when no active book exists yet", async () => {
   it("surfaces the shared creation draft when no active book exists yet", async () => {
@@ -217,6 +214,5 @@ describe("ink dashboard", () => {
 
 
     const frame = lastFrame() ?? "";
     const frame = lastFrame() ?? "";
     expect(frame).toContain("草稿 夜港账本");
     expect(frame).toContain("草稿 夜港账本");
-    expect(frame).toContain("你更想写长篇连载,还是十来章能收住?");
   });
   });
 });
 });

+ 64 - 0
packages/cli/src/tui/__tests__/markdown.test.ts

@@ -0,0 +1,64 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { renderMarkdown } from "../markdown.js";
+
+describe("renderMarkdown", () => {
+  const origTermProgram = process.env.TERM_PROGRAM;
+  afterEach(() => {
+    if (origTermProgram === undefined) delete process.env.TERM_PROGRAM;
+    else process.env.TERM_PROGRAM = origTermProgram;
+  });
+  it("converts **text** to ANSI bold", () => {
+    const result = renderMarkdown("这是 **加粗** 文本");
+    // Should contain ANSI bold on/off around 加粗
+    expect(result).toContain("\x1b[1m加粗\x1b[22m");
+    // Should NOT contain raw ** markers
+    expect(result).not.toContain("**");
+  });
+
+  it("converts * bullets to · bullets", () => {
+    const result = renderMarkdown("* 第一项\n* 第二项");
+    expect(result).toContain("· 第一项");
+    expect(result).toContain("· 第二项");
+    // No raw * bullets remaining
+    expect(result).not.toMatch(/^\s*\* /m);
+  });
+
+  it("renders bold inside list items", () => {
+    const result = renderMarkdown("* **昨日**: 总统套房\n* **今日**: 桥洞");
+    expect(result).toContain("\x1b[1m昨日\x1b[22m");
+    expect(result).toContain("\x1b[1m今日\x1b[22m");
+    expect(result).toContain("·");
+    expect(result).not.toContain("**");
+  });
+
+  it("does not leak bold across lines", () => {
+    const result = renderMarkdown("打磨**冲突场景**,还是梳理**心理转变弧线**。");
+    // Each bold region should be self-contained
+    const boldRegions = [...result.matchAll(/\x1b\[1m(.*?)\x1b\[22m/g)];
+    expect(boldRegions).toHaveLength(2);
+    expect(boldRegions[0][1]).toBe("冲突场景");
+    expect(boldRegions[1][1]).toBe("心理转变弧线");
+  });
+
+  it("renders tables with box-drawing characters", () => {
+    const result = renderMarkdown(
+      "| 属性 | 值 |\n|------|----|\n| 住所 | 桥洞 |",
+    );
+    expect(result).toContain("┌");
+    expect(result).toContain("│");
+    expect(result).toContain("└");
+  });
+
+  it("does not contain full-reset codes that override parent Ink color", () => {
+    // \x1b[0m resets ALL terminal attributes including color set by Ink's <Text>.
+    // renderMarkdown must not emit it, so the parent <Text color={...}> stays in effect.
+    const result = renderMarkdown("这是 **加粗** 文本");
+    expect(result).not.toContain("\x1b[0m");
+  });
+
+  it("falls back to raw text on parse error", () => {
+    // renderMarkdown should never throw; it returns raw text on failure
+    const result = renderMarkdown("plain text");
+    expect(result).toContain("plain text");
+  });
+});

+ 18 - 1
packages/cli/src/tui/app.ts

@@ -1,4 +1,5 @@
-import { basename } from "node:path";
+import { access } from "node:fs/promises";
+import { basename, dirname, join } from "node:path";
 import {
 import {
   appendInteractionMessage,
   appendInteractionMessage,
   processProjectInteractionInput,
   processProjectInteractionInput,
@@ -89,10 +90,26 @@ export async function processTuiInput(
   return { ...result, session: nextSession };
   return { ...result, session: nextSession };
 }
 }
 
 
+async function resolveProjectRoot(cwd: string): Promise<string> {
+  // If CWD is a book directory (contains book.json), walk up to the actual project root.
+  // Structure: <projectRoot>/books/<bookId>/book.json
+  try {
+    await access(join(cwd, "book.json"));
+    const parent = dirname(cwd);
+    if (basename(parent) === "books") {
+      return dirname(parent);
+    }
+  } catch {
+    // not a book directory
+  }
+  return cwd;
+}
+
 export async function launchTui(
 export async function launchTui(
   projectRoot: string,
   projectRoot: string,
   toolsOverride?: InteractionRuntimeTools,
   toolsOverride?: InteractionRuntimeTools,
 ): Promise<void> {
 ): Promise<void> {
+  projectRoot = await resolveProjectRoot(projectRoot);
   const { hasLlmConfig } = await ensureProject(projectRoot);
   const { hasLlmConfig } = await ensureProject(projectRoot);
   const projectLanguage = await detectProjectLanguage(projectRoot);
   const projectLanguage = await detectProjectLanguage(projectRoot);
   const locale = resolveTuiLocale(process.env, projectLanguage);
   const locale = resolveTuiLocale(process.env, projectLanguage);

+ 4 - 2
packages/cli/src/tui/chat-depth.ts

@@ -3,7 +3,7 @@ export type ChatDepth = "light" | "normal" | "deep";
 export interface ChatDepthProfile {
 export interface ChatDepthProfile {
   readonly depth: ChatDepth;
   readonly depth: ChatDepth;
   readonly temperature: number;
   readonly temperature: number;
-  readonly maxTokens: number;
+  readonly maxTokens?: number;
   readonly label: string;
   readonly label: string;
 }
 }
 
 
@@ -15,6 +15,8 @@ export function resolveChatDepthProfile(depth: ChatDepth): ChatDepthProfile {
       return { depth, temperature: 0.45, maxTokens: 420, label: "deep" };
       return { depth, temperature: 0.45, maxTokens: 420, label: "deep" };
     case "normal":
     case "normal":
     default:
     default:
-      return { depth: "normal", temperature: 0.4, maxTokens: 240, label: "normal" };
+      // No maxTokens — let the model decide its own output length.
+      // Only /depth light|deep explicitly caps tokens.
+      return { depth: "normal", temperature: 0.4, label: "normal" };
   }
   }
 }
 }

+ 9 - 4
packages/cli/src/tui/dashboard-model.ts

@@ -49,6 +49,7 @@ export interface BuildDashboardViewModelParams {
   readonly lastError?: string;
   readonly lastError?: string;
   readonly sinceTimestamp?: number;
   readonly sinceTimestamp?: number;
   readonly terminalRows?: number;
   readonly terminalRows?: number;
+  readonly scrollOffset?: number;
 }
 }
 
 
 export function buildDashboardViewModel(params: BuildDashboardViewModelParams): DashboardViewModel {
 export function buildDashboardViewModel(params: BuildDashboardViewModelParams): DashboardViewModel {
@@ -62,9 +63,13 @@ export function buildDashboardViewModel(params: BuildDashboardViewModelParams):
   const terminalRows = params.terminalRows ?? process.stdout.rows ?? 24;
   const terminalRows = params.terminalRows ?? process.stdout.rows ?? 24;
   const conversationLimit = Math.max(4, terminalRows - 10);
   const conversationLimit = Math.max(4, terminalRows - 10);
 
 
-  const messageRows = params.session.messages
-    .filter((message) => message.timestamp >= sinceTimestamp)
-    .slice(-conversationLimit)
+  const filteredMessages = params.session.messages
+    .filter((message) => message.timestamp >= sinceTimestamp);
+  const scrollOffset = params.scrollOffset ?? 0;
+  const endIndex = filteredMessages.length - scrollOffset;
+  const startIndex = Math.max(0, endIndex - conversationLimit);
+  const messageRows = filteredMessages
+    .slice(startIndex, endIndex > 0 ? endIndex : undefined)
     .map((message, index) => ({
     .map((message, index) => ({
       key: `${message.timestamp}-${index}`,
       key: `${message.timestamp}-${index}`,
       label: roleLabel(message.role, params.copy),
       label: roleLabel(message.role, params.copy),
@@ -98,7 +103,7 @@ export function buildDashboardViewModel(params: BuildDashboardViewModelParams):
       `${params.copy.labels.session} ${params.session.sessionId.slice(-4)}`,
       `${params.copy.labels.session} ${params.session.sessionId.slice(-4)}`,
       params.copy.labels.messageCount(params.session.messages.length),
       params.copy.labels.messageCount(params.session.messages.length),
     ].filter(Boolean).join(" · "),
     ].filter(Boolean).join(" · "),
-    statusPrimaryLine: `${params.copy.labels.stage} ${executionLabel} · ${params.copy.labels.mode} ${modeLabel} · ${params.copy.labels.model} ${params.modelLabel}`,
+    statusPrimaryLine: `${params.copy.labels.stage} ${executionLabel} · ${params.copy.labels.model} ${params.modelLabel}`,
     statusSecondaryLine: params.lastError
     statusSecondaryLine: params.lastError
       ? `${params.copy.labels.error} · ${compactInline(params.lastError)}`
       ? `${params.copy.labels.error} · ${compactInline(params.lastError)}`
       : params.isSubmitting && latestEventSummary
       : params.isSubmitting && latestEventSummary

+ 50 - 23
packages/cli/src/tui/dashboard.tsx

@@ -13,6 +13,7 @@ import { resolveComposerCaretState } from "./composer-caret.js";
 import { resolveChatDepthProfile, type ChatDepth } from "./chat-depth.js";
 import { resolveChatDepthProfile, type ChatDepth } from "./chat-depth.js";
 import { appendStreamingAssistantChunk, createOptimisticUserMessageSession } from "./chat-draft.js";
 import { appendStreamingAssistantChunk, createOptimisticUserMessageSession } from "./chat-draft.js";
 import { renderComposerDisplay } from "./composer-display.js";
 import { renderComposerDisplay } from "./composer-display.js";
+import { renderMarkdown } from "./markdown.js";
 import { formatTuiResult } from "./output.js";
 import { formatTuiResult } from "./output.js";
 import { buildDashboardViewModel, type DashboardMessageRow } from "./dashboard-model.js";
 import { buildDashboardViewModel, type DashboardMessageRow } from "./dashboard-model.js";
 import { buildInputHistory, moveHistoryCursor } from "./input-history.js";
 import { buildInputHistory, moveHistoryCursor } from "./input-history.js";
@@ -29,6 +30,7 @@ import {
   WARM_ACCENT, WARM_BORDER, WARM_MUTED, WARM_REPLY,
   WARM_ACCENT, WARM_BORDER, WARM_MUTED, WARM_REPLY,
   STATUS_SUCCESS, STATUS_ERROR, STATUS_ACTIVE, STATUS_IDLE,
   STATUS_SUCCESS, STATUS_ERROR, STATUS_ACTIVE, STATUS_IDLE,
   ROLE_USER, ROLE_SYSTEM,
   ROLE_USER, ROLE_SYSTEM,
+  isAppleTerminal,
 } from "./theme.js";
 } from "./theme.js";
 
 
 export interface InkTuiDashboardProps {
 export interface InkTuiDashboardProps {
@@ -45,6 +47,7 @@ export interface InkTuiDashboardProps {
   readonly slashSuggestions?: ReadonlyArray<string>;
   readonly slashSuggestions?: ReadonlyArray<string>;
   readonly selectedSlashIndex?: number;
   readonly selectedSlashIndex?: number;
   readonly showComposerCursor?: boolean;
   readonly showComposerCursor?: boolean;
+  readonly scrollOffset?: number;
   readonly onInputChange?: (value: string) => void;
   readonly onInputChange?: (value: string) => void;
   readonly onSubmit?: (value: string) => void;
   readonly onSubmit?: (value: string) => void;
 }
 }
@@ -77,6 +80,7 @@ export function InkTuiDashboard(props: InkTuiDashboardProps): React.JSX.Element
     isSubmitting: props.isSubmitting,
     isSubmitting: props.isSubmitting,
     lastError: props.lastError,
     lastError: props.lastError,
     sinceTimestamp: props.sinceTimestamp,
     sinceTimestamp: props.sinceTimestamp,
+    scrollOffset: props.scrollOffset,
   });
   });
   const activeAccent = props.isSubmitting ? WARM_ACCENT : statusColor(model.executionStatus);
   const activeAccent = props.isSubmitting ? WARM_ACCENT : statusColor(model.executionStatus);
   const composer = renderComposerDisplay(props.inputValue, model.composerPlaceholder, props.showComposerCursor ?? false);
   const composer = renderComposerDisplay(props.inputValue, model.composerPlaceholder, props.showComposerCursor ?? false);
@@ -108,9 +112,6 @@ export function InkTuiDashboard(props: InkTuiDashboardProps): React.JSX.Element
           <ExecutionBadge status={model.executionStatus} color={activeAccent} />
           <ExecutionBadge status={model.executionStatus} color={activeAccent} />
           <Text color={activeAccent}> {model.statusPrimaryLine}</Text>
           <Text color={activeAccent}> {model.statusPrimaryLine}</Text>
         </Box>
         </Box>
-        <Text color={model.errorText ? STATUS_ERROR : props.isSubmitting ? WARM_ACCENT : WARM_MUTED}>
-          {"  " + model.statusSecondaryLine}
-        </Text>
 
 
         {/* Composer input */}
         {/* Composer input */}
         <Box
         <Box
@@ -141,14 +142,6 @@ export function InkTuiDashboard(props: InkTuiDashboardProps): React.JSX.Element
               </Text>
               </Text>
             ) : null}
             ) : null}
           </Box>
           </Box>
-          <Box>
-            <Text color={props.isSubmitting ? STATUS_ACTIVE : WARM_MUTED}>
-              {model.composerStatus}
-            </Text>
-            <Text color={WARM_BORDER}> │ </Text>
-            <Text color={WARM_MUTED}>{model.composerHelper}</Text>
-          </Box>
-
           {/* Slash command suggestions */}
           {/* Slash command suggestions */}
           {props.slashSuggestions && props.slashSuggestions.length > 0 ? (
           {props.slashSuggestions && props.slashSuggestions.length > 0 ? (
             <Box flexDirection="column" marginTop={1} borderTop borderColor={WARM_BORDER}>
             <Box flexDirection="column" marginTop={1} borderTop borderColor={WARM_BORDER}>
@@ -180,6 +173,7 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
   const [lastError, setLastError] = useState<string | undefined>();
   const [lastError, setLastError] = useState<string | undefined>();
   const [sinceTimestamp, setSinceTimestamp] = useState<number | undefined>();
   const [sinceTimestamp, setSinceTimestamp] = useState<number | undefined>();
   const [selectedSlashIndex, setSelectedSlashIndex] = useState(0);
   const [selectedSlashIndex, setSelectedSlashIndex] = useState(0);
+  const [scrollOffset, setScrollOffset] = useState(0);
   const [historyState, setHistoryState] = useState<{ cursor: number | null; draft: string }>({
   const [historyState, setHistoryState] = useState<{ cursor: number | null; draft: string }>({
     cursor: null,
     cursor: null,
     draft: "",
     draft: "",
@@ -240,7 +234,12 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
     }
     }
 
 
     if (key.backspace || key.delete) {
     if (key.backspace || key.delete) {
-      setInputValue((current) => current.slice(0, -1));
+      setInputValue((current) => {
+        // Use Intl.Segmenter for grapheme-aware backspace (handles CJK, emoji, etc.)
+        const segments = [...new Intl.Segmenter().segment(current)];
+        segments.pop();
+        return segments.map((s) => s.segment).join("");
+      });
       setSelectedSlashIndex(0);
       setSelectedSlashIndex(0);
       return;
       return;
     }
     }
@@ -255,6 +254,18 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
       return;
       return;
     }
     }
 
 
+    // Page Up / Page Down for conversation scrolling
+    // Raw sequences: Page Up = \x1b[5~ , Page Down = \x1b[6~
+    if (_input === "\x1b[5~") {
+      const maxOffset = Math.max(0, session.messages.length - 4);
+      setScrollOffset((cur) => Math.min(maxOffset, cur + 3));
+      return;
+    }
+    if (_input === "\x1b[6~") {
+      setScrollOffset((cur) => Math.max(0, cur - 3));
+      return;
+    }
+
     if (key.downArrow) {
     if (key.downArrow) {
       const next = moveHistoryCursor(inputHistory, historyState, inputValue, "down");
       const next = moveHistoryCursor(inputHistory, historyState, inputValue, "down");
       setHistoryState(next.state);
       setHistoryState(next.state);
@@ -344,6 +355,7 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
       const routed = routeNaturalLanguageIntent(input, {
       const routed = routeNaturalLanguageIntent(input, {
         activeBookId,
         activeBookId,
         hasCreationDraft: Boolean(session.creationDraft),
         hasCreationDraft: Boolean(session.creationDraft),
+        hasFailed: session.currentExecution?.status === "failed",
       });
       });
       const userTimestamp = Date.now();
       const userTimestamp = Date.now();
       const assistantDraftTimestamp = routed.intent === "chat" ? userTimestamp + 1 : null;
       const assistantDraftTimestamp = routed.intent === "chat" ? userTimestamp + 1 : null;
@@ -352,6 +364,7 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
       setIsSubmitting(true);
       setIsSubmitting(true);
       setLastError(undefined);
       setLastError(undefined);
       setInputValue("");
       setInputValue("");
+      setScrollOffset(0);
       setHistoryState({ cursor: null, draft: "" });
       setHistoryState({ cursor: null, draft: "" });
       setSession((current) => createOptimisticUserMessageSession(current, input, userTimestamp));
       setSession((current) => createOptimisticUserMessageSession(current, input, userTimestamp));
 
 
@@ -416,6 +429,7 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
       slashSuggestions={slashSuggestions}
       slashSuggestions={slashSuggestions}
       selectedSlashIndex={selectedSlashIndex}
       selectedSlashIndex={selectedSlashIndex}
       showComposerCursor={composerCaret.visible}
       showComposerCursor={composerCaret.visible}
+      scrollOffset={scrollOffset}
       onInputChange={(value) => {
       onInputChange={(value) => {
         setInputValue(value);
         setInputValue(value);
         setSelectedSlashIndex(0);
         setSelectedSlashIndex(0);
@@ -429,31 +443,44 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
 }
 }
 
 
 function ConversationRow(props: { readonly row: DashboardMessageRow }): React.JSX.Element {
 function ConversationRow(props: { readonly row: DashboardMessageRow }): React.JSX.Element {
-  const { role, content, label } = props.row;
+  const { role, content } = props.row;
 
 
-  if (role === "user") {
+  // Terminal.app: use the same simple layout as the main branch to avoid
+  // triggering CoreGraphics crashes from complex Box nesting + ANSI codes.
+  if (isAppleTerminal) {
+    const prefix = role === "user" ? "│ " : role === "system" ? "· " : "◆ ";
+    const color = role === "user" ? ROLE_USER : role === "system" ? ROLE_SYSTEM : WARM_REPLY;
     return (
     return (
       <Box marginBottom={1}>
       <Box marginBottom={1}>
-        <Text color={ROLE_USER}>│ </Text>
-        <Text color={WARM_REPLY}>{content}</Text>
+        <Text color={role === "assistant" ? WARM_ACCENT : color}>{prefix}</Text>
+        <Text color={color}>{content}</Text>
+      </Box>
+    );
+  }
+
+  if (role === "user") {
+    return (
+      <Box flexDirection="row" marginBottom={1}>
+        <Box minWidth={2}><Text color={ROLE_USER}>│</Text></Box>
+        <Box flexDirection="column" flexShrink={1}><Text color={ROLE_USER}>{content}</Text></Box>
       </Box>
       </Box>
     );
     );
   }
   }
 
 
   if (role === "system") {
   if (role === "system") {
     return (
     return (
-      <Box marginBottom={1}>
-        <Text color={ROLE_SYSTEM}>· </Text>
-        <Text color={ROLE_SYSTEM}>{content}</Text>
+      <Box flexDirection="row" marginBottom={1}>
+        <Box minWidth={2}><Text color={ROLE_SYSTEM}>·</Text></Box>
+        <Box flexDirection="column" flexShrink={1}><Text color={ROLE_SYSTEM}>{content}</Text></Box>
       </Box>
       </Box>
     );
     );
   }
   }
 
 
-  // assistant
+  // assistant — render markdown (bold, tables, code, etc.)
   return (
   return (
-    <Box marginBottom={1}>
-      <Text color={WARM_ACCENT}>◆ </Text>
-      <Text color={WARM_REPLY}>{content}</Text>
+    <Box flexDirection="row" marginBottom={1}>
+      <Box minWidth={2}><Text color={WARM_ACCENT}>◆</Text></Box>
+      <Box flexDirection="column" flexShrink={1}><Text color={WARM_REPLY}>{renderMarkdown(content)}</Text></Box>
     </Box>
     </Box>
   );
   );
 }
 }

+ 64 - 0
packages/cli/src/tui/markdown.ts

@@ -0,0 +1,64 @@
+import { Marked } from "marked";
+import { markedTerminal } from "marked-terminal";
+import { isAppleTerminal } from "./theme.js";
+
+const marked = new Marked();
+marked.use(markedTerminal({
+  // Terminal width minus paddingX(2*2) + prefix(2) + margin(2) = 8, plus extra 2 for safety
+  width: Math.min(process.stdout.columns ?? 80, 100) - 10,
+  reflowText: true,
+  showSectionPrefix: false,
+  tab: 2,
+  // Preserve inline formatting through reflow: keep ** markers as plain text
+  // so reflowText won't split ANSI codes across lines. Post-process converts them.
+  listitem: (text: string) => text,
+  strong: (text: string) => `**${text}**`,
+  // cli-table3 defaults table headers to red; disable to inherit parent color
+  tableOptions: { style: { head: [] } },
+}) as never);
+
+const BOLD_ON = "\x1b[1m";
+const BOLD_OFF = "\x1b[22m";
+
+/** Strip ALL ANSI escape sequences from a string. */
+function stripAnsi(text: string): string {
+  // eslint-disable-next-line no-control-regex
+  return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
+}
+
+/**
+ * Full post-processing for terminals with ANSI support (iTerm2, etc.):
+ * 1. Strip \x1b[0m (full reset) that overrides Ink's <Text color>
+ * 2. Replace `* ` bullets with `· `
+ * 3. Convert `**text**` markers to ANSI bold
+ */
+function postProcess(text: string): string {
+  return text
+    .replace(/\x1b\[0m/g, "")
+    .replace(/^(\s*)\* /gm, "$1· ")
+    .replace(/\*\*(.+?)\*\*/g, `${BOLD_ON}$1${BOLD_OFF}`);
+}
+
+/**
+ * Terminal.app post-processing: use marked-terminal for layout (tables,
+ * lists, indentation) but strip ALL ANSI codes. Box-drawing characters
+ * (┌─┐│└┘) are plain Unicode and survive the strip.
+ */
+function postProcessPlain(text: string): string {
+  return stripAnsi(text)
+    .replace(/^(\s*)\* /gm, "$1· ")
+    .replace(/\*\*(.+?)\*\*/g, "$1");
+}
+
+export function renderMarkdown(text: string): string {
+  try {
+    const rendered = marked.parse(text);
+    if (typeof rendered !== "string") {
+      return text;
+    }
+    const trimmed = rendered.replace(/\n+$/, "");
+    return isAppleTerminal ? postProcessPlain(trimmed) : postProcess(trimmed);
+  } catch {
+    return text;
+  }
+}

+ 3 - 0
packages/cli/src/tui/theme.ts

@@ -1,3 +1,6 @@
+/** Whether the terminal is macOS Terminal.app. */
+export const isAppleTerminal = process.env.TERM_PROGRAM === "Apple_Terminal";
+
 export const WARM_ACCENT = "#c88a56";
 export const WARM_ACCENT = "#c88a56";
 export const WARM_MUTED = "#8f8374";
 export const WARM_MUTED = "#8f8374";
 export const WARM_REPLY = "#f0e6d8";
 export const WARM_REPLY = "#f0e6d8";

+ 1 - 1
packages/cli/src/tui/tools.ts

@@ -40,7 +40,7 @@ export async function createInteractionTools(
   projectRoot: string,
   projectRoot: string,
   hooks?: CliInteractionToolHooks,
   hooks?: CliInteractionToolHooks,
 ): Promise<InteractionRuntimeTools> {
 ): Promise<InteractionRuntimeTools> {
-  const config = await loadConfig();
+  const config = await loadConfig({ projectRoot });
   const pipeline = new PipelineRunner(buildPipelineConfig(config, projectRoot));
   const pipeline = new PipelineRunner(buildPipelineConfig(config, projectRoot));
   const state = new StateManager(projectRoot);
   const state = new StateManager(projectRoot);
   return createInteractionToolsFromDeps(pipeline, state, hooks);
   return createInteractionToolsFromDeps(pipeline, state, hooks);

+ 2 - 2
packages/cli/src/utils.ts

@@ -31,8 +31,8 @@ export function findProjectRoot(): string {
   return process.cwd();
   return process.cwd();
 }
 }
 
 
-export async function loadConfig(options?: { readonly requireApiKey?: boolean }): Promise<ProjectConfig> {
-  return loadProjectConfig(findProjectRoot(), options);
+export async function loadConfig(options?: { readonly requireApiKey?: boolean; readonly projectRoot?: string }): Promise<ProjectConfig> {
+  return loadProjectConfig(options?.projectRoot ?? findProjectRoot(), options);
 }
 }
 
 
 export function createClient(config: ProjectConfig) {
 export function createClient(config: ProjectConfig) {

+ 113 - 0
packages/core/src/__tests__/interaction-chat-tokens.test.ts

@@ -0,0 +1,113 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { createInteractionToolsFromDeps } from "../interaction/project-tools.js";
+
+const mockChatCompletion = vi.hoisted(() => vi.fn());
+
+vi.mock("../index.js", async (importOriginal) => {
+  const actual = await importOriginal<Record<string, unknown>>();
+  return { ...actual, chatCompletion: mockChatCompletion };
+});
+
+const fakePipeline = {
+  config: {
+    client: {} as object,
+    model: "gpt-4o",
+  },
+  writeNextChapter: vi.fn(),
+  reviseDraft: vi.fn(),
+};
+
+const fakeState = {
+  ensureControlDocuments: vi.fn(async () => {}),
+  bookDir: vi.fn(() => "/tmp/books/test"),
+  loadBookConfig: vi.fn(async () => undefined),
+  loadChapterIndex: vi.fn(async () => []),
+  saveChapterIndex: vi.fn(async () => undefined),
+  listBooks: vi.fn(async () => []),
+};
+
+const MOCK_RESPONSE = {
+  content: JSON.stringify({
+    assistantReply: "好的,你想写都市异能,请问主角是什么类型的能力?",
+    draft: { concept: "都市异能", missingFields: ["title", "genre"], readyToCreate: false },
+  }),
+  tokensUsed: { prompt: 5, completion: 80, total: 85 },
+};
+
+describe("chat tool – maxTokens forwarding", () => {
+  beforeEach(() => {
+    mockChatCompletion.mockResolvedValue({
+      content: "Hello",
+      tokensUsed: { prompt: 5, completion: 10, total: 15 },
+    });
+    mockChatCompletion.mockClear();
+  });
+
+  it("does not pass maxTokens to chatCompletion when depth has no maxTokens set", async () => {
+    const tools = createInteractionToolsFromDeps(
+      fakePipeline as never,
+      fakeState as never,
+      {
+        getChatRequestOptions: () => ({ temperature: 0.7 }),
+      },
+    );
+
+    await tools.chat?.("你好", { bookId: "test-book", automationMode: "manual" });
+
+    expect(mockChatCompletion).toHaveBeenCalledOnce();
+    const options = mockChatCompletion.mock.calls[0]?.[3] as Record<string, unknown> | undefined;
+    expect(options).not.toHaveProperty("maxTokens");
+  });
+
+  it("passes maxTokens to chatCompletion when depth explicitly sets it", async () => {
+    const tools = createInteractionToolsFromDeps(
+      fakePipeline as never,
+      fakeState as never,
+      {
+        getChatRequestOptions: () => ({ temperature: 0.7, maxTokens: 512 }),
+      },
+    );
+
+    await tools.chat?.("你好", { bookId: "test-book", automationMode: "manual" });
+
+    expect(mockChatCompletion).toHaveBeenCalledOnce();
+    const options = mockChatCompletion.mock.calls[0]?.[3] as Record<string, unknown> | undefined;
+    expect(options).toHaveProperty("maxTokens", 512);
+  });
+
+  it("rethrows real chatCompletion errors instead of silently falling back", async () => {
+    mockChatCompletion.mockRejectedValueOnce(new Error("provider down"));
+
+    const tools = createInteractionToolsFromDeps(
+      fakePipeline as never,
+      fakeState as never,
+      {
+        getChatRequestOptions: () => ({ temperature: 0.7 }),
+      },
+    );
+
+    await expect(
+      tools.chat?.("你好", { bookId: "test-book", automationMode: "manual" }),
+    ).rejects.toThrow("provider down");
+  });
+});
+
+describe("developBookDraft – maxTokens not capped", () => {
+  beforeEach(() => {
+    mockChatCompletion.mockResolvedValue(MOCK_RESPONSE);
+    mockChatCompletion.mockClear();
+  });
+
+  it("does not pass maxTokens to chatCompletion so thinking models are not truncated", async () => {
+    const tools = createInteractionToolsFromDeps(
+      fakePipeline as never,
+      fakeState as never,
+    );
+
+    await tools.developBookDraft?.("我想写都市异能", undefined);
+
+    expect(mockChatCompletion).toHaveBeenCalledOnce();
+    const options = mockChatCompletion.mock.calls[0]?.[3] as Record<string, unknown> | undefined;
+    expect(options).not.toHaveProperty("maxTokens");
+  });
+});

+ 1 - 1
packages/core/src/__tests__/interaction-nl-router.test.ts

@@ -51,7 +51,7 @@ describe("interaction natural-language router", () => {
   });
   });
 
 
   it("maps explanation requests to explain_failure", () => {
   it("maps explanation requests to explain_failure", () => {
-    expect(routeNaturalLanguageIntent("为什么主角名字没按设定走", { activeBookId: "harbor" })).toEqual({
+    expect(routeNaturalLanguageIntent("为什么主角名字没按设定走", { activeBookId: "harbor", hasFailed: true })).toEqual({
       intent: "explain_failure",
       intent: "explain_failure",
       bookId: "harbor",
       bookId: "harbor",
       instruction: "为什么主角名字没按设定走",
       instruction: "为什么主角名字没按设定走",

+ 2 - 1
packages/core/src/interaction/nl-router.ts

@@ -3,6 +3,7 @@ import type { InteractionRequest } from "./intents.js";
 export interface NaturalLanguageRoutingContext {
 export interface NaturalLanguageRoutingContext {
   readonly activeBookId?: string;
   readonly activeBookId?: string;
   readonly hasCreationDraft?: boolean;
   readonly hasCreationDraft?: boolean;
+  readonly hasFailed?: boolean;
 }
 }
 
 
 export function routeNaturalLanguageIntent(
 export function routeNaturalLanguageIntent(
@@ -225,7 +226,7 @@ export function routeNaturalLanguageIntent(
     };
     };
   }
   }
 
 
-  if (/(为什么|why)/i.test(trimmed)) {
+  if (/(为什么|why)/i.test(trimmed) && context.hasFailed) {
     return {
     return {
       intent: "explain_failure",
       intent: "explain_failure",
       ...(bookId ? { bookId } : {}),
       ...(bookId ? { bookId } : {}),

+ 7 - 6
packages/core/src/interaction/project-tools.ts

@@ -504,7 +504,7 @@ export function createInteractionToolsFromDeps(
             }, null, 2),
             }, null, 2),
           },
           },
         ],
         ],
-        { temperature: 0.4, maxTokens: 700 },
+        { temperature: 0.4 },
       );
       );
 
 
       const parsed = parseCreationDraftResult(response.content);
       const parsed = parseCreationDraftResult(response.content);
@@ -561,8 +561,9 @@ export function createInteractionToolsFromDeps(
     chat: async (input, options) => {
     chat: async (input, options) => {
       const bookLabel = options.bookId ?? "none";
       const bookLabel = options.bookId ?? "none";
       const chatRequestOptions = hooks?.getChatRequestOptions?.() ?? {};
       const chatRequestOptions = hooks?.getChatRequestOptions?.() ?? {};
-      const response = instrumentedPipeline.config?.client && instrumentedPipeline.config?.model
-        ? await chatCompletion(
+      let response: Awaited<ReturnType<typeof chatCompletion>> | undefined;
+      if (instrumentedPipeline.config?.client && instrumentedPipeline.config?.model) {
+        response = await chatCompletion(
           instrumentedPipeline.config.client,
           instrumentedPipeline.config.client,
           instrumentedPipeline.config.model,
           instrumentedPipeline.config.model,
           [
           [
@@ -582,11 +583,11 @@ export function createInteractionToolsFromDeps(
           ],
           ],
           {
           {
             temperature: chatRequestOptions.temperature ?? 0.4,
             temperature: chatRequestOptions.temperature ?? 0.4,
-            maxTokens: chatRequestOptions.maxTokens ?? 240,
+            ...(chatRequestOptions.maxTokens !== undefined && { maxTokens: chatRequestOptions.maxTokens }),
             onTextDelta: hooks?.onChatTextDelta,
             onTextDelta: hooks?.onChatTextDelta,
           },
           },
-        )
-        : undefined;
+        );
+      }
 
 
       return {
       return {
         __interaction: {
         __interaction: {

+ 5 - 4
packages/core/src/pipeline/runner.ts

@@ -2253,10 +2253,11 @@ ${matrix}`,
         this.buildImportReplayStateSeed(language),
         this.buildImportReplayStateSeed(language),
         "utf-8",
         "utf-8",
       ),
       ),
-      // NOTE: pending_hooks.md intentionally NOT reset here — hooks are
-      // chapter-content-specific and user may have invested significant
-      // effort in tuning them. The replay will incrementally add new hooks
-      // detected from the imported chapters without destroying existing ones.
+      writeFile(
+        join(storyDir, "pending_hooks.md"),
+        this.buildImportReplayHooksSeed(language),
+        "utf-8",
+      ),
       rm(join(storyDir, "chapter_summaries.md"), { force: true }),
       rm(join(storyDir, "chapter_summaries.md"), { force: true }),
       rm(join(storyDir, "subplot_board.md"), { force: true }),
       rm(join(storyDir, "subplot_board.md"), { force: true }),
       rm(join(storyDir, "emotional_arcs.md"), { force: true }),
       rm(join(storyDir, "emotional_arcs.md"), { force: true }),

+ 238 - 20
pnpm-lock.yaml

@@ -32,12 +32,18 @@ importers:
         specifier: ^6.0.0
         specifier: ^6.0.0
         version: 6.0.0(ink@7.0.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
         version: 6.0.0(ink@7.0.0(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
       marked:
       marked:
-        specifier: ^15.0.0
+        specifier: ^15.0.12
         version: 15.0.12
         version: 15.0.12
+      marked-terminal:
+        specifier: ^7.3.0
+        version: 7.3.0(marked@15.0.12)
       react:
       react:
         specifier: ^19.2.4
         specifier: ^19.2.4
         version: 19.2.4
         version: 19.2.4
     devDependencies:
     devDependencies:
+      '@types/marked-terminal':
+        specifier: ^6.1.1
+        version: 6.1.1
       '@types/node':
       '@types/node':
         specifier: ^22.0.0
         specifier: ^22.0.0
         version: 22.19.15
         version: 22.19.15
@@ -269,6 +275,10 @@ packages:
       '@types/react':
       '@types/react':
         optional: true
         optional: true
 
 
+  '@colors/colors@1.5.0':
+    resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
+    engines: {node: '>=0.1.90'}
+
   '@esbuild/aix-ppc64@0.25.12':
   '@esbuild/aix-ppc64@0.25.12':
     resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
     resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
@@ -917,6 +927,9 @@ packages:
   '@types/babel__traverse@7.28.0':
   '@types/babel__traverse@7.28.0':
     resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
     resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
 
 
+  '@types/cardinal@2.1.1':
+    resolution: {integrity: sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==}
+
   '@types/chai@5.2.3':
   '@types/chai@5.2.3':
     resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
     resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
 
 
@@ -929,6 +942,9 @@ packages:
   '@types/js-yaml@4.0.9':
   '@types/js-yaml@4.0.9':
     resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
     resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
 
 
+  '@types/marked-terminal@6.1.1':
+    resolution: {integrity: sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==}
+
   '@types/node-fetch@2.6.13':
   '@types/node-fetch@2.6.13':
     resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
     resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
 
 
@@ -1012,6 +1028,9 @@ packages:
     resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
     resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
 
 
+  any-promise@1.3.0:
+    resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+
   argparse@2.0.1:
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
 
@@ -1074,10 +1093,18 @@ packages:
     resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
     resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
 
 
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
   chalk@5.6.2:
   chalk@5.6.2:
     resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
     resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
     engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
     engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
 
 
+  char-regex@1.0.2:
+    resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
+    engines: {node: '>=10'}
+
   check-error@2.1.3:
   check-error@2.1.3:
     resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
     resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
     engines: {node: '>= 16'}
     engines: {node: '>= 16'}
@@ -1090,6 +1117,15 @@ packages:
     resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
     resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
 
 
+  cli-highlight@2.1.11:
+    resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==}
+    engines: {node: '>=8.0.0', npm: '>=5.0.0'}
+    hasBin: true
+
+  cli-table3@0.6.5:
+    resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
+    engines: {node: 10.* || >= 12.*}
+
   cli-truncate@6.0.0:
   cli-truncate@6.0.0:
     resolution: {integrity: sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==}
     resolution: {integrity: sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==}
     engines: {node: '>=22'}
     engines: {node: '>=22'}
@@ -1098,6 +1134,9 @@ packages:
     resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
     resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
     engines: {node: '>= 12'}
     engines: {node: '>= 12'}
 
 
+  cliui@7.0.4:
+    resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+
   cliui@8.0.1:
   cliui@8.0.1:
     resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
     resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
@@ -1209,6 +1248,9 @@ packages:
   emoji-regex@8.0.0:
   emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
 
 
+  emojilib@2.4.0:
+    resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==}
+
   enhanced-resolve@5.20.1:
   enhanced-resolve@5.20.1:
     resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
     resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
     engines: {node: '>=10.13.0'}
     engines: {node: '>=10.13.0'}
@@ -1347,6 +1389,10 @@ packages:
     resolution: {integrity: sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==}
     resolution: {integrity: sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==}
     engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
     engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
 
 
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
   has-symbols@1.1.0:
   has-symbols@1.1.0:
     resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
     resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
@@ -1362,6 +1408,9 @@ packages:
   headers-polyfill@4.0.3:
   headers-polyfill@4.0.3:
     resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
     resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
 
 
+  highlight.js@10.7.3:
+    resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
+
   hono@4.12.8:
   hono@4.12.8:
     resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==}
     resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==}
     engines: {node: '>=16.9.0'}
     engines: {node: '>=16.9.0'}
@@ -1565,6 +1614,17 @@ packages:
   magic-string@0.30.21:
   magic-string@0.30.21:
     resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
     resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
 
 
+  marked-terminal@7.3.0:
+    resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==}
+    engines: {node: '>=16.0.0'}
+    peerDependencies:
+      marked: '>=1 <16'
+
+  marked@11.2.0:
+    resolution: {integrity: sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==}
+    engines: {node: '>= 18'}
+    hasBin: true
+
   marked@15.0.12:
   marked@15.0.12:
     resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
     resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
     engines: {node: '>= 18'}
     engines: {node: '>= 18'}
@@ -1612,6 +1672,9 @@ packages:
     resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
     resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
     engines: {node: ^18.17.0 || >=20.5.0}
     engines: {node: ^18.17.0 || >=20.5.0}
 
 
+  mz@2.7.0:
+    resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+
   nanoid@3.3.11:
   nanoid@3.3.11:
     resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
     resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1622,6 +1685,10 @@ packages:
     engines: {node: '>=10.5.0'}
     engines: {node: '>=10.5.0'}
     deprecated: Use your platform's native DOMException instead
     deprecated: Use your platform's native DOMException instead
 
 
+  node-emoji@2.2.0:
+    resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==}
+    engines: {node: '>=18'}
+
   node-fetch@2.7.0:
   node-fetch@2.7.0:
     resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
     resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
     engines: {node: 4.x || >=6.0.0}
     engines: {node: 4.x || >=6.0.0}
@@ -1637,6 +1704,10 @@ packages:
   nth-check@2.1.1:
   nth-check@2.1.1:
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
 
 
+  object-assign@4.1.1:
+    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+    engines: {node: '>=0.10.0'}
+
   onetime@5.1.2:
   onetime@5.1.2:
     resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
     resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
     engines: {node: '>=6'}
     engines: {node: '>=6'}
@@ -1663,6 +1734,15 @@ packages:
   pako@1.0.11:
   pako@1.0.11:
     resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
     resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
 
 
+  parse5-htmlparser2-tree-adapter@6.0.1:
+    resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==}
+
+  parse5@5.1.1:
+    resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==}
+
+  parse5@6.0.1:
+    resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
+
   patch-console@2.0.0:
   patch-console@2.0.0:
     resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==}
     resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1761,6 +1841,10 @@ packages:
     resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
     resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
     engines: {node: '>=14'}
     engines: {node: '>=14'}
 
 
+  skin-tone@2.0.0:
+    resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
+    engines: {node: '>=8'}
+
   slice-ansi@9.0.0:
   slice-ansi@9.0.0:
     resolution: {integrity: sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==}
     resolution: {integrity: sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==}
     engines: {node: '>=22'}
     engines: {node: '>=22'}
@@ -1812,6 +1896,14 @@ packages:
   strip-literal@3.1.0:
   strip-literal@3.1.0:
     resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
     resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
 
 
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  supports-hyperlinks@3.2.0:
+    resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==}
+    engines: {node: '>=14.18'}
+
   tabbable@6.4.0:
   tabbable@6.4.0:
     resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
     resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
 
 
@@ -1833,6 +1925,13 @@ packages:
     resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==}
     resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
 
 
+  thenify-all@1.6.0:
+    resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+    engines: {node: '>=0.8'}
+
+  thenify@3.3.1:
+    resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+
   tinybench@2.9.0:
   tinybench@2.9.0:
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
 
 
@@ -1896,6 +1995,10 @@ packages:
   undici-types@6.21.0:
   undici-types@6.21.0:
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
 
 
+  unicode-emoji-modifier-base@1.0.0:
+    resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
+    engines: {node: '>=4'}
+
   until-async@3.0.2:
   until-async@3.0.2:
     resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
     resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
 
 
@@ -2080,10 +2183,18 @@ packages:
   yallist@3.1.1:
   yallist@3.1.1:
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
 
 
+  yargs-parser@20.2.9:
+    resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+    engines: {node: '>=10'}
+
   yargs-parser@21.1.1:
   yargs-parser@21.1.1:
     resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
     resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
 
 
+  yargs@16.2.0:
+    resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+    engines: {node: '>=10'}
+
   yargs@17.7.2:
   yargs@17.7.2:
     resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
     resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
@@ -2249,6 +2360,9 @@ snapshots:
     optionalDependencies:
     optionalDependencies:
       '@types/react': 19.2.14
       '@types/react': 19.2.14
 
 
+  '@colors/colors@1.5.0':
+    optional: true
+
   '@esbuild/aix-ppc64@0.25.12':
   '@esbuild/aix-ppc64@0.25.12':
     optional: true
     optional: true
 
 
@@ -2668,6 +2782,8 @@ snapshots:
     dependencies:
     dependencies:
       '@babel/types': 7.29.0
       '@babel/types': 7.29.0
 
 
+  '@types/cardinal@2.1.1': {}
+
   '@types/chai@5.2.3':
   '@types/chai@5.2.3':
     dependencies:
     dependencies:
       '@types/deep-eql': 4.0.2
       '@types/deep-eql': 4.0.2
@@ -2679,6 +2795,13 @@ snapshots:
 
 
   '@types/js-yaml@4.0.9': {}
   '@types/js-yaml@4.0.9': {}
 
 
+  '@types/marked-terminal@6.1.1':
+    dependencies:
+      '@types/cardinal': 2.1.1
+      '@types/node': 22.19.15
+      chalk: 5.6.2
+      marked: 11.2.0
+
   '@types/node-fetch@2.6.13':
   '@types/node-fetch@2.6.13':
     dependencies:
     dependencies:
       '@types/node': 22.19.15
       '@types/node': 22.19.15
@@ -2770,18 +2893,18 @@ snapshots:
     dependencies:
     dependencies:
       environment: 1.1.0
       environment: 1.1.0
 
 
-  ansi-regex@5.0.1:
-    optional: true
+  ansi-regex@5.0.1: {}
 
 
   ansi-regex@6.2.2: {}
   ansi-regex@6.2.2: {}
 
 
   ansi-styles@4.3.0:
   ansi-styles@4.3.0:
     dependencies:
     dependencies:
       color-convert: 2.0.1
       color-convert: 2.0.1
-    optional: true
 
 
   ansi-styles@6.2.3: {}
   ansi-styles@6.2.3: {}
 
 
+  any-promise@1.3.0: {}
+
   argparse@2.0.1: {}
   argparse@2.0.1: {}
 
 
   assertion-error@2.0.1: {}
   assertion-error@2.0.1: {}
@@ -2838,8 +2961,15 @@ snapshots:
       loupe: 3.2.1
       loupe: 3.2.1
       pathval: 2.0.1
       pathval: 2.0.1
 
 
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
   chalk@5.6.2: {}
   chalk@5.6.2: {}
 
 
+  char-regex@1.0.2: {}
+
   check-error@2.1.3: {}
   check-error@2.1.3: {}
 
 
   cli-boxes@4.0.1: {}
   cli-boxes@4.0.1: {}
@@ -2848,6 +2978,21 @@ snapshots:
     dependencies:
     dependencies:
       restore-cursor: 4.0.0
       restore-cursor: 4.0.0
 
 
+  cli-highlight@2.1.11:
+    dependencies:
+      chalk: 4.1.2
+      highlight.js: 10.7.3
+      mz: 2.7.0
+      parse5: 5.1.1
+      parse5-htmlparser2-tree-adapter: 6.0.1
+      yargs: 16.2.0
+
+  cli-table3@0.6.5:
+    dependencies:
+      string-width: 4.2.3
+    optionalDependencies:
+      '@colors/colors': 1.5.0
+
   cli-truncate@6.0.0:
   cli-truncate@6.0.0:
     dependencies:
     dependencies:
       slice-ansi: 9.0.0
       slice-ansi: 9.0.0
@@ -2856,6 +3001,12 @@ snapshots:
   cli-width@4.1.0:
   cli-width@4.1.0:
     optional: true
     optional: true
 
 
+  cliui@7.0.4:
+    dependencies:
+      string-width: 4.2.3
+      strip-ansi: 6.0.1
+      wrap-ansi: 7.0.0
+
   cliui@8.0.1:
   cliui@8.0.1:
     dependencies:
     dependencies:
       string-width: 4.2.3
       string-width: 4.2.3
@@ -2872,10 +3023,8 @@ snapshots:
   color-convert@2.0.1:
   color-convert@2.0.1:
     dependencies:
     dependencies:
       color-name: 1.1.4
       color-name: 1.1.4
-    optional: true
 
 
-  color-name@1.1.4:
-    optional: true
+  color-name@1.1.4: {}
 
 
   combined-stream@1.0.8:
   combined-stream@1.0.8:
     dependencies:
     dependencies:
@@ -2952,8 +3101,9 @@ snapshots:
 
 
   electron-to-chromium@1.5.313: {}
   electron-to-chromium@1.5.313: {}
 
 
-  emoji-regex@8.0.0:
-    optional: true
+  emoji-regex@8.0.0: {}
+
+  emojilib@2.4.0: {}
 
 
   enhanced-resolve@5.20.1:
   enhanced-resolve@5.20.1:
     dependencies:
     dependencies:
@@ -3105,8 +3255,7 @@ snapshots:
 
 
   gensync@1.0.0-beta.2: {}
   gensync@1.0.0-beta.2: {}
 
 
-  get-caller-file@2.0.5:
-    optional: true
+  get-caller-file@2.0.5: {}
 
 
   get-east-asian-width@1.5.0: {}
   get-east-asian-width@1.5.0: {}
 
 
@@ -3139,6 +3288,8 @@ snapshots:
   graphql@16.13.1:
   graphql@16.13.1:
     optional: true
     optional: true
 
 
+  has-flag@4.0.0: {}
+
   has-symbols@1.1.0: {}
   has-symbols@1.1.0: {}
 
 
   has-tostringtag@1.0.2:
   has-tostringtag@1.0.2:
@@ -3152,6 +3303,8 @@ snapshots:
   headers-polyfill@4.0.3:
   headers-polyfill@4.0.3:
     optional: true
     optional: true
 
 
+  highlight.js@10.7.3: {}
+
   hono@4.12.8: {}
   hono@4.12.8: {}
 
 
   htmlparser2@7.2.0:
   htmlparser2@7.2.0:
@@ -3216,8 +3369,7 @@ snapshots:
       - bufferutil
       - bufferutil
       - utf-8-validate
       - utf-8-validate
 
 
-  is-fullwidth-code-point@3.0.0:
-    optional: true
+  is-fullwidth-code-point@3.0.0: {}
 
 
   is-fullwidth-code-point@5.1.0:
   is-fullwidth-code-point@5.1.0:
     dependencies:
     dependencies:
@@ -3333,6 +3485,19 @@ snapshots:
     dependencies:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.5
       '@jridgewell/sourcemap-codec': 1.5.5
 
 
+  marked-terminal@7.3.0(marked@15.0.12):
+    dependencies:
+      ansi-escapes: 7.3.0
+      ansi-regex: 6.2.2
+      chalk: 5.6.2
+      cli-highlight: 2.1.11
+      cli-table3: 0.6.5
+      marked: 15.0.12
+      node-emoji: 2.2.0
+      supports-hyperlinks: 3.2.0
+
+  marked@11.2.0: {}
+
   marked@15.0.12: {}
   marked@15.0.12: {}
 
 
   math-intrinsics@1.1.0: {}
   math-intrinsics@1.1.0: {}
@@ -3382,10 +3547,23 @@ snapshots:
   mute-stream@2.0.0:
   mute-stream@2.0.0:
     optional: true
     optional: true
 
 
+  mz@2.7.0:
+    dependencies:
+      any-promise: 1.3.0
+      object-assign: 4.1.1
+      thenify-all: 1.6.0
+
   nanoid@3.3.11: {}
   nanoid@3.3.11: {}
 
 
   node-domexception@1.0.0: {}
   node-domexception@1.0.0: {}
 
 
+  node-emoji@2.2.0:
+    dependencies:
+      '@sindresorhus/is': 4.6.0
+      char-regex: 1.0.2
+      emojilib: 2.4.0
+      skin-tone: 2.0.0
+
   node-fetch@2.7.0:
   node-fetch@2.7.0:
     dependencies:
     dependencies:
       whatwg-url: 5.0.0
       whatwg-url: 5.0.0
@@ -3396,6 +3574,8 @@ snapshots:
     dependencies:
     dependencies:
       boolbase: 1.0.0
       boolbase: 1.0.0
 
 
+  object-assign@4.1.1: {}
+
   onetime@5.1.2:
   onetime@5.1.2:
     dependencies:
     dependencies:
       mimic-fn: 2.1.0
       mimic-fn: 2.1.0
@@ -3428,6 +3608,14 @@ snapshots:
 
 
   pako@1.0.11: {}
   pako@1.0.11: {}
 
 
+  parse5-htmlparser2-tree-adapter@6.0.1:
+    dependencies:
+      parse5: 6.0.1
+
+  parse5@5.1.1: {}
+
+  parse5@6.0.1: {}
+
   patch-console@2.0.0: {}
   patch-console@2.0.0: {}
 
 
   path-to-regexp@6.3.0:
   path-to-regexp@6.3.0:
@@ -3475,8 +3663,7 @@ snapshots:
       string_decoder: 1.1.1
       string_decoder: 1.1.1
       util-deprecate: 1.0.2
       util-deprecate: 1.0.2
 
 
-  require-directory@2.1.1:
-    optional: true
+  require-directory@2.1.1: {}
 
 
   reselect@5.1.1: {}
   reselect@5.1.1: {}
 
 
@@ -3536,6 +3723,10 @@ snapshots:
   signal-exit@4.1.0:
   signal-exit@4.1.0:
     optional: true
     optional: true
 
 
+  skin-tone@2.0.0:
+    dependencies:
+      unicode-emoji-modifier-base: 1.0.0
+
   slice-ansi@9.0.0:
   slice-ansi@9.0.0:
     dependencies:
     dependencies:
       ansi-styles: 6.2.3
       ansi-styles: 6.2.3
@@ -3564,7 +3755,6 @@ snapshots:
       emoji-regex: 8.0.0
       emoji-regex: 8.0.0
       is-fullwidth-code-point: 3.0.0
       is-fullwidth-code-point: 3.0.0
       strip-ansi: 6.0.1
       strip-ansi: 6.0.1
-    optional: true
 
 
   string-width@8.2.0:
   string-width@8.2.0:
     dependencies:
     dependencies:
@@ -3578,7 +3768,6 @@ snapshots:
   strip-ansi@6.0.1:
   strip-ansi@6.0.1:
     dependencies:
     dependencies:
       ansi-regex: 5.0.1
       ansi-regex: 5.0.1
-    optional: true
 
 
   strip-ansi@7.2.0:
   strip-ansi@7.2.0:
     dependencies:
     dependencies:
@@ -3588,6 +3777,15 @@ snapshots:
     dependencies:
     dependencies:
       js-tokens: 9.0.1
       js-tokens: 9.0.1
 
 
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  supports-hyperlinks@3.2.0:
+    dependencies:
+      has-flag: 4.0.0
+      supports-color: 7.2.0
+
   tabbable@6.4.0: {}
   tabbable@6.4.0: {}
 
 
   tagged-tag@1.0.0: {}
   tagged-tag@1.0.0: {}
@@ -3600,6 +3798,14 @@ snapshots:
 
 
   terminal-size@4.0.1: {}
   terminal-size@4.0.1: {}
 
 
+  thenify-all@1.6.0:
+    dependencies:
+      thenify: 3.3.1
+
+  thenify@3.3.1:
+    dependencies:
+      any-promise: 1.3.0
+
   tinybench@2.9.0: {}
   tinybench@2.9.0: {}
 
 
   tinyexec@0.3.2: {}
   tinyexec@0.3.2: {}
@@ -3651,6 +3857,8 @@ snapshots:
 
 
   undici-types@6.21.0: {}
   undici-types@6.21.0: {}
 
 
+  unicode-emoji-modifier-base@1.0.0: {}
+
   until-async@3.0.2:
   until-async@3.0.2:
     optional: true
     optional: true
 
 
@@ -3796,18 +4004,28 @@ snapshots:
       ansi-styles: 4.3.0
       ansi-styles: 4.3.0
       string-width: 4.2.3
       string-width: 4.2.3
       strip-ansi: 6.0.1
       strip-ansi: 6.0.1
-    optional: true
 
 
   ws@8.20.0: {}
   ws@8.20.0: {}
 
 
-  y18n@5.0.8:
-    optional: true
+  y18n@5.0.8: {}
 
 
   yallist@3.1.1: {}
   yallist@3.1.1: {}
 
 
+  yargs-parser@20.2.9: {}
+
   yargs-parser@21.1.1:
   yargs-parser@21.1.1:
     optional: true
     optional: true
 
 
+  yargs@16.2.0:
+    dependencies:
+      cliui: 7.0.4
+      escalade: 3.2.0
+      get-caller-file: 2.0.5
+      require-directory: 2.1.1
+      string-width: 4.2.3
+      y18n: 5.0.8
+      yargs-parser: 20.2.9
+
   yargs@17.7.2:
   yargs@17.7.2:
     dependencies:
     dependencies:
       cliui: 8.0.1
       cliui: 8.0.1