@bytespell/amux 0.0.11 → 0.0.13
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/.claude/settings.local.json +11 -0
- package/CLAUDE.md +104 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +118 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +135 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/{lib/mentions.d.ts → message-parser.d.ts} +3 -5
- package/dist/message-parser.d.ts.map +1 -0
- package/dist/message-parser.js +45 -0
- package/dist/message-parser.js.map +1 -0
- package/dist/message-parser.test.d.ts +2 -0
- package/dist/message-parser.test.d.ts.map +1 -0
- package/dist/message-parser.test.js +188 -0
- package/dist/message-parser.test.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +356 -0
- package/dist/server.js.map +1 -0
- package/dist/session-updates.d.ts +26 -0
- package/dist/session-updates.d.ts.map +1 -0
- package/dist/session-updates.js +68 -0
- package/dist/session-updates.js.map +1 -0
- package/dist/session-updates.test.d.ts +2 -0
- package/dist/session-updates.test.d.ts.map +1 -0
- package/dist/session-updates.test.js +223 -0
- package/dist/session-updates.test.js.map +1 -0
- package/dist/session.d.ts +208 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +580 -0
- package/dist/session.js.map +1 -0
- package/dist/state.d.ts +74 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +250 -0
- package/dist/state.js.map +1 -0
- package/dist/terminal.d.ts +47 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +137 -0
- package/dist/terminal.js.map +1 -0
- package/dist/types.d.ts +64 -2
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -31
- package/dist/types.js.map +1 -1
- package/dist/ws-adapter.d.ts +39 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +198 -0
- package/dist/ws-adapter.js.map +1 -0
- package/package.json +47 -24
- package/src/client.ts +162 -0
- package/src/index.ts +66 -0
- package/src/message-parser.test.ts +207 -0
- package/src/message-parser.ts +54 -0
- package/src/session-updates.test.ts +265 -0
- package/src/session-updates.ts +87 -0
- package/src/session.ts +737 -0
- package/src/state.ts +287 -0
- package/src/terminal.ts +164 -0
- package/src/types.ts +88 -0
- package/src/ws-adapter.ts +245 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +7 -0
- package/dist/chunk-5IPYOXBE.js +0 -32
- package/dist/chunk-5IPYOXBE.js.map +0 -1
- package/dist/chunk-C73RKCTS.js +0 -36
- package/dist/chunk-C73RKCTS.js.map +0 -1
- package/dist/chunk-VVXT4HQM.js +0 -779
- package/dist/chunk-VVXT4HQM.js.map +0 -1
- package/dist/lib/logger.d.ts +0 -24
- package/dist/lib/logger.js +0 -17
- package/dist/lib/logger.js.map +0 -1
- package/dist/lib/mentions.js +0 -7
- package/dist/lib/mentions.js.map +0 -1
- package/dist/streams/backends/index.d.ts +0 -88
- package/dist/streams/backends/index.js +0 -13
- package/dist/streams/backends/index.js.map +0 -1
- package/dist/streams/manager.d.ts +0 -55
- package/dist/streams/manager.js +0 -248
- package/dist/streams/manager.js.map +0 -1
- package/dist/types-DCRtrjjj.d.ts +0 -192
- package/scripts/fix-pty.cjs +0 -21
package/src/state.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
import type { SessionState, SessionMetadata, StoredEvent } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default state directory
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_STATE_DIR = path.join(
|
|
11
|
+
os.homedir(),
|
|
12
|
+
'.local',
|
|
13
|
+
'state',
|
|
14
|
+
'amux'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* State manager for amux sessions.
|
|
19
|
+
* Handles persistence of session state and history.
|
|
20
|
+
*/
|
|
21
|
+
export class StateManager {
|
|
22
|
+
private stateDir: string;
|
|
23
|
+
|
|
24
|
+
constructor(stateDir?: string) {
|
|
25
|
+
this.stateDir = stateDir ?? DEFAULT_STATE_DIR;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the state file path for a specific instance
|
|
30
|
+
*/
|
|
31
|
+
private getStateFilePath(instanceId: string): string {
|
|
32
|
+
return path.join(this.stateDir, `instance-${instanceId}.json`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get history file path for a session
|
|
37
|
+
*/
|
|
38
|
+
private getHistoryPath(sessionId: string): string {
|
|
39
|
+
return path.join(this.stateDir, `session-${sessionId}-history.json`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get sessions registry file path
|
|
44
|
+
*/
|
|
45
|
+
private getSessionsRegistryPath(): string {
|
|
46
|
+
return path.join(this.stateDir, 'sessions.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load persisted session state
|
|
51
|
+
*/
|
|
52
|
+
loadState(instanceId: string): SessionState {
|
|
53
|
+
console.log('[amux] Loading state for instance:', instanceId);
|
|
54
|
+
try {
|
|
55
|
+
const stateFile = this.getStateFilePath(instanceId);
|
|
56
|
+
if (fs.existsSync(stateFile)) {
|
|
57
|
+
const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8')) as SessionState;
|
|
58
|
+
console.log('[amux] Loaded persisted state:', JSON.stringify(data));
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.log('[amux] Failed to load persisted state:', (err as Error).message);
|
|
63
|
+
}
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Save session state to disk
|
|
69
|
+
*/
|
|
70
|
+
saveState(instanceId: string, state: SessionState): void {
|
|
71
|
+
try {
|
|
72
|
+
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
73
|
+
const stateFile = this.getStateFilePath(instanceId);
|
|
74
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.log('[amux] Failed to save state:', (err as Error).message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Store event to history for replay
|
|
82
|
+
*/
|
|
83
|
+
storeEvent(sessionId: string | null, event: StoredEvent): void {
|
|
84
|
+
if (!sessionId) return;
|
|
85
|
+
|
|
86
|
+
// Only store session updates and turn markers
|
|
87
|
+
const isSessionUpdate = event.type === 'session_update';
|
|
88
|
+
const isTurnMarker = event.type === 'turn_start' || event.type === 'turn_end';
|
|
89
|
+
|
|
90
|
+
if (!isSessionUpdate && !isTurnMarker) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
96
|
+
const historyFile = this.getHistoryPath(sessionId);
|
|
97
|
+
let events: StoredEvent[] = [];
|
|
98
|
+
if (fs.existsSync(historyFile)) {
|
|
99
|
+
const data = fs.readFileSync(historyFile, 'utf-8');
|
|
100
|
+
events = JSON.parse(data) as StoredEvent[];
|
|
101
|
+
}
|
|
102
|
+
events.push(event);
|
|
103
|
+
fs.writeFileSync(historyFile, JSON.stringify(events));
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.log('[amux] Failed to store event:', (err as Error).message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load history for replay
|
|
111
|
+
*/
|
|
112
|
+
loadHistory(sessionId: string | null): StoredEvent[] {
|
|
113
|
+
if (!sessionId) return [];
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const historyFile = this.getHistoryPath(sessionId);
|
|
117
|
+
if (fs.existsSync(historyFile)) {
|
|
118
|
+
const data = fs.readFileSync(historyFile, 'utf-8');
|
|
119
|
+
return JSON.parse(data) as StoredEvent[];
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.log('[amux] Failed to load history:', (err as Error).message);
|
|
123
|
+
}
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clear history for a session
|
|
129
|
+
*/
|
|
130
|
+
clearHistory(sessionId: string | null): void {
|
|
131
|
+
if (!sessionId) return;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const historyFile = this.getHistoryPath(sessionId);
|
|
135
|
+
if (fs.existsSync(historyFile)) {
|
|
136
|
+
fs.unlinkSync(historyFile);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.log('[amux] Failed to clear history:', (err as Error).message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Load sessions registry
|
|
145
|
+
*/
|
|
146
|
+
private loadSessionsRegistry(): { sessions: SessionMetadata[] } {
|
|
147
|
+
try {
|
|
148
|
+
const registryPath = this.getSessionsRegistryPath();
|
|
149
|
+
if (fs.existsSync(registryPath)) {
|
|
150
|
+
const data = fs.readFileSync(registryPath, 'utf-8');
|
|
151
|
+
return JSON.parse(data) as { sessions: SessionMetadata[] };
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.log('[amux] Failed to load sessions registry:', (err as Error).message);
|
|
155
|
+
}
|
|
156
|
+
return { sessions: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Save sessions registry
|
|
161
|
+
*/
|
|
162
|
+
private saveSessionsRegistry(registry: { sessions: SessionMetadata[] }): void {
|
|
163
|
+
try {
|
|
164
|
+
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
165
|
+
const registryPath = this.getSessionsRegistryPath();
|
|
166
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.log('[amux] Failed to save sessions registry:', (err as Error).message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Register a new session or update existing one
|
|
174
|
+
*/
|
|
175
|
+
registerSession(
|
|
176
|
+
sessionId: string,
|
|
177
|
+
cwd: string,
|
|
178
|
+
agentType: string,
|
|
179
|
+
displayName?: string
|
|
180
|
+
): void {
|
|
181
|
+
const registry = this.loadSessionsRegistry();
|
|
182
|
+
const existingIndex = registry.sessions.findIndex(s => s.id === sessionId);
|
|
183
|
+
|
|
184
|
+
const session: SessionMetadata = {
|
|
185
|
+
id: sessionId,
|
|
186
|
+
name: displayName || `Session ${sessionId.slice(0, 8)}`,
|
|
187
|
+
cwd,
|
|
188
|
+
messageCount: 0,
|
|
189
|
+
lastModified: Date.now(),
|
|
190
|
+
agentType,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (existingIndex >= 0) {
|
|
194
|
+
// Update existing session, preserve message count
|
|
195
|
+
const existing = registry.sessions[existingIndex];
|
|
196
|
+
session.messageCount = existing?.messageCount ?? 0;
|
|
197
|
+
registry.sessions[existingIndex] = session;
|
|
198
|
+
} else {
|
|
199
|
+
// Add new session at the beginning (most recent first)
|
|
200
|
+
registry.sessions.unshift(session);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.saveSessionsRegistry(registry);
|
|
204
|
+
console.log(`[amux] Registered session: ${sessionId}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Update session metadata
|
|
209
|
+
*/
|
|
210
|
+
updateSessionMetadata(
|
|
211
|
+
sessionId: string,
|
|
212
|
+
updates: Partial<Pick<SessionMetadata, 'messageCount' | 'lastModified' | 'name'>>
|
|
213
|
+
): void {
|
|
214
|
+
const registry = this.loadSessionsRegistry();
|
|
215
|
+
const session = registry.sessions.find(s => s.id === sessionId);
|
|
216
|
+
|
|
217
|
+
if (session) {
|
|
218
|
+
if (updates.messageCount !== undefined) {
|
|
219
|
+
session.messageCount = updates.messageCount;
|
|
220
|
+
}
|
|
221
|
+
if (updates.lastModified !== undefined) {
|
|
222
|
+
session.lastModified = updates.lastModified;
|
|
223
|
+
}
|
|
224
|
+
if (updates.name !== undefined) {
|
|
225
|
+
session.name = updates.name;
|
|
226
|
+
}
|
|
227
|
+
this.saveSessionsRegistry(registry);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Increment message count for a session
|
|
233
|
+
*/
|
|
234
|
+
incrementSessionMessageCount(sessionId: string): void {
|
|
235
|
+
const registry = this.loadSessionsRegistry();
|
|
236
|
+
const session = registry.sessions.find(s => s.id === sessionId);
|
|
237
|
+
|
|
238
|
+
if (session) {
|
|
239
|
+
session.messageCount += 1;
|
|
240
|
+
session.lastModified = Date.now();
|
|
241
|
+
this.saveSessionsRegistry(registry);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* List all sessions, optionally filtered by cwd
|
|
247
|
+
*/
|
|
248
|
+
listSessions(cwd?: string): SessionMetadata[] {
|
|
249
|
+
const registry = this.loadSessionsRegistry();
|
|
250
|
+
let sessions = registry.sessions;
|
|
251
|
+
|
|
252
|
+
// Filter by cwd if provided
|
|
253
|
+
if (cwd) {
|
|
254
|
+
sessions = sessions.filter(s => s.cwd === cwd);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Sort by lastModified (most recent first)
|
|
258
|
+
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
259
|
+
|
|
260
|
+
return sessions;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get a specific session by ID
|
|
265
|
+
*/
|
|
266
|
+
getSession(sessionId: string): SessionMetadata | undefined {
|
|
267
|
+
const registry = this.loadSessionsRegistry();
|
|
268
|
+
return registry.sessions.find(s => s.id === sessionId);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Delete a session from the registry and its history
|
|
273
|
+
*/
|
|
274
|
+
deleteSession(sessionId: string): void {
|
|
275
|
+
const registry = this.loadSessionsRegistry();
|
|
276
|
+
const index = registry.sessions.findIndex(s => s.id === sessionId);
|
|
277
|
+
|
|
278
|
+
if (index >= 0) {
|
|
279
|
+
registry.sessions.splice(index, 1);
|
|
280
|
+
this.saveSessionsRegistry(registry);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Also clear the history file
|
|
284
|
+
this.clearHistory(sessionId);
|
|
285
|
+
console.log(`[amux] Deleted session: ${sessionId}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Terminal state tracking
|
|
7
|
+
*/
|
|
8
|
+
export interface TerminalState {
|
|
9
|
+
process: ChildProcess;
|
|
10
|
+
output: string;
|
|
11
|
+
exitCode: number | null;
|
|
12
|
+
signal: string | null;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
outputByteLimit: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Manages terminal processes for ACP terminal capability.
|
|
19
|
+
* Handles spawning, output collection, and lifecycle management.
|
|
20
|
+
*/
|
|
21
|
+
export class TerminalManager {
|
|
22
|
+
private terminals = new Map<string, TerminalState>();
|
|
23
|
+
|
|
24
|
+
constructor(private defaultCwd: () => string) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a new terminal process
|
|
28
|
+
*/
|
|
29
|
+
create(params: acp.CreateTerminalRequest): acp.CreateTerminalResponse {
|
|
30
|
+
console.log('[amux] Create terminal:', params.command);
|
|
31
|
+
const terminalId = crypto.randomUUID();
|
|
32
|
+
const outputByteLimit = Number(params.outputByteLimit ?? 1024 * 1024); // Default 1MB
|
|
33
|
+
|
|
34
|
+
const termProc = spawn(params.command, params.args ?? [], {
|
|
35
|
+
cwd: params.cwd ?? this.defaultCwd(),
|
|
36
|
+
env: params.env
|
|
37
|
+
? { ...process.env, ...Object.fromEntries(params.env.map(e => [e.name, e.value])) }
|
|
38
|
+
: process.env,
|
|
39
|
+
shell: true,
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const terminal: TerminalState = {
|
|
44
|
+
process: termProc,
|
|
45
|
+
output: '',
|
|
46
|
+
exitCode: null,
|
|
47
|
+
signal: null,
|
|
48
|
+
truncated: false,
|
|
49
|
+
outputByteLimit,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const appendOutput = (data: Buffer): void => {
|
|
53
|
+
terminal.output += data.toString();
|
|
54
|
+
// Truncate from beginning if over limit
|
|
55
|
+
if (terminal.output.length > terminal.outputByteLimit) {
|
|
56
|
+
terminal.output = terminal.output.slice(-terminal.outputByteLimit);
|
|
57
|
+
terminal.truncated = true;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
termProc.stdout?.on('data', appendOutput);
|
|
62
|
+
termProc.stderr?.on('data', appendOutput);
|
|
63
|
+
|
|
64
|
+
termProc.on('exit', (code, signal) => {
|
|
65
|
+
console.log(`[amux] Terminal ${terminalId} exited with code ${code}, signal ${signal}`);
|
|
66
|
+
terminal.exitCode = code ?? null;
|
|
67
|
+
terminal.signal = signal ?? null;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
termProc.on('error', (err) => {
|
|
71
|
+
console.error(`[amux] Terminal ${terminalId} error:`, err.message);
|
|
72
|
+
terminal.output += `\nError: ${err.message}`;
|
|
73
|
+
terminal.exitCode = -1;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.terminals.set(terminalId, terminal);
|
|
77
|
+
console.log(`[amux] Created terminal ${terminalId} for command: ${params.command}`);
|
|
78
|
+
|
|
79
|
+
return { terminalId };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get terminal output
|
|
84
|
+
*/
|
|
85
|
+
getOutput(terminalId: string): acp.TerminalOutputResponse {
|
|
86
|
+
const terminal = this.terminals.get(terminalId);
|
|
87
|
+
if (!terminal) {
|
|
88
|
+
throw new Error(`Terminal ${terminalId} not found`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
output: terminal.output,
|
|
92
|
+
truncated: terminal.truncated,
|
|
93
|
+
exitStatus: terminal.exitCode !== null || terminal.signal !== null ? {
|
|
94
|
+
exitCode: terminal.exitCode,
|
|
95
|
+
signal: terminal.signal,
|
|
96
|
+
} : undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Wait for terminal to exit
|
|
102
|
+
*/
|
|
103
|
+
async waitForExit(terminalId: string): Promise<acp.WaitForTerminalExitResponse> {
|
|
104
|
+
const terminal = this.terminals.get(terminalId);
|
|
105
|
+
if (!terminal) {
|
|
106
|
+
throw new Error(`Terminal ${terminalId} not found`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If already exited, return immediately
|
|
110
|
+
if (terminal.exitCode !== null || terminal.signal !== null) {
|
|
111
|
+
return {
|
|
112
|
+
exitCode: terminal.exitCode,
|
|
113
|
+
signal: terminal.signal,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Wait for exit
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
terminal.process.on('exit', (code, signal) => {
|
|
120
|
+
resolve({
|
|
121
|
+
exitCode: code ?? null,
|
|
122
|
+
signal: signal ?? null,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Kill a terminal process
|
|
130
|
+
*/
|
|
131
|
+
kill(terminalId: string): acp.KillTerminalCommandResponse {
|
|
132
|
+
const terminal = this.terminals.get(terminalId);
|
|
133
|
+
if (!terminal) {
|
|
134
|
+
throw new Error(`Terminal ${terminalId} not found`);
|
|
135
|
+
}
|
|
136
|
+
terminal.process.kill('SIGTERM');
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Release a terminal (cleanup)
|
|
142
|
+
*/
|
|
143
|
+
release(terminalId: string): acp.ReleaseTerminalResponse {
|
|
144
|
+
const terminal = this.terminals.get(terminalId);
|
|
145
|
+
if (terminal) {
|
|
146
|
+
if (terminal.exitCode === null) {
|
|
147
|
+
terminal.process.kill('SIGKILL');
|
|
148
|
+
}
|
|
149
|
+
this.terminals.delete(terminalId);
|
|
150
|
+
}
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Kill all terminals (for shutdown)
|
|
156
|
+
*/
|
|
157
|
+
killAll(): void {
|
|
158
|
+
for (const [terminalId, terminal] of this.terminals) {
|
|
159
|
+
console.log(`[amux] Killing terminal ${terminalId}`);
|
|
160
|
+
terminal.process.kill('SIGKILL');
|
|
161
|
+
}
|
|
162
|
+
this.terminals.clear();
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent configuration - maps agent type to binary name
|
|
5
|
+
*/
|
|
6
|
+
export interface AgentConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
bin: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Built-in agent registry
|
|
13
|
+
*/
|
|
14
|
+
export const AGENTS: Record<string, AgentConfig> = {
|
|
15
|
+
'claude-code': {
|
|
16
|
+
name: 'Claude Code',
|
|
17
|
+
bin: 'claude-code-acp',
|
|
18
|
+
},
|
|
19
|
+
'codex': {
|
|
20
|
+
name: 'Codex',
|
|
21
|
+
bin: 'codex-acp',
|
|
22
|
+
},
|
|
23
|
+
'pi': {
|
|
24
|
+
name: 'Pi',
|
|
25
|
+
bin: 'pi-acp',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Session metadata for persistence
|
|
31
|
+
*/
|
|
32
|
+
export interface SessionMetadata {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
cwd: string;
|
|
36
|
+
messageCount: number;
|
|
37
|
+
lastModified: number;
|
|
38
|
+
agentType: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Persisted session state
|
|
43
|
+
*/
|
|
44
|
+
export interface SessionState {
|
|
45
|
+
cwd?: string;
|
|
46
|
+
sessionId?: string | null;
|
|
47
|
+
agentType?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Event stored in history for replay
|
|
52
|
+
*/
|
|
53
|
+
export interface StoredEvent {
|
|
54
|
+
type: string;
|
|
55
|
+
update?: acp.SessionUpdate;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Session restore info sent to UI
|
|
61
|
+
*/
|
|
62
|
+
export interface SessionRestoreInfo {
|
|
63
|
+
restored: boolean;
|
|
64
|
+
reason?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Configuration for creating an agent session
|
|
69
|
+
*/
|
|
70
|
+
export interface AgentSessionConfig {
|
|
71
|
+
/** Unique identifier for this session instance */
|
|
72
|
+
instanceId: string;
|
|
73
|
+
|
|
74
|
+
/** Base path for resolving agent binaries (node_modules/.bin) */
|
|
75
|
+
basePath: string;
|
|
76
|
+
|
|
77
|
+
/** Optional system context to inject (e.g., from a markdown file) */
|
|
78
|
+
systemContext?: string;
|
|
79
|
+
|
|
80
|
+
/** Optional fixed working directory (overrides user selection) */
|
|
81
|
+
fixedCwd?: string;
|
|
82
|
+
|
|
83
|
+
/** Optional initial agent type (defaults to 'claude-code') */
|
|
84
|
+
agentType?: string;
|
|
85
|
+
|
|
86
|
+
/** Optional state directory override */
|
|
87
|
+
stateDir?: string;
|
|
88
|
+
}
|