ape-claw 0.1.3 → 0.1.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 +36 -24
- package/docs/CLI_GUIDE.md +5 -0
- package/docs/GLOBAL_BACKEND.md +3 -1
- package/docs/PRODUCT_OVERVIEW.md +15 -1
- package/docs/STARTER_PACK.md +7 -5
- package/docs/SUPPORTED_NETWORKS.md +1 -1
- package/docs/operator/01-quickstart.md +100 -9
- package/docs/operator/02-dashboard.md +1 -1
- package/docs/operator/03-cli-reference.md +19 -7
- package/docs/operator/04-skills-library.md +13 -12
- package/docs/operator/06-deployment.md +60 -7
- package/docs/operator/09-env-reference.md +61 -0
- package/docs/social/STARTER_PACK_THREAD.md +3 -3
- package/package.json +1 -1
- package/src/cli.mjs +132 -16
- package/src/server/index.mjs +6 -2
- package/src/server/routes/forge-agent.mjs +820 -0
- package/src/server/routes/skills.mjs +39 -6
- package/src/server/storage/file-backend.mjs +31 -0
- package/src/telemetry-server.mjs +90 -4
- package/ui/forge/css/forge.css +30 -2
- package/ui/forge/index.html +20 -7
- package/ui/forge/js/forge-attachments.js +26 -38
- package/ui/forge/js/forge-chat.js +240 -20
- package/ui/forge/js/forge-data.js +76 -14
- package/ui/forge/js/forge-scene.js +1943 -391
- package/ui/index.html +53 -54
- package/ui/js/skills.js +57 -42
- package/ui/shared/sidebar-nav.js +1 -1
- package/ui/skills.html +80 -53
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { ROOT as PROJECT_ROOT } from "../../lib/paths.mjs";
|
|
8
9
|
import { getStorage } from "../storage/index.mjs";
|
|
@@ -21,6 +22,15 @@ function safeVersion(v) {
|
|
|
21
22
|
return s;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function yamlSafe(v) {
|
|
26
|
+
const s = String(v || "");
|
|
27
|
+
if (!s) return "\"\"";
|
|
28
|
+
if (/[:{}\[\]#&*!|>'"%@`,\n]/.test(s) || s.startsWith(" ") || s.endsWith(" ")) {
|
|
29
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
30
|
+
}
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
function resolveHumanizerSlug(allSlugs) {
|
|
25
35
|
const preferred = ["clawhub-humanizer", "clawhub-humanizer-2", "clawhub-afrexai-humanizer"];
|
|
26
36
|
for (const s of preferred) {
|
|
@@ -103,13 +113,21 @@ function upsertUserSkill(store, auth, skillcard, sourceUrl, createdAt) {
|
|
|
103
113
|
function installOpenClawSkillCard(skillcard, fallbackSlug = "") {
|
|
104
114
|
const slugValue = toSlug(skillcard?.slug || fallbackSlug || skillcard?.name || "");
|
|
105
115
|
if (!slugValue) throw new Error("invalid slug for OpenClaw install");
|
|
106
|
-
const skillDir = path.join(
|
|
116
|
+
const skillDir = path.join(os.homedir(), ".openclaw", "skills", slugValue);
|
|
107
117
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
108
118
|
const doc = String(skillcard?.documentation_md || "").trim();
|
|
109
119
|
const name = String(skillcard?.name || slugValue).trim();
|
|
110
120
|
const version = String(skillcard?.version || "1.0.0").trim();
|
|
111
121
|
const description = String(skillcard?.description || "").trim();
|
|
112
|
-
|
|
122
|
+
if (Buffer.byteLength(doc, "utf8") > 300_000) throw new Error("documentation_md too large");
|
|
123
|
+
const frontmatter = `---\nname: ${slugValue}\nversion: ${yamlSafe(version)}\ndescription: ${yamlSafe(description.slice(0, 300))}\n---\n`;
|
|
124
|
+
let content;
|
|
125
|
+
if (doc) {
|
|
126
|
+
const stripped = doc.replace(/^---[\s\S]*?---\s*/, "").trim();
|
|
127
|
+
content = `${frontmatter}\n${stripped}\n`;
|
|
128
|
+
} else {
|
|
129
|
+
content = `${frontmatter}\n# ${name}\n\n${description}\n`;
|
|
130
|
+
}
|
|
113
131
|
fs.writeFileSync(path.join(skillDir, "SKILL.md"), content, "utf8");
|
|
114
132
|
return { slug: slugValue, skillDir };
|
|
115
133
|
}
|
|
@@ -117,7 +135,7 @@ function installOpenClawSkillCard(skillcard, fallbackSlug = "") {
|
|
|
117
135
|
function uninstallOpenClawSkillBySlug(slug) {
|
|
118
136
|
const s = toSlug(slug || "");
|
|
119
137
|
if (!s) return { removed: null, missing: null };
|
|
120
|
-
const skillDir = path.join(
|
|
138
|
+
const skillDir = path.join(os.homedir(), ".openclaw", "skills", s);
|
|
121
139
|
const skillMd = path.join(skillDir, "SKILL.md");
|
|
122
140
|
if (!fs.existsSync(skillMd)) {
|
|
123
141
|
return { removed: null, missing: { slug: s, reason: "SKILL.md not found" } };
|
|
@@ -137,7 +155,7 @@ export function handleSkillsSearch(req, res, reqUrl) {
|
|
|
137
155
|
const sourceFilter = String(reqUrl.searchParams.get("source") || "").trim().toLowerCase();
|
|
138
156
|
const vettedFilter = String(reqUrl.searchParams.get("vetted") || "").trim();
|
|
139
157
|
const page = Math.max(1, Number(reqUrl.searchParams.get("page") || 1));
|
|
140
|
-
const limit = Math.min(
|
|
158
|
+
const limit = Math.min(15000, Math.max(1, Number(reqUrl.searchParams.get("limit") || 50)));
|
|
141
159
|
let results = store.getMergedSkillIndex();
|
|
142
160
|
if (sourceFilter && ["seed", "bundled", "imported", "user"].includes(sourceFilter)) results = results.filter((s) => s.source === sourceFilter);
|
|
143
161
|
if (vettedFilter === "1") results = results.filter((s) => s.vettedOk === true);
|
|
@@ -152,8 +170,23 @@ export function handleSkillsSearch(req, res, reqUrl) {
|
|
|
152
170
|
const total = results.length;
|
|
153
171
|
const pages = Math.ceil(total / limit);
|
|
154
172
|
const start = (page - 1) * limit;
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
const paginatedResults = results.slice(start, start + limit);
|
|
174
|
+
const slim = reqUrl.searchParams.get("slim") === "1";
|
|
175
|
+
const payload = slim
|
|
176
|
+
? paginatedResults.map((s) => ({
|
|
177
|
+
name: s.name, slug: s.slug, description: s.description,
|
|
178
|
+
riskTier: s.riskTier, source: s.source, vettedOk: s.vettedOk,
|
|
179
|
+
fileName: s.fileName || null,
|
|
180
|
+
onchainTokenId: s.onchainTokenId || null,
|
|
181
|
+
onchainMintTx: s.onchainMintTx || null,
|
|
182
|
+
onchainPublishTx: s.onchainPublishTx || null,
|
|
183
|
+
}))
|
|
184
|
+
: paginatedResults;
|
|
185
|
+
res.writeHead(200, {
|
|
186
|
+
"content-type": "application/json; charset=utf-8",
|
|
187
|
+
"cache-control": "public, max-age=120, s-maxage=300, stale-while-revalidate=600",
|
|
188
|
+
});
|
|
189
|
+
return res.end(JSON.stringify({ ok: true, total, page, limit, pages, results: payload }));
|
|
157
190
|
} catch (err) {
|
|
158
191
|
res.writeHead(500, { "content-type": "application/json; charset=utf-8" });
|
|
159
192
|
return res.end(JSON.stringify({ ok: false, error: err.message || "search failed" }));
|
|
@@ -21,6 +21,7 @@ const SKILLCARDS_USER_INDEX_PATH = path.join(SKILLCARDS_USER_DIR, "index.json");
|
|
|
21
21
|
const SKILLCARDS_SEED_DIR = path.join(PROJECT_ROOT, "skillcards", "seed");
|
|
22
22
|
const SKILLCARDS_BUNDLED_DIR = path.join(PROJECT_ROOT, "data", "skills");
|
|
23
23
|
const SKILLCARDS_IMPORTED_INDEX_PATH = path.join(PROJECT_ROOT, "skillcards", "imported", "index.json");
|
|
24
|
+
const SKILLCARDS_SNAPSHOT_PATH = path.join(PROJECT_ROOT, "data", "skills-search.json");
|
|
24
25
|
|
|
25
26
|
const MERGED_INDEX_CACHE_TTL_MS = 60_000;
|
|
26
27
|
let mergedSkillIndexCache = { data: null, expiresAt: 0 };
|
|
@@ -146,6 +147,36 @@ function buildMergedSkillIndex() {
|
|
|
146
147
|
}
|
|
147
148
|
} catch { /* skip */ }
|
|
148
149
|
|
|
150
|
+
// If all live sources are empty, load from the pre-built snapshot so the
|
|
151
|
+
// API never returns 0 skills (covers deployment misconfigs, missing volumes, etc).
|
|
152
|
+
if (merged.length === 0) {
|
|
153
|
+
try {
|
|
154
|
+
if (fs.existsSync(SKILLCARDS_SNAPSHOT_PATH)) {
|
|
155
|
+
const snap = JSON.parse(fs.readFileSync(SKILLCARDS_SNAPSHOT_PATH, "utf8"));
|
|
156
|
+
const results = Array.isArray(snap?.results) ? snap.results : [];
|
|
157
|
+
for (const s of results) {
|
|
158
|
+
if (s && typeof s === "object" && (s.name || s.slug)) {
|
|
159
|
+
merged.push({
|
|
160
|
+
name: String(s.name || s.slug || "").trim(),
|
|
161
|
+
slug: String(s.slug || "").trim(),
|
|
162
|
+
description: String(s.description || "").trim(),
|
|
163
|
+
fileName: String(s.fileName || "").trim() || null,
|
|
164
|
+
source: String(s.source || "imported").trim(),
|
|
165
|
+
vettedOk: Boolean(s.vettedOk),
|
|
166
|
+
importOk: Boolean(s.importOk !== false),
|
|
167
|
+
riskTier: Number(s.riskTier ?? 2),
|
|
168
|
+
sourceUrl: String(s.sourceUrl || "").trim() || null,
|
|
169
|
+
provenance: s.provenance || { publisher: "snapshot", signed: false },
|
|
170
|
+
onchainTokenId: s.onchainTokenId || null,
|
|
171
|
+
onchainMintTx: s.onchainMintTx || null,
|
|
172
|
+
onchainPublishTx: s.onchainPublishTx || null,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch { /* snapshot unavailable */ }
|
|
178
|
+
}
|
|
179
|
+
|
|
149
180
|
// Deduplicate by slug while preserving source precedence:
|
|
150
181
|
// seed < imported < user (later entries override earlier ones).
|
|
151
182
|
const bySlug = new Map();
|
package/src/telemetry-server.mjs
CHANGED
|
@@ -96,6 +96,7 @@ if (!fs.existsSync(SKILLCARDS_USER_INDEX_PATH)) {
|
|
|
96
96
|
const SKILLCARDS_SEED_DIR = path.join(ROOT, "skillcards", "seed");
|
|
97
97
|
const SKILLCARDS_BUNDLED_DIR = path.join(ROOT, "data", "skills");
|
|
98
98
|
const SKILLCARDS_IMPORTED_INDEX_PATH = path.join(ROOT, "skillcards", "imported", "index.json");
|
|
99
|
+
const SKILLCARDS_SNAPSHOT_PATH = path.join(ROOT, "data", "skills-search.json");
|
|
99
100
|
|
|
100
101
|
// Cache for merged skill index (60 seconds TTL)
|
|
101
102
|
let mergedSkillIndexCache = { data: null, expiresAt: 0 };
|
|
@@ -232,6 +233,37 @@ function buildMergedSkillIndex() {
|
|
|
232
233
|
// Skip if user index doesn't exist or can't be read
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
// If all live sources are empty, load from the pre-built snapshot so the
|
|
237
|
+
// API never returns 0 skills (covers path misconfigs, missing symlinks, etc).
|
|
238
|
+
if (merged.length === 0) {
|
|
239
|
+
try {
|
|
240
|
+
if (fs.existsSync(SKILLCARDS_SNAPSHOT_PATH)) {
|
|
241
|
+
const snap = JSON.parse(fs.readFileSync(SKILLCARDS_SNAPSHOT_PATH, "utf8"));
|
|
242
|
+
const results = Array.isArray(snap?.results) ? snap.results : [];
|
|
243
|
+
for (const s of results) {
|
|
244
|
+
if (s && typeof s === "object" && (s.name || s.slug)) {
|
|
245
|
+
merged.push({
|
|
246
|
+
name: String(s.name || s.slug || "").trim(),
|
|
247
|
+
slug: String(s.slug || "").trim(),
|
|
248
|
+
description: String(s.description || "").trim(),
|
|
249
|
+
fileName: String(s.fileName || "").trim() || null,
|
|
250
|
+
source: String(s.source || "imported").trim(),
|
|
251
|
+
vettedOk: Boolean(s.vettedOk),
|
|
252
|
+
importOk: Boolean(s.importOk !== false),
|
|
253
|
+
riskTier: Number(s.riskTier ?? 2),
|
|
254
|
+
sourceUrl: String(s.sourceUrl || "").trim() || null,
|
|
255
|
+
provenance: s.provenance || { publisher: "snapshot", signed: false },
|
|
256
|
+
onchainTokenId: s.onchainTokenId || null,
|
|
257
|
+
onchainMintTx: s.onchainMintTx || null,
|
|
258
|
+
onchainPublishTx: s.onchainPublishTx || null,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
console.log(`[skills] Loaded ${merged.length} skills from snapshot fallback`);
|
|
263
|
+
}
|
|
264
|
+
} catch { /* snapshot unavailable */ }
|
|
265
|
+
}
|
|
266
|
+
|
|
235
267
|
// Deduplicate by slug while preserving source precedence:
|
|
236
268
|
// seed < imported < user (later entries override earlier ones).
|
|
237
269
|
const bySlug = new Map();
|
|
@@ -840,7 +872,7 @@ const server = http.createServer((req, res) => {
|
|
|
840
872
|
const sourceFilter = String(reqUrl.searchParams.get("source") || "").trim().toLowerCase();
|
|
841
873
|
const vettedFilter = String(reqUrl.searchParams.get("vetted") || "").trim();
|
|
842
874
|
const page = Math.max(1, Number(reqUrl.searchParams.get("page") || 1));
|
|
843
|
-
const limit = Math.min(
|
|
875
|
+
const limit = Math.min(15000, Math.max(1, Number(reqUrl.searchParams.get("limit") || 50)));
|
|
844
876
|
|
|
845
877
|
let results = getMergedSkillIndex();
|
|
846
878
|
|
|
@@ -871,7 +903,22 @@ const server = http.createServer((req, res) => {
|
|
|
871
903
|
const end = start + limit;
|
|
872
904
|
const paginatedResults = results.slice(start, end);
|
|
873
905
|
|
|
874
|
-
|
|
906
|
+
const slim = reqUrl.searchParams.get("slim") === "1";
|
|
907
|
+
const payload = slim
|
|
908
|
+
? paginatedResults.map((s) => ({
|
|
909
|
+
name: s.name, slug: s.slug, description: s.description,
|
|
910
|
+
riskTier: s.riskTier, source: s.source, vettedOk: s.vettedOk,
|
|
911
|
+
fileName: s.fileName || null,
|
|
912
|
+
onchainTokenId: s.onchainTokenId || null,
|
|
913
|
+
onchainMintTx: s.onchainMintTx || null,
|
|
914
|
+
onchainPublishTx: s.onchainPublishTx || null,
|
|
915
|
+
}))
|
|
916
|
+
: paginatedResults;
|
|
917
|
+
|
|
918
|
+
res.writeHead(200, {
|
|
919
|
+
"content-type": "application/json; charset=utf-8",
|
|
920
|
+
"cache-control": "public, max-age=120, s-maxage=300, stale-while-revalidate=600",
|
|
921
|
+
});
|
|
875
922
|
return res.end(
|
|
876
923
|
JSON.stringify({
|
|
877
924
|
ok: true,
|
|
@@ -879,7 +926,7 @@ const server = http.createServer((req, res) => {
|
|
|
879
926
|
page,
|
|
880
927
|
limit,
|
|
881
928
|
pages,
|
|
882
|
-
results:
|
|
929
|
+
results: payload,
|
|
883
930
|
}),
|
|
884
931
|
);
|
|
885
932
|
} catch (err) {
|
|
@@ -921,6 +968,42 @@ const server = http.createServer((req, res) => {
|
|
|
921
968
|
return res.end(JSON.stringify({ ok: false, error: err.message || "get failed" }));
|
|
922
969
|
}
|
|
923
970
|
}
|
|
971
|
+
// Path-based skill lookup: /api/skills/<slug> (used by CLI and frontend)
|
|
972
|
+
if (pathname.startsWith("/api/skills/") && req.method === "GET" &&
|
|
973
|
+
pathname !== "/api/skills/search" && pathname !== "/api/skills/get" && pathname !== "/api/skills/stats") {
|
|
974
|
+
try {
|
|
975
|
+
const slug = decodeURIComponent(pathname.slice("/api/skills/".length)).trim();
|
|
976
|
+
if (!slug) {
|
|
977
|
+
res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
|
|
978
|
+
return res.end(JSON.stringify({ ok: false, error: "missing slug" }));
|
|
979
|
+
}
|
|
980
|
+
const all = getMergedSkillIndex();
|
|
981
|
+
const match = all.find((s) => s.slug === slug);
|
|
982
|
+
if (!match) {
|
|
983
|
+
res.writeHead(404, { "content-type": "application/json; charset=utf-8" });
|
|
984
|
+
return res.end(JSON.stringify({ ok: false, error: "skill not found" }));
|
|
985
|
+
}
|
|
986
|
+
let fullCard = null;
|
|
987
|
+
const bucketDirs = {
|
|
988
|
+
seed: SKILLCARDS_SEED_DIR,
|
|
989
|
+
bundled: SKILLCARDS_BUNDLED_DIR,
|
|
990
|
+
imported: path.join(ROOT, "skillcards", "imported"),
|
|
991
|
+
user: SKILLCARDS_USER_DIR,
|
|
992
|
+
};
|
|
993
|
+
const baseDir = bucketDirs[match.source];
|
|
994
|
+
if (baseDir && match.fileName) {
|
|
995
|
+
try {
|
|
996
|
+
const fp = path.join(baseDir, match.fileName);
|
|
997
|
+
if (fs.existsSync(fp)) fullCard = JSON.parse(fs.readFileSync(fp, "utf8"));
|
|
998
|
+
} catch { /* index metadata is still returned below */ }
|
|
999
|
+
}
|
|
1000
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
1001
|
+
return res.end(JSON.stringify({ ok: true, skill: match, card: fullCard }));
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
res.writeHead(500, { "content-type": "application/json; charset=utf-8" });
|
|
1004
|
+
return res.end(JSON.stringify({ ok: false, error: err.message || "get failed" }));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
924
1007
|
if (pathname === "/api/skills/stats" && req.method === "GET") {
|
|
925
1008
|
try {
|
|
926
1009
|
const all = getMergedSkillIndex();
|
|
@@ -937,7 +1020,10 @@ const server = http.createServer((req, res) => {
|
|
|
937
1020
|
riskTier: s.riskTier, description: String(s.description || "").slice(0, 150),
|
|
938
1021
|
onchainTokenId: s.onchainTokenId ?? null,
|
|
939
1022
|
}));
|
|
940
|
-
res.writeHead(200, {
|
|
1023
|
+
res.writeHead(200, {
|
|
1024
|
+
"content-type": "application/json; charset=utf-8",
|
|
1025
|
+
"cache-control": "public, max-age=120, s-maxage=300, stale-while-revalidate=600",
|
|
1026
|
+
});
|
|
941
1027
|
return res.end(JSON.stringify({ ok: true, total: all.length, seed, bundled, imported, user, vetted, onchain, recent }));
|
|
942
1028
|
} catch (err) {
|
|
943
1029
|
res.writeHead(500, { "content-type": "application/json; charset=utf-8" });
|
package/ui/forge/css/forge.css
CHANGED
|
@@ -39,7 +39,7 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
39
39
|
|
|
40
40
|
/* ── Layout ─────────────────────────────────────────────── */
|
|
41
41
|
.forge-app{
|
|
42
|
-
display:flex;flex-direction:column;
|
|
42
|
+
display:flex;flex-direction:column;height:100vh;height:100dvh;position:relative;z-index:3;overflow:hidden;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/* ── Header ─────────────────────────────────────────────── */
|
|
@@ -63,7 +63,7 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
63
63
|
|
|
64
64
|
/* ── Viewport ───────────────────────────────────────────── */
|
|
65
65
|
.forge-viewport{
|
|
66
|
-
position:relative;flex:1
|
|
66
|
+
position:relative;flex:1 1 0%;min-height:0;background:#000;overflow:hidden;
|
|
67
67
|
}
|
|
68
68
|
.forge-viewport canvas{display:block;width:100%;height:100%}
|
|
69
69
|
#forgeLabelLayer{position:absolute;inset:0;pointer-events:none;overflow:hidden}
|
|
@@ -196,6 +196,19 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
196
196
|
.forge-chat .chat-msg-name{font-size:.72rem;font-weight:700;color:var(--accent)}
|
|
197
197
|
.forge-chat .chat-msg-time{font-family:var(--font-mono);font-size:.58rem;color:var(--dim);margin-left:auto}
|
|
198
198
|
.forge-chat .chat-msg-text{font-size:.75rem;line-height:1.5;color:var(--text);word-break:break-word}
|
|
199
|
+
.forge-chat .chat-msg-text strong{color:#eefad6;font-weight:700}
|
|
200
|
+
.forge-chat .chat-msg-text code{
|
|
201
|
+
font-family:var(--font-mono);font-size:.68rem;
|
|
202
|
+
background:rgba(99,215,255,.08);border:1px solid rgba(99,215,255,.2);
|
|
203
|
+
color:#bce9ff;padding:1px 4px;border-radius:4px;
|
|
204
|
+
}
|
|
205
|
+
.forge-chat .chat-msg-text pre{
|
|
206
|
+
margin:6px 0;padding:8px;border-radius:6px;
|
|
207
|
+
background:rgba(0,0,0,.35);border:1px solid var(--border);
|
|
208
|
+
overflow-x:auto;
|
|
209
|
+
}
|
|
210
|
+
.forge-chat .chat-msg-text ul{margin:6px 0 6px 14px;padding:0}
|
|
211
|
+
.forge-chat .chat-msg-text li{margin:2px 0}
|
|
199
212
|
.forge-chat-input-bar{
|
|
200
213
|
display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--border);
|
|
201
214
|
background:linear-gradient(180deg,rgba(207,255,4,.02),transparent),var(--surface2);
|
|
@@ -441,6 +454,21 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
441
454
|
border-radius:6px;padding:40px 36px;text-align:center;position:relative;
|
|
442
455
|
box-shadow:0 0 80px rgba(207,255,4,.06),var(--panel-shadow);
|
|
443
456
|
}
|
|
457
|
+
.forge-offline-btn-agent{
|
|
458
|
+
background:linear-gradient(135deg,var(--accent),#a0d800);color:#0c0c0c;
|
|
459
|
+
font-size:.85rem;padding:11px 28px;border:none;cursor:pointer;
|
|
460
|
+
box-shadow:0 0 20px rgba(207,255,4,.25),0 0 40px rgba(207,255,4,.1);
|
|
461
|
+
animation:forge-agent-pulse 2s ease-in-out infinite;
|
|
462
|
+
}
|
|
463
|
+
.forge-offline-btn-agent:hover{
|
|
464
|
+
background:linear-gradient(135deg,#e2ff40,#cfff04);
|
|
465
|
+
box-shadow:0 0 28px rgba(207,255,4,.4),0 0 60px rgba(207,255,4,.15);
|
|
466
|
+
transform:translateY(-1px);
|
|
467
|
+
}
|
|
468
|
+
@keyframes forge-agent-pulse{
|
|
469
|
+
0%,100%{box-shadow:0 0 20px rgba(207,255,4,.25),0 0 40px rgba(207,255,4,.1)}
|
|
470
|
+
50%{box-shadow:0 0 28px rgba(207,255,4,.4),0 0 56px rgba(207,255,4,.18)}
|
|
471
|
+
}
|
|
444
472
|
.forge-offline-icon{font-size:3.5rem;margin-bottom:12px;filter:drop-shadow(0 0 18px rgba(207,255,4,.35))}
|
|
445
473
|
.forge-offline-title{
|
|
446
474
|
font-family:var(--font-display);font-weight:900;font-size:1.6rem;letter-spacing:.06em;
|
package/ui/forge/index.html
CHANGED
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
<span class="icon">💬</span> Agent Chat
|
|
96
96
|
<span class="badge" id="forgeChatBadge">0</span>
|
|
97
97
|
</div>
|
|
98
|
+
<div class="forge-chat-agent-indicator" id="forgeChatAgentIndicator" style="display:none;padding:4px 10px;font-size:11px;opacity:.7;font-family:'JetBrains Mono',monospace;">🦞 Talking to The Clawllector (OpenClaw agent)</div>
|
|
98
99
|
<div class="forge-chat-messages" id="forgeChatMessages">
|
|
99
100
|
<div class="chat-empty">
|
|
100
101
|
<div class="chat-empty-icon">💬</div>
|
|
@@ -102,8 +103,8 @@
|
|
|
102
103
|
</div>
|
|
103
104
|
</div>
|
|
104
105
|
<div class="forge-chat-input-bar">
|
|
105
|
-
<input type="text" id="forgeChatInput" placeholder="Message your agent..." maxlength="500"
|
|
106
|
-
<button class="chat-send-btn" id="forgeChatSendBtn" type="button"
|
|
106
|
+
<input type="text" id="forgeChatInput" placeholder="Message your agent..." maxlength="500">
|
|
107
|
+
<button class="chat-send-btn" id="forgeChatSendBtn" type="button">Send</button>
|
|
107
108
|
</div>
|
|
108
109
|
<div class="chat-input-meta">
|
|
109
110
|
<span class="chat-counter" id="forgeChatCounter">0/500</span>
|
|
@@ -178,7 +179,7 @@
|
|
|
178
179
|
<div class="forge-offline-divider"></div>
|
|
179
180
|
<p class="forge-offline-desc">
|
|
180
181
|
The Forge renders <strong>your</strong> installed skills as attachments on a 3D robot.
|
|
181
|
-
It discovers skills from your local
|
|
182
|
+
It discovers skills from your local OpenClaw directories, so it must be
|
|
182
183
|
hosted on your machine.
|
|
183
184
|
</p>
|
|
184
185
|
<div class="forge-offline-steps">
|
|
@@ -200,7 +201,7 @@
|
|
|
200
201
|
<span class="forge-offline-step-num">3</span>
|
|
201
202
|
<div>
|
|
202
203
|
<strong>Open the Forge</strong>
|
|
203
|
-
<code>http://localhost:
|
|
204
|
+
<code>http://localhost:8787/forge</code>
|
|
204
205
|
<span class="forge-offline-hint">Your installed skills appear as 3D attachments on the robot automatically</span>
|
|
205
206
|
</div>
|
|
206
207
|
</div>
|
|
@@ -208,9 +209,10 @@
|
|
|
208
209
|
<div class="forge-offline-divider"></div>
|
|
209
210
|
<p class="forge-offline-note">
|
|
210
211
|
The Forge reads skills installed via <code>npx ape-claw skill install</code>.<br>
|
|
211
|
-
Skills are
|
|
212
|
+
Skills are installed to <code>~/.openclaw/skills/</code> and synced to <code>~/.openclaw/workspace/skills/</code> automatically. <strong>Requires <a href="https://openclaw.ai" style="color:var(--accent)">OpenClaw</a>.</strong>
|
|
212
213
|
</p>
|
|
213
214
|
<div class="forge-offline-links">
|
|
215
|
+
<button class="forge-offline-btn forge-offline-btn-agent" id="forgeTryAgent" type="button">🦞 Try Our Agent</button>
|
|
214
216
|
<a class="forge-offline-btn" href="/skills">Browse Skills</a>
|
|
215
217
|
<a class="forge-offline-btn forge-offline-btn-secondary" href="https://github.com/simplefarmer69/ape-claw" target="_blank" rel="noopener">GitHub</a>
|
|
216
218
|
</div>
|
|
@@ -221,12 +223,23 @@
|
|
|
221
223
|
(async function detectLocal() {
|
|
222
224
|
try {
|
|
223
225
|
const r = await fetch("/api/health", { signal: AbortSignal.timeout(4000) });
|
|
224
|
-
if (r.ok) return;
|
|
226
|
+
if (r.ok) return;
|
|
225
227
|
} catch {}
|
|
226
|
-
// Not local — show the offline gate and hide the forge app
|
|
227
228
|
document.getElementById("forgeOfflineGate").style.display = "";
|
|
228
229
|
const app = document.querySelector(".forge-app");
|
|
229
230
|
if (app) app.style.display = "none";
|
|
231
|
+
|
|
232
|
+
document.getElementById("forgeTryAgent").addEventListener("click", function() {
|
|
233
|
+
document.getElementById("forgeOfflineGate").style.display = "none";
|
|
234
|
+
if (app) app.style.display = "";
|
|
235
|
+
const chatInput = document.getElementById("forgeChatInput");
|
|
236
|
+
if (chatInput) {
|
|
237
|
+
chatInput.disabled = false;
|
|
238
|
+
chatInput.focus();
|
|
239
|
+
}
|
|
240
|
+
const chatBtn = document.getElementById("forgeChatSendBtn");
|
|
241
|
+
if (chatBtn) chatBtn.disabled = false;
|
|
242
|
+
});
|
|
230
243
|
})();
|
|
231
244
|
</script>
|
|
232
245
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* floating disconnected in space.
|
|
8
8
|
*/
|
|
9
9
|
import * as THREE from "three";
|
|
10
|
+
import { RoundedBoxGeometry } from "three/addons/geometries/RoundedBoxGeometry.js";
|
|
10
11
|
import { MAT } from "./forge-scene.js";
|
|
11
12
|
|
|
12
13
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -95,9 +96,12 @@ const RIVET_C = 0xd0d8e0;
|
|
|
95
96
|
|
|
96
97
|
function armorPlate(mat, w, h, d) {
|
|
97
98
|
const group = new THREE.Group();
|
|
98
|
-
const
|
|
99
|
+
const minDim = Math.min(w, h, d);
|
|
100
|
+
const bodyGeo = minDim > 0.08
|
|
101
|
+
? new RoundedBoxGeometry(w, h, d, 2, minDim * 0.12)
|
|
102
|
+
: new THREE.BoxGeometry(w, h, d);
|
|
103
|
+
const body = new THREE.Mesh(bodyGeo, mat);
|
|
99
104
|
group.add(body);
|
|
100
|
-
// Raised edge frame
|
|
101
105
|
const frameMat = mat.clone();
|
|
102
106
|
frameMat.color = new THREE.Color(FRAME_C);
|
|
103
107
|
frameMat.emissiveIntensity = (frameMat.emissiveIntensity || 0) * 0.4;
|
|
@@ -106,14 +110,12 @@ function armorPlate(mat, w, h, d) {
|
|
|
106
110
|
group.add(new THREE.Mesh(new THREE.BoxGeometry(w + bw * 2, bw, d + bw), frameMat).translateY(-h / 2));
|
|
107
111
|
group.add(new THREE.Mesh(new THREE.BoxGeometry(bw, h, d + bw), frameMat).translateX(w / 2));
|
|
108
112
|
group.add(new THREE.Mesh(new THREE.BoxGeometry(bw, h, d + bw), frameMat).translateX(-w / 2));
|
|
109
|
-
// Center glow line
|
|
110
113
|
const glowMat = mat.clone();
|
|
111
114
|
glowMat.emissiveIntensity = (glowMat.emissiveIntensity || 0.5) * 2.5;
|
|
112
115
|
glowMat.transparent = true; glowMat.opacity = 0.7;
|
|
113
116
|
group.add(new THREE.Mesh(new THREE.BoxGeometry(w * 0.6, 0.015, d * 0.3), glowMat).translateZ(d / 2 + 0.005));
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const rGeo = new THREE.SphereGeometry(0.018, 5, 3);
|
|
117
|
+
const rMat = new THREE.MeshPhysicalMaterial({ color: RIVET_C, metalness: 0.95, roughness: 0.08, clearcoat: 1.0, clearcoatRoughness: 0.0 });
|
|
118
|
+
const rGeo = new THREE.SphereGeometry(0.018, 8, 6);
|
|
117
119
|
for (const [rx, ry] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) {
|
|
118
120
|
const rv = new THREE.Mesh(rGeo, rMat);
|
|
119
121
|
rv.position.set(rx * w * 0.4, ry * h * 0.35, d / 2 + 0.01);
|
|
@@ -125,21 +127,18 @@ function armorPlate(mat, w, h, d) {
|
|
|
125
127
|
function hexPlate(mat, r, h) {
|
|
126
128
|
const group = new THREE.Group();
|
|
127
129
|
group.add(new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, 6), mat));
|
|
128
|
-
// Inner hex detail (recessed)
|
|
129
130
|
const innerMat = mat.clone();
|
|
130
131
|
innerMat.emissiveIntensity = (innerMat.emissiveIntensity || 0.5) * 1.8;
|
|
131
132
|
innerMat.transparent = true; innerMat.opacity = 0.6;
|
|
132
133
|
const inner = new THREE.Mesh(new THREE.CylinderGeometry(r * 0.55, r * 0.55, h + 0.01, 6), innerMat);
|
|
133
134
|
group.add(inner);
|
|
134
|
-
// Center glow dot
|
|
135
135
|
const dotMat = mat.clone();
|
|
136
136
|
dotMat.emissiveIntensity = (dotMat.emissiveIntensity || 0.5) * 3;
|
|
137
|
-
group.add(new THREE.Mesh(new THREE.SphereGeometry(r * 0.18,
|
|
138
|
-
// Edge ring
|
|
137
|
+
group.add(new THREE.Mesh(new THREE.SphereGeometry(r * 0.18, 12, 8), dotMat).translateY(h / 2 + 0.01));
|
|
139
138
|
const edgeMat = mat.clone();
|
|
140
139
|
edgeMat.emissiveIntensity = (edgeMat.emissiveIntensity || 0.5) * 2;
|
|
141
140
|
edgeMat.transparent = true; edgeMat.opacity = 0.5;
|
|
142
|
-
const edge = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.06,
|
|
141
|
+
const edge = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.06, 10, 6), edgeMat);
|
|
143
142
|
edge.rotation.x = Math.PI / 2; edge.position.y = h / 2 + 0.005;
|
|
144
143
|
group.add(edge);
|
|
145
144
|
return group;
|
|
@@ -147,25 +146,21 @@ function hexPlate(mat, r, h) {
|
|
|
147
146
|
|
|
148
147
|
function energyModule(mat, r) {
|
|
149
148
|
const group = new THREE.Group();
|
|
150
|
-
const core = new THREE.Mesh(new THREE.OctahedronGeometry(r * 0.6), mat);
|
|
149
|
+
const core = new THREE.Mesh(new THREE.OctahedronGeometry(r * 0.6, 1), mat);
|
|
151
150
|
group.add(core);
|
|
152
|
-
// Inner glow sphere
|
|
153
151
|
const gMat = mat.clone();
|
|
154
152
|
gMat.emissiveIntensity = (gMat.emissiveIntensity || 1) * 3;
|
|
155
153
|
gMat.transparent = true; gMat.opacity = 0.5;
|
|
156
|
-
group.add(new THREE.Mesh(new THREE.SphereGeometry(r * 0.3,
|
|
157
|
-
|
|
158
|
-
const ring = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.1, 8, 24), mat.clone());
|
|
154
|
+
group.add(new THREE.Mesh(new THREE.SphereGeometry(r * 0.3, 16, 12), gMat));
|
|
155
|
+
const ring = new THREE.Mesh(new THREE.TorusGeometry(r, r * 0.1, 12, 32), mat.clone());
|
|
159
156
|
ring.material.transparent = true; ring.material.opacity = 0.6;
|
|
160
157
|
ring.rotation.x = Math.PI / 2;
|
|
161
158
|
group.add(ring);
|
|
162
|
-
|
|
163
|
-
const ring2 = new THREE.Mesh(new THREE.TorusGeometry(r * 0.8, r * 0.06, 6, 18), mat.clone());
|
|
159
|
+
const ring2 = new THREE.Mesh(new THREE.TorusGeometry(r * 0.8, r * 0.06, 10, 24), mat.clone());
|
|
164
160
|
ring2.material.transparent = true; ring2.material.opacity = 0.4;
|
|
165
161
|
ring2.rotation.z = Math.PI / 2;
|
|
166
162
|
group.add(ring2);
|
|
167
|
-
|
|
168
|
-
const bMat = new THREE.MeshStandardMaterial({ color: FRAME_C, metalness: 0.5, roughness: 0.5 });
|
|
163
|
+
const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.6, roughness: 0.4, clearcoat: 0.3, clearcoatRoughness: 0.4 });
|
|
169
164
|
group.add(new THREE.Mesh(new THREE.BoxGeometry(0.03, r * 1.2, 0.03), bMat).translateX(-r * 0.9));
|
|
170
165
|
return group;
|
|
171
166
|
}
|
|
@@ -195,24 +190,20 @@ function bladeFin(mat, h, w) {
|
|
|
195
190
|
transparent: true, opacity: 0.6,
|
|
196
191
|
blending: THREE.AdditiveBlending,
|
|
197
192
|
})));
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
group.add(new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.04, 0.06, 6), bMat).translateY(-h / 2 - 0.03));
|
|
193
|
+
const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.6, roughness: 0.4, clearcoat: 0.3, clearcoatRoughness: 0.4 });
|
|
194
|
+
group.add(new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.04, 0.06, 8), bMat).translateY(-h / 2 - 0.03));
|
|
201
195
|
return group;
|
|
202
196
|
}
|
|
203
197
|
|
|
204
198
|
function turretModule(mat, r) {
|
|
205
199
|
const group = new THREE.Group();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.
|
|
209
|
-
// Mount base
|
|
210
|
-
group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.6, r * 0.7, r * 0.4, 8), mat));
|
|
211
|
-
// Glow ring around base
|
|
200
|
+
const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.7, roughness: 0.3, clearcoat: 0.4, clearcoatRoughness: 0.3 });
|
|
201
|
+
group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.18, r * 0.22, r * 2.5, 12), bMat).rotateX(Math.PI / 2).translateY(r * 0.6));
|
|
202
|
+
group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 0.6, r * 0.7, r * 0.4, 12), mat));
|
|
212
203
|
const gMat = mat.clone();
|
|
213
204
|
gMat.emissiveIntensity = (gMat.emissiveIntensity || 0.5) * 2.5;
|
|
214
205
|
gMat.transparent = true; gMat.opacity = 0.5;
|
|
215
|
-
const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.65, r * 0.06,
|
|
206
|
+
const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.65, r * 0.06, 10, 24), gMat);
|
|
216
207
|
ring.rotation.x = Math.PI / 2;
|
|
217
208
|
group.add(ring);
|
|
218
209
|
return group;
|
|
@@ -220,17 +211,14 @@ function turretModule(mat, r) {
|
|
|
220
211
|
|
|
221
212
|
function sensorDome(mat, r) {
|
|
222
213
|
const group = new THREE.Group();
|
|
223
|
-
group.add(new THREE.Mesh(new THREE.SphereGeometry(r,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
group.add(new THREE.Mesh(new THREE.
|
|
227
|
-
// Antenna spike
|
|
228
|
-
group.add(new THREE.Mesh(new THREE.ConeGeometry(r * 0.08, r * 1.2, 5), mat.clone()).translateY(r * 0.9));
|
|
229
|
-
// Glow band
|
|
214
|
+
group.add(new THREE.Mesh(new THREE.SphereGeometry(r, 24, 16, 0, Math.PI * 2, 0, Math.PI / 2), mat));
|
|
215
|
+
const bMat = new THREE.MeshPhysicalMaterial({ color: FRAME_C, metalness: 0.6, roughness: 0.4, clearcoat: 0.3, clearcoatRoughness: 0.4 });
|
|
216
|
+
group.add(new THREE.Mesh(new THREE.CylinderGeometry(r * 1.1, r * 1.1, r * 0.15, 16), bMat));
|
|
217
|
+
group.add(new THREE.Mesh(new THREE.ConeGeometry(r * 0.08, r * 1.2, 8), mat.clone()).translateY(r * 0.9));
|
|
230
218
|
const gMat = mat.clone();
|
|
231
219
|
gMat.emissiveIntensity = (gMat.emissiveIntensity || 0.5) * 2.5;
|
|
232
220
|
gMat.transparent = true; gMat.opacity = 0.5;
|
|
233
|
-
const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.7, r * 0.04,
|
|
221
|
+
const ring = new THREE.Mesh(new THREE.TorusGeometry(r * 0.7, r * 0.04, 10, 24), gMat);
|
|
234
222
|
ring.rotation.x = Math.PI / 2; ring.position.y = r * 0.3;
|
|
235
223
|
group.add(ring);
|
|
236
224
|
return group;
|