@reefclaw/connect 0.1.39 → 0.1.41

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 (32) hide show
  1. package/README.md +23 -0
  2. package/assets/bridge/config.d.ts +7 -0
  3. package/assets/bridge/config.js +13 -0
  4. package/assets/bridge/gateway/agent-scope.d.ts +30 -0
  5. package/assets/bridge/gateway/agent-scope.js +67 -0
  6. package/assets/bridge/gateway/gateway-config.d.ts +18 -3
  7. package/assets/bridge/gateway/gateway-config.js +42 -4
  8. package/assets/bridge/gateway/gateway-ws-client.d.ts +24 -0
  9. package/assets/bridge/gateway/gateway-ws-client.js +87 -4
  10. package/assets/bridge/index.js +35 -30
  11. package/assets/bridge/providers/connector-update.d.ts +43 -0
  12. package/assets/bridge/providers/connector-update.js +149 -0
  13. package/assets/bridge/providers/gateway.d.ts +10 -5
  14. package/assets/bridge/providers/gateway.js +96 -79
  15. package/assets/plugin/exchange-adapter.d.ts +7 -2
  16. package/assets/plugin/index.js +12 -0
  17. package/assets/plugin/ingest/readiness-reporter.d.ts +8 -1
  18. package/assets/plugin/ingest/readiness-reporter.js +7 -3
  19. package/assets/plugin/live/stop-watcher.js +7 -1
  20. package/assets/plugin/openclaw.plugin.json +1 -1
  21. package/assets/plugin/paper-adapter.d.ts +1 -1
  22. package/assets/plugin/paper-adapter.js +2 -2
  23. package/assets/plugin/plugin-version.d.ts +21 -0
  24. package/assets/plugin/plugin-version.js +58 -0
  25. package/assets/plugin/simulator/exchange-simulator.js +5 -1
  26. package/assets/plugin/simulator/realistic-fills.d.ts +16 -0
  27. package/assets/plugin/simulator/realistic-fills.js +26 -2
  28. package/assets/plugin/simulator/types.d.ts +4 -0
  29. package/assets/shared/readiness.d.ts +5 -0
  30. package/dist/agents.js +92 -0
  31. package/dist/cli.js +16 -0
  32. package/package.json +5 -2
@@ -0,0 +1,58 @@
1
+ // Which ReefClaw plugin RELEASE is running, and how it was installed — the two
2
+ // facts the dashboard needs to (a) tell the trader an update exists and (b)
3
+ // offer the right update path (one click for npx installs, ClawHub steps for
4
+ // ClawHub installs, silence for the operator's own source/dist deploys).
5
+ //
6
+ // Version source: the `openclaw.plugin.json` that ships NEXT TO index.js in
7
+ // every installed layout. Both release channels stamp it with the package
8
+ // release version — installer/scripts/bundle-assets.mjs for `npx
9
+ // @reefclaw/connect`, plugin-package/scripts/assemble.mjs for ClawHub. The
10
+ // repo's own manifest carries the unstamped 0.1.0, which is what tells a
11
+ // source-tree / script-deployed box apart from a packaged install.
12
+ //
13
+ // History: the readiness report used to send a hardcoded internal constant
14
+ // ('3.8.0') that lived in a different namespace from the release versions
15
+ // ('0.1.x'), so the dashboard's update banner compared apples to oranges and
16
+ // never fired (memory feedback_plugin_update_banner_inert_version_namespace).
17
+ // The webapp still recognises that legacy value and nudges those boxes once.
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { join, resolve, sep } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+ /** The repo manifest's placeholder version. A box reporting it was not
22
+ * installed from a release package (no release will ever be 0.1.0 — the
23
+ * release line passed it long ago). */
24
+ export const UNSTAMPED_VERSION = '0.1.0';
25
+ const RELEASE_VERSION_RE = /^\d+\.\d+\.\d+$/;
26
+ export function resolvePluginInstallFacts(input) {
27
+ let version;
28
+ try {
29
+ const raw = readFileSync(join(input.pluginRoot, 'openclaw.plugin.json'), 'utf-8');
30
+ const v = JSON.parse(raw).version;
31
+ if (typeof v === 'string' && RELEASE_VERSION_RE.test(v.trim()))
32
+ version = v.trim();
33
+ }
34
+ catch {
35
+ // No (readable) manifest next to index.js — legacy deploy layout. Report
36
+ // nothing rather than a guess; the dashboard fails open (no banner).
37
+ }
38
+ return { version, channel: detectChannel(input, version) };
39
+ }
40
+ function detectChannel(input, version) {
41
+ // Unstamped or absent manifest → the repo's own: source tree or a dist deploy
42
+ // (the operator's rigs, updated by deploy scripts — never by the installer).
43
+ if (version === undefined || version === UNSTAMPED_VERSION)
44
+ return 'source';
45
+ // ClawHub packages carry the bootstrap skill at the package root
46
+ // (plugin-package/scripts/assemble.mjs); the npx layout deliberately does
47
+ // not. Checked FIRST: a box that once ran the npx installer and later
48
+ // installed from ClawHub loads the ClawHub copy.
49
+ if (existsSync(join(input.pluginRoot, 'skills')))
50
+ return 'clawhub';
51
+ const home = resolve(input.reefclawHome ?? join(homedir(), '.reefclaw'));
52
+ const root = resolve(input.pluginRoot);
53
+ if (input.connectorSupervisor === 'on' || root.startsWith(home + sep))
54
+ return 'npx';
55
+ // Stamped release package without an installer marker (placed by hand).
56
+ // The installer still updates it in place, so it gets the one-click path.
57
+ return 'npx';
58
+ }
@@ -9,6 +9,7 @@ import { randomUUID } from 'node:crypto';
9
9
  import { logger, formatError } from '../logger.js';
