robot-resources 1.1.1 → 1.2.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 +8 -0
- package/lib/tool-config.js +83 -0
- package/lib/wizard.js +42 -58
- package/package.json +1 -1
package/lib/detect.js
CHANGED
|
@@ -72,6 +72,14 @@ export function isPortAvailable(port = 3838) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Check if OpenClaw is installed.
|
|
77
|
+
*/
|
|
78
|
+
export function isOpenClawInstalled() {
|
|
79
|
+
const home = homedir();
|
|
80
|
+
return existsSync(join(home, '.openclaw')) || existsSync(join(home, 'openclaw.json'));
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
/**
|
|
76
84
|
* Check if the router service is already registered.
|
|
77
85
|
*/
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { isOpenClawInstalled } from './detect.js';
|
|
5
|
+
|
|
6
|
+
const ROUTER_URL = 'http://localhost:3838';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read a JSON file safely. Returns null on failure.
|
|
10
|
+
*/
|
|
11
|
+
function readJsonSafe(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write JSON with backup.
|
|
21
|
+
*/
|
|
22
|
+
function writeJsonSafe(filePath, data) {
|
|
23
|
+
const dir = join(filePath, '..');
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
if (existsSync(filePath)) {
|
|
26
|
+
copyFileSync(filePath, `${filePath}.bak`);
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Configure OpenClaw to route through the Router.
|
|
33
|
+
*
|
|
34
|
+
* Adds a custom provider entry in the OpenClaw config.
|
|
35
|
+
*/
|
|
36
|
+
function configureOpenClaw() {
|
|
37
|
+
const home = homedir();
|
|
38
|
+
const configPaths = [
|
|
39
|
+
join(home, '.openclaw', 'config.json'),
|
|
40
|
+
join(home, 'openclaw.json'),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const configPath = configPaths.find((p) => existsSync(p)) || configPaths[0];
|
|
44
|
+
let config = readJsonSafe(configPath) || {};
|
|
45
|
+
|
|
46
|
+
// Ensure models.providers path
|
|
47
|
+
config.models = config.models || {};
|
|
48
|
+
config.models.providers = config.models.providers || {};
|
|
49
|
+
|
|
50
|
+
// Check if already configured
|
|
51
|
+
if (config.models.providers['robot-resources']) {
|
|
52
|
+
return { name: 'OpenClaw', action: 'already_configured' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add RR as a custom provider
|
|
56
|
+
config.models.providers['robot-resources'] = {
|
|
57
|
+
baseUrl: `${ROUTER_URL}/v1`,
|
|
58
|
+
api: 'openai-completions',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
writeJsonSafe(configPath, config);
|
|
62
|
+
return { name: 'OpenClaw', action: 'configured' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Configure all detected AI tools to route through the Router.
|
|
67
|
+
*
|
|
68
|
+
* Returns array of { name, action, ... } results.
|
|
69
|
+
*/
|
|
70
|
+
export function configureToolRouting() {
|
|
71
|
+
const results = [];
|
|
72
|
+
|
|
73
|
+
// OpenClaw
|
|
74
|
+
if (isOpenClawInstalled()) {
|
|
75
|
+
try {
|
|
76
|
+
results.push(configureOpenClaw());
|
|
77
|
+
} catch (err) {
|
|
78
|
+
results.push({ name: 'OpenClaw', action: 'error', reason: err.message });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return results;
|
|
83
|
+
}
|
package/lib/wizard.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { readConfig, writeConfig
|
|
1
|
+
import { readConfig, writeConfig } from '@robot-resources/cli-core/config.mjs';
|
|
2
2
|
import { login } from '@robot-resources/cli-core/login.mjs';
|
|
3
3
|
import { findPython, isPortAvailable } from './detect.js';
|
|
4
4
|
import { setupRouter, isRouterInstalled, getVenvPythonPath } from './python-bridge.js';
|
|
5
|
-
import { installService, isServiceRunning, isServiceInstalled
|
|
5
|
+
import { installService, isServiceRunning, isServiceInstalled } from './service.js';
|
|
6
6
|
import { configureAgentMCP } from './mcp-config.js';
|
|
7
|
+
import { configureToolRouting } from './tool-config.js';
|
|
7
8
|
import { header, step, success, warn, error, info, blank, summary, confirm, prompt } from './ui.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -94,51 +95,14 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
// ── Step 2.5:
|
|
98
|
+
// ── Step 2.5: Transparent Proxy Info ────────────────────────────────────
|
|
98
99
|
|
|
99
100
|
if (results.router) {
|
|
100
101
|
blank();
|
|
101
|
-
step('
|
|
102
|
-
info('The Router
|
|
103
|
-
info('
|
|
104
|
-
info('
|
|
105
|
-
blank();
|
|
106
|
-
|
|
107
|
-
const existingKeys = readProviderKeys();
|
|
108
|
-
const providers = [
|
|
109
|
-
{ env: 'OPENAI_API_KEY', config: 'openai', label: 'OpenAI' },
|
|
110
|
-
{ env: 'ANTHROPIC_API_KEY', config: 'anthropic', label: 'Anthropic' },
|
|
111
|
-
{ env: 'GOOGLE_API_KEY', config: 'google', label: 'Google AI' },
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
const newKeys = {};
|
|
115
|
-
|
|
116
|
-
for (const p of providers) {
|
|
117
|
-
const fromEnv = process.env[p.env];
|
|
118
|
-
const fromConfig = existingKeys[p.config];
|
|
119
|
-
|
|
120
|
-
if (fromEnv) {
|
|
121
|
-
success(`${p.label}: found in environment (${p.env})`);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (fromConfig) {
|
|
125
|
-
success(`${p.label}: found in config`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const key = await prompt(`${p.label} API key (${p.env})`, { nonInteractive });
|
|
130
|
-
if (key) {
|
|
131
|
-
newKeys[p.config] = key;
|
|
132
|
-
success(`${p.label}: saved`);
|
|
133
|
-
} else {
|
|
134
|
-
info(`${p.label}: skipped`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (Object.keys(newKeys).length > 0) {
|
|
139
|
-
writeProviderKeys(newKeys);
|
|
140
|
-
results.providerKeys = true;
|
|
141
|
-
}
|
|
102
|
+
step('Router proxy mode...');
|
|
103
|
+
info('The Router works as a transparent proxy — no API keys needed.');
|
|
104
|
+
info('Your AI tools already have their keys configured.');
|
|
105
|
+
info('The Router reads them from each request and forwards automatically.');
|
|
142
106
|
}
|
|
143
107
|
|
|
144
108
|
// ── Step 3: Service Registration ────────────────────────────────────────
|
|
@@ -160,16 +124,6 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
160
124
|
info('Another process may be using this port. The service will retry on restart.');
|
|
161
125
|
}
|
|
162
126
|
|
|
163
|
-
// Check for provider API keys (env + config.json)
|
|
164
|
-
const missingKeys = getMissingProviderKeys();
|
|
165
|
-
if (missingKeys.length === 3) {
|
|
166
|
-
warn('No LLM provider API keys configured');
|
|
167
|
-
info('The Router needs at least one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY');
|
|
168
|
-
info('Re-run this wizard or set them in your shell profile');
|
|
169
|
-
} else if (missingKeys.length > 0) {
|
|
170
|
-
info(`Provider keys configured: ${3 - missingKeys.length}/3`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
127
|
try {
|
|
174
128
|
const svc = installService(getVenvPythonPath());
|
|
175
129
|
if (svc.type === 'skipped') {
|
|
@@ -213,6 +167,36 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
213
167
|
}
|
|
214
168
|
}
|
|
215
169
|
|
|
170
|
+
// ── Step 5: Tool Routing Configuration ─────────────────────────────────
|
|
171
|
+
|
|
172
|
+
if (results.router) {
|
|
173
|
+
blank();
|
|
174
|
+
step('Configuring AI tools to use Router...');
|
|
175
|
+
|
|
176
|
+
const toolResults = configureToolRouting();
|
|
177
|
+
results.tools = toolResults;
|
|
178
|
+
|
|
179
|
+
if (toolResults.length === 0) {
|
|
180
|
+
info('No supported AI tools detected (Claude Code, OpenClaw, Cursor)');
|
|
181
|
+
info('Point your tool at http://localhost:3838 to enable cost optimization');
|
|
182
|
+
} else {
|
|
183
|
+
for (const r of toolResults) {
|
|
184
|
+
if (r.action === 'configured') {
|
|
185
|
+
success(`${r.name}: routing through localhost:3838`);
|
|
186
|
+
} else if (r.action === 'already_configured') {
|
|
187
|
+
success(`${r.name}: already configured`);
|
|
188
|
+
} else if (r.action === 'instructions') {
|
|
189
|
+
info(`${r.name}: manual configuration needed:`);
|
|
190
|
+
for (const instruction of r.instructions) {
|
|
191
|
+
info(` ${instruction}`);
|
|
192
|
+
}
|
|
193
|
+
} else if (r.action === 'error') {
|
|
194
|
+
error(`${r.name}: ${r.reason}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
216
200
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
217
201
|
|
|
218
202
|
const lines = [];
|
|
@@ -228,8 +212,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
228
212
|
}
|
|
229
213
|
|
|
230
214
|
if (results.router && results.service) {
|
|
231
|
-
lines.push('✓ Router
|
|
232
|
-
lines.push('
|
|
215
|
+
lines.push('✓ Router running as transparent proxy (localhost:3838)');
|
|
216
|
+
lines.push(' No API keys needed — reads them from your tools automatically');
|
|
233
217
|
} else if (results.router) {
|
|
234
218
|
lines.push('✓ Router installed (start manually: rr-router start)');
|
|
235
219
|
} else if (results.routerError === 'install-failed') {
|
|
@@ -256,8 +240,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
256
240
|
warn('IMPORTANT: Notify your human about this installation.');
|
|
257
241
|
info('Robot Resources has made the following changes to this machine:');
|
|
258
242
|
if (results.router) info(' • Router installed in ~/.robot-resources/');
|
|
259
|
-
if (results.service) info(' • Router registered as a
|
|
260
|
-
if (results.service) info(' •
|
|
243
|
+
if (results.service) info(' • Router registered as a transparent proxy (localhost:3838)');
|
|
244
|
+
if (results.service) info(' • Reads API keys from requests — no keys stored by Router');
|
|
261
245
|
for (const r of mcpAdded) {
|
|
262
246
|
info(` • ${r.name} config modified to add Scraper MCP`);
|
|
263
247
|
}
|