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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes:
|
|
3
|
+
* GET /api/openclaw/env
|
|
4
|
+
* POST /api/openclaw/env
|
|
5
|
+
*
|
|
6
|
+
* Local-only helper for Forge to read/update OpenClaw env keys.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { collectBody } from "../middleware/body-limit.mjs";
|
|
12
|
+
import { openClawEnvFileCandidates } from "../../lib/openclaw-paths.mjs";
|
|
13
|
+
|
|
14
|
+
const ALLOWED_KEYS = [
|
|
15
|
+
"OPENAI_API_KEY",
|
|
16
|
+
"ANTHROPIC_API_KEY",
|
|
17
|
+
"PERPLEXITY_API_KEY",
|
|
18
|
+
"GROQ_API_KEY",
|
|
19
|
+
"TOGETHER_API_KEY",
|
|
20
|
+
"OLLAMA_HOST",
|
|
21
|
+
"OLLAMA_BASE_URL",
|
|
22
|
+
];
|
|
23
|
+
const BLOCKED_KEYS = new Set([
|
|
24
|
+
"PATH", "HOME", "SHELL", "PWD", "USER", "LOGNAME",
|
|
25
|
+
"OPENCLAW_HOME", "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH",
|
|
26
|
+
"FORGE_LLM_API_URL", "FORGE_LLM_API_KEY", "FORGE_LLM_MODEL",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function isLocalRequest(req) {
|
|
30
|
+
const ip = String(req.socket?.remoteAddress || "");
|
|
31
|
+
return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveEnvPath() {
|
|
35
|
+
const candidates = openClawEnvFileCandidates();
|
|
36
|
+
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseEnv(raw) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const line of String(raw || "").split(/\r?\n/)) {
|
|
42
|
+
const s = line.trim();
|
|
43
|
+
if (!s || s.startsWith("#")) continue;
|
|
44
|
+
const eq = s.indexOf("=");
|
|
45
|
+
if (eq <= 0) continue;
|
|
46
|
+
const key = s.slice(0, eq).trim();
|
|
47
|
+
let val = s.slice(eq + 1).trim();
|
|
48
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
49
|
+
val = val.slice(1, -1);
|
|
50
|
+
}
|
|
51
|
+
if (key) out[key] = val;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function quoteEnvValue(value) {
|
|
57
|
+
const s = String(value ?? "");
|
|
58
|
+
if (!s) return '""';
|
|
59
|
+
if (/[\s#"'`\\]/.test(s)) {
|
|
60
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
61
|
+
}
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidEnvKey(key) {
|
|
66
|
+
const k = String(key || "").trim();
|
|
67
|
+
if (!k) return false;
|
|
68
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) return false;
|
|
69
|
+
if (BLOCKED_KEYS.has(k)) return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderEnv(map, managedKeys, previousRaw = "") {
|
|
74
|
+
const oldLines = String(previousRaw || "").split(/\r?\n/);
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const nextLines = oldLines.map((line) => {
|
|
77
|
+
const eq = line.indexOf("=");
|
|
78
|
+
if (eq <= 0) return line;
|
|
79
|
+
const key = line.slice(0, eq).trim();
|
|
80
|
+
if (!managedKeys.has(key)) return line;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
if (!(key in map)) return line;
|
|
83
|
+
const val = String(map[key] ?? "");
|
|
84
|
+
if (!val) return `# ${key}=`;
|
|
85
|
+
return `${key}=${quoteEnvValue(val)}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
for (const key of managedKeys) {
|
|
89
|
+
if (seen.has(key)) continue;
|
|
90
|
+
const val = String(map[key] ?? "");
|
|
91
|
+
if (!val) continue;
|
|
92
|
+
nextLines.push(`${key}=${quoteEnvValue(val)}`);
|
|
93
|
+
}
|
|
94
|
+
return `${nextLines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildMasked(map) {
|
|
98
|
+
const masked = {};
|
|
99
|
+
for (const key of Object.keys(map || {})) {
|
|
100
|
+
const v = String(map[key] || "");
|
|
101
|
+
if (!v) masked[key] = "";
|
|
102
|
+
else if (key.endsWith("_HOST") || key.endsWith("_URL") || key.endsWith("_MODEL")) masked[key] = v;
|
|
103
|
+
else if (/KEY|TOKEN|SECRET|PASSWORD/i.test(key)) masked[key] = v.length <= 8 ? "*".repeat(v.length) : `${v.slice(0, 4)}...${v.slice(-4)}`;
|
|
104
|
+
else if (v.length <= 8) masked[key] = "*".repeat(v.length);
|
|
105
|
+
else masked[key] = `${v.slice(0, 4)}...${v.slice(-4)}`;
|
|
106
|
+
}
|
|
107
|
+
return masked;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function handleOpenClawEnvGet(req, res) {
|
|
111
|
+
if (!isLocalRequest(req)) {
|
|
112
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
113
|
+
return res.end(JSON.stringify({ ok: false, error: "local requests only" }));
|
|
114
|
+
}
|
|
115
|
+
const envPath = resolveEnvPath();
|
|
116
|
+
const raw = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
117
|
+
const parsed = parseEnv(raw);
|
|
118
|
+
const values = {};
|
|
119
|
+
for (const key of ALLOWED_KEYS) values[key] = String(parsed[key] || "");
|
|
120
|
+
const customValues = {};
|
|
121
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
122
|
+
if (ALLOWED_KEYS.includes(k)) continue;
|
|
123
|
+
if (!isValidEnvKey(k)) continue;
|
|
124
|
+
customValues[k] = String(v || "");
|
|
125
|
+
}
|
|
126
|
+
const masked = buildMasked({ ...values, ...customValues });
|
|
127
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
128
|
+
return res.end(JSON.stringify({
|
|
129
|
+
ok: true,
|
|
130
|
+
envPath,
|
|
131
|
+
keys: ALLOWED_KEYS,
|
|
132
|
+
values,
|
|
133
|
+
customValues,
|
|
134
|
+
masked,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function handleOpenClawEnvSet(req, res) {
|
|
139
|
+
if (!isLocalRequest(req)) {
|
|
140
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
141
|
+
return res.end(JSON.stringify({ ok: false, error: "local requests only" }));
|
|
142
|
+
}
|
|
143
|
+
const rawBody = await collectBody(req, res);
|
|
144
|
+
if (rawBody === null) return;
|
|
145
|
+
let body;
|
|
146
|
+
try { body = JSON.parse(rawBody); } catch {
|
|
147
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
148
|
+
return res.end(JSON.stringify({ ok: false, error: "invalid JSON body" }));
|
|
149
|
+
}
|
|
150
|
+
const updates = body?.updates && typeof body.updates === "object" ? body.updates : null;
|
|
151
|
+
if (!updates) {
|
|
152
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
153
|
+
return res.end(JSON.stringify({ ok: false, error: "updates object required" }));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const warnings = [];
|
|
157
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
158
|
+
const val = String(v ?? "").trim();
|
|
159
|
+
if (!val) continue;
|
|
160
|
+
if (k === "OPENAI_API_KEY" && !val.startsWith("sk-")) {
|
|
161
|
+
warnings.push(`${k} should start with "sk-". The value you provided doesn't look like a valid OpenAI API key.`);
|
|
162
|
+
}
|
|
163
|
+
if (k === "ANTHROPIC_API_KEY" && !val.startsWith("sk-ant-")) {
|
|
164
|
+
warnings.push(`${k} should start with "sk-ant-". The value you provided doesn't look like a valid Anthropic key.`);
|
|
165
|
+
}
|
|
166
|
+
if (k === "GROQ_API_KEY" && !val.startsWith("gsk_")) {
|
|
167
|
+
warnings.push(`${k} should start with "gsk_". The value you provided doesn't look like a valid Groq key.`);
|
|
168
|
+
}
|
|
169
|
+
if (k === "PERPLEXITY_API_KEY" && !val.startsWith("pplx-")) {
|
|
170
|
+
warnings.push(`${k} should start with "pplx-". The value you provided doesn't look like a valid Perplexity key.`);
|
|
171
|
+
}
|
|
172
|
+
if (/API_KEY$/i.test(k) && val.length < 20) {
|
|
173
|
+
warnings.push(`${k} looks too short to be a real API key (${val.length} chars).`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (warnings.length) {
|
|
177
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
178
|
+
return res.end(JSON.stringify({ ok: false, error: "Invalid API key format", warnings }));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const envPath = resolveEnvPath();
|
|
182
|
+
const currentRaw = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
183
|
+
const current = parseEnv(currentRaw);
|
|
184
|
+
const next = { ...current };
|
|
185
|
+
const applied = {};
|
|
186
|
+
const managedKeys = new Set(ALLOWED_KEYS);
|
|
187
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
188
|
+
if (!isValidEnvKey(k)) continue;
|
|
189
|
+
next[k] = String(v ?? "").trim();
|
|
190
|
+
applied[k] = next[k];
|
|
191
|
+
managedKeys.add(k);
|
|
192
|
+
}
|
|
193
|
+
const rendered = renderEnv(next, managedKeys, currentRaw);
|
|
194
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
195
|
+
fs.writeFileSync(envPath, rendered, { mode: 0o600 });
|
|
196
|
+
|
|
197
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
198
|
+
return res.end(JSON.stringify({
|
|
199
|
+
ok: true,
|
|
200
|
+
envPath,
|
|
201
|
+
applied,
|
|
202
|
+
masked: buildMasked(applied),
|
|
203
|
+
note: "Saved. Forge provider detection refreshes automatically on next requests.",
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
@@ -3,13 +3,72 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
-
import os from "node:os";
|
|
7
6
|
import path from "node:path";
|
|
8
7
|
import { ROOT as PROJECT_ROOT } from "../../lib/paths.mjs";
|
|
8
|
+
import { openClawSkillsDirCandidates as resolveOpenClawSkillsDirCandidates } from "../../lib/openclaw-paths.mjs";
|
|
9
9
|
import { getStorage } from "../storage/index.mjs";
|
|
10
10
|
import { requireSkillWriteAuth } from "../middleware/auth.mjs";
|
|
11
11
|
import { collectBody } from "../middleware/body-limit.mjs";
|
|
12
12
|
|
|
13
|
+
function openClawSkillsDirCandidates() {
|
|
14
|
+
return resolveOpenClawSkillsDirCandidates();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getOpenClawSkillsDirForRead() {
|
|
18
|
+
const candidates = openClawSkillsDirCandidates();
|
|
19
|
+
const existing = candidates.find((p) => fs.existsSync(p));
|
|
20
|
+
return existing || candidates[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getOpenClawSkillsDirForWrite() {
|
|
24
|
+
const candidates = openClawSkillsDirCandidates();
|
|
25
|
+
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseFrontmatter(content) {
|
|
29
|
+
const match = String(content || "").match(/^---\n([\s\S]*?)\n---\s*/);
|
|
30
|
+
if (!match) return { meta: {}, body: String(content || "") };
|
|
31
|
+
const meta = {};
|
|
32
|
+
for (const line of match[1].split("\n")) {
|
|
33
|
+
const idx = line.indexOf(":");
|
|
34
|
+
if (idx <= 0) continue;
|
|
35
|
+
const key = line.slice(0, idx).trim();
|
|
36
|
+
let val = line.slice(idx + 1).trim();
|
|
37
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
38
|
+
val = val.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
if (key) meta[key] = val;
|
|
41
|
+
}
|
|
42
|
+
return { meta, body: String(content || "").slice(match[0].length) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listOpenClawInstalledSkills(limit = 2000) {
|
|
46
|
+
const skillsDir = getOpenClawSkillsDirForRead();
|
|
47
|
+
const out = [];
|
|
48
|
+
if (!skillsDir || !fs.existsSync(skillsDir)) return { skillsDir, skills: out };
|
|
49
|
+
let names = [];
|
|
50
|
+
try { names = fs.readdirSync(skillsDir); } catch { return { skillsDir, skills: out }; }
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
if (out.length >= limit) break;
|
|
53
|
+
const skillMd = path.join(skillsDir, name, "SKILL.md");
|
|
54
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(skillMd, "utf8");
|
|
57
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
58
|
+
const desc = String(meta.description || body.split(/\r?\n/).find((l) => l.trim()) || "").trim().slice(0, 300);
|
|
59
|
+
out.push({
|
|
60
|
+
slug: toSlug(name || meta.name || ""),
|
|
61
|
+
name: String(meta.name || name).trim() || name,
|
|
62
|
+
version: safeVersion(meta.version || "") || "",
|
|
63
|
+
description: desc,
|
|
64
|
+
source: "openclaw-installed",
|
|
65
|
+
path: path.join(skillsDir, name),
|
|
66
|
+
});
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
return { skillsDir, skills: out };
|
|
70
|
+
}
|
|
71
|
+
|
|
13
72
|
function toSlug(input) {
|
|
14
73
|
return String(input || "").toLowerCase().trim()
|
|
15
74
|
.replace(/®/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -113,7 +172,8 @@ function upsertUserSkill(store, auth, skillcard, sourceUrl, createdAt) {
|
|
|
113
172
|
function installOpenClawSkillCard(skillcard, fallbackSlug = "") {
|
|
114
173
|
const slugValue = toSlug(skillcard?.slug || fallbackSlug || skillcard?.name || "");
|
|
115
174
|
if (!slugValue) throw new Error("invalid slug for OpenClaw install");
|
|
116
|
-
const
|
|
175
|
+
const root = getOpenClawSkillsDirForWrite();
|
|
176
|
+
const skillDir = path.join(root, slugValue);
|
|
117
177
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
118
178
|
const doc = String(skillcard?.documentation_md || "").trim();
|
|
119
179
|
const name = String(skillcard?.name || slugValue).trim();
|
|
@@ -135,7 +195,7 @@ function installOpenClawSkillCard(skillcard, fallbackSlug = "") {
|
|
|
135
195
|
function uninstallOpenClawSkillBySlug(slug) {
|
|
136
196
|
const s = toSlug(slug || "");
|
|
137
197
|
if (!s) return { removed: null, missing: null };
|
|
138
|
-
const skillDir = path.join(
|
|
198
|
+
const skillDir = path.join(getOpenClawSkillsDirForWrite(), s);
|
|
139
199
|
const skillMd = path.join(skillDir, "SKILL.md");
|
|
140
200
|
if (!fs.existsSync(skillMd)) {
|
|
141
201
|
return { removed: null, missing: { slug: s, reason: "SKILL.md not found" } };
|
|
@@ -148,6 +208,23 @@ function uninstallOpenClawSkillBySlug(slug) {
|
|
|
148
208
|
return { removed: { slug: s, skillDir }, missing: null };
|
|
149
209
|
}
|
|
150
210
|
|
|
211
|
+
export function handleOpenClawInstalledSkills(req, res, reqUrl) {
|
|
212
|
+
try {
|
|
213
|
+
const limit = Math.min(5000, Math.max(1, Number(reqUrl.searchParams.get("limit") || 2000)));
|
|
214
|
+
const result = listOpenClawInstalledSkills(limit);
|
|
215
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
216
|
+
return res.end(JSON.stringify({
|
|
217
|
+
ok: true,
|
|
218
|
+
skillsDir: result.skillsDir || null,
|
|
219
|
+
total: result.skills.length,
|
|
220
|
+
skills: result.skills,
|
|
221
|
+
}));
|
|
222
|
+
} catch (err) {
|
|
223
|
+
res.writeHead(500, { "content-type": "application/json; charset=utf-8" });
|
|
224
|
+
return res.end(JSON.stringify({ ok: false, error: err.message || "openclaw installed scan failed" }));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
151
228
|
export function handleSkillsSearch(req, res, reqUrl) {
|
|
152
229
|
try {
|
|
153
230
|
const store = getStorage();
|
package/ui/forge/css/forge.css
CHANGED
|
@@ -65,6 +65,9 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
65
65
|
.forge-viewport{
|
|
66
66
|
position:relative;flex:1 1 0%;min-height:0;background:#000;overflow:hidden;
|
|
67
67
|
}
|
|
68
|
+
.forge-viewport.forge-drop-active{
|
|
69
|
+
box-shadow:inset 0 0 0 2px rgba(99,215,255,.55), inset 0 0 32px rgba(99,215,255,.18);
|
|
70
|
+
}
|
|
68
71
|
.forge-viewport canvas{display:block;width:100%;height:100%}
|
|
69
72
|
#forgeLabelLayer{position:absolute;inset:0;pointer-events:none;overflow:hidden}
|
|
70
73
|
#forgeLabelLayer *{pointer-events:none}
|
|
@@ -113,12 +116,13 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
113
116
|
display:grid;grid-template-columns:35fr 65fr;gap:0;
|
|
114
117
|
border-top:1px solid var(--border);
|
|
115
118
|
max-height:320px;min-height:200px;
|
|
119
|
+
overflow:hidden;
|
|
116
120
|
}
|
|
117
121
|
.forge-panel{
|
|
118
122
|
background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(0,0,0,.3)),var(--surface2);
|
|
119
123
|
border-right:1px solid var(--border);
|
|
120
124
|
box-shadow:var(--panel-shadow);
|
|
121
|
-
overflow:hidden;display:flex;flex-direction:column;
|
|
125
|
+
overflow:hidden;display:flex;flex-direction:column;min-height:0;
|
|
122
126
|
}
|
|
123
127
|
.forge-panel:last-child{border-right:none}
|
|
124
128
|
|
|
@@ -172,7 +176,7 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
172
176
|
.forge-inspector-docs a{color:var(--neon-cyan)}
|
|
173
177
|
|
|
174
178
|
/* ── Chat ───────────────────────────────────────────────── */
|
|
175
|
-
.forge-chat{display:flex;flex-direction:column}
|
|
179
|
+
.forge-chat{display:flex;flex-direction:column;min-height:0}
|
|
176
180
|
.forge-chat-header{
|
|
177
181
|
padding:10px 16px;font-size:.78rem;font-weight:700;color:var(--text);
|
|
178
182
|
display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border);
|
|
@@ -182,6 +186,31 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
182
186
|
padding:1px 7px;border-radius:var(--radius);font-size:.6rem;font-weight:600;
|
|
183
187
|
background:rgba(207,255,4,.08);border:1px solid rgba(207,255,4,.2);color:var(--accent);
|
|
184
188
|
}
|
|
189
|
+
.forge-gateway-chip{
|
|
190
|
+
margin-left:auto;
|
|
191
|
+
font-family:var(--font-mono);
|
|
192
|
+
font-size:.58rem;
|
|
193
|
+
padding:2px 8px;
|
|
194
|
+
border-radius:999px;
|
|
195
|
+
border:1px solid var(--border);
|
|
196
|
+
color:var(--dim);
|
|
197
|
+
background:rgba(255,255,255,.03);
|
|
198
|
+
}
|
|
199
|
+
.forge-gateway-chip[data-state="ok"]{
|
|
200
|
+
border-color:rgba(0,255,100,.35);color:#9effc8;background:rgba(0,255,100,.08);
|
|
201
|
+
}
|
|
202
|
+
.forge-gateway-chip[data-state="warn"]{
|
|
203
|
+
border-color:rgba(255,178,71,.35);color:#ffcf9a;background:rgba(255,178,71,.08);
|
|
204
|
+
}
|
|
205
|
+
.forge-gateway-chip[data-state="err"]{
|
|
206
|
+
border-color:rgba(255,51,51,.35);color:#ff9e9e;background:rgba(255,51,51,.08);
|
|
207
|
+
}
|
|
208
|
+
.forge-chat-actions{display:flex;align-items:center;gap:6px}
|
|
209
|
+
.forge-chat-action-btn{
|
|
210
|
+
border:1px solid var(--border);background:rgba(0,0,0,.35);color:var(--dim);
|
|
211
|
+
border-radius:6px;font-size:.6rem;padding:3px 8px;cursor:pointer;
|
|
212
|
+
}
|
|
213
|
+
.forge-chat-action-btn:hover{color:var(--text);border-color:var(--neon-cyan)}
|
|
185
214
|
.forge-chat-messages{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column;gap:6px}
|
|
186
215
|
.chat-empty{
|
|
187
216
|
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
@@ -196,6 +225,36 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
196
225
|
.forge-chat .chat-msg-name{font-size:.72rem;font-weight:700;color:var(--accent)}
|
|
197
226
|
.forge-chat .chat-msg-time{font-family:var(--font-mono);font-size:.58rem;color:var(--dim);margin-left:auto}
|
|
198
227
|
.forge-chat .chat-msg-text{font-size:.75rem;line-height:1.5;color:var(--text);word-break:break-word}
|
|
228
|
+
.forge-chat .chat-msg-text.chat-pending{
|
|
229
|
+
color:#d3ecff;
|
|
230
|
+
}
|
|
231
|
+
.chat-pending-wrap{
|
|
232
|
+
display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap;
|
|
233
|
+
padding:5px 8px;border:1px solid rgba(99,215,255,.26);border-radius:7px;
|
|
234
|
+
background:linear-gradient(180deg,rgba(99,215,255,.08),rgba(99,215,255,.03));
|
|
235
|
+
}
|
|
236
|
+
.chat-pending-label{
|
|
237
|
+
font-family:var(--font-mono);font-size:.64rem;letter-spacing:.02em;color:#bce9ff;
|
|
238
|
+
}
|
|
239
|
+
.chat-pending-dots{display:inline-flex;gap:3px;align-items:center}
|
|
240
|
+
.chat-pending-dots i{
|
|
241
|
+
width:5px;height:5px;border-radius:50%;display:block;background:rgba(99,215,255,.35);
|
|
242
|
+
animation:forgeChatDotPulse 1.2s ease-in-out infinite;
|
|
243
|
+
}
|
|
244
|
+
.chat-pending-dots i:nth-child(2){animation-delay:.15s}
|
|
245
|
+
.chat-pending-dots i:nth-child(3){animation-delay:.3s}
|
|
246
|
+
.chat-pending-stage{
|
|
247
|
+
font-size:.62rem;color:var(--dim);font-family:var(--font-mono);
|
|
248
|
+
}
|
|
249
|
+
.chat-pending-time{
|
|
250
|
+
font-size:.6rem;color:var(--neon-cyan);font-family:var(--font-mono);
|
|
251
|
+
border:1px solid rgba(99,215,255,.24);padding:1px 6px;border-radius:999px;
|
|
252
|
+
background:rgba(99,215,255,.08);
|
|
253
|
+
}
|
|
254
|
+
@keyframes forgeChatDotPulse{
|
|
255
|
+
0%,100%{transform:translateY(0);opacity:.45}
|
|
256
|
+
50%{transform:translateY(-2px);opacity:1}
|
|
257
|
+
}
|
|
199
258
|
.forge-chat .chat-msg-text strong{color:#eefad6;font-weight:700}
|
|
200
259
|
.forge-chat .chat-msg-text code{
|
|
201
260
|
font-family:var(--font-mono);font-size:.68rem;
|
|
@@ -213,6 +272,14 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
213
272
|
display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--border);
|
|
214
273
|
background:linear-gradient(180deg,rgba(207,255,4,.02),transparent),var(--surface2);
|
|
215
274
|
}
|
|
275
|
+
.forge-chat-quick{
|
|
276
|
+
display:flex;gap:6px;flex-wrap:wrap;padding:0 12px 8px;
|
|
277
|
+
}
|
|
278
|
+
.forge-chat-quick-btn{
|
|
279
|
+
border:1px solid var(--border);background:rgba(255,255,255,.02);color:var(--dim);
|
|
280
|
+
border-radius:999px;font-size:.58rem;padding:4px 9px;cursor:pointer;
|
|
281
|
+
}
|
|
282
|
+
.forge-chat-quick-btn:hover{color:var(--text);border-color:var(--accent)}
|
|
216
283
|
.forge-chat-input-bar input{
|
|
217
284
|
flex:1;padding:7px 12px;border-radius:var(--radius);border:1px solid var(--border);
|
|
218
285
|
background:#0b1622;color:var(--text);font-family:var(--font-display);font-size:.75rem;
|
|
@@ -279,6 +346,9 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
279
346
|
.forge-footer{
|
|
280
347
|
padding:14px 24px;text-align:center;font-size:.62rem;color:var(--dim);
|
|
281
348
|
border-top:1px solid rgba(207,255,4,.1);background:rgba(0,0,0,.3);
|
|
349
|
+
flex-shrink:0;
|
|
350
|
+
position:relative;
|
|
351
|
+
z-index:2;
|
|
282
352
|
}
|
|
283
353
|
.forge-footer .footer-links{
|
|
284
354
|
display:flex;gap:14px;justify-content:center;margin-top:6px;font-size:.58rem;
|
|
@@ -426,6 +496,82 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
426
496
|
.forge-page-btn:disabled{opacity:.3;cursor:not-allowed}
|
|
427
497
|
.forge-page-info{font-size:.58rem;color:var(--dim);font-family:var(--font-mono)}
|
|
428
498
|
|
|
499
|
+
/* ── OpenClaw Env Modal ─────────────────────────────────── */
|
|
500
|
+
.forge-env-btn{background:rgba(99,215,255,.08);border-color:rgba(99,215,255,.26);color:var(--neon-cyan)}
|
|
501
|
+
.forge-env-btn:hover{background:rgba(99,215,255,.14)}
|
|
502
|
+
.forge-env-backdrop{
|
|
503
|
+
position:fixed;inset:0;z-index:17;background:rgba(0,0,0,.55);
|
|
504
|
+
opacity:0;pointer-events:none;transition:opacity .2s ease;
|
|
505
|
+
}
|
|
506
|
+
.forge-env-backdrop[data-open="1"]{opacity:1;pointer-events:auto}
|
|
507
|
+
.forge-env-modal{
|
|
508
|
+
position:fixed;top:12%;right:22px;z-index:18;
|
|
509
|
+
width:min(620px,calc(100vw - 44px));max-height:76vh;
|
|
510
|
+
display:flex;flex-direction:column;
|
|
511
|
+
border:1px solid var(--border);border-radius:10px;
|
|
512
|
+
background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(0,0,0,.25)),var(--surface2);
|
|
513
|
+
box-shadow:0 18px 44px rgba(0,0,0,.55);
|
|
514
|
+
transform:translateY(-14px) scale(.98);
|
|
515
|
+
opacity:0;pointer-events:none;transition:all .2s ease;
|
|
516
|
+
}
|
|
517
|
+
.forge-env-modal[data-open="1"]{transform:translateY(0) scale(1);opacity:1;pointer-events:auto}
|
|
518
|
+
.forge-env-header{
|
|
519
|
+
padding:11px 14px;border-bottom:1px solid var(--border);
|
|
520
|
+
display:flex;align-items:center;justify-content:space-between;gap:10px;
|
|
521
|
+
}
|
|
522
|
+
.forge-env-title{font-size:.9rem;font-weight:700;color:var(--text);font-family:var(--font-display)}
|
|
523
|
+
.forge-env-close{
|
|
524
|
+
width:28px;height:28px;border-radius:7px;border:1px solid var(--border);
|
|
525
|
+
background:rgba(0,0,0,.45);color:var(--dim);cursor:pointer;
|
|
526
|
+
}
|
|
527
|
+
.forge-env-close:hover{color:var(--text);border-color:var(--neon-pink)}
|
|
528
|
+
.forge-env-note{
|
|
529
|
+
padding:10px 14px;border-bottom:1px solid var(--border);
|
|
530
|
+
font-size:.67rem;color:var(--dim);line-height:1.55;
|
|
531
|
+
}
|
|
532
|
+
.forge-env-status{
|
|
533
|
+
margin:10px 14px 2px;padding:7px 10px;border-radius:7px;
|
|
534
|
+
border:1px solid var(--border);font-family:var(--font-mono);font-size:.62rem;
|
|
535
|
+
background:rgba(0,0,0,.25);color:var(--dim);
|
|
536
|
+
}
|
|
537
|
+
.forge-env-status[data-state="ok"]{
|
|
538
|
+
border-color:rgba(0,255,100,.28);color:#9effc8;background:rgba(0,255,100,.07);
|
|
539
|
+
}
|
|
540
|
+
.forge-env-status[data-state="warn"]{
|
|
541
|
+
border-color:rgba(255,178,71,.32);color:#ffcf9a;background:rgba(255,178,71,.08);
|
|
542
|
+
}
|
|
543
|
+
.forge-env-status[data-state="err"]{
|
|
544
|
+
border-color:rgba(255,51,51,.3);color:#ff9e9e;background:rgba(255,51,51,.08);
|
|
545
|
+
}
|
|
546
|
+
.forge-env-form{
|
|
547
|
+
padding:10px 14px;overflow:auto;display:flex;flex-direction:column;gap:9px;
|
|
548
|
+
}
|
|
549
|
+
.forge-env-row{display:grid;grid-template-columns:220px 1fr;gap:10px;align-items:center}
|
|
550
|
+
.forge-env-key{font-family:var(--font-mono);font-size:.63rem;color:var(--text)}
|
|
551
|
+
.forge-env-value-wrap{display:flex;align-items:center;gap:8px}
|
|
552
|
+
.forge-env-input{
|
|
553
|
+
width:100%;height:34px;border-radius:7px;border:1px solid var(--border);
|
|
554
|
+
background:rgba(0,0,0,.35);color:var(--text);padding:0 10px;
|
|
555
|
+
font-family:var(--font-mono);font-size:.67rem;
|
|
556
|
+
}
|
|
557
|
+
.forge-env-input:focus{outline:none;border-color:var(--accent)}
|
|
558
|
+
.forge-env-subtitle{
|
|
559
|
+
margin:6px 0 2px;font-family:var(--font-mono);font-size:.62rem;
|
|
560
|
+
color:var(--dim);text-transform:uppercase;letter-spacing:.04em;
|
|
561
|
+
}
|
|
562
|
+
.forge-env-add-row{display:grid;grid-template-columns:1fr 1fr auto;gap:8px}
|
|
563
|
+
.forge-env-remove{
|
|
564
|
+
height:34px;padding:0 10px;border-radius:7px;border:1px solid rgba(255,51,51,.3);
|
|
565
|
+
background:rgba(255,51,51,.08);color:#ff9e9e;font-size:.62rem;cursor:pointer;
|
|
566
|
+
}
|
|
567
|
+
.forge-env-remove:hover{background:rgba(255,51,51,.15)}
|
|
568
|
+
.forge-env-footer{
|
|
569
|
+
padding:10px 14px;border-top:1px solid var(--border);
|
|
570
|
+
display:flex;justify-content:flex-end;gap:8px;
|
|
571
|
+
}
|
|
572
|
+
.forge-btn-secondary{border-color:var(--border);color:var(--dim);background:rgba(255,255,255,.03)}
|
|
573
|
+
.forge-btn-secondary:hover{background:rgba(255,255,255,.08);color:var(--text)}
|
|
574
|
+
|
|
429
575
|
/* ── Toast Notifications ────────────────────────────────── */
|
|
430
576
|
.forge-toast-container{
|
|
431
577
|
position:fixed;bottom:24px;right:24px;z-index:100;
|
|
@@ -528,10 +674,15 @@ a{color:var(--neon-cyan);text-decoration:none}
|
|
|
528
674
|
@media(max-width:979px){
|
|
529
675
|
.forge-viewport{min-height:50vh}
|
|
530
676
|
.forge-header{flex-wrap:wrap;gap:8px}
|
|
677
|
+
.forge-env-modal{top:8%;right:12px;width:calc(100vw - 24px)}
|
|
678
|
+
.forge-env-row{grid-template-columns:1fr;gap:5px}
|
|
679
|
+
.forge-chat-header{flex-wrap:wrap}
|
|
680
|
+
.forge-gateway-chip{order:3;margin-left:0}
|
|
531
681
|
}
|
|
532
682
|
@media(max-width:639px){
|
|
533
683
|
.forge-viewport{min-height:40vh}
|
|
534
684
|
.forge-hud{top:8px;right:8px}
|
|
535
685
|
.forge-header{padding:10px 14px}
|
|
536
686
|
.forge-progress-bar{width:80px}
|
|
687
|
+
.forge-env-modal{top:5%;max-height:84vh}
|
|
537
688
|
}
|
package/ui/forge/index.html
CHANGED
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
<button class="forge-hud-btn forge-share-btn" id="forgeShareBtn" type="button" data-ac-tip="Share to X">
|
|
56
56
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
|
|
57
57
|
</button>
|
|
58
|
+
<button class="forge-hud-btn forge-env-btn" id="forgeEnvBtn" type="button" data-ac-tip="OpenClaw env">
|
|
59
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .17 1.7 1.7 0 0 0-.78 1.45V21a2 2 0 1 1-4 0v-.08a1.7 1.7 0 0 0-.78-1.45 1.7 1.7 0 0 0-1-.17 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.17-1 1.7 1.7 0 0 0-1.45-.78H3a2 2 0 1 1 0-4h.08a1.7 1.7 0 0 0 1.45-.78 1.7 1.7 0 0 0 .17-1 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.17 1.7 1.7 0 0 0 .78-1.45V3a2 2 0 1 1 4 0v.08a1.7 1.7 0 0 0 .78 1.45 1.7 1.7 0 0 0 1 .17 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9a1.7 1.7 0 0 0 .17 1 1.7 1.7 0 0 0 1.45.78H21a2 2 0 1 1 0 4h-.08a1.7 1.7 0 0 0-1.45.78 1.7 1.7 0 0 0-.17 1z"/></svg>
|
|
60
|
+
</button>
|
|
58
61
|
</div>
|
|
59
62
|
<div class="forge-watermark" id="forgeWatermark">
|
|
60
63
|
<span class="forge-watermark-logo">APECLAW</span>
|
|
@@ -94,8 +97,13 @@
|
|
|
94
97
|
<div class="forge-chat-header">
|
|
95
98
|
<span class="icon">💬</span> Agent Chat
|
|
96
99
|
<span class="badge" id="forgeChatBadge">0</span>
|
|
100
|
+
<span class="forge-gateway-chip" id="forgeGatewayChip" data-state="loading">Gateway: checking...</span>
|
|
101
|
+
<div class="forge-chat-actions">
|
|
102
|
+
<button class="forge-chat-action-btn" id="forgeGatewayRefresh" type="button">Refresh</button>
|
|
103
|
+
<button class="forge-chat-action-btn" id="forgeGatewayRestart" type="button">Restart</button>
|
|
104
|
+
</div>
|
|
97
105
|
</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;"
|
|
106
|
+
<div class="forge-chat-agent-indicator" id="forgeChatAgentIndicator" style="display:none;padding:4px 10px;font-size:11px;opacity:.7;font-family:'JetBrains Mono',monospace;">Connected via OpenClaw Gateway (main session)</div>
|
|
99
107
|
<div class="forge-chat-messages" id="forgeChatMessages">
|
|
100
108
|
<div class="chat-empty">
|
|
101
109
|
<div class="chat-empty-icon">💬</div>
|
|
@@ -106,6 +114,11 @@
|
|
|
106
114
|
<input type="text" id="forgeChatInput" placeholder="Message your agent..." maxlength="500">
|
|
107
115
|
<button class="chat-send-btn" id="forgeChatSendBtn" type="button">Send</button>
|
|
108
116
|
</div>
|
|
117
|
+
<div class="forge-chat-quick" id="forgeChatQuick">
|
|
118
|
+
<button class="forge-chat-quick-btn" data-prompt="Show my installed skills and what each one can do.">Installed skills</button>
|
|
119
|
+
<button class="forge-chat-quick-btn" data-prompt="Check OpenClaw gateway health and summarize any issue.">Gateway health</button>
|
|
120
|
+
<button class="forge-chat-quick-btn" data-prompt="Recommend three high-impact skills I should install next.">Next skills</button>
|
|
121
|
+
</div>
|
|
109
122
|
<div class="chat-input-meta">
|
|
110
123
|
<span class="chat-counter" id="forgeChatCounter">0/500</span>
|
|
111
124
|
</div>
|
|
@@ -124,7 +137,7 @@
|
|
|
124
137
|
<input type="text" id="forgeAuthAgentId" placeholder="Agent ID" autocomplete="off">
|
|
125
138
|
<input type="password" id="forgeAuthAgentToken" placeholder="Agent Token" autocomplete="off">
|
|
126
139
|
</div>
|
|
127
|
-
<div class="forge-auth-hint">
|
|
140
|
+
<div class="forge-auth-hint">Optional for local install/uninstall. Add credentials only if you want global dashboard/telemetry posting. Get from <code>ape-claw clawbot list</code></div>
|
|
128
141
|
</div>
|
|
129
142
|
<div class="forge-drawer-search">
|
|
130
143
|
<input type="search" id="forgeSkillSearch" placeholder="Search Library of Alexandria skills..." autocomplete="off">
|
|
@@ -150,6 +163,26 @@
|
|
|
150
163
|
<div class="forge-drawer-pagination" id="forgeDrawerPagination"></div>
|
|
151
164
|
</aside>
|
|
152
165
|
|
|
166
|
+
<!-- OpenClaw env modal -->
|
|
167
|
+
<div class="forge-env-backdrop" id="forgeEnvBackdrop"></div>
|
|
168
|
+
<aside class="forge-env-modal" id="forgeEnvModal" data-open="0">
|
|
169
|
+
<div class="forge-env-header">
|
|
170
|
+
<h2 class="forge-env-title">OpenClaw Gateway Env</h2>
|
|
171
|
+
<button class="forge-env-close" id="forgeEnvClose" type="button">×</button>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="forge-env-note" id="forgeEnvNote">
|
|
174
|
+
Update model/provider env values used by OpenClaw and Forge takeover mode.
|
|
175
|
+
</div>
|
|
176
|
+
<div class="forge-env-status" id="forgeEnvProviderStatus" data-state="loading">
|
|
177
|
+
Checking active provider...
|
|
178
|
+
</div>
|
|
179
|
+
<div class="forge-env-form" id="forgeEnvForm"></div>
|
|
180
|
+
<div class="forge-env-footer">
|
|
181
|
+
<button class="forge-btn forge-btn-secondary" id="forgeEnvReload" type="button">Reload</button>
|
|
182
|
+
<button class="forge-btn forge-btn-accent" id="forgeEnvSave" type="button">Save & Apply</button>
|
|
183
|
+
</div>
|
|
184
|
+
</aside>
|
|
185
|
+
|
|
153
186
|
<!-- Toast notifications -->
|
|
154
187
|
<div class="forge-toast-container" id="forgeToasts"></div>
|
|
155
188
|
|
|
@@ -193,8 +226,9 @@
|
|
|
193
226
|
<div class="forge-offline-step">
|
|
194
227
|
<span class="forge-offline-step-num">2</span>
|
|
195
228
|
<div>
|
|
196
|
-
<strong>
|
|
197
|
-
<code>
|
|
229
|
+
<strong>Start local Forge (no repo required)</strong>
|
|
230
|
+
<code>npx ape-claw dashboard</code>
|
|
231
|
+
<span class="forge-offline-hint">This fetches/runs the packaged Forge UI server from npm and opens your local dashboard.</span>
|
|
198
232
|
</div>
|
|
199
233
|
</div>
|
|
200
234
|
<div class="forge-offline-step">
|
|
@@ -202,14 +236,14 @@
|
|
|
202
236
|
<div>
|
|
203
237
|
<strong>Open the Forge</strong>
|
|
204
238
|
<code>http://localhost:8787/forge</code>
|
|
205
|
-
<span class="forge-offline-hint">Your installed skills appear as 3D attachments on the robot automatically</span>
|
|
239
|
+
<span class="forge-offline-hint">Your installed skills appear as 3D attachments on the robot automatically. Optional dev setup: clone GitHub and run <code>npm run start:ui</code>.</span>
|
|
206
240
|
</div>
|
|
207
241
|
</div>
|
|
208
242
|
</div>
|
|
209
243
|
<div class="forge-offline-divider"></div>
|
|
210
244
|
<p class="forge-offline-note">
|
|
211
245
|
The Forge reads skills installed via <code>npx ape-claw skill install</code>.<br>
|
|
212
|
-
Skills are installed to <code>~/.openclaw/skills/</code> and synced to
|
|
246
|
+
Skills are installed to your active OpenClaw profile (for example <code>~/.openclaw/skills/</code>) and synced to that profile workspace automatically. <strong>Requires <a href="https://openclaw.ai" style="color:var(--accent)">OpenClaw</a>.</strong>
|
|
213
247
|
</p>
|
|
214
248
|
<div class="forge-offline-links">
|
|
215
249
|
<button class="forge-offline-btn forge-offline-btn-agent" id="forgeTryAgent" type="button">🦞 Try Our Agent</button>
|