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 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
- return existsSync(join(home, '.openclaw', 'extensions', 'robot-resources-router'));
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 → auth.type or providers.anthropic.authToken
101
- * 3. providers.anthropic.apiKeyapikey
102
- * 4. Defaultapikey (conservative — proxy works fine)
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
- // Strip JSON5 features inline (comments + trailing commas)
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
+ }
@@ -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
- * Strip JSON5 features (comments + trailing commas) to produce valid JSON.
6
- * Handles single-line comments (//), multi-line comments, and trailing
7
- * commas before } or ]. Preserves // inside quoted strings (e.g. URLs).
8
- * No external dependency needed.
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 stripJson5(text) {
11
- // Match quoted strings (keep) or comments (remove) in one pass.
12
- // Strings are matched first so // inside "http://..." is preserved.
13
- const clean = text.replace(
14
- /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\/\/.*$|\/\*[\s\S]*?\*\//gm,
15
- (match) => (match.startsWith('"') || match.startsWith("'") ? match : ''),
16
- );
17
- // Remove trailing commas before } or ]
18
- return clean.replace(/,\s*([\]}])/g, '$1');
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. Authentication (GitHub OAuth)
13
- * 2. Router installation (Python venv + pip)
14
- * 3. Service registration (launchd/systemd)
15
- * 4. MCP auto-configuration (Claude Desktop, Cursor)
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: Authentication ──────────────────────────────────────────────
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 2.5: Transparent Proxy Info ────────────────────────────────────
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 3: Service Registration ────────────────────────────────────────
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 4: MCP Auto-Configuration ──────────────────────────────────────
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 supported agents detected (Claude Desktop, Cursor)');
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 5: Tool Routing Configuration ─────────────────────────────────
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 (Claude Code, OpenClaw, Cursor)');
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 lines = [];
171
+ const somethingInstalled = results.router || results.service
172
+ || mcpResults.some((r) => r.action === 'added' || r.action === 'exists');
206
173
 
207
- if (results.auth) {
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
- // ── Human notification (especially important for agent/non-interactive installs) ──
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
- const somethingInstalled = results.router || results.service || mcpAdded.length > 0;
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
- if (claimUrl) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.2.8",
3
+ "version": "1.3.0",
4
4
  "description": "Robot Resources — AI agent runtime tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {