agello 0.2.1 → 0.4.1
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/README.md +24 -22
- package/bin/agello.ts +0 -68
- package/package.json +6 -2
- package/src/panes.ts +86 -0
- package/src/screencast.ts +1 -8
- package/src/server.ts +82 -93
- package/src/terminal.ts +122 -0
- package/web/agent-bridge.js +389 -39
- package/web/index.html +12 -0
- package/web/terminal.js +121 -0
- package/src/player.ts +0 -234
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
|
|
3
|
+
export type SocketData = { kind: "screen" | "terminal" };
|
|
4
|
+
type Socket = ServerWebSocket<SocketData>;
|
|
5
|
+
const MAX_MESSAGE = 128 * 1024;
|
|
6
|
+
|
|
7
|
+
// The CLI owns Herdr's socket handshake and emits ordered ANSI frame envelopes.
|
|
8
|
+
// Never use --takeover: another controller must release its session first.
|
|
9
|
+
export function createTerminal(pane: string) {
|
|
10
|
+
let owner: Socket | null = null;
|
|
11
|
+
let childProcess: ReturnType<typeof Bun.spawn> | null = null;
|
|
12
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
13
|
+
|
|
14
|
+
function close(ws: Socket) {
|
|
15
|
+
if (owner !== ws) return;
|
|
16
|
+
owner = null;
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
const child = childProcess;
|
|
19
|
+
childProcess = null;
|
|
20
|
+
if (child) {
|
|
21
|
+
try { (child.stdin as any).end(); } catch {}
|
|
22
|
+
child.kill(); // Disconnect only; Herdr keeps the underlying pane alive.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fail(ws: Socket, message: string) {
|
|
27
|
+
ws.send(JSON.stringify({ type: "terminal.error", message }));
|
|
28
|
+
close(ws);
|
|
29
|
+
ws.close(1011, "Terminal disconnected");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function send(ws: Socket, frame: unknown) {
|
|
33
|
+
if (owner !== ws) return;
|
|
34
|
+
if (ws.getBufferedAmount() > 4 * 1024 * 1024) {
|
|
35
|
+
fail(ws, "연결이 느려 터미널을 닫았습니다. 다시 연결해 주세요.");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
ws.send(JSON.stringify(frame));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function pump(ws: Socket, child: ReturnType<typeof Bun.spawn>) {
|
|
42
|
+
let pending = "";
|
|
43
|
+
let stderr = "";
|
|
44
|
+
let received = false;
|
|
45
|
+
const readError = (async () => {
|
|
46
|
+
for await (const chunk of child.stderr as ReadableStream<Uint8Array>) {
|
|
47
|
+
stderr = (stderr + new TextDecoder().decode(chunk)).slice(-2000);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
try {
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
for await (const chunk of child.stdout as ReadableStream<Uint8Array>) {
|
|
53
|
+
if (owner !== ws) break;
|
|
54
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
55
|
+
if (pending.length > 8 * 1024 * 1024) throw new Error("Terminal frame too large");
|
|
56
|
+
let end: number;
|
|
57
|
+
while ((end = pending.indexOf("\n")) >= 0) {
|
|
58
|
+
const line = pending.slice(0, end);
|
|
59
|
+
pending = pending.slice(end + 1);
|
|
60
|
+
if (!line.trim()) continue;
|
|
61
|
+
const frame = JSON.parse(line);
|
|
62
|
+
if (frame.type === "terminal.frame" && frame.encoding === "ansi") {
|
|
63
|
+
if (!received) { received = true; clearTimeout(timer); }
|
|
64
|
+
send(ws, frame);
|
|
65
|
+
} else if (frame.type === "terminal.closed") {
|
|
66
|
+
fail(ws, frame.reason || "터미널 연결이 종료되었습니다.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await child.exited;
|
|
71
|
+
await readError;
|
|
72
|
+
if (owner === ws) fail(ws, stderr.trim() || "터미널 연결이 종료되었습니다. 다시 연결해 주세요.");
|
|
73
|
+
} catch {
|
|
74
|
+
if (owner === ws) fail(ws, "터미널 스트림을 읽을 수 없습니다.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
open(ws: Socket) {
|
|
80
|
+
if (owner) {
|
|
81
|
+
ws.send(JSON.stringify({ type: "terminal.error", message: "다른 브라우저가 터미널을 사용 중입니다." }));
|
|
82
|
+
ws.close(1008, "Terminal busy");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
owner = ws;
|
|
86
|
+
timer = setTimeout(() => fail(ws, "터미널 연결 시간이 초과되었습니다."), 10000);
|
|
87
|
+
},
|
|
88
|
+
message(ws: Socket, raw: string) {
|
|
89
|
+
if (owner !== ws) return;
|
|
90
|
+
try {
|
|
91
|
+
if (raw.length > MAX_MESSAGE) throw new Error("Message too large");
|
|
92
|
+
const msg = JSON.parse(raw);
|
|
93
|
+
const size = () => Number.isInteger(msg.cols) && Number.isInteger(msg.rows)
|
|
94
|
+
&& msg.cols >= 2 && msg.cols <= 500 && msg.rows >= 2 && msg.rows <= 300;
|
|
95
|
+
if (msg.type === "terminal.start" && !childProcess && size()) {
|
|
96
|
+
childProcess = Bun.spawn([Bun.which("herdr", { PATH: process.env.PATH }) ?? "herdr", "terminal", "session", "control", pane,
|
|
97
|
+
"--cols", String(msg.cols), "--rows", String(msg.rows)],
|
|
98
|
+
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
|
99
|
+
void pump(ws, childProcess);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!childProcess) throw new Error("Terminal not started");
|
|
103
|
+
let command: object;
|
|
104
|
+
if (msg.type === "terminal.input" && typeof msg.text === "string" && msg.text.length <= 65536) {
|
|
105
|
+
command = { type: msg.type, text: msg.text };
|
|
106
|
+
} else if (msg.type === "terminal.resize" && size()) {
|
|
107
|
+
command = { type: msg.type, cols: msg.cols, rows: msg.rows };
|
|
108
|
+
} else if (msg.type === "terminal.scroll" && ["up", "down"].includes(msg.direction)
|
|
109
|
+
&& Number.isInteger(msg.lines) && msg.lines > 0 && msg.lines <= 100) {
|
|
110
|
+
command = { type: msg.type, direction: msg.direction, lines: msg.lines };
|
|
111
|
+
} else throw new Error("Invalid terminal message");
|
|
112
|
+
const input = childProcess.stdin as Bun.FileSink;
|
|
113
|
+
if (input.write(JSON.stringify(command) + "\n") > 1024 * 1024) throw new Error("Input overflow");
|
|
114
|
+
input.flush();
|
|
115
|
+
} catch {
|
|
116
|
+
fail(ws, "터미널 요청을 처리할 수 없습니다.");
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
close,
|
|
120
|
+
stop() { if (owner) close(owner); },
|
|
121
|
+
};
|
|
122
|
+
}
|