sneakoscope 7.3.0 → 7.6.0

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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -1,7 +1,8 @@
1
1
  import fsp from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { ensureDir, exists, globalSksRoot, runProcess, writeTextAtomic } from '../fsx.js';
4
+ import { codexAppServerExecutablePath } from '../codex-control/codex-app-server-runtime-env.js';
5
+ import { ensureDir, exists, globalSksRoot, readJson, runProcess, writeTextAtomic } from '../fsx.js';
5
6
  export const TELEGRAM_HUB_LAUNCH_LABEL = 'com.sneakoscope.telegram-hub';
6
7
  export function telegramHubServicePaths(globalRoot = globalSksRoot(), home = process.env.HOME || os.homedir()) {
7
8
  const root = path.resolve(globalRoot);
@@ -10,10 +11,17 @@ export function telegramHubServicePaths(globalRoot = globalSksRoot(), home = pro
10
11
  global_root: root,
11
12
  launch_agent_path: path.join(path.resolve(home), 'Library', 'LaunchAgents', `${TELEGRAM_HUB_LAUNCH_LABEL}.plist`),
12
13
  stdout_log_path: path.join(logs, 'hub.out.log'),
13
- stderr_log_path: path.join(logs, 'hub.err.log')
14
+ stderr_log_path: path.join(logs, 'hub.err.log'),
15
+ receive_health_path: path.join(root, 'telegram', 'receive-health.json')
14
16
  };
15
17
  }
16
18
  export function telegramHubLaunchAgentSource(input) {
19
+ const home = path.dirname(path.dirname(path.dirname(input.paths.launch_agent_path)));
20
+ const executablePath = codexAppServerExecutablePath({
21
+ nodeBin: input.nodeBin,
22
+ home,
23
+ inheritedPath: ''
24
+ });
17
25
  const args = [
18
26
  '/usr/bin/caffeinate',
19
27
  '-i',
@@ -24,6 +32,7 @@ export function telegramHubLaunchAgentSource(input) {
24
32
  'run',
25
33
  '--project-root',
26
34
  path.resolve(input.projectRoot),
35
+ '--service-managed',
27
36
  '--json'
28
37
  ];
29
38
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -35,6 +44,7 @@ export function telegramHubLaunchAgentSource(input) {
35
44
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
36
45
  <key>ProcessType</key><string>Background</string>
37
46
  <key>ThrottleInterval</key><integer>5</integer>
47
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(executablePath)}</string></dict>
38
48
  <key>StandardOutPath</key><string>${xml(input.paths.stdout_log_path)}</string>
39
49
  <key>StandardErrorPath</key><string>${xml(input.paths.stderr_log_path)}</string>
40
50
  </dict></plist>
@@ -116,23 +126,91 @@ export async function telegramHubServiceStatus(options) {
116
126
  timedOut: false
117
127
  }));
118
128
  const text = `${result.stdout || ''}\n${result.stderr || ''}`;
119
- const state = text.match(/^[ \t]*state = ([^\n]+)/m)?.[1]?.trim() ?? null;
129
+ const launchState = text.match(/^[ \t]*state = ([^\n]+)/m)?.[1]?.trim() ?? null;
120
130
  const pidText = text.match(/^[ \t]*pid = (\d+)/m)?.[1] ?? null;
121
- const pid = pidText ? Number(pidText) : null;
131
+ const launchPid = pidText ? Number(pidText) : null;
122
132
  const loaded = result.code === 0;
