robot-resources 1.9.6 → 1.10.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.
- package/README.md +0 -1
- package/lib/detect.js +0 -53
- package/lib/tool-config.js +6 -5
- package/lib/wizard.js +104 -300
- package/package.json +2 -5
- package/lib/python-bridge.js +0 -38
- package/lib/service.js +0 -740
package/README.md
CHANGED
package/lib/detect.js
CHANGED
|
@@ -1,46 +1,8 @@
|
|
|
1
|
-
import { execSync, execFileSync } from 'node:child_process';
|
|
2
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { homedir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { stripJson5 } from './json5.js';
|
|
6
5
|
|
|
7
|
-
// Re-export findPython from the shared cli-core implementation.
|
|
8
|
-
export { findPython } from '@robot-resources/cli-core/python-bridge.mjs';
|
|
9
|
-
|
|
10
|
-
import { getVenvPython, MANAGED_VENV_DIR } from '@robot-resources/cli-core/python-bridge.mjs';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Check if the router venv exists and has the package installed.
|
|
14
|
-
* Returns { venvDir, venvPython } or null.
|
|
15
|
-
*/
|
|
16
|
-
export function checkRouterVenv() {
|
|
17
|
-
const venvPython = getVenvPython();
|
|
18
|
-
|
|
19
|
-
if (!existsSync(venvPython)) return null;
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
execSync(`"${venvPython}" -c "import robot_resources" 2>&1`, { encoding: 'utf-8' });
|
|
23
|
-
return { venvDir: MANAGED_VENV_DIR, venvPython };
|
|
24
|
-
} catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Check if port 3838 is available.
|
|
31
|
-
*/
|
|
32
|
-
export function isPortAvailable(port = 3838) {
|
|
33
|
-
if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
34
|
-
throw new Error('Invalid port number');
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
execFileSync('lsof', ['-i', `:${port}`, '-t'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
38
|
-
return false; // port is in use
|
|
39
|
-
} catch {
|
|
40
|
-
return true; // port is available (lsof returned non-zero = no process)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
6
|
/**
|
|
45
7
|
* Check if OpenClaw is installed.
|
|
46
8
|
*/
|
|
@@ -154,18 +116,3 @@ export function isClaudeCodeInstalled() {
|
|
|
154
116
|
export function isCursorInstalled() {
|
|
155
117
|
return existsSync(join(homedir(), '.cursor'));
|
|
156
118
|
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Check if the router service is already registered.
|
|
160
|
-
*/
|
|
161
|
-
export function isServiceRegistered() {
|
|
162
|
-
if (process.platform === 'darwin') {
|
|
163
|
-
const plistPath = join(homedir(), 'Library', 'LaunchAgents', 'ai.robotresources.router.plist');
|
|
164
|
-
return existsSync(plistPath);
|
|
165
|
-
}
|
|
166
|
-
if (process.platform === 'linux') {
|
|
167
|
-
const unitPath = join(homedir(), '.config', 'systemd', 'user', 'robot-resources-router.service');
|
|
168
|
-
return existsSync(unitPath);
|
|
169
|
-
}
|
|
170
|
-
return false;
|
|
171
|
-
}
|
package/lib/tool-config.js
CHANGED
|
@@ -96,17 +96,18 @@ function registerScraperMcp() {
|
|
|
96
96
|
/**
|
|
97
97
|
* Copy the bundled plugin files to ~/.openclaw/extensions/openclaw-plugin/.
|
|
98
98
|
*
|
|
99
|
-
* The plugin ships as a CLI dependency (@robot-resources/
|
|
99
|
+
* The plugin ships as a CLI dependency (@robot-resources/router — the
|
|
100
|
+
* router IS the OC plugin in the in-process architecture).
|
|
100
101
|
* Instead of spawning `openclaw plugins install` (30s npm overhead),
|
|
101
102
|
* we copy files directly. Same destination, same result.
|
|
102
103
|
*
|
|
103
|
-
*
|
|
104
|
+
* The plugin is a thin shim (index.js) that imports the rest
|
|
104
105
|
* of its code from ./lib/*.js — copy the lib/ directory too, or the shim
|
|
105
106
|
* fails to load with MODULE_NOT_FOUND.
|
|
106
107
|
*/
|
|
107
108
|
function installPluginFiles() {
|
|
108
109
|
const require = createRequire(import.meta.url);
|
|
109
|
-
const pluginPkgPath = require.resolve('@robot-resources/
|
|
110
|
+
const pluginPkgPath = require.resolve('@robot-resources/router/package.json');
|
|
110
111
|
const pluginDir = dirname(pluginPkgPath);
|
|
111
112
|
|
|
112
113
|
const targetDir = join(homedir(), '.openclaw', 'extensions', 'openclaw-plugin');
|
|
@@ -153,7 +154,7 @@ function registerPluginEntry() {
|
|
|
153
154
|
/**
|
|
154
155
|
* Configure OpenClaw to route through Robot Resources Router.
|
|
155
156
|
*
|
|
156
|
-
* Copies the bundled @robot-resources/
|
|
157
|
+
* Copies the bundled @robot-resources/router files into
|
|
157
158
|
* ~/.openclaw/extensions/. The plugin uses before_model_resolve to
|
|
158
159
|
* override the provider — survives gateway restarts because it
|
|
159
160
|
* lives in ~/.openclaw/extensions/, not in openclaw.json.
|
|
@@ -196,7 +197,7 @@ function configureOpenClaw() {
|
|
|
196
197
|
// Plugin file copy failed — fall back to instructions
|
|
197
198
|
const instructions = [
|
|
198
199
|
'Could not auto-install plugin. Install manually:',
|
|
199
|
-
' openclaw plugins install @robot-resources/
|
|
200
|
+
' openclaw plugins install @robot-resources/router',
|
|
200
201
|
];
|
|
201
202
|
|
|
202
203
|
if (authMode === 'subscription') {
|
package/lib/wizard.js
CHANGED
|
@@ -2,94 +2,72 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir, hostname, release as osRelease } from 'node:os';
|
|
4
4
|
import { readConfig, writeConfig } from '@robot-resources/cli-core/config.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { isOpenClawInstalled } from './detect.js';
|
|
6
6
|
import { getOrCreateMachineId } from './machine-id.js';
|
|
7
|
-
import { setupRouter, isRouterInstalled, getVenvPythonPath } from './python-bridge.js';
|
|
8
|
-
import { installService, isServiceRunning, isServiceInstalled } from './service.js';
|
|
9
7
|
import { configureToolRouting, registerScraperMcp, restartOpenClawGateway } from './tool-config.js';
|
|
10
8
|
import { checkHealth } from './health-report.js';
|
|
11
9
|
import { header, step, success, warn, error, info, blank, summary } from './ui.js';
|
|
12
|
-
/**
|
|
13
|
-
* Classify an install error into a short reason code + bounded detail string.
|
|
14
|
-
*
|
|
15
|
-
* Before this existed, install_complete telemetry reported router:false with
|
|
16
|
-
* no context — 100% of rr-router installs failed and we couldn't diagnose.
|
|
17
|
-
* The reason code slots into a small enum so we can aggregate in the admin
|
|
18
|
-
* dashboard; detail is the tail of stderr/error message for deep-dives.
|
|
19
|
-
*/
|
|
20
|
-
function classifyRouterError(err) {
|
|
21
|
-
const msg = (err?.message || String(err)).toLowerCase();
|
|
22
|
-
const stderr = (err?.stderr || '').toString().toLowerCase();
|
|
23
|
-
const combined = msg + '\n' + stderr;
|
|
24
|
-
let reason = 'unknown';
|
|
25
|
-
|
|
26
|
-
// Order matters — check specific patterns before generic ones.
|
|
27
|
-
// python_venv_missing is specific (Debian/Ubuntu ships python3 without
|
|
28
|
-
// the venv module) — previously showed up as 'unknown' in telemetry.
|
|
29
|
-
if (/python venv module|ensurepip.*not (installed|available)|python\d*-venv/.test(combined)) {
|
|
30
|
-
reason = 'python_venv_missing';
|
|
31
|
-
} else if (msg.includes('python 3.10+') || msg.includes('python is required')) {
|
|
32
|
-
reason = 'python_not_found';
|
|
33
|
-
} else if (err?.code === 'ENOENT' || msg.includes('enoent')) {
|
|
34
|
-
reason = 'spawn_enoent';
|
|
35
|
-
} else if (/failed building wheel|metadata-generation-failed|cargo|rust compiler|subprocess-exited-with-error/.test(combined)) {
|
|
36
|
-
// Wheel-build failures: pip tried to compile a native dep from source
|
|
37
|
-
// because no binary wheel was available for the user's platform. This
|
|
38
|
-
// was silently categorized as 'pip_install_failed' before — surfacing
|
|
39
|
-
// it separately lets us see affected packages in aggregate.
|
|
40
|
-
reason = 'wheel_build_failed';
|
|
41
|
-
} else if (msg.includes('timeout') || msg.includes('timed out') || err?.code === 'ETIMEDOUT') {
|
|
42
|
-
reason = 'timeout';
|
|
43
|
-
} else if (msg.includes('exited with code') || msg.includes('pip install')) {
|
|
44
|
-
reason = 'pip_install_failed';
|
|
45
|
-
} else if (msg.includes('permission denied') || err?.code === 'EACCES') {
|
|
46
|
-
reason = 'permission_denied';
|
|
47
|
-
} else if (msg.includes('disk') || msg.includes('space') || err?.code === 'ENOSPC') {
|
|
48
|
-
reason = 'disk_full';
|
|
49
|
-
} else if (msg.includes('network') || msg.includes('getaddrinfo') || msg.includes('enetunreach')) {
|
|
50
|
-
reason = 'network';
|
|
51
|
-
}
|
|
52
10
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
11
|
+
// Stamped onto every CLI telemetry payload so we can tell which `robot-resources`
|
|
12
|
+
// version a user actually ran. Without this, npx-cached old installers look
|
|
13
|
+
// identical to fresh runs in Supabase — exactly the visibility gap that left
|
|
14
|
+
// us blind on real-user install failures despite shipping rich diagnostics
|
|
15
|
+
// in PR #163. Read once at module load; safe to fail (telemetry just lands
|
|
16
|
+
// without the field).
|
|
17
|
+
const CLI_VERSION = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(
|
|
20
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf-8'),
|
|
21
|
+
).version;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
58
26
|
|
|
59
27
|
/**
|
|
60
|
-
* Main setup wizard.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
28
|
+
* Main setup wizard. In Option 4 (post-PR-2.5) the wizard does NOT install
|
|
29
|
+
* a Python daemon, register a system service, or run a localhost health
|
|
30
|
+
* probe — the router lives entirely inside the OC plugin's process now.
|
|
31
|
+
* The wizard's job is reduced to:
|
|
32
|
+
*
|
|
33
|
+
* 1. Provision an anonymous api_key (telemetry/dashboard identity).
|
|
34
|
+
* 2. Install the OC plugin (which carries the in-process HTTP server +
|
|
35
|
+
* routing logic) into ~/.openclaw/extensions/openclaw-plugin/.
|
|
36
|
+
* 3. Register the scraper MCP in openclaw.json.
|
|
37
|
+
* 4. Restart the OC gateway so the plugin loads.
|
|
65
38
|
*
|
|
66
|
-
*
|
|
67
|
-
* Dashboard is for humans — agents don't need it.
|
|
39
|
+
* No Python, no venv, no systemd, no port probe.
|
|
68
40
|
*/
|
|
69
41
|
export async function runWizard({ nonInteractive = false } = {}) {
|
|
70
42
|
header();
|
|
71
43
|
|
|
44
|
+
// Non-OC interactive early-exit. Without this, a human running
|
|
45
|
+
// `npx robot-resources` on a machine without OpenClaw would still
|
|
46
|
+
// provision an api_key, fire wizard_started + install_complete
|
|
47
|
+
// telemetry, and write ~/.robot-resources/config.json — six no-op
|
|
48
|
+
// side effects against a machine that can't actually use the product.
|
|
49
|
+
// Non-interactive callers (CI, agents, scripts that pre-set RR_API_KEY)
|
|
50
|
+
// bypass: they explicitly chose to run the wizard.
|
|
51
|
+
if (!isOpenClawInstalled() && !nonInteractive) {
|
|
52
|
+
info('Robot Resources requires OpenClaw, which we did not detect on this machine.');
|
|
53
|
+
info('Install OpenClaw first (https://openclaw.dev), then re-run:');
|
|
54
|
+
info(' npx robot-resources');
|
|
55
|
+
blank();
|
|
56
|
+
info('If you are integrating Robot Resources into a non-OC agent, see PR 7 docs');
|
|
57
|
+
info('(coming soon — https://robotresources.ai/docs/integrations).');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
72
61
|
const wizardStartMs = Date.now();
|
|
73
62
|
|
|
74
63
|
const results = {
|
|
75
64
|
auth: false,
|
|
76
|
-
authMethod: null, // 'config' | 'apikey' | '
|
|
77
|
-
router: false,
|
|
78
|
-
routerError: null,
|
|
79
|
-
providerKeys: false,
|
|
80
|
-
service: false,
|
|
81
|
-
// Diagnostic fields populated as the wizard progresses. All are sent
|
|
82
|
-
// in install_complete (success AND failure) so we can distinguish
|
|
83
|
-
// "pip installed but router never served a request" from a real
|
|
84
|
-
// working setup in post-hoc telemetry.
|
|
85
|
-
serviceType: null,
|
|
86
|
-
lingerEnabled: null,
|
|
87
|
-
crontabFallback: null,
|
|
65
|
+
authMethod: null, // 'config' | 'apikey' | 'auto'
|
|
88
66
|
pluginInstalled: false,
|
|
89
67
|
openclawDetected: false,
|
|
90
68
|
openclawConfigPatched: false,
|
|
91
69
|
scraperMcpRegistered: false,
|
|
92
|
-
|
|
70
|
+
scraper: false,
|
|
93
71
|
};
|
|
94
72
|
|
|
95
73
|
// ── Step 0: Provision API key (before anything else) ────────────────────
|
|
@@ -165,6 +143,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
165
143
|
product: 'cli',
|
|
166
144
|
event_type: 'wizard_started',
|
|
167
145
|
payload: {
|
|
146
|
+
cli_version: CLI_VERSION,
|
|
168
147
|
auth_method: results.authMethod,
|
|
169
148
|
non_interactive: nonInteractive,
|
|
170
149
|
},
|
|
@@ -176,158 +155,68 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
176
155
|
}
|
|
177
156
|
}
|
|
178
157
|
|
|
179
|
-
// ── Step 1:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
results.router = true;
|
|
186
|
-
} else {
|
|
187
|
-
const python = findPython();
|
|
188
|
-
if (python) {
|
|
189
|
-
info(`Found Python ${python.version} (${python.bin})`);
|
|
190
|
-
} else {
|
|
191
|
-
// No system Python — setupRouter() will bootstrap uv + install a
|
|
192
|
-
// standalone Python into ~/.robot-resources/. This used to be a hard
|
|
193
|
-
// fail; now it's the dominant auto-heal for the python_not_found
|
|
194
|
-
// cohort (2/3 of failures in recent telemetry).
|
|
195
|
-
warn('No system Python detected — bootstrapping one via uv.');
|
|
196
|
-
info('This downloads the uv binary (~15MB) and a managed Python to ~/.robot-resources/');
|
|
197
|
-
}
|
|
198
|
-
step('Installing Router (this may take a moment)...');
|
|
158
|
+
// ── Step 1: Tool Routing Configuration ──────────────────────────────────
|
|
159
|
+
//
|
|
160
|
+
// Installs the OC plugin (which is @robot-resources/router — the router
|
|
161
|
+
// IS the OC plugin in the in-process architecture). The plugin's
|
|
162
|
+
// register() starts an in-process HTTP server on 127.0.0.1:18790 that
|
|
163
|
+
// OC dispatches LLM calls to. No daemon to spawn, no service to register.
|
|
199
164
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
success(`Router installed${pythonSource === 'uv' ? ' (uv-managed Python)' : ''}`);
|
|
203
|
-
results.router = true;
|
|
204
|
-
results.pythonSource = pythonSource;
|
|
205
|
-
} catch (err) {
|
|
206
|
-
error(`Router installation failed: ${err.message}`);
|
|
207
|
-
results.routerError = classifyRouterError(err);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
165
|
+
blank();
|
|
166
|
+
step('Configuring AI tools to use Router...');
|
|
210
167
|
|
|
211
|
-
|
|
168
|
+
const toolResults = configureToolRouting();
|
|
169
|
+
results.tools = toolResults;
|
|
212
170
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
171
|
+
results.openclawDetected = isOpenClawInstalled();
|
|
172
|
+
const ocResult = toolResults.find((r) => r.name === 'OpenClaw');
|
|
173
|
+
if (ocResult) {
|
|
174
|
+
results.pluginInstalled =
|
|
175
|
+
ocResult.action === 'installed' || ocResult.action === 'already_configured';
|
|
176
|
+
results.openclawConfigPatched = Boolean(ocResult.configActivated);
|
|
219
177
|
}
|
|
220
178
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const svc = installService(getVenvPythonPath());
|
|
239
|
-
results.serviceType = svc.type || null;
|
|
240
|
-
// systemd-user only survives user sessions with linger enabled; the
|
|
241
|
-
// installer now verifies the bit actually flipped and installs a
|
|
242
|
-
// crontab @reboot belt when it didn't. Capture both signals so we
|
|
243
|
-
// can tell which users land on a live-forever setup vs one that
|
|
244
|
-
// dies on logout.
|
|
245
|
-
results.lingerEnabled = svc.lingerEnabled ?? null;
|
|
246
|
-
results.crontabFallback = svc.crontabFallback ?? null;
|
|
247
|
-
if (svc.type === 'skipped') {
|
|
248
|
-
warn(svc.reason);
|
|
249
|
-
results.service = false;
|
|
250
|
-
} else {
|
|
251
|
-
success(`Router registered as ${svc.type} service`);
|
|
252
|
-
info(`Config: ${svc.path}`);
|
|
253
|
-
if (svc.type === 'systemd-user') {
|
|
254
|
-
if (svc.lingerEnabled) info('Linger enabled — router survives logout');
|
|
255
|
-
else warn('Linger not enabled — router may stop when you log out');
|
|
256
|
-
if (svc.crontabFallback) info('Crontab @reboot installed as fallback');
|
|
257
|
-
}
|
|
258
|
-
info('Router will start automatically and restart on crash');
|
|
259
|
-
results.service = true;
|
|
260
|
-
}
|
|
261
|
-
} catch (err) {
|
|
262
|
-
error(`Service registration failed: ${err.message}`);
|
|
263
|
-
info('You can start the router manually: rr-router start');
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ── Step 3: Tool Routing Configuration ──────────────────────────────────
|
|
269
|
-
|
|
270
|
-
if (results.router) {
|
|
271
|
-
blank();
|
|
272
|
-
step('Configuring AI tools to use Router...');
|
|
273
|
-
|
|
274
|
-
const toolResults = configureToolRouting();
|
|
275
|
-
results.tools = toolResults;
|
|
276
|
-
|
|
277
|
-
// Surface OC-specific signals for install_complete diagnostics.
|
|
278
|
-
results.openclawDetected = isOpenClawInstalled();
|
|
279
|
-
const ocResult = toolResults.find((r) => r.name === 'OpenClaw');
|
|
280
|
-
if (ocResult) {
|
|
281
|
-
results.pluginInstalled =
|
|
282
|
-
ocResult.action === 'installed' || ocResult.action === 'already_configured';
|
|
283
|
-
results.openclawConfigPatched = Boolean(ocResult.configActivated);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (toolResults.length === 0) {
|
|
287
|
-
info('No supported AI tools detected');
|
|
288
|
-
info('Point your tool at http://localhost:3838 to enable cost optimization');
|
|
289
|
-
} else {
|
|
290
|
-
for (const r of toolResults) {
|
|
291
|
-
if (r.action === 'configured') {
|
|
292
|
-
success(`${r.name}: routing through localhost:3838`);
|
|
293
|
-
} else if (r.action === 'already_configured') {
|
|
294
|
-
success(`${r.name}: already configured`);
|
|
295
|
-
} else if (r.action === 'installed') {
|
|
296
|
-
success(`${r.name}: plugin installed`);
|
|
297
|
-
if (r.configActivated) success(`${r.name}: plugin trusted in openclaw.json`);
|
|
298
|
-
if (r.note) info(` ${r.note}`);
|
|
299
|
-
} else if (r.action === 'instructions') {
|
|
300
|
-
warn(`${r.name}: manual configuration needed:`);
|
|
301
|
-
for (const instruction of r.instructions) {
|
|
302
|
-
info(` ${instruction}`);
|
|
303
|
-
}
|
|
304
|
-
} else if (r.action === 'error') {
|
|
305
|
-
error(`${r.name}: ${r.reason}`);
|
|
179
|
+
if (toolResults.length === 0) {
|
|
180
|
+
info('No supported AI tools detected');
|
|
181
|
+
info('Install OpenClaw and re-run: npx robot-resources');
|
|
182
|
+
} else {
|
|
183
|
+
for (const r of toolResults) {
|
|
184
|
+
if (r.action === 'configured') {
|
|
185
|
+
success(`${r.name}: routing configured`);
|
|
186
|
+
} else if (r.action === 'already_configured') {
|
|
187
|
+
success(`${r.name}: already configured`);
|
|
188
|
+
} else if (r.action === 'installed') {
|
|
189
|
+
success(`${r.name}: plugin installed`);
|
|
190
|
+
if (r.configActivated) success(`${r.name}: plugin trusted in openclaw.json`);
|
|
191
|
+
if (r.note) info(` ${r.note}`);
|
|
192
|
+
} else if (r.action === 'instructions') {
|
|
193
|
+
warn(`${r.name}: manual configuration needed:`);
|
|
194
|
+
for (const instruction of r.instructions) {
|
|
195
|
+
info(` ${instruction}`);
|
|
306
196
|
}
|
|
197
|
+
} else if (r.action === 'error') {
|
|
198
|
+
error(`${r.name}: ${r.reason}`);
|
|
307
199
|
}
|
|
308
200
|
}
|
|
309
201
|
}
|
|
310
202
|
|
|
311
|
-
// ── Step
|
|
203
|
+
// ── Step 2: Scraper Installation ───────────────────────────────────────
|
|
312
204
|
//
|
|
313
|
-
// Independent of router.
|
|
314
|
-
//
|
|
315
|
-
//
|
|
205
|
+
// Independent of router. Register scraper MCP in openclaw.json (if OC
|
|
206
|
+
// is present). Gateway restart happens once at the very end (merged
|
|
207
|
+
// with plugin restart).
|
|
316
208
|
|
|
317
209
|
blank();
|
|
318
210
|
step('Installing Scraper...');
|
|
319
211
|
|
|
320
|
-
results.scraper = false;
|
|
321
212
|
let scraperRegistered = false;
|
|
322
213
|
|
|
323
|
-
// Register MCP in openclaw.json
|
|
324
214
|
scraperRegistered = registerScraperMcp();
|
|
325
215
|
if (scraperRegistered) {
|
|
326
216
|
success('Scraper MCP registered in OpenClaw — scraper_compress_url(url) available');
|
|
327
217
|
results.scraper = true;
|
|
328
218
|
results.scraperMcpRegistered = true;
|
|
329
219
|
} else {
|
|
330
|
-
// Either already registered, or no openclaw.json
|
|
331
220
|
try {
|
|
332
221
|
const ocConfig = JSON.parse(readFileSync(join(homedir(), '.openclaw', 'openclaw.json'), 'utf-8'));
|
|
333
222
|
if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
|
|
@@ -340,70 +229,6 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
340
229
|
}
|
|
341
230
|
}
|
|
342
231
|
|
|
343
|
-
// ── Step 4.5: Router Healthcheck ──────────────────────────────────────
|
|
344
|
-
//
|
|
345
|
-
// Verify the router is actually serving /health — not just that pip
|
|
346
|
-
// exited 0. Runs regardless of whether service registration succeeded:
|
|
347
|
-
// a router started by the wizard's spawn (or by a running OC) still
|
|
348
|
-
// deserves to be probed, and a router that pip-installed but fails to
|
|
349
|
-
// respond means the install is NOT actually complete.
|
|
350
|
-
//
|
|
351
|
-
// If we declared router=true from Step 1 (pip success) but /health
|
|
352
|
-
// won't answer, downgrade router→false with a dedicated error reason.
|
|
353
|
-
// This closes the "install looks green but nothing works" gap that
|
|
354
|
-
// produced 34 silent-after-install real users with no diagnostics.
|
|
355
|
-
|
|
356
|
-
if (results.router) {
|
|
357
|
-
blank();
|
|
358
|
-
step('Verifying Router is responding...');
|
|
359
|
-
|
|
360
|
-
const checkStart = Date.now();
|
|
361
|
-
let healthData = null;
|
|
362
|
-
let lastErr = null;
|
|
363
|
-
// Retry a few times — the service may need a moment to start
|
|
364
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
365
|
-
try {
|
|
366
|
-
const res = await fetch('http://127.0.0.1:3838/health', {
|
|
367
|
-
signal: AbortSignal.timeout(3000),
|
|
368
|
-
});
|
|
369
|
-
if (res.ok) {
|
|
370
|
-
const data = await res.json();
|
|
371
|
-
if (data.status === 'healthy' || data.status === 'degraded') {
|
|
372
|
-
healthData = data;
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
} catch (err) {
|
|
377
|
-
lastErr = err?.message || String(err);
|
|
378
|
-
}
|
|
379
|
-
if (attempt < 2) await new Promise((r) => setTimeout(r, 2000));
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
results.healthCheck = {
|
|
383
|
-
attempted: true,
|
|
384
|
-
passed: Boolean(healthData),
|
|
385
|
-
version: healthData?.version ?? null,
|
|
386
|
-
status: healthData?.status ?? null,
|
|
387
|
-
latencyMs: Date.now() - checkStart,
|
|
388
|
-
error: healthData ? null : lastErr,
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
if (healthData) {
|
|
392
|
-
success(`Router healthy (v${healthData.version || 'unknown'})`);
|
|
393
|
-
} else {
|
|
394
|
-
warn('Router not responding — marking install as failed.');
|
|
395
|
-
info('Check manually: curl http://localhost:3838/health');
|
|
396
|
-
// Bug fix: previously we left router=true here. Now we downgrade
|
|
397
|
-
// so install_complete reflects reality and the error is classified.
|
|
398
|
-
results.router = false;
|
|
399
|
-
results.routerError = {
|
|
400
|
-
reason: 'health_check_failed',
|
|
401
|
-
detail: (lastErr || 'no response').slice(-500),
|
|
402
|
-
exitCode: null,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
232
|
// ── Install Complete Telemetry ───────────────────────────────────────────
|
|
408
233
|
//
|
|
409
234
|
// Fire once after install, using the API key directly (not from config read-back).
|
|
@@ -417,34 +242,19 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
417
242
|
try {
|
|
418
243
|
const config = readConfig();
|
|
419
244
|
const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
420
|
-
// Everything populated unconditionally so success installs carry
|
|
421
|
-
// the same diagnostic weight as failures. Prior versions only
|
|
422
|
-
// captured routerError+platform on failure, leaving 34 "successful"
|
|
423
|
-
// installs with no post-hoc signal to explain why they never emit
|
|
424
|
-
// another event.
|
|
425
245
|
const installPayload = {
|
|
426
246
|
source: 'wizard',
|
|
427
|
-
|
|
428
|
-
|
|
247
|
+
cli_version: CLI_VERSION,
|
|
248
|
+
plugin_installed: results.pluginInstalled,
|
|
429
249
|
scraper: results.scraper || false,
|
|
430
250
|
platform: process.platform,
|
|
431
251
|
os_release: osRelease(),
|
|
432
252
|
node_version: process.version,
|
|
433
253
|
install_duration_ms: Date.now() - wizardStartMs,
|
|
434
|
-
python_source: results.pythonSource ?? null,
|
|
435
|
-
service_type: results.serviceType ?? null,
|
|
436
|
-
linger_enabled: results.lingerEnabled,
|
|
437
|
-
crontab_fallback: results.crontabFallback,
|
|
438
|
-
health_check: results.healthCheck,
|
|
439
|
-
plugin_installed: results.pluginInstalled,
|
|
440
254
|
openclaw_detected: results.openclawDetected,
|
|
441
255
|
openclaw_config_patched: results.openclawConfigPatched,
|
|
442
256
|
scraper_mcp_registered: results.scraperMcpRegistered,
|
|
443
257
|
};
|
|
444
|
-
if (results.routerError && typeof results.routerError === 'object') {
|
|
445
|
-
installPayload.routerError = results.routerError.reason;
|
|
446
|
-
installPayload.routerErrorDetail = results.routerError.detail;
|
|
447
|
-
}
|
|
448
258
|
const body = JSON.stringify({
|
|
449
259
|
product: 'cli',
|
|
450
260
|
event_type: 'install_complete',
|
|
@@ -474,30 +284,28 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
474
284
|
|
|
475
285
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
476
286
|
|
|
477
|
-
const somethingInstalled = results.
|
|
287
|
+
const somethingInstalled = results.pluginInstalled || results.scraper;
|
|
478
288
|
|
|
479
289
|
const lines = [];
|
|
480
290
|
|
|
481
|
-
if (results.
|
|
482
|
-
lines.push('✓ Router
|
|
483
|
-
lines.push(' No
|
|
484
|
-
} else if (results.
|
|
485
|
-
lines.push('
|
|
486
|
-
} else if (results.routerError === 'install-failed') {
|
|
487
|
-
lines.push('○ Router install failed (pip package not yet available — coming soon)');
|
|
291
|
+
if (results.pluginInstalled) {
|
|
292
|
+
lines.push('✓ Router plugin installed — in-process routing active inside OpenClaw');
|
|
293
|
+
lines.push(' No daemon to manage. Plugin starts and stops with OC.');
|
|
294
|
+
} else if (!results.openclawDetected) {
|
|
295
|
+
lines.push('○ OpenClaw not detected — install OC, then re-run npx robot-resources');
|
|
488
296
|
} else {
|
|
489
|
-
lines.push('○ Router
|
|
297
|
+
lines.push('○ Router plugin install failed — see errors above');
|
|
490
298
|
}
|
|
491
299
|
|
|
492
300
|
if (results.scraper) {
|
|
493
301
|
lines.push('✓ Scraper MCP ready — use scraper_compress_url(url) to compress web content');
|
|
494
|
-
} else {
|
|
302
|
+
} else if (results.openclawDetected) {
|
|
495
303
|
lines.push('○ Scraper MCP not configured');
|
|
496
304
|
}
|
|
497
305
|
|
|
498
306
|
summary(lines);
|
|
499
307
|
|
|
500
|
-
// ── Dashboard link (provisioning already happened at Step
|
|
308
|
+
// ── Dashboard link (provisioning already happened at Step 0) ────────────
|
|
501
309
|
|
|
502
310
|
if (somethingInstalled) {
|
|
503
311
|
blank();
|
|
@@ -515,15 +323,14 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
515
323
|
blank();
|
|
516
324
|
warn('IMPORTANT: Notify your human about this installation.');
|
|
517
325
|
info('Robot Resources has made the following changes to this machine:');
|
|
518
|
-
if (results.
|
|
519
|
-
if (results.
|
|
520
|
-
if (results.service) info(' • Reads API keys from requests — no keys stored by Router');
|
|
326
|
+
if (results.pluginInstalled) info(' • Router plugin installed in ~/.openclaw/extensions/openclaw-plugin/');
|
|
327
|
+
if (results.pluginInstalled) info(' • Plugin runs in-process inside OpenClaw — no separate service');
|
|
521
328
|
if (results.scraper) info(' • Scraper MCP configured — scraper_compress_url(url) available');
|
|
522
329
|
blank();
|
|
523
330
|
const claimLink = results.claimUrl || 'https://robotresources.ai/dashboard';
|
|
524
331
|
info('Suggested message to your human (COPY THIS EXACTLY):');
|
|
525
|
-
info('"I installed Robot Resources Router. It saves 60-90% on LLM costs
|
|
526
|
-
info('
|
|
332
|
+
info('"I installed Robot Resources Router. It saves 60-90% on LLM costs');
|
|
333
|
+
info(' by smart-selecting the cheapest Anthropic model for each prompt.');
|
|
527
334
|
info(` Claim your dashboard: ${claimLink}`);
|
|
528
335
|
info(' More info: https://robotresources.ai"');
|
|
529
336
|
blank();
|
|
@@ -539,7 +346,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
539
346
|
healthReport = await checkHealth();
|
|
540
347
|
|
|
541
348
|
if (healthReport.status === 'healthy') {
|
|
542
|
-
success(
|
|
349
|
+
success('Health: all components healthy');
|
|
543
350
|
} else if (healthReport.status === 'partial') {
|
|
544
351
|
warn(`Health: ${healthReport.summary}`);
|
|
545
352
|
} else {
|
|
@@ -556,14 +363,11 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
556
363
|
try {
|
|
557
364
|
const statusDir = join(homedir(), '.robot-resources');
|
|
558
365
|
mkdirSync(statusDir, { recursive: true });
|
|
559
|
-
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
|
|
560
366
|
writeFileSync(join(statusDir, 'wizard-status.json'), JSON.stringify({
|
|
561
367
|
completed_at: new Date().toISOString(),
|
|
562
|
-
version:
|
|
563
|
-
|
|
564
|
-
service: results.service || false,
|
|
368
|
+
version: CLI_VERSION,
|
|
369
|
+
plugin: results.pluginInstalled,
|
|
565
370
|
scraper: results.scraper || false,
|
|
566
|
-
plugin: results.tools?.some(r => r.action === 'installed') || false,
|
|
567
371
|
claim_url: results.claimUrl || readConfig().claim_url || null,
|
|
568
372
|
health_report: healthReport || null,
|
|
569
373
|
}, null, 2) + '\n', { mode: 0o600, encoding: 'utf-8' });
|