@sayknow-cli/coding-agent 0.3.10 → 0.3.12

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +0 -13
  2. package/dist/types/build-metadata.d.ts +7 -0
  3. package/dist/types/capability/mcp.d.ts +3 -1
  4. package/dist/types/commands/mcp.d.ts +1 -1
  5. package/dist/types/config/keybindings.d.ts +2 -2
  6. package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
  7. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  8. package/dist/types/modes/components/status-line/types.d.ts +6 -0
  9. package/dist/types/modes/components/status-line.d.ts +1 -0
  10. package/dist/types/modes/components/welcome.d.ts +1 -0
  11. package/dist/types/modes/rpc/rpc-client.d.ts +7 -2
  12. package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
  13. package/dist/types/notifications/telegram-daemon.d.ts +2 -0
  14. package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
  15. package/dist/types/runtime-mcp/types.d.ts +4 -3
  16. package/dist/types/session/agent-session.d.ts +16 -0
  17. package/dist/types/session/session-manager.d.ts +1 -0
  18. package/dist/types/tools/skill.d.ts +9 -8
  19. package/dist/types/utils/linkedom.d.ts +4 -0
  20. package/dist/types/utils/pasted-image-path.d.ts +5 -3
  21. package/package.json +7 -7
  22. package/scripts/build-binary.ts +2 -43
  23. package/scripts/compile-args.ts +106 -0
  24. package/scripts/generate-docs-index.ts +1 -0
  25. package/src/build-metadata.ts +104 -0
  26. package/src/capability/mcp.ts +3 -1
  27. package/src/cli/mcp-cli.ts +108 -26
  28. package/src/cli.ts +21 -0
  29. package/src/commands/mcp.ts +8 -8
  30. package/src/commands/stats.ts +6 -2
  31. package/src/config/keybindings.ts +2 -2
  32. package/src/config/mcp-schema.json +21 -17
  33. package/src/config/model-profiles.ts +23 -23
  34. package/src/discovery/builtin.ts +14 -0
  35. package/src/discovery/mcp-json.ts +14 -0
  36. package/src/eval/js/executor.ts +7 -1
  37. package/src/internal-urls/docs-index.generated.ts +3 -2
  38. package/src/internal-urls/skc-protocol.ts +14 -1
  39. package/src/modes/acp/acp-agent.ts +72 -16
  40. package/src/modes/acp/acp-client-bridge.ts +27 -5
  41. package/src/modes/acp/acp-event-mapper.ts +39 -1
  42. package/src/modes/components/assistant-message.ts +1 -1
  43. package/src/modes/components/hook-selector.ts +4 -0
  44. package/src/modes/components/model-selector.ts +17 -6
  45. package/src/modes/components/session-selector.ts +2 -2
  46. package/src/modes/components/status-line/presets.ts +5 -19
  47. package/src/modes/components/status-line/segments.ts +14 -0
  48. package/src/modes/components/status-line/types.ts +6 -0
  49. package/src/modes/components/status-line.ts +13 -6
  50. package/src/modes/components/tool-execution.ts +1 -1
  51. package/src/modes/components/welcome.ts +4 -1
  52. package/src/modes/controllers/command-controller.ts +1 -1
  53. package/src/modes/controllers/event-controller.ts +9 -9
  54. package/src/modes/controllers/input-controller.ts +8 -2
  55. package/src/modes/controllers/selector-controller.ts +7 -3
  56. package/src/modes/interactive-mode.ts +12 -4
  57. package/src/modes/rpc/rpc-client.ts +30 -6
  58. package/src/modes/utils/ui-helpers.ts +154 -32
  59. package/src/notifications/telegram-daemon.ts +28 -2
  60. package/src/prompts/tools/skill.md +2 -2
  61. package/src/runtime-mcp/config-writer.ts +4 -3
  62. package/src/runtime-mcp/config.ts +1 -0
  63. package/src/runtime-mcp/types.ts +4 -3
  64. package/src/session/agent-session.ts +117 -3
  65. package/src/session/session-manager.ts +68 -40
  66. package/src/skc-runtime/state-runtime.ts +105 -12
  67. package/src/skc-runtime/workflow-manifest.generated.json +4 -28
  68. package/src/skc-runtime/workflow-manifest.ts +1 -7
  69. package/src/slash-commands/builtin-registry.ts +9 -4
  70. package/src/tools/bash-interactive.ts +9 -4
  71. package/src/tools/browser/readable.ts +2 -2
  72. package/src/tools/fetch.ts +5 -5
  73. package/src/tools/skill.ts +14 -11
  74. package/src/utils/linkedom.ts +14 -0
  75. package/src/utils/pasted-image-path.ts +23 -9
  76. package/src/web/scrapers/arxiv.ts +2 -2
  77. package/src/web/scrapers/go-pkg.ts +2 -2
  78. package/src/web/scrapers/iacr.ts +2 -2
  79. package/src/web/scrapers/readthedocs.ts +2 -2
  80. package/src/web/scrapers/twitter.ts +3 -2
  81. package/src/web/scrapers/wikipedia.ts +2 -2
