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 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,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 { 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
- /**
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
- const rawDetail = err?.stderr?.trim?.() || err?.message || String(err);
54
- const detail = rawDetail.slice(-500);
55
-
56
- return { reason, detail, exitCode: err?.exitCode ?? null };
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. Handles the full onboarding flow:
61
- * 1. Router installation (Python venv + pip)
62
- * 2. Service registration (launchd/systemd)
63
- * 3. Tool routing (OpenClaw plugin + model activation)
64
- * 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.
65
38
  *
66
- * Auth is intentionally LAST. The router works fully without it.
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' | 'github'
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
- healthCheck: { attempted: false },
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: Router Installation ─────────────────────────────────────────
180
-
181
- step('Checking Router...');
182
-
183
- if (isRouterInstalled()) {
184
- success('Router already installed');
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
- try {
201
- const { pythonSource } = await setupRouter();
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
- // ── Step 1.5: Transparent Proxy Info ────────────────────────────────────
168
+ const toolResults = configureToolRouting();
169
+ results.tools = toolResults;
212
170
 
213
- if (results.router) {
214
- blank();
215
- step('Router proxy mode...');
216
- info('The Router works as a transparent proxy — no API keys needed.');
217
- info('Your AI tools already have their keys configured.');
218
- 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);
219
177
  }
220
178
 
221
- // ── Step 2: Service Registration ────────────────────────────────────────
222
-
223
- if (results.router) {
224
- blank();
225
- step('Configuring Router as system service...');
226
-
227
- if (isServiceRunning()) {
228
- success('Router service already running');
229
- results.service = true;
230
- } else {
231
- // Check port availability
232
- if (!isPortAvailable()) {
233
- warn('Port 3838 is already in use');
234
- info('Another process may be using this port. The service will retry on restart.');
235
- }
236
-
237
- try {
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 4: Scraper Installation ───────────────────────────────────────
203
+ // ── Step 2: Scraper Installation ───────────────────────────────────────
312
204
  //
313
- // Independent of router. Scraper works even if router failed to install.
314
- // Register scraper MCP in openclaw.json (if OC is present).
315
- // 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).
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
- router: results.router || false,
428
- service: results.service || false,
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.router || results.service || results.scraper;
287
+ const somethingInstalled = results.pluginInstalled || results.scraper;
478
288
 
479
289
  const lines = [];
480
290
 
481
- if (results.router && results.service) {
482
- lines.push('✓ Router running as transparent proxy (localhost:3838)');
483
- lines.push(' No API keys needed reads them from your tools automatically');
484
- } else if (results.router) {
485
- lines.push(' Router installed (start manually: rr-router start)');
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 not installed (Python 3.10+ required)');
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 1.5) ──────────
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.router) info(' • Router installed in ~/.robot-resources/');
519
- if (results.service) info(' • Router registered as a transparent proxy (localhost:3838)');
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(' 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.');
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(`Health: all components healthy`);
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: pkgVersion,
563
- router: results.router || false,
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' });