@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
@@ -18,7 +18,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = import.meta.require;
19
19
 
20
20
  // src/version.ts
21
- var VERSION = "0.2.11";
21
+ var VERSION = "0.2.13";
22
22
 
23
23
  // src/db/remote-storage.ts
24
24
  import pg from "pg";
@@ -36,23 +36,45 @@ function sslConfigFor(connectionString) {
36
36
 
37
37
  class PgAdapterAsync {
38
38
  pool;
39
- constructor(connectionString) {
40
- this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
39
+ client;
40
+ constructor(connectionString, pool, client) {
41
+ this.pool = pool ?? new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
42
+ this.client = client ?? null;
43
+ }
44
+ query(sql, params) {
45
+ return (this.client ?? this.pool).query(translatePlaceholders(sql), normalizeParams(params));
41
46
  }
42
47
  async run(sql, ...params) {
43
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
48
+ const result = await this.query(sql, params);
44
49
  return { changes: result.rowCount ?? 0 };
45
50
  }
46
51
  async get(sql, ...params) {
47
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
52
+ const result = await this.query(sql, params);
48
53
  return result.rows[0] ?? null;
49
54
  }
50
55
  async all(sql, ...params) {
51
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
56
+ const result = await this.query(sql, params);
52
57
  return result.rows;
53
58
  }
54
59
  async exec(sql) {
55
- await this.pool.query(sql);
60
+ await (this.client ?? this.pool).query(sql);
61
+ }
62
+ async transaction(operation) {
63
+ if (this.client)
64
+ return operation(this);
65
+ const client = await this.pool.connect();
66
+ const transaction = new PgAdapterAsync("", this.pool, client);
67
+ try {
68
+ await client.query("BEGIN");
69
+ const result = await operation(transaction);
70
+ await client.query("COMMIT");
71
+ return result;
72
+ } catch (error) {
73
+ await client.query("ROLLBACK");
74
+ throw error;
75
+ } finally {
76
+ client.release();
77
+ }
56
78
  }
57
79
  async close() {
58
80
  await this.pool.end();
@@ -60,6 +82,415 @@ class PgAdapterAsync {
60
82
  }
61
83
  var init_remote_storage = () => {};
62
84
 
85
+ // src/server/cloud-config.ts
86
+ function resolveCloudDatabaseUrl(env = process.env) {
87
+ return env.HASNA_RECORDINGS_DATABASE_URL || env.RECORDINGS_DATABASE_URL || env.DATABASE_URL || undefined;
88
+ }
89
+ function resolveSigningSecret(env = process.env) {
90
+ return env.HASNA_RECORDINGS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
91
+ }
92
+ function requireSigningSecret(env = process.env) {
93
+ const signingSecret = resolveSigningSecret(env);
94
+ if (!signingSecret) {
95
+ throw new Error("Cloud /v1 auth requires a signing secret (HASNA_RECORDINGS_API_SIGNING_KEY / HASNA_API_SIGNING_KEY / API_KEY_SIGNING_SECRET).");
96
+ }
97
+ if (Buffer.byteLength(signingSecret, "utf8") < 16) {
98
+ throw new Error("Cloud /v1 auth signing secret must be at least 16 bytes.");
99
+ }
100
+ return signingSecret;
101
+ }
102
+ function isCloudModeEnabled(env = process.env) {
103
+ const mode = (env.HASNA_RECORDINGS_STORAGE_MODE || env.RECORDINGS_STORAGE_MODE || "").toLowerCase();
104
+ if (mode === "remote" || mode === "hybrid")
105
+ return true;
106
+ return Boolean(resolveCloudDatabaseUrl(env));
107
+ }
108
+
109
+ // src/server/cloud-readiness.ts
110
+ var exports_cloud_readiness = {};
111
+ __export(exports_cloud_readiness, {
112
+ pingCloudReadiness: () => pingCloudReadiness,
113
+ pingCloudConnectivity: () => pingCloudConnectivity,
114
+ assertCloudSchemaReady: () => assertCloudSchemaReady,
115
+ assertCloudSchemaContract: () => assertCloudSchemaContract
116
+ });
117
+ function sameColumns(actual, expected) {
118
+ return actual.length === expected.length && actual.every((column, index) => column === expected[index]);
119
+ }
120
+ function constraintMatches(actual, expected) {
121
+ if (!actual.is_validated || actual.is_deferrable || actual.is_deferred || actual.constraint_type !== expected.type || !sameColumns(actual.columns, [...expected.columns]))
122
+ return false;
123
+ if (expected.type !== "f") {
124
+ return actual.referenced_schema === null && actual.referenced_table === null && actual.referenced_columns.length === 0 && actual.delete_action === null && actual.update_action === null && actual.match_type === null;
125
+ }
126
+ return actual.referenced_schema === "public" && actual.referenced_table === expected.referencedTable && sameColumns(actual.referenced_columns, [...expected.referencedColumns ?? []]) && actual.delete_action === expected.deleteAction && actual.update_action === "a" && actual.match_type === "s";
127
+ }
128
+ function uniqueIndexMatches(actual, expected) {
129
+ return actual.is_primary === expected.primary && actual.is_unique && actual.is_valid && actual.is_ready && !actual.nulls_not_distinct && actual.predicate === null && sameColumns(actual.columns, [...expected.columns]);
130
+ }
131
+ async function assertCloudSchemaContract(pg2) {
132
+ const columns = await pg2.all(`SELECT table_name, column_name, data_type, is_nullable
133
+ FROM information_schema.columns
134
+ WHERE table_schema = 'public'
135
+ AND table_name = ANY (ARRAY[${REQUIRED_CLOUD_TABLES.map((table) => `'${table}'`).join(", ")}])`);
136
+ const indexedColumns = new Map(columns.map((column) => [`${column.table_name}.${column.column_name}`, column]));
137
+ for (const [table, expectedColumns] of Object.entries(REQUIRED_COLUMNS)) {
138
+ for (const [column, [dataType, nullable]] of Object.entries(expectedColumns)) {
139
+ const actual = indexedColumns.get(`${table}.${column}`);
140
+ if (!actual)
141
+ throw new Error(`cloud schema is missing required column: ${table}.${column}`);
142
+ if (actual.data_type !== dataType || actual.is_nullable === "YES" !== nullable) {
143
+ throw new Error(`cloud schema has incompatible column: ${table}.${column} must be ${dataType} ${nullable ? "NULL" : "NOT NULL"}`);
144
+ }
145
+ }
146
+ }
147
+ const idempotencyColumns = columns.filter((column) => column.table_name === "recording_idempotency");
148
+ const expectedIdempotencyColumns = Object.keys(REQUIRED_COLUMNS.recording_idempotency);
149
+ const unexpectedIdempotencyColumn = idempotencyColumns.find((column) => !expectedIdempotencyColumns.includes(column.column_name));
150
+ if (unexpectedIdempotencyColumn) {
151
+ throw new Error(`cloud schema has unexpected column: recording_idempotency.${unexpectedIdempotencyColumn.column_name}`);
152
+ }
153
+ const constraints = await pg2.all(`SELECT table_row.relname AS table_name,
154
+ constraint_row.contype::text AS constraint_type,
155
+ constraint_row.convalidated AS is_validated,
156
+ constraint_row.condeferrable AS is_deferrable,
157
+ constraint_row.condeferred AS is_deferred,
158
+ ARRAY(
159
+ SELECT attribute_row.attname
160
+ FROM unnest(constraint_row.conkey) WITH ORDINALITY AS key_row(attnum, position)
161
+ JOIN pg_attribute attribute_row
162
+ ON attribute_row.attrelid = constraint_row.conrelid
163
+ AND attribute_row.attnum = key_row.attnum
164
+ ORDER BY key_row.position
165
+ ) AS columns,
166
+ referenced_namespace.nspname AS referenced_schema,
167
+ referenced_table.relname AS referenced_table,
168
+ CASE WHEN constraint_row.confrelid = 0 THEN ARRAY[]::text[] ELSE ARRAY(
169
+ SELECT attribute_row.attname
170
+ FROM unnest(constraint_row.confkey) WITH ORDINALITY AS key_row(attnum, position)
171
+ JOIN pg_attribute attribute_row
172
+ ON attribute_row.attrelid = constraint_row.confrelid
173
+ AND attribute_row.attnum = key_row.attnum
174
+ ORDER BY key_row.position
175
+ ) END AS referenced_columns,
176
+ NULLIF(constraint_row.confdeltype::text, ' ') AS delete_action,
177
+ NULLIF(constraint_row.confupdtype::text, ' ') AS update_action,
178
+ NULLIF(constraint_row.confmatchtype::text, ' ') AS match_type
179
+ FROM pg_constraint constraint_row
180
+ JOIN pg_class table_row ON table_row.oid = constraint_row.conrelid
181
+ JOIN pg_namespace namespace_row ON namespace_row.oid = table_row.relnamespace
182
+ LEFT JOIN pg_class referenced_table ON referenced_table.oid = constraint_row.confrelid
183
+ LEFT JOIN pg_namespace referenced_namespace ON referenced_namespace.oid = referenced_table.relnamespace
184
+ WHERE namespace_row.nspname = 'public'
185
+ AND table_row.relname = ANY (ARRAY[${Object.keys(REQUIRED_CONSTRAINTS).map((table) => `'${table}'`).join(", ")}])
186
+ AND constraint_row.contype IN ('p', 'u', 'f')`);
187
+ for (const [table, expectedConstraints] of Object.entries(REQUIRED_CONSTRAINTS)) {
188
+ const actualConstraints = constraints.filter((constraint) => constraint.table_name === table);
189
+ const exact = actualConstraints.length === expectedConstraints.length && expectedConstraints.every((expected) => actualConstraints.some((actual) => constraintMatches(actual, expected)));
190
+ if (!exact) {
191
+ throw new Error(`cloud schema has incompatible ${table} constraints`);
192
+ }
193
+ }
194
+ const uniqueIndexes = await pg2.all(`SELECT table_row.relname AS table_name,
195
+ index_row.indisprimary AS is_primary,
196
+ index_row.indisunique AS is_unique,
197
+ index_row.indisvalid AS is_valid,
198
+ index_row.indisready AS is_ready,
199
+ COALESCE((to_jsonb(index_row)->>'indnullsnotdistinct')::boolean, false) AS nulls_not_distinct,
200
+ pg_get_expr(index_row.indpred, index_row.indrelid) AS predicate,
201
+ ARRAY(
202
+ SELECT attribute_row.attname
203
+ FROM unnest(index_row.indkey::smallint[]) WITH ORDINALITY AS key_row(attnum, position)
204
+ JOIN pg_attribute attribute_row
205
+ ON attribute_row.attrelid = index_row.indrelid
206
+ AND attribute_row.attnum = key_row.attnum
207
+ WHERE key_row.position <= index_row.indnkeyatts
208
+ ORDER BY key_row.position
209
+ ) AS columns
210
+ FROM pg_index index_row
211
+ JOIN pg_class table_row ON table_row.oid = index_row.indrelid
212
+ JOIN pg_namespace namespace_row ON namespace_row.oid = table_row.relnamespace
213
+ WHERE namespace_row.nspname = 'public'
214
+ AND table_row.relname = ANY (ARRAY[${Object.keys(REQUIRED_UNIQUE_INDEXES).map((table) => `'${table}'`).join(", ")}])
215
+ AND index_row.indisunique`);
216
+ for (const [table, expectedIndexes] of Object.entries(REQUIRED_UNIQUE_INDEXES)) {
217
+ const actualIndexes = uniqueIndexes.filter((index) => index.table_name === table);
218
+ const exact = actualIndexes.length === expectedIndexes.length && expectedIndexes.every((expected) => actualIndexes.some((actual) => uniqueIndexMatches(actual, expected)));
219
+ if (!exact) {
220
+ throw new Error(`cloud schema has incompatible ${table} unique indexes`);
221
+ }
222
+ }
223
+ }
224
+ async function assertCloudSchemaReady(pg2) {
225
+ await assertCloudSchemaContract(pg2);
226
+ const row = await pg2.get(`SELECT
227
+ COALESCE((SELECT role_row.rolsuper FROM pg_roles role_row WHERE role_row.rolname = current_user), false) AS is_superuser,
228
+ COALESCE((SELECT role_row.rolcreaterole FROM pg_roles role_row WHERE role_row.rolname = current_user), false) AS can_create_role,
229
+ COALESCE((SELECT role_row.rolcreatedb FROM pg_roles role_row WHERE role_row.rolname = current_user), false) AS can_create_database,
230
+ COALESCE((SELECT role_row.rolreplication FROM pg_roles role_row WHERE role_row.rolname = current_user), false) AS can_replicate,
231
+ COALESCE((SELECT role_row.rolbypassrls FROM pg_roles role_row WHERE role_row.rolname = current_user), false) AS can_bypass_rls,
232
+ has_database_privilege(current_user, current_database(), 'CREATE') AS can_create_in_database,
233
+ has_database_privilege(current_user, current_database(), 'TEMPORARY') AS can_create_temporary_tables,
234
+ EXISTS (
235
+ SELECT 1 FROM pg_namespace schema_row
236
+ WHERE schema_row.nspname !~ '^pg_'
237
+ AND schema_row.nspname <> 'information_schema'
238
+ AND pg_has_role(current_user, schema_row.nspowner, 'MEMBER')
239
+ ) AS owns_user_schema,
240
+ EXISTS (
241
+ SELECT 1 FROM pg_namespace schema_row
242
+ WHERE schema_row.nspname !~ '^pg_'
243
+ AND schema_row.nspname <> 'information_schema'
244
+ AND has_schema_privilege(current_user, schema_row.oid, 'CREATE')
245
+ ) AS can_create_in_user_schema,
246
+ has_schema_privilege(current_user, 'public', 'USAGE') AS can_use_public,
247
+ EXISTS (
248
+ SELECT 1
249
+ FROM pg_class table_row
250
+ JOIN pg_namespace schema_row ON schema_row.oid = table_row.relnamespace
251
+ WHERE schema_row.nspname !~ '^pg_'
252
+ AND schema_row.nspname <> 'information_schema'
253
+ AND table_row.relkind IN ('r', 'p')
254
+ AND pg_has_role(current_user, table_row.relowner, 'MEMBER')
255
+ ) AS owns_user_table,
256
+ has_table_privilege(current_user, 'public.recordings', 'SELECT') AND
257
+ has_table_privilege(current_user, 'public.recordings', 'INSERT') AND
258
+ has_table_privilege(current_user, 'public.recordings', 'DELETE') AS can_recordings,
259
+ has_table_privilege(current_user, 'public.recordings', 'UPDATE') OR
260
+ has_any_column_privilege(current_user, 'public.recordings', 'UPDATE') OR
261
+ has_table_privilege(current_user, 'public.recordings', 'TRUNCATE') OR
262
+ has_table_privilege(current_user, 'public.recordings', 'REFERENCES') OR
263
+ has_any_column_privilege(current_user, 'public.recordings', 'REFERENCES') OR
264
+ has_table_privilege(current_user, 'public.recordings', 'TRIGGER') AS has_extra_recordings_privileges,
265
+ has_table_privilege(current_user, 'public.recording_tags', 'SELECT') AND
266
+ has_table_privilege(current_user, 'public.recording_tags', 'INSERT') AS can_recording_tags,
267
+ has_table_privilege(current_user, 'public.recording_tags', 'UPDATE') OR
268
+ has_any_column_privilege(current_user, 'public.recording_tags', 'UPDATE') OR
269
+ has_table_privilege(current_user, 'public.recording_tags', 'DELETE') OR
270
+ has_table_privilege(current_user, 'public.recording_tags', 'TRUNCATE') OR
271
+ has_table_privilege(current_user, 'public.recording_tags', 'REFERENCES') OR
272
+ has_any_column_privilege(current_user, 'public.recording_tags', 'REFERENCES') OR
273
+ has_table_privilege(current_user, 'public.recording_tags', 'TRIGGER') AS has_extra_recording_tags_privileges,
274
+ has_table_privilege(current_user, 'public.agents', 'SELECT') AND
275
+ has_table_privilege(current_user, 'public.agents', 'INSERT') AND
276
+ has_table_privilege(current_user, 'public.agents', 'UPDATE') AS can_agents,
277
+ has_table_privilege(current_user, 'public.agents', 'DELETE') OR
278
+ has_table_privilege(current_user, 'public.agents', 'TRUNCATE') OR
279
+ has_table_privilege(current_user, 'public.agents', 'REFERENCES') OR
280
+ has_any_column_privilege(current_user, 'public.agents', 'REFERENCES') OR
281
+ has_table_privilege(current_user, 'public.agents', 'TRIGGER') AS has_extra_agents_privileges,
282
+ has_table_privilege(current_user, 'public.projects', 'SELECT') AND
283
+ has_table_privilege(current_user, 'public.projects', 'INSERT') AND
284
+ has_table_privilege(current_user, 'public.projects', 'UPDATE') AS can_projects,
285
+ has_table_privilege(current_user, 'public.projects', 'DELETE') OR
286
+ has_table_privilege(current_user, 'public.projects', 'TRUNCATE') OR
287
+ has_table_privilege(current_user, 'public.projects', 'REFERENCES') OR
288
+ has_any_column_privilege(current_user, 'public.projects', 'REFERENCES') OR
289
+ has_table_privilege(current_user, 'public.projects', 'TRIGGER') AS has_extra_projects_privileges,
290
+ has_table_privilege(current_user, 'public.feedback', 'INSERT') AS can_feedback,
291
+ has_table_privilege(current_user, 'public.feedback', 'SELECT') OR
292
+ has_any_column_privilege(current_user, 'public.feedback', 'SELECT') OR
293
+ has_table_privilege(current_user, 'public.feedback', 'UPDATE') OR
294
+ has_any_column_privilege(current_user, 'public.feedback', 'UPDATE') OR
295
+ has_table_privilege(current_user, 'public.feedback', 'DELETE') OR
296
+ has_table_privilege(current_user, 'public.feedback', 'TRUNCATE') OR
297
+ has_table_privilege(current_user, 'public.feedback', 'REFERENCES') OR
298
+ has_any_column_privilege(current_user, 'public.feedback', 'REFERENCES') OR
299
+ has_table_privilege(current_user, 'public.feedback', 'TRIGGER') AS has_extra_feedback_privileges,
300
+ has_table_privilege(current_user, 'public.api_keys', 'SELECT') AS can_api_keys,
301
+ has_table_privilege(current_user, 'public.api_keys', 'INSERT') OR
302
+ has_any_column_privilege(current_user, 'public.api_keys', 'INSERT') OR
303
+ has_table_privilege(current_user, 'public.api_keys', 'UPDATE') OR
304
+ has_any_column_privilege(current_user, 'public.api_keys', 'UPDATE') OR
305
+ has_table_privilege(current_user, 'public.api_keys', 'DELETE') OR
306
+ has_table_privilege(current_user, 'public.api_keys', 'TRUNCATE') OR
307
+ has_table_privilege(current_user, 'public.api_keys', 'REFERENCES') OR
308
+ has_any_column_privilege(current_user, 'public.api_keys', 'REFERENCES') OR
309
+ has_table_privilege(current_user, 'public.api_keys', 'TRIGGER') AS has_extra_api_keys_privileges,
310
+ has_table_privilege(current_user, 'public.recording_idempotency', 'SELECT') AND
311
+ has_table_privilege(current_user, 'public.recording_idempotency', 'INSERT') AS can_recording_idempotency,
312
+ has_table_privilege(current_user, 'public.recording_idempotency', 'UPDATE') OR
313
+ has_any_column_privilege(current_user, 'public.recording_idempotency', 'UPDATE') OR
314
+ has_table_privilege(current_user, 'public.recording_idempotency', 'DELETE') OR
315
+ has_table_privilege(current_user, 'public.recording_idempotency', 'TRUNCATE') OR
316
+ has_table_privilege(current_user, 'public.recording_idempotency', 'REFERENCES') OR
317
+ has_any_column_privilege(current_user, 'public.recording_idempotency', 'REFERENCES') OR
318
+ has_table_privilege(current_user, 'public.recording_idempotency', 'TRIGGER') AS has_extra_recording_idempotency_privileges`);
319
+ if (row?.is_superuser || row?.can_create_role || row?.can_create_database || row?.can_replicate || row?.can_bypass_rls || row?.can_create_in_database || row?.can_create_temporary_tables || row?.owns_user_schema || row?.can_create_in_user_schema || row?.owns_user_table) {
320
+ throw new Error("cloud database request role violates the DML-only privilege contract");
321
+ }
322
+ if (!row?.can_use_public) {
323
+ throw new Error("cloud database role lacks required public schema USAGE privilege");
324
+ }
325
+ const denied = REQUIRED_CLOUD_TABLES.filter((table) => !row?.[`can_${table}`]);
326
+ if (denied.length > 0) {
327
+ throw new Error(`cloud database role lacks required DML privileges: ${denied.join(", ")}`);
328
+ }
329
+ const excessive = REQUIRED_CLOUD_TABLES.filter((table) => row?.[`has_extra_${table}_privileges`]);
330
+ if (excessive.length > 0) {
331
+ throw new Error(`cloud database role has surplus table privileges beyond runtime use: ${excessive.join(", ")}`);
332
+ }
333
+ }
334
+ async function pingCloudConnectivity(pg2) {
335
+ const res = await pg2.get("SELECT 1 as ok");
336
+ if (Number(res?.ok) !== 1)
337
+ throw new Error("cloud database ping failed");
338
+ return true;
339
+ }
340
+ async function pingCloudReadiness(pg2) {
341
+ await pingCloudConnectivity(pg2);
342
+ await assertCloudSchemaReady(pg2);
343
+ return true;
344
+ }
345
+ var REQUIRED_COLUMNS, REQUIRED_CLOUD_TABLES, REQUIRED_CONSTRAINTS, REQUIRED_UNIQUE_INDEXES;
346
+ var init_cloud_readiness = __esm(() => {
347
+ REQUIRED_COLUMNS = {
348
+ projects: {
349
+ id: ["text", false],
350
+ name: ["text", false],
351
+ path: ["text", false],
352
+ description: ["text", true],
353
+ created_at: ["text", false],
354
+ updated_at: ["text", false]
355
+ },
356
+ agents: {
357
+ id: ["text", false],
358
+ name: ["text", false],
359
+ description: ["text", true],
360
+ role: ["text", true],
361
+ metadata: ["text", true],
362
+ created_at: ["text", false],
363
+ last_seen_at: ["text", false],
364
+ active_project_id: ["text", true]
365
+ },
366
+ recordings: {
367
+ id: ["text", false],
368
+ audio_path: ["text", true],
369
+ raw_text: ["text", false],
370
+ processed_text: ["text", true],
371
+ processing_mode: ["text", false],
372
+ model_used: ["text", false],
373
+ enhancement_model: ["text", true],
374
+ duration_ms: ["integer", true],
375
+ language: ["text", true],
376
+ tags: ["text", true],
377
+ agent_id: ["text", true],
378
+ project_id: ["text", true],
379
+ session_id: ["text", true],
380
+ goal: ["text", true],
381
+ role: ["text", true],
382
+ task_list_id: ["text", true],
383
+ machine_id: ["text", true],
384
+ metadata: ["text", true],
385
+ created_at: ["text", false]
386
+ },
387
+ recording_tags: { recording_id: ["text", false], tag: ["text", false] },
388
+ feedback: {
389
+ id: ["text", false],
390
+ message: ["text", false],
391
+ email: ["text", true],
392
+ category: ["text", true],
393
+ version: ["text", true],
394
+ machine_id: ["text", true],
395
+ created_at: ["text", false]
396
+ },
397
+ api_keys: {
398
+ kid: ["text", false],
399
+ app: ["text", false],
400
+ agent: ["text", true],
401
+ scopes: ["jsonb", false],
402
+ token_hash: ["text", false],
403
+ issued_at: ["timestamp with time zone", false],
404
+ expires_at: ["timestamp with time zone", true],
405
+ revoked_at: ["timestamp with time zone", true],
406
+ revoked_reason: ["text", true],
407
+ last_used_at: ["timestamp with time zone", true],
408
+ created_by: ["text", true],
409
+ created_at: ["timestamp with time zone", false]
410
+ },
411
+ recording_idempotency: {
412
+ principal: ["text", false],
413
+ idempotency_key: ["text", false],
414
+ request_fingerprint: ["text", false],
415
+ recording_id: ["text", true],
416
+ created_at: ["text", false]
417
+ }
418
+ };
419
+ REQUIRED_CLOUD_TABLES = Object.keys(REQUIRED_COLUMNS);
420
+ REQUIRED_CONSTRAINTS = {
421
+ projects: [
422
+ { type: "p", columns: ["id"] },
423
+ { type: "u", columns: ["path"] }
424
+ ],
425
+ agents: [
426
+ { type: "p", columns: ["id"] },
427
+ { type: "u", columns: ["name"] },
428
+ {
429
+ type: "f",
430
+ columns: ["active_project_id"],
431
+ referencedTable: "projects",
432
+ referencedColumns: ["id"],
433
+ deleteAction: "n"
434
+ }
435
+ ],
436
+ recordings: [
437
+ { type: "p", columns: ["id"] },
438
+ {
439
+ type: "f",
440
+ columns: ["agent_id"],
441
+ referencedTable: "agents",
442
+ referencedColumns: ["id"],
443
+ deleteAction: "n"
444
+ },
445
+ {
446
+ type: "f",
447
+ columns: ["project_id"],
448
+ referencedTable: "projects",
449
+ referencedColumns: ["id"],
450
+ deleteAction: "n"
451
+ }
452
+ ],
453
+ recording_tags: [
454
+ { type: "p", columns: ["recording_id", "tag"] },
455
+ {
456
+ type: "f",
457
+ columns: ["recording_id"],
458
+ referencedTable: "recordings",
459
+ referencedColumns: ["id"],
460
+ deleteAction: "c"
461
+ }
462
+ ],
463
+ feedback: [{ type: "p", columns: ["id"] }],
464
+ api_keys: [
465
+ { type: "p", columns: ["kid"] },
466
+ { type: "u", columns: ["token_hash"] }
467
+ ],
468
+ recording_idempotency: [
469
+ { type: "p", columns: ["principal", "idempotency_key"] },
470
+ { type: "u", columns: ["recording_id"] },
471
+ {
472
+ type: "f",
473
+ columns: ["recording_id"],
474
+ referencedTable: "recordings",
475
+ referencedColumns: ["id"],
476
+ deleteAction: "n"
477
+ }
478
+ ]
479
+ };
480
+ REQUIRED_UNIQUE_INDEXES = {
481
+ projects: [{ primary: true, columns: ["id"] }, { primary: false, columns: ["path"] }],
482
+ agents: [{ primary: true, columns: ["id"] }, { primary: false, columns: ["name"] }],
483
+ recordings: [{ primary: true, columns: ["id"] }],
484
+ recording_tags: [{ primary: true, columns: ["recording_id", "tag"] }],
485
+ feedback: [{ primary: true, columns: ["id"] }],
486
+ api_keys: [{ primary: true, columns: ["kid"] }, { primary: false, columns: ["token_hash"] }],
487
+ recording_idempotency: [
488
+ { primary: true, columns: ["principal", "idempotency_key"] },
489
+ { primary: false, columns: ["recording_id"] }
490
+ ]
491
+ };
492
+ });
493
+
63
494
  // src/db/pg-migrations.ts
64
495
  var PG_MIGRATIONS;
65
496
  var init_pg_migrations = __esm(() => {
@@ -127,16 +558,61 @@ var init_pg_migrations = __esm(() => {
127
558
  machine_id TEXT,
128
559
  created_at TEXT NOT NULL DEFAULT NOW()::text
129
560
  )`,
130
- `ALTER TABLE agents ADD COLUMN IF NOT EXISTS active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
561
+ `ALTER TABLE agents ADD COLUMN IF NOT EXISTS active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`,
562
+ `CREATE TABLE IF NOT EXISTS recording_idempotency (
563
+ principal TEXT NOT NULL,
564
+ idempotency_key TEXT NOT NULL,
565
+ request_fingerprint TEXT NOT NULL,
566
+ recording_id TEXT NOT NULL UNIQUE REFERENCES recordings(id) ON DELETE CASCADE,
567
+ created_at TEXT NOT NULL DEFAULT NOW()::text,
568
+ PRIMARY KEY (principal, idempotency_key)
569
+ )`,
570
+ `ALTER TABLE recording_idempotency
571
+ DROP CONSTRAINT IF EXISTS recording_idempotency_recording_id_fkey;
572
+ ALTER TABLE recording_idempotency
573
+ ALTER COLUMN recording_id DROP NOT NULL;
574
+ ALTER TABLE recording_idempotency
575
+ ADD CONSTRAINT recording_idempotency_recording_id_fkey
576
+ FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE SET NULL`
131
577
  ];
132
578
  });
133
579
 
580
+ // src/server/migrate-command.ts
581
+ var exports_migrate_command = {};
582
+ __export(exports_migrate_command, {
583
+ runCloudMigration: () => runCloudMigration,
584
+ applyRecordedCloudMigrations: () => applyRecordedCloudMigrations
585
+ });
586
+ async function applyRecordedCloudMigrations(pg2, ensureApiKeySchema) {
587
+ await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (id SERIAL PRIMARY KEY, version INT UNIQUE NOT NULL, applied_at TIMESTAMPTZ DEFAULT NOW())`);
588
+ const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
589
+ const appliedSet = new Set(applied.map((row) => Number(row.version)));
590
+ for (let version = 0;version < PG_MIGRATIONS.length; version++) {
591
+ if (appliedSet.has(version))
592
+ continue;
593
+ await pg2.exec(PG_MIGRATIONS[version]);
594
+ await pg2.run("INSERT INTO _pg_migrations (version) VALUES (?) ON CONFLICT DO NOTHING", version);
595
+ }
596
+ await ensureApiKeySchema();
597
+ }
598
+ async function runCloudMigration(steps) {
599
+ await steps.pingConnectivity();
600
+ await steps.applyMigrations();
601
+ await steps.validateContract();
602
+ }
603
+ var init_migrate_command = __esm(() => {
604
+ init_pg_migrations();
605
+ });
606
+
134
607
  // src/server/cloud.ts
135
608
  var exports_cloud = {};
136
609
  __export(exports_cloud, {
137
610
  resolveSigningSecret: () => resolveSigningSecret,
138
611
  resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
612
+ requireSigningSecret: () => requireSigningSecret,
613
+ pingCloudConnectivity: () => pingCloudConnectivity2,
139
614
  pingCloud: () => pingCloud,
615
+ migrateCloudSchema: () => migrateCloudSchema,
140
616
  isCloudModeEnabled: () => isCloudModeEnabled,
141
617
  getCloudVerifier: () => getCloudVerifier,
142
618
  getCloudPg: () => getCloudPg,
@@ -147,18 +623,6 @@ __export(exports_cloud, {
147
623
  });
148
624
  import { verifyApiKey } from "@hasna/contracts/auth";
149
625
  import { ApiKeyStore } from "@hasna/contracts/auth";
150
- function resolveCloudDatabaseUrl(env = process.env) {
151
- return env.HASNA_RECORDINGS_DATABASE_URL || env.RECORDINGS_DATABASE_URL || env.DATABASE_URL || undefined;
152
- }
153
- function resolveSigningSecret(env = process.env) {
154
- return env.HASNA_RECORDINGS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
155
- }
156
- function isCloudModeEnabled(env = process.env) {
157
- const mode = (env.HASNA_RECORDINGS_STORAGE_MODE || env.RECORDINGS_STORAGE_MODE || "").toLowerCase();
158
- if (mode === "remote" || mode === "hybrid")
159
- return true;
160
- return Boolean(resolveCloudDatabaseUrl(env));
161
- }
162
626
  function getCloudPg() {
163
627
  if (cachedPg)
164
628
  return cachedPg;
@@ -192,10 +656,7 @@ function getApiKeyStore() {
192
656
  function getCloudVerifier() {
193
657
  if (cachedVerifier)
194
658
  return cachedVerifier;
195
- const signingSecret = resolveSigningSecret();
196
- if (!signingSecret) {
197
- throw new Error("Cloud /v1 auth requires a signing secret (HASNA_RECORDINGS_API_SIGNING_KEY / HASNA_API_SIGNING_KEY / API_KEY_SIGNING_SECRET).");
198
- }
659
+ const signingSecret = requireSigningSecret();
199
660
  const store = getApiKeyStore();
200
661
  cachedVerifier = verifyApiKey({
201
662
  app: RECORDINGS_APP_SLUG,
@@ -204,48 +665,23 @@ function getCloudVerifier() {
204
665
  });
205
666
  return cachedVerifier;
206
667
  }
207
- function isPrivilegeError(e) {
208
- const msg = e instanceof Error ? e.message.toLowerCase() : String(e).toLowerCase();
209
- return msg.includes("permission denied") || msg.includes("must be owner") || msg.includes("insufficient privilege");
210
- }
211
- async function requiredTablesExist() {
212
- const pg2 = getCloudPg();
213
- const row = await pg2.get(`SELECT
214
- (to_regclass('public.recordings') IS NOT NULL) AS has_recordings,
215
- (to_regclass('public.api_keys') IS NOT NULL) AS has_api_keys`);
216
- return Boolean(row?.has_recordings && row?.has_api_keys);
668
+ async function migrateCloudSchema() {
669
+ await applyRecordedCloudMigrations(getCloudPg(), () => getApiKeyStore().ensureSchema());
217
670
  }
218
671
  async function ensureCloudSchema() {
219
672
  if (schemaEnsured)
220
673
  return schemaEnsured;
221
- schemaEnsured = (async () => {
222
- const pg2 = getCloudPg();
223
- try {
224
- await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (id SERIAL PRIMARY KEY, version INT UNIQUE NOT NULL, applied_at TIMESTAMPTZ DEFAULT NOW())`);
225
- const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
226
- const appliedSet = new Set(applied.map((r) => Number(r.version)));
227
- for (let i = 0;i < PG_MIGRATIONS.length; i++) {
228
- if (appliedSet.has(i))
229
- continue;
230
- await pg2.exec(PG_MIGRATIONS[i]);
231
- await pg2.run("INSERT INTO _pg_migrations (version) VALUES (?) ON CONFLICT DO NOTHING", i);
232
- }
233
- await getApiKeyStore().ensureSchema();
234
- } catch (e) {
235
- if (isPrivilegeError(e) && await requiredTablesExist()) {
236
- console.warn("recordings-serve: schema is externally managed (owner-applied); the request-path role lacks DDL \u2014 continuing.");
237
- return;
238
- }
239
- schemaEnsured = null;
240
- throw e;
241
- }
242
- })();
674
+ schemaEnsured = assertCloudSchemaReady(getCloudPg()).catch((error) => {
675
+ schemaEnsured = null;
676
+ throw error;
677
+ });
243
678
  return schemaEnsured;
244
679
  }
245
- async function pingCloud() {
246
- const pg2 = getCloudPg();
247
- const res = await pg2.get("SELECT 1 as ok");
248
- return Number(res?.ok) === 1;
680
+ async function pingCloudConnectivity2(pg2 = getCloudPg()) {
681
+ return pingCloudConnectivity(pg2);
682
+ }
683
+ async function pingCloud(pg2 = getCloudPg()) {
684
+ return pingCloudReadiness(pg2);
249
685
  }
250
686
  async function closeCloud() {
251
687
  if (cachedPg)
@@ -258,7 +694,8 @@ async function closeCloud() {
258
694
  var RECORDINGS_APP_SLUG = "recordings", cachedPg = null, cachedStore = null, cachedVerifier = null, schemaEnsured = null;
259
695
  var init_cloud = __esm(() => {
260
696
  init_remote_storage();
261
- init_pg_migrations();
697
+ init_cloud_readiness();
698
+ init_migrate_command();
262
699
  });
263
700
 
264
701
  // src/db/errors.ts
@@ -280,6 +717,81 @@ var init_errors = __esm(() => {
280
717
  };
281
718
  });
282
719
 
720
+ // src/lib/recording-create-identity.ts
721
+ import { createHash } from "crypto";
722
+ function canonicalJson(value) {
723
+ if (value === null || typeof value !== "object")
724
+ return JSON.stringify(value);
725
+ if (Array.isArray(value))
726
+ return `[${value.map(canonicalJson).join(",")}]`;
727
+ const object = value;
728
+ return `{${Object.keys(object).filter((key) => object[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
729
+ }
730
+ function recordingCreateFingerprint(input) {
731
+ const persisted = {
732
+ id: input.id ?? null,
733
+ audio_path: input.audio_path || null,
734
+ raw_text: input.raw_text,
735
+ processed_text: input.processed_text || null,
736
+ processing_mode: input.processing_mode || "raw",
737
+ model_used: input.model_used || "gpt-4o-transcribe",
738
+ enhancement_model: input.enhancement_model || null,
739
+ duration_ms: input.duration_ms || 0,
740
+ language: input.language || null,
741
+ tags: input.tags || [],
742
+ agent_id: input.agent_id || null,
743
+ project_id: input.project_id || null,
744
+ session_id: input.session_id || null,
745
+ goal: input.goal || null,
746
+ role: input.role || null,
747
+ task_list_id: input.task_list_id || null,
748
+ machine_id: input.machine_id || null,
749
+ metadata: input.metadata || {}
750
+ };
751
+ return createHash("sha256").update(canonicalJson(persisted), "utf8").digest("hex");
752
+ }
753
+ function validateIdentityValue(value, label, nullMeansAbsent = false) {
754
+ if (value === undefined || nullMeansAbsent && value === null)
755
+ return;
756
+ if (typeof value !== "string") {
757
+ throw new ValidationError(`${label} must be a string`);
758
+ }
759
+ if (value.length === 0) {
760
+ throw new ValidationError(`${label} must not be empty`);
761
+ }
762
+ if (value.length > MAX_RECORDING_IDENTITY_LENGTH) {
763
+ throw new ValidationError(`${label} must not exceed ${MAX_RECORDING_IDENTITY_LENGTH} characters`);
764
+ }
765
+ if (value !== value.trim()) {
766
+ throw new ValidationError(`${label} must not contain leading or trailing whitespace`);
767
+ }
768
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
769
+ throw new ValidationError(`${label} must not contain control characters`);
770
+ }
771
+ if (/[^\u0020-\u007e]/.test(value)) {
772
+ throw new ValidationError(`${label} must contain only printable ASCII characters`);
773
+ }
774
+ return value;
775
+ }
776
+ function recordingCreateIdentity(input, idempotencyKey, options = {}) {
777
+ const bodyId = validateIdentityValue(input.id, "recording id", true);
778
+ const headerKey = validateIdentityValue(idempotencyKey, "idempotency key");
779
+ if (bodyId !== undefined && headerKey !== undefined && bodyId !== headerKey) {
780
+ throw new ValidationError("recording id conflicts with idempotency key");
781
+ }
782
+ const effectiveKey = headerKey ?? bodyId;
783
+ const { id: _runtimeId, ...inputWithoutId } = input;
784
+ const persistedId = options.bindIdempotencyKeyToId === false ? bodyId : effectiveKey;
785
+ return {
786
+ input: persistedId === undefined ? inputWithoutId : { ...inputWithoutId, id: persistedId },
787
+ idempotencyKey: effectiveKey
788
+ };
789
+ }
790
+ var MAX_RECORDING_IDENTITY_LENGTH = 255;
791
+ var init_recording_create_identity = __esm(() => {
792
+ init_errors();
793
+ });
794
+
283
795
  // src/server/repo.ts
284
796
  function shortUuid() {
285
797
  return crypto.randomUUID().slice(0, 8);
@@ -339,34 +851,83 @@ function parseProject(row) {
339
851
  updated_at: row["updated_at"]
340
852
  };
341
853
  }
342
- async function createRecording(pg2, input) {
854
+ async function createRecording(pg2, input, idempotencyKey, idempotencyContext) {
343
855
  if (typeof input.raw_text !== "string" || !input.raw_text) {
344
856
  throw new ValidationError("raw_text is required");
345
857
  }
346
- let resolvedAgentId = null;
347
- if (input.agent_id) {
348
- const agent = await getAgent(pg2, input.agent_id);
349
- resolvedAgentId = agent ? agent.id : (await registerAgent(pg2, input.agent_id)).id;
350
- }
351
- let resolvedProjectId = null;
352
- if (input.project_id) {
353
- const project = await getProject(pg2, input.project_id);
354
- if (!project)
355
- throw new ProjectNotFoundError(input.project_id);
356
- resolvedProjectId = project.id;
357
- }
358
- const id = shortUuid();
359
- await pg2.run(`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)
360
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 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, JSON.stringify(input.tags || []), resolvedAgentId, resolvedProjectId, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, JSON.stringify(input.metadata || {}));
361
- if (input.tags && input.tags.length > 0) {
362
- for (const tag of input.tags) {
363
- await pg2.run("INSERT INTO recording_tags (recording_id, tag) VALUES (?, ?) ON CONFLICT DO NOTHING", id, tag);
364
- }
365
- }
366
- const created = await getRecording(pg2, id);
367
- if (!created)
368
- throw new Error("failed to read back created recording");
369
- return created;
858
+ const identity = recordingCreateIdentity(input, idempotencyKey, {
859
+ bindIdempotencyKeyToId: false
860
+ });
861
+ input = identity.input;
862
+ const id = input.id || crypto.randomUUID();
863
+ const effectiveKey = identity.idempotencyKey;
864
+ if (effectiveKey !== undefined && !idempotencyContext?.principal) {
865
+ throw new ValidationError("idempotency principal is required");
866
+ }
867
+ const principal = idempotencyContext?.principal;
868
+ const requestFingerprint = recordingCreateFingerprint(input);
869
+ return pg2.transaction(async (transaction) => {
870
+ if (effectiveKey !== undefined && principal !== undefined) {
871
+ const lockRef = JSON.stringify([principal, effectiveKey]);
872
+ await transaction.get("SELECT pg_advisory_xact_lock(hashtextextended(?, 0))", lockRef);
873
+ const replay = await transaction.get(`SELECT request_fingerprint, recording_id
874
+ FROM recording_idempotency
875
+ WHERE principal = ? AND idempotency_key = ?`, principal, effectiveKey);
876
+ if (replay) {
877
+ if (replay.request_fingerprint !== requestFingerprint) {
878
+ throw new IdempotencyConflictError;
879
+ }
880
+ if (replay.recording_id === null)
881
+ throw new IdempotencyConflictError;
882
+ const existing = await getRecordingExact(transaction, replay.recording_id);
883
+ if (!existing)
884
+ throw new IdempotencyConflictError;
885
+ return existing;
886
+ }
887
+ if (await getRecordingExact(transaction, id)) {
888
+ throw new IdempotencyConflictError;
889
+ }
890
+ }
891
+ let resolvedAgentId = null;
892
+ if (input.agent_id) {
893
+ const agent = await getAgent(transaction, input.agent_id);
894
+ resolvedAgentId = agent ? agent.id : (await registerAgent(transaction, input.agent_id)).id;
895
+ }
896
+ let resolvedProjectId = null;
897
+ if (input.project_id) {
898
+ const project = await getProject(transaction, input.project_id);
899
+ if (!project)
900
+ throw new ProjectNotFoundError(input.project_id);
901
+ resolvedProjectId = project.id;
902
+ }
903
+ const insertResult = await transaction.run(`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)
904
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
905
+ ON CONFLICT(id) DO NOTHING`, 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, JSON.stringify(input.tags || []), resolvedAgentId, resolvedProjectId, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, JSON.stringify(input.metadata || {}));
906
+ if (insertResult.changes === 0) {
907
+ throw new IdempotencyConflictError;
908
+ }
909
+ if (input.tags && input.tags.length > 0) {
910
+ for (const tag of input.tags) {
911
+ await transaction.run("INSERT INTO recording_tags (recording_id, tag) VALUES (?, ?) ON CONFLICT DO NOTHING", id, tag);
912
+ }
913
+ }
914
+ if (effectiveKey !== undefined && principal !== undefined) {
915
+ const ledgerInsert = await transaction.run(`INSERT INTO recording_idempotency
916
+ (principal, idempotency_key, request_fingerprint, recording_id)
917
+ VALUES (?, ?, ?, ?)
918
+ ON CONFLICT DO NOTHING`, principal, effectiveKey, requestFingerprint, id);
919
+ if (ledgerInsert.changes === 0)
920
+ throw new IdempotencyConflictError;
921
+ }
922
+ const created = await getRecordingExact(transaction, id);
923
+ if (!created)
924
+ throw new Error("failed to read back created recording");
925
+ return created;
926
+ });
927
+ }
928
+ async function getRecordingExact(pg2, id) {
929
+ const row = await pg2.get("SELECT * FROM recordings WHERE id = ?", id);
930
+ return row ? parseRecording(row) : null;
370
931
  }
371
932
  async function getRecording(pg2, id) {
372
933
  let row = await pg2.get("SELECT * FROM recordings WHERE id = ?", id);
@@ -376,6 +937,19 @@ async function getRecording(pg2, id) {
376
937
  return row ? parseRecording(row) : null;
377
938
  }
378
939
  async function listRecordings(pg2, filter) {
940
+ const { where, params } = buildRecordingWhere(filter);
941
+ const limit = Math.min(Math.max(Number(filter?.limit) || 50, 1), 500);
942
+ const offset = Math.max(Number(filter?.offset) || 0, 0);
943
+ const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
944
+ const rows = await pg2.all(sql, ...params, limit, offset);
945
+ return rows.map(parseRecording);
946
+ }
947
+ async function countRecordings(pg2, filter) {
948
+ const { where, params } = buildRecordingWhere(filter);
949
+ const row = await pg2.get(`SELECT COUNT(*) as c FROM recordings ${where}`, ...params);
950
+ return Number(row.c);
951
+ }
952
+ function buildRecordingWhere(filter) {
379
953
  const conditions = [];
380
954
  const params = [];
381
955
  if (filter?.agent_id) {
@@ -414,12 +988,7 @@ async function listRecordings(pg2, filter) {
414
988
  params.push(filter.until);
415
989
  }
416
990
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
417
- const limit = Math.min(Math.max(Number(filter?.limit) || 50, 1), 500);
418
- const offset = Math.max(Number(filter?.offset) || 0, 0);
419
- params.push(limit, offset);
420
- const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
421
- const rows = await pg2.all(sql, ...params);
422
- return rows.map(parseRecording);
991
+ return { where, params };
423
992
  }
424
993
  async function deleteRecording(pg2, id) {
425
994
  const existing = await getRecording(pg2, id);
@@ -524,8 +1093,16 @@ async function saveFeedback(pg2, input) {
524
1093
  await pg2.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", input.message, input.email || null, input.category || "general", input.version || null);
525
1094
  return { saved: true };
526
1095
  }
1096
+ var IdempotencyConflictError;
527
1097
  var init_repo = __esm(() => {
528
1098
  init_errors();
1099
+ init_recording_create_identity();
1100
+ IdempotencyConflictError = class IdempotencyConflictError extends Error {
1101
+ constructor() {
1102
+ super("idempotency key is already in use");
1103
+ this.name = "IdempotencyConflictError";
1104
+ }
1105
+ };
529
1106
  });
530
1107
 
531
1108
  // src/server/v1.ts
@@ -552,22 +1129,23 @@ async function handleV1Request(req, url) {
552
1129
  const method = req.method.toUpperCase();
553
1130
  const isWrite = method !== "GET" && method !== "HEAD";
554
1131
  const requiredScopes = [isWrite ? "recordings:write" : "recordings:read"];
555
- let verifier;
1132
+ let decision;
556
1133
  try {
557
- verifier = getCloudVerifier();
558
- } catch (e) {
559
- return error(503, e.message);
1134
+ const verifier = getCloudVerifier();
1135
+ decision = await verifier.authenticate(req.headers, { method, path, requiredScopes });
1136
+ } catch {
1137
+ return error(503, "authentication service unavailable");
560
1138
  }
561
- const decision = await verifier.authenticate(req.headers, { method, path, requiredScopes });
562
1139
  if (!decision.ok) {
563
1140
  return error(decision.status, decision.message, { reason: decision.reason });
564
1141
  }
1142
+ let pg2;
565
1143
  try {
566
1144
  await ensureCloudSchema();
567
- } catch (e) {
568
- return error(503, `storage unavailable: ${e.message}`);
1145
+ pg2 = getCloudPg();
1146
+ } catch {
1147
+ return error(503, "storage unavailable");
569
1148
  }
570
- const pg2 = getCloudPg();
571
1149
  const segments = path.split("/").filter(Boolean);
572
1150
  const resource = segments[1];
573
1151
  const id = segments[2] ? decodeURIComponent(segments[2]) : undefined;
@@ -581,19 +1159,25 @@ async function handleV1Request(req, url) {
581
1159
  ...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
582
1160
  ...url.searchParams.get("session_id") ? { session_id: url.searchParams.get("session_id") } : {},
583
1161
  ...url.searchParams.get("processing_mode") ? { processing_mode: url.searchParams.get("processing_mode") } : {},
1162
+ ...url.searchParams.getAll("tags").length > 0 ? { tags: url.searchParams.getAll("tags") } : {},
584
1163
  ...url.searchParams.get("search") ? { search: url.searchParams.get("search") } : {},
1164
+ ...url.searchParams.get("since") ? { since: url.searchParams.get("since") } : {},
1165
+ ...url.searchParams.get("until") ? { until: url.searchParams.get("until") } : {},
585
1166
  ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
586
1167
  ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
587
1168
  };
588
- const recordings = await listRecordings(pg2, filter);
589
- return json({ recordings, count: recordings.length });
1169
+ const [recordings, count] = await Promise.all([
1170
+ listRecordings(pg2, filter),
1171
+ countRecordings(pg2, filter)
1172
+ ]);
1173
+ return json({ recordings, count });
590
1174
  }
591
1175
  if (method === "POST") {
592
1176
  const body = await readJson(req);
593
1177
  if (!body || typeof body.raw_text !== "string" || !body.raw_text.trim()) {
594
1178
  return error(400, "raw_text is required");
595
1179
  }
596
- const recording = await createRecording(pg2, body);
1180
+ const recording = await createRecording(pg2, body, req.headers.get("Idempotency-Key") ?? undefined, { principal: `${decision.principal.app}:${decision.principal.kid}` });
597
1181
  return json({ recording }, 201);
598
1182
  }
599
1183
  return error(405, `method ${method} not allowed on /v1/recordings`);
@@ -700,6 +1284,9 @@ async function handleV1Request(req, url) {
700
1284
  if (e instanceof ProjectNotFoundError || e instanceof ValidationError) {
701
1285
  return error(400, e.message);
702
1286
  }
1287
+ if (e instanceof IdempotencyConflictError) {
1288
+ return error(409, e.message);
1289
+ }
703
1290
  console.error(`[recordings-serve] unhandled ${method} ${path} error:`, e);
704
1291
  return error(500, "internal server error");
705
1292
  }
@@ -733,6 +1320,7 @@ function buildV1OpenApiDocument(version = VERSION) {
733
1320
  type: "object",
734
1321
  required: ["raw_text"],
735
1322
  properties: {
1323
+ id: { type: "string" },
736
1324
  raw_text: { type: "string" },
737
1325
  audio_path: { type: "string" },
738
1326
  processed_text: { type: "string" },
@@ -783,7 +1371,10 @@ function buildV1OpenApiDocument(version = VERSION) {
783
1371
  { name: "project_id", in: "query", schema: { type: "string" } },
784
1372
  { name: "session_id", in: "query", schema: { type: "string" } },
785
1373
  { name: "processing_mode", in: "query", schema: { type: "string", enum: ["raw", "enhanced"] } },
1374
+ { name: "tags", in: "query", style: "form", explode: true, schema: { type: "array", items: { type: "string" } } },
786
1375
  { name: "search", in: "query", schema: { type: "string" } },
1376
+ { name: "since", in: "query", schema: { type: "string" } },
1377
+ { name: "until", in: "query", schema: { type: "string" } },
787
1378
  { name: "limit", in: "query", schema: { type: "integer" } },
788
1379
  { name: "offset", in: "query", schema: { type: "integer" } }
789
1380
  ],
@@ -796,7 +1387,7 @@ function buildV1OpenApiDocument(version = VERSION) {
796
1387
  type: "object",
797
1388
  properties: {
798
1389
  recordings: { type: "array", items: { $ref: "#/components/schemas/Recording" } },
799
- count: { type: "integer" }
1390
+ count: { type: "integer", description: "Total matching recordings before pagination" }
800
1391
  }
801
1392
  }
802
1393
  }
@@ -5087,19 +5678,185 @@ var init_zod = __esm(() => {
5087
5678
  init_external();
5088
5679
  });
5089
5680
 
5090
- // src/lib/config.ts
5091
- import { copyFileSync, existsSync, readFileSync, mkdirSync, readdirSync, statSync } from "fs";
5092
- import { dirname, join, resolve } from "path";
5681
+ // src/lib/install-maintenance.ts
5682
+ import { randomUUID } from "crypto";
5683
+ import {
5684
+ chmodSync,
5685
+ lstatSync,
5686
+ mkdirSync,
5687
+ realpathSync,
5688
+ renameSync,
5689
+ rmSync,
5690
+ writeFileSync
5691
+ } from "fs";
5093
5692
  import { homedir } from "os";
5693
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
5694
+ import { spawnSync } from "child_process";
5695
+ function stateParent(env = process.env) {
5696
+ const home = env["HOME"] || env["USERPROFILE"] || homedir();
5697
+ return join(home, ".hasna");
5698
+ }
5699
+ function installMaintenanceMarkerPath(env = process.env) {
5700
+ return join(stateParent(env), INSTALL_MAINTENANCE_MARKER_NAME);
5701
+ }
5702
+ function storeReaderLeasesPath(env = process.env) {
5703
+ return join(stateParent(env), STORE_READER_LEASES_NAME);
5704
+ }
5705
+ function pathExists(path) {
5706
+ try {
5707
+ lstatSync(path);
5708
+ return true;
5709
+ } catch (error2) {
5710
+ if (error2.code === "ENOENT")
5711
+ return false;
5712
+ throw error2;
5713
+ }
5714
+ }
5715
+ function assertPrivateDirectory(path) {
5716
+ const details = lstatSync(path);
5717
+ if (!details.isDirectory() || details.isSymbolicLink()) {
5718
+ throw new Error(`Local Recordings coordination path is not a secure directory: ${path}`);
5719
+ }
5720
+ if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
5721
+ throw new Error(`Local Recordings coordination path has an unexpected owner: ${path}`);
5722
+ }
5723
+ if ((details.mode & 18) !== 0) {
5724
+ throw new Error(`Local Recordings coordination path is group/world writable: ${path}`);
5725
+ }
5726
+ }
5727
+ function ensureReaderRoot(env) {
5728
+ const parent = stateParent(env);
5729
+ const readerRoot = storeReaderLeasesPath(env);
5730
+ mkdirSync(parent, { recursive: true, mode: 448 });
5731
+ assertPrivateDirectory(parent);
5732
+ try {
5733
+ mkdirSync(readerRoot, { mode: 448 });
5734
+ } catch (error2) {
5735
+ if (error2.code !== "EEXIST")
5736
+ throw error2;
5737
+ }
5738
+ assertPrivateDirectory(readerRoot);
5739
+ return readerRoot;
5740
+ }
5741
+ function processStartIdentity() {
5742
+ const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(process.pid)], {
5743
+ encoding: "utf8",
5744
+ env: { ...process.env, LC_ALL: "C", LANG: "C", TZ: "UTC0" }
5745
+ });
5746
+ const identity = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
5747
+ if (!identity) {
5748
+ throw new Error("Could not establish the local Store reader process identity");
5749
+ }
5750
+ return identity;
5751
+ }
5752
+ function maintenanceUnavailable() {
5753
+ return new Error("Local Recordings storage is temporarily unavailable during app installation maintenance");
5754
+ }
5755
+ function assertMaintenanceAbsent(env) {
5756
+ if (pathExists(installMaintenanceMarkerPath(env)))
5757
+ throw maintenanceUnavailable();
5758
+ }
5759
+ function canonicalPotentialPath(path) {
5760
+ const absolutePath = resolve(path);
5761
+ let existingPath = absolutePath;
5762
+ const missingEntries = [];
5763
+ while (true) {
5764
+ try {
5765
+ return join(realpathSync(existingPath), ...missingEntries);
5766
+ } catch (error2) {
5767
+ if (error2.code !== "ENOENT")
5768
+ return absolutePath;
5769
+ const parent = dirname(existingPath);
5770
+ if (parent === existingPath)
5771
+ return absolutePath;
5772
+ missingEntries.unshift(basename(existingPath));
5773
+ existingPath = parent;
5774
+ }
5775
+ }
5776
+ }
5777
+ function pathIsWithin(root, candidate) {
5778
+ const relativePath = relative(root, candidate);
5779
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
5780
+ }
5781
+ function isGlobalRecordingsStatePath(path, env = process.env) {
5782
+ const globalStateRoot = join(stateParent(env), "recordings");
5783
+ return pathIsWithin(resolve(globalStateRoot), resolve(path)) || pathIsWithin(canonicalPotentialPath(globalStateRoot), canonicalPotentialPath(path));
5784
+ }
5785
+ function removeActiveLease(activeLease, releasedLease) {
5786
+ try {
5787
+ renameSync(activeLease, releasedLease);
5788
+ } catch (error2) {
5789
+ if (error2.code !== "ENOENT")
5790
+ throw error2;
5791
+ }
5792
+ rmSync(releasedLease, { recursive: true, force: true });
5793
+ }
5794
+ function acquireLocalStoreReaderLease(env = process.env) {
5795
+ assertMaintenanceAbsent(env);
5796
+ const readerRoot = ensureReaderRoot(env);
5797
+ const nonce = randomUUID();
5798
+ const pendingLease = join(readerRoot, `.pending-${process.pid}-${nonce}`);
5799
+ const activeLease = join(readerRoot, `lease-${process.pid}-${nonce}`);
5800
+ const releasedLease = join(readerRoot, `.released-${process.pid}-${nonce}`);
5801
+ let active = false;
5802
+ try {
5803
+ mkdirSync(pendingLease, { mode: 448 });
5804
+ const owner = join(pendingLease, "owner");
5805
+ writeFileSync(owner, `${process.pid}
5806
+ ${processStartIdentity()}
5807
+ `, {
5808
+ encoding: "utf8",
5809
+ flag: "wx",
5810
+ mode: 384
5811
+ });
5812
+ chmodSync(owner, 384);
5813
+ renameSync(pendingLease, activeLease);
5814
+ active = true;
5815
+ assertMaintenanceAbsent(env);
5816
+ } catch (error2) {
5817
+ if (active)
5818
+ removeActiveLease(activeLease, releasedLease);
5819
+ else
5820
+ rmSync(pendingLease, { recursive: true, force: true });
5821
+ throw error2;
5822
+ }
5823
+ let released = false;
5824
+ return () => {
5825
+ if (released)
5826
+ return;
5827
+ removeActiveLease(activeLease, releasedLease);
5828
+ released = true;
5829
+ };
5830
+ }
5831
+ async function withLocalStoreReaderLease(operation, env = process.env) {
5832
+ const release = acquireLocalStoreReaderLease(env);
5833
+ try {
5834
+ return await operation();
5835
+ } finally {
5836
+ release();
5837
+ }
5838
+ }
5839
+ var INSTALL_MAINTENANCE_MARKER_NAME = ".recordings-install-maintenance", STORE_READER_LEASES_NAME = ".recordings-store-readers";
5840
+ var init_install_maintenance = () => {};
5841
+
5842
+ // src/lib/config.ts
5843
+ import { copyFileSync, existsSync, readFileSync, mkdirSync as mkdirSync2, readdirSync, realpathSync as realpathSync2, statSync } from "fs";
5844
+ import { dirname as dirname2, join as join2, resolve as resolve2, sep as sep2 } from "path";
5845
+ import { homedir as homedir2 } from "os";
5094
5846
  function loadConfig(configPath) {
5095
5847
  const config = { ...DEFAULT_CONFIG };
5096
- const filePath = configPath || findConfigFile() || join(getDataDir(), "config.json");
5848
+ config.config_warnings = [];
5849
+ let explicitPostProcessingMode = false;
5850
+ let explicitTranscriberModel = false;
5851
+ const filePath = configPath || findConfigFile() || join2(getDataDir(), "config.json");
5097
5852
  let fileProvidedOpenAIKey = false;
5098
5853
  if (existsSync(filePath)) {
5099
5854
  try {
5100
5855
  const raw = readFileSync(filePath, "utf-8");
5101
5856
  const fileConfig = JSON.parse(raw);
5102
5857
  const expanded = expandEnvBackedConfig(fileConfig);
5858
+ explicitPostProcessingMode = typeof fileConfig.post_processing_mode === "string";
5859
+ explicitTranscriberModel = typeof fileConfig.transcriber_model === "string";
5103
5860
  Object.assign(config, expanded);
5104
5861
  fileProvidedOpenAIKey = typeof expanded.openai_api_key === "string" && expanded.openai_api_key.length > 0;
5105
5862
  } catch {}
@@ -5116,12 +5873,35 @@ function loadConfig(configPath) {
5116
5873
  if (process.env.RECORDINGS_MODEL) {
5117
5874
  config.transcription_model = process.env.RECORDINGS_MODEL;
5118
5875
  }
5876
+ if (process.env.RECORDINGS_REALTIME_SESSION_MODEL) {
5877
+ config.realtime_session_model = process.env.RECORDINGS_REALTIME_SESSION_MODEL;
5878
+ }
5879
+ if (process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL) {
5880
+ config.realtime_transcription_model = process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL;
5881
+ }
5119
5882
  if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
5120
5883
  config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
5121
5884
  }
5885
+ if (process.env.RECORDINGS_TRANSCRIBER_MODEL) {
5886
+ config.transcriber_model = process.env.RECORDINGS_TRANSCRIBER_MODEL;
5887
+ explicitTranscriberModel = true;
5888
+ }
5122
5889
  if (process.env.RECORDINGS_LANGUAGE) {
5123
5890
  config.language = process.env.RECORDINGS_LANGUAGE;
5124
5891
  }
5892
+ if (process.env.RECORDINGS_TRANSCRIPTION_PROMPT) {
5893
+ config.transcription_prompt = process.env.RECORDINGS_TRANSCRIPTION_PROMPT;
5894
+ }
5895
+ if (process.env.RECORDINGS_TRANSCRIBER_PROMPT) {
5896
+ config.transcriber_prompt = process.env.RECORDINGS_TRANSCRIBER_PROMPT;
5897
+ }
5898
+ if (process.env.RECORDINGS_POST_PROCESSING_MODE) {
5899
+ config.post_processing_mode = normalizePostProcessingMode(process.env.RECORDINGS_POST_PROCESSING_MODE, config.post_processing_mode ?? "auto");
5900
+ explicitPostProcessingMode = true;
5901
+ }
5902
+ if (process.env.RECORDINGS_AUTO_ENHANCE) {
5903
+ config.auto_enhance = parseBooleanEnv(process.env.RECORDINGS_AUTO_ENHANCE, config.auto_enhance);
5904
+ }
5125
5905
  if (process.env.HASNA_RECORDINGS_DB_PATH) {
5126
5906
  config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
5127
5907
  } else if (process.env.RECORDINGS_DB_PATH) {
@@ -5139,14 +5919,86 @@ function loadConfig(configPath) {
5139
5919
  if (!config.enhancement_api_key) {
5140
5920
  config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
5141
5921
  }
5922
+ if (!explicitTranscriberModel) {
5923
+ config.transcriber_model = config.enhancement_model;
5924
+ }
5925
+ normalizeModelSlots(config);
5926
+ normalizePostProcessingConfig(config, explicitPostProcessingMode);
5142
5927
  if (!config.db_path) {
5143
- config.db_path = join(getDataDir(), "recordings.db");
5928
+ config.db_path = join2(getDataDir(), "recordings.db");
5144
5929
  }
5145
5930
  if (!config.audio_dir) {
5146
- config.audio_dir = join(getDataDir(), "audio");
5931
+ config.audio_dir = join2(getDataDir(), "audio");
5147
5932
  }
5148
5933
  return config;
5149
5934
  }
5935
+ function normalizeModelSlots(config) {
5936
+ const warnings = config.config_warnings ?? [];
5937
+ config.config_warnings = warnings;
5938
+ const boundedModel = config.transcription_model?.trim() || DEFAULT_TRANSCRIPTION_MODEL;
5939
+ if (isRealtimeOnlyModel(boundedModel)) {
5940
+ warnings.push(`Ignoring RECORDINGS_MODEL=${boundedModel}; bounded transcription uses ${DEFAULT_TRANSCRIPTION_MODEL}.`);
5941
+ config.transcription_model = DEFAULT_TRANSCRIPTION_MODEL;
5942
+ } else {
5943
+ config.transcription_model = boundedModel;
5944
+ }
5945
+ const realtimeSessionModel = config.realtime_session_model?.trim() || DEFAULT_REALTIME_SESSION_MODEL;
5946
+ if (isTranscriptionOnlyModel(realtimeSessionModel)) {
5947
+ warnings.push(`Ignoring realtime session model ${realtimeSessionModel}; use ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL} as realtime_transcription_model instead.`);
5948
+ config.realtime_session_model = DEFAULT_REALTIME_SESSION_MODEL;
5949
+ } else {
5950
+ config.realtime_session_model = realtimeSessionModel;
5951
+ }
5952
+ const realtimeTranscriptionModel = config.realtime_transcription_model?.trim() || DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
5953
+ if (!isRealtimeTranscriptionModel(realtimeTranscriptionModel)) {
5954
+ warnings.push(`Ignoring realtime transcription model ${realtimeTranscriptionModel}; realtime transcription uses ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL}.`);
5955
+ config.realtime_transcription_model = DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
5956
+ } else {
5957
+ config.realtime_transcription_model = realtimeTranscriptionModel;
5958
+ }
5959
+ return config;
5960
+ }
5961
+ function isTranscriptionOnlyModel(model) {
5962
+ const m = model.trim().toLowerCase();
5963
+ return m === "whisper-1" || m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.includes("transcribe");
5964
+ }
5965
+ function isRealtimeOnlyModel(model) {
5966
+ const m = model.trim().toLowerCase();
5967
+ return m.startsWith("gpt-realtime");
5968
+ }
5969
+ function isRealtimeTranscriptionModel(model) {
5970
+ const m = model.trim().toLowerCase();
5971
+ return m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.startsWith("gpt-realtime") && m.includes("whisper");
5972
+ }
5973
+ function normalizePostProcessingMode(value, fallback = "auto") {
5974
+ const mode = value?.trim().toLowerCase();
5975
+ if (mode && POST_PROCESSING_MODES.has(mode)) {
5976
+ return mode;
5977
+ }
5978
+ return fallback;
5979
+ }
5980
+ function normalizePostProcessingConfig(config, preferPostProcessingMode = true) {
5981
+ if (preferPostProcessingMode) {
5982
+ config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
5983
+ config.auto_enhance = config.post_processing_mode !== "off";
5984
+ return config;
5985
+ }
5986
+ if (config.auto_enhance === false) {
5987
+ config.post_processing_mode = "off";
5988
+ } else {
5989
+ config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
5990
+ }
5991
+ config.auto_enhance = config.post_processing_mode !== "off";
5992
+ return config;
5993
+ }
5994
+ function parseBooleanEnv(value, fallback) {
5995
+ const normalized = value.trim().toLowerCase();
5996
+ if (["1", "true", "yes", "on"].includes(normalized))
5997
+ return true;
5998
+ if (["0", "false", "no", "off"].includes(normalized))
5999
+ return false;
6000
+ return fallback;
6001
+ }
5150
6002
  function expandEnvBackedConfig(config) {
5151
6003
  const expanded = { ...config };
5152
6004
  for (const key of ["openai_api_key", "enhancement_api_key"]) {
@@ -5165,36 +6017,68 @@ function getDataDir() {
5165
6017
  if (projectLocalDir)
5166
6018
  return projectLocalDir;
5167
6019
  const home = getHomeDir();
5168
- const newDir = join(home, ".hasna", "recordings");
5169
- const oldDir = join(home, ".recordings");
6020
+ const newDir = join2(home, ".hasna", "recordings");
6021
+ const oldDir = join2(home, ".recordings");
5170
6022
  if (existsSync(oldDir)) {
6023
+ const releaseLease = acquireLocalStoreReaderLease();
5171
6024
  try {
5172
- mergeDirectoryContents(oldDir, newDir);
5173
- } catch {}
6025
+ try {
6026
+ if (existsSync(oldDir))
6027
+ mergeDirectoryContents(oldDir, newDir);
6028
+ } catch {}
6029
+ } finally {
6030
+ releaseLease();
6031
+ }
5174
6032
  }
5175
6033
  return newDir;
5176
6034
  }
5177
6035
  function findProjectRecordingsPath(entry) {
5178
- const home = resolve(getHomeDir());
5179
- let dir = resolve(process.cwd());
6036
+ const cwd = canonicalExistingPath(process.cwd());
6037
+ const home = canonicalExistingPath(getHomeDir());
6038
+ const repositoryRoot = findRepositoryRoot(cwd);
6039
+ const cwdIsInsideHome = cwd === home || cwd.startsWith(`${home}${sep2}`);
6040
+ const repositoryIsInsideHome = repositoryRoot !== null && repositoryRoot !== home && repositoryRoot.startsWith(`${home}${sep2}`);
6041
+ const boundary = cwdIsInsideHome ? repositoryIsInsideHome ? repositoryRoot : home : repositoryRoot ?? cwd;
6042
+ const excludeBoundary = boundary === home;
6043
+ let dir = cwd;
5180
6044
  while (true) {
5181
- if (dir !== home) {
5182
- const candidate = entry ? join(dir, ".recordings", entry) : join(dir, ".recordings");
5183
- if (existsSync(candidate))
5184
- return candidate;
5185
- }
5186
- const parent = dirname(dir);
6045
+ if (excludeBoundary && dir === boundary)
6046
+ break;
6047
+ const candidate = entry ? join2(dir, ".recordings", entry) : join2(dir, ".recordings");
6048
+ if (existsSync(candidate))
6049
+ return candidate;
6050
+ if (dir === boundary)
6051
+ break;
6052
+ const parent = dirname2(dir);
5187
6053
  if (parent === dir)
5188
6054
  break;
5189
6055
  dir = parent;
5190
6056
  }
5191
6057
  return null;
5192
6058
  }
6059
+ function findRepositoryRoot(start) {
6060
+ let dir = start;
6061
+ while (true) {
6062
+ if (existsSync(join2(dir, ".git")))
6063
+ return dir;
6064
+ const parent = dirname2(dir);
6065
+ if (parent === dir)
6066
+ return null;
6067
+ dir = parent;
6068
+ }
6069
+ }
6070
+ function canonicalExistingPath(path) {
6071
+ try {
6072
+ return realpathSync2(path);
6073
+ } catch {
6074
+ return resolve2(path);
6075
+ }
6076
+ }
5193
6077
  function mergeDirectoryContents(sourceDir, targetDir) {
5194
- mkdirSync(targetDir, { recursive: true });
6078
+ mkdirSync2(targetDir, { recursive: true });
5195
6079
  for (const entry of readdirSync(sourceDir)) {
5196
- const sourcePath = join(sourceDir, entry);
5197
- const targetPath = join(targetDir, entry);
6080
+ const sourcePath = join2(sourceDir, entry);
6081
+ const targetPath = join2(targetDir, entry);
5198
6082
  const sourceStats = statSync(sourcePath);
5199
6083
  if (sourceStats.isDirectory()) {
5200
6084
  mergeDirectoryContents(sourcePath, targetPath);
@@ -5204,7 +6088,7 @@ function mergeDirectoryContents(sourceDir, targetDir) {
5204
6088
  }
5205
6089
  }
5206
6090
  function loadSecretKey(keyName) {
5207
- const secretsPath = join(getHomeDir(), ".secrets");
6091
+ const secretsPath = join2(getHomeDir(), ".secrets");
5208
6092
  if (!existsSync(secretsPath))
5209
6093
  return "";
5210
6094
  for (const candidate of listSecretFiles(secretsPath)) {
@@ -5231,7 +6115,7 @@ function listSecretFiles(path) {
5231
6115
  if (!stats.isDirectory())
5232
6116
  return [];
5233
6117
  return readdirSync(path).sort().flatMap((entry) => {
5234
- const child = join(path, entry);
6118
+ const child = join2(path, entry);
5235
6119
  try {
5236
6120
  const childStats = statSync(child);
5237
6121
  if (childStats.isDirectory())
@@ -5246,27 +6130,44 @@ function listSecretFiles(path) {
5246
6130
  }
5247
6131
  }
5248
6132
  function getHomeDir() {
5249
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
6133
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
5250
6134
  }
5251
6135
  function ensureDataDir(config) {
5252
- const { mkdirSync: mkdirSync2 } = __require("fs");
5253
- mkdirSync2(config.audio_dir, { recursive: true });
6136
+ const { mkdirSync: mkdirSync3 } = __require("fs");
5254
6137
  const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
5255
- if (dbDir)
5256
- mkdirSync2(dbDir, { recursive: true });
6138
+ const releaseLease = [config.audio_dir, dbDir].some((path) => path.length > 0 && isGlobalRecordingsStatePath(path)) ? acquireLocalStoreReaderLease() : () => {};
6139
+ try {
6140
+ mkdirSync3(config.audio_dir, { recursive: true });
6141
+ if (dbDir)
6142
+ mkdirSync3(dbDir, { recursive: true });
6143
+ } finally {
6144
+ releaseLease();
6145
+ }
5257
6146
  }
5258
- var DEFAULT_CONFIG;
6147
+ var POST_PROCESSING_MODES, DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-transcribe", DEFAULT_REALTIME_SESSION_MODEL = "gpt-realtime", DEFAULT_REALTIME_TRANSCRIPTION_MODEL = "gpt-realtime-whisper", DEFAULT_CONFIG;
5259
6148
  var init_config = __esm(() => {
6149
+ init_install_maintenance();
6150
+ POST_PROCESSING_MODES = new Set([
6151
+ "off",
6152
+ "auto",
6153
+ "always"
6154
+ ]);
5260
6155
  DEFAULT_CONFIG = {
5261
6156
  openai_api_key: "",
5262
6157
  enhancement_api_key: "",
5263
- transcription_model: "gpt-4o-transcribe",
6158
+ transcription_model: DEFAULT_TRANSCRIPTION_MODEL,
6159
+ realtime_session_model: DEFAULT_REALTIME_SESSION_MODEL,
6160
+ realtime_transcription_model: DEFAULT_REALTIME_TRANSCRIPTION_MODEL,
5264
6161
  enhancement_model: "gpt-4o",
6162
+ transcriber_model: "gpt-4o",
5265
6163
  language: "en",
5266
6164
  audio_format: "wav",
5267
6165
  sample_rate: 16000,
5268
6166
  record_command: "sox",
5269
6167
  hotkey: "space",
6168
+ transcription_prompt: "",
6169
+ transcriber_prompt: "",
6170
+ post_processing_mode: "auto",
5270
6171
  auto_enhance: true,
5271
6172
  enhance_triggers: [
5272
6173
  "say it better",
@@ -5283,7 +6184,8 @@ var init_config = __esm(() => {
5283
6184
  keyword_transforms: {},
5284
6185
  db_path: "",
5285
6186
  audio_dir: "",
5286
- max_recording_seconds: 1800
6187
+ max_recording_seconds: 1800,
6188
+ config_warnings: []
5287
6189
  };
5288
6190
  });
5289
6191
 
@@ -5320,14 +6222,14 @@ class SqliteAdapter {
5320
6222
  var init_sqlite_adapter = () => {};
5321
6223
 
5322
6224
  // src/db/database.ts
5323
- import { mkdirSync as mkdirSync2 } from "fs";
5324
- import { dirname as dirname2 } from "path";
6225
+ import { mkdirSync as mkdirSync3 } from "fs";
6226
+ import { dirname as dirname3 } from "path";
5325
6227
  function getDatabase(dbPath) {
5326
6228
  if (_db)
5327
6229
  return _db;
5328
6230
  const path = dbPath || loadConfig().db_path;
5329
- const dir = dirname2(path);
5330
- mkdirSync2(dir, { recursive: true });
6231
+ const dir = dirname3(path);
6232
+ mkdirSync3(dir, { recursive: true });
5331
6233
  _adapter = new SqliteAdapter(path);
5332
6234
  _db = _adapter.raw;
5333
6235
  _db.run("PRAGMA busy_timeout = 5000");
@@ -5491,20 +6393,36 @@ function parseRow(row) {
5491
6393
  created_at: row["created_at"]
5492
6394
  };
5493
6395
  }
5494
- function createRecording2(input, db) {
6396
+ function createRecording2(input, db, idempotencyKey) {
5495
6397
  const d = db || getDatabase();
5496
- const id = shortUuid2();
5497
- const tagsJson = JSON.stringify(input.tags || []);
5498
- const metadataJson = JSON.stringify(input.metadata || {});
5499
- 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)
5500
- 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);
5501
- if (input.tags && input.tags.length > 0) {
5502
- const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
5503
- for (const tag of input.tags) {
5504
- insertTag.run(id, tag);
5505
- }
5506
- }
5507
- return getRecording2(id, d);
6398
+ input = recordingCreateIdentity(input, idempotencyKey).input;
6399
+ const id = input.id || shortUuid2();
6400
+ const create = d.transaction(() => {
6401
+ if (input.id) {
6402
+ const existing = getRecording2(input.id, d);
6403
+ if (existing?.id === input.id)
6404
+ return existing;
6405
+ }
6406
+ const tagsJson = JSON.stringify(input.tags || []);
6407
+ const metadataJson = JSON.stringify(input.metadata || {});
6408
+ 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)
6409
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6410
+ 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);
6411
+ if (insertResult.changes === 0) {
6412
+ const existing = getRecording2(id, d);
6413
+ if (existing?.id === id)
6414
+ return existing;
6415
+ throw new Error("recording id conflict could not be read back");
6416
+ }
6417
+ if (input.tags && input.tags.length > 0) {
6418
+ const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
6419
+ for (const tag of input.tags) {
6420
+ insertTag.run(id, tag);
6421
+ }
6422
+ }
6423
+ return getRecording2(id, d);
6424
+ });
6425
+ return create();
5508
6426
  }
5509
6427
  function getRecording2(id, db) {
5510
6428
  const d = db || getDatabase();
@@ -5516,6 +6434,20 @@ function getRecording2(id, db) {
5516
6434
  }
5517
6435
  function listRecordings2(filter, db) {
5518
6436
  const d = db || getDatabase();
6437
+ const { where, params } = buildRecordingWhere2(filter);
6438
+ const limit = filter?.limit || 50;
6439
+ const offset = filter?.offset || 0;
6440
+ const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
6441
+ const rows = d.query(sql).all(...params, limit, offset);
6442
+ return rows.map(parseRow);
6443
+ }
6444
+ function countRecordings2(filter, db) {
6445
+ const d = db || getDatabase();
6446
+ const { where, params } = buildRecordingWhere2(filter);
6447
+ const row = d.query(`SELECT COUNT(*) as c FROM recordings ${where}`).get(...params);
6448
+ return row.c;
6449
+ }
6450
+ function buildRecordingWhere2(filter) {
5519
6451
  const conditions = [];
5520
6452
  const params = [];
5521
6453
  if (filter?.agent_id) {
@@ -5554,12 +6486,7 @@ function listRecordings2(filter, db) {
5554
6486
  params.push(filter.until);
5555
6487
  }
5556
6488
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5557
- const limit = filter?.limit || 50;
5558
- const offset = filter?.offset || 0;
5559
- const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
5560
- params.push(limit, offset);
5561
- const rows = d.query(sql).all(...params);
5562
- return rows.map(parseRow);
6489
+ return { where, params };
5563
6490
  }
5564
6491
  function deleteRecording2(id, db) {
5565
6492
  const d = db || getDatabase();
@@ -5590,6 +6517,7 @@ function getRecordingStats2(db) {
5590
6517
  }
5591
6518
  var init_recordings = __esm(() => {
5592
6519
  init_database();
6520
+ init_recording_create_identity();
5593
6521
  });
5594
6522
 
5595
6523
  // src/db/projects.ts
@@ -6002,6 +6930,7 @@ var init_client = __esm(() => {
6002
6930
  });
6003
6931
 
6004
6932
  // src/store.ts
6933
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
6005
6934
  function listQuery(filter) {
6006
6935
  if (!filter)
6007
6936
  return {};
@@ -6010,6 +6939,7 @@ function listQuery(filter) {
6010
6939
  project_id: filter.project_id,
6011
6940
  session_id: filter.session_id,
6012
6941
  processing_mode: filter.processing_mode,
6942
+ tags: filter.tags,
6013
6943
  search: filter.search,
6014
6944
  since: filter.since,
6015
6945
  until: filter.until,
@@ -6027,8 +6957,10 @@ function apiStore(client) {
6027
6957
  return {
6028
6958
  mode: "cloud-http",
6029
6959
  baseUrl: client.baseUrl,
6030
- async createRecording(input) {
6031
- const res = await client.create("recordings", input);
6960
+ async createRecording(input, idempotencyKey) {
6961
+ const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
6962
+ const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
6963
+ const res = await client.create("recordings", identity.input, identity.idempotencyKey);
6032
6964
  return unwrap(res, "recording");
6033
6965
  },
6034
6966
  async getRecording(id) {
@@ -6039,6 +6971,36 @@ function apiStore(client) {
6039
6971
  const { items } = await client.list("recordings", listQuery(filter));
6040
6972
  return items;
6041
6973
  },
6974
+ async countRecordings(filter) {
6975
+ const pageLimit = 500;
6976
+ const maxPageRequests = 1e4;
6977
+ let offset = 0;
6978
+ let pageRequests = 0;
6979
+ const seenPageKeys = new Set;
6980
+ while (pageRequests < maxPageRequests) {
6981
+ pageRequests += 1;
6982
+ const { items, raw } = await client.list("recordings", {
6983
+ ...listQuery(filter),
6984
+ limit: pageLimit,
6985
+ offset
6986
+ });
6987
+ const count = raw && typeof raw === "object" ? raw.count : undefined;
6988
+ if (typeof count !== "number" || !Number.isFinite(count)) {
6989
+ throw new Error("Recordings API response is missing a valid count");
6990
+ }
6991
+ if (count > items.length)
6992
+ return count;
6993
+ if (items.length === 0)
6994
+ return offset;
6995
+ offset += items.length;
6996
+ const pageKey = recordingPageFingerprint(items);
6997
+ if (seenPageKeys.has(pageKey)) {
6998
+ throw new Error("Recordings API ignored pagination while counting legacy results");
6999
+ }
7000
+ seenPageKeys.add(pageKey);
7001
+ }
7002
+ throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
7003
+ },
6042
7004
  async searchRecordings(query, filter) {
6043
7005
  const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
6044
7006
  return items;
@@ -6120,6 +7082,42 @@ function apiStore(client) {
6120
7082
  }
6121
7083
  };
6122
7084
  }
7085
+ function recordingPageFingerprint(items) {
7086
+ const hash = createHash2("sha256");
7087
+ const ids = items.map((item) => String(item.id ?? "")).sort();
7088
+ for (const id of ids) {
7089
+ hash.update(String(id.length));
7090
+ hash.update(":");
7091
+ hash.update(id);
7092
+ hash.update(";");
7093
+ }
7094
+ return `${items.length}:${hash.digest("hex")}`;
7095
+ }
7096
+ async function countStoreRecordings(store, filter) {
7097
+ if (store.countRecordings)
7098
+ return store.countRecordings(filter);
7099
+ const pageLimit = 500;
7100
+ const maxPageRequests = 1e4;
7101
+ const { limit: _limit, offset: _offset, ...unpaginated } = filter ?? {};
7102
+ const seenPageKeys = new Set;
7103
+ let offset = 0;
7104
+ for (let pageRequests = 0;pageRequests < maxPageRequests; pageRequests += 1) {
7105
+ const items = await store.listRecordings({
7106
+ ...unpaginated,
7107
+ limit: pageLimit,
7108
+ offset
7109
+ });
7110
+ if (items.length === 0)
7111
+ return offset;
7112
+ const pageKey = recordingPageFingerprint(items);
7113
+ if (seenPageKeys.has(pageKey)) {
7114
+ throw new Error("Legacy Store ignored pagination while counting recordings");
7115
+ }
7116
+ seenPageKeys.add(pageKey);
7117
+ offset += items.length;
7118
+ }
7119
+ throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
7120
+ }
6123
7121
  function getStore(env = process.env) {
6124
7122
  if (env === process.env && cached)
6125
7123
  return cached;
@@ -6136,53 +7134,58 @@ var init_store = __esm(() => {
6136
7134
  init_projects();
6137
7135
  init_feedback();
6138
7136
  init_client();
7137
+ init_recording_create_identity();
7138
+ init_install_maintenance();
6139
7139
  localStore = {
6140
7140
  mode: "local",
6141
7141
  baseUrl: null,
6142
- async createRecording(input) {
6143
- return createRecording2(input);
7142
+ async createRecording(input, idempotencyKey) {
7143
+ return withLocalStoreReaderLease(() => createRecording2(input, undefined, idempotencyKey));
6144
7144
  },
6145
7145
  async getRecording(id) {
6146
- return getRecording2(id);
7146
+ return withLocalStoreReaderLease(() => getRecording2(id));
6147
7147
  },
6148
7148
  async listRecordings(filter) {
6149
- return listRecordings2(filter);
7149
+ return withLocalStoreReaderLease(() => listRecordings2(filter));
7150
+ },
7151
+ async countRecordings(filter) {
7152
+ return withLocalStoreReaderLease(() => countRecordings2(filter));
6150
7153
  },
6151
7154
  async searchRecordings(query, filter) {
6152
- return searchRecordings(query, filter);
7155
+ return withLocalStoreReaderLease(() => searchRecordings(query, filter));
6153
7156
  },
6154
7157
  async deleteRecording(id) {
6155
- return deleteRecording2(id);
7158
+ return withLocalStoreReaderLease(() => deleteRecording2(id));
6156
7159
  },
6157
7160
  async getRecordingStats() {
6158
- return getRecordingStats2();
7161
+ return withLocalStoreReaderLease(() => getRecordingStats2());
6159
7162
  },
6160
7163
  async registerAgent(name, description, role) {
6161
- return registerAgent2(name, description, role);
7164
+ return withLocalStoreReaderLease(() => registerAgent2(name, description, role));
6162
7165
  },
6163
7166
  async getAgent(idOrName) {
6164
- return getAgent2(idOrName);
7167
+ return withLocalStoreReaderLease(() => getAgent2(idOrName));
6165
7168
  },
6166
7169
  async listAgents() {
6167
- return listAgents2();
7170
+ return withLocalStoreReaderLease(() => listAgents2());
6168
7171
  },
6169
7172
  async heartbeatAgent(idOrName) {
6170
- return heartbeatAgent2(idOrName);
7173
+ return withLocalStoreReaderLease(() => heartbeatAgent2(idOrName));
6171
7174
  },
6172
7175
  async setAgentFocus(idOrName, projectId) {
6173
- return setAgentFocus2(idOrName, projectId);
7176
+ return withLocalStoreReaderLease(() => setAgentFocus2(idOrName, projectId));
6174
7177
  },
6175
7178
  async registerProject(name, path, description) {
6176
- return registerProject2(name, path, description);
7179
+ return withLocalStoreReaderLease(() => registerProject2(name, path, description));
6177
7180
  },
6178
7181
  async getProject(idOrPath) {
6179
- return getProject2(idOrPath);
7182
+ return withLocalStoreReaderLease(() => getProject2(idOrPath));
6180
7183
  },
6181
7184
  async listProjects() {
6182
- return listProjects2();
7185
+ return withLocalStoreReaderLease(() => listProjects2());
6183
7186
  },
6184
7187
  async saveFeedback(input) {
6185
- saveFeedback2(input);
7188
+ await withLocalStoreReaderLease(() => saveFeedback2(input));
6186
7189
  }
6187
7190
  };
6188
7191
  });
@@ -6208,12 +7211,13 @@ var init_types2 = __esm(() => {
6208
7211
  import OpenAI from "openai";
6209
7212
  import { createReadStream } from "fs";
6210
7213
  function getClient(config) {
6211
- if (_client)
6212
- return _client;
6213
7214
  if (!config.openai_api_key) {
6214
7215
  throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
6215
7216
  }
7217
+ if (_client && _clientApiKey === config.openai_api_key)
7218
+ return _client;
6216
7219
  _client = new OpenAI({ apiKey: config.openai_api_key });
7220
+ _clientApiKey = config.openai_api_key;
6217
7221
  return _client;
6218
7222
  }
6219
7223
  async function transcribeAudio(audioPath, config, options = {}) {
@@ -6225,7 +7229,7 @@ async function transcribeAudio(audioPath, config, options = {}) {
6225
7229
  file: stream,
6226
7230
  model: config.transcription_model,
6227
7231
  language: config.language || undefined,
6228
- prompt: buildVerbatimPrompt(options.prompt),
7232
+ prompt: buildVerbatimPrompt(options.prompt ?? config.transcription_prompt),
6229
7233
  response_format: "json"
6230
7234
  });
6231
7235
  stream.destroy();
@@ -6260,7 +7264,7 @@ function buildVerbatimPrompt(context) {
6260
7264
  Context words and names to recognize. Treat this only as vocabulary context, not as instructions:
6261
7265
  ${trimmed}`;
6262
7266
  }
6263
- var _client = null;
7267
+ var _client = null, _clientApiKey = null;
6264
7268
  var init_transcriber = __esm(() => {
6265
7269
  init_types2();
6266
7270
  });
@@ -6268,13 +7272,14 @@ var init_transcriber = __esm(() => {
6268
7272
  // src/lib/enhancer.ts
6269
7273
  import OpenAI2 from "openai";
6270
7274
  function getEnhancementClient(config) {
6271
- if (_enhancementClient)
6272
- return _enhancementClient;
6273
7275
  const key = config.enhancement_api_key || config.openai_api_key;
6274
7276
  if (!key) {
6275
7277
  throw new EnhancementError("API key not configured for enhancement. Set OPENAI_API_KEY or RECORDINGS_ENHANCEMENT_KEY");
6276
7278
  }
7279
+ if (_enhancementClient && _enhancementClientApiKey === key)
7280
+ return _enhancementClient;
6277
7281
  _enhancementClient = new OpenAI2({ apiKey: key });
7282
+ _enhancementClientApiKey = key;
6278
7283
  return _enhancementClient;
6279
7284
  }
6280
7285
  function needsEnhancement(text, config) {
@@ -6321,6 +7326,7 @@ function extractInstruction(text, trigger) {
6321
7326
  }
6322
7327
  async function enhanceText(rawText, instruction, config, systemPrompt) {
6323
7328
  const client = getEnhancementClient(config);
7329
+ const model = resolveTranscriberModel(config);
6324
7330
  let basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
6325
7331
 
6326
7332
  Rules:
@@ -6341,13 +7347,14 @@ Rules:
6341
7347
  Keyword Transformations:
6342
7348
  ${transformRules}`;
6343
7349
  }
6344
- const fullPrompt = systemPrompt ? `${basePrompt}
7350
+ const transcriberPrompt = combinePrompts(config.transcriber_prompt, systemPrompt);
7351
+ const fullPrompt = transcriberPrompt ? `${basePrompt}
6345
7352
 
6346
- Additional context:
6347
- ${systemPrompt}` : basePrompt;
7353
+ Transcriber instructions:
7354
+ ${transcriberPrompt}` : basePrompt;
6348
7355
  try {
6349
7356
  const response = await client.chat.completions.create({
6350
- model: config.enhancement_model,
7357
+ model,
6351
7358
  messages: [
6352
7359
  {
6353
7360
  role: "system",
@@ -6365,7 +7372,7 @@ ${systemPrompt}` : basePrompt;
6365
7372
  return {
6366
7373
  original: rawText,
6367
7374
  enhanced,
6368
- model: config.enhancement_model,
7375
+ model,
6369
7376
  reasoning: null
6370
7377
  };
6371
7378
  } catch (error2) {
@@ -6373,29 +7380,74 @@ ${systemPrompt}` : basePrompt;
6373
7380
  throw new EnhancementError(`Enhancement failed: ${describeTranscriptionFailure(msg)}`);
6374
7381
  }
6375
7382
  }
6376
- async function processText(rawText, config, systemPrompt, options) {
6377
- const force = options?.force === true;
6378
- if (!force && !config.auto_enhance) {
6379
- return { text: rawText, mode: "raw", enhancement_model: null };
7383
+ async function processText(rawText, config, systemPromptOrOptions, legacyOptions) {
7384
+ const options = {
7385
+ ...normalizeProcessTextOptions(systemPromptOrOptions),
7386
+ ...legacyOptions
7387
+ };
7388
+ const postProcessingMode = options.force ? "always" : options.postProcessingMode ?? normalizePostProcessingMode(config.post_processing_mode, config.auto_enhance === false ? "off" : "auto");
7389
+ if (postProcessingMode === "off") {
7390
+ return {
7391
+ text: rawText,
7392
+ mode: "raw",
7393
+ enhancement_model: null,
7394
+ post_processing_mode: "off",
7395
+ enhancement_reason: null
7396
+ };
6380
7397
  }
6381
- const detection = needsEnhancement(rawText, config);
6382
- if (!force && !detection.needs) {
6383
- return { text: rawText, mode: "raw", enhancement_model: null };
7398
+ const detection = postProcessingMode === "always" ? {
7399
+ needs: true,
7400
+ reason: "Always-on post-processing",
7401
+ instruction: rawText
7402
+ } : needsEnhancement(rawText, config);
7403
+ if (!detection.needs) {
7404
+ return {
7405
+ text: rawText,
7406
+ mode: "raw",
7407
+ enhancement_model: null,
7408
+ post_processing_mode: postProcessingMode,
7409
+ enhancement_reason: detection.reason
7410
+ };
6384
7411
  }
6385
- const instruction = detection.needs ? detection.instruction : rawText;
6386
- const result = await enhanceText(rawText, instruction, config, systemPrompt);
7412
+ const result = await enhanceText(rawText, detection.instruction, config, options.systemPrompt);
6387
7413
  return {
6388
7414
  text: result.enhanced,
6389
7415
  mode: "enhanced",
6390
- enhancement_model: result.model
7416
+ enhancement_model: result.model,
7417
+ post_processing_mode: postProcessingMode,
7418
+ enhancement_reason: detection.reason
6391
7419
  };
6392
7420
  }
6393
- var _enhancementClient = null;
7421
+ function resolveTranscriberModel(config) {
7422
+ return config.transcriber_model || config.enhancement_model;
7423
+ }
7424
+ function normalizeProcessTextOptions(value) {
7425
+ if (typeof value === "string")
7426
+ return { systemPrompt: value };
7427
+ return value ?? {};
7428
+ }
7429
+ function combinePrompts(...prompts) {
7430
+ return prompts.map((prompt) => prompt?.trim() ?? "").filter(Boolean).join(`
7431
+
7432
+ `);
7433
+ }
7434
+ var _enhancementClient = null, _enhancementClientApiKey = null;
6394
7435
  var init_enhancer = __esm(() => {
6395
7436
  init_types2();
7437
+ init_config();
6396
7438
  init_transcriber();
6397
7439
  });
6398
7440
 
7441
+ // src/lib/machine.ts
7442
+ import { hostname } from "os";
7443
+ function currentMachineId(env = process.env, hostName = hostname()) {
7444
+ const configured = env.HASNA_MACHINE_ID?.trim();
7445
+ if (configured)
7446
+ return configured;
7447
+ return hostName.trim();
7448
+ }
7449
+ var init_machine = () => {};
7450
+
6399
7451
  // src/mcp/index.ts
6400
7452
  var exports_mcp = {};
6401
7453
  __export(exports_mcp, {
@@ -6403,6 +7455,13 @@ __export(exports_mcp, {
6403
7455
  });
6404
7456
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6405
7457
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7458
+ function runtimeConfig() {
7459
+ return {
7460
+ ...loadConfig(),
7461
+ db_path: config.db_path,
7462
+ audio_dir: config.audio_dir
7463
+ };
7464
+ }
6406
7465
  function buildServer() {
6407
7466
  const server = new McpServer({
6408
7467
  name: "recordings",
@@ -6414,34 +7473,205 @@ function buildServer() {
6414
7473
  }
6415
7474
  function errorResult(e) {
6416
7475
  const msg = e instanceof Error ? e.message : String(e);
6417
- return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
7476
+ return { content: [{ type: "text", text: `Error: ${truncateText(msg, 500)}` }], isError: true };
6418
7477
  }
6419
7478
  function compact(r) {
6420
- const t = (r.processed_text || r.raw_text).slice(0, 80);
6421
- return `${r.id.slice(0, 8)} | ${r.processing_mode} | ${r.created_at.slice(0, 16)} | ${t}${t.length >= 80 ? "..." : ""}`;
7479
+ const preview = truncateText(r.processed_text || r.raw_text, 100);
7480
+ return `${truncateText(r.id, 8)} | ${truncateText(r.processing_mode, 16)} | ${truncateText(r.created_at, 16)} | ${preview}`;
6422
7481
  }
6423
- function full(r) {
6424
- const lines = [`ID: ${r.id}`, `Mode: ${r.processing_mode}`, `Model: ${r.model_used}`];
7482
+ function detail(r) {
7483
+ const lines = [`ID: ${truncateText(r.id, 80)}`, `Mode: ${truncateText(r.processing_mode, 16)}`, `Model: ${truncateText(r.model_used, 80)}`];
6425
7484
  if (r.enhancement_model)
6426
- lines.push(`Enhanced by: ${r.enhancement_model}`);
7485
+ lines.push(`Enhanced by: ${truncateText(r.enhancement_model, 80)}`);
6427
7486
  if (r.duration_ms)
6428
7487
  lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
6429
7488
  if (r.language)
6430
- lines.push(`Language: ${r.language}`);
7489
+ lines.push(`Language: ${truncateText(r.language, 20)}`);
6431
7490
  if (r.tags.length > 0)
6432
- lines.push(`Tags: ${r.tags.join(", ")}`);
7491
+ lines.push(`Tags: ${r.tags.map((tag) => truncateText(tag, 80)).join(", ")}`);
6433
7492
  if (r.agent_id)
6434
- lines.push(`Agent: ${r.agent_id}`);
7493
+ lines.push(`Agent: ${truncateText(r.agent_id, 80)}`);
6435
7494
  if (r.project_id)
6436
- lines.push(`Project: ${r.project_id}`);
7495
+ lines.push(`Project: ${truncateText(r.project_id, 80)}`);
6437
7496
  if (r.session_id)
6438
- lines.push(`Session: ${r.session_id}`);
6439
- lines.push(`Created: ${r.created_at}`);
6440
- lines.push(`Text: ${r.raw_text}`);
7497
+ lines.push(`Session: ${truncateText(r.session_id, 80)}`);
7498
+ lines.push(`Created: ${truncateText(r.created_at, 40)}`);
7499
+ lines.push(`Text: ${stripTerminalControls(r.raw_text)}`);
6441
7500
  if (r.processed_text && r.processed_text !== r.raw_text) {
6442
- lines.push(`Enhanced: ${r.processed_text}`);
7501
+ lines.push(`Enhanced: ${stripTerminalControls(r.processed_text)}`);
6443
7502
  }
6444
7503
  return lines.join(`
7504
+ `);
7505
+ }
7506
+ function verboseRecording(r) {
7507
+ const lines = [
7508
+ `${truncateText(r.id, 8)} | ${truncateText(r.processing_mode, 16)} | ${truncateText(r.created_at, 16)}`,
7509
+ `Preview: ${truncateText(r.processed_text || r.raw_text, 180)}`,
7510
+ `Model: ${r.enhancement_model ? `${truncateText(r.model_used, 80)} -> ${truncateText(r.enhancement_model, 80)}` : truncateText(r.model_used, 80)}`
7511
+ ];
7512
+ if (r.duration_ms)
7513
+ lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
7514
+ if (r.language)
7515
+ lines.push(`Language: ${truncateText(r.language, 20)}`);
7516
+ if (r.tags.length > 0)
7517
+ lines.push(`Tags: ${summarizeTags(r.tags)}`);
7518
+ const scopes = [
7519
+ r.agent_id ? `agent=${truncateText(r.agent_id, 80)}` : null,
7520
+ r.project_id ? `project=${truncateText(r.project_id, 80)}` : null,
7521
+ r.session_id ? `session=${truncateText(r.session_id, 80)}` : null
7522
+ ].filter(Boolean);
7523
+ if (scopes.length > 0)
7524
+ lines.push(`Scope: ${scopes.join(" ")}`);
7525
+ lines.push(`Details: get_recording { "id": "${truncateText(r.id, 8)}" }`);
7526
+ return lines.join(`
7527
+ `);
7528
+ }
7529
+ function applyTranscriptionArgs(cfg, args) {
7530
+ if (args.language)
7531
+ cfg.language = args.language;
7532
+ const transcriptionPrompt = args.transcription_prompt ?? args.prompt;
7533
+ if (transcriptionPrompt !== undefined) {
7534
+ cfg.transcription_prompt = transcriptionPrompt;
7535
+ }
7536
+ const transcriberPrompt = args.transcriber_prompt ?? args.system_prompt;
7537
+ if (transcriberPrompt !== undefined) {
7538
+ cfg.transcriber_prompt = transcriberPrompt;
7539
+ }
7540
+ if (args.transcriber_model) {
7541
+ cfg.transcriber_model = args.transcriber_model;
7542
+ } else if (args.enhancement_model) {
7543
+ cfg.enhancement_model = args.enhancement_model;
7544
+ cfg.transcriber_model = args.enhancement_model;
7545
+ }
7546
+ const requestedMode = args.post_processing_mode ?? args.post_processing;
7547
+ if (args.no_enhance || args.enhance === false) {
7548
+ cfg.post_processing_mode = "off";
7549
+ normalizePostProcessingConfig(cfg, true);
7550
+ } else if (args.enhance === true) {
7551
+ cfg.post_processing_mode = "always";
7552
+ normalizePostProcessingConfig(cfg, true);
7553
+ } else if (requestedMode) {
7554
+ cfg.post_processing_mode = normalizePostProcessingMode(requestedMode, cfg.post_processing_mode ?? "auto");
7555
+ normalizePostProcessingConfig(cfg, true);
7556
+ } else {
7557
+ normalizePostProcessingConfig(cfg, false);
7558
+ }
7559
+ return cfg;
7560
+ }
7561
+ function buildTranscriptionMetadata(cfg, processed, sources = {}) {
7562
+ const transcriptionPromptConfigured = Boolean(cfg.transcription_prompt?.trim());
7563
+ const transcriberPromptConfigured = Boolean(cfg.transcriber_prompt?.trim());
7564
+ return {
7565
+ transcription_prompt: {
7566
+ configured: transcriptionPromptConfigured,
7567
+ source: sources.transcriptionPromptFromRequest ? "request" : transcriptionPromptConfigured ? "config" : "none"
7568
+ },
7569
+ transcriber_prompt: {
7570
+ configured: transcriberPromptConfigured,
7571
+ source: sources.transcriberPromptFromRequest ? "request" : transcriberPromptConfigured ? "config" : "none"
7572
+ },
7573
+ post_processing: {
7574
+ mode: processed.post_processing_mode,
7575
+ applied: processed.mode === "enhanced",
7576
+ reason: processed.enhancement_reason,
7577
+ model: processed.enhancement_model
7578
+ },
7579
+ transcriber_model: resolveTranscriberModel(cfg)
7580
+ };
7581
+ }
7582
+ function recordingJson(r) {
7583
+ return JSON.stringify({
7584
+ id: r.id,
7585
+ raw_text: r.raw_text,
7586
+ processed_text: r.processed_text,
7587
+ processing_mode: r.processing_mode,
7588
+ model_used: r.model_used,
7589
+ enhancement_model: r.enhancement_model,
7590
+ language: r.language,
7591
+ tags: r.tags,
7592
+ metadata: r.metadata,
7593
+ created_at: r.created_at
7594
+ }, null, 2);
7595
+ }
7596
+ function truncateText(value, max) {
7597
+ const normalized = sanitizeInline(value);
7598
+ const prefix = [];
7599
+ for (const point of normalized) {
7600
+ if (prefix.length === max) {
7601
+ return `${prefix.slice(0, Math.max(0, max - 3)).join("")}...`;
7602
+ }
7603
+ prefix.push(point);
7604
+ }
7605
+ return normalized;
7606
+ }
7607
+ function stripTerminalControls(value) {
7608
+ 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, "");
7609
+ }
7610
+ function sanitizeInline(value) {
7611
+ return stripTerminalControls(value).replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
7612
+ }
7613
+ function summarizeTags(tags) {
7614
+ const shown = tags.slice(0, 3).map((tag) => truncateText(tag, 20));
7615
+ if (tags.length > shown.length)
7616
+ shown.push(`+${tags.length - shown.length}`);
7617
+ return shown.join(", ");
7618
+ }
7619
+ function resolveMcpPagination(args, defaultLimit = 10) {
7620
+ const requested = Number.isFinite(args.limit) && args.limit && args.limit > 0 ? Math.floor(args.limit) : defaultLimit;
7621
+ const max = args.full ? 10 : 50;
7622
+ const rawOffset = args.cursor ?? args.offset ?? 0;
7623
+ return {
7624
+ limit: Math.min(requested, max),
7625
+ offset: Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset)) : 0,
7626
+ capped: requested > max
7627
+ };
7628
+ }
7629
+ function withoutPagination(filter) {
7630
+ const { limit: _limit, offset: _offset, ...rest } = filter;
7631
+ return rest;
7632
+ }
7633
+ function formatMcpCollection(label, recordings, options) {
7634
+ if (recordings.length === 0) {
7635
+ return options.total === 0 ? `No ${label} found.` : `No ${label} at cursor ${options.offset}.`;
7636
+ }
7637
+ const format = options.full ? verboseRecording : compact;
7638
+ const separator = options.full ? `
7639
+ ---
7640
+ ` : `
7641
+ `;
7642
+ const end = options.offset + recordings.length;
7643
+ const total = Math.max(options.total, end);
7644
+ const lines = [
7645
+ `${label}: showing ${recordings.length} of ${total} (${options.offset + 1}-${end}, limit ${options.limit})`,
7646
+ recordings.map(format).join(separator)
7647
+ ];
7648
+ const hints = [];
7649
+ if (options.capped)
7650
+ hints.push(`limit capped at ${options.limit} for MCP output`);
7651
+ if (end < total)
7652
+ hints.push(`next cursor: ${end}`);
7653
+ hints.push("use get_recording { id } for full details; set full=true for richer rows");
7654
+ lines.push(`Hints: ${hints.join("; ")}.`);
7655
+ return lines.join(separator);
7656
+ }
7657
+ function formatMcpPage(label, items, args, format) {
7658
+ const pagination = resolveMcpPagination(args, 20);
7659
+ const page = items.slice(pagination.offset, pagination.offset + pagination.limit);
7660
+ if (page.length === 0) {
7661
+ return items.length === 0 ? `No ${label}.` : `No ${label} at cursor ${pagination.offset}.`;
7662
+ }
7663
+ const end = pagination.offset + page.length;
7664
+ const hints = [];
7665
+ if (pagination.capped)
7666
+ hints.push(`limit capped at ${pagination.limit} for MCP output`);
7667
+ if (end < items.length)
7668
+ hints.push(`next cursor: ${end}`);
7669
+ return [
7670
+ `${label}: showing ${page.length} of ${items.length} (${pagination.offset + 1}-${end}, limit ${pagination.limit})`,
7671
+ page.map(format).join(`
7672
+ `),
7673
+ hints.length > 0 ? `Hints: ${hints.join("; ")}.` : ""
7674
+ ].filter(Boolean).join(`
6445
7675
  `);
6446
7676
  }
6447
7677
  async function saveRecordingMemento(args) {
@@ -6469,26 +7699,30 @@ function buildServer() {
6469
7699
  } catch {}
6470
7700
  }
6471
7701
  const toolDocs = {
6472
- transcribe_audio: `Transcribe audio file. Auto-enhances if needed.
6473
- 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)`,
7702
+ recordings_status: `Show safe service status for agents.
7703
+ Params: none
7704
+ 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.`,
7705
+ transcribe_audio: `Transcribe audio file and save raw plus optional processed text.
7706
+ 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)
7707
+ Returns: JSON recording summary with raw_text, processed_text, processing_mode, and safe metadata.`,
6474
7708
  save_recording: `Save text as recording. Auto-enhances if needed.
6475
- Params: text (string, required): text to save | enhance (bool): force enhancement | tags (string[]) | agent_id (string) | project_id (string) | session_id (string) | metadata (object)`,
7709
+ 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)`,
6476
7710
  get_recording: `Get recording by ID or prefix.
6477
7711
  Params: id (string, required): recording ID or prefix`,
6478
- list_recordings: `List recordings, compact by default, most recent first.
6479
- 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`,
6480
- search_recordings: `Search recordings by text content.
6481
- Params: query (string, required) | limit (number, default 10) | agent_id | project_id | full (bool): verbose output`,
7712
+ list_recordings: `List recordings, compact and capped by default, most recent first.
7713
+ 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.`,
7714
+ search_recordings: `Search recordings by text content, compact and capped by default.
7715
+ 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.`,
6482
7716
  delete_recording: `Delete recording by ID.
6483
7717
  Params: id (string, required)`,
6484
- recording_stats: `Recording count, mode breakdown, duration.
6485
- Params: none`,
7718
+ recording_stats: `Recording count, mode breakdown, duration, and capped model breakdown.
7719
+ Params: limit_models (number, default 10, max 10 compact / 50 verbose) | verbose (bool): allow up to 50 model counts`,
6486
7720
  detect_enhancement: `Check if text needs AI enhancement.
6487
7721
  Params: text (string, required)`,
6488
7722
  register_agent: `Register agent (idempotent). Auto-updates last_seen_at on re-register.
6489
7723
  Params: name (string, required) | description (string) | role (string)`,
6490
- list_agents: `List registered agents.
6491
- Params: none`,
7724
+ list_agents: `List registered agents, capped by default.
7725
+ Params: limit (number, default 20, max 50) | offset or cursor (number)`,
6492
7726
  get_agent: `Get agent by ID or name.
6493
7727
  Params: id (string, required)`,
6494
7728
  heartbeat: `Update last_seen_at to signal agent is active.
@@ -6497,16 +7731,59 @@ Params: agent_id (string, required): agent ID or name`,
6497
7731
  Params: agent_id (string, required) | project_id (string, nullable): project ID or null to clear`,
6498
7732
  register_project: `Register project (idempotent).
6499
7733
  Params: name (string, required) | path (string, required): absolute path | description (string)`,
6500
- list_projects: `List registered projects.
6501
- Params: none`
7734
+ list_projects: `List registered projects, capped by default.
7735
+ Params: limit (number, default 20, max 50) | offset or cursor (number)`
6502
7736
  };
6503
7737
  registerTool("describe_tool", "Get full param docs for any tool.", { name: exports_external.string() }, async (args) => {
6504
7738
  const doc = toolDocs[args.name];
6505
- return doc ? text(doc) : text(`Unknown tool: ${args.name}. Available: ${Object.keys(toolDocs).join(", ")}`);
7739
+ return doc ? text(doc) : text(`Unknown tool: ${truncateText(args.name, 80)}. Available: ${Object.keys(toolDocs).join(", ")}`);
7740
+ });
7741
+ registerTool("recordings_status", "Show safe service status for agents.", {}, async () => {
7742
+ try {
7743
+ const cfg = runtimeConfig();
7744
+ const stats = await getStore().getRecordingStats();
7745
+ return text(JSON.stringify({
7746
+ service: "recordings",
7747
+ version: VERSION,
7748
+ mcp: {
7749
+ default_http_port: DEFAULT_MCP_HTTP_PORT,
7750
+ endpoint: "/mcp"
7751
+ },
7752
+ config: {
7753
+ transcription_model: cfg.transcription_model,
7754
+ realtime_session_model: cfg.realtime_session_model,
7755
+ realtime_transcription_model: cfg.realtime_transcription_model,
7756
+ enhancement_model: cfg.enhancement_model,
7757
+ transcriber_model: resolveTranscriberModel(cfg),
7758
+ language: cfg.language,
7759
+ auto_enhance: cfg.auto_enhance,
7760
+ post_processing_mode: cfg.post_processing_mode,
7761
+ transcription_prompt_configured: Boolean(cfg.transcription_prompt?.trim()),
7762
+ transcriber_prompt_configured: Boolean(cfg.transcriber_prompt?.trim()),
7763
+ max_recording_seconds: cfg.max_recording_seconds,
7764
+ db_path: cfg.db_path,
7765
+ audio_dir: cfg.audio_dir,
7766
+ openai_api_key_configured: Boolean(cfg.openai_api_key),
7767
+ enhancement_api_key_configured: Boolean(cfg.enhancement_api_key || cfg.openai_api_key),
7768
+ config_warnings: cfg.config_warnings ?? []
7769
+ },
7770
+ stats
7771
+ }, null, 2));
7772
+ } catch (e) {
7773
+ return errorResult(e);
7774
+ }
6506
7775
  });
6507
7776
  registerTool("transcribe_audio", "Transcribe audio file. Auto-enhances if needed.", {
6508
7777
  audio_path: exports_external.string(),
6509
7778
  language: exports_external.string().optional(),
7779
+ transcription_prompt: exports_external.string().optional(),
7780
+ prompt: exports_external.string().optional(),
7781
+ transcriber_prompt: exports_external.string().optional(),
7782
+ system_prompt: exports_external.string().optional(),
7783
+ post_processing_mode: exports_external.enum(["off", "auto", "always"]).optional(),
7784
+ post_processing: exports_external.enum(["off", "auto", "always"]).optional(),
7785
+ transcriber_model: exports_external.string().optional(),
7786
+ enhancement_model: exports_external.string().optional(),
6510
7787
  no_enhance: exports_external.boolean().optional(),
6511
7788
  tags: exports_external.array(exports_external.string()).optional(),
6512
7789
  agent_id: exports_external.string().optional(),
@@ -6514,13 +7791,13 @@ Params: none`
6514
7791
  session_id: exports_external.string().optional()
6515
7792
  }, async (args) => {
6516
7793
  try {
6517
- const cfg = { ...config };
6518
- if (args.language)
6519
- cfg.language = args.language;
6520
- if (args.no_enhance)
6521
- cfg.auto_enhance = false;
7794
+ const cfg = applyTranscriptionArgs({ ...runtimeConfig() }, args);
6522
7795
  const transcription = await transcribeAudio(args.audio_path, cfg);
6523
7796
  const processed = await processText(transcription.text, cfg);
7797
+ const metadata = buildTranscriptionMetadata(cfg, processed, {
7798
+ transcriptionPromptFromRequest: args.transcription_prompt !== undefined || args.prompt !== undefined,
7799
+ transcriberPromptFromRequest: args.transcriber_prompt !== undefined || args.system_prompt !== undefined
7800
+ });
6524
7801
  const recording = await getStore().createRecording({
6525
7802
  audio_path: args.audio_path,
6526
7803
  raw_text: transcription.text,
@@ -6533,7 +7810,9 @@ Params: none`
6533
7810
  tags: args.tags,
6534
7811
  agent_id: args.agent_id,
6535
7812
  project_id: args.project_id,
6536
- session_id: args.session_id
7813
+ session_id: args.session_id,
7814
+ machine_id: currentMachineId(),
7815
+ metadata
6537
7816
  });
6538
7817
  if (args.agent_id) {
6539
7818
  await saveRecordingMemento({
@@ -6549,8 +7828,7 @@ Params: none`
6549
7828
  summary: `Recording ${recording.id.slice(0, 8)} for ${args.agent_id}`
6550
7829
  });
6551
7830
  }
6552
- const output = processed.mode === "enhanced" ? processed.text : transcription.text;
6553
- return text(`${recording.id.slice(0, 8)} | ${processed.mode} | ${output}`);
7831
+ return text(recordingJson(recording));
6554
7832
  } catch (e) {
6555
7833
  return errorResult(e);
6556
7834
  }
@@ -6558,6 +7836,12 @@ Params: none`
6558
7836
  registerTool("save_recording", "Save text as recording. Auto-enhances if needed.", {
6559
7837
  text: exports_external.string(),
6560
7838
  enhance: exports_external.boolean().optional(),
7839
+ transcriber_prompt: exports_external.string().optional(),
7840
+ system_prompt: exports_external.string().optional(),
7841
+ post_processing_mode: exports_external.enum(["off", "auto", "always"]).optional(),
7842
+ post_processing: exports_external.enum(["off", "auto", "always"]).optional(),
7843
+ transcriber_model: exports_external.string().optional(),
7844
+ enhancement_model: exports_external.string().optional(),
6561
7845
  tags: exports_external.array(exports_external.string()).optional(),
6562
7846
  agent_id: exports_external.string().optional(),
6563
7847
  project_id: exports_external.string().optional(),
@@ -6568,17 +7852,28 @@ Params: none`
6568
7852
  metadata: exports_external.record(exports_external.unknown()).optional()
6569
7853
  }, async (args) => {
6570
7854
  try {
7855
+ const cfg = applyTranscriptionArgs(runtimeConfig(), args);
6571
7856
  let processedText;
6572
7857
  let mode = "raw";
6573
7858
  let enhModel;
6574
- if (args.enhance !== false) {
6575
- const processed = await processText(args.text, config, undefined, { force: args.enhance === true });
6576
- if (processed.mode === "enhanced") {
6577
- processedText = processed.text;
6578
- mode = "enhanced";
6579
- enhModel = processed.enhancement_model || undefined;
6580
- }
7859
+ let processed = {
7860
+ text: args.text,
7861
+ mode: "raw",
7862
+ enhancement_model: null,
7863
+ post_processing_mode: cfg.post_processing_mode ?? "auto",
7864
+ enhancement_reason: null
7865
+ };
7866
+ if (cfg.post_processing_mode !== "off") {
7867
+ processed = await processText(args.text, cfg);
7868
+ }
7869
+ if (processed.mode === "enhanced") {
7870
+ processedText = processed.text;
7871
+ mode = "enhanced";
7872
+ enhModel = processed.enhancement_model || undefined;
6581
7873
  }
7874
+ const processingMetadata = buildTranscriptionMetadata(cfg, processed, {
7875
+ transcriberPromptFromRequest: args.transcriber_prompt !== undefined || args.system_prompt !== undefined
7876
+ });
6582
7877
  const recording = await getStore().createRecording({
6583
7878
  raw_text: args.text,
6584
7879
  processed_text: processedText,
@@ -6592,10 +7887,13 @@ Params: none`
6592
7887
  goal: args.goal,
6593
7888
  role: args.role,
6594
7889
  task_list_id: args.task_list_id,
6595
- metadata: args.metadata
7890
+ machine_id: currentMachineId(),
7891
+ metadata: {
7892
+ ...args.metadata ?? {},
7893
+ ...processingMetadata
7894
+ }
6596
7895
  });
6597
- const output = processedText || args.text;
6598
- return text(`${recording.id.slice(0, 8)} | ${mode} | ${output}`);
7896
+ return text(recordingJson(recording));
6599
7897
  } catch (e) {
6600
7898
  return errorResult(e);
6601
7899
  }
@@ -6604,8 +7902,8 @@ Params: none`
6604
7902
  try {
6605
7903
  const r = await getStore().getRecording(args.id);
6606
7904
  if (!r)
6607
- return text(`Not found: ${args.id}`);
6608
- return text(full(r));
7905
+ return text(`Not found: ${truncateText(args.id, 80)}`);
7906
+ return text(detail(r));
6609
7907
  } catch (e) {
6610
7908
  return errorResult(e);
6611
7909
  }
@@ -6613,6 +7911,7 @@ Params: none`
6613
7911
  registerTool("list_recordings", "List recordings. Compact default, recent first.", {
6614
7912
  limit: exports_external.number().optional(),
6615
7913
  offset: exports_external.number().optional(),
7914
+ cursor: exports_external.number().optional(),
6616
7915
  processing_mode: exports_external.enum(["raw", "enhanced"]).optional(),
6617
7916
  tags: exports_external.array(exports_external.string()).optional(),
6618
7917
  search: exports_external.string().optional(),
@@ -6624,9 +7923,10 @@ Params: none`
6624
7923
  full: exports_external.boolean().optional()
6625
7924
  }, async (args) => {
6626
7925
  try {
7926
+ const pagination = resolveMcpPagination(args);
6627
7927
  const filter = {
6628
- limit: args.limit || 10,
6629
- offset: args.offset,
7928
+ limit: pagination.limit,
7929
+ offset: pagination.offset,
6630
7930
  processing_mode: args.processing_mode,
6631
7931
  tags: args.tags,
6632
7932
  search: args.search,
@@ -6636,15 +7936,18 @@ Params: none`
6636
7936
  project_id: args.project_id,
6637
7937
  session_id: args.session_id
6638
7938
  };
6639
- const recordings = await getStore().listRecordings(filter);
6640
- if (recordings.length === 0)
6641
- return text("No recordings found.");
6642
- const fmt = args.full ? full : compact;
6643
- const sep = args.full ? `
6644
- ---
6645
- ` : `
6646
- `;
6647
- return text(`${recordings.length} recording(s):${sep}${recordings.map(fmt).join(sep)}`);
7939
+ const store = getStore();
7940
+ const [recordings, total] = await Promise.all([
7941
+ store.listRecordings(filter),
7942
+ countStoreRecordings(store, withoutPagination(filter))
7943
+ ]);
7944
+ return text(formatMcpCollection("recordings", recordings, {
7945
+ total,
7946
+ offset: pagination.offset,
7947
+ limit: pagination.limit,
7948
+ full: args.full,
7949
+ capped: pagination.capped
7950
+ }));
6648
7951
  } catch (e) {
6649
7952
  return errorResult(e);
6650
7953
  }
@@ -6652,42 +7955,63 @@ Params: none`
6652
7955
  registerTool("search_recordings", "Search recordings by text.", {
6653
7956
  query: exports_external.string(),
6654
7957
  limit: exports_external.number().optional(),
7958
+ offset: exports_external.number().optional(),
7959
+ cursor: exports_external.number().optional(),
6655
7960
  agent_id: exports_external.string().optional(),
6656
7961
  project_id: exports_external.string().optional(),
7962
+ session_id: exports_external.string().optional(),
6657
7963
  full: exports_external.boolean().optional()
6658
7964
  }, async (args) => {
6659
7965
  try {
6660
- const results = await getStore().searchRecordings(args.query, {
6661
- limit: args.limit || 10,
7966
+ const pagination = resolveMcpPagination(args);
7967
+ const filter = {
7968
+ limit: pagination.limit,
7969
+ offset: pagination.offset,
6662
7970
  agent_id: args.agent_id,
6663
- project_id: args.project_id
6664
- });
6665
- if (results.length === 0)
6666
- return text("No results.");
6667
- const fmt = args.full ? full : compact;
6668
- const sep = args.full ? `
6669
- ---
6670
- ` : `
6671
- `;
6672
- return text(`${results.length} result(s):${sep}${results.map(fmt).join(sep)}`);
7971
+ project_id: args.project_id,
7972
+ session_id: args.session_id
7973
+ };
7974
+ const store = getStore();
7975
+ const [results, total] = await Promise.all([
7976
+ store.searchRecordings(args.query, filter),
7977
+ countStoreRecordings(store, withoutPagination({ ...filter, search: args.query }))
7978
+ ]);
7979
+ return text(formatMcpCollection("results", results, {
7980
+ total,
7981
+ offset: pagination.offset,
7982
+ limit: pagination.limit,
7983
+ full: args.full,
7984
+ capped: pagination.capped
7985
+ }));
6673
7986
  } catch (e) {
6674
7987
  return errorResult(e);
6675
7988
  }
6676
7989
  });
6677
7990
  registerTool("delete_recording", "Delete recording by ID.", { id: exports_external.string() }, async (args) => {
6678
7991
  try {
6679
- return text(await getStore().deleteRecording(args.id) ? `Deleted ${args.id}` : `Not found: ${args.id}`);
7992
+ return text(await getStore().deleteRecording(args.id) ? `Deleted ${truncateText(args.id, 80)}` : `Not found: ${truncateText(args.id, 80)}`);
6680
7993
  } catch (e) {
6681
7994
  return errorResult(e);
6682
7995
  }
6683
7996
  });
6684
- registerTool("recording_stats", "Recording stats: count, modes, duration.", {}, async () => {
7997
+ registerTool("recording_stats", "Recording stats: count, modes, duration, and capped model breakdown.", {
7998
+ limit_models: exports_external.number().optional(),
7999
+ verbose: exports_external.boolean().optional()
8000
+ }, async (args) => {
6685
8001
  try {
6686
8002
  const s = await getStore().getRecordingStats();
6687
8003
  let out = `Total: ${s.total} | Raw: ${s.raw} | Enhanced: ${s.enhanced} | Duration: ${(s.total_duration_ms / 1000).toFixed(1)}s`;
6688
- if (Object.keys(s.by_model).length > 0) {
8004
+ const models = Object.entries(s.by_model).sort((a, b) => b[1] - a[1]);
8005
+ if (models.length > 0) {
8006
+ const maxModels = args.verbose ? 50 : 10;
8007
+ const requested = Number.isFinite(args.limit_models) && args.limit_models && args.limit_models > 0 ? Math.floor(args.limit_models) : maxModels;
8008
+ const limit = Math.min(requested, maxModels);
6689
8009
  out += `
6690
- ` + Object.entries(s.by_model).map(([m, c]) => `${m}: ${c}`).join(", ");
8010
+ Models: ` + models.slice(0, limit).map(([m, c]) => `${truncateText(m, 80)}: ${c}`).join(", ");
8011
+ if (limit < models.length) {
8012
+ out += `
8013
+ Hints: ${models.length - limit} more model(s); call recording_stats with verbose=true and limit_models up to 50.`;
8014
+ }
6691
8015
  }
6692
8016
  return text(out);
6693
8017
  } catch (e) {
@@ -6696,7 +8020,7 @@ Params: none`
6696
8020
  });
6697
8021
  registerTool("detect_enhancement", "Check if text needs AI enhancement.", { text: exports_external.string() }, async (args) => {
6698
8022
  try {
6699
- const r = needsEnhancement(args.text, config);
8023
+ const r = needsEnhancement(args.text, runtimeConfig());
6700
8024
  return text(`${r.needs ? "Yes" : "No"}: ${r.reason}`);
6701
8025
  } catch (e) {
6702
8026
  return errorResult(e);
@@ -6705,18 +8029,19 @@ Params: none`
6705
8029
  registerTool("register_agent", "Register agent (idempotent).", { name: exports_external.string(), description: exports_external.string().optional(), role: exports_external.string().optional() }, async (args) => {
6706
8030
  try {
6707
8031
  const a = await getStore().registerAgent(args.name, args.description, args.role);
6708
- return text(`${a.id} | ${a.name} | ${a.role}`);
8032
+ return text(`${truncateText(a.id, 80)} | ${truncateText(a.name, 80)} | ${truncateText(a.role, 40)}`);
6709
8033
  } catch (e) {
6710
8034
  return errorResult(e);
6711
8035
  }
6712
8036
  });
6713
- registerTool("list_agents", "List registered agents.", {}, async () => {
8037
+ registerTool("list_agents", "List registered agents, capped by default.", {
8038
+ limit: exports_external.number().optional(),
8039
+ offset: exports_external.number().optional(),
8040
+ cursor: exports_external.number().optional()
8041
+ }, async (args) => {
6714
8042
  try {
6715
8043
  const agents = await getStore().listAgents();
6716
- if (agents.length === 0)
6717
- return text("None.");
6718
- return text(agents.map((a) => `${a.id} | ${a.name} | ${a.role}`).join(`
6719
- `));
8044
+ 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)}`));
6720
8045
  } catch (e) {
6721
8046
  return errorResult(e);
6722
8047
  }
@@ -6725,8 +8050,8 @@ Params: none`
6725
8050
  try {
6726
8051
  const a = await getStore().getAgent(args.id);
6727
8052
  if (!a)
6728
- return text(`Not found: ${args.id}`);
6729
- return text(`${a.id} | ${a.name} | ${a.role} | ${a.last_seen_at}`);
8053
+ return text(`Not found: ${truncateText(args.id, 80)}`);
8054
+ return text(`${truncateText(a.id, 80)} | ${truncateText(a.name, 80)} | ${truncateText(a.role, 40)} | ${truncateText(a.last_seen_at, 40)}`);
6730
8055
  } catch (e) {
6731
8056
  return errorResult(e);
6732
8057
  }
@@ -6734,18 +8059,19 @@ Params: none`
6734
8059
  registerTool("register_project", "Register project (idempotent).", { name: exports_external.string(), path: exports_external.string(), description: exports_external.string().optional() }, async (args) => {
6735
8060
  try {
6736
8061
  const p = await getStore().registerProject(args.name, args.path, args.description);
6737
- return text(`${p.id.slice(0, 8)} | ${p.name} | ${p.path}`);
8062
+ return text(`${truncateText(p.id, 8)} | ${truncateText(p.name, 80)} | ${truncateText(p.path, 120)}`);
6738
8063
  } catch (e) {
6739
8064
  return errorResult(e);
6740
8065
  }
6741
8066
  });
6742
- registerTool("list_projects", "List registered projects.", {}, async () => {
8067
+ registerTool("list_projects", "List registered projects, capped by default.", {
8068
+ limit: exports_external.number().optional(),
8069
+ offset: exports_external.number().optional(),
8070
+ cursor: exports_external.number().optional()
8071
+ }, async (args) => {
6743
8072
  try {
6744
8073
  const projects = await getStore().listProjects();
6745
- if (projects.length === 0)
6746
- return text("None.");
6747
- return text(projects.map((p) => `${p.id.slice(0, 8)} | ${p.name} | ${p.path}`).join(`
6748
- `));
8074
+ return text(formatMcpPage("projects", projects, args, (project) => `${truncateText(project.id, 8)} | ${truncateText(project.name, 80)} | ${truncateText(project.path, 120)}`));
6749
8075
  } catch (e) {
6750
8076
  return errorResult(e);
6751
8077
  }
@@ -6754,8 +8080,8 @@ Params: none`
6754
8080
  try {
6755
8081
  const agent = await getStore().heartbeatAgent(args.agent_id);
6756
8082
  if (!agent)
6757
- return text(`Agent not found: ${args.agent_id}`);
6758
- return text(`${agent.id} | ${agent.name} | last_seen: ${agent.last_seen_at}`);
8083
+ return text(`Agent not found: ${truncateText(args.agent_id, 80)}`);
8084
+ return text(`${truncateText(agent.id, 80)} | ${truncateText(agent.name, 80)} | last_seen: ${truncateText(agent.last_seen_at, 40)}`);
6759
8085
  } catch (e) {
6760
8086
  return errorResult(e);
6761
8087
  }
@@ -6764,8 +8090,8 @@ Params: none`
6764
8090
  try {
6765
8091
  const agent = await getStore().setAgentFocus(args.agent_id, args.project_id ?? null);
6766
8092
  if (!agent)
6767
- return text(`Agent not found: ${args.agent_id}`);
6768
- return text(args.project_id ? `Focus set: ${args.project_id}` : "Focus cleared");
8093
+ return text(`Agent not found: ${truncateText(args.agent_id, 80)}`);
8094
+ return text(args.project_id ? `Focus set: ${truncateText(args.project_id, 80)}` : "Focus cleared");
6769
8095
  } catch (e) {
6770
8096
  return errorResult(e);
6771
8097
  }
@@ -6797,6 +8123,7 @@ var init_mcp = __esm(async () => {
6797
8123
  init_store();
6798
8124
  init_transcriber();
6799
8125
  init_enhancer();
8126
+ init_machine();
6800
8127
  config = loadConfig();
6801
8128
  ensureDataDir(config);
6802
8129
  if (false) {}
@@ -6837,7 +8164,16 @@ function checkRateLimit(ip) {
6837
8164
  }
6838
8165
  return { allowed: true };
6839
8166
  }
6840
- function buildFetch() {
8167
+ function buildFetch(options = {}) {
8168
+ const checkCloudAuth = options.checkCloudAuth ?? (async () => {
8169
+ const { getCloudVerifier: getCloudVerifier2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
8170
+ return getCloudVerifier2();
8171
+ });
8172
+ const checkCloud = options.pingCloud ?? (async () => {
8173
+ const { pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
8174
+ return pingCloud2();
8175
+ });
8176
+ const logError = options.logError ?? console.error;
6841
8177
  return async function fetch2(req, server) {
6842
8178
  const url = new URL(req.url);
6843
8179
  const path = url.pathname;
@@ -6867,9 +8203,11 @@ function buildFetch() {
6867
8203
  if (path === "/ready") {
6868
8204
  if (mode === "remote") {
6869
8205
  try {
6870
- await pingCloud();
6871
- } catch (e) {
6872
- return jsonResponse({ status: "unavailable", version: VERSION, mode, error: e.message }, 503);
8206
+ await checkCloudAuth();
8207
+ await checkCloud();
8208
+ } catch {
8209
+ logError("recordings-serve: readiness dependency check failed");
8210
+ return jsonResponse({ status: "unavailable", version: VERSION, mode, error: "dependency unavailable" }, 503);
6873
8211
  }
6874
8212
  }
6875
8213
  return jsonResponse({ status: "ready", version: VERSION, mode });
@@ -6908,9 +8246,9 @@ function buildFetch() {
6908
8246
  };
6909
8247
  }
6910
8248
  async function startServer(port, options) {
6911
- const hostname = options?.host || process.env.HOST || "127.0.0.1";
8249
+ const hostname2 = options?.host || process.env.HOST || "127.0.0.1";
6912
8250
  const fetch2 = buildFetch();
6913
- const server = Bun.serve({ port, hostname, fetch: fetch2 });
8251
+ const server = Bun.serve({ port, hostname: hostname2, fetch: fetch2 });
6914
8252
  const shutdown = async () => {
6915
8253
  try {
6916
8254
  const { closeCloud: closeCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
@@ -6922,12 +8260,11 @@ async function startServer(port, options) {
6922
8260
  process.on("SIGINT", shutdown);
6923
8261
  process.on("SIGTERM", shutdown);
6924
8262
  const mode = isCloudModeEnabled() ? "remote (A1 pure-remote)" : "local";
6925
- console.log(`recordings-serve listening on http://${hostname}:${port} (mode: ${mode})`);
8263
+ console.log(`recordings-serve listening on http://${hostname2}:${port} (mode: ${mode})`);
6926
8264
  return server;
6927
8265
  }
6928
8266
  var SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
6929
8267
  var init_serve = __esm(() => {
6930
- init_cloud();
6931
8268
  init_v1();
6932
8269
  init_openapi();
6933
8270
  SECURITY_HEADERS = {
@@ -6981,15 +8318,28 @@ Environment:
6981
8318
  HASNA_RECORDINGS_API_SIGNING_KEY HMAC signing secret for API-key auth`);
6982
8319
  }
6983
8320
  async function runMigrate() {
6984
- const { ensureCloudSchema: ensureCloudSchema2, pingCloud: pingCloud2, resolveCloudDatabaseUrl: resolveCloudDatabaseUrl2, closeCloud: closeCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
8321
+ const {
8322
+ closeCloud: closeCloud2,
8323
+ getCloudPg: getCloudPg2,
8324
+ migrateCloudSchema: migrateCloudSchema2,
8325
+ pingCloudConnectivity: pingCloudConnectivity3,
8326
+ resolveCloudDatabaseUrl: resolveCloudDatabaseUrl2
8327
+ } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
8328
+ const { assertCloudSchemaContract: assertCloudSchemaContract2 } = await Promise.resolve().then(() => (init_cloud_readiness(), exports_cloud_readiness));
8329
+ const { runCloudMigration: runCloudMigration2 } = await Promise.resolve().then(() => (init_migrate_command(), exports_migrate_command));
6985
8330
  if (!resolveCloudDatabaseUrl2()) {
6986
8331
  console.error("migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)");
6987
8332
  process.exit(2);
6988
8333
  }
6989
8334
  console.log("migrate: connecting\u2026");
6990
- await pingCloud2();
6991
- console.log("migrate: applying schema (recordings tables + api_keys)\u2026");
6992
- await ensureCloudSchema2();
8335
+ await runCloudMigration2({
8336
+ pingConnectivity: pingCloudConnectivity3,
8337
+ applyMigrations: async () => {
8338
+ console.log("migrate: applying schema (recordings tables + api_keys)\u2026");
8339
+ await migrateCloudSchema2();
8340
+ },
8341
+ validateContract: () => assertCloudSchemaContract2(getCloudPg2())
8342
+ });
6993
8343
  console.log("migrate: done");
6994
8344
  await closeCloud2();
6995
8345
  process.exit(0);