@serendb/serendesktop 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/dist/server.js +143 -42
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -4,9 +4,10 @@ import { exec as exec2 } from "child_process";
4
4
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
5
5
  import { createServer as createServer2 } from "http";
6
6
  import { request as httpsRequest } from "https";
7
- import { homedir as homedir5, platform as platform3 } from "os";
7
+ import { homedir as homedir6, platform as platform4 } from "os";
8
8
  import { join as join7, extname as extname2 } from "path";
9
9
  import { fileURLToPath as fileURLToPath2 } from "url";
10
+ import { createRequire as createRequire2 } from "module";
10
11
  import { WebSocketServer } from "ws";
11
12
 
12
13
  // src/events.ts
@@ -209,6 +210,16 @@ import { readFile, writeFile } from "fs/promises";
209
210
  import { randomUUID } from "crypto";
210
211
  import { resolve } from "path";
211
212
  import { platform } from "os";
213
+ function isAuthError(message) {
214
+ const lower = message.toLowerCase();
215
+ return lower.includes("invalid api key") || lower.includes("authentication required") || lower.includes("auth required") || lower.includes("please run /login") || lower.includes("authrequired");
216
+ }
217
+ function authErrorMessage(agentType) {
218
+ if (agentType === "claude-code") {
219
+ return "Claude Code is not logged in. Please open a terminal and run:\n\n claude login\n\nThen try starting the agent again.";
220
+ }
221
+ return "Agent authentication required. Please log in via the agent CLI first.";
222
+ }
212
223
  var sessions = /* @__PURE__ */ new Map();
