@hasna/recordings 0.1.23 → 0.1.24
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/LICENSE +191 -170
- package/README.md +16 -7
- package/dist/cli/index.js +114 -64
- package/dist/cli/storage.d.ts +3 -0
- package/dist/cli/storage.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/db/storage-config.d.ts +27 -0
- package/dist/db/storage-config.d.ts.map +1 -0
- package/dist/db/storage-sync.d.ts +36 -0
- package/dist/db/storage-sync.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +110 -50
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +13 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +2 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +519 -408
- package/dist/mcp/storage-tools.d.ts +3 -0
- package/dist/mcp/storage-tools.d.ts.map +1 -0
- package/dist/storage.d.ts +7 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +5656 -0
- package/dist/version.d.ts +1 -1
- package/package.json +8 -3
- package/scripts/install_macos_app.sh +17 -0
- package/src/native/Recordings/App/RecordingsApp.swift +3 -2
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +101 -80
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +26 -0
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +13 -1
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +92 -31
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +5 -1
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +12 -0
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +49 -0
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +54 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +58 -0
- package/dist/cli/cloud.d.ts +0 -3
- package/dist/cli/cloud.d.ts.map +0 -1
- package/dist/db/cloud-config.d.ts +0 -14
- package/dist/db/cloud-config.d.ts.map +0 -1
- package/dist/db/cloud-sync.d.ts +0 -29
- package/dist/db/cloud-sync.d.ts.map +0 -1
- package/dist/mcp/cloud-tools.d.ts +0 -3
- package/dist/mcp/cloud-tools.d.ts.map +0 -1
|
@@ -74,6 +74,55 @@ struct OpenAIAPIKeyStoreTests {
|
|
|
74
74
|
#expect(key == "secret-key")
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
@Test("Saving a key writes it to config.json so the CLI uses the same key")
|
|
78
|
+
func saveWritesConfig() throws {
|
|
79
|
+
let home = try makeHome()
|
|
80
|
+
|
|
81
|
+
try OpenAIAPIKeyStore.save(key: "sk-new-key", homePath: home.path)
|
|
82
|
+
|
|
83
|
+
let key = OpenAIAPIKeyStore.load(
|
|
84
|
+
homePath: home.path,
|
|
85
|
+
environment: [:],
|
|
86
|
+
userDefaultKey: nil
|
|
87
|
+
)
|
|
88
|
+
#expect(key == "sk-new-key")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test("Saving a key preserves unrelated config.json fields")
|
|
92
|
+
func savePreservesOtherFields() throws {
|
|
93
|
+
let home = try makeHome()
|
|
94
|
+
try writeConfig(home: home, [
|
|
95
|
+
"openai_api_key": "old-key",
|
|
96
|
+
"transcription_model": "gpt-4o-mini-transcribe",
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
try OpenAIAPIKeyStore.save(key: "sk-rotated", homePath: home.path)
|
|
100
|
+
|
|
101
|
+
let configURL = home
|
|
102
|
+
.appendingPathComponent(".hasna")
|
|
103
|
+
.appendingPathComponent("recordings")
|
|
104
|
+
.appendingPathComponent("config.json")
|
|
105
|
+
let data = try Data(contentsOf: configURL)
|
|
106
|
+
let json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
|
107
|
+
#expect(json["openai_api_key"] as? String == "sk-rotated")
|
|
108
|
+
#expect(json["transcription_model"] as? String == "gpt-4o-mini-transcribe")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test("Saving an empty key removes it from config.json")
|
|
112
|
+
func saveEmptyRemovesKey() throws {
|
|
113
|
+
let home = try makeHome()
|
|
114
|
+
try writeConfig(home: home, ["openai_api_key": "old-key"])
|
|
115
|
+
|
|
116
|
+
try OpenAIAPIKeyStore.save(key: " ", homePath: home.path)
|
|
117
|
+
|
|
118
|
+
let key = OpenAIAPIKeyStore.load(
|
|
119
|
+
homePath: home.path,
|
|
120
|
+
environment: [:],
|
|
121
|
+
userDefaultKey: nil
|
|
122
|
+
)
|
|
123
|
+
#expect(key == "")
|
|
124
|
+
}
|
|
125
|
+
|
|
77
126
|
private func makeHome() throws -> URL {
|
|
78
127
|
let url = FileManager.default.temporaryDirectory
|
|
79
128
|
.appendingPathComponent("recordings-key-store-\(UUID().uuidString)")
|
|
@@ -35,4 +35,58 @@ struct PasteTargetTests {
|
|
|
35
35
|
|
|
36
36
|
#expect(selected?.pid == 30)
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
@Test("paste target prefers the frontmost app over an arbitrary regular app")
|
|
40
|
+
func prefersFrontmostFallback() {
|
|
41
|
+
let candidates = [
|
|
42
|
+
PasteTargetCandidate(pid: 5, bundleIdentifier: "com.background", isRegularApp: true),
|
|
43
|
+
PasteTargetCandidate(pid: 30, bundleIdentifier: "com.notes", isRegularApp: true),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
let selected = RecordingEngine.selectPasteTarget(
|
|
47
|
+
candidates: candidates,
|
|
48
|
+
currentPid: 99,
|
|
49
|
+
targetBundleIdentifier: nil,
|
|
50
|
+
targetPid: nil,
|
|
51
|
+
frontmostPid: 30
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
#expect(selected?.pid == 30)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test("paste target never selects the recorder app even when frontmost")
|
|
58
|
+
func skipsOwnAppWhenFrontmost() {
|
|
59
|
+
let candidates = [
|
|
60
|
+
PasteTargetCandidate(pid: 99, bundleIdentifier: "com.hasna.recordings", isRegularApp: true),
|
|
61
|
+
PasteTargetCandidate(pid: 30, bundleIdentifier: "com.notes", isRegularApp: true),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
let selected = RecordingEngine.selectPasteTarget(
|
|
65
|
+
candidates: candidates,
|
|
66
|
+
currentPid: 99,
|
|
67
|
+
targetBundleIdentifier: nil,
|
|
68
|
+
targetPid: nil,
|
|
69
|
+
frontmostPid: 99
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
#expect(selected?.pid == 30)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test("captured pid wins over frontmost fallback")
|
|
76
|
+
func capturedPidBeatsFrontmost() {
|
|
77
|
+
let candidates = [
|
|
78
|
+
PasteTargetCandidate(pid: 10, bundleIdentifier: "com.editor", isRegularApp: true),
|
|
79
|
+
PasteTargetCandidate(pid: 30, bundleIdentifier: "com.notes", isRegularApp: true),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
let selected = RecordingEngine.selectPasteTarget(
|
|
83
|
+
candidates: candidates,
|
|
84
|
+
currentPid: 99,
|
|
85
|
+
targetBundleIdentifier: nil,
|
|
86
|
+
targetPid: 10,
|
|
87
|
+
frontmostPid: 30
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
#expect(selected?.pid == 10)
|
|
91
|
+
}
|
|
38
92
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
@testable import RecordingsLib
|
|
3
|
+
|
|
4
|
+
struct TranscriptResolutionTests {
|
|
5
|
+
@Test("CLI transcript is preferred when present")
|
|
6
|
+
func cliWins() {
|
|
7
|
+
let resolved = RecordingEngine.resolveFinalTranscript(
|
|
8
|
+
cliText: "full transcript from whole audio",
|
|
9
|
+
cliError: nil,
|
|
10
|
+
realtimeText: "partial realtime"
|
|
11
|
+
)
|
|
12
|
+
#expect(resolved.text == "full transcript from whole audio")
|
|
13
|
+
#expect(resolved.failureStatus == nil)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Test("Realtime transcript is used when CLI transcription fails")
|
|
17
|
+
func realtimeFallbackOnError() {
|
|
18
|
+
let resolved = RecordingEngine.resolveFinalTranscript(
|
|
19
|
+
cliText: nil,
|
|
20
|
+
cliError: "OpenAI API key invalid or expired — update it in Recordings Settings",
|
|
21
|
+
realtimeText: "what the user actually said"
|
|
22
|
+
)
|
|
23
|
+
#expect(resolved.text == "what the user actually said")
|
|
24
|
+
#expect(resolved.failureStatus == nil)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test("Realtime transcript is used when CLI returns empty text")
|
|
28
|
+
func realtimeFallbackOnEmpty() {
|
|
29
|
+
let resolved = RecordingEngine.resolveFinalTranscript(
|
|
30
|
+
cliText: " ",
|
|
31
|
+
cliError: nil,
|
|
32
|
+
realtimeText: "spoken words"
|
|
33
|
+
)
|
|
34
|
+
#expect(resolved.text == "spoken words")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test("CLI error is surfaced when no transcript exists at all")
|
|
38
|
+
func errorWhenNothing() {
|
|
39
|
+
let resolved = RecordingEngine.resolveFinalTranscript(
|
|
40
|
+
cliText: nil,
|
|
41
|
+
cliError: "OpenAI API key invalid or expired — update it in Recordings Settings",
|
|
42
|
+
realtimeText: " "
|
|
43
|
+
)
|
|
44
|
+
#expect(resolved.text == nil)
|
|
45
|
+
#expect(resolved.failureStatus == "OpenAI API key invalid or expired — update it in Recordings Settings")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test("Generic failure status when both transcripts are empty without error")
|
|
49
|
+
func genericEmpty() {
|
|
50
|
+
let resolved = RecordingEngine.resolveFinalTranscript(
|
|
51
|
+
cliText: "",
|
|
52
|
+
cliError: nil,
|
|
53
|
+
realtimeText: nil
|
|
54
|
+
)
|
|
55
|
+
#expect(resolved.text == nil)
|
|
56
|
+
#expect(resolved.failureStatus == "Empty transcription")
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/cli/cloud.d.ts
DELETED
package/dist/cli/cloud.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/cli/cloud.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BzC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkI5D"}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export type CloudMode = "local" | "hybrid" | "cloud";
|
|
2
|
-
export interface CloudConfig {
|
|
3
|
-
mode: CloudMode;
|
|
4
|
-
rds: {
|
|
5
|
-
host: string;
|
|
6
|
-
port: number;
|
|
7
|
-
username: string;
|
|
8
|
-
password_env: string;
|
|
9
|
-
ssl: boolean;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export declare function getCloudConfig(): CloudConfig;
|
|
13
|
-
export declare function getConnectionString(dbName?: string): string;
|
|
14
|
-
//# sourceMappingURL=cloud-config.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-config.d.ts","sourceRoot":"","sources":["../../src/db/cloud-config.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,EAAE;QACH,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,GAAG,EAAE,OAAO,CAAC;KACd,CAAC;CACH;AAgBD,wBAAgB,cAAc,IAAI,WAAW,CAiC5C;AAED,wBAAgB,mBAAmB,CAAC,MAAM,SAAe,GAAG,MAAM,CAiBjE"}
|
package/dist/db/cloud-sync.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { type Database } from "bun:sqlite";
|
|
2
|
-
import { PgAdapterAsync } from "./remote-storage.js";
|
|
3
|
-
export interface SyncResult {
|
|
4
|
-
table: string;
|
|
5
|
-
direction: "push" | "pull";
|
|
6
|
-
rows_read: number;
|
|
7
|
-
rows_written: number;
|
|
8
|
-
errors: string[];
|
|
9
|
-
}
|
|
10
|
-
export interface CloudStatus {
|
|
11
|
-
mode: string;
|
|
12
|
-
enabled: boolean;
|
|
13
|
-
db_path: string;
|
|
14
|
-
tables: Array<{
|
|
15
|
-
table: string;
|
|
16
|
-
rows: number;
|
|
17
|
-
}>;
|
|
18
|
-
}
|
|
19
|
-
export declare function getCloudPg(): Promise<PgAdapterAsync>;
|
|
20
|
-
export declare function runCloudMigrations(remote: PgAdapterAsync): Promise<void>;
|
|
21
|
-
export declare function getCloudStatus(db?: Database): CloudStatus;
|
|
22
|
-
export declare function pushCloudChanges(tables?: string[]): Promise<SyncResult[]>;
|
|
23
|
-
export declare function pullCloudChanges(tables?: string[]): Promise<SyncResult[]>;
|
|
24
|
-
export declare function syncCloudChanges(tables?: string[]): Promise<{
|
|
25
|
-
push: SyncResult[];
|
|
26
|
-
pull: SyncResult[];
|
|
27
|
-
}>;
|
|
28
|
-
export declare function parseCloudTables(raw?: string): string[];
|
|
29
|
-
//# sourceMappingURL=cloud-sync.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-sync.d.ts","sourceRoot":"","sources":["../../src/db/cloud-sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAKrD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAqFD,wBAAsB,UAAU,IAAI,OAAO,CAAC,cAAc,CAAC,CAE1D;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9E;AAED,wBAAgB,cAAc,CAAC,EAAE,GAAE,QAAwB,GAAG,WAAW,CAexE;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,MAAM,EAAsB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAuBlG;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,MAAM,EAAsB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAuBlG;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,MAAM,EAAsB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAAC,IAAI,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CAKhI;AAED,wBAAgB,gBAAgB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAIvD"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-tools.d.ts","sourceRoot":"","sources":["../../src/mcp/cloud-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAmFpE"}
|