@moxxy/channel-kit 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/dist/frame-pump.d.ts +77 -0
  3. package/dist/frame-pump.d.ts.map +1 -0
  4. package/dist/frame-pump.js +118 -0
  5. package/dist/frame-pump.js.map +1 -0
  6. package/dist/index.d.ts +19 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +19 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/ingest/dedupe.d.ts +31 -0
  11. package/dist/ingest/dedupe.d.ts.map +1 -0
  12. package/dist/ingest/dedupe.js +66 -0
  13. package/dist/ingest/dedupe.js.map +1 -0
  14. package/dist/ingest/http-server.d.ts +76 -0
  15. package/dist/ingest/http-server.d.ts.map +1 -0
  16. package/dist/ingest/http-server.js +101 -0
  17. package/dist/ingest/http-server.js.map +1 -0
  18. package/dist/pairing/host-code.d.ts +94 -0
  19. package/dist/pairing/host-code.d.ts.map +1 -0
  20. package/dist/pairing/host-code.js +107 -0
  21. package/dist/pairing/host-code.js.map +1 -0
  22. package/dist/pairing/tofu.d.ts +33 -0
  23. package/dist/pairing/tofu.d.ts.map +1 -0
  24. package/dist/pairing/tofu.js +41 -0
  25. package/dist/pairing/tofu.js.map +1 -0
  26. package/dist/permission.d.ts +28 -0
  27. package/dist/permission.d.ts.map +1 -0
  28. package/dist/permission.js +17 -0
  29. package/dist/permission.js.map +1 -0
  30. package/dist/plain-turn-renderer.d.ts +17 -0
  31. package/dist/plain-turn-renderer.d.ts.map +1 -0
  32. package/dist/plain-turn-renderer.js +28 -0
  33. package/dist/plain-turn-renderer.js.map +1 -0
  34. package/dist/secrets.d.ts +18 -0
  35. package/dist/secrets.d.ts.map +1 -0
  36. package/dist/secrets.js +16 -0
  37. package/dist/secrets.js.map +1 -0
  38. package/dist/turn.d.ts +96 -0
  39. package/dist/turn.d.ts.map +1 -0
  40. package/dist/turn.js +106 -0
  41. package/dist/turn.js.map +1 -0
  42. package/dist/voice-reply.d.ts +139 -0
  43. package/dist/voice-reply.d.ts.map +1 -0
  44. package/dist/voice-reply.js +333 -0
  45. package/dist/voice-reply.js.map +1 -0
  46. package/package.json +54 -0
  47. package/src/frame-pump.test.ts +209 -0
  48. package/src/frame-pump.ts +154 -0
  49. package/src/index.ts +66 -0
  50. package/src/ingest/dedupe.test.ts +58 -0
  51. package/src/ingest/dedupe.ts +66 -0
  52. package/src/ingest/http-server.test.ts +120 -0
  53. package/src/ingest/http-server.ts +173 -0
  54. package/src/pairing/host-code.test.ts +121 -0
  55. package/src/pairing/host-code.ts +159 -0
  56. package/src/pairing/tofu.test.ts +62 -0
  57. package/src/pairing/tofu.ts +60 -0
  58. package/src/permission.test.ts +69 -0
  59. package/src/permission.ts +46 -0
  60. package/src/plain-turn-renderer.ts +31 -0
  61. package/src/secrets.test.ts +59 -0
  62. package/src/secrets.ts +26 -0
  63. package/src/turn.test.ts +165 -0
  64. package/src/turn.ts +162 -0
  65. package/src/voice-reply.test.ts +316 -0
  66. package/src/voice-reply.ts +449 -0
