@phuetz/code-buddy 1.1.0 → 1.2.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 (291) hide show
  1. package/README.md +160 -165
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +69 -14
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/middleware/index.d.ts +1 -0
  20. package/dist/agent/middleware/index.js +1 -0
  21. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  22. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  23. package/dist/agent/model-benchmark.d.ts +77 -0
  24. package/dist/agent/model-benchmark.js +309 -0
  25. package/dist/agent/model-tier.d.ts +14 -0
  26. package/dist/agent/model-tier.js +71 -0
  27. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  28. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  29. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  30. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  31. package/dist/agent/multi-agent/session-tools.js +22 -5
  32. package/dist/agent/reasoning/mcts.js +20 -6
  33. package/dist/agent/session-end-flush.d.ts +6 -1
  34. package/dist/agent/session-end-flush.js +20 -4
  35. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  36. package/dist/agent/streaming/streaming-handler.js +8 -1
  37. package/dist/agent/tool-executor.js +1 -0
  38. package/dist/agent/tool-handler.d.ts +5 -0
  39. package/dist/agent/tool-handler.js +130 -4
  40. package/dist/browser-automation/browser-operator-executor.js +15 -0
  41. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  42. package/dist/browser-automation/browser-use-runner.js +492 -0
  43. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  44. package/dist/browser-automation/camofox-runner.js +287 -0
  45. package/dist/channels/dingtalk/index.js +3 -0
  46. package/dist/channels/discord/client.d.ts +9 -0
  47. package/dist/channels/discord/client.js +12 -0
  48. package/dist/channels/feishu/index.d.ts +167 -1
  49. package/dist/channels/feishu/index.js +432 -7
  50. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  51. package/dist/channels/gateway-lifecycle.js +219 -0
  52. package/dist/channels/google-chat/index.d.ts +5 -1
  53. package/dist/channels/google-chat/index.js +5 -1
  54. package/dist/channels/imessage/index.d.ts +14 -0
  55. package/dist/channels/imessage/index.js +64 -17
  56. package/dist/channels/index.d.ts +4 -0
  57. package/dist/channels/index.js +4 -0
  58. package/dist/channels/irc/index.d.ts +73 -3
  59. package/dist/channels/irc/index.js +446 -11
  60. package/dist/channels/line/index.js +3 -0
  61. package/dist/channels/mattermost/index.d.ts +51 -4
  62. package/dist/channels/mattermost/index.js +303 -20
  63. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  64. package/dist/channels/nextcloud-talk/index.js +367 -16
  65. package/dist/channels/nostr/index.d.ts +121 -1
  66. package/dist/channels/nostr/index.js +396 -8
  67. package/dist/channels/ntfy/index.js +3 -0
  68. package/dist/channels/qq/index.js +3 -0
  69. package/dist/channels/slash-parity.d.ts +117 -0
  70. package/dist/channels/slash-parity.js +185 -0
  71. package/dist/channels/synology-chat/index.js +3 -0
  72. package/dist/channels/teams/index.d.ts +3 -0
  73. package/dist/channels/teams/index.js +3 -0
  74. package/dist/channels/telegram/client.d.ts +9 -0
  75. package/dist/channels/telegram/client.js +12 -0
  76. package/dist/channels/twilio-voice/index.js +3 -0
  77. package/dist/channels/webchat/index.d.ts +7 -1
  78. package/dist/channels/webchat/index.js +7 -1
  79. package/dist/channels/wecom/index.js +3 -0
  80. package/dist/channels/weixin/index.js +3 -0
  81. package/dist/channels/zalo/index.js +3 -0
  82. package/dist/cli/config-loader.js +1 -1
  83. package/dist/codebuddy/client.d.ts +30 -1
  84. package/dist/codebuddy/client.js +167 -20
  85. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  86. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  87. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  88. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  90. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  91. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  92. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  93. package/dist/codebuddy/tool-definitions/index.js +2 -1
  94. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  95. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  96. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  97. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  98. package/dist/codebuddy/tools.d.ts +1 -1
  99. package/dist/codebuddy/tools.js +27 -24
  100. package/dist/commands/cli/hermes-commands.js +108 -0
  101. package/dist/commands/cli/native-engine-commands.js +102 -3
  102. package/dist/commands/cli/utility-commands.js +45 -0
  103. package/dist/commands/enhanced-command-handler.js +1 -1
  104. package/dist/commands/goal-cli.d.ts +30 -0
  105. package/dist/commands/goal-cli.js +210 -27
  106. package/dist/commands/handlers/goal-handler.d.ts +9 -2
  107. package/dist/commands/handlers/goal-handler.js +36 -9
  108. package/dist/commands/handlers/infra-handlers.js +36 -0
  109. package/dist/commands/handlers/memory-handlers.js +96 -1
  110. package/dist/commands/headless-slash.d.ts +2 -0
  111. package/dist/commands/headless-slash.js +12 -0
  112. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  113. package/dist/commands/llm-provider-resolution.js +87 -33
  114. package/dist/commands/ollama.d.ts +25 -0
  115. package/dist/commands/ollama.js +100 -0
  116. package/dist/commands/provider.d.ts +5 -0
  117. package/dist/commands/provider.js +106 -55
  118. package/dist/commands/spec-next.js +2 -1
  119. package/dist/commands/spec-plan.js +5 -16
  120. package/dist/commands/tunnel.d.ts +2 -0
  121. package/dist/commands/tunnel.js +48 -0
  122. package/dist/config/config-resolver.d.ts +2 -1
  123. package/dist/config/config-resolver.js +54 -41
  124. package/dist/config/constants.d.ts +28 -0
  125. package/dist/config/constants.js +7 -0
  126. package/dist/config/env-schema.js +756 -0
  127. package/dist/config/model-tools.js +4 -4
  128. package/dist/config/toml-config.d.ts +16 -0
  129. package/dist/config/toml-config.js +3 -0
  130. package/dist/context/context-manager-v2.js +1 -0
  131. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  132. package/dist/daemon/autonomous-daemon.js +3 -3
  133. package/dist/daemon/autonomous-loop.js +11 -2
  134. package/dist/daemon/colab-goal.js +9 -1
  135. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  136. package/dist/daemon/cron-agent-bridge.js +25 -9
  137. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  138. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  139. package/dist/desktop/engine-adapter.d.ts +14 -0
  140. package/dist/desktop-automation/automation-manager.js +16 -0
  141. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  142. package/dist/desktop-automation/omniparser-runner.js +115 -0
  143. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  144. package/dist/desktop-automation/smart-snapshot.js +72 -5
  145. package/dist/doctor/index.js +40 -23
  146. package/dist/events/types.d.ts +11 -0
  147. package/dist/fleet/colab-store.js +14 -2
  148. package/dist/fleet/peer-session-bridge.js +50 -12
  149. package/dist/fleet/peer-tool-bridge.js +14 -0
  150. package/dist/goals/goal-decomposer.d.ts +27 -0
  151. package/dist/goals/goal-decomposer.js +293 -0
  152. package/dist/goals/goal-judge-client.d.ts +15 -0
  153. package/dist/goals/goal-judge-client.js +45 -0
  154. package/dist/goals/goal-judge.d.ts +2 -0
  155. package/dist/goals/goal-judge.js +35 -6
  156. package/dist/goals/goal-loop.d.ts +14 -0
  157. package/dist/goals/goal-loop.js +47 -2
  158. package/dist/goals/goal-manager.d.ts +6 -0
  159. package/dist/goals/goal-manager.js +67 -14
  160. package/dist/goals/goal-state.d.ts +13 -0
  161. package/dist/goals/goal-state.js +83 -13
  162. package/dist/goals/goal-store.d.ts +3 -0
  163. package/dist/goals/goal-store.js +31 -15
  164. package/dist/hooks/user-hooks.js +17 -3
  165. package/dist/index.js +464 -25
  166. package/dist/input/text-to-speech.d.ts +2 -6
  167. package/dist/input/text-to-speech.js +2 -27
  168. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  169. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  170. package/dist/integrations/tailscale.d.ts +13 -0
  171. package/dist/integrations/tailscale.js +87 -34
  172. package/dist/knowledge/workspace-indexer.js +53 -9
  173. package/dist/mcp/client.js +1 -0
  174. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  175. package/dist/mcp/mcp-desktop-tools.js +158 -0
  176. package/dist/mcp/mcp-server.js +2 -0
  177. package/dist/memory/enhanced-memory.d.ts +10 -0
  178. package/dist/memory/enhanced-memory.js +33 -2
  179. package/dist/memory/index.d.ts +3 -1
  180. package/dist/memory/index.js +4 -1
  181. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  182. package/dist/memory/memory-auto-proposer.js +308 -0
  183. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  184. package/dist/memory/memory-candidate-queue.js +261 -0
  185. package/dist/memory/persistent-memory.d.ts +45 -2
  186. package/dist/memory/persistent-memory.js +236 -40
  187. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  188. package/dist/ml/bayesian-qualifier.js +35 -2
  189. package/dist/openclaw/gateway-bridge.js +5 -0
  190. package/dist/plugins/marketplace.d.ts +1 -0
  191. package/dist/plugins/marketplace.js +7 -0
  192. package/dist/prompts/execution-discipline.d.ts +14 -0
  193. package/dist/prompts/execution-discipline.js +29 -0
  194. package/dist/prompts/variation-injector.js +9 -2
  195. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  196. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  197. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  198. package/dist/protocols/acp/acp-session-store.js +77 -0
  199. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  200. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  201. package/dist/providers/active-llm-registry.d.ts +37 -0
  202. package/dist/providers/active-llm-registry.js +186 -0
  203. package/dist/providers/auxiliary-provider.d.ts +25 -0
  204. package/dist/providers/auxiliary-provider.js +192 -0
  205. package/dist/providers/codex-oauth.d.ts +1 -1
  206. package/dist/providers/codex-oauth.js +27 -4
  207. package/dist/providers/index.d.ts +3 -0
  208. package/dist/providers/index.js +4 -0
  209. package/dist/providers/provider-catalog.d.ts +70 -0
  210. package/dist/providers/provider-catalog.js +738 -0
  211. package/dist/providers/provider-fallback.d.ts +35 -0
  212. package/dist/providers/provider-fallback.js +171 -0
  213. package/dist/providers/xai-oauth.d.ts +128 -0
  214. package/dist/providers/xai-oauth.js +735 -0
  215. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  216. package/dist/scheduler/cron-scheduler.js +53 -7
  217. package/dist/scheduler/script-runner.d.ts +2 -0
  218. package/dist/scheduler/script-runner.js +19 -4
  219. package/dist/scheduler/watchdog-handlers.js +7 -20
  220. package/dist/search/usearch-index.js +7 -2
  221. package/dist/security/tool-policy/tool-groups.js +2 -0
  222. package/dist/server/index.js +17 -2
  223. package/dist/server/routes/mobile.d.ts +12 -4
  224. package/dist/server/routes/mobile.js +116 -1
  225. package/dist/server/tls-config.d.ts +35 -0
  226. package/dist/server/tls-config.js +142 -0
  227. package/dist/server/tunnel-manager.d.ts +20 -0
  228. package/dist/server/tunnel-manager.js +58 -0
  229. package/dist/server/websocket/fleet-bridge.js +5 -0
  230. package/dist/services/prompt-builder.d.ts +1 -0
  231. package/dist/services/prompt-builder.js +49 -8
  232. package/dist/shared/engine-types.d.ts +15 -1
  233. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  234. package/dist/sidecar/sidecar-bridge.js +21 -10
  235. package/dist/spec/spec-store.js +8 -1
  236. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  237. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  238. package/dist/templates/project-scaffolding.js +3 -2
  239. package/dist/themes/theme-schema.d.ts +10 -10
  240. package/dist/tools/application-profiles.js +38 -0
  241. package/dist/tools/bash/bash-tool.d.ts +1 -0
  242. package/dist/tools/bash/bash-tool.js +16 -2
  243. package/dist/tools/bash/command-validator.js +3 -0
  244. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  245. package/dist/tools/bash/rtk-rewrite.js +100 -0
  246. package/dist/tools/bash/streaming-executor.js +14 -1
  247. package/dist/tools/computer-control-tool.d.ts +16 -1
  248. package/dist/tools/computer-control-tool.js +317 -5
  249. package/dist/tools/document-generator.d.ts +14 -0
  250. package/dist/tools/document-generator.js +79 -0
  251. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  252. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  253. package/dist/tools/metadata.js +28 -0
  254. package/dist/tools/office-macro-tool.d.ts +10 -0
  255. package/dist/tools/office-macro-tool.js +93 -0
  256. package/dist/tools/registry/index.d.ts +2 -1
  257. package/dist/tools/registry/index.js +2 -1
  258. package/dist/tools/registry/memory-tools.d.ts +20 -0
  259. package/dist/tools/registry/memory-tools.js +225 -3
  260. package/dist/tools/registry/multimodal-tools.js +2 -2
  261. package/dist/tools/registry/vision-tools.d.ts +33 -0
  262. package/dist/tools/registry/vision-tools.js +308 -6
  263. package/dist/tools/registry/windows-tools.d.ts +2 -0
  264. package/dist/tools/registry/windows-tools.js +47 -0
  265. package/dist/tools/route-peer-tool.js +14 -0
  266. package/dist/tools/screenshot-tool.js +14 -2
  267. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  268. package/dist/tools/text-to-speech-tool.js +2 -12
  269. package/dist/tools/tool-selector.js +3 -59
  270. package/dist/utils/config-validation/schema.d.ts +40 -7
  271. package/dist/utils/config-validation/schema.js +130 -1
  272. package/dist/utils/cost-tracker.js +21 -1
  273. package/dist/utils/disk-guard.d.ts +180 -0
  274. package/dist/utils/disk-guard.js +404 -0
  275. package/dist/utils/interactive-setup.js +35 -2
  276. package/dist/utils/model-utils.js +11 -2
  277. package/dist/utils/output-sanitizer.js +11 -0
  278. package/dist/utils/provider-detector.d.ts +8 -7
  279. package/dist/utils/provider-detector.js +19 -77
  280. package/dist/utils/settings-manager.d.ts +7 -0
  281. package/dist/utils/token-display.d.ts +3 -3
  282. package/dist/utils/token-display.js +22 -4
  283. package/dist/wizard/onboarding.d.ts +39 -0
  284. package/dist/wizard/onboarding.js +433 -21
  285. package/dist/wizard/provider-onboarding.d.ts +8 -23
  286. package/dist/wizard/provider-onboarding.js +39 -158
  287. package/package.json +4 -2
  288. package/dist/plugins/provider-onboarding.d.ts +0 -23
  289. package/dist/plugins/provider-onboarding.js +0 -116
  290. package/dist/utils/kokoro-tts.d.ts +0 -8
  291. package/dist/utils/kokoro-tts.js +0 -67
@@ -1,11 +1,37 @@
1
1
  /**
2
2
  * Mattermost Channel Adapter
3
3
  *
4
- * Connects to Mattermost via WebSocket for real-time messaging.
5
- * Provides a lightweight in-process adapter for lifecycle and send flows.
4
+ * Connects to Mattermost via its real-time WebSocket gateway
5
+ * (`/api/v4/websocket`) for inbound events and the REST API
6
+ * (`/api/v4/posts`) for outbound messages.
7
+ *
8
+ * Transport details:
9
+ * - Auth: a personal-access / bot token (`config.token`) is sent both as a
10
+ * `Bearer` header on REST calls and via the WebSocket
11
+ * `authentication_challenge` action after the socket opens.
12
+ * - Inbound: the WS pushes JSON events; `posted` events carry the new post as
13
+ * a nested JSON string which we parse into an `InboundMessage`.
14
+ * - Outbound: `send()` POSTs `{channel_id, message}` to `/api/v4/posts`.
15
+ * - Resilience: unintended socket drops are recovered through the shared
16
+ * `ReconnectionManager` (exponential backoff + jitter), the same idiom used
17
+ * by the discord / slack / imessage adapters. An explicit `disconnect()`
18
+ * sets a `closing` flag and cancels any pending reconnect so a deliberate
19
+ * close never spins the backoff loop.
6
20
  */
