moqi-tui 0.2.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +782 -0
  3. package/bin/moqi.mjs +40 -0
  4. package/cordis.patch.yml +41 -0
  5. package/lib/cross-find.js +217 -0
  6. package/lib/file-index.js +121 -0
  7. package/lib/fleet-sources.js +114 -0
  8. package/lib/index.js +3999 -0
  9. package/lib/persist.js +194 -0
  10. package/lib/plugins.js +371 -0
  11. package/lib/presence.js +144 -0
  12. package/lib/rename.js +35 -0
  13. package/lib/rewind.js +94 -0
  14. package/lib/sessions-store.js +134 -0
  15. package/lib/startup.js +92 -0
  16. package/lib/tui/atfile.js +154 -0
  17. package/lib/tui/export.js +48 -0
  18. package/lib/tui/fleet.js +346 -0
  19. package/lib/tui/i18n.js +201 -0
  20. package/lib/tui/jobs.js +65 -0
  21. package/lib/tui/keys.js +205 -0
  22. package/lib/tui/markdown.js +368 -0
  23. package/lib/tui/mcp.js +95 -0
  24. package/lib/tui/panels.js +231 -0
  25. package/lib/tui/screen.js +156 -0
  26. package/lib/tui/state.js +502 -0
  27. package/lib/tui/stream.js +109 -0
  28. package/lib/tui/text.js +173 -0
  29. package/lib/tui/theme.js +183 -0
  30. package/lib/tui/themes.js +153 -0
  31. package/lib/tui/tooldetail.js +140 -0
  32. package/lib/tui/view.js +830 -0
  33. package/lib/tui/vim.js +222 -0
  34. package/lib/tui-host-core.js +141 -0
  35. package/lib/tui-host.js +48 -0
  36. package/lib/types/cross-find.d.ts +66 -0
  37. package/lib/types/file-index.d.ts +34 -0
  38. package/lib/types/fleet-sources.d.ts +34 -0
  39. package/lib/types/index.d.ts +51 -0
  40. package/lib/types/persist.d.ts +116 -0
  41. package/lib/types/plugins.d.ts +218 -0
  42. package/lib/types/presence.d.ts +48 -0
  43. package/lib/types/rename.d.ts +32 -0
  44. package/lib/types/rewind.d.ts +75 -0
  45. package/lib/types/sessions-store.d.ts +46 -0
  46. package/lib/types/startup.d.ts +45 -0
  47. package/lib/types/tui/atfile.d.ts +90 -0
  48. package/lib/types/tui/export.d.ts +18 -0
  49. package/lib/types/tui/fleet.d.ts +209 -0
  50. package/lib/types/tui/i18n.d.ts +34 -0
  51. package/lib/types/tui/jobs.d.ts +28 -0
  52. package/lib/types/tui/keys.d.ts +52 -0
  53. package/lib/types/tui/markdown.d.ts +14 -0
  54. package/lib/types/tui/mcp.d.ts +34 -0
  55. package/lib/types/tui/panels.d.ts +125 -0
  56. package/lib/types/tui/screen.d.ts +79 -0
  57. package/lib/types/tui/state.d.ts +323 -0
  58. package/lib/types/tui/stream.d.ts +78 -0
  59. package/lib/types/tui/text.d.ts +28 -0
  60. package/lib/types/tui/theme.d.ts +87 -0
  61. package/lib/types/tui/themes.d.ts +70 -0
  62. package/lib/types/tui/tooldetail.d.ts +45 -0
  63. package/lib/types/tui/view.d.ts +163 -0
  64. package/lib/types/tui/vim.d.ts +64 -0
  65. package/lib/types/tui-host-core.d.ts +62 -0
  66. package/lib/types/tui-host.d.ts +42 -0
  67. package/lib/types/version.d.ts +8 -0
  68. package/lib/types/voice.d.ts +227 -0
  69. package/lib/version.js +32 -0
  70. package/lib/voice.js +405 -0
  71. package/package.json +119 -0
  72. package/scripts/harness-root.mjs +88 -0
  73. package/scripts/install-profile.mjs +133 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Frame composition: header, transcript, palette popup, composer, footer.