123
- const running = loaded && state === 'running' && Boolean(pid);
133
+ const launchRunning = loaded && launchState === 'running' && Boolean(launchPid);
134
+ const receiveHealth = await readJson(paths.receive_health_path, null);
135
+ const currentReceiveHealth = receiveHealth?.schema === 'sks.telegram-receive-health.v1'
136
+ ? receiveHealth
137
+ : null;
138
+ const owner = await readJson(path.join(paths.global_root, 'telegram', 'owner.lock'), null);
139
+ const now = Date.now();
140
+ const isPidAlive = options.isPidAlive ?? localPidAlive;
141
+ const directOwnerFresh = Boolean(owner?.schema === 'sks.telegram-owner.v1'
142
+ && owner.host === os.hostname()
143
+ && Number.isInteger(owner.pid)
144
+ && owner.pid > 0
145
+ && Number.isFinite(Date.parse(owner.heartbeat_at))
146
+ && now - Date.parse(owner.heartbeat_at) <= 90_000
147
+ && isPidAlive(owner.pid));
148
+ const directIdentity = directOwnerFresh && owner
149
+ ? await (options.inspectDirectHub ?? inspectCenterSupervisedDirectHub)(owner.pid, options.projectRoot)
150
+ .catch(() => null)
151
+ : null;
152
+ const directRunning = Boolean(directOwnerFresh
153
+ && directIdentity
154
+ && directIdentity.parent_pid === directIdentity.supervisor_pid
155
+ && isPidAlive(directIdentity.supervisor_pid));
156
+ const launchHealthMatches = Boolean(launchRunning
157
+ && currentReceiveHealth
158
+ && currentReceiveHealth.pid === launchPid);
159
+ const mode = launchHealthMatches
160
+ ? 'launch_agent'
161
+ : directRunning
162
+ ? 'direct'
163
+ : launchRunning
164
+ ? 'launch_agent'
165
+ : null;
166
+ const running = launchRunning || directRunning;
167
+ const pid = mode === 'direct' ? owner?.pid ?? null : launchPid;
168
+ const state = mode === 'direct' ? 'direct' : launchState;
169
+ const matchingReceiveHealth = Boolean(running
170
+ && currentReceiveHealth
171
+ && currentReceiveHealth.pid === pid);
172
+ const receiveReady = Boolean(matchingReceiveHealth
173
+ && currentReceiveHealth?.state === 'ready'
174
+ && currentReceiveHealth.last_successful_poll_at
175
+ && Date.now() - Date.parse(currentReceiveHealth.last_successful_poll_at) <= 90_000
176
+ && currentReceiveHealth.dead_letter_ingress === 0);
124
177
  return {
125
178
  schema: 'sks.telegram-hub-service.v1',
126
- ok: running,
179
+ ok: receiveReady,
127
180
  supported: true,
128
181
  installed,
129
182
  loaded,
130
183
  running,
184
+ mode,
131
185
  state,
132
186
  pid,
187
+ receive_ready: receiveReady,
188
+ receive_state: receiveHealth?.schema === 'sks.telegram-receive-health.v1'
189
+ ? receiveHealth.state
190
+ : null,
191
+ receive_generation: receiveHealth?.schema === 'sks.telegram-receive-health.v1'
192
+ ? receiveHealth.generation
193
+ : null,
194
+ last_successful_poll_at: receiveHealth?.schema === 'sks.telegram-receive-health.v1'
195
+ ? receiveHealth.last_successful_poll_at
196
+ : null,
197
+ pending_ingress: receiveHealth?.schema === 'sks.telegram-receive-health.v1'
198
+ ? receiveHealth.pending_ingress
199
+ : 0,
200
+ dead_letter_ingress: receiveHealth?.schema === 'sks.telegram-receive-health.v1'
201
+ ? receiveHealth.dead_letter_ingress
202
+ : 0,
133
203
  service,
134
204
  launch_agent_path: paths.launch_agent_path,
135
- error: running ? null : loaded ? 'telegram_hub_not_running' : boundedError(result.stderr || result.stdout || 'telegram_hub_not_loaded')
205
+ error: receiveReady
206
+ ? null
207
+ : running
208
+ ? matchingReceiveHealth && Number(currentReceiveHealth?.dead_letter_ingress) > 0
209
+ ? 'telegram_ingress_dead_letter_present'
210
+ : 'telegram_receive_not_ready'
211
+ : loaded
212
+ ? 'telegram_hub_not_running'
213
+ : boundedError(result.stderr || result.stdout || 'telegram_hub_not_loaded')
136
214
  };
137
215
  }
