robot-resources 1.15.2 → 1.15.3

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/package.json CHANGED
@@ -1,54 +1,12 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.15.2",
4
- "description": "Robot Resources — AI agent tools. One command to install everything.",
5
- "type": "module",
6
- "bin": {
7
- "robot-resources-setup": "./bin/setup.js"
8
- },
9
- "main": "./bin/setup.js",
3
+ "version": "1.15.3",
4
+ "main": "index.js",
10
5
  "scripts": {
11
- "test": "vitest",
12
- "test:run": "vitest run"
13
- },
14
- "files": [
15
- "bin/",
16
- "lib/",
17
- "README.md"
18
- ],
19
- "dependencies": {
20
- "@inquirer/prompts": "^7.0.0",
21
- "@robot-resources/router": "*",
22
- "@robot-resources/scraper": "*"
23
- },
24
- "devDependencies": {
25
- "vitest": "^1.2.0"
26
- },
27
- "engines": {
28
- "node": ">=18.0.0"
29
- },
30
- "keywords": [
31
- "ai",
32
- "agents",
33
- "llm",
34
- "router",
35
- "scraper",
36
- "mcp",
37
- "cost-optimization",
38
- "robot-resources"
39
- ],
40
- "license": "MIT",
41
- "author": "Robot Resources Team",
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/robot-resources/packages.git",
45
- "directory": "packages/cli"
46
- },
47
- "homepage": "https://robotresources.ai",
48
- "bugs": {
49
- "url": "https://github.com/robot-resources/packages/issues"
6
+ "test": "echo \"Error: no test specified\" && exit 1"
50
7
  },
51
- "publishConfig": {
52
- "access": "public"
53
- }
8
+ "keywords": [],
9
+ "author": "",
10
+ "license": "ISC",
11
+ "description": "Private — in active development. Not yet for public use."
54
12
  }
package/README.md DELETED
@@ -1,104 +0,0 @@
1
- # Robot Resources
2
-
3
- > Tools for AI agents and agentic software. Humans have HR. Agents have RR.
4
-
5
- Robot Resources builds tools for AI agents and any software that makes LLM API calls — chatbots, RAG pipelines, AI-powered apps, internal tools. Two products today: **Router** (smart model selection — picks the right model for each task, 60-90% cost savings as a side effect) and **Scraper** (token compression for web content, median 91% token reduction). Both run locally. Both free.
6
-
7
- ## Quick Start
8
-
9
- ```bash
10
- npx -y robot-resources
11
- ```
12
-
13
- Detects your project shape (Node / Python / OpenClaw) and installs the right shim. No login, no API keys to enter, no signup. Router uses your existing provider keys — they never leave your machine.
14
-
15
- ## Router
16
-
17
- Smart model selection. Classifies each prompt by task type (coding / reasoning / analysis / simple_qa / creative / general), filters by model capability, then within the qualifying set picks the cheapest. Routes the right model for the task — cost savings (60-90% across mixed workloads) follow from that, not the other way around. Hybrid classification: keyword fast-path (~5ms, ~70% of prompts) + LLM slow-path for ambiguous prompts (~200ms). Routes across Anthropic, OpenAI, and Google when the corresponding keys are present.
18
-
19
- Three ways to install on a dev machine:
20
-
21
- - **OpenClaw users** get an in-process plugin inside the OC gateway. Anthropic, OpenAI, and Google calls each route to their native upstream — no cross-shape body translation.
22
- - **Node projects** get an auto-attach shim (`NODE_OPTIONS=--require .../auto.cjs`). Every Anthropic, OpenAI, and Google SDK call from any Node process routes automatically. No code changes.
23
- - **Python projects** get a `.pth` auto-attach shim in your venv. Every `anthropic` / `openai` / `google.generativeai` SDK call routes automatically. No code changes.
24
-
25
- For runtimes that ignore `NODE_OPTIONS` / `.pth` (Bun, Deno, Vercel Edge, Go, Rust, etc.), call the HTTP API directly: `POST https://api.robotresources.ai/v1/route`. Authed by API key, 100 req/min, CORS open.
26
-
27
- For explicit control inside JS / Python code, use the routing-decision library:
28
-
29
- ```bash
30
- npm install @robot-resources/router # JS / TS
31
- pip install robot-resources # Python (singular package name)
32
- ```
33
-
34
- ```js
35
- import { routePrompt } from '@robot-resources/router/routing';
36
- const decision = routePrompt('write a python function that reverses a string');
37
- // decision.selected_model → 'claude-haiku-4-5' (or similar — cheapest qualifying)
38
- ```
39
-
40
- ```python
41
- from robot_resources.router import route
42
- decision = route('write a python function that reverses a string')
43
- ```
44
-
45
- Returns a routing decision; your code makes the actual LLM call with the selected model. Each request goes from your machine straight to the lab's API (`api.anthropic.com` / `api.openai.com` / `generativelanguage.googleapis.com`) using your existing key for that lab. Nothing is relayed through our infrastructure.
46
-
47
- ## Scraper
48
-
49
- Token compression for web content. Fetches any URL, strips noise, returns clean markdown with token count. Median 91% token reduction per page (verified across 41 page types). Mozilla Readability extraction (0.97 F1). Content-aware token estimation calibrated per content type, ±15% of actual BPE. 3-tier fetch (fast / stealth via TLS fingerprint / render via headless browser), BFS multi-page crawl, robots.txt compliance.
50
-
51
- Three ways to consume:
52
-
53
- - **JS library** — `npm install @robot-resources/scraper` → `import { scrape } from '@robot-resources/scraper'`
54
- - **MCP server** — `npx -y @robot-resources/scraper scraper-mcp` exposes `scraper_compress_url(url)` and crawl tools to any MCP-compatible client. Auto-wired into OpenClaw by the wizard; for other clients (Cursor, Claude Code, Windsurf), add manually to your client's MCP config.
55
- - **OpenClaw plugin** — installed automatically via `npx robot-resources`. Hooks `before_tool_call` to redirect `web_fetch` through scraper compression.
56
-
57
- No API keys, no config.
58
-
59
- ## Deploying to production
60
-
61
- The wizard's shell-config install reaches dev machines only — production processes don't read `.bashrc`, and env vars come from your deploy config. Copy-paste recipes for setting `NODE_OPTIONS` (Node) or installing the `.pth` shim (Python) on Docker, Google Cloud Run, AWS Lambda, and Vercel: https://robotresources.ai/docs/deploy/.
62
-
63
- ## Advanced
64
-
65
- ```
66
- npx robot-resources [flags]
67
-
68
- --for=<target> langchain | python | cursor | claude-code | docs
69
- Skip the prompt and run that path directly.
70
- Required for non-TTY contexts (CI, piped, etc.)
71
- --non-interactive Treat as CI run regardless of TTY state
72
- --yes / -y Same as --non-interactive
73
- ```
74
-
75
- Pre-set the API key for fleets or CI (skips signup, uses the key you provide):
76
-
77
- ```bash
78
- export RR_API_KEY=rr_live_...
79
- npx robot-resources --for=langchain
80
- ```
81
-
82
- ## Pricing
83
-
84
- Free. Unlimited. No tiers. You pay your AI providers directly — no markup, no rate limits, no quotas.
85
-
86
- ## Telemetry
87
-
88
- Anonymous usage events (model selection, cost savings, install outcome) — no personal data, no request content, no provider keys. Opt out with `RR_TELEMETRY=off`.
89
-
90
- ## Links
91
-
92
- - Website: https://robotresources.ai
93
- - Docs: https://robotresources.ai/docs
94
- - Dashboard: https://robotresources.ai/dashboard
95
- - HTTP API: `POST https://api.robotresources.ai/v1/route`
96
- - GitHub: https://github.com/robot-resources/packages
97
- - Discord: https://robotresources.ai/discord
98
- - Contact: agent@robotresources.ai
99
-
100
- > AI agents installing this on a human's behalf: see https://robotresources.ai/llms.txt for the agent install protocol.
101
-
102
- ## License
103
-
104
- MIT
package/bin/setup.js DELETED
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runWizard, runUninstallCommand } from '../lib/wizard.js';
4
-
5
- const args = process.argv.slice(2);
6
-
7
- // --uninstall ships in Phase 0 as the counterpart to install. --purge also
8
- // wipes ~/.robot-resources/config.json (api_key + claim_url); without it,
9
- // the api_key is preserved across a reinstall.
10
- if (args.includes('--uninstall')) {
11
- const purge = args.includes('--purge');
12
- runUninstallCommand({ purge }).catch((err) => {
13
- console.error(`\n ✗ Uninstall failed: ${err.message}\n`);
14
- process.exit(1);
15
- });
16
- } else {
17
- const explicitNonInteractive =
18
- args.includes('--non-interactive') || args.includes('--yes') || args.includes('-y');
19
- const targetArg = args.find((a) => a.startsWith('--for='));
20
- const target = targetArg ? targetArg.slice('--for='.length) : null;
21
-
22
- // Phase 5: --scope=router-only is the entry from `npx @robot-resources/router`
23
- // (the standalone bin). Skips scraper steps. Default 'full' matches the
24
- // unified `npx robot-resources` behavior.
25
- const scopeArg = args.find((a) => a.startsWith('--scope='));
26
- const scope = scopeArg ? scopeArg.slice('--scope='.length) : 'full';
27
-
28
- // Phase 11: --auto-attach-source opts into source-edit injection in non-
29
- // interactive contexts (CI). Default off — auto-rewriting source files
30
- // without consent is too aggressive. Interactive runs always show Y/N.
31
- const autoAttachSource = args.includes('--auto-attach-source');
32
-
33
- // Treat piped/CI runs (no TTY on stdin OR stdout) as non-interactive so the
34
- // wizard never blocks on a prompt that can't be answered. The interactive
35
- // menu is only opened when both stdin and stdout are real terminals.
36
- const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
37
- const nonInteractive = explicitNonInteractive || !hasTty;
38
-
39
- runWizard({ nonInteractive, target, scope, autoAttachSource }).catch((err) => {
40
- console.error(`\n ✗ Setup failed: ${err.message}\n`);
41
- process.exit(1);
42
- });
43
- }
package/lib/auth.mjs DELETED
@@ -1,261 +0,0 @@
1
- import { createHash, randomBytes } from 'node:crypto';
2
- import { createServer } from 'node:http';
3
-
4
- // Supabase anon key is a public client key (like Stripe's publishable key).
5
- // Security is enforced by Row Level Security policies, not key secrecy.
6
- // Override via env vars for alternative Supabase instances.
7
- const SUPABASE_URL =
8
- process.env.SUPABASE_URL || 'https://tbnliojrqmcagojtvqpe.supabase.co';
9
- const SUPABASE_ANON_KEY =
10
- process.env.SUPABASE_ANON_KEY ||
11
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRibmxpb2pycW1jYWdvanR2cXBlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMyNjIxNzAsImV4cCI6MjA4ODgzODE3MH0.GKlpbVFgBbcV0OwxFZuOb-LfqtOu95ZiR33KNOONPI0';
12
-
13
- const PREFERRED_PORT = 54321;
14
-
15
- /**
16
- * Generate PKCE code verifier + challenge
17
- */
18
- function generatePKCE() {
19
- const verifier = randomBytes(32)
20
- .toString('base64url')
21
- .slice(0, 64);
22
- const challenge = createHash('sha256')
23
- .update(verifier)
24
- .digest('base64url');
25
- return { verifier, challenge };
26
- }
27
-
28
- /**
29
- * Build the Supabase OAuth URL for GitHub login
30
- */
31
- export function buildAuthUrl(codeChallenge, callbackUrl) {
32
- const params = new URLSearchParams({
33
- provider: 'github',
34
- redirect_to: callbackUrl,
35
- flow_type: 'pkce',
36
- code_challenge: codeChallenge,
37
- code_challenge_method: 'S256',
38
- });
39
- return `${SUPABASE_URL}/auth/v1/authorize?${params}`;
40
- }
41
-
42
- const MAX_BODY = 8192;
43
-
44
- /**
45
- * Create callback server. Returns { server, resultPromise, nonce }.
46
- * resultPromise resolves with { type: 'code', code } or { type: 'token', access_token }.
47
- * The nonce protects /receive-token from cross-origin requests.
48
- */
49
- function createCallbackServer() {
50
- let resolveResult, rejectResult;
51
- const resultPromise = new Promise((resolve, reject) => {
52
- resolveResult = resolve;
53
- rejectResult = reject;
54
- });
55
-
56
- const nonce = randomBytes(16).toString('hex');
57
- const tokenPath = `/receive-token/${nonce}`;
58
-
59
- const timeout = setTimeout(() => {
60
- server.close();
61
- rejectResult(new Error('Login timed out after 120 seconds'));
62
- }, 120_000);
63
-
64
- const server = createServer((req, res) => {
65
- const port = server.address()?.port ?? PREFERRED_PORT;
66
- const url = new URL(req.url, `http://localhost:${port}`);
67
-
68
- // Handle token/debug info posted from browser (nonce-protected)
69
- if (url.pathname === tokenPath && req.method === 'POST') {
70
- let body = '';
71
- req.on('data', (chunk) => {
72
- body += chunk;
73
- if (body.length > MAX_BODY) {
74
- req.destroy();
75
- clearTimeout(timeout);
76
- server.close();
77
- rejectResult(new Error('Request body too large'));
78
- }
79
- });
80
- req.on('end', () => {
81
- res.writeHead(200);
82
- res.end('ok');
83
- clearTimeout(timeout);
84
- server.close();
85
-
86
- if (body.startsWith('NO_FRAGMENT:')) {
87
- rejectResult(new Error('No tokens received from browser redirect'));
88
- } else {
89
- const params = new URLSearchParams(body);
90
- const accessToken = params.get('access_token');
91
- if (accessToken) {
92
- resolveResult({ type: 'token', access_token: accessToken });
93
- } else {
94
- rejectResult(new Error('Unexpected callback data'));
95
- }
96
- }
97
- });
98
- return;
99
- }
100
-
101
- if (url.pathname === '/callback') {
102
- const code = url.searchParams.get('code');
103
-
104
- if (code) {
105
- res.writeHead(200, { 'Content-Type': 'text/html' });
106
- res.end(`
107
- <html>
108
- <body style="background:#0a0a0a;color:#ff6600;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
109
- <div style="text-align:center">
110
- <h1>&#9632; Robot Resources</h1>
111
- <p style="color:#00ff41">Login successful. You can close this tab.</p>
112
- </div>
113
- </body>
114
- </html>
115
- `);
116
- clearTimeout(timeout);
117
- server.close();
118
- resolveResult({ type: 'code', code });
119
- } else {
120
- // No code in query params — serve page that captures the full URL
121
- // (fragment tokens, errors, etc.) and sends it back via nonce-protected endpoint
122
- res.writeHead(200, { 'Content-Type': 'text/html' });
123
- res.end(`
124
- <html>
125
- <body style="background:#0a0a0a;color:#ff6600;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
126
- <div style="text-align:center">
127
- <h1>&#9632; Robot Resources</h1>
128
- <p id="msg">Completing login...</p>
129
- </div>
130
- </body>
131
- <script>
132
- const fullUrl = window.location.href;
133
- const hash = window.location.hash.substring(1);
134
- const payload = hash || 'NO_FRAGMENT:' + fullUrl;
135
- fetch('${tokenPath}', { method: 'POST', body: payload })
136
- .then(() => {
137
- if (hash && hash.includes('access_token')) {
138
- document.getElementById('msg').style.color = '#00ff41';
139
- document.getElementById('msg').textContent = 'Login successful. You can close this tab.';
140
- } else {
141
- document.getElementById('msg').style.color = '#ffaa00';
142
- document.getElementById('msg').textContent = 'Something went wrong. Check terminal.';
143
- }
144
- });
145
- </script>
146
- </html>
147
- `);
148
- }
149
- return;
150
- }
151
-
152
- // Reject all other paths
153
- res.writeHead(404);
154
- res.end();
155
- });
156
-
157
- server.on('error', (err) => {
158
- clearTimeout(timeout);
159
- rejectResult(new Error(`Could not start callback server: ${err.message}`));
160
- });
161
-
162
- return { server, resultPromise, nonce };
163
- }
164
-
165
- /**
166
- * Exchange the auth code + PKCE verifier for a Supabase session.
167
- */
168
- async function exchangeCodeForSession(code, codeVerifier) {
169
- const res = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=pkce`, {
170
- method: 'POST',
171
- headers: {
172
- 'Content-Type': 'application/json',
173
- apikey: SUPABASE_ANON_KEY,
174
- },
175
- body: JSON.stringify({
176
- auth_code: code,
177
- code_verifier: codeVerifier,
178
- }),
179
- });
180
-
181
- if (!res.ok) {
182
- const body = await res.text();
183
- throw new Error(`Token exchange failed (${res.status}): ${body}`);
184
- }
185
-
186
- return res.json();
187
- }
188
-
189
- /**
190
- * Try to listen on the preferred port, fall back to OS-assigned port.
191
- */
192
- function listenWithFallback(server) {
193
- return new Promise((resolve, reject) => {
194
- server.once('error', (err) => {
195
- if (err.code === 'EADDRINUSE') {
196
- // Preferred port busy — let OS assign one
197
- server.listen(0, '127.0.0.1', () => resolve(server.address().port));
198
- } else {
199
- reject(err);
200
- }
201
- });
202
- server.listen(PREFERRED_PORT, '127.0.0.1', () => resolve(server.address().port));
203
- });
204
- }
205
-
206
- /**
207
- * Full OAuth flow with PKCE + implicit fallback.
208
- * Returns { access_token, refresh_token, user }.
209
- */
210
- export async function authenticate() {
211
- const { verifier, challenge } = generatePKCE();
212
-
213
- // Create server and wait for it to be listening
214
- const { server, resultPromise } = createCallbackServer();
215
-
216
- const port = await listenWithFallback(server);
217
- const callbackUrl = `http://localhost:${port}/callback`;
218
- const authUrl = buildAuthUrl(challenge, callbackUrl);
219
-
220
- console.log(`\n Auth URL: ${authUrl}\n`);
221
-
222
- // Open browser (use execFile to avoid shell injection)
223
- const { execFile } = await import('node:child_process');
224
- if (process.platform === 'win32') {
225
- // 'start' is a cmd.exe builtin, not an executable — must invoke via cmd.exe
226
- execFile('cmd.exe', ['/c', 'start', '""', authUrl]);
227
- } else {
228
- const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
229
- execFile(openCmd, [authUrl]);
230
- }
231
-
232
- console.log(' Waiting for GitHub authorization...\n');
233
-
234
- // Wait for the callback
235
- const result = await resultPromise;
236
-
237
- if (result.type === 'code') {
238
- // PKCE flow — exchange code for session
239
- const session = await exchangeCodeForSession(result.code, verifier);
240
- return {
241
- access_token: session.access_token,
242
- refresh_token: session.refresh_token,
243
- user: session.user,
244
- };
245
- } else {
246
- // Implicit flow fallback — we have the access_token directly
247
- const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
248
- headers: {
249
- apikey: SUPABASE_ANON_KEY,
250
- Authorization: `Bearer ${result.access_token}`,
251
- },
252
- });
253
- if (!res.ok) throw new Error('Failed to fetch user info');
254
- const user = await res.json();
255
- return {
256
- access_token: result.access_token,
257
- refresh_token: null,
258
- user,
259
- };
260
- }
261
- }
package/lib/config.mjs DELETED
@@ -1,55 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
-
5
- const CONFIG_DIR = join(homedir(), '.robot-resources');
6
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
-
8
- export function getConfigPath() {
9
- return CONFIG_FILE;
10
- }
11
-
12
- export function getConfigDir() {
13
- return CONFIG_DIR;
14
- }
15
-
16
- export function readConfig() {
17
- try {
18
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
19
- } catch {
20
- return {};
21
- }
22
- }
23
-
24
- export function writeConfig(data) {
25
- mkdirSync(CONFIG_DIR, { recursive: true });
26
- const existing = readConfig();
27
- const merged = { ...existing, ...data };
28
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
29
- return merged;
30
- }
31
-
32
- export function readProviderKeys() {
33
- const config = readConfig();
34
- return config.provider_keys || {};
35
- }
36
-
37
- export function writeProviderKeys(keys) {
38
- mkdirSync(CONFIG_DIR, { recursive: true });
39
- const existing = readConfig();
40
- const existingProviderKeys = existing.provider_keys || {};
41
- const merged = {
42
- ...existing,
43
- provider_keys: { ...existingProviderKeys, ...keys },
44
- };
45
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
46
- return merged;
47
- }
48
-
49
- export function clearConfig() {
50
- try {
51
- writeFileSync(CONFIG_FILE, '{}\n', { mode: 0o600 });
52
- } catch {
53
- // config file doesn't exist, that's fine
54
- }
55
- }