3
+ *
4
+ * The renderer is pure — it turns a snapshot of the app into the exact lines
5
+ * the screen should show, and reports where the cursor belongs. Nothing here
6
+ * touches the terminal or the Harness.
7
+ * @module
8
+ */
9
+ import { type FleetView } from './fleet.ts';
10
+ import { Composer, type Message, type Palette, type BackgroundAgent, type Picker, type Segment, type SessionSummary } from './state.ts';
11
+ import { type AtMenu } from './atfile.ts';
12
+ import type { PanelView } from './panels.ts';
13
+ /** Everything the renderer needs to draw one frame. */
14
+ export interface Snapshot {
15
+ columns: number;
16
+ rows: number;
17
+ title: string;
18
+ host: string;
19
+ modelName: string;
20
+ messages: readonly Message[];
21
+ /** The turn streaming in, prose and calls in arrival order. */
22
+ streamingSegments: readonly Segment[];
23
+ streamingReasoning: string;
24
+ streaming: boolean;
25
+ spinner: string;
26
+ status: string;
27
+ statusIsError: boolean;
28
+ overlay: string;
29
+ showThinking: boolean;
30
+ composer: Composer;
31
+ palette: Palette;
32
+ picker: Picker;
33
+ /** Rows scrolled up from the bottom of the transcript. */
34
+ scrollBack: number;
35
+ /** Whether each tool call is listed instead of summarized on one line. */
36
+ expandTools: boolean;
37
+ /** Open sessions, in creation order; the bar is hidden when there is one. */
38
+ sessions: readonly SessionSummary[];
39
+ /** Live agents other than the foreground one, newest last. */
40
+ background: readonly BackgroundAgent[];
41
+ /** Whether the background agents are listed instead of counted on one line. */
42
+ expandBackground: boolean;
43
+ /** Seconds the current reply has been running, for the activity line. */
44
+ elapsedSeconds: number;
45
+ promptTokens: number;
46
+ completionTokens: number;
47
+ totalTokens: number;
48
+ haveUsage: boolean;
49
+ contextLimit: number;
50
+ /** Output tokens per second for the last settled turn; 0 when unknown. */
51
+ tps?: number;
52
+ /** Prompt tokens served from cache, and the total prompt tokens they came from. */
53
+ cacheReadTokens?: number;
54
+ confirming: boolean;
55
+ /** Set while confirming: the yes/no question, drawn in the composer. */
56
+ confirmText?: string;
57
+ /** Whether a `/find` search is open, so its counter outranks the scroll hint. */
58
+ searchActive?: boolean;
59
+ /**
60
+ * Prompts queued while the active session's reply streams, drawn dimmed
61
+ * under the streaming block. Optional so snapshot builders without a
62
+ * queue render exactly as before.
63
+ */
64
+ queued?: readonly string[];
65
+ /**
66
+ * The cross-device overview, when one is open. Optional so every existing
67
+ * snapshot builder renders exactly as before.
68
+ */
69
+ fleet?: FleetView;
70
+ /**
71
+ * Push-to-talk state, while the microphone is open or whisper is running.
72
+ * Optional so every existing snapshot builder renders exactly as before.
73
+ */
74
+ voice?: VoicePhase;
75
+ /**
76
+ * The `@` file-completion menu, while an `@` token is being typed.
77
+ * Optional so every existing snapshot builder renders exactly as before.
78
+ */
79
+ atMenu?: AtMenu;
80
+ /**
81
+ * A trust-surface panel (approval, question, plan review). While one is
82
+ * open it owns the keyboard and replaces the transcript.
83
+ */
84
+ panel?: PanelView;
85
+ /**
86
+ * Index of the transcript turn under selection, marked with a gold bar.
87
+ * Optional so every existing snapshot builder renders exactly as before.
88
+ */
89
+ selectedTurn?: number;
90
+ /** One line a plugin contributed, drawn above the composer. */
91
+ pluginLine?: string;
92
+ /** The composer's vim mode, when modal editing is on. */
93
+ vimMode?: 'insert' | 'normal';
94
+ }
95
+ /** What push-to-talk is doing, for the footer indicator. */
96
+ export type VoicePhase = 'recording' | 'transcribing';
97
+ /** Geometry derived from the terminal size and the current composer height. */
98
+ export interface Layout {
99
+ contentWidth: number;
100
+ viewportRows: number;
101
+ paletteRows: number;
102
+ inputRows: number;
103
+ /** Rows the `@` file-completion popup shows, excluding its border. */
104
+ atRows: number;
105
+ /** One row when a plugin contributed a status line. */
106
+ pluginRows: number;
107
+ /** Rows the background-agent strip occupies, including any border. */
108
+ backgroundRows: number;
109
+ /** Rows the session tab bar occupies (0 or 1). */
110
+ sessionRows: number;
111
+ /** Cleared on a window too short to afford the header. */
112
+ showHeader: boolean;
113
+ /** Cleared on a window too short to afford the blank separator row. */
114
+ showGap: boolean;
115
+ }
116
+ /** Compute the geometry for a frame. */
117
+ export declare function layout(snapshot: Snapshot): Layout;
118
+ /** Strip the scheme and trailing slash from a base URL for the header. */
119
+ export declare function hostLabel(base: string): string;
120
+ /**
121
+ * How far back the transcript can scroll: anything beyond this is empty space
122
+ * above the first line, so the caller clamps to it rather than letting the view
123
+ * drift off the top.
124
+ */
125
+ export declare function maxScrollBack(snapshot: Snapshot): number;
126
+ /**
127
+ * Lines of the rendered body that contain `query`, case-insensitively.
128
+ *
129
+ * Matching runs over the printable text of each line — the styled form is full
130
+ * of SGR escapes the user never typed — and returns indexes into the same
131
+ * line array the viewport slices, so a hit can be scrolled to directly.
132
+ */
133
+ export declare function findMatches(snapshot: Snapshot, query: string): number[];
134
+ /** Build a full frame plus the cursor position for the screen to place. */
135
+ export declare function render(snapshot: Snapshot): {
136
+ lines: string[];
137
+ cursor: {
138
+ row: number;
139
+ column: number;
140
+ } | undefined;
141
+ };
142
+ /**
143
+ * The help text shown by `/help`, rendered as markdown in the transcript pane.
144
+ *
145
+ * It is longer than a default 80x24 window, and the overlay shows the *tail*
146
+ * of it, so the list has a budget: every line added here pushes one off the
147
+ * top, and what falls off first is the session keys. A new section therefore
148
+ * comes with an equal number of lines folded together further down — which is
149
+ * why several entries below read as two keys on one row.
150
+ */
151
+ /**
152
+ * The key reference.
153
+ *
154
+ * The strings live in the `i18n` catalog so one list covers both languages;
155
+ * this export is the English one, which the render tests assert against.
156
+ *
157
+ * The overlay shows the tail of it, so the list has a budget: a new section
158
+ * comes with an equal number of lines folded elsewhere. A line added to one
159
+ * language's copy in `i18n.ts` must be added to the other, or the two drift.
160
+ */
161
+ export declare const HELP_TEXT: string;
162
+ /** The key reference in the active language, for the `/help` overlay. */
163
+ export declare function keyReference(): string;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Vim modal editing for the composer.
3
+ *
4
+ * Deliberately a small subset with exact semantics: motions (`h l 0 ^ $ w b`),
5
+ * edits (`x X dd d$ d0 dw`), insert entries (`i I a A o O`), and `u` for undo.
6
+ * Anything else is ignored rather than guessed at, so an unexpected key can
7
+ * never mangle a draft.
8
+ *
9
+ * The composer is the only thing mutated; the app decides when to route keys
10
+ * here, which keeps this testable as plain state.
11
+ * @module
12
+ */
13
+ import type { Composer } from './state.ts';
14
+ /** Which vim mode the composer is in. */
15
+ export type VimMode = 'insert' | 'normal';
16
+ /** How many edits `u` can walk back. */
17
+ export declare const VIM_UNDO_LIMIT = 100;
18
+ /** What the caller should do with a key routed to vim mode. */
19
+ export type VimOutcome =
20
+ /** The key did something to the composer; repaint. */
21
+ 'handled'
22
+ /** The key switched modes; repaint. */
23
+ | 'mode'
24
+ /** The key belongs to ordinary editing; the caller should handle it. */
25
+ | 'pass';
26
+ /**
27
+ * Modal vim state over one composer.
28
+ *
29
+ * Insert mode is the default: enabling vim must not change what typing does.
30
+ * An operator key (`d`) waits for its motion, and any key that is not a valid
31
+ * continuation cancels it — vim's own rule, and the safe one for a draft.
32
+ */
33
+ export declare class Vim {
34
+ enabled: boolean;
35
+ mode: VimMode;
36
+ private operator;
37
+ private readonly undoStack;
38
+ /** Turn modal editing on or off; disabling returns to insert mode. */
39
+ setEnabled(enabled: boolean): void;
40
+ /** Whether the next key should be interpreted as a command. */
41
+ get normal(): boolean;
42
+ /** Remember the draft before an edit, so `u` can restore it. */
43
+ private snapshot;
44
+ /**
45
+ * Vim's `w`: the start of the next word.
46
+ *
47
+ * Readline's word motion (which ctrl+right uses) stops at the END of a word,
48
+ * so vim mode cannot borrow it without disagreeing with every vim user's
49
+ * muscle memory.
50
+ */
51
+ private wordForward;
52
+ /** Undo the last vim edit. */
53
+ undo(composer: Composer): void;
54
+ /**
55
+ * Handle one key.
56
+ *
57
+ * @param name - decoded key name (`h`, `esc`, `ctrl+j`, …).
58
+ * @param text - printable text the key carries, if any.
59
+ * @param composer - the buffer being edited.
60
+ */
61
+ handle(name: string, text: string, composer: Composer): VimOutcome;
62
+ /** `d` plus a motion, in terms of absolute buffers offsets. */
63
+ private deleteMotion;
64
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The `tuiHost` seam's framework-free half: what a shortcut claim and the
3
+ * status slot mean, with no Cordis import.
4
+ *
5
+ * It lives apart from `./tui-host.ts` so `npm test` can exercise it without a
6
+ * Harness on disk — the suites run from a bare `npm ci`, where `@deepseek-ai/*`
7
+ * does not resolve at all.
8
+ * @module
9
+ */
10
+ /** The name the service registers under (`ctx.tuiHost`). */
11
+ export declare const TUI_HOST_NAME = "tuiHost";
12
+ /** One plugin-registered key combination. */
13
+ export interface TuiShortcut {
14
+ /** Canonical key name the app dispatches, e.g. `ctrl+shift+g`. */
15
+ combo: string;
16
+ /** One-line description, for refusal messages and future help. */
17
+ label: string;
18
+ /** Called on the keypress; a throw is reported as a status line. */
19
+ handler: () => void;
20
+ }
21
+ /**
22
+ * Combinations the app itself owns.
23
+ *
24
+ * Built-ins win outright: a plugin that tries to take `ctrl+c` would be able to
25
+ * swallow the quit confirmation, so the registration is refused rather than
26
+ * silently ordered.
27
+ */
28
+ export declare const RESERVED_COMBOS: ReadonlySet<string>;
29
+ /** Why a registration was refused, or `undefined` when it was accepted. */
30
+ export declare function shortcutProblem(combo: string, taken: ReadonlySet<string>): string | undefined;
31
+ /** Shortcut claims, without any Cordis dependency — the testable half. */
32
+ export declare class ShortcutRegistry {
33
+ private readonly shortcuts;
34
+ /**
35
+ * Claim a key combination.
36
+ *
37
+ * @returns a disposer that releases it, or `undefined` when the combination
38
+ * is reserved, malformed, or already registered.
39
+ */
40
+ register(shortcut: TuiShortcut): (() => void) | undefined;
41
+ /** Every registered combination, in registration order. */
42
+ registered(): readonly TuiShortcut[];
43
+ /** The label for one combination, when it is claimed. */
44
+ labelOf(combo: string): string | undefined;
45
+ /**
46
+ * Run the handler for one key, if a plugin owns it.
47
+ *
48
+ * @returns whether the key was claimed, so the app can stop before it treats
49
+ * the key as text.
50
+ */
51
+ dispatch(combo: string): boolean;
52
+ }
53
+ /** The one-line status slot, also independent of Cordis. */
54
+ export declare class StatusLine {
55
+ private line;
56
+ /**
57
+ * Contribute the line. The slot is replaced, not stacked: a terminal has one
58
+ * line to give, and last registration wins — the same rule a status bar has.
59
+ */
60
+ set(text: string | undefined): () => void;
61
+ get(): string | undefined;
62
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The `tuiHost` service: the seam other plugins extend this terminal with.
3
+ *
4
+ * A plugin can own a key combination (Ctrl/Alt only, built-ins always win) and
5
+ * can contribute one line above the composer. Everything is disposer-scoped,
6
+ * so an unloaded plugin leaves nothing behind — and nothing here can take the
7
+ * keyboard away from the app's own bindings.
8
+ *
9
+ * This module is the Cordis-bound half; the rules it delegates to live in
10
+ * `./tui-host-core.ts` and are re-exported here, so `moqi-tui/tui-host`
11
+ * remains the one import a plugin needs.
12
+ * @module moqi-tui/tui-host
13
+ */
14
+ import { Service } from '@deepseek-ai/cordis';
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ import type { TuiShortcut } from './tui-host-core.ts';
17
+ export * from './tui-host-core.ts';
18
+ /**
19
+ * The extension seam (`ctx.tuiHost`). Plugins register shortcuts and a status
20
+ * line; the app dispatches keys and draws the line, and owns nothing else.
21
+ */
22
+ export declare class TuiHost extends Service {
23
+ private readonly shortcuts;
24
+ private readonly line;
25
+ constructor(ctx: Context);
26
+ /** Claim a key combination; see {@link ShortcutRegistry.register}. */
27
+ registerShortcut(shortcut: TuiShortcut): (() => void) | undefined;
28
+ /** Every registered combination, for help output and tests. */
29
+ registered(): readonly TuiShortcut[];
30
+ /** Run a key's plugin handler, if one is registered. */
31
+ dispatch(combo: string): boolean;
32
+ /** Contribute the one-line status above the composer. */
33
+ setStatusLine(text: string | undefined): () => void;
34
+ /** The status line a plugin contributed, if any. */
35
+ statusLine(): string | undefined;
36
+ }
37
+ declare module '@deepseek-ai/cordis' {
38
+ interface Context {
39
+ tuiHost: TuiHost;
40
+ }
41
+ }
42
+ export default TuiHost;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The package version, read from the `package.json` that ships beside the
3
+ * emitted code, so `--version` and `/about` always agree with the manifest
4
+ * without a second copy of the number to keep in sync.
5
+ * @module moqi-tui/version
6
+ */
7
+ /** This package's version, as `--version` prints it. */
8
+ export declare const VERSION: string;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Push-to-talk dictation, transcribed on this machine and nowhere else.
3
+ *
4
+ * The whole point of this feature is that a prompt spoken into the composer
5
+ * never becomes somebody else's training data, so there is no cloud endpoint
6
+ * here and no API key to lose: audio is captured by whatever recorder the
7
+ * system already has and handed to a local `whisper.cpp` binary. That also
8
+ * means the app cannot depend on any of it. A machine with no microphone
9
+ * stack, no whisper build, or no model is the normal case, not the error
10
+ * case — so every probe below reports what is missing in one line a person
11
+ * can act on, and the app is otherwise untouched.
12
+ *
13
+ * Nothing in this module is a runtime dependency of the bundle: the recorder
14
+ * and the transcriber are external processes discovered on `PATH`, and the
15
+ * decision logic is pure so it can be tested without either of them.
16
+ * @module
17
+ */
18
+ import type { ChildProcess } from 'node:child_process';
19
+ /** How long a transcription may run before it is killed, in milliseconds. */
20
+ export declare const TRANSCRIBE_TIMEOUT_MS = 120000;
21
+ /** How long a recorder gets to flush its WAV header after a stop, in milliseconds. */
22
+ export declare const RECORDER_FLUSH_MS = 2000;
23
+ /**
24
+ * A recorder this app knows how to drive.
25
+ *
26
+ * Every entry must produce 16 kHz mono signed 16-bit PCM, because that is the
27
+ * only format whisper.cpp reads without resampling it itself.
28
+ */
29
+ export interface RecorderSpec {
30
+ /** Executable to look for on `PATH`. */
31
+ command: string;
32
+ /** Package a person would install to get it, for the missing-dependency line. */
33
+ packageName: string;
34
+ /** Arguments that write the capture to `target`. */
35
+ args: (target: string) => string[];
36
+ }
37
+ /**
38
+ * Recorders in the order they are preferred.
39
+ *
40
+ * `arecord` comes first because on Linux it is part of alsa-utils, which is
41
+ * already installed anywhere sound works at all, and it talks to ALSA
42
+ * directly. `rec` and `sox` are the same program wearing two names; `rec`
43
+ * defaults to the system input device while `sox` has to be told `-d`, which
44
+ * is why they cannot share one entry.
45
+ */
46
+ export declare const RECORDERS: readonly RecorderSpec[];
47
+ /**
48
+ * Names whisper.cpp has shipped its command-line front end under.
49
+ *
50
+ * `whisper-cli` is what upstream builds today and `whisper-cpp` is what most
51
+ * distributions rename it to. Plain `main` is last on purpose: it is the
52
+ * historic name of the build output and still what an in-tree build produces,
53
+ * but it is far too generic to trust ahead of anything else on `PATH`.
54
+ */
55
+ export declare const WHISPER_COMMANDS: readonly string[];
56
+ /** Directories searched for a model when none was configured. */
57
+ export declare const MODEL_DIRECTORIES: readonly string[];
58
+ /**
59
+ * Model files looked for inside each directory, best first.
60
+ *
61
+ * Dictation is a handful of seconds of one speaker close to the microphone,
62
+ * which `base` already handles, so the ordering trades accuracy for the
63
+ * latency a person is standing there waiting through. English-only weights
64
+ * win their size class because they are measurably better at it.
65
+ */
66
+ export declare const MODEL_FILES: readonly string[];
67
+ /** What the user configured, from flags and the environment. */
68
+ export interface VoiceOptions {
69
+ /** `--voice-model`, an explicit path to a ggml weights file. */
70
+ model?: string;
71
+ /** `--voice-bin`, an explicit path to or name of the whisper executable. */
72
+ binary?: string;
73
+ /** `MOQI_WHISPER_MODEL`, the same thing from the environment. */
74
+ envModel?: string;
75
+ /** `MOQI_WHISPER_BIN`, the same thing from the environment. */
76
+ envBinary?: string;
77
+ /** `MOQI_WHISPER_LANG`; absent lets whisper use its own default. */
78
+ language?: string;
79
+ }
80
+ /** The filesystem questions {@link resolveVoiceSetup} needs answered. */
81
+ export interface VoiceProbe {
82
+ /** Whether this command resolves to something executable on `PATH`. */
83
+ hasCommand: (command: string) => boolean;
84
+ /** Whether this path exists and can be read. */
85
+ exists: (path: string) => boolean;
86
+ /** Home directory the `~` in {@link MODEL_DIRECTORIES} expands to. */
87
+ home: string;
88
+ }
89
+ /** The one thing that stopped voice input from being usable. */
90
+ export type VoiceGap =
91
+ /** Nothing on the system can capture audio. */
92
+ {
93
+ kind: 'recorder';
94
+ }
95
+ /** No whisper.cpp front end could be found. */
96
+ | {
97
+ kind: 'binary';
98
+ }
99
+ /** A binary was named explicitly and is not there. */
100
+ | {
101
+ kind: 'binary-missing';
102
+ path: string;
103
+ source: 'flag' | 'env';
104
+ }
105
+ /** A model was named explicitly and is not there. */
106
+ | {
107
+ kind: 'model-missing';
108
+ path: string;
109
+ source: 'flag' | 'env';
110
+ }
111
+ /** Nothing was named and the default search came up empty. */
112
+ | {
113
+ kind: 'model-unfound';
114
+ };
115
+ /** Everything needed to record and transcribe, once every probe has passed. */
116
+ export interface VoiceSetup {
117
+ recorder: RecorderSpec;
118
+ /** Command or absolute path to spawn for transcription. */
119
+ binary: string;
120
+ /** Absolute path to the ggml weights. */
121
+ model: string;
122
+ /** Language code to force, when one was configured. */
123
+ language: string | undefined;
124
+ }
125
+ /** Either a usable setup or the first thing missing from it. */
126
+ export type VoiceResolution = {
127
+ ok: true;
128
+ setup: VoiceSetup;
129
+ } | {
130
+ ok: false;
131
+ gap: VoiceGap;
132
+ };
133
+ /**
134
+ * Decide whether voice input can run, and with what.
135
+ *
136
+ * Pure apart from the injected probe, because the interesting part is the
137
+ * order the three dependencies are reported in and that is worth testing
138
+ * without a microphone: the recorder comes first because it is the one a
139
+ * person is most likely to already have, then the binary, then the weights —
140
+ * which is also the order in which they get harder to install.
141
+ */
142
+ export declare function resolveVoiceSetup(options: VoiceOptions, probe: VoiceProbe): VoiceResolution;
143
+ /** Resolve the weights from the flag, the environment, or the default search. */
144
+ export declare function resolveModel(options: VoiceOptions, probe: VoiceProbe): {
145
+ ok: true;
146
+ path: string;
147
+ } | {
148
+ ok: false;
149
+ gap: VoiceGap;
150
+ };
151
+ /**
152
+ * One line saying what is missing and how to get it.
153
+ *
154
+ * Deliberately a single sentence with a concrete next step in it: this lands
155
+ * in the footer, which is one line wide, and an error there that only says
156
+ * "voice unavailable" costs the reader a search through the README.
157
+ */
158
+ export declare function voiceGapMessage(gap: VoiceGap): string;
159
+ /**
160
+ * Arguments for one transcription run.
161
+ *
162
+ * `-np` suppresses whisper.cpp's banner and progress so the only thing left
163
+ * on stdout is the transcript; the segment timestamps are deliberately kept,
164
+ * because they are the one marker that reliably separates a result line from
165
+ * whatever a given build still prints alongside it.
166
+ */
167
+ export declare function whisperArgs(setup: VoiceSetup, wavPath: string): string[];
168
+ /**
169
+ * Turn whisper's stdout into the text a person meant to say.
170
+ *
171
+ * whisper.cpp prints one line per segment, `[00:00:00.000 --> 00:00:02.000]`
172
+ * and then the words. When any such line is present those lines are the whole
173
+ * answer and everything else is noise from a build that ignored `-np`; when
174
+ * none is, the build was asked for plain output and the lines are the text —
175
+ * minus the log chatter, which is recognisable by its `prefix:` shape.
176
+ */
177
+ export declare function parseWhisperText(stdout: string): string;
178
+ /**
179
+ * The text to splice into the composer at `cursor`.
180
+ *
181
+ * Dictation is usually appended to something already typed, and two utterances
182
+ * running together into one word is the kind of small wrongness that makes a
183
+ * feature feel broken, so a separator is added when the character before the
184
+ * cursor is not already one.
185
+ */
186
+ export declare function insertionFor(value: string, cursor: number, transcript: string): string;
187
+ /** Whether a command resolves to an executable somewhere on `PATH`. */
188
+ export declare function hasCommand(command: string, env?: NodeJS.ProcessEnv): boolean;
189
+ /** Whether a path exists and is readable. */
190
+ export declare function fileExists(path: string): boolean;
191
+ /** The probe that answers against the real filesystem. */
192
+ export declare function systemProbe(env?: NodeJS.ProcessEnv): VoiceProbe;
193
+ /** Read the environment half of the configuration. */
194
+ export declare function voiceOptionsFromEnv(env?: NodeJS.ProcessEnv): VoiceOptions;
195
+ /** A capture in progress: the recorder process and the file it is filling. */
196
+ export interface Recording {
197
+ child: ChildProcess;
198
+ wavPath: string;
199
+ }
200
+ /**
201
+ * Start the recorder.
202
+ *
203
+ * stdio is fully detached from this process's own: the app owns the alternate
204
+ * screen buffer, and a recorder writing a warning onto it would tear the frame
205
+ * apart with no way to repaint the damage.
206
+ */
207
+ export declare function startRecording(setup: VoiceSetup, wavPath: string): Recording;
208
+ /**
209
+ * Stop the recorder and wait for the file to be finished.
210
+ *
211
+ * SIGINT rather than SIGTERM because both recorders treat it as "wrap up":
212
+ * they close the WAV and go back and write the real length into the header.
213
+ * Killed harder, the file is left claiming a length of zero and whisper reads
214
+ * nothing out of it. The hard kill is only the fallback for a recorder that
215
+ * ignores the polite request.
216
+ */
217
+ export declare function stopRecording(recording: Recording): Promise<void>;
218
+ /**
219
+ * Transcribe one WAV file and return the text.
220
+ *
221
+ * Rejects with a one-line message rather than an exec error, because the
222
+ * caller puts whatever comes back straight into a status line that is one
223
+ * line wide.
224
+ */
225
+ export declare function transcribe(setup: VoiceSetup, wavPath: string): Promise<string>;
226
+ /** A one-line reason a transcription run failed. */
227
+ export declare function transcribeError(error: unknown, stderr: string): string;
package/lib/version.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The package version, read from the `package.json` that ships beside the
3
+ * emitted code, so `--version` and `/about` always agree with the manifest
4
+ * without a second copy of the number to keep in sync.
5
+ * @module moqi-tui/version
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+ /** Read the version out of `package.json`, with a safe fallback. */
9
+ function resolve() {
10
+ try {
11
+ // In `src/` the manifest is one level up; in `lib/` it is beside the
12
+ // emitted file, because `files` ships it with the bundle.
13
+ const here = new URL('.', import.meta.url);
14
+ for (const candidate of ['../package.json', 'package.json']) {
15
+ try {
16
+ const manifest = JSON.parse(readFileSync(new URL(candidate, here), 'utf8'));
17
+ if (typeof manifest.version === 'string' && manifest.version !== '') {
18
+ return manifest.version;
19
+ }
20
+ }
21
+ catch {
22
+ // Try the next candidate.
23
+ }
24
+ }
25
+ }
26
+ catch {
27
+ // Fall through to the placeholder.
28
+ }
29
+ return '0.0.0-unknown';
30
+ }
31
+ /** This package's version, as `--version` prints it. */
32
+ export const VERSION = resolve();