@v1nvn/readability 0.41.1 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,12 +10,12 @@ User-facing docs: [root README](../../README.md).
10
10
  ## Install
11
11
 
12
12
  ```bash
13
- npm install @v1nvn/readability@0.41.1
13
+ npm install @v1nvn/readability@0.43.0
14
14
  # or run on demand:
15
- npx @v1nvn/readability@0.41.1
15
+ npx @v1nvn/readability@0.43.0
16
16
  ```
17
17
 
18
- Requires Node >= 22. Build from source:
18
+ Requires Node >= 22.22.2 (jsdom 30's floor). Build from source:
19
19
 
20
20
  ```bash
21
21
  git clone <repo> && cd packages/readability
@@ -58,7 +58,7 @@ Add to your MCP client config (Claude Code, Claude Desktop, etc.):
58
58
  "mcpServers": {
59
59
  "readability": {
60
60
  "command": "npx",
61
- "args": ["-y", "@v1nvn/readability@0.41.1"],
61
+ "args": ["-y", "@v1nvn/readability@0.43.0"],
62
62
  },
63
63
  },
64
64
  }
@@ -98,6 +98,7 @@ Extracts the main article from rendered HTML and returns Markdown + metadata + d
98
98
  | `readabilityOverrides` | — | Escape hatch — passed verbatim to `new Readability(doc, …)`. Unstable. |
99
99
  | `chunk` | — | Split the extracted markdown into token-bounded chunks (RAG/embedding-ready). `{maxTokens, overlap?, strategy?}` (strategy defaults to `semantic`) — when set, `structuredContent.chunks` is an array of `{index, text, tokenCount, headingContext}`. Only applies to `format:"markdown" \| "text"`; HTML/JSON payloads carry no markdown body to slice and leave `chunks` unset. |
100
100
  | `imageInventory` | `false` | Emit `structuredContent.images` — an array of `{src, alt, width?, height?, caption}` for every `<img>` in the extracted article (absolute resolved srcs, placeholders skipped, caption from the enclosing `<figure>`'s `<figcaption>` else `alt`). Independent of the `images` inline-rendering option. |
101
+ | `cache` | `false` | Cache the result, keyed by a normalized hash of the HTML plus the output options; repeat calls with the same normalized HTML hit the cache and report `diagnostics.cache` (hit/miss + both hashes). Entries are addressable as `readability://page/{hash}` resources — [Resources](#resources-page-cache). |
101
102
  | `debug` | `false` | Emit `diagnostics.trace` with per-stage `{stage, ms}` timings (`normalize`, `readability`, `sanitize`, `turndown`, `metadata`). Debug-only — `trace` is absent otherwise. |
102
103
 
103
104
  **Fallback.** If Readability's `parse()` returns no article (e.g. an app shell or image-only page), a selector cascade salvages the first usable root — `article` → `main` → `[role=main]` → largest text-dense block → `body` — and reports `diagnostics.fallbackUsed: true` with `extractedNode` naming the root that was used.
@@ -1,4 +1,4 @@
1
- import { ft as describeError, j as readHtmlFile, mt as parseQuietly, t as extractArticleFromHtml } from "./extract-Cn_gf58d.js";
1
+ import { ft as describeError, ht as parseQuietly, j as readHtmlFile, mt as HelpRequested, t as extractArticleFromHtml } from "./extract-Cyv5qfGN.js";
2
2
  import { Command, InvalidArgumentError, Option } from "commander";
3
3
  //#region src/cli.ts
4
4
  var FORMATS = [
@@ -15,11 +15,12 @@ function buildProgram() {
15
15
  return new Command("readability extract").argument("[file]", "HTML file; stdin when omitted").addOption(new Option("--format <fmt>", "output format").choices(FORMATS).default("md")).option("--max-chars <n>", "truncate the output", parseMaxChars);
16
16
  }
17
17
  function parseArgs(argv) {
18
- const program = parseQuietly(buildProgram(), argv.slice(1));
19
- if (program === void 0) return;
20
- const { format, maxChars } = program.opts();
18
+ const program = buildProgram();
19
+ const parsed = parseQuietly(program, argv.slice(1));
20
+ if (parsed === void 0 || parsed instanceof HelpRequested) return parsed;
21
+ const { format, maxChars } = parsed.opts();
21
22
  return {
22
- file: program.args.at(0),
23
+ file: parsed.args.at(0),
23
24
  format,
24
25
  maxChars
25
26
  };
@@ -41,6 +42,10 @@ async function runCli(argv) {
41
42
  return 2;
42
43
  }
43
44
  const parsed = parseArgs(argv);
45
+ if (parsed instanceof HelpRequested) {
46
+ process.stdout.write(`${parsed.command.helpInformation()}\n`);
47
+ return 0;
48
+ }
44
49
  if (parsed === void 0) {
45
50
  process.stderr.write(buildProgram().helpInformation());
46
51
  return 2;
@@ -68,4 +73,4 @@ async function runCli(argv) {
68
73
  //#endregion
69
74
  export { runCli };
70
75
 
71
- //# sourceMappingURL=cli-B7SX_CrL.js.map
76
+ //# sourceMappingURL=cli-BE_Ml3rh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-BE_Ml3rh.js","names":[],"sources":["../../src/cli.ts"],"sourcesContent":["import { HelpRequested, parseQuietly } from '@v1nvn/agentic-core';\nimport { Command, InvalidArgumentError, Option } from 'commander';\n\nimport { describeError } from './errors.js';\nimport { extractArticleFromHtml } from './tools/extract.js';\nimport { readHtmlFile } from './tools/html-source.js';\n\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { Readable } from 'node:stream';\n\ntype CliFormat = 'html' | 'json' | 'md';\n\nexport interface ParsedArgs {\n readonly file: string | undefined;\n readonly format: CliFormat;\n readonly maxChars: number | undefined;\n}\n\nconst FORMATS: readonly CliFormat[] = ['html', 'json', 'md'];\n\nfunction parseMaxChars(value: string): number {\n const n = Number(value);\n if (!Number.isInteger(n)) {\n throw new InvalidArgumentError('must be an integer');\n }\n return n;\n}\n\nexport function buildProgram(): Command {\n return new Command('readability extract')\n .argument('[file]', 'HTML file; stdin when omitted')\n .addOption(\n new Option('--format <fmt>', 'output format')\n .choices(FORMATS)\n .default('md'),\n )\n .option('--max-chars <n>', 'truncate the output', parseMaxChars);\n}\n\nexport function parseArgs(\n argv: readonly string[],\n): HelpRequested | ParsedArgs | undefined {\n const program = buildProgram();\n const parsed = parseQuietly(program, argv.slice(1));\n if (parsed === undefined || parsed instanceof HelpRequested) {\n return parsed;\n }\n const { format, maxChars } = parsed.opts<{\n format: CliFormat;\n maxChars: number | undefined;\n }>();\n return { file: parsed.args.at(0), format, maxChars };\n}\n\n// The stream is injected rather than reading process.stdin directly so the\n// path is testable. Chunks may be Buffer (process.stdin) or string\n// (Readable.from), so both are handled.\nexport async function readHtml(\n file: string | undefined,\n stream: Readable,\n): Promise<string> {\n if (file !== undefined) {\n return readHtmlFile(file);\n }\n const chunks: string[] = [];\n for await (const chunk of stream) {\n if (typeof chunk === 'string') {\n chunks.push(chunk);\n } else {\n chunks.push(Buffer.from(chunk as Uint8Array).toString('utf8'));\n }\n }\n return chunks.join('');\n}\n\nfunction payloadText(result: CallToolResult): string {\n const first = result.content.at(0);\n return first !== undefined && 'text' in first ? first.text : '';\n}\n\nexport async function runCli(argv: readonly string[]): Promise<number> {\n if (argv[0] !== 'extract') {\n process.stderr.write(buildProgram().helpInformation());\n return 2;\n }\n\n const parsed = parseArgs(argv);\n if (parsed instanceof HelpRequested) {\n process.stdout.write(`${parsed.command.helpInformation()}\\n`);\n return 0;\n }\n if (parsed === undefined) {\n process.stderr.write(buildProgram().helpInformation());\n return 2;\n }\n\n try {\n const html = await readHtml(parsed.file, process.stdin);\n // json reuses the markdown pipeline; the structured object is serialized below.\n const pipelineFormat = parsed.format === 'html' ? 'html' : 'markdown';\n const result = extractArticleFromHtml({\n html,\n format: pipelineFormat,\n ...(parsed.maxChars !== undefined ? { maxChars: parsed.maxChars } : {}),\n });\n\n if (result.isError) {\n process.stderr.write(`${payloadText(result)}\\n`);\n return 1;\n }\n\n if (parsed.format === 'json') {\n process.stdout.write(\n `${JSON.stringify(result.structuredContent, null, 2)}\\n`,\n );\n } else {\n process.stdout.write(`${payloadText(result)}\\n`);\n }\n return 0;\n } catch (err) {\n process.stderr.write(`${describeError(err)}\\n`);\n return 1;\n }\n}\n"],"mappings":";;;AAkBA,IAAM,UAAgC;CAAC;CAAQ;CAAQ;AAAI;AAE3D,SAAS,cAAc,OAAuB;CAC5C,MAAM,IAAI,OAAO,KAAK;CACtB,IAAI,CAAC,OAAO,UAAU,CAAC,GACrB,MAAM,IAAI,qBAAqB,oBAAoB;CAErD,OAAO;AACT;AAEA,SAAgB,eAAwB;CACtC,OAAO,IAAI,QAAQ,qBAAqB,CAAC,CACtC,SAAS,UAAU,+BAA+B,CAAC,CACnD,UACC,IAAI,OAAO,kBAAkB,eAAe,CAAC,CAC1C,QAAQ,OAAO,CAAC,CAChB,QAAQ,IAAI,CACjB,CAAC,CACA,OAAO,mBAAmB,uBAAuB,aAAa;AACnE;AAEA,SAAgB,UACd,MACwC;CACxC,MAAM,UAAU,aAAa;CAC7B,MAAM,SAAS,aAAa,SAAS,KAAK,MAAM,CAAC,CAAC;CAClD,IAAI,WAAW,KAAA,KAAa,kBAAkB,eAC5C,OAAO;CAET,MAAM,EAAE,QAAQ,aAAa,OAAO,KAGjC;CACH,OAAO;EAAE,MAAM,OAAO,KAAK,GAAG,CAAC;EAAG;EAAQ;CAAS;AACrD;AAKA,eAAsB,SACpB,MACA,QACiB;CACjB,IAAI,SAAS,KAAA,GACX,OAAO,aAAa,IAAI;CAE1B,MAAM,SAAmB,CAAC;CAC1B,WAAW,MAAM,SAAS,QACxB,IAAI,OAAO,UAAU,UACnB,OAAO,KAAK,KAAK;MAEjB,OAAO,KAAK,OAAO,KAAK,KAAmB,CAAC,CAAC,SAAS,MAAM,CAAC;CAGjE,OAAO,OAAO,KAAK,EAAE;AACvB;AAEA,SAAS,YAAY,QAAgC;CACnD,MAAM,QAAQ,OAAO,QAAQ,GAAG,CAAC;CACjC,OAAO,UAAU,KAAA,KAAa,UAAU,QAAQ,MAAM,OAAO;AAC/D;AAEA,eAAsB,OAAO,MAA0C;CACrE,IAAI,KAAK,OAAO,WAAW;EACzB,QAAQ,OAAO,MAAM,aAAa,CAAC,CAAC,gBAAgB,CAAC;EACrD,OAAO;CACT;CAEA,MAAM,SAAS,UAAU,IAAI;CAC7B,IAAI,kBAAkB,eAAe;EACnC,QAAQ,OAAO,MAAM,GAAG,OAAO,QAAQ,gBAAgB,EAAE,GAAG;EAC5D,OAAO;CACT;CACA,IAAI,WAAW,KAAA,GAAW;EACxB,QAAQ,OAAO,MAAM,aAAa,CAAC,CAAC,gBAAgB,CAAC;EACrD,OAAO;CACT;CAEA,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,OAAO,MAAM,QAAQ,KAAK;EAEtD,MAAM,iBAAiB,OAAO,WAAW,SAAS,SAAS;EAC3D,MAAM,SAAS,uBAAuB;GACpC;GACA,QAAQ;GACR,GAAI,OAAO,aAAa,KAAA,IAAY,EAAE,UAAU,OAAO,SAAS,IAAI,CAAC;EACvE,CAAC;EAED,IAAI,OAAO,SAAS;GAClB,QAAQ,OAAO,MAAM,GAAG,YAAY,MAAM,EAAE,GAAG;GAC/C,OAAO;EACT;EAEA,IAAI,OAAO,WAAW,QACpB,QAAQ,OAAO,MACb,GAAG,KAAK,UAAU,OAAO,mBAAmB,MAAM,CAAC,EAAE,GACvD;OAEA,QAAQ,OAAO,MAAM,GAAG,YAAY,MAAM,EAAE,GAAG;EAEjD,OAAO;CACT,SAAS,KAAK;EACZ,QAAQ,OAAO,MAAM,GAAG,cAAc,GAAG,EAAE,GAAG;EAC9C,OAAO;CACT;AACF"}
@@ -14,12 +14,24 @@ import DOMPurify from "dompurify";
14
14
  import TurndownService from "turndown";
15
15
  import { gfm } from "turndown-plugin-gfm";
16
16
  //#region ../core/dist/index.js
17
- function parseQuietly(program, args, recover) {
17
+ var HelpRequested = class extends Error {
18
+ command;
19
+ constructor(command) {
20
+ super(`${command.name()} --help`);
21
+ this.command = command;
22
+ }
23
+ };
24
+ function parseQuietly(program, args) {
18
25
  function quiet(command) {
19
- command.allowExcessArguments(false).exitOverride().configureOutput({
26
+ command.configureOutput({
20
27
  writeOut: () => void 0,
21
28
  writeErr: () => void 0
22
29
  });
30
+ command.exitOverride((err) => {
31
+ const code = err.code;
32
+ if (code === "commander.help" || code === "commander.helpDisplayed") throw new HelpRequested(command);
33
+ throw err;
34
+ });
23
35
  command.commands.forEach(quiet);
24
36
  }
25
37
  try {
@@ -27,7 +39,7 @@ function parseQuietly(program, args, recover) {
27
39
  program.parse([...args], { from: "user" });
28
40
  return program;
29
41
  } catch (err) {
30
- return recover?.(err);
42
+ return err instanceof HelpRequested ? err : void 0;
31
43
  }
32
44
  }
33
45
  var LEVEL_RANK = {
@@ -37,9 +49,6 @@ var LEVEL_RANK = {
37
49
  error: 40,
38
50
  silent: Number.MAX_SAFE_INTEGER
39
51
  };
40
- function levelEnabled(active, wanted) {
41
- return LEVEL_RANK[wanted] >= LEVEL_RANK[active];
42
- }
43
52
  var LEVEL_LABEL = {
44
53
  debug: "DEBUG",
45
54
  error: "ERROR",
@@ -66,7 +75,7 @@ var Logger = class {
66
75
  this.log("warn", message);
67
76
  }
68
77
  log(level, message) {
69
- if (levelEnabled(this.activeLevel, level)) this.stderr(`${LEVEL_LABEL[level]} ${message}`);
78
+ if (LEVEL_RANK[level] >= LEVEL_RANK[this.activeLevel]) this.stderr(`${LEVEL_LABEL[level]} ${message}`);
70
79
  }
71
80
  };
72
81
  /** Close each closable in order (best-effort), then force-exit. */
@@ -107,7 +116,7 @@ function toErrorResult(err) {
107
116
  }
108
117
  var package_default = {
109
118
  name: "@v1nvn/readability",
110
- version: "0.41.1",
119
+ version: "0.43.0",
111
120
  description: "MCP server that turns rendered (post-JS) HTML into clean Markdown + metadata via Readability, Turndown, and DOMPurify.",
112
121
  type: "module",
113
122
  main: "dist/index.js",
@@ -2866,6 +2875,6 @@ function registerExtractTool(server) {
2866
2875
  }, extractHandler);
2867
2876
  }
2868
2877
  //#endregion
2869
- export { resolveLazyImages as $, selectorsSchema as A, absolutize as B, extractTablesInputSchema as C, localPathField as D, htmlToMarkdownInputShape as E, detectGating as F, sanitizeHtml as G, renderTable as H, TraceCollector as I, computeTextMetrics as J, isReaderable as K, assembleDiagnostics as L, truncateMarkdown as M, detectPagination as N, outlineInputSchema as O, resolveMetadata as P, normalizeDocument as Q, chunkMarkdown as R, extractSectionInputShape as S, htmlToMarkdownInputSchema as T, resolveHeaderKeys as U, parseTableMatrix as V, resolveCellText as W, nonEmpty as X, countWords as Y, applySelectors as Z, extractListInputSchema as _, extractLinksOutputShape as a, normalizeSiteKey as at, extractMetadataInputShape as b, extractTablesOutputShape as c, selectorMisses as ct, chunkTextInputSchema as d, ExtractionError as dt, resolveReadabilityOptions as et, chunkTextInputShape as f, describeError as ft, extractLinksInputShape as g, extractLinksInputSchema as h, shutdownOnSignals as ht, extractGridOutputShape as i, addPreset as it, readHtmlFile as j, outlineInputShape as k, outlineOutputShape as l, logger as lt, extractGridInputShape as m, parseQuietly as mt, registerExtractTool as n, isElement as nt, extractListOutputShape as o, presetForSite as ot, extractGridInputSchema as p, toErrorResult as pt, formatPayload as q, chunkTextOutputShape as r, registerResources as rt, extractMetadataOutputShape as s, removePreset as st, extractArticleFromHtml as t, buildDocument as tt, outputSchemaShape as u, loadConfig as ut, extractListInputShape as v, extractTablesInputShape as w, extractSectionInputSchema as x, extractMetadataInputSchema as y, toMarkdown as z };
2878
+ export { resolveLazyImages as $, selectorsSchema as A, absolutize as B, extractTablesInputSchema as C, localPathField as D, htmlToMarkdownInputShape as E, detectGating as F, sanitizeHtml as G, renderTable as H, TraceCollector as I, computeTextMetrics as J, isReaderable as K, assembleDiagnostics as L, truncateMarkdown as M, detectPagination as N, outlineInputSchema as O, resolveMetadata as P, normalizeDocument as Q, chunkMarkdown as R, extractSectionInputShape as S, htmlToMarkdownInputSchema as T, resolveHeaderKeys as U, parseTableMatrix as V, resolveCellText as W, nonEmpty as X, countWords as Y, applySelectors as Z, extractListInputSchema as _, extractLinksOutputShape as a, normalizeSiteKey as at, extractMetadataInputShape as b, extractTablesOutputShape as c, selectorMisses as ct, chunkTextInputSchema as d, ExtractionError as dt, resolveReadabilityOptions as et, chunkTextInputShape as f, describeError as ft, extractLinksInputShape as g, shutdownOnSignals as gt, extractLinksInputSchema as h, parseQuietly as ht, extractGridOutputShape as i, addPreset as it, readHtmlFile as j, outlineInputShape as k, outlineOutputShape as l, logger as lt, extractGridInputShape as m, HelpRequested as mt, registerExtractTool as n, isElement as nt, extractListOutputShape as o, presetForSite as ot, extractGridInputSchema as p, toErrorResult as pt, formatPayload as q, chunkTextOutputShape as r, registerResources as rt, extractMetadataOutputShape as s, removePreset as st, extractArticleFromHtml as t, buildDocument as tt, outputSchemaShape as u, loadConfig as ut, extractListInputShape as v, extractTablesInputShape as w, extractSectionInputSchema as x, extractMetadataInputSchema as y, toMarkdown as z };
2870
2879
 
2871
- //# sourceMappingURL=extract-Cn_gf58d.js.map
2880
+ //# sourceMappingURL=extract-Cyv5qfGN.js.map