robot-resources 1.2.8 → 1.3.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 +23 -10
- package/lib/json5.js +16 -0
- package/lib/tool-config.js +57 -13
- package/lib/wizard.js +70 -82
- package/package.json +1 -1
package/lib/detect.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync, execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { stripJson5 } from './json5.js';
|
|
5
6
|
|
|
6
7
|
// Re-export findPython from the shared cli-core implementation.
|
|
7
8
|
export { findPython } from '@robot-resources/cli-core/python-bridge.mjs';
|
|
@@ -85,7 +86,9 @@ export function isOpenClawInstalled() {
|
|
|
85
86
|
*/
|
|
86
87
|
export function isOpenClawPluginInstalled() {
|
|
87
88
|
const home = homedir();
|
|
88
|
-
|
|
89
|
+
const extDir = join(home, '.openclaw', 'extensions');
|
|
90
|
+
return existsSync(join(extDir, 'openclaw-plugin'))
|
|
91
|
+
|| existsSync(join(extDir, 'robot-resources-router'));
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
/**
|
|
@@ -97,9 +100,13 @@ export function isOpenClawPluginInstalled() {
|
|
|
97
100
|
*
|
|
98
101
|
* Detection order:
|
|
99
102
|
* 1. ANTHROPIC_AUTH_TOKEN env var → subscription
|
|
100
|
-
* 2. openclaw.json
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
+
* 2. openclaw.json config:
|
|
104
|
+
* a. auth.type === 'oauth' | 'subscription' → subscription
|
|
105
|
+
* b. auth.profiles.*.mode === 'token' → subscription
|
|
106
|
+
* c. gateway.auth.mode === 'token' → subscription
|
|
107
|
+
* d. providers.anthropic.authToken → subscription
|
|
108
|
+
* e. providers.anthropic.apiKey → apikey
|
|
109
|
+
* 3. Default → apikey (conservative — proxy works fine)
|
|
103
110
|
*/
|
|
104
111
|
export function getOpenClawAuthMode() {
|
|
105
112
|
// Env var is the strongest signal
|
|
@@ -118,18 +125,24 @@ export function getOpenClawAuthMode() {
|
|
|
118
125
|
|
|
119
126
|
try {
|
|
120
127
|
const raw = readFileSync(configPath, 'utf-8');
|
|
121
|
-
|
|
122
|
-
const clean = raw
|
|
123
|
-
.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\/\/.*$|\/\*[\s\S]*?\*\//gm,
|
|
124
|
-
(m) => (m.startsWith('"') || m.startsWith("'") ? m : ''))
|
|
125
|
-
.replace(/,\s*([\]}])/g, '$1');
|
|
126
|
-
const config = JSON.parse(clean);
|
|
128
|
+
const config = JSON.parse(stripJson5(raw));
|
|
127
129
|
|
|
128
130
|
// Check explicit auth type
|
|
129
131
|
if (config.auth?.type === 'oauth' || config.auth?.type === 'subscription') {
|
|
130
132
|
return 'subscription';
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
// Check auth profiles (real OC config: auth.profiles["anthropic:default"].mode)
|
|
136
|
+
const profiles = config.auth?.profiles;
|
|
137
|
+
if (profiles && typeof profiles === 'object') {
|
|
138
|
+
for (const profile of Object.values(profiles)) {
|
|
139
|
+
if (profile?.mode === 'token') return 'subscription';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check gateway auth mode
|
|
144
|
+
if (config.gateway?.auth?.mode === 'token') return 'subscription';
|
|
145
|
+
|
|
133
146
|
// Check for authToken in providers
|
|
134
147
|
const anthropic = config.models?.providers?.anthropic
|
|
135
148
|
|| config.providers?.anthropic;
|
package/lib/json5.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip JSON5 features (comments + trailing commas) to produce valid JSON.
|
|
3
|
+
*
|
|
4
|
+
* Handles single-line comments (//), multi-line comments, and trailing
|
|
5
|
+
* commas before } or ]. Preserves // inside quoted strings (e.g. URLs).
|
|
6
|
+
*
|
|
7
|
+
* Does NOT handle: unquoted keys, hex numbers, or backtick templates.
|
|
8
|
+
* These are valid JSON5 but uncommon in OpenClaw configs.
|
|
9
|
+
*/
|
|
10
|
+
export function stripJson5(text) {
|
|
11
|
+
const clean = text.replace(
|
|
12
|
+
/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\/\/.*$|\/\*[\s\S]*?\*\//gm,
|
|
13
|
+
(match) => (match.startsWith('"') || match.startsWith("'") ? match : ''),
|
|
14
|
+
);
|
|
15
|
+
return clean.replace(/,\s*([\]}])/g, '$1');
|
|
16
|
+
}
|
package/lib/tool-config.js
CHANGED
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
2
5
|
import { isOpenClawInstalled, isOpenClawPluginInstalled, getOpenClawAuthMode } from './detect.js';
|
|
6
|
+
import { stripJson5 } from './json5.js';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* Set robot-resources/auto as the default model in openclaw.json.
|
|
10
|
+
*
|
|
11
|
+
* This is required so the plugin's before_model_resolve hook fires.
|
|
12
|
+
* Without it, OpenClaw sends requests directly to Anthropic and the
|
|
13
|
+
* plugin never gets a chance to route.
|
|
14
|
+
*
|
|
15
|
+
* Returns true if the config was updated, false otherwise.
|
|
9
16
|
*/
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
function activateRouterModel() {
|
|
18
|
+
const configPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
22
|
+
const config = JSON.parse(stripJson5(raw));
|
|
23
|
+
|
|
24
|
+
// Ensure agents.defaults.model exists
|
|
25
|
+
if (!config.agents) config.agents = {};
|
|
26
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
27
|
+
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
28
|
+
|
|
29
|
+
const currentPrimary = config.agents.defaults.model.primary;
|
|
30
|
+
|
|
31
|
+
// Only change if not already pointing at robot-resources
|
|
32
|
+
if (currentPrimary && currentPrimary.startsWith('robot-resources/')) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
config.agents.defaults.model.primary = 'robot-resources/auto';
|
|
37
|
+
|
|
38
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
// Config missing or malformed — non-fatal
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
/**
|
|
@@ -47,10 +72,29 @@ function configureOpenClaw() {
|
|
|
47
72
|
stdio: 'ignore',
|
|
48
73
|
timeout: 30_000,
|
|
49
74
|
});
|
|
75
|
+
|
|
76
|
+
// Set robot-resources/auto as the default model so the plugin's
|
|
77
|
+
// before_model_resolve hook actually fires for every request.
|
|
78
|
+
const configActivated = activateRouterModel();
|
|
79
|
+
|
|
80
|
+
// Restart the gateway so it picks up the new plugin + config.
|
|
81
|
+
let gatewayRestarted = false;
|
|
82
|
+
try {
|
|
83
|
+
execFileSync('openclaw', ['gateway', 'restart'], {
|
|
84
|
+
stdio: 'ignore',
|
|
85
|
+
timeout: 15_000,
|
|
86
|
+
});
|
|
87
|
+
gatewayRestarted = true;
|
|
88
|
+
} catch {
|
|
89
|
+
// Non-fatal — gateway may not be running or command unsupported
|
|
90
|
+
}
|
|
91
|
+
|
|
50
92
|
return {
|
|
51
93
|
name: 'OpenClaw',
|
|
52
94
|
action: 'installed',
|
|
53
95
|
authMode,
|
|
96
|
+
configActivated,
|
|
97
|
+
gatewayRestarted,
|
|
54
98
|
note: authMode === 'subscription'
|
|
55
99
|
? 'Plugin required — subscription OAuth tokens are rejected by Anthropic when proxied via third-party clients.'
|
|
56
100
|
: undefined,
|
package/lib/wizard.js
CHANGED
|
@@ -9,10 +9,14 @@ import { header, step, success, warn, error, info, blank, summary, confirm, prom
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Main setup wizard. Handles the full onboarding flow:
|
|
12
|
-
* 1.
|
|
13
|
-
* 2.
|
|
14
|
-
* 3.
|
|
15
|
-
* 4.
|
|
12
|
+
* 1. Router installation (Python venv + pip)
|
|
13
|
+
* 2. Service registration (launchd/systemd)
|
|
14
|
+
* 3. MCP auto-configuration (detected agents)
|
|
15
|
+
* 4. Tool routing (OpenClaw plugin + model activation)
|
|
16
|
+
* 5. Dashboard login (optional, at the very end)
|
|
17
|
+
*
|
|
18
|
+
* Auth is intentionally LAST. The router works fully without it.
|
|
19
|
+
* Dashboard is for humans — agents don't need it.
|
|
16
20
|
*/
|
|
17
21
|
export async function runWizard({ nonInteractive = false } = {}) {
|
|
18
22
|
header();
|
|
@@ -27,48 +31,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
27
31
|
mcp: [],
|
|
28
32
|
};
|
|
29
33
|
|
|
30
|
-
// ── Step 1:
|
|
34
|
+
// ── Step 1: Router Installation ─────────────────────────────────────────
|
|
31
35
|
|
|
32
|
-
const config = readConfig();
|
|
33
|
-
|
|
34
|
-
if (config.api_key) {
|
|
35
|
-
success(`Already logged in as ${config.user_name || config.user_email || 'unknown'}`);
|
|
36
|
-
results.auth = true;
|
|
37
|
-
results.authMethod = 'config';
|
|
38
|
-
} else if (process.env.RR_API_KEY) {
|
|
39
|
-
// Agent flow: API key provided via environment variable
|
|
40
|
-
const envKey = process.env.RR_API_KEY;
|
|
41
|
-
if (!envKey.startsWith('rr_live_')) {
|
|
42
|
-
error('RR_API_KEY must start with rr_live_');
|
|
43
|
-
info('Get a valid key from POST /v1/auth/signup or the dashboard');
|
|
44
|
-
} else {
|
|
45
|
-
writeConfig({ api_key: envKey, signup_source: 'agent' });
|
|
46
|
-
success('API key loaded from RR_API_KEY environment variable');
|
|
47
|
-
results.auth = true;
|
|
48
|
-
results.authMethod = 'apikey';
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
step('Setting up your Robot Resources account...');
|
|
52
|
-
|
|
53
|
-
const shouldLogin = await confirm('Log in with GitHub?', { nonInteractive });
|
|
54
|
-
if (shouldLogin) {
|
|
55
|
-
try {
|
|
56
|
-
await login();
|
|
57
|
-
results.auth = true;
|
|
58
|
-
results.authMethod = 'github';
|
|
59
|
-
} catch (err) {
|
|
60
|
-
error(`Login failed: ${err.message}`);
|
|
61
|
-
info('You can log in later with: npx robot-resources login');
|
|
62
|
-
blank();
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
info('Skipping login. You can log in later with: npx robot-resources login');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Step 2: Router Installation ─────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
blank();
|
|
72
36
|
step('Checking Router...');
|
|
73
37
|
|
|
74
38
|
if (isRouterInstalled()) {
|
|
@@ -95,7 +59,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
95
59
|
}
|
|
96
60
|
}
|
|
97
61
|
|
|
98
|
-
// ── Step
|
|
62
|
+
// ── Step 1.5: Transparent Proxy Info ────────────────────────────────────
|
|
99
63
|
|
|
100
64
|
if (results.router) {
|
|
101
65
|
blank();
|
|
@@ -105,7 +69,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
105
69
|
info('The Router reads them from each request and forwards automatically.');
|
|
106
70
|
}
|
|
107
71
|
|
|
108
|
-
// ── Step
|
|
72
|
+
// ── Step 2: Service Registration ────────────────────────────────────────
|
|
109
73
|
|
|
110
74
|
if (results.router) {
|
|
111
75
|
blank();
|
|
@@ -142,7 +106,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
142
106
|
}
|
|
143
107
|
}
|
|
144
108
|
|
|
145
|
-
// ── Step
|
|
109
|
+
// ── Step 3: MCP Auto-Configuration ──────────────────────────────────────
|
|
146
110
|
|
|
147
111
|
blank();
|
|
148
112
|
step('Configuring MCP in detected agents...');
|
|
@@ -151,7 +115,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
151
115
|
results.mcp = mcpResults;
|
|
152
116
|
|
|
153
117
|
if (mcpResults.length === 0) {
|
|
154
|
-
info('No
|
|
118
|
+
info('No MCP-compatible agents detected');
|
|
155
119
|
info('You can manually add MCP servers to your agent config later');
|
|
156
120
|
} else {
|
|
157
121
|
for (const r of mcpResults) {
|
|
@@ -167,7 +131,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
167
131
|
}
|
|
168
132
|
}
|
|
169
133
|
|
|
170
|
-
// ── Step
|
|
134
|
+
// ── Step 4: Tool Routing Configuration ─────────────────────────────────
|
|
171
135
|
|
|
172
136
|
if (results.router) {
|
|
173
137
|
blank();
|
|
@@ -177,7 +141,7 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
177
141
|
results.tools = toolResults;
|
|
178
142
|
|
|
179
143
|
if (toolResults.length === 0) {
|
|
180
|
-
info('No supported AI tools detected
|
|
144
|
+
info('No supported AI tools detected');
|
|
181
145
|
info('Point your tool at http://localhost:3838 to enable cost optimization');
|
|
182
146
|
} else {
|
|
183
147
|
for (const r of toolResults) {
|
|
@@ -187,6 +151,8 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
187
151
|
success(`${r.name}: already configured`);
|
|
188
152
|
} else if (r.action === 'installed') {
|
|
189
153
|
success(`${r.name}: plugin installed`);
|
|
154
|
+
if (r.configActivated) success(`${r.name}: default model set to robot-resources/auto`);
|
|
155
|
+
if (r.gatewayRestarted) success(`${r.name}: gateway restarted`);
|
|
190
156
|
if (r.note) info(` ${r.note}`);
|
|
191
157
|
} else if (r.action === 'instructions') {
|
|
192
158
|
warn(`${r.name}: manual configuration needed:`);
|
|
@@ -202,17 +168,10 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
202
168
|
|
|
203
169
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
204
170
|
|
|
205
|
-
const
|
|
171
|
+
const somethingInstalled = results.router || results.service
|
|
172
|
+
|| mcpResults.some((r) => r.action === 'added' || r.action === 'exists');
|
|
206
173
|
|
|
207
|
-
|
|
208
|
-
const authLabel =
|
|
209
|
-
results.authMethod === 'apikey' ? 'Authenticated with API key' :
|
|
210
|
-
results.authMethod === 'config' ? 'Authenticated (saved credentials)' :
|
|
211
|
-
'Authenticated with GitHub';
|
|
212
|
-
lines.push(`✓ ${authLabel}`);
|
|
213
|
-
} else {
|
|
214
|
-
lines.push('○ Not logged in (run: npx robot-resources login)');
|
|
215
|
-
}
|
|
174
|
+
const lines = [];
|
|
216
175
|
|
|
217
176
|
if (results.router && results.service) {
|
|
218
177
|
lines.push('✓ Router running as transparent proxy (localhost:3838)');
|
|
@@ -234,9 +193,56 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
234
193
|
|
|
235
194
|
summary(lines);
|
|
236
195
|
|
|
237
|
-
// ──
|
|
196
|
+
// ── Step 5: Dashboard login (optional, always last) ────────────────────
|
|
197
|
+
//
|
|
198
|
+
// Auth is deliberately at the end. The router works fully without it.
|
|
199
|
+
// Dashboard is for humans — agents should skip this and tell their
|
|
200
|
+
// human to run the login command later if they want monitoring.
|
|
238
201
|
|
|
239
|
-
|
|
202
|
+
if (somethingInstalled) {
|
|
203
|
+
blank();
|
|
204
|
+
|
|
205
|
+
// Check if already authenticated (from a previous run)
|
|
206
|
+
const config = readConfig();
|
|
207
|
+
if (config.api_key) {
|
|
208
|
+
success(`Dashboard: logged in as ${config.user_name || config.user_email || 'unknown'}`);
|
|
209
|
+
results.auth = true;
|
|
210
|
+
results.authMethod = 'config';
|
|
211
|
+
} else if (process.env.RR_API_KEY) {
|
|
212
|
+
const envKey = process.env.RR_API_KEY;
|
|
213
|
+
if (envKey.startsWith('rr_live_')) {
|
|
214
|
+
writeConfig({ api_key: envKey, signup_source: 'agent' });
|
|
215
|
+
success('Dashboard: API key loaded from RR_API_KEY');
|
|
216
|
+
results.auth = true;
|
|
217
|
+
results.authMethod = 'apikey';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!results.auth) {
|
|
222
|
+
if (nonInteractive) {
|
|
223
|
+
// Agent flow: don't prompt, just tell the agent what to relay
|
|
224
|
+
info('Dashboard (optional): run "npx robot-resources login" to claim your dashboard');
|
|
225
|
+
} else {
|
|
226
|
+
// Interactive flow: offer login but don't block on "no"
|
|
227
|
+
const shouldLogin = await confirm('Claim your dashboard? (optional)', { defaultYes: false, nonInteractive });
|
|
228
|
+
if (shouldLogin) {
|
|
229
|
+
try {
|
|
230
|
+
await login();
|
|
231
|
+
results.auth = true;
|
|
232
|
+
results.authMethod = 'github';
|
|
233
|
+
success('Dashboard claimed');
|
|
234
|
+
} catch (err) {
|
|
235
|
+
error(`Login failed: ${err.message}`);
|
|
236
|
+
info('You can claim later: npx robot-resources login');
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
info('Claim later: npx robot-resources login');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Human notification (agent/non-interactive installs) ─────────────────
|
|
240
246
|
|
|
241
247
|
if (somethingInstalled && nonInteractive) {
|
|
242
248
|
blank();
|
|
@@ -249,29 +255,11 @@ export async function runWizard({ nonInteractive = false } = {}) {
|
|
|
249
255
|
info(` • ${r.name} config modified to add Scraper MCP`);
|
|
250
256
|
}
|
|
251
257
|
blank();
|
|
252
|
-
const claimUrl = process.env.RR_CLAIM_URL;
|
|
253
|
-
if (claimUrl) {
|
|
254
|
-
warn('CRITICAL: You MUST send this dashboard claim link to your human:');
|
|
255
|
-
info(` ${claimUrl}`);
|
|
256
|
-
blank();
|
|
257
|
-
} else {
|
|
258
|
-
warn('CRITICAL: You MUST send the dashboard claim link to your human.');
|
|
259
|
-
info('The claim_url was returned in the POST /v1/auth/signup response.');
|
|
260
|
-
info('Find it and include it in your message. Without it, your human cannot access the dashboard.');
|
|
261
|
-
blank();
|
|
262
|
-
}
|
|
263
258
|
info('Suggested message to your human (COPY THIS EXACTLY):');
|
|
264
259
|
info('"I installed Robot Resources Router. It saves 60-90% on LLM costs.');
|
|
265
260
|
info(' All calls now route through localhost:3838 for cost optimization.');
|
|
266
|
-
|
|
267
|
-
info(` Claim your dashboard: ${claimUrl}`);
|
|
268
|
-
} else {
|
|
269
|
-
info(' Claim your dashboard: <INSERT YOUR claim_url FROM SIGNUP RESPONSE>');
|
|
270
|
-
}
|
|
261
|
+
info(' Claim your dashboard: npx robot-resources login');
|
|
271
262
|
info(' More info: https://robotresources.ai"');
|
|
272
263
|
blank();
|
|
273
|
-
} else if (somethingInstalled) {
|
|
274
|
-
blank();
|
|
275
|
-
info('Dashboard & monitoring: https://robotresources.ai/dashboard');
|
|
276
264
|
}
|
|
277
265
|
}
|