robot-resources 1.11.2 → 1.12.1
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 +7 -1
- package/lib/detect.js +124 -0
- package/lib/install-node-shim.js +142 -0
- package/lib/install-python-shim.js +107 -0
- package/lib/non-oc-wizard.js +51 -26
- package/lib/shell-config.js +168 -0
- package/lib/uninstall.js +54 -9
- package/lib/venv-detect.js +85 -0
- package/lib/wizard.js +33 -18
- package/package.json +1 -1
package/bin/setup.js
CHANGED
|
@@ -19,13 +19,19 @@ if (args.includes('--uninstall')) {
|
|
|
19
19
|
const targetArg = args.find((a) => a.startsWith('--for='));
|
|
20
20
|
const target = targetArg ? targetArg.slice('--for='.length) : null;
|
|
21
21
|
|
|
22
|
+
// Phase 5: --scope=router-only is the entry from `npx @robot-resources/router`
|
|
23
|
+
// (the standalone bin). Skips scraper steps. Default 'full' matches the
|
|
24
|
+
// unified `npx robot-resources` behavior.
|
|
25
|
+
const scopeArg = args.find((a) => a.startsWith('--scope='));
|
|
26
|
+
const scope = scopeArg ? scopeArg.slice('--scope='.length) : 'full';
|
|
27
|
+
|
|
22
28
|
// Treat piped/CI runs (no TTY on stdin OR stdout) as non-interactive so the
|
|
23
29
|
// wizard never blocks on a prompt that can't be answered. The interactive
|
|
24
30
|
// menu is only opened when both stdin and stdout are real terminals.
|
|
25
31
|
const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
26
32
|
const nonInteractive = explicitNonInteractive || !hasTty;
|
|
27
33
|
|
|
28
|
-
runWizard({ nonInteractive, target }).catch((err) => {
|
|
34
|
+
runWizard({ nonInteractive, target, scope }).catch((err) => {
|
|
29
35
|
console.error(`\n ✗ Setup failed: ${err.message}\n`);
|
|
30
36
|
process.exit(1);
|
|
31
37
|
});
|
package/lib/detect.js
CHANGED
|
@@ -125,3 +125,127 @@ export function isClaudeCodeInstalled() {
|
|
|
125
125
|
export function isCursorInstalled() {
|
|
126
126
|
return existsSync(join(homedir(), '.cursor'));
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
// ── Agent-runtime detection (Phase 3) ─────────────────────────────────────
|
|
130
|
+
//
|
|
131
|
+
// Used by the non-OC wizard to pick which shim to install: a NODE_OPTIONS
|
|
132
|
+
// shell line for Node agents, or `pip install robot-resources` for Python.
|
|
133
|
+
// Returns { kind: 'node'|'python'|null, evidence: string[] } so the wizard
|
|
134
|
+
// can show the user WHY we picked a path (debuggable + builds trust).
|
|
135
|
+
//
|
|
136
|
+
// "Evidence" is the dep markers we found, in priority order. Empty string
|
|
137
|
+
// means a generic project (e.g. just package.json, no LLM SDK deps yet) —
|
|
138
|
+
// still picks the language but with low confidence.
|
|
139
|
+
|
|
140
|
+
const NODE_AGENT_DEPS = [
|
|
141
|
+
'@anthropic-ai/sdk',
|
|
142
|
+
'openai',
|
|
143
|
+
'@google/generative-ai',
|
|
144
|
+
'@google-ai/generativelanguage',
|
|
145
|
+
'langchain',
|
|
146
|
+
'@langchain/core',
|
|
147
|
+
'@langchain/anthropic',
|
|
148
|
+
'@langchain/openai',
|
|
149
|
+
'@langchain/google-genai',
|
|
150
|
+
'@langchain/langgraph',
|
|
151
|
+
'@mastra/core',
|
|
152
|
+
'crewai-js',
|
|
153
|
+
'llamaindex',
|
|
154
|
+
'ai', // Vercel AI SDK
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const PYTHON_AGENT_DEPS = [
|
|
158
|
+
'anthropic',
|
|
159
|
+
'openai',
|
|
160
|
+
'google-generativeai',
|
|
161
|
+
'langchain',
|
|
162
|
+
'langchain-anthropic',
|
|
163
|
+
'langchain-openai',
|
|
164
|
+
'langchain-google-genai',
|
|
165
|
+
'langgraph',
|
|
166
|
+
'crewai',
|
|
167
|
+
'llama-index',
|
|
168
|
+
'llama_index',
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Inspect cwd for evidence of a Node agent project. Returns null if no
|
|
173
|
+
* package.json, or `{ evidence: [...] }` describing matched dep markers
|
|
174
|
+
* (empty list means "Node project but no LLM-SDK deps detected" — generic).
|
|
175
|
+
*/
|
|
176
|
+
export function detectNodeAgent(cwd = process.cwd()) {
|
|
177
|
+
const pkgPath = join(cwd, 'package.json');
|
|
178
|
+
if (!existsSync(pkgPath)) return null;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
182
|
+
const allDeps = {
|
|
183
|
+
...(pkg.dependencies ?? {}),
|
|
184
|
+
...(pkg.devDependencies ?? {}),
|
|
185
|
+
...(pkg.peerDependencies ?? {}),
|
|
186
|
+
};
|
|
187
|
+
const evidence = NODE_AGENT_DEPS.filter(
|
|
188
|
+
(d) => Object.prototype.hasOwnProperty.call(allDeps, d),
|
|
189
|
+
);
|
|
190
|
+
return { evidence };
|
|
191
|
+
} catch {
|
|
192
|
+
// package.json unreadable but exists — call it Node, no evidence
|
|
193
|
+
return { evidence: [] };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Inspect cwd for evidence of a Python agent project. Looks at
|
|
199
|
+
* requirements.txt + pyproject.toml. Returns null if neither exists, or
|
|
200
|
+
* `{ evidence: [...] }`. Empty evidence still resolves to Python — many
|
|
201
|
+
* agent projects use ad-hoc deps not in our markers list.
|
|
202
|
+
*/
|
|
203
|
+
export function detectPythonAgent(cwd = process.cwd()) {
|
|
204
|
+
const reqPath = join(cwd, 'requirements.txt');
|
|
205
|
+
const pyProjPath = join(cwd, 'pyproject.toml');
|
|
206
|
+
const hasReq = existsSync(reqPath);
|
|
207
|
+
const hasPy = existsSync(pyProjPath);
|
|
208
|
+
if (!hasReq && !hasPy) return null;
|
|
209
|
+
|
|
210
|
+
const text = [
|
|
211
|
+
hasReq ? safeRead(reqPath) : '',
|
|
212
|
+
hasPy ? safeRead(pyProjPath) : '',
|
|
213
|
+
].join('\n').toLowerCase();
|
|
214
|
+
|
|
215
|
+
const evidence = PYTHON_AGENT_DEPS.filter((d) => {
|
|
216
|
+
// Match `dep`, `dep==`, `dep>=`, `dep[extras]`, or `"dep"`/`'dep'` in
|
|
217
|
+
// pyproject's dependencies array. Word-boundary on the left, anything
|
|
218
|
+
// version-ish on the right.
|
|
219
|
+
const escaped = d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
220
|
+
const re = new RegExp(`(^|[\\s"'\\[])${escaped}(\\s|[<>=!~\\["',]|$)`, 'm');
|
|
221
|
+
return re.test(text);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return { evidence };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function safeRead(path) {
|
|
228
|
+
try { return readFileSync(path, 'utf-8'); } catch { return ''; }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Decide which shim to install when OpenClaw is NOT detected. Picks Node OR
|
|
233
|
+
* Python based on cwd shape. When BOTH are present (full-stack monorepo
|
|
234
|
+
* with package.json AND pyproject.toml) the caller is responsible for
|
|
235
|
+
* resolving the ambiguity (interactively, or default to JS in --yes mode
|
|
236
|
+
* per the team decision in the plan).
|
|
237
|
+
*
|
|
238
|
+
* Returns one of:
|
|
239
|
+
* { kind: 'node', evidence: [...] }
|
|
240
|
+
* { kind: 'python', evidence: [...] }
|
|
241
|
+
* { kind: 'both', node: {...}, python: {...} }
|
|
242
|
+
* { kind: null } — unknown project shape, no clear path
|
|
243
|
+
*/
|
|
244
|
+
export function detectAgentRuntime(cwd = process.cwd()) {
|
|
245
|
+
const node = detectNodeAgent(cwd);
|
|
246
|
+
const python = detectPythonAgent(cwd);
|
|
247
|
+
if (node && python) return { kind: 'both', node, python };
|
|
248
|
+
if (node) return { kind: 'node', evidence: node.evidence };
|
|
249
|
+
if (python) return { kind: 'python', evidence: python.evidence };
|
|
250
|
+
return { kind: null };
|
|
251
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { writeShellLine, hasShellLine } from './shell-config.js';
|
|
2
|
+
import { readConfig } from './config.mjs';
|
|
3
|
+
import { detectNodeAgent } from './detect.js';
|
|
4
|
+
|
|
5
|
+
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Install the Node shim into the user's shell config. Phase 3 entry for
|
|
9
|
+
* the non-OC Node path.
|
|
10
|
+
*
|
|
11
|
+
* Steps:
|
|
12
|
+
* 1. Append the marker block to detected rc files (zsh / bash / fish)
|
|
13
|
+
* via shell-config.writeShellLine. Idempotent: re-running does nothing
|
|
14
|
+
* if the block is already present.
|
|
15
|
+
* 2. Emit `node_shim_installed` telemetry with the shell list, sdks
|
|
16
|
+
* detected, dry-run flag, plus per-file errors.
|
|
17
|
+
*
|
|
18
|
+
* The user has to start a new shell (or `source` the file) for the
|
|
19
|
+
* NODE_OPTIONS to take effect — we tell them this in the wizard's
|
|
20
|
+
* post-install message. For Phase 3 we don't try to mutate the running
|
|
21
|
+
* shell; that's a Phase 6 nice-to-have.
|
|
22
|
+
*
|
|
23
|
+
* Windows: shell-config.writeShellLine returns no rc files on Windows
|
|
24
|
+
* (we only support POSIX in P3). The wizard prints manual instructions
|
|
25
|
+
* for Windows users in `non-oc-wizard.js`.
|
|
26
|
+
*
|
|
27
|
+
* Returns a UI-friendly result the wizard can format and print.
|
|
28
|
+
*/
|
|
29
|
+
export async function installNodeShim({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
await emit({
|
|
32
|
+
shell: 'unsupported',
|
|
33
|
+
shell_config_path: null,
|
|
34
|
+
sdks_detected: detectSdks(cwd),
|
|
35
|
+
dry_run: dryRun,
|
|
36
|
+
reason: 'windows_not_supported_yet',
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: 'windows_not_supported_yet',
|
|
41
|
+
message:
|
|
42
|
+
'Windows shell-config writing is not yet supported. Set ' +
|
|
43
|
+
'NODE_OPTIONS=--require @robot-resources/router/auto manually in your ' +
|
|
44
|
+
'system environment variables, or wait for Phase 6.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sdks = detectSdks(cwd);
|
|
49
|
+
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
await emit({
|
|
52
|
+
shell: 'dryrun',
|
|
53
|
+
shell_config_path: null,
|
|
54
|
+
sdks_detected: sdks,
|
|
55
|
+
dry_run: true,
|
|
56
|
+
reason: null,
|
|
57
|
+
});
|
|
58
|
+
return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS to shell rc.' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const alreadyInstalled = hasShellLine();
|
|
62
|
+
const result = writeShellLine();
|
|
63
|
+
|
|
64
|
+
// Single shell value for the funnel even though we may have written to
|
|
65
|
+
// multiple rc files. Pick the dominant one for telemetry.
|
|
66
|
+
const dominant = pickDominantShell(result.written);
|
|
67
|
+
|
|
68
|
+
await emit({
|
|
69
|
+
shell: dominant,
|
|
70
|
+
shell_config_path: result.written.join(','),
|
|
71
|
+
sdks_detected: sdks,
|
|
72
|
+
dry_run: false,
|
|
73
|
+
already_installed: alreadyInstalled,
|
|
74
|
+
files_written: result.written.length,
|
|
75
|
+
files_with_errors: result.errors.length,
|
|
76
|
+
error_messages: result.errors.map((e) => `${e.path}: ${e.message}`).slice(0, 3),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (alreadyInstalled && result.written.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
already: true,
|
|
83
|
+
message: 'NODE_OPTIONS auto-attach already installed. No changes made.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (result.written.length === 0 && result.errors.length > 0) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
message: `Could not write to any shell rc file. Errors: ${result.errors.map((e) => e.message).join(', ')}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
written: result.written,
|
|
97
|
+
errors: result.errors,
|
|
98
|
+
message:
|
|
99
|
+
`Installed NODE_OPTIONS auto-attach in ${result.written.length} shell file(s). ` +
|
|
100
|
+
'Open a new terminal (or source the file) for it to take effect.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function detectSdks(cwd) {
|
|
105
|
+
const result = detectNodeAgent(cwd);
|
|
106
|
+
return result?.evidence ?? [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pickDominantShell(paths) {
|
|
110
|
+
// Use process.env.SHELL as the tiebreaker — that's the user's actual
|
|
111
|
+
// login shell. Fall back to the first written file's basename.
|
|
112
|
+
const shellEnv = (process.env.SHELL || '').toLowerCase();
|
|
113
|
+
if (shellEnv.includes('zsh')) return 'zsh';
|
|
114
|
+
if (shellEnv.includes('fish')) return 'fish';
|
|
115
|
+
if (shellEnv.includes('bash')) return 'bash';
|
|
116
|
+
if (paths[0]?.endsWith('.zshrc')) return 'zsh';
|
|
117
|
+
if (paths[0]?.endsWith('.bashrc') || paths[0]?.endsWith('.bash_profile')) return 'bash';
|
|
118
|
+
if (paths[0]?.endsWith('config.fish')) return 'fish';
|
|
119
|
+
return 'unknown';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function emit(payload) {
|
|
123
|
+
const config = readConfig();
|
|
124
|
+
if (!config.api_key) return;
|
|
125
|
+
try {
|
|
126
|
+
await fetch(`${PLATFORM_URL}/v1/telemetry`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
'Authorization': `Bearer ${config.api_key}`,
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
product: 'cli',
|
|
134
|
+
event_type: 'node_shim_installed',
|
|
135
|
+
payload: { ...payload, platform: process.platform },
|
|
136
|
+
}),
|
|
137
|
+
signal: AbortSignal.timeout(5_000),
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// Best-effort — never let telemetry break the install path.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { detectVenv, runPipInstall } from './venv-detect.js';
|
|
3
|
+
import { readConfig } from './config.mjs';
|
|
4
|
+
import { detectPythonAgent } from './detect.js';
|
|
5
|
+
|
|
6
|
+
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Install the Python shim into the user's active/cwd venv. Phase 3 entry
|
|
10
|
+
* for the non-OC Python path.
|
|
11
|
+
*
|
|
12
|
+
* Steps:
|
|
13
|
+
* 1. Resolve Python via detectVenv. Bail with `confidence: 'low'` if we
|
|
14
|
+
* can't find a venv — we never `pip install` into system Python (can
|
|
15
|
+
* break OS Python on Linux).
|
|
16
|
+
* 2. Run `pip install --upgrade robot-resources`. Captures exit code +
|
|
17
|
+
* stderr tail for telemetry.
|
|
18
|
+
* 3. Emit `python_shim_installed` telemetry with venv kind, Python
|
|
19
|
+
* version, detected SDK markers, pip exit code.
|
|
20
|
+
*
|
|
21
|
+
* Returns a UI-friendly result the wizard can format and print. Never
|
|
22
|
+
* throws — any unexpected error becomes a structured `{ ok: false, ... }`.
|
|
23
|
+
*/
|
|
24
|
+
export async function installPythonShim({ cwd = process.cwd() } = {}) {
|
|
25
|
+
const venv = detectVenv(cwd);
|
|
26
|
+
|
|
27
|
+
if (!venv.python) {
|
|
28
|
+
await emit({
|
|
29
|
+
kind: 'none',
|
|
30
|
+
python_version: null,
|
|
31
|
+
sdks_detected: detectSdks(cwd),
|
|
32
|
+
pip_exit_code: null,
|
|
33
|
+
reason: 'no_venv_found',
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: 'no_venv_found',
|
|
38
|
+
message:
|
|
39
|
+
'No active venv or ./.venv detected. Activate your venv first ' +
|
|
40
|
+
'(source .venv/bin/activate) or pass --python=/path/to/python, ' +
|
|
41
|
+
'then re-run.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pipResult = runPipInstall({
|
|
46
|
+
python: venv.python,
|
|
47
|
+
packageSpec: 'robot-resources>=0.2.0',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const pythonVersion = readPythonVersion(venv.python);
|
|
51
|
+
const sdks = detectSdks(cwd);
|
|
52
|
+
|
|
53
|
+
await emit({
|
|
54
|
+
kind: venv.kind,
|
|
55
|
+
python_version: pythonVersion,
|
|
56
|
+
sdks_detected: sdks,
|
|
57
|
+
pip_exit_code: pipResult.code,
|
|
58
|
+
pip_stderr_tail: pipResult.ok ? null : pipResult.stderr,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: pipResult.ok,
|
|
63
|
+
venv,
|
|
64
|
+
pythonVersion,
|
|
65
|
+
sdks,
|
|
66
|
+
pipResult,
|
|
67
|
+
message: pipResult.ok
|
|
68
|
+
? `Installed robot-resources into ${venv.kind} venv (${venv.python})`
|
|
69
|
+
: `pip install failed (exit ${pipResult.code}): ${pipResult.stderr.slice(0, 200)}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function detectSdks(cwd) {
|
|
74
|
+
const result = detectPythonAgent(cwd);
|
|
75
|
+
return result?.evidence ?? [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readPythonVersion(python) {
|
|
79
|
+
try {
|
|
80
|
+
const r = spawnSync(python, ['--version'], { encoding: 'utf-8' });
|
|
81
|
+
return (r.stdout || r.stderr || '').trim().replace(/^Python\s+/, '');
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function emit(payload) {
|
|
88
|
+
const config = readConfig();
|
|
89
|
+
if (!config.api_key) return;
|
|
90
|
+
try {
|
|
91
|
+
await fetch(`${PLATFORM_URL}/v1/telemetry`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Authorization': `Bearer ${config.api_key}`,
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
product: 'cli',
|
|
99
|
+
event_type: 'python_shim_installed',
|
|
100
|
+
payload: { ...payload, platform: process.platform },
|
|
101
|
+
}),
|
|
102
|
+
signal: AbortSignal.timeout(5_000),
|
|
103
|
+
});
|
|
104
|
+
} catch {
|
|
105
|
+
// Best-effort — never let telemetry break the install path.
|
|
106
|
+
}
|
|
107
|
+
}
|
package/lib/non-oc-wizard.js
CHANGED
|
@@ -5,6 +5,8 @@ import { isClaudeCodeInstalled, isCursorInstalled } from './detect.js';
|
|
|
5
5
|
import { configureClaudeCode, configureCursor } from './tool-config.js';
|
|
6
6
|
import { header, info, success, warn, blank } from './ui.js';
|
|
7
7
|
import { readConfig } from './config.mjs';
|
|
8
|
+
import { installNodeShim } from './install-node-shim.js';
|
|
9
|
+
import { installPythonShim } from './install-python-shim.js';
|
|
8
10
|
|
|
9
11
|
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
10
12
|
|
|
@@ -87,38 +89,61 @@ async function emitPathChosen(path) {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
function showJsPath() {
|
|
92
|
+
async function showJsPath() {
|
|
91
93
|
blank();
|
|
92
94
|
success('JS/TS integration');
|
|
93
95
|
blank();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
|
|
97
|
+
const result = await installNodeShim();
|
|
98
|
+
if (result.ok) {
|
|
99
|
+
if (result.already) {
|
|
100
|
+
info(result.message);
|
|
101
|
+
} else {
|
|
102
|
+
success(result.message);
|
|
103
|
+
for (const path of result.written ?? []) {
|
|
104
|
+
info(` • ${path}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
blank();
|
|
108
|
+
info('Once your shell picks up the new NODE_OPTIONS, every Node agent on');
|
|
109
|
+
info('this machine routes Anthropic SDK calls through Robot Resources.');
|
|
110
|
+
info('Open a new terminal — or run: source ~/.zshrc (or your shell rc)');
|
|
111
|
+
} else {
|
|
112
|
+
warn(result.message);
|
|
113
|
+
blank();
|
|
114
|
+
info('Manual install (paste into ~/.zshrc or ~/.bashrc):');
|
|
115
|
+
info(' export NODE_OPTIONS="${NODE_OPTIONS:-} --require @robot-resources/router/auto"');
|
|
116
|
+
}
|
|
101
117
|
blank();
|
|
102
|
-
info('
|
|
118
|
+
info('Docs: https://robotresources.ai/docs/langchain');
|
|
103
119
|
blank();
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
function showPythonPath() {
|
|
122
|
+
async function showPythonPath() {
|
|
107
123
|
blank();
|
|
108
124
|
success('Python integration');
|
|
109
125
|
blank();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
|
|
127
|
+
const result = await installPythonShim();
|
|
128
|
+
if (result.ok) {
|
|
129
|
+
success(result.message);
|
|
130
|
+
if (result.sdks?.length) {
|
|
131
|
+
info(` Detected SDKs: ${result.sdks.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
blank();
|
|
134
|
+
info('Set RR_AUTOATTACH=1 in your shell, then run your Python agent.');
|
|
135
|
+
info('Every anthropic.Anthropic() instance routes through Robot Resources.');
|
|
136
|
+
info(' echo \'export RR_AUTOATTACH=1\' >> ~/.zshrc # or your shell rc');
|
|
137
|
+
} else {
|
|
138
|
+
warn(result.message);
|
|
139
|
+
blank();
|
|
140
|
+
info('Manual install (run inside your venv):');
|
|
141
|
+
info(' pip install --upgrade robot-resources');
|
|
142
|
+
info('Then set:');
|
|
143
|
+
info(' export RR_AUTOATTACH=1');
|
|
144
|
+
}
|
|
120
145
|
blank();
|
|
121
|
-
info('
|
|
146
|
+
info('Docs: https://robotresources.ai/docs/crewai');
|
|
122
147
|
blank();
|
|
123
148
|
}
|
|
124
149
|
|
|
@@ -171,10 +196,10 @@ function showInstallOcPath() {
|
|
|
171
196
|
blank();
|
|
172
197
|
}
|
|
173
198
|
|
|
174
|
-
function runPath(path) {
|
|
199
|
+
async function runPath(path) {
|
|
175
200
|
switch (path) {
|
|
176
|
-
case 'js': showJsPath(); break;
|
|
177
|
-
case 'python': showPythonPath(); break;
|
|
201
|
+
case 'js': await showJsPath(); break;
|
|
202
|
+
case 'python': await showPythonPath(); break;
|
|
178
203
|
case 'mcp': showMcpPath(); break;
|
|
179
204
|
case 'docs': showDocsPath(); break;
|
|
180
205
|
case 'install-oc': showInstallOcPath(); break;
|
|
@@ -192,7 +217,7 @@ export async function runNonOcWizard({ nonInteractive = false, target = null } =
|
|
|
192
217
|
const normalized = normalizeTarget(target);
|
|
193
218
|
|
|
194
219
|
if (normalized) {
|
|
195
|
-
runPath(normalized);
|
|
220
|
+
await runPath(normalized);
|
|
196
221
|
await emitPathChosen(normalized);
|
|
197
222
|
return;
|
|
198
223
|
}
|
|
@@ -242,6 +267,6 @@ export async function runNonOcWizard({ nonInteractive = false, target = null } =
|
|
|
242
267
|
throw err;
|
|
243
268
|
}
|
|
244
269
|
|
|
245
|
-
runPath(chosen);
|
|
270
|
+
await runPath(chosen);
|
|
246
271
|
await emitPathChosen(chosen);
|
|
247
272
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Idempotent writer for the NODE_OPTIONS auto-attach line in shell rc files.
|
|
7
|
+
*
|
|
8
|
+
* Phase 3 ships POSIX-only support: zsh, bash, fish. Windows ships a printed-
|
|
9
|
+
* instructions fallback (Phase 6 problem). Every write is wrapped in a
|
|
10
|
+
* marker block so `--uninstall` can find and remove cleanly without
|
|
11
|
+
* regex-matching against the user's actual shell content:
|
|
12
|
+
*
|
|
13
|
+
* # >>> robot-resources: NODE_OPTIONS auto-attach >>>
|
|
14
|
+
* export NODE_OPTIONS="${NODE_OPTIONS:-} --require @robot-resources/router/auto"
|
|
15
|
+
* # <<< robot-resources <<<
|
|
16
|
+
*
|
|
17
|
+
* Behavior decisions (see plan):
|
|
18
|
+
* - If NODE_OPTIONS is already set with a different --require (rare; e.g.
|
|
19
|
+
* dd-trace), append ours after theirs. Both load. The user keeps their
|
|
20
|
+
* existing tooling. The shell expansion `${NODE_OPTIONS:-} ...` handles
|
|
21
|
+
* this correctly — POSIX shells concat space-separated --require flags.
|
|
22
|
+
* - Never clobber. If our marker block already exists, it's a no-op.
|
|
23
|
+
* - Write to ALL detected rc files (e.g. user has both .zshrc + .bashrc),
|
|
24
|
+
* so the user gets routing in whichever shell they actually open.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const MARK_BEGIN = '# >>> robot-resources: NODE_OPTIONS auto-attach >>>';
|
|
28
|
+
const MARK_END = '# <<< robot-resources <<<';
|
|
29
|
+
|
|
30
|
+
const POSIX_LINE =
|
|
31
|
+
'export NODE_OPTIONS="${NODE_OPTIONS:-} --require @robot-resources/router/auto"';
|
|
32
|
+
|
|
33
|
+
// Fish has different syntax (no `export`, uses `set -x`). Detected separately.
|
|
34
|
+
const FISH_LINE =
|
|
35
|
+
'set -x NODE_OPTIONS "$NODE_OPTIONS --require @robot-resources/router/auto"';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Discover which rc files are present for this user. Returns a list of
|
|
39
|
+
* absolute paths in priority order (zsh first, bash second, fish third).
|
|
40
|
+
* The wizard writes to ALL of them — users frequently edit one shell's
|
|
41
|
+
* rc and forget another, and we'd rather over-cover than under-cover.
|
|
42
|
+
*/
|
|
43
|
+
export function listShellRcFiles(home = homedir()) {
|
|
44
|
+
const candidates = [
|
|
45
|
+
{ kind: 'zsh', path: join(home, '.zshrc') },
|
|
46
|
+
{ kind: 'bash', path: join(home, '.bashrc') },
|
|
47
|
+
{ kind: 'bash', path: join(home, '.bash_profile') }, // macOS often uses this
|
|
48
|
+
{ kind: 'fish', path: join(home, '.config', 'fish', 'config.fish') },
|
|
49
|
+
];
|
|
50
|
+
return candidates.filter((c) => existsSync(c.path));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns true if at least one rc file already has our marker block.
|
|
55
|
+
* Used by both the wizard (skip-if-already-installed) and uninstall
|
|
56
|
+
* (gate the "remove" step).
|
|
57
|
+
*/
|
|
58
|
+
export function hasShellLine(home = homedir()) {
|
|
59
|
+
for (const { path } of listShellRcFiles(home)) {
|
|
60
|
+
try {
|
|
61
|
+
const text = readFileSync(path, 'utf-8');
|
|
62
|
+
if (text.includes(MARK_BEGIN)) return true;
|
|
63
|
+
} catch { /* unreadable rc, skip */ }
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Idempotently append the marker block to every detected rc file. Returns
|
|
70
|
+
* a list of files actually modified (empty if everything already had it).
|
|
71
|
+
*
|
|
72
|
+
* Each rc file is treated independently: the writer never aborts the
|
|
73
|
+
* others on one failure. Per-file errors are returned as warnings the
|
|
74
|
+
* caller can surface.
|
|
75
|
+
*/
|
|
76
|
+
export function writeShellLine(home = homedir()) {
|
|
77
|
+
const rcs = listShellRcFiles(home);
|
|
78
|
+
const written = [];
|
|
79
|
+
const errors = [];
|
|
80
|
+
|
|
81
|
+
if (rcs.length === 0) {
|
|
82
|
+
// POSIX shells but no rc file yet — create ~/.zshrc on macOS (default
|
|
83
|
+
// since 10.15), ~/.bashrc on Linux. Better than silently no-op'ing.
|
|
84
|
+
const fallback = process.platform === 'darwin'
|
|
85
|
+
? { kind: 'zsh', path: join(home, '.zshrc') }
|
|
86
|
+
: { kind: 'bash', path: join(home, '.bashrc') };
|
|
87
|
+
rcs.push(fallback);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const rc of rcs) {
|
|
91
|
+
try {
|
|
92
|
+
let text = '';
|
|
93
|
+
try { text = readFileSync(rc.path, 'utf-8'); } catch { /* file may not exist yet */ }
|
|
94
|
+
|
|
95
|
+
if (text.includes(MARK_BEGIN)) {
|
|
96
|
+
// Already installed. Skip silently.
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const line = rc.kind === 'fish' ? FISH_LINE : POSIX_LINE;
|
|
101
|
+
const block =
|
|
102
|
+
(text && !text.endsWith('\n') ? '\n' : '') +
|
|
103
|
+
'\n' + MARK_BEGIN + '\n' + line + '\n' + MARK_END + '\n';
|
|
104
|
+
|
|
105
|
+
// Append, don't rewrite — preserves the user's content exactly.
|
|
106
|
+
appendFileSync(rc.path, block, { mode: 0o644 });
|
|
107
|
+
written.push(rc.path);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
errors.push({ path: rc.path, message: err.message });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { written, errors };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Idempotently REMOVE the marker block from every detected rc file.
|
|
118
|
+
* Mirror of writeShellLine. Returns a list of files actually modified.
|
|
119
|
+
*
|
|
120
|
+
* Removal is text-based (find MARK_BEGIN, find MARK_END, splice). If the
|
|
121
|
+
* block was tampered with — e.g. user manually deleted MARK_END — we leave
|
|
122
|
+
* the file alone and surface a warning. Never destructive on partial state.
|
|
123
|
+
*/
|
|
124
|
+
export function removeShellLine(home = homedir()) {
|
|
125
|
+
const rcs = listShellRcFiles(home);
|
|
126
|
+
const removed = [];
|
|
127
|
+
const errors = [];
|
|
128
|
+
|
|
129
|
+
for (const rc of rcs) {
|
|
130
|
+
try {
|
|
131
|
+
const text = readFileSync(rc.path, 'utf-8');
|
|
132
|
+
const startIdx = text.indexOf(MARK_BEGIN);
|
|
133
|
+
if (startIdx === -1) continue;
|
|
134
|
+
const endIdx = text.indexOf(MARK_END, startIdx);
|
|
135
|
+
if (endIdx === -1) {
|
|
136
|
+
errors.push({ path: rc.path, message: 'marker_end_missing' });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Splice from MARK_BEGIN through end of MARK_END line + trailing newline.
|
|
141
|
+
const afterEnd = text.indexOf('\n', endIdx);
|
|
142
|
+
const sliceEnd = afterEnd === -1 ? text.length : afterEnd + 1;
|
|
143
|
+
|
|
144
|
+
// Walk back over the leading newline our writer added so we don't
|
|
145
|
+
// accumulate blank lines on repeated install/uninstall cycles.
|
|
146
|
+
let sliceStart = startIdx;
|
|
147
|
+
while (sliceStart > 0 && text[sliceStart - 1] === '\n') sliceStart--;
|
|
148
|
+
|
|
149
|
+
const next = text.slice(0, sliceStart) +
|
|
150
|
+
(sliceStart > 0 ? '\n' : '') +
|
|
151
|
+
text.slice(sliceEnd);
|
|
152
|
+
|
|
153
|
+
writeFileSync(rc.path, next, { mode: getMode(rc.path) });
|
|
154
|
+
removed.push(rc.path);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
errors.push({ path: rc.path, message: err.message });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { removed, errors };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getMode(path) {
|
|
164
|
+
try { return statSync(path).mode & 0o777; } catch { return 0o644; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Exported for tests + telemetry payloads.
|
|
168
|
+
export { MARK_BEGIN, MARK_END, POSIX_LINE, FISH_LINE };
|
package/lib/uninstall.js
CHANGED
|
@@ -2,21 +2,24 @@ import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { stripJson5 } from './json5.js';
|
|
5
|
+
import { removeShellLine } from './shell-config.js';
|
|
6
|
+
import { detectVenv } from './venv-detect.js';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Single source of truth for `npx robot-resources --uninstall`.
|
|
8
11
|
*
|
|
9
|
-
* Reverses
|
|
10
|
-
*
|
|
11
|
-
* (plugins.entries + plugins.allow +
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Reverses every install path the wizard might have taken:
|
|
13
|
+
* 1. OC plugin directories under ~/.openclaw/extensions/ (Phase 0)
|
|
14
|
+
* 2. Our entries in openclaw.json (plugins.entries + plugins.allow +
|
|
15
|
+
* mcp.servers) (Phase 0)
|
|
16
|
+
* 3. NODE_OPTIONS marker block in shell rc files (Phase 3 — Node shim)
|
|
17
|
+
* 4. `robot-resources` PyPI package in the resolved venv (Phase 3 —
|
|
18
|
+
* Python shim)
|
|
19
|
+
* 5. With --purge: ~/.robot-resources/ config dir (api_key + claim_url)
|
|
16
20
|
*
|
|
17
21
|
* `~/.robot-resources/config.json` is preserved by default so a subsequent
|
|
18
22
|
* re-install reuses the same api_key (and the user's claim_url stays valid).
|
|
19
|
-
* Pass { purge: true } to wipe it as well.
|
|
20
23
|
*
|
|
21
24
|
* Returns { components_removed: string[], errors: { component, message }[] }
|
|
22
25
|
* for telemetry. Failure to remove one component never aborts the others —
|
|
@@ -83,7 +86,49 @@ export function runUninstall({ purge = false } = {}) {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
// 3.
|
|
89
|
+
// 3. Shell config — remove the NODE_OPTIONS marker block from any rc
|
|
90
|
+
// files Phase 3's wizard wrote to. Idempotent: no-op if not present.
|
|
91
|
+
try {
|
|
92
|
+
const result = removeShellLine();
|
|
93
|
+
if (result.removed.length > 0) {
|
|
94
|
+
components_removed.push('shell_config_node_options');
|
|
95
|
+
}
|
|
96
|
+
for (const e of result.errors) {
|
|
97
|
+
errors.push({ component: 'shell_config_node_options', message: `${e.path}: ${e.message}` });
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
errors.push({ component: 'shell_config_node_options', message: err.message });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. Python shim — `pip uninstall -y robot-resources` against the resolved
|
|
104
|
+
// venv. Skip silently if no venv detected (the user may have installed
|
|
105
|
+
// via the wizard but already deleted the venv themselves).
|
|
106
|
+
try {
|
|
107
|
+
const venv = detectVenv();
|
|
108
|
+
if (venv.python) {
|
|
109
|
+
const result = spawnSync(venv.python, ['-m', 'pip', 'uninstall', '-y', 'robot-resources'], {
|
|
110
|
+
encoding: 'utf-8',
|
|
111
|
+
timeout: 60_000,
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
});
|
|
114
|
+
// pip exits 0 if removed, non-zero if package wasn't installed (also acceptable)
|
|
115
|
+
if (result.status === 0) {
|
|
116
|
+
components_removed.push('pip_robot_resources');
|
|
117
|
+
} else if (result.stderr && /not installed|skipping/i.test(result.stderr)) {
|
|
118
|
+
// Already gone — count as success silently.
|
|
119
|
+
} else if (result.status !== null) {
|
|
120
|
+
// Some other failure; record but don't abort
|
|
121
|
+
errors.push({
|
|
122
|
+
component: 'pip_robot_resources',
|
|
123
|
+
message: `pip exit ${result.status}: ${(result.stderr || '').slice(-200)}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
errors.push({ component: 'pip_robot_resources', message: err.message });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 5. Optionally wipe ~/.robot-resources/config.json (and any siblings)
|
|
87
132
|
if (purge) {
|
|
88
133
|
const rrDir = join(homedir(), '.robot-resources');
|
|
89
134
|
if (existsSync(rrDir)) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the Python interpreter we'll use for `pip install robot-resources`.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order (decided in the plan; never silently install into system
|
|
9
|
+
* Python — that can break OS Python on Linux):
|
|
10
|
+
*
|
|
11
|
+
* 1. $VIRTUAL_ENV — currently-active venv. Strongest signal.
|
|
12
|
+
* 2. ./.venv/bin/python — common cwd venv (uv, hatch, plain venv default).
|
|
13
|
+
* 3. ./venv/bin/python — alternative cwd venv name.
|
|
14
|
+
* 4. pyproject.toml [tool.uv]/[tool.poetry] hint — best effort.
|
|
15
|
+
* 5. Bail with confidence='low'. Caller prompts user or errors out with
|
|
16
|
+
* a `--python=/path/to/python` instruction in non-interactive mode.
|
|
17
|
+
*
|
|
18
|
+
* Returns:
|
|
19
|
+
* { python: string, kind: 'active'|'cwd-venv'|'pyproject', confidence: 'high' }
|
|
20
|
+
* OR
|
|
21
|
+
* { python: null, kind: 'none', confidence: 'low' }
|
|
22
|
+
*/
|
|
23
|
+
export function detectVenv(cwd = process.cwd()) {
|
|
24
|
+
// 1. Active venv — strongest signal.
|
|
25
|
+
const activeVenv = process.env.VIRTUAL_ENV;
|
|
26
|
+
if (activeVenv) {
|
|
27
|
+
const candidate = join(activeVenv, binSubdir(), 'python');
|
|
28
|
+
const candidate3 = join(activeVenv, binSubdir(), 'python3');
|
|
29
|
+
if (existsSync(candidate)) {
|
|
30
|
+
return { python: candidate, kind: 'active', confidence: 'high' };
|
|
31
|
+
}
|
|
32
|
+
if (existsSync(candidate3)) {
|
|
33
|
+
return { python: candidate3, kind: 'active', confidence: 'high' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. ./.venv (uv default, hatch default, plain venv default)
|
|
38
|
+
for (const dirname of ['.venv', 'venv']) {
|
|
39
|
+
const venvDir = join(cwd, dirname);
|
|
40
|
+
const cands = [
|
|
41
|
+
join(venvDir, binSubdir(), 'python'),
|
|
42
|
+
join(venvDir, binSubdir(), 'python3'),
|
|
43
|
+
];
|
|
44
|
+
for (const c of cands) {
|
|
45
|
+
if (existsSync(c)) {
|
|
46
|
+
return { python: c, kind: 'cwd-venv', confidence: 'high' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 4. Bail. Never silently install into system Python.
|
|
52
|
+
return { python: null, kind: 'none', confidence: 'low' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function binSubdir() {
|
|
56
|
+
return process.platform === 'win32' ? 'Scripts' : 'bin';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run `python -m pip install <package>` against the resolved interpreter.
|
|
61
|
+
* Captures exit code + stderr tail for telemetry. Never throws.
|
|
62
|
+
*
|
|
63
|
+
* Phase 3 ships with `--upgrade` so existing 0.1.0 installs migrate to
|
|
64
|
+
* the auto-attach-capable 0.2.0 transparently.
|
|
65
|
+
*/
|
|
66
|
+
export function runPipInstall({ python, packageSpec, timeoutMs = 120_000 }) {
|
|
67
|
+
if (!python) {
|
|
68
|
+
return { ok: false, code: -1, stderr: 'no python interpreter resolved' };
|
|
69
|
+
}
|
|
70
|
+
const args = ['-m', 'pip', 'install', '--upgrade', packageSpec];
|
|
71
|
+
const result = spawnSync(python, args, {
|
|
72
|
+
encoding: 'utf-8',
|
|
73
|
+
timeout: timeoutMs,
|
|
74
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Trim stderr to a sane size for telemetry — pip's full output is huge.
|
|
78
|
+
const stderr = (result.stderr || '').slice(-500);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
ok: result.status === 0,
|
|
82
|
+
code: result.status,
|
|
83
|
+
stderr,
|
|
84
|
+
};
|
|
85
|
+
}
|
package/lib/wizard.js
CHANGED
|
@@ -43,7 +43,7 @@ const CLI_VERSION = (() => {
|
|
|
43
43
|
*
|
|
44
44
|
* No Python, no venv, no systemd, no port probe.
|
|
45
45
|
*/
|
|
46
|
-
export async function runWizard({ nonInteractive = false, target = null } = {}) {
|
|
46
|
+
export async function runWizard({ nonInteractive = false, target = null, scope = 'full' } = {}) {
|
|
47
47
|
header();
|
|
48
48
|
|
|
49
49
|
// Detect OC once up front. Used both to branch into the non-OC wizard and
|
|
@@ -142,6 +142,15 @@ export async function runWizard({ nonInteractive = false, target = null } = {})
|
|
|
142
142
|
auth_method: results.authMethod,
|
|
143
143
|
non_interactive: nonInteractive,
|
|
144
144
|
openclaw_detected: openclawDetected,
|
|
145
|
+
// Phase 3: tags which install branch the wizard is about to take.
|
|
146
|
+
// 'oc' for the OpenClaw path, 'non-oc' covers everything the
|
|
147
|
+
// non-OC wizard handles (Node shim, Python shim, MCP, docs).
|
|
148
|
+
// Finer per-path attribution still comes from wizard_path_chosen.
|
|
149
|
+
entry: openclawDetected ? 'oc' : 'non-oc',
|
|
150
|
+
// Phase 5: 'full' is the unified `npx robot-resources` flow;
|
|
151
|
+
// 'router-only' is the standalone `npx @robot-resources/router`
|
|
152
|
+
// bin (skips scraper).
|
|
153
|
+
scope,
|
|
145
154
|
},
|
|
146
155
|
}),
|
|
147
156
|
signal: AbortSignal.timeout(5_000),
|
|
@@ -209,27 +218,33 @@ export async function runWizard({ nonInteractive = false, target = null } = {})
|
|
|
209
218
|
// Independent of router. Register scraper MCP in openclaw.json (if OC
|
|
210
219
|
// is present). Gateway restart happens once at the very end (merged
|
|
211
220
|
// with plugin restart).
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
step
|
|
221
|
+
//
|
|
222
|
+
// Phase 5: when invoked from `npx @robot-resources/router` (scope=router-only),
|
|
223
|
+
// we skip this step entirely. The standalone router CLI ships a smaller
|
|
224
|
+
// surface for users who explicitly want only routing, no scraper.
|
|
215
225
|
|
|
216
226
|
let scraperRegistered = false;
|
|
217
227
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
if (scope !== 'router-only') {
|
|
229
|
+
blank();
|
|
230
|
+
step('Installing Scraper...');
|
|
231
|
+
|
|
232
|
+
scraperRegistered = registerScraperMcp();
|
|
233
|
+
if (scraperRegistered) {
|
|
234
|
+
success('Scraper MCP registered in OpenClaw — scraper_compress_url(url) available');
|
|
235
|
+
results.scraper = true;
|
|
236
|
+
results.scraperMcpRegistered = true;
|
|
237
|
+
} else {
|
|
238
|
+
try {
|
|
239
|
+
const ocConfig = JSON.parse(readFileSync(join(homedir(), '.openclaw', 'openclaw.json'), 'utf-8'));
|
|
240
|
+
if (ocConfig?.mcp?.servers?.['robot-resources-scraper']) {
|
|
241
|
+
success('Scraper MCP already registered in OpenClaw');
|
|
242
|
+
results.scraper = true;
|
|
243
|
+
results.scraperMcpRegistered = true;
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// No openclaw.json — not on OC, skip
|
|
230
247
|
}
|
|
231
|
-
} catch {
|
|
232
|
-
// No openclaw.json — not on OC, skip
|
|
233
248
|
}
|
|
234
249
|
}
|
|
235
250
|
|