package/dist/turn.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Subscribe to ONLY the given turn's events. Returns the unsubscribe function.
3
+ * The caller controls unsubscribe placement (channels keep their final flush
4
+ * inside the subscription window so trailing events still feed the renderer).
5
+ */
6
+ export function subscribeTurn(source, turnId, onEvent) {
7
+ return source.log.subscribe((event) => {
8
+ if (event.turnId !== turnId)
9
+ return;
10
+ onEvent(event);
11
+ });
12
+ }
13
+ /**
14
+ * Run one turn to completion, draining the `runTurn` iterator. Rendering is
15
+ * expected to happen via a {@link subscribeTurn} subscription, not the yielded
16
+ * events (which are identical); errors propagate to the caller.
17
+ */
18
+ export async function driveTurn(session, opts) {
19
+ for await (const _event of session.runTurn(opts.prompt, {
20
+ turnId: opts.turnId,
21
+ ...(opts.model ? { model: opts.model } : {}),
22
+ signal: opts.signal,
23
+ })) {
24
+ void _event;
25
+ }
26
+ }
27
+ /**
28
+ * Single-flight turn state for a channel: one turn at a time, a per-turn
29
+ * AbortController, and a bounded set of turnIds THIS channel initiated.
30
+ *
31
+ * The own-turn-id set is what {@link mirrorText} filters on (invariant #8)
32
+ * rather than the coarse `busy` flag alone — so an `assistant_message`
33
+ * dispatched for our own turn AFTER `busy` flips false (async event ordering /
34
+ * RemoteSession replay) isn't re-mirrored as foreign.
35
+ */
36
+ export class TurnCoordinator {
37
+ busyFlag = false;
38
+ active = null;
39
+ ownTurnIds = new Set();
40
+ maxOwnTurnIds;
41
+ constructor(opts = {}) {
42
+ this.maxOwnTurnIds = opts.maxOwnTurnIds ?? 64;
43
+ }
44
+ get busy() {
45
+ return this.busyFlag;
46
+ }
47
+ /** The in-flight turn's controller (null when idle) — for /cancel handlers. */
48
+ get controller() {
49
+ return this.active;
50
+ }
51
+ /**
52
+ * Atomically claim the single turn slot. Synchronous on purpose: set `busy`
53
+ * BEFORE any await so a concurrently dispatched second turn can't slip past
54
+ * the guard. Returns null when a turn is already running (the channel replies
55
+ * "still working"); otherwise a lease whose `end()` releases the slot.
56
+ */
57
+ begin(turnId) {
58
+ if (this.busyFlag)
59
+ return null;
60
+ this.busyFlag = true;
61
+ const controller = new AbortController();
62
+ this.active = controller;
63
+ this.ownTurnIds.add(turnId);
64
+ if (this.ownTurnIds.size > this.maxOwnTurnIds) {
65
+ const oldest = this.ownTurnIds.values().next().value;
66
+ if (oldest !== undefined)
67
+ this.ownTurnIds.delete(oldest);
68
+ }
69
+ return {
70
+ turnId,
71
+ controller,
72
+ end: () => {
73
+ this.busyFlag = false;
74
+ if (this.active === controller)
75
+ this.active = null;
76
+ },
77
+ };
78
+ }
79
+ /** Abort the in-flight turn (stop / cancel paths). No-op when idle. */
80
+ abort(reason) {
81
+ if (this.active && !this.active.signal.aborted)
82
+ this.active.abort(reason);
83
+ }
84
+ isOwn(turnId) {
85
+ return this.ownTurnIds.has(turnId);
86
+ }
87
+ /**
88
+ * Foreign-turn mirror gate: returns the assistant prose to mirror for a turn
89
+ * this channel did NOT initiate (e.g. a co-attached surface ran one), or null
90
+ * when the event must not be mirrored — not an `assistant_message`, one of
91
+ * our own turnIds (invariant #8), a turn of ours currently rendering via the
92
+ * frame pump (`busy`), or empty content. The channel adds its own
93
+ * "have I served a target yet" check and does the send.
94
+ */
95
+ mirrorText(event) {
96
+ if (event.type !== 'assistant_message')
97
+ return null;
98
+ if (this.ownTurnIds.has(event.turnId))
99
+ return null;
100
+ if (this.busyFlag)
101
+ return null;
102
+ const text = event.content.trim();
103
+ return text ? text : null;
104
+ }
105
+ }
106
+ //# sourceMappingURL=turn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn.js","sourceRoot":"","sources":["../src/turn.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ import { spawn } from 'node:child_process';
2
+ import type { SynthesizeOptions, Synthesizer } from '@moxxy/sdk';
3
+ /**
4
+ * Voice-reply machinery shared by messaging channels that can speak the final
5
+ * assistant reply through the session's active {@link Synthesizer}
6
+ * (text-to-speech). Everything here is transport-agnostic — turning text into
7
+ * audio, cleaning markdown for speech, transcoding to OGG/Opus, and picking a
8
+ * filename. The messenger-specific delivery (grammy `sendVoice`, a discord.js
9
+ * `AttachmentBuilder`) stays in each channel plugin behind the
10
+ * {@link VoiceReplySink}.
11
+ *
12
+ * The load-bearing guarantee is that NOTHING here throws into the turn path: a
13
+ * missing synthesizer, a TTS failure, a missing ffmpeg, or a transport error
14
+ * all resolve to a typed result. Callers send the text reply first and treat a
15
+ * voice reply as best-effort, so a synth/transcode/delivery failure never
16
+ * breaks the (already-sent) text answer.
17
+ */
18
+ /** The slice of a session the voice-reply path needs: the synthesizer view. */
19
+ export interface SynthesizerSource {
20
+ readonly synthesizers: {
21
+ tryGetActive(): Synthesizer | null;
22
+ };
23
+ }
24
+ /**
25
+ * Strip markdown down to what should be *spoken*. Code fences become a short
26
+ * "(code omitted)" placeholder (reading a diff aloud is noise), links keep only
27
+ * their label, and the inline emphasis/heading/quote/bullet marks are removed
28
+ * so a TTS engine doesn't voice the punctuation. Whitespace is collapsed.
29
+ */
30
+ export declare function toSpeech(markdown: string): string;
31
+ export interface SynthesizeReplyOptions extends SynthesizeOptions {
32
+ /** Max characters of speech text handed to the backend. Default 1200. */
33
+ readonly maxChars?: number;
34
+ }
35
+ export type SynthesizeReplyResult = {
36
+ readonly ok: true;
37
+ readonly audio: Uint8Array;
38
+ readonly mimeType: string;
39
+ } | {
40
+ readonly ok: false;
41
+ readonly reason: 'no-synthesizer' | 'empty' | 'failed';
42
+ readonly error?: string;
43
+ };
44
+ /**
45
+ * Synthesize a reply through the session's active synthesizer. Cleans markdown
46
+ * for speech ({@link toSpeech}), truncates sensibly, and NEVER throws — every
47
+ * failure mode is a typed `ok:false` result so the turn path stays intact.
48
+ */
49
+ export declare function synthesizeReply(session: SynthesizerSource, text: string, opts?: SynthesizeReplyOptions): Promise<SynthesizeReplyResult>;
50
+ /** Map an audio mime type to a sensible file extension (for the attachment name
51
+ * a messenger uses to sniff the format). Falls back to `audio` when unknown. */
52
+ export declare function audioExtForMime(mimeType: string): string;
53
+ export interface EnsureOggOpusOptions {
54
+ /** Injectable `spawn` for tests. When set, the ffmpeg presence probe is NOT
55
+ * process-cached (so each test drives a deterministic outcome). */
56
+ readonly spawnImpl?: typeof spawn;
57
+ }
58
+ export interface EnsureOggOpusResult {
59
+ readonly audio: Uint8Array;
60
+ readonly mimeType: string;
61
+ /** True when ffmpeg ran to produce these bytes. */
62
+ readonly transcoded: boolean;
63
+ /** True when `audio` is OGG/Opus (safe to send as a native voice note); false
64
+ * when we returned the ORIGINAL bytes because ffmpeg was unavailable — the
65
+ * caller should then send plain audio instead of a voice note. */
66
+ readonly isOpus: boolean;
67
+ }
68
+ /**
69
+ * Ensure audio is OGG/Opus for a native voice note. Passthrough when it already
70
+ * is; otherwise transcode via ffmpeg (stdin→stdout). Gated on an ffmpeg
71
+ * presence probe (mirrors `checkVoiceCaptureAvailable`, cached per process). On
72
+ * no-ffmpeg — or any transcode failure — returns the ORIGINAL bytes with
73
+ * `isOpus:false` so the caller falls back to a plain audio message. Never throws.
74
+ */
75
+ export declare function ensureOggOpus(audio: Uint8Array, mimeType: string, opts?: EnsureOggOpusOptions): Promise<EnsureOggOpusResult>;
76
+ /** Reset the cached ffmpeg probe (tests only). */
77
+ export declare function __resetFfmpegProbeForTest(): void;
78
+ /** The transport boundary: a channel implements this to actually deliver the
79
+ * synthesized audio (grammy `sendVoice`/`sendAudio`, discord.js attachment). */
80
+ export interface VoiceReplySink {
81
+ send(audio: Uint8Array, meta: {
82
+ readonly mimeType: string;
83
+ /** Suggested attachment filename, e.g. `reply.ogg` / `reply.mp3`. */
84
+ readonly filename: string;
85
+ /** True when the bytes are OGG/Opus (send as a native voice note); false
86
+ * when they are plain audio (no ffmpeg to transcode). */
87
+ readonly isVoiceNote: boolean;
88
+ }): Promise<void>;
89
+ }
90
+ export type VoiceReplyOutcome = {
91
+ readonly status: 'sent';
92
+ readonly transcoded: boolean;
93
+ readonly isVoiceNote: boolean;
94
+ } | {
95
+ readonly status: 'skipped';
96
+ readonly reason: 'no-synthesizer' | 'empty';
97
+ } | {
98
+ readonly status: 'failed';
99
+ readonly reason: 'synth' | 'delivery';
100
+ readonly error?: string;
101
+ };
102
+ export type DeliverVoiceReplyOptions = SynthesizeReplyOptions & EnsureOggOpusOptions;
103
+ /**
104
+ * End-to-end best-effort voice reply: synthesize → ensure OGG/Opus → deliver
105
+ * through the channel's {@link VoiceReplySink}. NEVER throws — every failure is
106
+ * a typed outcome — so a caller can invoke it AFTER sending the text reply and
107
+ * be sure the (already-sent) text is never broken by a voice failure.
108
+ */
109
+ export declare function deliverVoiceReply(session: SynthesizerSource, text: string, sink: VoiceReplySink, opts?: DeliverVoiceReplyOptions): Promise<VoiceReplyOutcome>;
110
+ export interface VoiceToggleInput {
111
+ /** The `/voice` argument (`''`, `on`, `off`, `status`, or anything → toggle). */
112
+ readonly arg: string;
113
+ /** Current persisted state. */
114
+ readonly enabled: boolean;
115
+ /** Whether a synthesizer is active right now. */
116
+ readonly hasSynthesizer: boolean;
117
+ /** How this channel delivers audio, e.g. `a voice note` / `an audio file`. */
118
+ readonly delivery: string;
119
+ /** Install-guidance line appended when enabling with no active synthesizer. */
120
+ readonly noSynthesizerHint: string;
121
+ }
122
+ export interface VoiceToggleResult {
123
+ /** Desired enabled state after the command. */
124
+ readonly enabled: boolean;
125
+ /** Whether the caller should persist `enabled` (false for `status`). */
126
+ readonly persist: boolean;
127
+ /** User-facing reply text. */
128
+ readonly reply: string;
129
+ }
130
+ /**
131
+ * Resolve a channel-agnostic `/voice` toggle command into the new state + the
132
+ * reply to show. `on`/`off` are explicit, `status` just reports, anything else
133
+ * flips. Enabling with no active synthesizer still turns the preference on (so
134
+ * replies start speaking once a backend is installed) but appends install
135
+ * guidance. Wording is parameterized (`delivery`, `noSynthesizerHint`) so each
136
+ * messenger keeps its own phrasing.
137
+ */
138
+ export declare function resolveVoiceToggle(input: VoiceToggleInput): VoiceToggleResult;
139
+ //# sourceMappingURL=voice-reply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-reply.d.ts","sourceRoot":"","sources":["../src/voice-reply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,333 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { spawn } from 'node:child_process';
3
+ /**
4
+ * Strip markdown down to what should be *spoken*. Code fences become a short
5
+ * "(code omitted)" placeholder (reading a diff aloud is noise), links keep only
6
+ * their label, and the inline emphasis/heading/quote/bullet marks are removed
7
+ * so a TTS engine doesn't voice the punctuation. Whitespace is collapsed.
8
+ */
9
+ export function toSpeech(markdown) {
10
+ let t = markdown ?? '';
11
+ // Fenced code blocks (closed, then any unterminated trailing fence) → a short
12
+ // spoken placeholder. Must run BEFORE inline-code stripping (fences contain
13
+ // backticks).
14
+ t = t.replace(/```[\s\S]*?```/g, ' (code omitted) ');
15
+ t = t.replace(/```[\s\S]*$/g, ' (code omitted) ');
16
+ // Images `![alt](url)` → alt; links `[label](url)` → label.
17
+ t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1');
18
+ t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
19
+ // Inline code `x` → x.
20
+ t = t.replace(/`([^`]+)`/g, '$1');
21
+ // Emphasis / strikethrough marks.
22
+ t = t.replace(/(\*\*|__|~~|\*|_)/g, '');
23
+ // Leading heading / blockquote / list markers (line-anchored).
24
+ t = t.replace(/^\s{0,3}#{1,6}\s*/gm, '');
25
+ t = t.replace(/^\s{0,3}>\s?/gm, '');
26
+ t = t.replace(/^\s{0,3}[-*+]\s+/gm, '');
27
+ // Collapse runs of spaces/tabs and excess blank lines.
28
+ t = t.replace(/[ \t]+/g, ' ');
29
+ t = t.replace(/\n{3,}/g, '\n\n');
30
+ return t.trim();
31
+ }
32
+ /** Default ceiling on the characters we hand a TTS backend for one reply. Long
33
+ * answers are truncated at a sentence/word boundary so synthesis stays fast
34
+ * and cheap (a spoken 30-page essay helps no one). */
35
+ const DEFAULT_MAX_SPEECH_CHARS = 1_200;
36
+ function truncateForSpeech(text, max) {
37
+ if (text.length <= max)
38
+ return text;
39
+ const slice = text.slice(0, max);
40
+ const sentence = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('! '), slice.lastIndexOf('? '), slice.lastIndexOf('\n'));
41
+ const cut = sentence > max * 0.5 ? sentence + 1 : slice.lastIndexOf(' ');
42
+ const head = (cut > 0 ? slice.slice(0, cut) : slice).trim();
43
+ return `${head}…`;
44
+ }
45
+ /**
46
+ * Synthesize a reply through the session's active synthesizer. Cleans markdown
47
+ * for speech ({@link toSpeech}), truncates sensibly, and NEVER throws — every
48
+ * failure mode is a typed `ok:false` result so the turn path stays intact.
49
+ */
50
+ export async function synthesizeReply(session, text, opts = {}) {
51
+ let synth;
52
+ try {
53
+ synth = session.synthesizers.tryGetActive();
54
+ }
55
+ catch (err) {
56
+ return { ok: false, reason: 'failed', error: err instanceof Error ? err.message : String(err) };
57
+ }
58
+ if (!synth)
59
+ return { ok: false, reason: 'no-synthesizer' };
60
+ const speech = truncateForSpeech(toSpeech(text), opts.maxChars ?? DEFAULT_MAX_SPEECH_CHARS);
61
+ if (!speech)
62
+ return { ok: false, reason: 'empty' };
63
+ const synthOpts = {};
64
+ if (opts.voice !== undefined)
65
+ synthOpts.voice = opts.voice;
66
+ if (opts.language !== undefined)
67
+ synthOpts.language = opts.language;
68
+ if (opts.rate !== undefined)
69
+ synthOpts.rate = opts.rate;
70
+ if (opts.signal !== undefined)
71
+ synthOpts.signal = opts.signal;
72
+ try {
73
+ const result = await synth.synthesize(speech, synthOpts);
74
+ if (!result?.audio || result.audio.byteLength === 0) {
75
+ return { ok: false, reason: 'failed', error: 'synthesizer returned no audio' };
76
+ }
77
+ return { ok: true, audio: result.audio, mimeType: result.mimeType };
78
+ }
79
+ catch (err) {
80
+ return { ok: false, reason: 'failed', error: err instanceof Error ? err.message : String(err) };
81
+ }
82
+ }
83
+ /** True for a mime type Telegram accepts as a native voice note (OGG/Opus). */
84
+ function isOggOpusMime(mimeType) {
85
+ const m = mimeType.toLowerCase();
86
+ return m.includes('opus') || m.includes('ogg');
87
+ }
88
+ /** Map an audio mime type to a sensible file extension (for the attachment name
89
+ * a messenger uses to sniff the format). Falls back to `audio` when unknown. */
90
+ export function audioExtForMime(mimeType) {
91
+ const m = (mimeType || '').toLowerCase();
92
+ if (m.includes('opus') || m.includes('ogg'))
93
+ return 'ogg';
94
+ if (m.includes('mpeg') || m.includes('mp3'))
95
+ return 'mp3';
96
+ if (m.includes('wav'))
97
+ return 'wav';
98
+ if (m.includes('webm'))
99
+ return 'webm';
100
+ if (m.includes('aac') || m.includes('m4a') || m.includes('mp4'))
101
+ return 'm4a';
102
+ if (m.includes('flac'))
103
+ return 'flac';
104
+ return 'audio';
105
+ }
106
+ const TRANSCODE_ARGS = [
107
+ '-hide_banner',
108
+ '-loglevel',
109
+ 'error',
110
+ '-i',
111
+ 'pipe:0',
112
+ '-f',
113
+ 'ogg',
114
+ '-c:a',
115
+ 'libopus',
116
+ '-b:a',
117
+ '32k',
118
+ '-ac',
119
+ '1',
120
+ 'pipe:1',
121
+ ];
122
+ /** Ceiling on transcoded output we buffer (a runaway/adversarial input can't
123
+ * grow memory without bound). 25MB is well past any spoken reply. */
124
+ const MAX_TRANSCODE_OUTPUT_BYTES = 25 * 1024 * 1024;
125
+ /**
126
+ * Ensure audio is OGG/Opus for a native voice note. Passthrough when it already
127
+ * is; otherwise transcode via ffmpeg (stdin→stdout). Gated on an ffmpeg
128
+ * presence probe (mirrors `checkVoiceCaptureAvailable`, cached per process). On
129
+ * no-ffmpeg — or any transcode failure — returns the ORIGINAL bytes with
130
+ * `isOpus:false` so the caller falls back to a plain audio message. Never throws.
131
+ */
132
+ export async function ensureOggOpus(audio, mimeType, opts = {}) {
133
+ if (isOggOpusMime(mimeType)) {
134
+ return { audio, mimeType: 'audio/ogg', transcoded: false, isOpus: true };
135
+ }
136
+ const available = await probeFfmpeg(opts.spawnImpl);
137
+ if (!available)
138
+ return { audio, mimeType, transcoded: false, isOpus: false };
139
+ try {
140
+ const out = await transcodeToOggOpus(audio, opts.spawnImpl ?? spawn);
141
+ if (out.byteLength === 0)
142
+ return { audio, mimeType, transcoded: false, isOpus: false };
143
+ return { audio: out, mimeType: 'audio/ogg', transcoded: true, isOpus: true };
144
+ }
145
+ catch {
146
+ return { audio, mimeType, transcoded: false, isOpus: false };
147
+ }
148
+ }
149
+ // Process-cached ffmpeg availability (the probe spawns a subprocess; caching
150
+ // keeps a chatty channel from re-probing on every reply). Tests inject a
151
+ // `spawnImpl`, which bypasses the cache.
152
+ let ffmpegAvailable = null;
153
+ async function probeFfmpeg(spawnImpl) {
154
+ if (spawnImpl)
155
+ return runFfmpegProbe(spawnImpl);
156
+ ffmpegAvailable ??= runFfmpegProbe(spawn);
157
+ return ffmpegAvailable;
158
+ }
159
+ /** Reset the cached ffmpeg probe (tests only). */
160
+ export function __resetFfmpegProbeForTest() {
161
+ ffmpegAvailable = null;
162
+ }
163
+ function runFfmpegProbe(spawnImpl, command = 'ffmpeg', timeoutMs = 1_500) {
164
+ return new Promise((resolve) => {
165
+ let settled = false;
166
+ const done = (v) => {
167
+ if (settled)
168
+ return;
169
+ settled = true;
170
+ clearTimeout(timer);
171
+ resolve(v);
172
+ };
173
+ let child;
174
+ try {
175
+ child = spawnImpl(command, ['-version'], { stdio: ['ignore', 'ignore', 'ignore'] });
176
+ }
177
+ catch {
178
+ resolve(false);
179
+ return;
180
+ }
181
+ const timer = setTimeout(() => {
182
+ try {
183
+ if (!child.killed)
184
+ child.kill('SIGKILL');
185
+ }
186
+ catch {
187
+ /* ignore */
188
+ }
189
+ done(false);
190
+ }, timeoutMs);
191
+ timer.unref?.();
192
+ child.once('error', () => done(false));
193
+ child.once('close', (code) => done(code === 0));
194
+ });
195
+ }
196
+ function transcodeToOggOpus(audio, spawnImpl, command = 'ffmpeg', timeoutMs = 15_000) {
197
+ return new Promise((resolve, reject) => {
198
+ let child;
199
+ try {
200
+ child = spawnImpl(command, TRANSCODE_ARGS, { stdio: ['pipe', 'pipe', 'pipe'] });
201
+ }
202
+ catch (err) {
203
+ reject(err instanceof Error ? err : new Error(String(err)));
204
+ return;
205
+ }
206
+ const out = [];
207
+ const errChunks = [];
208
+ let outBytes = 0;
209
+ let over = false;
210
+ let settled = false;
211
+ const finish = (fn) => {
212
+ if (settled)
213
+ return;
214
+ settled = true;
215
+ clearTimeout(timer);
216
+ fn();
217
+ };
218
+ const timer = setTimeout(() => {
219
+ try {
220
+ if (!child.killed)
221
+ child.kill('SIGKILL');
222
+ }
223
+ catch {
224
+ /* ignore */
225
+ }
226
+ finish(() => reject(new Error('ffmpeg transcode timed out')));
227
+ }, timeoutMs);
228
+ timer.unref?.();
229
+ child.stdout?.on('data', (c) => {
230
+ if (over)
231
+ return;
232
+ if (outBytes + c.byteLength > MAX_TRANSCODE_OUTPUT_BYTES) {
233
+ over = true;
234
+ try {
235
+ child.kill('SIGKILL');
236
+ }
237
+ catch {
238
+ /* ignore */
239
+ }
240
+ return;
241
+ }
242
+ outBytes += c.byteLength;
243
+ out.push(Buffer.from(c));
244
+ });
245
+ child.stderr?.on('data', (c) => {
246
+ errChunks.push(Buffer.from(c));
247
+ while (Buffer.concat(errChunks).byteLength > 4_096)
248
+ errChunks.shift();
249
+ });
250
+ child.once('error', (err) => finish(() => reject(err instanceof Error ? err : new Error(String(err)))));
251
+ child.once('close', (code) => finish(() => {
252
+ if (code === 0 && out.length > 0) {
253
+ resolve(new Uint8Array(Buffer.concat(out)));
254
+ }
255
+ else if (code === 0) {
256
+ reject(new Error('ffmpeg produced no output'));
257
+ }
258
+ else {
259
+ reject(new Error(`ffmpeg exited ${code}: ${Buffer.concat(errChunks).toString('utf8').trim()}`));
260
+ }
261
+ }));
262
+ const stdin = child.stdin;
263
+ if (stdin) {
264
+ stdin.on('error', () => {
265
+ // EPIPE if ffmpeg died before consuming stdin — the close/error handler
266
+ // reports the real failure; swallow this to avoid an unhandled 'error'.
267
+ });
268
+ try {
269
+ stdin.end(Buffer.from(audio));
270
+ }
271
+ catch {
272
+ /* the close/error path reports it */
273
+ }
274
+ }
275
+ });
276
+ }
277
+ /**
278
+ * End-to-end best-effort voice reply: synthesize → ensure OGG/Opus → deliver
279
+ * through the channel's {@link VoiceReplySink}. NEVER throws — every failure is
280
+ * a typed outcome — so a caller can invoke it AFTER sending the text reply and
281
+ * be sure the (already-sent) text is never broken by a voice failure.
282
+ */
283
+ export async function deliverVoiceReply(session, text, sink, opts = {}) {
284
+ const synth = await synthesizeReply(session, text, opts);
285
+ if (!synth.ok) {
286
+ if (synth.reason === 'failed') {
287
+ return synth.error !== undefined
288
+ ? { status: 'failed', reason: 'synth', error: synth.error }
289
+ : { status: 'failed', reason: 'synth' };
290
+ }
291
+ return { status: 'skipped', reason: synth.reason };
292
+ }
293
+ const prepared = await ensureOggOpus(synth.audio, synth.mimeType, opts);
294
+ const filename = prepared.isOpus ? 'reply.ogg' : `reply.${audioExtForMime(prepared.mimeType)}`;
295
+ try {
296
+ await sink.send(prepared.audio, {
297
+ mimeType: prepared.mimeType,
298
+ filename,
299
+ isVoiceNote: prepared.isOpus,
300
+ });
301
+ }
302
+ catch (err) {
303
+ return { status: 'failed', reason: 'delivery', error: err instanceof Error ? err.message : String(err) };
304
+ }
305
+ return { status: 'sent', transcoded: prepared.transcoded, isVoiceNote: prepared.isOpus };
306
+ }
307
+ /**
308
+ * Resolve a channel-agnostic `/voice` toggle command into the new state + the
309
+ * reply to show. `on`/`off` are explicit, `status` just reports, anything else
310
+ * flips. Enabling with no active synthesizer still turns the preference on (so
311
+ * replies start speaking once a backend is installed) but appends install
312
+ * guidance. Wording is parameterized (`delivery`, `noSynthesizerHint`) so each
313
+ * messenger keeps its own phrasing.
314
+ */
315
+ export function resolveVoiceToggle(input) {
316
+ const arg = input.arg.trim().toLowerCase();
317
+ const hint = input.hasSynthesizer ? '' : `\n\n${input.noSynthesizerHint}`;
318
+ if (arg === 'status') {
319
+ const state = input.enabled ? 'ON' : 'OFF';
320
+ const note = input.enabled ? hint : '';
321
+ return { enabled: input.enabled, persist: false, reply: `Voice replies are ${state}.${note}` };
322
+ }
323
+ const desired = arg === 'on' ? true : arg === 'off' ? false : !input.enabled;
324
+ if (!desired) {
325
+ return { enabled: false, persist: true, reply: '🔇 Voice replies OFF.' };
326
+ }
327
+ return {
328
+ enabled: true,
329
+ persist: true,
330
+ reply: `🔊 Voice replies ON — I'll speak my final reply as ${input.delivery}.${hint}`,
331
+ };
332
+ }
333
+ //# sourceMappingURL=voice-reply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-reply.js","sourceRoot":"","sources":["../src/voice-reply.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@moxxy/channel-kit",
3
+ "version": "0.27.0",
4
+ "description": "Shared channel-building machinery for moxxy messaging channels: throttled send-once-then-edit frame pump, turnId-filtered turn running with single-flight coordination, pairing state machines (host-issued code + TOFU), vault-backed secret resolution, audited allow-list permissions, and an inbound-webhook ingest scaffold.",
5
+ "keywords": [
6
+ "moxxy",
7
+ "agent",
8
+ "channel",
9
+ "framework"
10
+ ],
11
+ "homepage": "https://moxxy.ai",
12
+ "bugs": {
13
+ "url": "https://github.com/moxxy-ai/moxxy/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/moxxy-ai/moxxy.git",
18
+ "directory": "packages/channel-kit"
19
+ },
20
+ "author": "Michal Makowski <michal.makowski97@gmail.com>",
21
+ "license": "MIT",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src"
37
+ ],
38
+ "dependencies": {
39
+ "@moxxy/sdk": "0.27.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.10.0",
43
+ "typescript": "^5.7.3",
44
+ "vitest": "^2.1.8",
45
+ "@moxxy/vitest-preset": "0.0.0",
46
+ "@moxxy/tsconfig": "0.0.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json",
50
+ "typecheck": "tsc -p tsconfig.json --noEmit",
51
+ "test": "vitest run",
52
+ "clean": "rm -rf dist .turbo"
53
+ }
54
+ }