@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.
- package/assets/plugin/config/plugin-config-io.d.ts +13 -0
- package/assets/plugin/config/plugin-config-io.js +27 -0
- package/assets/plugin/config/user-data-stream-config.d.ts +4 -0
- package/assets/plugin/config/user-data-stream-config.js +17 -2
- package/assets/plugin/connector-supervisor.d.ts +16 -0
- package/assets/plugin/connector-supervisor.js +34 -3
- package/assets/plugin/index.js +25 -6
- package/assets/plugin/strategy/builtin-strategies.js +7 -3
- package/assets/skill/SKILL.md +11 -1
- package/package.json +1 -1
- package/dist/daemon.js +0 -104
|
@@ -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
|
-
|
|
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 ??
|
|
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
|
|
69
|
+
logger.warn(TAG, `connector not found at ${indexJs} — supervisor idle`);
|
|
39
70
|
return null;
|
|
40
71
|
}
|
|
41
72
|
const spawnFn = opts.spawnFn ?? spawn;
|
package/assets/plugin/index.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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',
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reefclaw
|
|
3
|
-
version: 0.0.
|
|
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
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
|
-
}
|