@bytespell/amux 0.0.4 → 0.0.7
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/chunk-C73RKCTS.js +36 -0
- package/dist/chunk-C73RKCTS.js.map +1 -0
- package/dist/chunk-HJCRMFTD.js +771 -0
- package/dist/chunk-HJCRMFTD.js.map +1 -0
- package/dist/lib/logger.d.ts +24 -0
- package/dist/{src/lib → lib}/logger.js +1 -2
- package/dist/lib/mentions.d.ts +14 -0
- package/dist/lib/mentions.js +7 -0
- package/dist/streams/backends/index.d.ts +88 -0
- package/dist/streams/backends/index.js +13 -0
- package/dist/streams/manager.d.ts +49 -0
- package/dist/streams/manager.js +237 -0
- package/dist/streams/manager.js.map +1 -0
- package/dist/types-DoG5bt6C.d.ts +153 -0
- package/dist/types.d.ts +2 -0
- package/dist/{chunk-226DBKL3.js → types.js} +7 -8
- package/dist/types.js.map +1 -0
- package/package.json +10 -37
- package/scripts/fix-pty.cjs +21 -0
- package/dist/bin/cli.js +0 -28
- package/dist/bin/cli.js.map +0 -1
- package/dist/chunk-226DBKL3.js.map +0 -1
- package/dist/chunk-2NON2HR2.js +0 -1602
- package/dist/chunk-2NON2HR2.js.map +0 -1
- package/dist/chunk-L4DBPVMA.js +0 -122
- package/dist/chunk-L4DBPVMA.js.map +0 -1
- package/dist/chunk-OQ5K5ON2.js +0 -319
- package/dist/chunk-OQ5K5ON2.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -10
- package/dist/chunk-SX7NC3ZM.js +0 -65
- package/dist/chunk-SX7NC3ZM.js.map +0 -1
- package/dist/chunk-YYN3GXYP.js +0 -333
- package/dist/chunk-YYN3GXYP.js.map +0 -1
- package/dist/src/agents/eventStore.js +0 -22
- package/dist/src/agents/eventStore.js.map +0 -1
- package/dist/src/agents/manager.js +0 -12
- package/dist/src/agents/manager.js.map +0 -1
- package/dist/src/db/index.js +0 -10
- package/dist/src/server.js +0 -16
- package/dist/src/server.js.map +0 -1
- package/dist/src/trpc/files.js +0 -11
- package/dist/src/trpc/files.js.map +0 -1
- package/dist/src/types.js +0 -12
- package/dist/src/types.js.map +0 -1
- /package/dist/{src/lib → lib}/logger.js.map +0 -0
- /package/dist/{chunk-PZ5AY32C.js.map → lib/mentions.js.map} +0 -0
- /package/dist/{src/db → streams/backends}/index.js.map +0 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import {
|
|
2
|
+
debug,
|
|
3
|
+
warn
|
|
4
|
+
} from "./chunk-5IPYOXBE.js";
|
|
5
|
+
import {
|
|
6
|
+
parseMessageToContentBlocks
|
|
7
|
+
} from "./chunk-C73RKCTS.js";
|
|
8
|
+
|
|
9
|
+
// src/streams/backends/acp.ts
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
12
|
+
import { Readable, Writable } from "stream";
|
|
13
|
+
import * as fs from "fs/promises";
|
|
14
|
+
import * as fsSync from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import {
|
|
17
|
+
ClientSideConnection,
|
|
18
|
+
ndJsonStream
|
|
19
|
+
} from "@agentclientprotocol/sdk";
|
|
20
|
+
|
|
21
|
+
// src/streams/process.ts
|
|
22
|
+
import { execa } from "execa";
|
|
23
|
+
import treeKill from "tree-kill";
|
|
24
|
+
function spawn(options) {
|
|
25
|
+
const { command, args = [], cwd, env, timeoutMs } = options;
|
|
26
|
+
const subprocess = execa(command, args, {
|
|
27
|
+
cwd,
|
|
28
|
+
env: { ...process.env, ...env },
|
|
29
|
+
stdin: "pipe",
|
|
30
|
+
stdout: "pipe",
|
|
31
|
+
stderr: "pipe",
|
|
32
|
+
timeout: timeoutMs,
|
|
33
|
+
cleanup: true,
|
|
34
|
+
// Kill on parent exit
|
|
35
|
+
windowsHide: true
|
|
36
|
+
// Hide console window on Windows
|
|
37
|
+
});
|
|
38
|
+
const pid = subprocess.pid;
|
|
39
|
+
if (!pid) {
|
|
40
|
+
throw new Error(`Failed to spawn process: ${command}`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
pid,
|
|
44
|
+
stdin: subprocess.stdin,
|
|
45
|
+
stdout: subprocess.stdout,
|
|
46
|
+
stderr: subprocess.stderr,
|
|
47
|
+
kill: (signal = "SIGTERM") => killTree(pid, signal),
|
|
48
|
+
wait: async () => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await subprocess;
|
|
51
|
+
return { exitCode: result.exitCode };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return { exitCode: err.exitCode ?? 1 };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function killTree(pid, signal) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
treeKill(pid, signal, (err) => {
|
|
61
|
+
if (err && !err.message.includes("No such process")) {
|
|
62
|
+
console.error(`[process] kill error pid=${pid}:`, err.message);
|
|
63
|
+
}
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/streams/backends/acp.ts
|
|
70
|
+
var INIT_TIMEOUT_MS = 9e4;
|
|
71
|
+
function normalizeSessionUpdate(update) {
|
|
72
|
+
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
|
73
|
+
return update;
|
|
74
|
+
}
|
|
75
|
+
const content = update.content;
|
|
76
|
+
if (!content || !Array.isArray(content)) {
|
|
77
|
+
return update;
|
|
78
|
+
}
|
|
79
|
+
const normalizedContent = content.map((item) => {
|
|
80
|
+
if (item.type !== "diff") return item;
|
|
81
|
+
if (typeof item.content === "string") return item;
|
|
82
|
+
const newText = item.newText;
|
|
83
|
+
const oldText = item.oldText;
|
|
84
|
+
const path3 = item.path;
|
|
85
|
+
if (newText === void 0) return item;
|
|
86
|
+
const filePath = path3 ?? "file";
|
|
87
|
+
const oldLines = oldText ? oldText.split("\n") : [];
|
|
88
|
+
const newLines = newText.split("\n");
|
|
89
|
+
let unifiedDiff = `Index: ${filePath}
|
|
90
|
+
`;
|
|
91
|
+
unifiedDiff += "===================================================================\n";
|
|
92
|
+
unifiedDiff += `--- ${filePath}
|
|
93
|
+
`;
|
|
94
|
+
unifiedDiff += `+++ ${filePath}
|
|
95
|
+
`;
|
|
96
|
+
unifiedDiff += `@@ -${oldLines.length > 0 ? 1 : 0},${oldLines.length} +1,${newLines.length} @@
|
|
97
|
+
`;
|
|
98
|
+
for (const line of oldLines) {
|
|
99
|
+
unifiedDiff += `-${line}
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
for (const line of newLines) {
|
|
103
|
+
unifiedDiff += `+${line}
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
type: "diff",
|
|
108
|
+
content: unifiedDiff
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
...update,
|
|
113
|
+
content: normalizedContent
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function withTimeout(promise, ms, operation) {
|
|
117
|
+
return Promise.race([
|
|
118
|
+
promise,
|
|
119
|
+
new Promise(
|
|
120
|
+
(_, reject) => setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms)
|
|
121
|
+
)
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
var AcpDriver = class {
|
|
125
|
+
type = "acp";
|
|
126
|
+
streamType = "acp";
|
|
127
|
+
instances = /* @__PURE__ */ new Map();
|
|
128
|
+
getHistoryPath(storageDir) {
|
|
129
|
+
return path.join(storageDir, "history.json");
|
|
130
|
+
}
|
|
131
|
+
storeEvent(storageDir, update) {
|
|
132
|
+
const isSessionUpdate = typeof update === "object" && update !== null && "sessionUpdate" in update;
|
|
133
|
+
const isAmuxEvent = typeof update === "object" && update !== null && "amuxEvent" in update;
|
|
134
|
+
const isTurnMarker = isAmuxEvent && (update.amuxEvent === "turn_start" || update.amuxEvent === "turn_end");
|
|
135
|
+
if (!isSessionUpdate && !isTurnMarker) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!fsSync.existsSync(storageDir)) {
|
|
139
|
+
fsSync.mkdirSync(storageDir, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
const historyPath = this.getHistoryPath(storageDir);
|
|
142
|
+
let events = [];
|
|
143
|
+
try {
|
|
144
|
+
if (fsSync.existsSync(historyPath)) {
|
|
145
|
+
const data = fsSync.readFileSync(historyPath, "utf-8");
|
|
146
|
+
events = JSON.parse(data);
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
events = [];
|
|
150
|
+
}
|
|
151
|
+
events.push(update);
|
|
152
|
+
fsSync.writeFileSync(historyPath, JSON.stringify(events));
|
|
153
|
+
}
|
|
154
|
+
async start(streamId, config, cwd, backendState, emit, storageDir) {
|
|
155
|
+
const existingState = backendState;
|
|
156
|
+
const existingAcpSessionId = existingState?.acpSessionId ?? null;
|
|
157
|
+
if (this.instances.has(streamId)) {
|
|
158
|
+
await this.stop(streamId);
|
|
159
|
+
}
|
|
160
|
+
const args = config.args ?? [];
|
|
161
|
+
const env = config.env ?? {};
|
|
162
|
+
debug("acp", ` Spawning: ${config.command} ${args.join(" ")} in ${cwd}`);
|
|
163
|
+
const proc = spawn({
|
|
164
|
+
command: config.command,
|
|
165
|
+
args,
|
|
166
|
+
cwd,
|
|
167
|
+
env
|
|
168
|
+
});
|
|
169
|
+
proc.stderr.on("data", (data) => {
|
|
170
|
+
debug("acp", `[stderr] ${data.toString().trim()}`);
|
|
171
|
+
});
|
|
172
|
+
const storingEmit = (update) => {
|
|
173
|
+
this.storeEvent(storageDir, update);
|
|
174
|
+
emit(update);
|
|
175
|
+
};
|
|
176
|
+
const instance = {
|
|
177
|
+
process: proc,
|
|
178
|
+
connection: null,
|
|
179
|
+
acpSessionId: "",
|
|
180
|
+
pendingPermission: null,
|
|
181
|
+
permissionCallbacks: /* @__PURE__ */ new Map(),
|
|
182
|
+
emit: storingEmit,
|
|
183
|
+
terminals: /* @__PURE__ */ new Map(),
|
|
184
|
+
storageDir
|
|
185
|
+
};
|
|
186
|
+
proc.wait().then(({ exitCode }) => {
|
|
187
|
+
if (this.instances.has(streamId)) {
|
|
188
|
+
emit({ amuxEvent: "error", message: `Agent process exited with code ${exitCode}` });
|
|
189
|
+
this.instances.delete(streamId);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
const input = Writable.toWeb(proc.stdin);
|
|
194
|
+
const output = Readable.toWeb(proc.stdout);
|
|
195
|
+
const stream = ndJsonStream(input, output);
|
|
196
|
+
const client = this.createClient(streamId, instance);
|
|
197
|
+
instance.connection = new ClientSideConnection(() => client, stream);
|
|
198
|
+
const initResult = await withTimeout(
|
|
199
|
+
instance.connection.initialize({
|
|
200
|
+
protocolVersion: 1,
|
|
201
|
+
clientCapabilities: {
|
|
202
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
203
|
+
terminal: true
|
|
204
|
+
}
|
|
205
|
+
}),
|
|
206
|
+
INIT_TIMEOUT_MS,
|
|
207
|
+
"Agent initialization"
|
|
208
|
+
);
|
|
209
|
+
debug("acp", ` Initialized agent: ${initResult.agentInfo?.name} v${initResult.agentInfo?.version}`);
|
|
210
|
+
const canResume = initResult.agentCapabilities?.sessionCapabilities?.resume !== void 0;
|
|
211
|
+
let acpSessionId;
|
|
212
|
+
let sessionResult;
|
|
213
|
+
if (existingAcpSessionId && canResume) {
|
|
214
|
+
let resumeSucceeded = false;
|
|
215
|
+
try {
|
|
216
|
+
debug("acp", ` Resuming ACP session ${existingAcpSessionId}...`);
|
|
217
|
+
sessionResult = await withTimeout(
|
|
218
|
+
instance.connection.unstable_resumeSession({
|
|
219
|
+
sessionId: existingAcpSessionId,
|
|
220
|
+
cwd,
|
|
221
|
+
mcpServers: []
|
|
222
|
+
}),
|
|
223
|
+
INIT_TIMEOUT_MS,
|
|
224
|
+
"Session resume"
|
|
225
|
+
);
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
227
|
+
acpSessionId = existingAcpSessionId;
|
|
228
|
+
debug("acp", ` ACP session resumed successfully`);
|
|
229
|
+
resumeSucceeded = true;
|
|
230
|
+
} catch (resumeErr) {
|
|
231
|
+
debug("acp", ` Resume failed, creating new session: ${resumeErr}`);
|
|
232
|
+
}
|
|
233
|
+
if (!resumeSucceeded) {
|
|
234
|
+
sessionResult = await withTimeout(
|
|
235
|
+
instance.connection.newSession({ cwd, mcpServers: [] }),
|
|
236
|
+
INIT_TIMEOUT_MS,
|
|
237
|
+
"New session creation"
|
|
238
|
+
);
|
|
239
|
+
acpSessionId = sessionResult.sessionId;
|
|
240
|
+
debug("acp", ` New ACP session created: ${acpSessionId}`);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
debug("acp", ` Creating new ACP session in ${cwd}...`);
|
|
244
|
+
sessionResult = await withTimeout(
|
|
245
|
+
instance.connection.newSession({ cwd, mcpServers: [] }),
|
|
246
|
+
INIT_TIMEOUT_MS,
|
|
247
|
+
"New session creation"
|
|
248
|
+
);
|
|
249
|
+
acpSessionId = sessionResult.sessionId;
|
|
250
|
+
debug("acp", ` ACP session created: ${acpSessionId}`);
|
|
251
|
+
}
|
|
252
|
+
instance.acpSessionId = acpSessionId;
|
|
253
|
+
this.instances.set(streamId, instance);
|
|
254
|
+
const models = sessionResult?.models?.availableModels;
|
|
255
|
+
const modes = sessionResult?.modes?.availableModes;
|
|
256
|
+
if (sessionResult?.modes) {
|
|
257
|
+
emit({
|
|
258
|
+
sessionUpdate: "current_mode_update",
|
|
259
|
+
currentModeId: sessionResult.modes.currentModeId
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
acpSessionId,
|
|
264
|
+
models,
|
|
265
|
+
modes
|
|
266
|
+
};
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error(`[acp] Error starting agent for stream ${streamId}:`, err);
|
|
269
|
+
await proc.kill();
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
createClient(_streamId, instance) {
|
|
274
|
+
return {
|
|
275
|
+
async requestPermission(params) {
|
|
276
|
+
const requestId = randomUUID();
|
|
277
|
+
const permission = {
|
|
278
|
+
requestId,
|
|
279
|
+
toolCallId: params.toolCall.toolCallId,
|
|
280
|
+
title: params.toolCall.title ?? "Permission Required",
|
|
281
|
+
options: params.options.map((o) => ({
|
|
282
|
+
optionId: o.optionId,
|
|
283
|
+
name: o.name,
|
|
284
|
+
kind: o.kind
|
|
285
|
+
}))
|
|
286
|
+
};
|
|
287
|
+
instance.pendingPermission = permission;
|
|
288
|
+
instance.emit({ amuxEvent: "permission_request", permission });
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
instance.permissionCallbacks.set(requestId, {
|
|
291
|
+
resolve: (optionId) => {
|
|
292
|
+
instance.pendingPermission = null;
|
|
293
|
+
instance.emit({ amuxEvent: "permission_cleared" });
|
|
294
|
+
resolve({ outcome: { outcome: "selected", optionId } });
|
|
295
|
+
},
|
|
296
|
+
reject
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
async sessionUpdate(params) {
|
|
301
|
+
debug("acp", ` sessionUpdate received: ${JSON.stringify(params)}`);
|
|
302
|
+
const normalized = normalizeSessionUpdate(params.update);
|
|
303
|
+
instance.emit(normalized);
|
|
304
|
+
},
|
|
305
|
+
async readTextFile(params) {
|
|
306
|
+
const content = await fs.readFile(params.path, "utf-8");
|
|
307
|
+
return { content };
|
|
308
|
+
},
|
|
309
|
+
async writeTextFile(params) {
|
|
310
|
+
await fs.writeFile(params.path, params.content);
|
|
311
|
+
return {};
|
|
312
|
+
},
|
|
313
|
+
async createTerminal(params) {
|
|
314
|
+
debug("acp", ` createTerminal request: ${JSON.stringify(params)}`);
|
|
315
|
+
const terminalId = randomUUID();
|
|
316
|
+
const outputByteLimit = params.outputByteLimit ?? 1024 * 1024;
|
|
317
|
+
const termProc = nodeSpawn(params.command, params.args ?? [], {
|
|
318
|
+
cwd: params.cwd ?? void 0,
|
|
319
|
+
env: params.env ? { ...process.env, ...Object.fromEntries(params.env.map((e) => [e.name, e.value])) } : process.env,
|
|
320
|
+
shell: true,
|
|
321
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
322
|
+
});
|
|
323
|
+
const terminal = {
|
|
324
|
+
process: termProc,
|
|
325
|
+
output: "",
|
|
326
|
+
exitCode: null,
|
|
327
|
+
signal: null,
|
|
328
|
+
truncated: false,
|
|
329
|
+
outputByteLimit
|
|
330
|
+
};
|
|
331
|
+
const appendOutput = (data) => {
|
|
332
|
+
terminal.output += data.toString();
|
|
333
|
+
if (terminal.output.length > terminal.outputByteLimit) {
|
|
334
|
+
terminal.output = terminal.output.slice(-terminal.outputByteLimit);
|
|
335
|
+
terminal.truncated = true;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
termProc.stdout?.on("data", appendOutput);
|
|
339
|
+
termProc.stderr?.on("data", appendOutput);
|
|
340
|
+
termProc.on("exit", (code, signal) => {
|
|
341
|
+
debug("acp", ` Terminal ${terminalId} exited with code ${code}, signal ${signal}`);
|
|
342
|
+
terminal.exitCode = code ?? null;
|
|
343
|
+
terminal.signal = signal ?? null;
|
|
344
|
+
});
|
|
345
|
+
termProc.on("error", (err) => {
|
|
346
|
+
console.error(`[acp] Terminal ${terminalId} error:`, err.message);
|
|
347
|
+
terminal.output += `
|
|
348
|
+
Error: ${err.message}`;
|
|
349
|
+
terminal.exitCode = -1;
|
|
350
|
+
});
|
|
351
|
+
instance.terminals.set(terminalId, terminal);
|
|
352
|
+
debug("acp", ` Created terminal ${terminalId} for command: ${params.command}`);
|
|
353
|
+
return { terminalId };
|
|
354
|
+
},
|
|
355
|
+
async terminalOutput(params) {
|
|
356
|
+
debug("acp", ` terminalOutput request for terminal ${params.terminalId}`);
|
|
357
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
358
|
+
if (!terminal) {
|
|
359
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
output: terminal.output,
|
|
363
|
+
truncated: terminal.truncated,
|
|
364
|
+
exitStatus: terminal.exitCode !== null || terminal.signal !== null ? {
|
|
365
|
+
exitCode: terminal.exitCode,
|
|
366
|
+
signal: terminal.signal
|
|
367
|
+
} : void 0
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
async waitForTerminalExit(params) {
|
|
371
|
+
debug("acp", ` waitForTerminalExit request for terminal ${params.terminalId}`);
|
|
372
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
373
|
+
if (!terminal) {
|
|
374
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
375
|
+
}
|
|
376
|
+
if (terminal.exitCode !== null || terminal.signal !== null) {
|
|
377
|
+
return {
|
|
378
|
+
exitCode: terminal.exitCode,
|
|
379
|
+
signal: terminal.signal
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return new Promise((resolve) => {
|
|
383
|
+
terminal.process.on("exit", (code, signal) => {
|
|
384
|
+
resolve({
|
|
385
|
+
exitCode: code ?? null,
|
|
386
|
+
signal: signal ?? null
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
},
|
|
391
|
+
// Note: killTerminalCommand not in SDK Client interface yet, but we implement handlers
|
|
392
|
+
// for completeness when the SDK adds support
|
|
393
|
+
async killTerminal(params) {
|
|
394
|
+
debug("acp", ` killTerminal request for terminal ${params.terminalId}`);
|
|
395
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
396
|
+
if (!terminal) {
|
|
397
|
+
throw new Error(`Terminal ${params.terminalId} not found`);
|
|
398
|
+
}
|
|
399
|
+
terminal.process.kill("SIGTERM");
|
|
400
|
+
return {};
|
|
401
|
+
},
|
|
402
|
+
async releaseTerminal(params) {
|
|
403
|
+
debug("acp", ` releaseTerminal request for terminal ${params.terminalId}`);
|
|
404
|
+
const terminal = instance.terminals.get(params.terminalId);
|
|
405
|
+
if (terminal) {
|
|
406
|
+
if (terminal.exitCode === null) {
|
|
407
|
+
terminal.process.kill("SIGKILL");
|
|
408
|
+
}
|
|
409
|
+
instance.terminals.delete(params.terminalId);
|
|
410
|
+
}
|
|
411
|
+
return {};
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async input(streamId, raw, cwd, _emit) {
|
|
416
|
+
const instance = this.instances.get(streamId);
|
|
417
|
+
if (!instance) throw new Error(`No ACP instance for stream ${streamId}`);
|
|
418
|
+
this.storeEvent(instance.storageDir, {
|
|
419
|
+
sessionUpdate: "user_message_chunk",
|
|
420
|
+
content: { type: "text", text: raw }
|
|
421
|
+
});
|
|
422
|
+
const content = parseMessageToContentBlocks(raw, cwd);
|
|
423
|
+
debug("acp", ` Sending input to session ${instance.acpSessionId} with ${content.length} content block(s)...`);
|
|
424
|
+
const result = await instance.connection.prompt({
|
|
425
|
+
sessionId: instance.acpSessionId,
|
|
426
|
+
prompt: content
|
|
427
|
+
});
|
|
428
|
+
debug("acp", ` Input complete, stopReason: ${result.stopReason}`);
|
|
429
|
+
}
|
|
430
|
+
async stop(streamId) {
|
|
431
|
+
const instance = this.instances.get(streamId);
|
|
432
|
+
if (!instance) return;
|
|
433
|
+
for (const [, callback] of instance.permissionCallbacks) {
|
|
434
|
+
callback.reject(new Error("Agent stopped"));
|
|
435
|
+
}
|
|
436
|
+
instance.permissionCallbacks.clear();
|
|
437
|
+
instance.pendingPermission = null;
|
|
438
|
+
this.instances.delete(streamId);
|
|
439
|
+
await instance.process.kill();
|
|
440
|
+
}
|
|
441
|
+
async stopAll() {
|
|
442
|
+
const streamIds = [...this.instances.keys()];
|
|
443
|
+
await Promise.all(streamIds.map((id) => this.stop(id)));
|
|
444
|
+
}
|
|
445
|
+
isRunning(streamId) {
|
|
446
|
+
return this.instances.has(streamId);
|
|
447
|
+
}
|
|
448
|
+
/** Get backend state for persistence (ACP session ID for resumption) */
|
|
449
|
+
getState(streamId) {
|
|
450
|
+
const instance = this.instances.get(streamId);
|
|
451
|
+
if (!instance) return void 0;
|
|
452
|
+
return { acpSessionId: instance.acpSessionId };
|
|
453
|
+
}
|
|
454
|
+
/** Get stored events for replay on reconnect */
|
|
455
|
+
getReplayData(streamId) {
|
|
456
|
+
const instance = this.instances.get(streamId);
|
|
457
|
+
if (!instance) return void 0;
|
|
458
|
+
const historyPath = this.getHistoryPath(instance.storageDir);
|
|
459
|
+
try {
|
|
460
|
+
if (fsSync.existsSync(historyPath)) {
|
|
461
|
+
const data = fsSync.readFileSync(historyPath, "utf-8");
|
|
462
|
+
return JSON.parse(data);
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
/** Clear stored events for a stream */
|
|
469
|
+
clearHistory(streamId) {
|
|
470
|
+
const instance = this.instances.get(streamId);
|
|
471
|
+
if (!instance) return;
|
|
472
|
+
const historyPath = this.getHistoryPath(instance.storageDir);
|
|
473
|
+
try {
|
|
474
|
+
if (fsSync.existsSync(historyPath)) {
|
|
475
|
+
fsSync.unlinkSync(historyPath);
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
respondToPermission(streamId, requestId, optionId) {
|
|
481
|
+
const instance = this.instances.get(streamId);
|
|
482
|
+
const callback = instance?.permissionCallbacks.get(requestId);
|
|
483
|
+
if (!callback) {
|
|
484
|
+
throw new Error(`No pending permission request ${requestId}`);
|
|
485
|
+
}
|
|
486
|
+
callback.resolve(optionId);
|
|
487
|
+
instance.permissionCallbacks.delete(requestId);
|
|
488
|
+
}
|
|
489
|
+
getPendingPermission(streamId) {
|
|
490
|
+
const instance = this.instances.get(streamId);
|
|
491
|
+
return instance?.pendingPermission ?? null;
|
|
492
|
+
}
|
|
493
|
+
async cancel(streamId) {
|
|
494
|
+
const instance = this.instances.get(streamId);
|
|
495
|
+
if (!instance) return;
|
|
496
|
+
await instance.connection.cancel({ sessionId: instance.acpSessionId });
|
|
497
|
+
}
|
|
498
|
+
async setMode(streamId, modeId) {
|
|
499
|
+
const instance = this.instances.get(streamId);
|
|
500
|
+
if (!instance) throw new Error(`No ACP instance for stream ${streamId}`);
|
|
501
|
+
await instance.connection.setSessionMode({
|
|
502
|
+
sessionId: instance.acpSessionId,
|
|
503
|
+
modeId
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async setModel(streamId, modelId) {
|
|
507
|
+
const instance = this.instances.get(streamId);
|
|
508
|
+
if (!instance) throw new Error(`No ACP instance for stream ${streamId}`);
|
|
509
|
+
await instance.connection.unstable_setSessionModel({
|
|
510
|
+
sessionId: instance.acpSessionId,
|
|
511
|
+
modelId
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/streams/backends/mock.ts
|
|
517
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
518
|
+
function delay(ms) {
|
|
519
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
520
|
+
}
|
|
521
|
+
var MockDriver = class {
|
|
522
|
+
type = "mock";
|
|
523
|
+
streamType = "acp";
|
|
524
|
+
running = /* @__PURE__ */ new Set();
|
|
525
|
+
async start(streamId, _config, _cwd, _backendState, emit, _storageDir) {
|
|
526
|
+
debug("mock", ` Starting mock agent for stream ${streamId}`);
|
|
527
|
+
emit({
|
|
528
|
+
sessionUpdate: "current_mode_update",
|
|
529
|
+
currentModeId: "mock"
|
|
530
|
+
});
|
|
531
|
+
debug("mock", ` Mock agent ready for stream ${streamId}`);
|
|
532
|
+
this.running.add(streamId);
|
|
533
|
+
return {
|
|
534
|
+
acpSessionId: randomUUID2(),
|
|
535
|
+
models: [{ modelId: "mock-model", name: "Mock Model" }],
|
|
536
|
+
modes: [{ id: "mock", name: "Mock Mode" }]
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async input(streamId, raw, _cwd, emit) {
|
|
540
|
+
debug("mock", ` Mock input for stream ${streamId}: "${raw.slice(0, 50)}..."`);
|
|
541
|
+
const words = [
|
|
542
|
+
"This",
|
|
543
|
+
"is",
|
|
544
|
+
"a",
|
|
545
|
+
"mock",
|
|
546
|
+
"response",
|
|
547
|
+
"from",
|
|
548
|
+
"the",
|
|
549
|
+
"mock",
|
|
550
|
+
"agent.",
|
|
551
|
+
"It",
|
|
552
|
+
"simulates",
|
|
553
|
+
"streaming",
|
|
554
|
+
"text",
|
|
555
|
+
"chunks",
|
|
556
|
+
"for",
|
|
557
|
+
"performance",
|
|
558
|
+
"testing.",
|
|
559
|
+
"The",
|
|
560
|
+
"response",
|
|
561
|
+
"arrives",
|
|
562
|
+
"in",
|
|
563
|
+
"small",
|
|
564
|
+
"pieces",
|
|
565
|
+
"to",
|
|
566
|
+
"test",
|
|
567
|
+
"UI",
|
|
568
|
+
"rendering."
|
|
569
|
+
];
|
|
570
|
+
for (let i = 0; i < words.length; i += 3) {
|
|
571
|
+
const chunk = words.slice(i, i + 3).join(" ") + " ";
|
|
572
|
+
emit({
|
|
573
|
+
sessionUpdate: "agent_message_chunk",
|
|
574
|
+
content: { type: "text", text: chunk }
|
|
575
|
+
});
|
|
576
|
+
await delay(50);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async stop(streamId) {
|
|
580
|
+
debug("mock", ` Stopping mock agent for stream ${streamId}`);
|
|
581
|
+
this.running.delete(streamId);
|
|
582
|
+
}
|
|
583
|
+
async stopAll() {
|
|
584
|
+
const streamIds = [...this.running];
|
|
585
|
+
await Promise.all(streamIds.map((id) => this.stop(id)));
|
|
586
|
+
}
|
|
587
|
+
isRunning(streamId) {
|
|
588
|
+
return this.running.has(streamId);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/streams/backends/shell.ts
|
|
593
|
+
import * as fs2 from "fs";
|
|
594
|
+
import * as path2 from "path";
|
|
595
|
+
var pty = null;
|
|
596
|
+
async function getPty() {
|
|
597
|
+
if (!pty) {
|
|
598
|
+
pty = await import("node-pty");
|
|
599
|
+
}
|
|
600
|
+
return pty;
|
|
601
|
+
}
|
|
602
|
+
var MAX_SCROLLBACK = 1e5;
|
|
603
|
+
var SAVE_DEBOUNCE_MS = 1e3;
|
|
604
|
+
var PtyDriver = class {
|
|
605
|
+
type = "pty";
|
|
606
|
+
streamType = "pty";
|
|
607
|
+
isInteractive = true;
|
|
608
|
+
instances = /* @__PURE__ */ new Map();
|
|
609
|
+
getScrollbackPath(storageDir) {
|
|
610
|
+
return path2.join(storageDir, "scrollback.txt");
|
|
611
|
+
}
|
|
612
|
+
saveScrollback(instance) {
|
|
613
|
+
if (!fs2.existsSync(instance.storageDir)) {
|
|
614
|
+
fs2.mkdirSync(instance.storageDir, { recursive: true });
|
|
615
|
+
}
|
|
616
|
+
fs2.writeFileSync(this.getScrollbackPath(instance.storageDir), instance.scrollback);
|
|
617
|
+
}
|
|
618
|
+
loadScrollback(storageDir) {
|
|
619
|
+
const scrollbackPath = this.getScrollbackPath(storageDir);
|
|
620
|
+
try {
|
|
621
|
+
if (fs2.existsSync(scrollbackPath)) {
|
|
622
|
+
return fs2.readFileSync(scrollbackPath, "utf-8");
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
return "";
|
|
627
|
+
}
|
|
628
|
+
debouncedSave(instance) {
|
|
629
|
+
if (instance.saveTimeout) {
|
|
630
|
+
clearTimeout(instance.saveTimeout);
|
|
631
|
+
}
|
|
632
|
+
instance.saveTimeout = setTimeout(() => {
|
|
633
|
+
this.saveScrollback(instance);
|
|
634
|
+
instance.saveTimeout = null;
|
|
635
|
+
}, SAVE_DEBOUNCE_MS);
|
|
636
|
+
}
|
|
637
|
+
async start(streamId, _config, cwd, _backendState, emit, storageDir) {
|
|
638
|
+
const existing = this.instances.get(streamId);
|
|
639
|
+
if (existing) {
|
|
640
|
+
debug("shell", `Stream ${streamId} already running, reusing`);
|
|
641
|
+
existing.emit = emit;
|
|
642
|
+
return {};
|
|
643
|
+
}
|
|
644
|
+
const restoredScrollback = this.loadScrollback(storageDir);
|
|
645
|
+
if (restoredScrollback) {
|
|
646
|
+
debug("shell", `Restored ${restoredScrollback.length} bytes of scrollback for ${streamId}`);
|
|
647
|
+
}
|
|
648
|
+
const nodePty = await getPty();
|
|
649
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
650
|
+
debug("shell", `Spawning ${shell} in ${cwd}`);
|
|
651
|
+
let p;
|
|
652
|
+
try {
|
|
653
|
+
p = nodePty.spawn(shell, [], {
|
|
654
|
+
name: "xterm-256color",
|
|
655
|
+
cols: 80,
|
|
656
|
+
rows: 24,
|
|
657
|
+
cwd,
|
|
658
|
+
env: process.env
|
|
659
|
+
});
|
|
660
|
+
} catch (error) {
|
|
661
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
662
|
+
if (message.includes("posix_spawnp failed") && process.platform === "darwin") {
|
|
663
|
+
throw new Error(
|
|
664
|
+
`Failed to spawn shell on macOS: ${message}. This may be caused by macOS quarantine. Try: 1) Install globally: npm install -g @bytespell/shella && shella, or 2) Clear quarantine: xattr -dr com.apple.quarantine ~/.npm/_npx`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
const instance = {
|
|
670
|
+
pty: p,
|
|
671
|
+
scrollback: restoredScrollback,
|
|
672
|
+
cwd,
|
|
673
|
+
emit,
|
|
674
|
+
storageDir,
|
|
675
|
+
saveTimeout: null
|
|
676
|
+
};
|
|
677
|
+
p.onData((data) => {
|
|
678
|
+
instance.scrollback += data;
|
|
679
|
+
if (instance.scrollback.length > MAX_SCROLLBACK) {
|
|
680
|
+
instance.scrollback = instance.scrollback.slice(-MAX_SCROLLBACK);
|
|
681
|
+
}
|
|
682
|
+
this.debouncedSave(instance);
|
|
683
|
+
instance.emit({ amuxEvent: "terminal_output", data });
|
|
684
|
+
});
|
|
685
|
+
p.onExit(({ exitCode, signal }) => {
|
|
686
|
+
debug("shell", `PTY exited: code=${exitCode}, signal=${signal}`);
|
|
687
|
+
if (instance.saveTimeout) {
|
|
688
|
+
clearTimeout(instance.saveTimeout);
|
|
689
|
+
}
|
|
690
|
+
this.saveScrollback(instance);
|
|
691
|
+
instance.emit({
|
|
692
|
+
amuxEvent: "terminal_exit",
|
|
693
|
+
exitCode: exitCode ?? null,
|
|
694
|
+
signal: signal !== void 0 ? String(signal) : null
|
|
695
|
+
});
|
|
696
|
+
this.instances.delete(streamId);
|
|
697
|
+
});
|
|
698
|
+
this.instances.set(streamId, instance);
|
|
699
|
+
debug("shell", `Stream ${streamId} started`);
|
|
700
|
+
return {};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Get accumulated scrollback for replay on reconnect.
|
|
704
|
+
*/
|
|
705
|
+
getScrollback(streamId) {
|
|
706
|
+
return this.instances.get(streamId)?.scrollback;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get replay data (scrollback) for reconnecting clients.
|
|
710
|
+
*/
|
|
711
|
+
getReplayData(streamId) {
|
|
712
|
+
return this.getScrollback(streamId);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Write raw input to terminal (keystrokes from client).
|
|
716
|
+
*/
|
|
717
|
+
terminalWrite(streamId, data) {
|
|
718
|
+
const instance = this.instances.get(streamId);
|
|
719
|
+
if (!instance) {
|
|
720
|
+
warn("shell", `terminalWrite: stream ${streamId} not found`);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
instance.pty.write(data);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Resize terminal dimensions.
|
|
727
|
+
*/
|
|
728
|
+
terminalResize(streamId, cols, rows) {
|
|
729
|
+
const instance = this.instances.get(streamId);
|
|
730
|
+
if (!instance) {
|
|
731
|
+
warn("shell", `terminalResize: stream ${streamId} not found`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
instance.pty.resize(cols, rows);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Handle user input - writes raw text to terminal.
|
|
738
|
+
*/
|
|
739
|
+
async input(streamId, raw, _cwd, _emit) {
|
|
740
|
+
if (raw) {
|
|
741
|
+
this.terminalWrite(streamId, raw);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async stop(streamId) {
|
|
745
|
+
const instance = this.instances.get(streamId);
|
|
746
|
+
if (!instance) return;
|
|
747
|
+
debug("shell", `Stopping stream ${streamId}`);
|
|
748
|
+
if (instance.saveTimeout) {
|
|
749
|
+
clearTimeout(instance.saveTimeout);
|
|
750
|
+
}
|
|
751
|
+
this.saveScrollback(instance);
|
|
752
|
+
instance.pty.kill();
|
|
753
|
+
this.instances.delete(streamId);
|
|
754
|
+
}
|
|
755
|
+
async stopAll() {
|
|
756
|
+
debug("shell", `Stopping all streams (${this.instances.size})`);
|
|
757
|
+
for (const streamId of this.instances.keys()) {
|
|
758
|
+
await this.stop(streamId);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
isRunning(streamId) {
|
|
762
|
+
return this.instances.has(streamId);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
export {
|
|
767
|
+
AcpDriver,
|
|
768
|
+
MockDriver,
|
|
769
|
+
PtyDriver
|
|
770
|
+
};
|
|
771
|
+
//# sourceMappingURL=chunk-HJCRMFTD.js.map
|