@gajae-code/coding-agent 0.10.2 → 0.11.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 (476) hide show
  1. package/CHANGELOG.md +58 -2
  2. package/dist/types/append-only-mode.d.ts +1 -1
  3. package/dist/types/async/job-manager.d.ts +46 -7
  4. package/dist/types/cli/args.d.ts +4 -5
  5. package/dist/types/cli/daemon-cli.d.ts +12 -1
  6. package/dist/types/cli/notify-cli.d.ts +21 -1
  7. package/dist/types/cli.d.ts +3 -0
  8. package/dist/types/commands/daemon.d.ts +39 -0
  9. package/dist/types/commands/launch.d.ts +3 -0
  10. package/dist/types/commands/notify.d.ts +28 -1
  11. package/dist/types/commands/sdk.d.ts +33 -0
  12. package/dist/types/config/atomic-yaml-patch.d.ts +85 -0
  13. package/dist/types/config/model-profile-activation.d.ts +35 -15
  14. package/dist/types/config/model-profiles.d.ts +11 -5
  15. package/dist/types/config/model-registry.d.ts +3 -3
  16. package/dist/types/config/model-resolver.d.ts +39 -6
  17. package/dist/types/config/model-selector-value.d.ts +17 -0
  18. package/dist/types/config/models-config-schema.d.ts +10 -6
  19. package/dist/types/config/settings-schema.d.ts +133 -6
  20. package/dist/types/config/settings.d.ts +63 -35
  21. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -6
  22. package/dist/types/coordinator-mcp/server.d.ts +6 -49
  23. package/dist/types/daemon/builtin.d.ts +6 -5
  24. package/dist/types/daemon/control-types.d.ts +1 -1
  25. package/dist/types/discovery/mcp-json.d.ts +12 -1
  26. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  27. package/dist/types/extensibility/extensions/compact-handler.d.ts +4 -2
  28. package/dist/types/extensibility/extensions/get-commands-handler.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +10 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +3 -3
  31. package/dist/types/extensibility/extensions/types.d.ts +141 -16
  32. package/dist/types/extensibility/hooks/types.d.ts +1 -1
  33. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -2
  34. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  35. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +3 -4
  37. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +13 -0
  38. package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -1
  39. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
  40. package/dist/types/gjc-runtime/windows-powershell-command.d.ts +9 -0
  41. package/dist/types/harness-control-plane/operate.d.ts +5 -5
  42. package/dist/types/harness-control-plane/owner.d.ts +18 -4
  43. package/dist/types/harness-control-plane/receipts.d.ts +0 -1
  44. package/dist/types/harness-control-plane/sdk-transport.d.ts +43 -0
  45. package/dist/types/harness-control-plane/session-transport.d.ts +48 -0
  46. package/dist/types/harness-control-plane/types.d.ts +3 -4
  47. package/dist/types/lsp/index.d.ts +1 -1
  48. package/dist/types/main.d.ts +32 -38
  49. package/dist/types/modes/acp/acp-agent.d.ts +224 -53
  50. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -0
  51. package/dist/types/modes/acp/acp-mode.d.ts +6 -15
  52. package/dist/types/modes/acp/permission-mode.d.ts +4 -0
  53. package/dist/types/modes/acp/startup-options.d.ts +6 -0
  54. package/dist/types/modes/components/agent-dashboard.d.ts +3 -0
  55. package/dist/types/modes/components/model-selector.d.ts +1 -0
  56. package/dist/types/modes/components/notifications-settings-editor.d.ts +135 -0
  57. package/dist/types/modes/components/settings-selector.d.ts +6 -4
  58. package/dist/types/modes/components/welcome.d.ts +1 -1
  59. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  60. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  61. package/dist/types/modes/controllers/selector-controller.d.ts +35 -1
  62. package/dist/types/modes/index.d.ts +0 -4
  63. package/dist/types/modes/interactive-mode.d.ts +5 -2
  64. package/dist/types/modes/runtime-init.d.ts +2 -2
  65. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +6 -9
  66. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -6
  67. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +6 -6
  68. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +4 -9
  69. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +1 -1
  70. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +170 -30
  71. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +7 -7
  72. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +105 -0
  73. package/dist/types/modes/types.d.ts +7 -2
  74. package/dist/types/runtime-mcp/client.d.ts +2 -0
  75. package/dist/types/runtime-mcp/config.d.ts +5 -1
  76. package/dist/types/runtime-mcp/manager.d.ts +8 -1
  77. package/dist/types/runtime-mcp/types.d.ts +4 -0
  78. package/dist/types/sdk/acp/adapter.d.ts +67 -0
  79. package/dist/types/sdk/acp/index.d.ts +2 -0
  80. package/dist/types/sdk/broker/broker.d.ts +53 -0
  81. package/dist/types/sdk/broker/discovery.d.ts +30 -0
  82. package/dist/types/sdk/broker/ensure.d.ts +42 -0
  83. package/dist/types/sdk/broker/identity.d.ts +3 -0
  84. package/dist/types/sdk/broker/index.d.ts +8 -0
  85. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +95 -0
  86. package/dist/types/sdk/broker/lifecycle.d.ts +85 -0
  87. package/dist/types/sdk/broker/process-incarnation.d.ts +14 -0
  88. package/dist/types/sdk/broker/runtime.d.ts +33 -0
  89. package/dist/types/sdk/broker/session-index.d.ts +57 -0
  90. package/dist/types/sdk/broker/state-version.d.ts +8 -0
  91. package/dist/types/sdk/broker/transport.d.ts +9 -0
  92. package/dist/types/sdk/bus/chat-command-policy.d.ts +49 -0
  93. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +21 -0
  94. package/dist/types/sdk/bus/chat-daemon-control.d.ts +111 -0
  95. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +77 -0
  96. package/dist/types/sdk/bus/chat-effect-journal.d.ts +84 -0
  97. package/dist/types/{notifications → sdk/bus}/config-commands.d.ts +7 -2
  98. package/dist/types/sdk/bus/config.d.ts +274 -0
  99. package/dist/types/sdk/bus/conversation-store.d.ts +72 -0
  100. package/dist/types/sdk/bus/discord-conversation.d.ts +58 -0
  101. package/dist/types/sdk/bus/discord-daemon.d.ts +50 -0
  102. package/dist/types/sdk/bus/discord-live-provider.d.ts +70 -0
  103. package/dist/types/sdk/bus/discord-provider.d.ts +85 -0
  104. package/dist/types/{notifications → sdk/bus}/html-format.d.ts +2 -2
  105. package/dist/types/{notifications → sdk/bus}/index.d.ts +113 -13
  106. package/dist/types/{notifications → sdk/bus}/lifecycle-control-runtime.d.ts +4 -2
  107. package/dist/types/{notifications → sdk/bus}/lifecycle-orchestrator.d.ts +18 -21
  108. package/dist/types/{notifications → sdk/bus}/managed-daemon.d.ts +1 -1
  109. package/dist/types/sdk/bus/native-runtime-compatibility.d.ts +13 -0
  110. package/dist/types/sdk/bus/notification-orchestration.d.ts +170 -0
  111. package/dist/types/{notifications → sdk/bus}/notification-service.d.ts +7 -1
  112. package/dist/types/sdk/bus/session-control.d.ts +83 -0
  113. package/dist/types/sdk/bus/slack-conversation.d.ts +56 -0
  114. package/dist/types/sdk/bus/slack-daemon.d.ts +60 -0
  115. package/dist/types/sdk/bus/slack-live-provider.d.ts +59 -0
  116. package/dist/types/sdk/bus/slack-provider.d.ts +55 -0
  117. package/dist/types/{notifications → sdk/bus}/telegram-cli.d.ts +4 -4
  118. package/dist/types/{notifications → sdk/bus}/telegram-daemon-cli.d.ts +14 -5
  119. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -0
  120. package/dist/types/{notifications → sdk/bus}/telegram-daemon-control.d.ts +2 -2
  121. package/dist/types/{notifications → sdk/bus}/telegram-daemon.d.ts +78 -17
  122. package/dist/types/{notifications → sdk/bus}/telegram-reference.d.ts +2 -2
  123. package/dist/types/sdk/bus/telegram-setup.d.ts +128 -0
  124. package/dist/types/{notifications → sdk/bus}/topic-registry.d.ts +11 -0
  125. package/dist/types/sdk/cli/index.d.ts +1 -0
  126. package/dist/types/sdk/cli/session-cli.d.ts +19 -0
  127. package/dist/types/sdk/client/client.d.ts +1 -0
  128. package/dist/types/sdk/client/discovery.d.ts +27 -0
  129. package/dist/types/sdk/client/index.d.ts +2 -0
  130. package/dist/types/sdk/host/control/dispatch.d.ts +34 -0
  131. package/dist/types/sdk/host/control/index.d.ts +2 -0
  132. package/dist/types/sdk/host/control/operations.d.ts +65 -0
  133. package/dist/types/sdk/host/events.d.ts +35 -0
  134. package/dist/types/sdk/host/host.d.ts +35 -0
  135. package/dist/types/sdk/host/index.d.ts +4 -0
  136. package/dist/types/sdk/host/query/cursor.d.ts +62 -0
  137. package/dist/types/sdk/host/query/handlers.d.ts +70 -0
  138. package/dist/types/sdk/host/query/index.d.ts +3 -0
  139. package/dist/types/sdk/host/query/revision-store.d.ts +60 -0
  140. package/dist/types/sdk/host/reverse-leases.d.ts +45 -0
  141. package/dist/types/sdk/host/types.d.ts +32 -0
  142. package/dist/types/sdk/index.d.ts +6 -0
  143. package/dist/types/sdk/lifecycle-session.d.ts +14 -0
  144. package/dist/types/sdk/mcp/index.d.ts +1 -0
  145. package/dist/types/sdk/mcp/server.d.ts +37 -0
  146. package/dist/types/sdk/models.d.ts +44 -0
  147. package/dist/types/sdk/protocol/adapter-validation.d.ts +14 -0
  148. package/dist/types/sdk/protocol/operation-registry.d.ts +24 -0
  149. package/dist/types/{sdk.d.ts → sdk/session.d.ts} +44 -38
  150. package/dist/types/sdk/startup-capability.d.ts +50 -0
  151. package/dist/types/session/agent-session.d.ts +114 -21
  152. package/dist/types/session/default-model-selection.d.ts +25 -0
  153. package/dist/types/session/fallback-chain-controller.d.ts +52 -0
  154. package/dist/types/session/session-manager.d.ts +58 -1
  155. package/dist/types/setup/credential-auto-import.d.ts +36 -6
  156. package/dist/types/tools/ask-answer-registry.d.ts +1 -1
  157. package/dist/types/tools/browser/benchmark/detector-report.d.ts +63 -0
  158. package/dist/types/tools/browser/benchmark/run-suite.d.ts +18 -0
  159. package/dist/types/tools/browser/launch.d.ts +22 -0
  160. package/dist/types/tools/browser/profile-discovery.d.ts +31 -0
  161. package/dist/types/tools/browser/profile-posture.d.ts +37 -0
  162. package/dist/types/tools/browser/profile-reuse.d.ts +39 -0
  163. package/dist/types/tools/browser/profile-warmup.d.ts +39 -0
  164. package/dist/types/tools/browser/registry.d.ts +19 -0
  165. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  166. package/dist/types/tools/index.d.ts +8 -8
  167. package/dist/types/tools/subagent-render.d.ts +1 -0
  168. package/dist/types/tools/subagent.d.ts +6 -1
  169. package/package.json +34 -13
  170. package/scripts/build-sdk-package-smoke.ts +99 -0
  171. package/scripts/capture-notifications-settings-showcase.ts +435 -0
  172. package/scripts/compile-args.ts +4 -2
  173. package/scripts/g005-daemon-smoke.ts +4 -5
  174. package/scripts/g011-daemon-path-smoke.ts +5 -5
  175. package/scripts/generate-sdk-adapter-parity-manifest.ts +163 -0
  176. package/scripts/generate-sdk-operation-inventory.ts +867 -0
  177. package/scripts/generate-telegram-baseline-manifest.ts +128 -0
  178. package/scripts/manifest-schema.ts +124 -0
  179. package/scripts/run-test-manifest.ts +153 -0
  180. package/scripts/run-threat-model-check.ts +157 -0
  181. package/scripts/verify-gjc-sdk-canonicalization.ts +3467 -0
  182. package/scripts/verify-gjc-sdk-rename.ts +90 -0
  183. package/src/append-only-mode.ts +1 -1
  184. package/src/async/job-manager.ts +406 -63
  185. package/src/cli/args.ts +43 -12
  186. package/src/cli/auth-gateway-cli.ts +2 -2
  187. package/src/cli/config-cli.ts +2 -1
  188. package/src/cli/daemon-cli.ts +63 -14
  189. package/src/cli/list-models.ts +1 -1
  190. package/src/cli/notify-cli.ts +288 -286
  191. package/src/cli.ts +21 -2
  192. package/src/commands/daemon.ts +50 -6
  193. package/src/commands/harness.ts +93 -36
  194. package/src/commands/launch.ts +5 -2
  195. package/src/commands/mcp-serve.ts +19 -12
  196. package/src/commands/notify.ts +36 -3
  197. package/src/commands/sdk.ts +426 -0
  198. package/src/commit/agentic/agent.ts +1 -0
  199. package/src/config/atomic-yaml-patch.ts +423 -0
  200. package/src/config/model-profile-activation.ts +240 -89
  201. package/src/config/model-profiles.ts +18 -15
  202. package/src/config/model-registry.ts +51 -26
  203. package/src/config/model-resolver.ts +138 -51
  204. package/src/config/model-selector-value.ts +50 -0
  205. package/src/config/models-config-schema.ts +14 -21
  206. package/src/config/settings-schema.ts +128 -10
  207. package/src/config/settings.ts +1120 -158
  208. package/src/coordinator-mcp/model-preset.ts +74 -20
  209. package/src/coordinator-mcp/server.ts +1597 -3052
  210. package/src/daemon/builtin.ts +8 -5
  211. package/src/daemon/control-types.ts +1 -1
  212. package/src/defaults/gjc/skills/team/SKILL.md +4 -4
  213. package/src/discovery/mcp-json.ts +327 -20
  214. package/src/extensibility/custom-tools/types.ts +1 -1
  215. package/src/extensibility/extensions/compact-handler.ts +3 -3
  216. package/src/extensibility/extensions/get-commands-handler.ts +2 -2
  217. package/src/extensibility/extensions/loader.ts +69 -5
  218. package/src/extensibility/extensions/runner.ts +112 -4
  219. package/src/extensibility/extensions/types.ts +176 -17
  220. package/src/extensibility/hooks/types.ts +1 -1
  221. package/src/gjc-runtime/launch-tmux.ts +170 -86
  222. package/src/gjc-runtime/psmux-detect.ts +82 -4
  223. package/src/gjc-runtime/session-state-sidecar.ts +41 -31
  224. package/src/gjc-runtime/team-runtime.ts +54 -35
  225. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +53 -17
  226. package/src/gjc-runtime/tmux-owner-isolation.ts +1 -1
  227. package/src/gjc-runtime/tmux-sessions.ts +19 -13
  228. package/src/gjc-runtime/ultragoal-guard.ts +108 -14
  229. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  230. package/src/gjc-runtime/ultragoal-runtime.ts +227 -11
  231. package/src/gjc-runtime/windows-powershell-command.ts +58 -0
  232. package/src/harness-control-plane/operate.ts +9 -9
  233. package/src/harness-control-plane/owner.ts +283 -39
  234. package/src/harness-control-plane/receipts.ts +0 -1
  235. package/src/harness-control-plane/sdk-transport.ts +395 -0
  236. package/src/harness-control-plane/session-transport.ts +94 -0
  237. package/src/harness-control-plane/types.ts +1 -1
  238. package/src/internal-urls/docs-index.generated.ts +34 -35
  239. package/src/lsp/client.ts +42 -6
  240. package/src/lsp/index.ts +2 -2
  241. package/src/main.ts +299 -204
  242. package/src/modes/DESIGN.md +258 -0
  243. package/src/modes/acp/acp-agent.ts +1228 -2612
  244. package/src/modes/acp/acp-client-bridge.ts +2 -20
  245. package/src/modes/acp/acp-event-mapper.ts +18 -2
  246. package/src/modes/acp/acp-mode.ts +8 -36
  247. package/src/modes/acp/permission-mode.ts +25 -0
  248. package/src/modes/acp/startup-options.ts +6 -0
  249. package/src/modes/components/agent-dashboard.ts +37 -17
  250. package/src/modes/components/model-selector.ts +93 -35
  251. package/src/modes/components/notifications-settings-editor.ts +1379 -0
  252. package/src/modes/components/settings-selector.ts +60 -11
  253. package/src/modes/components/welcome.ts +4 -4
  254. package/src/modes/controllers/command-controller.ts +10 -17
  255. package/src/modes/controllers/event-controller.ts +11 -16
  256. package/src/modes/controllers/extension-ui-controller.ts +396 -28
  257. package/src/modes/controllers/selector-controller.ts +674 -36
  258. package/src/modes/index.ts +0 -22
  259. package/src/modes/interactive-mode.ts +87 -36
  260. package/src/modes/runtime-init.ts +298 -6
  261. package/src/modes/shared/agent-wire/approval-gate.ts +9 -11
  262. package/src/modes/shared/agent-wire/deep-interview-gate.ts +10 -11
  263. package/src/modes/shared/agent-wire/event-contract.ts +7 -8
  264. package/src/modes/shared/agent-wire/event-envelope.ts +9 -17
  265. package/src/modes/shared/agent-wire/event-observation.ts +16 -7
  266. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +1258 -87
  267. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +15 -15
  268. package/src/modes/shared/agent-wire/workflow-gate-types.ts +113 -0
  269. package/src/modes/types.ts +7 -2
  270. package/src/runtime-mcp/client.ts +94 -19
  271. package/src/runtime-mcp/config.ts +27 -15
  272. package/src/runtime-mcp/manager.ts +343 -105
  273. package/src/runtime-mcp/oauth-flow.ts +42 -12
  274. package/src/runtime-mcp/transports/http.ts +49 -19
  275. package/src/runtime-mcp/transports/stdio.ts +47 -28
  276. package/src/runtime-mcp/types.ts +10 -0
  277. package/src/sdk/acp/adapter.ts +418 -0
  278. package/src/sdk/acp/index.ts +2 -0
  279. package/src/sdk/broker/broker.ts +642 -0
  280. package/src/sdk/broker/discovery.ts +113 -0
  281. package/src/sdk/broker/ensure.ts +307 -0
  282. package/src/sdk/broker/identity.ts +42 -0
  283. package/src/sdk/broker/index.ts +8 -0
  284. package/src/sdk/broker/internal-source-marker-2178.txt +1 -0
  285. package/src/sdk/broker/internal-source.bunfig.toml +1 -0
  286. package/src/sdk/broker/lifecycle-ledger.ts +286 -0
  287. package/src/sdk/broker/lifecycle.ts +2157 -0
  288. package/src/sdk/broker/process-incarnation.ts +159 -0
  289. package/src/sdk/broker/runtime.ts +164 -0
  290. package/src/sdk/broker/session-index.ts +318 -0
  291. package/src/sdk/broker/state-version.ts +23 -0
  292. package/src/sdk/broker/transport.ts +125 -0
  293. package/src/sdk/bus/chat-command-policy.ts +107 -0
  294. package/src/sdk/bus/chat-daemon-cli.ts +214 -0
  295. package/src/sdk/bus/chat-daemon-control.ts +713 -0
  296. package/src/sdk/bus/chat-daemon-runtime.ts +526 -0
  297. package/src/sdk/bus/chat-effect-journal.ts +355 -0
  298. package/src/{notifications → sdk/bus}/config-commands.ts +39 -10
  299. package/src/sdk/bus/config.ts +433 -0
  300. package/src/sdk/bus/conversation-store.ts +391 -0
  301. package/src/sdk/bus/discord-conversation.ts +85 -0
  302. package/src/sdk/bus/discord-daemon.ts +2292 -0
  303. package/src/sdk/bus/discord-live-provider.ts +521 -0
  304. package/src/sdk/bus/discord-provider.ts +56 -0
  305. package/src/{notifications → sdk/bus}/html-format.ts +2 -2
  306. package/src/sdk/bus/index.ts +3979 -0
  307. package/src/{notifications → sdk/bus}/lifecycle-commands.ts +2 -0
  308. package/src/{notifications → sdk/bus}/lifecycle-control-runtime.ts +225 -196
  309. package/src/{notifications → sdk/bus}/lifecycle-orchestrator.ts +161 -74
  310. package/src/{notifications → sdk/bus}/managed-daemon.ts +2 -2
  311. package/src/sdk/bus/native-runtime-compatibility.ts +42 -0
  312. package/src/sdk/bus/natives-augment.d.ts +38 -0
  313. package/src/sdk/bus/notification-orchestration.ts +535 -0
  314. package/src/{notifications → sdk/bus}/notification-service.ts +106 -12
  315. package/src/sdk/bus/session-control.ts +333 -0
  316. package/src/sdk/bus/slack-conversation.ts +74 -0
  317. package/src/sdk/bus/slack-daemon.ts +2144 -0
  318. package/src/sdk/bus/slack-live-provider.ts +380 -0
  319. package/src/sdk/bus/slack-provider.ts +76 -0
  320. package/src/{notifications → sdk/bus}/telegram-cli.ts +8 -8
  321. package/src/sdk/bus/telegram-daemon-cli.ts +330 -0
  322. package/src/sdk/bus/telegram-daemon-contract.ts +14 -0
  323. package/src/{notifications → sdk/bus}/telegram-daemon-control.ts +4 -4
  324. package/src/{notifications → sdk/bus}/telegram-daemon.ts +603 -63
  325. package/src/{notifications → sdk/bus}/telegram-reference.ts +2 -2
  326. package/src/sdk/bus/telegram-setup.ts +663 -0
  327. package/src/{notifications → sdk/bus}/topic-registry.ts +32 -0
  328. package/src/sdk/cli/index.ts +1 -0
  329. package/src/sdk/cli/session-cli.ts +275 -0
  330. package/src/sdk/client/client.ts +1 -0
  331. package/src/sdk/client/discovery.ts +108 -0
  332. package/src/sdk/client/index.ts +2 -0
  333. package/src/sdk/host/control/dispatch.ts +367 -0
  334. package/src/sdk/host/control/index.ts +10 -0
  335. package/src/sdk/host/control/operations.ts +63 -0
  336. package/src/sdk/host/events.ts +91 -0
  337. package/src/sdk/host/host.ts +296 -0
  338. package/src/sdk/host/index.ts +4 -0
  339. package/src/sdk/host/query/cursor.ts +189 -0
  340. package/src/sdk/host/query/handlers.ts +533 -0
  341. package/src/sdk/host/query/index.ts +3 -0
  342. package/src/sdk/host/query/revision-store.ts +1126 -0
  343. package/src/sdk/host/reverse-leases.ts +325 -0
  344. package/src/sdk/host/types.ts +29 -0
  345. package/src/sdk/index.ts +13 -0
  346. package/src/sdk/lifecycle-session.ts +39 -0
  347. package/src/sdk/mcp/index.ts +1 -0
  348. package/src/sdk/mcp/server.ts +332 -0
  349. package/src/sdk/models.ts +168 -0
  350. package/src/sdk/protocol/adapter-validation.ts +48 -0
  351. package/src/sdk/protocol/operation-inventory.generated.json +5654 -0
  352. package/src/sdk/protocol/operation-registry.ts +312 -0
  353. package/src/{sdk.ts → sdk/session.ts} +339 -212
  354. package/src/sdk/startup-capability.ts +189 -0
  355. package/src/session/agent-session.ts +2650 -944
  356. package/src/session/default-model-selection.ts +93 -0
  357. package/src/session/fallback-chain-controller.ts +136 -0
  358. package/src/session/session-manager.ts +438 -11
  359. package/src/session/session-storage.ts +8 -1
  360. package/src/setup/credential-auto-import.ts +290 -35
  361. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -5
  362. package/src/slash-commands/builtin-registry.ts +2 -1
  363. package/src/task/executor.ts +62 -13
  364. package/src/task/index.ts +26 -16
  365. package/src/tools/ask-answer-registry.ts +1 -1
  366. package/src/tools/ask.ts +13 -8
  367. package/src/tools/browser/STEALTH-THIRD-PARTY-NOTICES.md +26 -0
  368. package/src/tools/browser/benchmark/detector-report.ts +157 -0
  369. package/src/tools/browser/benchmark/run-suite.ts +39 -0
  370. package/src/tools/browser/launch.ts +124 -19
  371. package/src/tools/browser/profile-discovery.ts +63 -0
  372. package/src/tools/browser/profile-posture.ts +59 -0
  373. package/src/tools/browser/profile-reuse.ts +102 -0
  374. package/src/tools/browser/profile-warmup.ts +109 -0
  375. package/src/tools/browser/registry.ts +87 -11
  376. package/src/tools/browser/tab-protocol.ts +1 -0
  377. package/src/tools/browser/tab-supervisor.ts +1 -0
  378. package/src/tools/browser/tab-worker.ts +1 -1
  379. package/src/tools/browser.ts +6 -0
  380. package/src/tools/index.ts +8 -8
  381. package/src/tools/puppeteer/14_stealth_permissions.txt +57 -0
  382. package/src/tools/puppeteer/15_stealth_webrtc.txt +60 -0
  383. package/src/tools/subagent-render.ts +19 -8
  384. package/src/tools/subagent.ts +84 -32
  385. package/src/tools/telegram-send.ts +2 -2
  386. package/vendor/insane-search/MANIFEST.json +2 -1
  387. package/vendor/insane-search/engine/bias_check.py +4 -1
  388. package/vendor/insane-search/engine/phase0.py +26 -1
  389. package/dist/types/harness-control-plane/rpc-adapter.d.ts +0 -69
  390. package/dist/types/modes/bridge/auth.d.ts +0 -12
  391. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +0 -9
  392. package/dist/types/modes/bridge/bridge-mode.d.ts +0 -47
  393. package/dist/types/modes/bridge/bridge-ui-context.d.ts +0 -88
  394. package/dist/types/modes/bridge/event-stream.d.ts +0 -8
  395. package/dist/types/modes/rpc/host-tools.d.ts +0 -1
  396. package/dist/types/modes/rpc/host-uris.d.ts +0 -1
  397. package/dist/types/modes/rpc/rpc-client.d.ts +0 -299
  398. package/dist/types/modes/rpc/rpc-mode.d.ts +0 -86
  399. package/dist/types/modes/rpc/rpc-socket-security.d.ts +0 -10
  400. package/dist/types/modes/rpc/rpc-types.d.ts +0 -810
  401. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +0 -18
  402. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +0 -37
  403. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +0 -2
  404. package/dist/types/modes/shared/agent-wire/handshake.d.ts +0 -56
  405. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +0 -16
  406. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +0 -17
  407. package/dist/types/modes/shared/agent-wire/protocol.d.ts +0 -25
  408. package/dist/types/modes/shared/agent-wire/responses.d.ts +0 -4
  409. package/dist/types/modes/shared/agent-wire/scopes.d.ts +0 -18
  410. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +0 -25
  411. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +0 -42
  412. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +0 -27
  413. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +0 -29
  414. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +0 -68
  415. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +0 -166
  416. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +0 -127
  417. package/dist/types/notifications/config.d.ts +0 -132
  418. package/src/harness-control-plane/rpc-adapter.ts +0 -290
  419. package/src/modes/bridge/auth.ts +0 -41
  420. package/src/modes/bridge/bridge-client-bridge.ts +0 -47
  421. package/src/modes/bridge/bridge-mode.ts +0 -675
  422. package/src/modes/bridge/bridge-ui-context.ts +0 -200
  423. package/src/modes/bridge/event-stream.ts +0 -70
  424. package/src/modes/rpc/host-tools.ts +0 -1
  425. package/src/modes/rpc/host-uris.ts +0 -1
  426. package/src/modes/rpc/rpc-client.ts +0 -1166
  427. package/src/modes/rpc/rpc-mode.ts +0 -1029
  428. package/src/modes/rpc/rpc-socket-security.ts +0 -115
  429. package/src/modes/rpc/rpc-types.ts +0 -637
  430. package/src/modes/shared/agent-wire/command-contract.ts +0 -18
  431. package/src/modes/shared/agent-wire/command-dispatch.ts +0 -498
  432. package/src/modes/shared/agent-wire/command-validation.ts +0 -181
  433. package/src/modes/shared/agent-wire/handshake.ts +0 -157
  434. package/src/modes/shared/agent-wire/host-tool-bridge.ts +0 -197
  435. package/src/modes/shared/agent-wire/host-uri-bridge.ts +0 -236
  436. package/src/modes/shared/agent-wire/protocol.ts +0 -46
  437. package/src/modes/shared/agent-wire/responses.ts +0 -17
  438. package/src/modes/shared/agent-wire/scopes.ts +0 -94
  439. package/src/modes/shared/agent-wire/session-registry.ts +0 -109
  440. package/src/modes/shared/agent-wire/ui-request-broker.ts +0 -150
  441. package/src/modes/shared/agent-wire/ui-result.ts +0 -48
  442. package/src/modes/shared/agent-wire/unattended-action-policy.ts +0 -365
  443. package/src/modes/shared/agent-wire/unattended-audit.ts +0 -176
  444. package/src/modes/shared/agent-wire/unattended-run-controller.ts +0 -426
  445. package/src/modes/shared/agent-wire/unattended-session.ts +0 -528
  446. package/src/notifications/config.ts +0 -219
  447. package/src/notifications/index.ts +0 -1835
  448. package/src/notifications/telegram-daemon-cli.ts +0 -243
  449. package/src/tools/puppeteer/09_stealth_locale.txt +0 -46
  450. /package/dist/types/{notifications → sdk/bus}/attachment-registry.d.ts +0 -0
  451. /package/dist/types/{notifications → sdk/bus}/chat-adapters.d.ts +0 -0
  452. /package/dist/types/{notifications → sdk/bus}/daemon-paths.d.ts +0 -0
  453. /package/dist/types/{notifications → sdk/bus}/engine.d.ts +0 -0
  454. /package/dist/types/{notifications → sdk/bus}/helpers.d.ts +0 -0
  455. /package/dist/types/{notifications → sdk/bus}/lifecycle-commands.d.ts +0 -0
  456. /package/dist/types/{notifications → sdk/bus}/operator-runtime.d.ts +0 -0
  457. /package/dist/types/{notifications → sdk/bus}/rate-limit-pool.d.ts +0 -0
  458. /package/dist/types/{notifications → sdk/bus}/recent-activity.d.ts +0 -0
  459. /package/dist/types/{notifications → sdk/bus}/reply-sent-store.d.ts +0 -0
  460. /package/dist/types/{notifications → sdk/bus}/rich-draft.d.ts +0 -0
  461. /package/dist/types/{notifications → sdk/bus}/rich-render.d.ts +0 -0
  462. /package/dist/types/{notifications → sdk/bus}/threaded-inbound.d.ts +0 -0
  463. /package/dist/types/{notifications → sdk/bus}/threaded-render.d.ts +0 -0
  464. /package/src/{notifications → sdk/bus}/attachment-registry.ts +0 -0
  465. /package/src/{notifications → sdk/bus}/chat-adapters.ts +0 -0
  466. /package/src/{notifications → sdk/bus}/daemon-paths.ts +0 -0
  467. /package/src/{notifications → sdk/bus}/engine.ts +0 -0
  468. /package/src/{notifications → sdk/bus}/helpers.ts +0 -0
  469. /package/src/{notifications → sdk/bus}/operator-runtime.ts +0 -0
  470. /package/src/{notifications → sdk/bus}/rate-limit-pool.ts +0 -0
  471. /package/src/{notifications → sdk/bus}/recent-activity.ts +0 -0
  472. /package/src/{notifications → sdk/bus}/reply-sent-store.ts +0 -0
  473. /package/src/{notifications → sdk/bus}/rich-draft.ts +0 -0
  474. /package/src/{notifications → sdk/bus}/rich-render.ts +0 -0
  475. /package/src/{notifications → sdk/bus}/threaded-inbound.ts +0 -0
  476. /package/src/{notifications → sdk/bus}/threaded-render.ts +0 -0
