@v1nvn/readability-mcp 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{cli-DLiDHTsY.js → cli-DjMqCuj9.js} +2 -2
- package/dist/assets/{cli-DLiDHTsY.js.map → cli-DjMqCuj9.js.map} +1 -1
- package/dist/assets/{extract-qa1jFe69.js → extract-D-E1pCsL.js} +2 -2
- package/dist/assets/{extract-qa1jFe69.js.map → extract-D-E1pCsL.js.map} +1 -1
- package/dist/index.js +2 -2
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { j as readHtmlFile, t as extractArticleFromHtml } from "./extract-
|
|
1
|
+
import { j as readHtmlFile, t as extractArticleFromHtml } from "./extract-D-E1pCsL.js";
|
|
2
2
|
import "node:fs";
|
|
3
3
|
import "node:os";
|
|
4
4
|
import "node:path";
|
|
@@ -84,4 +84,4 @@ async function runCli(argv) {
|
|
|
84
84
|
//#endregion
|
|
85
85
|
export { runCli };
|
|
86
86
|
|
|
87
|
-
//# sourceMappingURL=cli-
|
|
87
|
+
//# sourceMappingURL=cli-DjMqCuj9.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-DLiDHTsY.js","names":[],"sources":["../../../core/dist/index.js","../../src/cli.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n//#region src/hook.ts\n/**\n* Read the Claude Code hook event JSON from stdin; empty or unparseable input\n* yields an empty event, never a thrown error — a hook must always answer.\n*/\nfunction readHookEvent(stream = process.stdin) {\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tstream.setEncoding(\"utf8\");\n\t\tstream.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tstream.on(\"end\", () => {\n\t\t\ttry {\n\t\t\t\tresolve(JSON.parse(data));\n\t\t\t} catch {\n\t\t\t\tresolve({});\n\t\t\t}\n\t\t});\n\t\tstream.on(\"error\", () => {\n\t\t\tresolve({});\n\t\t});\n\t});\n}\n/**\n* Which transcript lastReply should read, per the hook event: transcript_path\n* when it names a real file, else session_id, else nothing (lastReply then\n* falls back to the newest session for the current project).\n*/\nfunction replyTarget(event) {\n\tif (event.transcript_path !== void 0 && isFile$1(event.transcript_path)) return event.transcript_path;\n\treturn event.session_id || void 0;\n}\nfunction isFile$1(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n/**\n* Print the UserPromptExpansion decision for a zero-token command: `block`\n* keeps the command from reaching the model, with the output as the `reason`.\n*/\nfunction emitHookBlock(reason) {\n\tprocess.stdout.write(`${JSON.stringify({\n\t\tdecision: \"block\",\n\t\treason\n\t})}\\n`);\n}\n//#endregion\n//#region src/cli.ts\nfunction parseQuietly(program, args) {\n\ttry {\n\t\tprogram.allowExcessArguments(false).exitOverride().configureOutput({\n\t\t\twriteOut: () => void 0,\n\t\t\twriteErr: () => void 0\n\t\t}).parse([...args], { from: \"user\" });\n\t\treturn program;\n\t} catch {\n\t\treturn;\n\t}\n}\nfunction printUsageAndExit(program) {\n\tconsole.error(program.helpInformation());\n\tprocess.exit(1);\n}\nasync function hookOrPrint(hook, failure, run, stdin = process.stdin) {\n\tif (hook) {\n\t\tlet reason;\n\t\ttry {\n\t\t\treason = await run(await readHookEvent(stdin));\n\t\t} catch (e) {\n\t\t\treason = `${failure}: ${e.message}`;\n\t\t}\n\t\temitHookBlock(reason);\n\t\treturn;\n\t}\n\ttry {\n\t\tconsole.log(await run(void 0));\n\t} catch (e) {\n\t\tconsole.error(e.message);\n\t\tprocess.exit(1);\n\t}\n}\n//#endregion\n//#region src/last-reply.ts\nfunction isFile(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\nfunction textBlocks(entry) {\n\tconst content = entry.message?.content;\n\tif (entry.type !== \"assistant\" || !Array.isArray(content)) return;\n\tconst blocks = content.filter((b) => typeof b === \"object\" && b !== null && b.type === \"text\");\n\treturn blocks.length > 0 ? blocks : void 0;\n}\n/**\n* The last assistant text reply from a Claude Code session, reproducing Claude\n* Code's `/copy` byte-for-byte: the last assistant entry that contains a\n* `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n* joined with a blank line, and no trailing newline.\n*\n* @param arg a transcript file, a session UUID under the project dir, or\n* nothing to use the newest session for the current project.\n*/\nfunction lastReply(arg) {\n\tconst claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), \".claude\");\n\tconst proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\"/\", \"-\");\n\tconst projDir = join(claudeDir, \"projects\", proj);\n\tlet file;\n\tif (arg !== void 0) file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n\telse if (existsSync(projDir)) {\n\t\tconst newest = readdirSync(projDir).filter((f) => f.endsWith(\".jsonl\")).map((f) => ({\n\t\t\tf,\n\t\t\tmtimeMs: statSync(join(projDir, f)).mtimeMs\n\t\t})).sort((a, b) => b.mtimeMs - a.mtimeMs).at(0);\n\t\tif (newest) file = join(projDir, newest.f);\n\t}\n\tif (file === void 0 || !isFile(file)) throw new Error(`no session transcript found in ${projDir}`);\n\tlet last;\n\tfor (const line of readFileSync(file, \"utf8\").split(\"\\n\")) {\n\t\tif (!line.includes(\"\\\"assistant\\\"\")) continue;\n\t\tlet entry;\n\t\ttry {\n\t\t\tentry = JSON.parse(line);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tconst blocks = textBlocks(entry);\n\t\tif (blocks) last = blocks;\n\t}\n\treturn (last ?? []).map((b) => b.text ?? \"\").join(\"\\n\\n\");\n}\n//#endregion\n//#region src/text-format.ts\n/**\n* Plain-text rendering primitives shared by the zai and tokens report\n* renderers. Output targets a monospace terminal / hook-block `reason`, so\n* everything here is fixed-width: padding, block-glyph bars, and compact\n* number formatting.\n*/\nvar MONTHS = [\n\t\"Jan\",\n\t\"Feb\",\n\t\"Mar\",\n\t\"Apr\",\n\t\"May\",\n\t\"Jun\",\n\t\"Jul\",\n\t\"Aug\",\n\t\"Sep\",\n\t\"Oct\",\n\t\"Nov\",\n\t\"Dec\"\n];\nvar EIGHTHS = [\n\t\"\",\n\t\"▏\",\n\t\"▎\",\n\t\"▍\",\n\t\"▌\",\n\t\"▋\",\n\t\"▊\",\n\t\"▉\"\n];\nfunction fmtTokens(n) {\n\tif (n == null || Number.isNaN(n)) return \"—\";\n\tif (n >= 1e9) return (n / 1e9).toFixed(1) + \"B\";\n\tif (n >= 1e6) return (n / 1e6).toFixed(1) + \"M\";\n\tif (n >= 1e3) return (n / 1e3).toFixed(1) + \"K\";\n\treturn String(n);\n}\nfunction fmtNum(n) {\n\treturn (n || 0).toLocaleString(\"en-US\");\n}\nfunction padR(s, n) {\n\treturn s.length >= n ? s : s + \" \".repeat(n - s.length);\n}\nfunction padL(s, n) {\n\treturn s.length >= n ? s : \" \".repeat(n - s.length) + s;\n}\n/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */\nfunction barField(v, max, width) {\n\tif (!v || v <= 0 || max <= 0) return \" \".repeat(width);\n\tconst scaled = v / max * width;\n\tlet full = Math.floor(scaled);\n\tlet fi = Math.round((scaled - full) * 8);\n\tif (fi === 8) {\n\t\tfull += 1;\n\t\tfi = 0;\n\t}\n\tif (full === 0 && fi === 0) fi = 1;\n\tlet s = \"█\".repeat(Math.min(full, width));\n\tif (full < width && fi > 0) s += EIGHTHS[fi];\n\tif (s.length < width) s += \" \".repeat(width - s.length);\n\treturn s.slice(0, width);\n}\n/** Filled/empty meter: █ for used, ░ for remaining. */\nfunction meter(pct, width) {\n\tlet filled = Math.round((pct || 0) / 100 * width);\n\tfilled = Math.max(0, Math.min(width, filled));\n\treturn \"█\".repeat(filled) + \"░\".repeat(width - filled);\n}\n//#endregion\nexport { MONTHS, barField, emitHookBlock, fmtNum, fmtTokens, hookOrPrint, lastReply, meter, padL, padR, parseQuietly, printUsageAndExit, readHookEvent, replyTarget };\n\n//# sourceMappingURL=index.js.map","import { parseQuietly } from '@v1nvn/agentic-core';\nimport { Command, InvalidArgumentError, Option } from 'commander';\n\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-mcp 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(argv: readonly string[]): ParsedArgs | undefined {\n const program = parseQuietly(buildProgram(), argv.slice(1));\n if (program === undefined) {\n return undefined;\n }\n const { format, maxChars } = program.opts<{\n format: CliFormat;\n maxChars: number | undefined;\n }>();\n return { file: program.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 === 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(\n `${err instanceof Error ? err.message : String(err)}\\n`,\n );\n return 1;\n }\n}\n"],"mappings":";;;;;;AAuDA,SAAS,aAAa,SAAS,MAAM;CACpC,IAAI;EACH,QAAQ,qBAAqB,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,gBAAgB;GAClE,gBAAgB,KAAK;GACrB,gBAAgB,KAAK;EACtB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,EAAE,MAAM,OAAO,CAAC;EACpC,OAAO;CACR,QAAQ;EACP;CACD;AACD;;;AChDA,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,yBAAyB,CAAC,CAC1C,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,UAAU,MAAiD;CACzE,MAAM,UAAU,aAAa,aAAa,GAAG,KAAK,MAAM,CAAC,CAAC;CAC1D,IAAI,YAAY,KAAA,GACd;CAEF,MAAM,EAAE,QAAQ,aAAa,QAAQ,KAGlC;CACH,OAAO;EAAE,MAAM,QAAQ,KAAK,GAAG,CAAC;EAAG;EAAQ;CAAS;AACtD;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,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,MACb,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GACtD;EACA,OAAO;CACT;AACF"}
|
|
1
|
+
{"version":3,"file":"cli-DjMqCuj9.js","names":[],"sources":["../../../core/dist/index.js","../../src/cli.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n//#region src/hook.ts\n/**\n* Read the Claude Code hook event JSON from stdin; empty or unparseable input\n* yields an empty event, never a thrown error — a hook must always answer.\n*/\nfunction readHookEvent(stream = process.stdin) {\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tstream.setEncoding(\"utf8\");\n\t\tstream.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tstream.on(\"end\", () => {\n\t\t\ttry {\n\t\t\t\tresolve(JSON.parse(data));\n\t\t\t} catch {\n\t\t\t\tresolve({});\n\t\t\t}\n\t\t});\n\t\tstream.on(\"error\", () => {\n\t\t\tresolve({});\n\t\t});\n\t});\n}\n/**\n* Which transcript lastReply should read, per the hook event: transcript_path\n* when it names a real file, else session_id, else nothing (lastReply then\n* falls back to the newest session for the current project).\n*/\nfunction replyTarget(event) {\n\tif (event.transcript_path !== void 0 && isFile$1(event.transcript_path)) return event.transcript_path;\n\treturn event.session_id || void 0;\n}\nfunction isFile$1(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n/**\n* Print the UserPromptExpansion decision for a zero-token command: `block`\n* keeps the command from reaching the model, with the output as the `reason`.\n*/\nfunction emitHookBlock(reason) {\n\tprocess.stdout.write(`${JSON.stringify({\n\t\tdecision: \"block\",\n\t\treason\n\t})}\\n`);\n}\n//#endregion\n//#region src/cli.ts\nfunction parseQuietly(program, args) {\n\ttry {\n\t\tprogram.allowExcessArguments(false).exitOverride().configureOutput({\n\t\t\twriteOut: () => void 0,\n\t\t\twriteErr: () => void 0\n\t\t}).parse([...args], { from: \"user\" });\n\t\treturn program;\n\t} catch {\n\t\treturn;\n\t}\n}\nfunction printUsageAndExit(program) {\n\tconsole.error(program.helpInformation());\n\tprocess.exit(1);\n}\nasync function hookOrPrint(hook, failure, run, stdin = process.stdin) {\n\tif (hook) {\n\t\tlet reason;\n\t\ttry {\n\t\t\treason = await run(await readHookEvent(stdin));\n\t\t} catch (e) {\n\t\t\treason = `${failure}: ${e.message}`;\n\t\t}\n\t\temitHookBlock(reason);\n\t\treturn;\n\t}\n\ttry {\n\t\tconsole.log(await run(void 0));\n\t} catch (e) {\n\t\tconsole.error(e.message);\n\t\tprocess.exit(1);\n\t}\n}\n//#endregion\n//#region src/last-reply.ts\nfunction isFile(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\nfunction textBlocks(entry) {\n\tconst content = entry.message?.content;\n\tif (entry.type !== \"assistant\" || !Array.isArray(content)) return;\n\tconst blocks = content.filter((b) => typeof b === \"object\" && b !== null && b.type === \"text\");\n\treturn blocks.length > 0 ? blocks : void 0;\n}\n/**\n* The last assistant text reply from a Claude Code session, reproducing Claude\n* Code's `/copy` byte-for-byte: the last assistant entry that contains a\n* `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n* joined with a blank line, and no trailing newline.\n*\n* @param arg a transcript file, a session UUID under the project dir, or\n* nothing to use the newest session for the current project.\n*/\nfunction lastReply(arg) {\n\tconst claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), \".claude\");\n\tconst proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\"/\", \"-\");\n\tconst projDir = join(claudeDir, \"projects\", proj);\n\tlet file;\n\tif (arg !== void 0) file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n\telse if (existsSync(projDir)) {\n\t\tconst newest = readdirSync(projDir).filter((f) => f.endsWith(\".jsonl\")).map((f) => ({\n\t\t\tf,\n\t\t\tmtimeMs: statSync(join(projDir, f)).mtimeMs\n\t\t})).sort((a, b) => b.mtimeMs - a.mtimeMs).at(0);\n\t\tif (newest) file = join(projDir, newest.f);\n\t}\n\tif (file === void 0 || !isFile(file)) throw new Error(`no session transcript found in ${projDir}`);\n\tlet last;\n\tfor (const line of readFileSync(file, \"utf8\").split(\"\\n\")) {\n\t\tif (!line.includes(\"\\\"assistant\\\"\")) continue;\n\t\tlet entry;\n\t\ttry {\n\t\t\tentry = JSON.parse(line);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tconst blocks = textBlocks(entry);\n\t\tif (blocks) last = blocks;\n\t}\n\treturn (last ?? []).map((b) => b.text ?? \"\").join(\"\\n\\n\");\n}\n//#endregion\n//#region src/text-format.ts\n/**\n* Plain-text rendering primitives shared by the zai and tokens report\n* renderers. Output targets a monospace terminal / hook-block `reason`, so\n* everything here is fixed-width: padding, block-glyph bars, and compact\n* number formatting.\n*/\nvar MONTHS = [\n\t\"Jan\",\n\t\"Feb\",\n\t\"Mar\",\n\t\"Apr\",\n\t\"May\",\n\t\"Jun\",\n\t\"Jul\",\n\t\"Aug\",\n\t\"Sep\",\n\t\"Oct\",\n\t\"Nov\",\n\t\"Dec\"\n];\nvar EIGHTHS = [\n\t\"\",\n\t\"▏\",\n\t\"▎\",\n\t\"▍\",\n\t\"▌\",\n\t\"▋\",\n\t\"▊\",\n\t\"▉\"\n];\nfunction fmtTokens(n) {\n\tif (n == null || Number.isNaN(n)) return \"—\";\n\tif (n >= 1e9) return (n / 1e9).toFixed(1) + \"B\";\n\tif (n >= 1e6) return (n / 1e6).toFixed(1) + \"M\";\n\tif (n >= 1e3) return (n / 1e3).toFixed(1) + \"K\";\n\treturn String(n);\n}\nfunction fmtNum(n) {\n\treturn (n || 0).toLocaleString(\"en-US\");\n}\nfunction padR(s, n) {\n\treturn s.length >= n ? s : s + \" \".repeat(n - s.length);\n}\nfunction padL(s, n) {\n\treturn s.length >= n ? s : \" \".repeat(n - s.length) + s;\n}\n/** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */\nfunction barField(v, max, width) {\n\tif (!v || v <= 0 || max <= 0) return \" \".repeat(width);\n\tconst scaled = v / max * width;\n\tlet full = Math.floor(scaled);\n\tlet fi = Math.round((scaled - full) * 8);\n\tif (fi === 8) {\n\t\tfull += 1;\n\t\tfi = 0;\n\t}\n\tif (full === 0 && fi === 0) fi = 1;\n\tlet s = \"█\".repeat(Math.min(full, width));\n\tif (full < width && fi > 0) s += EIGHTHS[fi];\n\tif (s.length < width) s += \" \".repeat(width - s.length);\n\treturn s.slice(0, width);\n}\n/** Filled/empty meter: █ for used, ░ for remaining. */\nfunction meter(pct, width) {\n\tlet filled = Math.round((pct || 0) / 100 * width);\n\tfilled = Math.max(0, Math.min(width, filled));\n\treturn \"█\".repeat(filled) + \"░\".repeat(width - filled);\n}\n//#endregion\nexport { MONTHS, barField, emitHookBlock, fmtNum, fmtTokens, hookOrPrint, lastReply, meter, padL, padR, parseQuietly, printUsageAndExit, readHookEvent, replyTarget };\n\n//# sourceMappingURL=index.js.map","import { parseQuietly } from '@v1nvn/agentic-core';\nimport { Command, InvalidArgumentError, Option } from 'commander';\n\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-mcp 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(argv: readonly string[]): ParsedArgs | undefined {\n const program = parseQuietly(buildProgram(), argv.slice(1));\n if (program === undefined) {\n return undefined;\n }\n const { format, maxChars } = program.opts<{\n format: CliFormat;\n maxChars: number | undefined;\n }>();\n return { file: program.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 === 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(\n `${err instanceof Error ? err.message : String(err)}\\n`,\n );\n return 1;\n }\n}\n"],"mappings":";;;;;;AAuDA,SAAS,aAAa,SAAS,MAAM;CACpC,IAAI;EACH,QAAQ,qBAAqB,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,gBAAgB;GAClE,gBAAgB,KAAK;GACrB,gBAAgB,KAAK;EACtB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,EAAE,MAAM,OAAO,CAAC;EACpC,OAAO;CACR,QAAQ;EACP;CACD;AACD;;;AChDA,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,yBAAyB,CAAC,CAC1C,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,UAAU,MAAiD;CACzE,MAAM,UAAU,aAAa,aAAa,GAAG,KAAK,MAAM,CAAC,CAAC;CAC1D,IAAI,YAAY,KAAA,GACd;CAEF,MAAM,EAAE,QAAQ,aAAa,QAAQ,KAGlC;CACH,OAAO;EAAE,MAAM,QAAQ,KAAK,GAAG,CAAC;EAAG;EAAQ;CAAS;AACtD;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,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,MACb,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GACtD;EACA,OAAO;CACT;AACF"}
|
|
@@ -13,7 +13,7 @@ import TurndownService from "turndown";
|
|
|
13
13
|
import { gfm } from "turndown-plugin-gfm";
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "@v1nvn/readability-mcp",
|
|
16
|
-
version: "0.
|
|
16
|
+
version: "0.22.0",
|
|
17
17
|
description: "MCP server that turns rendered (post-JS) HTML into clean Markdown + metadata via Readability, Turndown, and DOMPurify.",
|
|
18
18
|
type: "module",
|
|
19
19
|
main: "dist/index.js",
|
|
@@ -2846,4 +2846,4 @@ function registerExtractTool(server) {
|
|
|
2846
2846
|
//#endregion
|
|
2847
2847
|
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, registerResources as at, extractMetadataInputShape as b, extractTablesOutputShape as c, presetForSite as ct, chunkTextInputSchema as d, logger as dt, resolveReadabilityOptions as et, chunkTextInputShape as f, loadConfig as ft, extractLinksInputShape as g, extractLinksInputSchema as h, extractGridOutputShape as i, toErrorResult as it, readHtmlFile as j, outlineInputShape as k, outlineOutputShape as l, removePreset as lt, extractGridInputShape as m, registerExtractTool as n, isElement as nt, extractListOutputShape as o, addPreset as ot, extractGridInputSchema as p, formatPayload as q, chunkTextOutputShape as r, ExtractionError as rt, extractMetadataOutputShape as s, normalizeSiteKey as st, extractArticleFromHtml as t, buildDocument as tt, outputSchemaShape as u, selectorMisses as ut, extractListInputShape as v, extractTablesInputShape as w, extractSectionInputSchema as x, extractMetadataInputSchema as y, toMarkdown as z };
|
|
2848
2848
|
|
|
2849
|
-
//# sourceMappingURL=extract-
|
|
2849
|
+
//# sourceMappingURL=extract-D-E1pCsL.js.map
|