robot-resources 1.7.0 → 1.7.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
@@ -1,73 +1,105 @@
1
- # robot-resources — Unified Installer
1
+ # Robot Resources
2
2
 
3
- One command to install all Robot Resources tools:
3
+ > Tools for AI agents. Humans have HR. Agents have RR.
4
4
 
5
- ```
5
+ Robot Resources builds tools for AI agents and any software that makes LLM API calls — chatbots, RAG pipelines, AI-powered apps, internal tools. Two products today: **Router** (intelligent model routing, 60-90% cost savings) and **Scraper** (token compression for web content, median 91% token reduction). Both run locally, both free.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
6
10
  npx robot-resources
7
11
  ```
8
12
 
9
- ## What the wizard does
10
-
11
- 1. **GitHub OAuth** — authenticates via PKCE, saves API key to `~/.robot-resources/config.json`
12
- 2. **Router install** — detects Python 3.10+, creates venv, pip installs `robot-resources-router`
13
- 3. **Service registration** — registers router as launchd (macOS) or systemd (Linux) service
14
- 4. **Scraper** — installs @robot-resources/scraper for token-efficient web compression
13
+ One command: authenticates via GitHub OAuth, installs Router as an always-on service, registers Scraper as the default web compression tool, and configures your agent automatically.
15
14
 
16
- ## Adding a new tool
15
+ ## Products
17
16
 
18
- When you build a new product for RR, follow this checklist:
17
+ ### Router
19
18
 
20
- ### 1. Create the npx entry point
19
+ Intelligent LLM routing proxy. Classifies each prompt by task type, filters by model capability, routes to the cheapest model that qualifies.
21
20
 
22
- Your tool needs an npm package with a `bin` entry that runs a setup wizard. Two patterns:
21
+ - Hybrid classification: keyword detection (~5ms) + LLM fallback for ambiguous prompts (~200ms)
22
+ - Dynamic thresholds: simple tasks open cheap models (0.60), complex tasks require top models (0.85)
23
+ - 11 models across OpenAI, Anthropic, and Google — routes within your available providers
24
+ - 60-90% cost savings with zero quality loss
25
+ - OpenAI-compatible API on localhost:3838 — change your base_url and you're done
26
+ - OpenClaw compatible — plugin auto-installs, works with API keys and subscriptions
23
27
 
24
- - **Node.js tool:** Add `bin` to your package.json pointing to a setup script
25
- - **Python tool:** Create a thin npm wrapper (like `router/packages/router/`) that handles pip install
28
+ ```bash
29
+ npx @robot-resources/router
30
+ ```
26
31
 
27
- The wizard should:
28
- - Check `~/.robot-resources/config.json` — offer login if missing
29
- - Install the tool
30
- - Configure MCP if applicable
31
- - Print usage instructions
32
+ ### Scraper
32
33
 
33
- ### 2. Update the unified wizard
34
+ Token compression for web content. Fetches any URL, strips noise, returns clean markdown with token count.
34
35
 
35
- Edit `packages/cli/lib/wizard.js` to add your tool as a new step between service registration and MCP config.
36
+ - Mozilla Readability extraction (0.97 F1 accuracy)
37
+ - Content-aware token estimation (±15% of actual BPE)
38
+ - 3-tier fetch: fast, stealth (TLS fingerprint), render (headless browser)
39
+ - Multi-page BFS crawl with robots.txt compliance
40
+ - Median 91% token reduction per page
36
41
 
37
- ### 3. Update the landing page
42
+ Installed automatically via `npx robot-resources`. Available as MCP tool `scraper_compress_url(url)` in your agent.
38
43
 
39
- | File | What to change |
40
- |------|---------------|
41
- | `web/src/pages/Landing/types.ts` | Add install subsection if new section |
42
- | `web/src/pages/Landing/components/ContentPanel/animatedContentScreens.ts` | Add install screen (desktop + mobile) |
43
- | `web/src/pages/Landing/components/TerminalPanel/terminalCommands.ts` | Add install subcommand output |
44
+ ### Dashboard
44
45
 
45
- ### 4. Update agent-facing content
46
+ Usage dashboard with real-time telemetry. Auth via GitHub OAuth, KPI panels, routing stats, cost savings tracking. Mobile-responsive.
46
47
 
47
- | File | What to change |
48
- |------|---------------|
49
- | `web/public/llms.txt` | Add tool with `npx @robot-resources/<tool>` |
50
- | `web/public/llms-full.txt` | Add detailed section with npx install |
51
- | `web/public/.well-known/ai-resources.json` | Add SoftwareApplication entry with InstallAction |
48
+ ### MCP Servers
52
49
 
53
- ### 5. Update the publish workflow
50
+ Both products include MCP servers for AI agent integration:
54
51
 
55
- Add your package.json path to `.github/workflows/publish.yml` triggers and add a publish step.
52
+ ```bash
53
+ npx -y @robot-resources/router-mcp # Router stats + config
54
+ npx -y @robot-resources/scraper-mcp # Scraper compression
55
+ ```
56
56
 
57
- ### 6. Add to root workspaces
57
+ ## Pricing
58
58
 
59
- Add your package directory to the `workspaces` array in the root `package.json`.
59
+ Free. Unlimited. No tiers. Both tools run locally — you pay your AI providers directly. No markup, no rate limits, no quotas. Your API keys never leave your machine.
60
60
 
61
- ## Architecture
61
+ ## Monorepo Structure
62
62
 
63
63
  ```
64
- packages/cli-core/ Shared auth, config, login (private — never published)
65
- packages/cli/ This package (published as "robot-resources" on npm)
66
- bin/setup.js Entry point
67
- lib/wizard.js Orchestrator add new tools here
68
- lib/service.js launchd + systemd service registration
69
- lib/python-bridge.js Python venv management
70
- lib/detect.js Environment detection
71
- lib/tool-config.js OpenClaw plugin + model activation
72
- lib/ui.js Terminal formatting
64
+ robot-resources/
65
+ ├── router/ # LLM routing proxy (Python + TypeScript MCP)
66
+ ├── scraper/ # Token compression (TypeScript)
67
+ ├── web/ # Landing page + dashboard (React/Vite)
68
+ ├── platform/ # Backend API auth, telemetry (Hono/Cloudflare Workers)
69
+ ├── packages/ # Unified CLI installer (npx robot-resources)
70
+ ├── skills/ # Agent skills (ClawHub)
71
+ ├── _orchestrator/ # Business coordination
72
+ └── _brand/ # Brand assets and design system
73
73
  ```
