robot-resources 1.7.8 → 1.8.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/detect.js +16 -0
- package/lib/service.js +10 -2
- package/lib/tool-config.js +130 -2
- package/lib/wizard.js +68 -17
- package/package.json +4 -1
package/lib/detect.js
CHANGED
|
@@ -139,6 +139,22 @@ export function isHeadless() {
|
|
|
139
139
|
return false;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Check if Claude Code is installed.
|
|
144
|
+
* Looks for ~/.claude/ directory which Claude Code creates on first run.
|
|
145
|
+
*/
|
|
146
|
+
export function isClaudeCodeInstalled() {
|
|
147
|
+
return existsSync(join(homedir(), '.claude'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if Cursor is installed.
|
|
152
|
+
* Looks for ~/.cursor/ directory which Cursor creates on first run.
|
|
153
|
+
*/
|
|
154
|
+
export function isCursorInstalled() {
|
|
155
|
+
return existsSync(join(homedir(), '.cursor'));
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
/**
|
|
143
159
|
* Check if the router service is already registered.
|
|
144
160
|
*/
|
package/lib/service.js
CHANGED
|
@@ -335,8 +335,16 @@ export function installService(venvPythonPath) {
|
|
|
335
335
|
return {
|
|
336
336
|
type: 'skipped',
|
|
337
337
|
reason: 'Running inside Docker — service registration skipped.\n' +
|
|
338
|
-
'
|
|
339
|
-
|
|
338
|
+
' Options:\n' +
|
|
339
|
+
' 1. Dockerfile entrypoint:\n' +
|
|
340
|
+
` CMD ["${venvPythonPath}", "-m", "robot_resources.cli.main", "start"]\n` +
|
|
341
|
+
' 2. Docker Compose sidecar:\n' +
|
|
342
|
+
' services:\n' +
|
|
343
|
+
' router:\n' +
|
|
344
|
+
` command: ${venvPythonPath} -m robot_resources.cli.main start\n` +
|
|
345
|
+
' ports: ["3838:3838"]\n' +
|
|
346
|
+
' 3. Background process:\n' +
|
|
347
|
+
` ${venvPythonPath} -m robot_resources.cli.main start &`,
|
|
340
348
|
};
|
|
341
349
|
}
|
|
342
350
|
|
package/lib/tool-config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
|
-
import { isOpenClawInstalled, isOpenClawPluginInstalled, getOpenClawAuthMode } from './detect.js';
|
|
6
|
+
import { isOpenClawInstalled, isOpenClawPluginInstalled, getOpenClawAuthMode, isClaudeCodeInstalled, isCursorInstalled } from './detect.js';
|
|
7
7
|
import { stripJson5 } from './json5.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -205,10 +205,123 @@ function configureOpenClaw() {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Generate copy-pasteable SDK configuration instructions.
|
|
210
|
+
*
|
|
211
|
+
* Returned when no AI tools are auto-detected. Gives the developer
|
|
212
|
+
* exactly what they need to point their SDK at the router manually.
|
|
213
|
+
*/
|
|
214
|
+
function printManualInstructions() {
|
|
215
|
+
return {
|
|
216
|
+
name: 'Manual Configuration',
|
|
217
|
+
action: 'instructions',
|
|
218
|
+
instructions: [
|
|
219
|
+
'No AI tools detected for auto-configuration.',
|
|
220
|
+
'Point your SDK at the Router by setting the base URL:',
|
|
221
|
+
'',
|
|
222
|
+
' # OpenAI SDK / compatible clients',
|
|
223
|
+
' export OPENAI_BASE_URL=http://localhost:3838/v1',
|
|
224
|
+
'',
|
|
225
|
+
' # Anthropic SDK',
|
|
226
|
+
' export ANTHROPIC_BASE_URL=http://localhost:3838/v1',
|
|
227
|
+
'',
|
|
228
|
+
' # Google Generative AI',
|
|
229
|
+
' export GOOGLE_API_BASE=http://localhost:3838/v1',
|
|
230
|
+
'',
|
|
231
|
+
' # Or pass base_url directly in your code:',
|
|
232
|
+
' # OpenAI(base_url="http://localhost:3838/v1")',
|
|
233
|
+
' # Anthropic(base_url="http://localhost:3838/v1")',
|
|
234
|
+
'',
|
|
235
|
+
'Docs: https://github.com/robot-resources/robot-resources',
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Configure Claude Code to use the Router as an MCP server.
|
|
242
|
+
*
|
|
243
|
+
* Writes a robot-resources-router entry to ~/.claude/settings.json
|
|
244
|
+
* under the mcpServers key. Claude Code reads this on startup.
|
|
245
|
+
*/
|
|
246
|
+
function configureClaudeCode() {
|
|
247
|
+
const configPath = join(homedir(), '.claude', 'settings.json');
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
let config = {};
|
|
251
|
+
if (existsSync(configPath)) {
|
|
252
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
256
|
+
|
|
257
|
+
if (config.mcpServers['robot-resources-router']) {
|
|
258
|
+
return { name: 'Claude Code', action: 'already_configured' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
config.mcpServers['robot-resources-router'] = {
|
|
262
|
+
command: 'npx',
|
|
263
|
+
args: ['-y', '@robot-resources/router', 'mcp'],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
267
|
+
return { name: 'Claude Code', action: 'configured' };
|
|
268
|
+
} catch {
|
|
269
|
+
return {
|
|
270
|
+
name: 'Claude Code',
|
|
271
|
+
action: 'instructions',
|
|
272
|
+
instructions: [
|
|
273
|
+
'Could not auto-configure Claude Code. Add manually to ~/.claude/settings.json:',
|
|
274
|
+
' "mcpServers": { "robot-resources-router": { "command": "npx", "args": ["-y", "@robot-resources/router", "mcp"] } }',
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Configure Cursor to use the Router as an MCP server.
|
|
282
|
+
*
|
|
283
|
+
* Writes a robot-resources-router entry to ~/.cursor/mcp.json
|
|
284
|
+
* under the mcpServers key. Cursor reads this on startup.
|
|
285
|
+
*/
|
|
286
|
+
function configureCursor() {
|
|
287
|
+
const configPath = join(homedir(), '.cursor', 'mcp.json');
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
let config = {};
|
|
291
|
+
if (existsSync(configPath)) {
|
|
292
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
296
|
+
|
|
297
|
+
if (config.mcpServers['robot-resources-router']) {
|
|
298
|
+
return { name: 'Cursor', action: 'already_configured' };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
config.mcpServers['robot-resources-router'] = {
|
|
302
|
+
command: 'npx',
|
|
303
|
+
args: ['-y', '@robot-resources/router', 'mcp'],
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
307
|
+
return { name: 'Cursor', action: 'configured' };
|
|
308
|
+
} catch {
|
|
309
|
+
return {
|
|
310
|
+
name: 'Cursor',
|
|
311
|
+
action: 'instructions',
|
|
312
|
+
instructions: [
|
|
313
|
+
'Could not auto-configure Cursor. Add manually to ~/.cursor/mcp.json:',
|
|
314
|
+
' "mcpServers": { "robot-resources-router": { "command": "npx", "args": ["-y", "@robot-resources/router", "mcp"] } }',
|
|
315
|
+
],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
208
320
|
/**
|
|
209
321
|
* Configure all detected AI tools to route through the Router.
|
|
210
322
|
*
|
|
211
323
|
* Returns array of { name, action, ... } results.
|
|
324
|
+
* When no tools are detected, returns manual SDK instructions.
|
|
212
325
|
*/
|
|
213
326
|
export function configureToolRouting() {
|
|
214
327
|
const results = [];
|
|
@@ -218,6 +331,21 @@ export function configureToolRouting() {
|
|
|
218
331
|
results.push(configureOpenClaw());
|
|
219
332
|
}
|
|
220
333
|
|
|
334
|
+
// Claude Code
|
|
335
|
+
if (isClaudeCodeInstalled()) {
|
|
336
|
+
results.push(configureClaudeCode());
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Cursor
|
|
340
|
+
if (isCursorInstalled()) {
|
|
341
|
+
results.push(configureCursor());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Fallback: manual SDK instructions when no tools detected
|
|
345
|
+
if (results.length === 0) {
|
|
346
|
+
results.push(printManualInstructions());
|
|
347
|
+
}
|
|
348
|
+
|
|
221
349
|
return results;
|
|
222
350
|
}
|
|
223
351
|
|
|
@@ -266,4 +394,4 @@ async function restartOpenClawGateway() {
|
|
|
266
394
|
}
|
|
267
395
|
|
|
268
396
|
// Exported for testing and direct use
|
|
269
|
-
export { stripJson5, configureOpenClaw, registerScraperMcp, restartOpenClawGateway };
|
|
397
|
+
export { stripJson5, configureOpenClaw, configureClaudeCode, configureCursor, registerScraperMcp, restartOpenClawGateway };
|
package/lib/wizard.js
CHANGED
|
@@ -77,6 +77,44 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// ── Funnel marker: wizard_started ───────────────────────────────────────
|
|
81
|
+
//
|
|
82
|
+
// Sent immediately after auth so we have proof the wizard reached this
|
|
83
|
+
// point even if any install step crashes. Pairs with install_complete at
|
|
84
|
+
// the end to give us a "started → completed" funnel for diagnosing silent
|
|
85
|
+
// signups. Fire-and-forget — never fatal.
|
|
86
|
+
//
|
|
87
|
+
// Timeout asymmetry vs install_complete (5s, no retry vs 10s × 2 attempts):
|
|
88
|
+
// wizard_started is a best-effort funnel marker — losing it just means we
|
|
89
|
+
// miss one funnel datapoint. install_complete is a heartbeat that powers
|
|
90
|
+
// last_used_at and the "wizard ran successfully" signal, so it gets the
|
|
91
|
+
// longer timeout + retry to maximize delivery.
|
|
92
|
+
|
|
93
|
+
if (results.auth) {
|
|
94
|
+
try {
|
|
95
|
+
const config = readConfig();
|
|
96
|
+
const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
97
|
+
await fetch(`${platformUrl}/v1/telemetry`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Authorization': `Bearer ${config.api_key}`,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
product: 'cli',
|
|
105
|
+
event_type: 'wizard_started',
|
|
106
|
+
payload: {
|
|
107
|
+
auth_method: results.authMethod,
|
|
108
|
+
non_interactive: nonInteractive,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
signal: AbortSignal.timeout(5_000),
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
// Non-fatal — wizard_started is best-effort
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
// ── Step 1: Router Installation ─────────────────────────────────────────
|
|
81
119
|
|
|
82
120
|
step('Checking Router...');
|
|
@@ -254,29 +292,42 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
254
292
|
//
|
|
255
293
|
// Fire once after install, using the API key directly (not from config read-back).
|
|
256
294
|
// This immediately populates last_used_at and proves the key works end-to-end.
|
|
295
|
+
//
|
|
296
|
+
// Retry once with longer timeout — Cloudflare analytics showed client-side
|
|
297
|
+
// aborts on the original 5s single-attempt, leaving stranded signups with
|
|
298
|
+
// no telemetry. Two 10s attempts catch the long tail. Still fire-and-forget.
|
|
257
299
|
|
|
258
300
|
if (results.auth) {
|
|
259
301
|
try {
|
|
260
302
|
const config = readConfig();
|
|
261
303
|
const platformUrl = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
304
|
+
const body = JSON.stringify({
|
|
305
|
+
product: 'cli',
|
|
306
|
+
event_type: 'install_complete',
|
|
307
|
+
payload: {
|
|
308
|
+
router: results.router || false,
|
|
309
|
+
service: results.service || false,
|
|
310
|
+
scraper: results.scraper || false,
|
|
311
|
+
source: 'wizard',
|
|
267
312
|
},
|
|
268
|
-
body: JSON.stringify({
|
|
269
|
-
product: 'cli',
|
|
270
|
-
event_type: 'install_complete',
|
|
271
|
-
payload: {
|
|
272
|
-
router: results.router || false,
|
|
273
|
-
service: results.service || false,
|
|
274
|
-
scraper: results.scraper || false,
|
|
275
|
-
source: 'wizard',
|
|
276
|
-
},
|
|
277
|
-
}),
|
|
278
|
-
signal: AbortSignal.timeout(5_000),
|
|
279
313
|
});
|
|
314
|
+
|
|
315
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch(`${platformUrl}/v1/telemetry`, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: {
|
|
320
|
+
'Authorization': `Bearer ${config.api_key}`,
|
|
321
|
+
'Content-Type': 'application/json',
|
|
322
|
+
},
|
|
323
|
+
body,
|
|
324
|
+
signal: AbortSignal.timeout(10_000),
|
|
325
|
+
});
|
|
326
|
+
if (res.ok) break;
|
|
327
|
+
} catch {
|
|
328
|
+
// Try again on next iteration; outer catch handles total failure
|
|
329
|
+
}
|
|
330
|
+
}
|
|
280
331
|
} catch {
|
|
281
332
|
// Non-fatal — install_complete is best-effort
|
|
282
333
|
}
|
|
@@ -321,7 +372,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
321
372
|
|
|
322
373
|
// ── Human notification (agent/non-interactive installs) ─────────────────
|
|
323
374
|
|
|
324
|
-
if (somethingInstalled && nonInteractive) {
|
|
375
|
+
if (somethingInstalled && nonInteractive && !process.env.RR_API_KEY) {
|
|
325
376
|
blank();
|
|
326
377
|
warn('IMPORTANT: Notify your human about this installation.');
|
|
327
378
|
info('Robot Resources has made the following changes to this machine:');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robot-resources",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Robot Resources — AI agent tools. One command to install everything.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"@robot-resources/openclaw-plugin": "*",
|
|
22
22
|
"@robot-resources/scraper": "^0.3.1"
|
|
23
23
|
},
|
|
24
|
+
"optionalDependencies": {
|
|
25
|
+
"@robot-resources/router": "*"
|
|
26
|
+
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"vitest": "^1.2.0"
|
|
26
29
|
},
|