@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/cli/index.js
CHANGED
|
@@ -900,25 +900,206 @@ function collectValues(value, previous) {
|
|
|
900
900
|
|
|
901
901
|
// src/cli/index.ts
|
|
902
902
|
import chalk from "chalk";
|
|
903
|
-
import { spawnSync } from "child_process";
|
|
904
|
-
import {
|
|
905
|
-
|
|
903
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
904
|
+
import {
|
|
905
|
+
existsSync as existsSync4,
|
|
906
|
+
readFileSync as readFileSync3,
|
|
907
|
+
readdirSync as readdirSync2
|
|
908
|
+
} from "fs";
|
|
909
|
+
import { dirname as dirname4, join as pathJoin } from "path";
|
|
906
910
|
import { fileURLToPath } from "url";
|
|
907
911
|
|
|
908
912
|
// src/lib/config.ts
|
|
909
|
-
import { copyFileSync, existsSync as existsSync2, readFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
910
|
-
import { dirname, join as
|
|
913
|
+
import { copyFileSync, existsSync as existsSync2, readFileSync, mkdirSync as mkdirSync2, readdirSync, realpathSync as realpathSync2, statSync } from "fs";
|
|
914
|
+
import { dirname as dirname2, join as join3, resolve as resolve2, sep as sep2 } from "path";
|
|
915
|
+
import { homedir as homedir3 } from "os";
|
|
916
|
+
|
|
917
|
+
// src/lib/install-maintenance.ts
|
|
918
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
919
|
+
import {
|
|
920
|
+
chmodSync,
|
|
921
|
+
lstatSync,
|
|
922
|
+
mkdirSync,
|
|
923
|
+
realpathSync,
|
|
924
|
+
renameSync,
|
|
925
|
+
rmSync,
|
|
926
|
+
writeFileSync
|
|
927
|
+
} from "fs";
|
|
911
928
|
import { homedir as homedir2 } from "os";
|
|
929
|
+
import { basename, dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
930
|
+
import { spawnSync } from "child_process";
|
|
931
|
+
var INSTALL_MAINTENANCE_MARKER_NAME = ".recordings-install-maintenance";
|
|
932
|
+
var STORE_READER_LEASES_NAME = ".recordings-store-readers";
|
|
933
|
+
function stateParent(env = process.env) {
|
|
934
|
+
const home = env["HOME"] || env["USERPROFILE"] || homedir2();
|
|
935
|
+
return join2(home, ".hasna");
|
|
936
|
+
}
|
|
937
|
+
function installMaintenanceMarkerPath(env = process.env) {
|
|
938
|
+
return join2(stateParent(env), INSTALL_MAINTENANCE_MARKER_NAME);
|
|
939
|
+
}
|
|
940
|
+
function storeReaderLeasesPath(env = process.env) {
|
|
941
|
+
return join2(stateParent(env), STORE_READER_LEASES_NAME);
|
|
942
|
+
}
|
|
943
|
+
function pathExists(path) {
|
|
944
|
+
try {
|
|
945
|
+
lstatSync(path);
|
|
946
|
+
return true;
|
|
947
|
+
} catch (error) {
|
|
948
|
+
if (error.code === "ENOENT")
|
|
949
|
+
return false;
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function assertPrivateDirectory(path) {
|
|
954
|
+
const details = lstatSync(path);
|
|
955
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
956
|
+
throw new Error(`Local Recordings coordination path is not a secure directory: ${path}`);
|
|
957
|
+
}
|
|
958
|
+
if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
|
|
959
|
+
throw new Error(`Local Recordings coordination path has an unexpected owner: ${path}`);
|
|
960
|
+
}
|
|
961
|
+
if ((details.mode & 18) !== 0) {
|
|
962
|
+
throw new Error(`Local Recordings coordination path is group/world writable: ${path}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
function ensureReaderRoot(env) {
|
|
966
|
+
const parent = stateParent(env);
|
|
967
|
+
const readerRoot = storeReaderLeasesPath(env);
|
|
968
|
+
mkdirSync(parent, { recursive: true, mode: 448 });
|
|
969
|
+
assertPrivateDirectory(parent);
|
|
970
|
+
try {
|
|
971
|
+
mkdirSync(readerRoot, { mode: 448 });
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (error.code !== "EEXIST")
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
assertPrivateDirectory(readerRoot);
|
|
977
|
+
return readerRoot;
|
|
978
|
+
}
|
|
979
|
+
function processStartIdentity() {
|
|
980
|
+
const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(process.pid)], {
|
|
981
|
+
encoding: "utf8",
|
|
982
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C", TZ: "UTC0" }
|
|
983
|
+
});
|
|
984
|
+
const identity = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
|
|
985
|
+
if (!identity) {
|
|
986
|
+
throw new Error("Could not establish the local Store reader process identity");
|
|
987
|
+
}
|
|
988
|
+
return identity;
|
|
989
|
+
}
|
|
990
|
+
function maintenanceUnavailable() {
|
|
991
|
+
return new Error("Local Recordings storage is temporarily unavailable during app installation maintenance");
|
|
992
|
+
}
|
|
993
|
+
function assertMaintenanceAbsent(env) {
|
|
994
|
+
if (pathExists(installMaintenanceMarkerPath(env)))
|
|
995
|
+
throw maintenanceUnavailable();
|
|
996
|
+
}
|
|
997
|
+
function canonicalPotentialPath(path) {
|
|
998
|
+
const absolutePath = resolve(path);
|
|
999
|
+
let existingPath = absolutePath;
|
|
1000
|
+
const missingEntries = [];
|
|
1001
|
+
while (true) {
|
|
1002
|
+
try {
|
|
1003
|
+
return join2(realpathSync(existingPath), ...missingEntries);
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
if (error.code !== "ENOENT")
|
|
1006
|
+
return absolutePath;
|
|
1007
|
+
const parent = dirname(existingPath);
|
|
1008
|
+
if (parent === existingPath)
|
|
1009
|
+
return absolutePath;
|
|
1010
|
+
missingEntries.unshift(basename(existingPath));
|
|
1011
|
+
existingPath = parent;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function pathIsWithin(root, candidate) {
|
|
1016
|
+
const relativePath = relative(root, candidate);
|
|
1017
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
1018
|
+
}
|
|
1019
|
+
function isGlobalRecordingsStatePath(path, env = process.env) {
|
|
1020
|
+
const globalStateRoot = join2(stateParent(env), "recordings");
|
|
1021
|
+
return pathIsWithin(resolve(globalStateRoot), resolve(path)) || pathIsWithin(canonicalPotentialPath(globalStateRoot), canonicalPotentialPath(path));
|
|
1022
|
+
}
|
|
1023
|
+
function removeActiveLease(activeLease, releasedLease) {
|
|
1024
|
+
try {
|
|
1025
|
+
renameSync(activeLease, releasedLease);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (error.code !== "ENOENT")
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
rmSync(releasedLease, { recursive: true, force: true });
|
|
1031
|
+
}
|
|
1032
|
+
function acquireLocalStoreReaderLease(env = process.env) {
|
|
1033
|
+
assertMaintenanceAbsent(env);
|
|
1034
|
+
const readerRoot = ensureReaderRoot(env);
|
|
1035
|
+
const nonce = randomUUID3();
|
|
1036
|
+
const pendingLease = join2(readerRoot, `.pending-${process.pid}-${nonce}`);
|
|
1037
|
+
const activeLease = join2(readerRoot, `lease-${process.pid}-${nonce}`);
|
|
1038
|
+
const releasedLease = join2(readerRoot, `.released-${process.pid}-${nonce}`);
|
|
1039
|
+
let active = false;
|
|
1040
|
+
try {
|
|
1041
|
+
mkdirSync(pendingLease, { mode: 448 });
|
|
1042
|
+
const owner = join2(pendingLease, "owner");
|
|
1043
|
+
writeFileSync(owner, `${process.pid}
|
|
1044
|
+
${processStartIdentity()}
|
|
1045
|
+
`, {
|
|
1046
|
+
encoding: "utf8",
|
|
1047
|
+
flag: "wx",
|
|
1048
|
+
mode: 384
|
|
1049
|
+
});
|
|
1050
|
+
chmodSync(owner, 384);
|
|
1051
|
+
renameSync(pendingLease, activeLease);
|
|
1052
|
+
active = true;
|
|
1053
|
+
assertMaintenanceAbsent(env);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (active)
|
|
1056
|
+
removeActiveLease(activeLease, releasedLease);
|
|
1057
|
+
else
|
|
1058
|
+
rmSync(pendingLease, { recursive: true, force: true });
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
let released = false;
|
|
1062
|
+
return () => {
|
|
1063
|
+
if (released)
|
|
1064
|
+
return;
|
|
1065
|
+
removeActiveLease(activeLease, releasedLease);
|
|
1066
|
+
released = true;
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
async function withLocalStoreReaderLease(operation, env = process.env) {
|
|
1070
|
+
const release = acquireLocalStoreReaderLease(env);
|
|
1071
|
+
try {
|
|
1072
|
+
return await operation();
|
|
1073
|
+
} finally {
|
|
1074
|
+
release();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/lib/config.ts
|
|
1079
|
+
var POST_PROCESSING_MODES = new Set([
|
|
1080
|
+
"off",
|
|
1081
|
+
"auto",
|
|
1082
|
+
"always"
|
|
1083
|
+
]);
|
|
1084
|
+
var DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
|
|
1085
|
+
var DEFAULT_REALTIME_SESSION_MODEL = "gpt-realtime";
|
|
1086
|
+
var DEFAULT_REALTIME_TRANSCRIPTION_MODEL = "gpt-realtime-whisper";
|
|
912
1087
|
var DEFAULT_CONFIG = {
|
|
913
1088
|
openai_api_key: "",
|
|
914
1089
|
enhancement_api_key: "",
|
|
915
|
-
transcription_model:
|
|
1090
|
+
transcription_model: DEFAULT_TRANSCRIPTION_MODEL,
|
|
1091
|
+
realtime_session_model: DEFAULT_REALTIME_SESSION_MODEL,
|
|
1092
|
+
realtime_transcription_model: DEFAULT_REALTIME_TRANSCRIPTION_MODEL,
|
|
916
1093
|
enhancement_model: "gpt-4o",
|
|
1094
|
+
transcriber_model: "gpt-4o",
|
|
917
1095
|
language: "en",
|
|
918
1096
|
audio_format: "wav",
|
|
919
1097
|
sample_rate: 16000,
|
|
920
1098
|
record_command: "sox",
|
|
921
1099
|
hotkey: "space",
|
|
1100
|
+
transcription_prompt: "",
|
|
1101
|
+
transcriber_prompt: "",
|
|
1102
|
+
post_processing_mode: "auto",
|
|
922
1103
|
auto_enhance: true,
|
|
923
1104
|
enhance_triggers: [
|
|
924
1105
|
"say it better",
|
|
@@ -935,17 +1116,23 @@ var DEFAULT_CONFIG = {
|
|
|
935
1116
|
keyword_transforms: {},
|
|
936
1117
|
db_path: "",
|
|
937
1118
|
audio_dir: "",
|
|
938
|
-
max_recording_seconds: 1800
|
|
1119
|
+
max_recording_seconds: 1800,
|
|
1120
|
+
config_warnings: []
|
|
939
1121
|
};
|
|
940
1122
|
function loadConfig(configPath) {
|
|
941
1123
|
const config = { ...DEFAULT_CONFIG };
|
|
942
|
-
|
|
1124
|
+
config.config_warnings = [];
|
|
1125
|
+
let explicitPostProcessingMode = false;
|
|
1126
|
+
let explicitTranscriberModel = false;
|
|
1127
|
+
const filePath = configPath || findConfigFile() || join3(getDataDir(), "config.json");
|
|
943
1128
|
let fileProvidedOpenAIKey = false;
|
|
944
1129
|
if (existsSync2(filePath)) {
|
|
945
1130
|
try {
|
|
946
1131
|
const raw = readFileSync(filePath, "utf-8");
|
|
947
1132
|
const fileConfig = JSON.parse(raw);
|
|
948
1133
|
const expanded = expandEnvBackedConfig(fileConfig);
|
|
1134
|
+
explicitPostProcessingMode = typeof fileConfig.post_processing_mode === "string";
|
|
1135
|
+
explicitTranscriberModel = typeof fileConfig.transcriber_model === "string";
|
|
949
1136
|
Object.assign(config, expanded);
|
|
950
1137
|
fileProvidedOpenAIKey = typeof expanded.openai_api_key === "string" && expanded.openai_api_key.length > 0;
|
|
951
1138
|
} catch {}
|
|
@@ -962,12 +1149,35 @@ function loadConfig(configPath) {
|
|
|
962
1149
|
if (process.env.RECORDINGS_MODEL) {
|
|
963
1150
|
config.transcription_model = process.env.RECORDINGS_MODEL;
|
|
964
1151
|
}
|
|
1152
|
+
if (process.env.RECORDINGS_REALTIME_SESSION_MODEL) {
|
|
1153
|
+
config.realtime_session_model = process.env.RECORDINGS_REALTIME_SESSION_MODEL;
|
|
1154
|
+
}
|
|
1155
|
+
if (process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL) {
|
|
1156
|
+
config.realtime_transcription_model = process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL;
|
|
1157
|
+
}
|
|
965
1158
|
if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
|
|
966
1159
|
config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
|
|
967
1160
|
}
|
|
1161
|
+
if (process.env.RECORDINGS_TRANSCRIBER_MODEL) {
|
|
1162
|
+
config.transcriber_model = process.env.RECORDINGS_TRANSCRIBER_MODEL;
|
|
1163
|
+
explicitTranscriberModel = true;
|
|
1164
|
+
}
|
|
968
1165
|
if (process.env.RECORDINGS_LANGUAGE) {
|
|
969
1166
|
config.language = process.env.RECORDINGS_LANGUAGE;
|
|
970
1167
|
}
|
|
1168
|
+
if (process.env.RECORDINGS_TRANSCRIPTION_PROMPT) {
|
|
1169
|
+
config.transcription_prompt = process.env.RECORDINGS_TRANSCRIPTION_PROMPT;
|
|
1170
|
+
}
|
|
1171
|
+
if (process.env.RECORDINGS_TRANSCRIBER_PROMPT) {
|
|
1172
|
+
config.transcriber_prompt = process.env.RECORDINGS_TRANSCRIBER_PROMPT;
|
|
1173
|
+
}
|
|
1174
|
+
if (process.env.RECORDINGS_POST_PROCESSING_MODE) {
|
|
1175
|
+
config.post_processing_mode = normalizePostProcessingMode(process.env.RECORDINGS_POST_PROCESSING_MODE, config.post_processing_mode ?? "auto");
|
|
1176
|
+
explicitPostProcessingMode = true;
|
|
1177
|
+
}
|
|
1178
|
+
if (process.env.RECORDINGS_AUTO_ENHANCE) {
|
|
1179
|
+
config.auto_enhance = parseBooleanEnv(process.env.RECORDINGS_AUTO_ENHANCE, config.auto_enhance);
|
|
1180
|
+
}
|
|
971
1181
|
if (process.env.HASNA_RECORDINGS_DB_PATH) {
|
|
972
1182
|
config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
|
|
973
1183
|
} else if (process.env.RECORDINGS_DB_PATH) {
|
|
@@ -985,14 +1195,86 @@ function loadConfig(configPath) {
|
|
|
985
1195
|
if (!config.enhancement_api_key) {
|
|
986
1196
|
config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
|
|
987
1197
|
}
|
|
1198
|
+
if (!explicitTranscriberModel) {
|
|
1199
|
+
config.transcriber_model = config.enhancement_model;
|
|
1200
|
+
}
|
|
1201
|
+
normalizeModelSlots(config);
|
|
1202
|
+
normalizePostProcessingConfig(config, explicitPostProcessingMode);
|
|
988
1203
|
if (!config.db_path) {
|
|
989
|
-
config.db_path =
|
|
1204
|
+
config.db_path = join3(getDataDir(), "recordings.db");
|
|
990
1205
|
}
|
|
991
1206
|
if (!config.audio_dir) {
|
|
992
|
-
config.audio_dir =
|
|
1207
|
+
config.audio_dir = join3(getDataDir(), "audio");
|
|
1208
|
+
}
|
|
1209
|
+
return config;
|
|
1210
|
+
}
|
|
1211
|
+
function normalizeModelSlots(config) {
|
|
1212
|
+
const warnings = config.config_warnings ?? [];
|
|
1213
|
+
config.config_warnings = warnings;
|
|
1214
|
+
const boundedModel = config.transcription_model?.trim() || DEFAULT_TRANSCRIPTION_MODEL;
|
|
1215
|
+
if (isRealtimeOnlyModel(boundedModel)) {
|
|
1216
|
+
warnings.push(`Ignoring RECORDINGS_MODEL=${boundedModel}; bounded transcription uses ${DEFAULT_TRANSCRIPTION_MODEL}.`);
|
|
1217
|
+
config.transcription_model = DEFAULT_TRANSCRIPTION_MODEL;
|
|
1218
|
+
} else {
|
|
1219
|
+
config.transcription_model = boundedModel;
|
|
1220
|
+
}
|
|
1221
|
+
const realtimeSessionModel = config.realtime_session_model?.trim() || DEFAULT_REALTIME_SESSION_MODEL;
|
|
1222
|
+
if (isTranscriptionOnlyModel(realtimeSessionModel)) {
|
|
1223
|
+
warnings.push(`Ignoring realtime session model ${realtimeSessionModel}; use ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL} as realtime_transcription_model instead.`);
|
|
1224
|
+
config.realtime_session_model = DEFAULT_REALTIME_SESSION_MODEL;
|
|
1225
|
+
} else {
|
|
1226
|
+
config.realtime_session_model = realtimeSessionModel;
|
|
1227
|
+
}
|
|
1228
|
+
const realtimeTranscriptionModel = config.realtime_transcription_model?.trim() || DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
1229
|
+
if (!isRealtimeTranscriptionModel(realtimeTranscriptionModel)) {
|
|
1230
|
+
warnings.push(`Ignoring realtime transcription model ${realtimeTranscriptionModel}; realtime transcription uses ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL}.`);
|
|
1231
|
+
config.realtime_transcription_model = DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
1232
|
+
} else {
|
|
1233
|
+
config.realtime_transcription_model = realtimeTranscriptionModel;
|
|
1234
|
+
}
|
|
1235
|
+
return config;
|
|
1236
|
+
}
|
|
1237
|
+
function isTranscriptionOnlyModel(model) {
|
|
1238
|
+
const m = model.trim().toLowerCase();
|
|
1239
|
+
return m === "whisper-1" || m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.includes("transcribe");
|
|
1240
|
+
}
|
|
1241
|
+
function isRealtimeOnlyModel(model) {
|
|
1242
|
+
const m = model.trim().toLowerCase();
|
|
1243
|
+
return m.startsWith("gpt-realtime");
|
|
1244
|
+
}
|
|
1245
|
+
function isRealtimeTranscriptionModel(model) {
|
|
1246
|
+
const m = model.trim().toLowerCase();
|
|
1247
|
+
return m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.startsWith("gpt-realtime") && m.includes("whisper");
|
|
1248
|
+
}
|
|
1249
|
+
function normalizePostProcessingMode(value, fallback = "auto") {
|
|
1250
|
+
const mode = value?.trim().toLowerCase();
|
|
1251
|
+
if (mode && POST_PROCESSING_MODES.has(mode)) {
|
|
1252
|
+
return mode;
|
|
1253
|
+
}
|
|
1254
|
+
return fallback;
|
|
1255
|
+
}
|
|
1256
|
+
function normalizePostProcessingConfig(config, preferPostProcessingMode = true) {
|
|
1257
|
+
if (preferPostProcessingMode) {
|
|
1258
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
1259
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
1260
|
+
return config;
|
|
1261
|
+
}
|
|
1262
|
+
if (config.auto_enhance === false) {
|
|
1263
|
+
config.post_processing_mode = "off";
|
|
1264
|
+
} else {
|
|
1265
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
993
1266
|
}
|
|
1267
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
994
1268
|
return config;
|
|
995
1269
|
}
|
|
1270
|
+
function parseBooleanEnv(value, fallback) {
|
|
1271
|
+
const normalized = value.trim().toLowerCase();
|
|
1272
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
1273
|
+
return true;
|
|
1274
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
1275
|
+
return false;
|
|
1276
|
+
return fallback;
|
|
1277
|
+
}
|
|
996
1278
|
function expandEnvBackedConfig(config) {
|
|
997
1279
|
const expanded = { ...config };
|
|
998
1280
|
for (const key of ["openai_api_key", "enhancement_api_key"]) {
|
|
@@ -1011,36 +1293,68 @@ function getDataDir() {
|
|
|
1011
1293
|
if (projectLocalDir)
|
|
1012
1294
|
return projectLocalDir;
|
|
1013
1295
|
const home = getHomeDir();
|
|
1014
|
-
const newDir =
|
|
1015
|
-
const oldDir =
|
|
1296
|
+
const newDir = join3(home, ".hasna", "recordings");
|
|
1297
|
+
const oldDir = join3(home, ".recordings");
|
|
1016
1298
|
if (existsSync2(oldDir)) {
|
|
1299
|
+
const releaseLease = acquireLocalStoreReaderLease();
|
|
1017
1300
|
try {
|
|
1018
|
-
|
|
1019
|
-
|
|
1301
|
+
try {
|
|
1302
|
+
if (existsSync2(oldDir))
|
|
1303
|
+
mergeDirectoryContents(oldDir, newDir);
|
|
1304
|
+
} catch {}
|
|
1305
|
+
} finally {
|
|
1306
|
+
releaseLease();
|
|
1307
|
+
}
|
|
1020
1308
|
}
|
|
1021
1309
|
return newDir;
|
|
1022
1310
|
}
|
|
1023
1311
|
function findProjectRecordingsPath(entry) {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1312
|
+
const cwd = canonicalExistingPath(process.cwd());
|
|
1313
|
+
const home = canonicalExistingPath(getHomeDir());
|
|
1314
|
+
const repositoryRoot = findRepositoryRoot(cwd);
|
|
1315
|
+
const cwdIsInsideHome = cwd === home || cwd.startsWith(`${home}${sep2}`);
|
|
1316
|
+
const repositoryIsInsideHome = repositoryRoot !== null && repositoryRoot !== home && repositoryRoot.startsWith(`${home}${sep2}`);
|
|
1317
|
+
const boundary = cwdIsInsideHome ? repositoryIsInsideHome ? repositoryRoot : home : repositoryRoot ?? cwd;
|
|
1318
|
+
const excludeBoundary = boundary === home;
|
|
1319
|
+
let dir = cwd;
|
|
1026
1320
|
while (true) {
|
|
1027
|
-
if (dir
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1321
|
+
if (excludeBoundary && dir === boundary)
|
|
1322
|
+
break;
|
|
1323
|
+
const candidate = entry ? join3(dir, ".recordings", entry) : join3(dir, ".recordings");
|
|
1324
|
+
if (existsSync2(candidate))
|
|
1325
|
+
return candidate;
|
|
1326
|
+
if (dir === boundary)
|
|
1327
|
+
break;
|
|
1328
|
+
const parent = dirname2(dir);
|
|
1033
1329
|
if (parent === dir)
|
|
1034
1330
|
break;
|
|
1035
1331
|
dir = parent;
|
|
1036
1332
|
}
|
|
1037
1333
|
return null;
|
|
1038
1334
|
}
|
|
1335
|
+
function findRepositoryRoot(start) {
|
|
1336
|
+
let dir = start;
|
|
1337
|
+
while (true) {
|
|
1338
|
+
if (existsSync2(join3(dir, ".git")))
|
|
1339
|
+
return dir;
|
|
1340
|
+
const parent = dirname2(dir);
|
|
1341
|
+
if (parent === dir)
|
|
1342
|
+
return null;
|
|
1343
|
+
dir = parent;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function canonicalExistingPath(path) {
|
|
1347
|
+
try {
|
|
1348
|
+
return realpathSync2(path);
|
|
1349
|
+
} catch {
|
|
1350
|
+
return resolve2(path);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1039
1353
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
1040
|
-
|
|
1354
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
1041
1355
|
for (const entry of readdirSync(sourceDir)) {
|
|
1042
|
-
const sourcePath =
|
|
1043
|
-
const targetPath =
|
|
1356
|
+
const sourcePath = join3(sourceDir, entry);
|
|
1357
|
+
const targetPath = join3(targetDir, entry);
|
|
1044
1358
|
const sourceStats = statSync(sourcePath);
|
|
1045
1359
|
if (sourceStats.isDirectory()) {
|
|
1046
1360
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
@@ -1050,7 +1364,7 @@ function mergeDirectoryContents(sourceDir, targetDir) {
|
|
|
1050
1364
|
}
|
|
1051
1365
|
}
|
|
1052
1366
|
function loadSecretKey(keyName) {
|
|
1053
|
-
const secretsPath =
|
|
1367
|
+
const secretsPath = join3(getHomeDir(), ".secrets");
|
|
1054
1368
|
if (!existsSync2(secretsPath))
|
|
1055
1369
|
return "";
|
|
1056
1370
|
for (const candidate of listSecretFiles(secretsPath)) {
|
|
@@ -1077,7 +1391,7 @@ function listSecretFiles(path) {
|
|
|
1077
1391
|
if (!stats.isDirectory())
|
|
1078
1392
|
return [];
|
|
1079
1393
|
return readdirSync(path).sort().flatMap((entry) => {
|
|
1080
|
-
const child =
|
|
1394
|
+
const child = join3(path, entry);
|
|
1081
1395
|
try {
|
|
1082
1396
|
const childStats = statSync(child);
|
|
1083
1397
|
if (childStats.isDirectory())
|
|
@@ -1092,14 +1406,19 @@ function listSecretFiles(path) {
|
|
|
1092
1406
|
}
|
|
1093
1407
|
}
|
|
1094
1408
|
function getHomeDir() {
|
|
1095
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
1409
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
|
|
1096
1410
|
}
|
|
1097
1411
|
function ensureDataDir(config) {
|
|
1098
|
-
const { mkdirSync:
|
|
1099
|
-
mkdirSync2(config.audio_dir, { recursive: true });
|
|
1412
|
+
const { mkdirSync: mkdirSync3 } = __require("fs");
|
|
1100
1413
|
const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
|
|
1101
|
-
|
|
1102
|
-
|
|
1414
|
+
const releaseLease = [config.audio_dir, dbDir].some((path) => path.length > 0 && isGlobalRecordingsStatePath(path)) ? acquireLocalStoreReaderLease() : () => {};
|
|
1415
|
+
try {
|
|
1416
|
+
mkdirSync3(config.audio_dir, { recursive: true });
|
|
1417
|
+
if (dbDir)
|
|
1418
|
+
mkdirSync3(dbDir, { recursive: true });
|
|
1419
|
+
} finally {
|
|
1420
|
+
releaseLease();
|
|
1421
|
+
}
|
|
1103
1422
|
}
|
|
1104
1423
|
|
|
1105
1424
|
// src/db/sqlite-adapter.ts
|
|
@@ -1134,8 +1453,8 @@ class SqliteAdapter {
|
|
|
1134
1453
|
}
|
|
1135
1454
|
|
|
1136
1455
|
// src/db/database.ts
|
|
1137
|
-
import { mkdirSync as
|
|
1138
|
-
import { dirname as
|
|
1456
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
1457
|
+
import { dirname as dirname3 } from "path";
|
|
1139
1458
|
var _db = null;
|
|
1140
1459
|
var _adapter = null;
|
|
1141
1460
|
var MIGRATIONS = [
|
|
@@ -1221,8 +1540,8 @@ function getDatabase(dbPath) {
|
|
|
1221
1540
|
if (_db)
|
|
1222
1541
|
return _db;
|
|
1223
1542
|
const path = dbPath || loadConfig().db_path;
|
|
1224
|
-
const dir =
|
|
1225
|
-
|
|
1543
|
+
const dir = dirname3(path);
|
|
1544
|
+
mkdirSync3(dir, { recursive: true });
|
|
1226
1545
|
_adapter = new SqliteAdapter(path);
|
|
1227
1546
|
_db = _adapter.raw;
|
|
1228
1547
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
@@ -1278,6 +1597,63 @@ function shortUuid() {
|
|
|
1278
1597
|
return crypto.randomUUID().slice(0, 8);
|
|
1279
1598
|
}
|
|
1280
1599
|
|
|
1600
|
+
// src/db/errors.ts
|
|
1601
|
+
class ProjectNotFoundError extends Error {
|
|
1602
|
+
ref;
|
|
1603
|
+
constructor(ref) {
|
|
1604
|
+
super(`project not found: ${ref}`);
|
|
1605
|
+
this.name = "ProjectNotFoundError";
|
|
1606
|
+
this.ref = ref;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
class ValidationError extends Error {
|
|
1611
|
+
constructor(message) {
|
|
1612
|
+
super(message);
|
|
1613
|
+
this.name = "ValidationError";
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/lib/recording-create-identity.ts
|
|
1618
|
+
var MAX_RECORDING_IDENTITY_LENGTH = 255;
|
|
1619
|
+
function validateIdentityValue(value, label, nullMeansAbsent = false) {
|
|
1620
|
+
if (value === undefined || nullMeansAbsent && value === null)
|
|
1621
|
+
return;
|
|
1622
|
+
if (typeof value !== "string") {
|
|
1623
|
+
throw new ValidationError(`${label} must be a string`);
|
|
1624
|
+
}
|
|
1625
|
+
if (value.length === 0) {
|
|
1626
|
+
throw new ValidationError(`${label} must not be empty`);
|
|
1627
|
+
}
|
|
1628
|
+
if (value.length > MAX_RECORDING_IDENTITY_LENGTH) {
|
|
1629
|
+
throw new ValidationError(`${label} must not exceed ${MAX_RECORDING_IDENTITY_LENGTH} characters`);
|
|
1630
|
+
}
|
|
1631
|
+
if (value !== value.trim()) {
|
|
1632
|
+
throw new ValidationError(`${label} must not contain leading or trailing whitespace`);
|
|
1633
|
+
}
|
|
1634
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
1635
|
+
throw new ValidationError(`${label} must not contain control characters`);
|
|
1636
|
+
}
|
|
1637
|
+
if (/[^\u0020-\u007e]/.test(value)) {
|
|
1638
|
+
throw new ValidationError(`${label} must contain only printable ASCII characters`);
|
|
1639
|
+
}
|
|
1640
|
+
return value;
|
|
1641
|
+
}
|
|
1642
|
+
function recordingCreateIdentity(input, idempotencyKey, options = {}) {
|
|
1643
|
+
const bodyId = validateIdentityValue(input.id, "recording id", true);
|
|
1644
|
+
const headerKey = validateIdentityValue(idempotencyKey, "idempotency key");
|
|
1645
|
+
if (bodyId !== undefined && headerKey !== undefined && bodyId !== headerKey) {
|
|
1646
|
+
throw new ValidationError("recording id conflicts with idempotency key");
|
|
1647
|
+
}
|
|
1648
|
+
const effectiveKey = headerKey ?? bodyId;
|
|
1649
|
+
const { id: _runtimeId, ...inputWithoutId } = input;
|
|
1650
|
+
const persistedId = options.bindIdempotencyKeyToId === false ? bodyId : effectiveKey;
|
|
1651
|
+
return {
|
|
1652
|
+
input: persistedId === undefined ? inputWithoutId : { ...inputWithoutId, id: persistedId },
|
|
1653
|
+
idempotencyKey: effectiveKey
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1281
1657
|
// src/db/recordings.ts
|
|
1282
1658
|
function parseRow(row) {
|
|
1283
1659
|
return {
|
|
@@ -1302,20 +1678,36 @@ function parseRow(row) {
|
|
|
1302
1678
|
created_at: row["created_at"]
|
|
1303
1679
|
};
|
|
1304
1680
|
}
|
|
1305
|
-
function createRecording(input, db) {
|
|
1681
|
+
function createRecording(input, db, idempotencyKey) {
|
|
1306
1682
|
const d = db || getDatabase();
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
for (const tag of input.tags) {
|
|
1315
|
-
insertTag.run(id, tag);
|
|
1683
|
+
input = recordingCreateIdentity(input, idempotencyKey).input;
|
|
1684
|
+
const id = input.id || shortUuid();
|
|
1685
|
+
const create = d.transaction(() => {
|
|
1686
|
+
if (input.id) {
|
|
1687
|
+
const existing = getRecording(input.id, d);
|
|
1688
|
+
if (existing?.id === input.id)
|
|
1689
|
+
return existing;
|
|
1316
1690
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1691
|
+
const tagsJson = JSON.stringify(input.tags || []);
|
|
1692
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
1693
|
+
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)
|
|
1694
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1695
|
+
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);
|
|
1696
|
+
if (insertResult.changes === 0) {
|
|
1697
|
+
const existing = getRecording(id, d);
|
|
1698
|
+
if (existing?.id === id)
|
|
1699
|
+
return existing;
|
|
1700
|
+
throw new Error("recording id conflict could not be read back");
|
|
1701
|
+
}
|
|
1702
|
+
if (input.tags && input.tags.length > 0) {
|
|
1703
|
+
const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
|
|
1704
|
+
for (const tag of input.tags) {
|
|
1705
|
+
insertTag.run(id, tag);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return getRecording(id, d);
|
|
1709
|
+
});
|
|
1710
|
+
return create();
|
|
1319
1711
|
}
|
|
1320
1712
|
function getRecording(id, db) {
|
|
1321
1713
|
const d = db || getDatabase();
|
|
@@ -1327,6 +1719,20 @@ function getRecording(id, db) {
|
|
|
1327
1719
|
}
|
|
1328
1720
|
function listRecordings(filter, db) {
|
|
1329
1721
|
const d = db || getDatabase();
|
|
1722
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
1723
|
+
const limit = filter?.limit || 50;
|
|
1724
|
+
const offset = filter?.offset || 0;
|
|
1725
|
+
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
1726
|
+
const rows = d.query(sql).all(...params, limit, offset);
|
|
1727
|
+
return rows.map(parseRow);
|
|
1728
|
+
}
|
|
1729
|
+
function countRecordings(filter, db) {
|
|
1730
|
+
const d = db || getDatabase();
|
|
1731
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
1732
|
+
const row = d.query(`SELECT COUNT(*) as c FROM recordings ${where}`).get(...params);
|
|
1733
|
+
return row.c;
|
|
1734
|
+
}
|
|
1735
|
+
function buildRecordingWhere(filter) {
|
|
1330
1736
|
const conditions = [];
|
|
1331
1737
|
const params = [];
|
|
1332
1738
|
if (filter?.agent_id) {
|
|
@@ -1365,12 +1771,7 @@ function listRecordings(filter, db) {
|
|
|
1365
1771
|
params.push(filter.until);
|
|
1366
1772
|
}
|
|
1367
1773
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1368
|
-
|
|
1369
|
-
const offset = filter?.offset || 0;
|
|
1370
|
-
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
1371
|
-
params.push(limit, offset);
|
|
1372
|
-
const rows = d.query(sql).all(...params);
|
|
1373
|
-
return rows.map(parseRow);
|
|
1774
|
+
return { where, params };
|
|
1374
1775
|
}
|
|
1375
1776
|
function deleteRecording(id, db) {
|
|
1376
1777
|
const d = db || getDatabase();
|
|
@@ -1443,16 +1844,6 @@ function listProjects(db) {
|
|
|
1443
1844
|
return rows.map(parseProject);
|
|
1444
1845
|
}
|
|
1445
1846
|
|
|
1446
|
-
// src/db/errors.ts
|
|
1447
|
-
class ProjectNotFoundError extends Error {
|
|
1448
|
-
ref;
|
|
1449
|
-
constructor(ref) {
|
|
1450
|
-
super(`project not found: ${ref}`);
|
|
1451
|
-
this.name = "ProjectNotFoundError";
|
|
1452
|
-
this.ref = ref;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
1847
|
// src/db/agents.ts
|
|
1457
1848
|
function parseAgent(row) {
|
|
1458
1849
|
return {
|
|
@@ -1518,7 +1909,7 @@ function setAgentFocus(idOrName, projectId, db) {
|
|
|
1518
1909
|
}
|
|
1519
1910
|
|
|
1520
1911
|
// src/version.ts
|
|
1521
|
-
var VERSION = "0.2.
|
|
1912
|
+
var VERSION = "0.2.13";
|
|
1522
1913
|
|
|
1523
1914
|
// src/db/feedback.ts
|
|
1524
1915
|
function saveFeedback(input) {
|
|
@@ -1811,6 +2202,7 @@ function resolveStorageClient(name, env = process.env, fetchImpl) {
|
|
|
1811
2202
|
}
|
|
1812
2203
|
|
|
1813
2204
|
// src/store.ts
|
|
2205
|
+
import { createHash, randomUUID as randomUUID5 } from "crypto";
|
|
1814
2206
|
var APP = "recordings";
|
|
1815
2207
|
function listQuery(filter) {
|
|
1816
2208
|
if (!filter)
|
|
@@ -1820,6 +2212,7 @@ function listQuery(filter) {
|
|
|
1820
2212
|
project_id: filter.project_id,
|
|
1821
2213
|
session_id: filter.session_id,
|
|
1822
2214
|
processing_mode: filter.processing_mode,
|
|
2215
|
+
tags: filter.tags,
|
|
1823
2216
|
search: filter.search,
|
|
1824
2217
|
since: filter.since,
|
|
1825
2218
|
until: filter.until,
|
|
@@ -1836,58 +2229,63 @@ function unwrap(res, key) {
|
|
|
1836
2229
|
var localStore = {
|
|
1837
2230
|
mode: "local",
|
|
1838
2231
|
baseUrl: null,
|
|
1839
|
-
async createRecording(input) {
|
|
1840
|
-
return createRecording(input);
|
|
2232
|
+
async createRecording(input, idempotencyKey) {
|
|
2233
|
+
return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
|
|
1841
2234
|
},
|
|
1842
2235
|
async getRecording(id) {
|
|
1843
|
-
return getRecording(id);
|
|
2236
|
+
return withLocalStoreReaderLease(() => getRecording(id));
|
|
1844
2237
|
},
|
|
1845
2238
|
async listRecordings(filter) {
|
|
1846
|
-
return listRecordings(filter);
|
|
2239
|
+
return withLocalStoreReaderLease(() => listRecordings(filter));
|
|
2240
|
+
},
|
|
2241
|
+
async countRecordings(filter) {
|
|
2242
|
+
return withLocalStoreReaderLease(() => countRecordings(filter));
|
|
1847
2243
|
},
|
|
1848
2244
|
async searchRecordings(query, filter) {
|
|
1849
|
-
return searchRecordings(query, filter);
|
|
2245
|
+
return withLocalStoreReaderLease(() => searchRecordings(query, filter));
|
|
1850
2246
|
},
|
|
1851
2247
|
async deleteRecording(id) {
|
|
1852
|
-
return deleteRecording(id);
|
|
2248
|
+
return withLocalStoreReaderLease(() => deleteRecording(id));
|
|
1853
2249
|
},
|
|
1854
2250
|
async getRecordingStats() {
|
|
1855
|
-
return getRecordingStats();
|
|
2251
|
+
return withLocalStoreReaderLease(() => getRecordingStats());
|
|
1856
2252
|
},
|
|
1857
2253
|
async registerAgent(name, description, role) {
|
|
1858
|
-
return registerAgent(name, description, role);
|
|
2254
|
+
return withLocalStoreReaderLease(() => registerAgent(name, description, role));
|
|
1859
2255
|
},
|
|
1860
2256
|
async getAgent(idOrName) {
|
|
1861
|
-
return getAgent(idOrName);
|
|
2257
|
+
return withLocalStoreReaderLease(() => getAgent(idOrName));
|
|
1862
2258
|
},
|
|
1863
2259
|
async listAgents() {
|
|
1864
|
-
return listAgents();
|
|
2260
|
+
return withLocalStoreReaderLease(() => listAgents());
|
|
1865
2261
|
},
|
|
1866
2262
|
async heartbeatAgent(idOrName) {
|
|
1867
|
-
return heartbeatAgent(idOrName);
|
|
2263
|
+
return withLocalStoreReaderLease(() => heartbeatAgent(idOrName));
|
|
1868
2264
|
},
|
|
1869
2265
|
async setAgentFocus(idOrName, projectId) {
|
|
1870
|
-
return setAgentFocus(idOrName, projectId);
|
|
2266
|
+
return withLocalStoreReaderLease(() => setAgentFocus(idOrName, projectId));
|
|
1871
2267
|
},
|
|
1872
2268
|
async registerProject(name, path, description) {
|
|
1873
|
-
return registerProject(name, path, description);
|
|
2269
|
+
return withLocalStoreReaderLease(() => registerProject(name, path, description));
|
|
1874
2270
|
},
|
|
1875
2271
|
async getProject(idOrPath) {
|
|
1876
|
-
return getProject(idOrPath);
|
|
2272
|
+
return withLocalStoreReaderLease(() => getProject(idOrPath));
|
|
1877
2273
|
},
|
|
1878
2274
|
async listProjects() {
|
|
1879
|
-
return listProjects();
|
|
2275
|
+
return withLocalStoreReaderLease(() => listProjects());
|
|
1880
2276
|
},
|
|
1881
2277
|
async saveFeedback(input) {
|
|
1882
|
-
saveFeedback(input);
|
|
2278
|
+
await withLocalStoreReaderLease(() => saveFeedback(input));
|
|
1883
2279
|
}
|
|
1884
2280
|
};
|
|
1885
2281
|
function apiStore(client) {
|
|
1886
2282
|
return {
|
|
1887
2283
|
mode: "cloud-http",
|
|
1888
2284
|
baseUrl: client.baseUrl,
|
|
1889
|
-
async createRecording(input) {
|
|
1890
|
-
const
|
|
2285
|
+
async createRecording(input, idempotencyKey) {
|
|
2286
|
+
const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID5() : idempotencyKey;
|
|
2287
|
+
const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
|
|
2288
|
+
const res = await client.create("recordings", identity.input, identity.idempotencyKey);
|
|
1891
2289
|
return unwrap(res, "recording");
|
|
1892
2290
|
},
|
|
1893
2291
|
async getRecording(id) {
|
|
@@ -1898,6 +2296,36 @@ function apiStore(client) {
|
|
|
1898
2296
|
const { items } = await client.list("recordings", listQuery(filter));
|
|
1899
2297
|
return items;
|
|
1900
2298
|
},
|
|
2299
|
+
async countRecordings(filter) {
|
|
2300
|
+
const pageLimit = 500;
|
|
2301
|
+
const maxPageRequests = 1e4;
|
|
2302
|
+
let offset = 0;
|
|
2303
|
+
let pageRequests = 0;
|
|
2304
|
+
const seenPageKeys = new Set;
|
|
2305
|
+
while (pageRequests < maxPageRequests) {
|
|
2306
|
+
pageRequests += 1;
|
|
2307
|
+
const { items, raw } = await client.list("recordings", {
|
|
2308
|
+
...listQuery(filter),
|
|
2309
|
+
limit: pageLimit,
|
|
2310
|
+
offset
|
|
2311
|
+
});
|
|
2312
|
+
const count = raw && typeof raw === "object" ? raw.count : undefined;
|
|
2313
|
+
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
2314
|
+
throw new Error("Recordings API response is missing a valid count");
|
|
2315
|
+
}
|
|
2316
|
+
if (count > items.length)
|
|
2317
|
+
return count;
|
|
2318
|
+
if (items.length === 0)
|
|
2319
|
+
return offset;
|
|
2320
|
+
offset += items.length;
|
|
2321
|
+
const pageKey = recordingPageFingerprint(items);
|
|
2322
|
+
if (seenPageKeys.has(pageKey)) {
|
|
2323
|
+
throw new Error("Recordings API ignored pagination while counting legacy results");
|
|
2324
|
+
}
|
|
2325
|
+
seenPageKeys.add(pageKey);
|
|
2326
|
+
}
|
|
2327
|
+
throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
|
|
2328
|
+
},
|
|
1901
2329
|
async searchRecordings(query, filter) {
|
|
1902
2330
|
const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
|
|
1903
2331
|
return items;
|
|
@@ -1979,6 +2407,42 @@ function apiStore(client) {
|
|
|
1979
2407
|
}
|
|
1980
2408
|
};
|
|
1981
2409
|
}
|
|
2410
|
+
function recordingPageFingerprint(items) {
|
|
2411
|
+
const hash = createHash("sha256");
|
|
2412
|
+
const ids = items.map((item) => String(item.id ?? "")).sort();
|
|
2413
|
+
for (const id of ids) {
|
|
2414
|
+
hash.update(String(id.length));
|
|
2415
|
+
hash.update(":");
|
|
2416
|
+
hash.update(id);
|
|
2417
|
+
hash.update(";");
|
|
2418
|
+
}
|
|
2419
|
+
return `${items.length}:${hash.digest("hex")}`;
|
|
2420
|
+
}
|
|
2421
|
+
async function countStoreRecordings(store, filter) {
|
|
2422
|
+
if (store.countRecordings)
|
|
2423
|
+
return store.countRecordings(filter);
|
|
2424
|
+
const pageLimit = 500;
|
|
2425
|
+
const maxPageRequests = 1e4;
|
|
2426
|
+
const { limit: _limit, offset: _offset, ...unpaginated } = filter ?? {};
|
|
2427
|
+
const seenPageKeys = new Set;
|
|
2428
|
+
let offset = 0;
|
|
2429
|
+
for (let pageRequests = 0;pageRequests < maxPageRequests; pageRequests += 1) {
|
|
2430
|
+
const items = await store.listRecordings({
|
|
2431
|
+
...unpaginated,
|
|
2432
|
+
limit: pageLimit,
|
|
2433
|
+
offset
|
|
2434
|
+
});
|
|
2435
|
+
if (items.length === 0)
|
|
2436
|
+
return offset;
|
|
2437
|
+
const pageKey = recordingPageFingerprint(items);
|
|
2438
|
+
if (seenPageKeys.has(pageKey)) {
|
|
2439
|
+
throw new Error("Legacy Store ignored pagination while counting recordings");
|
|
2440
|
+
}
|
|
2441
|
+
seenPageKeys.add(pageKey);
|
|
2442
|
+
offset += items.length;
|
|
2443
|
+
}
|
|
2444
|
+
throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
|
|
2445
|
+
}
|
|
1982
2446
|
var cached = null;
|
|
1983
2447
|
function getStore(env = process.env) {
|
|
1984
2448
|
if (env === process.env && cached)
|
|
@@ -1992,7 +2456,7 @@ function getStore(env = process.env) {
|
|
|
1992
2456
|
|
|
1993
2457
|
// src/lib/recorder.ts
|
|
1994
2458
|
import { spawn as spawn2 } from "child_process";
|
|
1995
|
-
import { join as
|
|
2459
|
+
import { join as join4 } from "path";
|
|
1996
2460
|
import { existsSync as existsSync3 } from "fs";
|
|
1997
2461
|
|
|
1998
2462
|
// src/types/index.ts
|
|
@@ -2021,16 +2485,6 @@ class EnhancementError extends Error {
|
|
|
2021
2485
|
var _recordProcess = null;
|
|
2022
2486
|
var _currentFile = null;
|
|
2023
2487
|
async function checkRecordingDeps() {
|
|
2024
|
-
try {
|
|
2025
|
-
const proc = Bun.spawn(["which", "sox"], {
|
|
2026
|
-
stdout: "pipe",
|
|
2027
|
-
stderr: "pipe"
|
|
2028
|
-
});
|
|
2029
|
-
await proc.exited;
|
|
2030
|
-
if (proc.exitCode === 0) {
|
|
2031
|
-
return { available: true, tool: "sox", message: "sox is available" };
|
|
2032
|
-
}
|
|
2033
|
-
} catch {}
|
|
2034
2488
|
try {
|
|
2035
2489
|
const proc = Bun.spawn(["which", "rec"], {
|
|
2036
2490
|
stdout: "pipe",
|
|
@@ -2041,20 +2495,6 @@ async function checkRecordingDeps() {
|
|
|
2041
2495
|
return { available: true, tool: "rec", message: "rec is available" };
|
|
2042
2496
|
}
|
|
2043
2497
|
} catch {}
|
|
2044
|
-
try {
|
|
2045
|
-
const proc = Bun.spawn(["which", "ffmpeg"], {
|
|
2046
|
-
stdout: "pipe",
|
|
2047
|
-
stderr: "pipe"
|
|
2048
|
-
});
|
|
2049
|
-
await proc.exited;
|
|
2050
|
-
if (proc.exitCode === 0) {
|
|
2051
|
-
return {
|
|
2052
|
-
available: true,
|
|
2053
|
-
tool: "ffmpeg",
|
|
2054
|
-
message: "ffmpeg is available"
|
|
2055
|
-
};
|
|
2056
|
-
}
|
|
2057
|
-
} catch {}
|
|
2058
2498
|
return {
|
|
2059
2499
|
available: false,
|
|
2060
2500
|
tool: "none",
|
|
@@ -2067,11 +2507,20 @@ function startRecording(config) {
|
|
|
2067
2507
|
}
|
|
2068
2508
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2069
2509
|
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
2070
|
-
const filepath =
|
|
2510
|
+
const filepath = join4(config.audio_dir, filename);
|
|
2071
2511
|
const args = buildRecordArgs(filepath, config);
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2512
|
+
const releaseLease = isGlobalRecordingsStatePath(filepath) ? acquireLocalStoreReaderLease() : () => {};
|
|
2513
|
+
let recordProcess;
|
|
2514
|
+
try {
|
|
2515
|
+
recordProcess = spawn2(args[0], args.slice(1), {
|
|
2516
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2517
|
+
});
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
releaseLease();
|
|
2520
|
+
throw error;
|
|
2521
|
+
}
|
|
2522
|
+
_recordProcess = recordProcess;
|
|
2523
|
+
_currentFile = filepath;
|
|
2075
2524
|
if (config.max_recording_seconds > 0) {
|
|
2076
2525
|
setTimeout(() => {
|
|
2077
2526
|
if (_recordProcess && _currentFile === filepath) {
|
|
@@ -2080,14 +2529,18 @@ function startRecording(config) {
|
|
|
2080
2529
|
}
|
|
2081
2530
|
}, config.max_recording_seconds * 1000);
|
|
2082
2531
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2532
|
+
recordProcess.on("error", (err) => {
|
|
2533
|
+
if (_recordProcess === recordProcess) {
|
|
2534
|
+
_recordProcess = null;
|
|
2535
|
+
_currentFile = null;
|
|
2536
|
+
}
|
|
2537
|
+
releaseLease();
|
|
2087
2538
|
throw new RecordingError(`Recording process error: ${err.message}`);
|
|
2088
2539
|
});
|
|
2089
|
-
|
|
2090
|
-
_recordProcess
|
|
2540
|
+
recordProcess.on("exit", () => {
|
|
2541
|
+
if (_recordProcess === recordProcess)
|
|
2542
|
+
_recordProcess = null;
|
|
2543
|
+
releaseLease();
|
|
2091
2544
|
});
|
|
2092
2545
|
return filepath;
|
|
2093
2546
|
}
|
|
@@ -2122,7 +2575,7 @@ function buildRecordArgs(filepath, config) {
|
|
|
2122
2575
|
async function recordDuration(seconds, config) {
|
|
2123
2576
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2124
2577
|
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
2125
|
-
const filepath =
|
|
2578
|
+
const filepath = join4(config.audio_dir, filename);
|
|
2126
2579
|
const args = [
|
|
2127
2580
|
"rec",
|
|
2128
2581
|
"-r",
|
|
@@ -2136,29 +2589,36 @@ async function recordDuration(seconds, config) {
|
|
|
2136
2589
|
"0",
|
|
2137
2590
|
seconds.toString()
|
|
2138
2591
|
];
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2592
|
+
const releaseLease = isGlobalRecordingsStatePath(filepath) ? acquireLocalStoreReaderLease() : () => {};
|
|
2593
|
+
try {
|
|
2594
|
+
const proc = Bun.spawn(args, {
|
|
2595
|
+
stdout: "pipe",
|
|
2596
|
+
stderr: "pipe"
|
|
2597
|
+
});
|
|
2598
|
+
const exitCode = await proc.exited;
|
|
2599
|
+
if (exitCode !== 0 && !existsSync3(filepath)) {
|
|
2600
|
+
const stderr = await new Response(proc.stderr).text();
|
|
2601
|
+
throw new RecordingError(`Recording failed (exit ${exitCode}): ${stderr}`);
|
|
2602
|
+
}
|
|
2603
|
+
return filepath;
|
|
2604
|
+
} finally {
|
|
2605
|
+
releaseLease();
|
|
2147
2606
|
}
|
|
2148
|
-
return filepath;
|
|
2149
2607
|
}
|
|
2150
2608
|
|
|
2151
2609
|
// src/lib/transcriber.ts
|
|
2152
2610
|
import OpenAI from "openai";
|
|
2153
2611
|
import { createReadStream } from "fs";
|
|
2154
2612
|
var _client = null;
|
|
2613
|
+
var _clientApiKey = null;
|
|
2155
2614
|
function getClient(config) {
|
|
2156
|
-
if (_client)
|
|
2157
|
-
return _client;
|
|
2158
2615
|
if (!config.openai_api_key) {
|
|
2159
2616
|
throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
|
|
2160
2617
|
}
|
|
2618
|
+
if (_client && _clientApiKey === config.openai_api_key)
|
|
2619
|
+
return _client;
|
|
2161
2620
|
_client = new OpenAI({ apiKey: config.openai_api_key });
|
|
2621
|
+
_clientApiKey = config.openai_api_key;
|
|
2162
2622
|
return _client;
|
|
2163
2623
|
}
|
|
2164
2624
|
async function transcribeAudio(audioPath, config, options = {}) {
|
|
@@ -2170,7 +2630,7 @@ async function transcribeAudio(audioPath, config, options = {}) {
|
|
|
2170
2630
|
file: stream,
|
|
2171
2631
|
model: config.transcription_model,
|
|
2172
2632
|
language: config.language || undefined,
|
|
2173
|
-
prompt: buildVerbatimPrompt(options.prompt),
|
|
2633
|
+
prompt: buildVerbatimPrompt(options.prompt ?? config.transcription_prompt),
|
|
2174
2634
|
response_format: "json"
|
|
2175
2635
|
});
|
|
2176
2636
|
stream.destroy();
|
|
@@ -2198,7 +2658,7 @@ async function transcribeAudioStream(audioPath, config, options = {}) {
|
|
|
2198
2658
|
file: fileStream,
|
|
2199
2659
|
model: config.transcription_model,
|
|
2200
2660
|
language: config.language || undefined,
|
|
2201
|
-
prompt: buildVerbatimPrompt(options.prompt),
|
|
2661
|
+
prompt: buildVerbatimPrompt(options.prompt ?? config.transcription_prompt),
|
|
2202
2662
|
response_format: "text",
|
|
2203
2663
|
stream: true
|
|
2204
2664
|
});
|
|
@@ -2247,14 +2707,16 @@ ${trimmed}`;
|
|
|
2247
2707
|
// src/lib/enhancer.ts
|
|
2248
2708
|
import OpenAI2 from "openai";
|
|
2249
2709
|
var _enhancementClient = null;
|
|
2710
|
+
var _enhancementClientApiKey = null;
|
|
2250
2711
|
function getEnhancementClient(config) {
|
|
2251
|
-
if (_enhancementClient)
|
|
2252
|
-
return _enhancementClient;
|
|
2253
2712
|
const key = config.enhancement_api_key || config.openai_api_key;
|
|
2254
2713
|
if (!key) {
|
|
2255
2714
|
throw new EnhancementError("API key not configured for enhancement. Set OPENAI_API_KEY or RECORDINGS_ENHANCEMENT_KEY");
|
|
2256
2715
|
}
|
|
2716
|
+
if (_enhancementClient && _enhancementClientApiKey === key)
|
|
2717
|
+
return _enhancementClient;
|
|
2257
2718
|
_enhancementClient = new OpenAI2({ apiKey: key });
|
|
2719
|
+
_enhancementClientApiKey = key;
|
|
2258
2720
|
return _enhancementClient;
|
|
2259
2721
|
}
|
|
2260
2722
|
function needsEnhancement(text, config) {
|
|
@@ -2301,6 +2763,7 @@ function extractInstruction(text, trigger) {
|
|
|
2301
2763
|
}
|
|
2302
2764
|
async function enhanceText(rawText, instruction, config, systemPrompt) {
|
|
2303
2765
|
const client = getEnhancementClient(config);
|
|
2766
|
+
const model = resolveTranscriberModel(config);
|
|
2304
2767
|
let basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
|
|
2305
2768
|
|
|
2306
2769
|
Rules:
|
|
@@ -2321,13 +2784,14 @@ Rules:
|
|
|
2321
2784
|
Keyword Transformations:
|
|
2322
2785
|
${transformRules}`;
|
|
2323
2786
|
}
|
|
2324
|
-
const
|
|
2787
|
+
const transcriberPrompt = combinePrompts(config.transcriber_prompt, systemPrompt);
|
|
2788
|
+
const fullPrompt = transcriberPrompt ? `${basePrompt}
|
|
2325
2789
|
|
|
2326
|
-
|
|
2327
|
-
${
|
|
2790
|
+
Transcriber instructions:
|
|
2791
|
+
${transcriberPrompt}` : basePrompt;
|
|
2328
2792
|
try {
|
|
2329
2793
|
const response = await client.chat.completions.create({
|
|
2330
|
-
model
|
|
2794
|
+
model,
|
|
2331
2795
|
messages: [
|
|
2332
2796
|
{
|
|
2333
2797
|
role: "system",
|
|
@@ -2345,7 +2809,7 @@ ${systemPrompt}` : basePrompt;
|
|
|
2345
2809
|
return {
|
|
2346
2810
|
original: rawText,
|
|
2347
2811
|
enhanced,
|
|
2348
|
-
model
|
|
2812
|
+
model,
|
|
2349
2813
|
reasoning: null
|
|
2350
2814
|
};
|
|
2351
2815
|
} catch (error) {
|
|
@@ -2353,29 +2817,109 @@ ${systemPrompt}` : basePrompt;
|
|
|
2353
2817
|
throw new EnhancementError(`Enhancement failed: ${describeTranscriptionFailure(msg)}`);
|
|
2354
2818
|
}
|
|
2355
2819
|
}
|
|
2356
|
-
async function processText(rawText, config,
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
|
|
2820
|
+
async function processText(rawText, config, systemPromptOrOptions, legacyOptions) {
|
|
2821
|
+
const options = {
|
|
2822
|
+
...normalizeProcessTextOptions(systemPromptOrOptions),
|
|
2823
|
+
...legacyOptions
|
|
2824
|
+
};
|
|
2825
|
+
const postProcessingMode = options.force ? "always" : options.postProcessingMode ?? normalizePostProcessingMode(config.post_processing_mode, config.auto_enhance === false ? "off" : "auto");
|
|
2826
|
+
if (postProcessingMode === "off") {
|
|
2827
|
+
return {
|
|
2828
|
+
text: rawText,
|
|
2829
|
+
mode: "raw",
|
|
2830
|
+
enhancement_model: null,
|
|
2831
|
+
post_processing_mode: "off",
|
|
2832
|
+
enhancement_reason: null
|
|
2833
|
+
};
|
|
2360
2834
|
}
|
|
2361
|
-
const detection =
|
|
2362
|
-
|
|
2363
|
-
|
|
2835
|
+
const detection = postProcessingMode === "always" ? {
|
|
2836
|
+
needs: true,
|
|
2837
|
+
reason: "Always-on post-processing",
|
|
2838
|
+
instruction: rawText
|
|
2839
|
+
} : needsEnhancement(rawText, config);
|
|
2840
|
+
if (!detection.needs) {
|
|
2841
|
+
return {
|
|
2842
|
+
text: rawText,
|
|
2843
|
+
mode: "raw",
|
|
2844
|
+
enhancement_model: null,
|
|
2845
|
+
post_processing_mode: postProcessingMode,
|
|
2846
|
+
enhancement_reason: detection.reason
|
|
2847
|
+
};
|
|
2364
2848
|
}
|
|
2365
|
-
const
|
|
2366
|
-
const result = await enhanceText(rawText, instruction, config, systemPrompt);
|
|
2849
|
+
const result = await enhanceText(rawText, detection.instruction, config, options.systemPrompt);
|
|
2367
2850
|
return {
|
|
2368
2851
|
text: result.enhanced,
|
|
2369
2852
|
mode: "enhanced",
|
|
2370
|
-
enhancement_model: result.model
|
|
2853
|
+
enhancement_model: result.model,
|
|
2854
|
+
post_processing_mode: postProcessingMode,
|
|
2855
|
+
enhancement_reason: detection.reason
|
|
2371
2856
|
};
|
|
2372
2857
|
}
|
|
2858
|
+
function resolveTranscriberModel(config) {
|
|
2859
|
+
return config.transcriber_model || config.enhancement_model;
|
|
2860
|
+
}
|
|
2861
|
+
function normalizeProcessTextOptions(value) {
|
|
2862
|
+
if (typeof value === "string")
|
|
2863
|
+
return { systemPrompt: value };
|
|
2864
|
+
return value ?? {};
|
|
2865
|
+
}
|
|
2866
|
+
function combinePrompts(...prompts) {
|
|
2867
|
+
return prompts.map((prompt) => prompt?.trim() ?? "").filter(Boolean).join(`
|
|
2868
|
+
|
|
2869
|
+
`);
|
|
2870
|
+
}
|
|
2373
2871
|
|
|
2374
2872
|
// src/cli/options.ts
|
|
2873
|
+
var POST_PROCESSING_MODES2 = new Set(["off", "auto", "always"]);
|
|
2375
2874
|
function applyEnhancementOptions(config, opts) {
|
|
2376
|
-
if (opts.
|
|
2875
|
+
if (opts.postProcessing) {
|
|
2876
|
+
const requestedMode = opts.postProcessing.trim().toLowerCase();
|
|
2877
|
+
if (!POST_PROCESSING_MODES2.has(requestedMode)) {
|
|
2878
|
+
throw new Error("Invalid post-processing mode. Use one of: off, auto, always.");
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
const disableEnhancement = opts.enhance === false || opts.noEnhance === false;
|
|
2882
|
+
if (disableEnhancement) {
|
|
2377
2883
|
config.auto_enhance = false;
|
|
2884
|
+
config.post_processing_mode = "off";
|
|
2885
|
+
normalizePostProcessingConfig(config, true);
|
|
2886
|
+
} else if (opts.postProcessing) {
|
|
2887
|
+
config.post_processing_mode = normalizePostProcessingMode(opts.postProcessing, config.post_processing_mode ?? "auto");
|
|
2888
|
+
normalizePostProcessingConfig(config, true);
|
|
2889
|
+
} else {
|
|
2890
|
+
normalizePostProcessingConfig(config, false);
|
|
2891
|
+
}
|
|
2892
|
+
if (opts.transcriberPrompt !== undefined) {
|
|
2893
|
+
config.transcriber_prompt = opts.transcriberPrompt;
|
|
2894
|
+
} else if (opts.systemPrompt !== undefined) {
|
|
2895
|
+
config.transcriber_prompt = opts.systemPrompt;
|
|
2896
|
+
}
|
|
2897
|
+
if (opts.enhancementModel) {
|
|
2898
|
+
config.enhancement_model = opts.enhancementModel;
|
|
2899
|
+
}
|
|
2900
|
+
if (opts.transcriberModel) {
|
|
2901
|
+
config.transcriber_model = opts.transcriberModel;
|
|
2902
|
+
} else if (opts.enhancementModel) {
|
|
2903
|
+
config.transcriber_model = opts.enhancementModel;
|
|
2904
|
+
}
|
|
2905
|
+
if (opts.transcriptionModel) {
|
|
2906
|
+
config.transcription_model = opts.transcriptionModel;
|
|
2907
|
+
}
|
|
2908
|
+
if (opts.enhanceTriggersJson !== undefined) {
|
|
2909
|
+
const triggers = JSON.parse(opts.enhanceTriggersJson);
|
|
2910
|
+
if (!Array.isArray(triggers) || !triggers.every((trigger) => typeof trigger === "string")) {
|
|
2911
|
+
throw new Error("Invalid enhancement triggers snapshot; expected a JSON string array.");
|
|
2912
|
+
}
|
|
2913
|
+
config.enhance_triggers = triggers;
|
|
2378
2914
|
}
|
|
2915
|
+
if (opts.keywordTransformsJson !== undefined) {
|
|
2916
|
+
const transforms = JSON.parse(opts.keywordTransformsJson);
|
|
2917
|
+
if (typeof transforms !== "object" || transforms === null || Array.isArray(transforms) || !Object.values(transforms).every((value) => typeof value === "string")) {
|
|
2918
|
+
throw new Error("Invalid keyword transforms snapshot; expected a JSON string map.");
|
|
2919
|
+
}
|
|
2920
|
+
config.keyword_transforms = transforms;
|
|
2921
|
+
}
|
|
2922
|
+
normalizeModelSlots(config);
|
|
2379
2923
|
return config;
|
|
2380
2924
|
}
|
|
2381
2925
|
|
|
@@ -2416,22 +2960,355 @@ function upsertCodexStdioBlock(content, name, mcpCmd) {
|
|
|
2416
2960
|
const trimmed = cleaned.replace(/\s+$/, "");
|
|
2417
2961
|
const block = `[mcp_servers.${name}]
|
|
2418
2962
|
command = "${mcpCmd}"
|
|
2419
|
-
args = []
|
|
2963
|
+
args = ["--stdio"]
|
|
2420
2964
|
`;
|
|
2421
2965
|
return trimmed.length > 0 ? `${trimmed}
|
|
2422
2966
|
|
|
2423
2967
|
${block}` : block;
|
|
2424
2968
|
}
|
|
2425
2969
|
|
|
2970
|
+
// src/cli/macos-permissions.ts
|
|
2971
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2972
|
+
import { join as join5 } from "path";
|
|
2973
|
+
var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync2(executable, arguments_, options);
|
|
2974
|
+
function runMacOSPermissionRequest(appPath, runner = defaultPermissionHelperRunner) {
|
|
2975
|
+
const executable = join5(appPath, "Contents", "MacOS", "Recordings");
|
|
2976
|
+
const result = runner(executable, ["--request-permissions", "--open-permission-settings"], { stdio: "inherit" });
|
|
2977
|
+
return {
|
|
2978
|
+
exitCode: result.error ? 1 : result.status ?? 1,
|
|
2979
|
+
errorMessage: result.error?.message
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// src/lib/machine.ts
|
|
2984
|
+
import { hostname } from "os";
|
|
2985
|
+
function currentMachineId(env = process.env, hostName = hostname()) {
|
|
2986
|
+
const configured = env.HASNA_MACHINE_ID?.trim();
|
|
2987
|
+
if (configured)
|
|
2988
|
+
return configured;
|
|
2989
|
+
return hostName.trim();
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// src/lib/bun-runtime.ts
|
|
2993
|
+
import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
|
|
2994
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
2995
|
+
import { randomBytes } from "crypto";
|
|
2996
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2997
|
+
var BUN_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
2998
|
+
var INSTALLER_ENVIRONMENT_KEYS = [
|
|
2999
|
+
"HOME",
|
|
3000
|
+
"SSH_CONNECTION",
|
|
3001
|
+
"RECORDINGS_EXPECTED_TEAM_IDENTIFIER",
|
|
3002
|
+
"RECORDINGS_LAUNCH_TIMEOUT_SECONDS",
|
|
3003
|
+
"RECORDINGS_LOCK_STALE_SECONDS",
|
|
3004
|
+
"RECORDINGS_MAINTENANCE_STALE_SECONDS",
|
|
3005
|
+
"RECORDINGS_READER_DRAIN_TIMEOUT_MS",
|
|
3006
|
+
"RECORDINGS_SQLITE_BUSY_TIMEOUT_MS"
|
|
3007
|
+
];
|
|
3008
|
+
var INSTALLER_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
3009
|
+
function validateBunExecutable(candidate) {
|
|
3010
|
+
if (!isAbsolute2(candidate))
|
|
3011
|
+
return { reason: "path is not absolute" };
|
|
3012
|
+
let executable;
|
|
3013
|
+
try {
|
|
3014
|
+
executable = realpathSync3(candidate);
|
|
3015
|
+
if (!statSync2(executable).isFile())
|
|
3016
|
+
return { reason: "resolved path is not a regular file" };
|
|
3017
|
+
accessSync(executable, fsConstants.X_OK);
|
|
3018
|
+
} catch {
|
|
3019
|
+
return { reason: "path is missing, inaccessible, or not executable" };
|
|
3020
|
+
}
|
|
3021
|
+
const nonce = randomBytes(24).toString("hex");
|
|
3022
|
+
const probe = spawnSync3(executable, [
|
|
3023
|
+
"-e",
|
|
3024
|
+
`
|
|
3025
|
+
import { realpathSync, statSync } from "node:fs";
|
|
3026
|
+
const expected = process.argv[1];
|
|
3027
|
+
const nonce = process.argv[2];
|
|
3028
|
+
const actual = process.execPath;
|
|
3029
|
+
if (!expected || !nonce || realpathSync(actual) !== realpathSync(expected)) process.exit(66);
|
|
3030
|
+
if (!statSync(actual).isFile()) process.exit(66);
|
|
3031
|
+
if (!/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/.test(Bun.version)) {
|
|
3032
|
+
process.exit(66);
|
|
3033
|
+
}
|
|
3034
|
+
process.stdout.write(nonce + ":" + Bun.version);
|
|
3035
|
+
`,
|
|
3036
|
+
executable,
|
|
3037
|
+
nonce
|
|
3038
|
+
], {
|
|
3039
|
+
encoding: "utf8",
|
|
3040
|
+
env: {
|
|
3041
|
+
HOME: "/tmp",
|
|
3042
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
3043
|
+
TMPDIR: "/tmp"
|
|
3044
|
+
},
|
|
3045
|
+
maxBuffer: 1024,
|
|
3046
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3047
|
+
timeout: 5000
|
|
3048
|
+
});
|
|
3049
|
+
const output = probe.stdout?.trim() ?? "";
|
|
3050
|
+
const prefix = `${nonce}:`;
|
|
3051
|
+
const version = output.startsWith(prefix) ? output.slice(prefix.length) : "";
|
|
3052
|
+
if (probe.error || probe.status !== 0 || !BUN_VERSION_PATTERN.test(version)) {
|
|
3053
|
+
return { reason: "behavioral Bun -e probe failed" };
|
|
3054
|
+
}
|
|
3055
|
+
return { executable, version };
|
|
3056
|
+
}
|
|
3057
|
+
function resolveInstallBunExecutable(environment, activeExecutable = process.execPath) {
|
|
3058
|
+
if (environment.RECORDINGS_BUN_EXECUTABLE !== undefined) {
|
|
3059
|
+
const explicit = validateBunExecutable(environment.RECORDINGS_BUN_EXECUTABLE);
|
|
3060
|
+
if (!("executable" in explicit)) {
|
|
3061
|
+
throw new Error(`RECORDINGS_BUN_EXECUTABLE is not a validated general Bun interpreter: ${explicit.reason}`);
|
|
3062
|
+
}
|
|
3063
|
+
return explicit.executable;
|
|
3064
|
+
}
|
|
3065
|
+
const active = validateBunExecutable(activeExecutable);
|
|
3066
|
+
if ("executable" in active)
|
|
3067
|
+
return active.executable;
|
|
3068
|
+
throw new Error("The active recordings executable is not a general Bun interpreter; rerun app install from the Bun-interpreted package CLI or set RECORDINGS_BUN_EXECUTABLE to an explicitly trusted absolute Bun executable");
|
|
3069
|
+
}
|
|
3070
|
+
function createInstallerEnvironment(environment, bunExecutable) {
|
|
3071
|
+
const sanitized = {};
|
|
3072
|
+
for (const key of INSTALLER_ENVIRONMENT_KEYS) {
|
|
3073
|
+
const value = environment[key];
|
|
3074
|
+
if (value !== undefined)
|
|
3075
|
+
sanitized[key] = value;
|
|
3076
|
+
}
|
|
3077
|
+
sanitized.PATH = INSTALLER_PATH;
|
|
3078
|
+
sanitized.LC_ALL = "C";
|
|
3079
|
+
sanitized.LANG = "C";
|
|
3080
|
+
sanitized.TZ = "UTC0";
|
|
3081
|
+
sanitized.RECORDINGS_BUN_EXECUTABLE = bunExecutable;
|
|
3082
|
+
return sanitized;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
// src/lib/release-install-policy.ts
|
|
3086
|
+
import { createHash as createHash2 } from "crypto";
|
|
3087
|
+
import {
|
|
3088
|
+
chmodSync as chmodSync2,
|
|
3089
|
+
closeSync,
|
|
3090
|
+
constants,
|
|
3091
|
+
fstatSync,
|
|
3092
|
+
fsyncSync,
|
|
3093
|
+
mkdtempSync,
|
|
3094
|
+
openSync,
|
|
3095
|
+
readFileSync as readFileSync2,
|
|
3096
|
+
rmSync as rmSync2,
|
|
3097
|
+
writeFileSync as writeFileSync2
|
|
3098
|
+
} from "fs";
|
|
3099
|
+
import { isAbsolute as isAbsolute3, join as join6 } from "path";
|
|
3100
|
+
var LOWER_SHA256 = /^[a-f0-9]{64}$/;
|
|
3101
|
+
var LOWER_SOURCE_SHA = /^[a-f0-9]{40}$/;
|
|
3102
|
+
var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
|
3103
|
+
var TEAM_ID = /^[A-Z0-9]{10}$/;
|
|
3104
|
+
var SHORT_HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
|
|
3105
|
+
var MAX_MANIFEST_BYTES = 16 * 1024 * 1024;
|
|
3106
|
+
var MAX_ENVELOPE_BYTES = 1024 * 1024;
|
|
3107
|
+
function assertReleaseOnlyOptions(options) {
|
|
3108
|
+
if (options.approvedTarget !== "fleet") {
|
|
3109
|
+
throw new Error("release installs require --approved-target fleet");
|
|
3110
|
+
}
|
|
3111
|
+
if (options.approvedTargetIdentityKind !== undefined || options.approvedTargetIdentitySha256 !== "none") {
|
|
3112
|
+
throw new Error("release installs reject local-only target identity controls");
|
|
3113
|
+
}
|
|
3114
|
+
if (options.acknowledgeLocalSigningAndPermissions) {
|
|
3115
|
+
throw new Error("release installs reject local-only signing acknowledgement controls");
|
|
3116
|
+
}
|
|
3117
|
+
if (options.expectedOldIdentitySha256 !== undefined || options.expectedNewIdentitySha256 !== undefined || options.allowSigningIdentityMigration) {
|
|
3118
|
+
throw new Error("release installs reject local-only signing migration controls");
|
|
3119
|
+
}
|
|
3120
|
+
if (options.launch) {
|
|
3121
|
+
throw new Error("release --launch is unsupported until the signed update client provides canonical post-install launch verification");
|
|
3122
|
+
}
|
|
3123
|
+
if (options.launchTimeout !== undefined) {
|
|
3124
|
+
throw new Error("release --launch-timeout is unsupported without a verified release launch path");
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
function assertExpectedReleaseHostname(expected, actual) {
|
|
3128
|
+
if (!SHORT_HOSTNAME.test(expected)) {
|
|
3129
|
+
throw new Error("expected hostname is invalid; use one exact short hostname");
|
|
3130
|
+
}
|
|
3131
|
+
if (actual !== expected) {
|
|
3132
|
+
throw new Error(`install target hostname ${actual || "<empty>"} does not match the expected hostname ${expected}`);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
function parseLaunchTimeout(value) {
|
|
3136
|
+
const timeout = value ?? "10";
|
|
3137
|
+
if (!/^(?:[1-9]|[1-9][0-9]|1[01][0-9]|120)$/.test(timeout)) {
|
|
3138
|
+
throw new Error("launch timeout must be an integer between 1 and 120 seconds");
|
|
3139
|
+
}
|
|
3140
|
+
return timeout;
|
|
3141
|
+
}
|
|
3142
|
+
function prepareReleaseInstallInputs(input) {
|
|
3143
|
+
assertAbsolutePath(input.artifactPath, "artifact");
|
|
3144
|
+
assertAbsolutePath(input.manifestPath, "manifest");
|
|
3145
|
+
assertAbsolutePath(input.envelopePath, "envelope");
|
|
3146
|
+
if (!LOWER_SHA256.test(input.manifestSha256)) {
|
|
3147
|
+
throw new Error("manifest SHA-256 must be 64 lowercase hexadecimal characters");
|
|
3148
|
+
}
|
|
3149
|
+
if (!LOWER_SOURCE_SHA.test(input.expectedSourceSha)) {
|
|
3150
|
+
throw new Error("source SHA must be 40 lowercase hexadecimal characters");
|
|
3151
|
+
}
|
|
3152
|
+
if (!RELEASE_VERSION.test(input.expectedVersion)) {
|
|
3153
|
+
throw new Error("release version is invalid");
|
|
3154
|
+
}
|
|
3155
|
+
if (!input.expectedTeamId || !TEAM_ID.test(input.expectedTeamId)) {
|
|
3156
|
+
throw new Error("Team ID must be 10 uppercase alphanumeric characters");
|
|
3157
|
+
}
|
|
3158
|
+
const manifestBytes = readBoundedRegularFile(input.manifestPath, "manifest", MAX_MANIFEST_BYTES);
|
|
3159
|
+
const actualManifestSha256 = createHash2("sha256").update(manifestBytes).digest("hex");
|
|
3160
|
+
if (actualManifestSha256 !== input.manifestSha256) {
|
|
3161
|
+
throw new Error("manifest does not match the operator-approved SHA-256");
|
|
3162
|
+
}
|
|
3163
|
+
const envelopeBytes = readBoundedRegularFile(input.envelopePath, "envelope", MAX_ENVELOPE_BYTES);
|
|
3164
|
+
const manifest = parseObject(manifestBytes, "manifest");
|
|
3165
|
+
const envelope = parseObject(envelopeBytes, "envelope");
|
|
3166
|
+
assertReleaseManifest(manifest, input);
|
|
3167
|
+
assertReleaseEnvelope(envelope, manifest, manifestBytes.byteLength, input);
|
|
3168
|
+
const snapshotRoot = input.snapshotRoot ?? "/private/tmp";
|
|
3169
|
+
if (!isAbsolute3(snapshotRoot)) {
|
|
3170
|
+
throw new Error("release snapshot root must be absolute");
|
|
3171
|
+
}
|
|
3172
|
+
const snapshotDirectory = mkdtempSync(join6(snapshotRoot, "recordings-release-install."));
|
|
3173
|
+
let cleaned = false;
|
|
3174
|
+
const cleanup = () => {
|
|
3175
|
+
if (cleaned)
|
|
3176
|
+
return;
|
|
3177
|
+
cleaned = true;
|
|
3178
|
+
try {
|
|
3179
|
+
chmodSync2(snapshotDirectory, 448);
|
|
3180
|
+
} catch {}
|
|
3181
|
+
rmSync2(snapshotDirectory, { recursive: true, force: true });
|
|
3182
|
+
};
|
|
3183
|
+
try {
|
|
3184
|
+
const manifestSnapshot = join6(snapshotDirectory, `${input.manifestSha256}.manifest.json`);
|
|
3185
|
+
const envelopeDigest = createHash2("sha256").update(envelopeBytes).digest("hex");
|
|
3186
|
+
const envelopeSnapshot = join6(snapshotDirectory, `${envelopeDigest}.envelope.json`);
|
|
3187
|
+
writeSnapshot(manifestSnapshot, manifestBytes);
|
|
3188
|
+
writeSnapshot(envelopeSnapshot, envelopeBytes);
|
|
3189
|
+
chmodSync2(snapshotDirectory, 320);
|
|
3190
|
+
return { manifestPath: manifestSnapshot, envelopePath: envelopeSnapshot, cleanup };
|
|
3191
|
+
} catch (error) {
|
|
3192
|
+
cleanup();
|
|
3193
|
+
throw error;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
function assertAbsolutePath(value, label) {
|
|
3197
|
+
if (!isAbsolute3(value))
|
|
3198
|
+
throw new Error(`${label} path must be absolute`);
|
|
3199
|
+
}
|
|
3200
|
+
function readBoundedRegularFile(path, label, maximum) {
|
|
3201
|
+
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
3202
|
+
try {
|
|
3203
|
+
const metadata = fstatSync(descriptor);
|
|
3204
|
+
if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximum) {
|
|
3205
|
+
throw new Error(`${label} must be a non-empty bounded regular file`);
|
|
3206
|
+
}
|
|
3207
|
+
return readFileSync2(descriptor);
|
|
3208
|
+
} finally {
|
|
3209
|
+
closeSync(descriptor);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
function parseObject(bytes, label) {
|
|
3213
|
+
let parsed;
|
|
3214
|
+
try {
|
|
3215
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
3216
|
+
} catch {
|
|
3217
|
+
throw new Error(`${label} is not valid JSON`);
|
|
3218
|
+
}
|
|
3219
|
+
if (!isObject(parsed))
|
|
3220
|
+
throw new Error(`${label} must be a JSON object`);
|
|
3221
|
+
return parsed;
|
|
3222
|
+
}
|
|
3223
|
+
function assertReleaseManifest(manifest, input) {
|
|
3224
|
+
if (manifest.schema_version !== 4 || manifest.artifact_type !== "recordings-macos-app" || manifest.bundle_id !== "com.hasna.recordings") {
|
|
3225
|
+
throw new Error("manifest is not a schema-v4 Recordings release artifact");
|
|
3226
|
+
}
|
|
3227
|
+
if (manifest.git_sha !== input.expectedSourceSha) {
|
|
3228
|
+
throw new Error("manifest source SHA does not match the operator-approved source");
|
|
3229
|
+
}
|
|
3230
|
+
if (manifest.bundle_version !== input.expectedVersion) {
|
|
3231
|
+
throw new Error("manifest version does not match the operator-approved version");
|
|
3232
|
+
}
|
|
3233
|
+
const signing = objectField(manifest, "signing", "manifest");
|
|
3234
|
+
if (manifest.team_id !== input.expectedTeamId || signing.team_id !== input.expectedTeamId || signing.helper_team_id !== input.expectedTeamId) {
|
|
3235
|
+
throw new Error("manifest Team ID does not match the operator-approved Team ID");
|
|
3236
|
+
}
|
|
3237
|
+
for (const localOnlyField of [
|
|
3238
|
+
"artifact_policy",
|
|
3239
|
+
"approved_target",
|
|
3240
|
+
"approved_target_identity_kind",
|
|
3241
|
+
"approved_target_identity_sha256",
|
|
3242
|
+
"builder_identity_kind",
|
|
3243
|
+
"builder_identity_sha256",
|
|
3244
|
+
"non_notarized"
|
|
3245
|
+
]) {
|
|
3246
|
+
if (Object.hasOwn(manifest, localOnlyField)) {
|
|
3247
|
+
throw new Error("release manifest contains local-only policy fields");
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
function assertReleaseEnvelope(envelope, manifest, manifestByteCount, input) {
|
|
3252
|
+
const payload = objectField(envelope, "payload", "envelope");
|
|
3253
|
+
if (payload.purpose !== "update") {
|
|
3254
|
+
throw new Error("release app install requires an update envelope");
|
|
3255
|
+
}
|
|
3256
|
+
if (payload.manifest_sha256 !== input.manifestSha256 || payload.manifest_byte_count !== manifestByteCount) {
|
|
3257
|
+
throw new Error("release envelope does not bind the authenticated manifest snapshot");
|
|
3258
|
+
}
|
|
3259
|
+
if (payload.source_commit !== input.expectedSourceSha || payload.version !== input.expectedVersion || payload.signing_team_identifier !== input.expectedTeamId) {
|
|
3260
|
+
throw new Error("release envelope does not match the operator-approved provenance");
|
|
3261
|
+
}
|
|
3262
|
+
const archive = objectField(manifest, "archive", "manifest");
|
|
3263
|
+
const binding = objectField(manifest, "binding", "manifest");
|
|
3264
|
+
if (payload.build !== manifest.bundle_build_version || !LOWER_SHA256.test(stringField(archive, "sha256", "manifest archive")) || payload.artifact_sha256 !== archive.sha256 || !LOWER_SHA256.test(stringField(binding, "bundle_tree_sha256", "manifest binding")) || payload.candidate_tree_sha256 !== binding.bundle_tree_sha256) {
|
|
3265
|
+
throw new Error("release envelope and manifest artifact provenance differ");
|
|
3266
|
+
}
|
|
3267
|
+
const signature = stringField(envelope, "signature", "envelope");
|
|
3268
|
+
if (!/^[A-Za-z0-9+/]{86}==$/.test(signature)) {
|
|
3269
|
+
throw new Error("release envelope signature encoding is invalid");
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
function objectField(value, key, label) {
|
|
3273
|
+
const field = value[key];
|
|
3274
|
+
if (!isObject(field))
|
|
3275
|
+
throw new Error(`${label} is missing ${key}`);
|
|
3276
|
+
return field;
|
|
3277
|
+
}
|
|
3278
|
+
function stringField(value, key, label) {
|
|
3279
|
+
const field = value[key];
|
|
3280
|
+
if (typeof field !== "string")
|
|
3281
|
+
throw new Error(`${label} is missing ${key}`);
|
|
3282
|
+
return field;
|
|
3283
|
+
}
|
|
3284
|
+
function isObject(value) {
|
|
3285
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3286
|
+
}
|
|
3287
|
+
function writeSnapshot(path, bytes) {
|
|
3288
|
+
writeFileSync2(path, bytes, { flag: "wx", mode: 256 });
|
|
3289
|
+
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
3290
|
+
try {
|
|
3291
|
+
fsyncSync(descriptor);
|
|
3292
|
+
} finally {
|
|
3293
|
+
closeSync(descriptor);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
|
|
2426
3297
|
// src/cli/index.ts
|
|
2427
3298
|
var program = new Command;
|
|
2428
3299
|
program.name("recordings").description("Speech-to-text recording tool \u2014 record, transcribe, and enhance with AI").version(VERSION).option("--json", "Output as JSON").option("--agent <name>", "Agent name or ID").option("--project <name>", "Project name or ID").option("--session <id>", "Session ID");
|
|
2429
3300
|
registerEventsCommands(program, { source: "recordings" });
|
|
2430
|
-
|
|
3301
|
+
var DEFAULT_LIST_LIMIT = 20;
|
|
3302
|
+
var MAX_HUMAN_LIST_LIMIT = 50;
|
|
3303
|
+
var DEFAULT_LOG_LINES = 40;
|
|
3304
|
+
program.command("record").description("Record from microphone, transcribe, and optionally enhance").option("-d, --duration <seconds>", "Record for specific duration").option("--no-enhance", "Skip AI enhancement").option("--post-processing <mode>", "Post-processing mode: off, auto, or always").option("--prompt <prompt>", "Vocabulary/context prompt for transcription").option("--transcriber-prompt <prompt>", "Instructions for post-transcription cleanup").option("--system-prompt <prompt>", "Alias for --transcriber-prompt").option("--transcriber-model <model>", "Model for post-transcription cleanup").option("-t, --tags <tags>", "Comma-separated tags").option("-l, --language <lang>", "Language code (e.g. en, es, fr)").action(async (opts) => {
|
|
2431
3305
|
const config = loadConfig();
|
|
2432
3306
|
ensureDataDir(config);
|
|
3307
|
+
const parentOpts = program.opts();
|
|
2433
3308
|
if (opts.language)
|
|
2434
3309
|
config.language = opts.language;
|
|
3310
|
+
if (opts.prompt !== undefined)
|
|
3311
|
+
config.transcription_prompt = opts.prompt;
|
|
2435
3312
|
applyEnhancementOptions(config, opts);
|
|
2436
3313
|
const deps = await checkRecordingDeps();
|
|
2437
3314
|
if (!deps.available) {
|
|
@@ -2441,39 +3318,50 @@ program.command("record").description("Record from microphone, transcribe, and o
|
|
|
2441
3318
|
let audioPath;
|
|
2442
3319
|
if (opts.duration) {
|
|
2443
3320
|
const seconds = parseInt(opts.duration, 10);
|
|
2444
|
-
|
|
3321
|
+
if (!parentOpts.json) {
|
|
3322
|
+
console.log(chalk.blue(`Recording for ${seconds} seconds...`));
|
|
3323
|
+
}
|
|
2445
3324
|
audioPath = await recordDuration(seconds, config);
|
|
2446
|
-
|
|
3325
|
+
if (!parentOpts.json) {
|
|
3326
|
+
console.log(chalk.green("Recording complete."));
|
|
3327
|
+
}
|
|
2447
3328
|
} else {
|
|
2448
|
-
|
|
3329
|
+
if (!parentOpts.json) {
|
|
3330
|
+
console.log(chalk.blue("Recording... Press") + chalk.yellow(" Enter ") + chalk.blue("to stop."));
|
|
3331
|
+
}
|
|
2449
3332
|
audioPath = startRecording(config);
|
|
2450
|
-
await new Promise((
|
|
3333
|
+
await new Promise((resolve3) => {
|
|
2451
3334
|
process.stdin.setRawMode?.(true);
|
|
2452
3335
|
process.stdin.resume();
|
|
2453
3336
|
process.stdin.once("data", () => {
|
|
2454
3337
|
process.stdin.setRawMode?.(false);
|
|
2455
3338
|
process.stdin.pause();
|
|
2456
|
-
|
|
3339
|
+
resolve3();
|
|
2457
3340
|
});
|
|
2458
3341
|
});
|
|
2459
3342
|
stopRecording();
|
|
2460
|
-
|
|
3343
|
+
if (!parentOpts.json) {
|
|
3344
|
+
console.log(chalk.green("Recording stopped."));
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
if (!parentOpts.json) {
|
|
3348
|
+
console.log(chalk.blue("Transcribing..."));
|
|
2461
3349
|
}
|
|
2462
|
-
console.log(chalk.blue("Transcribing..."));
|
|
2463
3350
|
const transcription = await transcribeAudio(audioPath, config);
|
|
2464
|
-
|
|
3351
|
+
if (!parentOpts.json) {
|
|
3352
|
+
console.log(chalk.dim(`Raw: ${transcription.text}`));
|
|
3353
|
+
}
|
|
2465
3354
|
const processed = await processText(transcription.text, config);
|
|
2466
|
-
if (processed.mode === "enhanced") {
|
|
3355
|
+
if (!parentOpts.json && processed.mode === "enhanced") {
|
|
2467
3356
|
console.log(chalk.green(`
|
|
2468
3357
|
Enhanced output:`));
|
|
2469
3358
|
console.log(processed.text);
|
|
2470
|
-
} else {
|
|
3359
|
+
} else if (!parentOpts.json) {
|
|
2471
3360
|
console.log(chalk.green(`
|
|
2472
3361
|
Output:`));
|
|
2473
3362
|
console.log(transcription.text);
|
|
2474
3363
|
}
|
|
2475
3364
|
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
2476
|
-
const parentOpts = program.opts();
|
|
2477
3365
|
const recording = await getStore().createRecording({
|
|
2478
3366
|
audio_path: audioPath,
|
|
2479
3367
|
raw_text: transcription.text,
|
|
@@ -2486,7 +3374,12 @@ Output:`));
|
|
|
2486
3374
|
tags,
|
|
2487
3375
|
agent_id: parentOpts.agent || undefined,
|
|
2488
3376
|
project_id: parentOpts.project || undefined,
|
|
2489
|
-
session_id: parentOpts.session || undefined
|
|
3377
|
+
session_id: parentOpts.session || undefined,
|
|
3378
|
+
machine_id: currentMachineId(),
|
|
3379
|
+
metadata: buildTranscriptionMetadata(config, processed, {
|
|
3380
|
+
transcriptionPromptFromRequest: opts.prompt !== undefined,
|
|
3381
|
+
transcriberPromptFromRequest: opts.transcriberPrompt !== undefined || opts.systemPrompt !== undefined
|
|
3382
|
+
})
|
|
2490
3383
|
});
|
|
2491
3384
|
if (parentOpts.json) {
|
|
2492
3385
|
console.log(JSON.stringify(recording, null, 2));
|
|
@@ -2495,25 +3388,33 @@ Output:`));
|
|
|
2495
3388
|
Saved as ${recording.id.slice(0, 8)}`));
|
|
2496
3389
|
}
|
|
2497
3390
|
});
|
|
2498
|
-
program.command("transcribe <file>").description("Transcribe an existing audio file").option("--no-enhance", "Skip AI enhancement").option("--stream", "Stream transcription deltas while the file is processed").option("-t, --tags <tags>", "Comma-separated tags").option("--prompt <prompt>", "Vocabulary/context prompt for transcription").option("--system-prompt <prompt>", "
|
|
3391
|
+
program.command("transcribe <file>").description("Transcribe an existing audio file").option("--no-enhance", "Skip AI enhancement").option("--stream", "Stream transcription deltas while the file is processed").option("-t, --tags <tags>", "Comma-separated tags").option("--prompt <prompt>", "Vocabulary/context prompt for transcription").option("--transcriber-prompt <prompt>", "Instructions for post-transcription cleanup").option("--system-prompt <prompt>", "Alias for --transcriber-prompt").option("--post-processing <mode>", "Post-processing mode: off, auto, or always").option("--transcription-model <model>", "Model for bounded audio transcription").option("--transcriber-model <model>", "Model for post-transcription cleanup").option("--enhancement-model <model>", "Enhancement model fallback").option("--enhance-triggers-json <json>", "Frozen JSON string array of enhancement triggers").option("--keyword-transforms-json <json>", "Frozen JSON string map of keyword transforms").option("-l, --language <lang>", "Language code (e.g. en, es, fr)").option("--recording-id <id>", "Stable recording ID for idempotent retries").action(async (file, opts) => {
|
|
3392
|
+
const recordingId = recordingCreateIdentity({
|
|
3393
|
+
id: opts.recordingId,
|
|
3394
|
+
raw_text: ""
|
|
3395
|
+
}).input.id;
|
|
2499
3396
|
const config = loadConfig();
|
|
2500
3397
|
ensureDataDir(config);
|
|
3398
|
+
if (opts.language)
|
|
3399
|
+
config.language = opts.language;
|
|
3400
|
+
if (opts.prompt !== undefined)
|
|
3401
|
+
config.transcription_prompt = opts.prompt;
|
|
2501
3402
|
applyEnhancementOptions(config, opts);
|
|
2502
3403
|
const parentOpts = program.opts();
|
|
2503
3404
|
if (!parentOpts.json) {
|
|
2504
3405
|
console.log(chalk.blue("Transcribing..."));
|
|
2505
3406
|
}
|
|
2506
3407
|
const transcription = opts.stream ? await transcribeAudioStream(file, config, {
|
|
2507
|
-
prompt: opts.prompt,
|
|
2508
3408
|
onDelta: parentOpts.json ? undefined : (delta) => process.stdout.write(delta)
|
|
2509
|
-
}) : await transcribeAudio(file, config
|
|
3409
|
+
}) : await transcribeAudio(file, config);
|
|
2510
3410
|
if (opts.stream && !parentOpts.json) {
|
|
2511
3411
|
process.stdout.write(`
|
|
2512
3412
|
`);
|
|
2513
3413
|
}
|
|
2514
|
-
const processed = await processText(transcription.text, config
|
|
3414
|
+
const processed = await processText(transcription.text, config);
|
|
2515
3415
|
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
2516
3416
|
const recording = await getStore().createRecording({
|
|
3417
|
+
id: recordingId,
|
|
2517
3418
|
audio_path: file,
|
|
2518
3419
|
raw_text: transcription.text,
|
|
2519
3420
|
processed_text: processed.mode === "enhanced" ? processed.text : undefined,
|
|
@@ -2525,23 +3426,83 @@ program.command("transcribe <file>").description("Transcribe an existing audio f
|
|
|
2525
3426
|
tags,
|
|
2526
3427
|
agent_id: parentOpts.agent || undefined,
|
|
2527
3428
|
project_id: parentOpts.project || undefined,
|
|
2528
|
-
session_id: parentOpts.session || undefined
|
|
2529
|
-
|
|
2530
|
-
|
|
3429
|
+
session_id: parentOpts.session || undefined,
|
|
3430
|
+
machine_id: currentMachineId(),
|
|
3431
|
+
metadata: buildTranscriptionMetadata(config, processed, {
|
|
3432
|
+
transcriptionPromptFromRequest: opts.prompt !== undefined,
|
|
3433
|
+
transcriberPromptFromRequest: opts.transcriberPrompt !== undefined || opts.systemPrompt !== undefined
|
|
3434
|
+
})
|
|
3435
|
+
}, recordingId);
|
|
3436
|
+
if (parentOpts.json) {
|
|
3437
|
+
console.log(JSON.stringify(recording, null, 2));
|
|
3438
|
+
} else if (processed.mode === "enhanced") {
|
|
2531
3439
|
console.log(chalk.green("Enhanced:"));
|
|
2532
3440
|
console.log(processed.text);
|
|
2533
3441
|
} else {
|
|
2534
3442
|
console.log(chalk.green("Transcription:"));
|
|
2535
3443
|
console.log(transcription.text);
|
|
2536
3444
|
}
|
|
3445
|
+
if (!parentOpts.json) {
|
|
3446
|
+
console.log(chalk.dim(`Saved as ${recording.id.slice(0, 8)}`));
|
|
3447
|
+
}
|
|
3448
|
+
});
|
|
3449
|
+
program.command("save-text [text]").description("Save already-transcribed text as a recording").option("--text-file <path>", "Read transcript text from a UTF-8 file").option("--stdin", "Read transcript text from stdin").option("--audio-path <path>", "Audio file path associated with this transcript").option("--model-used <model>", "Model/source used to produce the raw transcript").option("--source <source>", "Transcript source label for metadata", "direct_text").option("--duration-ms <ms>", "Recording duration in milliseconds").option("-l, --language <lang>", "Language code").option("-t, --tags <tags>", "Comma-separated tags").option("--no-enhance", "Skip AI enhancement").option("--post-processing <mode>", "Post-processing mode: off, auto, or always").option("--transcriber-prompt <prompt>", "Instructions for post-transcription cleanup").option("--system-prompt <prompt>", "Alias for --transcriber-prompt").option("--transcription-model <model>", "Model for bounded audio transcription").option("--transcriber-model <model>", "Model for post-transcription cleanup").option("--enhancement-model <model>", "Enhancement model fallback").option("--enhance-triggers-json <json>", "Frozen JSON string array of enhancement triggers").option("--keyword-transforms-json <json>", "Frozen JSON string map of keyword transforms").option("--recording-id <id>", "Stable recording ID for idempotent retries").action(async (text, opts) => {
|
|
3450
|
+
const recordingId = recordingCreateIdentity({
|
|
3451
|
+
id: opts.recordingId,
|
|
3452
|
+
raw_text: ""
|
|
3453
|
+
}).input.id;
|
|
3454
|
+
const rawText = await readSaveTextInput(text, opts);
|
|
3455
|
+
const config = loadConfig();
|
|
3456
|
+
ensureDataDir(config);
|
|
3457
|
+
if (opts.language)
|
|
3458
|
+
config.language = opts.language;
|
|
3459
|
+
applyEnhancementOptions(config, opts);
|
|
3460
|
+
const processed = await processText(rawText, config);
|
|
3461
|
+
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
3462
|
+
const parentOpts = program.opts();
|
|
3463
|
+
const metadata = {
|
|
3464
|
+
...buildTranscriptionMetadata(config, processed, {
|
|
3465
|
+
transcriberPromptFromRequest: opts.transcriberPrompt !== undefined || opts.systemPrompt !== undefined
|
|
3466
|
+
}),
|
|
3467
|
+
transcription_source: opts.source || "direct_text",
|
|
3468
|
+
realtime: {
|
|
3469
|
+
fast_path: opts.source === "realtime_fast_path",
|
|
3470
|
+
model: opts.modelUsed || config.realtime_transcription_model || "direct-input",
|
|
3471
|
+
bounded_fallback: false
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
const recording = await getStore().createRecording({
|
|
3475
|
+
id: recordingId,
|
|
3476
|
+
audio_path: opts.audioPath || undefined,
|
|
3477
|
+
raw_text: rawText,
|
|
3478
|
+
processed_text: processed.mode === "enhanced" ? processed.text : undefined,
|
|
3479
|
+
processing_mode: processed.mode,
|
|
3480
|
+
model_used: opts.modelUsed || "direct-input",
|
|
3481
|
+
enhancement_model: processed.enhancement_model || undefined,
|
|
3482
|
+
duration_ms: opts.durationMs ? parseInt(opts.durationMs, 10) : 0,
|
|
3483
|
+
language: opts.language || undefined,
|
|
3484
|
+
tags,
|
|
3485
|
+
agent_id: parentOpts.agent || undefined,
|
|
3486
|
+
project_id: parentOpts.project || undefined,
|
|
3487
|
+
session_id: parentOpts.session || undefined,
|
|
3488
|
+
machine_id: currentMachineId(),
|
|
3489
|
+
metadata
|
|
3490
|
+
}, recordingId);
|
|
2537
3491
|
if (parentOpts.json) {
|
|
2538
3492
|
console.log(JSON.stringify(recording, null, 2));
|
|
3493
|
+
} else if (processed.mode === "enhanced") {
|
|
3494
|
+
console.log(processed.text);
|
|
2539
3495
|
} else {
|
|
2540
|
-
console.log(
|
|
3496
|
+
console.log(rawText);
|
|
2541
3497
|
}
|
|
2542
3498
|
});
|
|
2543
|
-
program.command("rewrite <text>").description("Rewrite provided text using an instruction").requiredOption("-i, --instruction <instruction>", "Rewrite instruction").action(async (text, opts) => {
|
|
3499
|
+
program.command("rewrite <text>").description("Rewrite provided text using an instruction").requiredOption("-i, --instruction <instruction>", "Rewrite instruction").option("--prompt <prompt>", "Frozen transcription vocabulary/context prompt").option("--transcriber-prompt <prompt>", "Frozen post-transcription instructions").option("--system-prompt <prompt>", "Alias for --transcriber-prompt").option("--post-processing <mode>", "Frozen post-processing mode").option("--language <lang>", "Frozen transcription language").option("--transcription-model <model>", "Frozen transcription model").option("--transcriber-model <model>", "Frozen rewrite model").option("--enhancement-model <model>", "Frozen enhancement fallback model").option("--enhance-triggers-json <json>", "Frozen JSON string array of enhancement triggers").option("--keyword-transforms-json <json>", "Frozen JSON string map of keyword transforms").action(async (text, opts) => {
|
|
2544
3500
|
const config = loadConfig();
|
|
3501
|
+
if (opts.language !== undefined)
|
|
3502
|
+
config.language = opts.language;
|
|
3503
|
+
if (opts.prompt !== undefined)
|
|
3504
|
+
config.transcription_prompt = opts.prompt;
|
|
3505
|
+
applyEnhancementOptions(config, opts);
|
|
2545
3506
|
const parentOpts = program.opts();
|
|
2546
3507
|
const instruction = `Instruction: ${opts.instruction}
|
|
2547
3508
|
|
|
@@ -2589,7 +3550,8 @@ program.command("save <text>").description("Save raw text as a recording (no aud
|
|
|
2589
3550
|
tags,
|
|
2590
3551
|
agent_id: parentOpts.agent,
|
|
2591
3552
|
project_id: parentOpts.project,
|
|
2592
|
-
session_id: parentOpts.session
|
|
3553
|
+
session_id: parentOpts.session,
|
|
3554
|
+
machine_id: currentMachineId()
|
|
2593
3555
|
});
|
|
2594
3556
|
if (parentOpts.json) {
|
|
2595
3557
|
console.log(JSON.stringify(recording, null, 2));
|
|
@@ -2602,65 +3564,67 @@ program.command("save <text>").description("Save raw text as a recording (no aud
|
|
|
2602
3564
|
process.exit(1);
|
|
2603
3565
|
}
|
|
2604
3566
|
});
|
|
2605
|
-
program.command("list").description("List recordings").option("-n, --limit <n>", "Max results", "20").option("--mode <mode>", "Filter by mode: raw or enhanced").option("-t, --tags <tags>", "Filter by tags").option("--since <date>", "After date (ISO)").option("--until <date>", "Before date (ISO)").action(async (opts) => {
|
|
3567
|
+
program.command("list").description("List recordings in compact form").option("-n, --limit <n>", "Max results", "20").option("--offset <n>", "Skip this many results").option("--cursor <n>", "Pagination cursor alias for --offset").option("--mode <mode>", "Filter by mode: raw or enhanced").option("-t, --tags <tags>", "Filter by tags").option("--since <date>", "After date (ISO)").option("--until <date>", "Before date (ISO)").option("--verbose", "Show more metadata per row without dumping full text").action(async (opts) => {
|
|
2606
3568
|
const parentOpts = program.opts();
|
|
2607
|
-
const
|
|
2608
|
-
|
|
3569
|
+
const pagination = resolvePagination(opts, parentOpts);
|
|
3570
|
+
const filter = {
|
|
3571
|
+
limit: pagination.limit,
|
|
2609
3572
|
processing_mode: opts.mode,
|
|
2610
|
-
tags: opts.tags
|
|
3573
|
+
tags: parseCsvList(opts.tags),
|
|
2611
3574
|
since: opts.since,
|
|
2612
3575
|
until: opts.until,
|
|
3576
|
+
offset: pagination.offset,
|
|
2613
3577
|
agent_id: parentOpts.agent,
|
|
2614
3578
|
project_id: parentOpts.project,
|
|
2615
3579
|
session_id: parentOpts.session
|
|
2616
|
-
}
|
|
3580
|
+
};
|
|
3581
|
+
const store = getStore();
|
|
3582
|
+
const recordings = await store.listRecordings(filter);
|
|
2617
3583
|
if (parentOpts.json) {
|
|
2618
3584
|
console.log(JSON.stringify(recordings, null, 2));
|
|
2619
3585
|
return;
|
|
2620
3586
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
}
|
|
2630
|
-
});
|
|
2631
|
-
program.command("show <id>").description("Show recording details").action(async (id) => {
|
|
2632
|
-
const parentOpts = program.opts();
|
|
2633
|
-
const recording = await getStore().getRecording(id);
|
|
2634
|
-
if (!recording) {
|
|
2635
|
-
console.error(chalk.red(`Recording not found: ${id}`));
|
|
2636
|
-
process.exit(1);
|
|
2637
|
-
}
|
|
2638
|
-
if (parentOpts.json) {
|
|
2639
|
-
console.log(JSON.stringify(recording, null, 2));
|
|
2640
|
-
return;
|
|
2641
|
-
}
|
|
2642
|
-
console.log(formatRecordingDetail(recording));
|
|
3587
|
+
const total = await countStoreRecordings(store, withoutPagination(filter));
|
|
3588
|
+
printRecordingCollection("recordings", recordings, {
|
|
3589
|
+
total,
|
|
3590
|
+
offset: pagination.offset,
|
|
3591
|
+
limit: pagination.limit,
|
|
3592
|
+
verbose: Boolean(opts.verbose),
|
|
3593
|
+
capped: pagination.capped,
|
|
3594
|
+
empty: "No recordings found."
|
|
3595
|
+
});
|
|
2643
3596
|
});
|
|
2644
|
-
program.command("
|
|
3597
|
+
program.command("show <id>").description("Show recording details").action((id) => printRecordingDetail(id));
|
|
3598
|
+
program.command("inspect <id>").description("Inspect recording details (alias for show)").action((id) => printRecordingDetail(id));
|
|
3599
|
+
program.command("search <query>").description("Search recordings by text content in compact form").option("-n, --limit <n>", "Max results", "20").option("--offset <n>", "Skip this many results").option("--cursor <n>", "Pagination cursor alias for --offset").option("--mode <mode>", "Filter by mode: raw or enhanced").option("-t, --tags <tags>", "Filter by tags").option("--since <date>", "After date (ISO)").option("--until <date>", "Before date (ISO)").option("--session <id>", "Filter by session ID").option("--verbose", "Show more metadata per row without dumping full text").action(async (query, opts) => {
|
|
2645
3600
|
const parentOpts = program.opts();
|
|
2646
|
-
const
|
|
2647
|
-
|
|
3601
|
+
const pagination = resolvePagination(opts, parentOpts);
|
|
3602
|
+
const filter = {
|
|
3603
|
+
limit: pagination.limit,
|
|
3604
|
+
offset: pagination.offset,
|
|
3605
|
+
processing_mode: opts.mode,
|
|
3606
|
+
tags: parseCsvList(opts.tags),
|
|
3607
|
+
since: opts.since,
|
|
3608
|
+
until: opts.until,
|
|
2648
3609
|
agent_id: parentOpts.agent,
|
|
2649
|
-
project_id: parentOpts.project
|
|
2650
|
-
|
|
3610
|
+
project_id: parentOpts.project,
|
|
3611
|
+
session_id: opts.session || parentOpts.session
|
|
3612
|
+
};
|
|
3613
|
+
const store = getStore();
|
|
3614
|
+
const results = await store.searchRecordings(query, filter);
|
|
2651
3615
|
if (parentOpts.json) {
|
|
2652
3616
|
console.log(JSON.stringify(results, null, 2));
|
|
2653
3617
|
return;
|
|
2654
3618
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
}
|
|
3619
|
+
const total = await countStoreRecordings(store, withoutPagination({ ...filter, search: query }));
|
|
3620
|
+
printRecordingCollection("results", results, {
|
|
3621
|
+
total,
|
|
3622
|
+
offset: pagination.offset,
|
|
3623
|
+
limit: pagination.limit,
|
|
3624
|
+
verbose: Boolean(opts.verbose),
|
|
3625
|
+
capped: pagination.capped,
|
|
3626
|
+
empty: "No results."
|
|
3627
|
+
});
|
|
2664
3628
|
});
|
|
2665
3629
|
program.command("delete <id>").description("Delete a recording").action(async (id) => {
|
|
2666
3630
|
const deleted = await getStore().deleteRecording(id);
|
|
@@ -2684,74 +3648,260 @@ program.command("stats").description("Show recording statistics").action(async (
|
|
|
2684
3648
|
console.log(` Raw: ${stats.raw}`);
|
|
2685
3649
|
console.log(` Enhanced: ${stats.enhanced}`);
|
|
2686
3650
|
console.log(` Duration: ${(stats.total_duration_ms / 1000).toFixed(1)}s`);
|
|
2687
|
-
|
|
3651
|
+
const modelEntries = Object.entries(stats.by_model).sort((a, b) => b[1] - a[1]);
|
|
3652
|
+
if (modelEntries.length > 0) {
|
|
2688
3653
|
console.log(` By model:`);
|
|
2689
|
-
for (const [model, count] of
|
|
2690
|
-
console.log(` ${model}: ${count}`);
|
|
3654
|
+
for (const [model, count] of modelEntries.slice(0, 10)) {
|
|
3655
|
+
console.log(` ${truncateText(model, 80)}: ${count}`);
|
|
3656
|
+
}
|
|
3657
|
+
if (modelEntries.length > 10) {
|
|
3658
|
+
console.log(chalk.dim(` ...${modelEntries.length - 10} more model(s). Use --json for the full breakdown.`));
|
|
2691
3659
|
}
|
|
2692
3660
|
}
|
|
2693
3661
|
});
|
|
2694
|
-
program.command("agents").description("List registered agents").action(async () => {
|
|
3662
|
+
program.command("agents").description("List registered agents").option("-n, --limit <n>", "Max results").option("--offset <n>", "Skip this many results").option("--cursor <n>", "Pagination cursor alias for --offset").option("--verbose", "Show descriptions and timestamps").action(async (opts) => {
|
|
2695
3663
|
const parentOpts = program.opts();
|
|
3664
|
+
const pagination = resolvePagination(opts, parentOpts);
|
|
2696
3665
|
const agents = await getStore().listAgents();
|
|
3666
|
+
const page = parentOpts.json ? maybePageJson(agents, pagination, opts) : pageItems(agents, pagination);
|
|
2697
3667
|
if (parentOpts.json) {
|
|
2698
|
-
console.log(JSON.stringify(
|
|
3668
|
+
console.log(JSON.stringify(page, null, 2));
|
|
2699
3669
|
return;
|
|
2700
3670
|
}
|
|
2701
|
-
if (
|
|
2702
|
-
console.log(chalk.dim("No agents registered."));
|
|
3671
|
+
if (page.length === 0) {
|
|
3672
|
+
console.log(chalk.dim(agents.length === 0 ? "No agents registered." : "No agents at this cursor."));
|
|
3673
|
+
if (agents.length > 0)
|
|
3674
|
+
console.log(chalk.dim("Try a lower --cursor."));
|
|
2703
3675
|
return;
|
|
2704
3676
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
3677
|
+
console.log(formatPageHeader("agents", page.length, agents.length, pagination.offset, pagination.limit));
|
|
3678
|
+
for (const a of page) {
|
|
3679
|
+
const line = `${chalk.cyan(truncateText(a.id, 80))} ${chalk.bold(truncateText(a.name, 80))} (${truncateText(a.role, 40)})`;
|
|
3680
|
+
if (opts.verbose) {
|
|
3681
|
+
console.log(`${line}
|
|
3682
|
+
last seen: ${truncateText(a.last_seen_at, 40)}${a.description ? `
|
|
3683
|
+
${truncateText(a.description, 140)}` : ""}`);
|
|
3684
|
+
} else {
|
|
3685
|
+
console.log(`${line} \u2014 ${truncateText(relativeHint(a.last_seen_at), 40)}`);
|
|
3686
|
+
}
|
|
2707
3687
|
}
|
|
3688
|
+
printPaginationHints(page.length, agents.length, pagination);
|
|
2708
3689
|
});
|
|
2709
|
-
program.command("
|
|
3690
|
+
var projectCommand = program.command("project").description("Manage registered projects");
|
|
3691
|
+
projectCommand.command("register").description("Register a project in the active Store").requiredOption("--name <name>", "Project name").requiredOption("--path <path>", "Stable project path or URI").option("--description <description>", "Project description").action(async (opts) => {
|
|
2710
3692
|
const parentOpts = program.opts();
|
|
3693
|
+
const project = await getStore().registerProject(opts.name, opts.path, opts.description);
|
|
3694
|
+
if (parentOpts.json) {
|
|
3695
|
+
console.log(JSON.stringify(project, null, 2));
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
console.log(`${chalk.cyan(truncateText(project.id, 80))} ${chalk.bold(truncateText(project.name, 80))} \u2014 ${truncatePath(project.path, 120)}`);
|
|
3699
|
+
});
|
|
3700
|
+
program.command("projects").description("List registered projects").option("-n, --limit <n>", "Max results").option("--offset <n>", "Skip this many results").option("--cursor <n>", "Pagination cursor alias for --offset").option("--verbose", "Show descriptions and timestamps").action(async (opts) => {
|
|
3701
|
+
const parentOpts = program.opts();
|
|
3702
|
+
const pagination = resolvePagination(opts, parentOpts);
|
|
2711
3703
|
const projects = await getStore().listProjects();
|
|
3704
|
+
const page = parentOpts.json ? maybePageJson(projects, pagination, opts) : pageItems(projects, pagination);
|
|
2712
3705
|
if (parentOpts.json) {
|
|
2713
|
-
console.log(JSON.stringify(
|
|
3706
|
+
console.log(JSON.stringify(page, null, 2));
|
|
2714
3707
|
return;
|
|
2715
3708
|
}
|
|
2716
|
-
if (
|
|
2717
|
-
console.log(chalk.dim("No projects registered."));
|
|
3709
|
+
if (page.length === 0) {
|
|
3710
|
+
console.log(chalk.dim(projects.length === 0 ? "No projects registered." : "No projects at this cursor."));
|
|
3711
|
+
if (projects.length > 0)
|
|
3712
|
+
console.log(chalk.dim("Try a lower --cursor."));
|
|
2718
3713
|
return;
|
|
2719
3714
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
3715
|
+
console.log(formatPageHeader("projects", page.length, projects.length, pagination.offset, pagination.limit));
|
|
3716
|
+
for (const p of page) {
|
|
3717
|
+
const line = `${chalk.cyan(truncateText(p.id, 8))} ${chalk.bold(truncateText(p.name, 80))}`;
|
|
3718
|
+
if (opts.verbose) {
|
|
3719
|
+
console.log(`${line}
|
|
3720
|
+
path: ${truncatePath(p.path, 120)}
|
|
3721
|
+
updated: ${truncateText(p.updated_at, 40)}${p.description ? `
|
|
3722
|
+
${truncateText(p.description, 140)}` : ""}`);
|
|
3723
|
+
} else {
|
|
3724
|
+
console.log(`${line} \u2014 ${truncatePath(p.path, 96)}`);
|
|
3725
|
+
}
|
|
2722
3726
|
}
|
|
3727
|
+
printPaginationHints(page.length, projects.length, pagination);
|
|
2723
3728
|
});
|
|
2724
3729
|
program.command("init").description("Initialize .recordings/ in current directory").action(() => {
|
|
2725
|
-
const { mkdirSync:
|
|
2726
|
-
const { join:
|
|
2727
|
-
const dir =
|
|
2728
|
-
const audioDir =
|
|
2729
|
-
const configFile =
|
|
2730
|
-
|
|
3730
|
+
const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync3, existsSync: existsSync5 } = __require("fs");
|
|
3731
|
+
const { join: join7 } = __require("path");
|
|
3732
|
+
const dir = join7(process.cwd(), ".recordings");
|
|
3733
|
+
const audioDir = join7(dir, "audio");
|
|
3734
|
+
const configFile = join7(dir, "config.json");
|
|
3735
|
+
mkdirSync4(audioDir, { recursive: true });
|
|
2731
3736
|
if (!existsSync5(configFile)) {
|
|
2732
3737
|
const defaultConf = {
|
|
2733
3738
|
transcription_model: "gpt-4o-transcribe",
|
|
3739
|
+
realtime_session_model: "gpt-realtime",
|
|
3740
|
+
realtime_transcription_model: "gpt-realtime-whisper",
|
|
2734
3741
|
enhancement_model: "gpt-4o",
|
|
3742
|
+
transcriber_model: "gpt-4o",
|
|
2735
3743
|
language: "en",
|
|
3744
|
+
transcription_prompt: "",
|
|
3745
|
+
transcriber_prompt: "",
|
|
3746
|
+
post_processing_mode: "auto",
|
|
2736
3747
|
auto_enhance: true
|
|
2737
3748
|
};
|
|
2738
|
-
|
|
3749
|
+
writeFileSync3(configFile, JSON.stringify(defaultConf, null, 2));
|
|
2739
3750
|
}
|
|
2740
3751
|
console.log(chalk.green("Initialized .recordings/ directory"));
|
|
2741
3752
|
console.log(chalk.dim(" config: .recordings/config.json"));
|
|
2742
3753
|
console.log(chalk.dim(" audio: .recordings/audio/"));
|
|
2743
3754
|
console.log(chalk.dim(" db: .recordings/recordings.db"));
|
|
2744
3755
|
});
|
|
2745
|
-
var appCommand = program.command("app").description("Manage the macOS
|
|
2746
|
-
appCommand.command("install").description("
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
3756
|
+
var appCommand = program.command("app").description("Manage the macOS app installed from this package");
|
|
3757
|
+
appCommand.command("install").description("Install a release or explicitly approved local-only Recordings.app artifact").requiredOption("--artifact <path>", "Finalized Recordings.app ZIP artifact").requiredOption("--manifest <path>", "Artifact provenance manifest").option("--envelope <path>", "Signed release envelope (required for release artifacts)").option("--expected-team-id <team>", "Required Developer ID TeamIdentifier for release artifacts").requiredOption("--manifest-sha256 <sha256>", "Authenticated release-manifest SHA-256").requiredOption("--expected-source-sha <sha>", "Exact approved 40-character source commit").requiredOption("--expected-version <version>", "Exact approved release version").option("--expected-hostname <hostname>", "Exact deployment hostname to verify before any install mutation").option("--artifact-policy <policy>", "Artifact policy: release or local-only", "release").option("--approved-target <station>", "Exact approved target; fleet for release artifacts", "fleet").option("--approved-target-identity-kind <kind>", "Target identity kind: hardware_uuid_sha256 or tailscale_node_id_sha256").option("--approved-target-identity-sha256 <sha256>", "Authenticated SHA-256 of the approved target identity; none for release artifacts", "none").option("--acknowledge-local-signing-and-permissions", "Acknowledge local-only ad-hoc identity and possible permission reauthorization").option("--expected-old-identity-sha256 <sha256>", "Exact installed identity approved for migration").option("--expected-new-identity-sha256 <sha256>", "Exact candidate identity approved for migration").option("--allow-signing-identity-migration", "Allow one reviewed signer change that requires new macOS permission approval").option("--launch", "Launch and verify the canonical app after installation").option("--launch-timeout <seconds>", "Canonical process launch timeout").action((opts) => {
|
|
3758
|
+
if (process.platform !== "darwin") {
|
|
3759
|
+
console.error(chalk.red("Recordings.app installation is only supported on macOS"));
|
|
3760
|
+
process.exit(1);
|
|
3761
|
+
}
|
|
3762
|
+
if (opts.artifactPolicy === "release") {
|
|
3763
|
+
if (!opts.envelope) {
|
|
3764
|
+
console.error(chalk.red("Release installation requires --envelope."));
|
|
3765
|
+
process.exit(1);
|
|
3766
|
+
}
|
|
3767
|
+
let preparedInputs;
|
|
3768
|
+
try {
|
|
3769
|
+
assertReleaseOnlyOptions(opts);
|
|
3770
|
+
if (opts.expectedHostname) {
|
|
3771
|
+
const hostnameResult = spawnSync4("/bin/hostname", ["-s"], {
|
|
3772
|
+
encoding: "utf8",
|
|
3773
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3774
|
+
env: {
|
|
3775
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
3776
|
+
LC_ALL: "C",
|
|
3777
|
+
LANG: "C",
|
|
3778
|
+
TZ: "UTC0"
|
|
3779
|
+
}
|
|
3780
|
+
});
|
|
3781
|
+
if (hostnameResult.error || hostnameResult.status !== 0) {
|
|
3782
|
+
throw new Error("could not determine the install target hostname");
|
|
3783
|
+
}
|
|
3784
|
+
assertExpectedReleaseHostname(opts.expectedHostname, hostnameResult.stdout.trim());
|
|
3785
|
+
}
|
|
3786
|
+
preparedInputs = prepareReleaseInstallInputs({
|
|
3787
|
+
artifactPath: opts.artifact,
|
|
3788
|
+
manifestPath: opts.manifest,
|
|
3789
|
+
envelopePath: opts.envelope,
|
|
3790
|
+
manifestSha256: opts.manifestSha256,
|
|
3791
|
+
expectedSourceSha: opts.expectedSourceSha,
|
|
3792
|
+
expectedVersion: opts.expectedVersion,
|
|
3793
|
+
expectedTeamId: opts.expectedTeamId
|
|
3794
|
+
});
|
|
3795
|
+
} catch (error) {
|
|
3796
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
3797
|
+
process.exit(1);
|
|
3798
|
+
}
|
|
3799
|
+
const updateClientPath = "/Applications/Recordings.app/Contents/Helpers/recordings-update-client";
|
|
3800
|
+
if (!existsSync4(updateClientPath)) {
|
|
3801
|
+
preparedInputs.cleanup();
|
|
3802
|
+
console.error(chalk.red("Root-owned Recordings update broker client is not installed."));
|
|
3803
|
+
process.exit(1);
|
|
3804
|
+
}
|
|
3805
|
+
const result2 = (() => {
|
|
3806
|
+
try {
|
|
3807
|
+
return spawnSync4(updateClientPath, [
|
|
3808
|
+
"install",
|
|
3809
|
+
"--artifact",
|
|
3810
|
+
opts.artifact,
|
|
3811
|
+
"--manifest",
|
|
3812
|
+
preparedInputs.manifestPath,
|
|
3813
|
+
"--envelope",
|
|
3814
|
+
preparedInputs.envelopePath
|
|
3815
|
+
], {
|
|
3816
|
+
stdio: "inherit",
|
|
3817
|
+
env: {
|
|
3818
|
+
HOME: process.env.HOME ?? "",
|
|
3819
|
+
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
3820
|
+
LC_ALL: "C",
|
|
3821
|
+
LANG: "C",
|
|
3822
|
+
TZ: "UTC0"
|
|
3823
|
+
}
|
|
3824
|
+
});
|
|
3825
|
+
} finally {
|
|
3826
|
+
preparedInputs.cleanup();
|
|
3827
|
+
}
|
|
3828
|
+
})();
|
|
3829
|
+
if (result2.error) {
|
|
3830
|
+
console.error(chalk.red(result2.error.message));
|
|
3831
|
+
process.exit(1);
|
|
3832
|
+
}
|
|
3833
|
+
process.exit(result2.status ?? 1);
|
|
3834
|
+
}
|
|
3835
|
+
if (opts.artifactPolicy !== "local-only" && opts.artifactPolicy !== "local_only") {
|
|
3836
|
+
console.error(chalk.red("Artifact policy must be release or local-only."));
|
|
3837
|
+
process.exit(1);
|
|
3838
|
+
}
|
|
3839
|
+
let launchTimeout;
|
|
3840
|
+
try {
|
|
3841
|
+
launchTimeout = parseLaunchTimeout(opts.launchTimeout);
|
|
3842
|
+
} catch (error) {
|
|
3843
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
3844
|
+
process.exit(1);
|
|
3845
|
+
}
|
|
3846
|
+
let bunExecutable;
|
|
3847
|
+
try {
|
|
3848
|
+
bunExecutable = resolveInstallBunExecutable(process.env);
|
|
3849
|
+
} catch (error) {
|
|
3850
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
3851
|
+
process.exit(1);
|
|
3852
|
+
}
|
|
3853
|
+
const installerPath = getMacOSInstallerPath();
|
|
3854
|
+
if (!existsSync4(installerPath)) {
|
|
3855
|
+
console.error(chalk.red(`App installer missing from package: ${installerPath}`));
|
|
2750
3856
|
process.exit(1);
|
|
2751
3857
|
}
|
|
2752
|
-
const
|
|
3858
|
+
const installerArgs = [
|
|
3859
|
+
installerPath,
|
|
3860
|
+
"--artifact",
|
|
3861
|
+
opts.artifact,
|
|
3862
|
+
"--manifest",
|
|
3863
|
+
opts.manifest,
|
|
3864
|
+
"--manifest-sha256",
|
|
3865
|
+
opts.manifestSha256,
|
|
3866
|
+
"--expected-source-sha",
|
|
3867
|
+
opts.expectedSourceSha,
|
|
3868
|
+
"--expected-version",
|
|
3869
|
+
opts.expectedVersion,
|
|
3870
|
+
"--artifact-policy",
|
|
3871
|
+
opts.artifactPolicy,
|
|
3872
|
+
"--approved-target",
|
|
3873
|
+
opts.approvedTarget,
|
|
3874
|
+
"--launch-timeout",
|
|
3875
|
+
launchTimeout
|
|
3876
|
+
];
|
|
3877
|
+
if (opts.expectedHostname) {
|
|
3878
|
+
installerArgs.push("--expected-hostname", opts.expectedHostname);
|
|
3879
|
+
}
|
|
3880
|
+
if (opts.approvedTargetIdentityKind) {
|
|
3881
|
+
installerArgs.push("--approved-target-identity-kind", opts.approvedTargetIdentityKind);
|
|
3882
|
+
}
|
|
3883
|
+
installerArgs.push("--approved-target-identity-sha256", opts.approvedTargetIdentitySha256);
|
|
3884
|
+
if (opts.expectedTeamId) {
|
|
3885
|
+
installerArgs.push("--expected-team-id", opts.expectedTeamId);
|
|
3886
|
+
}
|
|
3887
|
+
if (opts.acknowledgeLocalSigningAndPermissions) {
|
|
3888
|
+
installerArgs.push("--acknowledge-local-signing-and-permissions");
|
|
3889
|
+
}
|
|
3890
|
+
if (opts.allowSigningIdentityMigration) {
|
|
3891
|
+
installerArgs.push("--allow-signing-identity-migration");
|
|
3892
|
+
}
|
|
3893
|
+
if (opts.expectedOldIdentitySha256) {
|
|
3894
|
+
installerArgs.push("--expected-old-identity-sha256", opts.expectedOldIdentitySha256);
|
|
3895
|
+
}
|
|
3896
|
+
if (opts.expectedNewIdentitySha256) {
|
|
3897
|
+
installerArgs.push("--expected-new-identity-sha256", opts.expectedNewIdentitySha256);
|
|
3898
|
+
}
|
|
3899
|
+
if (opts.launch)
|
|
3900
|
+
installerArgs.push("--launch");
|
|
3901
|
+
const installerEnvironment = createInstallerEnvironment(process.env, bunExecutable);
|
|
3902
|
+
const result = spawnSync4("/bin/bash", installerArgs, {
|
|
2753
3903
|
stdio: "inherit",
|
|
2754
|
-
env:
|
|
3904
|
+
env: installerEnvironment
|
|
2755
3905
|
});
|
|
2756
3906
|
if (result.error) {
|
|
2757
3907
|
console.error(chalk.red(result.error.message));
|
|
@@ -2759,22 +3909,36 @@ appCommand.command("install").description("Build and install Recordings.app from
|
|
|
2759
3909
|
}
|
|
2760
3910
|
process.exit(result.status ?? 1);
|
|
2761
3911
|
});
|
|
2762
|
-
appCommand.command("status").description("Show installed Recordings.app status").action(() => {
|
|
3912
|
+
appCommand.command("status").description("Show installed Recordings.app status").option("--verbose", "Show package paths, code hash, and log path").action((opts) => {
|
|
2763
3913
|
const status = getMacOSAppStatus();
|
|
2764
3914
|
if (program.opts().json) {
|
|
2765
3915
|
console.log(JSON.stringify(status, null, 2));
|
|
2766
3916
|
return;
|
|
2767
3917
|
}
|
|
2768
|
-
console.log(
|
|
3918
|
+
console.log(chalk.bold("Recordings.app"));
|
|
3919
|
+
console.log(`Installed: ${status.installed ? "yes" : "no"}`);
|
|
3920
|
+
console.log(`Executable: ${status.executable ? "available" : "missing"}`);
|
|
2769
3921
|
console.log(`Installer: ${status.installer_available ? "available" : "missing"}`);
|
|
2770
3922
|
console.log(`Native sources: ${status.native_sources_available ? "available" : "missing"}`);
|
|
2771
|
-
console.log(`
|
|
2772
|
-
console.log(`Executable: ${status.executable ? "available" : "missing"}`);
|
|
2773
|
-
console.log(`Code hash: ${status.app_code_hash ?? "unavailable"}`);
|
|
3923
|
+
console.log(`Legacy duplicates: ${status.legacy_install_paths.length}`);
|
|
2774
3924
|
if (process.platform === "darwin") {
|
|
2775
3925
|
console.log(`Microphone: ${status.microphone_permission}`);
|
|
2776
3926
|
console.log(`Accessibility: ${status.accessibility_permission}`);
|
|
3927
|
+
}
|
|
3928
|
+
if (opts.verbose) {
|
|
3929
|
+
console.log(`Package: ${status.package_root}`);
|
|
3930
|
+
console.log(`Installed app: ${status.installed ? status.installed_app_path : "missing"}`);
|
|
3931
|
+
console.log(`Executable path: ${status.executable_path}`);
|
|
3932
|
+
for (const legacyPath of status.legacy_install_paths) {
|
|
3933
|
+
console.log(`Legacy app: ${legacyPath}`);
|
|
3934
|
+
}
|
|
3935
|
+
console.log(`Signing identifier: ${status.signing_identifier ?? "unavailable"}`);
|
|
3936
|
+
console.log(`Team identifier: ${status.team_identifier ?? "unavailable"}`);
|
|
3937
|
+
console.log(`Designated requirement: ${status.designated_requirement ?? "unavailable"}`);
|
|
3938
|
+
console.log(`Code hash: ${status.app_code_hash ?? "unavailable"}`);
|
|
2777
3939
|
console.log(`Log: ${status.log_path}`);
|
|
3940
|
+
} else {
|
|
3941
|
+
console.log(chalk.dim("Use --verbose for paths/code hash/log, or --json for the full status object."));
|
|
2778
3942
|
}
|
|
2779
3943
|
});
|
|
2780
3944
|
appCommand.command("permissions").description("Show macOS permission state for Recordings.app").action(() => {
|
|
@@ -2782,10 +3946,15 @@ appCommand.command("permissions").description("Show macOS permission state for R
|
|
|
2782
3946
|
const permissions = {
|
|
2783
3947
|
platform: status.platform,
|
|
2784
3948
|
bundle_id: "com.hasna.recordings",
|
|
3949
|
+
installed_app_path: status.installed_app_path,
|
|
3950
|
+
legacy_install_paths: status.legacy_install_paths,
|
|
2785
3951
|
microphone: status.microphone_permission,
|
|
2786
3952
|
accessibility: status.accessibility_permission,
|
|
2787
3953
|
app_code_hash: status.app_code_hash,
|
|
2788
3954
|
ad_hoc_signed: status.ad_hoc_signed,
|
|
3955
|
+
signing_identifier: status.signing_identifier,
|
|
3956
|
+
team_identifier: status.team_identifier,
|
|
3957
|
+
designated_requirement: status.designated_requirement,
|
|
2789
3958
|
log_path: status.log_path
|
|
2790
3959
|
};
|
|
2791
3960
|
if (program.opts().json) {
|
|
@@ -2816,27 +3985,20 @@ appCommand.command("request-permissions").description("Open Recordings.app and t
|
|
|
2816
3985
|
if (opts.reset) {
|
|
2817
3986
|
resetMacOSPermissions();
|
|
2818
3987
|
}
|
|
2819
|
-
const result =
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
"--args",
|
|
2823
|
-
"--request-permissions",
|
|
2824
|
-
"--open-permission-settings"
|
|
2825
|
-
], { stdio: "inherit" });
|
|
2826
|
-
if (result.error) {
|
|
2827
|
-
console.error(chalk.red(result.error.message));
|
|
2828
|
-
process.exit(1);
|
|
3988
|
+
const result = runMacOSPermissionRequest(status.installed_app_path);
|
|
3989
|
+
if (result.errorMessage) {
|
|
3990
|
+
console.error(chalk.red(result.errorMessage));
|
|
2829
3991
|
}
|
|
2830
|
-
process.exit(result.
|
|
3992
|
+
process.exit(result.exitCode);
|
|
2831
3993
|
});
|
|
2832
|
-
appCommand.command("log").description("Show the Recordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print",
|
|
3994
|
+
appCommand.command("log").description("Show the Recordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print", String(DEFAULT_LOG_LINES)).action((opts) => {
|
|
2833
3995
|
const status = getMacOSAppStatus();
|
|
2834
3996
|
if (!existsSync4(status.log_path)) {
|
|
2835
3997
|
console.log("");
|
|
2836
3998
|
return;
|
|
2837
3999
|
}
|
|
2838
|
-
const lines = Math.max(1, parseInt(opts.lines, 10) ||
|
|
2839
|
-
const result =
|
|
4000
|
+
const lines = Math.max(1, parseInt(opts.lines, 10) || DEFAULT_LOG_LINES);
|
|
4001
|
+
const result = spawnSync4("tail", ["-n", String(lines), status.log_path], {
|
|
2840
4002
|
encoding: "utf8"
|
|
2841
4003
|
});
|
|
2842
4004
|
if (result.error) {
|
|
@@ -2857,7 +4019,7 @@ appCommand.command("open").description("Open the installed Recordings.app").acti
|
|
|
2857
4019
|
console.error(chalk.red("Recordings.app is not installed. Run: recordings app install"));
|
|
2858
4020
|
process.exit(1);
|
|
2859
4021
|
}
|
|
2860
|
-
const result =
|
|
4022
|
+
const result = spawnSync4("open", [status.installed_app_path], { stdio: "inherit" });
|
|
2861
4023
|
if (result.error) {
|
|
2862
4024
|
console.error(chalk.red(result.error.message));
|
|
2863
4025
|
process.exit(1);
|
|
@@ -2878,7 +4040,14 @@ program.command("check").description("Check system dependencies (sox, API keys)"
|
|
|
2878
4040
|
},
|
|
2879
4041
|
openai_api_key_configured: Boolean(config.openai_api_key),
|
|
2880
4042
|
enhancement_api_key_configured: Boolean(enhKey),
|
|
2881
|
-
enhancement_model: config.enhancement_model
|
|
4043
|
+
enhancement_model: config.enhancement_model,
|
|
4044
|
+
transcriber_model: resolveTranscriberModel(config),
|
|
4045
|
+
realtime_session_model: config.realtime_session_model,
|
|
4046
|
+
realtime_transcription_model: config.realtime_transcription_model,
|
|
4047
|
+
post_processing_mode: config.post_processing_mode,
|
|
4048
|
+
transcription_prompt_configured: Boolean(config.transcription_prompt?.trim()),
|
|
4049
|
+
transcriber_prompt_configured: Boolean(config.transcriber_prompt?.trim()),
|
|
4050
|
+
config_warnings: config.config_warnings ?? []
|
|
2882
4051
|
}, null, 2));
|
|
2883
4052
|
return;
|
|
2884
4053
|
}
|
|
@@ -2893,16 +4062,18 @@ program.command("check").description("Check system dependencies (sox, API keys)"
|
|
|
2893
4062
|
console.log(chalk.red(`\u2717 OpenAI API key not found. Set OPENAI_API_KEY env var or add to ~/.secrets`));
|
|
2894
4063
|
}
|
|
2895
4064
|
if (enhKey) {
|
|
2896
|
-
console.log(chalk.green(`\u2713 Enhancement API key configured (model: ${config
|
|
4065
|
+
console.log(chalk.green(`\u2713 Enhancement API key configured (model: ${resolveTranscriberModel(config)})`));
|
|
2897
4066
|
} else {
|
|
2898
4067
|
console.log(chalk.yellow(`\u26A0 Enhancement API key not configured \u2014 enhancement disabled`));
|
|
2899
4068
|
}
|
|
2900
4069
|
});
|
|
2901
|
-
program.command("listen").description("Push-to-talk mode \u2014 press Space to start/stop recording, Esc to quit").option("-t, --tags <tags>", "Comma-separated tags for all recordings").option("--no-enhance", "Skip AI enhancement").option("-l, --language <lang>", "Language code").option("--copy", "Copy output to clipboard").option("--paste", "Copy output to clipboard AND paste into frontmost app").action(async (opts) => {
|
|
4070
|
+
program.command("listen").description("Push-to-talk mode \u2014 press Space to start/stop recording, Esc to quit").option("-t, --tags <tags>", "Comma-separated tags for all recordings").option("--no-enhance", "Skip AI enhancement").option("--post-processing <mode>", "Post-processing mode: off, auto, or always").option("--prompt <prompt>", "Vocabulary/context prompt for transcription").option("--transcriber-prompt <prompt>", "Instructions for post-transcription cleanup").option("--system-prompt <prompt>", "Alias for --transcriber-prompt").option("--transcriber-model <model>", "Model for post-transcription cleanup").option("-l, --language <lang>", "Language code").option("--copy", "Copy output to clipboard").option("--paste", "Copy output to clipboard AND paste into frontmost app").action(async (opts) => {
|
|
2902
4071
|
const config = loadConfig();
|
|
2903
4072
|
ensureDataDir(config);
|
|
2904
4073
|
if (opts.language)
|
|
2905
4074
|
config.language = opts.language;
|
|
4075
|
+
if (opts.prompt !== undefined)
|
|
4076
|
+
config.transcription_prompt = opts.prompt;
|
|
2906
4077
|
applyEnhancementOptions(config, opts);
|
|
2907
4078
|
const deps = await checkRecordingDeps();
|
|
2908
4079
|
if (!deps.available) {
|
|
@@ -2980,7 +4151,12 @@ Bye.`));
|
|
|
2980
4151
|
tags,
|
|
2981
4152
|
agent_id: parentOpts.agent || undefined,
|
|
2982
4153
|
project_id: parentOpts.project || undefined,
|
|
2983
|
-
session_id: parentOpts.session || undefined
|
|
4154
|
+
session_id: parentOpts.session || undefined,
|
|
4155
|
+
machine_id: currentMachineId(),
|
|
4156
|
+
metadata: buildTranscriptionMetadata(config, processed, {
|
|
4157
|
+
transcriptionPromptFromRequest: opts.prompt !== undefined,
|
|
4158
|
+
transcriberPromptFromRequest: opts.transcriberPrompt !== undefined || opts.systemPrompt !== undefined
|
|
4159
|
+
})
|
|
2984
4160
|
});
|
|
2985
4161
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
2986
4162
|
const modeLabel = processed.mode === "enhanced" ? chalk.green(" [enhanced] ") : chalk.dim(" [raw] ");
|
|
@@ -3006,12 +4182,12 @@ Bye.`));
|
|
|
3006
4182
|
});
|
|
3007
4183
|
});
|
|
3008
4184
|
program.command("shortcut").description("Set up a global keyboard shortcut for recording (macOS)").option("--raycast", "Generate Raycast script command").option("--karabiner", "Set up Fn key via Karabiner-Elements").option("--skhd", "Generate skhd hotkey config").option("--hammerspoon", "Generate Hammerspoon config").option("--script", "Just output the shell script path").action((opts) => {
|
|
3009
|
-
const { writeFileSync, mkdirSync:
|
|
4185
|
+
const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4, chmodSync: chmodSync3 } = __require("fs");
|
|
3010
4186
|
const { join: pathJoin2 } = __require("path");
|
|
3011
4187
|
const { homedir: getHome } = __require("os");
|
|
3012
4188
|
const home = getHome();
|
|
3013
4189
|
const scriptDir = pathJoin2(home, ".hasna", "recordings");
|
|
3014
|
-
|
|
4190
|
+
mkdirSync4(scriptDir, { recursive: true });
|
|
3015
4191
|
const scriptPath = pathJoin2(scriptDir, "record-toggle.sh");
|
|
3016
4192
|
const pidFile = pathJoin2(scriptDir, ".recording.pid");
|
|
3017
4193
|
const recordingsBin = pathJoin2(home, ".bun", "bin", "recordings");
|
|
@@ -3059,11 +4235,11 @@ else
|
|
|
3059
4235
|
osascript -e 'display notification "Recording started..." with title "Recordings"' 2>/dev/null || true
|
|
3060
4236
|
fi
|
|
3061
4237
|
`;
|
|
3062
|
-
|
|
3063
|
-
|
|
4238
|
+
writeFileSync3(scriptPath, script, "utf-8");
|
|
4239
|
+
chmodSync3(scriptPath, 493);
|
|
3064
4240
|
if (opts.karabiner) {
|
|
3065
4241
|
const karabinerDir = pathJoin2(home, ".config", "karabiner", "assets", "complex_modifications");
|
|
3066
|
-
|
|
4242
|
+
mkdirSync4(karabinerDir, { recursive: true });
|
|
3067
4243
|
const rule = {
|
|
3068
4244
|
title: "Recordings \u2014 Fn key to toggle recording",
|
|
3069
4245
|
rules: [
|
|
@@ -3087,7 +4263,7 @@ fi
|
|
|
3087
4263
|
]
|
|
3088
4264
|
};
|
|
3089
4265
|
const karabinerPath = pathJoin2(karabinerDir, "recordings-fn.json");
|
|
3090
|
-
|
|
4266
|
+
writeFileSync3(karabinerPath, JSON.stringify(rule, null, 2) + `
|
|
3091
4267
|
`, "utf-8");
|
|
3092
4268
|
console.log(chalk.green("Karabiner-Elements rule created!"));
|
|
3093
4269
|
console.log(chalk.dim(` ${karabinerPath}
|
|
@@ -3103,7 +4279,7 @@ fi
|
|
|
3103
4279
|
}
|
|
3104
4280
|
if (opts.raycast) {
|
|
3105
4281
|
const raycastDir = pathJoin2(home, ".config", "raycast", "script-commands");
|
|
3106
|
-
|
|
4282
|
+
mkdirSync4(raycastDir, { recursive: true });
|
|
3107
4283
|
const raycastScript = `#!/bin/bash
|
|
3108
4284
|
|
|
3109
4285
|
# Required parameters:
|
|
@@ -3118,8 +4294,8 @@ fi
|
|
|
3118
4294
|
${scriptPath}
|
|
3119
4295
|
`;
|
|
3120
4296
|
const raycastPath = pathJoin2(raycastDir, "toggle-recording.sh");
|
|
3121
|
-
|
|
3122
|
-
|
|
4297
|
+
writeFileSync3(raycastPath, raycastScript, "utf-8");
|
|
4298
|
+
chmodSync3(raycastPath, 493);
|
|
3123
4299
|
console.log(chalk.green("Raycast script command created!"));
|
|
3124
4300
|
console.log(chalk.dim(` ${raycastPath}`));
|
|
3125
4301
|
console.log(chalk.dim(" Open Raycast > Script Commands > reload to see it"));
|
|
@@ -3174,51 +4350,253 @@ ${scriptPath}
|
|
|
3174
4350
|
console.log(` Create a workflow with a Hotkey trigger \u2192 Run Script: ${scriptPath}
|
|
3175
4351
|
`);
|
|
3176
4352
|
});
|
|
4353
|
+
function buildTranscriptionMetadata(config, processed, sources = {}) {
|
|
4354
|
+
const transcriptionPromptConfigured = Boolean(config.transcription_prompt?.trim());
|
|
4355
|
+
const transcriberPromptConfigured = Boolean(config.transcriber_prompt?.trim());
|
|
4356
|
+
return {
|
|
4357
|
+
transcription_prompt: {
|
|
4358
|
+
configured: transcriptionPromptConfigured,
|
|
4359
|
+
source: sources.transcriptionPromptFromRequest ? "request" : transcriptionPromptConfigured ? "config" : "none"
|
|
4360
|
+
},
|
|
4361
|
+
transcriber_prompt: {
|
|
4362
|
+
configured: transcriberPromptConfigured,
|
|
4363
|
+
source: sources.transcriberPromptFromRequest ? "request" : transcriberPromptConfigured ? "config" : "none"
|
|
4364
|
+
},
|
|
4365
|
+
post_processing: {
|
|
4366
|
+
mode: processed.post_processing_mode,
|
|
4367
|
+
applied: processed.mode === "enhanced",
|
|
4368
|
+
reason: processed.enhancement_reason,
|
|
4369
|
+
model: processed.enhancement_model
|
|
4370
|
+
},
|
|
4371
|
+
transcriber_model: resolveTranscriberModel(config)
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
async function readSaveTextInput(text, opts) {
|
|
4375
|
+
const sourceCount = [
|
|
4376
|
+
text !== undefined,
|
|
4377
|
+
opts.textFile !== undefined,
|
|
4378
|
+
Boolean(opts.stdin)
|
|
4379
|
+
].filter(Boolean).length;
|
|
4380
|
+
if (sourceCount !== 1) {
|
|
4381
|
+
throw new Error("Provide transcript text as an argument, --text-file, or --stdin");
|
|
4382
|
+
}
|
|
4383
|
+
let rawText;
|
|
4384
|
+
if (opts.textFile !== undefined) {
|
|
4385
|
+
rawText = readFileSync3(opts.textFile, "utf8");
|
|
4386
|
+
} else if (opts.stdin) {
|
|
4387
|
+
rawText = await Bun.stdin.text();
|
|
4388
|
+
} else {
|
|
4389
|
+
rawText = text ?? "";
|
|
4390
|
+
}
|
|
4391
|
+
if (!rawText.trim()) {
|
|
4392
|
+
throw new Error("Transcript text is empty");
|
|
4393
|
+
}
|
|
4394
|
+
return rawText;
|
|
4395
|
+
}
|
|
4396
|
+
function resolvePagination(opts, parentOpts, defaultLimit = DEFAULT_LIST_LIMIT) {
|
|
4397
|
+
const parsedLimit = parseNonNegativeInt(opts.limit, defaultLimit);
|
|
4398
|
+
const requestedLimit = Math.min(Math.max(parsedLimit || defaultLimit, 1), 500);
|
|
4399
|
+
const offset = parseNonNegativeInt(opts.cursor ?? opts.offset, 0);
|
|
4400
|
+
const humanLimit = Math.min(requestedLimit, MAX_HUMAN_LIST_LIMIT);
|
|
4401
|
+
return {
|
|
4402
|
+
limit: parentOpts.json ? requestedLimit : humanLimit,
|
|
4403
|
+
offset,
|
|
4404
|
+
capped: !parentOpts.json && requestedLimit > humanLimit
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
function parseNonNegativeInt(value, fallback) {
|
|
4408
|
+
if (value === undefined)
|
|
4409
|
+
return fallback;
|
|
4410
|
+
if (!/^\d+$/.test(value.trim()))
|
|
4411
|
+
return fallback;
|
|
4412
|
+
const parsed = Number(value);
|
|
4413
|
+
return Number.isSafeInteger(parsed) ? parsed : fallback;
|
|
4414
|
+
}
|
|
4415
|
+
function parseCsvList(value) {
|
|
4416
|
+
if (!value)
|
|
4417
|
+
return;
|
|
4418
|
+
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
4419
|
+
return parts.length > 0 ? parts : undefined;
|
|
4420
|
+
}
|
|
4421
|
+
function withoutPagination(filter) {
|
|
4422
|
+
const { limit: _limit, offset: _offset, ...rest } = filter;
|
|
4423
|
+
return rest;
|
|
4424
|
+
}
|
|
4425
|
+
function maybePageJson(items, pagination, opts) {
|
|
4426
|
+
if (opts.limit === undefined && opts.offset === undefined && opts.cursor === undefined) {
|
|
4427
|
+
return items;
|
|
4428
|
+
}
|
|
4429
|
+
return pageItems(items, pagination);
|
|
4430
|
+
}
|
|
4431
|
+
function pageItems(items, pagination) {
|
|
4432
|
+
return items.slice(pagination.offset, pagination.offset + pagination.limit);
|
|
4433
|
+
}
|
|
4434
|
+
function formatPageHeader(label, shown, total, offset, limit) {
|
|
4435
|
+
const start = total === 0 ? 0 : offset + 1;
|
|
4436
|
+
const end = offset + shown;
|
|
4437
|
+
return chalk.bold(`${label}: showing ${shown} of ${total} (${start}-${end}, limit ${limit})
|
|
4438
|
+
`);
|
|
4439
|
+
}
|
|
4440
|
+
function printPaginationHints(shown, total, pagination) {
|
|
4441
|
+
const next = pagination.offset + shown;
|
|
4442
|
+
if (pagination.capped) {
|
|
4443
|
+
console.log(chalk.dim(`Limit capped at ${pagination.limit} for terminal output; use --json for larger machine-readable exports.`));
|
|
4444
|
+
}
|
|
4445
|
+
if (next < total) {
|
|
4446
|
+
console.log(chalk.dim(`Next page: add --cursor ${next}`));
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
function printRecordingCollection(label, recordings, options) {
|
|
4450
|
+
if (recordings.length === 0) {
|
|
4451
|
+
console.log(chalk.dim(options.empty));
|
|
4452
|
+
if (options.total > 0) {
|
|
4453
|
+
console.log(chalk.dim("Try a lower --cursor or remove filters."));
|
|
4454
|
+
}
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
const total = Math.max(options.total, options.offset + recordings.length);
|
|
4458
|
+
console.log(formatPageHeader(label, recordings.length, total, options.offset, options.limit));
|
|
4459
|
+
for (const recording of recordings) {
|
|
4460
|
+
console.log(options.verbose ? formatRecordingVerboseLine(recording) : formatRecordingLine(recording));
|
|
4461
|
+
}
|
|
4462
|
+
console.log("");
|
|
4463
|
+
printPaginationHints(recordings.length, total, {
|
|
4464
|
+
limit: options.limit,
|
|
4465
|
+
offset: options.offset,
|
|
4466
|
+
capped: options.capped
|
|
4467
|
+
});
|
|
4468
|
+
console.log(chalk.dim("Details: recordings show <id> or inspect <id>. Use --verbose for metadata, --json for raw records."));
|
|
4469
|
+
}
|
|
4470
|
+
async function printRecordingDetail(id) {
|
|
4471
|
+
const parentOpts = program.opts();
|
|
4472
|
+
const recording = await getStore().getRecording(id);
|
|
4473
|
+
if (!recording) {
|
|
4474
|
+
console.error(chalk.red(`Recording not found: ${id}`));
|
|
4475
|
+
process.exitCode = 1;
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
if (parentOpts.json) {
|
|
4479
|
+
console.log(JSON.stringify(recording, null, 2));
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
console.log(formatRecordingDetail(recording));
|
|
4483
|
+
}
|
|
3177
4484
|
function formatRecordingLine(r) {
|
|
3178
|
-
const id = chalk.cyan(r.id
|
|
4485
|
+
const id = chalk.cyan(truncateText(r.id, 8));
|
|
3179
4486
|
const mode = r.processing_mode === "enhanced" ? chalk.green("enhanced") : chalk.dim("raw");
|
|
3180
|
-
const text = (r.processed_text || r.raw_text
|
|
3181
|
-
const date = chalk.dim(r.created_at
|
|
3182
|
-
const tags = r.tags.length > 0 ? chalk.yellow(` [${r.tags
|
|
4487
|
+
const text = truncateText(r.processed_text || r.raw_text, 100);
|
|
4488
|
+
const date = chalk.dim(truncateText(r.created_at, 16));
|
|
4489
|
+
const tags = r.tags.length > 0 ? chalk.yellow(` [${summarizeTags(r.tags)}]`) : "";
|
|
3183
4490
|
return `${id} ${mode} ${date}${tags}
|
|
3184
|
-
${text}
|
|
4491
|
+
${text}`;
|
|
4492
|
+
}
|
|
4493
|
+
function formatRecordingVerboseLine(r) {
|
|
4494
|
+
const lines = [formatRecordingLine(r)];
|
|
4495
|
+
const model = r.enhancement_model ? `${truncateText(r.model_used, 80)} -> ${truncateText(r.enhancement_model, 80)}` : truncateText(r.model_used, 80);
|
|
4496
|
+
lines.push(` model: ${model}`);
|
|
4497
|
+
if (r.duration_ms)
|
|
4498
|
+
lines.push(` duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
|
|
4499
|
+
if (r.language)
|
|
4500
|
+
lines.push(` language: ${truncateText(r.language, 20)}`);
|
|
4501
|
+
if (r.audio_path)
|
|
4502
|
+
lines.push(` audio: ${truncatePath(r.audio_path, 120)}`);
|
|
4503
|
+
const scopes = [
|
|
4504
|
+
r.agent_id ? `agent=${truncateText(r.agent_id, 80)}` : null,
|
|
4505
|
+
r.project_id ? `project=${truncateText(r.project_id, 80)}` : null,
|
|
4506
|
+
r.session_id ? `session=${truncateText(r.session_id, 80)}` : null
|
|
4507
|
+
].filter(Boolean);
|
|
4508
|
+
if (scopes.length > 0)
|
|
4509
|
+
lines.push(` scope: ${scopes.join(" ")}`);
|
|
4510
|
+
return lines.join(`
|
|
4511
|
+
`);
|
|
3185
4512
|
}
|
|
3186
4513
|
function formatRecordingDetail(r) {
|
|
3187
4514
|
const lines = [
|
|
3188
|
-
chalk.bold(`Recording ${r.id
|
|
4515
|
+
chalk.bold(`Recording ${truncateText(r.id, 8)}`),
|
|
3189
4516
|
"",
|
|
3190
4517
|
` Mode: ${r.processing_mode === "enhanced" ? chalk.green("enhanced") : chalk.dim("raw")}`,
|
|
3191
|
-
` Model: ${r.model_used}`
|
|
4518
|
+
` Model: ${truncateText(r.model_used, 80)}`
|
|
3192
4519
|
];
|
|
3193
4520
|
if (r.enhancement_model) {
|
|
3194
|
-
lines.push(` Enhanced: ${r.enhancement_model}`);
|
|
4521
|
+
lines.push(` Enhanced: ${truncateText(r.enhancement_model, 80)}`);
|
|
3195
4522
|
}
|
|
3196
4523
|
if (r.duration_ms) {
|
|
3197
4524
|
lines.push(` Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
|
|
3198
4525
|
}
|
|
3199
4526
|
if (r.language) {
|
|
3200
|
-
lines.push(` Language: ${r.language}`);
|
|
4527
|
+
lines.push(` Language: ${truncateText(r.language, 20)}`);
|
|
3201
4528
|
}
|
|
3202
4529
|
if (r.audio_path) {
|
|
3203
|
-
lines.push(` Audio: ${r.audio_path}`);
|
|
4530
|
+
lines.push(` Audio: ${truncatePath(r.audio_path, 240)}`);
|
|
3204
4531
|
}
|
|
3205
4532
|
if (r.tags.length > 0) {
|
|
3206
|
-
lines.push(` Tags: ${r.tags.join(", ")}`);
|
|
4533
|
+
lines.push(` Tags: ${r.tags.map((tag) => truncateText(tag, 80)).join(", ")}`);
|
|
3207
4534
|
}
|
|
3208
|
-
lines.push(` Created: ${r.created_at}`);
|
|
4535
|
+
lines.push(` Created: ${truncateText(r.created_at, 40)}`);
|
|
3209
4536
|
lines.push("");
|
|
3210
4537
|
lines.push(chalk.bold("Raw text:"));
|
|
3211
|
-
lines.push(r.raw_text);
|
|
4538
|
+
lines.push(stripTerminalControls(r.raw_text));
|
|
3212
4539
|
if (r.processed_text && r.processed_text !== r.raw_text) {
|
|
3213
4540
|
lines.push("");
|
|
3214
4541
|
lines.push(chalk.bold("Enhanced text:"));
|
|
3215
|
-
lines.push(r.processed_text);
|
|
4542
|
+
lines.push(stripTerminalControls(r.processed_text));
|
|
3216
4543
|
}
|
|
3217
4544
|
return lines.join(`
|
|
3218
4545
|
`);
|
|
3219
4546
|
}
|
|
4547
|
+
function truncateText(value, max) {
|
|
4548
|
+
const normalized = sanitizeInline(value);
|
|
4549
|
+
const prefix = [];
|
|
4550
|
+
for (const point of normalized) {
|
|
4551
|
+
if (prefix.length === max) {
|
|
4552
|
+
return `${prefix.slice(0, Math.max(0, max - 3)).join("")}...`;
|
|
4553
|
+
}
|
|
4554
|
+
prefix.push(point);
|
|
4555
|
+
}
|
|
4556
|
+
return normalized;
|
|
4557
|
+
}
|
|
4558
|
+
function stripTerminalControls(value) {
|
|
4559
|
+
return value.replace(/(?:\u001b\]|\u009d)[\s\S]*?(?:\u0007|\u001b\\|\u009c)/g, "").replace(/(?:\u001b[PX^_]|\u0090|\u0098|\u009e|\u009f)[\s\S]*?(?:\u001b\\|\u009c)/g, "").replace(/(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]/g, "").replace(/\u001b[@-_]/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "");
|
|
4560
|
+
}
|
|
4561
|
+
function sanitizeInline(value) {
|
|
4562
|
+
return stripTerminalControls(value).replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
4563
|
+
}
|
|
4564
|
+
function summarizeTags(tags) {
|
|
4565
|
+
const shown = tags.slice(0, 3).map((tag) => truncateText(tag, 20));
|
|
4566
|
+
if (tags.length > shown.length)
|
|
4567
|
+
shown.push(`+${tags.length - shown.length}`);
|
|
4568
|
+
return shown.join(", ");
|
|
4569
|
+
}
|
|
4570
|
+
function truncatePath(value, max) {
|
|
4571
|
+
const normalized = sanitizeInline(value);
|
|
4572
|
+
const keep = Math.max(8, max - 15);
|
|
4573
|
+
const tail = [];
|
|
4574
|
+
let length = 0;
|
|
4575
|
+
for (const point of normalized) {
|
|
4576
|
+
length += 1;
|
|
4577
|
+
if (tail.length === keep)
|
|
4578
|
+
tail.shift();
|
|
4579
|
+
tail.push(point);
|
|
4580
|
+
}
|
|
4581
|
+
return length <= max ? normalized : `...${tail.join("")}`;
|
|
4582
|
+
}
|
|
4583
|
+
function relativeHint(value) {
|
|
4584
|
+
const time = Date.parse(value);
|
|
4585
|
+
if (!Number.isFinite(time))
|
|
4586
|
+
return value;
|
|
4587
|
+
const seconds = Math.max(0, Math.floor((Date.now() - time) / 1000));
|
|
4588
|
+
if (seconds < 60)
|
|
4589
|
+
return `${seconds}s ago`;
|
|
4590
|
+
const minutes = Math.floor(seconds / 60);
|
|
4591
|
+
if (minutes < 60)
|
|
4592
|
+
return `${minutes}m ago`;
|
|
4593
|
+
const hours = Math.floor(minutes / 60);
|
|
4594
|
+
if (hours < 24)
|
|
4595
|
+
return `${hours}h ago`;
|
|
4596
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
4597
|
+
}
|
|
3220
4598
|
program.command("mcp").description("Install recordings MCP server into Claude Code, Codex, or Gemini").option("--claude", "Install into Claude Code (via `claude mcp add`)").option("--codex", "Install into Codex (~/.codex/config.toml)").option("--gemini", "Install into Gemini (~/.gemini/settings.json)").option("--all", "Install into all supported agents").option("--uninstall", "Remove recordings MCP from config").action(async (opts) => {
|
|
3221
|
-
const { readFileSync:
|
|
4599
|
+
const { readFileSync: readFileSync4, writeFileSync: writeFileSync3, existsSync: fileExists } = __require("fs");
|
|
3222
4600
|
const { join: pathJoin2 } = __require("path");
|
|
3223
4601
|
const { homedir: getHome } = __require("os");
|
|
3224
4602
|
const { execSync } = __require("child_process");
|
|
@@ -3244,21 +4622,21 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
|
|
|
3244
4622
|
try {
|
|
3245
4623
|
execSync("claude mcp remove recordings", { stdio: "pipe" });
|
|
3246
4624
|
} catch {}
|
|
3247
|
-
execSync(`claude mcp add --transport stdio --scope user recordings -- ${mcpCmd}`, { stdio: "pipe" });
|
|
4625
|
+
execSync(`claude mcp add --transport stdio --scope user recordings -- ${mcpCmd} --stdio`, { stdio: "pipe" });
|
|
3248
4626
|
}
|
|
3249
4627
|
console.log(chalk.green(`${action} Claude Code (user scope in ~/.claude.json)`));
|
|
3250
4628
|
}
|
|
3251
4629
|
if (target === "codex") {
|
|
3252
4630
|
const configPath = pathJoin2(home, ".codex", "config.toml");
|
|
3253
4631
|
if (fileExists(configPath)) {
|
|
3254
|
-
const content =
|
|
4632
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
3255
4633
|
if (opts.uninstall) {
|
|
3256
4634
|
const { content: next, removed } = removeCodexServerBlock(content, "recordings");
|
|
3257
|
-
|
|
4635
|
+
writeFileSync3(configPath, next, "utf-8");
|
|
3258
4636
|
console.log(removed ? chalk.green(`Removed from Codex: ${configPath}`) : chalk.yellow(`Codex: no recordings MCP block found in ${configPath}`));
|
|
3259
4637
|
} else {
|
|
3260
4638
|
const next = upsertCodexStdioBlock(content, "recordings", mcpCmd);
|
|
3261
|
-
|
|
4639
|
+
writeFileSync3(configPath, next, "utf-8");
|
|
3262
4640
|
console.log(chalk.green(`Installed into Codex: ${configPath}`));
|
|
3263
4641
|
}
|
|
3264
4642
|
} else {
|
|
@@ -3269,16 +4647,16 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
|
|
|
3269
4647
|
const configPath = pathJoin2(home, ".gemini", "settings.json");
|
|
3270
4648
|
let config = {};
|
|
3271
4649
|
if (fileExists(configPath)) {
|
|
3272
|
-
config = JSON.parse(
|
|
4650
|
+
config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
3273
4651
|
}
|
|
3274
4652
|
const servers = config["mcpServers"] || {};
|
|
3275
4653
|
if (opts.uninstall) {
|
|
3276
4654
|
delete servers["recordings"];
|
|
3277
4655
|
} else {
|
|
3278
|
-
servers["recordings"] = { command: mcpCmd, args: [] };
|
|
4656
|
+
servers["recordings"] = { command: mcpCmd, args: ["--stdio"] };
|
|
3279
4657
|
}
|
|
3280
4658
|
config["mcpServers"] = servers;
|
|
3281
|
-
|
|
4659
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2) + `
|
|
3282
4660
|
`, "utf-8");
|
|
3283
4661
|
console.log(chalk.green(`${action} Gemini: ${configPath}`));
|
|
3284
4662
|
}
|
|
@@ -3308,13 +4686,14 @@ program.command("feedback <message>").description("Send feedback").option("--ema
|
|
|
3308
4686
|
function getMacOSAppStatus() {
|
|
3309
4687
|
const packageRoot = findPackageRoot();
|
|
3310
4688
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3311
|
-
const installedAppPath = pathJoin(home, "
|
|
4689
|
+
const installedAppPath = pathJoin(home, "Applications", "Recordings.app");
|
|
3312
4690
|
const executablePath = pathJoin(installedAppPath, "Contents", "MacOS", "Recordings");
|
|
3313
4691
|
const logPath = pathJoin(home, ".hasna", "recordings", "Recordings.log");
|
|
3314
|
-
const installerPath =
|
|
4692
|
+
const installerPath = getMacOSInstallerPath(packageRoot);
|
|
3315
4693
|
const nativeSourcesPath = pathJoin(packageRoot, "src", "native", "Recordings");
|
|
3316
4694
|
const signingInfo = getCodeSigningInfo(installedAppPath);
|
|
3317
|
-
const
|
|
4695
|
+
const legacyInstallPaths = findLegacyMacOSAppPaths(home, installedAppPath);
|
|
4696
|
+
const permissionStatus = legacyInstallPaths.length > 0 ? "ambiguous_multiple_installations" : null;
|
|
3318
4697
|
return {
|
|
3319
4698
|
platform: process.platform,
|
|
3320
4699
|
package_root: packageRoot,
|
|
@@ -3323,20 +4702,40 @@ function getMacOSAppStatus() {
|
|
|
3323
4702
|
native_sources_path: nativeSourcesPath,
|
|
3324
4703
|
native_sources_available: existsSync4(pathJoin(nativeSourcesPath, "Package.swift")),
|
|
3325
4704
|
installed_app_path: installedAppPath,
|
|
4705
|
+
legacy_install_paths: legacyInstallPaths,
|
|
3326
4706
|
installed: existsSync4(installedAppPath),
|
|
3327
4707
|
executable_path: executablePath,
|
|
3328
4708
|
executable: existsSync4(executablePath),
|
|
3329
4709
|
app_code_hash: signingInfo.cdHash,
|
|
3330
4710
|
ad_hoc_signed: signingInfo.adHoc,
|
|
3331
|
-
|
|
3332
|
-
|
|
4711
|
+
signing_identifier: signingInfo.identifier,
|
|
4712
|
+
team_identifier: signingInfo.teamIdentifier,
|
|
4713
|
+
designated_requirement: signingInfo.designatedRequirement,
|
|
4714
|
+
signature_authorities: signingInfo.authorities,
|
|
4715
|
+
microphone_permission: permissionStatus ?? getTccPermission("kTCCServiceMicrophone", home),
|
|
4716
|
+
accessibility_permission: permissionStatus ?? getTccPermission("kTCCServiceAccessibility", home),
|
|
3333
4717
|
log_path: logPath
|
|
3334
4718
|
};
|
|
3335
4719
|
}
|
|
4720
|
+
function findLegacyMacOSAppPaths(home, canonicalPath) {
|
|
4721
|
+
const candidates = [
|
|
4722
|
+
pathJoin(home, ".hasna", "recordings", "Recordings.app"),
|
|
4723
|
+
pathJoin("/", "Applications", "Recordings.app")
|
|
4724
|
+
];
|
|
4725
|
+
const userApplications = pathJoin(home, "Applications");
|
|
4726
|
+
if (existsSync4(userApplications)) {
|
|
4727
|
+
for (const entry of readdirSync2(userApplications, { withFileTypes: true })) {
|
|
4728
|
+
if (entry.isDirectory() && entry.name.startsWith("Recordings.app.")) {
|
|
4729
|
+
candidates.push(pathJoin(userApplications, entry.name));
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
return [...new Set(candidates)].filter((candidate) => candidate !== canonicalPath && existsSync4(candidate)).sort();
|
|
4734
|
+
}
|
|
3336
4735
|
function resetMacOSPermissions() {
|
|
3337
4736
|
const services = ["Microphone", "Accessibility"];
|
|
3338
4737
|
for (const service of services) {
|
|
3339
|
-
const result =
|
|
4738
|
+
const result = spawnSync4("tccutil", ["reset", service, "com.hasna.recordings"], {
|
|
3340
4739
|
stdio: "inherit"
|
|
3341
4740
|
});
|
|
3342
4741
|
if (result.error) {
|
|
@@ -3347,9 +4746,16 @@ function resetMacOSPermissions() {
|
|
|
3347
4746
|
}
|
|
3348
4747
|
function getCodeSigningInfo(appPath) {
|
|
3349
4748
|
if (process.platform !== "darwin" || !existsSync4(appPath)) {
|
|
3350
|
-
return {
|
|
4749
|
+
return {
|
|
4750
|
+
cdHash: null,
|
|
4751
|
+
adHoc: false,
|
|
4752
|
+
identifier: null,
|
|
4753
|
+
teamIdentifier: null,
|
|
4754
|
+
designatedRequirement: null,
|
|
4755
|
+
authorities: []
|
|
4756
|
+
};
|
|
3351
4757
|
}
|
|
3352
|
-
const result =
|
|
4758
|
+
const result = spawnSync4("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", appPath], {
|
|
3353
4759
|
encoding: "utf8",
|
|
3354
4760
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3355
4761
|
});
|
|
@@ -3357,32 +4763,31 @@ function getCodeSigningInfo(appPath) {
|
|
|
3357
4763
|
${result.stderr}`;
|
|
3358
4764
|
const cdHash = output.match(/^CDHash=([a-fA-F0-9]+)/m)?.[1]?.toLowerCase() ?? null;
|
|
3359
4765
|
const adHoc = /Signature=adhoc/.test(output);
|
|
3360
|
-
|
|
4766
|
+
const identifier = output.match(/^Identifier=(.+)$/m)?.[1]?.trim() ?? null;
|
|
4767
|
+
const teamIdentifier = output.match(/^TeamIdentifier=(.+)$/m)?.[1]?.trim() ?? null;
|
|
4768
|
+
const designatedRequirement = output.match(/^designated => (.+)$/m)?.[1]?.trim() ?? null;
|
|
4769
|
+
const authorities = [...output.matchAll(/^Authority=(.+)$/gm)].map((match) => match[1].trim());
|
|
4770
|
+
return { cdHash, adHoc, identifier, teamIdentifier, designatedRequirement, authorities };
|
|
3361
4771
|
}
|
|
3362
|
-
function getTccPermission(service, home
|
|
4772
|
+
function getTccPermission(service, home) {
|
|
3363
4773
|
if (process.platform !== "darwin")
|
|
3364
4774
|
return "unsupported";
|
|
3365
4775
|
const dbPaths = [
|
|
3366
4776
|
pathJoin(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
|
|
3367
4777
|
pathJoin("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
|
|
3368
4778
|
];
|
|
3369
|
-
const sql = "select auth_value
|
|
4779
|
+
const sql = "select auth_value from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
|
|
3370
4780
|
for (const dbPath of dbPaths) {
|
|
3371
4781
|
if (!existsSync4(dbPath))
|
|
3372
4782
|
continue;
|
|
3373
|
-
const result =
|
|
4783
|
+
const result = spawnSync4("/usr/bin/sqlite3", [dbPath, sql], {
|
|
3374
4784
|
encoding: "utf8",
|
|
3375
4785
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3376
4786
|
});
|
|
3377
4787
|
const value = result.stdout.trim();
|
|
3378
4788
|
if (!value)
|
|
3379
4789
|
continue;
|
|
3380
|
-
|
|
3381
|
-
const label = tccAuthValueLabel(authValue ?? "");
|
|
3382
|
-
if (label === "allowed" && currentCodeHash && csreqHex && !csreqHex.toLowerCase().includes(currentCodeHash.toLowerCase())) {
|
|
3383
|
-
return "stale_allowed_for_previous_app_build";
|
|
3384
|
-
}
|
|
3385
|
-
return label;
|
|
4790
|
+
return `${tccAuthValueLabel(value)}_identity_unverified`;
|
|
3386
4791
|
}
|
|
3387
4792
|
return "not_determined";
|
|
3388
4793
|
}
|
|
@@ -3401,24 +4806,27 @@ function tccAuthValueLabel(value) {
|
|
|
3401
4806
|
}
|
|
3402
4807
|
}
|
|
3403
4808
|
function findPackageRoot() {
|
|
3404
|
-
let current =
|
|
4809
|
+
let current = dirname4(fileURLToPath(import.meta.url));
|
|
3405
4810
|
while (true) {
|
|
3406
4811
|
const packagePath = pathJoin(current, "package.json");
|
|
3407
4812
|
if (existsSync4(packagePath)) {
|
|
3408
4813
|
try {
|
|
3409
|
-
const pkg = JSON.parse(
|
|
4814
|
+
const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
|
|
3410
4815
|
if (pkg.name === "@hasna/recordings") {
|
|
3411
4816
|
return current;
|
|
3412
4817
|
}
|
|
3413
4818
|
} catch {}
|
|
3414
4819
|
}
|
|
3415
|
-
const parent =
|
|
4820
|
+
const parent = dirname4(current);
|
|
3416
4821
|
if (parent === current) {
|
|
3417
4822
|
return process.cwd();
|
|
3418
4823
|
}
|
|
3419
4824
|
current = parent;
|
|
3420
4825
|
}
|
|
3421
4826
|
}
|
|
4827
|
+
function getMacOSInstallerPath(packageRoot = findPackageRoot()) {
|
|
4828
|
+
return pathJoin(packageRoot, "scripts", "install_macos_app.sh");
|
|
4829
|
+
}
|
|
3422
4830
|
program.parseAsync().catch((error) => {
|
|
3423
4831
|
const msg = error instanceof Error ? error.message : String(error);
|
|
3424
4832
|
console.error(`ERROR: ${msg}`);
|