@hasna/recordings 0.2.10 → 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.
Files changed (167) hide show
  1. package/README.md +350 -35
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1922 -77
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1990 -29
  167. 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 { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
905
- import { dirname as dirname3, join as pathJoin } from "path";
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 join2, resolve } from "path";
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: "gpt-4o-transcribe",
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
- const filePath = configPath || findConfigFile() || join2(getDataDir(), "config.json");
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 = join2(getDataDir(), "recordings.db");
1204
+ config.db_path = join3(getDataDir(), "recordings.db");
990
1205
  }
991
1206
  if (!config.audio_dir) {
992
- config.audio_dir = join2(getDataDir(), "audio");
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 = join2(home, ".hasna", "recordings");
1015
- const oldDir = join2(home, ".recordings");
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
- mergeDirectoryContents(oldDir, newDir);
1019
- } catch {}
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 home = resolve(getHomeDir());
1025
- let dir = resolve(process.cwd());
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 !== home) {
1028
- const candidate = entry ? join2(dir, ".recordings", entry) : join2(dir, ".recordings");
1029
- if (existsSync2(candidate))
1030
- return candidate;
1031
- }
1032
- const parent = dirname(dir);
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
- mkdirSync(targetDir, { recursive: true });
1354
+ mkdirSync2(targetDir, { recursive: true });
1041
1355
  for (const entry of readdirSync(sourceDir)) {
1042
- const sourcePath = join2(sourceDir, entry);
1043
- const targetPath = join2(targetDir, entry);
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 = join2(getHomeDir(), ".secrets");
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 = join2(path, entry);
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"] || homedir2();
1409
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
1096
1410
  }
1097
1411
  function ensureDataDir(config) {
1098
- const { mkdirSync: mkdirSync2 } = __require("fs");
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
- if (dbDir)
1102
- mkdirSync2(dbDir, { recursive: true });
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 mkdirSync2 } from "fs";
1138
- import { dirname as dirname2 } from "path";
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 = dirname2(path);
1225
- mkdirSync2(dir, { recursive: true });
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
- const id = shortUuid();
1308
- const tagsJson = JSON.stringify(input.tags || []);
1309
- const metadataJson = JSON.stringify(input.metadata || {});
1310
- 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)
1311
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).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);
1312
- if (input.tags && input.tags.length > 0) {
1313
- const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
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
- return getRecording(id, d);
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
- const limit = filter?.limit || 50;
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.10";
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 res = await client.create("recordings", input);
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 join3 } from "path";
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 = join3(config.audio_dir, filename);
2510
+ const filepath = join4(config.audio_dir, filename);
2071
2511
  const args = buildRecordArgs(filepath, config);
