audio-mcp 0.1.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/dist/audio/capture.d.ts +28 -0
  4. package/dist/audio/capture.js +2 -0
  5. package/dist/audio/capture.js.map +1 -0
  6. package/dist/audio/devices.d.ts +16 -0
  7. package/dist/audio/devices.js +65 -0
  8. package/dist/audio/devices.js.map +1 -0
  9. package/dist/audio/helper-capture.d.ts +36 -0
  10. package/dist/audio/helper-capture.js +135 -0
  11. package/dist/audio/helper-capture.js.map +1 -0
  12. package/dist/audio/helper-path.d.ts +10 -0
  13. package/dist/audio/helper-path.js +47 -0
  14. package/dist/audio/helper-path.js.map +1 -0
  15. package/dist/audio/wav.d.ts +52 -0
  16. package/dist/audio/wav.js +174 -0
  17. package/dist/audio/wav.js.map +1 -0
  18. package/dist/bin/audio-capture-helper +0 -0
  19. package/dist/config.d.ts +14 -0
  20. package/dist/config.js +27 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/errors.d.ts +18 -0
  23. package/dist/errors.js +21 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +10 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/logger.d.ts +14 -0
  29. package/dist/logger.js +34 -0
  30. package/dist/logger.js.map +1 -0
  31. package/dist/paths.d.ts +15 -0
  32. package/dist/paths.js +36 -0
  33. package/dist/paths.js.map +1 -0
  34. package/dist/server.d.ts +1 -0
  35. package/dist/server.js +112 -0
  36. package/dist/server.js.map +1 -0
  37. package/dist/session/manager.d.ts +58 -0
  38. package/dist/session/manager.js +184 -0
  39. package/dist/session/manager.js.map +1 -0
  40. package/dist/session/store.d.ts +35 -0
  41. package/dist/session/store.js +72 -0
  42. package/dist/session/store.js.map +1 -0
  43. package/dist/tools/index.d.ts +16 -0
  44. package/dist/tools/index.js +180 -0
  45. package/dist/tools/index.js.map +1 -0
  46. package/package.json +58 -0
@@ -0,0 +1,135 @@
1
+ import { spawn } from "node:child_process";
2
+ import { HELPER_BIN, verifyHelper } from "./helper-path.js";
3
+ import { AudioMcpError } from "../errors.js";
4
+ const GATEKEEPER_EXIT = 9; // process killed by Gatekeeper (SIGKILL)
5
+ export class HelperProcessCapture {
6
+ child = null;
7
+ dataCb = null;
8
+ stderrBuf = "";
9
+ exitPromise = null;
10
+ command;
11
+ constructor(opts = {}) {
12
+ this.command = opts.command ?? [HELPER_BIN];
13
+ }
14
+ onData(cb) {
15
+ this.dataCb = cb;
16
+ }
17
+ async start(opts) {
18
+ // Only verify the packaged binary when using the default helper path.
19
+ if (this.command.length === 1 && this.command[0] === HELPER_BIN) {
20
+ const badBinary = verifyHelper();
21
+ if (badBinary)
22
+ throw this.binaryErrorToAudioMcpError(badBinary);
23
+ }
24
+ const args = [
25
+ ...this.command.slice(1),
26
+ "capture",
27
+ `--mode=${opts.captureMode}`,
28
+ `--sample-rate=${opts.sampleRate}`,
29
+ ];
30
+ if (opts.micDeviceId)
31
+ args.push(`--mic-device-id=${opts.micDeviceId}`);
32
+ if (opts.excludePid)
33
+ args.push(`--exclude-pid=${opts.excludePid}`);
34
+ const executable = this.command[0];
35
+ const child = spawn(executable, args, { stdio: ["ignore", "pipe", "pipe"] });
36
+ this.child = child;
37
+ this.stderrBuf = "";
38
+ child.stdout.on("data", (chunk) => {
39
+ if (this.dataCb)
40
+ this.dataCb(chunk);
41
+ });
42
+ child.stderr.setEncoding("utf8");
43
+ child.stderr.on("data", (chunk) => {
44
+ this.stderrBuf += chunk;
45
+ });
46
+ this.exitPromise = new Promise((resolve) => {
47
+ child.once("exit", (code, signal) => {
48
+ // Translate signal into exit code convention (128 + signo).
49
+ if (code !== null)
50
+ resolve(code);
51
+ else if (signal === "SIGKILL")
52
+ resolve(GATEKEEPER_EXIT);
53
+ else
54
+ resolve(-1);
55
+ });
56
+ });
57
+ // Race start() against an early-exit to catch permission failures.
58
+ const earlyFail = await this.waitForEarlyFail(300);
59
+ if (earlyFail) {
60
+ throw this.translateExit(earlyFail);
61
+ }
62
+ }
63
+ waitForEarlyFail(ms) {
64
+ return new Promise((resolve) => {
65
+ let settled = false;
66
+ const timer = setTimeout(() => {
67
+ if (!settled) {
68
+ settled = true;
69
+ resolve(null);
70
+ }
71
+ }, ms);
72
+ this.exitPromise.then((code) => {
73
+ if (!settled) {
74
+ settled = true;
75
+ clearTimeout(timer);
76
+ resolve(code);
77
+ }
78
+ });
79
+ });
80
+ }
81
+ async stop() {
82
+ const child = this.child;
83
+ if (!child)
84
+ return;
85
+ if (!child.killed && child.exitCode === null) {
86
+ child.kill("SIGTERM");
87
+ }
88
+ if (this.exitPromise) {
89
+ await this.exitPromise;
90
+ }
91
+ this.child = null;
92
+ }
93
+ binaryErrorToAudioMcpError(err) {
94
+ if (err.reason === "missing") {
95
+ return new AudioMcpError("AUDIO_DEVICE_ERROR", err.message);
96
+ }
97
+ if (err.reason === "not_executable") {
98
+ return new AudioMcpError("AUDIO_DEVICE_ERROR", `${err.message}\n\nIf macOS Gatekeeper blocked the binary, run:\n xattr -d com.apple.quarantine "${HELPER_BIN}"\nOr approve it in System Settings → Privacy & Security.`);
99
+ }
100
+ return new AudioMcpError("AUDIO_DEVICE_ERROR", err.message);
101
+ }
102
+ translateExit(code) {
103
+ const stderrMsg = this.extractStderrMessage();
104
+ switch (code) {
105
+ case 2:
106
+ return new AudioMcpError("AUDIO_DEVICE_ERROR", `Microphone permission denied. ${stderrMsg}\n\n` +
107
+ "Grant mic access in System Settings → Privacy & Security → Microphone for your MCP client, then restart the client.");
108
+ case 3:
109
+ return new AudioMcpError("AUDIO_DEVICE_ERROR", `Screen Recording permission denied (required for system audio capture). ${stderrMsg}\n\n` +
110
+ "Grant access in System Settings → Privacy & Security → Screen Recording for your MCP client, then restart the client.");
111
+ case GATEKEEPER_EXIT:
112
+ return new AudioMcpError("AUDIO_DEVICE_ERROR", `macOS Gatekeeper blocked the audio helper binary. Run:\n` +
113
+ ` xattr -d com.apple.quarantine "${HELPER_BIN}"\n` +
114
+ `Or: System Settings → Privacy & Security → scroll to the blocked binary message → "Allow Anyway".`);
115
+ default:
116
+ return new AudioMcpError("AUDIO_DEVICE_ERROR", `audio-capture-helper exited with code ${code}. ${stderrMsg}`);
117
+ }
118
+ }
119
+ extractStderrMessage() {
120
+ const lines = this.stderrBuf.trim().split("\n").filter(Boolean);
121
+ for (let i = lines.length - 1; i >= 0; i--) {
122
+ try {
123
+ const parsed = JSON.parse(lines[i]);
124
+ if (parsed.level === "error" && parsed.msg) {
125
+ return parsed.msg;
126
+ }
127
+ }
128
+ catch {
129
+ /* not JSON */
130
+ }
131
+ }
132
+ return lines.length ? lines[lines.length - 1] : "";
133
+ }
134
+ }
135
+ //# sourceMappingURL=helper-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper-capture.js","sourceRoot":"","sources":["../../src/audio/helper-capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare const HELPER_BIN: string;
2
+ export declare class HelperBinaryError extends Error {
3
+ readonly reason: "missing" | "not_executable" | "gatekeeper" | "other";
4
+ constructor(message: string, reason: "missing" | "not_executable" | "gatekeeper" | "other");
5
+ }
6
+ /**
7
+ * Verify the helper binary is present and executable. Returns a structured
8
+ * error identifying which remediation the user should take.
9
+ */
10
+ export declare function verifyHelper(): HelperBinaryError | null;
@@ -0,0 +1,47 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname, join } from "node:path";
3
+ import { existsSync, accessSync, constants as fsConstants } from "node:fs";
4
+ /**
5
+ * Absolute path to the bundled audio-capture-helper binary. Resolves based
6
+ * on this module's compiled location: `dist/audio/helper-path.js` → `dist/bin/audio-capture-helper`.
7
+ * During development (when running TypeScript source directly), it falls
8
+ * back to `dist/bin/...` relative to the repo root.
9
+ */
10
+ function resolveHelperPath() {
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ // Compiled: dist/audio/helper-path.js → ../bin/audio-capture-helper
13
+ const compiled = join(here, "..", "bin", "audio-capture-helper");
14
+ if (existsSync(compiled))
15
+ return compiled;
16
+ // Dev fallback: <repo>/src/audio/helper-path.ts → <repo>/dist/bin/audio-capture-helper
17
+ const dev = join(here, "..", "..", "dist", "bin", "audio-capture-helper");
18
+ return dev;
19
+ }
20
+ export const HELPER_BIN = resolveHelperPath();
21
+ export class HelperBinaryError extends Error {
22
+ reason;
23
+ constructor(message, reason) {
24
+ super(message);
25
+ this.reason = reason;
26
+ this.name = "HelperBinaryError";
27
+ }
28
+ }
29
+ /**
30
+ * Verify the helper binary is present and executable. Returns a structured
31
+ * error identifying which remediation the user should take.
32
+ */
33
+ export function verifyHelper() {
34
+ if (!existsSync(HELPER_BIN)) {
35
+ return new HelperBinaryError(`audio-capture-helper not found at ${HELPER_BIN}. ` +
36
+ `Reinstall audio-mcp or run 'npm run build:helper' from source.`, "missing");
37
+ }
38
+ try {
39
+ accessSync(HELPER_BIN, fsConstants.X_OK);
40
+ }
41
+ catch {
42
+ return new HelperBinaryError(`audio-capture-helper is not executable at ${HELPER_BIN}. ` +
43
+ `Run 'chmod +x ${HELPER_BIN}' and try again.`, "not_executable");
44
+ }
45
+ return null;
46
+ }
47
+ //# sourceMappingURL=helper-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper-path.js","sourceRoot":"","sources":["../../src/audio/helper-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3E;;;;;GAKG;AACH,SAAS,iBAAiB;IACxB,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,oEAAoE;IACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,sBAAsB,CAAC,CAAC;IACjE,IAAI,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1C,uFAAuF;IACvF,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,sBAAsB,CAAC,CAAC;IAC1E,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,iBAAiB,EAAE,CAAC;AAE9C,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAGxB;IAFlB,YACE,OAAe,EACC,MAA6D;QAE7E,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,WAAM,GAAN,MAAM,CAAuD;QAG7E,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,iBAAiB,CAC1B,qCAAqC,UAAU,IAAI;YACjD,gEAAgE,EAClE,SAAS,CACV,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,UAAU,CAAC,UAAU,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,iBAAiB,CAC1B,6CAA6C,UAAU,IAAI;YACzD,iBAAiB,UAAU,kBAAkB,EAC/C,gBAAgB,CACjB,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,52 @@
1
+ export interface WavFormat {
2
+ sampleRate: number;
3
+ channels: number;
4
+ }
5
+ /** Build a 44-byte PCM WAV header. `dataSize` is the byte length of PCM data. */
6
+ export declare function buildWavHeader({ sampleRate, channels }: WavFormat, dataSize: number): Buffer;
7
+ /**
8
+ * Streaming WAV writer. Writes a zero-sized header up front, appends PCM
9
+ * chunks as they arrive, then patches the header sizes on `finalize()`.
10
+ * Audio data is never buffered entirely in memory.
11
+ */
12
+ export declare class WavWriter {
13
+ private readonly path;
14
+ private readonly format;
15
+ private stream;
16
+ private bytesWritten;
17
+ private closed;
18
+ constructor(path: string, format: WavFormat);
19
+ /** Append a PCM chunk. Returns once the chunk is flushed to the OS. */
20
+ append(chunk: Buffer): Promise<void>;
21
+ /** Close the stream and rewrite the RIFF/data sizes in the header. */
22
+ finalize(): Promise<{
23
+ bytesWritten: number;
24
+ durationSeconds: number;
25
+ }>;
26
+ get byteCount(): number;
27
+ }
28
+ export interface WavSlice {
29
+ audio: Buffer;
30
+ startSecond: number;
31
+ endSecond: number;
32
+ durationSeconds: number;
33
+ sampleRate: number;
34
+ channels: number;
35
+ byteLength: number;
36
+ }
37
+ /**
38
+ * Read the PCM-format fields from a WAV file header. Throws if the file is
39
+ * not a PCM WAV (format 1) with a well-formed RIFF/data structure.
40
+ */
41
+ export declare function readWavFormat(path: string): {
42
+ sampleRate: number;
43
+ channels: number;
44
+ bitsPerSample: number;
45
+ dataSize: number;
46
+ };
47
+ /**
48
+ * Read `[startSecond, endSecond)` from a PCM WAV and return a new
49
+ * self-contained WAV buffer wrapping just the requested slice. Clamps
50
+ * `endSecond` to the file's total duration.
51
+ */
52
+ export declare function sliceWav(path: string, startSecond: number, endSecond: number): WavSlice;
@@ -0,0 +1,174 @@
1
+ import { createWriteStream, promises as fsp, openSync, readSync, closeSync, statSync, } from "node:fs";
2
+ const HEADER_SIZE = 44;
3
+ const BITS_PER_SAMPLE = 16;
4
+ const BYTES_PER_SAMPLE = BITS_PER_SAMPLE / 8;
5
+ const PCM_FORMAT = 1;
6
+ /** Build a 44-byte PCM WAV header. `dataSize` is the byte length of PCM data. */
7
+ export function buildWavHeader({ sampleRate, channels }, dataSize) {
8
+ const buf = Buffer.alloc(HEADER_SIZE);
9
+ const byteRate = sampleRate * channels * BYTES_PER_SAMPLE;
10
+ const blockAlign = channels * BYTES_PER_SAMPLE;
11
+ buf.write("RIFF", 0, "ascii");
12
+ buf.writeUInt32LE(36 + dataSize, 4); // RIFF chunk size
13
+ buf.write("WAVE", 8, "ascii");
14
+ buf.write("fmt ", 12, "ascii");
15
+ buf.writeUInt32LE(16, 16); // fmt chunk size
16
+ buf.writeUInt16LE(PCM_FORMAT, 20);
17
+ buf.writeUInt16LE(channels, 22);
18
+ buf.writeUInt32LE(sampleRate, 24);
19
+ buf.writeUInt32LE(byteRate, 28);
20
+ buf.writeUInt16LE(blockAlign, 32);
21
+ buf.writeUInt16LE(BITS_PER_SAMPLE, 34);
22
+ buf.write("data", 36, "ascii");
23
+ buf.writeUInt32LE(dataSize, 40);
24
+ return buf;
25
+ }
26
+ /**
27
+ * Streaming WAV writer. Writes a zero-sized header up front, appends PCM
28
+ * chunks as they arrive, then patches the header sizes on `finalize()`.
29
+ * Audio data is never buffered entirely in memory.
30
+ */
31
+ export class WavWriter {
32
+ path;
33
+ format;
34
+ stream;
35
+ bytesWritten = 0;
36
+ closed = false;
37
+ constructor(path, format) {
38
+ this.path = path;
39
+ this.format = format;
40
+ this.stream = createWriteStream(path);
41
+ this.stream.write(buildWavHeader(format, 0));
42
+ }
43
+ /** Append a PCM chunk. Returns once the chunk is flushed to the OS. */
44
+ append(chunk) {
45
+ if (this.closed)
46
+ return Promise.reject(new Error("WavWriter already finalized"));
47
+ return new Promise((resolve, reject) => {
48
+ this.stream.write(chunk, (err) => {
49
+ if (err)
50
+ reject(err);
51
+ else {
52
+ this.bytesWritten += chunk.length;
53
+ resolve();
54
+ }
55
+ });
56
+ });
57
+ }
58
+ /** Close the stream and rewrite the RIFF/data sizes in the header. */
59
+ async finalize() {
60
+ if (this.closed) {
61
+ throw new Error("WavWriter already finalized");
62
+ }
63
+ this.closed = true;
64
+ await new Promise((resolve, reject) => {
65
+ this.stream.end((err) => (err ? reject(err) : resolve()));
66
+ });
67
+ const fh = await fsp.open(this.path, "r+");
68
+ try {
69
+ const riffSize = Buffer.alloc(4);
70
+ riffSize.writeUInt32LE(36 + this.bytesWritten, 0);
71
+ await fh.write(riffSize, 0, 4, 4);
72
+ const dataSize = Buffer.alloc(4);
73
+ dataSize.writeUInt32LE(this.bytesWritten, 0);
74
+ await fh.write(dataSize, 0, 4, 40);
75
+ }
76
+ finally {
77
+ await fh.close();
78
+ }
79
+ const durationSeconds = this.bytesWritten / (this.format.sampleRate * this.format.channels * BYTES_PER_SAMPLE);
80
+ return { bytesWritten: this.bytesWritten, durationSeconds };
81
+ }
82
+ get byteCount() {
83
+ return this.bytesWritten;
84
+ }
85
+ }
86
+ /**
87
+ * Read the PCM-format fields from a WAV file header. Throws if the file is
88
+ * not a PCM WAV (format 1) with a well-formed RIFF/data structure.
89
+ */
90
+ export function readWavFormat(path) {
91
+ const fd = openSync(path, "r");
92
+ try {
93
+ const buf = Buffer.alloc(HEADER_SIZE);
94
+ readSync(fd, buf, 0, HEADER_SIZE, 0);
95
+ if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
96
+ throw new Error("Not a RIFF/WAVE file");
97
+ }
98
+ const audioFormat = buf.readUInt16LE(20);
99
+ if (audioFormat !== PCM_FORMAT) {
100
+ throw new Error(`Unsupported WAV format: ${audioFormat} (only PCM is supported)`);
101
+ }
102
+ return {
103
+ channels: buf.readUInt16LE(22),
104
+ sampleRate: buf.readUInt32LE(24),
105
+ bitsPerSample: buf.readUInt16LE(34),
106
+ dataSize: buf.readUInt32LE(40),
107
+ };
108
+ }
109
+ finally {
110
+ closeSync(fd);
111
+ }
112
+ }
113
+ /**
114
+ * Read `[startSecond, endSecond)` from a PCM WAV and return a new
115
+ * self-contained WAV buffer wrapping just the requested slice. Clamps
116
+ * `endSecond` to the file's total duration.
117
+ */
118
+ export function sliceWav(path, startSecond, endSecond) {
119
+ if (startSecond < 0) {
120
+ throw new Error("startSecond must be >= 0");
121
+ }
122
+ if (endSecond <= startSecond) {
123
+ throw new Error("endSecond must be > startSecond");
124
+ }
125
+ const fmt = readWavFormat(path);
126
+ const bytesPerSec = fmt.sampleRate * fmt.channels * (fmt.bitsPerSample / 8);
127
+ const fileSize = statSync(path).size;
128
+ // During an active recording the header's dataSize is still 0 — the
129
+ // WavWriter patches it only on finalize(). Fall back to the live file
130
+ // size so callers can read partial audio mid-session.
131
+ const availableDataBytes = fmt.dataSize > 0
132
+ ? Math.min(fmt.dataSize, fileSize - HEADER_SIZE)
133
+ : Math.max(0, fileSize - HEADER_SIZE);
134
+ const totalSeconds = availableDataBytes / bytesPerSec;
135
+ const clampedEnd = Math.min(endSecond, totalSeconds);
136
+ if (clampedEnd <= startSecond) {
137
+ // Range is entirely past end of file — return empty slice.
138
+ return {
139
+ audio: buildWavHeader({ sampleRate: fmt.sampleRate, channels: fmt.channels }, 0),
140
+ startSecond,
141
+ endSecond: startSecond,
142
+ durationSeconds: 0,
143
+ sampleRate: fmt.sampleRate,
144
+ channels: fmt.channels,
145
+ byteLength: HEADER_SIZE,
146
+ };
147
+ }
148
+ // Align offsets to sample frame boundary.
149
+ const frameSize = fmt.channels * (fmt.bitsPerSample / 8);
150
+ const alignToFrame = (bytes) => Math.floor(bytes / frameSize) * frameSize;
151
+ const startByte = alignToFrame(Math.floor(startSecond * bytesPerSec));
152
+ const endByte = alignToFrame(Math.floor(clampedEnd * bytesPerSec));
153
+ const sliceBytes = endByte - startByte;
154
+ const fd = openSync(path, "r");
155
+ try {
156
+ const pcm = Buffer.alloc(sliceBytes);
157
+ readSync(fd, pcm, 0, sliceBytes, HEADER_SIZE + startByte);
158
+ const header = buildWavHeader({ sampleRate: fmt.sampleRate, channels: fmt.channels }, sliceBytes);
159
+ const audio = Buffer.concat([header, pcm]);
160
+ return {
161
+ audio,
162
+ startSecond,
163
+ endSecond: clampedEnd,
164
+ durationSeconds: sliceBytes / bytesPerSec,
165
+ sampleRate: fmt.sampleRate,
166
+ channels: fmt.channels,
167
+ byteLength: audio.length,
168
+ };
169
+ }
170
+ finally {
171
+ closeSync(fd);
172
+ }
173
+ }
174
+ //# sourceMappingURL=wav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wav.js","sourceRoot":"","sources":["../../src/audio/wav.ts"],"names":[],"mappings":"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"}
Binary file
@@ -0,0 +1,14 @@
1
+ import type { Paths } from "./paths.js";
2
+ import type { CaptureMode } from "./audio/capture.js";
3
+ export interface AudioMcpConfig {
4
+ default_source: string | null;
5
+ default_capture_mode: CaptureMode;
6
+ sample_rate: number;
7
+ sessions_dir: string | null;
8
+ }
9
+ export declare const DEFAULT_CONFIG: AudioMcpConfig;
10
+ /**
11
+ * Load config from disk, creating the file with defaults if missing.
12
+ * Unknown keys are preserved on rewrite. Missing keys fall back to defaults.
13
+ */
14
+ export declare function loadOrCreateConfig(paths: Paths): AudioMcpConfig;
package/dist/config.js ADDED
@@ -0,0 +1,27 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ export const DEFAULT_CONFIG = {
3
+ default_source: null,
4
+ default_capture_mode: "both",
5
+ sample_rate: 16000,
6
+ sessions_dir: null,
7
+ };
8
+ /**
9
+ * Load config from disk, creating the file with defaults if missing.
10
+ * Unknown keys are preserved on rewrite. Missing keys fall back to defaults.
11
+ */
12
+ export function loadOrCreateConfig(paths) {
13
+ if (!existsSync(paths.configFile)) {
14
+ writeFileSync(paths.configFile, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", "utf8");
15
+ return { ...DEFAULT_CONFIG };
16
+ }
17
+ try {
18
+ const raw = readFileSync(paths.configFile, "utf8");
19
+ const parsed = JSON.parse(raw);
20
+ return { ...DEFAULT_CONFIG, ...parsed };
21
+ }
22
+ catch {
23
+ // Malformed config — fall back to defaults without overwriting user's file.
24
+ return { ...DEFAULT_CONFIG };
25
+ }
26
+ }
27
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAYlE,MAAM,CAAC,MAAM,cAAc,GAAmB;IAC5C,cAAc,EAAE,IAAI;IACpB,oBAAoB,EAAE,MAAM;IAC5B,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,IAAI;CACnB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAY;IAC7C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,aAAa,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;QACxF,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;IAC/B,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;QAC1D,OAAO,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,EAAE,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;IAC/B,CAAC;AACH,CAAC"}
@@ -0,0 +1,18 @@
1
+ export declare const ErrorCode: {
2
+ readonly SESSION_ALREADY_ACTIVE: "SESSION_ALREADY_ACTIVE";
3
+ readonly SESSION_NOT_FOUND: "SESSION_NOT_FOUND";
4
+ readonly SESSION_STILL_ACTIVE: "SESSION_STILL_ACTIVE";
5
+ readonly AUDIO_DEVICE_ERROR: "AUDIO_DEVICE_ERROR";
6
+ readonly CHUNK_TOO_LARGE: "CHUNK_TOO_LARGE";
7
+ readonly NOT_IMPLEMENTED: "NOT_IMPLEMENTED";
8
+ readonly INVALID_INPUT: "INVALID_INPUT";
9
+ };
10
+ export type ErrorCodeName = keyof typeof ErrorCode;
11
+ export declare class AudioMcpError extends Error {
12
+ readonly code: ErrorCodeName;
13
+ constructor(code: ErrorCodeName, message: string);
14
+ toJSON(): {
15
+ code: ErrorCodeName;
16
+ message: string;
17
+ };
18
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,21 @@
1
+ export const ErrorCode = {
2
+ SESSION_ALREADY_ACTIVE: "SESSION_ALREADY_ACTIVE",
3
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
4
+ SESSION_STILL_ACTIVE: "SESSION_STILL_ACTIVE",
5
+ AUDIO_DEVICE_ERROR: "AUDIO_DEVICE_ERROR",
6
+ CHUNK_TOO_LARGE: "CHUNK_TOO_LARGE",
7
+ NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
8
+ INVALID_INPUT: "INVALID_INPUT",
9
+ };
10
+ export class AudioMcpError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.name = "AudioMcpError";
15
+ this.code = code;
16
+ }
17
+ toJSON() {
18
+ return { code: this.code, message: this.message };
19
+ }
20
+ }
21
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,sBAAsB,EAAE,wBAAwB;IAChD,iBAAiB,EAAE,mBAAmB;IACtC,oBAAoB,EAAE,sBAAsB;IAC5C,kBAAkB,EAAE,oBAAoB;IACxC,eAAe,EAAE,iBAAiB;IAClC,eAAe,EAAE,iBAAiB;IAClC,aAAa,EAAE,eAAe;CACtB,CAAC;AAIX,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtB,IAAI,CAAgB;IAEpC,YAAY,IAAmB,EAAE,OAAe;QAC9C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IACpD,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from "./server.js";
3
+ startServer().catch((err) => {
4
+ // Log to stderr so it is visible in the MCP client's server log pane.
5
+ // stdout is reserved for the JSON-RPC transport.
6
+ // eslint-disable-next-line no-console
7
+ console.error("audio-mcp fatal:", err instanceof Error ? err.message : err);
8
+ process.exit(1);
9
+ });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAC1B,sEAAsE;IACtE,iDAAiD;IACjD,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC5E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export interface Logger {
3
+ debug(msg: string, meta?: Record<string, unknown>): void;
4
+ info(msg: string, meta?: Record<string, unknown>): void;
5
+ warn(msg: string, meta?: Record<string, unknown>): void;
6
+ error(msg: string, meta?: Record<string, unknown>): void;
7
+ }
8
+ /**
9
+ * File-backed logger. Writes JSON-per-line to `filePath`, rolling when the
10
+ * file grows past 10 MB (rotates to `<filePath>.1`, overwriting any prior).
11
+ * Writes are synchronous to guarantee ordering under SIGINT without leaking
12
+ * into MCP's stdio stream.
13
+ */
14
+ export declare function createFileLogger(filePath: string): Logger;
package/dist/logger.js ADDED
@@ -0,0 +1,34 @@
1
+ import { appendFileSync, existsSync, renameSync, statSync } from "node:fs";
2
+ const MAX_LOG_BYTES = 10 * 1024 * 1024; // 10 MB
3
+ /**
4
+ * File-backed logger. Writes JSON-per-line to `filePath`, rolling when the
5
+ * file grows past 10 MB (rotates to `<filePath>.1`, overwriting any prior).
6
+ * Writes are synchronous to guarantee ordering under SIGINT without leaking
7
+ * into MCP's stdio stream.
8
+ */
9
+ export function createFileLogger(filePath) {
10
+ const write = (level, msg, meta) => {
11
+ try {
12
+ if (existsSync(filePath) && statSync(filePath).size > MAX_LOG_BYTES) {
13
+ renameSync(filePath, `${filePath}.1`);
14
+ }
15
+ const entry = JSON.stringify({
16
+ ts: new Date().toISOString(),
17
+ level,
18
+ msg,
19
+ ...(meta ?? {}),
20
+ });
21
+ appendFileSync(filePath, entry + "\n", "utf8");
22
+ }
23
+ catch {
24
+ // Never let logging failures propagate.
25
+ }
26
+ };
27
+ return {
28
+ debug: (msg, meta) => write("debug", msg, meta),
29
+ info: (msg, meta) => write("info", msg, meta),
30
+ warn: (msg, meta) => write("warn", msg, meta),
31
+ error: (msg, meta) => write("error", msg, meta),
32
+ };
33
+ }
34
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE3E,MAAM,aAAa,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AAWhD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,KAAK,GAAG,CAAC,KAAe,EAAE,GAAW,EAAE,IAA8B,EAAQ,EAAE;QACnF,IAAI,CAAC;YACH,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,CAAC,IAAI,GAAG,aAAa,EAAE,CAAC;gBACpE,UAAU,CAAC,QAAQ,EAAE,GAAG,QAAQ,IAAI,CAAC,CAAC;YACxC,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;gBAC3B,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBAC5B,KAAK;gBACL,GAAG;gBACH,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;aAChB,CAAC,CAAC;YACH,cAAc,CAAC,QAAQ,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,wCAAwC;QAC1C,CAAC;IACH,CAAC,CAAC;IACF,OAAO;QACL,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC;QAC/C,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;QAC7C,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;QAC7C,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC;KAChD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ export interface Paths {
2
+ root: string;
3
+ sessionsDir: string;
4
+ configFile: string;
5
+ logFile: string;
6
+ }
7
+ /** Expand a leading `~` in a path to the user's home directory. */
8
+ export declare function expandHome(p: string): string;
9
+ /**
10
+ * Resolve audio-mcp storage paths. By default rooted at `~/.audio-mcp`.
11
+ * A custom root can be supplied (used by tests to isolate storage in a tmp dir).
12
+ */
13
+ export declare function resolvePaths(root?: string, sessionsDirOverride?: string): Paths;
14
+ /** Ensure the root and sessions directories exist. Idempotent. */
15
+ export declare function ensureDirs(paths: Paths): void;