opencami 1.8.3 → 1.8.5

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-CH8wIyED.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-NcNCVGTL.js} +2 -1
  51. package/dist/server/assets/{router-DCjikH21.js → router-Brzpnz55.js} +234 -56
  52. package/dist/server/assets/{search-dialog-BnwiXpdA.js → search-dialog-BNhjVvKc.js} +3 -2
  53. package/dist/server/assets/{settings-dialog-ClKFnZ1x.js → settings-dialog-CWcmfDiV.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-NcNCVGTL.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-CH8wIyED.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;
@@ -618,7 +677,8 @@ class PersistentGatewayConnection {
618
677
  const connectId = randomUUID();
619
678
  const shouldFallback = process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "1" || process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "true";
620
679
  try {
621
- const connectParams = buildConnectParams(token, password, nonce);
680
+ const connectParams = buildConnectParams(url, token, password, nonce);
681
+ this._deviceId = connectParams.device?.id ?? "";
622
682
  ws.send(
623
683
  JSON.stringify({
624
684
  type: "req",
@@ -628,13 +688,23 @@ class PersistentGatewayConnection {
628
688
  })
629
689
  );
630
690
  const hello = await this._waitForRes(connectId, 1e4);
691
+ if (hello?.auth?.deviceToken) {
692
+ const identity = loadOrCreateDeviceIdentity();
693
+ storeDeviceToken(identity.deviceId, url, hello.auth.deviceToken);
694
+ }
631
695
  const grantedScopes = hello?.auth?.scopes;
632
696
  if (Array.isArray(grantedScopes) && !grantedScopes.includes("operator.read")) {
633
697
  throw new Error(
634
698
  `Gateway connected but missing required scope: operator.read (granted: ${grantedScopes.join(", ")})`
635
699
  );
636
700
  }
701
+ this._devicePending = false;
637
702
  } catch (err) {
703
+ const code = err instanceof GatewayResponseError ? err.code : "";
704
+ const message = err instanceof Error ? err.message : String(err);
705
+ if (code === "device_pending" || message.toLowerCase().includes("pending")) {
706
+ this._devicePending = true;
707
+ }
638
708
  if (!shouldFallback) throw err;
639
709
  console.warn(
640
710
  "[gateway-ws] Device auth connect failed; retrying without device identity (fallback enabled):",
@@ -658,7 +728,7 @@ class PersistentGatewayConnection {
658
728
  password: password || void 0
659
729
  },
660
730
  role: "operator",
661
- scopes: ["operator.read", "operator.write"],
731
+ scopes: ["operator.admin", "operator.approvals", "operator.pairing"],
662
732
  userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
663
733
  locale: process.env.LANG || "en"
664
734
  };
@@ -671,6 +741,7 @@ class PersistentGatewayConnection {
671
741
  })
672
742
  );
673
743
  await this._waitForRes(fallbackId, 1e4);
744
+ this._devicePending = false;
674
745
  }
675
746
  this.connected = true;
676
747
  this.reconnectDelay = 1e3;
@@ -688,7 +759,7 @@ class PersistentGatewayConnection {
688
759
  if (parsed.ok) {
689
760
  pending.resolve(parsed.payload);
690
761
  } else {
691
- pending.reject(new Error(parsed.error?.message ?? "gateway error"));
762
+ pending.reject(new GatewayResponseError(parsed.error?.message ?? "gateway error", parsed.error?.code));
692
763
  }
693
764
  }
694
765
  return;
@@ -733,9 +804,9 @@ class PersistentGatewayConnection {
733
804
  }
734
805
  _extractSessionKey(event) {
735
806
  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") {
807
+ if (typeof payload.sessionKey === "string") return payload.sessionKey;
808
+ if (typeof payload.session === "string") return payload.session;
809
+ if (payload.data && typeof payload.data?.sessionKey === "string") {
739
810
  return payload.data.sessionKey;
740
811
  }
741
812
  return null;
@@ -853,6 +924,10 @@ function subscribeGatewayEvents(sessionKey, listener) {
853
924
  const conn = getPersistentConnection();
854
925
  return conn.subscribe(sessionKey, listener);
855
926
  }
927
+ function getDeviceStatus() {
928
+ const conn = getPersistentConnection();
929
+ return conn.getDeviceStatus();
930
+ }
856
931
  async function gatewayConnectCheck() {
857
932
  const conn = getPersistentConnection();
858
933
  await conn.ensureConnected();
@@ -1317,9 +1392,34 @@ const RECOMMENDED_SKILLS = [
1317
1392
  "clawd-docs-v2",
1318
1393
  "therapy-mode"
1319
1394
  ];
1320
- function runCmd(cmd) {
1395
+ const execFileAsync = promisify(execFile);
1396
+ const ALLOWED_SORTS = /* @__PURE__ */ new Set(["trending", "downloads", "installs", "newest"]);
1397
+ const SLUG_PATTERN = /^[a-zA-Z0-9/_-]+$/;
1398
+ function parsePositiveInt(input, fallback, max) {
1399
+ if (!input) return fallback;
1400
+ const parsed = Number.parseInt(input, 10);
1401
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
1402
+ return Math.min(parsed, max);
1403
+ }
1404
+ function parseSort(input) {
1405
+ if (!input) return "trending";
1406
+ const normalized = input.trim().toLowerCase();
1407
+ return ALLOWED_SORTS.has(normalized) ? normalized : "trending";
1408
+ }
1409
+ function parseSlug(input) {
1410
+ if (typeof input !== "string") return null;
1411
+ const slug = input.trim();
1412
+ if (!slug || !SLUG_PATTERN.test(slug)) return null;
1413
+ return slug;
1414
+ }
1415
+ async function runCmd(args) {
1321
1416
  try {
1322
- return execSync(cmd, { encoding: "utf-8", timeout: 3e4 }).trim();
1417
+ const { stdout } = await execFileAsync("clawhub", args, {
1418
+ encoding: "utf-8",
1419
+ timeout: 3e4,
1420
+ maxBuffer: 1024 * 1024 * 8
1421
+ });
1422
+ return stdout.trim();
1323
1423
  } catch (err) {
1324
1424
  const msg = err instanceof Error ? err.message : String(err);
1325
1425
  throw new Error(`Command failed: ${msg}`);
@@ -1366,15 +1466,13 @@ const Route$n = createFileRoute("/api/skills")({
1366
1466
  const url = new URL(request.url);
1367
1467
  const action = url.searchParams.get("action") || "installed";
1368
1468
  if (action === "installed") {
1369
- const output = runCmd("clawhub list");
1469
+ const output = await runCmd(["list"]);
1370
1470
  return json({ ok: true, skills: parseInstalledSkills(output) });
1371
1471
  }
1372
1472
  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}`);
1473
+ const sort = parseSort(url.searchParams.get("sort"));
1474
+ const limit = parsePositiveInt(url.searchParams.get("limit"), 25, 200);
1475
+ const raw = await runCmd(["explore", "--json", "--limit", String(limit), "--sort", sort]);
1378
1476
  const output = raw.substring(raw.indexOf("{"));
1379
1477
  try {
1380
1478
  const data = JSON.parse(output);
@@ -1385,11 +1483,9 @@ const Route$n = createFileRoute("/api/skills")({
1385
1483
  }
1386
1484
  if (action === "search") {
1387
1485
  const q = url.searchParams.get("q") || "";
1388
- const limit = url.searchParams.get("limit") || "10";
1486
+ const limit = parsePositiveInt(url.searchParams.get("limit"), 10, 200);
1389
1487
  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}`);
1488
+ const raw = await runCmd(["search", q, "--limit", String(limit)]);
1393
1489
  const output = raw.replace(/^- Searching\n?/, "");
1394
1490
  return json({ ok: true, skills: parseSearchResults(output) });
1395
1491
  }
@@ -1403,7 +1499,7 @@ const Route$n = createFileRoute("/api/skills")({
1403
1499
  for (const sort of ["downloads", "installs", "newest", "trending"]) {
1404
1500
  if (found.size >= slugSet.size) break;
1405
1501
  try {
1406
- const raw = runCmd(`clawhub explore --json --limit 200 --sort ${sort}`);
1502
+ const raw = await runCmd(["explore", "--json", "--limit", "200", "--sort", sort]);
1407
1503
  const output = raw.substring(raw.indexOf("{"));
1408
1504
  const data = JSON.parse(output);
1409
1505
  const items = Array.isArray(data) ? data : data.items || [];
@@ -1431,15 +1527,14 @@ const Route$n = createFileRoute("/api/skills")({
1431
1527
  try {
1432
1528
  const body = await request.json().catch(() => ({}));
1433
1529
  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, "");
1530
+ const slug = parseSlug(body.slug);
1531
+ if (!slug) return json({ ok: false, error: "Valid slug is required" }, { status: 400 });
1437
1532
  if (action === "install") {
1438
- const output = runCmd(`clawhub install ${safeSlug} --no-input`);
1533
+ const output = await runCmd(["install", slug, "--no-input"]);
1439
1534
  return json({ ok: true, output });
1440
1535
  }
1441
1536
  if (action === "update") {
1442
- const output = runCmd(`clawhub update ${safeSlug} --no-input`);
1537
+ const output = await runCmd(["update", slug, "--no-input"]);
1443
1538
  return json({ ok: true, output });
1444
1539
  }
1445
1540
  return json({ ok: false, error: "Unknown action" }, { status: 400 });
@@ -1702,12 +1797,38 @@ const Route$k = createFileRoute("/api/ping")({
1702
1797
  GET: async () => {
1703
1798
  try {
1704
1799
  await gatewayConnectCheck();
1705
- return json({ ok: true });
1800
+ const status = getDeviceStatus();
1801
+ if (status.isPending) {
1802
+ return json(
1803
+ {
1804
+ ok: false,
1805
+ error: "device pending approval",
1806
+ deviceId: status.deviceId,
1807
+ approveCommand: `openclaw devices approve ${status.deviceId}`
1808
+ },
1809
+ { status: 503 }
1810
+ );
1811
+ }
1812
+ return json({ ok: true, deviceId: status.deviceId, isPending: false });
1706
1813
  } catch (err) {
1814
+ const status = getDeviceStatus();
1815
+ if (status.isPending) {
1816
+ return json(
1817
+ {
1818
+ ok: false,
1819
+ error: "device pending approval",
1820
+ deviceId: status.deviceId,
1821
+ approveCommand: `openclaw devices approve ${status.deviceId}`
1822
+ },
1823
+ { status: 503 }
1824
+ );
1825
+ }
1707
1826
  return json(
1708
1827
  {
1709
1828
  ok: false,
1710
- error: err instanceof Error ? err.message : String(err)
1829
+ error: err instanceof Error ? err.message : String(err),
1830
+ deviceId: status.deviceId,
1831
+ isPending: status.isPending
1711
1832
  },
1712
1833
  { status: 503 }
1713
1834
  );
@@ -1956,27 +2077,73 @@ Rules:
1956
2077
  }
1957
2078
  return [];
1958
2079
  }
1959
- async function testApiKey(apiKey) {
2080
+ async function testApiKey(options) {
2081
+ const llmOptions = typeof options === "string" ? { apiKey: options } : options;
1960
2082
  try {
1961
2083
  await chatCompletion(
1962
2084
  [{ role: "user", content: "Hi" }],
1963
- { apiKey, maxTokens: 1, timeoutMs: 5e3 }
2085
+ { ...llmOptions, maxTokens: 1, timeoutMs: 5e3 }
1964
2086
  );
1965
2087
  return true;
1966
2088
  } catch {
1967
2089
  return false;
1968
2090
  }
1969
2091
  }
2092
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
2093
+ const PRESET_BASE_URL_ORIGINS = /* @__PURE__ */ new Set([
2094
+ "https://api.openai.com",
2095
+ "https://openrouter.ai",
2096
+ "https://api.kilo.ai",
2097
+ "http://localhost:11434",
2098
+ "http://127.0.0.1:11434"
2099
+ ]);
2100
+ function getOrigin(rawBaseUrl) {
2101
+ try {
2102
+ return new URL(rawBaseUrl).origin;
2103
+ } catch {
2104
+ return null;
2105
+ }
2106
+ }
2107
+ function isAllowedClientBaseUrl(rawBaseUrl) {
2108
+ const parsed = new URL(rawBaseUrl);
2109
+ if (!["http:", "https:"].includes(parsed.protocol)) return false;
2110
+ if (parsed.username || parsed.password) return false;
2111
+ const hostname = parsed.hostname.toLowerCase();
2112
+ const isLocalHost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
2113
+ if (!isLocalHost && parsed.protocol !== "https:") return false;
2114
+ const origin = parsed.origin;
2115
+ if (PRESET_BASE_URL_ORIGINS.has(origin)) return true;
2116
+ const envBaseUrl = process.env.LLM_BASE_URL?.trim();
2117
+ const envOrigin = envBaseUrl ? getOrigin(envBaseUrl) : null;
2118
+ return Boolean(envOrigin && envOrigin === origin);
2119
+ }
2120
+ function detectProvider(rawBaseUrl) {
2121
+ const baseUrl = rawBaseUrl?.toLowerCase() || "";
2122
+ if (baseUrl.includes("openrouter.ai")) return "openrouter";
2123
+ if (baseUrl.includes("kilo.ai")) return "kilocode";
2124
+ return "openai";
2125
+ }
1970
2126
  function getLlmConfig(request) {
1971
2127
  const headerKey = request.headers.get("X-OpenAI-API-Key");
1972
2128
  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();
2129
+ const envBaseUrl = process.env.LLM_BASE_URL?.trim() || null;
2130
+ if (headerBaseUrl) {
2131
+ const origin = getOrigin(headerBaseUrl);
2132
+ if (!origin || !isAllowedClientBaseUrl(headerBaseUrl)) {
2133
+ return {
2134
+ apiKey: null,
2135
+ baseUrl: null,
2136
+ model: null,
2137
+ error: "Disallowed X-LLM-Base-URL value"
2138
+ };
2139
+ }
2140
+ }
2141
+ const baseUrl = headerBaseUrl || envBaseUrl || DEFAULT_OPENAI_BASE_URL;
2142
+ const provider = detectProvider(baseUrl);
2143
+ 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
2144
  const apiKey = headerKey?.trim() || envKey || null;
1978
2145
  const model = request.headers.get("X-LLM-Model")?.trim() || process.env.LLM_MODEL?.trim() || null;
1979
- return { apiKey, baseUrl, model };
2146
+ return { apiKey, baseUrl, model, error: null };
1980
2147
  }
1981
2148
  function generateHeuristicTitle(message) {
1982
2149
  let text = message.replace(/```[\s\S]*?```/g, " ");
@@ -2045,6 +2212,12 @@ const Route$g = createFileRoute("/api/llm-features")({
2045
2212
  });
2046
2213
  }
2047
2214
  const llmConfig = getLlmConfig(request);
2215
+ if (llmConfig.error) {
2216
+ return json({
2217
+ ok: false,
2218
+ error: llmConfig.error
2219
+ }, { status: 400 });
2220
+ }
2048
2221
  if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
2049
2222
  const title = generateHeuristicTitle(message);
2050
2223
  return json({
@@ -2085,6 +2258,12 @@ const Route$g = createFileRoute("/api/llm-features")({
2085
2258
  });
2086
2259
  }
2087
2260
  const llmConfig = getLlmConfig(request);
2261
+ if (llmConfig.error) {
2262
+ return json({
2263
+ ok: false,
2264
+ error: llmConfig.error
2265
+ }, { status: 400 });
2266
+ }
2088
2267
  if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
2089
2268
  return json({
2090
2269
  ok: true,
@@ -2115,6 +2294,12 @@ const Route$g = createFileRoute("/api/llm-features")({
2115
2294
  }
2116
2295
  case "test": {
2117
2296
  const llmConfig = getLlmConfig(request);
2297
+ if (llmConfig.error) {
2298
+ return json({
2299
+ ok: false,
2300
+ error: llmConfig.error
2301
+ }, { status: 400 });
2302
+ }
2118
2303
  if (!llmConfig.apiKey && !llmConfig.baseUrl?.includes("localhost")) {
2119
2304
  return json({
2120
2305
  ok: false,
@@ -2122,7 +2307,11 @@ const Route$g = createFileRoute("/api/llm-features")({
2122
2307
  });
2123
2308
  }
2124
2309
  try {
2125
- const valid = await testApiKey(llmConfig.apiKey || "");
2310
+ const valid = await testApiKey({
2311
+ apiKey: llmConfig.apiKey || "",
2312
+ ...llmConfig.baseUrl ? { baseUrl: llmConfig.baseUrl } : {},
2313
+ ...llmConfig.model ? { model: llmConfig.model } : {}
2314
+ });
2126
2315
  return json({
2127
2316
  ok: true,
2128
2317
  valid
@@ -3296,10 +3485,8 @@ const Route$5 = createFileRoute("/api/files/info")({
3296
3485
  );
3297
3486
  }
3298
3487
  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 });
3488
+ const fileInfo = await getFileInfo(path2);
3489
+ return json({ size: fileInfo.size });
3303
3490
  } catch (err) {
3304
3491
  const error = err;
3305
3492
  if (error.message.includes("invalid characters") || error.message.includes("traversal attempts")) {
@@ -3395,11 +3582,6 @@ function encodeFilename(filename) {
3395
3582
  const encodedFilename = encodeURIComponent(filename);
3396
3583
  return `filename*=UTF-8''${encodedFilename}`;
3397
3584
  }
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
3585
  const Route$4 = createFileRoute("/api/files/download")({
3404
3586
  server: {
3405
3587
  handlers: {
@@ -3427,13 +3609,9 @@ const Route$4 = createFileRoute("/api/files/download")({
3427
3609
  const headers = {
3428
3610
  "Content-Type": contentType,
3429
3611
  "Content-Disposition": contentDisposition,
3430
- "Content-Length": content.byteLength.toString()
3612
+ "Content-Length": content.byteLength.toString(),
3613
+ "Cache-Control": "private, no-store"
3431
3614
  };
3432
- if (isStaticAsset(fileInfo.name)) {
3433
- headers["Cache-Control"] = "public, max-age=31536000";
3434
- } else {
3435
- headers["Cache-Control"] = "no-cache";
3436
- }
3437
3615
  return new Response(content, { headers });
3438
3616
  } catch (err) {
3439
3617
  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-CH8wIyED.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-Brzpnz55.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-CH8wIyED.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-Brzpnz55.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() {