@hasna/recordings 0.3.2 → 0.3.9

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 (59) hide show
  1. package/README.md +38 -34
  2. package/bun.lock +96 -36
  3. package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
  4. package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
  5. package/dist/cli/index.js +1059 -219
  6. package/dist/cli/macos-shortcut.d.ts +2 -2
  7. package/dist/db/pg-migrations.d.ts +1 -1
  8. package/dist/http/client.d.ts +6 -14
  9. package/dist/http/client.d.ts.map +1 -1
  10. package/dist/index.js +1971 -175
  11. package/dist/lib/capture-probe.d.ts +3 -3
  12. package/dist/lib/capture-probe.d.ts.map +1 -1
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/macos-bundle.d.ts +1 -1
  15. package/dist/lib/release-install-policy.d.ts +21 -0
  16. package/dist/lib/release-install-policy.d.ts.map +1 -1
  17. package/dist/mcp/index.js +978 -142
  18. package/dist/sdk/index.d.ts +2 -2
  19. package/dist/sdk/index.js +7 -3
  20. package/dist/sdk/v1.generated.d.ts.map +1 -1
  21. package/dist/server/cloud-config.d.ts +4 -19
  22. package/dist/server/cloud-config.d.ts.map +1 -1
  23. package/dist/server/cloud.d.ts +1 -1
  24. package/dist/server/cloud.d.ts.map +1 -1
  25. package/dist/server/index.js +217 -299
  26. package/dist/server/serve.d.ts.map +1 -1
  27. package/dist/storage.d.ts +1 -1
  28. package/dist/storage.d.ts.map +1 -1
  29. package/dist/storage.js +1881 -90
  30. package/dist/store.d.ts.map +1 -1
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.d.ts.map +1 -1
  33. package/package.json +4 -3
  34. package/packaging/macos/build_release_pkg.sh +10 -4
  35. package/packaging/macos/managed_bootstrap.sh +4 -4
  36. package/packaging/macos/scripts/postinstall +2 -2
  37. package/packaging/macos/scripts/preinstall +2 -0
  38. package/scripts/generate-sdk.ts +17 -16
  39. package/scripts/install_macos_app.sh +22 -22
  40. package/scripts/macos_artifact.ts +48 -39
  41. package/scripts/migrate.ts +2 -2
  42. package/scripts/release-suite-gate.ts +174 -0
  43. package/scripts/set-version.ts +5 -5
  44. package/scripts/smoke_macos_app.sh +21 -21
  45. package/scripts/vacuity-manifests/version-sites.tsv +8 -4
  46. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  47. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
  48. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  49. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  50. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  51. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  52. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  53. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  54. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  55. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  56. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  57. package/src/native/Recordings/build.sh +14 -20
  58. package/dist/lib/retired-deployment-modes.d.ts +0 -24
  59. package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
