@phuetz/code-buddy 1.0.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 (312) hide show
  1. package/README.md +160 -164
  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 +101 -13
  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/lesson-auto-proposer.js +10 -0
  20. package/dist/agent/middleware/index.d.ts +1 -0
  21. package/dist/agent/middleware/index.js +1 -0
  22. package/dist/agent/middleware/session-duration.d.ts +36 -0
  23. package/dist/agent/middleware/session-duration.js +78 -0
  24. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  25. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  26. package/dist/agent/model-benchmark.d.ts +77 -0
  27. package/dist/agent/model-benchmark.js +309 -0
  28. package/dist/agent/model-tier.d.ts +14 -0
  29. package/dist/agent/model-tier.js +71 -0
  30. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  31. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  32. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  33. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  34. package/dist/agent/multi-agent/session-tools.js +22 -5
  35. package/dist/agent/reasoning/mcts.js +20 -6
  36. package/dist/agent/session-end-flush.d.ts +71 -0
  37. package/dist/agent/session-end-flush.js +233 -0
  38. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  39. package/dist/agent/streaming/streaming-handler.js +8 -1
  40. package/dist/agent/tool-executor.js +1 -0
  41. package/dist/agent/tool-handler.d.ts +5 -0
  42. package/dist/agent/tool-handler.js +130 -4
  43. package/dist/browser-automation/browser-operator-executor.js +15 -0
  44. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  45. package/dist/browser-automation/browser-use-runner.js +492 -0
  46. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  47. package/dist/browser-automation/camofox-runner.js +287 -0
  48. package/dist/channels/dingtalk/index.js +3 -0
  49. package/dist/channels/discord/client.d.ts +9 -0
  50. package/dist/channels/discord/client.js +12 -0
  51. package/dist/channels/feishu/index.d.ts +167 -1
  52. package/dist/channels/feishu/index.js +432 -7
  53. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  54. package/dist/channels/gateway-lifecycle.js +219 -0
  55. package/dist/channels/google-chat/index.d.ts +5 -1
  56. package/dist/channels/google-chat/index.js +5 -1
  57. package/dist/channels/imessage/index.d.ts +14 -0
  58. package/dist/channels/imessage/index.js +64 -17
  59. package/dist/channels/index.d.ts +4 -0
  60. package/dist/channels/index.js +4 -0
  61. package/dist/channels/irc/index.d.ts +73 -3
  62. package/dist/channels/irc/index.js +446 -11
  63. package/dist/channels/line/index.js +3 -0
  64. package/dist/channels/mattermost/index.d.ts +51 -4
  65. package/dist/channels/mattermost/index.js +303 -20
  66. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  67. package/dist/channels/nextcloud-talk/index.js +367 -16
  68. package/dist/channels/nostr/index.d.ts +121 -1
  69. package/dist/channels/nostr/index.js +396 -8
  70. package/dist/channels/ntfy/index.js +3 -0
  71. package/dist/channels/qq/index.js +3 -0
  72. package/dist/channels/slash-parity.d.ts +117 -0
  73. package/dist/channels/slash-parity.js +185 -0
  74. package/dist/channels/synology-chat/index.js +3 -0
  75. package/dist/channels/teams/index.d.ts +3 -0
  76. package/dist/channels/teams/index.js +3 -0
  77. package/dist/channels/telegram/client.d.ts +9 -0
  78. package/dist/channels/telegram/client.js +12 -0
  79. package/dist/channels/twilio-voice/index.js +3 -0
  80. package/dist/channels/webchat/index.d.ts +7 -1
  81. package/dist/channels/webchat/index.js +7 -1
  82. package/dist/channels/wecom/index.js +3 -0
  83. package/dist/channels/weixin/index.js +3 -0
  84. package/dist/channels/zalo/index.js +3 -0
  85. package/dist/cli/config-loader.js +1 -1
  86. package/dist/codebuddy/client.d.ts +30 -1
  87. package/dist/codebuddy/client.js +167 -20
  88. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  90. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  91. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  92. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  93. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  94. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  95. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  96. package/dist/codebuddy/tool-definitions/index.js +2 -1
  97. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  98. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  99. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  100. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  101. package/dist/codebuddy/tools.d.ts +1 -1
  102. package/dist/codebuddy/tools.js +27 -24
  103. package/dist/commands/cli/hermes-commands.js +108 -0
  104. package/dist/commands/cli/native-engine-commands.js +107 -3
  105. package/dist/commands/cli/utility-commands.js +45 -0
  106. package/dist/commands/enhanced-command-handler.js +5 -0
  107. package/dist/commands/goal-cli.d.ts +71 -0
  108. package/dist/commands/goal-cli.js +280 -0
  109. package/dist/commands/handlers/goal-handler.d.ts +34 -0
  110. package/dist/commands/handlers/goal-handler.js +155 -0
  111. package/dist/commands/handlers/index.d.ts +1 -0
  112. package/dist/commands/handlers/index.js +2 -0
  113. package/dist/commands/handlers/infra-handlers.js +36 -0
  114. package/dist/commands/handlers/memory-handlers.js +96 -1
  115. package/dist/commands/headless-slash.d.ts +2 -0
  116. package/dist/commands/headless-slash.js +12 -0
  117. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  118. package/dist/commands/llm-provider-resolution.js +87 -33
  119. package/dist/commands/ollama.d.ts +25 -0
  120. package/dist/commands/ollama.js +100 -0
  121. package/dist/commands/provider.d.ts +5 -0
  122. package/dist/commands/provider.js +106 -55
  123. package/dist/commands/slash/builtin-commands.js +20 -0
  124. package/dist/commands/spec-next.js +2 -1
  125. package/dist/commands/spec-plan.js +5 -16
  126. package/dist/commands/tunnel.d.ts +2 -0
  127. package/dist/commands/tunnel.js +48 -0
  128. package/dist/config/config-resolver.d.ts +2 -1
  129. package/dist/config/config-resolver.js +54 -41
  130. package/dist/config/constants.d.ts +28 -0
  131. package/dist/config/constants.js +7 -0
  132. package/dist/config/env-schema.js +770 -0
  133. package/dist/config/feature-flags.js +7 -0
  134. package/dist/config/model-tools.js +4 -4
  135. package/dist/config/toml-config.d.ts +16 -0
  136. package/dist/config/toml-config.js +3 -0
  137. package/dist/context/context-manager-v2.d.ts +39 -0
  138. package/dist/context/context-manager-v2.js +91 -0
  139. package/dist/daemon/agent-task-executor.js +12 -1
  140. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  141. package/dist/daemon/autonomous-daemon.js +5 -3
  142. package/dist/daemon/autonomous-loop.d.ts +21 -1
  143. package/dist/daemon/autonomous-loop.js +69 -0
  144. package/dist/daemon/colab-goal.d.ts +38 -0
  145. package/dist/daemon/colab-goal.js +81 -0
  146. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  147. package/dist/daemon/cron-agent-bridge.js +25 -9
  148. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  149. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  150. package/dist/desktop/engine-adapter.d.ts +14 -0
  151. package/dist/desktop-automation/automation-manager.js +16 -0
  152. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  153. package/dist/desktop-automation/omniparser-runner.js +115 -0
  154. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  155. package/dist/desktop-automation/smart-snapshot.js +72 -5
  156. package/dist/doctor/index.js +40 -23
  157. package/dist/events/types.d.ts +11 -0
  158. package/dist/fleet/colab-store.d.ts +21 -0
  159. package/dist/fleet/colab-store.js +28 -0
  160. package/dist/fleet/peer-session-bridge.d.ts +1 -1
  161. package/dist/fleet/peer-session-bridge.js +243 -2
  162. package/dist/fleet/peer-session-store.d.ts +3 -0
  163. package/dist/fleet/peer-tool-bridge.js +14 -0
  164. package/dist/fleet/privacy-lint.d.ts +8 -0
  165. package/dist/fleet/privacy-lint.js +22 -0
  166. package/dist/goals/goal-decomposer.d.ts +27 -0
  167. package/dist/goals/goal-decomposer.js +293 -0
  168. package/dist/goals/goal-judge-client.d.ts +15 -0
  169. package/dist/goals/goal-judge-client.js +45 -0
  170. package/dist/goals/goal-judge.d.ts +38 -0
  171. package/dist/goals/goal-judge.js +158 -0
  172. package/dist/goals/goal-loop.d.ts +37 -0
  173. package/dist/goals/goal-loop.js +101 -0
  174. package/dist/goals/goal-manager.d.ts +77 -0
  175. package/dist/goals/goal-manager.js +289 -0
  176. package/dist/goals/goal-state.d.ts +99 -0
  177. package/dist/goals/goal-state.js +315 -0
  178. package/dist/goals/goal-store.d.ts +28 -0
  179. package/dist/goals/goal-store.js +87 -0
  180. package/dist/goals/index.d.ts +5 -0
  181. package/dist/goals/index.js +6 -0
  182. package/dist/hooks/use-input-handler.js +36 -1
  183. package/dist/hooks/user-hooks.js +17 -3
  184. package/dist/index.js +506 -25
  185. package/dist/input/text-to-speech.d.ts +2 -6
  186. package/dist/input/text-to-speech.js +2 -27
  187. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  188. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  189. package/dist/integrations/tailscale.d.ts +13 -0
  190. package/dist/integrations/tailscale.js +87 -34
  191. package/dist/knowledge/workspace-indexer.js +53 -9
  192. package/dist/mcp/client.js +1 -0
  193. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  194. package/dist/mcp/mcp-desktop-tools.js +158 -0
  195. package/dist/mcp/mcp-server.js +2 -0
  196. package/dist/memory/enhanced-memory.d.ts +10 -0
  197. package/dist/memory/enhanced-memory.js +33 -2
  198. package/dist/memory/index.d.ts +3 -1
  199. package/dist/memory/index.js +4 -1
  200. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  201. package/dist/memory/memory-auto-proposer.js +308 -0
  202. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  203. package/dist/memory/memory-candidate-queue.js +261 -0
  204. package/dist/memory/persistent-memory.d.ts +45 -2
  205. package/dist/memory/persistent-memory.js +236 -40
  206. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  207. package/dist/ml/bayesian-qualifier.js +35 -2
  208. package/dist/observability/run-store.d.ts +1 -1
  209. package/dist/openclaw/gateway-bridge.js +5 -0
  210. package/dist/plugins/marketplace.d.ts +1 -0
  211. package/dist/plugins/marketplace.js +7 -0
  212. package/dist/prompts/execution-discipline.d.ts +14 -0
  213. package/dist/prompts/execution-discipline.js +29 -0
  214. package/dist/prompts/variation-injector.js +9 -2
  215. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  216. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  217. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  218. package/dist/protocols/acp/acp-session-store.js +77 -0
  219. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  220. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  221. package/dist/providers/active-llm-registry.d.ts +37 -0
  222. package/dist/providers/active-llm-registry.js +186 -0
  223. package/dist/providers/auxiliary-provider.d.ts +25 -0
  224. package/dist/providers/auxiliary-provider.js +192 -0
  225. package/dist/providers/codex-oauth.d.ts +1 -1
  226. package/dist/providers/codex-oauth.js +27 -4
  227. package/dist/providers/index.d.ts +3 -0
  228. package/dist/providers/index.js +4 -0
  229. package/dist/providers/provider-catalog.d.ts +70 -0
  230. package/dist/providers/provider-catalog.js +738 -0
  231. package/dist/providers/provider-fallback.d.ts +35 -0
  232. package/dist/providers/provider-fallback.js +171 -0
  233. package/dist/providers/xai-oauth.d.ts +128 -0
  234. package/dist/providers/xai-oauth.js +735 -0
  235. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  236. package/dist/scheduler/cron-scheduler.js +53 -7
  237. package/dist/scheduler/script-runner.d.ts +2 -0
  238. package/dist/scheduler/script-runner.js +19 -4
  239. package/dist/scheduler/watchdog-handlers.js +7 -20
  240. package/dist/search/usearch-index.js +7 -2
  241. package/dist/security/tool-policy/tool-groups.js +2 -0
  242. package/dist/server/index.js +17 -2
  243. package/dist/server/routes/mobile.d.ts +12 -4
  244. package/dist/server/routes/mobile.js +116 -1
  245. package/dist/server/tls-config.d.ts +35 -0
  246. package/dist/server/tls-config.js +142 -0
  247. package/dist/server/tunnel-manager.d.ts +20 -0
  248. package/dist/server/tunnel-manager.js +58 -0
  249. package/dist/server/websocket/fleet-bridge.d.ts +13 -1
  250. package/dist/server/websocket/fleet-bridge.js +16 -0
  251. package/dist/services/prompt-builder.d.ts +1 -0
  252. package/dist/services/prompt-builder.js +49 -8
  253. package/dist/shared/engine-types.d.ts +15 -1
  254. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  255. package/dist/sidecar/sidecar-bridge.js +21 -10
  256. package/dist/spec/spec-store.js +8 -1
  257. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  258. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  259. package/dist/templates/project-scaffolding.js +3 -2
  260. package/dist/themes/theme-schema.d.ts +10 -10
  261. package/dist/tools/application-profiles.js +38 -0
  262. package/dist/tools/bash/bash-tool.d.ts +1 -0
  263. package/dist/tools/bash/bash-tool.js +16 -2
  264. package/dist/tools/bash/command-validator.js +3 -0
  265. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  266. package/dist/tools/bash/rtk-rewrite.js +100 -0
  267. package/dist/tools/bash/streaming-executor.js +14 -1
  268. package/dist/tools/computer-control-tool.d.ts +16 -1
  269. package/dist/tools/computer-control-tool.js +317 -5
  270. package/dist/tools/document-generator.d.ts +14 -0
  271. package/dist/tools/document-generator.js +79 -0
  272. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  273. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  274. package/dist/tools/metadata.js +28 -0
  275. package/dist/tools/office-macro-tool.d.ts +10 -0
  276. package/dist/tools/office-macro-tool.js +93 -0
  277. package/dist/tools/registry/index.d.ts +2 -1
  278. package/dist/tools/registry/index.js +2 -1
  279. package/dist/tools/registry/memory-tools.d.ts +20 -0
  280. package/dist/tools/registry/memory-tools.js +225 -3
  281. package/dist/tools/registry/multimodal-tools.js +2 -2
  282. package/dist/tools/registry/vision-tools.d.ts +33 -0
  283. package/dist/tools/registry/vision-tools.js +308 -6
  284. package/dist/tools/registry/windows-tools.d.ts +2 -0
  285. package/dist/tools/registry/windows-tools.js +47 -0
  286. package/dist/tools/route-peer-tool.js +14 -0
  287. package/dist/tools/screenshot-tool.js +14 -2
  288. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  289. package/dist/tools/text-to-speech-tool.js +2 -12
  290. package/dist/tools/tool-selector.js +3 -59
  291. package/dist/utils/config-validation/schema.d.ts +40 -7
  292. package/dist/utils/config-validation/schema.js +130 -1
  293. package/dist/utils/cost-tracker.js +21 -1
  294. package/dist/utils/disk-guard.d.ts +180 -0
  295. package/dist/utils/disk-guard.js +404 -0
  296. package/dist/utils/interactive-setup.js +35 -2
  297. package/dist/utils/model-utils.js +11 -2
  298. package/dist/utils/output-sanitizer.js +11 -0
  299. package/dist/utils/provider-detector.d.ts +8 -7
  300. package/dist/utils/provider-detector.js +19 -77
  301. package/dist/utils/settings-manager.d.ts +7 -0
  302. package/dist/utils/token-display.d.ts +3 -3
  303. package/dist/utils/token-display.js +22 -4
  304. package/dist/wizard/onboarding.d.ts +39 -0
  305. package/dist/wizard/onboarding.js +433 -21
  306. package/dist/wizard/provider-onboarding.d.ts +8 -23
  307. package/dist/wizard/provider-onboarding.js +39 -158
  308. package/package.json +4 -2
  309. package/dist/plugins/provider-onboarding.d.ts +0 -23
  310. package/dist/plugins/provider-onboarding.js +0 -116
  311. package/dist/utils/kokoro-tts.d.ts +0 -8
  312. package/dist/utils/kokoro-tts.js +0 -67
