ape-claw 0.1.6 → 0.1.8
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 +157 -8
- package/docs/operator/01-quickstart.md +81 -72
- package/docs/operator/03-cli-reference.md +46 -5
- package/package.json +1 -1
- package/src/cli.mjs +551 -15
- package/src/lib/openclaw-paths.mjs +65 -0
- package/src/server/index.mjs +8 -2
- package/src/server/middleware/auth.mjs +10 -0
- package/src/server/routes/forge-agent.mjs +438 -58
- package/src/server/routes/openclaw-env.mjs +206 -0
- package/src/server/routes/skills.mjs +80 -3
- package/ui/forge/css/forge.css +153 -2
- package/ui/forge/index.html +40 -6
- package/ui/forge/js/forge-attachments.js +616 -377
- package/ui/forge/js/forge-chat.js +201 -90
- package/ui/forge/js/forge-data.js +305 -11
- package/ui/forge/js/forge-scene.js +178 -18
- package/ui/index.html +3 -3
package/src/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { readJson, writeJson, randomId } from "./lib/io.mjs";
|
|
8
8
|
import {
|
|
9
9
|
ROOT,
|
|
@@ -34,6 +34,11 @@ import { privateKeyToAccount } from "viem/accounts";
|
|
|
34
34
|
import { SkillNFT_ABI, SkillRegistry_ABI, IntentRegistry_ABI, ReceiptRegistry_ABI, PodVault_ABI, AgentAccount_ABI } from "./lib/v2-onchain-abi.mjs";
|
|
35
35
|
import { computeSkillcardContentHash, computeSkillVersionHash, readSkillcardJson, stableJsonStringify } from "./lib/v2-skillcard.mjs";
|
|
36
36
|
import { initPodWorkspace } from "./lib/pod-init.mjs";
|
|
37
|
+
import {
|
|
38
|
+
openClawControlUiIndexCandidates,
|
|
39
|
+
openClawSkillsDirCandidates,
|
|
40
|
+
openClawWorkspaceSkillsDirCandidates,
|
|
41
|
+
} from "./lib/openclaw-paths.mjs";
|
|
37
42
|
|
|
38
43
|
function parseArgs(argv) {
|
|
39
44
|
const out = { _: [] };
|
|
@@ -152,7 +157,10 @@ function installApeClawSkill(args) {
|
|
|
152
157
|
if (skillsRoot.includes("\0")) throw new Error("Invalid skills-dir path");
|
|
153
158
|
}
|
|
154
159
|
else if (scope === "local") skillsRoot = path.join(process.cwd(), ".cursor", "skills");
|
|
155
|
-
else
|
|
160
|
+
else {
|
|
161
|
+
const candidates = openClawSkillsDirCandidates();
|
|
162
|
+
skillsRoot = candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
163
|
+
}
|
|
156
164
|
|
|
157
165
|
const targetSkillDir = path.join(skillsRoot, "ape-claw");
|
|
158
166
|
const targetSkillPath = path.join(targetSkillDir, "SKILL.md");
|
|
@@ -250,12 +258,13 @@ function syncSkillToOpenClaw(cardObj, slug, skillsRoot) {
|
|
|
250
258
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
251
259
|
fs.writeFileSync(path.join(skillDir, "SKILL.md"), content, "utf8");
|
|
252
260
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
for (const workspaceRoot of openClawWorkspaceSkillsDirCandidates()) {
|
|
262
|
+
const openclawWorkspaceSkills = path.join(workspaceRoot, s);
|
|
263
|
+
try {
|
|
264
|
+
fs.mkdirSync(openclawWorkspaceSkills, { recursive: true });
|
|
265
|
+
fs.writeFileSync(path.join(openclawWorkspaceSkills, "SKILL.md"), content, "utf8");
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
259
268
|
|
|
260
269
|
return { slug: s, skillDir };
|
|
261
270
|
}
|
|
@@ -519,8 +528,8 @@ function resolveHumanizerDependencySlug(packageRoot) {
|
|
|
519
528
|
}
|
|
520
529
|
|
|
521
530
|
function installOpenClawSkillCard(cardObj, fallbackSlug = "") {
|
|
522
|
-
const
|
|
523
|
-
const skillsRoot =
|
|
531
|
+
const candidates = openClawSkillsDirCandidates();
|
|
532
|
+
const skillsRoot = candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
524
533
|
return syncSkillToOpenClaw(cardObj, fallbackSlug, skillsRoot);
|
|
525
534
|
}
|
|
526
535
|
|
|
@@ -647,6 +656,376 @@ function promptUser(question) {
|
|
|
647
656
|
});
|
|
648
657
|
}
|
|
649
658
|
|
|
659
|
+
const FORGE_DASHBOARD_URL = "http://localhost:8787/forge";
|
|
660
|
+
// Use 127.0.0.1 explicitly — on macOS, 'localhost' resolves to ::1 (IPv6)
|
|
661
|
+
// but the server binds to 0.0.0.0 (IPv4), causing the health check to fail.
|
|
662
|
+
const FORGE_HEALTH_URL = "http://127.0.0.1:8787/api/health";
|
|
663
|
+
const FORGE_OVERWRITE_MARKER = "Redirecting to ApeClaw Forge";
|
|
664
|
+
|
|
665
|
+
function hasOpenClawCli() {
|
|
666
|
+
const check = spawnSync("openclaw", ["--version"], { encoding: "utf8" });
|
|
667
|
+
return !check.error && typeof check.status === "number" && check.status === 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function dashboardUpgradeStorePath() {
|
|
671
|
+
return path.join(os.homedir(), ".ape-claw", "dashboard-upgrade.json");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function loadDashboardUpgradeStore() {
|
|
675
|
+
const p = dashboardUpgradeStorePath();
|
|
676
|
+
const data = readJson(p, {});
|
|
677
|
+
if (!data || typeof data !== "object") return {};
|
|
678
|
+
return data;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function writeDashboardUpgradeStore(data) {
|
|
682
|
+
const p = dashboardUpgradeStorePath();
|
|
683
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
684
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function getOpenClawVersion() {
|
|
688
|
+
const out = spawnSync("openclaw", ["--version"], { encoding: "utf8" });
|
|
689
|
+
if (out.error || out.status !== 0) return "";
|
|
690
|
+
return String(out.stdout || out.stderr || "").trim();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function detectOpenClawBinaryPath() {
|
|
694
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
695
|
+
const out = spawnSync(cmd, ["openclaw"], { encoding: "utf8" });
|
|
696
|
+
if (out.error || out.status !== 0) return "";
|
|
697
|
+
const line = String(out.stdout || "").split(/\r?\n/).map((x) => x.trim()).find(Boolean);
|
|
698
|
+
if (!line) return "";
|
|
699
|
+
try { return fs.realpathSync(line); } catch { return line; }
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function findOpenClawControlUiIndex() {
|
|
703
|
+
const seen = new Set();
|
|
704
|
+
const candidates = [];
|
|
705
|
+
const bin = detectOpenClawBinaryPath();
|
|
706
|
+
if (bin) {
|
|
707
|
+
const binDir = path.dirname(bin);
|
|
708
|
+
candidates.push(
|
|
709
|
+
path.resolve(binDir, "..", "lib", "node_modules", "openclaw", "dist", "control-ui", "index.html"),
|
|
710
|
+
path.resolve(binDir, "..", "node_modules", "openclaw", "dist", "control-ui", "index.html"),
|
|
711
|
+
path.resolve(binDir, "..", "dist", "control-ui", "index.html"),
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
candidates.push(...openClawControlUiIndexCandidates());
|
|
715
|
+
|
|
716
|
+
for (const p of candidates) {
|
|
717
|
+
if (!p || seen.has(p)) continue;
|
|
718
|
+
seen.add(p);
|
|
719
|
+
try {
|
|
720
|
+
if (fs.existsSync(p) && fs.statSync(p).isFile()) return p;
|
|
721
|
+
} catch {}
|
|
722
|
+
}
|
|
723
|
+
return "";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function buildForgeRedirectHtml(targetUrl) {
|
|
727
|
+
const target = String(targetUrl || FORGE_DASHBOARD_URL);
|
|
728
|
+
const safeTarget = JSON.stringify(target);
|
|
729
|
+
return `<!doctype html>
|
|
730
|
+
<html lang="en">
|
|
731
|
+
<head>
|
|
732
|
+
<meta charset="utf-8">
|
|
733
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
734
|
+
<meta http-equiv="refresh" content="0;url=${target}">
|
|
735
|
+
<title>Redirecting to ApeClaw Forge</title>
|
|
736
|
+
</head>
|
|
737
|
+
<body>
|
|
738
|
+
<p>Redirecting to ApeClaw Forge dashboard...</p>
|
|
739
|
+
<p><a id="forgeLink" href="${target}">Open Forge</a></p>
|
|
740
|
+
<script>
|
|
741
|
+
(function () {
|
|
742
|
+
var target = ${safeTarget};
|
|
743
|
+
try { window.location.replace(target); } catch {}
|
|
744
|
+
setTimeout(function () {
|
|
745
|
+
var a = document.getElementById("forgeLink");
|
|
746
|
+
if (a) a.href = target;
|
|
747
|
+
}, 0);
|
|
748
|
+
})();
|
|
749
|
+
</script>
|
|
750
|
+
</body>
|
|
751
|
+
</html>
|
|
752
|
+
`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function isForgeOverwriteActive(indexPath) {
|
|
756
|
+
if (!indexPath) return false;
|
|
757
|
+
try {
|
|
758
|
+
const raw = fs.readFileSync(indexPath, "utf8");
|
|
759
|
+
return raw.includes(FORGE_OVERWRITE_MARKER);
|
|
760
|
+
} catch {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function attemptOpenClawDashboardHardOverwrite(targetUrl = FORGE_DASHBOARD_URL) {
|
|
766
|
+
const indexPath = findOpenClawControlUiIndex();
|
|
767
|
+
if (!indexPath) {
|
|
768
|
+
return { attempted: true, applied: false, reason: "control_ui_index_not_found", path: null };
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const current = fs.readFileSync(indexPath, "utf8");
|
|
772
|
+
if (current.includes(FORGE_OVERWRITE_MARKER)) {
|
|
773
|
+
return { attempted: true, applied: true, reason: "already_overridden", path: indexPath };
|
|
774
|
+
}
|
|
775
|
+
const backupPath = `${indexPath}.apeclaw.bak`;
|
|
776
|
+
if (!fs.existsSync(backupPath)) {
|
|
777
|
+
fs.copyFileSync(indexPath, backupPath);
|
|
778
|
+
}
|
|
779
|
+
fs.writeFileSync(indexPath, buildForgeRedirectHtml(targetUrl), "utf8");
|
|
780
|
+
return { attempted: true, applied: true, reason: "overwritten", path: indexPath, backupPath };
|
|
781
|
+
} catch (err) {
|
|
782
|
+
return { attempted: true, applied: false, reason: err?.message || "overwrite_failed", path: indexPath };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function restoreOpenClawDashboardFromBackup() {
|
|
787
|
+
const indexPath = findOpenClawControlUiIndex();
|
|
788
|
+
if (!indexPath) {
|
|
789
|
+
return { ok: false, restored: false, reason: "control_ui_index_not_found" };
|
|
790
|
+
}
|
|
791
|
+
const backupPath = `${indexPath}.apeclaw.bak`;
|
|
792
|
+
if (!fs.existsSync(backupPath)) {
|
|
793
|
+
return { ok: false, restored: false, reason: "backup_not_found", path: indexPath, backupPath };
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
fs.copyFileSync(backupPath, indexPath);
|
|
797
|
+
return { ok: true, restored: true, path: indexPath, backupPath };
|
|
798
|
+
} catch (err) {
|
|
799
|
+
return { ok: false, restored: false, reason: err?.message || "restore_failed", path: indexPath, backupPath };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function checkForgeServerReady(timeoutMs = 1200) {
|
|
804
|
+
const deadline = Date.now() + Math.max(200, timeoutMs);
|
|
805
|
+
while (Date.now() < deadline) {
|
|
806
|
+
try {
|
|
807
|
+
const res = await fetch(FORGE_HEALTH_URL, { signal: AbortSignal.timeout(900) });
|
|
808
|
+
if (res.ok) return true;
|
|
809
|
+
} catch {}
|
|
810
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function ensureForgeServerRunning(packageRoot) {
|
|
816
|
+
const serverEntry = path.join(packageRoot, "src", "server", "index.mjs");
|
|
817
|
+
const forgeUiEntry = path.join(packageRoot, "ui", "forge", "index.html");
|
|
818
|
+
if (!fs.existsSync(serverEntry) || !fs.existsSync(forgeUiEntry)) {
|
|
819
|
+
return {
|
|
820
|
+
running: false,
|
|
821
|
+
started: false,
|
|
822
|
+
reason: "package_files_missing",
|
|
823
|
+
missing: {
|
|
824
|
+
serverEntry: fs.existsSync(serverEntry) ? "" : serverEntry,
|
|
825
|
+
forgeUiEntry: fs.existsSync(forgeUiEntry) ? "" : forgeUiEntry,
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (await checkForgeServerReady(4000)) {
|
|
831
|
+
return { running: true, started: false };
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const child = spawn(process.execPath, [serverEntry], {
|
|
835
|
+
cwd: packageRoot,
|
|
836
|
+
detached: true,
|
|
837
|
+
stdio: "ignore",
|
|
838
|
+
});
|
|
839
|
+
child.unref();
|
|
840
|
+
} catch {}
|
|
841
|
+
const running = await checkForgeServerReady(14000);
|
|
842
|
+
return { running, started: true };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function openBrowserUrl(url) {
|
|
846
|
+
const target = String(url || "").trim();
|
|
847
|
+
if (!target) return false;
|
|
848
|
+
try {
|
|
849
|
+
if (process.platform === "darwin") {
|
|
850
|
+
const out = spawnSync("open", [target], { stdio: "ignore" });
|
|
851
|
+
return out.status === 0;
|
|
852
|
+
}
|
|
853
|
+
if (process.platform === "win32") {
|
|
854
|
+
const out = spawnSync("cmd", ["/c", "start", "", target], { stdio: "ignore" });
|
|
855
|
+
return out.status === 0;
|
|
856
|
+
}
|
|
857
|
+
const out = spawnSync("xdg-open", [target], { stdio: "ignore" });
|
|
858
|
+
return out.status === 0;
|
|
859
|
+
} catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function gatewayStatusLooksReady(out) {
|
|
865
|
+
const s = String(out || "").toLowerCase();
|
|
866
|
+
if (!s) return false;
|
|
867
|
+
return s.includes('"running":true') || s.includes('"ok":true') || s.includes("running");
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function ensureOpenClawGatewayForForge() {
|
|
871
|
+
if (!hasOpenClawCli()) return { ok: false, action: "missing_cli" };
|
|
872
|
+
|
|
873
|
+
const configPath = path.join(process.env.HOME || "", ".openclaw", "openclaw.json");
|
|
874
|
+
if (!fs.existsSync(configPath)) {
|
|
875
|
+
spawnSync("openclaw", [
|
|
876
|
+
"onboard", "--non-interactive", "--accept-risk",
|
|
877
|
+
"--auth-choice", "skip", "--install-daemon",
|
|
878
|
+
"--skip-channels", "--skip-skills", "--skip-ui", "--json",
|
|
879
|
+
], { encoding: "utf8", timeout: 30000 });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const status1 = spawnSync("openclaw", ["gateway", "status", "--json"], { encoding: "utf8", timeout: 12000 });
|
|
883
|
+
const status1Out = `${status1.stdout || ""}\n${status1.stderr || ""}`;
|
|
884
|
+
if (status1.status === 0 && gatewayStatusLooksReady(status1Out)) {
|
|
885
|
+
ensureDevicePairedIfNeeded(status1Out);
|
|
886
|
+
return { ok: true, action: "already_running" };
|
|
887
|
+
}
|
|
888
|
+
const start = spawnSync("openclaw", ["gateway", "start"], { encoding: "utf8", timeout: 18000 });
|
|
889
|
+
const status2 = spawnSync("openclaw", ["gateway", "status", "--json"], { encoding: "utf8", timeout: 12000 });
|
|
890
|
+
const status2Out = `${status2.stdout || ""}\n${status2.stderr || ""}`;
|
|
891
|
+
if (start.status === 0 && status2.status === 0 && gatewayStatusLooksReady(status2Out)) {
|
|
892
|
+
ensureDevicePairedIfNeeded(status2Out);
|
|
893
|
+
return { ok: true, action: "started" };
|
|
894
|
+
}
|
|
895
|
+
const install = spawnSync("openclaw", ["gateway", "install"], { encoding: "utf8", timeout: 25000 });
|
|
896
|
+
const start2 = spawnSync("openclaw", ["gateway", "start"], { encoding: "utf8", timeout: 18000 });
|
|
897
|
+
const status3 = spawnSync("openclaw", ["gateway", "status", "--json"], { encoding: "utf8", timeout: 12000 });
|
|
898
|
+
const status3Out = `${status3.stdout || ""}\n${status3.stderr || ""}`;
|
|
899
|
+
if (status3.status === 0 && gatewayStatusLooksReady(status3Out)) {
|
|
900
|
+
ensureDevicePairedIfNeeded(status3Out);
|
|
901
|
+
return { ok: true, action: "installed_and_started" };
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
ok: false,
|
|
905
|
+
action: "failed",
|
|
906
|
+
details: String(status3.stderr || status3.stdout || start2.stderr || start2.stdout || install.stderr || install.stdout || "").trim(),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function ensureDevicePairedIfNeeded(statusOutput) {
|
|
911
|
+
const out = String(statusOutput || "");
|
|
912
|
+
if (!out.includes("pairing required") && !out.includes("device token mismatch") && !out.includes("unauthorized")) return;
|
|
913
|
+
try {
|
|
914
|
+
const listResult = spawnSync("openclaw", ["devices", "list", "--json"], { encoding: "utf8", timeout: 12000 });
|
|
915
|
+
const listOut = String(listResult.stdout || "");
|
|
916
|
+
const listJson = JSON.parse(listOut);
|
|
917
|
+
const pending = listJson?.pending || [];
|
|
918
|
+
for (const req of pending) {
|
|
919
|
+
const reqId = req?.requestId || req?.id || "";
|
|
920
|
+
if (!reqId) continue;
|
|
921
|
+
spawnSync("openclaw", ["devices", "approve", reqId], { encoding: "utf8", timeout: 10000 });
|
|
922
|
+
}
|
|
923
|
+
} catch {}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function ensureModelMatchesApiKey() {
|
|
927
|
+
try {
|
|
928
|
+
const envPath = path.join(process.env.HOME || "", ".openclaw", ".env");
|
|
929
|
+
if (!fs.existsSync(envPath)) return;
|
|
930
|
+
const raw = fs.readFileSync(envPath, "utf8");
|
|
931
|
+
const hasOpenAI = /^OPENAI_API_KEY\s*=\s*sk-/m.test(raw);
|
|
932
|
+
const hasAnthropic = /^ANTHROPIC_API_KEY\s*=\s*sk-ant-/m.test(raw);
|
|
933
|
+
if (!hasOpenAI || hasAnthropic) return;
|
|
934
|
+
|
|
935
|
+
const configPath = path.join(process.env.HOME || "", ".openclaw", "openclaw.json");
|
|
936
|
+
if (!fs.existsSync(configPath)) return;
|
|
937
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
938
|
+
const currentModel = cfg?.agents?.defaults?.model?.primary || "";
|
|
939
|
+
if (currentModel && currentModel.startsWith("openai/")) return;
|
|
940
|
+
|
|
941
|
+
spawnSync("openclaw", ["config", "set", "agents.defaults.model.primary", "openai/gpt-4o"], {
|
|
942
|
+
encoding: "utf8", timeout: 10000,
|
|
943
|
+
});
|
|
944
|
+
} catch {}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async function runForgeDashboardUpgrade({ packageRoot, attemptHardOverwrite = true }) {
|
|
948
|
+
if (!hasOpenClawCli()) {
|
|
949
|
+
return {
|
|
950
|
+
enabled: true,
|
|
951
|
+
ok: false,
|
|
952
|
+
reason: "openclaw_missing",
|
|
953
|
+
url: FORGE_DASHBOARD_URL,
|
|
954
|
+
next: [
|
|
955
|
+
"Install OpenClaw first: curl -fsSL https://openclaw.ai/install.sh | bash",
|
|
956
|
+
"Run: openclaw onboard --install-daemon",
|
|
957
|
+
],
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const settings = loadDashboardUpgradeStore();
|
|
962
|
+
const openclawVersion = getOpenClawVersion();
|
|
963
|
+
const openclawBinPath = detectOpenClawBinaryPath();
|
|
964
|
+
const overwritePath = findOpenClawControlUiIndex();
|
|
965
|
+
const overwriteActive = isForgeOverwriteActive(overwritePath);
|
|
966
|
+
const gateway = ensureOpenClawGatewayForForge();
|
|
967
|
+
ensureModelMatchesApiKey();
|
|
968
|
+
|
|
969
|
+
const server = await ensureForgeServerRunning(packageRoot);
|
|
970
|
+
const optedIn = Boolean(settings?.openclawOverwriteOptIn);
|
|
971
|
+
const shouldAttemptOverwrite = Boolean(attemptHardOverwrite) || optedIn;
|
|
972
|
+
|
|
973
|
+
let hardOverwrite = { attempted: false, applied: false, reason: "skipped" };
|
|
974
|
+
if (server.running && shouldAttemptOverwrite) {
|
|
975
|
+
hardOverwrite = attemptOpenClawDashboardHardOverwrite(FORGE_DASHBOARD_URL);
|
|
976
|
+
} else if (!server.running && shouldAttemptOverwrite) {
|
|
977
|
+
hardOverwrite = { attempted: false, applied: false, reason: "forge_unhealthy_skip_overwrite" };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const now = new Date().toISOString();
|
|
981
|
+
const nextSettings = {
|
|
982
|
+
...settings,
|
|
983
|
+
openclawOverwriteOptIn: Boolean(settings?.openclawOverwriteOptIn || attemptHardOverwrite),
|
|
984
|
+
lastSeen: {
|
|
985
|
+
openclawVersion,
|
|
986
|
+
openclawBinPath,
|
|
987
|
+
controlUiPath: overwritePath || "",
|
|
988
|
+
overwriteActiveAfterRun: Boolean(isForgeOverwriteActive(overwritePath)),
|
|
989
|
+
checkedAt: now,
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
if (hardOverwrite.applied) {
|
|
993
|
+
nextSettings.lastApplied = {
|
|
994
|
+
openclawVersion,
|
|
995
|
+
openclawBinPath,
|
|
996
|
+
controlUiPath: hardOverwrite.path || overwritePath || "",
|
|
997
|
+
appliedAt: now,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
writeDashboardUpgradeStore(nextSettings);
|
|
1001
|
+
|
|
1002
|
+
let overwriteReappliedAfterUpdate = false;
|
|
1003
|
+
const prevApplied = settings?.lastApplied || null;
|
|
1004
|
+
if (hardOverwrite.applied && prevApplied) {
|
|
1005
|
+
overwriteReappliedAfterUpdate = (
|
|
1006
|
+
String(prevApplied.openclawVersion || "") !== String(openclawVersion || "") ||
|
|
1007
|
+
String(prevApplied.openclawBinPath || "") !== String(openclawBinPath || "") ||
|
|
1008
|
+
(!overwriteActive && isForgeOverwriteActive(overwritePath))
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const browserOpened = server.running ? openBrowserUrl(FORGE_DASHBOARD_URL) : false;
|
|
1013
|
+
return {
|
|
1014
|
+
enabled: true,
|
|
1015
|
+
ok: server.running,
|
|
1016
|
+
mode: hardOverwrite.applied ? "hard-overwrite-temporary" : "fallback",
|
|
1017
|
+
url: FORGE_DASHBOARD_URL,
|
|
1018
|
+
hardOverwrite,
|
|
1019
|
+
server,
|
|
1020
|
+
browserOpened,
|
|
1021
|
+
gateway,
|
|
1022
|
+
warning: hardOverwrite.applied
|
|
1023
|
+
? "OpenClaw overwrite is temporary and may be reset by OpenClaw updates. Use 'ape-claw dashboard' as the stable entrypoint."
|
|
1024
|
+
: "",
|
|
1025
|
+
overwriteReappliedAfterUpdate,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
650
1029
|
function resolveRemoteApiBase(args = {}) {
|
|
651
1030
|
const explicit = String(args.api || "").trim();
|
|
652
1031
|
const fromEnv = String(process.env.APE_CLAW_API_BASE || process.env.APE_CLAW_TELEMETRY_URL || "").trim();
|
|
@@ -698,6 +1077,72 @@ async function main() {
|
|
|
698
1077
|
const asJson = Boolean(args.json);
|
|
699
1078
|
_asJson = asJson;
|
|
700
1079
|
const command = `ape-claw ${args._.join(" ")}`.trim();
|
|
1080
|
+
const allowUnverifiedAgentForCommand = (
|
|
1081
|
+
group === "dashboard" ||
|
|
1082
|
+
group === "doctor" ||
|
|
1083
|
+
group === "quickstart" ||
|
|
1084
|
+
(group === "chain" && sub === "info") ||
|
|
1085
|
+
(group === "market" && (sub === "collections" || sub === "listings")) ||
|
|
1086
|
+
(group === "allowlist" && sub === "audit")
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
if (group === "dashboard") {
|
|
1090
|
+
if (sub === "restore-openclaw") {
|
|
1091
|
+
const restored = restoreOpenClawDashboardFromBackup();
|
|
1092
|
+
if (asJson) return print(restored, true);
|
|
1093
|
+
console.log();
|
|
1094
|
+
if (restored.ok) {
|
|
1095
|
+
console.log(`\x1b[32m OpenClaw dashboard restored from backup.\x1b[0m`);
|
|
1096
|
+
console.log(` \x1b[2mPath:\x1b[0m ${restored.path}`);
|
|
1097
|
+
} else {
|
|
1098
|
+
console.log(`\x1b[33m Could not restore OpenClaw dashboard: ${restored.reason}\x1b[0m`);
|
|
1099
|
+
if (restored.backupPath) console.log(` \x1b[2mExpected backup:\x1b[0m ${restored.backupPath}`);
|
|
1100
|
+
}
|
|
1101
|
+
console.log();
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1106
|
+
const packageRoot = path.resolve(here, "..");
|
|
1107
|
+
const upgrade = await runForgeDashboardUpgrade({ packageRoot, attemptHardOverwrite: false });
|
|
1108
|
+
if (asJson) return print(upgrade, true);
|
|
1109
|
+
if (!upgrade.ok && upgrade.reason === "openclaw_missing") {
|
|
1110
|
+
console.log();
|
|
1111
|
+
console.log(`\x1b[31m OpenClaw is required before using the Forge dashboard upgrade.\x1b[0m`);
|
|
1112
|
+
for (const step of (upgrade.next || [])) console.log(` \x1b[2m${step}\x1b[0m`);
|
|
1113
|
+
console.log();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
console.log();
|
|
1117
|
+
console.log(`\x1b[1m\x1b[33m 🦞 APE CLAW DASHBOARD\x1b[0m`);
|
|
1118
|
+
console.log(` \x1b[36mURL:\x1b[0m ${upgrade.url}`);
|
|
1119
|
+
if (upgrade.server?.started) {
|
|
1120
|
+
console.log(` \x1b[36mLocal server:\x1b[0m started`);
|
|
1121
|
+
} else if (upgrade.server?.running) {
|
|
1122
|
+
console.log(` \x1b[36mLocal server:\x1b[0m already running`);
|
|
1123
|
+
} else {
|
|
1124
|
+
if (upgrade.server?.reason === "package_files_missing") {
|
|
1125
|
+
console.log(` \x1b[33mLocal Forge files missing in this installation.\x1b[0m`);
|
|
1126
|
+
console.log(` \x1b[2mFix:\x1b[0m run \x1b[36mnpx ape-claw dashboard\x1b[0m (npm will fetch latest package),`);
|
|
1127
|
+
console.log(` \x1b[2mor reinstall:\x1b[0m \x1b[36mnpm i -g ape-claw@latest\x1b[0m`);
|
|
1128
|
+
console.log(` \x1b[2mOptional dev path:\x1b[0m clone GitHub repo and run \x1b[36mnpm run start:ui\x1b[0m`);
|
|
1129
|
+
} else {
|
|
1130
|
+
console.log(` \x1b[33mLocal server check failed.\x1b[0m Run: \x1b[36mnpx ape-claw dashboard\x1b[0m`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (upgrade.warning) {
|
|
1134
|
+
console.log(` \x1b[33m${upgrade.warning}\x1b[0m`);
|
|
1135
|
+
}
|
|
1136
|
+
if (upgrade.overwriteReappliedAfterUpdate) {
|
|
1137
|
+
console.log(` \x1b[36mDetected OpenClaw update/layout change; reapplied dashboard overwrite (opt-in).\x1b[0m`);
|
|
1138
|
+
}
|
|
1139
|
+
if (!upgrade.browserOpened) {
|
|
1140
|
+
if (upgrade.server?.running) console.log(` \x1b[2mCould not auto-open browser. Open manually: ${upgrade.url}\x1b[0m`);
|
|
1141
|
+
else console.log(` \x1b[2mSkipped browser open because Forge health is not ready.\x1b[0m`);
|
|
1142
|
+
}
|
|
1143
|
+
console.log();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
701
1146
|
|
|
702
1147
|
// Allow skill installation in any directory without local ape-claw config.
|
|
703
1148
|
if (group === "skill" && sub === "install") {
|
|
@@ -835,6 +1280,44 @@ async function main() {
|
|
|
835
1280
|
result.starterPack.skipped = true;
|
|
836
1281
|
}
|
|
837
1282
|
|
|
1283
|
+
// ── Forge dashboard upgrade (opt-in, safe portable default) ──
|
|
1284
|
+
const skipForgeUpgrade = Boolean(args["no-forge-upgrade"]);
|
|
1285
|
+
const forceForgeUpgrade = Boolean(args["forge-upgrade"]);
|
|
1286
|
+
let enableForgeUpgrade = false;
|
|
1287
|
+
if (skipForgeUpgrade) {
|
|
1288
|
+
enableForgeUpgrade = false;
|
|
1289
|
+
} else if (forceForgeUpgrade) {
|
|
1290
|
+
enableForgeUpgrade = true;
|
|
1291
|
+
} else if (asJson) {
|
|
1292
|
+
enableForgeUpgrade = false;
|
|
1293
|
+
} else {
|
|
1294
|
+
console.log();
|
|
1295
|
+
console.log(`\x1b[1m\x1b[33m FORGE DASHBOARD UPGRADE\x1b[0m`);
|
|
1296
|
+
console.log(`\x1b[2m Replace the OpenClaw control dashboard with ApeClaw Forge locally\x1b[0m`);
|
|
1297
|
+
console.log(`\x1b[2m (safe fallback is used automatically when hard overwrite is unsupported).\x1b[0m`);
|
|
1298
|
+
console.log();
|
|
1299
|
+
const answer = await promptUser(" Upgrade to Forge dashboard now? [Y/n] ");
|
|
1300
|
+
enableForgeUpgrade = answer === "" || answer === "y" || answer === "yes";
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (enableForgeUpgrade) {
|
|
1304
|
+
result.forgeUpgrade = await runForgeDashboardUpgrade({
|
|
1305
|
+
packageRoot: result.packageRoot,
|
|
1306
|
+
attemptHardOverwrite: true,
|
|
1307
|
+
});
|
|
1308
|
+
} else {
|
|
1309
|
+
const currentUpgradeSettings = loadDashboardUpgradeStore();
|
|
1310
|
+
writeDashboardUpgradeStore({
|
|
1311
|
+
...currentUpgradeSettings,
|
|
1312
|
+
openclawOverwriteOptIn: false,
|
|
1313
|
+
lastSeen: {
|
|
1314
|
+
...(currentUpgradeSettings.lastSeen || {}),
|
|
1315
|
+
checkedAt: new Date().toISOString(),
|
|
1316
|
+
},
|
|
1317
|
+
});
|
|
1318
|
+
result.forgeUpgrade = { enabled: false, reason: "user_skipped", url: FORGE_DASHBOARD_URL };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
838
1321
|
if (asJson) return print(result, true);
|
|
839
1322
|
|
|
840
1323
|
const W = 64;
|
|
@@ -903,6 +1386,52 @@ async function main() {
|
|
|
903
1386
|
}
|
|
904
1387
|
}
|
|
905
1388
|
|
|
1389
|
+
const forge = result.forgeUpgrade || { enabled: false };
|
|
1390
|
+
console.log();
|
|
1391
|
+
console.log(`\x1b[1m 🧭 FORGE CONTROL DASHBOARD\x1b[0m`);
|
|
1392
|
+
console.log(` ${thinLine}`);
|
|
1393
|
+
if (forge.enabled) {
|
|
1394
|
+
if (forge.ok) {
|
|
1395
|
+
if (forge.mode === "hard-overwrite-temporary") {
|
|
1396
|
+
console.log(` \x1b[32mOpenClaw dashboard route overwritten locally\x1b[0m`);
|
|
1397
|
+
if (forge.hardOverwrite?.path) {
|
|
1398
|
+
console.log(` \x1b[2mPath:\x1b[0m ${forge.hardOverwrite.path}`);
|
|
1399
|
+
}
|
|
1400
|
+
console.log(` \x1b[33mTemporary override:\x1b[0m OpenClaw updates may reset this route.`);
|
|
1401
|
+
console.log(` \x1b[2mStable entrypoint remains:\x1b[0m npx ape-claw dashboard`);
|
|
1402
|
+
} else {
|
|
1403
|
+
console.log(` \x1b[33mHard overwrite unavailable; fallback route is active\x1b[0m`);
|
|
1404
|
+
}
|
|
1405
|
+
if (forge.server?.started) {
|
|
1406
|
+
console.log(` \x1b[36mForge server:\x1b[0m started`);
|
|
1407
|
+
} else if (forge.server?.running) {
|
|
1408
|
+
console.log(` \x1b[36mForge server:\x1b[0m already running`);
|
|
1409
|
+
}
|
|
1410
|
+
console.log(` \x1b[36mForge URL:\x1b[0m ${forge.url || FORGE_DASHBOARD_URL}`);
|
|
1411
|
+
if (!forge.browserOpened) {
|
|
1412
|
+
console.log(` \x1b[2mBrowser did not auto-open. Open manually:\x1b[0m ${forge.url || FORGE_DASHBOARD_URL}`);
|
|
1413
|
+
}
|
|
1414
|
+
if (forge.overwriteReappliedAfterUpdate) {
|
|
1415
|
+
console.log(` \x1b[36mDetected OpenClaw update/layout change; reapplied overwrite (opt-in).\x1b[0m`);
|
|
1416
|
+
}
|
|
1417
|
+
if (forge.gateway?.ok && forge.gateway?.action && forge.gateway.action !== "already_running") {
|
|
1418
|
+
console.log(` \x1b[36mOpenClaw gateway:\x1b[0m ${forge.gateway.action.replace(/_/g, " ")}`);
|
|
1419
|
+
}
|
|
1420
|
+
} else if (forge.reason === "openclaw_missing") {
|
|
1421
|
+
console.log(` \x1b[31mOpenClaw not detected. Install OpenClaw first, then run:\x1b[0m`);
|
|
1422
|
+
console.log(` \x1b[32m npx ape-claw dashboard\x1b[0m`);
|
|
1423
|
+
} else if (forge.reason === "forge_unhealthy_skip_overwrite" || !forge.server?.running) {
|
|
1424
|
+
console.log(` \x1b[33mForge server is not healthy yet. OpenClaw overwrite skipped for safety.\x1b[0m`);
|
|
1425
|
+
console.log(` \x1b[2mRetry (no repo required):\x1b[0m npx ape-claw dashboard`);
|
|
1426
|
+
console.log(` \x1b[2mOptional dev path:\x1b[0m clone GitHub repo and run npm run start:ui`);
|
|
1427
|
+
} else {
|
|
1428
|
+
console.log(` \x1b[33mForge upgrade attempted but did not complete.\x1b[0m`);
|
|
1429
|
+
console.log(` \x1b[2mRun manually:\x1b[0m npx ape-claw dashboard`);
|
|
1430
|
+
}
|
|
1431
|
+
} else {
|
|
1432
|
+
console.log(` \x1b[2mSkipped. Upgrade later with:\x1b[0m npx ape-claw dashboard`);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
906
1435
|
console.log();
|
|
907
1436
|
console.log(`\x1b[1m 🤖 YOUR CLAWBOT CAN NOW:\x1b[0m`);
|
|
908
1437
|
console.log(` ${thinLine}`);
|
|
@@ -925,17 +1454,19 @@ async function main() {
|
|
|
925
1454
|
console.log(` \x1b[32m${runner} skill install <slug>\x1b[0m`);
|
|
926
1455
|
console.log(` \x1b[36m2.\x1b[0m Browse all 10,000+ skills:`);
|
|
927
1456
|
console.log(` \x1b[4mhttps://apeclaw.ai/skills\x1b[0m`);
|
|
928
|
-
console.log(` \x1b[36m3.\x1b[0m
|
|
1457
|
+
console.log(` \x1b[36m3.\x1b[0m Open your upgraded dashboard:`);
|
|
1458
|
+
console.log(` \x1b[32m${runner} dashboard\x1b[0m`);
|
|
1459
|
+
console.log(` \x1b[36m4.\x1b[0m Register your clawbot (optional — enables telemetry + shared backend):`);
|
|
929
1460
|
console.log(` \x1b[32m${runner} clawbot register --agent-id my-bot --name "My ClawBot" --json\x1b[0m`);
|
|
930
|
-
console.log(` \x1b[
|
|
1461
|
+
console.log(` \x1b[36m5.\x1b[0m Verify your setup:`);
|
|
931
1462
|
console.log(` \x1b[32m${runner} doctor --json\x1b[0m`);
|
|
932
|
-
console.log(` \x1b[
|
|
1463
|
+
console.log(` \x1b[36m6.\x1b[0m Optional — for onchain execute/bridge commands:`);
|
|
933
1464
|
console.log(` \x1b[2mexport APE_CLAW_RPC_URL=https://rpc.apechain.com/http\x1b[0m`);
|
|
934
1465
|
console.log(` \x1b[2mexport APE_CLAW_WALLET_KEY=0x...\x1b[0m`);
|
|
935
1466
|
|
|
936
1467
|
console.log();
|
|
937
1468
|
console.log(` \x1b[2mDocs:\x1b[0m https://apeclaw.ai/docs`);
|
|
938
|
-
console.log(` \x1b[2mDash:\x1b[0m
|
|
1469
|
+
console.log(` \x1b[2mDash:\x1b[0m npx ape-claw dashboard`);
|
|
939
1470
|
console.log(` \x1b[2mHelp:\x1b[0m ${runner} --help`);
|
|
940
1471
|
console.log();
|
|
941
1472
|
console.log(`\x1b[1m\x1b[33m${dline}\x1b[0m`);
|
|
@@ -1076,6 +1607,10 @@ async function main() {
|
|
|
1076
1607
|
verifiedBot = v.agent;
|
|
1077
1608
|
sharedOpenseaKey = v.sharedOpenseaApiKey || "";
|
|
1078
1609
|
} else {
|
|
1610
|
+
if (allowUnverifiedAgentForCommand) {
|
|
1611
|
+
verifiedBot = null;
|
|
1612
|
+
sharedOpenseaKey = "";
|
|
1613
|
+
} else
|
|
1079
1614
|
// If this machine doesn't have clawbots.json (or it's out of date),
|
|
1080
1615
|
// try to verify against the shared backend when configured.
|
|
1081
1616
|
if (apiBaseForIdentity) {
|
|
@@ -2299,6 +2834,7 @@ async function main() {
|
|
|
2299
2834
|
commands: {
|
|
2300
2835
|
doctor: "ape-claw doctor --json",
|
|
2301
2836
|
quickstart: "ape-claw quickstart --json",
|
|
2837
|
+
dashboard: "ape-claw dashboard [restore-openclaw] [--json]",
|
|
2302
2838
|
"clawbot register": "ape-claw clawbot register --agent-id <id> --name <name> [--api https://api.apeclaw.ai --invite <token>] [--registration-key <key>] --json",
|
|
2303
2839
|
"clawbot list": "ape-claw clawbot list --json",
|
|
2304
2840
|
"auth set": "ape-claw auth set [--agent-id <id>] [--agent-token <token>] [--opensea-api-key <key>] [--private-key <pk>] --json",
|
|
@@ -2318,7 +2854,7 @@ async function main() {
|
|
|
2318
2854
|
"bridge execute (autonomous)": "ape-claw bridge execute --request <requestId> --execute --autonomous --json",
|
|
2319
2855
|
"bridge status": "ape-claw bridge status --request <requestId> --json",
|
|
2320
2856
|
"allowlist audit": "ape-claw allowlist audit --json",
|
|
2321
|
-
"skill install": "ape-claw skill install [<slug>] [--scope local] [--starter-pack | --no-starter-pack] [--allow-unvetted] [--allow-high-risk] [--allow-custom-api] [--allow-insecure-api] --json",
|
|
2857
|
+
"skill install": "ape-claw skill install [<slug>] [--scope local] [--starter-pack | --no-starter-pack] [--forge-upgrade | --no-forge-upgrade] [--allow-unvetted] [--allow-high-risk] [--allow-custom-api] [--allow-insecure-api] --json",
|
|
2322
2858
|
"v2 skill mint": "ape-claw v2 skill mint --rpc <url> --privateKey 0x... --skillNft 0x... --registry 0x... [--parentId 0] [--royalty-receiver 0x... --royalty-bps 500] --json",
|
|
2323
2859
|
"v2 skill publish": "ape-claw v2 skill publish --rpc <url> --privateKey 0x... --registry 0x... --skillId <id> --file <skillcard.json> [--uri ipfs://...] [--riskTier 1] --json",
|
|
2324
2860
|
"v2 intent create": "ape-claw v2 intent create --rpc <url> --privateKey 0x... --intents 0x... --payload '{...}' [--expiresAt <unixSec>] --json",
|