robot-resources 1.8.1 → 1.9.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/lib/service.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execSync, spawnSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join, dirname } from 'node:path';
@@ -6,6 +6,7 @@ import { readProviderKeys } from '@robot-resources/cli-core/config.mjs';
6
6
 
7
7
  const LABEL = 'ai.robotresources.router';
8
8
  const SERVICE_NAME = 'robot-resources-router.service';
9
+ const TASK_NAME = 'RobotResourcesRouter';
9
10
  const ROUTER_PORT = 3838;
10
11
 
11
12
  // Maps config.json provider_keys names to environment variable names
@@ -57,7 +58,11 @@ function resolveProviderEnv() {
57
58
  resolvedKeys[envName] = configKeys[configName];
58
59
  }
59
60
  }
60
- resolvedKeys['PATH'] = '/usr/local/bin:/usr/bin:/bin';
61
+ // Windows inherits PATH from the user profile when schtasks fires the task;
62
+ // overwriting it to a Unix path would break every `python.exe` resolution.
63
+ if (process.platform !== 'win32') {
64
+ resolvedKeys['PATH'] = '/usr/local/bin:/usr/bin:/bin';
65
+ }
61
66
  return resolvedKeys;
62
67
  }
63
68
 
@@ -317,6 +322,91 @@ function getLinuxUnitPath() {
317
322
  return isRoot() ? SYSTEM_UNIT_PATH : getUserUnitPath();
318
323
  }
319
324
 
325
+ // ─── Windows (Task Scheduler) ───────────────────────────────────────────────
326
+
327
+ function getWrapperCmdPath() {
328
+ return join(homedir(), '.robot-resources', 'rr-router-run.cmd');
329
+ }
330
+
331
+ function buildWrapperCmd(venvPythonPath, envFilePath, logsDir) {
332
+ // The wrapper sources router.env line-by-line into `set` (so quoting is
333
+ // handled by cmd.exe), then launches the router. stdout/stderr append to
334
+ // log files so the scheduled task — which has no attached console — leaves
335
+ // a trail the user can read.
336
+ return [
337
+ '@echo off',
338
+ 'setlocal enabledelayedexpansion',
339
+ `for /f "usebackq tokens=* delims=" %%a in ("${envFilePath}") do set "%%a"`,
340
+ `"${venvPythonPath}" -m robot_resources.cli.main start >> "${join(logsDir, 'router.stdout.log')}" 2>> "${join(logsDir, 'router.stderr.log')}"`,
341
+ '',
342
+ ].join('\r\n');
343
+ }
344
+
345
+ function runSchtasks(args, { ignoreStatus = false } = {}) {
346
+ // spawnSync (not execSync) because paths like `C:\Users\Some Person\...`
347
+ // have spaces and shell re-tokenization via execSync is a quoting footgun.
348
+ // spawnSync with an arg array passes each arg verbatim to schtasks.exe.
349
+ const res = spawnSync('schtasks.exe', args, { stdio: 'pipe', encoding: 'utf-8' });
350
+ if (!ignoreStatus && res.status !== 0) {
351
+ const stderr = (res.stderr || '').toString().trim();
352
+ const stdout = (res.stdout || '').toString().trim();
353
+ throw new Error(`schtasks ${args[0]} failed (status ${res.status}): ${stderr || stdout || 'unknown error'}`);
354
+ }
355
+ return res;
356
+ }
357
+
358
+ function installTaskScheduler(venvPythonPath) {
359
+ const logsDir = join(homedir(), '.robot-resources', 'logs');
360
+ mkdirSync(logsDir, { recursive: true });
361
+
362
+ const resolvedKeys = resolveProviderEnv();
363
+ const envFilePath = writeEnvFile(resolvedKeys);
364
+ const wrapperPath = getWrapperCmdPath();
365
+ writeFileSync(wrapperPath, buildWrapperCmd(venvPythonPath, envFilePath, logsDir));
366
+
367
+ // /rl LIMITED is the default — explicit here for readability. /f overwrites
368
+ // an existing task of the same name so the install is idempotent.
369
+ runSchtasks([
370
+ '/create', '/sc', 'onlogon',
371
+ '/tn', TASK_NAME,
372
+ '/tr', `"${wrapperPath}"`,
373
+ '/rl', 'LIMITED',
374
+ '/f',
375
+ ]);
376
+
377
+ // Launch now so the wizard's post-install health check can hit port 3838.
378
+ // Best-effort — if the immediate run fails, the task is still registered
379
+ // and will fire at next logon.
380
+ runSchtasks(['/run', '/tn', TASK_NAME], { ignoreStatus: true });
381
+ }
382
+
383
+ function uninstallTaskScheduler() {
384
+ runSchtasks(['/end', '/tn', TASK_NAME], { ignoreStatus: true });
385
+ runSchtasks(['/delete', '/tn', TASK_NAME, '/f'], { ignoreStatus: true });
386
+ if (existsSync(getWrapperCmdPath())) unlinkSync(getWrapperCmdPath());
387
+ }
388
+
389
+ function isTaskSchedulerRunning() {
390
+ const res = spawnSync(
391
+ 'schtasks.exe',
392
+ ['/query', '/tn', TASK_NAME, '/fo', 'LIST', '/v'],
393
+ { stdio: 'pipe', encoding: 'utf-8' },
394
+ );
395
+ if (res.status !== 0) return false;
396
+ // 0x41301 is TASK_RUNNING (HRESULT). Locale-independent — the localized
397
+ // "Running" / "In corso" / "実行中" string is unreliable across Windows SKUs.
398
+ return (res.stdout || '').includes('0x41301');
399
+ }
400
+
401
+ function isTaskSchedulerInstalled() {
402
+ const res = spawnSync(
403
+ 'schtasks.exe',
404
+ ['/query', '/tn', TASK_NAME],
405
+ { stdio: 'pipe' },
406
+ );
407
+ return res.status === 0;
408
+ }
409
+
320
410
  // ─── Public API ─────────────────────────────────────────────────────────────
