@reefclaw/openclaw-plugin 0.1.2 → 0.1.3
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.
- package/bridge/bridge.js +46 -3
- package/bridge/utils/skill-version.d.ts +9 -0
- package/bridge/utils/skill-version.js +45 -1
- package/ccxt/binance-ban-gate.js +3 -1
- package/ccxt/binance-public.d.ts +15 -0
- package/ccxt/binance-public.js +34 -1
- package/index.js +14 -0
- package/package.json +1 -1
- package/skills/reefclaw/SKILL.md +6 -0
package/bridge/bridge.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { logger } from './logger.js';
|
|
8
8
|
import { Connector } from './connector.js';
|
|
9
|
-
import { readLocalSkillVersion, validateSkillContent, compareSemver } from './utils/skill-version.js';
|
|
9
|
+
import { readLocalSkillVersion, readAgentVisibleSkillVersions, validateSkillContent, compareSemver } from './utils/skill-version.js';
|
|
10
10
|
import { verifySkillSignature, signatureRequired, readLastAppliedSignedAtMs, recordAppliedSignedAt, } from './utils/skill-signing.js';
|
|
11
11
|
import { isTradingMode, validateModeTransition, redactTokens } from '@reefclaw/shared';
|
|
12
12
|
import { OPERATOR_WRITE_METHODS } from './types.js';
|
|
@@ -837,6 +837,37 @@ export class Bridge {
|
|
|
837
837
|
logger.warn(TAG, `Failed to update workspace skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
|
|
838
838
|
}
|
|
839
839
|
}
|
|
840
|
+
// 2d. Also write to the plugin-extension bundled skill directory — the
|
|
841
|
+
// chat-channel install layout (`/plugins install clawhub:...`) loads the
|
|
842
|
+
// agent-visible skill from ~/.openclaw/extensions/<plugin>/skills/, and a
|
|
843
|
+
// plugin (re)install resets that copy to the bundled bootstrap while the
|
|
844
|
+
// workspace copy above (our version tracker) survives. Without this
|
|
845
|
+
// write the agent stays on the bootstrap forever while the pull reports
|
|
846
|
+
// "already up to date" (observed live 2026-07-11).
|
|
847
|
+
const extensionsDir = join(homedir(), '.openclaw', 'extensions');
|
|
848
|
+
if (existsSync(extensionsDir)) {
|
|
849
|
+
try {
|
|
850
|
+
for (const ext of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
851
|
+
if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
|
|
852
|
+
continue;
|
|
853
|
+
const extSkillsDir = join(extensionsDir, ext.name, 'skills');
|
|
854
|
+
if (!existsSync(extSkillsDir))
|
|
855
|
+
continue;
|
|
856
|
+
for (const skillEntry of readdirSync(extSkillsDir, { withFileTypes: true })) {
|
|
857
|
+
if (!skillEntry.isDirectory() || !skillEntry.name.includes('reefclaw'))
|
|
858
|
+
continue;
|
|
859
|
+
const extSkillMd = join(extSkillsDir, skillEntry.name, 'SKILL.md');
|
|
860
|
+
if (existsSync(extSkillMd)) {
|
|
861
|
+
writeFileSync(extSkillMd, content, 'utf-8');
|
|
862
|
+
logger.info(TAG, `Updated extension skill SKILL.md: ${extSkillMd}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (scanErr) {
|
|
868
|
+
logger.warn(TAG, `Failed to update extension skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
840
871
|
// 3. Invalidate skillsSnapshot cache WITHOUT wiping sessions.json
|
|
841
872
|
// Previously we wrote '{}' to sessions.json which destroyed OpenClaw chat history.
|
|
842
873
|
// Now we surgically remove only the skillsSnapshot key from each session entry,
|
|
@@ -979,12 +1010,24 @@ export class Bridge {
|
|
|
979
1010
|
return;
|
|
980
1011
|
}
|
|
981
1012
|
this.currentSkillVersion = readLocalSkillVersion();
|
|
982
|
-
|
|
1013
|
+
// The workspace tracker alone is NOT proof the agent has the full skill:
|
|
1014
|
+
// a plugin (re)install resets the agent-visible extension copy to the
|
|
1015
|
+
// bundled bootstrap while the workspace copy survives (observed live
|
|
1016
|
+
// 2026-07-11 — agent stranded on 0.0.5 with the tracker at 2.20.4). If
|
|
1017
|
+
// ANY agent-visible copy is behind the webapp version, re-apply anyway;
|
|
1018
|
+
// applySkillUpdate is idempotent across all copies.
|
|
1019
|
+
const staleAgentCopies = readAgentVisibleSkillVersions().filter((v) => compareSemver(body.version, v) > 0);
|
|
1020
|
+
if (compareSemver(body.version, this.currentSkillVersion) <= 0 && staleAgentCopies.length === 0) {
|
|
983
1021
|
logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} already >= webapp v${body.version} — nothing to do`);
|
|
984
1022
|
this.skillPullInFlight = false;
|
|
985
1023
|
return;
|
|
986
1024
|
}
|
|
987
|
-
|
|
1025
|
+
if (staleAgentCopies.length > 0 && compareSemver(body.version, this.currentSkillVersion) <= 0) {
|
|
1026
|
+
logger.info(TAG, `SKILL.md webapp pull: agent-visible skill copy at v${staleAgentCopies[0]} behind webapp v${body.version} (plugin reinstall reset it) — re-applying`);
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} behind webapp v${body.version} — applying`);
|
|
1030
|
+
}
|
|
988
1031
|
const result = await this.applySkillUpdate({ version: body.version, content: body.content });
|
|
989
1032
|
this.emit('agent_state', 'skill_update_applied', {
|
|
990
1033
|
event: 'skill_update_applied',
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
export declare function parseSkillVersion(content: string): string | null;
|
|
3
3
|
/** Read current SKILL.md version from disk */
|
|
4
4
|
export declare function readLocalSkillVersion(): string | null;
|
|
5
|
+
/** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
|
|
6
|
+
* plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
|
|
7
|
+
* — what OpenClaw's skill loader actually reads on chat-channel installs.
|
|
8
|
+
* A plugin (re)install resets these to the bundled bootstrap while the
|
|
9
|
+
* workspace copy (the version tracker above) survives, so the tracker alone
|
|
10
|
+
* cannot be trusted for "already up to date" (observed live 2026-07-11: agent
|
|
11
|
+
* stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
|
|
12
|
+
* unreadable dirs/files are skipped; no extensions dir → empty list. */
|
|
13
|
+
export declare function readAgentVisibleSkillVersions(): string[];
|
|
5
14
|
/**
|
|
6
15
|
* Compare two semver strings.
|
|
7
16
|
* Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SKILL.md version parsing utility for OTA updates
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
2
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
const SKILL_MD_PATH = join(homedir(), '.openclaw', 'workspace', 'SKILL.md');
|
|
@@ -18,6 +18,50 @@ export function readLocalSkillVersion() {
|
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
|
|
22
|
+
* plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
|
|
23
|
+
* — what OpenClaw's skill loader actually reads on chat-channel installs.
|
|
24
|
+
* A plugin (re)install resets these to the bundled bootstrap while the
|
|
25
|
+
* workspace copy (the version tracker above) survives, so the tracker alone
|
|
26
|
+
* cannot be trusted for "already up to date" (observed live 2026-07-11: agent
|
|
27
|
+
* stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
|
|
28
|
+
* unreadable dirs/files are skipped; no extensions dir → empty list. */
|
|
29
|
+
export function readAgentVisibleSkillVersions() {
|
|
30
|
+
const versions = [];
|
|
31
|
+
const extensionsDir = join(homedir(), '.openclaw', 'extensions');
|
|
32
|
+
let extEntries;
|
|
33
|
+
try {
|
|
34
|
+
extEntries = readdirSync(extensionsDir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return versions;
|
|
38
|
+
}
|
|
39
|
+
for (const ext of extEntries) {
|
|
40
|
+
if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
|
|
41
|
+
continue;
|
|
42
|
+
const skillsDir = join(extensionsDir, ext.name, 'skills');
|
|
43
|
+
let skillEntries;
|
|
44
|
+
try {
|
|
45
|
+
skillEntries = readdirSync(skillsDir, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const s of skillEntries) {
|
|
51
|
+
if (!s.isDirectory() || !s.name.includes('reefclaw'))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const v = parseSkillVersion(readFileSync(join(skillsDir, s.name, 'SKILL.md'), 'utf-8'));
|
|
55
|
+
if (v)
|
|
56
|
+
versions.push(v);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// unreadable/absent copy — nothing to report for this entry
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return versions;
|
|
64
|
+
}
|
|
21
65
|
// 120KB — the full SKILL.md crossed 100KB at v2.20.0 (103.7KB), which silently
|
|
22
66
|
// broke every OTA apply against the old 100KB cap. Keep comfortably under the
|
|
23
67
|
// 128KiB Cloudflare Durable-Object per-value hard limit the relay stores into.
|
package/ccxt/binance-ban-gate.js
CHANGED
|
@@ -128,6 +128,8 @@ const CONTEXT_WEIGHT = {
|
|
|
128
128
|
fetchOHLCV: 2,
|
|
129
129
|
fetchFundingRate: 1,
|
|
130
130
|
fetchOpenInterest: 1,
|
|
131
|
+
// Readiness gate's host→Binance reachability probe (fapiPublicGetTime, wt 1).
|
|
132
|
+
reachabilityProbe: 1,
|
|
131
133
|
// ExchangeInfoCache boot load — ccxt loadMarkets() → GET /fapi/v1/exchangeInfo,
|
|
132
134
|
// IP weight 1 (doc-verified developers.binance.com 2026-07-10). Was the
|
|
133
135
|
// known "ungated_ip" ccxt-internal call named in the window-summary comment.
|
|
@@ -200,7 +202,7 @@ const NEVER_PACE = new Set([
|
|
|
200
202
|
const SHED_FIRST = new Set([
|
|
201
203
|
'fetchTodayIncomeBreakdown', 'fetchTransfers', 'fetchRecentTradedSymbols',
|
|
202
204
|
'fetchTicker', 'fetchTickerRaw', 'fetchOHLCV', 'fetchOrderBook',
|
|
203
|
-
'fetchFundingRate', 'fetchOpenInterest',
|
|
205
|
+
'fetchFundingRate', 'fetchOpenInterest', 'reachabilityProbe',
|
|
204
206
|
]);
|
|
205
207
|
let weightWindowStart = 0;
|
|
206
208
|
let weightUsed = 0;
|
package/ccxt/binance-public.d.ts
CHANGED
|
@@ -15,4 +15,19 @@ export declare class BinancePublicApi {
|
|
|
15
15
|
fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
|
|
16
16
|
/** Fetch OHLCV candles. Returns null on error. */
|
|
17
17
|
fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
|
|
18
|
+
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
19
|
+
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
20
|
+
* `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
|
|
21
|
+
* `api.binance.com`, since the public client doesn't set defaultType:'future').
|
|
22
|
+
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
23
|
+
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
24
|
+
* noteBinanceError). Outcomes:
|
|
25
|
+
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
26
|
+
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
27
|
+
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
28
|
+
* - 'unreachable' network / DNS / timeout / other error */
|
|
29
|
+
probeReachability(): Promise<{
|
|
30
|
+
outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
|
|
31
|
+
driftMs: number | null;
|
|
32
|
+
}>;
|
|
18
33
|
}
|
package/ccxt/binance-public.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// No API keys required — only uses public market data.
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
|
-
import { assertNotBanned, noteBinanceError, noteSuccess } from './binance-ban-gate.js';
|
|
5
|
+
import { assertNotBanned, noteBinanceError, noteSuccess, BinanceBannedError } from './binance-ban-gate.js';
|
|
6
6
|
const TAG = 'binance-public';
|
|
7
7
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -144,4 +144,37 @@ export class BinancePublicApi {
|
|
|
144
144
|
return null;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
148
|
+
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
149
|
+
* `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
|
|
150
|
+
* `api.binance.com`, since the public client doesn't set defaultType:'future').
|
|
151
|
+
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
152
|
+
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
153
|
+
* noteBinanceError). Outcomes:
|
|
154
|
+
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
155
|
+
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
156
|
+
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
157
|
+
* - 'unreachable' network / DNS / timeout / other error */
|
|
158
|
+
async probeReachability() {
|
|
159
|
+
try {
|
|
160
|
+
assertNotBanned('reachabilityProbe');
|
|
161
|
+
const r = await this.exchange.fapiPublicGetTime({});
|
|
162
|
+
noteSuccess();
|
|
163
|
+
const serverTime = Number(r?.serverTime);
|
|
164
|
+
const driftMs = Number.isFinite(serverTime) ? serverTime - Date.now() : null;
|
|
165
|
+
return { outcome: 'reachable', driftMs };
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
noteBinanceError(err);
|
|
169
|
+
if (err instanceof BinanceBannedError) {
|
|
170
|
+
// Gate paused us (418/429 backoff or weight pacer) — we didn't actually
|
|
171
|
+
// reach the host, so the result is UNKNOWN, not a reachability failure.
|
|
172
|
+
return { outcome: 'unknown', driftMs: null };
|
|
173
|
+
}
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
const geo = msg.includes('451');
|
|
176
|
+
logger.warn(TAG, `probeReachability failed${geo ? ' (HTTP 451 geo-block)' : ''}: ${msg}`);
|
|
177
|
+
return { outcome: geo ? 'geo_blocked' : 'unreachable', driftMs: null };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
147
180
|
}
|
package/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { PositionStateStore } from './live/position-state-store.js';
|
|
|
29
29
|
import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
|
|
30
30
|
import { onReconcilerObservedClose, reconcileStateStoreOnStartup } from './ingest/reconciler-cleanup.js';
|
|
31
31
|
import { reconcileDbOpenVsExchange } from './ingest/reconcile-db-vs-exchange.js';
|
|
32
|
+
import { startReadinessReporter } from './ingest/readiness-reporter.js';
|
|
32
33
|
import { IntelMicrostructureAssembler } from './live/microstructure-assembler.js';
|
|
33
34
|
import { recordPositionReviewsTool } from './tools/record-position-reviews.js';
|
|
34
35
|
import { getMyRecentReviewsTool } from './tools/get-my-recent-reviews.js';
|
|
@@ -2020,6 +2021,19 @@ const paperTradingPlugin = {
|
|
|
2020
2021
|
pluginToolNames = toolNames;
|
|
2021
2022
|
pluginInitialised = true;
|
|
2022
2023
|
logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
|
|
2024
|
+
// Agent-readiness reporter (docs/AGENT_READINESS_GATE_PLAN.md Phase 1):
|
|
2025
|
+
// probe host→Binance reachability (HTTP 451 geo-block) + clock drift on the
|
|
2026
|
+
// host and POST a plain-English report to the webapp, so a silently-broken
|
|
2027
|
+
// agent shows an actionable dashboard alert instead of a false green.
|
|
2028
|
+
// Advisory + fire-and-forget; no token → no-op. Runs here once (guarded by
|
|
2029
|
+
// the pluginInitialised early-return → once per process) + on an unref'd
|
|
2030
|
+
// interval inside the reporter.
|
|
2031
|
+
startReadinessReporter({
|
|
2032
|
+
apiBaseUrl,
|
|
2033
|
+
token: resolveIngestToken({ connectionToken }),
|
|
2034
|
+
binanceApi,
|
|
2035
|
+
toolCount: toolNames.length,
|
|
2036
|
+
});
|
|
2023
2037
|
maybeStartConnectorSupervisor();
|
|
2024
2038
|
},
|
|
2025
2039
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "ReefClaw trading plugin for OpenClaw \u2014 paper trading with real Binance market data, plus the ReefClaw dashboard connector (supervised by OpenClaw, no service manager needed). Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/skills/reefclaw/SKILL.md
CHANGED
|
@@ -39,6 +39,12 @@ You are being connected to **ReefClaw**, a supervised trading control room. Your
|
|
|
39
39
|
|
|
40
40
|
**Do not attempt to trade before the full instructions arrive.** If trading tools are visible but this file is still a bootstrap version (0.0.x), the connection has not completed — finish connecting first.
|
|
41
41
|
|
|
42
|
+
## Host requirements (check before connecting)
|
|
43
|
+
|
|
44
|
+
ReefClaw trades on Binance USD-M Futures, so **the machine running this OpenClaw must be able to reach Binance** (`fapi.binance.com`). Binance geo-restricts many locations: cheap US cloud regions — and some other jurisdictions — return **HTTP 451** ("restricted location"), and the agent then cannot fetch market data or trade. This applies even in **paper mode**, which uses live Binance prices.
|
|
45
|
+
|
|
46
|
+
If your market-data calls fail with `451`, this host is geo-blocked — move the agent to a Binance-permitted region (most EU and several Asia VPS regions work). This is a hosting requirement; ReefClaw cannot configure around it.
|
|
47
|
+
|
|
42
48
|
## Connecting (first run) — saving the connect message
|
|
43
49
|
|
|
44
50
|
The onboarding page at [reefclaw.com/onboarding](https://reefclaw.com/onboarding) gives the user a single **connect message** that carries THREE fields:
|