10
10
  import { MAX_TRADE_HISTORY, DEFAULT_SIMULATION_CONFIG } from './types.js';
11
11
  import { fillMarketOrder, fillLimitOrder, parseSymbol } from './fill-engine.js';
12
+ import { MAX_BOOK_DRIFT_BPS } from './realistic-fills.js';
12
13
  import { updateMfe } from '../mfe.js';
13
14
  import { computeInvalidationHit } from '../pinned-plan.js';
14
15
  const TAG = 'simulator';
@@ -805,7 +806,10 @@ export class ExchangeSimulator extends EventEmitter {
805
806
  logger.info(TAG, `Market order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}` +
806
807
  ` (decision: ${eq.decisionPrice.toFixed(2)}, slippage: ${eq.slippageBps.toFixed(2)}bps` +
807
808
  `, latency: ${eq.latencyMs.toFixed(0)}ms, fee: ${eq.feeRate * 100}%` +
808
- `, book: ${eq.bookDepthAvailable ? `${eq.bookLevelsConsumed} levels` : 'unavailable'})`);
809
+ `, book: ${eq.bookDepthAvailable ? `${eq.bookLevelsConsumed} levels` : 'unavailable'}` +
810
+ `${eq.bookDriftBps !== undefined && Math.abs(eq.bookDriftBps) > MAX_BOOK_DRIFT_BPS
811
+ ? `, stale book ${eq.bookDriftBps.toFixed(0)}bps off the decision price: fill anchored`
812
+ : ''})`);
809
813
  }
