@drakulavich/kesha-voice-kit 1.30.0 → 1.31.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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/completions/kesha.bash +1 -1
  3. package/completions/kesha.fish +0 -1
  4. package/completions/kesha.zsh +3 -1
  5. package/docs/diagnostic-logs.md +36 -3
  6. package/model-plan.json +130 -0
  7. package/package.json +2 -2
  8. package/src/cli/action-result.ts +2 -1
  9. package/src/cli/command-session.ts +14 -3
  10. package/src/cli/completions.ts +4 -2
  11. package/src/cli/dispatch.ts +37 -5
  12. package/src/cli/init.ts +4 -4
  13. package/src/cli/install.ts +3 -2
  14. package/src/cli/main.ts +60 -33
  15. package/src/cli/mcp.ts +2 -0
  16. package/src/cli/options.ts +79 -0
  17. package/src/cli/record.ts +2 -1
  18. package/src/cli/say.ts +92 -11
  19. package/src/cli/stats.ts +9 -4
  20. package/src/cli/support-bundle.ts +3 -3
  21. package/src/cli.ts +1 -1
  22. package/src/diagnostic-log.ts +2 -9
  23. package/src/doctor.ts +59 -2
  24. package/src/engine/events.ts +15 -1
  25. package/src/engine-install.ts +6 -4
  26. package/src/engine-targets.ts +3 -3
  27. package/src/engine.ts +43 -16
  28. package/src/fluid-roots.ts +5 -0
  29. package/src/format.ts +11 -5
  30. package/src/install-lock.ts +45 -11
  31. package/src/install-plan.ts +76 -7
  32. package/src/kokoro-ane.ts +66 -3
  33. package/src/language-routing.ts +37 -0
  34. package/src/lib.ts +15 -9
  35. package/src/log.ts +2 -0
  36. package/src/mcp/audio-output.ts +2 -2
  37. package/src/mcp/tools.ts +7 -5
  38. package/src/paths.ts +2 -2
  39. package/src/process-tree.ts +62 -12
  40. package/src/shell-artifacts.ts +4 -3
  41. package/src/state-paths.ts +84 -0
  42. package/src/stats.ts +2 -10
  43. package/src/status.ts +34 -26
  44. package/src/synth.ts +33 -6
  45. package/src/transcribe.ts +10 -0
  46. package/src/voice-inventory.ts +51 -0
  47. package/src/voice-routing.ts +97 -6
  48. package/tsconfig.json +1 -1
package/README.md CHANGED
@@ -158,7 +158,7 @@ All of these install the Bun CLI wrapper; engine + models still download explici
158
158
  - [Use cases](docs/use-cases.md) — copy-paste recipes (transcribe a meeting, speak from OpenClaw, run offline, move the cache).
159
159
  - [Product positioning](docs/product-positioning.md) — supported workflows, non-goals, maturity labels, platform matrix.
160
160
  - [Changelog](CHANGELOG.md) — every release, with the behaviour changes spelled out.
