@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.7",
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
+ }
@@ -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
- _db = new Database(path, { create: true });
105
+ _adapter = new SqliteAdapter(path);
106
+ _db = _adapter.raw;
86
107
 
87
- // Pragmas for production SQLite
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
- // ── Start Server ────────────────────────────────────────────────────────────
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
- "register_agent",
415
- "Register this agent session. Returns agent_id for use in heartbeat/set_focus.",
416
- { name: z.string(), session_id: z.string().optional() },
417
- async (a: { name: string; session_id?: string }) => {
418
- const existing = [..._agentReg.values()].find(x => x.name === a.name);
419
- if (existing) { existing.last_seen_at = new Date().toISOString(); return { content: [{ type: "text" as const, text: JSON.stringify(existing) }] }; }
420
- const id = Math.random().toString(36).slice(2, 10);
421
- const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
422
- _agentReg.set(id, ag);
423
- return { content: [{ type: "text" as const, text: JSON.stringify(ag) }] };
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
- "heartbeat",
429
- "Update last_seen_at to signal agent is active.",
430
- { agent_id: z.string() },
431
- async (a: { agent_id: string }) => {
432
- const ag = _agentReg.get(a.agent_id);
433
- if (!ag) return { content: [{ type: "text" as const, text: `Agent not found: ${a.agent_id}` }], isError: true };
434
- ag.last_seen_at = new Date().toISOString();
435
- return { content: [{ type: "text" as const, text: `♥ ${ag.name} active` }] };
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
- "set_focus",
441
- "Set active project context for this agent session.",
442
- { agent_id: z.string(), project_id: z.string().optional() },
443
- async (a: { agent_id: string; project_id?: string }) => {
444
- const ag = _agentReg.get(a.agent_id);
445
- if (!ag) return { content: [{ type: "text" as const, text: `Agent not found: ${a.agent_id}` }], isError: true };
446
- (ag as any).project_id = a.project_id;
447
- return { content: [{ type: "text" as const, text: a.project_id ? `Focus: ${a.project_id}` : "Focus cleared" }] };
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