21
+ import WebSocket from 'ws';
7
22
  import { logger } from '../../utils/logger.js';
8
- import { BaseChannel } from '../core.js';
23
+ import { BaseChannel, } from '../core.js';
24
+ import { ReconnectionManager } from '../reconnection-manager.js';
25
+ /** Bounded wait for the WS upgrade + auth handshake before failing connect(). */
26
+ const MM_CONNECT_TIMEOUT_MS = 30000;
27
+ /**
28
+ * Legacy in-process adapter.
29
+ *
30
+ * @deprecated The real transport now lives in {@link MattermostChannel}, which
31
+ * opens a genuine WebSocket and POSTs through the REST API. This class is kept
32
+ * only for backward-compatible imports (`channels/index.ts` re-export and the
33
+ * `new-channels.test.ts` lifecycle suite) and performs no network I/O.
34
+ */
9
35
  export class MattermostAdapter {
10
36
  config;
11
37
  running = false;
@@ -16,14 +42,14 @@ export class MattermostAdapter {
16
42
  if (this.running) {
17
43
  throw new Error('MattermostAdapter is already running');
18
44
  }
19
- logger.debug('MattermostAdapter: connecting via WebSocket', { url: this.config.url });
45
+ logger.debug('MattermostAdapter: starting (in-process compatibility shim)', { url: this.config.url });
20
46
  this.running = true;
21
47
  }
22
48
  async stop() {
23
49
  if (!this.running) {
24
50
  throw new Error('MattermostAdapter is not running');
25
51
  }
26
- logger.debug('MattermostAdapter: disconnecting');
52
+ logger.debug('MattermostAdapter: stopping');
27
53
  this.running = false;
28
54
  }
29
55
  isRunning() {
@@ -53,8 +79,26 @@ export class MattermostAdapter {
53
79
  return { ...this.config };
54
80
  }
55
81
  }
82
+ /**
83
+ * Convert an `http(s)://host[:port]` base URL into the WebSocket gateway URL.
84
+ *
85
+ * The `^http` anchor maps `https → wss` and `http → ws` cleanly without
86
+ * accidentally rewriting an `http` that appears later in the string.
87
+ */
88
+ function toWebSocketUrl(baseUrl) {
89
+ const wsBase = baseUrl.replace(/\/+$/, '').replace(/^http/, 'ws');
90
+ return `${wsBase}/api/v4/websocket`;
91
+ }
56
92
  export class MattermostChannel extends BaseChannel {
57
- adapter;
93
+ mmConfig;
94
+ ws = null;
95
+ reconnectionManager;
96
+ /** Monotonic sequence number for outbound WS actions. */
97
+ seq = 1;
98
+ /** True once `disconnect()` is in progress so drops aren't treated as faults. */
99
+ closing = false;
100
+ /** Guards against resolving the connect() promise more than once. */
101
+ connectSettled = false;
58
102
  constructor(config) {
59
103
  super('mattermost', {
60
104
  type: 'mattermost',
@@ -67,36 +111,79 @@ export class MattermostChannel extends BaseChannel {
67
111
  rateLimit: config.rateLimit,
68
112
  options: config.options,
69
113
  });
70
- this.adapter = new MattermostAdapter({
114
+ this.mmConfig = {
71
115
  url: config.url,
72
116
  token: config.token,
73
117
  teamId: config.teamId,
118
+ maxRetries: config.maxRetries,
119
+ retryDelay: config.retryDelay,
120
+ };
121
+ // Persistent WS connection — drops are recovered with shared exponential
122
+ // backoff (same idiom as discord / slack / imessage).
123
+ this.reconnectionManager = new ReconnectionManager('mattermost', {
124
+ maxRetries: this.mmConfig.maxRetries ?? 10,
125
+ initialDelayMs: this.mmConfig.retryDelay ?? 1000,
126
+ maxDelayMs: 60000,
74
127
  });
75
128
  }
129
+ // ==========================================================================
130
+ // Lifecycle
131
+ // ==========================================================================
76
132
  async connect() {
77
- await this.adapter.start();
78
- this.status.connected = true;
79
- this.status.authenticated = true;
80
- this.status.lastActivity = new Date();
81
- this.emit('connected', this.type);
133
+ this.closing = false;
134
+ await this.openSocket();
82
135
  }
83
136
  async disconnect() {
84
- if (!this.status.connected)
137
+ if (!this.status.connected && !this.ws)
85
138
  return;
86
- await this.adapter.stop();
139
+ // Mark intentional so the 'close'/'error' handlers don't schedule a
140
+ // reconnect, then cancel any reconnect already pending.
141
+ this.closing = true;
142
+ this.reconnectionManager.cancel();
143
+ if (this.ws) {
144
+ try {
145
+ this.ws.removeAllListeners();
146
+ this.ws.close();
147
+ }
148
+ catch (error) {
149
+ logger.debug('MattermostChannel: error closing socket', {
150
+ error: error instanceof Error ? error.message : String(error),
151
+ });
152
+ }
153
+ this.ws = null;
154
+ }
87
155
  this.status.connected = false;
156
+ this.status.authenticated = false;
88
157
  this.status.lastActivity = new Date();
89
158
  this.emit('disconnected', this.type);
90
159
  }
91
160
  async send(message) {
92
161
  try {
93
- const result = message.replyTo
94
- ? await this.adapter.sendReply(message.channelId, message.replyTo, message.content)
95
- : await this.adapter.sendMessage(message.channelId, message.content);
162
+ const url = `${this.mmConfig.url.replace(/\/+$/, '')}/api/v4/posts`;
163
+ const body = {
164
+ channel_id: message.channelId,
165
+ message: message.content,
166
+ };
167
+ if (message.replyTo) {
168
+ body.root_id = message.replyTo;
169
+ }
170
+ const response = await fetch(url, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'Content-Type': 'application/json',
174
+ Authorization: `Bearer ${this.mmConfig.token}`,
175
+ },
176
+ body: JSON.stringify(body),
177
+ });
178
+ if (!response.ok) {
179
+ const errorText = await response.text().catch(() => '');
180
+ throw new Error(`Mattermost API error: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`);
181
+ }
182
+ const post = (await response.json());
96
183
  this.status.lastActivity = new Date();
97
184
  return {
98
- success: result.success,
99
- messageId: result.postId,
185
+ success: true,
186
+ messageId: post.id,
100
187
  timestamp: new Date(),
101
188
  };
102
189
  }
@@ -108,6 +195,202 @@ export class MattermostChannel extends BaseChannel {
108
195
  };
109
196
  }
110
197
  }
198
+ // ==========================================================================
199
+ // WebSocket transport
200
+ // ==========================================================================
201
+ openSocket() {
202
+ return new Promise((resolve, reject) => {
203
+ this.connectSettled = false;
204
+ const wsUrl = toWebSocketUrl(this.mmConfig.url);
205
+ logger.debug('MattermostChannel: opening WebSocket', { wsUrl });
206
+ let ws;
207
+ try {
208
+ ws = new WebSocket(wsUrl);
209
+ }
210
+ catch (error) {
211
+ reject(error instanceof Error ? error : new Error(String(error)));
212
+ return;
213
+ }
214
+ this.ws = ws;
215
+ // The WS upgrade can succeed while the server never sends a frame (bad
216
+ // token held open, wrong service on the port). Without a timeout the
217
+ // connect() Promise would never settle. Bound the handshake.
218
+ const connectTimer = setTimeout(() => {
219
+ if (this.connectSettled)
220
+ return;
221
+ this.connectSettled = true;
222
+ try {
223
+ ws.close();
224
+ }
225
+ catch {
226
+ /* ignore */
227
+ }
228
+ reject(new Error(`Mattermost WebSocket did not authenticate within ${MM_CONNECT_TIMEOUT_MS}ms`));
229
+ }, MM_CONNECT_TIMEOUT_MS);
230
+ if (typeof connectTimer.unref === 'function') {
231
+ connectTimer.unref();
232
+ }
233
+ const onConnected = () => {
234
+ clearTimeout(connectTimer);
235
+ resolve();
236
+ };
237
+ ws.on('open', () => {
238
+ // Mattermost expects the auth challenge as the first frame after open.
239
+ this.sendAction('authentication_challenge', { token: this.mmConfig.token });
240
+ });
241
+ ws.on('message', (data) => {
242
+ try {
243
+ const event = JSON.parse(data.toString());
244
+ this.handleWsEvent(event, onConnected);
245
+ }
246
+ catch (error) {
247
+ logger.warn('MattermostChannel: failed to parse WS frame', {
248
+ error: error instanceof Error ? error.message : String(error),
249
+ });
250
+ }
251
+ });
252
+ ws.on('close', () => {
253
+ clearTimeout(connectTimer);
254
+ // A close before the handshake settles must reject connect() (don't hang).
255
+ if (!this.connectSettled) {
256
+ this.connectSettled = true;
257
+ reject(new Error('Mattermost WebSocket closed before authentication'));
258
+ }
259
+ this.handleDrop();
260
+ });
261
+ ws.on('error', (error) => {
262
+ clearTimeout(connectTimer);
263
+ const err = error instanceof Error ? error : new Error(String(error));
264
+ if (this.listenerCount('error') > 0) {
265
+ this.emit('error', this.type, err);
266
+ }
267
+ if (!this.connectSettled) {
268
+ this.connectSettled = true;
269
+ reject(err);
270
+ }
271
+ this.handleDrop();
272
+ });
273
+ });
274
+ }
275
+ handleWsEvent(event, resolveConnect) {
276
+ // The `hello` event (or a successful auth reply) confirms the session.
277
+ const authedReply = event.seq_reply !== undefined && event.status === 'OK';
278
+ if (event.event === 'hello' || authedReply) {
279
+ this.onAuthenticated();
280
+ if (!this.connectSettled) {
281
+ this.connectSettled = true;
282
+ resolveConnect();
283
+ }
284
+ return;
285
+ }
286
+ if (event.event === 'posted') {
287
+ this.handlePostedEvent(event);
288
+ return;
289
+ }
290
+ // Respond to a server-side ping with a pong when applicable.
291
+ if (event.event === 'ping') {
292
+ this.sendAction('pong', {});
293
+ }
294
+ }
295
+ onAuthenticated() {
296
+ // Connectivity confirmed — reset the shared backoff state so a future drop
297
+ // starts from delay 0 rather than inheriting a stale retryCount.
298
+ this.reconnectionManager.onConnected();
299
+ this.status.connected = true;
300
+ this.status.authenticated = true;
301
+ this.status.lastActivity = new Date();
302
+ this.emit('connected', this.type);
303
+ logger.debug('MattermostChannel: authenticated and connected');
304
+ }
305
+ handlePostedEvent(event) {
306
+ const rawPost = event.data?.['post'];
307
+ if (typeof rawPost !== 'string')
308
+ return;
309
+ let post;
310
+ try {
311
+ post = JSON.parse(rawPost);
312
+ }
313
+ catch (error) {
314
+ logger.warn('MattermostChannel: failed to parse nested post JSON', {
315
+ error: error instanceof Error ? error.message : String(error),
316
+ });
317
+ return;
318
+ }
319
+ // System posts (joins, header changes, …) have a non-empty `type` — skip.
320
+ if (post.type)
321
+ return;
322
+ const channelName = typeof event.data?.['channel_name'] === 'string'
323
+ ? event.data['channel_name']
324
+ : undefined;
325
+ const inbound = {
326
+ id: post.id,
327
+ channel: {
328
+ id: post.channel_id,
329
+ type: 'mattermost',
330
+ name: channelName,
331
+ },
332
+ sender: {
333
+ id: post.user_id,
334
+ },
335
+ content: post.message,
336
+ contentType: 'text',
337
+ timestamp: post.create_at ? new Date(post.create_at) : new Date(),
338
+ threadId: post.root_id || undefined,
339
+ replyTo: post.root_id || undefined,
340
+ raw: { event, post },
341
+ };
342
+ this.status.lastActivity = new Date();
343
+ const parsed = this.parseCommand(inbound);
344
+ this.emit('message', parsed);
345
+ if (parsed.isCommand) {
346
+ this.emit('command', parsed);
347
+ }
348
+ }
349
+ /**
350
+ * Handle a socket drop. If the close was intentional (`disconnect()`), do
351
+ * nothing. Otherwise schedule a reconnect through the shared manager.
352
+ */
353
+ handleDrop() {
354
+ if (this.closing)
355
+ return;
356
+ const wasConnected = this.status.connected;
357
+ this.status.connected = false;
358
+ this.status.authenticated = false;
359
+ // Tear down the dead socket so we don't double-handle further events.
360
+ if (this.ws) {
361
+ try {
362
+ this.ws.removeAllListeners();
363
+ }
364
+ catch {
365
+ /* ignore */
366
+ }
367
+ this.ws = null;
368
+ }
369
+ if (wasConnected) {
370
+ this.emit('disconnected', this.type);
371
+ }
372
+ logger.debug('MattermostChannel: connection dropped, scheduling reconnect');
373
+ this.reconnectionManager.scheduleReconnect(async () => {
374
+ await this.openSocket();
375
+ // onConnected() is called from onAuthenticated() once `hello` arrives.
376
+ });
377
+ }
378
+ sendAction(action, data) {
379
+ if (this.ws?.readyState !== WebSocket.OPEN)
380
+ return;
381
+ this.ws.send(JSON.stringify({
382
+ seq: this.seq++,
383
+ action,
384
+ data,
385
+ }));
386
+ }
387
+ /** Expose the resolved WS gateway URL (handy for diagnostics / tests). */
388
+ getWebSocketUrl() {
389
+ return toWebSocketUrl(this.mmConfig.url);
390
+ }
391
+ getConfig() {
392
+ return { ...this.mmConfig };
393
+ }
111
394
  }
112
- export default MattermostAdapter;
395
+ export default MattermostChannel;
113
396
  //# sourceMappingURL=index.js.map
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * Nextcloud Talk Channel Adapter
3
3
  *
4
- * Connects to Nextcloud Talk for messaging and room management.
5
- * Provides a lightweight in-process adapter for lifecycle and room flows.
4
+ * Connects to Nextcloud Talk (Spreed) for messaging via the real Spreed chat
5
+ * REST API using an HTTP long-poll receive loop (`lookIntoFuture=1`), with
6
+ * auto-reconnect through the shared ReconnectionManager (same idiom as
7
+ * imessage/irc/discord/slack).
6
8
  */
9
+ import { EventEmitter } from 'events';
7
10
  import { BaseChannel, ChannelConfig, DeliveryResult, OutboundMessage } from '../core.js';
8
11
  export interface NextcloudTalkConfig {
9
12
  url: string;
10
13
  username: string;
11
14
  password: string;
12
15
  }
13
- export interface NextcloudTalkChannelConfig extends ChannelConfig {
14
- url: string;
15
- username: string;
16
- password: string;
17
- }
18
16
  export declare class NextcloudTalkAdapter {
19
17
  private config;
20
18
  private running;
@@ -40,8 +38,91 @@ export declare class NextcloudTalkAdapter {
40
38
  }>;
41
39
  getConfig(): NextcloudTalkConfig;
42
40
  }
41
+ export interface NextcloudTalkChannelConfig extends ChannelConfig {
42
+ /** Base Nextcloud URL, e.g. https://cloud.example.com (no trailing slash needed). */
43
+ url: string;
44
+ /** Nextcloud user for HTTP Basic auth. */
45
+ username: string;
46
+ /** App password / login password for HTTP Basic auth. */
47
+ password: string;
48
+ /** Optional explicit app password (preferred over `password` if set). */
49
+ appPassword?: string;
50
+ /** Optional bearer token — used instead of Basic auth when provided. */
51
+ token?: string;
52
+ bearer?: string;
53
+ /** Conversation/room token to long-poll for inbound messages. */
54
+ roomToken?: string;
55
+ /** Long-poll hold timeout in seconds (server-side). Default 30. */
56
+ pollTimeoutSecs?: number;
57
+ maxRetries?: number;
58
+ retryDelayMs?: number;
59
+ }
60
+ /**
61
+ * Real Nextcloud Talk (Spreed) client. Holds one outstanding long-poll request
62
+ * against `chat/<roomToken>?lookIntoFuture=1` at a time; the server returns 200
63
+ * with new messages or 304 on timeout. Drops are recovered via the shared
64
+ * ReconnectionManager. This is a genuinely persistent connection model — the
65
+ * same shape as imessage/irc.
66
+ */
67
+ export declare class NextcloudTalkClient extends EventEmitter {
68
+ private readonly baseUrl;
69
+ private readonly username;
70
+ private readonly password;
71
+ private readonly bearer;
72
+ private readonly roomToken;
73
+ private readonly pollTimeoutSecs;
74
+ private running;
75
+ /** True while an intentional disconnect is in progress — suppresses reconnect. */
76
+ private closing;
77
+ private reconnecting;
78
+ private lastKnownMessageId;
79
+ /** AbortController for the in-flight long-poll request. */
80
+ private pollAbort;
81
+ private readonly reconnectionManager;
82
+ constructor(config: NextcloudTalkChannelConfig);
83
+ isRunning(): boolean;
84
+ getRoomToken(): string;
85
+ /**
86
+ * Begin the long-poll receive loop. Returns immediately — `connect()` does
87
+ * NOT block on the first poll (a long-poll can hold open up to
88
+ * `pollTimeoutSecs`). The first successful poll (200 or 304) emits
89
+ * 'connected'.
90
+ */
91
+ start(): void;
92
+ /**
93
+ * Stop the loop, abort the in-flight request, and cancel any pending
94
+ * reconnect. Idempotent.
95
+ */
96
+ stop(): void;
97
+ private runPollLoop;
98
+ /**
99
+ * Issue one long-poll request. Resolves on 200 (with messages emitted) or 304
100
+ * (timeout, no messages). Throws on network failure / non-2xx-non-304 status
101
+ * (a real drop → triggers reconnect).
102
+ */
103
+ private pollOnce;
104
+ private toInbound;
105
+ /** Emit 'connected' exactly once per session (on first successful poll). */
106
+ private connectedEmitted;
107
+ private onConnectedOnce;
108
+ /**
109
+ * Recover a dropped connection via the shared ReconnectionManager
110
+ * (exponential backoff + jitter + exhaustion). `scheduleReconnect` is
111
+ * single-shot, so a failed attempt re-drives it (deferred past the manager's
112
+ * internal active-guard) until recovery or exhaustion — same idiom as
113
+ * irc/imessage.
114
+ */
115
+ private scheduleReconnect;
116
+ sendMessage(roomToken: string, text: string): Promise<{
117
+ success: boolean;
118
+ messageId?: string;
119
+ }>;
120
+ private buildHeaders;
121
+ private isAbortError;
122
+ }
43
123
  export declare class NextcloudTalkChannel extends BaseChannel {
44
- private adapter;
124
+ private client;
125
+ private readonly ncConfig;
45
126
  constructor(config: NextcloudTalkChannelConfig);
46
127
  connect(): Promise<void>;
47
128
  disconnect(): Promise<void>;