robot-resources 1.5.2 → 1.6.1

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.
@@ -1,7 +1,8 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'node:fs';
3
4
  import { homedir } from 'node:os';
4
- import { join } from 'node:path';
5
+ import { join, dirname } from 'node:path';
5
6
  import { isOpenClawInstalled, isOpenClawPluginInstalled, getOpenClawAuthMode } from './detect.js';
6
7
  import { stripJson5 } from './json5.js';
7
8
 
@@ -112,11 +113,56 @@ function registerScraperMcp() {
112
113
  }
113
114
  }
114
115
 
116
+ /**
117
+ * Copy the bundled plugin files to ~/.openclaw/extensions/openclaw-plugin/.
118
+ *
119
+ * The plugin ships as a CLI dependency (@robot-resources/openclaw-plugin).
120
+ * Instead of spawning `openclaw plugins install` (30s npm overhead),
121
+ * we copy the 3 files directly. Same destination, same result.
122
+ */
123
+ function installPluginFiles() {
124
+ const require = createRequire(import.meta.url);
125
+ const pluginPkgPath = require.resolve('@robot-resources/openclaw-plugin/package.json');
126
+ const pluginDir = dirname(pluginPkgPath);
127
+
128
+ const targetDir = join(homedir(), '.openclaw', 'extensions', 'openclaw-plugin');
129
+ mkdirSync(targetDir, { recursive: true });
130
+
131
+ for (const file of ['index.js', 'openclaw.plugin.json', 'package.json']) {
132
+ copyFileSync(join(pluginDir, file), join(targetDir, file));
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Register the plugin in openclaw.json so OC loads it on gateway start.
138
+ * Adds plugins.entries.openclaw-plugin = { enabled: true }.
139
+ */
140
+ function registerPluginEntry() {
141
+ const configPath = join(homedir(), '.openclaw', 'openclaw.json');
142
+
143
+ try {
144
+ const raw = readFileSync(configPath, 'utf-8');
145
+ const config = JSON.parse(stripJson5(raw));
146
+
147
+ if (!config.plugins) config.plugins = {};
148
+ if (!config.plugins.entries) config.plugins.entries = {};
149
+
150
+ // Already registered
151
+ if (config.plugins.entries['openclaw-plugin']) return;
152
+
153
+ config.plugins.entries['openclaw-plugin'] = { enabled: true };
154
+
155
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
156
+ } catch {
157
+ // Non-fatal — plugin may still auto-load from extensions dir
158
+ }
159
+ }
160
+
115
161
  /**
116
162
  * Configure OpenClaw to route through Robot Resources Router.
117
163
  *
118
- * Installs the @robot-resources/openclaw-plugin via OpenClaw's
119
- * native plugin system. The plugin uses before_model_resolve to
164
+ * Copies the bundled @robot-resources/openclaw-plugin files into
165
+ * ~/.openclaw/extensions/. The plugin uses before_model_resolve to
120
166
  * override the provider — survives gateway restarts because it
121
167
  * lives in ~/.openclaw/extensions/, not in openclaw.json.
122
168
  *
@@ -125,7 +171,7 @@ function registerScraperMcp() {
125
171
  * OAuth tokens from third-party clients, so HTTP proxy won't work.
126
172
  * - apikey: Plugin is preferred (survives restarts) but proxy also works.
127
173
  */
128
- async function configureOpenClaw() {
174
+ function configureOpenClaw() {
129
175
  const authMode = getOpenClawAuthMode();
130
176
 
131
177
  if (isOpenClawPluginInstalled()) {
@@ -137,39 +183,26 @@ async function configureOpenClaw() {
137
183
  }
138
184
 
139
185
  try {
140
- await spawnWithHeartbeat('openclaw', ['plugins', 'install', '@robot-resources/openclaw-plugin'], {
141
- label: 'Installing plugin',
142
- timeout: 30_000,
143
- });
186
+ installPluginFiles();
187
+
188
+ // Register plugin in openclaw.json so OC loads it on gateway start.
189
+ registerPluginEntry();
144
190
 
145
191
  // Set robot-resources/auto as the default model so the plugin's
146
192
  // before_model_resolve hook actually fires for every request.
147
193
  const configActivated = activateRouterModel();
148
194
 
149
- // Restart the gateway so it picks up the new plugin + config.
150
- let gatewayRestarted = false;
151
- try {
152
- await spawnWithHeartbeat('openclaw', ['gateway', 'restart'], {
153
- label: 'Restarting gateway',
154
- timeout: 15_000,
155
- });
156
- gatewayRestarted = true;
157
- } catch {
158
- // Non-fatal — gateway may not be running or command unsupported
159
- }
160
-
161
195
  return {
162
196
  name: 'OpenClaw',
163
197
  action: 'installed',
164
198
  authMode,
165
199
  configActivated,
166
- gatewayRestarted,
167
200
  note: authMode === 'subscription'
168
201
  ? 'Plugin required — subscription OAuth tokens are rejected by Anthropic when proxied via third-party clients.'
169
202
  : undefined,
170
203
  };
171
204
  } catch {
172
- // Plugin install failed — fall back to instructions
205
+ // Plugin file copy failed — fall back to instructions
173
206
  const instructions = [
174
207
  'Could not auto-install plugin. Install manually:',
175
208
  ' openclaw plugins install @robot-resources/openclaw-plugin',
@@ -199,16 +232,27 @@ async function configureOpenClaw() {
199
232
  *
200
233
  * Returns array of { name, action, ... } results.
201
234
  */
202
- export async function configureToolRouting() {
235
+ export function configureToolRouting() {
203
236
  const results = [];
204
237
 
205
238
  // OpenClaw
206
239
  if (isOpenClawInstalled()) {
207
- results.push(await configureOpenClaw());
240
+ results.push(configureOpenClaw());
208
241
  }
209
242
 
210
243
  return results;
211
244
  }
212
245
 
246
+ /**
247
+ * Restart the OpenClaw gateway so it picks up new plugin + config.
248
+ * Uses heartbeat to keep OC sessions alive during the restart.
249
+ */
250
+ async function restartOpenClawGateway() {
251
+ await spawnWithHeartbeat('openclaw', ['gateway', 'restart'], {
252
+ label: 'Restarting gateway',
253
+ timeout: 15_000,
254
+ });
255
+ }
256
+
213
257
  // Exported for testing and direct use
214
- export { stripJson5, configureOpenClaw, registerScraperMcp };
258
+ export { stripJson5, configureOpenClaw, registerScraperMcp, restartOpenClawGateway };
package/lib/wizard.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { readConfig, writeConfig } from '@robot-resources/cli-core/config.mjs';
2
- import { findPython, isPortAvailable, isHeadless } from './detect.js';
2
+ import { findPython, isPortAvailable, isHeadless, isOpenClawInstalled } from './detect.js';
3
3
  import { setupRouter, isRouterInstalled, getVenvPythonPath } from './python-bridge.js';
4
4
  import { installService, isServiceRunning, isServiceInstalled } from './service.js';
5
- import { configureToolRouting, registerScraperMcp } from './tool-config.js';
5
+ import { configureToolRouting, registerScraperMcp, restartOpenClawGateway } from './tool-config.js';
6
6
  import { header, step, success, warn, error, info, blank, summary } from './ui.js';
7
7
  /**
8
8
  * Main setup wizard. Handles the full onboarding flow:
@@ -172,7 +172,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
172
172
  blank();
173
173
  step('Configuring AI tools to use Router...');
174
174
 
175
- const toolResults = await configureToolRouting();
175
+ const toolResults = configureToolRouting();
176
176
  results.tools = toolResults;
177
177
 
178
178
  if (toolResults.length === 0) {
@@ -187,7 +187,6 @@ export async function runWizard({ nonInteractive = false } = {}) {
187
187
  } else if (r.action === 'installed') {
188
188
  success(`${r.name}: plugin installed`);
189
189
  if (r.configActivated) success(`${r.name}: default model set to robot-resources/auto`);
190
- if (r.gatewayRestarted) success(`${r.name}: gateway restarted`);
191
190
  if (r.note) info(` ${r.note}`);
192
191
  } else if (r.action === 'instructions') {
193
192
  warn(`${r.name}: manual configuration needed:`);
@@ -204,19 +203,20 @@ export async function runWizard({ nonInteractive = false } = {}) {
204
203
  // ── Step 4: Scraper Installation ───────────────────────────────────────
205
204
  //
206
205
  // Independent of router. Scraper works even if router failed to install.
207
- // 1. Register scraper MCP in openclaw.json (if OC is present)
208
- // 2. Restart gateway so OC picks up the new MCP server
209
- // No pre-cache needed — scraper is bundled as a CLI dependency
206
+ // Register scraper MCP in openclaw.json (if OC is present).
207
+ // Gateway restart happens once at the very end (merged with plugin restart).
210
208
 
211
209
  blank();
212
210
  step('Installing Scraper...');
213
211
 
214
212
  results.scraper = false;
213
+ let scraperRegistered = false;
215
214
 
216
215
  // Register MCP in openclaw.json
217
- const scraperRegistered = registerScraperMcp();
216
+ scraperRegistered = registerScraperMcp();
218
217
  if (scraperRegistered) {
219
218
  success('Scraper MCP registered in OpenClaw — scraper_compress_url(url) available');
219
+ results.scraper = true;
220
220
  } else {
221
221
  // Either already registered, or no openclaw.json
222
222
  try {
@@ -226,76 +226,14 @@ export async function runWizard({ nonInteractive = false } = {}) {
226
226
  const ocConfig = JSON.parse(readFs(joinPath(home(), '.openclaw', 'openclaw.json'), 'utf-8'));
227
227
  if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
228
228
  success('Scraper MCP already registered in OpenClaw');
229
+ results.scraper = true;
229
230
  }
230
231
  } catch {
231
232
  // No openclaw.json — not on OC, skip
232
233
  }
233
234
  }
234
235
 
235
- results.scraper = true;
236
-
237
- // Restart gateway so OC picks up the scraper MCP server
238
- if (scraperRegistered) {
239
- try {
240
- const { spawn: spawnProc } = await import('node:child_process');
241
- await new Promise((resolve, reject) => {
242
- const proc = spawnProc('openclaw', ['gateway', 'restart'], {
243
- stdio: ['ignore', 'pipe', 'pipe'],
244
- timeout: 15_000,
245
- });
246
- process.stdout.write(' Restarting gateway...\n');
247
- let seconds = 0;
248
- const hb = setInterval(() => {
249
- seconds += 4;
250
- process.stdout.write(` Restarting gateway... ${seconds}s\n`);
251
- }, 4000);
252
- proc.on('close', (code) => { clearInterval(hb); code === 0 ? resolve() : reject(new Error(`exit ${code}`)); });
253
- proc.on('error', (err) => { clearInterval(hb); reject(err); });
254
- });
255
- success('OpenClaw gateway restarted');
256
- results.scraper = true;
257
- } catch {
258
- // Non-fatal — gateway may not be running
259
- }
260
- }
261
-
262
- // ── Step 4.5: Healthchecks ────────────────────────────────────────────
263
-
264
- // Scraper MCP: verify the server starts without crashing
265
- if (results.scraper) {
266
- step('Verifying Scraper MCP starts...');
267
- try {
268
- const { spawn } = await import('node:child_process');
269
- const proc = spawn('npx', ['-y', '-p', '@robot-resources/scraper', 'scraper-mcp'], {
270
- stdio: ['pipe', 'pipe', 'pipe'],
271
- timeout: 10_000,
272
- });
273
-
274
- const healthy = await new Promise((resolve) => {
275
- const timer = setTimeout(() => {
276
- // Server stayed alive for 3s — it's working
277
- proc.kill();
278
- resolve(true);
279
- }, 3000);
280
-
281
- proc.on('error', () => { clearTimeout(timer); resolve(false); });
282
- proc.on('exit', (code) => {
283
- clearTimeout(timer);
284
- // MCP servers don't exit on their own — if it exited, it crashed
285
- resolve(code === 0);
286
- });
287
- });
288
-
289
- if (healthy) {
290
- success('Scraper MCP server healthy');
291
- } else {
292
- warn('Scraper MCP server exited unexpectedly');
293
- results.scraper = false;
294
- }
295
- } catch {
296
- warn('Could not verify Scraper MCP server');
297
- }
298
- }
236
+ // ── Step 4.5: Router Healthcheck ──────────────────────────────────────
299
237
 
300
238
  // Router: verify it's responding on localhost:3838
301
239
  if (results.service) {
@@ -385,4 +323,44 @@ export async function runWizard({ nonInteractive = false } = {}) {
385
323
  info(' More info: https://robotresources.ai"');
386
324
  blank();
387
325
  }
326
+
327
+ // ── Status file (persists results for agents that lose the session) ─────
328
+
329
+ if (somethingInstalled) {
330
+ try {
331
+ const { readFileSync: readFs, writeFileSync: writeFs, mkdirSync: mkFs } = await import('node:fs');
332
+ const { join: joinP } = await import('node:path');
333
+ const { homedir: homeP } = await import('node:os');
334
+ const statusDir = joinP(homeP(), '.robot-resources');
335
+ mkFs(statusDir, { recursive: true });
336
+ const pkgVersion = JSON.parse(readFs(new URL('../package.json', import.meta.url), 'utf-8')).version;
337
+ writeFs(joinP(statusDir, 'wizard-status.json'), JSON.stringify({
338
+ completed_at: new Date().toISOString(),
339
+ version: pkgVersion,
340
+ router: results.router || false,
341
+ service: results.service || false,
342
+ scraper: results.scraper || false,
343
+ plugin: results.tools?.some(r => r.action === 'installed') || false,
344
+ claim_url: results.claimUrl || readConfig().claim_url || null,
345
+ }, null, 2) + '\n', 'utf-8');
346
+ } catch {
347
+ // Non-fatal — status file is a convenience, not required
348
+ }
349
+ }
350
+
351
+ // ── Best-effort: Single gateway restart ────────────────────────────────
352
+ //
353
+ // Merged from two previous restarts (after plugin install + after scraper
354
+ // registration). Everything is already installed and registered above.
355
+ // If the session dies during this restart, the gateway picks up changes
356
+ // on its next natural restart.
357
+
358
+ if (isOpenClawInstalled() && (results.tools?.some(r => r.action === 'installed') || scraperRegistered)) {
359
+ try {
360
+ await restartOpenClawGateway();
361
+ success('OpenClaw gateway restarted');
362
+ } catch {
363
+ // Best-effort — gateway picks up changes on next restart
364
+ }
365
+ }
388
366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "description": "Robot Resources — AI agent runtime tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "@robot-resources/cli-core": "*",
21
+ "@robot-resources/openclaw-plugin": "*",
21
22
  "@robot-resources/scraper": "^0.2.0"
22
23
  },
23
24
  "devDependencies": {