@reefclaw/connect 0.1.2 → 0.1.4
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 +6 -0
- package/assets/plugin/connector-supervisor.d.ts +36 -0
- package/assets/plugin/connector-supervisor.js +149 -0
- package/assets/plugin/index.js +41 -0
- package/assets/skill/SKILL.md +11 -1
- package/dist/cli.js +7 -4
- package/dist/supervisor-config.js +38 -0
- package/package.json +1 -1
|
@@ -2,6 +2,12 @@ import type { TradingMode, ExchangeConfig } from '../types.js';
|
|
|
2
2
|
/** Full on-disk schema. Unknown keys are preserved on round-trip. */
|
|
3
3
|
export interface PluginConfigFile {
|
|
4
4
|
connectionToken?: string;
|
|
5
|
+
/** 'on' → the plugin spawns + restarts the relay connector as a child of
|
|
6
|
+
* the gateway process (no systemd — OpenClaw is the process manager).
|
|
7
|
+
* Written by the npx installer on fresh installs. MUST stay defaulted off:
|
|
8
|
+
* prod runs the bridge under systemd and a supervisor there would
|
|
9
|
+
* double-connect the relay room. Kill-switch: RC_CONNECTOR_SUPERVISOR=off. */
|
|
10
|
+
connectorSupervisor?: 'on' | 'off';
|
|
5
11
|
apiBaseUrl?: string;
|
|
6
12
|
intelligenceUrl?: string;
|
|
7
13
|
exchange?: ExchangeConfig;
|
|
@@ -0,0 +1,36 @@
|
|
|
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;
|
|
18
|
+
export interface ConnectorSupervisorOptions {
|
|
19
|
+
/** Directory holding the placed bridge (default ~/.reefclaw/bridge). */
|
|
20
|
+
bridgeDir?: string;
|
|
21
|
+
/** Injectable spawn for tests. */
|
|
22
|
+
spawnFn?: typeof spawn;
|
|
23
|
+
}
|
|
24
|
+
export interface ConnectorSupervisorHandle {
|
|
25
|
+
stop(): void;
|
|
26
|
+
/** Test/debug introspection. */
|
|
27
|
+
isRunning(): boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Start supervising the connector. Idempotent per process — repeat calls
|
|
31
|
+
* (OpenClaw can invoke register() more than once) return the existing handle.
|
|
32
|
+
* Returns null when the bridge isn't placed on disk (installer not run).
|
|
33
|
+
*/
|
|
34
|
+
export declare function startConnectorSupervisor(opts?: ConnectorSupervisorOptions): ConnectorSupervisorHandle | null;
|
|
35
|
+
/** Test-only: reset the module singleton. */
|
|
36
|
+
export declare function resetConnectorSupervisorForTest(): void;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Plugin-supervised ReefClaw connector (frictionless-onboarding phase 1).
|
|
2
|
+
//
|
|
3
|
+
// The plugin runs persistently inside the OpenClaw gateway process, so it can
|
|
4
|
+
// supervise the relay connector (the bridge) as a child process — making
|
|
5
|
+
// OpenClaw itself the process manager. This removes every host-level concern
|
|
6
|
+
// from onboarding: no systemd, no terminal, works inside containers, and the
|
|
7
|
+
// connector's lifetime is correctly tied to the gateway's (gateway down ⇒
|
|
8
|
+
// nothing to bridge anyway — the plugin holding the exchange keys lives in
|
|
9
|
+
// the same process).
|
|
10
|
+
//
|
|
11
|
+
// SAFETY: default OFF. Prod runs the bridge as `reefclaw-skill.service`
|
|
12
|
+
// (systemd) — a supervisor turned on there would double-connect the relay
|
|
13
|
+
// room. Only the fresh-install path (the npx installer) writes
|
|
14
|
+
// `connectorSupervisor: 'on'` into ~/.reefclaw/plugin-config.json.
|
|
15
|
+
// Kill-switch: RC_CONNECTOR_SUPERVISOR=off beats config.
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join, dirname } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { logger } from './logger.js';
|
|
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
|
+
}
|
|
49
|
+
const MIN_BACKOFF_MS = 5_000;
|
|
50
|
+
const MAX_BACKOFF_MS = 60_000;
|
|
51
|
+
/** A child that survives this long resets the backoff (it was healthy). */
|
|
52
|
+
const STABLE_RESET_MS = 5 * 60_000;
|
|
53
|
+
let singleton = null;
|
|
54
|
+
/**
|
|
55
|
+
* Start supervising the connector. Idempotent per process — repeat calls
|
|
56
|
+
* (OpenClaw can invoke register() more than once) return the existing handle.
|
|
57
|
+
* Returns null when the bridge isn't placed on disk (installer not run).
|
|
58
|
+
*/
|
|
59
|
+
export function startConnectorSupervisor(opts = {}) {
|
|
60
|
+
if (singleton)
|
|
61
|
+
return singleton;
|
|
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
|
+
}
|
|
67
|
+
const indexJs = join(bridgeDir, 'index.js');
|
|
68
|
+
if (!existsSync(indexJs)) {
|
|
69
|
+
logger.warn(TAG, `connector not found at ${indexJs} — supervisor idle`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const spawnFn = opts.spawnFn ?? spawn;
|
|
73
|
+
let child = null;
|
|
74
|
+
let stopped = false;
|
|
75
|
+
let backoffMs = MIN_BACKOFF_MS;
|
|
76
|
+
let restartTimer = null;
|
|
77
|
+
const launch = () => {
|
|
78
|
+
if (stopped)
|
|
79
|
+
return;
|
|
80
|
+
const startedAt = Date.now();
|
|
81
|
+
// NOT detached: the connector must die with the gateway (a gateway
|
|
82
|
+
// restart resurrects both, and an orphan bridge can never linger).
|
|
83
|
+
child = spawnFn(process.execPath, [indexJs, '--provider', 'gateway', '--log-level', 'info'], {
|
|
84
|
+
cwd: bridgeDir,
|
|
85
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
});
|
|
87
|
+
logger.info(TAG, `connector started (pid ${child.pid})`);
|
|
88
|
+
const forward = (stream, level) => {
|
|
89
|
+
stream?.on('data', (chunk) => {
|
|
90
|
+
for (const line of chunk.toString().split('\n')) {
|
|
91
|
+
if (line.trim())
|
|
92
|
+
logger[level](TAG, `[connector] ${line}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
forward(child.stdout, 'info');
|
|
97
|
+
forward(child.stderr, 'warn');
|
|
98
|
+
child.on('exit', (code, signal) => {
|
|
99
|
+
child = null;
|
|
100
|
+
if (stopped)
|
|
101
|
+
return;
|
|
102
|
+
const aliveMs = Date.now() - startedAt;
|
|
103
|
+
if (aliveMs >= STABLE_RESET_MS)
|
|
104
|
+
backoffMs = MIN_BACKOFF_MS;
|
|
105
|
+
// Exit is EXPECTED pre-token (the connector exits until the connect
|
|
106
|
+
// message is saved) — restart with backoff, exactly like systemd's
|
|
107
|
+
// Restart=always did.
|
|
108
|
+
logger.info(TAG, `connector exited (code=${code ?? 'null'} signal=${signal ?? 'null'} after ${Math.round(aliveMs / 1000)}s) — restarting in ${backoffMs / 1000}s`);
|
|
109
|
+
restartTimer = setTimeout(launch, backoffMs);
|
|
110
|
+
restartTimer.unref?.();
|
|
111
|
+
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
112
|
+
});
|
|
113
|
+
child.on('error', (err) => {
|
|
114
|
+
logger.error(TAG, `connector spawn failed: ${err.message}`);
|
|
115
|
+
child = null;
|
|
116
|
+
if (stopped)
|
|
117
|
+
return;
|
|
118
|
+
restartTimer = setTimeout(launch, backoffMs);
|
|
119
|
+
restartTimer.unref?.();
|
|
120
|
+
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
launch();
|
|
124
|
+
const handle = {
|
|
125
|
+
stop() {
|
|
126
|
+
stopped = true;
|
|
127
|
+
if (restartTimer)
|
|
128
|
+
clearTimeout(restartTimer);
|
|
129
|
+
if (child) {
|
|
130
|
+
try {
|
|
131
|
+
child.kill('SIGTERM');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* already gone */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
singleton = null;
|
|
138
|
+
},
|
|
139
|
+
isRunning() {
|
|
140
|
+
return child !== null;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
singleton = handle;
|
|
144
|
+
return handle;
|
|
145
|
+
}
|
|
146
|
+
/** Test-only: reset the module singleton. */
|
|
147
|
+
export function resetConnectorSupervisorForTest() {
|
|
148
|
+
singleton = null;
|
|
149
|
+
}
|
package/assets/plugin/index.js
CHANGED
|
@@ -40,6 +40,7 @@ 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
42
|
import { readPluginConfig } from './config/plugin-config-io.js';
|
|
43
|
+
import { startConnectorSupervisor, hasBundledBridge } from './connector-supervisor.js';
|
|
43
44
|
import { ToolGate } from './config/tool-gate.js';
|
|
44
45
|
import { gateStore } from './config/gate-store.js';
|
|
45
46
|
import { startAgentConfigPoller } from './config/agent-config-poller.js';
|
|
@@ -861,6 +862,13 @@ const paperTradingPlugin = {
|
|
|
861
862
|
if (pluginToolsFactory) {
|
|
862
863
|
api.registerTool(pluginToolsFactory, { names: pluginToolNames });
|
|
863
864
|
}
|
|
865
|
+
// The supervisor must be (re)evaluated on EVERY register call, not just
|
|
866
|
+
// the first: the first full register can run BEFORE the installer has
|
|
867
|
+
// written connectorSupervisor='on' (the gateway hot-reloads the plugin
|
|
868
|
+
// the moment `plugins install --link` records it), and OpenClaw's
|
|
869
|
+
// in-process restart re-invokes register() down THIS early-return path.
|
|
870
|
+
// startConnectorSupervisor() is itself a singleton — repeat calls no-op.
|
|
871
|
+
maybeStartConnectorSupervisor();
|
|
864
872
|
return;
|
|
865
873
|
}
|
|
866
874
|
logger.info(TAG, 'Initializing paper trading plugin...');
|
|
@@ -1984,6 +1992,39 @@ const paperTradingPlugin = {
|
|
|
1984
1992
|
pluginToolNames = toolNames;
|
|
1985
1993
|
pluginInitialised = true;
|
|
1986
1994
|
logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
|
|
1995
|
+
maybeStartConnectorSupervisor();
|
|
1987
1996
|
},
|
|
1988
1997
|
};
|
|
1998
|
+
/** Plugin-supervised connector (frictionless onboarding): when plugin-config
|
|
1999
|
+
* says connectorSupervisor='on' (written by the npx installer on fresh
|
|
2000
|
+
* installs — NEVER defaulted on, prod runs the bridge under systemd and
|
|
2001
|
+
* would double-connect the relay room), the plugin spawns + restarts the
|
|
2002
|
+
* relay connector as a child of the gateway process. OpenClaw is the process
|
|
2003
|
+
* manager: no systemd, works in containers, dies with the gateway.
|
|
2004
|
+
* Kill-switch: RC_CONNECTOR_SUPERVISOR=off. Called from EVERY register()
|
|
2005
|
+
* path (config may appear between calls); the supervisor itself is a
|
|
2006
|
+
* singleton so repeat calls no-op. */
|
|
2007
|
+
function maybeStartConnectorSupervisor() {
|
|
2008
|
+
try {
|
|
2009
|
+
const supervisorMode = readPluginConfig().connectorSupervisor;
|
|
2010
|
+
if (process.env.RC_CONNECTOR_SUPERVISOR === 'off') {
|
|
2011
|
+
if (supervisorMode === 'on')
|
|
2012
|
+
logger.warn(TAG, 'connector supervisor disabled by RC_CONNECTOR_SUPERVISOR=off');
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (supervisorMode === 'off')
|
|
2016
|
+
return;
|
|
2017
|
+
// 'on' → explicit opt-in (the npx installer writes this).
|
|
2018
|
+
// undefined → auto-start ONLY when the connector is bundled inside this
|
|
2019
|
+
// plugin package (the npm-channel @reefclaw/openclaw-plugin distribution
|
|
2020
|
+
// is self-contained by design). Prod's linked plugin dir has no bundled
|
|
2021
|
+
// bridge and no flag → stays off; systemd keeps owning prod's bridge.
|
|
2022
|
+
if (supervisorMode === 'on' || hasBundledBridge()) {
|
|
2023
|
+
startConnectorSupervisor();
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
catch (err) {
|
|
2027
|
+
logger.warn(TAG, `connector supervisor init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
1989
2030
|
export default paperTradingPlugin;
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reefclaw
|
|
3
|
-
version: 0.0.
|
|
3
|
+
version: 0.0.3
|
|
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 npm:@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/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { installPlugin } from './plugin.js';
|
|
11
11
|
import { installBridge } from './bridge.js';
|
|
12
12
|
import { installSkill } from './skill.js';
|
|
13
|
-
import {
|
|
13
|
+
import { enableConnectorSupervisor } from './supervisor-config.js';
|
|
14
14
|
import { checkGateway } from './validate.js';
|
|
15
15
|
import { readConfig, writeConfig, mergeReefClawConfig, gatewayAuthDowngradeNeeded, openClawInstalled, openClawConfigPath, readGatewayPort, } from './openclaw.js';
|
|
16
16
|
import { run, which } from './exec.js';
|
|
@@ -99,7 +99,10 @@ async function main() {
|
|
|
99
99
|
// invalid config.
|
|
100
100
|
const skill = installSkill();
|
|
101
101
|
installBridge();
|
|
102
|
-
|
|
102
|
+
// The plugin (inside the gateway) supervises the connector — no systemd/
|
|
103
|
+
// launchd/Task-Scheduler, works in containers. The gateway restart below is
|
|
104
|
+
// therefore also what STARTS the connector.
|
|
105
|
+
const supervised = enableConnectorSupervisor();
|
|
103
106
|
if (plugin.registered)
|
|
104
107
|
restartGateway();
|
|
105
108
|
await checkGateway(readGatewayPort(merged));
|
|
@@ -109,8 +112,8 @@ async function main() {
|
|
|
109
112
|
if (!skill.installed) {
|
|
110
113
|
warn('The agent skill was placed but not registered — see the message above to finish it.');
|
|
111
114
|
}
|
|
112
|
-
if (!
|
|
113
|
-
warn('
|
|
115
|
+
if (!supervised) {
|
|
116
|
+
warn('Connector supervision was not enabled — see the message above to finish it.');
|
|
114
117
|
}
|
|
115
118
|
nextSteps();
|
|
116
119
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Enable the plugin-supervised connector for this install: write
|
|
2
|
+
// `connectorSupervisor: 'on'` into ~/.reefclaw/plugin-config.json. The plugin
|
|
3
|
+
// (running inside the OpenClaw gateway) then spawns + restarts the connector
|
|
4
|
+
// itself — OpenClaw is the process manager. No systemd, no launchd, no Task
|
|
5
|
+
// Scheduler; works anywhere the gateway runs, including containers.
|
|
6
|
+
//
|
|
7
|
+
// Merge-preserving: the file may already exist (re-runs, or the operator's
|
|
8
|
+
// dashboard wrote exchange credentials); unknown keys survive.
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { REEFCLAW_HOME } from './paths.js';
|
|
12
|
+
import { step, ok, warn, info } from './ui.js';
|
|
13
|
+
export function enableConnectorSupervisor() {
|
|
14
|
+
step('Handing connector supervision to OpenClaw');
|
|
15
|
+
const path = join(REEFCLAW_HOME, 'plugin-config.json');
|
|
16
|
+
try {
|
|
17
|
+
let cfg = {};
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
cfg = JSON.parse(readFileSync(path, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
cfg.connectorSupervisor = 'on';
|
|
22
|
+
mkdirSync(REEFCLAW_HOME, { recursive: true, mode: 0o700 });
|
|
23
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
24
|
+
try {
|
|
25
|
+
chmodSync(path, 0o600); // mode is ignored when the file already existed
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* best-effort on non-POSIX filesystems */
|
|
29
|
+
}
|
|
30
|
+
ok('OpenClaw will start and supervise the connector (no service manager needed)');
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
warn(`could not write ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
info(`Add { "connectorSupervisor": "on" } to it yourself, then restart OpenClaw.`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|