@hasna/recordings 0.1.13 → 0.1.14

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/dist/cli/index.js CHANGED
@@ -7,7 +7,7 @@ var __require = import.meta.require;
7
7
  var require_package = __commonJS((exports, module) => {
8
8
  module.exports = {
9
9
  name: "@hasna/recordings",
10
- version: "0.1.13",
10
+ version: "0.1.14",
11
11
  type: "module",
12
12
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
13
13
  repository: {
@@ -10466,7 +10466,7 @@ async function processText(rawText, config, systemPrompt) {
10466
10466
  }
10467
10467
 
10468
10468
  // src/version.ts
10469
- var VERSION = "0.1.13";
10469
+ var VERSION = "0.1.14";
10470
10470
 
10471
10471
  // src/cli/index.ts
10472
10472
  var program = new Command;
@@ -10792,6 +10792,7 @@ appCommand.command("status").description("Show installed Recordings.app status")
10792
10792
  console.log(`Native sources: ${status.native_sources_available ? "available" : "missing"}`);
10793
10793
  console.log(`Installed app: ${status.installed ? status.installed_app_path : "missing"}`);
10794
10794
  console.log(`Executable: ${status.executable ? "available" : "missing"}`);
10795
+ console.log(`Code hash: ${status.app_code_hash ?? "unavailable"}`);
10795
10796
  if (process.platform === "darwin") {
10796
10797
  console.log(`Microphone: ${status.microphone_permission}`);
10797
10798
  console.log(`Accessibility: ${status.accessibility_permission}`);
@@ -10805,6 +10806,8 @@ appCommand.command("permissions").description("Show macOS permission state for R
10805
10806
  bundle_id: "com.hasna.recordings",
10806
10807
  microphone: status.microphone_permission,
10807
10808
  accessibility: status.accessibility_permission,
10809
+ app_code_hash: status.app_code_hash,
10810
+ ad_hoc_signed: status.ad_hoc_signed,
10808
10811
  log_path: status.log_path
10809
10812
  };
10810
10813
  if (program.opts().json) {
@@ -11316,6 +11319,8 @@ function getMacOSAppStatus() {
11316
11319
  const logPath = pathJoin(home, ".hasna", "recordings", "Recordings.log");
11317
11320
  const installerPath = pathJoin(packageRoot, "scripts", "install_macos_app.sh");
11318
11321
  const nativeSourcesPath = pathJoin(packageRoot, "src", "native", "Recordings");
11322
+ const signingInfo = getCodeSigningInfo(installedAppPath);
11323
+ const permissionCodeHash = signingInfo.adHoc ? signingInfo.cdHash : null;
11319
11324
  return {
11320
11325
  platform: process.platform,
11321
11326
  package_root: packageRoot,
@@ -11327,19 +11332,35 @@ function getMacOSAppStatus() {
11327
11332
  installed: existsSync6(installedAppPath),
11328
11333
  executable_path: executablePath,
11329
11334
  executable: existsSync6(executablePath),
11330
- microphone_permission: getTccPermission("kTCCServiceMicrophone", home),
11331
- accessibility_permission: getTccPermission("kTCCServiceAccessibility", home),
11335
+ app_code_hash: signingInfo.cdHash,
11336
+ ad_hoc_signed: signingInfo.adHoc,
11337
+ microphone_permission: getTccPermission("kTCCServiceMicrophone", home, permissionCodeHash),
11338
+ accessibility_permission: getTccPermission("kTCCServiceAccessibility", home, permissionCodeHash),
11332
11339
  log_path: logPath
11333
11340
  };
11334
11341
  }
11335
- function getTccPermission(service, home) {
11342
+ function getCodeSigningInfo(appPath) {
11343
+ if (process.platform !== "darwin" || !existsSync6(appPath)) {
11344
+ return { cdHash: null, adHoc: false };
11345
+ }
11346
+ const result = spawnSync("codesign", ["-d", "--verbose=4", appPath], {
11347
+ encoding: "utf8",
11348
+ stdio: ["ignore", "pipe", "pipe"]
11349
+ });
11350
+ const output = `${result.stdout}
11351
+ ${result.stderr}`;
11352
+ const cdHash = output.match(/^CDHash=([a-fA-F0-9]+)/m)?.[1]?.toLowerCase() ?? null;
11353
+ const adHoc = /Signature=adhoc/.test(output);
11354
+ return { cdHash, adHoc };
11355
+ }
11356
+ function getTccPermission(service, home, currentCodeHash) {
11336
11357
  if (process.platform !== "darwin")
11337
11358
  return "unsupported";
11338
11359
  const dbPaths = [
11339
11360
  pathJoin(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
11340
11361
  pathJoin("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
11341
11362
  ];
11342
- const sql = "select auth_value from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
11363
+ const sql = "select auth_value || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
11343
11364
  for (const dbPath of dbPaths) {
11344
11365
  if (!existsSync6(dbPath))
11345
11366
  continue;
@@ -11348,8 +11369,14 @@ function getTccPermission(service, home) {
11348
11369
  stdio: ["ignore", "pipe", "ignore"]
11349
11370
  });
11350
11371
  const value = result.stdout.trim();
11351
- if (value)
11352
- return tccAuthValueLabel(value);
11372
+ if (!value)
11373
+ continue;
11374
+ const [authValue, csreqHex = ""] = value.split("|");
11375
+ const label = tccAuthValueLabel(authValue ?? "");
11376
+ if (label === "allowed" && currentCodeHash && csreqHex && !csreqHex.toLowerCase().includes(currentCodeHash.toLowerCase())) {
11377
+ return "stale_allowed_for_previous_app_build";
11378
+ }
11379
+ return label;
11353
11380
  }
11354
11381
  return "not_determined";
11355
11382
  }
package/dist/mcp/index.js CHANGED
@@ -21,7 +21,7 @@ var __require = import.meta.require;
21
21
  var require_package = __commonJS((exports, module) => {
22
22
  module.exports = {
23
23
  name: "@hasna/recordings",
24
- version: "0.1.13",
24
+ version: "0.1.14",
25
25
  type: "module",
26
26
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
27
27
  repository: {
@@ -14983,7 +14983,7 @@ async function processText(rawText, config, systemPrompt) {
14983
14983
  }
14984
14984
 
14985
14985
  // src/version.ts
14986
- var VERSION = "0.1.13";
14986
+ var VERSION = "0.1.14";
14987
14987
 
14988
14988
  // src/mcp/index.ts
14989
14989
  var config = loadConfig();
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.13";
1
+ export declare const VERSION = "0.1.14";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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
  "repository": {
@@ -73,4 +73,36 @@ rm -rf "$APP_DEST"
73
73
  mkdir -p "$DATA_DIR"
74
74
  cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
75
75
 
76
+ current_cdhash() {
77
+ codesign -d --verbose=4 "$1" 2>&1 | awk -F= '/^CDHash=/ { print toupper($2); exit }'
78
+ }
79
+
80
+ tcc_csreq_hex() {
81
+ local db_path="$1"
82
+ local service="$2"
83
+ if [ ! -r "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
84
+ return 0
85
+ fi
86
+ sqlite3 "$db_path" \
87
+ "SELECT hex(csreq) FROM access WHERE service = '${service}' AND client = 'com.hasna.recordings' ORDER BY last_modified DESC LIMIT 1;" \
88
+ 2>/dev/null || true
89
+ }
90
+
91
+ reset_stale_permission() {
92
+ local service="$1"
93
+ local tcc_service="$2"
94
+ local db_path="$3"
95
+ local cdhash="$4"
96
+ local csreq_hex
97
+ csreq_hex="$(tcc_csreq_hex "$db_path" "$tcc_service" | tr '[:lower:]' '[:upper:]')"
98
+ if [ -n "$cdhash" ] && [ -n "$csreq_hex" ] && [[ "$csreq_hex" != *"$cdhash"* ]]; then
99
+ tccutil reset "$service" com.hasna.recordings >/dev/null 2>&1 || true
100
+ echo "Reset stale ${service} permission for the newly installed Recordings.app."
101
+ fi
102
+ }
103
+
104
+ APP_CDHASH="$(current_cdhash "$APP_DEST" || true)"
105
+ reset_stale_permission "Microphone" "kTCCServiceMicrophone" "${HOME}/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
106
+ reset_stale_permission "Accessibility" "kTCCServiceAccessibility" "/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
107
+
76
108
  echo "Installed Recordings.app from package: ${APP_DEST}"
@@ -1,5 +1,5 @@
1
1
  import SwiftUI
2
- import KeyboardShortcuts
2
+ @preconcurrency import KeyboardShortcuts
3
3
 
4
4
  public struct MenuBarPopover: View {
5
5
  @ObservedObject public var engine: RecordingEngine
@@ -121,6 +121,9 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
121
121
  public func finish(timeoutMilliseconds: UInt64 = 1_800) async -> String {
122
122
  guard isStreaming else { return accumulatedText }
123
123
  let initialCompletedCount = completedEventCount
124
+ if initialCompletedCount > 0 {
125
+ return stop()
126
+ }
124
127
  await commitInput()
125
128
 
126
129
  let deadline = Date().addingTimeInterval(TimeInterval(timeoutMilliseconds) / 1_000)
@@ -127,6 +127,7 @@ public final class RecordingEngine: ObservableObject {
127
127
  private var streamingText = ""
128
128
  private var recordedPCM = Data()
129
129
  private var activeAudioPath: String?
130
+ private var lastAccessibilityPromptAt: Date?
130
131
 
131
132
  // fn key monitor (CGEventTap-based, swallows fn to prevent emoji picker)
132
133
  private let fnMonitor = FnKeyMonitor()
@@ -638,7 +639,7 @@ public final class RecordingEngine: ObservableObject {
638
639
  // MARK: - Command Mode
639
640
 
640
641
  private func runCommandMode(instruction: String) {
641
- guard ensureAccessibilityPermission(prompt: true) else {
642
+ guard ensureAccessibilityPermission(prompt: shouldPromptAccessibility()) else {
642
643
  log("command mode blocked by accessibility permission")
643
644
  statusMessage = "Enable Accessibility permission for Recordings to rewrite selected text"
644
645
  return
@@ -704,9 +705,12 @@ public final class RecordingEngine: ObservableObject {
704
705
  pb.clearContents()
705
706
  pb.setString(text, forType: .string)
706
707
 
707
- guard ensureAccessibilityPermission(prompt: true) else {
708
+ let prompted = shouldPromptAccessibility()
709
+ guard ensureAccessibilityPermission(prompt: prompted) else {
708
710
  log("paste blocked by accessibility permission; copied to clipboard")
709
- self.statusMessage = "Copied — enable Accessibility permission for Recordings to paste"
711
+ self.statusMessage = prompted
712
+ ? "Copied — approve Accessibility for this Recordings app"
713
+ : "Copied — waiting for Accessibility approval"
710
714
  return
711
715
  }
712
716
 
@@ -744,14 +748,27 @@ public final class RecordingEngine: ObservableObject {
744
748
  }
745
749
 
746
750
  private func ensureAccessibilityPermission(prompt: Bool) -> Bool {
751
+ if AXIsProcessTrusted() {
752
+ return true
753
+ }
747
754
  if !prompt {
748
- return AXIsProcessTrusted()
755
+ return false
749
756
  }
750
757
  return AXIsProcessTrustedWithOptions(
751
758
  ["AXTrustedCheckOptionPrompt" as CFString: true] as CFDictionary
752
759
  )
753
760
  }
754
761
 
762
+ private func shouldPromptAccessibility() -> Bool {
763
+ let now = Date()
764
+ if let lastAccessibilityPromptAt,
765
+ now.timeIntervalSince(lastAccessibilityPromptAt) < 20 {
766
+ return false
767
+ }
768
+ lastAccessibilityPromptAt = now
769
+ return true
770
+ }
771
+
755
772
  private func log(_ message: String) {
756
773
  NativeAppLog.write(message, homePath: home)
757
774
  }
@@ -1,5 +1,5 @@
1
1
  import SwiftUI
2
- import KeyboardShortcuts
2
+ @preconcurrency import KeyboardShortcuts
3
3
 
4
4
  public struct SettingsView: View {
5
5
  @ObservedObject public var engine: RecordingEngine
@@ -45,6 +45,32 @@ public struct SettingsView: View {
45
45
  .foregroundStyle(.secondary)
46
46
  }
47
47
 
48
+ Section("Permissions") {
49
+ HStack {
50
+ Text("Microphone")
51
+ Spacer()
52
+ Text(engine.microphonePermissionLabel)
53
+ .foregroundStyle(.secondary)
54
+ }
55
+ Button("Request Microphone") {
56
+ engine.requestMicrophonePermission()
57
+ }
58
+ HStack {
59
+ Text("Accessibility")
60
+ Spacer()
61
+ Text(engine.accessibilityPermissionLabel)
62
+ .foregroundStyle(.secondary)
63
+ }
64
+ HStack {
65
+ Button("Request Accessibility") {
66
+ engine.requestAccessibilityPermission()
67
+ }
68
+ Button("Open Accessibility Settings") {
69
+ engine.openAccessibilitySettings()
70
+ }
71
+ }
72
+ }
73
+
48
74
  Section("System Prompt") {
49
75
  TextEditor(text: $projectStore.settings.globalSystemPrompt)
50
76
  .frame(height: 80)