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.
- 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-CH8wIyED.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-NcNCVGTL.js} +2 -1
- package/dist/server/assets/{router-DCjikH21.js → router-Brzpnz55.js} +234 -56
- package/dist/server/assets/{search-dialog-BnwiXpdA.js → search-dialog-BNhjVvKc.js} +3 -2
- package/dist/server/assets/{settings-dialog-ClKFnZ1x.js → settings-dialog-CWcmfDiV.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-NcNCVGTL.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-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.
|
|
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.
|
|
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
|
|
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
|
|
737
|
-
if (typeof payload
|
|
738
|
-
if (payload
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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")
|
|
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}`);
|
|
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")
|
|
1486
|
+
const limit = parsePositiveInt(url.searchParams.get("limit"), 10, 200);
|
|
1389
1487
|
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}`);
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
{
|
|
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
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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(
|
|
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
|
|
3300
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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() {
|