robot-resources 1.9.7 → 1.10.3
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/health-report.js +3 -3
- package/lib/tool-config.js +14 -13
- package/lib/wizard.js +90 -303
- package/package.json +2 -5
- package/lib/python-bridge.js +0 -38
- package/lib/service.js +0 -740
package/lib/wizard.js
CHANGED
|
@@ -2,20 +2,18 @@ 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
10
|
|
|
13
11
|
// Stamped onto every CLI telemetry payload so we can tell which `robot-resources`
|
|
14
12
|
// version a user actually ran. Without this, npx-cached old installers look
|
|
15
|
-
// identical to fresh runs in Supabase —
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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).
|
|
19
17
|
const CLI_VERSION = (() => {
|
|
20
18
|
try {
|
|
21
19
|
return JSON.parse(
|
|
@@ -25,87 +23,51 @@ const CLI_VERSION = (() => {
|
|
|
25
23
|
return null;
|
|
26
24
|
}
|
|
27
25
|
})();
|
|
28
|
-
/**
|
|
29
|
-
* Classify an install error into a short reason code + bounded detail string.
|
|
30
|
-
*
|
|
31
|
-
* Before this existed, install_complete telemetry reported router:false with
|
|
32
|
-
* no context — 100% of rr-router installs failed and we couldn't diagnose.
|
|
33
|
-
* The reason code slots into a small enum so we can aggregate in the admin
|
|
34
|
-
* dashboard; detail is the tail of stderr/error message for deep-dives.
|
|
35
|
-
*/
|
|
36
|
-
function classifyRouterError(err) {
|
|
37
|
-
const msg = (err?.message || String(err)).toLowerCase();
|
|
38
|
-
const stderr = (err?.stderr || '').toString().toLowerCase();
|
|
39
|
-
const combined = msg + '\n' + stderr;
|
|
40
|
-
let reason = 'unknown';
|
|
41
|
-
|
|
42
|
-
// Order matters — check specific patterns before generic ones.
|
|
43
|
-
// python_venv_missing is specific (Debian/Ubuntu ships python3 without
|
|
44
|
-
// the venv module) — previously showed up as 'unknown' in telemetry.
|
|
45
|
-
if (/python venv module|ensurepip.*not (installed|available)|python\d*-venv/.test(combined)) {
|
|
46
|
-
reason = 'python_venv_missing';
|
|
47
|
-
} else if (msg.includes('python 3.10+') || msg.includes('python is required')) {
|
|
48
|
-
reason = 'python_not_found';
|
|
49
|
-
} else if (err?.code === 'ENOENT' || msg.includes('enoent')) {
|
|
50
|
-
reason = 'spawn_enoent';
|
|
51
|
-
} else if (/failed building wheel|metadata-generation-failed|cargo|rust compiler|subprocess-exited-with-error/.test(combined)) {
|
|
52
|
-
// Wheel-build failures: pip tried to compile a native dep from source
|
|
53
|
-
// because no binary wheel was available for the user's platform. This
|
|
54
|
-
// was silently categorized as 'pip_install_failed' before — surfacing
|
|
55
|
-
// it separately lets us see affected packages in aggregate.
|
|
56
|
-
reason = 'wheel_build_failed';
|
|
57
|
-
} else if (msg.includes('timeout') || msg.includes('timed out') || err?.code === 'ETIMEDOUT') {
|
|
58
|
-
reason = 'timeout';
|
|
59
|
-
} else if (msg.includes('exited with code') || msg.includes('pip install')) {
|
|
60
|
-
reason = 'pip_install_failed';
|
|
61
|
-
} else if (msg.includes('permission denied') || err?.code === 'EACCES') {
|
|
62
|
-
reason = 'permission_denied';
|
|
63
|
-
} else if (msg.includes('disk') || msg.includes('space') || err?.code === 'ENOSPC') {
|
|
64
|
-
reason = 'disk_full';
|
|
65
|
-
} else if (msg.includes('network') || msg.includes('getaddrinfo') || msg.includes('enetunreach')) {
|
|
66
|
-
reason = 'network';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const rawDetail = err?.stderr?.trim?.() || err?.message || String(err);
|
|
70
|
-
const detail = rawDetail.slice(-500);
|
|
71
|
-
|
|
72
|
-
return { reason, detail, exitCode: err?.exitCode ?? null };
|
|
73
|
-
}
|
|
74
26
|
|
|
75
27
|
/**
|
|
76
|
-
* Main setup wizard.
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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/robot-resources-router/.
|
|
36
|
+
* 3. Register the scraper MCP in openclaw.json.
|
|
37
|
+
* 4. Restart the OC gateway so the plugin loads.
|
|
81
38
|
*
|
|
82
|
-
*
|
|
83
|
-
* Dashboard is for humans — agents don't need it.
|
|
39
|
+
* No Python, no venv, no systemd, no port probe.
|
|
84
40
|
*/
|
|
85
41
|
export async function runWizard({ nonInteractive = false } = {}) {
|
|
86
42
|
header();
|
|
87
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
|
+
|
|
88
61
|
const wizardStartMs = Date.now();
|
|
89
62
|
|
|
90
63
|
const results = {
|
|
91
64
|
auth: false,
|
|
92
|
-
authMethod: null, // 'config' | 'apikey' | '
|
|
93
|
-
router: false,
|
|
94
|
-
routerError: null,
|
|
95
|
-
providerKeys: false,
|
|
96
|
-
service: false,
|
|
97
|
-
// Diagnostic fields populated as the wizard progresses. All are sent
|
|
98
|
-
// in install_complete (success AND failure) so we can distinguish
|
|
99
|
-
// "pip installed but router never served a request" from a real
|
|
100
|
-
// working setup in post-hoc telemetry.
|
|
101
|
-
serviceType: null,
|
|
102
|
-
lingerEnabled: null,
|
|
103
|
-
crontabFallback: null,
|
|
65
|
+
authMethod: null, // 'config' | 'apikey' | 'auto'
|
|
104
66
|
pluginInstalled: false,
|
|
105
67
|
openclawDetected: false,
|
|
106
68
|
openclawConfigPatched: false,
|
|
107
69
|
scraperMcpRegistered: false,
|
|
108
|
-
|
|
70
|
+
scraper: false,
|
|
109
71
|
};
|
|
110
72
|
|
|
111
73
|
// ── Step 0: Provision API key (before anything else) ────────────────────
|
|
@@ -193,158 +155,68 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
193
155
|
}
|
|
194
156
|
}
|
|
195
157
|
|
|
196
|
-
// ── Step 1:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
results.router = true;
|
|
203
|
-
} else {
|
|
204
|
-
const python = findPython();
|
|
205
|
-
if (python) {
|
|
206
|
-
info(`Found Python ${python.version} (${python.bin})`);
|
|
207
|
-
} else {
|
|
208
|
-
// No system Python — setupRouter() will bootstrap uv + install a
|
|
209
|
-
// standalone Python into ~/.robot-resources/. This used to be a hard
|
|
210
|
-
// fail; now it's the dominant auto-heal for the python_not_found
|
|
211
|
-
// cohort (2/3 of failures in recent telemetry).
|
|
212
|
-
warn('No system Python detected — bootstrapping one via uv.');
|
|
213
|
-
info('This downloads the uv binary (~15MB) and a managed Python to ~/.robot-resources/');
|
|
214
|
-
}
|
|
215
|
-
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.
|
|
216
164
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
success(`Router installed${pythonSource === 'uv' ? ' (uv-managed Python)' : ''}`);
|
|
220
|
-
results.router = true;
|
|
221
|
-
results.pythonSource = pythonSource;
|
|
222
|
-
} catch (err) {
|
|
223
|
-
error(`Router installation failed: ${err.message}`);
|
|
224
|
-
results.routerError = classifyRouterError(err);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
165
|
+
blank();
|
|
166
|
+
step('Configuring AI tools to use Router...');
|
|
227
167
|
|
|
228
|
-
|
|
168
|
+
const toolResults = configureToolRouting();
|
|
169
|
+
results.tools = toolResults;
|
|
229
170
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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);
|
|
236
177
|
}
|
|
237
178
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const svc = installService(getVenvPythonPath());
|
|
256
|
-
results.serviceType = svc.type || null;
|
|
257
|
-
// systemd-user only survives user sessions with linger enabled; the
|
|
258
|
-
// installer now verifies the bit actually flipped and installs a
|
|
259
|
-
// crontab @reboot belt when it didn't. Capture both signals so we
|
|
260
|
-
// can tell which users land on a live-forever setup vs one that
|
|
261
|
-
// dies on logout.
|
|
262
|
-
results.lingerEnabled = svc.lingerEnabled ?? null;
|
|
263
|
-
results.crontabFallback = svc.crontabFallback ?? null;
|
|
264
|
-
if (svc.type === 'skipped') {
|
|
265
|
-
warn(svc.reason);
|
|
266
|
-
results.service = false;
|
|
267
|
-
} else {
|
|
268
|
-
success(`Router registered as ${svc.type} service`);
|
|
269
|
-
info(`Config: ${svc.path}`);
|
|
270
|
-
if (svc.type === 'systemd-user') {
|
|
271
|
-
if (svc.lingerEnabled) info('Linger enabled — router survives logout');
|
|
272
|
-
else warn('Linger not enabled — router may stop when you log out');
|
|
273
|
-
if (svc.crontabFallback) info('Crontab @reboot installed as fallback');
|
|
274
|
-
}
|
|
275
|
-
info('Router will start automatically and restart on crash');
|
|
276
|
-
results.service = true;
|
|
277
|
-
}
|
|
278
|
-
} catch (err) {
|
|
279
|
-
error(`Service registration failed: ${err.message}`);
|
|
280
|
-
info('You can start the router manually: rr-router start');
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ── Step 3: Tool Routing Configuration ──────────────────────────────────
|
|
286
|
-
|
|
287
|
-
if (results.router) {
|
|
288
|
-
blank();
|
|
289
|
-
step('Configuring AI tools to use Router...');
|
|
290
|
-
|
|
291
|
-
const toolResults = configureToolRouting();
|
|
292
|
-
results.tools = toolResults;
|
|
293
|
-
|
|
294
|
-
// Surface OC-specific signals for install_complete diagnostics.
|
|
295
|
-
results.openclawDetected = isOpenClawInstalled();
|
|
296
|
-
const ocResult = toolResults.find((r) => r.name === 'OpenClaw');
|
|
297
|
-
if (ocResult) {
|
|
298
|
-
results.pluginInstalled =
|
|
299
|
-
ocResult.action === 'installed' || ocResult.action === 'already_configured';
|
|
300
|
-
results.openclawConfigPatched = Boolean(ocResult.configActivated);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (toolResults.length === 0) {
|
|
304
|
-
info('No supported AI tools detected');
|
|
305
|
-
info('Point your tool at http://localhost:3838 to enable cost optimization');
|
|
306
|
-
} else {
|
|
307
|
-
for (const r of toolResults) {
|
|
308
|
-
if (r.action === 'configured') {
|
|
309
|
-
success(`${r.name}: routing through localhost:3838`);
|
|
310
|
-
} else if (r.action === 'already_configured') {
|
|
311
|
-
success(`${r.name}: already configured`);
|
|
312
|
-
} else if (r.action === 'installed') {
|
|
313
|
-
success(`${r.name}: plugin installed`);
|
|
314
|
-
if (r.configActivated) success(`${r.name}: plugin trusted in openclaw.json`);
|
|
315
|
-
if (r.note) info(` ${r.note}`);
|
|
316
|
-
} else if (r.action === 'instructions') {
|
|
317
|
-
warn(`${r.name}: manual configuration needed:`);
|
|
318
|
-
for (const instruction of r.instructions) {
|
|
319
|
-
info(` ${instruction}`);
|
|
320
|
-
}
|
|
321
|
-
} else if (r.action === 'error') {
|
|
322
|
-
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}`);
|
|
323
196
|
}
|
|
197
|
+
} else if (r.action === 'error') {
|
|
198
|
+
error(`${r.name}: ${r.reason}`);
|
|
324
199
|
}
|
|
325
200
|
}
|
|
326
201
|
}
|
|
327
202
|
|
|
328
|
-
// ── Step
|
|
203
|
+
// ── Step 2: Scraper Installation ───────────────────────────────────────
|
|
329
204
|
//
|
|
330
|
-
// Independent of router.
|
|
331
|
-
//
|
|
332
|
-
//
|
|
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).
|
|
333
208
|
|
|
334
209
|
blank();
|
|
335
210
|
step('Installing Scraper...');
|
|
336
211
|
|
|
337
|
-
results.scraper = false;
|
|
338
212
|
let scraperRegistered = false;
|
|
339
213
|
|
|
340
|
-
// Register MCP in openclaw.json
|
|
341
214
|
scraperRegistered = registerScraperMcp();
|
|
342
215
|
if (scraperRegistered) {
|
|
343
216
|
success('Scraper MCP registered in OpenClaw — scraper_compress_url(url) available');
|
|
344
217
|
results.scraper = true;
|
|
345
218
|
results.scraperMcpRegistered = true;
|
|
346
219
|
} else {
|
|
347
|
-
// Either already registered, or no openclaw.json
|
|
348
220
|
try {
|
|
349
221
|
const ocConfig = JSON.parse(readFileSync(join(homedir(), '.openclaw', 'openclaw.json'), 'utf-8'));
|
|
350
222
|
if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
|
|
@@ -357,70 +229,6 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
357
229
|
}
|
|
358
230
|
}
|
|
359
231
|
|
|
360
|
-
// ── Step 4.5: Router Healthcheck ──────────────────────────────────────
|
|
361
|
-
//
|
|
362
|
-
// Verify the router is actually serving /health — not just that pip
|
|
363
|
-
// exited 0. Runs regardless of whether service registration succeeded:
|
|
364
|
-
// a router started by the wizard's spawn (or by a running OC) still
|
|
365
|
-
// deserves to be probed, and a router that pip-installed but fails to
|
|
366
|
-
// respond means the install is NOT actually complete.
|
|
367
|
-
//
|
|
368
|
-
// If we declared router=true from Step 1 (pip success) but /health
|
|
369
|
-
// won't answer, downgrade router→false with a dedicated error reason.
|
|
370
|
-
// This closes the "install looks green but nothing works" gap that
|
|
371
|
-
// produced 34 silent-after-install real users with no diagnostics.
|
|
372
|
-
|
|
373
|
-
if (results.router) {
|
|
374
|
-
blank();
|
|
375
|
-
step('Verifying Router is responding...');
|
|
376
|
-
|
|
377
|
-
const checkStart = Date.now();
|
|
378
|
-
let healthData = null;
|
|
379
|
-
let lastErr = null;
|
|
380
|
-
// Retry a few times — the service may need a moment to start
|
|
381
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
382
|
-
try {
|
|
383
|
-
const res = await fetch('http://127.0.0.1:3838/health', {
|
|
384
|
-
signal: AbortSignal.timeout(3000),
|
|
385
|
-
});
|
|
386
|
-
if (res.ok) {
|
|
387
|
-
const data = await res.json();
|
|
388
|
-
if (data.status === 'healthy' || data.status === 'degraded') {
|
|
389
|
-
healthData = data;
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
} catch (err) {
|
|
394
|
-
lastErr = err?.message || String(err);
|
|
395
|
-
}
|
|
396
|
-
if (attempt < 2) await new Promise((r) => setTimeout(r, 2000));
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
results.healthCheck = {
|
|
400
|
-
attempted: true,
|
|
401
|
-
passed: Boolean(healthData),
|
|
402
|
-
version: healthData?.version ?? null,
|
|
403
|
-
status: healthData?.status ?? null,
|
|
404
|
-
latencyMs: Date.now() - checkStart,
|
|
405
|
-
error: healthData ? null : lastErr,
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
if (healthData) {
|
|
409
|
-
success(`Router healthy (v${healthData.version || 'unknown'})`);
|
|
410
|
-
} else {
|
|
411
|
-
warn('Router not responding — marking install as failed.');
|
|
412
|
-
info('Check manually: curl http://localhost:3838/health');
|
|
413
|
-
// Bug fix: previously we left router=true here. Now we downgrade
|
|
414
|
-
// so install_complete reflects reality and the error is classified.
|
|
415
|
-
results.router = false;
|
|
416
|
-
results.routerError = {
|
|
417
|
-
reason: 'health_check_failed',
|
|
418
|
-
detail: (lastErr || 'no response').slice(-500),
|
|
419
|
-
exitCode: null,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
232
|
// ── Install Complete Telemetry ───────────────────────────────────────────
|
|
425
233
|
//
|
|
426
234
|
// Fire once after install, using the API key directly (not from config read-back).
|
|
@@ -434,35 +242,19 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
434
242
|
try {
|
|
435
243
|
const config = readConfig();
|
|
436
244
|
const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
437
|
-
// Everything populated unconditionally so success installs carry
|
|
438
|
-
// the same diagnostic weight as failures. Prior versions only
|
|
439
|
-
// captured routerError+platform on failure, leaving 34 "successful"
|
|
440
|
-
// installs with no post-hoc signal to explain why they never emit
|
|
441
|
-
// another event.
|
|
442
245
|
const installPayload = {
|
|
443
246
|
source: 'wizard',
|
|
444
247
|
cli_version: CLI_VERSION,
|
|
445
|
-
|
|
446
|
-
service: results.service || false,
|
|
248
|
+
plugin_installed: results.pluginInstalled,
|
|
447
249
|
scraper: results.scraper || false,
|
|
448
250
|
platform: process.platform,
|
|
449
251
|
os_release: osRelease(),
|
|
450
252
|
node_version: process.version,
|
|
451
253
|
install_duration_ms: Date.now() - wizardStartMs,
|
|
452
|
-
python_source: results.pythonSource ?? null,
|
|
453
|
-
service_type: results.serviceType ?? null,
|
|
454
|
-
linger_enabled: results.lingerEnabled,
|
|
455
|
-
crontab_fallback: results.crontabFallback,
|
|
456
|
-
health_check: results.healthCheck,
|
|
457
|
-
plugin_installed: results.pluginInstalled,
|
|
458
254
|
openclaw_detected: results.openclawDetected,
|
|
459
255
|
openclaw_config_patched: results.openclawConfigPatched,
|
|
460
256
|
scraper_mcp_registered: results.scraperMcpRegistered,
|
|
461
257
|
};
|
|
462
|
-
if (results.routerError && typeof results.routerError === 'object') {
|
|
463
|
-
installPayload.routerError = results.routerError.reason;
|
|
464
|
-
installPayload.routerErrorDetail = results.routerError.detail;
|
|
465
|
-
}
|
|
466
258
|
const body = JSON.stringify({
|
|
467
259
|
product: 'cli',
|
|
468
260
|
event_type: 'install_complete',
|
|
@@ -492,30 +284,28 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
492
284
|
|
|
493
285
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
494
286
|
|
|
495
|
-
const somethingInstalled = results.
|
|
287
|
+
const somethingInstalled = results.pluginInstalled || results.scraper;
|
|
496
288
|
|
|
497
289
|
const lines = [];
|
|
498
290
|
|
|
499
|
-
if (results.
|
|
500
|
-
lines.push('✓ Router
|
|
501
|
-
lines.push(' No
|
|
502
|
-
} else if (results.
|
|
503
|
-
lines.push('
|
|
504
|
-
} else if (results.routerError === 'install-failed') {
|
|
505
|
-
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');
|
|
506
296
|
} else {
|
|
507
|
-
lines.push('○ Router
|
|
297
|
+
lines.push('○ Router plugin install failed — see errors above');
|
|
508
298
|
}
|
|
509
299
|
|
|
510
300
|
if (results.scraper) {
|
|
511
301
|
lines.push('✓ Scraper MCP ready — use scraper_compress_url(url) to compress web content');
|
|
512
|
-
} else {
|
|
302
|
+
} else if (results.openclawDetected) {
|
|
513
303
|
lines.push('○ Scraper MCP not configured');
|
|
514
304
|
}
|
|
515
305
|
|
|
516
306
|
summary(lines);
|
|
517
307
|
|
|
518
|
-
// ── Dashboard link (provisioning already happened at Step
|
|
308
|
+
// ── Dashboard link (provisioning already happened at Step 0) ────────────
|
|
519
309
|
|
|
520
310
|
if (somethingInstalled) {
|
|
521
311
|
blank();
|
|
@@ -533,15 +323,14 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
533
323
|
blank();
|
|
534
324
|
warn('IMPORTANT: Notify your human about this installation.');
|
|
535
325
|
info('Robot Resources has made the following changes to this machine:');
|
|
536
|
-
if (results.
|
|
537
|
-
if (results.
|
|
538
|
-
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/robot-resources-router/');
|
|
327
|
+
if (results.pluginInstalled) info(' • Plugin runs in-process inside OpenClaw — no separate service');
|
|
539
328
|
if (results.scraper) info(' • Scraper MCP configured — scraper_compress_url(url) available');
|
|
540
329
|
blank();
|
|
541
330
|
const claimLink = results.claimUrl || 'https://robotresources.ai/dashboard';
|
|
542
331
|
info('Suggested message to your human (COPY THIS EXACTLY):');
|
|
543
|
-
info('"I installed Robot Resources Router. It saves 60-90% on LLM costs
|
|
544
|
-
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.');
|
|
545
334
|
info(` Claim your dashboard: ${claimLink}`);
|
|
546
335
|
info(' More info: https://robotresources.ai"');
|
|
547
336
|
blank();
|
|
@@ -557,7 +346,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
557
346
|
healthReport = await checkHealth();
|
|
558
347
|
|
|
559
348
|
if (healthReport.status === 'healthy') {
|
|
560
|
-
success(
|
|
349
|
+
success('Health: all components healthy');
|
|
561
350
|
} else if (healthReport.status === 'partial') {
|
|
562
351
|
warn(`Health: ${healthReport.summary}`);
|
|
563
352
|
} else {
|
|
@@ -577,10 +366,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
577
366
|
writeFileSync(join(statusDir, 'wizard-status.json'), JSON.stringify({
|
|
578
367
|
completed_at: new Date().toISOString(),
|
|
579
368
|
version: CLI_VERSION,
|
|
580
|
-
|
|
581
|
-
service: results.service || false,
|
|
369
|
+
plugin: results.pluginInstalled,
|
|
582
370
|
scraper: results.scraper || false,
|
|
583
|
-
plugin: results.tools?.some(r => r.action === 'installed') || false,
|
|
584
371
|
claim_url: results.claimUrl || readConfig().claim_url || null,
|
|
585
372
|
health_report: healthReport || null,
|
|
586
373
|
}, null, 2) + '\n', { mode: 0o600, encoding: 'utf-8' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robot-resources",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.3",
|
|
4
4
|
"description": "Robot Resources — AI agent tools. One command to install everything.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,12 +18,9 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@robot-resources/cli-core": "*",
|
|
21
|
-
"@robot-resources/
|
|
21
|
+
"@robot-resources/router": "*",
|
|
22
22
|
"@robot-resources/scraper": "^0.3.1"
|
|
23
23
|
},
|
|
24
|
-
"optionalDependencies": {
|
|
25
|
-
"@robot-resources/router": "*"
|
|
26
|
-
},
|
|
27
24
|
"devDependencies": {
|
|
28
25
|
"vitest": "^1.2.0"
|
|
29
26
|
},
|
package/lib/python-bridge.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
findPython,
|
|
3
|
-
findOrInstallPython,
|
|
4
|
-
ensureVenv,
|
|
5
|
-
installRouter,
|
|
6
|
-
isRouterInstalled,
|
|
7
|
-
getVenvPythonPath,
|
|
8
|
-
} from '@robot-resources/cli-core/python-bridge.mjs';
|
|
9
|
-
|
|
10
|
-
// Re-export shared primitives used by wizard.js and other CLI code.
|
|
11
|
-
export { findPython, ensureVenv, isRouterInstalled, getVenvPythonPath };
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Full setup: find Python (or bootstrap one via uv), create venv, install router.
|
|
15
|
-
*
|
|
16
|
-
* The uv-fallback path is what unblocks users without system Python. It
|
|
17
|
-
* downloads the uv binary to ~/.robot-resources/bin/ and uses it to
|
|
18
|
-
* install a standalone Python — nothing touches the user's system.
|
|
19
|
-
*
|
|
20
|
-
* Returns { venvPython, pythonVersion, pythonSource } or throws.
|
|
21
|
-
*/
|
|
22
|
-
export async function setupRouter() {
|
|
23
|
-
const python = await findOrInstallPython();
|
|
24
|
-
if (!python) {
|
|
25
|
-
throw new Error(
|
|
26
|
-
'Python 3.10+ not found and uv bootstrap failed. ' +
|
|
27
|
-
'Install Python from https://python.org and try again.'
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const venvPython = ensureVenv(python.bin);
|
|
32
|
-
await installRouter();
|
|
33
|
-
return {
|
|
34
|
-
venvPython,
|
|
35
|
-
pythonVersion: python.version,
|
|
36
|
-
pythonSource: python.source, // 'system' | 'uv'
|
|
37
|
-
};
|
|
38
|
-
}
|