recmp3-cli 1.0.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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export { deleteSecret, getSecret, keychainAvailable, setSecret } from './chunk-FNZ6ZCOK.js';
3
+ import './chunk-DDXRBIWU.js';
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ import { findFfmpeg } from './chunk-7NR5CU7W.js';
3
+ import { AudioCaptureError } from './chunk-NUWDWBJQ.js';
4
+ import { execFile, spawn } from 'child_process';
5
+ import { stat } from 'fs/promises';
6
+ import { promisify } from 'util';
7
+
8
+ var execFileAsync = promisify(execFile);
9
+ var LinuxPulseCapture = class {
10
+ process = null;
11
+ startedAt = null;
12
+ outputPath = null;
13
+ recording = false;
14
+ async start(opts) {
15
+ const ffmpeg = await findFfmpeg();
16
+ const args = [
17
+ "-hide_banner",
18
+ "-loglevel",
19
+ "error",
20
+ "-f",
21
+ "pulse",
22
+ "-i",
23
+ opts.source,
24
+ "-ac",
25
+ String(opts.channels),
26
+ "-ar",
27
+ String(opts.sampleRate),
28
+ "-c:a",
29
+ "pcm_s16le",
30
+ "-y",
31
+ opts.outputPath
32
+ ];
33
+ this.outputPath = opts.outputPath;
34
+ this.startedAt = /* @__PURE__ */ new Date();
35
+ const proc = spawn(ffmpeg, args, { stdio: ["pipe", "pipe", "pipe"] });
36
+ this.process = proc;
37
+ this.recording = true;
38
+ await new Promise((resolve, reject) => {
39
+ const timer = setTimeout(() => resolve(), 500);
40
+ proc.on("error", (err) => {
41
+ clearTimeout(timer);
42
+ this.recording = false;
43
+ reject(new AudioCaptureError(`Failed to start ffmpeg: ${err.message}`));
44
+ });
45
+ proc.on("close", (code) => {
46
+ clearTimeout(timer);
47
+ this.recording = false;
48
+ });
49
+ let stderrBuf = "";
50
+ proc.stderr?.on("data", (chunk) => {
51
+ stderrBuf += chunk.toString();
52
+ });
53
+ setTimeout(() => {
54
+ if (!proc.pid) {
55
+ clearTimeout(timer);
56
+ reject(
57
+ new AudioCaptureError(
58
+ `ffmpeg failed to start. Check audio source: "${opts.source}"`
59
+ )
60
+ );
61
+ }
62
+ }, 200);
63
+ });
64
+ if (!proc.pid) {
65
+ throw new AudioCaptureError(
66
+ `ffmpeg failed to start. Run 'recmp3 sources' to list available audio sources.`
67
+ );
68
+ }
69
+ }
70
+ async stop() {
71
+ if (!this.process || !this.recording) {
72
+ throw new AudioCaptureError("Not currently recording.");
73
+ }
74
+ const proc = this.process;
75
+ const startedAt = this.startedAt ?? /* @__PURE__ */ new Date();
76
+ const outputPath = this.outputPath;
77
+ const endedAt = /* @__PURE__ */ new Date();
78
+ this.recording = false;
79
+ this.process = null;
80
+ this.startedAt = null;
81
+ this.outputPath = null;
82
+ await new Promise((resolve, reject) => {
83
+ proc.on("close", () => resolve());
84
+ proc.on("error", reject);
85
+ try {
86
+ proc.stdin?.write("q");
87
+ proc.stdin?.end();
88
+ } catch {
89
+ proc.kill("SIGTERM");
90
+ }
91
+ const forceKill = setTimeout(() => {
92
+ proc.kill("SIGTERM");
93
+ }, 5e3);
94
+ proc.on("close", () => clearTimeout(forceKill));
95
+ });
96
+ const fileStat = await stat(outputPath).catch(() => ({ size: 0 }));
97
+ const durationSec = (endedAt.getTime() - startedAt.getTime()) / 1e3;
98
+ return {
99
+ path: outputPath,
100
+ durationSec,
101
+ sizeBytes: fileStat.size,
102
+ startedAt,
103
+ endedAt
104
+ };
105
+ }
106
+ isRecording() {
107
+ return this.recording;
108
+ }
109
+ async dispose() {
110
+ if (this.process) {
111
+ try {
112
+ this.process.stdin?.end();
113
+ this.process.kill("SIGTERM");
114
+ } catch {
115
+ }
116
+ this.process = null;
117
+ }
118
+ this.recording = false;
119
+ }
120
+ };
121
+ var LinuxPulseCaptureFactory = class {
122
+ create() {
123
+ return new LinuxPulseCapture();
124
+ }
125
+ async listSources() {
126
+ try {
127
+ const { stdout } = await execFileAsync("pactl", [
128
+ "list",
129
+ "sources",
130
+ "short"
131
+ ]);
132
+ const sources = [];
133
+ for (const line of stdout.trim().split("\n")) {
134
+ const parts = line.split(" ");
135
+ if (parts.length < 2) continue;
136
+ const id = parts[1];
137
+ if (!id) continue;
138
+ const isMonitor = id.includes(".monitor");
139
+ sources.push({
140
+ id,
141
+ label: isMonitor ? `${id} (system audio monitor)` : id,
142
+ isDefault: id === "default" || false
143
+ });
144
+ }
145
+ const hasDefault = sources.some((s) => s.id === "default");
146
+ if (!hasDefault) {
147
+ sources.unshift({
148
+ id: "default",
149
+ label: "default (system default)",
150
+ isDefault: true
151
+ });
152
+ }
153
+ return sources;
154
+ } catch {
155
+ try {
156
+ const ffmpeg = await findFfmpeg();
157
+ const { stderr } = await execFileAsync(ffmpeg, [
158
+ "-sources",
159
+ "pulse",
160
+ "-hide_banner"
161
+ ]);
162
+ const sources = [
163
+ { id: "default", label: "default (system default)", isDefault: true }
164
+ ];
165
+ for (const line of stderr.split("\n")) {
166
+ const match = line.match(/^\s+(\S+)\s/);
167
+ if (match)
168
+ sources.push({ id: match[1], label: match[1], isDefault: false });
169
+ }
170
+ return sources;
171
+ } catch {
172
+ return [
173
+ { id: "default", label: "default (system default)", isDefault: true }
174
+ ];
175
+ }
176
+ }
177
+ }
178
+ defaultSource() {
179
+ return process.env.RECMP3_SOURCE ?? "default";
180
+ }
181
+ };
182
+
183
+ export { LinuxPulseCapture, LinuxPulseCaptureFactory };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export { LocalWhisperProvider } from './chunk-NY5EJT5D.js';
3
+ import './chunk-DDXRBIWU.js';
4
+ import './chunk-NUWDWBJQ.js';
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import { findFfmpeg } from './chunk-7NR5CU7W.js';
3
+ import { AudioCaptureError } from './chunk-NUWDWBJQ.js';
4
+ import { execFile, spawn } from 'child_process';
5
+ import { stat } from 'fs/promises';
6
+ import { promisify } from 'util';
7
+
8
+ var execFileAsync = promisify(execFile);
9
+ var MacAvFoundationCapture = class {
10
+ process = null;
11
+ startedAt = null;
12
+ outputPath = null;
13
+ recording = false;
14
+ async start(opts) {
15
+ const ffmpeg = await findFfmpeg();
16
+ const source = opts.source.startsWith(":") ? opts.source : `:${opts.source}`;
17
+ const args = [
18
+ "-hide_banner",
19
+ "-loglevel",
20
+ "error",
21
+ "-f",
22
+ "avfoundation",
23
+ "-i",
24
+ source,
25
+ "-ac",
26
+ String(opts.channels),
27
+ "-ar",
28
+ String(opts.sampleRate),
29
+ "-c:a",
30
+ "pcm_s16le",
31
+ "-y",
32
+ opts.outputPath
33
+ ];
34
+ this.outputPath = opts.outputPath;
35
+ this.startedAt = /* @__PURE__ */ new Date();
36
+ const proc = spawn(ffmpeg, args, { stdio: ["pipe", "pipe", "pipe"] });
37
+ this.process = proc;
38
+ this.recording = true;
39
+ await new Promise((resolve, reject) => {
40
+ const timer = setTimeout(() => resolve(), 800);
41
+ proc.on("error", (err) => {
42
+ clearTimeout(timer);
43
+ this.recording = false;
44
+ reject(
45
+ new AudioCaptureError(
46
+ `Failed to start ffmpeg: ${err.message}. Grant microphone access in System Settings \u2192 Privacy & Security \u2192 Microphone.`
47
+ )
48
+ );
49
+ });
50
+ });
51
+ }
52
+ async stop() {
53
+ if (!this.process || !this.recording)
54
+ throw new AudioCaptureError("Not recording.");
55
+ const proc = this.process;
56
+ const startedAt = this.startedAt ?? /* @__PURE__ */ new Date();
57
+ const outputPath = this.outputPath;
58
+ const endedAt = /* @__PURE__ */ new Date();
59
+ this.recording = false;
60
+ this.process = null;
61
+ await new Promise((resolve) => {
62
+ proc.on("close", resolve);
63
+ try {
64
+ proc.stdin?.write("q");
65
+ proc.stdin?.end();
66
+ } catch {
67
+ proc.kill("SIGTERM");
68
+ }
69
+ setTimeout(() => proc.kill("SIGTERM"), 5e3);
70
+ });
71
+ const fileStat = await stat(outputPath).catch(() => ({ size: 0 }));
72
+ return {
73
+ path: outputPath,
74
+ durationSec: (endedAt.getTime() - startedAt.getTime()) / 1e3,
75
+ sizeBytes: fileStat.size,
76
+ startedAt,
77
+ endedAt
78
+ };
79
+ }
80
+ isRecording() {
81
+ return this.recording;
82
+ }
83
+ async dispose() {
84
+ if (this.process) {
85
+ try {
86
+ this.process.kill("SIGTERM");
87
+ } catch {
88
+ }
89
+ this.process = null;
90
+ }
91
+ this.recording = false;
92
+ }
93
+ };
94
+ var MacAvFoundationFactory = class {
95
+ create() {
96
+ return new MacAvFoundationCapture();
97
+ }
98
+ async listSources() {
99
+ try {
100
+ const ffmpeg = await findFfmpeg();
101
+ const { stderr } = await execFileAsync(ffmpeg, [
102
+ "-f",
103
+ "avfoundation",
104
+ "-list_devices",
105
+ "true",
106
+ "-i",
107
+ ""
108
+ ]);
109
+ const sources = [];
110
+ let inAudioSection = false;
111
+ for (const line of stderr.split("\n")) {
112
+ if (line.includes("AVFoundation audio devices:")) {
113
+ inAudioSection = true;
114
+ continue;
115
+ }
116
+ if (!inAudioSection) continue;
117
+ const match = line.match(/\[(\d+)\] (.+)/);
118
+ if (match) {
119
+ sources.push({
120
+ id: match[1],
121
+ label: match[2],
122
+ isDefault: match[1] === "0"
123
+ });
124
+ }
125
+ }
126
+ return sources.length > 0 ? sources : [{ id: "0", label: "Default audio device", isDefault: true }];
127
+ } catch {
128
+ return [{ id: "0", label: "Default audio device", isDefault: true }];
129
+ }
130
+ }
131
+ defaultSource() {
132
+ return process.env.RECMP3_SOURCE ?? "0";
133
+ }
134
+ };
135
+
136
+ export { MacAvFoundationCapture, MacAvFoundationFactory };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { createProvider, providerUploads } from './chunk-XGHYROLT.js';
3
+ import './chunk-NY5EJT5D.js';
4
+ import './chunk-DDXRBIWU.js';
5
+ import './chunk-NUWDWBJQ.js';
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ import { findFfmpeg } from './chunk-7NR5CU7W.js';
3
+ import { AudioCaptureError } from './chunk-NUWDWBJQ.js';
4
+ import { execFile, spawn } from 'child_process';
5
+ import { stat } from 'fs/promises';
6
+ import { promisify } from 'util';
7
+
8
+ var execFileAsync = promisify(execFile);
9
+ var WindowsDshowCapture = class {
10
+ process = null;
11
+ startedAt = null;
12
+ outputPath = null;
13
+ recording = false;
14
+ async start(opts) {
15
+ const ffmpeg = await findFfmpeg();
16
+ const deviceName = opts.source === "default" ? await this.getDefaultDevice() : opts.source;
17
+ const args = [
18
+ "-hide_banner",
19
+ "-loglevel",
20
+ "error",
21
+ "-f",
22
+ "dshow",
23
+ "-i",
24
+ `audio=${deviceName}`,
25
+ "-ac",
26
+ String(opts.channels),
27
+ "-ar",
28
+ String(opts.sampleRate),
29
+ "-c:a",
30
+ "pcm_s16le",
31
+ "-y",
32
+ opts.outputPath
33
+ ];
34
+ this.outputPath = opts.outputPath;
35
+ this.startedAt = /* @__PURE__ */ new Date();
36
+ const proc = spawn(ffmpeg, args, { stdio: ["pipe", "pipe", "pipe"] });
37
+ this.process = proc;
38
+ this.recording = true;
39
+ await new Promise((resolve, reject) => {
40
+ const timer = setTimeout(() => resolve(), 1e3);
41
+ proc.on("error", (err) => {
42
+ clearTimeout(timer);
43
+ this.recording = false;
44
+ reject(
45
+ new AudioCaptureError(
46
+ `Failed to start recording: ${err.message}. Run 'recmp3 sources' to list available devices.`
47
+ )
48
+ );
49
+ });
50
+ });
51
+ }
52
+ async getDefaultDevice() {
53
+ const sources = await this.listSources();
54
+ const first = sources.find((s) => !s.label.includes("monitor")) ?? sources[0];
55
+ return first?.id ?? "Microphone";
56
+ }
57
+ async stop() {
58
+ if (!this.process || !this.recording)
59
+ throw new AudioCaptureError("Not recording.");
60
+ const proc = this.process;
61
+ const startedAt = this.startedAt ?? /* @__PURE__ */ new Date();
62
+ const outputPath = this.outputPath;
63
+ const endedAt = /* @__PURE__ */ new Date();
64
+ this.recording = false;
65
+ this.process = null;
66
+ await new Promise((resolve) => {
67
+ proc.on("close", resolve);
68
+ try {
69
+ proc.stdin?.write("q");
70
+ proc.stdin?.end();
71
+ } catch {
72
+ proc.kill("SIGTERM");
73
+ }
74
+ setTimeout(() => proc.kill("SIGTERM"), 5e3);
75
+ });
76
+ const fileStat = await stat(outputPath).catch(() => ({ size: 0 }));
77
+ return {
78
+ path: outputPath,
79
+ durationSec: (endedAt.getTime() - startedAt.getTime()) / 1e3,
80
+ sizeBytes: fileStat.size,
81
+ startedAt,
82
+ endedAt
83
+ };
84
+ }
85
+ isRecording() {
86
+ return this.recording;
87
+ }
88
+ async dispose() {
89
+ if (this.process) {
90
+ try {
91
+ this.process.kill("SIGTERM");
92
+ } catch {
93
+ }
94
+ this.process = null;
95
+ }
96
+ this.recording = false;
97
+ }
98
+ async listSources() {
99
+ try {
100
+ const ffmpeg = await findFfmpeg();
101
+ const { stderr } = await execFileAsync(ffmpeg, [
102
+ "-list_devices",
103
+ "true",
104
+ "-f",
105
+ "dshow",
106
+ "-i",
107
+ "dummy"
108
+ ]);
109
+ const sources = [];
110
+ let inAudioSection = false;
111
+ for (const line of stderr.split("\n")) {
112
+ if (line.includes("DirectShow audio devices")) {
113
+ inAudioSection = true;
114
+ continue;
115
+ }
116
+ if (line.includes("DirectShow video devices")) {
117
+ inAudioSection = false;
118
+ continue;
119
+ }
120
+ if (!inAudioSection) continue;
121
+ const match = line.match(/"([^"]+)"/);
122
+ if (match)
123
+ sources.push({
124
+ id: match[1],
125
+ label: match[1],
126
+ isDefault: sources.length === 0
127
+ });
128
+ }
129
+ return sources;
130
+ } catch {
131
+ return [
132
+ { id: "Microphone", label: "Microphone (default)", isDefault: true }
133
+ ];
134
+ }
135
+ }
136
+ };
137
+ var WindowsDshowFactory = class {
138
+ capture = new WindowsDshowCapture();
139
+ create() {
140
+ return new WindowsDshowCapture();
141
+ }
142
+ async listSources() {
143
+ return this.capture.listSources();
144
+ }
145
+ defaultSource() {
146
+ return process.env.RECMP3_SOURCE ?? "default";
147
+ }
148
+ };
149
+
150
+ export { WindowsDshowCapture, WindowsDshowFactory };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "recmp3-cli",
3
+ "version": "1.0.0",
4
+ "description": "Record audio, transcribe with AI, output developer-ready prompts — for humans and AI agents, from a single terminal command.",
5
+ "type": "module",
6
+ "bin": {
7
+ "recmp3": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "LICENSE",
12
+ "LICENSE-COMMERCIAL.md",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "tsup && chmod +x dist/index.js",
20
+ "dev": "tsx src/index.ts",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "biome check src",
23
+ "format": "biome format --write src",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.29.0",
30
+ "clipboardy": "^4.0.0",
31
+ "commander": "^13.1.0",
32
+ "dotenv": "^16.4.5",
33
+ "env-paths": "^3.0.0",
34
+ "ink": "^5.1.0",
35
+ "ora": "^8.1.1",
36
+ "picocolors": "^1.1.1",
37
+ "react": "^18.3.1",
38
+ "zod": "^3.23.8"
39
+ },
40
+ "devDependencies": {
41
+ "@biomejs/biome": "^1.9.3",
42
+ "@types/node": "^20.16.11",
43
+ "@types/react": "^18.3.11",
44
+ "@vitest/coverage-v8": "4.1.8",
45
+ "msw": "^2.4.9",
46
+ "tsup": "^8.3.0",
47
+ "tsx": "^4.19.1",
48
+ "typescript": "^5.6.3",
49
+ "vitest": "^4.1.8"
50
+ },
51
+ "optionalDependencies": {
52
+ "keytar": "^7.9.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=20.0.0"
56
+ },
57
+ "license": "AGPL-3.0-or-later",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/aedneth/recmp3-cli.git"
61
+ },
62
+ "keywords": [
63
+ "cli",
64
+ "audio",
65
+ "recording",
66
+ "transcription",
67
+ "whisper",
68
+ "groq",
69
+ "developer-tools",
70
+ "vibecoding",
71
+ "mcp",
72
+ "ai-agent",
73
+ "agent-native"
74
+ ]
75
+ }