@@ -811,12 +811,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
811
811
  {
812
812
  name: "session",
813
813
  priority: 88,
814
- description: "Show current session info or delete current session",
814
+ description: "Show session info or delete the current session transcript/artifacts",
815
815
  acpDescription: "Show session information",
816
816
  acpInputHint: "info|delete",
817
817
  subcommands: [
818
818
  { name: "info", description: "Show current session id, title, and workspace" },
819
- { name: "delete", description: "Delete current session and return to selector" },
819
+ { name: "delete", description: "Delete current session transcript and artifacts" },
820
820
  ],
821
821
  allowArgs: true,
822
822
  handle: async (command, runtime) => {
@@ -845,7 +845,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
845
845
  return usage(`Failed to delete session: ${errorMessage(err)}`, runtime);
846
846
  }
847
847
  await runtime.output(
848
- `Session deleted: ${sessionFile}. Use ACP \`session/load\` to switch to another session.`,
848
+ [
849
+ `Deleted current session transcript and artifacts: ${sessionFile}`,
850
+ "Other sessions and topic/history metadata were not deleted.",
851
+ ].join("\n"),
849
852
  );
850
853
  return commandConsumed();
851
854
  }
@@ -1021,8 +1024,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1021
1024
  return commandConsumed();
1022
1025
  }
1023
1026
  if (args === "login" || args.startsWith("login ")) {
1027
+ const providerId = args.slice("login".length).trim();
1028
+ const loginCommand = providerId ? `/login ${providerId}` : "/login [provider-id]";
1024
1029
  await runtime.output(
1025
- "Use the terminal UI /login selector for browser, device-code, or manual callback provider login.",
1030
+ `Open the terminal UI and run ${loginCommand} for OAuth/subscription account login. Paste callbacks with /login <redirect URL or code>.`,
1026
1031
  );
1027
1032
  return commandConsumed();
1028
1033
  }
@@ -12,7 +12,6 @@ import {
12
12
  } from "@sayknow-cli/tui";
13
13
  import { sanitizeText } from "@sayknow-cli/utils";
14
14
  import type { Terminal as XtermTerminalType } from "@xterm/headless";
15
- import xterm from "@xterm/headless";
16
15
  import { Settings } from "../config/settings";
17
16
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
18
17
  import type { Theme } from "../modes/theme/theme";
@@ -32,8 +31,6 @@ function normalizeCaptureChunk(chunk: string): string {
32
31
  return sanitizeWithOptionalSixelPassthrough(normalized, sanitizeText);
33
32
  }
34
33
 
