@drakulavich/kesha-voice-kit 1.24.6 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  - **Transcribe locally** — [25 languages](docs/languages.md#speech-to-text-25), up to ~19x faster than Whisper on Apple Silicon, ~2.5x on CPU
17
17
  - **Speak back** — text-to-speech in [9 languages](docs/languages.md#text-to-speech)
18
18
  - **Plug into agents** — ship voice workflows as CLI commands, an MCP server, an <a href="docs/openclaw.md">OpenClaw</a> skill, or a <a href="docs/hermes.md">Hermes</a> agent
19
- - **Small Rust engine** — single ~20MB binary, no ffmpeg, no Python, no native Node addons
19
+ - **Small Rust engine** — single ~60MB binary, no ffmpeg, no Python, no native Node addons
20
20
 
21
21
  <p align="center">
22
22
  <img src="https://github.com/drakulavich/kesha-voice-kit/raw/main/demo.gif" alt="kesha demo — English + Russian transcription with automatic language detection" width="800">
@@ -24,16 +24,21 @@
24
24
 
25
25
  ## Quick Start
26
26
 
27
- Runtime: **[Bun](https://bun.sh)** >= 1.3.0 · Platforms: macOS arm64, Linux x64, Windows x64.
27
+ Runtime: **[Bun](https://bun.sh)** >= 1.3.0 · Platforms: macOS arm64, Linux x64. Windows x64 is currently blocked at install ([#216](https://github.com/drakulavich/kesha-voice-kit/issues/216)).
28
28
 
29
29
  ```bash
30
30
  # 1. Install Bun (skip if you have it) — Linux & macOS:
31
31
  curl -fsSL https://bun.sh/install | bash # or: brew install oven-sh/bun/bun
32
32
  # Windows: powershell -c "irm bun.sh/install.ps1 | iex"
33
+ # if `bun --version` fails, reload PATH: exec $SHELL -l
33
34
 
34
35
  # 2. Install Kesha:
35
36
  bun add -g @drakulavich/kesha-voice-kit
36
- kesha install # downloads engine + models (explicit — never automatic)
37
+ kesha --version # confirms `kesha` resolved on PATH
38
+ kesha install --plan # preview exact download/disk sizes first — downloads nothing
39
+ kesha install # ~2.5 GB (engine + speech-to-text models), explicit — never automatic.
40
+ # No progress bar during the model step; can take several minutes.
41
+ # Prefer a guided wizard? `kesha init` walks through the same choices interactively.
37
42
 
38
43
  # 3. Transcribe:
39
44
  kesha audio.ogg # transcript to stdout
@@ -51,6 +56,8 @@ kesha --format json audio.ogg # full JSON with lang fields
51
56
  kesha --json --timestamps audio.ogg # JSON with timestamped segments
52
57
  kesha --toon audio.ogg # compact LLM-friendly TOON
53
58
  kesha status # show installed backend info
59
+ kesha status --disk # + recursive cache disk usage
60
+ kesha status --json # machine-readable, for scripts
54
61
  ```
55
62
 
56
63
  Multiple files get `head`-style headers; stdout is the transcript, stderr is errors — pipe-friendly:
@@ -64,6 +71,7 @@ $ kesha freedom.ogg tahiti.ogg
64
71
  Таити, Таити! Не были мы ни в какой Таити! Нас и тут неплохо кормят.
65
72
  ```
66
73
 
74
+ - **Record from the mic (macOS):** `kesha record --out hello.wav` writes microphone audio to a WAV file (`kesha hello.wav` transcribes it). macOS prompts for microphone access on first use — grant it under System Settings → Privacy & Security → Microphone if it was denied. On Linux/Windows or headless boxes, pass any existing audio file straight to `kesha` instead.
67
75
  - **Long / silence-heavy audio:** install VAD (`kesha install --vad`); Kesha auto-uses it past 120 s. Without VAD, long audio falls back to fixed ASR chunks. See [docs/vad.md](docs/vad.md).
68
76
  - **Speaker diarization** (darwin-arm64): `kesha install --diarize`, then `kesha --json --vad --speakers meeting.m4a` stamps each segment with a `speaker` id. Linux/Windows return a clear "darwin-arm64 only" error ([#199](https://github.com/drakulavich/kesha-voice-kit/issues/199)).
69
77
 
@@ -72,7 +80,8 @@ $ kesha freedom.ogg tahiti.ogg
72
80
  Kesha speaks back in [9 languages](docs/languages.md#text-to-speech), auto-picking the voice from the text's language. Override with `--lang <code>` or `--voice <id>`.
73
81
 
74
82
  ```bash
75
- kesha install --tts # opt-in models (~990MB)
83
+ kesha install --tts # English voices; sizes differ per platform — preview: kesha install --plan
84
+ kesha install --tts en ru # + Russian (+~890 MB, Vosk)
76
85
  kesha say "Hello, world" > hello.wav
77
86
  kesha say "Привет, мир" > privet.wav # auto-routes by language
78
87
  kesha say --voice ru-vosk-m02 "Голос в текст." > ru.wav
@@ -117,7 +126,7 @@ All of these install the Bun CLI wrapper; engine + models still download explici
117
126
  - **MCP server** — `kesha mcp` exposes transcribe/synthesize/list tools to any MCP client (Claude, Cursor, Codex, Gemini). Setup: [docs/mcp.md](docs/mcp.md).
118
127
  - **OpenClaw** — give your LLM agent ears. Install & config: [docs/openclaw.md](docs/openclaw.md).
119
128
  - **Hermes Agent** — local STT/TTS through Hermes command providers. Setup: [docs/hermes.md](docs/hermes.md).
120
- - **Raycast** (macOS) — transcribe selected audio & speak the clipboard from the launcher. Source + install: [`raycast/`](raycast/).
129
+ - **Raycast** (macOS) — offline microphone dictation from the launcher: *Dictate to Clipboard* records with a live signal meter, auto-stops on silence, transcribes locally, and copies the text. [Install from the Raycast Store](https://www.raycast.com/drakulavich/kesha-voice-kit) · source: [`raycast/`](raycast/).
121
130
  - **Programmatic API** — `@drakulavich/kesha-voice-kit/core` for use inside a Bun program. See [docs/api.md](docs/api.md).
122
131
 
123
132
  ## More
@@ -28,7 +28,7 @@ _kesha_completion() {
28
28
  record) opts="--help -h --out --max-seconds --debug" ;;
29
29
  say) opts="--help -h --voice --lang --out --rate --list-voices --ssml --format --bitrate --sample-rate --no-expand-abbrev --verbose --debug" ;;
30
30
  stats) opts="--help -h --format" ;;
31
- status) opts="--help -h --disk" ;;
31
+ status) opts="--help -h --disk --json" ;;
32
32
  support-bundle) opts="--help -h --output --include-logs" ;;
33
33
  *) opts="--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad --quiet --no-color" ;;
34
34
  esac
@@ -42,7 +42,7 @@ complete -c kesha -n '__fish_seen_subcommand_from init' -l onnx -d 'Preselect ON
42
42
  complete -c kesha -n '__fish_seen_subcommand_from init' -l no-cache -d 'Re-download even if cached'
43
43
  complete -c kesha -n '__fish_seen_subcommand_from init' -l plan -d 'Show the selected install plan without downloading'
44
44
  complete -c kesha -n '__fish_seen_subcommand_from init' -l yes -d 'Accept defaults and run without prompts'
45
- complete -c kesha -n '__fish_seen_subcommand_from init' -l tts -d 'Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)'
45
+ complete -c kesha -n '__fish_seen_subcommand_from init' -l tts -d 'Preselect TTS models (English; more languages via kesha install --tts <langs>)'
46
46
  complete -c kesha -n '__fish_seen_subcommand_from init' -l vad -d 'Preselect Silero VAD (~2.3MB) for long-audio preprocessing'
47
47
  complete -c kesha -n '__fish_seen_subcommand_from init' -l diarize -d 'Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)'
48
48
  complete -c kesha -n '__fish_seen_subcommand_from install' -l help -d 'Show help'
@@ -51,7 +51,7 @@ complete -c kesha -n '__fish_seen_subcommand_from install' -l coreml -d 'Force C
51
51
  complete -c kesha -n '__fish_seen_subcommand_from install' -l onnx -d 'Force ONNX backend'
52
52
  complete -c kesha -n '__fish_seen_subcommand_from install' -l no-cache -d 'Re-download even if cached'