74
+
75
+ ## Development
76
+
77
+ Each product has its own setup:
78
+
79
+ - [Router](./router/README.md)
80
+ - [Scraper](./scraper/packages/scraper/README.md)
81
+ - [Platform](./platform/README.md)
82
+ - [CLI](./packages/cli/README.md)
83
+
84
+ ## CI/CD
85
+
86
+ Unified pipeline on push/PR to main:
87
+
88
+ - **Router**: ruff, mypy, pytest
89
+ - **Scraper**: tsc, vitest
90
+ - **Web**: tsc, eslint
91
+ - **Publish**: 7 npm packages via OIDC (`router`, `router-mcp`, `scraper`, `scraper-mcp`, `scraper-tracking`, `cli-core`, `cli`)
92
+
93
+ ## Links
94
+
95
+ - Website: https://robotresources.ai
96
+ - GitHub: https://github.com/robot-resources
97
+ - npm: `npx robot-resources`
98
+ - Twitter/X: https://x.com/robotresources
99
+ - Discord: https://robotresources.ai/discord
100
+ - Contact: agent@robotresources.ai
101
+ - Agent docs: https://robotresources.ai/llms.txt
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,124 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readConfig } from '@robot-resources/cli-core/config.mjs';
5
+
6
+ const PROBE_TIMEOUT_MS = 5_000;
7
+
8
+ /**
9
+ * Run post-install health checks against all Robot Resources components.
10
+ *
11
+ * Probes:
12
+ * 1. Router — GET http://127.0.0.1:3838/health
13
+ * 2. Scraper — check openclaw.json for scraper MCP registration
14
+ * 3. Platform — GET {platformUrl}/v1/health with api_key
15
+ * 4. MCP — check openclaw.json for openclaw-plugin registration
16
+ *
17
+ * @returns {{ status: 'healthy'|'partial'|'failed', components: Object, summary: string }}
18
+ */
19
+ export async function checkHealth() {
20
+ const config = readConfig();
21
+
22
+ const [router, scraper, platform, mcp] = await Promise.all([
23
+ probeRouter(),
24
+ probeScraper(),
25
+ probePlatform(config),
26
+ probeMcp(),
27
+ ]);
28
+
29
+ const components = { router, scraper, platform, mcp };
30
+ const healthyCount = Object.values(components).filter((c) => c.healthy).length;
31
+ const total = Object.keys(components).length;
32
+
33
+ let status;
34
+ if (healthyCount === total) {
35
+ status = 'healthy';
36
+ } else if (healthyCount === 0) {
37
+ status = 'failed';
38
+ } else {
39
+ status = 'partial';
40
+ }
41
+
42
+ const failing = Object.entries(components)
43
+ .filter(([, c]) => !c.healthy)
44
+ .map(([name, c]) => `${name}: ${c.detail}`);
45
+
46
+ const summary =
47
+ status === 'healthy'
48
+ ? `All ${total} components healthy.`
49
+ : `${healthyCount}/${total} healthy. Issues: ${failing.join('; ')}`;
50
+
51
+ return { status, components, summary };
52
+ }
53
+
54
+ async function probeRouter() {
55
+ try {
56
+ if (typeof fetch === 'undefined') {
57
+ return { healthy: false, detail: 'fetch unavailable' };
58
+ }
59
+ const res = await fetch('http://127.0.0.1:3838/health', {
60
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
61
+ });
62
+ if (!res.ok) {
63
+ return { healthy: false, detail: `HTTP ${res.status}` };
64
+ }
65
+ const data = await res.json();
66
+ if (data.status === 'healthy' || data.status === 'degraded') {
67
+ return { healthy: true, detail: `running (v${data.version || 'unknown'})` };
68
+ }
69
+ return { healthy: false, detail: `status: ${data.status}` };
70
+ } catch (err) {
71
+ const detail = err.name === 'AbortError' ? 'timeout' : 'unreachable';
72
+ return { healthy: false, detail };
73
+ }
74
+ }
75
+
76
+ function probeScraper() {
77
+ try {
78
+ const ocPath = join(homedir(), '.openclaw', 'openclaw.json');
79
+ const ocConfig = JSON.parse(readFileSync(ocPath, 'utf-8'));
80
+ const hasServer = !!ocConfig?.mcp?.servers?.['robot-resources-scraper'];
81
+ return {
82
+ healthy: hasServer,
83
+ detail: hasServer ? 'MCP registered' : 'scraper MCP not registered',
84
+ };
85
+ } catch {
86
+ return { healthy: false, detail: 'openclaw.json not found' };
87
+ }
88
+ }
89
+
90
+ async function probePlatform(config) {
91
+ if (!config.api_key) {
92
+ return { healthy: false, detail: 'no API key configured' };
93
+ }
94
+ try {
95
+ if (typeof fetch === 'undefined') {
96
+ return { healthy: false, detail: 'fetch unavailable' };
97
+ }
98
+ const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
99
+ const res = await fetch(`${platformUrl}/health`, {
100
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
101
+ });
102
+ return {
103
+ healthy: res.ok,
104
+ detail: res.ok ? 'reachable' : `HTTP ${res.status}`,
105
+ };
106
+ } catch (err) {
107
+ const detail = err.name === 'AbortError' ? 'timeout' : 'unreachable';
108
+ return { healthy: false, detail };
109
+ }
110
+ }
111
+
112
+ function probeMcp() {
113
+ try {
114
+ const ocPath = join(homedir(), '.openclaw', 'openclaw.json');
115
+ const ocConfig = JSON.parse(readFileSync(ocPath, 'utf-8'));
116
+ const hasPlugin = !!ocConfig?.plugins?.entries?.['openclaw-plugin']?.enabled;
117
+ return {
118
+ healthy: hasPlugin,
119
+ detail: hasPlugin ? 'plugin registered' : 'openclaw-plugin not registered',
120
+ };
121
+ } catch {
122
+ return { healthy: false, detail: 'openclaw.json not found' };
123
+ }
124
+ }
@@ -0,0 +1,31 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+
6
+ /**
7
+ * Reads or creates a persistent machine identifier.
8
+ * Used for telemetry deduplication across sessions.
9
+ *
10
+ * @param {string} [configDir] - Directory to store .machine-id (defaults to ~/.robot-resources)
11
+ * @returns {string} A UUID v4 machine identifier
12
+ */
13
+ export function getOrCreateMachineId(configDir) {
14
+ const dir = configDir ?? join(homedir(), '.robot-resources');
15
+ const machineIdPath = join(dir, '.machine-id');
16
+
17
+ try {
18
+ const stored = readFileSync(machineIdPath, 'utf-8').trim();
19
+ if (stored) return stored;
20
+ } catch {
21
+ // File doesn't exist or can't be read — fall through to generate
22
+ }
23
+
24
+ const machineId = randomUUID();
25
+ try {
26
+ mkdirSync(dir, { recursive: true });
27
+ writeFileSync(machineIdPath, machineId, 'utf-8');
28
+ } catch { /* non-fatal */ }
29
+
30
+ return machineId;
31
+ }
@@ -1,15 +1,19 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createRequire } from 'node:module';
3
- import { readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'node:fs';
3
+ import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { isOpenClawInstalled, isOpenClawPluginInstalled, getOpenClawAuthMode } from './detect.js';
7
7
  import { stripJson5 } from './json5.js';
8
8
 
9
+ /**
10
+ * Heartbeat interval in ms. OC kills processes after 5s of no output
11
+ * (noOutputTimeoutMs = 5000). This must stay under that threshold.
12
+ */
13
+ const HEARTBEAT_INTERVAL_MS = 4000;
14
+
9
15
  /**
10
16
  * Run a command with a heartbeat to keep agent sessions alive.
11
- * OC kills processes after 5s of no output (noOutputTimeoutMs).
12
- * Prints immediately, then every 4s (safely under the 5s threshold).
13
17
  */
14
18
  function spawnWithHeartbeat(cmd, args, { label, timeout = 30_000 } = {}) {
15
19
  return new Promise((resolve, reject) => {
@@ -21,9 +25,9 @@ function spawnWithHeartbeat(cmd, args, { label, timeout = 30_000 } = {}) {
21
25
  process.stdout.write(` ${label}...\n`);
22
26
  let seconds = 0;
23
27
  const heartbeat = setInterval(() => {
24
- seconds += 4;
28
+ seconds += HEARTBEAT_INTERVAL_MS / 1000;
25
29
  process.stdout.write(` ${label}... ${seconds}s\n`);
26
- }, 4000);
30
+ }, HEARTBEAT_INTERVAL_MS);
27
31
 
28
32
  proc.on('close', (code) => {
29
33
  clearInterval(heartbeat);
@@ -39,39 +43,52 @@ function spawnWithHeartbeat(cmd, args, { label, timeout = 30_000 } = {}) {
39
43
  }
40
44
 
41
45
  /**
42
- * Set robot-resources/auto as the default model in openclaw.json.
46
+ * Read openclaw.json, creating it with a minimal structure if it doesn't exist.
47
+ * Returns parsed config object. Throws on malformed JSON (caller handles).
48
+ */
49
+ function readOrCreateOpenClawConfig() {
50
+ const configDir = join(homedir(), '.openclaw');
51
+ const configPath = join(configDir, 'openclaw.json');
52
+
53
+ if (!existsSync(configPath)) {
54
+ mkdirSync(configDir, { recursive: true });
55
+ const minimal = {};
56
+ writeFileSync(configPath, JSON.stringify(minimal, null, 2) + '\n', 'utf-8');
57
+ return minimal;
58
+ }
59
+
60
+ const raw = readFileSync(configPath, 'utf-8');
61
+ return JSON.parse(stripJson5(raw));
62
+ }
63
+
64
+ /**
65
+ * Trust the Robot Resources plugin in OpenClaw config.
43
66
  *
44
- * This is required so the plugin's before_model_resolve hook fires.
45
- * Without it, OpenClaw sends requests directly to Anthropic and the
46
- * plugin never gets a chance to route.
67
+ * Adds "openclaw-plugin" to plugins.allow so OpenClaw loads it without
68
+ * provenance warnings. The plugin's before_model_resolve hook intercepts
69
+ * ALL LLM calls regardless of the default model — no need to change the
70
+ * default model (which causes LiveSessionModelSwitchError in OC).
47
71
  *
48
72
  * Returns true if the config was updated, false otherwise.
49
73
  */
50
- function activateRouterModel() {
74
+ function trustPlugin() {
51
75
  const configPath = join(homedir(), '.openclaw', 'openclaw.json');
52
76
 
53
77
  try {
54
- const raw = readFileSync(configPath, 'utf-8');
55
- const config = JSON.parse(stripJson5(raw));
56
-
57
- // Ensure agents.defaults.model exists
58
- if (!config.agents) config.agents = {};
59
- if (!config.agents.defaults) config.agents.defaults = {};
60
- if (!config.agents.defaults.model) config.agents.defaults.model = {};
78
+ const config = readOrCreateOpenClawConfig();
61
79
 
62
- const currentPrimary = config.agents.defaults.model.primary;
80
+ if (!config.plugins) config.plugins = {};
81
+ if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
63
82
 
64
- // Only change if not already pointing at robot-resources
65
- if (currentPrimary && currentPrimary.startsWith('robot-resources/')) {
83
+ if (config.plugins.allow.includes('openclaw-plugin')) {
66
84
  return false;
67
85
  }
68
86
 
69
- config.agents.defaults.model.primary = 'robot-resources/auto';
87
+ config.plugins.allow.push('openclaw-plugin');
70
88
 
71
89
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
72
90
  return true;
73
91
  } catch {
74
- // Config missing or malformed — non-fatal
75
92
  return false;
76
93
  }
77
94
  }
@@ -89,8 +106,7 @@ function registerScraperMcp() {
89
106
  const configPath = join(homedir(), '.openclaw', 'openclaw.json');
90
107
 
91
108
  try {
92
- const raw = readFileSync(configPath, 'utf-8');
93
- const config = JSON.parse(stripJson5(raw));
109
+ const config = readOrCreateOpenClawConfig();
94
110
 
95
111
  if (!config.mcp) config.mcp = {};
96
112
  if (!config.mcp.servers) config.mcp.servers = {};
@@ -141,8 +157,7 @@ function registerPluginEntry() {
141
157
  const configPath = join(homedir(), '.openclaw', 'openclaw.json');
142
158
 
143
159
  try {
144
- const raw = readFileSync(configPath, 'utf-8');
145
- const config = JSON.parse(stripJson5(raw));
160
+ const config = readOrCreateOpenClawConfig();
146
161
 
147
162
  if (!config.plugins) config.plugins = {};
148
163
  if (!config.plugins.entries) config.plugins.entries = {};
@@ -188,9 +203,8 @@ function configureOpenClaw() {
188
203
  // Register plugin in openclaw.json so OC loads it on gateway start.
189
204
  registerPluginEntry();
190
205
 
191
- // Set robot-resources/auto as the default model so the plugin's
192
- // before_model_resolve hook actually fires for every request.
193
- const configActivated = activateRouterModel();
206
+ // Trust the plugin so OC loads it without provenance warnings.
207
+ const configActivated = trustPlugin();
194
208
 
195
209
  return {
196
210
  name: 'OpenClaw',
@@ -245,13 +259,30 @@ export function configureToolRouting() {
245
259
 
246
260
  /**
247
261
  * Restart the OpenClaw gateway so it picks up new plugin + config.
248
- * Uses heartbeat to keep OC sessions alive during the restart.
262
+ * Retries up to 3 times with 2s backoff gateway restart MUST succeed
263
+ * for MCP tools to be available. Silent failure = tools broken for user.
249
264
  */
250
265
  async function restartOpenClawGateway() {
251
- await spawnWithHeartbeat('openclaw', ['gateway', 'restart'], {
252
- label: 'Restarting gateway',
253
- timeout: 15_000,
254
- });
266
+ const MAX_RETRIES = 3;
267
+ const BACKOFF_MS = 2000;
268
+ let lastError;
269
+
270
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
271
+ try {
272
+ await spawnWithHeartbeat('openclaw', ['gateway', 'restart'], {
273
+ label: `Restarting gateway${attempt > 1 ? ` (attempt ${attempt}/${MAX_RETRIES})` : ''}`,
274
+ timeout: 15_000,
275
+ });
276
+ return; // success
277
+ } catch (err) {
278
+ lastError = err;
279
+ if (attempt < MAX_RETRIES) {
280
+ await new Promise(r => setTimeout(r, BACKOFF_MS));
281
+ }
282
+ }
283
+ }
284
+
285
+ throw lastError;
255
286
  }
256
287
 
257
288
  // Exported for testing and direct use
package/lib/wizard.js CHANGED
@@ -1,8 +1,13 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir, hostname } from 'node:os';
1
4
  import { readConfig, writeConfig } from '@robot-resources/cli-core/config.mjs';
2
5
  import { findPython, isPortAvailable, isHeadless, isOpenClawInstalled } from './detect.js';
6
+ import { getOrCreateMachineId } from './machine-id.js';
3
7
  import { setupRouter, isRouterInstalled, getVenvPythonPath } from './python-bridge.js';
4
8
  import { installService, isServiceRunning, isServiceInstalled } from './service.js';
5
9
  import { configureToolRouting, registerScraperMcp, restartOpenClawGateway } from './tool-config.js';
10
+ import { checkHealth } from './health-report.js';
6
11
  import { header, step, success, warn, error, info, blank, summary } from './ui.js';
7
12
  /**
8
13
  * Main setup wizard. Handles the full onboarding flow:
@@ -36,24 +41,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
36
41
  const config = readConfig();
37
42
  if (!config.api_key && !process.env.RR_API_KEY) {
38
43
  try {
39
- const { hostname } = await import('node:os');
40
- const { join } = await import('node:path');
41
- const { homedir } = await import('node:os');
42
- const { readFileSync, writeFileSync, mkdirSync } = await import('node:fs');
43
- const { randomUUID } = await import('node:crypto');
44
-
45
- const rrDir = join(homedir(), '.robot-resources');
46
- const machineIdPath = join(rrDir, '.machine-id');
47
- let machineId;
48
- try {
49
- machineId = readFileSync(machineIdPath, 'utf-8').trim();
50
- } catch {
51
- machineId = randomUUID();
52
- try {
53
- mkdirSync(rrDir, { recursive: true });
54
- writeFileSync(machineIdPath, machineId, 'utf-8');
55
- } catch { /* non-fatal */ }
56
- }
44
+ const machineId = getOrCreateMachineId();
57
45
 
58
46
  const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
59
47
  const res = await fetch(`${platformUrl}/v1/auth/signup`, {
@@ -184,7 +172,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
184
172
  success(`${r.name}: already configured`);
185
173
  } else if (r.action === 'installed') {
186
174
  success(`${r.name}: plugin installed`);
187
- if (r.configActivated) success(`${r.name}: default model set to robot-resources/auto`);
175
+ if (r.configActivated) success(`${r.name}: plugin trusted in openclaw.json`);
188
176
  if (r.note) info(` ${r.note}`);
189
177
  } else if (r.action === 'instructions') {
190
178
  warn(`${r.name}: manual configuration needed:`);
@@ -218,10 +206,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
218
206
  } else {
219
207
  // Either already registered, or no openclaw.json
220
208
  try {
221
- const { readFileSync: readFs } = await import('node:fs');
222
- const { join: joinPath } = await import('node:path');
223
- const { homedir: home } = await import('node:os');
224
- const ocConfig = JSON.parse(readFs(joinPath(home(), '.openclaw', 'openclaw.json'), 'utf-8'));
209
+ const ocConfig = JSON.parse(readFileSync(join(homedir(), '.openclaw', 'openclaw.json'), 'utf-8'));
225
210
  if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
226
211
  success('Scraper MCP already registered in OpenClaw');
227
212
  results.scraper = true;
@@ -354,17 +339,52 @@ export async function runWizard({ nonInteractive = false } = {}) {
354
339
  blank();
355
340
  }
356
341
 
342
+ // ── Best-effort: Single gateway restart ────────────────────────────────
343
+ //
344
+ // Merged from two previous restarts (after plugin install + after scraper
345
+ // registration). Everything is already installed and registered above.
346
+ // If the session dies during this restart, the gateway picks up changes
347
+ // on its next natural restart.
348
+
349
+ if (isOpenClawInstalled() && (results.tools?.some(r => r.action === 'installed') || scraperRegistered)) {
350
+ try {
351
+ await restartOpenClawGateway();
352
+ success('OpenClaw gateway restarted');
353
+ } catch {
354
+ warn('Gateway restart failed — tools may not be available until OpenClaw restarts.');
355
+ warn('Run manually: openclaw gateway restart');
356
+ }
357
+ }
358
+
359
+ // ── Post-install health check ──────────────────────────────────────────
360
+
361
+ let healthReport = null;
362
+ if (somethingInstalled) {
363
+ try {
364
+ blank();
365
+ step('Running post-install health check...');
366
+ healthReport = await checkHealth();
367
+
368
+ if (healthReport.status === 'healthy') {
369
+ success(`Health: all components healthy`);
370
+ } else if (healthReport.status === 'partial') {
371
+ warn(`Health: ${healthReport.summary}`);
372
+ } else {
373
+ error(`Health: ${healthReport.summary}`);
374
+ }
375
+ } catch {
376
+ // Non-fatal — health check is informational
377
+ }
378
+ }
379
+
357
380
  // ── Status file (persists results for agents that lose the session) ─────
358
381
 
359
382
  if (somethingInstalled) {
360
383
  try {
361
- const { readFileSync: readFs, writeFileSync: writeFs, mkdirSync: mkFs } = await import('node:fs');
362
- const { join: joinP } = await import('node:path');
363
- const { homedir: homeP } = await import('node:os');
364
- const statusDir = joinP(homeP(), '.robot-resources');
365
- mkFs(statusDir, { recursive: true });
366
- const pkgVersion = JSON.parse(readFs(new URL('../package.json', import.meta.url), 'utf-8')).version;
367
- writeFs(joinP(statusDir, 'wizard-status.json'), JSON.stringify({
384
+ const statusDir = join(homedir(), '.robot-resources');
385
+ mkdirSync(statusDir, { recursive: true });
386
+ const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
387
+ writeFileSync(join(statusDir, 'wizard-status.json'), JSON.stringify({
368
388
  completed_at: new Date().toISOString(),
369
389
  version: pkgVersion,
370
390
  router: results.router || false,
@@ -372,25 +392,10 @@ export async function runWizard({ nonInteractive = false } = {}) {
372
392
  scraper: results.scraper || false,
373
393
  plugin: results.tools?.some(r => r.action === 'installed') || false,
374
394
  claim_url: results.claimUrl || readConfig().claim_url || null,
375
- }, null, 2) + '\n', 'utf-8');
395
+ health_report: healthReport || null,
396
+ }, null, 2) + '\n', { mode: 0o600, encoding: 'utf-8' });
376
397
  } catch {
377
398
  // Non-fatal — status file is a convenience, not required
378
399
  }
379
400
  }
380
-
381
- // ── Best-effort: Single gateway restart ────────────────────────────────
382
- //
383
- // Merged from two previous restarts (after plugin install + after scraper
384
- // registration). Everything is already installed and registered above.
385
- // If the session dies during this restart, the gateway picks up changes
386
- // on its next natural restart.
387
-
388
- if (isOpenClawInstalled() && (results.tools?.some(r => r.action === 'installed') || scraperRegistered)) {
389
- try {
390
- await restartOpenClawGateway();
391
- success('OpenClaw gateway restarted');
392
- } catch {
393
- // Best-effort — gateway picks up changes on next restart
394
- }
395
- }
396
401
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.7.0",
4
- "description": "Robot Resources — AI agent runtime tools. One command to install everything.",
3
+ "version": "1.7.2",
4
+ "description": "Robot Resources — AI agent tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "robot-resources-setup": "./bin/setup.js"