@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.
- package/README.md +2 -0
- package/dist/cli/index.js +395 -37
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/index.js +78 -11
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts +8 -2
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +147 -30
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +20 -3
- package/scripts/install_macos_app.sh +76 -0
- package/src/native/Recordings/{Recordings → App}/RecordingsApp.swift +5 -1
- package/src/native/Recordings/Package.resolved +21 -3
- package/src/native/Recordings/Package.swift +16 -5
- package/src/native/Recordings/{Recordings → RecordingsLib}/Info.plist +6 -0
- package/src/native/Recordings/{Recordings → RecordingsLib}/MenuBarPopover.swift +55 -11
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +36 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +164 -0
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +113 -0
- package/src/native/Recordings/{Recordings → RecordingsLib}/ProjectStore.swift +11 -11
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +383 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +835 -0
- package/src/native/Recordings/{Recordings → RecordingsLib}/SettingsView.swift +19 -5
- package/src/native/Recordings/{Recordings → RecordingsLib}/VoiceShortcuts.swift +12 -8
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +63 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +23 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +33 -0
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +92 -0
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +58 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +113 -0
- package/src/native/Recordings/build.sh +6 -6
- package/.takumi/settings.local.json +0 -7
- package/bun.lock +0 -250
- package/bunfig.toml +0 -2
- package/src/__tests__/agents.test.ts +0 -136
- package/src/__tests__/config.test.ts +0 -252
- package/src/__tests__/database.test.ts +0 -167
- package/src/__tests__/enhancer.test.ts +0 -639
- package/src/__tests__/preload.ts +0 -4
- package/src/__tests__/projects.test.ts +0 -109
- package/src/__tests__/recorder.test.ts +0 -278
- package/src/__tests__/recordings.test.ts +0 -353
- package/src/__tests__/transcriber.test.ts +0 -322
- package/src/__tests__/types.test.ts +0 -75
- package/src/cli/index.ts +0 -988
- package/src/db/agents.ts +0 -104
- package/src/db/database.ts +0 -163
- package/src/db/pg-migrations.ts +0 -82
- package/src/db/projects.ts +0 -71
- package/src/db/recordings.ts +0 -225
- package/src/index.ts +0 -81
- package/src/lib/config.ts +0 -223
- package/src/lib/enhancer.ts +0 -173
- package/src/lib/recorder.ts +0 -198
- package/src/lib/transcriber.ts +0 -105
- package/src/mcp/index.ts +0 -464
- package/src/native/Recordings/Recordings/RecordingEngine.swift +0 -455
- package/src/native/Recordings/test_fn.swift +0 -79
- package/src/native/Recordings/test_fn2.swift +0 -33
- package/src/types/index.ts +0 -144
- package/tsconfig.json +0 -21
- /package/src/native/Recordings/{Recordings → RecordingsLib}/FnKeyMonitor.swift +0 -0
- /package/src/native/Recordings/{Recordings → RecordingsLib}/Recordings.entitlements +0 -0
package/src/lib/transcriber.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import OpenAI from "openai";
|
|
2
|
-
import { createReadStream } from "fs";
|
|
3
|
-
import type { RecordingsConfig, TranscriptionResult } from "../types/index.js";
|
|
4
|
-
import { TranscriptionError } from "../types/index.js";
|
|
5
|
-
|
|
6
|
-
let _client: OpenAI | null = null;
|
|
7
|
-
|
|
8
|
-
function getClient(config: RecordingsConfig): OpenAI {
|
|
9
|
-
if (_client) return _client;
|
|
10
|
-
if (!config.openai_api_key) {
|
|
11
|
-
throw new TranscriptionError(
|
|
12
|
-
"OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets"
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
_client = new OpenAI({ apiKey: config.openai_api_key });
|
|
16
|
-
return _client;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function resetClient(): void {
|
|
20
|
-
_client = null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function transcribeAudio(
|
|
24
|
-
audioPath: string,
|
|
25
|
-
config: RecordingsConfig
|
|
26
|
-
): Promise<TranscriptionResult> {
|
|
27
|
-
const client = getClient(config);
|
|
28
|
-
const startTime = Date.now();
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const transcription = await client.audio.transcriptions.create({
|
|
32
|
-
file: createReadStream(audioPath),
|
|
33
|
-
model: config.transcription_model,
|
|
34
|
-
language: config.language || undefined,
|
|
35
|
-
response_format: "json",
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const durationMs = Date.now() - startTime;
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
text: transcription.text,
|
|
42
|
-
duration_ms: durationMs,
|
|
43
|
-
model: config.transcription_model,
|
|
44
|
-
language: (transcription as unknown as Record<string, unknown>).language as string | null,
|
|
45
|
-
};
|
|
46
|
-
} catch (error) {
|
|
47
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
48
|
-
throw new TranscriptionError(`Transcription failed: ${msg}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function transcribeBuffer(
|
|
53
|
-
buffer: Buffer,
|
|
54
|
-
filename: string,
|
|
55
|
-
config: RecordingsConfig
|
|
56
|
-
): Promise<TranscriptionResult> {
|
|
57
|
-
const client = getClient(config);
|
|
58
|
-
const startTime = Date.now();
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const file = new File([new Uint8Array(buffer)], filename, {
|
|
62
|
-
type: getMimeType(filename),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const transcription = await client.audio.transcriptions.create({
|
|
66
|
-
file,
|
|
67
|
-
model: config.transcription_model,
|
|
68
|
-
language: config.language || undefined,
|
|
69
|
-
response_format: "json",
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const durationMs = Date.now() - startTime;
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
text: transcription.text,
|
|
76
|
-
duration_ms: durationMs,
|
|
77
|
-
model: config.transcription_model,
|
|
78
|
-
language: (transcription as unknown as Record<string, unknown>).language as string | null,
|
|
79
|
-
};
|
|
80
|
-
} catch (error) {
|
|
81
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
82
|
-
throw new TranscriptionError(`Transcription failed: ${msg}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function getMimeType(filename: string): string {
|
|
87
|
-
const ext = filename.split(".").pop()?.toLowerCase();
|
|
88
|
-
switch (ext) {
|
|
89
|
-
case "wav":
|
|
90
|
-
return "audio/wav";
|
|
91
|
-
case "mp3":
|
|
92
|
-
return "audio/mpeg";
|
|
93
|
-
case "m4a":
|
|
94
|
-
return "audio/mp4";
|
|
95
|
-
case "webm":
|
|
96
|
-
return "audio/webm";
|
|
97
|
-
case "mp4":
|
|
98
|
-
return "audio/mp4";
|
|
99
|
-
case "mpeg":
|
|
100
|
-
case "mpga":
|
|
101
|
-
return "audio/mpeg";
|
|
102
|
-
default:
|
|
103
|
-
return "audio/wav";
|
|
104
|
-
}
|
|
105
|
-
}
|
package/src/mcp/index.ts
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
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 { registerCloudTools } from "@hasna/cloud";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { loadConfig, ensureDataDir } from "../lib/config.js";
|
|
7
|
-
import { getDatabase, getAdapter } from "../db/database.js";
|
|
8
|
-
import {
|
|
9
|
-
createRecording,
|
|
10
|
-
getRecording,
|
|
11
|
-
listRecordings,
|
|
12
|
-
deleteRecording,
|
|
13
|
-
searchRecordings,
|
|
14
|
-
getRecordingStats,
|
|
15
|
-
} from "../db/recordings.js";
|
|
16
|
-
import { registerAgent, getAgent, listAgents, heartbeatAgent, setAgentFocus } from "../db/agents.js";
|
|
17
|
-
import {
|
|
18
|
-
registerProject,
|
|
19
|
-
getProject,
|
|
20
|
-
listProjects,
|
|
21
|
-
} from "../db/projects.js";
|
|
22
|
-
import { transcribeAudio, transcribeBuffer } from "../lib/transcriber.js";
|
|
23
|
-
import { processText, needsEnhancement } from "../lib/enhancer.js";
|
|
24
|
-
import type { Recording, RecordingFilter } from "../types/index.js";
|
|
25
|
-
|
|
26
|
-
// ── Initialize ──────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
const config = loadConfig();
|
|
29
|
-
ensureDataDir(config);
|
|
30
|
-
getDatabase(config.db_path);
|
|
31
|
-
|
|
32
|
-
const server = new McpServer({
|
|
33
|
-
name: "recordings",
|
|
34
|
-
version: "0.0.3",
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
function text(content: string) {
|
|
40
|
-
return { content: [{ type: "text" as const, text: content }] };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function errorResult(e: unknown) {
|
|
44
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
45
|
-
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function compact(r: Recording): string {
|
|
49
|
-
const t = (r.processed_text || r.raw_text).slice(0, 80);
|
|
50
|
-
return `${r.id.slice(0, 8)} | ${r.processing_mode} | ${r.created_at.slice(0, 16)} | ${t}${t.length >= 80 ? "..." : ""}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function full(r: Recording): string {
|
|
54
|
-
const lines: string[] = [`ID: ${r.id}`, `Mode: ${r.processing_mode}`, `Model: ${r.model_used}`];
|
|
55
|
-
if (r.enhancement_model) lines.push(`Enhanced by: ${r.enhancement_model}`);
|
|
56
|
-
if (r.duration_ms) lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
|
|
57
|
-
if (r.language) lines.push(`Language: ${r.language}`);
|
|
58
|
-
if (r.tags.length > 0) lines.push(`Tags: ${r.tags.join(", ")}`);
|
|
59
|
-
if (r.agent_id) lines.push(`Agent: ${r.agent_id}`);
|
|
60
|
-
if (r.project_id) lines.push(`Project: ${r.project_id}`);
|
|
61
|
-
if (r.session_id) lines.push(`Session: ${r.session_id}`);
|
|
62
|
-
lines.push(`Created: ${r.created_at}`);
|
|
63
|
-
lines.push(`Text: ${r.raw_text}`);
|
|
64
|
-
if (r.processed_text && r.processed_text !== r.raw_text) {
|
|
65
|
-
lines.push(`Enhanced: ${r.processed_text}`);
|
|
66
|
-
}
|
|
67
|
-
return lines.join("\n");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ── Full tool schemas for describe_tool ─────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
const toolDocs: Record<string, string> = {
|
|
73
|
-
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)",
|
|
74
|
-
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)",
|
|
75
|
-
get_recording: "Get recording by ID or prefix.\nParams: id (string, required): recording ID or prefix",
|
|
76
|
-
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",
|
|
77
|
-
search_recordings: "Search recordings by text content.\nParams: query (string, required) | limit (number, default 10) | agent_id | project_id | full (bool): verbose output",
|
|
78
|
-
delete_recording: "Delete recording by ID.\nParams: id (string, required)",
|
|
79
|
-
recording_stats: "Recording count, mode breakdown, duration.\nParams: none",
|
|
80
|
-
detect_enhancement: "Check if text needs AI enhancement.\nParams: text (string, required)",
|
|
81
|
-
register_agent: "Register agent (idempotent). Auto-updates last_seen_at on re-register.\nParams: name (string, required) | description (string) | role (string)",
|
|
82
|
-
list_agents: "List registered agents.\nParams: none",
|
|
83
|
-
get_agent: "Get agent by ID or name.\nParams: id (string, required)",
|
|
84
|
-
heartbeat: "Update last_seen_at to signal agent is active.\nParams: agent_id (string, required): agent ID or name",
|
|
85
|
-
set_focus: "Set active project context for this agent session.\nParams: agent_id (string, required) | project_id (string, nullable): project ID or null to clear",
|
|
86
|
-
register_project: "Register project (idempotent).\nParams: name (string, required) | path (string, required): absolute path | description (string)",
|
|
87
|
-
list_projects: "List registered projects.\nParams: none",
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// ── Meta Tool ───────────────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
server.tool(
|
|
93
|
-
"describe_tool",
|
|
94
|
-
"Get full param docs for any tool.",
|
|
95
|
-
{ name: z.string() },
|
|
96
|
-
async (args) => {
|
|
97
|
-
const doc = toolDocs[args.name];
|
|
98
|
-
return doc ? text(doc) : text(`Unknown tool: ${args.name}. Available: ${Object.keys(toolDocs).join(", ")}`);
|
|
99
|
-
}
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
// ── Recording Tools (lean stubs — no param descriptions) ────────────────────
|
|
103
|
-
|
|
104
|
-
server.tool(
|
|
105
|
-
"transcribe_audio",
|
|
106
|
-
"Transcribe audio file. Auto-enhances if needed.",
|
|
107
|
-
{
|
|
108
|
-
audio_path: z.string(),
|
|
109
|
-
language: z.string().optional(),
|
|
110
|
-
no_enhance: z.boolean().optional(),
|
|
111
|
-
tags: z.array(z.string()).optional(),
|
|
112
|
-
agent_id: z.string().optional(),
|
|
113
|
-
project_id: z.string().optional(),
|
|
114
|
-
session_id: z.string().optional(),
|
|
115
|
-
},
|
|
116
|
-
async (args) => {
|
|
117
|
-
try {
|
|
118
|
-
const cfg = { ...config };
|
|
119
|
-
if (args.language) cfg.language = args.language;
|
|
120
|
-
if (args.no_enhance) cfg.auto_enhance = false;
|
|
121
|
-
|
|
122
|
-
const transcription = await transcribeAudio(args.audio_path, cfg);
|
|
123
|
-
const processed = await processText(transcription.text, cfg);
|
|
124
|
-
|
|
125
|
-
const recording = createRecording({
|
|
126
|
-
audio_path: args.audio_path,
|
|
127
|
-
raw_text: transcription.text,
|
|
128
|
-
processed_text: processed.mode === "enhanced" ? processed.text : undefined,
|
|
129
|
-
processing_mode: processed.mode,
|
|
130
|
-
model_used: transcription.model,
|
|
131
|
-
enhancement_model: processed.enhancement_model || undefined,
|
|
132
|
-
duration_ms: transcription.duration_ms,
|
|
133
|
-
language: transcription.language || undefined,
|
|
134
|
-
tags: args.tags,
|
|
135
|
-
agent_id: args.agent_id,
|
|
136
|
-
project_id: args.project_id,
|
|
137
|
-
session_id: args.session_id,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const output = processed.mode === "enhanced" ? processed.text : transcription.text;
|
|
141
|
-
return text(`${recording.id.slice(0, 8)} | ${processed.mode} | ${output}`);
|
|
142
|
-
} catch (e) {
|
|
143
|
-
return errorResult(e);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
server.tool(
|
|
149
|
-
"save_recording",
|
|
150
|
-
"Save text as recording. Auto-enhances if needed.",
|
|
151
|
-
{
|
|
152
|
-
text: z.string(),
|
|
153
|
-
enhance: z.boolean().optional(),
|
|
154
|
-
tags: z.array(z.string()).optional(),
|
|
155
|
-
agent_id: z.string().optional(),
|
|
156
|
-
project_id: z.string().optional(),
|
|
157
|
-
session_id: z.string().optional(),
|
|
158
|
-
goal: z.string().optional().describe("Goal or purpose of this recording session (e.g. 'code review for PR #123')"),
|
|
159
|
-
role: z.string().optional().describe("Agent role for this session (e.g. 'dev agent for connectdev')"),
|
|
160
|
-
task_list_id: z.string().optional().describe("Task list ID to bind this recording to"),
|
|
161
|
-
metadata: z.record(z.unknown()).optional(),
|
|
162
|
-
},
|
|
163
|
-
async (args) => {
|
|
164
|
-
try {
|
|
165
|
-
let processedText: string | undefined;
|
|
166
|
-
let mode: "raw" | "enhanced" = "raw";
|
|
167
|
-
let enhModel: string | undefined;
|
|
168
|
-
|
|
169
|
-
if (args.enhance !== false) {
|
|
170
|
-
const processed = await processText(args.text, config);
|
|
171
|
-
if (processed.mode === "enhanced") {
|
|
172
|
-
processedText = processed.text;
|
|
173
|
-
mode = "enhanced";
|
|
174
|
-
enhModel = processed.enhancement_model || undefined;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const recording = createRecording({
|
|
179
|
-
raw_text: args.text,
|
|
180
|
-
processed_text: processedText,
|
|
181
|
-
processing_mode: mode,
|
|
182
|
-
model_used: "direct-input",
|
|
183
|
-
enhancement_model: enhModel,
|
|
184
|
-
tags: args.tags,
|
|
185
|
-
agent_id: args.agent_id,
|
|
186
|
-
project_id: args.project_id,
|
|
187
|
-
session_id: args.session_id,
|
|
188
|
-
goal: args.goal,
|
|
189
|
-
role: args.role,
|
|
190
|
-
task_list_id: args.task_list_id,
|
|
191
|
-
metadata: args.metadata,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const output = processedText || args.text;
|
|
195
|
-
return text(`${recording.id.slice(0, 8)} | ${mode} | ${output}`);
|
|
196
|
-
} catch (e) {
|
|
197
|
-
return errorResult(e);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
server.tool(
|
|
203
|
-
"get_recording",
|
|
204
|
-
"Get recording by ID or prefix.",
|
|
205
|
-
{ id: z.string() },
|
|
206
|
-
async (args) => {
|
|
207
|
-
try {
|
|
208
|
-
const r = getRecording(args.id);
|
|
209
|
-
if (!r) return text(`Not found: ${args.id}`);
|
|
210
|
-
return text(full(r));
|
|
211
|
-
} catch (e) {
|
|
212
|
-
return errorResult(e);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
server.tool(
|
|
218
|
-
"list_recordings",
|
|
219
|
-
"List recordings. Compact default, recent first.",
|
|
220
|
-
{
|
|
221
|
-
limit: z.number().optional(),
|
|
222
|
-
offset: z.number().optional(),
|
|
223
|
-
processing_mode: z.enum(["raw", "enhanced"]).optional(),
|
|
224
|
-
tags: z.array(z.string()).optional(),
|
|
225
|
-
search: z.string().optional(),
|
|
226
|
-
since: z.string().optional(),
|
|
227
|
-
until: z.string().optional(),
|
|
228
|
-
agent_id: z.string().optional(),
|
|
229
|
-
project_id: z.string().optional(),
|
|
230
|
-
session_id: z.string().optional(),
|
|
231
|
-
full: z.boolean().optional(),
|
|
232
|
-
},
|
|
233
|
-
async (args) => {
|
|
234
|
-
try {
|
|
235
|
-
const filter: RecordingFilter = {
|
|
236
|
-
limit: args.limit || 10,
|
|
237
|
-
offset: args.offset,
|
|
238
|
-
processing_mode: args.processing_mode,
|
|
239
|
-
tags: args.tags,
|
|
240
|
-
search: args.search,
|
|
241
|
-
since: args.since,
|
|
242
|
-
until: args.until,
|
|
243
|
-
agent_id: args.agent_id,
|
|
244
|
-
project_id: args.project_id,
|
|
245
|
-
session_id: args.session_id,
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const recordings = listRecordings(filter);
|
|
249
|
-
if (recordings.length === 0) return text("No recordings found.");
|
|
250
|
-
|
|
251
|
-
const fmt = args.full ? full : compact;
|
|
252
|
-
const sep = args.full ? "\n---\n" : "\n";
|
|
253
|
-
return text(`${recordings.length} recording(s):${sep}${recordings.map(fmt).join(sep)}`);
|
|
254
|
-
} catch (e) {
|
|
255
|
-
return errorResult(e);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
server.tool(
|
|
261
|
-
"search_recordings",
|
|
262
|
-
"Search recordings by text.",
|
|
263
|
-
{
|
|
264
|
-
query: z.string(),
|
|
265
|
-
limit: z.number().optional(),
|
|
266
|
-
agent_id: z.string().optional(),
|
|
267
|
-
project_id: z.string().optional(),
|
|
268
|
-
full: z.boolean().optional(),
|
|
269
|
-
},
|
|
270
|
-
async (args) => {
|
|
271
|
-
try {
|
|
272
|
-
const results = searchRecordings(args.query, {
|
|
273
|
-
limit: args.limit || 10,
|
|
274
|
-
agent_id: args.agent_id,
|
|
275
|
-
project_id: args.project_id,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
if (results.length === 0) return text("No results.");
|
|
279
|
-
|
|
280
|
-
const fmt = args.full ? full : compact;
|
|
281
|
-
const sep = args.full ? "\n---\n" : "\n";
|
|
282
|
-
return text(`${results.length} result(s):${sep}${results.map(fmt).join(sep)}`);
|
|
283
|
-
} catch (e) {
|
|
284
|
-
return errorResult(e);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
server.tool(
|
|
290
|
-
"delete_recording",
|
|
291
|
-
"Delete recording by ID.",
|
|
292
|
-
{ id: z.string() },
|
|
293
|
-
async (args) => {
|
|
294
|
-
try {
|
|
295
|
-
return text(deleteRecording(args.id) ? `Deleted ${args.id}` : `Not found: ${args.id}`);
|
|
296
|
-
} catch (e) {
|
|
297
|
-
return errorResult(e);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
server.tool(
|
|
303
|
-
"recording_stats",
|
|
304
|
-
"Recording stats: count, modes, duration.",
|
|
305
|
-
{},
|
|
306
|
-
async () => {
|
|
307
|
-
try {
|
|
308
|
-
const s = getRecordingStats();
|
|
309
|
-
let out = `Total: ${s.total} | Raw: ${s.raw} | Enhanced: ${s.enhanced} | Duration: ${(s.total_duration_ms / 1000).toFixed(1)}s`;
|
|
310
|
-
if (Object.keys(s.by_model).length > 0) {
|
|
311
|
-
out += "\n" + Object.entries(s.by_model).map(([m, c]) => `${m}: ${c}`).join(", ");
|
|
312
|
-
}
|
|
313
|
-
return text(out);
|
|
314
|
-
} catch (e) {
|
|
315
|
-
return errorResult(e);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
server.tool(
|
|
321
|
-
"detect_enhancement",
|
|
322
|
-
"Check if text needs AI enhancement.",
|
|
323
|
-
{ text: z.string() },
|
|
324
|
-
async (args) => {
|
|
325
|
-
try {
|
|
326
|
-
const r = needsEnhancement(args.text, config);
|
|
327
|
-
return text(`${r.needs ? "Yes" : "No"}: ${r.reason}`);
|
|
328
|
-
} catch (e) {
|
|
329
|
-
return errorResult(e);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
// ── Agent Tools ─────────────────────────────────────────────────────────────
|
|
335
|
-
|
|
336
|
-
server.tool(
|
|
337
|
-
"register_agent",
|
|
338
|
-
"Register agent (idempotent).",
|
|
339
|
-
{ name: z.string(), description: z.string().optional(), role: z.string().optional() },
|
|
340
|
-
async (args) => {
|
|
341
|
-
try {
|
|
342
|
-
const a = registerAgent(args.name, args.description, args.role);
|
|
343
|
-
return text(`${a.id} | ${a.name} | ${a.role}`);
|
|
344
|
-
} catch (e) {
|
|
345
|
-
return errorResult(e);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
server.tool(
|
|
351
|
-
"list_agents",
|
|
352
|
-
"List registered agents.",
|
|
353
|
-
{},
|
|
354
|
-
async () => {
|
|
355
|
-
try {
|
|
356
|
-
const agents = listAgents();
|
|
357
|
-
if (agents.length === 0) return text("None.");
|
|
358
|
-
return text(agents.map((a) => `${a.id} | ${a.name} | ${a.role}`).join("\n"));
|
|
359
|
-
} catch (e) {
|
|
360
|
-
return errorResult(e);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
server.tool(
|
|
366
|
-
"get_agent",
|
|
367
|
-
"Get agent by ID or name.",
|
|
368
|
-
{ id: z.string() },
|
|
369
|
-
async (args) => {
|
|
370
|
-
try {
|
|
371
|
-
const a = getAgent(args.id);
|
|
372
|
-
if (!a) return text(`Not found: ${args.id}`);
|
|
373
|
-
return text(`${a.id} | ${a.name} | ${a.role} | ${a.last_seen_at}`);
|
|
374
|
-
} catch (e) {
|
|
375
|
-
return errorResult(e);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
// ── Project Tools ───────────────────────────────────────────────────────────
|
|
381
|
-
|
|
382
|
-
server.tool(
|
|
383
|
-
"register_project",
|
|
384
|
-
"Register project (idempotent).",
|
|
385
|
-
{ name: z.string(), path: z.string(), description: z.string().optional() },
|
|
386
|
-
async (args) => {
|
|
387
|
-
try {
|
|
388
|
-
const p = registerProject(args.name, args.path, args.description);
|
|
389
|
-
return text(`${p.id.slice(0, 8)} | ${p.name} | ${p.path}`);
|
|
390
|
-
} catch (e) {
|
|
391
|
-
return errorResult(e);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
server.tool(
|
|
397
|
-
"list_projects",
|
|
398
|
-
"List registered projects.",
|
|
399
|
-
{},
|
|
400
|
-
async () => {
|
|
401
|
-
try {
|
|
402
|
-
const projects = listProjects();
|
|
403
|
-
if (projects.length === 0) return text("None.");
|
|
404
|
-
return text(projects.map((p) => `${p.id.slice(0, 8)} | ${p.name} | ${p.path}`).join("\n"));
|
|
405
|
-
} catch (e) {
|
|
406
|
-
return errorResult(e);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
// ── Heartbeat & Focus ───────────────────────────────────────────────────────
|
|
412
|
-
|
|
413
|
-
server.tool(
|
|
414
|
-
"heartbeat",
|
|
415
|
-
"Update last_seen_at to signal agent is active. Call periodically during long tasks.",
|
|
416
|
-
{ agent_id: z.string().describe("Agent ID or name") },
|
|
417
|
-
async (args) => {
|
|
418
|
-
try {
|
|
419
|
-
const agent = heartbeatAgent(args.agent_id);
|
|
420
|
-
if (!agent) return text(`Agent not found: ${args.agent_id}`);
|
|
421
|
-
return text(`${agent.id} | ${agent.name} | last_seen: ${agent.last_seen_at}`);
|
|
422
|
-
} catch (e) {
|
|
423
|
-
return errorResult(e);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
server.tool(
|
|
429
|
-
"set_focus",
|
|
430
|
-
"Set active project context for this agent session.",
|
|
431
|
-
{ agent_id: z.string().describe("Agent ID or name"), project_id: z.string().nullable().optional().describe("Project ID to focus on, or null to clear") },
|
|
432
|
-
async (args) => {
|
|
433
|
-
try {
|
|
434
|
-
const agent = setAgentFocus(args.agent_id, args.project_id ?? null);
|
|
435
|
-
if (!agent) return text(`Agent not found: ${args.agent_id}`);
|
|
436
|
-
return text(args.project_id ? `Focus set: ${args.project_id}` : "Focus cleared");
|
|
437
|
-
} catch (e) {
|
|
438
|
-
return errorResult(e);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
);
|
|
442
|
-
|
|
443
|
-
server.tool(
|
|
444
|
-
"send_feedback",
|
|
445
|
-
"Send feedback about this service",
|
|
446
|
-
{
|
|
447
|
-
message: z.string().describe("Feedback message"),
|
|
448
|
-
email: z.string().optional().describe("Contact email (optional)"),
|
|
449
|
-
category: z.enum(["bug", "feature", "general"]).optional().describe("Feedback category"),
|
|
450
|
-
},
|
|
451
|
-
async (params: { message: string; email?: string; category?: string }) => {
|
|
452
|
-
const adapter = getAdapter();
|
|
453
|
-
const pkg = require("../../package.json");
|
|
454
|
-
adapter.run(
|
|
455
|
-
"INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)",
|
|
456
|
-
params.message, params.email || null, params.category || "general", pkg.version
|
|
457
|
-
);
|
|
458
|
-
return text("Feedback saved. Thank you!");
|
|
459
|
-
}
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
const transport = new StdioServerTransport();
|
|
463
|
-
registerCloudTools(server, "recordings");
|
|
464
|
-
await server.connect(transport);
|