robot-resources 1.15.2 → 1.15.3

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,504 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { createRequire } from 'node:module';
3
- import { readFileSync, writeFileSync, copyFileSync, cpSync, mkdirSync, existsSync, rmSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { join, dirname } from 'node:path';
6
- import { isOpenClawInstalled, isOpenClawPluginInstalled, isScraperOcPluginInstalled, getOpenClawAuthMode, isClaudeCodeInstalled, isCursorInstalled } from './detect.js';
7
- import { stripJson5 } from './json5.js';
8
-
9
- /**
10
- * Read openclaw.json, creating it with a minimal structure if it doesn't exist.
11
- * Returns parsed config object. Throws on malformed JSON (caller handles).
12
- */
13
- function readOrCreateOpenClawConfig() {
14
- const configDir = join(homedir(), '.openclaw');
15
- const configPath = join(configDir, 'openclaw.json');
16
-
17
- if (!existsSync(configPath)) {
18
- mkdirSync(configDir, { recursive: true });
19
- const minimal = {};
20
- writeFileSync(configPath, JSON.stringify(minimal, null, 2) + '\n', 'utf-8');
21
- return minimal;
22
- }
23
-
24
- const raw = readFileSync(configPath, 'utf-8');
25
- return JSON.parse(stripJson5(raw));
26
- }
27
-
28
- /**
29
- * Trust the Robot Resources plugin in OpenClaw config.
30
- *
31
- * Adds "robot-resources-router" to plugins.allow so OpenClaw loads it without
32
- * provenance warnings. The plugin's before_model_resolve hook intercepts
33
- * ALL LLM calls regardless of the default model — no need to change the
34
- * default model (which causes LiveSessionModelSwitchError in OC).
35
- *
36
- * Returns true if the config was updated, false otherwise.
37
- */
38
- function trustPlugin() {
39
- const configPath = join(homedir(), '.openclaw', 'openclaw.json');
40
-
41
- try {
42
- const config = readOrCreateOpenClawConfig();
43
-
44
- if (!config.plugins) config.plugins = {};
45
- if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
46
-
47
- if (config.plugins.allow.includes('robot-resources-router')) {
48
- return false;
49
- }
50
-
51
- config.plugins.allow.push('robot-resources-router');
52
-
53
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
54
- return true;
55
- } catch {
56
- return false;
57
- }
58
- }
59
-
60
- /**
61
- * Register scraper-mcp as an MCP server in openclaw.json.
62
- *
63
- * This makes scraper_compress_url and scraper_crawl_url available
64
- * as native tools in OpenClaw. The plugin's before_tool_call hook
65
- * then intercepts web_fetch to route through the scraper by default.
66
- *
67
- * Returns true if the config was updated, false otherwise.
68
- */
69
- function registerScraperMcp() {
70
- const configPath = join(homedir(), '.openclaw', 'openclaw.json');
71
-
72
- try {
73
- const config = readOrCreateOpenClawConfig();
74
-
75
- if (!config.mcp) config.mcp = {};
76
- if (!config.mcp.servers) config.mcp.servers = {};
77
-
78
- // Already registered
79
- if (config.mcp.servers['robot-resources-scraper']) {
80
- return false;
81
- }
82
-
83
- config.mcp.servers['robot-resources-scraper'] = {
84
- command: 'npx',
85
- args: ['-y', '-p', '@robot-resources/scraper', 'scraper-mcp'],
86
- };
87
-
88
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
89
- return true;
90
- } catch {
91
- // Config missing or malformed — non-fatal
92
- return false;
93
- }
94
- }
95
-
96
- /**
97
- * Copy the bundled plugin files to ~/.openclaw/extensions/robot-resources-router/.
98
- *
99
- * The plugin ships as a CLI dependency (@robot-resources/router — the
100
- * router IS the OC plugin in the in-process architecture).
101
- * Instead of spawning `openclaw plugins install` (30s npm overhead),
102
- * we copy files directly. Same destination, same result.
103
- *
104
- * The plugin is a thin shim (index.js) that imports the rest
105
- * of its code from ./lib/*.js — copy the lib/ directory too, or the shim
106
- * fails to load with MODULE_NOT_FOUND.
107
- */
108
- function installPluginFiles() {
109
- const require = createRequire(import.meta.url);
110
- const pluginPkgPath = require.resolve('@robot-resources/router/package.json');
111
- const pluginDir = dirname(pluginPkgPath);
112
-
113
- const targetDir = join(homedir(), '.openclaw', 'extensions', 'robot-resources-router');
114
- mkdirSync(targetDir, { recursive: true });
115
-
116
- for (const file of ['index.js', 'openclaw.plugin.json', 'package.json']) {
117
- copyFileSync(join(pluginDir, file), join(targetDir, file));
118
- }
119
-
120
- // Copy lib/ recursively. Clear the destination first so files removed in
121
- // a new version don't linger from a previous install.
122
- const srcLib = join(pluginDir, 'lib');
123
- const dstLib = join(targetDir, 'lib');
124
- if (existsSync(srcLib)) {
125
- rmSync(dstLib, { recursive: true, force: true });
126
- cpSync(srcLib, dstLib, { recursive: true });
127
- }
128
- }
129
-
130
- /**
131
- * Register the plugin in openclaw.json so OC loads it on gateway start.
132
- * Adds plugins.entries.robot-resources-router = { enabled: true }.
133
- */
134
- function registerPluginEntry() {
135
- const configPath = join(homedir(), '.openclaw', 'openclaw.json');
136
-
137
- try {
138
- const config = readOrCreateOpenClawConfig();
139
-
140
- if (!config.plugins) config.plugins = {};
141
- if (!config.plugins.entries) config.plugins.entries = {};
142
-
143
- // Already registered
144
- if (config.plugins.entries['robot-resources-router']) return;
145
-
146
- config.plugins.entries['robot-resources-router'] = { enabled: true };
147
-
148
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
149
- } catch {
150
- // Non-fatal — plugin may still auto-load from extensions dir
151
- }
152
- }
153
-
154
- /**
155
- * Trust the scraper OC plugin in OpenClaw config.
156
- *
157
- * Adds "robot-resources-scraper-oc-plugin" to plugins.allow so OpenClaw
158
- * loads it without provenance warnings. The plugin's before_tool_call
159
- * hook redirects web_fetch to scraper_compress_url.
160
- *
161
- * Returns true if the config was updated, false otherwise.
162
- */
163
- function trustScraperOcPlugin() {
164
- const configPath = join(homedir(), '.openclaw', 'openclaw.json');
165
-
166
- try {
167
- const config = readOrCreateOpenClawConfig();
168
-
169
- if (!config.plugins) config.plugins = {};
170
- if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
171
-
172
- if (config.plugins.allow.includes('robot-resources-scraper-oc-plugin')) {
173
- return false;
174
- }
175
-
176
- config.plugins.allow.push('robot-resources-scraper-oc-plugin');
177
-
178
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
179
- return true;
180
- } catch {
181
- return false;
182
- }
183
- }
184
-
185
- /**
186
- * Copy the bundled scraper OC plugin files to
187
- * ~/.openclaw/extensions/robot-resources-scraper-oc-plugin/.
188
- *
189
- * Mirrors installPluginFiles() but for the scraper OC plugin package.
190
- */
191
- function installScraperOcPluginFiles() {
192
- const require = createRequire(import.meta.url);
193
- // OC plugin lives as a subfolder inside the scraper package post-consolidation.
194
- const scraperPkgPath = require.resolve('@robot-resources/scraper/package.json');
195
- const pluginDir = join(dirname(scraperPkgPath), 'oc-plugin');
196
-
197
- const targetDir = join(homedir(), '.openclaw', 'extensions', 'robot-resources-scraper-oc-plugin');
198
- mkdirSync(targetDir, { recursive: true });
199
-
200
- for (const file of ['index.js', 'openclaw.plugin.json', 'package.json']) {
201
- copyFileSync(join(pluginDir, file), join(targetDir, file));
202
- }
203
-
204
- // Copy lib/ recursively. Clear destination first so files removed in
205
- // a new version don't linger from a previous install.
206
- const srcLib = join(pluginDir, 'lib');
207
- const dstLib = join(targetDir, 'lib');
208
- if (existsSync(srcLib)) {
209
- rmSync(dstLib, { recursive: true, force: true });
210
- cpSync(srcLib, dstLib, { recursive: true });
211
- }
212
- }
213
-
214
- /**
215
- * Register the scraper OC plugin in openclaw.json so OC loads it on
216
- * gateway start. Adds plugins.entries['robot-resources-scraper-oc-plugin'] = { enabled: true }.
217
- */
218
- function registerScraperOcPluginEntry() {
219
- const configPath = join(homedir(), '.openclaw', 'openclaw.json');
220
-
221
- try {
222
- const config = readOrCreateOpenClawConfig();
223
-
224
- if (!config.plugins) config.plugins = {};
225
- if (!config.plugins.entries) config.plugins.entries = {};
226
-
227
- if (config.plugins.entries['robot-resources-scraper-oc-plugin']) return;
228
-
229
- config.plugins.entries['robot-resources-scraper-oc-plugin'] = { enabled: true };
230
-
231
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
232
- } catch {
233
- // Non-fatal — plugin may still auto-load from extensions dir
234
- }
235
- }
236
-
237
- /**
238
- * Configure OpenClaw to route through Robot Resources Router.
239
- *
240
- * Copies the bundled @robot-resources/router files into
241
- * ~/.openclaw/extensions/. The plugin uses before_model_resolve to
242
- * override the provider — survives gateway restarts because it
243
- * lives in ~/.openclaw/extensions/, not in openclaw.json.
244
- *
245
- * Auth mode detection:
246
- * - subscription (OAuth token): Plugin is REQUIRED. Anthropic rejects
247
- * OAuth tokens from third-party clients, so HTTP proxy won't work.
248
- * - apikey: Plugin is preferred (survives restarts) but proxy also works.
249
- */
250
- function configureOpenClaw() {
251
- const authMode = getOpenClawAuthMode();
252
-
253
- const routerWasInstalled = isOpenClawPluginInstalled();
254
- const scraperWasInstalled = isScraperOcPluginInstalled();
255
-
256
- if (routerWasInstalled && scraperWasInstalled) {
257
- return {
258
- name: 'OpenClaw',
259
- action: 'already_configured',
260
- authMode,
261
- };
262
- }
263
-
264
- try {
265
- let configActivated = false;
266
-
267
- if (!routerWasInstalled) {
268
- installPluginFiles();
269
- registerPluginEntry();
270
- configActivated = trustPlugin();
271
- }
272
-
273
- if (!scraperWasInstalled) {
274
- installScraperOcPluginFiles();
275
- registerScraperOcPluginEntry();
276
- // OR-combine so configActivated reflects "any plugin entry was added to allow".
277
- configActivated = trustScraperOcPlugin() || configActivated;
278
- }
279
-
280
- return {
281
- name: 'OpenClaw',
282
- action: 'installed',
283
- authMode,
284
- configActivated,
285
- note: authMode === 'subscription'
286
- ? 'Plugin required — subscription OAuth tokens are rejected by Anthropic when proxied via third-party clients.'
287
- : undefined,
288
- };
289
- } catch {
290
- // Plugin file copy failed — fall back to instructions
291
- const instructions = [
292
- 'Could not auto-install plugin. Install manually:',
293
- ' openclaw plugins install @robot-resources/router',
294
- ];
295
-
296
- if (authMode === 'subscription') {
297
- instructions.push(
298
- 'IMPORTANT: Subscription mode detected (OAuth token).',
299
- 'The plugin is required — HTTP proxy cannot forward OAuth tokens.',
300
- 'Anthropic rejects OAuth tokens from third-party clients.',
301
- );
302
- }
303
-
304
- instructions.push('Docs: https://github.com/robot-resources/packages');
305
-
306
- return {
307
- name: 'OpenClaw',
308
- action: 'instructions',
309
- authMode,
310
- instructions,
311
- };
312
- }
313
- }
314
-
315
- /**
316
- * Generate copy-pasteable SDK configuration instructions.
317
- *
318
- * Returned when no AI tools are auto-detected. Gives the developer
319
- * exactly what they need to point their SDK at the router manually.
320
- */
321
- function printManualInstructions() {
322
- return {
323
- name: 'Manual Configuration',
324
- action: 'instructions',
325
- instructions: [
326
- 'No AI tools detected for auto-configuration.',
327
- 'Point your SDK at the Router by setting the base URL:',
328
- '',
329
- ' # OpenAI SDK / compatible clients (include /v1 in the URL)',
330
- ' export OPENAI_BASE_URL=http://localhost:3838/v1',
331
- ' # OpenAI(base_url="http://localhost:3838/v1")',
332
- '',
333
- ' # Anthropic SDK (NO /v1 — the SDK appends /v1/messages itself)',
334
- ' export ANTHROPIC_BASE_URL=http://localhost:3838',
335
- ' # Anthropic(base_url="http://localhost:3838")',
336
- '',
337
- ' # Google / Gemini: native SDK is NOT supported via base_url.',
338
- ' # Use the OpenAI-compatible client with a Gemini model name:',
339
- ' # OpenAI(base_url="http://localhost:3838/v1")',
340
- ' # model = "gemini-2.5-flash"',
341
- '',
342
- 'Docs: https://github.com/robot-resources/packages',
343
- ],
344
- };
345
- }
346
-
347
- /**
348
- * Configure Claude Code to use the Router as an MCP server.
349
- *
350
- * Writes a robot-resources-router entry to ~/.claude/settings.json
351
- * under the mcpServers key. Claude Code reads this on startup.
352
- */
353
- function configureClaudeCode() {
354
- const configPath = join(homedir(), '.claude', 'settings.json');
355
-
356
- try {
357
- let config = {};
358
- if (existsSync(configPath)) {
359
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
360
- }
361
-
362
- if (!config.mcpServers) config.mcpServers = {};
363
-
364
- if (config.mcpServers['robot-resources-router']) {
365
- return { name: 'Claude Code', action: 'already_configured' };
366
- }
367
-
368
- config.mcpServers['robot-resources-router'] = {
369
- command: 'npx',
370
- args: ['-y', '@robot-resources/router', 'mcp'],
371
- };
372
-
373
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
374
- return { name: 'Claude Code', action: 'configured' };
375
- } catch {
376
- return {
377
- name: 'Claude Code',
378
- action: 'instructions',
379
- instructions: [
380
- 'Could not auto-configure Claude Code. Add manually to ~/.claude/settings.json:',
381
- ' "mcpServers": { "robot-resources-router": { "command": "npx", "args": ["-y", "@robot-resources/router", "mcp"] } }',
382
- ],
383
- };
384
- }
385
- }
386
-
387
- /**
388
- * Configure Cursor to use the Router as an MCP server.
389
- *
390
- * Writes a robot-resources-router entry to ~/.cursor/mcp.json
391
- * under the mcpServers key. Cursor reads this on startup.
392
- */
393
- function configureCursor() {
394
- const configPath = join(homedir(), '.cursor', 'mcp.json');
395
-
396
- try {
397
- let config = {};
398
- if (existsSync(configPath)) {
399
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
400
- }
401
-
402
- if (!config.mcpServers) config.mcpServers = {};
403
-
404
- if (config.mcpServers['robot-resources-router']) {
405
- return { name: 'Cursor', action: 'already_configured' };
406
- }
407
-
408
- config.mcpServers['robot-resources-router'] = {
409
- command: 'npx',
410
- args: ['-y', '@robot-resources/router', 'mcp'],
411
- };
412
-
413
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
414
- return { name: 'Cursor', action: 'configured' };
415
- } catch {
416
- return {
417
- name: 'Cursor',
418
- action: 'instructions',
419
- instructions: [
420
- 'Could not auto-configure Cursor. Add manually to ~/.cursor/mcp.json:',
421
- ' "mcpServers": { "robot-resources-router": { "command": "npx", "args": ["-y", "@robot-resources/router", "mcp"] } }',
422
- ],
423
- };
424
- }
425
- }
426
-
427
- /**
428
- * Configure all detected AI tools to route through the Router.
429
- *
430
- * Returns array of { name, action, ... } results.
431
- * When no tools are detected, returns manual SDK instructions.
432
- */
433
- export function configureToolRouting() {
434
- const results = [];
435
-
436
- // OpenClaw
437
- if (isOpenClawInstalled()) {
438
- results.push(configureOpenClaw());
439
- }
440
-
441
- // Claude Code
442
- if (isClaudeCodeInstalled()) {
443
- results.push(configureClaudeCode());
444
- }
445
-
446
- // Cursor
447
- if (isCursorInstalled()) {
448
- results.push(configureCursor());
449
- }
450
-
451
- // Fallback: manual SDK instructions when no tools detected
452
- if (results.length === 0) {
453
- results.push(printManualInstructions());
454
- }
455
-
456
- return results;
457
- }
458
-
459
- /**
460
- * Run a command with a heartbeat to keep agent sessions alive.
461
- * OC kills processes after 5s of no output (noOutputTimeoutMs = 5000).
462
- * Prints immediately, then every 4s (safely under the 5s threshold).
463
- */
464
- function spawnWithHeartbeat(cmd, args, { label, timeout = 30_000 } = {}) {
465
- return new Promise((resolve, reject) => {
466
- const proc = spawn(cmd, args, {
467
- stdio: ['ignore', 'pipe', 'pipe'],
468
- timeout,
469
- });
470
-
471
- process.stdout.write(` ${label}...\n`);
472
- let seconds = 0;
473
- const heartbeat = setInterval(() => {
474
- seconds += 4;
475
- process.stdout.write(` ${label}... ${seconds}s\n`);
476
- }, 4000);
477
-
478
- proc.on('close', (code) => {
479
- clearInterval(heartbeat);
480
- if (code === 0) resolve();
481
- else reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
482
- });
483
-
484
- proc.on('error', (err) => {
485
- clearInterval(heartbeat);
486
- reject(err);
487
- });
488
- });
489
- }
490
-
491
- /**
492
- * Restart the OpenClaw gateway so it picks up new plugin + config.
493
- * Uses heartbeat to keep OC sessions alive during the restart.
494
- * Telegram survives this restart — tested end-to-end (PR #89).
495
- */
496
- async function restartOpenClawGateway() {
497
- await spawnWithHeartbeat('openclaw', ['gateway', 'restart'], {
498
- label: 'Restarting gateway',
499
- timeout: 15_000,
500
- });
501
- }
502
-
503
- // Exported for testing and direct use
504
- export { stripJson5, configureOpenClaw, configureClaudeCode, configureCursor, registerScraperMcp, restartOpenClawGateway };
package/lib/ui.js DELETED
@@ -1,87 +0,0 @@
1
- import { createInterface } from 'node:readline';
2
-
3
- // ANSI color helpers (no dependencies)
4
- const c = {
5
- reset: '\x1b[0m',
6
- bold: '\x1b[1m',
7
- dim: '\x1b[2m',
8
- green: '\x1b[32m',
9
- yellow: '\x1b[33m',
10
- red: '\x1b[31m',
11
- cyan: '\x1b[36m',
12
- orange: '\x1b[38;5;208m',
13
- };
14
-
15
- export function header() {
16
- console.log(`\n ${c.orange}${c.bold}██ Robot Resources — Setup${c.reset}\n`);
17
- }
18
-
19
- export function step(msg) {
20
- console.log(` ${c.cyan}→${c.reset} ${msg}`);
21
- }
22
-
23
- export function success(msg) {
24
- console.log(` ${c.green}✓${c.reset} ${msg}`);
25
- }
26
-
27
- export function warn(msg) {
28
- console.log(` ${c.yellow}!${c.reset} ${msg}`);
29
- }
30
-
31
- export function error(msg) {
32
- console.log(` ${c.red}✗${c.reset} ${msg}`);
33
- }
34
-
35
- export function info(msg) {
36
- console.log(` ${c.dim}${msg}${c.reset}`);
37
- }
38
-
39
- export function blank() {
40
- console.log('');
41
- }
42
-
43
- export function summary(lines) {
44
- console.log(`\n ${c.orange}${c.bold}── Summary ──${c.reset}\n`);
45
- for (const line of lines) {
46
- console.log(` ${line}`);
47
- }
48
- console.log('');
49
- }
50
-
51
- /**
52
- * Prompt for free-text input (e.g. API keys).
53
- * Returns the trimmed answer, or empty string if skipped.
54
- * In non-interactive mode, returns the default value.
55
- */
56
- export function prompt(question, { defaultValue = '', nonInteractive = false } = {}) {
57
- if (nonInteractive) return Promise.resolve(defaultValue);
58
-
59
- const rl = createInterface({ input: process.stdin, output: process.stdout });
60
-
61
- return new Promise((resolve) => {
62
- rl.question(` ${question}: `, (answer) => {
63
- rl.close();
64
- resolve(answer.trim());
65
- });
66
- });
67
- }
68
-
69
- /**
70
- * Prompt for yes/no confirmation. Returns true for yes.
71
- * In non-interactive mode, returns the default value.
72
- */
73
- export function confirm(question, { defaultYes = true, nonInteractive = false } = {}) {
74
- if (nonInteractive) return Promise.resolve(defaultYes);
75
-
76
- const hint = defaultYes ? 'Y/n' : 'y/N';
77
- const rl = createInterface({ input: process.stdin, output: process.stdout });
78
-
79
- return new Promise((resolve) => {
80
- rl.question(` ${question} (${hint}): `, (answer) => {
81
- rl.close();
82
- const trimmed = answer.trim().toLowerCase();
83
- if (trimmed === '') resolve(defaultYes);
84
- else resolve(trimmed === 'y' || trimmed === 'yes');
85
- });
86
- });
87
- }