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 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 nonInteractive = args.includes('--non-interactive') || args.includes('--yes') || args.includes('-y');
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
- runWizard({ nonInteractive }).catch((err) => {
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>&#9632; Robot Resources</h1>
111
+ <p style="color:#00ff41">Login successful. You can close this tab.</p>
112
+ </div>
113
+ </body>
114
+ </html>
115
+ `);
116
+ clearTimeout(timeout);
117
+ server.close();
118
+ resolveResult({ type: 'code', code });
119
+ } else {
120
+ // No code in query params — serve page that captures the full URL
121
+ // (fragment tokens, errors, etc.) and sends it back via nonce-protected endpoint
122
+ res.writeHead(200, { 'Content-Type': 'text/html' });
123
+ res.end(`
124
+ <html>
125
+ <body style="background:#0a0a0a;color:#ff6600;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
126
+ <div style="text-align:center">
127
+ <h1>&#9632; Robot Resources</h1>
128
+ <p id="msg">Completing login...</p>
129
+ </div>
130
+ </body>
131
+ <script>
132
+ const fullUrl = window.location.href;
133
+ const hash = window.location.hash.substring(1);
134
+ const payload = hash || 'NO_FRAGMENT:' + fullUrl;
135
+ fetch('${tokenPath}', { method: 'POST', body: payload })
136
+ .then(() => {
137
+ if (hash && hash.includes('access_token')) {
138
+ document.getElementById('msg').style.color = '#00ff41';
139
+ document.getElementById('msg').textContent = 'Login successful. You can close this tab.';
140
+ } else {
141
+ document.getElementById('msg').style.color = '#ffaa00';
142
+ document.getElementById('msg').textContent = 'Something went wrong. Check terminal.';
143
+ }
144
+ });
145
+ </script>
146
+ </html>
147
+ `);
148
+ }
149
+ return;
150
+ }
151
+
152
+ // Reject all other paths
153
+ res.writeHead(404);
154
+ res.end();
155
+ });
156
+
157
+ server.on('error', (err) => {
158
+ clearTimeout(timeout);
159
+ rejectResult(new Error(`Could not start callback server: ${err.message}`));
160
+ });
161
+
162
+ return { server, resultPromise, nonce };
163
+ }
164
+
165
+ /**
166
+ * Exchange the auth code + PKCE verifier for a Supabase session.
167
+ */
168
+ async function exchangeCodeForSession(code, codeVerifier) {
169
+ const res = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=pkce`, {
170
+ method: 'POST',
171
+ headers: {
172
+ 'Content-Type': 'application/json',
173
+ apikey: SUPABASE_ANON_KEY,
174
+ },
175
+ body: JSON.stringify({
176
+ auth_code: code,
177
+ code_verifier: codeVerifier,
178
+ }),
179
+ });
180
+
181
+ if (!res.ok) {
182
+ const body = await res.text();
183
+ throw new Error(`Token exchange failed (${res.status}): ${body}`);
184
+ }
185
+
186
+ return res.json();
187
+ }
188
+
189
+ /**
190
+ * Try to listen on the preferred port, fall back to OS-assigned port.
191
+ */
192
+ function listenWithFallback(server) {
193
+ return new Promise((resolve, reject) => {
194
+ server.once('error', (err) => {
195
+ if (err.code === 'EADDRINUSE') {
196
+ // Preferred port busy — let OS assign one
197
+ server.listen(0, '127.0.0.1', () => resolve(server.address().port));
198
+ } else {
199
+ reject(err);
200
+ }
201
+ });
202
+ server.listen(PREFERRED_PORT, '127.0.0.1', () => resolve(server.address().port));
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Full OAuth flow with PKCE + implicit fallback.
208
+ * Returns { access_token, refresh_token, user }.
209
+ */
210
+ export async function authenticate() {
211
+ const { verifier, challenge } = generatePKCE();
212
+
213
+ // Create server and wait for it to be listening
214
+ const { server, resultPromise } = createCallbackServer();
215
+
216
+ const port = await listenWithFallback(server);
217
+ const callbackUrl = `http://localhost:${port}/callback`;
218
+ const authUrl = buildAuthUrl(challenge, callbackUrl);
219
+
220
+ console.log(`\n Auth URL: ${authUrl}\n`);
221
+
222
+ // Open browser (use execFile to avoid shell injection)
223
+ const { execFile } = await import('node:child_process');
224
+ if (process.platform === 'win32') {
225
+ // 'start' is a cmd.exe builtin, not an executable — must invoke via cmd.exe
226
+ execFile('cmd.exe', ['/c', 'start', '""', authUrl]);
227
+ } else {
228
+ const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
229
+ execFile(openCmd, [authUrl]);
230
+ }
231
+
232
+ console.log(' Waiting for GitHub authorization...\n');
233
+
234
+ // Wait for the callback
235
+ const result = await resultPromise;
236
+
237
+ if (result.type === 'code') {
238
+ // PKCE flow — exchange code for session
239
+ const session = await exchangeCodeForSession(result.code, verifier);
240
+ return {
241
+ access_token: session.access_token,
242
+ refresh_token: session.refresh_token,
243
+ user: session.user,
244
+ };
245
+ } else {
246
+ // Implicit flow fallback — we have the access_token directly
247
+ const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
248
+ headers: {
249
+ apikey: SUPABASE_ANON_KEY,
250
+ Authorization: `Bearer ${result.access_token}`,
251
+ },
252
+ });
253
+ if (!res.ok) throw new Error('Failed to fetch user info');
254
+ const user = await res.json();
255
+ return {
256
+ access_token: result.access_token,
257
+ refresh_token: null,
258
+ user,
259
+ };
260
+ }
261
+ }
package/lib/config.mjs 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
+ }
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { readConfig } from '@robot-resources/cli-core/config.mjs';
4
+ import { readConfig } from './config.mjs';
5
5
 
6
6
  const PROBE_TIMEOUT_MS = 5_000;
7
7
 
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 '@robot-resources/cli-core/config.mjs';
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 interactive early-exit. Without this, a human running
48
- // `npx robot-resources` on a machine without OpenClaw would still
49
- // provision an api_key, fire wizard_started + install_complete
50
- // telemetry, and write ~/.robot-resources/config.json six no-op
51
- // side effects against a machine that can't actually use the product.
52
- // Non-interactive callers (CI, agents, scripts that pre-set RR_API_KEY)
53
- // bypass: they explicitly chose to run the wizard.
54
- if (!isOpenClawInstalled() && !nonInteractive) {
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.10.5",
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
- "@robot-resources/cli-core": "*",
20
+ "@inquirer/prompts": "^7.0.0",
21
21
  "@robot-resources/router": "*",
22
22
  "@robot-resources/scraper": "*"
23
23
  },