@hasna/recordings 0.2.13 → 0.3.1

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 (120) hide show
  1. package/Dockerfile.package +4 -5
  2. package/README.md +113 -8
  3. package/bun.lock +2 -2
  4. package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
  5. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
  6. package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
  7. package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
  8. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
  9. package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
  10. package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
  11. package/dist/__tests__/preload.d.ts +1 -4
  12. package/dist/__tests__/preload.d.ts.map +1 -1
  13. package/dist/cli/desktop-snapshot.d.ts +20 -0
  14. package/dist/cli/desktop-snapshot.d.ts.map +1 -0
  15. package/dist/cli/index.js +2078 -277
  16. package/dist/cli/macos-permissions.d.ts +72 -0
  17. package/dist/cli/macos-permissions.d.ts.map +1 -1
  18. package/dist/cli/macos-shortcut.d.ts +139 -0
  19. package/dist/cli/macos-shortcut.d.ts.map +1 -0
  20. package/dist/cli/trigger-probe.d.ts +172 -0
  21. package/dist/cli/trigger-probe.d.ts.map +1 -0
  22. package/dist/db/database.d.ts +9 -0
  23. package/dist/db/database.d.ts.map +1 -1
  24. package/dist/http/client.d.ts +10 -7
  25. package/dist/http/client.d.ts.map +1 -1
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +634 -49
  29. package/dist/lib/capture-probe.d.ts +216 -0
  30. package/dist/lib/capture-probe.d.ts.map +1 -0
  31. package/dist/lib/config.d.ts.map +1 -1
  32. package/dist/lib/macos-bundle.d.ts +50 -0
  33. package/dist/lib/macos-bundle.d.ts.map +1 -0
  34. package/dist/lib/persistence-probe.d.ts +208 -0
  35. package/dist/lib/persistence-probe.d.ts.map +1 -0
  36. package/dist/lib/recorder.d.ts.map +1 -1
  37. package/dist/lib/retired-deployment-modes.d.ts +24 -0
  38. package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
  39. package/dist/lib/transcriber.d.ts +60 -0
  40. package/dist/lib/transcriber.d.ts.map +1 -1
  41. package/dist/mcp/index.js +140 -56
  42. package/dist/server/cloud-config.d.ts +25 -3
  43. package/dist/server/cloud-config.d.ts.map +1 -1
  44. package/dist/server/cloud-readiness.d.ts.map +1 -1
  45. package/dist/server/cloud.d.ts +8 -7
  46. package/dist/server/cloud.d.ts.map +1 -1
  47. package/dist/server/index.js +187 -79
  48. package/dist/server/migrate-command.d.ts.map +1 -1
  49. package/dist/server/serve.d.ts.map +1 -1
  50. package/dist/storage.d.ts +2 -2
  51. package/dist/storage.d.ts.map +1 -1
  52. package/dist/storage.js +108 -49
  53. package/dist/store.d.ts +1 -1
  54. package/dist/store.d.ts.map +1 -1
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.d.ts.map +1 -1
  57. package/package.json +25 -6
  58. package/scripts/build_native_fs_guard.sh +15 -2
  59. package/scripts/ci-linux-suite.ts +403 -0
  60. package/scripts/ci-native-build.ts +402 -0
  61. package/scripts/enforce_identity_migration.sh +110 -0
  62. package/scripts/generate-sdk.ts +1 -1
  63. package/scripts/install_macos_app.sh +138 -24
  64. package/scripts/macos_artifact.ts +205 -33
  65. package/scripts/native/realtime-settle-probe/README.md +39 -0
  66. package/scripts/native/realtime-settle-probe/main.swift +135 -0
  67. package/scripts/policy/local-only-approved-targets.txt +21 -0
  68. package/scripts/read_local_only_targets.sh +203 -0
  69. package/scripts/scan-artifact.ts +40 -0
  70. package/scripts/set-version.ts +145 -0
  71. package/scripts/vacuity-manifest-gen.ts +96 -0
  72. package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
  73. package/scripts/vacuity-manifests/install-chain.tsv +13 -0
  74. package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
  75. package/scripts/vacuity-manifests/reorder.tsv +4 -0
  76. package/scripts/vacuity-manifests/source-side.tsv +15 -0
  77. package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
  78. package/scripts/vacuity-manifests/version-sites.tsv +61 -0
  79. package/scripts/vacuity-mutation-battery.ts +266 -0
  80. package/src/native/Recordings/App/MenuBarStatusView.swift +14 -7
  81. package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
  82. package/src/native/Recordings/App/SidebarView.swift +1 -1
  83. package/src/native/Recordings/Package.swift +24 -5
  84. package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
  85. package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
  86. package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
  87. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
  88. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
  89. package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
  90. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
  91. package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
  92. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
  93. package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
  94. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
  95. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
  96. package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
  97. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
  98. package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
  99. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
  100. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
  101. package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
  102. package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
  103. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
  104. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
  105. package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
  106. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
  107. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
  108. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
  109. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
  110. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
  111. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
  112. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
  113. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
  114. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
  115. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
  116. package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
  117. package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
  118. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
  119. package/src/native/Recordings/build.sh +75 -9
  120. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
package/dist/cli/index.js CHANGED
@@ -5,7 +5,7 @@ var __require = import.meta.require;
5
5
  // src/cli/index.ts
6
6
  import { Command } from "commander";
7
7
 
8
- // node_modules/@hasna/events/dist/commander.js
8
+ // ../../node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/commander.js
9
9
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
10
10
  import { existsSync } from "fs";
11
11
  import { homedir } from "os";
