@lelouchhe/webagent 0.1.9 → 0.1.10

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.
@@ -1,104 +0,0 @@
1
- export function handleAgentEvent(event, sessions, store, bridge, config, sseManager, pushService) {
2
- if ("sessionId" in event && event.sessionId && sessions.restoringSessions.has(event.sessionId))
3
- return;
4
- switch (event.type) {
5
- case "connected":
6
- event.cancelTimeout = config.cancelTimeout;
7
- break;
8
- case "session_created":
9
- if (event.configOptions?.length)
10
- sessions.cachedConfigOptions = event.configOptions;
11
- for (const opt of event.configOptions ?? []) {
12
- store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
13
- }
14
- break;
15
- case "config_option_update":
16
- if (event.configOptions?.length)
17
- sessions.cachedConfigOptions = event.configOptions;
18
- for (const opt of event.configOptions ?? []) {
19
- store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
20
- }
21
- break;
22
- case "message_chunk":
23
- sessions.flushThinkingBuffer(event.sessionId);
24
- sessions.appendAssistant(event.sessionId, event.text);
25
- break;
26
- case "thought_chunk":
27
- sessions.flushAssistantBuffer(event.sessionId);
28
- sessions.appendThinking(event.sessionId, event.text);
29
- break;
30
- case "tool_call":
31
- sessions.flushBuffers(event.sessionId);
32
- store.saveEvent(event.sessionId, event.type, { id: event.id, title: event.title, kind: event.kind, rawInput: event.rawInput });
33
- break;
34
- case "tool_call_update":
35
- store.saveEvent(event.sessionId, event.type, { id: event.id, status: event.status, content: event.content });
36
- break;
37
- case "plan":
38
- sessions.flushBuffers(event.sessionId);
39
- store.saveEvent(event.sessionId, event.type, { entries: event.entries });
40
- break;
41
- case "permission_request": {
42
- sessions.flushBuffers(event.sessionId);
43
- store.saveEvent(event.sessionId, event.type, {
44
- requestId: event.requestId, title: event.title, options: event.options,
45
- });
46
- sessions.pendingPermissions.set(event.requestId, {
47
- requestId: event.requestId,
48
- sessionId: event.sessionId,
49
- title: event.title,
50
- options: event.options.map((o) => ({ optionId: o.optionId, label: o.label ?? o.name ?? o.optionId })),
51
- });
52
- // Auto-approve permissions in autopilot mode (allow_once only to avoid persisting across mode switches)
53
- const mode = store.getSession(event.sessionId)?.mode ?? "";
54
- if (mode.includes("#autopilot")) {
55
- const opt = event.options.find((o) => o.kind === "allow_once");
56
- if (opt) {
57
- bridge.resolvePermission(event.requestId, opt.optionId);
58
- sessions.pendingPermissions.delete(event.requestId);
59
- const optionName = opt.label ?? opt.optionId;
60
- store.saveEvent(event.sessionId, "permission_response", {
61
- requestId: event.requestId, optionName, denied: false,
62
- });
63
- // Broadcast both so the frontend can render then collapse the permission card
64
- sseManager.broadcast(event);
65
- const resolvedEvent = {
66
- type: "permission_resolved",
67
- sessionId: event.sessionId,
68
- requestId: event.requestId,
69
- optionName,
70
- denied: false,
71
- };
72
- sseManager.broadcast(resolvedEvent);
73
- return;
74
- }
75
- }
76
- break;
77
- }
78
- case "prompt_done":
79
- sessions.activePrompts.delete(event.sessionId);
80
- sessions.flushBuffers(event.sessionId);
81
- store.saveEvent(event.sessionId, event.type, { stopReason: event.stopReason });
82
- break;
83
- case "error":
84
- if (event.sessionId) {
85
- sessions.activePrompts.delete(event.sessionId);
86
- }
87
- break;
88
- }
89
- sseManager.broadcast(event);
90
- // Push notification check (after broadcast so clients get the event first)
91
- if (pushService && "sessionId" in event && event.sessionId) {
92
- const session = store.getSession(event.sessionId);
93
- const eventData = {};
94
- if (event.type === "permission_request") {
95
- eventData.description = event.title;
96
- }
97
- if (pushService.maybeNotify(event.sessionId, session?.title ?? null, event.type, eventData)) {
98
- const notification = pushService.formatNotification(event.sessionId, session?.title ?? null, event.type, eventData);
99
- pushService.sendToAll(notification).catch((err) => {
100
- console.error("[push] failed to send:", err);
101
- });
102
- }
103
- }
104
- }
@@ -1,168 +0,0 @@
1
- import webpush from "web-push";
2
- import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- const VAPID_FILE = "vapid.json";
5
- /** Remove a subscription after this many consecutive send failures. */
6
- const MAX_CONSECUTIVE_FAILURES = 5;
7
- export class PushService {
8
- store;
9
- vapidKeys;
10
- clientVisibility = new Map(); // clientId → visible
11
- clientEndpoints = new Map(); // clientId → push endpoint
12
- clientSessions = new Map(); // clientId → currently viewed sessionId
13
- /** endpoint → consecutive failure count (absent or 0 = healthy) */
14
- failureCounts = new Map();
15
- constructor(store, dataDir, vapidSubject) {
16
- this.store = store;
17
- this.vapidKeys = this.loadOrGenerateKeys(dataDir);
18
- webpush.setVapidDetails(vapidSubject, this.vapidKeys.publicKey, this.vapidKeys.privateKey);
19
- }
20
- // ---------------------------------------------------------------------------
21
- // VAPID keys
22
- // ---------------------------------------------------------------------------
23
- loadOrGenerateKeys(dataDir) {
24
- const filePath = join(dataDir, VAPID_FILE);
25
- if (existsSync(filePath)) {
26
- chmodSync(filePath, 0o600);
27
- const keys = JSON.parse(readFileSync(filePath, "utf8"));
28
- console.log("[push] loaded VAPID keys");
29
- return keys;
30
- }
31
- const keys = webpush.generateVAPIDKeys();
32
- writeFileSync(filePath, JSON.stringify(keys, null, 2) + "\n", { mode: 0o600 });
33
- console.log("[push] generated new VAPID keys");
34
- return keys;
35
- }
36
- getPublicKey() {
37
- return this.vapidKeys.publicKey;
38
- }
39
- // ---------------------------------------------------------------------------
40
- // Notification formatting
41
- // ---------------------------------------------------------------------------
42
- formatNotification(sessionId, sessionTitle, eventType, eventData) {
43
- const title = sessionTitle || "WebAgent";
44
- let body;
45
- switch (eventType) {
46
- case "permission_request":
47
- body = `⚿ ${eventData.description ?? "Permission requested"}`;
48
- break;
49
- case "prompt_done":
50
- body = "✓ Task complete";
51
- break;
52
- case "bash_done": {
53
- const cmd = eventData.command ?? "command";
54
- const code = eventData.exitCode ?? "?";
55
- body = `$ ${cmd} — exit ${code}`;
56
- break;
57
- }
58
- default:
59
- body = eventType;
60
- }
61
- return { title, body, data: { sessionId } };
62
- }
63
- // ---------------------------------------------------------------------------
64
- // Client visibility tracking
65
- // ---------------------------------------------------------------------------
66
- setClientVisibility(clientId, visible) {
67
- this.clientVisibility.set(clientId, visible);
68
- }
69
- setClientSession(clientId, sessionId) {
70
- this.clientSessions.set(clientId, sessionId);
71
- }
72
- registerClient(clientId, endpoint) {
73
- this.clientEndpoints.set(clientId, endpoint);
74
- }
75
- removeClient(clientId) {
76
- this.clientVisibility.delete(clientId);
77
- this.clientEndpoints.delete(clientId);
78
- this.clientSessions.delete(clientId);
79
- }
80
- hasVisibleClient() {
81
- for (const visible of this.clientVisibility.values()) {
82
- if (visible)
83
- return true;
84
- }
85
- return false;
86
- }
87
- /** Check if a specific endpoint has at least one visible client. */
88
- isEndpointVisible(endpoint) {
89
- for (const [clientId, ep] of this.clientEndpoints) {
90
- if (ep === endpoint && this.clientVisibility.get(clientId))
91
- return true;
92
- }
93
- return false;
94
- }
95
- /**
96
- * Check if a specific endpoint has a visible client viewing the given session.
97
- * A client with no session set does not suppress any session's push.
98
- */
99
- isEndpointVisibleForSession(endpoint, sessionId) {
100
- for (const [clientId, ep] of this.clientEndpoints) {
101
- if (ep === endpoint
102
- && this.clientVisibility.get(clientId)
103
- && this.clientSessions.get(clientId) === sessionId)
104
- return true;
105
- }
106
- return false;
107
- }
108
- // ---------------------------------------------------------------------------
109
- // High-level: decide whether to push, and if so, send
110
- // ---------------------------------------------------------------------------
111
- static NOTIFIABLE = new Set(["permission_request", "prompt_done", "bash_done"]);
112
- /**
113
- * Check if this event should trigger a push notification.
114
- * Returns true if a notification should be sent (caller should then call sendToAll).
115
- * Per-subscription visibility filtering happens inside sendToAll.
116
- */
117
- maybeNotify(sessionId, sessionTitle, eventType, eventData) {
118
- if (!PushService.NOTIFIABLE.has(eventType))
119
- return false;
120
- return true;
121
- }
122
- // ---------------------------------------------------------------------------
123
- // Send push to all subscriptions
124
- // ---------------------------------------------------------------------------
125
- async sendToAll(notification) {
126
- const subs = this.store.getAllSubscriptions();
127
- if (subs.length === 0)
128
- return;
129
- const payload = JSON.stringify(notification);
130
- // Per-subscription visibility: skip endpoints where a visible client is viewing this session
131
- const targets = subs.filter((sub) => !this.isEndpointVisibleForSession(sub.endpoint, notification.data.sessionId));
132
- if (targets.length === 0)
133
- return;
134
- const results = await Promise.allSettled(targets.map((sub) => this.sendOne({ endpoint: sub.endpoint, keys: { auth: sub.auth, p256dh: sub.p256dh } }, payload)));
135
- for (let i = 0; i < results.length; i++) {
136
- const result = results[i];
137
- const endpoint = targets[i].endpoint;
138
- if (result.status === "fulfilled") {
139
- this.failureCounts.delete(endpoint);
140
- }
141
- else {
142
- const err = result.reason;
143
- if (err.statusCode === 410) {
144
- // Subscription expired — clean up immediately
145
- this.store.removeSubscription(endpoint);
146
- this.failureCounts.delete(endpoint);
147
- console.log(`[push] removed expired subscription (410): ${endpoint.slice(0, 60)}…`);
148
- }
149
- else {
150
- const count = (this.failureCounts.get(endpoint) ?? 0) + 1;
151
- if (count >= MAX_CONSECUTIVE_FAILURES) {
152
- this.store.removeSubscription(endpoint);
153
- this.failureCounts.delete(endpoint);
154
- console.log(`[push] removed subscription after ${count} consecutive failures: ${endpoint.slice(0, 60)}…`);
155
- }
156
- else {
157
- this.failureCounts.set(endpoint, count);
158
- console.error(`[push] send failed (${count}/${MAX_CONSECUTIVE_FAILURES}) for ${endpoint.slice(0, 60)}…:`, result.reason);
159
- }
160
- }
161
- }
162
- }
163
- }
164
- /** Send a single push notification. Extracted for testability. */
165
- sendOne(sub, payload) {
166
- return webpush.sendNotification(sub, payload);
167
- }
168
- }