@reefclaw/connect 0.1.0 → 0.1.2

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.
@@ -22,6 +22,8 @@ export declare class Bridge {
22
22
  * round-trip. `null` means "unknown yet" — the plugin is the authority. */
23
23
  private lastKnownTradingMode;
24
24
  private readonly listeners;
25
+ /** rc_ connection token — also authenticates the webapp skill-content pull */
26
+ private readonly connectionToken;
25
27
  constructor(provider: OpenClawProvider, connectorConfig: ConnectorConfig);
26
28
  /** Start the bridge: connect to relay + start provider */
27
29
  start(): void;
@@ -91,6 +93,17 @@ export declare class Bridge {
91
93
  * has nothing stored; throws on transport error.
92
94
  */
93
95
  private pullAndApplySkillUpdate;
96
+ /**
97
+ * Pull the full SKILL.md from the webapp's authenticated endpoint and apply
98
+ * it when newer than local. This is how a FRESH install upgrades from the
99
+ * bundled bootstrap (v0.0.x) to the real trading instructions — the relay's
100
+ * per-room Durable-Object store is empty for a new user, so the relay push
101
+ * channel alone can never deliver the first full copy. Token-gated: no valid
102
+ * rc_ token → 401 → no content (the full SKILL.md never ships unauthenticated).
103
+ * Same downgrade guard + applySkillUpdate pipeline (incl. the C1 signature
104
+ * gate when enforced) as the relay pull path.
105
+ */
106
+ private pullSkillContentFromWebapp;
94
107
  /** Check relay's latest SKILL.md version against local and auto-update if stale */
95
108
  private handleConnectAck;
96
109
  }
@@ -77,10 +77,13 @@ export class Bridge {
77
77
  // Bound listeners for cleanup
78
78
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
79
  listeners = [];
80
+ /** rc_ connection token — also authenticates the webapp skill-content pull */
81
+ connectionToken;
80
82
  constructor(provider, connectorConfig) {
81
83
  this.provider = provider;
82
84
  this.currentSkillVersion = readLocalSkillVersion();
83
85
  logger.info(TAG, `Local SKILL.md version: ${this.currentSkillVersion ?? 'unknown'}`);
86
+ this.connectionToken = connectorConfig.token;
84
87
  this.connector = new Connector(connectorConfig, {
85
88
  onRequest: (frame) => this.handleRequest(frame),
86
89
  onStateChange: (state) => this.handleStateChange(state),
@@ -94,6 +97,11 @@ export class Bridge {
94
97
  this.startThrottleTimer();
95
98
  this.provider.start();
96
99
  this.connector.connect();
100
+ // Fresh installs ship only the thin BOOTSTRAP SKILL.md (v0.0.x) — the full
101
+ // trading instructions live behind the authenticated webapp endpoint, not
102
+ // in the public npm package. Fire-and-forget with retries; the relay OTA
103
+ // path (handleConnectAck) remains the push channel for updates.
104
+ void this.pullSkillContentFromWebapp();
97
105
  }
98
106
  /** Stop the bridge: disconnect + stop provider */
99
107
  stop() {
@@ -897,6 +905,65 @@ export class Bridge {
897
905
  });
898
906
  return updateResult;
899
907
  }
908
+ /**
909
+ * Pull the full SKILL.md from the webapp's authenticated endpoint and apply
910
+ * it when newer than local. This is how a FRESH install upgrades from the
911
+ * bundled bootstrap (v0.0.x) to the real trading instructions — the relay's
912
+ * per-room Durable-Object store is empty for a new user, so the relay push
913
+ * channel alone can never deliver the first full copy. Token-gated: no valid
914
+ * rc_ token → 401 → no content (the full SKILL.md never ships unauthenticated).
915
+ * Same downgrade guard + applySkillUpdate pipeline (incl. the C1 signature
916
+ * gate when enforced) as the relay pull path.
917
+ */
918
+ async pullSkillContentFromWebapp(attempt = 1) {
919
+ const MAX_ATTEMPTS = 5;
920
+ // www is load-bearing: reefclaw.com 307-redirects and Node fetch strips the
921
+ // Authorization header on cross-origin redirect (verified 2026-03-18).
922
+ const base = (process.env.REEFCLAW_API_URL || 'https://www.reefclaw.com').replace(/\/$/, '');
923
+ try {
924
+ const res = await fetch(`${base}/api/internal/skill-content`, {
925
+ headers: { Authorization: `Bearer ${this.connectionToken}` },
926
+ signal: AbortSignal.timeout(15_000),
927
+ });
928
+ if (res.status === 401 || res.status === 403) {
929
+ logger.warn(TAG, `SKILL.md webapp pull: not authorized (${res.status}) — connect the account first`);
930
+ return; // A bad token won't get better by retrying.
931
+ }
932
+ if (!res.ok)
933
+ throw new Error(`HTTP ${res.status}`);
934
+ const body = (await res.json());
935
+ if (!body?.version || !body?.content) {
936
+ logger.warn(TAG, 'SKILL.md webapp pull: response missing version/content');
937
+ return;
938
+ }
939
+ this.currentSkillVersion = readLocalSkillVersion();
940
+ if (compareSemver(body.version, this.currentSkillVersion) <= 0) {
941
+ logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} already >= webapp v${body.version} — nothing to do`);
942
+ return;
943
+ }
944
+ logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} behind webapp v${body.version} — applying`);
945
+ const result = await this.applySkillUpdate({ version: body.version, content: body.content });
946
+ this.emit('agent_state', 'skill_update_applied', {
947
+ event: 'skill_update_applied',
948
+ version: body.version,
949
+ success: result.success,
950
+ source: 'webapp_pull',
951
+ timestamp: new Date().toISOString(),
952
+ });
953
+ if (!result.success)
954
+ logger.error(TAG, `SKILL.md webapp pull: apply failed: ${result.message}`);
955
+ }
956
+ catch (err) {
957
+ const msg = err instanceof Error ? err.message : String(err);
958
+ if (attempt >= MAX_ATTEMPTS) {
959
+ logger.error(TAG, `SKILL.md webapp pull failed after ${MAX_ATTEMPTS} attempts: ${msg}`);
960
+ return;
961
+ }
962
+ const delayMs = attempt * 30_000;
963
+ logger.warn(TAG, `SKILL.md webapp pull failed (attempt ${attempt}/${MAX_ATTEMPTS}): ${msg} — retrying in ${delayMs / 1000}s`);
964
+ setTimeout(() => void this.pullSkillContentFromWebapp(attempt + 1), delayMs).unref?.();
965
+ }
966
+ }
900
967
  /** Check relay's latest SKILL.md version against local and auto-update if stale */
901
968
  async handleConnectAck(payload) {
902
969
  const latestVersion = payload.latestSkillVersion;
@@ -18,7 +18,10 @@ export function readLocalSkillVersion() {
18
18
  return null;
19
19
  }
20
20
  }
21
- const MAX_SKILL_CONTENT_SIZE = 100_000; // 100KB
21
+ // 120KB the full SKILL.md crossed 100KB at v2.20.0 (103.7KB), which silently
22
+ // broke every OTA apply against the old 100KB cap. Keep comfortably under the
23
+ // 128KiB Cloudflare Durable-Object per-value hard limit the relay stores into.
24
+ const MAX_SKILL_CONTENT_SIZE = 120_000;
22
25
  const SEMVER_REGEX = /^\d+\.\d+\.\d+$/;
23
26
  /**
24
27
  * Compare two semver strings.