161
- - **Diagnostics:** `kesha doctor`, `kesha support-bundle` (redacted `.tar.gz` for issues), and `kesha logs` produce local, content-free diagnostics — see [docs/diagnostic-logs.md](docs/diagnostic-logs.md). Every failure prints a stable `error [CODE]: …` line and a documented [process exit code](docs/errors.md#process-exit-codes).
161
+ - **Diagnostics:** `kesha doctor`, `kesha support-bundle` (redacted `.tar.gz` for issues), and `kesha logs` produce local, content-free diagnostics — see [docs/diagnostic-logs.md](docs/diagnostic-logs.md). One variable, `KESHA_HOME`, moves everything Kesha writes (models, logs, stats) into a directory of your choice — see [where Kesha keeps its files](docs/diagnostic-logs.md#where-kesha-keeps-its-files). Every failure prints a stable `error [CODE]: …` line and a documented [process exit code](docs/errors.md#process-exit-codes).
162
162
  - **Scripting & CI:** `--json` (or `--toon`) for machine-readable output, `--include-errors` (with either) to get per-file failures on stdout alongside the results, `--quiet`/`-q` to silence progress, and `--no-color` (or `NO_COLOR=1`) for plain logs. Colors switch off automatically when `CI=true`.
163
163
  - **Privacy / Local Stats:** Stats are **off by default** and fully local. Opt in with `kesha stats enable` to record content-free operational metrics in a local SQLite database — never networked, never storing audio, transcripts, text, or paths. Full commands & lifecycle: [docs/local-stats.md](docs/local-stats.md).
164
164
 
@@ -38,4 +38,4 @@ _kesha_completion() {
38
38
  fi
39
39
  }
40
40
 
41
- complete -F _kesha_completion kesha
41
+ complete -o default -F _kesha_completion kesha
@@ -1,5 +1,4 @@
1
1
  # fish completion for kesha.
2
- complete -c kesha -f
3
2
  complete -c kesha -n '__fish_use_subcommand' -a 'completions' -d 'Print shell completion script for bash, zsh, or fish'
4
3
  complete -c kesha -n '__fish_use_subcommand' -a 'doctor' -d 'Collect support diagnostics without changing local state'
5
4
  complete -c kesha -n '__fish_use_subcommand' -a 'init' -d 'Interactive setup guide for Kesha features'
@@ -40,6 +40,7 @@ _kesha() {
40
40
  '--no-color[Disable ANSI colors (also via NO_COLOR=1; auto-off when CI=true)]'
41
41
  else
42
42
  _describe -t commands 'kesha command' commands
43
+ _files
43
44
  fi
44
45
  return
45
46
  fi
@@ -155,7 +156,8 @@ _kesha() {
155
156
  '--vad[Force Silero VAD preprocessing (kesha install --vad first). Without this, VAD auto-engages on audio ≥ 120s.]' \
156
157
  '--no-vad[Force full-file ASR for short/medium files; long audio fails early. Incompatible with --speakers]' \
157
158
  '--quiet[Suppress progress output; print only results and errors]' \
158
- '--no-color[Disable ANSI colors (also via NO_COLOR=1; auto-off when CI=true)]'
159
+ '--no-color[Disable ANSI colors (also via NO_COLOR=1; auto-off when CI=true)]' \
160
+ '*:audio file:_files'
159
161
  ;;
160
162
  esac
161
163
  }
@@ -43,9 +43,11 @@ stage names, exit codes, duration numbers, and stable error codes.
43
43
 
44
44
  When enabled, `kesha install`, `kesha <audio>`, and `kesha say` record command
45
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.
46
+ `engine.exit`, `engine.debug` (the Engine's own `KESHA_DEBUG` timeline, carrying
47
+ its event name and typed fields but never its message text), and `command.finish`.
48
+ These events use only counts, booleans, format extensions, duration milliseconds,
49
+ and bucket labels. In the default `retain-on-failure` mode, successful runs still
50
+ leave no log file behind.
49
51
 
50
52
  Diagnostic logs must not store:
51
53
 
@@ -75,6 +77,37 @@ Set `KESHA_LOG_DIR` to override the directory. The active file is
75
77
  `kesha.ndjson`; rotated files are named `kesha.1.ndjson`, `kesha.2.ndjson`, and
76
78
  so on.
77
79
 
80
+ ## Where Kesha keeps its files
81
+
82
+ Kesha writes to four places. Each resolves by the same rule: its own variable
83
+ when set, otherwise the path under `KESHA_HOME` when that is set, otherwise the
84
+ platform default. `KESHA_HOME` uses one layout on every platform, so a test run,
85
+ a CI job or a second profile needs exactly one variable to leave your real
86
+ models, logs and Stats untouched. Nothing is moved when you set it: a fresh
87
+ `KESHA_HOME` starts with an empty cache, and `kesha install` fills it. Two roots FluidAudio owns stay outside it on darwin-arm64: the Kokoro ANE bundles and voice packs under `~/.cache/fluidaudio`, and its Silero VAD copy under `~/Library/Application Support/FluidAudio`; `status --disk` reports both as external roots, and `kesha install --tts` stages into the first regardless of `KESHA_HOME`.
88
+
89
+ | What | Default (macOS / Windows / Linux) | Under `KESHA_HOME` | Own variable |
90
+ |---|---|---|---|
91
+ | Model cache: engine, models, `recordings/` | `~/.cache/kesha` everywhere | `<home>/cache` | `KESHA_CACHE_DIR` |
92
+ | Diagnostic log directory | `~/Library/Logs/kesha` / `%LOCALAPPDATA%\kesha\logs` / `$XDG_STATE_HOME/kesha/logs` | `<home>/logs` | `KESHA_LOG_DIR` |
93
+ | Stats DB | `~/Library/Application Support/kesha/stats.sqlite` / `%APPDATA%\kesha\stats.sqlite` / `$XDG_DATA_HOME/kesha/stats.sqlite` | `<home>/stats.sqlite` | `KESHA_STATS_DB` |
94
+ | MCP audio (`kesha mcp` synthesis output) | `<tmpdir>/kesha-mcp` | `<home>/mcp-audio` | none |
95
+
96
+ `kesha status --json` and `kesha doctor --json` report every path together with
97
+ the rule that decided it (`default`, `KESHA_HOME`, or the variable name), so an
98
+ isolated run can be verified from one command:
99
+
100
+ ```bash
101
+ KESHA_HOME=/tmp/kesha-ci kesha status --json | jq .paths
102
+ ```
103
+
104
+ An empty variable counts as unset; a relative path is resolved against the
105
+ working directory when the command starts. The engine itself reads only
106
+ `KESHA_CACHE_DIR`, so the CLI hands it the resolved cache root under that name
107
+ whenever `KESHA_HOME` decided it. FluidAudio's own Silero VAD copy under
108
+ `~/Library/Application Support/FluidAudio` is placed by FluidAudio and stays
109
+ outside `KESHA_HOME`; `kesha status --disk` lists it as an external root.
110
+
78
111
  The first implementation rotates at 10 MB and keeps 5 rotated files. `kesha logs
79
112
  reset` deletes Kesha log files but preserves the selected mode.
80
113
 
package/model-plan.json CHANGED
@@ -34,6 +34,136 @@
34
34
  { "relPath": "models/vosk-ru/bert/model.onnx", "sizeBytes": 654361598 },
35
35
  { "relPath": "models/vosk-ru/bert/vocab.txt", "sizeBytes": 1780720 }
36
36
  ],
37
+ "aneEn": [
38
+ { "relPath": "KokoroAlbert.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
39
+ { "relPath": "KokoroAlbert.mlmodelc/coremldata.bin", "sizeBytes": 433 },
40
+ { "relPath": "KokoroAlbert.mlmodelc/metadata.json", "sizeBytes": 2480 },
41
+ { "relPath": "KokoroAlbert.mlmodelc/model.mil", "sizeBytes": 101485 },
42
+ { "relPath": "KokoroAlbert.mlmodelc/weights/weight.bin", "sizeBytes": 5718848 },
43
+ { "relPath": "KokoroPostAlbert.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
44
+ { "relPath": "KokoroPostAlbert.mlmodelc/coremldata.bin", "sizeBytes": 556 },
45
+ { "relPath": "KokoroPostAlbert.mlmodelc/metadata.json", "sizeBytes": 4171 },
46
+ { "relPath": "KokoroPostAlbert.mlmodelc/model.mil", "sizeBytes": 60047 },
47
+ { "relPath": "KokoroPostAlbert.mlmodelc/weights/weight.bin", "sizeBytes": 13806464 },
48
+ { "relPath": "KokoroAlignment.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
49
+ { "relPath": "KokoroAlignment.mlmodelc/coremldata.bin", "sizeBytes": 484 },
50
+ { "relPath": "KokoroAlignment.mlmodelc/metadata.json", "sizeBytes": 3021 },
51
+ { "relPath": "KokoroAlignment.mlmodelc/model.mil", "sizeBytes": 8194 },
52
+ { "relPath": "KokoroAlignment.mlmodelc/weights/weight.bin", "sizeBytes": 4128 },
53
+ { "relPath": "KokoroProsody.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
54
+ { "relPath": "KokoroProsody.mlmodelc/coremldata.bin", "sizeBytes": 421 },
55
+ { "relPath": "KokoroProsody.mlmodelc/metadata.json", "sizeBytes": 2721 },
56
+ { "relPath": "KokoroProsody.mlmodelc/model.mil", "sizeBytes": 79705 },
57
+ { "relPath": "KokoroProsody.mlmodelc/weights/weight.bin", "sizeBytes": 8454272 },
58
+ { "relPath": "KokoroNoise_v2.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
59
+ { "relPath": "KokoroNoise_v2.mlmodelc/coremldata.bin", "sizeBytes": 440 },
60
+ { "relPath": "KokoroNoise_v2.mlmodelc/metadata.json", "sizeBytes": 3020 },
61
+ { "relPath": "KokoroNoise_v2.mlmodelc/model.mil", "sizeBytes": 93152 },
62
+ { "relPath": "KokoroNoise_v2.mlmodelc/weights/weight.bin", "sizeBytes": 4580160 },
63
+ { "relPath": "KokoroVocoder.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
64
+ { "relPath": "KokoroVocoder.mlmodelc/coremldata.bin", "sizeBytes": 626 },
65
+ { "relPath": "KokoroVocoder.mlmodelc/metadata.json", "sizeBytes": 4392 },
66
+ { "relPath": "KokoroVocoder.mlmodelc/model.mil", "sizeBytes": 309130 },
67
+ { "relPath": "KokoroVocoder.mlmodelc/weights/weight.bin", "sizeBytes": 48889920 },
68
+ { "relPath": "KokoroTail.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
69
+ { "relPath": "KokoroTail.mlmodelc/coremldata.bin", "sizeBytes": 392 },
70
+ { "relPath": "KokoroTail.mlmodelc/metadata.json", "sizeBytes": 1872 },
71
+ { "relPath": "KokoroTail.mlmodelc/model.mil", "sizeBytes": 7014 },
72
+ { "relPath": "KokoroTail.mlmodelc/weights/weight.bin", "sizeBytes": 81088 },
73
+ { "relPath": "vocab.json", "sizeBytes": 1416 },
74
+ { "relPath": "af_heart.bin", "sizeBytes": 522240 }
75
+ ],
76
+ "kokoroG2p": [
77
+ { "relPath": "G2PEncoder.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
78
+ { "relPath": "G2PEncoder.mlmodelc/coremldata.bin", "sizeBytes": 398 },
79
+ { "relPath": "G2PEncoder.mlmodelc/metadata.json", "sizeBytes": 2034 },
80
+ { "relPath": "G2PEncoder.mlmodelc/model.mil", "sizeBytes": 20392 },
81
+ { "relPath": "G2PEncoder.mlmodelc/weights/weight.bin", "sizeBytes": 694592 },
82
+ { "relPath": "G2PDecoder.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
83
+ { "relPath": "G2PDecoder.mlmodelc/coremldata.bin", "sizeBytes": 545 },
84
+ { "relPath": "G2PDecoder.mlmodelc/metadata.json", "sizeBytes": 3064 },
85
+ { "relPath": "G2PDecoder.mlmodelc/model.mil", "sizeBytes": 19737 },
86
+ { "relPath": "G2PDecoder.mlmodelc/weights/weight.bin", "sizeBytes": 828030 },
87
+ { "relPath": "g2p_vocab.json", "sizeBytes": 1665 },
88
+ { "relPath": "us_lexicon_cache.json", "sizeBytes": 10444631 }
89
+ ],
90
+ "aneVoices": [
91
+ { "relPath": "af_alloy.bin", "sizeBytes": 522240 },
92
+ { "relPath": "af_aoede.bin", "sizeBytes": 522240 },
93
+ { "relPath": "af_bella.bin", "sizeBytes": 522240 },
94
+ { "relPath": "af_jessica.bin", "sizeBytes": 522240 },
95
+ { "relPath": "af_kore.bin", "sizeBytes": 522240 },
96
+ { "relPath": "af_nicole.bin", "sizeBytes": 522240 },
97
+ { "relPath": "af_nova.bin", "sizeBytes": 522240 },
98
+ { "relPath": "af_river.bin", "sizeBytes": 522240 },
99
+ { "relPath": "af_sarah.bin", "sizeBytes": 522240 },
100
+ { "relPath": "af_sky.bin", "sizeBytes": 522240 },
101
+ { "relPath": "am_adam.bin", "sizeBytes": 522240 },
102
+ { "relPath": "am_echo.bin", "sizeBytes": 522240 },
103
+ { "relPath": "am_eric.bin", "sizeBytes": 522240 },
104
+ { "relPath": "am_fenrir.bin", "sizeBytes": 522240 },
105
+ { "relPath": "am_liam.bin", "sizeBytes": 522240 },
106
+ { "relPath": "am_michael.bin", "sizeBytes": 522240 },
107
+ { "relPath": "am_onyx.bin", "sizeBytes": 522240 },
108
+ { "relPath": "am_puck.bin", "sizeBytes": 522240 },
109
+ { "relPath": "am_santa.bin", "sizeBytes": 522240 },
110
+ { "relPath": "bm_lewis.bin", "sizeBytes": 522240 },
111
+ { "relPath": "em_alex.bin", "sizeBytes": 522240 },
112
+ { "relPath": "ff_siwis.bin", "sizeBytes": 522240 },
113
+ { "relPath": "hm_omega.bin", "sizeBytes": 522240 },
114
+ { "relPath": "im_nicola.bin", "sizeBytes": 522240 },
115
+ { "relPath": "jm_kumo.bin", "sizeBytes": 522240 },
116
+ { "relPath": "pm_alex.bin", "sizeBytes": 522240 }
117
+ ],
118
+ "aneZh": [
119
+ { "relPath": "KokoroAlbert.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
120
+ { "relPath": "KokoroAlbert.mlmodelc/coremldata.bin", "sizeBytes": 433 },
121
+ { "relPath": "KokoroAlbert.mlmodelc/metadata.json", "sizeBytes": 2480 },
122
+ { "relPath": "KokoroAlbert.mlmodelc/model.mil", "sizeBytes": 101485 },
123
+ { "relPath": "KokoroAlbert.mlmodelc/weights/weight.bin", "sizeBytes": 5718848 },
124
+ { "relPath": "KokoroPostAlbert.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
125
+ { "relPath": "KokoroPostAlbert.mlmodelc/coremldata.bin", "sizeBytes": 556 },
126
+ { "relPath": "KokoroPostAlbert.mlmodelc/metadata.json", "sizeBytes": 4171 },
127
+ { "relPath": "KokoroPostAlbert.mlmodelc/model.mil", "sizeBytes": 60047 },
128
+ { "relPath": "KokoroPostAlbert.mlmodelc/weights/weight.bin", "sizeBytes": 13806464 },
129
+ { "relPath": "KokoroAlignment.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
130
+ { "relPath": "KokoroAlignment.mlmodelc/coremldata.bin", "sizeBytes": 484 },
131
+ { "relPath": "KokoroAlignment.mlmodelc/metadata.json", "sizeBytes": 3021 },
132
+ { "relPath": "KokoroAlignment.mlmodelc/model.mil", "sizeBytes": 8194 },
133
+ { "relPath": "KokoroAlignment.mlmodelc/weights/weight.bin", "sizeBytes": 4128 },
134
+ { "relPath": "KokoroProsody.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
135
+ { "relPath": "KokoroProsody.mlmodelc/coremldata.bin", "sizeBytes": 422 },
136
+ { "relPath": "KokoroProsody.mlmodelc/metadata.json", "sizeBytes": 2721 },
137
+ { "relPath": "KokoroProsody.mlmodelc/model.mil", "sizeBytes": 79705 },
138
+ { "relPath": "KokoroProsody.mlmodelc/weights/weight.bin", "sizeBytes": 8454272 },
139
+ { "relPath": "KokoroNoise_v2.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
140
+ { "relPath": "KokoroNoise_v2.mlmodelc/coremldata.bin", "sizeBytes": 440 },
141
+ { "relPath": "KokoroNoise_v2.mlmodelc/metadata.json", "sizeBytes": 3091 },
142
+ { "relPath": "KokoroNoise_v2.mlmodelc/model.mil", "sizeBytes": 93152 },
143
+ { "relPath": "KokoroNoise_v2.mlmodelc/weights/weight.bin", "sizeBytes": 4580160 },
144
+ { "relPath": "KokoroVocoder.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
145
+ { "relPath": "KokoroVocoder.mlmodelc/coremldata.bin", "sizeBytes": 627 },
146
+ { "relPath": "KokoroVocoder.mlmodelc/metadata.json", "sizeBytes": 4392 },
147
+ { "relPath": "KokoroVocoder.mlmodelc/model.mil", "sizeBytes": 309127 },
148
+ { "relPath": "KokoroVocoder.mlmodelc/weights/weight.bin", "sizeBytes": 48889920 },
149
+ { "relPath": "KokoroTail.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
150
+ { "relPath": "KokoroTail.mlmodelc/coremldata.bin", "sizeBytes": 392 },
151
+ { "relPath": "KokoroTail.mlmodelc/metadata.json", "sizeBytes": 1872 },
152
+ { "relPath": "KokoroTail.mlmodelc/model.mil", "sizeBytes": 7014 },
153
+ { "relPath": "KokoroTail.mlmodelc/weights/weight.bin", "sizeBytes": 81088 },
154
+ { "relPath": "vocab.json", "sizeBytes": 1796 },
155
+ { "relPath": "voices/zf_001.bin", "sizeBytes": 522240 },
156
+ { "relPath": "voices/zm_050.bin", "sizeBytes": 522240 },
157
+ { "relPath": "g2pw/g2pw.mlmodelc/analytics/coremldata.bin", "sizeBytes": 243 },
158
+ { "relPath": "g2pw/g2pw.mlmodelc/coremldata.bin", "sizeBytes": 515 },
159
+ { "relPath": "g2pw/g2pw.mlmodelc/metadata.json", "sizeBytes": 3717 },
160
+ { "relPath": "g2pw/g2pw.mlmodelc/model.mil", "sizeBytes": 245446 },
161
+ { "relPath": "g2pw/g2pw.mlmodelc/weights/weight.bin", "sizeBytes": 159294962 }
162
+ ],
163
+ "aneZhG2p": [
164
+ { "relPath": "g2p/pinyin_phrases.bin", "sizeBytes": 9552083 },
165
+ { "relPath": "g2p/pinyin_single.bin", "sizeBytes": 480169 }
166
+ ],
37
167
  "diarize": [
38
168
  { "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Manifest.json", "sizeBytes": 617 },
39
169
  { "relPath": "models/diarize/SortformerNvidiaLow_v2.mlpackage/Data/com.apple.CoreML/model.mlmodel", "sizeBytes": 856521 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakulavich/kesha-voice-kit",
3
- "version": "1.30.0",
3
+ "version": "1.31.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": {
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "keshaEngine": {
43
- "version": "1.25.0"
43
+ "version": "1.26.0"
44
44
  },
45
45
  "scripts": {
46
46
  "test": "bun test --timeout 30000",
@@ -1,4 +1,5 @@
1
1
  import { log } from "../log";
2
+ import { renderInvalidArg } from "./options";
2
3
 
3
4
  export type ActionMessage = { level: "success" | "info" | "warn"; text: string };
4
5
 
@@ -9,7 +10,7 @@ export type ActionResult =
9
10
  /** Renders an action's messages, writing `stdout` verbatim; a rejected action exits 2. */
10
11
  export function emitActionResult(result: ActionResult): void {
11
12
  if (!result.ok) {
12
- log.error(result.error);
13
+ log.error(renderInvalidArg(result.error));
13
14
  if (result.hint) log.warn(result.hint);
14
15
  process.exit(2);
15
16
  }
@@ -50,9 +50,8 @@ export async function runCommandSession(
50
50
  setEngineDebugSink((event) =>
51
51
  session.diagnosticLog.event("engine.debug", {
52
52
  t_ms: event.t_ms,
53
- event: event.event ?? null,
54
- message: event.message,
55
- fields: event.fields === undefined ? null : JSON.stringify(event.fields),
53
+ engineEvent: event.event ?? null,
54
+ ...engineDebugFields(event.fields),
56
55
  }),
57
56
  );
58
57
  let outcome: CommandOutcome;
@@ -74,6 +73,18 @@ export async function runCommandSession(
74
73
  return outcome;
75
74
  }
76
75
 
76
+ // The message is prose and `event` is a reserved log field, so only the engine's typed fields ride along (Exploratory S9-F5).
77
+ function engineDebugFields(fields: unknown): DiagnosticLogFields {
78
+ const out: DiagnosticLogFields = {};
79
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) return out;
80
+ for (const [key, value] of Object.entries(fields)) {
81
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
82
+ out[key] = value;
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+
77
88
  function closeSessionQuietly(
78
89
  session: CommandSession,
79
90
  command: StatsCommandName,
@@ -1,5 +1,6 @@
1
1
  import { defineCommand } from "citty";
2
2
  import { log } from "../log";
3
+ import { renderInvalidArg } from "./options";
3
4
  // Inlined because import.meta.url escapes the embedded filesystem in the compiled .deb/.rpm binary (#914).
4
5
  import bashCompletions from "../../completions/kesha.bash" with { type: "text" };
5
6
  import zshCompletions from "../../completions/kesha.zsh" with { type: "text" };
@@ -29,14 +30,15 @@ export const completionsCommand = defineCommand({
29
30
  args: {
30
31
  shell: {
31
32
  type: "positional",
32
- required: true,
33
+ required: false,
33
34
  description: "Shell: bash | zsh | fish",
34
35
  },
35
36
  },
36
37
  async run({ args }: { args: CompletionsCommandArgs }) {
37
38
  const shell = args.shell;
38
39
  if (!shell || !isShell(shell)) {
39
- log.error("usage: kesha completions <bash|zsh|fish>");
40
+ log.error(renderInvalidArg(shell ? `unknown shell '${shell}' (bash, zsh or fish)` : "missing shell (bash, zsh or fish)"));
41
+ process.stderr.write("usage: kesha completions <bash|zsh|fish>\n");
40
42
  process.exit(2);
41
43
  }
42
44
  process.stdout.write(SHELL_SCRIPTS[shell]);
@@ -1,9 +1,10 @@
1
- import { runMain, type CommandDef } from "citty";
1
+ import { runMain, type ArgsDef, type CommandDef } from "citty";
2
2
  import { existsSync } from "fs";
3
3
  import { log } from "../log";
4
4
  import { guardStdoutWrites } from "../stdout-pipe";
5
5
  import { suggestCommand } from "../suggest-command";
6
6
  import { applyCliContext, resolveCliContext } from "./context";
7
+ import { rejectUnknownOptions, renderInvalidArg } from "./options";
7
8
 
8
9
  // Lazy loaders so a cold CLI spawn transpiles only the invoked command, not the whole graph (#568).
9
10
  // `CommandDef<any>`: citty's generic is invariant in the arg shape and each command has its own schema.
@@ -89,18 +90,42 @@ export function unknownCommandMessages(token: string, subcommandKeys: string[]):
89
90
  return { errorLine: `unknown command '${token}'`, warnLines };
90
91
  }
91
92
 
93
+ async function resolveArgsDef(command: CommandDef<any>): Promise<ArgsDef> {
94
+ const args = command.args;
95
+ return (typeof args === "function" ? await args() : await args) ?? {};
96
+ }
97
+
98
+ /** `kesha --json record --out j.wav` is `record --json --out j.wav`: flag-shaped tokens before a subcommand name move behind it (S3-F3). */
99
+ export function hoistLeadingFlags(rawArgs: string[], subcommandKeys: string[]): string[] {
100
+ let i = 0;
101
+ while (i < rawArgs.length && rawArgs[i]!.startsWith("-") && rawArgs[i] !== "--") i++;
102
+ const name = rawArgs[i];
103
+ if (i === 0 || name === undefined || !subcommandKeys.includes(name)) return rawArgs;
104
+ return [name, ...rawArgs.slice(0, i), ...rawArgs.slice(i + 1)];
105
+ }
106
+
107
+ /** A subcommand name that a valued leading flag hid from hoisting, unless a file of that name exists. */
108
+ function misplacedSubcommand(rawArgs: string[], subcommandKeys: string[]): string | undefined {
109
+ const end = rawArgs.indexOf("--");
110
+ return rawArgs.slice(0, end === -1 ? undefined : end).find((arg) => subcommandKeys.includes(arg) && !existsSync(arg));
111
+ }
112
+
92
113
  export async function runCli(argv = process.argv.slice(2)): Promise<void> {
93
114
  guardStdoutWrites();
94
115
  const context = resolveCliContext(argv);
95
116
  applyCliContext(context);
96
117
 
97
- const [firstArg, ...restArgs] = context.rawArgs;
98
118
  const subcommandKeys = SUBCOMMAND_NAMES;
119
+ const rawArgs = hoistLeadingFlags(context.rawArgs, subcommandKeys);
120
+ const [firstArg, ...restArgs] = rawArgs;
99
121
 
100
122
  switch (classifyFirstArg(firstArg, subcommandKeys)) {
101
- case "subcommand":
102
- await runMain(await SUBCOMMANDS[firstArg!]!(), { rawArgs: restArgs });
123
+ case "subcommand": {
124
+ const command = await SUBCOMMANDS[firstArg!]!();
125
+ rejectUnknownOptions(restArgs, await resolveArgsDef(command));
126
+ await runMain(command, { rawArgs: restArgs });
103
127
  return;
128
+ }
104
129
 
105
130
  case "unknown": {
106
131
  // Extensionless existing files are valid transcription inputs; bare non-path tokens are likely command typos.
@@ -112,8 +137,15 @@ export async function runCli(argv = process.argv.slice(2)): Promise<void> {
112
137
  }
113
138
 
114
139
  default: {
140
+ const misplaced = misplacedSubcommand(rawArgs, subcommandKeys);
141
+ if (misplaced !== undefined) {
142
+ log.error(renderInvalidArg(`put global flags after the subcommand: kesha ${misplaced} ...`));
143
+ process.exit(2);
144
+ }
115
145
  const { createMainCommand } = await import("./main");
116
- await runMain(createMainCommand(context), { rawArgs: context.rawArgs });
146
+ const command = createMainCommand(context);
147
+ rejectUnknownOptions(rawArgs, await resolveArgsDef(command));
148
+ await runMain(command, { rawArgs });
117
149
  }
118
150
  }
119
151
  }
package/src/cli/init.ts CHANGED
@@ -32,16 +32,16 @@ export type TtsLangPrompt = (preselect: string[]) => Promise<string[]>;
32
32
  /** Signature of the interactive yes/no prompt (injectable for tests). */
33
33
  export type ConfirmPrompt = (message: string, initialValue: boolean) => Promise<boolean>;
34
34
 
35
- /** Ctrl-C at any prompt ends init without downloading, rather than falling through with a default. */
35
+ /** Ctrl-C at any prompt ends init without downloading, exiting 130 like an interrupted install so `kesha init && …` stops (S4-F1). */
36
36
  function exitIfCancelled<T>(answer: T | symbol): T {
37
37
  if (isCancel(answer)) {
38
38
  cancel("Init cancelled.");
39
- process.exit(0);
39
+ process.exit(130);
40
40
  }
41
41
  return answer as T;
42
42
  }
43
43
 
44
- async function promptTtsLangs(preselect: string[]): Promise<string[]> {
44
+ export async function promptTtsLangs(preselect: string[]): Promise<string[]> {
45
45
  // A corrupt engine must leave init offering the fallback list rather than aborting the
46
46
  // guided setup that would replace it (#770).
47
47
  const caps = await getEngineCapabilities().catch(() => null);
@@ -57,7 +57,7 @@ async function promptTtsLangs(preselect: string[]): Promise<string[]> {
57
57
  }
58
58
 
59
59
  /** #677: keep every prompt on clack — its close() unpipes stdin, deadlocking any readline sharing it. */
60
- async function promptConfirm(message: string, initialValue: boolean): Promise<boolean> {
60
+ export async function promptConfirm(message: string, initialValue: boolean): Promise<boolean> {
61
61
  return exitIfCancelled(await confirm({ message, initialValue }));
62
62
  }
63
63
 
@@ -6,6 +6,7 @@ import { getEngineBinPath, getEngineCapabilities, type EngineCapabilities } from
6
6
  import { exitCodeFor, KeshaError } from "../engine/events";
7
7
  import { renderInstallPlan } from "../install-plan";
8
8
  import { maybeAskForStar } from "../star";
9
+ import { renderInvalidArg } from "./options";
9
10
  import { log } from "../log";
10
11
  import { packageVersion } from "../package-info";
11
12
  import { isSemver } from "../semver.mjs";
@@ -321,8 +322,8 @@ export const installCommand = defineCommand({
321
322
  try {
322
323
  ttsLangs = resolveTtsLangs({ tts: args.tts === true, positionals }, supported);
323
324
  } catch (err) {
324
- log.error(errorMessage(err));
325
- process.exit(1);
325
+ log.error(renderInvalidArg(errorMessage(err)));
326
+ process.exit(2);
326
327
  }
327
328
  await performInstall({
328
329
  noCache: resolveNoCacheFlag(args, rawArgs),