@openagents-org/agent-launcher 0.2.158 → 0.2.160

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.158",
3
+ "version": "0.2.160",
4
4
  "description": "OpenAgents Launcher \u2014 install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/daemon.js CHANGED
@@ -120,9 +120,13 @@ class Daemon {
120
120
  try {
121
121
  if (action === 'create_agent') {
122
122
  const type = (args.type || '').trim();
123
- const createArgs = ['create', name, '--type', type, '--install'];
124
- if (args.workingDir) createArgs.push('--path', String(args.workingDir));
125
- const r1 = await this._runAgn(createArgs);
123
+ // Working directory: use the one the user picked, else a managed folder
124
+ // under the launcher home so every remote-created agent has a clean,
125
+ // predictable home without the user typing a path.
126
+ let workingDir = (args.workingDir || '').trim();
127
+ if (!workingDir) workingDir = path.join(os.homedir(), '.openagents', 'agents', name);
128
+ try { fs.mkdirSync(workingDir, { recursive: true }); } catch {}
129
+ const r1 = await this._runAgn(['create', name, '--type', type, '--install', '--path', workingDir]);
126
130
  if (r1.code !== 0) throw new Error(r1.stderr || r1.stdout || 'create failed');
127
131
  // Optional credentials for API-key agents (generic → provider mapping
128
132
  // happens in env resolution).
package/src/installer.js CHANGED
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const { execSync, exec } = require('child_process');
7
7
  const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
8
8
  const { EnvManager } = require('./env');
9
+ const { nodeDistUrls, installRegistry } = require('./mirrors');
9
10
  const { readinessReason, REASON } = require('./adapters/health-status');
10
11
 
11
12
  const STATUS_CACHE_TTL_MS = 10000;
@@ -1013,8 +1014,14 @@ class Installer {
1013
1014
  .split(/\s+/)
1014
1015
  .filter(Boolean);
1015
1016
  // Use --save so npm tracks the package in package.json (prevents pruning
1016
- // on next install).
1017
- const npmArgs = ['install', '--loglevel=verbose', '--save', '--prefix', prefixDir, ...pkgArgs];
1017
+ // on next install). --registry is added only when we'd be helping — see
1018
+ // installRegistry(); a user's own registry choice is never overridden.
1019
+ const registry = installRegistry();
1020
+ const npmArgs = [
1021
+ 'install', '--loglevel=verbose', '--save', '--prefix', prefixDir,
1022
+ ...(registry ? ['--registry', registry] : []),
1023
+ ...pkgArgs,
1024
+ ];
1018
1025
  displayCmd = `npm ${npmArgs.join(' ')}`;
1019
1026
 
1020
1027
  const direct = this._resolveNodeNpmCli();
@@ -1031,7 +1038,8 @@ class Installer {
1031
1038
  }
1032
1039
  } else {
1033
1040
  // Legacy fallback: quoted prefix inside a shell string.
1034
- const args = `install --loglevel=verbose --save --prefix "${prefixDir}" ${pkgArgs.join(' ')}`;
1041
+ const registryArg = registry ? `--registry ${registry} ` : '';
1042
+ const args = `install --loglevel=verbose --save --prefix "${prefixDir}" ${registryArg}${pkgArgs.join(' ')}`;
1035
1043
  spawnFile = this._wrapForWindowsShell(this._resolveNpmCommand(args));
1036
1044
  useShell = isWin ? (env.ComSpec || 'C:\\Windows\\System32\\cmd.exe') : true;
1037
1045
  displayCmd = spawnFile;
@@ -1623,11 +1631,13 @@ class Installer {
1623
1631
  if (plat === 'windows') {
1624
1632
  // Download portable zip — no admin required
1625
1633
  const arch = os.arch() === 'x64' ? 'x64' : 'x86';
1626
- const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-win-${arch}.zip`;
1627
1634
  const zipPath = path.join(os.tmpdir(), `node-${nodeVersion}.zip`);
1628
1635
 
1629
- if (onData) onData(`Downloading ${url}...\n`);
1630
- await this._downloadFile(url, zipPath, onData);
1636
+ await this._downloadFirst(
1637
+ nodeDistUrls(`${nodeVersion}/node-${nodeVersion}-win-${arch}.zip`),
1638
+ zipPath,
1639
+ onData
1640
+ );
1631
1641
 
1632
1642
  // Extract to ~/.openagents/nodejs/
1633
1643
  const nodejsDir = path.join(this.configDir, 'nodejs');
@@ -1688,11 +1698,13 @@ class Installer {
1688
1698
  const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
1689
1699
  const ext = plat === 'macos' ? 'tar.gz' : 'tar.xz';
1690
1700
  const platName = plat === 'macos' ? 'darwin' : 'linux';
1691
- const url = `https://nodejs.org/dist/${nodeVersion}/node-${nodeVersion}-${platName}-${arch}.${ext}`;
1692
1701
  const tarPath = path.join(os.tmpdir(), `node-${nodeVersion}.${ext}`);
1693
1702
 
1694
- if (onData) onData(`Downloading ${url}...\n`);
1695
- await this._downloadFile(url, tarPath, onData);
1703
+ await this._downloadFirst(
1704
+ nodeDistUrls(`${nodeVersion}/node-${nodeVersion}-${platName}-${arch}.${ext}`),
1705
+ tarPath,
1706
+ onData
1707
+ );
1696
1708
 
1697
1709
  const nodeDir = path.join(this.configDir, 'nodejs');
1698
1710
  fs.mkdirSync(nodeDir, { recursive: true });
@@ -1725,23 +1737,61 @@ class Installer {
1725
1737
  /**
1726
1738
  * Download a file with progress reporting.
1727
1739
  */
1740
+ /**
1741
+ * Try each candidate URL in order until one delivers the file. Mirrors carry
1742
+ * identical bytes, so a slow or unreachable origin costs one timeout instead
1743
+ * of failing the whole install.
1744
+ */
1745
+ async _downloadFirst(urls, destPath, onData) {
1746
+ let lastErr = null;
1747
+ for (const url of urls) {
1748
+ try {
1749
+ if (onData) onData(`Downloading ${url}...\n`);
1750
+ await this._downloadFile(url, destPath, onData);
1751
+ return url;
1752
+ } catch (err) {
1753
+ lastErr = err;
1754
+ if (onData) onData(` failed (${err.message}) — trying next source\n`);
1755
+ }
1756
+ }
1757
+ throw lastErr || new Error('Download failed: no sources available');
1758
+ }
1759
+
1760
+ /**
1761
+ * Download a file with progress reporting.
1762
+ *
1763
+ * Writes to `${destPath}.part` and renames on success: a partial file left at
1764
+ * the final path reads as "already installed" on the next run and produces a
1765
+ * corrupt runtime. A stall timeout and a short-read check are what turn a
1766
+ * silently truncated transfer into a retryable error.
1767
+ */
1728
1768
  _downloadFile(url, destPath, onData) {
1729
1769
  const https = require('https');
1730
1770
  const http = require('http');
1771
+ const STALL_TIMEOUT_MS = 30000;
1772
+ const tmpPath = `${destPath}.part`;
1773
+ try { fs.unlinkSync(tmpPath); } catch {}
1774
+
1731
1775
  return new Promise((resolve, reject) => {
1732
1776
  const get = url.startsWith('https') ? https.get : http.get;
1733
- get(url, (res) => {
1777
+ const fail = (err) => {
1778
+ try { fs.unlinkSync(tmpPath); } catch {}
1779
+ reject(err);
1780
+ };
1781
+ const req = get(url, (res) => {
1734
1782
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
1735
- // Follow redirect
1736
- return this._downloadFile(res.headers.location, destPath, onData).then(resolve, reject);
1783
+ res.resume();
1784
+ const next = new URL(res.headers.location, url).toString();
1785
+ return this._downloadFile(next, destPath, onData).then(resolve, reject);
1737
1786
  }
1738
1787
  if (res.statusCode !== 200) {
1739
- return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
1788
+ res.resume();
1789
+ return fail(new Error(`Download failed: HTTP ${res.statusCode}`));
1740
1790
  }
1741
1791
  const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
1742
1792
  let downloaded = 0;
1743
1793
  let lastPercent = -1;
1744
- const file = fs.createWriteStream(destPath);
1794
+ const file = fs.createWriteStream(tmpPath);
1745
1795
  res.on('data', (chunk) => {
1746
1796
  downloaded += chunk.length;
1747
1797
  if (totalBytes > 0) {
@@ -1752,10 +1802,29 @@ class Installer {
1752
1802
  }
1753
1803
  }
1754
1804
  });
1805
+ res.on('error', fail);
1806
+ file.on('error', fail);
1755
1807
  res.pipe(file);
1756
- file.on('finish', () => { file.close(); resolve(); });
1757
- file.on('error', reject);
1758
- }).on('error', reject);
1808
+ file.on('finish', () => {
1809
+ file.close(() => {
1810
+ if (totalBytes > 0 && downloaded !== totalBytes) {
1811
+ return fail(new Error(`Truncated download: ${downloaded} of ${totalBytes} bytes`));
1812
+ }
1813
+ try {
1814
+ fs.renameSync(tmpPath, destPath);
1815
+ } catch (err) {
1816
+ return fail(err);
1817
+ }
1818
+ resolve();
1819
+ });
1820
+ });
1821
+ });
1822
+ // Guard the connection itself: without this a black-holed origin keeps
1823
+ // the promise pending forever and the install just sits there.
1824
+ req.setTimeout(STALL_TIMEOUT_MS, () => {
1825
+ req.destroy(new Error(`Download timed out after ${STALL_TIMEOUT_MS / 1000}s`));
1826
+ });
1827
+ req.on('error', fail);
1759
1828
  });
1760
1829
  }
1761
1830
 
package/src/mirrors.js ADDED
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Region-aware download origins.
5
+ *
6
+ * The core downloads a portable Node.js runtime (nodejs.org) and talks to the
7
+ * npm registry. Both are slow to unusable from mainland China, which shows up
8
+ * as an install that appears to hang. Mirrors carry byte-identical copies under
9
+ * the same path layout, so the only decision is which origin to try first —
10
+ * every list keeps the official one as the final fallback.
11
+ *
12
+ * Where the region comes from, in order:
13
+ * 1. OPENAGENTS_NODE_MIRRORS / OPENAGENTS_NPM_MIRRORS — explicit, comma
14
+ * separated base URLs. The launcher sets these from its own Settings →
15
+ * Network preference so the core inherits the user's actual choice
16
+ * instead of guessing again in a child process.
17
+ * 2. OPENAGENTS_DOWNLOAD_REGION = cn | global — explicit override for CLI
18
+ * users and support.
19
+ * 3. Timezone / locale detection (best effort).
20
+ */
21
+
22
+ const OFFICIAL_NODE = 'https://nodejs.org/dist';
23
+ const CN_NODE = [
24
+ 'https://cdn.npmmirror.com/binaries/node',
25
+ 'https://mirrors.aliyun.com/nodejs-release',
26
+ 'https://mirrors.huaweicloud.com/nodejs',
27
+ ];
28
+ const OFFICIAL_NPM = 'https://registry.npmjs.org';
29
+ const CN_NPM = [
30
+ 'https://registry.npmmirror.com',
31
+ 'https://mirrors.cloud.tencent.com/npm',
32
+ ];
33
+
34
+ function envBases(name) {
35
+ return (process.env[name] || '')
36
+ .split(',')
37
+ .map((s) => s.trim().replace(/\/+$/, ''))
38
+ .filter((s) => /^https?:\/\//.test(s));
39
+ }
40
+
41
+ function inChina() {
42
+ const region = (process.env.OPENAGENTS_DOWNLOAD_REGION || '').toLowerCase();
43
+ if (region === 'cn') return true;
44
+ if (region === 'global') return false;
45
+ try {
46
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
47
+ if (/Shanghai|Chongqing|Chungking|Urumqi|Harbin|Kashgar|PRC/i.test(tz)) return true;
48
+ } catch {}
49
+ const locale = (process.env.LC_ALL || process.env.LANG || '').toLowerCase();
50
+ if (locale.startsWith('zh_cn') || locale.startsWith('zh-cn')) return true;
51
+ return false;
52
+ }
53
+
54
+ function bases(envName, official, mirrors) {
55
+ const fromEnv = envBases(envName);
56
+ if (fromEnv.length) {
57
+ // Trust the caller's list, but never drop the official origin entirely —
58
+ // a mirror outage must still degrade to "slow", not "broken".
59
+ return fromEnv.includes(official) ? fromEnv : [...fromEnv, official];
60
+ }
61
+ return inChina() ? [...mirrors, official] : [official];
62
+ }
63
+
64
+ /** Candidate URLs for a Node dist path, e.g. "v22.22.3/node-v22.22.3-darwin-arm64.tar.gz". */
65
+ function nodeDistUrls(relPath) {
66
+ return bases('OPENAGENTS_NODE_MIRRORS', OFFICIAL_NODE, CN_NODE).map(
67
+ (base) => `${base}/${relPath}`
68
+ );
69
+ }
70
+
71
+ /** Candidate URLs for an npm registry path, e.g. "@scope/pkg/latest". */
72
+ function npmUrls(relPath) {
73
+ return bases('OPENAGENTS_NPM_MIRRORS', OFFICIAL_NPM, CN_NPM).map(
74
+ (base) => `${base}/${relPath}`
75
+ );
76
+ }
77
+
78
+ /** Registry base for `npm install --registry`. */
79
+ function npmRegistry() {
80
+ return process.env.npm_config_registry || npmUrls('').slice(0, 1)[0].replace(/\/$/, '');
81
+ }
82
+
83
+ /** A registry the user (or the launcher) has already chosen, if any. */
84
+ function userConfiguredRegistry() {
85
+ if (process.env.npm_config_registry) return process.env.npm_config_registry;
86
+ try {
87
+ const fs = require('fs');
88
+ const os = require('os');
89
+ const path = require('path');
90
+ const rc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf-8');
91
+ const match = /^\s*registry\s*=\s*(\S+)/m.exec(rc);
92
+ if (match) return match[1];
93
+ } catch {}
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Registry to force via `--registry` when installing an agent runtime, or null
99
+ * to leave npm's own resolution alone.
100
+ *
101
+ * Agent runtimes are the biggest download of a first run (a CLI plus its whole
102
+ * dependency tree), and they install through a child npm process where our own
103
+ * mirror logic can't reach — the flag is the only lever. It stays null unless
104
+ * we'd actually be helping: an explicit npm_config_registry or a registry in
105
+ * the user's .npmrc (corporate proxy, private mirror) is honoured as-is.
106
+ */
107
+ function installRegistry() {
108
+ if (userConfiguredRegistry()) return null;
109
+ return inChina() ? CN_NPM[0] : null;
110
+ }
111
+
112
+ module.exports = {
113
+ nodeDistUrls,
114
+ npmUrls,
115
+ npmRegistry,
116
+ installRegistry,
117
+ userConfiguredRegistry,
118
+ inChina,
119
+ };
@@ -14,6 +14,7 @@ const os = require('os');
14
14
  const path = require('path');
15
15
  const readline = require('readline');
16
16
  const { execSync, spawnSync } = require('child_process');
17
+ const { npmUrls } = require('./mirrors');
17
18
 
18
19
  const PKG_NAME = '@openagents-org/agent-launcher';
19
20
  const CACHE_FILE = path.join(os.homedir(), '.openagents', '.update-check.json');
@@ -51,10 +52,10 @@ function saveCache(latest) {
51
52
  } catch {}
52
53
  }
53
54
 
54
- function fetchLatest(timeoutMs = 2500) {
55
+ function fetchLatestFrom(url, timeoutMs) {
55
56
  return new Promise((resolve) => {
56
57
  const req = https.request(
57
- `https://registry.npmjs.org/${PKG_NAME}/latest`,
58
+ url,
58
59
  { method: 'GET', timeout: timeoutMs, headers: { Accept: 'application/json' } },
59
60
  (res) => {
60
61
  let body = '';
@@ -73,6 +74,21 @@ function fetchLatest(timeoutMs = 2500) {
73
74
  });
74
75
  }
75
76
 
77
+ /**
78
+ * Ask each registry in turn (mirror first in China) with a per-origin budget,
79
+ * so the check either answers quickly or gives up quietly — it must never be
80
+ * what makes the CLI feel slow to start.
81
+ */
82
+ async function fetchLatest(timeoutMs = 2500) {
83
+ const urls = npmUrls(`${PKG_NAME}/latest`);
84
+ const perOrigin = Math.max(1200, Math.floor(timeoutMs / urls.length));
85
+ for (const url of urls) {
86
+ const version = await fetchLatestFrom(url, perOrigin);
87
+ if (version) return version;
88
+ }
89
+ return null;
90
+ }
91
+
76
92
  /**
77
93
  * Return { current, latest, isNewer } or null if the check failed / offline.
78
94
  */