@reefclaw/openclaw-plugin 0.1.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 (371) hide show
  1. package/.gitignore +8 -0
  2. package/audit/mode-transition-audit.d.ts +11 -0
  3. package/audit/mode-transition-audit.js +29 -0
  4. package/balance-utils.d.ts +36 -0
  5. package/balance-utils.js +98 -0
  6. package/bridge/bridge.d.ts +109 -0
  7. package/bridge/bridge.js +1036 -0
  8. package/bridge/config.d.ts +43 -0
  9. package/bridge/config.js +176 -0
  10. package/bridge/connector.d.ts +52 -0
  11. package/bridge/connector.js +293 -0
  12. package/bridge/event-replay-buffer.d.ts +43 -0
  13. package/bridge/event-replay-buffer.js +109 -0
  14. package/bridge/gateway/event-parser.d.ts +209 -0
  15. package/bridge/gateway/event-parser.js +793 -0
  16. package/bridge/gateway/gateway-config.d.ts +39 -0
  17. package/bridge/gateway/gateway-config.js +86 -0
  18. package/bridge/gateway/gateway-http-client.d.ts +50 -0
  19. package/bridge/gateway/gateway-http-client.js +165 -0
  20. package/bridge/gateway/gateway-ws-client.d.ts +116 -0
  21. package/bridge/gateway/gateway-ws-client.js +417 -0
  22. package/bridge/gateway/poller.d.ts +146 -0
  23. package/bridge/gateway/poller.js +505 -0
  24. package/bridge/gateway/tool-discovery.d.ts +25 -0
  25. package/bridge/gateway/tool-discovery.js +198 -0
  26. package/bridge/index.d.ts +2 -0
  27. package/bridge/index.js +253 -0
  28. package/bridge/logger.d.ts +2 -0
  29. package/bridge/logger.js +2 -0
  30. package/bridge/provider.d.ts +156 -0
  31. package/bridge/provider.js +2 -0
  32. package/bridge/providers/emergency-commands.d.ts +54 -0
  33. package/bridge/providers/emergency-commands.js +235 -0
  34. package/bridge/providers/gateway.d.ts +322 -0
  35. package/bridge/providers/gateway.js +2302 -0
  36. package/bridge/providers/mock.d.ts +37 -0
  37. package/bridge/providers/mock.js +385 -0
  38. package/bridge/providers/onboarding-commands.d.ts +83 -0
  39. package/bridge/providers/onboarding-commands.js +213 -0
  40. package/bridge/providers/risk-calculator.d.ts +96 -0
  41. package/bridge/providers/risk-calculator.js +369 -0
  42. package/bridge/setup.d.ts +32 -0
  43. package/bridge/setup.js +226 -0
  44. package/bridge/types.d.ts +584 -0
  45. package/bridge/types.js +50 -0
  46. package/bridge/utils/reconnect.d.ts +6 -0
  47. package/bridge/utils/reconnect.js +6 -0
  48. package/bridge/utils/skill-signing.d.ts +51 -0
  49. package/bridge/utils/skill-signing.js +138 -0
  50. package/bridge/utils/skill-version.d.ts +17 -0
  51. package/bridge/utils/skill-version.js +74 -0
  52. package/ccxt/binance-ban-gate.d.ts +47 -0
  53. package/ccxt/binance-ban-gate.js +409 -0
  54. package/ccxt/binance-private.d.ts +325 -0
  55. package/ccxt/binance-private.js +1415 -0
  56. package/ccxt/binance-public.d.ts +18 -0
  57. package/ccxt/binance-public.js +147 -0
  58. package/config/agent-config-client.d.ts +55 -0
  59. package/config/agent-config-client.js +145 -0
  60. package/config/agent-config-poller.d.ts +25 -0
  61. package/config/agent-config-poller.js +100 -0
  62. package/config/brackets-config.d.ts +22 -0
  63. package/config/brackets-config.js +58 -0
  64. package/config/gate-store.d.ts +18 -0
  65. package/config/gate-store.js +61 -0
  66. package/config/plugin-config-io.d.ts +181 -0
  67. package/config/plugin-config-io.js +84 -0
  68. package/config/position-review-config.d.ts +35 -0
  69. package/config/position-review-config.js +105 -0
  70. package/config/tool-gate.d.ts +53 -0
  71. package/config/tool-gate.js +125 -0
  72. package/config/user-data-stream-config.d.ts +85 -0
  73. package/config/user-data-stream-config.js +224 -0
  74. package/connector-supervisor.d.ts +36 -0
  75. package/connector-supervisor.js +149 -0
  76. package/exchange-adapter.d.ts +49 -0
  77. package/exchange-adapter.js +4 -0
  78. package/index.d.ts +30 -0
  79. package/index.js +2030 -0
  80. package/ingest/pending-entry-metadata.d.ts +52 -0
  81. package/ingest/pending-entry-metadata.js +182 -0
  82. package/ingest/position-auto-capture.d.ts +98 -0
  83. package/ingest/position-auto-capture.js +394 -0
  84. package/ingest/position-decisions-client.d.ts +318 -0
  85. package/ingest/position-decisions-client.js +296 -0
  86. package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
  87. package/ingest/reconcile-db-vs-exchange.js +114 -0
  88. package/ingest/reconciler-cleanup.d.ts +37 -0
  89. package/ingest/reconciler-cleanup.js +147 -0
  90. package/ingest/rest-gap-filler.d.ts +191 -0
  91. package/ingest/rest-gap-filler.js +565 -0
  92. package/ingest/touched-symbols-store.d.ts +25 -0
  93. package/ingest/touched-symbols-store.js +96 -0
  94. package/ingest/trade-store-client.d.ts +40 -0
  95. package/ingest/trade-store-client.js +125 -0
  96. package/ingest/ws-ingest.d.ts +43 -0
  97. package/ingest/ws-ingest.js +126 -0
  98. package/learning/setup-family.d.ts +21 -0
  99. package/learning/setup-family.js +103 -0
  100. package/lifecycle/install-signal-handlers.d.ts +33 -0
  101. package/lifecycle/install-signal-handlers.js +112 -0
  102. package/lifecycle/shutdown-coordinator.d.ts +43 -0
  103. package/lifecycle/shutdown-coordinator.js +131 -0
  104. package/live/bracket-id.d.ts +18 -0
  105. package/live/bracket-id.js +81 -0
  106. package/live/bracket-ledger.d.ts +54 -0
  107. package/live/bracket-ledger.js +267 -0
  108. package/live/bracket-manager.d.ts +82 -0
  109. package/live/bracket-manager.js +478 -0
  110. package/live/bracket-params.d.ts +22 -0
  111. package/live/bracket-params.js +124 -0
  112. package/live/bracket-reconciler.d.ts +95 -0
  113. package/live/bracket-reconciler.js +573 -0
  114. package/live/bracket-types.d.ts +102 -0
  115. package/live/bracket-types.js +8 -0
  116. package/live/deposit-tracker.d.ts +62 -0
  117. package/live/deposit-tracker.js +97 -0
  118. package/live/emergency-controls.d.ts +32 -0
  119. package/live/emergency-controls.js +226 -0
  120. package/live/exchange-errors.d.ts +12 -0
  121. package/live/exchange-errors.js +130 -0
  122. package/live/exchange-info-cache.d.ts +35 -0
  123. package/live/exchange-info-cache.js +119 -0
  124. package/live/fact-subscriber.d.ts +78 -0
  125. package/live/fact-subscriber.js +182 -0
  126. package/live/intent-journal.d.ts +42 -0
  127. package/live/intent-journal.js +122 -0
  128. package/live/listen-key-manager.d.ts +70 -0
  129. package/live/listen-key-manager.js +169 -0
  130. package/live/live-adapter.d.ts +264 -0
  131. package/live/live-adapter.js +1665 -0
  132. package/live/live-balance-enricher.d.ts +32 -0
  133. package/live/live-balance-enricher.js +104 -0
  134. package/live/live-bracket-api.d.ts +13 -0
  135. package/live/live-bracket-api.js +20 -0
  136. package/live/live-state-store.d.ts +194 -0
  137. package/live/live-state-store.js +450 -0
  138. package/live/local-signal-service.d.ts +57 -0
  139. package/live/local-signal-service.js +146 -0
  140. package/live/local-strategy-evaluator.d.ts +62 -0
  141. package/live/local-strategy-evaluator.js +127 -0
  142. package/live/microstructure-assembler.d.ts +54 -0
  143. package/live/microstructure-assembler.js +148 -0
  144. package/live/order-poller.d.ts +29 -0
  145. package/live/order-poller.js +125 -0
  146. package/live/position-state-store.d.ts +83 -0
  147. package/live/position-state-store.js +237 -0
  148. package/live/proposal-decision-listener.d.ts +64 -0
  149. package/live/proposal-decision-listener.js +288 -0
  150. package/live/proposal-manager.d.ts +76 -0
  151. package/live/proposal-manager.js +140 -0
  152. package/live/rate-limiter.d.ts +47 -0
  153. package/live/rate-limiter.js +159 -0
  154. package/live/reconciler.d.ts +39 -0
  155. package/live/reconciler.js +175 -0
  156. package/live/setup-buckets.d.ts +7 -0
  157. package/live/setup-buckets.js +33 -0
  158. package/live/slippage-tracker.d.ts +45 -0
  159. package/live/slippage-tracker.js +78 -0
  160. package/live/stop-watcher.d.ts +34 -0
  161. package/live/stop-watcher.js +158 -0
  162. package/live/user-data-active-probe.d.ts +54 -0
  163. package/live/user-data-active-probe.js +180 -0
  164. package/live/user-data-stream-controller.d.ts +200 -0
  165. package/live/user-data-stream-controller.js +579 -0
  166. package/live/user-data-stream-ws.d.ts +22 -0
  167. package/live/user-data-stream-ws.js +63 -0
  168. package/live/user-data-stream.d.ts +243 -0
  169. package/live/user-data-stream.js +704 -0
  170. package/logger.d.ts +2 -0
  171. package/logger.js +2 -0
  172. package/mfe.d.ts +21 -0
  173. package/mfe.js +68 -0
  174. package/onboarding/mode-ladder.d.ts +1 -0
  175. package/onboarding/mode-ladder.js +3 -0
  176. package/onboarding/runtime.d.ts +71 -0
  177. package/onboarding/runtime.js +153 -0
  178. package/openclaw.plugin.json +94 -0
  179. package/package.json +27 -0
  180. package/paper-adapter.d.ts +24 -0
  181. package/paper-adapter.js +91 -0
  182. package/persistence/state-manager.d.ts +42 -0
  183. package/persistence/state-manager.js +164 -0
  184. package/pinned-plan.d.ts +9 -0
  185. package/pinned-plan.js +23 -0
  186. package/risk/pre-trade-check.d.ts +38 -0
  187. package/risk/pre-trade-check.js +345 -0
  188. package/risk/pre-trade-types.d.ts +60 -0
  189. package/risk/pre-trade-types.js +3 -0
  190. package/scripts/assemble.mjs +114 -0
  191. package/shadow/shadow-tracker.d.ts +36 -0
  192. package/shadow/shadow-tracker.js +151 -0
  193. package/shadow/types.d.ts +42 -0
  194. package/shadow/types.js +20 -0
  195. package/shared/indicators-extended.d.ts +52 -0
  196. package/shared/indicators-extended.js +291 -0
  197. package/shared/indicators.d.ts +15 -0
  198. package/shared/indicators.js +114 -0
  199. package/signals/conditions/registry.d.ts +16 -0
  200. package/signals/conditions/registry.js +1274 -0
  201. package/signals/conditions/types.d.ts +1 -0
  202. package/signals/conditions/types.js +4 -0
  203. package/signals/direction-rules.d.ts +3 -0
  204. package/signals/direction-rules.js +24 -0
  205. package/signals/entry-rules.d.ts +6 -0
  206. package/signals/entry-rules.js +33 -0
  207. package/signals/serialize-context.d.ts +4 -0
  208. package/signals/serialize-context.js +39 -0
  209. package/signals/stop-rules.d.ts +3 -0
  210. package/signals/stop-rules.js +48 -0
  211. package/signals/strategy-adapter.d.ts +14 -0
  212. package/signals/strategy-adapter.js +122 -0
  213. package/signals/types.d.ts +1 -0
  214. package/signals/types.js +8 -0
  215. package/simulator/exchange-simulator.d.ts +93 -0
  216. package/simulator/exchange-simulator.js +684 -0
  217. package/simulator/fill-engine.d.ts +53 -0
  218. package/simulator/fill-engine.js +276 -0
  219. package/simulator/paper-market-feed.d.ts +26 -0
  220. package/simulator/paper-market-feed.js +104 -0
  221. package/simulator/realistic-fills.d.ts +59 -0
  222. package/simulator/realistic-fills.js +175 -0
  223. package/simulator/types.d.ts +219 -0
  224. package/simulator/types.js +43 -0
  225. package/skills/reefclaw/SKILL.md +97 -0
  226. package/strategy/builtin-strategies.d.ts +2 -0
  227. package/strategy/builtin-strategies.js +109 -0
  228. package/strategy/condition-registry.d.ts +3 -0
  229. package/strategy/condition-registry.js +153 -0
  230. package/strategy/evaluator.d.ts +67 -0
  231. package/strategy/evaluator.js +93 -0
  232. package/tools/assessment-validation.d.ts +118 -0
  233. package/tools/assessment-validation.js +415 -0
  234. package/tools/attach-brackets.d.ts +34 -0
  235. package/tools/attach-brackets.js +363 -0
  236. package/tools/audit-bracket-protection.d.ts +49 -0
  237. package/tools/audit-bracket-protection.js +527 -0
  238. package/tools/cancel-all-orders.d.ts +7 -0
  239. package/tools/cancel-all-orders.js +5 -0
  240. package/tools/cancel-order.d.ts +10 -0
  241. package/tools/cancel-order.js +14 -0
  242. package/tools/check-position-health.d.ts +46 -0
  243. package/tools/check-position-health.js +194 -0
  244. package/tools/clear-exchange-credentials.d.ts +24 -0
  245. package/tools/clear-exchange-credentials.js +70 -0
  246. package/tools/close-position.d.ts +22 -0
  247. package/tools/close-position.js +449 -0
  248. package/tools/create-order.d.ts +54 -0
  249. package/tools/create-order.js +338 -0
  250. package/tools/exit-gate.d.ts +58 -0
  251. package/tools/exit-gate.js +162 -0
  252. package/tools/fetch-balance.d.ts +5 -0
  253. package/tools/fetch-balance.js +4 -0
  254. package/tools/fetch-ohlcv.d.ts +11 -0
  255. package/tools/fetch-ohlcv.js +8 -0
  256. package/tools/fetch-open-orders.d.ts +7 -0
  257. package/tools/fetch-open-orders.js +4 -0
  258. package/tools/fetch-positions.d.ts +7 -0
  259. package/tools/fetch-positions.js +4 -0
  260. package/tools/fetch-ticker.d.ts +11 -0
  261. package/tools/fetch-ticker.js +5 -0
  262. package/tools/get-agent-profile.d.ts +4 -0
  263. package/tools/get-agent-profile.js +6 -0
  264. package/tools/get-analytics.d.ts +6 -0
  265. package/tools/get-analytics.js +7 -0
  266. package/tools/get-backtest.d.ts +12 -0
  267. package/tools/get-backtest.js +91 -0
  268. package/tools/get-basis.d.ts +7 -0
  269. package/tools/get-basis.js +7 -0
  270. package/tools/get-bracket-config.d.ts +11 -0
  271. package/tools/get-bracket-config.js +24 -0
  272. package/tools/get-cascade-risk.d.ts +7 -0
  273. package/tools/get-cascade-risk.js +8 -0
  274. package/tools/get-crypto-metrics.d.ts +18 -0
  275. package/tools/get-crypto-metrics.js +45 -0
  276. package/tools/get-cvd.d.ts +6 -0
  277. package/tools/get-cvd.js +6 -0
  278. package/tools/get-divergences.d.ts +6 -0
  279. package/tools/get-divergences.js +6 -0
  280. package/tools/get-funding-context.d.ts +6 -0
  281. package/tools/get-funding-context.js +16 -0
  282. package/tools/get-liquidation-levels.d.ts +7 -0
  283. package/tools/get-liquidation-levels.js +7 -0
  284. package/tools/get-liquidation-pulse.d.ts +9 -0
  285. package/tools/get-liquidation-pulse.js +22 -0
  286. package/tools/get-market-breadth.d.ts +6 -0
  287. package/tools/get-market-breadth.js +8 -0
  288. package/tools/get-market-intel.d.ts +19 -0
  289. package/tools/get-market-intel.js +116 -0
  290. package/tools/get-market-structure.d.ts +47 -0
  291. package/tools/get-market-structure.js +198 -0
  292. package/tools/get-my-mined-patterns.d.ts +20 -0
  293. package/tools/get-my-mined-patterns.js +61 -0
  294. package/tools/get-my-proposed-learnings.d.ts +20 -0
  295. package/tools/get-my-proposed-learnings.js +55 -0
  296. package/tools/get-my-recent-reviews.d.ts +22 -0
  297. package/tools/get-my-recent-reviews.js +66 -0
  298. package/tools/get-orderbook.d.ts +21 -0
  299. package/tools/get-orderbook.js +32 -0
  300. package/tools/get-pattern-scan.d.ts +7 -0
  301. package/tools/get-pattern-scan.js +8 -0
  302. package/tools/get-regime.d.ts +6 -0
  303. package/tools/get-regime.js +7 -0
  304. package/tools/get-relevant-learnings.d.ts +21 -0
  305. package/tools/get-relevant-learnings.js +65 -0
  306. package/tools/get-resting-liquidity.d.ts +6 -0
  307. package/tools/get-resting-liquidity.js +11 -0
  308. package/tools/get-risk-scenario.d.ts +29 -0
  309. package/tools/get-risk-scenario.js +47 -0
  310. package/tools/get-risk-summary.d.ts +51 -0
  311. package/tools/get-risk-summary.js +118 -0
  312. package/tools/get-sentiment.d.ts +4 -0
  313. package/tools/get-sentiment.js +6 -0
  314. package/tools/get-session-review.d.ts +7 -0
  315. package/tools/get-session-review.js +8 -0
  316. package/tools/get-setup-detail.d.ts +7 -0
  317. package/tools/get-setup-detail.js +303 -0
  318. package/tools/get-signals.d.ts +15 -0
  319. package/tools/get-signals.js +54 -0
  320. package/tools/get-sizing.d.ts +6 -0
  321. package/tools/get-sizing.js +6 -0
  322. package/tools/get-trade-feedback.d.ts +7 -0
  323. package/tools/get-trade-feedback.js +8 -0
  324. package/tools/get-trade-flow.d.ts +7 -0
  325. package/tools/get-trade-flow.js +7 -0
  326. package/tools/get-volume-analysis.d.ts +21 -0
  327. package/tools/get-volume-analysis.js +74 -0
  328. package/tools/get-volume-profile.d.ts +7 -0
  329. package/tools/get-volume-profile.js +7 -0
  330. package/tools/helpers.d.ts +25 -0
  331. package/tools/helpers.js +38 -0
  332. package/tools/intel-api.d.ts +14 -0
  333. package/tools/intel-api.js +52 -0
  334. package/tools/intel-cache.d.ts +25 -0
  335. package/tools/intel-cache.js +133 -0
  336. package/tools/list-strategies.d.ts +7 -0
  337. package/tools/list-strategies.js +6 -0
  338. package/tools/modify-stop.d.ts +17 -0
  339. package/tools/modify-stop.js +81 -0
  340. package/tools/modify-target.d.ts +17 -0
  341. package/tools/modify-target.js +71 -0
  342. package/tools/propose-learning.d.ts +22 -0
  343. package/tools/propose-learning.js +65 -0
  344. package/tools/query-review-outcomes.d.ts +30 -0
  345. package/tools/query-review-outcomes.js +64 -0
  346. package/tools/query-trades.d.ts +21 -0
  347. package/tools/query-trades.js +37 -0
  348. package/tools/record-position-reviews.d.ts +38 -0
  349. package/tools/record-position-reviews.js +147 -0
  350. package/tools/save-strategy.d.ts +16 -0
  351. package/tools/save-strategy.js +46 -0
  352. package/tools/scan-pairs.d.ts +18 -0
  353. package/tools/scan-pairs.js +220 -0
  354. package/tools/score-setup.d.ts +31 -0
  355. package/tools/score-setup.js +268 -0
  356. package/tools/set-bracket-requirement.d.ts +18 -0
  357. package/tools/set-bracket-requirement.js +81 -0
  358. package/tools/set-exchange-credentials.d.ts +25 -0
  359. package/tools/set-exchange-credentials.js +80 -0
  360. package/tools/set-trading-mode.d.ts +26 -0
  361. package/tools/set-trading-mode.js +135 -0
  362. package/tools/test-exchange-credentials.d.ts +16 -0
  363. package/tools/test-exchange-credentials.js +100 -0
  364. package/tools/toggle-strategy.d.ts +8 -0
  365. package/tools/toggle-strategy.js +8 -0
  366. package/trading-params-cache.d.ts +26 -0
  367. package/trading-params-cache.js +52 -0
  368. package/types.d.ts +110 -0
  369. package/types.js +7 -0
  370. package/util/plugin-paths.d.ts +3 -0
  371. package/util/plugin-paths.js +15 -0
