@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 +35 -8
- package/dist/mcp/index.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/scripts/install_macos_app.sh +32 -0
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +1 -1
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +3 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +21 -4
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +27 -1
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.
|
|
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.
|
|
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
|
-
|
|
11331
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.1.14";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -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}"
|
|
@@ -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:
|
|
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
|
-
|
|
708
|
+
let prompted = shouldPromptAccessibility()
|
|
709
|
+
guard ensureAccessibilityPermission(prompt: prompted) else {
|
|
708
710
|
log("paste blocked by accessibility permission; copied to clipboard")
|
|
709
|
-
self.statusMessage =
|
|
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
|
|
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)
|