robot-resources 1.9.7 → 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 CHANGED
@@ -57,7 +57,6 @@ Available as MCP tool after install:
57
57
  ## MCP Servers
58
58
 
59
59
  ```bash
60
- npx -y @robot-resources/router-mcp # Router stats + config
61
60
  npx -y @robot-resources/scraper-mcp # Scraper compression
62
61
  ```
63
62
 
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
- }
@@ -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/openclaw-plugin).
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
- * Since 0.5.5, the plugin is a thin shim (index.js) that imports the rest
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/openclaw-plugin/package.json');
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/openclaw-plugin files into
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/openclaw-plugin',
200
+ ' openclaw plugins install @robot-resources/router',
200
201
  ];
201
202
 
202
203
  if (authMode === 'subscription') {
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 { findPython, isPortAvailable, isHeadless, isOpenClawInstalled } from './detect.js';
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 — which is exactly the visibility gap
16
- // that left us blind on real-user install failures despite shipping rich
17
- // diagnostics in PR #163. Read once at module load; safe to fail (telemetry
18
- // just lands without the field).
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. Handles the full onboarding flow:
77
- * 1. Router installation (Python venv + pip)
78
- * 2. Service registration (launchd/systemd)
79
- * 3. Tool routing (OpenClaw plugin + model activation)
80
- * 4. Dashboard link
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.
81
38
  *
82
- * Auth is intentionally LAST. The router works fully without it.
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' | 'github'
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
- healthCheck: { attempted: false },
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: Router Installation ─────────────────────────────────────────
197
-
198
- step('Checking Router...');
199
-
200
- if (isRouterInstalled()) {
201
- success('Router already installed');
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
- try {
218
- const { pythonSource } = await setupRouter();
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
- // ── Step 1.5: Transparent Proxy Info ────────────────────────────────────
168
+ const toolResults = configureToolRouting();
169
+ results.tools = toolResults;
229
170
 
230
- if (results.router) {
231
- blank();
232
- step('Router proxy mode...');
233
- info('The Router works as a transparent proxy — no API keys needed.');
234
- info('Your AI tools already have their keys configured.');
235
- info('The Router reads them from each request and forwards automatically.');
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
- // ── Step 2: Service Registration ────────────────────────────────────────
239
-
240
- if (results.router) {
241
- blank();
242
- step('Configuring Router as system service...');
243
-
244
- if (isServiceRunning()) {
245
- success('Router service already running');
246
- results.service = true;
247
- } else {
248
- // Check port availability
249
- if (!isPortAvailable()) {
250
- warn('Port 3838 is already in use');
251
- info('Another process may be using this port. The service will retry on restart.');
252
- }
253
-
254
- try {
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 4: Scraper Installation ───────────────────────────────────────
203
+ // ── Step 2: Scraper Installation ───────────────────────────────────────
329
204
  //
330
- // Independent of router. Scraper works even if router failed to install.
331
- // Register scraper MCP in openclaw.json (if OC is present).
332
- // Gateway restart happens once at the very end (merged with plugin restart).
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
- router: results.router || false,
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.router || results.service || results.scraper;
287
+ const somethingInstalled = results.pluginInstalled || results.scraper;
496
288
 
497
289
  const lines = [];
498
290
 
499
- if (results.router && results.service) {
500
- lines.push('✓ Router running as transparent proxy (localhost:3838)');
501
- lines.push(' No API keys needed reads them from your tools automatically');
502
- } else if (results.router) {
503
- lines.push(' Router installed (start manually: rr-router start)');
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 not installed (Python 3.10+ required)');
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 1.5) ──────────
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.router) info(' • Router installed in ~/.robot-resources/');
537
- if (results.service) info(' • Router registered as a transparent proxy (localhost:3838)');
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/openclaw-plugin/');
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(' All calls now route through localhost:3838 for cost optimization.');
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(`Health: all components healthy`);
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
- router: results.router || false,
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' });