@@ -900,13 +900,13 @@ function collectValues(value, previous) {
900
900
 
901
901
  // src/cli/index.ts
902
902
  import chalk from "chalk";
903
- import { spawnSync as spawnSync4 } from "child_process";
903
+ import { spawnSync as spawnSync7 } from "child_process";
904
904
  import {
905
- existsSync as existsSync4,
906
- readFileSync as readFileSync3,
905
+ existsSync as existsSync8,
906
+ readFileSync as readFileSync4,
907
907
  readdirSync as readdirSync2
908
908
  } from "fs";
909
- import { dirname as dirname4, join as pathJoin } from "path";
909
+ import { dirname as dirname5, join as pathJoin } from "path";
910
910
  import { fileURLToPath } from "url";
911
911
 
912
912
  // src/lib/config.ts
@@ -1335,7 +1335,8 @@ function findProjectRecordingsPath(entry) {
1335
1335
  function findRepositoryRoot(start) {
1336
1336
  let dir = start;
1337
1337
  while (true) {
1338
- if (existsSync2(join3(dir, ".git")))
1338
+ const gitPath = join3(dir, ".git");
1339
+ if (isRepositoryMarker(gitPath))
1339
1340
  return dir;
1340
1341
  const parent = dirname2(dir);
1341
1342
  if (parent === dir)
@@ -1343,6 +1344,15 @@ function findRepositoryRoot(start) {
1343
1344
  dir = parent;
1344
1345
  }
1345
1346
  }
1347
+ function isRepositoryMarker(gitPath) {
1348
+ try {
1349
+ if (statSync(gitPath).isFile())
1350
+ return true;
1351
+ return existsSync2(join3(gitPath, "HEAD"));
1352
+ } catch {
1353
+ return false;
1354
+ }
1355
+ }
1346
1356
  function canonicalExistingPath(path) {
1347
1357
  try {
1348
1358
  return realpathSync2(path);
@@ -1371,14 +1381,17 @@ function loadSecretKey(keyName) {
1371
1381
  try {
1372
1382
  const content = readFileSync(candidate, "utf-8");
1373
1383
  const match = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`));
1374
- if (match)
1375
- return match[1];
1384
+ const doubleQuotedValue = match?.[1];
1385
+ if (doubleQuotedValue !== undefined)
1386
+ return doubleQuotedValue;
1376
1387
  const match2 = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`));
1377
- if (match2)
1378
- return match2[1];
1388
+ const singleQuotedValue = match2?.[1];
1389
+ if (singleQuotedValue !== undefined)
1390
+ return singleQuotedValue;
1379
1391
  const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
1380
- if (match3)
1381
- return match3[1].trim().replace(/^["']|["']$/g, "");
1392
+ const unquotedValue = match3?.[1];
1393
+ if (unquotedValue !== undefined)
1394
+ return unquotedValue.trim().replace(/^["']|["']$/g, "");
1382
1395
  } catch {}
1383
1396
  }
1384
1397
  return "";
@@ -1536,6 +1549,7 @@ var MIGRATIONS = [
1536
1549
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
1537
1550
  `
1538
1551
  ];
1552
+ var CURRENT_MIGRATION_LEVEL = MIGRATIONS.length - 1;
1539
1553
  function getDatabase(dbPath) {
1540
1554
  if (_db)
1541
1555
  return _db;
@@ -1559,7 +1573,11 @@ function runMigrations(db) {
1559
1573
  const currentLevel = result?.max_id ?? -1;
1560
1574
  for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
1561
1575
  try {
1562
- db.run(MIGRATIONS[i]);
1576
+ const migration = MIGRATIONS[i];
1577
+ if (migration === undefined) {
1578
+ throw new Error(`Missing SQLite migration at version ${i}`);
1579
+ }
1580
+ db.run(migration);
1563
1581
  db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
1564
1582
  } catch (e) {
1565
1583
  if (isBenignMigrationError(e)) {
@@ -1909,7 +1927,7 @@ function setAgentFocus(idOrName, projectId, db) {
1909
1927
  }
1910
1928
 
1911
1929
  // src/version.ts
1912
- var VERSION = "0.2.13";
1930
+ var VERSION = "0.3.1";
1913
1931
 
1914
1932
  // src/db/feedback.ts
1915
1933
  function saveFeedback(input) {
@@ -1917,29 +1935,54 @@ function saveFeedback(input) {
1917
1935
  db.query("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(input.message, input.email ?? null, input.category ?? "general", input.version ?? VERSION);
1918
1936
  }
1919
1937
 
1938
+ // src/lib/retired-deployment-modes.ts
1939
+ var RETIRED_DEPLOYMENT_MODES = [
1940
+ "local",
1941
+ "self_hosted",
1942
+ "cloud",
1943
+ "remote",
1944
+ "hybrid"
1945
+ ];
1946
+ function normalizeModeToken(value) {
1947
+ return value.trim().toLowerCase().replace(/-/g, "_");
1948
+ }
1949
+ function asRetiredDeploymentMode(value) {
1950
+ const token = normalizeModeToken(value);
1951
+ return RETIRED_DEPLOYMENT_MODES.includes(token) ? token : null;
1952
+ }
1953
+ function retiredDeploymentModeError(rawValue, sourceEnvKey, replacement) {
1954
+ const mode = asRetiredDeploymentMode(rawValue);
1955
+ const value = mode === "local" ? replacement.onBox : replacement.offBox;
1956
+ 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}).`);
1957
+ }
1958
+
1920
1959
  // src/http/client.ts
1921
- var DEPRECATED_MODE_ALIASES = ["self_hosted", "remote", "hybrid"];
1960
+ var SERVER_BACKEND_VALUES = ["sqlite", "postgresql", "postgres"];
1922
1961
  function envToken(name) {
1923
1962
  return name.toUpperCase().replace(/-/g, "_");
1924
1963
  }
1925
- function normalizeMode(value) {
1926
- const normalized = value.trim().toLowerCase().replace(/-/g, "_");
1927
- if (normalized === "local")
1928
- return { mode: "local", deprecatedAlias: null };
1929
- if (normalized === "cloud")
1930
- return { mode: "cloud", deprecatedAlias: null };
1931
- if (DEPRECATED_MODE_ALIASES.includes(normalized)) {
1932
- return { mode: "cloud", deprecatedAlias: normalized };
1964
+ function clientStoreReplacement(envKey) {
1965
+ return { envKey, onBox: "sqlite", offBox: "http" };
1966
+ }
1967
+ function normalizeClientStore(value, sourceEnvKey, replacementEnvKey) {
1968
+ const normalized = normalizeModeToken(value);
1969
+ if (normalized === "sqlite")
1970
+ return "sqlite";
1971
+ if (normalized === "http" || normalized === "https")
1972
+ return "http";
1973
+ if (asRetiredDeploymentMode(value)) {
1974
+ throw retiredDeploymentModeError(value, sourceEnvKey, clientStoreReplacement(replacementEnvKey));
1933
1975
  }
1934
- throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
1976
+ throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
1935
1977
  }
1936
- function defaultCloudBaseUrl(name) {
1978
+ function defaultApiBaseUrl(name) {
1937
1979
  return `https://${name}.hasna.xyz`;
1938
1980
  }
1939
1981
  function envKeys(name) {
1940
1982
  const token = envToken(name);
1941
1983
  return {
1942
- modeKeys: [`HASNA_${token}_STORAGE_MODE`, `HASNA_${token}_MODE`, `${token}_STORAGE_MODE`, `${token}_MODE`],
1984
+ storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
1985
+ retiredModeKeys: [`HASNA_${token}_STORAGE_MODE`, `HASNA_${token}_MODE`, `${token}_STORAGE_MODE`, `${token}_MODE`],
1943
1986
  apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
1944
1987
  apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
1945
1988
  };
@@ -1952,6 +1995,20 @@ function firstEnv(env, keys) {
1952
1995
  }
1953
1996
  return null;
1954
1997
  }
1998
+ function assertNoRetiredMode(env, keys) {
1999
+ for (const key of keys.retiredModeKeys) {
2000
+ const value = env[key]?.trim();
2001
+ if (!value)
2002
+ continue;
2003
+ const normalized = normalizeModeToken(value);
2004
+ if (SERVER_BACKEND_VALUES.includes(normalized))
2005
+ continue;
2006
+ if (asRetiredDeploymentMode(value)) {
2007
+ throw retiredDeploymentModeError(value, key, clientStoreReplacement(keys.storeKeys[0]));
2008
+ }
2009
+ 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.`);
2010
+ }
2011
+ }
1955
2012
  function toV1BaseUrl(apiUrl) {
1956
2013
  const url = new URL(apiUrl);
1957
2014
  if (url.protocol !== "http:" && url.protocol !== "https:") {
@@ -1967,45 +2024,42 @@ function toV1BaseUrl(apiUrl) {
1967
2024
  }
1968
2025
  function resolveTransport(name, env = process.env) {
1969
2026
  const keys = envKeys(name);
1970
- const modeHit = firstEnv(env, keys.modeKeys);
2027
+ assertNoRetiredMode(env, keys);
2028
+ const storeHit = firstEnv(env, keys.storeKeys);
1971
2029
  const urlHit = firstEnv(env, keys.apiUrlKeys);
1972
2030
  const keyHit = firstEnv(env, keys.apiKeyKeys);
1973
- let mode = "local";
1974
- let deprecatedAlias = null;
2031
+ let requested = "sqlite";
1975
2032
  let modeSource = "default";
1976
- if (modeHit) {
1977
- const normalized = normalizeMode(modeHit.value);
1978
- mode = normalized.mode;
1979
- deprecatedAlias = normalized.deprecatedAlias;
1980
- modeSource = modeHit.key;
2033
+ if (storeHit) {
2034
+ requested = normalizeClientStore(storeHit.value, storeHit.key, keys.storeKeys[0]);
2035
+ modeSource = storeHit.key;
1981
2036
  } else if (urlHit && keyHit) {
1982
- mode = "cloud";
2037
+ requested = "http";
1983
2038
  modeSource = "auto:api-url+api-key";
1984
2039
  }
1985
- if (mode === "local") {
1986
- return { transport: "local", mode, deprecatedAlias, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
2040
+ if (requested === "sqlite") {
2041
+ return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
1987
2042
  }
1988
2043
  if (!keyHit) {
1989
2044
  return {
1990
- transport: "local",
1991
- mode,
1992
- deprecatedAlias,
2045
+ transport: "sqlite",
2046
+ requested,
1993
2047
  modeSource,
1994
2048
  baseUrl: null,
1995
2049
  apiKeyPresent: false,
1996
2050
  misconfigured: true,
1997
- warning: `${modeSource}=cloud but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to cloud.`
2051
+ warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
1998
2052
  };
1999
2053
  }
2000
- const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
2054
+ const rawUrl = urlHit?.value ?? defaultApiBaseUrl(name);
2001
2055
  let baseUrl;
2002
2056
  try {
2003
2057
  baseUrl = toV1BaseUrl(rawUrl);
2004
2058
  } catch (error) {
2005
2059
  const message = error instanceof Error ? error.message : String(error);
2006
- return { transport: "local", mode, deprecatedAlias, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
2060
+ return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
2007
2061
  }
2008
- return { transport: "cloud-http", mode, deprecatedAlias, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
2062
+ return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
2009
2063
  }
2010
2064
 
2011
2065
  class HasnaHttpError extends Error {
@@ -2117,6 +2171,8 @@ function createHttpTransport(options) {
2117
2171
  const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
2118
2172
  await sleep(backoff + jitter);
2119
2173
  }
2174
+ if (last === null)
2175
+ throw new Error(`Request to ${rel} completed without a result`);
2120
2176
  throw last.error;
2121
2177
  }
2122
2178
  return {
@@ -2188,17 +2244,17 @@ function createStorageClient(name, transport) {
2188
2244
  function resolveStorageClient(name, env = process.env, fetchImpl) {
2189
2245
  const resolution = resolveTransport(name, env);
2190
2246
  if (resolution.misconfigured) {
2191
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
2247
+ throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
2192
2248
  }
2193
- if (resolution.transport === "local" || !resolution.baseUrl) {
2194
- return { transport: "local", client: null, resolution };
2249
+ if (resolution.transport === "sqlite" || !resolution.baseUrl) {
2250
+ return { transport: "sqlite", client: null, resolution };
2195
2251
  }
2196
2252
  const keys = envKeys(name);
2197
2253
  const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
2198
2254
  if (!apiKey)
2199
- throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
2255
+ throw new Error(`Client for '${name}' resolved to the /v1 API without an API key.`);
2200
2256
  const transport = createHttpTransport({ name, baseUrl: resolution.baseUrl, apiKey, ...fetchImpl ? { fetchImpl } : {} });
2201
- return { transport: "cloud-http", client: createStorageClient(name, transport), resolution };
2257
+ return { transport: "http", client: createStorageClient(name, transport), resolution };
2202
2258
  }
2203
2259
 
2204
2260
  // src/store.ts
@@ -2222,12 +2278,15 @@ function listQuery(filter) {
2222
2278
  }
2223
2279
  function unwrap(res, key) {
2224
2280
  if (res && typeof res === "object" && key in res) {
2225
- return res[key];
2281
+ const value = res[key];
2282
+ if (value === undefined)
2283
+ throw new Error(`Response field ${key} is undefined`);
2284
+ return value;
2226
2285
  }
2227
2286
  return res;
2228
2287
  }
2229
2288
  var localStore = {
2230
- mode: "local",
2289
+ mode: "sqlite",
2231
2290
  baseUrl: null,
2232
2291
  async createRecording(input, idempotencyKey) {
2233
2292
  return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
@@ -2280,7 +2339,7 @@ var localStore = {
2280
2339
  };
2281
2340
  function apiStore(client) {
2282
2341
  return {
2283
- mode: "cloud-http",
2342
+ mode: "http",
2284
2343
  baseUrl: client.baseUrl,
2285
2344
  async createRecording(input, idempotencyKey) {
2286
2345
  const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID5() : idempotencyKey;
@@ -2448,7 +2507,7 @@ function getStore(env = process.env) {
2448
2507
  if (env === process.env && cached)
2449
2508
  return cached;
2450
2509
  const resolved = resolveStorageClient(APP, env);
2451
- const store = resolved.transport === "cloud-http" ? apiStore(resolved.client) : localStore;
2510
+ const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
2452
2511
  if (env === process.env)
2453
2512
  cached = store;
2454
2513
  return store;
@@ -2509,10 +2568,14 @@ function startRecording(config) {
2509
2568
  const filename = `recording-${timestamp}.${config.audio_format}`;
2510
2569
  const filepath = join4(config.audio_dir, filename);
2511
2570
  const args = buildRecordArgs(filepath, config);
2571
+ const [command, ...commandArgs] = args;
2572
+ if (command === undefined) {
2573
+ throw new RecordingError("Recording command is empty.");
2574
+ }
2512
2575
  const releaseLease = isGlobalRecordingsStatePath(filepath) ? acquireLocalStoreReaderLease() : () => {};
2513
2576
  let recordProcess;
2514
2577
  try {
2515
- recordProcess = spawn2(args[0], args.slice(1), {
2578
+ recordProcess = spawn2(command, commandArgs, {
2516
2579
  stdio: ["pipe", "pipe", "pipe"]
2517
2580
  });
2518
2581
  } catch (error) {
@@ -2621,6 +2684,48 @@ function getClient(config) {
2621
2684
  _clientApiKey = config.openai_api_key;
2622
2685
  return _client;
2623
2686
  }
2687
+ function redactKeyMaterial(text) {
2688
+ return text.replace(/\b(sk|rk|org)-[A-Za-z0-9_*\-]{4,}/g, "[redacted]").replace(/\bBearer\s+[A-Za-z0-9._*\-]{4,}/gi, "Bearer [redacted]").replace(/\bBasic\s+[A-Za-z0-9+/=]{8,}/gi, "Basic [redacted]").replace(/:\/\/[^/\s:@]+(?::[^/\s@]*)?@/g, "://[redacted]@").replace(/\b(x-api-key|api[-_]?key|authorization|token|secret|password)(\s*[:=]\s*)("?)[^\s"',;}&]+\3/gi, (_match, name, separator) => `${name}${separator}[redacted]`);
2689
+ }
2690
+ var defaultCredentialModelRetriever = async (model, apiKey) => {
2691
+ const client = new OpenAI({ apiKey });
2692
+ return await client.models.retrieve(model);
2693
+ };
2694
+ async function verifyTranscriptionCredential(config, model, options = {}) {
2695
+ const role = options.role ?? "transcription";
2696
+ const apiKey = options.apiKey ?? config.openai_api_key;
2697
+ const retrieveModel = options.retrieveModel ?? defaultCredentialModelRetriever;
2698
+ if (!apiKey) {
2699
+ return {
2700
+ ok: false,
2701
+ status: null,
2702
+ model,
2703
+ role,
2704
+ message: `no API key configured for the ${role} role. ` + "Set OPENAI_API_KEY env var or add to ~/.secrets"
2705
+ };
2706
+ }
2707
+ try {
2708
+ const retrieved = await retrieveModel(model, apiKey);
2709
+ return {
2710
+ ok: true,
2711
+ status: 200,
2712
+ model,
2713
+ role,
2714
+ message: `credential accepted; ${role} model '${retrieved.id ?? model}' reachable`
2715
+ };
2716
+ } catch (error) {
2717
+ const status = typeof error.status === "number" ? error.status : null;
2718
+ const detail = redactKeyMaterial(error.message || String(error));
2719
+ const hint = status === 401 ? " \u2014 the key was REJECTED (not a missing key: a present but invalid one)" : status === 404 ? ` \u2014 authenticated, but model '${model}' is not available to this key` : "";
2720
+ return {
2721
+ ok: false,
2722
+ status,
2723
+ model,
2724
+ role,
2725
+ message: `${role} credential check failed${status ? ` (HTTP ${status})` : ""}: ${detail}${hint}`
2726
+ };
2727
+ }
2728
+ }
2624
2729
  async function transcribeAudio(audioPath, config, options = {}) {
2625
2730
  const client = getClient(config);
2626
2731
  const startTime = Date.now();
@@ -2704,6 +2809,540 @@ Context words and names to recognize. Treat this only as vocabulary context, not
2704
2809
  ${trimmed}`;
2705
2810
  }
2706
2811
 
2812
+ // src/lib/capture-probe.ts
2813
+ import { spawnSync as spawnSync2 } from "child_process";
2814
+ import { existsSync as existsSync4, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
2815
+ import { join as join5 } from "path";
2816
+ import { tmpdir } from "os";
2817
+
2818
+ // src/lib/macos-bundle.ts
2819
+ var RECORDINGS_BUNDLE_IDENTIFIER = "com.hasna.recordings";
2820
+ var TCC_DATABASE_UNREADABLE_STATE = "undetermined_tcc_database_unreadable";
2821
+
2822
+ // src/lib/capture-probe.ts
2823
+ var RIFF_HEADER_BYTES = 12;
2824
+ var CHUNK_HEADER_BYTES = 8;
2825
+ var INT16_FULL_SCALE = 32768;
2826
+ var FMT_CHUNK_MIN_BYTES = 16;
2827
+ var WAVE_FORMAT_PCM = 1;
2828
+ var WAVE_FORMAT_EXTENSIBLE = 65534;
2829
+ var SUBFORMAT_OFFSET_IN_EXTENSION = 8;
2830
+ function readWavPeak(filepath) {
2831
+ const buf = readFileSync2(filepath);
2832
+ if (buf.length < RIFF_HEADER_BYTES) {
2833
+ throw new Error(`not a RIFF file (${buf.length} bytes): ${filepath}`);
2834
+ }
2835
+ if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
2836
+ throw new Error(`not a RIFF/WAVE file: ${filepath}`);
2837
+ }
2838
+ let offset = RIFF_HEADER_BYTES;
2839
+ let bitsPerSample = null;
2840
+ let formatTag = null;
2841
+ let dataStart = -1;
2842
+ let dataLength = 0;
2843
+ while (offset + CHUNK_HEADER_BYTES <= buf.length) {
2844
+ const chunkId = buf.toString("ascii", offset, offset + 4);
2845
+ const chunkSize = buf.readUInt32LE(offset + 4);
2846
+ const body = offset + CHUNK_HEADER_BYTES;
2847
+ if (chunkId === "fmt " && formatTag === null) {
2848
+ if (chunkSize < FMT_CHUNK_MIN_BYTES || body + FMT_CHUNK_MIN_BYTES > buf.length) {
2849
+ throw new Error(`truncated fmt chunk (${chunkSize} bytes, need ${FMT_CHUNK_MIN_BYTES}): ${filepath}`);
2850
+ }
2851
+ formatTag = buf.readUInt16LE(body);
2852
+ bitsPerSample = buf.readUInt16LE(body + 14);
2853
+ if (formatTag === WAVE_FORMAT_EXTENSIBLE) {
2854
+ const extensionStart = body + FMT_CHUNK_MIN_BYTES;
2855
+ const subFormatAt = extensionStart + SUBFORMAT_OFFSET_IN_EXTENSION;
2856
+ if (chunkSize < FMT_CHUNK_MIN_BYTES + 10 || subFormatAt + 2 > buf.length) {
2857
+ throw new Error(`WAVE_FORMAT_EXTENSIBLE without a readable SubFormat GUID: ${filepath}`);
2858
+ }
2859
+ formatTag = buf.readUInt16LE(subFormatAt);
2860
+ }
2861
+ } else if (chunkId === "data" && dataStart < 0) {
2862
+ dataStart = body;
2863
+ const remaining = buf.length - body;
2864
+ dataLength = chunkSize === 0 ? remaining : Math.min(chunkSize, remaining);
2865
+ }
2866
+ const declaredAdvance = chunkSize + chunkSize % 2;
2867
+ offset = body + Math.min(declaredAdvance, Math.max(0, buf.length - body));
2868
+ }
2869
+ if (dataStart < 0) {
2870
+ throw new Error(`no data chunk in WAV: ${filepath}`);
2871
+ }
2872
+ if (bitsPerSample === null || formatTag === null) {
2873
+ throw new Error(`no fmt chunk in WAV, so the sample format is unknown: ${filepath}`);
2874
+ }
2875
+ if (formatTag !== WAVE_FORMAT_PCM) {
2876
+ throw new Error(`expected uncompressed PCM (format tag ${WAVE_FORMAT_PCM}), got tag ${formatTag}: ${filepath}`);
2877
+ }
2878
+ if (bitsPerSample !== 16) {
2879
+ throw new Error(`expected 16-bit PCM, got ${bitsPerSample}-bit: ${filepath}`);
2880
+ }
2881
+ const samples = Math.floor(dataLength / 2);
2882
+ let maxAbs = 0;
2883
+ for (let i = 0;i < samples; i++) {
2884
+ const value = Math.abs(buf.readInt16LE(dataStart + i * 2));
2885
+ if (value > maxAbs)
2886
+ maxAbs = value;
2887
+ }
2888
+ return { samples, peak: maxAbs / INT16_FULL_SCALE };
2889
+ }
2890
+ var DEFAULT_PROBE_SECONDS = 1;
2891
+ var MAX_PROBE_SECONDS = 60;
2892
+ var DEFAULT_RECORD_EXECUTABLE = "rec";
2893
+ var SILENCE_PEAK_THRESHOLD = 0.001;
2894
+ var PROBE_TIMEOUT_GRACE_MS = 1e4;
2895
+ function probeMicrophoneCapture(config, options = {}) {
2896
+ const requested = options.seconds ?? DEFAULT_PROBE_SECONDS;
2897
+ const seconds = Math.min(Math.max(requested, 1), MAX_PROBE_SECONDS);
2898
+ const executable = options.executable ?? DEFAULT_RECORD_EXECUTABLE;
2899
+ const base = {
2900
+ tool: null,
2901
+ seconds,
2902
+ samples: 0,
2903
+ peak: 0,
2904
+ silent: null
2905
+ };
2906
+ const filepath = join5(tmpdir(), `recordings-capture-probe-${process.pid}-${Date.now()}.wav`);
2907
+ try {
2908
+ const result = spawnSync2(executable, [
2909
+ "-q",
2910
+ "-r",
2911
+ String(config.sample_rate),
2912
+ "-c",
2913
+ "1",
2914
+ "-b",
2915
+ "16",
2916
+ filepath,
2917
+ "trim",
2918
+ "0",
2919
+ String(seconds)
2920
+ ], {
2921
+ stdio: ["ignore", "ignore", "pipe"],
2922
+ encoding: "utf8",
2923
+ timeout: options.timeoutMs ?? seconds * 1000 + PROBE_TIMEOUT_GRACE_MS
2924
+ });
2925
+ if (result.signal) {
2926
+ return {
2927
+ ...base,
2928
+ ok: false,
2929
+ message: `'${executable}' did not finish within ` + `${((options.timeoutMs ?? seconds * 1000 + PROBE_TIMEOUT_GRACE_MS) / 1000).toFixed(1)}s ` + `and was killed (${result.signal}). The input device is most likely busy or absent.`
2930
+ };
2931
+ }
2932
+ if (result.error) {
2933
+ return {
2934
+ ...base,
2935
+ ok: false,
2936
+ message: `could not run '${executable}': ${result.error.message}`
2937
+ };
2938
+ }
2939
+ if (!existsSync4(filepath)) {
2940
+ const stderr = (result.stderr || "").trim();
2941
+ return {
2942
+ ...base,
2943
+ ok: false,
2944
+ message: `'${executable}' wrote no audio file${stderr ? `: ${stderr}` : ""}`
2945
+ };
2946
+ }
2947
+ let peak;
2948
+ try {
2949
+ peak = readWavPeak(filepath);
2950
+ } catch (error) {
2951
+ return {
2952
+ ...base,
2953
+ ok: false,
2954
+ message: `could not read captured audio: ${error.message}`
2955
+ };
2956
+ }
2957
+ const tool = executable;
2958
+ if (peak.samples === 0) {
2959
+ return {
2960
+ ...base,
2961
+ tool,
2962
+ ok: false,
2963
+ message: "capture produced no samples"
2964
+ };
2965
+ }
2966
+ if (peak.peak <= SILENCE_PEAK_THRESHOLD) {
2967
+ const subject = captureProbeSubject();
2968
+ return {
2969
+ ...base,
2970
+ tool,
2971
+ samples: peak.samples,
2972
+ peak: peak.peak,
2973
+ ok: false,
2974
+ silent: true,
2975
+ message: `captured ${peak.samples} samples with no usable signal ` + `(peak ${peak.peak.toFixed(6)}, at or below the ${SILENCE_PEAK_THRESHOLD} silence threshold). ` + (process.platform === "darwin" ? "On macOS this is what a process WITHOUT the Microphone permission receives \u2014 " + "CoreAudio zero-fills its buffers instead of returning an error. " + "Also rule out a hardware-muted or unselected input device. " : "The input device delivered no signal \u2014 check that it is selected and not muted. ") + `This probe's capture is attributed to ${subject.subject}. ${subject.note}`
2976
+ };
2977
+ }
2978
+ return {
2979
+ tool,
2980
+ seconds,
2981
+ samples: peak.samples,
2982
+ peak: peak.peak,
2983
+ silent: false,
2984
+ ok: true,
2985
+ message: `captured ${peak.samples} samples, peak amplitude ${peak.peak.toFixed(6)} ` + `(proves capture for ${captureProbeSubject().subject})`
2986
+ };
2987
+ } finally {
2988
+ rmSync2(filepath, { force: true });
2989
+ }
2990
+ }
2991
+ function captureProbeSubject(env = process.env, options = {}) {
2992
+ const platform = options.platform ?? process.platform;
2993
+ if (platform !== "darwin") {
2994
+ return {
2995
+ headless: false,
2996
+ subject_known: true,
2997
+ subject: "this process",
2998
+ note: "microphone access is not gated by TCC on this platform"
2999
+ };
3000
+ }
3001
+ const overSsh = Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
3002
+ const inTmux = Boolean(env.TMUX || env.TMUX_PANE);
3003
+ const termProgram = env.TERM_PROGRAM?.trim();
3004
+ const hasTty = options.hasTty ?? Boolean(process.stdin.isTTY || process.stdout.isTTY);
3005
+ if (overSsh) {
3006
+ const subject2 = "the SSH session (sshd), not Recordings.app";
3007
+ return {
3008
+ headless: true,
3009
+ subject_known: true,
3010
+ subject: subject2,
3011
+ note: "Running over SSH: macOS cannot display a consent prompt to a session with no GUI, " + "so Microphone stays not_determined and a silent capture here says NOTHING about " + "whether Recordings.app can record. Judge the app by its own TCC entry and its log."
3012
+ };
3013
+ }
3014
+ if (inTmux) {
3015
+ const subject2 = "the tmux server, not Recordings.app and not this pane's shell";
3016
+ return {
3017
+ headless: false,
3018
+ subject_known: true,
3019
+ subject: subject2,
3020
+ note: "Running inside tmux: TCC attributes this capture to the tmux binary, which holds its " + "own grant. tmux also snapshots the environment at pane creation, so SSH variables may " + "be missing even in a remote session \u2014 treat a silent result as inconclusive about both " + "Recordings.app and about whether anyone could have been prompted."
3021
+ };
3022
+ }
3023
+ if (!termProgram && !hasTty) {
3024
+ return {
3025
+ headless: true,
3026
+ subject_known: false,
3027
+ subject: "an unidentified responsible process (not Recordings.app)",
3028
+ note: "The responsible process could not be identified: no SSH variables, no TERM_PROGRAM and " + "no tty, which is what launchd, cron, CI and sudo look like. Whatever holds the grant, it " + "is not Recordings.app, and nothing here can be shown a consent prompt. Inconclusive."
3029
+ };
3030
+ }
3031
+ const subject = `${termProgram || "the terminal application running this command"}, not Recordings.app`;
3032
+ return {
3033
+ headless: false,
3034
+ subject_known: Boolean(termProgram),
3035
+ subject,
3036
+ note: `Grants are per responsible process: this probe exercises ${subject}. ` + "A pass proves the microphone hardware and the input device work; it does not " + "transfer to Recordings.app, which needs its own grant."
3037
+ };
3038
+ }
3039
+ var TCC_UNREADABLE_STATE = TCC_DATABASE_UNREADABLE_STATE;
3040
+ function classifyPermissionState(permissionState) {
3041
+ switch (permissionState) {
3042
+ case "allowed":
3043
+ case "allowed_identity_unverified":
3044
+ case "denied":
3045
+ case "limited":
3046
+ case "unknown":
3047
+ case "stale_allowed_for_previous_app_build":
3048
+ case "unverified_no_installed_bundle":
3049
+ return "requested";
3050
+ case "not_determined":
3051
+ return "never_requested";
3052
+ default:
3053
+ break;
3054
+ }
3055
+ if (permissionState.startsWith("unknown("))
3056
+ return "requested";
3057
+ if (permissionState.endsWith("_identity_unverified"))
3058
+ return "requested";
3059
+ return "unknown";
3060
+ }
3061
+ function microphoneGrantInstruction(options) {
3062
+ const candidates = [options.installedAppPath, ...options.otherAppPaths ?? []].filter((path) => Boolean(path)).filter((path, index, all) => all.indexOf(path) === index).filter((path) => existsSync4(path));
3063
+ const bundlePath = candidates[0] ?? null;
3064
+ const steps = [];
3065
+ if (!bundlePath) {
3066
+ steps.push("No Recordings.app bundle was found on disk, so there is nothing to grant Microphone to yet. " + "Install the app first ('recordings app install').");
3067
+ return {
3068
+ bundle_path: null,
3069
+ bundle_identifier: RECORDINGS_BUNDLE_IDENTIFIER,
3070
+ candidate_bundle_paths: candidates,
3071
+ steps
3072
+ };
3073
+ }
3074
+ if (candidates.length > 1) {
3075
+ steps.push(`AMBIGUOUS: ${candidates.length} Recordings.app bundles exist (${candidates.join(", ")}). ` + "A TCC grant is bound to the bundle's code signature, so granting one does not grant the " + "other, and the toggle in Settings does not say which is which. Remove the bundles you are " + "not running before granting, or the grant may attach to the wrong one.");
3076
+ }
3077
+ steps.push(`At the keyboard on the machine itself (not over SSH), launch ${bundlePath} and start a ` + "recording once. macOS shows the consent sheet titled " + `"\u201CRecordings\u201D would like to access the microphone" \u2014 click Allow.`);
3078
+ steps.push("If no sheet appears, open System Settings \u2192 Privacy & Security \u2192 Microphone " + "and switch ON the row named \u201CRecordings\u201D. " + `That row is bundle ${RECORDINGS_BUNDLE_IDENTIFIER} at ${bundlePath}; the binary that ` + `receives the grant is ${join5(bundlePath, "Contents", "MacOS", "Recordings")}.`);
3079
+ if (options.requestState === "never_requested") {
3080
+ steps.push("Note: the app has never requested microphone access on this machine (no TCC entry exists), " + "so the Microphone list will NOT contain a \u201CRecordings\u201D row until the app asks once. " + "Do the launch-and-record step first; the Settings toggle only exists afterwards.");
3081
+ } else if (options.requestState === "unknown") {
3082
+ steps.push("Note: whether the app has already asked could NOT be determined \u2014 the permission state " + `did not resolve (e.g. ${TCC_UNREADABLE_STATE}: the user TCC database could not be read). ` + "So a \u201CRecordings\u201D row may or may not already be in the Microphone list. Full Disk Access " + "is inherited from the session's responsible process, not granted per-tool, so re-read the " + "state from a plain ssh shell (whose parent holds it) rather than granting it to tmux or " + "bun \u2014 and not under sudo, which changes the responsible process. Do not conclude anything " + "from this state alone.");
3083
+ }
3084
+ steps.push("Confirm afterwards with 'recordings app status --json' \u2014 microphone_permission must read " + "allowed, and a fresh line in ~/.hasna/recordings/Recordings.log must read " + "'RecordingEngine init; microphone=Microphone allowed'.");
3085
+ steps.push("The grant is bound to the bundle's code signature: re-signing or rebuilding the app with a " + "different identity voids it and this instruction has to be repeated.");
3086
+ return {
3087
+ bundle_path: bundlePath,
3088
+ bundle_identifier: RECORDINGS_BUNDLE_IDENTIFIER,
3089
+ candidate_bundle_paths: candidates,
3090
+ steps
3091
+ };
3092
+ }
3093
+
3094
+ // src/lib/persistence-probe.ts
3095
+ import { Database as Database2 } from "bun:sqlite";
3096
+ import { existsSync as existsSync5 } from "fs";
3097
+ var AUTO_FLIP_MODE_SOURCE = "auto:api-url+api-key";
3098
+ function safeBaseUrl(baseUrl) {
3099
+ if (!baseUrl)
3100
+ return baseUrl;
3101
+ try {
3102
+ const url = new URL(baseUrl);
3103
+ if (!url.username && !url.password)
3104
+ return baseUrl;
3105
+ url.username = "";
3106
+ url.password = "";
3107
+ return `${url.toString().replace(/\/+$/, "")} (credentials in the URL redacted)`;
3108
+ } catch {
3109
+ return "[unparseable URL redacted]";
3110
+ }
3111
+ }
3112
+ function readLocalRecordingCount(dbPath) {
3113
+ try {
3114
+ const db = new Database2(dbPath, { readonly: true });
3115
+ try {
3116
+ const row = db.query("SELECT COUNT(*) AS count FROM recordings").get();
3117
+ return typeof row?.count === "number" ? row.count : null;
3118
+ } finally {
3119
+ db.close();
3120
+ }
3121
+ } catch {
3122
+ return null;
3123
+ }
3124
+ }
3125
+ function localStoreIsBehindSchema(dbPath) {
3126
+ if (!existsSync5(dbPath))
3127
+ return null;
3128
+ try {
3129
+ const db = new Database2(dbPath, { readonly: true });
3130
+ try {
3131
+ const row = db.query("SELECT MAX(id) AS max_id FROM _migrations").get();
3132
+ const level = typeof row?.max_id === "number" ? row.max_id : -1;
3133
+ return level < CURRENT_MIGRATION_LEVEL;
3134
+ } finally {
3135
+ db.close();
3136
+ }
3137
+ } catch {
3138
+ return null;
3139
+ }
3140
+ }
3141
+ function describeActiveStore(config, env = process.env) {
3142
+ const localDbPath = config.db_path;
3143
+ const localDbPresent = existsSync5(localDbPath);
3144
+ const localDbRecordings = localDbPresent ? readLocalRecordingCount(localDbPath) : null;
3145
+ let resolution;
3146
+ try {
3147
+ resolution = resolveTransport(APP, env);
3148
+ } catch (error) {
3149
+ return {
3150
+ transport: "sqlite",
3151
+ mode_source: "unresolved",
3152
+ base_url: null,
3153
+ local_db_path: localDbPath,
3154
+ local_db_present: localDbPresent,
3155
+ local_db_recordings: localDbRecordings,
3156
+ divergent: false,
3157
+ warning: redactKeyMaterial(`could not resolve the active store: ${error.message}`)
3158
+ };
3159
+ }
3160
+ const divergent = resolution.transport === "http" && (localDbRecordings ?? 0) > 0;
3161
+ const warnings = [];
3162
+ if (resolution.warning)
3163
+ warnings.push(resolution.warning);
3164
+ if (resolution.transport === "http" && localDbPresent && localDbRecordings === null) {
3165
+ warnings.push(`writes go to ${safeBaseUrl(resolution.baseUrl)}, and ${localDbPath} exists but could not be counted, ` + "so whether a second dataset is sitting there is UNKNOWN \u2014 not 'none'.");
3166
+ }
3167
+ if (divergent) {
3168
+ warnings.push(`writes go to ${safeBaseUrl(resolution.baseUrl)}, but ${localDbPath} still holds ` + `${localDbRecordings} recordings from an earlier on-box-only period. ` + "That file is NOT the live store \u2014 auditing it undercounts and looks like data loss.");
3169
+ }
3170
+ if (resolution.transport === "http" && resolution.modeSource === AUTO_FLIP_MODE_SOURCE) {
3171
+ warnings.push("the API transport was selected by the mere PRESENCE of " + "HASNA_RECORDINGS_API_URL + HASNA_RECORDINGS_API_KEY, with no store variable set. " + "Check `launchctl getenv HASNA_RECORDINGS_API_URL` too: a launchd session variable " + "is inherited by the GUI app as well as by shells, and is invisible in a login profile.");
3172
+ }
3173
+ return {
3174
+ transport: resolution.transport,
3175
+ mode_source: resolution.modeSource,
3176
+ base_url: safeBaseUrl(resolution.baseUrl),
3177
+ local_db_path: localDbPath,
3178
+ local_db_present: localDbPresent,
3179
+ local_db_recordings: localDbRecordings,
3180
+ divergent,
3181
+ warning: warnings.length > 0 ? warnings.join(" ") : null
3182
+ };
3183
+ }
3184
+ var PROBE_SEVERITY_GLYPHS = {
3185
+ pass: "\u2713",
3186
+ warn: "?",
3187
+ fail: "\u2717"
3188
+ };
3189
+ function persistenceOutcomeSeverity(outcome) {
3190
+ switch (outcome) {
3191
+ case "proved":
3192
+ return "pass";
3193
+ case "skipped":
3194
+ return "warn";
3195
+ case "failed":
3196
+ return "fail";
3197
+ }
3198
+ }
3199
+ function renderPersistenceMarker(outcome, paint) {
3200
+ const severity = persistenceOutcomeSeverity(outcome);
3201
+ return paint[severity](PROBE_SEVERITY_GLYPHS[severity]);
3202
+ }
3203
+ var PERSISTENCE_PROBE_TAG = "check-probe";
3204
+ var PERSISTENCE_PROBE_MARKER_PREFIX = "recordings check --probe persistence round-trip";
3205
+ async function probeRecordingPersistence(options = {}) {
3206
+ const store = options.store ?? getStore();
3207
+ const stamp = (options.now?.() ?? new Date).toISOString();
3208
+ const marker = `${PERSISTENCE_PROBE_MARKER_PREFIX} ${stamp}`;
3209
+ const transport = store.mode === "http" ? "http" : "sqlite";
3210
+ const base = {
3211
+ attempted: false,
3212
+ transport,
3213
+ base_url: safeBaseUrl(store.baseUrl),
3214
+ recording_id: null,
3215
+ read_back: false,
3216
+ cleaned_up: false,
3217
+ created_local_store: false,
3218
+ reachable: null
3219
+ };
3220
+ if (transport === "sqlite" && options.localStoreIsLegacy === true && !options.allowLocalMigration) {
3221
+ return {
3222
+ ...base,
3223
+ ok: false,
3224
+ outcome: "skipped",
3225
+ message: "NOT MEASURED: the local SQLite store is behind the current schema, and writing to it " + "would apply pending migrations to a legacy file. Migrate deliberately " + "('recordings migrate') or re-run with --probe-store-write to accept the migration as " + "part of the probe."
3226
+ };
3227
+ }
3228
+ if (transport === "http" && !options.allowRemoteWrite) {
3229
+ let reachable = null;
3230
+ let reachabilityDetail = "";
3231
+ try {
3232
+ await store.getRecordingStats();
3233
+ reachable = true;
3234
+ reachabilityDetail = " The store did answer a read-only stats request, so it is reachable " + "and the credential is accepted.";
3235
+ } catch (error) {
3236
+ reachable = false;
3237
+ reachabilityDetail = " The store did NOT answer a read-only stats request: " + `${redactKeyMaterial(error.message)}.`;
3238
+ }
3239
+ return {
3240
+ ...base,
3241
+ reachable,
3242
+ ok: false,
3243
+ outcome: "skipped",
3244
+ message: `NOT MEASURED: the write round-trip was skipped, because it would add a marker recording ` + `to the shared API store at ${safeBaseUrl(store.baseUrl)}, which is production for every ` + `machine pointed at it. Re-run with --probe-store-write to prove the write path for real.` + reachabilityDetail
3245
+ };
3246
+ }
3247
+ let created = null;
3248
+ try {
3249
+ created = await store.createRecording({
3250
+ raw_text: marker,
3251
+ processing_mode: "raw",
3252
+ model_used: "check-probe",
3253
+ duration_ms: 0,
3254
+ tags: ["diagnostic", PERSISTENCE_PROBE_TAG],
3255
+ metadata: { persistence_probe: true, probed_at: stamp }
3256
+ });
3257
+ } catch (error) {
3258
+ return {
3259
+ ...base,
3260
+ attempted: true,
3261
+ ok: false,
3262
+ outcome: "failed",
3263
+ message: `store rejected the write: ${redactKeyMaterial(error.message)}. ` + "If this was a timeout rather than a refusal, the write may still have committed under " + `an id this process never saw \u2014 check with 'recordings search "${PERSISTENCE_PROBE_MARKER_PREFIX}"'.`
3264
+ };
3265
+ }
3266
+ if (!created?.id) {
3267
+ return {
3268
+ ...base,
3269
+ attempted: true,
3270
+ ok: false,
3271
+ outcome: "failed",
3272
+ message: "store accepted the write but returned no id"
3273
+ };
3274
+ }
3275
+ let readBack = false;
3276
+ let readError = null;
3277
+ try {
3278
+ const fetched = await store.getRecording(created.id);
3279
+ readBack = fetched?.raw_text === marker;
3280
+ if (fetched && !readBack) {
3281
+ readError = "the stored transcript did not match what was written";
3282
+ } else if (!fetched) {
3283
+ readError = "the write was accepted but the recording could not be read back";
3284
+ }
3285
+ } catch (error) {
3286
+ readError = `read-back failed: ${redactKeyMaterial(error.message)}`;
3287
+ }
3288
+ let cleanedUp = false;
3289
+ let cleanupError = null;
3290
+ try {
3291
+ const deleteReported = await store.deleteRecording(created.id);
3292
+ if (!deleteReported) {
3293
+ cleanupError = "the store reported nothing to delete";
3294
+ } else {
3295
+ const stillThere = await store.getRecording(created.id);
3296
+ if (stillThere) {
3297
+ cleanupError = "the store reported the delete succeeded but the recording is still readable";
3298
+ } else {
3299
+ cleanedUp = true;
3300
+ }
3301
+ }
3302
+ } catch (error) {
3303
+ cleanupError = redactKeyMaterial(error.message);
3304
+ }
3305
+ const createdLocalStore = transport === "sqlite" && options.localStoreExistedBefore === false;
3306
+ const where = store.baseUrl ? `the API store at ${safeBaseUrl(store.baseUrl)}` : "the local SQLite store";
3307
+ const createdNote = createdLocalStore ? " (this probe CREATED and migrated the local SQLite store, which did not exist before)" : "";
3308
+ if (readError) {
3309
+ return {
3310
+ ...base,
3311
+ attempted: true,
3312
+ recording_id: created.id,
3313
+ cleaned_up: cleanedUp,
3314
+ created_local_store: createdLocalStore,
3315
+ ok: false,
3316
+ outcome: "failed",
3317
+ message: `${readError} (${where}, id ${created.id.slice(0, 8)})${createdNote}`
3318
+ };
3319
+ }
3320
+ if (!cleanedUp) {
3321
+ return {
3322
+ ...base,
3323
+ attempted: true,
3324
+ recording_id: created.id,
3325
+ read_back: true,
3326
+ cleaned_up: false,
3327
+ created_local_store: createdLocalStore,
3328
+ ok: false,
3329
+ outcome: "failed",
3330
+ message: `wrote and read back a marker recording in ${where}, but could NOT confirm its removal ` + `(${cleanupError ?? "cleanup failed"}). The probe recording ${created.id} may still be ` + `there \u2014 remove it with 'recordings delete ${created.id}'.${createdNote}`
3331
+ };
3332
+ }
3333
+ return {
3334
+ ...base,
3335
+ attempted: true,
3336
+ recording_id: created.id,
3337
+ read_back: true,
3338
+ cleaned_up: true,
3339
+ created_local_store: createdLocalStore,
3340
+ ok: true,
3341
+ outcome: "proved",
3342
+ message: `wrote, read back and confirmed removal of a marker recording in ${where}${createdNote}`
3343
+ };
3344
+ }
3345
+
2707
3346
  // src/lib/enhancer.ts
2708
3347
  import OpenAI2 from "openai";
2709
3348
  var _enhancementClient = null;
@@ -2968,80 +3607,805 @@ ${block}` : block;
2968
3607
  }
2969
3608
 
2970
3609
  // src/cli/macos-permissions.ts
2971
- import { spawnSync as spawnSync2 } from "child_process";
2972
- import { join as join5 } from "path";
2973
- var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync2(executable, arguments_, options);
3610
+ import { spawnSync as spawnSync3 } from "child_process";
3611
+ import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
3612
+ import { tmpdir as tmpdir2 } from "os";
3613
+ import { join as join6 } from "path";
3614
+ var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync3(executable, arguments_, options);
2974
3615
  function runMacOSPermissionRequest(appPath, runner = defaultPermissionHelperRunner) {
2975
- const executable = join5(appPath, "Contents", "MacOS", "Recordings");
3616
+ const executable = join6(appPath, "Contents", "MacOS", "Recordings");
2976
3617
  const result = runner(executable, ["--request-permissions", "--open-permission-settings"], { stdio: "inherit" });
2977
3618
  return {
2978
3619
  exitCode: result.error ? 1 : result.status ?? 1,
2979
3620
  errorMessage: result.error?.message
2980
3621
  };
2981
3622
  }
2982
-
2983
- // src/lib/machine.ts
2984
- import { hostname } from "os";
2985
- function currentMachineId(env = process.env, hostName = hostname()) {
2986
- const configured = env.HASNA_MACHINE_ID?.trim();
2987
- if (configured)
2988
- return configured;
2989
- return hostName.trim();
3623
+ var CODESIGN_REQUIREMENT_SATISFIED_STATUS = 0;
3624
+ var CODESIGN_REQUIREMENT_UNSATISFIED_STATUS = 3;
3625
+ function tccDatabasePaths(home) {
3626
+ return [
3627
+ join6(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
3628
+ join6("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
3629
+ ];
2990
3630
  }
2991
-
2992
- // src/lib/bun-runtime.ts
2993
- import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
2994
- import { isAbsolute as isAbsolute2 } from "path";
2995
- import { randomBytes } from "crypto";
2996
- import { spawnSync as spawnSync3 } from "child_process";
2997
- var BUN_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
2998
- var INSTALLER_ENVIRONMENT_KEYS = [
2999
- "HOME",
3000
- "SSH_CONNECTION",
3001
- "RECORDINGS_EXPECTED_TEAM_IDENTIFIER",
3002
- "RECORDINGS_LAUNCH_TIMEOUT_SECONDS",
3003
- "RECORDINGS_LOCK_STALE_SECONDS",
3004
- "RECORDINGS_MAINTENANCE_STALE_SECONDS",
3005
- "RECORDINGS_READER_DRAIN_TIMEOUT_MS",
3006
- "RECORDINGS_SQLITE_BUSY_TIMEOUT_MS"
3007
- ];
3008
- var INSTALLER_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
3009
- function validateBunExecutable(candidate) {
3010
- if (!isAbsolute2(candidate))
3011
- return { reason: "path is not absolute" };
3012
- let executable;
3631
+ function tccAuthValueLabel(value) {
3632
+ switch (value) {
3633
+ case "0":
3634
+ return "denied";
3635
+ case "1":
3636
+ return "unknown";
3637
+ case "2":
3638
+ return "allowed";
3639
+ case "3":
3640
+ return "limited";
3641
+ default:
3642
+ return `unknown(${value})`;
3643
+ }
3644
+ }
3645
+ var defaultCodesignRequirementRunner = (requirementPath, appPath) => spawnSync3("/usr/bin/codesign", ["--verify", "--verbose=2", "-R", requirementPath, appPath], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
3646
+ function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = defaultCodesignRequirementRunner, bundleExists = existsSync6) {
3647
+ const normalized = csreqHex.trim();
3648
+ if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(normalized)) {
3649
+ return "unverifiable";
3650
+ }
3651
+ if (!appPath || !bundleExists(appPath))
3652
+ return "unverifiable";
3653
+ let scratchDirectory = null;
3013
3654
  try {
3014
- executable = realpathSync3(candidate);
3015
- if (!statSync2(executable).isFile())
3016
- return { reason: "resolved path is not a regular file" };
3017
- accessSync(executable, fsConstants.X_OK);
3655
+ scratchDirectory = mkdtempSync(join6(tmpdir2(), "recordings-tcc-csreq-"));
3656
+ const requirementPath = join6(scratchDirectory, "tcc-requirement.bin");
3657
+ writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
3658
+ const result = runner(requirementPath, appPath);
3659
+ if (result.error)
3660
+ return "unverifiable";
3661
+ if (result.status === CODESIGN_REQUIREMENT_SATISFIED_STATUS)
3662
+ return "satisfied";
3663
+ if (result.status === CODESIGN_REQUIREMENT_UNSATISFIED_STATUS)
3664
+ return "unsatisfied";
3665
+ return "unverifiable";
3018
3666
  } catch {
3019
- return { reason: "path is missing, inaccessible, or not executable" };
3667
+ return "unverifiable";
3668
+ } finally {
3669
+ if (scratchDirectory)
3670
+ rmSync3(scratchDirectory, { recursive: true, force: true });
3020
3671
  }
3021
- const nonce = randomBytes(24).toString("hex");
3022
- const probe = spawnSync3(executable, [
3023
- "-e",
3024
- `
3025
- import { realpathSync, statSync } from "node:fs";
3026
- const expected = process.argv[1];
3027
- const nonce = process.argv[2];
3028
- const actual = process.execPath;
3029
- if (!expected || !nonce || realpathSync(actual) !== realpathSync(expected)) process.exit(66);
3030
- if (!statSync(actual).isFile()) process.exit(66);
3031
- if (!/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/.test(Bun.version)) {
3032
- process.exit(66);
3033
- }
3034
- process.stdout.write(nonce + ":" + Bun.version);
3035
- `,
3036
- executable,
3037
- nonce
3038
- ], {
3039
- encoding: "utf8",
3040
- env: {
3041
- HOME: "/tmp",
3042
- PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
3043
- TMPDIR: "/tmp"
3044
- },
3672
+ }
3673
+ var defaultTccPermissionProbe = {
3674
+ databasePresence: (dbPath) => {
3675
+ try {
3676
+ statSync2(dbPath);
3677
+ return "present";
3678
+ } catch (error) {
3679
+ return error.code === "ENOENT" ? "absent" : "indeterminate";
3680
+ }
3681
+ },
3682
+ readAccessRow: (dbPath, service) => {
3683
+ const sql = "select ifnull(auth_value, '') || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = '" + RECORDINGS_BUNDLE_IDENTIFIER + "' and client_type = 0 order by last_modified desc limit 1;";
3684
+ const result = spawnSync3("/usr/bin/sqlite3", [dbPath, sql], {
3685
+ encoding: "utf8",
3686
+ stdio: ["ignore", "pipe", "pipe"]
3687
+ });
3688
+ if (result.error)
3689
+ return { kind: "unreadable", detail: result.error.message };
3690
+ if (result.status !== 0) {
3691
+ const detail = (result.stderr ?? "").trim() || `sqlite3 exited ${result.status}`;
3692
+ if (/no such table/i.test(detail))
3693
+ return { kind: "absent" };
3694
+ return { kind: "unreadable", detail };
3695
+ }
3696
+ const value = result.stdout?.trim() ?? "";
3697
+ if (!value)
3698
+ return { kind: "absent" };
3699
+ const [authValue = "", csreqHex = ""] = value.split("|");
3700
+ return { kind: "row", row: { authValue, csreqHex } };
3701
+ },
3702
+ verifyStoredRequirement: (csreqHex, appPath) => verifyStoredRequirementWithCodesign(csreqHex, appPath),
3703
+ describeStoredRequirement: (csreqHex) => describeStoredRequirementWithCsreq(csreqHex)
3704
+ };
3705
+ function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath) => spawnSync3("/usr/bin/csreq", ["-r", requirementPath, "-t"], {
3706
+ encoding: "utf8",
3707
+ stdio: ["ignore", "pipe", "ignore"]
3708
+ })) {
3709
+ const normalized = csreqHex.trim();
3710
+ if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(normalized)) {
3711
+ return null;
3712
+ }
3713
+ let scratchDirectory = null;
3714
+ try {
3715
+ scratchDirectory = mkdtempSync(join6(tmpdir2(), "recordings-tcc-decode-"));
3716
+ const requirementPath = join6(scratchDirectory, "tcc-requirement.bin");
3717
+ writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
3718
+ const result = runner(requirementPath);
3719
+ if (result.error || result.status !== 0)
3720
+ return null;
3721
+ const text = (result.stdout ?? "").trim();
3722
+ return text.length > 0 ? text : null;
3723
+ } catch {
3724
+ return null;
3725
+ } finally {
3726
+ if (scratchDirectory)
3727
+ rmSync3(scratchDirectory, { recursive: true, force: true });
3728
+ }
3729
+ }
3730
+ function resolveTccGrant(options) {
3731
+ const probe = options.probe ?? defaultTccPermissionProbe;
3732
+ let sawUnreadableDatabase = false;
3733
+ for (const dbPath of tccDatabasePaths(options.home)) {
3734
+ const presence = probe.databasePresence(dbPath);
3735
+ if (presence === "indeterminate") {
3736
+ sawUnreadableDatabase = true;
3737
+ continue;
3738
+ }
3739
+ if (presence === "absent")
3740
+ continue;
3741
+ const lookup = probe.readAccessRow(dbPath, options.service);
3742
+ if (lookup.kind === "unreadable") {
3743
+ sawUnreadableDatabase = true;
3744
+ continue;
3745
+ }
3746
+ if (lookup.kind === "absent")
3747
+ continue;
3748
+ const row = lookup.row;
3749
+ const storedRequirement = probe.describeStoredRequirement?.(row.csreqHex) ?? null;
3750
+ const durability = classifyTccGrantDurability({
3751
+ designatedRequirement: storedRequirement,
3752
+ adHocSigned: false
3753
+ });
3754
+ const authValue = row.authValue.trim();
3755
+ if (authValue.length === 0)
3756
+ continue;
3757
+ const label = tccAuthValueLabel(authValue);
3758
+ if (label !== "allowed") {
3759
+ return { state: label, storedRequirement, durability };
3760
+ }
3761
+ if (!options.appPath) {
3762
+ return { state: "unverified_no_installed_bundle", storedRequirement, durability };
3763
+ }
3764
+ switch (probe.verifyStoredRequirement(row.csreqHex, options.appPath)) {
3765
+ case "satisfied":
3766
+ return { state: "allowed", storedRequirement, durability };
3767
+ case "unsatisfied":
3768
+ return { state: "stale_allowed_for_previous_app_build", storedRequirement, durability };
3769
+ default:
3770
+ return { state: "allowed_identity_unverified", storedRequirement, durability };
3771
+ }
3772
+ }
3773
+ return {
3774
+ state: sawUnreadableDatabase ? TCC_DATABASE_UNREADABLE_STATE : "not_determined",
3775
+ storedRequirement: null,
3776
+ durability: "unknown"
3777
+ };
3778
+ }
3779
+ function classifyTccGrantDurability(options) {
3780
+ if (options.adHocSigned)
3781
+ return "dies_on_rebuild_cdhash_pinned";
3782
+ const requirement = options.designatedRequirement?.trim();
3783
+ if (!requirement)
3784
+ return "unknown";
3785
+ const operators = requirement.replace(/"[^"]*"/g, '""');
3786
+ if (/\bcdhash\b/i.test(operators))
3787
+ return "dies_on_rebuild_cdhash_pinned";
3788
+ if (/\banchor\s+apple\s+generic\b/i.test(operators))
3789
+ return "survives_rebuild_developer_id";
3790
+ if (/\banchor\s+apple\b/i.test(operators))
3791
+ return "survives_rebuild_certificate_anchored";
3792
+ if (/\bcertificate\s+(root|leaf|\d+)\b|\banchor\s+H""/i.test(operators)) {
3793
+ return "survives_rebuild_certificate_anchored";
3794
+ }
3795
+ return "unknown";
3796
+ }
3797
+ function describeTccAuthorizationSubject(appPath) {
3798
+ if (!appPath) {
3799
+ return `${RECORDINGS_BUNDLE_IDENTIFIER} (no installed bundle found \u2014 nothing to report a grant for)`;
3800
+ }
3801
+ return `${appPath} (${RECORDINGS_BUNDLE_IDENTIFIER})`;
3802
+ }
3803
+
3804
+ // src/cli/macos-shortcut.ts
3805
+ import { spawnSync as spawnSync4 } from "child_process";
3806
+ import { existsSync as existsSync7 } from "fs";
3807
+ var TRIGGER_DEFAULTS_EXECUTABLE = process.platform === "darwin" ? "/usr/bin/defaults" : process.env.RECORDINGS_TEST_DEFAULTS_EXECUTABLE ?? null;
3808
+ var RECORDINGS_BUNDLE_ID = RECORDINGS_BUNDLE_IDENTIFIER;
3809
+ var SHORTCUT_USER_DEFAULTS_PREFIX = "KeyboardShortcuts_";
3810
+ var TOGGLE_RECORDING_SHORTCUT_NAME = "toggleRecording";
3811
+ var USE_FN_KEY_DEFAULTS_KEY = "useFnKey";
3812
+ var DEFAULT_TOGGLE_RECORDING_CHORD = "f5";
3813
+ var TOGGLE_RECORDING_DEFAULTS_KEY = `${SHORTCUT_USER_DEFAULTS_PREFIX}${TOGGLE_RECORDING_SHORTCUT_NAME}`;
3814
+ var TRIGGER_GRANT_REQUIREMENTS = [
3815
+ {
3816
+ id: "hotkey",
3817
+ label: "Hotkey",
3818
+ mechanism: "Carbon RegisterEventHotKey, via KeyboardShortcuts 1.12.0",
3819
+ tccService: null,
3820
+ settingsPath: null
3821
+ },
3822
+ {
3823
+ id: "fn",
3824
+ label: "fn/Globe",
3825
+ mechanism: "CGEventTap (.defaultTap \u2014 it swallows fn)",
3826
+ tccService: "kTCCServiceAccessibility",
3827
+ settingsPath: "System Settings > Privacy & Security > Accessibility"
3828
+ }
3829
+ ];
3830
+ var NON_CHORD_KEYS = {
3831
+ fn: "fn/Globe is not a Carbon key, so it cannot be part of a chord. Enable it with: recordings shortcut --fn on",
3832
+ function: "fn/Globe is not a Carbon key, so it cannot be part of a chord. Enable it with: recordings shortcut --fn on",
3833
+ globe: "fn/Globe is not a Carbon key, so it cannot be part of a chord. Enable it with: recordings shortcut --fn on",
3834
+ "\uD83C\uDF10": "fn/Globe is not a Carbon key, so it cannot be part of a chord. Enable it with: recordings shortcut --fn on"
3835
+ };
3836
+ var CARBON_MODIFIERS = {
3837
+ cmd: 256,
3838
+ shift: 512,
3839
+ opt: 2048,
3840
+ ctrl: 4096
3841
+ };
3842
+ var MODIFIER_ALIASES = {
3843
+ cmd: "cmd",
3844
+ command: "cmd",
3845
+ meta: "cmd",
3846
+ super: "cmd",
3847
+ "\u2318": "cmd",
3848
+ shift: "shift",
3849
+ "\u21E7": "shift",
3850
+ opt: "opt",
3851
+ option: "opt",
3852
+ alt: "opt",
3853
+ "\u2325": "opt",
3854
+ ctrl: "ctrl",
3855
+ control: "ctrl",
3856
+ "\u2303": "ctrl"
3857
+ };
3858
+ var MODIFIER_GLYPHS = [
3859
+ ["ctrl", "\u2303"],
3860
+ ["opt", "\u2325"],
3861
+ ["shift", "\u21E7"],
3862
+ ["cmd", "\u2318"]
3863
+ ];
3864
+ var CARBON_KEY_CODES = {
3865
+ a: 0,
3866
+ s: 1,
3867
+ d: 2,
3868
+ f: 3,
3869
+ h: 4,
3870
+ g: 5,
3871
+ z: 6,
3872
+ x: 7,
3873
+ c: 8,
3874
+ v: 9,
3875
+ b: 11,
3876
+ q: 12,
3877
+ w: 13,
3878
+ e: 14,
3879
+ r: 15,
3880
+ y: 16,
3881
+ t: 17,
3882
+ o: 31,
3883
+ u: 32,
3884
+ i: 34,
3885
+ p: 35,
3886
+ l: 37,
3887
+ j: 38,
3888
+ k: 40,
3889
+ n: 45,
3890
+ m: 46,
3891
+ "1": 18,
3892
+ "2": 19,
3893
+ "3": 20,
3894
+ "4": 21,
3895
+ "6": 22,
3896
+ "5": 23,
3897
+ "9": 25,
3898
+ "7": 26,
3899
+ "8": 28,
3900
+ "0": 29,
3901
+ equal: 24,
3902
+ minus: 27,
3903
+ rightbracket: 30,
3904
+ leftbracket: 33,
3905
+ quote: 39,
3906
+ semicolon: 41,
3907
+ backslash: 42,
3908
+ comma: 43,
3909
+ slash: 44,
3910
+ period: 47,
3911
+ grave: 50,
3912
+ tab: 48,
3913
+ space: 49,
3914
+ return: 36,
3915
+ enter: 36,
3916
+ delete: 51,
3917
+ escape: 53,
3918
+ esc: 53,
3919
+ f1: 122,
3920
+ f2: 120,
3921
+ f3: 99,
3922
+ f4: 118,
3923
+ f5: 96,
3924
+ f6: 97,
3925
+ f7: 98,
3926
+ f8: 100,
3927
+ f9: 101,
3928
+ f10: 109,
3929
+ f11: 103,
3930
+ f12: 111,
3931
+ f13: 105,
3932
+ f14: 107,
3933
+ f15: 113,
3934
+ f16: 106,
3935
+ f17: 64,
3936
+ f18: 79,
3937
+ f19: 80,
3938
+ f20: 90,
3939
+ home: 115,
3940
+ end: 119,
3941
+ pageup: 116,
3942
+ pagedown: 121,
3943
+ left: 123,
3944
+ right: 124,
3945
+ down: 125,
3946
+ up: 126
3947
+ };
3948
+ var KEY_LABELS = new Map;
3949
+ for (const [name, code] of Object.entries(CARBON_KEY_CODES)) {
3950
+ if (!KEY_LABELS.has(code))
3951
+ KEY_LABELS.set(code, name);
3952
+ }
3953
+
3954
+ class ShortcutParseError extends Error {
3955
+ }
3956
+ function listBindableKeys() {
3957
+ return Object.keys(CARBON_KEY_CODES);
3958
+ }
3959
+ function parseShortcutChord(input) {
3960
+ const trimmed = input.trim();
3961
+ if (!trimmed)
3962
+ throw new ShortcutParseError("Shortcut is empty");
3963
+ const normalized = trimmed.replace(/[\u2303\u2325\u21E7\u2318]/g, (glyph) => `${glyph}+`).toLowerCase();
3964
+ const tokens = normalized.split("+").map((token) => token.trim()).filter((token) => token.length > 0);
3965
+ if (tokens.length === 0)
3966
+ throw new ShortcutParseError(`Could not parse shortcut "${input}"`);
3967
+ let carbonModifiers = 0;
3968
+ const keyTokens = [];
3969
+ for (const token of tokens) {
3970
+ const modifier = MODIFIER_ALIASES[token];
3971
+ if (modifier) {
3972
+ carbonModifiers |= CARBON_MODIFIERS[modifier];
3973
+ continue;
3974
+ }
3975
+ keyTokens.push(token);
3976
+ }
3977
+ if (keyTokens.length === 0) {
3978
+ throw new ShortcutParseError(`"${input}" is only modifiers \u2014 a shortcut needs a real key, e.g. "f13" or "ctrl+opt+r"`);
3979
+ }
3980
+ if (keyTokens.length > 1) {
3981
+ throw new ShortcutParseError(`"${input}" names more than one key (${keyTokens.join(", ")}) \u2014 bind exactly one`);
3982
+ }
3983
+ const [keyToken] = keyTokens;
3984
+ if (keyToken === undefined) {
3985
+ throw new ShortcutParseError(`Could not parse shortcut "${input}"`);
3986
+ }
3987
+ const nonChordReason = NON_CHORD_KEYS[keyToken];
3988
+ if (nonChordReason)
3989
+ throw new ShortcutParseError(nonChordReason);
3990
+ const carbonKeyCode = CARBON_KEY_CODES[keyToken];
3991
+ if (carbonKeyCode === undefined) {
3992
+ throw new ShortcutParseError(`Unknown key "${keyToken}" \u2014 see 'recordings shortcut --keys'`);
3993
+ }
3994
+ return { carbonKeyCode, carbonModifiers };
3995
+ }
3996
+ function formatShortcut(shortcut) {
3997
+ let prefix = "";
3998
+ for (const [token, glyph] of MODIFIER_GLYPHS) {
3999
+ if ((shortcut.carbonModifiers & CARBON_MODIFIERS[token]) === CARBON_MODIFIERS[token]) {
4000
+ prefix += glyph;
4001
+ }
4002
+ }
4003
+ const label = KEY_LABELS.get(shortcut.carbonKeyCode);
4004
+ return `${prefix}${label ? label.toUpperCase() : `keycode ${shortcut.carbonKeyCode}`}`;
4005
+ }
4006
+ var defaultBundleIdentifierReader = (bundlePath) => {
4007
+ if (TRIGGER_DEFAULTS_EXECUTABLE === null)
4008
+ return null;
4009
+ if (!existsSync7(`${bundlePath}/Contents/Info.plist`) && !existsSync7(`${bundlePath}/Contents/Info`)) {
4010
+ return null;
4011
+ }
4012
+ const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["read", `${bundlePath}/Contents/Info`, "CFBundleIdentifier"], { encoding: "utf8" });
4013
+ if (result.status !== 0)
4014
+ return null;
4015
+ return result.stdout.trim() || null;
4016
+ };
4017
+ var PROCESS_LISTER_EXECUTABLE = process.platform === "darwin" ? "/bin/ps" : process.env.RECORDINGS_TEST_PS_EXECUTABLE ?? "/bin/ps";
4018
+ var defaultProcessLister = () => {
4019
+ const result = spawnSync4(PROCESS_LISTER_EXECUTABLE, ["-Awwo", "comm="], { encoding: "utf8" });
4020
+ if (result.status !== 0)
4021
+ return null;
4022
+ return result.stdout;
4023
+ };
4024
+ function runningAppBundlePaths(probes = {}) {
4025
+ const listProcesses = probes.listProcesses ?? defaultProcessLister;
4026
+ const readBundleIdentifier = probes.readBundleIdentifier ?? defaultBundleIdentifierReader;
4027
+ const listing = listProcesses();
4028
+ if (!listing)
4029
+ return [];
4030
+ const suffixPattern = /\/Contents\/MacOS\/[^/]+$/;
4031
+ const paths = new Set;
4032
+ const identifierCache = new Map;
4033
+ const readCached = (candidate) => {
4034
+ const cached2 = identifierCache.get(candidate);
4035
+ if (cached2 !== undefined)
4036
+ return cached2;
4037
+ const identifier = readBundleIdentifier(candidate);
4038
+ identifierCache.set(candidate, identifier);
4039
+ return identifier;
4040
+ };
4041
+ for (const raw of listing.split(`
4042
+ `)) {
4043
+ const line = raw.trim();
4044
+ const suffix = suffixPattern.exec(line);
4045
+ if (!suffix)
4046
+ continue;
4047
+ const bundle = line.slice(0, suffix.index);
4048
+ for (let index = bundle.indexOf("/");index !== -1; index = bundle.indexOf("/", index + 1)) {
4049
+ const candidate = bundle.slice(index);
4050
+ if (!isThisApp(candidate, readCached))
4051
+ continue;
4052
+ paths.add(candidate);
4053
+ break;
4054
+ }
4055
+ }
4056
+ return [...paths].sort();
4057
+ }
4058
+ function isThisApp(candidate, readBundleIdentifier) {
4059
+ return readBundleIdentifier(candidate) === RECORDINGS_BUNDLE_ID;
4060
+ }
4061
+ function readDefault(key) {
4062
+ if (TRIGGER_DEFAULTS_EXECUTABLE === null)
4063
+ return null;
4064
+ const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["read", RECORDINGS_BUNDLE_ID, key], {
4065
+ encoding: "utf8"
4066
+ });
4067
+ if (result.status !== 0)
4068
+ return null;
4069
+ return result.stdout.trim();
4070
+ }
4071
+ function writeDefault(key, value) {
4072
+ if (TRIGGER_DEFAULTS_EXECUTABLE === null) {
4073
+ throw new Error(`cannot write ${key}: this machine has no readable app UserDefaults domain`);
4074
+ }
4075
+ const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, key, "-string", value], {
4076
+ encoding: "utf8"
4077
+ });
4078
+ if (result.status !== 0) {
4079
+ throw new Error(`defaults write ${key} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
4080
+ }
4081
+ }
4082
+ function parseStoredShortcut(raw) {
4083
+ if (!raw)
4084
+ return null;
4085
+ try {
4086
+ const parsed = JSON.parse(raw);
4087
+ if (typeof parsed.carbonKeyCode !== "number" || typeof parsed.carbonModifiers !== "number") {
4088
+ return null;
4089
+ }
4090
+ return { carbonKeyCode: parsed.carbonKeyCode, carbonModifiers: parsed.carbonModifiers };
4091
+ } catch {
4092
+ return null;
4093
+ }
4094
+ }
4095
+ function readTriggerState() {
4096
+ const rawShortcut = readDefault(TOGGLE_RECORDING_DEFAULTS_KEY);
4097
+ const rawFn = readDefault(USE_FN_KEY_DEFAULTS_KEY);
4098
+ return {
4099
+ shortcut: parseStoredShortcut(rawShortcut),
4100
+ rawShortcut,
4101
+ useFnKey: rawFn === "1" || rawFn?.toLowerCase() === "true"
4102
+ };
4103
+ }
4104
+ function writeShortcut(shortcut) {
4105
+ writeDefault(TOGGLE_RECORDING_DEFAULTS_KEY, JSON.stringify({ carbonKeyCode: shortcut.carbonKeyCode, carbonModifiers: shortcut.carbonModifiers }));
4106
+ }
4107
+ function writeUseFnKey(enabled) {
4108
+ if (TRIGGER_DEFAULTS_EXECUTABLE === null) {
4109
+ throw new Error(`cannot write ${USE_FN_KEY_DEFAULTS_KEY}: this machine has no readable app UserDefaults domain`);
4110
+ }
4111
+ const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, USE_FN_KEY_DEFAULTS_KEY, "-bool", enabled ? "true" : "false"], { encoding: "utf8" });
4112
+ if (result.status !== 0) {
4113
+ throw new Error(`defaults write ${USE_FN_KEY_DEFAULTS_KEY} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
4114
+ }
4115
+ }
4116
+
4117
+ // src/cli/trigger-probe.ts
4118
+ import { closeSync, openSync, readSync, statSync as statSync3 } from "fs";
4119
+ var FN_BLOCKING_ACCESSIBILITY_STATES = [
4120
+ "denied",
4121
+ "stale_allowed_for_previous_app_build"
4122
+ ];
4123
+ var APP_LOG_TAIL_BYTES = 1024 * 1024;
4124
+ var TRIGGER_LOG_MARKER = "trigger bindings:";
4125
+ function resolveHotkeyBinding(state) {
4126
+ const base = { defaults_key: TOGGLE_RECORDING_DEFAULTS_KEY };
4127
+ if (state.shortcut) {
4128
+ return {
4129
+ ...base,
4130
+ state: "bound",
4131
+ chord: formatShortcut(state.shortcut),
4132
+ shortcut: state.shortcut,
4133
+ stored_raw: state.rawShortcut,
4134
+ can_fire: "yes"
4135
+ };
4136
+ }
4137
+ if (state.rawShortcut === null) {
4138
+ return {
4139
+ ...base,
4140
+ state: "default_pending",
4141
+ chord: formatShortcut(parseShortcutChord(DEFAULT_TOGGLE_RECORDING_CHORD)),
4142
+ shortcut: null,
4143
+ stored_raw: null,
4144
+ can_fire: "yes"
4145
+ };
4146
+ }
4147
+ const normalized = state.rawShortcut.trim().toLowerCase();
4148
+ if (normalized === "0" || normalized === "false") {
4149
+ return {
4150
+ ...base,
4151
+ state: "cleared",
4152
+ chord: null,
4153
+ shortcut: null,
4154
+ stored_raw: state.rawShortcut,
4155
+ can_fire: "no"
4156
+ };
4157
+ }
4158
+ return {
4159
+ ...base,
4160
+ state: "unreadable",
4161
+ chord: null,
4162
+ shortcut: null,
4163
+ stored_raw: state.rawShortcut,
4164
+ can_fire: "unknown"
4165
+ };
4166
+ }
4167
+ function resolveFnTrigger(useFnKey, accessibilityPermission) {
4168
+ const base = {
4169
+ use_fn_key: useFnKey,
4170
+ accessibility_permission: accessibilityPermission,
4171
+ defaults_key: USE_FN_KEY_DEFAULTS_KEY
4172
+ };
4173
+ if (!useFnKey)
4174
+ return { ...base, can_fire: "no" };
4175
+ if (accessibilityPermission !== null) {
4176
+ if (FN_BLOCKING_ACCESSIBILITY_STATES.includes(accessibilityPermission)) {
4177
+ return { ...base, can_fire: "no" };
4178
+ }
4179
+ if (accessibilityPermission.startsWith("allowed"))
4180
+ return { ...base, can_fire: "yes" };
4181
+ }
4182
+ return { ...base, can_fire: "unknown" };
4183
+ }
4184
+ function parseTriggerBindingsLog(logText) {
4185
+ if (!logText)
4186
+ return null;
4187
+ const lines = logText.split(`
4188
+ `);
4189
+ for (let index = lines.length - 1;index >= 0; index -= 1) {
4190
+ const line = lines[index];
4191
+ if (line === undefined)
4192
+ continue;
4193
+ const markerAt = line.indexOf(TRIGGER_LOG_MARKER);
4194
+ if (markerAt === -1)
4195
+ continue;
4196
+ const body = line.slice(markerAt + TRIGGER_LOG_MARKER.length);
4197
+ const useFnKey = matchBoolean(body, "useFnKey");
4198
+ const fnMonitorRunning = matchBoolean(body, "fnMonitorRunning");
4199
+ if (useFnKey === null || fnMonitorRunning === null)
4200
+ continue;
4201
+ const blocked = matchField2(body, "blocked", null);
4202
+ return {
4203
+ observed_at: /^\[([^\]]+)\]/.exec(line)?.[1] ?? null,
4204
+ shortcut_stored: matchField2(body, "shortcutStored", "shortcutArmed"),
4205
+ use_fn_key: useFnKey,
4206
+ fn_monitor_running: fnMonitorRunning,
4207
+ microphone: matchField2(body, "microphone", "accessibility"),
4208
+ accessibility: matchField2(body, "accessibility", "blocked"),
4209
+ blocked: blocked === null || blocked === "none" ? null : blocked
4210
+ };
4211
+ }
4212
+ return null;
4213
+ }
4214
+ function matchField2(body, field, nextField) {
4215
+ const start = body.indexOf(`${field}=`);
4216
+ if (start === -1)
4217
+ return null;
4218
+ const valueAt = start + field.length + 1;
4219
+ if (nextField === null)
4220
+ return body.slice(valueAt).trim() || null;
4221
+ const end = body.indexOf(` ${nextField}=`, valueAt);
4222
+ if (end === -1)
4223
+ return body.slice(valueAt).trim() || null;
4224
+ return body.slice(valueAt, end).trim() || null;
4225
+ }
4226
+ function matchBoolean(body, field) {
4227
+ const match = new RegExp(`(?:^|\\s)${field}=(true|false)(?:\\s|$)`).exec(body);
4228
+ if (!match)
4229
+ return null;
4230
+ return match[1] === "true";
4231
+ }
4232
+ function readAppLogTail(logPath, maxBytes = APP_LOG_TAIL_BYTES) {
4233
+ if (!logPath)
4234
+ return null;
4235
+ let handle = null;
4236
+ try {
4237
+ const size = statSync3(logPath).size;
4238
+ if (size === 0)
4239
+ return null;
4240
+ const length = Math.min(size, maxBytes);
4241
+ const buffer = Buffer.allocUnsafe(length);
4242
+ handle = openSync(logPath, "r");
4243
+ const read = readSync(handle, buffer, 0, length, size - length);
4244
+ return buffer.subarray(0, read).toString("utf8");
4245
+ } catch {
4246
+ return null;
4247
+ } finally {
4248
+ if (handle !== null) {
4249
+ try {
4250
+ closeSync(handle);
4251
+ } catch {}
4252
+ }
4253
+ }
4254
+ }
4255
+ function diagnoseTrigger(input) {
4256
+ const hotkey = resolveHotkeyBinding(input.trigger);
4257
+ const fn = resolveFnTrigger(input.trigger.useFnKey, input.accessibilityPermission);
4258
+ const failures = [];
4259
+ const warnings = [];
4260
+ const notes = [];
4261
+ const canFire = hotkey.can_fire !== "no" || fn.can_fire !== "no";
4262
+ if (!canFire) {
4263
+ failures.push(`No recording trigger can fire: ${describeHotkey(hotkey)}, and ${describeFn(fn)}. ` + `Nothing will start a recording from the keyboard. Fix it with one of:
4264
+ ` + ` recordings shortcut --reset (bind the app's default hotkey again)
4265
+ ` + ` recordings shortcut --set "f13" (bind a hotkey of your choosing)
4266
+ ` + " recordings shortcut --fn on (use fn/Globe as push-to-talk)");
4267
+ }
4268
+ if (hotkey.state === "unreadable") {
4269
+ warnings.push(`The stored hotkey value is not a shortcut this CLI can read (${hotkey.stored_raw}). ` + "The app decodes it with the same JSON shape, so it most likely registers no hotkey " + "either \u2014 but that is not proved here, so it is not counted as a failure. Re-bind it " + "with 'recordings shortcut --reset' to be certain.");
4270
+ }
4271
+ if (hotkey.state === "default_pending") {
4272
+ notes.push(`No hotkey is stored, so the app registers its own default ${hotkey.chord} the next ` + "time it launches. Nothing is wrong; it is just not written down yet.");
4273
+ }
4274
+ if (hotkey.state === "cleared" && fn.can_fire !== "no") {
4275
+ notes.push("The hotkey was explicitly cleared (the app stores a disabled marker rather than " + "removing the key), so fn/Globe is the only trigger.");
4276
+ }
4277
+ if (fn.use_fn_key && fn.can_fire === "no") {
4278
+ warnings.push(`fn/Globe is enabled but its Accessibility grant is ${fn.accessibility_permission}, and ` + "the fn trigger is a CGEventTap that macOS will not create without it. Grant it in " + "System Settings > Privacy & Security > Accessibility, then quit and reopen the app.");
4279
+ }
4280
+ if (fn.use_fn_key && fn.can_fire === "unknown") {
4281
+ warnings.push(`fn/Globe is enabled and its Accessibility grant reads ${fn.accessibility_permission ?? "unmeasured"}` + " \u2014 that is not a denial and not a pass, so whether the fn tap can be created is " + "undecided here. 'recordings app permissions' reports the grant in full.");
4282
+ }
4283
+ const observation = input.observation;
4284
+ if (!observation) {
4285
+ notes.push("The app has not logged a resolved trigger, so whether its fn event tap is running is " + "unknown. The app writes that line at launch and on every trigger change; if it is " + "absent the app has not run since the log was last truncated.");
4286
+ } else {
4287
+ const when = observation.observed_at ? ` at ${observation.observed_at}` : "";
4288
+ notes.push(`The app last reported${when}: fn tap ${observation.fn_monitor_running ? "running" : "NOT running"}` + `, useFnKey=${observation.use_fn_key}, hotkey ${observation.shortcut_stored ?? "unreported"}.`);
4289
+ if (observation.use_fn_key !== input.trigger.useFnKey) {
4290
+ warnings.push(`The app is running with useFnKey=${observation.use_fn_key} while storage now says ` + `${input.trigger.useFnKey}${when ? ` (last reported${when})` : ""}. A running instance keeps ` + "the trigger it registered with, so the current setting is NOT armed \u2014 quit and " + "reopen the app.");
4291
+ } else if (hotkey.state === "bound" && observation.shortcut_stored !== null && observation.shortcut_stored !== `carbonKeyCode=${hotkey.shortcut.carbonKeyCode} carbonModifiers=${hotkey.shortcut.carbonModifiers}`) {
4292
+ warnings.push(`The app registered hotkey ${observation.shortcut_stored}${when} while storage now holds ` + `${hotkey.chord}. The running instance keeps what it registered with, so the stored ` + "hotkey is NOT armed \u2014 quit and reopen the app.");
4293
+ }
4294
+ if (observation.use_fn_key && !observation.fn_monitor_running) {
4295
+ warnings.push(`The app itself reported its fn event tap NOT running${when}. It retries while ` + "Accessibility is trusted, so this may since have recovered \u2014 it is reported, not " + "counted as a failure. 'recordings app log' shows what happened next.");
4296
+ }
4297
+ if (observation.blocked) {
4298
+ warnings.push(`The app reported its trigger blocked${when}: ${observation.blocked}`);
4299
+ }
4300
+ }
4301
+ return {
4302
+ can_fire: canFire,
4303
+ summary: `hotkey ${describeHotkey(hotkey)}, ${describeFn(fn)}`,
4304
+ defaults_domain: RECORDINGS_BUNDLE_ID,
4305
+ hotkey,
4306
+ fn,
4307
+ app_observation: observation,
4308
+ failures,
4309
+ warnings,
4310
+ notes
4311
+ };
4312
+ }
4313
+ function describeHotkey(hotkey) {
4314
+ switch (hotkey.state) {
4315
+ case "bound":
4316
+ return hotkey.chord;
4317
+ case "default_pending":
4318
+ return `${hotkey.chord} (the app's default, registered at next launch)`;
4319
+ case "cleared":
4320
+ return "cleared (nothing is registered)";
4321
+ case "unreadable":
4322
+ return `unreadable (${hotkey.stored_raw})`;
4323
+ }
4324
+ }
4325
+ function describeFn(fn) {
4326
+ if (!fn.use_fn_key)
4327
+ return "fn/Globe off";
4328
+ if (fn.can_fire === "no")
4329
+ return `fn/Globe on but Accessibility is ${fn.accessibility_permission}`;
4330
+ if (fn.can_fire === "unknown")
4331
+ return "fn/Globe on, Accessibility undecided";
4332
+ return "fn/Globe on";
4333
+ }
4334
+ function probeTriggerDiagnostics(input) {
4335
+ if (TRIGGER_DEFAULTS_EXECUTABLE === null)
4336
+ return null;
4337
+ return diagnoseTrigger({
4338
+ trigger: readTriggerState(),
4339
+ accessibilityPermission: input.accessibilityPermission,
4340
+ observation: parseTriggerBindingsLog(readAppLogTail(input.appLogPath))
4341
+ });
4342
+ }
4343
+ function describeTriggerPickup(runningBundlePaths) {
4344
+ return { armed: runningBundlePaths.length === 0, runningBundlePaths };
4345
+ }
4346
+
4347
+ // src/lib/machine.ts
4348
+ import { hostname } from "os";
4349
+ function currentMachineId(env = process.env, hostName = hostname()) {
4350
+ const configured = env.HASNA_MACHINE_ID?.trim();
4351
+ if (configured)
4352
+ return configured;
4353
+ return hostName.trim();
4354
+ }
4355
+
4356
+ // src/lib/bun-runtime.ts
4357
+ import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as statSync4 } from "fs";
4358
+ import { isAbsolute as isAbsolute2 } from "path";
4359
+ import { randomBytes } from "crypto";
4360
+ import { spawnSync as spawnSync5 } from "child_process";
4361
+ var BUN_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
4362
+ var INSTALLER_ENVIRONMENT_KEYS = [
4363
+ "HOME",
4364
+ "SSH_CONNECTION",
4365
+ "RECORDINGS_EXPECTED_TEAM_IDENTIFIER",
4366
+ "RECORDINGS_LAUNCH_TIMEOUT_SECONDS",
4367
+ "RECORDINGS_LOCK_STALE_SECONDS",
4368
+ "RECORDINGS_MAINTENANCE_STALE_SECONDS",
4369
+ "RECORDINGS_READER_DRAIN_TIMEOUT_MS",
4370
+ "RECORDINGS_SQLITE_BUSY_TIMEOUT_MS"
4371
+ ];
4372
+ var INSTALLER_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
4373
+ function validateBunExecutable(candidate) {
4374
+ if (!isAbsolute2(candidate))
4375
+ return { reason: "path is not absolute" };
4376
+ let executable;
4377
+ try {
4378
+ executable = realpathSync3(candidate);
4379
+ if (!statSync4(executable).isFile())
4380
+ return { reason: "resolved path is not a regular file" };
4381
+ accessSync(executable, fsConstants.X_OK);
4382
+ } catch {
4383
+ return { reason: "path is missing, inaccessible, or not executable" };
4384
+ }
4385
+ const nonce = randomBytes(24).toString("hex");
4386
+ const probe = spawnSync5(executable, [
4387
+ "-e",
4388
+ `
4389
+ import { realpathSync, statSync } from "node:fs";
4390
+ const expected = process.argv[1];
4391
+ const nonce = process.argv[2];
4392
+ const actual = process.execPath;
4393
+ if (!expected || !nonce || realpathSync(actual) !== realpathSync(expected)) process.exit(66);
4394
+ if (!statSync(actual).isFile()) process.exit(66);
4395
+ if (!/^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/.test(Bun.version)) {
4396
+ process.exit(66);
4397
+ }
4398
+ process.stdout.write(nonce + ":" + Bun.version);
4399
+ `,
4400
+ executable,
4401
+ nonce
4402
+ ], {
4403
+ encoding: "utf8",
4404
+ env: {
4405
+ HOME: "/tmp",
4406
+ PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
4407
+ TMPDIR: "/tmp"
4408
+ },
3045
4409
  maxBuffer: 1024,
3046
4410
  stdio: ["ignore", "pipe", "ignore"],
3047
4411
  timeout: 5000
@@ -3086,17 +4450,17 @@ function createInstallerEnvironment(environment, bunExecutable) {
3086
4450
  import { createHash as createHash2 } from "crypto";
3087
4451
  import {
3088
4452
  chmodSync as chmodSync2,
3089
- closeSync,
4453
+ closeSync as closeSync2,
3090
4454
  constants,
3091
4455
  fstatSync,
3092
4456
  fsyncSync,
3093
- mkdtempSync,
3094
- openSync,
3095
- readFileSync as readFileSync2,
3096
- rmSync as rmSync2,
3097
- writeFileSync as writeFileSync2
4457
+ mkdtempSync as mkdtempSync2,
4458
+ openSync as openSync2,
4459
+ readFileSync as readFileSync3,
4460
+ rmSync as rmSync4,
4461
+ writeFileSync as writeFileSync3
3098
4462
  } from "fs";
3099
- import { isAbsolute as isAbsolute3, join as join6 } from "path";
4463
+ import { isAbsolute as isAbsolute3, join as join7 } from "path";
3100
4464
  var LOWER_SHA256 = /^[a-f0-9]{64}$/;
3101
4465
  var LOWER_SOURCE_SHA = /^[a-f0-9]{40}$/;
3102
4466
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
@@ -3169,7 +4533,7 @@ function prepareReleaseInstallInputs(input) {
3169
4533
  if (!isAbsolute3(snapshotRoot)) {
3170
4534
  throw new Error("release snapshot root must be absolute");
3171
4535
  }
3172
- const snapshotDirectory = mkdtempSync(join6(snapshotRoot, "recordings-release-install."));
4536
+ const snapshotDirectory = mkdtempSync2(join7(snapshotRoot, "recordings-release-install."));
3173
4537
  let cleaned = false;
3174
4538
  const cleanup = () => {
3175
4539
  if (cleaned)
@@ -3178,12 +4542,12 @@ function prepareReleaseInstallInputs(input) {
3178
4542
  try {
3179
4543
  chmodSync2(snapshotDirectory, 448);
3180
4544
  } catch {}
3181
- rmSync2(snapshotDirectory, { recursive: true, force: true });
4545
+ rmSync4(snapshotDirectory, { recursive: true, force: true });
3182
4546
  };
3183
4547
  try {
3184
- const manifestSnapshot = join6(snapshotDirectory, `${input.manifestSha256}.manifest.json`);
4548
+ const manifestSnapshot = join7(snapshotDirectory, `${input.manifestSha256}.manifest.json`);
3185
4549
  const envelopeDigest = createHash2("sha256").update(envelopeBytes).digest("hex");
3186
- const envelopeSnapshot = join6(snapshotDirectory, `${envelopeDigest}.envelope.json`);
4550
+ const envelopeSnapshot = join7(snapshotDirectory, `${envelopeDigest}.envelope.json`);
3187
4551
  writeSnapshot(manifestSnapshot, manifestBytes);
3188
4552
  writeSnapshot(envelopeSnapshot, envelopeBytes);
3189
4553
  chmodSync2(snapshotDirectory, 320);
@@ -3198,15 +4562,15 @@ function assertAbsolutePath(value, label) {
3198
4562
  throw new Error(`${label} path must be absolute`);
3199
4563
  }
3200
4564
  function readBoundedRegularFile(path, label, maximum) {
3201
- const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
4565
+ const descriptor = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
3202
4566
  try {
3203
4567
  const metadata = fstatSync(descriptor);
3204
4568
  if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximum) {
3205
4569
  throw new Error(`${label} must be a non-empty bounded regular file`);
3206
4570
  }
3207
- return readFileSync2(descriptor);
4571
+ return readFileSync3(descriptor);
3208
4572
  } finally {
3209
- closeSync(descriptor);
4573
+ closeSync2(descriptor);
3210
4574
  }
3211
4575
  }
3212
4576
  function parseObject(bytes, label) {
@@ -3285,12 +4649,73 @@ function isObject(value) {
3285
4649
  return typeof value === "object" && value !== null && !Array.isArray(value);
3286
4650
  }
3287
4651
  function writeSnapshot(path, bytes) {
3288
- writeFileSync2(path, bytes, { flag: "wx", mode: 256 });
3289
- const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
4652
+ writeFileSync3(path, bytes, { flag: "wx", mode: 256 });
4653
+ const descriptor = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
3290
4654
  try {
3291
4655
  fsyncSync(descriptor);
3292
4656
  } finally {
3293
- closeSync(descriptor);
4657
+ closeSync2(descriptor);
4658
+ }
4659
+ }
4660
+
4661
+ // src/cli/desktop-snapshot.ts
4662
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync3, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync5 } from "fs";
4663
+ import { dirname as dirname4, join as join8, resolve as resolve3 } from "path";
4664
+ import { spawnSync as spawnSync6 } from "child_process";
4665
+ var SCREEN_CAPTURE_EXECUTABLE = "/usr/sbin/screencapture";
4666
+ var DEFAULT_DESKTOP_SNAPSHOT = "desktop-snapshot.png";
4667
+ function runScreenCapture(executable, arguments_) {
4668
+ const result = spawnSync6(executable, arguments_, {
4669
+ encoding: "utf8",
4670
+ stdio: ["ignore", "ignore", "pipe"]
4671
+ });
4672
+ return {
4673
+ error: result.error,
4674
+ status: result.status,
4675
+ stderr: result.stderr
4676
+ };
4677
+ }
4678
+ function exportDesktopSnapshot(output = DEFAULT_DESKTOP_SNAPSHOT, dependencies = {}) {
4679
+ if ((dependencies.platform ?? process.platform) !== "darwin") {
4680
+ throw new Error("Desktop snapshots are only supported on macOS");
4681
+ }
4682
+ const destination = resolve3(dependencies.cwd ?? process.cwd(), output);
4683
+ const destinationDirectory = dirname4(destination);
4684
+ mkdirSync4(destinationDirectory, { recursive: true });
4685
+ const stagingDirectory = mkdtempSync3(join8(destinationDirectory, ".recordings-desktop-"));
4686
+ chmodSync3(stagingDirectory, 448);
4687
+ const stagingPath = join8(stagingDirectory, "snapshot.png");
4688
+ try {
4689
+ const capture = dependencies.capture ?? runScreenCapture;
4690
+ const result = capture(SCREEN_CAPTURE_EXECUTABLE, [
4691
+ "-x",
4692
+ "-m",
4693
+ "-C",
4694
+ "-t",
4695
+ "png",
4696
+ stagingPath
4697
+ ]);
4698
+ if (result.error) {
4699
+ throw new Error(`Could not run macOS screencapture: ${result.error.message}`);
4700
+ }
4701
+ if (result.status !== 0) {
4702
+ const detail = result.stderr?.trim();
4703
+ throw new Error(detail ? `Desktop snapshot failed: ${detail}` : "Desktop snapshot failed; allow Screen Recording access for the invoking terminal");
4704
+ }
4705
+ let size = 0;
4706
+ try {
4707
+ const snapshot = statSync5(stagingPath);
4708
+ if (snapshot.isFile())
4709
+ size = snapshot.size;
4710
+ } catch {}
4711
+ if (size === 0) {
4712
+ throw new Error("Desktop snapshot produced no image; allow Screen Recording access for the invoking terminal");
4713
+ }
4714
+ chmodSync3(stagingPath, 384);
4715
+ renameSync2(stagingPath, destination);
4716
+ return destination;
4717
+ } finally {
4718
+ rmSync5(stagingDirectory, { recursive: true, force: true });
3294
4719
  }
3295
4720
  }
3296
4721
 
@@ -3330,13 +4755,13 @@ program.command("record").description("Record from microphone, transcribe, and o
3330
4755
  console.log(chalk.blue("Recording... Press") + chalk.yellow(" Enter ") + chalk.blue("to stop."));
3331
4756
  }
3332
4757
  audioPath = startRecording(config);
3333
- await new Promise((resolve3) => {
4758
+ await new Promise((resolve4) => {
3334
4759
  process.stdin.setRawMode?.(true);
3335
4760
  process.stdin.resume();
3336
4761
  process.stdin.once("data", () => {
3337
4762
  process.stdin.setRawMode?.(false);
3338
4763
  process.stdin.pause();
3339
- resolve3();
4764
+ resolve4();
3340
4765
  });
3341
4766
  });
3342
4767
  stopRecording();
@@ -3525,7 +4950,7 @@ ${text}`;
3525
4950
  process.exit(1);
3526
4951
  }
3527
4952
  });
3528
- program.command("save <text>").description("Save raw text as a recording (no audio). Routes to the self_hosted API when configured, else local.").option("-t, --tags <tags>", "Comma-separated tags").option("--enhance", "Enhance the text via the configured model before saving").option("--model <model>", "Value for model_used", "direct-input").action(async (rawText, opts) => {
4953
+ program.command("save <text>").description("Save raw text as a recording (no audio). Routes to the /v1 API when configured, else the on-box SQLite store.").option("-t, --tags <tags>", "Comma-separated tags").option("--enhance", "Enhance the text via the configured model before saving").option("--model <model>", "Value for model_used", "direct-input").action(async (rawText, opts) => {
3529
4954
  const parentOpts = program.opts();
3530
4955
  let processedText;
3531
4956
  let mode = "raw";
@@ -3727,13 +5152,13 @@ program.command("projects").description("List registered projects").option("-n,
3727
5152
  printPaginationHints(page.length, projects.length, pagination);
3728
5153
  });
3729
5154
  program.command("init").description("Initialize .recordings/ in current directory").action(() => {
3730
- const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync3, existsSync: existsSync5 } = __require("fs");
3731
- const { join: join7 } = __require("path");
3732
- const dir = join7(process.cwd(), ".recordings");
3733
- const audioDir = join7(dir, "audio");
3734
- const configFile = join7(dir, "config.json");
3735
- mkdirSync4(audioDir, { recursive: true });
3736
- if (!existsSync5(configFile)) {
5155
+ const { mkdirSync: mkdirSync5, writeFileSync: writeFileSync4, existsSync: existsSync9 } = __require("fs");
5156
+ const { join: join9 } = __require("path");
5157
+ const dir = join9(process.cwd(), ".recordings");
5158
+ const audioDir = join9(dir, "audio");
5159
+ const configFile = join9(dir, "config.json");
5160
+ mkdirSync5(audioDir, { recursive: true });
5161
+ if (!existsSync9(configFile)) {
3737
5162
  const defaultConf = {
3738
5163
  transcription_model: "gpt-4o-transcribe",
3739
5164
  realtime_session_model: "gpt-realtime",
@@ -3746,7 +5171,7 @@ program.command("init").description("Initialize .recordings/ in current director
3746
5171
  post_processing_mode: "auto",
3747
5172
  auto_enhance: true
3748
5173
  };
3749
- writeFileSync3(configFile, JSON.stringify(defaultConf, null, 2));
5174
+ writeFileSync4(configFile, JSON.stringify(defaultConf, null, 2));
3750
5175
  }
3751
5176
  console.log(chalk.green("Initialized .recordings/ directory"));
3752
5177
  console.log(chalk.dim(" config: .recordings/config.json"));
@@ -3754,7 +5179,7 @@ program.command("init").description("Initialize .recordings/ in current director
3754
5179
  console.log(chalk.dim(" db: .recordings/recordings.db"));
3755
5180
  });
3756
5181
  var appCommand = program.command("app").description("Manage the macOS app installed from this package");
3757
- appCommand.command("install").description("Install a release or explicitly approved local-only Recordings.app artifact").requiredOption("--artifact <path>", "Finalized Recordings.app ZIP artifact").requiredOption("--manifest <path>", "Artifact provenance manifest").option("--envelope <path>", "Signed release envelope (required for release artifacts)").option("--expected-team-id <team>", "Required Developer ID TeamIdentifier for release artifacts").requiredOption("--manifest-sha256 <sha256>", "Authenticated release-manifest SHA-256").requiredOption("--expected-source-sha <sha>", "Exact approved 40-character source commit").requiredOption("--expected-version <version>", "Exact approved release version").option("--expected-hostname <hostname>", "Exact deployment hostname to verify before any install mutation").option("--artifact-policy <policy>", "Artifact policy: release or local-only", "release").option("--approved-target <station>", "Exact approved target; fleet for release artifacts", "fleet").option("--approved-target-identity-kind <kind>", "Target identity kind: hardware_uuid_sha256 or tailscale_node_id_sha256").option("--approved-target-identity-sha256 <sha256>", "Authenticated SHA-256 of the approved target identity; none for release artifacts", "none").option("--acknowledge-local-signing-and-permissions", "Acknowledge local-only ad-hoc identity and possible permission reauthorization").option("--expected-old-identity-sha256 <sha256>", "Exact installed identity approved for migration").option("--expected-new-identity-sha256 <sha256>", "Exact candidate identity approved for migration").option("--allow-signing-identity-migration", "Allow one reviewed signer change that requires new macOS permission approval").option("--launch", "Launch and verify the canonical app after installation").option("--launch-timeout <seconds>", "Canonical process launch timeout").action((opts) => {
5182
+ appCommand.command("install").description("Install a release or explicitly approved local-only Recordings.app artifact").requiredOption("--artifact <path>", "Finalized Recordings.app ZIP artifact").requiredOption("--manifest <path>", "Artifact provenance manifest").option("--envelope <path>", "Signed release envelope (required for release artifacts)").option("--expected-team-id <team>", "Required Developer ID TeamIdentifier for release artifacts").requiredOption("--manifest-sha256 <sha256>", "Authenticated release-manifest SHA-256").requiredOption("--expected-source-sha <sha>", "Exact approved 40-character source commit").requiredOption("--expected-version <version>", "Exact approved release version").option("--expected-hostname <hostname>", "Exact deployment hostname to verify before any install mutation").option("--artifact-policy <policy>", "Artifact policy: release or local-only", "release").option("--approved-target <station>", "Exact approved target; fleet for release artifacts", "fleet").option("--approved-target-identity-kind <kind>", "Target identity kind: hardware_uuid_sha256 or tailscale_node_id_sha256").option("--approved-target-identity-sha256 <sha256>", "Authenticated SHA-256 of the approved target identity; none for release artifacts", "none").option("--acknowledge-local-signing-and-permissions", "Acknowledge local-only ad-hoc identity and possible permission reauthorization").option("--expected-old-identity-sha256 <sha256>", "Exact installed identity approved for migration").option("--expected-new-identity-sha256 <sha256>", "Exact candidate identity approved for migration").option("--allow-signing-identity-migration", "Allow one reviewed signer change that requires new macOS permission approval").option("--allow-adhoc-identity-migration", "Accept that replacing an ad-hoc signed local-only app voids its Microphone and Accessibility grants").option("--launch", "Launch and verify the canonical app after installation").option("--launch-timeout <seconds>", "Canonical process launch timeout").action((opts) => {
3758
5183
  if (process.platform !== "darwin") {
3759
5184
  console.error(chalk.red("Recordings.app installation is only supported on macOS"));
3760
5185
  process.exit(1);
@@ -3768,7 +5193,7 @@ appCommand.command("install").description("Install a release or explicitly appro
3768
5193
  try {
3769
5194
  assertReleaseOnlyOptions(opts);
3770
5195
  if (opts.expectedHostname) {
3771
- const hostnameResult = spawnSync4("/bin/hostname", ["-s"], {
5196
+ const hostnameResult = spawnSync7("/bin/hostname", ["-s"], {
3772
5197
  encoding: "utf8",
3773
5198
  stdio: ["ignore", "pipe", "ignore"],
3774
5199
  env: {
@@ -3797,14 +5222,14 @@ appCommand.command("install").description("Install a release or explicitly appro
3797
5222
  process.exit(1);
3798
5223
  }
3799
5224
  const updateClientPath = "/Applications/Recordings.app/Contents/Helpers/recordings-update-client";
3800
- if (!existsSync4(updateClientPath)) {
5225
+ if (!existsSync8(updateClientPath)) {
3801
5226
  preparedInputs.cleanup();
3802
5227
  console.error(chalk.red("Root-owned Recordings update broker client is not installed."));
3803
5228
  process.exit(1);
3804
5229
  }
3805
5230
  const result2 = (() => {
3806
5231
  try {
3807
- return spawnSync4(updateClientPath, [
5232
+ return spawnSync7(updateClientPath, [
3808
5233
  "install",
3809
5234
  "--artifact",
3810
5235
  opts.artifact,
@@ -3851,7 +5276,7 @@ appCommand.command("install").description("Install a release or explicitly appro
3851
5276
  process.exit(1);
3852
5277
  }
3853
5278
  const installerPath = getMacOSInstallerPath();
3854
- if (!existsSync4(installerPath)) {
5279
+ if (!existsSync8(installerPath)) {
3855
5280
  console.error(chalk.red(`App installer missing from package: ${installerPath}`));
3856
5281
  process.exit(1);
3857
5282
  }
@@ -3890,6 +5315,9 @@ appCommand.command("install").description("Install a release or explicitly appro
3890
5315
  if (opts.allowSigningIdentityMigration) {
3891
5316
  installerArgs.push("--allow-signing-identity-migration");
3892
5317
  }
5318
+ if (opts.allowAdhocIdentityMigration) {
5319
+ installerArgs.push("--allow-adhoc-identity-migration");
5320
+ }
3893
5321
  if (opts.expectedOldIdentitySha256) {
3894
5322
  installerArgs.push("--expected-old-identity-sha256", opts.expectedOldIdentitySha256);
3895
5323
  }
@@ -3899,7 +5327,7 @@ appCommand.command("install").description("Install a release or explicitly appro
3899
5327
  if (opts.launch)
3900
5328
  installerArgs.push("--launch");
3901
5329
  const installerEnvironment = createInstallerEnvironment(process.env, bunExecutable);
3902
- const result = spawnSync4("/bin/bash", installerArgs, {
5330
+ const result = spawnSync7("/bin/bash", installerArgs, {
3903
5331
  stdio: "inherit",
3904
5332
  env: installerEnvironment
3905
5333
  });
@@ -3911,8 +5339,12 @@ appCommand.command("install").description("Install a release or explicitly appro
3911
5339
  });
3912
5340
  appCommand.command("status").description("Show installed Recordings.app status").option("--verbose", "Show package paths, code hash, and log path").action((opts) => {
3913
5341
  const status = getMacOSAppStatus();
5342
+ const trigger = probeTriggerDiagnostics({
5343
+ accessibilityPermission: process.platform === "darwin" ? status.accessibility_permission : null,
5344
+ appLogPath: status.log_path
5345
+ });
3914
5346
  if (program.opts().json) {
3915
- console.log(JSON.stringify(status, null, 2));
5347
+ console.log(JSON.stringify({ ...status, trigger }, null, 2));
3916
5348
  return;
3917
5349
  }
3918
5350
  console.log(chalk.bold("Recordings.app"));
@@ -3925,6 +5357,10 @@ appCommand.command("status").description("Show installed Recordings.app status")
3925
5357
  console.log(`Microphone: ${status.microphone_permission}`);
3926
5358
  console.log(`Accessibility: ${status.accessibility_permission}`);
3927
5359
  }
5360
+ if (trigger) {
5361
+ console.log(`Trigger: ${trigger.summary}${trigger.can_fire ? "" : " \u2014 NOTHING CAN FIRE"}`);
5362
+ console.log(chalk.dim(" 'recordings check' reports the trigger in full and exits non-zero when nothing can fire."));
5363
+ }
3928
5364
  if (opts.verbose) {
3929
5365
  console.log(`Package: ${status.package_root}`);
3930
5366
  console.log(`Installed app: ${status.installed ? status.installed_app_path : "missing"}`);
@@ -3945,9 +5381,11 @@ appCommand.command("permissions").description("Show macOS permission state for R
3945
5381
  const status = getMacOSAppStatus();
3946
5382
  const permissions = {
3947
5383
  platform: status.platform,
3948
- bundle_id: "com.hasna.recordings",
5384
+ bundle_id: RECORDINGS_BUNDLE_IDENTIFIER,
3949
5385
  installed_app_path: status.installed_app_path,
5386
+ installed: status.installed,
3950
5387
  legacy_install_paths: status.legacy_install_paths,
5388
+ permission_subject: describeTccAuthorizationSubject(status.installed_app_path),
3951
5389
  microphone: status.microphone_permission,
3952
5390
  accessibility: status.accessibility_permission,
3953
5391
  app_code_hash: status.app_code_hash,
@@ -3955,14 +5393,24 @@ appCommand.command("permissions").description("Show macOS permission state for R
3955
5393
  signing_identifier: status.signing_identifier,
3956
5394
  team_identifier: status.team_identifier,
3957
5395
  designated_requirement: status.designated_requirement,
5396
+ microphone_grant_durability: status.microphone_grant_durability,
5397
+ accessibility_grant_durability: status.accessibility_grant_durability,
5398
+ microphone_stored_requirement: status.microphone_stored_requirement,
5399
+ accessibility_stored_requirement: status.accessibility_stored_requirement,
5400
+ ambiguous_installations: status.ambiguous_installations,
5401
+ warnings: buildPermissionWarnings(status),
3958
5402
  log_path: status.log_path
3959
5403
  };
3960
5404
  if (program.opts().json) {
3961
5405
  console.log(JSON.stringify(permissions, null, 2));
3962
5406
  return;
3963
5407
  }
3964
- console.log(`Microphone: ${permissions.microphone}`);
3965
- console.log(`Accessibility: ${permissions.accessibility}`);
5408
+ console.log(`Subject: ${permissions.permission_subject}`);
5409
+ console.log(`Microphone: ${permissions.microphone} (${permissions.microphone_grant_durability})`);
5410
+ console.log(`Accessibility: ${permissions.accessibility} (${permissions.accessibility_grant_durability})`);
5411
+ for (const warning of permissions.warnings) {
5412
+ console.log(chalk.yellow(`Warning: ${warning}`));
5413
+ }
3966
5414
  console.log(`Log: ${permissions.log_path}`);
3967
5415
  });
3968
5416
  appCommand.command("reset-permissions").description("Reset macOS Microphone and Accessibility permissions for Recordings.app").action(() => {
@@ -3993,12 +5441,12 @@ appCommand.command("request-permissions").description("Open Recordings.app and t
3993
5441
  });
3994
5442
  appCommand.command("log").description("Show the Recordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print", String(DEFAULT_LOG_LINES)).action((opts) => {
3995
5443
  const status = getMacOSAppStatus();
3996
- if (!existsSync4(status.log_path)) {
5444
+ if (!existsSync8(status.log_path)) {
3997
5445
  console.log("");
3998
5446
  return;
3999
5447
  }
4000
5448
  const lines = Math.max(1, parseInt(opts.lines, 10) || DEFAULT_LOG_LINES);
4001
- const result = spawnSync4("tail", ["-n", String(lines), status.log_path], {
5449
+ const result = spawnSync7("tail", ["-n", String(lines), status.log_path], {
4002
5450
  encoding: "utf8"
4003
5451
  });
4004
5452
  if (result.error) {
@@ -4009,6 +5457,14 @@ appCommand.command("log").description("Show the Recordings.app diagnostic log").
4009
5457
  if (result.stderr)
4010
5458
  process.stderr.write(result.stderr);
4011
5459
  });
5460
+ appCommand.command("snapshot [output]").description("Write the current main desktop to a PNG for local debugging").action((output) => {
5461
+ const path = exportDesktopSnapshot(output);
5462
+ if (program.opts().json) {
5463
+ console.log(JSON.stringify({ path }, null, 2));
5464
+ } else {
5465
+ console.log(chalk.green(`Desktop snapshot written: ${path}`));
5466
+ }
5467
+ });
4012
5468
  appCommand.command("open").description("Open the installed Recordings.app").action(() => {
4013
5469
  const status = getMacOSAppStatus();
4014
5470
  if (process.platform !== "darwin") {
@@ -4019,18 +5475,60 @@ appCommand.command("open").description("Open the installed Recordings.app").acti
4019
5475
  console.error(chalk.red("Recordings.app is not installed. Run: recordings app install"));
4020
5476
  process.exit(1);
4021
5477
  }
4022
- const result = spawnSync4("open", [status.installed_app_path], { stdio: "inherit" });
5478
+ const result = spawnSync7("open", [status.installed_app_path], { stdio: "inherit" });
4023
5479
  if (result.error) {
4024
5480
  console.error(chalk.red(result.error.message));
4025
5481
  process.exit(1);
4026
5482
  }
4027
5483
  process.exit(result.status ?? 1);
4028
5484
  });
4029
- program.command("check").description("Check system dependencies (sox, API keys)").action(async () => {
5485
+ program.command("check").description("Check system dependencies (sox, API keys) and macOS capture permissions. " + "Use --probe to exercise the microphone and credential for real.").option("--probe", "Prove capability instead of reporting presence: record a short sample and assert signal, and make one authenticated API request").option("--probe-seconds <seconds>", `Length of the capture probe in seconds (1-${MAX_PROBE_SECONDS})`, String(DEFAULT_PROBE_SECONDS)).option("--probe-store-write", "Allow the persistence probe to write a marker recording to a SHARED API store. Off by default: that store is production for every machine pointed at it").action(async (opts) => {
4030
5486
  const config = loadConfig();
4031
5487
  const parentOpts = program.opts();
4032
5488
  const deps = await checkRecordingDeps();
4033
5489
  const enhKey = config.enhancement_api_key || config.openai_api_key;
5490
+ const transcriberModel = resolveTranscriberModel(config);
5491
+ const transcriptionModel = config.transcription_model;
5492
+ const macStatus = process.platform === "darwin" ? getMacOSAppStatus() : null;
5493
+ const trigger = probeTriggerDiagnostics({
5494
+ accessibilityPermission: macStatus?.accessibility_permission ?? null,
5495
+ appLogPath: macStatus?.log_path ?? null
5496
+ });
5497
+ const activeStore = describeActiveStore(config);
5498
+ const localStoreWasLegacy = localStoreIsBehindSchema(config.db_path);
5499
+ let capture = null;
5500
+ let credential = null;
5501
+ let enhancementCredential = null;
5502
+ let persistence = null;
5503
+ if (opts.probe) {
5504
+ const parsedSeconds = Number.parseInt(String(opts.probeSeconds), 10);
5505
+ const seconds = Number.isFinite(parsedSeconds) && parsedSeconds > 0 ? Math.min(parsedSeconds, MAX_PROBE_SECONDS) : DEFAULT_PROBE_SECONDS;
5506
+ capture = deps.available ? probeMicrophoneCapture(config, { seconds }) : {
5507
+ ok: false,
5508
+ tool: null,
5509
+ seconds,
5510
+ samples: 0,
5511
+ peak: 0,
5512
+ silent: null,
5513
+ message: deps.message
5514
+ };
5515
+ credential = await verifyTranscriptionCredential(config, transcriptionModel, {
5516
+ apiKey: config.openai_api_key,
5517
+ role: "transcription"
5518
+ });
5519
+ enhancementCredential = enhKey ? await verifyTranscriptionCredential(config, transcriberModel, {
5520
+ apiKey: enhKey,
5521
+ role: "enhancement"
5522
+ }) : null;
5523
+ persistence = await probeRecordingPersistence({
5524
+ allowRemoteWrite: Boolean(opts.probeStoreWrite),
5525
+ allowLocalMigration: Boolean(opts.probeStoreWrite),
5526
+ localStoreExistedBefore: activeStore.local_db_present,
5527
+ localStoreIsLegacy: localStoreWasLegacy
5528
+ });
5529
+ }
5530
+ const probeFailed = Boolean(opts.probe && (capture && !capture.ok || credential && !credential.ok || enhancementCredential && !enhancementCredential.ok || persistence && persistence.outcome === "failed"));
5531
+ const triggerFailed = trigger !== null && !trigger.can_fire;
4034
5532
  if (parentOpts.json) {
4035
5533
  console.log(JSON.stringify({
4036
5534
  recording: {
@@ -4041,14 +5539,30 @@ program.command("check").description("Check system dependencies (sox, API keys)"
4041
5539
  openai_api_key_configured: Boolean(config.openai_api_key),
4042
5540
  enhancement_api_key_configured: Boolean(enhKey),
4043
5541
  enhancement_model: config.enhancement_model,
4044
- transcriber_model: resolveTranscriberModel(config),
5542
+ transcriber_model: transcriberModel,
4045
5543
  realtime_session_model: config.realtime_session_model,
4046
5544
  realtime_transcription_model: config.realtime_transcription_model,
4047
5545
  post_processing_mode: config.post_processing_mode,
4048
5546
  transcription_prompt_configured: Boolean(config.transcription_prompt?.trim()),
4049
5547
  transcriber_prompt_configured: Boolean(config.transcriber_prompt?.trim()),
4050
- config_warnings: config.config_warnings ?? []
5548
+ config_warnings: config.config_warnings ?? [],
5549
+ microphone_permission: macStatus?.microphone_permission ?? "unsupported",
5550
+ accessibility_permission: macStatus?.accessibility_permission ?? "unsupported",
5551
+ active_store: activeStore,
5552
+ capture_probe: capture,
5553
+ capture_probe_subject: captureProbeSubject(),
5554
+ microphone_grant_instruction: macStatus ? microphoneGrantInstruction({
5555
+ installedAppPath: macStatus.installed_app_path,
5556
+ otherAppPaths: macStatus.legacy_install_paths,
5557
+ requestState: classifyPermissionState(macStatus.microphone_permission)
5558
+ }) : null,
5559
+ credential_probe: credential,
5560
+ enhancement_credential_probe: enhancementCredential,
5561
+ persistence_probe: persistence,
5562
+ trigger
4051
5563
  }, null, 2));
5564
+ if (probeFailed || triggerFailed)
5565
+ process.exitCode = 1;
4052
5566
  return;
4053
5567
  }
4054
5568
  if (deps.available) {
@@ -4057,16 +5571,84 @@ program.command("check").description("Check system dependencies (sox, API keys)"
4057
5571
  console.log(chalk.red(`\u2717 ${deps.message}`));
4058
5572
  }
4059
5573
  if (config.openai_api_key) {
4060
- console.log(chalk.green(`\u2713 OpenAI API key configured`));
5574
+ console.log(chalk.green(`\u2713 OpenAI API key present`) + chalk.dim(opts.probe ? "" : " (presence only \u2014 run 'recordings check --probe' to verify it is accepted)"));
4061
5575
  } else {
4062
5576
  console.log(chalk.red(`\u2717 OpenAI API key not found. Set OPENAI_API_KEY env var or add to ~/.secrets`));
4063
5577
  }
4064
5578
  if (enhKey) {
4065
- console.log(chalk.green(`\u2713 Enhancement API key configured (model: ${resolveTranscriberModel(config)})`));
5579
+ console.log(chalk.green(`\u2713 Enhancement API key present (model: ${transcriberModel})`) + chalk.dim(opts.probe ? "" : " (presence only \u2014 'recordings check --probe' verifies it is accepted)"));
4066
5580
  } else {
4067
5581
  console.log(chalk.yellow(`\u26A0 Enhancement API key not configured \u2014 enhancement disabled`));
4068
5582
  }
5583
+ console.log(chalk.green("\u2713") + ` Active store: ${activeStore.transport}` + (activeStore.base_url ? ` \u2192 ${activeStore.base_url}` : ` \u2192 ${activeStore.local_db_path}`) + chalk.dim(` (selected by ${activeStore.mode_source})`));
5584
+ if (activeStore.divergent) {
5585
+ console.log(chalk.yellow("\u26A0") + ` Two datasets present: ${activeStore.local_db_recordings} recordings sit in ` + `${activeStore.local_db_path}, which is NOT the live store.`);
5586
+ }
5587
+ if (activeStore.warning) {
5588
+ console.log(chalk.dim(` ${activeStore.warning}`));
5589
+ }
5590
+ if (macStatus) {
5591
+ const micState = macStatus.microphone_permission;
5592
+ const micOk = micState.startsWith("allowed");
5593
+ const micVerified = micState === "allowed";
5594
+ const micStaleGrant = micState === "stale_allowed_for_previous_app_build";
5595
+ const micUnreadable = micState === TCC_UNREADABLE_STATE;
5596
+ console.log((micOk ? chalk.green("\u2713") : micUnreadable ? chalk.yellow("?") : chalk.red("\u2717")) + ` Microphone permission: ${micState}`);
5597
+ if (micOk) {
5598
+ console.log(chalk.dim(micVerified ? ` Note: this is the TCC row for bundle id ${RECORDINGS_BUNDLE_IDENTIFIER}, and its ` + "stored code-signing requirement still validates against the installed bundle \u2014 " + "so the grant binds to the app that is installed, not to a previous build." : ` Note: this is the TCC row for bundle id ${RECORDINGS_BUNDLE_IDENTIFIER}. The row's ` + "code-signing requirement was NOT verified against the installed bundle, so a " + "bundle re-signed with a different identity can still be denied at runtime. " + "Confirm in the app's log."));
5599
+ }
5600
+ if (micStaleGrant) {
5601
+ console.log(chalk.dim(" The TCC row says allowed, but its stored code-signing requirement does NOT match " + "the installed bundle \u2014 the grant belongs to a previous build and macOS will deny " + "at runtime. Toggling it in System Settings will not fix it; the row must be reset " + "(`recordings app reset-permissions`) and re-granted so it binds to the bundle " + "that is installed now."));
5602
+ }
5603
+ if (micUnreadable) {
5604
+ console.log(chalk.dim(" Could not read the TCC database. This is NOT a denial and NOT proof the app never " + "asked. Reading it needs Full Disk Access, which is held by the session's " + "RESPONSIBLE process and inherited by its children \u2014 not granted per-tool: on " + "a fleet Mac `bun` is explicitly denied and still reads the database over SSH, " + "because it inherits sshd's grant. So re-run from a plain ssh shell rather than " + "granting Full Disk Access to tmux or bun, and not under sudo, which changes the " + "responsible process. A missing sqlite3, a locked database or a corrupt file " + "produce this same state, so the cause is not established either."));
5605
+ }
5606
+ if (!micOk && !micUnreadable) {
5607
+ console.log(chalk.dim(" Recordings.app cannot capture audio without this. macOS does not error when it is " + "missing \u2014 it delivers silent audio. This grant CANNOT be set remotely; it needs a " + "human at the keyboard:"));
5608
+ const instruction = microphoneGrantInstruction({
5609
+ installedAppPath: macStatus.installed_app_path,
5610
+ otherAppPaths: macStatus.legacy_install_paths,
5611
+ requestState: classifyPermissionState(macStatus.microphone_permission)
5612
+ });
5613
+ for (const [index, step] of instruction.steps.entries()) {
5614
+ console.log(chalk.dim(` ${index + 1}. ${step}`));
5615
+ }
5616
+ }
5617
+ console.log((macStatus.accessibility_permission.startsWith("allowed") ? chalk.green("\u2713") : chalk.yellow("\u26A0")) + ` Accessibility permission: ${macStatus.accessibility_permission}`);
5618
+ }
5619
+ if (trigger)
5620
+ reportTriggerDiagnosis(trigger);
5621
+ if (capture) {
5622
+ console.log((capture.ok ? chalk.green("\u2713") : chalk.red("\u2717")) + ` Microphone capture probe: ${capture.message}`);
5623
+ const subject = captureProbeSubject();
5624
+ console.log(chalk.dim(` subject: ${subject.note}`) + (subject.subject_known ? "" : chalk.yellow(" [subject unidentified \u2014 inconclusive]")));
5625
+ }
5626
+ if (credential) {
5627
+ console.log((credential.ok ? chalk.green("\u2713") : chalk.red("\u2717")) + ` Transcription credential (${credential.model}): ${credential.message}`);
5628
+ }
5629
+ if (enhancementCredential) {
5630
+ console.log((enhancementCredential.ok ? chalk.green("\u2713") : chalk.red("\u2717")) + ` Enhancement credential (${enhancementCredential.model}): ${enhancementCredential.message}`);
5631
+ }
5632
+ if (persistence) {
5633
+ const persistenceMarker = renderPersistenceMarker(persistence.outcome, {
5634
+ pass: chalk.green,
5635
+ warn: chalk.yellow,
5636
+ fail: chalk.red
5637
+ });
5638
+ console.log(persistenceMarker + ` Persistence round-trip: ${persistence.message}`);
5639
+ }
5640
+ if (probeFailed || triggerFailed)
5641
+ process.exitCode = 1;
4069
5642
  });
5643
+ function reportTriggerDiagnosis(trigger) {
5644
+ console.log((trigger.can_fire ? chalk.green("\u2713") : chalk.red("\u2717")) + ` Recording trigger: ${trigger.summary}`);
5645
+ for (const failure of trigger.failures)
5646
+ console.log(chalk.red(` ${failure}`));
5647
+ for (const warning of trigger.warnings)
5648
+ console.log(chalk.yellow(` \u26A0 ${warning}`));
5649
+ for (const note of trigger.notes)
5650
+ console.log(chalk.dim(` ${note}`));
5651
+ }
4070
5652
  program.command("listen").description("Push-to-talk mode \u2014 press Space to start/stop recording, Esc to quit").option("-t, --tags <tags>", "Comma-separated tags for all recordings").option("--no-enhance", "Skip AI enhancement").option("--post-processing <mode>", "Post-processing mode: off, auto, or always").option("--prompt <prompt>", "Vocabulary/context prompt for transcription").option("--transcriber-prompt <prompt>", "Instructions for post-transcription cleanup").option("--system-prompt <prompt>", "Alias for --transcriber-prompt").option("--transcriber-model <model>", "Model for post-transcription cleanup").option("-l, --language <lang>", "Language code").option("--copy", "Copy output to clipboard").option("--paste", "Copy output to clipboard AND paste into frontmost app").action(async (opts) => {
4071
5653
  const config = loadConfig();
4072
5654
  ensureDataDir(config);
@@ -4181,19 +5763,212 @@ Bye.`));
4181
5763
  }
4182
5764
  });
4183
5765
  });
4184
- program.command("shortcut").description("Set up a global keyboard shortcut for recording (macOS)").option("--raycast", "Generate Raycast script command").option("--karabiner", "Set up Fn key via Karabiner-Elements").option("--skhd", "Generate skhd hotkey config").option("--hammerspoon", "Generate Hammerspoon config").option("--script", "Just output the shell script path").action((opts) => {
4185
- const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4, chmodSync: chmodSync3 } = __require("fs");
5766
+ program.command("shortcut").description("Show or change the global recording trigger (macOS). Exits non-zero when a change was " + "written while Recordings.app was running, because the running instance keeps the " + "trigger it registered with \u2014 the write is stored but not armed until it is reopened.").option("--set <chord>", 'Set the app hotkey, e.g. "f13" or "ctrl+opt+r"').option("--reset", "Reset the app hotkey to the app's built-in default").option("--fn <state>", "Use fn/Globe as push-to-talk: on|off").option("--keys", "List the key names accepted by --set").option("--script", "Write the app-less toggle script and print its path").option("--raycast", "Generate a Raycast script command (requires Raycast)").option("--karabiner", "Generate a Karabiner-Elements rule (requires Karabiner-Elements)").option("--skhd", "Print an skhd hotkey config (requires skhd)").option("--hammerspoon", "Print a Hammerspoon config (requires Hammerspoon)").action((opts) => {
5767
+ const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync5, chmodSync: chmodSync4, existsSync: existsSync9 } = __require("fs");
4186
5768
  const { join: pathJoin2 } = __require("path");
4187
5769
  const { homedir: getHome } = __require("os");
5770
+ const { spawnSync: runSync } = __require("child_process");
4188
5771
  const home = getHome();
4189
5772
  const scriptDir = pathJoin2(home, ".hasna", "recordings");
4190
- mkdirSync4(scriptDir, { recursive: true });
4191
5773
  const scriptPath = pathJoin2(scriptDir, "record-toggle.sh");
4192
5774
  const pidFile = pathJoin2(scriptDir, ".recording.pid");
4193
5775
  const recordingsBin = pathJoin2(home, ".bun", "bin", "recordings");
4194
- const script = `#!/bin/bash
5776
+ const audioDir = pathJoin2(scriptDir, "audio");
5777
+ if (opts.keys) {
5778
+ console.log(chalk.bold(`Keys accepted by --set (combine with cmd/ctrl/opt/shift):
5779
+ `));
5780
+ console.log(listBindableKeys().join(" "));
5781
+ console.log(chalk.dim(`
5782
+ fn/Globe is not in this list \u2014 it is a separate trigger, not a chord:
5783
+ `) + " recordings shortcut --fn on");
5784
+ return;
5785
+ }
5786
+ const appDefaultsReachable = TRIGGER_DEFAULTS_EXECUTABLE !== null;
5787
+ function requireMacOS(what) {
5788
+ if (appDefaultsReachable)
5789
+ return true;
5790
+ console.error(chalk.red(`${what} is only available on macOS`));
5791
+ process.exitCode = 1;
5792
+ return false;
5793
+ }
5794
+ let runningBundlesCache = null;
5795
+ function runningBundles() {
5796
+ runningBundlesCache ??= runningAppBundlePaths();
5797
+ return runningBundlesCache;
5798
+ }
5799
+ function grantTargetPaths() {
5800
+ const running = runningBundles();
5801
+ if (running.length > 0)
5802
+ return { paths: running, running: true };
5803
+ const status = getMacOSAppStatus();
5804
+ return {
5805
+ paths: [...status.installed ? [status.installed_app_path] : [], ...status.legacy_install_paths],
5806
+ running: false
5807
+ };
5808
+ }
5809
+ function showState() {
5810
+ const state = readTriggerState();
5811
+ console.log(chalk.bold(`Recording trigger
5812
+ `));
5813
+ if (state.shortcut) {
5814
+ console.log(` Hotkey ${chalk.cyan(formatShortcut(state.shortcut))}`);
5815
+ } else if (state.rawShortcut) {
5816
+ console.log(` Hotkey ${chalk.yellow(`unreadable (${state.rawShortcut})`)}`);
5817
+ } else {
5818
+ console.log(` Hotkey ${chalk.dim("not set \u2014 the app writes its default")} ` + `${chalk.cyan(DEFAULT_TOGGLE_RECORDING_CHORD.toUpperCase())} ${chalk.dim("on next launch")}`);
5819
+ }
5820
+ console.log(` fn/Globe key ${state.useFnKey ? chalk.cyan("on") : chalk.dim("off")}`);
5821
+ console.log();
5822
+ function showGrantTargets() {
5823
+ const running = runningBundles();
5824
+ const status = getMacOSAppStatus();
5825
+ const installed = [
5826
+ ...status.installed ? [status.installed_app_path] : [],
5827
+ ...status.legacy_install_paths
5828
+ ];
5829
+ console.log(chalk.bold(`Which bundle the grants apply to
5830
+ `));
5831
+ if (running.length === 1) {
5832
+ const [runningBundle] = running;
5833
+ if (runningBundle === undefined)
5834
+ throw new Error("Running app bundle path is missing");
5835
+ console.log(` Running ${chalk.cyan(runningBundle)}`);
5836
+ } else if (running.length > 1) {
5837
+ console.log(` Running ${chalk.red(`${running.length} instances \u2014 grants are ambiguous`)}`);
5838
+ for (const path of running)
5839
+ console.log(` ${path}`);
5840
+ } else {
5841
+ console.log(` Running ${chalk.yellow("not running")}`);
5842
+ }
5843
+ if (installed.length > 0) {
5844
+ console.log(` Installed ${installed.join(`
5845
+ `)}`);
5846
+ }
5847
+ if (installed.length > 1) {
5848
+ console.log(chalk.yellow(" More than one installed copy \u2014 a grant given to one does not cover the others."));
5849
+ }
5850
+ console.log(chalk.dim(`
5851
+ This command reports the app's stored trigger, not its permission state:
5852
+ ` + ` a CLI inherits the terminal's grants, so it cannot measure the app's.
5853
+ ` + " Use 'recordings app permissions' for the bundle's actual grants."));
5854
+ console.log();
5855
+ }
5856
+ showGrantTargets();
5857
+ console.log(chalk.bold(`Permissions each trigger needs
5858
+ `));
5859
+ for (const requirement of TRIGGER_GRANT_REQUIREMENTS) {
5860
+ const grant = requirement.tccService ? chalk.yellow(requirement.tccService.replace(/^kTCCService/, "")) : chalk.green("none");
5861
+ console.log(` ${requirement.label.padEnd(15)} ${grant} \u2014 ${requirement.mechanism}`);
5862
+ if (requirement.settingsPath) {
5863
+ console.log(` ${" ".repeat(15)} ${requirement.settingsPath} > enable Recordings`);
5864
+ }
5865
+ }
5866
+ console.log();
5867
+ console.log(chalk.bold(`Change it
5868
+ `));
5869
+ console.log(` recordings shortcut --set f13`);
5870
+ console.log(` recordings shortcut --set "ctrl+opt+r"`);
5871
+ console.log(` recordings shortcut --fn on`);
5872
+ console.log(` recordings shortcut --reset`);
5873
+ console.log(chalk.dim(`
5874
+ Or in the app: Settings > Recording Shortcut.`));
5875
+ console.log(chalk.dim(" A changed hotkey is picked up the next time the app launches."));
5876
+ }
5877
+ function reportPickup() {
5878
+ const pickup = describeTriggerPickup(runningBundles());
5879
+ if (pickup.armed) {
5880
+ console.log(chalk.dim(" Recordings.app is not running; it will register this on next launch."));
5881
+ return;
5882
+ }
5883
+ console.log(chalk.yellow(" Recordings.app is running and still holds the previous trigger.") + `
5884
+ Quit and reopen it to arm this one:`);
5885
+ for (const path of pickup.runningBundlePaths)
5886
+ console.log(chalk.dim(` ${path}`));
5887
+ console.log(chalk.dim(" Exiting non-zero: the value was written, but the trigger that fires right now is " + "still the old one."));
5888
+ process.exitCode = 1;
5889
+ }
5890
+ if (opts.set !== undefined) {
5891
+ if (!requireMacOS("Setting the app hotkey"))
5892
+ return;
5893
+ let shortcut;
5894
+ try {
5895
+ shortcut = parseShortcutChord(String(opts.set));
5896
+ } catch (error) {
5897
+ if (error instanceof ShortcutParseError) {
5898
+ console.error(chalk.red(error.message));
5899
+ process.exitCode = 1;
5900
+ return;
5901
+ }
5902
+ throw error;
5903
+ }
5904
+ writeShortcut(shortcut);
5905
+ console.log(chalk.green(`Hotkey set to ${formatShortcut(shortcut)}`));
5906
+ console.log(chalk.dim(` ${RECORDINGS_BUNDLE_IDENTIFIER} ${TOGGLE_RECORDING_DEFAULTS_KEY}`));
5907
+ reportPickup();
5908
+ return;
5909
+ }
5910
+ if (opts.reset) {
5911
+ if (!requireMacOS("Resetting the app hotkey"))
5912
+ return;
5913
+ const shortcut = parseShortcutChord(DEFAULT_TOGGLE_RECORDING_CHORD);
5914
+ writeShortcut(shortcut);
5915
+ console.log(chalk.green(`Hotkey reset to ${formatShortcut(shortcut)}`));
5916
+ console.log(chalk.dim(` ${RECORDINGS_BUNDLE_IDENTIFIER} ${TOGGLE_RECORDING_DEFAULTS_KEY}`));
5917
+ reportPickup();
5918
+ return;
5919
+ }
5920
+ if (opts.fn !== undefined) {
5921
+ if (!requireMacOS("Changing the fn/Globe trigger"))
5922
+ return;
5923
+ const raw = String(opts.fn).toLowerCase();
5924
+ const enable = raw === "on" || raw === "true" || raw === "yes" || raw === "1";
5925
+ const disable = raw === "off" || raw === "false" || raw === "no" || raw === "0";
5926
+ if (!enable && !disable) {
5927
+ console.error(chalk.red(`--fn expects on or off, got "${opts.fn}"`));
5928
+ process.exitCode = 1;
5929
+ return;
5930
+ }
5931
+ writeUseFnKey(enable);
5932
+ console.log(chalk.green(`fn/Globe trigger ${enable ? "enabled" : "disabled"}`));
5933
+ console.log(chalk.dim(` ${RECORDINGS_BUNDLE_IDENTIFIER} ${USE_FN_KEY_DEFAULTS_KEY}`));
5934
+ if (enable) {
5935
+ const fnGrant = TRIGGER_GRANT_REQUIREMENTS.find((entry) => entry.id === "fn");
5936
+ if (fnGrant?.settingsPath) {
5937
+ console.log(`
5938
+ fn is a ${fnGrant.mechanism}, so it needs ` + `${fnGrant.tccService?.replace(/^kTCCService/, "") ?? "no"} permission:
5939
+ ` + ` ${fnGrant.settingsPath} > enable Recordings`);
5940
+ const target = grantTargetPaths();
5941
+ if (target.paths.length === 0) {
5942
+ console.log(chalk.dim(" (no installed Recordings.app found to grant it to)"));
5943
+ }
5944
+ for (const path of target.paths) {
5945
+ console.log(chalk.dim(` grant it to: ${path}${target.running ? "" : " (not running \u2014 installed copy)"}`));
5946
+ }
5947
+ }
5948
+ console.log(chalk.dim(`
5949
+ While enabled the app swallows fn, so fn stops reaching other apps
5950
+ ` + " (emoji picker, input-source switching). Turn it off with --fn off."));
5951
+ }
5952
+ console.log();
5953
+ reportPickup();
5954
+ return;
5955
+ }
5956
+ const wantsExternalLauncher = Boolean(opts.script) || Boolean(opts.raycast) || Boolean(opts.karabiner) || Boolean(opts.skhd) || Boolean(opts.hammerspoon);
5957
+ if (!wantsExternalLauncher) {
5958
+ if (!requireMacOS("Reading the app hotkey"))
5959
+ return;
5960
+ showState();
5961
+ return;
5962
+ }
5963
+ function writeToggleScript() {
5964
+ mkdirSync5(scriptDir, { recursive: true });
5965
+ const script = `#!/bin/bash
4195
5966
  # Toggle recording on/off. Run this from a global hotkey.
4196
5967
  # Each press toggles: start recording -> stop + transcribe + copy to clipboard
5968
+ #
5969
+ # This is the app-less path. If Recordings.app is running, prefer its own hotkey
5970
+ # ("recordings shortcut --set ..."): the app streams transcription and pastes into
5971
+ # the focused field, which this script cannot do.
4197
5972
  set -e
4198
5973
 
4199
5974
  PID_FILE="${pidFile}"
@@ -4206,7 +5981,7 @@ if [ -f "$PID_FILE" ]; then
4206
5981
  rm -f "$PID_FILE"
4207
5982
 
4208
5983
  # Find the most recent audio file
4209
- AUDIO_DIR="${pathJoin2(scriptDir, "audio")}"
5984
+ AUDIO_DIR="${audioDir}"
4210
5985
  LATEST=$(ls -t "$AUDIO_DIR"/*.wav 2>/dev/null | head -1)
4211
5986
 
4212
5987
  if [ -n "$LATEST" ]; then
@@ -4218,8 +5993,6 @@ if [ -f "$PID_FILE" ]; then
4218
5993
  fi
4219
5994
  if [ -n "$TEXT" ]; then
4220
5995
  echo -n "$TEXT" | pbcopy
4221
- # Optional: paste into frontmost app
4222
- # osascript -e 'delay 0.1' -e 'tell application "System Events" to keystroke "v" using command down'
4223
5996
  fi
4224
5997
  fi
4225
5998
 
@@ -4227,19 +6000,39 @@ if [ -f "$PID_FILE" ]; then
4227
6000
  osascript -e 'display notification "Recording saved and copied to clipboard" with title "Recordings"' 2>/dev/null || true
4228
6001
  else
4229
6002
  # Start recording in background
4230
- mkdir -p "${pathJoin2(scriptDir, "audio")}"
4231
- rec -r 16000 -c 1 -b 16 "${pathJoin2(scriptDir, "audio")}/recording-$(date +%Y%m%dT%H%M%S).wav" trim 0 300 &
6003
+ mkdir -p "${audioDir}"
6004
+ rec -r 16000 -c 1 -b 16 "${audioDir}/recording-$(date +%Y%m%dT%H%M%S).wav" trim 0 300 &
4232
6005
  echo $! > "$PID_FILE"
4233
6006
 
4234
6007
  # Notification
4235
6008
  osascript -e 'display notification "Recording started..." with title "Recordings"' 2>/dev/null || true
4236
6009
  fi
4237
6010
  `;
4238
- writeFileSync3(scriptPath, script, "utf-8");
4239
- chmodSync3(scriptPath, 493);
6011
+ writeFileSync4(scriptPath, script, "utf-8");
6012
+ chmodSync4(scriptPath, 493);
6013
+ }
6014
+ function launcherMissing(label, present, install) {
6015
+ if (present)
6016
+ return false;
6017
+ console.error(chalk.red(`${label} is not installed \u2014 nothing would fire this hotkey.`));
6018
+ console.error(chalk.dim(` Install it with: ${install}`));
6019
+ console.error(chalk.dim(` Or use the app's own hotkey, which needs no extra software:
6020
+ ` + " recordings shortcut --set f13"));
6021
+ process.exitCode = 1;
6022
+ return true;
6023
+ }
4240
6024
  if (opts.karabiner) {
6025
+ if (!requireMacOS("Karabiner-Elements setup"))
6026
+ return;
6027
+ console.log(chalk.yellow("The app can use fn/Globe natively \u2014 no driver, no reboot:") + `
6028
+ recordings shortcut --fn on
6029
+ `);
6030
+ if (launcherMissing("Karabiner-Elements", existsSync9("/Applications/Karabiner-Elements.app"), "brew install --cask karabiner-elements")) {
6031
+ return;
6032
+ }
4241
6033
  const karabinerDir = pathJoin2(home, ".config", "karabiner", "assets", "complex_modifications");
4242
- mkdirSync4(karabinerDir, { recursive: true });
6034
+ mkdirSync5(karabinerDir, { recursive: true });
6035
+ writeToggleScript();
4243
6036
  const rule = {
4244
6037
  title: "Recordings \u2014 Fn key to toggle recording",
4245
6038
  rules: [
@@ -4263,23 +6056,27 @@ fi
4263
6056
  ]
4264
6057
  };
4265
6058
  const karabinerPath = pathJoin2(karabinerDir, "recordings-fn.json");
4266
- writeFileSync3(karabinerPath, JSON.stringify(rule, null, 2) + `
6059
+ writeFileSync4(karabinerPath, JSON.stringify(rule, null, 2) + `
4267
6060
  `, "utf-8");
4268
- console.log(chalk.green("Karabiner-Elements rule created!"));
6061
+ console.log(chalk.green("Karabiner-Elements rule written:"));
4269
6062
  console.log(chalk.dim(` ${karabinerPath}
4270
6063
  `));
4271
- console.log("To activate:");
6064
+ console.log("It does nothing until you enable it:");
4272
6065
  console.log(" 1. Open Karabiner-Elements");
4273
6066
  console.log(" 2. Go to Complex Modifications tab");
4274
6067
  console.log(" 3. Click Add Predefined Rule");
4275
6068
  console.log(' 4. Enable "Fn key toggles speech recording"');
4276
- console.log(chalk.dim(`
4277
- Press Fn to start recording, Fn again to stop + copy to clipboard`));
4278
6069
  return;
4279
6070
  }
4280
6071
  if (opts.raycast) {
6072
+ if (!requireMacOS("Raycast setup"))
6073
+ return;
6074
+ if (launcherMissing("Raycast", existsSync9("/Applications/Raycast.app"), "brew install --cask raycast")) {
6075
+ return;
6076
+ }
6077
+ writeToggleScript();
4281
6078
  const raycastDir = pathJoin2(home, ".config", "raycast", "script-commands");
4282
- mkdirSync4(raycastDir, { recursive: true });
6079
+ mkdirSync5(raycastDir, { recursive: true });
4283
6080
  const raycastScript = `#!/bin/bash
4284
6081
 
4285
6082
  # Required parameters:
@@ -4294,15 +6091,20 @@ fi
4294
6091
  ${scriptPath}
4295
6092
  `;
4296
6093
  const raycastPath = pathJoin2(raycastDir, "toggle-recording.sh");
4297
- writeFileSync3(raycastPath, raycastScript, "utf-8");
4298
- chmodSync3(raycastPath, 493);
4299
- console.log(chalk.green("Raycast script command created!"));
6094
+ writeFileSync4(raycastPath, raycastScript, "utf-8");
6095
+ chmodSync4(raycastPath, 493);
6096
+ console.log(chalk.green("Raycast script command written:"));
4300
6097
  console.log(chalk.dim(` ${raycastPath}`));
4301
- console.log(chalk.dim(" Open Raycast > Script Commands > reload to see it"));
4302
- console.log(chalk.dim(" Then assign a hotkey in Raycast preferences"));
6098
+ console.log(chalk.dim(" Raycast > Script Commands > reload, then assign a hotkey."));
4303
6099
  return;
4304
6100
  }
4305
6101
  if (opts.skhd) {
6102
+ if (!requireMacOS("skhd setup"))
6103
+ return;
6104
+ const skhdPresent = runSync("/usr/bin/env", ["which", "skhd"], { encoding: "utf8" }).status === 0;
6105
+ if (launcherMissing("skhd", skhdPresent, "brew install koekeishiya/formulae/skhd"))
6106
+ return;
6107
+ writeToggleScript();
4306
6108
  console.log(chalk.bold(`Add to ~/.skhdrc:
4307
6109
  `));
4308
6110
  console.log(chalk.cyan(` fn - space : ${scriptPath}`));
@@ -4311,6 +6113,12 @@ ${scriptPath}
4311
6113
  return;
4312
6114
  }
4313
6115
  if (opts.hammerspoon) {
6116
+ if (!requireMacOS("Hammerspoon setup"))
6117
+ return;
6118
+ if (launcherMissing("Hammerspoon", existsSync9("/Applications/Hammerspoon.app"), "brew install --cask hammerspoon")) {
6119
+ return;
6120
+ }
6121
+ writeToggleScript();
4314
6122
  console.log(chalk.bold(`Add to ~/.hammerspoon/init.lua:
4315
6123
  `));
4316
6124
  console.log(chalk.cyan(` hs.hotkey.bind({"ctrl"}, "space", function()
@@ -4320,35 +6128,8 @@ ${scriptPath}
4320
6128
  Then reload Hammerspoon config`));
4321
6129
  return;
4322
6130
  }
4323
- console.log(chalk.bold("Global shortcut script created:"));
4324
- console.log(chalk.cyan(` ${scriptPath}
4325
- `));
4326
- console.log(`Bind it to a hotkey using any of these:
4327
- `);
4328
- console.log(chalk.bold(" Karabiner-Elements") + chalk.dim(" (for Fn key specifically)"));
4329
- console.log(` brew install --cask karabiner-elements`);
4330
- console.log(` recordings shortcut --karabiner
4331
- `);
4332
- console.log(chalk.bold(" Raycast"));
4333
- console.log(` recordings shortcut --raycast
4334
- `);
4335
- console.log(chalk.bold(" skhd"));
4336
- console.log(` recordings shortcut --skhd
4337
- `);
4338
- console.log(chalk.bold(" Hammerspoon"));
4339
- console.log(` recordings shortcut --hammerspoon
4340
- `);
4341
- console.log(chalk.bold(" macOS Automator"));
4342
- console.log(` 1. Open Automator > Quick Action`);
4343
- console.log(` 2. Add "Run Shell Script" action`);
4344
- console.log(` 3. Paste: ${scriptPath}`);
4345
- console.log(` 4. Save as "Toggle Recording"`);
4346
- console.log(` 5. System Settings > Keyboard > Shortcuts > Services`);
4347
- console.log(` 6. Assign a shortcut to "Toggle Recording"
4348
- `);
4349
- console.log(chalk.bold(" Alfred"));
4350
- console.log(` Create a workflow with a Hotkey trigger \u2192 Run Script: ${scriptPath}
4351
- `);
6131
+ writeToggleScript();
6132
+ console.log(scriptPath);
4352
6133
  });
4353
6134
  function buildTranscriptionMetadata(config, processed, sources = {}) {
4354
6135
  const transcriptionPromptConfigured = Boolean(config.transcription_prompt?.trim());
@@ -4382,7 +6163,7 @@ async function readSaveTextInput(text, opts) {
4382
6163
  }
4383
6164
  let rawText;
4384
6165
  if (opts.textFile !== undefined) {
4385
- rawText = readFileSync3(opts.textFile, "utf8");
6166
+ rawText = readFileSync4(opts.textFile, "utf8");
4386
6167
  } else if (opts.stdin) {
4387
6168
  rawText = await Bun.stdin.text();
4388
6169
  } else {
@@ -4596,7 +6377,7 @@ function relativeHint(value) {
4596
6377
  return `${Math.floor(hours / 24)}d ago`;
4597
6378
  }
4598
6379
  program.command("mcp").description("Install recordings MCP server into Claude Code, Codex, or Gemini").option("--claude", "Install into Claude Code (via `claude mcp add`)").option("--codex", "Install into Codex (~/.codex/config.toml)").option("--gemini", "Install into Gemini (~/.gemini/settings.json)").option("--all", "Install into all supported agents").option("--uninstall", "Remove recordings MCP from config").action(async (opts) => {
4599
- const { readFileSync: readFileSync4, writeFileSync: writeFileSync3, existsSync: fileExists } = __require("fs");
6380
+ const { readFileSync: readFileSync5, writeFileSync: writeFileSync4, existsSync: fileExists } = __require("fs");
4600
6381
  const { join: pathJoin2 } = __require("path");
4601
6382
  const { homedir: getHome } = __require("os");
4602
6383
  const { execSync } = __require("child_process");
@@ -4629,14 +6410,14 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
4629
6410
  if (target === "codex") {
4630
6411
  const configPath = pathJoin2(home, ".codex", "config.toml");
4631
6412
  if (fileExists(configPath)) {
4632
- const content = readFileSync4(configPath, "utf-8");
6413
+ const content = readFileSync5(configPath, "utf-8");
4633
6414
  if (opts.uninstall) {
4634
6415
  const { content: next, removed } = removeCodexServerBlock(content, "recordings");
4635
- writeFileSync3(configPath, next, "utf-8");
6416
+ writeFileSync4(configPath, next, "utf-8");
4636
6417
  console.log(removed ? chalk.green(`Removed from Codex: ${configPath}`) : chalk.yellow(`Codex: no recordings MCP block found in ${configPath}`));
4637
6418
  } else {
4638
6419
  const next = upsertCodexStdioBlock(content, "recordings", mcpCmd);
4639
- writeFileSync3(configPath, next, "utf-8");
6420
+ writeFileSync4(configPath, next, "utf-8");
4640
6421
  console.log(chalk.green(`Installed into Codex: ${configPath}`));
4641
6422
  }
4642
6423
  } else {
@@ -4647,7 +6428,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
4647
6428
  const configPath = pathJoin2(home, ".gemini", "settings.json");
4648
6429
  let config = {};
4649
6430
  if (fileExists(configPath)) {
4650
- config = JSON.parse(readFileSync4(configPath, "utf-8"));
6431
+ config = JSON.parse(readFileSync5(configPath, "utf-8"));
4651
6432
  }
4652
6433
  const servers = config["mcpServers"] || {};
4653
6434
  if (opts.uninstall) {
@@ -4656,7 +6437,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
4656
6437
  servers["recordings"] = { command: mcpCmd, args: ["--stdio"] };
4657
6438
  }
4658
6439
  config["mcpServers"] = servers;
4659
- writeFileSync3(configPath, JSON.stringify(config, null, 2) + `
6440
+ writeFileSync4(configPath, JSON.stringify(config, null, 2) + `
4660
6441
  `, "utf-8");
4661
6442
  console.log(chalk.green(`${action} Gemini: ${configPath}`));
4662
6443
  }
@@ -4686,58 +6467,105 @@ program.command("feedback <message>").description("Send feedback").option("--ema
4686
6467
  function getMacOSAppStatus() {
4687
6468
  const packageRoot = findPackageRoot();
4688
6469
  const home = process.env.HOME || process.env.USERPROFILE || "";
4689
- const installedAppPath = pathJoin(home, "Applications", "Recordings.app");
6470
+ const canonicalAppPath = pathJoin(home, "Applications", "Recordings.app");
6471
+ const legacyInstallPaths = findLegacyMacOSAppPaths(home, canonicalAppPath);
6472
+ const installedAppPath = resolveInstalledAppPath(home, canonicalAppPath, legacyInstallPaths);
6473
+ const installedAppPathForGrants = existsSync8(installedAppPath) ? installedAppPath : null;
4690
6474
  const executablePath = pathJoin(installedAppPath, "Contents", "MacOS", "Recordings");
4691
6475
  const logPath = pathJoin(home, ".hasna", "recordings", "Recordings.log");
4692
6476
  const installerPath = getMacOSInstallerPath(packageRoot);
4693
6477
  const nativeSourcesPath = pathJoin(packageRoot, "src", "native", "Recordings");
4694
6478
  const signingInfo = getCodeSigningInfo(installedAppPath);
4695
- const legacyInstallPaths = findLegacyMacOSAppPaths(home, installedAppPath);
4696
- const permissionStatus = legacyInstallPaths.length > 0 ? "ambiguous_multiple_installations" : null;
6479
+ const ambiguousInstallations = [canonicalAppPath, ...legacyInstallPaths].filter((path) => existsSync8(path)).length > 1;
6480
+ const microphoneGrant = getTccGrant("kTCCServiceMicrophone", home, installedAppPathForGrants);
6481
+ const accessibilityGrant = getTccGrant("kTCCServiceAccessibility", home, installedAppPathForGrants);
4697
6482
  return {
4698
6483
  platform: process.platform,
4699
6484
  package_root: packageRoot,
4700
6485
  installer_path: installerPath,
4701
- installer_available: existsSync4(installerPath),
6486
+ installer_available: existsSync8(installerPath),
4702
6487
  native_sources_path: nativeSourcesPath,
4703
- native_sources_available: existsSync4(pathJoin(nativeSourcesPath, "Package.swift")),
6488
+ native_sources_available: existsSync8(pathJoin(nativeSourcesPath, "Package.swift")),
4704
6489
  installed_app_path: installedAppPath,
4705
6490
  legacy_install_paths: legacyInstallPaths,
4706
- installed: existsSync4(installedAppPath),
6491
+ installed: existsSync8(installedAppPath),
4707
6492
  executable_path: executablePath,
4708
- executable: existsSync4(executablePath),
6493
+ executable: existsSync8(executablePath),
4709
6494
  app_code_hash: signingInfo.cdHash,
4710
6495
  ad_hoc_signed: signingInfo.adHoc,
4711
6496
  signing_identifier: signingInfo.identifier,
4712
6497
  team_identifier: signingInfo.teamIdentifier,
4713
6498
  designated_requirement: signingInfo.designatedRequirement,
4714
6499
  signature_authorities: signingInfo.authorities,
4715
- microphone_permission: permissionStatus ?? getTccPermission("kTCCServiceMicrophone", home),
4716
- accessibility_permission: permissionStatus ?? getTccPermission("kTCCServiceAccessibility", home),
6500
+ microphone_permission: microphoneGrant.state,
6501
+ accessibility_permission: accessibilityGrant.state,
6502
+ ambiguous_installations: ambiguousInstallations,
6503
+ microphone_grant_durability: microphoneGrant.durability,
6504
+ accessibility_grant_durability: accessibilityGrant.durability,
6505
+ microphone_stored_requirement: microphoneGrant.storedRequirement,
6506
+ accessibility_stored_requirement: accessibilityGrant.storedRequirement,
4717
6507
  log_path: logPath
4718
6508
  };
4719
6509
  }
6510
+ function buildPermissionWarnings(status) {
6511
+ const warnings = [];
6512
+ if (status.platform !== "darwin")
6513
+ return warnings;
6514
+ if (!status.installed) {
6515
+ warnings.push(`no app bundle exists at ${status.installed_app_path}, so the states above describe no ` + "installed code \u2014 install the app before trusting them");
6516
+ }
6517
+ if (status.ambiguous_installations) {
6518
+ warnings.push("more than one Recordings.app is installed, so the states above may describe a bundle " + `other than the one macOS granted: reporting on ${status.installed_app_path}, also ` + `present are ${status.legacy_install_paths.join(", ")}`);
6519
+ }
6520
+ for (const [service, durability] of [
6521
+ ["Microphone", status.microphone_grant_durability],
6522
+ ["Accessibility", status.accessibility_grant_durability]
6523
+ ]) {
6524
+ if (durability === "dies_on_rebuild_cdhash_pinned") {
6525
+ warnings.push(`the ${service} grant is pinned to one exact build, so the next rebuild will silently ` + "revoke it \u2014 re-sign with a stable certificate identity to keep it");
6526
+ }
6527
+ }
6528
+ for (const [service, state] of [
6529
+ ["Microphone", status.microphone_permission],
6530
+ ["Accessibility", status.accessibility_permission]
6531
+ ]) {
6532
+ if (state === "undetermined_tcc_database_unreadable") {
6533
+ warnings.push(`the TCC database holding the ${service} decision could not be read, so that state is ` + "unknown rather than ungranted \u2014 the usual cause is that this process lacks Full " + "Disk Access");
6534
+ }
6535
+ }
6536
+ return warnings;
6537
+ }
6538
+ function resolveInstalledAppPath(home, canonicalPath, legacyPaths) {
6539
+ const preference = [
6540
+ canonicalPath,
6541
+ pathJoin("/", "Applications", "Recordings.app"),
6542
+ pathJoin(home, ".hasna", "recordings", "Recordings.app")
6543
+ ];
6544
+ for (const candidate of preference) {
6545
+ if (existsSync8(candidate))
6546
+ return candidate;
6547
+ }
6548
+ return legacyPaths.find((candidate) => existsSync8(candidate)) ?? canonicalPath;
6549
+ }
4720
6550
  function findLegacyMacOSAppPaths(home, canonicalPath) {
4721
6551
  const candidates = [
4722
6552
  pathJoin(home, ".hasna", "recordings", "Recordings.app"),
4723
6553
  pathJoin("/", "Applications", "Recordings.app")
4724
6554
  ];
4725
6555
  const userApplications = pathJoin(home, "Applications");
4726
- if (existsSync4(userApplications)) {
6556
+ if (existsSync8(userApplications)) {
4727
6557
  for (const entry of readdirSync2(userApplications, { withFileTypes: true })) {
4728
6558
  if (entry.isDirectory() && entry.name.startsWith("Recordings.app.")) {
4729
6559
  candidates.push(pathJoin(userApplications, entry.name));
4730
6560
  }
4731
6561
  }
4732
6562
  }
4733
- return [...new Set(candidates)].filter((candidate) => candidate !== canonicalPath && existsSync4(candidate)).sort();
6563
+ return [...new Set(candidates)].filter((candidate) => candidate !== canonicalPath && existsSync8(candidate)).sort();
4734
6564
  }
4735
6565
  function resetMacOSPermissions() {
4736
6566
  const services = ["Microphone", "Accessibility"];
4737
6567
  for (const service of services) {
4738
- const result = spawnSync4("tccutil", ["reset", service, "com.hasna.recordings"], {
4739
- stdio: "inherit"
4740
- });
6568
+ const result = spawnSync7("/usr/bin/tccutil", ["reset", service, RECORDINGS_BUNDLE_IDENTIFIER], { stdio: "inherit" });
4741
6569
  if (result.error) {
4742
6570
  console.error(chalk.red(result.error.message));
4743
6571
  process.exit(1);
@@ -4745,7 +6573,7 @@ function resetMacOSPermissions() {
4745
6573
  }
4746
6574
  }
4747
6575
  function getCodeSigningInfo(appPath) {
4748
- if (process.platform !== "darwin" || !existsSync4(appPath)) {
6576
+ if (process.platform !== "darwin" || !existsSync8(appPath)) {
4749
6577
  return {
4750
6578
  cdHash: null,
4751
6579
  adHoc: false,
@@ -4755,7 +6583,7 @@ function getCodeSigningInfo(appPath) {
4755
6583
  authorities: []
4756
6584
  };
4757
6585
  }
4758
- const result = spawnSync4("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", appPath], {
6586
+ const result = spawnSync7("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", appPath], {
4759
6587
  encoding: "utf8",
4760
6588
  stdio: ["ignore", "pipe", "pipe"]
4761
6589
  });
@@ -4766,58 +6594,31 @@ ${result.stderr}`;
4766
6594
  const identifier = output.match(/^Identifier=(.+)$/m)?.[1]?.trim() ?? null;
4767
6595
  const teamIdentifier = output.match(/^TeamIdentifier=(.+)$/m)?.[1]?.trim() ?? null;
4768
6596
  const designatedRequirement = output.match(/^designated => (.+)$/m)?.[1]?.trim() ?? null;
4769
- const authorities = [...output.matchAll(/^Authority=(.+)$/gm)].map((match) => match[1].trim());
6597
+ const authorities = [...output.matchAll(/^Authority=(.+)$/gm)].flatMap((match) => {
6598
+ const authority = match[1];
6599
+ return authority === undefined ? [] : [authority.trim()];
6600
+ });
4770
6601
  return { cdHash, adHoc, identifier, teamIdentifier, designatedRequirement, authorities };
4771
6602
  }
4772
- function getTccPermission(service, home) {
4773
- if (process.platform !== "darwin")
4774
- return "unsupported";
4775
- const dbPaths = [
4776
- pathJoin(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
4777
- pathJoin("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
4778
- ];
4779
- const sql = "select auth_value from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
4780
- for (const dbPath of dbPaths) {
4781
- if (!existsSync4(dbPath))
4782
- continue;
4783
- const result = spawnSync4("/usr/bin/sqlite3", [dbPath, sql], {
4784
- encoding: "utf8",
4785
- stdio: ["ignore", "pipe", "ignore"]
4786
- });
4787
- const value = result.stdout.trim();
4788
- if (!value)
4789
- continue;
4790
- return `${tccAuthValueLabel(value)}_identity_unverified`;
4791
- }
4792
- return "not_determined";
4793
- }
4794
- function tccAuthValueLabel(value) {
4795
- switch (value) {
4796
- case "0":
4797
- return "denied";
4798
- case "1":
4799
- return "unknown";
4800
- case "2":
4801
- return "allowed";
4802
- case "3":
4803
- return "limited";
4804
- default:
4805
- return `unknown(${value})`;
6603
+ function getTccGrant(service, home, appPath) {
6604
+ if (process.platform !== "darwin") {
6605
+ return { state: "unsupported", storedRequirement: null, durability: "unknown" };
4806
6606
  }
6607
+ return resolveTccGrant({ service, home, appPath });
4807
6608
  }
4808
6609
  function findPackageRoot() {
4809
- let current = dirname4(fileURLToPath(import.meta.url));
6610
+ let current = dirname5(fileURLToPath(import.meta.url));
4810
6611
  while (true) {
4811
6612
  const packagePath = pathJoin(current, "package.json");
4812
- if (existsSync4(packagePath)) {
6613
+ if (existsSync8(packagePath)) {
4813
6614
  try {
4814
- const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
6615
+ const pkg = JSON.parse(readFileSync4(packagePath, "utf8"));
4815
6616
  if (pkg.name === "@hasna/recordings") {
4816
6617
  return current;
4817
6618
  }
4818
6619
  } catch {}
4819
6620
  }
4820
- const parent = dirname4(current);
6621
+ const parent = dirname5(current);
4821
6622
  if (parent === current) {
4822
6623
  return process.cwd();
4823
6624
  }