138
216
  export async function removeTelegramHubService(options) {
@@ -150,8 +228,15 @@ function unsupportedStatus(paths, error, uid) {
150
228
  installed: false,
151
229
  loaded: false,
152
230
  running: false,
231
+ mode: null,
153
232
  state: null,
154
233
  pid: null,
234
+ receive_ready: false,
235
+ receive_state: null,
236
+ receive_generation: null,
237
+ last_successful_poll_at: null,
238
+ pending_ingress: 0,
239
+ dead_letter_ingress: 0,
155
240
  service: `${launchDomain(uid)}/${TELEGRAM_HUB_LAUNCH_LABEL}`,
156
241
  launch_agent_path: paths.launch_agent_path,
157
242
  error
@@ -159,7 +244,7 @@ function unsupportedStatus(paths, error, uid) {
159
244
  }
160
245
  async function waitForTelegramHubService(options) {
161
246
  let status = await telegramHubServiceStatus(options);
162
- for (let attempt = 0; attempt < 25 && !status.running; attempt += 1) {
247
+ for (let attempt = 0; attempt < 80 && !status.ok; attempt += 1) {
163
248
  await new Promise((resolve) => setTimeout(resolve, 100));
164
249
  status = await telegramHubServiceStatus(options);
165
250
  }
@@ -171,6 +256,50 @@ function launchDomain(uid = typeof process.getuid === 'function' ? process.getui
171
256
  function boundedError(value) {
172
257
  return String(value || '').replace(/(?:\/Users|\/home)\/[^\s]+/g, '[path-redacted]').slice(0, 500);
173
258
  }
259
+ function localPidAlive(pid) {
260
+ try {
261
+ process.kill(pid, 0);
262
+ return true;
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ }
268
+ async function inspectCenterSupervisedDirectHub(pid, projectRoot) {
269
+ const result = await runProcess('/bin/ps', [
270
+ '-p',
271
+ String(pid),
272
+ '-o',
273
+ 'ppid=',
274
+ '-o',
275
+ 'command='
276
+ ], {
277
+ timeoutMs: 1_000,
278
+ maxOutputBytes: 16 * 1024
279
+ });
280
+ if (result.code !== 0 || result.timedOut)
281
+ return null;
282
+ const match = result.stdout.trim().match(/^(\d+)\s+([\s\S]+)$/);
283
+ if (!match)
284
+ return null;
285
+ const parentText = match[1];
286
+ const command = match[2];
287
+ if (!parentText || !command)
288
+ return null;
289
+ const parentPid = Number(parentText);
290
+ const supervisorMatch = command.match(/(?:^|\s)--center-supervisor-pid\s+([1-9]\d*)(?:\s|$)/);
291
+ if (!Number.isSafeInteger(parentPid)
292
+ || !supervisorMatch
293
+ || !/(?:^|\s)telegram\s+hub\s+run(?:\s|$)/.test(command)
294
+ || !/(?:^|\s)--center-supervised(?:\s|$)/.test(command)
295
+ || !command.includes(`--project-root ${path.resolve(projectRoot)}`)) {
296
+ return null;
297
+ }
298
+ const supervisorPid = Number(supervisorMatch[1]);
299
+ if (!Number.isSafeInteger(supervisorPid))
300
+ return null;
301
+ return { parent_pid: parentPid, supervisor_pid: supervisorPid };
302
+ }
174
303
  function xml(value) {
175
304
  return value
176
305
  .replace(/&/g, '&amp;')
@@ -1,10 +1,15 @@
1
+ import fsp from 'node:fs/promises';
1
2
  import os from 'node:os';
2
3
  import path from 'node:path';
3
- import { ensureDir, globalSksRoot, nowIso, readJson, runProcess, sha256, which, writeJsonAtomic } from '../fsx.js';
4
+ import { errorCodeOf } from '../errors/message.js';
5
+ import { ensureDir, globalSksRoot, nowIso, readJson, runProcess, sha256, which, writeBinaryAtomic, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
6
  import { REMOTE_MACHINE_REGISTRY_SCHEMA, REMOTE_SESSION_INDEX_SCHEMA, RemoteCodexSessionBindingStore, remoteCodexSessionBindingsPath, remoteMachineRegistryPath, remoteSessionIndexPath, validateAllowedRoot, validateRemoteMachineRegistry, validateRemoteSessionIndex } from '../remote/index.js';
5
- import { TelegramBotApiClient } from './bot-api.js';
6
- import { isPositiveTelegramId, validateTelegramBotToken } from './config.js';
7
+ import { probeTelegramBotReadiness, TelegramBotApiClient, TelegramBotApiError } from './bot-api.js';
8
+ import { buildTelegramConfigV2, isPositiveTelegramId, readTelegramKeychainToken, readTelegramPreviousConfig, telegramTokenFingerprint, validateTelegramBotToken } from './config.js';
7
9
  import { telegramHubPaths } from './hub.js';
10
+ import { TelegramIngressSpool } from './ingress-spool.js';
11
+ import { TelegramTopicRegistry } from './ledgers.js';
12
+ import { TelegramOwnerConflictError, TelegramOwnerLock } from './owner-lock.js';
8
13
  const KEYCHAIN_SERVICE = 'com.sneakoscope.telegram.bot';
9
14
  const KEYCHAIN_WRITER_SWIFT = `import Foundation
10
15
  import Security
@@ -15,6 +20,12 @@ let v:[String:Any]=[kSecValueData as String:Data(k.utf8)]
15
20
  var r=SecItemUpdate(q as CFDictionary,v as CFDictionary)
16
21
  if r==errSecItemNotFound{var n=q;n[kSecValueData as String]=Data(k.utf8);r=SecItemAdd(n as CFDictionary,nil)}
17
22
  if r != errSecSuccess{FileHandle.standardError.write(Data("keychain_status=\\(r)\\n".utf8));exit(1)}`;
23
+ const KEYCHAIN_DELETE_SWIFT = `import Foundation
24
+ import Security
25
+ let a=CommandLine.arguments[1],s=CommandLine.arguments[2]
26
+ let q:[String:Any]=[kSecClass as String:kSecClassGenericPassword,kSecAttrAccount as String:a,kSecAttrService as String:s]
27
+ let r=SecItemDelete(q as CFDictionary)
28
+ if r != errSecSuccess && r != errSecItemNotFound{FileHandle.standardError.write(Data("keychain_status=\\(r)\\n".utf8));exit(1)}`;
18
29
  export async function setupTelegramLocalCoding(input) {
19
30
  const token = validateTelegramBotToken(input.token.trim());
20
31
  const explicitPair = normalizeExplicitPair(input.pairedChatId, input.pairedUserId);
@@ -23,93 +34,228 @@ export async function setupTelegramLocalCoding(input) {
23
34
  if (rootIssue)
24
35
  throw new Error(`telegram_project_root_invalid:${rootIssue}`);
25
36
  const globalRoot = path.resolve(input.globalRoot ?? globalSksRoot());
37
+ const setupOwner = new TelegramOwnerLock({
38
+ lockPath: telegramHubPaths(globalRoot).owner,
39
+ tokenFingerprint: telegramTokenFingerprint(token)
40
+ });
41
+ try {
42
+ await setupOwner.acquire();
43
+ }
44
+ catch (error) {
45
+ if (error instanceof TelegramOwnerConflictError) {
46
+ throw new Error('telegram_hub_must_be_stopped_before_setup');
47
+ }
48
+ throw error;
49
+ }
50
+ try {
51
+ return await setupTelegramLocalCodingLocked(input, token, explicitPair, projectRoot, globalRoot);
52
+ }
53
+ finally {
54
+ await setupOwner.release();
55
+ }
56
+ }
57
+ async function setupTelegramLocalCodingLocked(input, token, explicitPair, projectRoot, globalRoot) {
26
58
  const api = input.api ?? new TelegramBotApiClient(token, { timeoutMs: 12_000, maxRetries: 0 });
27
- const bot = await api.call('getMe', {});
28
- const pairing = explicitPair ?? await detectPrivateStart(api);
59
+ const readiness = await probeTelegramBotReadiness(api).catch(throwTelegramSetupApiError);
60
+ if (readiness.webhook_configured) {
61
+ throw new Error('telegram_webhook_conflict:remove_webhook_before_long_polling');
62
+ }
63
+ const detectedPair = explicitPair === null
64
+ ? await detectPrivateStart(api, readiness.bot.username, false).catch(throwTelegramSetupApiError)
65
+ : null;
66
+ const pairing = explicitPair ?? detectedPair;
29
67
  const account = input.account?.trim() || process.env.USER || os.userInfo().username || 'sks';
30
- await (input.keychainWriter ?? writeTelegramKeychain)(token, KEYCHAIN_SERVICE, account);
31
68
  const hostname = input.hostname ?? os.hostname();
32
69
  const machineId = `local-${sha256(hostname).slice(0, 12)}`;
33
70
  const projectId = `project-${sha256(projectRoot).slice(0, 12)}`;
34
71
  const sessionId = `telegram-${sha256(`${machineId}:${projectId}`).slice(0, 12)}`;
35
- const bindingStore = new RemoteCodexSessionBindingStore(remoteCodexSessionBindingsPath(projectRoot));
72
+ const bindingPath = remoteCodexSessionBindingsPath(projectRoot);
73
+ const machinePath = remoteMachineRegistryPath(globalRoot);
74
+ const indexPath = remoteSessionIndexPath(projectRoot);
75
+ const hubPaths = telegramHubPaths(globalRoot);
76
+ const configPath = hubPaths.config;
77
+ const stateSnapshots = await Promise.all([
78
+ snapshotSetupFile(bindingPath),
79
+ snapshotSetupFile(machinePath),
80
+ snapshotSetupFile(indexPath),
81
+ snapshotSetupFile(hubPaths.topics),
82
+ snapshotSetupFile(hubPaths.projection),
83
+ snapshotSetupFile(hubPaths.idempotency),
84
+ snapshotSetupFile(hubPaths.ingress),
85
+ snapshotSetupFile(hubPaths.receive_health),
86
+ snapshotSetupFile(configPath)
87
+ ]);
88
+ const keychainWriter = input.keychainWriter ?? writeTelegramKeychain;
89
+ const keychainReader = input.keychainReader
90
+ ?? (input.keychainWriter ? async () => null : readTelegramKeychain);
91
+ const keychainDeleter = input.keychainDeleter
92
+ ?? (input.keychainWriter
93
+ ? async () => { throw new Error('telegram_keychain_rollback_unavailable'); }
94
+ : deleteTelegramKeychain);
95
+ const previousToken = await keychainReader(KEYCHAIN_SERVICE, account);
96
+ const previousConfig = await readPreviousTelegramConfig(configPath);
97
+ const tokenFingerprint = telegramTokenFingerprint(token);
98
+ const samePersistedIdentity = previousConfig?.schema === 'sks.telegram-config.v2'
99
+ && previousConfig.bot_user_id === readiness.bot.id
100
+ && previousConfig.bot_token_fingerprint === tokenFingerprint
101
+ && previousConfig.paired_chat_id === pairing.chatId
102
+ && previousConfig.paired_user_id === pairing.userId
103
+ && input.resetSession !== true;
104
+ const configGeneration = previousConfig?.schema === 'sks.telegram-config.v2'
105
+ ? samePersistedIdentity
106
+ ? previousConfig.config_generation
107
+ : previousConfig.config_generation + 1
108
+ : 1;
109
+ const config = buildTelegramConfigV2({
110
+ botTokenRef: { type: 'keychain', service: KEYCHAIN_SERVICE, account },
111
+ botUserId: readiness.bot.id,
112
+ botTokenFingerprint: tokenFingerprint,
113
+ pairedChatId: pairing.chatId,
114
+ pairedUserId: pairing.userId,
115
+ configGeneration,
116
+ longPollTimeoutSec: 25,
117
+ ownerStaleMs: 60_000,
118
+ protectContent: true,
119
+ silentNotifications: false
120
+ });
121
+ const identityChanged = previousConfig?.schema !== 'sks.telegram-config.v2'
122
+ || previousConfig.identity_fingerprint !== config.identity_fingerprint
123
+ || input.resetSession === true;
124
+ const ingress = new TelegramIngressSpool(hubPaths.ingress, {
125
+ identityFingerprint: config.identity_fingerprint,
126
+ ...(previousToken
127
+ ? { legacyTokenFingerprint: telegramTokenFingerprint(previousToken) }
128
+ : {})
129
+ });
130
+ if (identityChanged)
131
+ await ingress.assertIdentityRotationSafe();
132
+ const bindingStore = new RemoteCodexSessionBindingStore(bindingPath);
36
133
  const existingBinding = await bindingStore.find(sessionId);
37
- const binding = existingBinding && input.resetSession !== true
38
- ? existingBinding
39
- : await bindingStore.upsert({
40
- session_id: sessionId,
134
+ let credentialWriteAttempted = false;
135
+ try {
136
+ credentialWriteAttempted = true;
137
+ await keychainWriter(token, KEYCHAIN_SERVICE, account);
138
+ if (input.keychainWriter === undefined) {
139
+ await verifyTelegramKeychainReadback(token, KEYCHAIN_SERVICE, account, keychainReader);
140
+ }
141
+ const binding = existingBinding && input.resetSession !== true
142
+ ? existingBinding
143
+ : await bindingStore.upsert({
144
+ session_id: sessionId,
145
+ machine_id: machineId,
146
+ project_id: projectId,
147
+ project_root: projectRoot,
148
+ codex_thread_id: null,
149
+ last_turn_id: null,
150
+ last_turn_status: null
151
+ });
152
+ const registry = await upsertLocalMachine(machinePath, {
153
+ id: machineId,
154
+ display_name: `This Mac (${hostname})`.slice(0, 120),
155
+ transport: 'local',
156
+ allowed_roots: [projectRoot],
157
+ enabled: true
158
+ });
159
+ await upsertLocalTarget(indexPath, registry, {
41
160
  machine_id: machineId,
42
161
  project_id: projectId,
43
- project_root: projectRoot,
44
- codex_thread_id: null,
45
- last_turn_id: null,
46
- last_turn_status: null
162
+ project_root: projectRoot
47
163
  });
48
- const machinePath = remoteMachineRegistryPath(globalRoot);
49
- const registry = await upsertLocalMachine(machinePath, {
50
- id: machineId,
51
- display_name: `This Mac (${hostname})`.slice(0, 120),
52
- transport: 'local',
53
- allowed_roots: [projectRoot],
54
- enabled: true
55
- });
56
- const indexPath = remoteSessionIndexPath(projectRoot);
57
- await upsertLocalTarget(indexPath, registry, {
58
- machine_id: machineId,
59
- project_id: projectId,
60
- project_root: projectRoot
61
- });
62
- const config = {
63
- schema: 'sks.telegram-config.v1',
64
- bot_token_ref: { type: 'keychain', service: KEYCHAIN_SERVICE, account },
65
- paired_chat_ids: [pairing.chatId],
66
- paired_user_ids: [pairing.userId],
67
- long_poll_timeout_sec: 25,
68
- owner_stale_ms: 60_000,
69
- protect_content: true,
70
- silent_notifications: false
71
- };
72
- const configPath = telegramHubPaths(globalRoot).config;
73
- await writeJsonAtomic(configPath, config);
74
- return {
75
- schema: 'sks.telegram-setup.v1',
76
- ok: true,
77
- bot: {
78
- id: bot?.id === undefined ? null : String(bot.id),
79
- username: bot?.username ? String(bot.username) : null
80
- },
81
- pairing: {
82
- chat_id: pairing.chatId,
83
- user_id: pairing.userId,
84
- detected: explicitPair === null
85
- },
86
- machine_id: machineId,
87
- project_id: projectId,
88
- project_root: projectRoot,
89
- session_id: sessionId,
90
- codex_thread_id: binding.codex_thread_id,
91
- codex_thread_state: binding.codex_thread_id ? 'ready' : 'pending_first_turn',
92
- token_storage: 'macos-keychain',
93
- config_path: configPath,
94
- machine_registry_path: machinePath,
95
- session_index_path: indexPath
96
- };
164
+ if (identityChanged) {
165
+ await new TelegramTopicRegistry(hubPaths.topics, {
166
+ identityFingerprint: config.identity_fingerprint
167
+ }).invalidateAll();
168
+ await ingress.resetForIdentity();
169
+ await writeJsonAtomic(hubPaths.projection, {
170
+ schema: 'sks.telegram-projection-state.v1',
171
+ sessions: {}
172
+ });
173
+ await writeTextAtomic(hubPaths.idempotency, '', { mode: 0o600 });
174
+ await fsp.unlink(hubPaths.receive_health).catch((error) => {
175
+ if (errorCodeOf(error) !== 'ENOENT')
176
+ throw error;
177
+ });
178
+ }
179
+ await writeJsonAtomic(configPath, config);
180
+ if (detectedPair) {
181
+ await acknowledgePrivateStart(api, detectedPair.updateId).catch(throwTelegramSetupApiError);
182
+ }
183
+ return {
184
+ schema: 'sks.telegram-setup.v1',
185
+ ok: true,
186
+ bot: readiness.bot,
187
+ pairing: {
188
+ chat_id: pairing.chatId,
189
+ user_id: pairing.userId,
190
+ detected: explicitPair === null
191
+ },
192
+ machine_id: machineId,
193
+ project_id: projectId,
194
+ project_root: projectRoot,
195
+ session_id: sessionId,
196
+ codex_thread_id: binding.codex_thread_id,
197
+ codex_thread_state: binding.codex_thread_id ? 'ready' : 'pending_first_turn',
198
+ token_storage: 'macos-keychain',
199
+ config_path: configPath,
200
+ machine_registry_path: machinePath,
201
+ session_index_path: indexPath
202
+ };
203
+ }
204
+ catch (error) {
205
+ const rollbackErrors = [];
206
+ for (const snapshot of [...stateSnapshots].reverse()) {
207
+ await restoreSetupFile(snapshot).catch((rollbackError) => {
208
+ rollbackErrors.push(publicSetupError(rollbackError));
209
+ });
210
+ }
211
+ if (credentialWriteAttempted) {
212
+ const restoreCredential = previousToken === null
213
+ ? keychainDeleter(KEYCHAIN_SERVICE, account)
214
+ : keychainWriter(previousToken, KEYCHAIN_SERVICE, account);
215
+ await restoreCredential.catch((rollbackError) => {
216
+ rollbackErrors.push(publicSetupError(rollbackError));
217
+ });
218
+ }
219
+ if (rollbackErrors.length > 0) {
220
+ throw new Error(`telegram_setup_rollback_failed:${publicSetupError(error)}:${rollbackErrors.join(',')}`);
221
+ }
222
+ throw error;
223
+ }
97
224
  }
98
- export async function detectPrivateStart(api) {
225
+ async function readPreviousTelegramConfig(file) {
226
+ const raw = await readJson(file, null);
227
+ if (raw === null)
228
+ return null;
229
+ const config = readTelegramPreviousConfig(raw);
230
+ if (!config)
231
+ throw new Error('telegram_config_invalid:unsupported_or_malformed');
232
+ return config;
233
+ }
234
+ export async function detectPrivateStart(api, botUsername = null, consume = true) {
99
235
  const updates = await api.getUpdates({ timeout: 0, allowed_updates: ['message'] });
100
- const match = [...updates].reverse().find(isPrivateStart);
236
+ const candidates = updates.filter((update) => isPrivateStart(update, botUsername));
237
+ const identities = new Set(candidates.map((update) => (`${String(update.message.chat.id)}:${String(update.message.from.id)}`)));
238
+ if (identities.size > 1) {
239
+ throw new Error('telegram_pairing_ambiguous:retry_with_explicit_private_chat_and_user_ids');
240
+ }
241
+ const match = candidates.at(-1);
101
242
  if (!match?.message?.from) {
102
243
  throw new Error('telegram_pairing_start_not_found:send_/start_to_the_bot_then_retry');
103
244
  }
245
+ if (consume)
246
+ await acknowledgePrivateStart(api, match.update_id);
247
+ return {
248
+ chatId: String(match.message.chat.id),
249
+ userId: String(match.message.from.id),
250
+ updateId: match.update_id
251
+ };
252
+ }
253
+ async function acknowledgePrivateStart(api, updateId) {
104
254
  await api.getUpdates({
105
- offset: match.update_id + 1,
255
+ offset: updateId + 1,
106
256
  timeout: 0,
107
257
  allowed_updates: ['message']
108
258
  });
109
- return {
110
- chatId: String(match.message.chat.id),
111
- userId: String(match.message.from.id)
112
- };
113
259
  }
114
260
  export async function writeTelegramKeychain(token, service, account) {
115
261
  if (process.platform !== 'darwin')
@@ -123,6 +269,65 @@ export async function writeTelegramKeychain(token, service, account) {
123
269
  if (result.code !== 0 || result.timedOut)
124
270
  throw new Error('telegram_keychain_store_failed');
125
271
  }
272
+ export async function readTelegramKeychain(service, account) {
273
+ return readTelegramKeychainToken(service, account, { timeoutMs: 5_000 })
274
+ .catch((error) => {
275
+ if (error instanceof Error && error.message === 'telegram_keychain_requires_macos')
276
+ throw error;
277
+ throw new Error('telegram_keychain_snapshot_failed');
278
+ });
279
+ }
280
+ export async function verifyTelegramKeychainReadback(expectedToken, service, account, reader = readTelegramKeychain) {
281
+ const storedToken = await reader(service, account).catch(() => null);
282
+ if (storedToken !== expectedToken)
283
+ throw new Error('telegram_keychain_readback_failed');
284
+ }
285
+ export async function deleteTelegramKeychain(service, account) {
286
+ if (process.platform !== 'darwin')
287
+ throw new Error('telegram_keychain_requires_macos');
288
+ const swift = await which('swift').catch(() => null) || '/usr/bin/swift';
289
+ const result = await runProcess(swift, ['-e', KEYCHAIN_DELETE_SWIFT, account, service], {
290
+ timeoutMs: 30_000,
291
+ maxOutputBytes: 8 * 1024
292
+ });
293
+ if (result.code !== 0 || result.timedOut)
294
+ throw new Error('telegram_keychain_delete_failed');
295
+ }
296
+ async function snapshotSetupFile(file) {
297
+ const stat = await fsp.lstat(file).catch((error) => {
298
+ if (errorCodeOf(error) === 'ENOENT')
299
+ return null;
300
+ throw error;
301
+ });
302
+ if (stat === null)
303
+ return { file, contents: null, mode: null };
304
+ if (!stat.isFile() || stat.isSymbolicLink())
305
+ throw new Error('telegram_setup_state_snapshot_invalid');
306
+ return {
307
+ file,
308
+ contents: await fsp.readFile(file),
309
+ mode: stat.mode & 0o777
310
+ };
311
+ }
312
+ async function restoreSetupFile(snapshot) {
313
+ if (snapshot.contents === null) {
314
+ await fsp.unlink(snapshot.file).catch((error) => {
315
+ if (errorCodeOf(error) !== 'ENOENT')
316
+ throw error;
317
+ });
318
+ return;
319
+ }
320
+ await writeBinaryAtomic(snapshot.file, snapshot.contents, {
321
+ ...(snapshot.mode === null ? {} : { mode: snapshot.mode })
322
+ });
323
+ }
324
+ function publicSetupError(error) {
325
+ return (error instanceof Error ? error.message : String(error))
326
+ .replace(/\b\d{5,}:[A-Za-z0-9_-]{20,}\b/g, '[redacted]')
327
+ .replace(/(?:\/Users|\/home)\/[^\s]+/g, '[path-redacted]')
328
+ .replace(/[:,]+/g, '_')
329
+ .slice(0, 160);
330
+ }
126
331
  async function upsertLocalMachine(file, machine) {
127
332
  await ensureDir(path.dirname(file));
128
333
  const raw = await readJson(file, null);
@@ -206,11 +411,24 @@ function normalizeExplicitPair(chatId, userId) {
206
411
  }
207
412
  return { chatId: chat, userId: user };
208
413
  }
209
- function isPrivateStart(update) {
414
+ function isPrivateStart(update, botUsername) {
210
415
  const message = update.message;
211
416
  if (!message || message.chat.type !== 'private' || !message.from)
212
417
  return false;
213
- return /^\/start(?:@\w+)?(?:\s|$)/i.test(String(message.text || '').trim());
418
+ const command = /^\/start(?:@([A-Za-z0-9_]+))?(?:\s|$)/i.exec(String(message.text || '').trim());
419
+ if (!command)
420
+ return false;
421
+ const addressedBot = command[1];
422
+ return !addressedBot || (botUsername !== null && addressedBot.toLowerCase() === botUsername.toLowerCase());
423
+ }
424
+ function throwTelegramSetupApiError(error) {
425
+ if (error instanceof TelegramBotApiError && (error.errorCode === 401 || error.errorCode === 404)) {
426
+ throw new Error('telegram_bot_auth_failed');
427
+ }
428
+ if (error instanceof TelegramBotApiError && error.errorCode === 409) {
429
+ throw new Error('telegram_409_conflict:stop_other_poller_and_retry');
430
+ }
431
+ throw error;
214
432
  }
215
433
  export function telegramSetupKeychainReference(account = process.env.USER || os.userInfo().username || 'sks') {
216
434
  return { type: 'keychain', service: KEYCHAIN_SERVICE, account };
@@ -1,3 +1,6 @@
1
+ export function uniqueValues(values) {
2
+ return [...new Set(values)];
3
+ }
1
4
  export function uniqueStrings(values) {
2
5
  return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
3
6
  }