@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.
- package/.claude/scheduled_tasks.lock +1 -0
- package/LICENSE +21 -0
- package/dist/cli/index.js +1446 -0
- package/dist/mcp/index.js +4882 -0
- package/package.json +46 -0
- 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 +405 -0
- package/src/native/RecordingsHelper.swift +352 -0
- package/src/types/index.ts +138 -0
- package/tsconfig.json +21 -0
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { loadConfig, ensureDataDir } from "../lib/config.js";
|
|
6
|
+
import { getDatabase } from "../db/database.js";
|
|
7
|
+
import {
|
|
8
|
+
createRecording,
|
|
9
|
+
getRecording,
|
|
10
|
+
listRecordings,
|
|
11
|
+
deleteRecording,
|
|
12
|
+
searchRecordings,
|
|
13
|
+
getRecordingStats,
|
|
14
|
+
} from "../db/recordings.js";
|
|
15
|
+
import { registerAgent, getAgent, listAgents } from "../db/agents.js";
|
|
16
|
+
import {
|
|
17
|
+
registerProject,
|
|
18
|
+
getProject,
|
|
19
|
+
listProjects,
|
|
20
|
+
} from "../db/projects.js";
|
|
21
|
+
import { transcribeAudio, transcribeBuffer } from "../lib/transcriber.js";
|
|
22
|
+
import { processText, needsEnhancement } from "../lib/enhancer.js";
|
|
23
|
+
import type { Recording, RecordingFilter } from "../types/index.js";
|
|
24
|
+
|
|
25
|
+
// ── Initialize ──────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
ensureDataDir(config);
|
|
29
|
+
getDatabase(config.db_path);
|
|
30
|
+
|
|
31
|
+
const server = new McpServer({
|
|
32
|
+
name: "recordings",
|
|
33
|
+
version: "0.0.3",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function text(content: string) {
|
|
39
|
+
return { content: [{ type: "text" as const, text: content }] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function errorResult(e: unknown) {
|
|
43
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
44
|
+
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compact(r: Recording): string {
|
|
48
|
+
const t = (r.processed_text || r.raw_text).slice(0, 80);
|
|
49
|
+
return `${r.id.slice(0, 8)} | ${r.processing_mode} | ${r.created_at.slice(0, 16)} | ${t}${t.length >= 80 ? "..." : ""}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function full(r: Recording): string {
|
|
53
|
+
const lines: string[] = [`ID: ${r.id}`, `Mode: ${r.processing_mode}`, `Model: ${r.model_used}`];
|
|
54
|
+
if (r.enhancement_model) lines.push(`Enhanced by: ${r.enhancement_model}`);
|
|
55
|
+
if (r.duration_ms) lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
|
|
56
|
+
if (r.language) lines.push(`Language: ${r.language}`);
|
|
57
|
+
if (r.tags.length > 0) lines.push(`Tags: ${r.tags.join(", ")}`);
|
|
58
|
+
if (r.agent_id) lines.push(`Agent: ${r.agent_id}`);
|
|
59
|
+
if (r.project_id) lines.push(`Project: ${r.project_id}`);
|
|
60
|
+
if (r.session_id) lines.push(`Session: ${r.session_id}`);
|
|
61
|
+
lines.push(`Created: ${r.created_at}`);
|
|
62
|
+
lines.push(`Text: ${r.raw_text}`);
|
|
63
|
+
if (r.processed_text && r.processed_text !== r.raw_text) {
|
|
64
|
+
lines.push(`Enhanced: ${r.processed_text}`);
|
|
65
|
+
}
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Full tool schemas for describe_tool ─────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const toolDocs: Record<string, string> = {
|
|
72
|
+
transcribe_audio: "Transcribe audio file. Auto-enhances if needed.\nParams: audio_path (string, required): path to wav/mp3/m4a/webm | language (string): ISO code e.g. en/es/fr | no_enhance (bool): skip AI enhancement | tags (string[]): tags | agent_id (string) | project_id (string) | session_id (string)",
|
|
73
|
+
save_recording: "Save text as recording. Auto-enhances if needed.\nParams: text (string, required): text to save | enhance (bool): force enhancement | tags (string[]) | agent_id (string) | project_id (string) | session_id (string) | metadata (object)",
|
|
74
|
+
get_recording: "Get recording by ID or prefix.\nParams: id (string, required): recording ID or prefix",
|
|
75
|
+
list_recordings: "List recordings, compact by default, most recent first.\nParams: limit (number, default 10) | offset (number) | processing_mode ('raw'|'enhanced') | tags (string[]) | search (string): text search | since/until (ISO date) | agent_id | project_id | session_id | full (bool): verbose output",
|
|
76
|
+
search_recordings: "Search recordings by text content.\nParams: query (string, required) | limit (number, default 10) | agent_id | project_id | full (bool): verbose output",
|
|
77
|
+
delete_recording: "Delete recording by ID.\nParams: id (string, required)",
|
|
78
|
+
recording_stats: "Recording count, mode breakdown, duration.\nParams: none",
|
|
79
|
+
detect_enhancement: "Check if text needs AI enhancement.\nParams: text (string, required)",
|
|
80
|
+
register_agent: "Register agent (idempotent).\nParams: name (string, required) | description (string) | role (string)",
|
|
81
|
+
list_agents: "List registered agents.\nParams: none",
|
|
82
|
+
get_agent: "Get agent by ID or name.\nParams: id (string, required)",
|
|
83
|
+
register_project: "Register project (idempotent).\nParams: name (string, required) | path (string, required): absolute path | description (string)",
|
|
84
|
+
list_projects: "List registered projects.\nParams: none",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ── Meta Tool ───────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
server.tool(
|
|
90
|
+
"describe_tool",
|
|
91
|
+
"Get full param docs for any tool.",
|
|
92
|
+
{ name: z.string() },
|
|
93
|
+
async (args) => {
|
|
94
|
+
const doc = toolDocs[args.name];
|
|
95
|
+
return doc ? text(doc) : text(`Unknown tool: ${args.name}. Available: ${Object.keys(toolDocs).join(", ")}`);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// ── Recording Tools (lean stubs — no param descriptions) ────────────────────
|
|
100
|
+
|
|
101
|
+
server.tool(
|
|
102
|
+
"transcribe_audio",
|
|
103
|
+
"Transcribe audio file. Auto-enhances if needed.",
|
|
104
|
+
{
|
|
105
|
+
audio_path: z.string(),
|
|
106
|
+
language: z.string().optional(),
|
|
107
|
+
no_enhance: z.boolean().optional(),
|
|
108
|
+
tags: z.array(z.string()).optional(),
|
|
109
|
+
agent_id: z.string().optional(),
|
|
110
|
+
project_id: z.string().optional(),
|
|
111
|
+
session_id: z.string().optional(),
|
|
112
|
+
},
|
|
113
|
+
async (args) => {
|
|
114
|
+
try {
|
|
115
|
+
const cfg = { ...config };
|
|
116
|
+
if (args.language) cfg.language = args.language;
|
|
117
|
+
if (args.no_enhance) cfg.auto_enhance = false;
|
|
118
|
+
|
|
119
|
+
const transcription = await transcribeAudio(args.audio_path, cfg);
|
|
120
|
+
const processed = await processText(transcription.text, cfg);
|
|
121
|
+
|
|
122
|
+
const recording = createRecording({
|
|
123
|
+
audio_path: args.audio_path,
|
|
124
|
+
raw_text: transcription.text,
|
|
125
|
+
processed_text: processed.mode === "enhanced" ? processed.text : undefined,
|
|
126
|
+
processing_mode: processed.mode,
|
|
127
|
+
model_used: transcription.model,
|
|
128
|
+
enhancement_model: processed.enhancement_model || undefined,
|
|
129
|
+
duration_ms: transcription.duration_ms,
|
|
130
|
+
language: transcription.language || undefined,
|
|
131
|
+
tags: args.tags,
|
|
132
|
+
agent_id: args.agent_id,
|
|
133
|
+
project_id: args.project_id,
|
|
134
|
+
session_id: args.session_id,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const output = processed.mode === "enhanced" ? processed.text : transcription.text;
|
|
138
|
+
return text(`${recording.id.slice(0, 8)} | ${processed.mode} | ${output}`);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return errorResult(e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
server.tool(
|
|
146
|
+
"save_recording",
|
|
147
|
+
"Save text as recording. Auto-enhances if needed.",
|
|
148
|
+
{
|
|
149
|
+
text: z.string(),
|
|
150
|
+
enhance: z.boolean().optional(),
|
|
151
|
+
tags: z.array(z.string()).optional(),
|
|
152
|
+
agent_id: z.string().optional(),
|
|
153
|
+
project_id: z.string().optional(),
|
|
154
|
+
session_id: z.string().optional(),
|
|
155
|
+
metadata: z.record(z.unknown()).optional(),
|
|
156
|
+
},
|
|
157
|
+
async (args) => {
|
|
158
|
+
try {
|
|
159
|
+
let processedText: string | undefined;
|
|
160
|
+
let mode: "raw" | "enhanced" = "raw";
|
|
161
|
+
let enhModel: string | undefined;
|
|
162
|
+
|
|
163
|
+
if (args.enhance !== false) {
|
|
164
|
+
const processed = await processText(args.text, config);
|
|
165
|
+
if (processed.mode === "enhanced") {
|
|
166
|
+
processedText = processed.text;
|
|
167
|
+
mode = "enhanced";
|
|
168
|
+
enhModel = processed.enhancement_model || undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const recording = createRecording({
|
|
173
|
+
raw_text: args.text,
|
|
174
|
+
processed_text: processedText,
|
|
175
|
+
processing_mode: mode,
|
|
176
|
+
model_used: "direct-input",
|
|
177
|
+
enhancement_model: enhModel,
|
|
178
|
+
tags: args.tags,
|
|
179
|
+
agent_id: args.agent_id,
|
|
180
|
+
project_id: args.project_id,
|
|
181
|
+
session_id: args.session_id,
|
|
182
|
+
metadata: args.metadata,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const output = processedText || args.text;
|
|
186
|
+
return text(`${recording.id.slice(0, 8)} | ${mode} | ${output}`);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return errorResult(e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
server.tool(
|
|
194
|
+
"get_recording",
|
|
195
|
+
"Get recording by ID or prefix.",
|
|
196
|
+
{ id: z.string() },
|
|
197
|
+
async (args) => {
|
|
198
|
+
try {
|
|
199
|
+
const r = getRecording(args.id);
|
|
200
|
+
if (!r) return text(`Not found: ${args.id}`);
|
|
201
|
+
return text(full(r));
|
|
202
|
+
} catch (e) {
|
|
203
|
+
return errorResult(e);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
server.tool(
|
|
209
|
+
"list_recordings",
|
|
210
|
+
"List recordings. Compact default, recent first.",
|
|
211
|
+
{
|
|
212
|
+
limit: z.number().optional(),
|
|
213
|
+
offset: z.number().optional(),
|
|
214
|
+
processing_mode: z.enum(["raw", "enhanced"]).optional(),
|
|
215
|
+
tags: z.array(z.string()).optional(),
|
|
216
|
+
search: z.string().optional(),
|
|
217
|
+
since: z.string().optional(),
|
|
218
|
+
until: z.string().optional(),
|
|
219
|
+
agent_id: z.string().optional(),
|
|
220
|
+
project_id: z.string().optional(),
|
|
221
|
+
session_id: z.string().optional(),
|
|
222
|
+
full: z.boolean().optional(),
|
|
223
|
+
},
|
|
224
|
+
async (args) => {
|
|
225
|
+
try {
|
|
226
|
+
const filter: RecordingFilter = {
|
|
227
|
+
limit: args.limit || 10,
|
|
228
|
+
offset: args.offset,
|
|
229
|
+
processing_mode: args.processing_mode,
|
|
230
|
+
tags: args.tags,
|
|
231
|
+
search: args.search,
|
|
232
|
+
since: args.since,
|
|
233
|
+
until: args.until,
|
|
234
|
+
agent_id: args.agent_id,
|
|
235
|
+
project_id: args.project_id,
|
|
236
|
+
session_id: args.session_id,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const recordings = listRecordings(filter);
|
|
240
|
+
if (recordings.length === 0) return text("No recordings found.");
|
|
241
|
+
|
|
242
|
+
const fmt = args.full ? full : compact;
|
|
243
|
+
const sep = args.full ? "\n---\n" : "\n";
|
|
244
|
+
return text(`${recordings.length} recording(s):${sep}${recordings.map(fmt).join(sep)}`);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
return errorResult(e);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
server.tool(
|
|
252
|
+
"search_recordings",
|
|
253
|
+
"Search recordings by text.",
|
|
254
|
+
{
|
|
255
|
+
query: z.string(),
|
|
256
|
+
limit: z.number().optional(),
|
|
257
|
+
agent_id: z.string().optional(),
|
|
258
|
+
project_id: z.string().optional(),
|
|
259
|
+
full: z.boolean().optional(),
|
|
260
|
+
},
|
|
261
|
+
async (args) => {
|
|
262
|
+
try {
|
|
263
|
+
const results = searchRecordings(args.query, {
|
|
264
|
+
limit: args.limit || 10,
|
|
265
|
+
agent_id: args.agent_id,
|
|
266
|
+
project_id: args.project_id,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (results.length === 0) return text("No results.");
|
|
270
|
+
|
|
271
|
+
const fmt = args.full ? full : compact;
|
|
272
|
+
const sep = args.full ? "\n---\n" : "\n";
|
|
273
|
+
return text(`${results.length} result(s):${sep}${results.map(fmt).join(sep)}`);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return errorResult(e);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
server.tool(
|
|
281
|
+
"delete_recording",
|
|
282
|
+
"Delete recording by ID.",
|
|
283
|
+
{ id: z.string() },
|
|
284
|
+
async (args) => {
|
|
285
|
+
try {
|
|
286
|
+
return text(deleteRecording(args.id) ? `Deleted ${args.id}` : `Not found: ${args.id}`);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
return errorResult(e);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
server.tool(
|
|
294
|
+
"recording_stats",
|
|
295
|
+
"Recording stats: count, modes, duration.",
|
|
296
|
+
{},
|
|
297
|
+
async () => {
|
|
298
|
+
try {
|
|
299
|
+
const s = getRecordingStats();
|
|
300
|
+
let out = `Total: ${s.total} | Raw: ${s.raw} | Enhanced: ${s.enhanced} | Duration: ${(s.total_duration_ms / 1000).toFixed(1)}s`;
|
|
301
|
+
if (Object.keys(s.by_model).length > 0) {
|
|
302
|
+
out += "\n" + Object.entries(s.by_model).map(([m, c]) => `${m}: ${c}`).join(", ");
|
|
303
|
+
}
|
|
304
|
+
return text(out);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return errorResult(e);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
server.tool(
|
|
312
|
+
"detect_enhancement",
|
|
313
|
+
"Check if text needs AI enhancement.",
|
|
314
|
+
{ text: z.string() },
|
|
315
|
+
async (args) => {
|
|
316
|
+
try {
|
|
317
|
+
const r = needsEnhancement(args.text, config);
|
|
318
|
+
return text(`${r.needs ? "Yes" : "No"}: ${r.reason}`);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
return errorResult(e);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// ── Agent Tools ─────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
server.tool(
|
|
328
|
+
"register_agent",
|
|
329
|
+
"Register agent (idempotent).",
|
|
330
|
+
{ name: z.string(), description: z.string().optional(), role: z.string().optional() },
|
|
331
|
+
async (args) => {
|
|
332
|
+
try {
|
|
333
|
+
const a = registerAgent(args.name, args.description, args.role);
|
|
334
|
+
return text(`${a.id} | ${a.name} | ${a.role}`);
|
|
335
|
+
} catch (e) {
|
|
336
|
+
return errorResult(e);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
server.tool(
|
|
342
|
+
"list_agents",
|
|
343
|
+
"List registered agents.",
|
|
344
|
+
{},
|
|
345
|
+
async () => {
|
|
346
|
+
try {
|
|
347
|
+
const agents = listAgents();
|
|
348
|
+
if (agents.length === 0) return text("None.");
|
|
349
|
+
return text(agents.map((a) => `${a.id} | ${a.name} | ${a.role}`).join("\n"));
|
|
350
|
+
} catch (e) {
|
|
351
|
+
return errorResult(e);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
server.tool(
|
|
357
|
+
"get_agent",
|
|
358
|
+
"Get agent by ID or name.",
|
|
359
|
+
{ id: z.string() },
|
|
360
|
+
async (args) => {
|
|
361
|
+
try {
|
|
362
|
+
const a = getAgent(args.id);
|
|
363
|
+
if (!a) return text(`Not found: ${args.id}`);
|
|
364
|
+
return text(`${a.id} | ${a.name} | ${a.role} | ${a.last_seen_at}`);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
return errorResult(e);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// ── Project Tools ───────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
server.tool(
|
|
374
|
+
"register_project",
|
|
375
|
+
"Register project (idempotent).",
|
|
376
|
+
{ name: z.string(), path: z.string(), description: z.string().optional() },
|
|
377
|
+
async (args) => {
|
|
378
|
+
try {
|
|
379
|
+
const p = registerProject(args.name, args.path, args.description);
|
|
380
|
+
return text(`${p.id.slice(0, 8)} | ${p.name} | ${p.path}`);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return errorResult(e);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
server.tool(
|
|
388
|
+
"list_projects",
|
|
389
|
+
"List registered projects.",
|
|
390
|
+
{},
|
|
391
|
+
async () => {
|
|
392
|
+
try {
|
|
393
|
+
const projects = listProjects();
|
|
394
|
+
if (projects.length === 0) return text("None.");
|
|
395
|
+
return text(projects.map((p) => `${p.id.slice(0, 8)} | ${p.name} | ${p.path}`).join("\n"));
|
|
396
|
+
} catch (e) {
|
|
397
|
+
return errorResult(e);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// ── Start Server ────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
const transport = new StdioServerTransport();
|
|
405
|
+
await server.connect(transport);
|