@hasna/recordings 0.1.7 → 0.1.8
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/bun.lock +31 -0
- package/dist/cli/index.js +9101 -134
- package/dist/db/agents.d.ts +2 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/database.d.ts +3 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts +7 -0
- package/dist/db/pg-migrations.d.ts.map +1 -0
- package/dist/index.js +8926 -21
- package/dist/mcp/index.js +9183 -198
- package/package.json +2 -1
- package/src/cli/index.ts +18 -1
- package/src/db/agents.ts +23 -0
- package/src/db/database.ts +33 -4
- package/src/db/pg-migrations.ts +82 -0
- package/src/mcp/index.ts +43 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/recordings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dev:mcp": "bun run src/mcp/index.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@hasna/cloud": "^0.1.0",
|
|
30
31
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
31
32
|
"chalk": "^5.4.1",
|
|
32
33
|
"commander": "^13.1.0",
|
package/src/cli/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { loadConfig, ensureDataDir } from "../lib/config.js";
|
|
5
|
-
import { getDatabase } from "../db/database.js";
|
|
5
|
+
import { getDatabase, getAdapter } from "../db/database.js";
|
|
6
6
|
import {
|
|
7
7
|
createRecording,
|
|
8
8
|
getRecording,
|
|
@@ -1094,6 +1094,23 @@ program
|
|
|
1094
1094
|
}
|
|
1095
1095
|
});
|
|
1096
1096
|
|
|
1097
|
+
// ── Feedback ────────────────────────────────────────────────────────────────
|
|
1098
|
+
|
|
1099
|
+
program
|
|
1100
|
+
.command("feedback <message>")
|
|
1101
|
+
.description("Send feedback")
|
|
1102
|
+
.option("--email <email>", "Contact email")
|
|
1103
|
+
.option("--category <category>", "Category: bug, feature, general")
|
|
1104
|
+
.action((message: string, opts: { email?: string; category?: string }) => {
|
|
1105
|
+
const adapter = getAdapter();
|
|
1106
|
+
const pkg = require("../../package.json");
|
|
1107
|
+
adapter.run(
|
|
1108
|
+
"INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)",
|
|
1109
|
+
message, opts.email || null, opts.category || "general", pkg.version
|
|
1110
|
+
);
|
|
1111
|
+
console.log(chalk.green("Feedback saved. Thank you!"));
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1097
1114
|
// ── Run ─────────────────────────────────────────────────────────────────────
|
|
1098
1115
|
|
|
1099
1116
|
program.parse();
|
package/src/db/agents.ts
CHANGED
|
@@ -79,3 +79,26 @@ export function listAgents(db?: Database): Agent[] {
|
|
|
79
79
|
.all() as Record<string, unknown>[];
|
|
80
80
|
return rows.map(parseAgent);
|
|
81
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
|
+
}
|
package/src/db/database.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { SqliteAdapter } from "@hasna/cloud";
|
|
2
3
|
import { mkdirSync } from "fs";
|
|
3
4
|
import { dirname } from "path";
|
|
4
5
|
import { loadConfig } from "../lib/config.js";
|
|
5
6
|
|
|
6
7
|
let _db: Database | null = null;
|
|
8
|
+
let _adapter: SqliteAdapter | null = null;
|
|
7
9
|
|
|
8
10
|
const MIGRATIONS = [
|
|
9
11
|
// Migration 0: Initial schema
|
|
@@ -71,6 +73,24 @@ const MIGRATIONS = [
|
|
|
71
73
|
ALTER TABLE recordings ADD COLUMN task_list_id TEXT;
|
|
72
74
|
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
73
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
|
+
`,
|
|
74
94
|
];
|
|
75
95
|
|
|
76
96
|
export function getDatabase(dbPath?: string): Database {
|
|
@@ -82,12 +102,11 @@ export function getDatabase(dbPath?: string): Database {
|
|
|
82
102
|
const dir = dirname(path);
|
|
83
103
|
mkdirSync(dir, { recursive: true });
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
_adapter = new SqliteAdapter(path);
|
|
106
|
+
_db = _adapter.raw;
|
|
86
107
|
|
|
87
|
-
//
|
|
88
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
108
|
+
// SqliteAdapter already sets WAL and foreign_keys; add busy_timeout
|
|
89
109
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
90
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
91
110
|
|
|
92
111
|
runMigrations(_db);
|
|
93
112
|
return _db;
|
|
@@ -118,11 +137,21 @@ export function closeDatabase(): void {
|
|
|
118
137
|
if (_db) {
|
|
119
138
|
_db.close();
|
|
120
139
|
_db = null;
|
|
140
|
+
_adapter = null;
|
|
121
141
|
}
|
|
122
142
|
}
|
|
123
143
|
|
|
124
144
|
export function resetDatabase(): void {
|
|
125
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!;
|
|
126
155
|
}
|
|
127
156
|
|
|
128
157
|
export function getDbPath(): string {
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
];
|
package/src/mcp/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { loadConfig, ensureDataDir } from "../lib/config.js";
|
|
6
|
-
import { getDatabase } from "../db/database.js";
|
|
6
|
+
import { getDatabase, getAdapter } from "../db/database.js";
|
|
7
7
|
import {
|
|
8
8
|
createRecording,
|
|
9
9
|
getRecording,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
searchRecordings,
|
|
13
13
|
getRecordingStats,
|
|
14
14
|
} from "../db/recordings.js";
|
|
15
|
-
import { registerAgent, getAgent, listAgents } from "../db/agents.js";
|
|
15
|
+
import { registerAgent, getAgent, listAgents, heartbeatAgent, setAgentFocus } from "../db/agents.js";
|
|
16
16
|
import {
|
|
17
17
|
registerProject,
|
|
18
18
|
getProject,
|
|
@@ -77,9 +77,11 @@ const toolDocs: Record<string, string> = {
|
|
|
77
77
|
delete_recording: "Delete recording by ID.\nParams: id (string, required)",
|
|
78
78
|
recording_stats: "Recording count, mode breakdown, duration.\nParams: none",
|
|
79
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)",
|
|
80
|
+
register_agent: "Register agent (idempotent). Auto-updates last_seen_at on re-register.\nParams: name (string, required) | description (string) | role (string)",
|
|
81
81
|
list_agents: "List registered agents.\nParams: none",
|
|
82
82
|
get_agent: "Get agent by ID or name.\nParams: id (string, required)",
|
|
83
|
+
heartbeat: "Update last_seen_at to signal agent is active.\nParams: agent_id (string, required): agent ID or name",
|
|
84
|
+
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",
|
|
83
85
|
register_project: "Register project (idempotent).\nParams: name (string, required) | path (string, required): absolute path | description (string)",
|
|
84
86
|
list_projects: "List registered projects.\nParams: none",
|
|
85
87
|
};
|
|
@@ -405,46 +407,54 @@ server.tool(
|
|
|
405
407
|
}
|
|
406
408
|
);
|
|
407
409
|
|
|
408
|
-
// ──
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const _agentReg = new Map<string, { id: string; name: string; last_seen_at: string }>();
|
|
410
|
+
// ── Heartbeat & Focus ───────────────────────────────────────────────────────
|
|
412
411
|
|
|
413
412
|
server.tool(
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
{
|
|
417
|
-
async (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
413
|
+
"heartbeat",
|
|
414
|
+
"Update last_seen_at to signal agent is active. Call periodically during long tasks.",
|
|
415
|
+
{ agent_id: z.string().describe("Agent ID or name") },
|
|
416
|
+
async (args) => {
|
|
417
|
+
try {
|
|
418
|
+
const agent = heartbeatAgent(args.agent_id);
|
|
419
|
+
if (!agent) return text(`Agent not found: ${args.agent_id}`);
|
|
420
|
+
return text(`${agent.id} | ${agent.name} | last_seen: ${agent.last_seen_at}`);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
return errorResult(e);
|
|
423
|
+
}
|
|
424
424
|
}
|
|
425
425
|
);
|
|
426
426
|
|
|
427
427
|
server.tool(
|
|
428
|
-
"
|
|
429
|
-
"
|
|
430
|
-
{ agent_id: z.string() },
|
|
431
|
-
async (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
428
|
+
"set_focus",
|
|
429
|
+
"Set active project context for this agent session.",
|
|
430
|
+
{ 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") },
|
|
431
|
+
async (args) => {
|
|
432
|
+
try {
|
|
433
|
+
const agent = setAgentFocus(args.agent_id, args.project_id ?? null);
|
|
434
|
+
if (!agent) return text(`Agent not found: ${args.agent_id}`);
|
|
435
|
+
return text(args.project_id ? `Focus set: ${args.project_id}` : "Focus cleared");
|
|
436
|
+
} catch (e) {
|
|
437
|
+
return errorResult(e);
|
|
438
|
+
}
|
|
436
439
|
}
|
|
437
440
|
);
|
|
438
441
|
|
|
439
442
|
server.tool(
|
|
440
|
-
"
|
|
441
|
-
"
|
|
442
|
-
{
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
443
|
+
"send_feedback",
|
|
444
|
+
"Send feedback about this service",
|
|
445
|
+
{
|
|
446
|
+
message: z.string().describe("Feedback message"),
|
|
447
|
+
email: z.string().optional().describe("Contact email (optional)"),
|
|
448
|
+
category: z.enum(["bug", "feature", "general"]).optional().describe("Feedback category"),
|
|
449
|
+
},
|
|
450
|
+
async (params: { message: string; email?: string; category?: string }) => {
|
|
451
|
+
const adapter = getAdapter();
|
|
452
|
+
const pkg = require("../../package.json");
|
|
453
|
+
adapter.run(
|
|
454
|
+
"INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)",
|
|
455
|
+
params.message, params.email || null, params.category || "general", pkg.version
|
|
456
|
+
);
|
|
457
|
+
return text("Feedback saved. Thank you!");
|
|
448
458
|
}
|
|
449
459
|
);
|
|
450
460
|
|