@hasna/recordings 0.1.11 → 0.1.13

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.
Files changed (69) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/index.js +395 -37
  3. package/dist/db/database.d.ts.map +1 -1
  4. package/dist/db/recordings.d.ts.map +1 -1
  5. package/dist/index.js +78 -11
  6. package/dist/lib/config.d.ts.map +1 -1
  7. package/dist/lib/enhancer.d.ts.map +1 -1
  8. package/dist/lib/recorder.d.ts.map +1 -1
  9. package/dist/lib/transcriber.d.ts +8 -2
  10. package/dist/lib/transcriber.d.ts.map +1 -1
  11. package/dist/mcp/index.js +147 -30
  12. package/dist/types/index.d.ts +3 -0
  13. package/dist/types/index.d.ts.map +1 -1
  14. package/dist/version.d.ts +2 -0
  15. package/dist/version.d.ts.map +1 -0
  16. package/package.json +20 -3
  17. package/scripts/install_macos_app.sh +76 -0
  18. package/src/native/Recordings/{Recordings → App}/RecordingsApp.swift +5 -1
  19. package/src/native/Recordings/Package.resolved +21 -3
  20. package/src/native/Recordings/Package.swift +16 -5
  21. package/src/native/Recordings/{Recordings → RecordingsLib}/Info.plist +6 -0
  22. package/src/native/Recordings/{Recordings → RecordingsLib}/MenuBarPopover.swift +55 -11
  23. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +36 -0
  24. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +164 -0
  25. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +113 -0
  26. package/src/native/Recordings/{Recordings → RecordingsLib}/ProjectStore.swift +11 -11
  27. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +383 -0
  28. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +835 -0
  29. package/src/native/Recordings/{Recordings → RecordingsLib}/SettingsView.swift +19 -5
  30. package/src/native/Recordings/{Recordings → RecordingsLib}/VoiceShortcuts.swift +12 -8
  31. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +63 -0
  32. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +23 -0
  33. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +33 -0
  34. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +92 -0
  35. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +58 -0
  36. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +113 -0
  37. package/src/native/Recordings/build.sh +6 -6
  38. package/.takumi/settings.local.json +0 -7
  39. package/bun.lock +0 -250
  40. package/bunfig.toml +0 -2
  41. package/src/__tests__/agents.test.ts +0 -136
  42. package/src/__tests__/config.test.ts +0 -252
  43. package/src/__tests__/database.test.ts +0 -167
  44. package/src/__tests__/enhancer.test.ts +0 -639
  45. package/src/__tests__/preload.ts +0 -4
  46. package/src/__tests__/projects.test.ts +0 -109
  47. package/src/__tests__/recorder.test.ts +0 -278
  48. package/src/__tests__/recordings.test.ts +0 -353
  49. package/src/__tests__/transcriber.test.ts +0 -322
  50. package/src/__tests__/types.test.ts +0 -75
  51. package/src/cli/index.ts +0 -988
  52. package/src/db/agents.ts +0 -104
  53. package/src/db/database.ts +0 -163
  54. package/src/db/pg-migrations.ts +0 -82
  55. package/src/db/projects.ts +0 -71
  56. package/src/db/recordings.ts +0 -225
  57. package/src/index.ts +0 -81
  58. package/src/lib/config.ts +0 -223
  59. package/src/lib/enhancer.ts +0 -173
  60. package/src/lib/recorder.ts +0 -198
  61. package/src/lib/transcriber.ts +0 -105
  62. package/src/mcp/index.ts +0 -464
  63. package/src/native/Recordings/Recordings/RecordingEngine.swift +0 -455
  64. package/src/native/Recordings/test_fn.swift +0 -79
  65. package/src/native/Recordings/test_fn2.swift +0 -33
  66. package/src/types/index.ts +0 -144
  67. package/tsconfig.json +0 -21
  68. /package/src/native/Recordings/{Recordings → RecordingsLib}/FnKeyMonitor.swift +0 -0
  69. /package/src/native/Recordings/{Recordings → RecordingsLib}/Recordings.entitlements +0 -0
@@ -1,12 +1,19 @@
1
1
  import SwiftUI
2
2
  import KeyboardShortcuts
3
3
 
