Compare commits

..

3 commits

26 changed files with 751 additions and 4564 deletions

View file

@ -1,36 +1,26 @@
# lmc # aicl
TypeScript CLI tool for streaming LLM chat with inline markdown rendering. Bun CLI tool for streaming LLM chat with inline markdown rendering.
## Commands ## Commands
``` ```
pnpm run dev # dev run (rolldown bundle + node) bun run dev # dev run
pnpm run check # biome lint + format + organize imports + typecheck bun run prod # run bundled output
pnpm run build # check + bundle index.ts and tts.ts (minified ESM) bun run check # biome lint + format + organize imports
pnpm run build:bin # check + bundle with esbuild (CJS) + build SEA binaries
pnpm run clean # remove dist/
``` ```
## Architecture ## Architecture
- `src/index.ts` — LMC CLI entry point, orchestration - `index.ts` — 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/index.ts``Renderer` class: streams LLM response, buffers markdown, renders to stdout/stderr
- `src/render/markdown.ts``renderMarkdown()`: markdown via `marked` + Shiki + `@cliffy/table` - `src/render/markdown.ts``renderMarkdown()`: syntax-highlighted markdown via `@comark/ansi` + Shiki
- `src/render/codeToANSI.ts` — Shiki token output to ANSI escape sequences
## Gotchas ## Gotchas
- **Node.js** — Use modern Node APIs (`node:fs/promises`, `node:stream/consumers`, etc.). - **Bun only** — not Node. Use `Bun.file()`, `Bun.argv`, `Bun.sleep()`.
- **No tests** — none exist. - **No tests** — none exist.
- **Biome config**: double quotes, space indent, auto-import-organize on. `pnpm run check` also runs `tsc --noEmit`. - **Biome config**: double quotes, space indent, auto-import-organize on. Run `bun run check` before committing.
- **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 ## Style

127
README.md
View file

@ -1,130 +1,15 @@
# lmc # aicl
A CLI tool for LLM chats with terminal native streaming. To install dependencies:
## 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 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
## Requirements
- [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 ```bash
# Bundle and install locally bun install
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
``` ```
_Package to come later_ To run:
## Usage
### Chat (lmc)
```bash ```bash
lmc [options] <prompt...> bun run index.ts
``` ```
| Flag | Short | Type | Description | This project was created using `bun init` in bun v1.3.11. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
| ------------ | ----- | ------- | ----------------------------------------------------- |
| `--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 on-the-fly
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) |
| `--url` | `-u` | boolean | Fetch text from a URL instead of stdin |
| `--edit` | `-e` | boolean | Edit the input with your $EDITOR before processing |
**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/)
Wants:
- `mpv` for playback
- `sox` for wav merging with `--wait`
## Development
```bash
pnpm run dev # dev run
pnpm 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 Marked + 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 |
| `@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 |

355
bun.lock Normal file
View file

@ -0,0 +1,355 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "aicl",
"dependencies": {
"@ai-sdk/openai-compatible": "^2.0.41",
"ai": "^6.0.161",
"ansi-styles": "^6.2.3",
"chalk": "^5.6.2",
"cli-table3": "^0.6.5",
"kokoro-js": "^1.2.1",
"shiki": "^4.0.2",
"terminal-link": "^5.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=="],
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@huggingface/jinja": ["@huggingface/jinja@0.5.7", "", {}, "sha512-OosMEbF/R6zkKNNzqhI7kvKYCpo1F0UeIv46/h4D4UjVEKKd6k3TiV8sgu6fkreX4lbBiRI+lZG8UnXnqVQmEQ=="],
"@huggingface/transformers": ["@huggingface/transformers@3.8.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.3", "onnxruntime-node": "1.21.0", "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", "sharp": "^0.34.1" } }, "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@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@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
"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=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"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=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
"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=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="],
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="],
"has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"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=="],
"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=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
"kokoro-js": ["kokoro-js@1.2.1", "", { "dependencies": { "@huggingface/transformers": "^3.5.1", "phonemizer": "^1.2.1" } }, "sha512-oq0HZJWis3t8lERkMJh84WLU86dpYD0EuBPtqYnLlQzyFP1OkyBRDcweAqCfhNOpltyN9j/azp1H6uuC47gShw=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
"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=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"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=="],
"onnxruntime-common": ["onnxruntime-common@1.21.0", "", {}, "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="],
"onnxruntime-node": ["onnxruntime-node@1.21.0", "", { "dependencies": { "global-agent": "^3.0.0", "onnxruntime-common": "1.21.0", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw=="],
"onnxruntime-web": ["onnxruntime-web@1.22.0-dev.20250409-89f8206ba4", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ=="],
"phonemizer": ["phonemizer@1.2.1", "", {}, "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A=="],
"platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="],
"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=="],
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"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=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"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=="],
"tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="],
"terminal-link": ["terminal-link@5.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^4.1.0" } }, "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"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=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.22.0-dev.20250409-89f8206ba4", "", {}, "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="],
}
}

93
index.ts Normal file
View file

@ -0,0 +1,93 @@
import { parseArgs } from "node:util";
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 Renderer from "./src/render";
const { values: args, positionals } = parseArgs({
args: Bun.argv,
options: {
model: { type: "string" },
m: { type: "string" },
new: { type: "boolean" },
n: { type: "boolean" },
save: { type: "boolean" },
s: { type: "boolean" },
plain: { type: "boolean" },
plainOut: { type: "boolean" },
plainErr: { type: "boolean" },
},
strict: true,
allowPositionals: true,
});
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 modelFlag = args.model ?? args.m;
const saveConfigFlag = args.save ?? args.s;
const config = await loadConfig();
config.model = modelFlag ?? config.model;
if (!config.model) {
console.error(
"Please run 'aicl --model <model> --save' to set a default model.",
);
process.exit(1);
}
if (saveConfigFlag) {
await saveConfig(config);
return;
}
const renderer = new Renderer(plainOut, plainErr);
const provider = createOpenAICompatible({
name: "llama.cpp",
apiKey: "local",
baseURL: "http://127.0.0.1:8080/v1",
includeUsage: true, // Include usage information in streaming responses
});
let convo: Conversation = {
model: config.model,
messages: [],
};
if (!newConvo) {
convo = await loadConversation(config.model);
if (args.model || args.m) {
convo.model = modelFlag ?? config.model;
}
}
const prompt = positionals.slice(2).join(" ") || "Introduce yourself.";
convo.messages.push({ role: "user", content: prompt });
const { fullStream, text, totalUsage } = streamText({
model: provider(convo.model),
messages: convo.messages,
});
const start = Date.now();
await renderer.renderStream(fullStream);
const stop = Date.now();
convo.messages.push({ role: "assistant", content: await text });
await saveConversation(convo);
await renderer.renderStats(totalUsage, start, stop);
}
await main();

66
kokoro.ts Normal file
View file

@ -0,0 +1,66 @@
/* Step before building:
* 1. Copy onnxruntime-node/bin to out/
* 2. Modify onnxruntime-node/dist/binding.js for bun
* 3. Profit */
import os from "node:os";
import { $, which } from "bun";
import chalk from "chalk";
import { KokoroTTS, TextSplitterStream } from "kokoro-js";
const model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
const tts = await KokoroTTS.from_pretrained(model_id, {
dtype: "fp32", // fp32 and q8 work best; fp16 results in silent segments
device: "cpu",
});
const stream = new TextSplitterStream();
const input = await $`wl-paste`.text();
if (!input) {
console.error("No input");
process.exit(1);
}
stream.push(input);
console.log(chalk.blue("Welcome to aitts!\n"));
console.log(input);
await $`rm -r ${os.homedir()}/.aitts/`.nothrow();
await $`mkdir ${os.homedir()}/.aitts/`.nothrow();
let fileId = 0;
const files: string[] = [];
const processText = async (text: string) => {
const audio = await tts.generate(text, {
voice: "af_bella",
speed: 1.2,
});
const file = `${os.homedir()}/.aitts/${fileId}.wav`;
audio.save(file);
files.push(file);
fileId++;
};
const play = async () => {
for (const file of files) {
console.log(chalk.green(`Playing ${file}`));
await Bun.spawn({
cmd: ["mpv", file],
stdout: "inherit",
}).exited;
process.stdout.moveCursor(-9999, -5);
process.stdout.clearScreenDown();
}
};
let playing: Promise<void> | undefined;
for (const text of stream) {
await processText(text);
if (!playing && which("mpv")) {
playing = play();
}
}
await playing;

View file

@ -1,41 +1,39 @@
{ {
"name": "lmc", "name": "aicl",
"module": "index.ts",
"type": "module", "type": "module",
"private": true, "private": true,
"bin": { "bin": {
"lmc": "dist/index.js", "aicl": "out/index.js",
"ttsc": "dist/tts.js" "aitts": "out/kokoro.js"
}, },
"scripts": { "scripts": {
"dev": "pnpm run bundle src/index.ts >/dev/null && node dist/index.js", "dev": "bun run index.ts",
"check": "biome check --write && pnpm run tc", "prod": "bun run out/index.js",
"clean": "rm -rf dist", "check": "bunx --bun @biomejs/biome check --write && bun run tc",
"build": "pnpm run check && pnpm run bundle --minify src/index.ts && pnpm run bundle --minify src/tts.ts", "build:js": "bun run check && bun run --parallel build:bundle build:kokoro && bun run --parallel shebang shebang:tts",
"build:bin": "pnpm run check && pnpm run bundle:bin --minify src/index.ts && pnpm run bundle:bin --minify src/tts.ts && pnpm run bin", "build:bundle": "bun build --target=bun --production --sourcemap --outdir=out index.ts",
"bundle": "rolldown --dir=dist --platform=node --transform.target=node22 --format=esm", "build:kokoro": "bun build --target=bun --production --sourcemap --outdir=out kokoro.ts",
"bundle:bin": "esbuild --bundle --outdir=dist --platform=node --target=node26 --format=cjs", "build:bin": "bun build --compile --outfile=out/aicl index.ts",
"bin": "node --build-sea sea-config.json && node --build-sea sea-config-tts.json", "shebang": "sed -i '1i#!\\/usr\\/bin\\/env bun' out/index.js",
"shebang:tts": "sed -i '1i#!\\/usr\\/bin\\/env bun' out/kokoro.js",
"tc": "tsc --noEmit --incremental" "tc": "tsc --noEmit --incremental"
}, },
"devDependencies": { "devDependencies": {
"@ai-sdk/openai-compatible": "^2.0.50",
"@biomejs/biome": "2.4.12", "@biomejs/biome": "2.4.12",
"@clack/prompts": "^1.5.1", "@types/bun": "latest"
"@cliffy/table": "jsr:^1.2.1", },
"@mozilla/readability": "^0.6.0", "peerDependencies": {
"@types/node": "^25.9.3", "typescript": "^5"
"ai": "^6.0.206", },
"dependencies": {
"@ai-sdk/openai-compatible": "^2.0.41",
"ai": "^6.0.161",
"ansi-styles": "^6.2.3", "ansi-styles": "^6.2.3",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"esbuild": "^0.28.1", "cli-table3": "^0.6.5",
"execa": "^9.6.1", "kokoro-js": "^1.2.1",
"happy-dom": "^20.10.3", "shiki": "^4.0.2",
"marked": "^18.0.5", "terminal-link": "^5.0.0"
"rolldown": "^1.1.1",
"shiki": "^4.2.0",
"string-width": "^8.2.1",
"terminal-link": "^5.0.0",
"typescript": "^5.9.3",
"wrap-ansi": "^10.0.0"
} }
} }

1504
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
minimumReleaseAge: 4320
allowBuilds:
esbuild: true

View file

@ -1,6 +0,0 @@
{
"main": "dist/tts.js",
"output": "dist/ttsc",
"useCodeCache": true,
"disableExperimentalSEAWarning": true
}

View file

@ -1,6 +0,0 @@
{
"main": "dist/index.js",
"output": "dist/lmc",
"useCodeCache": true,
"disableExperimentalSEAWarning": true
}

View file

@ -1,79 +0,0 @@
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 configPath = `${home}/.lmc_config.json`;
export interface AppConfig {
baseURL: string;
model: string;
}
export async function initConfig(old: AppConfig | null): Promise<AppConfig> {
console.log();
intro(chalk.blue("Welcome to LMC. Let's make a new config!"));
const baseURL = await text({
message: "What's your base URL:",
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);
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,
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 (!model || isCancel(model)) process.exit(1);
const config = { baseURL, model } satisfies AppConfig;
await fs.writeFile(configPath, 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) => model.id);
}
export async function loadConfig(
configFlag: boolean | undefined,
): Promise<AppConfig | null> {
let config: AppConfig;
try {
const text = await fs.readFile(configPath, "utf8");
config = JSON.parse(text);
} catch {
if (configFlag) return null;
console.error(`Config not found at ${configPath}`);
return null;
}
return config;
}

24
src/config/index.ts Normal file
View file

@ -0,0 +1,24 @@
import os from "node:os";
import chalk from "chalk";
const home = os.homedir();
const configFile = Bun.file(`${home}/.aicl_config.json`);
export interface AppConfig {
model?: string;
}
export async function loadConfig(): Promise<AppConfig> {
let config: AppConfig;
try {
config = await configFile.json();
} catch {
config = {};
}
return config;
}
export async function saveConfig(config: AppConfig): Promise<void> {
await configFile.write(JSON.stringify(config, null, " "));
console.log(chalk.green("Configuration saved!"));
}

View file

@ -1,38 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import type { ModelMessage } from "ai";
const home = os.homedir();
const convoPath = `${home}/.lmc_convo.json`;
export interface IConversation {
model: string;
messages: ModelMessage[];
}
export class Conversation implements IConversation {
messages: ModelMessage[] = [];
constructor(public model: string) {}
async load() {
try {
const text = await fs.readFile(convoPath, "utf8");
const convo: IConversation = JSON.parse(text);
this.model = convo.model;
this.messages = convo.messages;
} catch {
console.error("No conversation found, starting one.");
}
}
async save() {
await fs.writeFile(
convoPath,
JSON.stringify({
model: this.model,
messages: this.messages,
} satisfies IConversation),
);
}
}

24
src/conversation/index.ts Normal file
View file

@ -0,0 +1,24 @@
import os from "node:os";
import type { ModelMessage } from "ai";
const home = os.homedir();
export const convoFile = Bun.file(`${home}/.aicl_convo.json`);
export interface Conversation {
model: string;
messages: ModelMessage[];
}
export async function loadConversation(
defaultModel: string,
): Promise<Conversation> {
try {
return await convoFile.json();
} catch {
return { model: defaultModel, messages: [] };
}
}
export async function saveConversation(convo: Conversation): Promise<void> {
await convoFile.write(JSON.stringify(convo));
}

View file

@ -1,27 +0,0 @@
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 (isErrorLike(err)) {
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);
}

View file

@ -1,107 +0,0 @@
#!/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";
import { streamText } from "ai";
import { initConfig, loadConfig } from "./config";
import { Conversation } from "./conversation";
import { handleTopLevel } from "./errors";
import {
base,
commitDiff,
contextFence,
diffFence,
fileFence,
review,
} from "./prompts";
import Renderer from "./render";
async function main() {
const { values: args, positionals } = util.parseArgs({
args: process.argv.slice(2),
options: {
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", short: "v" },
"git-message": { type: "boolean", short: "g" },
review: { type: "boolean", short: "r" },
file: { type: "string", short: "f", multiple: true },
stdin: { type: "boolean", short: "i" },
},
strict: true,
allowPositionals: true,
});
if (args.version) {
console.log("0.0.0");
return;
}
const config = await loadConfig(args.config);
if (args.config || !config) {
await initConfig(config);
return;
}
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({
name: "llama.cpp",
apiKey: "local",
baseURL: config.baseURL,
includeUsage: true, // Include usage information in streaming responses
});
const convo = new Conversation(model);
if (!args.new) {
await convo.load();
if (args.model) convo.model = args.model;
}
const input = positionals.join(" ").trim();
let stdinText = "";
if (args.stdin) stdinText = await consumers.text(process.stdin);
const files = [];
for (const file of args.file ?? [])
files.push(fileFence(file, await fs.readFile(file, "utf8")));
let prompt = base(input, [
...files,
stdinText ? contextFence(stdinText) : "",
]);
if (args["git-message"]) {
prompt = commitDiff(stdinText, [...files]);
} else if (args.review) {
prompt = review([...files, stdinText ? diffFence(stdinText) : "", input]);
}
if (!prompt) throw new Error("No prompt given.");
convo.messages.push({ role: "user", content: prompt });
const { fullStream, text, totalUsage } = streamText({
model: provider(convo.model),
messages: convo.messages,
});
await renderer.renderStream(fullStream);
convo.messages.push({ role: "assistant", content: await text });
await convo.save();
await renderer.renderStats(totalUsage);
}
main().catch(handleTopLevel);

View file

@ -1,31 +0,0 @@
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) => `<file name="${name}">
${input}
</file>
`;
export const diffFence = (input: string) => `<diff>
${input}
</diff>`;
export const contextFence = (input: string) => `<context>
${input}
</context>`;
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.
Let me know if you need any more context to complete your review.`;

