@remnic/capture-audio 9.63.0 → 9.63.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.
package/dist/cli-bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // openclaw-engram: Local-first memory plugin
3
3
  import {
4
4
  runCapture
5
- } from "./chunk-MFWH245M.js";
5
+ } from "./chunk-4Z3DZYEB.js";
6
6
 
7
7
  // src/cli-bin.ts
8
8
  runCapture({ argv: process.argv.slice(2) }).then((code) => {
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ declare const CAPTURE_AUDIO_VERSION = "9.14.0";
12
12
  declare const DEFAULT_HOST = "127.0.0.1";
13
13
  declare const DEFAULT_PORT = 4340;
14
14
  /** Spool schema version, persisted in the `meta` table. */
15
- declare const SPOOL_SCHEMA_VERSION = 2;
15
+ declare const SPOOL_SCHEMA_VERSION = 3;
16
16
 
17
17
  /**
18
18
  * Error taxonomy for @remnic/capture-audio.
@@ -142,6 +142,166 @@ declare function encodeCursor(startedAtUtc: string, id: string): string;
142
142
  /** Absent cursor → null (first page); malformed cursor → 400. */
143
143
  declare function decodeCursor(value: string | null | undefined): Cursor | null;
144
144
 
145
+ /**
146
+ * Native capture helper resolver + supervised process runner (issue #1897,
147
+ * "audio native macOS helper" slice — Node side only).
148
+ *
149
+ * The native recorder is the ONE shared macOS helper shipped by #2138
150
+ * (`remnic-capture-helper`), driven here through its `audio-capture`
151
+ * subcommand. It emits one JSONL `ChunkEvent` per recorded WAV chunk on
152
+ * stdout. This module is deliberately à-la-carte, mirroring the VAD/STT
153
+ * adapters and the screen daemon's helper seam:
154
+ *
155
+ * - The helper ships as an OPTIONAL, per-platform package
156
+ * (`@remnic/capture-native-darwin-arm64` / `-x64`) that exports a
157
+ * `helperBinaryPath` and declares the same binary under `bin`. It is a
158
+ * peer dependency, never a runtime dependency, so `@remnic/capture-audio`
159
+ * installs and works on any platform without it.
160
+ * - The package specifier is COMPUTED from `process.platform`/`arch` so a
161
+ * static importer never bundles a foreign-arch binary, and resolution uses
162
+ * Node module resolution (`require.resolve`).
163
+ * - `REMNIC_CAPTURE_HELPER_BIN` overrides resolution with an explicit binary
164
+ * path (manual installs and the hardware-free test seam, which points it at
165
+ * a fake script emitting canned JSON).
166
+ * - A missing optional package reports the EXACT install command instead of a
167
+ * raw resolver error.
168
+ *
169
+ * The runner is the sole owner of the child process: it spawns the helper,
170
+ * parses stdout strictly line-by-line, reports validated events to a callback,
171
+ * reports stderr/errors separately, and restarts only UNEXPECTED exits with
172
+ * bounded exponential backoff. It never writes the Spool and never invents a
173
+ * conversation — the processing/assembly layer owns eventual Spool writes
174
+ * downstream of the validated events this runner surfaces.
175
+ */
176
+ /** One recorded audio chunk, as emitted by the native helper on stdout (JSONL). */
177
+ interface ChunkEvent {
178
+ path: string;
179
+ channel: "mic" | "system";
180
+ startedAtUtc: string;
181
+ endedAtUtc: string;
182
+ device: string | null;
183
+ }
184
+ /** Which channels the `audio-capture` subcommand records. */
185
+ type ChannelSelection = "mic" | "system" | "both";
186
+ /** A resolved native helper: its source specifier and the on-disk binary path. */
187
+ interface HelperResolution {
188
+ specifier: string;
189
+ binaryPath: string;
190
+ }
191
+ /** The narrow child-process surface the runner depends on (injectable for tests). */
192
+ interface HelperChild {
193
+ stdout: {
194
+ on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
195
+ };
196
+ stderr: {
197
+ on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
198
+ };
199
+ once(event: "error", listener: (err: Error) => void): unknown;
200
+ once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
201
+ kill(signal?: NodeJS.Signals): boolean;
202
+ readonly killed?: boolean;
203
+ readonly pid?: number;
204
+ }
205
+ /** Spawns the helper binary. Defaults to a `node:child_process` adapter. */
206
+ type HelperSpawn = (binaryPath: string, args: string[]) => HelperChild;
207
+ /** An opaque restart-timer token returned by `scheduleRestart`. */
208
+ type RestartTimer = unknown;
209
+ interface ResolveHelperDeps {
210
+ platform?: NodeJS.Platform;
211
+ arch?: string;
212
+ /** `require.resolve`-style resolver; defaults to this module's require. */
213
+ resolve?: (specifier: string) => string;
214
+ readFile?: (file: string) => string;
215
+ /** Environment source for the `REMNIC_CAPTURE_HELPER_BIN` override. */
216
+ env?: NodeJS.ProcessEnv;
217
+ }
218
+ interface NativeRunnerOptions {
219
+ /** Directory the helper writes WAV chunks into (`audio-capture --out`). */
220
+ outDir: string;
221
+ chunkSeconds: number;
222
+ /** Channels to record; defaults to "both". */
223
+ channel?: ChannelSelection;
224
+ /** Optional CoreAudio microphone device UID (`--device`). */
225
+ device?: string | null;
226
+ /** Called once per validated ChunkEvent. */
227
+ onChunk: (event: ChunkEvent) => void;
228
+ /** Called for a rejected stdout line or a spawn/child error. */
229
+ onError?: (error: Error) => void;
230
+ /** Called once per complete stderr line. */
231
+ onStderr?: (line: string) => void;
232
+ /** Pre-resolved helper; when absent the runner resolves it lazily on `start()`. */
233
+ resolution?: HelperResolution;
234
+ resolveBinary?: (deps: ResolveHelperDeps) => HelperResolution;
235
+ spawn?: HelperSpawn;
236
+ /** Max consecutive unexpected restarts before giving up (default 5). */
237
+ maxRestarts?: number;
238
+ /** First backoff delay in ms (default 500). */
239
+ baseBackoffMs?: number;
240
+ /** Backoff ceiling in ms (default 30000). */
241
+ maxBackoffMs?: number;
242
+ scheduleRestart?: (fn: () => void, delayMs: number) => RestartTimer;
243
+ cancelRestart?: (timer: RestartTimer) => void;
244
+ }
245
+ /** A running native-capture supervisor. */
246
+ interface NativeCaptureRunner {
247
+ start(): void;
248
+ /** Stop the helper (SIGTERM) and resolve once it exits and its final chunk is read. */
249
+ stop(): Promise<void>;
250
+ /** True between a `start()` and its matching `stop()`. */
251
+ readonly running: boolean;
252
+ }
253
+ /** The env var that overrides package resolution with an explicit binary path. */
254
+ declare const HELPER_BIN_ENV = "REMNIC_CAPTURE_HELPER_BIN";
255
+ /**
256
+ * Compute the optional native-helper package specifier for a platform/arch.
257
+ * The helper is macOS-only and hardware-gated; every other platform (and any
258
+ * unsupported macOS architecture) throws loudly rather than resolving to a
259
+ * package that cannot exist.
260
+ */
261
+ declare function helperPackageSpecifier(platform: NodeJS.Platform | string, arch: string): string;
262
+ /**
263
+ * Resolve the native helper binary. Order: explicit `REMNIC_CAPTURE_HELPER_BIN`
264
+ * override, then the computed platform package's declared executable (resolved
265
+ * via Node module resolution, identical to its `helperBinaryPath` export).
266
+ * Throws a CaptureConfigError naming the exact install command when the
267
+ * optional package is not installed.
268
+ */
269
+ declare function resolveHelperBinary(deps?: ResolveHelperDeps): HelperResolution;
270
+ /** Build the `audio-capture` argv from runner options (#2138 helper contract). */
271
+ declare function buildHelperArgs(opts: Pick<NativeRunnerOptions, "outDir" | "chunkSeconds" | "channel" | "device">): string[];
272
+ /** Parse and validate one JSONL line into a ChunkEvent; throws on anything malformed. */
273
+ declare function parseChunkEvent(line: string): ChunkEvent;
274
+ /**
275
+ * Run the helper's one-shot `device-enumerate` subcommand and return the parsed
276
+ * device list. Bounded, argv-only, and injectable for tests. Throws a
277
+ * CaptureInputError on a nonzero exit, empty output, or invalid JSON.
278
+ */
279
+ declare function enumerateDevices(binaryPath: string, spawn?: HelperSpawn, timeoutMs?: number): Promise<unknown[]>;
280
+ /**
281
+ * Create a supervised native-capture runner. Dependency-injectable: pass
282
+ * `spawn`, `resolution`/`resolveBinary`, and `scheduleRestart`/`cancelRestart`
283
+ * to drive it deterministically in tests.
284
+ */
285
+ declare function createNativeCaptureRunner(options: NativeRunnerOptions): NativeCaptureRunner;
286
+
287
+ /** Ceiling on chunks held in the reorder buffer. */
288
+ declare const MAX_BUFFERED_CHUNKS = 512;
289
+ /** Consecutive apply failures before a chunk is parked. */
290
+ declare const QUARANTINE_AFTER_FAILURES = 3;
291
+ type PendingChunkReason = "evicted" | "quarantined";
292
+ interface PendingChunkInput {
293
+ id: string;
294
+ wavPath: string;
295
+ startedAtUtc: string;
296
+ endedAtUtc: string;
297
+ channel: ChunkEvent["channel"];
298
+ device: string | null;
299
+ reason: PendingChunkReason;
300
+ }
301
+ interface PendingChunkRecord extends PendingChunkInput {
302
+ createdAtUtc: string;
303
+ }
304
+
145
305
  /**
146
306
  * SQLite spool — the daemon's local buffer of captured conversations.
147
307
  *
@@ -162,6 +322,7 @@ declare function decodeCursor(value: string | null | undefined): Cursor | null;
162
322
  * connector never ingests half a meeting and pagination is deterministic
163
323
  * even when two conversations share a start timestamp.
164
324
  */
325
+
165
326
  type ConversationState = "capturing" | "final";
166
327
  type ChunkStatus = "pending" | "transcribed" | "failed" | "deleted";
167
328
  interface SegmentInput {
@@ -408,6 +569,9 @@ declare class Spool {
408
569
  readSpeakerClusters(): SpeakerClusterRow[];
409
570
  listSpeakers(): SpeakerRow[];
410
571
  pendingChunkCount(): number;
572
+ recordPendingChunk(input: PendingChunkInput): void;
573
+ listPendingChunks(reason?: PendingChunkReason): PendingChunkRecord[];
574
+ deletePendingChunk(id: string): void;
411
575
  stats(): {
412
576
  conversations: number;
413
577
  segments: number;
@@ -863,148 +1027,6 @@ declare class SpeakerClusterer {
863
1027
  clusters(): SpeakerCluster[];
864
1028
  }
865
1029
 
866
- /**
867
- * Native capture helper resolver + supervised process runner (issue #1897,
868
- * "audio native macOS helper" slice — Node side only).
869
- *
870
- * The native recorder is the ONE shared macOS helper shipped by #2138
871
- * (`remnic-capture-helper`), driven here through its `audio-capture`
872
- * subcommand. It emits one JSONL `ChunkEvent` per recorded WAV chunk on
873
- * stdout. This module is deliberately à-la-carte, mirroring the VAD/STT
874
- * adapters and the screen daemon's helper seam:
875
- *
876
- * - The helper ships as an OPTIONAL, per-platform package
877
- * (`@remnic/capture-native-darwin-arm64` / `-x64`) that exports a
878
- * `helperBinaryPath` and declares the same binary under `bin`. It is a
879
- * peer dependency, never a runtime dependency, so `@remnic/capture-audio`
880
- * installs and works on any platform without it.
881
- * - The package specifier is COMPUTED from `process.platform`/`arch` so a
882
- * static importer never bundles a foreign-arch binary, and resolution uses
883
- * Node module resolution (`require.resolve`).
884
- * - `REMNIC_CAPTURE_HELPER_BIN` overrides resolution with an explicit binary
885
- * path (manual installs and the hardware-free test seam, which points it at
886
- * a fake script emitting canned JSON).
887
- * - A missing optional package reports the EXACT install command instead of a
888
- * raw resolver error.
889
- *
890
- * The runner is the sole owner of the child process: it spawns the helper,
891
- * parses stdout strictly line-by-line, reports validated events to a callback,
892
- * reports stderr/errors separately, and restarts only UNEXPECTED exits with
893
- * bounded exponential backoff. It never writes the Spool and never invents a
894
- * conversation — the processing/assembly layer owns eventual Spool writes
895
- * downstream of the validated events this runner surfaces.
896
- */
897
- /** One recorded audio chunk, as emitted by the native helper on stdout (JSONL). */
898
- interface ChunkEvent {
899
- path: string;
900
- channel: "mic" | "system";
901
- startedAtUtc: string;
902
- endedAtUtc: string;
903
- device: string | null;
904
- }
905
- /** Which channels the `audio-capture` subcommand records. */
906
- type ChannelSelection = "mic" | "system" | "both";
907
- /** A resolved native helper: its source specifier and the on-disk binary path. */
908
- interface HelperResolution {
909
- specifier: string;
910
- binaryPath: string;
911
- }
912
- /** The narrow child-process surface the runner depends on (injectable for tests). */
913
- interface HelperChild {
914
- stdout: {
915
- on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
916
- };
917
- stderr: {
918
- on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
919
- };
920
- once(event: "error", listener: (err: Error) => void): unknown;
921
- once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
922
- kill(signal?: NodeJS.Signals): boolean;
923
- readonly killed?: boolean;
924
- readonly pid?: number;
925
- }
926
- /** Spawns the helper binary. Defaults to a `node:child_process` adapter. */
927
- type HelperSpawn = (binaryPath: string, args: string[]) => HelperChild;
928
- /** An opaque restart-timer token returned by `scheduleRestart`. */
929
- type RestartTimer = unknown;
930
- interface ResolveHelperDeps {
931
- platform?: NodeJS.Platform;
932
- arch?: string;
933
- /** `require.resolve`-style resolver; defaults to this module's require. */
934
- resolve?: (specifier: string) => string;
935
- readFile?: (file: string) => string;
936
- /** Environment source for the `REMNIC_CAPTURE_HELPER_BIN` override. */
937
- env?: NodeJS.ProcessEnv;
938
- }
939
- interface NativeRunnerOptions {
940
- /** Directory the helper writes WAV chunks into (`audio-capture --out`). */
941
- outDir: string;
942
- chunkSeconds: number;
943
- /** Channels to record; defaults to "both". */
944
- channel?: ChannelSelection;
945
- /** Optional CoreAudio microphone device UID (`--device`). */
946
- device?: string | null;
947
- /** Called once per validated ChunkEvent. */
948
- onChunk: (event: ChunkEvent) => void;
949
- /** Called for a rejected stdout line or a spawn/child error. */
950
- onError?: (error: Error) => void;
951
- /** Called once per complete stderr line. */
952
- onStderr?: (line: string) => void;
953
- /** Pre-resolved helper; when absent the runner resolves it lazily on `start()`. */
954
- resolution?: HelperResolution;
955
- resolveBinary?: (deps: ResolveHelperDeps) => HelperResolution;
956
- spawn?: HelperSpawn;
957
- /** Max consecutive unexpected restarts before giving up (default 5). */
958
- maxRestarts?: number;
959
- /** First backoff delay in ms (default 500). */
960
- baseBackoffMs?: number;
961
- /** Backoff ceiling in ms (default 30000). */
962
- maxBackoffMs?: number;
963
- scheduleRestart?: (fn: () => void, delayMs: number) => RestartTimer;
964
- cancelRestart?: (timer: RestartTimer) => void;
965
- }
966
- /** A running native-capture supervisor. */
967
- interface NativeCaptureRunner {
968
- start(): void;
969
- /** Stop the helper (SIGTERM) and resolve once it exits and its final chunk is read. */
970
- stop(): Promise<void>;
971
- /** True between a `start()` and its matching `stop()`. */
972
- readonly running: boolean;
973
- }
974
- /** The env var that overrides package resolution with an explicit binary path. */
975
- declare const HELPER_BIN_ENV = "REMNIC_CAPTURE_HELPER_BIN";
976
- /**
977
- * Compute the optional native-helper package specifier for a platform/arch.
978
- * The helper is macOS-only and hardware-gated; every other platform (and any
979
- * unsupported macOS architecture) throws loudly rather than resolving to a
980
- * package that cannot exist.
981
- */
982
- declare function helperPackageSpecifier(platform: NodeJS.Platform | string, arch: string): string;
983
- /**
984
- * Resolve the native helper binary. Order: explicit `REMNIC_CAPTURE_HELPER_BIN`
985
- * override, then the computed platform package's declared executable (resolved
986
- * via Node module resolution, identical to its `helperBinaryPath` export).
987
- * Throws a CaptureConfigError naming the exact install command when the
988
- * optional package is not installed.
989
- */
990
- declare function resolveHelperBinary(deps?: ResolveHelperDeps): HelperResolution;
991
- /** Build the `audio-capture` argv from runner options (#2138 helper contract). */
992
- declare function buildHelperArgs(opts: Pick<NativeRunnerOptions, "outDir" | "chunkSeconds" | "channel" | "device">): string[];
993
- /** Parse and validate one JSONL line into a ChunkEvent; throws on anything malformed. */
994
- declare function parseChunkEvent(line: string): ChunkEvent;
995
- /**
996
- * Run the helper's one-shot `device-enumerate` subcommand and return the parsed
997
- * device list. Bounded, argv-only, and injectable for tests. Throws a
998
- * CaptureInputError on a nonzero exit, empty output, or invalid JSON.
999
- */
1000
- declare function enumerateDevices(binaryPath: string, spawn?: HelperSpawn, timeoutMs?: number): Promise<unknown[]>;
1001
- /**
1002
- * Create a supervised native-capture runner. Dependency-injectable: pass
1003
- * `spawn`, `resolution`/`resolveBinary`, and `scheduleRestart`/`cancelRestart`
1004
- * to drive it deterministically in tests.
1005
- */
1006
- declare function createNativeCaptureRunner(options: NativeRunnerOptions): NativeCaptureRunner;
1007
-
1008
1030
  /**
1009
1031
  * Chunk processor (issue #1897) — turns completed native WAV chunk events
1010
1032
  * into durable, replay-safe conversations in the spool.
@@ -1073,9 +1095,20 @@ interface ChunkProcessor {
1073
1095
  /** Drain, then flip open conversations to `final`; returns the count closed. */
1074
1096
  finalize(): Promise<number>;
1075
1097
  }
1098
+
1076
1099
  declare function chunkStableId(event: ChunkEvent): string;
1077
1100
  declare function createChunkProcessor(deps: ChunkProcessorDeps): ChunkProcessor;
1078
1101
 
1102
+ interface OrphanScanInput {
1103
+ rawDirectory: string;
1104
+ spool: Spool;
1105
+ }
1106
+ /**
1107
+ * Rebuild chunk events from durable pending rows and leftover WAVs so a
1108
+ * restart can feed them through the live processor (issue #2379).
1109
+ */
1110
+ declare function scanOrphanedChunks(input: OrphanScanInput): ChunkEvent[];
1111
+
1079
1112
  /**
1080
1113
  * Live capture wiring (issue #1897) — assembles the native helper runner and
1081
1114
  * the chunk processor into one start/stop unit the daemon drives.
@@ -1217,4 +1250,4 @@ interface EnrollSelfResult {
1217
1250
  */
1218
1251
  declare function enrollSelf(input: EnrollSelfInput): EnrollSelfResult;
1219
1252
 
1220
- export { type AssembledConversation, type AssemblerOptions, type AssemblyAppendInput, type AssemblyAppendResult, type AssemblySegment, CAPTURE_AUDIO_VERSION, CaptureConfigError, CaptureInputError, type CapturePaths, type ChannelSelection, type ChunkEvent, type ChunkProcessor, type ChunkProcessorDeps, type ChunkStatus, type ChunkTranscribeInput, type CliIo, ConversationAssembler, type ConversationInput, type ConversationPage, type ConversationState, type Cursor, DEFAULT_CONVERSATION_GAP_MINUTES, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SERVICE_LABEL, DESKTOP_SOURCE_ID, type DaemonConfig, type DaemonConversation, type DaemonDeps, type DaemonHandle, type DaemonSegment, type DedupSegment, DesktopDaemonError, type DeviceConfig, type DiarizationConfig, type Embedding, type EnrollSelfInput, type EnrollSelfResult, HELPER_BIN_ENV, type HelperResolution, type HelperSpawn, type LiveCapture, type LiveCaptureOptions, type ModelDownloadInput, type ModelDownloadResult, type NativeCaptureRunner, type NativeRunnerOptions, type PidRecord, type QueryFinalOptions, REPLAY_COMMIT_BATCH, type ReplayResult, type ResolveHelperDeps, SELF_SPEAKER_ID, SPOOL_SCHEMA_VERSION, type SegmentInput, type ServicePlan, type ServiceSpec, type SherpaOnnxModule, type SileroVadInput, type SpeakerCluster, type SpeakerClusterRow, SpeakerClusterer, type SpeakerInput, type SpeakerRow, Spool, type SttConfig, type TranscribedSegment, type VadConfig, type WhisperRunResult, type WhisperTranscriptionInput, assembleConversations, assertValidTimezone, bearerFromHeader, buildHelperArgs, buildWhisperArgs, captureBaseDir, capturePaths, chunkStableId, cosineSimilarity, createChunkProcessor, createDesktopConnector, createLiveCapture, createNativeCaptureRunner, createRequestHandler, createSileroVad, daemonConversationToWearable, decodeCursor, dedupeCrossChannel, defaultDaemonConfig, downloadWhisperModel, encodeCursor, enrollSelf, ensureDesktopConnectorRegistered, enumerateDevices, generateToken, helperPackageSpecifier, ingestReplayDir, ingestReplayDirResponsive, installService, isProcessAlive, loadDaemonConfig, loadOrCreateToken, loadSherpaOnnx, parseChunkEvent, parseDaemonConfig, parseLimit, parseTranscriptDate, parseWhisperJson, planService, pruneExpiredRawAudio, readPidFile, readPidRecord, removePidFile, removePidFileIfOwner, renderLaunchAgent, renderSystemdUnit, resolveCaptureAudioToken, resolveHelperBinary, resolveModelPath, runCapture, runWhisperCli, serializeDaemonConfig, sileroVadConfig, startDaemon, superviseReplay, tokensMatch, transcribeWithWhisper, uninstallService, wearableConnectorRegistration, whisperModelUrl, wordJaccard, writePidFile };
1253
+ export { type AssembledConversation, type AssemblerOptions, type AssemblyAppendInput, type AssemblyAppendResult, type AssemblySegment, CAPTURE_AUDIO_VERSION, CaptureConfigError, CaptureInputError, type CapturePaths, type ChannelSelection, type ChunkEvent, type ChunkProcessor, type ChunkProcessorDeps, type ChunkStatus, type ChunkTranscribeInput, type CliIo, ConversationAssembler, type ConversationInput, type ConversationPage, type ConversationState, type Cursor, DEFAULT_CONVERSATION_GAP_MINUTES, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SERVICE_LABEL, DESKTOP_SOURCE_ID, type DaemonConfig, type DaemonConversation, type DaemonDeps, type DaemonHandle, type DaemonSegment, type DedupSegment, DesktopDaemonError, type DeviceConfig, type DiarizationConfig, type Embedding, type EnrollSelfInput, type EnrollSelfResult, HELPER_BIN_ENV, type HelperResolution, type HelperSpawn, type LiveCapture, type LiveCaptureOptions, MAX_BUFFERED_CHUNKS, type ModelDownloadInput, type ModelDownloadResult, type NativeCaptureRunner, type NativeRunnerOptions, type PidRecord, QUARANTINE_AFTER_FAILURES, type QueryFinalOptions, REPLAY_COMMIT_BATCH, type ReplayResult, type ResolveHelperDeps, SELF_SPEAKER_ID, SPOOL_SCHEMA_VERSION, type SegmentInput, type ServicePlan, type ServiceSpec, type SherpaOnnxModule, type SileroVadInput, type SpeakerCluster, type SpeakerClusterRow, SpeakerClusterer, type SpeakerInput, type SpeakerRow, Spool, type SttConfig, type TranscribedSegment, type VadConfig, type WhisperRunResult, type WhisperTranscriptionInput, assembleConversations, assertValidTimezone, bearerFromHeader, buildHelperArgs, buildWhisperArgs, captureBaseDir, capturePaths, chunkStableId, cosineSimilarity, createChunkProcessor, createDesktopConnector, createLiveCapture, createNativeCaptureRunner, createRequestHandler, createSileroVad, daemonConversationToWearable, decodeCursor, dedupeCrossChannel, defaultDaemonConfig, downloadWhisperModel, encodeCursor, enrollSelf, ensureDesktopConnectorRegistered, enumerateDevices, generateToken, helperPackageSpecifier, ingestReplayDir, ingestReplayDirResponsive, installService, isProcessAlive, loadDaemonConfig, loadOrCreateToken, loadSherpaOnnx, parseChunkEvent, parseDaemonConfig, parseLimit, parseTranscriptDate, parseWhisperJson, planService, pruneExpiredRawAudio, readPidFile, readPidRecord, removePidFile, removePidFileIfOwner, renderLaunchAgent, renderSystemdUnit, resolveCaptureAudioToken, resolveHelperBinary, resolveModelPath, runCapture, runWhisperCli, scanOrphanedChunks, serializeDaemonConfig, sileroVadConfig, startDaemon, superviseReplay, tokensMatch, transcribeWithWhisper, uninstallService, wearableConnectorRegistration, whisperModelUrl, wordJaccard, writePidFile };
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  DEFAULT_PORT,
10
10
  DEFAULT_SERVICE_LABEL,
11
11
  HELPER_BIN_ENV,
12
+ MAX_BUFFERED_CHUNKS,
13
+ QUARANTINE_AFTER_FAILURES,
12
14
  REPLAY_COMMIT_BATCH,
13
15
  SELF_SPEAKER_ID,
14
16
  SPOOL_SCHEMA_VERSION,
@@ -61,6 +63,7 @@ import {
61
63
  resolveModelPath,
62
64
  runCapture,
63
65
  runWhisperCli,
66
+ scanOrphanedChunks,
64
67
  serializeDaemonConfig,
65
68
  startDaemon,
66
69
  superviseReplay,
@@ -70,7 +73,7 @@ import {
70
73
  whisperModelUrl,
71
74
  wordJaccard,
72
75
  writePidFile
73
- } from "./chunk-MFWH245M.js";
76
+ } from "./chunk-4Z3DZYEB.js";
74
77
 
75
78
  // src/vad.ts
76
79
  import { statSync } from "fs";
@@ -334,6 +337,8 @@ export {
334
337
  DESKTOP_SOURCE_ID,
335
338
  DesktopDaemonError,
336
339
  HELPER_BIN_ENV,
340
+ MAX_BUFFERED_CHUNKS,
341
+ QUARANTINE_AFTER_FAILURES,
337
342
  REPLAY_COMMIT_BATCH,
338
343
  SELF_SPEAKER_ID,
339
344
  SPOOL_SCHEMA_VERSION,
@@ -390,6 +395,7 @@ export {
390
395
  resolveModelPath,
391
396
  runCapture,
392
397
  runWhisperCli,
398
+ scanOrphanedChunks,
393
399
  serializeDaemonConfig,
394
400
  sileroVadConfig,
395
401
  startDaemon,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/vad.ts","../src/connector.ts"],"sourcesContent":["import { statSync } from \"node:fs\";\n\nimport { CaptureConfigError } from \"./errors.js\";\nimport { expandTilde } from \"./paths.js\";\n\n\nexport interface SileroVadInput {\n modelPath: string;\n minSpeechMs: number;\n minSilenceMs?: number;\n maxSpeechMs?: number;\n threshold?: number;\n threads?: number;\n}\n\nexport interface SherpaOnnxModule {\n Vad: new (config: unknown, bufferSeconds: number) => unknown;\n}\n\nexport function sileroVadConfig(input: SileroVadInput): { config: object; bufferSeconds: number } {\n if (typeof input.modelPath !== \"string\" || input.modelPath.trim() === \"\") {\n throw new CaptureConfigError(\"Silero VAD modelPath must be a non-empty string\");\n }\n if (!Number.isFinite(input.minSpeechMs) || input.minSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD minSpeechMs must be positive\");\n }\n const minSilenceMs = input.minSilenceMs ?? 500;\n const maxSpeechMs = input.maxSpeechMs ?? 30_000;\n const threshold = input.threshold ?? 0.5;\n const threads = input.threads ?? 1;\n if (!Number.isFinite(minSilenceMs) || minSilenceMs < 0 || !Number.isFinite(maxSpeechMs) || maxSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD duration settings are invalid\");\n }\n if (maxSpeechMs < input.minSpeechMs) {\n throw new CaptureConfigError(\"Silero VAD maxSpeechMs must be greater than or equal to minSpeechMs\");\n }\n if (!Number.isFinite(threshold) || threshold <= 0 || threshold >= 1) {\n throw new CaptureConfigError(\"Silero VAD threshold must be between 0 and 1\");\n }\n if (!Number.isInteger(threads) || threads <= 0) {\n throw new CaptureConfigError(\"Silero VAD threads must be a positive integer\");\n }\n return {\n config: {\n sileroVad: {\n model: expandTilde(input.modelPath),\n threshold,\n minSpeechDuration: input.minSpeechMs / 1_000,\n minSilenceDuration: minSilenceMs / 1_000,\n maxSpeechDuration: maxSpeechMs / 1_000,\n windowSize: 512,\n },\n sampleRate: 16_000,\n debug: false,\n numThreads: threads,\n },\n bufferSeconds: 60,\n };\n}\n\nexport function resolveSherpaExport(module: unknown): SherpaOnnxModule | null {\n const candidates = [module, (module as { default?: unknown } | null)?.default];\n for (const candidate of candidates) {\n if (candidate && typeof candidate === \"object\" && typeof (candidate as SherpaOnnxModule).Vad === \"function\") {\n return candidate as SherpaOnnxModule;\n }\n }\n return null;\n}\n\nexport async function loadSherpaOnnx(\n importModule: (specifier: string) => Promise<unknown> = (specifier) => import(specifier),\n): Promise<SherpaOnnxModule> {\n // Computed specifier keeps the optional sherpa-onnx-node peer out of the static\n // dependency graph so the package builds and runs without it installed.\n const specifier = \"sherpa-onnx-\" + \"node\";\n let module: unknown;\n try {\n module = await importModule(specifier);\n } catch (error) {\n // A genuinely absent package surfaces as ERR_MODULE_NOT_FOUND naming the\n // specifier as a package, with no require stack. An installed-but-broken\n // package (missing native .node, dlopen failure) has a different code\n // and/or a require stack, so it must NOT be reported as \"not installed\".\n const err = error as NodeJS.ErrnoException & { requireStack?: unknown };\n const message = error instanceof Error ? error.message : \"\";\n const packageMissing =\n err.code === \"ERR_MODULE_NOT_FOUND\" &&\n err.requireStack === undefined &&\n message.includes(`package '${specifier}'`);\n if (packageMissing) {\n throw new CaptureConfigError(\n \"Silero VAD requires optional dependency sherpa-onnx-node; install it before enabling VAD\",\n );\n }\n // Installed but unusable. Report that distinctly, but keep the message\n // operator-safe: never echo the raw error (it can leak absolute native\n // library paths), per the CaptureConfigError contract.\n throw new CaptureConfigError(\n \"Silero VAD could not load the installed sherpa-onnx-node native runtime; verify its native build and shared-library dependencies\",\n );\n }\n const resolved = resolveSherpaExport(module);\n if (!resolved) {\n throw new CaptureConfigError(\"sherpa-onnx-node does not expose the Vad API required by capture-audio\");\n }\n return resolved;\n}\n\nfunction isRegularFile(filePath: string): boolean {\n try {\n return statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\nexport async function createSileroVad(\n input: SileroVadInput,\n load: () => Promise<SherpaOnnxModule> = loadSherpaOnnx,\n exists: (path: string) => boolean = isRegularFile,\n): Promise<unknown> {\n const { config, bufferSeconds } = sileroVadConfig(input);\n const modelPath = expandTilde(input.modelPath);\n if (!exists(modelPath)) {\n throw new CaptureConfigError(`Silero VAD model not found at ${modelPath}; set vad.modelPath to a readable model file`);\n }\n const { Vad } = await load();\n return new Vad(config, bufferSeconds);\n}\n","/**\n * `desktop` wearable source connector (issue #1897, component 4).\n *\n * À-la-carte optional companion of @remnic/core: installing core alone\n * never pulls this in; core discovers it at runtime via a\n * computed-specifier dynamic import (registry entry {id:\"desktop\",\n * suffix:\"capture-audio\"}) or via a direct import of @remnic/capture-audio,\n * which self-registers idempotently.\n *\n * The connector is a pure API client + normalizer over the capture-audio\n * daemon's loopback HTTP API: no file IO beyond reading the local token,\n * no memory writes, no pipeline behavior (all of that stays in core so\n * desktop audio gets the same cleanup/corrections/trust gating as every\n * other wearable source).\n *\n * Token resolution (in order): settings.apiKey (config) ->\n * REMNIC_CAPTURE_AUDIO_TOKEN env -> the daemon's local token file\n * (~/.remnic/capture/token) when the base URL is loopback.\n */\n\nimport { readFileSync } from \"node:fs\";\n\nimport {\n registerWearableConnector,\n getWearableConnector,\n type WearableAuthCheck,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableConversation,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableSourceConnector,\n type WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport { DEFAULT_HOST, DEFAULT_PORT } from \"./constants.js\";\nimport { capturePaths } from \"./paths.js\";\nimport { isLoopbackHost } from \"./util.js\";\nimport type { ConversationPage, DaemonConversation, DaemonSegment } from \"./spool.js\";\n\nexport const DESKTOP_SOURCE_ID = \"desktop\";\nexport const DESKTOP_DISPLAY_NAME = \"Desktop audio\";\nconst DEFAULT_BASE_URL = `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;\n\n/** Bounded probe timeout so a wedged daemon reports unreachable instead of hanging. */\nconst PROBE_TIMEOUT_MS = 15_000;\n\nfunction withTimeout(signal: AbortSignal | undefined): AbortSignal {\n const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS);\n return signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\n/** Error raised for a genuine backend failure (never for an empty day). */\nexport class DesktopDaemonError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DesktopDaemonError\";\n }\n}\n\nfunction baseUrlIsLoopback(baseUrl: string): boolean {\n try {\n return isLoopbackHost(new URL(baseUrl).hostname);\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the daemon bearer token. The local token file is read ONLY for\n * a loopback base URL — a remote reader must supply the token explicitly\n * (config/env), never inherit this machine's local token.\n */\nexport function resolveCaptureAudioToken(\n configured: string | undefined,\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n if (typeof configured === \"string\" && configured.trim().length > 0) return configured.trim();\n const fromEnv = env.REMNIC_CAPTURE_AUDIO_TOKEN;\n if (typeof fromEnv === \"string\" && fromEnv.trim().length > 0) return fromEnv.trim();\n if (baseUrlIsLoopback(baseUrl)) {\n try {\n const token = readFileSync(capturePaths().tokenPath, \"utf8\").trim();\n if (token.length > 0) return token;\n } catch {\n // no local token file — treat as unauthenticated (health will 401)\n }\n }\n return undefined;\n}\n\nfunction daemonSegmentToWearable(seg: DaemonSegment): WearableTranscriptSegment {\n return {\n text: seg.textRaw,\n // The wearables speaker registry owns naming; the connector only\n // supplies the stable per-source key (spk_<n> | self | \"unknown\").\n speakerKey: seg.speakerKey ?? \"unknown\",\n isWearer: seg.isWearer,\n startIso: seg.startUtc,\n endIso: seg.endUtc,\n };\n}\n\nexport function daemonConversationToWearable(conv: DaemonConversation): WearableConversation {\n return {\n id: conv.id,\n source: DESKTOP_SOURCE_ID,\n startIso: conv.startedAtUtc,\n endIso: conv.endedAtUtc ?? undefined,\n segments: conv.segments.map(daemonSegmentToWearable),\n };\n}\n\ninterface DesktopClientOptions {\n baseUrl: string;\n /** Resolved per request so env / token-file rotation applies without a rebuild. */\n getToken: () => string | undefined;\n}\n\n/** Linear trailing-slash trim (avoids a backtracking regex on the base URL). */\nfunction trimTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47 /* \"/\" */) end--;\n return value.slice(0, end);\n}\n\nclass DesktopClient {\n #baseUrl: string;\n #getToken: () => string | undefined;\n\n constructor(options: DesktopClientOptions) {\n this.#baseUrl = trimTrailingSlashes(options.baseUrl);\n this.#getToken = options.getToken;\n }\n\n #headers(): Record<string, string> {\n const token = this.#getToken();\n return token ? { authorization: `Bearer ${token}` } : {};\n }\n\n async verifyAuth(signal?: AbortSignal): Promise<WearableAuthCheck> {\n let res: Response;\n try {\n res = await fetch(`${this.#baseUrl}/v1/health`, { headers: this.#headers(), signal: withTimeout(signal) });\n } catch (err) {\n // Honor caller cancellation; do not mislabel a deliberate abort.\n if (signal?.aborted) throw err;\n // Timeout / connection refused / DNS / offline: the daemon isn't\n // reachable. This is NOT an auth failure and must never throw (AC2).\n return { ok: false, detail: \"unreachable\" };\n }\n if (res.status === 401) return { ok: false, detail: \"unauthorized\" };\n if (!res.ok) return { ok: false, detail: `HTTP ${res.status}` };\n let body: { ok?: unknown; version?: unknown };\n try {\n body = (await res.json()) as { ok?: unknown; version?: unknown };\n } catch (err) {\n // Honor caller cancellation during the body read; a non-JSON body\n // otherwise just reads as unhealthy (never throws for that, AC2).\n if (signal?.aborted) throw err;\n body = {};\n }\n if (body.ok === true) {\n return { ok: true, detail: typeof body.version === \"string\" ? `capture-audio ${body.version}` : undefined };\n }\n return { ok: false, detail: \"unhealthy\" };\n }\n\n async fetchConversations(opts: WearableFetchOptions): Promise<WearableFetchPage> {\n let res: Response;\n try {\n const url = new URL(`${this.#baseUrl}/v1/conversations`);\n url.searchParams.set(\"date\", opts.date);\n url.searchParams.set(\"timezone\", opts.timezone);\n if (opts.cursor) url.searchParams.set(\"cursor\", opts.cursor);\n res = await fetch(url, { headers: this.#headers(), signal: withTimeout(opts.signal) });\n } catch (err) {\n // Honor caller cancellation instead of reporting a daemon fault.\n if (opts.signal?.aborted) throw err;\n // Timeout, offline daemon, or a malformed configured baseUrl all\n // surface consistently as unreachable (never a raw TypeError).\n throw new DesktopDaemonError(\n `desktop capture daemon unreachable at ${this.#baseUrl} (is remnic-capture-audio running?)`,\n );\n }\n if (!res.ok) {\n // A non-2xx is a backend/auth failure, distinct from an empty day.\n throw new DesktopDaemonError(`desktop capture daemon returned HTTP ${res.status}`);\n }\n let page: ConversationPage;\n try {\n page = (await res.json()) as ConversationPage;\n } catch (err) {\n // Honor caller cancellation that lands mid-body-read.\n if (opts.signal?.aborted) throw err;\n // A 200 with an empty/non-JSON body is a backend fault, not an empty day.\n throw new DesktopDaemonError(\"desktop capture daemon returned a non-JSON conversations response\");\n }\n if (!page || !Array.isArray(page.conversations)) {\n throw new DesktopDaemonError(\"desktop capture daemon returned a malformed conversations page\");\n }\n return {\n conversations: page.conversations.map(daemonConversationToWearable),\n nextCursor: page.nextCursor ?? null,\n };\n }\n}\n\nexport function createDesktopConnector(options: WearableConnectorFactoryOptions): WearableSourceConnector {\n const baseUrl = options.settings.baseUrl?.trim() || DEFAULT_BASE_URL;\n // One client per connector, but the token is resolved per request (below)\n // so env / on-disk token rotation takes effect without a rebuild.\n let client: DesktopClient | null = null;\n const getClient = (): DesktopClient => {\n if (!client) {\n client = new DesktopClient({ baseUrl, getToken: () => resolveCaptureAudioToken(options.settings.apiKey, baseUrl) });\n }\n return client;\n };\n return {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n verifyAuth: (signal?: AbortSignal) => getClient().verifyAuth(signal),\n fetchConversations: (opts: WearableFetchOptions) => getClient().fetchConversations(opts),\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n factory: createDesktopConnector,\n};\n\n/** Idempotently register the desktop connector with the core registry. */\nexport function ensureDesktopConnectorRegistered(): boolean {\n if (getWearableConnector(DESKTOP_SOURCE_ID) !== undefined) return false;\n registerWearableConnector(wearableConnectorRegistration);\n return true;\n}\n\nensureDesktopConnectorRegistered();\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/vad.ts","../src/connector.ts"],"sourcesContent":["import { statSync } from \"node:fs\";\n\nimport { CaptureConfigError } from \"./errors.js\";\nimport { expandTilde } from \"./paths.js\";\n\n\nexport interface SileroVadInput {\n modelPath: string;\n minSpeechMs: number;\n minSilenceMs?: number;\n maxSpeechMs?: number;\n threshold?: number;\n threads?: number;\n}\n\nexport interface SherpaOnnxModule {\n Vad: new (config: unknown, bufferSeconds: number) => unknown;\n}\n\nexport function sileroVadConfig(input: SileroVadInput): { config: object; bufferSeconds: number } {\n if (typeof input.modelPath !== \"string\" || input.modelPath.trim() === \"\") {\n throw new CaptureConfigError(\"Silero VAD modelPath must be a non-empty string\");\n }\n if (!Number.isFinite(input.minSpeechMs) || input.minSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD minSpeechMs must be positive\");\n }\n const minSilenceMs = input.minSilenceMs ?? 500;\n const maxSpeechMs = input.maxSpeechMs ?? 30_000;\n const threshold = input.threshold ?? 0.5;\n const threads = input.threads ?? 1;\n if (!Number.isFinite(minSilenceMs) || minSilenceMs < 0 || !Number.isFinite(maxSpeechMs) || maxSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD duration settings are invalid\");\n }\n if (maxSpeechMs < input.minSpeechMs) {\n throw new CaptureConfigError(\"Silero VAD maxSpeechMs must be greater than or equal to minSpeechMs\");\n }\n if (!Number.isFinite(threshold) || threshold <= 0 || threshold >= 1) {\n throw new CaptureConfigError(\"Silero VAD threshold must be between 0 and 1\");\n }\n if (!Number.isInteger(threads) || threads <= 0) {\n throw new CaptureConfigError(\"Silero VAD threads must be a positive integer\");\n }\n return {\n config: {\n sileroVad: {\n model: expandTilde(input.modelPath),\n threshold,\n minSpeechDuration: input.minSpeechMs / 1_000,\n minSilenceDuration: minSilenceMs / 1_000,\n maxSpeechDuration: maxSpeechMs / 1_000,\n windowSize: 512,\n },\n sampleRate: 16_000,\n debug: false,\n numThreads: threads,\n },\n bufferSeconds: 60,\n };\n}\n\nexport function resolveSherpaExport(module: unknown): SherpaOnnxModule | null {\n const candidates = [module, (module as { default?: unknown } | null)?.default];\n for (const candidate of candidates) {\n if (candidate && typeof candidate === \"object\" && typeof (candidate as SherpaOnnxModule).Vad === \"function\") {\n return candidate as SherpaOnnxModule;\n }\n }\n return null;\n}\n\nexport async function loadSherpaOnnx(\n importModule: (specifier: string) => Promise<unknown> = (specifier) => import(specifier),\n): Promise<SherpaOnnxModule> {\n // Computed specifier keeps the optional sherpa-onnx-node peer out of the static\n // dependency graph so the package builds and runs without it installed.\n const specifier = \"sherpa-onnx-\" + \"node\";\n let module: unknown;\n try {\n module = await importModule(specifier);\n } catch (error) {\n // A genuinely absent package surfaces as ERR_MODULE_NOT_FOUND naming the\n // specifier as a package, with no require stack. An installed-but-broken\n // package (missing native .node, dlopen failure) has a different code\n // and/or a require stack, so it must NOT be reported as \"not installed\".\n const err = error as NodeJS.ErrnoException & { requireStack?: unknown };\n const message = error instanceof Error ? error.message : \"\";\n const packageMissing =\n err.code === \"ERR_MODULE_NOT_FOUND\" &&\n err.requireStack === undefined &&\n message.includes(`package '${specifier}'`);\n if (packageMissing) {\n throw new CaptureConfigError(\n \"Silero VAD requires optional dependency sherpa-onnx-node; install it before enabling VAD\",\n );\n }\n // Installed but unusable. Report that distinctly, but keep the message\n // operator-safe: never echo the raw error (it can leak absolute native\n // library paths), per the CaptureConfigError contract.\n throw new CaptureConfigError(\n \"Silero VAD could not load the installed sherpa-onnx-node native runtime; verify its native build and shared-library dependencies\",\n );\n }\n const resolved = resolveSherpaExport(module);\n if (!resolved) {\n throw new CaptureConfigError(\"sherpa-onnx-node does not expose the Vad API required by capture-audio\");\n }\n return resolved;\n}\n\nfunction isRegularFile(filePath: string): boolean {\n try {\n return statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\nexport async function createSileroVad(\n input: SileroVadInput,\n load: () => Promise<SherpaOnnxModule> = loadSherpaOnnx,\n exists: (path: string) => boolean = isRegularFile,\n): Promise<unknown> {\n const { config, bufferSeconds } = sileroVadConfig(input);\n const modelPath = expandTilde(input.modelPath);\n if (!exists(modelPath)) {\n throw new CaptureConfigError(`Silero VAD model not found at ${modelPath}; set vad.modelPath to a readable model file`);\n }\n const { Vad } = await load();\n return new Vad(config, bufferSeconds);\n}\n","/**\n * `desktop` wearable source connector (issue #1897, component 4).\n *\n * À-la-carte optional companion of @remnic/core: installing core alone\n * never pulls this in; core discovers it at runtime via a\n * computed-specifier dynamic import (registry entry {id:\"desktop\",\n * suffix:\"capture-audio\"}) or via a direct import of @remnic/capture-audio,\n * which self-registers idempotently.\n *\n * The connector is a pure API client + normalizer over the capture-audio\n * daemon's loopback HTTP API: no file IO beyond reading the local token,\n * no memory writes, no pipeline behavior (all of that stays in core so\n * desktop audio gets the same cleanup/corrections/trust gating as every\n * other wearable source).\n *\n * Token resolution (in order): settings.apiKey (config) ->\n * REMNIC_CAPTURE_AUDIO_TOKEN env -> the daemon's local token file\n * (~/.remnic/capture/token) when the base URL is loopback.\n */\n\nimport { readFileSync } from \"node:fs\";\n\nimport {\n registerWearableConnector,\n getWearableConnector,\n type WearableAuthCheck,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableConversation,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableSourceConnector,\n type WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport { DEFAULT_HOST, DEFAULT_PORT } from \"./constants.js\";\nimport { capturePaths } from \"./paths.js\";\nimport { isLoopbackHost } from \"./util.js\";\nimport type { ConversationPage, DaemonConversation, DaemonSegment } from \"./spool.js\";\n\nexport const DESKTOP_SOURCE_ID = \"desktop\";\nexport const DESKTOP_DISPLAY_NAME = \"Desktop audio\";\nconst DEFAULT_BASE_URL = `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;\n\n/** Bounded probe timeout so a wedged daemon reports unreachable instead of hanging. */\nconst PROBE_TIMEOUT_MS = 15_000;\n\nfunction withTimeout(signal: AbortSignal | undefined): AbortSignal {\n const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS);\n return signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\n/** Error raised for a genuine backend failure (never for an empty day). */\nexport class DesktopDaemonError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DesktopDaemonError\";\n }\n}\n\nfunction baseUrlIsLoopback(baseUrl: string): boolean {\n try {\n return isLoopbackHost(new URL(baseUrl).hostname);\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the daemon bearer token. The local token file is read ONLY for\n * a loopback base URL — a remote reader must supply the token explicitly\n * (config/env), never inherit this machine's local token.\n */\nexport function resolveCaptureAudioToken(\n configured: string | undefined,\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n if (typeof configured === \"string\" && configured.trim().length > 0) return configured.trim();\n const fromEnv = env.REMNIC_CAPTURE_AUDIO_TOKEN;\n if (typeof fromEnv === \"string\" && fromEnv.trim().length > 0) return fromEnv.trim();\n if (baseUrlIsLoopback(baseUrl)) {\n try {\n const token = readFileSync(capturePaths().tokenPath, \"utf8\").trim();\n if (token.length > 0) return token;\n } catch {\n // no local token file — treat as unauthenticated (health will 401)\n }\n }\n return undefined;\n}\n\nfunction daemonSegmentToWearable(seg: DaemonSegment): WearableTranscriptSegment {\n return {\n text: seg.textRaw,\n // The wearables speaker registry owns naming; the connector only\n // supplies the stable per-source key (spk_<n> | self | \"unknown\").\n speakerKey: seg.speakerKey ?? \"unknown\",\n isWearer: seg.isWearer,\n startIso: seg.startUtc,\n endIso: seg.endUtc,\n };\n}\n\nexport function daemonConversationToWearable(conv: DaemonConversation): WearableConversation {\n return {\n id: conv.id,\n source: DESKTOP_SOURCE_ID,\n startIso: conv.startedAtUtc,\n endIso: conv.endedAtUtc ?? undefined,\n segments: conv.segments.map(daemonSegmentToWearable),\n };\n}\n\ninterface DesktopClientOptions {\n baseUrl: string;\n /** Resolved per request so env / token-file rotation applies without a rebuild. */\n getToken: () => string | undefined;\n}\n\n/** Linear trailing-slash trim (avoids a backtracking regex on the base URL). */\nfunction trimTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47 /* \"/\" */) end--;\n return value.slice(0, end);\n}\n\nclass DesktopClient {\n #baseUrl: string;\n #getToken: () => string | undefined;\n\n constructor(options: DesktopClientOptions) {\n this.#baseUrl = trimTrailingSlashes(options.baseUrl);\n this.#getToken = options.getToken;\n }\n\n #headers(): Record<string, string> {\n const token = this.#getToken();\n return token ? { authorization: `Bearer ${token}` } : {};\n }\n\n async verifyAuth(signal?: AbortSignal): Promise<WearableAuthCheck> {\n let res: Response;\n try {\n res = await fetch(`${this.#baseUrl}/v1/health`, { headers: this.#headers(), signal: withTimeout(signal) });\n } catch (err) {\n // Honor caller cancellation; do not mislabel a deliberate abort.\n if (signal?.aborted) throw err;\n // Timeout / connection refused / DNS / offline: the daemon isn't\n // reachable. This is NOT an auth failure and must never throw (AC2).\n return { ok: false, detail: \"unreachable\" };\n }\n if (res.status === 401) return { ok: false, detail: \"unauthorized\" };\n if (!res.ok) return { ok: false, detail: `HTTP ${res.status}` };\n let body: { ok?: unknown; version?: unknown };\n try {\n body = (await res.json()) as { ok?: unknown; version?: unknown };\n } catch (err) {\n // Honor caller cancellation during the body read; a non-JSON body\n // otherwise just reads as unhealthy (never throws for that, AC2).\n if (signal?.aborted) throw err;\n body = {};\n }\n if (body.ok === true) {\n return { ok: true, detail: typeof body.version === \"string\" ? `capture-audio ${body.version}` : undefined };\n }\n return { ok: false, detail: \"unhealthy\" };\n }\n\n async fetchConversations(opts: WearableFetchOptions): Promise<WearableFetchPage> {\n let res: Response;\n try {\n const url = new URL(`${this.#baseUrl}/v1/conversations`);\n url.searchParams.set(\"date\", opts.date);\n url.searchParams.set(\"timezone\", opts.timezone);\n if (opts.cursor) url.searchParams.set(\"cursor\", opts.cursor);\n res = await fetch(url, { headers: this.#headers(), signal: withTimeout(opts.signal) });\n } catch (err) {\n // Honor caller cancellation instead of reporting a daemon fault.\n if (opts.signal?.aborted) throw err;\n // Timeout, offline daemon, or a malformed configured baseUrl all\n // surface consistently as unreachable (never a raw TypeError).\n throw new DesktopDaemonError(\n `desktop capture daemon unreachable at ${this.#baseUrl} (is remnic-capture-audio running?)`,\n );\n }\n if (!res.ok) {\n // A non-2xx is a backend/auth failure, distinct from an empty day.\n throw new DesktopDaemonError(`desktop capture daemon returned HTTP ${res.status}`);\n }\n let page: ConversationPage;\n try {\n page = (await res.json()) as ConversationPage;\n } catch (err) {\n // Honor caller cancellation that lands mid-body-read.\n if (opts.signal?.aborted) throw err;\n // A 200 with an empty/non-JSON body is a backend fault, not an empty day.\n throw new DesktopDaemonError(\"desktop capture daemon returned a non-JSON conversations response\");\n }\n if (!page || !Array.isArray(page.conversations)) {\n throw new DesktopDaemonError(\"desktop capture daemon returned a malformed conversations page\");\n }\n return {\n conversations: page.conversations.map(daemonConversationToWearable),\n nextCursor: page.nextCursor ?? null,\n };\n }\n}\n\nexport function createDesktopConnector(options: WearableConnectorFactoryOptions): WearableSourceConnector {\n const baseUrl = options.settings.baseUrl?.trim() || DEFAULT_BASE_URL;\n // One client per connector, but the token is resolved per request (below)\n // so env / on-disk token rotation takes effect without a rebuild.\n let client: DesktopClient | null = null;\n const getClient = (): DesktopClient => {\n if (!client) {\n client = new DesktopClient({ baseUrl, getToken: () => resolveCaptureAudioToken(options.settings.apiKey, baseUrl) });\n }\n return client;\n };\n return {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n verifyAuth: (signal?: AbortSignal) => getClient().verifyAuth(signal),\n fetchConversations: (opts: WearableFetchOptions) => getClient().fetchConversations(opts),\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n factory: createDesktopConnector,\n};\n\n/** Idempotently register the desktop connector with the core registry. */\nexport function ensureDesktopConnectorRegistered(): boolean {\n if (getWearableConnector(DESKTOP_SOURCE_ID) !== undefined) return false;\n registerWearableConnector(wearableConnectorRegistration);\n return true;\n}\n\nensureDesktopConnectorRegistered();\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/capture-audio",
3
- "version": "9.63.0",
3
+ "version": "9.63.2",
4
4
  "description": "Desktop audio capture daemon for Remnic — local spool, loopback HTTP API, and replay ingestion for the wearable source `desktop` (à-la-carte)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,13 +29,13 @@
29
29
  "tsup": "^8.0.0",
30
30
  "typescript": "^5.7.0",
31
31
  "tsx": "^4.0.0",
32
- "@remnic/core": "9.63.0"
32
+ "@remnic/core": "9.63.2"
33
33
  },
34
34
  "peerDependencies": {
35
- "@remnic/core": "^9.63.0",
35
+ "@remnic/core": "^9.63.2",
36
36
  "sherpa-onnx-node": "*",
37
- "@remnic/capture-native-darwin-arm64": "^9.63.0",
38
- "@remnic/capture-native-darwin-x64": "^9.63.0"
37
+ "@remnic/capture-native-darwin-arm64": "^9.63.2",
38
+ "@remnic/capture-native-darwin-x64": "^9.63.2"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "sherpa-onnx-node": {
@@ -67,6 +67,6 @@
67
67
  "build": "tsup src/index.ts src/cli-bin.ts --format esm --dts",
68
68
  "precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
69
69
  "check-types": "tsc --noEmit",
70
- "test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/config.test.ts src/validate.test.ts src/token.test.ts src/spool.test.ts src/replay.test.ts src/daemon.test.ts src/cli.test.ts src/stt.test.ts src/model.test.ts src/janitor.test.ts src/vad.test.ts src/dedup.test.ts src/assembly.test.ts src/diarization.test.ts src/connector.test.ts src/e2e.test.ts src/native.test.ts src/processor.test.ts src/capture.test.ts src/service.test.ts src/enroll.test.ts"
70
+ "test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/config.test.ts src/validate.test.ts src/token.test.ts src/spool.test.ts src/replay.test.ts src/daemon.test.ts src/cli.test.ts src/stt.test.ts src/model.test.ts src/janitor.test.ts src/vad.test.ts src/dedup.test.ts src/assembly.test.ts src/diarization.test.ts src/connector.test.ts src/e2e.test.ts src/native.test.ts src/processor.test.ts src/processor-policy.test.ts src/capture.test.ts src/service.test.ts src/enroll.test.ts"
71
71
  }
72
72
  }