robot-resources 1.9.3 → 1.9.5

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.
Files changed (2) hide show
  1. package/lib/wizard.js +96 -20
  2. package/package.json +1 -1
package/lib/wizard.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { homedir, hostname } from 'node:os';
3
+ import { homedir, hostname, release as osRelease } from 'node:os';
4
4
  import { readConfig, writeConfig } from '@robot-resources/cli-core/config.mjs';
5
5
  import { findPython, isPortAvailable, isHeadless, isOpenClawInstalled } from './detect.js';
6
6
  import { getOrCreateMachineId } from './machine-id.js';
@@ -19,12 +19,25 @@ import { header, step, success, warn, error, info, blank, summary } from './ui.j
19
19
  */
20
20
  function classifyRouterError(err) {
21
21
  const msg = (err?.message || String(err)).toLowerCase();
22
+ const stderr = (err?.stderr || '').toString().toLowerCase();
23
+ const combined = msg + '\n' + stderr;
22
24
  let reason = 'unknown';
23
25
 
24
- if (msg.includes('python 3.10+') || msg.includes('python is required')) {
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')) {
25
32
  reason = 'python_not_found';
26
33
  } else if (err?.code === 'ENOENT' || msg.includes('enoent')) {
27
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';
28
41
  } else if (msg.includes('timeout') || msg.includes('timed out') || err?.code === 'ETIMEDOUT') {
29
42
  reason = 'timeout';
30
43
  } else if (msg.includes('exited with code') || msg.includes('pip install')) {
@@ -56,6 +69,8 @@ function classifyRouterError(err) {
56
69
  export async function runWizard({ nonInteractive = false } = {}) {
57
70
  header();
58
71
 
72
+ const wizardStartMs = Date.now();
73
+
59
74
  const results = {
60
75
  auth: false,
61
76
  authMethod: null, // 'config' | 'apikey' | 'github'
@@ -63,6 +78,16 @@ export async function runWizard({ nonInteractive = false } = {}) {
63
78
  routerError: null,
64
79
  providerKeys: false,
65
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
+ pluginInstalled: false,
87
+ openclawDetected: false,
88
+ openclawConfigPatched: false,
89
+ scraperMcpRegistered: false,
90
+ healthCheck: { attempted: false },
66
91
  };
67
92
 
68
93
  // ── Step 0: Provision API key (before anything else) ────────────────────
@@ -209,6 +234,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
209
234
 
210
235
  try {
211
236
  const svc = installService(getVenvPythonPath());
237
+ results.serviceType = svc.type || null;
212
238
  if (svc.type === 'skipped') {
213
239
  warn(svc.reason);
214
240
  results.service = false;
@@ -234,6 +260,15 @@ export async function runWizard({ nonInteractive = false } = {}) {
234
260
  const toolResults = configureToolRouting();
235
261
  results.tools = toolResults;
236
262
 
263
+ // Surface OC-specific signals for install_complete diagnostics.
264
+ results.openclawDetected = isOpenClawInstalled();
265
+ const ocResult = toolResults.find((r) => r.name === 'OpenClaw');
266
+ if (ocResult) {
267
+ results.pluginInstalled =
268
+ ocResult.action === 'installed' || ocResult.action === 'already_configured';
269
+ results.openclawConfigPatched = Boolean(ocResult.configActivated);
270
+ }
271
+
237
272
  if (toolResults.length === 0) {
238
273
  info('No supported AI tools detected');
239
274
  info('Point your tool at http://localhost:3838 to enable cost optimization');
@@ -276,6 +311,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
276
311
  if (scraperRegistered) {
277
312
  success('Scraper MCP registered in OpenClaw — scraper_compress_url(url) available');
278
313
  results.scraper = true;
314
+ results.scraperMcpRegistered = true;
279
315
  } else {
280
316
  // Either already registered, or no openclaw.json
281
317
  try {
@@ -283,6 +319,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
283
319
  if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
284
320
  success('Scraper MCP already registered in OpenClaw');
285
321
  results.scraper = true;
322
+ results.scraperMcpRegistered = true;
286
323
  }
287
324
  } catch {
288
325
  // No openclaw.json — not on OC, skip
@@ -290,13 +327,25 @@ export async function runWizard({ nonInteractive = false } = {}) {
290
327
  }
291
328
 
292
329
  // ── Step 4.5: Router Healthcheck ──────────────────────────────────────
330
+ //
331
+ // Verify the router is actually serving /health — not just that pip
332
+ // exited 0. Runs regardless of whether service registration succeeded:
333
+ // a router started by the wizard's spawn (or by a running OC) still
334
+ // deserves to be probed, and a router that pip-installed but fails to
335
+ // respond means the install is NOT actually complete.
336
+ //
337
+ // If we declared router=true from Step 1 (pip success) but /health
338
+ // won't answer, downgrade router→false with a dedicated error reason.
339
+ // This closes the "install looks green but nothing works" gap that
340
+ // produced 34 silent-after-install real users with no diagnostics.
293
341
 
294
- // Router: verify it's responding on localhost:3838
295
- if (results.service) {
342
+ if (results.router) {
296
343
  blank();
297
344
  step('Verifying Router is responding...');
298
345
 
299
- let healthy = false;
346
+ const checkStart = Date.now();
347
+ let healthData = null;
348
+ let lastErr = null;
300
349
  // Retry a few times — the service may need a moment to start
301
350
  for (let attempt = 0; attempt < 3; attempt++) {
302
351
  try {
@@ -306,20 +355,38 @@ export async function runWizard({ nonInteractive = false } = {}) {
306
355
  if (res.ok) {
307
356
  const data = await res.json();
308
357
  if (data.status === 'healthy' || data.status === 'degraded') {
309
- success(`Router healthy (v${data.version || 'unknown'})`);
310
- healthy = true;
358
+ healthData = data;
311
359
  break;
312
360
  }
313
361
  }
314
- } catch {
315
- // Wait before retrying
316
- await new Promise((r) => setTimeout(r, 2000));
362
+ } catch (err) {
363
+ lastErr = err?.message || String(err);
317
364
  }
365
+ if (attempt < 2) await new Promise((r) => setTimeout(r, 2000));
318
366
  }
319
367
 
320
- if (!healthy) {
321
- warn('Router not responding yet — it may need a few more seconds to start');
368
+ results.healthCheck = {
369
+ attempted: true,
370
+ passed: Boolean(healthData),
371
+ version: healthData?.version ?? null,
372
+ status: healthData?.status ?? null,
373
+ latencyMs: Date.now() - checkStart,
374
+ error: healthData ? null : lastErr,
375
+ };
376
+
377
+ if (healthData) {
378
+ success(`Router healthy (v${healthData.version || 'unknown'})`);
379
+ } else {
380
+ warn('Router not responding — marking install as failed.');
322
381
  info('Check manually: curl http://localhost:3838/health');
382
+ // Bug fix: previously we left router=true here. Now we downgrade
383
+ // so install_complete reflects reality and the error is classified.
384
+ results.router = false;
385
+ results.routerError = {
386
+ reason: 'health_check_failed',
387
+ detail: (lastErr || 'no response').slice(-500),
388
+ exitCode: null,
389
+ };
323
390
  }
324
391
  }
325
392
 
@@ -336,22 +403,31 @@ export async function runWizard({ nonInteractive = false } = {}) {
336
403
  try {
337
404
  const config = readConfig();
338
405
  const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
406
+ // Everything populated unconditionally so success installs carry
407
+ // the same diagnostic weight as failures. Prior versions only
408
+ // captured routerError+platform on failure, leaving 34 "successful"
409
+ // installs with no post-hoc signal to explain why they never emit
410
+ // another event.
339
411
  const installPayload = {
412
+ source: 'wizard',
340
413
  router: results.router || false,
341
414
  service: results.service || false,
342
415
  scraper: results.scraper || false,
343
- source: 'wizard',
416
+ platform: process.platform,
417
+ os_release: osRelease(),
418
+ node_version: process.version,
419
+ install_duration_ms: Date.now() - wizardStartMs,
420
+ python_source: results.pythonSource ?? null,
421
+ service_type: results.serviceType ?? null,
422
+ health_check: results.healthCheck,
423
+ plugin_installed: results.pluginInstalled,
424
+ openclaw_detected: results.openclawDetected,
425
+ openclaw_config_patched: results.openclawConfigPatched,
426
+ scraper_mcp_registered: results.scraperMcpRegistered,
344
427
  };
345
- if (results.pythonSource) {
346
- // 'system' when the user had Python installed, 'uv' when we
347
- // auto-bootstrapped one. Lets us measure how many installs were
348
- // rescued by the uv fallback.
349
- installPayload.pythonSource = results.pythonSource;
350
- }
351
428
  if (results.routerError && typeof results.routerError === 'object') {
352
429
  installPayload.routerError = results.routerError.reason;
353
430
  installPayload.routerErrorDetail = results.routerError.detail;
354
- installPayload.platform = process.platform;
355
431
  }
356
432
  const body = JSON.stringify({
357
433
  product: 'cli',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "description": "Robot Resources — AI agent tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {