From 7b730631b2e1c1f9a5e96b70b0d02a3728301ecd Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 30 Apr 2026 16:13:17 -0400 Subject: [PATCH 01/76] docs: README with usage, architecture, and dependency info --- README.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7c56c4c..b2f0c47 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,120 @@ # lmc -To install dependencies: +A Bun-based CLI tool for LLM chats with terminal native streaming. + +## Features + +- **Streaming markdown rendering** — incremental rendering with syntax highlighting +- **Conversation persistence** — chat history saved to JSON +- **Model configuration** — persist model preferences to JSON +- **Reasoning display** — thinking tokens rendered _in style_ on stderr +- **Usage stats** — input/output tokens and elapsed time after each response +- **TTY-aware** — automatically disables markdown rendering when output is piped +- **Text-to-speech** — `ttsc` sub-command for streaming TTS via Kokoro model + +## Requirements + +- [Bun](https://bun.com) v1.3+ +- [uv](https://docs.astral.sh/uv/) — only for `ttsc` (text-to-speech) + +## Installation ```bash bun install +bun build:js +bun link + +# For ttsc only +cp -r tts ~/.ttsc ``` -To run: +_Package to come later_ + +## Usage + +### Chat (lmc) ```bash -bun run index.ts +lmc [options] ``` -This project was created using `bun init` in bun v1.3.11. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +| Flag | Short | Type | Description | +| ------------ | ----- | ------- | ------------------------------------------------- | +| `--new` | `-n` | boolean | Start a new conversation (clear history) | +| `--model` | `-m` | string | Set model name for this session | +| `--save` | `-s` | boolean | Save the specified model (--model) as the default | +| `--plain` | — | boolean | Disable markdown rendering (plain text output) | +| `--plainOut` | — | boolean | Disable markdown rendering for stdout only | +| `--plainErr` | — | boolean | Disable markdown rendering for stderr only | + +**Examples:** + +```bash +lmc --new "Introduce yourself." # New conversation +lmc "What is TypeScript?" # Continue most recent chat +lmc -m llama3 "Explain quantum computing" # Chat with specific model +lmc --model llama3 --save # Set default model +lmc --plain "Plain text output" # No markdown rendering +``` + +### Text-to-Speech (ttsc) + +```bash +echo "text" | ttsc [options] +``` + +| Flag | Short | Type | Description | +| -------- | ----- | ------- | -------------------------------------------------------- | +| `--wait` | `-w` | boolean | Wait for all processing before exiting (don't auto-play) | + +**Examples:** + +```bash +echo "Hello, world!" | ttsc +echo "Read this aloud" | ttsc --wait +``` + +Requires: [uv](https://docs.astral.sh/uv/) + +Wants: + +- `mpv` for playback +- `sox` for wav merging with `--wait` + +## Development + +```bash +bun run dev # dev run +bun run prod # run bundled output +bun run check # biome lint + format + organize imports +``` + +## Architecture + +``` +index.ts — LMC entry point, orchestration +tts.ts — TTSC entry point, orchestration +src/render/index.ts — Renderer class: streams LLM response, buffers markdown +src/render/markdown.ts — renderMarkdown(): syntax-highlighted markdown via Bun.markdown + Shiki +src/conversation/ — Conversation persistence +src/config/ — App config persistence +tts/ — TTS python stub (Kokoro pipeline) +out/ — Bundled output (index.js, tts.js) +``` + +## Configuration + +- **Conversation history:** `~/.lmc_convo.json` +- **Model config:** `~/.lmc_config.json` +- **TTS files:** `~/.ttsc/` + +## Dependencies + +| Package | Purpose | +| --------------------------- | ----------------------------------------- | +| `ai` (Vercel AI SDK) | Streaming text/usage | +| `@ai-sdk/openai-compatible` | OpenAI-compatible API client | +| `shiki` | Syntax highlighting (kanagawa-wave theme) | +| `cli-table3` | ASCII table rendering | +| `chalk` | Terminal string styling | +| `terminal-link` | Clickable terminal hyperlinks | From 94895aacd59a1df893396733508398467b45ffb8 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 1 May 2026 21:23:21 -0400 Subject: [PATCH 02/76] style: improve codespan styling and checkbox markers --- src/render/markdown.ts | 4 ++-- testRender.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index b7f5c7e..258cdfe 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -77,7 +77,7 @@ export async function renderMarkdown( const indent = " ".repeat(depth); let marker = ""; if (checked !== undefined) { - marker = checked ? "[✓]" : "[ ]"; + marker = checked ? "• [✓]" : "• [ ]"; } else if (ordered) { const n = (start ?? 1) + index; marker = `${n}.`; @@ -104,7 +104,7 @@ export async function renderMarkdown( strong: (children) => chalk.bold(children), emphasis: (children) => chalk.italic(children), strikethrough: (children) => chalk.grey.strikethrough(children), - codespan: (children) => chalk.bgWhite.black(` ${children} `), + codespan: (children) => chalk.bgBlack.yellowBright(` ${children} `), link: (children, { href }) => chalk.blueBright.underline(terminalLink(children, href)), image: () => "", diff --git a/testRender.ts b/testRender.ts index a31dfe4..6373337 100644 --- a/testRender.ts +++ b/testRender.ts @@ -1,6 +1,6 @@ import { renderMarkdown } from "./src/render/markdown.ts"; -const text = await Bun.file("test.md").text(); +const text = await Bun.file("README.md").text(); const [columns] = process.stdout.getWindowSize(); process.stdout.write(await renderMarkdown(text, columns)); From f5c8bdfe994e2f655d1f07458604df288e6733ad Mon Sep 17 00:00:00 2001 From: mitchell Date: Sat, 2 May 2026 02:08:13 -0400 Subject: [PATCH 03/76] refactor: conversation into a class --- index.ts | 23 ++++++----------------- src/conversation/index.ts | 37 ++++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/index.ts b/index.ts index e30be42..abe7d23 100644 --- a/index.ts +++ b/index.ts @@ -3,11 +3,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; import { loadConfig, saveConfig } from "./src/config"; -import { - type Conversation, - loadConversation, - saveConversation, -} from "./src/conversation"; +import { Conversation } from "./src/conversation"; import Renderer from "./src/render"; const { values: args, positionals } = parseArgs({ @@ -36,8 +32,6 @@ async function main() { const config = await loadConfig(); - config.model = modelFlag ?? config.model; - if (!config.model) { console.error( "Please run 'lmc --model --save' to set a default model.", @@ -46,6 +40,7 @@ async function main() { } if (saveConfigFlag) { + config.model = modelFlag ?? config.model; await saveConfig(config); return; } @@ -59,17 +54,11 @@ async function main() { includeUsage: true, // Include usage information in streaming responses }); - let convo: Conversation = { - model: config.model, - messages: [], - }; + const convo = new Conversation(modelFlag ?? config.model); if (!newConvo) { - convo = await loadConversation(config.model); - - if (args.model || args.m) { - convo.model = modelFlag ?? config.model; - } + await convo.load(); + if (modelFlag) convo.model = modelFlag; } const prompt = positionals.slice(2).join(" ") || "Introduce yourself."; @@ -85,7 +74,7 @@ async function main() { const stop = Date.now(); convo.messages.push({ role: "assistant", content: await text }); - await saveConversation(convo); + await convo.save(); await renderer.renderStats(totalUsage, start, stop); } diff --git a/src/conversation/index.ts b/src/conversation/index.ts index 557e6b5..9fb01f4 100644 --- a/src/conversation/index.ts +++ b/src/conversation/index.ts @@ -2,23 +2,34 @@ import os from "node:os"; import type { ModelMessage } from "ai"; const home = os.homedir(); -export const convoFile = Bun.file(`${home}/.lmc_convo.json`); +const convoFile = Bun.file(`${home}/.lmc_convo.json`); -export interface Conversation { +export interface IConversation { model: string; messages: ModelMessage[]; } -export async function loadConversation( - defaultModel: string, -): Promise { - try { - return await convoFile.json(); - } catch { - return { model: defaultModel, messages: [] }; +export class Conversation implements IConversation { + messages: ModelMessage[] = []; + + constructor(public model: string) {} + + async load() { + try { + const convo: IConversation = await convoFile.json(); + this.model = convo.model; + this.messages = convo.messages; + } catch { + console.error("No conversation found, starting one."); + } + } + + async save() { + await convoFile.write( + JSON.stringify({ + model: this.model, + messages: this.messages, + } satisfies IConversation), + ); } } - -export async function saveConversation(convo: Conversation): Promise { - await convoFile.write(JSON.stringify(convo)); -} From 57acb09a188704dc2080a4147aad5dbe67b326b9 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sat, 2 May 2026 22:22:09 -0400 Subject: [PATCH 04/76] feat(tts): add processing duration timing --- tts.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tts.ts b/tts.ts index 16f8b8b..0ee0b0f 100644 --- a/tts.ts +++ b/tts.ts @@ -3,6 +3,8 @@ import { parseArgs } from "node:util"; import { $, which } from "bun"; import chalk from "chalk"; +const start = Date.now(); + const { values: args } = parseArgs({ args: Bun.argv, options: { @@ -43,9 +45,14 @@ const koko = Bun.spawn({ }); if (!wait) { - koko.exited.then(() => console.log(chalk.green("\nProcessing finished."))); + koko.exited.then(() => { + const stop = Date.now(); + const duration = ((stop - start) / 1000).toFixed(2); + console.log(chalk.green(`\nProcessing finished: ${duration}s`)); + }); } +let first = true; const decoder = new TextDecoder(); for await (const line of koko.stdout) { const decoded = decoder.decode(line).trim(); @@ -58,9 +65,16 @@ for await (const line of koko.stdout) { continue; } + if (first) { + first = false; + const stop = Date.now(); + const duration = ((stop - start) / 1000).toFixed(2); + console.log(chalk.blue(`\nTime-to-audio: ${duration}s`)); + } + for (const info of infos) { const file = `${home}/.ttsc/${info.file}.wav`; - console.log(`\n${chalk.blue(file)}:\n${info.text}`); + console.log(`\n${chalk.blue(file)}:\n${info.text}\n`); if (!wait) { await Bun.spawn({ @@ -77,7 +91,9 @@ for await (const line of koko.stdout) { await koko.exited; if (!wait) process.exit(); -console.log(chalk.green("\nProcessing finished.")); +const stop = Date.now(); +const duration = ((stop - start) / 1000).toFixed(2); +console.log(chalk.green(`\nProcessing finished: ${duration}s`)); console.log(chalk.yellow(`Combining audio with sox...`)); if (which("sox")) { From 79fd0a93eb80ffa59804d77c7d41905b67e7ec66 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 3 May 2026 01:47:10 -0400 Subject: [PATCH 05/76] fix: model saving --- index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index abe7d23..7d6f0b6 100644 --- a/index.ts +++ b/index.ts @@ -24,15 +24,17 @@ const { values: args, positionals } = parseArgs({ }); async function main() { + const config = await loadConfig(); + const newConvo = args.new ?? args.n; const plainOut = args.plainOut ?? args.plain ?? !process.stdout.isTTY; const plainErr = args.plainErr ?? args.plain ?? !process.stderr.isTTY; const modelFlag = args.model ?? args.m; const saveConfigFlag = args.save ?? args.s; - const config = await loadConfig(); + const model = modelFlag ?? config.model; - if (!config.model) { + if (!model) { console.error( "Please run 'lmc --model --save' to set a default model.", ); @@ -54,7 +56,7 @@ async function main() { includeUsage: true, // Include usage information in streaming responses }); - const convo = new Conversation(modelFlag ?? config.model); + const convo = new Conversation(model); if (!newConvo) { await convo.load(); From d9bffdec2b6a165c9f1c423337b6c2fcd5942d48 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 3 May 2026 02:29:27 -0400 Subject: [PATCH 06/76] feat: spinner while waiting for response --- bun.lock | 13 +++++++++++++ package.json | 1 + src/render/index.ts | 9 +++++++++ 3 files changed, 23 insertions(+) diff --git a/bun.lock b/bun.lock index 6b1f630..5dfddb9 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "aicl", "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", + "@clack/prompts": "^1.3.0", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", @@ -49,6 +50,10 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], + "@clack/core": ["@clack/core@1.3.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA=="], + + "@clack/prompts": ["@clack/prompts@1.3.0", "", { "dependencies": { "@clack/core": "1.3.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -117,6 +122,12 @@ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], @@ -155,6 +166,8 @@ "shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/package.json b/package.json index 85b43af..4ff09f2 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", + "@clack/prompts": "^1.3.0", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", diff --git a/src/render/index.ts b/src/render/index.ts index 0e78648..4956b7d 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -1,4 +1,5 @@ import type { WriteStream } from "node:tty"; +import { spinner } from "@clack/prompts"; import type { AsyncIterableStream, LanguageModelUsage, @@ -87,9 +88,17 @@ export default class Renderer { async renderStream(stream: AsyncIterableStream>) { this.resetCounts(); + const spin = spinner({ withGuide: false, indicator: "timer" }); + if (!this.isPlain) { + spin.start("Waiting for response"); + } + for await (const part of stream) { switch (part.type) { case "reasoning-start": { + if (!this.isPlain) { + spin.clear(); + } if (supportsColorStderr) this.write( `${styles.dim.open}${styles.italic.open}`, From 3c9a5e625a27853c8dcde08a1e2198715d3ec316 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 3 May 2026 21:10:39 -0400 Subject: [PATCH 07/76] feat: add interactive config setup and baseURL support --- index.ts | 26 +++++++-------------- src/config/index.ts | 53 ++++++++++++++++++++++++++++++++++++------ src/render/index.ts | 3 +++ src/render/markdown.ts | 3 ++- 4 files changed, 60 insertions(+), 25 deletions(-) diff --git a/index.ts b/index.ts index 7d6f0b6..91a8ffd 100644 --- a/index.ts +++ b/index.ts @@ -2,7 +2,7 @@ import { parseArgs } from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; -import { loadConfig, saveConfig } from "./src/config"; +import { initConfig, loadConfig } from "./src/config"; import { Conversation } from "./src/conversation"; import Renderer from "./src/render"; @@ -13,8 +13,8 @@ const { values: args, positionals } = parseArgs({ m: { type: "string" }, new: { type: "boolean" }, n: { type: "boolean" }, - save: { type: "boolean" }, - s: { type: "boolean" }, + config: { type: "boolean" }, + c: { type: "boolean" }, plain: { type: "boolean" }, plainOut: { type: "boolean" }, plainErr: { type: "boolean" }, @@ -24,26 +24,18 @@ const { values: args, positionals } = parseArgs({ }); async function main() { - const config = await loadConfig(); - const newConvo = args.new ?? args.n; const plainOut = args.plainOut ?? args.plain ?? !process.stdout.isTTY; const plainErr = args.plainErr ?? args.plain ?? !process.stderr.isTTY; const modelFlag = args.model ?? args.m; - const saveConfigFlag = args.save ?? args.s; + const configFlag = args.config ?? args.c; + + const config = await loadConfig(); const model = modelFlag ?? config.model; - if (!model) { - console.error( - "Please run 'lmc --model --save' to set a default model.", - ); - process.exit(1); - } - - if (saveConfigFlag) { - config.model = modelFlag ?? config.model; - await saveConfig(config); + if (configFlag) { + await initConfig(config); return; } @@ -52,7 +44,7 @@ async function main() { const provider = createOpenAICompatible({ name: "llama.cpp", apiKey: "local", - baseURL: "http://127.0.0.1:8080/v1", + baseURL: config.baseURL, includeUsage: true, // Include usage information in streaming responses }); diff --git a/src/config/index.ts b/src/config/index.ts index 8281bf7..40664c9 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,11 +1,55 @@ import os from "node:os"; +import { autocomplete, intro, isCancel, outro, text } from "@clack/prompts"; import chalk from "chalk"; const home = os.homedir(); const configFile = Bun.file(`${home}/.lmc_config.json`); export interface AppConfig { - model?: string; + baseURL: string; + model: string; +} + +export async function initConfig(old?: AppConfig): Promise { + intro(chalk.blue("Welcome to LMC. Let's make a new config!")); + const baseURL = await text({ + message: "What's your base URL:", + initialValue: old?.baseURL, + defaultValue: "http://127.0.0.1:8080/v1", + placeholder: "http://127.0.0.1:8080/v1", + }); + if (isCancel(baseURL)) process.exit(1); + + const models = await getModels(baseURL); + const model = await autocomplete({ + message: "Choose a default model:", + initialValue: old?.model, + placeholder: old?.model, + options: models, + }); + if (isCancel(model)) process.exit(1); + + const config = { baseURL, model } satisfies AppConfig; + + await configFile.write(JSON.stringify(config, null, " ")); + outro(chalk.green("Configuration saved!")); + + return config; +} + +type Models = { + data: { + created: number; + id: string; + object: "model"; + owned_by: string; + }[]; +}; + +async function getModels(url: string) { + const response = await fetch(`${url}/models`); + const models = (await response.json()) as Models; + return models.data.map((model) => ({ value: model.id })); } export async function loadConfig(): Promise { @@ -13,12 +57,7 @@ export async function loadConfig(): Promise { try { config = await configFile.json(); } catch { - config = {}; + config = await initConfig(); } return config; } - -export async function saveConfig(config: AppConfig): Promise { - await configFile.write(JSON.stringify(config, null, " ")); - console.log(chalk.green("Configuration saved!")); -} diff --git a/src/render/index.ts b/src/render/index.ts index 4956b7d..012458c 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -130,6 +130,9 @@ export default class Renderer { break; } case "text-start": { + if (!this.isPlain) { + spin.clear(); + } this.write("\n"); break; } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 258cdfe..d576ffd 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -112,5 +112,6 @@ export async function renderMarkdown( html: () => "", }); - return rendered.replace(/\n\n$/, "\n"); + const replaced = rendered.replace(/\n\n$/, "\n"); + return Bun.wrapAnsi(replaced, columns, { trim: false }); } From 8ff27c5de806be2143cfcbe4345eff781e06f511 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 3 May 2026 21:21:56 -0400 Subject: [PATCH 08/76] fix: only clear spinner when it was actually spinning --- src/render/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/render/index.ts b/src/render/index.ts index 012458c..b3b5f69 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -89,14 +89,16 @@ export default class Renderer { this.resetCounts(); const spin = spinner({ withGuide: false, indicator: "timer" }); + let spinning = false; if (!this.isPlain) { + spinning = true; spin.start("Waiting for response"); } for await (const part of stream) { switch (part.type) { case "reasoning-start": { - if (!this.isPlain) { + if (spinning) { spin.clear(); } if (supportsColorStderr) @@ -130,7 +132,7 @@ export default class Renderer { break; } case "text-start": { - if (!this.isPlain) { + if (spinning) { spin.clear(); } this.write("\n"); From 9da57fe2fc3d6d9db2cb107252e3b8430a42a9b8 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 3 May 2026 22:15:08 -0400 Subject: [PATCH 09/76] refactor: replace cli-table3 with @cliffy/table --- .npmrc | 1 + bun.lock | 18 ++++-------------- package.json | 2 +- src/render/markdown.ts | 19 +++++++++++++------ 4 files changed, 19 insertions(+), 21 deletions(-) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/bun.lock b/bun.lock index 5dfddb9..ae13d78 100644 --- a/bun.lock +++ b/bun.lock @@ -7,10 +7,10 @@ "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", "@clack/prompts": "^1.3.0", + "@cliffy/table": "npm:@jsr/cliffy__table", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", - "cli-table3": "^0.6.5", "shiki": "^4.0.2", "terminal-link": "^5.0.0", }, @@ -54,7 +54,9 @@ "@clack/prompts": ["@clack/prompts@1.3.0", "", { "dependencies": { "@clack/core": "1.3.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw=="], - "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@cliffy/table": ["@jsr/cliffy__table@1.0.1", "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.1.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-jNROvMpkTfDDjpVAhnVlqg0Oq2XHE4BY88dNzA0nExOC+Phi0tRTn/djKFG2NYkR/6P0NtrsgNUuan+TwTxo+A=="], + + "@jsr/std__fmt": ["@jsr/std__fmt@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.10.tgz", {}, "sha512-Q2N8XDNkh/c1IKTi+sO4Vm64M5m4W0o47qTURmMX0vgHm9AJ6jL7jEQ2kw0M1T2rBZiwAY78eUbYzFyOYWaCqQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -94,8 +96,6 @@ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], @@ -108,16 +108,12 @@ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -136,8 +132,6 @@ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], @@ -170,12 +164,8 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "supports-hyperlinks": ["supports-hyperlinks@4.4.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg=="], diff --git a/package.json b/package.json index 4ff09f2..c2a4ed9 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,10 @@ "dependencies": { "@ai-sdk/openai-compatible": "^2.0.41", "@clack/prompts": "^1.3.0", + "@cliffy/table": "npm:@jsr/cliffy__table", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", - "cli-table3": "^0.6.5", "shiki": "^4.0.2", "terminal-link": "^5.0.0" } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index d576ffd..ac855a8 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -1,5 +1,5 @@ +import { Table } from "@cliffy/table"; import chalk from "chalk"; -import CliTable3 from "cli-table3"; import { type BundledLanguage, type BundledTheme, @@ -32,7 +32,8 @@ export async function renderMarkdown( } catch {} } - let currentTable = new CliTable3(); + let header: string[] = []; + let body: string[][] = []; const rendered = Bun.markdown.render(md, { heading: (children, { level }) => { @@ -88,17 +89,23 @@ export async function renderMarkdown( }, hr: () => `${chalk.dim("─".repeat(columns))}\n\n`, table: (_children) => { - const toRender = currentTable.toString(); - currentTable = new CliTable3(); + const table = new Table(); + table.header(header); + table.body(body); + table.border(); + table.maxColWidth(Math.floor(columns / header.length)); + const toRender = table.toString(); + header = []; + body = []; return `${toRender}\n\n`; }, tr: (children) => { - if (children) currentTable.push(children.split(":tablesep:").slice(1)); + if (children) body.push(children.split(":tablesep:").slice(1)); return ""; }, td: (children) => `:tablesep:${children}`, th: (children) => { - currentTable.options.head.push(children); + header.push(chalk.red(children)); return ""; }, strong: (children) => chalk.bold(children), From 5e733a4e14c42189ef87c1b265c47fff74decac0 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 3 May 2026 22:15:27 -0400 Subject: [PATCH 10/76] refactor: spinner handling and markdown line wrapping --- src/render/index.ts | 10 ++-------- src/render/markdown.ts | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/render/index.ts b/src/render/index.ts index b3b5f69..de6ca96 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -89,18 +89,14 @@ export default class Renderer { this.resetCounts(); const spin = spinner({ withGuide: false, indicator: "timer" }); - let spinning = false; if (!this.isPlain) { - spinning = true; spin.start("Waiting for response"); } for await (const part of stream) { switch (part.type) { case "reasoning-start": { - if (spinning) { - spin.clear(); - } + spin.clear(); if (supportsColorStderr) this.write( `${styles.dim.open}${styles.italic.open}`, @@ -132,9 +128,7 @@ export default class Renderer { break; } case "text-start": { - if (spinning) { - spin.clear(); - } + spin.clear(); this.write("\n"); break; } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index ac855a8..bd4d777 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -46,15 +46,19 @@ export async function renderMarkdown( chalk.greenBright, ]; const color = colors[level - 1] ?? chalk.white; - return color.bold(`${"#".repeat(level)} ${children}\n\n`); + return Bun.wrapAnsi( + color.bold(`${"#".repeat(level)} ${children}\n\n`), + columns, + ); }, - paragraph: (children) => `${children}\n\n`, + paragraph: (children) => Bun.wrapAnsi(`${children}\n\n`, columns), blockquote: (children) => { const lines = children.split("\n"); - return lines + const toRender = lines .map((line) => `${chalk.dim("> ")}${line}`) .join("\n") .replace(/\n$/, "\n\n"); + return Bun.wrapAnsi(toRender, columns); }, code: (children, meta) => { let toRender = children; @@ -73,7 +77,10 @@ export async function renderMarkdown( return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; }, list: (children, { depth }) => - `${depth > 0 ? "\n" : ""}${children.trimEnd()}${depth === 0 ? "\n\n" : ""}`, + Bun.wrapAnsi( + `${depth > 0 ? "\n" : ""}${children.trimEnd()}${depth === 0 ? "\n\n" : ""}`, + columns, + ), listItem: (children, { index, depth, ordered, start, checked }) => { const indent = " ".repeat(depth); let marker = ""; @@ -119,6 +126,5 @@ export async function renderMarkdown( html: () => "", }); - const replaced = rendered.replace(/\n\n$/, "\n"); - return Bun.wrapAnsi(replaced, columns, { trim: false }); + return rendered.replace(/\n\n$/, "\n"); } From b9610663a68ab0530d41191ef4f5c68ea11aac69 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 3 May 2026 23:11:02 -0400 Subject: [PATCH 11/76] refactor: adjust markdown line wrapping --- src/render/markdown.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index bd4d777..55cf103 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -49,16 +49,18 @@ export async function renderMarkdown( return Bun.wrapAnsi( color.bold(`${"#".repeat(level)} ${children}\n\n`), columns, + { trim: false }, ); }, - paragraph: (children) => Bun.wrapAnsi(`${children}\n\n`, columns), + paragraph: (children) => + Bun.wrapAnsi(`${children}\n\n`, columns - 2, { trim: false }), blockquote: (children) => { - const lines = children.split("\n"); + const lines = Bun.wrapAnsi(children, columns).split("\n"); const toRender = lines .map((line) => `${chalk.dim("> ")}${line}`) .join("\n") - .replace(/\n$/, "\n\n"); - return Bun.wrapAnsi(toRender, columns); + .concat("\n\n"); + return toRender; }, code: (children, meta) => { let toRender = children; @@ -77,10 +79,7 @@ export async function renderMarkdown( return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; }, list: (children, { depth }) => - Bun.wrapAnsi( - `${depth > 0 ? "\n" : ""}${children.trimEnd()}${depth === 0 ? "\n\n" : ""}`, - columns, - ), + `${depth > 0 ? "\n" : ""}${children.trimEnd()}${depth === 0 ? "\n\n" : ""}`, listItem: (children, { index, depth, ordered, start, checked }) => { const indent = " ".repeat(depth); let marker = ""; @@ -92,19 +91,21 @@ export async function renderMarkdown( } else { marker = "•"; } - return `${indent} ${marker} ${children}\n`; + return Bun.wrapAnsi(`${indent} ${marker} ${children}\n`, columns, { + trim: false, + }); }, hr: () => `${chalk.dim("─".repeat(columns))}\n\n`, table: (_children) => { - const table = new Table(); - table.header(header); - table.body(body); - table.border(); - table.maxColWidth(Math.floor(columns / header.length)); - const toRender = table.toString(); + const table = new Table() + .header(header) + .body(body) + .border() + .maxColWidth(Math.floor(columns / header.length) - 2) + .toString(); header = []; body = []; - return `${toRender}\n\n`; + return `${table}\n\n`; }, tr: (children) => { if (children) body.push(children.split(":tablesep:").slice(1)); From ea5b3728a5bae1b368620f816be8bb80ffacdcb6 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 4 May 2026 00:05:37 -0400 Subject: [PATCH 12/76] feat: wrap TTS output text to terminal width --- tts.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tts.ts b/tts.ts index 0ee0b0f..d14078d 100644 --- a/tts.ts +++ b/tts.ts @@ -69,12 +69,14 @@ for await (const line of koko.stdout) { first = false; const stop = Date.now(); const duration = ((stop - start) / 1000).toFixed(2); - console.log(chalk.blue(`\nTime-to-audio: ${duration}s`)); + console.log(chalk.green(`\nTime-to-audio: ${duration}s`)); } for (const info of infos) { + const [columns] = process.stdout.getWindowSize(); + const text = Bun.wrapAnsi(info.text, columns); const file = `${home}/.ttsc/${info.file}.wav`; - console.log(`\n${chalk.blue(file)}:\n${info.text}\n`); + console.log(`\n${chalk.blue(file)}:\n${text}\n`); if (!wait) { await Bun.spawn({ From 22fc4c874f94b2aae3722b81c0656e01369fee0c Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 4 May 2026 03:42:27 -0400 Subject: [PATCH 13/76] fix: correct markdown rendering column widths and list formatting --- src/render/markdown.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 55cf103..c0f25fd 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -53,9 +53,13 @@ export async function renderMarkdown( ); }, paragraph: (children) => - Bun.wrapAnsi(`${children}\n\n`, columns - 2, { trim: false }), + Bun.wrapAnsi(`${children}\n\n`, columns, { trim: false }), blockquote: (children) => { - const lines = Bun.wrapAnsi(children, columns).split("\n"); + const lines = Bun.wrapAnsi(`\n${children.trim()}`, columns - 2, { + trim: false, + }) + .concat("\n") + .split("\n"); const toRender = lines .map((line) => `${chalk.dim("> ")}${line}`) .join("\n") @@ -78,8 +82,10 @@ export async function renderMarkdown( return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; }, - list: (children, { depth }) => - `${depth > 0 ? "\n" : ""}${children.trimEnd()}${depth === 0 ? "\n\n" : ""}`, + list: (children, { depth }) => { + const trimmed = children.trimEnd(); + return depth === 0 ? `${trimmed}\n\n` : `\n${trimmed}`; + }, listItem: (children, { index, depth, ordered, start, checked }) => { const indent = " ".repeat(depth); let marker = ""; @@ -91,6 +97,7 @@ export async function renderMarkdown( } else { marker = "•"; } + return Bun.wrapAnsi(`${indent} ${marker} ${children}\n`, columns, { trim: false, }); @@ -101,7 +108,7 @@ export async function renderMarkdown( .header(header) .body(body) .border() - .maxColWidth(Math.floor(columns / header.length) - 2) + .maxColWidth(Math.floor(columns / header.length) - 3) .toString(); header = []; body = []; From c051f340a830a1160d5e01f313ec55f6e9e11d52 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 4 May 2026 16:45:27 -0400 Subject: [PATCH 14/76] fix: indent wrapped list items and remove codespan padding --- src/render/markdown.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index c0f25fd..f42680b 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -87,7 +87,6 @@ export async function renderMarkdown( return depth === 0 ? `${trimmed}\n\n` : `\n${trimmed}`; }, listItem: (children, { index, depth, ordered, start, checked }) => { - const indent = " ".repeat(depth); let marker = ""; if (checked !== undefined) { marker = checked ? "• [✓]" : "• [ ]"; @@ -98,9 +97,12 @@ export async function renderMarkdown( marker = "•"; } - return Bun.wrapAnsi(`${indent} ${marker} ${children}\n`, columns, { - trim: false, - }); + const cols = columns - (depth + 1) * 2; + return Bun.wrapAnsi(`${marker} ${children}`, cols, { trim: false }) + .split("\n") + .map((line) => ` ${line}`) + .join("\n") + .concat("\n"); }, hr: () => `${chalk.dim("─".repeat(columns))}\n\n`, table: (_children) => { @@ -126,7 +128,7 @@ export async function renderMarkdown( strong: (children) => chalk.bold(children), emphasis: (children) => chalk.italic(children), strikethrough: (children) => chalk.grey.strikethrough(children), - codespan: (children) => chalk.bgBlack.yellowBright(` ${children} `), + codespan: (children) => chalk.bgBlack.yellowBright(children), link: (children, { href }) => chalk.blueBright.underline(terminalLink(children, href)), image: () => "", From decfd2bde032abf0dee858599f35f48974d02a84 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 4 May 2026 18:28:14 -0400 Subject: [PATCH 15/76] fix: improve markdown list rendering and show messages for unsupported --- src/render/markdown.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index f42680b..701f9ea 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -83,8 +83,8 @@ export async function renderMarkdown( return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; }, list: (children, { depth }) => { - const trimmed = children.trimEnd(); - return depth === 0 ? `${trimmed}\n\n` : `\n${trimmed}`; + const trimmed = children.replaceAll("\n\n", "\n"); + return depth === 0 ? `${trimmed}\n` : `\n${trimmed}`; }, listItem: (children, { index, depth, ordered, start, checked }) => { let marker = ""; @@ -100,7 +100,7 @@ export async function renderMarkdown( const cols = columns - (depth + 1) * 2; return Bun.wrapAnsi(`${marker} ${children}`, cols, { trim: false }) .split("\n") - .map((line) => ` ${line}`) + .map((line) => (line.length > 0 ? ` ${line}` : "")) .join("\n") .concat("\n"); }, @@ -131,9 +131,9 @@ export async function renderMarkdown( codespan: (children) => chalk.bgBlack.yellowBright(children), link: (children, { href }) => chalk.blueBright.underline(terminalLink(children, href)), - image: () => "", + image: () => chalk.red("Images are unsupported (for now)\n"), text: (children) => children, - html: () => "", + html: () => chalk.red("HTML is unsupported\n"), }); return rendered.replace(/\n\n$/, "\n"); From 7f0498fc846ca1838a906fc8da0a49ce2037a637 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 4 May 2026 22:02:07 -0400 Subject: [PATCH 16/76] fix: improve config loading flow and table column width --- index.ts | 8 ++++---- src/config/index.ts | 19 +++++++++++++------ src/render/markdown.ts | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/index.ts b/index.ts index 91a8ffd..4a88d6f 100644 --- a/index.ts +++ b/index.ts @@ -30,15 +30,15 @@ async function main() { const modelFlag = args.model ?? args.m; const configFlag = args.config ?? args.c; - const config = await loadConfig(); + const config = await loadConfig(configFlag); - const model = modelFlag ?? config.model; - - if (configFlag) { + if (configFlag || !config) { await initConfig(config); return; } + const model = modelFlag ?? config.model; + const renderer = new Renderer(plainOut, plainErr); const provider = createOpenAICompatible({ diff --git a/src/config/index.ts b/src/config/index.ts index 40664c9..7b7193c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -10,13 +10,14 @@ export interface AppConfig { model: string; } -export async function initConfig(old?: AppConfig): Promise { +export async function initConfig(old: AppConfig | null): Promise { + console.log(); intro(chalk.blue("Welcome to LMC. Let's make a new config!")); + const baseURL = await text({ message: "What's your base URL:", - initialValue: old?.baseURL, - defaultValue: "http://127.0.0.1:8080/v1", - placeholder: "http://127.0.0.1:8080/v1", + defaultValue: old?.baseURL ?? "http://127.0.0.1:8080/v1", + placeholder: old?.baseURL ?? "http://127.0.0.1:8080/v1", }); if (isCancel(baseURL)) process.exit(1); @@ -32,6 +33,7 @@ export async function initConfig(old?: AppConfig): Promise { const config = { baseURL, model } satisfies AppConfig; await configFile.write(JSON.stringify(config, null, " ")); + outro(chalk.green("Configuration saved!")); return config; @@ -52,12 +54,17 @@ async function getModels(url: string) { return models.data.map((model) => ({ value: model.id })); } -export async function loadConfig(): Promise { +export async function loadConfig( + configFlag: boolean | undefined, +): Promise { let config: AppConfig; try { config = await configFile.json(); } catch { - config = await initConfig(); + if (configFlag) return null; + console.error(`Config not found at ${configFile.name}`); + return null; } + return config; } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 701f9ea..565193b 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -110,7 +110,7 @@ export async function renderMarkdown( .header(header) .body(body) .border() - .maxColWidth(Math.floor(columns / header.length) - 3) + .maxColWidth(Math.floor(columns / header.length) - header.length) .toString(); header = []; body = []; From a6422c51aa490dba1b44f487dd02125ed2e5f8a0 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 5 May 2026 04:07:54 -0400 Subject: [PATCH 17/76] feat: show reasoning duration after thinking phase --- src/render/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/render/index.ts b/src/render/index.ts index de6ca96..edc8fe1 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -88,6 +88,8 @@ export default class Renderer { async renderStream(stream: AsyncIterableStream>) { this.resetCounts(); + let reasonStart = 0; + const spin = spinner({ withGuide: false, indicator: "timer" }); if (!this.isPlain) { spin.start("Waiting for response"); @@ -96,6 +98,7 @@ export default class Renderer { for await (const part of stream) { switch (part.type) { case "reasoning-start": { + reasonStart = Date.now(); spin.clear(); if (supportsColorStderr) this.write( @@ -118,6 +121,8 @@ export default class Renderer { } else { this.write("\n--- Done Thinking ---\n", process.stderr); } + const time = (Date.now() - reasonStart) / 1000; + this.write(`Thought for ${time.toFixed(2)}s\n`, process.stderr); if (supportsColorStderr) this.write( From e91a8a20a123e3d9853cfc03f5dea080b369d94a Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 7 May 2026 21:04:36 -0400 Subject: [PATCH 18/76] fix: config improvements --- src/config/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 7b7193c..514d023 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -24,9 +24,15 @@ export async function initConfig(old: AppConfig | null): Promise { const models = await getModels(baseURL); const model = await autocomplete({ message: "Choose a default model:", - initialValue: old?.model, - placeholder: old?.model, + initialValue: old?.model ?? models[0]?.value, + placeholder: old?.model ?? models[0]?.value, options: models, + validate: (value) => + models + .map((model) => model.value) + .includes(typeof value === "string" ? value : "") + ? undefined + : `Unable to find model "${value}"`, }); if (isCancel(model)) process.exit(1); From efb44c4414c9375a6472cdc563e7258215f29873 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 8 May 2026 08:54:57 -0400 Subject: [PATCH 19/76] refactor: state rendering --- index.ts | 4 +--- src/render/index.ts | 20 +++++++++----------- src/render/markdown.ts | 8 ++++++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/index.ts b/index.ts index 4a88d6f..d8431c1 100644 --- a/index.ts +++ b/index.ts @@ -63,14 +63,12 @@ async function main() { messages: convo.messages, }); - const start = Date.now(); await renderer.renderStream(fullStream); - const stop = Date.now(); convo.messages.push({ role: "assistant", content: await text }); await convo.save(); - await renderer.renderStats(totalUsage, start, stop); + await renderer.renderStats(totalUsage); } await main(); diff --git a/src/render/index.ts b/src/render/index.ts index edc8fe1..4d1028c 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -16,6 +16,8 @@ export default class Renderer { private memOk = true; private buffer = ""; private unsafeIndex = 0; + private start = 0; + private stop = 0; constructor( private isPlain = false, @@ -88,8 +90,6 @@ export default class Renderer { async renderStream(stream: AsyncIterableStream>) { this.resetCounts(); - let reasonStart = 0; - const spin = spinner({ withGuide: false, indicator: "timer" }); if (!this.isPlain) { spin.start("Waiting for response"); @@ -98,7 +98,7 @@ export default class Renderer { for await (const part of stream) { switch (part.type) { case "reasoning-start": { - reasonStart = Date.now(); + this.start = Date.now(); spin.clear(); if (supportsColorStderr) this.write( @@ -121,7 +121,7 @@ export default class Renderer { } else { this.write("\n--- Done Thinking ---\n", process.stderr); } - const time = (Date.now() - reasonStart) / 1000; + const time = (Date.now() - this.start) / 1000; this.write(`Thought for ${time.toFixed(2)}s\n`, process.stderr); if (supportsColorStderr) @@ -133,6 +133,7 @@ export default class Renderer { break; } case "text-start": { + if (this.start === 0) this.start = Date.now(); spin.clear(); this.write("\n"); break; @@ -146,6 +147,7 @@ export default class Renderer { break; } case "text-end": { + this.stop = Date.now(); if (this.buffer) { await this.render(this.buffer); this.buffer = ""; @@ -160,16 +162,12 @@ export default class Renderer { } } - async renderStats( - usage: PromiseLike, - start: number, - stop: number, - ) { + async renderStats(usage: PromiseLike) { const { inputTokens, outputTokens } = await usage; const context = (inputTokens ?? 0) / 1000; const output = (outputTokens ?? 0) / 1000; - const time = (stop - start) / 1000; - const template = `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Time: ${time.toFixed(2)}s\n`; + const toks = (outputTokens ?? 0) / ((this.stop - this.start) / 1000); + const template = `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Toks: ${toks.toFixed(2)}/s\n`; this.write(chalkStderr.dim(template), process.stderr); } } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 565193b..d3d1354 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -83,7 +83,7 @@ export async function renderMarkdown( return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; }, list: (children, { depth }) => { - const trimmed = children.replaceAll("\n\n", "\n"); + const trimmed = children.replace(/\n+/g, "\n"); return depth === 0 ? `${trimmed}\n` : `\n${trimmed}`; }, listItem: (children, { index, depth, ordered, start, checked }) => { @@ -132,7 +132,11 @@ export async function renderMarkdown( link: (children, { href }) => chalk.blueBright.underline(terminalLink(children, href)), image: () => chalk.red("Images are unsupported (for now)\n"), - text: (children) => children, + text: (children) => + children + .replaceAll("$\\rightarrow$", "->") + .replaceAll("$\\leftarrow$", "<-") + .replaceAll("$\\to$", "->"), html: () => chalk.red("HTML is unsupported\n"), }); From 7578e0ed2a6e4ac0fee32502e148d17c4a6027e0 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 11 May 2026 19:42:10 -0400 Subject: [PATCH 20/76] fix: improve markdown rendering and codespan styling --- src/render/markdown.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index d3d1354..7fbd70e 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -58,6 +58,7 @@ export async function renderMarkdown( const lines = Bun.wrapAnsi(`\n${children.trim()}`, columns - 2, { trim: false, }) + .replace(/\n([a-zA-Z-_]+)\n/g, "\n$1") .concat("\n") .split("\n"); const toRender = lines @@ -83,7 +84,9 @@ export async function renderMarkdown( return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; }, list: (children, { depth }) => { - const trimmed = children.replace(/\n+/g, "\n"); + const trimmed = children + .replace(/\n+/g, "\n") + .replace(/\n([a-zA-Z-_]+)\n/g, "\n$1"); return depth === 0 ? `${trimmed}\n` : `\n${trimmed}`; }, listItem: (children, { index, depth, ordered, start, checked }) => { @@ -128,7 +131,7 @@ export async function renderMarkdown( strong: (children) => chalk.bold(children), emphasis: (children) => chalk.italic(children), strikethrough: (children) => chalk.grey.strikethrough(children), - codespan: (children) => chalk.bgBlack.yellowBright(children), + codespan: (children) => chalk.bgYellow.black(children), link: (children, { href }) => chalk.blueBright.underline(terminalLink(children, href)), image: () => chalk.red("Images are unsupported (for now)\n"), From 8cb1191fdeef1ab063b2a9582d13a465ae990be7 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 11 May 2026 19:42:32 -0400 Subject: [PATCH 21/76] refactor: simplify model selection in config initialization --- src/config/index.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 514d023..8fed513 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -24,13 +24,11 @@ export async function initConfig(old: AppConfig | null): Promise { const models = await getModels(baseURL); const model = await autocomplete({ message: "Choose a default model:", - initialValue: old?.model ?? models[0]?.value, - placeholder: old?.model ?? models[0]?.value, - options: models, + initialValue: models.includes(old?.model ?? "") ? old?.model : models[0], + placeholder: models.includes(old?.model ?? "") ? old?.model : models[0], + options: models.map((model) => ({ value: model })), validate: (value) => - models - .map((model) => model.value) - .includes(typeof value === "string" ? value : "") + models.includes(typeof value === "string" ? value : "") ? undefined : `Unable to find model "${value}"`, }); @@ -57,7 +55,7 @@ type Models = { async function getModels(url: string) { const response = await fetch(`${url}/models`); const models = (await response.json()) as Models; - return models.data.map((model) => ({ value: model.id })); + return models.data.map((model) => model.id); } export async function loadConfig( From 91175c70b6553b5e5c440c945efed1978b26cf7f Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 12 May 2026 00:03:17 -0400 Subject: [PATCH 22/76] feat: add URL support to TTS for fetching web page text --- bun.lock | 18 ++++++++++++++++++ package.json | 2 ++ tts.ts | 22 +++++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index ae13d78..f5ade78 100644 --- a/bun.lock +++ b/bun.lock @@ -8,9 +8,11 @@ "@ai-sdk/openai-compatible": "^2.0.41", "@clack/prompts": "^1.3.0", "@cliffy/table": "npm:@jsr/cliffy__table", + "@mozilla/readability": "^0.6.0", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", + "happy-dom-without-node": "^14.12.3", "shiki": "^4.0.2", "terminal-link": "^5.0.0", }, @@ -58,6 +60,8 @@ "@jsr/std__fmt": ["@jsr/std__fmt@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.10.tgz", {}, "sha512-Q2N8XDNkh/c1IKTi+sO4Vm64M5m4W0o47qTURmMX0vgHm9AJ6jL7jEQ2kw0M1T2rBZiwAY78eUbYzFyOYWaCqQ=="], + "@mozilla/readability": ["@mozilla/readability@0.6.0", "", {}, "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], @@ -114,6 +118,8 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -124,6 +130,8 @@ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "happy-dom-without-node": ["happy-dom-without-node@14.12.3", "", { "dependencies": { "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^14.0.0" } }, "sha512-1wp8+GFneT8mBjVnzancXHRuscEUH3vnb38lfCHPxuSu6OiRw1kQzHFbTGYlNCU2YXOlVIlzsS6xg9nAr7Xg6Q=="], + "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], @@ -152,6 +160,8 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -172,6 +182,8 @@ "terminal-link": ["terminal-link@5.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^4.1.0" } }, "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA=="], + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -192,6 +204,12 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], diff --git a/package.json b/package.json index c2a4ed9..3be45bd 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,11 @@ "@ai-sdk/openai-compatible": "^2.0.41", "@clack/prompts": "^1.3.0", "@cliffy/table": "npm:@jsr/cliffy__table", + "@mozilla/readability": "^0.6.0", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", + "happy-dom-without-node": "^14.12.3", "shiki": "^4.0.2", "terminal-link": "^5.0.0" } diff --git a/tts.ts b/tts.ts index d14078d..5f37923 100644 --- a/tts.ts +++ b/tts.ts @@ -1,7 +1,10 @@ import os from "node:os"; +import process from "node:process"; import { parseArgs } from "node:util"; +import { Readability } from "@mozilla/readability"; import { $, which } from "bun"; import chalk from "chalk"; +import { Window } from "happy-dom-without-node"; const start = Date.now(); @@ -10,13 +13,30 @@ const { values: args } = parseArgs({ options: { wait: { type: "boolean" }, w: { type: "boolean" }, + url: { type: "boolean" }, + u: { type: "boolean" }, }, strict: true, allowPositionals: true, }); const wait = args.wait ?? args.w; -const text = await Bun.stdin.text(); +const url = args.url ?? args.u; +let text = await Bun.stdin.text(); + +if (url) { + const result = await fetch(text.trim()); + const window = new Window({ url: text.trim() }); + const doc = window.document; + doc.write(await result.text()); + await window.happyDOM.waitUntilComplete(); + const readable = new Readability(doc).parse(); + if (readable?.textContent) text = readable.textContent; + if (text.trim() === result.url) { + console.error("Unable to read web page."); + process.exit(1); + } +} if (!text) { console.error("No input given."); From af52e89928d73a90dc04fa5ecae75d3beef8eb07 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 12 May 2026 14:16:07 -0400 Subject: [PATCH 23/76] fix: improve markdown rendering with proper wide character support --- src/render/index.ts | 6 ++++-- src/render/markdown.ts | 35 +++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/render/index.ts b/src/render/index.ts index 4d1028c..af24b89 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -48,10 +48,12 @@ export default class Renderer { private checkContainment(text: string, pipe: WriteStream = process.stdout) { const [columns, rows] = pipe.getWindowSize(); - this.charsSinceNL += text.length; + this.charsSinceNL += Bun.stringWidth(text); if (text.includes("\n")) { this.lineCount += text.match(/\n/g)?.length ?? 1; - this.charsSinceNL = text.length - text.lastIndexOf("\n") - 1; + this.charsSinceNL = + Bun.stringWidth(text) - + Bun.stringWidth(text.slice(0, text.lastIndexOf("\n"))); } else if (this.charsSinceNL > columns) { this.lineCount++; this.charsSinceNL = this.charsSinceNL - columns; diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 7fbd70e..beaf244 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -1,4 +1,4 @@ -import { Table } from "@cliffy/table"; +import { Cell, Table } from "@cliffy/table"; import chalk from "chalk"; import { type BundledLanguage, @@ -12,6 +12,12 @@ import { codeToANSI } from "./codeToANSI"; let highlighter: HighlighterGeneric | undefined; let highligherLangs = ""; +class SWCell extends Cell { + public override get length() { + return Bun.stringWidth(this.toString(), { ambiguousIsNarrow: true }); + } +} + export async function renderMarkdown( text: PromiseLike | string, columns: number, @@ -32,8 +38,8 @@ export async function renderMarkdown( } catch {} } - let header: string[] = []; - let body: string[][] = []; + let header: SWCell[] = []; + let body: SWCell[][] = []; const rendered = Bun.markdown.render(md, { heading: (children, { level }) => { @@ -58,7 +64,8 @@ export async function renderMarkdown( const lines = Bun.wrapAnsi(`\n${children.trim()}`, columns - 2, { trim: false, }) - .replace(/\n([a-zA-Z-_]+)\n/g, "\n$1") + .replace(/\n(\s?[a-zA-Z_-]+\s?)\n/g, "\n$1") + .replace(/\n+/g, "\n") .concat("\n") .split("\n"); const toRender = lines @@ -86,7 +93,7 @@ export async function renderMarkdown( list: (children, { depth }) => { const trimmed = children .replace(/\n+/g, "\n") - .replace(/\n([a-zA-Z-_]+)\n/g, "\n$1"); + .replace(/\n(\s?[a-zA-Z_-]+\s?)\n/g, "\n$1"); return depth === 0 ? `${trimmed}\n` : `\n${trimmed}`; }, listItem: (children, { index, depth, ordered, start, checked }) => { @@ -113,20 +120,28 @@ export async function renderMarkdown( .header(header) .body(body) .border() - .maxColWidth(Math.floor(columns / header.length) - header.length) + .maxColWidth( + Math.floor(columns / header.length) - Math.floor(header.length / 2), + ) .toString(); header = []; body = []; return `${table}\n\n`; }, tr: (children) => { - if (children) body.push(children.split(":tablesep:").slice(1)); - return ""; + if (!children) return; + body.push( + children + .split(":tablesep:") + .slice(1) + .map((value) => new SWCell(value)), + ); + return null; }, td: (children) => `:tablesep:${children}`, th: (children) => { - header.push(chalk.red(children)); - return ""; + header.push(new SWCell(chalk.red(children))); + return null; }, strong: (children) => chalk.bold(children), emphasis: (children) => chalk.italic(children), From 7367cfdfe365a2a14774d555d78d718fe1c8a6ec Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 12 May 2026 16:34:13 -0400 Subject: [PATCH 24/76] fix: blockquote and list whitespace handling in markdown rendering --- src/render/markdown.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index beaf244..e1f652e 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -64,13 +64,14 @@ export async function renderMarkdown( const lines = Bun.wrapAnsi(`\n${children.trim()}`, columns - 2, { trim: false, }) - .replace(/\n(\s?[a-zA-Z_-]+\s?)\n/g, "\n$1") .replace(/\n+/g, "\n") .concat("\n") .split("\n"); const toRender = lines - .map((line) => `${chalk.dim("> ")}${line}`) + .map((line) => `> ${line}`) .join("\n") + .replace(/(\n> +\S+) *\n> */g, "$1 ") + .replaceAll(">", chalk.dim(">")) .concat("\n\n"); return toRender; }, @@ -91,9 +92,7 @@ export async function renderMarkdown( return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; }, list: (children, { depth }) => { - const trimmed = children - .replace(/\n+/g, "\n") - .replace(/\n(\s?[a-zA-Z_-]+\s?)\n/g, "\n$1"); + const trimmed = children.replace(/\n+/g, "\n"); return depth === 0 ? `${trimmed}\n` : `\n${trimmed}`; }, listItem: (children, { index, depth, ordered, start, checked }) => { @@ -112,6 +111,7 @@ export async function renderMarkdown( .split("\n") .map((line) => (line.length > 0 ? ` ${line}` : "")) .join("\n") + .replace(/(\n +\S+) *\n */g, "$1 ") .concat("\n"); }, hr: () => `${chalk.dim("─".repeat(columns))}\n\n`, From 0ed6a2333471d163fec9e8749c1f712b90b51ef1 Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 14 May 2026 02:52:49 -0400 Subject: [PATCH 25/76] refactor: simplify renderer drain handling and expand LaTeX arrow replacements --- src/render/index.ts | 18 ++++-------------- src/render/markdown.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/render/index.ts b/src/render/index.ts index af24b89..91ac9f4 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -13,7 +13,6 @@ import { renderMarkdown } from "./markdown"; export default class Renderer { private lineCount = 0; private charsSinceNL = 0; - private memOk = true; private buffer = ""; private unsafeIndex = 0; private start = 0; @@ -22,22 +21,15 @@ export default class Renderer { constructor( private isPlain = false, private isPlainErr = false, - ) { - process.stdout.addListener("drain", () => { - this.memOk = true; - }); - process.stderr.addListener("drain", () => { - this.memOk = true; - }); - } + ) {} private write(text: string, pipe: WriteStream = process.stdout) { - this.memOk = pipe.write(text); + pipe.write(text); } private moveClearReset(y: number, pipe: WriteStream = process.stdout) { - this.memOk = pipe.moveCursor(-9999, y); - this.memOk = pipe.clearScreenDown(); + pipe.moveCursor(-9999, y); + pipe.clearScreenDown(); this.resetCounts(); } @@ -159,8 +151,6 @@ export default class Renderer { break; } } - - while (!this.memOk) await Bun.sleep(10); } } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index e1f652e..6417450 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -152,8 +152,12 @@ export async function renderMarkdown( image: () => chalk.red("Images are unsupported (for now)\n"), text: (children) => children - .replaceAll("$\\rightarrow$", "->") - .replaceAll("$\\leftarrow$", "<-") + .replaceAll("$\\rightarrow$", "-->") + .replaceAll("$\\leftarrow$", "<--") + .replaceAll("$\\leftrightarrow$", "<->") + .replaceAll("$\\Rightarrow$", "==>") + .replaceAll("$\\Leftarrow$", "<==") + .replaceAll("$\\Leftrightarrow$", "<=>") .replaceAll("$\\to$", "->"), html: () => chalk.red("HTML is unsupported\n"), }); From 7441a9fc191356fed9a101ec0df67a6b131984f1 Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 14 May 2026 03:06:46 -0400 Subject: [PATCH 26/76] docs: update README with new flags, examples, and dependencies --- README.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b2f0c47..981bddb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Bun-based CLI tool for LLM chats with terminal native streaming. - **Conversation persistence** — chat history saved to JSON - **Model configuration** — persist model preferences to JSON - **Reasoning display** — thinking tokens rendered _in style_ on stderr -- **Usage stats** — input/output tokens and elapsed time after each response +- **Usage stats** — input/output tokens and tokens/sec rate after each response - **TTY-aware** — automatically disables markdown rendering when output is piped - **Text-to-speech** — `ttsc` sub-command for streaming TTS via Kokoro model @@ -38,22 +38,22 @@ _Package to come later_ lmc [options] ``` -| Flag | Short | Type | Description | -| ------------ | ----- | ------- | ------------------------------------------------- | -| `--new` | `-n` | boolean | Start a new conversation (clear history) | -| `--model` | `-m` | string | Set model name for this session | -| `--save` | `-s` | boolean | Save the specified model (--model) as the default | -| `--plain` | — | boolean | Disable markdown rendering (plain text output) | -| `--plainOut` | — | boolean | Disable markdown rendering for stdout only | -| `--plainErr` | — | boolean | Disable markdown rendering for stderr only | +| Flag | Short | Type | Description | +| ------------ | ----- | ------- | ----------------------------------------------------- | +| `--new` | `-n` | boolean | Start a new conversation (clear history) | +| `--model` | `-m` | string | Set model name for this session | +| `--config` | `-c` | boolean | Initialize a new config file (`~/.lmc_config.json`) | +| `--plain` | — | boolean | Disable markdown rendering for both stdout and stderr | +| `--plainOut` | — | boolean | Disable markdown rendering for stdout only | +| `--plainErr` | — | boolean | Disable markdown rendering for stderr only | **Examples:** ```bash +lmc --config # Initialize/edit config lmc --new "Introduce yourself." # New conversation lmc "What is TypeScript?" # Continue most recent chat -lmc -m llama3 "Explain quantum computing" # Chat with specific model -lmc --model llama3 --save # Set default model +lmc -m llama3 "Explain quantum computing" # Chat with specific model on-the-fly lmc --plain "Plain text output" # No markdown rendering ``` @@ -66,12 +66,14 @@ echo "text" | ttsc [options] | Flag | Short | Type | Description | | -------- | ----- | ------- | -------------------------------------------------------- | | `--wait` | `-w` | boolean | Wait for all processing before exiting (don't auto-play) | +| `--url` | `-u` | boolean | Fetch text from a URL instead of stdin | **Examples:** ```bash echo "Hello, world!" | ttsc echo "Read this aloud" | ttsc --wait +echo "https://example.com" | ttsc --url ``` Requires: [uv](https://docs.astral.sh/uv/) @@ -113,8 +115,10 @@ out/ — Bundled output (index.js, tts.js) | Package | Purpose | | --------------------------- | ----------------------------------------- | | `ai` (Vercel AI SDK) | Streaming text/usage | -| `@ai-sdk/openai-compatible` | OpenAI-compatible API client | -| `shiki` | Syntax highlighting (kanagawa-wave theme) | -| `cli-table3` | ASCII table rendering | +| `@clack/prompts` | Interactive prompts and spinner | +| `@cliffy/table` | ASCII table rendering | +| `@mozilla/readability` | HTML-to-text extraction (ttsc) | | `chalk` | Terminal string styling | +| `happy-dom` | DOM parsing for URLs (ttsc) | +| `shiki` | Syntax highlighting (kanagawa-wave theme) | | `terminal-link` | Clickable terminal hyperlinks | From 03dee78e0c2b6332c6a2b0ad3605d08b2807f83c Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 14 May 2026 04:34:01 -0400 Subject: [PATCH 27/76] fix: change URL flag from boolean to string type for proper URL passing --- tts.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tts.ts b/tts.ts index 5f37923..fc131a5 100644 --- a/tts.ts +++ b/tts.ts @@ -13,8 +13,8 @@ const { values: args } = parseArgs({ options: { wait: { type: "boolean" }, w: { type: "boolean" }, - url: { type: "boolean" }, - u: { type: "boolean" }, + url: { type: "string" }, + u: { type: "string" }, }, strict: true, allowPositionals: true, @@ -22,23 +22,23 @@ const { values: args } = parseArgs({ const wait = args.wait ?? args.w; const url = args.url ?? args.u; -let text = await Bun.stdin.text(); +let input = url ? url : await Bun.stdin.text(); -if (url) { - const result = await fetch(text.trim()); - const window = new Window({ url: text.trim() }); +if (url !== undefined) { + const result = await fetch(input.trim()); + const window = new Window({ url: input.trim() }); const doc = window.document; doc.write(await result.text()); await window.happyDOM.waitUntilComplete(); const readable = new Readability(doc).parse(); - if (readable?.textContent) text = readable.textContent; - if (text.trim() === result.url) { + if (readable?.textContent) input = readable.textContent; + if (input.trim() === result.url) { console.error("Unable to read web page."); process.exit(1); } } -if (!text) { +if (!input) { console.error("No input given."); process.exit(1); } @@ -59,7 +59,7 @@ console.log( const koko = Bun.spawn({ cmd: ["uv", "run", "--", "python", "-u", "main.py"], cwd, - stdin: Buffer.from(text), + stdin: Buffer.from(input), stdout: "pipe", stderr: "inherit", }); From 4f9e7a7a1ad4c5d24e3658ccd9c0cc3198ab8145 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 17 May 2026 19:40:45 -0400 Subject: [PATCH 28/76] style: enhance markdown inline formatting colors --- src/render/markdown.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 6417450..53461df 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -120,9 +120,7 @@ export async function renderMarkdown( .header(header) .body(body) .border() - .maxColWidth( - Math.floor(columns / header.length) - Math.floor(header.length / 2), - ) + .maxColWidth(Math.floor((columns - 3) / header.length)) .toString(); header = []; body = []; @@ -140,13 +138,13 @@ export async function renderMarkdown( }, td: (children) => `:tablesep:${children}`, th: (children) => { - header.push(new SWCell(chalk.red(children))); + header.push(new SWCell(chalk.red.bold(children))); return null; }, - strong: (children) => chalk.bold(children), - emphasis: (children) => chalk.italic(children), + strong: (children) => chalk.bold.rgb(254, 254, 254)(children), + emphasis: (children) => chalk.italic.rgb(254, 254, 254)(children), strikethrough: (children) => chalk.grey.strikethrough(children), - codespan: (children) => chalk.bgYellow.black(children), + codespan: (children) => chalk.bgYellow.black.bold(children), link: (children, { href }) => chalk.blueBright.underline(terminalLink(children, href)), image: () => chalk.red("Images are unsupported (for now)\n"), From b212fee2004377cd653c625142b1a493a04ed3a8 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 25 May 2026 19:35:47 -0400 Subject: [PATCH 29/76] refactor: replace Bun.markdown with marked library and custom renderer --- bun.lock | 3 + package.json | 1 + src/render/markdown.ts | 285 ++++++++++++++++++++++++----------------- 3 files changed, 169 insertions(+), 120 deletions(-) diff --git a/bun.lock b/bun.lock index f5ade78..9468bf3 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "ansi-styles": "^6.2.3", "chalk": "^5.6.2", "happy-dom-without-node": "^14.12.3", + "marked": "^18.0.4", "shiki": "^4.0.2", "terminal-link": "^5.0.0", }, @@ -142,6 +143,8 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "marked": ["marked@18.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], diff --git a/package.json b/package.json index 3be45bd..0ad14b9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "ansi-styles": "^6.2.3", "chalk": "^5.6.2", "happy-dom-without-node": "^14.12.3", + "marked": "^18.0.4", "shiki": "^4.0.2", "terminal-link": "^5.0.0" } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 53461df..dcd85af 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -1,5 +1,6 @@ import { Cell, Table } from "@cliffy/table"; import chalk from "chalk"; +import { marked, Renderer, type Tokens } from "marked"; import { type BundledLanguage, type BundledTheme, @@ -18,6 +19,164 @@ class SWCell extends Cell { } } +class LmcRenderer extends Renderer { + private columns: number; + + constructor(columns: number) { + super(); + this.columns = columns; + } + + override heading({ tokens, depth }: Tokens.Heading) { + const colors = [ + chalk.cyan.underline, + chalk.blue, + chalk.green, + chalk.cyanBright, + chalk.blueBright, + chalk.greenBright, + ]; + const color = colors[depth - 1] ?? chalk.white; + const text = this.parser.parseInline(tokens); + return Bun.wrapAnsi( + color.bold(`${"#".repeat(depth)} ${text}\n\n`), + this.columns, + { trim: false }, + ); + } + + override paragraph({ tokens }: Tokens.Paragraph) { + const text = this.parser.parseInline(tokens); + return Bun.wrapAnsi(`${text}\n\n`, this.columns, { trim: false }); + } + + override blockquote({ tokens }: Tokens.Blockquote) { + const text = this.parser.parse(tokens).trim(); + const lines = Bun.wrapAnsi(text, this.columns - 2, { trim: false }) + .replace(/\n+/g, "\n") + .concat("\n") + .split("\n"); + const toRender = lines + .map((line) => `> ${line}`) + .join("\n") + .concat("\n\n"); + return toRender.replaceAll(">", chalk.dim(">")); + } + + override code({ text, lang, escaped: _ }: Tokens.Code) { + let toRender = text; + + if (highlighter && lang) { + try { + toRender = codeToANSI( + highlighter, + text, + lang as BundledLanguage, + "kanagawa-wave", + ); + } catch {} + } + + return `\`\`\`${chalk.blue(lang ?? "")}\n${toRender}\`\`\`\n`; + } + + override list({ items }: Tokens.List) { + const renderedItems = items.map((item) => this.listitem(item)).join(""); + const trimmed = renderedItems.replace(/\n+/g, "\n"); + return `${trimmed}\n`; + } + + override listitem({ task, checked, tokens }: Tokens.ListItem) { + const text = this.parser.parse(tokens); + let marker = ""; + if (task) { + marker = checked ? "• [✓]" : "• [ ]"; + } else { + marker = "•"; + } + + const cols = this.columns - 2; + return Bun.wrapAnsi(`${marker} ${text}`, cols, { trim: false }) + .split("\n") + .map((line) => (line.length > 0 ? ` ${line}` : "")) + .join("\n") + .concat("\n"); + } + + override hr() { + return `${chalk.dim("─".repeat(this.columns))}\n\n`; + } + + override table({ header, rows }: Tokens.Table) { + const headerCells = header.map((col) => { + const text = this.parser.parseInline(col.tokens); + return new SWCell(chalk.red.bold(text)); + }); + + const body = rows.map((row) => + row.map((col) => { + const text = this.parser.parseInline(col.tokens); + return new SWCell(text); + }), + ); + + const table = new Table() + .header(headerCells) + .body(body) + .border() + .maxColWidth(Math.floor((this.columns - 3) / headerCells.length)) + .toString(); + + return `${table}\n\n`; + } + + override strong({ tokens }: Tokens.Strong) { + const text = this.parser.parseInline(tokens); + return chalk.bold.rgb(254, 254, 254)(text); + } + + override em({ tokens }: Tokens.Em) { + const text = this.parser.parseInline(tokens); + return chalk.italic.rgb(254, 254, 254)(text); + } + + override del({ tokens }: Tokens.Del) { + const text = this.parser.parseInline(tokens); + return chalk.grey.strikethrough(text); + } + + override codespan({ text }: Tokens.Codespan) { + return chalk.bgYellow.black.bold(text); + } + + override link({ href, tokens }: Tokens.Link) { + const text = this.parser.parseInline(tokens); + return chalk.blueBright.underline(terminalLink(text, href)); + } + + override image({ text: _ }: Tokens.Image) { + return chalk.red("Images are unsupported (for now)\n"); + } + + override text(token: Tokens.Text) { + if (token.tokens) { + return this.parser.parseInline(token.tokens); + } + return token.text + .replaceAll("$\\rightarrow$", "-->") + .replaceAll("$\\leftarrow$", "<--") + .replaceAll("$\\leftrightarrow$", "<->") + .replaceAll("$\\Rightarrow$", "==>") + .replaceAll("$\\Leftarrow$", "<==") + .replaceAll("$\\Leftrightarrow$", "<=>") + .replaceAll("$\\to$", "->"); + } + + override html({ text: _ }: Tokens.HTML | Tokens.Tag) { + return chalk.red("HTML is unsupported\n"); + } +} + export async function renderMarkdown( text: PromiseLike | string, columns: number, @@ -38,127 +197,13 @@ export async function renderMarkdown( } catch {} } - let header: SWCell[] = []; - let body: SWCell[][] = []; - - const rendered = Bun.markdown.render(md, { - heading: (children, { level }) => { - const colors = [ - chalk.cyan.underline, - chalk.blue, - chalk.green, - chalk.cyanBright, - chalk.blueBright, - chalk.greenBright, - ]; - const color = colors[level - 1] ?? chalk.white; - return Bun.wrapAnsi( - color.bold(`${"#".repeat(level)} ${children}\n\n`), - columns, - { trim: false }, - ); - }, - paragraph: (children) => - Bun.wrapAnsi(`${children}\n\n`, columns, { trim: false }), - blockquote: (children) => { - const lines = Bun.wrapAnsi(`\n${children.trim()}`, columns - 2, { - trim: false, - }) - .replace(/\n+/g, "\n") - .concat("\n") - .split("\n"); - const toRender = lines - .map((line) => `> ${line}`) - .join("\n") - .replace(/(\n> +\S+) *\n> */g, "$1 ") - .replaceAll(">", chalk.dim(">")) - .concat("\n\n"); - return toRender; - }, - code: (children, meta) => { - let toRender = children; - - if (highlighter && meta?.language) { - try { - toRender = codeToANSI( - highlighter, - children, - meta.language as BundledLanguage, - "kanagawa-wave", - ); - } catch {} - } - - return `\`\`\`${chalk.blue(meta?.language ?? "")}\n${toRender}\`\`\`\n`; - }, - list: (children, { depth }) => { - const trimmed = children.replace(/\n+/g, "\n"); - return depth === 0 ? `${trimmed}\n` : `\n${trimmed}`; - }, - listItem: (children, { index, depth, ordered, start, checked }) => { - let marker = ""; - if (checked !== undefined) { - marker = checked ? "• [✓]" : "• [ ]"; - } else if (ordered) { - const n = (start ?? 1) + index; - marker = `${n}.`; - } else { - marker = "•"; - } - - const cols = columns - (depth + 1) * 2; - return Bun.wrapAnsi(`${marker} ${children}`, cols, { trim: false }) - .split("\n") - .map((line) => (line.length > 0 ? ` ${line}` : "")) - .join("\n") - .replace(/(\n +\S+) *\n */g, "$1 ") - .concat("\n"); - }, - hr: () => `${chalk.dim("─".repeat(columns))}\n\n`, - table: (_children) => { - const table = new Table() - .header(header) - .body(body) - .border() - .maxColWidth(Math.floor((columns - 3) / header.length)) - .toString(); - header = []; - body = []; - return `${table}\n\n`; - }, - tr: (children) => { - if (!children) return; - body.push( - children - .split(":tablesep:") - .slice(1) - .map((value) => new SWCell(value)), - ); - return null; - }, - td: (children) => `:tablesep:${children}`, - th: (children) => { - header.push(new SWCell(chalk.red.bold(children))); - return null; - }, - strong: (children) => chalk.bold.rgb(254, 254, 254)(children), - emphasis: (children) => chalk.italic.rgb(254, 254, 254)(children), - strikethrough: (children) => chalk.grey.strikethrough(children), - codespan: (children) => chalk.bgYellow.black.bold(children), - link: (children, { href }) => - chalk.blueBright.underline(terminalLink(children, href)), - image: () => chalk.red("Images are unsupported (for now)\n"), - text: (children) => - children - .replaceAll("$\\rightarrow$", "-->") - .replaceAll("$\\leftarrow$", "<--") - .replaceAll("$\\leftrightarrow$", "<->") - .replaceAll("$\\Rightarrow$", "==>") - .replaceAll("$\\Leftarrow$", "<==") - .replaceAll("$\\Leftrightarrow$", "<=>") - .replaceAll("$\\to$", "->"), - html: () => chalk.red("HTML is unsupported\n"), + const renderer = new LmcRenderer(columns); + marked.setOptions({ + renderer, + gfm: true, }); + const rendered = await marked.parse(md); + return rendered.replace(/\n\n$/, "\n"); } From 0c71e2cd5406de85e983b76ee45720dd9f4d9a1b Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 25 May 2026 23:13:16 -0400 Subject: [PATCH 30/76] refactor: replace Bun APIs with Node.js equivalents Replace Bun.argv with process.argv, Bun.file() with fs/promises, Bun.stringWidth with string-width package, and Bun.wrapAnsi with wrap-ansi package for better compatibility. --- bun.lock | 12 ++++++++++++ index.ts | 2 +- package.json | 4 +++- src/config/index.ts | 10 ++++++---- src/conversation/index.ts | 9 ++++++--- src/render/index.ts | 6 +++--- src/render/markdown.ts | 14 ++++++++------ 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 9468bf3..2c3a667 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,9 @@ "happy-dom-without-node": "^14.12.3", "marked": "^18.0.4", "shiki": "^4.0.2", + "string-width": "^8.2.1", "terminal-link": "^5.0.0", + "wrap-ansi": "^10.0.0", }, "devDependencies": { "@biomejs/biome": "2.4.12", @@ -101,6 +103,8 @@ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], @@ -131,6 +135,8 @@ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "happy-dom-without-node": ["happy-dom-without-node@14.12.3", "", { "dependencies": { "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^14.0.0" } }, "sha512-1wp8+GFneT8mBjVnzancXHRuscEUH3vnb38lfCHPxuSu6OiRw1kQzHFbTGYlNCU2YXOlVIlzsS6xg9nAr7Xg6Q=="], "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], @@ -177,8 +183,12 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "supports-hyperlinks": ["supports-hyperlinks@4.4.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg=="], @@ -213,6 +223,8 @@ "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], diff --git a/index.ts b/index.ts index d8431c1..14530ef 100644 --- a/index.ts +++ b/index.ts @@ -7,7 +7,7 @@ import { Conversation } from "./src/conversation"; import Renderer from "./src/render"; const { values: args, positionals } = parseArgs({ - args: Bun.argv, + args: process.argv, options: { model: { type: "string" }, m: { type: "string" }, diff --git a/package.json b/package.json index 0ad14b9..bbca7b7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "happy-dom-without-node": "^14.12.3", "marked": "^18.0.4", "shiki": "^4.0.2", - "terminal-link": "^5.0.0" + "string-width": "^8.2.1", + "terminal-link": "^5.0.0", + "wrap-ansi": "^10.0.0" } } diff --git a/src/config/index.ts b/src/config/index.ts index 8fed513..2ddabcc 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,9 +1,10 @@ +import fs from "node:fs/promises"; import os from "node:os"; import { autocomplete, intro, isCancel, outro, text } from "@clack/prompts"; import chalk from "chalk"; const home = os.homedir(); -const configFile = Bun.file(`${home}/.lmc_config.json`); +const configPath = `${home}/.lmc_config.json`; export interface AppConfig { baseURL: string; @@ -36,7 +37,7 @@ export async function initConfig(old: AppConfig | null): Promise { const config = { baseURL, model } satisfies AppConfig; - await configFile.write(JSON.stringify(config, null, " ")); + await fs.writeFile(configPath, JSON.stringify(config, null, " ")); outro(chalk.green("Configuration saved!")); @@ -63,10 +64,11 @@ export async function loadConfig( ): Promise { let config: AppConfig; try { - config = await configFile.json(); + const text = await fs.readFile(configPath); + config = JSON.parse(text.toString("utf8")); } catch { if (configFlag) return null; - console.error(`Config not found at ${configFile.name}`); + console.error(`Config not found at ${configPath}`); return null; } diff --git a/src/conversation/index.ts b/src/conversation/index.ts index 9fb01f4..6d408c2 100644 --- a/src/conversation/index.ts +++ b/src/conversation/index.ts @@ -1,8 +1,9 @@ +import fs from "node:fs/promises"; import os from "node:os"; import type { ModelMessage } from "ai"; const home = os.homedir(); -const convoFile = Bun.file(`${home}/.lmc_convo.json`); +const convoPath = `${home}/.lmc_convo.json`; export interface IConversation { model: string; @@ -16,7 +17,8 @@ export class Conversation implements IConversation { async load() { try { - const convo: IConversation = await convoFile.json(); + const text = await fs.readFile(convoPath); + const convo: IConversation = JSON.parse(text.toString("utf8")); this.model = convo.model; this.messages = convo.messages; } catch { @@ -25,7 +27,8 @@ export class Conversation implements IConversation { } async save() { - await convoFile.write( + await fs.writeFile( + convoPath, JSON.stringify({ model: this.model, messages: this.messages, diff --git a/src/render/index.ts b/src/render/index.ts index 91ac9f4..01fce63 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -8,6 +8,7 @@ import type { } from "ai"; import styles from "ansi-styles"; import { chalkStderr, supportsColorStderr } from "chalk"; +import stringWidth from "string-width"; import { renderMarkdown } from "./markdown"; export default class Renderer { @@ -40,12 +41,11 @@ export default class Renderer { private checkContainment(text: string, pipe: WriteStream = process.stdout) { const [columns, rows] = pipe.getWindowSize(); - this.charsSinceNL += Bun.stringWidth(text); + this.charsSinceNL += stringWidth(text); if (text.includes("\n")) { this.lineCount += text.match(/\n/g)?.length ?? 1; this.charsSinceNL = - Bun.stringWidth(text) - - Bun.stringWidth(text.slice(0, text.lastIndexOf("\n"))); + stringWidth(text) - stringWidth(text.slice(0, text.lastIndexOf("\n"))); } else if (this.charsSinceNL > columns) { this.lineCount++; this.charsSinceNL = this.charsSinceNL - columns; diff --git a/src/render/markdown.ts b/src/render/markdown.ts index dcd85af..a745b82 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -7,7 +7,9 @@ import { createHighlighter, type HighlighterGeneric, } from "shiki"; +import stringWidth from "string-width"; import terminalLink from "terminal-link"; +import wrapAnsi from "wrap-ansi"; import { codeToANSI } from "./codeToANSI"; let highlighter: HighlighterGeneric | undefined; @@ -15,7 +17,7 @@ let highligherLangs = ""; class SWCell extends Cell { public override get length() { - return Bun.stringWidth(this.toString(), { ambiguousIsNarrow: true }); + return stringWidth(this.toString(), { ambiguousIsNarrow: true }); } } @@ -23,7 +25,7 @@ class LmcRenderer extends Renderer { private columns: number; constructor(columns: number) { - super(); + super({ gfm: true }); this.columns = columns; } @@ -38,7 +40,7 @@ class LmcRenderer extends Renderer { ]; const color = colors[depth - 1] ?? chalk.white; const text = this.parser.parseInline(tokens); - return Bun.wrapAnsi( + return wrapAnsi( color.bold(`${"#".repeat(depth)} ${text}\n\n`), this.columns, { trim: false }, @@ -47,12 +49,12 @@ class LmcRenderer extends Renderer { override paragraph({ tokens }: Tokens.Paragraph) { const text = this.parser.parseInline(tokens); - return Bun.wrapAnsi(`${text}\n\n`, this.columns, { trim: false }); + return wrapAnsi(`${text}\n\n`, this.columns, { trim: false }); } override blockquote({ tokens }: Tokens.Blockquote) { const text = this.parser.parse(tokens).trim(); - const lines = Bun.wrapAnsi(text, this.columns - 2, { trim: false }) + const lines = wrapAnsi(text, this.columns - 2, { trim: false }) .replace(/\n+/g, "\n") .concat("\n") .split("\n"); @@ -96,7 +98,7 @@ class LmcRenderer extends Renderer { } const cols = this.columns - 2; - return Bun.wrapAnsi(`${marker} ${text}`, cols, { trim: false }) + return wrapAnsi(`${marker} ${text}`, cols, { trim: false }) .split("\n") .map((line) => (line.length > 0 ? ` ${line}` : "")) .join("\n") From a887e1b185d9d415a0e4e623d78da675a28b76ea Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 25 May 2026 23:18:40 -0400 Subject: [PATCH 31/76] fix: improve markdown rendering whitespace and list numbering --- src/render/markdown.ts | 57 +++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index a745b82..9ccb4b0 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -41,7 +41,7 @@ class LmcRenderer extends Renderer { const color = colors[depth - 1] ?? chalk.white; const text = this.parser.parseInline(tokens); return wrapAnsi( - color.bold(`${"#".repeat(depth)} ${text}\n\n`), + color.bold(`${"#".repeat(depth)} ${text}\n`), this.columns, { trim: false }, ); @@ -49,23 +49,19 @@ class LmcRenderer extends Renderer { override paragraph({ tokens }: Tokens.Paragraph) { const text = this.parser.parseInline(tokens); - return wrapAnsi(`${text}\n\n`, this.columns, { trim: false }); + return wrapAnsi(`\n${text}\n`, this.columns, { trim: false }); } override blockquote({ tokens }: Tokens.Blockquote) { const text = this.parser.parse(tokens).trim(); - const lines = wrapAnsi(text, this.columns - 2, { trim: false }) - .replace(/\n+/g, "\n") - .concat("\n") - .split("\n"); - const toRender = lines - .map((line) => `> ${line}`) - .join("\n") - .concat("\n\n"); - return toRender.replaceAll(">", chalk.dim(">")); + const lines = wrapAnsi(`\n${text}\n`, this.columns - 2, { + trim: false, + }).split("\n"); + const toRender = lines.map((line) => `> ${line}`).join("\n"); + return `${toRender.replaceAll(">", chalk.dim(">"))}\n`; } - override code({ text, lang, escaped: _ }: Tokens.Code) { + override code({ text, lang }: Tokens.Code) { let toRender = text; if (highlighter && lang) { @@ -79,20 +75,26 @@ class LmcRenderer extends Renderer { } catch {} } - return `\`\`\`${chalk.blue(lang ?? "")}\n${toRender}\`\`\`\n`; + return `\n\`\`\`${chalk.blue(lang ?? "")}\n${toRender}\n\`\`\`\n`; } - override list({ items }: Tokens.List) { - const renderedItems = items.map((item) => this.listitem(item)).join(""); - const trimmed = renderedItems.replace(/\n+/g, "\n"); - return `${trimmed}\n`; + override list({ items, ordered, start }: Tokens.List) { + const num = start || 1; + const renderedItems = items + .map((item, index) => + this.listitem(item, ordered ? num + index : undefined), + ) + .join("\n"); + return `\n${renderedItems}\n`.replaceAll("\n\n", "\n"); } - override listitem({ task, checked, tokens }: Tokens.ListItem) { + override listitem({ task, checked, tokens }: Tokens.ListItem, num?: number) { const text = this.parser.parse(tokens); let marker = ""; if (task) { marker = checked ? "• [✓]" : "• [ ]"; + } else if (num !== undefined) { + marker = `${num}.`; } else { marker = "•"; } @@ -101,12 +103,11 @@ class LmcRenderer extends Renderer { return wrapAnsi(`${marker} ${text}`, cols, { trim: false }) .split("\n") .map((line) => (line.length > 0 ? ` ${line}` : "")) - .join("\n") - .concat("\n"); + .join("\n"); } override hr() { - return `${chalk.dim("─".repeat(this.columns))}\n\n`; + return `\n${chalk.dim("─".repeat(this.columns))}\n`; } override table({ header, rows }: Tokens.Table) { @@ -126,10 +127,10 @@ class LmcRenderer extends Renderer { .header(headerCells) .body(body) .border() - .maxColWidth(Math.floor((this.columns - 3) / headerCells.length)) + .maxColWidth(Math.floor((this.columns - 4) / headerCells.length)) .toString(); - return `${table}\n\n`; + return `\n${table}\n`; } override strong({ tokens }: Tokens.Strong) { @@ -157,7 +158,7 @@ class LmcRenderer extends Renderer { } override image({ text: _ }: Tokens.Image) { - return chalk.red("Images are unsupported (for now)\n"); + return chalk.red("\nImages are unsupported (for now)\n"); } override text(token: Tokens.Text) { @@ -175,7 +176,11 @@ class LmcRenderer extends Renderer { } override html({ text: _ }: Tokens.HTML | Tokens.Tag) { - return chalk.red("HTML is unsupported\n"); + return chalk.red("\nHTML is unsupported\n"); + } + + override br() { + return "\n"; } } @@ -207,5 +212,5 @@ export async function renderMarkdown( const rendered = await marked.parse(md); - return rendered.replace(/\n\n$/, "\n"); + return rendered.trimStart(); } From c277418349c286b09e2f487c16da60e8add65b4b Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 26 May 2026 03:43:23 -0400 Subject: [PATCH 32/76] fix: list/blockquote newlines and table width --- src/render/markdown.ts | 51 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 9ccb4b0..64276c1 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -17,7 +17,7 @@ let highligherLangs = ""; class SWCell extends Cell { public override get length() { - return stringWidth(this.toString(), { ambiguousIsNarrow: true }); + return stringWidth(this.toString()); } } @@ -25,7 +25,7 @@ class LmcRenderer extends Renderer { private columns: number; constructor(columns: number) { - super({ gfm: true }); + super(); this.columns = columns; } @@ -49,14 +49,13 @@ class LmcRenderer extends Renderer { override paragraph({ tokens }: Tokens.Paragraph) { const text = this.parser.parseInline(tokens); - return wrapAnsi(`\n${text}\n`, this.columns, { trim: false }); + return wrapAnsi(`\n${text}\n`, this.columns - 2, { trim: false }); } override blockquote({ tokens }: Tokens.Blockquote) { - const text = this.parser.parse(tokens).trim(); - const lines = wrapAnsi(`\n${text}\n`, this.columns - 2, { - trim: false, - }).split("\n"); + let text = this.parser.parse(tokens).trim(); + if (!text.includes("\n")) text = wrapAnsi(text, this.columns - 2); + const lines = `\n${text}\n`.split("\n"); const toRender = lines.map((line) => `> ${line}`).join("\n"); return `${toRender.replaceAll(">", chalk.dim(">"))}\n`; } @@ -85,33 +84,36 @@ class LmcRenderer extends Renderer { this.listitem(item, ordered ? num + index : undefined), ) .join("\n"); - return `\n${renderedItems}\n`.replaceAll("\n\n", "\n"); + return `\n${renderedItems}\n`; } - override listitem({ task, checked, tokens }: Tokens.ListItem, num?: number) { - const text = this.parser.parse(tokens); + override listitem({ tokens }: Tokens.ListItem, num?: number) { + const text = this.parser.parse(tokens).trim(); let marker = ""; - if (task) { - marker = checked ? "• [✓]" : "• [ ]"; - } else if (num !== undefined) { + if (num !== undefined) { marker = `${num}.`; } else { marker = "•"; } - const cols = this.columns - 2; - return wrapAnsi(`${marker} ${text}`, cols, { trim: false }) + let item = `${marker} ${text}`; + if (!item.includes("\n")) item = wrapAnsi(item, this.columns - 2); + return item .split("\n") .map((line) => (line.length > 0 ? ` ${line}` : "")) .join("\n"); } + override checkbox({ checked }: Tokens.Checkbox): string { + return checked ? "[✓] " : "[ ] "; + } + override hr() { return `\n${chalk.dim("─".repeat(this.columns))}\n`; } override table({ header, rows }: Tokens.Table) { - const headerCells = header.map((col) => { + const headers = header.map((col) => { const text = this.parser.parseInline(col.tokens); return new SWCell(chalk.red.bold(text)); }); @@ -123,11 +125,13 @@ class LmcRenderer extends Renderer { }), ); + const cols = this.columns - (headers.length * 3 + 1); + const maxWidth = Math.floor(cols / headers.length); const table = new Table() - .header(headerCells) + .header(headers) .body(body) .border() - .maxColWidth(Math.floor((this.columns - 4) / headerCells.length)) + .maxColWidth(maxWidth) .toString(); return `\n${table}\n`; @@ -161,8 +165,8 @@ class LmcRenderer extends Renderer { return chalk.red("\nImages are unsupported (for now)\n"); } - override text(token: Tokens.Text) { - if (token.tokens) { + override text(token: Tokens.Text | Tokens.Escape) { + if (token.type === "text" && token.tokens) { return this.parser.parseInline(token.tokens); } return token.text @@ -205,12 +209,7 @@ export async function renderMarkdown( } const renderer = new LmcRenderer(columns); - marked.setOptions({ - renderer, - gfm: true, - }); - - const rendered = await marked.parse(md); + const rendered = await marked.parse(md, { renderer }); return rendered.trimStart(); } From fc4934af4a06ddcdd385c5c2f117f18f7dcd2780 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 27 May 2026 18:33:14 -0400 Subject: [PATCH 33/76] refactor: move wrapAnsi from blockquote/listitem to text tokens --- src/render/markdown.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 64276c1..71b9f6b 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -40,24 +40,19 @@ class LmcRenderer extends Renderer { ]; const color = colors[depth - 1] ?? chalk.white; const text = this.parser.parseInline(tokens); - return wrapAnsi( - color.bold(`${"#".repeat(depth)} ${text}\n`), - this.columns, - { trim: false }, - ); + return color.bold(`${"#".repeat(depth)} ${text}\n`); } override paragraph({ tokens }: Tokens.Paragraph) { - const text = this.parser.parseInline(tokens); - return wrapAnsi(`\n${text}\n`, this.columns - 2, { trim: false }); + const text = wrapAnsi(this.parser.parseInline(tokens), this.columns - 4); + return `\n${text}\n`; } override blockquote({ tokens }: Tokens.Blockquote) { - let text = this.parser.parse(tokens).trim(); - if (!text.includes("\n")) text = wrapAnsi(text, this.columns - 2); + const text = this.parser.parse(tokens).trim(); const lines = `\n${text}\n`.split("\n"); const toRender = lines.map((line) => `> ${line}`).join("\n"); - return `${toRender.replaceAll(">", chalk.dim(">"))}\n`; + return `\n${toRender.replaceAll(">", chalk.dim(">"))}\n`; } override code({ text, lang }: Tokens.Code) { @@ -96,8 +91,7 @@ class LmcRenderer extends Renderer { marker = "•"; } - let item = `${marker} ${text}`; - if (!item.includes("\n")) item = wrapAnsi(item, this.columns - 2); + const item = `${marker} ${text}`; return item .split("\n") .map((line) => (line.length > 0 ? ` ${line}` : "")) @@ -167,7 +161,8 @@ class LmcRenderer extends Renderer { override text(token: Tokens.Text | Tokens.Escape) { if (token.type === "text" && token.tokens) { - return this.parser.parseInline(token.tokens); + const parsed = this.parser.parseInline(token.tokens); + return wrapAnsi(parsed, this.columns - 12); } return token.text .replaceAll("$\\rightarrow$", "-->") From 6372dfc41fdc225c58145e7a671159f58ab1703c Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 28 May 2026 00:52:00 -0400 Subject: [PATCH 34/76] refactor: extract latex symbol replacement into private method --- src/render/markdown.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 71b9f6b..9c6d4f8 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -164,14 +164,7 @@ class LmcRenderer extends Renderer { const parsed = this.parser.parseInline(token.tokens); return wrapAnsi(parsed, this.columns - 12); } - return token.text - .replaceAll("$\\rightarrow$", "-->") - .replaceAll("$\\leftarrow$", "<--") - .replaceAll("$\\leftrightarrow$", "<->") - .replaceAll("$\\Rightarrow$", "==>") - .replaceAll("$\\Leftarrow$", "<==") - .replaceAll("$\\Leftrightarrow$", "<=>") - .replaceAll("$\\to$", "->"); + return this.replaceLatex(token.text); } override html({ text: _ }: Tokens.HTML | Tokens.Tag) { @@ -181,6 +174,17 @@ class LmcRenderer extends Renderer { override br() { return "\n"; } + + private replaceLatex(text: string) { + return text + .replaceAll("$\\rightarrow$", "-->") + .replaceAll("$\\leftarrow$", "<--") + .replaceAll("$\\leftrightarrow$", "<->") + .replaceAll("$\\Rightarrow$", "==>") + .replaceAll("$\\Leftarrow$", "<==") + .replaceAll("$\\Leftrightarrow$", "<=>") + .replaceAll("$\\to$", "->"); + } } export async function renderMarkdown( From 3e96007f1ffacc41ad9548a97977428ae130c9a7 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 3 Jun 2026 15:52:49 -0400 Subject: [PATCH 35/76] refactor: migrate shiki to core API with dynamic language/theme loading - Update code block regex to support symbols/numbers in language identifiers - Adjust codespan background color to a softer shade --- index.ts | 6 ++++++ src/render/codeToANSI.ts | 4 ++-- src/render/markdown.ts | 28 ++++++++++++++++------------ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/index.ts b/index.ts index 14530ef..dd17b66 100644 --- a/index.ts +++ b/index.ts @@ -15,6 +15,7 @@ const { values: args, positionals } = parseArgs({ n: { type: "boolean" }, config: { type: "boolean" }, c: { type: "boolean" }, + v: { type: "boolean" }, plain: { type: "boolean" }, plainOut: { type: "boolean" }, plainErr: { type: "boolean" }, @@ -30,6 +31,11 @@ async function main() { const modelFlag = args.model ?? args.m; const configFlag = args.config ?? args.c; + if (args.v) { + console.log("0.0.0"); + return; + } + const config = await loadConfig(configFlag); if (configFlag || !config) { diff --git a/src/render/codeToANSI.ts b/src/render/codeToANSI.ts index d5a081e..fc54b8e 100644 --- a/src/render/codeToANSI.ts +++ b/src/render/codeToANSI.ts @@ -24,7 +24,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import c from "chalk"; -import type { BundledLanguage, BundledTheme, HighlighterGeneric } from "shiki"; +import type { BundledLanguage, BundledTheme, HighlighterCore } from "shiki"; // From @shikijs/vscode-textmate enum FontStyle { @@ -37,7 +37,7 @@ enum FontStyle { } export function codeToANSI( - highlighter: HighlighterGeneric, + highlighter: HighlighterCore, code: string, lang: BundledLanguage, theme: BundledTheme, diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 9c6d4f8..905c003 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -1,18 +1,15 @@ import { Cell, Table } from "@cliffy/table"; import chalk from "chalk"; import { marked, Renderer, type Tokens } from "marked"; -import { - type BundledLanguage, - type BundledTheme, - createHighlighter, - type HighlighterGeneric, -} from "shiki"; +import type { BundledLanguage, HighlighterCore } from "shiki"; +import { createHighlighterCore } from "shiki/core"; +import { createOnigurumaEngine } from "shiki/engine/oniguruma"; import stringWidth from "string-width"; import terminalLink from "terminal-link"; import wrapAnsi from "wrap-ansi"; import { codeToANSI } from "./codeToANSI"; -let highlighter: HighlighterGeneric | undefined; +let highlighter: HighlighterCore | undefined; let highligherLangs = ""; class SWCell extends Cell { @@ -147,7 +144,7 @@ class LmcRenderer extends Renderer { } override codespan({ text }: Tokens.Codespan) { - return chalk.bgYellow.black.bold(text); + return chalk.bgRgb(165, 165, 190).black.bold(text); } override link({ href, tokens }: Tokens.Link) { @@ -194,15 +191,22 @@ export async function renderMarkdown( const md = await text; const langs = md - .match(/```[a-z]+\n/g) + .match(/```\S+\n/g) ?.map((lang) => lang.replace("```", "").trimEnd()); if (langs && langs?.join() !== highligherLangs) { + const { bundledThemes, bundledLanguages, bundledLanguagesAlias } = + await import("shiki/bundle/full"); try { highligherLangs = langs.join(); - highlighter = await createHighlighter({ - langs, - themes: ["kanagawa-wave", "kanagawa-lotus"], + highlighter = await createHighlighterCore({ + langs: langs.map( + (lang) => + bundledLanguages[lang as BundledLanguage] || + bundledLanguagesAlias[lang], + ), + themes: [bundledThemes["kanagawa-wave"]], + engine: createOnigurumaEngine(import("shiki/wasm")), }); } catch {} } From cd3749b07ea82b524fd83ede21ad7a5f3c2edda1 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 26 May 2026 02:52:41 -0400 Subject: [PATCH 36/76] build: replace bun install/build with pnpm/esbuild --- bun.lock | 232 --------- package.json | 21 +- pnpm-lock.yaml | 1102 +++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + 4 files changed, 1113 insertions(+), 244 deletions(-) delete mode 100644 bun.lock create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 2c3a667..0000000 --- a/bun.lock +++ /dev/null @@ -1,232 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "aicl", - "dependencies": { - "@ai-sdk/openai-compatible": "^2.0.41", - "@clack/prompts": "^1.3.0", - "@cliffy/table": "npm:@jsr/cliffy__table", - "@mozilla/readability": "^0.6.0", - "ai": "^6.0.161", - "ansi-styles": "^6.2.3", - "chalk": "^5.6.2", - "happy-dom-without-node": "^14.12.3", - "marked": "^18.0.4", - "shiki": "^4.0.2", - "string-width": "^8.2.1", - "terminal-link": "^5.0.0", - "wrap-ansi": "^10.0.0", - }, - "devDependencies": { - "@biomejs/biome": "2.4.12", - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.98", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ol+nP8PIlj8FjN8qKlxhE89N0woqAaGi9CUBGp1boe3RafpphJ7WMuq/RErSvxtwTqje03TP+zIdzP113krxRg=="], - - "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], - - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], - - "@biomejs/biome": ["@biomejs/biome@2.4.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.12", "@biomejs/cli-darwin-x64": "2.4.12", "@biomejs/cli-linux-arm64": "2.4.12", "@biomejs/cli-linux-arm64-musl": "2.4.12", "@biomejs/cli-linux-x64": "2.4.12", "@biomejs/cli-linux-x64-musl": "2.4.12", "@biomejs/cli-win32-arm64": "2.4.12", "@biomejs/cli-win32-x64": "2.4.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], - - "@clack/core": ["@clack/core@1.3.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA=="], - - "@clack/prompts": ["@clack/prompts@1.3.0", "", { "dependencies": { "@clack/core": "1.3.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw=="], - - "@cliffy/table": ["@jsr/cliffy__table@1.0.1", "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.1.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-jNROvMpkTfDDjpVAhnVlqg0Oq2XHE4BY88dNzA0nExOC+Phi0tRTn/djKFG2NYkR/6P0NtrsgNUuan+TwTxo+A=="], - - "@jsr/std__fmt": ["@jsr/std__fmt@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.10.tgz", {}, "sha512-Q2N8XDNkh/c1IKTi+sO4Vm64M5m4W0o47qTURmMX0vgHm9AJ6jL7jEQ2kw0M1T2rBZiwAY78eUbYzFyOYWaCqQ=="], - - "@mozilla/readability": ["@mozilla/readability@0.6.0", "", {}, "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ=="], - - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - - "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], - - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], - - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="], - - "@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], - - "@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="], - - "@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], - - "@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], - - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], - - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - - "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], - - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], - - "ai": ["ai@6.0.161", "", { "dependencies": { "@ai-sdk/gateway": "3.0.98", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ufhmijmx2YyWTPAicGgtpLOB/xD7mG8zKs1pT1Trj+JL/3r1rS8fkMi/cHZoChSAQSGB4pgmcWVxDrVTUvK2IQ=="], - - "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], - - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - - "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], - - "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], - - "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], - - "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], - - "happy-dom-without-node": ["happy-dom-without-node@14.12.3", "", { "dependencies": { "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^14.0.0" } }, "sha512-1wp8+GFneT8mBjVnzancXHRuscEUH3vnb38lfCHPxuSu6OiRw1kQzHFbTGYlNCU2YXOlVIlzsS6xg9nAr7Xg6Q=="], - - "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], - - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - - "marked": ["marked@18.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA=="], - - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - - "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], - - "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], - - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], - - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - - "shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - - "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], - - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - - "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - - "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], - - "supports-hyperlinks": ["supports-hyperlinks@4.4.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg=="], - - "terminal-link": ["terminal-link@5.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^4.1.0" } }, "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA=="], - - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], - - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - - "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], - - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - - "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], - - "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], - - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - } -} diff --git a/package.json b/package.json index bbca7b7..458c538 100644 --- a/package.json +++ b/package.json @@ -4,24 +4,21 @@ "type": "module", "private": true, "bin": { - "lmc": "out/index.js", - "ttsc": "out/tts.js" + "lmc": "dist/index.js", + "ttsc": "dist/tts.js" }, "scripts": { - "dev": "bun run index.ts", - "prod": "bun run out/index.js", - "check": "bunx --bun @biomejs/biome check --write && bun run tc", - "build:js": "bun run check && bun run --parallel build:bundle build:tts && bun run --parallel shebang shebang:tts", - "build:bundle": "bun build --target=bun --production --sourcemap --outdir=out index.ts", - "build:tts": "bun build --target=bun --production --sourcemap --outdir=out tts.ts", - "build:bin": "bun build --compile --outfile=out/lmc index.ts", - "shebang": "sed -i '1i#!\\/usr\\/bin\\/env bun' out/index.js", - "shebang:tts": "sed -i '1i#!\\/usr\\/bin\\/env bun' out/tts.js", + "dev": "tsx index.ts", + "prod": "node dist/index.js", + "check": "biome check --write && pnpm run tc", + "build": "pnpm run check && pnpm run bundle", + "bundle": "esbuild index.ts --bundle --outdir=dist --platform=node --target=node18 --format=esm --packages=external", "tc": "tsc --noEmit --incremental" }, "devDependencies": { "@biomejs/biome": "2.4.12", - "@types/bun": "latest" + "@types/bun": "latest", + "esbuild": "0.28.0" }, "peerDependencies": { "typescript": "^5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..9cacba1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1102 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ai-sdk/openai-compatible': + specifier: ^2.0.41 + version: 2.0.48(zod@4.4.3) + '@clack/prompts': + specifier: ^1.3.0 + version: 1.4.0 + '@cliffy/table': + specifier: npm:@jsr/cliffy__table + version: '@jsr/cliffy__table@1.1.0' + '@mozilla/readability': + specifier: ^0.6.0 + version: 0.6.0 + ai: + specifier: ^6.0.161 + version: 6.0.191(zod@4.4.3) + ansi-styles: + specifier: ^6.2.3 + version: 6.2.3 + chalk: + specifier: ^5.6.2 + version: 5.6.2 + happy-dom-without-node: + specifier: ^14.12.3 + version: 14.12.3 + marked: + specifier: ^18.0.4 + version: 18.0.4 + shiki: + specifier: ^4.0.2 + version: 4.1.0 + string-width: + specifier: ^8.2.1 + version: 8.2.1 + terminal-link: + specifier: ^5.0.0 + version: 5.0.0 + typescript: + specifier: ^5 + version: 5.9.3 + wrap-ansi: + specifier: ^10.0.0 + version: 10.0.0 + devDependencies: + '@biomejs/biome': + specifier: 2.4.12 + version: 2.4.12 + '@types/bun': + specifier: latest + version: 1.3.14 + esbuild: + specifier: 0.28.0 + version: 0.28.0 + +packages: + + '@ai-sdk/gateway@3.0.120': + resolution: {integrity: sha512-MYKAeD2q7/sa1ZdqtL2tw0Me0B8Tok6Q/fhkJDhJl39dG8u+VBlWO9yk9lcdm784bM418o1EKObo4aOxs6+18Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai-compatible@2.0.48': + resolution: {integrity: sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.27': + resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.10': + resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} + engines: {node: '>=18'} + + '@biomejs/biome@2.4.12': + resolution: {integrity: sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.12': + resolution: {integrity: sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.12': + resolution: {integrity: sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.12': + resolution: {integrity: sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.12': + resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.12': + resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.12': + resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.12': + resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.12': + resolution: {integrity: sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@clack/core@1.3.1': + resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.4.0': + resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} + engines: {node: '>= 20.12.0'} + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jsr/cliffy__table@1.1.0': + resolution: {integrity: sha512-EYKlk3L0mpJoJYD27+BlaLsjaTGbcG6Sun+HffbSRcxGD1xCuldKPynFKmsoos967OAAeYfOlrjJeD93Y//Ucg==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.1.0.tgz} + + '@jsr/std__fmt@1.0.10': + resolution: {integrity: sha512-Q2N8XDNkh/c1IKTi+sO4Vm64M5m4W0o47qTURmMX0vgHm9AJ6jL7jEQ2kw0M1T2rBZiwAY78eUbYzFyOYWaCqQ==, tarball: https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.10.tgz} + + '@mozilla/readability@0.6.0': + resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} + engines: {node: '>=14.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@shikijs/core@4.1.0': + resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.1.0': + resolution: {integrity: sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.1.0': + resolution: {integrity: sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.1.0': + resolution: {integrity: sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.1.0': + resolution: {integrity: sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.1.0': + resolution: {integrity: sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==} + engines: {node: '>=20'} + + '@shikijs/types@4.1.0': + resolution: {integrity: sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/bun@1.3.14': + resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + + ai@6.0.191: + resolution: {integrity: sha512-zAxvjKebQE7YkSyyNIl0OM7i6/zygnKeF+yNUjD4nWOelYrG+LpDd6RnH6mjySI4zUpZ7o4wbnmAy8jc6u98vQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + bun-types@1.3.14: + resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + happy-dom-without-node@14.12.3: + resolution: {integrity: sha512-1wp8+GFneT8mBjVnzancXHRuscEUH3vnb38lfCHPxuSu6OiRw1kQzHFbTGYlNCU2YXOlVIlzsS6xg9nAr7Xg6Q==} + engines: {node: '>=16.0.0'} + + has-flag@5.0.1: + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} + engines: {node: '>=12'} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + marked@18.0.4: + resolution: {integrity: sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==} + engines: {node: '>= 20'} + hasBin: true + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + shiki@4.1.0: + resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==} + engines: {node: '>=20'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-hyperlinks@4.4.0: + resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==} + engines: {node: '>=20'} + + terminal-link@5.0.0: + resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} + engines: {node: '>=20'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ai-sdk/gateway@3.0.120(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/openai-compatible@2.0.48(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + zod: 4.4.3 + + '@ai-sdk/provider-utils@4.0.27(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.10 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.8 + zod: 4.4.3 + + '@ai-sdk/provider@3.0.10': + dependencies: + json-schema: 0.4.0 + + '@biomejs/biome@2.4.12': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.12 + '@biomejs/cli-darwin-x64': 2.4.12 + '@biomejs/cli-linux-arm64': 2.4.12 + '@biomejs/cli-linux-arm64-musl': 2.4.12 + '@biomejs/cli-linux-x64': 2.4.12 + '@biomejs/cli-linux-x64-musl': 2.4.12 + '@biomejs/cli-win32-arm64': 2.4.12 + '@biomejs/cli-win32-x64': 2.4.12 + + '@biomejs/cli-darwin-arm64@2.4.12': + optional: true + + '@biomejs/cli-darwin-x64@2.4.12': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.12': + optional: true + + '@biomejs/cli-linux-arm64@2.4.12': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.12': + optional: true + + '@biomejs/cli-linux-x64@2.4.12': + optional: true + + '@biomejs/cli-win32-arm64@2.4.12': + optional: true + + '@biomejs/cli-win32-x64@2.4.12': + optional: true + + '@clack/core@1.3.1': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.4.0': + dependencies: + '@clack/core': 1.3.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@jsr/cliffy__table@1.1.0': + dependencies: + '@jsr/std__fmt': 1.0.10 + + '@jsr/std__fmt@1.0.10': {} + + '@mozilla/readability@0.6.0': {} + + '@opentelemetry/api@1.9.1': {} + + '@shikijs/core@4.1.0': + dependencies: + '@shikijs/primitive': 4.1.0 + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + + '@shikijs/primitive@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + + '@shikijs/types@4.1.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/bun@1.3.14': + dependencies: + bun-types: 1.3.14 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.1': {} + + '@vercel/oidc@3.2.0': {} + + ai@6.0.191(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 3.0.120(zod@4.4.3) + '@ai-sdk/provider': 3.0.10 + '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@opentelemetry/api': 1.9.1 + zod: 4.4.3 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + + bun-types@1.3.14: + dependencies: + '@types/node': 25.9.1 + + ccount@2.0.1: {} + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + entities@4.5.0: {} + + environment@1.1.0: {} + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + eventsource-parser@3.0.8: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + get-east-asian-width@1.6.0: {} + + happy-dom-without-node@14.12.3: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 14.2.0 + + has-flag@5.0.1: {} + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-void-elements@3.0.0: {} + + json-schema@0.4.0: {} + + marked@18.0.4: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + property-information@7.1.0: {} + + punycode@2.3.1: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + shiki@4.1.0: + dependencies: + '@shikijs/core': 4.1.0 + '@shikijs/engine-javascript': 4.1.0 + '@shikijs/engine-oniguruma': 4.1.0 + '@shikijs/langs': 4.1.0 + '@shikijs/themes': 4.1.0 + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + sisteransi@1.0.5: {} + + space-separated-tokens@2.0.2: {} + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + supports-color@10.2.2: {} + + supports-hyperlinks@4.4.0: + dependencies: + has-flag: 5.0.1 + supports-color: 10.2.2 + + terminal-link@5.0.0: + dependencies: + ansi-escapes: 7.3.0 + supports-hyperlinks: 4.4.0 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + trim-lines@3.0.1: {} + + typescript@5.9.3: {} + + undici-types@7.24.6: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.1 + strip-ansi: 7.2.0 + + zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..5ed0b5a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true From d98ddb3ca73a513021a1e65769fd324c067bc513 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 26 May 2026 19:00:20 -0400 Subject: [PATCH 37/76] build: migrate to commonjs and add node single executable app support - Switch module type to commonjs in package.json - Update esbuild target to node22 and remove explicit ESM format flag - Add `bin` script for building a native binary using `node --build-sea` - Introduce sea-config.json with code cache enabled for faster startup --- index.ts | 5 ++++- package.json | 11 ++++++----- sea-config.json | 6 ++++++ 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 sea-config.json diff --git a/index.ts b/index.ts index dd17b66..29960f9 100644 --- a/index.ts +++ b/index.ts @@ -77,4 +77,7 @@ async function main() { await renderer.renderStats(totalUsage); } -await main(); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/package.json b/package.json index 458c538..63eb70c 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { "name": "lmc", "module": "index.ts", - "type": "module", + "main": "dist/index.js", + "type": "commonjs", "private": true, "bin": { - "lmc": "dist/index.js", - "ttsc": "dist/tts.js" + "lmc": "dist/index.js" }, "scripts": { "dev": "tsx index.ts", "prod": "node dist/index.js", "check": "biome check --write && pnpm run tc", - "build": "pnpm run check && pnpm run bundle", - "bundle": "esbuild index.ts --bundle --outdir=dist --platform=node --target=node18 --format=esm --packages=external", + "build": "pnpm run check && pnpm run bundle && pnpm run bin", + "bundle": "esbuild index.ts --bundle --outdir=dist --platform=node --target=node22", + "bin": "node --build-sea sea-config.json", "tc": "tsc --noEmit --incremental" }, "devDependencies": { diff --git a/sea-config.json b/sea-config.json new file mode 100644 index 0000000..4beec69 --- /dev/null +++ b/sea-config.json @@ -0,0 +1,6 @@ +{ + "main": "dist/index.js", + "output": "dist/lmc", + "useCodeCache": true, + "disableExperimentalSEAWarning": true +} From f6680375824337148a70cf0851b837650351c058 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 26 May 2026 19:27:41 -0400 Subject: [PATCH 38/76] chore(config): pin @types/bun, tighten TypeScript strictness, and refactor config logic - Pin `@types/bun` to `^1.3.14` for reproducible builds instead of using `latest` - Update `tsconfig.json` with stricter type-checking flags and reorganize compiler options - Refactor `initConfig` to DRY up the model default logic and add a null guard before process exit --- package.json | 2 +- pnpm-lock.yaml | 2 +- src/config/index.ts | 9 +++++--- testRender.ts | 2 +- tsconfig.json | 50 +++++++++++++++++++++++++++------------------ 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 63eb70c..cd6a5ce 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@biomejs/biome": "2.4.12", - "@types/bun": "latest", + "@types/bun": "^1.3.14", "esbuild": "0.28.0" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cacba1..874a0f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: specifier: 2.4.12 version: 2.4.12 '@types/bun': - specifier: latest + specifier: ^1.3.14 version: 1.3.14 esbuild: specifier: 0.28.0 diff --git a/src/config/index.ts b/src/config/index.ts index 2ddabcc..9e23618 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -23,17 +23,20 @@ export async function initConfig(old: AppConfig | null): Promise { if (isCancel(baseURL)) process.exit(1); const models = await getModels(baseURL); + const initialValue = models.includes(old?.model ?? "") + ? old?.model + : models[0]; const model = await autocomplete({ message: "Choose a default model:", - initialValue: models.includes(old?.model ?? "") ? old?.model : models[0], - placeholder: models.includes(old?.model ?? "") ? old?.model : models[0], + initialValue, + placeholder: initialValue ?? "Start typing...", options: models.map((model) => ({ value: model })), validate: (value) => models.includes(typeof value === "string" ? value : "") ? undefined : `Unable to find model "${value}"`, }); - if (isCancel(model)) process.exit(1); + if (!model || isCancel(model)) process.exit(1); const config = { baseURL, model } satisfies AppConfig; diff --git a/testRender.ts b/testRender.ts index 6373337..4fecfd9 100644 --- a/testRender.ts +++ b/testRender.ts @@ -1,4 +1,4 @@ -import { renderMarkdown } from "./src/render/markdown.ts"; +import { renderMarkdown } from "./src/render/markdown"; const text = await Bun.file("README.md").text(); diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..edeab3e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,39 @@ { + // Visit https://aka.ms/tsconfig to read more about this file "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "esnext", + "target": "esnext", "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, + "esModuleInterop": true, "noEmit": true, + // For nodejs: + "lib": ["esnext"], + "types": ["bun"], + // and npm install -D @types/node - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, + // Stricter Typechecking Options "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + // Style Options + "noImplicitReturns": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + //"allowImportingTsExtensions": true, + "allowArbitraryExtensions": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true } } From 43c3402931782626d320f256659347e8dfc8bafa Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 26 May 2026 20:00:47 -0400 Subject: [PATCH 39/76] chore(deps): consolidate all dependencies into devDependencies and clear production fields - Move runtime packages from `dependencies` to `devDependencies` - Remove `peerDependencies` as it is no longer required --- package.json | 14 ++++++-------- pnpm-lock.yaml | 21 ++++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index cd6a5ce..b31b6c7 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,7 @@ "devDependencies": { "@biomejs/biome": "2.4.12", "@types/bun": "^1.3.14", - "esbuild": "0.28.0" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { + "esbuild": "0.28.0", "@ai-sdk/openai-compatible": "^2.0.41", "@clack/prompts": "^1.3.0", "@cliffy/table": "npm:@jsr/cliffy__table", @@ -37,6 +32,9 @@ "shiki": "^4.0.2", "string-width": "^8.2.1", "terminal-link": "^5.0.0", - "wrap-ansi": "^10.0.0" - } + "wrap-ansi": "^10.0.0", + "typescript": "^5" + }, + "peerDependencies": {}, + "dependencies": {} } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 874a0f3..76b5399 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,10 +7,13 @@ settings: importers: .: - dependencies: + devDependencies: '@ai-sdk/openai-compatible': specifier: ^2.0.41 version: 2.0.48(zod@4.4.3) + '@biomejs/biome': + specifier: 2.4.12 + version: 2.4.12 '@clack/prompts': specifier: ^1.3.0 version: 1.4.0 @@ -20,6 +23,9 @@ importers: '@mozilla/readability': specifier: ^0.6.0 version: 0.6.0 + '@types/bun': + specifier: ^1.3.14 + version: 1.3.14 ai: specifier: ^6.0.161 version: 6.0.191(zod@4.4.3) @@ -29,6 +35,9 @@ importers: chalk: specifier: ^5.6.2 version: 5.6.2 + esbuild: + specifier: 0.28.0 + version: 0.28.0 happy-dom-without-node: specifier: ^14.12.3 version: 14.12.3 @@ -50,16 +59,6 @@ importers: wrap-ansi: specifier: ^10.0.0 version: 10.0.0 - devDependencies: - '@biomejs/biome': - specifier: 2.4.12 - version: 2.4.12 - '@types/bun': - specifier: ^1.3.14 - version: 1.3.14 - esbuild: - specifier: 0.28.0 - version: 0.28.0 packages: From 6392cf3b76d958cd00934ce76cba918225272627 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 1 Jun 2026 00:44:10 -0400 Subject: [PATCH 40/76] chore: update package.json to reflect reality --- package.json | 12 +++++------- sea-config.json | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b31b6c7..66c76b6 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,16 @@ { "name": "lmc", "module": "index.ts", - "main": "dist/index.js", - "type": "commonjs", + "type": "module", "private": true, "bin": { - "lmc": "dist/index.js" + "lmc": "dist/lmc" }, "scripts": { - "dev": "tsx index.ts", - "prod": "node dist/index.js", + "dev": "pnpm run bundle &>/dev/null && node dist/index.cjs", "check": "biome check --write && pnpm run tc", - "build": "pnpm run check && pnpm run bundle && pnpm run bin", - "bundle": "esbuild index.ts --bundle --outdir=dist --platform=node --target=node22", + "build": "pnpm run check && pnpm run bundle --minify && pnpm run bin", + "bundle": "esbuild index.ts --bundle --outfile=dist/index.cjs --platform=node", "bin": "node --build-sea sea-config.json", "tc": "tsc --noEmit --incremental" }, diff --git a/sea-config.json b/sea-config.json index 4beec69..83b28b4 100644 --- a/sea-config.json +++ b/sea-config.json @@ -1,5 +1,5 @@ { - "main": "dist/index.js", + "main": "dist/index.cjs", "output": "dist/lmc", "useCodeCache": true, "disableExperimentalSEAWarning": true From bbee38be80c01d28d395c871ddea296290bfeceb Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 1 Jun 2026 16:36:57 -0400 Subject: [PATCH 41/76] refactor(build): switch from esbuild to rolldown --- index.ts | 3 +- package.json | 25 +-- pnpm-lock.yaml | 496 ++++++++++++++++++++++-------------------------- sea-config.json | 2 +- 4 files changed, 246 insertions(+), 280 deletions(-) diff --git a/index.ts b/index.ts index 29960f9..cc090fa 100644 --- a/index.ts +++ b/index.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import { parseArgs } from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; @@ -15,10 +16,10 @@ const { values: args, positionals } = parseArgs({ n: { type: "boolean" }, config: { type: "boolean" }, c: { type: "boolean" }, - v: { type: "boolean" }, plain: { type: "boolean" }, plainOut: { type: "boolean" }, plainErr: { type: "boolean" }, + v: { type: "boolean" }, }, strict: true, allowPositionals: true, diff --git a/package.json b/package.json index 66c76b6..8607d62 100644 --- a/package.json +++ b/package.json @@ -4,35 +4,36 @@ "type": "module", "private": true, "bin": { - "lmc": "dist/lmc" + "lmc": "dist/index.js" }, "scripts": { - "dev": "pnpm run bundle &>/dev/null && node dist/index.cjs", + "dev": "pnpm run bundle &>/dev/null && node dist/index.js", "check": "biome check --write && pnpm run tc", - "build": "pnpm run check && pnpm run bundle --minify && pnpm run bin", - "bundle": "esbuild index.ts --bundle --outfile=dist/index.cjs --platform=node", + "clean": "rm -rf dist", + "build": "pnpm run check && pnpm run bundle --minify", + "build:bin": "pnpm run check && pnpm run bundle:bin --minify && pnpm run bin", + "bundle": "rolldown index.ts --dir=dist --platform=node", + "bundle:bin": "rolldown index.ts --dir=dist --platform=node --no-code-splitting --format=cjs", "bin": "node --build-sea sea-config.json", "tc": "tsc --noEmit --incremental" }, "devDependencies": { - "@biomejs/biome": "2.4.12", - "@types/bun": "^1.3.14", - "esbuild": "0.28.0", "@ai-sdk/openai-compatible": "^2.0.41", + "@biomejs/biome": "2.4.12", "@clack/prompts": "^1.3.0", "@cliffy/table": "npm:@jsr/cliffy__table", "@mozilla/readability": "^0.6.0", + "@types/bun": "^1.3.14", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", "happy-dom-without-node": "^14.12.3", "marked": "^18.0.4", + "rolldown": "^1.0.3", "shiki": "^4.0.2", "string-width": "^8.2.1", "terminal-link": "^5.0.0", - "wrap-ansi": "^10.0.0", - "typescript": "^5" - }, - "peerDependencies": {}, - "dependencies": {} + "typescript": "^5", + "wrap-ansi": "^10.0.0" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76b5399..fec87b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 1.4.0 '@cliffy/table': specifier: npm:@jsr/cliffy__table - version: '@jsr/cliffy__table@1.1.0' + version: '@jsr/cliffy__table@1.1.1' '@mozilla/readability': specifier: ^0.6.0 version: 0.6.0 @@ -35,15 +35,15 @@ importers: chalk: specifier: ^5.6.2 version: 5.6.2 - esbuild: - specifier: 0.28.0 - version: 0.28.0 happy-dom-without-node: specifier: ^14.12.3 version: 14.12.3 marked: specifier: ^18.0.4 version: 18.0.4 + rolldown: + specifier: ^1.0.3 + version: 1.0.3 shiki: specifier: ^4.0.2 version: 4.1.0 @@ -149,164 +149,17 @@ packages: resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} engines: {node: '>= 20.12.0'} - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@jsr/cliffy__table@1.1.0': - resolution: {integrity: sha512-EYKlk3L0mpJoJYD27+BlaLsjaTGbcG6Sun+HffbSRcxGD1xCuldKPynFKmsoos967OAAeYfOlrjJeD93Y//Ucg==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.1.0.tgz} + '@jsr/cliffy__table@1.1.1': + resolution: {integrity: sha512-LcmLDrR/5uUzNklMNCqIK2L8RYdyyX27/QydFxxzi+HeB5sMyjOWOI636HPbYBGcr+gB7xlNGRi3EYgBXZnfWg==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.1.1.tgz} '@jsr/std__fmt@1.0.10': resolution: {integrity: sha512-Q2N8XDNkh/c1IKTi+sO4Vm64M5m4W0o47qTURmMX0vgHm9AJ6jL7jEQ2kw0M1T2rBZiwAY78eUbYzFyOYWaCqQ==, tarball: https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.10.tgz} @@ -315,10 +168,117 @@ packages: resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} engines: {node: '>=14.0.0'} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@shikijs/core@4.1.0': resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==} engines: {node: '>=20'} @@ -353,6 +313,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/bun@1.3.14': resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} @@ -427,11 +390,6 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} - engines: {node: '>=18'} - hasBin: true - eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -514,6 +472,11 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + shiki@4.1.0: resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==} engines: {node: '>=20'} @@ -554,6 +517,9 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -678,85 +644,23 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@esbuild/aix-ppc64@0.28.0': + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 optional: true - '@esbuild/android-arm64@0.28.0': + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-arm@0.28.0': + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-x64@0.28.0': - optional: true - - '@esbuild/darwin-arm64@0.28.0': - optional: true - - '@esbuild/darwin-x64@0.28.0': - optional: true - - '@esbuild/freebsd-arm64@0.28.0': - optional: true - - '@esbuild/freebsd-x64@0.28.0': - optional: true - - '@esbuild/linux-arm64@0.28.0': - optional: true - - '@esbuild/linux-arm@0.28.0': - optional: true - - '@esbuild/linux-ia32@0.28.0': - optional: true - - '@esbuild/linux-loong64@0.28.0': - optional: true - - '@esbuild/linux-mips64el@0.28.0': - optional: true - - '@esbuild/linux-ppc64@0.28.0': - optional: true - - '@esbuild/linux-riscv64@0.28.0': - optional: true - - '@esbuild/linux-s390x@0.28.0': - optional: true - - '@esbuild/linux-x64@0.28.0': - optional: true - - '@esbuild/netbsd-arm64@0.28.0': - optional: true - - '@esbuild/netbsd-x64@0.28.0': - optional: true - - '@esbuild/openbsd-arm64@0.28.0': - optional: true - - '@esbuild/openbsd-x64@0.28.0': - optional: true - - '@esbuild/openharmony-arm64@0.28.0': - optional: true - - '@esbuild/sunos-x64@0.28.0': - optional: true - - '@esbuild/win32-arm64@0.28.0': - optional: true - - '@esbuild/win32-ia32@0.28.0': - optional: true - - '@esbuild/win32-x64@0.28.0': - optional: true - - '@jsr/cliffy__table@1.1.0': + '@jsr/cliffy__table@1.1.1': dependencies: '@jsr/std__fmt': 1.0.10 @@ -764,8 +668,68 @@ snapshots: '@mozilla/readability@0.6.0': {} + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + '@opentelemetry/api@1.9.1': {} + '@oxc-project/types@0.133.0': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@shikijs/core@4.1.0': dependencies: '@shikijs/primitive': 4.1.0 @@ -808,6 +772,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + '@types/bun@1.3.14': dependencies: bun-types: 1.3.14 @@ -870,35 +839,6 @@ snapshots: environment@1.1.0: {} - esbuild@0.28.0: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 - eventsource-parser@3.0.8: {} fast-string-truncated-width@3.0.3: {} @@ -997,6 +937,27 @@ snapshots: dependencies: regex-utilities: 2.3.0 + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + shiki@4.1.0: dependencies: '@shikijs/core': 4.1.0 @@ -1044,6 +1005,9 @@ snapshots: trim-lines@3.0.1: {} + tslib@2.8.1: + optional: true + typescript@5.9.3: {} undici-types@7.24.6: {} diff --git a/sea-config.json b/sea-config.json index 83b28b4..4beec69 100644 --- a/sea-config.json +++ b/sea-config.json @@ -1,5 +1,5 @@ { - "main": "dist/index.cjs", + "main": "dist/index.js", "output": "dist/lmc", "useCodeCache": true, "disableExperimentalSEAWarning": true From 4912864a75a4fd3a18321284d0cd15b27fa6d848 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 7 Jun 2026 16:11:08 -0400 Subject: [PATCH 42/76] chore: update build scripts and refine markdown rendering --- package.json | 13 +++++++------ pnpm-workspace.yaml | 2 -- src/render/markdown.ts | 9 +++++---- 3 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index 8607d62..45f8a6d 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,18 @@ "type": "module", "private": true, "bin": { - "lmc": "dist/index.js" + "lmc": "dist/index.js", + "ttsc": "dist/tts.js" }, "scripts": { "dev": "pnpm run bundle &>/dev/null && node dist/index.js", "check": "biome check --write && pnpm run tc", "clean": "rm -rf dist", - "build": "pnpm run check && pnpm run bundle --minify", - "build:bin": "pnpm run check && pnpm run bundle:bin --minify && pnpm run bin", - "bundle": "rolldown index.ts --dir=dist --platform=node", - "bundle:bin": "rolldown index.ts --dir=dist --platform=node --no-code-splitting --format=cjs", - "bin": "node --build-sea sea-config.json", + "build": "pnpm run check && pnpm run bundle --minify index.ts && pnpm run bundle --minify tts.ts", + "build:bin": "pnpm run check && pnpm run bundle:bin --minify index.ts && pnpm run bundle:bin --minify tts.ts && pnpm run bin", + "bundle": "rolldown --dir=dist --platform=node --transform.target=node22", + "bundle:bin": "rolldown --dir=dist --platform=node --no-code-splitting --format=cjs", + "bin": "node --build-sea sea-config.json && node --build-sea sea-config-tts.json", "tc": "tsc --noEmit --incremental" }, "devDependencies": { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 5ed0b5a..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -allowBuilds: - esbuild: true diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 905c003..e3271ae 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -130,12 +130,12 @@ class LmcRenderer extends Renderer { override strong({ tokens }: Tokens.Strong) { const text = this.parser.parseInline(tokens); - return chalk.bold.rgb(254, 254, 254)(text); + return chalk.bold.rgb(245, 245, 245)(text); } override em({ tokens }: Tokens.Em) { const text = this.parser.parseInline(tokens); - return chalk.italic.rgb(254, 254, 254)(text); + return chalk.italic.rgb(245, 245, 245)(text); } override del({ tokens }: Tokens.Del) { @@ -144,7 +144,7 @@ class LmcRenderer extends Renderer { } override codespan({ text }: Tokens.Codespan) { - return chalk.bgRgb(165, 165, 190).black.bold(text); + return chalk.bgRgb(192, 192, 192).black.bold(text); } override link({ href, tokens }: Tokens.Link) { @@ -192,7 +192,8 @@ export async function renderMarkdown( const langs = md .match(/```\S+\n/g) - ?.map((lang) => lang.replace("```", "").trimEnd()); + ?.map((lang) => lang.replace("```", "").trimEnd()) + .sort(); if (langs && langs?.join() !== highligherLangs) { const { bundledThemes, bundledLanguages, bundledLanguagesAlias } = From 3235a086334b954a787a6fb6e60646cf96116403 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 7 Jun 2026 22:13:19 -0400 Subject: [PATCH 43/76] refactor(tts): replace Bun-specific APIs with execa and Node.js stdlib - Replace `$` and `Bun.spawn` with `execa` for process execution - Swap `Bun.JSONL.parse` for native `JSON.parse` and use `node:readline` for stdin handling --- package.json | 3 +- pnpm-lock.yaml | 268 +++++++++++++++++++++++++++++++++++++++++-------- tts.ts | 89 +++++++++------- 3 files changed, 277 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 45f8a6d..51abcf1 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", - "happy-dom-without-node": "^14.12.3", + "execa": "^9.6.1", + "happy-dom": "^20.10.2", "marked": "^18.0.4", "rolldown": "^1.0.3", "shiki": "^4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fec87b7..5644768 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 1.4.0 '@cliffy/table': specifier: npm:@jsr/cliffy__table - version: '@jsr/cliffy__table@1.1.1' + version: '@jsr/cliffy__table@1.2.1' '@mozilla/readability': specifier: ^0.6.0 version: 0.6.0 @@ -35,9 +35,12 @@ importers: chalk: specifier: ^5.6.2 version: 5.6.2 - happy-dom-without-node: - specifier: ^14.12.3 - version: 14.12.3 + execa: + specifier: ^9.6.1 + version: 9.6.1 + happy-dom: + specifier: ^20.10.2 + version: 20.10.2 marked: specifier: ^18.0.4 version: 18.0.4 @@ -158,8 +161,8 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@jsr/cliffy__table@1.1.1': - resolution: {integrity: sha512-LcmLDrR/5uUzNklMNCqIK2L8RYdyyX27/QydFxxzi+HeB5sMyjOWOI636HPbYBGcr+gB7xlNGRi3EYgBXZnfWg==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.1.1.tgz} + '@jsr/cliffy__table@1.2.1': + resolution: {integrity: sha512-C/edvcMwtnbFx0zyoeGRcC+ZRvGYqxEhRi19HpfbfiYS4ZErKjEwMJmavO/DN7Wxbk34UpJP+UQKL7nKe+dYkA==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.2.1.tgz} '@jsr/std__fmt@1.0.10': resolution: {integrity: sha512-Q2N8XDNkh/c1IKTi+sO4Vm64M5m4W0o47qTURmMX0vgHm9AJ6jL7jEQ2kw0M1T2rBZiwAY78eUbYzFyOYWaCqQ==, tarball: https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.10.tgz} @@ -279,6 +282,9 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/core@4.1.0': resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==} engines: {node: '>=20'} @@ -310,6 +316,10 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -331,6 +341,12 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} @@ -356,6 +372,10 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + bun-types@1.3.14: resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} @@ -375,6 +395,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -382,8 +406,8 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} environment@1.1.0: @@ -394,6 +418,10 @@ packages: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -403,13 +431,21 @@ packages: fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} - happy-dom-without-node@14.12.3: - resolution: {integrity: sha512-1wp8+GFneT8mBjVnzancXHRuscEUH3vnb38lfCHPxuSu6OiRw1kQzHFbTGYlNCU2YXOlVIlzsS6xg9nAr7Xg6Q==} - engines: {node: '>=16.0.0'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + happy-dom@20.10.2: + resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==} + engines: {node: '>=20.0.0'} has-flag@5.0.1: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} @@ -424,6 +460,25 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -450,19 +505,35 @@ packages: micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + oniguruma-parser@0.12.2: resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -477,10 +548,22 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + shiki@4.1.0: resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==} engines: {node: '>=20'} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -498,6 +581,10 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -510,10 +597,6 @@ packages: resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} engines: {node: '>=20'} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -528,6 +611,10 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -549,22 +636,35 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true wrap-ansi@10.0.0: resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} engines: {node: '>=20'} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -660,7 +760,7 @@ snapshots: tslib: 2.8.1 optional: true - '@jsr/cliffy__table@1.1.1': + '@jsr/cliffy__table@1.2.1': dependencies: '@jsr/std__fmt': 1.0.10 @@ -730,6 +830,8 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/core@4.1.0': dependencies: '@shikijs/primitive': 4.1.0 @@ -770,6 +872,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/spec@1.1.0': {} '@tybys/wasm-util@0.10.2': @@ -795,6 +899,12 @@ snapshots: '@types/unist@3.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.1 + '@ungap/structured-clone@1.3.1': {} '@vercel/oidc@3.2.0': {} @@ -815,6 +925,10 @@ snapshots: ansi-styles@6.2.3: {} + buffer-image-size@0.6.4: + dependencies: + '@types/node': 25.9.1 + bun-types@1.3.14: dependencies: '@types/node': 25.9.1 @@ -829,18 +943,39 @@ snapshots: comma-separated-tokens@2.0.3: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dequal@2.0.3: {} devlop@1.1.0: dependencies: dequal: 2.0.3 - entities@4.5.0: {} + entities@7.0.1: {} environment@1.1.0: {} eventsource-parser@3.0.8: {} + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -851,14 +986,29 @@ snapshots: dependencies: fast-string-width: 3.0.2 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + get-east-asian-width@1.6.0: {} - happy-dom-without-node@14.12.3: + get-stream@9.0.1: dependencies: - entities: 4.5.0 - webidl-conversions: 7.0.0 + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + happy-dom@20.10.2: + dependencies: + '@types/node': 25.9.1 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 whatwg-mimetype: 3.0.0 - whatwg-url: 14.2.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate has-flag@5.0.1: {} @@ -882,6 +1032,16 @@ snapshots: html-void-elements@3.0.0: {} + human-signals@8.0.1: {} + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + isexe@2.0.0: {} + json-schema@0.4.0: {} marked@18.0.4: {} @@ -915,6 +1075,11 @@ snapshots: micromark-util-types@2.0.2: {} + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + oniguruma-parser@0.12.2: {} oniguruma-to-es@4.3.6: @@ -923,9 +1088,17 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - property-information@7.1.0: {} + parse-ms@4.0.0: {} - punycode@2.3.1: {} + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + property-information@7.1.0: {} regex-recursion@6.0.2: dependencies: @@ -958,6 +1131,12 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.3 '@rolldown/binding-win32-x64-msvc': 1.0.3 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + shiki@4.1.0: dependencies: '@shikijs/core': 4.1.0 @@ -969,6 +1148,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} space-separated-tokens@2.0.2: {} @@ -987,6 +1168,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-final-newline@4.0.0: {} + supports-color@10.2.2: {} supports-hyperlinks@4.4.0: @@ -999,10 +1182,6 @@ snapshots: ansi-escapes: 7.3.0 supports-hyperlinks: 4.4.0 - tr46@5.1.1: - dependencies: - punycode: 2.3.1 - trim-lines@3.0.1: {} tslib@2.8.1: @@ -1012,6 +1191,8 @@ snapshots: undici-types@7.24.6: {} + unicorn-magic@0.3.0: {} + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -1045,14 +1226,11 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - webidl-conversions@7.0.0: {} - whatwg-mimetype@3.0.0: {} - whatwg-url@14.2.0: + which@2.0.2: dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 + isexe: 2.0.0 wrap-ansi@10.0.0: dependencies: @@ -1060,6 +1238,10 @@ snapshots: string-width: 8.2.1 strip-ansi: 7.2.0 + ws@8.21.0: {} + + yoctocolors@2.1.2: {} + zod@4.4.3: {} zwitch@2.0.4: {} diff --git a/tts.ts b/tts.ts index fc131a5..af47e3e 100644 --- a/tts.ts +++ b/tts.ts @@ -1,15 +1,17 @@ import os from "node:os"; import process from "node:process"; +import { createInterface } from "node:readline"; import { parseArgs } from "node:util"; import { Readability } from "@mozilla/readability"; -import { $, which } from "bun"; import chalk from "chalk"; -import { Window } from "happy-dom-without-node"; +import { execa } from "execa"; +import { Window } from "happy-dom"; +import wrapAnsi from "wrap-ansi"; const start = Date.now(); const { values: args } = parseArgs({ - args: Bun.argv, + args: process.argv, options: { wait: { type: "boolean" }, w: { type: "boolean" }, @@ -22,7 +24,15 @@ const { values: args } = parseArgs({ const wait = args.wait ?? args.w; const url = args.url ?? args.u; -let input = url ? url : await Bun.stdin.text(); + +let input = url + ? url + : await new Promise((resolve) => { + let data = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => (data += chunk)); + process.stdin.on("end", () => resolve(data)); + }); if (url !== undefined) { const result = await fetch(input.trim()); @@ -45,9 +55,8 @@ if (!input) { const home = os.homedir(); const cwd = `${home}/.ttsc/`; -$.cwd(cwd); -await $`rm *.wav`.nothrow(); +await execa({ cwd, shell: true, reject: false })`rm *.wav`; console.log(chalk.blue("Welcome to TTSC!")); console.log( @@ -56,16 +65,16 @@ console.log( ), ); -const koko = Bun.spawn({ - cmd: ["uv", "run", "--", "python", "-u", "main.py"], +const koko = execa({ cwd, - stdin: Buffer.from(input), + input: Buffer.from(input), // execa v6+ uses 'input'. Use 'stdin' for v5 stdout: "pipe", stderr: "inherit", -}); +})`uv run -- python -u main.py`; if (!wait) { - koko.exited.then(() => { + koko.catch(() => {}); + koko.then(() => { const stop = Date.now(); const duration = ((stop - start) / 1000).toFixed(2); console.log(chalk.green(`\nProcessing finished: ${duration}s`)); @@ -73,15 +82,17 @@ if (!wait) { } let first = true; -const decoder = new TextDecoder(); -for await (const line of koko.stdout) { - const decoded = decoder.decode(line).trim(); +const rl = createInterface({ input: koko.stdout, crlfDelay: Infinity }); - let infos: { file: number; text: string }[]; +for await (const line of rl) { + const trimmed = line.trim(); + if (!trimmed) continue; + + let info: { file: number; text: string }; try { - infos = Bun.JSONL.parse(decoded) as { file: number; text: string }[]; + info = JSON.parse(trimmed); } catch { - console.log(decoded); + console.log(trimmed); continue; } @@ -92,25 +103,22 @@ for await (const line of koko.stdout) { console.log(chalk.green(`\nTime-to-audio: ${duration}s`)); } - for (const info of infos) { - const [columns] = process.stdout.getWindowSize(); - const text = Bun.wrapAnsi(info.text, columns); - const file = `${home}/.ttsc/${info.file}.wav`; - console.log(`\n${chalk.blue(file)}:\n${text}\n`); + const [columns] = process.stdout.getWindowSize(); + const text = wrapAnsi(info.text, columns); + const file = `${home}/.ttsc/${info.file}.wav`; + console.log(`\n${chalk.blue(file)}:\n${text}\n`); - if (!wait) { - await Bun.spawn({ - cmd: ["mpv", file], - cwd, - stdin: "ignore", - stdout: "inherit", - stderr: "inherit", - }).exited; - } + if (!wait) { + await execa({ + cwd, + stdin: "ignore", + stdout: "inherit", + stderr: "inherit", + })`mpv ${file}`; } } -await koko.exited; +await koko; if (!wait) process.exit(); const stop = Date.now(); @@ -118,19 +126,22 @@ const duration = ((stop - start) / 1000).toFixed(2); console.log(chalk.green(`\nProcessing finished: ${duration}s`)); console.log(chalk.yellow(`Combining audio with sox...`)); -if (which("sox")) { - await $`sox *.wav output.wav`; +try { + await execa({ cwd, shell: true })`sox *.wav output.wav`; console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); -} else { - console.error("Sox not found. Unable to combine outputs."); +} catch (err) { + if (err instanceof Object && "code" in err && err.code === "ENOENT") { + console.error("Sox not found. Unable to combine outputs."); + } else { + throw err; + } console.log(chalk.yellow(`Audio available in ${cwd}`)); process.exit(); } -await Bun.spawn({ - cmd: ["mpv", "--pause", "output.wav"], +await execa({ cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit", -}).exited; +})`mpv --pause output.wav`; From 39cb573619d87344fba3abb4cd145692ca114f06 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 8 Jun 2026 15:17:07 -0400 Subject: [PATCH 44/76] refactor(tts): make cjs compatible and add sea config --- sea-config-tts.json | 6 ++ tts.ts | 251 +++++++++++++++++++++++--------------------- 2 files changed, 135 insertions(+), 122 deletions(-) create mode 100644 sea-config-tts.json diff --git a/sea-config-tts.json b/sea-config-tts.json new file mode 100644 index 0000000..1f147f7 --- /dev/null +++ b/sea-config-tts.json @@ -0,0 +1,6 @@ +{ + "main": "dist/tts.js", + "output": "dist/ttsc", + "useCodeCache": true, + "disableExperimentalSEAWarning": true +} diff --git a/tts.ts b/tts.ts index af47e3e..1343c24 100644 --- a/tts.ts +++ b/tts.ts @@ -8,140 +8,147 @@ import { execa } from "execa"; import { Window } from "happy-dom"; import wrapAnsi from "wrap-ansi"; -const start = Date.now(); +async function main() { + const start = Date.now(); -const { values: args } = parseArgs({ - args: process.argv, - options: { - wait: { type: "boolean" }, - w: { type: "boolean" }, - url: { type: "string" }, - u: { type: "string" }, - }, - strict: true, - allowPositionals: true, -}); + const { values: args } = parseArgs({ + args: process.argv.slice(2), + options: { + wait: { type: "boolean" }, + w: { type: "boolean" }, + url: { type: "string" }, + u: { type: "string" }, + }, + strict: true, + allowPositionals: false, + }); -const wait = args.wait ?? args.w; -const url = args.url ?? args.u; + const wait = args.wait ?? args.w; + const url = args.url ?? args.u; -let input = url - ? url - : await new Promise((resolve) => { - let data = ""; - process.stdin.setEncoding("utf8"); - process.stdin.on("data", (chunk) => (data += chunk)); - process.stdin.on("end", () => resolve(data)); - }); + let input = url + ? url + : await new Promise((resolve) => { + let data = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => (data += chunk)); + process.stdin.on("end", () => resolve(data)); + }); -if (url !== undefined) { - const result = await fetch(input.trim()); - const window = new Window({ url: input.trim() }); - const doc = window.document; - doc.write(await result.text()); - await window.happyDOM.waitUntilComplete(); - const readable = new Readability(doc).parse(); - if (readable?.textContent) input = readable.textContent; - if (input.trim() === result.url) { - console.error("Unable to read web page."); + if (url !== undefined) { + const result = await fetch(input.trim()); + const window = new Window({ url: input.trim() }); + const doc = window.document; + doc.write(await result.text()); + await window.happyDOM.waitUntilComplete(); + const readable = new Readability(doc).parse(); + if (readable?.textContent) input = readable.textContent; + if (input.trim() === result.url) { + console.error("Unable to read web page."); + process.exit(1); + } + } + + if (!input) { + console.error("No input given."); process.exit(1); } -} -if (!input) { - console.error("No input given."); - process.exit(1); -} + const home = os.homedir(); + const cwd = `${home}/.ttsc/`; -const home = os.homedir(); -const cwd = `${home}/.ttsc/`; + await execa({ cwd, shell: true, reject: false })`rm *.wav`; -await execa({ cwd, shell: true, reject: false })`rm *.wav`; + console.log(chalk.blue("Welcome to TTSC!")); + console.log( + chalk.yellow( + "Beginning TTS processing... (This will take extra time the first time)", + ), + ); -console.log(chalk.blue("Welcome to TTSC!")); -console.log( - chalk.yellow( - "Beginning TTS processing... (This will take extra time the first time)", - ), -); - -const koko = execa({ - cwd, - input: Buffer.from(input), // execa v6+ uses 'input'. Use 'stdin' for v5 - stdout: "pipe", - stderr: "inherit", -})`uv run -- python -u main.py`; - -if (!wait) { - koko.catch(() => {}); - koko.then(() => { - const stop = Date.now(); - const duration = ((stop - start) / 1000).toFixed(2); - console.log(chalk.green(`\nProcessing finished: ${duration}s`)); - }); -} - -let first = true; -const rl = createInterface({ input: koko.stdout, crlfDelay: Infinity }); - -for await (const line of rl) { - const trimmed = line.trim(); - if (!trimmed) continue; - - let info: { file: number; text: string }; - try { - info = JSON.parse(trimmed); - } catch { - console.log(trimmed); - continue; - } - - if (first) { - first = false; - const stop = Date.now(); - const duration = ((stop - start) / 1000).toFixed(2); - console.log(chalk.green(`\nTime-to-audio: ${duration}s`)); - } - - const [columns] = process.stdout.getWindowSize(); - const text = wrapAnsi(info.text, columns); - const file = `${home}/.ttsc/${info.file}.wav`; - console.log(`\n${chalk.blue(file)}:\n${text}\n`); + const koko = execa({ + cwd, + input, + stdout: "pipe", + stderr: "inherit", + })`uv run -- python -u main.py`; if (!wait) { - await execa({ - cwd, - stdin: "ignore", - stdout: "inherit", - stderr: "inherit", - })`mpv ${file}`; + koko.catch(() => {}); + koko.then(() => { + const stop = Date.now(); + const duration = ((stop - start) / 1000).toFixed(2); + console.log(chalk.green(`\nProcessing finished: ${duration}s`)); + }); } + + let first = true; + const rl = createInterface({ input: koko.stdout, crlfDelay: Infinity }); + + for await (const line of rl) { + const trimmed = line.trim(); + if (!trimmed) continue; + + let info: { file: number; text: string }; + try { + info = JSON.parse(trimmed); + } catch { + console.log(trimmed); + continue; + } + + if (first) { + first = false; + const stop = Date.now(); + const duration = ((stop - start) / 1000).toFixed(2); + console.log(chalk.green(`\nTime-to-audio: ${duration}s`)); + } + + const [columns] = process.stdout.getWindowSize(); + const text = wrapAnsi(info.text, columns); + const file = `${home}/.ttsc/${info.file}.wav`; + console.log(`\n${chalk.blue(file)}:\n${text}\n`); + + if (!wait) { + await execa({ + cwd, + stdin: "ignore", + stdout: "inherit", + stderr: "inherit", + })`mpv ${file}`; + } + } + + await koko; + if (!wait) process.exit(); + + const stop = Date.now(); + const duration = ((stop - start) / 1000).toFixed(2); + console.log(chalk.green(`\nProcessing finished: ${duration}s`)); + + console.log(chalk.yellow(`Combining audio with sox...`)); + try { + await execa({ cwd, shell: true })`sox *.wav output.wav`; + console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); + } catch (err) { + if (err instanceof Object && "code" in err && err.code === "ENOENT") { + console.error("Sox not found. Unable to combine outputs."); + } else { + throw err; + } + console.log(chalk.yellow(`Audio available in ${cwd}`)); + process.exit(); + } + + await execa({ + cwd, + stdin: "ignore", + stdout: "inherit", + stderr: "inherit", + })`mpv --pause output.wav`; } -await koko; -if (!wait) process.exit(); - -const stop = Date.now(); -const duration = ((stop - start) / 1000).toFixed(2); -console.log(chalk.green(`\nProcessing finished: ${duration}s`)); - -console.log(chalk.yellow(`Combining audio with sox...`)); -try { - await execa({ cwd, shell: true })`sox *.wav output.wav`; - console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); -} catch (err) { - if (err instanceof Object && "code" in err && err.code === "ENOENT") { - console.error("Sox not found. Unable to combine outputs."); - } else { - throw err; - } - console.log(chalk.yellow(`Audio available in ${cwd}`)); - process.exit(); -} - -await execa({ - cwd, - stdin: "ignore", - stdout: "inherit", - stderr: "inherit", -})`mpv --pause output.wav`; +main().catch((err) => { + console.error(err); + process.exit(1); +}); From a03e6e94a9e87af9fb52fce3909dfb590f0f5db3 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 8 Jun 2026 16:09:28 -0400 Subject: [PATCH 45/76] feat(tts): add --edit flag and fix CLI argument parsing - Improved input trimming and console output formatting in `tts.ts` - Updated codespan colors in the markdown renderer for better visual contrast - Disabled `noPropertyAccessFromIndexSignature` in tsconfig.json --- index.ts | 4 ++-- src/render/markdown.ts | 2 +- tsconfig.json | 4 ++-- tts.ts | 38 ++++++++++++++++++++++++++++++-------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/index.ts b/index.ts index cc090fa..41f7431 100644 --- a/index.ts +++ b/index.ts @@ -8,7 +8,7 @@ import { Conversation } from "./src/conversation"; import Renderer from "./src/render"; const { values: args, positionals } = parseArgs({ - args: process.argv, + args: process.argv.slice(2), options: { model: { type: "string" }, m: { type: "string" }, @@ -62,7 +62,7 @@ async function main() { if (modelFlag) convo.model = modelFlag; } - const prompt = positionals.slice(2).join(" ") || "Introduce yourself."; + const prompt = positionals.join(" ") || "Introduce yourself."; convo.messages.push({ role: "user", content: prompt }); const { fullStream, text, totalUsage } = streamText({ diff --git a/src/render/markdown.ts b/src/render/markdown.ts index e3271ae..9e8269e 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -144,7 +144,7 @@ class LmcRenderer extends Renderer { } override codespan({ text }: Tokens.Codespan) { - return chalk.bgRgb(192, 192, 192).black.bold(text); + return chalk.bgRgb(192, 192, 192).rgb(24, 24, 24).bold(text); } override link({ href, tokens }: Tokens.Link) { diff --git a/tsconfig.json b/tsconfig.json index edeab3e..d5dde93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,12 +23,12 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true, + "noPropertyAccessFromIndexSignature": false, // Recommended Options "strict": true, "jsx": "react-jsx", - //"allowImportingTsExtensions": true, + "allowImportingTsExtensions": false, "allowArbitraryExtensions": true, "verbatimModuleSyntax": true, "isolatedModules": true, diff --git a/tts.ts b/tts.ts index 1343c24..c6be5bc 100644 --- a/tts.ts +++ b/tts.ts @@ -1,3 +1,4 @@ +import fs from "node:fs/promises"; import os from "node:os"; import process from "node:process"; import { createInterface } from "node:readline"; @@ -18,6 +19,8 @@ async function main() { w: { type: "boolean" }, url: { type: "string" }, u: { type: "string" }, + edit: { type: "boolean" }, + e: { type: "boolean" }, }, strict: true, allowPositionals: false, @@ -25,6 +28,7 @@ async function main() { const wait = args.wait ?? args.w; const url = args.url ?? args.u; + const edit = args.edit ?? args.e; let input = url ? url @@ -34,16 +38,17 @@ async function main() { process.stdin.on("data", (chunk) => (data += chunk)); process.stdin.on("end", () => resolve(data)); }); + input = input.trim(); if (url !== undefined) { - const result = await fetch(input.trim()); - const window = new Window({ url: input.trim() }); + const result = await fetch(input); + const window = new Window({ url: input }); const doc = window.document; doc.write(await result.text()); await window.happyDOM.waitUntilComplete(); const readable = new Readability(doc).parse(); - if (readable?.textContent) input = readable.textContent; - if (input.trim() === result.url) { + if (readable?.textContent) input = readable.textContent.trim(); + if (input === result.url) { console.error("Unable to read web page."); process.exit(1); } @@ -59,6 +64,18 @@ async function main() { await execa({ cwd, shell: true, reject: false })`rm *.wav`; + if (edit) { + const file = await fs.open(`${cwd}input.txt`, "w+"); + await file.writeFile(input); + const editor = process.env.EDITOR || "nano"; + await execa({ + stdin: "inherit", + stdout: "inherit", + })`${editor} ${cwd}input.txt`; + input = await file.readFile("utf8"); + await file.close(); + } + console.log(chalk.blue("Welcome to TTSC!")); console.log( chalk.yellow( @@ -107,9 +124,11 @@ async function main() { const [columns] = process.stdout.getWindowSize(); const text = wrapAnsi(info.text, columns); const file = `${home}/.ttsc/${info.file}.wav`; - console.log(`\n${chalk.blue(file)}:\n${text}\n`); - if (!wait) { + if (wait) { + console.write("."); + } else { + console.log(`\n${chalk.blue(file)}\n${text}\n`); await execa({ cwd, stdin: "ignore", @@ -126,10 +145,9 @@ async function main() { const duration = ((stop - start) / 1000).toFixed(2); console.log(chalk.green(`\nProcessing finished: ${duration}s`)); - console.log(chalk.yellow(`Combining audio with sox...`)); + console.log(chalk.yellow(`Combining audio with sox...\n`)); try { await execa({ cwd, shell: true })`sox *.wav output.wav`; - console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); } catch (err) { if (err instanceof Object && "code" in err && err.code === "ENOENT") { console.error("Sox not found. Unable to combine outputs."); @@ -140,6 +158,10 @@ async function main() { process.exit(); } + const [columns] = process.stdout.getWindowSize(); + console.log(wrapAnsi(input, columns)); + + console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); await execa({ cwd, stdin: "ignore", From 21dac683a22e4f614c20cfc8eb5d1da35570acf4 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 8 Jun 2026 20:17:22 -0400 Subject: [PATCH 46/76] refactor: migrate from bun to node types and fix discrepancies --- package.json | 4 ++-- pnpm-lock.yaml | 27 ++++++++++----------------- src/config/index.ts | 4 ++-- src/conversation/index.ts | 4 ++-- testRender.ts | 6 ------ tsconfig.json | 2 +- tts.ts | 2 +- 7 files changed, 18 insertions(+), 31 deletions(-) delete mode 100644 testRender.ts diff --git a/package.json b/package.json index 51abcf1..c2b7580 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ttsc": "dist/tts.js" }, "scripts": { - "dev": "pnpm run bundle &>/dev/null && node dist/index.js", + "dev": "pnpm run bundle index.ts &>/dev/null && node dist/index.js", "check": "biome check --write && pnpm run tc", "clean": "rm -rf dist", "build": "pnpm run check && pnpm run bundle --minify index.ts && pnpm run bundle --minify tts.ts", @@ -24,7 +24,7 @@ "@clack/prompts": "^1.3.0", "@cliffy/table": "npm:@jsr/cliffy__table", "@mozilla/readability": "^0.6.0", - "@types/bun": "^1.3.14", + "@types/node": "^25.9.2", "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5644768..03d92d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,9 @@ importers: '@mozilla/readability': specifier: ^0.6.0 version: 0.6.0 - '@types/bun': - specifier: ^1.3.14 - version: 1.3.14 + '@types/node': + specifier: ^25.9.2 + version: 25.9.2 ai: specifier: ^6.0.161 version: 6.0.191(zod@4.4.3) @@ -326,9 +326,6 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - '@types/bun@1.3.14': - resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -338,6 +335,9 @@ packages: '@types/node@25.9.1': resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@25.9.2': + resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -376,9 +376,6 @@ packages: resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} engines: {node: '>=4.0'} - bun-types@1.3.14: - resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -881,10 +878,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/bun@1.3.14': - dependencies: - bun-types: 1.3.14 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -897,6 +890,10 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/node@25.9.2': + dependencies: + undici-types: 7.24.6 + '@types/unist@3.0.3': {} '@types/whatwg-mimetype@3.0.2': {} @@ -929,10 +926,6 @@ snapshots: dependencies: '@types/node': 25.9.1 - bun-types@1.3.14: - dependencies: - '@types/node': 25.9.1 - ccount@2.0.1: {} chalk@5.6.2: {} diff --git a/src/config/index.ts b/src/config/index.ts index 9e23618..f885994 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -67,8 +67,8 @@ export async function loadConfig( ): Promise { let config: AppConfig; try { - const text = await fs.readFile(configPath); - config = JSON.parse(text.toString("utf8")); + const text = await fs.readFile(configPath, "utf8"); + config = JSON.parse(text); } catch { if (configFlag) return null; console.error(`Config not found at ${configPath}`); diff --git a/src/conversation/index.ts b/src/conversation/index.ts index 6d408c2..96b1923 100644 --- a/src/conversation/index.ts +++ b/src/conversation/index.ts @@ -17,8 +17,8 @@ export class Conversation implements IConversation { async load() { try { - const text = await fs.readFile(convoPath); - const convo: IConversation = JSON.parse(text.toString("utf8")); + const text = await fs.readFile(convoPath, "utf8"); + const convo: IConversation = JSON.parse(text); this.model = convo.model; this.messages = convo.messages; } catch { diff --git a/testRender.ts b/testRender.ts deleted file mode 100644 index 4fecfd9..0000000 --- a/testRender.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { renderMarkdown } from "./src/render/markdown"; - -const text = await Bun.file("README.md").text(); - -const [columns] = process.stdout.getWindowSize(); -process.stdout.write(await renderMarkdown(text, columns)); diff --git a/tsconfig.json b/tsconfig.json index d5dde93..d593bb5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "noEmit": true, // For nodejs: "lib": ["esnext"], - "types": ["bun"], + "types": ["node"], // and npm install -D @types/node // Stricter Typechecking Options diff --git a/tts.ts b/tts.ts index c6be5bc..976da80 100644 --- a/tts.ts +++ b/tts.ts @@ -126,7 +126,7 @@ async function main() { const file = `${home}/.ttsc/${info.file}.wav`; if (wait) { - console.write("."); + process.stdout.write("."); } else { console.log(`\n${chalk.blue(file)}\n${text}\n`); await execa({ From 574ebdc8a1415c6594fe8372eb153d83d172cee6 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 9 Jun 2026 14:21:57 -0400 Subject: [PATCH 47/76] fix(markdown): handle
tokens and adjust codespan text color --- src/render/markdown.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 9e8269e..4450ce0 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -144,7 +144,7 @@ class LmcRenderer extends Renderer { } override codespan({ text }: Tokens.Codespan) { - return chalk.bgRgb(192, 192, 192).rgb(24, 24, 24).bold(text); + return chalk.bgRgb(165, 165, 192).rgb(0, 0, 0).bold(text); } override link({ href, tokens }: Tokens.Link) { @@ -164,8 +164,9 @@ class LmcRenderer extends Renderer { return this.replaceLatex(token.text); } - override html({ text: _ }: Tokens.HTML | Tokens.Tag) { - return chalk.red("\nHTML is unsupported\n"); + override html({ text }: Tokens.HTML | Tokens.Tag) { + if (text === "
") return this.br(); + return chalk.red(text); } override br() { From 7bf8951b620e0e47e6571c34b5196ab07eeb02cd Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 9 Jun 2026 20:18:10 -0400 Subject: [PATCH 48/76] fix(tts): --edit file handling and file globbing --- tts.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tts.ts b/tts.ts index 976da80..8af1144 100644 --- a/tts.ts +++ b/tts.ts @@ -62,18 +62,19 @@ async function main() { const home = os.homedir(); const cwd = `${home}/.ttsc/`; - await execa({ cwd, shell: true, reject: false })`rm *.wav`; + const wavs = await Array.fromAsync(fs.glob(`${cwd}*.wav`)); + await execa({ cwd, shell: true, reject: false })`rm ${wavs}`; if (edit) { - const file = await fs.open(`${cwd}input.txt`, "w+"); - await file.writeFile(input); + const file = `${cwd}input.txt`; + await fs.writeFile(file, input); const editor = process.env.EDITOR || "nano"; await execa({ + cwd, stdin: "inherit", stdout: "inherit", - })`${editor} ${cwd}input.txt`; - input = await file.readFile("utf8"); - await file.close(); + })`${editor} input.txt`; + input = await fs.readFile(file, "utf8"); } console.log(chalk.blue("Welcome to TTSC!")); @@ -100,7 +101,7 @@ async function main() { } let first = true; - const rl = createInterface({ input: koko.stdout, crlfDelay: Infinity }); + const rl = createInterface({ input: koko.stdout }); for await (const line of rl) { const trimmed = line.trim(); @@ -147,7 +148,8 @@ async function main() { console.log(chalk.yellow(`Combining audio with sox...\n`)); try { - await execa({ cwd, shell: true })`sox *.wav output.wav`; + const wavs = await Array.fromAsync(fs.glob(`${cwd}*.wav`)); + await execa({ cwd, shell: true })`sox ${wavs} output.wav`; } catch (err) { if (err instanceof Object && "code" in err && err.code === "ENOENT") { console.error("Sox not found. Unable to combine outputs."); From 32f9dd549595aaa300af8239555123d0f6c46ab7 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 9 Jun 2026 20:33:31 -0400 Subject: [PATCH 49/76] refactor(tts): replace string interpolation with path.join() --- tts.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tts.ts b/tts.ts index 8af1144..907a05c 100644 --- a/tts.ts +++ b/tts.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import os from "node:os"; +import path from "node:path"; import process from "node:process"; import { createInterface } from "node:readline"; import { parseArgs } from "node:util"; @@ -60,13 +61,13 @@ async function main() { } const home = os.homedir(); - const cwd = `${home}/.ttsc/`; + const cwd = path.join(home, ".ttsc"); - const wavs = await Array.fromAsync(fs.glob(`${cwd}*.wav`)); + const wavs = await Array.fromAsync(fs.glob(path.join(cwd, "*.wav"))); await execa({ cwd, shell: true, reject: false })`rm ${wavs}`; if (edit) { - const file = `${cwd}input.txt`; + const file = path.join(cwd, "input.txt"); await fs.writeFile(file, input); const editor = process.env.EDITOR || "nano"; await execa({ @@ -124,7 +125,7 @@ async function main() { const [columns] = process.stdout.getWindowSize(); const text = wrapAnsi(info.text, columns); - const file = `${home}/.ttsc/${info.file}.wav`; + const file = path.join(cwd, `${info.file}.wav`); if (wait) { process.stdout.write("."); @@ -148,7 +149,7 @@ async function main() { console.log(chalk.yellow(`Combining audio with sox...\n`)); try { - const wavs = await Array.fromAsync(fs.glob(`${cwd}*.wav`)); + const wavs = await Array.fromAsync(fs.glob(path.join(cwd, "*.wav"))); await execa({ cwd, shell: true })`sox ${wavs} output.wav`; } catch (err) { if (err instanceof Object && "code" in err && err.code === "ENOENT") { @@ -163,7 +164,9 @@ async function main() { const [columns] = process.stdout.getWindowSize(); console.log(wrapAnsi(input, columns)); - console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); + console.log( + chalk.green(`Playing audio from ${path.join(cwd, "output.wav")}`), + ); await execa({ cwd, stdin: "ignore", From 4674cca337bd645a2e8d45987c055d2f077489f8 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 9 Jun 2026 21:27:08 -0400 Subject: [PATCH 50/76] refactor(tts): replace execa shell command with native fs.rm iteration --- tts.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tts.ts b/tts.ts index 907a05c..844166b 100644 --- a/tts.ts +++ b/tts.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -63,8 +64,8 @@ async function main() { const home = os.homedir(); const cwd = path.join(home, ".ttsc"); - const wavs = await Array.fromAsync(fs.glob(path.join(cwd, "*.wav"))); - await execa({ cwd, shell: true, reject: false })`rm ${wavs}`; + const wavs = fs.glob(path.join(cwd, "*.wav")); + for await (const wav of wavs) await fs.rm(wav); if (edit) { const file = path.join(cwd, "input.txt"); From 0e07268bfb91a73b4072f56f7ca0ef63b7bbad73 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 12 Jun 2026 03:28:01 -0400 Subject: [PATCH 51/76] chore(docs): update README.md --- README.md | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 981bddb..d8ddfa9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lmc -A Bun-based CLI tool for LLM chats with terminal native streaming. +A CLI tool for LLM chats with terminal native streaming. ## Features @@ -14,15 +14,20 @@ A Bun-based CLI tool for LLM chats with terminal native streaming. ## Requirements -- [Bun](https://bun.com) v1.3+ +- [pnpm](https://pnpm.io) v11+ +- [Node.js](https://nodejs.org) v22+ - [uv](https://docs.astral.sh/uv/) — only for `ttsc` (text-to-speech) ## Installation ```bash -bun install -bun build:js -bun link +# Bundle and install locally +pnpm build +pnpm add --global . + +# Build a single-file exe +pnpm build:bin +cp dist/lmc dist/ttsc /somewhere/bin/ # For ttsc only cp -r tts ~/.ttsc @@ -67,6 +72,7 @@ echo "text" | ttsc [options] | -------- | ----- | ------- | -------------------------------------------------------- | | `--wait` | `-w` | boolean | Wait for all processing before exiting (don't auto-play) | | `--url` | `-u` | boolean | Fetch text from a URL instead of stdin | +| `--edit` | `-e` | boolean | Edit the input with your $EDITOR before processing | **Examples:** @@ -86,9 +92,8 @@ Wants: ## Development ```bash -bun run dev # dev run -bun run prod # run bundled output -bun run check # biome lint + format + organize imports +pnpm run dev # dev run +pnpm run check # biome lint + format + organize imports ``` ## Architecture @@ -97,7 +102,7 @@ bun run check # biome lint + format + organize imports index.ts — LMC entry point, orchestration tts.ts — TTSC entry point, orchestration src/render/index.ts — Renderer class: streams LLM response, buffers markdown -src/render/markdown.ts — renderMarkdown(): syntax-highlighted markdown via Bun.markdown + Shiki +src/render/markdown.ts — renderMarkdown(): syntax-highlighted markdown via Marked + Shiki src/conversation/ — Conversation persistence src/config/ — App config persistence tts/ — TTS python stub (Kokoro pipeline) @@ -112,13 +117,14 @@ out/ — Bundled output (index.js, tts.js) ## Dependencies -| Package | Purpose | -| --------------------------- | ----------------------------------------- | -| `ai` (Vercel AI SDK) | Streaming text/usage | -| `@clack/prompts` | Interactive prompts and spinner | -| `@cliffy/table` | ASCII table rendering | -| `@mozilla/readability` | HTML-to-text extraction (ttsc) | -| `chalk` | Terminal string styling | -| `happy-dom` | DOM parsing for URLs (ttsc) | -| `shiki` | Syntax highlighting (kanagawa-wave theme) | -| `terminal-link` | Clickable terminal hyperlinks | +| Package | Purpose | +| ---------------------- | ------------------------------- | +| `ai` (Vercel AI SDK) | Streaming text/usage | +| `@clack/prompts` | Interactive prompts and spinner | +| `@cliffy/table` | ASCII table rendering | +| `@mozilla/readability` | HTML-to-text extraction (ttsc) | +| `chalk` | Terminal string styling | +| `happy-dom` | DOM parsing for URLs (ttsc) | +| `shiki` | Syntax highlighting | +| `terminal-link` | Clickable terminal hyperlinks | +| `marked` | Markdown parsing | From 749559e19cd9ff223cf9af35cd7fa17c8d0e0783 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 12 Jun 2026 19:39:19 -0400 Subject: [PATCH 52/76] fix(tts): sox combination order; refactor input logic flow - Replace manual `process.stdin` listeners with `events.once()` - Switch from `process.exit()` to `return` for cleaner async execution paths - Update Node.js module imports to use default exports consistently --- index.ts | 4 ++-- tts.ts | 63 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/index.ts b/index.ts index 41f7431..45cdd57 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { parseArgs } from "node:util"; +import util from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; @@ -7,7 +7,7 @@ import { initConfig, loadConfig } from "./src/config"; import { Conversation } from "./src/conversation"; import Renderer from "./src/render"; -const { values: args, positionals } = parseArgs({ +const { values: args, positionals } = util.parseArgs({ args: process.argv.slice(2), options: { model: { type: "string" }, diff --git a/tts.ts b/tts.ts index 844166b..7bf3ceb 100644 --- a/tts.ts +++ b/tts.ts @@ -1,10 +1,11 @@ #!/usr/bin/env node +import events from "node:events"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import process from "node:process"; -import { createInterface } from "node:readline"; -import { parseArgs } from "node:util"; +import readline from "node:readline"; +import util from "node:util"; import { Readability } from "@mozilla/readability"; import chalk from "chalk"; import { execa } from "execa"; @@ -14,7 +15,7 @@ import wrapAnsi from "wrap-ansi"; async function main() { const start = Date.now(); - const { values: args } = parseArgs({ + const { values: args } = util.parseArgs({ args: process.argv.slice(2), options: { wait: { type: "boolean" }, @@ -32,41 +33,34 @@ async function main() { const url = args.url ?? args.u; const edit = args.edit ?? args.e; - let input = url - ? url - : await new Promise((resolve) => { - let data = ""; - process.stdin.setEncoding("utf8"); - process.stdin.on("data", (chunk) => (data += chunk)); - process.stdin.on("end", () => resolve(data)); - }); - input = input.trim(); + let input = ""; - if (url !== undefined) { - const result = await fetch(input); - const window = new Window({ url: input }); + if (url?.trim()) { + const result = await fetch(url.trim()); + const window = new Window({ url: url.trim() }); const doc = window.document; doc.write(await result.text()); await window.happyDOM.waitUntilComplete(); const readable = new Readability(doc).parse(); - if (readable?.textContent) input = readable.textContent.trim(); - if (input === result.url) { - console.error("Unable to read web page."); - process.exit(1); + if (!readable?.textContent) { + throw "Unable to read web page."; } + input = readable.textContent.trim(); + } else { + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (data) => { + input += data; + }); + await events.once(process.stdin, "end"); } if (!input) { - console.error("No input given."); - process.exit(1); + throw "No input given."; } const home = os.homedir(); const cwd = path.join(home, ".ttsc"); - const wavs = fs.glob(path.join(cwd, "*.wav")); - for await (const wav of wavs) await fs.rm(wav); - if (edit) { const file = path.join(cwd, "input.txt"); await fs.writeFile(file, input); @@ -75,7 +69,8 @@ async function main() { cwd, stdin: "inherit", stdout: "inherit", - })`${editor} input.txt`; + stderr: "inherit", + })`${editor} ${file}`; input = await fs.readFile(file, "utf8"); } @@ -86,6 +81,9 @@ async function main() { ), ); + const wavs = fs.glob(path.join(cwd, "*.wav")); + for await (const wav of wavs) await fs.rm(wav); + const koko = execa({ cwd, input, @@ -103,7 +101,8 @@ async function main() { } let first = true; - const rl = createInterface({ input: koko.stdout }); + const files = []; + const rl = readline.createInterface({ input: koko.stdout }); for await (const line of rl) { const trimmed = line.trim(); @@ -124,13 +123,14 @@ async function main() { console.log(chalk.green(`\nTime-to-audio: ${duration}s`)); } - const [columns] = process.stdout.getWindowSize(); - const text = wrapAnsi(info.text, columns); const file = path.join(cwd, `${info.file}.wav`); if (wait) { + files.push(file); process.stdout.write("."); } else { + const [columns] = process.stdout.getWindowSize(); + const text = wrapAnsi(info.text, columns); console.log(`\n${chalk.blue(file)}\n${text}\n`); await execa({ cwd, @@ -142,7 +142,7 @@ async function main() { } await koko; - if (!wait) process.exit(); + if (!wait) return; const stop = Date.now(); const duration = ((stop - start) / 1000).toFixed(2); @@ -150,8 +150,7 @@ async function main() { console.log(chalk.yellow(`Combining audio with sox...\n`)); try { - const wavs = await Array.fromAsync(fs.glob(path.join(cwd, "*.wav"))); - await execa({ cwd, shell: true })`sox ${wavs} output.wav`; + await execa({ cwd, shell: true })`sox ${files} output.wav`; } catch (err) { if (err instanceof Object && "code" in err && err.code === "ENOENT") { console.error("Sox not found. Unable to combine outputs."); @@ -159,7 +158,7 @@ async function main() { throw err; } console.log(chalk.yellow(`Audio available in ${cwd}`)); - process.exit(); + return; } const [columns] = process.stdout.getWindowSize(); From dd6df6dd6dbda5c76da939f362df0d34880fcdbc Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 14 Jun 2026 15:39:41 -0400 Subject: [PATCH 53/76] refactor(tts): CLI argument parsing and input handling --- tts.ts | 52 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/tts.ts b/tts.ts index 7bf3ceb..559ecf6 100644 --- a/tts.ts +++ b/tts.ts @@ -12,41 +12,34 @@ import { execa } from "execa"; import { Window } from "happy-dom"; import wrapAnsi from "wrap-ansi"; +class InputError extends Error { + override name = "InputError"; +} + async function main() { const start = Date.now(); - const { values: args } = util.parseArgs({ + const { values: args, positionals } = util.parseArgs({ args: process.argv.slice(2), options: { wait: { type: "boolean" }, w: { type: "boolean" }, - url: { type: "string" }, - u: { type: "string" }, + url: { type: "boolean" }, + u: { type: "boolean" }, edit: { type: "boolean" }, e: { type: "boolean" }, }, strict: true, - allowPositionals: false, + allowPositionals: true, }); const wait = args.wait ?? args.w; const url = args.url ?? args.u; const edit = args.edit ?? args.e; - let input = ""; + let input = positionals.join(" ").trim(); - if (url?.trim()) { - const result = await fetch(url.trim()); - const window = new Window({ url: url.trim() }); - const doc = window.document; - doc.write(await result.text()); - await window.happyDOM.waitUntilComplete(); - const readable = new Readability(doc).parse(); - if (!readable?.textContent) { - throw "Unable to read web page."; - } - input = readable.textContent.trim(); - } else { + if (!input) { process.stdin.setEncoding("utf8"); process.stdin.on("data", (data) => { input += data; @@ -55,7 +48,24 @@ async function main() { } if (!input) { - throw "No input given."; + throw new InputError( + "No input was given. Use positional arguments or stdin.", + ); + } + + if (url) { + const result = await fetch(input); + const window = new Window({ url: input }); + const doc = window.document; + doc.write(await result.text()); + await window.happyDOM.waitUntilComplete(); + const readable = new Readability(doc).parse(); + if (!readable?.textContent) { + throw new InputError( + "Unable to read webpage. This usually means it's blocking scrapers.", + ); + } + input = readable.textContent.trim(); } const home = os.homedir(); @@ -63,7 +73,7 @@ async function main() { if (edit) { const file = path.join(cwd, "input.txt"); - await fs.writeFile(file, input); + await fs.writeFile(file, input, "utf8"); const editor = process.env.EDITOR || "nano"; await execa({ cwd, @@ -154,11 +164,11 @@ async function main() { } catch (err) { if (err instanceof Object && "code" in err && err.code === "ENOENT") { console.error("Sox not found. Unable to combine outputs."); + console.log(chalk.yellow(`Audio available in ${cwd}`)); + return; } else { throw err; } - console.log(chalk.yellow(`Audio available in ${cwd}`)); - return; } const [columns] = process.stdout.getWindowSize(); From 710d2e75937305bda250ca7ec2db7f03be8d5fdf Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 15 Jun 2026 02:42:20 -0400 Subject: [PATCH 54/76] build: switch binary bundler to esbuild - Add explicit `--format=esm` flag to `build` - Update `rolldown` to ^1.1.1 and add `esbuild` ^0.28.1 as a dependency - Create `pnpm-workspace.yaml` with `allowBuilds: esbuild` to support native prebuilt binaries across platforms --- package.json | 7 +- pnpm-lock.yaml | 445 +++++++++++++++++++++++++++++++++++--------- pnpm-workspace.yaml | 2 + 3 files changed, 364 insertions(+), 90 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index c2b7580..23a5b72 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "clean": "rm -rf dist", "build": "pnpm run check && pnpm run bundle --minify index.ts && pnpm run bundle --minify tts.ts", "build:bin": "pnpm run check && pnpm run bundle:bin --minify index.ts && pnpm run bundle:bin --minify tts.ts && pnpm run bin", - "bundle": "rolldown --dir=dist --platform=node --transform.target=node22", - "bundle:bin": "rolldown --dir=dist --platform=node --no-code-splitting --format=cjs", + "bundle": "rolldown --dir=dist --platform=node --transform.target=node22 --format=esm", + "bundle:bin": "esbuild --bundle --outdir=dist --platform=node --target=node26 --format=cjs", "bin": "node --build-sea sea-config.json && node --build-sea sea-config-tts.json", "tc": "tsc --noEmit --incremental" }, @@ -28,10 +28,11 @@ "ai": "^6.0.161", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", + "esbuild": "^0.28.1", "execa": "^9.6.1", "happy-dom": "^20.10.2", "marked": "^18.0.4", - "rolldown": "^1.0.3", + "rolldown": "^1.1.1", "shiki": "^4.0.2", "string-width": "^8.2.1", "terminal-link": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03d92d4..22f3aaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: chalk: specifier: ^5.6.2 version: 5.6.2 + esbuild: + specifier: ^0.28.1 + version: 0.28.1 execa: specifier: ^9.6.1 version: 9.6.1 @@ -45,8 +48,8 @@ importers: specifier: ^18.0.4 version: 18.0.4 rolldown: - specifier: ^1.0.3 - version: 1.0.3 + specifier: ^1.1.1 + version: 1.1.1 shiki: specifier: ^4.0.2 version: 4.1.0 @@ -152,14 +155,170 @@ packages: resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} engines: {node: '>= 20.12.0'} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@jsr/cliffy__table@1.2.1': resolution: {integrity: sha512-C/edvcMwtnbFx0zyoeGRcC+ZRvGYqxEhRi19HpfbfiYS4ZErKjEwMJmavO/DN7Wxbk34UpJP+UQKL7nKe+dYkA==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.2.1.tgz} @@ -171,8 +330,8 @@ packages: resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} engines: {node: '>=14.0.0'} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -181,100 +340,100 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.135.0': + resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==} - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.1': + resolution: {integrity: sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.1': + resolution: {integrity: sha512-rRZRPy/Ynb+Mxu0O6tfPldHeDgAn0sRij+IOUy6sFdUlv3hArGW/DloE3GfAxtqpOJuRNgF74Nr5gM4xBeU2jQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.1': + resolution: {integrity: sha512-/MtefPxhKPyWWFM8L45OWiEqRf+eSU2Qv9ZAyTaoZOoGcoPKxbbhjTJO2/U2IThv0uDZ4NWHc3/oTsR6IEOtww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.1': + resolution: {integrity: sha512-202K+cpIi1kx/Zn7AtxBi4LTXSY67Aszb2K9rNsuW7FeBeh0nqoNmYLOSZidV0p88VPBzMmTZcHAdPNo3kRYzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-wl9NfeXNUwrXtUc063tddmZFUI6qiNs1CNOwni0OL4vC7MqVSYugra3ZgtDmtVy8e0DluJTENmzIv2BwqLzT4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-at2EO4o7D/PJLC4Xik16bU4CcjQE2tSv1LfqMA0TRYQYQihRm3gZeDB8xaX28A9SFedibcAk5DeMCKt4REKG0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-5PUjZx366h9tkJTPJF5eibxOlK3sGoeRiBJLLjjEB5/kLDuhr6qB3LkhqLz1smXNgsX+pBhnbcJBrPE30HznAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-1WK84XPeio3tjP1sM/TMXiC0G1i1iq1qGZ71KfNQjEFLU1kwD+Cv5T8nGySg/JUFwLbaScu6ve9DmeXlmqpkFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-1nS1X5z1uMJ369RU25hTpKCFvUwXZp12dIzlzk4S+UxCTcSVGsAE6tzkOSufv/7jnmAtK0ZlrsJxh2fGmsnVSw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-NwX/wspnq4vYyMFsqbYvzums3ki/Tk8FZbMzMAovPDp3OfLeYKby/D+9osokadXuYEV3OvpeHlwnr/bG8QMixA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-+n46LhDrJFQM+229y4oXtVpj1G50U/+XuHMlpnisFTEXhrg9f/YIjp/HymX+PVJjBEr7XHRs3CFLelV464pqwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.1': + resolution: {integrity: sha512-qGwEu47zOWYo7LdRHhCWTNhzwGtxXpdY6CERs8QEOqC0PXGGics/e3vHnyEUKt8xK6YkbZXFUCeklrpB6js8ag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.1': + resolution: {integrity: sha512-qczfgEH8u0wHGGOXtA7UMAybNKuQjjEXairyQaw4WzjiMztfbgatG1h4OKays/smhtwbWltpKCRGtVhU6h40Sg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-4psXSh63mSbwJF+mB8/9yfUUEzBiHYcUjxa32EO9ZwKy0Ypwjcg4F10D8SvVXgd+isy2UUUjF9HJJnDu1T/4Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-MUvC/HLXVjzkQkWiExdVTEEWf0py+GfWm8WKSZsekG3ih6a21iy0BHPF07X3JIf3ifoklZXTIaHTLPBgH1C3dw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -411,6 +570,11 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -540,8 +704,8 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.1: + resolution: {integrity: sha512-IN750c0p+s3jqJIsFLRZrQazmbAB1kkQDTtQjSt/gbS2ywLhlv4R5Shazer0FZKmuo/BsO3/w2UoYnUjuOZqHg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -741,22 +905,100 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@emnapi/core@1.10.0': + '@emnapi/core@1.11.0': dependencies: - '@emnapi/wasi-threads': 1.2.1 + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@1.11.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + '@jsr/cliffy__table@1.2.1': dependencies: '@jsr/std__fmt': 1.0.10 @@ -765,64 +1007,64 @@ snapshots: '@mozilla/readability@0.6.0': {} - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 '@tybys/wasm-util': 0.10.2 optional: true '@opentelemetry/api@1.9.1': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.135.0': {} - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.1': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.1': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.1': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.1': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.1': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.1': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.1': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.1': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.1': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.1': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.1': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.1': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.1': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.1': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.1': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -952,6 +1194,35 @@ snapshots: environment@1.1.0: {} + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + eventsource-parser@3.0.8: {} execa@9.6.1: @@ -1103,26 +1374,26 @@ snapshots: dependencies: regex-utilities: 2.3.0 - rolldown@1.0.3: + rolldown@1.1.1: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.135.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.1 + '@rolldown/binding-darwin-arm64': 1.1.1 + '@rolldown/binding-darwin-x64': 1.1.1 + '@rolldown/binding-freebsd-x64': 1.1.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.1 + '@rolldown/binding-linux-arm64-gnu': 1.1.1 + '@rolldown/binding-linux-arm64-musl': 1.1.1 + '@rolldown/binding-linux-ppc64-gnu': 1.1.1 + '@rolldown/binding-linux-s390x-gnu': 1.1.1 + '@rolldown/binding-linux-x64-gnu': 1.1.1 + '@rolldown/binding-linux-x64-musl': 1.1.1 + '@rolldown/binding-openharmony-arm64': 1.1.1 + '@rolldown/binding-wasm32-wasi': 1.1.1 + '@rolldown/binding-win32-arm64-msvc': 1.1.1 + '@rolldown/binding-win32-x64-msvc': 1.1.1 shebang-command@2.0.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..5ed0b5a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true From fc7d9101e9646b64c67234b0850848ffe6b3f869 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 16 Jun 2026 16:45:17 -0400 Subject: [PATCH 55/76] refactor(tts): remove InputError class and use native Error --- tts.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tts.ts b/tts.ts index 559ecf6..94cce7b 100644 --- a/tts.ts +++ b/tts.ts @@ -12,10 +12,6 @@ import { execa } from "execa"; import { Window } from "happy-dom"; import wrapAnsi from "wrap-ansi"; -class InputError extends Error { - override name = "InputError"; -} - async function main() { const start = Date.now(); @@ -48,9 +44,7 @@ async function main() { } if (!input) { - throw new InputError( - "No input was given. Use positional arguments or stdin.", - ); + throw new Error("No input was given. Use positional arguments or stdin."); } if (url) { @@ -61,7 +55,7 @@ async function main() { await window.happyDOM.waitUntilComplete(); const readable = new Readability(doc).parse(); if (!readable?.textContent) { - throw new InputError( + throw new Error( "Unable to read webpage. This usually means it's blocking scrapers.", ); } From f3146ab087b58f177c85eb692d0e536e316b5e28 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 16 Jun 2026 16:57:36 -0400 Subject: [PATCH 56/76] feat(lmc): add --git-message and --review modes, standardize args casing --- index.ts | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index 45cdd57..35c5ec7 100644 --- a/index.ts +++ b/index.ts @@ -17,9 +17,14 @@ const { values: args, positionals } = util.parseArgs({ config: { type: "boolean" }, c: { type: "boolean" }, plain: { type: "boolean" }, - plainOut: { type: "boolean" }, - plainErr: { type: "boolean" }, + "plain-out": { type: "boolean" }, + "plain-err": { type: "boolean" }, + version: { type: "boolean" }, v: { type: "boolean" }, + "git-message": { type: "boolean" }, + g: { type: "boolean" }, + review: { type: "boolean" }, + r: { type: "boolean" }, }, strict: true, allowPositionals: true, @@ -27,12 +32,15 @@ const { values: args, positionals } = util.parseArgs({ async function main() { const newConvo = args.new ?? args.n; - const plainOut = args.plainOut ?? args.plain ?? !process.stdout.isTTY; - const plainErr = args.plainErr ?? args.plain ?? !process.stderr.isTTY; + const plainOut = args["plain-out"] ?? args.plain ?? !process.stdout.isTTY; + const plainErr = args["plain-err"] ?? args.plain ?? !process.stderr.isTTY; const modelFlag = args.model ?? args.m; + const versionFlag = args.version ?? args.v; const configFlag = args.config ?? args.c; + const messageFlag = args["git-message"] ?? args.g; + const reviewFlag = args.review ?? args.r; - if (args.v) { + if (versionFlag) { console.log("0.0.0"); return; } @@ -62,7 +70,23 @@ async function main() { if (modelFlag) convo.model = modelFlag; } - const prompt = positionals.join(" ") || "Introduce yourself."; + let prompt = "Introduce yourself."; + const input = positionals.join(" ").trim(); + if (messageFlag) { + prompt = `\`\`\`diff +${input} +\`\`\` + +Generate a conventional commit message for this diff. If you are unable to generate a reasonable option, ask for more context.`; + } else if (reviewFlag) { + prompt = `\`\`\`diff +${input} +\`\`\` + +Review the above diff for any potential bugs, best practices, performance issues, or security flaws. Let me know if you need any more context to compelete your review.`; + } else if (input) { + prompt = input; + } convo.messages.push({ role: "user", content: prompt }); const { fullStream, text, totalUsage } = streamText({ From 56352d87ecfb51aeebfc9efab2b5c938526bf91e Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 16 Jun 2026 18:58:33 -0400 Subject: [PATCH 57/76] fix(entry): improve error message formatting - Relocate `util.parseArgs()` inside the main function for cleaner scoping - Update `.catch` handlers in `index.ts` and `tts.ts` to wrap text to terminal width and colorize errors in red --- index.ts | 53 +++++++++++++++++++++++++++++------------------------ tts.ts | 5 ++++- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/index.ts b/index.ts index 35c5ec7..e7c52a3 100644 --- a/index.ts +++ b/index.ts @@ -2,35 +2,37 @@ import util from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; +import chalk from "chalk"; +import wrapAnsi from "wrap-ansi"; import { initConfig, loadConfig } from "./src/config"; import { Conversation } from "./src/conversation"; import Renderer from "./src/render"; -const { values: args, positionals } = util.parseArgs({ - args: process.argv.slice(2), - options: { - model: { type: "string" }, - m: { type: "string" }, - new: { type: "boolean" }, - n: { type: "boolean" }, - config: { type: "boolean" }, - c: { type: "boolean" }, - plain: { type: "boolean" }, - "plain-out": { type: "boolean" }, - "plain-err": { type: "boolean" }, - version: { type: "boolean" }, - v: { type: "boolean" }, - "git-message": { type: "boolean" }, - g: { type: "boolean" }, - review: { type: "boolean" }, - r: { type: "boolean" }, - }, - strict: true, - allowPositionals: true, -}); - async function main() { + const { values: args, positionals } = util.parseArgs({ + args: process.argv.slice(2), + options: { + model: { type: "string" }, + m: { type: "string" }, + new: { type: "boolean" }, + n: { type: "boolean" }, + config: { type: "boolean" }, + c: { type: "boolean" }, + plain: { type: "boolean" }, + "plain-out": { type: "boolean" }, + "plain-err": { type: "boolean" }, + version: { type: "boolean" }, + v: { type: "boolean" }, + "git-message": { type: "boolean" }, + g: { type: "boolean" }, + review: { type: "boolean" }, + r: { type: "boolean" }, + }, + strict: true, + allowPositionals: true, + }); + const newConvo = args.new ?? args.n; const plainOut = args["plain-out"] ?? args.plain ?? !process.stdout.isTTY; const plainErr = args["plain-err"] ?? args.plain ?? !process.stderr.isTTY; @@ -103,6 +105,9 @@ Review the above diff for any potential bugs, best practices, performance issues } main().catch((err) => { - console.error(err); + if ("message" in err) { + const [columns] = process.stderr.getWindowSize(); + console.error(wrapAnsi(chalk.red(err.message), columns)); + } else console.log(err); process.exit(1); }); diff --git a/tts.ts b/tts.ts index 94cce7b..06d3693 100644 --- a/tts.ts +++ b/tts.ts @@ -180,6 +180,9 @@ async function main() { } main().catch((err) => { - console.error(err); + if ("message" in err) { + const [columns] = process.stderr.getWindowSize(); + console.error(wrapAnsi(chalk.red(err.message), columns)); + } else console.log(err); process.exit(1); }); From f8e0f9a3682ddca20feadbc78388e14cf73dcd73 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 17 Jun 2026 00:48:15 -0400 Subject: [PATCH 58/76] refactor(errors): extract top-level error handling into shared utility --- index.ts | 11 ++--------- src/errors.ts | 22 ++++++++++++++++++++++ tts.ts | 9 ++------- 3 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 src/errors.ts diff --git a/index.ts b/index.ts index e7c52a3..baff42e 100644 --- a/index.ts +++ b/index.ts @@ -2,11 +2,10 @@ import util from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; -import chalk from "chalk"; -import wrapAnsi from "wrap-ansi"; import { initConfig, loadConfig } from "./src/config"; import { Conversation } from "./src/conversation"; +import { handleTopLevel } from "./src/errors"; import Renderer from "./src/render"; async function main() { @@ -104,10 +103,4 @@ Review the above diff for any potential bugs, best practices, performance issues await renderer.renderStats(totalUsage); } -main().catch((err) => { - if ("message" in err) { - const [columns] = process.stderr.getWindowSize(); - console.error(wrapAnsi(chalk.red(err.message), columns)); - } else console.log(err); - process.exit(1); -}); +main().catch(handleTopLevel); diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..a8e9e05 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,22 @@ +import { chalkStderr } from "chalk"; +import wrapAnsi from "wrap-ansi"; + +export function handleTopLevel(err: unknown) { + if ( + typeof err === "object" && + err !== null && + "message" in err && + typeof err.message === "string" + ) { + if (process.stderr.isTTY) { + const [columns] = process.stderr.getWindowSize(); + console.error(wrapAnsi(chalkStderr.red(err.message), columns)); + } else { + console.error(err.message); + } + } else { + console.error(String(err)); + } + + process.exit(1); +} diff --git a/tts.ts b/tts.ts index 06d3693..920f146 100644 --- a/tts.ts +++ b/tts.ts @@ -11,6 +11,7 @@ import chalk from "chalk"; import { execa } from "execa"; import { Window } from "happy-dom"; import wrapAnsi from "wrap-ansi"; +import { handleTopLevel } from "./src/errors"; async function main() { const start = Date.now(); @@ -179,10 +180,4 @@ async function main() { })`mpv --pause output.wav`; } -main().catch((err) => { - if ("message" in err) { - const [columns] = process.stderr.getWindowSize(); - console.error(wrapAnsi(chalk.red(err.message), columns)); - } else console.log(err); - process.exit(1); -}); +main().catch(handleTopLevel); From 3c3dab15256dde1e4d84563625cd8f6f3b53d675 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 17 Jun 2026 01:12:38 -0400 Subject: [PATCH 59/76] chore: move entry files to src/ directory and update paths --- package.json | 8 ++++---- index.ts => src/index.ts | 8 ++++---- tts.ts => src/tts.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename index.ts => src/index.ts (93%) rename tts.ts => src/tts.ts (99%) diff --git a/package.json b/package.json index 23a5b72..c64f686 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lmc", - "module": "index.ts", + "module": "src/index.ts", "type": "module", "private": true, "bin": { @@ -8,11 +8,11 @@ "ttsc": "dist/tts.js" }, "scripts": { - "dev": "pnpm run bundle index.ts &>/dev/null && node dist/index.js", + "dev": "pnpm run bundle src/index.ts &>/dev/null && node dist/index.js", "check": "biome check --write && pnpm run tc", "clean": "rm -rf dist", - "build": "pnpm run check && pnpm run bundle --minify index.ts && pnpm run bundle --minify tts.ts", - "build:bin": "pnpm run check && pnpm run bundle:bin --minify index.ts && pnpm run bundle:bin --minify tts.ts && pnpm run bin", + "build": "pnpm run check && pnpm run bundle --minify src/index.ts && pnpm run bundle --minify src/tts.ts", + "build:bin": "pnpm run check && pnpm run bundle:bin --minify src/index.ts && pnpm run bundle:bin --minify src/tts.ts && pnpm run bin", "bundle": "rolldown --dir=dist --platform=node --transform.target=node22 --format=esm", "bundle:bin": "esbuild --bundle --outdir=dist --platform=node --target=node26 --format=cjs", "bin": "node --build-sea sea-config.json && node --build-sea sea-config-tts.json", diff --git a/index.ts b/src/index.ts similarity index 93% rename from index.ts rename to src/index.ts index baff42e..a99fe41 100644 --- a/index.ts +++ b/src/index.ts @@ -3,10 +3,10 @@ import util from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; -import { initConfig, loadConfig } from "./src/config"; -import { Conversation } from "./src/conversation"; -import { handleTopLevel } from "./src/errors"; -import Renderer from "./src/render"; +import { initConfig, loadConfig } from "./config"; +import { Conversation } from "./conversation"; +import { handleTopLevel } from "./errors"; +import Renderer from "./render"; async function main() { const { values: args, positionals } = util.parseArgs({ diff --git a/tts.ts b/src/tts.ts similarity index 99% rename from tts.ts rename to src/tts.ts index 920f146..4ac3da5 100644 --- a/tts.ts +++ b/src/tts.ts @@ -11,7 +11,7 @@ import chalk from "chalk"; import { execa } from "execa"; import { Window } from "happy-dom"; import wrapAnsi from "wrap-ansi"; -import { handleTopLevel } from "./src/errors"; +import { handleTopLevel } from "./errors"; async function main() { const start = Date.now(); From eec90209d24cd1e9377b75b9051244a8038e3dba Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 17 Jun 2026 01:25:27 -0400 Subject: [PATCH 60/76] refactor(prompts): extract prompt templates to dedicated module --- src/index.ts | 18 ++++-------------- src/prompts.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 src/prompts.ts diff --git a/src/index.ts b/src/index.ts index a99fe41..4f7b4b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { streamText } from "ai"; import { initConfig, loadConfig } from "./config"; import { Conversation } from "./conversation"; import { handleTopLevel } from "./errors"; +import { Prompts } from "./prompts"; import Renderer from "./render"; async function main() { @@ -73,21 +74,10 @@ async function main() { let prompt = "Introduce yourself."; const input = positionals.join(" ").trim(); - if (messageFlag) { - prompt = `\`\`\`diff -${input} -\`\`\` + if (messageFlag) prompt = Prompts.commit(input); + else if (reviewFlag) prompt = Prompts.review(input); + else if (input) prompt = input; -Generate a conventional commit message for this diff. If you are unable to generate a reasonable option, ask for more context.`; - } else if (reviewFlag) { - prompt = `\`\`\`diff -${input} -\`\`\` - -Review the above diff for any potential bugs, best practices, performance issues, or security flaws. Let me know if you need any more context to compelete your review.`; - } else if (input) { - prompt = input; - } convo.messages.push({ role: "user", content: prompt }); const { fullStream, text, totalUsage } = streamText({ diff --git a/src/prompts.ts b/src/prompts.ts new file mode 100644 index 0000000..94969d4 --- /dev/null +++ b/src/prompts.ts @@ -0,0 +1,15 @@ +export const Prompts = { + default: "Introduce yourself.", + + commit: (input: string) => `\`\`\`diff +${input} +\`\`\` + +Generate a conventional commit message for this diff. If you are unable to generate a reasonable option, ask for more context.`, + + review: (input: string) => `\`\`\`diff +${input} +\`\`\` + +Review the above diff for any potential bugs, best practices, performance issues, or security flaws. Let me know if you need any more context to complete your review.`, +}; From 19272bc0b4060a3ada336b571f8f9f827c3c95fb Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 17 Jun 2026 01:40:53 -0400 Subject: [PATCH 61/76] refactor: flatten source directory structure --- src/{config/index.ts => config.ts} | 0 src/{conversation/index.ts => conversation.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{config/index.ts => config.ts} (100%) rename src/{conversation/index.ts => conversation.ts} (100%) diff --git a/src/config/index.ts b/src/config.ts similarity index 100% rename from src/config/index.ts rename to src/config.ts diff --git a/src/conversation/index.ts b/src/conversation.ts similarity index 100% rename from src/conversation/index.ts rename to src/conversation.ts From 7652e62d47d2789e4a8350f943e39c4ffed82e00 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 17 Jun 2026 12:22:32 -0400 Subject: [PATCH 62/76] refactor(entries): simplify stdin reading with node:stream/consumers --- src/index.ts | 5 ++++- src/tts.ts | 10 ++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4f7b4b4..1fb0c4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import consumers from "node:stream/consumers"; import util from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { streamText } from "ai"; @@ -72,8 +73,10 @@ async function main() { if (modelFlag) convo.model = modelFlag; } + let input = positionals.join(" ").trim(); + if (input === "-") input = await consumers.text(process.stdin); + let prompt = "Introduce yourself."; - const input = positionals.join(" ").trim(); if (messageFlag) prompt = Prompts.commit(input); else if (reviewFlag) prompt = Prompts.review(input); else if (input) prompt = input; diff --git a/src/tts.ts b/src/tts.ts index 4ac3da5..56981d7 100644 --- a/src/tts.ts +++ b/src/tts.ts @@ -1,10 +1,10 @@ #!/usr/bin/env node -import events from "node:events"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import readline from "node:readline"; +import consumers from "node:stream/consumers"; import util from "node:util"; import { Readability } from "@mozilla/readability"; import chalk from "chalk"; @@ -36,13 +36,7 @@ async function main() { let input = positionals.join(" ").trim(); - if (!input) { - process.stdin.setEncoding("utf8"); - process.stdin.on("data", (data) => { - input += data; - }); - await events.once(process.stdin, "end"); - } + if (!input) input = await consumers.text(process.stdin); if (!input) { throw new Error("No input was given. Use positional arguments or stdin."); From 4bccbcc9ee7075f48078940738b33db6b0d734d9 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 17 Jun 2026 22:48:54 -0400 Subject: [PATCH 63/76] feat(cli): add --file flag for file review and refactor prompt generation - Inject current date into review prompts to improve LLM context accuracy - Remove unused `module` field from package.json --- package.json | 1 - src/index.ts | 24 +++++++++++++++++++----- src/prompts.ts | 25 ++++++++++++++++++------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index c64f686..cda4c8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "lmc", - "module": "src/index.ts", "type": "module", "private": true, "bin": { diff --git a/src/index.ts b/src/index.ts index 1fb0c4f..e3044a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import fs from "node:fs/promises"; import consumers from "node:stream/consumers"; import util from "node:util"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; @@ -7,7 +8,7 @@ import { streamText } from "ai"; import { initConfig, loadConfig } from "./config"; import { Conversation } from "./conversation"; import { handleTopLevel } from "./errors"; -import { Prompts } from "./prompts"; +import { commitDiff, file, reviewDiff, reviewFile } from "./prompts"; import Renderer from "./render"; async function main() { @@ -29,6 +30,8 @@ async function main() { g: { type: "boolean" }, review: { type: "boolean" }, r: { type: "boolean" }, + file: { type: "string" }, + f: { type: "string" }, }, strict: true, allowPositionals: true, @@ -42,6 +45,7 @@ async function main() { const configFlag = args.config ?? args.c; const messageFlag = args["git-message"] ?? args.g; const reviewFlag = args.review ?? args.r; + const fileFlag = args.file ?? args.f; if (versionFlag) { console.log("0.0.0"); @@ -76,10 +80,20 @@ async function main() { let input = positionals.join(" ").trim(); if (input === "-") input = await consumers.text(process.stdin); - let prompt = "Introduce yourself."; - if (messageFlag) prompt = Prompts.commit(input); - else if (reviewFlag) prompt = Prompts.review(input); - else if (input) prompt = input; + let prompt = input; + if (messageFlag) { + prompt = commitDiff(input); + } else if (reviewFlag && fileFlag) { + const content = await fs.readFile(fileFlag, "utf8"); + prompt = reviewFile(file(fileFlag, content)); + } else if (fileFlag) { + const content = await fs.readFile(fileFlag, "utf8"); + prompt = `${file(fileFlag, content)}\n\n${input}`; + } else if (reviewFlag) { + prompt = reviewDiff(input); + } + + if (!prompt) throw new Error("No prompt given."); convo.messages.push({ role: "user", content: prompt }); diff --git a/src/prompts.ts b/src/prompts.ts index 94969d4..daf1735 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -1,15 +1,26 @@ -export const Prompts = { - default: "Introduce yourself.", +export const file = (name: string, input: string) => `${name}: +\`\`\` +${input} +\`\`\``; - commit: (input: string) => `\`\`\`diff +export const commitDiff = (input: string) => `\`\`\`diff ${input} \`\`\` -Generate a conventional commit message for this diff. If you are unable to generate a reasonable option, ask for more context.`, +Generate a conventional commit message for this diff. If you are unable to generate a reasonable option, ask for more context.`; - review: (input: string) => `\`\`\`diff +export const reviewFile = (input: string) => `${input} + +Todays date is ${new Date().toISOString()}, your version info is likely out-of-date. + +Review the included file for any potential bugs, best practices, performance issues, or security flaws. +Let me know if you need any more context to complete your review.`; + +export const reviewDiff = (input: string) => `\`\`\`diff ${input} \`\`\` -Review the above diff for any potential bugs, best practices, performance issues, or security flaws. Let me know if you need any more context to complete your review.`, -}; +Todays date is ${new Date().toISOString()}, your version info is likely out-of-date. + +Review the included diff for any potential bugs, best practices, performance issues, or security flaws. +Let me know if you need any more context to complete your review.`; From 4725f148f3ef9b40fa823c3433a2e5687bb14273 Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 18 Jun 2026 16:13:39 -0400 Subject: [PATCH 64/76] chore: migrate @cliffy/table to native jsr protocol & fix prompt typos --- .npmrc | 1 - package.json | 2 +- pnpm-lock.yaml | 2 +- src/prompts.ts | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 .npmrc diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 41583e3..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -@jsr:registry=https://npm.jsr.io diff --git a/package.json b/package.json index cda4c8c..4f28a1e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@ai-sdk/openai-compatible": "^2.0.41", "@biomejs/biome": "2.4.12", "@clack/prompts": "^1.3.0", - "@cliffy/table": "npm:@jsr/cliffy__table", + "@cliffy/table": "jsr:^1.2.1", "@mozilla/readability": "^0.6.0", "@types/node": "^25.9.2", "ai": "^6.0.161", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22f3aaa..b897059 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,7 +18,7 @@ importers: specifier: ^1.3.0 version: 1.4.0 '@cliffy/table': - specifier: npm:@jsr/cliffy__table + specifier: jsr:^1.2.1 version: '@jsr/cliffy__table@1.2.1' '@mozilla/readability': specifier: ^0.6.0 diff --git a/src/prompts.ts b/src/prompts.ts index daf1735..21f1a27 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -11,7 +11,7 @@ Generate a conventional commit message for this diff. If you are unable to gener export const reviewFile = (input: string) => `${input} -Todays date is ${new Date().toISOString()}, your version info is likely out-of-date. +Today's date is ${new Date().toISOString()}, your version info is likely out-of-date. Review the included file for any potential bugs, best practices, performance issues, or security flaws. Let me know if you need any more context to complete your review.`; @@ -20,7 +20,7 @@ export const reviewDiff = (input: string) => `\`\`\`diff ${input} \`\`\` -Todays date is ${new Date().toISOString()}, your version info is likely out-of-date. +Today's date is ${new Date().toISOString()}, your version info is likely out-of-date. Review the included diff for any potential bugs, best practices, performance issues, or security flaws. Let me know if you need any more context to complete your review.`; From e2253b912dd733edde380973a0c73db9fccbf347 Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 18 Jun 2026 17:47:09 -0400 Subject: [PATCH 65/76] chore(deps): update dependencies and configure pnpm minimum release age --- package.json | 16 ++-- pnpm-lock.yaml | 197 +++++++++++++++++++++----------------------- pnpm-workspace.yaml | 1 + 3 files changed, 104 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index 4f28a1e..690ac1a 100644 --- a/package.json +++ b/package.json @@ -18,24 +18,24 @@ "tc": "tsc --noEmit --incremental" }, "devDependencies": { - "@ai-sdk/openai-compatible": "^2.0.41", + "@ai-sdk/openai-compatible": "^2.0.50", "@biomejs/biome": "2.4.12", - "@clack/prompts": "^1.3.0", + "@clack/prompts": "^1.5.1", "@cliffy/table": "jsr:^1.2.1", "@mozilla/readability": "^0.6.0", - "@types/node": "^25.9.2", - "ai": "^6.0.161", + "@types/node": "^25.9.3", + "ai": "^6.0.206", "ansi-styles": "^6.2.3", "chalk": "^5.6.2", "esbuild": "^0.28.1", "execa": "^9.6.1", - "happy-dom": "^20.10.2", - "marked": "^18.0.4", + "happy-dom": "^20.10.3", + "marked": "^18.0.5", "rolldown": "^1.1.1", - "shiki": "^4.0.2", + "shiki": "^4.2.0", "string-width": "^8.2.1", "terminal-link": "^5.0.0", - "typescript": "^5", + "typescript": "^5.9.3", "wrap-ansi": "^10.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b897059..d46eb49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,14 +9,14 @@ importers: .: devDependencies: '@ai-sdk/openai-compatible': - specifier: ^2.0.41 - version: 2.0.48(zod@4.4.3) + specifier: ^2.0.50 + version: 2.0.50(zod@4.4.3) '@biomejs/biome': specifier: 2.4.12 version: 2.4.12 '@clack/prompts': - specifier: ^1.3.0 - version: 1.4.0 + specifier: ^1.5.1 + version: 1.5.1 '@cliffy/table': specifier: jsr:^1.2.1 version: '@jsr/cliffy__table@1.2.1' @@ -24,11 +24,11 @@ importers: specifier: ^0.6.0 version: 0.6.0 '@types/node': - specifier: ^25.9.2 - version: 25.9.2 + specifier: ^25.9.3 + version: 25.9.3 ai: - specifier: ^6.0.161 - version: 6.0.191(zod@4.4.3) + specifier: ^6.0.206 + version: 6.0.206(zod@4.4.3) ansi-styles: specifier: ^6.2.3 version: 6.2.3 @@ -42,17 +42,17 @@ importers: specifier: ^9.6.1 version: 9.6.1 happy-dom: - specifier: ^20.10.2 - version: 20.10.2 + specifier: ^20.10.3 + version: 20.10.3 marked: - specifier: ^18.0.4 - version: 18.0.4 + specifier: ^18.0.5 + version: 18.0.5 rolldown: specifier: ^1.1.1 version: 1.1.1 shiki: - specifier: ^4.0.2 - version: 4.1.0 + specifier: ^4.2.0 + version: 4.2.0 string-width: specifier: ^8.2.1 version: 8.2.1 @@ -60,7 +60,7 @@ importers: specifier: ^5.0.0 version: 5.0.0 typescript: - specifier: ^5 + specifier: ^5.9.3 version: 5.9.3 wrap-ansi: specifier: ^10.0.0 @@ -68,20 +68,20 @@ importers: packages: - '@ai-sdk/gateway@3.0.120': - resolution: {integrity: sha512-MYKAeD2q7/sa1ZdqtL2tw0Me0B8Tok6Q/fhkJDhJl39dG8u+VBlWO9yk9lcdm784bM418o1EKObo4aOxs6+18Q==} + '@ai-sdk/gateway@3.0.132': + resolution: {integrity: sha512-sFZFGk6aVSNKmgDrb14efaAbvM71AB3UUvaTsU+bvhWP9jrkRrwix/jFX8IoOAJk0/X7Xf7p3m0J2O2G6ljZbA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai-compatible@2.0.48': - resolution: {integrity: sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q==} + '@ai-sdk/openai-compatible@2.0.50': + resolution: {integrity: sha512-HyuxddF2Yv5G8qxK/0uksAINjQ4h6TpwOqHuqzsCM0u78/JWAW2OXcIplQeB44PIAORgPjbMzrw9DhnPYHMskA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.27': - resolution: {integrity: sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==} + '@ai-sdk/provider-utils@4.0.29': + resolution: {integrity: sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -147,12 +147,12 @@ packages: cpu: [x64] os: [win32] - '@clack/core@1.3.1': - resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==} + '@clack/core@1.4.1': + resolution: {integrity: sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==} engines: {node: '>= 20.12.0'} - '@clack/prompts@1.4.0': - resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} + '@clack/prompts@1.5.1': + resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} engines: {node: '>= 20.12.0'} '@emnapi/core@1.11.0': @@ -444,32 +444,32 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/core@4.1.0': - resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==} + '@shikijs/core@4.2.0': + resolution: {integrity: sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.1.0': - resolution: {integrity: sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==} + '@shikijs/engine-javascript@4.2.0': + resolution: {integrity: sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.1.0': - resolution: {integrity: sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==} + '@shikijs/engine-oniguruma@4.2.0': + resolution: {integrity: sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==} engines: {node: '>=20'} - '@shikijs/langs@4.1.0': - resolution: {integrity: sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==} + '@shikijs/langs@4.2.0': + resolution: {integrity: sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==} engines: {node: '>=20'} - '@shikijs/primitive@4.1.0': - resolution: {integrity: sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==} + '@shikijs/primitive@4.2.0': + resolution: {integrity: sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==} engines: {node: '>=20'} - '@shikijs/themes@4.1.0': - resolution: {integrity: sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==} + '@shikijs/themes@4.2.0': + resolution: {integrity: sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==} engines: {node: '>=20'} - '@shikijs/types@4.1.0': - resolution: {integrity: sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==} + '@shikijs/types@4.2.0': + resolution: {integrity: sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -491,11 +491,8 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} - - '@types/node@25.9.2': - resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + '@types/node@25.9.3': + resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -513,8 +510,8 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} - ai@6.0.191: - resolution: {integrity: sha512-zAxvjKebQE7YkSyyNIl0OM7i6/zygnKeF+yNUjD4nWOelYrG+LpDd6RnH6mjySI4zUpZ7o4wbnmAy8jc6u98vQ==} + ai@6.0.206: + resolution: {integrity: sha512-hVdB5PozMMxmkPBfbCxkFOn0eVeCMi/imkfPvk65cxL4O8oIrvjUxDUX8dGkdkWG1No+R7e7D9ti2DHO61Z4YQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -575,8 +572,8 @@ packages: engines: {node: '>=18'} hasBin: true - eventsource-parser@3.0.8: - resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} execa@9.6.1: @@ -604,8 +601,8 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - happy-dom@20.10.2: - resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==} + happy-dom@20.10.3: + resolution: {integrity: sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw==} engines: {node: '>=20.0.0'} has-flag@5.0.1: @@ -643,8 +640,8 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - marked@18.0.4: - resolution: {integrity: sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==} + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} engines: {node: '>= 20'} hasBin: true @@ -692,8 +689,8 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -717,8 +714,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@4.1.0: - resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==} + shiki@4.2.0: + resolution: {integrity: sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==} engines: {node: '>=20'} signal-exit@4.1.0: @@ -834,24 +831,24 @@ packages: snapshots: - '@ai-sdk/gateway@3.0.120(zod@4.4.3)': + '@ai-sdk/gateway@3.0.132(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) '@vercel/oidc': 3.2.0 zod: 4.4.3 - '@ai-sdk/openai-compatible@2.0.48(zod@4.4.3)': + '@ai-sdk/openai-compatible@2.0.50(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) zod: 4.4.3 - '@ai-sdk/provider-utils@4.0.27(zod@4.4.3)': + '@ai-sdk/provider-utils@4.0.29(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.10 '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.8 + eventsource-parser: 3.1.0 zod: 4.4.3 '@ai-sdk/provider@3.0.10': @@ -893,14 +890,14 @@ snapshots: '@biomejs/cli-win32-x64@2.4.12': optional: true - '@clack/core@1.3.1': + '@clack/core@1.4.1': dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clack/prompts@1.4.0': + '@clack/prompts@1.5.1': dependencies: - '@clack/core': 1.3.1 + '@clack/core': 1.4.1 fast-string-width: 3.0.2 fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 @@ -1071,40 +1068,40 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@shikijs/core@4.1.0': + '@shikijs/core@4.2.0': dependencies: - '@shikijs/primitive': 4.1.0 - '@shikijs/types': 4.1.0 + '@shikijs/primitive': 4.2.0 + '@shikijs/types': 4.2.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.1.0': + '@shikijs/engine-javascript@4.2.0': dependencies: - '@shikijs/types': 4.1.0 + '@shikijs/types': 4.2.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.1.0': + '@shikijs/engine-oniguruma@4.2.0': dependencies: - '@shikijs/types': 4.1.0 + '@shikijs/types': 4.2.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.1.0': + '@shikijs/langs@4.2.0': dependencies: - '@shikijs/types': 4.1.0 + '@shikijs/types': 4.2.0 - '@shikijs/primitive@4.1.0': + '@shikijs/primitive@4.2.0': dependencies: - '@shikijs/types': 4.1.0 + '@shikijs/types': 4.2.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/themes@4.1.0': + '@shikijs/themes@4.2.0': dependencies: - '@shikijs/types': 4.1.0 + '@shikijs/types': 4.2.0 - '@shikijs/types@4.1.0': + '@shikijs/types@4.2.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -1128,11 +1125,7 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@25.9.1': - dependencies: - undici-types: 7.24.6 - - '@types/node@25.9.2': + '@types/node@25.9.3': dependencies: undici-types: 7.24.6 @@ -1142,17 +1135,17 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.3 '@ungap/structured-clone@1.3.1': {} '@vercel/oidc@3.2.0': {} - ai@6.0.191(zod@4.4.3): + ai@6.0.206(zod@4.4.3): dependencies: - '@ai-sdk/gateway': 3.0.120(zod@4.4.3) + '@ai-sdk/gateway': 3.0.132(zod@4.4.3) '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.27(zod@4.4.3) + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) '@opentelemetry/api': 1.9.1 zod: 4.4.3 @@ -1166,7 +1159,7 @@ snapshots: buffer-image-size@0.6.4: dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.3 ccount@2.0.1: {} @@ -1223,7 +1216,7 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 - eventsource-parser@3.0.8: {} + eventsource-parser@3.1.0: {} execa@9.6.1: dependencies: @@ -1261,9 +1254,9 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - happy-dom@20.10.2: + happy-dom@20.10.3: dependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.3 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 @@ -1285,7 +1278,7 @@ snapshots: hast-util-whitespace: 3.0.0 html-void-elements: 3.0.0 mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 + property-information: 7.2.0 space-separated-tokens: 2.0.2 stringify-entities: 4.0.4 zwitch: 2.0.4 @@ -1308,7 +1301,7 @@ snapshots: json-schema@0.4.0: {} - marked@18.0.4: {} + marked@18.0.5: {} mdast-util-to-hast@13.2.1: dependencies: @@ -1362,7 +1355,7 @@ snapshots: dependencies: parse-ms: 4.0.0 - property-information@7.1.0: {} + property-information@7.2.0: {} regex-recursion@6.0.2: dependencies: @@ -1401,14 +1394,14 @@ snapshots: shebang-regex@3.0.0: {} - shiki@4.1.0: + shiki@4.2.0: dependencies: - '@shikijs/core': 4.1.0 - '@shikijs/engine-javascript': 4.1.0 - '@shikijs/engine-oniguruma': 4.1.0 - '@shikijs/langs': 4.1.0 - '@shikijs/themes': 4.1.0 - '@shikijs/types': 4.1.0 + '@shikijs/core': 4.2.0 + '@shikijs/engine-javascript': 4.2.0 + '@shikijs/engine-oniguruma': 4.2.0 + '@shikijs/langs': 4.2.0 + '@shikijs/themes': 4.2.0 + '@shikijs/types': 4.2.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5ed0b5a..8ad854b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ +minimumReleaseAge: 4320 allowBuilds: esbuild: true From d157978d52b8bfd6cea18edfb46ee322f028419b Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 18 Jun 2026 18:24:02 -0400 Subject: [PATCH 66/76] docs: update AGENTS.md to reflect Node.js/pnpm migration --- AGENTS.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b0bb2e1..4f1015f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,26 +1,36 @@ # lmc -Bun CLI tool for streaming LLM chat with inline markdown rendering. +TypeScript CLI tool for streaming LLM chat with inline markdown rendering. ## Commands ``` -bun run dev # dev run -bun run prod # run bundled output -bun run check # biome lint + format + organize imports +pnpm run dev # dev run (rolldown bundle + node) +pnpm run check # biome lint + format + organize imports + typecheck +pnpm run build # check + bundle index.ts and tts.ts (minified ESM) +pnpm run build:bin # check + bundle with esbuild (CJS) + build SEA binaries +pnpm run clean # remove dist/ ``` ## Architecture -- `index.ts` — CLI entry point, orchestration +- `src/index.ts` — LMC CLI entry point, orchestration +- `src/tts.ts` — TTSC CLI entry point (text-to-speech, Kokoro pipeline via `uv run`) +- `src/config.ts` — `initConfig()`, `loadConfig()`: manages `~/.lmc_config.json` +- `src/conversation.ts` — `Conversation` class: load/save to `~/.lmc_convo.json` +- `src/errors.ts` — `handleTopLevel()`: TTY-aware error wrapping +- `src/prompts.ts` — `commitDiff()`, `reviewFile()`, `reviewDiff()`, `file()`: prompt builders - `src/render/index.ts` — `Renderer` class: streams LLM response, buffers markdown, renders to stdout/stderr -- `src/render/markdown.ts` — `renderMarkdown()`: syntax-highlighted markdown via `@comark/ansi` + Shiki +- `src/render/markdown.ts` — `renderMarkdown()`: markdown via `marked` + Shiki + `@cliffy/table` +- `src/render/codeToANSI.ts` — Shiki token output to ANSI escape sequences ## Gotchas -- **Bun only** — not Node. Use `Bun.file()`, `Bun.argv`, `Bun.sleep()`. +- **Node.js** — Use modern Node APIs (`node:fs/promises`, `node:stream/consumers`, etc.). - **No tests** — none exist. -- **Biome config**: double quotes, space indent, auto-import-organize on. Run `bun run check` before committing. +- **Biome config**: double quotes, space indent, auto-import-organize on. `pnpm run check` also runs `tsc --noEmit`. +- **Build tools**: rolldown for ESM bundles, esbuild for CJS binaries, `node --build-sea` for SEA single-executable apps. +- **TTS**: requires Python with `uv` — Kokoro pipeline lives in `tts/`. ## Style From bf5e223abf37c008a39fc6091eeeb42966133197 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 19 Jun 2026 14:28:16 -0400 Subject: [PATCH 67/76] refactor: rename flag variables for improved clarity and intent --- src/index.ts | 38 +++++++++++++++++++------------------- src/tts.ts | 18 +++++++++--------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/index.ts b/src/index.ts index e3044a7..f5c3ad8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,26 +40,26 @@ async function main() { const newConvo = args.new ?? args.n; const plainOut = args["plain-out"] ?? args.plain ?? !process.stdout.isTTY; const plainErr = args["plain-err"] ?? args.plain ?? !process.stderr.isTTY; - const modelFlag = args.model ?? args.m; - const versionFlag = args.version ?? args.v; - const configFlag = args.config ?? args.c; - const messageFlag = args["git-message"] ?? args.g; - const reviewFlag = args.review ?? args.r; - const fileFlag = args.file ?? args.f; + const modelOverride = args.model ?? args.m; + const showVersion = args.version ?? args.v; + const editConfig = args.config ?? args.c; + const generateGitMessage = args["git-message"] ?? args.g; + const generateReview = args.review ?? args.r; + const inputFile = args.file ?? args.f; - if (versionFlag) { + if (showVersion) { console.log("0.0.0"); return; } - const config = await loadConfig(configFlag); + const config = await loadConfig(editConfig); - if (configFlag || !config) { + if (editConfig || !config) { await initConfig(config); return; } - const model = modelFlag ?? config.model; + const model = modelOverride ?? config.model; const renderer = new Renderer(plainOut, plainErr); @@ -74,22 +74,22 @@ async function main() { if (!newConvo) { await convo.load(); - if (modelFlag) convo.model = modelFlag; + if (modelOverride) convo.model = modelOverride; } let input = positionals.join(" ").trim(); if (input === "-") input = await consumers.text(process.stdin); let prompt = input; - if (messageFlag) { + if (generateGitMessage) { prompt = commitDiff(input); - } else if (reviewFlag && fileFlag) { - const content = await fs.readFile(fileFlag, "utf8"); - prompt = reviewFile(file(fileFlag, content)); - } else if (fileFlag) { - const content = await fs.readFile(fileFlag, "utf8"); - prompt = `${file(fileFlag, content)}\n\n${input}`; - } else if (reviewFlag) { + } else if (generateReview && inputFile) { + const content = await fs.readFile(inputFile, "utf8"); + prompt = reviewFile(file(inputFile, content)); + } else if (inputFile) { + const content = await fs.readFile(inputFile, "utf8"); + prompt = `${file(inputFile, content)}\n\n${input}`; + } else if (generateReview) { prompt = reviewDiff(input); } diff --git a/src/tts.ts b/src/tts.ts index 56981d7..75f37b1 100644 --- a/src/tts.ts +++ b/src/tts.ts @@ -30,19 +30,19 @@ async function main() { allowPositionals: true, }); - const wait = args.wait ?? args.w; - const url = args.url ?? args.u; - const edit = args.edit ?? args.e; + const waitAndCombine = args.wait ?? args.w; + const readURL = args.url ?? args.u; + const editContent = args.edit ?? args.e; let input = positionals.join(" ").trim(); - if (!input) input = await consumers.text(process.stdin); + if (!input) input = (await consumers.text(process.stdin)).trim(); if (!input) { throw new Error("No input was given. Use positional arguments or stdin."); } - if (url) { + if (readURL) { const result = await fetch(input); const window = new Window({ url: input }); const doc = window.document; @@ -60,7 +60,7 @@ async function main() { const home = os.homedir(); const cwd = path.join(home, ".ttsc"); - if (edit) { + if (editContent) { const file = path.join(cwd, "input.txt"); await fs.writeFile(file, input, "utf8"); const editor = process.env.EDITOR || "nano"; @@ -90,7 +90,7 @@ async function main() { stderr: "inherit", })`uv run -- python -u main.py`; - if (!wait) { + if (!waitAndCombine) { koko.catch(() => {}); koko.then(() => { const stop = Date.now(); @@ -124,7 +124,7 @@ async function main() { const file = path.join(cwd, `${info.file}.wav`); - if (wait) { + if (waitAndCombine) { files.push(file); process.stdout.write("."); } else { @@ -141,7 +141,7 @@ async function main() { } await koko; - if (!wait) return; + if (!waitAndCombine) return; const stop = Date.now(); const duration = ((stop - start) / 1000).toFixed(2); From 5b138df664a3ab7cb63480e8b1c8988e820938b1 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 19 Jun 2026 14:33:56 -0400 Subject: [PATCH 68/76] refactor(errors): extract isErrorLike and use ExecaError class in tts --- src/errors.ts | 17 +++++++++++------ src/tts.ts | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index a8e9e05..2eac281 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,13 +1,18 @@ import { chalkStderr } from "chalk"; import wrapAnsi from "wrap-ansi"; +export interface ErrorLike { + message: string; +} + +export const isErrorLike = (err: unknown): err is ErrorLike => + typeof err === "object" && + err !== null && + "message" in err && + typeof err.message === "string"; + export function handleTopLevel(err: unknown) { - if ( - typeof err === "object" && - err !== null && - "message" in err && - typeof err.message === "string" - ) { + if (isErrorLike(err)) { if (process.stderr.isTTY) { const [columns] = process.stderr.getWindowSize(); console.error(wrapAnsi(chalkStderr.red(err.message), columns)); diff --git a/src/tts.ts b/src/tts.ts index 75f37b1..4e8c36c 100644 --- a/src/tts.ts +++ b/src/tts.ts @@ -8,7 +8,7 @@ import consumers from "node:stream/consumers"; import util from "node:util"; import { Readability } from "@mozilla/readability"; import chalk from "chalk"; -import { execa } from "execa"; +import { ExecaError, execa } from "execa"; import { Window } from "happy-dom"; import wrapAnsi from "wrap-ansi"; import { handleTopLevel } from "./errors"; @@ -151,7 +151,7 @@ async function main() { try { await execa({ cwd, shell: true })`sox ${files} output.wav`; } catch (err) { - if (err instanceof Object && "code" in err && err.code === "ENOENT") { + if (err instanceof ExecaError && err.code === "ENOENT") { console.error("Sox not found. Unable to combine outputs."); console.log(chalk.yellow(`Audio available in ${cwd}`)); return; From e1033398e977a4dfbd07d5ef925004f281ecb285 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 19 Jun 2026 14:54:03 -0400 Subject: [PATCH 69/76] fix(markdown): correct list item whitespace and indentation --- package.json | 2 +- src/render/index.ts | 2 +- src/render/markdown.ts | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 690ac1a..b27a415 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "ttsc": "dist/tts.js" }, "scripts": { - "dev": "pnpm run bundle src/index.ts &>/dev/null && node dist/index.js", + "dev": "pnpm run bundle src/index.ts >/dev/null && node dist/index.js", "check": "biome check --write && pnpm run tc", "clean": "rm -rf dist", "build": "pnpm run check && pnpm run bundle --minify src/index.ts && pnpm run bundle --minify src/tts.ts", diff --git a/src/render/index.ts b/src/render/index.ts index 01fce63..a75eee2 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -51,7 +51,7 @@ export default class Renderer { this.charsSinceNL = this.charsSinceNL - columns; } - if (this.lineCount >= Math.max(rows - 10, 1)) { + if (this.lineCount >= Math.max(rows - 4, 1)) { this.moveClearReset(-this.lineCount, pipe); } } diff --git a/src/render/markdown.ts b/src/render/markdown.ts index 4450ce0..ca7c59d 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -76,11 +76,12 @@ class LmcRenderer extends Renderer { this.listitem(item, ordered ? num + index : undefined), ) .join("\n"); + return `\n${renderedItems}\n`; } override listitem({ tokens }: Tokens.ListItem, num?: number) { - const text = this.parser.parse(tokens).trim(); + const text = this.parser.parse(tokens); let marker = ""; if (num !== undefined) { marker = `${num}.`; @@ -91,7 +92,8 @@ class LmcRenderer extends Renderer { const item = `${marker} ${text}`; return item .split("\n") - .map((line) => (line.length > 0 ? ` ${line}` : "")) + .filter((line) => line.length > 0) + .map((line) => ` ${line}`) .join("\n"); } From 49c4138a1c37d36edd669a2c775c6f41922911be Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 19 Jun 2026 17:35:05 -0400 Subject: [PATCH 70/76] feat(tts): add --pause flag to control mpv playback --- src/tts.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tts.ts b/src/tts.ts index 4e8c36c..e6aab05 100644 --- a/src/tts.ts +++ b/src/tts.ts @@ -21,6 +21,8 @@ async function main() { options: { wait: { type: "boolean" }, w: { type: "boolean" }, + pause: { type: "boolean" }, + p: { type: "boolean" }, url: { type: "boolean" }, u: { type: "boolean" }, edit: { type: "boolean" }, @@ -31,6 +33,7 @@ async function main() { }); const waitAndCombine = args.wait ?? args.w; + const pausePlayer = args.pause ?? args.p; const readURL = args.url ?? args.u; const editContent = args.edit ?? args.e; @@ -136,7 +139,7 @@ async function main() { stdin: "ignore", stdout: "inherit", stderr: "inherit", - })`mpv ${file}`; + })`mpv${pausePlayer ? " --pause" : ""} ${file}`; } } @@ -171,7 +174,7 @@ async function main() { stdin: "ignore", stdout: "inherit", stderr: "inherit", - })`mpv --pause output.wav`; + })`mpv${pausePlayer ? " --pause" : ""} output.wav`; } main().catch(handleTopLevel); From da317b7177e75cad4a54ef06786e8ef181f877b6 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 24 Jun 2026 11:54:09 -0400 Subject: [PATCH 71/76] fix(markdown): trim whitespace from parsed list item content --- src/render/markdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/render/markdown.ts b/src/render/markdown.ts index ca7c59d..7190de8 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -81,7 +81,7 @@ class LmcRenderer extends Renderer { } override listitem({ tokens }: Tokens.ListItem, num?: number) { - const text = this.parser.parse(tokens); + const text = this.parser.parse(tokens).trim(); let marker = ""; if (num !== undefined) { marker = `${num}.`; From b1730bfaf47b2d6b9c3f0ecefd42a7bcc542390d Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 26 Jun 2026 16:55:57 -0400 Subject: [PATCH 72/76] refactor(lmc): modularize prompt generation and add XML-like fences --- src/index.ts | 32 ++++++++++++++++++++------------ src/prompts.ts | 39 ++++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/index.ts b/src/index.ts index f5c3ad8..be9e0f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,14 @@ import { streamText } from "ai"; import { initConfig, loadConfig } from "./config"; import { Conversation } from "./conversation"; import { handleTopLevel } from "./errors"; -import { commitDiff, file, reviewDiff, reviewFile } from "./prompts"; +import { + base, + commitDiff, + contextFence, + diffFence, + fileFence, + review, +} from "./prompts"; import Renderer from "./render"; async function main() { @@ -32,6 +39,8 @@ async function main() { r: { type: "boolean" }, file: { type: "string" }, f: { type: "string" }, + stdin: { type: "boolean" }, + i: { type: "boolean" }, }, strict: true, allowPositionals: true, @@ -46,6 +55,7 @@ async function main() { const generateGitMessage = args["git-message"] ?? args.g; const generateReview = args.review ?? args.r; const inputFile = args.file ?? args.f; + const stdinContent = args.stdin ?? args.i; if (showVersion) { console.log("0.0.0"); @@ -77,20 +87,18 @@ async function main() { if (modelOverride) convo.model = modelOverride; } - let input = positionals.join(" ").trim(); - if (input === "-") input = await consumers.text(process.stdin); + const input = positionals.join(" ").trim(); + let stdinText = ""; + if (stdinContent) stdinText = await consumers.text(process.stdin); + let fileText = ""; + if (inputFile) + fileText = fileFence(inputFile, await fs.readFile(inputFile, "utf8")); - let prompt = input; + let prompt = base(input, [fileText, contextFence(stdinText)]); if (generateGitMessage) { - prompt = commitDiff(input); - } else if (generateReview && inputFile) { - const content = await fs.readFile(inputFile, "utf8"); - prompt = reviewFile(file(inputFile, content)); - } else if (inputFile) { - const content = await fs.readFile(inputFile, "utf8"); - prompt = `${file(inputFile, content)}\n\n${input}`; + prompt = commitDiff(stdinText, [fileText]); } else if (generateReview) { - prompt = reviewDiff(input); + prompt = review([fileText, stdinText ? diffFence(stdinText) : "", input]); } if (!prompt) throw new Error("No prompt given."); diff --git a/src/prompts.ts b/src/prompts.ts index 21f1a27..31fae99 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -1,26 +1,31 @@ -export const file = (name: string, input: string) => `${name}: -\`\`\` +const blocks = (input?: string[]) => + input?.map((block) => (block ? `${block}\n` : "")).join("") ?? ""; + +export const base = (input: string, extra?: string[]) => + `${blocks(extra)}${input}`; + +export const fileFence = (name: string, input: string) => ` ${input} -\`\`\``; + +`; -export const commitDiff = (input: string) => `\`\`\`diff +export const diffFence = (input: string) => ` ${input} -\`\`\` +`; -Generate a conventional commit message for this diff. If you are unable to generate a reasonable option, ask for more context.`; +export const contextFence = (input: string) => ` +${input} +`; -export const reviewFile = (input: string) => `${input} +export const commitDiff = ( + input: string, + extra?: string[], +) => `${blocks(extra)}${diffFence(input)} +Generate a conventional commit message for the included context. If you are unable to generate a reasonable option, ask for more context.`; + +export const review = (input: string[]) => `${blocks(input)} +Review the included context for any potential bugs, best practices, performance issues, or security flaws. Today's date is ${new Date().toISOString()}, your version info is likely out-of-date. -Review the included file for any potential bugs, best practices, performance issues, or security flaws. -Let me know if you need any more context to complete your review.`; - -export const reviewDiff = (input: string) => `\`\`\`diff -${input} -\`\`\` - -Today's date is ${new Date().toISOString()}, your version info is likely out-of-date. - -Review the included diff for any potential bugs, best practices, performance issues, or security flaws. Let me know if you need any more context to complete your review.`; From 5b6c65d30f9c9403050fdbce86a9e7d16948f3e2 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 26 Jun 2026 17:06:12 -0400 Subject: [PATCH 73/76] refactor(lmc): use short property for options and simplify flags --- src/index.ts | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index be9e0f1..59e6635 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,40 +22,32 @@ async function main() { const { values: args, positionals } = util.parseArgs({ args: process.argv.slice(2), options: { - model: { type: "string" }, - m: { type: "string" }, - new: { type: "boolean" }, - n: { type: "boolean" }, - config: { type: "boolean" }, - c: { type: "boolean" }, + model: { type: "string", short: "m" }, + new: { type: "boolean", short: "n" }, + config: { type: "boolean", short: "c" }, plain: { type: "boolean" }, "plain-out": { type: "boolean" }, "plain-err": { type: "boolean" }, - version: { type: "boolean" }, - v: { type: "boolean" }, - "git-message": { type: "boolean" }, - g: { type: "boolean" }, - review: { type: "boolean" }, - r: { type: "boolean" }, - file: { type: "string" }, - f: { type: "string" }, - stdin: { type: "boolean" }, - i: { type: "boolean" }, + version: { type: "boolean", short: "v" }, + "git-message": { type: "boolean", short: "g" }, + review: { type: "boolean", short: "r" }, + file: { type: "string", short: "f" }, + stdin: { type: "boolean", short: "i" }, }, strict: true, allowPositionals: true, }); - const newConvo = args.new ?? args.n; + const newConvo = args.new; const plainOut = args["plain-out"] ?? args.plain ?? !process.stdout.isTTY; const plainErr = args["plain-err"] ?? args.plain ?? !process.stderr.isTTY; - const modelOverride = args.model ?? args.m; - const showVersion = args.version ?? args.v; - const editConfig = args.config ?? args.c; - const generateGitMessage = args["git-message"] ?? args.g; - const generateReview = args.review ?? args.r; - const inputFile = args.file ?? args.f; - const stdinContent = args.stdin ?? args.i; + const modelOverride = args.model; + const showVersion = args.version; + const editConfig = args.config; + const generateGitMessage = args["git-message"]; + const generateReview = args.review; + const inputFile = args.file; + const stdinContent = args.stdin; if (showVersion) { console.log("0.0.0"); From 4aaa80898d0e4c5c08f15ef68b0d9153b4e973fa Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 26 Jun 2026 17:46:21 -0400 Subject: [PATCH 74/76] feat(lmc): support multiple files via -f flag and fix empty context --- src/index.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 59e6635..ad63ce8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,7 +31,7 @@ async function main() { version: { type: "boolean", short: "v" }, "git-message": { type: "boolean", short: "g" }, review: { type: "boolean", short: "r" }, - file: { type: "string", short: "f" }, + file: { type: "string", short: "f", multiple: true }, stdin: { type: "boolean", short: "i" }, }, strict: true, @@ -46,7 +46,7 @@ async function main() { const editConfig = args.config; const generateGitMessage = args["git-message"]; const generateReview = args.review; - const inputFile = args.file; + const inputFiles = args.file; const stdinContent = args.stdin; if (showVersion) { @@ -82,15 +82,18 @@ async function main() { const input = positionals.join(" ").trim(); let stdinText = ""; if (stdinContent) stdinText = await consumers.text(process.stdin); - let fileText = ""; - if (inputFile) - fileText = fileFence(inputFile, await fs.readFile(inputFile, "utf8")); + const files = []; + for (const file of inputFiles ?? []) + files.push(fileFence(file, await fs.readFile(file, "utf8"))); - let prompt = base(input, [fileText, contextFence(stdinText)]); + let prompt = base(input, [ + ...files, + stdinText ? contextFence(stdinText) : "", + ]); if (generateGitMessage) { - prompt = commitDiff(stdinText, [fileText]); + prompt = commitDiff(stdinText, [...files]); } else if (generateReview) { - prompt = review([fileText, stdinText ? diffFence(stdinText) : "", input]); + prompt = review([...files, stdinText ? diffFence(stdinText) : "", input]); } if (!prompt) throw new Error("No prompt given."); From a8cbfb3665e9f056dac5463738cd8eeb99455b14 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sat, 27 Jun 2026 03:31:03 -0400 Subject: [PATCH 75/76] refactor(lmc): remove intermediate vars and directly access CLI args --- src/index.ts | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index ad63ce8..dba66eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,30 +38,22 @@ async function main() { allowPositionals: true, }); - const newConvo = args.new; const plainOut = args["plain-out"] ?? args.plain ?? !process.stdout.isTTY; const plainErr = args["plain-err"] ?? args.plain ?? !process.stderr.isTTY; - const modelOverride = args.model; - const showVersion = args.version; - const editConfig = args.config; - const generateGitMessage = args["git-message"]; - const generateReview = args.review; - const inputFiles = args.file; - const stdinContent = args.stdin; - if (showVersion) { + if (args.version) { console.log("0.0.0"); return; } - const config = await loadConfig(editConfig); + const config = await loadConfig(args.config); - if (editConfig || !config) { + if (args.config || !config) { await initConfig(config); return; } - const model = modelOverride ?? config.model; + const model = args.model ?? config.model; const renderer = new Renderer(plainOut, plainErr); @@ -74,25 +66,25 @@ async function main() { const convo = new Conversation(model); - if (!newConvo) { + if (!args.new) { await convo.load(); - if (modelOverride) convo.model = modelOverride; + if (args.model) convo.model = args.model; } const input = positionals.join(" ").trim(); let stdinText = ""; - if (stdinContent) stdinText = await consumers.text(process.stdin); + if (args.stdin) stdinText = await consumers.text(process.stdin); const files = []; - for (const file of inputFiles ?? []) + for (const file of args.file ?? []) files.push(fileFence(file, await fs.readFile(file, "utf8"))); let prompt = base(input, [ ...files, stdinText ? contextFence(stdinText) : "", ]); - if (generateGitMessage) { + if (args["git-message"]) { prompt = commitDiff(stdinText, [...files]); - } else if (generateReview) { + } else if (args.review) { prompt = review([...files, stdinText ? diffFence(stdinText) : "", input]); } From 159547e79958ba11a604418e44de9fa7ef0861a7 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 13 Jul 2026 19:30:20 -0400 Subject: [PATCH 76/76] fix(lmc): handle interrupt signal gracefully --- src/index.ts | 5 ++--- src/render/index.ts | 8 +++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index dba66eb..56c93ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,9 +38,6 @@ async function main() { allowPositionals: true, }); - const plainOut = args["plain-out"] ?? args.plain ?? !process.stdout.isTTY; - const plainErr = args["plain-err"] ?? args.plain ?? !process.stderr.isTTY; - if (args.version) { console.log("0.0.0"); return; @@ -55,6 +52,8 @@ async function main() { const model = args.model ?? config.model; + const plainOut = args["plain-out"] ?? args.plain ?? !process.stdout.isTTY; + const plainErr = args["plain-err"] ?? args.plain ?? !process.stderr.isTTY; const renderer = new Renderer(plainOut, plainErr); const provider = createOpenAICompatible({ diff --git a/src/render/index.ts b/src/render/index.ts index a75eee2..2819467 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -84,7 +84,13 @@ export default class Renderer { async renderStream(stream: AsyncIterableStream>) { this.resetCounts(); - const spin = spinner({ withGuide: false, indicator: "timer" }); + const spin = spinner({ + withGuide: false, + indicator: "timer", + onCancel: () => { + process.exit(130); + }, + }); if (!this.isPlain) { spin.start("Waiting for response"); }