ape-claw 0.1.4 → 0.1.6

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.4",
3
+ "version": "0.1.6",
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");
@@ -227,7 +227,12 @@ function syncSkillToOpenClaw(cardObj, slug, skillsRoot) {
227
227
  const rawDoc = String(cardObj?.documentation_md || "").trim();
228
228
  const displayName = String(cardObj?.name || s).trim();
229
229
  const versionValue = String(cardObj?.version || "1.0.0").trim();
230
- 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);
231
236
  const descOneLine = descriptionValue.replace(/\n/g, " ").slice(0, 300);
232
237
 
233
238
  const openclawFrontmatter = `---\nname: ${s}\nversion: ${yamlSafe(versionValue)}\ndescription: ${yamlSafe(descOneLine)}\n---\n`;
@@ -245,7 +250,8 @@ function syncSkillToOpenClaw(cardObj, slug, skillsRoot) {
245
250
  fs.mkdirSync(skillDir, { recursive: true });
246
251
  fs.writeFileSync(path.join(skillDir, "SKILL.md"), content, "utf8");
247
252
 
248
- 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);
249
255
  try {
250
256
  fs.mkdirSync(openclawWorkspaceSkills, { recursive: true });
251
257
  fs.writeFileSync(path.join(openclawWorkspaceSkills, "SKILL.md"), content, "utf8");
@@ -373,6 +379,68 @@ function safeSkillVersion(v) {
373
379
  return s;
374
380
  }
375
381
 
382
+ const TRUSTED_SKILL_API_HOSTS = new Set([
383
+ "apeclaw.ai", "www.apeclaw.ai", "api.apeclaw.ai",
384
+ "acceptable-cat-production.up.railway.app",
385
+ ]);
386
+
387
+ const SKILL_API_FALLBACK = "https://acceptable-cat-production.up.railway.app";
388
+
389
+ function resolveSkillApiBase(args = {}) {
390
+ const explicit = String(args.api || "").trim();
391
+ const fromEnv = String(process.env.APE_CLAW_API_URL || "").trim();
392
+ const raw = explicit || fromEnv || "https://apeclaw.ai";
393
+ let u;
394
+ try {
395
+ u = new URL(raw);
396
+ } catch {
397
+ throw new Error(`Invalid APE_CLAW_API_URL: ${raw}`);
398
+ }
399
+ const isLoopback = u.hostname === "localhost" || u.hostname === "127.0.0.1";
400
+ if (u.protocol !== "https:" && !(isLoopback && Boolean(args["allow-insecure-api"]))) {
401
+ throw new Error("Remote skill API must use HTTPS. For local dev only, use --allow-insecure-api with localhost.");
402
+ }
403
+ if (!TRUSTED_SKILL_API_HOSTS.has(u.hostname) && !Boolean(args["allow-custom-api"])) {
404
+ throw new Error(`Untrusted skill API host: ${u.hostname}. Use --allow-custom-api to override.`);
405
+ }
406
+ return u.origin.replace(/\/+$/, "");
407
+ }
408
+
409
+ function assertRemoteSkillCardSafe({ requestedSlug, card, skillMeta = null, asJson = false, allowUnvetted = false, allowHighRisk = false }) {
410
+ if (!card || typeof card !== "object") throw new Error("Remote API returned invalid skill card object");
411
+ const normalizedRequested = toSlug(requestedSlug);
412
+ const normalizedCardSlug = toSlug(card.slug || card.name || "");
413
+ if (!normalizedCardSlug) throw new Error("Remote skill card missing slug/name");
414
+ if (normalizedCardSlug !== normalizedRequested) {
415
+ throw new Error(`Remote skill slug mismatch (requested=${normalizedRequested}, received=${normalizedCardSlug})`);
416
+ }
417
+ const version = safeSkillVersion(card.version || "1.0.0");
418
+ if (!version) throw new Error("Remote skill version is invalid");
419
+ const description = String(card.description || "").trim();
420
+ if (!description) throw new Error("Remote skill description is required");
421
+ const documentation = String(card.documentation_md || "");
422
+ if (Buffer.byteLength(documentation, "utf8") > 300_000) {
423
+ throw new Error("Remote skill documentation is too large (>300KB)");
424
+ }
425
+ if (/[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(documentation)) {
426
+ throw new Error("Remote skill documentation contains control characters");
427
+ }
428
+
429
+ const metaRiskTierRaw = Number(skillMeta?.riskTier ?? card?.constraints?.riskTier ?? card?.riskTier ?? 2);
430
+ const metaRiskTier = Number.isFinite(metaRiskTierRaw) ? Math.max(1, Math.min(3, Math.round(metaRiskTierRaw))) : 2;
431
+ if (metaRiskTier >= 3 && !allowHighRisk) {
432
+ throw new Error(`Remote skill risk tier ${metaRiskTier} requires explicit --allow-high-risk`);
433
+ }
434
+ // If API metadata is present, require vetting by default.
435
+ if (skillMeta && skillMeta.vettedOk !== true && !allowUnvetted) {
436
+ throw new Error("Remote skill is not vetted. Use --allow-unvetted to install anyway.");
437
+ }
438
+
439
+ if (!asJson && skillMeta && skillMeta.vettedOk === true) {
440
+ console.log("\x1b[2m Security: vetted skill metadata confirmed by API.\x1b[0m");
441
+ }
442
+ }
443
+
376
444
  function resolveBundledSkillFile(packageRoot, slug) {
377
445
  const skillsDataDir = path.join(packageRoot, "data", "skills");
378
446
  const target = path.join(skillsDataDir, `${slug}.json`);
@@ -397,17 +465,49 @@ function resolveBundledSkillFile(packageRoot, slug) {
397
465
  return "";
398
466
  }
399
467
 
400
- async function fetchSkillFromApi(slug) {
401
- const apiBase = process.env.APE_CLAW_API_URL || "https://apeclaw.ai";
402
- const url = `${apiBase}/api/skills/${encodeURIComponent(slug)}`;
468
+ async function fetchSkillFromApiOnce(slug, apiBase) {
469
+ const url = `${apiBase}/api/skills/get?slug=${encodeURIComponent(slug)}`;
470
+ const res = await fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15000) });
471
+ if (!res.ok) return null;
472
+ const ct = String(res.headers.get("content-type") || "");
473
+ if (!ct.includes("json")) return null;
474
+ const json = await res.json();
475
+ if (!json?.ok) return null;
476
+
477
+ const skillMeta = json?.skill && typeof json.skill === "object" ? json.skill : null;
478
+ let card = json?.card && typeof json.card === "object" ? json.card : null;
479
+ if (!card && skillMeta) {
480
+ card = {
481
+ name: skillMeta.name || slug,
482
+ slug: skillMeta.slug || slug,
483
+ version: "1.0.0",
484
+ description: skillMeta.description || skillMeta.name || slug,
485
+ riskTier: skillMeta.riskTier ?? 2,
486
+ provenance: skillMeta.provenance || { publisher: "imported", signed: false },
487
+ constraints: { riskTier: skillMeta.riskTier ?? 2 },
488
+ documentation_md: skillMeta.description
489
+ ? `# ${skillMeta.name || slug}\n\n${skillMeta.description}`
490
+ : `# ${skillMeta.name || slug}\n`,
491
+ };
492
+ }
493
+ return { card, skillMeta, url, apiBase };
494
+ }
495
+
496
+ async function fetchSkillFromApi(slug, args = {}) {
497
+ const apiBase = resolveSkillApiBase(args);
403
498
  try {
404
- const res = await fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15000) });
405
- if (!res.ok) return null;
406
- const json = await res.json();
407
- return json?.card || json?.skill || json || null;
408
- } catch {
409
- return null;
499
+ const result = await fetchSkillFromApiOnce(slug, apiBase);
500
+ if (result) return result;
501
+ } catch { /* primary failed, try fallback */ }
502
+
503
+ if (apiBase !== SKILL_API_FALLBACK) {
504
+ try {
505
+ const fallbackResult = await fetchSkillFromApiOnce(slug, SKILL_API_FALLBACK);
506
+ if (fallbackResult) return fallbackResult;
507
+ } catch { /* fallback also failed */ }
410
508
  }
