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.
@@ -56,6 +56,39 @@ This document lists all environment variables used by ApeClaw, organized by comp
56
56
  | `APE_CLAW_INVITE_MAX_USES` | Maximum uses per invite token | No | `5` |
57
57
  | `APE_CLAW_POD_DIR` | Pod workspace directory path | No | Auto-detected |
58
58
 
59
+ ## Forge Agent Variables
60
+
61
+ The forge agent auto-detects your LLM provider. Set **any one** of the API key variables below — the first one found is used.
62
+
63
+ **LLM Provider (set one):**
64
+
65
+ | Variable | Provider | Default Model |
66
+ |----------|----------|---------------|
67
+ | `PERPLEXITY_API_KEY` | Perplexity Sonar (web-grounded) | `sonar-pro` |
68
+ | `OPENAI_API_KEY` | OpenAI | `gpt-4o` |
69
+ | `ANTHROPIC_API_KEY` | Anthropic Claude | `claude-sonnet-4-20250514` |
70
+ | `GROQ_API_KEY` | Groq | `llama-3.3-70b-versatile` |
71
+ | `TOGETHER_API_KEY` | Together AI | `meta-llama/Llama-3.3-70B-Instruct-Turbo` |
72
+ | `OLLAMA_HOST` | Ollama (local, no key needed) | `llama3.2` |
73
+
74
+ **Explicit override (any OpenAI-compatible endpoint):**
75
+
76
+ | Variable | Description | Default |
77
+ |----------|-------------|---------|
78
+ | `FORGE_LLM_API_URL` | Full chat completions URL | Auto-detected from provider |
79
+ | `FORGE_LLM_API_KEY` | API key for custom endpoint | Auto-detected from provider |
80
+ | `FORGE_LLM_MODEL` | Model name override | Auto-detected from provider |
81
+
82
+ **Agent identity:**
83
+
84
+ | Variable | Description | Default |
85
+ |----------|-------------|---------|
86
+ | `FORGE_AGENT_ID` | ClawBot agent ID | `"the-clawllector"` |
87
+ | `FORGE_AGENT_TOKEN` | Pre-provisioned ClawBot token for verified identity | Auto-registers on startup |
88
+ | `FORGE_AGENT_NAME` | Display name shown in chat | `"The Clawllector"` |
89
+
90
+ Without any LLM key the endpoint returns 503 and the forge chat falls back to the basic `/api/chat` relay.
91
+
59
92
  ## External Service Variables
60
93
 
61
94
  | Variable | Description | Required | Default |
@@ -117,6 +150,34 @@ export APE_CLAW_CORS_ORIGINS=https://apeclaw.ai
117
150
  export APE_CLAW_STORAGE=file # or "sqlite"
118
151
  ```
119
152
 
153
+ ### Forge Agent (Local) — pick any provider
154
+
155
+ ```bash
156
+ # Option A: OpenAI
157
+ export OPENAI_API_KEY=sk-...
158
+
159
+ # Option B: Anthropic
160
+ export ANTHROPIC_API_KEY=sk-ant-...
161
+
162
+ # Option C: Perplexity
163
+ export PERPLEXITY_API_KEY=pplx-...
164
+
165
+ # Option D: Groq (free tier available)
166
+ export GROQ_API_KEY=gsk_...
167
+
168
+ # Option E: Local Ollama (no key needed)
169
+ export OLLAMA_HOST=http://localhost:11434
170
+
171
+ # Option F: Any OpenAI-compatible endpoint
172
+ export FORGE_LLM_API_URL=https://your-endpoint.com/v1/chat/completions
173
+ export FORGE_LLM_API_KEY=your-key
174
+ export FORGE_LLM_MODEL=your-model
175
+
176
+ # Optional identity overrides:
177
+ export FORGE_AGENT_NAME="My Bot"
178
+ export FORGE_AGENT_ID=my-forge-bot
179
+ ```
180
+
120
181
  ### Pod Operations
121
182
 
122
183
  ```bash
@@ -15,7 +15,7 @@ Press Enter. 61 skills load in about four seconds.
15
15
  That's it. You have a working agent. No browsing a marketplace for an hour.
16
16
 
17
17
  ```
18
- npx ape-claw skill install --scope local
18
+ npx ape-claw skill install
19
19
  ```
20
20
 
21
21
  Here's what you actually get (thread)
@@ -152,8 +152,8 @@ Still in the library at apeclaw.ai/skills. Just not in the box you get on day on
152
152
 
