@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.
@@ -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":"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"}
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.6",
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",
@@ -207,7 +207,7 @@ describe("getDataDir", () => {
207
207
  test("returns a string", () => {
208
208
  const dir = getDataDir();
209
209
  expect(typeof dir).toBe("string");
210
- expect(dir).toContain(".recordings");
210
+ expect(dir).toContain("recordings");
211
211
  });
212
212
  });
213
213
 
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 "${appPath}"`, { stdio: "pipe" });
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:"${appPath}", hidden:true}'`,
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
+ }
@@ -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/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.RECORDINGS_DB_PATH) {
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
- // Fall back to ~/.recordings
129
- return join(homedir(), ".recordings");
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
- // ── 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
 
@@ -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
- return "\(home)/.recordings"
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 = {