810
814
  else {
811
815
  logger.info(TAG, `Market order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}`);
@@ -1,4 +1,18 @@
1
1
  import type { OrderBookDepth, SimulationConfig, ExecutionQuality } from './types.js';
2
+ /**
3
+ * How far (bps) the cached book's mid may sit from the decision price before
4
+ * the fill is anchored to the decision price instead of the raw book VWAP.
5
+ *
6
+ * The book is refreshed only when a tool fetches it (tools/helpers.ts
7
+ * fetchOrderBook, typically at entry), so a stop or target hours later used to
8
+ * fill against an hours-old book: an ADA/USDT short stop with a 0.2123 breach
9
+ * mark filled at 0.207103, 245 bps in the trader's favour, booking a -1.04R
10
+ * loss as -0.02R (2026-09-16 audit). Inside the band the book is fresh enough
11
+ * to price the fill directly (unchanged behaviour); outside it the book only
12
+ * shapes the impact (VWAP vs mid), applied to the price the decision was
13
+ * actually made on.
14
+ */
15
+ export declare const MAX_BOOK_DRIFT_BPS = 10;
2
16
  /**
3
17
  * Walk the order book to compute a volume-weighted average fill price.
4
18
  *
@@ -45,6 +59,8 @@ export declare function getFeeRate(orderType: 'market' | 'limit', config: Simula
45
59
  *
46
60
  * Combines: orderbook VWAP + latency drift + appropriate fee rate.
47
61
  * Falls back to simple random slippage if no orderbook is available.
62
+ * A book whose mid has drifted more than MAX_BOOK_DRIFT_BPS from the decision
63
+ * price is stale: its impact is kept, the level is re-anchored.
48
64
  *
49
65
  * @returns fillPrice and ExecutionQuality metrics
50
66
  */
@@ -1,6 +1,20 @@
1
1
  // Phase 9a: Realistic fill simulation.
2
2
  // Order book-aware VWAP fills, latency modeling, maker/taker fees.
3
3
  import { DEFAULT_SIMULATION_CONFIG, priceToBps } from './types.js';
4
+ /**
5
+ * How far (bps) the cached book's mid may sit from the decision price before
6
+ * the fill is anchored to the decision price instead of the raw book VWAP.
7
+ *
8
+ * The book is refreshed only when a tool fetches it (tools/helpers.ts
9
+ * fetchOrderBook, typically at entry), so a stop or target hours later used to
10
+ * fill against an hours-old book: an ADA/USDT short stop with a 0.2123 breach
11
+ * mark filled at 0.207103, 245 bps in the trader's favour, booking a -1.04R
12
+ * loss as -0.02R (2026-09-16 audit). Inside the band the book is fresh enough
13
+ * to price the fill directly (unchanged behaviour); outside it the book only
14
+ * shapes the impact (VWAP vs mid), applied to the price the decision was
15
+ * actually made on.
16
+ */
17
+ export const MAX_BOOK_DRIFT_BPS = 10;
4
18
  /**
5
19
  * Walk the order book to compute a volume-weighted average fill price.
6
20
  *
@@ -92,6 +106,8 @@ export function getFeeRate(orderType, config) {
92
106
  *
93
107
  * Combines: orderbook VWAP + latency drift + appropriate fee rate.
94
108
  * Falls back to simple random slippage if no orderbook is available.
109
+ * A book whose mid has drifted more than MAX_BOOK_DRIFT_BPS from the decision
110
+ * price is stale: its impact is kept, the level is re-anchored.
95
111
  *
96
112
  * @returns fillPrice and ExecutionQuality metrics
97
113
  */
@@ -103,6 +119,7 @@ export function computeRealisticMarketFill(side, amount, decisionPrice, orderboo
103
119
  let latencyImpactBps;
104
120
  let bookLevelsConsumed;
105
121
  let bookDepthAvailable;
122
+ let bookDriftBps;
106
123
  if (orderbook && orderbook.asks.length > 0 && orderbook.bids.length > 0) {
107
124
  // Book-aware VWAP fill
108
125
  const { vwap, levelsConsumed } = computeBookAwareFillPrice(side, amount, orderbook);
@@ -113,8 +130,14 @@ export function computeRealisticMarketFill(side, amount, decisionPrice, orderboo
113
130
  marketImpactBps = priceToBps(vwap, midPrice);
114
131
  // For sells, impact is negative (received less), so take absolute for the metric
115
132
  // but keep signed for the actual price
116
- // Apply latency drift on top of VWAP
117
- const latencyResult = applyLatencyDrift(vwap, side, latencyMs, volFactor);
133
+ // A book that has drifted from the decision price is stale: keep its
134
+ // impact, re-anchor the level (see MAX_BOOK_DRIFT_BPS).
135
+ bookDriftBps = priceToBps(midPrice, decisionPrice);
136
+ const level = Math.abs(bookDriftBps) > MAX_BOOK_DRIFT_BPS
137
+ ? decisionPrice * (1 + marketImpactBps / 10_000)
138
+ : vwap;
139
+ // Apply latency drift on top of the (possibly re-anchored) VWAP
140
+ const latencyResult = applyLatencyDrift(level, side, latencyMs, volFactor);
118
141
  fillPrice = latencyResult.adjustedPrice;
119
142
  latencyImpactBps = latencyResult.latencyImpactBps;
120
143
  }
@@ -147,6 +170,7 @@ export function computeRealisticMarketFill(side, amount, decisionPrice, orderboo
147
170
  feePaid,
148
171
  bookLevelsConsumed,
149
172
  bookDepthAvailable,
173
+ ...(bookDriftBps !== undefined ? { bookDriftBps } : {}),
150
174
  };
151
175
  return { fillPrice, executionQuality };
152
176
  }
@@ -39,6 +39,10 @@ export interface ExecutionQuality {
39
39
  feePaid: number;
40
40
  bookLevelsConsumed: number;
41
41
  bookDepthAvailable: boolean;
42
+ /** Signed bps between the cached book's mid and the decision price at fill
43
+ * time. Beyond MAX_BOOK_DRIFT_BPS the fill was anchored to the decision
44
+ * price (stale book). Absent when no book was available. */
45
+ bookDriftBps?: number;
42
46
  /** Age of the quote the fill priced against (fill time − ticker.timestamp).
43
47
  * Surfaces feed staleness (issue #202); absent on records from before the
44
48
  * field existed or when the ticker carried no usable timestamp. */
@@ -48,7 +48,12 @@ export interface ReadinessReport {
48
48
  checks: ReadinessCheck[];
49
49
  /** Best-effort agent facts for display. */
50
50
  agent?: {
51
+ /** Release version from the shipped manifest ('0.1.27'). Legacy plugins
52
+ * sent an internal '3.x' constant — the webapp treats that as "behind". */
51
53
  pluginVersion?: string;
54
+ /** 'npx' | 'clawhub' | 'source' — how the plugin was installed. Kept
55
+ * `string` so an older webapp renders reports from a newer plugin. */
56
+ installChannel?: string;
52
57
  toolCount?: number;
53
58
  /** Trading venue this agent executes on ('binance' | 'hyperliquid').
54
59
  * Kept `string` so an older webapp renders reports from a newer plugin.
package/dist/agents.js ADDED
@@ -0,0 +1,92 @@
1
+ // Which OpenClaw agent ReefClaw attaches to, and a loud notice when the
2
+ // gateway hosts more than one.
3
+ //
4
+ // The installer targets the gateway's DEFAULT agent: `openclaw skills install`
5
+ // puts the trading skill in its workspace, the bridge's heartbeat cron runs on
6
+ // it, and the bridge addresses chat to it and shows only its runs
7
+ // (skill/src/gateway/agent-scope.ts applies the same rule). On a single-agent
8
+ // box that is simply `main`. On a gateway that already runs, say, a business
9
+ // agent as its default, a fresh "trading" agent added alongside gets NOTHING
10
+ // and the business agent starts trading — silently, until 2026-09-16. This
11
+ // module makes that choice visible at install time. A full "pick the agent"
12
+ // flow (skill placed with --agent, heartbeat cron with agentId, per-agent
13
+ // paths in the bridge) is a later feature.
14
+ import { readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { REEFCLAW_HOME } from './paths.js';
17
+ export const DEFAULT_AGENT_ID = 'main';
18
+ function normalizeAgentId(value) {
19
+ if (typeof value !== 'string')
20
+ return undefined;
21
+ const trimmed = value.trim().toLowerCase();
22
+ return trimmed.length > 0 ? trimmed : undefined;
23
+ }
24
+ function agentEntries(cfg) {
25
+ const list = cfg?.agents?.list;
26
+ if (!Array.isArray(list))
27
+ return [];
28
+ return list.filter((e) => !!e && typeof e === 'object' && !Array.isArray(e));
29
+ }
30
+ /** Every agent id the gateway lists; empty on a single-agent box. */
31
+ export function listAgentIds(cfg) {
32
+ const ids = [];
33
+ for (const entry of agentEntries(cfg)) {
34
+ const id = normalizeAgentId(entry.id);
35
+ if (id && !ids.includes(id))
36
+ ids.push(id);
37
+ }
38
+ return ids;
39
+ }
40
+ /** OpenClaw's own rule (agent-scope-config.ts `resolveDefaultAgentId`): the
41
+ * `agents.list` entry flagged `default: true`, else the first, else `main`. */
42
+ export function resolveDefaultAgentId(cfg) {
43
+ const entries = agentEntries(cfg);
44
+ if (entries.length === 0)
45
+ return DEFAULT_AGENT_ID;
46
+ const chosen = entries.find((e) => e.default === true) ?? entries[0];
47
+ return normalizeAgentId(chosen.id) ?? DEFAULT_AGENT_ID;
48
+ }
49
+ /** Best-effort read of ~/.reefclaw/plugin-config.json (the bridge reads the
50
+ * same file for its `agentId` override). Null when absent or unreadable. */
51
+ export function readPluginConfig(path = join(REEFCLAW_HOME, 'plugin-config.json')) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
54
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ export function describeAgentAttachment(cfg, pluginConfig) {
61
+ const defaultAgentId = resolveDefaultAgentId(cfg);
62
+ const override = normalizeAgentId(pluginConfig?.agentId);
63
+ const agentId = override ?? defaultAgentId;
64
+ const hosted = listAgentIds(cfg);
65
+ return {
66
+ agentId,
67
+ source: override ? 'override' : 'default',
68
+ defaultAgentId,
69
+ hosted,
70
+ shared: hosted.some((id) => id !== agentId),
71
+ };
72
+ }
73
+ /** The lines the installer prints. Pure, so the copy is unit-tested. */
74
+ export function agentAttachmentNotice(a) {
75
+ if (!a.shared) {
76
+ return { level: 'ok', lines: [`ReefClaw attaches to agent '${a.agentId}'`] };
77
+ }
78
+ const others = a.hosted.filter((id) => id !== a.agentId).join(', ');
79
+ const lines = [
80
+ `this gateway also hosts: ${others}. ReefClaw attaches to '${a.agentId}' `
81
+ + (a.source === 'override'
82
+ ? '(the agentId set in ~/.reefclaw/plugin-config.json).'
83
+ : '(the gateway default agent).'),
84
+ `The trading skill goes into that agent's workspace, the heartbeat runs on it, and the dashboard shows only its runs.`,
85
+ `To trade on a different agent, flag it "default": true in agents.list of openclaw.json and re-run this installer, `
86
+ + `or give the trading agent its own OpenClaw instance (cleanest).`,
87
+ ];
88
+ if (a.source === 'override' && a.agentId !== a.defaultAgentId) {
89
+ lines.push(`Note: the override only steers the bridge on this version; the skill and heartbeat still land on the default agent '${a.defaultAgentId}'.`);
90
+ }
91
+ return { level: 'warn', lines };
92
+ }
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { installPlugin } from './plugin.js';
14
14
  import { installBridge } from './bridge.js';
15
15
  import { installSkill } from './skill.js';
16
16
  import { enableConnectorSupervisor } from './supervisor-config.js';
17
+ import { describeAgentAttachment, readPluginConfig, agentAttachmentNotice } from './agents.js';
17
18
  import { tuneGatewayMemory } from './gateway-tuning.js';
18
19
  import { checkGateway, checkBinanceRegion, checkHyperliquidRegion } from './validate.js';
19
20
  import { readConfig, writeConfig, mergeReefClawConfig, gatewayAuthAlreadyRelaxed, openClawInstalled, openClawConfigPath, readGatewayPort, } from './openclaw.js';
@@ -149,6 +150,20 @@ function parseVenueArg(argv) {
149
150
  }
150
151
  return null;
151
152
  }
153
+ /** Say which agent ReefClaw is about to attach to. On a gateway that hosts
154
+ * several agents this is the difference between the trading agent trading
155
+ * and the business agent trading (see agents.ts). */
156
+ function noticeAgentAttachment(config) {
157
+ step('Choosing the agent ReefClaw attaches to');
158
+ const notice = agentAttachmentNotice(describeAgentAttachment(config, readPluginConfig()));
159
+ if (notice.level === 'ok') {
160
+ ok(notice.lines[0]);
161
+ return;
162
+ }
163
+ warn(notice.lines[0]);
164
+ for (const line of notice.lines.slice(1))
165
+ info(line);
166
+ }
152
167
  async function main() {
153
168
  banner(bold(cyan('ReefClaw connect')) + dim(' — link your OpenClaw agent to ReefClaw (paper trading)'));
154
169
  preflight();
@@ -165,6 +180,7 @@ async function main() {
165
180
  binanceReachable = await checkBinanceRegion();
166
181
  }
167
182
  const pre = readConfig();
183
+ noticeAgentAttachment(pre);
168
184
  const plugin = installPlugin();
169
185
  const merged = wireConfig(pre);
170
186
  // After wireConfig: the config is schema-valid (flat legacy keys migrated),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,5 +29,8 @@
29
29
  "vitest": "^4.0.18"
30
30
  },
31
31
  "license": "MIT",
32
- "homepage": "https://reefclaw.com"
32
+ "homepage": "https://reefclaw.com",
33
+ "bugs": {
34
+ "email": "support@reefclaw.com"
35
+ }
33
36
  }