321
411
 
322
412
  /**
@@ -367,6 +457,11 @@ export function installService(venvPythonPath) {
367
457
  return { type: 'systemd-user', path: getUserUnitPath() };
368
458
  }
369
459
 
460
+ if (process.platform === 'win32') {
461
+ installTaskScheduler(venvPythonPath);
462
+ return { type: 'schtasks', path: getWrapperCmdPath() };
463
+ }
464
+
370
465
  throw new Error(
371
466
  `Service registration not supported on ${process.platform}.\n` +
372
467
  ` Run the router manually: rr-router start`
@@ -383,6 +478,7 @@ export function uninstallService() {
383
478
  if (existsSync(SYSTEM_UNIT_PATH)) return uninstallSystemdSystem();
384
479
  if (existsSync(getUserUnitPath())) return uninstallSystemdUser();
385
480
  }
481
+ if (process.platform === 'win32') return uninstallTaskScheduler();
386
482
  }
387
483
 
388
484
  /**
@@ -407,6 +503,7 @@ export function isServiceRunning() {
407
503
  return false;
408
504
  }
409
505
  }
506
+ if (process.platform === 'win32') return isTaskSchedulerRunning();
410
507
  return false;
411
508
  }
412
509
 
@@ -416,6 +513,7 @@ export function isServiceRunning() {
416
513
  export function isServiceInstalled() {
417
514
  if (process.platform === 'darwin') return existsSync(getPlistPath());
418
515
  if (process.platform === 'linux') return existsSync(SYSTEM_UNIT_PATH) || existsSync(getUserUnitPath());
516
+ if (process.platform === 'win32') return isTaskSchedulerInstalled();
419
517
  return false;
420
518
  }
421
519
 
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createRequire } from 'node:module';
3
- import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'node:fs';
3
+ import { readFileSync, writeFileSync, copyFileSync, cpSync, mkdirSync, existsSync, rmSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { isOpenClawInstalled, isOpenClawPluginInstalled, getOpenClawAuthMode, isClaudeCodeInstalled, isCursorInstalled } from './detect.js';
@@ -98,7 +98,11 @@ function registerScraperMcp() {
98
98
  *
99
99
  * The plugin ships as a CLI dependency (@robot-resources/openclaw-plugin).
100
100
  * Instead of spawning `openclaw plugins install` (30s npm overhead),
101
- * we copy the 3 files directly. Same destination, same result.
101
+ * we copy files directly. Same destination, same result.
102
+ *
103
+ * Since 0.5.5, the plugin is a thin shim (index.js) that imports the rest
104
+ * of its code from ./lib/*.js — copy the lib/ directory too, or the shim
105
+ * fails to load with MODULE_NOT_FOUND.
102
106
  */
103
107
  function installPluginFiles() {
104
108
  const require = createRequire(import.meta.url);
@@ -111,6 +115,15 @@ function installPluginFiles() {
111
115
  for (const file of ['index.js', 'openclaw.plugin.json', 'package.json']) {
112
116
  copyFileSync(join(pluginDir, file), join(targetDir, file));
113
117
  }
118
+
119
+ // Copy lib/ recursively. Clear the destination first so files removed in
120
+ // a new version don't linger from a previous install.
121
+ const srcLib = join(pluginDir, 'lib');
122
+ const dstLib = join(targetDir, 'lib');
123
+ if (existsSync(srcLib)) {
124
+ rmSync(dstLib, { recursive: true, force: true });
125
+ cpSync(srcLib, dstLib, { recursive: true });
126
+ }
114
127
  }
115
128
 
116
129
  /**
package/lib/wizard.js CHANGED
@@ -162,9 +162,6 @@ export async function runWizard({ nonInteractive = false } = {}) {
162
162
  if (isServiceRunning()) {
163
163
  success('Router service already running');
164
164
  results.service = true;
165
- } else if (process.platform === 'win32') {
166
- warn('Windows detected — automatic service not supported');
167
- info('Run the router manually: rr-router start');
168
165
  } else {
169
166
  // Check port availability
170
167
  if (!isPortAvailable()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robot-resources",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "Robot Resources — AI agent tools. One command to install everything.",
5
5
  "type": "module",
6
6
  "bin": {