@serendb/serendesktop 0.1.0

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/dist/server.js ADDED
@@ -0,0 +1,2598 @@
1
+ // src/server.ts
2
+ import { randomBytes as randomBytes3, createHash as createHash3 } from "crypto";
3
+ import { exec as exec2 } from "child_process";
4
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
5
+ import { createServer as createServer2 } from "http";
6
+ import { request as httpsRequest } from "https";
7
+ import { homedir as homedir5, platform as platform3 } from "os";
8
+ import { join as join7, extname as extname2 } from "path";
9
+ import { fileURLToPath as fileURLToPath2 } from "url";
10
+ import { WebSocketServer } from "ws";
11
+
12
+ // src/events.ts
13
+ var subscribers = /* @__PURE__ */ new Map();
14
+ var authenticatedClients = /* @__PURE__ */ new Set();
15
+ function addClient(ws) {
16
+ authenticatedClients.add(ws);
17
+ ws.on("close", () => authenticatedClients.delete(ws));
18
+ }
19
+ function emit(event, params) {
20
+ const localSubs = subscribers.get(event);
21
+ if (localSubs) {
22
+ for (const cb of localSubs) {
23
+ try {
24
+ cb(params);
25
+ } catch (err) {
26
+ console.error(`[Events] Local subscriber error for ${event}:`, err);
27
+ }
28
+ }
29
+ }
30
+ const notification = JSON.stringify({
31
+ jsonrpc: "2.0",
32
+ method: event,
33
+ params: params ?? null
34
+ });
35
+ for (const client of authenticatedClients) {
36
+ if (client.readyState === client.OPEN) {
37
+ try {
38
+ client.send(notification);
39
+ } catch (err) {
40
+ console.error("[Events] Failed to send to client:", err);
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ // src/handlers/chat.ts
47
+ import Database from "better-sqlite3";
48
+ var db;
49
+ function initChatDb(dbPath) {
50
+ db = new Database(dbPath);
51
+ db.pragma("journal_mode = WAL");
52
+ db.exec(`
53
+ CREATE TABLE IF NOT EXISTS conversations (
54
+ id TEXT PRIMARY KEY,
55
+ title TEXT NOT NULL,
56
+ created_at INTEGER NOT NULL,
57
+ selected_model TEXT,
58
+ selected_provider TEXT,
59
+ is_archived INTEGER NOT NULL DEFAULT 0
60
+ );
61
+ CREATE TABLE IF NOT EXISTS messages (
62
+ id TEXT PRIMARY KEY,
63
+ conversation_id TEXT NOT NULL,
64
+ role TEXT NOT NULL,
65
+ content TEXT NOT NULL,
66
+ model TEXT,
67
+ timestamp INTEGER NOT NULL,
68
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id)
69
+ );
70
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation
71
+ ON messages(conversation_id, timestamp);
72
+ `);
73
+ }
74
+ async function createConversation(params) {
75
+ const conv = {
76
+ id: params.id,
77
+ title: params.title,
78
+ created_at: Date.now(),
79
+ selected_model: params.selectedModel ?? null,
80
+ selected_provider: params.selectedProvider ?? null,
81
+ is_archived: false
82
+ };
83
+ db.prepare(
84
+ `INSERT OR REPLACE INTO conversations (id, title, created_at, selected_model, selected_provider, is_archived)
85
+ VALUES (?, ?, ?, ?, ?, ?)`
86
+ ).run(
87
+ conv.id,
88
+ conv.title,
89
+ conv.created_at,
90
+ conv.selected_model,
91
+ conv.selected_provider,
92
+ conv.is_archived ? 1 : 0
93
+ );
94
+ return conv;
95
+ }
96
+ async function getConversations() {
97
+ const rows = db.prepare(
98
+ `SELECT * FROM conversations WHERE is_archived = 0 ORDER BY created_at DESC`
99
+ ).all();
100
+ return rows.map((r) => ({ ...r, is_archived: r.is_archived === 1 }));
101
+ }
102
+ async function getConversation(params) {
103
+ const row = db.prepare(`SELECT * FROM conversations WHERE id = ?`).get(params.id);
104
+ if (!row) return null;
105
+ return { ...row, is_archived: row.is_archived === 1 };
106
+ }
107
+ async function updateConversation(params) {
108
+ const existing = await getConversation({ id: params.id });
109
+ if (!existing) return;
110
+ const updated = {
111
+ title: params.title ?? existing.title,
112
+ selected_model: params.selectedModel !== void 0 ? params.selectedModel : existing.selected_model,
113
+ selected_provider: params.selectedProvider !== void 0 ? params.selectedProvider : existing.selected_provider
114
+ };
115
+ db.prepare(
116
+ `UPDATE conversations SET title = ?, selected_model = ?, selected_provider = ? WHERE id = ?`
117
+ ).run(
118
+ updated.title,
119
+ updated.selected_model,
120
+ updated.selected_provider,
121
+ params.id
122
+ );
123
+ }
124
+ async function archiveConversation(params) {
125
+ db.prepare(`UPDATE conversations SET is_archived = 1 WHERE id = ?`).run(
126
+ params.id
127
+ );
128
+ }
129
+ async function deleteConversation(params) {
130
+ db.prepare(`DELETE FROM messages WHERE conversation_id = ?`).run(params.id);
131
+ db.prepare(`DELETE FROM conversations WHERE id = ?`).run(params.id);
132
+ }
133
+ async function saveMessage(params) {
134
+ db.prepare(
135
+ `INSERT OR REPLACE INTO messages (id, conversation_id, role, content, model, timestamp)
136
+ VALUES (?, ?, ?, ?, ?, ?)`
137
+ ).run(
138
+ params.id,
139
+ params.conversationId,
140
+ params.role,
141
+ params.content,
142
+ params.model,
143
+ params.timestamp
144
+ );
145
+ }
146
+ async function getMessages(params) {
147
+ const rows = db.prepare(
148
+ `SELECT * FROM messages WHERE conversation_id = ?
149
+ ORDER BY timestamp DESC LIMIT ?`
150
+ ).all(params.conversationId, params.limit);
151
+ return rows.reverse();
152
+ }
153
+
154
+ // src/rpc.ts
155
+ var handlers = /* @__PURE__ */ new Map();
156
+ function errorResponse(code, message, id) {
157
+ const response = {
158
+ jsonrpc: "2.0",
159
+ error: { code, message },
160
+ id
161
+ };
162
+ return JSON.stringify(response);
163
+ }
164
+ function successResponse(result, id) {
165
+ const response = {
166
+ jsonrpc: "2.0",
167
+ result,
168
+ id
169
+ };
170
+ return JSON.stringify(response);
171
+ }
172
+ function registerHandler(method, handler) {
173
+ handlers.set(method, handler);
174
+ }
175
+ async function handleMessage(raw) {
176
+ let request;
177
+ try {
178
+ request = JSON.parse(raw);
179
+ } catch {
180
+ return errorResponse(-32700, "Parse error", null);
181
+ }
182
+ const id = request.id ?? null;
183
+ const isNotification = request.id === void 0;
184
+ if (!request.method || typeof request.method !== "string") {
185
+ return errorResponse(-32600, "Invalid request: missing method", id);
186
+ }
187
+ const handler = handlers.get(request.method);
188
+ if (!handler) {
189
+ if (isNotification) return null;
190
+ return errorResponse(-32601, `Method not found: ${request.method}`, id);
191
+ }
192
+ try {
193
+ const result = await handler(request.params);
194
+ if (isNotification) return null;
195
+ return successResponse(result, id);
196
+ } catch (err) {
197
+ const message = err instanceof Error ? err.message : "Internal error";
198
+ if (isNotification) return null;
199
+ return errorResponse(-32e3, message, id);
200
+ }
201
+ }
202
+
203
+ // src/handlers/acp.ts
204
+ import * as acp from "@agentclientprotocol/sdk";
205
+ import { spawn, execFile } from "child_process";
206
+ import { Readable, Writable } from "stream";
207
+ import { existsSync } from "fs";
208
+ import { readFile, writeFile } from "fs/promises";
209
+ import { randomUUID } from "crypto";
210
+ import { resolve } from "path";
211
+ import { platform } from "os";
212
+ var sessions = /* @__PURE__ */ new Map();
213
+ function createClient(sessionId) {
214
+ return {
215
+ async requestPermission(params) {
216
+ const requestId = randomUUID();
217
+ const session = sessions.get(sessionId);
218
+ if (!session) throw new Error("Session not found");
219
+ emit("acp://permission-request", {
220
+ sessionId,
221
+ requestId,
222
+ toolCall: params.toolCall,
223
+ options: params.options
224
+ });
225
+ const optionId = await new Promise((resolve3, reject) => {
226
+ session.pendingPermissions.set(requestId, resolve3);
227
+ setTimeout(() => {
228
+ session.pendingPermissions.delete(requestId);
229
+ reject(new Error("Permission request timed out"));
230
+ }, 3e5);
231
+ });
232
+ return {
233
+ outcome: { outcome: "selected", optionId }
234
+ };
235
+ },
236
+ async sessionUpdate(params) {
237
+ handleSessionUpdate(sessionId, params);
238
+ },
239
+ async readTextFile(params) {
240
+ const content = await readFile(params.path, "utf-8");
241
+ return { content };
242
+ },
243
+ async writeTextFile(params) {
244
+ const session = sessions.get(sessionId);
245
+ if (!session) throw new Error("Session not found");
246
+ let oldText = "";
247
+ try {
248
+ oldText = await readFile(params.path, "utf-8");
249
+ } catch {
250
+ }
251
+ const proposalId = randomUUID();
252
+ emit("acp://diff-proposal", {
253
+ sessionId,
254
+ proposalId,
255
+ path: params.path,
256
+ oldText,
257
+ newText: params.content
258
+ });
259
+ const accepted = await new Promise((resolve3, reject) => {
260
+ session.pendingDiffProposals.set(proposalId, resolve3);
261
+ setTimeout(() => {
262
+ session.pendingDiffProposals.delete(proposalId);
263
+ reject(new Error("Diff proposal timed out"));
264
+ }, 3e5);
265
+ });
266
+ if (!accepted) {
267
+ throw new Error("File write rejected by user");
268
+ }
269
+ await writeFile(params.path, params.content, "utf-8");
270
+ return {};
271
+ }
272
+ };
273
+ }
274
+ function handleSessionUpdate(sessionId, notification) {
275
+ const update = notification.update;
276
+ switch (update.sessionUpdate) {
277
+ case "agent_message_chunk":
278
+ if (update.content.type === "text") {
279
+ emit("acp://message-chunk", {
280
+ sessionId,
281
+ text: update.content.text
282
+ });
283
+ }
284
+ break;
285
+ case "agent_thought_chunk":
286
+ if (update.content.type === "text") {
287
+ emit("acp://message-chunk", {
288
+ sessionId,
289
+ text: update.content.text,
290
+ isThought: true
291
+ });
292
+ }
293
+ break;
294
+ case "tool_call":
295
+ emit("acp://tool-call", {
296
+ sessionId,
297
+ toolCallId: update.toolCallId,
298
+ title: update.title,
299
+ kind: update.kind,
300
+ status: update.status
301
+ });
302
+ break;
303
+ case "tool_call_update": {
304
+ if (update.content) {
305
+ for (const block of update.content) {
306
+ if ("diff" in block || block.path) {
307
+ const diff = block;
308
+ emit("acp://diff", {
309
+ sessionId,
310
+ toolCallId: update.toolCallId,
311
+ path: diff.path,
312
+ oldText: diff.oldText ?? diff.old_text ?? "",
313
+ newText: diff.newText ?? diff.new_text ?? ""
314
+ });
315
+ }
316
+ }
317
+ }
318
+ emit("acp://tool-result", {
319
+ sessionId,
320
+ toolCallId: update.toolCallId,
321
+ status: update.status
322
+ });
323
+ break;
324
+ }
325
+ case "plan":
326
+ emit("acp://plan-update", {
327
+ sessionId,
328
+ entries: update.entries ?? []
329
+ });
330
+ break;
331
+ default:
332
+ break;
333
+ }
334
+ }
335
+ var AGENT_BINARIES = {
336
+ "claude-code": "seren-acp-claude",
337
+ codex: "seren-acp-codex"
338
+ };
339
+ function findAgentCommand(agentType) {
340
+ const binBase = AGENT_BINARIES[agentType];
341
+ if (!binBase) {
342
+ throw new Error(`Unknown agent type: ${agentType}`);
343
+ }
344
+ return findAgentBinary(binBase);
345
+ }
346
+ function findAgentBinary(binBase) {
347
+ const ext = platform() === "win32" ? ".exe" : "";
348
+ const binName = `${binBase}${ext}`;
349
+ const home2 = process.env.HOME ?? "~";
350
+ const candidates = [
351
+ // 1. runtime/bin/ (bundled with seren-local)
352
+ resolve(import.meta.dirname, "../../bin", binName),
353
+ // 2. ~/.seren-local/bin/ (user install location)
354
+ resolve(home2, ".seren-local/bin", binName),
355
+ // 3. Seren Desktop embedded-runtime (development)
356
+ resolve(home2, "Projects/Seren_Projects/seren-desktop/src-tauri/embedded-runtime/bin", binName)
357
+ ];
358
+ if (binBase === "seren-acp-claude") {
359
+ const legacyName = `acp_agent${ext}`;
360
+ candidates.push(
361
+ resolve(import.meta.dirname, "../../bin", legacyName),
362
+ resolve(home2, ".seren-local/bin", legacyName),
363
+ resolve(home2, "Projects/Seren_Projects/seren-desktop/src-tauri/embedded-runtime/bin", legacyName)
364
+ );
365
+ }
366
+ for (const candidate of candidates) {
367
+ if (existsSync(candidate)) {
368
+ console.log(`[ACP] Found ${binBase} binary at: ${candidate}`);
369
+ return candidate;
370
+ }
371
+ }
372
+ throw new Error(
373
+ `Agent binary '${binBase}' not found. Checked locations:
374
+ ${candidates.map((p) => ` - ${p}`).join("\n")}`
375
+ );
376
+ }
377
+ async function isCommandAvailable(command) {
378
+ const which = platform() === "win32" ? "where" : "which";
379
+ return new Promise((resolve3) => {
380
+ execFile(which, [command], (err) => resolve3(!err));
381
+ });
382
+ }
383
+ async function acpSpawn(params) {
384
+ const { agentType, cwd, sandboxMode, thinking } = params;
385
+ const sessionId = randomUUID();
386
+ const command = findAgentCommand(agentType);
387
+ const resolvedCwd = resolve(cwd);
388
+ const args = [];
389
+ if (sandboxMode) {
390
+ args.push("--sandbox", sandboxMode);
391
+ }
392
+ const agentProcess = spawn(command, args, {
393
+ cwd: resolvedCwd,
394
+ stdio: ["pipe", "pipe", "pipe"],
395
+ env: { ...process.env }
396
+ });
397
+ if (!agentProcess.stdin || !agentProcess.stdout) {
398
+ throw new Error("Failed to create agent process stdio");
399
+ }
400
+ if (agentProcess.stderr) {
401
+ const readline = await import("readline");
402
+ const rl = readline.createInterface({ input: agentProcess.stderr });
403
+ rl.on("line", (line) => {
404
+ console.log(`[ACP Agent stderr] ${line}`);
405
+ });
406
+ }
407
+ const input = Writable.toWeb(agentProcess.stdin);
408
+ const output = Readable.toWeb(
409
+ agentProcess.stdout
410
+ );
411
+ const stream = acp.ndJsonStream(input, output);
412
+ const client = createClient(sessionId);
413
+ const connection = new acp.ClientSideConnection(
414
+ (_agent) => client,
415
+ stream
416
+ );
417
+ const session = {
418
+ id: sessionId,
419
+ agentType,
420
+ cwd: resolvedCwd,
421
+ status: "initializing",
422
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
423
+ connection,
424
+ process: agentProcess,
425
+ pendingPermissions: /* @__PURE__ */ new Map(),
426
+ pendingDiffProposals: /* @__PURE__ */ new Map(),
427
+ cancelling: false
428
+ };
429
+ sessions.set(sessionId, session);
430
+ agentProcess.on("exit", (code, signal) => {
431
+ session.status = "terminated";
432
+ emit("acp://session-status", {
433
+ sessionId,
434
+ status: "terminated"
435
+ });
436
+ console.log(
437
+ `[ACP] Agent ${sessionId} exited: code=${code}, signal=${signal}`
438
+ );
439
+ });
440
+ try {
441
+ const initResult = await connection.initialize({
442
+ protocolVersion: acp.PROTOCOL_VERSION,
443
+ clientCapabilities: {
444
+ fs: {
445
+ readTextFile: true,
446
+ writeTextFile: true
447
+ },
448
+ terminal: true
449
+ }
450
+ });
451
+ session.status = "ready";
452
+ emit("acp://session-status", {
453
+ sessionId,
454
+ status: "ready",
455
+ agentInfo: initResult.agentInfo
456
+ });
457
+ const meta = thinking ? { claudeCode: { options: { maxThinkingTokens: thinking.maxTokens ?? 16e3 } } } : void 0;
458
+ const sessionResult = await connection.newSession({
459
+ cwd: resolvedCwd,
460
+ mcpServers: [],
461
+ ...meta ? { _meta: meta } : {}
462
+ });
463
+ session.acpSessionId = sessionResult.sessionId ?? sessionId;
464
+ } catch (err) {
465
+ session.status = "error";
466
+ emit("acp://error", {
467
+ sessionId,
468
+ error: `Failed to initialize agent: ${err instanceof Error ? err.message : JSON.stringify(err)}`
469
+ });
470
+ throw err;
471
+ }
472
+ return {
473
+ id: sessionId,
474
+ agentType,
475
+ cwd: resolvedCwd,
476
+ status: session.status,
477
+ createdAt: session.createdAt
478
+ };
479
+ }
480
+ async function acpPrompt(params) {
481
+ const { sessionId, prompt, context } = params;
482
+ const session = sessions.get(sessionId);
483
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
484
+ session.status = "prompting";
485
+ emit("acp://session-status", { sessionId, status: "prompting" });
486
+ const acpSessionId = session.acpSessionId ?? sessionId;
487
+ const promptContent = [
488
+ { type: "text", text: prompt }
489
+ ];
490
+ if (context) {
491
+ for (const item of context) {
492
+ if (item.text) {
493
+ promptContent.push({ type: "text", text: item.text });
494
+ }
495
+ }
496
+ }
497
+ try {
498
+ const result = await session.connection.prompt({
499
+ sessionId: acpSessionId,
500
+ prompt: promptContent
501
+ });
502
+ emit("acp://prompt-complete", {
503
+ sessionId,
504
+ stopReason: result.stopReason ?? "end_turn"
505
+ });
506
+ } catch (err) {
507
+ emit("acp://error", {
508
+ sessionId,
509
+ error: `Prompt failed: ${err instanceof Error ? err.message : JSON.stringify(err)}`
510
+ });
511
+ throw err;
512
+ } finally {
513
+ session.cancelling = false;
514
+ if (session.status === "prompting") {
515
+ session.status = "ready";
516
+ }
517
+ }
518
+ }
519
+ async function acpCancel(params) {
520
+ const { sessionId } = params;
521
+ const session = sessions.get(sessionId);
522
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
523
+ if (session.cancelling) {
524
+ console.log(`[ACP] Cancel already in progress for ${sessionId}, ignoring duplicate`);
525
+ return;
526
+ }
527
+ session.cancelling = true;
528
+ try {
529
+ const acpSessionId = session.acpSessionId ?? sessionId;
530
+ await session.connection.cancel({ sessionId: acpSessionId });
531
+ } finally {
532
+ session.cancelling = false;
533
+ }
534
+ }
535
+ async function acpTerminate(params) {
536
+ const { sessionId } = params;
537
+ const session = sessions.get(sessionId);
538
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
539
+ session.process.kill();
540
+ sessions.delete(sessionId);
541
+ session.status = "terminated";
542
+ emit("acp://session-status", { sessionId, status: "terminated" });
543
+ }
544
+ async function acpListSessions() {
545
+ return Array.from(sessions.values()).map((s) => ({
546
+ id: s.id,
547
+ agentType: s.agentType,
548
+ cwd: s.cwd,
549
+ status: s.status,
550
+ createdAt: s.createdAt
551
+ }));
552
+ }
553
+ async function acpSetPermissionMode(params) {
554
+ const { sessionId, mode } = params;
555
+ const session = sessions.get(sessionId);
556
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
557
+ const acpSessionId = session.acpSessionId ?? sessionId;
558
+ await session.connection.setSessionMode({
559
+ sessionId: acpSessionId,
560
+ modeId: mode
561
+ });
562
+ }
563
+ async function acpRespondToPermission(params) {
564
+ const { sessionId, requestId, optionId } = params;
565
+ const session = sessions.get(sessionId);
566
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
567
+ const resolver = session.pendingPermissions.get(requestId);
568
+ if (!resolver) throw new Error(`No pending permission: ${requestId}`);
569
+ session.pendingPermissions.delete(requestId);
570
+ resolver(optionId);
571
+ }
572
+ async function acpRespondToDiffProposal(params) {
573
+ const { sessionId, proposalId, accepted } = params;
574
+ const session = sessions.get(sessionId);
575
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
576
+ const resolver = session.pendingDiffProposals.get(proposalId);
577
+ if (!resolver) throw new Error(`No pending diff proposal: ${proposalId}`);
578
+ session.pendingDiffProposals.delete(proposalId);
579
+ resolver(accepted);
580
+ }
581
+ async function acpGetAvailableAgents() {
582
+ const agents = [
583
+ { type: "claude-code", name: "Claude Code", description: "AI coding assistant by Anthropic", command: "seren-acp-claude" },
584
+ { type: "codex", name: "Codex", description: "AI coding assistant powered by OpenAI Codex", command: "seren-acp-codex" }
585
+ ];
586
+ return agents.map((agent) => {
587
+ let available = false;
588
+ let unavailableReason;
589
+ try {
590
+ findAgentCommand(agent.type);
591
+ available = true;
592
+ } catch (err) {
593
+ unavailableReason = err.message;
594
+ }
595
+ return { ...agent, available, unavailableReason };
596
+ });
597
+ }
598
+ async function acpCheckAgentAvailable(params) {
599
+ try {
600
+ findAgentCommand(params.agentType);
601
+ return true;
602
+ } catch {
603
+ return false;
604
+ }
605
+ }
606
+ async function acpEnsureClaudeCli() {
607
+ if (await isCommandAvailable("claude")) {
608
+ return "claude";
609
+ }
610
+ const npmCmd = platform() === "win32" ? "npm.cmd" : "npm";
611
+ return new Promise((resolve3, reject) => {
612
+ const proc = execFile(
613
+ npmCmd,
614
+ ["install", "-g", "@anthropic-ai/claude-code"],
615
+ (err, stdout, stderr) => {
616
+ if (err) {
617
+ reject(
618
+ new Error(
619
+ `Failed to install Claude Code CLI: ${stderr || err.message}`
620
+ )
621
+ );
622
+ return;
623
+ }
624
+ console.log(`[ACP] Claude Code CLI installed: ${stdout}`);
625
+ resolve3("claude");
626
+ }
627
+ );
628
+ });
629
+ }
630
+
631
+ // src/handlers/dialogs.ts
632
+ import { execFile as execFile2 } from "child_process";
633
+ import { platform as platform2 } from "os";
634
+ import { dirname } from "path";
635
+ var os = platform2();
636
+ function exec(cmd, args) {
637
+ return new Promise((resolve3, reject) => {
638
+ execFile2(cmd, args, { timeout: 6e4 }, (err, stdout) => {
639
+ if (err) {
640
+ if (err.code === 1 || err.killed) {
641
+ resolve3("");
642
+ return;
643
+ }
644
+ reject(err);
645
+ return;
646
+ }
647
+ resolve3(stdout.trim());
648
+ });
649
+ });
650
+ }
651
+ async function openFolderDialog() {
652
+ let result;
653
+ if (os === "darwin") {
654
+ result = await exec("osascript", [
655
+ "-e",
656
+ 'POSIX path of (choose folder with prompt "Select a folder")'
657
+ ]);
658
+ } else if (os === "linux") {
659
+ result = await exec("zenity", ["--file-selection", "--directory", "--title=Select a folder"]);
660
+ } else if (os === "win32") {
661
+ result = await exec("powershell", [
662
+ "-NoProfile",
663
+ "-Command",
664
+ "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null; $f = New-Object System.Windows.Forms.FolderBrowserDialog; if ($f.ShowDialog() -eq 'OK') { $f.SelectedPath }"
665
+ ]);
666
+ } else {
667
+ throw new Error(`Unsupported platform: ${os}`);
668
+ }
669
+ return result || null;
670
+ }
671
+ async function openFileDialog() {
672
+ let result;
673
+ if (os === "darwin") {
674
+ result = await exec("osascript", [
675
+ "-e",
676
+ 'POSIX path of (choose file with prompt "Select a file")'
677
+ ]);
678
+ } else if (os === "linux") {
679
+ result = await exec("zenity", ["--file-selection", "--title=Select a file"]);
680
+ } else if (os === "win32") {
681
+ result = await exec("powershell", [
682
+ "-NoProfile",
683
+ "-Command",
684
+ "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null; $f = New-Object System.Windows.Forms.OpenFileDialog; if ($f.ShowDialog() -eq 'OK') { $f.FileName }"
685
+ ]);
686
+ } else {
687
+ throw new Error(`Unsupported platform: ${os}`);
688
+ }
689
+ return result || null;
690
+ }
691
+ async function saveFileDialog(params) {
692
+ let result;
693
+ if (os === "darwin") {
694
+ const prompt = params.defaultPath ? `POSIX path of (choose file name with prompt "Save as" default name "${params.defaultPath}")` : 'POSIX path of (choose file name with prompt "Save as")';
695
+ result = await exec("osascript", ["-e", prompt]);
696
+ } else if (os === "linux") {
697
+ const args = ["--file-selection", "--save", "--title=Save as"];
698
+ if (params.defaultPath) args.push(`--filename=${params.defaultPath}`);
699
+ result = await exec("zenity", args);
700
+ } else if (os === "win32") {
701
+ result = await exec("powershell", [
702
+ "-NoProfile",
703
+ "-Command",
704
+ "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null; $f = New-Object System.Windows.Forms.SaveFileDialog; if ($f.ShowDialog() -eq 'OK') { $f.FileName }"
705
+ ]);
706
+ } else {
707
+ throw new Error(`Unsupported platform: ${os}`);
708
+ }
709
+ return result || null;
710
+ }
711
+ async function revealInFileManager(params) {
712
+ if (os === "darwin") {
713
+ await exec("open", ["-R", params.path]);
714
+ } else if (os === "linux") {
715
+ await exec("xdg-open", [dirname(params.path)]);
716
+ } else if (os === "win32") {
717
+ await exec("explorer", [`/select,${params.path}`]);
718
+ } else {
719
+ throw new Error(`Unsupported platform: ${os}`);
720
+ }
721
+ }
722
+
723
+ // src/handlers/fs.ts
724
+ import {
725
+ access,
726
+ readFile as fsReadFile,
727
+ realpath,
728
+ writeFile as fsWriteFile,
729
+ mkdir,
730
+ readdir,
731
+ rename,
732
+ rm,
733
+ stat
734
+ } from "fs/promises";
735
+ import { realpathSync as realpathSyncNative } from "fs";
736
+ import { homedir, tmpdir } from "os";
737
+ import { join, resolve as resolve2 } from "path";
738
+ var home = homedir();
739
+ var tmp = tmpdir();
740
+ var homeReal;
741
+ var tmpReal;
742
+ try {
743
+ homeReal = realpathSyncNative(home);
744
+ } catch {
745
+ homeReal = home;
746
+ }
747
+ try {
748
+ tmpReal = realpathSyncNative(tmp);
749
+ } catch {
750
+ tmpReal = tmp;
751
+ }
752
+ function isAllowedPath(p) {
753
+ return p.startsWith(home) || p.startsWith(tmp) || p.startsWith(homeReal) || p.startsWith(tmpReal);
754
+ }
755
+ async function validatePathReal(requestedPath) {
756
+ const resolved = resolve2(requestedPath);
757
+ if (!isAllowedPath(resolved)) {
758
+ throw new Error("Access denied: path must be within home directory");
759
+ }
760
+ try {
761
+ const real = await realpath(resolved);
762
+ if (!isAllowedPath(real)) {
763
+ throw new Error("Access denied: symlink target is outside home directory");
764
+ }
765
+ return real;
766
+ } catch (err) {
767
+ if (err.code === "ENOENT") {
768
+ return resolved;
769
+ }
770
+ throw err;
771
+ }
772
+ }
773
+ async function listDirectory(params) {
774
+ const dir = await validatePathReal(params.path);
775
+ const entries = await readdir(dir, { withFileTypes: true });
776
+ return entries.map((entry) => ({
777
+ name: entry.name,
778
+ path: join(dir, entry.name),
779
+ is_directory: entry.isDirectory()
780
+ }));
781
+ }
782
+ async function readFile2(params) {
783
+ const filePath = await validatePathReal(params.path);
784
+ return fsReadFile(filePath, "utf-8");
785
+ }
786
+ async function readFileBase64(params) {
787
+ const filePath = await validatePathReal(params.path);
788
+ const buffer = await fsReadFile(filePath);
789
+ return Buffer.from(buffer).toString("base64");
790
+ }
791
+ async function writeFile2(params) {
792
+ const filePath = await validatePathReal(params.path);
793
+ await fsWriteFile(filePath, params.content, "utf-8");
794
+ }
795
+ async function pathExists(params) {
796
+ const p = await validatePathReal(params.path);
797
+ try {
798
+ await access(p);
799
+ return true;
800
+ } catch {
801
+ return false;
802
+ }
803
+ }
804
+ async function isDirectory(params) {
805
+ const p = await validatePathReal(params.path);
806
+ try {
807
+ const s = await stat(p);
808
+ return s.isDirectory();
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
813
+ async function createFile(params) {
814
+ const filePath = await validatePathReal(params.path);
815
+ await fsWriteFile(filePath, params.content ?? "", "utf-8");
816
+ }
817
+ async function createDirectory(params) {
818
+ const dir = await validatePathReal(params.path);
819
+ await mkdir(dir, { recursive: true });
820
+ }
821
+ async function deletePath(params) {
822
+ const p = await validatePathReal(params.path);
823
+ await rm(p, { recursive: true, force: true });
824
+ }
825
+ async function renamePath(params) {
826
+ const oldP = await validatePathReal(params.oldPath);
827
+ const newP = await validatePathReal(params.newPath);
828
+ await rename(oldP, newP);
829
+ }
830
+
831
+ // src/services/vector-store.ts
832
+ import Database2 from "better-sqlite3";
833
+ import * as sqliteVec from "sqlite-vec";
834
+ import { createHash } from "crypto";
835
+ import { mkdirSync, existsSync as existsSync2 } from "fs";
836
+ import { join as join2 } from "path";
837
+ import { homedir as homedir2 } from "os";
838
+ var EMBEDDING_DIM = 1536;
839
+ function getDataDir() {
840
+ return join2(homedir2(), ".seren-local", "data");
841
+ }
842
+ function getVectorDbPath(projectPath) {
843
+ const hash = createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
844
+ return join2(getDataDir(), "indexes", `${hash}.db`);
845
+ }
846
+ function hasIndex(projectPath) {
847
+ return existsSync2(getVectorDbPath(projectPath));
848
+ }
849
+ function openDb(projectPath) {
850
+ const dbPath = getVectorDbPath(projectPath);
851
+ const dir = join2(getDataDir(), "indexes");
852
+ if (!existsSync2(dir)) {
853
+ mkdirSync(dir, { recursive: true });
854
+ }
855
+ const db2 = new Database2(dbPath);
856
+ sqliteVec.load(db2);
857
+ return db2;
858
+ }
859
+ function initVectorDb(projectPath) {
860
+ const db2 = openDb(projectPath);
861
+ db2.exec(`
862
+ CREATE TABLE IF NOT EXISTS code_chunks (
863
+ id INTEGER PRIMARY KEY,
864
+ file_path TEXT NOT NULL,
865
+ start_line INTEGER NOT NULL,
866
+ end_line INTEGER NOT NULL,
867
+ content TEXT NOT NULL,
868
+ chunk_type TEXT NOT NULL,
869
+ symbol_name TEXT,
870
+ language TEXT NOT NULL,
871
+ file_hash TEXT NOT NULL,
872
+ indexed_at INTEGER NOT NULL
873
+ )
874
+ `);
875
+ db2.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_file ON code_chunks(file_path)`);
876
+ db2.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_hash ON code_chunks(file_hash)`);
877
+ db2.exec(`
878
+ CREATE VIRTUAL TABLE IF NOT EXISTS code_embeddings USING vec0(
879
+ chunk_id INTEGER PRIMARY KEY,
880
+ embedding float[${EMBEDDING_DIM}]
881
+ )
882
+ `);
883
+ db2.exec(`
884
+ CREATE TABLE IF NOT EXISTS index_metadata (
885
+ key TEXT PRIMARY KEY,
886
+ value TEXT NOT NULL
887
+ )
888
+ `);
889
+ db2.prepare(
890
+ "INSERT OR REPLACE INTO index_metadata (key, value) VALUES ('project_path', ?)"
891
+ ).run(projectPath);
892
+ const stats = getIndexStats(db2);
893
+ db2.close();
894
+ return stats;
895
+ }
896
+ function getIndexStats(db2) {
897
+ const totalChunks = db2.prepare("SELECT COUNT(*) as c FROM code_chunks").get().c;
898
+ const totalFiles = db2.prepare("SELECT COUNT(DISTINCT file_path) as c FROM code_chunks").get().c;
899
+ const lastIndexed = db2.prepare("SELECT MAX(indexed_at) as m FROM code_chunks").get().m;
900
+ return {
901
+ total_chunks: totalChunks,
902
+ total_files: totalFiles,
903
+ last_indexed: lastIndexed ?? null
904
+ };
905
+ }
906
+ function getStats(projectPath) {
907
+ if (!hasIndex(projectPath)) {
908
+ return { total_chunks: 0, total_files: 0, last_indexed: null };
909
+ }
910
+ const db2 = openDb(projectPath);
911
+ const stats = getIndexStats(db2);
912
+ db2.close();
913
+ return stats;
914
+ }
915
+ function float32ArrayToBuffer(arr) {
916
+ const f32 = new Float32Array(arr);
917
+ return Buffer.from(f32.buffer);
918
+ }
919
+ function insertChunks(projectPath, chunks) {
920
+ const db2 = openDb(projectPath);
921
+ const now = Date.now();
922
+ const insertChunk = db2.prepare(`
923
+ INSERT INTO code_chunks (file_path, start_line, end_line, content, chunk_type, symbol_name, language, file_hash, indexed_at)
924
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
925
+ `);
926
+ const insertEmbedding = db2.prepare(`
927
+ INSERT INTO code_embeddings (chunk_id, embedding) VALUES (?, ?)
928
+ `);
929
+ const ids = [];
930
+ const transaction = db2.transaction(() => {
931
+ for (const chunk of chunks) {
932
+ if (chunk.embedding.length !== EMBEDDING_DIM) {
933
+ throw new Error(
934
+ `Embedding dimension mismatch: expected ${EMBEDDING_DIM}, got ${chunk.embedding.length}`
935
+ );
936
+ }
937
+ const result = insertChunk.run(
938
+ chunk.file_path,
939
+ chunk.start_line,
940
+ chunk.end_line,
941
+ chunk.content,
942
+ chunk.chunk_type,
943
+ chunk.symbol_name,
944
+ chunk.language,
945
+ chunk.file_hash,
946
+ now
947
+ );
948
+ const chunkId = Number(result.lastInsertRowid);
949
+ insertEmbedding.run(chunkId, float32ArrayToBuffer(chunk.embedding));
950
+ ids.push(chunkId);
951
+ }
952
+ });
953
+ transaction();
954
+ db2.close();
955
+ return ids;
956
+ }
957
+ function deleteFileChunks(projectPath, filePath) {
958
+ const db2 = openDb(projectPath);
959
+ const chunkIds = db2.prepare("SELECT id FROM code_chunks WHERE file_path = ?").all(filePath).map((row) => row.id);
960
+ const deleteEmbedding = db2.prepare(
961
+ "DELETE FROM code_embeddings WHERE chunk_id = ?"
962
+ );
963
+ for (const id of chunkIds) {
964
+ deleteEmbedding.run(id);
965
+ }
966
+ const result = db2.prepare("DELETE FROM code_chunks WHERE file_path = ?").run(filePath);
967
+ db2.close();
968
+ return result.changes;
969
+ }
970
+ function searchSimilar(projectPath, queryEmbedding, limit) {
971
+ const db2 = openDb(projectPath);
972
+ const embeddingBlob = float32ArrayToBuffer(queryEmbedding);
973
+ const rows = db2.prepare(
974
+ `SELECT
975
+ c.id, c.file_path, c.start_line, c.end_line, c.content,
976
+ c.chunk_type, c.symbol_name, c.language, c.file_hash, c.indexed_at,
977
+ e.distance
978
+ FROM code_embeddings e
979
+ JOIN code_chunks c ON c.id = e.chunk_id
980
+ WHERE e.embedding MATCH ?
981
+ ORDER BY e.distance
982
+ LIMIT ?`
983
+ ).all(embeddingBlob, limit);
984
+ db2.close();
985
+ return rows.map((row) => ({
986
+ chunk: {
987
+ id: row.id,
988
+ file_path: row.file_path,
989
+ start_line: row.start_line,
990
+ end_line: row.end_line,
991
+ content: row.content,
992
+ chunk_type: row.chunk_type,
993
+ symbol_name: row.symbol_name,
994
+ language: row.language,
995
+ file_hash: row.file_hash,
996
+ indexed_at: row.indexed_at
997
+ },
998
+ distance: row.distance
999
+ }));
1000
+ }
1001
+ function fileNeedsReindex(projectPath, filePath, currentHash) {
1002
+ if (!hasIndex(projectPath)) return true;
1003
+ const db2 = openDb(projectPath);
1004
+ const row = db2.prepare("SELECT file_hash FROM code_chunks WHERE file_path = ? LIMIT 1").get(filePath);
1005
+ db2.close();
1006
+ if (!row) return true;
1007
+ return row.file_hash !== currentHash;
1008
+ }
1009
+
1010
+ // src/services/chunker.ts
1011
+ import { readFileSync, readdirSync, statSync } from "fs";
1012
+ import { join as join3, extname, relative } from "path";
1013
+ import { createHash as createHash2 } from "crypto";
1014
+ var MAX_CHUNK_LINES = 100;
1015
+ var MIN_CHUNK_LINES = 5;
1016
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1017
+ var IGNORE_PATTERNS = [
1018
+ "node_modules",
1019
+ ".git",
1020
+ ".svn",
1021
+ ".hg",
1022
+ "target",
1023
+ "dist",
1024
+ "build",
1025
+ ".next",
1026
+ ".nuxt",
1027
+ "__pycache__",
1028
+ ".pytest_cache",
1029
+ ".mypy_cache",
1030
+ "venv",
1031
+ ".venv",
1032
+ "env",
1033
+ ".env",
1034
+ ".idea",
1035
+ ".vscode",
1036
+ ".DS_Store",
1037
+ "Thumbs.db",
1038
+ "package-lock.json",
1039
+ "yarn.lock",
1040
+ "pnpm-lock.yaml",
1041
+ "Cargo.lock"
1042
+ ];
1043
+ var WILDCARD_SUFFIXES = [".min.js", ".min.css", ".map"];
1044
+ var LANG_MAP = {
1045
+ rs: "rust",
1046
+ ts: "typescript",
1047
+ tsx: "typescript",
1048
+ js: "javascript",
1049
+ jsx: "javascript",
1050
+ py: "python",
1051
+ go: "go",
1052
+ java: "java",
1053
+ c: "c",
1054
+ h: "c",
1055
+ cpp: "cpp",
1056
+ cc: "cpp",
1057
+ cxx: "cpp",
1058
+ hpp: "cpp",
1059
+ cs: "csharp",
1060
+ rb: "ruby",
1061
+ php: "php",
1062
+ swift: "swift",
1063
+ kt: "kotlin",
1064
+ kts: "kotlin",
1065
+ scala: "scala",
1066
+ r: "r",
1067
+ sql: "sql",
1068
+ sh: "shell",
1069
+ bash: "shell",
1070
+ zsh: "shell",
1071
+ ps1: "powershell",
1072
+ yml: "yaml",
1073
+ yaml: "yaml",
1074
+ json: "json",
1075
+ toml: "toml",
1076
+ xml: "xml",
1077
+ html: "html",
1078
+ htm: "html",
1079
+ css: "css",
1080
+ scss: "css",
1081
+ sass: "css",
1082
+ less: "css",
1083
+ md: "markdown",
1084
+ markdown: "markdown",
1085
+ vue: "vue",
1086
+ svelte: "svelte"
1087
+ };
1088
+ function detectLanguage(filePath) {
1089
+ const ext = extname(filePath).slice(1).toLowerCase();
1090
+ return LANG_MAP[ext] ?? null;
1091
+ }
1092
+ function shouldIgnore(name) {
1093
+ if (IGNORE_PATTERNS.includes(name)) return true;
1094
+ for (const suffix of WILDCARD_SUFFIXES) {
1095
+ if (name.endsWith(suffix)) return true;
1096
+ }
1097
+ return false;
1098
+ }
1099
+ function computeHash(content) {
1100
+ return createHash2("sha256").update(content).digest("hex").slice(0, 16);
1101
+ }
1102
+ function discoverFiles(projectPath) {
1103
+ const files = [];
1104
+ discoverRecursive(projectPath, projectPath, files);
1105
+ return files;
1106
+ }
1107
+ function discoverRecursive(root, current, files) {
1108
+ let entries;
1109
+ try {
1110
+ entries = readdirSync(current);
1111
+ } catch {
1112
+ return;
1113
+ }
1114
+ for (const name of entries) {
1115
+ if (shouldIgnore(name)) continue;
1116
+ const fullPath = join3(current, name);
1117
+ let stat2;
1118
+ try {
1119
+ stat2 = statSync(fullPath);
1120
+ } catch {
1121
+ continue;
1122
+ }
1123
+ if (stat2.isDirectory()) {
1124
+ discoverRecursive(root, fullPath, files);
1125
+ } else if (stat2.isFile()) {
1126
+ const language = detectLanguage(fullPath);
1127
+ if (!language) continue;
1128
+ if (stat2.size > MAX_FILE_SIZE) continue;
1129
+ let content;
1130
+ try {
1131
+ content = readFileSync(fullPath, "utf-8");
1132
+ } catch {
1133
+ continue;
1134
+ }
1135
+ files.push({
1136
+ path: fullPath,
1137
+ relative_path: relative(root, fullPath),
1138
+ language,
1139
+ size: stat2.size,
1140
+ hash: computeHash(content)
1141
+ });
1142
+ }
1143
+ }
1144
+ }
1145
+ function chunkFile(file) {
1146
+ const content = readFileSync(file.path, "utf-8");
1147
+ const lines = content.split("\n");
1148
+ let chunks;
1149
+ switch (file.language) {
1150
+ case "rust":
1151
+ chunks = chunkBraceLanguage(lines, detectRustBlock);
1152
+ break;
1153
+ case "typescript":
1154
+ case "javascript":
1155
+ chunks = chunkBraceLanguage(lines, detectJsBlock);
1156
+ break;
1157
+ case "python":
1158
+ chunks = chunkPython(lines);
1159
+ break;
1160
+ default:
1161
+ chunks = chunkGeneric(lines);
1162
+ }
1163
+ if (chunks.length === 0) {
1164
+ chunks = chunkGeneric(lines);
1165
+ }
1166
+ return { file, chunks };
1167
+ }
1168
+ function chunkBraceLanguage(lines, detect) {
1169
+ const chunks = [];
1170
+ let currentStart = null;
1171
+ let braceDepth = 0;
1172
+ let currentType = "block";
1173
+ let currentName = null;
1174
+ for (let i = 0; i < lines.length; i++) {
1175
+ const line = lines[i];
1176
+ const trimmed = line.trim();
1177
+ if (currentStart === null) {
1178
+ const block = detect(trimmed);
1179
+ if (block) {
1180
+ currentStart = i;
1181
+ currentType = block.type;
1182
+ currentName = block.name;
1183
+ braceDepth = 0;
1184
+ }
1185
+ }
1186
+ if (currentStart !== null) {
1187
+ for (const ch of line) {
1188
+ if (ch === "{") braceDepth++;
1189
+ else if (ch === "}") braceDepth--;
1190
+ }
1191
+ if (braceDepth <= 0 && (line.includes("}") || line.includes(";"))) {
1192
+ const content = lines.slice(currentStart, i + 1).join("\n");
1193
+ if (i - currentStart + 1 >= MIN_CHUNK_LINES) {
1194
+ chunks.push({
1195
+ start_line: currentStart + 1,
1196
+ end_line: i + 1,
1197
+ content,
1198
+ chunk_type: currentType,
1199
+ symbol_name: currentName
1200
+ });
1201
+ }
1202
+ currentStart = null;
1203
+ currentName = null;
1204
+ }
1205
+ }
1206
+ }
1207
+ return chunks;
1208
+ }
1209
+ function extractIdAfter(line, keyword) {
1210
+ const idx = line.indexOf(keyword);
1211
+ if (idx === -1) return null;
1212
+ const after = line.slice(idx + keyword.length);
1213
+ const match = after.match(/^[a-zA-Z_]\w*/);
1214
+ return match ? match[0] : null;
1215
+ }
1216
+ function detectRustBlock(line) {
1217
+ if (line.startsWith("//") || line.startsWith("#[")) return null;
1218
+ if (/^(pub\s+)?(async\s+)?fn\s/.test(line))
1219
+ return { type: "function", name: extractIdAfter(line, "fn ") };
1220
+ if (/^impl[\s<]/.test(line))
1221
+ return { type: "class", name: extractIdAfter(line, "impl ") };
1222
+ if (/^(pub\s+)?struct\s/.test(line))
1223
+ return { type: "class", name: extractIdAfter(line, "struct ") };
1224
+ if (/^(pub\s+)?enum\s/.test(line))
1225
+ return { type: "class", name: extractIdAfter(line, "enum ") };
1226
+ if (/^(pub\s+)?mod\s/.test(line))
1227
+ return { type: "module", name: extractIdAfter(line, "mod ") };
1228
+ return null;
1229
+ }
1230
+ function detectJsBlock(line) {
1231
+ if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) return null;
1232
+ if (/^(export\s+)?(async\s+)?function\s/.test(line))
1233
+ return { type: "function", name: extractIdAfter(line, "function ") };
1234
+ if (/^(export\s+)?const\s.*=\s*(async\s+)?\(/.test(line))
1235
+ return { type: "function", name: extractIdAfter(line, "const ") };
1236
+ if (/^(export\s+)?(default\s+)?class\s/.test(line))
1237
+ return { type: "class", name: extractIdAfter(line, "class ") };
1238
+ if (/^(export\s+)?interface\s/.test(line))
1239
+ return { type: "class", name: extractIdAfter(line, "interface ") };
1240
+ if (/^(export\s+)?type\s/.test(line))
1241
+ return { type: "class", name: extractIdAfter(line, "type ") };
1242
+ return null;
1243
+ }
1244
+ function chunkPython(lines) {
1245
+ const chunks = [];
1246
+ let currentStart = null;
1247
+ let currentIndent = 0;
1248
+ let currentType = "block";
1249
+ let currentName = null;
1250
+ for (let i = 0; i < lines.length; i++) {
1251
+ const line = lines[i];
1252
+ const trimmed = line.trim();
1253
+ const indent = line.length - line.trimStart().length;
1254
+ if (currentStart === null) {
1255
+ const block = detectPythonBlock(trimmed);
1256
+ if (block) {
1257
+ currentStart = i;
1258
+ currentIndent = indent;
1259
+ currentType = block.type;
1260
+ currentName = block.name;
1261
+ }
1262
+ } else if (trimmed.length > 0 && indent <= currentIndent && i > currentStart) {
1263
+ const content = lines.slice(currentStart, i).join("\n");
1264
+ if (i - currentStart >= MIN_CHUNK_LINES) {
1265
+ chunks.push({
1266
+ start_line: currentStart + 1,
1267
+ end_line: i,
1268
+ content,
1269
+ chunk_type: currentType,
1270
+ symbol_name: currentName
1271
+ });
1272
+ }
1273
+ currentStart = null;
1274
+ currentName = null;
1275
+ const block = detectPythonBlock(trimmed);
1276
+ if (block) {
1277
+ currentStart = i;
1278
+ currentIndent = indent;
1279
+ currentType = block.type;
1280
+ currentName = block.name;
1281
+ }
1282
+ }
1283
+ }
1284
+ if (currentStart !== null) {
1285
+ const content = lines.slice(currentStart).join("\n");
1286
+ if (lines.length - currentStart >= MIN_CHUNK_LINES) {
1287
+ chunks.push({
1288
+ start_line: currentStart + 1,
1289
+ end_line: lines.length,
1290
+ content,
1291
+ chunk_type: currentType,
1292
+ symbol_name: currentName
1293
+ });
1294
+ }
1295
+ }
1296
+ return chunks;
1297
+ }
1298
+ function detectPythonBlock(line) {
1299
+ if (line.startsWith("#") || line.startsWith('"""') || line.startsWith("'''")) return null;
1300
+ if (/^(async\s+)?def\s/.test(line))
1301
+ return { type: "function", name: extractIdAfter(line, "def ") };
1302
+ if (/^class\s/.test(line))
1303
+ return { type: "class", name: extractIdAfter(line, "class ") };
1304
+ return null;
1305
+ }
1306
+ function chunkGeneric(lines) {
1307
+ const chunks = [];
1308
+ if (lines.length <= MAX_CHUNK_LINES) {
1309
+ if (lines.length >= MIN_CHUNK_LINES) {
1310
+ chunks.push({
1311
+ start_line: 1,
1312
+ end_line: lines.length,
1313
+ content: lines.join("\n"),
1314
+ chunk_type: "file",
1315
+ symbol_name: null
1316
+ });
1317
+ }
1318
+ return chunks;
1319
+ }
1320
+ let start = 0;
1321
+ while (start < lines.length) {
1322
+ const end = Math.min(start + MAX_CHUNK_LINES, lines.length);
1323
+ chunks.push({
1324
+ start_line: start + 1,
1325
+ end_line: end,
1326
+ content: lines.slice(start, end).join("\n"),
1327
+ chunk_type: "block",
1328
+ symbol_name: null
1329
+ });
1330
+ start = end;
1331
+ }
1332
+ return chunks;
1333
+ }
1334
+ function estimateIndexing(files) {
1335
+ let totalChunks = 0;
1336
+ let totalTokens = 0;
1337
+ for (const file of files) {
1338
+ try {
1339
+ const chunked = chunkFile(file);
1340
+ totalChunks += chunked.chunks.length;
1341
+ for (const chunk of chunked.chunks) {
1342
+ totalTokens += Math.floor(chunk.content.length / 4);
1343
+ }
1344
+ } catch {
1345
+ }
1346
+ }
1347
+ return { chunks: totalChunks, tokens: totalTokens };
1348
+ }
1349
+
1350
+ // src/handlers/indexing.ts
1351
+ async function initProjectIndex(params) {
1352
+ return initVectorDb(params.projectPath);
1353
+ }
1354
+ async function getIndexStatus(params) {
1355
+ return getStats(params.projectPath);
1356
+ }
1357
+ async function hasProjectIndex(params) {
1358
+ return hasIndex(params.projectPath);
1359
+ }
1360
+ async function searchCodebase(params) {
1361
+ if (params.queryEmbedding.length !== EMBEDDING_DIM) {
1362
+ throw new Error(
1363
+ `Query embedding dimension mismatch: expected ${EMBEDDING_DIM}, got ${params.queryEmbedding.length}`
1364
+ );
1365
+ }
1366
+ return searchSimilar(
1367
+ params.projectPath,
1368
+ params.queryEmbedding,
1369
+ params.limit
1370
+ );
1371
+ }
1372
+ async function fileNeedsReindex2(params) {
1373
+ return fileNeedsReindex(
1374
+ params.projectPath,
1375
+ params.filePath,
1376
+ params.fileHash
1377
+ );
1378
+ }
1379
+ async function deleteFileIndex(params) {
1380
+ return deleteFileChunks(params.projectPath, params.filePath);
1381
+ }
1382
+ async function indexChunks(params) {
1383
+ return insertChunks(params.projectPath, params.chunks);
1384
+ }
1385
+ async function discoverProjectFiles(params) {
1386
+ return discoverFiles(params.projectPath);
1387
+ }
1388
+ async function chunkFile2(params) {
1389
+ return chunkFile(params.file);
1390
+ }
1391
+ async function estimateIndexing2(params) {
1392
+ return estimateIndexing(params.files);
1393
+ }
1394
+ async function computeFileHash(params) {
1395
+ return computeHash(params.content);
1396
+ }
1397
+ async function getEmbeddingDimension() {
1398
+ return EMBEDDING_DIM;
1399
+ }
1400
+
1401
+ // src/handlers/mcp.ts
1402
+ import { randomUUID as randomUUID2 } from "crypto";
1403
+ var processes = /* @__PURE__ */ new Map();
1404
+ function sendRequest(proc, method, params) {
1405
+ const id = randomUUID2();
1406
+ return new Promise((resolve3, reject) => {
1407
+ const timeout = setTimeout(() => {
1408
+ proc.pendingRequests.delete(id);
1409
+ reject(new Error(`MCP request timeout: ${method}`));
1410
+ }, 3e4);
1411
+ proc.pendingRequests.set(id, {
1412
+ resolve: (v) => {
1413
+ clearTimeout(timeout);
1414
+ resolve3(v);
1415
+ },
1416
+ reject: (e) => {
1417
+ clearTimeout(timeout);
1418
+ reject(e);
1419
+ }
1420
+ });
1421
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
1422
+ proc.child.stdin?.write(msg + "\n");
1423
+ });
1424
+ }
1425
+ async function mcpDisconnect(params) {
1426
+ const proc = processes.get(params.serverName);
1427
+ if (proc) {
1428
+ proc.child.kill();
1429
+ processes.delete(params.serverName);
1430
+ }
1431
+ }
1432
+ async function mcpReadResource(params) {
1433
+ const proc = processes.get(params.serverName);
1434
+ if (!proc) {
1435
+ throw new Error(`Server '${params.serverName}' not connected`);
1436
+ }
1437
+ return sendRequest(proc, "resources/read", { uri: params.uri });
1438
+ }
1439
+
1440
+ // src/handlers/openclaw.ts
1441
+ import { execFile as execFile3, spawn as spawn2 } from "child_process";
1442
+ import { createServer } from "net";
1443
+ import {
1444
+ readFile as readFile3,
1445
+ writeFile as writeFile3,
1446
+ mkdir as mkdir2,
1447
+ access as access2,
1448
+ constants
1449
+ } from "fs/promises";
1450
+ import { join as join4 } from "path";
1451
+ import { homedir as homedir3 } from "os";
1452
+ import { randomBytes } from "crypto";
1453
+ import WebSocket from "ws";
1454
+ var childProcess = null;
1455
+ var processStatus = "stopped";
1456
+ var hookToken = null;
1457
+ var port = 0;
1458
+ var restartCount = 0;
1459
+ var startedAt = null;
1460
+ var wsClient = null;
1461
+ var reconnectTimer = null;
1462
+ var monitorTimer = null;
1463
+ var channels = [];
1464
+ var trustSettings = /* @__PURE__ */ new Map();
1465
+ var approvedIds = /* @__PURE__ */ new Set();
1466
+ var MAX_RESTART_ATTEMPTS = 3;
1467
+ var OPENCLAW_DIR = join4(homedir3(), ".openclaw");
1468
+ var CONFIG_PATH = join4(OPENCLAW_DIR, "openclaw.json");
1469
+ var SETTINGS_PATH = join4(homedir3(), ".seren-local", "settings.json");
1470
+ async function loadSettings() {
1471
+ try {
1472
+ const data = await readFile3(SETTINGS_PATH, "utf-8");
1473
+ return JSON.parse(data);
1474
+ } catch {
1475
+ return {};
1476
+ }
1477
+ }
1478
+ async function saveSettings(settings) {
1479
+ await mkdir2(join4(homedir3(), ".seren-local"), { recursive: true });
1480
+ await writeFile3(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
1481
+ }
1482
+ async function getSetting(params) {
1483
+ const settings = await loadSettings();
1484
+ return settings[params.key] ?? null;
1485
+ }
1486
+ async function setSetting(params) {
1487
+ const settings = await loadSettings();
1488
+ settings[params.key] = params.value;
1489
+ await saveSettings(settings);
1490
+ }
1491
+ function findAvailablePort() {
1492
+ return new Promise((resolve3, reject) => {
1493
+ const server = createServer();
1494
+ server.listen(0, "127.0.0.1", () => {
1495
+ const addr = server.address();
1496
+ if (addr && typeof addr === "object") {
1497
+ const p = addr.port;
1498
+ server.close(() => resolve3(p));
1499
+ } else {
1500
+ server.close(() => reject(new Error("Failed to get port")));
1501
+ }
1502
+ });
1503
+ server.on("error", reject);
1504
+ });
1505
+ }
1506
+ async function getOrCreateToken() {
1507
+ if (hookToken) return hookToken;
1508
+ try {
1509
+ const config2 = JSON.parse(await readFile3(CONFIG_PATH, "utf-8"));
1510
+ if (config2.hookToken) {
1511
+ hookToken = config2.hookToken;
1512
+ return hookToken;
1513
+ }
1514
+ } catch {
1515
+ }
1516
+ hookToken = randomBytes(32).toString("hex");
1517
+ await mkdir2(OPENCLAW_DIR, { recursive: true });
1518
+ let config = {};
1519
+ try {
1520
+ config = JSON.parse(await readFile3(CONFIG_PATH, "utf-8"));
1521
+ } catch {
1522
+ }
1523
+ config.hookToken = hookToken;
1524
+ await writeFile3(CONFIG_PATH, JSON.stringify(config, null, 2), {
1525
+ mode: 384
1526
+ });
1527
+ return hookToken;
1528
+ }
1529
+ async function findOpenClawEntrypoint() {
1530
+ const candidates = [
1531
+ // Global npm install
1532
+ join4(homedir3(), ".seren-local", "lib", "node_modules", "openclaw", "openclaw.mjs"),
1533
+ // Development
1534
+ join4(homedir3(), ".openclaw", "openclaw.mjs")
1535
+ ];
1536
+ try {
1537
+ const cmd = process.platform === "win32" ? "where" : "which";
1538
+ const path = await new Promise((resolve3, reject) => {
1539
+ execFile3(cmd, ["openclaw"], (err, stdout) => {
1540
+ if (err) reject(err);
1541
+ else resolve3(stdout.trim());
1542
+ });
1543
+ });
1544
+ if (path) candidates.unshift(path);
1545
+ } catch {
1546
+ }
1547
+ for (const candidate of candidates) {
1548
+ try {
1549
+ await access2(candidate, constants.R_OK);
1550
+ return candidate;
1551
+ } catch {
1552
+ }
1553
+ }
1554
+ throw new Error(
1555
+ "OpenClaw not found. Install it or ensure openclaw.mjs is in ~/.openclaw/"
1556
+ );
1557
+ }
1558
+ function connectWebSocket() {
1559
+ if (!port || !hookToken) return;
1560
+ const url = `ws://127.0.0.1:${port}/ws?token=${hookToken}`;
1561
+ wsClient = new WebSocket(url);
1562
+ wsClient.on("open", () => {
1563
+ console.log("[OpenClaw] WebSocket connected");
1564
+ });
1565
+ wsClient.on("message", (data) => {
1566
+ try {
1567
+ const msg = JSON.parse(data.toString());
1568
+ handleOpenClawEvent(msg);
1569
+ } catch (err) {
1570
+ console.error("[OpenClaw] Failed to parse WS message:", err);
1571
+ }
1572
+ });
1573
+ wsClient.on("close", () => {
1574
+ wsClient = null;
1575
+ if (processStatus === "running") {
1576
+ scheduleWsReconnect();
1577
+ }
1578
+ });
1579
+ wsClient.on("error", (err) => {
1580
+ console.error("[OpenClaw] WebSocket error:", err.message);
1581
+ });
1582
+ }
1583
+ function scheduleWsReconnect() {
1584
+ if (reconnectTimer) return;
1585
+ const delay = Math.min(1e3 * 2 ** restartCount, 3e4);
1586
+ reconnectTimer = setTimeout(() => {
1587
+ reconnectTimer = null;
1588
+ if (processStatus === "running") {
1589
+ connectWebSocket();
1590
+ }
1591
+ }, delay);
1592
+ }
1593
+ function handleOpenClawEvent(msg) {
1594
+ const { type, ...payload } = msg;
1595
+ switch (type) {
1596
+ case "channel:connected":
1597
+ case "channel:disconnected":
1598
+ case "channel:error":
1599
+ emit("openclaw://channel-event", { type, ...payload });
1600
+ break;
1601
+ case "message:received":
1602
+ emit("openclaw://message-received", payload);
1603
+ break;
1604
+ default:
1605
+ console.log("[OpenClaw] Unknown event:", type);
1606
+ }
1607
+ }
1608
+ function startProcessMonitor() {
1609
+ if (monitorTimer) return;
1610
+ monitorTimer = setInterval(() => {
1611
+ if (!childProcess || processStatus !== "running") return;
1612
+ try {
1613
+ childProcess.kill(0);
1614
+ } catch {
1615
+ processStatus = "crashed";
1616
+ emit("openclaw://status-changed", { status: "crashed" });
1617
+ if (restartCount < MAX_RESTART_ATTEMPTS) {
1618
+ restartCount++;
1619
+ processStatus = "restarting";
1620
+ emit("openclaw://status-changed", { status: "restarting" });
1621
+ openclawStart({}).catch(
1622
+ (err) => console.error("[OpenClaw] Auto-restart failed:", err)
1623
+ );
1624
+ }
1625
+ }
1626
+ }, 2e3);
1627
+ }
1628
+ function stopProcessMonitor() {
1629
+ if (monitorTimer) {
1630
+ clearInterval(monitorTimer);
1631
+ monitorTimer = null;
1632
+ }
1633
+ }
1634
+ async function openclawStart(_params) {
1635
+ if (processStatus === "running") return;
1636
+ processStatus = "starting";
1637
+ emit("openclaw://status-changed", { status: "starting" });
1638
+ const entrypoint = await findOpenClawEntrypoint();
1639
+ const token = await getOrCreateToken();
1640
+ port = await findAvailablePort();
1641
+ childProcess = spawn2("node", [entrypoint, "gateway", "--allow-unconfigured"], {
1642
+ cwd: OPENCLAW_DIR,
1643
+ stdio: ["pipe", "pipe", "pipe"],
1644
+ env: {
1645
+ ...process.env,
1646
+ OPENCLAW_GATEWAY_PORT: String(port),
1647
+ OPENCLAW_GATEWAY_TOKEN: token,
1648
+ OPENCLAW_GATEWAY_HOST: "127.0.0.1",
1649
+ OPENCLAW_SKIP_CHANNELS: "1"
1650
+ }
1651
+ });
1652
+ if (childProcess.stdout) {
1653
+ const readline = await import("readline");
1654
+ const rl = readline.createInterface({ input: childProcess.stdout });
1655
+ rl.on("line", (line) => console.log(`[OpenClaw stdout] ${line}`));
1656
+ }
1657
+ if (childProcess.stderr) {
1658
+ const readline = await import("readline");
1659
+ const rl = readline.createInterface({ input: childProcess.stderr });
1660
+ rl.on("line", (line) => console.log(`[OpenClaw stderr] ${line}`));
1661
+ }
1662
+ childProcess.on("exit", (code, signal) => {
1663
+ console.log(`[OpenClaw] Process exited: code=${code}, signal=${signal}`);
1664
+ childProcess = null;
1665
+ if (processStatus !== "stopped" && processStatus !== "restarting") {
1666
+ processStatus = "crashed";
1667
+ emit("openclaw://status-changed", { status: "crashed" });
1668
+ }
1669
+ });
1670
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
1671
+ if (childProcess && !childProcess.killed) {
1672
+ processStatus = "running";
1673
+ startedAt = Date.now();
1674
+ restartCount = 0;
1675
+ emit("openclaw://status-changed", { status: "running" });
1676
+ connectWebSocket();
1677
+ startProcessMonitor();
1678
+ } else {
1679
+ processStatus = "crashed";
1680
+ emit("openclaw://status-changed", { status: "crashed" });
1681
+ throw new Error("OpenClaw process failed to start");
1682
+ }
1683
+ }
1684
+ async function openclawStop(_params) {
1685
+ processStatus = "stopped";
1686
+ emit("openclaw://status-changed", { status: "stopped" });
1687
+ stopProcessMonitor();
1688
+ if (wsClient) {
1689
+ wsClient.close();
1690
+ wsClient = null;
1691
+ }
1692
+ if (reconnectTimer) {
1693
+ clearTimeout(reconnectTimer);
1694
+ reconnectTimer = null;
1695
+ }
1696
+ if (childProcess) {
1697
+ childProcess.kill("SIGTERM");
1698
+ const proc = childProcess;
1699
+ setTimeout(() => {
1700
+ try {
1701
+ proc.kill("SIGKILL");
1702
+ } catch {
1703
+ }
1704
+ }, 5e3);
1705
+ childProcess = null;
1706
+ }
1707
+ startedAt = null;
1708
+ }
1709
+ async function openclawRestart(_params) {
1710
+ await openclawStop({});
1711
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
1712
+ await openclawStart({});
1713
+ }
1714
+ async function openclawStatus(_params) {
1715
+ return {
1716
+ status: processStatus,
1717
+ port: processStatus === "running" ? port : null,
1718
+ uptimeSecs: startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : null,
1719
+ channels,
1720
+ restartCount
1721
+ };
1722
+ }
1723
+ async function openclawListChannels(_params) {
1724
+ if (processStatus !== "running") return [];
1725
+ const entrypoint = await findOpenClawEntrypoint();
1726
+ return new Promise((resolve3) => {
1727
+ execFile3(
1728
+ "node",
1729
+ [entrypoint, "channels", "status", "--json"],
1730
+ { cwd: OPENCLAW_DIR, timeout: 1e4 },
1731
+ (err, stdout) => {
1732
+ if (err) {
1733
+ console.error("[OpenClaw] Failed to list channels:", err);
1734
+ resolve3([]);
1735
+ return;
1736
+ }
1737
+ try {
1738
+ const data = JSON.parse(stdout);
1739
+ const result = [];
1740
+ const accounts = data.channelAccounts || {};
1741
+ for (const [id, info] of Object.entries(accounts)) {
1742
+ const ch = info;
1743
+ result.push({
1744
+ id,
1745
+ platform: id.split(":")[0] || id,
1746
+ displayName: ch.label || id,
1747
+ status: ch.running ? "connected" : "disconnected",
1748
+ errorMessage: ch.error
1749
+ });
1750
+ }
1751
+ channels.length = 0;
1752
+ channels.push(...result);
1753
+ resolve3(result);
1754
+ } catch {
1755
+ resolve3([]);
1756
+ }
1757
+ }
1758
+ );
1759
+ });
1760
+ }
1761
+ async function openclawConnectChannel(params) {
1762
+ const { platform: plat, credentials } = params;
1763
+ switch (plat) {
1764
+ case "signal":
1765
+ if (!credentials?.phone) throw new Error("Signal requires a phone number");
1766
+ break;
1767
+ case "telegram":
1768
+ if (!credentials?.botToken) throw new Error("Telegram requires a bot token");
1769
+ break;
1770
+ case "discord":
1771
+ if (!credentials?.botToken) throw new Error("Discord requires a bot token");
1772
+ break;
1773
+ case "slack":
1774
+ if (!credentials?.botToken || !credentials?.appToken)
1775
+ throw new Error("Slack requires both botToken and appToken");
1776
+ break;
1777
+ }
1778
+ let config = {};
1779
+ try {
1780
+ config = JSON.parse(await readFile3(CONFIG_PATH, "utf-8"));
1781
+ } catch {
1782
+ }
1783
+ if (!config.channels) config.channels = {};
1784
+ switch (plat) {
1785
+ case "signal":
1786
+ config.channels.signal = {
1787
+ enabled: true,
1788
+ account: credentials.phone
1789
+ };
1790
+ break;
1791
+ case "telegram":
1792
+ config.channels.telegram = {
1793
+ enabled: true,
1794
+ botToken: credentials.botToken
1795
+ };
1796
+ break;
1797
+ case "discord":
1798
+ config.channels.discord = {
1799
+ enabled: true,
1800
+ botToken: credentials.botToken
1801
+ };
1802
+ break;
1803
+ case "slack":
1804
+ config.channels.slack = {
1805
+ enabled: true,
1806
+ botToken: credentials.botToken,
1807
+ appToken: credentials.appToken
1808
+ };
1809
+ break;
1810
+ case "whatsapp":
1811
+ config.channels.whatsapp = { enabled: true };
1812
+ break;
1813
+ default:
1814
+ config.channels[plat] = { enabled: true, ...credentials };
1815
+ }
1816
+ if (hookToken) config.hookToken = hookToken;
1817
+ await mkdir2(OPENCLAW_DIR, { recursive: true });
1818
+ await writeFile3(CONFIG_PATH, JSON.stringify(config, null, 2), {
1819
+ mode: 384
1820
+ });
1821
+ return { success: true, platform: plat };
1822
+ }
1823
+ async function openclawDisconnectChannel(params) {
1824
+ const { channelId } = params;
1825
+ const entrypoint = await findOpenClawEntrypoint();
1826
+ return new Promise((resolve3, reject) => {
1827
+ execFile3(
1828
+ "node",
1829
+ [entrypoint, "channels", "remove", "--channel", channelId, "--delete"],
1830
+ { cwd: OPENCLAW_DIR, timeout: 1e4 },
1831
+ (err) => {
1832
+ if (err) reject(new Error(`Failed to disconnect: ${err.message}`));
1833
+ else resolve3();
1834
+ }
1835
+ );
1836
+ });
1837
+ }
1838
+ async function openclawSetTrust(params) {
1839
+ const { channelId, trustLevel, agentMode } = params;
1840
+ trustSettings.set(channelId, { trustLevel, agentMode });
1841
+ const settings = await loadSettings();
1842
+ const trust = settings.openclawTrust || {};
1843
+ trust[channelId] = { trustLevel, agentMode };
1844
+ settings.openclawTrust = trust;
1845
+ await saveSettings(settings);
1846
+ }
1847
+ async function openclawSend(params) {
1848
+ const { channel, to, message } = params;
1849
+ if (processStatus !== "running" || !port || !hookToken) {
1850
+ throw new Error("OpenClaw is not running");
1851
+ }
1852
+ const key = `${channel}:${to}`;
1853
+ const trust = trustSettings.get(channel);
1854
+ if (trust?.trustLevel === "approval-required" && !approvedIds.has(key)) {
1855
+ emit("openclaw://approval-needed", {
1856
+ id: `${key}:${Date.now()}`,
1857
+ channel,
1858
+ to,
1859
+ message,
1860
+ platform: channel.split(":")[0] || channel
1861
+ });
1862
+ throw new Error("Message requires approval");
1863
+ }
1864
+ approvedIds.delete(key);
1865
+ const url = `http://127.0.0.1:${port}/hooks/agent`;
1866
+ const body = JSON.stringify({ message, channel, to });
1867
+ const response = await fetch(url, {
1868
+ method: "POST",
1869
+ headers: {
1870
+ "Content-Type": "application/json",
1871
+ Authorization: `Bearer ${hookToken}`
1872
+ },
1873
+ body
1874
+ });
1875
+ if (!response.ok) {
1876
+ throw new Error(`OpenClaw send failed: ${response.status}`);
1877
+ }
1878
+ const result = await response.text();
1879
+ return result;
1880
+ }
1881
+ async function openclawGrantApproval(params) {
1882
+ const { channel, to } = params;
1883
+ approvedIds.add(`${channel}:${to}`);
1884
+ }
1885
+ async function openclawGetQr(params) {
1886
+ const { platform: plat } = params;
1887
+ const entrypoint = await findOpenClawEntrypoint();
1888
+ return new Promise((resolve3, reject) => {
1889
+ execFile3(
1890
+ "node",
1891
+ [entrypoint, "channels", "qr", "--platform", plat, "--json"],
1892
+ { cwd: OPENCLAW_DIR, timeout: 3e4 },
1893
+ (err, stdout) => {
1894
+ if (err) reject(new Error(`Failed to get QR: ${err.message}`));
1895
+ else {
1896
+ try {
1897
+ const data = JSON.parse(stdout);
1898
+ resolve3(data.qr || data.qrCode || stdout.trim());
1899
+ } catch {
1900
+ resolve3(stdout.trim());
1901
+ }
1902
+ }
1903
+ }
1904
+ );
1905
+ });
1906
+ }
1907
+
1908
+ // src/handlers/sync.ts
1909
+ import { watch } from "fs";
1910
+ var watcher = null;
1911
+ var watchingPath = null;
1912
+ async function startWatching(params) {
1913
+ if (watcher) {
1914
+ watcher.close();
1915
+ watcher = null;
1916
+ watchingPath = null;
1917
+ }
1918
+ watchingPath = params.path;
1919
+ watcher = watch(
1920
+ params.path,
1921
+ { recursive: true },
1922
+ (eventType, filename) => {
1923
+ if (!filename) return;
1924
+ if (filename.includes("node_modules") || filename.includes(".git") || filename.endsWith(".DS_Store")) {
1925
+ return;
1926
+ }
1927
+ const filePath = filename.startsWith("/") ? filename : `${params.path}/${filename}`;
1928
+ emit("file-changed", {
1929
+ paths: [filePath],
1930
+ kind: eventType
1931
+ });
1932
+ emit("sync-status", {
1933
+ status: "syncing",
1934
+ message: `File changed: ${filePath}`,
1935
+ watchingPath
1936
+ });
1937
+ setTimeout(() => {
1938
+ emit("sync-status", {
1939
+ status: "synced",
1940
+ message: null,
1941
+ watchingPath
1942
+ });
1943
+ }, 500);
1944
+ }
1945
+ );
1946
+ watcher.on("error", (error) => {
1947
+ emit("sync-status", {
1948
+ status: "error",
1949
+ message: `Watch error: ${error}`,
1950
+ watchingPath: null
1951
+ });
1952
+ });
1953
+ emit("sync-status", {
1954
+ status: "synced",
1955
+ message: `Watching: ${params.path}`,
1956
+ watchingPath: params.path
1957
+ });
1958
+ }
1959
+ async function stopWatching() {
1960
+ if (watcher) {
1961
+ watcher.close();
1962
+ watcher = null;
1963
+ watchingPath = null;
1964
+ }
1965
+ emit("sync-status", {
1966
+ status: "idle",
1967
+ message: null,
1968
+ watchingPath: null
1969
+ });
1970
+ }
1971
+
1972
+ // src/handlers/wallet.ts
1973
+ import { randomBytes as randomBytes2 } from "crypto";
1974
+ import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
1975
+ import { join as join5 } from "path";
1976
+ import { homedir as homedir4 } from "os";
1977
+ import {
1978
+ privateKeyToAccount
1979
+ } from "viem/accounts";
1980
+ import {
1981
+ createPublicClient,
1982
+ http,
1983
+ formatUnits,
1984
+ toHex,
1985
+ isHex,
1986
+ getAddress
1987
+ } from "viem";
1988
+ import { base } from "viem/chains";
1989
+ var SEREN_DIR = join5(homedir4(), ".seren-local");
1990
+ var WALLET_FILE = join5(SEREN_DIR, "data", "crypto-wallet.json");
1991
+ var USDC_CONTRACT_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1992
+ var BASE_RPC_URL = "https://mainnet.base.org";
1993
+ async function loadStore() {
1994
+ try {
1995
+ const data = await readFile4(WALLET_FILE, "utf-8");
1996
+ return JSON.parse(data);
1997
+ } catch {
1998
+ return null;
1999
+ }
2000
+ }
2001
+ async function saveStore(store) {
2002
+ await mkdir3(join5(SEREN_DIR, "data"), { recursive: true });
2003
+ await writeFile4(WALLET_FILE, JSON.stringify(store), "utf-8");
2004
+ }
2005
+ async function clearStore() {
2006
+ try {
2007
+ await writeFile4(WALLET_FILE, "{}", "utf-8");
2008
+ } catch {
2009
+ }
2010
+ }
2011
+ function loadAccount(store) {
2012
+ let key = store.privateKey;
2013
+ if (!key.startsWith("0x")) {
2014
+ key = `0x${key}`;
2015
+ }
2016
+ return privateKeyToAccount(key);
2017
+ }
2018
+ function chainIdFromNetwork(network) {
2019
+ const eip155Match = network.match(/^eip155:(\d+)$/);
2020
+ if (eip155Match) return BigInt(eip155Match[1]);
2021
+ const map = {
2022
+ base: 8453n,
2023
+ "base-sepolia": 84532n,
2024
+ ethereum: 1n,
2025
+ "ethereum-sepolia": 11155111n,
2026
+ avalanche: 43114n,
2027
+ "avalanche-fuji": 43113n
2028
+ };
2029
+ return map[network] ?? null;
2030
+ }
2031
+ function parseRequirements(json) {
2032
+ const body = JSON.parse(json);
2033
+ if (body.minimumRequired !== void 0 && body.currentBalance !== void 0) {
2034
+ throw new Error("Prepaid payment not supported for wallet signing");
2035
+ }
2036
+ const version = body.x402Version;
2037
+ if (!version) throw new Error("Missing x402Version");
2038
+ if (version === 1) {
2039
+ const accepts = body.accepts ?? [];
2040
+ const first = accepts[0];
2041
+ return {
2042
+ x402Version: 1,
2043
+ resource: first ? {
2044
+ url: first.resource,
2045
+ description: first.description,
2046
+ mimeType: first.mimeType ?? "application/json"
2047
+ } : null,
2048
+ options: accepts.map(
2049
+ (opt) => ({
2050
+ scheme: opt.scheme,
2051
+ network: opt.network,
2052
+ asset: opt.asset,
2053
+ amount: opt.maxAmountRequired ?? opt.amount,
2054
+ payTo: opt.payTo,
2055
+ maxTimeoutSeconds: opt.maxTimeoutSeconds,
2056
+ extra: opt.extra ?? {}
2057
+ })
2058
+ )
2059
+ };
2060
+ }
2061
+ if (version === 2) {
2062
+ return {
2063
+ x402Version: 2,
2064
+ resource: body.resource ?? null,
2065
+ options: (body.accepts ?? []).map(
2066
+ (opt) => ({
2067
+ scheme: opt.scheme,
2068
+ network: opt.network,
2069
+ asset: opt.asset,
2070
+ amount: opt.amount,
2071
+ payTo: opt.payTo,
2072
+ maxTimeoutSeconds: opt.maxTimeoutSeconds,
2073
+ extra: opt.extra ?? {}
2074
+ })
2075
+ )
2076
+ };
2077
+ }
2078
+ throw new Error(`Unsupported x402Version: ${version}`);
2079
+ }
2080
+ var transferWithAuthorizationTypes = {
2081
+ TransferWithAuthorization: [
2082
+ { name: "from", type: "address" },
2083
+ { name: "to", type: "address" },
2084
+ { name: "value", type: "uint256" },
2085
+ { name: "validAfter", type: "uint256" },
2086
+ { name: "validBefore", type: "uint256" },
2087
+ { name: "nonce", type: "bytes32" }
2088
+ ]
2089
+ };
2090
+ function getExtra(option, ...path) {
2091
+ let current = option.extra;
2092
+ for (const key of path) {
2093
+ if (current == null || typeof current !== "object") return void 0;
2094
+ current = current[key];
2095
+ }
2096
+ return typeof current === "string" ? current : void 0;
2097
+ }
2098
+ async function signPayload(account, requirements, option) {
2099
+ const chainId = chainIdFromNetwork(option.network);
2100
+ if (!chainId) throw new Error(`Unsupported network: ${option.network}`);
2101
+ const typedVC = getExtra(
2102
+ option,
2103
+ "eip712TypedData",
2104
+ "domain",
2105
+ "verifyingContract"
2106
+ );
2107
+ if (typedVC && typedVC.toLowerCase() !== option.asset.toLowerCase()) {
2108
+ throw new Error(
2109
+ `Mismatched verifyingContract (${typedVC}) for asset ${option.asset}`
2110
+ );
2111
+ }
2112
+ const verifyingContract = getAddress(
2113
+ typedVC ?? option.asset
2114
+ );
2115
+ const domainName = getExtra(option, "name") ?? getExtra(option, "eip712TypedData", "domain", "name") ?? "USD Coin";
2116
+ const domainVersion = getExtra(option, "version") ?? getExtra(option, "eip712TypedData", "domain", "version") ?? "2";
2117
+ const now = BigInt(Math.floor(Date.now() / 1e3));
2118
+ const validAfterStr = getExtra(
2119
+ option,
2120
+ "eip712TypedData",
2121
+ "message",
2122
+ "validAfter"
2123
+ );
2124
+ const validBeforeStr = getExtra(
2125
+ option,
2126
+ "eip712TypedData",
2127
+ "message",
2128
+ "validBefore"
2129
+ );
2130
+ const validAfter = validAfterStr ? BigInt(validAfterStr) : now - 60n;
2131
+ const validBefore = validBeforeStr ? BigInt(validBeforeStr) : now + BigInt(option.maxTimeoutSeconds);
2132
+ const nonceStr = getExtra(option, "eip712TypedData", "message", "nonce");
2133
+ let nonce;
2134
+ if (nonceStr && isHex(nonceStr) && nonceStr.length === 66) {
2135
+ nonce = nonceStr;
2136
+ } else {
2137
+ nonce = toHex(randomBytes2(32), { size: 32 });
2138
+ }
2139
+ const domain = {
2140
+ name: domainName,
2141
+ version: domainVersion,
2142
+ chainId,
2143
+ verifyingContract
2144
+ };
2145
+ const message = {
2146
+ from: account.address,
2147
+ to: getAddress(option.payTo),
2148
+ value: BigInt(option.amount),
2149
+ validAfter,
2150
+ validBefore,
2151
+ nonce
2152
+ };
2153
+ const signature = await account.signTypedData({
2154
+ domain,
2155
+ types: transferWithAuthorizationTypes,
2156
+ primaryType: "TransferWithAuthorization",
2157
+ message
2158
+ });
2159
+ const authorization = {
2160
+ from: account.address,
2161
+ to: getAddress(option.payTo),
2162
+ value: option.amount,
2163
+ validAfter: validAfter.toString(),
2164
+ validBefore: validBefore.toString(),
2165
+ nonce
2166
+ };
2167
+ const payload = requirements.x402Version === 1 ? {
2168
+ x402Version: 1,
2169
+ scheme: option.scheme,
2170
+ network: option.network,
2171
+ payload: { signature, authorization }
2172
+ } : {
2173
+ x402Version: 2,
2174
+ resource: requirements.resource,
2175
+ accepted: option,
2176
+ payload: { signature, authorization }
2177
+ };
2178
+ const headerValue = Buffer.from(JSON.stringify(payload)).toString("base64");
2179
+ const headerName = requirements.x402Version === 1 ? "X-PAYMENT" : "PAYMENT-SIGNATURE";
2180
+ return {
2181
+ headerName,
2182
+ headerValue,
2183
+ x402Version: requirements.x402Version ?? 2
2184
+ };
2185
+ }
2186
+ async function storeCryptoPrivateKey(params) {
2187
+ const { privateKey } = params;
2188
+ if (!privateKey) throw new Error("Empty private key");
2189
+ let key = privateKey;
2190
+ if (!key.startsWith("0x")) {
2191
+ key = `0x${key}`;
2192
+ }
2193
+ const account = privateKeyToAccount(key);
2194
+ const address = account.address;
2195
+ await saveStore({ privateKey: key, walletAddress: address });
2196
+ return address;
2197
+ }
2198
+ async function getCryptoWalletAddress() {
2199
+ const store = await loadStore();
2200
+ return store?.walletAddress ?? null;
2201
+ }
2202
+ async function clearCryptoWallet() {
2203
+ await clearStore();
2204
+ }
2205
+ async function signX402Payment(params) {
2206
+ const store = await loadStore();
2207
+ if (!store?.privateKey) throw new Error("Wallet not configured");
2208
+ const account = loadAccount(store);
2209
+ const requirements = parseRequirements(params.requirementsJson);
2210
+ const option = requirements.options[0];
2211
+ if (!option) throw new Error("No x402 payment option in requirements");
2212
+ return signPayload(account, requirements, option);
2213
+ }
2214
+ async function getCryptoUsdcBalance() {
2215
+ const store = await loadStore();
2216
+ if (!store?.walletAddress) throw new Error("Wallet not configured");
2217
+ const client = createPublicClient({
2218
+ chain: base,
2219
+ transport: http(BASE_RPC_URL)
2220
+ });
2221
+ const balanceRaw = await client.readContract({
2222
+ address: USDC_CONTRACT_BASE,
2223
+ abi: [
2224
+ {
2225
+ name: "balanceOf",
2226
+ type: "function",
2227
+ stateMutability: "view",
2228
+ inputs: [{ name: "account", type: "address" }],
2229
+ outputs: [{ name: "", type: "uint256" }]
2230
+ }
2231
+ ],
2232
+ functionName: "balanceOf",
2233
+ args: [store.walletAddress]
2234
+ });
2235
+ const formatted = formatUnits(balanceRaw, 6);
2236
+ const balance = Number.parseFloat(formatted).toFixed(2);
2237
+ return {
2238
+ balance,
2239
+ balanceRaw: balanceRaw.toString(),
2240
+ network: "Base"
2241
+ };
2242
+ }
2243
+
2244
+ // src/handlers/index.ts
2245
+ function registerAllHandlers() {
2246
+ registerHandler("list_directory", listDirectory);
2247
+ registerHandler("read_file", readFile2);
2248
+ registerHandler("read_file_base64", readFileBase64);
2249
+ registerHandler("write_file", writeFile2);
2250
+ registerHandler("path_exists", pathExists);
2251
+ registerHandler("is_directory", isDirectory);
2252
+ registerHandler("create_file", createFile);
2253
+ registerHandler("create_directory", createDirectory);
2254
+ registerHandler("delete_path", deletePath);
2255
+ registerHandler("rename_path", renamePath);
2256
+ registerHandler("open_folder_dialog", openFolderDialog);
2257
+ registerHandler("open_file_dialog", openFileDialog);
2258
+ registerHandler("save_file_dialog", saveFileDialog);
2259
+ registerHandler("reveal_in_file_manager", revealInFileManager);
2260
+ registerHandler("acp_spawn", acpSpawn);
2261
+ registerHandler("acp_prompt", acpPrompt);
2262
+ registerHandler("acp_cancel", acpCancel);
2263
+ registerHandler("acp_terminate", acpTerminate);
2264
+ registerHandler("acp_list_sessions", acpListSessions);
2265
+ registerHandler("acp_set_permission_mode", acpSetPermissionMode);
2266
+ registerHandler("acp_respond_to_permission", acpRespondToPermission);
2267
+ registerHandler("acp_respond_to_diff_proposal", acpRespondToDiffProposal);
2268
+ registerHandler("acp_get_available_agents", acpGetAvailableAgents);
2269
+ registerHandler("acp_check_agent_available", acpCheckAgentAvailable);
2270
+ registerHandler("acp_ensure_claude_cli", acpEnsureClaudeCli);
2271
+ registerHandler("openclaw_start", openclawStart);
2272
+ registerHandler("openclaw_stop", openclawStop);
2273
+ registerHandler("openclaw_restart", openclawRestart);
2274
+ registerHandler("openclaw_status", openclawStatus);
2275
+ registerHandler("openclaw_list_channels", openclawListChannels);
2276
+ registerHandler("openclaw_connect_channel", openclawConnectChannel);
2277
+ registerHandler("openclaw_disconnect_channel", openclawDisconnectChannel);
2278
+ registerHandler("openclaw_set_trust", openclawSetTrust);
2279
+ registerHandler("openclaw_send", openclawSend);
2280
+ registerHandler("openclaw_grant_approval", openclawGrantApproval);
2281
+ registerHandler("openclaw_get_qr", openclawGetQr);
2282
+ registerHandler("get_setting", getSetting);
2283
+ registerHandler("set_setting", setSetting);
2284
+ registerHandler("store_crypto_private_key", storeCryptoPrivateKey);
2285
+ registerHandler("get_crypto_wallet_address", getCryptoWalletAddress);
2286
+ registerHandler("clear_crypto_wallet", clearCryptoWallet);
2287
+ registerHandler("sign_x402_payment", signX402Payment);
2288
+ registerHandler("get_crypto_usdc_balance", getCryptoUsdcBalance);
2289
+ registerHandler("start_watching", startWatching);
2290
+ registerHandler("stop_watching", stopWatching);
2291
+ registerHandler("init_project_index", initProjectIndex);
2292
+ registerHandler("get_index_status", getIndexStatus);
2293
+ registerHandler("has_project_index", hasProjectIndex);
2294
+ registerHandler("search_codebase", searchCodebase);
2295
+ registerHandler("file_needs_reindex", fileNeedsReindex2);
2296
+ registerHandler("delete_file_index", deleteFileIndex);
2297
+ registerHandler("index_chunks", indexChunks);
2298
+ registerHandler("discover_project_files", discoverProjectFiles);
2299
+ registerHandler("chunk_file", chunkFile2);
2300
+ registerHandler("estimate_indexing", estimateIndexing2);
2301
+ registerHandler("compute_file_hash", computeFileHash);
2302
+ registerHandler("get_embedding_dimension", getEmbeddingDimension);
2303
+ registerHandler("mcp_disconnect", mcpDisconnect);
2304
+ registerHandler("mcp_read_resource", mcpReadResource);
2305
+ registerHandler("create_conversation", createConversation);
2306
+ registerHandler("get_conversations", getConversations);
2307
+ registerHandler("get_conversation", getConversation);
2308
+ registerHandler("update_conversation", updateConversation);
2309
+ registerHandler("archive_conversation", archiveConversation);
2310
+ registerHandler("delete_conversation", deleteConversation);
2311
+ registerHandler("save_message", saveMessage);
2312
+ registerHandler("get_messages", getMessages);
2313
+ }
2314
+
2315
+ // src/update-check.ts
2316
+ import { readFileSync as readFileSync2 } from "fs";
2317
+ import { join as join6, dirname as dirname2 } from "path";
2318
+ import { fileURLToPath } from "url";
2319
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
2320
+ function getInstalledVersion() {
2321
+ try {
2322
+ const pkg = JSON.parse(
2323
+ readFileSync2(join6(__dirname, "..", "package.json"), "utf-8")
2324
+ );
2325
+ return pkg.version ?? "0.0.0";
2326
+ } catch {
2327
+ return "0.0.0";
2328
+ }
2329
+ }
2330
+ async function getLatestVersion() {
2331
+ try {
2332
+ const controller = new AbortController();
2333
+ const timeout = setTimeout(() => controller.abort(), 5e3);
2334
+ const res = await fetch("https://registry.npmjs.org/@serendb/runtime/latest", {
2335
+ signal: controller.signal
2336
+ });
2337
+ clearTimeout(timeout);
2338
+ if (!res.ok) return null;
2339
+ const data = await res.json();
2340
+ return data.version ?? null;
2341
+ } catch {
2342
+ return null;
2343
+ }
2344
+ }
2345
+ function isNewer(latest, current) {
2346
+ const l = latest.split(".").map(Number);
2347
+ const c = current.split(".").map(Number);
2348
+ for (let i = 0; i < 3; i++) {
2349
+ if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
2350
+ if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
2351
+ }
2352
+ return false;
2353
+ }
2354
+ function checkForUpdates() {
2355
+ const current = getInstalledVersion();
2356
+ getLatestVersion().then((latest) => {
2357
+ if (latest && isNewer(latest, current)) {
2358
+ console.log("");
2359
+ console.log(` \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`);
2360
+ console.log(` \u2551 Update available: v${current} \u2192 v${latest.padEnd(10)} \u2551`);
2361
+ console.log(` \u2551 Run: npm update -g @serendb/runtime \u2551`);
2362
+ console.log(` \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`);
2363
+ console.log("");
2364
+ }
2365
+ }).catch(() => {
2366
+ });
2367
+ }
2368
+
2369
+ // src/server.ts
2370
+ var PORT = Number(process.env.SEREN_PORT) || 19420;
2371
+ var NO_OPEN = process.argv.includes("--no-open");
2372
+ var AUTH_TOKEN = process.env.SEREN_RUNTIME_TOKEN || randomBytes3(32).toString("hex");
2373
+ var __dirname2 = fileURLToPath2(new URL(".", import.meta.url));
2374
+ var PUBLIC_DIR = join7(__dirname2, "..", "public");
2375
+ function computeBuildHash() {
2376
+ try {
2377
+ const indexPath = join7(PUBLIC_DIR, "index.html");
2378
+ const content = readFileSync3(indexPath, "utf-8");
2379
+ return createHash3("sha256").update(content).digest("hex").slice(0, 12);
2380
+ } catch {
2381
+ return "unknown";
2382
+ }
2383
+ }
2384
+ var BUILD_HASH = computeBuildHash();
2385
+ var MIME_TYPES = {
2386
+ ".html": "text/html; charset=utf-8",
2387
+ ".js": "text/javascript; charset=utf-8",
2388
+ ".css": "text/css; charset=utf-8",
2389
+ ".json": "application/json",
2390
+ ".svg": "image/svg+xml",
2391
+ ".png": "image/png",
2392
+ ".jpg": "image/jpeg",
2393
+ ".jpeg": "image/jpeg",
2394
+ ".gif": "image/gif",
2395
+ ".ico": "image/x-icon",
2396
+ ".woff": "font/woff",
2397
+ ".woff2": "font/woff2",
2398
+ ".ttf": "font/ttf",
2399
+ ".wasm": "application/wasm"
2400
+ };
2401
+ function serveHtml(res) {
2402
+ const indexPath = join7(PUBLIC_DIR, "index.html");
2403
+ try {
2404
+ let html = readFileSync3(indexPath, "utf-8");
2405
+ html = html.replace(
2406
+ "<head>",
2407
+ `<head><meta name="seren-build-hash" content="${BUILD_HASH}">`
2408
+ );
2409
+ res.writeHead(200, {
2410
+ "Content-Type": "text/html; charset=utf-8",
2411
+ "Cache-Control": "no-cache, no-store, must-revalidate",
2412
+ "Pragma": "no-cache",
2413
+ "Expires": "0"
2414
+ });
2415
+ res.end(html);
2416
+ return true;
2417
+ } catch {
2418
+ return false;
2419
+ }
2420
+ }
2421
+ function serveStatic(urlPath, res) {
2422
+ const safePath = urlPath.split("?")[0].replace(/\.\./g, "");
2423
+ if (safePath === "/" || safePath === "/index.html") {
2424
+ return serveHtml(res);
2425
+ }
2426
+ const filePath = join7(PUBLIC_DIR, safePath);
2427
+ if (!filePath.startsWith(PUBLIC_DIR)) {
2428
+ return false;
2429
+ }
2430
+ try {
2431
+ const stat2 = statSync2(filePath);
2432
+ if (stat2.isFile()) {
2433
+ const ext = extname2(filePath).toLowerCase();
2434
+ const mime = MIME_TYPES[ext] || "application/octet-stream";
2435
+ const content = readFileSync3(filePath);
2436
+ res.writeHead(200, { "Content-Type": mime, "Cache-Control": "public, max-age=3600" });
2437
+ res.end(content);
2438
+ return true;
2439
+ }
2440
+ } catch {
2441
+ }
2442
+ return false;
2443
+ }
2444
+ function serveSpaFallback(res) {
2445
+ return serveHtml(res);
2446
+ }
2447
+ function openBrowser(url) {
2448
+ const cmd = platform3() === "darwin" ? `open "${url}"` : platform3() === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
2449
+ exec2(cmd, (err) => {
2450
+ if (err) console.log(`[Seren Local] Could not open browser: ${err.message}`);
2451
+ });
2452
+ }
2453
+ function isLocalhost(addr) {
2454
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
2455
+ }
2456
+ var GATEWAY_HOST = "api.serendb.com";
2457
+ var MCP_GATEWAY_HOST = "mcp.serendb.com";
2458
+ function proxyToGateway(req, res, host) {
2459
+ const targetPath = (req.url || "").replace(/^\/(api|mcp)/, "");
2460
+ const chunks = [];
2461
+ req.on("data", (chunk) => chunks.push(chunk));
2462
+ req.on("end", () => {
2463
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
2464
+ const forwardHeaders = {};
2465
+ for (const [key, value] of Object.entries(req.headers)) {
2466
+ if (key === "host" || key === "origin" || key === "referer") continue;
2467
+ if (value) forwardHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
2468
+ }
2469
+ forwardHeaders["host"] = host;
2470
+ const proxyReq = httpsRequest(
2471
+ {
2472
+ hostname: host,
2473
+ port: 443,
2474
+ path: targetPath,
2475
+ method: req.method,
2476
+ headers: forwardHeaders
2477
+ },
2478
+ (proxyRes) => {
2479
+ const responseHeaders = {};
2480
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
2481
+ if (value) responseHeaders[key] = value;
2482
+ }
2483
+ responseHeaders["access-control-allow-origin"] = "*";
2484
+ responseHeaders["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
2485
+ responseHeaders["access-control-allow-headers"] = "Content-Type, Authorization";
2486
+ res.writeHead(proxyRes.statusCode ?? 502, responseHeaders);
2487
+ proxyRes.pipe(res);
2488
+ }
2489
+ );
2490
+ proxyReq.on("error", (err) => {
2491
+ console.error("[Seren Local] Gateway proxy error:", err.message);
2492
+ res.writeHead(502, { "Content-Type": "application/json" });
2493
+ res.end(JSON.stringify({ error: "Gateway proxy error", message: err.message }));
2494
+ });
2495
+ if (body) proxyReq.write(body);
2496
+ proxyReq.end();
2497
+ });
2498
+ }
2499
+ var httpServer = createServer2((req, res) => {
2500
+ if (!isLocalhost(req.socket.remoteAddress)) {
2501
+ res.writeHead(403);
2502
+ res.end("Forbidden: only localhost connections allowed");
2503
+ return;
2504
+ }
2505
+ res.setHeader("Access-Control-Allow-Origin", "*");
2506
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2507
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
2508
+ if (req.method === "OPTIONS") {
2509
+ res.writeHead(204);
2510
+ res.end();
2511
+ return;
2512
+ }
2513
+ if (req.url?.startsWith("/api/") || req.url === "/api") {
2514
+ proxyToGateway(req, res, GATEWAY_HOST);
2515
+ return;
2516
+ }
2517
+ if (req.url?.startsWith("/mcp/") || req.url === "/mcp") {
2518
+ proxyToGateway(req, res, MCP_GATEWAY_HOST);
2519
+ return;
2520
+ }
2521
+ if (req.url === "/health") {
2522
+ res.writeHead(200, { "Content-Type": "application/json" });
2523
+ res.end(JSON.stringify({ status: "ok", version: "0.1.0", token: AUTH_TOKEN, buildHash: BUILD_HASH }));
2524
+ return;
2525
+ }
2526
+ const urlPath = req.url || "/";
2527
+ if (serveStatic(urlPath, res)) return;
2528
+ if (serveSpaFallback(res)) return;
2529
+ res.writeHead(404);
2530
+ res.end();
2531
+ });
2532
+ var wss = new WebSocketServer({ server: httpServer });
2533
+ var authenticatedSockets = /* @__PURE__ */ new WeakSet();
2534
+ wss.on("connection", (ws, req) => {
2535
+ if (!isLocalhost(req.socket.remoteAddress)) {
2536
+ ws.close(4003, "Forbidden");
2537
+ return;
2538
+ }
2539
+ console.log("[Seren Local] Browser connecting (awaiting auth)...");
2540
+ const authTimeout = setTimeout(() => {
2541
+ if (!authenticatedSockets.has(ws)) {
2542
+ console.warn("[Seren Local] Auth timeout, closing connection");
2543
+ ws.close(4001, "Authentication timeout");
2544
+ }
2545
+ }, 5e3);
2546
+ ws.on("message", async (data) => {
2547
+ const raw = typeof data === "string" ? data : data.toString();
2548
+ if (!authenticatedSockets.has(ws)) {
2549
+ clearTimeout(authTimeout);
2550
+ try {
2551
+ const authMsg = JSON.parse(raw);
2552
+ if (authMsg.method === "auth" && authMsg.params?.token === AUTH_TOKEN) {
2553
+ authenticatedSockets.add(ws);
2554
+ addClient(ws);
2555
+ console.log("[Seren Local] Browser authenticated");
2556
+ if (authMsg.id != null) {
2557
+ ws.send(JSON.stringify({ jsonrpc: "2.0", result: { authenticated: true }, id: authMsg.id }));
2558
+ }
2559
+ return;
2560
+ }
2561
+ } catch {
2562
+ }
2563
+ console.warn("[Seren Local] Invalid auth token, closing connection");
2564
+ ws.close(4002, "Invalid auth token");
2565
+ return;
2566
+ }
2567
+ const response = await handleMessage(raw);
2568
+ if (response !== null) {
2569
+ ws.send(response);
2570
+ }
2571
+ });
2572
+ ws.on("close", () => {
2573
+ clearTimeout(authTimeout);
2574
+ console.log("[Seren Local] Browser disconnected");
2575
+ });
2576
+ });
2577
+ var dataDir = join7(homedir5(), ".seren-local");
2578
+ mkdirSync2(dataDir, { recursive: true });
2579
+ initChatDb(join7(dataDir, "conversations.db"));
2580
+ registerAllHandlers();
2581
+ httpServer.listen(PORT, "127.0.0.1", () => {
2582
+ const url = `http://127.0.0.1:${PORT}`;
2583
+ const hasSpa = existsSync3(join7(PUBLIC_DIR, "index.html"));
2584
+ console.log(`[Seren Local] Listening on ${url}`);
2585
+ if (hasSpa) {
2586
+ console.log(`[Seren Local] Serving app at ${url}`);
2587
+ if (!NO_OPEN) openBrowser(url);
2588
+ } else {
2589
+ console.log("[Seren Local] No embedded SPA found (runtime/public/). Run build:embed to bundle the app.");
2590
+ }
2591
+ checkForUpdates();
2592
+ });
2593
+ export {
2594
+ AUTH_TOKEN,
2595
+ PORT,
2596
+ httpServer,
2597
+ wss
2598
+ };