@drakulavich/kesha-voice-kit 1.19.0-beta.1 → 1.19.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
@@ -96,10 +96,10 @@ Each segment gets a `speaker` integer (cluster ID, stable within one file). Linu
96
96
 
97
97
  ## Text-to-speech
98
98
 
99
- Kesha speaks back via Kokoro-82M (English) and Vosk-TTS (Russian) — voice auto-picks from the text's language. On darwin-arm64, Kokoro uses FluidAudio CoreML instead of ONNX:
99
+ Kesha speaks back via Kokoro-82M (English) and Vosk-TTS (Russian) — voice auto-picks from the text's language. On darwin-arm64, Kokoro uses FluidAudio CoreML instead of ONNX; FluidAudio stores that CoreML cache at `~/.cache/fluidaudio/Models/kokoro`, outside Kesha's pinned model cache:
100
100
 
101
101
  ```bash
102
- kesha install --tts # ~990MB (Kokoro + Vosk-TTS RU, opt-in)
102
+ kesha install --tts # TTS, opt-in; Darwin Kokoro uses FluidAudio cache
103
103
  kesha say "Hello, world" > hello.wav
104
104
  kesha say "Привет, мир" > privet.wav # auto-routes (Milena on darwin, ru-vosk-m02 elsewhere)
105
105
  ```
