@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.
Files changed (89) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/CLAUDE.md +104 -0
  3. package/LICENSE +21 -0
  4. package/README.md +215 -0
  5. package/dist/cli.d.ts +14 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +118 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/client.d.ts +68 -0
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +135 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/index.d.ts +41 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +44 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/{lib/mentions.d.ts → message-parser.d.ts} +3 -5
  18. package/dist/message-parser.d.ts.map +1 -0
  19. package/dist/message-parser.js +45 -0
  20. package/dist/message-parser.js.map +1 -0
  21. package/dist/message-parser.test.d.ts +2 -0
  22. package/dist/message-parser.test.d.ts.map +1 -0
  23. package/dist/message-parser.test.js +188 -0
  24. package/dist/message-parser.test.js.map +1 -0
  25. package/dist/server.d.ts +24 -0
  26. package/dist/server.d.ts.map +1 -0
  27. package/dist/server.js +356 -0
  28. package/dist/server.js.map +1 -0
  29. package/dist/session-updates.d.ts +26 -0
  30. package/dist/session-updates.d.ts.map +1 -0
  31. package/dist/session-updates.js +68 -0
  32. package/dist/session-updates.js.map +1 -0
  33. package/dist/session-updates.test.d.ts +2 -0
  34. package/dist/session-updates.test.d.ts.map +1 -0
  35. package/dist/session-updates.test.js +223 -0
  36. package/dist/session-updates.test.js.map +1 -0
  37. package/dist/session.d.ts +208 -0
  38. package/dist/session.d.ts.map +1 -0
  39. package/dist/session.js +580 -0
  40. package/dist/session.js.map +1 -0
  41. package/dist/state.d.ts +74 -0
  42. package/dist/state.d.ts.map +1 -0
  43. package/dist/state.js +250 -0
  44. package/dist/state.js.map +1 -0
  45. package/dist/terminal.d.ts +47 -0
  46. package/dist/terminal.d.ts.map +1 -0
  47. package/dist/terminal.js +137 -0
  48. package/dist/terminal.js.map +1 -0
  49. package/dist/types.d.ts +64 -2
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +16 -31
  52. package/dist/types.js.map +1 -1
  53. package/dist/ws-adapter.d.ts +39 -0
  54. package/dist/ws-adapter.d.ts.map +1 -0
  55. package/dist/ws-adapter.js +198 -0
  56. package/dist/ws-adapter.js.map +1 -0
  57. package/package.json +47 -24
  58. package/src/client.ts +162 -0
  59. package/src/index.ts +66 -0
  60. package/src/message-parser.test.ts +207 -0
  61. package/src/message-parser.ts +54 -0
  62. package/src/session-updates.test.ts +265 -0
  63. package/src/session-updates.ts +87 -0
  64. package/src/session.ts +737 -0
  65. package/src/state.ts +287 -0
  66. package/src/terminal.ts +164 -0
  67. package/src/types.ts +88 -0
  68. package/src/ws-adapter.ts +245 -0
  69. package/tsconfig.json +22 -0
  70. package/vitest.config.ts +7 -0
  71. package/dist/chunk-5IPYOXBE.js +0 -32
  72. package/dist/chunk-5IPYOXBE.js.map +0 -1
  73. package/dist/chunk-C73RKCTS.js +0 -36
  74. package/dist/chunk-C73RKCTS.js.map +0 -1
  75. package/dist/chunk-VVXT4HQM.js +0 -779
  76. package/dist/chunk-VVXT4HQM.js.map +0 -1
  77. package/dist/lib/logger.d.ts +0 -24
  78. package/dist/lib/logger.js +0 -17
  79. package/dist/lib/logger.js.map +0 -1
  80. package/dist/lib/mentions.js +0 -7
  81. package/dist/lib/mentions.js.map +0 -1
  82. package/dist/streams/backends/index.d.ts +0 -88
  83. package/dist/streams/backends/index.js +0 -13
  84. package/dist/streams/backends/index.js.map +0 -1
  85. package/dist/streams/manager.d.ts +0 -55
  86. package/dist/streams/manager.js +0 -248
  87. package/dist/streams/manager.js.map +0 -1
  88. package/dist/types-DCRtrjjj.d.ts +0 -192
  89. package/scripts/fix-pty.cjs +0 -21
@@ -0,0 +1,245 @@
1
+ import { WebSocket, WebSocketServer } from 'ws';
2
+ import type { AgentSession, AgentSessionEvents } from './session.js';
3
+
4
+ /**
5
+ * WebSocket message from client
6
+ */
7
+ interface WsClientMessage {
8
+ type: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ /**
13
+ * Options for the WebSocket adapter
14
+ */
15
+ export interface WsAdapterOptions {
16
+ /**
17
+ * Send history on connect (default: true)
18
+ */
19
+ sendHistoryOnConnect?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Create a WebSocket adapter for an AgentSession.
24
+ *
25
+ * Bridges session events to WebSocket clients and handles incoming messages.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { AgentSession } from 'amux';
30
+ * import { createWsAdapter } from 'amux/ws';
31
+ * import { WebSocketServer } from 'ws';
32
+ *
33
+ * const session = new AgentSession({ ... });
34
+ * const wss = new WebSocketServer({ server, path: '/ws' });
35
+ *
36
+ * createWsAdapter(session, wss);
37
+ *
38
+ * await session.spawnAgent();
39
+ * ```
40
+ */
41
+ export function createWsAdapter(
42
+ session: AgentSession,
43
+ wss: WebSocketServer,
44
+ options: WsAdapterOptions = {}
45
+ ): {
46
+ /** Get connected client count */
47
+ clientCount: () => number;
48
+ /** Broadcast a custom message to all clients */
49
+ broadcast: (message: unknown) => void;
50
+ /** Close all connections */
51
+ close: () => void;
52
+ } {
53
+ const clients = new Set<WebSocket>();
54
+ const sendHistoryOnConnect = options.sendHistoryOnConnect ?? true;
55
+
56
+ // Helper to broadcast to all clients
57
+ function broadcast(message: unknown): void {
58
+ const data = JSON.stringify(message);
59
+ for (const client of clients) {
60
+ if (client.readyState === WebSocket.OPEN) {
61
+ client.send(data);
62
+ }
63
+ }
64
+ }
65
+
66
+ // Wire up session events to broadcast
67
+ const eventHandlers: { [K in keyof AgentSessionEvents]?: (data: AgentSessionEvents[K]) => void } = {
68
+ ready: (data) => broadcast({ type: 'ready', ...data }),
69
+ connecting: () => broadcast({ type: 'connecting' }),
70
+ update: (data) => broadcast({ type: 'session_update', update: data }),
71
+ turn_start: () => broadcast({ type: 'turn_start' }),
72
+ turn_end: () => broadcast({ type: 'turn_end' }),
73
+ permission_request: (data) => broadcast({ type: 'permission_request', ...data }),
74
+ prompt_complete: (data) => broadcast({ type: 'prompt_complete', ...data }),
75
+ session_created: (data) => broadcast({ type: 'session_created', ...data }),
76
+ session_switched: (data) => broadcast({ type: 'session_switched', ...data }),
77
+ history_replay: (data) => broadcast({ type: 'history_replay', ...data }),
78
+ error: (data) => broadcast({ type: 'error', ...data }),
79
+ agent_exit: (data) => broadcast({ type: 'agent_exit', ...data }),
80
+ };
81
+
82
+ // Register all event handlers
83
+ for (const [event, handler] of Object.entries(eventHandlers)) {
84
+ session.on(event as keyof AgentSessionEvents, handler as (data: unknown) => void);
85
+ }
86
+
87
+ // Handle WebSocket connections
88
+ wss.on('connection', (ws: WebSocket) => {
89
+ console.log('[amux-ws] Client connected');
90
+ clients.add(ws);
91
+
92
+ // Send current state to new client
93
+ if (session.isConnected) {
94
+ ws.send(JSON.stringify({
95
+ type: 'ready',
96
+ cwd: session.cwd,
97
+ sessionId: session.sessionId,
98
+ capabilities: session.agentCapabilities,
99
+ agent: session.getAgentInfo(),
100
+ availableAgents: session.getAvailableAgents(),
101
+ }));
102
+
103
+ // Send history to hydrate the chat UI
104
+ if (sendHistoryOnConnect) {
105
+ const history = session.loadHistory();
106
+ if (history.length > 0) {
107
+ ws.send(JSON.stringify({
108
+ type: 'history_replay',
109
+ previousSessionId: session.sessionId,
110
+ events: history,
111
+ eventCount: history.length,
112
+ }));
113
+ }
114
+ }
115
+ } else {
116
+ ws.send(JSON.stringify({ type: 'connecting' }));
117
+ }
118
+
119
+ // Handle incoming messages
120
+ ws.on('message', async (data: Buffer) => {
121
+ try {
122
+ const msg = JSON.parse(data.toString()) as WsClientMessage;
123
+ await handleMessage(ws, msg, session);
124
+ } catch (err) {
125
+ console.error('[amux-ws] Invalid message:', err);
126
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
127
+ }
128
+ });
129
+
130
+ ws.on('close', () => {
131
+ console.log('[amux-ws] Client disconnected');
132
+ clients.delete(ws);
133
+ });
134
+
135
+ ws.on('error', (err) => {
136
+ console.error('[amux-ws] WebSocket error:', err);
137
+ clients.delete(ws);
138
+ });
139
+ });
140
+
141
+ return {
142
+ clientCount: () => clients.size,
143
+ broadcast,
144
+ close: () => {
145
+ for (const client of clients) {
146
+ client.close();
147
+ }
148
+ clients.clear();
149
+ },
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Handle incoming WebSocket message
155
+ */
156
+ async function handleMessage(
157
+ ws: WebSocket,
158
+ msg: WsClientMessage,
159
+ session: AgentSession
160
+ ): Promise<void> {
161
+ switch (msg.type) {
162
+ case 'prompt':
163
+ if (!session.isConnected || !session.sessionId) {
164
+ ws.send(JSON.stringify({ type: 'error', message: 'Agent not ready' }));
165
+ return;
166
+ }
167
+ try {
168
+ await session.prompt(msg.message as string);
169
+ } catch {
170
+ // Error already emitted by session
171
+ }
172
+ break;
173
+
174
+ case 'cancel':
175
+ await session.cancel();
176
+ break;
177
+
178
+ case 'permission_response':
179
+ session.respondToPermission(msg.requestId as string, msg.optionId as string);
180
+ break;
181
+
182
+ case 'change_cwd':
183
+ try {
184
+ await session.changeCwd(msg.path as string);
185
+ } catch (err) {
186
+ ws.send(JSON.stringify({ type: 'error', message: (err as Error).message }));
187
+ }
188
+ break;
189
+
190
+ case 'new_session':
191
+ try {
192
+ await session.newSession();
193
+ } catch (err) {
194
+ ws.send(JSON.stringify({ type: 'error', message: (err as Error).message }));
195
+ }
196
+ break;
197
+
198
+ case 'set_mode':
199
+ try {
200
+ await session.setMode(msg.modeId as string);
201
+ } catch (err) {
202
+ ws.send(JSON.stringify({ type: 'error', message: (err as Error).message }));
203
+ }
204
+ break;
205
+
206
+ case 'set_model':
207
+ try {
208
+ await session.setModel(msg.modelId as string);
209
+ } catch (err) {
210
+ ws.send(JSON.stringify({ type: 'error', message: (err as Error).message }));
211
+ }
212
+ break;
213
+
214
+ case 'change_agent':
215
+ try {
216
+ await session.changeAgent(msg.agentType as string);
217
+ } catch (err) {
218
+ ws.send(JSON.stringify({ type: 'error', message: (err as Error).message }));
219
+ }
220
+ break;
221
+
222
+ case 'get_history': {
223
+ const history = session.loadHistory();
224
+ ws.send(JSON.stringify({ type: 'history', events: history, sessionId: session.sessionId }));
225
+ break;
226
+ }
227
+
228
+ case 'list_sessions': {
229
+ const sessions = session.listSessions();
230
+ ws.send(JSON.stringify({ type: 'sessions', sessions }));
231
+ break;
232
+ }
233
+
234
+ case 'switch_session':
235
+ try {
236
+ await session.switchSession(msg.sessionId as string);
237
+ } catch (err) {
238
+ ws.send(JSON.stringify({ type: 'error', message: (err as Error).message }));
239
+ }
240
+ break;
241
+
242
+ default:
243
+ ws.send(JSON.stringify({ type: 'error', message: `Unknown message type: ${msg.type}` }));
244
+ }
245
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "noUncheckedIndexedAccess": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
22
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts'],
6
+ },
7
+ });
@@ -1,32 +0,0 @@
1
- // src/lib/logger.ts
2
- var verbose = false;
3
- function setVerbose(v) {
4
- verbose = v;
5
- }
6
- function isVerbose() {
7
- return verbose;
8
- }
9
- function debug(tag, message) {
10
- if (verbose) {
11
- console.log(`[${tag}] ${message}`);
12
- }
13
- }
14
- function info(tag, message) {
15
- console.log(`[${tag}] ${message}`);
16
- }
17
- function warn(tag, message) {
18
- console.warn(`[${tag}] ${message}`);
19
- }
20
- function error(tag, message) {
21
- console.error(`[${tag}] ${message}`);
22
- }
23
-
24
- export {
25
- setVerbose,
26
- isVerbose,
27
- debug,
28
- info,
29
- warn,
30
- error
31
- };
32
- //# sourceMappingURL=chunk-5IPYOXBE.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/logger.ts"],"sourcesContent":["/**\n * Simple logger with verbose mode toggle.\n * In production, debug logs are suppressed unless verbose mode is enabled.\n */\n\nlet verbose = false;\n\nexport function setVerbose(v: boolean) {\n verbose = v;\n}\n\nexport function isVerbose(): boolean {\n return verbose;\n}\n\n/**\n * Log a debug message. Only shown when verbose mode is enabled.\n */\nexport function debug(tag: string, message: string) {\n if (verbose) {\n console.log(`[${tag}] ${message}`);\n }\n}\n\n/**\n * Log an info message. Always shown.\n */\nexport function info(tag: string, message: string) {\n console.log(`[${tag}] ${message}`);\n}\n\n/**\n * Log a warning. Always shown.\n */\nexport function warn(tag: string, message: string) {\n console.warn(`[${tag}] ${message}`);\n}\n\n/**\n * Log an error. Always shown.\n */\nexport function error(tag: string, message: string) {\n console.error(`[${tag}] ${message}`);\n}\n"],"mappings":";AAKA,IAAI,UAAU;AAEP,SAAS,WAAW,GAAY;AACrC,YAAU;AACZ;AAEO,SAAS,YAAqB;AACnC,SAAO;AACT;AAKO,SAAS,MAAM,KAAa,SAAiB;AAClD,MAAI,SAAS;AACX,YAAQ,IAAI,IAAI,GAAG,KAAK,OAAO,EAAE;AAAA,EACnC;AACF;AAKO,SAAS,KAAK,KAAa,SAAiB;AACjD,UAAQ,IAAI,IAAI,GAAG,KAAK,OAAO,EAAE;AACnC;AAKO,SAAS,KAAK,KAAa,SAAiB;AACjD,UAAQ,KAAK,IAAI,GAAG,KAAK,OAAO,EAAE;AACpC;AAKO,SAAS,MAAM,KAAa,SAAiB;AAClD,UAAQ,MAAM,IAAI,GAAG,KAAK,OAAO,EAAE;AACrC;","names":[]}
@@ -1,36 +0,0 @@
1
- // src/lib/mentions.ts
2
- import path from "path";
3
- function parseMessageToContentBlocks(message, workingDir) {
4
- const blocks = [];
5
- const mentionRegex = /@(\.{0,2}[\w\/\.\-]+)/g;
6
- let lastIndex = 0;
7
- for (const match of message.matchAll(mentionRegex)) {
8
- if (match.index > lastIndex) {
9
- const text = message.slice(lastIndex, match.index);
10
- if (text.trim()) {
11
- blocks.push({ type: "text", text });
12
- }
13
- }
14
- const relativePath = match[1];
15
- const absolutePath = path.resolve(workingDir, relativePath);
16
- blocks.push({
17
- type: "resource_link",
18
- uri: `file://${absolutePath}`,
19
- name: relativePath
20
- // Required per ACP spec - use the path the user typed
21
- });
22
- lastIndex = match.index + match[0].length;
23
- }
24
- if (lastIndex < message.length) {
25
- const text = message.slice(lastIndex);
26
- if (text.trim()) {
27
- blocks.push({ type: "text", text });
28
- }
29
- }
30
- return blocks.length > 0 ? blocks : [{ type: "text", text: message }];
31
- }
32
-
33
- export {
34
- parseMessageToContentBlocks
35
- };
36
- //# sourceMappingURL=chunk-C73RKCTS.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/mentions.ts"],"sourcesContent":["import type { ContentBlock } from '../types.js';\nimport path from 'path';\n\n/**\n * Parse message text with @mentions into ContentBlock array.\n * @mentions become resource_link blocks, other text becomes text blocks.\n *\n * Examples:\n * - \"hello world\" → [{ type: 'text', text: 'hello world' }]\n * - \"@src/foo.ts\" → [{ type: 'resource_link', uri: 'file://...', name: 'src/foo.ts' }]\n * - \"check @src/foo.ts for bugs\" → text + resource_link + text\n */\nexport function parseMessageToContentBlocks(\n message: string,\n workingDir: string\n): ContentBlock[] {\n const blocks: ContentBlock[] = [];\n // Match @path/to/file.ts, @./local.ts, @../parent/file.ts\n // Allows leading ./ or ../ followed by path characters\n const mentionRegex = /@(\\.{0,2}[\\w\\/\\.\\-]+)/g;\n\n let lastIndex = 0;\n for (const match of message.matchAll(mentionRegex)) {\n // Add text before mention\n if (match.index! > lastIndex) {\n const text = message.slice(lastIndex, match.index);\n if (text.trim()) {\n blocks.push({ type: 'text', text });\n }\n }\n\n // Add resource_link for mention\n const relativePath = match[1];\n const absolutePath = path.resolve(workingDir, relativePath);\n blocks.push({\n type: 'resource_link',\n uri: `file://${absolutePath}`,\n name: relativePath, // Required per ACP spec - use the path the user typed\n });\n\n lastIndex = match.index! + match[0].length;\n }\n\n // Add remaining text\n if (lastIndex < message.length) {\n const text = message.slice(lastIndex);\n if (text.trim()) {\n blocks.push({ type: 'text', text });\n }\n }\n\n // If no mentions found, return single text block\n return blocks.length > 0 ? blocks : [{ type: 'text', text: message }];\n}\n"],"mappings":";AACA,OAAO,UAAU;AAWV,SAAS,4BACd,SACA,YACgB;AAChB,QAAM,SAAyB,CAAC;AAGhC,QAAM,eAAe;AAErB,MAAI,YAAY;AAChB,aAAW,SAAS,QAAQ,SAAS,YAAY,GAAG;AAElD,QAAI,MAAM,QAAS,WAAW;AAC5B,YAAM,OAAO,QAAQ,MAAM,WAAW,MAAM,KAAK;AACjD,UAAI,KAAK,KAAK,GAAG;AACf,eAAO,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;AAAA,MACpC;AAAA,IACF;AAGA,UAAM,eAAe,MAAM,CAAC;AAC5B,UAAM,eAAe,KAAK,QAAQ,YAAY,YAAY;AAC1D,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,KAAK,UAAU,YAAY;AAAA,MAC3B,MAAM;AAAA;AAAA,IACR,CAAC;AAED,gBAAY,MAAM,QAAS,MAAM,CAAC,EAAE;AAAA,EACtC;AAGA,MAAI,YAAY,QAAQ,QAAQ;AAC9B,UAAM,OAAO,QAAQ,MAAM,SAAS;AACpC,QAAI,KAAK,KAAK,GAAG;AACf,aAAO,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;AAAA,IACpC;AAAA,EACF;AAGA,SAAO,OAAO,SAAS,IAAI,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC;AACtE;","names":[]}