@kolisachint/hoocode-agent 0.4.104 → 0.4.106
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/CHANGELOG.md +32 -0
- package/dist/modes/interactive/components/voice-panel.d.ts +56 -0
- package/dist/modes/interactive/components/voice-panel.d.ts.map +1 -0
- package/dist/modes/interactive/components/voice-panel.js +195 -0
- package/dist/modes/interactive/components/voice-panel.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +39 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +262 -40
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice-transcribe.d.ts +83 -1
- package/dist/modes/interactive/voice-transcribe.d.ts.map +1 -1
- package/dist/modes/interactive/voice-transcribe.js +181 -0
- package/dist/modes/interactive/voice-transcribe.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* ERROR no model found # fatal error; process exits non-zero
|
|
10
10
|
* ```
|
|
11
11
|
*
|
|
12
|
+
* `voicetools serve` (see `VoiceDaemon` below) reuses this same line protocol
|
|
13
|
+
* plus a few daemon-only events (READY, LEVEL, PHASE).
|
|
14
|
+
*
|
|
12
15
|
* The caller wires `onSegment` to inject text into the editor (via bracketed
|
|
13
16
|
* paste) and `onStatus` / `onError` to surface feedback.
|
|
14
17
|
*/
|
|
15
|
-
export type VoiceStatus = "recording" | "transcribing" | "done" | string;
|
|
18
|
+
export type VoiceStatus = "recording" | "transcribing" | "done" | "listening" | string;
|
|
16
19
|
export interface VoiceTranscribeHandlers {
|
|
17
20
|
/** A decoded chunk of text. Injected into the editor by the caller. */
|
|
18
21
|
onSegment: (text: string) => void;
|
|
@@ -29,5 +32,84 @@ export interface VoiceSession {
|
|
|
29
32
|
stop(): void;
|
|
30
33
|
readonly running: boolean;
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Spawn `voicetools transcribe` for a single capture. This is the fallback
|
|
37
|
+
* path for binaries that don't support `serve` (see `VoiceDaemon`): every
|
|
38
|
+
* push-to-talk press pays the model load cold start.
|
|
39
|
+
*/
|
|
32
40
|
export declare function startVoiceTranscribe(bin: string, handlers: VoiceTranscribeHandlers): VoiceSession;
|
|
41
|
+
/**
|
|
42
|
+
* Handlers for a persistent `voicetools serve` daemon. Extends the base
|
|
43
|
+
* transcribe handlers with the daemon-only events:
|
|
44
|
+
* - `onReady` fires once after models finish loading.
|
|
45
|
+
* - `onLevel` per-audio-chunk RMS, for a live meter/waveform.
|
|
46
|
+
* - `onPhase` phase markers (e.g. `"silence"` when trailing silence begins).
|
|
47
|
+
* - `onPartial` interim transcript while the user speaks — the FULL growing
|
|
48
|
+
* hypothesis each time (supersedes the previous), never committed.
|
|
49
|
+
* - `onFinal` the complete committed transcript for the utterance, emitted
|
|
50
|
+
* once before DONE — this is the text to inject into the editor.
|
|
51
|
+
* - `onCrash` the process died after having been ready (caller should drop
|
|
52
|
+
* the reference and respawn lazily on the next push-to-talk).
|
|
53
|
+
*/
|
|
54
|
+
export interface VoiceDaemonHandlers extends VoiceTranscribeHandlers {
|
|
55
|
+
onReady?: () => void;
|
|
56
|
+
onLevel?: (rms: number) => void;
|
|
57
|
+
onPhase?: (phase: string) => void;
|
|
58
|
+
onPartial?: (text: string) => void;
|
|
59
|
+
onFinal?: (text: string) => void;
|
|
60
|
+
onCrash?: (message: string) => void;
|
|
61
|
+
/** Fired when the daemon has been idle (no capture) for `idleTimeoutMs`. */
|
|
62
|
+
onIdle?: () => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Outcome of {@link VoiceDaemon.spawn}. `reason: "unsupported"` means the
|
|
66
|
+
* process exited before READY with no ERROR line at all — the signature of
|
|
67
|
+
* an old binary rejecting the unrecognized `serve` subcommand — and the
|
|
68
|
+
* caller should silently fall back to `startVoiceTranscribe`. `reason:
|
|
69
|
+
* "error"` means a genuine ERROR line (or OS-level spawn failure) was seen;
|
|
70
|
+
* `handlers.onError` has already been called with it, and the caller should
|
|
71
|
+
* surface that (not retry with the legacy path, which would just hit the
|
|
72
|
+
* same failure) while leaving daemon mode available to retry next press.
|
|
73
|
+
*/
|
|
74
|
+
export type VoiceDaemonSpawnResult = {
|
|
75
|
+
ok: true;
|
|
76
|
+
daemon: VoiceDaemon;
|
|
77
|
+
} | {
|
|
78
|
+
ok: false;
|
|
79
|
+
reason: "unsupported" | "error";
|
|
80
|
+
};
|
|
81
|
+
/** Tunables passed to `voicetools serve` on spawn. Omitted fields use the binary's defaults. */
|
|
82
|
+
export interface VoiceDaemonOptions {
|
|
83
|
+
/** Trailing-silence timeout, in ms, before the binary auto-stops a capture (`--silence-ms`). */
|
|
84
|
+
silenceMs?: number;
|
|
85
|
+
/** Idle timeout, in ms. After a capture completes, the daemon auto-shuts down
|
|
86
|
+
* if no new capture starts within this window, releasing the warm model from
|
|
87
|
+
* memory. `0` disables idle shutdown (daemon stays warm forever). */
|
|
88
|
+
idleTimeoutMs?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A persistent `voicetools serve` process: models are loaded once and stay
|
|
92
|
+
* warm across captures. Only one capture runs at a time; call `startCapture`
|
|
93
|
+
* to open the mic and `cancel` to stop early. `spawn` doubles as the support
|
|
94
|
+
* probe for old binaries (see {@link VoiceDaemonSpawnResult}).
|
|
95
|
+
*/
|
|
96
|
+
export declare class VoiceDaemon {
|
|
97
|
+
private readonly proc;
|
|
98
|
+
private readonly handlers;
|
|
99
|
+
private closed;
|
|
100
|
+
private idleTimer;
|
|
101
|
+
private readonly idleTimeoutMs;
|
|
102
|
+
private constructor();
|
|
103
|
+
get isReady(): boolean;
|
|
104
|
+
static spawn(bin: string, handlers: VoiceDaemonHandlers, options?: VoiceDaemonOptions): Promise<VoiceDaemonSpawnResult>;
|
|
105
|
+
private startIdleTimer;
|
|
106
|
+
private stopIdleTimer;
|
|
107
|
+
private handleLine;
|
|
108
|
+
/** Begin a capture: opens the mic, streams PARTIAL/FINAL (or SEGMENT), ends with DONE. */
|
|
109
|
+
startCapture(): void;
|
|
110
|
+
/** Cancel the in-flight capture, if any. Idempotent. */
|
|
111
|
+
cancel(): void;
|
|
112
|
+
/** Ask the daemon to exit gracefully, force-killing if it doesn't within 1s. */
|
|
113
|
+
shutdown(): void;
|
|
114
|
+
}
|
|
33
115
|
//# sourceMappingURL=voice-transcribe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-transcribe.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/voice-transcribe.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,uBAAuB;IACvC,uEAAuE;IACvE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,2EAA2E;IAC3E,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAUD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,uBAAuB,GAAG,YAAY,CAoEjG","sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { type ChildProcess, spawn } from \"child_process\";\n\n/**\n * Drives the external `voicetools` binary and streams its stdout line protocol\n * into callbacks. One line per event on stdout (stderr is free for debug logs):\n *\n * ```text\n * STATUS recording # state transition (recording | transcribing | ...)\n * SEGMENT hello world # a chunk of decoded text\n * DONE # finished successfully\n * ERROR no model found # fatal error; process exits non-zero\n * ```\n *\n * The caller wires `onSegment` to inject text into the editor (via bracketed\n * paste) and `onStatus` / `onError` to surface feedback.\n */\n\nexport type VoiceStatus = \"recording\" | \"transcribing\" | \"done\" | string;\n\nexport interface VoiceTranscribeHandlers {\n\t/** A decoded chunk of text. Injected into the editor by the caller. */\n\tonSegment: (text: string) => void;\n\t/** A state transition reported by the binary, or `\"done\"` on completion. */\n\tonStatus: (status: VoiceStatus) => void;\n\t/** A fatal error: spawn failure, protocol ERROR line, or non-zero exit. */\n\tonError: (message: string) => void;\n}\n\n/**\n * A running voice-transcribe session. Call `stop()` to cancel early (e.g. the\n * user pressing the shortcut again). `stop()` is idempotent.\n */\nexport interface VoiceSession {\n\tstop(): void;\n\treadonly running: boolean;\n}\n\n/** Build a friendly message for a spawn failure, calling out a missing binary. */\nfunction describeSpawnError(err: unknown, bin: string): string {\n\tif (err && typeof err === \"object\" && (err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\treturn `voicetools binary not found (tried \"${bin}\"). Install it or set VOICETOOLS_BIN to its path.`;\n\t}\n\treturn err instanceof Error ? err.message : String(err);\n}\n\nexport function startVoiceTranscribe(bin: string, handlers: VoiceTranscribeHandlers): VoiceSession {\n\tlet proc: ChildProcess;\n\ttry {\n\t\tproc = spawn(bin, [\"transcribe\"], {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t} catch (err) {\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\tlet stopped = false;\n\tlet finished = false;\n\tlet rl: Interface | undefined;\n\n\tconst finish = (): void => {\n\t\tif (finished) return;\n\t\tfinished = true;\n\t\trl?.close();\n\t};\n\n\tif (!proc.stdout) {\n\t\tproc.kill();\n\t\thandlers.onError(\"failed to capture voicetools stdout\");\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\trl = createInterface({ input: proc.stdout });\n\trl.on(\"line\", (line) => {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\thandlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\thandlers.onSegment(line.slice(8));\n\t\t} else if (line === \"DONE\") {\n\t\t\thandlers.onStatus(\"done\");\n\t\t\tfinish();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\tfinish();\n\t\t}\n\t});\n\n\tproc.on(\"error\", (err) => {\n\t\tif (stopped || finished) return;\n\t\tfinish();\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t});\n\n\tproc.on(\"close\", (code) => {\n\t\tconst wasFinished = finished;\n\t\tfinish();\n\t\tif (stopped || wasFinished) return;\n\t\tif (code && code !== 0) {\n\t\t\thandlers.onError(`voicetools exited with code ${code}`);\n\t\t}\n\t});\n\n\treturn {\n\t\tstop: () => {\n\t\t\tif (stopped) return;\n\t\t\tstopped = true;\n\t\t\tfinish();\n\t\t\tproc.kill();\n\t\t},\n\t\tget running() {\n\t\t\treturn !finished && !stopped;\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"voice-transcribe.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/voice-transcribe.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAEvF,MAAM,WAAW,uBAAuB;IACvC,uEAAuE;IACvE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,2EAA2E;IAC3E,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAUD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,uBAAuB,GAAG,YAAY,CAoEjG;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,sBAAsB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAA;CAAE,CAAC;AAExH,gGAAgG;AAChG,MAAM,WAAW,kBAAkB;IAClC,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;yEAEqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IAMtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAN1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,OAAO,eAMN;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,MAAM,CAAC,KAAK,CACX,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,sBAAsB,CAAC,CA8EjC;IAED,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,UAAU;IAsBlB,0FAA0F;IAC1F,YAAY,IAAI,IAAI,CAInB;IAED,wDAAwD;IACxD,MAAM,IAAI,IAAI,CAGb;IAED,gFAAgF;IAChF,QAAQ,IAAI,IAAI,CAcf;CACD","sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { type ChildProcess, spawn } from \"child_process\";\n\n/**\n * Drives the external `voicetools` binary and streams its stdout line protocol\n * into callbacks. One line per event on stdout (stderr is free for debug logs):\n *\n * ```text\n * STATUS recording # state transition (recording | transcribing | ...)\n * SEGMENT hello world # a chunk of decoded text\n * DONE # finished successfully\n * ERROR no model found # fatal error; process exits non-zero\n * ```\n *\n * `voicetools serve` (see `VoiceDaemon` below) reuses this same line protocol\n * plus a few daemon-only events (READY, LEVEL, PHASE).\n *\n * The caller wires `onSegment` to inject text into the editor (via bracketed\n * paste) and `onStatus` / `onError` to surface feedback.\n */\n\nexport type VoiceStatus = \"recording\" | \"transcribing\" | \"done\" | \"listening\" | string;\n\nexport interface VoiceTranscribeHandlers {\n\t/** A decoded chunk of text. Injected into the editor by the caller. */\n\tonSegment: (text: string) => void;\n\t/** A state transition reported by the binary, or `\"done\"` on completion. */\n\tonStatus: (status: VoiceStatus) => void;\n\t/** A fatal error: spawn failure, protocol ERROR line, or non-zero exit. */\n\tonError: (message: string) => void;\n}\n\n/**\n * A running voice-transcribe session. Call `stop()` to cancel early (e.g. the\n * user pressing the shortcut again). `stop()` is idempotent.\n */\nexport interface VoiceSession {\n\tstop(): void;\n\treadonly running: boolean;\n}\n\n/** Build a friendly message for a spawn failure, calling out a missing binary. */\nfunction describeSpawnError(err: unknown, bin: string): string {\n\tif (err && typeof err === \"object\" && (err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\treturn `voicetools binary not found (tried \"${bin}\"). Install it or set VOICETOOLS_BIN to its path.`;\n\t}\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Spawn `voicetools transcribe` for a single capture. This is the fallback\n * path for binaries that don't support `serve` (see `VoiceDaemon`): every\n * push-to-talk press pays the model load cold start.\n */\nexport function startVoiceTranscribe(bin: string, handlers: VoiceTranscribeHandlers): VoiceSession {\n\tlet proc: ChildProcess;\n\ttry {\n\t\tproc = spawn(bin, [\"transcribe\"], {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t} catch (err) {\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\tlet stopped = false;\n\tlet finished = false;\n\tlet rl: Interface | undefined;\n\n\tconst finish = (): void => {\n\t\tif (finished) return;\n\t\tfinished = true;\n\t\trl?.close();\n\t};\n\n\tif (!proc.stdout) {\n\t\tproc.kill();\n\t\thandlers.onError(\"failed to capture voicetools stdout\");\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\trl = createInterface({ input: proc.stdout });\n\trl.on(\"line\", (line) => {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\thandlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\thandlers.onSegment(line.slice(8));\n\t\t} else if (line === \"DONE\") {\n\t\t\thandlers.onStatus(\"done\");\n\t\t\tfinish();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\tfinish();\n\t\t}\n\t});\n\n\tproc.on(\"error\", (err) => {\n\t\tif (stopped || finished) return;\n\t\tfinish();\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t});\n\n\tproc.on(\"close\", (code) => {\n\t\tconst wasFinished = finished;\n\t\tfinish();\n\t\tif (stopped || wasFinished) return;\n\t\tif (code && code !== 0) {\n\t\t\thandlers.onError(`voicetools exited with code ${code}`);\n\t\t}\n\t});\n\n\treturn {\n\t\tstop: () => {\n\t\t\tif (stopped) return;\n\t\t\tstopped = true;\n\t\t\tfinish();\n\t\t\tproc.kill();\n\t\t},\n\t\tget running() {\n\t\t\treturn !finished && !stopped;\n\t\t},\n\t};\n}\n\n/**\n * Handlers for a persistent `voicetools serve` daemon. Extends the base\n * transcribe handlers with the daemon-only events:\n * - `onReady` fires once after models finish loading.\n * - `onLevel` per-audio-chunk RMS, for a live meter/waveform.\n * - `onPhase` phase markers (e.g. `\"silence\"` when trailing silence begins).\n * - `onPartial` interim transcript while the user speaks — the FULL growing\n * hypothesis each time (supersedes the previous), never committed.\n * - `onFinal` the complete committed transcript for the utterance, emitted\n * once before DONE — this is the text to inject into the editor.\n * - `onCrash` the process died after having been ready (caller should drop\n * the reference and respawn lazily on the next push-to-talk).\n */\nexport interface VoiceDaemonHandlers extends VoiceTranscribeHandlers {\n\tonReady?: () => void;\n\tonLevel?: (rms: number) => void;\n\tonPhase?: (phase: string) => void;\n\tonPartial?: (text: string) => void;\n\tonFinal?: (text: string) => void;\n\tonCrash?: (message: string) => void;\n\t/** Fired when the daemon has been idle (no capture) for `idleTimeoutMs`. */\n\tonIdle?: () => void;\n}\n\n/**\n * Outcome of {@link VoiceDaemon.spawn}. `reason: \"unsupported\"` means the\n * process exited before READY with no ERROR line at all — the signature of\n * an old binary rejecting the unrecognized `serve` subcommand — and the\n * caller should silently fall back to `startVoiceTranscribe`. `reason:\n * \"error\"` means a genuine ERROR line (or OS-level spawn failure) was seen;\n * `handlers.onError` has already been called with it, and the caller should\n * surface that (not retry with the legacy path, which would just hit the\n * same failure) while leaving daemon mode available to retry next press.\n */\nexport type VoiceDaemonSpawnResult = { ok: true; daemon: VoiceDaemon } | { ok: false; reason: \"unsupported\" | \"error\" };\n\n/** Tunables passed to `voicetools serve` on spawn. Omitted fields use the binary's defaults. */\nexport interface VoiceDaemonOptions {\n\t/** Trailing-silence timeout, in ms, before the binary auto-stops a capture (`--silence-ms`). */\n\tsilenceMs?: number;\n\t/** Idle timeout, in ms. After a capture completes, the daemon auto-shuts down\n\t * if no new capture starts within this window, releasing the warm model from\n\t * memory. `0` disables idle shutdown (daemon stays warm forever). */\n\tidleTimeoutMs?: number;\n}\n\n/**\n * A persistent `voicetools serve` process: models are loaded once and stay\n * warm across captures. Only one capture runs at a time; call `startCapture`\n * to open the mic and `cancel` to stop early. `spawn` doubles as the support\n * probe for old binaries (see {@link VoiceDaemonSpawnResult}).\n */\nexport class VoiceDaemon {\n\tprivate closed = false;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate readonly idleTimeoutMs: number;\n\n\tprivate constructor(\n\t\tprivate readonly proc: ChildProcess,\n\t\tprivate readonly handlers: VoiceDaemonHandlers,\n\t\tidleTimeoutMs: number,\n\t) {\n\t\tthis.idleTimeoutMs = idleTimeoutMs;\n\t}\n\n\tget isReady(): boolean {\n\t\treturn !this.closed;\n\t}\n\n\tstatic spawn(\n\t\tbin: string,\n\t\thandlers: VoiceDaemonHandlers,\n\t\toptions?: VoiceDaemonOptions,\n\t): Promise<VoiceDaemonSpawnResult> {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst args = [\"serve\"];\n\t\t\tif (options?.silenceMs !== undefined) {\n\t\t\t\targs.push(\"--silence-ms\", String(Math.max(0, Math.round(options.silenceMs))));\n\t\t\t}\n\t\t\tlet proc: ChildProcess;\n\t\t\ttry {\n\t\t\t\tproc = spawn(bin, args, { stdio: [\"pipe\", \"pipe\", \"ignore\"] });\n\t\t\t} catch (err) {\n\t\t\t\thandlers.onError(describeSpawnError(err, bin));\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!proc.stdout || !proc.stdin) {\n\t\t\t\tproc.kill();\n\t\t\t\thandlers.onError(\"failed to open voicetools serve stdio\");\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tlet daemon: VoiceDaemon | undefined;\n\t\t\tlet sawPreReadyError = false;\n\t\t\tconst rl = createInterface({ input: proc.stdout });\n\n\t\t\tconst fail = (reason: \"unsupported\" | \"error\"): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\trl.close();\n\t\t\t\tresolve({ ok: false, reason });\n\t\t\t};\n\n\t\t\trl.on(\"line\", (line) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tdaemon.handleLine(line);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (settled) return;\n\t\t\t\tif (line === \"READY\") {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tdaemon = new VoiceDaemon(proc, handlers, options?.idleTimeoutMs ?? 0);\n\t\t\t\t\thandlers.onReady?.();\n\t\t\t\t\tresolve({ ok: true, daemon });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (line.startsWith(\"ERROR \")) {\n\t\t\t\t\t// Loading can fail before READY (e.g. no model installed yet).\n\t\t\t\t\t// Surface it now; the process still exits right after.\n\t\t\t\t\tsawPreReadyError = true;\n\t\t\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"error\", (err) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(describeSpawnError(err, bin));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!sawPreReadyError) handlers.onError(describeSpawnError(err, bin));\n\t\t\t\tfail(\"error\");\n\t\t\t});\n\n\t\t\tproc.on(\"close\", (code) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(`voicetools serve exited with code ${code ?? \"unknown\"}`);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfail(sawPreReadyError ? \"error\" : \"unsupported\");\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate startIdleTimer(): void {\n\t\tthis.stopIdleTimer();\n\t\tif (this.idleTimeoutMs <= 0 || this.closed) return;\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tthis.idleTimer = undefined;\n\t\t\tif (this.closed) return;\n\t\t\tthis.handlers.onIdle?.();\n\t\t}, this.idleTimeoutMs);\n\t}\n\n\tprivate stopIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = undefined;\n\t\t}\n\t}\n\n\tprivate handleLine(line: string): void {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\tthis.handlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"PARTIAL \")) {\n\t\t\tthis.handlers.onPartial?.(line.slice(8));\n\t\t} else if (line.startsWith(\"FINAL \")) {\n\t\t\tthis.handlers.onFinal?.(line.slice(6));\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\tthis.handlers.onSegment(line.slice(8));\n\t\t} else if (line.startsWith(\"LEVEL \")) {\n\t\t\tconst rms = Number.parseFloat(line.slice(6).trim());\n\t\t\tif (!Number.isNaN(rms)) this.handlers.onLevel?.(rms);\n\t\t} else if (line.startsWith(\"PHASE \")) {\n\t\t\tthis.handlers.onPhase?.(line.slice(6).trim());\n\t\t} else if (line === \"DONE\") {\n\t\t\tthis.handlers.onStatus(\"done\");\n\t\t\tthis.startIdleTimer();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\tthis.handlers.onError(line.slice(6).trim());\n\t\t}\n\t}\n\n\t/** Begin a capture: opens the mic, streams PARTIAL/FINAL (or SEGMENT), ends with DONE. */\n\tstartCapture(): void {\n\t\tif (this.closed) return;\n\t\tthis.stopIdleTimer();\n\t\tthis.proc.stdin?.write(\"START\\n\");\n\t}\n\n\t/** Cancel the in-flight capture, if any. Idempotent. */\n\tcancel(): void {\n\t\tif (this.closed) return;\n\t\tthis.proc.stdin?.write(\"CANCEL\\n\");\n\t}\n\n\t/** Ask the daemon to exit gracefully, force-killing if it doesn't within 1s. */\n\tshutdown(): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.stopIdleTimer();\n\t\ttry {\n\t\t\tthis.proc.stdin?.write(\"SHUTDOWN\\n\");\n\t\t} catch {\n\t\t\t// stdin may already be gone (process died); force-kill below covers it.\n\t\t}\n\t\tconst proc = this.proc;\n\t\tconst killTimer = setTimeout(() => {\n\t\t\tif (!proc.killed) proc.kill();\n\t\t}, 1000);\n\t\tproc.once(\"close\", () => clearTimeout(killTimer));\n\t}\n}\n"]}
|
|
@@ -7,6 +7,11 @@ function describeSpawnError(err, bin) {
|
|
|
7
7
|
}
|
|
8
8
|
return err instanceof Error ? err.message : String(err);
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Spawn `voicetools transcribe` for a single capture. This is the fallback
|
|
12
|
+
* path for binaries that don't support `serve` (see `VoiceDaemon`): every
|
|
13
|
+
* push-to-talk press pays the model load cold start.
|
|
14
|
+
*/
|
|
10
15
|
export function startVoiceTranscribe(bin, handlers) {
|
|
11
16
|
let proc;
|
|
12
17
|
try {
|
|
@@ -77,4 +82,180 @@ export function startVoiceTranscribe(bin, handlers) {
|
|
|
77
82
|
},
|
|
78
83
|
};
|
|
79
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* A persistent `voicetools serve` process: models are loaded once and stay
|
|
87
|
+
* warm across captures. Only one capture runs at a time; call `startCapture`
|
|
88
|
+
* to open the mic and `cancel` to stop early. `spawn` doubles as the support
|
|
89
|
+
* probe for old binaries (see {@link VoiceDaemonSpawnResult}).
|
|
90
|
+
*/
|
|
91
|
+
export class VoiceDaemon {
|
|
92
|
+
proc;
|
|
93
|
+
handlers;
|
|
94
|
+
closed = false;
|
|
95
|
+
idleTimer;
|
|
96
|
+
idleTimeoutMs;
|
|
97
|
+
constructor(proc, handlers, idleTimeoutMs) {
|
|
98
|
+
this.proc = proc;
|
|
99
|
+
this.handlers = handlers;
|
|
100
|
+
this.idleTimeoutMs = idleTimeoutMs;
|
|
101
|
+
}
|
|
102
|
+
get isReady() {
|
|
103
|
+
return !this.closed;
|
|
104
|
+
}
|
|
105
|
+
static spawn(bin, handlers, options) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const args = ["serve"];
|
|
108
|
+
if (options?.silenceMs !== undefined) {
|
|
109
|
+
args.push("--silence-ms", String(Math.max(0, Math.round(options.silenceMs))));
|
|
110
|
+
}
|
|
111
|
+
let proc;
|
|
112
|
+
try {
|
|
113
|
+
proc = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
handlers.onError(describeSpawnError(err, bin));
|
|
117
|
+
resolve({ ok: false, reason: "error" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!proc.stdout || !proc.stdin) {
|
|
121
|
+
proc.kill();
|
|
122
|
+
handlers.onError("failed to open voicetools serve stdio");
|
|
123
|
+
resolve({ ok: false, reason: "error" });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let settled = false;
|
|
127
|
+
let daemon;
|
|
128
|
+
let sawPreReadyError = false;
|
|
129
|
+
const rl = createInterface({ input: proc.stdout });
|
|
130
|
+
const fail = (reason) => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
settled = true;
|
|
134
|
+
rl.close();
|
|
135
|
+
resolve({ ok: false, reason });
|
|
136
|
+
};
|
|
137
|
+
rl.on("line", (line) => {
|
|
138
|
+
if (daemon) {
|
|
139
|
+
daemon.handleLine(line);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (settled)
|
|
143
|
+
return;
|
|
144
|
+
if (line === "READY") {
|
|
145
|
+
settled = true;
|
|
146
|
+
daemon = new VoiceDaemon(proc, handlers, options?.idleTimeoutMs ?? 0);
|
|
147
|
+
handlers.onReady?.();
|
|
148
|
+
resolve({ ok: true, daemon });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (line.startsWith("ERROR ")) {
|
|
152
|
+
// Loading can fail before READY (e.g. no model installed yet).
|
|
153
|
+
// Surface it now; the process still exits right after.
|
|
154
|
+
sawPreReadyError = true;
|
|
155
|
+
handlers.onError(line.slice(6).trim());
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
proc.on("error", (err) => {
|
|
159
|
+
if (daemon) {
|
|
160
|
+
if (!daemon.closed) {
|
|
161
|
+
daemon.closed = true;
|
|
162
|
+
handlers.onCrash?.(describeSpawnError(err, bin));
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!sawPreReadyError)
|
|
167
|
+
handlers.onError(describeSpawnError(err, bin));
|
|
168
|
+
fail("error");
|
|
169
|
+
});
|
|
170
|
+
proc.on("close", (code) => {
|
|
171
|
+
if (daemon) {
|
|
172
|
+
if (!daemon.closed) {
|
|
173
|
+
daemon.closed = true;
|
|
174
|
+
handlers.onCrash?.(`voicetools serve exited with code ${code ?? "unknown"}`);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
fail(sawPreReadyError ? "error" : "unsupported");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
startIdleTimer() {
|
|
183
|
+
this.stopIdleTimer();
|
|
184
|
+
if (this.idleTimeoutMs <= 0 || this.closed)
|
|
185
|
+
return;
|
|
186
|
+
this.idleTimer = setTimeout(() => {
|
|
187
|
+
this.idleTimer = undefined;
|
|
188
|
+
if (this.closed)
|
|
189
|
+
return;
|
|
190
|
+
this.handlers.onIdle?.();
|
|
191
|
+
}, this.idleTimeoutMs);
|
|
192
|
+
}
|
|
193
|
+
stopIdleTimer() {
|
|
194
|
+
if (this.idleTimer) {
|
|
195
|
+
clearTimeout(this.idleTimer);
|
|
196
|
+
this.idleTimer = undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
handleLine(line) {
|
|
200
|
+
if (line.startsWith("STATUS ")) {
|
|
201
|
+
this.handlers.onStatus(line.slice(7).trim());
|
|
202
|
+
}
|
|
203
|
+
else if (line.startsWith("PARTIAL ")) {
|
|
204
|
+
this.handlers.onPartial?.(line.slice(8));
|
|
205
|
+
}
|
|
206
|
+
else if (line.startsWith("FINAL ")) {
|
|
207
|
+
this.handlers.onFinal?.(line.slice(6));
|
|
208
|
+
}
|
|
209
|
+
else if (line.startsWith("SEGMENT ")) {
|
|
210
|
+
this.handlers.onSegment(line.slice(8));
|
|
211
|
+
}
|
|
212
|
+
else if (line.startsWith("LEVEL ")) {
|
|
213
|
+
const rms = Number.parseFloat(line.slice(6).trim());
|
|
214
|
+
if (!Number.isNaN(rms))
|
|
215
|
+
this.handlers.onLevel?.(rms);
|
|
216
|
+
}
|
|
217
|
+
else if (line.startsWith("PHASE ")) {
|
|
218
|
+
this.handlers.onPhase?.(line.slice(6).trim());
|
|
219
|
+
}
|
|
220
|
+
else if (line === "DONE") {
|
|
221
|
+
this.handlers.onStatus("done");
|
|
222
|
+
this.startIdleTimer();
|
|
223
|
+
}
|
|
224
|
+
else if (line.startsWith("ERROR ")) {
|
|
225
|
+
this.handlers.onError(line.slice(6).trim());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Begin a capture: opens the mic, streams PARTIAL/FINAL (or SEGMENT), ends with DONE. */
|
|
229
|
+
startCapture() {
|
|
230
|
+
if (this.closed)
|
|
231
|
+
return;
|
|
232
|
+
this.stopIdleTimer();
|
|
233
|
+
this.proc.stdin?.write("START\n");
|
|
234
|
+
}
|
|
235
|
+
/** Cancel the in-flight capture, if any. Idempotent. */
|
|
236
|
+
cancel() {
|
|
237
|
+
if (this.closed)
|
|
238
|
+
return;
|
|
239
|
+
this.proc.stdin?.write("CANCEL\n");
|
|
240
|
+
}
|
|
241
|
+
/** Ask the daemon to exit gracefully, force-killing if it doesn't within 1s. */
|
|
242
|
+
shutdown() {
|
|
243
|
+
if (this.closed)
|
|
244
|
+
return;
|
|
245
|
+
this.closed = true;
|
|
246
|
+
this.stopIdleTimer();
|
|
247
|
+
try {
|
|
248
|
+
this.proc.stdin?.write("SHUTDOWN\n");
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// stdin may already be gone (process died); force-kill below covers it.
|
|
252
|
+
}
|
|
253
|
+
const proc = this.proc;
|
|
254
|
+
const killTimer = setTimeout(() => {
|
|
255
|
+
if (!proc.killed)
|
|
256
|
+
proc.kill();
|
|
257
|
+
}, 1000);
|
|
258
|
+
proc.once("close", () => clearTimeout(killTimer));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
80
261
|
//# sourceMappingURL=voice-transcribe.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-transcribe.js","sourceRoot":"","sources":["../../../src/modes/interactive/voice-transcribe.ts"],"names":[],"mappings":"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","sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { type ChildProcess, spawn } from \"child_process\";\n\n/**\n * Drives the external `voicetools` binary and streams its stdout line protocol\n * into callbacks. One line per event on stdout (stderr is free for debug logs):\n *\n * ```text\n * STATUS recording # state transition (recording | transcribing | ...)\n * SEGMENT hello world # a chunk of decoded text\n * DONE # finished successfully\n * ERROR no model found # fatal error; process exits non-zero\n * ```\n *\n * The caller wires `onSegment` to inject text into the editor (via bracketed\n * paste) and `onStatus` / `onError` to surface feedback.\n */\n\nexport type VoiceStatus = \"recording\" | \"transcribing\" | \"done\" | string;\n\nexport interface VoiceTranscribeHandlers {\n\t/** A decoded chunk of text. Injected into the editor by the caller. */\n\tonSegment: (text: string) => void;\n\t/** A state transition reported by the binary, or `\"done\"` on completion. */\n\tonStatus: (status: VoiceStatus) => void;\n\t/** A fatal error: spawn failure, protocol ERROR line, or non-zero exit. */\n\tonError: (message: string) => void;\n}\n\n/**\n * A running voice-transcribe session. Call `stop()` to cancel early (e.g. the\n * user pressing the shortcut again). `stop()` is idempotent.\n */\nexport interface VoiceSession {\n\tstop(): void;\n\treadonly running: boolean;\n}\n\n/** Build a friendly message for a spawn failure, calling out a missing binary. */\nfunction describeSpawnError(err: unknown, bin: string): string {\n\tif (err && typeof err === \"object\" && (err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\treturn `voicetools binary not found (tried \"${bin}\"). Install it or set VOICETOOLS_BIN to its path.`;\n\t}\n\treturn err instanceof Error ? err.message : String(err);\n}\n\nexport function startVoiceTranscribe(bin: string, handlers: VoiceTranscribeHandlers): VoiceSession {\n\tlet proc: ChildProcess;\n\ttry {\n\t\tproc = spawn(bin, [\"transcribe\"], {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t} catch (err) {\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\tlet stopped = false;\n\tlet finished = false;\n\tlet rl: Interface | undefined;\n\n\tconst finish = (): void => {\n\t\tif (finished) return;\n\t\tfinished = true;\n\t\trl?.close();\n\t};\n\n\tif (!proc.stdout) {\n\t\tproc.kill();\n\t\thandlers.onError(\"failed to capture voicetools stdout\");\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\trl = createInterface({ input: proc.stdout });\n\trl.on(\"line\", (line) => {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\thandlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\thandlers.onSegment(line.slice(8));\n\t\t} else if (line === \"DONE\") {\n\t\t\thandlers.onStatus(\"done\");\n\t\t\tfinish();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\tfinish();\n\t\t}\n\t});\n\n\tproc.on(\"error\", (err) => {\n\t\tif (stopped || finished) return;\n\t\tfinish();\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t});\n\n\tproc.on(\"close\", (code) => {\n\t\tconst wasFinished = finished;\n\t\tfinish();\n\t\tif (stopped || wasFinished) return;\n\t\tif (code && code !== 0) {\n\t\t\thandlers.onError(`voicetools exited with code ${code}`);\n\t\t}\n\t});\n\n\treturn {\n\t\tstop: () => {\n\t\t\tif (stopped) return;\n\t\t\tstopped = true;\n\t\t\tfinish();\n\t\t\tproc.kill();\n\t\t},\n\t\tget running() {\n\t\t\treturn !finished && !stopped;\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"voice-transcribe.js","sourceRoot":"","sources":["../../../src/modes/interactive/voice-transcribe.ts"],"names":[],"mappings":"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","sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { type ChildProcess, spawn } from \"child_process\";\n\n/**\n * Drives the external `voicetools` binary and streams its stdout line protocol\n * into callbacks. One line per event on stdout (stderr is free for debug logs):\n *\n * ```text\n * STATUS recording # state transition (recording | transcribing | ...)\n * SEGMENT hello world # a chunk of decoded text\n * DONE # finished successfully\n * ERROR no model found # fatal error; process exits non-zero\n * ```\n *\n * `voicetools serve` (see `VoiceDaemon` below) reuses this same line protocol\n * plus a few daemon-only events (READY, LEVEL, PHASE).\n *\n * The caller wires `onSegment` to inject text into the editor (via bracketed\n * paste) and `onStatus` / `onError` to surface feedback.\n */\n\nexport type VoiceStatus = \"recording\" | \"transcribing\" | \"done\" | \"listening\" | string;\n\nexport interface VoiceTranscribeHandlers {\n\t/** A decoded chunk of text. Injected into the editor by the caller. */\n\tonSegment: (text: string) => void;\n\t/** A state transition reported by the binary, or `\"done\"` on completion. */\n\tonStatus: (status: VoiceStatus) => void;\n\t/** A fatal error: spawn failure, protocol ERROR line, or non-zero exit. */\n\tonError: (message: string) => void;\n}\n\n/**\n * A running voice-transcribe session. Call `stop()` to cancel early (e.g. the\n * user pressing the shortcut again). `stop()` is idempotent.\n */\nexport interface VoiceSession {\n\tstop(): void;\n\treadonly running: boolean;\n}\n\n/** Build a friendly message for a spawn failure, calling out a missing binary. */\nfunction describeSpawnError(err: unknown, bin: string): string {\n\tif (err && typeof err === \"object\" && (err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\treturn `voicetools binary not found (tried \"${bin}\"). Install it or set VOICETOOLS_BIN to its path.`;\n\t}\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Spawn `voicetools transcribe` for a single capture. This is the fallback\n * path for binaries that don't support `serve` (see `VoiceDaemon`): every\n * push-to-talk press pays the model load cold start.\n */\nexport function startVoiceTranscribe(bin: string, handlers: VoiceTranscribeHandlers): VoiceSession {\n\tlet proc: ChildProcess;\n\ttry {\n\t\tproc = spawn(bin, [\"transcribe\"], {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t} catch (err) {\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\tlet stopped = false;\n\tlet finished = false;\n\tlet rl: Interface | undefined;\n\n\tconst finish = (): void => {\n\t\tif (finished) return;\n\t\tfinished = true;\n\t\trl?.close();\n\t};\n\n\tif (!proc.stdout) {\n\t\tproc.kill();\n\t\thandlers.onError(\"failed to capture voicetools stdout\");\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\trl = createInterface({ input: proc.stdout });\n\trl.on(\"line\", (line) => {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\thandlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\thandlers.onSegment(line.slice(8));\n\t\t} else if (line === \"DONE\") {\n\t\t\thandlers.onStatus(\"done\");\n\t\t\tfinish();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\tfinish();\n\t\t}\n\t});\n\n\tproc.on(\"error\", (err) => {\n\t\tif (stopped || finished) return;\n\t\tfinish();\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t});\n\n\tproc.on(\"close\", (code) => {\n\t\tconst wasFinished = finished;\n\t\tfinish();\n\t\tif (stopped || wasFinished) return;\n\t\tif (code && code !== 0) {\n\t\t\thandlers.onError(`voicetools exited with code ${code}`);\n\t\t}\n\t});\n\n\treturn {\n\t\tstop: () => {\n\t\t\tif (stopped) return;\n\t\t\tstopped = true;\n\t\t\tfinish();\n\t\t\tproc.kill();\n\t\t},\n\t\tget running() {\n\t\t\treturn !finished && !stopped;\n\t\t},\n\t};\n}\n\n/**\n * Handlers for a persistent `voicetools serve` daemon. Extends the base\n * transcribe handlers with the daemon-only events:\n * - `onReady` fires once after models finish loading.\n * - `onLevel` per-audio-chunk RMS, for a live meter/waveform.\n * - `onPhase` phase markers (e.g. `\"silence\"` when trailing silence begins).\n * - `onPartial` interim transcript while the user speaks — the FULL growing\n * hypothesis each time (supersedes the previous), never committed.\n * - `onFinal` the complete committed transcript for the utterance, emitted\n * once before DONE — this is the text to inject into the editor.\n * - `onCrash` the process died after having been ready (caller should drop\n * the reference and respawn lazily on the next push-to-talk).\n */\nexport interface VoiceDaemonHandlers extends VoiceTranscribeHandlers {\n\tonReady?: () => void;\n\tonLevel?: (rms: number) => void;\n\tonPhase?: (phase: string) => void;\n\tonPartial?: (text: string) => void;\n\tonFinal?: (text: string) => void;\n\tonCrash?: (message: string) => void;\n\t/** Fired when the daemon has been idle (no capture) for `idleTimeoutMs`. */\n\tonIdle?: () => void;\n}\n\n/**\n * Outcome of {@link VoiceDaemon.spawn}. `reason: \"unsupported\"` means the\n * process exited before READY with no ERROR line at all — the signature of\n * an old binary rejecting the unrecognized `serve` subcommand — and the\n * caller should silently fall back to `startVoiceTranscribe`. `reason:\n * \"error\"` means a genuine ERROR line (or OS-level spawn failure) was seen;\n * `handlers.onError` has already been called with it, and the caller should\n * surface that (not retry with the legacy path, which would just hit the\n * same failure) while leaving daemon mode available to retry next press.\n */\nexport type VoiceDaemonSpawnResult = { ok: true; daemon: VoiceDaemon } | { ok: false; reason: \"unsupported\" | \"error\" };\n\n/** Tunables passed to `voicetools serve` on spawn. Omitted fields use the binary's defaults. */\nexport interface VoiceDaemonOptions {\n\t/** Trailing-silence timeout, in ms, before the binary auto-stops a capture (`--silence-ms`). */\n\tsilenceMs?: number;\n\t/** Idle timeout, in ms. After a capture completes, the daemon auto-shuts down\n\t * if no new capture starts within this window, releasing the warm model from\n\t * memory. `0` disables idle shutdown (daemon stays warm forever). */\n\tidleTimeoutMs?: number;\n}\n\n/**\n * A persistent `voicetools serve` process: models are loaded once and stay\n * warm across captures. Only one capture runs at a time; call `startCapture`\n * to open the mic and `cancel` to stop early. `spawn` doubles as the support\n * probe for old binaries (see {@link VoiceDaemonSpawnResult}).\n */\nexport class VoiceDaemon {\n\tprivate closed = false;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate readonly idleTimeoutMs: number;\n\n\tprivate constructor(\n\t\tprivate readonly proc: ChildProcess,\n\t\tprivate readonly handlers: VoiceDaemonHandlers,\n\t\tidleTimeoutMs: number,\n\t) {\n\t\tthis.idleTimeoutMs = idleTimeoutMs;\n\t}\n\n\tget isReady(): boolean {\n\t\treturn !this.closed;\n\t}\n\n\tstatic spawn(\n\t\tbin: string,\n\t\thandlers: VoiceDaemonHandlers,\n\t\toptions?: VoiceDaemonOptions,\n\t): Promise<VoiceDaemonSpawnResult> {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst args = [\"serve\"];\n\t\t\tif (options?.silenceMs !== undefined) {\n\t\t\t\targs.push(\"--silence-ms\", String(Math.max(0, Math.round(options.silenceMs))));\n\t\t\t}\n\t\t\tlet proc: ChildProcess;\n\t\t\ttry {\n\t\t\t\tproc = spawn(bin, args, { stdio: [\"pipe\", \"pipe\", \"ignore\"] });\n\t\t\t} catch (err) {\n\t\t\t\thandlers.onError(describeSpawnError(err, bin));\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!proc.stdout || !proc.stdin) {\n\t\t\t\tproc.kill();\n\t\t\t\thandlers.onError(\"failed to open voicetools serve stdio\");\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tlet daemon: VoiceDaemon | undefined;\n\t\t\tlet sawPreReadyError = false;\n\t\t\tconst rl = createInterface({ input: proc.stdout });\n\n\t\t\tconst fail = (reason: \"unsupported\" | \"error\"): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\trl.close();\n\t\t\t\tresolve({ ok: false, reason });\n\t\t\t};\n\n\t\t\trl.on(\"line\", (line) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tdaemon.handleLine(line);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (settled) return;\n\t\t\t\tif (line === \"READY\") {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tdaemon = new VoiceDaemon(proc, handlers, options?.idleTimeoutMs ?? 0);\n\t\t\t\t\thandlers.onReady?.();\n\t\t\t\t\tresolve({ ok: true, daemon });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (line.startsWith(\"ERROR \")) {\n\t\t\t\t\t// Loading can fail before READY (e.g. no model installed yet).\n\t\t\t\t\t// Surface it now; the process still exits right after.\n\t\t\t\t\tsawPreReadyError = true;\n\t\t\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"error\", (err) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(describeSpawnError(err, bin));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!sawPreReadyError) handlers.onError(describeSpawnError(err, bin));\n\t\t\t\tfail(\"error\");\n\t\t\t});\n\n\t\t\tproc.on(\"close\", (code) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(`voicetools serve exited with code ${code ?? \"unknown\"}`);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfail(sawPreReadyError ? \"error\" : \"unsupported\");\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate startIdleTimer(): void {\n\t\tthis.stopIdleTimer();\n\t\tif (this.idleTimeoutMs <= 0 || this.closed) return;\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tthis.idleTimer = undefined;\n\t\t\tif (this.closed) return;\n\t\t\tthis.handlers.onIdle?.();\n\t\t}, this.idleTimeoutMs);\n\t}\n\n\tprivate stopIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = undefined;\n\t\t}\n\t}\n\n\tprivate handleLine(line: string): void {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\tthis.handlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"PARTIAL \")) {\n\t\t\tthis.handlers.onPartial?.(line.slice(8));\n\t\t} else if (line.startsWith(\"FINAL \")) {\n\t\t\tthis.handlers.onFinal?.(line.slice(6));\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\tthis.handlers.onSegment(line.slice(8));\n\t\t} else if (line.startsWith(\"LEVEL \")) {\n\t\t\tconst rms = Number.parseFloat(line.slice(6).trim());\n\t\t\tif (!Number.isNaN(rms)) this.handlers.onLevel?.(rms);\n\t\t} else if (line.startsWith(\"PHASE \")) {\n\t\t\tthis.handlers.onPhase?.(line.slice(6).trim());\n\t\t} else if (line === \"DONE\") {\n\t\t\tthis.handlers.onStatus(\"done\");\n\t\t\tthis.startIdleTimer();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\tthis.handlers.onError(line.slice(6).trim());\n\t\t}\n\t}\n\n\t/** Begin a capture: opens the mic, streams PARTIAL/FINAL (or SEGMENT), ends with DONE. */\n\tstartCapture(): void {\n\t\tif (this.closed) return;\n\t\tthis.stopIdleTimer();\n\t\tthis.proc.stdin?.write(\"START\\n\");\n\t}\n\n\t/** Cancel the in-flight capture, if any. Idempotent. */\n\tcancel(): void {\n\t\tif (this.closed) return;\n\t\tthis.proc.stdin?.write(\"CANCEL\\n\");\n\t}\n\n\t/** Ask the daemon to exit gracefully, force-killing if it doesn't within 1s. */\n\tshutdown(): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.stopIdleTimer();\n\t\ttry {\n\t\t\tthis.proc.stdin?.write(\"SHUTDOWN\\n\");\n\t\t} catch {\n\t\t\t// stdin may already be gone (process died); force-kill below covers it.\n\t\t}\n\t\tconst proc = this.proc;\n\t\tconst killTimer = setTimeout(() => {\n\t\t\tif (!proc.killed) proc.kill();\n\t\t}, 1000);\n\t\tproc.once(\"close\", () => clearTimeout(killTimer));\n\t}\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolisachint/hoocode-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.106",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"hoocodeConfig": {
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"prepublishOnly": "npm run clean && npm run build"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@kolisachint/hoocode-agent-core": "^0.4.
|
|
49
|
-
"@kolisachint/hoocode-ai": "^0.4.
|
|
50
|
-
"@kolisachint/hoocode-tui": "^0.4.
|
|
48
|
+
"@kolisachint/hoocode-agent-core": "^0.4.106",
|
|
49
|
+
"@kolisachint/hoocode-ai": "^0.4.106",
|
|
50
|
+
"@kolisachint/hoocode-tui": "^0.4.106",
|
|
51
51
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
52
52
|
"chalk": "^5.5.0",
|
|
53
53
|
"cli-highlight": "^2.1.11",
|