@@ -288,7 +288,7 @@ are missing.
288
288
  | SpeechBrain ECAPA-TDNN | Audio language detection | ~86MB | [HuggingFace](https://huggingface.co/speechbrain/lang-id-voxlingua107-ecapa) |
289
289
  | Apple NLLanguageRecognizer | Text language detection | built-in | macOS system framework |
290
290
  | Silero VAD v5 (opt-in) | Voice activity detection | ~2.3MB | [snakers4/silero-vad](https://github.com/snakers4/silero-vad) |
291
- | Kokoro-82M / Vosk-TTS (opt-in) | Text-to-speech | ~990MB | [FluidAudio Kokoro](https://github.com/FluidInference/FluidAudio) on darwin-arm64; ONNX Kokoro elsewhere · [Vosk-TTS](https://github.com/alphacep/vosk-tts) |
291
+ | Kokoro-82M / Vosk-TTS (opt-in) | Text-to-speech | ~990MB | [FluidAudio Kokoro](https://github.com/FluidInference/FluidAudio) on darwin-arm64 (FluidAudio cache, not Kesha-verified); ONNX Kokoro elsewhere · [Vosk-TTS](https://github.com/alphacep/vosk-tts) |
292
292
 
293
293
  All models run through `kesha-engine` — a Rust binary using [FluidAudio](https://github.com/FluidInference/FluidAudio) (CoreML) on Apple Silicon and [ort](https://github.com/pykeio/ort) (ONNX Runtime) on other platforms.
294
294
 
@@ -321,9 +321,23 @@ Kesha can collect local diagnostics without downloading models or mutating cache
321
321
  ```bash
322
322
  kesha doctor --json --redact
323
323
  kesha support-bundle --output kesha-support.tar.gz
324
+ kesha support-bundle --include-logs --output kesha-support-with-logs.tar.gz
325
+ kesha logs status
326
+ kesha logs status --json
324
327
  ```
325
328
 
326
329
  `support-bundle` creates a redacted `.tar.gz` archive for GitHub issues. It includes runtime, engine, cache, optional-component, Stats status, and known Kesha environment settings. It does not include audio, transcripts, model files, or the Stats database.
330
+ Diagnostic log contents are excluded by default; pass `--include-logs` to add a
331
+ bounded tail of Kesha's privacy-safe NDJSON diagnostic log.
332
+
333
+ `kesha logs` manages local, rotated diagnostic logs for troubleshooting. Logs
334
+ default to `retain-on-failure` and also support explicit `off` and `on` modes.
335
+ Use `kesha logs status --json` for a stable machine-readable status contract
336
+ covering the active path, mode, size, rotation settings, and rotated-file list.
337
+ They use content-free NDJSON events: command/stage names, versions, durations,
338
+ exit codes, and coarse audio metadata only. They must not store audio,
339
+ transcripts, input text, generated speech text, file names, full paths, raw
340
+ stdout/stderr, environment variables, tokens, or URLs. See [Diagnostic logs](docs/diagnostic-logs.md).
327
341
 
328
342
  ## Local Stats privacy and lifecycle
329
343
 
package/SKILL.md CHANGED
@@ -25,6 +25,7 @@ Local voice toolkit: transcribe voice messages to text, synthesize speech, detec
25
25
  - **Need to send a voice note (Telegram, WhatsApp, Signal, Discord)**: synthesize directly into messenger-native OGG/Opus with `kesha say --format ogg-opus --out reply.ogg "<text>"`. Default is mono 24 kHz @ 32 kbps - what Telegram `sendVoice` expects. No WAV redirect and no `ffmpeg` round-trip.
26
26
  - **Need local file playback/debug output**: WAV is still available with `kesha say --out reply.wav "<text>"`, but do not use WAV for Telegram voice replies. Auto-routes by detected language (Kokoro-82M for English, Vosk-TTS for Russian). On darwin-arm64, English Kokoro uses FluidAudio CoreML instead of ONNX. For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
27
27
  - **Need to detect what language a file is in** before choosing a pipeline: `kesha --json audio.ogg` returns both audio-based and text-based language detection with confidence scores.
28
+ - **Need to capture your own voice** for transcription or as a voice-note source: `kesha record --out clip.wav` records up to 120s (override with `--max-seconds`) of mono 16 kHz WAV from the default microphone. Pipe straight into `kesha --json clip.wav` to close the loop.
28
29
 
29
30
  ## OpenClaw plugin setup
30
31
 
@@ -150,9 +151,12 @@ Each `segment.speaker` is a number (cluster id, stable within one file). On Linu
150
151
  - `kesha audio.ogg` — plain transcript on stdout
151
152
  - `kesha --format transcript audio.ogg` — transcript + `[lang: ru, confidence: 0.99]` footer
152
153
  - `kesha --json --timestamps audio.ogg` — JSON with timestamped `segments`
154
+ - `kesha --toon audio.ogg` — TOON (compact, LLM-friendly JSON encoding); preferred when piping multi-file results to an LLM/agent
153
155
  - `kesha --verbose audio.ogg` — human-readable with language info
154
156
  - `kesha --lang en audio.ogg` — warn if detected language differs (useful sanity check)
155
157
 
158
+ **Long audio:** files ≥ 120 s auto-engage Silero VAD chunking; force on with `--vad` or off with `--no-vad`. Short files use full-file ASR by default.
159
+
156
160
  ## TTS: synthesize speech
157
161
 
158
162
  ```bash
@@ -188,12 +192,25 @@ Format is also inferred from `--out` extension (`.ogg` / `.opus` / `.oga` → OG
188
192
 
189
193
  ## Install
190
194
 
195
+ **Humans use `kesha init` (guided). Agents and scripts use `kesha install` (deterministic).**
196
+
191
197
  ```bash
192
- bun add -g @drakulavich/kesha-voice-kit # global CLI install
193
- kesha install # downloads engine (~350 MB)
194
- kesha install --tts # adds Kokoro + Vosk-TTS RU (~990 MB more, for TTS)
198
+ bun add -g @drakulavich/kesha-voice-kit # global CLI install (always first)
199
+
200
+ # For humans: interactive setup that prompts for backend / TTS / VAD / diarize
201
+ kesha init
202
+
203
+ # For agents and CI: explicit, scriptable install commands
204
+ kesha install # engine only (~350 MB)
205
+ kesha install --tts # + Kokoro + Vosk-TTS RU (~990 MB more)
206
+ kesha install --tts --vad # + Silero VAD (long-audio chunking)
207
+ kesha install --tts --vad --diarize # + speaker diarization (darwin-arm64 only)
195
208
  ```
196
209
 
210
+ Kesha's runtime error/warning messages adapt to the same split: when `kesha` is invoked from a TTY, hints suggest `kesha init`; when stderr is piped (CI logs, OpenClaw, agent subprocess), hints suggest the equivalent `kesha install [...flags]`. Both run the same install code under the hood — pick the one your caller is.
211
+
212
+ For pre-release builds: `bun add -g @drakulavich/kesha-voice-kit@beta` (current `beta` channel; `@latest` stays on the last stable release).
213
+
197
214
  No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled inside Vosk-TTS. `macos-*` voices need no install either — they use voices already on the Mac.
198
215
 
199
216
  ## Supported languages
@@ -207,6 +224,13 @@ No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled
207
224
  - ASR: ~19× faster than OpenAI Whisper on Apple Silicon (CoreML via FluidAudio), ~2.5× on CPU (ONNX via `ort`).
208
225
  - TTS: sub-second latency for short utterances on Apple Silicon.
209
226
 
227
+ ## Troubleshooting
228
+
229
+ - `kesha doctor` — collect support diagnostics without changing local state. Add `--json` for machine-readable output, `--redact` to scrub secrets and home paths before sharing.
230
+ - `kesha logs` — manage privacy-safe local diagnostic logs. Default mode is `retain-on-failure` (events buffered in memory, flushed to disk only when a command fails). `kesha logs mode on` captures every run, `kesha logs path` prints the NDJSON file, `kesha logs disable` turns it off entirely.
231
+ - `kesha support-bundle --output bundle.tar.gz` — produce a redacted `.tar.gz` for filing an issue. Add `--include-logs` to bundle a bounded tail of diagnostic logs.
232
+ - `kesha stats` — manage local anonymous performance stats (per-command latency percentiles). Actions: `enable | disable | status | week | errors | export | reset | vacuum | retention`. Stays on the machine.
233
+
210
234
  ## Why local
211
235
 
212
236
  No API keys to manage. No per-minute billing. Voice data never leaves the machine — important for regulated industries, personal messaging, and anything that shouldn't be in a third-party log.
@@ -6,7 +6,7 @@ _kesha_completion() {
6
6
  COMPREPLY=()
7
7
  cur="${COMP_WORDS[COMP_CWORD]}"
8
8
  command="${COMP_WORDS[1]}"
9
- commands="completions doctor init install manpage record say stats status support-bundle"
9
+ commands="completions doctor init install logs manpage record say stats status support-bundle"
10
10
 
11
11
  if [[ "$COMP_CWORD" -eq 1 ]]; then
12
12
  if [[ "$cur" == -* ]]; then
@@ -22,12 +22,13 @@ _kesha_completion() {
22
22
  doctor) opts="--help -h --json --redact" ;;
23
23
  init) opts="--help -h --coreml --onnx --no-cache --plan --yes --tts --vad --diarize" ;;
24
24
  install) opts="--help -h --coreml --onnx --no-cache --plan --tts --vad --diarize" ;;
25
+ logs) opts="--help -h --json" ;;
25
26
  manpage) opts="--help -h" ;;
26
27
  record) opts="--help -h --out --max-seconds --debug" ;;
27
28
  say) opts="--help -h --voice --lang --out --rate --list-voices --ssml --format --bitrate --sample-rate --no-expand-abbrev --verbose --debug" ;;
28
29
  stats) opts="--help -h --format" ;;
29
30
  status) opts="--help -h --disk" ;;
30
- support-bundle) opts="--help -h --output" ;;
31
+ support-bundle) opts="--help -h --output --include-logs" ;;
31
32
  *) opts="--help -h --version -v --json --toon --timestamps --speakers --include-errors --verbose --format --lang --debug --vad --no-vad" ;;
32
33
  esac
33
34
 
@@ -4,6 +4,7 @@ complete -c kesha -n '__fish_use_subcommand' -a 'completions' -d 'Print shell co
4
4
  complete -c kesha -n '__fish_use_subcommand' -a 'doctor' -d 'Collect support diagnostics without changing local state'
5
5
  complete -c kesha -n '__fish_use_subcommand' -a 'init' -d 'Interactive setup guide for Kesha features'
6
6
  complete -c kesha -n '__fish_use_subcommand' -a 'install' -d 'Download inference engine and models'
7
+ complete -c kesha -n '__fish_use_subcommand' -a 'logs' -d 'Manage local privacy-safe diagnostic logs'
7
8
  complete -c kesha -n '__fish_use_subcommand' -a 'manpage' -d 'Print the kesha(1) manpage'
8
9
  complete -c kesha -n '__fish_use_subcommand' -a 'record' -d 'Record microphone audio to a WAV file'
9
10
  complete -c kesha -n '__fish_use_subcommand' -a 'say' -d 'Synthesize speech from text (TTS). Writes audio to stdout (or --out file). Defaults to WAV; use --format ogg-opus for messenger-ready voice notes.'
@@ -50,6 +51,9 @@ complete -c kesha -n '__fish_seen_subcommand_from install' -l plan -d 'Show down
50
51
  complete -c kesha -n '__fish_seen_subcommand_from install' -l tts -d 'Also install TTS models (Kokoro EN + Vosk-TTS RU, ~990MB)'
51
52
  complete -c kesha -n '__fish_seen_subcommand_from install' -l vad -d 'Also install Silero VAD (~2.3MB) for long-audio preprocessing'
52
53
  complete -c kesha -n '__fish_seen_subcommand_from install' -l diarize -d 'Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)'
54
+ complete -c kesha -n '__fish_seen_subcommand_from logs' -l help -d 'Show help'
55
+ complete -c kesha -n '__fish_seen_subcommand_from logs' -s h -d 'Show help'
56
+ complete -c kesha -n '__fish_seen_subcommand_from logs' -l json -d 'Output status as JSON'
53
57
  complete -c kesha -n '__fish_seen_subcommand_from manpage' -l help -d 'Show help'
54
58
  complete -c kesha -n '__fish_seen_subcommand_from manpage' -s h -d 'Show help'
55
59
  complete -c kesha -n '__fish_seen_subcommand_from record' -l help -d 'Show help'
@@ -80,3 +84,4 @@ complete -c kesha -n '__fish_seen_subcommand_from status' -l disk -d 'Include re
80
84
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l help -d 'Show help'
81
85
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -s h -d 'Show help'
82
86
  complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l output -r -d 'Write archive to this .tar.gz path'
87
+ complete -c kesha -n '__fish_seen_subcommand_from support-bundle' -l include-logs -d 'Include a bounded tail of privacy-safe diagnostic logs'
@@ -8,6 +8,7 @@ _kesha() {
8
8
  'doctor:Collect support diagnostics without changing local state'
9
9
  'init:Interactive setup guide for Kesha features'
10
10
  'install:Download inference engine and models'
11
+ 'logs:Manage local privacy-safe diagnostic logs'
11
12
  'manpage:Print the kesha(1) manpage'
12
13
  'record:Record microphone audio to a WAV file'
13
14
  'say:Synthesize speech from text (TTS). Writes audio to stdout (or --out file). Defaults to WAV; use --format ogg-opus for messenger-ready voice notes.'
@@ -73,6 +74,11 @@ _kesha() {
73
74
  '--vad[Also install Silero VAD (~2.3MB) for long-audio preprocessing]' \
74
75
  '--diarize[Also install the Sortformer streaming-diarization model (~245MB, darwin-arm64 only — #199)]'
75
76
  ;;
77
+ logs)
78
+ _arguments '--help[Show help]' \
79
+ '-h[Show help]' \
80
+ '--json[Output status as JSON]'
81
+ ;;
76
82
  manpage)
77
83
  _arguments '--help[Show help]' \
78
84
  '-h[Show help]'
@@ -113,7 +119,8 @@ _kesha() {
113
119
  support-bundle)
114
120
  _arguments '--help[Show help]' \
115
121
  '-h[Show help]' \
116
- '--output=[Write archive to this .tar.gz path]:output:'
122
+ '--output=[Write archive to this .tar.gz path]:output:' \
123
+ '--include-logs[Include a bounded tail of privacy-safe diagnostic logs]'
117
124
  ;;
118
125
  *)
119
126
  _arguments '--help[Show help]' \
@@ -0,0 +1,93 @@
1
+ # Diagnostic Logs
2
+
3
+ Kesha diagnostic logs are local, rotated NDJSON event logs for install, engine,
4
+ and runtime troubleshooting. They are separate from Kesha Stats: Stats stores
5
+ aggregate timings in SQLite after explicit opt-in, while diagnostic logs keep a
6
+ small local ring buffer of structured events.
7
+
8
+ Diagnostic logs default to `retain-on-failure`: passing commands leave no log
9
+ file, while failed commands can preserve a small local trace for debugging.
10
+
11
+ ```bash
12
+ kesha logs status
13
+ kesha logs status --json
14
+ kesha logs enable
15
+ kesha logs disable
16
+ kesha logs mode retain-on-failure
17
+ kesha logs path
18
+ kesha logs reset
19
+ ```
20
+
21
+ ## Modes
22
+
23
+ Diagnostic logs follow a Playwright-style retention model:
24
+
25
+ - `off`: do not record diagnostic events.
26
+ - `on`: append events to the rotated local log immediately.
27
+ - `retain-on-failure`: buffer events for the current command and write them only
28
+ if the command fails. This is the default.
29
+
30
+ `kesha logs enable` is shorthand for `kesha logs mode on`; `kesha logs disable`
31
+ is shorthand for `kesha logs mode off`. `retain-on-failure` keeps passing runs
32
+ artifact-free while failed runs keep enough context to debug.
33
+
34
+ `kesha logs status --json` prints the same local status as a stable JSON object:
35
+ `dir`, `activePath`, `statePath`, `exists`, `activeSizeBytes`, `rotatedFiles`,
36
+ `totalSizeBytes`, `mode`, `maxBytes`, and `retain`.
37
+
38
+ ## Privacy Contract
39
+
40
+ Diagnostic logs are content-free. Event payloads are built from an allowlist of
41
+ typed fields such as command name, app version, platform, backend, feature flags,
42
+ stage names, exit codes, duration numbers, and stable error codes.
43
+
44
+ When enabled, `kesha install`, `kesha <audio>`, and `kesha say` record command
45
+ lifecycle events such as `command.start`, `input.audio`, `input.missing`,
46
+ `engine.exit`, and `command.finish`. These events use only counts, booleans,
47
+ format extensions, duration milliseconds, and bucket labels. In the default
48
+ `retain-on-failure` mode, successful runs still leave no log file behind.
49
+
50
+ Diagnostic logs must not store:
51
+
52
+ - audio bytes
53
+ - transcripts
54
+ - input text or generated speech text
55
+ - file names, basenames, or full file paths
56
+ - raw stdout or stderr
57
+ - environment variables
58
+ - URLs, tokens, secrets, API keys, or cloud identifiers
59
+ - model files or model contents
60
+
61
+ For audio shape, logs should use coarse metadata such as extension, duration
62
+ bucket, size bucket, sample-rate bucket, or channel count. Do not log a path or
63
+ basename even when it looks harmless; names like `therapy-session.m4a` can be
64
+ private.
65
+
66
+ ## Storage
67
+
68
+ Default paths:
69
+
70
+ - macOS: `~/Library/Logs/kesha/kesha.ndjson`
71
+ - Linux: `${XDG_STATE_HOME:-~/.local/state}/kesha/logs/kesha.ndjson`
72
+ - Windows: `%LOCALAPPDATA%\kesha\logs\kesha.ndjson`
73
+
74
+ Set `KESHA_LOG_DIR` to override the directory. The active file is
75
+ `kesha.ndjson`; rotated files are named `kesha.1.ndjson`, `kesha.2.ndjson`, and
76
+ so on.
77
+
78
+ The first implementation rotates at 10 MB and keeps 5 rotated files. `kesha logs
79
+ reset` deletes Kesha log files but preserves the selected mode.
80
+
81
+ ## Support Bundles
82
+
83
+ `kesha support-bundle` does not include diagnostic log contents by default. Use
84
+ `kesha support-bundle --include-logs` to add a bounded tail of the active
85
+ already-sanitized NDJSON log when a support issue needs recent command events.
86
+ Recommended flow for a bug report:
87
+
88
+ ```bash
89
+ kesha logs status
90
+ kesha logs mode retain-on-failure
91
+ # reproduce the failure
92
+ kesha support-bundle --include-logs --output kesha-support-with-logs.tar.gz
93
+ ```
package/man/kesha.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH KESHA 1 "May 2026" "kesha 1.19.0-beta.1" "User Commands"
1
+ .TH KESHA 1 "May 2026" "kesha 1.19.0" "User Commands"
2
2
  .SH NAME
3
3
  kesha \- open-source local voice toolkit
4
4
  .SH SYNOPSIS
@@ -23,6 +23,9 @@ Interactive setup guide for Kesha features
23
23
  .B install
24
24
  Download inference engine and models
25
25
  .TP
26
+ .B logs
27
+ Manage local privacy\-safe diagnostic logs
28
+ .TP
26
29
  .B manpage
27
30
  Print the kesha(1) manpage
28
31
  .TP
@@ -129,6 +132,10 @@ Also install Silero VAD (~2.3MB) for long\-audio preprocessing
129
132
  .TP
130
133
  .B \-\-diarize
131
134
  Also install the Sortformer streaming\-diarization model (~245MB, darwin\-arm64 only — #199)
135
+ .SS logs
136
+ .TP
137
+ .B \-\-json
138
+ Output status as JSON
132
139
  .SS record
133
140
  .TP
134
141
  .B \-\-out
@@ -188,6 +195,9 @@ Include recursive cache disk usage
188
195
  .TP
189
196
  .B \-\-output
190
197
  Write archive to this .tar.gz path
198
+ .TP
199
+ .B \-\-include\-logs
200
+ Include a bounded tail of privacy\-safe diagnostic logs
191
201
  .SH FILES
192
202
  .TP
193
203
  .B completions/kesha.bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.19.0-beta.1",
3
+ "version": "1.19.0",
4
4
  "description": "Give your local tools a voice: fast STT in 25 languages (~19x faster than Whisper on Apple Silicon, ONNX CPU fallback), TTS via Kokoro/Vosk/macOS voices, VAD + 107-language detection. Rust engine, Bun CLI, OpenClaw skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,9 +19,9 @@
19
19
  "tsconfig.json",
20
20
  "openclaw.plugin.json",
21
21
  "openclaw-plugin.cjs",
22
+ "docs/diagnostic-logs.md",
22
23
  "docs/product-positioning.md",
23
24
  "SKILL.md",
24
- "scripts/postinstall.cjs",
25
25
  "LICENSE",
26
26
  "NOTICES.md",
27
27
  "README.md"
@@ -32,7 +32,7 @@
32
32
  ]
33
33
  },
34
34
  "keshaEngine": {
35
- "version": "1.19.0-beta.1"
35
+ "version": "1.19.0"
36
36
  },
37
37
  "scripts": {
38
38
  "test": "bun test",
@@ -49,8 +49,7 @@
49
49
  "check": "bun run lint && bun run check:versions && bun run test:cli-fast",
50
50
  "check:workflows": "bun .github/scripts/check-workflows.ts",
51
51
  "check:release-manifest": "bun .github/scripts/release-manifest.mjs --check",
52
- "check:versions": "bun .github/scripts/check-versions.ts",
53
- "postinstall": "node scripts/postinstall.cjs"
52
+ "check:versions": "bun .github/scripts/check-versions.ts"
54
53
  },
55
54
  "keywords": [
56
55
  "asr",
@@ -1,4 +1,4 @@
1
- import { runMain } from "citty";
1
+ import { runMain, type CommandDef } from "citty";
2
2
  import { existsSync } from "fs";
3
3
  import { log } from "../log";
4
4
  import { suggestCommand } from "../suggest-command";
@@ -6,6 +6,7 @@ import { completionsCommand } from "./completions";
6
6
  import { doctorCommand } from "./doctor";
7
7
  import { initCommand } from "./init";
8
8
  import { installCommand } from "./install";
9
+ import { logsCommand } from "./logs";
9
10
  import { manpageCommand } from "./manpage";
10
11
  import { recordCommand } from "./record";
11
12
  import { sayCommand } from "./say";
@@ -14,18 +15,23 @@ import { statusCommand } from "./status";
14
15
  import { supportBundleCommand } from "./support-bundle";
15
16
  import { mainCommand } from "./main";
16
17
 
17
- const SUBCOMMANDS = [
18
- "doctor",
19
- "init",
20
- "install",
21
- "status",
22
- "record",
23
- "say",
24
- "stats",
25
- "support-bundle",
26
- "completions",
27
- "manpage",
28
- ];
18
+ // Single source of truth: keyed lookup also feeds the `did you mean` suggester.
19
+ // `CommandDef<any>` is intentional — citty's generic is invariant in the args
20
+ // shape, and each subcommand has its own arg schema; the value here is only
21
+ // passed back to `runMain`, which re-reads the schema from the def itself.
22
+ const SUBCOMMANDS: Record<string, CommandDef<any>> = {
23
+ doctor: doctorCommand,
24
+ init: initCommand,
25
+ install: installCommand,
26
+ logs: logsCommand,
27
+ status: statusCommand,
28
+ record: recordCommand,
29
+ say: sayCommand,
30
+ stats: statsCommand,
31
+ "support-bundle": supportBundleCommand,
32
+ completions: completionsCommand,
33
+ manpage: manpageCommand,
34
+ };
29
35
 
30
36
  function isPathLike(arg: string): boolean {
31
37
  return arg.includes(".") || arg.includes("/") || existsSync(arg);
@@ -34,53 +40,8 @@ function isPathLike(arg: string): boolean {
34
40
  export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
35
41
  const [firstArg, ...restArgs] = rawArgs;
36
42
 
37
- if (firstArg === "doctor") {
38
- await runMain(doctorCommand, { rawArgs: restArgs });
39
- return;
40
- }
41
-
42
- if (firstArg === "init") {
43
- await runMain(initCommand, { rawArgs: restArgs });
44
- return;
45
- }
46
-
47
- if (firstArg === "completions") {
48
- await runMain(completionsCommand, { rawArgs: restArgs });
49
- return;
50
- }
51
-
52
- if (firstArg === "install") {
53
- await runMain(installCommand, { rawArgs: restArgs });
54
- return;
55
- }
56
-
57
- if (firstArg === "manpage") {
58
- await runMain(manpageCommand, { rawArgs: restArgs });
59
- return;
60
- }
61
-
62
- if (firstArg === "status") {
63
- await runMain(statusCommand, { rawArgs: restArgs });
64
- return;
65
- }
66
-
67
- if (firstArg === "record") {
68
- await runMain(recordCommand, { rawArgs: restArgs });
69
- return;
70
- }
71
-
72
- if (firstArg === "say") {
73
- await runMain(sayCommand, { rawArgs: restArgs });
74
- return;
75
- }
76
-
77
- if (firstArg === "stats") {
78
- await runMain(statsCommand, { rawArgs: restArgs });
79
- return;
80
- }
81
-
82
- if (firstArg === "support-bundle") {
83
- await runMain(supportBundleCommand, { rawArgs: restArgs });
43
+ if (firstArg && Object.hasOwn(SUBCOMMANDS, firstArg)) {
44
+ await runMain(SUBCOMMANDS[firstArg], { rawArgs: restArgs });
84
45
  return;
85
46
  }
86
47
 
@@ -88,7 +49,7 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
88
49
  // Extensionless existing files remain valid transcription inputs; missing
89
50
  // bare tokens are more likely command typos and should not start the engine.
90
51
  if (firstArg && !firstArg.startsWith("-") && !isPathLike(firstArg)) {
91
- const suggestion = suggestCommand(firstArg, SUBCOMMANDS);
52
+ const suggestion = suggestCommand(firstArg, Object.keys(SUBCOMMANDS));
92
53
  log.error(`unknown command '${firstArg}'`);
93
54
  if (suggestion && suggestion !== firstArg) {
94
55
  log.warn(`(Did you mean ${suggestion}?)`);
@@ -5,6 +5,7 @@ import { renderInstallPlan } from "../install-plan";
5
5
  import { maybeAskForStar } from "../star";
6
6
  import { log } from "../log";
7
7
  import { packageVersion } from "../package-info";
8
+ import { createDiagnosticLogSession, type DiagnosticLogSession } from "../diagnostic-log";
8
9
 
9
10
  export interface InstallCommandArgs {
10
11
  coreml: boolean;
@@ -46,6 +47,28 @@ function defaultBackendForPlatform(): string | undefined {
46
47
  return undefined;
47
48
  }
48
49
 
50
+ type InstallDiagnosticErrorKind = "validation_failed" | "install_failed";
51
+
52
+ function finishInstallDiagnostic(
53
+ diagnosticLog: DiagnosticLogSession | null,
54
+ startedAt: number,
55
+ status: "success" | "failed",
56
+ errorKind?: InstallDiagnosticErrorKind,
57
+ ): void {
58
+ if (!diagnosticLog) return;
59
+ try {
60
+ diagnosticLog.event("command.finish", {
61
+ command: "install",
62
+ status,
63
+ durationMs: Math.round(performance.now() - startedAt),
64
+ ...(errorKind ? { errorKind } : {}),
65
+ });
66
+ diagnosticLog.finish(status);
67
+ } catch (err) {
68
+ log.debug(`install diagnostic log finish dropped: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ }
71
+
49
72
  export async function performInstall(
50
73
  noCache: boolean,
51
74
  backend?: string,
@@ -58,26 +81,42 @@ export async function performInstall(
58
81
  log.info(await renderInstallPlan({ noCache, backend, tts, vad, diarize }));
59
82
  return;
60
83
  }
61
- if (diarize && !(process.platform === "darwin" && process.arch === "arm64")) {
62
- log.error(
63
- "--diarize is currently darwin-arm64 only " +
64
- "(see https://github.com/drakulavich/kesha-voice-kit/issues/199).",
65
- );
66
- process.exit(1);
67
- }
68
- const platformBackend = defaultBackendForPlatform();
69
- if (backend && !process.env.KESHA_ENGINE_BIN && platformBackend && backend !== platformBackend) {
70
- log.error(
71
- `Requested backend "${backend}" is not available on this platform; ` +
72
- `the release engine uses "${platformBackend}".`,
73
- );
74
- process.exit(1);
75
- }
84
+
85
+ let diagnosticLog: DiagnosticLogSession | null = null;
86
+ let errorKind: InstallDiagnosticErrorKind = "install_failed";
87
+ const startedAt = performance.now();
76
88
  try {
89
+ diagnosticLog = createDiagnosticLogSession();
90
+ diagnosticLog.event("command.start", {
91
+ command: "install",
92
+ backend: backend ?? "auto",
93
+ noCache,
94
+ tts,
95
+ vad,
96
+ diarize,
97
+ });
98
+
99
+ if (diarize && !(process.platform === "darwin" && process.arch === "arm64")) {
100
+ errorKind = "validation_failed";
101
+ throw new Error(
102
+ "--diarize is currently darwin-arm64 only " +
103
+ "(see https://github.com/drakulavich/kesha-voice-kit/issues/199).",
104
+ );
105
+ }
106
+ const platformBackend = defaultBackendForPlatform();
107
+ if (backend && !process.env.KESHA_ENGINE_BIN && platformBackend && backend !== platformBackend) {
108
+ errorKind = "validation_failed";
109
+ throw new Error(
110
+ `Requested backend "${backend}" is not available on this platform; ` +
111
+ `the release engine uses "${platformBackend}".`,
112
+ );
113
+ }
77
114
  await downloadEngine(noCache, backend, { tts, vad, diarize });
78
115
  await maybeAskForStar(getEngineBinPath(), packageVersion, log);
116
+ finishInstallDiagnostic(diagnosticLog, startedAt, "success");
79
117
  } catch (err: unknown) {
80
118
  const message = err instanceof Error ? err.message : String(err);
119
+ finishInstallDiagnostic(diagnosticLog, startedAt, "failed", errorKind);
81
120
  log.error(message);
82
121
  process.exit(1);
83
122
  }