@bytespell/amux 0.0.10 → 0.0.12
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 +204 -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/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.d.ts +207 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +563 -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.ts +54 -0
- package/src/session-updates.ts +87 -0
- package/src/session.ts +719 -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/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-DV6-SxsB.d.ts +0 -192
- package/scripts/fix-pty.cjs +0 -21
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
/**
|
|
3
|
+
* Create a WebSocket adapter for an AgentSession.
|
|
4
|
+
*
|
|
5
|
+
* Bridges session events to WebSocket clients and handles incoming messages.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { AgentSession } from 'amux';
|
|
10
|
+
* import { createWsAdapter } from 'amux/ws';
|
|
11
|
+
* import { WebSocketServer } from 'ws';
|
|
12
|
+
*
|
|
13
|
+
* const session = new AgentSession({ ... });
|
|
14
|
+
* const wss = new WebSocketServer({ server, path: '/ws' });
|
|
15
|
+
*
|
|
16
|
+
* createWsAdapter(session, wss);
|
|
17
|
+
*
|
|
18
|
+
* await session.spawnAgent();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function createWsAdapter(session, wss, options = {}) {
|
|
22
|
+
const clients = new Set();
|
|
23
|
+
const sendHistoryOnConnect = options.sendHistoryOnConnect ?? true;
|
|
24
|
+
// Helper to broadcast to all clients
|
|
25
|
+
function broadcast(message) {
|
|
26
|
+
const data = JSON.stringify(message);
|
|
27
|
+
for (const client of clients) {
|
|
28
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
29
|
+
client.send(data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Wire up session events to broadcast
|
|
34
|
+
const eventHandlers = {
|
|
35
|
+
ready: (data) => broadcast({ type: 'ready', ...data }),
|
|
36
|
+
connecting: () => broadcast({ type: 'connecting' }),
|
|
37
|
+
update: (data) => broadcast({ type: 'session_update', update: data }),
|
|
38
|
+
turn_start: () => broadcast({ type: 'turn_start' }),
|
|
39
|
+
turn_end: () => broadcast({ type: 'turn_end' }),
|
|
40
|
+
permission_request: (data) => broadcast({ type: 'permission_request', ...data }),
|
|
41
|
+
prompt_complete: (data) => broadcast({ type: 'prompt_complete', ...data }),
|
|
42
|
+
session_created: (data) => broadcast({ type: 'session_created', ...data }),
|
|
43
|
+
session_switched: (data) => broadcast({ type: 'session_switched', ...data }),
|
|
44
|
+
history_replay: (data) => broadcast({ type: 'history_replay', ...data }),
|
|
45
|
+
error: (data) => broadcast({ type: 'error', ...data }),
|
|
46
|
+
agent_exit: (data) => broadcast({ type: 'agent_exit', ...data }),
|
|
47
|
+
};
|
|
48
|
+
// Register all event handlers
|
|
49
|
+
for (const [event, handler] of Object.entries(eventHandlers)) {
|
|
50
|
+
session.on(event, handler);
|
|
51
|
+
}
|
|
52
|
+
// Handle WebSocket connections
|
|
53
|
+
wss.on('connection', (ws) => {
|
|
54
|
+
console.log('[amux-ws] Client connected');
|
|
55
|
+
clients.add(ws);
|
|
56
|
+
// Send current state to new client
|
|
57
|
+
if (session.isConnected) {
|
|
58
|
+
ws.send(JSON.stringify({
|
|
59
|
+
type: 'ready',
|
|
60
|
+
cwd: session.cwd,
|
|
61
|
+
sessionId: session.sessionId,
|
|
62
|
+
capabilities: session.agentCapabilities,
|
|
63
|
+
agent: session.getAgentInfo(),
|
|
64
|
+
availableAgents: session.getAvailableAgents(),
|
|
65
|
+
}));
|
|
66
|
+
// Send history to hydrate the chat UI
|
|
67
|
+
if (sendHistoryOnConnect) {
|
|
68
|
+
const history = session.loadHistory();
|
|
69
|
+
if (history.length > 0) {
|
|
70
|
+
ws.send(JSON.stringify({
|
|
71
|
+
type: 'history_replay',
|
|
72
|
+
previousSessionId: session.sessionId,
|
|
73
|
+
events: history,
|
|
74
|
+
eventCount: history.length,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
ws.send(JSON.stringify({ type: 'connecting' }));
|
|
81
|
+
}
|
|
82
|
+
// Handle incoming messages
|
|
83
|
+
ws.on('message', async (data) => {
|
|
84
|
+
try {
|
|
85
|
+
const msg = JSON.parse(data.toString());
|
|
86
|
+
await handleMessage(ws, msg, session);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error('[amux-ws] Invalid message:', err);
|
|
90
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
ws.on('close', () => {
|
|
94
|
+
console.log('[amux-ws] Client disconnected');
|
|
95
|
+
clients.delete(ws);
|
|
96
|
+
});
|
|
97
|
+
ws.on('error', (err) => {
|
|
98
|
+
console.error('[amux-ws] WebSocket error:', err);
|
|
99
|
+
clients.delete(ws);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
clientCount: () => clients.size,
|
|
104
|
+
broadcast,
|
|
105
|
+
close: () => {
|
|
106
|
+
for (const client of clients) {
|
|
107
|
+
client.close();
|
|
108
|
+
}
|
|
109
|
+
clients.clear();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Handle incoming WebSocket message
|
|
115
|
+
*/
|
|
116
|
+
async function handleMessage(ws, msg, session) {
|
|
117
|
+
switch (msg.type) {
|
|
118
|
+
case 'prompt':
|
|
119
|
+
if (!session.isConnected || !session.sessionId) {
|
|
120
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Agent not ready' }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await session.prompt(msg.message);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Error already emitted by session
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case 'cancel':
|
|
131
|
+
await session.cancel();
|
|
132
|
+
break;
|
|
133
|
+
case 'permission_response':
|
|
134
|
+
session.respondToPermission(msg.requestId, msg.optionId);
|
|
135
|
+
break;
|
|
136
|
+
case 'change_cwd':
|
|
137
|
+
try {
|
|
138
|
+
await session.changeCwd(msg.path);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
case 'new_session':
|
|
145
|
+
try {
|
|
146
|
+
await session.newSession();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case 'set_mode':
|
|
153
|
+
try {
|
|
154
|
+
await session.setMode(msg.modeId);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case 'set_model':
|
|
161
|
+
try {
|
|
162
|
+
await session.setModel(msg.modelId);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
case 'change_agent':
|
|
169
|
+
try {
|
|
170
|
+
await session.changeAgent(msg.agentType);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case 'get_history': {
|
|
177
|
+
const history = session.loadHistory();
|
|
178
|
+
ws.send(JSON.stringify({ type: 'history', events: history, sessionId: session.sessionId }));
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'list_sessions': {
|
|
182
|
+
const sessions = session.listSessions();
|
|
183
|
+
ws.send(JSON.stringify({ type: 'sessions', sessions }));
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'switch_session':
|
|
187
|
+
try {
|
|
188
|
+
await session.switchSession(msg.sessionId);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
ws.send(JSON.stringify({ type: 'error', message: `Unknown message type: ${msg.type}` }));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=ws-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-adapter.js","sourceRoot":"","sources":["../src/ws-adapter.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,36 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytespell/amux",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Agent Multiplexer -
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "https://github.com/bytespell-oss/shella"
|
|
8
|
-
},
|
|
3
|
+
"version": "0.0.12",
|
|
4
|
+
"description": "Agent Multiplexer - EventEmitter-based agent session management",
|
|
9
5
|
"type": "module",
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"scripts/"
|
|
13
|
-
],
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
14
8
|
"exports": {
|
|
15
|
-
".":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"./lib/logger": "./dist/lib/logger.js",
|
|
20
|
-
"./lib/mentions": "./dist/lib/mentions.js"
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
21
13
|
},
|
|
22
14
|
"scripts": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "eslint src",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
26
22
|
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"acp",
|
|
25
|
+
"agent",
|
|
26
|
+
"claude",
|
|
27
|
+
"multiplexer",
|
|
28
|
+
"ai",
|
|
29
|
+
"websocket"
|
|
30
|
+
],
|
|
31
|
+
"author": "",
|
|
32
|
+
"license": "MIT",
|
|
27
33
|
"dependencies": {
|
|
28
|
-
"@agentclientprotocol/sdk": "^0.
|
|
29
|
-
"
|
|
30
|
-
"node-pty": "^1.2.0-beta.2",
|
|
31
|
-
"tree-kill": "^1.2.2"
|
|
34
|
+
"@agentclientprotocol/sdk": "^0.13.0",
|
|
35
|
+
"ws": "^8.19.0"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^
|
|
38
|
+
"@types/node": "^22.10.0",
|
|
39
|
+
"@types/ws": "^8.18.1",
|
|
40
|
+
"typescript": "~5.7.0",
|
|
41
|
+
"vitest": "^3.1.3"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@zed-industries/claude-code-acp": ">=0.13.0",
|
|
45
|
+
"@zed-industries/codex-acp": ">=0.9.0",
|
|
46
|
+
"pi-acp": ">=0.0.14"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@zed-industries/claude-code-acp": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"@zed-industries/codex-acp": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"pi-acp": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
35
58
|
}
|
|
36
59
|
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
4
|
+
|
|
5
|
+
import { isToolCallUpdate, normalizeSessionUpdate } from './session-updates.js';
|
|
6
|
+
import type { TerminalManager } from './terminal.js';
|
|
7
|
+
import type { StoredEvent } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Callback for routing events
|
|
11
|
+
*/
|
|
12
|
+
type EventCallback = (event: StoredEvent) => void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pending permission request tracking
|
|
16
|
+
*/
|
|
17
|
+
interface PendingPermission {
|
|
18
|
+
resolve: (value: acp.RequestPermissionResponse) => void;
|
|
19
|
+
reject: (reason: Error) => void;
|
|
20
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* ACP Client implementation.
|
|
25
|
+
* Bridges agent notifications to WebSocket clients and handles
|
|
26
|
+
* file operations and terminal management.
|
|
27
|
+
*/
|
|
28
|
+
export class AmuxClient implements acp.Client {
|
|
29
|
+
pendingPermissions = new Map<string, PendingPermission>();
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private terminalManager: TerminalManager,
|
|
33
|
+
private onEvent: EventCallback
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handle permission request from agent
|
|
38
|
+
*/
|
|
39
|
+
async requestPermission(params: acp.RequestPermissionRequest): Promise<acp.RequestPermissionResponse> {
|
|
40
|
+
console.log('[amux] Permission request:', params.toolCall?.title);
|
|
41
|
+
|
|
42
|
+
// Send permission request
|
|
43
|
+
const requestId = crypto.randomUUID();
|
|
44
|
+
this.onEvent({
|
|
45
|
+
type: 'permission_request',
|
|
46
|
+
requestId,
|
|
47
|
+
toolCall: params.toolCall,
|
|
48
|
+
options: params.options,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Wait for response from client (with timeout)
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const timeout = setTimeout(() => {
|
|
54
|
+
this.pendingPermissions.delete(requestId);
|
|
55
|
+
resolve({ outcome: { outcome: 'cancelled' } });
|
|
56
|
+
}, 300000); // 5 minute timeout
|
|
57
|
+
|
|
58
|
+
this.pendingPermissions.set(requestId, { resolve, reject, timeout });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle permission response from UI
|
|
64
|
+
*/
|
|
65
|
+
handlePermissionResponse(requestId: string, optionId: string): void {
|
|
66
|
+
const pending = this.pendingPermissions.get(requestId);
|
|
67
|
+
if (pending) {
|
|
68
|
+
clearTimeout(pending.timeout);
|
|
69
|
+
this.pendingPermissions.delete(requestId);
|
|
70
|
+
pending.resolve({
|
|
71
|
+
outcome: { outcome: 'selected', optionId },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Handle session update from agent
|
|
78
|
+
*/
|
|
79
|
+
async sessionUpdate(params: acp.SessionNotification): Promise<void> {
|
|
80
|
+
const update = params.update;
|
|
81
|
+
|
|
82
|
+
// Normalize the update (convert Claude-style diffs to unified format)
|
|
83
|
+
const normalized = normalizeSessionUpdate(update);
|
|
84
|
+
|
|
85
|
+
// Route to session
|
|
86
|
+
this.onEvent({ type: 'session_update', update: normalized });
|
|
87
|
+
|
|
88
|
+
// Log updates for debugging
|
|
89
|
+
if (update.sessionUpdate === 'agent_message_chunk') {
|
|
90
|
+
// Don't log every chunk
|
|
91
|
+
} else if (isToolCallUpdate(update)) {
|
|
92
|
+
console.log(`[amux] Tool call: ${update.title} (${update.status}) id=${(update as { toolCallId?: string }).toolCallId}`);
|
|
93
|
+
} else if (update.sessionUpdate === 'tool_call_update') {
|
|
94
|
+
// Don't log every update
|
|
95
|
+
} else if (update.sessionUpdate === 'current_mode_update') {
|
|
96
|
+
console.log(`[amux] Mode update: ${update.currentModeId}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(`[amux] Session update: ${update.sessionUpdate}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Write text file (ACP fs capability)
|
|
104
|
+
*/
|
|
105
|
+
async writeTextFile(params: acp.WriteTextFileRequest): Promise<acp.WriteTextFileResponse> {
|
|
106
|
+
console.log('[amux] Write text file:', params.path);
|
|
107
|
+
try {
|
|
108
|
+
fs.writeFileSync(params.path, params.content);
|
|
109
|
+
return {};
|
|
110
|
+
} catch (err) {
|
|
111
|
+
throw new Error(`Failed to write file: ${(err as Error).message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read text file (ACP fs capability)
|
|
117
|
+
*/
|
|
118
|
+
async readTextFile(params: acp.ReadTextFileRequest): Promise<acp.ReadTextFileResponse> {
|
|
119
|
+
console.log('[amux] Read text file:', params.path);
|
|
120
|
+
try {
|
|
121
|
+
const content = fs.readFileSync(params.path, 'utf-8');
|
|
122
|
+
return { content };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new Error(`Failed to read file: ${(err as Error).message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create terminal (ACP terminal capability)
|
|
130
|
+
*/
|
|
131
|
+
async createTerminal(params: acp.CreateTerminalRequest): Promise<acp.CreateTerminalResponse> {
|
|
132
|
+
return this.terminalManager.create(params);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get terminal output (ACP terminal capability)
|
|
137
|
+
*/
|
|
138
|
+
async terminalOutput(params: acp.TerminalOutputRequest): Promise<acp.TerminalOutputResponse> {
|
|
139
|
+
return this.terminalManager.getOutput(params.terminalId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Wait for terminal exit (ACP terminal capability)
|
|
144
|
+
*/
|
|
145
|
+
async waitForTerminalExit(params: acp.WaitForTerminalExitRequest): Promise<acp.WaitForTerminalExitResponse> {
|
|
146
|
+
return this.terminalManager.waitForExit(params.terminalId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Kill terminal (ACP terminal capability)
|
|
151
|
+
*/
|
|
152
|
+
async killTerminal(params: acp.KillTerminalCommandRequest): Promise<acp.KillTerminalCommandResponse> {
|
|
153
|
+
return this.terminalManager.kill(params.terminalId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Release terminal (ACP terminal capability)
|
|
158
|
+
*/
|
|
159
|
+
async releaseTerminal(params: acp.ReleaseTerminalRequest): Promise<acp.ReleaseTerminalResponse> {
|
|
160
|
+
return this.terminalManager.release(params.terminalId);
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* amux - Agent Multiplexer
|
|
3
|
+
*
|
|
4
|
+
* A library for managing ACP agent sessions with an EventEmitter interface.
|
|
5
|
+
* Transport-agnostic - wire up to WebSocket, HTTP, IPC, or anything else.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { AgentSession, createWsAdapter } from '@bytespell/amux';
|
|
10
|
+
* import { WebSocketServer } from 'ws';
|
|
11
|
+
*
|
|
12
|
+
* const session = new AgentSession({
|
|
13
|
+
* instanceId: 'my-instance',
|
|
14
|
+
* basePath: __dirname,
|
|
15
|
+
* systemContext: 'You are a helpful assistant...',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Option 1: Use the WebSocket adapter
|
|
19
|
+
* const wss = new WebSocketServer({ port: 3000 });
|
|
20
|
+
* createWsAdapter(session, wss);
|
|
21
|
+
*
|
|
22
|
+
* // Option 2: Handle events yourself
|
|
23
|
+
* session.on('ready', (data) => console.log('Ready:', data));
|
|
24
|
+
* session.on('update', (data) => myTransport.send(data));
|
|
25
|
+
*
|
|
26
|
+
* await session.spawnAgent();
|
|
27
|
+
* await session.prompt('Hello!');
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Core session management
|
|
32
|
+
export { AgentSession } from './session.js';
|
|
33
|
+
export type { AgentSessionEvents } from './session.js';
|
|
34
|
+
|
|
35
|
+
// WebSocket adapter
|
|
36
|
+
export { createWsAdapter } from './ws-adapter.js';
|
|
37
|
+
export type { WsAdapterOptions } from './ws-adapter.js';
|
|
38
|
+
|
|
39
|
+
// Supporting components (for advanced usage)
|
|
40
|
+
export { AmuxClient } from './client.js';
|
|
41
|
+
export { TerminalManager } from './terminal.js';
|
|
42
|
+
export { StateManager } from './state.js';
|
|
43
|
+
|
|
44
|
+
// Session update utilities
|
|
45
|
+
export {
|
|
46
|
+
isToolCallUpdate,
|
|
47
|
+
isToolCallUpdateMessage,
|
|
48
|
+
isDiffContent,
|
|
49
|
+
normalizeSessionUpdate,
|
|
50
|
+
} from './session-updates.js';
|
|
51
|
+
|
|
52
|
+
// Message parsing utilities
|
|
53
|
+
export { parseMessageToContentBlocks } from './message-parser.js';
|
|
54
|
+
|
|
55
|
+
// Types
|
|
56
|
+
export type {
|
|
57
|
+
AgentSessionConfig,
|
|
58
|
+
AgentConfig,
|
|
59
|
+
SessionMetadata,
|
|
60
|
+
SessionState,
|
|
61
|
+
SessionRestoreInfo,
|
|
62
|
+
StoredEvent,
|
|
63
|
+
} from './types.js';
|
|
64
|
+
|
|
65
|
+
// Built-in agents registry
|
|
66
|
+
export { AGENTS } from './types.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse message text with @mentions into ContentBlock array.
|
|
6
|
+
* @mentions become resource_link blocks, other text becomes text blocks.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* - "hello world" → [{ type: 'text', text: 'hello world' }]
|
|
10
|
+
* - "@src/foo.ts" → [{ type: 'resource_link', uri: 'file://...', name: 'src/foo.ts' }]
|
|
11
|
+
* - "check @src/foo.ts for bugs" → text + resource_link + text
|
|
12
|
+
*/
|
|
13
|
+
export function parseMessageToContentBlocks(
|
|
14
|
+
message: string,
|
|
15
|
+
workingDir: string
|
|
16
|
+
): acp.ContentBlock[] {
|
|
17
|
+
const blocks: acp.ContentBlock[] = [];
|
|
18
|
+
// Match @path/to/file.ts, @./local.ts, @../parent/file.ts
|
|
19
|
+
// Allows leading ./ or ../ followed by path characters
|
|
20
|
+
const mentionRegex = /@(\.{0,2}[\w\/\.\-]+)/g;
|
|
21
|
+
|
|
22
|
+
let lastIndex = 0;
|
|
23
|
+
for (const match of message.matchAll(mentionRegex)) {
|
|
24
|
+
// Add text before mention
|
|
25
|
+
if (match.index! > lastIndex) {
|
|
26
|
+
const text = message.slice(lastIndex, match.index);
|
|
27
|
+
if (text.trim()) {
|
|
28
|
+
blocks.push({ type: 'text', text });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add resource_link for mention
|
|
33
|
+
const relativePath = match[1]!;
|
|
34
|
+
const absolutePath = path.resolve(workingDir, relativePath);
|
|
35
|
+
blocks.push({
|
|
36
|
+
type: 'resource_link',
|
|
37
|
+
uri: `file://${absolutePath}`,
|
|
38
|
+
name: relativePath,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
lastIndex = match.index! + match[0].length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add remaining text
|
|
45
|
+
if (lastIndex < message.length) {
|
|
46
|
+
const text = message.slice(lastIndex);
|
|
47
|
+
if (text.trim()) {
|
|
48
|
+
blocks.push({ type: 'text', text });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If no mentions found, return single text block
|
|
53
|
+
return blocks.length > 0 ? blocks : [{ type: 'text', text: message }];
|
|
54
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard for tool_call session updates
|
|
5
|
+
*/
|
|
6
|
+
export function isToolCallUpdate(
|
|
7
|
+
update: acp.SessionUpdate
|
|
8
|
+
): update is acp.ToolCall & { sessionUpdate: 'tool_call' } {
|
|
9
|
+
return update.sessionUpdate === 'tool_call';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Type guard for tool_call_update session updates
|
|
14
|
+
*/
|
|
15
|
+
export function isToolCallUpdateMessage(
|
|
16
|
+
update: acp.SessionUpdate
|
|
17
|
+
): update is acp.ToolCallUpdate & { sessionUpdate: 'tool_call_update' } {
|
|
18
|
+
return update.sessionUpdate === 'tool_call_update';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type guard for diff content items
|
|
23
|
+
*/
|
|
24
|
+
export function isDiffContent(
|
|
25
|
+
item: acp.ToolCallContent
|
|
26
|
+
): item is acp.Diff & { type: 'diff' } {
|
|
27
|
+
return item.type === 'diff';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize ACP session updates.
|
|
32
|
+
* Claude sends Edit/Write diffs as {newText, oldText, path} instead of unified diff.
|
|
33
|
+
* We convert these to standard unified diff format so the UI doesn't need agent-specific logic.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeSessionUpdate(
|
|
36
|
+
update: acp.SessionUpdate
|
|
37
|
+
): acp.SessionUpdate {
|
|
38
|
+
// Only process tool_call and tool_call_update events with content arrays
|
|
39
|
+
if (!isToolCallUpdate(update) && !isToolCallUpdateMessage(update)) {
|
|
40
|
+
return update;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = update.content;
|
|
44
|
+
if (!content || !Array.isArray(content)) {
|
|
45
|
+
return update;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for diff items that need normalization
|
|
49
|
+
const normalizedContent = content.map((item): acp.ToolCallContent => {
|
|
50
|
+
if (!isDiffContent(item)) return item;
|
|
51
|
+
|
|
52
|
+
const diffItem = item;
|
|
53
|
+
const newText = diffItem.newText;
|
|
54
|
+
const oldText = diffItem.oldText;
|
|
55
|
+
const filePath = diffItem.path ?? 'file';
|
|
56
|
+
|
|
57
|
+
// Generate unified diff
|
|
58
|
+
const oldLines = oldText ? oldText.split('\n') : [];
|
|
59
|
+
const newLines = newText.split('\n');
|
|
60
|
+
|
|
61
|
+
let unifiedDiff = `Index: ${filePath}\n`;
|
|
62
|
+
unifiedDiff += '===================================================================\n';
|
|
63
|
+
unifiedDiff += `--- ${filePath}\n`;
|
|
64
|
+
unifiedDiff += `+++ ${filePath}\n`;
|
|
65
|
+
unifiedDiff += `@@ -${oldLines.length > 0 ? 1 : 0},${oldLines.length} +1,${newLines.length} @@\n`;
|
|
66
|
+
|
|
67
|
+
for (const line of oldLines) {
|
|
68
|
+
unifiedDiff += `-${line}\n`;
|
|
69
|
+
}
|
|
70
|
+
for (const line of newLines) {
|
|
71
|
+
unifiedDiff += `+${line}\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Return normalized diff item
|
|
75
|
+
return {
|
|
76
|
+
type: 'diff',
|
|
77
|
+
newText: unifiedDiff,
|
|
78
|
+
oldText: '',
|
|
79
|
+
path: filePath,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...update,
|
|
85
|
+
content: normalizedContent,
|
|
86
|
+
};
|
|
87
|
+
}
|