2072
- _recordProcess = spawn2(args[0], args.slice(1), {
2073
- stdio: ["pipe", "pipe", "pipe"]
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
- _currentFile = filepath;
2084
- _recordProcess.on("error", (err) => {
2085
- _recordProcess = null;
2086
- _currentFile = null;
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
- _recordProcess.on("exit", () => {
2090
- _recordProcess = null;
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 = join3(config.audio_dir, filename);
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 proc = Bun.spawn(args, {
2140
- stdout: "pipe",
2141
- stderr: "pipe"
2142
- });
2143
- const exitCode = await proc.exited;
2144
- if (exitCode !== 0 && !existsSync3(filepath)) {
2145
- const stderr = await new Response(proc.stderr).text();
2146
- throw new RecordingError(`Recording failed (exit ${exitCode}): ${stderr}`);
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 fullPrompt = systemPrompt ? `${basePrompt}
2787
+ const transcriberPrompt = combinePrompts(config.transcriber_prompt, systemPrompt);
2788
+ const fullPrompt = transcriberPrompt ? `${basePrompt}
2325
2789
 
2326
- Additional context:
2327
- ${systemPrompt}` : basePrompt;
2790
+ Transcriber instructions:
2791
+ ${transcriberPrompt}` : basePrompt;
2328
2792
  try {
2329
2793
  const response = await client.chat.completions.create({
2330
- model: config.enhancement_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: config.enhancement_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, systemPrompt, options) {
2357
- const force = options?.force === true;
2358
- if (!force && !config.auto_enhance) {
2359
- return { text: rawText, mode: "raw", enhancement_model: null };
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 = needsEnhancement(rawText, config);
2362
- if (!force && !detection.needs) {
2363
- return { text: rawText, mode: "raw", enhancement_model: null };
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 instruction = detection.needs ? detection.instruction : rawText;
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.enhance === false || opts.noEnhance === false) {
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
- 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("-t, --tags <tags>", "Comma-separated tags").option("-l, --language <lang>", "Language code (e.g. en, es, fr)").action(async (opts) => {
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
- console.log(chalk.blue(`Recording for ${seconds} seconds...`));
3321
+ if (!parentOpts.json) {
3322
+ console.log(chalk.blue(`Recording for ${seconds} seconds...`));
3323
+ }
2445
3324
  audioPath = await recordDuration(seconds, config);
2446
- console.log(chalk.green("Recording complete."));
3325
+ if (!parentOpts.json) {
3326
+ console.log(chalk.green("Recording complete."));
3327
+ }
2447
3328
  } else {
2448
- console.log(chalk.blue("Recording... Press") + chalk.yellow(" Enter ") + chalk.blue("to stop."));
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((resolve2) => {
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
- resolve2();
3339
+ resolve3();
2457
3340
  });
2458
3341
  });
2459
3342
  stopRecording();
2460
- console.log(chalk.green("Recording stopped."));
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
- console.log(chalk.dim(`Raw: ${transcription.text}`));
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>", "System prompt for enhancement context").action(async (file, opts) => {
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, { prompt: opts.prompt });
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, opts.systemPrompt);
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
- if (processed.mode === "enhanced") {
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(chalk.dim(`Saved as ${recording.id.slice(0, 8)}`));
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 recordings = await getStore().listRecordings({
2608
- limit: parseInt(opts.limit, 10),
3569
+ const pagination = resolvePagination(opts, parentOpts);
3570
+ const filter = {
3571
+ limit: pagination.limit,
2609
3572
  processing_mode: opts.mode,
2610
- tags: opts.tags ? opts.tags.split(",") : undefined,
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
- if (recordings.length === 0) {
2622
- console.log(chalk.dim("No recordings found."));
2623
- return;
2624
- }
2625
- console.log(chalk.bold(`${recordings.length} recording(s):
2626
- `));
2627
- for (const r of recordings) {
2628
- console.log(formatRecordingLine(r));
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("search <query>").description("Search recordings by text content").option("-n, --limit <n>", "Max results", "20").action(async (query, opts) => {
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 results = await getStore().searchRecordings(query, {
2647
- limit: parseInt(opts.limit, 10),
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
- if (results.length === 0) {
2656
- console.log(chalk.dim("No results."));
2657
- return;
2658
- }
2659
- console.log(chalk.bold(`${results.length} result(s):
2660
- `));
2661
- for (const r of results) {
2662
- console.log(formatRecordingLine(r));
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
- if (Object.keys(stats.by_model).length > 0) {
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 Object.entries(stats.by_model)) {
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(agents, null, 2));
3668
+ console.log(JSON.stringify(page, null, 2));
2699
3669
  return;
2700
3670
  }
2701
- if (agents.length === 0) {
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
- for (const a of agents) {
2706
- console.log(`${chalk.cyan(a.id)} ${chalk.bold(a.name)} (${a.role}) \u2014 last seen ${a.last_seen_at}`);
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("projects").description("List registered projects").action(async () => {
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(projects, null, 2));
3706
+ console.log(JSON.stringify(page, null, 2));
2714
3707
  return;
2715
3708
  }
2716
- if (projects.length === 0) {
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
- for (const p of projects) {
2721
- console.log(`${chalk.cyan(p.id.slice(0, 8))} ${chalk.bold(p.name)} \u2014 ${p.path}`);
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: mkdirSync3, writeFileSync, existsSync: existsSync5 } = __require("fs");
2726
- const { join: join4 } = __require("path");
2727
- const dir = join4(process.cwd(), ".recordings");
2728
- const audioDir = join4(dir, "audio");
2729
- const configFile = join4(dir, "config.json");
2730
- mkdirSync3(audioDir, { recursive: true });
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
- writeFileSync(configFile, JSON.stringify(defaultConf, null, 2));
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 menu bar app installed from this package");
2746
- appCommand.command("install").description("Build and install Recordings.app from the installed package").option("--mode <mode>", "Swift build mode: debug or release", "release").action((opts) => {
2747
- const status = getMacOSAppStatus();
2748
- if (!status.installer_available) {
2749
- console.error(chalk.red(`App installer missing from package: ${status.installer_path}`));
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 result = spawnSync("bash", [status.installer_path, "--mode", opts.mode], {
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: process.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(`Package: ${status.package_root}`);
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(`Installed app: ${status.installed ? status.installed_app_path : "missing"}`);
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 = spawnSync("open", [
2820
- "-n",
2821
- status.installed_app_path,
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.status ?? 1);
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", "120").action((opts) => {
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) || 120);
2839
- const result = spawnSync("tail", ["-n", String(lines), status.log_path], {
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 = spawnSync("open", [status.installed_app_path], { stdio: "inherit" });
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.enhancement_model})`));
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: mkdirSync3, chmodSync } = __require("fs");
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
- mkdirSync3(scriptDir, { recursive: true });
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
- writeFileSync(scriptPath, script, "utf-8");
3063
- chmodSync(scriptPath, 493);
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
- mkdirSync3(karabinerDir, { recursive: true });
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
- writeFileSync(karabinerPath, JSON.stringify(rule, null, 2) + `
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
- mkdirSync3(raycastDir, { recursive: true });
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
- writeFileSync(raycastPath, raycastScript, "utf-8");
3122
- chmodSync(raycastPath, 493);
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.slice(0, 8));
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).slice(0, 80);
3181
- const date = chalk.dim(r.created_at.slice(0, 16));
3182
- const tags = r.tags.length > 0 ? chalk.yellow(` [${r.tags.join(", ")}]`) : "";
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}${text.length >= 80 ? "..." : ""}`;
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.slice(0, 8)}`),
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: readFileSync3, writeFileSync, existsSync: fileExists } = __require("fs");
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 = readFileSync3(configPath, "utf-8");
4632
+ const content = readFileSync4(configPath, "utf-8");
3255
4633
  if (opts.uninstall) {
3256
4634
  const { content: next, removed } = removeCodexServerBlock(content, "recordings");
3257
- writeFileSync(configPath, next, "utf-8");
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
- writeFileSync(configPath, next, "utf-8");
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(readFileSync3(configPath, "utf-8"));
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
- writeFileSync(configPath, JSON.stringify(config, null, 2) + `
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, ".hasna", "recordings", "Recordings.app");
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 = pathJoin(packageRoot, "scripts", "install_macos_app.sh");
4692
+ const installerPath = getMacOSInstallerPath(packageRoot);
3315
4693
  const nativeSourcesPath = pathJoin(packageRoot, "src", "native", "Recordings");
3316
4694
  const signingInfo = getCodeSigningInfo(installedAppPath);
3317
- const permissionCodeHash = signingInfo.adHoc ? signingInfo.cdHash : null;
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
- microphone_permission: getTccPermission("kTCCServiceMicrophone", home, permissionCodeHash),
3332
- accessibility_permission: getTccPermission("kTCCServiceAccessibility", home, permissionCodeHash),
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 = spawnSync("tccutil", ["reset", service, "com.hasna.recordings"], {
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 { cdHash: null, adHoc: false };
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 = spawnSync("codesign", ["-d", "--verbose=4", appPath], {
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
- return { cdHash, adHoc };
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, currentCodeHash) {
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 || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
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 = spawnSync("sqlite3", [dbPath, sql], {
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
- const [authValue, csreqHex = ""] = value.split("|");
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 = dirname3(fileURLToPath(import.meta.url));
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(readFileSync2(packagePath, "utf8"));
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 = dirname3(current);
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}`);