robot-resources 1.5.1 → 1.6.0

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,13 +1,15 @@
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
 
8
9
  /**
9
- * Run a command with a heartbeat that prints progress every 5 seconds.
10
- * Prevents agent sessions from timing out during silent operations.
10
+ * 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).
11
13
  */
12
14
  function spawnWithHeartbeat(cmd, args, { label, timeout = 30_000 } = {}) {
13
15
  return new Promise((resolve, reject) => {
@@ -16,11 +18,12 @@ function spawnWithHeartbeat(cmd, args, { label, timeout = 30_000 } = {}) {
16
18
  timeout,
17
19
  });
18
20
 
21
+ process.stdout.write(` ${label}...\n`);
19
22
  let seconds = 0;
20
23
  const heartbeat = setInterval(() => {
21
- seconds += 5;
24
+ seconds += 4;
22
25
  process.stdout.write(` ${label}... ${seconds}s\n`);
23
- }, 5000);
26
+ }, 4000);
24
27
 
25
28
  proc.on('close', (code) => {
26
29
  clearInterval(heartbeat);
@@ -110,11 +113,56 @@ function registerScraperMcp() {
110
113
  }
111
114
  }
112
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
+
113
161
  /**
114
162
  * Configure OpenClaw to route through Robot Resources Router.
115
163
  *
116
- * Installs the @robot-resources/openclaw-plugin via OpenClaw's
117
- * 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
118
166
  * override the provider — survives gateway restarts because it
119
167
  * lives in ~/.openclaw/extensions/, not in openclaw.json.
120
168
  *
@@ -123,7 +171,7 @@ function registerScraperMcp() {
123
171
  * OAuth tokens from third-party clients, so HTTP proxy won't work.
124
172
  * - apikey: Plugin is preferred (survives restarts) but proxy also works.
125
173
  */
126
- async function configureOpenClaw() {
174
+ function configureOpenClaw() {
127
175
  const authMode = getOpenClawAuthMode();
128
176
 
129
177
  if (isOpenClawPluginInstalled()) {
@@ -135,39 +183,26 @@ async function configureOpenClaw() {
135
183
  }
136
184
 
137
185
  try {
138
- await spawnWithHeartbeat('openclaw', ['plugins', 'install', '@robot-resources/openclaw-plugin'], {
139
- label: 'Installing plugin',
140
- timeout: 30_000,
141
- });
186
+ installPluginFiles();
187
+
188
+ // Register plugin in openclaw.json so OC loads it on gateway start.
189
+ registerPluginEntry();
142
190
 
143
191
  // Set robot-resources/auto as the default model so the plugin's
144
192
  // before_model_resolve hook actually fires for every request.
145
193
  const configActivated = activateRouterModel();
146
194
 
147
- // Restart the gateway so it picks up the new plugin + config.
148
- let gatewayRestarted = false;
149
- try {
150
- await spawnWithHeartbeat('openclaw', ['gateway', 'restart'], {
151
- label: 'Restarting gateway',
152
- timeout: 15_000,
153
- });
154
- gatewayRestarted = true;
155
- } catch {
156
- // Non-fatal — gateway may not be running or command unsupported
157
- }
158
-
159
195
  return {
160
196
  name: 'OpenClaw',
161
197
  action: 'installed',
162
198
  authMode,
163
199
  configActivated,
164
- gatewayRestarted,
165
200
  note: authMode === 'subscription'
166
201
  ? 'Plugin required — subscription OAuth tokens are rejected by Anthropic when proxied via third-party clients.'
167
202
  : undefined,
168
203
  };
169
204
  } catch {
170
- // Plugin install failed — fall back to instructions
205
+ // Plugin file copy failed — fall back to instructions
171
206
  const instructions = [
172
207
  'Could not auto-install plugin. Install manually:',
173
208
  ' openclaw plugins install @robot-resources/openclaw-plugin',
@@ -197,16 +232,27 @@ async function configureOpenClaw() {
197
232
  *
198
233
  * Returns array of { name, action, ... } results.
199
234
  */
200
- export async function configureToolRouting() {
235
+ export function configureToolRouting() {
201
236
  const results = [];
202
237
 
203
238
  // OpenClaw
204
239
  if (isOpenClawInstalled()) {
205
- results.push(await configureOpenClaw());
240
+ results.push(configureOpenClaw());
206
241
  }
207
242
 
208
243
  return results;
209
244
  }
210
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
+
211
257
  // Exported for testing and direct use
212
- 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,75 +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
- let seconds = 0;
247
- const hb = setInterval(() => {
248
- seconds += 5;
249
- process.stdout.write(` Restarting gateway... ${seconds}s\n`);
250
- }, 5000);
251
- proc.on('close', (code) => { clearInterval(hb); code === 0 ? resolve() : reject(new Error(`exit ${code}`)); });
252
- proc.on('error', (err) => { clearInterval(hb); reject(err); });
253
- });
254
- success('OpenClaw gateway restarted');
255
- results.scraper = true;
256
- } catch {
257
- // Non-fatal — gateway may not be running
258
- }
259
- }
260
-
261
- // ── Step 4.5: Healthchecks ────────────────────────────────────────────
262
-
263
- // Scraper MCP: verify the server starts without crashing
264
- if (results.scraper) {
265
- step('Verifying Scraper MCP starts...');
266
- try {
267
- const { spawn } = await import('node:child_process');
268
- const proc = spawn('npx', ['-y', '-p', '@robot-resources/scraper', 'scraper-mcp'], {
269
- stdio: ['pipe', 'pipe', 'pipe'],
270
- timeout: 10_000,
271
- });
272
-
273
- const healthy = await new Promise((resolve) => {
274
- const timer = setTimeout(() => {
275
- // Server stayed alive for 3s — it's working
276
- proc.kill();
277
- resolve(true);
278
- }, 3000);
279
-
280
- proc.on('error', () => { clearTimeout(timer); resolve(false); });
281
- proc.on('exit', (code) => {
282
- clearTimeout(timer);
283
- // MCP servers don't exit on their own — if it exited, it crashed
284
- resolve(code === 0);
285
- });
286
- });
287
-
288
- if (healthy) {
289
- success('Scraper MCP server healthy');
290
- } else {
291
- warn('Scraper MCP server exited unexpectedly');
292
- results.scraper = false;
293
- }
294
- } catch {
295
- warn('Could not verify Scraper MCP server');
296
- }
297
- }
236
+ // ── Step 4.5: Router Healthcheck ──────────────────────────────────────
298
237
 
299
238
  // Router: verify it's responding on localhost:3838
300
239
  if (results.service) {
@@ -384,4 +323,20 @@ export async function runWizard({ nonInteractive = false } = {}) {
384
323
  info(' More info: https://robotresources.ai"');
385
324
  blank();
386
325
  }
326
+
327
+ // ── Best-effort: Single gateway restart ────────────────────────────────
328
+ //
329
+ // Merged from two previous restarts (after plugin install + after scraper
330
+ // registration). Everything is already installed and registered above.
331
+ // If the session dies during this restart, the gateway picks up changes
332
+ // on its next natural restart.
333
+
334
+ if (isOpenClawInstalled() && (results.tools?.some(r => r.action === 'installed') || scraperRegistered)) {
335
+ try {
336
+ await restartOpenClawGateway();
337
+ success('OpenClaw gateway restarted');
338
+ } catch {
339
+ // Best-effort — gateway picks up changes on next restart
340
+ }
341
+ }
387
342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
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": {