robot-resources 1.15.2 → 1.15.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/lib/wizard.js DELETED
@@ -1,523 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { homedir, hostname, release as osRelease } from 'node:os';
4
- import { readConfig, writeConfig } from './config.mjs';
5
- import { isOpenClawInstalled } from './detect.js';
6
- import { getOrCreateMachineId } from './machine-id.js';
7
- import { configureToolRouting, registerScraperMcp, restartOpenClawGateway } from './tool-config.js';
8
- import { checkHealth } from './health-report.js';
9
- import { header, step, success, warn, error, info, blank, summary } from './ui.js';
10
- import { runNonOcWizard } from './non-oc-wizard.js';
11
- import { runUninstall } from './uninstall.js';
12
-
13
- // Stamped onto every CLI telemetry payload so we can tell which `robot-resources`
14
- // version a user actually ran. Without this, npx-cached old installers look
15
- // identical to fresh runs in Supabase — exactly the visibility gap that left
16
- // us blind on real-user install failures despite shipping rich diagnostics
17
- // in PR #163. Read once at module load; safe to fail (telemetry just lands
18
- // without the field).
19
- const CLI_VERSION = (() => {
20
- try {
21
- return JSON.parse(
22
- readFileSync(new URL('../package.json', import.meta.url), 'utf-8'),
23
- ).version;
24
- } catch {
25
- return null;
26
- }
27
- })();
28
-
29
- /**
30
- * Fire `install_complete` once after the wizard finishes — for BOTH OC
31
- * and non-OC paths. Phase 11 fix: previously this only fired on the OC
32
- * path because the non-OC branch returned early. The 7-day funnel showed
33
- * 0/58 non-OC users hitting this event for a week. Now both paths fire
34
- * with a `path: 'oc' | 'non-oc'` discriminator so funnel queries can
35
- * segment without a second event type.
36
- *
37
- * Best-effort with one retry. Total budget: ~20s. Telemetry never blocks
38
- * the wizard exit beyond that.
39
- */
40
- async function emitInstallComplete({ apiKey, payload }) {
41
- if (!apiKey) return;
42
- const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
43
- const body = JSON.stringify({
44
- product: 'cli',
45
- event_type: 'install_complete',
46
- payload,
47
- });
48
- for (let attempt = 0; attempt < 2; attempt++) {
49
- try {
50
- const res = await fetch(`${platformUrl}/v1/telemetry`, {
51
- method: 'POST',
52
- headers: {
53
- 'Authorization': `Bearer ${apiKey}`,
54
- 'Content-Type': 'application/json',
55
- },
56
- body,
57
- signal: AbortSignal.timeout(10_000),
58
- });
59
- if (res.ok) break;
60
- } catch {
61
- // Try again on next iteration.
62
- }
63
- }
64
- }
65
-
66
- /**
67
- * Main setup wizard. In Option 4 (post-PR-2.5) the wizard does NOT install
68
- * a Python daemon, register a system service, or run a localhost health
69
- * probe — the router lives entirely inside the OC plugin's process now.
70
- * The wizard's job is reduced to:
71
- *
72
- * 1. Provision an anonymous api_key (telemetry/dashboard identity).
73
- * 2. Install both OC plugins:
74
- * a. router plugin (in-process HTTP server + routing logic) into
75
- * ~/.openclaw/extensions/robot-resources-router/
76
- * b. scraper OC plugin (web_fetch → scraper_compress_url hook) into
77
- * ~/.openclaw/extensions/robot-resources-scraper-oc-plugin/
78
- * 3. Register the scraper MCP in openclaw.json.
79
- * 4. Restart the OC gateway so the plugins load.
80
- *
81
- * No Python, no venv, no systemd, no port probe.
82
- */
83
- export async function runWizard({ nonInteractive = false, target = null, scope = 'full', autoAttachSource = false } = {}) {
84
- header();
85
-
86
- // Detect OC once up front. Used both to branch into the non-OC wizard and
87
- // to tag the wizard_started payload, so the funnel can be segmented OC vs
88
- // non-OC without a second event type.
89
- const openclawDetected = isOpenClawInstalled();
90
- const wizardStartMs = Date.now();
91
-
92
- const results = {
93
- auth: false,
94
- authMethod: null, // 'config' | 'apikey' | 'auto'
95
- pluginInstalled: false,
96
- openclawDetected,
97
- openclawConfigPatched: false,
98
- scraperMcpRegistered: false,
99
- scraper: false,
100
- };
101
-
102
- // ── Step 0: Provision API key (before anything else) ────────────────────
103
- //
104
- // Runs for BOTH the OC and non-OC paths. Provisioning before the non-OC
105
- // hand-off closes the funnel blind spot where every non-OpenClaw install
106
- // was invisible to telemetry (no api_keys row, no wizard_started, no
107
- // agent_signup_meta). If the session dies later, telemetry still works
108
- // for all tools. Single fetch() with 10s timeout — no prompts, no browser.
109
-
110
- {
111
- const config = readConfig();
112
- if (!config.api_key && !process.env.RR_API_KEY) {
113
- try {
114
- const machineId = getOrCreateMachineId();
115
-
116
- const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
117
- const res = await fetch(`${platformUrl}/v1/auth/signup`, {
118
- method: 'POST',
119
- headers: { 'Content-Type': 'application/json' },
120
- body: JSON.stringify({
121
- agent_name: hostname(),
122
- platform: 'cli',
123
- machine_id: machineId,
124
- }),
125
- signal: AbortSignal.timeout(10_000),
126
- });
127
-
128
- if (res.ok) {
129
- const { data } = await res.json();
130
- writeConfig({
131
- api_key: data.api_key,
132
- key_id: data.key_id,
133
- claim_url: data.claim_url,
134
- signup_source: 'auto',
135
- });
136
- results.auth = true;
137
- results.authMethod = 'auto';
138
- results.claimUrl = data.claim_url;
139
- }
140
- } catch {
141
- // Non-fatal — tools work without telemetry
142
- }
143
- } else {
144
- results.auth = true;
145
- results.authMethod = config.api_key ? 'config' : 'apikey';
146
- results.claimUrl = config.claim_url || null;
147
- }
148
- }
149
-
150
- // ── Funnel marker: wizard_started ───────────────────────────────────────
151
- //
152
- // Sent immediately after auth, before either path branches, so we have
153
- // proof the wizard reached this point even if a later step crashes. Pairs
154
- // with install_complete (OC path) or wizard_path_chosen (non-OC path) to
155
- // give us a "started → done" funnel. The openclaw_detected field lets us
156
- // segment OC vs non-OC funnels without a second event type.
157
- //
158
- // Timeout asymmetry vs install_complete (5s, no retry vs 10s × 2 attempts):
159
- // wizard_started is a best-effort funnel marker — losing it just means we
160
- // miss one funnel datapoint. install_complete is a heartbeat that powers
161
- // last_used_at and the "wizard ran successfully" signal, so it gets the
162
- // longer timeout + retry to maximize delivery.
163
-
164
- if (results.auth) {
165
- try {
166
- const config = readConfig();
167
- const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
168
- await fetch(`${platformUrl}/v1/telemetry`, {
169
- method: 'POST',
170
- headers: {
171
- 'Authorization': `Bearer ${config.api_key}`,
172
- 'Content-Type': 'application/json',
173
- },
174
- body: JSON.stringify({
175
- product: 'cli',
176
- event_type: 'wizard_started',
177
- payload: {
178
- cli_version: CLI_VERSION,
179
- auth_method: results.authMethod,
180
- non_interactive: nonInteractive,
181
- openclaw_detected: openclawDetected,
182
- // Phase 3: tags which install branch the wizard is about to take.
183
- // 'oc' for the OpenClaw path, 'non-oc' covers everything the
184
- // non-OC wizard handles (Node shim, Python shim, MCP, docs).
185
- // Finer per-path attribution still comes from wizard_path_chosen.
186
- entry: openclawDetected ? 'oc' : 'non-oc',
187
- // Phase 5: 'full' is the unified `npx robot-resources` flow;
188
- // 'router-only' is the standalone `npx @robot-resources/router`
189
- // bin (skips scraper).
190
- scope,
191
- },
192
- }),
193
- signal: AbortSignal.timeout(5_000),
194
- });
195
- } catch {
196
- // Non-fatal — wizard_started is best-effort
197
- }
198
- }
199
-
200
- // Non-OC branch. Hands off to the multi-agent compatibility wizard which
201
- // routes the user to the right install path (npm install / pip install /
202
- // MCP config / docs / install-OC). The non-OC wizard's wizard_path_chosen
203
- // telemetry now fires too, since Step 0 above provisioned an api_key.
204
- if (!openclawDetected) {
205
- await runNonOcWizard({ nonInteractive, target, autoAttachSource });
206
-
207
- // Phase 11: install_complete now fires for non-OC too. Closes the
208
- // funnel signal that was 0/58 for a week.
209
- if (results.auth) {
210
- try {
211
- const config = readConfig();
212
- await emitInstallComplete({
213
- apiKey: config.api_key,
214
- payload: {
215
- source: 'wizard',
216
- path: 'non-oc',
217
- cli_version: CLI_VERSION,
218
- target: target ?? null,
219
- scope,
220
- auto_attach_source: !!autoAttachSource,
221
- platform: process.platform,
222
- os_release: osRelease(),
223
- node_version: process.version,
224
- install_duration_ms: Date.now() - wizardStartMs,
225
- non_interactive: nonInteractive,
226
- },
227
- });
228
- } catch { /* non-fatal */ }
229
- }
230
- return;
231
- }
232
-
233
- // ── Step 1: Tool Routing Configuration ──────────────────────────────────
234
- //
235
- // Installs the OC plugin (which is @robot-resources/router — the router
236
- // IS the OC plugin in the in-process architecture). The plugin's
237
- // register() starts an in-process HTTP server on 127.0.0.1:18790 that
238
- // OC dispatches LLM calls to. No daemon to spawn, no service to register.
239
-
240
- blank();
241
- step('Configuring AI tools to use Router...');
242
-
243
- const toolResults = configureToolRouting();
244
- results.tools = toolResults;
245
-
246
- const ocResult = toolResults.find((r) => r.name === 'OpenClaw');
247
- if (ocResult) {
248
- results.pluginInstalled =
249
- ocResult.action === 'installed' || ocResult.action === 'already_configured';
250
- results.openclawConfigPatched = Boolean(ocResult.configActivated);
251
- }
252
-
253
- if (toolResults.length === 0) {
254
- info('No supported AI tools detected');
255
- info('Install OpenClaw and re-run: npx robot-resources');
256
- } else {
257
- for (const r of toolResults) {
258
- if (r.action === 'configured') {
259
- success(`${r.name}: routing configured`);
260
- } else if (r.action === 'already_configured') {
261
- success(`${r.name}: already configured`);
262
- } else if (r.action === 'installed') {
263
- success(`${r.name}: plugin installed`);
264
- if (r.configActivated) success(`${r.name}: plugin trusted in openclaw.json`);
265
- if (r.note) info(` ${r.note}`);
266
- } else if (r.action === 'instructions') {
267
- warn(`${r.name}: manual configuration needed:`);
268
- for (const instruction of r.instructions) {
269
- info(` ${instruction}`);
270
- }
271
- } else if (r.action === 'error') {
272
- error(`${r.name}: ${r.reason}`);
273
- }
274
- }
275
- }
276
-
277
- // ── Step 2: Scraper Installation ───────────────────────────────────────
278
- //
279
- // Independent of router. Register scraper MCP in openclaw.json (if OC
280
- // is present). Gateway restart happens once at the very end (merged
281
- // with plugin restart).
282
- //
283
- // Phase 5: when invoked from `npx @robot-resources/router` (scope=router-only),
284
- // we skip this step entirely. The standalone router CLI ships a smaller
285
- // surface for users who explicitly want only routing, no scraper.
286
-
287
- let scraperRegistered = false;
288
-
289
- if (scope !== 'router-only') {
290
- blank();
291
- step('Installing Scraper...');
292
-
293
- scraperRegistered = registerScraperMcp();
294
- if (scraperRegistered) {
295
- success('Scraper MCP registered in OpenClaw — scraper_compress_url(url) available');
296
- results.scraper = true;
297
- results.scraperMcpRegistered = true;
298
- } else {
299
- try {
300
- const ocConfig = JSON.parse(readFileSync(join(homedir(), '.openclaw', 'openclaw.json'), 'utf-8'));
301
- if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
302
- success('Scraper MCP already registered in OpenClaw');
303
- results.scraper = true;
304
- results.scraperMcpRegistered = true;
305
- }
306
- } catch {
307
- // No openclaw.json — not on OC, skip
308
- }
309
- }
310
- }
311
-
312
- // ── Install Complete Telemetry ───────────────────────────────────────────
313
- //
314
- // Fire once after the OC install path completes. Non-OC fires its own
315
- // install_complete higher up (right after runNonOcWizard returns), so
316
- // both paths now produce this funnel signal — see emitInstallComplete().
317
-
318
- if (results.auth) {
319
- try {
320
- const config = readConfig();
321
- await emitInstallComplete({
322
- apiKey: config.api_key,
323
- payload: {
324
- source: 'wizard',
325
- path: 'oc',
326
- cli_version: CLI_VERSION,
327
- plugin_installed: results.pluginInstalled,
328
- scraper: results.scraper || false,
329
- platform: process.platform,
330
- os_release: osRelease(),
331
- node_version: process.version,
332
- install_duration_ms: Date.now() - wizardStartMs,
333
- openclaw_detected: results.openclawDetected,
334
- openclaw_config_patched: results.openclawConfigPatched,
335
- scraper_mcp_registered: results.scraperMcpRegistered,
336
- },
337
- });
338
- } catch {
339
- // Non-fatal — install_complete is best-effort.
340
- }
341
- }
342
-
343
- // ── Summary ─────────────────────────────────────────────────────────────
344
-
345
- const somethingInstalled = results.pluginInstalled || results.scraper;
346
-
347
- const lines = [];
348
-
349
- if (results.pluginInstalled) {
350
- lines.push('✓ Router plugin installed — in-process routing active inside OpenClaw');
351
- lines.push(' No daemon to manage. Plugin starts and stops with OC.');
352
- } else if (!results.openclawDetected) {
353
- lines.push('○ OpenClaw not detected — install OC, then re-run npx robot-resources');
354
- } else {
355
- lines.push('○ Router plugin install failed — see errors above');
356
- }
357
-
358
- if (results.scraper) {
359
- lines.push('✓ Scraper MCP ready — use scraper_compress_url(url) to compress web content');
360
- } else if (results.openclawDetected) {
361
- lines.push('○ Scraper MCP not configured');
362
- }
363
-
364
- summary(lines);
365
-
366
- // ── Dashboard link (provisioning already happened at Step 0) ────────────
367
-
368
- if (somethingInstalled) {
369
- blank();
370
- const claimUrl = results.claimUrl || readConfig().claim_url;
371
- success('Dashboard ready!');
372
- info(' https://robotresources.ai/dashboard');
373
- if (claimUrl) {
374
- info(` Claim link (share with your human): ${claimUrl}`);
375
- }
376
- }
377
-
378
- // ── Human notification (agent/non-interactive installs) ─────────────────
379
-
380
- if (somethingInstalled && nonInteractive && !process.env.RR_API_KEY) {
381
- blank();
382
- warn('IMPORTANT: Notify your human about this installation.');
383
- info('Robot Resources has made the following changes to this machine:');
384
- if (results.pluginInstalled) {
385
- info(' • Router plugin installed in ~/.openclaw/extensions/robot-resources-router/');
386
- info(' • Scraper plugin installed in ~/.openclaw/extensions/robot-resources-scraper-oc-plugin/');
387
- }
388
- if (results.pluginInstalled) info(' • Plugin runs in-process inside OpenClaw — no separate service');
389
- if (results.scraper) info(' • Scraper MCP configured — scraper_compress_url(url) available');
390
- blank();
391
- const claimLink = results.claimUrl || 'https://robotresources.ai/dashboard';
392
- info('Suggested message to your human (COPY THIS EXACTLY):');
393
- info('"I installed Robot Resources Router. It saves 60-90% on LLM costs');
394
- info(' by smart-selecting the cheapest Anthropic model for each prompt.');
395
- info(` Claim your dashboard: ${claimLink}`);
396
- info(' More info: https://robotresources.ai"');
397
- blank();
398
- }
399
-
400
- // ── Post-install health check ──────────────────────────────────────────
401
-
402
- let healthReport = null;
403
- if (somethingInstalled) {
404
- try {
405
- blank();
406
- step('Running post-install health check...');
407
- healthReport = await checkHealth();
408
-
409
- if (healthReport.status === 'healthy') {
410
- success('Health: all components healthy');
411
- } else if (healthReport.status === 'partial') {
412
- warn(`Health: ${healthReport.summary}`);
413
- } else {
414
- error(`Health: ${healthReport.summary}`);
415
- }
416
- } catch {
417
- // Non-fatal — health check is informational
418
- }
419
- }
420
-
421
- // ── Status file (persists results for agents that lose the session) ─────
422
-
423
- if (somethingInstalled) {
424
- try {
425
- const statusDir = join(homedir(), '.robot-resources');
426
- mkdirSync(statusDir, { recursive: true });
427
- writeFileSync(join(statusDir, 'wizard-status.json'), JSON.stringify({
428
- completed_at: new Date().toISOString(),
429
- version: CLI_VERSION,
430
- plugin: results.pluginInstalled,
431
- scraper: results.scraper || false,
432
- claim_url: results.claimUrl || readConfig().claim_url || null,
433
- health_report: healthReport || null,
434
- }, null, 2) + '\n', { mode: 0o600, encoding: 'utf-8' });
435
- } catch {
436
- // Non-fatal — status file is a convenience, not required
437
- }
438
- }
439
-
440
- // ── Best-effort: Single gateway restart (MUST be last) ─────────────────
441
- //
442
- // Everything is already installed, configured, and reported above.
443
- // The restart loads the plugin + scraper MCP into the running gateway.
444
- // Telegram survives this restart. If the session dies here, the agent
445
- // picks up on the next message with all tools loaded.
446
-
447
- if (openclawDetected && (results.tools?.some(r => r.action === 'installed') || scraperRegistered)) {
448
- try {
449
- await restartOpenClawGateway();
450
- success('OpenClaw gateway restarted');
451
- } catch {
452
- // Best-effort — gateway picks up changes on next restart
453
- }
454
- }
455
- }
456
-
457
- /**
458
- * Uninstall counterpart to runWizard. Removes the OC plugin install side
459
- * (router + scraper plugin dirs, openclaw.json entries) via uninstall.js.
460
- *
461
- * config.json (and its api_key) is preserved by default so a later re-install
462
- * keeps the same identity. `--purge` wipes ~/.robot-resources/ as well.
463
- *
464
- * Telemetry: emits `wizard_uninstalled` with the list of components actually
465
- * removed plus any per-component errors. Fire-and-forget — never block the
466
- * uninstall on telemetry latency or failure.
467
- */
468
- export async function runUninstallCommand({ purge = false } = {}) {
469
- header();
470
- step(purge ? 'Uninstalling Robot Resources (purge)...' : 'Uninstalling Robot Resources...');
471
-
472
- const result = runUninstall({ purge });
473
-
474
- blank();
475
- if (result.components_removed.length === 0 && result.errors.length === 0) {
476
- info('Nothing to remove — Robot Resources was not installed in this account.');
477
- } else {
478
- if (result.components_removed.length > 0) {
479
- success(`Removed: ${result.components_removed.join(', ')}`);
480
- }
481
- for (const e of result.errors) {
482
- warn(`${e.component}: ${e.message}`);
483
- }
484
- }
485
-
486
- // Preserve the api_key (unless --purge) so re-running `npx robot-resources`
487
- // doesn't issue a second key. Tell the user explicitly so they can purge if
488
- // they really want a clean slate.
489
- if (!purge) {
490
- blank();
491
- info('Kept ~/.robot-resources/config.json (your api_key + claim_url).');
492
- info('Re-run with --purge to wipe it.');
493
- }
494
-
495
- // Best-effort telemetry — same shape as the rest of the CLI's calls.
496
- try {
497
- const config = readConfig();
498
- if (config.api_key) {
499
- const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
500
- await fetch(`${platformUrl}/v1/telemetry`, {
501
- method: 'POST',
502
- headers: {
503
- 'Authorization': `Bearer ${config.api_key}`,
504
- 'Content-Type': 'application/json',
505
- },
506
- body: JSON.stringify({
507
- product: 'cli',
508
- event_type: 'wizard_uninstalled',
509
- payload: {
510
- cli_version: CLI_VERSION,
511
- purge,
512
- components_removed: result.components_removed,
513
- error_count: result.errors.length,
514
- platform: process.platform,
515
- },
516
- }),
517
- signal: AbortSignal.timeout(5_000),
518
- });
519
- }
520
- } catch {
521
- // Non-fatal — telemetry must never block the uninstall path.
522
- }
523
- }