@@ -17,8 +17,125 @@ var __export = (target, all) => {
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = import.meta.require;
19
19
 
20
+ // package.json
21
+ var package_default;
22
+ var init_package = __esm(() => {
23
+ package_default = {
24
+ name: "@hasna/recordings",
25
+ version: "0.3.9",
26
+ type: "module",
27
+ description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
28
+ repository: {
29
+ type: "git",
30
+ url: "git+https://github.com/hasna/apps"
31
+ },
32
+ main: "dist/index.js",
33
+ types: "dist/index.d.ts",
34
+ bin: {
35
+ recordings: "dist/cli/index.js",
36
+ "recordings-mcp": "dist/mcp/index.js",
37
+ "recordings-serve": "dist/server/index.js"
38
+ },
39
+ exports: {
40
+ ".": {
41
+ import: "./dist/index.js",
42
+ types: "./dist/index.d.ts"
43
+ },
44
+ "./storage": {
45
+ import: "./dist/storage.js",
46
+ types: "./dist/storage.d.ts"
47
+ },
48
+ "./sdk": {
49
+ import: "./dist/sdk/index.js",
50
+ types: "./dist/sdk/index.d.ts"
51
+ }
52
+ },
53
+ engines: {
54
+ bun: ">=1.0.0"
55
+ },
56
+ scripts: {
57
+ clean: "rm -rf dist",
58
+ build: "bun run clean && bun run build:cli && bun run build:mcp && bun run build:serve && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
59
+ "build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
60
+ "build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
61
+ "build:serve": "bun build src/server/index.ts --target=bun --outfile=dist/server/index.js --external=@modelcontextprotocol/sdk --external=@hasna/contracts --external=openai --external=pg",
62
+ "build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai",
63
+ "build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
64
+ "generate:sdk": "bun run scripts/generate-sdk.ts",
65
+ migrate: "bun run scripts/migrate.ts",
66
+ typecheck: "tsc --noEmit",
67
+ "typecheck:tcc-contract": "tsc --noEmit -p tsconfig.tcc-contract.json",
68
+ test: "bun test",
69
+ "verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
70
+ "verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
71
+ "verify:ci-native": "bun scripts/ci-native-build.ts",
72
+ "test:gated": 'bun scripts/ci-linux-suite.ts --check && RECORDINGS_TEST_TIMEOUT_MS="${RECORDINGS_TEST_TIMEOUT_MS:-120000}" bun test --timeout "$RECORDINGS_TEST_TIMEOUT_MS" $(bun scripts/ci-linux-suite.ts --gated)',
73
+ "test:vacuity-battery": 'bun scripts/vacuity-manifest-gen.ts > "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv" && bun scripts/vacuity-mutation-battery.ts "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv"',
74
+ "test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
75
+ "test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
76
+ "test:vacuity-battery:vars": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/variable-operands.tsv",
77
+ "test:vacuity-battery:reorder": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/reorder.tsv",
78
+ "test:coverage": "bun test --coverage",
79
+ "dev:cli": "bun run src/cli/index.ts",
80
+ "desktop:snapshot": "bun run src/cli/index.ts app snapshot",
81
+ "dev:mcp": "bun run src/mcp/index.ts",
82
+ "verify:release": "bun run scripts/release-guard.ts",
83
+ "scan:artifact": "bun run scripts/scan-artifact.ts",
84
+ "version:set": "bun run scripts/set-version.ts",
85
+ "version:check": "bun run scripts/set-version.ts --check",
86
+ prepack: "bun run prepack:platform-gate && bun run version:check && bun run build && bun run verify:release && bun run scan:artifact",
87
+ "prepack:platform-gate": `bash -c 'if [ "$(uname -s)" = Darwin ]; then bun run build:native-fs-guard; else echo "WARN: native fs-guard build skipped on $(uname -s) \u2014 dry-run/CI pack; publish recordings from macOS"; fi'`,
88
+ prepublishOnly: "bun run typecheck && bun run release-suite-gate",
89
+ "release-suite-gate": "bun run scripts/release-suite-gate.ts",
90
+ "typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json"
91
+ },
92
+ files: [
93
+ "dist/",
94
+ "scripts/",
95
+ "Dockerfile.package",
96
+ "bun.lock",
97
+ "src/native/Recordings/App/",
98
+ "src/native/Recordings/RecordingsLib/",
99
+ "src/native/Recordings/RecordingsTests/",
100
+ "src/native/Recordings/Updater/",
101
+ "src/native/Recordings/Package.swift",
102
+ "src/native/Recordings/Package.resolved",
103
+ "src/native/Recordings/build.sh",
104
+ "packaging/macos/",
105
+ "README.md",
106
+ "LICENSE"
107
+ ],
108
+ dependencies: {
109
+ "@hasna/contracts": "0.13.4",
110
+ "@hasna/events": "0.1.11",
111
+ "@modelcontextprotocol/sdk": "^1.12.1",
112
+ chalk: "^5.4.1",
113
+ commander: "^13.1.0",
114
+ openai: "^5.1.0",
115
+ pg: "^8.13.3",
116
+ zod: "^3.24.2"
117
+ },
118
+ devDependencies: {
119
+ "@types/bun": "^1.2.5",
120
+ "@types/pg": "^8.11.11",
121
+ "node-api-headers": "1.9.0",
122
+ typescript: "^5.8.2"
123
+ },
124
+ publishConfig: {
125
+ registry: "https://registry.npmjs.org",
126
+ access: "public"
127
+ },
128
+ license: "Apache-2.0",
129
+ author: "Hasna <andrei@hasna.com>"
130
+ };
131
+ });
132
+
20
133
  // src/version.ts
21
- var VERSION = "0.3.2";
134
+ var VERSION;
135
+ var init_version = __esm(() => {
136
+ init_package();
137
+ VERSION = package_default.version;
138
+ });
22
139
 
23
140
  // src/db/remote-storage.ts
24
141
  import pg from "pg";
@@ -82,59 +199,12 @@ class PgAdapterAsync {
82
199
  }
83
200
  var init_remote_storage = () => {};
84
201
 
85
- // src/lib/retired-deployment-modes.ts
86
- function normalizeModeToken(value) {
87
- return value.trim().toLowerCase().replace(/-/g, "_");
88
- }
89
- function asRetiredDeploymentMode(value) {
90
- const token = normalizeModeToken(value);
91
- return RETIRED_DEPLOYMENT_MODES.includes(token) ? token : null;
92
- }
93
- function retiredDeploymentModeError(rawValue, sourceEnvKey, replacement) {
94
- const mode = asRetiredDeploymentMode(rawValue);
95
- const value = mode === "local" ? replacement.onBox : replacement.offBox;
96
- return new Error(`${sourceEnvKey}=${rawValue} names a deployment mode, and deployment modes are removed: ` + `${RETIRED_DEPLOYMENT_MODES.join(" | ")} no longer select anything. ` + `Set ${replacement.envKey}=${value} instead ` + `(${replacement.envKey} takes ${replacement.onBox} or ${replacement.offBox}).`);
97
- }
98
- var RETIRED_DEPLOYMENT_MODES;
99
- var init_retired_deployment_modes = __esm(() => {
100
- RETIRED_DEPLOYMENT_MODES = [
101
- "local",
102
- "self_hosted",
103
- "cloud",
104
- "remote",
105
- "hybrid"
106
- ];
107
- });
108
-
109
202
  // src/server/cloud-config.ts
110
- function backendReplacement() {
111
- return { envKey: DATA_BACKEND_ENV_KEYS[0], onBox: "sqlite", offBox: "postgresql" };
112
- }
113
- function resolveCloudDatabaseUrl(env = process.env) {
203
+ function resolveDatabaseUrl(env = process.env) {
114
204
  return env.HASNA_RECORDINGS_DATABASE_URL || env.RECORDINGS_DATABASE_URL || env.DATABASE_URL || undefined;
115
205
  }
116
- function configuredDataBackend(env = process.env) {
117
- for (const key of DATA_BACKEND_ENV_KEYS) {
118
- const raw = env[key]?.trim();
119
- if (!raw)
120
- continue;
121
- const token = normalizeModeToken(raw);
122
- if (token === "sqlite")
123
- return { backend: "sqlite", source: key };
124
- if (token === "postgresql" || token === "postgres")
125
- return { backend: "postgresql", source: key };
126
- if (asRetiredDeploymentMode(raw)) {
127
- throw retiredDeploymentModeError(raw, key, backendReplacement());
128
- }
129
- throw new Error(`${key}=${raw} is not a data backend. Use ${DATA_BACKEND_ENV_KEYS[0]}=sqlite or ` + `${DATA_BACKEND_ENV_KEYS[0]}=postgresql.`);
130
- }
131
- return null;
132
- }
133
206
  function resolveDataBackend(env = process.env) {
134
- const configured = configuredDataBackend(env);
135
- if (configured)
136
- return configured.backend;
137
- return resolveCloudDatabaseUrl(env) ? "postgresql" : "sqlite";
207
+ return resolveDatabaseUrl(env) ? "postgresql" : "sqlite";
138
208
  }
139
209
  function isPostgresBackendEnabled(env = process.env) {
140
210
  return resolveDataBackend(env) === "postgresql";
@@ -152,14 +222,6 @@ function requireSigningSecret(env = process.env) {
152
222
  }
153
223
  return signingSecret;
154
224
  }
155
- var DATA_BACKEND_ENV_KEYS;
156
- var init_cloud_config = __esm(() => {
157
- init_retired_deployment_modes();
158
- DATA_BACKEND_ENV_KEYS = [
159
- "HASNA_RECORDINGS_STORAGE_MODE",
160
- "RECORDINGS_STORAGE_MODE"
161
- ];
162
- });
163
225
 
164
226
  // src/server/cloud-readiness.ts
165
227
  var exports_cloud_readiness = {};
@@ -221,7 +283,7 @@ async function assertCloudSchemaContract(pg2) {
221
283
  ON attribute_row.attrelid = constraint_row.conrelid
222
284
  AND attribute_row.attnum = key_row.attnum
223
285
  ORDER BY key_row.position
224
- ) AS columns,
286
+ )::text[] AS columns,
225
287
  referenced_namespace.nspname AS referenced_schema,
226
288
  referenced_table.relname AS referenced_table,
227
289
  CASE WHEN constraint_row.confrelid = 0 THEN ARRAY[]::text[] ELSE ARRAY(
@@ -231,7 +293,7 @@ async function assertCloudSchemaContract(pg2) {
231
293
  ON attribute_row.attrelid = constraint_row.confrelid
232
294
  AND attribute_row.attnum = key_row.attnum
233
295
  ORDER BY key_row.position
234
- ) END AS referenced_columns,
296
+ )::text[] END AS referenced_columns,
235
297
  NULLIF(constraint_row.confdeltype::text, ' ') AS delete_action,
236
298
  NULLIF(constraint_row.confupdtype::text, ' ') AS update_action,
237
299
  NULLIF(constraint_row.confmatchtype::text, ' ') AS match_type
@@ -265,7 +327,7 @@ async function assertCloudSchemaContract(pg2) {
265
327
  AND attribute_row.attnum = key_row.attnum
266
328
  WHERE key_row.position <= index_row.indnkeyatts
267
329
  ORDER BY key_row.position
268
- ) AS columns
330
+ )::text[] AS columns
269
331
  FROM pg_index index_row
270
332
  JOIN pg_class table_row ON table_row.oid = index_row.indrelid
271
333
  JOIN pg_namespace namespace_row ON namespace_row.oid = table_row.relnamespace
@@ -671,8 +733,8 @@ var init_migrate_command = __esm(() => {
671
733
  var exports_cloud = {};
672
734
  __export(exports_cloud, {
673
735
  resolveSigningSecret: () => resolveSigningSecret,
736
+ resolveDatabaseUrl: () => resolveDatabaseUrl,
674
737
  resolveDataBackend: () => resolveDataBackend,
675
- resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
676
738
  requireSigningSecret: () => requireSigningSecret,
677
739
  pingCloudConnectivity: () => pingCloudConnectivity2,
678
740
  pingCloud: () => pingCloud,
@@ -682,7 +744,6 @@ __export(exports_cloud, {
682
744
  getCloudPg: () => getCloudPg,
683
745
  getApiKeyStore: () => getApiKeyStore,
684
746
  ensureCloudSchema: () => ensureCloudSchema,
685
- configuredDataBackend: () => configuredDataBackend,
686
747
  closeCloud: () => closeCloud,
687
748
  RECORDINGS_APP_SLUG: () => RECORDINGS_APP_SLUG
688
749
  });
@@ -691,7 +752,7 @@ import { ApiKeyStore } from "@hasna/contracts/auth";
691
752
  function getCloudPg() {
692
753
  if (cachedPg)
693
754
  return cachedPg;
694
- const url = resolveCloudDatabaseUrl();
755
+ const url = resolveDatabaseUrl();
695
756
  if (!url) {
696
757
  throw new Error("The /v1 API requires a PostgreSQL database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL).");
697
758
  }
@@ -759,10 +820,8 @@ async function closeCloud() {
759
820
  var RECORDINGS_APP_SLUG = "recordings", cachedPg = null, cachedStore = null, cachedVerifier = null, schemaEnsured = null;
760
821
  var init_cloud = __esm(() => {
761
822
  init_remote_storage();
762
- init_cloud_config();
763
823
  init_cloud_readiness();
764
824
  init_migrate_command();
765
- init_cloud_config();
766
825
  });
767
826
 
768
827
  // src/db/errors.ts
@@ -1672,6 +1731,7 @@ function buildV1OpenApiDocument(version = VERSION) {
1672
1731
  }
1673
1732
  var recordingSchema, agentSchema, projectSchema;
1674
1733
  var init_openapi = __esm(() => {
1734
+ init_version();
1675
1735
  recordingSchema = {
1676
1736
  type: "object",
1677
1737
  properties: {
@@ -6727,38 +6787,34 @@ function saveFeedback2(input) {
6727
6787
  }
6728
6788
  var init_feedback = __esm(() => {
6729
6789
  init_database();
6790
+ init_version();
6730
6791
  });
6731
6792
 
6732
6793
  // src/http/client.ts
6794
+ import { createClientTransport } from "@hasna/contracts/client";
6795
+ import {
6796
+ createHasnaStorageClient,
6797
+ resolveStorageClient
6798
+ } from "@hasna/contracts/client/storage";
6733
6799
  function envToken(name) {
6734
6800
  return name.toUpperCase().replace(/-/g, "_");
6735
6801
  }
6736
- function clientStoreReplacement(envKey) {
6737
- return { envKey, onBox: "sqlite", offBox: "http" };
6802
+ function envKeys(name) {
6803
+ const token = envToken(name);
6804
+ return {
6805
+ storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
6806
+ apiUrlKeys: [`HASNA_${token}_API_URL`],
6807
+ apiKeyKeys: [`HASNA_${token}_API_KEY`]
6808
+ };
6738
6809
  }
6739
- function normalizeClientStore(value, sourceEnvKey, replacementEnvKey) {
6740
- const normalized = normalizeModeToken(value);
6810
+ function normalizeClientStore(value) {
6811
+ const normalized = value.trim().toLowerCase();
6741
6812
  if (normalized === "sqlite")
6742
6813
  return "sqlite";
6743
6814
  if (normalized === "http" || normalized === "https")
6744
6815
  return "http";
6745
- if (asRetiredDeploymentMode(value)) {
6746
- throw retiredDeploymentModeError(value, sourceEnvKey, clientStoreReplacement(replacementEnvKey));
6747
- }
6748
6816
  throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
6749
6817
  }
6750
- function defaultApiBaseUrl(name) {
6751
- return `http://localhost:8874`;
6752
- }
6753
- function envKeys(name) {
6754
- const token = envToken(name);
6755
- return {
6756
- storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
6757
- retiredModeKeys: [`HASNA_${token}_STORAGE_MODE`, `HASNA_${token}_MODE`, `${token}_STORAGE_MODE`, `${token}_MODE`],
6758
- apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
6759
- apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
6760
- };
6761
- }
6762
6818
  function firstEnv(env, keys) {
6763
6819
  for (const key of keys) {
6764
6820
  const value = env[key]?.trim();
@@ -6767,20 +6823,6 @@ function firstEnv(env, keys) {
6767
6823
  }
6768
6824
  return null;
6769
6825
  }
6770
- function assertNoRetiredMode(env, keys) {
6771
- for (const key of keys.retiredModeKeys) {
6772
- const value = env[key]?.trim();
6773
- if (!value)
6774
- continue;
6775
- const normalized = normalizeModeToken(value);
6776
- if (SERVER_BACKEND_VALUES.includes(normalized))
6777
- continue;
6778
- if (asRetiredDeploymentMode(value)) {
6779
- throw retiredDeploymentModeError(value, key, clientStoreReplacement(keys.storeKeys[0]));
6780
- }
6781
- throw new Error(`${key}=${value} is not a client store. ${key} no longer selects one; ` + `set ${keys.storeKeys[0]}=sqlite or ${keys.storeKeys[0]}=http instead.`);
6782
- }
6783
- }
6784
6826
  function toV1BaseUrl(apiUrl) {
6785
6827
  const url = new URL(apiUrl);
6786
6828
  if (url.protocol !== "http:" && url.protocol !== "https:") {
@@ -6796,22 +6838,44 @@ function toV1BaseUrl(apiUrl) {
6796
6838
  }
6797
6839
  function resolveTransport(name, env = process.env) {
6798
6840
  const keys = envKeys(name);
6799
- assertNoRetiredMode(env, keys);
6800
6841
  const storeHit = firstEnv(env, keys.storeKeys);
6801
6842
  const urlHit = firstEnv(env, keys.apiUrlKeys);
6802
6843
  const keyHit = firstEnv(env, keys.apiKeyKeys);
6803
6844
  let requested = "sqlite";
6804
6845
  let modeSource = "default";
6805
6846
  if (storeHit) {
6806
- requested = normalizeClientStore(storeHit.value, storeHit.key, keys.storeKeys[0]);
6847
+ requested = normalizeClientStore(storeHit.value);
6807
6848
  modeSource = storeHit.key;
6808
6849
  } else if (urlHit && keyHit) {
6809
6850
  requested = "http";
6810
6851
  modeSource = "auto:api-url+api-key";
6852
+ } else if (urlHit || keyHit) {
6853
+ const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
6854
+ const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
6855
+ return {
6856
+ transport: "sqlite",
6857
+ requested,
6858
+ modeSource,
6859
+ baseUrl: null,
6860
+ apiKeyPresent: Boolean(keyHit),
6861
+ misconfigured: true,
6862
+ warning: `${present} is set but ${missing} is not: the hosted API is only ` + `selected when BOTH are present. Set ${missing}, or unset ${present} to ` + `use the on-box store.`
6863
+ };
6811
6864
  }
6812
6865
  if (requested === "sqlite") {
6813
6866
  return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
6814
6867
  }
6868
+ if (!urlHit) {
6869
+ return {
6870
+ transport: "sqlite",
6871
+ requested,
6872
+ modeSource,
6873
+ baseUrl: null,
6874
+ apiKeyPresent: Boolean(keyHit),
6875
+ misconfigured: true,
6876
+ warning: `${modeSource}=http but no API URL is set (${keys.apiUrlKeys[0]}). Refusing to route to the API.`
6877
+ };
6878
+ }
6815
6879
  if (!keyHit) {
6816
6880
  return {
6817
6881
  transport: "sqlite",
@@ -6823,7 +6887,7 @@ function resolveTransport(name, env = process.env) {
6823
6887
  warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
6824
6888
  };
6825
6889
  }
6826
- const rawUrl = urlHit?.value ?? defaultApiBaseUrl(name);
6890
+ const rawUrl = urlHit.value;
6827
6891
  let baseUrl;
6828
6892
  try {
6829
6893
  baseUrl = toV1BaseUrl(rawUrl);
@@ -6833,201 +6897,38 @@ function resolveTransport(name, env = process.env) {
6833
6897
  }
6834
6898
  return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
6835
6899
  }
6836
- function appendQuery(path, query) {
6837
- if (!query)
6838
- return path;
6839
- const params = new URLSearchParams;
6840
- for (const [key, value] of Object.entries(query)) {
6841
- if (value === null || value === undefined)
6842
- continue;
6843
- if (Array.isArray(value))
6844
- for (const v of value)
6845
- params.append(key, String(v));
6846
- else
6847
- params.append(key, String(value));
6848
- }
6849
- const qs = params.toString();
6850
- return qs ? `${path}${path.includes("?") ? "&" : "?"}${qs}` : path;
6851
- }
6852
- function createHttpTransport(options) {
6853
- const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
6854
- const base = options.baseUrl.replace(/\/+$/, "");
6855
- const timeoutMs = options.timeoutMs ?? 30000;
6856
- const sleep = options.sleepImpl ?? defaultSleep;
6857
- async function once(method, rel, url, body, opts) {
6858
- const headers = {
6859
- "x-api-key": options.apiKey,
6860
- Authorization: `Bearer ${options.apiKey}`,
6861
- Accept: "application/json",
6862
- ...opts.headers ?? {}
6863
- };
6864
- if (opts.idempotencyKey)
6865
- headers["Idempotency-Key"] = opts.idempotencyKey;
6866
- const init = { method, headers };
6867
- if (body !== undefined) {
6868
- headers["Content-Type"] = "application/json";
6869
- init.body = JSON.stringify(body);
6870
- }
6871
- const controller = new AbortController;
6872
- const onAbort = () => controller.abort();
6873
- if (opts.signal) {
6874
- if (opts.signal.aborted)
6875
- controller.abort();
6876
- else
6877
- opts.signal.addEventListener("abort", onAbort, { once: true });
6878
- }
6879
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
6880
- init.signal = controller.signal;
6881
- let response;
6882
- try {
6883
- response = await fetchImpl(url, init);
6884
- } catch (error2) {
6885
- const err = error2 instanceof Error ? error2 : new Error(String(error2));
6886
- if (opts.signal?.aborted)
6887
- return { ok: false, retryable: false, error: err };
6888
- return { ok: false, retryable: true, error: err };
6889
- } finally {
6890
- clearTimeout(timer);
6891
- if (opts.signal)
6892
- opts.signal.removeEventListener("abort", onAbort);
6893
- }
6894
- const text = await response.text();
6895
- let parsed = undefined;
6896
- if (text.length > 0) {
6897
- try {
6898
- parsed = JSON.parse(text);
6899
- } catch {
6900
- parsed = text;
6901
- }
6902
- }
6903
- if (!response.ok) {
6904
- return { ok: false, retryable: RETRY_STATUSES.has(response.status), error: new HasnaHttpError(method, rel, response.status, parsed) };
6905
- }
6906
- return { ok: true, value: parsed };
6907
- }
6908
- async function request(method, path, body, opts = {}) {
6909
- const upper = method.toUpperCase();
6910
- const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
6911
- const url = `${base}${rel}`;
6912
- const methodRetryable = IDEMPOTENT.has(upper) || Boolean(opts.idempotencyKey);
6913
- const maxRetries = opts.retries ?? 2;
6914
- const maxAttempts = methodRetryable ? maxRetries + 1 : 1;
6915
- let last = null;
6916
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
6917
- const result = await once(upper, rel, url, body, opts);
6918
- if (result.ok)
6919
- return result.value;
6920
- last = result;
6921
- const canRetry = methodRetryable && result.retryable && attempt < maxAttempts;
6922
- if (!canRetry)
6923
- break;
6924
- const backoff = Math.min(2000, 200 * 2 ** (attempt - 1));
6925
- const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
6926
- await sleep(backoff + jitter);
6927
- }
6928
- if (last === null)
6929
- throw new Error(`Request to ${rel} completed without a result`);
6930
- throw last.error;
6931
- }
6932
- return {
6933
- baseUrl: base,
6934
- request,
6935
- get: (path, opts) => request("GET", path, undefined, opts),
6936
- post: (path, body, opts) => request("POST", path, body, opts),
6937
- patch: (path, body, opts) => request("PATCH", path, body, opts),
6938
- put: (path, body, opts) => request("PUT", path, body, opts),
6939
- del: (path, body, opts) => request("DELETE", path, body, opts)
6940
- };
6941
- }
6942
- function newIdempotencyKey() {
6943
- const g = globalThis;
6944
- if (g.crypto?.randomUUID)
6945
- return g.crypto.randomUUID();
6946
- return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
6947
- }
6948
- function extractItems(raw, extraKeys = []) {
6949
- if (Array.isArray(raw))
6950
- return raw;
6951
- if (raw && typeof raw === "object") {
6952
- const obj = raw;
6953
- for (const key of [...extraKeys, "items", "data", "results", "rows", "records"]) {
6954
- if (Array.isArray(obj[key]))
6955
- return obj[key];
6956
- }
6957
- }
6958
- return [];
6959
- }
6960
- function createStorageClient(name, transport) {
6961
- const rp = (r) => `/${r.replace(/^\/+|\/+$/g, "")}`;
6962
- const ep = (r, id) => `${rp(r)}/${encodeURIComponent(String(id))}`;
6963
- return {
6964
- name,
6965
- baseUrl: transport.baseUrl,
6966
- transport,
6967
- async list(resource, query) {
6968
- const raw = await transport.get(rp(resource), { query });
6969
- return { items: extractItems(raw, [resource]), raw };
6970
- },
6971
- async get(resource, id) {
6972
- try {
6973
- return await transport.get(ep(resource, id));
6974
- } catch (error2) {
6975
- if (error2 instanceof HasnaHttpError && error2.status === 404)
6976
- return null;
6977
- throw error2;
6978
- }
6979
- },
6980
- async create(resource, body, idempotencyKey) {
6981
- return transport.post(rp(resource), body, { idempotencyKey: idempotencyKey ?? newIdempotencyKey() });
6982
- },
6983
- async update(resource, id, patch, method = "PATCH") {
6984
- const call = method === "PUT" ? transport.put : transport.patch;
6985
- return call(ep(resource, id), patch);
6986
- },
6987
- async delete(resource, id) {
6988
- try {
6989
- await transport.del(ep(resource, id));
6990
- } catch (error2) {
6991
- if (error2 instanceof HasnaHttpError && error2.status === 404)
6992
- return;
6993
- throw error2;
6994
- }
6995
- }
6996
- };
6997
- }
6998
- function resolveStorageClient(name, env = process.env, fetchImpl) {
6900
+ function resolveStoreClient(name, env = process.env) {
6999
6901
  const resolution = resolveTransport(name, env);
7000
6902
  if (resolution.misconfigured) {
6903
+ const wired2 = createClientTransport(name, env);
6904
+ if (wired2.transport === "http") {
6905
+ return {
6906
+ transport: "http",
6907
+ client: createHasnaStorageClient(name, wired2.client),
6908
+ resolution: {
6909
+ transport: "http",
6910
+ requested: "http",
6911
+ modeSource: resolution.modeSource === "default" ? "auto:api-url+seam-credential" : resolution.modeSource,
6912
+ baseUrl: wired2.resolution.baseUrl,
6913
+ apiKeyPresent: true,
6914
+ misconfigured: false,
6915
+ warning: null
6916
+ }
6917
+ };
6918
+ }
7001
6919
  throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
7002
6920
  }
7003
6921
  if (resolution.transport === "sqlite" || !resolution.baseUrl) {
7004
6922
  return { transport: "sqlite", client: null, resolution };
7005
6923
  }
7006
- const keys = envKeys(name);
7007
- const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
7008
- if (!apiKey)
6924
+ const wired = createClientTransport(name, env);
6925
+ if (wired.transport !== "http") {
7009
6926
  throw new Error(`Client for '${name}' resolved to the /v1 API without an API key.`);
7010
- const transport = createHttpTransport({ name, baseUrl: resolution.baseUrl, apiKey, ...fetchImpl ? { fetchImpl } : {} });
7011
- return { transport: "http", client: createStorageClient(name, transport), resolution };
6927
+ }
6928
+ return { transport: "http", client: createHasnaStorageClient(name, wired.client), resolution };
7012
6929
  }
7013
- var SERVER_BACKEND_VALUES, HasnaHttpError, RETRY_STATUSES, IDEMPOTENT, defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
6930
+ var RETRY_STATUSES, IDEMPOTENT;
7014
6931
  var init_client = __esm(() => {
7015
- init_retired_deployment_modes();
7016
- SERVER_BACKEND_VALUES = ["sqlite", "postgresql", "postgres"];
7017
- HasnaHttpError = class HasnaHttpError extends Error {
7018
- status;
7019
- method;
7020
- path;
7021
- body;
7022
- constructor(method, path, status, body) {
7023
- super(`Hasna request failed: ${method} ${path} -> ${status}`);
7024
- this.name = "HasnaHttpError";
7025
- this.status = status;
7026
- this.method = method;
7027
- this.path = path;
7028
- this.body = body;
7029
- }
7030
- };
7031
6932
  RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
7032
6933
  IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
7033
6934
  });
@@ -7059,6 +6960,22 @@ function unwrap(res, key) {
7059
6960
  }
7060
6961
  return res;
7061
6962
  }
6963
+ async function listResource(client, resource, query) {
6964
+ const raw = await client.transport.get(`/${resource}`, query ? { query } : undefined);
6965
+ return { items: extractEnvelopeItems(raw, resource), raw };
6966
+ }
6967
+ function extractEnvelopeItems(raw, resource) {
6968
+ if (Array.isArray(raw))
6969
+ return raw;
6970
+ if (raw && typeof raw === "object") {
6971
+ const obj = raw;
6972
+ for (const key of [resource, "items", "data", "results", "rows", "records"]) {
6973
+ if (Array.isArray(obj[key]))
6974
+ return obj[key];
6975
+ }
6976
+ }
6977
+ return [];
6978
+ }
7062
6979
  function apiStore(client) {
7063
6980
  return {
7064
6981
  mode: "http",
@@ -7066,7 +6983,7 @@ function apiStore(client) {
7066
6983
  async createRecording(input, idempotencyKey) {
7067
6984
  const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
7068
6985
  const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
7069
- const res = await client.create("recordings", identity.input, identity.idempotencyKey);
6986
+ const res = await client.create("recordings", identity.input, { idempotencyKey: identity.idempotencyKey });
7070
6987
  return unwrap(res, "recording");
7071
6988
  },
7072
6989
  async getRecording(id) {
@@ -7074,7 +6991,7 @@ function apiStore(client) {
7074
6991
  return res ? unwrap(res, "recording") : null;
7075
6992
  },
7076
6993
  async listRecordings(filter) {
7077
- const { items } = await client.list("recordings", listQuery(filter));
6994
+ const { items } = await listResource(client, "recordings", listQuery(filter));
7078
6995
  return items;
7079
6996
  },
7080
6997
  async countRecordings(filter) {
@@ -7085,7 +7002,7 @@ function apiStore(client) {
7085
7002
  const seenPageKeys = new Set;
7086
7003
  while (pageRequests < maxPageRequests) {
7087
7004
  pageRequests += 1;
7088
- const { items, raw } = await client.list("recordings", {
7005
+ const { items, raw } = await listResource(client, "recordings", {
7089
7006
  ...listQuery(filter),
7090
7007
  limit: pageLimit,
7091
7008
  offset
@@ -7108,7 +7025,7 @@ function apiStore(client) {
7108
7025
  throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
7109
7026
  },
7110
7027
  async searchRecordings(query, filter) {
7111
- const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
7028
+ const { items } = await listResource(client, "recordings", listQuery({ ...filter ?? {}, search: query }));
7112
7029
  return items;
7113
7030
  },
7114
7031
  async deleteRecording(id) {
@@ -7140,7 +7057,7 @@ function apiStore(client) {
7140
7057
  return res ? unwrap(res, "agent") : null;
7141
7058
  },
7142
7059
  async listAgents() {
7143
- const { items } = await client.list("agents");
7060
+ const { items } = await listResource(client, "agents");
7144
7061
  return items;
7145
7062
  },
7146
7063
  async heartbeatAgent(idOrName) {
@@ -7180,7 +7097,7 @@ function apiStore(client) {
7180
7097
  return res ? unwrap(res, "project") : null;
7181
7098
  },
7182
7099
  async listProjects() {
7183
- const { items } = await client.list("projects");
7100
+ const { items } = await listResource(client, "projects");
7184
7101
  return items;
7185
7102
  },
7186
7103
  async saveFeedback(input) {
@@ -7227,7 +7144,7 @@ async function countStoreRecordings(store, filter) {
7227
7144
  function getStore(env = process.env) {
7228
7145
  if (env === process.env && cached)
7229
7146
  return cached;
7230
- const resolved = resolveStorageClient(APP, env);
7147
+ const resolved = resolveStoreClient(APP, env);
7231
7148
  const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
7232
7149
  if (env === process.env)
7233
7150
  cached = store;
@@ -8229,6 +8146,7 @@ var init_mcp = __esm(async () => {
8229
8146
  init_store();
8230
8147
  init_transcriber();
8231
8148
  init_enhancer();
8149
+ init_version();
8232
8150
  init_machine();
8233
8151
  config = loadConfig();
8234
8152
  ensureDataDir(config);
@@ -8371,7 +8289,7 @@ async function startServer(port, options) {
8371
8289
  }
8372
8290
  var SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
8373
8291
  var init_serve = __esm(() => {
8374
- init_cloud_config();
8292
+ init_version();
8375
8293
  init_v1();
8376
8294
  init_openapi();
8377
8295
  SECURITY_HEADERS = {
@@ -8385,6 +8303,7 @@ var init_serve = __esm(() => {
8385
8303
  });
8386
8304
 
8387
8305
  // src/server/index.ts
8306
+ init_version();
8388
8307
  var DEFAULT_PORT = 8874;
8389
8308
  function parsePort() {
8390
8309
  const arg = process.argv.find((a) => a === "--port" || a.startsWith("--port="));
@@ -8421,8 +8340,7 @@ Commands:
8421
8340
  migrate Apply the PostgreSQL schema, then exit. Idempotent.
8422
8341
 
8423
8342
  Environment:
8424
- HASNA_RECORDINGS_STORAGE_MODE Data backend: sqlite | postgresql
8425
- HASNA_RECORDINGS_DATABASE_URL PostgreSQL DSN (implies postgresql)
8343
+ HASNA_RECORDINGS_DATABASE_URL PostgreSQL DSN (present -> postgresql backend, else sqlite)
8426
8344
  HASNA_RECORDINGS_API_SIGNING_KEY HMAC signing secret for API-key auth`);
8427
8345
  }
8428
8346
  async function runMigrate() {
@@ -8431,11 +8349,11 @@ async function runMigrate() {
8431
8349
  getCloudPg: getCloudPg2,
8432
8350
  migrateCloudSchema: migrateCloudSchema2,
8433
8351
  pingCloudConnectivity: pingCloudConnectivity3,
8434
- resolveCloudDatabaseUrl: resolveCloudDatabaseUrl2
8352
+ resolveDatabaseUrl: resolveDatabaseUrl2
8435
8353
  } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
8436
8354
  const { assertCloudSchemaContract: assertCloudSchemaContract2 } = await Promise.resolve().then(() => (init_cloud_readiness(), exports_cloud_readiness));
8437
8355
  const { runCloudMigration: runCloudMigration2 } = await Promise.resolve().then(() => (init_migrate_command(), exports_migrate_command));
8438
- if (!resolveCloudDatabaseUrl2()) {
8356
+ if (!resolveDatabaseUrl2()) {
8439
8357
  console.error("migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)");
8440
8358
  process.exit(2);
8441
8359
  }