@hasna/recordings 0.2.11 → 0.2.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.
- package/README.md +349 -69
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1919 -98
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1973 -236
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
package/dist/storage.js
CHANGED
|
@@ -33,23 +33,200 @@ class SqliteAdapter {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// src/db/database.ts
|
|
36
|
-
import { mkdirSync as
|
|
37
|
-
import { dirname as
|
|
36
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
37
|
+
import { dirname as dirname3 } from "path";
|
|
38
38
|
|
|
39
39
|
// src/lib/config.ts
|
|
40
|
-
import { copyFileSync, existsSync, readFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
41
|
-
import { dirname, join, resolve } from "path";
|
|
40
|
+
import { copyFileSync, existsSync, readFileSync, mkdirSync as mkdirSync2, readdirSync, realpathSync as realpathSync2, statSync } from "fs";
|
|
41
|
+
import { dirname as dirname2, join as join2, resolve as resolve2, sep as sep2 } from "path";
|
|
42
|
+
import { homedir as homedir2 } from "os";
|
|
43
|
+
|
|
44
|
+
// src/lib/install-maintenance.ts
|
|
45
|
+
import { randomUUID } from "crypto";
|
|
46
|
+
import {
|
|
47
|
+
chmodSync,
|
|
48
|
+
lstatSync,
|
|
49
|
+
mkdirSync,
|
|
50
|
+
realpathSync,
|
|
51
|
+
renameSync,
|
|
52
|
+
rmSync,
|
|
53
|
+
writeFileSync
|
|
54
|
+
} from "fs";
|
|
42
55
|
import { homedir } from "os";
|
|
56
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
57
|
+
import { spawnSync } from "child_process";
|
|
58
|
+
var INSTALL_MAINTENANCE_MARKER_NAME = ".recordings-install-maintenance";
|
|
59
|
+
var STORE_READER_LEASES_NAME = ".recordings-store-readers";
|
|
60
|
+
function stateParent(env = process.env) {
|
|
61
|
+
const home = env["HOME"] || env["USERPROFILE"] || homedir();
|
|
62
|
+
return join(home, ".hasna");
|
|
63
|
+
}
|
|
64
|
+
function installMaintenanceMarkerPath(env = process.env) {
|
|
65
|
+
return join(stateParent(env), INSTALL_MAINTENANCE_MARKER_NAME);
|
|
66
|
+
}
|
|
67
|
+
function storeReaderLeasesPath(env = process.env) {
|
|
68
|
+
return join(stateParent(env), STORE_READER_LEASES_NAME);
|
|
69
|
+
}
|
|
70
|
+
function pathExists(path) {
|
|
71
|
+
try {
|
|
72
|
+
lstatSync(path);
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code === "ENOENT")
|
|
76
|
+
return false;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function assertPrivateDirectory(path) {
|
|
81
|
+
const details = lstatSync(path);
|
|
82
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
83
|
+
throw new Error(`Local Recordings coordination path is not a secure directory: ${path}`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
|
|
86
|
+
throw new Error(`Local Recordings coordination path has an unexpected owner: ${path}`);
|
|
87
|
+
}
|
|
88
|
+
if ((details.mode & 18) !== 0) {
|
|
89
|
+
throw new Error(`Local Recordings coordination path is group/world writable: ${path}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function ensureReaderRoot(env) {
|
|
93
|
+
const parent = stateParent(env);
|
|
94
|
+
const readerRoot = storeReaderLeasesPath(env);
|
|
95
|
+
mkdirSync(parent, { recursive: true, mode: 448 });
|
|
96
|
+
assertPrivateDirectory(parent);
|
|
97
|
+
try {
|
|
98
|
+
mkdirSync(readerRoot, { mode: 448 });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.code !== "EEXIST")
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
assertPrivateDirectory(readerRoot);
|
|
104
|
+
return readerRoot;
|
|
105
|
+
}
|
|
106
|
+
function processStartIdentity() {
|
|
107
|
+
const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(process.pid)], {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C", TZ: "UTC0" }
|
|
110
|
+
});
|
|
111
|
+
const identity = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
|
|
112
|
+
if (!identity) {
|
|
113
|
+
throw new Error("Could not establish the local Store reader process identity");
|
|
114
|
+
}
|
|
115
|
+
return identity;
|
|
116
|
+
}
|
|
117
|
+
function maintenanceUnavailable() {
|
|
118
|
+
return new Error("Local Recordings storage is temporarily unavailable during app installation maintenance");
|
|
119
|
+
}
|
|
120
|
+
function assertMaintenanceAbsent(env) {
|
|
121
|
+
if (pathExists(installMaintenanceMarkerPath(env)))
|
|
122
|
+
throw maintenanceUnavailable();
|
|
123
|
+
}
|
|
124
|
+
function canonicalPotentialPath(path) {
|
|
125
|
+
const absolutePath = resolve(path);
|
|
126
|
+
let existingPath = absolutePath;
|
|
127
|
+
const missingEntries = [];
|
|
128
|
+
while (true) {
|
|
129
|
+
try {
|
|
130
|
+
return join(realpathSync(existingPath), ...missingEntries);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error.code !== "ENOENT")
|
|
133
|
+
return absolutePath;
|
|
134
|
+
const parent = dirname(existingPath);
|
|
135
|
+
if (parent === existingPath)
|
|
136
|
+
return absolutePath;
|
|
137
|
+
missingEntries.unshift(basename(existingPath));
|
|
138
|
+
existingPath = parent;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function pathIsWithin(root, candidate) {
|
|
143
|
+
const relativePath = relative(root, candidate);
|
|
144
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
145
|
+
}
|
|
146
|
+
function isGlobalRecordingsStatePath(path, env = process.env) {
|
|
147
|
+
const globalStateRoot = join(stateParent(env), "recordings");
|
|
148
|
+
return pathIsWithin(resolve(globalStateRoot), resolve(path)) || pathIsWithin(canonicalPotentialPath(globalStateRoot), canonicalPotentialPath(path));
|
|
149
|
+
}
|
|
150
|
+
function removeActiveLease(activeLease, releasedLease) {
|
|
151
|
+
try {
|
|
152
|
+
renameSync(activeLease, releasedLease);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.code !== "ENOENT")
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
rmSync(releasedLease, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
function acquireLocalStoreReaderLease(env = process.env) {
|
|
160
|
+
assertMaintenanceAbsent(env);
|
|
161
|
+
const readerRoot = ensureReaderRoot(env);
|
|
162
|
+
const nonce = randomUUID();
|
|
163
|
+
const pendingLease = join(readerRoot, `.pending-${process.pid}-${nonce}`);
|
|
164
|
+
const activeLease = join(readerRoot, `lease-${process.pid}-${nonce}`);
|
|
165
|
+
const releasedLease = join(readerRoot, `.released-${process.pid}-${nonce}`);
|
|
166
|
+
let active = false;
|
|
167
|
+
try {
|
|
168
|
+
mkdirSync(pendingLease, { mode: 448 });
|
|
169
|
+
const owner = join(pendingLease, "owner");
|
|
170
|
+
writeFileSync(owner, `${process.pid}
|
|
171
|
+
${processStartIdentity()}
|
|
172
|
+
`, {
|
|
173
|
+
encoding: "utf8",
|
|
174
|
+
flag: "wx",
|
|
175
|
+
mode: 384
|
|
176
|
+
});
|
|
177
|
+
chmodSync(owner, 384);
|
|
178
|
+
renameSync(pendingLease, activeLease);
|
|
179
|
+
active = true;
|
|
180
|
+
assertMaintenanceAbsent(env);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (active)
|
|
183
|
+
removeActiveLease(activeLease, releasedLease);
|
|
184
|
+
else
|
|
185
|
+
rmSync(pendingLease, { recursive: true, force: true });
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
let released = false;
|
|
189
|
+
return () => {
|
|
190
|
+
if (released)
|
|
191
|
+
return;
|
|
192
|
+
removeActiveLease(activeLease, releasedLease);
|
|
193
|
+
released = true;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function withLocalStoreReaderLease(operation, env = process.env) {
|
|
197
|
+
const release = acquireLocalStoreReaderLease(env);
|
|
198
|
+
try {
|
|
199
|
+
return await operation();
|
|
200
|
+
} finally {
|
|
201
|
+
release();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/lib/config.ts
|
|
206
|
+
var POST_PROCESSING_MODES = new Set([
|
|
207
|
+
"off",
|
|
208
|
+
"auto",
|
|
209
|
+
"always"
|
|
210
|
+
]);
|
|
211
|
+
var DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
|
|
212
|
+
var DEFAULT_REALTIME_SESSION_MODEL = "gpt-realtime";
|
|
213
|
+
var DEFAULT_REALTIME_TRANSCRIPTION_MODEL = "gpt-realtime-whisper";
|
|
43
214
|
var DEFAULT_CONFIG = {
|
|
44
215
|
openai_api_key: "",
|
|
45
216
|
enhancement_api_key: "",
|
|
46
|
-
transcription_model:
|
|
217
|
+
transcription_model: DEFAULT_TRANSCRIPTION_MODEL,
|
|
218
|
+
realtime_session_model: DEFAULT_REALTIME_SESSION_MODEL,
|
|
219
|
+
realtime_transcription_model: DEFAULT_REALTIME_TRANSCRIPTION_MODEL,
|
|
47
220
|
enhancement_model: "gpt-4o",
|
|
221
|
+
transcriber_model: "gpt-4o",
|
|
48
222
|
language: "en",
|
|
49
223
|
audio_format: "wav",
|
|
50
224
|
sample_rate: 16000,
|
|
51
225
|
record_command: "sox",
|
|
52
226
|
hotkey: "space",
|
|
227
|
+
transcription_prompt: "",
|
|
228
|
+
transcriber_prompt: "",
|
|
229
|
+
post_processing_mode: "auto",
|
|
53
230
|
auto_enhance: true,
|
|
54
231
|
enhance_triggers: [
|
|
55
232
|
"say it better",
|
|
@@ -66,17 +243,23 @@ var DEFAULT_CONFIG = {
|
|
|
66
243
|
keyword_transforms: {},
|
|
67
244
|
db_path: "",
|
|
68
245
|
audio_dir: "",
|
|
69
|
-
max_recording_seconds: 1800
|
|
246
|
+
max_recording_seconds: 1800,
|
|
247
|
+
config_warnings: []
|
|
70
248
|
};
|
|
71
249
|
function loadConfig(configPath) {
|
|
72
250
|
const config = { ...DEFAULT_CONFIG };
|
|
73
|
-
|
|
251
|
+
config.config_warnings = [];
|
|
252
|
+
let explicitPostProcessingMode = false;
|
|
253
|
+
let explicitTranscriberModel = false;
|
|
254
|
+
const filePath = configPath || findConfigFile() || join2(getDataDir(), "config.json");
|
|
74
255
|
let fileProvidedOpenAIKey = false;
|
|
75
256
|
if (existsSync(filePath)) {
|
|
76
257
|
try {
|
|
77
258
|
const raw = readFileSync(filePath, "utf-8");
|
|
78
259
|
const fileConfig = JSON.parse(raw);
|
|
79
260
|
const expanded = expandEnvBackedConfig(fileConfig);
|
|
261
|
+
explicitPostProcessingMode = typeof fileConfig.post_processing_mode === "string";
|
|
262
|
+
explicitTranscriberModel = typeof fileConfig.transcriber_model === "string";
|
|
80
263
|
Object.assign(config, expanded);
|
|
81
264
|
fileProvidedOpenAIKey = typeof expanded.openai_api_key === "string" && expanded.openai_api_key.length > 0;
|
|
82
265
|
} catch {}
|
|
@@ -93,12 +276,35 @@ function loadConfig(configPath) {
|
|
|
93
276
|
if (process.env.RECORDINGS_MODEL) {
|
|
94
277
|
config.transcription_model = process.env.RECORDINGS_MODEL;
|
|
95
278
|
}
|
|
279
|
+
if (process.env.RECORDINGS_REALTIME_SESSION_MODEL) {
|
|
280
|
+
config.realtime_session_model = process.env.RECORDINGS_REALTIME_SESSION_MODEL;
|
|
281
|
+
}
|
|
282
|
+
if (process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL) {
|
|
283
|
+
config.realtime_transcription_model = process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL;
|
|
284
|
+
}
|
|
96
285
|
if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
|
|
97
286
|
config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
|
|
98
287
|
}
|
|
288
|
+
if (process.env.RECORDINGS_TRANSCRIBER_MODEL) {
|
|
289
|
+
config.transcriber_model = process.env.RECORDINGS_TRANSCRIBER_MODEL;
|
|
290
|
+
explicitTranscriberModel = true;
|
|
291
|
+
}
|
|
99
292
|
if (process.env.RECORDINGS_LANGUAGE) {
|
|
100
293
|
config.language = process.env.RECORDINGS_LANGUAGE;
|
|
101
294
|
}
|
|
295
|
+
if (process.env.RECORDINGS_TRANSCRIPTION_PROMPT) {
|
|
296
|
+
config.transcription_prompt = process.env.RECORDINGS_TRANSCRIPTION_PROMPT;
|
|
297
|
+
}
|
|
298
|
+
if (process.env.RECORDINGS_TRANSCRIBER_PROMPT) {
|
|
299
|
+
config.transcriber_prompt = process.env.RECORDINGS_TRANSCRIBER_PROMPT;
|
|
300
|
+
}
|
|
301
|
+
if (process.env.RECORDINGS_POST_PROCESSING_MODE) {
|
|
302
|
+
config.post_processing_mode = normalizePostProcessingMode(process.env.RECORDINGS_POST_PROCESSING_MODE, config.post_processing_mode ?? "auto");
|
|
303
|
+
explicitPostProcessingMode = true;
|
|
304
|
+
}
|
|
305
|
+
if (process.env.RECORDINGS_AUTO_ENHANCE) {
|
|
306
|
+
config.auto_enhance = parseBooleanEnv(process.env.RECORDINGS_AUTO_ENHANCE, config.auto_enhance);
|
|
307
|
+
}
|
|
102
308
|
if (process.env.HASNA_RECORDINGS_DB_PATH) {
|
|
103
309
|
config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
|
|
104
310
|
} else if (process.env.RECORDINGS_DB_PATH) {
|
|
@@ -116,14 +322,86 @@ function loadConfig(configPath) {
|
|
|
116
322
|
if (!config.enhancement_api_key) {
|
|
117
323
|
config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
|
|
118
324
|
}
|
|
325
|
+
if (!explicitTranscriberModel) {
|
|
326
|
+
config.transcriber_model = config.enhancement_model;
|
|
327
|
+
}
|
|
328
|
+
normalizeModelSlots(config);
|
|
329
|
+
normalizePostProcessingConfig(config, explicitPostProcessingMode);
|
|
119
330
|
if (!config.db_path) {
|
|
120
|
-
config.db_path =
|
|
331
|
+
config.db_path = join2(getDataDir(), "recordings.db");
|
|
121
332
|
}
|
|
122
333
|
if (!config.audio_dir) {
|
|
123
|
-
config.audio_dir =
|
|
334
|
+
config.audio_dir = join2(getDataDir(), "audio");
|
|
335
|
+
}
|
|
336
|
+
return config;
|
|
337
|
+
}
|
|
338
|
+
function normalizeModelSlots(config) {
|
|
339
|
+
const warnings = config.config_warnings ?? [];
|
|
340
|
+
config.config_warnings = warnings;
|
|
341
|
+
const boundedModel = config.transcription_model?.trim() || DEFAULT_TRANSCRIPTION_MODEL;
|
|
342
|
+
if (isRealtimeOnlyModel(boundedModel)) {
|
|
343
|
+
warnings.push(`Ignoring RECORDINGS_MODEL=${boundedModel}; bounded transcription uses ${DEFAULT_TRANSCRIPTION_MODEL}.`);
|
|
344
|
+
config.transcription_model = DEFAULT_TRANSCRIPTION_MODEL;
|
|
345
|
+
} else {
|
|
346
|
+
config.transcription_model = boundedModel;
|
|
347
|
+
}
|
|
348
|
+
const realtimeSessionModel = config.realtime_session_model?.trim() || DEFAULT_REALTIME_SESSION_MODEL;
|
|
349
|
+
if (isTranscriptionOnlyModel(realtimeSessionModel)) {
|
|
350
|
+
warnings.push(`Ignoring realtime session model ${realtimeSessionModel}; use ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL} as realtime_transcription_model instead.`);
|
|
351
|
+
config.realtime_session_model = DEFAULT_REALTIME_SESSION_MODEL;
|
|
352
|
+
} else {
|
|
353
|
+
config.realtime_session_model = realtimeSessionModel;
|
|
354
|
+
}
|
|
355
|
+
const realtimeTranscriptionModel = config.realtime_transcription_model?.trim() || DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
356
|
+
if (!isRealtimeTranscriptionModel(realtimeTranscriptionModel)) {
|
|
357
|
+
warnings.push(`Ignoring realtime transcription model ${realtimeTranscriptionModel}; realtime transcription uses ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL}.`);
|
|
358
|
+
config.realtime_transcription_model = DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
359
|
+
} else {
|
|
360
|
+
config.realtime_transcription_model = realtimeTranscriptionModel;
|
|
124
361
|
}
|
|
125
362
|
return config;
|
|
126
363
|
}
|
|
364
|
+
function isTranscriptionOnlyModel(model) {
|
|
365
|
+
const m = model.trim().toLowerCase();
|
|
366
|
+
return m === "whisper-1" || m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.includes("transcribe");
|
|
367
|
+
}
|
|
368
|
+
function isRealtimeOnlyModel(model) {
|
|
369
|
+
const m = model.trim().toLowerCase();
|
|
370
|
+
return m.startsWith("gpt-realtime");
|
|
371
|
+
}
|
|
372
|
+
function isRealtimeTranscriptionModel(model) {
|
|
373
|
+
const m = model.trim().toLowerCase();
|
|
374
|
+
return m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.startsWith("gpt-realtime") && m.includes("whisper");
|
|
375
|
+
}
|
|
376
|
+
function normalizePostProcessingMode(value, fallback = "auto") {
|
|
377
|
+
const mode = value?.trim().toLowerCase();
|
|
378
|
+
if (mode && POST_PROCESSING_MODES.has(mode)) {
|
|
379
|
+
return mode;
|
|
380
|
+
}
|
|
381
|
+
return fallback;
|
|
382
|
+
}
|
|
383
|
+
function normalizePostProcessingConfig(config, preferPostProcessingMode = true) {
|
|
384
|
+
if (preferPostProcessingMode) {
|
|
385
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
386
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
387
|
+
return config;
|
|
388
|
+
}
|
|
389
|
+
if (config.auto_enhance === false) {
|
|
390
|
+
config.post_processing_mode = "off";
|
|
391
|
+
} else {
|
|
392
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
393
|
+
}
|
|
394
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
395
|
+
return config;
|
|
396
|
+
}
|
|
397
|
+
function parseBooleanEnv(value, fallback) {
|
|
398
|
+
const normalized = value.trim().toLowerCase();
|
|
399
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
400
|
+
return true;
|
|
401
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
402
|
+
return false;
|
|
403
|
+
return fallback;
|
|
404
|
+
}
|
|
127
405
|
function expandEnvBackedConfig(config) {
|
|
128
406
|
const expanded = { ...config };
|
|
129
407
|
for (const key of ["openai_api_key", "enhancement_api_key"]) {
|
|
@@ -142,36 +420,68 @@ function getDataDir() {
|
|
|
142
420
|
if (projectLocalDir)
|
|
143
421
|
return projectLocalDir;
|
|
144
422
|
const home = getHomeDir();
|
|
145
|
-
const newDir =
|
|
146
|
-
const oldDir =
|
|
423
|
+
const newDir = join2(home, ".hasna", "recordings");
|
|
424
|
+
const oldDir = join2(home, ".recordings");
|
|
147
425
|
if (existsSync(oldDir)) {
|
|
426
|
+
const releaseLease = acquireLocalStoreReaderLease();
|
|
148
427
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
428
|
+
try {
|
|
429
|
+
if (existsSync(oldDir))
|
|
430
|
+
mergeDirectoryContents(oldDir, newDir);
|
|
431
|
+
} catch {}
|
|
432
|
+
} finally {
|
|
433
|
+
releaseLease();
|
|
434
|
+
}
|
|
151
435
|
}
|
|
152
436
|
return newDir;
|
|
153
437
|
}
|
|
154
438
|
function findProjectRecordingsPath(entry) {
|
|
155
|
-
const
|
|
156
|
-
|
|
439
|
+
const cwd = canonicalExistingPath(process.cwd());
|
|
440
|
+
const home = canonicalExistingPath(getHomeDir());
|
|
441
|
+
const repositoryRoot = findRepositoryRoot(cwd);
|
|
442
|
+
const cwdIsInsideHome = cwd === home || cwd.startsWith(`${home}${sep2}`);
|
|
443
|
+
const repositoryIsInsideHome = repositoryRoot !== null && repositoryRoot !== home && repositoryRoot.startsWith(`${home}${sep2}`);
|
|
444
|
+
const boundary = cwdIsInsideHome ? repositoryIsInsideHome ? repositoryRoot : home : repositoryRoot ?? cwd;
|
|
445
|
+
const excludeBoundary = boundary === home;
|
|
446
|
+
let dir = cwd;
|
|
157
447
|
while (true) {
|
|
158
|
-
if (dir
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
448
|
+
if (excludeBoundary && dir === boundary)
|
|
449
|
+
break;
|
|
450
|
+
const candidate = entry ? join2(dir, ".recordings", entry) : join2(dir, ".recordings");
|
|
451
|
+
if (existsSync(candidate))
|
|
452
|
+
return candidate;
|
|
453
|
+
if (dir === boundary)
|
|
454
|
+
break;
|
|
455
|
+
const parent = dirname2(dir);
|
|
164
456
|
if (parent === dir)
|
|
165
457
|
break;
|
|
166
458
|
dir = parent;
|
|
167
459
|
}
|
|
168
460
|
return null;
|
|
169
461
|
}
|
|
462
|
+
function findRepositoryRoot(start) {
|
|
463
|
+
let dir = start;
|
|
464
|
+
while (true) {
|
|
465
|
+
if (existsSync(join2(dir, ".git")))
|
|
466
|
+
return dir;
|
|
467
|
+
const parent = dirname2(dir);
|
|
468
|
+
if (parent === dir)
|
|
469
|
+
return null;
|
|
470
|
+
dir = parent;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function canonicalExistingPath(path) {
|
|
474
|
+
try {
|
|
475
|
+
return realpathSync2(path);
|
|
476
|
+
} catch {
|
|
477
|
+
return resolve2(path);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
170
480
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
171
|
-
|
|
481
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
172
482
|
for (const entry of readdirSync(sourceDir)) {
|
|
173
|
-
const sourcePath =
|
|
174
|
-
const targetPath =
|
|
483
|
+
const sourcePath = join2(sourceDir, entry);
|
|
484
|
+
const targetPath = join2(targetDir, entry);
|
|
175
485
|
const sourceStats = statSync(sourcePath);
|
|
176
486
|
if (sourceStats.isDirectory()) {
|
|
177
487
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
@@ -181,7 +491,7 @@ function mergeDirectoryContents(sourceDir, targetDir) {
|
|
|
181
491
|
}
|
|
182
492
|
}
|
|
183
493
|
function loadSecretKey(keyName) {
|
|
184
|
-
const secretsPath =
|
|
494
|
+
const secretsPath = join2(getHomeDir(), ".secrets");
|
|
185
495
|
if (!existsSync(secretsPath))
|
|
186
496
|
return "";
|
|
187
497
|
for (const candidate of listSecretFiles(secretsPath)) {
|
|
@@ -208,7 +518,7 @@ function listSecretFiles(path) {
|
|
|
208
518
|
if (!stats.isDirectory())
|
|
209
519
|
return [];
|
|
210
520
|
return readdirSync(path).sort().flatMap((entry) => {
|
|
211
|
-
const child =
|
|
521
|
+
const child = join2(path, entry);
|
|
212
522
|
try {
|
|
213
523
|
const childStats = statSync(child);
|
|
214
524
|
if (childStats.isDirectory())
|
|
@@ -223,14 +533,19 @@ function listSecretFiles(path) {
|
|
|
223
533
|
}
|
|
224
534
|
}
|
|
225
535
|
function getHomeDir() {
|
|
226
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
536
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
227
537
|
}
|
|
228
538
|
function ensureDataDir(config) {
|
|
229
|
-
const { mkdirSync:
|
|
230
|
-
mkdirSync2(config.audio_dir, { recursive: true });
|
|
539
|
+
const { mkdirSync: mkdirSync3 } = __require("fs");
|
|
231
540
|
const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
|
|
232
|
-
|
|
233
|
-
|
|
541
|
+
const releaseLease = [config.audio_dir, dbDir].some((path) => path.length > 0 && isGlobalRecordingsStatePath(path)) ? acquireLocalStoreReaderLease() : () => {};
|
|
542
|
+
try {
|
|
543
|
+
mkdirSync3(config.audio_dir, { recursive: true });
|
|
544
|
+
if (dbDir)
|
|
545
|
+
mkdirSync3(dbDir, { recursive: true });
|
|
546
|
+
} finally {
|
|
547
|
+
releaseLease();
|
|
548
|
+
}
|
|
234
549
|
}
|
|
235
550
|
|
|
236
551
|
// src/db/database.ts
|
|
@@ -319,8 +634,8 @@ function getDatabase(dbPath) {
|
|
|
319
634
|
if (_db)
|
|
320
635
|
return _db;
|
|
321
636
|
const path = dbPath || loadConfig().db_path;
|
|
322
|
-
const dir =
|
|
323
|
-
|
|
637
|
+
const dir = dirname3(path);
|
|
638
|
+
mkdirSync3(dir, { recursive: true });
|
|
324
639
|
_adapter = new SqliteAdapter(path);
|
|
325
640
|
_db = _adapter.raw;
|
|
326
641
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
@@ -376,6 +691,63 @@ function shortUuid() {
|
|
|
376
691
|
return crypto.randomUUID().slice(0, 8);
|
|
377
692
|
}
|
|
378
693
|
|
|
694
|
+
// src/db/errors.ts
|
|
695
|
+
class ProjectNotFoundError extends Error {
|
|
696
|
+
ref;
|
|
697
|
+
constructor(ref) {
|
|
698
|
+
super(`project not found: ${ref}`);
|
|
699
|
+
this.name = "ProjectNotFoundError";
|
|
700
|
+
this.ref = ref;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
class ValidationError extends Error {
|
|
705
|
+
constructor(message) {
|
|
706
|
+
super(message);
|
|
707
|
+
this.name = "ValidationError";
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/lib/recording-create-identity.ts
|
|
712
|
+
var MAX_RECORDING_IDENTITY_LENGTH = 255;
|
|
713
|
+
function validateIdentityValue(value, label, nullMeansAbsent = false) {
|
|
714
|
+
if (value === undefined || nullMeansAbsent && value === null)
|
|
715
|
+
return;
|
|
716
|
+
if (typeof value !== "string") {
|
|
717
|
+
throw new ValidationError(`${label} must be a string`);
|
|
718
|
+
}
|
|
719
|
+
if (value.length === 0) {
|
|
720
|
+
throw new ValidationError(`${label} must not be empty`);
|
|
721
|
+
}
|
|
722
|
+
if (value.length > MAX_RECORDING_IDENTITY_LENGTH) {
|
|
723
|
+
throw new ValidationError(`${label} must not exceed ${MAX_RECORDING_IDENTITY_LENGTH} characters`);
|
|
724
|
+
}
|
|
725
|
+
if (value !== value.trim()) {
|
|
726
|
+
throw new ValidationError(`${label} must not contain leading or trailing whitespace`);
|
|
727
|
+
}
|
|
728
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
729
|
+
throw new ValidationError(`${label} must not contain control characters`);
|
|
730
|
+
}
|
|
731
|
+
if (/[^\u0020-\u007e]/.test(value)) {
|
|
732
|
+
throw new ValidationError(`${label} must contain only printable ASCII characters`);
|
|
733
|
+
}
|
|
734
|
+
return value;
|
|
735
|
+
}
|
|
736
|
+
function recordingCreateIdentity(input, idempotencyKey, options = {}) {
|
|
737
|
+
const bodyId = validateIdentityValue(input.id, "recording id", true);
|
|
738
|
+
const headerKey = validateIdentityValue(idempotencyKey, "idempotency key");
|
|
739
|
+
if (bodyId !== undefined && headerKey !== undefined && bodyId !== headerKey) {
|
|
740
|
+
throw new ValidationError("recording id conflicts with idempotency key");
|
|
741
|
+
}
|
|
742
|
+
const effectiveKey = headerKey ?? bodyId;
|
|
743
|
+
const { id: _runtimeId, ...inputWithoutId } = input;
|
|
744
|
+
const persistedId = options.bindIdempotencyKeyToId === false ? bodyId : effectiveKey;
|
|
745
|
+
return {
|
|
746
|
+
input: persistedId === undefined ? inputWithoutId : { ...inputWithoutId, id: persistedId },
|
|
747
|
+
idempotencyKey: effectiveKey
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
379
751
|
// src/db/recordings.ts
|
|
380
752
|
function parseRow(row) {
|
|
381
753
|
return {
|
|
@@ -400,20 +772,36 @@ function parseRow(row) {
|
|
|
400
772
|
created_at: row["created_at"]
|
|
401
773
|
};
|
|
402
774
|
}
|
|
403
|
-
function createRecording(input, db) {
|
|
775
|
+
function createRecording(input, db, idempotencyKey) {
|
|
404
776
|
const d = db || getDatabase();
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
for (const tag of input.tags) {
|
|
413
|
-
insertTag.run(id, tag);
|
|
777
|
+
input = recordingCreateIdentity(input, idempotencyKey).input;
|
|
778
|
+
const id = input.id || shortUuid();
|
|
779
|
+
const create = d.transaction(() => {
|
|
780
|
+
if (input.id) {
|
|
781
|
+
const existing = getRecording(input.id, d);
|
|
782
|
+
if (existing?.id === input.id)
|
|
783
|
+
return existing;
|
|
414
784
|
}
|
|
415
|
-
|
|
416
|
-
|
|
785
|
+
const tagsJson = JSON.stringify(input.tags || []);
|
|
786
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
787
|
+
const insertResult = d.query(`INSERT INTO recordings (id, audio_path, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
|
|
788
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
789
|
+
ON CONFLICT(id) DO NOTHING`).run(id, input.audio_path || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, metadataJson);
|
|
790
|
+
if (insertResult.changes === 0) {
|
|
791
|
+
const existing = getRecording(id, d);
|
|
792
|
+
if (existing?.id === id)
|
|
793
|
+
return existing;
|
|
794
|
+
throw new Error("recording id conflict could not be read back");
|
|
795
|
+
}
|
|
796
|
+
if (input.tags && input.tags.length > 0) {
|
|
797
|
+
const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
|
|
798
|
+
for (const tag of input.tags) {
|
|
799
|
+
insertTag.run(id, tag);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return getRecording(id, d);
|
|
803
|
+
});
|
|
804
|
+
return create();
|
|
417
805
|
}
|
|
418
806
|
function getRecording(id, db) {
|
|
419
807
|
const d = db || getDatabase();
|
|
@@ -425,6 +813,20 @@ function getRecording(id, db) {
|
|
|
425
813
|
}
|
|
426
814
|
function listRecordings(filter, db) {
|
|
427
815
|
const d = db || getDatabase();
|
|
816
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
817
|
+
const limit = filter?.limit || 50;
|
|
818
|
+
const offset = filter?.offset || 0;
|
|
819
|
+
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
820
|
+
const rows = d.query(sql).all(...params, limit, offset);
|
|
821
|
+
return rows.map(parseRow);
|
|
822
|
+
}
|
|
823
|
+
function countRecordings(filter, db) {
|
|
824
|
+
const d = db || getDatabase();
|
|
825
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
826
|
+
const row = d.query(`SELECT COUNT(*) as c FROM recordings ${where}`).get(...params);
|
|
827
|
+
return row.c;
|
|
828
|
+
}
|
|
829
|
+
function buildRecordingWhere(filter) {
|
|
428
830
|
const conditions = [];
|
|
429
831
|
const params = [];
|
|
430
832
|
if (filter?.agent_id) {
|
|
@@ -463,12 +865,7 @@ function listRecordings(filter, db) {
|
|
|
463
865
|
params.push(filter.until);
|
|
464
866
|
}
|
|
465
867
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
466
|
-
|
|
467
|
-
const offset = filter?.offset || 0;
|
|
468
|
-
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
469
|
-
params.push(limit, offset);
|
|
470
|
-
const rows = d.query(sql).all(...params);
|
|
471
|
-
return rows.map(parseRow);
|
|
868
|
+
return { where, params };
|
|
472
869
|
}
|
|
473
870
|
function deleteRecording(id, db) {
|
|
474
871
|
const d = db || getDatabase();
|
|
@@ -541,16 +938,6 @@ function listProjects(db) {
|
|
|
541
938
|
return rows.map(parseProject);
|
|
542
939
|
}
|
|
543
940
|
|
|
544
|
-
// src/db/errors.ts
|
|
545
|
-
class ProjectNotFoundError extends Error {
|
|
546
|
-
ref;
|
|
547
|
-
constructor(ref) {
|
|
548
|
-
super(`project not found: ${ref}`);
|
|
549
|
-
this.name = "ProjectNotFoundError";
|
|
550
|
-
this.ref = ref;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
941
|
// src/db/agents.ts
|
|
555
942
|
function parseAgent(row) {
|
|
556
943
|
return {
|
|
@@ -616,7 +1003,7 @@ function setAgentFocus(idOrName, projectId, db) {
|
|
|
616
1003
|
}
|
|
617
1004
|
|
|
618
1005
|
// src/version.ts
|
|
619
|
-
var VERSION = "0.2.
|
|
1006
|
+
var VERSION = "0.2.13";
|
|
620
1007
|
|
|
621
1008
|
// src/db/feedback.ts
|
|
622
1009
|
function saveFeedback(input) {
|
|
@@ -909,6 +1296,7 @@ function resolveStorageClient(name, env = process.env, fetchImpl) {
|
|
|
909
1296
|
}
|
|
910
1297
|
|
|
911
1298
|
// src/store.ts
|
|
1299
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
912
1300
|
var APP = "recordings";
|
|
913
1301
|
function listQuery(filter) {
|
|
914
1302
|
if (!filter)
|
|
@@ -918,6 +1306,7 @@ function listQuery(filter) {
|
|
|
918
1306
|
project_id: filter.project_id,
|
|
919
1307
|
session_id: filter.session_id,
|
|
920
1308
|
processing_mode: filter.processing_mode,
|
|
1309
|
+
tags: filter.tags,
|
|
921
1310
|
search: filter.search,
|
|
922
1311
|
since: filter.since,
|
|
923
1312
|
until: filter.until,
|
|
@@ -934,58 +1323,63 @@ function unwrap(res, key) {
|
|
|
934
1323
|
var localStore = {
|
|
935
1324
|
mode: "local",
|
|
936
1325
|
baseUrl: null,
|
|
937
|
-
async createRecording(input) {
|
|
938
|
-
return createRecording(input);
|
|
1326
|
+
async createRecording(input, idempotencyKey) {
|
|
1327
|
+
return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
|
|
939
1328
|
},
|
|
940
1329
|
async getRecording(id) {
|
|
941
|
-
return getRecording(id);
|
|
1330
|
+
return withLocalStoreReaderLease(() => getRecording(id));
|
|
942
1331
|
},
|
|
943
1332
|
async listRecordings(filter) {
|
|
944
|
-
return listRecordings(filter);
|
|
1333
|
+
return withLocalStoreReaderLease(() => listRecordings(filter));
|
|
1334
|
+
},
|
|
1335
|
+
async countRecordings(filter) {
|
|
1336
|
+
return withLocalStoreReaderLease(() => countRecordings(filter));
|
|
945
1337
|
},
|
|
946
1338
|
async searchRecordings(query, filter) {
|
|
947
|
-
return searchRecordings(query, filter);
|
|
1339
|
+
return withLocalStoreReaderLease(() => searchRecordings(query, filter));
|
|
948
1340
|
},
|
|
949
1341
|
async deleteRecording(id) {
|
|
950
|
-
return deleteRecording(id);
|
|
1342
|
+
return withLocalStoreReaderLease(() => deleteRecording(id));
|
|
951
1343
|
},
|
|
952
1344
|
async getRecordingStats() {
|
|
953
|
-
return getRecordingStats();
|
|
1345
|
+
return withLocalStoreReaderLease(() => getRecordingStats());
|
|
954
1346
|
},
|
|
955
1347
|
async registerAgent(name, description, role) {
|
|
956
|
-
return registerAgent(name, description, role);
|
|
1348
|
+
return withLocalStoreReaderLease(() => registerAgent(name, description, role));
|
|
957
1349
|
},
|
|
958
1350
|
async getAgent(idOrName) {
|
|
959
|
-
return getAgent(idOrName);
|
|
1351
|
+
return withLocalStoreReaderLease(() => getAgent(idOrName));
|
|
960
1352
|
},
|
|
961
1353
|
async listAgents() {
|
|
962
|
-
return listAgents();
|
|
1354
|
+
return withLocalStoreReaderLease(() => listAgents());
|
|
963
1355
|
},
|
|
964
1356
|
async heartbeatAgent(idOrName) {
|
|
965
|
-
return heartbeatAgent(idOrName);
|
|
1357
|
+
return withLocalStoreReaderLease(() => heartbeatAgent(idOrName));
|
|
966
1358
|
},
|
|
967
1359
|
async setAgentFocus(idOrName, projectId) {
|
|
968
|
-
return setAgentFocus(idOrName, projectId);
|
|
1360
|
+
return withLocalStoreReaderLease(() => setAgentFocus(idOrName, projectId));
|
|
969
1361
|
},
|
|
970
1362
|
async registerProject(name, path, description) {
|
|
971
|
-
return registerProject(name, path, description);
|
|
1363
|
+
return withLocalStoreReaderLease(() => registerProject(name, path, description));
|
|
972
1364
|
},
|
|
973
1365
|
async getProject(idOrPath) {
|
|
974
|
-
return getProject(idOrPath);
|
|
1366
|
+
return withLocalStoreReaderLease(() => getProject(idOrPath));
|
|
975
1367
|
},
|
|
976
1368
|
async listProjects() {
|
|
977
|
-
return listProjects();
|
|
1369
|
+
return withLocalStoreReaderLease(() => listProjects());
|
|
978
1370
|
},
|
|
979
1371
|
async saveFeedback(input) {
|
|
980
|
-
saveFeedback(input);
|
|
1372
|
+
await withLocalStoreReaderLease(() => saveFeedback(input));
|
|
981
1373
|
}
|
|
982
1374
|
};
|
|
983
1375
|
function apiStore(client) {
|
|
984
1376
|
return {
|
|
985
1377
|
mode: "cloud-http",
|
|
986
1378
|
baseUrl: client.baseUrl,
|
|
987
|
-
async createRecording(input) {
|
|
988
|
-
const
|
|
1379
|
+
async createRecording(input, idempotencyKey) {
|
|
1380
|
+
const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
|
|
1381
|
+
const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
|
|
1382
|
+
const res = await client.create("recordings", identity.input, identity.idempotencyKey);
|
|
989
1383
|
return unwrap(res, "recording");
|
|
990
1384
|
},
|
|
991
1385
|
async getRecording(id) {
|
|
@@ -996,6 +1390,36 @@ function apiStore(client) {
|
|
|
996
1390
|
const { items } = await client.list("recordings", listQuery(filter));
|
|
997
1391
|
return items;
|
|
998
1392
|
},
|
|
1393
|
+
async countRecordings(filter) {
|
|
1394
|
+
const pageLimit = 500;
|
|
1395
|
+
const maxPageRequests = 1e4;
|
|
1396
|
+
let offset = 0;
|
|
1397
|
+
let pageRequests = 0;
|
|
1398
|
+
const seenPageKeys = new Set;
|
|
1399
|
+
while (pageRequests < maxPageRequests) {
|
|
1400
|
+
pageRequests += 1;
|
|
1401
|
+
const { items, raw } = await client.list("recordings", {
|
|
1402
|
+
...listQuery(filter),
|
|
1403
|
+
limit: pageLimit,
|
|
1404
|
+
offset
|
|
1405
|
+
});
|
|
1406
|
+
const count = raw && typeof raw === "object" ? raw.count : undefined;
|
|
1407
|
+
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
1408
|
+
throw new Error("Recordings API response is missing a valid count");
|
|
1409
|
+
}
|
|
1410
|
+
if (count > items.length)
|
|
1411
|
+
return count;
|
|
1412
|
+
if (items.length === 0)
|
|
1413
|
+
return offset;
|
|
1414
|
+
offset += items.length;
|
|
1415
|
+
const pageKey = recordingPageFingerprint(items);
|
|
1416
|
+
if (seenPageKeys.has(pageKey)) {
|
|
1417
|
+
throw new Error("Recordings API ignored pagination while counting legacy results");
|
|
1418
|
+
}
|
|
1419
|
+
seenPageKeys.add(pageKey);
|
|
1420
|
+
}
|
|
1421
|
+
throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
|
|
1422
|
+
},
|
|
999
1423
|
async searchRecordings(query, filter) {
|
|
1000
1424
|
const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
|
|
1001
1425
|
return items;
|
|
@@ -1077,6 +1501,17 @@ function apiStore(client) {
|
|
|
1077
1501
|
}
|
|
1078
1502
|
};
|
|
1079
1503
|
}
|
|
1504
|
+
function recordingPageFingerprint(items) {
|
|
1505
|
+
const hash = createHash("sha256");
|
|
1506
|
+
const ids = items.map((item) => String(item.id ?? "")).sort();
|
|
1507
|
+
for (const id of ids) {
|
|
1508
|
+
hash.update(String(id.length));
|
|
1509
|
+
hash.update(":");
|
|
1510
|
+
hash.update(id);
|
|
1511
|
+
hash.update(";");
|
|
1512
|
+
}
|
|
1513
|
+
return `${items.length}:${hash.digest("hex")}`;
|
|
1514
|
+
}
|
|
1080
1515
|
var cached = null;
|
|
1081
1516
|
function getStore(env = process.env) {
|
|
1082
1517
|
if (env === process.env && cached)
|