@hasna/recordings 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +349 -69
  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 +1919 -98
  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 +1973 -236
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
package/dist/mcp/index.js CHANGED
@@ -4039,19 +4039,196 @@ var coerce = {
4039
4039
  };
4040
4040
  var NEVER = INVALID;
4041
4041
  // src/lib/config.ts
4042
- import { copyFileSync, existsSync, readFileSync, mkdirSync, readdirSync, statSync } from "fs";
4043
- import { dirname, join, resolve } from "path";
4042
+ import { copyFileSync, existsSync, readFileSync, mkdirSync as mkdirSync2, readdirSync, realpathSync as realpathSync2, statSync } from "fs";
4043
+ import { dirname as dirname2, join as join2, resolve as resolve2, sep as sep2 } from "path";
4044
+ import { homedir as homedir2 } from "os";
4045
+
4046
+ // src/lib/install-maintenance.ts
4047
+ import { randomUUID } from "crypto";
4048
+ import {
4049
+ chmodSync,
4050
+ lstatSync,
4051
+ mkdirSync,
4052
+ realpathSync,
4053
+ renameSync,
4054
+ rmSync,
4055
+ writeFileSync
4056
+ } from "fs";
4044
4057
  import { homedir } from "os";
4058
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
4059
+ import { spawnSync } from "child_process";
4060
+ var INSTALL_MAINTENANCE_MARKER_NAME = ".recordings-install-maintenance";
4061
+ var STORE_READER_LEASES_NAME = ".recordings-store-readers";
4062
+ function stateParent(env = process.env) {
4063
+ const home = env["HOME"] || env["USERPROFILE"] || homedir();
4064
+ return join(home, ".hasna");
4065
+ }
4066
+ function installMaintenanceMarkerPath(env = process.env) {
4067
+ return join(stateParent(env), INSTALL_MAINTENANCE_MARKER_NAME);
4068
+ }
4069
+ function storeReaderLeasesPath(env = process.env) {
4070
+ return join(stateParent(env), STORE_READER_LEASES_NAME);
4071
+ }
4072
+ function pathExists(path) {
4073
+ try {
4074
+ lstatSync(path);
4075
+ return true;
4076
+ } catch (error) {
4077
+ if (error.code === "ENOENT")
4078
+ return false;
4079
+ throw error;
4080
+ }
4081
+ }
4082
+ function assertPrivateDirectory(path) {
4083
+ const details = lstatSync(path);
4084
+ if (!details.isDirectory() || details.isSymbolicLink()) {
4085
+ throw new Error(`Local Recordings coordination path is not a secure directory: ${path}`);
4086
+ }
4087
+ if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
4088
+ throw new Error(`Local Recordings coordination path has an unexpected owner: ${path}`);
4089
+ }
4090
+ if ((details.mode & 18) !== 0) {
4091
+ throw new Error(`Local Recordings coordination path is group/world writable: ${path}`);
4092
+ }
4093
+ }
4094
+ function ensureReaderRoot(env) {
4095
+ const parent = stateParent(env);
4096
+ const readerRoot = storeReaderLeasesPath(env);
4097
+ mkdirSync(parent, { recursive: true, mode: 448 });
4098
+ assertPrivateDirectory(parent);
4099
+ try {
4100
+ mkdirSync(readerRoot, { mode: 448 });
4101
+ } catch (error) {
4102
+ if (error.code !== "EEXIST")
4103
+ throw error;
4104
+ }
4105
+ assertPrivateDirectory(readerRoot);
4106
+ return readerRoot;
4107
+ }
4108
+ function processStartIdentity() {
4109
+ const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(process.pid)], {
4110
+ encoding: "utf8",
4111
+ env: { ...process.env, LC_ALL: "C", LANG: "C", TZ: "UTC0" }
4112
+ });
4113
+ const identity = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
4114
+ if (!identity) {
4115
+ throw new Error("Could not establish the local Store reader process identity");
4116
+ }
4117
+ return identity;
4118
+ }
4119
+ function maintenanceUnavailable() {
4120
+ return new Error("Local Recordings storage is temporarily unavailable during app installation maintenance");
4121
+ }
4122
+ function assertMaintenanceAbsent(env) {
4123
+ if (pathExists(installMaintenanceMarkerPath(env)))
4124
+ throw maintenanceUnavailable();
4125
+ }
4126
+ function canonicalPotentialPath(path) {
4127
+ const absolutePath = resolve(path);
4128
+ let existingPath = absolutePath;
4129
+ const missingEntries = [];
4130
+ while (true) {
4131
+ try {
4132
+ return join(realpathSync(existingPath), ...missingEntries);
4133
+ } catch (error) {
4134
+ if (error.code !== "ENOENT")
4135
+ return absolutePath;
4136
+ const parent = dirname(existingPath);
4137
+ if (parent === existingPath)
4138
+ return absolutePath;
4139
+ missingEntries.unshift(basename(existingPath));
4140
+ existingPath = parent;
4141
+ }
4142
+ }
4143
+ }
4144
+ function pathIsWithin(root, candidate) {
4145
+ const relativePath = relative(root, candidate);
4146
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
4147
+ }
4148
+ function isGlobalRecordingsStatePath(path, env = process.env) {
4149
+ const globalStateRoot = join(stateParent(env), "recordings");
4150
+ return pathIsWithin(resolve(globalStateRoot), resolve(path)) || pathIsWithin(canonicalPotentialPath(globalStateRoot), canonicalPotentialPath(path));
4151
+ }
4152
+ function removeActiveLease(activeLease, releasedLease) {
4153
+ try {
4154
+ renameSync(activeLease, releasedLease);
4155
+ } catch (error) {
4156
+ if (error.code !== "ENOENT")
4157
+ throw error;
4158
+ }
4159
+ rmSync(releasedLease, { recursive: true, force: true });
4160
+ }
4161
+ function acquireLocalStoreReaderLease(env = process.env) {
4162
+ assertMaintenanceAbsent(env);
4163
+ const readerRoot = ensureReaderRoot(env);
4164
+ const nonce = randomUUID();
4165
+ const pendingLease = join(readerRoot, `.pending-${process.pid}-${nonce}`);
4166
+ const activeLease = join(readerRoot, `lease-${process.pid}-${nonce}`);
4167
+ const releasedLease = join(readerRoot, `.released-${process.pid}-${nonce}`);
4168
+ let active = false;
4169
+ try {
4170
+ mkdirSync(pendingLease, { mode: 448 });
4171
+ const owner = join(pendingLease, "owner");
4172
+ writeFileSync(owner, `${process.pid}
4173
+ ${processStartIdentity()}
4174
+ `, {
4175
+ encoding: "utf8",
4176
+ flag: "wx",
4177
+ mode: 384
4178
+ });
4179
+ chmodSync(owner, 384);
4180
+ renameSync(pendingLease, activeLease);
4181
+ active = true;
4182
+ assertMaintenanceAbsent(env);
4183
+ } catch (error) {
4184
+ if (active)
4185
+ removeActiveLease(activeLease, releasedLease);
4186
+ else
4187
+ rmSync(pendingLease, { recursive: true, force: true });
4188
+ throw error;
4189
+ }
4190
+ let released = false;
4191
+ return () => {
4192
+ if (released)
4193
+ return;
4194
+ removeActiveLease(activeLease, releasedLease);
4195
+ released = true;
4196
+ };
4197
+ }
4198
+ async function withLocalStoreReaderLease(operation, env = process.env) {
4199
+ const release = acquireLocalStoreReaderLease(env);
4200
+ try {
4201
+ return await operation();
4202
+ } finally {
4203
+ release();
4204
+ }
4205
+ }
4206
+
4207
+ // src/lib/config.ts
4208
+ var POST_PROCESSING_MODES = new Set([
4209
+ "off",
4210
+ "auto",
4211
+ "always"
4212
+ ]);
4213
+ var DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
4214
+ var DEFAULT_REALTIME_SESSION_MODEL = "gpt-realtime";
4215
+ var DEFAULT_REALTIME_TRANSCRIPTION_MODEL = "gpt-realtime-whisper";
4045
4216
  var DEFAULT_CONFIG = {
4046
4217
  openai_api_key: "",
4047
4218
  enhancement_api_key: "",
4048
- transcription_model: "gpt-4o-transcribe",
4219
+ transcription_model: DEFAULT_TRANSCRIPTION_MODEL,
4220
+ realtime_session_model: DEFAULT_REALTIME_SESSION_MODEL,
4221
+ realtime_transcription_model: DEFAULT_REALTIME_TRANSCRIPTION_MODEL,
4049
4222
  enhancement_model: "gpt-4o",
4223
+ transcriber_model: "gpt-4o",
4050
4224
  language: "en",
4051
4225
  audio_format: "wav",
4052
4226
  sample_rate: 16000,
4053
4227
  record_command: "sox",
4054
4228
  hotkey: "space",
4229
+ transcription_prompt: "",
4230
+ transcriber_prompt: "",
4231
+ post_processing_mode: "auto",
4055
4232
  auto_enhance: true,
4056
4233
  enhance_triggers: [
4057
4234
  "say it better",
@@ -4068,17 +4245,23 @@ var DEFAULT_CONFIG = {
4068
4245
  keyword_transforms: {},
4069
4246
  db_path: "",
4070
4247
  audio_dir: "",
4071
- max_recording_seconds: 1800
4248
+ max_recording_seconds: 1800,
4249
+ config_warnings: []
4072
4250
  };
4073
4251
  function loadConfig(configPath) {
4074
4252
  const config = { ...DEFAULT_CONFIG };
4075
- const filePath = configPath || findConfigFile() || join(getDataDir(), "config.json");
4253
+ config.config_warnings = [];
4254
+ let explicitPostProcessingMode = false;
4255
+ let explicitTranscriberModel = false;
4256
+ const filePath = configPath || findConfigFile() || join2(getDataDir(), "config.json");
4076
4257
  let fileProvidedOpenAIKey = false;
4077
4258
  if (existsSync(filePath)) {
4078
4259
  try {
4079
4260
  const raw = readFileSync(filePath, "utf-8");
4080
4261
  const fileConfig = JSON.parse(raw);
4081
4262
  const expanded = expandEnvBackedConfig(fileConfig);
4263
+ explicitPostProcessingMode = typeof fileConfig.post_processing_mode === "string";
4264
+ explicitTranscriberModel = typeof fileConfig.transcriber_model === "string";
4082
4265
  Object.assign(config, expanded);
4083
4266
  fileProvidedOpenAIKey = typeof expanded.openai_api_key === "string" && expanded.openai_api_key.length > 0;
4084
4267
  } catch {}
@@ -4095,12 +4278,35 @@ function loadConfig(configPath) {
4095
4278
  if (process.env.RECORDINGS_MODEL) {
4096
4279
  config.transcription_model = process.env.RECORDINGS_MODEL;
4097
4280
  }
4281
+ if (process.env.RECORDINGS_REALTIME_SESSION_MODEL) {
4282
+ config.realtime_session_model = process.env.RECORDINGS_REALTIME_SESSION_MODEL;
4283
+ }
4284
+ if (process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL) {
4285
+ config.realtime_transcription_model = process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL;
4286
+ }
4098
4287
  if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
4099
4288
  config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
4100
4289
  }
4290
+ if (process.env.RECORDINGS_TRANSCRIBER_MODEL) {
4291
+ config.transcriber_model = process.env.RECORDINGS_TRANSCRIBER_MODEL;
4292
+ explicitTranscriberModel = true;
4293
+ }
4101
4294
  if (process.env.RECORDINGS_LANGUAGE) {
4102
4295
  config.language = process.env.RECORDINGS_LANGUAGE;
4103
4296
  }
4297
+ if (process.env.RECORDINGS_TRANSCRIPTION_PROMPT) {
4298
+ config.transcription_prompt = process.env.RECORDINGS_TRANSCRIPTION_PROMPT;
4299
+ }
4300
+ if (process.env.RECORDINGS_TRANSCRIBER_PROMPT) {
4301
+ config.transcriber_prompt = process.env.RECORDINGS_TRANSCRIBER_PROMPT;
4302
+ }
4303
+ if (process.env.RECORDINGS_POST_PROCESSING_MODE) {
4304
+ config.post_processing_mode = normalizePostProcessingMode(process.env.RECORDINGS_POST_PROCESSING_MODE, config.post_processing_mode ?? "auto");
4305
+ explicitPostProcessingMode = true;
4306
+ }
4307
+ if (process.env.RECORDINGS_AUTO_ENHANCE) {
4308
+ config.auto_enhance = parseBooleanEnv(process.env.RECORDINGS_AUTO_ENHANCE, config.auto_enhance);
4309
+ }
4104
4310
  if (process.env.HASNA_RECORDINGS_DB_PATH) {
4105
4311
  config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
4106
4312
  } else if (process.env.RECORDINGS_DB_PATH) {
@@ -4118,14 +4324,86 @@ function loadConfig(configPath) {
4118
4324
  if (!config.enhancement_api_key) {
4119
4325
  config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
4120
4326
  }
4327
+ if (!explicitTranscriberModel) {
4328
+ config.transcriber_model = config.enhancement_model;
4329
+ }
4330
+ normalizeModelSlots(config);
4331
+ normalizePostProcessingConfig(config, explicitPostProcessingMode);
4121
4332
  if (!config.db_path) {
4122
- config.db_path = join(getDataDir(), "recordings.db");
4333
+ config.db_path = join2(getDataDir(), "recordings.db");
4123
4334
  }
4124
4335
  if (!config.audio_dir) {
4125
- config.audio_dir = join(getDataDir(), "audio");
4336
+ config.audio_dir = join2(getDataDir(), "audio");
4337
+ }
4338
+ return config;
4339
+ }
4340
+ function normalizeModelSlots(config) {
4341
+ const warnings = config.config_warnings ?? [];
4342
+ config.config_warnings = warnings;
4343
+ const boundedModel = config.transcription_model?.trim() || DEFAULT_TRANSCRIPTION_MODEL;
4344
+ if (isRealtimeOnlyModel(boundedModel)) {
4345
+ warnings.push(`Ignoring RECORDINGS_MODEL=${boundedModel}; bounded transcription uses ${DEFAULT_TRANSCRIPTION_MODEL}.`);
4346
+ config.transcription_model = DEFAULT_TRANSCRIPTION_MODEL;
4347
+ } else {
4348
+ config.transcription_model = boundedModel;
4349
+ }
4350
+ const realtimeSessionModel = config.realtime_session_model?.trim() || DEFAULT_REALTIME_SESSION_MODEL;
4351
+ if (isTranscriptionOnlyModel(realtimeSessionModel)) {
4352
+ warnings.push(`Ignoring realtime session model ${realtimeSessionModel}; use ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL} as realtime_transcription_model instead.`);
4353
+ config.realtime_session_model = DEFAULT_REALTIME_SESSION_MODEL;
4354
+ } else {
4355
+ config.realtime_session_model = realtimeSessionModel;
4356
+ }
4357
+ const realtimeTranscriptionModel = config.realtime_transcription_model?.trim() || DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
4358
+ if (!isRealtimeTranscriptionModel(realtimeTranscriptionModel)) {
4359
+ warnings.push(`Ignoring realtime transcription model ${realtimeTranscriptionModel}; realtime transcription uses ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL}.`);
4360
+ config.realtime_transcription_model = DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
4361
+ } else {
4362
+ config.realtime_transcription_model = realtimeTranscriptionModel;
4363
+ }
4364
+ return config;
4365
+ }
4366
+ function isTranscriptionOnlyModel(model) {
4367
+ const m = model.trim().toLowerCase();
4368
+ return m === "whisper-1" || m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.includes("transcribe");
4369
+ }
4370
+ function isRealtimeOnlyModel(model) {
4371
+ const m = model.trim().toLowerCase();
4372
+ return m.startsWith("gpt-realtime");
4373
+ }
4374
+ function isRealtimeTranscriptionModel(model) {
4375
+ const m = model.trim().toLowerCase();
4376
+ return m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.startsWith("gpt-realtime") && m.includes("whisper");
4377
+ }
4378
+ function normalizePostProcessingMode(value, fallback = "auto") {
4379
+ const mode = value?.trim().toLowerCase();
4380
+ if (mode && POST_PROCESSING_MODES.has(mode)) {
4381
+ return mode;
4382
+ }
4383
+ return fallback;
4384
+ }
4385
+ function normalizePostProcessingConfig(config, preferPostProcessingMode = true) {
4386
+ if (preferPostProcessingMode) {
4387
+ config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
4388
+ config.auto_enhance = config.post_processing_mode !== "off";
4389
+ return config;
4390
+ }
4391
+ if (config.auto_enhance === false) {
4392
+ config.post_processing_mode = "off";
4393
+ } else {
4394
+ config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
4126
4395
  }
4396
+ config.auto_enhance = config.post_processing_mode !== "off";
4127
4397
  return config;
4128
4398
  }
4399
+ function parseBooleanEnv(value, fallback) {
4400
+ const normalized = value.trim().toLowerCase();
4401
+ if (["1", "true", "yes", "on"].includes(normalized))
4402
+ return true;
4403
+ if (["0", "false", "no", "off"].includes(normalized))
4404
+ return false;
4405
+ return fallback;
4406
+ }
4129
4407
  function expandEnvBackedConfig(config) {
4130
4408
  const expanded = { ...config };
4131
4409
  for (const key of ["openai_api_key", "enhancement_api_key"]) {
@@ -4144,36 +4422,68 @@ function getDataDir() {
4144
4422
  if (projectLocalDir)
4145
4423
  return projectLocalDir;
4146
4424
  const home = getHomeDir();
4147
- const newDir = join(home, ".hasna", "recordings");
4148
- const oldDir = join(home, ".recordings");
4425
+ const newDir = join2(home, ".hasna", "recordings");
4426
+ const oldDir = join2(home, ".recordings");
4149
4427
  if (existsSync(oldDir)) {
4428
+ const releaseLease = acquireLocalStoreReaderLease();
4150
4429
  try {
4151
- mergeDirectoryContents(oldDir, newDir);
4152
- } catch {}
4430
+ try {
4431
+ if (existsSync(oldDir))
4432
+ mergeDirectoryContents(oldDir, newDir);
4433
+ } catch {}
4434
+ } finally {
4435
+ releaseLease();
4436
+ }
4153
4437
  }
4154
4438
  return newDir;
4155
4439
  }
4156
4440
  function findProjectRecordingsPath(entry) {
4157
- const home = resolve(getHomeDir());
4158
- let dir = resolve(process.cwd());
4441
+ const cwd = canonicalExistingPath(process.cwd());
4442
+ const home = canonicalExistingPath(getHomeDir());
4443
+ const repositoryRoot = findRepositoryRoot(cwd);
4444
+ const cwdIsInsideHome = cwd === home || cwd.startsWith(`${home}${sep2}`);
4445
+ const repositoryIsInsideHome = repositoryRoot !== null && repositoryRoot !== home && repositoryRoot.startsWith(`${home}${sep2}`);
4446
+ const boundary = cwdIsInsideHome ? repositoryIsInsideHome ? repositoryRoot : home : repositoryRoot ?? cwd;
4447
+ const excludeBoundary = boundary === home;
4448
+ let dir = cwd;
4159
4449
  while (true) {
4160
- if (dir !== home) {
4161
- const candidate = entry ? join(dir, ".recordings", entry) : join(dir, ".recordings");
4162
- if (existsSync(candidate))
4163
- return candidate;
4164
- }
4165
- const parent = dirname(dir);
4450
+ if (excludeBoundary && dir === boundary)
4451
+ break;
4452
+ const candidate = entry ? join2(dir, ".recordings", entry) : join2(dir, ".recordings");
4453
+ if (existsSync(candidate))
4454
+ return candidate;
4455
+ if (dir === boundary)
4456
+ break;
4457
+ const parent = dirname2(dir);
4166
4458
  if (parent === dir)
4167
4459
  break;
4168
4460
  dir = parent;
4169
4461
  }
4170
4462
  return null;
4171
4463
  }
4464
+ function findRepositoryRoot(start) {
4465
+ let dir = start;
4466
+ while (true) {
4467
+ if (existsSync(join2(dir, ".git")))
4468
+ return dir;
4469
+ const parent = dirname2(dir);
4470
+ if (parent === dir)
4471
+ return null;
4472
+ dir = parent;
4473
+ }
4474
+ }
4475
+ function canonicalExistingPath(path) {
4476
+ try {
4477
+ return realpathSync2(path);
4478
+ } catch {
4479
+ return resolve2(path);
4480
+ }
4481
+ }
4172
4482
  function mergeDirectoryContents(sourceDir, targetDir) {
4173
- mkdirSync(targetDir, { recursive: true });
4483
+ mkdirSync2(targetDir, { recursive: true });
4174
4484
  for (const entry of readdirSync(sourceDir)) {
4175
- const sourcePath = join(sourceDir, entry);
4176
- const targetPath = join(targetDir, entry);
4485
+ const sourcePath = join2(sourceDir, entry);
4486
+ const targetPath = join2(targetDir, entry);
4177
4487
  const sourceStats = statSync(sourcePath);
4178
4488
  if (sourceStats.isDirectory()) {
4179
4489
  mergeDirectoryContents(sourcePath, targetPath);
@@ -4183,7 +4493,7 @@ function mergeDirectoryContents(sourceDir, targetDir) {
4183
4493
  }
4184
4494
  }
4185
4495
  function loadSecretKey(keyName) {
4186
- const secretsPath = join(getHomeDir(), ".secrets");
4496
+ const secretsPath = join2(getHomeDir(), ".secrets");
4187
4497
  if (!existsSync(secretsPath))
4188
4498
  return "";
4189
4499
  for (const candidate of listSecretFiles(secretsPath)) {
@@ -4210,7 +4520,7 @@ function listSecretFiles(path) {
4210
4520
  if (!stats.isDirectory())
4211
4521
  return [];
4212
4522
  return readdirSync(path).sort().flatMap((entry) => {
4213
- const child = join(path, entry);
4523
+ const child = join2(path, entry);
4214
4524
  try {
4215
4525
  const childStats = statSync(child);
4216
4526
  if (childStats.isDirectory())
@@ -4225,14 +4535,19 @@ function listSecretFiles(path) {
4225
4535
  }
4226
4536
  }
4227
4537
  function getHomeDir() {
4228
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
4538
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
4229
4539
  }
4230
4540
  function ensureDataDir(config) {
4231
- const { mkdirSync: mkdirSync2 } = __require("fs");
4232
- mkdirSync2(config.audio_dir, { recursive: true });
4541
+ const { mkdirSync: mkdirSync3 } = __require("fs");
4233
4542
  const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
4234
- if (dbDir)
4235
- mkdirSync2(dbDir, { recursive: true });
4543
+ const releaseLease = [config.audio_dir, dbDir].some((path) => path.length > 0 && isGlobalRecordingsStatePath(path)) ? acquireLocalStoreReaderLease() : () => {};
4544
+ try {
4545
+ mkdirSync3(config.audio_dir, { recursive: true });
4546
+ if (dbDir)
4547
+ mkdirSync3(dbDir, { recursive: true });
4548
+ } finally {
4549
+ releaseLease();
4550
+ }
4236
4551
  }
4237
4552
 
4238
4553
  // src/db/sqlite-adapter.ts
@@ -4267,8 +4582,8 @@ class SqliteAdapter {
4267
4582
  }
4268
4583
 
4269
4584
  // src/db/database.ts
4270
- import { mkdirSync as mkdirSync2 } from "fs";
4271
- import { dirname as dirname2 } from "path";
4585
+ import { mkdirSync as mkdirSync3 } from "fs";
4586
+ import { dirname as dirname3 } from "path";
4272
4587
  var _db = null;
4273
4588
  var _adapter = null;
4274
4589
  var MIGRATIONS = [
@@ -4354,8 +4669,8 @@ function getDatabase(dbPath) {
4354
4669
  if (_db)
4355
4670
  return _db;
4356
4671
  const path = dbPath || loadConfig().db_path;
4357
- const dir = dirname2(path);
4358
- mkdirSync2(dir, { recursive: true });
4672
+ const dir = dirname3(path);
4673
+ mkdirSync3(dir, { recursive: true });
4359
4674
  _adapter = new SqliteAdapter(path);
4360
4675
  _db = _adapter.raw;
4361
4676
  _db.run("PRAGMA busy_timeout = 5000");
@@ -4411,6 +4726,63 @@ function shortUuid() {
4411
4726
  return crypto.randomUUID().slice(0, 8);
4412
4727
  }
4413
4728
 
4729
+ // src/db/errors.ts
4730
+ class ProjectNotFoundError extends Error {
4731
+ ref;
4732
+ constructor(ref) {
4733
+ super(`project not found: ${ref}`);
4734
+ this.name = "ProjectNotFoundError";
4735
+ this.ref = ref;
4736
+ }
4737
+ }
4738
+
4739
+ class ValidationError extends Error {
4740
+ constructor(message) {
4741
+ super(message);
4742
+ this.name = "ValidationError";
4743
+ }
4744
+ }
4745
+
4746
+ // src/lib/recording-create-identity.ts
4747
+ var MAX_RECORDING_IDENTITY_LENGTH = 255;
4748
+ function validateIdentityValue(value, label, nullMeansAbsent = false) {
4749
+ if (value === undefined || nullMeansAbsent && value === null)
4750
+ return;
4751
+ if (typeof value !== "string") {
4752
+ throw new ValidationError(`${label} must be a string`);
4753
+ }
4754
+ if (value.length === 0) {
4755
+ throw new ValidationError(`${label} must not be empty`);
4756
+ }
4757
+ if (value.length > MAX_RECORDING_IDENTITY_LENGTH) {
4758
+ throw new ValidationError(`${label} must not exceed ${MAX_RECORDING_IDENTITY_LENGTH} characters`);
4759
+ }
4760
+ if (value !== value.trim()) {
4761
+ throw new ValidationError(`${label} must not contain leading or trailing whitespace`);
4762
+ }
4763
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
4764
+ throw new ValidationError(`${label} must not contain control characters`);
4765
+ }
4766
+ if (/[^\u0020-\u007e]/.test(value)) {
4767
+ throw new ValidationError(`${label} must contain only printable ASCII characters`);
4768
+ }
4769
+ return value;
4770
+ }
4771
+ function recordingCreateIdentity(input, idempotencyKey, options = {}) {
4772
+ const bodyId = validateIdentityValue(input.id, "recording id", true);
4773
+ const headerKey = validateIdentityValue(idempotencyKey, "idempotency key");
4774
+ if (bodyId !== undefined && headerKey !== undefined && bodyId !== headerKey) {
4775
+ throw new ValidationError("recording id conflicts with idempotency key");
4776
+ }
4777
+ const effectiveKey = headerKey ?? bodyId;
4778
+ const { id: _runtimeId, ...inputWithoutId } = input;
4779
+ const persistedId = options.bindIdempotencyKeyToId === false ? bodyId : effectiveKey;
4780
+ return {
4781
+ input: persistedId === undefined ? inputWithoutId : { ...inputWithoutId, id: persistedId },
4782
+ idempotencyKey: effectiveKey
4783
+ };
4784
+ }
4785
+
4414
4786
  // src/db/recordings.ts
4415
4787
  function parseRow(row) {
4416
4788
  return {
@@ -4435,20 +4807,36 @@ function parseRow(row) {
4435
4807
  created_at: row["created_at"]
4436
4808
  };
4437
4809
  }
4438
- function createRecording(input, db) {
4810
+ function createRecording(input, db, idempotencyKey) {
4439
4811
  const d = db || getDatabase();
4440
- const id = shortUuid();
4441
- const tagsJson = JSON.stringify(input.tags || []);
4442
- const metadataJson = JSON.stringify(input.metadata || {});
4443
- 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)
4444
- 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);
4445
- if (input.tags && input.tags.length > 0) {
4446
- const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
4447
- for (const tag of input.tags) {
4448
- insertTag.run(id, tag);
4812
+ input = recordingCreateIdentity(input, idempotencyKey).input;
4813
+ const id = input.id || shortUuid();
4814
+ const create = d.transaction(() => {
4815
+ if (input.id) {
4816
+ const existing = getRecording(input.id, d);
4817
+ if (existing?.id === input.id)
4818
+ return existing;
4819
+ }
4820
+ const tagsJson = JSON.stringify(input.tags || []);
4821
+ const metadataJson = JSON.stringify(input.metadata || {});
4822
+ 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)
4823
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4824
+ 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);
4825
+ if (insertResult.changes === 0) {
4826
+ const existing = getRecording(id, d);
4827
+ if (existing?.id === id)
4828
+ return existing;
4829
+ throw new Error("recording id conflict could not be read back");
4830
+ }
4831
+ if (input.tags && input.tags.length > 0) {
4832
+ const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
4833
+ for (const tag of input.tags) {
4834
+ insertTag.run(id, tag);
4835
+ }
4449
4836
  }
4450
- }
4451
- return getRecording(id, d);
4837
+ return getRecording(id, d);
4838
+ });
4839
+ return create();
4452
4840
  }
4453
4841
  function getRecording(id, db) {
4454
4842
  const d = db || getDatabase();
@@ -4460,6 +4848,20 @@ function getRecording(id, db) {
4460
4848
  }
4461
4849
  function listRecordings(filter, db) {
4462
4850
  const d = db || getDatabase();
4851
+ const { where, params } = buildRecordingWhere(filter);
4852
+ const limit = filter?.limit || 50;
4853
+ const offset = filter?.offset || 0;
4854
+ const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
4855
+ const rows = d.query(sql).all(...params, limit, offset);
4856
+ return rows.map(parseRow);
4857
+ }
4858
+ function countRecordings(filter, db) {
4859
+ const d = db || getDatabase();
4860
+ const { where, params } = buildRecordingWhere(filter);
4861
+ const row = d.query(`SELECT COUNT(*) as c FROM recordings ${where}`).get(...params);
4862
+ return row.c;
4863
+ }
4864
+ function buildRecordingWhere(filter) {
4463
4865
  const conditions = [];
4464
4866
  const params = [];
4465
4867
  if (filter?.agent_id) {
@@ -4498,12 +4900,7 @@ function listRecordings(filter, db) {
4498
4900
  params.push(filter.until);
4499
4901
  }
4500
4902
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4501
- const limit = filter?.limit || 50;
4502
- const offset = filter?.offset || 0;
4503
- const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
4504
- params.push(limit, offset);
4505
- const rows = d.query(sql).all(...params);
4506
- return rows.map(parseRow);
4903
+ return { where, params };
4507
4904
  }
4508
4905
  function deleteRecording(id, db) {
4509
4906
  const d = db || getDatabase();
@@ -4576,16 +4973,6 @@ function listProjects(db) {
4576
4973
  return rows.map(parseProject);
4577
4974
  }
4578
4975
 
4579
- // src/db/errors.ts
4580
- class ProjectNotFoundError extends Error {
4581
- ref;
4582
- constructor(ref) {
4583
- super(`project not found: ${ref}`);
4584
- this.name = "ProjectNotFoundError";
4585
- this.ref = ref;
4586
- }
4587
- }
4588
-
4589
4976
  // src/db/agents.ts
4590
4977
  function parseAgent(row) {
4591
4978
  return {
@@ -4651,7 +5038,7 @@ function setAgentFocus(idOrName, projectId, db) {
4651
5038
  }
4652
5039
 
4653
5040
  // src/version.ts
4654
- var VERSION = "0.2.11";
5041
+ var VERSION = "0.2.13";
4655
5042
 
4656
5043
  // src/db/feedback.ts
4657
5044
  function saveFeedback(input) {
@@ -4944,6 +5331,7 @@ function resolveStorageClient(name, env = process.env, fetchImpl) {
4944
5331
  }
4945
5332
 
4946
5333
  // src/store.ts
5334
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
4947
5335
  var APP = "recordings";
4948
5336
  function listQuery(filter) {
4949
5337
  if (!filter)
@@ -4953,6 +5341,7 @@ function listQuery(filter) {
4953
5341
  project_id: filter.project_id,
4954
5342
  session_id: filter.session_id,
4955
5343
  processing_mode: filter.processing_mode,
5344
+ tags: filter.tags,
4956
5345
  search: filter.search,
4957
5346
  since: filter.since,
4958
5347
  until: filter.until,
@@ -4969,58 +5358,63 @@ function unwrap(res, key) {
4969
5358
  var localStore = {
4970
5359
  mode: "local",
4971
5360
  baseUrl: null,
4972
- async createRecording(input) {
4973
- return createRecording(input);
5361
+ async createRecording(input, idempotencyKey) {
5362
+ return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
4974
5363
  },
4975
5364
  async getRecording(id) {
4976
- return getRecording(id);
5365
+ return withLocalStoreReaderLease(() => getRecording(id));
4977
5366
  },
4978
5367
  async listRecordings(filter) {
4979
- return listRecordings(filter);
5368
+ return withLocalStoreReaderLease(() => listRecordings(filter));
5369
+ },
5370
+ async countRecordings(filter) {
5371
+ return withLocalStoreReaderLease(() => countRecordings(filter));
4980
5372
  },
4981
5373
  async searchRecordings(query, filter) {
4982
- return searchRecordings(query, filter);
5374
+ return withLocalStoreReaderLease(() => searchRecordings(query, filter));
4983
5375
  },
4984
5376
  async deleteRecording(id) {
4985
- return deleteRecording(id);
5377
+ return withLocalStoreReaderLease(() => deleteRecording(id));
4986
5378
  },
4987
5379
  async getRecordingStats() {
4988
- return getRecordingStats();
5380
+ return withLocalStoreReaderLease(() => getRecordingStats());
4989
5381
  },
4990
5382
  async registerAgent(name, description, role) {
4991
- return registerAgent(name, description, role);
5383
+ return withLocalStoreReaderLease(() => registerAgent(name, description, role));
4992
5384
  },
4993
5385
  async getAgent(idOrName) {
4994
- return getAgent(idOrName);
5386
+ return withLocalStoreReaderLease(() => getAgent(idOrName));
4995
5387
  },
4996
5388
  async listAgents() {
4997
- return listAgents();
5389
+ return withLocalStoreReaderLease(() => listAgents());
4998
5390
  },
4999
5391
  async heartbeatAgent(idOrName) {
5000
- return heartbeatAgent(idOrName);
5392
+ return withLocalStoreReaderLease(() => heartbeatAgent(idOrName));
5001
5393
  },
5002
5394
  async setAgentFocus(idOrName, projectId) {
5003
- return setAgentFocus(idOrName, projectId);
5395
+ return withLocalStoreReaderLease(() => setAgentFocus(idOrName, projectId));
5004
5396
  },
5005
5397
  async registerProject(name, path, description) {
5006
- return registerProject(name, path, description);
5398
+ return withLocalStoreReaderLease(() => registerProject(name, path, description));
5007
5399
  },
5008
5400
  async getProject(idOrPath) {
5009
- return getProject(idOrPath);
5401
+ return withLocalStoreReaderLease(() => getProject(idOrPath));
5010
5402
  },
5011
5403
  async listProjects() {
5012
- return listProjects();
5404
+ return withLocalStoreReaderLease(() => listProjects());
5013
5405
  },
5014
5406
  async saveFeedback(input) {
5015
- saveFeedback(input);
5407
+ await withLocalStoreReaderLease(() => saveFeedback(input));
5016
5408
  }
5017
5409
  };
5018
5410
  function apiStore(client) {
5019
5411
  return {
5020
5412
  mode: "cloud-http",
5021
5413
  baseUrl: client.baseUrl,
5022
- async createRecording(input) {
5023
- const res = await client.create("recordings", input);
5414
+ async createRecording(input, idempotencyKey) {
5415
+ const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
5416
+ const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
5417
+ const res = await client.create("recordings", identity.input, identity.idempotencyKey);
5024
5418
  return unwrap(res, "recording");
5025
5419
  },
5026
5420
  async getRecording(id) {
@@ -5031,6 +5425,36 @@ function apiStore(client) {
5031
5425
  const { items } = await client.list("recordings", listQuery(filter));
5032
5426
  return items;
5033
5427
  },
5428
+ async countRecordings(filter) {
5429
+ const pageLimit = 500;
5430
+ const maxPageRequests = 1e4;
5431
+ let offset = 0;
5432
+ let pageRequests = 0;
5433
+ const seenPageKeys = new Set;
5434
+ while (pageRequests < maxPageRequests) {
5435
+ pageRequests += 1;
5436
+ const { items, raw } = await client.list("recordings", {
5437
+ ...listQuery(filter),
5438
+ limit: pageLimit,
5439
+ offset
5440
+ });
5441
+ const count = raw && typeof raw === "object" ? raw.count : undefined;
5442
+ if (typeof count !== "number" || !Number.isFinite(count)) {
5443
+ throw new Error("Recordings API response is missing a valid count");
5444
+ }
5445
+ if (count > items.length)
5446
+ return count;
5447
+ if (items.length === 0)
5448
+ return offset;
5449
+ offset += items.length;
5450
+ const pageKey = recordingPageFingerprint(items);
5451
+ if (seenPageKeys.has(pageKey)) {
5452
+ throw new Error("Recordings API ignored pagination while counting legacy results");
5453
+ }
5454
+ seenPageKeys.add(pageKey);
5455
+ }
5456
+ throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
5457
+ },
5034
5458
  async searchRecordings(query, filter) {
5035
5459
  const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
5036
5460
  return items;
@@ -5112,6 +5536,42 @@ function apiStore(client) {
5112
5536
  }
5113
5537
  };
5114
5538
  }
5539
+ function recordingPageFingerprint(items) {
5540
+ const hash = createHash("sha256");
5541
+ const ids = items.map((item) => String(item.id ?? "")).sort();
5542
+ for (const id of ids) {
5543
+ hash.update(String(id.length));
5544
+ hash.update(":");
5545
+ hash.update(id);
5546
+ hash.update(";");
5547
+ }
5548
+ return `${items.length}:${hash.digest("hex")}`;
5549
+ }
5550
+ async function countStoreRecordings(store, filter) {
5551
+ if (store.countRecordings)
5552
+ return store.countRecordings(filter);
5553
+ const pageLimit = 500;
5554
+ const maxPageRequests = 1e4;
5555
+ const { limit: _limit, offset: _offset, ...unpaginated } = filter ?? {};
5556
+ const seenPageKeys = new Set;
5557
+ let offset = 0;
5558
+ for (let pageRequests = 0;pageRequests < maxPageRequests; pageRequests += 1) {
5559
+ const items = await store.listRecordings({
5560
+ ...unpaginated,
5561
+ limit: pageLimit,
5562
+ offset
5563
+ });
5564
+ if (items.length === 0)
5565
+ return offset;
5566
+ const pageKey = recordingPageFingerprint(items);
5567
+ if (seenPageKeys.has(pageKey)) {
5568
+ throw new Error("Legacy Store ignored pagination while counting recordings");
5569
+ }
5570
+ seenPageKeys.add(pageKey);
5571
+ offset += items.length;
5572
+ }
5573
+ throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
5574
+ }
5115
5575
  var cached = null;
5116
5576
  function getStore(env = process.env) {
5117
5577
  if (env === process.env && cached)
@@ -5144,13 +5604,15 @@ class EnhancementError extends Error {
5144
5604
 
5145
5605
  // src/lib/transcriber.ts
5146
5606
  var _client = null;
5607
+ var _clientApiKey = null;
5147
5608
  function getClient(config) {
5148
- if (_client)
5149
- return _client;
5150
5609
  if (!config.openai_api_key) {
5151
5610
  throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
5152
5611
  }
5612
+ if (_client && _clientApiKey === config.openai_api_key)
5613
+ return _client;
5153
5614
  _client = new OpenAI({ apiKey: config.openai_api_key });
5615
+ _clientApiKey = config.openai_api_key;
5154
5616
  return _client;
5155
5617
  }
5156
5618
  async function transcribeAudio(audioPath, config, options = {}) {
@@ -5162,7 +5624,7 @@ async function transcribeAudio(audioPath, config, options = {}) {
5162
5624
  file: stream,
5163
5625
  model: config.transcription_model,
5164
5626
  language: config.language || undefined,
5165
- prompt: buildVerbatimPrompt(options.prompt),
5627
+ prompt: buildVerbatimPrompt(options.prompt ?? config.transcription_prompt),
5166
5628
  response_format: "json"
5167
5629
  });
5168
5630
  stream.destroy();
@@ -5201,14 +5663,16 @@ ${trimmed}`;
5201
5663
  // src/lib/enhancer.ts
5202
5664
  import OpenAI2 from "openai";
5203
5665
  var _enhancementClient = null;
5666
+ var _enhancementClientApiKey = null;
5204
5667
  function getEnhancementClient(config) {
5205
- if (_enhancementClient)
5206
- return _enhancementClient;
5207
5668
  const key = config.enhancement_api_key || config.openai_api_key;
5208
5669
  if (!key) {
5209
5670
  throw new EnhancementError("API key not configured for enhancement. Set OPENAI_API_KEY or RECORDINGS_ENHANCEMENT_KEY");
5210
5671
  }
5672
+ if (_enhancementClient && _enhancementClientApiKey === key)
5673
+ return _enhancementClient;
5211
5674
  _enhancementClient = new OpenAI2({ apiKey: key });
5675
+ _enhancementClientApiKey = key;
5212
5676
  return _enhancementClient;
5213
5677
  }
5214
5678
  function needsEnhancement(text, config) {
@@ -5255,6 +5719,7 @@ function extractInstruction(text, trigger) {
5255
5719
  }
5256
5720
  async function enhanceText(rawText, instruction, config, systemPrompt) {
5257
5721
  const client = getEnhancementClient(config);
5722
+ const model = resolveTranscriberModel(config);
5258
5723
  let basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
5259
5724
 
5260
5725
  Rules:
@@ -5275,13 +5740,14 @@ Rules:
5275
5740
  Keyword Transformations:
5276
5741
  ${transformRules}`;
5277
5742
  }
5278
- const fullPrompt = systemPrompt ? `${basePrompt}
5743
+ const transcriberPrompt = combinePrompts(config.transcriber_prompt, systemPrompt);
5744
+ const fullPrompt = transcriberPrompt ? `${basePrompt}
5279
5745
 
5280
- Additional context:
5281
- ${systemPrompt}` : basePrompt;
5746
+ Transcriber instructions:
5747
+ ${transcriberPrompt}` : basePrompt;
5282
5748
  try {
5283
5749
  const response = await client.chat.completions.create({
5284
- model: config.enhancement_model,
5750
+ model,
5285
5751
  messages: [
5286
5752
  {
5287
5753
  role: "system",
@@ -5299,7 +5765,7 @@ ${systemPrompt}` : basePrompt;
5299
5765
  return {
5300
5766
  original: rawText,
5301
5767
  enhanced,
5302
- model: config.enhancement_model,
5768
+ model,
5303
5769
  reasoning: null
5304
5770
  };
5305
5771
  } catch (error) {
@@ -5307,27 +5773,77 @@ ${systemPrompt}` : basePrompt;
5307
5773
  throw new EnhancementError(`Enhancement failed: ${describeTranscriptionFailure(msg)}`);
5308
5774
  }
5309
5775
  }
5310
- async function processText(rawText, config, systemPrompt, options) {
5311
- const force = options?.force === true;
5312
- if (!force && !config.auto_enhance) {
5313
- return { text: rawText, mode: "raw", enhancement_model: null };
5776
+ async function processText(rawText, config, systemPromptOrOptions, legacyOptions) {
5777
+ const options = {
5778
+ ...normalizeProcessTextOptions(systemPromptOrOptions),
5779
+ ...legacyOptions
5780
+ };
5781
+ const postProcessingMode = options.force ? "always" : options.postProcessingMode ?? normalizePostProcessingMode(config.post_processing_mode, config.auto_enhance === false ? "off" : "auto");
5782
+ if (postProcessingMode === "off") {
5783
+ return {
5784
+ text: rawText,
5785
+ mode: "raw",
5786
+ enhancement_model: null,
5787
+ post_processing_mode: "off",
5788
+ enhancement_reason: null
5789
+ };
5314
5790
  }
5315
- const detection = needsEnhancement(rawText, config);
5316
- if (!force && !detection.needs) {
5317
- return { text: rawText, mode: "raw", enhancement_model: null };
5791
+ const detection = postProcessingMode === "always" ? {
5792
+ needs: true,
5793
+ reason: "Always-on post-processing",
5794
+ instruction: rawText
5795
+ } : needsEnhancement(rawText, config);
5796
+ if (!detection.needs) {
5797
+ return {
5798
+ text: rawText,
5799
+ mode: "raw",
5800
+ enhancement_model: null,
5801
+ post_processing_mode: postProcessingMode,
5802
+ enhancement_reason: detection.reason
5803
+ };
5318
5804
  }
5319
- const instruction = detection.needs ? detection.instruction : rawText;
5320
- const result = await enhanceText(rawText, instruction, config, systemPrompt);
5805
+ const result = await enhanceText(rawText, detection.instruction, config, options.systemPrompt);
5321
5806
  return {
5322
5807
  text: result.enhanced,
5323
5808
  mode: "enhanced",
5324
- enhancement_model: result.model
5809
+ enhancement_model: result.model,
5810
+ post_processing_mode: postProcessingMode,
5811
+ enhancement_reason: detection.reason
5325
5812
  };
5326
5813
  }
5814
+ function resolveTranscriberModel(config) {
5815
+ return config.transcriber_model || config.enhancement_model;
5816
+ }
5817
+ function normalizeProcessTextOptions(value) {
5818
+ if (typeof value === "string")
5819
+ return { systemPrompt: value };
5820
+ return value ?? {};
5821
+ }
5822
+ function combinePrompts(...prompts) {
5823
+ return prompts.map((prompt) => prompt?.trim() ?? "").filter(Boolean).join(`
5824
+
5825
+ `);
5826
+ }
5827
+
5828
+ // src/lib/machine.ts
5829
+ import { hostname } from "os";
5830
+ function currentMachineId(env = process.env, hostName = hostname()) {
5831
+ const configured = env.HASNA_MACHINE_ID?.trim();
5832
+ if (configured)
5833
+ return configured;
5834
+ return hostName.trim();
5835
+ }
5327
5836
 
5328
5837
  // src/mcp/index.ts
5329
5838
  var config = loadConfig();
5330
5839
  ensureDataDir(config);
5840
+ function runtimeConfig() {
5841
+ return {
5842
+ ...loadConfig(),
5843
+ db_path: config.db_path,
5844
+ audio_dir: config.audio_dir
5845
+ };
5846
+ }
5331
5847
  function buildServer() {
5332
5848
  const server = new McpServer({
5333
5849
  name: "recordings",
@@ -5339,34 +5855,205 @@ function buildServer() {
5339
5855
  }
5340
5856
  function errorResult(e) {
5341
5857
  const msg = e instanceof Error ? e.message : String(e);
5342
- return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
5858
+ return { content: [{ type: "text", text: `Error: ${truncateText(msg, 500)}` }], isError: true };
5343
5859
  }
5344
5860
  function compact(r) {
5345
- const t = (r.processed_text || r.raw_text).slice(0, 80);
5346
- return `${r.id.slice(0, 8)} | ${r.processing_mode} | ${r.created_at.slice(0, 16)} | ${t}${t.length >= 80 ? "..." : ""}`;
5861
+ const preview = truncateText(r.processed_text || r.raw_text, 100);
5862
+ return `${truncateText(r.id, 8)} | ${truncateText(r.processing_mode, 16)} | ${truncateText(r.created_at, 16)} | ${preview}`;
5347
5863
  }
5348
- function full(r) {
5349
- const lines = [`ID: ${r.id}`, `Mode: ${r.processing_mode}`, `Model: ${r.model_used}`];
5864
+ function detail(r) {
5865
+ const lines = [`ID: ${truncateText(r.id, 80)}`, `Mode: ${truncateText(r.processing_mode, 16)}`, `Model: ${truncateText(r.model_used, 80)}`];
5350
5866
  if (r.enhancement_model)
5351
- lines.push(`Enhanced by: ${r.enhancement_model}`);
5867
+ lines.push(`Enhanced by: ${truncateText(r.enhancement_model, 80)}`);
5352
5868
  if (r.duration_ms)
5353
5869
  lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
5354
5870
  if (r.language)
5355
- lines.push(`Language: ${r.language}`);
5871
+ lines.push(`Language: ${truncateText(r.language, 20)}`);
5356
5872
  if (r.tags.length > 0)
5357
- lines.push(`Tags: ${r.tags.join(", ")}`);
5873
+ lines.push(`Tags: ${r.tags.map((tag) => truncateText(tag, 80)).join(", ")}`);
5358
5874
  if (r.agent_id)
5359
- lines.push(`Agent: ${r.agent_id}`);
5875
+ lines.push(`Agent: ${truncateText(r.agent_id, 80)}`);
5360
5876
  if (r.project_id)
5361
- lines.push(`Project: ${r.project_id}`);
5877
+ lines.push(`Project: ${truncateText(r.project_id, 80)}`);
5362
5878
  if (r.session_id)
5363
- lines.push(`Session: ${r.session_id}`);
5364
- lines.push(`Created: ${r.created_at}`);
5365
- lines.push(`Text: ${r.raw_text}`);
5879
+ lines.push(`Session: ${truncateText(r.session_id, 80)}`);
5880
+ lines.push(`Created: ${truncateText(r.created_at, 40)}`);
5881
+ lines.push(`Text: ${stripTerminalControls(r.raw_text)}`);
5366
5882
  if (r.processed_text && r.processed_text !== r.raw_text) {
5367
- lines.push(`Enhanced: ${r.processed_text}`);
5883
+ lines.push(`Enhanced: ${stripTerminalControls(r.processed_text)}`);
5368
5884
  }
5369
5885
  return lines.join(`
5886
+ `);
5887
+ }
5888
+ function verboseRecording(r) {
5889
+ const lines = [
5890
+ `${truncateText(r.id, 8)} | ${truncateText(r.processing_mode, 16)} | ${truncateText(r.created_at, 16)}`,
5891
+ `Preview: ${truncateText(r.processed_text || r.raw_text, 180)}`,
5892
+ `Model: ${r.enhancement_model ? `${truncateText(r.model_used, 80)} -> ${truncateText(r.enhancement_model, 80)}` : truncateText(r.model_used, 80)}`
5893
+ ];
5894
+ if (r.duration_ms)
5895
+ lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
5896
+ if (r.language)
5897
+ lines.push(`Language: ${truncateText(r.language, 20)}`);
5898
+ if (r.tags.length > 0)
5899
+ lines.push(`Tags: ${summarizeTags(r.tags)}`);
5900
+ const scopes = [
5901
+ r.agent_id ? `agent=${truncateText(r.agent_id, 80)}` : null,
5902
+ r.project_id ? `project=${truncateText(r.project_id, 80)}` : null,
5903
+ r.session_id ? `session=${truncateText(r.session_id, 80)}` : null
5904
+ ].filter(Boolean);
5905
+ if (scopes.length > 0)
5906
+ lines.push(`Scope: ${scopes.join(" ")}`);
5907
+ lines.push(`Details: get_recording { "id": "${truncateText(r.id, 8)}" }`);
5908
+ return lines.join(`
5909
+ `);
5910
+ }
5911
+ function applyTranscriptionArgs(cfg, args) {
5912
+ if (args.language)
5913
+ cfg.language = args.language;
5914
+ const transcriptionPrompt = args.transcription_prompt ?? args.prompt;
5915
+ if (transcriptionPrompt !== undefined) {
5916
+ cfg.transcription_prompt = transcriptionPrompt;
5917
+ }
5918
+ const transcriberPrompt = args.transcriber_prompt ?? args.system_prompt;
5919
+ if (transcriberPrompt !== undefined) {
5920
+ cfg.transcriber_prompt = transcriberPrompt;
5921
+ }
5922
+ if (args.transcriber_model) {
5923
+ cfg.transcriber_model = args.transcriber_model;
5924
+ } else if (args.enhancement_model) {
5925
+ cfg.enhancement_model = args.enhancement_model;
5926
+ cfg.transcriber_model = args.enhancement_model;
5927
+ }
5928
+ const requestedMode = args.post_processing_mode ?? args.post_processing;
5929
+ if (args.no_enhance || args.enhance === false) {
5930
+ cfg.post_processing_mode = "off";
5931
+ normalizePostProcessingConfig(cfg, true);
5932
+ } else if (args.enhance === true) {
5933
+ cfg.post_processing_mode = "always";
5934
+ normalizePostProcessingConfig(cfg, true);
5935
+ } else if (requestedMode) {
5936
+ cfg.post_processing_mode = normalizePostProcessingMode(requestedMode, cfg.post_processing_mode ?? "auto");
5937
+ normalizePostProcessingConfig(cfg, true);
5938
+ } else {
5939
+ normalizePostProcessingConfig(cfg, false);
5940
+ }
5941
+ return cfg;
5942
+ }
5943
+ function buildTranscriptionMetadata(cfg, processed, sources = {}) {
5944
+ const transcriptionPromptConfigured = Boolean(cfg.transcription_prompt?.trim());
5945
+ const transcriberPromptConfigured = Boolean(cfg.transcriber_prompt?.trim());
5946
+ return {
5947
+ transcription_prompt: {
5948
+ configured: transcriptionPromptConfigured,
5949
+ source: sources.transcriptionPromptFromRequest ? "request" : transcriptionPromptConfigured ? "config" : "none"
5950
+ },
5951
+ transcriber_prompt: {
5952
+ configured: transcriberPromptConfigured,
5953
+ source: sources.transcriberPromptFromRequest ? "request" : transcriberPromptConfigured ? "config" : "none"
5954
+ },
5955
+ post_processing: {
5956
+ mode: processed.post_processing_mode,
5957
+ applied: processed.mode === "enhanced",
5958
+ reason: processed.enhancement_reason,
5959
+ model: processed.enhancement_model
5960
+ },
5961
+ transcriber_model: resolveTranscriberModel(cfg)
5962
+ };
5963
+ }
5964
+ function recordingJson(r) {
5965
+ return JSON.stringify({
5966
+ id: r.id,
5967
+ raw_text: r.raw_text,
5968
+ processed_text: r.processed_text,
5969
+ processing_mode: r.processing_mode,
5970
+ model_used: r.model_used,
5971
+ enhancement_model: r.enhancement_model,
5972
+ language: r.language,
5973
+ tags: r.tags,
5974
+ metadata: r.metadata,
5975
+ created_at: r.created_at
5976
+ }, null, 2);
5977
+ }
5978
+ function truncateText(value, max) {
5979
+ const normalized = sanitizeInline(value);
5980
+ const prefix = [];
5981
+ for (const point of normalized) {
5982
+ if (prefix.length === max) {
5983
+ return `${prefix.slice(0, Math.max(0, max - 3)).join("")}...`;
5984
+ }
5985
+ prefix.push(point);
5986
+ }
5987
+ return normalized;
5988
+ }
5989
+ function stripTerminalControls(value) {
5990
+ 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, "");
5991
+ }
5992
+ function sanitizeInline(value) {
5993
+ return stripTerminalControls(value).replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
5994
+ }
5995
+ function summarizeTags(tags) {
5996
+ const shown = tags.slice(0, 3).map((tag) => truncateText(tag, 20));
5997
+ if (tags.length > shown.length)
5998
+ shown.push(`+${tags.length - shown.length}`);
5999
+ return shown.join(", ");
6000
+ }
6001
+ function resolveMcpPagination(args, defaultLimit = 10) {
6002
+ const requested = Number.isFinite(args.limit) && args.limit && args.limit > 0 ? Math.floor(args.limit) : defaultLimit;
6003
+ const max = args.full ? 10 : 50;
6004
+ const rawOffset = args.cursor ?? args.offset ?? 0;
6005
+ return {
6006
+ limit: Math.min(requested, max),
6007
+ offset: Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset)) : 0,
6008
+ capped: requested > max
6009
+ };
6010
+ }
6011
+ function withoutPagination(filter) {
6012
+ const { limit: _limit, offset: _offset, ...rest } = filter;
6013
+ return rest;
6014
+ }
6015
+ function formatMcpCollection(label, recordings, options) {
6016
+ if (recordings.length === 0) {
6017
+ return options.total === 0 ? `No ${label} found.` : `No ${label} at cursor ${options.offset}.`;
6018
+ }
6019
+ const format = options.full ? verboseRecording : compact;
6020
+ const separator = options.full ? `
6021
+ ---
6022
+ ` : `
6023
+ `;
6024
+ const end = options.offset + recordings.length;
6025
+ const total = Math.max(options.total, end);
6026
+ const lines = [
6027
+ `${label}: showing ${recordings.length} of ${total} (${options.offset + 1}-${end}, limit ${options.limit})`,
6028
+ recordings.map(format).join(separator)
6029
+ ];
6030
+ const hints = [];
6031
+ if (options.capped)
6032
+ hints.push(`limit capped at ${options.limit} for MCP output`);
6033
+ if (end < total)
6034
+ hints.push(`next cursor: ${end}`);
6035
+ hints.push("use get_recording { id } for full details; set full=true for richer rows");
6036
+ lines.push(`Hints: ${hints.join("; ")}.`);
6037
+ return lines.join(separator);
6038
+ }
6039
+ function formatMcpPage(label, items, args, format) {
6040
+ const pagination = resolveMcpPagination(args, 20);
6041
+ const page = items.slice(pagination.offset, pagination.offset + pagination.limit);
6042
+ if (page.length === 0) {
6043
+ return items.length === 0 ? `No ${label}.` : `No ${label} at cursor ${pagination.offset}.`;
6044
+ }
6045
+ const end = pagination.offset + page.length;
6046
+ const hints = [];
6047
+ if (pagination.capped)
6048
+ hints.push(`limit capped at ${pagination.limit} for MCP output`);
6049
+ if (end < items.length)
6050
+ hints.push(`next cursor: ${end}`);
6051
+ return [
6052
+ `${label}: showing ${page.length} of ${items.length} (${pagination.offset + 1}-${end}, limit ${pagination.limit})`,
6053
+ page.map(format).join(`
6054
+ `),
6055
+ hints.length > 0 ? `Hints: ${hints.join("; ")}.` : ""
6056
+ ].filter(Boolean).join(`
5370
6057
  `);
5371
6058
  }
5372
6059
  async function saveRecordingMemento(args) {
@@ -5394,26 +6081,30 @@ function buildServer() {
5394
6081
  } catch {}
5395
6082
  }
5396
6083
  const toolDocs = {
5397
- transcribe_audio: `Transcribe audio file. Auto-enhances if needed.
5398
- Params: audio_path (string, required): path to wav/mp3/m4a/webm | language (string): ISO code e.g. en/es/fr | no_enhance (bool): skip AI enhancement | tags (string[]): tags | agent_id (string) | project_id (string) | session_id (string)`,
6084
+ recordings_status: `Show safe service status for agents.
6085
+ Params: none
6086
+ Returns: JSON with package version, MCP HTTP defaults, active transcription/enhancement models, language, data paths, key-presence booleans, and recording counts. Never returns secret values.`,
6087
+ transcribe_audio: `Transcribe audio file and save raw plus optional processed text.
6088
+ Params: audio_path (string, required): path to wav/mp3/m4a/webm | language (string): ISO code e.g. en/es/fr | transcription_prompt or prompt (string): STT vocabulary/context only | transcriber_prompt or system_prompt (string): post-transcription cleanup instructions | post_processing_mode ('off'|'auto'|'always') | transcriber_model (string) | no_enhance (bool): alias for post_processing_mode=off | tags (string[]) | agent_id (string) | project_id (string) | session_id (string)
6089
+ Returns: JSON recording summary with raw_text, processed_text, processing_mode, and safe metadata.`,
5399
6090
  save_recording: `Save text as recording. Auto-enhances if needed.
5400
- Params: text (string, required): text to save | enhance (bool): force enhancement | tags (string[]) | agent_id (string) | project_id (string) | session_id (string) | metadata (object)`,
6091
+ Params: text (string, required): text to save | enhance (bool): true forces always, false disables | transcriber_prompt or system_prompt (string): post-processing instructions | post_processing_mode ('off'|'auto'|'always') | tags (string[]) | agent_id (string) | project_id (string) | session_id (string) | metadata (object)`,
5401
6092
  get_recording: `Get recording by ID or prefix.
5402
6093
  Params: id (string, required): recording ID or prefix`,
5403
- list_recordings: `List recordings, compact by default, most recent first.
5404
- Params: limit (number, default 10) | offset (number) | processing_mode ('raw'|'enhanced') | tags (string[]) | search (string): text search | since/until (ISO date) | agent_id | project_id | session_id | full (bool): verbose output`,
5405
- search_recordings: `Search recordings by text content.
5406
- Params: query (string, required) | limit (number, default 10) | agent_id | project_id | full (bool): verbose output`,
6094
+ list_recordings: `List recordings, compact and capped by default, most recent first.
6095
+ Params: limit (number, default 10, max 50 compact / 10 verbose) | offset or cursor (number) | processing_mode ('raw'|'enhanced') | tags (string[]) | search (string): text search | since/until (ISO date) | agent_id | project_id | session_id | full (bool): richer metadata rows with bounded previews. Use get_recording for full text/details.`,
6096
+ search_recordings: `Search recordings by text content, compact and capped by default.
6097
+ Params: query (string, required) | limit (number, default 10, max 50 compact / 10 verbose) | offset or cursor (number) | agent_id | project_id | full (bool): richer metadata rows with bounded previews. Use get_recording for full text/details.`,
5407
6098
  delete_recording: `Delete recording by ID.
5408
6099
  Params: id (string, required)`,
5409
- recording_stats: `Recording count, mode breakdown, duration.
5410
- Params: none`,
6100
+ recording_stats: `Recording count, mode breakdown, duration, and capped model breakdown.
6101
+ Params: limit_models (number, default 10, max 10 compact / 50 verbose) | verbose (bool): allow up to 50 model counts`,
5411
6102
  detect_enhancement: `Check if text needs AI enhancement.
5412
6103
  Params: text (string, required)`,
5413
6104
  register_agent: `Register agent (idempotent). Auto-updates last_seen_at on re-register.
5414
6105
  Params: name (string, required) | description (string) | role (string)`,
5415
- list_agents: `List registered agents.
5416
- Params: none`,
6106
+ list_agents: `List registered agents, capped by default.
6107
+ Params: limit (number, default 20, max 50) | offset or cursor (number)`,
5417
6108
  get_agent: `Get agent by ID or name.
5418
6109
  Params: id (string, required)`,
5419
6110
  heartbeat: `Update last_seen_at to signal agent is active.
@@ -5422,16 +6113,59 @@ Params: agent_id (string, required): agent ID or name`,
5422
6113
  Params: agent_id (string, required) | project_id (string, nullable): project ID or null to clear`,
5423
6114
  register_project: `Register project (idempotent).
5424
6115
  Params: name (string, required) | path (string, required): absolute path | description (string)`,
5425
- list_projects: `List registered projects.
5426
- Params: none`
6116
+ list_projects: `List registered projects, capped by default.
6117
+ Params: limit (number, default 20, max 50) | offset or cursor (number)`
5427
6118
  };
5428
6119
  registerTool("describe_tool", "Get full param docs for any tool.", { name: exports_external.string() }, async (args) => {
5429
6120
  const doc = toolDocs[args.name];
5430
- return doc ? text(doc) : text(`Unknown tool: ${args.name}. Available: ${Object.keys(toolDocs).join(", ")}`);
6121
+ return doc ? text(doc) : text(`Unknown tool: ${truncateText(args.name, 80)}. Available: ${Object.keys(toolDocs).join(", ")}`);
6122
+ });
6123
+ registerTool("recordings_status", "Show safe service status for agents.", {}, async () => {
6124
+ try {
6125
+ const cfg = runtimeConfig();
6126
+ const stats = await getStore().getRecordingStats();
6127
+ return text(JSON.stringify({
6128
+ service: "recordings",
6129
+ version: VERSION,
6130
+ mcp: {
6131
+ default_http_port: DEFAULT_MCP_HTTP_PORT,
6132
+ endpoint: "/mcp"
6133
+ },
6134
+ config: {
6135
+ transcription_model: cfg.transcription_model,
6136
+ realtime_session_model: cfg.realtime_session_model,
6137
+ realtime_transcription_model: cfg.realtime_transcription_model,
6138
+ enhancement_model: cfg.enhancement_model,
6139
+ transcriber_model: resolveTranscriberModel(cfg),
6140
+ language: cfg.language,
6141
+ auto_enhance: cfg.auto_enhance,
6142
+ post_processing_mode: cfg.post_processing_mode,
6143
+ transcription_prompt_configured: Boolean(cfg.transcription_prompt?.trim()),
6144
+ transcriber_prompt_configured: Boolean(cfg.transcriber_prompt?.trim()),
6145
+ max_recording_seconds: cfg.max_recording_seconds,
6146
+ db_path: cfg.db_path,
6147
+ audio_dir: cfg.audio_dir,
6148
+ openai_api_key_configured: Boolean(cfg.openai_api_key),
6149
+ enhancement_api_key_configured: Boolean(cfg.enhancement_api_key || cfg.openai_api_key),
6150
+ config_warnings: cfg.config_warnings ?? []
6151
+ },
6152
+ stats
6153
+ }, null, 2));
6154
+ } catch (e) {
6155
+ return errorResult(e);
6156
+ }
5431
6157
  });
5432
6158
  registerTool("transcribe_audio", "Transcribe audio file. Auto-enhances if needed.", {
5433
6159
  audio_path: exports_external.string(),
5434
6160
  language: exports_external.string().optional(),
6161
+ transcription_prompt: exports_external.string().optional(),
6162
+ prompt: exports_external.string().optional(),
6163
+ transcriber_prompt: exports_external.string().optional(),
6164
+ system_prompt: exports_external.string().optional(),
6165
+ post_processing_mode: exports_external.enum(["off", "auto", "always"]).optional(),
6166
+ post_processing: exports_external.enum(["off", "auto", "always"]).optional(),
6167
+ transcriber_model: exports_external.string().optional(),
6168
+ enhancement_model: exports_external.string().optional(),
5435
6169
  no_enhance: exports_external.boolean().optional(),
5436
6170
  tags: exports_external.array(exports_external.string()).optional(),
5437
6171
  agent_id: exports_external.string().optional(),
@@ -5439,13 +6173,13 @@ Params: none`
5439
6173
  session_id: exports_external.string().optional()
5440
6174
  }, async (args) => {
5441
6175
  try {
5442
- const cfg = { ...config };
5443
- if (args.language)
5444
- cfg.language = args.language;
5445
- if (args.no_enhance)
5446
- cfg.auto_enhance = false;
6176
+ const cfg = applyTranscriptionArgs({ ...runtimeConfig() }, args);
5447
6177
  const transcription = await transcribeAudio(args.audio_path, cfg);
5448
6178
  const processed = await processText(transcription.text, cfg);
6179
+ const metadata = buildTranscriptionMetadata(cfg, processed, {
6180
+ transcriptionPromptFromRequest: args.transcription_prompt !== undefined || args.prompt !== undefined,
6181
+ transcriberPromptFromRequest: args.transcriber_prompt !== undefined || args.system_prompt !== undefined
6182
+ });
5449
6183
  const recording = await getStore().createRecording({
5450
6184
  audio_path: args.audio_path,
5451
6185
  raw_text: transcription.text,
@@ -5458,7 +6192,9 @@ Params: none`
5458
6192
  tags: args.tags,
5459
6193
  agent_id: args.agent_id,
5460
6194
  project_id: args.project_id,
5461
- session_id: args.session_id
6195
+ session_id: args.session_id,
6196
+ machine_id: currentMachineId(),
6197
+ metadata
5462
6198
  });
5463
6199
  if (args.agent_id) {
5464
6200
  await saveRecordingMemento({
@@ -5474,8 +6210,7 @@ Params: none`
5474
6210
  summary: `Recording ${recording.id.slice(0, 8)} for ${args.agent_id}`
5475
6211
  });
5476
6212
  }
5477
- const output = processed.mode === "enhanced" ? processed.text : transcription.text;
5478
- return text(`${recording.id.slice(0, 8)} | ${processed.mode} | ${output}`);
6213
+ return text(recordingJson(recording));
5479
6214
  } catch (e) {
5480
6215
  return errorResult(e);
5481
6216
  }
@@ -5483,6 +6218,12 @@ Params: none`
5483
6218
  registerTool("save_recording", "Save text as recording. Auto-enhances if needed.", {
5484
6219
  text: exports_external.string(),
5485
6220
  enhance: exports_external.boolean().optional(),
6221
+ transcriber_prompt: exports_external.string().optional(),
6222
+ system_prompt: exports_external.string().optional(),
6223
+ post_processing_mode: exports_external.enum(["off", "auto", "always"]).optional(),
6224
+ post_processing: exports_external.enum(["off", "auto", "always"]).optional(),
6225
+ transcriber_model: exports_external.string().optional(),
6226
+ enhancement_model: exports_external.string().optional(),
5486
6227
  tags: exports_external.array(exports_external.string()).optional(),
5487
6228
  agent_id: exports_external.string().optional(),
5488
6229
  project_id: exports_external.string().optional(),
@@ -5493,17 +6234,28 @@ Params: none`
5493
6234
  metadata: exports_external.record(exports_external.unknown()).optional()
5494
6235
  }, async (args) => {
5495
6236
  try {
6237
+ const cfg = applyTranscriptionArgs(runtimeConfig(), args);
5496
6238
  let processedText;
5497
6239
  let mode = "raw";
5498
6240
  let enhModel;
5499
- if (args.enhance !== false) {
5500
- const processed = await processText(args.text, config, undefined, { force: args.enhance === true });
5501
- if (processed.mode === "enhanced") {
5502
- processedText = processed.text;
5503
- mode = "enhanced";
5504
- enhModel = processed.enhancement_model || undefined;
5505
- }
6241
+ let processed = {
6242
+ text: args.text,
6243
+ mode: "raw",
6244
+ enhancement_model: null,
6245
+ post_processing_mode: cfg.post_processing_mode ?? "auto",
6246
+ enhancement_reason: null
6247
+ };
6248
+ if (cfg.post_processing_mode !== "off") {
6249
+ processed = await processText(args.text, cfg);
5506
6250
  }
6251
+ if (processed.mode === "enhanced") {
6252
+ processedText = processed.text;
6253
+ mode = "enhanced";
6254
+ enhModel = processed.enhancement_model || undefined;
6255
+ }
6256
+ const processingMetadata = buildTranscriptionMetadata(cfg, processed, {
6257
+ transcriberPromptFromRequest: args.transcriber_prompt !== undefined || args.system_prompt !== undefined
6258
+ });
5507
6259
  const recording = await getStore().createRecording({
5508
6260
  raw_text: args.text,
5509
6261
  processed_text: processedText,
@@ -5517,10 +6269,13 @@ Params: none`
5517
6269
  goal: args.goal,
5518
6270
  role: args.role,
5519
6271
  task_list_id: args.task_list_id,
5520
- metadata: args.metadata
6272
+ machine_id: currentMachineId(),
6273
+ metadata: {
6274
+ ...args.metadata ?? {},
6275
+ ...processingMetadata
6276
+ }
5521
6277
  });
5522
- const output = processedText || args.text;
5523
- return text(`${recording.id.slice(0, 8)} | ${mode} | ${output}`);
6278
+ return text(recordingJson(recording));
5524
6279
  } catch (e) {
5525
6280
  return errorResult(e);
5526
6281
  }
@@ -5529,8 +6284,8 @@ Params: none`
5529
6284
  try {
5530
6285
  const r = await getStore().getRecording(args.id);
5531
6286
  if (!r)
5532
- return text(`Not found: ${args.id}`);
5533
- return text(full(r));
6287
+ return text(`Not found: ${truncateText(args.id, 80)}`);
6288
+ return text(detail(r));
5534
6289
  } catch (e) {
5535
6290
  return errorResult(e);
5536
6291
  }
@@ -5538,6 +6293,7 @@ Params: none`
5538
6293
  registerTool("list_recordings", "List recordings. Compact default, recent first.", {
5539
6294
  limit: exports_external.number().optional(),
5540
6295
  offset: exports_external.number().optional(),
6296
+ cursor: exports_external.number().optional(),
5541
6297
  processing_mode: exports_external.enum(["raw", "enhanced"]).optional(),
5542
6298
  tags: exports_external.array(exports_external.string()).optional(),
5543
6299
  search: exports_external.string().optional(),
@@ -5549,9 +6305,10 @@ Params: none`
5549
6305
  full: exports_external.boolean().optional()
5550
6306
  }, async (args) => {
5551
6307
  try {
6308
+ const pagination = resolveMcpPagination(args);
5552
6309
  const filter = {
5553
- limit: args.limit || 10,
5554
- offset: args.offset,
6310
+ limit: pagination.limit,
6311
+ offset: pagination.offset,
5555
6312
  processing_mode: args.processing_mode,
5556
6313
  tags: args.tags,
5557
6314
  search: args.search,
@@ -5561,15 +6318,18 @@ Params: none`
5561
6318
  project_id: args.project_id,
5562
6319
  session_id: args.session_id
5563
6320
  };
5564
- const recordings = await getStore().listRecordings(filter);
5565
- if (recordings.length === 0)
5566
- return text("No recordings found.");
5567
- const fmt = args.full ? full : compact;
5568
- const sep = args.full ? `
5569
- ---
5570
- ` : `
5571
- `;
5572
- return text(`${recordings.length} recording(s):${sep}${recordings.map(fmt).join(sep)}`);
6321
+ const store = getStore();
6322
+ const [recordings, total] = await Promise.all([
6323
+ store.listRecordings(filter),
6324
+ countStoreRecordings(store, withoutPagination(filter))
6325
+ ]);
6326
+ return text(formatMcpCollection("recordings", recordings, {
6327
+ total,
6328
+ offset: pagination.offset,
6329
+ limit: pagination.limit,
6330
+ full: args.full,
6331
+ capped: pagination.capped
6332
+ }));
5573
6333
  } catch (e) {
5574
6334
  return errorResult(e);
5575
6335
  }
@@ -5577,42 +6337,63 @@ Params: none`
5577
6337
  registerTool("search_recordings", "Search recordings by text.", {
5578
6338
  query: exports_external.string(),
5579
6339
  limit: exports_external.number().optional(),
6340
+ offset: exports_external.number().optional(),
6341
+ cursor: exports_external.number().optional(),
5580
6342
  agent_id: exports_external.string().optional(),
5581
6343
  project_id: exports_external.string().optional(),
6344
+ session_id: exports_external.string().optional(),
5582
6345
  full: exports_external.boolean().optional()
5583
6346
  }, async (args) => {
5584
6347
  try {
5585
- const results = await getStore().searchRecordings(args.query, {
5586
- limit: args.limit || 10,
6348
+ const pagination = resolveMcpPagination(args);
6349
+ const filter = {
6350
+ limit: pagination.limit,
6351
+ offset: pagination.offset,
5587
6352
  agent_id: args.agent_id,
5588
- project_id: args.project_id
5589
- });
5590
- if (results.length === 0)
5591
- return text("No results.");
5592
- const fmt = args.full ? full : compact;
5593
- const sep = args.full ? `
5594
- ---
5595
- ` : `
5596
- `;
5597
- return text(`${results.length} result(s):${sep}${results.map(fmt).join(sep)}`);
6353
+ project_id: args.project_id,
6354
+ session_id: args.session_id
6355
+ };
6356
+ const store = getStore();
6357
+ const [results, total] = await Promise.all([
6358
+ store.searchRecordings(args.query, filter),
6359
+ countStoreRecordings(store, withoutPagination({ ...filter, search: args.query }))
6360
+ ]);
6361
+ return text(formatMcpCollection("results", results, {
6362
+ total,
6363
+ offset: pagination.offset,
6364
+ limit: pagination.limit,
6365
+ full: args.full,
6366
+ capped: pagination.capped
6367
+ }));
5598
6368
  } catch (e) {
5599
6369
  return errorResult(e);
5600
6370
  }
5601
6371
  });
5602
6372
  registerTool("delete_recording", "Delete recording by ID.", { id: exports_external.string() }, async (args) => {
5603
6373
  try {
5604
- return text(await getStore().deleteRecording(args.id) ? `Deleted ${args.id}` : `Not found: ${args.id}`);
6374
+ return text(await getStore().deleteRecording(args.id) ? `Deleted ${truncateText(args.id, 80)}` : `Not found: ${truncateText(args.id, 80)}`);
5605
6375
  } catch (e) {
5606
6376
  return errorResult(e);
5607
6377
  }
5608
6378
  });
5609
- registerTool("recording_stats", "Recording stats: count, modes, duration.", {}, async () => {
6379
+ registerTool("recording_stats", "Recording stats: count, modes, duration, and capped model breakdown.", {
6380
+ limit_models: exports_external.number().optional(),
6381
+ verbose: exports_external.boolean().optional()
6382
+ }, async (args) => {
5610
6383
  try {
5611
6384
  const s = await getStore().getRecordingStats();
5612
6385
  let out = `Total: ${s.total} | Raw: ${s.raw} | Enhanced: ${s.enhanced} | Duration: ${(s.total_duration_ms / 1000).toFixed(1)}s`;
5613
- if (Object.keys(s.by_model).length > 0) {
6386
+ const models = Object.entries(s.by_model).sort((a, b) => b[1] - a[1]);
6387
+ if (models.length > 0) {
6388
+ const maxModels = args.verbose ? 50 : 10;
6389
+ const requested = Number.isFinite(args.limit_models) && args.limit_models && args.limit_models > 0 ? Math.floor(args.limit_models) : maxModels;
6390
+ const limit = Math.min(requested, maxModels);
5614
6391
  out += `
5615
- ` + Object.entries(s.by_model).map(([m, c]) => `${m}: ${c}`).join(", ");
6392
+ Models: ` + models.slice(0, limit).map(([m, c]) => `${truncateText(m, 80)}: ${c}`).join(", ");
6393
+ if (limit < models.length) {
6394
+ out += `
6395
+ Hints: ${models.length - limit} more model(s); call recording_stats with verbose=true and limit_models up to 50.`;
6396
+ }
5616
6397
  }
5617
6398
  return text(out);
5618
6399
  } catch (e) {
@@ -5621,7 +6402,7 @@ Params: none`
5621
6402
  });
5622
6403
  registerTool("detect_enhancement", "Check if text needs AI enhancement.", { text: exports_external.string() }, async (args) => {
5623
6404
  try {
5624
- const r = needsEnhancement(args.text, config);
6405
+ const r = needsEnhancement(args.text, runtimeConfig());
5625
6406
  return text(`${r.needs ? "Yes" : "No"}: ${r.reason}`);
5626
6407
  } catch (e) {
5627
6408
  return errorResult(e);
@@ -5630,18 +6411,19 @@ Params: none`
5630
6411
  registerTool("register_agent", "Register agent (idempotent).", { name: exports_external.string(), description: exports_external.string().optional(), role: exports_external.string().optional() }, async (args) => {
5631
6412
  try {
5632
6413
  const a = await getStore().registerAgent(args.name, args.description, args.role);
5633
- return text(`${a.id} | ${a.name} | ${a.role}`);
6414
+ return text(`${truncateText(a.id, 80)} | ${truncateText(a.name, 80)} | ${truncateText(a.role, 40)}`);
5634
6415
  } catch (e) {
5635
6416
  return errorResult(e);
5636
6417
  }
5637
6418
  });
5638
- registerTool("list_agents", "List registered agents.", {}, async () => {
6419
+ registerTool("list_agents", "List registered agents, capped by default.", {
6420
+ limit: exports_external.number().optional(),
6421
+ offset: exports_external.number().optional(),
6422
+ cursor: exports_external.number().optional()
6423
+ }, async (args) => {
5639
6424
  try {
5640
6425
  const agents = await getStore().listAgents();
5641
- if (agents.length === 0)
5642
- return text("None.");
5643
- return text(agents.map((a) => `${a.id} | ${a.name} | ${a.role}`).join(`
5644
- `));
6426
+ return text(formatMcpPage("agents", agents, args, (agent) => `${truncateText(agent.id, 80)} | ${truncateText(agent.name, 80)} | ${truncateText(agent.role, 40)} | last_seen: ${truncateText(agent.last_seen_at, 40)}`));
5645
6427
  } catch (e) {
5646
6428
  return errorResult(e);
5647
6429
  }
@@ -5650,8 +6432,8 @@ Params: none`
5650
6432
  try {
5651
6433
  const a = await getStore().getAgent(args.id);
5652
6434
  if (!a)
5653
- return text(`Not found: ${args.id}`);
5654
- return text(`${a.id} | ${a.name} | ${a.role} | ${a.last_seen_at}`);
6435
+ return text(`Not found: ${truncateText(args.id, 80)}`);
6436
+ return text(`${truncateText(a.id, 80)} | ${truncateText(a.name, 80)} | ${truncateText(a.role, 40)} | ${truncateText(a.last_seen_at, 40)}`);
5655
6437
  } catch (e) {
5656
6438
  return errorResult(e);
5657
6439
  }
@@ -5659,18 +6441,19 @@ Params: none`
5659
6441
  registerTool("register_project", "Register project (idempotent).", { name: exports_external.string(), path: exports_external.string(), description: exports_external.string().optional() }, async (args) => {
5660
6442
  try {
5661
6443
  const p = await getStore().registerProject(args.name, args.path, args.description);
5662
- return text(`${p.id.slice(0, 8)} | ${p.name} | ${p.path}`);
6444
+ return text(`${truncateText(p.id, 8)} | ${truncateText(p.name, 80)} | ${truncateText(p.path, 120)}`);
5663
6445
  } catch (e) {
5664
6446
  return errorResult(e);
5665
6447
  }
5666
6448
  });
5667
- registerTool("list_projects", "List registered projects.", {}, async () => {
6449
+ registerTool("list_projects", "List registered projects, capped by default.", {
6450
+ limit: exports_external.number().optional(),
6451
+ offset: exports_external.number().optional(),
6452
+ cursor: exports_external.number().optional()
6453
+ }, async (args) => {
5668
6454
  try {
5669
6455
  const projects = await getStore().listProjects();
5670
- if (projects.length === 0)
5671
- return text("None.");
5672
- return text(projects.map((p) => `${p.id.slice(0, 8)} | ${p.name} | ${p.path}`).join(`
5673
- `));
6456
+ return text(formatMcpPage("projects", projects, args, (project) => `${truncateText(project.id, 8)} | ${truncateText(project.name, 80)} | ${truncateText(project.path, 120)}`));
5674
6457
  } catch (e) {
5675
6458
  return errorResult(e);
5676
6459
  }
@@ -5679,8 +6462,8 @@ Params: none`
5679
6462
  try {
5680
6463
  const agent = await getStore().heartbeatAgent(args.agent_id);
5681
6464
  if (!agent)
5682
- return text(`Agent not found: ${args.agent_id}`);
5683
- return text(`${agent.id} | ${agent.name} | last_seen: ${agent.last_seen_at}`);
6465
+ return text(`Agent not found: ${truncateText(args.agent_id, 80)}`);
6466
+ return text(`${truncateText(agent.id, 80)} | ${truncateText(agent.name, 80)} | last_seen: ${truncateText(agent.last_seen_at, 40)}`);
5684
6467
  } catch (e) {
5685
6468
  return errorResult(e);
5686
6469
  }
@@ -5689,8 +6472,8 @@ Params: none`
5689
6472
  try {
5690
6473
  const agent = await getStore().setAgentFocus(args.agent_id, args.project_id ?? null);
5691
6474
  if (!agent)
5692
- return text(`Agent not found: ${args.agent_id}`);
5693
- return text(args.project_id ? `Focus set: ${args.project_id}` : "Focus cleared");
6475
+ return text(`Agent not found: ${truncateText(args.agent_id, 80)}`);
6476
+ return text(args.project_id ? `Focus set: ${truncateText(args.project_id, 80)}` : "Focus cleared");
5694
6477
  } catch (e) {
5695
6478
  return errorResult(e);
5696
6479
  }