@@ -1,11 +1,15 @@
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 { logger } from '../../utils/logger.js';
8
- import { BaseChannel } from '../core.js';
11
+ import { BaseChannel, } from '../core.js';
12
+ import { ReconnectionManager } from '../reconnection-manager.js';
9
13
  export class NextcloudTalkAdapter {
10
14
  config;
11
15
  running = false;
@@ -64,8 +68,325 @@ export class NextcloudTalkAdapter {
64
68
  return { ...this.config };
65
69
  }
66
70
  }
71
+ const SPREED_CHAT_PATH = '/ocs/v2.php/apps/spreed/api/v1/chat';
72
+ /**
73
+ * Real Nextcloud Talk (Spreed) client. Holds one outstanding long-poll request
74
+ * against `chat/<roomToken>?lookIntoFuture=1` at a time; the server returns 200
75
+ * with new messages or 304 on timeout. Drops are recovered via the shared
76
+ * ReconnectionManager. This is a genuinely persistent connection model — the
77
+ * same shape as imessage/irc.
78
+ */
79
+ export class NextcloudTalkClient extends EventEmitter {
80
+ baseUrl;
81
+ username;
82
+ password;
83
+ bearer;
84
+ roomToken;
85
+ pollTimeoutSecs;
86
+ running = false;
87
+ /** True while an intentional disconnect is in progress — suppresses reconnect. */
88
+ closing = false;
89
+ reconnecting = false;
90
+ lastKnownMessageId = 0;
91
+ /** AbortController for the in-flight long-poll request. */
92
+ pollAbort = null;
93
+ reconnectionManager;
94
+ constructor(config) {
95
+ super();
96
+ this.baseUrl = config.url.replace(/\/+$/, '');
97
+ this.username = config.username;
98
+ this.password = config.appPassword ?? config.password;
99
+ this.bearer = config.bearer ?? config.token;
100
+ this.roomToken = config.roomToken ?? '';
101
+ this.pollTimeoutSecs = config.pollTimeoutSecs ?? 30;
102
+ this.reconnectionManager = new ReconnectionManager('nextcloud-talk', {
103
+ maxRetries: config.maxRetries ?? 10,
104
+ initialDelayMs: config.retryDelayMs ?? 2000,
105
+ maxDelayMs: 60000,
106
+ });
107
+ }
108
+ isRunning() {
109
+ return this.running;
110
+ }
111
+ getRoomToken() {
112
+ return this.roomToken;
113
+ }
114
+ // --------------------------------------------------------------------------
115
+ // Lifecycle
116
+ // --------------------------------------------------------------------------
117
+ /**
118
+ * Begin the long-poll receive loop. Returns immediately — `connect()` does
119
+ * NOT block on the first poll (a long-poll can hold open up to
120
+ * `pollTimeoutSecs`). The first successful poll (200 or 304) emits
121
+ * 'connected'.
122
+ */
123
+ start() {
124
+ if (this.running) {
125
+ throw new Error('NextcloudTalkClient is already running');
126
+ }
127
+ this.running = true;
128
+ this.closing = false;
129
+ // Fresh session — clear any inherited backoff state.
130
+ this.reconnectionManager.onConnected();
131
+ logger.debug('NextcloudTalkClient: starting long-poll loop', {
132
+ url: this.baseUrl,
133
+ room: this.roomToken,
134
+ });
135
+ void this.runPollLoop();
136
+ }
137
+ /**
138
+ * Stop the loop, abort the in-flight request, and cancel any pending
139
+ * reconnect. Idempotent.
140
+ */
141
+ stop() {
142
+ if (!this.running && !this.closing) {
143
+ return;
144
+ }
145
+ this.closing = true;
146
+ this.running = false;
147
+ this.reconnecting = false;
148
+ this.reconnectionManager.cancel();
149
+ if (this.pollAbort) {
150
+ this.pollAbort.abort();
151
+ this.pollAbort = null;
152
+ }
153
+ logger.debug('NextcloudTalkClient: stopped');
154
+ }
155
+ // --------------------------------------------------------------------------
156
+ // Receive (long-poll)
157
+ // --------------------------------------------------------------------------
158
+ async runPollLoop() {
159
+ while (this.running && !this.closing) {
160
+ try {
161
+ await this.pollOnce();
162
+ // A successful poll (messages or 304 timeout) means the link is
163
+ // healthy — reset shared backoff so a future drop starts at delay 0.
164
+ this.reconnectionManager.onConnected();
165
+ this.reconnecting = false;
166
+ this.onConnectedOnce();
167
+ }
168
+ catch (err) {
169
+ // Intentional teardown — exit silently, never reconnect.
170
+ if (this.closing || !this.running || this.isAbortError(err)) {
171
+ return;
172
+ }
173
+ const error = err instanceof Error ? err : new Error(String(err));
174
+ logger.warn('NextcloudTalkClient: poll error, scheduling reconnect', {
175
+ error: error.message,
176
+ });
177
+ // Surface the error, but only when a listener exists — a bare
178
+ // EventEmitter 'error' with no listener throws and would abort the
179
+ // reconnect we're about to schedule. Scheduling comes first regardless.
180
+ this.scheduleReconnect();
181
+ if (this.listenerCount('error') > 0) {
182
+ this.emit('error', error);
183
+ }
184
+ // Hand control to the ReconnectionManager — it owns the retry cadence.
185
+ return;
186
+ }
187
+ }
188
+ }
189
+ /**
190
+ * Issue one long-poll request. Resolves on 200 (with messages emitted) or 304
191
+ * (timeout, no messages). Throws on network failure / non-2xx-non-304 status
192
+ * (a real drop → triggers reconnect).
193
+ */
194
+ async pollOnce() {
195
+ const params = new URLSearchParams({
196
+ lookIntoFuture: '1',
197
+ timeout: String(this.pollTimeoutSecs),
198
+ lastKnownMessageId: String(this.lastKnownMessageId),
199
+ setReadMarker: '0',
200
+ });
201
+ const url = `${this.baseUrl}${SPREED_CHAT_PATH}/${encodeURIComponent(this.roomToken)}?${params.toString()}`;
202
+ // Abort the request slightly after the server-side hold timeout so a wedged
203
+ // connection can't pin the loop forever.
204
+ const abort = new AbortController();
205
+ this.pollAbort = abort;
206
+ const watchdog = setTimeout(() => abort.abort(), (this.pollTimeoutSecs + 10) * 1000);
207
+ watchdog.unref?.();
208
+ let response;
209
+ try {
210
+ response = await fetch(url, {
211
+ method: 'GET',
212
+ headers: this.buildHeaders(),
213
+ signal: abort.signal,
214
+ });
215
+ }
216
+ finally {
217
+ clearTimeout(watchdog);
218
+ if (this.pollAbort === abort) {
219
+ this.pollAbort = null;
220
+ }
221
+ }
222
+ // 304 = long-poll timed out with no new messages — a SUCCESSFUL empty poll,
223
+ // not an error and not a reconnect trigger.
224
+ if (response.status === 304) {
225
+ return;
226
+ }
227
+ if (!response.ok) {
228
+ const body = await response.text().catch(() => '');
229
+ throw new Error(`Nextcloud Talk poll failed: ${response.status} ${response.statusText} - ${body}`);
230
+ }
231
+ const lastGiven = response.headers.get('X-Chat-Last-Given');
232
+ const envelope = (await response.json().catch(() => ({})));
233
+ const messages = envelope.ocs?.data ?? [];
234
+ let maxId = this.lastKnownMessageId;
235
+ for (const raw of messages) {
236
+ if (typeof raw.id === 'number' && raw.id > maxId) {
237
+ maxId = raw.id;
238
+ }
239
+ // Skip system messages (joins/leaves/etc.) — they have no user content.
240
+ if (raw.systemMessage) {
241
+ continue;
242
+ }
243
+ this.emit('message', this.toInbound(raw));
244
+ }
245
+ // Advance the cursor from the server-provided header when present, else from
246
+ // the max message id we saw.
247
+ const givenId = lastGiven ? Number.parseInt(lastGiven, 10) : NaN;
248
+ if (Number.isFinite(givenId) && givenId > this.lastKnownMessageId) {
249
+ this.lastKnownMessageId = givenId;
250
+ }
251
+ else if (maxId > this.lastKnownMessageId) {
252
+ this.lastKnownMessageId = maxId;
253
+ }
254
+ }
255
+ toInbound(raw) {
256
+ const sender = raw.actorId ?? 'unknown';
257
+ const content = raw.message ?? '';
258
+ const isCommand = content.startsWith('/');
259
+ const contentType = isCommand ? 'command' : 'text';
260
+ const message = {
261
+ id: String(raw.id),
262
+ channel: {
263
+ id: raw.token ?? this.roomToken,
264
+ type: 'nextcloud-talk',
265
+ name: raw.token ?? this.roomToken,
266
+ isGroup: true,
267
+ },
268
+ sender: {
269
+ id: sender,
270
+ username: sender,
271
+ displayName: raw.actorDisplayName ?? sender,
272
+ isBot: raw.actorType === 'bots',
273
+ },
274
+ content,
275
+ contentType,
276
+ timestamp: raw.timestamp ? new Date(raw.timestamp * 1000) : new Date(),
277
+ raw,
278
+ };
279
+ if (isCommand) {
280
+ const parts = content.slice(1).split(/\s+/);
281
+ message.isCommand = true;
282
+ message.commandName = parts[0];
283
+ message.commandArgs = parts.slice(1);
284
+ }
285
+ return message;
286
+ }
287
+ /** Emit 'connected' exactly once per session (on first successful poll). */
288
+ connectedEmitted = false;
289
+ onConnectedOnce() {
290
+ if (!this.connectedEmitted) {
291
+ this.connectedEmitted = true;
292
+ this.emit('connected');
293
+ }
294
+ }
295
+ // --------------------------------------------------------------------------
296
+ // Reconnection
297
+ // --------------------------------------------------------------------------
298
+ /**
299
+ * Recover a dropped connection via the shared ReconnectionManager
300
+ * (exponential backoff + jitter + exhaustion). `scheduleReconnect` is
301
+ * single-shot, so a failed attempt re-drives it (deferred past the manager's
302
+ * internal active-guard) until recovery or exhaustion — same idiom as
303
+ * irc/imessage.
304
+ */
305
+ scheduleReconnect() {
306
+ if (this.reconnecting || this.closing || !this.running) {
307
+ return;
308
+ }
309
+ if (this.reconnectionManager.listenerCount('exhausted') === 0) {
310
+ this.reconnectionManager.on('exhausted', () => {
311
+ this.reconnecting = false;
312
+ this.running = false;
313
+ this.emit('disconnected', new Error('Nextcloud Talk reconnection failed after all retries'));
314
+ logger.error('NextcloudTalkClient: reconnection failed permanently');
315
+ });
316
+ }
317
+ this.reconnecting = true;
318
+ this.emit('disconnected', new Error('Nextcloud Talk connection dropped'));
319
+ this.reconnectionManager.scheduleReconnect(async () => {
320
+ // One probe poll. Success resumes the steady-state loop; failure re-drives
321
+ // the manager.
322
+ try {
323
+ await this.pollOnce();
324
+ this.reconnecting = false;
325
+ this.reconnectionManager.onConnected();
326
+ this.emit('reconnected');
327
+ this.onConnectedOnce();
328
+ // Resume the steady-state loop.
329
+ void this.runPollLoop();
330
+ }
331
+ catch (error) {
332
+ if (this.isAbortError(error) || this.closing || !this.running) {
333
+ return; // intentional teardown mid-reconnect
334
+ }
335
+ if (this.running && !this.closing) {
336
+ setTimeout(() => {
337
+ if (this.running && this.reconnecting && !this.closing) {
338
+ this.reconnecting = false;
339
+ this.scheduleReconnect();
340
+ }
341
+ }, 0).unref?.();
342
+ }
343
+ throw error instanceof Error ? error : new Error(String(error));
344
+ }
345
+ });
346
+ }
347
+ // --------------------------------------------------------------------------
348
+ // Send
349
+ // --------------------------------------------------------------------------
350
+ async sendMessage(roomToken, text) {
351
+ const url = `${this.baseUrl}${SPREED_CHAT_PATH}/${encodeURIComponent(roomToken)}`;
352
+ const response = await fetch(url, {
353
+ method: 'POST',
354
+ headers: { ...this.buildHeaders(), 'Content-Type': 'application/json' },
355
+ body: JSON.stringify({ message: text }),
356
+ });
357
+ if (!response.ok) {
358
+ const body = await response.text().catch(() => '');
359
+ throw new Error(`Nextcloud Talk send failed: ${response.status} ${response.statusText} - ${body}`);
360
+ }
361
+ const envelope = (await response.json().catch(() => ({})));
362
+ const id = envelope.ocs?.data?.id;
363
+ return { success: true, messageId: id !== undefined ? String(id) : undefined };
364
+ }
365
+ // --------------------------------------------------------------------------
366
+ // Helpers
367
+ // --------------------------------------------------------------------------
368
+ buildHeaders() {
369
+ const headers = {
370
+ 'OCS-APIRequest': 'true',
371
+ Accept: 'application/json',
372
+ };
373
+ if (this.bearer) {
374
+ headers.Authorization = `Bearer ${this.bearer}`;
375
+ }
376
+ else {
377
+ const basic = Buffer.from(`${this.username}:${this.password}`).toString('base64');
378
+ headers.Authorization = `Basic ${basic}`;
379
+ }
380
+ return headers;
381
+ }
382
+ isAbortError(err) {
383
+ return (err instanceof Error &&
384
+ (err.name === 'AbortError' || /aborted/i.test(err.message)));
385
+ }
386
+ }
67
387
  export class NextcloudTalkChannel extends BaseChannel {
68
- adapter;
388
+ client = null;
389
+ ncConfig;
69
390
  constructor(config) {
70
391
  super('nextcloud-talk', {
71
392
  type: 'nextcloud-talk',
@@ -78,30 +399,60 @@ export class NextcloudTalkChannel extends BaseChannel {
78
399
  rateLimit: config.rateLimit,
79
400
  options: config.options,
80
401
  });
81
- this.adapter = new NextcloudTalkAdapter({
82
- url: config.url,
83
- username: config.username,
84
- password: config.password,
85
- });
402
+ this.ncConfig = config;
86
403
  }
87
404
  async connect() {
88
- await this.adapter.start();
89
- this.status.connected = true;
90
- this.status.authenticated = true;
91
- this.status.lastActivity = new Date();
92
- this.emit('connected', this.type);
405
+ const client = new NextcloudTalkClient(this.ncConfig);
406
+ this.client = client;
407
+ client.on('message', (message) => {
408
+ this.status.lastActivity = new Date();
409
+ this.emit('message', message);
410
+ if (message.isCommand) {
411
+ this.emit('command', message);
412
+ }
413
+ });
414
+ // 'connected' fires on the first successful poll — that's when we flip the
415
+ // channel status to connected (a long-poll can hold open for ~30s, so we do
416
+ // NOT block connect() on it).
417
+ client.on('connected', () => {
418
+ this.status.connected = true;
419
+ this.status.authenticated = true;
420
+ this.status.lastActivity = new Date();
421
+ this.emit('connected', this.type);
422
+ });
423
+ client.on('reconnected', () => {
424
+ this.status.connected = true;
425
+ this.status.lastActivity = new Date();
426
+ });
427
+ client.on('disconnected', (err) => {
428
+ this.status.connected = false;
429
+ this.emit('disconnected', this.type, err);
430
+ });
431
+ client.on('error', (err) => {
432
+ this.status.error = err.message;
433
+ this.emit('error', this.type, err);
434
+ });
435
+ client.start();
93
436
  }
94
437
  async disconnect() {
438
+ if (this.client) {
439
+ this.client.stop();
440
+ this.client.removeAllListeners();
441
+ this.client = null;
442
+ }
95
443
  if (!this.status.connected)
96
444
  return;
97
- await this.adapter.stop();
98
445
  this.status.connected = false;
99
446
  this.status.lastActivity = new Date();
100
447
  this.emit('disconnected', this.type);
101
448
  }
102
449
  async send(message) {
450
+ if (!this.client) {
451
+ return { success: false, error: 'Not connected', timestamp: new Date() };
452
+ }
453
+ const room = message.channelId || this.client.getRoomToken();
103
454
  try {
104
- const result = await this.adapter.sendMessage(message.channelId, message.content);
455
+ const result = await this.client.sendMessage(room, message.content);
105
456
  this.status.lastActivity = new Date();
106
457
  return {
107
458
  success: result.success,
@@ -12,7 +12,30 @@ export interface NostrConfig {
12
12
  export interface NostrChannelConfig extends ChannelConfig {
13
13
  privateKey?: string;
14
14
  relays: string[];
15
+ /** Single relay convenience alias (merged with `relays`). */
16
+ relay?: string;
17
+ /** Optional NIP-01 filters merged into the subscription REQ. */
18
+ filters?: Record<string, unknown>;
19
+ /** Max reconnection attempts per relay (default 10). */
20
+ maxRetries?: number;
21
+ /** Initial reconnect backoff in ms (default 1000). */
22
+ reconnectDelayMs?: number;
23
+ /** How long `send()` waits for a relay `OK` ack before failing (default 8000). */
24
+ publishTimeoutMs?: number;
15
25
  }
26
+ /**
27
+ * A minimal subset of the WHATWG/`ws` WebSocket surface the relay client uses.
28
+ * Lets us accept the Node global `WebSocket` without pulling in DOM lib types.
29
+ */
30
+ interface RelaySocket {
31
+ readyState: number;
32
+ send(data: string): void;
33
+ close(code?: number, reason?: string): void;
34
+ terminate?(): void;
35
+ addEventListener(type: string, listener: (event: unknown) => void): void;
36
+ }
37
+ /** Factory so tests / runtimes can inject a socket constructor. */
38
+ export type RelaySocketFactory = (url: string) => RelaySocket;
16
39
  export declare class NostrAdapter {
17
40
  private static readonly BECH32_CHARSET;
18
41
  private static readonly BECH32_GENERATOR;
@@ -32,6 +55,13 @@ export declare class NostrAdapter {
32
55
  addRelay(url: string): void;
33
56
  removeRelay(url: string): void;
34
57
  getConfig(): NostrConfig;
58
+ /**
59
+ * Resolve the configured secret as raw 32 bytes: 64-char hex as-is, `nsec...`
60
+ * bech32 decoded to its 32-byte payload, otherwise a deterministic sha256 of
61
+ * the arbitrary secret. No configured key => a fresh random 32 bytes.
62
+ * Public so {@link NostrChannel} signs under the *same* identity (esp. nsec).
63
+ */
64
+ getSecretKeyBytes(): Buffer;
35
65
  private resolvePrivateKeyBytes;
36
66
  private encodeBech32;
37
67
  private decodeBech32;
@@ -41,11 +71,101 @@ export declare class NostrAdapter {
41
71
  private expandBech32Hrp;
42
72
  private bech32Polymod;
43
73
  }
74
+ /**
75
+ * Real Nostr relay channel (NIP-01).
76
+ *
77
+ * Opens persistent WebSocket connections to each configured relay, subscribes
78
+ * to kind-1 (text note) events via a `REQ` frame, and surfaces inbound events
79
+ * as `'message'` events on the channel. Dropped sockets are recovered with the
80
+ * shared {@link ReconnectionManager} (exponential backoff + jitter), the same
81
+ * idiom used by the discord/slack/imessage adapters.
82
+ *
83
+ * Connection semantics are *optimistic*: `connect()` marks the channel
84
+ * connected and returns immediately, opening sockets in the background. This
85
+ * mirrors how a real Nostr client behaves (any one of N relays may be down)
86
+ * and lets the channel come up even when a relay is unreachable.
87
+ *
88
+ * `NostrAdapter` (above) is retained unchanged as the key / bech32 / relay-list
89
+ * utility — its `start()`/`sendDirectMessage()` are legacy in-process stubs
90
+ * kept only for the existing unit tests. The real transport lives here.
91
+ */
44
92
  export declare class NostrChannel extends BaseChannel {
45
93
  private adapter;
46
- constructor(config: NostrChannelConfig);
94
+ private relays;
95
+ private readonly subId;
96
+ private readonly filters;
97
+ private readonly maxRetries;
98
+ private readonly reconnectDelayMs;
99
+ /** Set while an intentional disconnect is in progress so `close` skips reconnect. */
100
+ private shuttingDown;
101
+ /** Injectable socket factory (defaults to the Node global `WebSocket`). */
102
+ private readonly socketFactory;
103
+ /** Outstanding publishes keyed by event id, resolved on the relay's `OK` ack. */
104
+ private readonly pendingPublishes;
105
+ /** How long to wait for a relay `["OK", id, ...]` ack before timing out. */
106
+ private readonly publishTimeoutMs;
107
+ constructor(config: NostrChannelConfig, socketFactory?: RelaySocketFactory);
108
+ /**
109
+ * Optimistically connect: mark the channel up, emit `'connected'`, then open
110
+ * every relay socket in the background. Resolves immediately — it never waits
111
+ * on a socket `'open'`, so an unreachable relay can't block startup.
112
+ */
47
113
  connect(): Promise<void>;
48
114
  disconnect(): Promise<void>;
115
+ /**
116
+ * Build, BIP-340 sign, and publish a Nostr kind-1 event for the outbound
117
+ * message.
118
+ *
119
+ * When a secret key is configured we derive the x-only `pubkey` and the
120
+ * Schnorr `sig` via `@noble/curves` (audited secp256k1/BIP-340), compute the
121
+ * canonical sha256 `id`, publish `["EVENT", event]` to every connected relay,
122
+ * and resolve from the relay's `["OK", id, accepted, msg]` acknowledgement.
123
+ *
124
+ * HONEST GATE: if NO secret key is configured we do NOT sign with a throwaway
125
+ * key — we return the same clear error as before. Live publishing is gated
126
+ * only on the operator providing their nsec/hex key + a reachable relay.
127
+ */
49
128
  send(message: OutboundMessage): Promise<DeliveryResult>;
129
+ /**
130
+ * Publish a signed event to every open relay socket and resolve from the
131
+ * FIRST `["OK", id, ...]` acknowledgement (or a timeout). The pending entry is
132
+ * keyed by event id and resolved by {@link handleRelayData}'s `OK` branch.
133
+ */
134
+ private publishToRelays;
135
+ private openRelay;
136
+ private scheduleRelayReconnect;
137
+ private sendSubscription;
138
+ private handleRelayData;
139
+ private emitInbound;
140
+ /**
141
+ * Build the canonical unsigned Nostr event for `content`. The `id` is the
142
+ * sha256 of the NIP-01 serialization `[0, pubkey, created_at, kind, tags,
143
+ * content]`. Used for the honest no-key error path (still surfaces a real id).
144
+ */
145
+ private buildUnsignedEvent;
146
+ /**
147
+ * Build a fully BIP-340 signed Nostr kind-1 event for `content`.
148
+ *
149
+ * `pubkey` and `sig` are derived from the same secp256k1 secret via
150
+ * `@noble/curves`' Schnorr primitives: `schnorr.getPublicKey` yields the
151
+ * x-only pubkey (correct BIP-340 even-y normalization), `id` is the sha256 of
152
+ * the NIP-01 serialization, and `sig = schnorr.sign(id, sk)`. Nostr signs the
153
+ * 32-byte id directly (no extra pre-hash), which is exactly noble's raw-message
154
+ * contract — so `schnorr.verify(sig, id, pubkey)` round-trips.
155
+ */
156
+ private buildSignedEvent;
157
+ /** x-only 32-byte public key as lowercase hex (Nostr event `pubkey` field). */
158
+ private getPublicKeyHex;
159
+ /**
160
+ * Resolve the signing secret as raw 32 bytes. Delegates to the adapter's
161
+ * canonical resolver so hex AND `nsec...` bech32 keys decode to the SAME
162
+ * identity the operator configured (a duplicated resolver here previously
163
+ * dropped the nsec branch and silently signed under a sha256-derived key).
164
+ */
165
+ private resolvePrivateKeyBytes;
166
+ private closeSocket;
167
+ private errorFromEvent;
168
+ /** Expose the configured relay URLs (post construction). */
169
+ getRelayUrls(): string[];
50
170
  }
51
171
  export default NostrAdapter;