153
153
  The prompt defaults to yes, but you have options:
154
154
 
155
- npx ape-claw skill install --scope local --starter-pack installs without asking
156
- npx ape-claw skill install --scope local --no-starter-pack skips it
155
+ npx ape-claw skill install --starter-pack installs without asking
156
+ npx ape-claw skill install --no-starter-pack skips it
157
157
  No flag means it asks you
158
158
 
159
159
  You can install it later. You can also skip it entirely and pick from 10,000+ skills one at a time.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ape-claw",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "ApeChain bridge and NFT execution CLI with telemetry for OpenClaw agents",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/cli.mjs CHANGED
@@ -144,15 +144,15 @@ function installApeClawSkill(args) {
144
144
  throw new Error(`Source skill missing at ${sourceSkillPath}`);
145
145
  }
146
146
 
147
- const scope = String(args.scope || "local").toLowerCase();
147
+ const scope = String(args.scope || "global").toLowerCase();
148
148
  const explicitSkillsDir = args["skills-dir"] ? String(args["skills-dir"]) : "";
149
149
  let skillsRoot;
150
150
  if (explicitSkillsDir) {
151
151
  skillsRoot = path.resolve(explicitSkillsDir);
152
152
  if (skillsRoot.includes("\0")) throw new Error("Invalid skills-dir path");
153
153
  }
154
- else if (scope === "global") skillsRoot = path.join(os.homedir(), ".openclaw", "skills");
155
- else skillsRoot = path.join(process.cwd(), ".cursor", "skills");
154
+ else if (scope === "local") skillsRoot = path.join(process.cwd(), ".cursor", "skills");
155
+ else skillsRoot = path.join(os.homedir(), ".openclaw", "skills");
156
156
 
157
157
  const targetSkillDir = path.join(skillsRoot, "ape-claw");
158
158
  const targetSkillPath = path.join(targetSkillDir, "SKILL.md");
@@ -211,6 +211,15 @@ function installApeClawSkill(args) {
211
211
  };
212
212
  }
213
213
 
