@lelouchhe/webagent 0.1.4 → 0.1.6
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 +49 -10
- package/bin/webagent.mjs +31 -18
- package/config.toml +3 -0
- package/dist/index.html +3 -3
- package/dist/js/app.mmlj9sk2.js +34 -0
- package/dist/js/{commands.mmk25uhc.js → commands.mmlj9sk2.js} +198 -5
- package/dist/js/{connection.mmk25uhc.js → connection.mmlj9sk2.js} +14 -3
- package/dist/js/{events.mmk25uhc.js → events.mmlj9sk2.js} +66 -4
- package/dist/js/{images.mmk25uhc.js → images.mmlj9sk2.js} +1 -1
- package/dist/js/{input.mmk25uhc.js → input.mmlj9sk2.js} +23 -4
- package/dist/js/{render.mmk25uhc.js → render.mmlj9sk2.js} +1 -1
- package/dist/js/{state.mmk25uhc.js → state.mmlj9sk2.js} +2 -0
- package/dist/sw.js +47 -1
- package/lib/config.js +5 -0
- package/lib/daemon.js +278 -0
- package/lib/event-handler.js +95 -0
- package/lib/push-service.js +112 -0
- package/lib/routes.js +66 -1
- package/lib/server.js +7 -81
- package/lib/store.js +19 -0
- package/lib/types.js +1 -0
- package/lib/ws-handler.js +19 -1
- package/package.json +3 -1
- package/dist/js/app.mmk25uhc.js +0 -10
- /package/dist/{styles.mmk25uhc.css → styles.mmlj9sk2.css} +0 -0
package/lib/routes.js
CHANGED
|
@@ -13,7 +13,7 @@ const MIME = {
|
|
|
13
13
|
".gif": "image/gif",
|
|
14
14
|
".webp": "image/webp",
|
|
15
15
|
};
|
|
16
|
-
export function createRequestHandler(store, publicDir, dataDir, limits) {
|
|
16
|
+
export function createRequestHandler(store, publicDir, dataDir, limits, pushService) {
|
|
17
17
|
return async (req, res) => {
|
|
18
18
|
const url = req.url ?? "/";
|
|
19
19
|
// --- API routes ---
|
|
@@ -89,6 +89,71 @@ export function createRequestHandler(store, publicDir, dataDir, limits) {
|
|
|
89
89
|
res.end(JSON.stringify({ path: relPath, url: imgUrl }));
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
+
// --- Push notification routes ---
|
|
93
|
+
// GET /api/push/vapid-key
|
|
94
|
+
if (url === "/api/push/vapid-key" && req.method === "GET") {
|
|
95
|
+
if (!pushService) {
|
|
96
|
+
res.writeHead(404);
|
|
97
|
+
res.end(JSON.stringify({ error: "Push not configured" }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
res.end(JSON.stringify({ publicKey: pushService.getPublicKey() }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// POST /api/push/subscribe
|
|
104
|
+
if (url === "/api/push/subscribe" && req.method === "POST") {
|
|
105
|
+
if (!pushService) {
|
|
106
|
+
res.writeHead(404);
|
|
107
|
+
res.end(JSON.stringify({ error: "Push not configured" }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const chunks = [];
|
|
111
|
+
for await (const chunk of req)
|
|
112
|
+
chunks.push(chunk);
|
|
113
|
+
let body;
|
|
114
|
+
try {
|
|
115
|
+
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
res.writeHead(400);
|
|
119
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!body.endpoint || !body.keys?.auth || !body.keys?.p256dh) {
|
|
123
|
+
res.writeHead(400);
|
|
124
|
+
res.end(JSON.stringify({ error: "Missing endpoint or keys (auth, p256dh)" }));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
store.saveSubscription(body.endpoint, body.keys.auth, body.keys.p256dh);
|
|
128
|
+
res.writeHead(201);
|
|
129
|
+
res.end(JSON.stringify({ ok: true }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// POST /api/push/unsubscribe
|
|
133
|
+
if (url === "/api/push/unsubscribe" && req.method === "POST") {
|
|
134
|
+
if (!pushService) {
|
|
135
|
+
res.writeHead(404);
|
|
136
|
+
res.end(JSON.stringify({ error: "Push not configured" }));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const chunks = [];
|
|
140
|
+
for await (const chunk of req)
|
|
141
|
+
chunks.push(chunk);
|
|
142
|
+
let body;
|
|
143
|
+
try {
|
|
144
|
+
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
res.writeHead(400);
|
|
148
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (body.endpoint) {
|
|
152
|
+
store.removeSubscription(body.endpoint);
|
|
153
|
+
}
|
|
154
|
+
res.end(JSON.stringify({ ok: true }));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
92
157
|
res.writeHead(404);
|
|
93
158
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
94
159
|
return;
|
package/lib/server.js
CHANGED
|
@@ -9,6 +9,8 @@ import { SessionManager } from "./session-manager.js";
|
|
|
9
9
|
import { TitleService } from "./title-service.js";
|
|
10
10
|
import { createRequestHandler } from "./routes.js";
|
|
11
11
|
import { setupWsHandler, broadcast } from "./ws-handler.js";
|
|
12
|
+
import { handleAgentEvent } from "./event-handler.js";
|
|
13
|
+
import { PushService } from "./push-service.js";
|
|
12
14
|
const config = loadConfig();
|
|
13
15
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
14
16
|
const PUBLIC_DIR = join(__dirname, "..", config.public_dir);
|
|
@@ -17,9 +19,11 @@ const store = new Store(config.data_dir);
|
|
|
17
19
|
console.log(`[store] using ${config.data_dir}/`);
|
|
18
20
|
const sessions = new SessionManager(store, config.default_cwd, config.data_dir);
|
|
19
21
|
const titleService = new TitleService(store, sessions, config.default_cwd);
|
|
22
|
+
const pushService = new PushService(store, config.data_dir, config.push.vapid_subject);
|
|
23
|
+
console.log(`[push] VAPID public key ready`);
|
|
20
24
|
let bridge = null;
|
|
21
25
|
// --- HTTP + WebSocket servers ---
|
|
22
|
-
const server = createServer(createRequestHandler(store, PUBLIC_DIR, config.data_dir, config.limits));
|
|
26
|
+
const server = createServer(createRequestHandler(store, PUBLIC_DIR, config.data_dir, config.limits, pushService));
|
|
23
27
|
const wss = new WebSocketServer({ server });
|
|
24
28
|
setupWsHandler({
|
|
25
29
|
wss,
|
|
@@ -28,90 +32,12 @@ setupWsHandler({
|
|
|
28
32
|
titleService,
|
|
29
33
|
getBridge: () => bridge,
|
|
30
34
|
limits: config.limits,
|
|
35
|
+
pushService,
|
|
31
36
|
});
|
|
32
|
-
// --- Bridge initialization ---
|
|
33
37
|
async function initBridge() {
|
|
34
38
|
const b = new AgentBridge(config.agent_cmd);
|
|
35
39
|
b.on("event", (event) => {
|
|
36
|
-
|
|
37
|
-
return;
|
|
38
|
-
switch (event.type) {
|
|
39
|
-
case "connected":
|
|
40
|
-
event.cancelTimeout = config.limits.cancel_timeout;
|
|
41
|
-
break;
|
|
42
|
-
case "session_created":
|
|
43
|
-
if (event.configOptions?.length)
|
|
44
|
-
sessions.cachedConfigOptions = event.configOptions;
|
|
45
|
-
for (const opt of event.configOptions ?? []) {
|
|
46
|
-
store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
|
|
47
|
-
}
|
|
48
|
-
break;
|
|
49
|
-
case "config_option_update":
|
|
50
|
-
if (event.configOptions?.length)
|
|
51
|
-
sessions.cachedConfigOptions = event.configOptions;
|
|
52
|
-
for (const opt of event.configOptions ?? []) {
|
|
53
|
-
store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
|
|
54
|
-
}
|
|
55
|
-
break;
|
|
56
|
-
case "message_chunk":
|
|
57
|
-
sessions.flushThinkingBuffer(event.sessionId);
|
|
58
|
-
sessions.appendAssistant(event.sessionId, event.text);
|
|
59
|
-
break;
|
|
60
|
-
case "thought_chunk":
|
|
61
|
-
sessions.flushAssistantBuffer(event.sessionId);
|
|
62
|
-
sessions.appendThinking(event.sessionId, event.text);
|
|
63
|
-
break;
|
|
64
|
-
case "tool_call":
|
|
65
|
-
sessions.flushBuffers(event.sessionId);
|
|
66
|
-
store.saveEvent(event.sessionId, event.type, { id: event.id, title: event.title, kind: event.kind, rawInput: event.rawInput });
|
|
67
|
-
break;
|
|
68
|
-
case "tool_call_update":
|
|
69
|
-
store.saveEvent(event.sessionId, event.type, { id: event.id, status: event.status, content: event.content });
|
|
70
|
-
break;
|
|
71
|
-
case "plan":
|
|
72
|
-
sessions.flushBuffers(event.sessionId);
|
|
73
|
-
store.saveEvent(event.sessionId, event.type, { entries: event.entries });
|
|
74
|
-
break;
|
|
75
|
-
case "permission_request": {
|
|
76
|
-
sessions.flushBuffers(event.sessionId);
|
|
77
|
-
store.saveEvent(event.sessionId, event.type, {
|
|
78
|
-
requestId: event.requestId, title: event.title, options: event.options,
|
|
79
|
-
});
|
|
80
|
-
// Auto-approve permissions in autopilot mode (allow_once only to avoid persisting across mode switches)
|
|
81
|
-
const mode = store.getSession(event.sessionId)?.mode ?? "";
|
|
82
|
-
if (mode.includes("#autopilot")) {
|
|
83
|
-
const opt = event.options.find((o) => o.kind === "allow_once");
|
|
84
|
-
if (opt) {
|
|
85
|
-
b.resolvePermission(event.requestId, opt.optionId);
|
|
86
|
-
const optionName = opt.label ?? opt.optionId;
|
|
87
|
-
store.saveEvent(event.sessionId, "permission_response", {
|
|
88
|
-
requestId: event.requestId, optionName, denied: false,
|
|
89
|
-
});
|
|
90
|
-
// Skip broadcasting the permission_request — send resolved directly
|
|
91
|
-
broadcast(wss, {
|
|
92
|
-
type: "permission_resolved",
|
|
93
|
-
sessionId: event.sessionId,
|
|
94
|
-
requestId: event.requestId,
|
|
95
|
-
optionName,
|
|
96
|
-
denied: false,
|
|
97
|
-
});
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case "prompt_done":
|
|
104
|
-
sessions.activePrompts.delete(event.sessionId);
|
|
105
|
-
sessions.flushBuffers(event.sessionId);
|
|
106
|
-
store.saveEvent(event.sessionId, event.type, { stopReason: event.stopReason });
|
|
107
|
-
break;
|
|
108
|
-
case "error":
|
|
109
|
-
if (event.sessionId) {
|
|
110
|
-
sessions.activePrompts.delete(event.sessionId);
|
|
111
|
-
}
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
broadcast(wss, event);
|
|
40
|
+
handleAgentEvent(event, sessions, store, wss, b, { cancelTimeout: config.limits.cancel_timeout }, pushService);
|
|
115
41
|
});
|
|
116
42
|
await b.start();
|
|
117
43
|
bridge = b;
|
package/lib/store.js
CHANGED
|
@@ -27,6 +27,13 @@ export class Store {
|
|
|
27
27
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
28
28
|
);
|
|
29
29
|
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, seq);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
endpoint TEXT NOT NULL UNIQUE,
|
|
33
|
+
auth TEXT NOT NULL,
|
|
34
|
+
p256dh TEXT NOT NULL,
|
|
35
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
36
|
+
);
|
|
30
37
|
`);
|
|
31
38
|
// Migrate existing tables: add columns if missing
|
|
32
39
|
const cols = this.db.prepare("PRAGMA table_info(sessions)").all();
|
|
@@ -95,6 +102,18 @@ export class Store {
|
|
|
95
102
|
query += " ORDER BY seq";
|
|
96
103
|
return this.db.prepare(query).all(...params);
|
|
97
104
|
}
|
|
105
|
+
// --- Push subscriptions ---
|
|
106
|
+
saveSubscription(endpoint, auth, p256dh) {
|
|
107
|
+
this.db.prepare(`INSERT INTO push_subscriptions (endpoint, auth, p256dh)
|
|
108
|
+
VALUES (?, ?, ?)
|
|
109
|
+
ON CONFLICT(endpoint) DO UPDATE SET auth = excluded.auth, p256dh = excluded.p256dh`).run(endpoint, auth, p256dh);
|
|
110
|
+
}
|
|
111
|
+
removeSubscription(endpoint) {
|
|
112
|
+
this.db.prepare("DELETE FROM push_subscriptions WHERE endpoint = ?").run(endpoint);
|
|
113
|
+
}
|
|
114
|
+
getAllSubscriptions() {
|
|
115
|
+
return this.db.prepare("SELECT * FROM push_subscriptions").all();
|
|
116
|
+
}
|
|
98
117
|
close() {
|
|
99
118
|
this.db.close();
|
|
100
119
|
}
|
package/lib/types.js
CHANGED
|
@@ -31,6 +31,7 @@ export const WsMessageSchema = z.discriminatedUnion("type", [
|
|
|
31
31
|
z.object({ type: z.literal("set_config_option"), sessionId: z.string(), configId: z.string(), value: z.string() }),
|
|
32
32
|
z.object({ type: z.literal("bash_exec"), sessionId: z.string(), command: z.string() }),
|
|
33
33
|
z.object({ type: z.literal("bash_cancel"), sessionId: z.string() }),
|
|
34
|
+
z.object({ type: z.literal("visibility"), visible: z.boolean() }),
|
|
34
35
|
]);
|
|
35
36
|
// --- Utility ---
|
|
36
37
|
export function errorMessage(err) {
|
package/lib/ws-handler.js
CHANGED
|
@@ -41,9 +41,13 @@ function send(ws, event) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
export function setupWsHandler(deps) {
|
|
44
|
-
const { wss, store, sessions, titleService, getBridge, limits } = deps;
|
|
44
|
+
const { wss, store, sessions, titleService, getBridge, limits, pushService } = deps;
|
|
45
|
+
let nextClientId = 1;
|
|
45
46
|
wss.on("connection", (ws) => {
|
|
47
|
+
const clientId = `ws-${nextClientId++}`;
|
|
46
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)
|
|
47
51
|
const pingInterval = setInterval(() => {
|
|
48
52
|
if (ws.readyState === WebSocket.OPEN)
|
|
49
53
|
ws.ping();
|
|
@@ -235,6 +239,15 @@ export function setupWsHandler(deps) {
|
|
|
235
239
|
const stored = outputTruncated ? "[truncated]\n" + output : output;
|
|
236
240
|
store.saveEvent(msg.sessionId, "bash_result", { output: stored, code, signal });
|
|
237
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
|
+
}
|
|
238
251
|
});
|
|
239
252
|
child.on("error", (err) => {
|
|
240
253
|
sessions.runningBashProcs.delete(msg.sessionId);
|
|
@@ -248,6 +261,10 @@ export function setupWsHandler(deps) {
|
|
|
248
261
|
interruptBashProc(sessions.runningBashProcs.get(msg.sessionId));
|
|
249
262
|
break;
|
|
250
263
|
}
|
|
264
|
+
case "visibility": {
|
|
265
|
+
pushService?.setClientVisibility(clientId, msg.visible);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
251
268
|
}
|
|
252
269
|
}
|
|
253
270
|
catch (err) {
|
|
@@ -256,6 +273,7 @@ export function setupWsHandler(deps) {
|
|
|
256
273
|
});
|
|
257
274
|
ws.on("close", () => {
|
|
258
275
|
clearInterval(pingInterval);
|
|
276
|
+
pushService?.removeClient(clientId);
|
|
259
277
|
console.log(`[ws] client disconnected (total: ${wss.clients.size})`);
|
|
260
278
|
});
|
|
261
279
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lelouchhe/webagent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "A terminal-style web UI for ACP-compatible agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@agentclientprotocol/sdk": "^0.14.1",
|
|
44
|
+
"@types/web-push": "^3.6.4",
|
|
44
45
|
"better-sqlite3": "^12.6.2",
|
|
45
46
|
"smol-toml": "^1.6.0",
|
|
47
|
+
"web-push": "^3.6.7",
|
|
46
48
|
"ws": "^8.19.0",
|
|
47
49
|
"zod": "^4.3.6"
|
|
48
50
|
},
|
package/dist/js/app.mmk25uhc.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Boot entry point — imports all modules and starts the app
|
|
2
|
-
|
|
3
|
-
import './render.mmk25uhc.js'; // theme, click-to-collapse listeners
|
|
4
|
-
import './commands.mmk25uhc.js'; // slash menu listeners
|
|
5
|
-
import './images.mmk25uhc.js'; // attach/paste listeners
|
|
6
|
-
import './input.mmk25uhc.js'; // keyboard/send listeners
|
|
7
|
-
import { connect } from './connection.mmk25uhc.js';
|
|
8
|
-
|
|
9
|
-
connect();
|
|
10
|
-
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js');
|
|
File without changes
|