Compare commits
22 commits
feat/tts-i
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 159547e799 | |||
| a8cbfb3665 | |||
| 4aaa80898d | |||
| 5b6c65d30f | |||
| b1730bfaf4 | |||
| da317b7177 | |||
| 49c4138a1c | |||
| e1033398e9 | |||
| 5b138df664 | |||
| bf5e223abf | |||
| d157978d52 | |||
| e2253b912d | |||
| 4725f148f3 | |||
| 4bccbcc9ee | |||
| 7652e62d47 | |||
| 19272bc0b4 | |||
| eec90209d2 | |||
| 3c3dab1525 | |||
| f8e0f9a368 | |||
| 56352d87ec | |||
| f3146ab087 | |||
| fc7d9101e9 |
14 changed files with 324 additions and 244 deletions
1
.npmrc
1
.npmrc
|
|
@ -1 +0,0 @@
|
|||
@jsr:registry=https://npm.jsr.io
|
||||
26
AGENTS.md
26
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
|
||||
|
||||
|
|
|
|||
84
index.ts
84
index.ts
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
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 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" },
|
||||
plainOut: { type: "boolean" },
|
||||
plainErr: { type: "boolean" },
|
||||
v: { 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 configFlag = args.config ?? args.c;
|
||||
|
||||
if (args.v) {
|
||||
console.log("0.0.0");
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await loadConfig(configFlag);
|
||||
|
||||
if (configFlag || !config) {
|
||||
await initConfig(config);
|
||||
return;
|
||||
}
|
||||
|
||||
const model = modelFlag ?? config.model;
|
||||
|
||||
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 (!newConvo) {
|
||||
await convo.load();
|
||||
if (modelFlag) convo.model = modelFlag;
|
||||
}
|
||||
|
||||
const prompt = positionals.join(" ") || "Introduce yourself.";
|
||||
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((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
25
package.json
25
package.json
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "lmc",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"bin": {
|
||||
|
|
@ -8,35 +7,35 @@
|
|||
"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",
|
||||
"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",
|
||||
"@cliffy/table": "npm:@jsr/cliffy__table",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
199
pnpm-lock.yaml
generated
199
pnpm-lock.yaml
generated
|
|
@ -9,26 +9,26 @@ 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: npm:@jsr/cliffy__table
|
||||
specifier: jsr:^1.2.1
|
||||
version: '@jsr/cliffy__table@1.2.1'
|
||||
'@mozilla/readability':
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
minimumReleaseAge: 4320
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
|
|
|
|||
27
src/errors.ts
Normal file
27
src/errors.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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);
|
||||
}
|
||||
107
src/index.ts
Normal file
107
src/index.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
#!/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);
|
||||
31
src/prompts.ts
Normal file
31
src/prompts.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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.`;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,13 @@ export default class Renderer {
|
|||
async renderStream(stream: AsyncIterableStream<TextStreamPart<ToolSet>>) {
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class LmcRenderer extends Renderer {
|
|||
this.listitem(item, ordered ? num + index : undefined),
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
return `\n${renderedItems}\n`;
|
||||
}
|
||||
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
#!/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";
|
||||
import { execa } from "execa";
|
||||
import { ExecaError, execa } from "execa";
|
||||
import { Window } from "happy-dom";
|
||||
import wrapAnsi from "wrap-ansi";
|
||||
|
||||
class InputError extends Error {
|
||||
override name = "InputError";
|
||||
}
|
||||
import { handleTopLevel } from "./errors";
|
||||
|
||||
async function main() {
|
||||
const start = Date.now();
|
||||
|
|
@ -24,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" },
|
||||
|
|
@ -33,27 +32,20 @@ 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 pausePlayer = args.pause ?? args.p;
|
||||
const readURL = args.url ?? args.u;
|
||||
const editContent = args.edit ?? args.e;
|
||||
|
||||
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)).trim();
|
||||
|
||||
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) {
|
||||
if (readURL) {
|
||||
const result = await fetch(input);
|
||||
const window = new Window({ url: input });
|
||||
const doc = window.document;
|
||||
|
|
@ -61,7 +53,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.",
|
||||
);
|
||||
}
|
||||
|
|
@ -71,7 +63,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";
|
||||
|
|
@ -101,7 +93,7 @@ async function main() {
|
|||
stderr: "inherit",
|
||||
})`uv run -- python -u main.py`;
|
||||
|
||||
if (!wait) {
|
||||
if (!waitAndCombine) {
|
||||
koko.catch(() => {});
|
||||
koko.then(() => {
|
||||
const stop = Date.now();
|
||||
|
|
@ -135,7 +127,7 @@ async function main() {
|
|||
|
||||
const file = path.join(cwd, `${info.file}.wav`);
|
||||
|
||||
if (wait) {
|
||||
if (waitAndCombine) {
|
||||
files.push(file);
|
||||
process.stdout.write(".");
|
||||
} else {
|
||||
|
|
@ -147,12 +139,12 @@ async function main() {
|
|||
stdin: "ignore",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})`mpv ${file}`;
|
||||
})`mpv${pausePlayer ? " --pause" : ""} ${file}`;
|
||||
}
|
||||
}
|
||||
|
||||
await koko;
|
||||
if (!wait) return;
|
||||
if (!waitAndCombine) return;
|
||||
|
||||
const stop = Date.now();
|
||||
const duration = ((stop - start) / 1000).toFixed(2);
|
||||
|
|
@ -162,7 +154,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;
|
||||
|
|
@ -182,10 +174,7 @@ async function main() {
|
|||
stdin: "ignore",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})`mpv --pause output.wav`;
|
||||
})`mpv${pausePlayer ? " --pause" : ""} output.wav`;
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
main().catch(handleTopLevel);
|
||||
Loading…
Add table
Add a link
Reference in a new issue