35
- const XtermTerminal = xterm.Terminal;
36
-
37
34
  function normalizeInputForPty(data: string, applicationCursorKeysMode: boolean): string {
38
35
  const kitty = parseKittySequence(data);
39
36
  if (kitty?.eventType === 3) {
@@ -113,6 +110,7 @@ class BashInteractiveOverlayComponent implements Component {
113
110
  private readonly command: string,
114
111
  private readonly uiTheme: Theme,
115
112
  private readonly getTerminalRows: () => number,
113
+ XtermTerminal: typeof XtermTerminalType,
116
114
  ) {
117
115
  this.#terminal = new XtermTerminal({
118
116
  cols: 120,
@@ -305,10 +303,17 @@ export async function runInteractiveBashPty(
305
303
  headBytes: resolveOutputSinkHeadBytes(settings),
306
304
  maxColumns: resolveOutputMaxColumns(settings),
307
305
  });
306
+ const { default: xterm } = await import("@xterm/headless");
307
+ const XtermTerminal = xterm.Terminal;
308
308
  const result = await ui.custom<BashInteractiveResult>(
309
309
  (tui, uiTheme, _keybindings, done) => {
310
310
  const session = new PtySession();
311
- const component = new BashInteractiveOverlayComponent(options.command, uiTheme, () => tui.terminal.rows);
311
+ const component = new BashInteractiveOverlayComponent(
312
+ options.command,
313
+ uiTheme,
314
+ () => tui.terminal.rows,
315
+ XtermTerminal,
316
+ );
312
317
  component.setSession(session);
313
318
  let finished = false;
314
319
  const finalize = (run: PtyRunResult) => {
@@ -1,5 +1,5 @@
1
1
  import { Readability } from "@mozilla/readability";
2
- import { parseHTML } from "linkedom";
2
+ import { parseHtmlLazy } from "../../utils/linkedom";
3
3
  import { htmlToBasicMarkdown } from "../../web/scrapers/types";
4
4
 
5
5
  export type ReadableFormat = "text" | "markdown";
@@ -31,7 +31,7 @@ export async function extractReadableFromHtml(
31
31
  url: string,
32
32
  format: ReadableFormat,
33
33
  ): Promise<ReadableResult | null> {
34
- const { document } = parseHTML(html);
34
+ const { document } = await parseHtmlLazy(html);
35
35
 
36
36
  // --- Primary: Readability article extraction ---
37
37
  const article = new Readability(document).parse();
@@ -5,7 +5,6 @@ import type { ImageContent, TextContent } from "@sayknow-cli/ai";
5
5
  import { htmlToMarkdown } from "@sayknow-cli/natives";
6
6
  import { type Component, Text } from "@sayknow-cli/tui";
7
7
  import { $which, ptree, truncate } from "@sayknow-cli/utils";
8
- import { parseHTML } from "linkedom";
9
8
  import type { Settings } from "../config/settings";
10
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
10
  import { type Theme, theme } from "../modes/theme/theme";
@@ -15,6 +14,7 @@ import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
15
14
  import { renderStatusLine } from "../tui";
16
15
  import { CachedOutputBlock } from "../tui/output-block";
17
16
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
17
+ import { parseHtmlLazy } from "../utils/linkedom";
18
18
  import { ensureTool } from "../utils/tools-manager";
19
19
  import { INSANE_NOTES, tryInsaneFetch } from "../web/insane/bridge";
20
20
  import { validatePublicHttpUrl, validatePublicHttpUrlForInsane } from "../web/insane/url-guard";
@@ -483,9 +483,9 @@ function cleanFeedText(text: string): string {
483
483
  /**
484
484
  * Parse RSS/Atom feed to markdown
485
485
  */
486
- function parseFeedToMarkdown(content: string, maxItems = 10): string {
486
+ async function parseFeedToMarkdown(content: string, maxItems = 10): Promise<string> {
487
487
  try {
488
- const doc = parseHTML(content).document;
488
+ const doc = (await parseHtmlLazy(content)).document;
489
489
 
490
490
  // Try RSS
491
491
  const channel = doc.querySelector("channel");
@@ -1026,7 +1026,7 @@ async function renderUrl(
1026
1026
  }
1027
1027
 
1028
1028
  if (isFeed || (isXml && (rawContent.includes("<rss") || rawContent.includes("<feed")))) {
1029
- const parsed = parseFeedToMarkdown(rawContent);
1029
+ const parsed = await parseFeedToMarkdown(rawContent);
1030
1030
  const output = finalizeOutput(parsed);
1031
1031
  return {
1032
1032
  url,
@@ -1119,7 +1119,7 @@ async function renderUrl(
1119
1119
  const altResult = await loadPage(resolved, { timeout, signal });
1120
1120
  if (altResult.ok && altResult.content.trim().length > 200) {
1121
1121
  notes.push(`Used feed alternate: ${resolved}`);
1122
- const parsed = parseFeedToMarkdown(altResult.content);
1122
+ const parsed = await parseFeedToMarkdown(altResult.content);
1123
1123
  const output = finalizeOutput(parsed);
1124
1124
  return {
1125
1125
  url,
@@ -4,15 +4,16 @@
4
4
  * Lets the agent hand off to another available skill in the current turn. The
5
5
  * callee's SKILL.md is dispatched through the same custom-message path used by
6
6
  * `/skill:<name>` typing, as a user-attribution message delivered same-turn
7
- * (without `deliverAs: "nextTurn"`). Before dispatch, the tool calls
8
- * `skc state <caller> handoff --to <callee>` in-process via the state-runtime
9
- * function so caller and callee mode-states plus `skill-active-state.json`
10
- * transition atomically.
7
+ * (without `deliverAs: "nextTurn"`). When the caller is one of the canonical
8
+ * workflow skills, the tool first runs the native state handoff so workflow
9
+ * mode-state and `skill-active-state.json` transition atomically. Runtime
10
+ * project/user skills have no native workflow mode-state, so they are dispatched
11
+ * directly and tracked by the prompt observer instead.
11
12
  *
12
- * Chaining is refused unless the caller's `current_phase` is in
13
- * `{complete, completed, handoff, failed, cancelled, canceled, inactive}`. The
14
- * agent declares readiness either by writing `current_phase: "handoff"` to its
15
- * mode-state or by running the handoff verb directly.
13
+ * Canonical workflow chaining is refused unless the caller's `current_phase` is
14
+ * in `{complete, completed, handoff, failed, cancelled, canceled, inactive}`.
15
+ * The agent declares readiness either by writing `current_phase: "handoff"` to
16
+ * its mode-state or by running the handoff verb directly.
16
17
  */
17
18
 
18
19
  import type { AgentTool, AgentToolResult } from "@sayknow-cli/agent-core";
@@ -24,6 +25,7 @@ import { buildSkillPromptMessage } from "../extensibility/skills";
24
25
  import skillDescription from "../prompts/tools/skill.md" with { type: "text" };
25
26
  import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
26
27
  import { runNativeStateCommand } from "../skc-runtime/state-runtime";
28
+ import { isCanonicalSkcWorkflowSkill } from "../skill-state/active-state";
27
29
  import type { ToolSession } from ".";
28
30
  import { ToolError } from "./tool-errors";
29
31
 
@@ -130,9 +132,10 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
130
132
  throw new ToolError(`skill tool: unknown skill "${requestedName}".${hint}`);
131
133
  }
132
134
 
133
- // Phase guard + atomic handoff. Only runs when transitioning between
134
- // distinct skills (same-skill recursion was already refused above).
135
- if (activeSkill) {
135
+ // Phase guard + atomic native handoff only apply to canonical workflow
136
+ // skills. Runtime project/user skills do not have a native mode-state,
137
+ // so there is no `skc state <skill>` command to run for them.
138
+ if (activeSkill && isCanonicalSkcWorkflowSkill(activeSkill)) {
136
139
  const phase = (this.#session.getActiveSkillPhase?.() ?? "running").trim().toLowerCase();
137
140
  if (!TERMINAL_PHASES.has(phase)) {
138
141
  throw new ToolError(
@@ -0,0 +1,14 @@
1
+ type LinkedomModule = typeof import("linkedom");
2
+
3
+ type ParseHtml = LinkedomModule["parseHTML"];
4
+
5
+ let parseHtml: ParseHtml | undefined;
6
+ let linkedomImport: Promise<LinkedomModule> | undefined;
7
+
8
+ export async function parseHtmlLazy(markup: string): Promise<ReturnType<ParseHtml>> {
9
+ if (!parseHtml) {
10
+ linkedomImport ??= import("linkedom");
11
+ parseHtml = (await linkedomImport).parseHTML;
12
+ }
13
+ return parseHtml(markup);
14
+ }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Resolve pasted editor text to an image file path.
3
3
  *
4
- * Terminals insert a shell-escaped filesystem path when a file is drag-dropped
5
- * onto them (e.g. macOS `Screenshot\ 2026-07-07\ at\ 11.06.38 PM.png`, where
6
- * the visible "space" before AM/PM is U+202F and stays unescaped). When the
7
- * entire paste is a single path to an existing image file, the interactive
8
- * editor attaches the image and inserts an `[image N]` placeholder instead of
9
- * leaving the raw path in the prompt.
4
+ * Some terminal/clipboard integrations paste a temporary filesystem path after a
5
+ * copied image is pasted into the editor (for example `/tmp/clipboard-...png`).
6
+ * Only those recognized clipboard temp files are auto-attached and replaced with
7
+ * an `[image N] source="/path"` reference so the model receives both the image
8
+ * payload and the raw temp file path. Ordinary image paths remain literal prompt
9
+ * text; users attach saved files explicitly with `@path/to/image.png`.
10
10
  */
11
11
  import * as fs from "node:fs";
12
12
  import * as os from "node:os";
@@ -14,6 +14,7 @@ import * as path from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
 
16
16
  const IMAGE_FILE_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp)$/i;
17
+ const CLIPBOARD_TEMP_BASENAME_PATTERN = /^clipboard-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9-]+\.(?:png|jpe?g|gif|webp)$/i;
17
18
 
18
19
  export interface DecodePastedPathOptions {
19
20
  /**
@@ -150,16 +151,25 @@ function hasSupportedImageMagic(filePath: string): boolean {
150
151
  }
151
152
  }
152
153
 
154
+ function isRecognizedClipboardTempPath(filePath: string): boolean {
155
+ const resolved = path.resolve(filePath);
156
+ const tempRoot = path.resolve(os.tmpdir());
157
+ if (resolved !== tempRoot && !resolved.startsWith(`${tempRoot}${path.sep}`)) return false;
158
+ return CLIPBOARD_TEMP_BASENAME_PATTERN.test(path.basename(resolved));
159
+ }
160
+
153
161
  /**
154
- * Returns the resolved path when the whole pasted text is a single path to an
155
- * existing image file (verified by extension AND content signature),
156
- * otherwise `undefined` (the paste is inserted as text).
162
+ * Returns the resolved path when the whole pasted text is a recognized clipboard
163
+ * temp path to an existing image file, otherwise `undefined` (the paste is
164
+ * inserted as text). Arbitrary saved image paths are not auto-attached here;
165
+ * users attach them explicitly with `@path/to/image.png`.
157
166
  */
158
167
  export function resolvePastedImagePath(text: string, options?: ResolvePastedImagePathOptions): string | undefined {
159
168
  const candidate = decodePastedPathCandidate(text, options);
160
169
  if (!candidate || !IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
161
170
 
162
171
  const resolved = path.resolve(options?.cwd ?? process.cwd(), candidate);
172
+ if (!isRecognizedClipboardTempPath(resolved)) return undefined;
163
173
  try {
164
174
  if (!fs.statSync(resolved).isFile()) return undefined;
165
175
  } catch {
@@ -168,3 +178,7 @@ export function resolvePastedImagePath(text: string, options?: ResolvePastedImag
168
178
  if (!hasSupportedImageMagic(resolved)) return undefined;
169
179
  return resolved;
170
180
  }
181
+
182
+ export function formatPastedImageReference(placeholder: string, imagePath: string): string {
183
+ return `${placeholder} source=${JSON.stringify(imagePath)}`;
184
+ }
@@ -1,4 +1,4 @@
1
- import { parseHTML } from "linkedom";
1
+ import { parseHtmlLazy } from "../../utils/linkedom";
2
2
  import type { RenderResult, SpecialHandler } from "./types";
3
3
  import { buildResult, loadPage } from "./types";
4
4
  import { convertWithMarkit, fetchBinary } from "./utils";
@@ -31,7 +31,7 @@ export const handleArxiv: SpecialHandler = async (
31
31
  if (!result.ok) return null;
32
32
 
33
33
  // Parse the Atom feed response
34
- const doc = parseHTML(result.content).document;
34
+ const doc = (await parseHtmlLazy(result.content)).document;
35
35
  const entry = doc.querySelector("entry");
36
36
 
37
37
  if (!entry) return null;
@@ -1,5 +1,5 @@
1
1
  import { tryParseJson } from "@sayknow-cli/utils";
2
- import { parseHTML } from "linkedom";
2
+ import { parseHtmlLazy } from "../../utils/linkedom";
3
3
  import type { RenderResult, SpecialHandler } from "./types";
4
4
  import { buildResult, htmlToBasicMarkdown, loadPage } from "./types";
5
5
 
@@ -97,7 +97,7 @@ export const handleGoPkg: SpecialHandler = async (
97
97
  });
98
98
  }
99
99
 
100
- const doc = parseHTML(pageResult.content).document;
100
+ const doc = (await parseHtmlLazy(pageResult.content)).document;
101
101
 
102
102
  // Extract actual module path from breadcrumb or header
103
103
  const breadcrumb = doc.querySelector(".go-Breadcrumb");
@@ -1,4 +1,4 @@
1
- import { parseHTML } from "linkedom";
1
+ import { parseHtmlLazy } from "../../utils/linkedom";
2
2
  import type { RenderResult, SpecialHandler } from "./types";
3
3
  import { buildResult, loadPage } from "./types";
4
4
  import { convertWithMarkit, fetchBinary } from "./utils";
@@ -30,7 +30,7 @@ export const handleIacr: SpecialHandler = async (
30
30
 
31
31
  if (!result.ok) return null;
32
32
 
33
- const doc = parseHTML(result.content).document;
33
+ const doc = (await parseHtmlLazy(result.content)).document;
34
34
 
35
35
  // Extract metadata from the page
36
36
  const title =
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Read the Docs handler for web-fetch
3
3
  */
4
- import { parseHTML } from "linkedom";
4
+ import { parseHtmlLazy } from "../../utils/linkedom";
5
5
  import { buildResult, htmlToBasicMarkdown, loadPage, type RenderResult, type SpecialHandler } from "./types";
6
6
 
7
7
  export const handleReadTheDocs: SpecialHandler = async (
@@ -39,7 +39,7 @@ export const handleReadTheDocs: SpecialHandler = async (
39
39
  }
40
40
 
41
41
  // Parse HTML
42
- const root = parseHTML(result.content).document;
42
+ const root = (await parseHtmlLazy(result.content)).document;
43
43
 
44
44
  // Extract main content from common Read the Docs selectors
45
45
  let mainContent =
@@ -1,5 +1,6 @@
1
- import { type HTMLElement, parseHTML } from "linkedom";
1
+ import type { HTMLElement } from "linkedom";
2
2
  import { ToolAbortError } from "../../tools/tool-errors";
3
+ import { parseHtmlLazy } from "../../utils/linkedom";
3
4
  import type { RenderResult, SpecialHandler } from "./types";
4
5
  import { buildResult, loadPage } from "./types";
5
6
 
@@ -33,7 +34,7 @@ export const handleTwitter: SpecialHandler = async (
33
34
 
34
35
  if (result.ok && result.content.length > 500) {
35
36
  // Parse the Nitter HTML
36
- const doc = parseHTML(result.content).document;
37
+ const doc = (await parseHtmlLazy(result.content)).document;
37
38
 
38
39
  // Extract tweet content
39
40
  const tweetContent = doc.querySelector(".tweet-content")?.textContent?.trim();
@@ -1,4 +1,4 @@
1
- import { parseHTML } from "linkedom";
1
+ import { parseHtmlLazy } from "../../utils/linkedom";
2
2
  import type { RenderResult, SpecialHandler } from "./types";
3
3
  import { buildResult, loadPage } from "./types";
4
4
 
@@ -45,7 +45,7 @@ export const handleWikipedia: SpecialHandler = async (
45
45
  const contentResult = await loadPage(contentUrl, { timeout, signal });
46
46
 
47
47
  if (contentResult.ok) {
48
- const doc = parseHTML(contentResult.content).document;
48
+ const doc = (await parseHtmlLazy(contentResult.content)).document;
49
49
 
50
50
  // Extract main content sections
51
51
  const sections = doc.querySelectorAll("section");