509
+
510
+ return { card: null, skillMeta: null, url: `${apiBase}/api/skills/get?slug=${encodeURIComponent(slug)}`, apiBase };
411
511
  }
412
512
 
413
513
  function resolveHumanizerDependencySlug(packageRoot) {
@@ -419,7 +519,8 @@ function resolveHumanizerDependencySlug(packageRoot) {
419
519
  }
420
520
 
421
521
  function installOpenClawSkillCard(cardObj, fallbackSlug = "") {
422
- const skillsRoot = path.join(ROOT, ".cursor", "skills");
522
+ const homeDir = process.env.OPENCLAW_HOME || os.homedir();
523
+ const skillsRoot = path.join(homeDir, ".openclaw", "skills");
423
524
  return syncSkillToOpenClaw(cardObj, fallbackSlug, skillsRoot);
424
525
  }
425
526
 
@@ -615,7 +716,16 @@ async function main() {
615
716
  } catch (bundledErr) {
616
717
  // Not in bundled data — fetch from API
617
718
  if (!asJson) console.log(`\x1b[2m Skill not bundled locally, fetching from API…\x1b[0m`);
618
- const card = await fetchSkillFromApi(requestedSkillSlug);
719
+ let remote;
720
+ try {
721
+ remote = await fetchSkillFromApi(requestedSkillSlug, args);
722
+ } catch (apiErr) {
723
+ if (asJson) return print({ ok: false, error: `Skill "${requestedSkillSlug}" rejected: ${apiErr.message}` }, true);
724
+ console.error(`\x1b[31m ✗ ${apiErr.message}\x1b[0m`);
725
+ process.exitCode = 1;
726
+ return;
727
+ }
728
+ const card = remote?.card || null;
619
729
  if (!card || typeof card !== "object" || (!card.name && !card.slug)) {
620
730
  if (asJson) return print({ ok: false, error: `Skill "${requestedSkillSlug}" not found (bundled: ${bundledErr.message}, API: not found)` }, true);
621
731
  console.error(`\x1b[31m ✗ Skill "${requestedSkillSlug}" not found in bundled library or API.\x1b[0m`);
@@ -623,6 +733,22 @@ async function main() {
623
733
  process.exitCode = 1;
624
734
  return;
625
735
  }
736
+ try {
737
+ assertRemoteSkillCardSafe({
738
+ requestedSlug: requestedSkillSlug,
739
+ card,
740
+ skillMeta: remote?.skillMeta || null,
741
+ asJson,
742
+ allowUnvetted: Boolean(args["allow-unvetted"]),
743
+ allowHighRisk: Boolean(args["allow-high-risk"]),
744
+ });
745
+ } catch (safeErr) {
746
+ if (asJson) return print({ ok: false, error: safeErr.message }, true);
747
+ console.error(`\x1b[31m ✗ ${safeErr.message}\x1b[0m`);
748
+ console.error(`\x1b[2m Use --allow-unvetted and/or --allow-high-risk only if you trust this source.\x1b[0m`);
749
+ process.exitCode = 1;
750
+ return;
751
+ }
626
752
  // Write fetched card to a temp file and install it
627
753
  const tmpDir = path.join(packageRoot, "data", "skills");
628
754
  fs.mkdirSync(tmpDir, { recursive: true });
@@ -2192,7 +2318,7 @@ async function main() {
2192
2318
  "bridge execute (autonomous)": "ape-claw bridge execute --request <requestId> --execute --autonomous --json",
2193
2319
  "bridge status": "ape-claw bridge status --request <requestId> --json",
2194
2320
  "allowlist audit": "ape-claw allowlist audit --json",
2195
- "skill install": "ape-claw skill install [<slug>] [--scope local] [--starter-pack | --no-starter-pack] --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",
2196
2322
  "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",
2197
2323
  "v2 skill publish": "ape-claw v2 skill publish --rpc <url> --privateKey 0x... --registry 0x... --skillId <id> --file <skillcard.json> [--uri ipfs://...] [--riskTier 1] --json",
2198
2324
  "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);