opencami 1.7.0 → 1.8.3

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 (83) hide show
  1. package/README.md +167 -7
  2. package/bin/opencami.js +15 -6
  3. package/dist/client/assets/{CSPContext-a-MQmQQt.js → CSPContext-DeJH85nm.js} +1 -1
  4. package/dist/client/assets/{DirectionContext-DRcND-Cm.js → DirectionContext-CxhRpXkm.js} +1 -1
  5. package/dist/client/assets/_sessionKey-CQE0brGK.js +23 -0
  6. package/dist/client/assets/agents-CMTFd_sG.js +2 -0
  7. package/dist/client/assets/agents-screen-BNQGEqcW.js +1 -0
  8. package/dist/client/assets/bots-B6oGzCxP.js +2 -0
  9. package/dist/client/assets/bots-screen-Be3cfGgq.js +1 -0
  10. package/dist/client/assets/button-D9Plv7hu.js +1 -0
  11. package/dist/client/assets/composite-B2KCZKKL.js +1 -0
  12. package/dist/client/assets/{connect-BX1MWO82.js → connect-DuJfnyNK.js} +1 -1
  13. package/dist/client/assets/dashboard-00GpXm5V.js +1 -0
  14. package/dist/client/assets/event-DD8Cz4O9.js +1 -0
  15. package/dist/client/assets/file-explorer-screen-CxwemBES.js +1 -0
  16. package/dist/client/assets/files-DyBJVXBu.js +2 -0
  17. package/dist/client/assets/{index-BlC2sH55.js → index-DtGzE-ea.js} +1 -1
  18. package/dist/client/assets/{index-Dg0mbvtv.js → index-Yo5UhdZV.js} +1 -1
  19. package/dist/client/assets/keyboard-shortcuts-dialog-BZwd-iyV.js +1 -0
  20. package/dist/client/assets/{main-CEuT8-Qi.js → main-CgwdHc9W.js} +16 -8
  21. package/dist/client/assets/{markdown-DKD6ZLRJ.js → markdown-DtWnt4NA.js} +1 -1
  22. package/dist/client/assets/memory-l756yiNq.js +2 -0
  23. package/dist/client/assets/memory-screen-BQtVRuzE.js +1 -0
  24. package/dist/client/assets/menu-BsS6CDf_.js +1 -0
  25. package/dist/client/assets/{opencami-logo-DA69yVKc.js → opencami-logo-Bmge6-FB.js} +1 -1
  26. package/dist/client/assets/popupStateMapping-D0ZbJR_o.js +1 -0
  27. package/dist/client/assets/{proxy-Cawf6X0W.js → proxy-CYZeDXoy.js} +1 -1
  28. package/dist/client/assets/{react-K9goXsVv.js → react-DODKNyyU.js} +1 -1
  29. package/dist/client/assets/search-dialog-DW91SK30.js +1 -0
  30. package/dist/client/assets/session-export-dialog-CliO9Ob-.js +1 -0
  31. package/dist/client/assets/settings-dialog-C1u52aju.js +1 -0
  32. package/dist/client/assets/skills-8T_avaVb.js +2 -0
  33. package/dist/client/assets/{skills-panel-DFL-3BRH.js → skills-panel-DSiH-DLs.js} +1 -1
  34. package/dist/client/assets/styles-DvaLh0o1.css +1 -0
  35. package/dist/client/assets/switch-DbgQPO6i.js +1 -0
  36. package/dist/client/assets/tabs-BsAvZnlD.js +1 -0
  37. package/dist/client/assets/tooltip-DLmutB5C.js +1 -0
  38. package/dist/client/assets/use-file-explorer-state-Cg_yDYJl.js +12 -0
  39. package/dist/client/assets/useBaseUiId-KQTzRPLp.js +1 -0
  40. package/dist/client/assets/useCompositeItem-BPY2_hF_.js +1 -0
  41. package/dist/client/assets/{useControlled-Cl9XA2_f.js → useControlled-B5pEEz2V.js} +1 -1
  42. package/dist/client/assets/{useMutation-C5bTdeC1.js → useMutation-BsQD6FKe.js} +1 -1
  43. package/dist/client/assets/useQuery-CmAJuY2W.js +1 -0
  44. package/dist/client/assets/visuallyHidden-COI6QeQH.js +1 -0
  45. package/dist/client/sw.js +5 -164
  46. package/dist/server/assets/{_sessionKey-BBG3ZUlo.js → _sessionKey-C9o7YfxA.js} +878 -755
  47. package/dist/server/assets/_tanstack-start-manifest_v-BMCAWon2.js +4 -0
  48. package/dist/server/assets/dashboard-GCKodTiJ.js +214 -0
  49. package/dist/server/assets/{index-BgMPaOsU.js → index-Bw-bA_2M.js} +4 -3
  50. package/dist/server/assets/{router-Bl2uabfY.js → router-DCjikH21.js} +704 -207
  51. package/dist/server/assets/{search-dialog-BtSQW9SR.js → search-dialog-BnwiXpdA.js} +5 -4
  52. package/dist/server/assets/settings-dialog-ClKFnZ1x.js +1511 -0
  53. package/dist/server/server.js +2 -2
  54. package/package.json +1 -1
  55. package/dist/client/assets/_sessionKey-BAmpzUOP.js +0 -23
  56. package/dist/client/assets/agents-BkeWu_3a.js +0 -2
  57. package/dist/client/assets/agents-screen-Cb76bcxn.js +0 -1
  58. package/dist/client/assets/bots-CyJwr-JU.js +0 -2
  59. package/dist/client/assets/bots-screen-CzNjLsQH.js +0 -1
  60. package/dist/client/assets/button-DNC5N25i.js +0 -1
  61. package/dist/client/assets/composite-Bliqcmg4.js +0 -1
  62. package/dist/client/assets/file-explorer-screen-CpY1O_ag.js +0 -1
  63. package/dist/client/assets/files-HiN5rXWq.js +0 -2
  64. package/dist/client/assets/keyboard-shortcuts-dialog-C2Hq19LN.js +0 -1
  65. package/dist/client/assets/memory-lhzf-8Q4.js +0 -2
  66. package/dist/client/assets/memory-screen-Zq9qfnJK.js +0 -1
  67. package/dist/client/assets/menu-47ooFeSm.js +0 -1
  68. package/dist/client/assets/owner-CFRNz_Tp.js +0 -1
  69. package/dist/client/assets/popupStateMapping-D5k-jOeY.js +0 -1
  70. package/dist/client/assets/search-dialog-C5Yae9rb.js +0 -1
  71. package/dist/client/assets/session-export-dialog-CBeTfbll.js +0 -1
  72. package/dist/client/assets/settings-dialog-CoeG9M1b.js +0 -1
  73. package/dist/client/assets/skills-BEkw619A.js +0 -2
  74. package/dist/client/assets/styles-D4EBtWYc.css +0 -1
  75. package/dist/client/assets/switch-DAFvLxNX.js +0 -1
  76. package/dist/client/assets/tabs-B2Y_7MvG.js +0 -1
  77. package/dist/client/assets/tooltip-D57Pal0B.js +0 -1
  78. package/dist/client/assets/use-file-explorer-state-DppKEjcl.js +0 -12
  79. package/dist/client/assets/useButton-DVAfkehQ.js +0 -1
  80. package/dist/client/assets/useCompositeItem-CzdGhGcj.js +0 -1
  81. package/dist/client/assets/visuallyHidden-CO3ZD5AQ.js +0 -1
  82. package/dist/server/assets/_tanstack-start-manifest_v-DmMFarHb.js +0 -4
  83. package/dist/server/assets/settings-dialog-D3fOAswX.js +0 -1173
@@ -1,21 +1,29 @@
1
1
  import { createRootRoute, Outlet, HeadContent, Scripts, createFileRoute, lazyRouteComponent, redirect, createRouter } from "@tanstack/react-router";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
4
- import { randomUUID } from "node:crypto";
5
- import WebSocket from "ws";
6
- import { readFileSync, existsSync } from "node:fs";
7
- import path, { join, resolve, relative, extname } from "node:path";
4
+ import crypto, { randomUUID } from "node:crypto";
5
+ import fs, { readFileSync, existsSync } from "node:fs";
8
6
  import os, { homedir } from "node:os";
7
+ import path, { join, resolve, relative, extname } from "node:path";
8
+ import WebSocket from "ws";
9
9
  import { json } from "@tanstack/router-core/ssr/client";
10
+ import { PassThrough, Readable } from "node:stream";
10
11
  import { execSync } from "node:child_process";
