@reefclaw/connect 0.1.3 → 0.1.5

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.
@@ -1,4 +1,17 @@
1
1
  import type { TradingMode, ExchangeConfig } from '../types.js';
2
+ /** The connection the AGENT saves during onboarding lives in OpenClaw's own
3
+ * config (`skills.entries.reefclaw.config` in ~/.openclaw/openclaw.json) —
4
+ * the connector reads it there, and since the chat-install flow never writes
5
+ * ~/.reefclaw/plugin-config.json, the PLUGIN must fall back to it too or
6
+ * every fresh install has working dashboards but token-starved intel tools,
7
+ * journaling ingest, and config polling (hit live on the first real user,
8
+ * 2026-07-06). Nested `.config.*` is the schema-valid shape; legacy flat
9
+ * fields are read as a fallback. Fail-soft: {} on any problem. */
10
+ export declare function readOpenClawConnection(): {
11
+ token?: string;
12
+ userId?: string;
13
+ relayUrl?: string;
14
+ };
2
15
  /** Full on-disk schema. Unknown keys are preserved on round-trip. */
3
16
  export interface PluginConfigFile {
4
17
  connectionToken?: string;
@@ -8,6 +8,33 @@
8
8
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { dirname, join } from 'node:path';
11
+ /** The connection the AGENT saves during onboarding lives in OpenClaw's own
12
+ * config (`skills.entries.reefclaw.config` in ~/.openclaw/openclaw.json) —
13
+ * the connector reads it there, and since the chat-install flow never writes
14
+ * ~/.reefclaw/plugin-config.json, the PLUGIN must fall back to it too or
15
+ * every fresh install has working dashboards but token-starved intel tools,
16
+ * journaling ingest, and config polling (hit live on the first real user,
17
+ * 2026-07-06). Nested `.config.*` is the schema-valid shape; legacy flat
18
+ * fields are read as a fallback. Fail-soft: {} on any problem. */
19
+ export function readOpenClawConnection() {
20
+ try {
21
+ const path = process.env.OPENCLAW_CONFIG_PATH ?? join(homedir(), '.openclaw', 'openclaw.json');
22
+ if (!existsSync(path))
23
+ return {};
24
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
25
+ const entry = parsed?.skills?.entries?.reefclaw;
26
+ if (!entry)
27
+ return {};
28
+ return {
29
+ token: entry.config?.token ?? entry.token,
30
+ userId: entry.config?.userId ?? entry.userId,
31
+ relayUrl: entry.config?.relayUrl ?? entry.relayUrl,
32
+ };
33
+ }
34
+ catch {
35
+ return {};
36
+ }
37
+ }
11
38
  export function defaultConfigPath() {
12
39
  return join(homedir(), '.reefclaw', 'plugin-config.json');
13
40
  }
@@ -64,6 +64,10 @@ export declare function getUserDataStreamIngestBaseUrl(config?: PluginConfigFile
64
64
  *
65
65
  * Both ingest paths (position-decisions journal + WS audit-trail) use this. */
66
66
  export declare function resolveIngestToken(config?: PluginConfigFile): string;
67
+ /** Resolve the trader's ReefClaw userId: env (legacy prod deployments) →
68
+ * the connection the agent saved in OpenClaw's config. Ingest paths need it
69
+ * alongside the token; fresh installs only ever have the latter source. */
70
+ export declare function resolveReefclawUserId(): string;
67
71
  /** Is the WS authoritative for reads (observe or enforce)? In shadow it's
68
72
  * only observed — REST remains the source of truth for getPositions etc. */
69
73
  export declare function userDataStreamAuthoritative(mode: UserDataStreamMode): boolean;
@@ -20,7 +20,7 @@
20
20
  // Transitions must progress forward only (no skipping). Deploy script enforces.
21
21
  // Reverse transitions to any earlier stage (including 'shadow' and 'off') are
22
22
  // always permitted as a safety-net rollback.
23
- import { readPluginConfig } from './plugin-config-io.js';
23
+ import { readPluginConfig, readOpenClawConnection } from './plugin-config-io.js';
24
24
  const VALID = new Set(['off', 'shadow', 'observe', 'enforce']);
25
25
  /** Allowed forward transitions. Reverse transitions are always permitted so
26
26
  * the operator can fall back to REST if WS misbehaves.
@@ -138,7 +138,22 @@ export function resolveIngestToken(config) {
138
138
  const fromConfig = config?.connectionToken;
139
139
  if (typeof fromConfig === 'string' && fromConfig.trim())
140
140
  return fromConfig.trim();
141
- return process.env.WEBAPP_INGEST_TOKEN ?? '';
141
+ const fromEnv = process.env.WEBAPP_INGEST_TOKEN ?? '';
142
+ if (fromEnv.trim())
143
+ return fromEnv.trim();
144
+ // Chat/npx onboarding stores the token only where the AGENT saves it —
145
+ // OpenClaw's own config. Without this fallback every fresh install has a
146
+ // token-starved plugin (intel tools, ingest, config poller all dead).
147
+ return readOpenClawConnection().token?.trim() ?? '';
148
+ }
149
+ /** Resolve the trader's ReefClaw userId: env (legacy prod deployments) →
150
+ * the connection the agent saved in OpenClaw's config. Ingest paths need it
151
+ * alongside the token; fresh installs only ever have the latter source. */
152
+ export function resolveReefclawUserId() {
153
+ const fromEnv = process.env.REEFCLAW_USER_ID ?? '';
154
+ if (fromEnv.trim())
155
+ return fromEnv.trim();
156
+ return readOpenClawConnection().userId?.trim() ?? '';
142
157
  }
143
158
  /** Is the WS authoritative for reads (observe or enforce)? In shadow it's
144
159
  * only observed — REST remains the source of truth for getPositions etc. */
@@ -1,4 +1,20 @@
1
1
  import { spawn } from 'node:child_process';
2
+ /** Bridge bundled INSIDE the plugin package (the npm-channel distribution
3
+ * `@reefclaw/openclaw-plugin` ships the connector at <pluginRoot>/bridge).
4
+ * Compiled connector-supervisor.js sits at the plugin dist root, so the
5
+ * bundled bridge is a sibling directory. */
6
+ export declare function bundledBridgeDir(): string;
7
+ /** Where the connector lives, in preference order: bundled-in-package first
8
+ * (npm-channel install — self-contained), then the npx installer's
9
+ * placement. Returns null when neither exists. */
10
+ export declare function resolveBridgeDir(): string | null;
11
+ /** True when the plugin should supervise the connector even WITHOUT an
12
+ * explicit connectorSupervisor='on' in plugin-config: the connector is
13
+ * bundled inside this plugin package, which only the npm-channel
14
+ * distribution does. Prod's linked plugin dir and the npx installer's
15
+ * ~/.reefclaw/plugin have no bridge/ subdir, so they stay opt-in — prod
16
+ * keeps running the bridge under systemd and must never double-connect. */
17
+ export declare function hasBundledBridge(): boolean;
2
18
  export interface ConnectorSupervisorOptions {
3
19
  /** Directory holding the placed bridge (default ~/.reefclaw/bridge). */
4
20
  bridgeDir?: string;
@@ -16,9 +16,36 @@
16
16
  import { spawn } from 'node:child_process';
17
17
  import { existsSync } from 'node:fs';
18
18
  import { homedir } from 'node:os';
19
- import { join } from 'node:path';
19
+ import { join, dirname } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
20
21
  import { logger } from './logger.js';
21
22
  const TAG = 'connector-supervisor';
23
+ /** Bridge bundled INSIDE the plugin package (the npm-channel distribution
24
+ * `@reefclaw/openclaw-plugin` ships the connector at <pluginRoot>/bridge).
25
+ * Compiled connector-supervisor.js sits at the plugin dist root, so the
26
+ * bundled bridge is a sibling directory. */
27
+ export function bundledBridgeDir() {
28
+ return join(dirname(fileURLToPath(import.meta.url)), 'bridge');
29
+ }
30
+ /** Where the connector lives, in preference order: bundled-in-package first
31
+ * (npm-channel install — self-contained), then the npx installer's
32
+ * placement. Returns null when neither exists. */
33
+ export function resolveBridgeDir() {
34
+ for (const dir of [bundledBridgeDir(), join(homedir(), '.reefclaw', 'bridge')]) {
35
+ if (existsSync(join(dir, 'index.js')))
36
+ return dir;
37
+ }
38
+ return null;
39
+ }
40
+ /** True when the plugin should supervise the connector even WITHOUT an
41
+ * explicit connectorSupervisor='on' in plugin-config: the connector is
42
+ * bundled inside this plugin package, which only the npm-channel
43
+ * distribution does. Prod's linked plugin dir and the npx installer's
44
+ * ~/.reefclaw/plugin have no bridge/ subdir, so they stay opt-in — prod
45
+ * keeps running the bridge under systemd and must never double-connect. */
46
+ export function hasBundledBridge() {
47
+ return existsSync(join(bundledBridgeDir(), 'index.js'));
48
+ }
22
49
  const MIN_BACKOFF_MS = 5_000;
23
50
  const MAX_BACKOFF_MS = 60_000;
24
51
  /** A child that survives this long resets the backoff (it was healthy). */
@@ -32,10 +59,14 @@ let singleton = null;
32
59
  export function startConnectorSupervisor(opts = {}) {
33
60
  if (singleton)
34
61
  return singleton;
35
- const bridgeDir = opts.bridgeDir ?? join(homedir(), '.reefclaw', 'bridge');
62
+ const bridgeDir = opts.bridgeDir ?? resolveBridgeDir();
63
+ if (!bridgeDir) {
64
+ logger.warn(TAG, 'connector not found (no bundled bridge/ and no ~/.reefclaw/bridge) — supervisor idle');
65
+ return null;
66
+ }
36
67
  const indexJs = join(bridgeDir, 'index.js');
37
68
  if (!existsSync(indexJs)) {
38
- logger.warn(TAG, `connector not found at ${indexJs} — supervisor idle (run the installer first)`);
69
+ logger.warn(TAG, `connector not found at ${indexJs} — supervisor idle`);
39
70
  return null;
40
71
  }
41
72
  const spawnFn = opts.spawnFn ?? spawn;
@@ -20,7 +20,7 @@ import { DEFAULT_CONFIG } from './types.js';
20
20
  import { PaperAdapter } from './paper-adapter.js';
21
21
  import { LiveAdapter } from './live/live-adapter.js';
22
22
  import { loadBracketMode } from './config/brackets-config.js';
23
- import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamDbWrite, getUserDataStreamIngestBaseUrl, resolveIngestToken, } from './config/user-data-stream-config.js';
23
+ import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamDbWrite, getUserDataStreamIngestBaseUrl, resolveIngestToken, resolveReefclawUserId, } from './config/user-data-stream-config.js';
24
24
  import { TradeStoreClient } from './ingest/trade-store-client.js';
25
25
  import { ProposalManager } from './live/proposal-manager.js';
26
26
  import { ProposalDecisionListener } from './live/proposal-decision-listener.js';
@@ -39,8 +39,8 @@ import { proposeLearningTool } from './tools/propose-learning.js';
39
39
  import { queryReviewOutcomesTool } from './tools/query-review-outcomes.js';
40
40
  import { loadPositionReviewMode } from './config/position-review-config.js';
41
41
  import { installSignalHandlers } from './lifecycle/install-signal-handlers.js';
42
- import { readPluginConfig } from './config/plugin-config-io.js';
43
- import { startConnectorSupervisor } from './connector-supervisor.js';
42
+ import { readPluginConfig, readOpenClawConnection } from './config/plugin-config-io.js';
43
+ import { startConnectorSupervisor, hasBundledBridge } from './connector-supervisor.js';
44
44
  import { ToolGate } from './config/tool-gate.js';
45
45
  import { gateStore } from './config/gate-store.js';
46
46
  import { startAgentConfigPoller } from './config/agent-config-poller.js';
@@ -945,6 +945,17 @@ const paperTradingPlugin = {
945
945
  catch (err) {
946
946
  logger.warn(TAG, `Could not read ReefClaw config: ${formatError(err)}`);
947
947
  }
948
+ if (!connectionToken) {
949
+ // Chat/npx onboarding saves the connection ONLY in OpenClaw's config
950
+ // (skills.entries.reefclaw.config) — the agent writes it there and the
951
+ // connector reads it there. Fall back so a fresh install's intel tools,
952
+ // ingest, and config poller aren't token-starved (first-user bug).
953
+ const conn = readOpenClawConnection();
954
+ if (conn.token) {
955
+ connectionToken = conn.token;
956
+ logger.info(TAG, 'Connection token loaded from OpenClaw config (skills.entries.reefclaw)');
957
+ }
958
+ }
948
959
  if (connectionToken) {
949
960
  logger.info(TAG, `Connection token loaded (prefix: ${connectionToken.slice(0, 7)}...)`);
950
961
  }
@@ -1020,7 +1031,7 @@ const paperTradingPlugin = {
1020
1031
  // Ingest token: plugin-config.json `connectionToken` preferred, legacy
1021
1032
  // WEBAPP_INGEST_TOKEN env as fallback — see resolveIngestToken.
1022
1033
  const ingestToken = resolveIngestToken(cfgForIngest);
1023
- const reefclawUserId = process.env.REEFCLAW_USER_ID ?? '';
1034
+ const reefclawUserId = resolveReefclawUserId();
1024
1035
  if (ingestToken && reefclawUserId) {
1025
1036
  const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForIngest);
1026
1037
  positionDecisionsClient = new PositionDecisionsClient({
@@ -1124,7 +1135,7 @@ const paperTradingPlugin = {
1124
1135
  cfgForIngest = undefined;
1125
1136
  }
1126
1137
  const ingestToken = resolveIngestToken(cfgForIngest);
1127
- const reefclawUserId = process.env.REEFCLAW_USER_ID ?? '';
1138
+ const reefclawUserId = resolveReefclawUserId();
1128
1139
  if (userDataStreamMode !== 'off' &&
1129
1140
  dbWriteMode === 'on' &&
1130
1141
  ingestToken &&
@@ -2010,8 +2021,16 @@ function maybeStartConnectorSupervisor() {
2010
2021
  if (process.env.RC_CONNECTOR_SUPERVISOR === 'off') {
2011
2022
  if (supervisorMode === 'on')
2012
2023
  logger.warn(TAG, 'connector supervisor disabled by RC_CONNECTOR_SUPERVISOR=off');
2024
+ return;
2013
2025
  }
2014
- else if (supervisorMode === 'on') {
2026
+ if (supervisorMode === 'off')
2027
+ return;
2028
+ // 'on' → explicit opt-in (the npx installer writes this).
2029
+ // undefined → auto-start ONLY when the connector is bundled inside this
2030
+ // plugin package (the npm-channel @reefclaw/openclaw-plugin distribution
2031
+ // is self-contained by design). Prod's linked plugin dir has no bundled
2032
+ // bridge and no flag → stays off; systemd keeps owning prod's bridge.
2033
+ if (supervisorMode === 'on' || hasBundledBridge()) {
2015
2034
  startConnectorSupervisor();
2016
2035
  }
2017
2036
  }
@@ -1,6 +1,10 @@
1
- // 6 built-in strategy definitions for the Strategy Box.
2
- // Seeded into the strategies table on first run with is_builtin: true.
3
- // Users can toggle them on/off but cannot delete or modify them.
1
+ // LEGACY strategy configs used ONLY as fixtures by the local-evaluator
2
+ // tests (strategy/__tests__/evaluator.test.ts). NOT seeded anywhere and NOT
3
+ // part of the product catalog: the shipped catalog lives in
4
+ // intelligence/src/signals/strategy-store.ts (exactly two template
5
+ // strategies since the 2026-07-06 trim — see CLAUDE.md), and the plugin's
6
+ // local evaluator (signals.evaluator, default 'central') receives its
7
+ // strategy set from the intel service, not from this file.
4
8
  export const BUILTIN_STRATEGIES = [
5
9
  {
6
10
  name: 'trend_continuation',
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: reefclaw
3
- version: 0.0.2
3
+ version: 0.0.5
4
4
  description: ReefClaw trading control room — bootstrap (connects your agent; full trading instructions arrive automatically after first connect)
5
5
  author: ReefClaw
6
6
  homepage: https://reefclaw.com
@@ -66,6 +66,16 @@ When a user gives you this message (or just a bare `Here's my ReefClaw token: rc
66
66
  - After saving, the connector picks the config up within seconds (it restarts until a token appears). The user does not need to run any command.
67
67
  - If you cannot write the config yourself (e.g. exec or file access is restricted in this session), show the user this exact JSON snippet and where it goes, and offer to verify it after they save it.
68
68
 
69
+ ## Installing without any terminal (preferred)
70
+
71
+ OpenClaw can install the ReefClaw plugin through its own owner chat command — no exec, no terminal:
72
+
73
+ 1. In `~/.openclaw/openclaw.json` set `commands.plugins: true` and add the user's **sender id** (visible in this session) to `commands.ownerAllowFrom`.
74
+ 2. Tell the user to type `/restart` in this chat.
75
+ 3. Then the user types: `/plugins install clawhub:@reefclaw/openclaw-plugin` — the install restarts OpenClaw automatically; the bundled connector starts by itself and the dashboard flips to Connected.
76
+
77
+ Only fall back to `npx @reefclaw/connect` (below) when the chat install is unavailable.
78
+
69
79
  ## If you cannot run the installer (elevated exec disabled)
70
80
 
71
81
  Fresh OpenClaw installs deny elevated (host) exec from chat sessions. If `npx @reefclaw/connect` is blocked, do NOT dead-end — offer the user this choice:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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": {
package/dist/daemon.js DELETED
@@ -1,104 +0,0 @@
1
- // Keep the bridge running across reboots. Linux/systemd-user is implemented
2
- // fully; macOS and Windows fall back to printing the manual run command (a
3
- // launchd/Task-Scheduler unit is a follow-up). The bridge reads its config from
4
- // ~/.openclaw/openclaw.json, so until the user pastes their connect message the
5
- // service will start, find no token, and restart — harmless; it connects within
6
- // seconds of the agent writing the config.
7
- import { writeFileSync, mkdirSync } from 'node:fs';
8
- import { join } from 'node:path';
9
- import { homedir, userInfo } from 'node:os';
10
- import { BRIDGE_DIR } from './paths.js';
11
- import { run, which } from './exec.js';
12
- import { step, ok, info, warn } from './ui.js';
13
- const SERVICE_NAME = 'reefclaw-bridge';
14
- const NODE = process.execPath; // absolute path to the node running the installer
15
- /**
16
- * Build the systemd user-unit text. Pure + exported so the path-quoting is
17
- * unit-testable. Both `node` (process.execPath) and `bridgeDir` (under the
18
- * user's home) can contain spaces. Quoting rules differ per directive:
19
- * - ExecStart= is parsed with shell-like word splitting, so an unquoted
20
- * `ExecStart=/home/a b/node …` reads the binary as `/home/a` — QUOTE both
21
- * the binary and the script path.
22
- * - WorkingDirectory= takes the raw value after `=` as a single path (no word
23
- * splitting) — spaces are safe UNQUOTED, and quotes are treated as literal
24
- * characters, failing the unit with "path is not absolute" (verified live
25
- * on systemd 255 / Ubuntu 24.04). Do NOT quote it.
26
- */
27
- export function buildSystemdUnit(node, bridgeDir) {
28
- const indexJs = join(bridgeDir, 'index.js');
29
- return `[Unit]
30
- Description=ReefClaw connector - bridges OpenClaw to the ReefClaw dashboard
31
- After=network-online.target
32
- Wants=network-online.target
33
-
34
- [Service]
35
- Type=simple
36
- WorkingDirectory=${bridgeDir}
37
- ExecStart="${node}" "${indexJs}" --provider gateway --log-level info
38
- Restart=always
39
- RestartSec=5s
40
-
41
- [Install]
42
- WantedBy=default.target
43
- `;
44
- }
45
- function manualHint() {
46
- warn('Could not set up an auto-start service on this OS yet.');
47
- info('Keep the connector running with this command (leave it open / use your own service manager):');
48
- info(` "${NODE}" "${join(BRIDGE_DIR, 'index.js')}" --provider gateway`);
49
- }
50
- function installSystemd() {
51
- if (!which('systemctl'))
52
- return false;
53
- const unitDir = join(homedir(), '.config', 'systemd', 'user');
54
- mkdirSync(unitDir, { recursive: true });
55
- const unit = buildSystemdUnit(NODE, BRIDGE_DIR);
56
- writeFileSync(join(unitDir, `${SERVICE_NAME}.service`), unit, 'utf-8');
57
- run('systemctl', ['--user', 'daemon-reload']);
58
- const enabled = run('systemctl', ['--user', 'enable', '--now', `${SERVICE_NAME}.service`]);
59
- if (!enabled.ok) {
60
- warn('systemd --user enable/start did not succeed:');
61
- if (enabled.stderr.trim())
62
- info(enabled.stderr.trim().split('\n').slice(-2).join('\n'));
63
- info(`Try: systemctl --user enable --now ${SERVICE_NAME}.service`);
64
- return false;
65
- }
66
- // `enable --now` can exit 0 while the unit failed to load (e.g. a bad unit
67
- // file setting) — verify the unit actually came up before claiming ✓.
68
- // 'active' = running; 'activating' = the expected pre-token restart loop
69
- // (the bridge exits until the user pastes their connect message, and
70
- // Restart=always re-launches it). Anything else (inactive/failed) means the
71
- // unit never loaded.
72
- const active = run('systemctl', ['--user', 'is-active', `${SERVICE_NAME}.service`]);
73
- const state = active.stdout.trim();
74
- if (state !== 'active' && state !== 'activating') {
75
- warn(`the service did not come up (state: ${state || 'unknown'}).`);
76
- info(`Inspect: systemctl --user status ${SERVICE_NAME}.service`);
77
- return false;
78
- }
79
- // Linger lets the user service run without an active login session (servers).
80
- // Best-effort: needs privileges; non-fatal if it fails.
81
- const linger = run('loginctl', ['enable-linger', userInfo().username]);
82
- if (linger.ok) {
83
- info('enabled linger (service survives logout / reboot)');
84
- }
85
- else {
86
- info('note: run `sudo loginctl enable-linger $USER` so the connector survives logout.');
87
- }
88
- return true;
89
- }
90
- export function installDaemon() {
91
- step('Starting the connector as a background service');
92
- if (process.platform === 'linux') {
93
- if (installSystemd()) {
94
- ok(`connector running as a systemd user service (${SERVICE_NAME})`);
95
- info(`logs: journalctl --user -u ${SERVICE_NAME} -f`);
96
- return true;
97
- }
98
- manualHint();
99
- return false;
100
- }
101
- // macOS / Windows: manual for now (launchd / Task Scheduler unit is a follow-up).
102
- manualHint();
103
- return false;
104
- }