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 +68 -6
- package/bin/setup.js +11 -0
- package/lib/detect.js +85 -0
- package/lib/mcp-config.js +90 -0
- package/lib/python-bridge.js +28 -0
- package/lib/service.js +270 -0
- package/lib/ui.js +87 -0
- package/lib/wizard.js +261 -0
- package/package.json +41 -6
- package/index.js +0 -6
package/README.md
CHANGED
|
@@ -1,11 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# robot-resources — Unified Installer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One command to install all Robot Resources tools:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```
|
|
6
|
+
npx robot-resources
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## What the wizard does
|
|
8
10
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
"description": "AI agent runtime
|
|
5
|
-
"
|
|
6
|
-
"
|
|
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
|
-
"
|
|
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
|
}
|