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.
- package/lib/tool-config.js +76 -30
- package/lib/wizard.js +26 -71
- package/package.json +2 -1
package/lib/tool-config.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
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
|
|
10
|
-
*
|
|
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 +=
|
|
24
|
+
seconds += 4;
|
|
22
25
|
process.stdout.write(` ${label}... ${seconds}s\n`);
|
|
23
|
-
},
|
|
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
|
-
*
|
|
117
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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
|
|
235
|
+
export function configureToolRouting() {
|
|
201
236
|
const results = [];
|
|
202
237
|
|
|
203
238
|
// OpenClaw
|
|
204
239
|
if (isOpenClawInstalled()) {
|
|
205
|
-
results.push(
|
|
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 =
|
|
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
|
-
//
|
|
208
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|