robot-resources 1.13.0 → 1.14.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.
@@ -2,54 +2,31 @@ import { writeShellLine, hasShellLine } from './shell-config.js';
2
2
  import { readConfig } from './config.mjs';
3
3
  import { detectNodeAgent } from './detect.js';
4
4
  import { installRouterFiles } from './install-router-files.js';
5
+ import { writePersistedNodeOptions } from './windows-env.js';
5
6
 
6
7
  const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
7
8
 
8
9
  /**
9
- * Install the Node shim into the user's shell config. Phase 3 entry for
10
- * the non-OC Node path.
10
+ * Install the Node shim into the user's shell config (POSIX) or user
11
+ * environment registry (Windows).
11
12
  *
12
- * Steps:
13
- * 1. Copy the bundled `@robot-resources/router` files to a stable absolute
14
- * path under `~/.robot-resources/router/` (mirrors the OC plugin pattern
15
- * at `~/.openclaw/extensions/`). Phase 8 fix: previously NODE_OPTIONS
16
- * used the bare module name `@robot-resources/router/auto` which only
17
- * resolved when the user was cd'd inside a project that had the
18
- * package in its node_modules. From any other cwd, EVERY `node`
19
- * command crashed with "Cannot find module".
20
- * 2. Append the marker block to detected rc files (zsh / bash / fish)
21
- * with the ABSOLUTE PATH to the copied auto.cjs.
22
- * 3. Emit `node_shim_installed` telemetry.
13
+ * POSIX path (Phases 3 + 8):
14
+ * 1. Copy `@robot-resources/router` to ~/.robot-resources/router/
15
+ * (absolute path; survives cwd changes + npm/npx cache cleanup).
16
+ * 2. Append marker block with `--require <abs path>` to detected rc files
17
+ * (zsh / bash / fish).
23
18
  *
24
- * The user has to start a new shell (or `source` the file) for the
25
- * NODE_OPTIONS to take effect we tell them this in the wizard's
26
- * post-install message.
19
+ * Windows path (Phase 9):
20
+ * 1. Same router-files copy`homedir()` is platform-aware.
21
+ * 2. `setx NODE_OPTIONS "..."` writes to HKCU\\Environment so every new
22
+ * cmd / PowerShell / Win+R-launched Node process inherits it.
27
23
  *
28
- * Windows: shell-config.writeShellLine returns no rc files on Windows
29
- * (we only support POSIX in P3). The wizard prints manual instructions
30
- * for Windows users in `non-oc-wizard.js`.
24
+ * Both paths emit `node_shim_installed` telemetry. The user has to open a
25
+ * new terminal for the change to take effect.
31
26
  *
32
27
  * Returns a UI-friendly result the wizard can format and print.
33
28
  */
34
29
  export async function installNodeShim({ cwd = process.cwd(), dryRun = false } = {}) {
35
- if (process.platform === 'win32') {
36
- await emit({
37
- shell: 'unsupported',
38
- shell_config_path: null,
39
- sdks_detected: detectSdks(cwd),
40
- dry_run: dryRun,
41
- reason: 'windows_not_supported_yet',
42
- });
43
- return {
44
- ok: false,
45
- reason: 'windows_not_supported_yet',
46
- message:
47
- 'Windows shell-config writing is not yet supported. Set ' +
48
- 'NODE_OPTIONS to point at ~/.robot-resources/router/auto.cjs manually ' +
49
- 'in your system environment variables, or wait for Phase 6.',
50
- };
51
- }
52
-
53
30
  const sdks = detectSdks(cwd);
54
31
 
55
32
  if (dryRun) {
@@ -60,18 +37,18 @@ export async function installNodeShim({ cwd = process.cwd(), dryRun = false } =
60
37
  dry_run: true,
61
38
  reason: null,
62
39
  });
63
- return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS to shell rc.' };
40
+ return { ok: true, message: 'Dry-run: would have written NODE_OPTIONS.' };
64
41
  }
65
42
 
66
43
  // Phase 8: copy router to an absolute path under ~/.robot-resources/router/
67
- // before we wire the shell config. If the copy fails, we don't write a
68
- // broken NODE_OPTIONS line.
44
+ // before we wire the env config. If the copy fails, we don't write a
45
+ // broken NODE_OPTIONS line on either platform.
69
46
  let autoPath;
