@hasna/recordings 0.1.11 → 0.1.13

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 (69) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/index.js +395 -37
  3. package/dist/db/database.d.ts.map +1 -1
  4. package/dist/db/recordings.d.ts.map +1 -1
  5. package/dist/index.js +78 -11
  6. package/dist/lib/config.d.ts.map +1 -1
  7. package/dist/lib/enhancer.d.ts.map +1 -1
  8. package/dist/lib/recorder.d.ts.map +1 -1
  9. package/dist/lib/transcriber.d.ts +8 -2
  10. package/dist/lib/transcriber.d.ts.map +1 -1
  11. package/dist/mcp/index.js +147 -30
  12. package/dist/types/index.d.ts +3 -0
  13. package/dist/types/index.d.ts.map +1 -1
  14. package/dist/version.d.ts +2 -0
  15. package/dist/version.d.ts.map +1 -0
  16. package/package.json +20 -3
  17. package/scripts/install_macos_app.sh +76 -0
  18. package/src/native/Recordings/{Recordings → App}/RecordingsApp.swift +5 -1
  19. package/src/native/Recordings/Package.resolved +21 -3
  20. package/src/native/Recordings/Package.swift +16 -5
  21. package/src/native/Recordings/{Recordings → RecordingsLib}/Info.plist +6 -0
  22. package/src/native/Recordings/{Recordings → RecordingsLib}/MenuBarPopover.swift +55 -11
  23. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +36 -0
  24. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +164 -0
  25. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +113 -0
  26. package/src/native/Recordings/{Recordings → RecordingsLib}/ProjectStore.swift +11 -11
  27. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +383 -0
  28. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +835 -0
  29. package/src/native/Recordings/{Recordings → RecordingsLib}/SettingsView.swift +19 -5
  30. package/src/native/Recordings/{Recordings → RecordingsLib}/VoiceShortcuts.swift +12 -8
  31. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +63 -0
  32. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +23 -0
  33. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +33 -0
  34. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +92 -0
  35. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +58 -0
  36. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +113 -0
  37. package/src/native/Recordings/build.sh +6 -6
  38. package/.takumi/settings.local.json +0 -7
  39. package/bun.lock +0 -250
  40. package/bunfig.toml +0 -2
  41. package/src/__tests__/agents.test.ts +0 -136
  42. package/src/__tests__/config.test.ts +0 -252
  43. package/src/__tests__/database.test.ts +0 -167
  44. package/src/__tests__/enhancer.test.ts +0 -639
  45. package/src/__tests__/preload.ts +0 -4
  46. package/src/__tests__/projects.test.ts +0 -109
  47. package/src/__tests__/recorder.test.ts +0 -278
  48. package/src/__tests__/recordings.test.ts +0 -353
  49. package/src/__tests__/transcriber.test.ts +0 -322
  50. package/src/__tests__/types.test.ts +0 -75
  51. package/src/cli/index.ts +0 -988
  52. package/src/db/agents.ts +0 -104
  53. package/src/db/database.ts +0 -163
  54. package/src/db/pg-migrations.ts +0 -82
  55. package/src/db/projects.ts +0 -71
  56. package/src/db/recordings.ts +0 -225
  57. package/src/index.ts +0 -81
  58. package/src/lib/config.ts +0 -223
  59. package/src/lib/enhancer.ts +0 -173
  60. package/src/lib/recorder.ts +0 -198
  61. package/src/lib/transcriber.ts +0 -105
  62. package/src/mcp/index.ts +0 -464
  63. package/src/native/Recordings/Recordings/RecordingEngine.swift +0 -455
  64. package/src/native/Recordings/test_fn.swift +0 -79
  65. package/src/native/Recordings/test_fn2.swift +0 -33
  66. package/src/types/index.ts +0 -144
  67. package/tsconfig.json +0 -21
  68. /package/src/native/Recordings/{Recordings → RecordingsLib}/FnKeyMonitor.swift +0 -0
  69. /package/src/native/Recordings/{Recordings → RecordingsLib}/Recordings.entitlements +0 -0