View file

@ -24,7 +24,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */ SOFTWARE. */
import c from "chalk"; import c from "chalk";
import type { BundledLanguage, BundledTheme, HighlighterCore } from "shiki"; import type { BundledLanguage, BundledTheme, HighlighterGeneric } from "shiki";
// From @shikijs/vscode-textmate // From @shikijs/vscode-textmate
enum FontStyle { enum FontStyle {
@ -37,7 +37,7 @@ enum FontStyle {
} }
export function codeToANSI( export function codeToANSI(
highlighter: HighlighterCore, highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>,
code: string, code: string,
lang: BundledLanguage, lang: BundledLanguage,
theme: BundledTheme, theme: BundledTheme,

View file

@ -1,5 +1,4 @@
import type { WriteStream } from "node:tty"; import type { WriteStream } from "node:tty";
import { spinner } from "@clack/prompts";
import type { import type {
AsyncIterableStream, AsyncIterableStream,
LanguageModelUsage, LanguageModelUsage,
@ -8,29 +7,34 @@ import type {
} from "ai"; } from "ai";
import styles from "ansi-styles"; import styles from "ansi-styles";
import { chalkStderr, supportsColorStderr } from "chalk"; import { chalkStderr, supportsColorStderr } from "chalk";
import stringWidth from "string-width";
import { renderMarkdown } from "./markdown"; import { renderMarkdown } from "./markdown";
export default class Renderer { export default class Renderer {
private lineCount = 0; private lineCount = 0;
private charsSinceNL = 0; private charsSinceNL = 0;
private memOk = true;
private buffer = ""; private buffer = "";
private unsafeIndex = 0; private unsafeIndex = 0;
private start = 0;
private stop = 0;
constructor( constructor(
private isPlain = false, private isPlain = false,
private isPlainErr = 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) { private write(text: string, pipe: WriteStream = process.stdout) {
pipe.write(text); this.memOk = pipe.write(text);
} }
private moveClearReset(y: number, pipe: WriteStream = process.stdout) { private moveClearReset(y: number, pipe: WriteStream = process.stdout) {
pipe.moveCursor(-9999, y); this.memOk = pipe.moveCursor(-9999, y);
pipe.clearScreenDown(); this.memOk = pipe.clearScreenDown();
this.resetCounts(); this.resetCounts();
} }
@ -41,17 +45,16 @@ export default class Renderer {
private checkContainment(text: string, pipe: WriteStream = process.stdout) { private checkContainment(text: string, pipe: WriteStream = process.stdout) {
const [columns, rows] = pipe.getWindowSize(); const [columns, rows] = pipe.getWindowSize();
this.charsSinceNL += stringWidth(text); this.charsSinceNL += text.length;
if (text.includes("\n")) { if (text.includes("\n")) {
this.lineCount += text.match(/\n/g)?.length ?? 1; this.lineCount += text.match(/\n/g)?.length ?? 1;
this.charsSinceNL = this.charsSinceNL = text.length - text.lastIndexOf("\n") - 1;
stringWidth(text) - stringWidth(text.slice(0, text.lastIndexOf("\n")));
} else if (this.charsSinceNL > columns) { } else if (this.charsSinceNL > columns) {
this.lineCount++; this.lineCount++;
this.charsSinceNL = this.charsSinceNL - columns; this.charsSinceNL = this.charsSinceNL - columns;
} }
if (this.lineCount >= Math.max(rows - 4, 1)) { if (this.lineCount >= Math.max(rows - 10, 1)) {
this.moveClearReset(-this.lineCount, pipe); this.moveClearReset(-this.lineCount, pipe);
} }
} }
@ -84,22 +87,9 @@ export default class Renderer {
async renderStream(stream: AsyncIterableStream<TextStreamPart<ToolSet>>) { async renderStream(stream: AsyncIterableStream<TextStreamPart<ToolSet>>) {
this.resetCounts(); this.resetCounts();
const spin = spinner({
withGuide: false,
indicator: "timer",
onCancel: () => {
process.exit(130);
},
});
if (!this.isPlain) {
spin.start("Waiting for response");
}
for await (const part of stream) { for await (const part of stream) {
switch (part.type) { switch (part.type) {
case "reasoning-start": { case "reasoning-start": {
this.start = Date.now();
spin.clear();
if (supportsColorStderr) if (supportsColorStderr)
this.write( this.write(
`${styles.dim.open}${styles.italic.open}`, `${styles.dim.open}${styles.italic.open}`,
@ -121,8 +111,6 @@ export default class Renderer {
} else { } else {
this.write("\n--- Done Thinking ---\n", process.stderr); this.write("\n--- Done Thinking ---\n", process.stderr);
} }
const time = (Date.now() - this.start) / 1000;
this.write(`Thought for ${time.toFixed(2)}s\n`, process.stderr);
if (supportsColorStderr) if (supportsColorStderr)
this.write( this.write(
@ -133,8 +121,6 @@ export default class Renderer {
break; break;
} }
case "text-start": { case "text-start": {
if (this.start === 0) this.start = Date.now();
spin.clear();
this.write("\n"); this.write("\n");
break; break;
} }
@ -147,7 +133,6 @@ export default class Renderer {
break; break;
} }
case "text-end": { case "text-end": {
this.stop = Date.now();
if (this.buffer) { if (this.buffer) {
await this.render(this.buffer); await this.render(this.buffer);
this.buffer = ""; this.buffer = "";
@ -157,15 +142,21 @@ export default class Renderer {
break; break;
} }
} }
while (!this.memOk) await Bun.sleep(10);
} }
} }
async renderStats(usage: PromiseLike<LanguageModelUsage>) { async renderStats(
usage: PromiseLike<LanguageModelUsage>,
start: number,
stop: number,
) {
const { inputTokens, outputTokens } = await usage; const { inputTokens, outputTokens } = await usage;
const context = (inputTokens ?? 0) / 1000; const context = (inputTokens ?? 0) / 1000;
const output = (outputTokens ?? 0) / 1000; const output = (outputTokens ?? 0) / 1000;
const toks = (outputTokens ?? 0) / ((this.stop - this.start) / 1000); const time = (stop - start) / 1000;
const template = `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Toks: ${toks.toFixed(2)}/s\n`; const template = `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Time: ${time.toFixed(2)}s\n`;
this.write(chalkStderr.dim(template), process.stderr); this.write(chalkStderr.dim(template), process.stderr);
} }
} }

View file

@ -1,192 +1,17 @@
import { Cell, Table } from "@cliffy/table";
import chalk from "chalk"; import chalk from "chalk";
import { marked, Renderer, type Tokens } from "marked"; import CliTable3 from "cli-table3";
import type { BundledLanguage, HighlighterCore } from "shiki"; import {
import { createHighlighterCore } from "shiki/core"; type BundledLanguage,
import { createOnigurumaEngine } from "shiki/engine/oniguruma"; type BundledTheme,
import stringWidth from "string-width"; createHighlighter,
type HighlighterGeneric,
} from "shiki";
import terminalLink from "terminal-link"; import terminalLink from "terminal-link";
import wrapAnsi from "wrap-ansi";
import { codeToANSI } from "./codeToANSI"; import { codeToANSI } from "./codeToANSI";
let highlighter: HighlighterCore | undefined; let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | undefined;
let highligherLangs = ""; let highligherLangs = "";
class SWCell extends Cell {
public override get length() {
return stringWidth(this.toString());
}
}
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 color.bold(`${"#".repeat(depth)} ${text}\n`);
}
override paragraph({ tokens }: Tokens.Paragraph) {
const text = wrapAnsi(this.parser.parseInline(tokens), this.columns - 4);
return `\n${text}\n`;
}
override blockquote({ tokens }: Tokens.Blockquote) {
const text = this.parser.parse(tokens).trim();
const lines = `\n${text}\n`.split("\n");
const toRender = lines.map((line) => `> ${line}`).join("\n");
return `\n${toRender.replaceAll(">", chalk.dim(">"))}\n`;
}
override code({ text, lang }: Tokens.Code) {
let toRender = text;
if (highlighter && lang) {
try {
toRender = codeToANSI(
highlighter,
text,
lang as BundledLanguage,
"kanagawa-wave",
);
} catch {}
}
return `\n\`\`\`${chalk.blue(lang ?? "")}\n${toRender}\n\`\`\`\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`;
}
override listitem({ tokens }: Tokens.ListItem, num?: number) {
const text = this.parser.parse(tokens).trim();
let marker = "";
if (num !== undefined) {
marker = `${num}.`;
} else {
marker = "•";
}
const item = `${marker} ${text}`;
return item
.split("\n")
.filter((line) => line.length > 0)
.map((line) => ` ${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 headers = 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 cols = this.columns - (headers.length * 3 + 1);
const maxWidth = Math.floor(cols / headers.length);
const table = new Table()
.header(headers)
.body(body)
.border()
.maxColWidth(maxWidth)
.toString();
return `\n${table}\n`;
}
override strong({ tokens }: Tokens.Strong) {
const text = this.parser.parseInline(tokens);
return chalk.bold.rgb(245, 245, 245)(text);
}
override em({ tokens }: Tokens.Em) {
const text = this.parser.parseInline(tokens);
return chalk.italic.rgb(245, 245, 245)(text);
}
override del({ tokens }: Tokens.Del) {
const text = this.parser.parseInline(tokens);
return chalk.grey.strikethrough(text);
}
override codespan({ text }: Tokens.Codespan) {
return chalk.bgRgb(165, 165, 192).rgb(0, 0, 0).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("\nImages are unsupported (for now)\n");
}
override text(token: Tokens.Text | Tokens.Escape) {
if (token.type === "text" && token.tokens) {
const parsed = this.parser.parseInline(token.tokens);
return wrapAnsi(parsed, this.columns - 12);
}
return this.replaceLatex(token.text);
}
override html({ text }: Tokens.HTML | Tokens.Tag) {
if (text === "<br>") return this.br();
return chalk.red(text);
}
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( export async function renderMarkdown(
text: PromiseLike<string> | string, text: PromiseLike<string> | string,
columns: number, columns: number,
@ -194,29 +19,98 @@ export async function renderMarkdown(
const md = await text; const md = await text;
const langs = md const langs = md
.match(/```\S+\n/g) .match(/```[a-z]+\n/g)
?.map((lang) => lang.replace("```", "").trimEnd()) ?.map((lang) => lang.replace("```", "").trimEnd());
.sort();
if (langs && langs?.join() !== highligherLangs) { if (langs && langs?.join() !== highligherLangs) {
const { bundledThemes, bundledLanguages, bundledLanguagesAlias } =
await import("shiki/bundle/full");
try { try {
highligherLangs = langs.join(); highligherLangs = langs.join();
highlighter = await createHighlighterCore({ highlighter = await createHighlighter({
langs: langs.map( langs,
(lang) => themes: ["kanagawa-wave", "kanagawa-lotus"],
bundledLanguages[lang as BundledLanguage] ||
bundledLanguagesAlias[lang],
),
themes: [bundledThemes["kanagawa-wave"]],
engine: createOnigurumaEngine(import("shiki/wasm")),
}); });
} catch {} } catch {}
} }
const renderer = new LmcRenderer(columns); let currentTable = new CliTable3();
const rendered = await marked.parse(md, { renderer });
return rendered.trimStart(); 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 color.bold(`${"#".repeat(level)} ${children}\n\n`);
},
paragraph: (children) => `${children}\n\n`,
blockquote: (children) => {
const lines = children.split("\n");
return lines
.map((line) => `${chalk.dim("> ")}${line}`)
.join("\n")
.replace(/\n$/, "\n\n");
},
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 }) =>
`${depth > 0 ? "\n" : ""}${children.trimEnd()}${depth === 0 ? "\n\n" : ""}`,
listItem: (children, { index, depth, ordered, start, checked }) => {
const indent = " ".repeat(depth);
let marker = "";
if (checked !== undefined) {
marker = checked ? "[✓]" : "[ ]";
} else if (ordered) {
const n = (start ?? 1) + index;
marker = `${n}.`;
} else {
marker = "•";
}
return `${indent} ${marker} ${children}\n`;
},
hr: () => `${chalk.dim("─".repeat(columns))}\n\n`,
table: (_children) => {
const toRender = currentTable.toString();
currentTable = new CliTable3();
return `${toRender}\n\n`;
},
tr: (children) => {
if (children) currentTable.push(children.split(":tablesep:").slice(1));
return "";
},
td: (children) => `:tablesep:${children}`,
th: (children) => {
currentTable.options.head.push(children);
return "";
},
strong: (children) => chalk.bold(children),
emphasis: (children) => chalk.italic(children),
strikethrough: (children) => chalk.grey.strikethrough(children),
codespan: (children) => chalk.bgWhite.black(` ${children} `),
link: (children, { href }) =>
chalk.blueBright.underline(terminalLink(children, href)),
image: () => "",
text: (children) => children,
html: () => "",
});
return rendered.replace(/\n\n$/, "\n");
} }

View file

@ -1,180 +0,0 @@
#!/usr/bin/env node
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";
import { ExecaError, execa } from "execa";
import { Window } from "happy-dom";
import wrapAnsi from "wrap-ansi";
import { handleTopLevel } from "./errors";
async function main() {
const start = Date.now();
const { values: args, positionals } = util.parseArgs({
args: process.argv.slice(2),
options: {
wait: { type: "boolean" },
w: { type: "boolean" },
pause: { type: "boolean" },
p: { type: "boolean" },
url: { type: "boolean" },
u: { type: "boolean" },
edit: { type: "boolean" },
e: { type: "boolean" },
},
strict: true,
allowPositionals: true,
});
const waitAndCombine = args.wait ?? args.w;
const pausePlayer = args.pause ?? args.p;
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)).trim();
if (!input) {
throw new Error("No input was given. Use positional arguments or stdin.");
}
if (readURL) {
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 Error(
"Unable to read webpage. This usually means it's blocking scrapers.",
);
}
input = readable.textContent.trim();
}
const home = os.homedir();
const cwd = path.join(home, ".ttsc");
if (editContent) {
const file = path.join(cwd, "input.txt");
await fs.writeFile(file, input, "utf8");
const editor = process.env.EDITOR || "nano";
await execa({
cwd,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
})`${editor} ${file}`;
input = await fs.readFile(file, "utf8");
}
console.log(chalk.blue("Welcome to TTSC!"));
console.log(
chalk.yellow(
"Beginning TTS processing... (This will take extra time the first time)",
),
);
const wavs = fs.glob(path.join(cwd, "*.wav"));
for await (const wav of wavs) await fs.rm(wav);
const koko = execa({
cwd,
input,
stdout: "pipe",
stderr: "inherit",
})`uv run -- python -u main.py`;
if (!waitAndCombine) {
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 files = [];
const rl = readline.createInterface({ input: koko.stdout });
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 file = path.join(cwd, `${info.file}.wav`);
if (waitAndCombine) {
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,
stdin: "ignore",
stdout: "inherit",
stderr: "inherit",
})`mpv${pausePlayer ? " --pause" : ""} ${file}`;
}
}
await koko;
if (!waitAndCombine) return;
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...\n`));
try {
await execa({ cwd, shell: true })`sox ${files} output.wav`;
} catch (err) {
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;
} else {
throw err;
}
}
const [columns] = process.stdout.getWindowSize();
console.log(wrapAnsi(input, columns));
console.log(
chalk.green(`Playing audio from ${path.join(cwd, "output.wav")}`),
);
await execa({
cwd,
stdin: "ignore",
stdout: "inherit",
stderr: "inherit",
})`mpv${pausePlayer ? " --pause" : ""} output.wav`;
}
main().catch(handleTopLevel);

6
testRender.ts Normal file
View file

@ -0,0 +1,6 @@
import { renderMarkdown } from "./src/render/markdown.ts";
const text = await Bun.file("test.md").text();
const [columns] = process.stdout.getWindowSize();
process.stdout.write(await renderMarkdown(text, columns));

View file

@ -1,39 +1,29 @@
{ {
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": { "compilerOptions": {
// Environment Settings // Environment setup & latest features
// See also https://aka.ms/tsconfig/module "lib": ["ESNext"],
"module": "esnext", "target": "ESNext",
"target": "esnext", "module": "Preserve",
"moduleResolution": "bundler",
"esModuleInterop": true,
"noEmit": true,
// For nodejs:
"lib": ["esnext"],
"types": ["node"],
// and npm install -D @types/node
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
"noImplicitReturns": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": false,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"allowImportingTsExtensions": false,
"allowArbitraryExtensions": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force", "moduleDetection": "force",
"skipLibCheck": true "jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
} }
} }

View file

@ -1,13 +0,0 @@
from kokoro import KPipeline
import soundfile as sf
import sys
import json
text = sys.stdin.read()
pipeline = KPipeline(lang_code="a", repo_id="hexgrad/Kokoro-82M")
generator = pipeline(text, voice="af_heart", speed=1.2)
for i, (gs, ps, audio) in enumerate(generator):
sf.write(f"{i}.wav", audio, 24000)
print(json.dumps({"file": i, "text": gs}))

View file

@ -1,8 +0,0 @@
[project]
name = "tts"
version = "0.1.0"
requires-python = "==3.12.*"
dependencies = [
"kokoro==0.9.4",
"soundfile==0.13.1",
]

2127
tts/uv.lock generated

File diff suppressed because it is too large Load diff