@lelouchhe/webagent 0.1.7 → 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.
- 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/package.json +7 -8
- 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/bridge.js +0 -284
- package/lib/config.js +0 -62
- package/lib/daemon.js +0 -278
- package/lib/event-handler.js +0 -95
- package/lib/push-service.js +0 -112
- package/lib/routes.js +0 -202
- package/lib/server.js +0 -70
- package/lib/session-manager.js +0 -199
- package/lib/store.js +0 -120
- package/lib/title-service.js +0 -71
- package/lib/types.js +0 -48
- package/lib/ws-handler.js +0 -280
package/lib/store.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import Database from "better-sqlite3";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
export class Store {
|
|
5
|
-
db;
|
|
6
|
-
constructor(dataDir) {
|
|
7
|
-
mkdirSync(dataDir, { recursive: true });
|
|
8
|
-
this.db = new Database(join(dataDir, "webagent.db"));
|
|
9
|
-
this.db.pragma("journal_mode = WAL");
|
|
10
|
-
this.migrate();
|
|
11
|
-
}
|
|
12
|
-
migrate() {
|
|
13
|
-
this.db.exec(`
|
|
14
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
15
|
-
id TEXT PRIMARY KEY,
|
|
16
|
-
cwd TEXT NOT NULL,
|
|
17
|
-
title TEXT,
|
|
18
|
-
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
|
|
19
|
-
last_active_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
20
|
-
);
|
|
21
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
22
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
-
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
24
|
-
seq INTEGER NOT NULL,
|
|
25
|
-
type TEXT NOT NULL,
|
|
26
|
-
data TEXT NOT NULL DEFAULT '{}',
|
|
27
|
-
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
|
28
|
-
);
|
|
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
|
-
);
|
|
37
|
-
`);
|
|
38
|
-
// Migrate existing tables: add columns if missing
|
|
39
|
-
const cols = this.db.prepare("PRAGMA table_info(sessions)").all();
|
|
40
|
-
const colNames = new Set(cols.map(c => c.name));
|
|
41
|
-
if (!colNames.has("title")) {
|
|
42
|
-
this.db.exec("ALTER TABLE sessions ADD COLUMN title TEXT");
|
|
43
|
-
}
|
|
44
|
-
if (!colNames.has("last_active_at")) {
|
|
45
|
-
this.db.exec("ALTER TABLE sessions ADD COLUMN last_active_at TEXT");
|
|
46
|
-
// Backfill from created_at
|
|
47
|
-
this.db.exec("UPDATE sessions SET last_active_at = created_at WHERE last_active_at IS NULL");
|
|
48
|
-
}
|
|
49
|
-
if (!colNames.has("model")) {
|
|
50
|
-
this.db.exec("ALTER TABLE sessions ADD COLUMN model TEXT");
|
|
51
|
-
}
|
|
52
|
-
if (!colNames.has("mode")) {
|
|
53
|
-
this.db.exec("ALTER TABLE sessions ADD COLUMN mode TEXT");
|
|
54
|
-
}
|
|
55
|
-
if (!colNames.has("reasoning_effort")) {
|
|
56
|
-
this.db.exec("ALTER TABLE sessions ADD COLUMN reasoning_effort TEXT");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
createSession(id, cwd) {
|
|
60
|
-
this.db.prepare("INSERT INTO sessions (id, cwd) VALUES (?, ?)").run(id, cwd);
|
|
61
|
-
return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
62
|
-
}
|
|
63
|
-
listSessions() {
|
|
64
|
-
return this.db.prepare("SELECT * FROM sessions ORDER BY COALESCE(last_active_at, created_at) DESC").all();
|
|
65
|
-
}
|
|
66
|
-
getSession(id) {
|
|
67
|
-
return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
68
|
-
}
|
|
69
|
-
deleteSession(id) {
|
|
70
|
-
this.db.prepare("DELETE FROM events WHERE session_id = ?").run(id);
|
|
71
|
-
this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
72
|
-
}
|
|
73
|
-
updateSessionTitle(id, title) {
|
|
74
|
-
this.db.prepare("UPDATE sessions SET title = ? WHERE id = ?").run(title, id);
|
|
75
|
-
}
|
|
76
|
-
updateSessionLastActive(id) {
|
|
77
|
-
this.db.prepare("UPDATE sessions SET last_active_at = strftime('%Y-%m-%d %H:%M:%f', 'now') WHERE id = ?").run(id);
|
|
78
|
-
}
|
|
79
|
-
/** Update a config option value (model, mode, reasoning_effort) for a session. */
|
|
80
|
-
updateSessionConfig(id, configId, value) {
|
|
81
|
-
const column = { model: "model", mode: "mode", reasoning_effort: "reasoning_effort" }[configId];
|
|
82
|
-
if (!column)
|
|
83
|
-
return;
|
|
84
|
-
this.db.prepare(`UPDATE sessions SET ${column} = ? WHERE id = ?`).run(value, id);
|
|
85
|
-
}
|
|
86
|
-
saveEvent(sessionId, type, data = {}) {
|
|
87
|
-
const seq = this.db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM events WHERE session_id = ?").get(sessionId).next;
|
|
88
|
-
this.db.prepare("INSERT INTO events (session_id, seq, type, data) VALUES (?, ?, ?, ?)").run(sessionId, seq, type, JSON.stringify(data));
|
|
89
|
-
return this.db.prepare("SELECT * FROM events WHERE session_id = ? AND seq = ?")
|
|
90
|
-
.get(sessionId, seq);
|
|
91
|
-
}
|
|
92
|
-
getEvents(sessionId, opts) {
|
|
93
|
-
let query = "SELECT * FROM events WHERE session_id = ?";
|
|
94
|
-
const params = [sessionId];
|
|
95
|
-
if (opts?.afterSeq != null) {
|
|
96
|
-
query += " AND seq > ?";
|
|
97
|
-
params.push(opts.afterSeq);
|
|
98
|
-
}
|
|
99
|
-
if (opts?.excludeThinking) {
|
|
100
|
-
query += " AND type != 'thinking'";
|
|
101
|
-
}
|
|
102
|
-
query += " ORDER BY seq";
|
|
103
|
-
return this.db.prepare(query).all(...params);
|
|
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
|
-
}
|
|
117
|
-
close() {
|
|
118
|
-
this.db.close();
|
|
119
|
-
}
|
|
120
|
-
}
|
package/lib/title-service.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
const TITLE_MODEL = "claude-haiku-4.5";
|
|
2
|
-
export class TitleService {
|
|
3
|
-
titleSessionId = null;
|
|
4
|
-
activeSourceSessions = new Set();
|
|
5
|
-
cancelledSourceSessions = new Set();
|
|
6
|
-
defaultCwd;
|
|
7
|
-
store;
|
|
8
|
-
sessions;
|
|
9
|
-
constructor(store, sessions, defaultCwd) {
|
|
10
|
-
this.store = store;
|
|
11
|
-
this.sessions = sessions;
|
|
12
|
-
this.defaultCwd = defaultCwd;
|
|
13
|
-
}
|
|
14
|
-
/** Generate a title for the session (non-blocking, fire-and-forget). */
|
|
15
|
-
generate(bridge, userMessage, sessionId, onTitle) {
|
|
16
|
-
if (this.sessions.sessionHasTitle.has(sessionId) || this.activeSourceSessions.has(sessionId))
|
|
17
|
-
return;
|
|
18
|
-
this._generate(bridge, userMessage, sessionId).then((title) => {
|
|
19
|
-
if (title && onTitle)
|
|
20
|
-
onTitle(title);
|
|
21
|
-
}).catch((err) => {
|
|
22
|
-
console.error(`[title] generation failed:`, err);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
async _generate(bridge, userMessage, sessionId) {
|
|
26
|
-
this.activeSourceSessions.add(sessionId);
|
|
27
|
-
const tsId = await this.ensureTitleSession(bridge);
|
|
28
|
-
if (!tsId) {
|
|
29
|
-
this.activeSourceSessions.delete(sessionId);
|
|
30
|
-
this.cancelledSourceSessions.delete(sessionId);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
try {
|
|
34
|
-
const prompt = `Generate a short title (max 30 chars, no quotes) for a chat that starts with this message. Reply with ONLY the title, nothing else:\n\n${userMessage.slice(0, 500)}`;
|
|
35
|
-
const title = await bridge.promptForText(tsId, prompt);
|
|
36
|
-
if (!title || this.cancelledSourceSessions.has(sessionId))
|
|
37
|
-
return;
|
|
38
|
-
const cleaned = title.replace(/^["']|["']$/g, "").trim().slice(0, 30);
|
|
39
|
-
if (!cleaned)
|
|
40
|
-
return;
|
|
41
|
-
this.store.updateSessionTitle(sessionId, cleaned);
|
|
42
|
-
this.sessions.sessionHasTitle.add(sessionId);
|
|
43
|
-
return cleaned;
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
this.activeSourceSessions.delete(sessionId);
|
|
47
|
-
this.cancelledSourceSessions.delete(sessionId);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
async cancel(sessionId, bridge) {
|
|
51
|
-
this.cancelledSourceSessions.add(sessionId);
|
|
52
|
-
if (!this.titleSessionId || !this.activeSourceSessions.has(sessionId))
|
|
53
|
-
return;
|
|
54
|
-
await bridge.cancel(this.titleSessionId);
|
|
55
|
-
}
|
|
56
|
-
/** Ensure the dedicated title session exists. Returns session ID or null. */
|
|
57
|
-
async ensureTitleSession(bridge) {
|
|
58
|
-
if (this.titleSessionId)
|
|
59
|
-
return this.titleSessionId;
|
|
60
|
-
try {
|
|
61
|
-
const id = await bridge.newSession(this.defaultCwd, { silent: true });
|
|
62
|
-
this.sessions.liveSessions.add(id);
|
|
63
|
-
await bridge.setConfigOption(id, "model", TITLE_MODEL).catch(() => []);
|
|
64
|
-
this.titleSessionId = id;
|
|
65
|
-
return id;
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
package/lib/types.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { z } from "zod/v4";
|
|
2
|
-
// --- Inbound WS messages (client → server) ---
|
|
3
|
-
const ImageSchema = z.object({
|
|
4
|
-
data: z.string(),
|
|
5
|
-
mimeType: z.string(),
|
|
6
|
-
path: z.string().optional(),
|
|
7
|
-
});
|
|
8
|
-
export const WsMessageSchema = z.discriminatedUnion("type", [
|
|
9
|
-
z.object({
|
|
10
|
-
type: z.literal("new_session"),
|
|
11
|
-
cwd: z.string().optional(),
|
|
12
|
-
inheritFromSessionId: z.string().optional(),
|
|
13
|
-
}),
|
|
14
|
-
z.object({ type: z.literal("resume_session"), sessionId: z.string() }),
|
|
15
|
-
z.object({ type: z.literal("delete_session"), sessionId: z.string() }),
|
|
16
|
-
z.object({
|
|
17
|
-
type: z.literal("prompt"),
|
|
18
|
-
sessionId: z.string(),
|
|
19
|
-
text: z.string(),
|
|
20
|
-
images: z.array(ImageSchema).optional(),
|
|
21
|
-
}),
|
|
22
|
-
z.object({
|
|
23
|
-
type: z.literal("permission_response"),
|
|
24
|
-
sessionId: z.string().optional(),
|
|
25
|
-
requestId: z.string(),
|
|
26
|
-
optionId: z.string().optional(),
|
|
27
|
-
optionName: z.string().optional(),
|
|
28
|
-
denied: z.boolean().optional(),
|
|
29
|
-
}),
|
|
30
|
-
z.object({ type: z.literal("cancel"), sessionId: z.string() }),
|
|
31
|
-
z.object({ type: z.literal("set_config_option"), sessionId: z.string(), configId: z.string(), value: z.string() }),
|
|
32
|
-
z.object({ type: z.literal("bash_exec"), sessionId: z.string(), command: z.string() }),
|
|
33
|
-
z.object({ type: z.literal("bash_cancel"), sessionId: z.string() }),
|
|
34
|
-
z.object({ type: z.literal("visibility"), visible: z.boolean() }),
|
|
35
|
-
]);
|
|
36
|
-
// --- Utility ---
|
|
37
|
-
export function errorMessage(err) {
|
|
38
|
-
if (err instanceof Error)
|
|
39
|
-
return err.message;
|
|
40
|
-
if (typeof err === "string")
|
|
41
|
-
return err;
|
|
42
|
-
try {
|
|
43
|
-
return JSON.stringify(err);
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return String(err);
|
|
47
|
-
}
|
|
48
|
-
}
|
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
|
-
}
|