@hasna/recordings 0.0.3

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,81 @@
1
+ import { type Database } from "bun:sqlite";
2
+ import { getDatabase, shortUuid } from "./database.js";
3
+ import type { Agent } from "../types/index.js";
4
+
5
+ function parseAgent(row: Record<string, unknown>): Agent {
6
+ return {
7
+ id: row["id"] as string,
8
+ name: row["name"] as string,
9
+ description: (row["description"] as string) || null,
10
+ role: (row["role"] as string) || "agent",
11
+ metadata: JSON.parse((row["metadata"] as string) || "{}") as Record<string, unknown>,
12
+ created_at: row["created_at"] as string,
13
+ last_seen_at: row["last_seen_at"] as string,
14
+ };
15
+ }
16
+
17
+ export function registerAgent(
18
+ name: string,
19
+ description?: string,
20
+ role?: string,
21
+ db?: Database
22
+ ): Agent {
23
+ const d = db || getDatabase();
24
+ const now = new Date().toISOString();
25
+
26
+ // Check if agent exists by name
27
+ const existing = d
28
+ .query("SELECT * FROM agents WHERE name = ?")
29
+ .get(name) as Record<string, unknown> | undefined;
30
+
31
+ if (existing) {
32
+ d.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(
33
+ now,
34
+ existing["id"] as string
35
+ );
36
+ return getAgent(existing["id"] as string, d)!;
37
+ }
38
+
39
+ const id = shortUuid();
40
+ d.query(
41
+ "INSERT INTO agents (id, name, description, role, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?, ?)"
42
+ ).run(id, name, description || null, role || "agent", now, now);
43
+
44
+ return getAgent(id, d)!;
45
+ }
46
+
47
+ export function getAgent(
48
+ idOrName: string,
49
+ db?: Database
50
+ ): Agent | null {
51
+ const d = db || getDatabase();
52
+
53
+ // Try by ID
54
+ let row = d
55
+ .query("SELECT * FROM agents WHERE id = ?")
56
+ .get(idOrName) as Record<string, unknown> | undefined;
57
+
58
+ // Try by name
59
+ if (!row) {
60
+ row = d
61
+ .query("SELECT * FROM agents WHERE name = ?")
62
+ .get(idOrName) as Record<string, unknown> | undefined;
63
+ }
64
+
65
+ // Try by partial ID prefix
66
+ if (!row) {
67
+ row = d
68
+ .query("SELECT * FROM agents WHERE id LIKE ? || '%'")
69
+ .get(idOrName) as Record<string, unknown> | undefined;
70
+ }
71
+
72
+ return row ? parseAgent(row) : null;
73
+ }
74
+
75
+ export function listAgents(db?: Database): Agent[] {
76
+ const d = db || getDatabase();
77
+ const rows = d
78
+ .query("SELECT * FROM agents ORDER BY last_seen_at DESC")
79
+ .all() as Record<string, unknown>[];
80
+ return rows.map(parseAgent);
81
+ }
@@ -0,0 +1,126 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "fs";
3
+ import { dirname } from "path";
4
+ import { loadConfig } from "../lib/config.js";
5
+
6
+ let _db: Database | null = null;
7
+
8
+ const MIGRATIONS = [
9
+ // Migration 0: Initial schema
10
+ `
11
+ CREATE TABLE IF NOT EXISTS projects (
12
+ id TEXT PRIMARY KEY,
13
+ name TEXT NOT NULL,
14
+ path TEXT UNIQUE NOT NULL,
15
+ description TEXT,
16
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
17
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS agents (
21
+ id TEXT PRIMARY KEY,
22
+ name TEXT NOT NULL UNIQUE,
23
+ description TEXT,
24
+ role TEXT DEFAULT 'agent',
25
+ metadata TEXT DEFAULT '{}',
26
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
27
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS recordings (
31
+ id TEXT PRIMARY KEY,
32
+ audio_path TEXT,
33
+ raw_text TEXT NOT NULL,
34
+ processed_text TEXT,
35
+ processing_mode TEXT NOT NULL DEFAULT 'raw' CHECK(processing_mode IN ('raw', 'enhanced')),
36
+ model_used TEXT NOT NULL DEFAULT 'gpt-4o-mini-transcribe',
37
+ enhancement_model TEXT,
38
+ duration_ms INTEGER DEFAULT 0,
39
+ language TEXT,
40
+ tags TEXT DEFAULT '[]',
41
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
42
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
43
+ session_id TEXT,
44
+ metadata TEXT DEFAULT '{}',
45
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS recording_tags (
49
+ recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
50
+ tag TEXT NOT NULL,
51
+ PRIMARY KEY (recording_id, tag)
52
+ );
53
+
54
+ CREATE TABLE IF NOT EXISTS _migrations (
55
+ id INTEGER PRIMARY KEY,
56
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_recordings_agent ON recordings(agent_id);
60
+ CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id);
61
+ CREATE INDEX IF NOT EXISTS idx_recordings_session ON recordings(session_id);
62
+ CREATE INDEX IF NOT EXISTS idx_recordings_created ON recordings(created_at);
63
+ CREATE INDEX IF NOT EXISTS idx_recordings_mode ON recordings(processing_mode);
64
+ CREATE INDEX IF NOT EXISTS idx_recording_tags_tag ON recording_tags(tag);
65
+ `,
66
+ ];
67
+
68
+ export function getDatabase(dbPath?: string): Database {
69
+ if (_db) return _db;
70
+
71
+ const path = dbPath || loadConfig().db_path;
72
+
73
+ // Ensure directory exists
74
+ const dir = dirname(path);
75
+ mkdirSync(dir, { recursive: true });
76
+
77
+ _db = new Database(path, { create: true });
78
+
79
+ // Pragmas for production SQLite
80
+ _db.run("PRAGMA journal_mode = WAL");
81
+ _db.run("PRAGMA busy_timeout = 5000");
82
+ _db.run("PRAGMA foreign_keys = ON");
83
+
84
+ runMigrations(_db);
85
+ return _db;
86
+ }
87
+
88
+ function runMigrations(db: Database): void {
89
+ // Ensure _migrations table exists
90
+ db.run(`
91
+ CREATE TABLE IF NOT EXISTS _migrations (
92
+ id INTEGER PRIMARY KEY,
93
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
94
+ )
95
+ `);
96
+
97
+ const result = db
98
+ .query("SELECT MAX(id) as max_id FROM _migrations")
99
+ .get() as { max_id: number | null } | null;
100
+
101
+ const currentLevel = result?.max_id ?? -1;
102
+
103
+ for (let i = currentLevel + 1; i < MIGRATIONS.length; i++) {
104
+ db.run(MIGRATIONS[i]!);
105
+ db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
106
+ }
107
+ }
108
+
109
+ export function closeDatabase(): void {
110
+ if (_db) {
111
+ _db.close();
112
+ _db = null;
113
+ }
114
+ }
115
+
116
+ export function resetDatabase(): void {
117
+ _db = null;
118
+ }
119
+
120
+ export function getDbPath(): string {
121
+ return loadConfig().db_path;
122
+ }
123
+
124
+ export function shortUuid(): string {
125
+ return crypto.randomUUID().slice(0, 8);
126
+ }
@@ -0,0 +1,71 @@
1
+ import { type Database } from "bun:sqlite";
2
+ import { getDatabase } from "./database.js";
3
+ import type { Project } from "../types/index.js";
4
+
5
+ function parseProject(row: Record<string, unknown>): Project {
6
+ return {
7
+ id: row["id"] as string,
8
+ name: row["name"] as string,
9
+ path: row["path"] as string,
10
+ description: (row["description"] as string) || null,
11
+ created_at: row["created_at"] as string,
12
+ updated_at: row["updated_at"] as string,
13
+ };
14
+ }
15
+
16
+ export function registerProject(
17
+ name: string,
18
+ path: string,
19
+ description?: string,
20
+ db?: Database
21
+ ): Project {
22
+ const d = db || getDatabase();
23
+ const now = new Date().toISOString();
24
+
25
+ // Check if project exists by path (idempotent)
26
+ const existing = d
27
+ .query("SELECT * FROM projects WHERE path = ?")
28
+ .get(path) as Record<string, unknown> | undefined;
29
+
30
+ if (existing) {
31
+ d.query("UPDATE projects SET updated_at = ? WHERE id = ?").run(
32
+ now,
33
+ existing["id"] as string
34
+ );
35
+ return getProject(existing["id"] as string, d)!;
36
+ }
37
+
38
+ const id = crypto.randomUUID();
39
+ d.query(
40
+ "INSERT INTO projects (id, name, path, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
41
+ ).run(id, name, path, description || null, now, now);
42
+
43
+ return getProject(id, d)!;
44
+ }
45
+
46
+ export function getProject(
47
+ idOrPath: string,
48
+ db?: Database
49
+ ): Project | null {
50
+ const d = db || getDatabase();
51
+
52
+ let row = d
53
+ .query("SELECT * FROM projects WHERE id = ?")
54
+ .get(idOrPath) as Record<string, unknown> | undefined;
55
+
56
+ if (!row) {
57
+ row = d
58
+ .query("SELECT * FROM projects WHERE path = ?")
59
+ .get(idOrPath) as Record<string, unknown> | undefined;
60
+ }
61
+
62
+ return row ? parseProject(row) : null;
63
+ }
64
+
65
+ export function listProjects(db?: Database): Project[] {
66
+ const d = db || getDatabase();
67
+ const rows = d
68
+ .query("SELECT * FROM projects ORDER BY updated_at DESC")
69
+ .all() as Record<string, unknown>[];
70
+ return rows.map(parseProject);
71
+ }
@@ -0,0 +1,219 @@
1
+ import { type Database } from "bun:sqlite";
2
+ import { getDatabase, shortUuid } from "./database.js";
3
+ import type {
4
+ Recording,
5
+ CreateRecordingInput,
6
+ RecordingFilter,
7
+ } from "../types/index.js";
8
+ import { RecordingNotFoundError } from "../types/index.js";
9
+
10
+ function parseRow(row: Record<string, unknown>): Recording {
11
+ return {
12
+ id: row["id"] as string,
13
+ audio_path: (row["audio_path"] as string) || null,
14
+ raw_text: row["raw_text"] as string,
15
+ processed_text: (row["processed_text"] as string) || null,
16
+ processing_mode: (row["processing_mode"] as Recording["processing_mode"]) || "raw",
17
+ model_used: (row["model_used"] as string) || "gpt-4o-mini-transcribe",
18
+ enhancement_model: (row["enhancement_model"] as string) || null,
19
+ duration_ms: (row["duration_ms"] as number) || 0,
20
+ language: (row["language"] as string) || null,
21
+ tags: JSON.parse((row["tags"] as string) || "[]") as string[],
22
+ agent_id: (row["agent_id"] as string) || null,
23
+ project_id: (row["project_id"] as string) || null,
24
+ session_id: (row["session_id"] as string) || null,
25
+ metadata: JSON.parse((row["metadata"] as string) || "{}") as Record<string, unknown>,
26
+ created_at: row["created_at"] as string,
27
+ };
28
+ }
29
+
30
+ export function createRecording(
31
+ input: CreateRecordingInput,
32
+ db?: Database
33
+ ): Recording {
34
+ const d = db || getDatabase();
35
+ const id = crypto.randomUUID();
36
+ const tagsJson = JSON.stringify(input.tags || []);
37
+ const metadataJson = JSON.stringify(input.metadata || {});
38
+
39
+ d.query(
40
+ `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)
41
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
42
+ ).run(
43
+ id,
44
+ input.audio_path || null,
45
+ input.raw_text,
46
+ input.processed_text || null,
47
+ input.processing_mode || "raw",
48
+ input.model_used || "gpt-4o-mini-transcribe",
49
+ input.enhancement_model || null,
50
+ input.duration_ms || 0,
51
+ input.language || null,
52
+ tagsJson,
53
+ input.agent_id || null,
54
+ input.project_id || null,
55
+ input.session_id || null,
56
+ metadataJson
57
+ );
58
+
59
+ // Insert tags into normalized table
60
+ if (input.tags && input.tags.length > 0) {
61
+ const insertTag = d.query(
62
+ "INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)"
63
+ );
64
+ for (const tag of input.tags) {
65
+ insertTag.run(id, tag);
66
+ }
67
+ }
68
+
69
+ return getRecording(id, d)!;
70
+ }
71
+
72
+ export function getRecording(
73
+ id: string,
74
+ db?: Database
75
+ ): Recording | null {
76
+ const d = db || getDatabase();
77
+
78
+ // Try by ID first
79
+ let row = d
80
+ .query("SELECT * FROM recordings WHERE id = ?")
81
+ .get(id) as Record<string, unknown> | undefined;
82
+
83
+ // Try by partial ID prefix
84
+ if (!row) {
85
+ row = d
86
+ .query("SELECT * FROM recordings WHERE id LIKE ? || '%'")
87
+ .get(id) as Record<string, unknown> | undefined;
88
+ }
89
+
90
+ return row ? parseRow(row) : null;
91
+ }
92
+
93
+ export function listRecordings(
94
+ filter?: RecordingFilter,
95
+ db?: Database
96
+ ): Recording[] {
97
+ const d = db || getDatabase();
98
+ const conditions: string[] = [];
99
+ const params: (string | number)[] = [];
100
+
101
+ if (filter?.agent_id) {
102
+ conditions.push("agent_id = ?");
103
+ params.push(filter.agent_id);
104
+ }
105
+ if (filter?.project_id) {
106
+ conditions.push("project_id = ?");
107
+ params.push(filter.project_id);
108
+ }
109
+ if (filter?.session_id) {
110
+ conditions.push("session_id = ?");
111
+ params.push(filter.session_id);
112
+ }
113
+ if (filter?.processing_mode) {
114
+ conditions.push("processing_mode = ?");
115
+ params.push(filter.processing_mode);
116
+ }
117
+ if (filter?.tags && filter.tags.length > 0) {
118
+ for (const tag of filter.tags) {
119
+ conditions.push(
120
+ "id IN (SELECT recording_id FROM recording_tags WHERE tag = ?)"
121
+ );
122
+ params.push(tag);
123
+ }
124
+ }
125
+ if (filter?.search) {
126
+ conditions.push(
127
+ "(raw_text LIKE ? OR processed_text LIKE ? OR tags LIKE ?)"
128
+ );
129
+ const q = `%${filter.search}%`;
130
+ params.push(q, q, q);
131
+ }
132
+ if (filter?.since) {
133
+ conditions.push("created_at >= ?");
134
+ params.push(filter.since);
135
+ }
136
+ if (filter?.until) {
137
+ conditions.push("created_at <= ?");
138
+ params.push(filter.until);
139
+ }
140
+
141
+ const where =
142
+ conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
143
+ const limit = filter?.limit || 50;
144
+ const offset = filter?.offset || 0;
145
+
146
+ const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
147
+ params.push(limit, offset);
148
+
149
+ const rows = d.query(sql).all(...params) as Record<string, unknown>[];
150
+ return rows.map(parseRow);
151
+ }
152
+
153
+ export function deleteRecording(
154
+ id: string,
155
+ db?: Database
156
+ ): boolean {
157
+ const d = db || getDatabase();
158
+ const result = d.query("DELETE FROM recordings WHERE id = ?").run(id);
159
+ return result.changes > 0;
160
+ }
161
+
162
+ export function searchRecordings(
163
+ query: string,
164
+ filter?: RecordingFilter,
165
+ db?: Database
166
+ ): Recording[] {
167
+ return listRecordings({ ...filter, search: query }, db);
168
+ }
169
+
170
+ export function getRecordingStats(db?: Database): {
171
+ total: number;
172
+ raw: number;
173
+ enhanced: number;
174
+ total_duration_ms: number;
175
+ by_model: Record<string, number>;
176
+ } {
177
+ const d = db || getDatabase();
178
+
179
+ const total = (
180
+ d.query("SELECT COUNT(*) as c FROM recordings").get() as { c: number }
181
+ ).c;
182
+ const raw = (
183
+ d
184
+ .query(
185
+ "SELECT COUNT(*) as c FROM recordings WHERE processing_mode = 'raw'"
186
+ )
187
+ .get() as { c: number }
188
+ ).c;
189
+ const enhanced = (
190
+ d
191
+ .query(
192
+ "SELECT COUNT(*) as c FROM recordings WHERE processing_mode = 'enhanced'"
193
+ )
194
+ .get() as { c: number }
195
+ ).c;
196
+ const totalDuration = (
197
+ d
198
+ .query("SELECT COALESCE(SUM(duration_ms), 0) as d FROM recordings")
199
+ .get() as { d: number }
200
+ ).d;
201
+
202
+ const modelRows = d
203
+ .query(
204
+ "SELECT model_used, COUNT(*) as c FROM recordings GROUP BY model_used"
205
+ )
206
+ .all() as { model_used: string; c: number }[];
207
+ const byModel: Record<string, number> = {};
208
+ for (const row of modelRows) {
209
+ byModel[row.model_used] = row.c;
210
+ }
211
+
212
+ return {
213
+ total,
214
+ raw,
215
+ enhanced,
216
+ total_duration_ms: totalDuration,
217
+ by_model: byModel,
218
+ };
219
+ }
package/src/index.ts ADDED
@@ -0,0 +1,81 @@
1
+ // ── Types ───────────────────────────────────────────────────────────────────
2
+ export type {
3
+ Recording,
4
+ CreateRecordingInput,
5
+ RecordingFilter,
6
+ ProcessingMode,
7
+ Agent,
8
+ Project,
9
+ RecordingsConfig,
10
+ TranscriptionResult,
11
+ EnhancementResult,
12
+ } from "./types/index.js";
13
+
14
+ export {
15
+ RecordingNotFoundError,
16
+ RecordingError,
17
+ TranscriptionError,
18
+ EnhancementError,
19
+ } from "./types/index.js";
20
+
21
+ // ── Database ────────────────────────────────────────────────────────────────
22
+ export {
23
+ getDatabase,
24
+ closeDatabase,
25
+ resetDatabase,
26
+ getDbPath,
27
+ shortUuid,
28
+ } from "./db/database.js";
29
+
30
+ // ── Recordings CRUD ─────────────────────────────────────────────────────────
31
+ export {
32
+ createRecording,
33
+ getRecording,
34
+ listRecordings,
35
+ deleteRecording,
36
+ searchRecordings,
37
+ getRecordingStats,
38
+ } from "./db/recordings.js";
39
+
40
+ // ── Agents ──────────────────────────────────────────────────────────────────
41
+ export { registerAgent, getAgent, listAgents } from "./db/agents.js";
42
+
43
+ // ── Projects ────────────────────────────────────────────────────────────────
44
+ export {
45
+ registerProject,
46
+ getProject,
47
+ listProjects,
48
+ } from "./db/projects.js";
49
+
50
+ // ── Config ──────────────────────────────────────────────────────────────────
51
+ export {
52
+ loadConfig,
53
+ getDataDir,
54
+ ensureDataDir,
55
+ DEFAULT_CONFIG,
56
+ } from "./lib/config.js";
57
+
58
+ // ── Transcription ───────────────────────────────────────────────────────────
59
+ export {
60
+ transcribeAudio,
61
+ transcribeBuffer,
62
+ resetClient,
63
+ } from "./lib/transcriber.js";
64
+
65
+ // ── Enhancement ─────────────────────────────────────────────────────────────
66
+ export {
67
+ needsEnhancement,
68
+ enhanceText,
69
+ processText,
70
+ resetEnhancementClient,
71
+ } from "./lib/enhancer.js";
72
+
73
+ // ── Recorder ────────────────────────────────────────────────────────────────
74
+ export {
75
+ startRecording,
76
+ stopRecording,
77
+ isRecording,
78
+ getCurrentFile,
79
+ checkRecordingDeps,
80
+ recordDuration,
81
+ } from "./lib/recorder.js";