213
224
  function createClient(sessionId) {
214
225
  return {
@@ -222,8 +233,8 @@ function createClient(sessionId) {
222
233
  toolCall: params.toolCall,
223
234
  options: params.options
224
235
  });
225
- const optionId = await new Promise((resolve3, reject) => {
226
- session.pendingPermissions.set(requestId, resolve3);
236
+ const optionId = await new Promise((resolve4, reject) => {
237
+ session.pendingPermissions.set(requestId, resolve4);
227
238
  setTimeout(() => {
228
239
  session.pendingPermissions.delete(requestId);
229
240
  reject(new Error("Permission request timed out"));
@@ -256,8 +267,8 @@ function createClient(sessionId) {
256
267
  oldText,
257
268
  newText: params.content
258
269
  });
259
- const accepted = await new Promise((resolve3, reject) => {
260
- session.pendingDiffProposals.set(proposalId, resolve3);
270
+ const accepted = await new Promise((resolve4, reject) => {
271
+ session.pendingDiffProposals.set(proposalId, resolve4);
261
272
  setTimeout(() => {
262
273
  session.pendingDiffProposals.delete(proposalId);
263
274
  reject(new Error("Diff proposal timed out"));
@@ -376,8 +387,8 @@ ${candidates.map((p) => ` - ${p}`).join("\n")}`
376
387
  }
377
388
  async function isCommandAvailable(command) {
378
389
  const which = platform() === "win32" ? "where" : "which";
379
- return new Promise((resolve3) => {
380
- execFile(which, [command], (err) => resolve3(!err));
390
+ return new Promise((resolve4) => {
391
+ execFile(which, [command], (err) => resolve4(!err));
381
392
  });
382
393
  }
383
394
  async function acpSpawn(params) {
@@ -463,11 +474,13 @@ async function acpSpawn(params) {
463
474
  session.acpSessionId = sessionResult.sessionId ?? sessionId;
464
475
  } catch (err) {
465
476
  session.status = "error";
477
+ const rawMessage = err instanceof Error ? err.message : JSON.stringify(err);
478
+ const errorMsg = isAuthError(rawMessage) ? authErrorMessage(agentType) : `Failed to initialize agent: ${rawMessage}`;
466
479
  emit("acp://error", {
467
480
  sessionId,
468
- error: `Failed to initialize agent: ${err instanceof Error ? err.message : JSON.stringify(err)}`
481
+ error: errorMsg
469
482
  });
470
- throw err;
483
+ throw new Error(errorMsg);
471
484
  }
472
485
  return {
473
486
  id: sessionId,
@@ -504,11 +517,13 @@ async function acpPrompt(params) {
504
517
  stopReason: result.stopReason ?? "end_turn"
505
518
  });
506
519
  } catch (err) {
520
+ const rawMessage = err instanceof Error ? err.message : JSON.stringify(err);
521
+ const errorMsg = isAuthError(rawMessage) ? authErrorMessage(session.agentType) : `Prompt failed: ${rawMessage}`;
507
522
  emit("acp://error", {
508
523
  sessionId,
509
- error: `Prompt failed: ${err instanceof Error ? err.message : JSON.stringify(err)}`
524
+ error: errorMsg
510
525
  });
511
- throw err;
526
+ throw new Error(errorMsg);
512
527
  } finally {
513
528
  session.cancelling = false;
514
529
  if (session.status === "prompting") {
@@ -608,7 +623,7 @@ async function acpEnsureClaudeCli() {
608
623
  return "claude";
609
624
  }
610
625
  const npmCmd = platform() === "win32" ? "npm.cmd" : "npm";
611
- return new Promise((resolve3, reject) => {
626
+ return new Promise((resolve4, reject) => {
612
627
  const proc = execFile(
613
628
  npmCmd,
614
629
  ["install", "-g", "@anthropic-ai/claude-code"],
@@ -622,7 +637,7 @@ async function acpEnsureClaudeCli() {
622
637
  return;
623
638
  }
624
639
  console.log(`[ACP] Claude Code CLI installed: ${stdout}`);
625
- resolve3("claude");
640
+ resolve4("claude");
626
641
  }
627
642
  );
628
643
  });
@@ -634,17 +649,17 @@ import { platform as platform2 } from "os";
634
649
  import { dirname } from "path";
635
650
  var os = platform2();
636
651
  function exec(cmd, args) {
637
- return new Promise((resolve3, reject) => {
652
+ return new Promise((resolve4, reject) => {
638
653
  execFile2(cmd, args, { timeout: 6e4 }, (err, stdout) => {
639
654
  if (err) {
640
655
  if (err.code === 1 || err.killed) {
641
- resolve3("");
656
+ resolve4("");
642
657
  return;
643
658
  }
644
659
  reject(err);
645
660
  return;
646
661
  }
647
- resolve3(stdout.trim());
662
+ resolve4(stdout.trim());
648
663
  });
649
664
  });
650
665
  }
@@ -1403,7 +1418,7 @@ import { randomUUID as randomUUID2 } from "crypto";
1403
1418
  var processes = /* @__PURE__ */ new Map();
1404
1419
  function sendRequest(proc, method, params) {
1405
1420
  const id = randomUUID2();
1406
- return new Promise((resolve3, reject) => {
1421
+ return new Promise((resolve4, reject) => {
1407
1422
  const timeout = setTimeout(() => {
1408
1423
  proc.pendingRequests.delete(id);
1409
1424
  reject(new Error(`MCP request timeout: ${method}`));
@@ -1411,7 +1426,7 @@ function sendRequest(proc, method, params) {
1411
1426
  proc.pendingRequests.set(id, {
1412
1427
  resolve: (v) => {
1413
1428
  clearTimeout(timeout);
1414
- resolve3(v);
1429
+ resolve4(v);
1415
1430
  },
1416
1431
  reject: (e) => {
1417
1432
  clearTimeout(timeout);
@@ -1489,13 +1504,13 @@ async function setSetting(params) {
1489
1504
  await saveSettings(settings);
1490
1505
  }
1491
1506
  function findAvailablePort() {
1492
- return new Promise((resolve3, reject) => {
1507
+ return new Promise((resolve4, reject) => {
1493
1508
  const server = createServer();
1494
1509
  server.listen(0, "127.0.0.1", () => {
1495
1510
  const addr = server.address();
1496
1511
  if (addr && typeof addr === "object") {
1497
1512
  const p = addr.port;
1498
- server.close(() => resolve3(p));
1513
+ server.close(() => resolve4(p));
1499
1514
  } else {
1500
1515
  server.close(() => reject(new Error("Failed to get port")));
1501
1516
  }
@@ -1535,10 +1550,10 @@ async function findOpenClawEntrypoint() {
1535
1550
  ];
1536
1551
  try {
1537
1552
  const cmd = process.platform === "win32" ? "where" : "which";
1538
- const path = await new Promise((resolve3, reject) => {
1553
+ const path = await new Promise((resolve4, reject) => {
1539
1554
  execFile3(cmd, ["openclaw"], (err, stdout) => {
1540
1555
  if (err) reject(err);
1541
- else resolve3(stdout.trim());
1556
+ else resolve4(stdout.trim());
1542
1557
  });
1543
1558
  });
1544
1559
  if (path) candidates.unshift(path);
@@ -1667,7 +1682,7 @@ async function openclawStart(_params) {
1667
1682
  emit("openclaw://status-changed", { status: "crashed" });
1668
1683
  }
1669
1684
  });
1670
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
1685
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
1671
1686
  if (childProcess && !childProcess.killed) {
1672
1687
  processStatus = "running";
1673
1688
  startedAt = Date.now();
@@ -1708,7 +1723,7 @@ async function openclawStop(_params) {
1708
1723
  }
1709
1724
  async function openclawRestart(_params) {
1710
1725
  await openclawStop({});
1711
- await new Promise((resolve3) => setTimeout(resolve3, 500));
1726
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
1712
1727
  await openclawStart({});
1713
1728
  }
1714
1729
  async function openclawStatus(_params) {
@@ -1723,7 +1738,7 @@ async function openclawStatus(_params) {
1723
1738
  async function openclawListChannels(_params) {
1724
1739
  if (processStatus !== "running") return [];
1725
1740
  const entrypoint = await findOpenClawEntrypoint();
1726
- return new Promise((resolve3) => {
1741
+ return new Promise((resolve4) => {
1727
1742
  execFile3(
1728
1743
  "node",
1729
1744
  [entrypoint, "channels", "status", "--json"],
@@ -1731,7 +1746,7 @@ async function openclawListChannels(_params) {
1731
1746
  (err, stdout) => {
1732
1747
  if (err) {
1733
1748
  console.error("[OpenClaw] Failed to list channels:", err);
1734
- resolve3([]);
1749
+ resolve4([]);
1735
1750
  return;
1736
1751
  }
1737
1752
  try {
@@ -1750,9 +1765,9 @@ async function openclawListChannels(_params) {
1750
1765
  }
1751
1766
  channels.length = 0;
1752
1767
  channels.push(...result);
1753
- resolve3(result);
1768
+ resolve4(result);
1754
1769
  } catch {
1755
- resolve3([]);
1770
+ resolve4([]);
1756
1771
  }
1757
1772
  }
1758
1773
  );
@@ -1823,14 +1838,14 @@ async function openclawConnectChannel(params) {
1823
1838
  async function openclawDisconnectChannel(params) {
1824
1839
  const { channelId } = params;
1825
1840
  const entrypoint = await findOpenClawEntrypoint();
1826
- return new Promise((resolve3, reject) => {
1841
+ return new Promise((resolve4, reject) => {
1827
1842
  execFile3(
1828
1843
  "node",
1829
1844
  [entrypoint, "channels", "remove", "--channel", channelId, "--delete"],
1830
1845
  { cwd: OPENCLAW_DIR, timeout: 1e4 },
1831
1846
  (err) => {
1832
1847
  if (err) reject(new Error(`Failed to disconnect: ${err.message}`));
1833
- else resolve3();
1848
+ else resolve4();
1834
1849
  }
1835
1850
  );
1836
1851
  });
@@ -1885,7 +1900,7 @@ async function openclawGrantApproval(params) {
1885
1900
  async function openclawGetQr(params) {
1886
1901
  const { platform: plat } = params;
1887
1902
  const entrypoint = await findOpenClawEntrypoint();
1888
- return new Promise((resolve3, reject) => {
1903
+ return new Promise((resolve4, reject) => {
1889
1904
  execFile3(
1890
1905
  "node",
1891
1906
  [entrypoint, "channels", "qr", "--platform", plat, "--json"],
@@ -1895,9 +1910,9 @@ async function openclawGetQr(params) {
1895
1910
  else {
1896
1911
  try {
1897
1912
  const data = JSON.parse(stdout);
1898
- resolve3(data.qr || data.qrCode || stdout.trim());
1913
+ resolve4(data.qr || data.qrCode || stdout.trim());
1899
1914
  } catch {
1900
- resolve3(stdout.trim());
1915
+ resolve4(stdout.trim());
1901
1916
  }
1902
1917
  }
1903
1918
  }
@@ -1969,11 +1984,93 @@ async function stopWatching() {
1969
1984
  });
1970
1985
  }
1971
1986
 
1987
+ // src/handlers/updater.ts
1988
+ import { spawn as spawn3 } from "child_process";
1989
+ import { platform as platform3, homedir as homedir4 } from "os";
1990
+ import { resolve as resolve3 } from "path";
1991
+ import { createRequire } from "module";
1992
+ var require2 = createRequire(import.meta.url);
1993
+ var pkg = require2("../../package.json");
1994
+ var CURRENT_VERSION = pkg.version;
1995
+ var PACKAGE_NAME = "@serendb/serendesktop";
1996
+ var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
1997
+ async function checkForUpdate() {
1998
+ try {
1999
+ const controller = new AbortController();
2000
+ const timeout = setTimeout(() => controller.abort(), 1e4);
2001
+ const res = await fetch(REGISTRY_URL, {
2002
+ signal: controller.signal,
2003
+ headers: { Accept: "application/json" }
2004
+ });
2005
+ clearTimeout(timeout);
2006
+ if (!res.ok) {
2007
+ console.warn(`[Updater] Registry returned ${res.status}`);
2008
+ return { currentVersion: CURRENT_VERSION, latestVersion: null, updateAvailable: false };
2009
+ }
2010
+ const data = await res.json();
2011
+ const latestVersion = data.version ?? null;
2012
+ const updateAvailable = latestVersion !== null && latestVersion !== CURRENT_VERSION && isNewer(latestVersion, CURRENT_VERSION);
2013
+ console.log(`[Updater] Current: ${CURRENT_VERSION}, Latest: ${latestVersion}, Update: ${updateAvailable}`);
2014
+ return { currentVersion: CURRENT_VERSION, latestVersion, updateAvailable };
2015
+ } catch (err) {
2016
+ console.warn("[Updater] Failed to check for updates:", err);
2017
+ return { currentVersion: CURRENT_VERSION, latestVersion: null, updateAvailable: false };
2018
+ }
2019
+ }
2020
+ async function installUpdate() {
2021
+ const home2 = homedir4();
2022
+ const serenDir = resolve3(home2, ".seren-local");
2023
+ const nodeDir = resolve3(serenDir, "node");
2024
+ const binDir = resolve3(serenDir, "bin");
2025
+ const isWin = platform3() === "win32";
2026
+ const nodeBinDir = isWin ? nodeDir : resolve3(nodeDir, "bin");
2027
+ const npmCmd = isWin ? resolve3(nodeBinDir, "npm.cmd") : resolve3(nodeBinDir, "npm");
2028
+ const serendesktopCmd = isWin ? "serendesktop.cmd" : "serendesktop";
2029
+ if (isWin) {
2030
+ const script = [
2031
+ `timeout /t 2 /nobreak >nul`,
2032
+ `set "PATH=${nodeBinDir};${binDir};%PATH%"`,
2033
+ `"${npmCmd}" install -g ${PACKAGE_NAME} --prefix "${serenDir}"`,
2034
+ `"${serendesktopCmd}"`
2035
+ ].join(" && ");
2036
+ const child = spawn3("cmd", ["/c", script], {
2037
+ detached: true,
2038
+ stdio: "ignore",
2039
+ windowsHide: true
2040
+ });
2041
+ child.unref();
2042
+ } else {
2043
+ const script = [
2044
+ `sleep 2`,
2045
+ `export PATH="${nodeBinDir}:${binDir}:$PATH"`,
2046
+ `"${npmCmd}" install -g ${PACKAGE_NAME} --prefix "${serenDir}"`,
2047
+ `"${serendesktopCmd}"`
2048
+ ].join(" && ");
2049
+ const child = spawn3("bash", ["-c", script], {
2050
+ detached: true,
2051
+ stdio: "ignore"
2052
+ });
2053
+ child.unref();
2054
+ }
2055
+ console.log("[Updater] Detached updater spawned, exiting server...");
2056
+ setTimeout(() => process.exit(0), 500);
2057
+ return { started: true };
2058
+ }
2059
+ function isNewer(a, b) {
2060
+ const pa = a.split(".").map(Number);
2061
+ const pb = b.split(".").map(Number);
2062
+ for (let i = 0; i < 3; i++) {
2063
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
2064
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
2065
+ }
2066
+ return false;
2067
+ }
2068
+
1972
2069
  // src/handlers/wallet.ts
1973
2070
  import { randomBytes as randomBytes2 } from "crypto";
1974
2071
  import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
1975
2072
  import { join as join5 } from "path";
1976
- import { homedir as homedir4 } from "os";
2073
+ import { homedir as homedir5 } from "os";
1977
2074
  import {
1978
2075
  privateKeyToAccount
1979
2076
  } from "viem/accounts";
@@ -1986,7 +2083,7 @@ import {
1986
2083
  getAddress
1987
2084
  } from "viem";
1988
2085
  import { base } from "viem/chains";
1989
- var SEREN_DIR = join5(homedir4(), ".seren-local");
2086
+ var SEREN_DIR = join5(homedir5(), ".seren-local");
1990
2087
  var WALLET_FILE = join5(SEREN_DIR, "data", "crypto-wallet.json");
1991
2088
  var USDC_CONTRACT_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1992
2089
  var BASE_RPC_URL = "https://mainnet.base.org";
@@ -2302,6 +2399,8 @@ function registerAllHandlers() {
2302
2399
  registerHandler("get_embedding_dimension", getEmbeddingDimension);
2303
2400
  registerHandler("mcp_disconnect", mcpDisconnect);
2304
2401
  registerHandler("mcp_read_resource", mcpReadResource);
2402
+ registerHandler("check_for_update", checkForUpdate);
2403
+ registerHandler("install_update", installUpdate);
2305
2404
  registerHandler("create_conversation", createConversation);
2306
2405
  registerHandler("get_conversations", getConversations);
2307
2406
  registerHandler("get_conversation", getConversation);
@@ -2319,10 +2418,10 @@ import { fileURLToPath } from "url";
2319
2418
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2320
2419
  function getInstalledVersion() {
2321
2420
  try {
2322
- const pkg = JSON.parse(
2421
+ const pkg2 = JSON.parse(
2323
2422
  readFileSync2(join6(__dirname, "..", "package.json"), "utf-8")
2324
2423
  );
2325
- return pkg.version ?? "0.0.0";
2424
+ return pkg2.version ?? "0.0.0";
2326
2425
  } catch {
2327
2426
  return "0.0.0";
2328
2427
  }
@@ -2342,7 +2441,7 @@ async function getLatestVersion() {
2342
2441
  return null;
2343
2442
  }
2344
2443
  }
2345
- function isNewer(latest, current) {
2444
+ function isNewer2(latest, current) {
2346
2445
  const l = latest.split(".").map(Number);
2347
2446
  const c = current.split(".").map(Number);
2348
2447
  for (let i = 0; i < 3; i++) {
@@ -2354,7 +2453,7 @@ function isNewer(latest, current) {
2354
2453
  function checkForUpdates() {
2355
2454
  const current = getInstalledVersion();
2356
2455
  getLatestVersion().then((latest) => {
2357
- if (latest && isNewer(latest, current)) {
2456
+ if (latest && isNewer2(latest, current)) {
2358
2457
  console.log("");
2359
2458
  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
2459
  console.log(` \u2551 Update available: v${current} \u2192 v${latest.padEnd(10)} \u2551`);
@@ -2367,6 +2466,8 @@ function checkForUpdates() {
2367
2466
  }
2368
2467
 
2369
2468
  // src/server.ts
2469
+ var require3 = createRequire2(import.meta.url);
2470
+ var APP_VERSION = require3("../package.json").version;
2370
2471
  var PORT = Number(process.env.SEREN_PORT) || 19420;
2371
2472
  var NO_OPEN = process.argv.includes("--no-open");
2372
2473
  var AUTH_TOKEN = process.env.SEREN_RUNTIME_TOKEN || randomBytes3(32).toString("hex");
@@ -2445,7 +2546,7 @@ function serveSpaFallback(res) {
2445
2546
  return serveHtml(res);
2446
2547
  }
2447
2548
  function openBrowser(url) {
2448
- const cmd = platform3() === "darwin" ? `open "${url}"` : platform3() === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
2549
+ const cmd = platform4() === "darwin" ? `open "${url}"` : platform4() === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
2449
2550
  exec2(cmd, (err) => {
2450
2551
  if (err) console.log(`[Seren Local] Could not open browser: ${err.message}`);
2451
2552
  });
@@ -2520,7 +2621,7 @@ var httpServer = createServer2((req, res) => {
2520
2621
  }
2521
2622
  if (req.url === "/health") {
2522
2623
  res.writeHead(200, { "Content-Type": "application/json" });
2523
- res.end(JSON.stringify({ status: "ok", version: "0.1.0", token: AUTH_TOKEN, buildHash: BUILD_HASH }));
2624
+ res.end(JSON.stringify({ status: "ok", version: APP_VERSION, token: AUTH_TOKEN, buildHash: BUILD_HASH }));
2524
2625
  return;
2525
2626
  }
2526
2627
  const urlPath = req.url || "/";
@@ -2574,7 +2675,7 @@ wss.on("connection", (ws, req) => {
2574
2675
  console.log("[Seren Local] Browser disconnected");
2575
2676
  });
2576
2677
  });
2577
- var dataDir = join7(homedir5(), ".seren-local");
2678
+ var dataDir = join7(homedir6(), ".seren-local");
2578
2679
  mkdirSync2(dataDir, { recursive: true });
2579
2680
  initChatDb(join7(dataDir, "conversations.db"));
2580
2681
  registerAllHandlers();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serendb/serendesktop",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Seren Local runtime — enables ACP agents, local MCP, and file access",
5
5
  "type": "module",
6
6
  "license": "MIT",