robot-resources 1.1.2 → 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 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, readProviderKeys, writeProviderKeys } from '@robot-resources/cli-core/config.mjs';
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, getMissingProviderKeys } from './service.js';
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: Provider API Keys ──────────────────────────────────────────
98
+ // ── Step 2.5: Transparent Proxy Info ────────────────────────────────────
98
99
 
99
100
  if (results.router) {
100
101
  blank();
101
- step('Configuring LLM provider API keys...');
102
- info('The Router forwards your requests to LLM providers on your behalf.');
103
- info('Keys are stored locally in ~/.robot-resources/config.json (readable only by you).');
104
- info('You need at least one provider key. Press Enter to skip any provider.');
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 installed and running as system service');
232
- lines.push(' Proxy: http://localhost:3838');
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 system service (localhost:3838)');
260
- if (results.service) info(' • All LLM calls using this proxy will be cost-optimized');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Robot Resources — AI agent runtime tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {