@kolisachint/hoocode-agent 0.4.105 → 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 +14 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +20 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice-transcribe.d.ts +16 -1
- package/dist/modes/interactive/voice-transcribe.d.ts.map +1 -1
- package/dist/modes/interactive/voice-transcribe.js +31 -4
- 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
|
@@ -58,6 +58,8 @@ export interface VoiceDaemonHandlers extends VoiceTranscribeHandlers {
|
|
|
58
58
|
onPartial?: (text: string) => void;
|
|
59
59
|
onFinal?: (text: string) => void;
|
|
60
60
|
onCrash?: (message: string) => void;
|
|
61
|
+
/** Fired when the daemon has been idle (no capture) for `idleTimeoutMs`. */
|
|
62
|
+
onIdle?: () => void;
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* Outcome of {@link VoiceDaemon.spawn}. `reason: "unsupported"` means the
|
|
@@ -76,6 +78,15 @@ export type VoiceDaemonSpawnResult = {
|
|
|
76
78
|
ok: false;
|
|
77
79
|
reason: "unsupported" | "error";
|
|
78
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
|
+
}
|
|
79
90
|
/**
|
|
80
91
|
* A persistent `voicetools serve` process: models are loaded once and stay
|
|
81
92
|
* warm across captures. Only one capture runs at a time; call `startCapture`
|
|
@@ -86,9 +97,13 @@ export declare class VoiceDaemon {
|
|
|
86
97
|
private readonly proc;
|
|
87
98
|
private readonly handlers;
|
|
88
99
|
private closed;
|
|
100
|
+
private idleTimer;
|
|
101
|
+
private readonly idleTimeoutMs;
|
|
89
102
|
private constructor();
|
|
90
103
|
get isReady(): boolean;
|
|
91
|
-
static spawn(bin: string, handlers: VoiceDaemonHandlers): Promise<VoiceDaemonSpawnResult>;
|
|
104
|
+
static spawn(bin: string, handlers: VoiceDaemonHandlers, options?: VoiceDaemonOptions): Promise<VoiceDaemonSpawnResult>;
|
|
105
|
+
private startIdleTimer;
|
|
106
|
+
private stopIdleTimer;
|
|
92
107
|
private handleLine;
|
|
93
108
|
/** Begin a capture: opens the mic, streams PARTIAL/FINAL (or SEGMENT), ends with DONE. */
|
|
94
109
|
startCapture(): void;
|
|
@@ -1 +1 @@
|
|
|
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;CACpC;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;;;;;GAKG;AACH,qBAAa,WAAW;IAItB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ1B,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,eAGH;IAEJ,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA0ExF;IAED,OAAO,CAAC,UAAU;IAqBlB,0FAA0F;IAC1F,YAAY,IAAI,IAAI,CAGnB;IAED,wDAAwD;IACxD,MAAM,IAAI,IAAI,CAGb;IAED,gFAAgF;IAChF,QAAQ,IAAI,IAAI,CAaf;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}\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/**\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\n\tprivate constructor(\n\t\tprivate readonly proc: ChildProcess,\n\t\tprivate readonly handlers: VoiceDaemonHandlers,\n\t) {}\n\n\tget isReady(): boolean {\n\t\treturn !this.closed;\n\t}\n\n\tstatic spawn(bin: string, handlers: VoiceDaemonHandlers): Promise<VoiceDaemonSpawnResult> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet proc: ChildProcess;\n\t\t\ttry {\n\t\t\t\tproc = spawn(bin, [\"serve\"], { 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);\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 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} 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.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\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"]}
|
|
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"]}
|
|
@@ -92,18 +92,25 @@ export class VoiceDaemon {
|
|
|
92
92
|
proc;
|
|
93
93
|
handlers;
|
|
94
94
|
closed = false;
|
|
95
|
-
|
|
95
|
+
idleTimer;
|
|
96
|
+
idleTimeoutMs;
|
|
97
|
+
constructor(proc, handlers, idleTimeoutMs) {
|
|
96
98
|
this.proc = proc;
|
|
97
99
|
this.handlers = handlers;
|
|
100
|
+
this.idleTimeoutMs = idleTimeoutMs;
|
|
98
101
|
}
|
|
99
102
|
get isReady() {
|
|
100
103
|
return !this.closed;
|
|
101
104
|
}
|
|
102
|
-
static spawn(bin, handlers) {
|
|
105
|
+
static spawn(bin, handlers, options) {
|
|
103
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
|
+
}
|
|
104
111
|
let proc;
|
|
105
112
|
try {
|
|
106
|
-
proc = spawn(bin,
|
|
113
|
+
proc = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
107
114
|
}
|
|
108
115
|
catch (err) {
|
|
109
116
|
handlers.onError(describeSpawnError(err, bin));
|
|
@@ -136,7 +143,7 @@ export class VoiceDaemon {
|
|
|
136
143
|
return;
|
|
137
144
|
if (line === "READY") {
|
|
138
145
|
settled = true;
|
|
139
|
-
daemon = new VoiceDaemon(proc, handlers);
|
|
146
|
+
daemon = new VoiceDaemon(proc, handlers, options?.idleTimeoutMs ?? 0);
|
|
140
147
|
handlers.onReady?.();
|
|
141
148
|
resolve({ ok: true, daemon });
|
|
142
149
|
return;
|
|
@@ -172,6 +179,23 @@ export class VoiceDaemon {
|
|
|
172
179
|
});
|
|
173
180
|
});
|
|
174
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
|
+
}
|
|
175
199
|
handleLine(line) {
|
|
176
200
|
if (line.startsWith("STATUS ")) {
|
|
177
201
|
this.handlers.onStatus(line.slice(7).trim());
|
|
@@ -195,6 +219,7 @@ export class VoiceDaemon {
|
|
|
195
219
|
}
|
|
196
220
|
else if (line === "DONE") {
|
|
197
221
|
this.handlers.onStatus("done");
|
|
222
|
+
this.startIdleTimer();
|
|
198
223
|
}
|
|
199
224
|
else if (line.startsWith("ERROR ")) {
|
|
200
225
|
this.handlers.onError(line.slice(6).trim());
|
|
@@ -204,6 +229,7 @@ export class VoiceDaemon {
|
|
|
204
229
|
startCapture() {
|
|
205
230
|
if (this.closed)
|
|
206
231
|
return;
|
|
232
|
+
this.stopIdleTimer();
|
|
207
233
|
this.proc.stdin?.write("START\n");
|
|
208
234
|
}
|
|
209
235
|
/** Cancel the in-flight capture, if any. Idempotent. */
|
|
@@ -217,6 +243,7 @@ export class VoiceDaemon {
|
|
|
217
243
|
if (this.closed)
|
|
218
244
|
return;
|
|
219
245
|
this.closed = true;
|
|
246
|
+
this.stopIdleTimer();
|
|
220
247
|
try {
|
|
221
248
|
this.proc.stdin?.write("SHUTDOWN\n");
|
|
222
249
|
}
|
|
@@ -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 * `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}\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/**\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\n\tprivate constructor(\n\t\tprivate readonly proc: ChildProcess,\n\t\tprivate readonly handlers: VoiceDaemonHandlers,\n\t) {}\n\n\tget isReady(): boolean {\n\t\treturn !this.closed;\n\t}\n\n\tstatic spawn(bin: string, handlers: VoiceDaemonHandlers): Promise<VoiceDaemonSpawnResult> {\n\t\treturn new Promise((resolve) => {\n\t\t\tlet proc: ChildProcess;\n\t\t\ttry {\n\t\t\t\tproc = spawn(bin, [\"serve\"], { 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);\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 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} 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.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\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"]}
|
|
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",
|