package/src/db/agents.ts DELETED
@@ -1,104 +0,0 @@
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
- }
82
-
83
- export function heartbeatAgent(idOrName: string, db?: Database): Agent | null {
84
- const d = db || getDatabase();
85
- const agent = getAgent(idOrName, d);
86
- if (!agent) return null;
87
- d.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(
88
- new Date().toISOString(),
89
- agent.id
90
- );
91
- return getAgent(agent.id, d);
92
- }
93
-
94
- export function setAgentFocus(idOrName: string, projectId: string | null, db?: Database): Agent | null {
95
- const d = db || getDatabase();
96
- const agent = getAgent(idOrName, d);
97
- if (!agent) return null;
98
- d.query("UPDATE agents SET active_project_id = ?, last_seen_at = ? WHERE id = ?").run(
99
- projectId,
100
- new Date().toISOString(),
101
- agent.id
102
- );
103
- return getAgent(agent.id, d);
104
- }
@@ -1,163 +0,0 @@
1
- import { Database } from "bun:sqlite";
2
- import { SqliteAdapter } from "@hasna/cloud";
3
- import { mkdirSync } from "fs";
4
- import { dirname } from "path";
5
- import { loadConfig } from "../lib/config.js";
6
-
7
- let _db: Database | null = null;
8
- let _adapter: SqliteAdapter | null = null;
9
-
10
- const MIGRATIONS = [
11
- // Migration 0: Initial schema
12
- `
13
- CREATE TABLE IF NOT EXISTS projects (
14
- id TEXT PRIMARY KEY,
15
- name TEXT NOT NULL,
16
- path TEXT UNIQUE NOT NULL,
17
- description TEXT,
18
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
19
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
20
- );
21
-
22
- CREATE TABLE IF NOT EXISTS agents (
23
- id TEXT PRIMARY KEY,
24
- name TEXT NOT NULL UNIQUE,
25
- description TEXT,
26
- role TEXT DEFAULT 'agent',
27
- metadata TEXT DEFAULT '{}',
28
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
29
- last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
30
- );
31
-
32
- CREATE TABLE IF NOT EXISTS recordings (
33
- id TEXT PRIMARY KEY,
34
- audio_path TEXT,
35
- raw_text TEXT NOT NULL,
36
- processed_text TEXT,
37
- processing_mode TEXT NOT NULL DEFAULT 'raw' CHECK(processing_mode IN ('raw', 'enhanced')),
38
- model_used TEXT NOT NULL DEFAULT 'gpt-4o-mini-transcribe',
39
- enhancement_model TEXT,
40
- duration_ms INTEGER DEFAULT 0,
41
- language TEXT,
42
- tags TEXT DEFAULT '[]',
43
- agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
44
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
45
- session_id TEXT,
46
- metadata TEXT DEFAULT '{}',
47
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
48
- );
49
-
50
- CREATE TABLE IF NOT EXISTS recording_tags (
51
- recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
52
- tag TEXT NOT NULL,
53
- PRIMARY KEY (recording_id, tag)
54
- );
55
-
56
- CREATE TABLE IF NOT EXISTS _migrations (
57
- id INTEGER PRIMARY KEY,
58
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
59
- );
60
-
61
- CREATE INDEX IF NOT EXISTS idx_recordings_agent ON recordings(agent_id);
62
- CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id);
63
- CREATE INDEX IF NOT EXISTS idx_recordings_session ON recordings(session_id);
64
- CREATE INDEX IF NOT EXISTS idx_recordings_created ON recordings(created_at);
65
- CREATE INDEX IF NOT EXISTS idx_recordings_mode ON recordings(processing_mode);
66
- CREATE INDEX IF NOT EXISTS idx_recording_tags_tag ON recording_tags(tag);
67
- `,
68
-
69
- // Migration 2: session tagging attributes
70
- `
71
- ALTER TABLE recordings ADD COLUMN goal TEXT;
72
- ALTER TABLE recordings ADD COLUMN role TEXT;
73
- ALTER TABLE recordings ADD COLUMN task_list_id TEXT;
74
- INSERT OR IGNORE INTO _migrations (id) VALUES (2);
75
- `,
76
-
77
- // Migration 3: feedback table
78
- `
79
- CREATE TABLE IF NOT EXISTS feedback (
80
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
81
- message TEXT NOT NULL,
82
- email TEXT,
83
- category TEXT DEFAULT 'general',
84
- version TEXT,
85
- machine_id TEXT,
86
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
87
- );
88
- `,
89
-
90
- // Migration 4: agent focus
91
- `
92
- ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
93
- `,
94
- ];
95
-
96
- export function getDatabase(dbPath?: string): Database {
97
- if (_db) return _db;
98
-
99
- const path = dbPath || loadConfig().db_path;
100
-
101
- // Ensure directory exists
102
- const dir = dirname(path);
103
- mkdirSync(dir, { recursive: true });
104
-
105
- _adapter = new SqliteAdapter(path);
106
- _db = _adapter.raw;
107
-
108
- // SqliteAdapter already sets WAL and foreign_keys; add busy_timeout
109
- _db.run("PRAGMA busy_timeout = 5000");
110
-
111
- runMigrations(_db);
112
- return _db;
113
- }
114
-
115
- function runMigrations(db: Database): void {
116
- // Ensure _migrations table exists
117
- db.run(`
118
- CREATE TABLE IF NOT EXISTS _migrations (
119
- id INTEGER PRIMARY KEY,
120
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
121
- )
122
- `);
123
-
124
- const result = db
125
- .query("SELECT MAX(id) as max_id FROM _migrations")
126
- .get() as { max_id: number | null } | null;
127
-
128
- const currentLevel = result?.max_id ?? -1;
129
-
130
- for (let i = currentLevel + 1; i < MIGRATIONS.length; i++) {
131
- db.run(MIGRATIONS[i]!);
132
- db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
133
- }
134
- }
135
-
136
- export function closeDatabase(): void {
137
- if (_db) {
138
- _db.close();
139
- _db = null;
140
- _adapter = null;
141
- }
142
- }
143
-
144
- export function resetDatabase(): void {
145
- _db = null;
146
- _adapter = null;
147
- }
148
-
149
- /** Get the SqliteAdapter for direct SQL queries (e.g. feedback). */
150
- export function getAdapter(): SqliteAdapter {
151
- if (!_adapter) {
152
- getDatabase(); // force initialization
153
- }
154
- return _adapter!;
155
- }
156
-
157
- export function getDbPath(): string {
158
- return loadConfig().db_path;
159
- }
160
-
161
- export function shortUuid(): string {
162
- return crypto.randomUUID().slice(0, 8);
163
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * PostgreSQL migrations for open-recordings cloud sync.
3
- *
4
- * Equivalent to the SQLite schema in database.ts, translated for PostgreSQL.
5
- */
6
-
7
- export const PG_MIGRATIONS: string[] = [
8
- // Migration 0: Initial schema — projects, agents, recordings, recording_tags
9
- `CREATE TABLE IF NOT EXISTS projects (
10
- id TEXT PRIMARY KEY,
11
- name TEXT NOT NULL,
12
- path TEXT UNIQUE NOT NULL,
13
- description TEXT,
14
- created_at TEXT NOT NULL DEFAULT NOW()::text,
15
- updated_at TEXT NOT NULL DEFAULT NOW()::text
16
- )`,
17
-
18
- `CREATE TABLE IF NOT EXISTS agents (
19
- id TEXT PRIMARY KEY,
20
- name TEXT NOT NULL UNIQUE,
21
- description TEXT,
22
- role TEXT DEFAULT 'agent',
23
- metadata TEXT DEFAULT '{}',
24
- created_at TEXT NOT NULL DEFAULT NOW()::text,
25
- last_seen_at TEXT NOT NULL DEFAULT NOW()::text
26
- )`,
27
-
28
- `CREATE TABLE IF NOT EXISTS recordings (
29
- id TEXT PRIMARY KEY,
30
- audio_path TEXT,
31
- raw_text TEXT NOT NULL,
32
- processed_text TEXT,
33
- processing_mode TEXT NOT NULL DEFAULT 'raw' CHECK(processing_mode IN ('raw', 'enhanced')),
34
- model_used TEXT NOT NULL DEFAULT 'gpt-4o-mini-transcribe',
35
- enhancement_model TEXT,
36
- duration_ms INTEGER DEFAULT 0,
37
- language TEXT,
38
- tags TEXT DEFAULT '[]',
39
- agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
40
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
41
- session_id TEXT,
42
- metadata TEXT DEFAULT '{}',
43
- created_at TEXT NOT NULL DEFAULT NOW()::text
44
- )`,
45
-
46
- `CREATE TABLE IF NOT EXISTS recording_tags (
47
- recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
48
- tag TEXT NOT NULL,
49
- PRIMARY KEY (recording_id, tag)
50
- )`,
51
-
52
- `CREATE TABLE IF NOT EXISTS _migrations (
53
- id INTEGER PRIMARY KEY,
54
- applied_at TEXT NOT NULL DEFAULT NOW()::text
55
- )`,
56
-
57
- `CREATE INDEX IF NOT EXISTS idx_recordings_agent ON recordings(agent_id)`,
58
- `CREATE INDEX IF NOT EXISTS idx_recordings_project ON recordings(project_id)`,
59
- `CREATE INDEX IF NOT EXISTS idx_recordings_session ON recordings(session_id)`,
60
- `CREATE INDEX IF NOT EXISTS idx_recordings_created ON recordings(created_at)`,
61
- `CREATE INDEX IF NOT EXISTS idx_recordings_mode ON recordings(processing_mode)`,
62
- `CREATE INDEX IF NOT EXISTS idx_recording_tags_tag ON recording_tags(tag)`,
63
-
64
- // Migration 2: session tagging attributes
65
- `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS goal TEXT`,
66
- `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS role TEXT`,
67
- `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS task_list_id TEXT`,
68
-
69
- // Migration 3: feedback table
70
- `CREATE TABLE IF NOT EXISTS feedback (
71
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
72
- message TEXT NOT NULL,
73
- email TEXT,
74
- category TEXT DEFAULT 'general',
75
- version TEXT,
76
- machine_id TEXT,
77
- created_at TEXT NOT NULL DEFAULT NOW()::text
78
- )`,
79
-
80
- // Migration 4: agent focus
81
- `ALTER TABLE agents ADD COLUMN IF NOT EXISTS active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`,
82
- ];
@@ -1,71 +0,0 @@
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
- }
@@ -1,225 +0,0 @@
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
- goal: (row["goal"] as string) || null,
26
- role: (row["role"] as string) || null,
27
- task_list_id: (row["task_list_id"] as string) || null,
28
- metadata: JSON.parse((row["metadata"] as string) || "{}") as Record<string, unknown>,
29
- created_at: row["created_at"] as string,
30
- };
31
- }
32
-
33
- export function createRecording(
34
- input: CreateRecordingInput,
35
- db?: Database
36
- ): Recording {
37
- const d = db || getDatabase();
38
- const id = crypto.randomUUID();
39
- const tagsJson = JSON.stringify(input.tags || []);
40
- const metadataJson = JSON.stringify(input.metadata || {});
41
-
42
- d.query(
43
- `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, goal, role, task_list_id, metadata)
44
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
45
- ).run(
46
- id,
47
- input.audio_path || null,
48
- input.raw_text,
49
- input.processed_text || null,
50
- input.processing_mode || "raw",
51
- input.model_used || "gpt-4o-mini-transcribe",
52
- input.enhancement_model || null,
53
- input.duration_ms || 0,
54
- input.language || null,
55
- tagsJson,
56
- input.agent_id || null,
57
- input.project_id || null,
58
- input.session_id || null,
59
- input.goal || null,
60
- input.role || null,
61
- input.task_list_id || null,
62
- metadataJson
63
- );
64
-
65
- // Insert tags into normalized table
66
- if (input.tags && input.tags.length > 0) {
67
- const insertTag = d.query(
68
- "INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)"
69
- );
70
- for (const tag of input.tags) {
71
- insertTag.run(id, tag);
72
- }
73
- }
74
-
75
- return getRecording(id, d)!;
76
- }
77
-
78
- export function getRecording(
79
- id: string,
80
- db?: Database
81
- ): Recording | null {
82
- const d = db || getDatabase();
83
-
84
- // Try by ID first
85
- let row = d
86
- .query("SELECT * FROM recordings WHERE id = ?")
87
- .get(id) as Record<string, unknown> | undefined;
88
-
89
- // Try by partial ID prefix
90
- if (!row) {
91
- row = d
92
- .query("SELECT * FROM recordings WHERE id LIKE ? || '%'")
93
- .get(id) as Record<string, unknown> | undefined;
94
- }
95
-
96
- return row ? parseRow(row) : null;
97
- }
98
-
99
- export function listRecordings(
100
- filter?: RecordingFilter,
101
- db?: Database
102
- ): Recording[] {
103
- const d = db || getDatabase();
104
- const conditions: string[] = [];
105
- const params: (string | number)[] = [];
106
-
107
- if (filter?.agent_id) {
108
- conditions.push("agent_id = ?");
109
- params.push(filter.agent_id);
110
- }
111
- if (filter?.project_id) {
112
- conditions.push("project_id = ?");
113
- params.push(filter.project_id);
114
- }
115
- if (filter?.session_id) {
116
- conditions.push("session_id = ?");
117
- params.push(filter.session_id);
118
- }
119
- if (filter?.processing_mode) {
120
- conditions.push("processing_mode = ?");
121
- params.push(filter.processing_mode);
122
- }
123
- if (filter?.tags && filter.tags.length > 0) {
124
- for (const tag of filter.tags) {
125
- conditions.push(
126
- "id IN (SELECT recording_id FROM recording_tags WHERE tag = ?)"
127
- );
128
- params.push(tag);
129
- }
130
- }
131
- if (filter?.search) {
132
- conditions.push(
133
- "(raw_text LIKE ? OR processed_text LIKE ? OR tags LIKE ?)"
134
- );
135
- const q = `%${filter.search}%`;
136
- params.push(q, q, q);
137
- }
138
- if (filter?.since) {
139
- conditions.push("created_at >= ?");
140
- params.push(filter.since);
141
- }
142
- if (filter?.until) {
143
- conditions.push("created_at <= ?");
144
- params.push(filter.until);
145
- }
146
-
147
- const where =
148
- conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
149
- const limit = filter?.limit || 50;
150
- const offset = filter?.offset || 0;
151
-
152
- const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
153
- params.push(limit, offset);
154
-
155
- const rows = d.query(sql).all(...params) as Record<string, unknown>[];
156
- return rows.map(parseRow);
157
- }
158
-
159
- export function deleteRecording(
160
- id: string,
161
- db?: Database
162
- ): boolean {
163
- const d = db || getDatabase();
164
- const result = d.query("DELETE FROM recordings WHERE id = ?").run(id);
165
- return result.changes > 0;
166
- }
167
-
168
- export function searchRecordings(
169
- query: string,
170
- filter?: RecordingFilter,
171
- db?: Database
172
- ): Recording[] {
173
- return listRecordings({ ...filter, search: query }, db);
174
- }
175
-
176
- export function getRecordingStats(db?: Database): {
177
- total: number;
178
- raw: number;
179
- enhanced: number;
180
- total_duration_ms: number;
181
- by_model: Record<string, number>;
182
- } {
183
- const d = db || getDatabase();
184
-
185
- const total = (
186
- d.query("SELECT COUNT(*) as c FROM recordings").get() as { c: number }
187
- ).c;
188
- const raw = (
189
- d
190
- .query(
191
- "SELECT COUNT(*) as c FROM recordings WHERE processing_mode = 'raw'"
192
- )
193
- .get() as { c: number }
194
- ).c;
195
- const enhanced = (
196
- d
197
- .query(
198
- "SELECT COUNT(*) as c FROM recordings WHERE processing_mode = 'enhanced'"
199
- )
200
- .get() as { c: number }
201
- ).c;
202
- const totalDuration = (
203
- d
204
- .query("SELECT COALESCE(SUM(duration_ms), 0) as d FROM recordings")
205
- .get() as { d: number }
206
- ).d;
207
-
208
- const modelRows = d
209
- .query(
210
- "SELECT model_used, COUNT(*) as c FROM recordings GROUP BY model_used"
211
- )
212
- .all() as { model_used: string; c: number }[];
213
- const byModel: Record<string, number> = {};
214
- for (const row of modelRows) {
215
- byModel[row.model_used] = row.c;
216
- }
217
-
218
- return {
219
- total,
220
- raw,
221
- enhanced,
222
- total_duration_ms: totalDuration,
223
- by_model: byModel,
224
- };
225
- }