@lelouchhe/webagent 0.1.6 → 0.1.9

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/lib/ws-handler.js DELETED
@@ -1,280 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { WebSocket, WebSocketServer } from "ws";
3
- import { WsMessageSchema, errorMessage } from "./types.js";
4
- const IS_WIN = process.platform === "win32";
5
- function interruptBashProc(proc) {
6
- if (!proc)
7
- return;
8
- if (IS_WIN && typeof proc.pid === "number") {
9
- // Windows: kill entire process tree since there are no process groups
10
- spawn("taskkill", ["/T", "/F", "/PID", String(proc.pid)]).unref();
11
- return;
12
- }
13
- if (typeof proc.pid === "number") {
14
- try {
15
- process.kill(-proc.pid, "SIGINT");
16
- return;
17
- }
18
- catch {
19
- // Fall through to direct child kill when the process is not a group leader.
20
- }
21
- }
22
- proc.kill("SIGINT");
23
- }
24
- export function broadcast(wss, event, exclude) {
25
- const msg = JSON.stringify(event);
26
- for (const client of wss.clients) {
27
- if (client.readyState === WebSocket.OPEN && client !== exclude) {
28
- try {
29
- client.send(msg);
30
- }
31
- catch { /* client gone mid-send */ }
32
- }
33
- }
34
- }
35
- function send(ws, event) {
36
- if (ws.readyState === WebSocket.OPEN) {
37
- try {
38
- ws.send(JSON.stringify(event));
39
- }
40
- catch { /* client gone mid-send */ }
41
- }
42
- }
43
- export function setupWsHandler(deps) {
44
- const { wss, store, sessions, titleService, getBridge, limits, pushService } = deps;
45
- let nextClientId = 1;
46
- wss.on("connection", (ws) => {
47
- const clientId = `ws-${nextClientId++}`;
48
- console.log(`[ws] client connected (total: ${wss.clients.size})`);
49
- // Track client for push notification visibility — actual state sent by client
50
- // (no default assumed; client sends visibility message in onopen)
51
- const pingInterval = setInterval(() => {
52
- if (ws.readyState === WebSocket.OPEN)
53
- ws.ping();
54
- }, 30_000);
55
- ws.on("message", async (raw) => {
56
- // Parse & validate
57
- let parsed;
58
- try {
59
- parsed = JSON.parse(raw.toString());
60
- }
61
- catch {
62
- send(ws, { type: "error", message: "Invalid JSON" });
63
- return;
64
- }
65
- const result = WsMessageSchema.safeParse(parsed);
66
- if (!result.success) {
67
- send(ws, { type: "error", message: `Invalid message: ${result.error.message}` });
68
- return;
69
- }
70
- const msg = result.data;
71
- try {
72
- const bridge = getBridge();
73
- switch (msg.type) {
74
- case "new_session": {
75
- if (!bridge) {
76
- send(ws, { type: "error", message: "Agent not ready yet" });
77
- return;
78
- }
79
- const created = await sessions.createSession(bridge, msg.cwd, msg.inheritFromSessionId);
80
- if (created.configOptions.length) {
81
- send(ws, {
82
- type: "config_option_update",
83
- sessionId: created.sessionId,
84
- configOptions: created.configOptions,
85
- });
86
- }
87
- break;
88
- }
89
- case "resume_session": {
90
- if (!bridge) {
91
- send(ws, { type: "error", message: "Agent not ready yet" });
92
- return;
93
- }
94
- try {
95
- const event = await sessions.resumeSession(bridge, msg.sessionId);
96
- send(ws, event);
97
- }
98
- catch {
99
- send(ws, { type: "session_expired", sessionId: msg.sessionId });
100
- }
101
- break;
102
- }
103
- case "delete_session": {
104
- sessions.deleteSession(msg.sessionId);
105
- broadcast(wss, { type: "session_deleted", sessionId: msg.sessionId });
106
- console.log(`[session] deleted: ${msg.sessionId.slice(0, 8)}…`);
107
- break;
108
- }
109
- case "prompt": {
110
- if (!bridge) {
111
- send(ws, { type: "error", message: "No active bridge" });
112
- return;
113
- }
114
- const images = msg.images;
115
- const userData = {
116
- text: msg.text,
117
- ...(images && { images: images.map((i) => ({ path: i.path, mimeType: i.mimeType })) }),
118
- };
119
- store.saveEvent(msg.sessionId, "user_message", userData);
120
- store.updateSessionLastActive(msg.sessionId);
121
- // Generate title once the session actually gets one; canceled/failed attempts can retry later.
122
- if (!sessions.sessionHasTitle.has(msg.sessionId)) {
123
- titleService.generate(bridge, msg.text, msg.sessionId, (title) => {
124
- broadcast(wss, { type: "session_title_updated", sessionId: msg.sessionId, title });
125
- });
126
- }
127
- // Broadcast to other clients
128
- const userEvent = JSON.stringify({ type: "user_message", sessionId: msg.sessionId, ...userData });
129
- for (const client of wss.clients) {
130
- if (client !== ws && client.readyState === WebSocket.OPEN) {
131
- client.send(userEvent);
132
- }
133
- }
134
- sessions.activePrompts.add(msg.sessionId);
135
- bridge.prompt(msg.sessionId, msg.text, images).catch((err) => {
136
- send(ws, { type: "error", message: errorMessage(err) });
137
- });
138
- break;
139
- }
140
- case "permission_response": {
141
- if (!bridge)
142
- return;
143
- if (msg.denied) {
144
- bridge.denyPermission(msg.requestId);
145
- }
146
- else if (msg.optionId) {
147
- bridge.resolvePermission(msg.requestId, msg.optionId);
148
- }
149
- if (msg.sessionId) {
150
- store.saveEvent(msg.sessionId, "permission_response", {
151
- requestId: msg.requestId,
152
- optionName: msg.optionName || "",
153
- denied: !!msg.denied,
154
- });
155
- }
156
- broadcast(wss, {
157
- type: "permission_resolved",
158
- sessionId: msg.sessionId,
159
- requestId: msg.requestId,
160
- optionName: msg.optionName || "",
161
- denied: !!msg.denied,
162
- });
163
- break;
164
- }
165
- case "cancel": {
166
- interruptBashProc(sessions.runningBashProcs.get(msg.sessionId));
167
- if (bridge) {
168
- await titleService.cancel(msg.sessionId, bridge);
169
- }
170
- await bridge?.cancel(msg.sessionId);
171
- break;
172
- }
173
- case "set_config_option": {
174
- if (!bridge) {
175
- send(ws, { type: "error", message: "Agent not ready yet" });
176
- return;
177
- }
178
- try {
179
- const configOptions = await bridge.setConfigOption(msg.sessionId, msg.configId, msg.value);
180
- for (const opt of configOptions) {
181
- store.updateSessionConfig(msg.sessionId, opt.id, opt.currentValue);
182
- }
183
- send(ws, { type: "config_set", configId: msg.configId, value: msg.value });
184
- if (configOptions.length) {
185
- broadcast(wss, { type: "config_option_update", sessionId: msg.sessionId, configOptions }, ws);
186
- }
187
- }
188
- catch (err) {
189
- send(ws, { type: "error", message: `Failed to set ${msg.configId}: ${errorMessage(err)}` });
190
- }
191
- break;
192
- }
193
- case "bash_exec": {
194
- if (sessions.runningBashProcs.has(msg.sessionId)) {
195
- send(ws, { type: "error", message: "A bash command is already running in this session" });
196
- return;
197
- }
198
- const cwd = sessions.getSessionCwd(msg.sessionId);
199
- store.saveEvent(msg.sessionId, "bash_command", { command: msg.command });
200
- // Broadcast to other clients
201
- const bashEvent = JSON.stringify({
202
- type: "bash_command", sessionId: msg.sessionId, command: msg.command,
203
- });
204
- for (const client of wss.clients) {
205
- if (client !== ws && client.readyState === WebSocket.OPEN) {
206
- client.send(bashEvent);
207
- }
208
- }
209
- const shell = IS_WIN ? (process.env.COMSPEC || "cmd.exe") : (process.env.SHELL || "bash");
210
- const shellArgs = IS_WIN ? ["/s", "/c", msg.command] : ["-c", msg.command];
211
- const child = spawn(shell, shellArgs, {
212
- cwd,
213
- detached: !IS_WIN,
214
- env: { ...process.env, TERM: "dumb" },
215
- stdio: ["ignore", "pipe", "pipe"],
216
- });
217
- sessions.runningBashProcs.set(msg.sessionId, child);
218
- let output = "";
219
- let outputTruncated = false;
220
- const onData = (stream) => (chunk) => {
221
- const text = chunk.toString();
222
- if (!outputTruncated) {
223
- output += text;
224
- if (output.length > limits.bash_output) {
225
- output = output.slice(-limits.bash_output);
226
- outputTruncated = true;
227
- }
228
- }
229
- else {
230
- // Keep only the tail within the limit
231
- output = (output + text).slice(-limits.bash_output);
232
- }
233
- broadcast(wss, { type: "bash_output", sessionId: msg.sessionId, text, stream });
234
- };
235
- child.stdout.on("data", onData("stdout"));
236
- child.stderr.on("data", onData("stderr"));
237
- child.on("close", (code, signal) => {
238
- sessions.runningBashProcs.delete(msg.sessionId);
239
- const stored = outputTruncated ? "[truncated]\n" + output : output;
240
- store.saveEvent(msg.sessionId, "bash_result", { output: stored, code, signal });
241
- broadcast(wss, { type: "bash_done", sessionId: msg.sessionId, code, signal });
242
- // Push notification for bash completion
243
- if (pushService) {
244
- const session = store.getSession(msg.sessionId);
245
- const eventData = { command: msg.command, exitCode: code };
246
- if (pushService.maybeNotify(msg.sessionId, session?.title ?? null, "bash_done", eventData)) {
247
- const notification = pushService.formatNotification(msg.sessionId, session?.title ?? null, "bash_done", eventData);
248
- pushService.sendToAll(notification).catch(err => console.error("[push] failed to send:", err));
249
- }
250
- }
251
- });
252
- child.on("error", (err) => {
253
- sessions.runningBashProcs.delete(msg.sessionId);
254
- const errMsg = errorMessage(err);
255
- store.saveEvent(msg.sessionId, "bash_result", { output: errMsg, code: -1, signal: null });
256
- broadcast(wss, { type: "bash_done", sessionId: msg.sessionId, code: -1, signal: null, error: errMsg });
257
- });
258
- break;
259
- }
260
- case "bash_cancel": {
261
- interruptBashProc(sessions.runningBashProcs.get(msg.sessionId));
262
- break;
263
- }
264
- case "visibility": {
265
- pushService?.setClientVisibility(clientId, msg.visible);
266
- break;
267
- }
268
- }
269
- }
270
- catch (err) {
271
- send(ws, { type: "error", message: errorMessage(err) });
272
- }
273
- });
274
- ws.on("close", () => {
275
- clearInterval(pingInterval);
276
- pushService?.removeClient(clientId);
277
- console.log(`[ws] client disconnected (total: ${wss.clients.size})`);
278
- });
279
- });
280
- }