colana 1.0.0-beta.65 → 1.0.0-beta.67
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 +1 -1
- package/server/auto-installer.js +146 -80
- package/server/preflight.js +8 -9
- package/server/pty-manager.js +37 -3
package/package.json
CHANGED
package/server/auto-installer.js
CHANGED
|
@@ -31,107 +31,155 @@ const MODULE = 'auto-installer';
|
|
|
31
31
|
* standard binary directories and adds them to the running process's PATH,
|
|
32
32
|
* allowing subsequent commandExistsSync / PTY spawns to find newly installed CLIs.
|
|
33
33
|
*
|
|
34
|
+
* Windows: npm global dirs, Python scripts dirs, pipx/uv bin dirs, MS Store paths.
|
|
35
|
+
* macOS: /opt/homebrew/bin (Apple Silicon), /usr/local/bin, ~/.local/bin, npm global.
|
|
36
|
+
* Linux: /usr/local/bin, ~/.local/bin, ~/.cargo/bin, npm global.
|
|
37
|
+
*
|
|
34
38
|
* Safe to call multiple times — skips directories already in PATH.
|
|
35
|
-
* Safe to call on non-Windows — returns immediately.
|
|
36
39
|
*/
|
|
37
40
|
export function refreshPathAfterInstall() {
|
|
38
|
-
|
|
41
|
+
const isWin = process.platform === 'win32';
|
|
42
|
+
const separator = isWin ? ';' : ':';
|
|
39
43
|
const additions = [];
|
|
40
|
-
const currentPath =
|
|
44
|
+
const currentPath = process.env.PATH || '';
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* Helper: add a directory to PATH if it exists and isn't already present.
|
|
48
|
+
* Windows: case-insensitive substring check (existing behaviour).
|
|
49
|
+
* Unix: exact segment match (prevents /usr/local/bin matching /usr/local/bin2).
|
|
44
50
|
* @param {string|undefined} dir
|
|
45
51
|
*/
|
|
46
52
|
function addIfMissing(dir) {
|
|
47
53
|
if (!dir) return;
|
|
48
54
|
const trimmed = dir.trim();
|
|
49
|
-
if (
|
|
50
|
-
|
|
55
|
+
if (!trimmed) return;
|
|
56
|
+
if (isWin) {
|
|
57
|
+
if (!currentPath.toLowerCase().includes(trimmed.toLowerCase())) {
|
|
58
|
+
additions.push(trimmed);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
const segments = currentPath.split(':');
|
|
62
|
+
if (!segments.includes(trimmed)) {
|
|
63
|
+
additions.push(trimmed);
|
|
64
|
+
}
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let npmPrefix = null;
|
|
57
|
-
try {
|
|
58
|
-
npmPrefix = execFileSync('npm', ['config', 'get', 'prefix'], {
|
|
59
|
-
timeout: 5000, encoding: 'utf-8', shell: true,
|
|
60
|
-
}).trim();
|
|
61
|
-
addIfMissing(npmPrefix);
|
|
62
|
-
} catch { /* npm might not be installed */ }
|
|
63
|
-
|
|
64
|
-
// 2. Standard %APPDATA%\npm — the default npm global bin dir on Windows.
|
|
65
|
-
// Some Node.js installs (especially MS Store) don't add this to system PATH.
|
|
66
|
-
// This is a hard-coded fallback that covers the most common case.
|
|
67
|
-
const appData = process.env.APPDATA;
|
|
68
|
-
if (appData) {
|
|
69
|
-
addIfMissing(`${appData}\\npm`);
|
|
70
|
-
}
|
|
68
|
+
if (isWin) {
|
|
69
|
+
// ——— Windows-specific directories ———
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// 1. npm global bin directory via `npm config get prefix`
|
|
72
|
+
// On Windows, global .cmd wrappers live directly in the prefix dir.
|
|
73
|
+
try {
|
|
74
|
+
const npmPrefix = execFileSync('npm', ['config', 'get', 'prefix'], {
|
|
75
|
+
timeout: 5000, encoding: 'utf-8', shell: true,
|
|
76
|
+
}).trim();
|
|
77
|
+
addIfMissing(npmPrefix);
|
|
78
|
+
} catch { /* npm might not be installed */ }
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
], { timeout: 5000, encoding: 'utf-8', shell: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
84
|
-
addIfMissing(pyScripts);
|
|
85
|
-
} catch { /* python might not be installed */ }
|
|
80
|
+
// 2. Standard %APPDATA%\npm — the default npm global bin dir on Windows.
|
|
81
|
+
const appData = process.env.APPDATA;
|
|
82
|
+
if (appData) {
|
|
83
|
+
addIfMissing(`${appData}\\npm`);
|
|
84
|
+
}
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
addIfMissing(sysScripts);
|
|
93
|
-
} catch { /* ignore */ }
|
|
94
|
-
|
|
95
|
-
// 6. pipx default bin directory (%USERPROFILE%\.local\bin on Windows)
|
|
96
|
-
const userProfile = process.env.USERPROFILE;
|
|
97
|
-
if (userProfile) {
|
|
98
|
-
addIfMissing(`${userProfile}\\.local\\bin`);
|
|
99
|
-
}
|
|
86
|
+
// 3. %LOCALAPPDATA%\npm — alternate npm global location on some setups
|
|
87
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
88
|
+
if (localAppData) {
|
|
89
|
+
addIfMissing(`${localAppData}\\npm`);
|
|
90
|
+
}
|
|
100
91
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
// 4. Python user scripts directory (for pip --user installs like aider)
|
|
93
|
+
try {
|
|
94
|
+
const pyScripts = execFileSync('python', [
|
|
95
|
+
'-c', "import sysconfig; print(sysconfig.get_path('scripts', 'nt_user'))",
|
|
96
|
+
], { timeout: 5000, encoding: 'utf-8', shell: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
97
|
+
addIfMissing(pyScripts);
|
|
98
|
+
} catch { /* python might not be installed */ }
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
timeout: 5000, encoding: 'utf-8', shell: true,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const msStoreScripts = execFileSync('python', [
|
|
120
|
-
'-c', "import site, os; print(os.path.normpath(os.path.join(site.getusersitepackages(), '..', 'Scripts')))",
|
|
121
|
-
], { timeout: 5000, encoding: 'utf-8', shell: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
122
|
-
addIfMissing(msStoreScripts);
|
|
123
|
-
} catch { /* python might not be MS Store or not installed */ }
|
|
100
|
+
// 5. Python system scripts directory
|
|
101
|
+
try {
|
|
102
|
+
const sysScripts = execFileSync('python', [
|
|
103
|
+
'-c', "import sysconfig; print(sysconfig.get_path('scripts'))",
|
|
104
|
+
], { timeout: 5000, encoding: 'utf-8', shell: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
105
|
+
addIfMissing(sysScripts);
|
|
106
|
+
} catch { /* ignore */ }
|
|
107
|
+
|
|
108
|
+
// 6. pipx default bin directory (%USERPROFILE%\.local\bin on Windows)
|
|
109
|
+
const userProfile = process.env.USERPROFILE;
|
|
110
|
+
if (userProfile) {
|
|
111
|
+
addIfMissing(`${userProfile}\\.local\\bin`);
|
|
112
|
+
}
|
|
124
113
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
114
|
+
// 7. uv tool bin directory
|
|
115
|
+
if (appData) {
|
|
116
|
+
addIfMissing(`${appData}\\uv\\bin`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 8. npm global bin directory via `npm bin -g`
|
|
120
|
+
try {
|
|
121
|
+
const npmBin = execFileSync('npm', ['bin', '-g'], {
|
|
122
|
+
timeout: 5000, encoding: 'utf-8', shell: true,
|
|
123
|
+
}).trim();
|
|
124
|
+
addIfMissing(npmBin);
|
|
125
|
+
} catch { /* ignore */ }
|
|
126
|
+
|
|
127
|
+
// 9. MS Store Python user scripts
|
|
128
|
+
try {
|
|
129
|
+
const msStoreScripts = execFileSync('python', [
|
|
130
|
+
'-c', "import site, os; print(os.path.normpath(os.path.join(site.getusersitepackages(), '..', 'Scripts')))",
|
|
131
|
+
], { timeout: 5000, encoding: 'utf-8', shell: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
132
|
+
addIfMissing(msStoreScripts);
|
|
133
|
+
} catch { /* ignore */ }
|
|
134
|
+
|
|
135
|
+
// 10. Python base Scripts dir (next to python.exe)
|
|
136
|
+
try {
|
|
137
|
+
const pyBaseScripts = execFileSync('python', [
|
|
138
|
+
'-c', "import sys, os; print(os.path.join(os.path.dirname(sys.executable), 'Scripts'))",
|
|
139
|
+
], { timeout: 5000, encoding: 'utf-8', shell: true, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
140
|
+
addIfMissing(pyBaseScripts);
|
|
141
|
+
} catch { /* ignore */ }
|
|
142
|
+
|
|
143
|
+
} else {
|
|
144
|
+
// ——— macOS + Linux directories ———
|
|
145
|
+
const home = os.homedir();
|
|
146
|
+
|
|
147
|
+
// 1. Homebrew (Apple Silicon macOS installs to /opt/homebrew)
|
|
148
|
+
if (process.platform === 'darwin') {
|
|
149
|
+
addIfMissing('/opt/homebrew/bin');
|
|
150
|
+
addIfMissing('/opt/homebrew/sbin');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. /usr/local/bin (Homebrew Intel macOS + manual installs on Linux)
|
|
154
|
+
addIfMissing('/usr/local/bin');
|
|
155
|
+
|
|
156
|
+
// 3. ~/.local/bin (pipx, uv, user-installed Python tools)
|
|
157
|
+
addIfMissing(`${home}/.local/bin`);
|
|
158
|
+
|
|
159
|
+
// 4. ~/.cargo/bin (Rust-based tools)
|
|
160
|
+
addIfMissing(`${home}/.cargo/bin`);
|
|
161
|
+
|
|
162
|
+
// 5. npm global bin directory (handles nvm, volta, fnm, Homebrew node)
|
|
163
|
+
try {
|
|
164
|
+
const npmBin = execFileSync('npm', ['bin', '-g'], {
|
|
165
|
+
timeout: 5000, encoding: 'utf-8',
|
|
166
|
+
}).trim();
|
|
167
|
+
addIfMissing(npmBin);
|
|
168
|
+
} catch { /* npm might not be installed */ }
|
|
169
|
+
|
|
170
|
+
// 6. npm prefix/bin (alternative layout via nvm, volta, fnm)
|
|
171
|
+
try {
|
|
172
|
+
const npmPrefix = execFileSync('npm', ['config', 'get', 'prefix'], {
|
|
173
|
+
timeout: 5000, encoding: 'utf-8',
|
|
174
|
+
}).trim();
|
|
175
|
+
if (npmPrefix) {
|
|
176
|
+
addIfMissing(`${npmPrefix}/bin`);
|
|
177
|
+
}
|
|
178
|
+
} catch { /* ignore */ }
|
|
179
|
+
}
|
|
132
180
|
|
|
133
181
|
if (additions.length > 0) {
|
|
134
|
-
process.env.PATH = additions.join(
|
|
182
|
+
process.env.PATH = additions.join(separator) + separator + process.env.PATH;
|
|
135
183
|
logger.info(MODULE, 'Refreshed PATH', { added: additions });
|
|
136
184
|
}
|
|
137
185
|
}
|
|
@@ -937,7 +985,25 @@ async function runInstallCommand(provider, providerConfig, progress) {
|
|
|
937
985
|
}
|
|
938
986
|
}
|
|
939
987
|
|
|
940
|
-
// Fallback 1:
|
|
988
|
+
// Fallback 1: Homebrew (macOS or Linuxbrew) — more reliable than pip on Python 3.14+
|
|
989
|
+
if (!hasUv && process.platform !== 'win32') {
|
|
990
|
+
try {
|
|
991
|
+
const brewExists = await commandExistsAsync('brew').then(() => true).catch(() => false);
|
|
992
|
+
if (brewExists) {
|
|
993
|
+
progress.log('Trying: brew install uv (Homebrew)...');
|
|
994
|
+
await runShellCommand('brew', ['install', 'uv'], progress, 180_000);
|
|
995
|
+
refreshPathAfterInstall();
|
|
996
|
+
hasUv = await commandExistsAsync('uv').then(() => true).catch(() => false);
|
|
997
|
+
if (hasUv) {
|
|
998
|
+
progress.log('uv installed via Homebrew.');
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
progress.log('brew install uv failed or Homebrew not available.');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Fallback 2: invoke uv via `python -m uv` — bypasses PATH entirely
|
|
941
1007
|
// since Python can find its own installed modules regardless of Scripts dir.
|
|
942
1008
|
if (!hasUv) {
|
|
943
1009
|
progress.log('Trying python -m uv (bypasses PATH)...');
|
|
@@ -950,7 +1016,7 @@ async function runInstallCommand(provider, providerConfig, progress) {
|
|
|
950
1016
|
}
|
|
951
1017
|
}
|
|
952
1018
|
|
|
953
|
-
// Fallback
|
|
1019
|
+
// Fallback 3: direct binary search in known Python Scripts dirs (Windows only).
|
|
954
1020
|
// Uses semicolons (not newlines) for Windows cmd.exe compatibility.
|
|
955
1021
|
if (!hasUv && process.platform === 'win32') {
|
|
956
1022
|
progress.log('Searching for uv binary in Python Scripts directories...');
|
package/server/preflight.js
CHANGED
|
@@ -130,15 +130,14 @@ function checkBinaryInstalled(provider, providerConfig) {
|
|
|
130
130
|
return { ok: true };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// Recovery:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
133
|
+
// Recovery: PATH may be stale if a CLI was installed after the server started
|
|
134
|
+
// (e.g. via wizard). Refresh from common tool directories and retry once.
|
|
135
|
+
refreshPathAfterInstall();
|
|
136
|
+
if (commandExistsSync(binary)) {
|
|
137
|
+
return { ok: true };
|
|
138
|
+
}
|
|
139
|
+
if (fallbackBinary && commandExistsSync(fallbackBinary)) {
|
|
140
|
+
return { ok: true };
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
return {
|
package/server/pty-manager.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { getAgentEnv, getSetting, resolveSessionAuthMode } from './settings-store.js';
|
|
22
22
|
import { recordUsageWithSource } from './usage-tracker.js';
|
|
23
23
|
import config from './config.js';
|
|
24
|
-
import { commandExistsSync, resolveNpmGlobalEntryPoint } from './platform.js';
|
|
24
|
+
import { commandExistsSync, getCommandPath, resolveNpmGlobalEntryPoint } from './platform.js';
|
|
25
25
|
import { isGitRepo, getHeadSha, getWorkingTreeStatus } from './git-ops.js';
|
|
26
26
|
import { createCheckpoint } from './checkpoint-store.js';
|
|
27
27
|
import { syncBeforeSpawn } from './context-sync.js';
|
|
@@ -349,6 +349,10 @@ function verifyGatewayToken(_port, token) {
|
|
|
349
349
|
* Records the token, auth method, and timestamp so subsequent startups can verify.
|
|
350
350
|
*/
|
|
351
351
|
function writeGatewayMarker(token) {
|
|
352
|
+
// Cache the token in memory so subsequent sequential calls within this process
|
|
353
|
+
// can verify against it without re-reading config (which may hold a different,
|
|
354
|
+
// local token after we adopted a foreign gateway).
|
|
355
|
+
_activeGatewayToken = token;
|
|
352
356
|
try {
|
|
353
357
|
const dir = path.dirname(GATEWAY_MARKER_PATH);
|
|
354
358
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -481,6 +485,16 @@ async function killGatewayOnPort(port) {
|
|
|
481
485
|
// Concurrent callers piggyback on the in-flight promise.
|
|
482
486
|
let _gatewayEnsurePromise = null;
|
|
483
487
|
|
|
488
|
+
// In-memory cache of the last token written to the gateway marker file.
|
|
489
|
+
// After adopting a foreign gateway token, this prevents subsequent sequential
|
|
490
|
+
// calls from re-triggering the mismatch/kill/adopt cycle (because the local
|
|
491
|
+
// config token differs from the adopted marker token).
|
|
492
|
+
let _activeGatewayToken = null;
|
|
493
|
+
|
|
494
|
+
/** Test-only: get/reset _activeGatewayToken for test isolation. */
|
|
495
|
+
function _getActiveGatewayToken() { return _activeGatewayToken; }
|
|
496
|
+
function _resetActiveGatewayToken() { _activeGatewayToken = null; }
|
|
497
|
+
|
|
484
498
|
/**
|
|
485
499
|
* Ensure the OpenClaw gateway daemon is running on the given port.
|
|
486
500
|
* Uses a singleton promise so concurrent callers coalesce into one execution.
|
|
@@ -537,7 +551,10 @@ async function _ensureOpenClawGatewayImpl(port) {
|
|
|
537
551
|
// Fall through to spawn new gateway
|
|
538
552
|
} else {
|
|
539
553
|
// Gateway is listening — check if we spawned it with our current token
|
|
540
|
-
|
|
554
|
+
// Use the in-memory cached token if available (set after a previous adoption
|
|
555
|
+
// in this process), otherwise fall back to the config-derived token.
|
|
556
|
+
const verifyToken = _activeGatewayToken || gatewayToken;
|
|
557
|
+
const tokenValid = verifyGatewayToken(port, verifyToken);
|
|
541
558
|
if (tokenValid) {
|
|
542
559
|
console.log('[pty-manager] OpenClaw gateway already running, token valid (marker match)');
|
|
543
560
|
return;
|
|
@@ -856,6 +873,17 @@ export async function startPtyAgent(projectId, projectPath, task, options = {})
|
|
|
856
873
|
}
|
|
857
874
|
}
|
|
858
875
|
|
|
876
|
+
// On macOS/Linux, resolve the binary to a full path. node-pty's posix_spawnp
|
|
877
|
+
// uses the spawned env's PATH, which may be stale if the CLI was installed
|
|
878
|
+
// after the server started (e.g. via wizard). getCommandPath() calls `which`
|
|
879
|
+
// in a subshell that inherits the refreshed process.env.PATH.
|
|
880
|
+
if (process.platform !== 'win32') {
|
|
881
|
+
const fullPath = getCommandPath(binary);
|
|
882
|
+
if (fullPath) {
|
|
883
|
+
binary = fullPath;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
859
887
|
// 1. Create session record with mode='pty'
|
|
860
888
|
const session = createSession({
|
|
861
889
|
projectId,
|
|
@@ -2276,6 +2304,10 @@ export {
|
|
|
2276
2304
|
extractCodexSessionIdFromRolloutFile as _extractCodexSessionIdFromRolloutFile,
|
|
2277
2305
|
detectCodexSessionIdFromFilesystem as _detectCodexSessionIdFromFilesystem,
|
|
2278
2306
|
isValidProviderSessionId as _isValidProviderSessionId,
|
|
2307
|
+
writeGatewayMarker as _writeGatewayMarker,
|
|
2308
|
+
verifyGatewayToken as _verifyGatewayToken,
|
|
2309
|
+
_getActiveGatewayToken,
|
|
2310
|
+
_resetActiveGatewayToken,
|
|
2279
2311
|
};
|
|
2280
2312
|
|
|
2281
2313
|
/**
|
|
@@ -2291,6 +2323,8 @@ export async function restartOpenClawGateway() {
|
|
|
2291
2323
|
console.log('[pty-manager] Restarting OpenClaw gateway to reload MCP config...');
|
|
2292
2324
|
// Clear any in-flight singleton promise so the ensure call below runs fresh
|
|
2293
2325
|
_gatewayEnsurePromise = null;
|
|
2326
|
+
// Clear cached adopted token so the fresh ensure call re-derives from config
|
|
2327
|
+
_activeGatewayToken = null;
|
|
2294
2328
|
// Clear marker before kill so ensureOpenClawGateway knows to respawn
|
|
2295
2329
|
try { fs.unlinkSync(GATEWAY_MARKER_PATH); } catch { /* may not exist */ }
|
|
2296
2330
|
await killGatewayOnPort(gwPort);
|
|
@@ -2317,7 +2351,7 @@ startIdleCleanup();
|
|
|
2317
2351
|
* Used by personal-agent-routes to distinguish gateway-level 401 from provider-level 401.
|
|
2318
2352
|
*/
|
|
2319
2353
|
export function isGatewaySpawnedByColana() {
|
|
2320
|
-
const token = getOrCreateGatewayToken();
|
|
2354
|
+
const token = _activeGatewayToken || getOrCreateGatewayToken();
|
|
2321
2355
|
return verifyGatewayToken(null, token);
|
|
2322
2356
|
}
|
|
2323
2357
|
|