@hasna/recordings 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -191
  2. package/bun.lock +219 -0
  3. package/bunfig.toml +2 -0
  4. package/dist/__tests__/preload.d.ts +5 -0
  5. package/dist/__tests__/preload.d.ts.map +1 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +1446 -0
  9. package/dist/db/agents.d.ts +6 -0
  10. package/dist/db/agents.d.ts.map +1 -0
  11. package/dist/db/database.d.ts +7 -0
  12. package/dist/db/database.d.ts.map +1 -0
  13. package/dist/db/projects.d.ts +6 -0
  14. package/dist/db/projects.d.ts.map +1 -0
  15. package/dist/db/recordings.d.ts +15 -0
  16. package/dist/db/recordings.d.ts.map +1 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +769 -30752
  20. package/dist/lib/config.d.ts +6 -0
  21. package/dist/lib/config.d.ts.map +1 -0
  22. package/dist/lib/enhancer.d.ts +25 -0
  23. package/dist/lib/enhancer.d.ts.map +1 -0
  24. package/dist/lib/recorder.d.ts +30 -0
  25. package/dist/lib/recorder.d.ts.map +1 -0
  26. package/dist/lib/transcriber.d.ts +5 -0
  27. package/dist/lib/transcriber.d.ts.map +1 -0
  28. package/dist/mcp/index.d.ts +3 -0
  29. package/dist/mcp/index.d.ts.map +1 -0
  30. package/dist/mcp/index.js +4912 -0
  31. package/dist/types/index.d.ts +103 -0
  32. package/dist/types/index.d.ts.map +1 -0
  33. package/package.json +35 -44
  34. package/src/__tests__/agents.test.ts +136 -0
  35. package/src/__tests__/config.test.ts +252 -0
  36. package/src/__tests__/database.test.ts +167 -0
  37. package/src/__tests__/enhancer.test.ts +574 -0
  38. package/src/__tests__/preload.ts +4 -0
  39. package/src/__tests__/projects.test.ts +109 -0
  40. package/src/__tests__/recorder.test.ts +278 -0
  41. package/src/__tests__/recordings.test.ts +353 -0
  42. package/src/__tests__/transcriber.test.ts +322 -0
  43. package/src/__tests__/types.test.ts +75 -0
  44. package/src/cli/index.ts +1078 -0
  45. package/src/db/agents.ts +81 -0
  46. package/src/db/database.ts +126 -0
  47. package/src/db/projects.ts +71 -0
  48. package/src/db/recordings.ts +219 -0
  49. package/src/index.ts +81 -0
  50. package/src/lib/config.ts +166 -0
  51. package/src/lib/enhancer.ts +167 -0
  52. package/src/lib/recorder.ts +198 -0
  53. package/src/lib/transcriber.ts +105 -0
  54. package/src/mcp/index.ts +446 -0
  55. package/src/native/RecordingsHelper.swift +352 -0
  56. package/src/types/index.ts +138 -0
  57. package/tsconfig.json +21 -0
  58. package/README.md +0 -145
  59. package/dist/index.js.map +0 -272
  60. package/scripts/postinstall.ts +0 -80
@@ -0,0 +1,352 @@
1
+ import Cocoa
2
+ import Carbon
3
+
4
+ // ── RecordingsHelper ────────────────────────────────────────────────────────
5
+ // Lightweight menu bar app that toggles speech recording with a global hotkey.
6
+ // Press the hotkey to start recording, press again to stop + transcribe + paste.
7
+ // Works like Wispr Flow — no UI chrome, just a menu bar icon and a hotkey.
8
+
9
+ class AppDelegate: NSObject, NSApplicationDelegate {
10
+ var statusItem: NSStatusItem!
11
+ var isRecording = false
12
+ var recordProcess: Process?
13
+ var currentAudioPath: String?
14
+ var hotkeyRef: EventHotKeyRef?
15
+ var spaceDownTime: Date?
16
+ var longPressTimer: Timer?
17
+ let longPressDuration: TimeInterval = 1.0 // Hold space for 1 second to activate
18
+
19
+ let recordingsDir: String = {
20
+ let home = FileManager.default.homeDirectoryForCurrentUser.path
21
+ return "\(home)/.recordings"
22
+ }()
23
+
24
+ let audioDir: String = {
25
+ let home = FileManager.default.homeDirectoryForCurrentUser.path
26
+ return "\(home)/.recordings/audio"
27
+ }()
28
+
29
+ let recordingsBin: String = {
30
+ let home = FileManager.default.homeDirectoryForCurrentUser.path
31
+ return "\(home)/.bun/bin/recordings"
32
+ }()
33
+
34
+ func applicationDidFinishLaunching(_ notification: Notification) {
35
+ // Create audio dir
36
+ try? FileManager.default.createDirectory(atPath: audioDir, withIntermediateDirectories: true)
37
+
38
+ // Menu bar icon
39
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
40
+ updateIcon()
41
+
42
+ // Menu
43
+ let menu = NSMenu()
44
+ menu.addItem(NSMenuItem(title: "Hold Space (1s) to Record", action: #selector(toggleRecording), keyEquivalent: ""))
45
+ menu.addItem(NSMenuItem.separator())
46
+ menu.addItem(NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: "q"))
47
+ statusItem.menu = menu
48
+
49
+ // Register global hotkey: F5
50
+ registerHotkey()
51
+
52
+ showNotification(title: "Recordings", body: "Ready — hold Space for 1s to record")
53
+ }
54
+
55
+ func updateIcon() {
56
+ if let button = statusItem.button {
57
+ button.title = isRecording ? "⏺" : "🎙"
58
+ }
59
+ }
60
+
61
+ // ── Global Hotkey ───────────────────────────────────────────────────────
62
+
63
+ func registerHotkey() {
64
+ // Hold Space for 1+ second to start recording, release to stop.
65
+ // Uses CGEventTap to monitor all keyboard events globally.
66
+ // Normal space presses (< 1 second) pass through untouched.
67
+
68
+ let eventMask: CGEventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue) | (1 << CGEventType.flagsChanged.rawValue)
69
+
70
+ guard let tap = CGEvent.tapCreate(
71
+ tap: .cgSessionEventTap,
72
+ place: .headInsertEventTap,
73
+ options: .defaultTap,
74
+ eventsOfInterest: eventMask,
75
+ callback: { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
76
+ let appDelegate = Unmanaged<AppDelegate>.fromOpaque(refcon!).takeUnretainedValue()
77
+ let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
78
+
79
+ // Space bar = keycode 49
80
+ guard keyCode == 49 else {
81
+ return Unmanaged.passRetained(event)
82
+ }
83
+
84
+ if type == .keyDown {
85
+ // Ignore key repeat events (auto-repeat while held)
86
+ let isRepeat = event.getIntegerValueField(.keyboardEventAutorepeat)
87
+ if isRepeat != 0 {
88
+ // Already tracking this press — suppress the repeat
89
+ if appDelegate.spaceDownTime != nil {
90
+ return nil // Swallow repeat while we're tracking
91
+ }
92
+ return Unmanaged.passRetained(event)
93
+ }
94
+
95
+ if appDelegate.spaceDownTime == nil {
96
+ appDelegate.spaceDownTime = Date()
97
+ // Start a timer — if space is still held after 1s, begin recording
98
+ DispatchQueue.main.async {
99
+ appDelegate.longPressTimer?.invalidate()
100
+ appDelegate.longPressTimer = Timer.scheduledTimer(withTimeInterval: appDelegate.longPressDuration, repeats: false) { _ in
101
+ if appDelegate.spaceDownTime != nil && !appDelegate.isRecording {
102
+ appDelegate.startRecording()
103
+ }
104
+ }
105
+ }
106
+ }
107
+ // Let the keyDown through for now (normal typing)
108
+ return Unmanaged.passRetained(event)
109
+
110
+ } else if type == .keyUp {
111
+ let wasLongPress = appDelegate.isRecording
112
+
113
+ if appDelegate.isRecording {
114
+ // Space released after recording — stop and transcribe
115
+ DispatchQueue.main.async {
116
+ appDelegate.stopAndTranscribe()
117
+ }
118
+ }
119
+
120
+ // Cancel timer
121
+ DispatchQueue.main.async {
122
+ appDelegate.longPressTimer?.invalidate()
123
+ appDelegate.longPressTimer = nil
124
+ }
125
+ appDelegate.spaceDownTime = nil
126
+
127
+ if wasLongPress {
128
+ // Swallow the keyUp so it doesn't type a space
129
+ return nil
130
+ }
131
+ return Unmanaged.passRetained(event)
132
+ }
133
+
134
+ return Unmanaged.passRetained(event)
135
+ },
136
+ userInfo: Unmanaged.passUnretained(self).toOpaque()
137
+ ) else {
138
+ showNotification(title: "Error", body: "Could not create event tap. Grant Accessibility permission in System Settings > Privacy > Accessibility")
139
+ return
140
+ }
141
+
142
+ let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
143
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
144
+ CGEvent.tapEnable(tap: tap, enable: true)
145
+ }
146
+
147
+ // ── Recording ────────────────────────────────────────────────────────────
148
+
149
+ @objc func toggleRecording() {
150
+ if isRecording { stopAndTranscribe() } else { startRecording() }
151
+ }
152
+
153
+ func startRecording() {
154
+ let formatter = DateFormatter()
155
+ formatter.dateFormat = "yyyyMMdd'T'HHmmss"
156
+ let filename = "recording-\(formatter.string(from: Date())).wav"
157
+ let filepath = "\(audioDir)/\(filename)"
158
+ currentAudioPath = filepath
159
+
160
+ // Run ffmpeg via bash — pipe stdin so we can send 'q' to stop gracefully
161
+ let process = Process()
162
+ let stdinPipe = Pipe()
163
+ process.executableURL = URL(fileURLWithPath: "/bin/bash")
164
+ process.arguments = ["-c", """
165
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
166
+ if command -v ffmpeg &>/dev/null; then
167
+ ffmpeg -f avfoundation -i ":0" -ar 16000 -ac 1 -t 300 "\(filepath)" -y 2>/dev/null
168
+ elif command -v rec &>/dev/null; then
169
+ rec -r 16000 -c 1 -b 16 "\(filepath)" trim 0 300
170
+ else
171
+ exit 1
172
+ fi
173
+ """]
174
+ process.standardInput = stdinPipe
175
+ process.standardOutput = FileHandle.nullDevice
176
+ process.standardError = FileHandle.nullDevice
177
+
178
+ do {
179
+ try process.run()
180
+ recordProcess = process
181
+ stdinPipeRef = stdinPipe
182
+ isRecording = true
183
+ updateIcon()
184
+ showNotification(title: "Recording...", body: "Press F5 to stop")
185
+ } catch {
186
+ showNotification(title: "Error", body: "Could not start recording: \(error.localizedDescription)")
187
+ }
188
+ }
189
+
190
+ var stdinPipeRef: Pipe?
191
+
192
+ func stopAndTranscribe() {
193
+ guard let process = recordProcess else { return }
194
+
195
+ // Send 'q' to ffmpeg stdin for graceful stop (flushes WAV header)
196
+ if let pipe = stdinPipeRef {
197
+ pipe.fileHandleForWriting.write("q".data(using: .utf8)!)
198
+ try? pipe.fileHandleForWriting.close()
199
+ }
200
+ // Wait for graceful exit, then force if needed
201
+ Thread.sleep(forTimeInterval: 1.0)
202
+ if process.isRunning {
203
+ process.terminate()
204
+ Thread.sleep(forTimeInterval: 0.3)
205
+ }
206
+ if process.isRunning {
207
+ process.interrupt()
208
+ }
209
+ recordProcess = nil
210
+ stdinPipeRef = nil
211
+ isRecording = false
212
+ updateIcon()
213
+
214
+ guard let audioPath = currentAudioPath else { return }
215
+ currentAudioPath = nil
216
+
217
+ // Check if audio file exists and has content
218
+ let fileManager = FileManager.default
219
+ guard fileManager.fileExists(atPath: audioPath) else {
220
+ showNotification(title: "Error", body: "Audio file not created — check mic permissions")
221
+ return
222
+ }
223
+
224
+ let attrs = try? fileManager.attributesOfItem(atPath: audioPath)
225
+ let size = attrs?[.size] as? Int ?? 0
226
+ if size < 1000 {
227
+ showNotification(title: "Error", body: "Audio too short (\(size) bytes) — try speaking longer")
228
+ return
229
+ }
230
+
231
+ showNotification(title: "Transcribing...", body: "Processing your recording")
232
+
233
+ // Transcribe in background
234
+ DispatchQueue.global(qos: .userInitiated).async { [self] in
235
+ let output = self.runCommand(self.recordingsBin, arguments: ["transcribe", audioPath, "--json"])
236
+
237
+ // The CLI outputs "Transcribing...\n" then the text, then JSON on --json
238
+ // Find the JSON object in the output
239
+ var text = ""
240
+
241
+ // Try to find JSON in the output
242
+ if let jsonStart = output.range(of: "{"),
243
+ let jsonEnd = output.range(of: "}", options: .backwards) {
244
+ let jsonStr = String(output[jsonStart.lowerBound...jsonEnd.upperBound])
245
+ if let data = jsonStr.data(using: .utf8),
246
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
247
+ if let pt = json["processed_text"] as? String, !pt.isEmpty {
248
+ text = pt
249
+ } else if let rt = json["raw_text"] as? String, !rt.isEmpty {
250
+ text = rt
251
+ }
252
+ }
253
+ }
254
+
255
+ // Fallback: grab everything after the last newline before JSON
256
+ if text.isEmpty {
257
+ let lines = output.components(separatedBy: "\n").filter { !$0.isEmpty && !$0.contains("Transcribing") && !$0.contains("{") }
258
+ for line in lines {
259
+ let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
260
+ if !trimmed.isEmpty && !trimmed.hasPrefix("Saved") {
261
+ text = trimmed
262
+ break
263
+ }
264
+ }
265
+ }
266
+
267
+ guard !text.isEmpty else {
268
+ DispatchQueue.main.async {
269
+ let dbg = output.prefix(200)
270
+ self.showNotification(title: "Error", body: "Empty transcription. Output: \(dbg)")
271
+ }
272
+ return
273
+ }
274
+
275
+ DispatchQueue.main.async {
276
+ // Copy to clipboard
277
+ let pasteboard = NSPasteboard.general
278
+ pasteboard.clearContents()
279
+ pasteboard.setString(text, forType: .string)
280
+
281
+ // Auto-paste into the frontmost app (like Wispr Flow)
282
+ self.simulatePaste()
283
+
284
+ self.showNotification(title: "Done", body: String(text.prefix(80)))
285
+ }
286
+ }
287
+ }
288
+
289
+ // ── Helpers ─────────────────────────────────────────────────────────────
290
+
291
+ func simulatePaste() {
292
+ // Simulate Cmd+V
293
+ let source = CGEventSource(stateID: .hidSystemState)
294
+
295
+ let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: true) // V key
296
+ keyDown?.flags = .maskCommand
297
+ keyDown?.post(tap: .cghidEventTap)
298
+
299
+ let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: false)
300
+ keyUp?.flags = .maskCommand
301
+ keyUp?.post(tap: .cghidEventTap)
302
+ }
303
+
304
+ func runCommand(_ command: String, arguments: [String]) -> String {
305
+ let process = Process()
306
+ let pipe = Pipe()
307
+ let errPipe = Pipe()
308
+ process.executableURL = URL(fileURLWithPath: "/bin/bash")
309
+
310
+ let home = FileManager.default.homeDirectoryForCurrentUser.path
311
+ let args = arguments.map { "\"\($0)\"" }.joined(separator: " ")
312
+ process.arguments = ["-c", """
313
+ export PATH="\(home)/.bun/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
314
+ "\(command)" \(args)
315
+ """]
316
+ process.standardOutput = pipe
317
+ process.standardError = errPipe
318
+
319
+ do {
320
+ try process.run()
321
+ process.waitUntilExit()
322
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
323
+ return String(data: data, encoding: .utf8) ?? ""
324
+ } catch {
325
+ return "ERROR: \(error.localizedDescription)"
326
+ }
327
+ }
328
+
329
+ func showNotification(title: String, body: String) {
330
+ let process = Process()
331
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
332
+ process.arguments = ["-e", "display notification \"\(body)\" with title \"\(title)\""]
333
+ process.standardOutput = FileHandle.nullDevice
334
+ process.standardError = FileHandle.nullDevice
335
+ try? process.run()
336
+ }
337
+
338
+ @objc func quit() {
339
+ if let process = recordProcess {
340
+ process.interrupt()
341
+ }
342
+ NSApplication.shared.terminate(nil)
343
+ }
344
+ }
345
+
346
+ // ── Main ────────────────────────────────────────────────────────────────────
347
+
348
+ let app = NSApplication.shared
349
+ app.setActivationPolicy(.accessory) // Menu bar only, no dock icon
350
+ let delegate = AppDelegate()
351
+ app.delegate = delegate
352
+ app.run()
@@ -0,0 +1,138 @@
1
+ // ── Recording Types ──────────────────────────────────────────────────────────
2
+
3
+ export interface Recording {
4
+ id: string;
5
+ audio_path: string | null;
6
+ raw_text: string;
7
+ processed_text: string | null;
8
+ processing_mode: ProcessingMode;
9
+ model_used: string;
10
+ enhancement_model: string | null;
11
+ duration_ms: number;
12
+ language: string | null;
13
+ tags: string[];
14
+ agent_id: string | null;
15
+ project_id: string | null;
16
+ session_id: string | null;
17
+ metadata: Record<string, unknown>;
18
+ created_at: string;
19
+ }
20
+
21
+ export type ProcessingMode = "raw" | "enhanced";
22
+
23
+ export interface CreateRecordingInput {
24
+ audio_path?: string;
25
+ raw_text: string;
26
+ processed_text?: string;
27
+ processing_mode?: ProcessingMode;
28
+ model_used?: string;
29
+ enhancement_model?: string;
30
+ duration_ms?: number;
31
+ language?: string;
32
+ tags?: string[];
33
+ agent_id?: string;
34
+ project_id?: string;
35
+ session_id?: string;
36
+ metadata?: Record<string, unknown>;
37
+ }
38
+
39
+ export interface RecordingFilter {
40
+ agent_id?: string;
41
+ project_id?: string;
42
+ session_id?: string;
43
+ processing_mode?: ProcessingMode;
44
+ tags?: string[];
45
+ search?: string;
46
+ since?: string;
47
+ until?: string;
48
+ limit?: number;
49
+ offset?: number;
50
+ }
51
+
52
+ // ── Agent Types ─────────────────────────────────────────────────────────────
53
+
54
+ export interface Agent {
55
+ id: string;
56
+ name: string;
57
+ description: string | null;
58
+ role: string;
59
+ metadata: Record<string, unknown>;
60
+ created_at: string;
61
+ last_seen_at: string;
62
+ }
63
+
64
+ // ── Project Types ───────────────────────────────────────────────────────────
65
+
66
+ export interface Project {
67
+ id: string;
68
+ name: string;
69
+ path: string;
70
+ description: string | null;
71
+ created_at: string;
72
+ updated_at: string;
73
+ }
74
+
75
+ // ── Config Types ────────────────────────────────────────────────────────────
76
+
77
+ export interface RecordingsConfig {
78
+ openai_api_key: string;
79
+ enhancement_api_key: string;
80
+ transcription_model: string;
81
+ enhancement_model: string;
82
+ language: string;
83
+ audio_format: "wav" | "mp3" | "m4a" | "webm";
84
+ sample_rate: number;
85
+ record_command: string;
86
+ hotkey: string;
87
+ auto_enhance: boolean;
88
+ enhance_triggers: string[];
89
+ db_path: string;
90
+ audio_dir: string;
91
+ max_recording_seconds: number;
92
+ }
93
+
94
+ // ── Transcription Types ─────────────────────────────────────────────────────
95
+
96
+ export interface TranscriptionResult {
97
+ text: string;
98
+ duration_ms: number;
99
+ model: string;
100
+ language: string | null;
101
+ }
102
+
103
+ export interface EnhancementResult {
104
+ original: string;
105
+ enhanced: string;
106
+ model: string;
107
+ reasoning: string | null;
108
+ }
109
+
110
+ // ── Errors ──────────────────────────────────────────────────────────────────
111
+
112
+ export class RecordingNotFoundError extends Error {
113
+ constructor(id: string) {
114
+ super(`Recording not found: ${id}`);
115
+ this.name = "RecordingNotFoundError";
116
+ }
117
+ }
118
+
119
+ export class RecordingError extends Error {
120
+ constructor(message: string) {
121
+ super(message);
122
+ this.name = "RecordingError";
123
+ }
124
+ }
125
+
126
+ export class TranscriptionError extends Error {
127
+ constructor(message: string) {
128
+ super(message);
129
+ this.name = "TranscriptionError";
130
+ }
131
+ }
132
+
133
+ export class EnhancementError extends Error {
134
+ constructor(message: string) {
135
+ super(message);
136
+ this.name = "EnhancementError";
137
+ }
138
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "types": ["bun"]
18
+ },
19
+ "include": ["src/**/*.ts"],
20
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
21
+ }
package/README.md DELETED
@@ -1,145 +0,0 @@
1
- # @hasna/recordings
2
-
3
- Local AI-powered voice recording & transcription. Record audio, transcribe with OpenAI Whisper, polish transcripts, and search across everything.
4
-
5
- ## Install
6
-
7
- ```bash
8
- bun add -g @hasna/recordings
9
- ```
10
-
11
- Or run directly:
12
-
13
- ```bash
14
- bunx @hasna/recordings
15
- ```
16
-
17
- ## Quick Start
18
-
19
- 1. Set your API key:
20
-
21
- ```bash
22
- export OPENAI_API_KEY="sk-..."
23
- ```
24
-
25
- 2. Start the server:
26
-
27
- ```bash
28
- recordings serve
29
- ```
30
-
31
- 3. Open the web UI at `http://localhost:3456` or use the API directly.
32
-
33
- ## Features
34
-
35
- - Record audio from your microphone (via sox)
36
- - Transcribe recordings with OpenAI Whisper
37
- - AI-polish transcripts (remove filler words, fix grammar)
38
- - Full-text search across all transcriptions
39
- - Web UI for managing recordings
40
- - MCP server for Claude Desktop integration
41
- - REST API for programmatic access
42
- - SQLite-backed metadata storage
43
-
44
- ## CLI
45
-
46
- ```bash
47
- # Start the API server
48
- recordings serve
49
-
50
- # Start MCP server (stdio transport)
51
- recordings mcp
52
-
53
- # Print version
54
- recordings --version
55
-
56
- # Print help
57
- recordings --help
58
- ```
59
-
60
- ## MCP Server
61
-
62
- Add to your Claude Desktop config (`claude_desktop_config.json`):
63
-
64
- ```json
65
- {
66
- "mcpServers": {
67
- "recordings": {
68
- "command": "bunx",
69
- "args": ["@hasna/recordings", "mcp"]
70
- }
71
- }
72
- }
73
- ```
74
-
75
- ### Available Tools
76
-
77
- | Tool | Description |
78
- |------|-------------|
79
- | `start_recording` | Start microphone recording |
80
- | `stop_recording` | Stop the current recording |
81
- | `list_recordings` | List all recordings |
82
- | `get_recording` | Get recording details |
83
- | `delete_recording` | Delete a recording |
84
- | `transcribe_recording` | Transcribe a recording |
85
- | `transcribe_audio` | Transcribe an audio file by path |
86
- | `polish_text` | AI-polish raw text |
87
- | `search_transcriptions` | Search across transcriptions |
88
- | `get_config` | Get configuration |
89
- | `update_config` | Update configuration |
90
- | `get_status` | Get server status |
91
-
92
- ## API Endpoints
93
-
94
- | Method | Endpoint | Description |
95
- |--------|----------|-------------|
96
- | GET | `/api/status` | Server status |
97
- | GET | `/api/recordings` | List recordings |
98
- | GET | `/api/recordings/:id` | Get recording |
99
- | DELETE | `/api/recordings/:id` | Delete recording |
100
- | POST | `/api/recordings/start` | Start recording |
101
- | POST | `/api/recordings/stop` | Stop recording |
102
- | POST | `/api/recordings/:id/transcribe` | Transcribe recording |
103
- | POST | `/api/recordings/:id/polish` | Polish transcript |
104
- | POST | `/api/transcribe` | Upload & transcribe audio file |
105
- | GET | `/api/search?q=query` | Search transcriptions |
106
- | GET | `/api/config` | Get config |
107
- | PUT | `/api/config` | Update config |
108
-
109
- ## Data Storage
110
-
111
- All data is stored in `~/.recordings/`:
112
-
113
- ```
114
- ~/.recordings/
115
- ├── config.json # User configuration
116
- ├── recordings.db # SQLite database (metadata + transcripts)
117
- ├── audio/ # Recorded audio files (.wav)
118
- ├── uploads/ # Uploaded audio files
119
- └── exports/ # Exported transcripts
120
- ```
121
-
122
- ## Environment Variables
123
-
124
- | Variable | Required | Description |
125
- |----------|----------|-------------|
126
- | `OPENAI_API_KEY` | Yes | OpenAI API key for Whisper transcription |
127
-
128
- ## Prerequisites
129
-
130
- - [Bun](https://bun.sh) >= 1.3.9
131
- - [sox](https://sox.sourceforge.net/) for audio recording (`brew install sox`)
132
- - An [OpenAI API key](https://platform.openai.com/api-keys)
133
-
134
- ## Development
135
-
136
- ```bash
137
- git clone https://github.com/hasna/recordings.git
138
- cd recordings
139
- bun install
140
- bun run dev
141
- ```
142
-
143
- ## License
144
-
145
- Apache-2.0