robot-resources 1.15.1 → 1.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -49
- package/README.md +0 -104
- package/bin/setup.js +0 -43
- package/lib/auth.mjs +0 -261
- package/lib/config.mjs +0 -55
- package/lib/detect.js +0 -254
- package/lib/health-report.js +0 -130
- package/lib/install-node-shim.js +0 -188
- package/lib/install-python-shim.js +0 -107
- package/lib/install-router-files.js +0 -48
- package/lib/json5.js +0 -16
- package/lib/login.mjs +0 -54
- package/lib/machine-id.js +0 -31
- package/lib/non-oc-wizard.js +0 -615
- package/lib/shell-config.js +0 -183
- package/lib/source-edit-attach.js +0 -469
- package/lib/tool-config.js +0 -504
- package/lib/ui.js +0 -87
- package/lib/uninstall.js +0 -204
- package/lib/venv-detect.js +0 -85
- package/lib/windows-env.js +0 -202
- package/lib/wizard.js +0 -523
package/lib/non-oc-wizard.js
DELETED
|
@@ -1,615 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { extname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { select, input } from '@inquirer/prompts';
|
|
5
|
-
import { isClaudeCodeInstalled, isCursorInstalled, detectAgentRuntime } from './detect.js';
|
|
6
|
-
import { configureClaudeCode, configureCursor } from './tool-config.js';
|
|
7
|
-
import { header, info, success, warn, blank, confirm } from './ui.js';
|
|
8
|
-
import { readConfig } from './config.mjs';
|
|
9
|
-
import { installNodeShim } from './install-node-shim.js';
|
|
10
|
-
import { installPythonShim } from './install-python-shim.js';
|
|
11
|
-
import {
|
|
12
|
-
findAgentSourceFile,
|
|
13
|
-
hasSourceMarker,
|
|
14
|
-
writeSourceMarker,
|
|
15
|
-
previewInjection,
|
|
16
|
-
pathForTelemetry,
|
|
17
|
-
} from './source-edit-attach.js';
|
|
18
|
-
|
|
19
|
-
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
20
|
-
|
|
21
|
-
const PATH_LABELS = {
|
|
22
|
-
js: 'JS/TS agent (LangChain, LangGraph, Mastra, etc.)',
|
|
23
|
-
python: 'Python agent (LangChain, LlamaIndex, CrewAI, etc.)',
|
|
24
|
-
mcp: 'Cursor / Claude Code / other MCP tool',
|
|
25
|
-
docs: "Just point me at docs, I'll integrate manually",
|
|
26
|
-
'install-oc': 'Install OpenClaw first — exit',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const VALID_TARGETS = new Set(Object.keys(PATH_LABELS).concat(['langchain', 'claude-code']));
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Inspect cwd to guess what the user is building. Returns one of the path
|
|
33
|
-
* keys, or null if we can't tell. Order matters: detect-by-file beats
|
|
34
|
-
* detect-by-installed-tool, since cwd evidence is stronger than "the user
|
|
35
|
-
* has Cursor installed somewhere on this machine."
|
|
36
|
-
*/
|
|
37
|
-
export function detectDefaultPath(cwd = process.cwd()) {
|
|
38
|
-
const pkgPath = join(cwd, 'package.json');
|
|
39
|
-
if (existsSync(pkgPath)) {
|
|
40
|
-
try {
|
|
41
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
42
|
-
const allDeps = {
|
|
43
|
-
...(pkg.dependencies ?? {}),
|
|
44
|
-
...(pkg.devDependencies ?? {}),
|
|
45
|
-
};
|
|
46
|
-
const jsAgentMarkers = ['langchain', '@langchain/core', '@langchain/langgraph', '@mastra/core', 'crewai-js', 'llamaindex'];
|
|
47
|
-
if (jsAgentMarkers.some((m) => Object.prototype.hasOwnProperty.call(allDeps, m))) {
|
|
48
|
-
return 'js';
|
|
49
|
-
}
|
|
50
|
-
// Generic JS project still defaults to JS (cheaper than asking).
|
|
51
|
-
return 'js';
|
|
52
|
-
} catch {
|
|
53
|
-
// fall through
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) {
|
|
58
|
-
return 'python';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (isCursorInstalled() || isClaudeCodeInstalled()) {
|
|
62
|
-
return 'mcp';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normalizeTarget(target) {
|
|
69
|
-
if (!target) return null;
|
|
70
|
-
const t = String(target).toLowerCase();
|
|
71
|
-
if (!VALID_TARGETS.has(t)) return null;
|
|
72
|
-
// Aliases — friendly synonyms map to canonical path keys.
|
|
73
|
-
if (t === 'langchain') return 'js';
|
|
74
|
-
if (t === 'claude-code') return 'mcp';
|
|
75
|
-
return t;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function emitPathChosen(path) {
|
|
79
|
-
const config = readConfig();
|
|
80
|
-
if (!config.api_key) return; // wizard didn't get to provision; can't authenticate
|
|
81
|
-
try {
|
|
82
|
-
await fetch(`${PLATFORM_URL}/v1/telemetry`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: {
|
|
85
|
-
Authorization: `Bearer ${config.api_key}`,
|
|
86
|
-
'Content-Type': 'application/json',
|
|
87
|
-
},
|
|
88
|
-
body: JSON.stringify({
|
|
89
|
-
product: 'cli',
|
|
90
|
-
event_type: 'wizard_path_chosen',
|
|
91
|
-
payload: { path, platform: process.platform },
|
|
92
|
-
}),
|
|
93
|
-
signal: AbortSignal.timeout(5_000),
|
|
94
|
-
});
|
|
95
|
-
} catch {
|
|
96
|
-
// Best-effort — never let telemetry break the install path.
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function emitNodeEntryPatched(payload) {
|
|
101
|
-
const config = readConfig();
|
|
102
|
-
if (!config.api_key) return;
|
|
103
|
-
try {
|
|
104
|
-
await fetch(`${PLATFORM_URL}/v1/telemetry`, {
|
|
105
|
-
method: 'POST',
|
|
106
|
-
headers: {
|
|
107
|
-
Authorization: `Bearer ${config.api_key}`,
|
|
108
|
-
'Content-Type': 'application/json',
|
|
109
|
-
},
|
|
110
|
-
body: JSON.stringify({
|
|
111
|
-
product: 'cli',
|
|
112
|
-
event_type: 'node_entry_patched',
|
|
113
|
-
payload: { ...payload, platform: process.platform },
|
|
114
|
-
}),
|
|
115
|
-
signal: AbortSignal.timeout(5_000),
|
|
116
|
-
});
|
|
117
|
-
} catch {
|
|
118
|
-
// Best-effort — never let telemetry break the install path.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const VALID_SOURCE_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx']);
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Expand `~` to $HOME. Same trick shells use. Anything else (env vars,
|
|
126
|
-
* `..` segments) we leave to `path.resolve` to handle.
|
|
127
|
-
*/
|
|
128
|
-
function expandPath(raw, cwd) {
|
|
129
|
-
let p = raw.trim();
|
|
130
|
-
if (p === '~') p = homedir();
|
|
131
|
-
else if (p.startsWith('~/') || p.startsWith('~\\')) p = join(homedir(), p.slice(2));
|
|
132
|
-
if (!isAbsolute(p)) p = resolve(cwd, p);
|
|
133
|
-
return p;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Phase 11 + 11.1 — offer to inject `require('@robot-resources/router/auto')`
|
|
138
|
-
* (or ESM equivalent) at the top of the user's entry file. Phase 11 wrote
|
|
139
|
-
* the plumbing; Phase 11.1 swaps the package.json-gated detector for an
|
|
140
|
-
* SDK-import scanner that finds files actually talking to AI models.
|
|
141
|
-
*
|
|
142
|
-
* Why source-edit at all: NODE_OPTIONS in shell config never reaches cron /
|
|
143
|
-
* Docker / systemd / Lambda agents because those launchers don't read shell
|
|
144
|
-
* rc files. A line in the source survives any launcher.
|
|
145
|
-
*
|
|
146
|
-
* Decision matrix (now driven by findAgentSourceFile):
|
|
147
|
-
* - 0 SDK files found → free-text path prompt (interactive) or skip (CI)
|
|
148
|
-
* - 1 candidate → preview + Y/N (skip the chooser)
|
|
149
|
-
* - 2+, clear winner → preview winner + Y/N (skip the chooser)
|
|
150
|
-
* - 2+, ambiguous → select() chooser, then Y/N
|
|
151
|
-
* - Marker already present → skip silently
|
|
152
|
-
* - Non-interactive without `autoAttachSource` → skip with a hint
|
|
153
|
-
*
|
|
154
|
-
* Source files are sacred — every accept path goes through Y/N (or the
|
|
155
|
-
* explicit `--auto-attach-source` opt-in for CI).
|
|
156
|
-
*/
|
|
157
|
-
async function maybeInjectSourceEdit({ nonInteractive, autoAttachSource, cwd = process.cwd() } = {}) {
|
|
158
|
-
const scan = findAgentSourceFile(cwd);
|
|
159
|
-
const baseTelemetry = {
|
|
160
|
-
candidates_scanned: scan.scanned,
|
|
161
|
-
total_files_walked: scan.walked,
|
|
162
|
-
winner_score: scan.candidates[0]?.score ?? null,
|
|
163
|
-
runner_up_score: scan.candidates[1]?.score ?? null,
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// ── 0 candidates ───────────────────────────────────────────────────────
|
|
167
|
-
if (!scan.winner) {
|
|
168
|
-
if (nonInteractive) {
|
|
169
|
-
blank();
|
|
170
|
-
info(`We scanned ${scan.walked} source files and found no AI SDK imports.`);
|
|
171
|
-
info('Add this line manually at the top of your agent entry file:');
|
|
172
|
-
info(" require('@robot-resources/router/auto'); // CJS, or import '...' for ESM/TS");
|
|
173
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'no_sdk_imports_found' });
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Interactive 0-candidate: ask the user to point us at the file.
|
|
178
|
-
blank();
|
|
179
|
-
info(`We scanned ${scan.walked} source files in this directory and found no AI SDK imports.`);
|
|
180
|
-
blank();
|
|
181
|
-
let raw;
|
|
182
|
-
try {
|
|
183
|
-
raw = await input({
|
|
184
|
-
message: 'Path to your agent file (or press Enter to skip):',
|
|
185
|
-
default: '',
|
|
186
|
-
});
|
|
187
|
-
} catch {
|
|
188
|
-
// Ctrl-C or terminal closed.
|
|
189
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'declined_path_prompt' });
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (!raw || !raw.trim()) {
|
|
193
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'declined_path_prompt' });
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const target = expandPath(raw, cwd);
|
|
198
|
-
if (!existsSync(target)) {
|
|
199
|
-
warn(`File does not exist: ${target}`);
|
|
200
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'failed', error: 'path_missing' });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (!VALID_SOURCE_EXTS.has(extname(target).toLowerCase())) {
|
|
204
|
-
warn(`Refusing to edit ${extname(target)} — only .js/.mjs/.cjs/.ts/.tsx/.jsx are supported.`);
|
|
205
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'failed', error: 'invalid_extension' });
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (hasSourceMarker(target)) {
|
|
209
|
-
info(`${pathForTelemetry(target, cwd)} already has the auto-attach line — leaving it as-is.`);
|
|
210
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'already_present', entry_path: pathForTelemetry(target, cwd) });
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Outside-cwd: extra confirmation. The user meant a path relative to
|
|
215
|
-
// their project; if they typed something far away, double-check.
|
|
216
|
-
const isOutsideCwd = !target.startsWith(cwd + '/') && target !== cwd;
|
|
217
|
-
if (isOutsideCwd) {
|
|
218
|
-
const ok = await confirm(`${target} is outside your current directory. Edit it anyway?`, { defaultYes: false });
|
|
219
|
-
if (!ok) {
|
|
220
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'declined_path_prompt' });
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return await applyInjection({
|
|
226
|
-
target,
|
|
227
|
-
cwd,
|
|
228
|
-
nonInteractive,
|
|
229
|
-
autoAttachSource,
|
|
230
|
-
baseTelemetry,
|
|
231
|
-
viaPrompt: true,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ── 1+ candidates ──────────────────────────────────────────────────────
|
|
236
|
-
const winnerPath = scan.winner;
|
|
237
|
-
|
|
238
|
-
if (hasSourceMarker(winnerPath)) {
|
|
239
|
-
info(`${pathForTelemetry(winnerPath, cwd)} already has the auto-attach line — leaving it as-is.`);
|
|
240
|
-
await emitNodeEntryPatched({
|
|
241
|
-
...baseTelemetry,
|
|
242
|
-
outcome: 'already_present',
|
|
243
|
-
entry_path: pathForTelemetry(winnerPath, cwd),
|
|
244
|
-
});
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (nonInteractive && !autoAttachSource) {
|
|
249
|
-
blank();
|
|
250
|
-
info('Skipping source-attach in non-interactive mode (auto-rewriting source needs consent).');
|
|
251
|
-
info(`To enable it in CI, re-run with: npx robot-resources --for=langchain --auto-attach-source`);
|
|
252
|
-
info(`Or add this line manually at the top of ${pathForTelemetry(winnerPath, cwd)}:`);
|
|
253
|
-
info(" require('@robot-resources/router/auto'); // CJS, or import '...' for ESM/TS");
|
|
254
|
-
await emitNodeEntryPatched({
|
|
255
|
-
...baseTelemetry,
|
|
256
|
-
outcome: 'skipped_non_interactive',
|
|
257
|
-
entry_path: pathForTelemetry(winnerPath, cwd),
|
|
258
|
-
});
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Resolve the actual target. Skip the chooser when winner is unambiguous —
|
|
263
|
-
// most users see exactly one Y/N.
|
|
264
|
-
let target = winnerPath;
|
|
265
|
-
if (!nonInteractive && scan.ambiguous && scan.candidates.length > 1) {
|
|
266
|
-
const choices = scan.candidates.slice(0, 6).map((c) => ({
|
|
267
|
-
name: `${pathForTelemetry(c.path, cwd)} (score ${c.score})`,
|
|
268
|
-
value: c.path,
|
|
269
|
-
}));
|
|
270
|
-
choices.push({ name: 'Skip — I\'ll add the line manually', value: '__skip__' });
|
|
271
|
-
try {
|
|
272
|
-
target = await select({
|
|
273
|
-
message: 'Multiple files import an AI SDK. Which one is your agent?',
|
|
274
|
-
default: winnerPath,
|
|
275
|
-
choices,
|
|
276
|
-
});
|
|
277
|
-
} catch {
|
|
278
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'declined' });
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if (target === '__skip__') {
|
|
282
|
-
await emitNodeEntryPatched({ ...baseTelemetry, outcome: 'declined' });
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return await applyInjection({
|
|
288
|
-
target,
|
|
289
|
-
cwd,
|
|
290
|
-
nonInteractive,
|
|
291
|
-
autoAttachSource,
|
|
292
|
-
baseTelemetry,
|
|
293
|
-
viaPrompt: false,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Common write path used by both the "winner detected" and "user typed a
|
|
299
|
-
* path" branches. Shows the diff, asks Y/N (unless --auto-attach-source),
|
|
300
|
-
* writes the marker, emits telemetry. Single source of truth so the two
|
|
301
|
-
* branches can't drift.
|
|
302
|
-
*/
|
|
303
|
-
async function applyInjection({ target, cwd, nonInteractive, autoAttachSource, baseTelemetry, viaPrompt }) {
|
|
304
|
-
const preview = previewInjection(target);
|
|
305
|
-
blank();
|
|
306
|
-
success('Add the auto-attach line so routing also works under cron / Docker / systemd / Lambda?');
|
|
307
|
-
blank();
|
|
308
|
-
info(`File: ${pathForTelemetry(target, cwd)}`);
|
|
309
|
-
blank();
|
|
310
|
-
for (const line of preview.split('\n')) info(line);
|
|
311
|
-
blank();
|
|
312
|
-
|
|
313
|
-
let proceed = autoAttachSource;
|
|
314
|
-
if (!nonInteractive && !autoAttachSource) {
|
|
315
|
-
proceed = await confirm('Add the line?', { defaultYes: true });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (!proceed) {
|
|
319
|
-
info('Skipped — you can re-run anytime.');
|
|
320
|
-
await emitNodeEntryPatched({
|
|
321
|
-
...baseTelemetry,
|
|
322
|
-
outcome: 'declined',
|
|
323
|
-
entry_path: pathForTelemetry(target, cwd),
|
|
324
|
-
});
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const result = writeSourceMarker(target);
|
|
329
|
-
if (result.ok) {
|
|
330
|
-
success(`Added the auto-attach line to ${pathForTelemetry(target, cwd)} (${result.syntax}).`);
|
|
331
|
-
if (result.backupWritten) info(` Backup written to ${pathForTelemetry(`${target}.rr-backup`, cwd)}`);
|
|
332
|
-
info('Now your agent loads the router on every startup — no env vars, no terminal restart.');
|
|
333
|
-
await emitNodeEntryPatched({
|
|
334
|
-
...baseTelemetry,
|
|
335
|
-
outcome: viaPrompt ? 'patched_via_prompt' : 'patched',
|
|
336
|
-
entry_path: pathForTelemetry(target, cwd),
|
|
337
|
-
import_syntax: result.syntax,
|
|
338
|
-
backup_written: !!result.backupWritten,
|
|
339
|
-
});
|
|
340
|
-
} else {
|
|
341
|
-
warn(`Could not patch ${pathForTelemetry(target, cwd)}: ${result.error}`);
|
|
342
|
-
info('You can add the line yourself — see the snippet above.');
|
|
343
|
-
await emitNodeEntryPatched({
|
|
344
|
-
...baseTelemetry,
|
|
345
|
-
outcome: 'failed',
|
|
346
|
-
entry_path: pathForTelemetry(target, cwd),
|
|
347
|
-
error: result.error,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function showJsPath({ nonInteractive = false, autoAttachSource = false } = {}) {
|
|
353
|
-
blank();
|
|
354
|
-
success('JS/TS integration');
|
|
355
|
-
blank();
|
|
356
|
-
|
|
357
|
-
const result = await installNodeShim();
|
|
358
|
-
if (result.ok) {
|
|
359
|
-
if (result.already) {
|
|
360
|
-
info(result.message);
|
|
361
|
-
} else {
|
|
362
|
-
success(result.message);
|
|
363
|
-
for (const path of result.written ?? []) {
|
|
364
|
-
info(` • ${path}`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
blank();
|
|
368
|
-
info('Phase 3+ NODE_OPTIONS shim installed. Works for desktop dev sessions');
|
|
369
|
-
info('after a terminal restart, but does NOT reach cron / Docker / systemd /');
|
|
370
|
-
info('Lambda. The next step adds a one-line require to your agent source so');
|
|
371
|
-
info('routing works regardless of how the process is launched.');
|
|
372
|
-
} else {
|
|
373
|
-
warn(result.message);
|
|
374
|
-
blank();
|
|
375
|
-
if (process.platform === 'win32') {
|
|
376
|
-
info('Manual install: in a new cmd, run:');
|
|
377
|
-
info(' setx NODE_OPTIONS "--require %USERPROFILE%\\.robot-resources\\router\\auto.cjs"');
|
|
378
|
-
} else {
|
|
379
|
-
info('Manual install (paste into ~/.zshrc or ~/.bashrc):');
|
|
380
|
-
info(' export NODE_OPTIONS="${NODE_OPTIONS:-} --require ~/.robot-resources/router/auto.cjs"');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Phase 11 — source-edit injection. Reaches cron / Docker / systemd / Lambda
|
|
385
|
-
// since the line lives in the user's source, not in shell config.
|
|
386
|
-
await maybeInjectSourceEdit({ nonInteractive, autoAttachSource });
|
|
387
|
-
|
|
388
|
-
blank();
|
|
389
|
-
info('Docs: https://robotresources.ai/docs/langchain');
|
|
390
|
-
blank();
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function showPythonPath() {
|
|
394
|
-
blank();
|
|
395
|
-
success('Python integration');
|
|
396
|
-
blank();
|
|
397
|
-
|
|
398
|
-
const result = await installPythonShim();
|
|
399
|
-
if (result.ok) {
|
|
400
|
-
success(result.message);
|
|
401
|
-
if (result.sdks?.length) {
|
|
402
|
-
info(` Detected SDKs: ${result.sdks.join(', ')}`);
|
|
403
|
-
}
|
|
404
|
-
blank();
|
|
405
|
-
info('Run your Python agent — every anthropic / openai / google_generativeai');
|
|
406
|
-
info('SDK call routes through Robot Resources automatically.');
|
|
407
|
-
info('To opt out for a single command: RR_AUTOATTACH=0 python your-script.py');
|
|
408
|
-
} else {
|
|
409
|
-
warn(result.message);
|
|
410
|
-
blank();
|
|
411
|
-
info('Manual install (run inside your venv):');
|
|
412
|
-
info(' pip install --upgrade robot-resources');
|
|
413
|
-
}
|
|
414
|
-
blank();
|
|
415
|
-
info('Docs: https://robotresources.ai/docs/crewai');
|
|
416
|
-
blank();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function showMcpPath() {
|
|
420
|
-
blank();
|
|
421
|
-
success('MCP tool integration');
|
|
422
|
-
blank();
|
|
423
|
-
let cursorOk = false;
|
|
424
|
-
let claudeOk = false;
|
|
425
|
-
if (isCursorInstalled()) {
|
|
426
|
-
try {
|
|
427
|
-
const result = configureCursor();
|
|
428
|
-
cursorOk = result?.action === 'configured' || result?.action === 'already_configured';
|
|
429
|
-
info(`Cursor: ${cursorOk ? 'configured' : 'see manual instructions below'}`);
|
|
430
|
-
} catch {
|
|
431
|
-
warn('Cursor: failed to write ~/.cursor/mcp.json automatically');
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
if (isClaudeCodeInstalled()) {
|
|
435
|
-
try {
|
|
436
|
-
const result = configureClaudeCode();
|
|
437
|
-
claudeOk = result?.action === 'configured' || result?.action === 'already_configured';
|
|
438
|
-
info(`Claude Code: ${claudeOk ? 'configured' : 'see manual instructions below'}`);
|
|
439
|
-
} catch {
|
|
440
|
-
warn('Claude Code: failed to write ~/.claude/settings.json automatically');
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
if (!cursorOk && !claudeOk) {
|
|
444
|
-
info('We did not detect Cursor or Claude Code on this machine.');
|
|
445
|
-
info('Manual setup: https://robotresources.ai/docs/cursor-mcp');
|
|
446
|
-
}
|
|
447
|
-
blank();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function showDocsPath() {
|
|
451
|
-
blank();
|
|
452
|
-
success('Docs');
|
|
453
|
-
blank();
|
|
454
|
-
info('Integration guides: https://robotresources.ai/docs');
|
|
455
|
-
info('HTTP API: https://robotresources.ai/docs/http-api');
|
|
456
|
-
info('GitHub: https://github.com/robot-resources/packages');
|
|
457
|
-
blank();
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function showInstallOcPath() {
|
|
461
|
-
blank();
|
|
462
|
-
info('OpenClaw is the easiest way to use Robot Resources.');
|
|
463
|
-
info('Install OpenClaw first (https://openclaw.dev), then re-run:');
|
|
464
|
-
info(' npx robot-resources');
|
|
465
|
-
blank();
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function runPath(path, opts = {}) {
|
|
469
|
-
switch (path) {
|
|
470
|
-
case 'js': await showJsPath(opts); break;
|
|
471
|
-
case 'python': await showPythonPath(); break;
|
|
472
|
-
case 'mcp': showMcpPath(); break;
|
|
473
|
-
case 'docs': showDocsPath(); break;
|
|
474
|
-
case 'install-oc': showInstallOcPath(); break;
|
|
475
|
-
default: showInstallOcPath(); break;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Runs the non-OC wizard. Three modes:
|
|
481
|
-
* - target supplied (--for=<target>): run that path directly, no prompt
|
|
482
|
-
* - non-interactive AND no target: print hint with --for= options and exit
|
|
483
|
-
* - interactive: 5-option menu via @inquirer/prompts.select
|
|
484
|
-
*/
|
|
485
|
-
export async function runNonOcWizard({ nonInteractive = false, target = null, autoAttachSource = false } = {}) {
|
|
486
|
-
const normalized = normalizeTarget(target);
|
|
487
|
-
|
|
488
|
-
if (normalized) {
|
|
489
|
-
await runPath(normalized, { nonInteractive, autoAttachSource });
|
|
490
|
-
await emitPathChosen(normalized);
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (nonInteractive) {
|
|
495
|
-
// Phase 3.5: when project shape is unambiguous (cwd has a package.json
|
|
496
|
-
// OR a Python project file), auto-install the matching shim instead of
|
|
497
|
-
// bailing with a hint. CI/agents running `npx robot-resources` from
|
|
498
|
-
// their repo are exactly this case — making them pass `--for=` is
|
|
499
|
-
// friction we don't need.
|
|
500
|
-
//
|
|
501
|
-
// The Phase 3 noninteractive_no_target path is preserved for the case
|
|
502
|
-
// where detection finds nothing (truly empty cwd, generic shell run).
|
|
503
|
-
const runtime = detectAgentRuntime();
|
|
504
|
-
let autoTarget = null;
|
|
505
|
-
if (runtime.kind === 'node' || runtime.kind === 'both') autoTarget = 'js';
|
|
506
|
-
else if (runtime.kind === 'python') autoTarget = 'python';
|
|
507
|
-
|
|
508
|
-
if (autoTarget) {
|
|
509
|
-
// Brief context so the install step doesn't feel sudden.
|
|
510
|
-
info(`Detected a ${autoTarget === 'js' ? 'Node' : 'Python'} project — installing the matching shim automatically.`);
|
|
511
|
-
info(' Pass --for=<other> to override, or --uninstall to remove later.');
|
|
512
|
-
blank();
|
|
513
|
-
await runPath(autoTarget, { nonInteractive, autoAttachSource });
|
|
514
|
-
await emitPathChosen(autoTarget);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
info('Robot Resources requires OpenClaw, which we did not detect on this machine.');
|
|
519
|
-
info('To bypass this prompt in CI / non-TTY contexts, re-run with --for=<target>:');
|
|
520
|
-
info(' npx robot-resources --for=langchain # JS/TS agent');
|
|
521
|
-
info(' npx robot-resources --for=python # Python agent');
|
|
522
|
-
info(' npx robot-resources --for=cursor # Cursor MCP config');
|
|
523
|
-
info(' npx robot-resources --for=claude-code # Claude Code MCP config');
|
|
524
|
-
info(' npx robot-resources --for=docs # docs URL');
|
|
525
|
-
blank();
|
|
526
|
-
await emitPathChosen('noninteractive_no_target');
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Interactive menu.
|
|
531
|
-
header();
|
|
532
|
-
info('Robot Resources requires OpenClaw, which we did not detect on this machine.');
|
|
533
|
-
info('What are you building? Pick the closest match — we\'ll show the install steps.');
|
|
534
|
-
blank();
|
|
535
|
-
|
|
536
|
-
const defaultPath = detectDefaultPath() ?? 'js';
|
|
537
|
-
|
|
538
|
-
// Phase 3.6: race the prompt against a timeout. Some agent runners report
|
|
539
|
-
// stdin.isTTY=true (so `nonInteractive` ends up false), then never deliver
|
|
540
|
-
// a keystroke — `select()` blocks forever and the wizard exits silently
|
|
541
|
-
// without firing wizard_path_chosen. The May-2 23:31 RU cluster all hit
|
|
542
|
-
// this: 5 wizard_started events, zero wizard_path_chosen.
|
|
543
|
-
//
|
|
544
|
-
// After SELECT_TIMEOUT_MS, we treat the session as practically
|
|
545
|
-
// non-interactive and fall through to the same auto-detect path the
|
|
546
|
-
// nonInteractive branch above uses (Phase 3.5).
|
|
547
|
-
const SELECT_TIMEOUT_MS = Number(process.env.RR_WIZARD_SELECT_TIMEOUT_MS) || 30_000;
|
|
548
|
-
const TIMED_OUT = Symbol('select_timeout');
|
|
549
|
-
|
|
550
|
-
let chosen;
|
|
551
|
-
let timer;
|
|
552
|
-
try {
|
|
553
|
-
chosen = await Promise.race([
|
|
554
|
-
select({
|
|
555
|
-
message: 'What are you building?',
|
|
556
|
-
default: defaultPath,
|
|
557
|
-
choices: [
|
|
558
|
-
{ name: PATH_LABELS.js, value: 'js' },
|
|
559
|
-
{ name: PATH_LABELS.python, value: 'python' },
|
|
560
|
-
{ name: PATH_LABELS.mcp, value: 'mcp' },
|
|
561
|
-
{ name: PATH_LABELS.docs, value: 'docs' },
|
|
562
|
-
{ name: PATH_LABELS['install-oc'], value: 'install-oc' },
|
|
563
|
-
],
|
|
564
|
-
}),
|
|
565
|
-
new Promise((resolve) => {
|
|
566
|
-
timer = setTimeout(() => resolve(TIMED_OUT), SELECT_TIMEOUT_MS);
|
|
567
|
-
// Don't keep the event loop alive purely for this timer — once the
|
|
568
|
-
// user picks, the process should be free to exit.
|
|
569
|
-
if (typeof timer.unref === 'function') timer.unref();
|
|
570
|
-
}),
|
|
571
|
-
]);
|
|
572
|
-
} catch (err) {
|
|
573
|
-
// User hit Ctrl-C or terminal closed — exit cleanly, but mark the funnel
|
|
574
|
-
// so we can distinguish "agent shown the prompt and bailed" from
|
|
575
|
-
// "wizard never reached the prompt at all" in Supabase.
|
|
576
|
-
if (err && (err.name === 'ExitPromptError' || err.code === 'ABORT_ERR')) {
|
|
577
|
-
await emitPathChosen('aborted');
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
throw err;
|
|
581
|
-
} finally {
|
|
582
|
-
if (timer) clearTimeout(timer);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (chosen === TIMED_OUT) {
|
|
586
|
-
// No keystroke after SELECT_TIMEOUT_MS — practically non-interactive.
|
|
587
|
-
// Mirror the Phase 3.5 auto-detect logic.
|
|
588
|
-
blank();
|
|
589
|
-
info(`No input received after ${Math.round(SELECT_TIMEOUT_MS / 1000)}s — treating session as non-interactive.`);
|
|
590
|
-
const runtime = detectAgentRuntime();
|
|
591
|
-
let autoTarget = null;
|
|
592
|
-
if (runtime.kind === 'node' || runtime.kind === 'both') autoTarget = 'js';
|
|
593
|
-
else if (runtime.kind === 'python') autoTarget = 'python';
|
|
594
|
-
|
|
595
|
-
if (autoTarget) {
|
|
596
|
-
info(`Detected a ${autoTarget === 'js' ? 'Node' : 'Python'} project — installing the matching shim automatically.`);
|
|
597
|
-
blank();
|
|
598
|
-
// Treat the timed-out interactive session as non-interactive for the
|
|
599
|
-
// source-edit step too — we don't have a TTY to ask the Y/N on.
|
|
600
|
-
await runPath(autoTarget, { nonInteractive: true, autoAttachSource });
|
|
601
|
-
await emitPathChosen(autoTarget);
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Truly empty cwd — nothing to install. Surface the timeout so the
|
|
606
|
-
// funnel can be queried separately from genuine aborts.
|
|
607
|
-
blank();
|
|
608
|
-
info('No project shape detected — nothing to install. Re-run with --for=<target>.');
|
|
609
|
-
await emitPathChosen('interactive_timeout');
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
await runPath(chosen, { nonInteractive, autoAttachSource });
|
|
614
|
-
await emitPathChosen(chosen);
|
|
615
|
-
}
|