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.
- package/lib/tool-config.js +70 -26
- package/lib/wizard.js +50 -72
- package/package.json +2 -1
package/lib/tool-config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
|
|
@@ -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
|
-
*
|
|
119
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
235
|
+
export function configureToolRouting() {
|
|
203
236
|
const results = [];
|
|
204
237
|
|
|
205
238
|
// OpenClaw
|
|
206
239
|
if (isOpenClawInstalled()) {
|
|
207
|
-
results.push(
|
|
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 =
|
|
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,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
|
-
|
|
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.
|
|
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": {
|