4
- struct SettingsView: View {
5
- @ObservedObject var engine: RecordingEngine
6
- @ObservedObject var shortcuts: VoiceShortcuts
7
- @ObservedObject var projectStore: ProjectStore
4
+ public struct SettingsView: View {
5
+ @ObservedObject public var engine: RecordingEngine
6
+ @ObservedObject public var shortcuts: VoiceShortcuts
7
+ @ObservedObject public var projectStore: ProjectStore
8
+ @AppStorage("openAIAPIKey") private var openAIAPIKey = ""
9
+
10
+ public init(engine: RecordingEngine, shortcuts: VoiceShortcuts, projectStore: ProjectStore) {
11
+ self.engine = engine
12
+ self.shortcuts = shortcuts
13
+ self.projectStore = projectStore
14
+ }
8
15
 
9
- var body: some View {
16
+ public var body: some View {
10
17
  TabView {
11
18
  generalTab.tabItem { Label("General", systemImage: "gear") }
12
19
  projectsTab.tabItem { Label("Projects", systemImage: "folder") }
@@ -19,6 +26,13 @@ struct SettingsView: View {
19
26
 
20
27
  private var generalTab: some View {
21
28
  Form {
29
+ Section("OpenAI") {
30
+ SecureField("API key", text: $openAIAPIKey)
31
+ .textFieldStyle(.roundedBorder)
32
+ Text("Used for realtime transcription in the menu bar app.")
33
+ .foregroundStyle(.secondary)
34
+ }
35
+
22
36
  Section("Recording Shortcut") {
23
37
  HStack {
24
38
  Text("Shortcut")
@@ -2,12 +2,12 @@ import SwiftUI
2
2
 
3
3
  // MARK: - Voice Shortcut
4
4
 
5
- struct VoiceShortcut: Identifiable, Codable {
6
- let id: UUID
7
- var trigger: String // e.g. "add disclaimer"
8
- var content: String // text to insert when trigger is spoken
5
+ public struct VoiceShortcut: Identifiable, Codable {
6
+ public let id: UUID
7
+ public var trigger: String // e.g. "add disclaimer"
8
+ public var content: String // text to insert when trigger is spoken
9
9
 
10
- init(trigger: String, content: String) {
10
+ public init(trigger: String, content: String) {
11
11
  self.id = UUID()
12
12
  self.trigger = trigger
13
13
  self.content = content
@@ -17,15 +17,15 @@ struct VoiceShortcut: Identifiable, Codable {
17
17
  // MARK: - Voice Shortcuts Manager
18
18
 
19
19
  @MainActor
20
- final class VoiceShortcuts: ObservableObject {
21
- @Published var shortcuts: [VoiceShortcut] = []
20
+ public final class VoiceShortcuts: ObservableObject {
21
+ @Published public var shortcuts: [VoiceShortcut] = []
22
22
 
23
23
  private let storageURL: URL = {
24
24
  let home = FileManager.default.homeDirectoryForCurrentUser
25
25
  return home.appendingPathComponent(".hasna/recordings/voice-shortcuts.json")
26
26
  }()
27
27
 
28
- init() {
28
+ public init() {
29
29
  load()
30
30
  }
31
31
 
@@ -64,6 +64,10 @@ final class VoiceShortcuts: ObservableObject {
64
64
  private func save() {
65
65
  do {
66
66
  let data = try JSONEncoder().encode(shortcuts)
67
+ try FileManager.default.createDirectory(
68
+ at: storageURL.deletingLastPathComponent(),
69
+ withIntermediateDirectories: true
70
+ )
67
71
  try data.write(to: storageURL, options: .atomic)
68
72
  } catch {
69
73
  print("Failed to save voice shortcuts: \(error)")
@@ -0,0 +1,63 @@
1
+ import Testing
2
+ @testable import RecordingsLib
3
+
4
+ struct CLIRunnerTests {
5
+ @Test("parseError detects ERROR prefix")
6
+ func parseError() {
7
+ #expect(CLIRunner.parseError("ERROR: OpenAI API key not configured on this Mac") == "OpenAI API key not configured on this Mac")
8
+ }
9
+
10
+ @Test("parseError returns nil for normal output")
11
+ func noError() {
12
+ #expect(CLIRunner.parseError("Hello world") == nil)
13
+ }
14
+
15
+ @Test("parseError handles API key error")
16
+ func apiKeyError() {
17
+ #expect(CLIRunner.parseError("ERROR: OpenAI API key not configured") == "OpenAI API key not configured on this Mac")
18
+ }
19
+
20
+ @Test("parseError truncates long messages to 120 chars")
21
+ func truncation() {
22
+ let longMsg = String(repeating: "a", count: 200)
23
+ let input = "ERROR: \(longMsg)"
24
+ let result = CLIRunner.parseError(input)!
25
+ #expect(result.count <= 120)
26
+ }
27
+
28
+ @Test("parseJSON extracts raw_text from JSON")
29
+ func parseJSONRawText() {
30
+ let output = """
31
+ {"raw_text": "Hello world", "processed_text": null}
32
+ """
33
+ #expect(CLIRunner.parseJSON(output) == "Hello world")
34
+ }
35
+
36
+ @Test("parseJSON prefers processed_text over raw_text")
37
+ func parseJSONProcessedText() {
38
+ let output = """
39
+ {"raw_text": "Hello world", "processed_text": "Hello World (enhanced)"}
40
+ """
41
+ #expect(CLIRunner.parseJSON(output) == "Hello World (enhanced)")
42
+ }
43
+
44
+ @Test("parseJSON falls back to plain text")
45
+ func parseJSONFallback() {
46
+ let output = "Transcribing...\nHello world\nSaved to file"
47
+ #expect(CLIRunner.parseJSON(output) == "Hello world")
48
+ }
49
+
50
+ @Test("parseJSON returns nil for empty output")
51
+ func emptyOutput() {
52
+ #expect(CLIRunner.parseJSON("") == nil)
53
+ }
54
+
55
+ @Test("parseJSON handles empty transcription")
56
+ func emptyTranscription() {
57
+ let output = """
58
+ {"raw_text": "", "processed_text": ""}
59
+ """
60
+ // Both are empty, so it should fall back to plain text extraction
61
+ #expect(CLIRunner.parseJSON(output) == nil)
62
+ }
63
+ }
@@ -0,0 +1,23 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ struct NativeAppDiagnosticsTests {
6
+ @Test("Native app log writes to recordings log file")
7
+ func logWritesFile() throws {
8
+ let home = try makeHome()
9
+
10
+ NativeAppLog.write("diagnostic-test", homePath: home)
11
+
12
+ let path = "\(home)/.hasna/recordings/Recordings.log"
13
+ let text = try String(contentsOfFile: path, encoding: .utf8)
14
+ #expect(text.contains("diagnostic-test"))
15
+ }
16
+
17
+ private func makeHome() throws -> String {
18
+ let url = FileManager.default.temporaryDirectory
19
+ .appendingPathComponent("recordings-diagnostics-tests-\(UUID().uuidString)")
20
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
21
+ return url.path
22
+ }
23
+ }
@@ -0,0 +1,33 @@
1
+ import AVFoundation
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ struct NativePCMRecorderTests {
6
+ @Test("Realtime output format is OpenAI-compatible 24 kHz mono PCM16")
7
+ func realtimeOutputFormat() throws {
8
+ let format = try #require(NativePCMRecorder.realtimeOutputFormat())
9
+
10
+ #expect(format.commonFormat == .pcmFormatInt16)
11
+ #expect(format.sampleRate == 24_000)
12
+ #expect(format.channelCount == 1)
13
+ #expect(format.isInterleaved)
14
+ }
15
+
16
+ @Test("Extracts PCM16 bytes from audio buffer")
17
+ func extractPCM16Data() throws {
18
+ let format = try #require(NativePCMRecorder.realtimeOutputFormat())
19
+ let buffer = try #require(AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 4))
20
+ buffer.frameLength = 4
21
+
22
+ let samples = try #require(buffer.int16ChannelData?[0])
23
+ samples[0] = 1
24
+ samples[1] = -2
25
+ samples[2] = 3
26
+ samples[3] = -4
27
+
28
+ let data = NativePCMRecorder.extractPCM16Data(from: buffer)
29
+
30
+ #expect(data.count == 8)
31
+ #expect(data.withUnsafeBytes { $0.load(as: Int16.self) } == 1)
32
+ }
33
+ }
@@ -0,0 +1,92 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ struct OpenAIAPIKeyStoreTests {
6
+ @Test("Environment key has highest priority")
7
+ func environmentKeyWins() {
8
+ let key = OpenAIAPIKeyStore.load(
9
+ homePath: "/tmp/recordings-missing-home",
10
+ environment: ["OPENAI_API_KEY": "env-key"],
11
+ userDefaultKey: "stored-key"
12
+ )
13
+ #expect(key == "env-key")
14
+ }
15
+
16
+ @Test("User default key is used before config file")
17
+ func userDefaultKeyWins() throws {
18
+ let home = try makeHome()
19
+ try writeConfig(home: home, ["openai_api_key": "config-key"])
20
+
21
+ let key = OpenAIAPIKeyStore.load(
22
+ homePath: home.path,
23
+ environment: [:],
24
+ userDefaultKey: "stored-key"
25
+ )
26
+ #expect(key == "stored-key")
27
+ }
28
+
29
+ @Test("Config file key is loaded from installed app data")
30
+ func configFileKey() throws {
31
+ let home = try makeHome()
32
+ try writeConfig(home: home, ["openai_api_key": "config-key"])
33
+
34
+ let key = OpenAIAPIKeyStore.load(
35
+ homePath: home.path,
36
+ environment: [:],
37
+ userDefaultKey: nil
38
+ )
39
+ #expect(key == "config-key")
40
+ }
41
+
42
+ @Test("Config file can reference environment variable")
43
+ func configEnvReference() throws {
44
+ let home = try makeHome()
45
+ try writeConfig(home: home, ["openai_api_key": "$RECORDINGS_API_KEY"])
46
+
47
+ let key = OpenAIAPIKeyStore.load(
48
+ homePath: home.path,
49
+ environment: ["RECORDINGS_API_KEY": "referenced-key"],
50
+ userDefaultKey: nil
51
+ )
52
+ #expect(key == "referenced-key")
53
+ }
54
+
55
+ @Test("Secrets env files are searched recursively")
56
+ func recursiveSecrets() throws {
57
+ let home = try makeHome()
58
+ let secretDir = home
59
+ .appendingPathComponent(".secrets")
60
+ .appendingPathComponent("hasnaxyz")
61
+ .appendingPathComponent("openai")
62
+ try FileManager.default.createDirectory(at: secretDir, withIntermediateDirectories: true)
63
+ try "export OPENAI_API_KEY='secret-key'\n".write(
64
+ to: secretDir.appendingPathComponent("live.env"),
65
+ atomically: true,
66
+ encoding: .utf8
67
+ )
68
+
69
+ let key = OpenAIAPIKeyStore.load(
70
+ homePath: home.path,
71
+ environment: [:],
72
+ userDefaultKey: nil
73
+ )
74
+ #expect(key == "secret-key")
75
+ }
76
+
77
+ private func makeHome() throws -> URL {
78
+ let url = FileManager.default.temporaryDirectory
79
+ .appendingPathComponent("recordings-key-store-\(UUID().uuidString)")
80
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
81
+ return url
82
+ }
83
+
84
+ private func writeConfig(home: URL, _ config: [String: String]) throws {
85
+ let configDir = home
86
+ .appendingPathComponent(".hasna")
87
+ .appendingPathComponent("recordings")
88
+ try FileManager.default.createDirectory(at: configDir, withIntermediateDirectories: true)
89
+ let data = try JSONSerialization.data(withJSONObject: config, options: [.prettyPrinted])
90
+ try data.write(to: configDir.appendingPathComponent("config.json"))
91
+ }
92
+ }
@@ -0,0 +1,58 @@
1
+ import Testing
2
+ @testable import RecordingsLib
3
+
4
+ struct ProjectStoreTests {
5
+ @Test("matchProject by bundle ID")
6
+ func matchByBundleId() {
7
+ let projects = [
8
+ RecProject(name: "MyApp", appBundleIds: ["com.example.myapp"]),
9
+ RecProject(name: "Other", appBundleIds: ["com.example.other"]),
10
+ ]
11
+ let result = ProjectStore.matchProject(windowTitle: nil, bundleId: "com.example.myapp", projects: projects)
12
+ #expect(result?.name == "MyApp")
13
+ }
14
+
15
+ @Test("matchProject by window title containing project name")
16
+ func matchByWindowTitle() {
17
+ let projects = [
18
+ RecProject(name: "Hasna", path: nil),
19
+ RecProject(name: "Other", path: nil),
20
+ ]
21
+ let result = ProjectStore.matchProject(windowTitle: "Hasna — Takumi", bundleId: nil, projects: projects)
22
+ #expect(result?.name == "Hasna")
23
+ }
24
+
25
+ @Test("matchProject by path component in window title")
26
+ func matchByPathComponent() {
27
+ let projects = [
28
+ RecProject(name: "Backend API", path: "/projects/backend-api"),
29
+ RecProject(name: "Other", path: nil),
30
+ ]
31
+ let result = ProjectStore.matchProject(windowTitle: "backend-api — VSCode", bundleId: nil, projects: projects)
32
+ #expect(result?.name == "Backend API")
33
+ }
34
+
35
+ @Test("matchProject returns nil when no match")
36
+ func noMatch() {
37
+ let projects = [
38
+ RecProject(name: "ProjectA", appBundleIds: ["com.a"]),
39
+ ]
40
+ let result = ProjectStore.matchProject(windowTitle: "ProjectB", bundleId: "com.b", projects: projects)
41
+ #expect(result == nil)
42
+ }
43
+
44
+ @Test("matchProject returns nil for empty projects")
45
+ func emptyProjects() {
46
+ let result = ProjectStore.matchProject(windowTitle: "Anything", bundleId: "com.any", projects: [])
47
+ #expect(result == nil)
48
+ }
49
+
50
+ @Test("matchProject is case-insensitive")
51
+ func caseInsensitive() {
52
+ let projects = [
53
+ RecProject(name: "myapp", path: nil),
54
+ ]
55
+ let result = ProjectStore.matchProject(windowTitle: "MyApp - Editor", bundleId: nil, projects: projects)
56
+ #expect(result?.name == "myapp")
57
+ }
58
+ }
@@ -0,0 +1,113 @@
1
+ import Testing
2
+ @testable import RecordingsLib
3
+
4
+ // MARK: - RealtimeTranscriptionClient Event Parsing Tests
5
+
6
+ struct RealtimeTranscriptionTests {
7
+ @Test("Model ID is set to latest gpt-4o-transcribe")
8
+ func modelID() {
9
+ #expect(RealtimeTranscriptionClient.modelID == "gpt-4o-transcribe")
10
+ }
11
+
12
+ @Test("Parses transcription delta events")
13
+ func parseDelta() {
14
+ let deltaJSON = """
15
+ {"type":"conversation.item.input_audio_transcription.delta","delta":"Hello "}
16
+ """
17
+ #expect(RealtimeTranscriptionClient.parseDeltaTestHelper(deltaJSON) == "Hello ")
18
+ }
19
+
20
+ @Test("Parses transcription completed event")
21
+ func parseCompleted() {
22
+ let completedJSON = """
23
+ {"type":"conversation.item.input_audio_transcription.completed","transcript":"Hello world"}
24
+ """
25
+ #expect(RealtimeTranscriptionClient.parseDeltaTestHelper(completedJSON) == "Hello world")
26
+ }
27
+
28
+ @Test("Returns nil for non-transcription events")
29
+ func ignoreOtherEvents() {
30
+ let sessionJSON = """
31
+ {"type":"session.created","session":{"id":"abc"}}
32
+ """
33
+ #expect(RealtimeTranscriptionClient.parseDeltaTestHelper(sessionJSON) == nil)
34
+ }
35
+
36
+ @Test("Returns nil for malformed JSON")
37
+ func malformedJSON() {
38
+ #expect(RealtimeTranscriptionClient.parseDeltaTestHelper("not json") == nil)
39
+ }
40
+
41
+ @Test("Returns nil for empty delta")
42
+ func emptyDelta() {
43
+ let emptyJSON = """
44
+ {"type":"conversation.item.input_audio_transcription.delta","delta":""}
45
+ """
46
+ // Empty string delta is still valid — client should handle it
47
+ #expect(RealtimeTranscriptionClient.parseDeltaTestHelper(emptyJSON) == "")
48
+ }
49
+
50
+ @Test("Detects error events")
51
+ func detectError() {
52
+ let errorJSON = """
53
+ {"type":"error","error":{"message":"Invalid API key","code":401}}
54
+ """
55
+ #expect(RealtimeTranscriptionClient.isSessionErrorTestHelper(errorJSON) == true)
56
+ }
57
+
58
+ @Test("Does not flag normal events as errors")
59
+ func noFalsePositives() {
60
+ #expect(RealtimeTranscriptionClient.isSessionErrorTestHelper(
61
+ "{\"type\":\"conversation.item.input_audio_transcription.delta\"}"
62
+ ) == false)
63
+ }
64
+
65
+ @Test("Parse error message from error event")
66
+ func parseErrorMessage() {
67
+ let errorJSON = """
68
+ {"type":"error","error":{"message":"Model not found","code":404}}
69
+ """
70
+ #expect(RealtimeTranscriptionClient.parseErrorTestHelper(errorJSON) == "Model not found")
71
+ }
72
+
73
+ @Test("Builds strict verbatim prompt with vocabulary context")
74
+ func buildPrompt() {
75
+ let prompt = RealtimeTranscriptionClient.buildPromptTestHelper("Alumia, Takumi")
76
+ #expect(prompt.contains("verbatim"))
77
+ #expect(prompt.contains("Do not summarize"))
78
+ #expect(prompt.contains("vocabulary context"))
79
+ #expect(prompt.contains("Alumia"))
80
+ }
81
+
82
+ @Test("Builds realtime transcription session update event")
83
+ func buildSessionUpdateEvent() {
84
+ let event = RealtimeTranscriptionClient.sessionUpdateTestHelper(prompt: "Use Alumia as vocabulary", language: "en")
85
+ #expect(event["type"] as? String == "session.update")
86
+
87
+ let session = event["session"] as? [String: Any]
88
+ #expect(session?["type"] as? String == "transcription")
89
+
90
+ let audio = session?["audio"] as? [String: Any]
91
+ let input = audio?["input"] as? [String: Any]
92
+ let format = input?["format"] as? [String: Any]
93
+ #expect(format?["type"] as? String == "audio/pcm")
94
+ #expect(format?["rate"] as? Int == 24_000)
95
+
96
+ let transcription = input?["transcription"] as? [String: Any]
97
+ #expect(transcription?["model"] as? String == "gpt-4o-transcribe")
98
+ #expect(transcription?["prompt"] as? String == "Use Alumia as vocabulary")
99
+ #expect(transcription?["language"] as? String == "en")
100
+
101
+ let turnDetection = input?["turn_detection"] as? [String: Any]
102
+ #expect(turnDetection?["type"] as? String == "server_vad")
103
+
104
+ let include = session?["include"] as? [String]
105
+ #expect(include?.contains("item.input_audio_transcription.logprobs") == true)
106
+ }
107
+
108
+ @Test("Joins transcript parts without dropping spoken text")
109
+ func joinParts() {
110
+ let text = RealtimeTranscriptionClient.joinTranscriptPartsTestHelper(["Hello", "world.", " Next"])
111
+ #expect(text == "Hello world. Next")
112
+ }
113
+ }
@@ -1,4 +1,4 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
2
  # Build Recordings.app for macOS 26
3
3
  # Usage: ./build.sh [debug|release]
4
4
 
@@ -9,7 +9,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  cd "$SCRIPT_DIR"
10
10
 
11
11
  echo "Building Recordings.app ($MODE)..."
12
- swift build -c "$MODE"
12
+ swift build -c "$MODE" --product App
13
13
 
14
14
  # Create .app bundle
15
15
  BUILD_DIR=".build/$MODE"
@@ -21,14 +21,14 @@ rm -rf "$APP_DIR"
21
21
  mkdir -p "$MACOS"
22
22
 
23
23
  # Copy binary
24
- cp "$BUILD_DIR/Recordings" "$MACOS/Recordings"
24
+ cp "$BUILD_DIR/App" "$MACOS/Recordings"
25
25
 
26
26
  # Copy Info.plist
27
- cp Recordings/Info.plist "$CONTENTS/Info.plist"
27
+ cp RecordingsLib/Info.plist "$CONTENTS/Info.plist"
28
28
 
29
29
  # Copy entitlements (for codesigning)
30
- if [ -f Recordings/Recordings.entitlements ]; then
31
- codesign --force --sign - --entitlements Recordings/Recordings.entitlements "$APP_DIR" 2>/dev/null || true
30
+ if [ -f RecordingsLib/Recordings.entitlements ]; then
31
+ codesign --force --sign - --entitlements RecordingsLib/Recordings.entitlements "$APP_DIR" 2>/dev/null || true
32
32
  fi
33
33
 
34
34
  echo "✓ Built $APP_DIR"
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(ssh apple03:*)"
5
- ]
6
- }
7
- }