@hasna/recordings 0.1.6 → 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 +9132 -143
- 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/db/recordings.d.ts.map +1 -1
- package/dist/index.js +8952 -27
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/mcp/index.js +9199 -203
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/config.test.ts +1 -1
- package/src/cli/index.ts +29 -9
- package/src/db/agents.ts +23 -0
- package/src/db/database.ts +33 -4
- package/src/db/pg-migrations.ts +82 -0
- package/src/lib/config.ts +22 -5
- package/src/mcp/index.ts +43 -33
- package/src/native/Recordings/Recordings/RecordingEngine.swift +1 -1
- package/src/native/Recordings/Recordings/VoiceShortcuts.swift +1 -1
- package/src/native/Recordings/build.sh +2 -2
- package/src/native/RecordingsHelper.swift +9 -2
package/dist/types/index.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface Recording {
|
|
|
12
12
|
agent_id: string | null;
|
|
13
13
|
project_id: string | null;
|
|
14
14
|
session_id: string | null;
|
|
15
|
+
goal: string | null;
|
|
16
|
+
role: string | null;
|
|
17
|
+
task_list_id: string | null;
|
|
15
18
|
metadata: Record<string, unknown>;
|
|
16
19
|
created_at: string;
|
|
17
20
|
}
|
|
@@ -29,6 +32,9 @@ export interface CreateRecordingInput {
|
|
|
29
32
|
agent_id?: string;
|
|
30
33
|
project_id?: string;
|
|
31
34
|
session_id?: string;
|
|
35
|
+
goal?: string;
|
|
36
|
+
role?: string;
|
|
37
|
+
task_list_id?: string;
|
|
32
38
|
metadata?: Record<string, unknown>;
|
|
33
39
|
}
|
|
34
40
|
export interface RecordingFilter {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,cAAc,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,UAAU,CAAC;AAEhD,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAID,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAID,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAID,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,EAAE,EAAE,MAAM;CAIvB;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
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,
|
|
@@ -485,24 +485,27 @@ program
|
|
|
485
485
|
const { existsSync: fileExists } = require("node:fs") as typeof import("node:fs");
|
|
486
486
|
const home = getHome();
|
|
487
487
|
|
|
488
|
-
const appPath = pathJoin(home, ".recordings", "RecordingsHelper.app");
|
|
488
|
+
const appPath = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app");
|
|
489
|
+
const oldAppPath = pathJoin(home, ".recordings", "RecordingsHelper.app");
|
|
489
490
|
|
|
490
|
-
if (!fileExists(appPath)) {
|
|
491
|
+
if (!fileExists(appPath) && !fileExists(oldAppPath)) {
|
|
491
492
|
console.error(chalk.red("RecordingsHelper.app not found. Run: recordings shortcut --install"));
|
|
492
493
|
process.exit(1);
|
|
493
494
|
}
|
|
494
495
|
|
|
496
|
+
const resolvedAppPath = fileExists(appPath) ? appPath : oldAppPath;
|
|
497
|
+
|
|
495
498
|
// Kill existing instance
|
|
496
499
|
try { execSync("pkill -f RecordingsHelper", { stdio: "pipe" }); } catch { /* not running */ }
|
|
497
500
|
|
|
498
501
|
// Launch
|
|
499
|
-
execSync(`open "${
|
|
502
|
+
execSync(`open "${resolvedAppPath}"`, { stdio: "pipe" });
|
|
500
503
|
console.log(chalk.green("Recordings helper launched — press F5 to record"));
|
|
501
504
|
|
|
502
505
|
if (opts.login) {
|
|
503
506
|
try {
|
|
504
507
|
execSync(
|
|
505
|
-
`osascript -e 'tell application "System Events" to make login item at end with properties {path:"${
|
|
508
|
+
`osascript -e 'tell application "System Events" to make login item at end with properties {path:"${resolvedAppPath}", hidden:true}'`,
|
|
506
509
|
{ stdio: "pipe" }
|
|
507
510
|
);
|
|
508
511
|
console.log(chalk.green("Added to Login Items — will start on boot"));
|
|
@@ -688,7 +691,7 @@ program
|
|
|
688
691
|
const { homedir: getHome } = require("node:os") as typeof import("node:os");
|
|
689
692
|
const home = getHome();
|
|
690
693
|
|
|
691
|
-
const scriptDir = pathJoin(home, ".recordings");
|
|
694
|
+
const scriptDir = pathJoin(home, ".hasna", "recordings");
|
|
692
695
|
mkdirSync(scriptDir, { recursive: true });
|
|
693
696
|
|
|
694
697
|
const scriptPath = pathJoin(scriptDir, "record-toggle.sh");
|
|
@@ -747,7 +750,7 @@ fi
|
|
|
747
750
|
// Install the native menu bar app — no config needed, just works with F5
|
|
748
751
|
const { execSync: exec } = require("node:child_process") as typeof import("node:child_process");
|
|
749
752
|
|
|
750
|
-
const appPath = pathJoin(home, ".recordings", "RecordingsHelper.app");
|
|
753
|
+
const appPath = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app");
|
|
751
754
|
const srcSwift = pathJoin(__dirname, "..", "native", "RecordingsHelper.swift");
|
|
752
755
|
const distApp = pathJoin(__dirname, "..", "RecordingsHelper.app");
|
|
753
756
|
|
|
@@ -757,10 +760,10 @@ fi
|
|
|
757
760
|
} else if (fileExists(srcSwift)) {
|
|
758
761
|
// Compile from source
|
|
759
762
|
console.log(chalk.blue("Compiling native helper app..."));
|
|
760
|
-
const appDir = pathJoin(home, ".recordings", "RecordingsHelper.app", "Contents", "MacOS");
|
|
763
|
+
const appDir = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app", "Contents", "MacOS");
|
|
761
764
|
mkdirSync(appDir, { recursive: true });
|
|
762
765
|
|
|
763
|
-
const plistDir = pathJoin(home, ".recordings", "RecordingsHelper.app", "Contents");
|
|
766
|
+
const plistDir = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app", "Contents");
|
|
764
767
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
765
768
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
766
769
|
<plist version="1.0"><dict>
|
|
@@ -1091,6 +1094,23 @@ program
|
|
|
1091
1094
|
}
|
|
1092
1095
|
});
|
|
1093
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
|
+
|
|
1094
1114
|
// ── Run ─────────────────────────────────────────────────────────────────────
|
|
1095
1115
|
|
|
1096
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/lib/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, cpSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import type { RecordingsConfig } from "../types/index.js";
|
|
@@ -67,7 +67,9 @@ export function loadConfig(configPath?: string): RecordingsConfig {
|
|
|
67
67
|
if (process.env.RECORDINGS_LANGUAGE) {
|
|
68
68
|
config.language = process.env.RECORDINGS_LANGUAGE;
|
|
69
69
|
}
|
|
70
|
-
if (process.env.
|
|
70
|
+
if (process.env.HASNA_RECORDINGS_DB_PATH) {
|
|
71
|
+
config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
|
|
72
|
+
} else if (process.env.RECORDINGS_DB_PATH) {
|
|
71
73
|
config.db_path = process.env.RECORDINGS_DB_PATH;
|
|
72
74
|
}
|
|
73
75
|
if (process.env.RECORDINGS_AUDIO_DIR) {
|
|
@@ -115,7 +117,7 @@ function findConfigFile(): string | null {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
export function getDataDir(): string {
|
|
118
|
-
// Check for .recordings in cwd hierarchy
|
|
120
|
+
// Check for .recordings in cwd hierarchy (project-local)
|
|
119
121
|
let dir = process.cwd();
|
|
120
122
|
const root = "/";
|
|
121
123
|
while (dir !== root) {
|
|
@@ -125,8 +127,23 @@ export function getDataDir(): string {
|
|
|
125
127
|
if (parent === dir) break;
|
|
126
128
|
dir = parent;
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
|
|
131
|
+
// Global: ~/.hasna/recordings (with backward compat from ~/.recordings)
|
|
132
|
+
const home = homedir();
|
|
133
|
+
const newDir = join(home, ".hasna", "recordings");
|
|
134
|
+
const oldDir = join(home, ".recordings");
|
|
135
|
+
|
|
136
|
+
// Auto-migrate from old location if new dir doesn't exist yet
|
|
137
|
+
if (!existsSync(newDir) && existsSync(oldDir)) {
|
|
138
|
+
try {
|
|
139
|
+
mkdirSync(join(home, ".hasna"), { recursive: true });
|
|
140
|
+
cpSync(oldDir, newDir, { recursive: true });
|
|
141
|
+
} catch {
|
|
142
|
+
// Fall through to use new dir
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return newDir;
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
function loadSecretKey(keyName: string): string {
|
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
|
|
|
@@ -69,7 +69,7 @@ final class RecordingEngine: ObservableObject {
|
|
|
69
69
|
private let fnMonitor = FnKeyMonitor()
|
|
70
70
|
|
|
71
71
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
72
|
-
private var audioDir: String { "\(home)/.recordings/audio" }
|
|
72
|
+
private var audioDir: String { "\(home)/.hasna/recordings/audio" }
|
|
73
73
|
|
|
74
74
|
init() {
|
|
75
75
|
try? FileManager.default.createDirectory(atPath: audioDir, withIntermediateDirectories: true)
|
|
@@ -22,7 +22,7 @@ final class VoiceShortcuts: ObservableObject {
|
|
|
22
22
|
|
|
23
23
|
private let storageURL: URL = {
|
|
24
24
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
|
25
|
-
return home.appendingPathComponent(".recordings/voice-shortcuts.json")
|
|
25
|
+
return home.appendingPathComponent(".hasna/recordings/voice-shortcuts.json")
|
|
26
26
|
}()
|
|
27
27
|
|
|
28
28
|
init() {
|
|
@@ -33,8 +33,8 @@ fi
|
|
|
33
33
|
|
|
34
34
|
echo "✓ Built $APP_DIR"
|
|
35
35
|
echo ""
|
|
36
|
-
echo "To install to ~/.recordings/:"
|
|
37
|
-
echo " cp -r $APP_DIR ~/.recordings/Recordings.app"
|
|
36
|
+
echo "To install to ~/.hasna/recordings/:"
|
|
37
|
+
echo " cp -r $APP_DIR ~/.hasna/recordings/Recordings.app"
|
|
38
38
|
echo ""
|
|
39
39
|
echo "To run:"
|
|
40
40
|
echo " open $APP_DIR"
|
|
@@ -18,12 +18,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
18
18
|
|
|
19
19
|
let recordingsDir: String = {
|
|
20
20
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
21
|
-
|
|
21
|
+
let newDir = "\(home)/.hasna/recordings"
|
|
22
|
+
let oldDir = "\(home)/.recordings"
|
|
23
|
+
// Auto-migrate from old location
|
|
24
|
+
if !FileManager.default.fileExists(atPath: newDir) && FileManager.default.fileExists(atPath: oldDir) {
|
|
25
|
+
try? FileManager.default.createDirectory(atPath: "\(home)/.hasna", withIntermediateDirectories: true)
|
|
26
|
+
try? FileManager.default.copyItem(atPath: oldDir, toPath: newDir)
|
|
27
|
+
}
|
|
28
|
+
return newDir
|
|
22
29
|
}()
|
|
23
30
|
|
|
24
31
|
let audioDir: String = {
|
|
25
32
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
26
|
-
return "\(home)/.recordings/audio"
|
|
33
|
+
return "\(home)/.hasna/recordings/audio"
|
|
27
34
|
}()
|
|
28
35
|
|
|
29
36
|
let recordingsBin: String = {
|