@lelouchhe/webagent 0.1.7 → 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/README.md +21 -18
- package/config.toml +1 -1
- package/dist/index.html +2 -2
- package/dist/js/app.IXP5KGP6.js +8 -0
- package/dist/{styles.mmmmfxhu.css → styles.01a9ju9l.css} +8 -0
- package/dist/sw.js +1 -1
- package/lib/event-handler.js +15 -6
- package/lib/push-service.js +66 -10
- package/lib/routes.js +816 -89
- package/lib/server.js +12 -10
- package/lib/session-manager.js +79 -2
- package/lib/shared/constants.js +16 -0
- package/lib/sse-manager.js +80 -0
- package/lib/store.js +61 -7
- package/lib/types.js +0 -35
- package/package.json +5 -6
- package/dist/js/app.mmmmfxhu.js +0 -34
- package/dist/js/commands.mmmmfxhu.js +0 -647
- package/dist/js/connection.mmmmfxhu.js +0 -87
- package/dist/js/events.mmmmfxhu.js +0 -694
- package/dist/js/images.mmmmfxhu.js +0 -58
- package/dist/js/input.mmmmfxhu.js +0 -215
- package/dist/js/render.mmmmfxhu.js +0 -200
- package/dist/js/state.mmmmfxhu.js +0 -203
- package/lib/ws-handler.js +0 -280
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
|
-
}
|