70
47
  try {
71
48
  autoPath = installRouterFiles();
72
49
  } catch (err) {
73
50
  await emit({
74
- shell: 'unknown',
51
+ shell: process.platform === 'win32' ? 'win32' : 'unknown',
75
52
  shell_config_path: null,
76
53
  sdks_detected: sdks,
77
54
  dry_run: false,
@@ -84,6 +61,48 @@ export async function installNodeShim({ cwd = process.cwd(), dryRun = false } =
84
61
  };
85
62
  }
86
63
 
64
+ // Windows branch — Phase 9. Mirrors the POSIX flow in shape: detect
65
+ // already-installed via the persisted registry value, write via setx,
66
+ // emit equivalent telemetry.
67
+ if (process.platform === 'win32') {
68
+ const winResult = writePersistedNodeOptions({ autoPath });
69
+ await emit({
70
+ shell: 'win32',
71
+ shell_config_path: 'HKCU\\Environment\\NODE_OPTIONS',
72
+ sdks_detected: sdks,
73
+ dry_run: false,
74
+ already_installed: !!winResult.already,
75
+ files_written: winResult.ok && !winResult.already ? 1 : 0,
76
+ files_with_errors: winResult.ok ? 0 : 1,
77
+ error_messages: winResult.ok ? [] : [winResult.error_message || winResult.reason || 'unknown'],
78
+ auto_path: autoPath,
79
+ win_node_options_length: winResult.length,
80
+ reason: winResult.ok ? null : winResult.reason,
81
+ });
82
+ if (winResult.ok && winResult.already) {
83
+ return {
84
+ ok: true,
85
+ already: true,
86
+ message: 'NODE_OPTIONS already includes the auto-attach line. No changes made.',
87
+ };
88
+ }
89
+ if (!winResult.ok) {
90
+ return {
91
+ ok: false,
92
+ reason: winResult.reason,
93
+ message: `Could not set NODE_OPTIONS via setx (${winResult.reason}): ${winResult.error_message || ''}`,
94
+ };
95
+ }
96
+ return {
97
+ ok: true,
98
+ written: ['HKCU\\Environment\\NODE_OPTIONS'],
99
+ errors: [],
100
+ message:
101
+ 'Set NODE_OPTIONS in your user environment (HKCU\\Environment). ' +
102
+ 'Open a new terminal for it to take effect. Existing terminals will not see the change.',
103
+ };
104
+ }
105
+
87
106
  const alreadyInstalled = hasShellLine();
88
107
  const result = writeShellLine({ autoPath });
89
108
 
@@ -106,13 +106,23 @@ async function showJsPath() {
106
106
  }
107
107
  blank();
108
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)');
109
+ info('this machine routes Anthropic, OpenAI, and Google SDK calls through');
110
+ info('Robot Resources.');
111
+ if (process.platform === 'win32') {
112
+ info('Open a new cmd / PowerShell window — current terminals will not see the change.');
113
+ } else {
114
+ info('Open a new terminal — or run: source ~/.zshrc (or your shell rc)');
115
+ }
111
116
  } else {
112
117
  warn(result.message);
113
118
  blank();
114
- info('Manual install (paste into ~/.zshrc or ~/.bashrc):');
115
- info(' export NODE_OPTIONS="${NODE_OPTIONS:-} --require @robot-resources/router/auto"');
119
+ if (process.platform === 'win32') {
120
+ info('Manual install: in a new cmd, run:');
121
+ info(' setx NODE_OPTIONS "--require %USERPROFILE%\\.robot-resources\\router\\auto.cjs"');
122
+ } else {
123
+ info('Manual install (paste into ~/.zshrc or ~/.bashrc):');
124
+ info(' export NODE_OPTIONS="${NODE_OPTIONS:-} --require ~/.robot-resources/router/auto.cjs"');
125
+ }
116
126
  }
117
127
  blank();
118
128
  info('Docs: https://robotresources.ai/docs/langchain');
package/lib/uninstall.js CHANGED
@@ -5,6 +5,7 @@ import { stripJson5 } from './json5.js';
5
5
  import { removeShellLine } from './shell-config.js';
6
6
  import { detectVenv } from './venv-detect.js';
7
7
  import { spawnSync } from 'node:child_process';
8
+ import { removePersistedNodeOptions } from './windows-env.js';
8
9
 