214
+ function yamlSafe(val) {
215
+ const s = String(val);
216
+ if (!s) return '""';
217
+ if (/[:{}\[\]#&*!|>'"%@`,\n]/.test(s) || s.startsWith(" ") || s.endsWith(" ")) {
218
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
219
+ }
220
+ return s;
221
+ }
222
+
214
223
  function syncSkillToOpenClaw(cardObj, slug, skillsRoot) {
215
224
  const s = String(slug || cardObj?.slug || cardObj?.name || "").toLowerCase().trim()
216
225
  .replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -218,11 +227,15 @@ function syncSkillToOpenClaw(cardObj, slug, skillsRoot) {
218
227
  const rawDoc = String(cardObj?.documentation_md || "").trim();
219
228
  const displayName = String(cardObj?.name || s).trim();
220
229
  const versionValue = String(cardObj?.version || "1.0.0").trim();
221
- const descriptionValue = String(cardObj?.description || "").trim();
230
+ let descriptionValue = String(cardObj?.description || "").trim();
231
+ // Strip embedded YAML frontmatter that some imported skills have in their description
232
+ if (descriptionValue.startsWith("---")) {
233
+ descriptionValue = descriptionValue.replace(/^---[\s\S]*?---\s*/, "").trim();
234
+ }
235
+ if (!descriptionValue) descriptionValue = String(cardObj?.desc || displayName);
222
236
  const descOneLine = descriptionValue.replace(/\n/g, " ").slice(0, 300);
223
237
 
224
- // OpenClaw expects: name = slug, description in frontmatter
225
- const openclawFrontmatter = `---\nname: ${s}\nversion: ${versionValue}\ndescription: ${descOneLine}\n---\n`;
238
+ const openclawFrontmatter = `---\nname: ${s}\nversion: ${yamlSafe(versionValue)}\ndescription: ${yamlSafe(descOneLine)}\n---\n`;
226
239
 
227
240
  let content;
228
241
  if (rawDoc) {
@@ -237,7 +250,8 @@ function syncSkillToOpenClaw(cardObj, slug, skillsRoot) {
237
250
  fs.mkdirSync(skillDir, { recursive: true });
238
251
  fs.writeFileSync(path.join(skillDir, "SKILL.md"), content, "utf8");
239
252
 
240
- const openclawWorkspaceSkills = path.join(os.homedir(), ".openclaw", "workspace", "skills", s);
253
+ const homeDir = process.env.OPENCLAW_HOME || os.homedir();
254
+ const openclawWorkspaceSkills = path.join(homeDir, ".openclaw", "workspace", "skills", s);
241
255
  try {
242
256
  fs.mkdirSync(openclawWorkspaceSkills, { recursive: true });
243
257
  fs.writeFileSync(path.join(openclawWorkspaceSkills, "SKILL.md"), content, "utf8");
@@ -365,6 +379,63 @@ function safeSkillVersion(v) {
365
379
  return s;
366
380
  }
367
381
 
382
+ const TRUSTED_SKILL_API_HOSTS = new Set(["apeclaw.ai", "www.apeclaw.ai", "api.apeclaw.ai"]);
383
+
384
+ function resolveSkillApiBase(args = {}) {
385
+ const explicit = String(args.api || "").trim();
386
+ const fromEnv = String(process.env.APE_CLAW_API_URL || "").trim();
387
+ const raw = explicit || fromEnv || "https://apeclaw.ai";
388
+ let u;
389
+ try {
390
+ u = new URL(raw);
391
+ } catch {
392
+ throw new Error(`Invalid APE_CLAW_API_URL: ${raw}`);
393
+ }
394
+ const isLoopback = u.hostname === "localhost" || u.hostname === "127.0.0.1";
395
+ if (u.protocol !== "https:" && !(isLoopback && Boolean(args["allow-insecure-api"]))) {
396
+ throw new Error("Remote skill API must use HTTPS. For local dev only, use --allow-insecure-api with localhost.");
397
+ }
398
+ if (!TRUSTED_SKILL_API_HOSTS.has(u.hostname) && !Boolean(args["allow-custom-api"])) {
399
+ throw new Error(`Untrusted skill API host: ${u.hostname}. Use --allow-custom-api to override.`);
400
+ }
401
+ return u.origin.replace(/\/+$/, "");
402
+ }
403
+
404
+ function assertRemoteSkillCardSafe({ requestedSlug, card, skillMeta = null, asJson = false, allowUnvetted = false, allowHighRisk = false }) {
405
+ if (!card || typeof card !== "object") throw new Error("Remote API returned invalid skill card object");
406
+ const normalizedRequested = toSlug(requestedSlug);
407
+ const normalizedCardSlug = toSlug(card.slug || card.name || "");
408
+ if (!normalizedCardSlug) throw new Error("Remote skill card missing slug/name");
409
+ if (normalizedCardSlug !== normalizedRequested) {
410
+ throw new Error(`Remote skill slug mismatch (requested=${normalizedRequested}, received=${normalizedCardSlug})`);
411
+ }
412
+ const version = safeSkillVersion(card.version || "1.0.0");
413
+ if (!version) throw new Error("Remote skill version is invalid");
414
+ const description = String(card.description || "").trim();
415
+ if (!description) throw new Error("Remote skill description is required");
416
+ const documentation = String(card.documentation_md || "");
417
+ if (Buffer.byteLength(documentation, "utf8") > 300_000) {
418
+ throw new Error("Remote skill documentation is too large (>300KB)");
419
+ }
420
+ if (/[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(documentation)) {
421
+ throw new Error("Remote skill documentation contains control characters");
422
+ }
423
+
424
+ const metaRiskTierRaw = Number(skillMeta?.riskTier ?? card?.constraints?.riskTier ?? card?.riskTier ?? 2);
425
+ const metaRiskTier = Number.isFinite(metaRiskTierRaw) ? Math.max(1, Math.min(3, Math.round(metaRiskTierRaw))) : 2;
426
+ if (metaRiskTier >= 3 && !allowHighRisk) {
427
+ throw new Error(`Remote skill risk tier ${metaRiskTier} requires explicit --allow-high-risk`);
428
+ }
429
+ // If API metadata is present, require vetting by default.
430
+ if (skillMeta && skillMeta.vettedOk !== true && !allowUnvetted) {
431
+ throw new Error("Remote skill is not vetted. Use --allow-unvetted to install anyway.");
432
+ }
433
+
434
+ if (!asJson && skillMeta && skillMeta.vettedOk === true) {
435
+ console.log("\x1b[2m Security: vetted skill metadata confirmed by API.\x1b[0m");
436
+ }
437
+ }
438
+
368
439
  function resolveBundledSkillFile(packageRoot, slug) {
369
440
  const skillsDataDir = path.join(packageRoot, "data", "skills");
370
441
  const target = path.join(skillsDataDir, `${slug}.json`);
@@ -389,16 +460,35 @@ function resolveBundledSkillFile(packageRoot, slug) {
389
460
  return "";
390
461
  }
391
462
 
392
- async function fetchSkillFromApi(slug) {
393
- const apiBase = process.env.APE_CLAW_API_URL || "https://apeclaw.ai";
394
- const url = `${apiBase}/api/skills/${encodeURIComponent(slug)}`;
463
+ async function fetchSkillFromApi(slug, args = {}) {
464
+ const apiBase = resolveSkillApiBase(args);
465
+ const url = `${apiBase}/api/skills/get?slug=${encodeURIComponent(slug)}`;
395
466
  try {
396
467
  const res = await fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15000) });
397
- if (!res.ok) return null;
468
+ if (!res.ok) return { card: null, skillMeta: null, url, apiBase };
398
469
  const json = await res.json();
399
- return json?.card || json?.skill || json || null;
470
+ if (!json?.ok) return { card: null, skillMeta: null, url, apiBase };
471
+
472
+ const skillMeta = json?.skill && typeof json.skill === "object" ? json.skill : null;
473
+ // Prefer the full card JSON; fall back to synthesising a minimal card from index metadata
474
+ let card = json?.card && typeof json.card === "object" ? json.card : null;
475
+ if (!card && skillMeta) {
476
+ card = {
477
+ name: skillMeta.name || slug,
478
+ slug: skillMeta.slug || slug,
479
+ version: "1.0.0",
480
+ description: skillMeta.description || skillMeta.name || slug,
481
+ riskTier: skillMeta.riskTier ?? 2,
482
+ provenance: skillMeta.provenance || { publisher: "imported", signed: false },
483
+ constraints: { riskTier: skillMeta.riskTier ?? 2 },
484
+ documentation_md: skillMeta.description
485
+ ? `# ${skillMeta.name || slug}\n\n${skillMeta.description}`
486
+ : `# ${skillMeta.name || slug}\n`,
487
+ };
488
+ }
489
+ return { card, skillMeta, url, apiBase };
400
490
  } catch {
401
- return null;
491
+ return { card: null, skillMeta: null, url, apiBase };
402
492
  }
403
493
  }
404
494
 
@@ -411,7 +501,8 @@ function resolveHumanizerDependencySlug(packageRoot) {
411
501
  }
412
502
 
413
503
  function installOpenClawSkillCard(cardObj, fallbackSlug = "") {
414
- const skillsRoot = path.join(ROOT, ".cursor", "skills");
504
+ const homeDir = process.env.OPENCLAW_HOME || os.homedir();
505
+ const skillsRoot = path.join(homeDir, ".openclaw", "skills");
415
506
  return syncSkillToOpenClaw(cardObj, fallbackSlug, skillsRoot);
416
507
  }
417
508
 
@@ -607,7 +698,16 @@ async function main() {
607
698
  } catch (bundledErr) {
608
699
  // Not in bundled data — fetch from API
609
700
  if (!asJson) console.log(`\x1b[2m Skill not bundled locally, fetching from API…\x1b[0m`);
610
- const card = await fetchSkillFromApi(requestedSkillSlug);
701
+ let remote;
702
+ try {
703
+ remote = await fetchSkillFromApi(requestedSkillSlug, args);
704
+ } catch (apiErr) {
705
+ if (asJson) return print({ ok: false, error: `Skill "${requestedSkillSlug}" rejected: ${apiErr.message}` }, true);
706
+ console.error(`\x1b[31m ✗ ${apiErr.message}\x1b[0m`);
707
+ process.exitCode = 1;
708
+ return;
709
+ }
710
+ const card = remote?.card || null;
611
711
  if (!card || typeof card !== "object" || (!card.name && !card.slug)) {
612
712
  if (asJson) return print({ ok: false, error: `Skill "${requestedSkillSlug}" not found (bundled: ${bundledErr.message}, API: not found)` }, true);
613
713
  console.error(`\x1b[31m ✗ Skill "${requestedSkillSlug}" not found in bundled library or API.\x1b[0m`);
@@ -615,6 +715,22 @@ async function main() {
615
715
  process.exitCode = 1;
616
716
  return;
617
717
  }
718
+ try {
719
+ assertRemoteSkillCardSafe({
720
+ requestedSlug: requestedSkillSlug,
721
+ card,
722
+ skillMeta: remote?.skillMeta || null,
723
+ asJson,
724
+ allowUnvetted: Boolean(args["allow-unvetted"]),
725
+ allowHighRisk: Boolean(args["allow-high-risk"]),
726
+ });
727
+ } catch (safeErr) {
728
+ if (asJson) return print({ ok: false, error: safeErr.message }, true);
729
+ console.error(`\x1b[31m ✗ ${safeErr.message}\x1b[0m`);
730
+ console.error(`\x1b[2m Use --allow-unvetted and/or --allow-high-risk only if you trust this source.\x1b[0m`);
731
+ process.exitCode = 1;
732
+ return;
733
+ }
618
734
  // Write fetched card to a temp file and install it
619
735
  const tmpDir = path.join(packageRoot, "data", "skills");
620
736
  fs.mkdirSync(tmpDir, { recursive: true });
@@ -2184,7 +2300,7 @@ async function main() {
2184
2300
  "bridge execute (autonomous)": "ape-claw bridge execute --request <requestId> --execute --autonomous --json",
2185
2301
  "bridge status": "ape-claw bridge status --request <requestId> --json",
2186
2302
  "allowlist audit": "ape-claw allowlist audit --json",
2187
- "skill install": "ape-claw skill install [<slug>] [--scope local] [--starter-pack | --no-starter-pack] --json",
2303
+ "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",
2188
2304
  "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",
2189
2305
  "v2 skill publish": "ape-claw v2 skill publish --rpc <url> --privateKey 0x... --registry 0x... --skillId <id> --file <skillcard.json> [--uri ipfs://...] [--riskTier 1] --json",
2190
2306
  "v2 intent create": "ape-claw v2 intent create --rpc <url> --privateKey 0x... --intents 0x... --payload '{...}' [--expiresAt <unixSec>] --json",
@@ -30,6 +30,7 @@ import {
30
30
  handleChatStream, handleChatGet, handleChatRooms,
31
31
  handleChatPost, handleChatReact,
32
32
  } from "./routes/chat.mjs";
33
+ import { handleForgeChat, handleForgeStatus, initForgeAgent } from "./routes/forge-agent.mjs";
33
34
  import { handleV2ReceiptGet, handleV2Config } from "./routes/v2.mjs";
34
35
  import { handlePodStatus, handlePodStop, handlePodFiles, handleStarterPack } from "./routes/pod.mjs";
35
36
  import {
@@ -41,7 +42,7 @@ import {
41
42
  handleIndex, handleStaticFile,
42
43
  } from "./routes/static.mjs";
43
44
 
44
- const PORT = Number(process.env.APE_CLAW_UI_PORT || 8787);
45
+ const PORT = Number(process.env.PORT || process.env.APE_CLAW_UI_PORT || 8787);
45
46
  const BIND_HOST = String(process.env.APE_CLAW_BIND_HOST || "").trim();
46
47
 
47
48
  const RL_READ = { limit: 60, windowMs: 60_000, keyPrefix: "read" };
@@ -58,7 +59,8 @@ if (!process.env.OPENSEA_API_KEY) {
58
59
 
59
60
  initStorage();
60
61
  initSseBroadcast();
61
- logger.info("Storage initialized, SSE broadcast active");
62
+ initForgeAgent();
63
+ logger.info("Storage initialized, SSE broadcast active, forge agent ready");
62
64
 
63
65
  function safeHandler(fn) {
64
66
  return (req, res, ...args) => {
@@ -122,6 +124,8 @@ const server = http.createServer((req, res) => {
122
124
  if (pathname === "/api/invites/create" && req.method === "POST") return safeHandler(handleInviteCreate)(req, res);
123
125
  if (pathname === "/api/clawbots/register" && req.method === "POST") return safeHandler(handleClawbotsRegister)(req, res);
124
126
  if (pathname === "/api/events" && req.method === "POST") return safeHandler(handlePostEvent)(req, res);
127
+ if (pathname === "/api/forge/status" && req.method === "GET") return safeHandler(handleForgeStatus)(req, res);
128
+ if (pathname === "/api/forge/chat" && req.method === "POST") return safeHandler(handleForgeChat)(req, res);
125
129
  if (pathname === "/api/chat/stream") return safeHandler(handleChatStream)(req, res, reqUrl);
126
130
  if (pathname === "/api/chat" && req.method === "GET") return safeHandler(handleChatGet)(req, res, reqUrl);
127
131
  if (pathname === "/api/chat/rooms" && req.method === "GET") return safeHandler(handleChatRooms)(req, res, reqUrl);