11
12
  import { readFile, mkdir, writeFile, rename, stat, readdir, rm, realpath, lstat } from "node:fs/promises";
12
13
  import { posix } from "path";
13
- const appCss = "/assets/styles-D4EBtWYc.css";
14
+ const appCss = "/assets/styles-DvaLh0o1.css";
14
15
  const swRegisterScript = `
15
16
  (() => {
16
17
  // Skip PWA service worker inside Capacitor native shell — they conflict
17
18
  // with the native networking layer and caching.
18
19
  if (window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()) return;
20
+ // Unregister any existing service workers — old SW was breaking SSE streaming
21
+ if ('serviceWorker' in navigator) {
22
+ navigator.serviceWorker.getRegistrations().then((regs) => {
23
+ regs.forEach((reg) => reg.unregister());
24
+ });
25
+ }
26
+ if (false) { // SW disabled — causes streaming issues
19
27
  if ('serviceWorker' in navigator) {
20
28
  window.addEventListener('load', () => {
21
29
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
@@ -37,6 +45,7 @@ const swRegisterScript = `
37
45
  .catch((err) => console.warn('[SW] Registration failed:', err));
38
46
  });
39
47
  }
48
+ }
40
49
  })()
41
50
  `;
42
51
  const themeScript = `
@@ -211,7 +220,7 @@ function NotFoundRedirect() {
211
220
  }
212
221
  return null;
213
222
  }
