@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/cli.js +6325 -4420
  3. package/dist/types/cli/bench-cli.d.ts +1 -0
  4. package/dist/types/config/model-resolver.d.ts +6 -3
  5. package/dist/types/config/settings-schema.d.ts +62 -0
  6. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  7. package/dist/types/launch/broker-list-order.test.d.ts +1 -0
  8. package/dist/types/live/attestation.d.ts +2 -0
  9. package/dist/types/live/controller.d.ts +10 -2
  10. package/dist/types/live/protocol.d.ts +1 -1
  11. package/dist/types/live/transport.d.ts +6 -19
  12. package/dist/types/live/visualizer.d.ts +8 -11
  13. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  14. package/dist/types/modes/components/custom-message.d.ts +1 -1
  15. package/dist/types/modes/components/message-frame.d.ts +8 -4
  16. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  18. package/dist/types/modes/interactive-mode.d.ts +3 -6
  19. package/dist/types/modes/types.d.ts +9 -5
  20. package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
  21. package/dist/types/session/agent-session-types.d.ts +8 -1
  22. package/dist/types/session/agent-session.d.ts +20 -1
  23. package/dist/types/session/auth-storage.d.ts +1 -1
  24. package/dist/types/session/eval-runner.d.ts +2 -0
  25. package/dist/types/session/messages.d.ts +2 -0
  26. package/dist/types/session/session-tools.d.ts +15 -0
  27. package/dist/types/session/streaming-output.d.ts +8 -0
  28. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  29. package/dist/types/stt/index.d.ts +0 -2
  30. package/dist/types/stt/stt-controller.d.ts +7 -0
  31. package/dist/types/task/executor.d.ts +3 -1
  32. package/dist/types/task/structured-subagent.d.ts +3 -0
  33. package/dist/types/task/types.d.ts +11 -11
  34. package/dist/types/thinking.d.ts +13 -0
  35. package/dist/types/tiny/title-client.d.ts +10 -0
  36. package/dist/types/tools/builtin-names.d.ts +1 -1
  37. package/dist/types/tools/computer/protocol.d.ts +43 -0
  38. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  39. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  40. package/dist/types/tools/computer/worker.d.ts +15 -0
  41. package/dist/types/tools/computer-renderer.d.ts +22 -0
  42. package/dist/types/tools/computer.d.ts +71 -0
  43. package/dist/types/tools/context.d.ts +2 -0
  44. package/dist/types/tools/default-renderer.d.ts +21 -0
  45. package/dist/types/tools/eval-format/index.d.ts +7 -0
  46. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  47. package/dist/types/tools/eval-format/julia.d.ts +2 -0
  48. package/dist/types/tools/eval-format/python.d.ts +2 -0
  49. package/dist/types/tools/eval-format/ruby.d.ts +2 -0
  50. package/dist/types/tools/index.d.ts +2 -0
  51. package/dist/types/tools/resolve.d.ts +7 -0
  52. package/dist/types/tools/todo.d.ts +4 -1
  53. package/dist/types/tts/streaming-player.d.ts +10 -43
  54. package/dist/types/utils/tools-manager.d.ts +1 -2
  55. package/dist/types/web/search/providers/base.d.ts +16 -0
  56. package/dist/types/web/search/providers/brave.d.ts +2 -0
  57. package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
  58. package/dist/types/web/search/providers/gemini.d.ts +3 -0
  59. package/dist/types/web/search/providers/jina.d.ts +2 -0
  60. package/dist/types/web/search/providers/kagi.d.ts +2 -0
  61. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  62. package/dist/types/web/search/providers/parallel.d.ts +2 -0
  63. package/dist/types/web/search/providers/perplexity.d.ts +3 -0
  64. package/dist/types/web/search/providers/searxng.d.ts +10 -0
  65. package/dist/types/web/search/providers/tavily.d.ts +8 -0
  66. package/dist/types/web/search/query.d.ts +190 -0
  67. package/package.json +12 -12
  68. package/src/cli/args.ts +1 -0
  69. package/src/cli/bench-cli.ts +12 -1
  70. package/src/cli/setup-cli.ts +2 -14
  71. package/src/cli/web-search-cli.ts +7 -0
  72. package/src/cli.ts +8 -0
  73. package/src/config/model-registry.ts +6 -0
  74. package/src/config/model-resolver.ts +17 -6
  75. package/src/config/settings-schema.ts +66 -0
  76. package/src/debug/raw-sse-buffer.ts +157 -30
  77. package/src/eval/executor-base.ts +1 -0
  78. package/src/eval/js/executor.ts +2 -0
  79. package/src/exec/bash-executor.ts +1 -0
  80. package/src/export/share.ts +4 -3
  81. package/src/extensibility/extensions/wrapper.ts +68 -12
  82. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  83. package/src/launch/broker-list-order.test.ts +89 -0
  84. package/src/launch/broker.ts +49 -8
  85. package/src/live/attestation.ts +91 -0
  86. package/src/live/controller.ts +76 -23
  87. package/src/live/protocol.test.ts +3 -3
  88. package/src/live/protocol.ts +1 -1
  89. package/src/live/transport.ts +72 -140
  90. package/src/live/visualizer.ts +114 -134
  91. package/src/modes/components/assistant-message.ts +7 -2
  92. package/src/modes/components/custom-message.ts +4 -1
  93. package/src/modes/components/message-frame.ts +14 -8
  94. package/src/modes/components/session-account-selector.ts +62 -0
  95. package/src/modes/components/tool-execution.ts +17 -110
  96. package/src/modes/controllers/input-controller.ts +55 -47
  97. package/src/modes/controllers/live-command-controller.ts +82 -5
  98. package/src/modes/controllers/selector-controller.ts +62 -0
  99. package/src/modes/interactive-mode.ts +79 -5
  100. package/src/modes/types.ts +10 -4
  101. package/src/modes/utils/ui-helpers.ts +7 -8
  102. package/src/prompts/system/computer-safety.md +14 -0
  103. package/src/prompts/system/resolve-device-reminder.md +1 -1
  104. package/src/prompts/system/workflow-notice.md +1 -1
  105. package/src/prompts/tools/ast-edit.md +1 -0
  106. package/src/prompts/tools/bash.md +1 -0
  107. package/src/prompts/tools/computer.md +26 -0
  108. package/src/prompts/tools/eval.md +2 -2
  109. package/src/prompts/tools/task.md +5 -2
  110. package/src/prompts/tools/web-search.md +2 -0
  111. package/src/sdk.ts +13 -4
  112. package/src/session/agent-session-types.ts +9 -0
  113. package/src/session/agent-session.ts +70 -0
  114. package/src/session/auth-storage.ts +1 -0
  115. package/src/session/eval-runner.ts +5 -0
  116. package/src/session/messages.ts +3 -0
  117. package/src/session/queued-messages.ts +7 -1
  118. package/src/session/session-tools.ts +37 -0
  119. package/src/session/streaming-output.ts +52 -5
  120. package/src/slash-commands/builtin-registry.ts +165 -9
  121. package/src/slash-commands/helpers/session-pin.ts +44 -0
  122. package/src/stt/downloader.ts +0 -2
  123. package/src/stt/index.ts +0 -2
  124. package/src/stt/stt-controller.ts +57 -146
  125. package/src/system-prompt.ts +4 -0
  126. package/src/task/executor.ts +17 -9
  127. package/src/task/index.ts +14 -34
  128. package/src/task/structured-subagent.ts +4 -0
  129. package/src/task/types.ts +15 -13
  130. package/src/thinking.ts +27 -0
  131. package/src/tiny/title-client.ts +22 -0
  132. package/src/tools/ast-edit.ts +4 -1
  133. package/src/tools/bash-interactive.ts +90 -86
  134. package/src/tools/bash.ts +13 -0
  135. package/src/tools/browser/render.ts +2 -1
  136. package/src/tools/builtin-names.ts +1 -0
  137. package/src/tools/computer/protocol.ts +28 -0
  138. package/src/tools/computer/supervisor.ts +258 -0
  139. package/src/tools/computer/worker-entry.ts +25 -0
  140. package/src/tools/computer/worker.ts +135 -0
  141. package/src/tools/computer-renderer.ts +108 -0
  142. package/src/tools/computer.ts +433 -0
  143. package/src/tools/context.ts +2 -0
  144. package/src/tools/default-renderer.ts +139 -0
  145. package/src/tools/essential-tools.ts +1 -0
  146. package/src/tools/eval-format/index.ts +24 -0
  147. package/src/tools/eval-format/javascript.ts +952 -0
  148. package/src/tools/eval-format/julia.ts +446 -0
  149. package/src/tools/eval-format/python.ts +544 -0
  150. package/src/tools/eval-format/ruby.ts +380 -0
  151. package/src/tools/eval-render.ts +12 -6
  152. package/src/tools/index.ts +5 -0
  153. package/src/tools/renderers.ts +2 -0
  154. package/src/tools/resolve.ts +10 -1
  155. package/src/tools/todo.ts +58 -6
  156. package/src/tools/xdev.ts +54 -26
  157. package/src/tts/streaming-player.ts +81 -340
  158. package/src/utils/clipboard.ts +1 -30
  159. package/src/utils/mac-file-urls.applescript +37 -0
  160. package/src/utils/tool-choice.ts +14 -0
  161. package/src/utils/tools-manager.ts +1 -19
  162. package/src/web/search/index.ts +28 -5
  163. package/src/web/search/providers/anthropic.ts +62 -4
  164. package/src/web/search/providers/base.ts +16 -0
  165. package/src/web/search/providers/brave.ts +30 -3
  166. package/src/web/search/providers/codex.ts +14 -1
  167. package/src/web/search/providers/duckduckgo.ts +23 -1
  168. package/src/web/search/providers/ecosia.ts +5 -1
  169. package/src/web/search/providers/exa.ts +28 -1
  170. package/src/web/search/providers/firecrawl.ts +37 -3
  171. package/src/web/search/providers/gemini.ts +12 -2
  172. package/src/web/search/providers/google.ts +2 -1
  173. package/src/web/search/providers/jina.ts +31 -8
  174. package/src/web/search/providers/kagi.ts +10 -1
  175. package/src/web/search/providers/kimi.ts +16 -1
  176. package/src/web/search/providers/mojeek.ts +14 -1
  177. package/src/web/search/providers/parallel.ts +48 -2
  178. package/src/web/search/providers/perplexity.ts +94 -6
  179. package/src/web/search/providers/searxng.ts +145 -9
  180. package/src/web/search/providers/startpage.ts +8 -2
  181. package/src/web/search/providers/synthetic.ts +7 -1
  182. package/src/web/search/providers/tavily.ts +52 -3
  183. package/src/web/search/providers/tinyfish.ts +6 -1
  184. package/src/web/search/providers/xai.ts +49 -2
  185. package/src/web/search/providers/zai.ts +19 -1
  186. package/src/web/search/query.ts +850 -0
  187. package/dist/types/stt/recorder.d.ts +0 -30
  188. package/dist/types/stt/transcriber.d.ts +0 -14
  189. package/dist/types/stt/wav.d.ts +0 -29
  190. package/dist/types/tts/player.d.ts +0 -32
  191. package/src/live/audio-worklet.txt +0 -59
  192. package/src/live/browser-runtime.txt +0 -221
  193. package/src/stt/recorder.ts +0 -551
  194. package/src/stt/transcriber.ts +0 -60
  195. package/src/stt/wav.ts +0 -173
  196. package/src/tts/player.ts +0 -137
