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.
- package/README.md +63 -96
- package/dist/client/assets/{CSPContext-DeJH85nm.js → CSPContext-6t3O1emU.js} +1 -1
- package/dist/client/assets/{DirectionContext-CxhRpXkm.js → DirectionContext-C6goXEY_.js} +1 -1
- package/dist/client/assets/_sessionKey-B5Viv43f.js +23 -0
- package/dist/client/assets/agents-BmE6QOwl.js +2 -0
- package/dist/client/assets/agents-screen-pHUzJxX5.js +1 -0
- package/dist/client/assets/bots-BeOkwrXr.js +2 -0
- package/dist/client/assets/{bots-screen-Be3cfGgq.js → bots-screen-B79bAYvf.js} +1 -1
- package/dist/client/assets/{button-D9Plv7hu.js → button-CK8tKQ-Z.js} +1 -1
- package/dist/client/assets/{composite-B2KCZKKL.js → composite-feK0c-xF.js} +1 -1
- package/dist/client/assets/{connect-DuJfnyNK.js → connect-02tmQV_v.js} +1 -1
- package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/client/assets/{dashboard-00GpXm5V.js → dashboard-DQ0zDQKd.js} +1 -1
- package/dist/client/assets/event-BsD1rqGT.js +1 -0
- package/dist/client/assets/file-explorer-screen-Ds7LeJTd.js +1 -0
- package/dist/client/assets/files-e40B1zFy.js +2 -0
- package/dist/client/assets/go-CxLEBnE3.js +1 -0
- package/dist/client/assets/{index-Yo5UhdZV.js → index-lK3yGoTI.js} +1 -1
- package/dist/client/assets/{index-DtGzE-ea.js → index-rljDU_1M.js} +2 -2
- package/dist/client/assets/keyboard-shortcuts-dialog-Bb_GOr9L.js +1 -0
- package/dist/client/assets/main-Dq6jpr6-.js +210 -0
- package/dist/client/assets/{markdown-DtWnt4NA.js → markdown-C7_Aipwd.js} +37 -37
- package/dist/client/assets/memory-C7UG-1le.js +2 -0
- package/dist/client/assets/memory-screen-CUFBWsq5.js +1 -0
- package/dist/client/assets/menu-n6L--M9R.js +1 -0
- package/dist/client/assets/{opencami-logo-Bmge6-FB.js → opencami-logo-zuSBm5Br.js} +1 -1
- package/dist/client/assets/php-Dhbhpdrm.js +1 -0
- package/dist/client/assets/proxy-BU8Bw1Vt.js +9 -0
- package/dist/client/assets/{react-DODKNyyU.js → react-BLyCEWpN.js} +1 -1
- package/dist/client/assets/ruby-NiQIzKut.js +1 -0
- package/dist/client/assets/search-dialog-yB4w5ajo.js +1 -0
- package/dist/client/assets/session-export-dialog-qbZgd2Zo.js +1 -0
- package/dist/client/assets/settings-dialog-CHJbvpgk.js +1 -0
- package/dist/client/assets/skills-DoKPPhNY.js +2 -0
- package/dist/client/assets/{skills-panel-DSiH-DLs.js → skills-panel-BH27r3nC.js} +1 -1
- package/dist/client/assets/styles-CXV5jZiD.css +1 -0
- package/dist/client/assets/{swift-Dg5xB15N.js → swift-D82vCrfD.js} +1 -1
- package/dist/client/assets/switch-BD3a0LRm.js +1 -0
- package/dist/client/assets/tabs-DI1e-kzz.js +1 -0
- package/dist/client/assets/tooltip-BbH3QWvK.js +1 -0
- package/dist/client/assets/use-file-explorer-state-DBfLeAyz.js +12 -0
- package/dist/client/assets/{useBaseUiId-KQTzRPLp.js → useBaseUiId-MgM4ouhx.js} +1 -1
- package/dist/client/assets/{useCompositeItem-BPY2_hF_.js → useCompositeItem-OhltNFdZ.js} +1 -1
- package/dist/client/assets/{useControlled-B5pEEz2V.js → useControlled-BQxTgsOd.js} +1 -1
- package/dist/client/assets/{useMutation-BsQD6FKe.js → useMutation-12DyB3Ox.js} +1 -1
- package/dist/client/assets/useOnFirstRender-7qoaK5sI.js +1 -0
- package/dist/client/assets/{useQuery-CmAJuY2W.js → useQuery-Ctiljcrr.js} +1 -1
- package/dist/server/assets/{_sessionKey-C9o7YfxA.js → _sessionKey-DzsJfprr.js} +3 -3
- package/dist/server/assets/{_tanstack-start-manifest_v-BMCAWon2.js → _tanstack-start-manifest_v-C5HBDfQB.js} +1 -1
- package/dist/server/assets/{index-Bw-bA_2M.js → index-BFHEmXpN.js} +2 -1
- package/dist/server/assets/{router-DCjikH21.js → router-BZPatFG9.js} +245 -62
- package/dist/server/assets/{search-dialog-BnwiXpdA.js → search-dialog-DQRkARXw.js} +3 -2
- package/dist/server/assets/{settings-dialog-ClKFnZ1x.js → settings-dialog-Bc1ta26X.js} +3 -2
- package/dist/server/server.js +195 -38
- package/package.json +4 -1
- package/dist/client/assets/_sessionKey-CQE0brGK.js +0 -23
- package/dist/client/assets/agents-CMTFd_sG.js +0 -2
- package/dist/client/assets/agents-screen-BNQGEqcW.js +0 -1
- package/dist/client/assets/bots-B6oGzCxP.js +0 -2
- package/dist/client/assets/csharp-K5feNrxe.js +0 -1
- package/dist/client/assets/event-DD8Cz4O9.js +0 -1
- package/dist/client/assets/file-explorer-screen-CxwemBES.js +0 -1
- package/dist/client/assets/files-DyBJVXBu.js +0 -2
- package/dist/client/assets/go-Dn2_MT6a.js +0 -1
- package/dist/client/assets/keyboard-shortcuts-dialog-BZwd-iyV.js +0 -1
- package/dist/client/assets/main-CgwdHc9W.js +0 -210
- package/dist/client/assets/memory-l756yiNq.js +0 -2
- package/dist/client/assets/memory-screen-BQtVRuzE.js +0 -1
- package/dist/client/assets/menu-BsS6CDf_.js +0 -1
- package/dist/client/assets/php-CDn_0X-4.js +0 -1
- package/dist/client/assets/popupStateMapping-D0ZbJR_o.js +0 -1
- package/dist/client/assets/proxy-CYZeDXoy.js +0 -9
- package/dist/client/assets/ruby-FDmvQDUv.js +0 -1
- package/dist/client/assets/search-dialog-DW91SK30.js +0 -1
- package/dist/client/assets/session-export-dialog-CliO9Ob-.js +0 -1
- package/dist/client/assets/settings-dialog-C1u52aju.js +0 -1
- package/dist/client/assets/skills-8T_avaVb.js +0 -2
- package/dist/client/assets/styles-DvaLh0o1.css +0 -1
- package/dist/client/assets/switch-DbgQPO6i.js +0 -1
- package/dist/client/assets/tabs-BsAvZnlD.js +0 -1
- package/dist/client/assets/tooltip-DLmutB5C.js +0 -1
- 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-
|
|
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-
|
|
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-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
737
|
-
if (typeof payload
|
|
738
|
-
if (payload
|
|
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
|
-
|
|
840
|
-
const _g = globalThis;
|
|
917
|
+
const _proc = process;
|
|
841
918
|
function getPersistentConnection() {
|
|
842
|
-
if (!
|
|
843
|
-
|
|
919
|
+
if (!_proc.__opencamiGatewayInstance) {
|
|
920
|
+
_proc.__opencamiGatewayInstance = new PersistentGatewayConnection();
|
|
844
921
|
}
|
|
845
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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")
|
|
1374
|
-
const limit = url.searchParams.get("limit")
|
|
1375
|
-
const
|
|
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")
|
|
1491
|
+
const limit = parsePositiveInt(url.searchParams.get("limit"), 10, 200);
|
|
1389
1492
|
if (!q.trim()) return json({ ok: true, skills: [] });
|
|
1390
|
-
const
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
{
|
|
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
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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(
|
|
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
|
|
3300
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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() {
|