@@ -0,0 +1,43 @@
1
+ export interface ResolvedConfig {
2
+ token: string;
3
+ userId: string;
4
+ relayUrl: string;
5
+ }
6
+ /** Read the OpenClaw config file and extract ReefClaw skill settings.
7
+ * Prefers the schema-valid nested shape (entry.config.*), falls back to the
8
+ * legacy flat shape per-field. */
9
+ export declare function readOpenClawConfig(): {
10
+ token?: string;
11
+ userId?: string;
12
+ relayUrl?: string;
13
+ } | null;
14
+ /** Write/merge token (and optional fields) into OpenClaw config */
15
+ export declare function writeOpenClawConfig(token: string, userId?: string, relayUrl?: string): void;
16
+ /**
17
+ * Check and fix gateway scope config on startup. Runs once — reads openclaw.json,
18
+ * applies missing settings if needed, writes back. No-op if already correct.
19
+ */
20
+ export declare function ensureGatewayScopeConfigOnStartup(): void;
21
+ export interface CliArgs {
22
+ provider: 'mock' | 'gateway';
23
+ token?: string;
24
+ userId?: string;
25
+ relayUrl?: string;
26
+ logLevel?: string;
27
+ setup?: boolean | string;
28
+ }
29
+ /**
30
+ * Resolve config with priority:
31
+ * 1. CLI args (--token, --user-id, --relay-url)
32
+ * 2. Environment variables (REEFCLAW_TOKEN, REEFCLAW_USER_ID, REEFCLAW_RELAY_URL)
33
+ * 3. OpenClaw config file (~/.openclaw/openclaw.json)
34
+ * 4. Fallback env vars (MOCK_TOKEN, USER_ID)
35
+ * 5. Defaults (relay URL)
36
+ *
37
+ * Returns null fields if token/userId can't be resolved.
38
+ */
39
+ export declare function resolveConfig(args: CliArgs): {
40
+ token: string | null;
41
+ userId: string | null;
42
+ relayUrl: string;
43
+ };
@@ -0,0 +1,176 @@
1
+ // Config reader/writer for OpenClaw skill configuration.
2
+ // Reads/writes token to ~/.openclaw/openclaw.json under skills.entries.reefclaw.
3
+ // Provides unified config resolution: CLI args > env vars > OpenClaw config > fallback env > defaults.
4
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { homedir } from 'os';
7
+ import JSON5 from 'json5';
8
+ import { logger } from './logger.js';
9
+ const TAG = 'config';
10
+ const OPENCLAW_DIR = join(homedir(), '.openclaw');
11
+ const CONFIG_PATH = join(OPENCLAW_DIR, 'openclaw.json');
12
+ const DEFAULT_RELAY_URL = 'wss://reefclaw.radunlupsa.partykit.dev';
13
+ // ---- Read/write OpenClaw config ----
14
+ /** Read the OpenClaw config file and extract ReefClaw skill settings.
15
+ * Prefers the schema-valid nested shape (entry.config.*), falls back to the
16
+ * legacy flat shape per-field. */
17
+ export function readOpenClawConfig() {
18
+ try {
19
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
20
+ const config = JSON5.parse(raw);
21
+ const reefclaw = config?.skills?.entries?.reefclaw;
22
+ if (reefclaw) {
23
+ logger.debug(TAG, `Read config from ${CONFIG_PATH}`);
24
+ return {
25
+ token: reefclaw.config?.token ?? reefclaw.token,
26
+ userId: reefclaw.config?.userId ?? reefclaw.userId,
27
+ relayUrl: reefclaw.config?.relayUrl ?? reefclaw.relayUrl,
28
+ };
29
+ }
30
+ return null;
31
+ }
32
+ catch {
33
+ // File doesn't exist or isn't valid JSON
34
+ return null;
35
+ }
36
+ }
37
+ /** Write/merge token (and optional fields) into OpenClaw config */
38
+ export function writeOpenClawConfig(token, userId, relayUrl) {
39
+ let config = {};
40
+ // Read existing config to merge
41
+ try {
42
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
43
+ config = JSON5.parse(raw);
44
+ }
45
+ catch {
46
+ // Start fresh
47
+ }
48
+ // Ensure nested structure
49
+ if (!config.skills)
50
+ config.skills = {};
51
+ if (!config.skills.entries)
52
+ config.skills.entries = {};
53
+ if (!config.skills.entries.reefclaw)
54
+ config.skills.entries.reefclaw = {};
55
+ const entry = config.skills.entries.reefclaw;
56
+ // Write the schema-valid nested shape and migrate any legacy flat keys away
57
+ // — flat keys fail OpenClaw config validation (2026.6+) and crash the
58
+ // gateway on its next restart.
59
+ if (!entry.config)
60
+ entry.config = {};
61
+ entry.config.token = token;
62
+ if (userId)
63
+ entry.config.userId = userId;
64
+ if (relayUrl)
65
+ entry.config.relayUrl = relayUrl;
66
+ delete entry.token;
67
+ delete entry.userId;
68
+ delete entry.relayUrl;
69
+ // Ensure gateway.controlUi settings for OpenClaw v2026.4.2+ scope compatibility.
70
+ // Without these, the skill's WS connection gets scopes cleared (missing scope: operator.write).
71
+ ensureGatewayScopeConfig(config);
72
+ // Write — file holds the connection token, so restrict to owner.
73
+ mkdirSync(OPENCLAW_DIR, { recursive: true, mode: 0o700 });
74
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
75
+ // writeFileSync's `mode` only applies when it CREATES the file; on an
76
+ // existing openclaw.json (the common case) the old — possibly world-readable
77
+ // — permissions survive. chmod every time so a token-bearing config is never
78
+ // left group/world-readable. Best-effort (no-op / unsupported on some FS).
79
+ restrictConfigPermissions();
80
+ logger.info(TAG, `Saved config to ${CONFIG_PATH}`);
81
+ }
82
+ /** Force owner-only (0600) perms on the config file. `writeFileSync`'s mode
83
+ * option is ignored when the file already exists, so this must run after
84
+ * every write to keep the token-bearing config from lingering world-readable.
85
+ * Best-effort — never throws (chmod is a no-op on some Windows filesystems). */
86
+ function restrictConfigPermissions() {
87
+ try {
88
+ chmodSync(CONFIG_PATH, 0o600);
89
+ }
90
+ catch (err) {
91
+ logger.debug(TAG, `Could not chmod ${CONFIG_PATH} to 0600: ${err instanceof Error ? err.message : String(err)}`);
92
+ }
93
+ }
94
+ // ---- Gateway scope compatibility (OpenClaw v2026.4.2+) ----
95
+ /**
96
+ * Ensure gateway.controlUi has the required settings for WS scope preservation.
97
+ * Without these, OpenClaw v2026.4.2+ clears operator.write scopes on connections
98
+ * without device identity, breaking mission presentation and chat RPCs.
99
+ */
100
+ function ensureGatewayScopeConfig(config) {
101
+ if (!config.gateway)
102
+ config.gateway = {};
103
+ const gw = config.gateway;
104
+ if (!gw.controlUi)
105
+ gw.controlUi = {};
106
+ const cui = gw.controlUi;
107
+ let changed = false;
108
+ if (cui.dangerouslyDisableDeviceAuth !== true) {
109
+ cui.dangerouslyDisableDeviceAuth = true;
110
+ changed = true;
111
+ }
112
+ if (cui.allowInsecureAuth !== true) {
113
+ cui.allowInsecureAuth = true;
114
+ changed = true;
115
+ }
116
+ // Write-once: only flip (and disclose) when a flag was absent/false, so the
117
+ // security-relevant downgrade is announced the one time it actually happens,
118
+ // never on every boot. These flags relax the local gateway's device-identity
119
+ // auth (required for OpenClaw v2026.4.2+ WS scope preservation) — the
120
+ // operator should know they were set.
121
+ if (changed) {
122
+ logger.warn(TAG, 'Enabled gateway.controlUi.{dangerouslyDisableDeviceAuth,allowInsecureAuth} — this relaxes local gateway device-identity auth (needed for OpenClaw v2026.4.2+ WS scope preservation). Keep the gateway bound to localhost.');
123
+ }
124
+ return changed;
125
+ }
126
+ /**
127
+ * Check and fix gateway scope config on startup. Runs once — reads openclaw.json,
128
+ * applies missing settings if needed, writes back. No-op if already correct.
129
+ */
130
+ export function ensureGatewayScopeConfigOnStartup() {
131
+ try {
132
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
133
+ const config = JSON5.parse(raw);
134
+ if (ensureGatewayScopeConfig(config)) {
135
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
136
+ restrictConfigPermissions(); // mode above is ignored on an existing file
137
+ logger.info(TAG, 'Applied gateway scope config for OpenClaw v2026.4.2+ compatibility');
138
+ }
139
+ }
140
+ catch {
141
+ // Config doesn't exist or can't be parsed — skip (setup hasn't run yet)
142
+ }
143
+ }
144
+ /**
145
+ * Resolve config with priority:
146
+ * 1. CLI args (--token, --user-id, --relay-url)
147
+ * 2. Environment variables (REEFCLAW_TOKEN, REEFCLAW_USER_ID, REEFCLAW_RELAY_URL)
148
+ * 3. OpenClaw config file (~/.openclaw/openclaw.json)
149
+ * 4. Fallback env vars (MOCK_TOKEN, USER_ID)
150
+ * 5. Defaults (relay URL)
151
+ *
152
+ * Returns null fields if token/userId can't be resolved.
153
+ */
154
+ export function resolveConfig(args) {
155
+ // Read OpenClaw config
156
+ const savedConfig = readOpenClawConfig();
157
+ const token = args.token
158
+ || process.env.REEFCLAW_TOKEN
159
+ || savedConfig?.token
160
+ || process.env.MOCK_TOKEN
161
+ || null;
162
+ const userId = args.userId
163
+ || process.env.REEFCLAW_USER_ID
164
+ || savedConfig?.userId
165
+ || process.env.USER_ID
166
+ || null;
167
+ const relayUrl = args.relayUrl
168
+ || process.env.REEFCLAW_RELAY_URL
169
+ || savedConfig?.relayUrl
170
+ || process.env.RELAY_URL
171
+ || DEFAULT_RELAY_URL;
172
+ if (savedConfig?.token && !args.token && !process.env.REEFCLAW_TOKEN) {
173
+ logger.info(TAG, 'Using token from OpenClaw config');
174
+ }
175
+ return { token, userId, relayUrl };
176
+ }
@@ -0,0 +1,52 @@
1
+ import type { ConnectorConfig, RequestFrame, Frame } from './types.js';
2
+ export type ConnectorState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'failed';
3
+ export interface ConnectorCallbacks {
4
+ /** Called when a request frame arrives from the relay (browser command) */
5
+ onRequest: (frame: RequestFrame) => void;
6
+ /** Called when connection state changes */
7
+ onStateChange: (state: ConnectorState) => void;
8
+ /** Called when the relay sends a connection ACK with metadata */
9
+ onConnectAck?: (payload: Record<string, unknown>) => void;
10
+ }
11
+ export declare class Connector {
12
+ private readonly config;
13
+ private readonly callbacks;
14
+ private ws;
15
+ private state;
16
+ private attempt;
17
+ private reconnectTimer;
18
+ private pendingRequests;
19
+ private readonly reconnectConfig;
20
+ private readonly heartbeatConfig;
21
+ private heartbeatTimer;
22
+ private lastMessageAt;
23
+ private destroyed;
24
+ constructor(config: ConnectorConfig, callbacks: ConnectorCallbacks);
25
+ /** Build the relay WebSocket URL (token is sent via Authorization header, not URL) */
26
+ private buildUrl;
27
+ /** Start connecting to the relay */
28
+ connect(): void;
29
+ /** Send an event frame to the relay (provider → relay → browser) */
30
+ sendEvent(frame: Frame): void;
31
+ /** Send a response frame to the relay (answering a browser request) */
32
+ sendResponse(id: string, ok: boolean, payload?: unknown, error?: {
33
+ code: number;
34
+ message: string;
35
+ }): void;
36
+ /** Send a request and wait for a response (e.g. reconcile request originated by skill) */
37
+ sendRequest(method: string, params?: Record<string, unknown>): Promise<unknown>;
38
+ /** Check if currently connected */
39
+ isConnected(): boolean;
40
+ /** Get current state */
41
+ getState(): ConnectorState;
42
+ /** Gracefully disconnect and clean up */
43
+ destroy(): void;
44
+ private handleMessage;
45
+ private scheduleReconnect;
46
+ private setState;
47
+ private rejectAllPending;
48
+ private startHeartbeat;
49
+ private stopHeartbeat;
50
+ private clearReconnectTimer;
51
+ private forceReconnect;
52
+ }
@@ -0,0 +1,293 @@
1
+ // WebSocket client that connects to the ReefClaw relay server.
2
+ // Handles reconnection with exponential backoff, command whitelist,
3
+ // and pending request tracking.
4
+ import WebSocket from 'ws';
5
+ import { logger } from './logger.js';
6
+ import { computeDelay } from './utils/reconnect.js';
7
+ import { ALLOWED_METHODS as METHODS_LIST, FATAL_CLOSE_CODES, } from './types.js';
8
+ const TAG = 'connector';
9
+ const DEFAULT_HEARTBEAT = {
10
+ intervalMs: 30_000, // 30s — less aggressive than browser (15s) since skill runs on a stable server
11
+ timeoutMs: 10_000, // 10s — generous for server-to-edge latency
12
+ };
13
+ const DEFAULT_RECONNECT = {
14
+ baseDelayMs: 1000,
15
+ maxDelayMs: 30_000,
16
+ maxFastAttempts: 10,
17
+ jitterFactor: 0.3,
18
+ slowRetryMs: 60_000,
19
+ };
20
+ const REQUEST_TIMEOUT_MS = 15_000;
21
+ export class Connector {
22
+ config;
23
+ callbacks;
24
+ ws = null;
25
+ state = 'disconnected';
26
+ attempt = 0;
27
+ reconnectTimer = null;
28
+ pendingRequests = new Map();
29
+ reconnectConfig;
30
+ heartbeatConfig;
31
+ heartbeatTimer = null;
32
+ lastMessageAt = 0;
33
+ destroyed = false;
34
+ constructor(config, callbacks) {
35
+ this.config = config;
36
+ this.callbacks = callbacks;
37
+ this.reconnectConfig = { ...DEFAULT_RECONNECT, ...config.reconnect };
38
+ this.heartbeatConfig = { ...DEFAULT_HEARTBEAT };
39
+ }
40
+ /** Build the relay WebSocket URL (token is sent via Authorization header, not URL) */
41
+ buildUrl() {
42
+ const base = this.config.relayUrl.replace(/\/$/, '');
43
+ return `${base}/parties/reefclaw/${this.config.userId}`;
44
+ }
45
+ /** Start connecting to the relay */
46
+ connect() {
47
+ if (this.destroyed)
48
+ return;
49
+ // Guard against concurrent connect calls (e.g. forceReconnect + pending timer)
50
+ if (this.state === 'connecting' || this.state === 'connected')
51
+ return;
52
+ this.clearReconnectTimer();
53
+ this.setState(this.attempt === 0 ? 'connecting' : 'reconnecting');
54
+ const url = this.buildUrl();
55
+ logger.info(TAG, `Connecting to relay (attempt ${this.attempt + 1})`, { url });
56
+ // Token in Authorization header — keeps long-lived rc_* secret out of edge/proxy/access logs
57
+ // that capture full request URIs. Browsers can't set headers on WebSocket, so the browser
58
+ // path stays on ?jwt= (short-lived Clerk JWT, auto-rotated).
59
+ const ws = new WebSocket(url, {
60
+ headers: {
61
+ Authorization: `Bearer ${this.config.token}`,
62
+ },
63
+ });
64
+ this.ws = ws;
65
+ ws.on('open', () => {
66
+ logger.info(TAG, 'WebSocket open, awaiting auth ack');
67
+ });
68
+ ws.on('message', (data) => {
69
+ this.handleMessage(data);
70
+ });
71
+ ws.on('pong', () => {
72
+ this.lastMessageAt = Date.now();
73
+ });
74
+ ws.on('close', (code, reason) => {
75
+ logger.info(TAG, `WebSocket closed: ${code} ${reason.toString()}`);
76
+ this.ws = null;
77
+ this.stopHeartbeat();
78
+ this.rejectAllPending('Connection closed');
79
+ if (this.destroyed)
80
+ return;
81
+ // Fatal codes: do not reconnect
82
+ if (FATAL_CLOSE_CODES.includes(code)) {
83
+ logger.error(TAG, `Fatal close code ${code} — not reconnecting`);
84
+ this.setState('failed');
85
+ return;
86
+ }
87
+ // 4002 = stale skill connection on relay. Wait for it to time out, then retry.
88
+ if (code === 4002) {
89
+ logger.warn(TAG, 'Stale skill connection on relay — waiting 10s before retry');
90
+ this.setState('reconnecting');
91
+ this.reconnectTimer = setTimeout(() => {
92
+ this.reconnectTimer = null;
93
+ this.connect();
94
+ }, 10_000);
95
+ return;
96
+ }
97
+ this.scheduleReconnect();
98
+ });
99
+ ws.on('error', (err) => {
100
+ logger.error(TAG, `WebSocket error: ${err.message}`);
101
+ });
102
+ }
103
+ /** Send an event frame to the relay (provider → relay → browser) */
104
+ sendEvent(frame) {
105
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
106
+ return;
107
+ this.ws.send(JSON.stringify(frame));
108
+ }
109
+ /** Send a response frame to the relay (answering a browser request) */
110
+ sendResponse(id, ok, payload, error) {
111
+ const frame = { type: 'res', id, ok, ...(payload !== undefined && { payload }), ...(error && { error }) };
112
+ this.sendEvent(frame);
113
+ }
114
+ /** Send a request and wait for a response (e.g. reconcile request originated by skill) */
115
+ sendRequest(method, params) {
116
+ return new Promise((resolve, reject) => {
117
+ const id = `skill-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
118
+ const frame = { type: 'req', id, method, ...(params && { params }) };
119
+ const timer = setTimeout(() => {
120
+ this.pendingRequests.delete(id);
121
+ reject(new Error(`Request ${method} timed out`));
122
+ }, REQUEST_TIMEOUT_MS);
123
+ this.pendingRequests.set(id, { resolve, reject, timer });
124
+ this.sendEvent(frame);
125
+ });
126
+ }
127
+ /** Check if currently connected */
128
+ isConnected() {
129
+ return this.state === 'connected';
130
+ }
131
+ /** Get current state */
132
+ getState() {
133
+ return this.state;
134
+ }
135
+ /** Gracefully disconnect and clean up */
136
+ destroy() {
137
+ this.destroyed = true;
138
+ this.stopHeartbeat();
139
+ this.clearReconnectTimer();
140
+ this.rejectAllPending('Connector destroyed');
141
+ if (this.ws) {
142
+ this.ws.close(1000, 'Skill shutting down');
143
+ this.ws = null;
144
+ }
145
+ this.setState('disconnected');
146
+ }
147
+ // ---- Private ----
148
+ handleMessage(data) {
149
+ // Any message from relay proves the connection is alive
150
+ this.lastMessageAt = Date.now();
151
+ let frame;
152
+ try {
153
+ frame = JSON.parse(data.toString());
154
+ }
155
+ catch {
156
+ logger.warn(TAG, 'Received unparseable message');
157
+ return;
158
+ }
159
+ if (frame.type === 'res') {
160
+ // Connection ack
161
+ if (frame.id === '_connect' && frame.ok) {
162
+ logger.info(TAG, 'Authenticated with relay', frame.payload);
163
+ this.attempt = 0;
164
+ this.setState('connected');
165
+ this.startHeartbeat();
166
+ if (this.callbacks.onConnectAck && frame.payload && typeof frame.payload === 'object') {
167
+ this.callbacks.onConnectAck(frame.payload);
168
+ }
169
+ return;
170
+ }
171
+ // Rate limit / error from relay
172
+ if (frame.id.startsWith('_')) {
173
+ if (!frame.ok) {
174
+ const errMsg = frame.error?.message ?? 'unknown';
175
+ if (frame.error?.code === 429) {
176
+ logger.warn(TAG, `Rate limited by relay — non-chat events being dropped. Consider reducing market data frequency.`);
177
+ }
178
+ else {
179
+ logger.warn(TAG, `Relay error: ${errMsg}`);
180
+ }
181
+ }
182
+ return;
183
+ }
184
+ // Response to a pending request
185
+ const pending = this.pendingRequests.get(frame.id);
186
+ if (pending) {
187
+ clearTimeout(pending.timer);
188
+ this.pendingRequests.delete(frame.id);
189
+ if (frame.ok) {
190
+ pending.resolve(frame.payload);
191
+ }
192
+ else {
193
+ pending.reject(new Error(frame.error?.message ?? 'Request failed'));
194
+ }
195
+ }
196
+ return;
197
+ }
198
+ if (frame.type === 'req') {
199
+ // Validate against command whitelist
200
+ if (!METHODS_LIST.includes(frame.method)) {
201
+ logger.warn(TAG, `Unknown method: ${frame.method}`);
202
+ this.sendResponse(frame.id, false, undefined, {
203
+ code: 400,
204
+ message: `Unknown method: ${frame.method}`,
205
+ });
206
+ return;
207
+ }
208
+ this.callbacks.onRequest(frame);
209
+ return;
210
+ }
211
+ // Event frames from relay — unexpected for a skill connection, but log
212
+ if (frame.type === 'event') {
213
+ logger.debug(TAG, `Received event (unexpected for skill): ${frame.event}`);
214
+ }
215
+ }
216
+ scheduleReconnect() {
217
+ this.attempt++;
218
+ let delay;
219
+ if (this.attempt <= this.reconnectConfig.maxFastAttempts) {
220
+ // Fast phase: exponential backoff
221
+ delay = computeDelay(this.attempt - 1, this.reconnectConfig);
222
+ logger.info(TAG, `Reconnecting in ${Math.round(delay)}ms (attempt ${this.attempt}/${this.reconnectConfig.maxFastAttempts})`);
223
+ }
224
+ else {
225
+ // Slow phase: fixed interval, never give up
226
+ delay = this.reconnectConfig.slowRetryMs;
227
+ logger.info(TAG, `Slow retry in ${Math.round(delay)}ms (attempt ${this.attempt})`);
228
+ }
229
+ this.setState('reconnecting');
230
+ this.reconnectTimer = setTimeout(() => {
231
+ this.reconnectTimer = null;
232
+ this.connect();
233
+ }, delay);
234
+ }
235
+ setState(newState) {
236
+ if (this.state === newState)
237
+ return;
238
+ const prev = this.state;
239
+ this.state = newState;
240
+ logger.debug(TAG, `State: ${prev} → ${newState}`);
241
+ this.callbacks.onStateChange(newState);
242
+ }
243
+ rejectAllPending(reason) {
244
+ for (const [id, pending] of this.pendingRequests) {
245
+ clearTimeout(pending.timer);
246
+ pending.reject(new Error(reason));
247
+ }
248
+ this.pendingRequests.clear();
249
+ }
250
+ // ---- Heartbeat ----
251
+ startHeartbeat() {
252
+ this.stopHeartbeat();
253
+ this.lastMessageAt = Date.now();
254
+ this.heartbeatTimer = setInterval(() => {
255
+ const silence = Date.now() - this.lastMessageAt;
256
+ if (silence > this.heartbeatConfig.intervalMs + this.heartbeatConfig.timeoutMs) {
257
+ // No message received for interval + timeout — connection is dead
258
+ logger.error(TAG, `Heartbeat timeout: no relay message for ${Math.round(silence / 1000)}s — forcing reconnect`);
259
+ this.stopHeartbeat();
260
+ this.forceReconnect();
261
+ return;
262
+ }
263
+ // Send a native WebSocket ping (protocol-level, handled by PartyKit automatically).
264
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
265
+ this.ws.ping();
266
+ }
267
+ }, this.heartbeatConfig.intervalMs);
268
+ logger.debug(TAG, `Heartbeat started: interval=${this.heartbeatConfig.intervalMs}ms timeout=${this.heartbeatConfig.timeoutMs}ms`);
269
+ }
270
+ stopHeartbeat() {
271
+ if (this.heartbeatTimer) {
272
+ clearInterval(this.heartbeatTimer);
273
+ this.heartbeatTimer = null;
274
+ }
275
+ }
276
+ clearReconnectTimer() {
277
+ if (this.reconnectTimer) {
278
+ clearTimeout(this.reconnectTimer);
279
+ this.reconnectTimer = null;
280
+ }
281
+ }
282
+ forceReconnect() {
283
+ if (this.ws) {
284
+ this.ws.removeAllListeners();
285
+ this.ws.close(1000, 'Heartbeat timeout');
286
+ this.ws = null;
287
+ }
288
+ this.rejectAllPending('Heartbeat timeout');
289
+ this.setState('disconnected');
290
+ this.clearReconnectTimer();
291
+ this.scheduleReconnect();
292
+ }
293
+ }
@@ -0,0 +1,43 @@
1
+ import type { Channel } from './types.js';
2
+ export interface BufferedEvent {
3
+ channel: Channel;
4
+ event: string;
5
+ payload: unknown;
6
+ seq: number;
7
+ /** Wall-clock millis when the frame was recorded. Used for age eviction
8
+ * and for cross-channel ordering when replayed in gapFill. */
9
+ timestamp: number;
10
+ }
11
+ export interface EventReplayBufferOptions {
12
+ /** Per-channel max events. Older entries dropped when capacity hit. */
13
+ perChannelCapacity?: number;
14
+ /** Per-channel max age (ms). Older entries dropped on append AND on `since`. */
15
+ maxAgeMs?: number;
16
+ /** Test-only injected clock. */
17
+ now?: () => number;
18
+ }
19
+ export declare class EventReplayBuffer {
20
+ private readonly perChannelCapacity;
21
+ private readonly maxAgeMs;
22
+ private readonly now;
23
+ private readonly buffers;
24
+ constructor(opts?: EventReplayBufferOptions);
25
+ /** Append a frame that was just sent to the relay. The seq must be the
26
+ * same value placed on the wire so the browser's seq tracker matches. */
27
+ record(channel: Channel, event: string, payload: unknown, seq: number): void;
28
+ /** Return all buffered events on `channel` with seq > lastSeq, in seq order.
29
+ * Lazy age-eviction keeps the buffer trim even when nothing has been
30
+ * appended for a while. */
31
+ since(channel: Channel, lastSeq: number): BufferedEvent[];
32
+ /** Return all buffered events newer than the per-channel `lastSeq`, sorted
33
+ * by timestamp so the browser replays them in roughly the order they
34
+ * were originally emitted. Within a channel, seq is monotonic so the
35
+ * timestamp sort still preserves per-channel ordering — frames emitted
36
+ * in the same millisecond fall back to (channel, seq) for determinism. */
37
+ sinceMany(lastSequences: Partial<Record<Channel, number>>): BufferedEvent[];
38
+ /** Drop the buffer (e.g. on bridge shutdown). */
39
+ clear(): void;
40
+ /** Diagnostic — total events held across all channels. */
41
+ size(): number;
42
+ private evictExpired;
43
+ }