@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
@@ -5,8 +5,12 @@
5
5
  * Supports NIP-04 encrypted direct messages.
6
6
  */
7
7
  import { createECDH, createHash, randomBytes } from 'crypto';
8
+ import { sha256 } from '@noble/hashes/sha256';
9
+ import { bytesToHex } from '@noble/hashes/utils';
10
+ import { schnorr } from '@noble/curves/secp256k1';
8
11
  import { logger } from '../../utils/logger.js';
9
- import { BaseChannel } from '../core.js';
12
+ import { BaseChannel, } from '../core.js';
13
+ import { ReconnectionManager } from '../reconnection-manager.js';
10
14
  export class NostrAdapter {
11
15
  static BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
12
16
  static BECH32_GENERATOR = [
@@ -78,6 +82,15 @@ export class NostrAdapter {
78
82
  getConfig() {
79
83
  return { ...this.config, relays: [...this.config.relays] };
80
84
  }
85
+ /**
86
+ * Resolve the configured secret as raw 32 bytes: 64-char hex as-is, `nsec...`
87
+ * bech32 decoded to its 32-byte payload, otherwise a deterministic sha256 of
88
+ * the arbitrary secret. No configured key => a fresh random 32 bytes.
89
+ * Public so {@link NostrChannel} signs under the *same* identity (esp. nsec).
90
+ */
91
+ getSecretKeyBytes() {
92
+ return this.resolvePrivateKeyBytes();
93
+ }
81
94
  resolvePrivateKeyBytes() {
82
95
  const raw = this.config.privateKey;
83
96
  if (!raw) {
@@ -201,9 +214,40 @@ export class NostrAdapter {
201
214
  return chk;
202
215
  }
203
216
  }
217
+ /**
218
+ * Real Nostr relay channel (NIP-01).
219
+ *
220
+ * Opens persistent WebSocket connections to each configured relay, subscribes
221
+ * to kind-1 (text note) events via a `REQ` frame, and surfaces inbound events
222
+ * as `'message'` events on the channel. Dropped sockets are recovered with the
223
+ * shared {@link ReconnectionManager} (exponential backoff + jitter), the same
224
+ * idiom used by the discord/slack/imessage adapters.
225
+ *
226
+ * Connection semantics are *optimistic*: `connect()` marks the channel
227
+ * connected and returns immediately, opening sockets in the background. This
228
+ * mirrors how a real Nostr client behaves (any one of N relays may be down)
229
+ * and lets the channel come up even when a relay is unreachable.
230
+ *
231
+ * `NostrAdapter` (above) is retained unchanged as the key / bech32 / relay-list
232
+ * utility — its `start()`/`sendDirectMessage()` are legacy in-process stubs
233
+ * kept only for the existing unit tests. The real transport lives here.
234
+ */
204
235
  export class NostrChannel extends BaseChannel {
205
236
  adapter;
206
- constructor(config) {
237
+ relays = [];
238
+ subId;
239
+ filters;
240
+ maxRetries;
241
+ reconnectDelayMs;
242
+ /** Set while an intentional disconnect is in progress so `close` skips reconnect. */
243
+ shuttingDown = false;
244
+ /** Injectable socket factory (defaults to the Node global `WebSocket`). */
245
+ socketFactory;
246
+ /** Outstanding publishes keyed by event id, resolved on the relay's `OK` ack. */
247
+ pendingPublishes = new Map();
248
+ /** How long to wait for a relay `["OK", id, ...]` ack before timing out. */
249
+ publishTimeoutMs;
250
+ constructor(config, socketFactory) {
207
251
  super('nostr', {
208
252
  type: 'nostr',
209
253
  enabled: config.enabled,
@@ -215,33 +259,120 @@ export class NostrChannel extends BaseChannel {
215
259
  rateLimit: config.rateLimit,
216
260
  options: config.options,
217
261
  });
262
+ const relayUrls = [
263
+ ...(Array.isArray(config.relays) ? config.relays : []),
264
+ ...(config.relay ? [config.relay] : []),
265
+ ].filter((u) => typeof u === 'string' && u.length > 0);
218
266
  this.adapter = new NostrAdapter({
219
267
  privateKey: config.privateKey,
220
- relays: config.relays,
268
+ relays: relayUrls,
221
269
  });
270
+ this.filters = config.filters ?? {};
271
+ this.maxRetries = config.maxRetries ?? 10;
272
+ this.reconnectDelayMs = config.reconnectDelayMs ?? 1000;
273
+ this.publishTimeoutMs = config.publishTimeoutMs ?? 8000;
274
+ this.subId = `cb-${randomBytes(6).toString('hex')}`;
275
+ this.socketFactory =
276
+ socketFactory ??
277
+ ((url) => new globalThis.WebSocket(url));
278
+ this.relays = relayUrls.map((url) => ({
279
+ url,
280
+ socket: null,
281
+ reconnection: new ReconnectionManager(`nostr:${url}`, {
282
+ maxRetries: this.maxRetries,
283
+ initialDelayMs: this.reconnectDelayMs,
284
+ maxDelayMs: 60000,
285
+ }),
286
+ }));
222
287
  }
288
+ /**
289
+ * Optimistically connect: mark the channel up, emit `'connected'`, then open
290
+ * every relay socket in the background. Resolves immediately — it never waits
291
+ * on a socket `'open'`, so an unreachable relay can't block startup.
292
+ */
223
293
  async connect() {
224
- await this.adapter.start();
294
+ this.shuttingDown = false;
225
295
  this.status.connected = true;
226
296
  this.status.authenticated = true;
227
297
  this.status.lastActivity = new Date();
228
298
  this.emit('connected', this.type);
299
+ for (const relay of this.relays) {
300
+ this.openRelay(relay);
301
+ }
229
302
  }
230
303
  async disconnect() {
231
304
  if (!this.status.connected)
232
305
  return;
233
- await this.adapter.stop();
306
+ this.shuttingDown = true;
307
+ for (const relay of this.relays) {
308
+ relay.reconnection.cancel();
309
+ this.closeSocket(relay);
310
+ }
311
+ // Fail any in-flight publishes so awaiting `send()` calls can't hang.
312
+ for (const pending of this.pendingPublishes.values()) {
313
+ pending.resolve({ accepted: false, message: 'channel disconnected', relayUrl: '' });
314
+ }
315
+ this.pendingPublishes.clear();
234
316
  this.status.connected = false;
317
+ this.status.authenticated = false;
235
318
  this.status.lastActivity = new Date();
236
319
  this.emit('disconnected', this.type);
237
320
  }
321
+ /**
322
+ * Build, BIP-340 sign, and publish a Nostr kind-1 event for the outbound
323
+ * message.
324
+ *
325
+ * When a secret key is configured we derive the x-only `pubkey` and the
326
+ * Schnorr `sig` via `@noble/curves` (audited secp256k1/BIP-340), compute the
327
+ * canonical sha256 `id`, publish `["EVENT", event]` to every connected relay,
328
+ * and resolve from the relay's `["OK", id, accepted, msg]` acknowledgement.
329
+ *
330
+ * HONEST GATE: if NO secret key is configured we do NOT sign with a throwaway
331
+ * key — we return the same clear error as before. Live publishing is gated
332
+ * only on the operator providing their nsec/hex key + a reachable relay.
333
+ */
238
334
  async send(message) {
239
335
  try {
240
- const result = await this.adapter.sendDirectMessage(message.channelId, message.content);
336
+ // Honesty guard: never sign with the random fallback key. A configured
337
+ // key is required to produce a publishable, attributable event.
338
+ if (!this.adapter.getConfig().privateKey) {
339
+ const unsigned = this.buildUnsignedEvent(message.content);
340
+ return {
341
+ success: false,
342
+ messageId: unsigned.id,
343
+ error: 'Nostr send requires a configured secret key / signer (no key configured)',
344
+ timestamp: new Date(),
345
+ };
346
+ }
347
+ const event = this.buildSignedEvent(message.content);
241
348
  this.status.lastActivity = new Date();
349
+ const sockets = this.relays
350
+ .map((r) => r.socket)
351
+ .filter((s) => s !== null && s.readyState === 1 /* OPEN */);
352
+ if (sockets.length === 0) {
353
+ return {
354
+ success: false,
355
+ messageId: event.id,
356
+ error: 'Nostr send failed: no relay socket is open',
357
+ timestamp: new Date(),
358
+ };
359
+ }
360
+ const ack = await this.publishToRelays(event, sockets);
361
+ if (ack.accepted) {
362
+ logger.debug('NostrChannel: relay accepted published event', {
363
+ id: event.id,
364
+ relay: ack.relayUrl,
365
+ });
366
+ return {
367
+ success: true,
368
+ messageId: event.id,
369
+ timestamp: new Date(),
370
+ };
371
+ }
242
372
  return {
243
- success: result.success,
244
- messageId: result.eventId,
373
+ success: false,
374
+ messageId: event.id,
375
+ error: ack.message || 'Nostr relay rejected the event',
245
376
  timestamp: new Date(),
246
377
  };
247
378
  }
@@ -253,6 +384,263 @@ export class NostrChannel extends BaseChannel {
253
384
  };
254
385
  }
255
386
  }
387
+ /**
388
+ * Publish a signed event to every open relay socket and resolve from the
389
+ * FIRST `["OK", id, ...]` acknowledgement (or a timeout). The pending entry is
390
+ * keyed by event id and resolved by {@link handleRelayData}'s `OK` branch.
391
+ */
392
+ publishToRelays(event, sockets) {
393
+ return new Promise((resolve) => {
394
+ const timer = setTimeout(() => {
395
+ this.pendingPublishes.delete(event.id);
396
+ resolve({ accepted: false, message: 'timed out waiting for relay OK', relayUrl: '' });
397
+ }, this.publishTimeoutMs);
398
+ // Don't keep the event loop alive solely for this ack timer.
399
+ if (typeof timer.unref === 'function') {
400
+ timer.unref();
401
+ }
402
+ this.pendingPublishes.set(event.id, {
403
+ resolve: (ok) => {
404
+ clearTimeout(timer);
405
+ this.pendingPublishes.delete(event.id);
406
+ resolve(ok);
407
+ },
408
+ timer,
409
+ });
410
+ const frame = JSON.stringify(['EVENT', event]);
411
+ for (const socket of sockets) {
412
+ try {
413
+ socket.send(frame);
414
+ }
415
+ catch (error) {
416
+ logger.debug('NostrChannel: failed to send EVENT frame', {
417
+ error: error instanceof Error ? error.message : String(error),
418
+ });
419
+ }
420
+ }
421
+ });
422
+ }
423
+ // ============================================================================
424
+ // Relay socket lifecycle
425
+ // ============================================================================
426
+ openRelay(relay) {
427
+ let socket;
428
+ try {
429
+ socket = this.socketFactory(relay.url);
430
+ }
431
+ catch (error) {
432
+ logger.warn('NostrChannel: failed to construct relay socket', {
433
+ url: relay.url,
434
+ error: error instanceof Error ? error.message : String(error),
435
+ });
436
+ this.scheduleRelayReconnect(relay);
437
+ return;
438
+ }
439
+ relay.socket = socket;
440
+ socket.addEventListener('open', () => {
441
+ logger.debug('NostrChannel: relay open', { url: relay.url });
442
+ relay.reconnection.onConnected();
443
+ this.status.connected = true;
444
+ this.status.lastActivity = new Date();
445
+ this.sendSubscription(socket);
446
+ });
447
+ socket.addEventListener('message', (event) => {
448
+ const data = event.data;
449
+ this.handleRelayData(relay, data);
450
+ });
451
+ socket.addEventListener('error', (event) => {
452
+ const err = this.errorFromEvent(event);
453
+ logger.debug('NostrChannel: relay socket error', { url: relay.url, error: err.message });
454
+ // BaseChannel is an EventEmitter — only emit 'error' when someone listens,
455
+ // otherwise EventEmitter throws and crashes the process.
456
+ if (this.listenerCount('error') > 0) {
457
+ this.emit('error', this.type, err);
458
+ }
459
+ });
460
+ socket.addEventListener('close', () => {
461
+ relay.socket = null;
462
+ if (this.shuttingDown)
463
+ return;
464
+ logger.debug('NostrChannel: relay closed, scheduling reconnect', { url: relay.url });
465
+ this.scheduleRelayReconnect(relay);
466
+ });
467
+ }
468
+ scheduleRelayReconnect(relay) {
469
+ if (this.shuttingDown)
470
+ return;
471
+ relay.reconnection.scheduleReconnect(async () => {
472
+ if (this.shuttingDown)
473
+ return;
474
+ this.openRelay(relay);
475
+ });
476
+ }
477
+ sendSubscription(socket) {
478
+ const filter = { kinds: [1], ...this.filters };
479
+ const req = JSON.stringify(['REQ', this.subId, filter]);
480
+ try {
481
+ socket.send(req);
482
+ }
483
+ catch (error) {
484
+ logger.debug('NostrChannel: failed to send REQ', {
485
+ error: error instanceof Error ? error.message : String(error),
486
+ });
487
+ }
488
+ }
489
+ handleRelayData(relay, data) {
490
+ let frame;
491
+ try {
492
+ const text = typeof data === 'string' ? data : String(data);
493
+ frame = JSON.parse(text);
494
+ }
495
+ catch {
496
+ logger.debug('NostrChannel: ignoring non-JSON relay frame', { url: relay.url });
497
+ return;
498
+ }
499
+ if (!Array.isArray(frame) || frame.length === 0)
500
+ return;
501
+ const kind = frame[0];
502
+ if (kind === 'EVENT') {
503
+ // ["EVENT", <subId>, <event>]
504
+ const event = frame[2];
505
+ if (event && typeof event === 'object') {
506
+ this.emitInbound(event, relay.url);
507
+ }
508
+ return;
509
+ }
510
+ if (kind === 'EOSE') {
511
+ // End of stored events — nothing to do.
512
+ return;
513
+ }
514
+ if (kind === 'NOTICE') {
515
+ logger.info('NostrChannel: relay NOTICE', { url: relay.url, message: String(frame[1] ?? '') });
516
+ return;
517
+ }
518
+ if (kind === 'OK') {
519
+ // Publish acknowledgement: ["OK", <eventId>, <ok>, <message>]
520
+ const eventId = typeof frame[1] === 'string' ? frame[1] : '';
521
+ const accepted = frame[2] === true;
522
+ const message = typeof frame[3] === 'string' ? frame[3] : '';
523
+ logger.debug('NostrChannel: relay OK', { url: relay.url, eventId, accepted, message });
524
+ const pending = this.pendingPublishes.get(eventId);
525
+ if (pending) {
526
+ pending.resolve({ accepted, message, relayUrl: relay.url });
527
+ }
528
+ return;
529
+ }
530
+ }
531
+ emitInbound(event, relayUrl) {
532
+ const content = typeof event.content === 'string' ? event.content : '';
533
+ const pubkey = typeof event.pubkey === 'string' ? event.pubkey : 'unknown';
534
+ const id = typeof event.id === 'string' ? event.id : `nostr-${Date.now()}`;
535
+ const createdAt = typeof event.created_at === 'number' ? event.created_at : Math.floor(Date.now() / 1000);
536
+ const inbound = {
537
+ id,
538
+ channel: {
539
+ id: relayUrl,
540
+ type: 'nostr',
541
+ name: relayUrl,
542
+ isDM: false,
543
+ },
544
+ sender: {
545
+ id: pubkey,
546
+ username: pubkey,
547
+ },
548
+ content,
549
+ contentType: 'text',
550
+ timestamp: new Date(createdAt * 1000),
551
+ raw: event,
552
+ };
553
+ this.status.lastActivity = new Date();
554
+ const parsed = this.parseCommand(inbound);
555
+ this.emit('message', parsed);
556
+ if (parsed.isCommand) {
557
+ this.emit('command', parsed);
558
+ }
559
+ }
560
+ /**
561
+ * Build the canonical unsigned Nostr event for `content`. The `id` is the
562
+ * sha256 of the NIP-01 serialization `[0, pubkey, created_at, kind, tags,
563
+ * content]`. Used for the honest no-key error path (still surfaces a real id).
564
+ */
565
+ buildUnsignedEvent(content) {
566
+ const pubkey = this.getPublicKeyHex();
567
+ const created_at = Math.floor(Date.now() / 1000);
568
+ const kind = 1;
569
+ const tags = [];
570
+ const serialized = JSON.stringify([0, pubkey, created_at, kind, tags, content]);
571
+ const id = bytesToHex(sha256(new TextEncoder().encode(serialized)));
572
+ return { id, pubkey, created_at, kind, tags, content };
573
+ }
574
+ /**
575
+ * Build a fully BIP-340 signed Nostr kind-1 event for `content`.
576
+ *
577
+ * `pubkey` and `sig` are derived from the same secp256k1 secret via
578
+ * `@noble/curves`' Schnorr primitives: `schnorr.getPublicKey` yields the
579
+ * x-only pubkey (correct BIP-340 even-y normalization), `id` is the sha256 of
580
+ * the NIP-01 serialization, and `sig = schnorr.sign(id, sk)`. Nostr signs the
581
+ * 32-byte id directly (no extra pre-hash), which is exactly noble's raw-message
582
+ * contract — so `schnorr.verify(sig, id, pubkey)` round-trips.
583
+ */
584
+ buildSignedEvent(content) {
585
+ const sk = this.resolvePrivateKeyBytes();
586
+ const pubkey = bytesToHex(schnorr.getPublicKey(sk));
587
+ const created_at = Math.floor(Date.now() / 1000);
588
+ const kind = 1;
589
+ const tags = [];
590
+ const serialized = JSON.stringify([0, pubkey, created_at, kind, tags, content]);
591
+ const id = bytesToHex(sha256(new TextEncoder().encode(serialized)));
592
+ const sig = bytesToHex(schnorr.sign(id, sk));
593
+ return { id, pubkey, created_at, kind, tags, content, sig };
594
+ }
595
+ /** x-only 32-byte public key as lowercase hex (Nostr event `pubkey` field). */
596
+ getPublicKeyHex() {
597
+ return bytesToHex(schnorr.getPublicKey(this.resolvePrivateKeyBytes()));
598
+ }
599
+ /**
600
+ * Resolve the signing secret as raw 32 bytes. Delegates to the adapter's
601
+ * canonical resolver so hex AND `nsec...` bech32 keys decode to the SAME
602
+ * identity the operator configured (a duplicated resolver here previously
603
+ * dropped the nsec branch and silently signed under a sha256-derived key).
604
+ */
605
+ resolvePrivateKeyBytes() {
606
+ return this.adapter.getSecretKeyBytes();
607
+ }
608
+ closeSocket(relay) {
609
+ const socket = relay.socket;
610
+ relay.socket = null;
611
+ if (!socket)
612
+ return;
613
+ try {
614
+ // Prefer terminate(): the socket may still be CONNECTING against a dead
615
+ // relay, where close() would hang waiting for a handshake that won't come.
616
+ if (typeof socket.terminate === 'function') {
617
+ socket.terminate();
618
+ }
619
+ else {
620
+ socket.close();
621
+ }
622
+ }
623
+ catch (error) {
624
+ logger.debug('NostrChannel: error closing relay socket', {
625
+ url: relay.url,
626
+ error: error instanceof Error ? error.message : String(error),
627
+ });
628
+ }
629
+ }
630
+ errorFromEvent(event) {
631
+ if (event instanceof Error)
632
+ return event;
633
+ const maybe = event;
634
+ if (maybe?.error instanceof Error)
635
+ return maybe.error;
636
+ if (typeof maybe?.message === 'string')
637
+ return new Error(maybe.message);
638
+ return new Error('Nostr relay socket error');
639
+ }
640
+ /** Expose the configured relay URLs (post construction). */
641
+ getRelayUrls() {
642
+ return this.relays.map((r) => r.url);
643
+ }
256
644
  }
257
645
  export default NostrAdapter;
258
646
  //# sourceMappingURL=index.js.map
@@ -99,6 +99,9 @@ export class NtfyChannel extends BaseChannel {
99
99
  tags: config.tags,
100
100
  });
101
101
  }
102
+ // REST adapter — outbound messages are one-shot HTTP POSTs to an ntfy topic;
103
+ // there is no persistent subscription stream held open here, so reconnection
104
+ // (ReconnectionManager) is N/A.
102
105
  async connect() {
103
106
  await this.adapter.start();
104
107
  this.status.connected = true;
@@ -97,6 +97,9 @@ export class QQChannel extends BaseChannel {
97
97
  autoEscape: config.autoEscape,
98
98
  });
99
99
  }
100
+ // REST adapter — outbound messages are one-shot HTTP POSTs to a OneBot v11
101
+ // gateway; there is no persistent connection held open here, so reconnection
102
+ // (ReconnectionManager) is N/A.
100
103
  async connect() {
101
104
  await this.adapter.start();
102
105
  this.status.connected = true;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Per-Platform Slash-Command Parity Manifest
3
+ *
4
+ * Defines the expected slash commands per messaging platform and
5
+ * compares them against the actual registered commands per adapter.
6
+ * Returns a machine-readable readiness report for each platform.
7
+ */
8
+ import type { ChannelType, BaseChannel } from './core.js';
9
+ import { ChannelManager } from './core.js';
10
+ /**
11
+ * Describes a single expected slash command on a platform.
12
+ */
13
+ export interface SlashCommandSpec {
14
+ /** Command name (without leading /) */
15
+ name: string;
16
+ /** Human-readable description */
17
+ description: string;
18
+ /** Whether this command is required for parity (default: true) */
19
+ required?: boolean;
20
+ }
21
+ /**
22
+ * Per-platform parity result for a single command.
23
+ */
24
+ export interface SlashCommandParityEntry {
25
+ /** Command name */
26
+ name: string;
27
+ /** Whether this command is present on the platform adapter */
28
+ present: boolean;
29
+ /** Whether this command is required */
30
+ required: boolean;
31
+ }
32
+ /**
33
+ * Per-platform parity report.
34
+ */
35
+ export interface PlatformSlashParityReport {
36
+ /** Platform identifier */
37
+ platform: ChannelType;
38
+ /** Whether the platform adapter is registered in the channel manager */
39
+ adapterRegistered: boolean;
40
+ /** Total expected commands */
41
+ expectedCount: number;
42
+ /** Number of commands present on the adapter */
43
+ presentCount: number;
44
+ /** Number of missing required commands */
45
+ missingRequiredCount: number;
46
+ /** Number of missing optional commands */
47
+ missingOptionalCount: number;
48
+ /** Per-command parity entries */
49
+ commands: SlashCommandParityEntry[];
50
+ /** Overall parity status */
51
+ status: 'full' | 'partial' | 'none' | 'no-adapter';
52
+ }
53
+ /**
54
+ * Full parity manifest across all platforms.
55
+ */
56
+ export interface SlashParityManifest {
57
+ /** Generation timestamp */
58
+ generatedAt: string;
59
+ /** Overall parity status */
60
+ ok: boolean;
61
+ /** Total platforms checked */
62
+ totalPlatforms: number;
63
+ /** Number of platforms with full parity */
64
+ fullParityCount: number;
65
+ /** Number of platforms with partial parity */
66
+ partialParityCount: number;
67
+ /** Number of platforms with no parity (missing adapter) */
68
+ noAdapterCount: number;
69
+ /** Per-platform reports */
70
+ platforms: PlatformSlashParityReport[];
71
+ }
72
+ /**
73
+ * The canonical set of slash commands expected on each platform.
74
+ *
75
+ * These represent the Code Buddy bot commands that users should be
76
+ * able to invoke from each messaging platform. The manifest is
77
+ * intentionally conservative — platforms may support additional
78
+ * commands beyond what is listed here.
79
+ */
80
+ export declare const EXPECTED_SLASH_COMMANDS: Record<string, SlashCommandSpec[]>;
81
+ /**
82
+ * Interface for adapters that expose their registered commands.
83
+ *
84
+ * Channel adapters can optionally implement this interface to
85
+ * allow the parity checker to extract their actual registered
86
+ * commands. Otherwise, fallback heuristics are used.
87
+ */
88
+ export interface SlashCommandProvider {
89
+ /** Return the list of registered slash command names (without leading /) */
90
+ getRegisteredCommands(): string[];
91
+ }
92
+ /**
93
+ * Extract the actual registered commands from a channel adapter.
94
+ *
95
+ * Uses the SlashCommandProvider interface if available, otherwise
96
+ * returns an empty array (no commands can be detected).
97
+ */
98
+ export declare function extractActualCommands(channel: BaseChannel): string[];
99
+ /**
100
+ * Build a parity report for a single platform.
101
+ *
102
+ * @param platform - The platform channel type
103
+ * @param expectedCommands - The expected slash commands
104
+ * @param actualCommands - The actual registered commands
105
+ * @param adapterRegistered - Whether the adapter is in the ChannelManager
106
+ */
107
+ export declare function buildPlatformParityReport(platform: ChannelType, expectedCommands: SlashCommandSpec[], actualCommands: string[], adapterRegistered: boolean): PlatformSlashParityReport;
108
+ /**
109
+ * Build the full slash-command parity manifest across all tracked platforms.
110
+ *
111
+ * @param manager - Optional ChannelManager instance (defaults to singleton)
112
+ */
113
+ export declare function buildSlashParityManifest(manager?: ChannelManager): SlashParityManifest;
114
+ /**
115
+ * Render the parity manifest as a human-readable text report.
116
+ */
117
+ export declare function renderSlashParityManifest(manifest: SlashParityManifest): string;