opencami 1.8.3 → 1.8.6

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 (82) hide show
  1. package/README.md +63 -96
  2. package/dist/client/assets/{CSPContext-DeJH85nm.js → CSPContext-6t3O1emU.js} +1 -1
  3. package/dist/client/assets/{DirectionContext-CxhRpXkm.js → DirectionContext-C6goXEY_.js} +1 -1
  4. package/dist/client/assets/_sessionKey-B5Viv43f.js +23 -0
  5. package/dist/client/assets/agents-BmE6QOwl.js +2 -0
  6. package/dist/client/assets/agents-screen-pHUzJxX5.js +1 -0
  7. package/dist/client/assets/bots-BeOkwrXr.js +2 -0
  8. package/dist/client/assets/{bots-screen-Be3cfGgq.js → bots-screen-B79bAYvf.js} +1 -1
  9. package/dist/client/assets/{button-D9Plv7hu.js → button-CK8tKQ-Z.js} +1 -1
  10. package/dist/client/assets/{composite-B2KCZKKL.js → composite-feK0c-xF.js} +1 -1
  11. package/dist/client/assets/{connect-DuJfnyNK.js → connect-02tmQV_v.js} +1 -1
  12. package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
  13. package/dist/client/assets/{dashboard-00GpXm5V.js → dashboard-DQ0zDQKd.js} +1 -1
  14. package/dist/client/assets/event-BsD1rqGT.js +1 -0
  15. package/dist/client/assets/file-explorer-screen-Ds7LeJTd.js +1 -0
  16. package/dist/client/assets/files-e40B1zFy.js +2 -0
  17. package/dist/client/assets/go-CxLEBnE3.js +1 -0
  18. package/dist/client/assets/{index-Yo5UhdZV.js → index-lK3yGoTI.js} +1 -1
  19. package/dist/client/assets/{index-DtGzE-ea.js → index-rljDU_1M.js} +2 -2
  20. package/dist/client/assets/keyboard-shortcuts-dialog-Bb_GOr9L.js +1 -0
  21. package/dist/client/assets/main-Dq6jpr6-.js +210 -0
  22. package/dist/client/assets/{markdown-DtWnt4NA.js → markdown-C7_Aipwd.js} +37 -37
  23. package/dist/client/assets/memory-C7UG-1le.js +2 -0
  24. package/dist/client/assets/memory-screen-CUFBWsq5.js +1 -0
  25. package/dist/client/assets/menu-n6L--M9R.js +1 -0
  26. package/dist/client/assets/{opencami-logo-Bmge6-FB.js → opencami-logo-zuSBm5Br.js} +1 -1
  27. package/dist/client/assets/php-Dhbhpdrm.js +1 -0
  28. package/dist/client/assets/proxy-BU8Bw1Vt.js +9 -0
  29. package/dist/client/assets/{react-DODKNyyU.js → react-BLyCEWpN.js} +1 -1
  30. package/dist/client/assets/ruby-NiQIzKut.js +1 -0
  31. package/dist/client/assets/search-dialog-yB4w5ajo.js +1 -0
  32. package/dist/client/assets/session-export-dialog-qbZgd2Zo.js +1 -0
  33. package/dist/client/assets/settings-dialog-CHJbvpgk.js +1 -0
  34. package/dist/client/assets/skills-DoKPPhNY.js +2 -0
  35. package/dist/client/assets/{skills-panel-DSiH-DLs.js → skills-panel-BH27r3nC.js} +1 -1
  36. package/dist/client/assets/styles-CXV5jZiD.css +1 -0
  37. package/dist/client/assets/{swift-Dg5xB15N.js → swift-D82vCrfD.js} +1 -1
  38. package/dist/client/assets/switch-BD3a0LRm.js +1 -0
  39. package/dist/client/assets/tabs-DI1e-kzz.js +1 -0
  40. package/dist/client/assets/tooltip-BbH3QWvK.js +1 -0
  41. package/dist/client/assets/use-file-explorer-state-DBfLeAyz.js +12 -0
  42. package/dist/client/assets/{useBaseUiId-KQTzRPLp.js → useBaseUiId-MgM4ouhx.js} +1 -1
  43. package/dist/client/assets/{useCompositeItem-BPY2_hF_.js → useCompositeItem-OhltNFdZ.js} +1 -1
  44. package/dist/client/assets/{useControlled-B5pEEz2V.js → useControlled-BQxTgsOd.js} +1 -1
  45. package/dist/client/assets/{useMutation-BsQD6FKe.js → useMutation-12DyB3Ox.js} +1 -1
  46. package/dist/client/assets/useOnFirstRender-7qoaK5sI.js +1 -0
  47. package/dist/client/assets/{useQuery-CmAJuY2W.js → useQuery-Ctiljcrr.js} +1 -1
  48. package/dist/server/assets/{_sessionKey-C9o7YfxA.js → _sessionKey-DzsJfprr.js} +3 -3
  49. package/dist/server/assets/{_tanstack-start-manifest_v-BMCAWon2.js → _tanstack-start-manifest_v-C5HBDfQB.js} +1 -1
  50. package/dist/server/assets/{index-Bw-bA_2M.js → index-BFHEmXpN.js} +2 -1
  51. package/dist/server/assets/{router-DCjikH21.js → router-BZPatFG9.js} +245 -62
  52. package/dist/server/assets/{search-dialog-BnwiXpdA.js → search-dialog-DQRkARXw.js} +3 -2
  53. package/dist/server/assets/{settings-dialog-ClKFnZ1x.js → settings-dialog-Bc1ta26X.js} +3 -2
  54. package/dist/server/server.js +195 -38
  55. package/package.json +4 -1
  56. package/dist/client/assets/_sessionKey-CQE0brGK.js +0 -23
  57. package/dist/client/assets/agents-CMTFd_sG.js +0 -2
  58. package/dist/client/assets/agents-screen-BNQGEqcW.js +0 -1
  59. package/dist/client/assets/bots-B6oGzCxP.js +0 -2
  60. package/dist/client/assets/csharp-K5feNrxe.js +0 -1
  61. package/dist/client/assets/event-DD8Cz4O9.js +0 -1
  62. package/dist/client/assets/file-explorer-screen-CxwemBES.js +0 -1
  63. package/dist/client/assets/files-DyBJVXBu.js +0 -2
  64. package/dist/client/assets/go-Dn2_MT6a.js +0 -1
  65. package/dist/client/assets/keyboard-shortcuts-dialog-BZwd-iyV.js +0 -1
  66. package/dist/client/assets/main-CgwdHc9W.js +0 -210
  67. package/dist/client/assets/memory-l756yiNq.js +0 -2
  68. package/dist/client/assets/memory-screen-BQtVRuzE.js +0 -1
  69. package/dist/client/assets/menu-BsS6CDf_.js +0 -1
  70. package/dist/client/assets/php-CDn_0X-4.js +0 -1
  71. package/dist/client/assets/popupStateMapping-D0ZbJR_o.js +0 -1
  72. package/dist/client/assets/proxy-CYZeDXoy.js +0 -9
  73. package/dist/client/assets/ruby-FDmvQDUv.js +0 -1
  74. package/dist/client/assets/search-dialog-DW91SK30.js +0 -1
  75. package/dist/client/assets/session-export-dialog-CliO9Ob-.js +0 -1
  76. package/dist/client/assets/settings-dialog-C1u52aju.js +0 -1
  77. package/dist/client/assets/skills-8T_avaVb.js +0 -2
  78. package/dist/client/assets/styles-DvaLh0o1.css +0 -1
  79. package/dist/client/assets/switch-DbgQPO6i.js +0 -1
  80. package/dist/client/assets/tabs-BsAvZnlD.js +0 -1
  81. package/dist/client/assets/tooltip-DLmutB5C.js +0 -1
  82. package/dist/client/assets/use-file-explorer-state-Cg_yDYJl.js +0 -12