@@ -0,0 +1,1379 @@
1
+ import {
2
+ type Component,
3
+ type Focusable,
4
+ getKeybindings,
5
+ Input,
6
+ SecretInput,
7
+ type SecretValue,
8
+ truncateToWidth,
9
+ wrapTextWithAnsi,
10
+ } from "@gajae-code/tui";
11
+ import type { CasReceipt } from "../../config/atomic-yaml-patch";
12
+ import type {
13
+ BlockedTelegramRestoreResult,
14
+ ProposedTelegramIdentity,
15
+ TelegramDaemonReconnectOutcome,
16
+ } from "../../sdk/bus/notification-orchestration";
17
+ import type {
18
+ NotificationHealthReport,
19
+ NotificationRecoveryReport,
20
+ NotificationStatusReport,
21
+ NotificationTestResult,
22
+ } from "../../sdk/bus/notification-service";
23
+ import type { NotificationSessionReconcileResult, NotificationSessionStatus } from "../../sdk/bus/session-control";
24
+ import { theme } from "../theme/theme";
25
+
26
+ /** Safe scalar notification preferences. Credentials and destination IDs are deliberately absent. */
27
+ export interface NotificationsEditorPreferences {
28
+ redact: boolean;
29
+ verbosity: "lean" | "verbose";
30
+ sessionScope: "all" | "primary";
31
+ richEnabled: boolean;
32
+ richDraftEnabled: boolean;
33
+ }
34
+
35
+ /** Secret-safe snapshot used to render the Notifications tab. */
36
+ export interface NotificationsEditorState {
37
+ status: NotificationStatusReport;
38
+ session: NotificationSessionStatus;
39
+ preferences: NotificationsEditorPreferences;
40
+ health?: NotificationHealthReport;
41
+ }
42
+
43
+ /**
44
+ * A one-way setup value accepted from the masked input. The operation adapter
45
+ * consumes the secret and returns a safe-to-render prepared draft.
46
+ */
47
+ export interface NotificationsEditorSetupInput {
48
+ token: SecretValue;
49
+ /** Optional: omit to use daemon-aware private-chat discovery when polling is safe. */
50
+ chatId?: string;
51
+ richEnabled: boolean;
52
+ richDraftEnabled: boolean;
53
+ }
54
+
55
+ /**
56
+ * Secret-safe setup draft. The adapter may retain its private credential while
57
+ * this object is live, but no raw token is exposed through this contract.
58
+ */
59
+ export interface PreparedTelegramConfiguration {
60
+ chatId: string;
61
+ tokenMask: string;
62
+ tokenFingerprint?: string;
63
+ richEnabled: boolean;
64
+ richDraftEnabled: boolean;
65
+ }
66
+
67
+ export interface NotificationsPreflightResult {
68
+ status: "ready" | "aborted" | "cancelled" | "error";
69
+ identity: ProposedTelegramIdentity;
70
+ message: string;
71
+ pairingSource?: "discovered" | "provided" | "reused";
72
+ draft?: PreparedTelegramConfiguration;
73
+ }
74
+
75
+ /** A successful configuration save always carries an opaque CAS receipt. */
76
+ export type NotificationsConfigureCommitResult =
77
+ | { status: "saved"; receipt: CasReceipt; message: string }
78
+ | {
79
+ status: "blocked_identity";
80
+ receipt: CasReceipt;
81
+ message: string;
82
+ restore(): Promise<BlockedTelegramRestoreResult>;
83
+ retainCommitted(): void;
84
+ };
85
+
86
+ export interface NotificationsMutationResult {
87
+ message: string;
88
+ receipt?: CasReceipt;
89
+ }
90
+
91
+ export type NotificationsSaveInactiveResult =
92
+ | { status: "saved_inactive"; receipt: CasReceipt; message: string }
93
+ | { status: "unavailable"; guidance: string };
94
+
95
+ /**
96
+ * Controller-injected boundary for the direct Notifications editor.
97
+ *
98
+ * The component only renders this contract and sends user intent through it:
99
+ * it never reads Settings, starts a daemon, or calls notification services.
100
+ */
101
+ export interface NotificationsEditorOperations {
102
+ /** Combines a buildNotificationStatusReport-shaped status snapshot with the current session-control query. */
103
+ loadState(): Promise<NotificationsEditorState>;
104
+ /** Performs offline health refreshes and a non-cancellable reachability probe when requested. */
105
+ refreshHealth(input: { probe: boolean; signal?: AbortSignal }): Promise<NotificationHealthReport>;
106
+ sendTest(): Promise<NotificationTestResult>;
107
+ recover(): Promise<NotificationRecoveryReport>;
108
+ reconnect(): Promise<TelegramDaemonReconnectOutcome>;
109
+ /** Validates/discovers a Telegram destination and returns a secret-safe prepared draft. */
110
+ preflightProposedIdentity(
111
+ input: NotificationsEditorSetupInput,
112
+ signal: AbortSignal,
113
+ ): Promise<NotificationsPreflightResult>;
114
+ /** Atomically persists a prepared Telegram configuration and returns its CAS-backed receipt. */
115
+ commitConfigure(draft: PreparedTelegramConfiguration): Promise<NotificationsConfigureCommitResult>;
116
+ /** Stores a prepared Telegram configuration inactive when the mixed-adapter rule permits it. */
117
+ saveInactive(draft: PreparedTelegramConfiguration): Promise<NotificationsSaveInactiveResult>;
118
+ /** Clears adapter-local ephemeral credential material for a prepared setup draft. Idempotent. */
119
+ discardConfigureDraft(draft: PreparedTelegramConfiguration): void;
120
+ /** Enables globally without requesting or exposing a credential. */
121
+ enableGlobally(): Promise<NotificationsMutationResult>;
122
+ disableGlobally(): Promise<NotificationsMutationResult>;
123
+ /** Removes only Telegram configuration; the adapter preserves Discord/Slack when configured. */
124
+ removeTelegram(): Promise<NotificationsMutationResult & { globallyDisabled?: boolean }>;
125
+ setSessionLocal(enabled: boolean): Promise<NotificationSessionReconcileResult>;
126
+ /** Atomically persists the scalar preference draft. */
127
+ commitPreferences(preferences: NotificationsEditorPreferences): Promise<NotificationsMutationResult>;
128
+ /** Reconciles the current session after durable global configuration changes. */
129
+ reconcileCurrentSession(): Promise<NotificationSessionReconcileResult>;
130
+ }
131
+
132
+ export interface NotificationsSettingsEditorCallbacks {
133
+ onCancel?: () => void;
134
+ }
135
+
136
+ type EditorMode =
137
+ | "home"
138
+ | "provider-selection"
139
+ | "chat-entry"
140
+ | "token-entry"
141
+ | "pairing"
142
+ | "review"
143
+ | "preferences"
144
+ | "confirmation";
145
+ type ConfirmationAction = "disable" | "remove" | "blocked_identity";
146
+
147
+ type HomeActionId =
148
+ | "configure"
149
+ | "enable"
150
+ | "disable"
151
+ | "session"
152
+ | "refresh"
153
+ | "probe"
154
+ | "test"
155
+ | "recover"
156
+ | "reconnect"
157
+ | "remove"
158
+ | "preferences";
159
+
160
+ interface Action {
161
+ id: HomeActionId | "telegram" | "external" | "save" | "save-inactive" | "cancel" | "confirm";
162
+ label: string;
163
+ description: string;
164
+ }
165
+
166
+ const TOKEN_PATTERN = /\d{6,}:[A-Za-z0-9_-]{20,}/g;
167
+ const VISIBLE_ACTIONS = 5;
168
+
169
+ function emptyState(): NotificationsEditorState {
170
+ return {
171
+ status: {
172
+ enabled: false,
173
+ redact: false,
174
+ verbosity: "lean",
175
+ globallyConfigured: false,
176
+ telegram: { botTokenMasked: "(not set)", channel: undefined, configured: false, tokenFingerprint: undefined },
177
+ discord: { botTokenMasked: "(not set)", channel: undefined, configured: false },
178
+ slack: { botTokenMasked: "(not set)", channel: undefined, configured: false },
179
+ },
180
+ session: {
181
+ eligible: false,
182
+ locallyEnabled: false,
183
+ effectiveEnabled: false,
184
+ running: false,
185
+ environment: "default",
186
+ },
187
+ preferences: {
188
+ redact: false,
189
+ verbosity: "lean",
190
+ sessionScope: "all",
191
+ richEnabled: true,
192
+ richDraftEnabled: false,
193
+ },
194
+ };
195
+ }
196
+
197
+ function statusLabel(level: "ok" | "warn" | "error"): "OK" | "WARNING" | "ERROR" {
198
+ return level === "ok" ? "OK" : level === "warn" ? "WARNING" : "ERROR";
199
+ }
200
+
201
+ function safeDetail(value: string | undefined, fallback: string): string {
202
+ const detail = (value ?? fallback)
203
+ .replace(TOKEN_PATTERN, "<redacted>")
204
+ .replace(/[\r\n]+/g, " ")
205
+ .trim();
206
+ return detail || fallback;
207
+ }
208
+
209
+ function formatAge(ageMs: number | undefined): string {
210
+ if (ageMs === undefined || !Number.isFinite(ageMs) || ageMs < 0) return "unknown";
211
+ if (ageMs < 1_000) return "just now";
212
+ if (ageMs < 60_000) return `${Math.floor(ageMs / 1_000)}s ago`;
213
+ if (ageMs < 3_600_000) return `${Math.floor(ageMs / 60_000)}m ago`;
214
+ return `${Math.floor(ageMs / 3_600_000)}h ago`;
215
+ }
216
+
217
+ function formatTimestamp(timestamp: number | undefined): string {
218
+ if (timestamp === undefined || !Number.isFinite(timestamp) || timestamp < 0) return "unknown";
219
+ return new Date(timestamp).toISOString();
220
+ }
221
+
222
+ function maskedToken(value: string | undefined, fallback: string): string {
223
+ if (!value) return fallback;
224
+ const maskLength = [...value].filter(character => character === "•" || character === "*").length;
225
+ return "•".repeat(Math.max(4, maskLength));
226
+ }
227
+
228
+ function copyPreferences(preferences: NotificationsEditorPreferences): NotificationsEditorPreferences {
229
+ return { ...preferences };
230
+ }
231
+
232
+ /**
233
+ * Direct tab content for notification setup and operations. It is deliberately
234
+ * independent of Settings and notification services; WI7b injects the adapter.
235
+ */
236
+ export class NotificationsSettingsEditorComponent implements Component, Focusable {
237
+ focused = false;
238
+ #state = emptyState();
239
+ #mode: EditorMode = "home";
240
+ #selectedIndex = 0;
241
+ #status = "INFO — Loading notification status…";
242
+ #lastTest: NotificationTestResult | undefined;
243
+ #prepared: PreparedTelegramConfiguration | undefined;
244
+ #preflightIdentity: ProposedTelegramIdentity["status"] | undefined;
245
+ #preferencesDraft: NotificationsEditorPreferences | undefined;
246
+ #confirmation: ConfirmationAction | undefined;
247
+ #blockedCommit: Extract<NotificationsConfigureCommitResult, { status: "blocked_identity" }> | undefined;
248
+ #pairingPhase: "discovery" | "validation" = "discovery";
249
+ #chatInput = new Input();
250
+ #tokenInput = new SecretInput();
251
+ #abortController = new AbortController();
252
+ #cancellableWork: Promise<void> | undefined;
253
+ #guarded = false;
254
+ #disposed = false;
255
+ #loadSequence = 0;
256
+
257
+ constructor(
258
+ private readonly operations: NotificationsEditorOperations,
259
+ private readonly callbacks: NotificationsSettingsEditorCallbacks = {},
260
+ ) {
261
+ this.#chatInput.onSubmit = () => {
262
+ this.#mode = "token-entry";
263
+ this.#status = "INFO — Paste the Telegram bot token. It is masked and never shown again.";
264
+ };
265
+ this.#chatInput.onEscape = () => this.#cancelCurrentMode();
266
+ this.#tokenInput.onSubmit = token => this.#startPreflight(token);
267
+ this.#tokenInput.onEscape = () => this.#cancelCurrentMode();
268
+ void this.#loadState();
269
+ }
270
+
271
+ /** True while a guarded operation or blocked restore/retain decision owns the editor. */
272
+ get navigationLocked(): boolean {
273
+ return this.#guarded || this.#blockedCommit !== undefined;
274
+ }
275
+
276
+ /** Current state is exposed for selector lifecycle coordination and focused tests. */
277
+ get mode(): EditorMode {
278
+ return this.#mode;
279
+ }
280
+
281
+ invalidate(): void {
282
+ this.#chatInput.invalidate();
283
+ this.#tokenInput.invalidate();
284
+ }
285
+
286
+ dispose(): void {
287
+ if (this.#disposed || this.navigationLocked) return;
288
+ this.#disposed = true;
289
+ this.#abortController.abort();
290
+ this.#clearDrafts(true);
291
+ }
292
+
293
+ render(width: number): string[] {
294
+ if (this.#mode === "provider-selection") return this.#renderProviderSelection(width);
295
+ if (this.#mode === "chat-entry") return this.#renderChatEntry(width);
296
+ if (this.#mode === "token-entry") return this.#renderTokenEntry(width);
297
+ if (this.#mode === "pairing") return this.#renderPairing(width);
298
+ if (this.#mode === "review") return this.#renderReview(width);
299
+ if (this.#mode === "preferences") return this.#renderPreferences(width);
300
+ if (this.#mode === "confirmation") return this.#renderConfirmation(width);
301
+ return this.#renderHome(width);
302
+ }
303
+
304
+ handleInput(data: string): void {
305
+ if (this.#disposed) return;
306
+ if (this.#guarded) {
307
+ this.#status =
308
+ "WARNING — Request in progress; it may already have been delivered or started. Navigation is locked.";
309
+ return;
310
+ }
311
+ if (this.#cancellableWork) {
312
+ if (this.#matchesCancel(data)) {
313
+ this.#cancelCancellableWork(
314
+ "Cancellable notification operation cancelled; saved configuration is unchanged.",
315
+ );
316
+ } else {
317
+ this.#status = "PENDING — Cancellable notification operation in progress. Press Esc to cancel it.";
318
+ }
319
+ return;
320
+ }
321
+ if (this.#mode === "chat-entry") {
322
+ this.#chatInput.handleInput(data);
323
+ return;
324
+ }
325
+ if (this.#mode === "token-entry") {
326
+ this.#tokenInput.handleInput(data);
327
+ return;
328
+ }
329
+ if (this.#mode === "pairing") {
330
+ if (this.#matchesCancel(data))
331
+ this.#cancelCancellableWork("Pairing cancelled; no notification configuration was changed.");
332
+ return;
333
+ }
334
+ if (this.#mode === "provider-selection") {
335
+ this.#handleListInput(data, this.#providerActions());
336
+ return;
337
+ }
338
+ if (this.#mode === "review") {
339
+ this.#handleListInput(data, this.#reviewActions());
340
+ return;
341
+ }
342
+ if (this.#mode === "preferences") {
343
+ this.#handlePreferencesInput(data);
344
+ return;
345
+ }
346
+ if (this.#mode === "confirmation") {
347
+ this.#handleConfirmationInput(data);
348
+ return;
349
+ }
350
+ this.#handleListInput(data, this.#homeActions());
351
+ }
352
+
353
+ #matchesCancel(data: string): boolean {
354
+ return getKeybindings().matches(data, "tui.select.cancel");
355
+ }
356
+
357
+ #handleListInput(data: string, actions: readonly Action[]): void {
358
+ if (this.#matchesCancel(data)) {
359
+ this.#cancelCurrentMode();
360
+ return;
361
+ }
362
+ const keybindings = getKeybindings();
363
+ if (keybindings.matches(data, "tui.select.up")) {
364
+ this.#selectedIndex = this.#selectedIndex === 0 ? actions.length - 1 : this.#selectedIndex - 1;
365
+ return;
366
+ }
367
+ if (keybindings.matches(data, "tui.select.down")) {
368
+ this.#selectedIndex = this.#selectedIndex === actions.length - 1 ? 0 : this.#selectedIndex + 1;
369
+ return;
370
+ }
371
+ if (keybindings.matches(data, "tui.select.confirm") || data === " " || data === "\n") {
372
+ const action = actions[this.#selectedIndex];
373
+ if (action) this.#activate(action.id);
374
+ }
375
+ }
376
+
377
+ #handlePreferencesInput(data: string): void {
378
+ const actions = this.#preferenceActions();
379
+ if (this.#matchesCancel(data)) {
380
+ this.#preferencesDraft = undefined;
381
+ this.#mode = "home";
382
+ this.#selectedIndex = 0;
383
+ this.#status = "INFO — Preference changes discarded.";
384
+ return;
385
+ }
386
+ const keybindings = getKeybindings();
387
+ if (keybindings.matches(data, "tui.select.up")) {
388
+ this.#selectedIndex = this.#selectedIndex === 0 ? actions.length - 1 : this.#selectedIndex - 1;
389
+ return;
390
+ }
391
+ if (keybindings.matches(data, "tui.select.down")) {
392
+ this.#selectedIndex = this.#selectedIndex === actions.length - 1 ? 0 : this.#selectedIndex + 1;
393
+ return;
394
+ }
395
+ if (keybindings.matches(data, "tui.select.confirm") || data === " " || data === "\n") {
396
+ const action = actions[this.#selectedIndex];
397
+ if (!action) return;
398
+ if (action.id === "save") {
399
+ this.#savePreferences();
400
+ return;
401
+ }
402
+ if (action.id === "cancel") {
403
+ this.#preferencesDraft = undefined;
404
+ this.#mode = "home";
405
+ this.#selectedIndex = 0;
406
+ this.#status = "INFO — Preference changes discarded.";
407
+ return;
408
+ }
409
+ this.#togglePreference(action.id);
410
+ }
411
+ }
412
+
413
+ #handleConfirmationInput(data: string): void {
414
+ const blocked = this.#confirmation === "blocked_identity";
415
+ if (this.#matchesCancel(data)) {
416
+ if (blocked) this.#retainBlockedConfiguration();
417
+ else {
418
+ this.#confirmation = undefined;
419
+ this.#mode = "home";
420
+ this.#selectedIndex = 0;
421
+ this.#status = "INFO — Confirmation cancelled; configuration is unchanged.";
422
+ }
423
+ return;
424
+ }
425
+ const keybindings = getKeybindings();
426
+ if (keybindings.matches(data, "tui.select.up") || keybindings.matches(data, "tui.select.down")) {
427
+ this.#selectedIndex = this.#selectedIndex === 0 ? 1 : 0;
428
+ return;
429
+ }
430
+ if (keybindings.matches(data, "tui.select.confirm") || data === " " || data === "\n") {
431
+ if (blocked) {
432
+ if (this.#selectedIndex === 0) this.#restoreBlockedConfiguration();
433
+ else this.#retainBlockedConfiguration();
434
+ return;
435
+ }
436
+ if (this.#selectedIndex === 0) this.#confirmDestructiveAction();
437
+ else {
438
+ this.#confirmation = undefined;
439
+ this.#mode = "home";
440
+ this.#selectedIndex = 0;
441
+ this.#status = "INFO — Confirmation cancelled; configuration is unchanged.";
442
+ }
443
+ }
444
+ }
445
+
446
+ #activate(id: Action["id"]): void {
447
+ switch (id) {
448
+ case "configure":
449
+ this.#mode = "provider-selection";
450
+ this.#selectedIndex = 0;
451
+ this.#status = "INFO — Select a notification provider to begin setup.";
452
+ return;
453
+ case "telegram":
454
+ this.#mode = "chat-entry";
455
+ this.#selectedIndex = 0;
456
+ this.#chatInput.setValue("");
457
+ this.#tokenInput.clear();
458
+ this.#status = "INFO — Enter a private chat ID, or leave it blank for guided pairing discovery.";
459
+ return;
460
+ case "external":
461
+ this.#status = "INFO — Discord and Slack credentials are managed in their respective provider settings.";
462
+ return;
463
+ case "enable":
464
+ this.#enableGlobally();
465
+ return;
466
+ case "disable":
467
+ this.#openConfirmation("disable");
468
+ return;
469
+ case "session":
470
+ this.#setSessionLocal(!this.#state.session.locallyEnabled);
471
+ return;
472
+ case "refresh":
473
+ this.#refreshHealth(false);
474
+ return;
475
+ case "probe":
476
+ this.#refreshHealth(true);
477
+ return;
478
+ case "test":
479
+ this.#sendTest();
480
+ return;
481
+ case "recover":
482
+ this.#recover();
483
+ return;
484
+ case "reconnect":
485
+ this.#reconnect();
486
+ return;
487
+ case "remove":
488
+ this.#openConfirmation("remove");
489
+ return;
490
+ case "preferences":
491
+ this.#preferencesDraft = copyPreferences(this.#state.preferences);
492
+ this.#selectedIndex = 0;
493
+ this.#mode = "preferences";
494
+ this.#status = "INFO — Edit the draft, then explicitly save it atomically.";
495
+ return;
496
+ case "save":
497
+ this.#commitConfiguration();
498
+ return;
499
+ case "save-inactive":
500
+ this.#saveInactive();
501
+ return;
502
+ case "cancel":
503
+ this.#cancelCurrentMode();
504
+ return;
505
+ default:
506
+ return;
507
+ }
508
+ }
509
+
510
+ #cancelCurrentMode(): void {
511
+ if (this.#mode === "home") {
512
+ this.callbacks.onCancel?.();
513
+ return;
514
+ }
515
+ if (this.#mode === "pairing") {
516
+ this.#cancelCancellableWork("Pairing cancelled; no notification configuration was changed.");
517
+ return;
518
+ }
519
+ this.#clearDrafts();
520
+ this.#mode = "home";
521
+ this.#selectedIndex = 0;
522
+ this.#status = "INFO — Setup cancelled; saved notification configuration is unchanged.";
523
+ }
524
+
525
+ #clearDrafts(disposeSecret = false): void {
526
+ if (this.#prepared) this.operations.discardConfigureDraft(this.#prepared);
527
+ this.#prepared = undefined;
528
+ this.#preflightIdentity = undefined;
529
+ this.#pairingPhase = "discovery";
530
+ this.#preferencesDraft = undefined;
531
+ this.#chatInput.setValue("");
532
+ this.#tokenInput.clear();
533
+ if (disposeSecret) this.#tokenInput.dispose();
534
+ }
535
+
536
+ #startPreflight(token: SecretValue): void {
537
+ const chatId = this.#chatInput.getValue().trim() || undefined;
538
+ const preferences = this.#state.preferences;
539
+ this.#pairingPhase = chatId ? "validation" : "discovery";
540
+ this.#mode = "pairing";
541
+ this.#status = chatId
542
+ ? "PENDING — Private-chat validation in progress. Escape cancels before configuration changes."
543
+ : "PENDING — Pairing discovery in progress. Escape cancels before configuration changes.";
544
+ this.#runCancellable(
545
+ signal =>
546
+ this.operations.preflightProposedIdentity(
547
+ {
548
+ token,
549
+ chatId,
550
+ richEnabled: preferences.richEnabled,
551
+ richDraftEnabled: preferences.richDraftEnabled,
552
+ },
553
+ signal,
554
+ ),
555
+ result => {
556
+ this.#tokenInput.clear();
557
+ if (result.status === "aborted" || result.status === "cancelled") {
558
+ this.#clearDrafts();
559
+ this.#mode = "home";
560
+ this.#selectedIndex = 0;
561
+ this.#status = "ABORTED — Pairing cancelled; saved notification configuration is unchanged.";
562
+ return;
563
+ }
564
+ if (result.status !== "ready" || !result.draft) {
565
+ this.#clearDrafts();
566
+ this.#mode = "home";
567
+ this.#selectedIndex = 0;
568
+ this.#status = `ERROR — ${safeDetail(result.message, "Telegram setup could not be completed.")}`;
569
+ return;
570
+ }
571
+ this.#prepared = result.draft;
572
+ this.#preflightIdentity = result.identity.status;
573
+ this.#mode = "review";
574
+ this.#selectedIndex = 0;
575
+ const identity = result.identity.status;
576
+ this.#status =
577
+ identity === "foreign" || identity === "unknown"
578
+ ? `WARNING — ${safeDetail(result.message, "A foreign or unknown Telegram daemon identity blocks activation.")}`
579
+ : `OK — ${safeDetail(result.message, "Telegram setup is ready to save.")}`;
580
+ },
581
+ );
582
+ }
583
+
584
+ #cancelCancellableWork(message: string): void {
585
+ this.#abortController.abort();
586
+ this.#clearDrafts();
587
+ this.#mode = "home";
588
+ this.#selectedIndex = 0;
589
+ this.#status = `ABORTED — ${message}`;
590
+ }
591
+
592
+ #runCancellable<T>(work: (signal: AbortSignal) => Promise<T>, complete: (result: T) => void): void {
593
+ if (this.#abortController.signal.aborted) this.#abortController = new AbortController();
594
+ const controller = this.#abortController;
595
+ let pending!: Promise<void>;
596
+ pending = (async () => {
597
+ try {
598
+ const result = await work(controller.signal);
599
+ if (this.#disposed || controller.signal.aborted || controller !== this.#abortController) return;
600
+ complete(result);
601
+ } catch {
602
+ if (this.#disposed || controller.signal.aborted || controller !== this.#abortController) return;
603
+ this.#mode = "home";
604
+ this.#selectedIndex = 0;
605
+ this.#status = "ERROR — Cancellable notification operation failed safely; retry when ready.";
606
+ } finally {
607
+ if (this.#cancellableWork === pending) this.#cancellableWork = undefined;
608
+ }
609
+ })();
610
+ this.#cancellableWork = pending;
611
+ }
612
+
613
+ #refreshHealth(probe: boolean): void {
614
+ if (probe) {
615
+ this.#runGuarded(
616
+ "Health probe in progress. It cannot be cancelled once started.",
617
+ () => this.operations.refreshHealth({ probe: true }),
618
+ health => {
619
+ this.#state = { ...this.#state, health };
620
+ this.#status = `${statusLabel(health.overall)} — ${this.#healthSummary(health)}`;
621
+ },
622
+ );
623
+ return;
624
+ }
625
+ this.#status = "PENDING — Refreshing notification health.";
626
+ this.#runCancellable(
627
+ signal => this.operations.refreshHealth({ probe: false, signal }),
628
+ health => {
629
+ this.#state = { ...this.#state, health };
630
+ this.#status = `${statusLabel(health.overall)} — ${this.#healthSummary(health)}`;
631
+ },
632
+ );
633
+ }
634
+
635
+ #runGuarded<T>(pending: string, work: () => Promise<T>, complete: (result: T) => Promise<void> | void): void {
636
+ if (this.#guarded || this.#disposed) return;
637
+ this.#guarded = true;
638
+ this.#status = `PENDING — ${pending} Navigation is locked; it may already have started or delivered.`;
639
+ void (async () => {
640
+ try {
641
+ const result = await work();
642
+ if (this.#disposed) return;
643
+ await complete(result);
644
+ } catch {
645
+ if (!this.#disposed) {
646
+ this.#status = "ERROR — Notification action failed safely. Retry when ready; no credential is shown.";
647
+ }
648
+ } finally {
649
+ if (!this.#disposed) this.#guarded = false;
650
+ }
651
+ })();
652
+ }
653
+
654
+ async #afterDurableMutation(): Promise<boolean> {
655
+ await this.operations.reconcileCurrentSession();
656
+ const health = await this.operations.refreshHealth({ probe: false });
657
+ if (this.#disposed) return false;
658
+ this.#state = { ...this.#state, health };
659
+ await this.#loadState();
660
+ return !this.#disposed;
661
+ }
662
+
663
+ async #refreshAfterOperation(): Promise<boolean> {
664
+ const health = await this.operations.refreshHealth({ probe: false });
665
+ if (this.#disposed) return false;
666
+ this.#state = { ...this.#state, health };
667
+ await this.#loadState();
668
+ return !this.#disposed;
669
+ }
670
+
671
+ #enableGlobally(): void {
672
+ this.#runGuarded(
673
+ "Enabling global notifications.",
674
+ () => this.operations.enableGlobally(),
675
+ async result => {
676
+ if (!(await this.#afterDurableMutation())) return;
677
+
678
+ this.#status = `OK — ${safeDetail(result.message, "Global notifications enabled using stored credentials.")}`;
679
+ },
680
+ );
681
+ }
682
+
683
+ #confirmDestructiveAction(): void {
684
+ const action = this.#confirmation;
685
+ this.#confirmation = undefined;
686
+ if (action === "disable") {
687
+ this.#runGuarded(
688
+ "Disabling notifications globally.",
689
+ () => this.operations.disableGlobally(),
690
+ async result => {
691
+ if (!(await this.#afterDurableMutation())) return;
692
+ this.#mode = "home";
693
+ this.#selectedIndex = 0;
694
+ this.#status = `OK — ${safeDetail(result.message, "Notifications disabled globally.")}`;
695
+ },
696
+ );
697
+ return;
698
+ }
699
+ if (action === "remove") {
700
+ this.#runGuarded(
701
+ "Removing Telegram configuration.",
702
+ () => this.operations.removeTelegram(),
703
+ async result => {
704
+ if (!(await this.#afterDurableMutation())) return;
705
+ this.#mode = "home";
706
+ this.#selectedIndex = 0;
707
+ this.#status = `OK — ${safeDetail(
708
+ result.message,
709
+ result.globallyDisabled
710
+ ? "Telegram removed and global notifications disabled."
711
+ : "Telegram removed; other adapters remain enabled.",
712
+ )}`;
713
+ },
714
+ );
715
+ }
716
+ }
717
+
718
+ #restoreBlockedConfiguration(): void {
719
+ const blocked = this.#blockedCommit;
720
+ if (!blocked) return;
721
+ this.#runGuarded(
722
+ "Restoring the previous Telegram configuration.",
723
+ () => blocked.restore(),
724
+ async result => {
725
+ this.#blockedCommit = undefined;
726
+ this.#confirmation = undefined;
727
+ if (!(await this.#refreshAfterOperation())) return;
728
+ this.#mode = "home";
729
+ this.#selectedIndex = 0;
730
+ switch (result.status) {
731
+ case "restored":
732
+ this.#status = "OK — Previous Telegram configuration restored after the blocked activation.";
733
+ return;
734
+ case "conflict":
735
+ this.#status = `ERROR — Previous configuration was not restored because settings changed at: ${result.paths.join(", ")}. Saved configuration remains inactive.`;
736
+ return;
737
+ case "still_blocked":
738
+ this.#status =
739
+ "ERROR — Previous configuration restored, but activation remains blocked by a foreign daemon. Current session remains inactive.";
740
+ return;
741
+ case "discarded":
742
+ this.#status =
743
+ "WARNING — Previous configuration was not restored. Saved configuration remains inactive.";
744
+ return;
745
+ }
746
+ },
747
+ );
748
+ }
749
+
750
+ #retainBlockedConfiguration(): void {
751
+ const blocked = this.#blockedCommit;
752
+ if (!blocked) return;
753
+ this.#runGuarded(
754
+ "Keeping saved Telegram configuration inactive.",
755
+ async () => blocked.retainCommitted(),
756
+ async () => {
757
+ this.#blockedCommit = undefined;
758
+ this.#confirmation = undefined;
759
+ if (!(await this.#refreshAfterOperation())) return;
760
+ this.#mode = "home";
761
+ this.#selectedIndex = 0;
762
+ this.#status =
763
+ "WARNING — Configuration saved but activation blocked by a foreign daemon. Saved configuration remains inactive.";
764
+ },
765
+ );
766
+ }
767
+
768
+ #openConfirmation(action: Exclude<ConfirmationAction, "blocked_identity">): void {
769
+ this.#confirmation = action;
770
+ this.#mode = "confirmation";
771
+ this.#selectedIndex = 1;
772
+ this.#status =
773
+ action === "disable"
774
+ ? "WARNING — Disable globally stops configured adapters. Confirm explicitly to continue."
775
+ : "WARNING — Remove only Telegram credentials. Discord and Slack remain unchanged.";
776
+ }
777
+
778
+ #setSessionLocal(enabled: boolean): void {
779
+ this.#runGuarded(
780
+ enabled ? "Enabling notifications for this session." : "Disabling notifications for this session.",
781
+ () => this.operations.setSessionLocal(enabled),
782
+ async result => {
783
+ if (!(await this.#refreshAfterOperation())) return;
784
+
785
+ if (!result.status.eligible) {
786
+ this.#status = "WARNING — Session notification controls are unavailable in this host session.";
787
+ return;
788
+ }
789
+ this.#status = `OK — Session notifications ${result.status.locallyEnabled ? "enabled" : "disabled"}; runtime is ${
790
+ result.status.running ? "active" : "inactive"
791
+ }.`;
792
+ },
793
+ );
794
+ }
795
+
796
+ #sendTest(): void {
797
+ this.#runGuarded(
798
+ "Sending a notification test.",
799
+ () => this.operations.sendTest(),
800
+ async result => {
801
+ this.#lastTest = result;
802
+ if (!(await this.#refreshAfterOperation())) return;
803
+
804
+ this.#status = `${result.ok ? "OK" : "ERROR"} — Test ${result.ok ? "delivered" : "failed"}: ${safeDetail(
805
+ result.detail,
806
+ "No delivery detail returned.",
807
+ )}`;
808
+ },
809
+ );
810
+ }
811
+
812
+ #recover(): void {
813
+ this.#runGuarded(
814
+ "Recovering notification delivery.",
815
+ () => this.operations.recover(),
816
+ async result => {
817
+ if (!(await this.#refreshAfterOperation())) return;
818
+
819
+ this.#status = `OK — Recovery scanned ${result.endpointsScanned} endpoint(s); removed ${result.endpointsRemoved.length}. ${safeDetail(
820
+ result.daemon.detail,
821
+ "Daemon recovery completed.",
822
+ )}`;
823
+ },
824
+ );
825
+ }
826
+
827
+ #reconnect(): void {
828
+ this.#runGuarded(
829
+ "Reconnecting the Telegram runtime.",
830
+ () => this.operations.reconnect(),
831
+ async result => {
832
+ if (!(await this.#refreshAfterOperation())) return;
833
+
834
+ this.#status =
835
+ result === "blocked_identity"
836
+ ? "ERROR — Telegram activation is blocked by a foreign daemon. Current session is stopped; foreign daemon untouched."
837
+ : `OK — Telegram daemon ${result}.`;
838
+ },
839
+ );
840
+ }
841
+
842
+ #commitConfiguration(): void {
843
+ const draft = this.#prepared;
844
+ if (!draft) {
845
+ this.#mode = "home";
846
+ this.#status = "ERROR — The setup draft expired. Re-enter the masked Telegram token.";
847
+ return;
848
+ }
849
+ this.#runGuarded(
850
+ "Saving Telegram configuration.",
851
+ () => this.operations.commitConfigure(draft),
852
+ async result => {
853
+ this.#clearDrafts();
854
+ if (result.status === "blocked_identity") {
855
+ this.#blockedCommit = result;
856
+ if (!(await this.#refreshAfterOperation())) return;
857
+ this.#confirmation = "blocked_identity";
858
+ this.#mode = "confirmation";
859
+ this.#selectedIndex = 0;
860
+ this.#status = `ERROR — ${safeDetail(
861
+ result.message,
862
+ "Configuration saved but activation blocked by a foreign daemon.",
863
+ )}`;
864
+ return;
865
+ }
866
+ if (!(await this.#afterDurableMutation())) return;
867
+ this.#mode = "home";
868
+ this.#selectedIndex = 0;
869
+ this.#status = `OK — ${safeDetail(result.message, "Telegram configuration saved and reconciled.")}`;
870
+ },
871
+ );
872
+ }
873
+
874
+ #saveInactive(): void {
875
+ const draft = this.#prepared;
876
+ if (!draft) return;
877
+ this.#runGuarded(
878
+ "Saving Telegram configuration inactive for later.",
879
+ () => this.operations.saveInactive(draft),
880
+ async result => {
881
+ if (result.status === "unavailable") {
882
+ this.#mode = "review";
883
+ this.#status = `WARNING — ${safeDetail(result.guidance, "Save inactive is unavailable while another adapter is globally enabled.")}`;
884
+ return;
885
+ }
886
+ if (!(await this.#afterDurableMutation())) return;
887
+
888
+ this.#clearDrafts();
889
+ this.#mode = "home";
890
+ this.#selectedIndex = 0;
891
+ this.#status = `OK — ${safeDetail(result.message, "Telegram configuration saved inactive; foreign daemon untouched.")}`;
892
+ },
893
+ );
894
+ }
895
+
896
+ #savePreferences(): void {
897
+ const preferences = this.#preferencesDraft;
898
+ if (!preferences) return;
899
+ this.#runGuarded(
900
+ "Saving notification preferences.",
901
+ () => this.operations.commitPreferences(preferences),
902
+ async result => {
903
+ if (!(await this.#afterDurableMutation())) return;
904
+ this.#preferencesDraft = undefined;
905
+ this.#mode = "home";
906
+ this.#selectedIndex = 0;
907
+ this.#status = `OK — ${safeDetail(result.message, "Notification preferences saved atomically.")}`;
908
+ },
909
+ );
910
+ }
911
+
912
+ #togglePreference(id: Action["id"]): void {
913
+ const draft = this.#preferencesDraft;
914
+ if (!draft) return;
915
+ switch (id) {
916
+ case "configure":
917
+ draft.redact = !draft.redact;
918
+ return;
919
+ case "enable":
920
+ draft.verbosity = draft.verbosity === "lean" ? "verbose" : "lean";
921
+ return;
922
+ case "disable":
923
+ draft.sessionScope = draft.sessionScope === "all" ? "primary" : "all";
924
+ return;
925
+ case "session":
926
+ draft.richEnabled = !draft.richEnabled;
927
+ return;
928
+ case "refresh":
929
+ draft.richDraftEnabled = !draft.richDraftEnabled;
930
+ return;
931
+ default:
932
+ return;
933
+ }
934
+ }
935
+
936
+ #providerActions(): readonly Action[] {
937
+ return [
938
+ {
939
+ id: "telegram",
940
+ label: "Telegram",
941
+ description: "Configure a masked bot token and optionally validate or discover a private-chat destination.",
942
+ },
943
+ {
944
+ id: "external",
945
+ label: "Discord (managed elsewhere)",
946
+ description:
947
+ "Discord credentials are configured by the Discord provider integration, not this Telegram setup flow.",
948
+ },
949
+ {
950
+ id: "external",
951
+ label: "Slack (managed elsewhere)",
952
+ description:
953
+ "Slack credentials are configured by the Slack provider integration, not this Telegram setup flow.",
954
+ },
955
+ ];
956
+ }
957
+
958
+ #homeActions(): readonly Action[] {
959
+ return [
960
+ {
961
+ id: "configure",
962
+ label: this.#state.status.telegram.configured ? "Reconfigure Telegram" : "Configure Telegram",
963
+ description:
964
+ "Enter a masked Telegram credential and optionally a private-chat destination; guided discovery is available.",
965
+ },
966
+ {
967
+ id: "enable",
968
+ label: "Enable globally",
969
+ description: "Use stored complete credentials without showing a token.",
970
+ },
971
+ {
972
+ id: "disable",
973
+ label: "Disable globally",
974
+ description: "Stops all globally configured notification adapters after confirmation.",
975
+ },
976
+ {
977
+ id: "session",
978
+ label: this.#state.session.locallyEnabled
979
+ ? "Turn session notifications off"
980
+ : "Turn session notifications on",
981
+ description: "Changes only the current session-local notification gate.",
982
+ },
983
+ {
984
+ id: "refresh",
985
+ label: "Refresh health",
986
+ description: "Read the current local health and daemon ownership state.",
987
+ },
988
+ {
989
+ id: "probe",
990
+ label: "Probe health",
991
+ description: "Optionally check Telegram reachability; once started, the probe runs to completion.",
992
+ },
993
+ {
994
+ id: "test",
995
+ label: "Send test notification",
996
+ description: "Sends one test; it may already be delivered once started.",
997
+ },
998
+ {
999
+ id: "recover",
1000
+ label: "Recover notification delivery",
1001
+ description: "Safely remove only dead-owner artifacts.",
1002
+ },
1003
+ {
1004
+ id: "reconnect",
1005
+ label: "Reconnect Telegram runtime",
1006
+ description: "Reconcile the daemon identity without disturbing a foreign owner.",
1007
+ },
1008
+ {
1009
+ id: "remove",
1010
+ label: "Remove Telegram",
1011
+ description: "Removes Telegram only; Discord and Slack remain unchanged.",
1012
+ },
1013
+ {
1014
+ id: "preferences",
1015
+ label: "Notification preferences",
1016
+ description: "Draft safe scalar preferences, then save them atomically.",
1017
+ },
1018
+ ];
1019
+ }
1020
+
1021
+ #reviewActions(): readonly Action[] {
1022
+ const foreignOrUnknown = this.#preflightIdentity === "foreign" || this.#preflightIdentity === "unknown";
1023
+ if (foreignOrUnknown) {
1024
+ return [
1025
+ {
1026
+ id: "save-inactive",
1027
+ label: "Save inactive for later",
1028
+ description: "Store Telegram only when no enabled Discord or Slack adapter would be disabled.",
1029
+ },
1030
+ { id: "cancel", label: "Cancel setup", description: "Discard this transient setup draft." },
1031
+ ];
1032
+ }
1033
+ return [
1034
+ {
1035
+ id: "save",
1036
+ label: "Save configuration",
1037
+ description: "Atomically save this reviewed Telegram configuration.",
1038
+ },
1039
+ { id: "cancel", label: "Cancel setup", description: "Discard this transient setup draft." },
1040
+ ];
1041
+ }
1042
+
1043
+ #preferenceActions(): readonly Action[] {
1044
+ const draft = this.#preferencesDraft ?? this.#state.preferences;
1045
+ return [
1046
+ {
1047
+ id: "configure",
1048
+ label: `Redact notification content: ${draft.redact ? "on" : "off"}`,
1049
+ description: "Toggle the unsaved redaction preference.",
1050
+ },
1051
+ {
1052
+ id: "enable",
1053
+ label: `Notification verbosity: ${draft.verbosity}`,
1054
+ description: "Toggle between lean and verbose drafts.",
1055
+ },
1056
+ {
1057
+ id: "disable",
1058
+ label: `Session scope: ${draft.sessionScope}`,
1059
+ description: "Toggle between all and primary sessions.",
1060
+ },
1061
+ {
1062
+ id: "session",
1063
+ label: `Telegram rich messages: ${draft.richEnabled ? "on" : "off"}`,
1064
+ description: "Toggle the unsaved rich-message preference.",
1065
+ },
1066
+ {
1067
+ id: "refresh",
1068
+ label: `Telegram rich drafts: ${draft.richDraftEnabled ? "on" : "off"}`,
1069
+ description: "Toggle the unsaved rich-draft preference.",
1070
+ },
1071
+ { id: "save", label: "Save preferences", description: "Atomically persist this preference draft." },
1072
+ { id: "cancel", label: "Cancel and discard draft", description: "Leave saved preferences unchanged." },
1073
+ ];
1074
+ }
1075
+
1076
+ #renderHome(width: number): string[] {
1077
+ const lines = this.#renderSummary(width);
1078
+ lines.push("");
1079
+ this.#renderActionList(lines, width, this.#homeActions());
1080
+ return lines;
1081
+ }
1082
+
1083
+ #renderProviderSelection(width: number): string[] {
1084
+ const lines = [theme.bold(theme.fg("accent", "Choose a notification provider"))];
1085
+ this.#appendWrapped(
1086
+ lines,
1087
+ "Select a provider to configure. Telegram setup uses a masked token and an optional private-chat ID; Discord and Slack credentials are managed by their provider integrations.",
1088
+ width,
1089
+ "muted",
1090
+ );
1091
+ lines.push("");
1092
+ this.#renderActionList(lines, width, this.#providerActions());
1093
+ return lines;
1094
+ }
1095
+
1096
+ #renderChatEntry(width: number): string[] {
1097
+ const lines = [theme.bold(theme.fg("accent", "Telegram setup — private chat ID (optional)"))];
1098
+ this.#appendWrapped(
1099
+ lines,
1100
+ "Enter a Telegram private chat ID to validate it, or leave this blank for guided private-chat discovery when polling is safe. A live same-token daemon reuses and validates its stored chat without polling.",
1101
+ width,
1102
+ "muted",
1103
+ );
1104
+ lines.push("");
1105
+ this.#chatInput.focused = this.focused;
1106
+ for (const line of this.#chatInput.render(width)) lines.push(line);
1107
+ lines.push("");
1108
+ this.#appendStatus(lines, width);
1109
+ lines.push(theme.fg("dim", " Enter to continue · Esc to cancel setup"));
1110
+ return lines;
1111
+ }
1112
+
1113
+ #renderTokenEntry(width: number): string[] {
1114
+ const lines = [theme.bold(theme.fg("accent", "Telegram setup — masked bot token"))];
1115
+ this.#appendWrapped(
1116
+ lines,
1117
+ "Paste the bot token. Only bullets render; the saved token is never prefilled or displayed.",
1118
+ width,
1119
+ "muted",
1120
+ );
1121
+ lines.push("");
1122
+ this.#tokenInput.focused = this.focused;
1123
+ for (const line of this.#tokenInput.render(width)) lines.push(line);
1124
+ lines.push("");
1125
+ this.#appendStatus(lines, width);
1126
+ lines.push(theme.fg("dim", " Enter to validate · Esc to discard setup"));
1127
+ return lines;
1128
+ }
1129
+
1130
+ #renderPairing(width: number): string[] {
1131
+ const validating = this.#pairingPhase === "validation";
1132
+ const lines = [
1133
+ theme.bold(
1134
+ theme.fg(
1135
+ "accent",
1136
+ validating ? "Telegram setup — private-chat validation" : "Telegram setup — pairing discovery",
1137
+ ),
1138
+ ),
1139
+ ];
1140
+ this.#appendWrapped(
1141
+ lines,
1142
+ validating
1143
+ ? "Validating the supplied private-chat destination. This is cancellable and has not changed configuration."
1144
+ : "Discovering a private chat when polling is safe. A live same-token daemon reuses and validates its stored chat without polling. This is cancellable and has not changed configuration.",
1145
+ width,
1146
+ "muted",
1147
+ );
1148
+ lines.push("");
1149
+ this.#appendStatus(lines, width);
1150
+ lines.push(theme.fg("dim", " Esc to cancel pairing · No configuration is saved until review confirms it"));
1151
+ return lines;
1152
+ }
1153
+
1154
+ #renderReview(width: number): string[] {
1155
+ const draft = this.#prepared;
1156
+ const lines = [theme.bold(theme.fg("accent", "Review Telegram notification setup"))];
1157
+ lines.push(
1158
+ this.#truncate(
1159
+ ` Token: ${maskedToken(draft?.tokenMask, "(expired)")} fingerprint: ${draft?.tokenFingerprint ?? "(not available)"}`,
1160
+ width,
1161
+ ),
1162
+ );
1163
+ lines.push(
1164
+ this.#truncate(
1165
+ ` Private chat: ${draft?.chatId ?? "(expired)"} rich: ${draft?.richEnabled ? "on" : "off"} drafts: ${draft?.richDraftEnabled ? "on" : "off"}`,
1166
+ width,
1167
+ ),
1168
+ );
1169
+ lines.push("");
1170
+ this.#renderActionList(lines, width, this.#reviewActions());
1171
+ return lines;
1172
+ }
1173
+
1174
+ #renderPreferences(width: number): string[] {
1175
+ const lines = [theme.bold(theme.fg("accent", "Notification preferences — unsaved draft"))];
1176
+ this.#appendWrapped(
1177
+ lines,
1178
+ "These safe scalar changes do not affect settings until Save preferences completes atomically.",
1179
+ width,
1180
+ "muted",
1181
+ );
1182
+ lines.push("");
1183
+ this.#renderActionList(lines, width, this.#preferenceActions());
1184
+ return lines;
1185
+ }
1186
+
1187
+ #renderConfirmation(width: number): string[] {
1188
+ const blocked = this.#confirmation === "blocked_identity";
1189
+ const removing = this.#confirmation === "remove";
1190
+ const lines = [
1191
+ theme.bold(
1192
+ theme.fg(
1193
+ "accent",
1194
+ blocked
1195
+ ? "Telegram activation blocked by foreign daemon"
1196
+ : removing
1197
+ ? "Remove Telegram configuration?"
1198
+ : "Disable notifications globally?",
1199
+ ),
1200
+ ),
1201
+ ];
1202
+ this.#appendWrapped(
1203
+ lines,
1204
+ blocked
1205
+ ? "Configuration saved but activation blocked by a foreign daemon. Restore the CAS-protected previous configuration, or keep the saved configuration inactive."
1206
+ : removing
1207
+ ? "This removes only Telegram credentials. Configured Discord and Slack adapters remain unchanged."
1208
+ : "This disables all globally configured notification adapters. It does not change a session-local preference.",
1209
+ width,
1210
+ "muted",
1211
+ );
1212
+ lines.push("");
1213
+ this.#renderActionList(
1214
+ lines,
1215
+ width,
1216
+ blocked
1217
+ ? [
1218
+ {
1219
+ id: "confirm",
1220
+ label: "Restore previous configuration",
1221
+ description: "Safest default: restore only if the CAS receipt still matches.",
1222
+ },
1223
+ {
1224
+ id: "cancel",
1225
+ label: "Keep saved (inactive)",
1226
+ description: "Keep the saved configuration while this session remains blocked from activation.",
1227
+ },
1228
+ ]
1229
+ : [
1230
+ { id: "confirm", label: "Confirm", description: "Apply this explicit destructive action." },
1231
+ { id: "cancel", label: "Cancel", description: "Return without changing configuration." },
1232
+ ],
1233
+ );
1234
+ return lines;
1235
+ }
1236
+
1237
+ #renderSummary(width: number): string[] {
1238
+ const { status, session, health } = this.#state;
1239
+ const lines = [theme.bold(theme.fg("accent", "Notifications"))];
1240
+ lines.push(
1241
+ this.#truncate(
1242
+ ` Global: ${status.enabled ? "enabled" : "disabled"} · configured: ${status.globallyConfigured ? "yes" : "no"} · Telegram: ${
1243
+ status.telegram.configured ? "configured" : "not configured"
1244
+ }`,
1245
+ width,
1246
+ ),
1247
+ );
1248
+ lines.push(
1249
+ this.#truncate(
1250
+ ` Session: ${session.effectiveEnabled ? "ACTIVE" : "inactive"} · local: ${session.locallyEnabled ? "on" : "off"} · runtime: ${
1251
+ session.running ? "running" : "stopped"
1252
+ } · environment: ${session.environment}`,
1253
+ width,
1254
+ ),
1255
+ );
1256
+ if (width >= 100) {
1257
+ lines.push(
1258
+ this.#truncate(
1259
+ ` Telegram identity: ${status.telegram.botTokenMasked} · fingerprint: ${status.telegram.tokenFingerprint ?? "(not set)"} · chat: ${
1260
+ status.telegram.channel ?? "(not set)"
1261
+ }`,
1262
+ width,
1263
+ ),
1264
+ );
1265
+ }
1266
+ if (health && width >= 120) {
1267
+ lines.push(
1268
+ this.#truncate(
1269
+ ` Health: ${statusLabel(health.overall)} · heartbeat: ${formatTimestamp(health.daemon.heartbeatAt)} (${formatAge(
1270
+ health.daemon.heartbeatAgeMs,
1271
+ )})`,
1272
+ width,
1273
+ ),
1274
+ );
1275
+ lines.push(
1276
+ this.#truncate(
1277
+ ` Daemon: ${health.daemon.ownerId ?? "none"} pid ${health.daemon.pid ?? "unknown"} · generation ${
1278
+ health.daemon.generation ?? "unknown"
1279
+ }/${health.daemon.currentGeneration} (${health.daemon.generationRelation})`,
1280
+ width,
1281
+ ),
1282
+ );
1283
+ lines.push(
1284
+ this.#truncate(
1285
+ ` Endpoints: ${health.endpoints.total} total, ${health.endpoints.live} live, ${health.endpoints.dead} dead, ${
1286
+ health.endpoints.unknown
1287
+ } unknown · reachability: ${health.reachability.probed ? (health.reachability.ok ? "OK" : "ERROR") : "not probed"}`,
1288
+ width,
1289
+ ),
1290
+ );
1291
+ }
1292
+ if (this.#lastTest && width >= 120) {
1293
+ lines.push(
1294
+ this.#truncate(
1295
+ ` Last in-editor test: ${this.#lastTest.ok ? "OK" : "ERROR"} — ${safeDetail(this.#lastTest.detail, "No detail returned.")}`,
1296
+ width,
1297
+ ),
1298
+ );
1299
+ }
1300
+ return lines;
1301
+ }
1302
+
1303
+ #renderActionList(lines: string[], width: number, actions: readonly Action[]): void {
1304
+ if (actions.length === 0) return;
1305
+ this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, actions.length - 1));
1306
+ const start = Math.max(
1307
+ 0,
1308
+ Math.min(this.#selectedIndex - Math.floor(VISIBLE_ACTIONS / 2), actions.length - VISIBLE_ACTIONS),
1309
+ );
1310
+ const end = Math.min(actions.length, start + VISIBLE_ACTIONS);
1311
+ for (let index = start; index < end; index += 1) {
1312
+ const action = actions[index];
1313
+ if (!action) continue;
1314
+ const selected = index === this.#selectedIndex;
1315
+ const prefix = selected ? `${theme.symbol("nav.cursor")} ` : " ";
1316
+ const label = selected ? theme.bold(theme.fg("accent", action.label)) : theme.fg("text", action.label);
1317
+ lines.push(this.#truncate(prefix + label, width));
1318
+ }
1319
+ if (start > 0 || end < actions.length)
1320
+ lines.push(theme.fg("dim", this.#truncate(` (${this.#selectedIndex + 1}/${actions.length})`, width)));
1321
+ const selected = actions[this.#selectedIndex];
1322
+ if (selected) this.#appendWrapped(lines, selected.description, width, "dim", 1);
1323
+ this.#appendStatus(lines, width);
1324
+ lines.push(theme.fg("dim", this.#truncate(" Enter/Space action · ↑↓ move · Esc cancel", width)));
1325
+ }
1326
+
1327
+ #appendStatus(lines: string[], width: number): void {
1328
+ const separator = " — ";
1329
+ const separatorIndex = this.#status.indexOf(separator);
1330
+ const label = separatorIndex === -1 ? this.#status : this.#status.slice(0, separatorIndex);
1331
+ const guidance = separatorIndex === -1 ? "" : this.#status.slice(separatorIndex + separator.length).trim();
1332
+ const color = this.#status.startsWith("OK")
1333
+ ? "success"
1334
+ : this.#status.startsWith("WARNING") || this.#status.startsWith("ABORTED")
1335
+ ? "warning"
1336
+ : this.#status.startsWith("ERROR")
1337
+ ? "error"
1338
+ : "muted";
1339
+
1340
+ lines.push(theme.fg(color, this.#truncate(` ${label}`, width)));
1341
+ if (!guidance) return;
1342
+ for (const line of wrapTextWithAnsi(guidance, Math.max(1, width - 4))) {
1343
+ lines.push(theme.fg(color, ` ${line}`));
1344
+ }
1345
+ }
1346
+
1347
+ #appendWrapped(
1348
+ lines: string[],
1349
+ text: string,
1350
+ width: number,
1351
+ color: "muted" | "dim",
1352
+ maxLines = Number.POSITIVE_INFINITY,
1353
+ ): void {
1354
+ const wrapped = wrapTextWithAnsi(text, Math.max(1, width - 4));
1355
+ for (const line of wrapped.slice(0, maxLines)) lines.push(theme.fg(color, ` ${line}`));
1356
+ }
1357
+
1358
+ #truncate(text: string, width: number): string {
1359
+ return truncateToWidth(text, Math.max(1, width));
1360
+ }
1361
+
1362
+ #healthSummary(health: NotificationHealthReport): string {
1363
+ const first = health.checks.find(check => check.level === health.overall) ?? health.checks[0];
1364
+ return safeDetail(first?.detail, "Health refreshed.");
1365
+ }
1366
+
1367
+ async #loadState(): Promise<void> {
1368
+ const sequence = ++this.#loadSequence;
1369
+ try {
1370
+ const state = await this.operations.loadState();
1371
+ if (this.#disposed || sequence !== this.#loadSequence) return;
1372
+ this.#state = state;
1373
+ if (state.health) this.#status = `${statusLabel(state.health.overall)} — ${this.#healthSummary(state.health)}`;
1374
+ } catch {
1375
+ if (this.#disposed || sequence !== this.#loadSequence) return;
1376
+ this.#status = "WARNING — Notification status is temporarily unavailable; refresh health to retry.";
1377
+ }
1378
+ }
1379
+ }