robot-resources 1.10.5 → 1.11.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/bin/setup.js +11 -2
- package/lib/auth.mjs +261 -0
- package/lib/config.mjs +55 -0
- package/lib/health-report.js +1 -1
- package/lib/login.mjs +54 -0
- package/lib/non-oc-wizard.js +238 -0
- package/lib/wizard.js +11 -16
- package/package.json +2 -2
package/bin/setup.js
CHANGED
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
import { runWizard } from '../lib/wizard.js';
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
|
-
const
|
|
6
|
+
const explicitNonInteractive =
|
|
7
|
+
args.includes('--non-interactive') || args.includes('--yes') || args.includes('-y');
|
|
8
|
+
const targetArg = args.find((a) => a.startsWith('--for='));
|
|
9
|
+
const target = targetArg ? targetArg.slice('--for='.length) : null;
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
// Treat piped/CI runs (no TTY on stdin OR stdout) as non-interactive so the
|
|
12
|
+
// wizard never blocks on a prompt that can't be answered. The interactive
|
|
13
|
+
// menu is only opened when both stdin and stdout are real terminals.
|
|
14
|
+
const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
15
|
+
const nonInteractive = explicitNonInteractive || !hasTty;
|
|
16
|
+
|
|
17
|
+
runWizard({ nonInteractive, target }).catch((err) => {
|
|
9
18
|
console.error(`\n ✗ Setup failed: ${err.message}\n`);
|
|
10
19
|
process.exit(1);
|
|
11
20
|
});
|
package/lib/auth.mjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
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>■ 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>■ 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
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
}
|
package/lib/health-report.js
CHANGED
package/lib/login.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { authenticate } from './auth.mjs';
|
|
2
|
+
import { writeConfig, getConfigPath } from './config.mjs';
|
|
3
|
+
|
|
4
|
+
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate an API key via the platform API.
|
|
8
|
+
*/
|
|
9
|
+
export async function createApiKey(accessToken) {
|
|
10
|
+
const res = await fetch(`${PLATFORM_URL}/v1/keys`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
Authorization: `Bearer ${accessToken}`,
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify({ name: `cli-${new Date().toISOString().slice(0, 10)}` }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const body = await res.text();
|
|
21
|
+
throw new Error(`Failed to create API key (${res.status}): ${body}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { data } = await res.json();
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function login() {
|
|
29
|
+
console.log('\n ██ Robot Resources — Login\n');
|
|
30
|
+
console.log(' Opening GitHub in your browser...');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { access_token, user } = await authenticate();
|
|
34
|
+
|
|
35
|
+
console.log(` ✓ Authenticated as ${user.user_metadata?.user_name || user.email}`);
|
|
36
|
+
console.log(' Generating API key...');
|
|
37
|
+
|
|
38
|
+
const key = await createApiKey(access_token);
|
|
39
|
+
|
|
40
|
+
writeConfig({
|
|
41
|
+
api_key: key.key,
|
|
42
|
+
key_id: key.id,
|
|
43
|
+
key_name: key.name,
|
|
44
|
+
user_email: user.email,
|
|
45
|
+
user_name: user.user_metadata?.user_name || null,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(` ✓ API key saved to ${getConfigPath()}`);
|
|
49
|
+
console.log(`\n You're all set. Router and Scraper will pick up the key automatically.\n`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`\n ✗ Login failed: ${err.message}\n`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { select } from '@inquirer/prompts';
|
|
4
|
+
import { isClaudeCodeInstalled, isCursorInstalled } from './detect.js';
|
|
5
|
+
import { configureClaudeCode, configureCursor } from './tool-config.js';
|
|
6
|
+
import { header, info, success, warn, blank } from './ui.js';
|
|
7
|
+
import { readConfig } from './config.mjs';
|
|
8
|
+
|
|
9
|
+
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
10
|
+
|
|
11
|
+
const PATH_LABELS = {
|
|
12
|
+
js: 'JS/TS agent (LangChain, LangGraph, Mastra, etc.)',
|
|
13
|
+
python: 'Python agent (LangChain, LlamaIndex, CrewAI, etc.)',
|
|
14
|
+
mcp: 'Cursor / Claude Code / other MCP tool',
|
|
15
|
+
docs: "Just point me at docs, I'll integrate manually",
|
|
16
|
+
'install-oc': 'Install OpenClaw first — exit',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const VALID_TARGETS = new Set(Object.keys(PATH_LABELS).concat(['langchain', 'claude-code']));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Inspect cwd to guess what the user is building. Returns one of the path
|
|
23
|
+
* keys, or null if we can't tell. Order matters: detect-by-file beats
|
|
24
|
+
* detect-by-installed-tool, since cwd evidence is stronger than "the user
|
|
25
|
+
* has Cursor installed somewhere on this machine."
|
|
26
|
+
*/
|
|
27
|
+
export function detectDefaultPath(cwd = process.cwd()) {
|
|
28
|
+
const pkgPath = join(cwd, 'package.json');
|
|
29
|
+
if (existsSync(pkgPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
32
|
+
const allDeps = {
|
|
33
|
+
...(pkg.dependencies ?? {}),
|
|
34
|
+
...(pkg.devDependencies ?? {}),
|
|
35
|
+
};
|
|
36
|
+
const jsAgentMarkers = ['langchain', '@langchain/core', '@langchain/langgraph', '@mastra/core', 'crewai-js', 'llamaindex'];
|
|
37
|
+
if (jsAgentMarkers.some((m) => Object.prototype.hasOwnProperty.call(allDeps, m))) {
|
|
38
|
+
return 'js';
|
|
39
|
+
}
|
|
40
|
+
// Generic JS project still defaults to JS (cheaper than asking).
|
|
41
|
+
return 'js';
|
|
42
|
+
} catch {
|
|
43
|
+
// fall through
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) {
|
|
48
|
+
return 'python';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isCursorInstalled() || isClaudeCodeInstalled()) {
|
|
52
|
+
return 'mcp';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeTarget(target) {
|
|
59
|
+
if (!target) return null;
|
|
60
|
+
const t = String(target).toLowerCase();
|
|
61
|
+
if (!VALID_TARGETS.has(t)) return null;
|
|
62
|
+
// Aliases — friendly synonyms map to canonical path keys.
|
|
63
|
+
if (t === 'langchain') return 'js';
|
|
64
|
+
if (t === 'claude-code') return 'mcp';
|
|
65
|
+
return t;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function emitPathChosen(path) {
|
|
69
|
+
const config = readConfig();
|
|
70
|
+
if (!config.api_key) return; // wizard didn't get to provision; can't authenticate
|
|
71
|
+
try {
|
|
72
|
+
await fetch(`${PLATFORM_URL}/v1/telemetry`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
product: 'cli',
|
|
80
|
+
event_type: 'wizard_path_chosen',
|
|
81
|
+
payload: { path, platform: process.platform },
|
|
82
|
+
}),
|
|
83
|
+
signal: AbortSignal.timeout(5_000),
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
// Best-effort — never let telemetry break the install path.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function showJsPath() {
|
|
91
|
+
blank();
|
|
92
|
+
success('JS/TS integration');
|
|
93
|
+
blank();
|
|
94
|
+
info('Install:');
|
|
95
|
+
info(' npm install @robot-resources/router');
|
|
96
|
+
blank();
|
|
97
|
+
info('Use:');
|
|
98
|
+
info(' import { routePrompt } from \'@robot-resources/router/routing\';');
|
|
99
|
+
info(' const decision = routePrompt(\'write a python function\');');
|
|
100
|
+
info(' console.log(decision.selected_model); // e.g. \'claude-haiku-4-5\'');
|
|
101
|
+
blank();
|
|
102
|
+
info('Full docs: https://robotresources.ai/docs/langchain');
|
|
103
|
+
blank();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function showPythonPath() {
|
|
107
|
+
blank();
|
|
108
|
+
success('Python integration');
|
|
109
|
+
blank();
|
|
110
|
+
info('Install:');
|
|
111
|
+
info(' pip install robot-resources-router');
|
|
112
|
+
blank();
|
|
113
|
+
info('Use:');
|
|
114
|
+
info(' from rr_router import route');
|
|
115
|
+
info(' decision = route(\'write a python function\')');
|
|
116
|
+
info(' print(decision[\'selected_model\']) # e.g. \'claude-haiku-4-5\'');
|
|
117
|
+
blank();
|
|
118
|
+
info('Full docs: https://robotresources.ai/docs/python');
|
|
119
|
+
blank();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function showMcpPath() {
|
|
123
|
+
blank();
|
|
124
|
+
success('MCP tool integration');
|
|
125
|
+
blank();
|
|
126
|
+
let cursorOk = false;
|
|
127
|
+
let claudeOk = false;
|
|
128
|
+
if (isCursorInstalled()) {
|
|
129
|
+
try {
|
|
130
|
+
const result = configureCursor();
|
|
131
|
+
cursorOk = result?.action === 'configured' || result?.action === 'already_configured';
|
|
132
|
+
info(`Cursor: ${cursorOk ? 'configured' : 'see manual instructions below'}`);
|
|
133
|
+
} catch {
|
|
134
|
+
warn('Cursor: failed to write ~/.cursor/mcp.json automatically');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (isClaudeCodeInstalled()) {
|
|
138
|
+
try {
|
|
139
|
+
const result = configureClaudeCode();
|
|
140
|
+
claudeOk = result?.action === 'configured' || result?.action === 'already_configured';
|
|
141
|
+
info(`Claude Code: ${claudeOk ? 'configured' : 'see manual instructions below'}`);
|
|
142
|
+
} catch {
|
|
143
|
+
warn('Claude Code: failed to write ~/.claude/settings.json automatically');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!cursorOk && !claudeOk) {
|
|
147
|
+
info('We did not detect Cursor or Claude Code on this machine.');
|
|
148
|
+
info('Manual setup: https://robotresources.ai/docs/cursor-mcp');
|
|
149
|
+
}
|
|
150
|
+
blank();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function showDocsPath() {
|
|
154
|
+
blank();
|
|
155
|
+
success('Docs');
|
|
156
|
+
blank();
|
|
157
|
+
info('Integration guides: https://robotresources.ai/docs');
|
|
158
|
+
info('HTTP API: https://robotresources.ai/docs/http-api');
|
|
159
|
+
info('GitHub: https://github.com/robot-resources/robot-resources');
|
|
160
|
+
blank();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function showInstallOcPath() {
|
|
164
|
+
blank();
|
|
165
|
+
info('OpenClaw is the easiest way to use Robot Resources.');
|
|
166
|
+
info('Install OpenClaw first (https://openclaw.dev), then re-run:');
|
|
167
|
+
info(' npx robot-resources');
|
|
168
|
+
blank();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function runPath(path) {
|
|
172
|
+
switch (path) {
|
|
173
|
+
case 'js': showJsPath(); break;
|
|
174
|
+
case 'python': showPythonPath(); break;
|
|
175
|
+
case 'mcp': showMcpPath(); break;
|
|
176
|
+
case 'docs': showDocsPath(); break;
|
|
177
|
+
case 'install-oc': showInstallOcPath(); break;
|
|
178
|
+
default: showInstallOcPath(); break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Runs the non-OC wizard. Three modes:
|
|
184
|
+
* - target supplied (--for=<target>): run that path directly, no prompt
|
|
185
|
+
* - non-interactive AND no target: print hint with --for= options and exit
|
|
186
|
+
* - interactive: 5-option menu via @inquirer/prompts.select
|
|
187
|
+
*/
|
|
188
|
+
export async function runNonOcWizard({ nonInteractive = false, target = null } = {}) {
|
|
189
|
+
const normalized = normalizeTarget(target);
|
|
190
|
+
|
|
191
|
+
if (normalized) {
|
|
192
|
+
runPath(normalized);
|
|
193
|
+
await emitPathChosen(normalized);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (nonInteractive) {
|
|
198
|
+
info('Robot Resources requires OpenClaw, which we did not detect on this machine.');
|
|
199
|
+
info('To bypass this prompt in CI / non-TTY contexts, re-run with --for=<target>:');
|
|
200
|
+
info(' npx robot-resources --for=langchain # JS/TS agent');
|
|
201
|
+
info(' npx robot-resources --for=python # Python agent');
|
|
202
|
+
info(' npx robot-resources --for=cursor # Cursor MCP config');
|
|
203
|
+
info(' npx robot-resources --for=claude-code # Claude Code MCP config');
|
|
204
|
+
info(' npx robot-resources --for=docs # docs URL');
|
|
205
|
+
blank();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Interactive menu.
|
|
210
|
+
header();
|
|
211
|
+
info('Robot Resources requires OpenClaw, which we did not detect on this machine.');
|
|
212
|
+
info('What are you building? Pick the closest match — we\'ll show the install steps.');
|
|
213
|
+
blank();
|
|
214
|
+
|
|
215
|
+
const defaultPath = detectDefaultPath() ?? 'js';
|
|
216
|
+
|
|
217
|
+
let chosen;
|
|
218
|
+
try {
|
|
219
|
+
chosen = await select({
|
|
220
|
+
message: 'What are you building?',
|
|
221
|
+
default: defaultPath,
|
|
222
|
+
choices: [
|
|
223
|
+
{ name: PATH_LABELS.js, value: 'js' },
|
|
224
|
+
{ name: PATH_LABELS.python, value: 'python' },
|
|
225
|
+
{ name: PATH_LABELS.mcp, value: 'mcp' },
|
|
226
|
+
{ name: PATH_LABELS.docs, value: 'docs' },
|
|
227
|
+
{ name: PATH_LABELS['install-oc'], value: 'install-oc' },
|
|
228
|
+
],
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// User hit Ctrl-C or terminal closed — exit cleanly.
|
|
232
|
+
if (err && (err.name === 'ExitPromptError' || err.code === 'ABORT_ERR')) return;
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
runPath(chosen);
|
|
237
|
+
await emitPathChosen(chosen);
|
|
238
|
+
}
|
package/lib/wizard.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir, hostname, release as osRelease } from 'node:os';
|
|
4
|
-
import { readConfig, writeConfig } from '
|
|
4
|
+
import { readConfig, writeConfig } from './config.mjs';
|
|
5
5
|
import { isOpenClawInstalled } from './detect.js';
|
|
6
6
|
import { getOrCreateMachineId } from './machine-id.js';
|
|
7
7
|
import { configureToolRouting, registerScraperMcp, restartOpenClawGateway } from './tool-config.js';
|
|
8
8
|
import { checkHealth } from './health-report.js';
|
|
9
9
|
import { header, step, success, warn, error, info, blank, summary } from './ui.js';
|
|
10
|
+
import { runNonOcWizard } from './non-oc-wizard.js';
|
|
10
11
|
|
|
11
12
|
// Stamped onto every CLI telemetry payload so we can tell which `robot-resources`
|
|
12
13
|
// version a user actually ran. Without this, npx-cached old installers look
|
|
@@ -41,23 +42,17 @@ const CLI_VERSION = (() => {
|
|
|
41
42
|
*
|
|
42
43
|
* No Python, no venv, no systemd, no port probe.
|
|
43
44
|
*/
|
|
44
|
-
export async function runWizard({ nonInteractive = false } = {}) {
|
|
45
|
+
export async function runWizard({ nonInteractive = false, target = null } = {}) {
|
|
45
46
|
header();
|
|
46
47
|
|
|
47
|
-
// Non-OC
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
info('Robot Resources requires OpenClaw, which we did not detect on this machine.');
|
|
56
|
-
info('Install OpenClaw first (https://openclaw.dev), then re-run:');
|
|
57
|
-
info(' npx robot-resources');
|
|
58
|
-
blank();
|
|
59
|
-
info('If you are integrating Robot Resources into a non-OC agent, see PR 7 docs');
|
|
60
|
-
info('(coming soon — https://robotresources.ai/docs/integrations).');
|
|
48
|
+
// Non-OC branch. Hands off to the multi-agent compatibility wizard which
|
|
49
|
+
// routes the user to the right install path (npm install / pip install /
|
|
50
|
+
// MCP config / docs / install-OC). Non-interactive callers bypass into the
|
|
51
|
+
// OC install path only when --for=<target> isn't supplied; otherwise they
|
|
52
|
+
// get the print-and-exit hint with the supported --for= options.
|
|
53
|
+
// Pre-PR-8 this was a 17-line print-and-exit; PR 8 made it interactive.
|
|
54
|
+
if (!isOpenClawInstalled()) {
|
|
55
|
+
await runNonOcWizard({ nonInteractive, target });
|
|
61
56
|
return;
|
|
62
57
|
}
|
|
63
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robot-resources",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Robot Resources — AI agent tools. One command to install everything.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@
|
|
20
|
+
"@inquirer/prompts": "^7.0.0",
|
|
21
21
|
"@robot-resources/router": "*",
|
|
22
22
|
"@robot-resources/scraper": "*"
|
|
23
23
|
},
|