@@ -8,10 +8,11 @@ import path, { join, resolve, relative, extname } from "node:path";
8
8
  import WebSocket from "ws";
9
9
  import { json } from "@tanstack/router-core/ssr/client";
10
10
  import { PassThrough, Readable } from "node:stream";
11
- import { execSync } from "node:child_process";
11
+ import { execFile, execSync } from "node:child_process";
12
+ import { promisify } from "node:util";
12
13
  import { readFile, mkdir, writeFile, rename, stat, readdir, rm, realpath, lstat } from "node:fs/promises";
13
14
  import { posix } from "path";
14
- const appCss = "/assets/styles-DvaLh0o1.css";
15
+ const appCss = "/assets/styles-CXV5jZiD.css";
15
16
  const swRegisterScript = `
16
17
  (() => {
17
18
  // Skip PWA service worker inside Capacitor native shell — they conflict
@@ -369,11 +370,11 @@ const $$splitComponentImporter$2 = () => import("./agents-CmQ4vvXm.js");
369
370
  const Route$t = createFileRoute("/agents")({
370
371
  component: lazyRouteComponent($$splitComponentImporter$2, "component")
371
372
  });
372
- const $$splitComponentImporter$1 = () => import("./index-Bw-bA_2M.js");
373
+ const $$splitComponentImporter$1 = () => import("./index-BFHEmXpN.js");
373
374
  const Route$s = createFileRoute("/")({
374
375
  component: lazyRouteComponent($$splitComponentImporter$1, "component")
375
376
  });
376
- const $$splitComponentImporter = () => import("./_sessionKey-C9o7YfxA.js").then((n) => n.$);
377
+ const $$splitComponentImporter = () => import("./_sessionKey-DzsJfprr.js").then((n) => n.$);
377
378
  const Route$r = createFileRoute("/chat/$sessionKey")({
378
379
  component: lazyRouteComponent($$splitComponentImporter, "component")
379
380
  });
@@ -409,6 +410,46 @@ function ensureDir(filePath) {
409
410
  function resolveDeviceIdentityPath() {
410
411
  return path.join(os.homedir(), ".opencami", "identity", "device.json");
411
412
  }
413
+ function resolveDeviceTokensPath() {
414
+ return path.join(os.homedir(), ".opencami", "identity", "device-tokens.json");
415
+ }
416
+ function hashGatewayUrl(url) {
417
+ return crypto.createHash("sha256").update(url.trim()).digest("hex");
418
+ }
419
+ function loadDeviceToken(deviceId, gatewayUrl) {
420
+ const filePath = resolveDeviceTokensPath();
421
+ try {
422
+ if (!fs.existsSync(filePath)) return "";
423
+ const raw = fs.readFileSync(filePath, "utf8");
424
+ const parsed = JSON.parse(raw);
425
+ const key = `${deviceId}:${hashGatewayUrl(gatewayUrl)}`;
426
+ const token = parsed[key];
427
+ return typeof token === "string" ? token : "";
428
+ } catch {
429
+ return "";
430
+ }
431
+ }
432
+ function storeDeviceToken(deviceId, gatewayUrl, token) {
433
+ if (!token) return;
434
+ const filePath = resolveDeviceTokensPath();
435
+ const key = `${deviceId}:${hashGatewayUrl(gatewayUrl)}`;
436
+ try {
437
+ let parsed = {};
438
+ if (fs.existsSync(filePath)) {
439
+ const raw = fs.readFileSync(filePath, "utf8");
440
+ parsed = JSON.parse(raw);
441
+ }
442
+ parsed[key] = token;
443
+ ensureDir(filePath);
444
+ fs.writeFileSync(filePath, `${JSON.stringify(parsed, null, 2)}
445
+ `, { mode: 384 });
446
+ try {
447
+ fs.chmodSync(filePath, 384);
448
+ } catch {
449
+ }
450
+ } catch {
451
+ }
452
+ }
412
453
  function loadOrCreateDeviceIdentity(filePath = resolveDeviceIdentityPath()) {
413
454
  try {
414
455
  if (fs.existsSync(filePath)) {
@@ -482,17 +523,18 @@ function loadOrCreateInstanceId() {
482
523
  }
483
524
  return v;
484
525
  }
485
- function buildConnectParams(token, password, nonce) {
526
+ function buildConnectParams(url, token, password, nonce) {
486
527
  const clientId = "openclaw-control-ui";
487
528
  const clientMode = "webchat";
488
529
  const role = "operator";
489
- const scopes = ["operator.read", "operator.write"];
530
+ const scopes = ["operator.admin", "operator.approvals", "operator.pairing"];
490
531
  if (!nonce) {
491
532
  throw new Error(
492
533
  "OpenClaw did not send connect.challenge nonce in time. If you are connecting cross-origin, ensure your origin is allowed (gateway.controlUi.allowedOrigins)."
493
534
  );
494
535
  }
495
536
  const identity = loadOrCreateDeviceIdentity();
537
+ const storedToken = loadDeviceToken(identity.deviceId, url);
496
538
  const signedAtMs = Date.now();
497
539
  const payload = buildDeviceAuthPayload({
498
540
  deviceId: identity.deviceId,
@@ -519,7 +561,8 @@ function buildConnectParams(token, password, nonce) {
519
561
  caps: [],
520
562
  auth: {
521
563
  token: token || void 0,
522
- password: password || void 0
564
+ password: password || void 0,
565
+ deviceToken: storedToken || void 0
523
566
  },
524
567
  role: "operator",
525
568
  scopes,
@@ -534,6 +577,14 @@ function buildConnectParams(token, password, nonce) {
534
577
  locale: process.env.LANG || "en"
535
578
  };
536
579
  }
580
+ class GatewayResponseError extends Error {
581
+ code;
582
+ constructor(message, code) {
583
+ super(message);
584
+ this.name = "GatewayResponseError";
585
+ this.code = code;
586
+ }
587
+ }
537
588
  class PersistentGatewayConnection {
538
589
  ws = null;
539
590
  connected = false;
@@ -543,6 +594,8 @@ class PersistentGatewayConnection {
543
594
  reconnectDelay = 1e3;
544
595
  maxReconnectDelay = 3e4;
545
596
  destroyed = false;
597
+ _devicePending = false;
598
+ _deviceId = "";
546
599
  // Event listeners keyed by sessionKey — each sessionKey can have multiple listeners
547
600
  sessionListeners = /* @__PURE__ */ new Map();
548
601
  // Listeners that receive ALL events (for debugging or global subscriptions)
@@ -552,6 +605,12 @@ class PersistentGatewayConnection {
552
605
  get isConnected() {
553
606
  return this.connected && this.ws?.readyState === WebSocket.OPEN;
554
607
  }
608
+ getDeviceStatus() {
609
+ return {
610
+ deviceId: this._deviceId,
611
+ isPending: this._devicePending
612
+ };
613
+ }
555
614
  /** Ensure the persistent connection is up and authenticated. */
556
615
  async ensureConnected() {
557
616
  if (this.isConnected) return;
@@ -566,6 +625,13 @@ class PersistentGatewayConnection {
566
625
  async _connect() {
567
626
  if (this.destroyed) throw new Error("Connection destroyed");
568
627
  const { url, token, password } = getGatewayConfig();
628
+ if (this.ws) {
629
+ try {
630
+ this.ws.close();
631
+ } catch {
632
+ }
633
+ this.ws = null;
634
+ }
569
635
  const origin = process.env.OPENCAMI_ORIGIN?.trim();
570
636
  const ws = origin ? new WebSocket(url, { headers: { Origin: origin } }) : new WebSocket(url);
571
637
  this.ws = ws;
@@ -618,7 +684,8 @@ class PersistentGatewayConnection {
618
684
  const connectId = randomUUID();
619
685
  const shouldFallback = process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "1" || process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "true";
620
686
  try {
621
- const connectParams = buildConnectParams(token, password, nonce);
687
+ const connectParams = buildConnectParams(url, token, password, nonce);
688
+ this._deviceId = connectParams.device?.id ?? "";
622
689
  ws.send(
623
690
  JSON.stringify({
624
691
  type: "req",
@@ -628,13 +695,23 @@ class PersistentGatewayConnection {
628
695
  })
629
696
  );
630
697
  const hello = await this._waitForRes(connectId, 1e4);
698
+ if (hello?.auth?.deviceToken) {
699
+ const identity = loadOrCreateDeviceIdentity();
700
+ storeDeviceToken(identity.deviceId, url, hello.auth.deviceToken);
701
+ }
631
702
  const grantedScopes = hello?.auth?.scopes;
632
703
  if (Array.isArray(grantedScopes) && !grantedScopes.includes("operator.read")) {
633
704
  throw new Error(
634
705
  `Gateway connected but missing required scope: operator.read (granted: ${grantedScopes.join(", ")})`
635
706
  );
636
707
  }
708
+ this._devicePending = false;
637
709
  } catch (err) {
710
+ const code = err instanceof GatewayResponseError ? err.code : "";
711
+ const message = err instanceof Error ? err.message : String(err);
712
+ if (code === "device_pending" || message.toLowerCase().includes("pending")) {
713
+ this._devicePending = true;
714
+ }
638
715
  if (!shouldFallback) throw err;
639
716
  console.warn(
640
717
  "[gateway-ws] Device auth connect failed; retrying without device identity (fallback enabled):",
@@ -658,7 +735,7 @@ class PersistentGatewayConnection {
658
735
  password: password || void 0
659
736
  },
660
737
  role: "operator",
661
- scopes: ["operator.read", "operator.write"],
738
+ scopes: ["operator.admin", "operator.approvals", "operator.pairing"],
662
739
  userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
663
740
  locale: process.env.LANG || "en"
664
741
  };
@@ -671,6 +748,7 @@ class PersistentGatewayConnection {
671
748
  })
672
749
  );
673
750
  await this._waitForRes(fallbackId, 1e4);
751
+ this._devicePending = false;
674
752
  }
675
753
  this.connected = true;
676
754
  this.reconnectDelay = 1e3;
@@ -688,7 +766,7 @@ class PersistentGatewayConnection {
688
766
  if (parsed.ok) {
689
767
  pending.resolve(parsed.payload);
690
768
  } else {
691
- pending.reject(new Error(parsed.error?.message ?? "gateway error"));
769
+ pending.reject(new GatewayResponseError(parsed.error?.message ?? "gateway error", parsed.error?.code));
692
770
  }
693
771
  }
694
772
  return;
@@ -733,9 +811,9 @@ class PersistentGatewayConnection {
733
811
  }
734
812
  _extractSessionKey(event) {
735
813
  const payload = event.payload;
736
- if (typeof payload?.sessionKey === "string") return payload.sessionKey;
737
- if (typeof payload?.session === "string") return payload.session;
738
- if (payload?.data && typeof payload.data?.sessionKey === "string") {
814
+ if (typeof payload.sessionKey === "string") return payload.sessionKey;
815
+ if (typeof payload.session === "string") return payload.session;
816
+ if (payload.data && typeof payload.data?.sessionKey === "string") {
739
817
  return payload.data.sessionKey;
740
818
  }
741
819
  return null;
@@ -836,14 +914,12 @@ class PersistentGatewayConnection {
836
914
  this.globalListeners.clear();
837
915
  }
838
916
  }
839
- let _instance = null;
840
- const _g = globalThis;
917
+ const _proc = process;
841
918
  function getPersistentConnection() {
842
- if (!_g.__opencamiGatewayInstance) {
843
- _g.__opencamiGatewayInstance = new PersistentGatewayConnection();
919
+ if (!_proc.__opencamiGatewayInstance) {
920
+ _proc.__opencamiGatewayInstance = new PersistentGatewayConnection();
844
921
  }
845
- _instance = _g.__opencamiGatewayInstance;
846
- return _instance;
922
+ return _proc.__opencamiGatewayInstance;
847
923
  }
848
924
  async function gatewayRpc(method, params) {
849
925
  const conn = getPersistentConnection();
@@ -853,6 +929,10 @@ function subscribeGatewayEvents(sessionKey, listener) {
853
929
  const conn = getPersistentConnection();
854
930
  return conn.subscribe(sessionKey, listener);
855
931
  }
932
+ function getDeviceStatus() {
933
+ const conn = getPersistentConnection();
934
+ return conn.getDeviceStatus();
935
+ }
856
936
  async function gatewayConnectCheck() {
857
937
  const conn = getPersistentConnection();
858
938
  await conn.ensureConnected();
@@ -1317,9 +1397,34 @@ const RECOMMENDED_SKILLS = [
1317
1397
  "clawd-docs-v2",
1318
1398
  "therapy-mode"
1319
1399
  ];
1320
- function runCmd(cmd) {
1400
+ const execFileAsync = promisify(execFile);
1401
+ const ALLOWED_SORTS = /* @__PURE__ */ new Set(["trending", "downloads", "installs", "newest"]);
1402
+ const SLUG_PATTERN = /^[a-zA-Z0-9/_-]+$/;
1403
+ function parsePositiveInt(input, fallback, max) {
1404
+ if (!input) return fallback;
1405
+ const parsed = Number.parseInt(input, 10);
1406
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
1407
+ return Math.min(parsed, max);
1408
+ }
1409
+ function parseSort(input) {
1410
+ if (!input) return "trending";
1411
+ const normalized = input.trim().toLowerCase();
1412
+ return ALLOWED_SORTS.has(normalized) ? normalized : "trending";
1413
+ }
1414
+ function parseSlug(input) {
1415
+ if (typeof input !== "string") return null;
1416
+ const slug = input.trim();
1417
+ if (!slug || !SLUG_PATTERN.test(slug)) return null;
1418
+ return slug;
1419
+ }
1420
+ async function runCmd(args) {
1321
1421
  try {
1322
- return execSync(cmd, { encoding: "utf-8", timeout: 3e4 }).trim();
1422
+ const { stdout } = await execFileAsync("clawhub", args, {
1423
+ encoding: "utf-8",
1424
+ timeout: 3e4,
1425
+ maxBuffer: 1024 * 1024 * 8
1426
+ });
1427
+ return stdout.trim();
1323
1428
  } catch (err) {
1324
1429
  const msg = err instanceof Error ? err.message : String(err);
1325
1430
  throw new Error(`Command failed: ${msg}`);
@@ -1366,15 +1471,13 @@ const Route$n = createFileRoute("/api/skills")({
1366
1471
  const url = new URL(request.url);
1367
1472
  const action = url.searchParams.get("action") || "installed";
1368
1473
  if (action === "installed") {
1369
- const output = runCmd("clawhub list");
1474
+ const output = await runCmd(["list"]);
1370
1475
  return json({ ok: true, skills: parseInstalledSkills(output) });
1371
1476
  }
1372
1477
  if (action === "explore") {
1373
- const sort = url.searchParams.get("sort") || "trending";
1374
- const limit = url.searchParams.get("limit") || "25";
1375
- const safeSort = sort.replace(/[^a-z-]/gi, "");
1376
- const safeLimit = String(parseInt(limit, 10) || 25);
1377
- const raw = runCmd(`clawhub explore --json --limit ${safeLimit} --sort ${safeSort}`);
1478
+ const sort = parseSort(url.searchParams.get("sort"));
1479
+ const limit = parsePositiveInt(url.searchParams.get("limit"), 25, 200);
1480
+ const raw = await runCmd(["explore", "--json", "--limit", String(limit), "--sort", sort]);
1378
1481
  const output = raw.substring(raw.indexOf("{"));
1379
1482
  try {
1380
1483
  const data = JSON.parse(output);
@@ -1385,11 +1488,9 @@ const Route$n = createFileRoute("/api/skills")({
1385
1488
  }
1386
1489
  if (action === "search") {
1387
1490
  const q = url.searchParams.get("q") || "";
1388
- const limit = url.searchParams.get("limit") || "10";
1491
+ const limit = parsePositiveInt(url.searchParams.get("limit"), 10, 200);
1389
1492
  if (!q.trim()) return json({ ok: true, skills: [] });
1390
- const safeLimit = String(parseInt(limit, 10) || 10);
1391
- const safeQ = q.replace(/"/g, '\\"');
1392
- const raw = runCmd(`clawhub search "${safeQ}" --limit ${safeLimit}`);
1493
+ const raw = await runCmd(["search", q, "--limit", String(limit)]);
1393
1494
  const output = raw.replace(/^- Searching\n?/, "");
1394
1495
  return json({ ok: true, skills: parseSearchResults(output) });
1395
1496
  }
@@ -1403,7 +1504,7 @@ const Route$n = createFileRoute("/api/skills")({
1403
1504
  for (const sort of ["downloads", "installs", "newest", "trending"]) {
1404
1505
  if (found.size >= slugSet.size) break;
1405
1506
  try {
1406
- const raw = runCmd(`clawhub explore --json --limit 200 --sort ${sort}`);
1507
+ const raw = await runCmd(["explore", "--json", "--limit", "200", "--sort", sort]);
1407
1508
  const output = raw.substring(raw.indexOf("{"));
1408
1509
  const data = JSON.parse(output);
1409
1510
  const items = Array.isArray(data) ? data : data.items || [];
@@ -1431,15 +1532,14 @@ const Route$n = createFileRoute("/api/skills")({
1431
1532
  try {
1432
1533
  const body = await request.json().catch(() => ({}));
1433
1534
  const action = typeof body.action === "string" ? body.action : "";
1434
- const slug = typeof body.slug === "string" ? body.slug.trim() : "";
1435
- if (!slug) return json({ ok: false, error: "slug is required" }, { status: 400 });
1436
- const safeSlug = slug.replace(/[^a-zA-Z0-9/_-]/g, "");
1535
+ const slug = parseSlug(body.slug);
1536
+ if (!slug) return json({ ok: false, error: "Valid slug is required" }, { status: 400 });
1437
1537
  if (action === "install") {
1438
- const output = runCmd(`clawhub install ${safeSlug} --no-input`);
1538
+ const output = await runCmd(["install", slug, "--no-input"]);
1439
1539
  return json({ ok: true, output });
1440
1540
  }
1441
1541
  if (action === "update") {
1442
- const output = runCmd(`clawhub update ${safeSlug} --no-input`);
1542
+ const output = await runCmd(["update", slug, "--no-input"]);
1443
1543
  return json({ ok: true, output });
1444
1544
  }
1445
1545
  return json({ ok: false, error: "Unknown action" }, { status: 400 });
@@ -1702,12 +1802,38 @@ const Route$k = createFileRoute("/api/ping")({
1702
1802
  GET: async () => {
1703
1803
  try {
1704
1804
  await gatewayConnectCheck();
1705
- return json({ ok: true });
1805
+ const status = getDeviceStatus();
1806
+ if (status.isPending) {
1807
+ return json(
1808
+ {
1809
+ ok: false,
1810
+ error: "device pending approval",
1811
+ deviceId: status.deviceId,
1812
+ approveCommand: `openclaw devices approve ${status.deviceId}`
1813
+ },
1814
+ { status: 503 }
1815
+ );
1816
+ }
1817
+ return json({ ok: true, deviceId: status.deviceId, isPending: false });
1706
1818
  } catch (err) {
1819
+ const status = getDeviceStatus();
1820
+ if (status.isPending) {
1821
+ return json(
1822
+ {
1823
+ ok: false,
1824
+ error: "device pending approval",
1825
+ deviceId: status.deviceId,
1826
+ approveCommand: `openclaw devices approve ${status.deviceId}`
1827
+ },
1828
+ { status: 503 }
1829
+ );
1830
+ }
1707
1831
  return json(
1708
1832
  {
1709
1833
  ok: false,
1710
- error: err instanceof Error ? err.message : String(err)
1834
+ error: err instanceof Error ? err.message : String(err),
1835
+ deviceId: status.deviceId,
1836
+ isPending: status.isPending
1711
1837
  },
1712
1838
  { status: 503 }
1713
1839
  );
@@ -1956,27 +2082,73 @@ Rules:
1956
2082
  }
1957
2083
  return [];
1958
2084
  }
1959
- async function testApiKey(apiKey) {
2085
+ async function testApiKey(options) {
2086
+ const llmOptions = typeof options === "string" ? { apiKey: options } : options;
1960
2087
  try {
1961
2088
  await chatCompletion(
1962
2089
  [{ role: "user", content: "Hi" }],
1963
- { apiKey, maxTokens: 1, timeoutMs: 5e3 }
2090
+ { ...llmOptions, maxTokens: 1, timeoutMs: 5e3 }
1964
2091
  );
1965
2092
  return true;
1966
2093
  } catch {
1967
2094
  return false;
1968
2095
  }
1969
2096
  }
2097
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
2098
+ const PRESET_BASE_URL_ORIGINS = /* @__PURE__ */ new Set([
2099
+ "https://api.openai.com",
2100
+ "https://openrouter.ai",
2101
+ "https://api.kilo.ai",
2102
+ "http://localhost:11434",
2103
+ "http://127.0.0.1:11434"
2104
+ ]);
2105
+ function getOrigin(rawBaseUrl) {
2106
+ try {
2107
+ return new URL(rawBaseUrl).origin;
2108
+ } catch {
2109
+ return null;
2110
+ }
2111
+ }
2112
+ function isAllowedClientBaseUrl(rawBaseUrl) {
2113
+ const parsed = new URL(rawBaseUrl);
2114
+ if (!["http:", "https:"].includes(parsed.protocol)) return false;
2115
+ if (parsed.username || parsed.password) return false;
2116
+ const hostname = parsed.hostname.toLowerCase();
2117
+ const isLocalHost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
2118
+ if (!isLocalHost && parsed.protocol !== "https:") return false;
2119
+ const origin = parsed.origin;
2120
+ if (PRESET_BASE_URL_ORIGINS.has(origin)) return true;
2121
+ const envBaseUrl = process.env.LLM_BASE_URL?.trim();
2122
+ const envOrigin = envBaseUrl ? getOrigin(envBaseUrl) : null;
2123
+ return Boolean(envOrigin && envOrigin === origin);
2124
+ }
2125
+ function detectProvider(rawBaseUrl) {
2126
+ const baseUrl = rawBaseUrl?.toLowerCase() || "";
2127
+ if (baseUrl.includes("openrouter.ai")) return "openrouter";
2128
+ if (baseUrl.includes("kilo.ai")) return "kilocode";
2129
+ return "openai";
2130
+ }
1970
2131
  function getLlmConfig(request) {
1971
2132
  const headerKey = request.headers.get("X-OpenAI-API-Key");
1972
2133
  const headerBaseUrl = request.headers.get("X-LLM-Base-URL")?.trim() || null;
1973
- const baseUrl = headerBaseUrl || process.env.LLM_BASE_URL?.trim() || null;
1974
- const isOpenRouter = baseUrl?.includes("openrouter.ai");
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();
2134
+ const envBaseUrl = process.env.LLM_BASE_URL?.trim() || null;
2135
+ if (headerBaseUrl) {
2136
+ const origin = getOrigin(headerBaseUrl);
2137
+ if (!origin || !isAllowedClientBaseUrl(headerBaseUrl)) {
2138
+ return {
2139
+ apiKey: null,
2140
+ baseUrl: null,
2141
+ model: null,
2142
+ error: "Disallowed X-LLM-Base-URL value"
2143
+ };
2144
+ }
2145
+ }
2146
+ const baseUrl = headerBaseUrl || envBaseUrl || DEFAULT_OPENAI_BASE_URL;
2147
+ const provider = detectProvider(baseUrl);
2148
+ const envKey = provider === "openrouter" ? process.env.OPENROUTER_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() : provider === "kilocode" ? process.env.KILOCODE_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() : process.env.OPENAI_API_KEY?.trim();
1977
2149
  const apiKey = headerKey?.trim() || envKey || null;
1978
2150
  const model = request.headers.get("X-LLM-Model")?.trim() || process.env.LLM_MODEL?.trim() || null;
1979
- return { apiKey, baseUrl, model };
2151
+ return { apiKey, baseUrl, model, error: null };
1980
2152
  }
1981
2153
  function generateHeuristicTitle(message) {
1982
2154
  let text = message.replace(/```[\s\S]*?```/g, " ");
@@ -2045,6 +2217,12 @@ const Route$g = createFileRoute("/api/llm-features")({
2045
2217
  });
2046
2218
  }
2047
2219
  const llmConfig = getLlmConfig(request);
2220
+ if (llmConfig.error) {
2221
+ return json({
2222
+ ok: false,
2223
+ error: llmConfig.error
2224
+ }, { status: 400 });
2225
+ }
2048
2226
  if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
2049
2227
  const title = generateHeuristicTitle(message);
2050
2228
  return json({
@@ -2085,6 +2263,12 @@ const Route$g = createFileRoute("/api/llm-features")({
2085
2263
  });
2086
2264
  }
2087
2265
  const llmConfig = getLlmConfig(request);
2266
+ if (llmConfig.error) {
2267
+ return json({
2268
+ ok: false,
2269
+ error: llmConfig.error
2270
+ }, { status: 400 });
2271
+ }
2088
2272
  if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
2089
2273
  return json({
2090
2274
  ok: true,
@@ -2115,6 +2299,12 @@ const Route$g = createFileRoute("/api/llm-features")({
2115
2299
  }
2116
2300
  case "test": {
2117
2301
  const llmConfig = getLlmConfig(request);
2302
+ if (llmConfig.error) {
2303
+ return json({
2304
+ ok: false,
2305
+ error: llmConfig.error
2306
+ }, { status: 400 });
2307
+ }
2118
2308
  if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
2119
2309
  return json({
2120
2310
  ok: false,
@@ -2122,7 +2312,11 @@ const Route$g = createFileRoute("/api/llm-features")({
2122
2312
  });
2123
2313
  }
2124
2314
  try {
2125
- const valid = await testApiKey(llmConfig.apiKey || "");
2315
+ const valid = await testApiKey({
2316
+ apiKey: llmConfig.apiKey || "",
2317
+ ...llmConfig.baseUrl ? { baseUrl: llmConfig.baseUrl } : {},
2318
+ ...llmConfig.model ? { model: llmConfig.model } : {}
2319
+ });
2126
2320
  return json({
2127
2321
  ok: true,
2128
2322
  valid
@@ -3296,10 +3490,8 @@ const Route$5 = createFileRoute("/api/files/info")({
3296
3490
  );
3297
3491
  }
3298
3492
  const path2 = validatePath(rawPath, "Path parameter");
3299
- const root = process.env.FILES_ROOT?.trim() ? resolve(process.env.FILES_ROOT) : process.env.HOME || "/home";
3300
- const absolutePath = resolve(root, path2.replace(/^\/+/, ""));
3301
- const stats = await stat(absolutePath);
3302
- return json({ size: stats.size });
3493
+ const fileInfo = await getFileInfo(path2);
3494
+ return json({ size: fileInfo.size });
3303
3495
  } catch (err) {
3304
3496
  const error = err;
3305
3497
  if (error.message.includes("invalid characters") || error.message.includes("traversal attempts")) {
@@ -3395,11 +3587,6 @@ function encodeFilename(filename) {
3395
3587
  const encodedFilename = encodeURIComponent(filename);
3396
3588
  return `filename*=UTF-8''${encodedFilename}`;
3397
3589
  }
3398
- function isStaticAsset(filename) {
3399
- const ext = filename.split(".").pop()?.toLowerCase() || "";
3400
- const staticExts = ["png", "jpg", "jpeg", "gif", "webp", "svg", "css", "js", "woff", "woff2", "ttf", "otf"];
3401
- return staticExts.includes(ext);
3402
- }
3403
3590
  const Route$4 = createFileRoute("/api/files/download")({
3404
3591
  server: {
3405
3592
  handlers: {
@@ -3427,13 +3614,9 @@ const Route$4 = createFileRoute("/api/files/download")({
3427
3614
  const headers = {
3428
3615
  "Content-Type": contentType,
3429
3616
  "Content-Disposition": contentDisposition,
3430
- "Content-Length": content.byteLength.toString()
3617
+ "Content-Length": content.byteLength.toString(),
3618
+ "Cache-Control": "private, no-store"
3431
3619
  };
3432
- if (isStaticAsset(fileInfo.name)) {
3433
- headers["Cache-Control"] = "public, max-age=31536000";
3434
- } else {
3435
- headers["Cache-Control"] = "no-cache";
3436
- }
3437
3620
  return new Response(content, { headers });
3438
3621
  } catch (err) {
3439
3622
  const error = err;
@@ -5,7 +5,7 @@ import { HugeiconsIcon } from "@hugeicons/react";
5
5
  import { Search01Icon, Cancel01Icon, Loading03Icon } from "@hugeicons/core-free-icons";
6
6
  import { D as DialogRoot, a as DialogContent } from "./use-file-explorer-state-s7CS50ho.js";
7
7
  import { useQueryClient } from "@tanstack/react-query";
8
- import { c as chatQueryKeys } from "./_sessionKey-C9o7YfxA.js";
8
+ import { c as chatQueryKeys } from "./_sessionKey-DzsJfprr.js";
9
9
  import { c as cn } from "./button-CwY2OHFj.js";
10
10
  import "@base-ui/react/dialog";
11
11
  import "zustand";
@@ -26,7 +26,7 @@ import "remark-gfm";
26
26
  import "./index-Dl2BOKP7.js";
27
27
  import "zustand/middleware";
28
28
  import "react-dom";
29
- import "./router-DCjikH21.js";
29
+ import "./router-BZPatFG9.js";
30
30
  import "node:crypto";
31
31
  import "node:fs";
32
32
  import "node:os";
@@ -35,6 +35,7 @@ import "ws";
35
35
  import "@tanstack/router-core/ssr/client";
36
36
  import "node:stream";
37
37
  import "node:child_process";
38
+ import "node:util";
38
39
  import "node:fs/promises";
39
40
  import "path";
40
41
  import "@base-ui/react/merge-props";
@@ -8,7 +8,7 @@ import { D as DialogRoot, a as DialogContent, b as DialogTitle, c as DialogDescr
8
8
  import { S as Switch } from "./switch-BbkUeVDV.js";
9
9
  import { T as Tabs, a as TabsList, b as TabsTab } from "./tabs-DDFZob0m.js";
10
10
  import { u as useChatSettings } from "./index-Dl2BOKP7.js";
11
- import { u as useLlmSettings, g as getLlmProviderDefaults } from "./_sessionKey-C9o7YfxA.js";
11
+ import { u as useLlmSettings, g as getLlmProviderDefaults } from "./_sessionKey-DzsJfprr.js";
12
12
  import "@base-ui/react/merge-props";
13
13
  import "@base-ui/react/use-render";
14
14
  import "class-variance-authority";
@@ -35,7 +35,7 @@ import "react-markdown";
35
35
  import "remark-breaks";
36
36
  import "remark-gfm";
37
37
  import "react-dom";
38
- import "./router-DCjikH21.js";
38
+ import "./router-BZPatFG9.js";
39
39
  import "node:crypto";
40
40
  import "node:fs";
41
41
  import "node:os";
@@ -44,6 +44,7 @@ import "ws";
44
44
  import "@tanstack/router-core/ssr/client";
45
45
  import "node:stream";
46
46
  import "node:child_process";
47
+ import "node:util";
47
48
  import "node:fs/promises";
48
49
  import "path";
49
50
  function getInitialEnabled() {