@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.
- package/LICENSE +21 -191
- package/bun.lock +219 -0
- package/bunfig.toml +2 -0
- package/dist/__tests__/preload.d.ts +5 -0
- package/dist/__tests__/preload.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1446 -0
- package/dist/db/agents.d.ts +6 -0
- package/dist/db/agents.d.ts.map +1 -0
- package/dist/db/database.d.ts +7 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/projects.d.ts +6 -0
- package/dist/db/projects.d.ts.map +1 -0
- package/dist/db/recordings.d.ts +15 -0
- package/dist/db/recordings.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +769 -30752
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/enhancer.d.ts +25 -0
- package/dist/lib/enhancer.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +30 -0
- package/dist/lib/recorder.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts +5 -0
- package/dist/lib/transcriber.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +4912 -0
- package/dist/types/index.d.ts +103 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +35 -44
- package/src/__tests__/agents.test.ts +136 -0
- package/src/__tests__/config.test.ts +252 -0
- package/src/__tests__/database.test.ts +167 -0
- package/src/__tests__/enhancer.test.ts +574 -0
- package/src/__tests__/preload.ts +4 -0
- package/src/__tests__/projects.test.ts +109 -0
- package/src/__tests__/recorder.test.ts +278 -0
- package/src/__tests__/recordings.test.ts +353 -0
- package/src/__tests__/transcriber.test.ts +322 -0
- package/src/__tests__/types.test.ts +75 -0
- package/src/cli/index.ts +1078 -0
- package/src/db/agents.ts +81 -0
- package/src/db/database.ts +126 -0
- package/src/db/projects.ts +71 -0
- package/src/db/recordings.ts +219 -0
- package/src/index.ts +81 -0
- package/src/lib/config.ts +166 -0
- package/src/lib/enhancer.ts +167 -0
- package/src/lib/recorder.ts +198 -0
- package/src/lib/transcriber.ts +105 -0
- package/src/mcp/index.ts +446 -0
- package/src/native/RecordingsHelper.swift +352 -0
- package/src/types/index.ts +138 -0
- package/tsconfig.json +21 -0
- package/README.md +0 -145
- package/dist/index.js.map +0 -272
- package/scripts/postinstall.ts +0 -80
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface Recording {
|
|
2
|
+
id: string;
|
|
3
|
+
audio_path: string | null;
|
|
4
|
+
raw_text: string;
|
|
5
|
+
processed_text: string | null;
|
|
6
|
+
processing_mode: ProcessingMode;
|
|
7
|
+
model_used: string;
|
|
8
|
+
enhancement_model: string | null;
|
|
9
|
+
duration_ms: number;
|
|
10
|
+
language: string | null;
|
|
11
|
+
tags: string[];
|
|
12
|
+
agent_id: string | null;
|
|
13
|
+
project_id: string | null;
|
|
14
|
+
session_id: string | null;
|
|
15
|
+
metadata: Record<string, unknown>;
|
|
16
|
+
created_at: string;
|
|
17
|
+
}
|
|
18
|
+
export type ProcessingMode = "raw" | "enhanced";
|
|
19
|
+
export interface CreateRecordingInput {
|
|
20
|
+
audio_path?: string;
|
|
21
|
+
raw_text: string;
|
|
22
|
+
processed_text?: string;
|
|
23
|
+
processing_mode?: ProcessingMode;
|
|
24
|
+
model_used?: string;
|
|
25
|
+
enhancement_model?: string;
|
|
26
|
+
duration_ms?: number;
|
|
27
|
+
language?: string;
|
|
28
|
+
tags?: string[];
|
|
29
|
+
agent_id?: string;
|
|
30
|
+
project_id?: string;
|
|
31
|
+
session_id?: string;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
export interface RecordingFilter {
|
|
35
|
+
agent_id?: string;
|
|
36
|
+
project_id?: string;
|
|
37
|
+
session_id?: string;
|
|
38
|
+
processing_mode?: ProcessingMode;
|
|
39
|
+
tags?: string[];
|
|
40
|
+
search?: string;
|
|
41
|
+
since?: string;
|
|
42
|
+
until?: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
offset?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface Agent {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string | null;
|
|
50
|
+
role: string;
|
|
51
|
+
metadata: Record<string, unknown>;
|
|
52
|
+
created_at: string;
|
|
53
|
+
last_seen_at: string;
|
|
54
|
+
}
|
|
55
|
+
export interface Project {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
path: string;
|
|
59
|
+
description: string | null;
|
|
60
|
+
created_at: string;
|
|
61
|
+
updated_at: string;
|
|
62
|
+
}
|
|
63
|
+
export interface RecordingsConfig {
|
|
64
|
+
openai_api_key: string;
|
|
65
|
+
enhancement_api_key: string;
|
|
66
|
+
transcription_model: string;
|
|
67
|
+
enhancement_model: string;
|
|
68
|
+
language: string;
|
|
69
|
+
audio_format: "wav" | "mp3" | "m4a" | "webm";
|
|
70
|
+
sample_rate: number;
|
|
71
|
+
record_command: string;
|
|
72
|
+
hotkey: string;
|
|
73
|
+
auto_enhance: boolean;
|
|
74
|
+
enhance_triggers: string[];
|
|
75
|
+
db_path: string;
|
|
76
|
+
audio_dir: string;
|
|
77
|
+
max_recording_seconds: number;
|
|
78
|
+
}
|
|
79
|
+
export interface TranscriptionResult {
|
|
80
|
+
text: string;
|
|
81
|
+
duration_ms: number;
|
|
82
|
+
model: string;
|
|
83
|
+
language: string | null;
|
|
84
|
+
}
|
|
85
|
+
export interface EnhancementResult {
|
|
86
|
+
original: string;
|
|
87
|
+
enhanced: string;
|
|
88
|
+
model: string;
|
|
89
|
+
reasoning: string | null;
|
|
90
|
+
}
|
|
91
|
+
export declare class RecordingNotFoundError extends Error {
|
|
92
|
+
constructor(id: string);
|
|
93
|
+
}
|
|
94
|
+
export declare class RecordingError extends Error {
|
|
95
|
+
constructor(message: string);
|
|
96
|
+
}
|
|
97
|
+
export declare class TranscriptionError extends Error {
|
|
98
|
+
constructor(message: string);
|
|
99
|
+
}
|
|
100
|
+
export declare class EnhancementError extends Error {
|
|
101
|
+
constructor(message: string);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,55 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/recordings",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Local AI-powered voice recording & transcription — record, transcribe, polish, search",
|
|
3
|
+
"version": "0.1.2",
|
|
5
4
|
"type": "module",
|
|
5
|
+
"description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
6
8
|
"bin": {
|
|
7
|
-
"recordings": "dist/index.js"
|
|
9
|
+
"recordings": "dist/cli/index.js",
|
|
10
|
+
"recordings-mcp": "dist/mcp/index.js"
|
|
8
11
|
},
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"postinstall": "bun run scripts/postinstall.ts",
|
|
17
|
-
"dev": "bun run dev:api & bun run dev:web",
|
|
18
|
-
"dev:api": "bun run packages/api-server/src/index.ts",
|
|
19
|
-
"dev:web": "cd apps/web && bun run dev",
|
|
20
|
-
"mcp": "bun run packages/mcp-server/src/index.ts",
|
|
21
|
-
"build": "bun run build.ts",
|
|
22
|
-
"prepublishOnly": "bun run build"
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
23
17
|
},
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bun run build:cli && bun run build:mcp && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
|
|
20
|
+
"build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
|
|
21
|
+
"build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
|
|
22
|
+
"build:lib": "bun build src/index.ts --target=bun --outfile=dist/index.js --external=openai",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"test:coverage": "bun test --coverage",
|
|
26
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
27
|
+
"dev:mcp": "bun run src/mcp/index.ts"
|
|
27
28
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
31
|
+
"chalk": "^5.4.1",
|
|
32
|
+
"commander": "^13.1.0",
|
|
33
|
+
"openai": "^5.1.0",
|
|
34
|
+
"zod": "^3.24.2"
|
|
30
35
|
},
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"recordings",
|
|
35
|
-
"transcription",
|
|
36
|
-
"voice",
|
|
37
|
-
"openai",
|
|
38
|
-
"whisper",
|
|
39
|
-
"mcp",
|
|
40
|
-
"cli",
|
|
41
|
-
"bun"
|
|
42
|
-
],
|
|
43
|
-
"repository": {
|
|
44
|
-
"type": "git",
|
|
45
|
-
"url": "git+https://github.com/hasna/recordings.git"
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "^1.2.5",
|
|
38
|
+
"typescript": "^5.8.2"
|
|
46
39
|
},
|
|
47
|
-
"
|
|
48
|
-
"
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "restricted",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
49
43
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"apps/*",
|
|
53
|
-
"packages/*"
|
|
54
|
-
]
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "Hasna <andrei@hasna.com>"
|
|
55
46
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { mkdirSync, rmSync, existsSync } from "fs";
|
|
5
|
+
import {
|
|
6
|
+
getDatabase,
|
|
7
|
+
closeDatabase,
|
|
8
|
+
resetDatabase,
|
|
9
|
+
} from "../db/database.js";
|
|
10
|
+
import { registerAgent, getAgent, listAgents } from "../db/agents.js";
|
|
11
|
+
import { type Database } from "bun:sqlite";
|
|
12
|
+
|
|
13
|
+
let tempDir: string;
|
|
14
|
+
let db: Database;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
resetDatabase();
|
|
18
|
+
tempDir = join(tmpdir(), `open-recordings-test-agents-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
19
|
+
mkdirSync(tempDir, { recursive: true });
|
|
20
|
+
const dbPath = join(tempDir, "test.db");
|
|
21
|
+
db = getDatabase(dbPath);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
closeDatabase();
|
|
26
|
+
resetDatabase();
|
|
27
|
+
if (existsSync(tempDir)) {
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("registerAgent", () => {
|
|
33
|
+
test("creates a new agent with name only", () => {
|
|
34
|
+
const agent = registerAgent("maximus", undefined, undefined, db);
|
|
35
|
+
expect(agent).toBeDefined();
|
|
36
|
+
expect(agent.name).toBe("maximus");
|
|
37
|
+
expect(agent.role).toBe("agent");
|
|
38
|
+
expect(agent.description).toBeNull();
|
|
39
|
+
expect(agent.metadata).toEqual({});
|
|
40
|
+
expect(agent.id).toHaveLength(8);
|
|
41
|
+
expect(agent.created_at).toBeDefined();
|
|
42
|
+
expect(agent.last_seen_at).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("creates a new agent with description and role", () => {
|
|
46
|
+
const agent = registerAgent("cassius", "A helper agent", "assistant", db);
|
|
47
|
+
expect(agent.name).toBe("cassius");
|
|
48
|
+
expect(agent.description).toBe("A helper agent");
|
|
49
|
+
expect(agent.role).toBe("assistant");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns existing agent and updates last_seen_at (idempotent)", () => {
|
|
53
|
+
const first = registerAgent("aurelius", undefined, undefined, db);
|
|
54
|
+
// Wait a tiny bit to ensure timestamp difference
|
|
55
|
+
const second = registerAgent("aurelius", undefined, undefined, db);
|
|
56
|
+
|
|
57
|
+
expect(second.id).toBe(first.id);
|
|
58
|
+
expect(second.name).toBe("aurelius");
|
|
59
|
+
// last_seen_at should be updated
|
|
60
|
+
expect(new Date(second.last_seen_at).getTime()).toBeGreaterThanOrEqual(
|
|
61
|
+
new Date(first.last_seen_at).getTime()
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("does not duplicate agents on re-register", () => {
|
|
66
|
+
registerAgent("brutus", undefined, undefined, db);
|
|
67
|
+
registerAgent("brutus", undefined, undefined, db);
|
|
68
|
+
registerAgent("brutus", undefined, undefined, db);
|
|
69
|
+
|
|
70
|
+
const agents = listAgents(db);
|
|
71
|
+
const brutusAgents = agents.filter((a) => a.name === "brutus");
|
|
72
|
+
expect(brutusAgents).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("getAgent", () => {
|
|
77
|
+
test("finds agent by ID", () => {
|
|
78
|
+
const created = registerAgent("titus", undefined, undefined, db);
|
|
79
|
+
const found = getAgent(created.id, db);
|
|
80
|
+
expect(found).toBeDefined();
|
|
81
|
+
expect(found!.name).toBe("titus");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("finds agent by name", () => {
|
|
85
|
+
registerAgent("nero", "The emperor", undefined, db);
|
|
86
|
+
const found = getAgent("nero", db);
|
|
87
|
+
expect(found).toBeDefined();
|
|
88
|
+
expect(found!.description).toBe("The emperor");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("finds agent by partial ID prefix", () => {
|
|
92
|
+
const created = registerAgent("cicero", undefined, undefined, db);
|
|
93
|
+
const prefix = created.id.substring(0, 4);
|
|
94
|
+
const found = getAgent(prefix, db);
|
|
95
|
+
expect(found).toBeDefined();
|
|
96
|
+
expect(found!.name).toBe("cicero");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("returns null for non-existent agent", () => {
|
|
100
|
+
const found = getAgent("nonexistent", db);
|
|
101
|
+
expect(found).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("listAgents", () => {
|
|
106
|
+
test("returns empty array when no agents", () => {
|
|
107
|
+
const agents = listAgents(db);
|
|
108
|
+
expect(agents).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("returns all agents ordered by last_seen_at DESC", () => {
|
|
112
|
+
registerAgent("alpha", undefined, undefined, db);
|
|
113
|
+
registerAgent("beta", undefined, undefined, db);
|
|
114
|
+
registerAgent("gamma", undefined, undefined, db);
|
|
115
|
+
|
|
116
|
+
const agents = listAgents(db);
|
|
117
|
+
expect(agents).toHaveLength(3);
|
|
118
|
+
// All three agents are present
|
|
119
|
+
const names = agents.map((a) => a.name);
|
|
120
|
+
expect(names).toContain("alpha");
|
|
121
|
+
expect(names).toContain("beta");
|
|
122
|
+
expect(names).toContain("gamma");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("parses metadata JSON correctly", () => {
|
|
126
|
+
const agent = registerAgent("delta", undefined, undefined, db);
|
|
127
|
+
// Manually update metadata for testing
|
|
128
|
+
db.query("UPDATE agents SET metadata = ? WHERE id = ?").run(
|
|
129
|
+
JSON.stringify({ tool: "test" }),
|
|
130
|
+
agent.id
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const found = getAgent(agent.id, db);
|
|
134
|
+
expect(found!.metadata).toEqual({ tool: "test" });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
|
|
5
|
+
import { loadConfig, getDataDir, ensureDataDir, DEFAULT_CONFIG } from "../lib/config.js";
|
|
6
|
+
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
// Save and restore env vars
|
|
10
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
11
|
+
const envKeys = [
|
|
12
|
+
"OPENAI_API_KEY",
|
|
13
|
+
"RECORDINGS_API_KEY",
|
|
14
|
+
"RECORDINGS_ENHANCEMENT_KEY",
|
|
15
|
+
"RECORDINGS_MODEL",
|
|
16
|
+
"RECORDINGS_ENHANCEMENT_MODEL",
|
|
17
|
+
"RECORDINGS_LANGUAGE",
|
|
18
|
+
"RECORDINGS_DB_PATH",
|
|
19
|
+
"RECORDINGS_AUDIO_DIR",
|
|
20
|
+
"RECORDINGS_MAX_SECONDS",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tempDir = join(tmpdir(), `open-recordings-test-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
25
|
+
mkdirSync(tempDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
// Save env vars
|
|
28
|
+
for (const key of envKeys) {
|
|
29
|
+
savedEnv[key] = process.env[key];
|
|
30
|
+
delete process.env[key];
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
// Restore env vars
|
|
36
|
+
for (const key of envKeys) {
|
|
37
|
+
if (savedEnv[key] !== undefined) {
|
|
38
|
+
process.env[key] = savedEnv[key];
|
|
39
|
+
} else {
|
|
40
|
+
delete process.env[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (existsSync(tempDir)) {
|
|
45
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("DEFAULT_CONFIG", () => {
|
|
50
|
+
test("has expected default values", () => {
|
|
51
|
+
expect(DEFAULT_CONFIG.transcription_model).toBe("gpt-4o-mini-transcribe");
|
|
52
|
+
expect(DEFAULT_CONFIG.enhancement_model).toBe("gpt-4o");
|
|
53
|
+
expect(DEFAULT_CONFIG.language).toBe("en");
|
|
54
|
+
expect(DEFAULT_CONFIG.audio_format).toBe("wav");
|
|
55
|
+
expect(DEFAULT_CONFIG.sample_rate).toBe(16000);
|
|
56
|
+
expect(DEFAULT_CONFIG.record_command).toBe("sox");
|
|
57
|
+
expect(DEFAULT_CONFIG.hotkey).toBe("space");
|
|
58
|
+
expect(DEFAULT_CONFIG.auto_enhance).toBe(true);
|
|
59
|
+
expect(DEFAULT_CONFIG.max_recording_seconds).toBe(300);
|
|
60
|
+
expect(DEFAULT_CONFIG.enhance_triggers).toContain("say it better");
|
|
61
|
+
expect(DEFAULT_CONFIG.enhance_triggers).toContain("rewrite this");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("loadConfig", () => {
|
|
66
|
+
test("returns defaults when no config file or env vars", () => {
|
|
67
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
68
|
+
expect(config.transcription_model).toBe("gpt-4o-mini-transcribe");
|
|
69
|
+
expect(config.enhancement_model).toBe("gpt-4o");
|
|
70
|
+
expect(config.language).toBe("en");
|
|
71
|
+
expect(config.audio_format).toBe("wav");
|
|
72
|
+
expect(config.auto_enhance).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("loads config from file", () => {
|
|
76
|
+
const configPath = join(tempDir, "config.json");
|
|
77
|
+
writeFileSync(
|
|
78
|
+
configPath,
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
transcription_model: "whisper-1",
|
|
81
|
+
language: "fr",
|
|
82
|
+
auto_enhance: false,
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const config = loadConfig(configPath);
|
|
87
|
+
expect(config.transcription_model).toBe("whisper-1");
|
|
88
|
+
expect(config.language).toBe("fr");
|
|
89
|
+
expect(config.auto_enhance).toBe(false);
|
|
90
|
+
// Other defaults still present
|
|
91
|
+
expect(config.audio_format).toBe("wav");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("ignores invalid JSON config file", () => {
|
|
95
|
+
const configPath = join(tempDir, "bad-config.json");
|
|
96
|
+
writeFileSync(configPath, "this is not json {{{");
|
|
97
|
+
|
|
98
|
+
const config = loadConfig(configPath);
|
|
99
|
+
// Should fall back to defaults
|
|
100
|
+
expect(config.transcription_model).toBe("gpt-4o-mini-transcribe");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("env var OPENAI_API_KEY overrides config", () => {
|
|
104
|
+
process.env.OPENAI_API_KEY = "sk-env-key";
|
|
105
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
106
|
+
expect(config.openai_api_key).toBe("sk-env-key");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("env var RECORDINGS_API_KEY overrides OPENAI_API_KEY", () => {
|
|
110
|
+
process.env.OPENAI_API_KEY = "sk-openai";
|
|
111
|
+
process.env.RECORDINGS_API_KEY = "sk-recordings";
|
|
112
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
113
|
+
expect(config.openai_api_key).toBe("sk-recordings");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("env var RECORDINGS_ENHANCEMENT_KEY sets enhancement_api_key", () => {
|
|
117
|
+
process.env.RECORDINGS_ENHANCEMENT_KEY = "sk-enhance";
|
|
118
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
119
|
+
expect(config.enhancement_api_key).toBe("sk-enhance");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("env var RECORDINGS_MODEL overrides transcription_model", () => {
|
|
123
|
+
process.env.RECORDINGS_MODEL = "whisper-1";
|
|
124
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
125
|
+
expect(config.transcription_model).toBe("whisper-1");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("env var RECORDINGS_ENHANCEMENT_MODEL overrides enhancement_model", () => {
|
|
129
|
+
process.env.RECORDINGS_ENHANCEMENT_MODEL = "gpt-3.5-turbo";
|
|
130
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
131
|
+
expect(config.enhancement_model).toBe("gpt-3.5-turbo");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("env var RECORDINGS_LANGUAGE overrides language", () => {
|
|
135
|
+
process.env.RECORDINGS_LANGUAGE = "de";
|
|
136
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
137
|
+
expect(config.language).toBe("de");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("env var RECORDINGS_DB_PATH overrides db_path", () => {
|
|
141
|
+
process.env.RECORDINGS_DB_PATH = "/custom/db.sqlite";
|
|
142
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
143
|
+
expect(config.db_path).toBe("/custom/db.sqlite");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("env var RECORDINGS_AUDIO_DIR overrides audio_dir", () => {
|
|
147
|
+
process.env.RECORDINGS_AUDIO_DIR = "/custom/audio";
|
|
148
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
149
|
+
expect(config.audio_dir).toBe("/custom/audio");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("env var RECORDINGS_MAX_SECONDS overrides max_recording_seconds", () => {
|
|
153
|
+
process.env.RECORDINGS_MAX_SECONDS = "60";
|
|
154
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
155
|
+
expect(config.max_recording_seconds).toBe(60);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("sets default db_path when not configured", () => {
|
|
159
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
160
|
+
expect(config.db_path).toBeTruthy();
|
|
161
|
+
expect(config.db_path).toContain("recordings.db");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("sets default audio_dir when not configured", () => {
|
|
165
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
166
|
+
expect(config.audio_dir).toBeTruthy();
|
|
167
|
+
expect(config.audio_dir).toContain("audio");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("enhancement_api_key falls back to openai_api_key", () => {
|
|
171
|
+
process.env.OPENAI_API_KEY = "sk-shared";
|
|
172
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
173
|
+
expect(config.enhancement_api_key).toBe("sk-shared");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("file config values are overridden by env vars", () => {
|
|
177
|
+
const configPath = join(tempDir, "config.json");
|
|
178
|
+
writeFileSync(
|
|
179
|
+
configPath,
|
|
180
|
+
JSON.stringify({ language: "fr", transcription_model: "file-model" })
|
|
181
|
+
);
|
|
182
|
+
process.env.RECORDINGS_LANGUAGE = "ja";
|
|
183
|
+
|
|
184
|
+
const config = loadConfig(configPath);
|
|
185
|
+
expect(config.language).toBe("ja"); // env wins
|
|
186
|
+
expect(config.transcription_model).toBe("file-model"); // no env override for this
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("ensureDataDir", () => {
|
|
191
|
+
test("creates audio_dir and db directory", () => {
|
|
192
|
+
const audioDir = join(tempDir, "audio-out");
|
|
193
|
+
const dbPath = join(tempDir, "db-dir/recordings.db");
|
|
194
|
+
const config = {
|
|
195
|
+
...DEFAULT_CONFIG,
|
|
196
|
+
audio_dir: audioDir,
|
|
197
|
+
db_path: dbPath,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
ensureDataDir(config);
|
|
201
|
+
expect(existsSync(audioDir)).toBe(true);
|
|
202
|
+
expect(existsSync(join(tempDir, "db-dir"))).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("getDataDir", () => {
|
|
207
|
+
test("returns a string", () => {
|
|
208
|
+
const dir = getDataDir();
|
|
209
|
+
expect(typeof dir).toBe("string");
|
|
210
|
+
expect(dir).toContain(".recordings");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("loadSecretKey (via loadConfig)", () => {
|
|
215
|
+
test("loads API key from ~/.secrets with double quotes", () => {
|
|
216
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
217
|
+
// The key is either loaded from ~/.secrets or empty
|
|
218
|
+
expect(typeof config.openai_api_key).toBe("string");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("loads secret key with single quotes format", () => {
|
|
222
|
+
// We can't easily mock ~/.secrets, but we test the regex patterns directly
|
|
223
|
+
// by testing the config loading with different env overrides
|
|
224
|
+
// The loadSecretKey function is internal, so we test its effect indirectly
|
|
225
|
+
// When no env var is set and no ~/.secrets has the key, it returns ""
|
|
226
|
+
const config = loadConfig(join(tempDir, "nonexistent.json"));
|
|
227
|
+
// At minimum, the function doesn't crash
|
|
228
|
+
expect(config).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("findConfigFile (via loadConfig without explicit path)", () => {
|
|
233
|
+
test("loads config without explicit path (uses findConfigFile)", () => {
|
|
234
|
+
// When no configPath is given, findConfigFile walks up from cwd
|
|
235
|
+
const config = loadConfig();
|
|
236
|
+
expect(config).toBeDefined();
|
|
237
|
+
expect(config.transcription_model).toBeTruthy();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("ensureDataDir edge cases", () => {
|
|
242
|
+
test("handles db_path without directory separator", () => {
|
|
243
|
+
const config = {
|
|
244
|
+
...DEFAULT_CONFIG,
|
|
245
|
+
audio_dir: join(tempDir, "audio"),
|
|
246
|
+
db_path: "recordings.db", // no directory part
|
|
247
|
+
};
|
|
248
|
+
// Should not throw - dbDir will be empty string
|
|
249
|
+
ensureDataDir(config);
|
|
250
|
+
expect(existsSync(join(tempDir, "audio"))).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
});
|