package/src/stt/wav.ts DELETED
@@ -1,173 +0,0 @@
1
- /**
2
- * Minimal WAV (RIFF/PCM) decoder producing the Float32Array @ 16 kHz mono that
3
- * transformers.js `automatic-speech-recognition` expects. Ports the decode/
4
- * mono-mix/resample logic from the retired Python `transcribe.py` (which read
5
- * via the stdlib `wave` module) so STT no longer shells out to Python.
6
- *
7
- * Supported sample formats: PCM uint8 (8-bit), int16 (16-bit), int32 (32-bit),
8
- * and IEEE float32 (format tag 3). Any number of channels is mixed down to mono.
9
- */
10
-
11
- /** transformers.js Whisper feature extractor operates at 16 kHz. */
12
- export const TARGET_SAMPLE_RATE = 16_000;
13
-
14
- const WAV_FORMAT_PCM = 1;
15
- const WAV_FORMAT_IEEE_FLOAT = 3;
16
- const WAV_FORMAT_EXTENSIBLE = 0xfffe;
17
-
18
- interface WavData {
19
- format: number;
20
- channels: number;
21
- sampleRate: number;
22
- bitsPerSample: number;
23
- /** Raw PCM/float bytes from the `data` chunk. */
24
- samples: DataView;
25
- }
26
-
27
- function readFourCc(view: DataView, offset: number): string {
28
- return String.fromCharCode(
29
- view.getUint8(offset),
30
- view.getUint8(offset + 1),
31
- view.getUint8(offset + 2),
32
- view.getUint8(offset + 3),
33
- );
34
- }
35
-
36
- /** Parse the RIFF container, returning the `fmt ` parameters and `data` bytes. */
37
- function parseWav(buffer: ArrayBuffer): WavData {
38
- const view = new DataView(buffer);
39
- if (buffer.byteLength < 12 || readFourCc(view, 0) !== "RIFF" || readFourCc(view, 8) !== "WAVE") {
40
- throw new Error("Not a RIFF/WAVE file");
41
- }
42
-
43
- let format: number | undefined;
44
- let channels = 0;
45
- let sampleRate = 0;
46
- let bitsPerSample = 0;
47
- let samples: DataView | undefined;
48
-
49
- // Chunks begin after the 12-byte RIFF/WAVE header; each is an 8-byte header
50
- // (4-char id + uint32 LE size) followed by `size` bytes padded to even.
51
- let offset = 12;
52
- while (offset + 8 <= buffer.byteLength) {
53
- const id = readFourCc(view, offset);
54
- const size = view.getUint32(offset + 4, true);
55
- const body = offset + 8;
56
- if (id === "fmt ") {
57
- format = view.getUint16(body, true);
58
- channels = view.getUint16(body + 2, true);
59
- sampleRate = view.getUint32(body + 4, true);
60
- bitsPerSample = view.getUint16(body + 14, true);
61
- // WAVE_FORMAT_EXTENSIBLE (ffmpeg & friends): the real codec is the
62
- // first 2 bytes of the SubFormat GUID in the fmt extension.
63
- if (format === WAV_FORMAT_EXTENSIBLE && size >= 40) format = view.getUint16(body + 24, true);
64
- } else if (id === "data") {
65
- const length = Math.min(size, buffer.byteLength - body);
66
- samples = new DataView(buffer, body, length);
67
- }
68
- offset = body + size + (size % 2);
69
- }
70
-
71
- if (format === undefined || samples === undefined || channels < 1 || sampleRate < 1) {
72
- throw new Error("WAV file missing fmt/data chunks");
73
- }
74
- return { format, channels, sampleRate, bitsPerSample, samples };
75
- }
76
-
77
- /** Decode raw PCM/float bytes into interleaved normalized [-1, 1] float samples. */
78
- function decodeSamples(wav: WavData): Float32Array {
79
- const { format, bitsPerSample, samples } = wav;
80
- const view = samples;
81
- if (format === WAV_FORMAT_IEEE_FLOAT && bitsPerSample === 32) {
82
- const count = Math.floor(view.byteLength / 4);
83
- const out = new Float32Array(count);
84
- for (let i = 0; i < count; i += 1) out[i] = view.getFloat32(i * 4, true);
85
- return out;
86
- }
87
- if (format !== WAV_FORMAT_PCM) {
88
- throw new Error(`Unsupported WAV format tag: ${format}`);
89
- }
90
- if (bitsPerSample === 16) {
91
- const count = Math.floor(view.byteLength / 2);
92
- const out = new Float32Array(count);
93
- for (let i = 0; i < count; i += 1) out[i] = view.getInt16(i * 2, true) / 32_768;
94
- return out;
95
- }
96
- if (bitsPerSample === 8) {
97
- // 8-bit PCM is unsigned, centered at 128.
98
- const count = view.byteLength;
99
- const out = new Float32Array(count);
100
- for (let i = 0; i < count; i += 1) out[i] = (view.getUint8(i) - 128) / 128;
101
- return out;
102
- }
103
- if (bitsPerSample === 32) {
104
- const count = Math.floor(view.byteLength / 4);
105
- const out = new Float32Array(count);
106
- for (let i = 0; i < count; i += 1) out[i] = view.getInt32(i * 4, true) / 2_147_483_648;
107
- return out;
108
- }
109
- throw new Error(`Unsupported PCM sample width: ${bitsPerSample} bits`);
110
- }
111
-
112
- /** Average interleaved channels down to a single mono track. */
113
- function mixToMono(interleaved: Float32Array, channels: number): Float32Array {
114
- if (channels <= 1) return interleaved;
115
- const frames = Math.floor(interleaved.length / channels);
116
- const out = new Float32Array(frames);
117
- for (let frame = 0; frame < frames; frame += 1) {
118
- let sum = 0;
119
- for (let channel = 0; channel < channels; channel += 1) sum += interleaved[frame * channels + channel]!;
120
- out[frame] = sum / channels;
121
- }
122
- return out;
123
- }
124
-
125
- /**
126
- * Resample via linear interpolation, mirroring the Python `np.interp` over
127
- * `linspace(0, n-1, targetLen)` against `arange(n)`.
128
- */
129
- export function resampleLinear(input: Float32Array, fromRate: number, toRate: number): Float32Array {
130
- if (fromRate === toRate || input.length === 0) return input;
131
- const n = input.length;
132
- const targetLen = Math.max(1, Math.floor((n * toRate) / fromRate));
133
- const out = new Float32Array(targetLen);
134
- if (targetLen === 1) {
135
- out[0] = input[0]!;
136
- return out;
137
- }
138
- const step = (n - 1) / (targetLen - 1);
139
- for (let i = 0; i < targetLen; i += 1) {
140
- const pos = i * step;
141
- const lo = Math.floor(pos);
142
- const hi = Math.min(lo + 1, n - 1);
143
- const frac = pos - lo;
144
- out[i] = input[lo]! * (1 - frac) + input[hi]! * frac;
145
- }
146
- return out;
147
- }
148
-
149
- /**
150
- * Decode a WAV byte buffer into a 16 kHz mono Float32Array suitable for the
151
- * transformers.js Whisper pipeline.
152
- */
153
- export function decodeWavToMono16k(buffer: ArrayBuffer): Float32Array {
154
- const wav = parseWav(buffer);
155
- const interleaved = decodeSamples(wav);
156
- const mono = mixToMono(interleaved, wav.channels);
157
- return resampleLinear(mono, wav.sampleRate, TARGET_SAMPLE_RATE);
158
- }
159
-
160
- /**
161
- * Decode interleaved little-endian signed 16-bit PCM bytes into normalized
162
- * [-1, 1] mono float samples. The live recorder streams raw s16le frames from
163
- * sox/ffmpeg/arecord stdout (no RIFF container), so this is the hot-path
164
- * counterpart to {@link decodeWavToMono16k}. `bytes` MUST be 2-byte aligned;
165
- * callers buffer any trailing odd byte across chunk boundaries.
166
- */
167
- export function decodePcmS16LE(bytes: Uint8Array): Float32Array {
168
- const count = bytes.length >>> 1;
169
- const view = new DataView(bytes.buffer, bytes.byteOffset, count * 2);
170
- const out = new Float32Array(count);
171
- for (let i = 0; i < count; i += 1) out[i] = view.getInt16(i * 2, true) / 32_768;
172
- return out;
173
- }
package/src/tts/player.ts DELETED
@@ -1,137 +0,0 @@
1
- /**
2
- * Cross-platform audio-file playback via the system's built-in players.
3
- *
4
- * The selection logic is split into a pure, injectable builder
5
- * ({@link playerCommandsFor}) so it can be unit-tested without spawning a
6
- * process or touching PATH, and a thin runtime wrapper ({@link playAudioFile})
7
- * that walks the resulting fallback chain.
8
- */
9
- import * as fs from "node:fs/promises";
10
- import { $which } from "@oh-my-pi/pi-utils";
11
- import { getToolPath } from "../utils/tools-manager";
12
-
13
- export interface PlayerCommand {
14
- cmd: string;
15
- args: string[];
16
- }
17
-
18
- /** Injection seam for {@link playerCommandsFor} — defaults to real PATH/tools lookups. */
19
- export interface PlayerLookup {
20
- which?: (bin: string) => string | null;
21
- ffmpeg?: () => string | null;
22
- }
23
-
24
- /**
25
- * Build the ordered list of playback commands to try for `filePath` on the
26
- * given platform. Pure + injectable so the selection logic is testable without
27
- * spawning anything.
28
- *
29
- * - darwin: `afplay` (always present on macOS).
30
- * - win32: PowerShell `Media.SoundPlayer.PlaySync()` (no extra deps).
31
- * - linux/other POSIX: `paplay` (PulseAudio) → `aplay` (ALSA) → the bundled
32
- * static `ffmpeg` (`-f pulse` then `-f alsa`). Empty result means nothing is
33
- * available and the caller should surface an install hint.
34
- */
35
- export function playerCommandsFor(
36
- platform: NodeJS.Platform,
37
- filePath: string,
38
- lookup: PlayerLookup = {},
39
- ): PlayerCommand[] {
40
- const which = lookup.which ?? $which;
41
- const ffmpeg = lookup.ffmpeg ?? ((): string | null => getToolPath("ffmpeg"));
42
-
43
- if (platform === "darwin") {
44
- return [{ cmd: "afplay", args: [filePath] }];
45
- }
46
- if (platform === "win32") {
47
- return [
48
- {
49
- cmd: "powershell",
50
- args: ["-NoProfile", "-Command", `(New-Object Media.SoundPlayer '${filePath}').PlaySync()`],
51
- },
52
- ];
53
- }
54
-
55
- // Linux and other POSIX desktops share the PulseAudio/ALSA fallback chain.
56
- const commands: PlayerCommand[] = [];
57
- const paplay = which("paplay");
58
- if (paplay) commands.push({ cmd: paplay, args: [filePath] });
59
- const aplay = which("aplay");
60
- if (aplay) commands.push({ cmd: aplay, args: [filePath] });
61
- const ffmpegBin = ffmpeg();
62
- if (ffmpegBin) {
63
- commands.push({
64
- cmd: ffmpegBin,
65
- args: ["-loglevel", "error", "-nostdin", "-i", filePath, "-f", "pulse", "default"],
66
- });
67
- commands.push({
68
- cmd: ffmpegBin,
69
- args: ["-loglevel", "error", "-nostdin", "-i", filePath, "-f", "alsa", "default"],
70
- });
71
- }
72
- return commands;
73
- }
74
-
75
- export interface PlayAudioOptions {
76
- signal?: AbortSignal;
77
- }
78
-
79
- function playbackAbortError(signal: AbortSignal): Error {
80
- const reason = signal.reason;
81
- return reason instanceof Error ? reason : new DOMException("Audio playback aborted", "AbortError");
82
- }
83
-
84
- /**
85
- * Play `filePath` through the speakers, trying each candidate command in order
86
- * and returning on the first clean exit. Throws an actionable Error if no
87
- * player exists or every candidate fails (with the collected stderr).
88
- */
89
- export async function playAudioFile(filePath: string, options: PlayAudioOptions = {}): Promise<void> {
90
- const { signal } = options;
91
- if (signal?.aborted) throw playbackAbortError(signal);
92
- const commands = playerCommandsFor(process.platform, filePath);
93
- if (commands.length === 0) {
94
- throw new Error(
95
- "No audio player available. Install PulseAudio (paplay) or ALSA (aplay), " +
96
- "or run `omp setup speech` to download a bundled ffmpeg.",
97
- );
98
- }
99
-
100
- const failures: string[] = [];
101
- for (const command of commands) {
102
- if (signal?.aborted) throw playbackAbortError(signal);
103
- try {
104
- const proc = Bun.spawn([command.cmd, ...command.args], { stdout: "ignore", stderr: "pipe" });
105
- let killTimer: NodeJS.Timeout | undefined;
106
- const abort = (): void => {
107
- proc.kill("SIGTERM");
108
- killTimer = setTimeout(() => proc.kill("SIGKILL"), 500);
109
- killTimer.unref?.();
110
- };
111
- signal?.addEventListener("abort", abort, { once: true });
112
- try {
113
- const code = await proc.exited;
114
- if (signal?.aborted) throw playbackAbortError(signal);
115
- if (code === 0) return;
116
- let stderr = "";
117
- if (proc.stderr && typeof proc.stderr !== "number") {
118
- stderr = await new Response(proc.stderr as ReadableStream).text();
119
- }
120
- failures.push(`${command.cmd} exited ${code}${stderr.trim() ? `: ${stderr.trim()}` : ""}`);
121
- } finally {
122
- signal?.removeEventListener("abort", abort);
123
- if (killTimer) clearTimeout(killTimer);
124
- }
125
- } catch (err) {
126
- if (signal?.aborted) throw playbackAbortError(signal);
127
- failures.push(`${command.cmd}: ${err instanceof Error ? err.message : String(err)}`);
128
- }
129
- }
130
-
131
- throw new Error(`Audio playback failed:\n${failures.join("\n")}`);
132
- }
133
-
134
- /** Best-effort temp-file cleanup used by callers after playback. */
135
- export async function removeTempFile(filePath: string): Promise<void> {
136
- await fs.unlink(filePath).catch(() => {});
137
- }