214
- const Route$x = createRootRoute({
223
+ const Route$B = createRootRoute({
215
224
  notFoundComponent: NotFoundRedirect,
216
225
  head: () => ({
217
226
  meta: [
@@ -319,12 +328,12 @@ function RootDocument({ children }) {
319
328
  ] })
320
329
  ] });
321
330
  }
322
- const $$splitComponentImporter$8 = () => import("./skills-Cy8xclXY.js");
323
- const Route$w = createFileRoute("/skills")({
324
- component: lazyRouteComponent($$splitComponentImporter$8, "component")
331
+ const $$splitComponentImporter$9 = () => import("./skills-Cy8xclXY.js");
332
+ const Route$A = createFileRoute("/skills")({
333
+ component: lazyRouteComponent($$splitComponentImporter$9, "component")
325
334
  });
326
- const $$splitComponentImporter$7 = () => import("./new-Dzk5YxE9.js");
327
- const Route$v = createFileRoute("/new")({
335
+ const $$splitComponentImporter$8 = () => import("./new-Dzk5YxE9.js");
336
+ const Route$z = createFileRoute("/new")({
328
337
  beforeLoad: function redirectToNewChat() {
329
338
  throw redirect({
330
339
  to: "/chat/$sessionKey",
@@ -334,34 +343,38 @@ const Route$v = createFileRoute("/new")({
334
343
  replace: true
335
344
  });
336
345
  },
346
+ component: lazyRouteComponent($$splitComponentImporter$8, "component")
347
+ });
348
+ const $$splitComponentImporter$7 = () => import("./memory-BqZOoD7Q.js");
349
+ const Route$y = createFileRoute("/memory")({
337
350
  component: lazyRouteComponent($$splitComponentImporter$7, "component")
338
351
  });
339
- const $$splitComponentImporter$6 = () => import("./memory-BqZOoD7Q.js");
340
- const Route$u = createFileRoute("/memory")({
352
+ const $$splitComponentImporter$6 = () => import("./files-DYdXlQDr.js");
353
+ const Route$x = createFileRoute("/files")({
341
354
  component: lazyRouteComponent($$splitComponentImporter$6, "component")
342
355
  });
343
- const $$splitComponentImporter$5 = () => import("./files-DYdXlQDr.js");
344
- const Route$t = createFileRoute("/files")({
356
+ const $$splitComponentImporter$5 = () => import("./dashboard-GCKodTiJ.js");
357
+ const Route$w = createFileRoute("/dashboard")({
345
358
  component: lazyRouteComponent($$splitComponentImporter$5, "component")
346
359
  });
347
360
  const $$splitComponentImporter$4 = () => import("./connect-CbgijWz4.js");
348
- const Route$s = createFileRoute("/connect")({
361
+ const Route$v = createFileRoute("/connect")({
349
362
  component: lazyRouteComponent($$splitComponentImporter$4, "component")
350
363
  });
351
364
  const $$splitComponentImporter$3 = () => import("./bots-Byt6jv0a.js");
352
- const Route$r = createFileRoute("/bots")({
365
+ const Route$u = createFileRoute("/bots")({
353
366
  component: lazyRouteComponent($$splitComponentImporter$3, "component")
354
367
  });
355
368
  const $$splitComponentImporter$2 = () => import("./agents-CmQ4vvXm.js");
356
- const Route$q = createFileRoute("/agents")({
369
+ const Route$t = createFileRoute("/agents")({
357
370
  component: lazyRouteComponent($$splitComponentImporter$2, "component")
358
371
  });
359
- const $$splitComponentImporter$1 = () => import("./index-BgMPaOsU.js");
360
- const Route$p = createFileRoute("/")({
372
+ const $$splitComponentImporter$1 = () => import("./index-Bw-bA_2M.js");
373
+ const Route$s = createFileRoute("/")({
361
374
  component: lazyRouteComponent($$splitComponentImporter$1, "component")
362
375
  });
363
- const $$splitComponentImporter = () => import("./_sessionKey-BBG3ZUlo.js").then((n) => n.$);
364
- const Route$o = createFileRoute("/chat/$sessionKey")({
376
+ const $$splitComponentImporter = () => import("./_sessionKey-C9o7YfxA.js").then((n) => n.$);
377
+ const Route$r = createFileRoute("/chat/$sessionKey")({
365
378
  component: lazyRouteComponent($$splitComponentImporter, "component")
366
379
  });
367
380
  function getGatewayConfig() {
@@ -375,24 +388,150 @@ function getGatewayConfig() {
375
388
  }
376
389
  return { url, token, password };
377
390
  }
378
- function buildConnectParams(token, password) {
391
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
392
+ function base64UrlEncode(buf) {
393
+ return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
394
+ }
395
+ function derivePublicKeyRaw(publicKeyPem) {
396
+ const spki = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
397
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
398
+ return spki.subarray(ED25519_SPKI_PREFIX.length);
399
+ }
400
+ return spki;
401
+ }
402
+ function fingerprintPublicKey(publicKeyPem) {
403
+ const raw = derivePublicKeyRaw(publicKeyPem);
404
+ return crypto.createHash("sha256").update(raw).digest("hex");
405
+ }
406
+ function ensureDir(filePath) {
407
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
408
+ }
409
+ function resolveDeviceIdentityPath() {
410
+ return path.join(os.homedir(), ".opencami", "identity", "device.json");
411
+ }
412
+ function loadOrCreateDeviceIdentity(filePath = resolveDeviceIdentityPath()) {
413
+ try {
414
+ if (fs.existsSync(filePath)) {
415
+ const raw = fs.readFileSync(filePath, "utf8");
416
+ const parsed = JSON.parse(raw);
417
+ if (parsed?.version === 1 && typeof parsed.deviceId === "string" && typeof parsed.publicKeyPem === "string" && typeof parsed.privateKeyPem === "string") {
418
+ const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
419
+ if (derivedId && derivedId !== parsed.deviceId) {
420
+ const updated = { ...parsed, deviceId: derivedId };
421
+ fs.writeFileSync(filePath, `${JSON.stringify(updated, null, 2)}
422
+ `, { mode: 384 });
423
+ try {
424
+ fs.chmodSync(filePath, 384);
425
+ } catch {
426
+ }
427
+ return { deviceId: derivedId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
428
+ }
429
+ return { deviceId: parsed.deviceId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
430
+ }
431
+ }
432
+ } catch {
433
+ }
434
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
435
+ const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
436
+ const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
437
+ const deviceId = fingerprintPublicKey(publicKeyPem);
438
+ ensureDir(filePath);
439
+ const stored = {
440
+ version: 1,
441
+ deviceId,
442
+ publicKeyPem,
443
+ privateKeyPem,
444
+ createdAtMs: Date.now()
445
+ };
446
+ fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}
447
+ `, { mode: 384 });
448
+ try {
449
+ fs.chmodSync(filePath, 384);
450
+ } catch {
451
+ }
452
+ return { deviceId, publicKeyPem, privateKeyPem };
453
+ }
454
+ function signDevicePayload(privateKeyPem, payload) {
455
+ const key = crypto.createPrivateKey(privateKeyPem);
456
+ return base64UrlEncode(crypto.sign(null, Buffer.from(payload, "utf8"), key));
457
+ }
458
+ function publicKeyRawBase64UrlFromPem(publicKeyPem) {
459
+ return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
460
+ }
461
+ function buildDeviceAuthPayload(args) {
462
+ const scopes = args.scopes.join(",");
463
+ const token = args.token ?? "";
464
+ return ["v2", args.deviceId, args.clientId, args.clientMode, args.role, scopes, String(args.signedAtMs), token, args.nonce].join("|");
465
+ }
466
+ function loadOrCreateInstanceId() {
467
+ const filePath = path.join(os.homedir(), ".opencami", "identity", "instance-id.txt");
468
+ try {
469
+ if (fs.existsSync(filePath)) {
470
+ const v2 = fs.readFileSync(filePath, "utf8").trim();
471
+ if (v2) return v2;
472
+ }
473
+ } catch {
474
+ }
475
+ const v = randomUUID();
476
+ ensureDir(filePath);
477
+ fs.writeFileSync(filePath, `${v}
478
+ `, { mode: 384 });
479
+ try {
480
+ fs.chmodSync(filePath, 384);
481
+ } catch {
482
+ }
483
+ return v;
484
+ }
485
+ function buildConnectParams(token, password, nonce) {
486
+ const clientId = "openclaw-control-ui";
487
+ const clientMode = "webchat";
488
+ const role = "operator";
489
+ const scopes = ["operator.read", "operator.write"];
490
+ if (!nonce) {
491
+ throw new Error(
492
+ "OpenClaw did not send connect.challenge nonce in time. If you are connecting cross-origin, ensure your origin is allowed (gateway.controlUi.allowedOrigins)."
493
+ );
494
+ }
495
+ const identity = loadOrCreateDeviceIdentity();
496
+ const signedAtMs = Date.now();
497
+ const payload = buildDeviceAuthPayload({
498
+ deviceId: identity.deviceId,
499
+ clientId,
500
+ clientMode,
501
+ role,
502
+ scopes,
503
+ signedAtMs,
504
+ token: token || null,
505
+ nonce
506
+ });
507
+ const signature = signDevicePayload(identity.privateKeyPem, payload);
379
508
  return {
380
509
  minProtocol: 3,
381
510
  maxProtocol: 3,
382
511
  client: {
383
- id: "gateway-client",
512
+ id: clientId,
384
513
  displayName: "OpenCami",
385
514
  version: "dev",
386
515
  platform: process.platform,
387
- mode: "ui",
388
- instanceId: randomUUID()
516
+ mode: clientMode,
517
+ instanceId: loadOrCreateInstanceId()
389
518
  },
519
+ caps: [],
390
520
  auth: {
391
521
  token: token || void 0,
392
522
  password: password || void 0
393
523
  },
394
524
  role: "operator",
395
- scopes: ["operator.read", "operator.write", "operator.admin"]
525
+ scopes,
526
+ device: {
527
+ id: identity.deviceId,
528
+ publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
529
+ signature,
530
+ signedAt: signedAtMs,
531
+ nonce
532
+ },
533
+ userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
534
+ locale: process.env.LANG || "en"
396
535
  };
397
536
  }
398
537
  class PersistentGatewayConnection {
@@ -427,7 +566,8 @@ class PersistentGatewayConnection {
427
566
  async _connect() {
428
567
  if (this.destroyed) throw new Error("Connection destroyed");
429
568
  const { url, token, password } = getGatewayConfig();
430
- const ws = new WebSocket(url);
569
+ const origin = process.env.OPENCAMI_ORIGIN?.trim();
570
+ const ws = origin ? new WebSocket(url, { headers: { Origin: origin } }) : new WebSocket(url);
431
571
  this.ws = ws;
432
572
  await new Promise((resolve2, reject) => {
433
573
  const onOpen = () => {
@@ -445,19 +585,93 @@ class PersistentGatewayConnection {
445
585
  ws.on("open", onOpen);
446
586
  ws.on("error", onError);
447
587
  });
588
+ const nonce = await new Promise((resolve2) => {
589
+ let done = false;
590
+ const timer = setTimeout(() => {
591
+ if (done) return;
592
+ done = true;
593
+ resolve2("");
594
+ }, 3e3);
595
+ const onMessage = (data) => {
596
+ try {
597
+ const str = typeof data === "string" ? data : data.toString();
598
+ const parsed = JSON.parse(str);
599
+ if (parsed.type === "event" && parsed.event === "connect.challenge") {
600
+ const n = parsed.payload?.nonce;
601
+ if (typeof n === "string" && n.length > 0) {
602
+ clearTimeout(timer);
603
+ ws.off("message", onMessage);
604
+ if (done) return;
605
+ done = true;
606
+ resolve2(n);
607
+ }
608
+ }
609
+ } catch {
610
+ }
611
+ };
612
+ ws.on("message", onMessage);
613
+ });
448
614
  ws.on("message", (data) => this._onMessage(data));
449
615
  ws.on("close", () => this._onClose());
450
616
  ws.on("error", () => {
451
617
  });
452
618
  const connectId = randomUUID();
453
- const connectParams = buildConnectParams(token, password);
454
- ws.send(JSON.stringify({
455
- type: "req",
456
- id: connectId,
457
- method: "connect",
458
- params: connectParams
459
- }));
460
- await this._waitForRes(connectId, 1e4);
619
+ const shouldFallback = process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "1" || process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "true";
620
+ try {
621
+ const connectParams = buildConnectParams(token, password, nonce);
622
+ ws.send(
623
+ JSON.stringify({
624
+ type: "req",
625
+ id: connectId,
626
+ method: "connect",
627
+ params: connectParams
628
+ })
629
+ );
630
+ const hello = await this._waitForRes(connectId, 1e4);
631
+ const grantedScopes = hello?.auth?.scopes;
632
+ if (Array.isArray(grantedScopes) && !grantedScopes.includes("operator.read")) {
633
+ throw new Error(
634
+ `Gateway connected but missing required scope: operator.read (granted: ${grantedScopes.join(", ")})`
635
+ );
636
+ }
637
+ } catch (err) {
638
+ if (!shouldFallback) throw err;
639
+ console.warn(
640
+ "[gateway-ws] Device auth connect failed; retrying without device identity (fallback enabled):",
641
+ err instanceof Error ? err.message : err
642
+ );
643
+ const fallbackId = randomUUID();
644
+ const fallbackParams = {
645
+ minProtocol: 3,
646
+ maxProtocol: 3,
647
+ client: {
648
+ id: "openclaw-control-ui",
649
+ displayName: "OpenCami",
650
+ version: "dev",
651
+ platform: process.platform,
652
+ mode: "webchat",
653
+ instanceId: loadOrCreateInstanceId()
654
+ },
655
+ caps: [],
656
+ auth: {
657
+ token: token || void 0,
658
+ password: password || void 0
659
+ },
660
+ role: "operator",
661
+ scopes: ["operator.read", "operator.write"],
662
+ userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
663
+ locale: process.env.LANG || "en"
664
+ };
665
+ ws.send(
666
+ JSON.stringify({
667
+ type: "req",
668
+ id: fallbackId,
669
+ method: "connect",
670
+ params: fallbackParams
671
+ })
672
+ );
673
+ await this._waitForRes(fallbackId, 1e4);
674
+ }
461
675
  this.connected = true;
462
676
  this.reconnectDelay = 1e3;
463
677
  console.log("[gateway-ws] Persistent connection established");
@@ -623,10 +837,12 @@ class PersistentGatewayConnection {
623
837
  }
624
838
  }
625
839
  let _instance = null;
840
+ const _g = globalThis;
626
841
  function getPersistentConnection() {
627
- if (!_instance) {
628
- _instance = new PersistentGatewayConnection();
842
+ if (!_g.__opencamiGatewayInstance) {
843
+ _g.__opencamiGatewayInstance = new PersistentGatewayConnection();
629
844
  }
845
+ _instance = _g.__opencamiGatewayInstance;
630
846
  return _instance;
631
847
  }
632
848
  async function gatewayRpc(method, params) {
@@ -728,7 +944,7 @@ async function ttsEdge(text, voice) {
728
944
  }
729
945
  });
730
946
  }
731
- const Route$n = createFileRoute("/api/tts")({
947
+ const Route$q = createFileRoute("/api/tts")({
732
948
  server: {
733
949
  handlers: {
734
950
  POST: async ({ request }) => {
@@ -883,7 +1099,7 @@ async function sttOpenAI(audioBlob, apiKey, language) {
883
1099
  const data = await res.json();
884
1100
  return { text: data.text || "" };
885
1101
  }
886
- const Route$m = createFileRoute("/api/stt")({
1102
+ const Route$p = createFileRoute("/api/stt")({
887
1103
  server: {
888
1104
  handlers: {
889
1105
  POST: async ({ request }) => {
@@ -985,7 +1201,7 @@ const Route$m = createFileRoute("/api/stt")({
985
1201
  }
986
1202
  }
987
1203
  });
988
- const Route$l = createFileRoute("/api/stream")({
1204
+ const Route$o = createFileRoute("/api/stream")({
989
1205
  server: {
990
1206
  handlers: {
991
1207
  GET: async ({ request }) => {
@@ -997,88 +1213,86 @@ const Route$l = createFileRoute("/api/stream")({
997
1213
  { status: 400, headers: { "content-type": "application/json" } }
998
1214
  );
999
1215
  }
1216
+ const pass = new PassThrough();
1000
1217
  const encoder = new TextEncoder();
1001
- let unsubscribe = null;
1002
1218
  let closed = false;
1003
- const stream = new ReadableStream({
1004
- start(controller) {
1005
- controller.enqueue(encoder.encode(": connected\n\n"));
1006
- function sendSSE(event, data) {
1007
- if (closed) return;
1008
- try {
1009
- controller.enqueue(
1010
- encoder.encode(`event: ${event}
1219
+ let unsubscribe = null;
1220
+ function sendSSE(event, data) {
1221
+ if (closed) return;
1222
+ try {
1223
+ pass.write(encoder.encode(`event: ${event}
1011
1224
  data: ${JSON.stringify(data)}
1012
1225
 
1013
- `)
1014
- );
1015
- } catch {
1226
+ `));
1227
+ } catch {
1228
+ }
1229
+ }
1230
+ function cleanup() {
1231
+ if (closed) return;
1232
+ closed = true;
1233
+ if (unsubscribe) {
1234
+ unsubscribe();
1235
+ unsubscribe = null;
1236
+ }
1237
+ try {
1238
+ pass.end();
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ pass.write(encoder.encode(": connected\n\n"));
1243
+ let gotAgentStream = false;
1244
+ unsubscribe = subscribeGatewayEvents(sessionKey, (evt) => {
1245
+ if (evt.event === "agent") {
1246
+ const payload = evt.payload;
1247
+ const agentStream = payload.stream;
1248
+ if (agentStream === "assistant") {
1249
+ gotAgentStream = true;
1250
+ const data = payload.data ?? payload;
1251
+ const text = typeof data.delta === "string" ? data.delta : typeof data.text === "string" ? data.text : typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : "";
1252
+ if (text) {
1253
+ sendSSE("delta", { text, sessionKey });
1254
+ }
1255
+ } else if (agentStream === "tool") {
1256
+ gotAgentStream = true;
1257
+ const tdata = payload.data ?? payload;
1258
+ sendSSE("tool", {
1259
+ name: tdata.name ?? tdata.toolName ?? payload.name ?? "",
1260
+ status: tdata.phase ?? tdata.status ?? payload.phase ?? "running",
1261
+ id: tdata.id ?? tdata.toolCallId ?? payload.id ?? "",
1262
+ sessionKey
1263
+ });
1264
+ } else if (agentStream === "lifecycle") {
1265
+ const ldata = payload.data ?? payload;
1266
+ const phase = ldata.phase ?? payload.phase;
1267
+ if (phase === "end" || phase === "error") {
1268
+ sendSSE("done", {
1269
+ sessionKey,
1270
+ status: phase,
1271
+ error: phase === "error" ? payload.error : void 0
1272
+ });
1273
+ cleanup();
1016
1274
  }
1017
1275
  }
1018
- let gotAgentStream = false;
1019
- unsubscribe = subscribeGatewayEvents(sessionKey, (evt) => {
1020
- if (evt.event === "agent") {
1021
- const payload = evt.payload;
1022
- const agentStream = payload.stream;
1023
- if (agentStream === "assistant") {
1024
- gotAgentStream = true;
1025
- const data = payload.data ?? payload;
1026
- const text = typeof data.delta === "string" ? data.delta : typeof data.text === "string" ? data.text : typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : "";
1027
- if (text) {
1028
- sendSSE("delta", { text, sessionKey });
1029
- }
1030
- } else if (agentStream === "tool") {
1031
- gotAgentStream = true;
1032
- const tdata = payload.data ?? payload;
1033
- sendSSE("tool", {
1034
- name: tdata.name ?? tdata.toolName ?? payload.name ?? "",
1035
- status: tdata.phase ?? tdata.status ?? payload.phase ?? "running",
1036
- id: tdata.id ?? tdata.toolCallId ?? payload.id ?? "",
1037
- sessionKey
1038
- });
1039
- } else if (agentStream === "lifecycle") {
1040
- const ldata = payload.data ?? payload;
1041
- const phase = ldata.phase ?? payload.phase;
1042
- if (phase === "end" || phase === "error") {
1043
- sendSSE("done", {
1044
- sessionKey,
1045
- status: phase,
1046
- error: phase === "error" ? payload.error : void 0
1047
- });
1048
- }
1049
- }
1050
- } else if (evt.event === "chat") {
1051
- const payload = evt.payload;
1052
- const kind = payload.kind;
1053
- if (kind === "delta" && !gotAgentStream) {
1054
- const text = typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : "";
1055
- if (text) {
1056
- sendSSE("delta", { text, sessionKey });
1057
- }
1058
- } else if (kind === "final") {
1059
- sendSSE("done", { sessionKey, status: "end" });
1060
- }
1276
+ } else if (evt.event === "chat") {
1277
+ const payload = evt.payload;
1278
+ const state = payload.state ?? payload.kind;
1279
+ const msg = payload.message;
1280
+ if (state === "delta" && !gotAgentStream) {
1281
+ const content = Array.isArray(msg?.content) ? msg.content : [];
1282
+ const firstBlock = content[0];
1283
+ const text = typeof firstBlock?.text === "string" ? firstBlock.text : typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : "";
1284
+ if (text) {
1285
+ sendSSE("delta", { text, sessionKey });
1061
1286
  }
1062
- });
1063
- },
1064
- cancel() {
1065
- closed = true;
1066
- if (unsubscribe) {
1067
- unsubscribe();
1068
- unsubscribe = null;
1287
+ } else if (state === "final") {
1288
+ sendSSE("done", { sessionKey, status: "end" });
1289
+ cleanup();
1069
1290
  }
1070
1291
  }
1071
1292
  });
1072
- if (request.signal) {
1073
- request.signal.addEventListener("abort", () => {
1074
- closed = true;
1075
- if (unsubscribe) {
1076
- unsubscribe();
1077
- unsubscribe = null;
1078
- }
1079
- });
1080
- }
1081
- return new Response(stream, {
1293
+ request.signal?.addEventListener("abort", cleanup);
1294
+ const webStream = Readable.toWeb(pass);
1295
+ return new Response(webStream, {
1082
1296
  headers: {
1083
1297
  "content-type": "text/event-stream",
1084
1298
  "cache-control": "no-cache, no-transform",
@@ -1144,7 +1358,7 @@ function parseSearchResults(output) {
1144
1358
  return { slug: line.trim(), displayName: line.trim(), version: "", summary: "" };
1145
1359
  });
1146
1360
  }
1147
- const Route$k = createFileRoute("/api/skills")({
1361
+ const Route$n = createFileRoute("/api/skills")({
1148
1362
  server: {
1149
1363
  handlers: {
1150
1364
  GET: async ({ request }) => {
@@ -1263,7 +1477,7 @@ function normalizeSessions(payload) {
1263
1477
  });
1264
1478
  return { sessions: normalized };
1265
1479
  }
1266
- const Route$j = createFileRoute("/api/sessions")({
1480
+ const Route$m = createFileRoute("/api/sessions")({
1267
1481
  server: {
1268
1482
  handlers: {
1269
1483
  GET: async () => {
@@ -1416,7 +1630,7 @@ const Route$j = createFileRoute("/api/sessions")({
1416
1630
  }
1417
1631
  }
1418
1632
  });
1419
- const Route$i = createFileRoute("/api/send")({
1633
+ const Route$l = createFileRoute("/api/send")({
1420
1634
  server: {
1421
1635
  handlers: {
1422
1636
  POST: async ({ request }) => {
@@ -1482,7 +1696,7 @@ const Route$i = createFileRoute("/api/send")({
1482
1696
  }
1483
1697
  }
1484
1698
  });
1485
- const Route$h = createFileRoute("/api/ping")({
1699
+ const Route$k = createFileRoute("/api/ping")({
1486
1700
  server: {
1487
1701
  handlers: {
1488
1702
  GET: async () => {
@@ -1506,7 +1720,7 @@ const categories = { "core": [{ "id": "cami", "name": "Cami", "emoji": "🦎", "
1506
1720
  const personasData = {
1507
1721
  categories
1508
1722
  };
1509
- const Route$g = createFileRoute("/api/personas")({
1723
+ const Route$j = createFileRoute("/api/personas")({
1510
1724
  server: {
1511
1725
  handlers: {
1512
1726
  GET: async () => {
@@ -1551,7 +1765,7 @@ function resolveSessionsDir() {
1551
1765
  )
1552
1766
  };
1553
1767
  }
1554
- const Route$f = createFileRoute("/api/paths")({
1768
+ const Route$i = createFileRoute("/api/paths")({
1555
1769
  server: {
1556
1770
  handlers: {
1557
1771
  GET: () => {
@@ -1576,7 +1790,7 @@ function parseModelName(modelId) {
1576
1790
  return word.charAt(0).toUpperCase() + word.slice(1);
1577
1791
  }).join(" ");
1578
1792
  }
1579
- const Route$e = createFileRoute("/api/models")({
1793
+ const Route$h = createFileRoute("/api/models")({
1580
1794
  server: {
1581
1795
  handlers: {
1582
1796
  GET: async () => {
@@ -1755,11 +1969,13 @@ async function testApiKey(apiKey) {
1755
1969
  }
1756
1970
  function getLlmConfig(request) {
1757
1971
  const headerKey = request.headers.get("X-OpenAI-API-Key");
1758
- const baseUrl = request.headers.get("X-LLM-Base-URL")?.trim() || null;
1972
+ const headerBaseUrl = request.headers.get("X-LLM-Base-URL")?.trim() || null;
1973
+ const baseUrl = headerBaseUrl || process.env.LLM_BASE_URL?.trim() || null;
1759
1974
  const isOpenRouter = baseUrl?.includes("openrouter.ai");
1760
- const envKey = isOpenRouter ? process.env.OPENROUTER_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() : process.env.OPENAI_API_KEY?.trim();
1975
+ const isKilocode = baseUrl?.includes("kilo.ai");
1976
+ const envKey = isOpenRouter ? process.env.OPENROUTER_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() : isKilocode ? process.env.KILOCODE_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() : process.env.OPENAI_API_KEY?.trim();
1761
1977
  const apiKey = headerKey?.trim() || envKey || null;
1762
- const model = request.headers.get("X-LLM-Model")?.trim() || null;
1978
+ const model = request.headers.get("X-LLM-Model")?.trim() || process.env.LLM_MODEL?.trim() || null;
1763
1979
  return { apiKey, baseUrl, model };
1764
1980
  }
1765
1981
  function generateHeuristicTitle(message) {
@@ -1782,7 +1998,7 @@ function generateHeuristicTitle(message) {
1782
1998
  }
1783
1999
  return title || message.slice(0, 50);
1784
2000
  }
1785
- const Route$d = createFileRoute("/api/llm-features")({
2001
+ const Route$g = createFileRoute("/api/llm-features")({
1786
2002
  server: {
1787
2003
  handlers: {
1788
2004
  /**
@@ -1792,10 +2008,12 @@ const Route$d = createFileRoute("/api/llm-features")({
1792
2008
  try {
1793
2009
  const hasEnvKey = Boolean(process.env.OPENAI_API_KEY?.trim());
1794
2010
  const hasOpenRouterKey = Boolean(process.env.OPENROUTER_API_KEY?.trim());
2011
+ const hasKilocodeKey = Boolean(process.env.KILOCODE_API_KEY?.trim());
1795
2012
  return json({
1796
2013
  ok: true,
1797
2014
  hasEnvKey,
1798
- hasOpenRouterKey
2015
+ hasOpenRouterKey,
2016
+ hasKilocodeKey
1799
2017
  });
1800
2018
  } catch (err) {
1801
2019
  return json({
@@ -1934,7 +2152,7 @@ const Route$d = createFileRoute("/api/llm-features")({
1934
2152
  }
1935
2153
  }
1936
2154
  });
1937
- const Route$c = createFileRoute("/api/history")({
2155
+ const Route$f = createFileRoute("/api/history")({
1938
2156
  server: {
1939
2157
  handlers: {
1940
2158
  GET: async ({ request }) => {
@@ -1997,7 +2215,7 @@ function parseFollowUps(text) {
1997
2215
  const lines = text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => line.replace(/^\d+[.)\s]+/, "").trim()).map((line) => line.replace(/^[-•*]\s*/, "").trim()).map((line) => line.replace(/^["']|["']$/g, "").trim()).filter((line) => line.length > 0 && line.length < 150);
1998
2216
  return lines.slice(0, 3);
1999
2217
  }
2000
- const Route$b = createFileRoute("/api/follow-ups")({
2218
+ const Route$e = createFileRoute("/api/follow-ups")({
2001
2219
  server: {
2002
2220
  handlers: {
2003
2221
  POST: async ({ request }) => {
@@ -2108,7 +2326,7 @@ function parsePatch(value) {
2108
2326
  if (Object.keys(patch).length === 0) return null;
2109
2327
  return patch;
2110
2328
  }
2111
- const Route$a = createFileRoute("/api/cron")({
2329
+ const Route$d = createFileRoute("/api/cron")({
2112
2330
  server: {
2113
2331
  handlers: {
2114
2332
  GET: async ({ request }) => {
@@ -2185,7 +2403,7 @@ function resolveModel(m) {
2185
2403
  if (typeof m === "object" && "primary" in m) return m.primary;
2186
2404
  return void 0;
2187
2405
  }
2188
- const Route$9 = createFileRoute("/api/agents")({
2406
+ const Route$c = createFileRoute("/api/agents")({
2189
2407
  server: {
2190
2408
  handlers: {
2191
2409
  GET: async () => {
@@ -2605,7 +2823,7 @@ function validateFilename(filename) {
2605
2823
  return true;
2606
2824
  }
2607
2825
  const MAX_FILE_SIZE = 100 * 1024 * 1024;
2608
- const Route$8 = createFileRoute("/api/files/upload")({
2826
+ const Route$b = createFileRoute("/api/files/upload")({
2609
2827
  server: {
2610
2828
  handlers: {
2611
2829
  POST: async ({ request }) => {
@@ -2700,7 +2918,7 @@ const Route$8 = createFileRoute("/api/files/upload")({
2700
2918
  }
2701
2919
  });
2702
2920
  const MAX_TEXT_SIZE$1 = 5 * 1024 * 1024;
2703
- const Route$7 = createFileRoute("/api/files/save")({
2921
+ const Route$a = createFileRoute("/api/files/save")({
2704
2922
  server: {
2705
2923
  handlers: {
2706
2924
  POST: async ({ request }) => {
@@ -2742,7 +2960,7 @@ const Route$7 = createFileRoute("/api/files/save")({
2742
2960
  }
2743
2961
  }
2744
2962
  });
2745
- const Route$6 = createFileRoute("/api/files/rename")({
2963
+ const Route$9 = createFileRoute("/api/files/rename")({
2746
2964
  server: {
2747
2965
  handlers: {
2748
2966
  POST: async ({ request }) => {
@@ -2885,7 +3103,7 @@ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2885
3103
  "diff",
2886
3104
  "patch"
2887
3105
  ]);
2888
- const Route$5 = createFileRoute("/api/files/read")({
3106
+ const Route$8 = createFileRoute("/api/files/read")({
2889
3107
  server: {
2890
3108
  handlers: {
2891
3109
  GET: async ({ request }) => {
@@ -2977,7 +3195,7 @@ function validateFolderName(path2) {
2977
3195
  throw new Error("Invalid folder name");
2978
3196
  }
2979
3197
  }
2980
- const Route$4 = createFileRoute("/api/files/mkdir")({
3198
+ const Route$7 = createFileRoute("/api/files/mkdir")({
2981
3199
  server: {
2982
3200
  handlers: {
2983
3201
  POST: async ({ request }) => {
@@ -3026,7 +3244,7 @@ const Route$4 = createFileRoute("/api/files/mkdir")({
3026
3244
  }
3027
3245
  }
3028
3246
  });
3029
- const Route$3 = createFileRoute("/api/files/list")({
3247
+ const Route$6 = createFileRoute("/api/files/list")({
3030
3248
  server: {
3031
3249
  handlers: {
3032
3250
  GET: async ({ request }) => {
@@ -3061,7 +3279,7 @@ const Route$3 = createFileRoute("/api/files/list")({
3061
3279
  }
3062
3280
  }
3063
3281
  });
3064
- const Route$2 = createFileRoute("/api/files/info")({
3282
+ const Route$5 = createFileRoute("/api/files/info")({
3065
3283
  server: {
3066
3284
  handlers: {
3067
3285
  GET: async ({ request }) => {
@@ -3182,7 +3400,7 @@ function isStaticAsset(filename) {
3182
3400
  const staticExts = ["png", "jpg", "jpeg", "gif", "webp", "svg", "css", "js", "woff", "woff2", "ttf", "otf"];
3183
3401
  return staticExts.includes(ext);
3184
3402
  }
3185
- const Route$1 = createFileRoute("/api/files/download")({
3403
+ const Route$4 = createFileRoute("/api/files/download")({
3186
3404
  server: {
3187
3405
  handlers: {
3188
3406
  GET: async ({ request }) => {
@@ -3248,7 +3466,7 @@ const Route$1 = createFileRoute("/api/files/download")({
3248
3466
  }
3249
3467
  }
3250
3468
  });
3251
- const Route = createFileRoute("/api/files/delete")({
3469
+ const Route$3 = createFileRoute("/api/files/delete")({
3252
3470
  server: {
3253
3471
  handlers: {
3254
3472
  DELETE: async ({ request }) => {
@@ -3306,176 +3524,452 @@ const Route = createFileRoute("/api/files/delete")({
3306
3524
  }
3307
3525
  }
3308
3526
  });
3309
- const SkillsRoute = Route$w.update({
3527
+ function clampPercent(value) {
3528
+ if (!Number.isFinite(value)) return 0;
3529
+ return Math.max(0, Math.min(100, value));
3530
+ }
3531
+ function sampleCpuTimes() {
3532
+ const cpus = os.cpus();
3533
+ let idle = 0;
3534
+ let total = 0;
3535
+ for (const cpu of cpus) {
3536
+ idle += cpu.times.idle;
3537
+ total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
3538
+ }
3539
+ return { idle, total };
3540
+ }
3541
+ async function getCpuUsagePercent(sampleMs = 500) {
3542
+ const start = sampleCpuTimes();
3543
+ await new Promise((resolve2) => setTimeout(resolve2, sampleMs));
3544
+ const end = sampleCpuTimes();
3545
+ const idleDelta = end.idle - start.idle;
3546
+ const totalDelta = end.total - start.total;
3547
+ if (totalDelta <= 0) return 0;
3548
+ return clampPercent((totalDelta - idleDelta) / totalDelta * 100);
3549
+ }
3550
+ function getDiskStats() {
3551
+ try {
3552
+ const raw = execSync("df -k / | tail -1", {
3553
+ encoding: "utf8",
3554
+ stdio: ["ignore", "pipe", "ignore"]
3555
+ }).trim();
3556
+ const parts = raw.split(/\s+/);
3557
+ if (parts.length < 5) return { used: 0, total: 0 };
3558
+ const totalKb = Number(parts[1]);
3559
+ const usedKb = Number(parts[2]);
3560
+ if (!Number.isFinite(totalKb) || totalKb <= 0) return { used: 0, total: 0 };
3561
+ const total = totalKb * 1024;
3562
+ const used = usedKb * 1024;
3563
+ return { used, total };
3564
+ } catch {
3565
+ return { used: 0, total: 0 };
3566
+ }
3567
+ }
3568
+ function formatUptime(seconds) {
3569
+ const d = Math.floor(seconds / 86400);
3570
+ const h = Math.floor(seconds % 86400 / 3600);
3571
+ const m = Math.floor(seconds % 3600 / 60);
3572
+ const parts = [];
3573
+ if (d > 0) parts.push(`${d}d`);
3574
+ if (h > 0) parts.push(`${h}h`);
3575
+ parts.push(`${m}m`);
3576
+ return parts.join(" ");
3577
+ }
3578
+ function formatBytesCompact(bytes) {
3579
+ const units = ["B", "KB", "MB", "GB", "TB"];
3580
+ let value = Math.max(0, bytes);
3581
+ let unitIndex = 0;
3582
+ while (value >= 1024 && unitIndex < units.length - 1) {
3583
+ value /= 1024;
3584
+ unitIndex += 1;
3585
+ }
3586
+ const digits = value >= 100 ? 0 : value >= 10 ? 1 : 2;
3587
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
3588
+ }
3589
+ function getNetworkIo() {
3590
+ try {
3591
+ const netRaw = execSync("cat /proc/net/dev", {
3592
+ encoding: "utf8",
3593
+ stdio: ["ignore", "pipe", "ignore"],
3594
+ timeout: 3e3
3595
+ });
3596
+ const lines = netRaw.split("\n").map((line) => line.trim()).filter(Boolean);
3597
+ let selected = null;
3598
+ for (const line of lines.slice(2)) {
3599
+ const [ifaceRaw, dataRaw] = line.split(":");
3600
+ if (!ifaceRaw || !dataRaw) continue;
3601
+ const iface = ifaceRaw.trim();
3602
+ const fields = dataRaw.trim().split(/\s+/);
3603
+ if (fields.length < 16) continue;
3604
+ const rx = Number(fields[0]);
3605
+ const tx = Number(fields[8]);
3606
+ if (!Number.isFinite(rx) || !Number.isFinite(tx)) continue;
3607
+ const entry = { iface, rx, tx };
3608
+ if (iface !== "lo") {
3609
+ selected = entry;
3610
+ break;
3611
+ }
3612
+ if (!selected) selected = entry;
3613
+ }
3614
+ if (!selected) return { rx: "0 MB", tx: "0 MB" };
3615
+ return {
3616
+ rx: formatBytesCompact(selected.rx),
3617
+ tx: formatBytesCompact(selected.tx)
3618
+ };
3619
+ } catch {
3620
+ return { rx: "0 MB", tx: "0 MB" };
3621
+ }
3622
+ }
3623
+ function getTopProcesses() {
3624
+ try {
3625
+ const procs = execSync("ps aux --sort=-%cpu | head -6 | tail -5", {
3626
+ encoding: "utf8",
3627
+ stdio: ["ignore", "pipe", "ignore"],
3628
+ timeout: 3e3
3629
+ });
3630
+ return procs.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
3631
+ const parts = line.split(/\s+/);
3632
+ if (parts.length < 11) return null;
3633
+ const cpu = Number(parts[2]);
3634
+ const mem = Number(parts[3]);
3635
+ const command = parts.slice(10).join(" ");
3636
+ const name = command.split("/").pop()?.split(" ")[0] || command;
3637
+ if (!Number.isFinite(cpu) || !Number.isFinite(mem)) return null;
3638
+ return {
3639
+ name: name.slice(0, 30),
3640
+ cpu,
3641
+ mem
3642
+ };
3643
+ }).filter(
3644
+ (p) => p !== null
3645
+ );
3646
+ } catch {
3647
+ return [];
3648
+ }
3649
+ }
3650
+ async function getSystemStats() {
3651
+ const totalRam = os.totalmem();
3652
+ const freeRam = os.freemem();
3653
+ const usedRam = Math.max(0, totalRam - freeRam);
3654
+ const [cpuUsage, disk] = await Promise.all([
3655
+ getCpuUsagePercent(500),
3656
+ Promise.resolve(getDiskStats())
3657
+ ]);
3658
+ const cpus = os.cpus();
3659
+ const cpuModel = cpus[0]?.model ?? "Unknown CPU";
3660
+ const cores = cpus.length;
3661
+ return {
3662
+ cpu: cpuUsage,
3663
+ ram: { used: usedRam, total: totalRam },
3664
+ disk,
3665
+ load: os.loadavg().map((v) => Number(v.toFixed(2))),
3666
+ uptime: formatUptime(os.uptime()),
3667
+ network: getNetworkIo(),
3668
+ topProcesses: getTopProcesses(),
3669
+ cpuModel,
3670
+ cores
3671
+ };
3672
+ }
3673
+ async function getGatewayStats() {
3674
+ try {
3675
+ const sessionsJson = execSync("openclaw sessions list --json 2>/dev/null", {
3676
+ encoding: "utf8",
3677
+ stdio: ["ignore", "pipe", "ignore"],
3678
+ timeout: 5e3
3679
+ });
3680
+ const sessionsData = JSON.parse(sessionsJson);
3681
+ const activeSessions = sessionsData.count ?? sessionsData.sessions?.length ?? 0;
3682
+ return { activeSessions, tokensToday: 0, costToday: 0 };
3683
+ } catch {
3684
+ return { activeSessions: 0, tokensToday: 0, costToday: 0 };
3685
+ }
3686
+ }
3687
+ async function getCronOverview() {
3688
+ try {
3689
+ const cronJson = execSync("openclaw cron list --json 2>/dev/null", {
3690
+ encoding: "utf8",
3691
+ stdio: ["ignore", "pipe", "ignore"],
3692
+ timeout: 5e3
3693
+ });
3694
+ const data = JSON.parse(cronJson);
3695
+ const jobs = data.jobs ?? [];
3696
+ return jobs.map((job) => {
3697
+ const schedule = job.schedule;
3698
+ const state = job.state;
3699
+ let scheduleStr = "—";
3700
+ if (schedule) {
3701
+ if (schedule.expr) scheduleStr = String(schedule.expr);
3702
+ else if (schedule.kind === "every" && schedule.everyMs)
3703
+ scheduleStr = `every ${Number(schedule.everyMs) / 1e3 / 60}m`;
3704
+ else if (schedule.kind === "at") scheduleStr = `at ${schedule.at}`;
3705
+ else if (schedule.kind) scheduleStr = String(schedule.kind);
3706
+ }
3707
+ let nextRun = null;
3708
+ if (state?.nextRunAtMs && Number(state.nextRunAtMs) > 0) {
3709
+ nextRun = new Date(Number(state.nextRunAtMs)).toISOString();
3710
+ }
3711
+ let lastRun = null;
3712
+ if (state?.lastRunAtMs && Number(state.lastRunAtMs) > 0) {
3713
+ lastRun = new Date(Number(state.lastRunAtMs)).toISOString();
3714
+ }
3715
+ const rawStatus = state?.lastStatus;
3716
+ const lastStatus = rawStatus === "ok" ? "ok" : rawStatus === "error" ? "error" : rawStatus === "idle" ? "idle" : "unknown";
3717
+ return {
3718
+ id: String(job.id ?? ""),
3719
+ name: String(job.name ?? job.id ?? "Unnamed"),
3720
+ schedule: scheduleStr,
3721
+ enabled: Boolean(job.enabled ?? true),
3722
+ nextRun,
3723
+ lastRun,
3724
+ lastStatus
3725
+ };
3726
+ });
3727
+ } catch {
3728
+ return [];
3729
+ }
3730
+ }
3731
+ const Route$2 = createFileRoute("/api/dashboard/system")({
3732
+ server: {
3733
+ handlers: {
3734
+ GET: async () => {
3735
+ try {
3736
+ const data = await getSystemStats();
3737
+ return json(data);
3738
+ } catch (err) {
3739
+ return json(
3740
+ { error: err instanceof Error ? err.message : String(err) },
3741
+ { status: 500 }
3742
+ );
3743
+ }
3744
+ }
3745
+ }
3746
+ }
3747
+ });
3748
+ const Route$1 = createFileRoute("/api/dashboard/gateway")({
3749
+ server: {
3750
+ handlers: {
3751
+ GET: async () => {
3752
+ try {
3753
+ const data = await getGatewayStats();
3754
+ return json(data);
3755
+ } catch (err) {
3756
+ return json(
3757
+ { error: err instanceof Error ? err.message : String(err) },
3758
+ { status: 500 }
3759
+ );
3760
+ }
3761
+ }
3762
+ }
3763
+ }
3764
+ });
3765
+ const Route = createFileRoute("/api/dashboard/crons")({
3766
+ server: {
3767
+ handlers: {
3768
+ GET: async () => {
3769
+ try {
3770
+ const data = await getCronOverview();
3771
+ return json({ jobs: data });
3772
+ } catch (err) {
3773
+ return json(
3774
+ { error: err instanceof Error ? err.message : String(err) },
3775
+ { status: 500 }
3776
+ );
3777
+ }
3778
+ }
3779
+ }
3780
+ }
3781
+ });
3782
+ const SkillsRoute = Route$A.update({
3310
3783
  id: "/skills",
3311
3784
  path: "/skills",
3312
- getParentRoute: () => Route$x
3785
+ getParentRoute: () => Route$B
3313
3786
  });
3314
- const NewRoute = Route$v.update({
3787
+ const NewRoute = Route$z.update({
3315
3788
  id: "/new",
3316
3789
  path: "/new",
3317
- getParentRoute: () => Route$x
3790
+ getParentRoute: () => Route$B
3318
3791
  });
3319
- const MemoryRoute = Route$u.update({
3792
+ const MemoryRoute = Route$y.update({
3320
3793
  id: "/memory",
3321
3794
  path: "/memory",
3322
- getParentRoute: () => Route$x
3795
+ getParentRoute: () => Route$B
3323
3796
  });
3324
- const FilesRoute = Route$t.update({
3797
+ const FilesRoute = Route$x.update({
3325
3798
  id: "/files",
3326
3799
  path: "/files",
3327
- getParentRoute: () => Route$x
3800
+ getParentRoute: () => Route$B
3801
+ });
3802
+ const DashboardRoute = Route$w.update({
3803
+ id: "/dashboard",
3804
+ path: "/dashboard",
3805
+ getParentRoute: () => Route$B
3328
3806
  });
3329
- const ConnectRoute = Route$s.update({
3807
+ const ConnectRoute = Route$v.update({
3330
3808
  id: "/connect",
3331
3809
  path: "/connect",
3332
- getParentRoute: () => Route$x
3810
+ getParentRoute: () => Route$B
3333
3811
  });
3334
- const BotsRoute = Route$r.update({
3812
+ const BotsRoute = Route$u.update({
3335
3813
  id: "/bots",
3336
3814
  path: "/bots",
3337
- getParentRoute: () => Route$x
3815
+ getParentRoute: () => Route$B
3338
3816
  });
3339
- const AgentsRoute = Route$q.update({
3817
+ const AgentsRoute = Route$t.update({
3340
3818
  id: "/agents",
3341
3819
  path: "/agents",
3342
- getParentRoute: () => Route$x
3820
+ getParentRoute: () => Route$B
3343
3821
  });
3344
- const IndexRoute = Route$p.update({
3822
+ const IndexRoute = Route$s.update({
3345
3823
  id: "/",
3346
3824
  path: "/",
3347
- getParentRoute: () => Route$x
3825
+ getParentRoute: () => Route$B
3348
3826
  });
3349
- const ChatSessionKeyRoute = Route$o.update({
3827
+ const ChatSessionKeyRoute = Route$r.update({
3350
3828
  id: "/chat/$sessionKey",
3351
3829
  path: "/chat/$sessionKey",
3352
- getParentRoute: () => Route$x
3830
+ getParentRoute: () => Route$B
3353
3831
  });
3354
- const ApiTtsRoute = Route$n.update({
3832
+ const ApiTtsRoute = Route$q.update({
3355
3833
  id: "/api/tts",
3356
3834
  path: "/api/tts",
3357
- getParentRoute: () => Route$x
3835
+ getParentRoute: () => Route$B
3358
3836
  });
3359
- const ApiSttRoute = Route$m.update({
3837
+ const ApiSttRoute = Route$p.update({
3360
3838
  id: "/api/stt",
3361
3839
  path: "/api/stt",
3362
- getParentRoute: () => Route$x
3840
+ getParentRoute: () => Route$B
3363
3841
  });
3364
- const ApiStreamRoute = Route$l.update({
3842
+ const ApiStreamRoute = Route$o.update({
3365
3843
  id: "/api/stream",
3366
3844
  path: "/api/stream",
3367
- getParentRoute: () => Route$x
3845
+ getParentRoute: () => Route$B
3368
3846
  });
3369
- const ApiSkillsRoute = Route$k.update({
3847
+ const ApiSkillsRoute = Route$n.update({
3370
3848
  id: "/api/skills",
3371
3849
  path: "/api/skills",
3372
- getParentRoute: () => Route$x
3850
+ getParentRoute: () => Route$B
3373
3851
  });
3374
- const ApiSessionsRoute = Route$j.update({
3852
+ const ApiSessionsRoute = Route$m.update({
3375
3853
  id: "/api/sessions",
3376
3854
  path: "/api/sessions",
3377
- getParentRoute: () => Route$x
3855
+ getParentRoute: () => Route$B
3378
3856
  });
3379
- const ApiSendRoute = Route$i.update({
3857
+ const ApiSendRoute = Route$l.update({
3380
3858
  id: "/api/send",
3381
3859
  path: "/api/send",
3382
- getParentRoute: () => Route$x
3860
+ getParentRoute: () => Route$B
3383
3861
  });
3384
- const ApiPingRoute = Route$h.update({
3862
+ const ApiPingRoute = Route$k.update({
3385
3863
  id: "/api/ping",
3386
3864
  path: "/api/ping",
3387
- getParentRoute: () => Route$x
3865
+ getParentRoute: () => Route$B
3388
3866
  });
3389
- const ApiPersonasRoute = Route$g.update({
3867
+ const ApiPersonasRoute = Route$j.update({
3390
3868
  id: "/api/personas",
3391
3869
  path: "/api/personas",
3392
- getParentRoute: () => Route$x
3870
+ getParentRoute: () => Route$B
3393
3871
  });
3394
- const ApiPathsRoute = Route$f.update({
3872
+ const ApiPathsRoute = Route$i.update({
3395
3873
  id: "/api/paths",
3396
3874
  path: "/api/paths",
3397
- getParentRoute: () => Route$x
3875
+ getParentRoute: () => Route$B
3398
3876
  });
3399
- const ApiModelsRoute = Route$e.update({
3877
+ const ApiModelsRoute = Route$h.update({
3400
3878
  id: "/api/models",
3401
3879
  path: "/api/models",
3402
- getParentRoute: () => Route$x
3880
+ getParentRoute: () => Route$B
3403
3881
  });
3404
- const ApiLlmFeaturesRoute = Route$d.update({
3882
+ const ApiLlmFeaturesRoute = Route$g.update({
3405
3883
  id: "/api/llm-features",
3406
3884
  path: "/api/llm-features",
3407
- getParentRoute: () => Route$x
3885
+ getParentRoute: () => Route$B
3408
3886
  });
3409
- const ApiHistoryRoute = Route$c.update({
3887
+ const ApiHistoryRoute = Route$f.update({
3410
3888
  id: "/api/history",
3411
3889
  path: "/api/history",
3412
- getParentRoute: () => Route$x
3890
+ getParentRoute: () => Route$B
3413
3891
  });
3414
- const ApiFollowUpsRoute = Route$b.update({
3892
+ const ApiFollowUpsRoute = Route$e.update({
3415
3893
  id: "/api/follow-ups",
3416
3894
  path: "/api/follow-ups",
3417
- getParentRoute: () => Route$x
3895
+ getParentRoute: () => Route$B
3418
3896
  });
3419
- const ApiCronRoute = Route$a.update({
3897
+ const ApiCronRoute = Route$d.update({
3420
3898
  id: "/api/cron",
3421
3899
  path: "/api/cron",
3422
- getParentRoute: () => Route$x
3900
+ getParentRoute: () => Route$B
3423
3901
  });
3424
- const ApiAgentsRoute = Route$9.update({
3902
+ const ApiAgentsRoute = Route$c.update({
3425
3903
  id: "/api/agents",
3426
3904
  path: "/api/agents",
3427
- getParentRoute: () => Route$x
3905
+ getParentRoute: () => Route$B
3428
3906
  });
3429
- const ApiFilesUploadRoute = Route$8.update({
3907
+ const ApiFilesUploadRoute = Route$b.update({
3430
3908
  id: "/api/files/upload",
3431
3909
  path: "/api/files/upload",
3432
- getParentRoute: () => Route$x
3910
+ getParentRoute: () => Route$B
3433
3911
  });
3434
- const ApiFilesSaveRoute = Route$7.update({
3912
+ const ApiFilesSaveRoute = Route$a.update({
3435
3913
  id: "/api/files/save",
3436
3914
  path: "/api/files/save",
3437
- getParentRoute: () => Route$x
3915
+ getParentRoute: () => Route$B
3438
3916
  });
3439
- const ApiFilesRenameRoute = Route$6.update({
3917
+ const ApiFilesRenameRoute = Route$9.update({
3440
3918
  id: "/api/files/rename",
3441
3919
  path: "/api/files/rename",
3442
- getParentRoute: () => Route$x
3920
+ getParentRoute: () => Route$B
3443
3921
  });
3444
- const ApiFilesReadRoute = Route$5.update({
3922
+ const ApiFilesReadRoute = Route$8.update({
3445
3923
  id: "/api/files/read",
3446
3924
  path: "/api/files/read",
3447
- getParentRoute: () => Route$x
3925
+ getParentRoute: () => Route$B
3448
3926
  });
3449
- const ApiFilesMkdirRoute = Route$4.update({
3927
+ const ApiFilesMkdirRoute = Route$7.update({
3450
3928
  id: "/api/files/mkdir",
3451
3929
  path: "/api/files/mkdir",
3452
- getParentRoute: () => Route$x
3930
+ getParentRoute: () => Route$B
3453
3931
  });
3454
- const ApiFilesListRoute = Route$3.update({
3932
+ const ApiFilesListRoute = Route$6.update({
3455
3933
  id: "/api/files/list",
3456
3934
  path: "/api/files/list",
3457
- getParentRoute: () => Route$x
3935
+ getParentRoute: () => Route$B
3458
3936
  });
3459
- const ApiFilesInfoRoute = Route$2.update({
3937
+ const ApiFilesInfoRoute = Route$5.update({
3460
3938
  id: "/api/files/info",
3461
3939
  path: "/api/files/info",
3462
- getParentRoute: () => Route$x
3940
+ getParentRoute: () => Route$B
3463
3941
  });
3464
- const ApiFilesDownloadRoute = Route$1.update({
3942
+ const ApiFilesDownloadRoute = Route$4.update({
3465
3943
  id: "/api/files/download",
3466
3944
  path: "/api/files/download",
3467
- getParentRoute: () => Route$x
3945
+ getParentRoute: () => Route$B
3468
3946
  });
3469
- const ApiFilesDeleteRoute = Route.update({
3947
+ const ApiFilesDeleteRoute = Route$3.update({
3470
3948
  id: "/api/files/delete",
3471
3949
  path: "/api/files/delete",
3472
- getParentRoute: () => Route$x
3950
+ getParentRoute: () => Route$B
3951
+ });
3952
+ const ApiDashboardSystemRoute = Route$2.update({
3953
+ id: "/api/dashboard/system",
3954
+ path: "/api/dashboard/system",
3955
+ getParentRoute: () => Route$B
3956
+ });
3957
+ const ApiDashboardGatewayRoute = Route$1.update({
3958
+ id: "/api/dashboard/gateway",
3959
+ path: "/api/dashboard/gateway",
3960
+ getParentRoute: () => Route$B
3961
+ });
3962
+ const ApiDashboardCronsRoute = Route.update({
3963
+ id: "/api/dashboard/crons",
3964
+ path: "/api/dashboard/crons",
3965
+ getParentRoute: () => Route$B
3473
3966
  });
3474
3967
  const rootRouteChildren = {
3475
3968
  IndexRoute,
3476
3969
  AgentsRoute,
3477
3970
  BotsRoute,
3478
3971
  ConnectRoute,
3972
+ DashboardRoute,
3479
3973
  FilesRoute,
3480
3974
  MemoryRoute,
3481
3975
  NewRoute,
@@ -3496,6 +3990,9 @@ const rootRouteChildren = {
3496
3990
  ApiSttRoute,
3497
3991
  ApiTtsRoute,
3498
3992
  ChatSessionKeyRoute,
3993
+ ApiDashboardCronsRoute,
3994
+ ApiDashboardGatewayRoute,
3995
+ ApiDashboardSystemRoute,
3499
3996
  ApiFilesDeleteRoute,
3500
3997
  ApiFilesDownloadRoute,
3501
3998
  ApiFilesInfoRoute,
@@ -3506,7 +4003,7 @@ const rootRouteChildren = {
3506
4003
  ApiFilesSaveRoute,
3507
4004
  ApiFilesUploadRoute
3508
4005
  };
3509
- const routeTree = Route$x._addFileChildren(rootRouteChildren)._addFileTypes();
4006
+ const routeTree = Route$B._addFileChildren(rootRouteChildren)._addFileTypes();
3510
4007
  const getRouter = () => {
3511
4008
  const router2 = createRouter({
3512
4009
  routeTree,
@@ -3521,7 +4018,7 @@ const router = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
3521
4018
  getRouter
3522
4019
  }, Symbol.toStringTag, { value: "Module" }));
3523
4020
  export {
3524
- Route$p as R,
3525
- Route$o as a,
4021
+ Route$s as R,
4022
+ Route$r as a,
3526
4023
  router as r
3527
4024
  };