aegis-bridge 2.15.3 → 2.15.4
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/dist/cli.js +4 -0
- package/dist/hook-settings.d.ts +2 -0
- package/dist/hook-settings.js +19 -2
- package/dist/hook.d.ts +2 -1
- package/dist/hook.js +27 -3
- package/dist/server.js +65 -45
- package/dist/session.d.ts +11 -0
- package/dist/session.js +4 -0
- package/dist/shutdown-utils.d.ts +5 -0
- package/dist/shutdown-utils.js +24 -0
- package/dist/startup.d.ts +1 -0
- package/dist/startup.js +1 -0
- package/dist/tmux.d.ts +6 -0
- package/dist/tmux.js +81 -7
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import { parseIntSafe, getErrorMessage } from './validation.js';
|
|
13
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
15
|
+
/** Current aegis-bridge version read from package.json at startup. */
|
|
15
16
|
const VERSION = pkg.version;
|
|
17
|
+
/** Check whether a required external dependency can be executed. */
|
|
16
18
|
function checkDependency(name, command) {
|
|
17
19
|
try {
|
|
18
20
|
execSync(`${command} 2>/dev/null`, { stdio: 'ignore' });
|
|
@@ -22,6 +24,7 @@ function checkDependency(name, command) {
|
|
|
22
24
|
return false;
|
|
23
25
|
}
|
|
24
26
|
}
|
|
27
|
+
/** Render the startup banner shown when launching the HTTP server. */
|
|
25
28
|
function printBanner(port) {
|
|
26
29
|
console.log(`
|
|
27
30
|
┌─────────────────────────────────────────┐
|
|
@@ -107,6 +110,7 @@ async function handleCreate(args) {
|
|
|
107
110
|
console.log(` Read: curl ${baseUrl}/v1/sessions/${sessionId}/read`);
|
|
108
111
|
console.log(` Kill: curl -X DELETE ${baseUrl}/v1/sessions/${sessionId}`);
|
|
109
112
|
}
|
|
113
|
+
/** Main CLI entry point that dispatches subcommands and bootstraps the server. */
|
|
110
114
|
async function main() {
|
|
111
115
|
const args = process.argv.slice(2);
|
|
112
116
|
// Help
|
package/dist/hook-settings.d.ts
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Issue #169: Phase 2 — Inject CC settings.json with HTTP hooks.
|
|
15
15
|
*/
|
|
16
|
+
/** Build a normalized path to .claude/settings.local.json for Unix and Windows workDirs. */
|
|
17
|
+
export declare function buildProjectSettingsPath(workDir: string, platform?: NodeJS.Platform): string;
|
|
16
18
|
/** CC hook events that support `type: "http"`.
|
|
17
19
|
*
|
|
18
20
|
* All CC hook events support HTTP hooks. We register the most useful ones
|
package/dist/hook-settings.js
CHANGED
|
@@ -45,6 +45,23 @@ function normalizeHookBaseUrl(baseUrl) {
|
|
|
45
45
|
return baseUrl.replace('0.0.0.0', '127.0.0.1');
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/** Build a normalized path to .claude/settings.local.json for Unix and Windows workDirs. */
|
|
49
|
+
export function buildProjectSettingsPath(workDir, platform = process.platform) {
|
|
50
|
+
let normalizedWorkDir = platform === 'win32'
|
|
51
|
+
? workDir.replace(/\//g, '\\')
|
|
52
|
+
: workDir.replace(/\\/g, '/');
|
|
53
|
+
// On Linux, resolve() prepends CWD to Windows paths like "D:\Users\dev"
|
|
54
|
+
// because Linux doesn't understand Windows drive letters. Only resolve
|
|
55
|
+
// paths that are NOT already absolute on the target platform.
|
|
56
|
+
const isWinAbs = /^[A-Za-z]:\\/.test(normalizedWorkDir);
|
|
57
|
+
const isUnixAbs = /^\//.test(normalizedWorkDir);
|
|
58
|
+
const alreadyAbs = (platform === 'win32' && isWinAbs) || isUnixAbs;
|
|
59
|
+
if (!alreadyAbs)
|
|
60
|
+
normalizedWorkDir = resolve(normalizedWorkDir);
|
|
61
|
+
// Normalize separators to match the target platform.
|
|
62
|
+
const result = join(normalizedWorkDir, '.claude', 'settings.local.json');
|
|
63
|
+
return platform === 'win32' ? result.replace(/\//g, '\\') : result;
|
|
64
|
+
}
|
|
48
65
|
/**
|
|
49
66
|
* Validate a workDir path for use in hook settings resolution.
|
|
50
67
|
* Defense-in-depth against path traversal: rejects paths containing ".." segments
|
|
@@ -145,7 +162,7 @@ export async function writeHookSettingsFile(baseUrl, sessionId, hookSecret, work
|
|
|
145
162
|
let merged = {};
|
|
146
163
|
const safeWorkDir = workDir ? validateWorkDirPath(workDir) : undefined;
|
|
147
164
|
if (safeWorkDir) {
|
|
148
|
-
const projectSettingsPath =
|
|
165
|
+
const projectSettingsPath = buildProjectSettingsPath(safeWorkDir);
|
|
149
166
|
if (existsSync(projectSettingsPath)) {
|
|
150
167
|
try {
|
|
151
168
|
const raw = await readFile(projectSettingsPath, 'utf-8');
|
|
@@ -210,7 +227,7 @@ export async function cleanupStaleSessionHooks(workDir, activeSessionIds) {
|
|
|
210
227
|
const safeWorkDir = workDir ? validateWorkDirPath(workDir) : undefined;
|
|
211
228
|
if (!safeWorkDir)
|
|
212
229
|
return;
|
|
213
|
-
const projectSettingsPath =
|
|
230
|
+
const projectSettingsPath = buildProjectSettingsPath(safeWorkDir);
|
|
214
231
|
if (!existsSync(projectSettingsPath))
|
|
215
232
|
return;
|
|
216
233
|
try {
|
package/dist/hook.d.ts
CHANGED
package/dist/hook.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* }
|
|
17
17
|
*/
|
|
18
18
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
|
|
19
|
-
import { join, dirname } from 'node:path';
|
|
19
|
+
import { join, dirname, resolve } from 'node:path';
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { execFileSync } from 'node:child_process';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
@@ -32,6 +32,17 @@ const MAP_FILE = join(BRIDGE_DIR, 'session_map.json');
|
|
|
32
32
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
33
33
|
const TMUX_PANE_RE = /^%\d+$/;
|
|
34
34
|
const DEFAULT_POINTER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
function normalizeCommandPath(pathValue, platform = process.platform) {
|
|
36
|
+
return platform === 'win32' ? pathValue.replace(/\//g, '\\') : pathValue.replace(/\\/g, '/');
|
|
37
|
+
}
|
|
38
|
+
function quoteCommandPath(pathValue, platform = process.platform) {
|
|
39
|
+
const normalized = normalizeCommandPath(pathValue, platform);
|
|
40
|
+
return `"${normalized.replace(/"/g, '\\"')}"`;
|
|
41
|
+
}
|
|
42
|
+
/** Build a shell-safe command string that invokes hook.js with an explicit Node executable. */
|
|
43
|
+
export function buildHookCommand(scriptPath, nodeExecutable = process.execPath, platform = process.platform) {
|
|
44
|
+
return `${quoteCommandPath(nodeExecutable, platform)} ${quoteCommandPath(scriptPath, platform)}`;
|
|
45
|
+
}
|
|
35
46
|
function getPointerTtlMs() {
|
|
36
47
|
const raw = process.env.AEGIS_CONTINUATION_POINTER_TTL_MS ?? process.env.MANUS_CONTINUATION_POINTER_TTL_MS;
|
|
37
48
|
const parsed = raw ? Number(raw) : NaN;
|
|
@@ -176,7 +187,7 @@ function install() {
|
|
|
176
187
|
}
|
|
177
188
|
settings = parsed.data;
|
|
178
189
|
}
|
|
179
|
-
const hookCommand =
|
|
190
|
+
const hookCommand = buildHookCommand(join(__dirname, 'hook.js'));
|
|
180
191
|
const hooks = (settings.hooks || {});
|
|
181
192
|
const sessionStart = (hooks.SessionStart || []);
|
|
182
193
|
// Check if already installed
|
|
@@ -204,4 +215,17 @@ function install() {
|
|
|
204
215
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
205
216
|
console.log(`Aegis hook installed in ${settingsPath}`);
|
|
206
217
|
}
|
|
207
|
-
|
|
218
|
+
const isDirectExecution = (() => {
|
|
219
|
+
const argv1 = process.argv[1];
|
|
220
|
+
if (!argv1)
|
|
221
|
+
return false;
|
|
222
|
+
try {
|
|
223
|
+
return resolve(argv1) === resolve(__filename);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
})();
|
|
229
|
+
if (isDirectExecution) {
|
|
230
|
+
main();
|
|
231
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -49,6 +49,7 @@ import { MemoryBridge } from './memory-bridge.js';
|
|
|
49
49
|
import { cleanupTerminatedSessionState } from './session-cleanup.js';
|
|
50
50
|
import { normalizeApiErrorPayload } from './api-error-envelope.js';
|
|
51
51
|
import { listenWithRetry, removePidFile, writePidFile } from './startup.js';
|
|
52
|
+
import { isWindowsShutdownMessage, parseShutdownTimeoutMs } from './shutdown-utils.js';
|
|
52
53
|
import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, compareSemver, extractCCVersion, MIN_CC_VERSION, permissionProfileSchema, } from './validation.js';
|
|
53
54
|
const __filename = fileURLToPath(import.meta.url);
|
|
54
55
|
const __dirname = path.dirname(__filename);
|
|
@@ -1765,56 +1766,67 @@ async function main() {
|
|
|
1765
1766
|
// Issue #361: Graceful shutdown handler
|
|
1766
1767
|
// Issue #415: Reentrance guard at handler level prevents double execution on rapid SIGINT
|
|
1767
1768
|
let shuttingDown = false;
|
|
1769
|
+
const shutdownTimeoutMs = parseShutdownTimeoutMs(process.env.AEGIS_SHUTDOWN_TIMEOUT_MS);
|
|
1768
1770
|
async function gracefulShutdown(signal) {
|
|
1769
1771
|
console.log(`${signal} received, shutting down gracefully...`);
|
|
1770
|
-
|
|
1772
|
+
const forceExitTimer = setTimeout(() => {
|
|
1773
|
+
console.error(`Graceful shutdown timed out after ${shutdownTimeoutMs}ms — forcing process exit`);
|
|
1774
|
+
process.exit(1);
|
|
1775
|
+
}, shutdownTimeoutMs);
|
|
1776
|
+
forceExitTimer.unref?.();
|
|
1771
1777
|
try {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1778
|
+
// 1. Stop accepting new requests
|
|
1779
|
+
try {
|
|
1780
|
+
await app.close();
|
|
1781
|
+
}
|
|
1782
|
+
catch (e) {
|
|
1783
|
+
console.error('Error closing server:', e);
|
|
1784
|
+
}
|
|
1785
|
+
// 2. Stop background monitors and intervals
|
|
1786
|
+
monitor.stop();
|
|
1787
|
+
swarmMonitor.stop();
|
|
1788
|
+
clearInterval(reaperInterval);
|
|
1789
|
+
clearInterval(zombieReaperInterval);
|
|
1790
|
+
clearInterval(metricsSaveInterval);
|
|
1791
|
+
clearInterval(ipPruneInterval);
|
|
1792
|
+
clearInterval(authFailPruneInterval);
|
|
1793
|
+
clearInterval(authSweepInterval);
|
|
1794
|
+
// Issue #569: Kill all CC sessions and tmux windows before exit
|
|
1795
|
+
try {
|
|
1796
|
+
await killAllSessions(sessions, tmux);
|
|
1797
|
+
}
|
|
1798
|
+
catch (e) {
|
|
1799
|
+
console.error('Error killing sessions:', e);
|
|
1800
|
+
}
|
|
1801
|
+
// 3. Destroy channels (awaits Telegram poll loop)
|
|
1802
|
+
try {
|
|
1803
|
+
await channels.destroy();
|
|
1804
|
+
}
|
|
1805
|
+
catch (e) {
|
|
1806
|
+
console.error('Error destroying channels:', e);
|
|
1807
|
+
}
|
|
1808
|
+
// 4. Save session state
|
|
1809
|
+
try {
|
|
1810
|
+
await sessions.save();
|
|
1811
|
+
}
|
|
1812
|
+
catch (e) {
|
|
1813
|
+
console.error('Error saving sessions:', e);
|
|
1814
|
+
}
|
|
1815
|
+
// 5. Save metrics
|
|
1816
|
+
try {
|
|
1817
|
+
await metrics.save();
|
|
1818
|
+
}
|
|
1819
|
+
catch (e) {
|
|
1820
|
+
console.error('Error saving metrics:', e);
|
|
1821
|
+
}
|
|
1822
|
+
// 6. Cleanup PID file
|
|
1823
|
+
removePidFile(pidFilePath);
|
|
1824
|
+
console.log('Graceful shutdown complete');
|
|
1825
|
+
process.exit(0);
|
|
1810
1826
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1827
|
+
finally {
|
|
1828
|
+
clearTimeout(forceExitTimer);
|
|
1813
1829
|
}
|
|
1814
|
-
// 6. Cleanup PID file
|
|
1815
|
-
removePidFile(pidFilePath);
|
|
1816
|
-
console.log('Graceful shutdown complete');
|
|
1817
|
-
process.exit(0);
|
|
1818
1830
|
}
|
|
1819
1831
|
process.on('SIGTERM', () => { if (!shuttingDown) {
|
|
1820
1832
|
shuttingDown = true;
|
|
@@ -1824,6 +1836,14 @@ async function main() {
|
|
|
1824
1836
|
shuttingDown = true;
|
|
1825
1837
|
void gracefulShutdown('SIGINT');
|
|
1826
1838
|
} });
|
|
1839
|
+
if (process.platform === 'win32') {
|
|
1840
|
+
process.on('message', (message) => {
|
|
1841
|
+
if (!shuttingDown && isWindowsShutdownMessage(message)) {
|
|
1842
|
+
shuttingDown = true;
|
|
1843
|
+
void gracefulShutdown('WINMSG');
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1827
1847
|
process.on('unhandledRejection', (reason) => {
|
|
1828
1848
|
console.error('unhandledRejection:', reason);
|
|
1829
1849
|
});
|
package/dist/session.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { type UIState } from './terminal-parser.js';
|
|
|
10
10
|
import type { Config } from './config.js';
|
|
11
11
|
import { type PermissionPolicy, type PermissionProfile } from './validation.js';
|
|
12
12
|
import { type PermissionDecision } from './permission-request-manager.js';
|
|
13
|
+
/**
|
|
14
|
+
* Canonical runtime metadata for an Aegis-managed Claude Code session.
|
|
15
|
+
*
|
|
16
|
+
* This structure is persisted to disk and reused by the REST API, SSE layer,
|
|
17
|
+
* monitoring loop, and session recovery logic.
|
|
18
|
+
*/
|
|
13
19
|
export interface SessionInfo {
|
|
14
20
|
id: string;
|
|
15
21
|
windowId: string;
|
|
@@ -43,6 +49,7 @@ export interface SessionInfo {
|
|
|
43
49
|
permissionProfile?: PermissionProfile;
|
|
44
50
|
prd?: string;
|
|
45
51
|
}
|
|
52
|
+
/** Persisted session store keyed by Aegis session ID. */
|
|
46
53
|
export interface SessionState {
|
|
47
54
|
sessions: Record<string, SessionInfo>;
|
|
48
55
|
}
|
|
@@ -57,6 +64,10 @@ export interface SessionState {
|
|
|
57
64
|
export declare function detectApprovalMethod(paneText: string): 'numbered' | 'yes';
|
|
58
65
|
/** Resolves a pending PermissionRequest hook with a decision. */
|
|
59
66
|
export type { PermissionDecision };
|
|
67
|
+
/**
|
|
68
|
+
* Coordinates session lifecycle, persistence, transcript discovery, and
|
|
69
|
+
* interactive approval/question flows for all managed Claude Code sessions.
|
|
70
|
+
*/
|
|
60
71
|
export declare class SessionManager {
|
|
61
72
|
private tmux;
|
|
62
73
|
private config;
|
package/dist/session.js
CHANGED
|
@@ -52,6 +52,10 @@ export function detectApprovalMethod(paneText) {
|
|
|
52
52
|
}
|
|
53
53
|
return 'yes';
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Coordinates session lifecycle, persistence, transcript discovery, and
|
|
57
|
+
* interactive approval/question flows for all managed Claude Code sessions.
|
|
58
|
+
*/
|
|
55
59
|
export class SessionManager {
|
|
56
60
|
tmux;
|
|
57
61
|
config;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shutdown-utils.ts — reusable shutdown helpers for server signal handling.
|
|
3
|
+
*/
|
|
4
|
+
export declare function parseShutdownTimeoutMs(rawValue: string | undefined, fallbackMs?: number): number;
|
|
5
|
+
export declare function isWindowsShutdownMessage(message: unknown): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shutdown-utils.ts — reusable shutdown helpers for server signal handling.
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 15_000;
|
|
5
|
+
export function parseShutdownTimeoutMs(rawValue, fallbackMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
|
|
6
|
+
const parsed = Number(rawValue);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed < 1_000)
|
|
8
|
+
return fallbackMs;
|
|
9
|
+
return Math.floor(parsed);
|
|
10
|
+
}
|
|
11
|
+
export function isWindowsShutdownMessage(message) {
|
|
12
|
+
if (typeof message === 'string') {
|
|
13
|
+
const normalized = message.trim().toLowerCase();
|
|
14
|
+
return normalized === 'shutdown' || normalized === 'graceful-shutdown';
|
|
15
|
+
}
|
|
16
|
+
if (typeof message === 'object' && message !== null && 'type' in message) {
|
|
17
|
+
const typeValue = message.type;
|
|
18
|
+
if (typeof typeValue === 'string') {
|
|
19
|
+
const normalized = typeValue.trim().toLowerCase();
|
|
20
|
+
return normalized === 'shutdown' || normalized === 'graceful-shutdown';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
package/dist/startup.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
2
|
export declare function writePidFile(stateDir: string): string;
|
|
3
3
|
export declare function removePidFile(pidFilePath: string): void;
|
|
4
|
+
/** Read the parent PID for a Linux process from /proc. */
|
|
4
5
|
export declare function readPpid(pid: number): number;
|
|
5
6
|
export declare function listenWithRetry(app: ReturnType<typeof Fastify>, port: number, host: string, stateDir: string, maxRetries?: number): Promise<void>;
|
package/dist/startup.js
CHANGED
|
@@ -41,6 +41,7 @@ function pidExists(pid) {
|
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
/** Read the parent PID for a Linux process from /proc. */
|
|
44
45
|
export function readPpid(pid) {
|
|
45
46
|
const status = readFileSync(`/proc/${pid}/status`, 'utf-8');
|
|
46
47
|
const match = status.match(/^PPid:\s+(\d+)/m);
|
package/dist/tmux.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Wraps tmux CLI commands to manage windows inside a named session.
|
|
5
5
|
* Port of CCBot's tmux_manager.py to TypeScript.
|
|
6
6
|
*/
|
|
7
|
+
/** Build the platform-specific launch wrapper that clears inherited tmux vars. */
|
|
8
|
+
export declare function buildClaudeLaunchCommand(baseCommand: string, platform?: NodeJS.Platform): string;
|
|
7
9
|
/** Thrown when a tmux command exceeds its timeout. */
|
|
8
10
|
export declare class TmuxTimeoutError extends Error {
|
|
9
11
|
constructor(args: string[], timeoutMs: number);
|
|
@@ -89,10 +91,14 @@ export declare class TmuxManager {
|
|
|
89
91
|
* Values never appear in terminal scrollback or capture-pane output.
|
|
90
92
|
*/
|
|
91
93
|
private setEnvSecure;
|
|
94
|
+
/** #909: Windows variant — set tmux env and dot-source a temp .ps1 in the active pane. */
|
|
95
|
+
private setEnvSecureWin32;
|
|
92
96
|
/** #837: Direct variant of setEnvSecure that uses sendKeysDirectInternal instead of
|
|
93
97
|
* sendKeys, safe to call from inside a serialize() callback without deadlocking.
|
|
94
98
|
* Identical logic otherwise. */
|
|
95
99
|
private setEnvSecureDirect;
|
|
100
|
+
/** #909: Direct Windows variant that avoids serialize() re-entry. */
|
|
101
|
+
private setEnvSecureDirectWin32;
|
|
96
102
|
/** P1 fix: Check if a window exists. Returns true if window is in the session.
|
|
97
103
|
* #357: Uses a short-lived cache to avoid repeated tmux CLI calls. */
|
|
98
104
|
windowExists(windowId: string): Promise<boolean>;
|
package/dist/tmux.js
CHANGED
|
@@ -16,6 +16,16 @@ import { computeProjectHash } from './path-utils.js';
|
|
|
16
16
|
function shellEscape(s) {
|
|
17
17
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
18
18
|
}
|
|
19
|
+
function powerShellSingleQuote(value) {
|
|
20
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
21
|
+
}
|
|
22
|
+
/** Build the platform-specific launch wrapper that clears inherited tmux vars. */
|
|
23
|
+
export function buildClaudeLaunchCommand(baseCommand, platform = process.platform) {
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
return `Remove-Item Env:TMUX -ErrorAction SilentlyContinue; Remove-Item Env:TMUX_PANE -ErrorAction SilentlyContinue; ${baseCommand}`;
|
|
26
|
+
}
|
|
27
|
+
return `unset TMUX TMUX_PANE && exec ${baseCommand}`;
|
|
28
|
+
}
|
|
19
29
|
/** Validate that an env var key contains only safe characters (Issue #630: uppercase only, aligned with session.ts). */
|
|
20
30
|
const ENV_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
21
31
|
const execFileAsync = promisify(execFile);
|
|
@@ -309,13 +319,9 @@ export class TmuxManager {
|
|
|
309
319
|
if (existsSync(settingsPath)) {
|
|
310
320
|
cmd += ` --settings ${shellEscape(settingsPath)}`;
|
|
311
321
|
}
|
|
312
|
-
// Issue #68:
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
// - Color capabilities reduced to 256
|
|
316
|
-
// - Clipboard passthrough via tmux load-buffer instead of OSC 52
|
|
317
|
-
// Prefixing with 'unset' ensures CC gets a clean environment.
|
|
318
|
-
cmd = `unset TMUX TMUX_PANE && exec ${cmd}`;
|
|
322
|
+
// Issue #68 / #909: Clear inherited tmux vars before launching CC.
|
|
323
|
+
// Linux/macOS uses `unset`; Windows uses PowerShell env removal.
|
|
324
|
+
cmd = buildClaudeLaunchCommand(cmd);
|
|
319
325
|
// Send the command to start Claude
|
|
320
326
|
await this.sendKeys(windowId, cmd, true);
|
|
321
327
|
// Issue #7: Verify Claude process started by checking pane command.
|
|
@@ -393,6 +399,10 @@ export class TmuxManager {
|
|
|
393
399
|
* Values never appear in terminal scrollback or capture-pane output.
|
|
394
400
|
*/
|
|
395
401
|
async setEnvSecure(windowId, env) {
|
|
402
|
+
if (process.platform === 'win32') {
|
|
403
|
+
await this.setEnvSecureWin32(windowId, env);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
396
406
|
const fs = await import('node:fs/promises');
|
|
397
407
|
const path = await import('node:path');
|
|
398
408
|
// Validate env var keys before interpolation
|
|
@@ -428,10 +438,44 @@ export class TmuxManager {
|
|
|
428
438
|
}
|
|
429
439
|
catch { /* already deleted by shell */ }
|
|
430
440
|
}
|
|
441
|
+
/** #909: Windows variant — set tmux env and dot-source a temp .ps1 in the active pane. */
|
|
442
|
+
async setEnvSecureWin32(windowId, env) {
|
|
443
|
+
const fs = await import('node:fs/promises');
|
|
444
|
+
const path = await import('node:path');
|
|
445
|
+
for (const key of Object.keys(env)) {
|
|
446
|
+
if (!ENV_KEY_RE.test(key)) {
|
|
447
|
+
throw new Error(`Invalid env var key: '${key}' — must match ${ENV_KEY_RE.source}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const tmpFile = path.join(tmpdir(), `.aegis-env-${randomBytes(16).toString('hex')}.ps1`);
|
|
451
|
+
const lines = Object.entries(env).map(([key, val]) => `$env:${key} = ${powerShellSingleQuote(val)}`);
|
|
452
|
+
await fs.writeFile(tmpFile, lines.join('\n') + '\n', { mode: 0o600 });
|
|
453
|
+
for (const [key, val] of Object.entries(env)) {
|
|
454
|
+
await this.tmux('set-environment', '-t', this.sessionName, key, val);
|
|
455
|
+
}
|
|
456
|
+
const psPath = powerShellSingleQuote(tmpFile);
|
|
457
|
+
const cmd = `. ${psPath}; Remove-Item -LiteralPath ${psPath} -Force -ErrorAction SilentlyContinue`;
|
|
458
|
+
await this.sendKeys(windowId, cmd, true);
|
|
459
|
+
await this.pollUntil(async () => { try {
|
|
460
|
+
await stat(tmpFile);
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return true;
|
|
465
|
+
} }, 50, 750);
|
|
466
|
+
try {
|
|
467
|
+
await fs.unlink(tmpFile);
|
|
468
|
+
}
|
|
469
|
+
catch { /* already deleted by shell */ }
|
|
470
|
+
}
|
|
431
471
|
/** #837: Direct variant of setEnvSecure that uses sendKeysDirectInternal instead of
|
|
432
472
|
* sendKeys, safe to call from inside a serialize() callback without deadlocking.
|
|
433
473
|
* Identical logic otherwise. */
|
|
434
474
|
async setEnvSecureDirect(windowId, env) {
|
|
475
|
+
if (process.platform === 'win32') {
|
|
476
|
+
await this.setEnvSecureDirectWin32(windowId, env);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
435
479
|
const fs = await import('node:fs/promises');
|
|
436
480
|
const path = await import('node:path');
|
|
437
481
|
for (const key of Object.keys(env)) {
|
|
@@ -460,6 +504,36 @@ export class TmuxManager {
|
|
|
460
504
|
}
|
|
461
505
|
catch { /* already deleted by shell */ }
|
|
462
506
|
}
|
|
507
|
+
/** #909: Direct Windows variant that avoids serialize() re-entry. */
|
|
508
|
+
async setEnvSecureDirectWin32(windowId, env) {
|
|
509
|
+
const fs = await import('node:fs/promises');
|
|
510
|
+
const path = await import('node:path');
|
|
511
|
+
for (const key of Object.keys(env)) {
|
|
512
|
+
if (!ENV_KEY_RE.test(key)) {
|
|
513
|
+
throw new Error(`Invalid env var key: '${key}' — must match ${ENV_KEY_RE.source}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const tmpFile = path.join(tmpdir(), `.aegis-env-${randomBytes(16).toString('hex')}.ps1`);
|
|
517
|
+
const lines = Object.entries(env).map(([key, val]) => `$env:${key} = ${powerShellSingleQuote(val)}`);
|
|
518
|
+
await fs.writeFile(tmpFile, lines.join('\n') + '\n', { mode: 0o600 });
|
|
519
|
+
for (const [key, val] of Object.entries(env)) {
|
|
520
|
+
await this.tmuxInternal('set-environment', '-t', this.sessionName, key, val);
|
|
521
|
+
}
|
|
522
|
+
const psPath = powerShellSingleQuote(tmpFile);
|
|
523
|
+
const cmd = `. ${psPath}; Remove-Item -LiteralPath ${psPath} -Force -ErrorAction SilentlyContinue`;
|
|
524
|
+
await this.sendKeysDirectInternal(windowId, cmd, true);
|
|
525
|
+
await this.pollUntil(async () => { try {
|
|
526
|
+
await stat(tmpFile);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return true;
|
|
531
|
+
} }, 50, 750);
|
|
532
|
+
try {
|
|
533
|
+
await fs.unlink(tmpFile);
|
|
534
|
+
}
|
|
535
|
+
catch { /* already deleted by shell */ }
|
|
536
|
+
}
|
|
463
537
|
/** P1 fix: Check if a window exists. Returns true if window is in the session.
|
|
464
538
|
* #357: Uses a short-lived cache to avoid repeated tmux CLI calls. */
|
|
465
539
|
async windowExists(windowId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-bridge",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
|
|
6
6
|
"main": "dist/server.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"build": "tsc && npm run build:copy-dashboard",
|
|
28
28
|
"build:copy-dashboard": "node scripts/copy-dashboard.mjs",
|
|
29
29
|
"build:dashboard": "cd dashboard && npm ci && npm run build",
|
|
30
|
+
"docs": "typedoc",
|
|
30
31
|
"start": "node dist/cli.js",
|
|
31
32
|
"dev": "tsc && node dist/cli.js",
|
|
32
33
|
"prepublishOnly": "npm run build:dashboard && npm run build",
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"@types/ws": "^8.18.1",
|
|
73
74
|
"lockfile-lint": "5.0.0",
|
|
74
75
|
"ts-morph": "^27.0.2",
|
|
76
|
+
"typedoc": "^0.28.18",
|
|
75
77
|
"typescript": "^6.0.2",
|
|
76
78
|
"vitest": "^4.1.2"
|
|
77
79
|
}
|