53
53
  complete -c kesha -n '__fish_seen_subcommand_from install' -l plan -d 'Show download, disk, and warm-up plan without changing local state'
54
- complete -c kesha -n '__fish_seen_subcommand_from install' -l tts -d 'Also install TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)'
54
+ complete -c kesha -n '__fish_seen_subcommand_from install' -l tts -d 'Also install TTS models (English only; add codes for more, e.g. --tts en ru (preview sizes: kesha install --plan))'
55
55
  complete -c kesha -n '__fish_seen_subcommand_from install' -l vad -d 'Also install Silero VAD (~2.3MB) for long-audio preprocessing'
56
56
  complete -c kesha -n '__fish_seen_subcommand_from install' -l diarize -d 'Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)'
57
57
  complete -c kesha -n '__fish_seen_subcommand_from logs' -l help -d 'Show help'
@@ -86,6 +86,7 @@ complete -c kesha -n '__fish_seen_subcommand_from stats' -l format -r -d 'Export
86
86
  complete -c kesha -n '__fish_seen_subcommand_from status' -l help -d 'Show help'
87
87
  complete -c kesha -n '__fish_seen_subcommand_from status' -s h -d 'Show help'
88
88
  complete -c kesha -n '__fish_seen_subcommand_from status' -l disk -d 'Include recursive cache disk usage'
89
+ complete -c kesha -n '__fish_seen_subcommand_from status' -l json -d 'Output status as JSON'
89
90
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l help -d 'Show help'
90
91
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -s h -d 'Show help'
91
92
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l output -r -d 'Write archive to this .tar.gz path'
@@ -62,7 +62,7 @@ _kesha() {
62
62
  '--no-cache[Re-download even if cached]' \
63
63
  '--plan[Show the selected install plan without downloading]' \
64
64
  '--yes[Accept defaults and run without prompts]' \
65
- '--tts[Preselect TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)]' \
65
+ '--tts[Preselect TTS models (English; more languages via kesha install --tts <langs>)]' \
66
66
  '--vad[Preselect Silero VAD (~2.3MB) for long-audio preprocessing]' \
67
67
  '--diarize[Preselect Sortformer speaker diarization (~245MB, darwin-arm64 only)]'
68
68
  ;;
@@ -73,7 +73,7 @@ _kesha() {
73
73
  '--onnx[Force ONNX backend]' \
74
74
  '--no-cache[Re-download even if cached]' \
75
75
  '--plan[Show download, disk, and warm-up plan without changing local state]' \
76
- '--tts[Also install TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)]' \
76
+ '--tts[Also install TTS models (English only; add codes for more, e.g. --tts en ru (preview sizes: kesha install --plan))]' \
77
77
  '--vad[Also install Silero VAD (~2.3MB) for long-audio preprocessing]' \
78
78
  '--diarize[Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)]'
79
79
  ;;
@@ -121,7 +121,8 @@ _kesha() {
121
121
  status)
122
122
  _arguments '--help[Show help]' \
123
123
  '-h[Show help]' \
124
- '--disk[Include recursive cache disk usage]'
124
+ '--disk[Include recursive cache disk usage]' \
125
+ '--json[Output status as JSON]'
125
126
  ;;
126
127
  support-bundle)
127
128
  _arguments '--help[Show help]' \
@@ -60,19 +60,28 @@ The product promise is not "one model for every audio problem." The promise is a
60
60
 
61
61
  | Capability | macOS arm64 | macOS x64 | Linux x64 | Windows x64 | Nix path |
62
62
  |---|---|---|---|---|---|
63
- | STT | Supported, CoreML path | Not shipped | Supported, ONNX CPU path | Supported, ONNX CPU path | `aarch64-darwin`, `x86_64-linux` |
64
- | Audio language detection | Supported | Not shipped | Supported | Supported | Supported where the flake builds |
65
- | VAD | Supported with `kesha install --vad` | Not shipped | Supported with `kesha install --vad` | Supported with `kesha install --vad` | Supported where the engine path includes VAD assets |
66
- | TTS: English Kokoro | Supported, FluidAudio/CoreML in release builds | Not shipped | Supported, ONNX path | Supported, ONNX path | Supported except where noted in `docs/nix-install.md` |
67
- | TTS: Russian Vosk-TTS | Supported | Not shipped | Supported | Supported | Supported except where noted in `docs/nix-install.md` |
63
+ | STT | Supported, CoreML path | Not shipped | Supported, ONNX CPU path | Blocked at install (#216) | `aarch64-darwin`, `x86_64-linux` |
64
+ | Audio language detection | Supported | Not shipped | Supported | Blocked at install (#216) | Supported where the flake builds |
65
+ | VAD | Supported with `kesha install --vad` | Not shipped | Supported with `kesha install --vad` | Blocked at install (#216) | Supported where the engine path includes VAD assets |
66
+ | TTS: English Kokoro | Supported, FluidAudio/CoreML in release builds | Not shipped | Supported, ONNX path | Blocked at install (#216) | Supported except where noted in `docs/nix-install.md` |
67
+ | TTS: Russian Vosk-TTS | Supported | Not shipped | Supported | Blocked at install (#216) | Supported except where noted in `docs/nix-install.md` |
68
68
  | macOS system voices | Supported | Not shipped | Not applicable | Not applicable | Supported on `aarch64-darwin` |
69
69
  | Speaker diarization | Preview, darwin-arm64 only | Not supported | Not supported | Not supported | Not wired into the Nix build yet |
70
70
  | Raycast extension | Supported | Not shipped | Not applicable | Not applicable | Not applicable |
71
- | OpenClaw integration | Supported through CLI route | Not shipped | Supported through CLI route | Supported through CLI route | Use the installed `kesha` command from the chosen path |
72
- | Hermes Agent integration | Supported through command providers | Not shipped | Supported through command providers | Supported through command providers | Use the installed `kesha` command from the chosen path |
71
+ | OpenClaw integration | Supported through CLI route | Not shipped | Supported through CLI route | Blocked at install (#216) | Use the installed `kesha` command from the chosen path |
72
+ | Hermes Agent integration | Supported through command providers | Not shipped | Supported through command providers | Blocked at install (#216) | Use the installed `kesha` command from the chosen path |
73
73
 
74
74
  `macOS x64` means Intel Macs. Kesha does not currently publish a `darwin-x64` engine binary, so Intel Macs are intentionally marked as not shipped rather than implied to use the ONNX fallback.
75
75
 
76
+ `Windows x64` shows "Blocked at install" because the CLI's install step
77
+ refuses to run on Windows, not because no binary exists — `build-engine.yml`
78
+ builds and publishes `kesha-engine-windows-x64.exe` with every release. The
79
+ Vosk-TTS native dependency trips MSVC at link time, so the CLI blocks the
80
+ install client-side rather than shipping a broken engine. See
81
+ [issue #216](https://github.com/drakulavich/kesha-voice-kit/issues/216) for
82
+ the underlying failure and the v1.4.x workaround; the toolkit is unusable on
83
+ Windows until that lands.
84
+
76
85
  ## When to choose something else
77
86
 
78
87
  Use a hosted transcription provider when you need managed uptime, centralized audit logs, team permissions, or no local model cache. Use a streaming ASR stack when partial transcripts during a live call matter more than local-first batch reliability. Use a domain-specific ASR model when medical, legal, or noisy industrial audio quality is the primary requirement.
package/man/kesha.1 CHANGED
@@ -112,7 +112,7 @@ Show the selected install plan without downloading
112
112
  Accept defaults and run without prompts
113
113
  .TP
114
114
  .B \-\-tts
115
- Preselect TTS models (Kokoro EN + Vosk\-TTS RU, ~990MB)
115
+ Preselect TTS models (English; more languages via kesha install \-\-tts <langs>)
116
116
  .TP
117
117
  .B \-\-vad
118
118
  Preselect Silero VAD (~2.3MB) for long\-audio preprocessing
@@ -134,7 +134,7 @@ Re\-download even if cached
134
134
  Show download, disk, and warm\-up plan without changing local state
135
135
  .TP
136
136
  .B \-\-tts
137
- Also install TTS models (Kokoro EN + Vosk\-TTS RU, ~990MB)
137
+ Also install TTS models (English only; add codes for more, e.g. \-\-tts en ru (preview sizes: kesha install \-\-plan))
138
138
  .TP
139
139
  .B \-\-vad
140
140
  Also install Silero VAD (~2.3MB) for long\-audio preprocessing
@@ -200,6 +200,9 @@ Export format: json | csv
200
200
  .TP
201
201
  .B \-\-disk
202
202
  Include recursive cache disk usage
203
+ .TP
204
+ .B \-\-json
205
+ Output status as JSON
203
206
  .SS support-bundle
204
207
  .TP
205
208
  .B \-\-output
@@ -0,0 +1,43 @@
1
+ {
2
+ "asr": [
3
+ { "relPath": "models/parakeet-tdt-v3/encoder-model.onnx", "sizeBytes": 41770866 },
4
+ { "relPath": "models/parakeet-tdt-v3/encoder-model.onnx.data", "sizeBytes": 2435420160 },
5
+ { "relPath": "models/parakeet-tdt-v3/decoder_joint-model.onnx", "sizeBytes": 72520893 },
6
+ { "relPath": "models/parakeet-tdt-v3/nemo128.onnx", "sizeBytes": 139764 },
7
+ { "relPath": "models/parakeet-tdt-v3/vocab.txt", "sizeBytes": 93939 }
8
+ ],
9
+ "langId": [
10
+ { "relPath": "models/lang-id-ecapa/lang-id-ecapa.onnx", "sizeBytes": 759814 },
11
+ { "relPath": "models/lang-id-ecapa/lang-id-ecapa.onnx.data", "sizeBytes": 85327872 },
12
+ { "relPath": "models/lang-id-ecapa/labels.json", "sizeBytes": 646 }
13
+ ],
14
+ "vad": [
15
+ { "relPath": "models/silero-vad/silero_vad.onnx", "sizeBytes": 2327524 }
16
+ ],
17
+ "g2pCharsiu": [
18
+ { "relPath": "models/g2p/byt5-tiny/encoder_model.onnx", "sizeBytes": 12478704 },
19
+ { "relPath": "models/g2p/byt5-tiny/decoder_model.onnx", "sizeBytes": 11983268 },
20
+ { "relPath": "models/g2p/byt5-tiny/decoder_with_past_model.onnx", "sizeBytes": 5427260 }
21
+ ],
22
+ "kokoroGraph": { "relPath": "models/kokoro-82m/model.onnx", "sizeBytes": 325532387 },
23
+ "kokoroVoices": {
24
+ "en": { "relPath": "models/kokoro-82m/voices/am_michael.bin", "sizeBytes": 522240 },
25
+ "es": { "relPath": "models/kokoro-82m/voices/em_alex.bin", "sizeBytes": 522240 },
26
+ "fr": { "relPath": "models/kokoro-82m/voices/ff_siwis.bin", "sizeBytes": 522240 },
27
+ "it": { "relPath": "models/kokoro-82m/voices/im_nicola.bin", "sizeBytes": 522240 },
28
+ "pt": { "relPath": "models/kokoro-82m/voices/pm_alex.bin", "sizeBytes": 522240 }
29
+ },
30
+ "voskRu": [
31
+ { "relPath": "models/vosk-ru/model.onnx", "sizeBytes": 179314533 },
32
+ { "relPath": "models/vosk-ru/dictionary", "sizeBytes": 101431118 },
33
+ { "relPath": "models/vosk-ru/config.json", "sizeBytes": 1518 },
34
+ { "relPath": "models/vosk-ru/bert/model.onnx", "sizeBytes": 654361598 },
35
+ { "relPath": "models/vosk-ru/bert/vocab.txt", "sizeBytes": 1780720 }
36
+ ],
37
+ "diarize": [
38
+ { "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Manifest.json", "sizeBytes": 617 },
39
+ { "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/model.mlmodel", "sizeBytes": 7080357 },
40
+ { "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/weights/0-weight.bin", "sizeBytes": 5930400 },
41
+ { "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/weights/1-weight.bin", "sizeBytes": 232161600 }
42
+ ]
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.24.6",
3
+ "version": "1.25.0",
4
4
  "description": "Give your tools a voice — speech to text and back, 25 languages, up to ~19× faster than Whisper. On your machine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "completions/",
16
16
  "man/",
17
17
  "src/",
18
+ "model-plan.json",
18
19
  "package.json",
19
20
  "tsconfig.json",
20
21
  "openclaw.plugin.json",
@@ -38,14 +39,14 @@
38
39
  }
39
40
  },
40
41
  "keshaEngine": {
41
- "version": "1.24.6"
42
+ "version": "1.24.7"
42
43
  },
43
44
  "scripts": {
44
- "test": "bun test",
45
- "test:watch": "bun test --watch",
46
- "test:unit": "bun test tests/unit/",
47
- "test:cli-fast": "bun test tests/unit/ tests/integration/cli-contracts.test.ts tests/integration/e2e-cli.test.ts tests/integration/say.test.ts",
48
- "test:integration": "bun test tests/integration/",
45
+ "test": "bun test --timeout 30000",
46
+ "test:watch": "bun test --timeout 30000 --watch",
47
+ "test:unit": "bun test --timeout 30000 tests/unit/",
48
+ "test:cli-fast": "bun test --timeout 30000 tests/unit/ tests/integration/cli-contracts.test.ts tests/integration/e2e-cli.test.ts tests/integration/say.test.ts",
49
+ "test:integration": "bun test --timeout 30000 tests/integration/",
49
50
  "coverage:ts": "bun run test:cli-fast --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage/ts",
50
51
  "coverage:check:ts": "bun .github/scripts/check-coverage.ts --preset=ts --lcov=coverage/ts/lcov.info --summary=coverage/ts/summary.md",
51
52
  "coverage:rust": "mkdir -p coverage/rust && cd rust && cargo llvm-cov nextest --features tts --lcov --output-path ../coverage/rust/lcov.info",
@@ -54,6 +55,7 @@
54
55
  "lint": "bunx tsc --noEmit",
55
56
  "check": "bun run lint && bun run check:versions && bun run test:cli-fast",
56
57
  "check:workflows": "bun .github/scripts/check-workflows.ts",
58
+ "check:specs": "bunx @fission-ai/openspec@1.4.1 validate --specs --strict",
57
59
  "check:release-manifest": "bun .github/scripts/release-manifest.mjs --check",
58
60
  "check:versions": "bun .github/scripts/check-versions.ts"
59
61
  },
@@ -91,7 +93,7 @@
91
93
  "dependencies": {
92
94
  "@clack/prompts": "^1.5.0",
93
95
  "@modelcontextprotocol/sdk": "^1.29.0",
94
- "@toon-format/toon": "^2.1.0",
96
+ "@toon-format/toon": "^4.1.0",
95
97
  "citty": "^0.2.2",
96
98
  "fastest-levenshtein": "^1.0.16",
97
99
  "picocolors": "^1.1.1",
@@ -0,0 +1,18 @@
1
+ import { log } from "../log";
2
+
3
+ export type ActionMessage = { level: "success" | "info" | "warn"; text: string };
4
+
5
+ export type ActionResult =
6
+ | { ok: true; messages: ActionMessage[]; stdout?: string }
7
+ | { ok: false; error: string; hint?: string };
8
+
9
+ /** Renders an action's messages, writing `stdout` verbatim; a rejected action exits 2. */
10
+ export function emitActionResult(result: ActionResult): void {
11
+ if (!result.ok) {
12
+ log.error(result.error);
13
+ if (result.hint) log.warn(result.hint);
14
+ process.exit(2);
15
+ }
16
+ for (const message of result.messages) log[message.level](message.text);
17
+ if (result.stdout !== undefined) process.stdout.write(result.stdout);
18
+ }
@@ -0,0 +1,86 @@
1
+ import { createDiagnosticLogSession } from "../diagnostic-log";
2
+ import type {
3
+ DiagnosticLogFields,
4
+ DiagnosticLogSession,
5
+ DiagnosticSessionStatus,
6
+ } from "../diagnostic-log";
7
+ import { errorMessage } from "../error-utils";
8
+ import { log } from "../log";
9
+ import { createStatsRecorder } from "../stats";
10
+ import type { StatsCommandName, StatsRecorder } from "../stats";
11
+
12
+ export interface CommandSession {
13
+ readonly stats: StatsRecorder;
14
+ readonly diagnosticLog: DiagnosticLogSession;
15
+ }
16
+
17
+ export interface CommandOutcome {
18
+ status: DiagnosticSessionStatus;
19
+ itemCount: number;
20
+ /** Merged into the `command.finish` event alongside `command` and `status`. */
21
+ finishFields?: DiagnosticLogFields;
22
+ /** Exit code the caller should terminate with; omitted means "don't exit". */
23
+ exitCode?: number;
24
+ }
25
+
26
+ export interface CommandSessionFactories {
27
+ createStats: (command: StatsCommandName) => StatsRecorder;
28
+ createDiagnosticLog: () => DiagnosticLogSession;
29
+ }
30
+
31
+ const defaultFactories: CommandSessionFactories = {
32
+ createStats: createStatsRecorder,
33
+ createDiagnosticLog: createDiagnosticLogSession,
34
+ };
35
+
36
+ /** Runs one CLI command's body between `command.start` / `command.finish`, closing both recorders exactly once; `body` reports failure by returning a {@link CommandOutcome}, never by exiting. */
37
+ export async function runCommandSession(
38
+ command: StatsCommandName,
39
+ startFields: DiagnosticLogFields,
40
+ body: (session: CommandSession) => Promise<CommandOutcome>,
41
+ factories: CommandSessionFactories = defaultFactories,
42
+ ): Promise<CommandOutcome> {
43
+ const session: CommandSession = {
44
+ stats: factories.createStats(command),
45
+ diagnosticLog: factories.createDiagnosticLog(),
46
+ };
47
+ session.diagnosticLog.event("command.start", { command, ...startFields });
48
+
49
+ let outcome: CommandOutcome;
50
+ try {
51
+ outcome = await body(session);
52
+ } catch (err) {
53
+ closeSessionQuietly(session, command, { status: "failed", itemCount: 0 });
54
+ throw err;
55
+ }
56
+
57
+ // A flush error must not displace a failure the command already has to report (Greptile P1/P2 on #607).
58
+ if (outcome.status === "failed") {
59
+ closeSessionQuietly(session, command, outcome);
60
+ } else {
61
+ closeSession(session, command, outcome);
62
+ }
63
+ return outcome;
64
+ }
65
+
66
+ function closeSessionQuietly(
67
+ session: CommandSession,
68
+ command: StatsCommandName,
69
+ outcome: CommandOutcome,
70
+ ): void {
71
+ try {
72
+ closeSession(session, command, outcome);
73
+ } catch (err) {
74
+ log.debug(`command session cleanup failed: ${errorMessage(err)}`);
75
+ }
76
+ }
77
+
78
+ function closeSession(session: CommandSession, command: StatsCommandName, outcome: CommandOutcome): void {
79
+ session.diagnosticLog.event("command.finish", {
80
+ command,
81
+ status: outcome.status,
82
+ ...outcome.finishFields,
83
+ });
84
+ session.stats.finish(outcome.status, outcome.itemCount);
85
+ session.diagnosticLog.finish(outcome.status);
86
+ }
@@ -0,0 +1,108 @@
1
+ import { log, setColorEnabled } from "../log";
2
+
3
+ /** The global flags resolved before citty, so every command sees the same decision. */
4
+ export interface CliContext {
5
+ quiet: boolean;
6
+ disableColor: boolean;
7
+ }
8
+
9
+ export interface ResolvedCliContext extends CliContext {
10
+ /** `rawArgs` with the global flag tokens removed, ready to hand to citty. */
11
+ rawArgs: string[];
12
+ }
13
+
14
+ // Falsey grammar shared by --no-color, --quiet, and CI. Mirrors KESHA_DEBUG so
15
+ // `CI=false`/`CI=0` and `--no-color=false` opt back in.
16
+ const FALSEY_VALUES = new Set(["", "0", "false", "no", "off"]);
17
+ function isFalsey(v: string): boolean {
18
+ return FALSEY_VALUES.has(v.trim().toLowerCase());
19
+ }
20
+
21
+ /** True when NO_COLOR came from the user's environment, so re-enabling colors must not clear it. */
22
+ export const USER_FORCED_NO_COLOR =
23
+ process.env.NO_COLOR !== undefined && !isFalsey(process.env.NO_COLOR);
24
+
25
+ /**
26
+ * Strip a boolean global flag out of rawArgs so citty never sees it. Matches
27
+ * any of `names` bare (sets the flag) and the `--name=<value>` form for the
28
+ * long names (`<falsey>` explicitly turns the flag back off).
29
+ */
30
+ function stripBooleanFlag(
31
+ rawArgs: string[],
32
+ names: string[],
33
+ ): { value: boolean; rawArgs: string[] } {
34
+ const valuedPrefixes = names.filter((n) => n.startsWith("--")).map((n) => `${n}=`);
35
+ let value = false;
36
+ const cleaned: string[] = [];
37
+ for (const arg of rawArgs) {
38
+ if (names.includes(arg)) {
39
+ value = true;
40
+ } else if (valuedPrefixes.some((prefix) => arg.startsWith(prefix))) {
41
+ value = !isFalsey(arg.slice(arg.indexOf("=") + 1));
42
+ } else {
43
+ cleaned.push(arg);
44
+ }
45
+ }
46
+ return { value, rawArgs: cleaned };
47
+ }
48
+
49
+ /**
50
+ * Decide whether ANSI colors should be disabled (#531) and return rawArgs with
51
+ * any `--no-color[=value]` token stripped so citty never sees it.
52
+ *
53
+ * Colors are disabled when `--no-color` (bare) or `--no-color=<truthy>` is
54
+ * passed, or when the environment looks like CI (`CI` set to a non-falsey value
55
+ * — GitHub Actions, GitLab, CircleCI, … export `CI=true`). `--no-color=false`
56
+ * and `CI=false`/`CI=0` explicitly opt back in.
57
+ */
58
+ export function resolveColorMode(
59
+ rawArgs: string[],
60
+ env: { CI?: string } = process.env as { CI?: string },
61
+ ): { disableColor: boolean; rawArgs: string[] } {
62
+ const { value, rawArgs: cleaned } = stripBooleanFlag(rawArgs, ["--no-color"]);
63
+ const ci = env.CI !== undefined && !isFalsey(env.CI);
64
+ return { disableColor: value || ci, rawArgs: cleaned };
65
+ }
66
+
67
+ /**
68
+ * Detect `--quiet`/`-q` (and `--quiet=<value>`) and return rawArgs with the
69
+ * token stripped (#526). Resolved before citty — like `--no-color` — so quiet
70
+ * is global: it works for every command, not just the transcribe path, and a
71
+ * subcommand that doesn't declare it never sees the flag.
72
+ */
73
+ export function resolveQuietMode(rawArgs: string[]): { quiet: boolean; rawArgs: string[] } {
74
+ const { value, rawArgs: cleaned } = stripBooleanFlag(rawArgs, ["--quiet", "-q"]);
75
+ return { quiet: value, rawArgs: cleaned };
76
+ }
77
+
78
+ /** Resolves every pre-citty global flag into one value, with the tokens stripped from rawArgs. */
79
+ export function resolveCliContext(
80
+ rawArgs: string[],
81
+ env: { CI?: string } = process.env as { CI?: string },
82
+ ): ResolvedCliContext {
83
+ const color = resolveColorMode(rawArgs, env);
84
+ const quiet = resolveQuietMode(color.rawArgs);
85
+ return { quiet: quiet.quiet, disableColor: color.disableColor, rawArgs: quiet.rawArgs };
86
+ }
87
+
88
+ /**
89
+ * Sync NO_COLOR with the resolved color decision so engine subprocesses inherit
90
+ * the right value. Separated from setColorEnabled so it can be tested without
91
+ * picocolors side-effects.
92
+ *
93
+ * Never clears a NO_COLOR the user exported before this process started.
94
+ */
95
+ export function applyColorEnv(disableColor: boolean): void {
96
+ if (disableColor) {
97
+ process.env.NO_COLOR = "1";
98
+ } else if (!USER_FORCED_NO_COLOR) {
99
+ delete process.env.NO_COLOR;
100
+ }
101
+ }
102
+
103
+ /** Applies the context to the process-wide output sinks, writing every value so an earlier in-process run (unit tests, `kesha mcp`) can't leak its settings into a later one. */
104
+ export function applyCliContext(context: CliContext): void {
105
+ setColorEnabled(!context.disableColor);
106
+ applyColorEnv(context.disableColor);
107
+ log.quietEnabled = context.quiet;
108
+ }