robot-resources 0.0.1 → 1.0.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/README.md CHANGED
@@ -1,11 +1,73 @@
1
- # Robot Resources
1
+ # robot-resources — Unified Installer
2
2
 
3
- AI agent runtime infrastructure.
3
+ One command to install all Robot Resources tools:
4
4
 
5
- ## Coming Soon
5
+ ```
6
+ npx robot-resources
7
+ ```
6
8
 
7
- This package is under active development. Stay tuned for updates.
9
+ ## What the wizard does
8
10
 
9
- ## Contact
11
+ 1. **GitHub OAuth** — authenticates via PKCE, saves API key to `~/.robot-resources/config.json`
12
+ 2. **Router install** — detects Python 3.10+, creates venv, pip installs `robot-resources-router`
13
+ 3. **Service registration** — registers router as launchd (macOS) or systemd (Linux) service
14
+ 4. **MCP auto-config** — detects Claude Desktop and Cursor, injects scraper MCP entry
10
15
 
11
- For inquiries, reach out to the Robot Resources Team.
16
+ ## Adding a new tool
17
+
18
+ When you build a new product for RR, follow this checklist:
19
+
20
+ ### 1. Create the npx entry point
21
+
22
+ Your tool needs an npm package with a `bin` entry that runs a setup wizard. Two patterns:
23
+
24
+ - **Node.js tool:** Add `bin` to your package.json pointing to a setup script
25
+ - **Python tool:** Create a thin npm wrapper (like `router/packages/router/`) that handles pip install
26
+
27
+ The wizard should:
28
+ - Check `~/.robot-resources/config.json` — offer login if missing
29
+ - Install the tool
30
+ - Configure MCP if applicable
31
+ - Print usage instructions
32
+
33
+ ### 2. Update the unified wizard
34
+
35
+ Edit `packages/cli/lib/wizard.js` to add your tool as a new step between service registration and MCP config.
36
+
37
+ ### 3. Update the landing page
38
+
39
+ | File | What to change |
40
+ |------|---------------|
41
+ | `web/src/pages/Landing/types.ts` | Add install subsection if new section |
42
+ | `web/src/pages/Landing/components/ContentPanel/animatedContentScreens.ts` | Add install screen (desktop + mobile) |
43
+ | `web/src/pages/Landing/components/TerminalPanel/terminalCommands.ts` | Add install subcommand output |
44
+
45
+ ### 4. Update agent-facing content
46
+
47
+ | File | What to change |
48
+ |------|---------------|
49
+ | `web/public/llms.txt` | Add tool with `npx @robot-resources/<tool>` |
50
+ | `web/public/llms-full.txt` | Add detailed section with npx install |
51
+ | `web/public/.well-known/ai-resources.json` | Add SoftwareApplication entry with InstallAction |
52
+
53
+ ### 5. Update the publish workflow
54
+
55
+ Add your package.json path to `.github/workflows/publish.yml` triggers and add a publish step.
56
+
57
+ ### 6. Add to root workspaces
58
+
59
+ Add your package directory to the `workspaces` array in the root `package.json`.
60
+
61
+ ## Architecture
62
+
63
+ ```
64
+ packages/cli-core/ Shared auth, config, login (private — never published)
65
+ packages/cli/ This package (published as "robot-resources" on npm)
66
+ bin/setup.js Entry point
67
+ lib/wizard.js Orchestrator — add new tools here
68
+ lib/service.js launchd + systemd service registration
69
+ lib/mcp-config.js Agent detection + MCP JSON patching
70
+ lib/python-bridge.js Python venv management
71
+ lib/detect.js Environment detection
72
+ lib/ui.js Terminal formatting
73
+ ```
package/bin/setup.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runWizard } from '../lib/wizard.js';
4
+
5
+ const args = process.argv.slice(2);
6
+ const nonInteractive = args.includes('--non-interactive') || args.includes('--yes') || args.includes('-y');
7
+
8
+ runWizard({ nonInteractive }).catch((err) => {
9
+ console.error(`\n ✗ Setup failed: ${err.message}\n`);
10
+ process.exit(1);
11
+ });
package/lib/detect.js ADDED
@@ -0,0 +1,85 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ // Re-export findPython from the shared cli-core implementation.
7
+ export { findPython } from '@robot-resources/cli-core/python-bridge.mjs';
8
+
9
+ import { getVenvPython, MANAGED_VENV_DIR } from '@robot-resources/cli-core/python-bridge.mjs';
10
+
11
+ /**
12
+ * Check if the router venv exists and has the package installed.
13
+ * Returns { venvDir, venvPython } or null.
14
+ */
15
+ export function checkRouterVenv() {
16
+ const venvPython = getVenvPython();
17
+
18
+ if (!existsSync(venvPython)) return null;
19
+
20
+ try {
21
+ execSync(`"${venvPython}" -c "import robot_resources" 2>&1`, { encoding: 'utf-8' });
22
+ return { venvDir: MANAGED_VENV_DIR, venvPython };
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Detect installed AI agents by checking their config file locations.
30
+ */
31
+ export function detectAgents() {
32
+ const home = homedir();
33
+ const agents = [];
34
+
35
+ const known = [
36
+ {
37
+ name: 'Claude Desktop',
38
+ configPath: process.platform === 'darwin'
39
+ ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
40
+ : join(home, '.config', 'Claude', 'claude_desktop_config.json'),
41
+ },
42
+ {
43
+ name: 'Cursor',
44
+ configPath: join(home, '.cursor', 'mcp.json'),
45
+ },
46
+ ];
47
+
48
+ for (const agent of known) {
49
+ // Check if the config file or its parent directory exists
50
+ const configExists = existsSync(agent.configPath);
51
+ const dirExists = existsSync(join(agent.configPath, '..'));
52
+ if (configExists || dirExists) {
53
+ agents.push({ ...agent, configExists });
54
+ }
55
+ }
56
+
57
+ return agents;
58
+ }
59
+
60
+ /**
61
+ * Check if port 3838 is available.
62
+ */
63
+ export function isPortAvailable(port = 3838) {
64
+ try {
65
+ execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf-8' });
66
+ return false; // port is in use
67
+ } catch {
68
+ return true; // port is available (lsof returned non-zero = no process)
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Check if the router service is already registered.
74
+ */
75
+ export function isServiceRegistered() {
76
+ if (process.platform === 'darwin') {
77
+ const plistPath = join(homedir(), 'Library', 'LaunchAgents', 'ai.robotresources.router.plist');
78
+ return existsSync(plistPath);
79
+ }
80
+ if (process.platform === 'linux') {
81
+ const unitPath = join(homedir(), '.config', 'systemd', 'user', 'robot-resources-router.service');
82
+ return existsSync(unitPath);
83
+ }
84
+ return false;
85
+ }
@@ -0,0 +1,90 @@
1
+ import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { detectAgents } from './detect.js';
4
+
5
+ const SCRAPER_MCP_ENTRY = {
6
+ command: 'npx',
7
+ args: ['-y', '@robot-resources/scraper-mcp'],
8
+ };
9
+
10
+ const MCP_KEY = 'robot-resources-scraper';
11
+
12
+ /**
13
+ * Read a JSON file safely. Returns null on failure.
14
+ */
15
+ function readJsonSafe(filePath) {
16
+ try {
17
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Write JSON with backup. Creates parent directory if needed.
25
+ */
26
+ function writeJsonSafe(filePath, data) {
27
+ const dir = join(filePath, '..');
28
+ mkdirSync(dir, { recursive: true });
29
+
30
+ // Create backup if file exists
31
+ if (existsSync(filePath)) {
32
+ copyFileSync(filePath, `${filePath}.bak`);
33
+ }
34
+
35
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
36
+ }
37
+
38
+ /**
39
+ * Configure MCP in all detected agents.
40
+ * Returns array of { name, configPath, action } results.
41
+ */
42
+ export function configureAgentMCP() {
43
+ const agents = detectAgents();
44
+ const results = [];
45
+
46
+ for (const agent of agents) {
47
+ try {
48
+ let config = agent.configExists ? readJsonSafe(agent.configPath) : {};
49
+
50
+ if (!config) {
51
+ results.push({ name: agent.name, configPath: agent.configPath, action: 'skipped', reason: 'invalid JSON' });
52
+ continue;
53
+ }
54
+
55
+ // Ensure mcpServers key exists
56
+ config.mcpServers = config.mcpServers || {};
57
+
58
+ // Skip if already configured
59
+ if (config.mcpServers[MCP_KEY]) {
60
+ results.push({ name: agent.name, configPath: agent.configPath, action: 'exists' });
61
+ continue;
62
+ }
63
+
64
+ // Add scraper MCP
65
+ config.mcpServers[MCP_KEY] = SCRAPER_MCP_ENTRY;
66
+ writeJsonSafe(agent.configPath, config);
67
+ results.push({ name: agent.name, configPath: agent.configPath, action: 'added' });
68
+ } catch (err) {
69
+ results.push({ name: agent.name, configPath: agent.configPath, action: 'error', reason: err.message });
70
+ }
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ /**
77
+ * Remove RR MCP entries from all detected agents.
78
+ */
79
+ export function removeAgentMCP() {
80
+ const agents = detectAgents();
81
+
82
+ for (const agent of agents) {
83
+ if (!agent.configExists) continue;
84
+ const config = readJsonSafe(agent.configPath);
85
+ if (!config?.mcpServers?.[MCP_KEY]) continue;
86
+
87
+ delete config.mcpServers[MCP_KEY];
88
+ writeJsonSafe(agent.configPath, config);
89
+ }
90
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ findPython,
3
+ ensureVenv,
4
+ installRouter,
5
+ isRouterInstalled,
6
+ getVenvPythonPath,
7
+ } from '@robot-resources/cli-core/python-bridge.mjs';
8
+
9
+ // Re-export shared primitives used by wizard.js and other CLI code.
10
+ export { ensureVenv, isRouterInstalled, getVenvPythonPath };
11
+
12
+ /**
13
+ * Full setup: find Python, create venv, install router.
14
+ * Returns { venvPython, pythonVersion } or throws.
15
+ */
16
+ export async function setupRouter() {
17
+ const python = findPython();
18
+ if (!python) {
19
+ throw new Error(
20
+ 'Python 3.10+ not found. Install Python from https://python.org and try again.\n' +
21
+ ' The Router requires Python. Scraper and MCP tools work without it.'
22
+ );
23
+ }
24
+
25
+ const venvPython = ensureVenv(python.bin);
26
+ installRouter();
27
+ return { venvPython, pythonVersion: python.version };
28
+ }
package/lib/service.js ADDED
@@ -0,0 +1,270 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, writeFileSync, unlinkSync, mkdirSync, chmodSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join, dirname } from 'node:path';
5
+ import { readProviderKeys } from '@robot-resources/cli-core/config.mjs';
6
+
7
+ const LABEL = 'ai.robotresources.router';
8
+ const ROUTER_PORT = 3838;
9
+
10
+ // Maps config.json provider_keys names to environment variable names
11
+ const CONFIG_TO_ENV = {
12
+ openai: 'OPENAI_API_KEY',
13
+ anthropic: 'ANTHROPIC_API_KEY',
14
+ google: 'GOOGLE_API_KEY',
15
+ };
16
+
17
+ // ─── macOS (launchd) ────────────────────────────────────────────────────────
18
+
19
+ function getPlistPath() {
20
+ return join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
21
+ }
22
+
23
+ function buildPlist(venvPythonPath) {
24
+ const home = homedir();
25
+ const logsDir = join(home, '.robot-resources', 'logs');
26
+
27
+ // Snapshot provider API keys: env vars take priority, then config.json
28
+ const envVars = {};
29
+ const configKeys = readProviderKeys();
30
+ const keyNames = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY'];
31
+ for (const key of keyNames) {
32
+ const value = process.env[key];
33
+ if (value) {
34
+ envVars[key] = value;
35
+ }
36
+ }
37
+ // Fill in from config.json for any keys not found in environment
38
+ for (const [configName, envName] of Object.entries(CONFIG_TO_ENV)) {
39
+ if (!envVars[envName] && configKeys[configName]) {
40
+ envVars[envName] = configKeys[configName];
41
+ }
42
+ }
43
+ // Ensure PATH includes common binary locations
44
+ envVars.PATH = '/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin';
45
+
46
+ const envEntries = Object.entries(envVars)
47
+ .map(([k, v]) => ` <key>${k}</key>\n <string>${v}</string>`)
48
+ .join('\n');
49
+
50
+ return `<?xml version="1.0" encoding="UTF-8"?>
51
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
52
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
53
+ <plist version="1.0">
54
+ <dict>
55
+ <key>Label</key>
56
+ <string>${LABEL}</string>
57
+ <key>ProgramArguments</key>
58
+ <array>
59
+ <string>${venvPythonPath}</string>
60
+ <string>-m</string>
61
+ <string>robot_resources.cli.main</string>
62
+ <string>start</string>
63
+ </array>
64
+ <key>RunAtLoad</key>
65
+ <true/>
66
+ <key>KeepAlive</key>
67
+ <true/>
68
+ <key>StandardOutPath</key>
69
+ <string>${logsDir}/router.stdout.log</string>
70
+ <key>StandardErrorPath</key>
71
+ <string>${logsDir}/router.stderr.log</string>
72
+ <key>EnvironmentVariables</key>
73
+ <dict>
74
+ ${envEntries}
75
+ </dict>
76
+ <key>WorkingDirectory</key>
77
+ <string>${home}/.robot-resources</string>
78
+ </dict>
79
+ </plist>
80
+ `;
81
+ }
82
+
83
+ function installLaunchd(venvPythonPath) {
84
+ const plistPath = getPlistPath();
85
+ const logsDir = join(homedir(), '.robot-resources', 'logs');
86
+ const launchAgentsDir = join(homedir(), 'Library', 'LaunchAgents');
87
+
88
+ mkdirSync(logsDir, { recursive: true });
89
+ mkdirSync(launchAgentsDir, { recursive: true });
90
+
91
+ // Unload existing service if present
92
+ if (existsSync(plistPath)) {
93
+ try {
94
+ execSync(`launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null`, { stdio: 'pipe' });
95
+ } catch {
96
+ // Not loaded — fine
97
+ }
98
+ }
99
+
100
+ writeFileSync(plistPath, buildPlist(venvPythonPath));
101
+ chmodSync(plistPath, 0o600);
102
+ execSync(`launchctl bootstrap gui/$(id -u) "${plistPath}"`, { stdio: 'pipe' });
103
+ }
104
+
105
+ function uninstallLaunchd() {
106
+ const plistPath = getPlistPath();
107
+ if (!existsSync(plistPath)) return;
108
+
109
+ try {
110
+ execSync(`launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null`, { stdio: 'pipe' });
111
+ } catch {
112
+ // Already unloaded
113
+ }
114
+ unlinkSync(plistPath);
115
+ }
116
+
117
+ function isLaunchdRunning() {
118
+ try {
119
+ const output = execSync(`launchctl print gui/$(id -u)/${LABEL} 2>&1`, { encoding: 'utf-8' });
120
+ return output.includes('state = running');
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ // ─── Linux (systemd) ────────────────────────────────────────────────────────
127
+
128
+ function getUnitPath() {
129
+ return join(homedir(), '.config', 'systemd', 'user', 'robot-resources-router.service');
130
+ }
131
+
132
+ function buildUnit(venvPythonPath) {
133
+ const home = homedir();
134
+ const logsDir = join(home, '.robot-resources', 'logs');
135
+
136
+ // Snapshot provider API keys: env vars take priority, then config.json
137
+ const envLines = [];
138
+ const configKeys = readProviderKeys();
139
+ const keyNames = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY'];
140
+ const resolvedKeys = {};
141
+ for (const key of keyNames) {
142
+ if (process.env[key]) {
143
+ resolvedKeys[key] = process.env[key];
144
+ }
145
+ }
146
+ for (const [configName, envName] of Object.entries(CONFIG_TO_ENV)) {
147
+ if (!resolvedKeys[envName] && configKeys[configName]) {
148
+ resolvedKeys[envName] = configKeys[configName];
149
+ }
150
+ }
151
+ for (const [key, value] of Object.entries(resolvedKeys)) {
152
+ envLines.push(`Environment=${key}=${value}`);
153
+ }
154
+ envLines.push('Environment=PATH=/usr/local/bin:/usr/bin:/bin');
155
+
156
+ return `[Unit]
157
+ Description=Robot Resources Router — LLM cost optimization proxy
158
+ After=network-online.target
159
+ Wants=network-online.target
160
+
161
+ [Service]
162
+ Type=simple
163
+ ExecStart=${venvPythonPath} -m robot_resources.cli.main start
164
+ Restart=on-failure
165
+ RestartSec=5
166
+ ${envLines.join('\n')}
167
+ WorkingDirectory=${home}/.robot-resources
168
+ StandardOutput=append:${logsDir}/router.stdout.log
169
+ StandardError=append:${logsDir}/router.stderr.log
170
+
171
+ [Install]
172
+ WantedBy=default.target
173
+ `;
174
+ }
175
+
176
+ function installSystemd(venvPythonPath) {
177
+ const unitPath = getUnitPath();
178
+ const logsDir = join(homedir(), '.robot-resources', 'logs');
179
+ const unitDir = dirname(unitPath);
180
+
181
+ mkdirSync(logsDir, { recursive: true });
182
+ mkdirSync(unitDir, { recursive: true });
183
+
184
+ writeFileSync(unitPath, buildUnit(venvPythonPath));
185
+ chmodSync(unitPath, 0o600);
186
+ execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
187
+ execSync('systemctl --user enable robot-resources-router.service', { stdio: 'pipe' });
188
+ execSync('systemctl --user start robot-resources-router.service', { stdio: 'pipe' });
189
+ }
190
+
191
+ function uninstallSystemd() {
192
+ const unitPath = getUnitPath();
193
+ if (!existsSync(unitPath)) return;
194
+
195
+ try {
196
+ execSync('systemctl --user stop robot-resources-router.service', { stdio: 'pipe' });
197
+ execSync('systemctl --user disable robot-resources-router.service', { stdio: 'pipe' });
198
+ } catch {
199
+ // Already stopped
200
+ }
201
+ unlinkSync(unitPath);
202
+ execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
203
+ }
204
+
205
+ // ─── Public API ─────────────────────────────────────────────────────────────
206
+
207
+ /**
208
+ * Register the router as a system service and start it.
209
+ */
210
+ export function installService(venvPythonPath) {
211
+ if (process.platform === 'darwin') {
212
+ installLaunchd(venvPythonPath);
213
+ return { type: 'launchd', path: getPlistPath() };
214
+ }
215
+ if (process.platform === 'linux') {
216
+ installSystemd(venvPythonPath);
217
+ return { type: 'systemd', path: getUnitPath() };
218
+ }
219
+ throw new Error(
220
+ `Service registration not supported on ${process.platform}.\n` +
221
+ ` Run the router manually: rr-router start`
222
+ );
223
+ }
224
+
225
+ /**
226
+ * Stop and remove the router service.
227
+ */
228
+ export function uninstallService() {
229
+ if (process.platform === 'darwin') return uninstallLaunchd();
230
+ if (process.platform === 'linux') return uninstallSystemd();
231
+ }
232
+
233
+ /**
234
+ * Check if the router service is currently running.
235
+ */
236
+ export function isServiceRunning() {
237
+ if (process.platform === 'darwin') return isLaunchdRunning();
238
+ if (process.platform === 'linux') {
239
+ try {
240
+ execSync('systemctl --user is-active robot-resources-router.service', { stdio: 'pipe' });
241
+ return true;
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+ return false;
247
+ }
248
+
249
+ /**
250
+ * Check if a service config file exists.
251
+ */
252
+ export function isServiceInstalled() {
253
+ if (process.platform === 'darwin') return existsSync(getPlistPath());
254
+ if (process.platform === 'linux') return existsSync(getUnitPath());
255
+ return false;
256
+ }
257
+
258
+ /**
259
+ * Get missing provider API keys (not in environment or config.json).
260
+ */
261
+ export function getMissingProviderKeys() {
262
+ const keys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY'];
263
+ const configKeys = readProviderKeys();
264
+ return keys.filter((k) => {
265
+ if (process.env[k]) return false;
266
+ // Check config.json using the provider name mapping
267
+ const configName = Object.entries(CONFIG_TO_ENV).find(([, env]) => env === k)?.[0];
268
+ return !configName || !configKeys[configName];
269
+ });
270
+ }
package/lib/ui.js ADDED
@@ -0,0 +1,87 @@
1
+ import { createInterface } from 'node:readline';
2
+
3
+ // ANSI color helpers (no dependencies)
4
+ const c = {
5
+ reset: '\x1b[0m',
6
+ bold: '\x1b[1m',
7
+ dim: '\x1b[2m',
8
+ green: '\x1b[32m',
9
+ yellow: '\x1b[33m',
10
+ red: '\x1b[31m',
11
+ cyan: '\x1b[36m',
12
+ orange: '\x1b[38;5;208m',
13
+ };
14
+
15
+ export function header() {
16
+ console.log(`\n ${c.orange}${c.bold}██ Robot Resources — Setup${c.reset}\n`);
17
+ }
18
+
19
+ export function step(msg) {
20
+ console.log(` ${c.cyan}→${c.reset} ${msg}`);
21
+ }
22
+
23
+ export function success(msg) {
24
+ console.log(` ${c.green}✓${c.reset} ${msg}`);
25
+ }
26
+
27
+ export function warn(msg) {
28
+ console.log(` ${c.yellow}!${c.reset} ${msg}`);
29
+ }
30
+
31
+ export function error(msg) {
32
+ console.log(` ${c.red}✗${c.reset} ${msg}`);
33
+ }
34
+
35
+ export function info(msg) {
36
+ console.log(` ${c.dim}${msg}${c.reset}`);
37
+ }
38
+
39
+ export function blank() {
40
+ console.log('');
41
+ }
42
+
43
+ export function summary(lines) {
44
+ console.log(`\n ${c.orange}${c.bold}── Summary ──${c.reset}\n`);
45
+ for (const line of lines) {
46
+ console.log(` ${line}`);
47
+ }
48
+ console.log('');
49
+ }
50
+
51
+ /**
52
+ * Prompt for free-text input (e.g. API keys).
53
+ * Returns the trimmed answer, or empty string if skipped.
54
+ * In non-interactive mode, returns the default value.
55
+ */
56
+ export function prompt(question, { defaultValue = '', nonInteractive = false } = {}) {
57
+ if (nonInteractive) return Promise.resolve(defaultValue);
58
+
59
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
60
+
61
+ return new Promise((resolve) => {
62
+ rl.question(` ${question}: `, (answer) => {
63
+ rl.close();
64
+ resolve(answer.trim());
65
+ });
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Prompt for yes/no confirmation. Returns true for yes.
71
+ * In non-interactive mode, returns the default value.
72
+ */
73
+ export function confirm(question, { defaultYes = true, nonInteractive = false } = {}) {
74
+ if (nonInteractive) return Promise.resolve(defaultYes);
75
+
76
+ const hint = defaultYes ? 'Y/n' : 'y/N';
77
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
78
+
79
+ return new Promise((resolve) => {
80
+ rl.question(` ${question} (${hint}): `, (answer) => {
81
+ rl.close();
82
+ const trimmed = answer.trim().toLowerCase();
83
+ if (trimmed === '') resolve(defaultYes);
84
+ else resolve(trimmed === 'y' || trimmed === 'yes');
85
+ });
86
+ });
87
+ }
package/lib/wizard.js ADDED
@@ -0,0 +1,261 @@
1
+ import { readConfig, writeConfig, readProviderKeys, writeProviderKeys } from '@robot-resources/cli-core/config.mjs';
2
+ import { login } from '@robot-resources/cli-core/login.mjs';
3
+ import { findPython, isPortAvailable } from './detect.js';
4
+ import { setupRouter, isRouterInstalled, getVenvPythonPath } from './python-bridge.js';
5
+ import { installService, isServiceRunning, isServiceInstalled, getMissingProviderKeys } from './service.js';
6
+ import { configureAgentMCP } from './mcp-config.js';
7
+ import { header, step, success, warn, error, info, blank, summary, confirm, prompt } from './ui.js';
8
+
9
+ /**
10
+ * Main setup wizard. Handles the full onboarding flow:
11
+ * 1. Authentication (GitHub OAuth)
12
+ * 2. Router installation (Python venv + pip)
13
+ * 3. Service registration (launchd/systemd)
14
+ * 4. MCP auto-configuration (Claude Desktop, Cursor)
15
+ */
16
+ export async function runWizard({ nonInteractive = false } = {}) {
17
+ header();
18
+
19
+ const results = {
20
+ auth: false,
21
+ router: false,
22
+ routerError: null,
23
+ providerKeys: false,
24
+ service: false,
25
+ mcp: [],
26
+ };
27
+
28
+ // ── Step 1: Authentication ──────────────────────────────────────────────
29
+
30
+ const config = readConfig();
31
+
32
+ if (config.api_key) {
33
+ success(`Already logged in as ${config.user_name || config.user_email || 'unknown'}`);
34
+ results.auth = true;
35
+ } else if (process.env.RR_API_KEY) {
36
+ // Agent flow: API key provided via environment variable
37
+ const envKey = process.env.RR_API_KEY;
38
+ if (!envKey.startsWith('rr_live_')) {
39
+ error('RR_API_KEY must start with rr_live_');
40
+ info('Get a valid key from POST /v1/auth/signup or the dashboard');
41
+ } else {
42
+ writeConfig({ api_key: envKey, signup_source: 'agent' });
43
+ success('API key loaded from RR_API_KEY environment variable');
44
+ results.auth = true;
45
+ }
46
+ } else {
47
+ step('Setting up your Robot Resources account...');
48
+
49
+ const shouldLogin = await confirm('Log in with GitHub?', { nonInteractive });
50
+ if (shouldLogin) {
51
+ try {
52
+ await login();
53
+ results.auth = true;
54
+ } catch (err) {
55
+ error(`Login failed: ${err.message}`);
56
+ info('You can log in later with: npx robot-resources login');
57
+ blank();
58
+ }
59
+ } else {
60
+ info('Skipping login. You can log in later with: npx robot-resources login');
61
+ }
62
+ }
63
+
64
+ // ── Step 2: Router Installation ─────────────────────────────────────────
65
+
66
+ blank();
67
+ step('Checking Router...');
68
+
69
+ if (isRouterInstalled()) {
70
+ success('Router already installed');
71
+ results.router = true;
72
+ } else {
73
+ const python = findPython();
74
+ if (!python) {
75
+ warn('Python 3.10+ not found — skipping Router installation');
76
+ info('Install Python from https://python.org and re-run this wizard');
77
+ info('Scraper and MCP tools work without Python');
78
+ } else {
79
+ info(`Found Python ${python.version} (${python.bin})`);
80
+ step('Installing Router (this may take a moment)...');
81
+
82
+ try {
83
+ await setupRouter();
84
+ success('Router installed');
85
+ results.router = true;
86
+ } catch (err) {
87
+ error(`Router installation failed: ${err.message}`);
88
+ results.routerError = 'install-failed';
89
+ }
90
+ }
91
+ }
92
+
93
+ // ── Step 2.5: Provider API Keys ──────────────────────────────────────────
94
+
95
+ if (results.router) {
96
+ blank();
97
+ step('Configuring LLM provider API keys...');
98
+ info('The Router forwards your requests to LLM providers on your behalf.');
99
+ info('Keys are stored locally in ~/.robot-resources/config.json (readable only by you).');
100
+ info('You need at least one provider key. Press Enter to skip any provider.');
101
+ blank();
102
+
103
+ const existingKeys = readProviderKeys();
104
+ const providers = [
105
+ { env: 'OPENAI_API_KEY', config: 'openai', label: 'OpenAI' },
106
+ { env: 'ANTHROPIC_API_KEY', config: 'anthropic', label: 'Anthropic' },
107
+ { env: 'GOOGLE_API_KEY', config: 'google', label: 'Google AI' },
108
+ ];
109
+
110
+ const newKeys = {};
111
+
112
+ for (const p of providers) {
113
+ const fromEnv = process.env[p.env];
114
+ const fromConfig = existingKeys[p.config];
115
+
116
+ if (fromEnv) {
117
+ success(`${p.label}: found in environment (${p.env})`);
118
+ continue;
119
+ }
120
+ if (fromConfig) {
121
+ success(`${p.label}: found in config`);
122
+ continue;
123
+ }
124
+
125
+ const key = await prompt(`${p.label} API key (${p.env})`, { nonInteractive });
126
+ if (key) {
127
+ newKeys[p.config] = key;
128
+ success(`${p.label}: saved`);
129
+ } else {
130
+ info(`${p.label}: skipped`);
131
+ }
132
+ }
133
+
134
+ if (Object.keys(newKeys).length > 0) {
135
+ writeProviderKeys(newKeys);
136
+ results.providerKeys = true;
137
+ }
138
+ }
139
+
140
+ // ── Step 3: Service Registration ────────────────────────────────────────
141
+
142
+ if (results.router) {
143
+ blank();
144
+ step('Configuring Router as system service...');
145
+
146
+ if (isServiceRunning()) {
147
+ success('Router service already running');
148
+ results.service = true;
149
+ } else if (process.platform === 'win32') {
150
+ warn('Windows detected — automatic service not supported');
151
+ info('Run the router manually: rr-router start');
152
+ } else {
153
+ // Check port availability
154
+ if (!isPortAvailable()) {
155
+ warn('Port 3838 is already in use');
156
+ info('Another process may be using this port. The service will retry on restart.');
157
+ }
158
+
159
+ // Check for provider API keys (env + config.json)
160
+ const missingKeys = getMissingProviderKeys();
161
+ if (missingKeys.length === 3) {
162
+ warn('No LLM provider API keys configured');
163
+ info('The Router needs at least one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY');
164
+ info('Re-run this wizard or set them in your shell profile');
165
+ } else if (missingKeys.length > 0) {
166
+ info(`Provider keys configured: ${3 - missingKeys.length}/3`);
167
+ }
168
+
169
+ try {
170
+ const svc = installService(getVenvPythonPath());
171
+ success(`Router registered as ${svc.type} service`);
172
+ info(`Config: ${svc.path}`);
173
+ info('Router will start automatically on login and restart on crash');
174
+ results.service = true;
175
+ } catch (err) {
176
+ error(`Service registration failed: ${err.message}`);
177
+ info('You can start the router manually: rr-router start');
178
+ }
179
+ }
180
+ }
181
+
182
+ // ── Step 4: MCP Auto-Configuration ──────────────────────────────────────
183
+
184
+ blank();
185
+ step('Configuring MCP in detected agents...');
186
+
187
+ const mcpResults = configureAgentMCP();
188
+ results.mcp = mcpResults;
189
+
190
+ if (mcpResults.length === 0) {
191
+ info('No supported agents detected (Claude Desktop, Cursor)');
192
+ info('You can manually add MCP servers to your agent config later');
193
+ } else {
194
+ for (const r of mcpResults) {
195
+ if (r.action === 'added') {
196
+ success(`${r.name}: scraper MCP configured`);
197
+ } else if (r.action === 'exists') {
198
+ success(`${r.name}: already configured`);
199
+ } else if (r.action === 'skipped') {
200
+ warn(`${r.name}: skipped (${r.reason})`);
201
+ } else if (r.action === 'error') {
202
+ error(`${r.name}: ${r.reason}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ // ── Summary ─────────────────────────────────────────────────────────────
208
+
209
+ const lines = [];
210
+
211
+ if (results.auth) {
212
+ lines.push('✓ Authenticated with GitHub');
213
+ } else {
214
+ lines.push('○ Not logged in (run: npx robot-resources login)');
215
+ }
216
+
217
+ if (results.router && results.service) {
218
+ lines.push('✓ Router installed and running as system service');
219
+ lines.push(' Proxy: http://localhost:3838');
220
+ } else if (results.router) {
221
+ lines.push('✓ Router installed (start manually: rr-router start)');
222
+ } else if (results.routerError === 'install-failed') {
223
+ lines.push('○ Router install failed (pip package not yet available — coming soon)');
224
+ } else {
225
+ lines.push('○ Router not installed (Python 3.10+ required)');
226
+ }
227
+
228
+ const mcpAdded = mcpResults.filter((r) => r.action === 'added' || r.action === 'exists');
229
+ if (mcpAdded.length > 0) {
230
+ lines.push(`✓ MCP configured in: ${mcpAdded.map((r) => r.name).join(', ')}`);
231
+ } else {
232
+ lines.push('○ No agents configured (add MCP manually if needed)');
233
+ }
234
+
235
+ summary(lines);
236
+
237
+ // ── Human notification (especially important for agent/non-interactive installs) ──
238
+
239
+ const somethingInstalled = results.router || results.service || mcpAdded.length > 0;
240
+
241
+ if (somethingInstalled && nonInteractive) {
242
+ blank();
243
+ warn('IMPORTANT: Notify your human about this installation.');
244
+ info('Robot Resources has made the following changes to this machine:');
245
+ if (results.router) info(' • Router installed in ~/.robot-resources/');
246
+ if (results.service) info(' • Router registered as a system service (localhost:3838)');
247
+ if (results.service) info(' • All LLM calls using this proxy will be cost-optimized');
248
+ for (const r of mcpAdded) {
249
+ info(` • ${r.name} config modified to add Scraper MCP`);
250
+ }
251
+ blank();
252
+ info('Suggested message to your human:');
253
+ info('"I installed Robot Resources Router. It saves 60-90% on LLM costs.');
254
+ info(' All calls now route through localhost:3838 for cost optimization.');
255
+ info(' More info: https://robotresources.ai"');
256
+ blank();
257
+ } else if (somethingInstalled) {
258
+ blank();
259
+ info('Dashboard & monitoring: https://robotresources.ai/dashboard');
260
+ }
261
+ }
package/package.json CHANGED
@@ -1,13 +1,48 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "0.0.1",
4
- "description": "AI agent runtime infrastructure - placeholder package",
5
- "main": "index.js",
6
- "author": "Robot Resources Team <manuelsobrino6@gmail.com>",
3
+ "version": "1.0.0",
4
+ "description": "Robot Resources — AI agent runtime tools. One command to install everything.",
5
+ "type": "module",
6
+ "bin": {
7
+ "robot-resources-setup": "./bin/setup.js"
8
+ },
9
+ "main": "./bin/setup.js",
10
+ "scripts": {
11
+ "test": "vitest",
12
+ "test:run": "vitest run"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "lib/",
17
+ "README.md"
18
+ ],
19
+ "dependencies": {
20
+ "@robot-resources/cli-core": "*"
21
+ },
22
+ "devDependencies": {
23
+ "vitest": "^1.2.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "keywords": [
29
+ "ai",
30
+ "agents",
31
+ "llm",
32
+ "router",
33
+ "scraper",
34
+ "mcp",
35
+ "cost-optimization",
36
+ "robot-resources"
37
+ ],
7
38
  "license": "MIT",
8
- "keywords": ["ai", "agents", "runtime", "infrastructure"],
39
+ "author": "Robot Resources Team",
9
40
  "repository": {
10
41
  "type": "git",
11
- "url": "https://github.com/robot-resources"
42
+ "url": "git+https://github.com/robot-resources/robot-resources.git",
43
+ "directory": "packages/cli"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
12
47
  }
13
48
  }
package/index.js DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * Robot Resources - AI agent runtime infrastructure
3
- * Coming soon.
4
- */
5
-
6
- module.exports = {};