9
10
  /**
10
11
  * Single source of truth for `npx robot-resources --uninstall`.
@@ -86,18 +87,32 @@ export function runUninstall({ purge = false } = {}) {
86
87
  }
87
88
  }
88
89
 
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');
90
+ // 3. Shell config / Windows registry — remove the NODE_OPTIONS line
91
+ // Phase 3's wizard wrote to. Idempotent: no-op if not present.
92
+ if (process.platform === 'win32') {
93
+ // Phase 9: restore from backup or clear HKCU\Environment\NODE_OPTIONS.
94
+ try {
95
+ const result = removePersistedNodeOptions();
96
+ if (result.ok && result.action !== 'noop') {
97
+ components_removed.push(`win_node_options_${result.action}`);
98
+ } else if (!result.ok) {
99
+ errors.push({ component: 'win_node_options', message: 'reg_restore_failed' });
100
+ }
101
+ } catch (err) {
102
+ errors.push({ component: 'win_node_options', message: err.message });
95
103
  }
96
- for (const e of result.errors) {
97
- errors.push({ component: 'shell_config_node_options', message: `${e.path}: ${e.message}` });
104
+ } else {
105
+ try {
106
+ const result = removeShellLine();
107
+ if (result.removed.length > 0) {
108
+ components_removed.push('shell_config_node_options');
109
+ }
110
+ for (const e of result.errors) {
111
+ errors.push({ component: 'shell_config_node_options', message: `${e.path}: ${e.message}` });
112
+ }
113
+ } catch (err) {
114
+ errors.push({ component: 'shell_config_node_options', message: err.message });
98
115
  }
99
- } catch (err) {
100
- errors.push({ component: 'shell_config_node_options', message: err.message });
101
116
  }
102
117
 
103
118
  // 3b. Copied router dir at ~/.robot-resources/router/ (Phase 8). The shell
@@ -0,0 +1,202 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ /**
7
+ * Windows NODE_OPTIONS persistence (Phase 9).
8
+ *
9
+ * The POSIX path uses a marker block in `~/.bashrc` / `~/.zshrc`. Windows
10
+ * has no equivalent shell rc that all Node-launched processes read — cmd,
11
+ * PowerShell, and Win+R-launched .exe all draw env vars from the user
12
+ * registry under HKCU\Environment.
13
+ *
14
+ * `setx NODE_OPTIONS "value"` writes there. New processes pick it up.
15
+ * The current shell does NOT see the change — user has to open a new
16
+ * terminal. Same UX caveat as POSIX.
17
+ *
18
+ * Why not edit PowerShell `$PROFILE`: ExecutionPolicy on locked-down
19
+ * corporate fleets often blocks unsigned `.ps1`. cmd-launched Node
20
+ * processes also miss it. setx is universal across both.
21
+ *
22
+ * Idempotency: we read the persisted NODE_OPTIONS via `reg query` (not
23
+ * `process.env.NODE_OPTIONS`, which is the current-shell value, not the
24
+ * persistent one). If our `--require <auto>` is already present, no-op.
25
+ *
26
+ * Uninstall: backup the user's PRE-modification value to
27
+ * `~/.robot-resources/windows-prior-node-options.txt`. On `--uninstall`,
28
+ * restore from backup; if the backup is missing or empty, clear the var.
29
+ *
30
+ * Truncation note: `setx` truncates values at 1024 chars. If a user
31
+ * already has a long NODE_OPTIONS plus other dev tooling, we may be
32
+ * close to the limit. We surface the merged length in telemetry so we
33
+ * can spot truncation in Supabase.
34
+ */
35
+
36
+ const REG_PATH = 'HKCU\\Environment';
37
+ const VAR_NAME = 'NODE_OPTIONS';
38
+ const SETX_LIMIT = 1024;
39
+
40
+ function backupFilePath(home = homedir()) {
41
+ return join(home, '.robot-resources', 'windows-prior-node-options.txt');
42
+ }
43
+
44
+ /**
45
+ * Read the persisted NODE_OPTIONS value from HKCU\Environment.
46
+ * Returns the string (possibly empty) or null if reading failed.
47
+ */
48
+ export function readPersistedNodeOptions() {
49
+ const res = spawnSync(
50
+ 'reg.exe',
51
+ ['query', REG_PATH, '/v', VAR_NAME],
52
+ { stdio: 'pipe', encoding: 'utf-8' },
53
+ );
54
+ if (res.status !== 0) {
55
+ // `reg query` returns non-zero when the value doesn't exist. That's a
56
+ // valid state — empty NODE_OPTIONS — not an error.
57
+ const stderr = (res.stderr || '').toLowerCase();
58
+ if (stderr.includes('unable to find') || stderr.includes('not exist')) {
59
+ return '';
60
+ }
61
+ return null;
62
+ }
63
+ // Output looks like:
64
+ // HKEY_CURRENT_USER\Environment
65
+ // NODE_OPTIONS REG_SZ --require ...
66
+ // We extract the value after REG_SZ. The value can contain spaces; take
67
+ // everything after the last "REG_SZ" occurrence on its line.
68
+ const lines = (res.stdout || '').split(/\r?\n/);
69
+ for (const line of lines) {
70
+ const m = line.match(/^\s*NODE_OPTIONS\s+REG_(?:SZ|EXPAND_SZ)\s+(.*)$/);
71
+ if (m) return m[1].trim();
72
+ }
73
+ return '';
74
+ }
75
+
76
+ /**
77
+ * Set NODE_OPTIONS in HKCU\Environment to the given value (idempotent
78
+ * append of `--require <autoPath>` to whatever was there).
79
+ *
80
+ * Returns:
81
+ * { ok: true, already: boolean, prior: string, written: string, length: number }
82
+ * { ok: false, reason, error_message? }
83
+ */
84
+ export function writePersistedNodeOptions({ autoPath, home = homedir() }) {
85
+ if (!autoPath) {
86
+ return { ok: false, reason: 'missing_auto_path' };
87
+ }
88
+
89
+ const prior = readPersistedNodeOptions();
90
+ if (prior === null) {
91
+ return { ok: false, reason: 'reg_query_failed' };
92
+ }
93
+
94
+ // Quote the path so a path with spaces survives Node's --require parser.
95
+ // Node accepts both quoted and unquoted forms; quoting is safer.
96
+ const ourArg = `--require "${autoPath}"`;
97
+
98
+ if (prior.includes(ourArg) || prior.includes(`--require ${autoPath}`)) {
99
+ return {
100
+ ok: true,
101
+ already: true,
102
+ prior,
103
+ written: prior,
104
+ length: prior.length,
105
+ };
106
+ }
107
+
108
+ const merged = prior ? `${prior} ${ourArg}` : ourArg;
109
+
110
+ if (merged.length > SETX_LIMIT) {
111
+ // setx truncates silently at 1024 chars. Refuse rather than write a
112
+ // broken value. User must shorten their existing NODE_OPTIONS first.
113
+ return {
114
+ ok: false,
115
+ reason: 'setx_limit_exceeded',
116
+ error_message: `merged value is ${merged.length} chars; setx truncates at ${SETX_LIMIT}`,
117
+ length: merged.length,
118
+ };
119
+ }
120
+
121
+ // Backup the prior value BEFORE writing, so --uninstall can restore.
122
+ // Even if backup write fails (disk full, permissions), we still proceed —
123
+ // the registry-level setx is the source of truth, the backup is just a
124
+ // convenience for restore.
125
+ try {
126
+ const backup = backupFilePath(home);
127
+ mkdirSync(join(home, '.robot-resources'), { recursive: true });
128
+ writeFileSync(backup, prior, { encoding: 'utf-8' });
129
+ } catch {
130
+ // Best-effort backup; continue.
131
+ }
132
+
133
+ const setxRes = spawnSync(
134
+ 'setx.exe',
135
+ [VAR_NAME, merged],
136
+ { stdio: 'pipe', encoding: 'utf-8' },
137
+ );
138
+ if (setxRes.status !== 0) {
139
+ const stderr = (setxRes.stderr || '').toString().trim();
140
+ return {
141
+ ok: false,
142
+ reason: 'setx_failed',
143
+ error_message: stderr.slice(0, 200) || `exit ${setxRes.status}`,
144
+ length: merged.length,
145
+ };
146
+ }
147
+
148
+ return {
149
+ ok: true,
150
+ already: false,
151
+ prior,
152
+ written: merged,
153
+ length: merged.length,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Reverse `writePersistedNodeOptions`. Reads the backup file (if present)
159
+ * and restores that value via setx. If no backup is present or the backup
160
+ * is empty, clears the registry value entirely (`reg delete`).
161
+ *
162
+ * Returns { ok, restored_to: string, action: 'restored'|'cleared'|'noop' }
163
+ */
164
+ export function removePersistedNodeOptions({ home = homedir() } = {}) {
165
+ const current = readPersistedNodeOptions();
166
+ if (current === null) return { ok: false, action: 'noop' };
167
+ if (current === '') return { ok: true, restored_to: '', action: 'noop' };
168
+
169
+ const backupPath = backupFilePath(home);
170
+ let priorValue = '';
171
+ if (existsSync(backupPath)) {
172
+ try { priorValue = readFileSync(backupPath, 'utf-8'); } catch { /* */ }
173
+ }
174
+
175
+ if (priorValue === '') {
176
+ // No prior value to restore — clear the var entirely.
177
+ const res = spawnSync(
178
+ 'reg.exe',
179
+ ['delete', REG_PATH, '/v', VAR_NAME, '/f'],
180
+ { stdio: 'pipe', encoding: 'utf-8' },
181
+ );
182
+ if (existsSync(backupPath)) try { unlinkSync(backupPath); } catch { /* */ }
183
+ return {
184
+ ok: res.status === 0,
185
+ restored_to: '',
186
+ action: 'cleared',
187
+ };
188
+ }
189
+
190
+ // Restore the prior value.
191
+ const res = spawnSync(
192
+ 'setx.exe',
193
+ [VAR_NAME, priorValue],
194
+ { stdio: 'pipe', encoding: 'utf-8' },
195
+ );
196
+ if (existsSync(backupPath)) try { unlinkSync(backupPath); } catch { /* */ }
197
+ return {
198
+ ok: res.status === 0,
199
+ restored_to: priorValue,
200
+ action: 'restored',
201
+ };
202
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "Robot Resources — AI agent tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {