@hasna/recordings 0.0.3 → 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.
package/dist/index.js ADDED
@@ -0,0 +1,832 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/types/index.ts
5
+ class RecordingNotFoundError extends Error {
6
+ constructor(id) {
7
+ super(`Recording not found: ${id}`);
8
+ this.name = "RecordingNotFoundError";
9
+ }
10
+ }
11
+
12
+ class RecordingError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "RecordingError";
16
+ }
17
+ }
18
+
19
+ class TranscriptionError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "TranscriptionError";
23
+ }
24
+ }
25
+
26
+ class EnhancementError extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = "EnhancementError";
30
+ }
31
+ }
32
+ // src/db/database.ts
33
+ import { Database } from "bun:sqlite";
34
+ import { mkdirSync } from "fs";
35
+ import { dirname } from "path";
36
+
37
+ // src/lib/config.ts
38
+ import { existsSync, readFileSync } from "fs";
39
+ import { join } from "path";
40
+ import { homedir } from "os";
41
+ var DEFAULT_CONFIG = {
42
+ openai_api_key: "",
43
+ enhancement_api_key: "",
44
+ transcription_model: "gpt-4o-mini-transcribe",
45
+ enhancement_model: "gpt-4o",
46
+ language: "en",
47
+ audio_format: "wav",
48
+ sample_rate: 16000,
49
+ record_command: "sox",
50
+ hotkey: "space",
51
+ auto_enhance: true,
52
+ enhance_triggers: [
53
+ "say it better",
54
+ "rewrite this",
55
+ "make it sound",
56
+ "clean this up",
57
+ "fix this",
58
+ "rephrase",
59
+ "write it properly",
60
+ "make it professional",
61
+ "improve this",
62
+ "polish this"
63
+ ],
64
+ db_path: "",
65
+ audio_dir: "",
66
+ max_recording_seconds: 300
67
+ };
68
+ function loadConfig(configPath) {
69
+ const config = { ...DEFAULT_CONFIG };
70
+ const filePath = configPath || findConfigFile() || join(getDataDir(), "config.json");
71
+ if (existsSync(filePath)) {
72
+ try {
73
+ const raw = readFileSync(filePath, "utf-8");
74
+ const fileConfig = JSON.parse(raw);
75
+ Object.assign(config, fileConfig);
76
+ } catch {}
77
+ }
78
+ if (process.env.OPENAI_API_KEY) {
79
+ config.openai_api_key = process.env.OPENAI_API_KEY;
80
+ }
81
+ if (process.env.RECORDINGS_API_KEY) {
82
+ config.openai_api_key = process.env.RECORDINGS_API_KEY;
83
+ }
84
+ if (process.env.RECORDINGS_ENHANCEMENT_KEY) {
85
+ config.enhancement_api_key = process.env.RECORDINGS_ENHANCEMENT_KEY;
86
+ }
87
+ if (process.env.RECORDINGS_MODEL) {
88
+ config.transcription_model = process.env.RECORDINGS_MODEL;
89
+ }
90
+ if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
91
+ config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
92
+ }
93
+ if (process.env.RECORDINGS_LANGUAGE) {
94
+ config.language = process.env.RECORDINGS_LANGUAGE;
95
+ }
96
+ if (process.env.RECORDINGS_DB_PATH) {
97
+ config.db_path = process.env.RECORDINGS_DB_PATH;
98
+ }
99
+ if (process.env.RECORDINGS_AUDIO_DIR) {
100
+ config.audio_dir = process.env.RECORDINGS_AUDIO_DIR;
101
+ }
102
+ if (process.env.RECORDINGS_MAX_SECONDS) {
103
+ config.max_recording_seconds = parseInt(process.env.RECORDINGS_MAX_SECONDS, 10);
104
+ }
105
+ if (!config.openai_api_key) {
106
+ config.openai_api_key = loadSecretKey("OPENAI_API_KEY");
107
+ }
108
+ if (!config.enhancement_api_key) {
109
+ config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
110
+ }
111
+ if (!config.db_path) {
112
+ config.db_path = join(getDataDir(), "recordings.db");
113
+ }
114
+ if (!config.audio_dir) {
115
+ config.audio_dir = join(getDataDir(), "audio");
116
+ }
117
+ return config;
118
+ }
119
+ function findConfigFile() {
120
+ let dir = process.cwd();
121
+ const root = "/";
122
+ while (dir !== root) {
123
+ const candidate = join(dir, ".recordings", "config.json");
124
+ if (existsSync(candidate))
125
+ return candidate;
126
+ const parent = join(dir, "..");
127
+ if (parent === dir)
128
+ break;
129
+ dir = parent;
130
+ }
131
+ return null;
132
+ }
133
+ function getDataDir() {
134
+ let dir = process.cwd();
135
+ const root = "/";
136
+ while (dir !== root) {
137
+ const candidate = join(dir, ".recordings");
138
+ if (existsSync(candidate))
139
+ return candidate;
140
+ const parent = join(dir, "..");
141
+ if (parent === dir)
142
+ break;
143
+ dir = parent;
144
+ }
145
+ return join(homedir(), ".recordings");
146
+ }
147
+ function loadSecretKey(keyName) {
148
+ const secretsPath = join(homedir(), ".secrets");
149
+ if (!existsSync(secretsPath))
150
+ return "";
151
+ try {
152
+ const content = readFileSync(secretsPath, "utf-8");
153
+ const match = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`));
154
+ if (match)
155
+ return match[1];
156
+ const match2 = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`));
157
+ if (match2)
158
+ return match2[1];
159
+ const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
160
+ if (match3)
161
+ return match3[1].trim().replace(/^["']|["']$/g, "");
162
+ } catch {}
163
+ return "";
164
+ }
165
+ function ensureDataDir(config) {
166
+ const { mkdirSync } = __require("fs");
167
+ mkdirSync(config.audio_dir, { recursive: true });
168
+ const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
169
+ if (dbDir)
170
+ mkdirSync(dbDir, { recursive: true });
171
+ }
172
+
173
+ // src/db/database.ts
174
+ var _db = null;
175
+ var MIGRATIONS = [
176
+ `
177
+ CREATE TABLE IF NOT EXISTS projects (
178
+ id TEXT PRIMARY KEY,
179
+ name TEXT NOT NULL,
180
+ path TEXT UNIQUE NOT NULL,
181
+ description TEXT,
182
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
183
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
184
+ );
185
+
186
+ CREATE TABLE IF NOT EXISTS agents (
187
+ id TEXT PRIMARY KEY,
188
+ name TEXT NOT NULL UNIQUE,
189
+ description TEXT,
190
+ role TEXT DEFAULT 'agent',
191
+ metadata TEXT DEFAULT '{}',
192
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
193
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
194
+ );
195
+
196
+ CREATE TABLE IF NOT EXISTS recordings (
197
+ id TEXT PRIMARY KEY,
198
+ audio_path TEXT,
199
+ raw_text TEXT NOT NULL,
200
+ processed_text TEXT,
201
+ processing_mode TEXT NOT NULL DEFAULT 'raw' CHECK(processing_mode IN ('raw', 'enhanced')),
202
+ model_used TEXT NOT NULL DEFAULT 'gpt-4o-mini-transcribe',
203
+ enhancement_model TEXT,
204
+ duration_ms INTEGER DEFAULT 0,
205
+ language TEXT,
206
+ tags TEXT DEFAULT '[]',
207
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
208
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
209
+ session_id TEXT,
210
+ metadata TEXT DEFAULT '{}',
211
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
212
+ );
213
+
214
+ CREATE TABLE IF NOT EXISTS recording_tags (
215
+ recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
216
+ tag TEXT NOT NULL,
217
+ PRIMARY KEY (recording_id, tag)
218
+ );
219
+
220
+ CREATE TABLE IF NOT EXISTS _migrations (
221
+ id INTEGER PRIMARY KEY,
222
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
223
+ );
224
+
225
+ CREATE INDEX IF NOT EXISTS idx_recordings_agent ON recordings(agent_id);
226
+ CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id);
227
+ CREATE INDEX IF NOT EXISTS idx_recordings_session ON recordings(session_id);
228
+ CREATE INDEX IF NOT EXISTS idx_recordings_created ON recordings(created_at);
229
+ CREATE INDEX IF NOT EXISTS idx_recordings_mode ON recordings(processing_mode);
230
+ CREATE INDEX IF NOT EXISTS idx_recording_tags_tag ON recording_tags(tag);
231
+ `
232
+ ];
233
+ function getDatabase(dbPath) {
234
+ if (_db)
235
+ return _db;
236
+ const path = dbPath || loadConfig().db_path;
237
+ const dir = dirname(path);
238
+ mkdirSync(dir, { recursive: true });
239
+ _db = new Database(path, { create: true });
240
+ _db.run("PRAGMA journal_mode = WAL");
241
+ _db.run("PRAGMA busy_timeout = 5000");
242
+ _db.run("PRAGMA foreign_keys = ON");
243
+ runMigrations(_db);
244
+ return _db;
245
+ }
246
+ function runMigrations(db) {
247
+ db.run(`
248
+ CREATE TABLE IF NOT EXISTS _migrations (
249
+ id INTEGER PRIMARY KEY,
250
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
251
+ )
252
+ `);
253
+ const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
254
+ const currentLevel = result?.max_id ?? -1;
255
+ for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
256
+ db.run(MIGRATIONS[i]);
257
+ db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
258
+ }
259
+ }
260
+ function closeDatabase() {
261
+ if (_db) {
262
+ _db.close();
263
+ _db = null;
264
+ }
265
+ }
266
+ function resetDatabase() {
267
+ _db = null;
268
+ }
269
+ function getDbPath() {
270
+ return loadConfig().db_path;
271
+ }
272
+ function shortUuid() {
273
+ return crypto.randomUUID().slice(0, 8);
274
+ }
275
+ // src/db/recordings.ts
276
+ function parseRow(row) {
277
+ return {
278
+ id: row["id"],
279
+ audio_path: row["audio_path"] || null,
280
+ raw_text: row["raw_text"],
281
+ processed_text: row["processed_text"] || null,
282
+ processing_mode: row["processing_mode"] || "raw",
283
+ model_used: row["model_used"] || "gpt-4o-mini-transcribe",
284
+ enhancement_model: row["enhancement_model"] || null,
285
+ duration_ms: row["duration_ms"] || 0,
286
+ language: row["language"] || null,
287
+ tags: JSON.parse(row["tags"] || "[]"),
288
+ agent_id: row["agent_id"] || null,
289
+ project_id: row["project_id"] || null,
290
+ session_id: row["session_id"] || null,
291
+ metadata: JSON.parse(row["metadata"] || "{}"),
292
+ created_at: row["created_at"]
293
+ };
294
+ }
295
+ function createRecording(input, db) {
296
+ const d = db || getDatabase();
297
+ const id = crypto.randomUUID();
298
+ const tagsJson = JSON.stringify(input.tags || []);
299
+ const metadataJson = JSON.stringify(input.metadata || {});
300
+ d.query(`INSERT INTO recordings (id, audio_path, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, metadata)
301
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.audio_path || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-mini-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, metadataJson);
302
+ if (input.tags && input.tags.length > 0) {
303
+ const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
304
+ for (const tag of input.tags) {
305
+ insertTag.run(id, tag);
306
+ }
307
+ }
308
+ return getRecording(id, d);
309
+ }
310
+ function getRecording(id, db) {
311
+ const d = db || getDatabase();
312
+ let row = d.query("SELECT * FROM recordings WHERE id = ?").get(id);
313
+ if (!row) {
314
+ row = d.query("SELECT * FROM recordings WHERE id LIKE ? || '%'").get(id);
315
+ }
316
+ return row ? parseRow(row) : null;
317
+ }
318
+ function listRecordings(filter, db) {
319
+ const d = db || getDatabase();
320
+ const conditions = [];
321
+ const params = [];
322
+ if (filter?.agent_id) {
323
+ conditions.push("agent_id = ?");
324
+ params.push(filter.agent_id);
325
+ }
326
+ if (filter?.project_id) {
327
+ conditions.push("project_id = ?");
328
+ params.push(filter.project_id);
329
+ }
330
+ if (filter?.session_id) {
331
+ conditions.push("session_id = ?");
332
+ params.push(filter.session_id);
333
+ }
334
+ if (filter?.processing_mode) {
335
+ conditions.push("processing_mode = ?");
336
+ params.push(filter.processing_mode);
337
+ }
338
+ if (filter?.tags && filter.tags.length > 0) {
339
+ for (const tag of filter.tags) {
340
+ conditions.push("id IN (SELECT recording_id FROM recording_tags WHERE tag = ?)");
341
+ params.push(tag);
342
+ }
343
+ }
344
+ if (filter?.search) {
345
+ conditions.push("(raw_text LIKE ? OR processed_text LIKE ? OR tags LIKE ?)");
346
+ const q = `%${filter.search}%`;
347
+ params.push(q, q, q);
348
+ }
349
+ if (filter?.since) {
350
+ conditions.push("created_at >= ?");
351
+ params.push(filter.since);
352
+ }
353
+ if (filter?.until) {
354
+ conditions.push("created_at <= ?");
355
+ params.push(filter.until);
356
+ }
357
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
358
+ const limit = filter?.limit || 50;
359
+ const offset = filter?.offset || 0;
360
+ const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
361
+ params.push(limit, offset);
362
+ const rows = d.query(sql).all(...params);
363
+ return rows.map(parseRow);
364
+ }
365
+ function deleteRecording(id, db) {
366
+ const d = db || getDatabase();
367
+ const result = d.query("DELETE FROM recordings WHERE id = ?").run(id);
368
+ return result.changes > 0;
369
+ }
370
+ function searchRecordings(query, filter, db) {
371
+ return listRecordings({ ...filter, search: query }, db);
372
+ }
373
+ function getRecordingStats(db) {
374
+ const d = db || getDatabase();
375
+ const total = d.query("SELECT COUNT(*) as c FROM recordings").get().c;
376
+ const raw = d.query("SELECT COUNT(*) as c FROM recordings WHERE processing_mode = 'raw'").get().c;
377
+ const enhanced = d.query("SELECT COUNT(*) as c FROM recordings WHERE processing_mode = 'enhanced'").get().c;
378
+ const totalDuration = d.query("SELECT COALESCE(SUM(duration_ms), 0) as d FROM recordings").get().d;
379
+ const modelRows = d.query("SELECT model_used, COUNT(*) as c FROM recordings GROUP BY model_used").all();
380
+ const byModel = {};
381
+ for (const row of modelRows) {
382
+ byModel[row.model_used] = row.c;
383
+ }
384
+ return {
385
+ total,
386
+ raw,
387
+ enhanced,
388
+ total_duration_ms: totalDuration,
389
+ by_model: byModel
390
+ };
391
+ }
392
+ // src/db/agents.ts
393
+ function parseAgent(row) {
394
+ return {
395
+ id: row["id"],
396
+ name: row["name"],
397
+ description: row["description"] || null,
398
+ role: row["role"] || "agent",
399
+ metadata: JSON.parse(row["metadata"] || "{}"),
400
+ created_at: row["created_at"],
401
+ last_seen_at: row["last_seen_at"]
402
+ };
403
+ }
404
+ function registerAgent(name, description, role, db) {
405
+ const d = db || getDatabase();
406
+ const now = new Date().toISOString();
407
+ const existing = d.query("SELECT * FROM agents WHERE name = ?").get(name);
408
+ if (existing) {
409
+ d.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now, existing["id"]);
410
+ return getAgent(existing["id"], d);
411
+ }
412
+ const id = shortUuid();
413
+ d.query("INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)").run(id, name, description || null, role || "agent", now, now);
414
+ return getAgent(id, d);
415
+ }
416
+ function getAgent(idOrName, db) {
417
+ const d = db || getDatabase();
418
+ let row = d.query("SELECT * FROM agents WHERE id = ?").get(idOrName);
419
+ if (!row) {
420
+ row = d.query("SELECT * FROM agents WHERE name = ?").get(idOrName);
421
+ }
422
+ if (!row) {
423
+ row = d.query("SELECT * FROM agents WHERE id LIKE ? || '%'").get(idOrName);
424
+ }
425
+ return row ? parseAgent(row) : null;
426
+ }
427
+ function listAgents(db) {
428
+ const d = db || getDatabase();
429
+ const rows = d.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all();
430
+ return rows.map(parseAgent);
431
+ }
432
+ // src/db/projects.ts
433
+ function parseProject(row) {
434
+ return {
435
+ id: row["id"],
436
+ name: row["name"],
437
+ path: row["path"],
438
+ description: row["description"] || null,
439
+ created_at: row["created_at"],
440
+ updated_at: row["updated_at"]
441
+ };
442
+ }
443
+ function registerProject(name, path, description, db) {
444
+ const d = db || getDatabase();
445
+ const now = new Date().toISOString();
446
+ const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
447
+ if (existing) {
448
+ d.query("UPDATE projects SET updated_at = ? WHERE id = ?").run(now, existing["id"]);
449
+ return getProject(existing["id"], d);
450
+ }
451
+ const id = crypto.randomUUID();
452
+ d.query("INSERT INTO projects (id, name, path, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(id, name, path, description || null, now, now);
453
+ return getProject(id, d);
454
+ }
455
+ function getProject(idOrPath, db) {
456
+ const d = db || getDatabase();
457
+ let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
458
+ if (!row) {
459
+ row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
460
+ }
461
+ return row ? parseProject(row) : null;
462
+ }
463
+ function listProjects(db) {
464
+ const d = db || getDatabase();
465
+ const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
466
+ return rows.map(parseProject);
467
+ }
468
+ // src/lib/transcriber.ts
469
+ import OpenAI from "openai";
470
+ import { createReadStream } from "fs";
471
+ var _client = null;
472
+ function getClient(config) {
473
+ if (_client)
474
+ return _client;
475
+ if (!config.openai_api_key) {
476
+ throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
477
+ }
478
+ _client = new OpenAI({ apiKey: config.openai_api_key });
479
+ return _client;
480
+ }
481
+ function resetClient() {
482
+ _client = null;
483
+ }
484
+ async function transcribeAudio(audioPath, config) {
485
+ const client = getClient(config);
486
+ const startTime = Date.now();
487
+ try {
488
+ const transcription = await client.audio.transcriptions.create({
489
+ file: createReadStream(audioPath),
490
+ model: config.transcription_model,
491
+ language: config.language || undefined,
492
+ response_format: "json"
493
+ });
494
+ const durationMs = Date.now() - startTime;
495
+ return {
496
+ text: transcription.text,
497
+ duration_ms: durationMs,
498
+ model: config.transcription_model,
499
+ language: transcription.language
500
+ };
501
+ } catch (error) {
502
+ const msg = error instanceof Error ? error.message : String(error);
503
+ throw new TranscriptionError(`Transcription failed: ${msg}`);
504
+ }
505
+ }
506
+ async function transcribeBuffer(buffer, filename, config) {
507
+ const client = getClient(config);
508
+ const startTime = Date.now();
509
+ try {
510
+ const file = new File([new Uint8Array(buffer)], filename, {
511
+ type: getMimeType(filename)
512
+ });
513
+ const transcription = await client.audio.transcriptions.create({
514
+ file,
515
+ model: config.transcription_model,
516
+ language: config.language || undefined,
517
+ response_format: "json"
518
+ });
519
+ const durationMs = Date.now() - startTime;
520
+ return {
521
+ text: transcription.text,
522
+ duration_ms: durationMs,
523
+ model: config.transcription_model,
524
+ language: transcription.language
525
+ };
526
+ } catch (error) {
527
+ const msg = error instanceof Error ? error.message : String(error);
528
+ throw new TranscriptionError(`Transcription failed: ${msg}`);
529
+ }
530
+ }
531
+ function getMimeType(filename) {
532
+ const ext = filename.split(".").pop()?.toLowerCase();
533
+ switch (ext) {
534
+ case "wav":
535
+ return "audio/wav";
536
+ case "mp3":
537
+ return "audio/mpeg";
538
+ case "m4a":
539
+ return "audio/mp4";
540
+ case "webm":
541
+ return "audio/webm";
542
+ case "mp4":
543
+ return "audio/mp4";
544
+ case "mpeg":
545
+ case "mpga":
546
+ return "audio/mpeg";
547
+ default:
548
+ return "audio/wav";
549
+ }
550
+ }
551
+ // src/lib/enhancer.ts
552
+ import OpenAI2 from "openai";
553
+ var _enhancementClient = null;
554
+ function getEnhancementClient(config) {
555
+ if (_enhancementClient)
556
+ return _enhancementClient;
557
+ const key = config.enhancement_api_key || config.openai_api_key;
558
+ if (!key) {
559
+ throw new EnhancementError("API key not configured for enhancement. Set OPENAI_API_KEY or RECORDINGS_ENHANCEMENT_KEY");
560
+ }
561
+ _enhancementClient = new OpenAI2({ apiKey: key });
562
+ return _enhancementClient;
563
+ }
564
+ function resetEnhancementClient() {
565
+ _enhancementClient = null;
566
+ }
567
+ function needsEnhancement(text, config) {
568
+ const lower = text.toLowerCase().trim();
569
+ for (const trigger of config.enhance_triggers) {
570
+ if (lower.includes(trigger.toLowerCase())) {
571
+ return {
572
+ needs: true,
573
+ reason: `Explicit trigger: "${trigger}"`,
574
+ instruction: extractInstruction(text, trigger)
575
+ };
576
+ }
577
+ }
578
+ const instructionPatterns = [
579
+ /(?:write|draft|compose|create)\s+(?:an?\s+)?(?:email|message|response|reply|letter|note|text|slack|dm)/i,
580
+ /(?:give|provide|send)\s+(?:them|him|her|it|the\s+agent|the\s+team)\s+(?:full\s+)?instructions/i,
581
+ /(?:tell|ask)\s+(?:them|him|her|it|the\s+agent)\s+(?:to|that)/i,
582
+ /(?:make\s+it|make\s+this)\s+(?:sound|look|read)\s+(?:more\s+)?(?:professional|formal|casual|friendly|better)/i,
583
+ /(?:ok\s+so|okay\s+so|alright\s+so)\s+(?:say|write|tell|put)/i,
584
+ /(?:i\s+need|i\s+want)\s+(?:the\s+agent|it|them|you)\s+to\s+(?:build|create|implement|design|make)/i
585
+ ];
586
+ for (const pattern of instructionPatterns) {
587
+ if (pattern.test(text)) {
588
+ return {
589
+ needs: true,
590
+ reason: `Instruction pattern detected`,
591
+ instruction: text
592
+ };
593
+ }
594
+ }
595
+ return { needs: false, reason: "Direct dictation", instruction: text };
596
+ }
597
+ function extractInstruction(text, trigger) {
598
+ const lower = text.toLowerCase();
599
+ const idx = lower.indexOf(trigger.toLowerCase());
600
+ if (idx === -1)
601
+ return text;
602
+ const after = text.substring(idx + trigger.length).trim();
603
+ const before = text.substring(0, idx).trim();
604
+ if (before.length > after.length && before.length > 10) {
605
+ return before;
606
+ }
607
+ return text;
608
+ }
609
+ async function enhanceText(rawText, instruction, config) {
610
+ const client = getEnhancementClient(config);
611
+ try {
612
+ const response = await client.chat.completions.create({
613
+ model: config.enhancement_model,
614
+ messages: [
615
+ {
616
+ role: "system",
617
+ content: `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
618
+
619
+ Rules:
620
+ - Output ONLY the enhanced/rewritten text \u2014 no explanations, no preamble
621
+ - Preserve the user's intent and meaning
622
+ - Fix grammar, structure, and clarity
623
+ - If the user is giving instructions (e.g., "write an email saying..."), produce the actual output (the email), not a description of it
624
+ - If the user says "say it better" or similar, rewrite their preceding text to be clearer and more professional
625
+ - Match the appropriate tone (formal for business, casual for personal)`
626
+ },
627
+ {
628
+ role: "user",
629
+ content: instruction
630
+ }
631
+ ],
632
+ temperature: 0.3,
633
+ max_tokens: 4096
634
+ });
635
+ const enhanced = response.choices[0]?.message?.content?.trim() || rawText;
636
+ return {
637
+ original: rawText,
638
+ enhanced,
639
+ model: config.enhancement_model,
640
+ reasoning: null
641
+ };
642
+ } catch (error) {
643
+ const msg = error instanceof Error ? error.message : String(error);
644
+ throw new EnhancementError(`Enhancement failed: ${msg}`);
645
+ }
646
+ }
647
+ async function processText(rawText, config) {
648
+ if (!config.auto_enhance) {
649
+ return { text: rawText, mode: "raw", enhancement_model: null };
650
+ }
651
+ const detection = needsEnhancement(rawText, config);
652
+ if (!detection.needs) {
653
+ return { text: rawText, mode: "raw", enhancement_model: null };
654
+ }
655
+ const result = await enhanceText(rawText, detection.instruction, config);
656
+ return {
657
+ text: result.enhanced,
658
+ mode: "enhanced",
659
+ enhancement_model: result.model
660
+ };
661
+ }
662
+ // src/lib/recorder.ts
663
+ import { spawn } from "child_process";
664
+ import { join as join2 } from "path";
665
+ import { existsSync as existsSync2 } from "fs";
666
+ var _recordProcess = null;
667
+ var _currentFile = null;
668
+ async function checkRecordingDeps() {
669
+ try {
670
+ const proc = Bun.spawn(["which", "sox"], {
671
+ stdout: "pipe",
672
+ stderr: "pipe"
673
+ });
674
+ await proc.exited;
675
+ if (proc.exitCode === 0) {
676
+ return { available: true, tool: "sox", message: "sox is available" };
677
+ }
678
+ } catch {}
679
+ try {
680
+ const proc = Bun.spawn(["which", "rec"], {
681
+ stdout: "pipe",
682
+ stderr: "pipe"
683
+ });
684
+ await proc.exited;
685
+ if (proc.exitCode === 0) {
686
+ return { available: true, tool: "rec", message: "rec is available" };
687
+ }
688
+ } catch {}
689
+ try {
690
+ const proc = Bun.spawn(["which", "ffmpeg"], {
691
+ stdout: "pipe",
692
+ stderr: "pipe"
693
+ });
694
+ await proc.exited;
695
+ if (proc.exitCode === 0) {
696
+ return {
697
+ available: true,
698
+ tool: "ffmpeg",
699
+ message: "ffmpeg is available"
700
+ };
701
+ }
702
+ } catch {}
703
+ return {
704
+ available: false,
705
+ tool: "none",
706
+ message: "No recording tool found. Install sox: brew install sox (macOS) or apt install sox (Linux)"
707
+ };
708
+ }
709
+ function startRecording(config) {
710
+ if (_recordProcess) {
711
+ throw new RecordingError("Already recording. Stop the current recording first.");
712
+ }
713
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
714
+ const filename = `recording-${timestamp}.${config.audio_format}`;
715
+ const filepath = join2(config.audio_dir, filename);
716
+ const args = buildRecordArgs(filepath, config);
717
+ _recordProcess = spawn(args[0], args.slice(1), {
718
+ stdio: ["pipe", "pipe", "pipe"]
719
+ });
720
+ _currentFile = filepath;
721
+ _recordProcess.on("error", (err) => {
722
+ _recordProcess = null;
723
+ _currentFile = null;
724
+ throw new RecordingError(`Recording process error: ${err.message}`);
725
+ });
726
+ _recordProcess.on("exit", () => {
727
+ _recordProcess = null;
728
+ });
729
+ return filepath;
730
+ }
731
+ function stopRecording() {
732
+ if (!_recordProcess) {
733
+ return null;
734
+ }
735
+ const filepath = _currentFile;
736
+ _recordProcess.kill("SIGINT");
737
+ _recordProcess = null;
738
+ _currentFile = null;
739
+ return filepath;
740
+ }
741
+ function isRecording() {
742
+ return _recordProcess !== null;
743
+ }
744
+ function getCurrentFile() {
745
+ return _currentFile;
746
+ }
747
+ function buildRecordArgs(filepath, config) {
748
+ const format = config.audio_format;
749
+ const rate = config.sample_rate;
750
+ const maxSeconds = config.max_recording_seconds;
751
+ return [
752
+ "rec",
753
+ "-r",
754
+ rate.toString(),
755
+ "-c",
756
+ "1",
757
+ "-b",
758
+ "16",
759
+ filepath,
760
+ "trim",
761
+ "0",
762
+ maxSeconds.toString()
763
+ ];
764
+ }
765
+ async function recordDuration(seconds, config) {
766
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
767
+ const filename = `recording-${timestamp}.${config.audio_format}`;
768
+ const filepath = join2(config.audio_dir, filename);
769
+ const args = [
770
+ "rec",
771
+ "-r",
772
+ config.sample_rate.toString(),
773
+ "-c",
774
+ "1",
775
+ "-b",
776
+ "16",
777
+ filepath,
778
+ "trim",
779
+ "0",
780
+ seconds.toString()
781
+ ];
782
+ const proc = Bun.spawn(args, {
783
+ stdout: "pipe",
784
+ stderr: "pipe"
785
+ });
786
+ const exitCode = await proc.exited;
787
+ if (exitCode !== 0 && !existsSync2(filepath)) {
788
+ const stderr = await new Response(proc.stderr).text();
789
+ throw new RecordingError(`Recording failed (exit ${exitCode}): ${stderr}`);
790
+ }
791
+ return filepath;
792
+ }
793
+ export {
794
+ transcribeBuffer,
795
+ transcribeAudio,
796
+ stopRecording,
797
+ startRecording,
798
+ shortUuid,
799
+ searchRecordings,
800
+ resetEnhancementClient,
801
+ resetDatabase,
802
+ resetClient,
803
+ registerProject,
804
+ registerAgent,
805
+ recordDuration,
806
+ processText,
807
+ needsEnhancement,
808
+ loadConfig,
809
+ listRecordings,
810
+ listProjects,
811
+ listAgents,
812
+ isRecording,
813
+ getRecordingStats,
814
+ getRecording,
815
+ getProject,
816
+ getDbPath,
817
+ getDatabase,
818
+ getDataDir,
819
+ getCurrentFile,
820
+ getAgent,
821
+ ensureDataDir,
822
+ enhanceText,
823
+ deleteRecording,
824
+ createRecording,
825
+ closeDatabase,
826
+ checkRecordingDeps,
827
+ TranscriptionError,
828
+ RecordingNotFoundError,
829
+ RecordingError,
830
+ EnhancementError,
831
+ DEFAULT_CONFIG
832
+ };