ape-claw 0.1.0

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.
Files changed (114) hide show
  1. package/.cursor/skills/ape-claw/SKILL.md +322 -0
  2. package/LICENSE +21 -0
  3. package/README.md +826 -0
  4. package/allowlists/opensea-slug-overrides.json +13 -0
  5. package/allowlists/recommended.apechain.json +322 -0
  6. package/config/clawbots.example.json +3 -0
  7. package/config/policy.example.json +27 -0
  8. package/data/starter-pack-bundle.json +1 -0
  9. package/data/starter-pack.json +495 -0
  10. package/docs/ACP_BOUNTIES.md +108 -0
  11. package/docs/APECLAW_V2_ALPHA.md +206 -0
  12. package/docs/AUTONOMY_AND_SUBSTRATE.md +69 -0
  13. package/docs/CLAWBOTS_AND_INVITES.md +102 -0
  14. package/docs/CLI_GUIDE.md +124 -0
  15. package/docs/CONTRIBUTING.md +130 -0
  16. package/docs/DASHBOARD_GUIDE.md +108 -0
  17. package/docs/GLOBAL_BACKEND.md +145 -0
  18. package/docs/ONCHAIN_V2_GUIDE.md +140 -0
  19. package/docs/PRODUCT_OVERVIEW.md +127 -0
  20. package/docs/README.md +40 -0
  21. package/docs/SKILLCARDS_AND_IMPORTER.md +147 -0
  22. package/docs/STARTER_PACK.md +297 -0
  23. package/docs/SUPPORTED_NETWORKS.md +58 -0
  24. package/docs/TELEMETRY_AND_EVENTS.md +103 -0
  25. package/docs/THE_POD_RUNNER.md +198 -0
  26. package/docs/V1_WORKFLOWS.md +108 -0
  27. package/docs/V2_ONCHAIN_SKILLS.md +157 -0
  28. package/docs/WEB4_PLAN_STATUS.md +95 -0
  29. package/docs/WEB4_SWARM_MODEL.md +104 -0
  30. package/docs/archive/AUTONOMY_AND_SUBSTRATE.md +66 -0
  31. package/docs/archive/WEB4_PLAN_STATUS.md +93 -0
  32. package/docs/archive/WEB4_SWARM_MODEL.md +98 -0
  33. package/docs/developer/01-architecture.md +345 -0
  34. package/docs/developer/02-contracts.md +1034 -0
  35. package/docs/developer/03-writing-modules.md +513 -0
  36. package/docs/developer/04-skillcard-spec.md +336 -0
  37. package/docs/developer/05-backend-api.md +1079 -0
  38. package/docs/developer/06-telemetry.md +798 -0
  39. package/docs/developer/07-testing.md +546 -0
  40. package/docs/developer/08-contributing.md +211 -0
  41. package/docs/operator/01-quickstart.md +49 -0
  42. package/docs/operator/02-dashboard.md +174 -0
  43. package/docs/operator/03-cli-reference.md +818 -0
  44. package/docs/operator/04-skills-library.md +169 -0
  45. package/docs/operator/05-pod-operations.md +314 -0
  46. package/docs/operator/06-deployment.md +299 -0
  47. package/docs/operator/07-safety-and-policy.md +311 -0
  48. package/docs/operator/08-troubleshooting.md +457 -0
  49. package/docs/operator/09-env-reference.md +238 -0
  50. package/docs/social/STARTER_PACK_THREAD.md +209 -0
  51. package/package.json +77 -0
  52. package/skillcards/import-sources.json +93 -0
  53. package/skillcards/seed/acp-bounty-poll.v1.json +38 -0
  54. package/skillcards/seed/acp-bounty-post.v1.json +55 -0
  55. package/skillcards/seed/acp-browse.v1.json +41 -0
  56. package/skillcards/seed/acp-fulfill-and-route.v1.json +56 -0
  57. package/skillcards/seed/apeclaw-bridge-relay.v1.json +46 -0
  58. package/skillcards/seed/apeclaw-nft-autobuy.v1.json +60 -0
  59. package/skillcards/seed/apeclaw-receipt-recorder.v1.json +64 -0
  60. package/skillcards/seed/humanizer.v1.json +74 -0
  61. package/skillcards/seed/otherside-navigator.v1.json +116 -0
  62. package/skillcards/seed/stonkbrokers-launcher.v1.json +280 -0
  63. package/skillcards/seed/walkie-p2p.v1.json +66 -0
  64. package/src/cli/index.mjs +8 -0
  65. package/src/cli.mjs +1929 -0
  66. package/src/lib/bridge-relay.mjs +294 -0
  67. package/src/lib/clawbots.mjs +94 -0
  68. package/src/lib/io.mjs +36 -0
  69. package/src/lib/market.mjs +233 -0
  70. package/src/lib/nft-opensea.mjs +159 -0
  71. package/src/lib/paths.mjs +17 -0
  72. package/src/lib/pod-init.mjs +40 -0
  73. package/src/lib/policy.mjs +112 -0
  74. package/src/lib/rpc.mjs +49 -0
  75. package/src/lib/telemetry.mjs +92 -0
  76. package/src/lib/v2-onchain-abi.mjs +294 -0
  77. package/src/lib/v2-skillcard.mjs +27 -0
  78. package/src/server/index.mjs +169 -0
  79. package/src/server/logger.mjs +21 -0
  80. package/src/server/middleware/auth.mjs +90 -0
  81. package/src/server/middleware/body-limit.mjs +35 -0
  82. package/src/server/middleware/cors.mjs +33 -0
  83. package/src/server/middleware/rate-limit.mjs +44 -0
  84. package/src/server/routes/chat.mjs +178 -0
  85. package/src/server/routes/clawbots.mjs +182 -0
  86. package/src/server/routes/events.mjs +95 -0
  87. package/src/server/routes/health.mjs +72 -0
  88. package/src/server/routes/pod.mjs +64 -0
  89. package/src/server/routes/quotes.mjs +161 -0
  90. package/src/server/routes/skills.mjs +239 -0
  91. package/src/server/routes/static.mjs +161 -0
  92. package/src/server/routes/v2.mjs +48 -0
  93. package/src/server/sse.mjs +73 -0
  94. package/src/server/storage/file-backend.mjs +295 -0
  95. package/src/server/storage/index.mjs +37 -0
  96. package/src/server/storage/sqlite-backend.mjs +380 -0
  97. package/src/telemetry-server.mjs +1604 -0
  98. package/ui/css/dashboard.css +792 -0
  99. package/ui/css/skills.css +689 -0
  100. package/ui/docs.html +840 -0
  101. package/ui/favicon-180.png +0 -0
  102. package/ui/favicon-192.png +0 -0
  103. package/ui/favicon-32.png +0 -0
  104. package/ui/favicon-lobster.png +0 -0
  105. package/ui/favicon.svg +10 -0
  106. package/ui/index.html +2957 -0
  107. package/ui/js/dashboard.js +1766 -0
  108. package/ui/js/skills.js +1621 -0
  109. package/ui/pod.html +909 -0
  110. package/ui/shared/motion.css +286 -0
  111. package/ui/shared/motion.js +170 -0
  112. package/ui/shared/sidebar-nav.css +379 -0
  113. package/ui/shared/sidebar-nav.js +137 -0
  114. package/ui/skills.html +2879 -0
package/src/cli.mjs ADDED
@@ -0,0 +1,1929 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawnSync } from "node:child_process";
7
+ import { readJson, writeJson, randomId } from "./lib/io.mjs";
8
+ import {
9
+ QUOTES_PATH,
10
+ BRIDGE_REQUESTS_PATH,
11
+ POLICY_PATH,
12
+ ALLOWLIST_PATH,
13
+ OPENSEA_OVERRIDES_PATH,
14
+ } from "./lib/paths.mjs";
15
+ import {
16
+ loadPolicy,
17
+ loadAllowlist,
18
+ normalizeAllowlist,
19
+ resolveCollectionTarget,
20
+ enforceBuyPolicy,
21
+ enforceBridgePolicy,
22
+ } from "./lib/policy.mjs";
23
+ import { emitEvent } from "./lib/telemetry.mjs";
24
+ import { getListings, enrichAllowlistWithOpenSea } from "./lib/market.mjs";
25
+ import { quoteBridgeRelay, executeBridgeRelay, getBridgeRelayStatus } from "./lib/bridge-relay.mjs";
26
+ import { getListingFulfillmentData, executeListingFulfillmentTx } from "./lib/nft-opensea.mjs";
27
+ import { resolveRpcUrl } from "./lib/rpc.mjs";
28
+ import { verifyClawbot, registerClawbot, listClawbots, loadClawbotsConfig } from "./lib/clawbots.mjs";
29
+ import { createInterface } from "node:readline";
30
+ import { createPublicClient, createWalletClient, http, getContract, keccak256, toHex } from "viem";
31
+ import { privateKeyToAccount } from "viem/accounts";
32
+ import { SkillNFT_ABI, SkillRegistry_ABI, IntentRegistry_ABI, ReceiptRegistry_ABI, PodVault_ABI, AgentAccount_ABI } from "./lib/v2-onchain-abi.mjs";
33
+ import { computeSkillcardContentHash, computeSkillVersionHash, readSkillcardJson, stableJsonStringify } from "./lib/v2-skillcard.mjs";
34
+ import { initPodWorkspace } from "./lib/pod-init.mjs";
35
+
36
+ function parseArgs(argv) {
37
+ const out = { _: [] };
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const a = argv[i];
40
+ if (a.startsWith("--")) {
41
+ const key = a.slice(2);
42
+ const next = argv[i + 1];
43
+ if (!next || next.startsWith("--")) out[key] = true;
44
+ else {
45
+ out[key] = next;
46
+ i++;
47
+ }
48
+ } else out._.push(a);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function print(data, asJson) {
54
+ if (asJson) console.log(JSON.stringify(data, null, 2));
55
+ else if (typeof data === "string") console.log(data);
56
+ else console.log(JSON.stringify(data, null, 2));
57
+ }
58
+
59
+ // Global agentId ref — set after identity resolution, used by fail()
60
+ let _agentId = "local-cli";
61
+ let _asJson = false;
62
+
63
+ function fail(message, command, payload = {}) {
64
+ emitEvent({
65
+ eventType: "policy.blocked",
66
+ agentId: _agentId,
67
+ command,
68
+ payload,
69
+ ok: false,
70
+ error: message,
71
+ });
72
+ if (_asJson) {
73
+ console.log(JSON.stringify({ ok: false, error: message, command }, null, 2));
74
+ } else {
75
+ console.error(message);
76
+ }
77
+ process.exit(1);
78
+ }
79
+
80
+ function loadState(filePath) {
81
+ const obj = readJson(filePath, {});
82
+ return obj && typeof obj === "object" ? obj : {};
83
+ }
84
+
85
+ function isoDay(value = new Date()) {
86
+ return new Date(value).toISOString().slice(0, 10);
87
+ }
88
+
89
+ function expectedBuyConfirmPhrase(quote) {
90
+ return `BUY ${quote.collection} #${quote.tokenId} ${quote.priceApe} APE`;
91
+ }
92
+
93
+ function expectedBridgeConfirmPhrase(req) {
94
+ return `BRIDGE ${req.amount} ${req.token} ${req.from}->${req.to}`;
95
+ }
96
+
97
+ function authStorePath() {
98
+ return path.join(os.homedir(), ".ape-claw", "auth.json");
99
+ }
100
+
101
+ function loadAuthStore() {
102
+ const p = authStorePath();
103
+ const data = readJson(p, {});
104
+ return data && typeof data === "object" ? data : {};
105
+ }
106
+
107
+ function writeAuthStore(data) {
108
+ const p = authStorePath();
109
+ fs.mkdirSync(path.dirname(p), { recursive: true });
110
+ fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 0o600 });
111
+ }
112
+
113
+ function maskSecret(value) {
114
+ const v = String(value || "");
115
+ if (!v) return "";
116
+ if (v.length <= 8) return "*".repeat(v.length);
117
+ return `${v.slice(0, 4)}...${v.slice(-4)}`;
118
+ }
119
+
120
+ function spentTodayFromQuotes(quotes, dayKey) {
121
+ return Object.values(quotes).reduce((sum, q) => {
122
+ if (!q || !q.executedAt || !q.executed) return sum;
123
+ if (isoDay(q.executedAt) !== dayKey) return sum;
124
+ return sum + (Number(q.priceApe) || 0);
125
+ }, 0);
126
+ }
127
+
128
+ function spentTodayFromBridge(requests, dayKey) {
129
+ return Object.values(requests).reduce((sum, r) => {
130
+ if (!r || !r.submittedAt) return sum;
131
+ if (r.status === "quoted") return sum;
132
+ if (isoDay(r.submittedAt) !== dayKey) return sum;
133
+ return sum + (Number(r.amount) || 0);
134
+ }, 0);
135
+ }
136
+
137
+ function installApeClawSkill(args) {
138
+ const here = path.dirname(fileURLToPath(import.meta.url));
139
+ const packageRoot = path.resolve(here, "..");
140
+ const sourceSkillPath = path.join(packageRoot, ".cursor", "skills", "ape-claw", "SKILL.md");
141
+ if (!fs.existsSync(sourceSkillPath)) {
142
+ throw new Error(`Source skill missing at ${sourceSkillPath}`);
143
+ }
144
+
145
+ const scope = String(args.scope || "local").toLowerCase();
146
+ const explicitSkillsDir = args["skills-dir"] ? String(args["skills-dir"]) : "";
147
+ let skillsRoot;
148
+ if (explicitSkillsDir) skillsRoot = path.resolve(explicitSkillsDir);
149
+ else if (scope === "global") skillsRoot = path.join(os.homedir(), ".openclaw", "skills");
150
+ else skillsRoot = path.join(process.cwd(), ".cursor", "skills");
151
+
152
+ const targetSkillDir = path.join(skillsRoot, "ape-claw");
153
+ const targetSkillPath = path.join(targetSkillDir, "SKILL.md");
154
+ fs.mkdirSync(targetSkillDir, { recursive: true });
155
+ fs.copyFileSync(sourceSkillPath, targetSkillPath);
156
+
157
+ const localPolicyPath = path.join(process.cwd(), "config", "policy.json");
158
+ const examplePolicyPath = path.join(packageRoot, "config", "policy.example.json");
159
+ if (!fs.existsSync(localPolicyPath) && fs.existsSync(examplePolicyPath)) {
160
+ fs.mkdirSync(path.dirname(localPolicyPath), { recursive: true });
161
+ fs.copyFileSync(examplePolicyPath, localPolicyPath);
162
+ }
163
+
164
+ const localAllowlistPath = path.join(process.cwd(), "allowlists", "recommended.apechain.json");
165
+ const sourceAllowlistPath = path.join(packageRoot, "allowlists", "recommended.apechain.json");
166
+ if (!fs.existsSync(localAllowlistPath) && fs.existsSync(sourceAllowlistPath)) {
167
+ fs.mkdirSync(path.dirname(localAllowlistPath), { recursive: true });
168
+ fs.copyFileSync(sourceAllowlistPath, localAllowlistPath);
169
+ }
170
+
171
+ const localClawbotsPath = path.join(process.cwd(), "config", "clawbots.json");
172
+ const exampleClawbotsPath = path.join(packageRoot, "config", "clawbots.example.json");
173
+ if (!fs.existsSync(localClawbotsPath) && fs.existsSync(exampleClawbotsPath)) {
174
+ fs.copyFileSync(exampleClawbotsPath, localClawbotsPath);
175
+ }
176
+
177
+ const check = spawnSync("openclaw", ["skills", "check"], {
178
+ cwd: process.cwd(),
179
+ encoding: "utf8",
180
+ });
181
+ const openclawAvailable = !check.error && typeof check.status === "number";
182
+ const openclawCheckOk = openclawAvailable && check.status === 0;
183
+
184
+ return {
185
+ installed: true,
186
+ scope,
187
+ sourceSkillPath,
188
+ skillsRoot,
189
+ packageRoot,
190
+ skillPath: targetSkillPath,
191
+ starterPack: { installed: 0, skipped: false, categories: {}, skills: [] },
192
+ openclawAvailable,
193
+ openclawCheckOk,
194
+ openclawCheckOutput: openclawAvailable
195
+ ? (check.stdout || check.stderr || "").trim()
196
+ : "openclaw CLI not found in PATH",
197
+ next: openclawAvailable
198
+ ? [
199
+ "openclaw skills list",
200
+ "openclaw skills check",
201
+ ]
202
+ : [
203
+ "Install OpenClaw CLI or add it to PATH",
204
+ "Run: openclaw skills list",
205
+ ],
206
+ };
207
+ }
208
+
209
+ function installStarterPack({ packageRoot, skillsRoot }) {
210
+ const starterPackInstalled = [];
211
+ const bundlePath = path.join(packageRoot, "data", "starter-pack-bundle.json");
212
+ const legacyPackPath = path.join(packageRoot, "data", "starter-pack.json");
213
+
214
+ const starterDir = path.join(skillsRoot, "starter-pack");
215
+ fs.mkdirSync(starterDir, { recursive: true });
216
+ const manifestEntries = [];
217
+ let curatedSkills = [];
218
+ if (fs.existsSync(legacyPackPath)) {
219
+ try {
220
+ const curatedPack = JSON.parse(fs.readFileSync(legacyPackPath, "utf8"));
221
+ curatedSkills = Array.isArray(curatedPack?.skills) ? curatedPack.skills : [];
222
+ } catch {}
223
+ }
224
+
225
+ if (fs.existsSync(bundlePath)) {
226
+ const bundle = JSON.parse(fs.readFileSync(bundlePath, "utf8"));
227
+ const bundleSkills = Array.isArray(bundle?.skills) ? bundle.skills : [];
228
+ const bundleBySlug = new Map();
229
+ for (const item of bundleSkills) {
230
+ const s = String(item?.slug || "").trim();
231
+ if (s) bundleBySlug.set(s, item);
232
+ }
233
+ const installFromBundle = curatedSkills.length
234
+ ? curatedSkills.map((c) => bundleBySlug.get(String(c?.slug || "").trim())).filter(Boolean)
235
+ : bundleSkills;
236
+ for (const entry of installFromBundle) {
237
+ const slug = String(entry.slug || "").trim();
238
+ if (!slug || !entry.fullJson) continue;
239
+ const targetJson = path.join(starterDir, `${slug}.json`);
240
+ fs.writeFileSync(targetJson, JSON.stringify(entry.fullJson));
241
+ manifestEntries.push({
242
+ slug,
243
+ name: entry.name,
244
+ category: entry.category,
245
+ description: entry.description,
246
+ vettedOk: entry.vettedOk,
247
+ onchain: entry.onchain,
248
+ installedAt: new Date().toISOString(),
249
+ });
250
+ starterPackInstalled.push({
251
+ slug,
252
+ name: entry.name,
253
+ category: entry.category,
254
+ description: entry.description,
255
+ });
256
+ }
257
+ } else if (fs.existsSync(legacyPackPath)) {
258
+ const pack = JSON.parse(fs.readFileSync(legacyPackPath, "utf8"));
259
+ const skillsDataDir = path.join(packageRoot, "data", "skills");
260
+ for (const entry of (pack.skills || [])) {
261
+ const slug = String(entry.slug || "").trim();
262
+ if (!slug) continue;
263
+ const sourceJson = path.join(skillsDataDir, `${slug}.json`);
264
+ if (!fs.existsSync(sourceJson)) continue;
265
+ const targetJson = path.join(starterDir, `${slug}.json`);
266
+ fs.copyFileSync(sourceJson, targetJson);
267
+ manifestEntries.push({
268
+ slug,
269
+ name: entry.name,
270
+ category: entry.category,
271
+ description: entry.description,
272
+ vettedOk: entry.vettedOk,
273
+ onchain: entry.onchain,
274
+ installedAt: new Date().toISOString(),
275
+ });
276
+ starterPackInstalled.push({
277
+ slug,
278
+ name: entry.name,
279
+ category: entry.category,
280
+ description: entry.description,
281
+ });
282
+ }
283
+ }
284
+
285
+ if (manifestEntries.length > 0) {
286
+ const manifest = {
287
+ version: 1,
288
+ installedAt: new Date().toISOString(),
289
+ count: manifestEntries.length,
290
+ skills: manifestEntries,
291
+ };
292
+ fs.writeFileSync(
293
+ path.join(starterDir, "_manifest.json"),
294
+ JSON.stringify(manifest, null, 2),
295
+ );
296
+ }
297
+
298
+ const categorySummary = {};
299
+ for (const s of starterPackInstalled) {
300
+ categorySummary[s.category] = (categorySummary[s.category] || 0) + 1;
301
+ }
302
+
303
+ return {
304
+ installed: starterPackInstalled.length,
305
+ skipped: false,
306
+ categories: categorySummary,
307
+ skills: starterPackInstalled,
308
+ };
309
+ }
310
+
311
+ function promptUser(question) {
312
+ return new Promise((resolve) => {
313
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
314
+ rl.question(question, (answer) => {
315
+ rl.close();
316
+ resolve(answer.trim().toLowerCase());
317
+ });
318
+ });
319
+ }
320
+
321
+ function resolveRemoteApiBase(args = {}) {
322
+ const explicit = String(args.api || "").trim();
323
+ const fromEnv = String(process.env.APE_CLAW_API_BASE || process.env.APE_CLAW_TELEMETRY_URL || "").trim();
324
+ const base = explicit || fromEnv;
325
+ return base ? base.replace(/\/+$/, "") : "";
326
+ }
327
+
328
+ async function registerClawbotRemote({ apiBase, agentId, displayName, registrationKey, invite }) {
329
+ const headers = { "content-type": "application/json" };
330
+ if (registrationKey) headers["x-registration-key"] = registrationKey;
331
+ const res = await fetch(`${apiBase}/api/clawbots/register`, {
332
+ method: "POST",
333
+ headers,
334
+ body: JSON.stringify({ agentId, name: displayName, ...(invite ? { invite } : {}) }),
335
+ });
336
+ const text = await res.text();
337
+ let data = null;
338
+ try { data = JSON.parse(text); } catch {}
339
+ if (!res.ok) {
340
+ const msg = data?.error || `HTTP ${res.status}`;
341
+ throw new Error(`remote registration failed: ${msg}`);
342
+ }
343
+ return data;
344
+ }
345
+
346
+ async function verifyClawbotRemote({ apiBase, agentId, agentToken }) {
347
+ const res = await fetch(`${apiBase}/api/clawbots/verify`, {
348
+ method: "POST",
349
+ headers: {
350
+ "content-type": "application/json",
351
+ "x-agent-id": agentId,
352
+ "x-agent-token": agentToken,
353
+ },
354
+ body: JSON.stringify({ agentId }),
355
+ });
356
+ const text = await res.text();
357
+ let data = null;
358
+ try { data = JSON.parse(text); } catch {}
359
+ if (!res.ok) {
360
+ const msg = data?.reason || data?.error || `HTTP ${res.status}`;
361
+ throw new Error(`remote verify failed: ${msg}`);
362
+ }
363
+ return data;
364
+ }
365
+
366
+ async function main() {
367
+ const args = parseArgs(process.argv.slice(2));
368
+ const [group, sub] = args._;
369
+ const asJson = Boolean(args.json);
370
+ _asJson = asJson;
371
+ const command = `ape-claw ${args._.join(" ")}`.trim();
372
+
373
+ // Allow skill installation in any directory without local ape-claw config.
374
+ if (group === "skill" && sub === "install") {
375
+ const result = installApeClawSkill(args);
376
+ emitEvent({ eventType: "skill.install.ran", command, dryRun: true, result });
377
+
378
+ // ── Starter Pack: 61 curated, globally-applicable skills (opt-in) ──
379
+ const skipStarterPack = Boolean(args["no-starter-pack"]);
380
+ const forceStarterPack = Boolean(args["starter-pack"]);
381
+ const bundlePath = path.join(result.packageRoot, "data", "starter-pack-bundle.json");
382
+ const legacyPackPath = path.join(result.packageRoot, "data", "starter-pack.json");
383
+ const starterPackAvailable = fs.existsSync(bundlePath) || fs.existsSync(legacyPackPath);
384
+
385
+ let installPack = false;
386
+ if (skipStarterPack || !starterPackAvailable) {
387
+ installPack = false;
388
+ } else if (forceStarterPack) {
389
+ installPack = true;
390
+ } else if (asJson) {
391
+ installPack = false;
392
+ } else {
393
+ console.log();
394
+ console.log(`\x1b[1m\x1b[33m 📦 STARTER PACK AVAILABLE\x1b[0m`);
395
+ console.log(`\x1b[2m 61 curated, security-vetted skills across productivity, dev tools,\x1b[0m`);
396
+ console.log(`\x1b[2m security, analytics, SEO, automation, and memory.\x1b[0m`);
397
+ console.log();
398
+ const answer = await promptUser(" Install the starter pack? [Y/n] ");
399
+ installPack = answer === "" || answer === "y" || answer === "yes";
400
+ }
401
+
402
+ if (installPack) {
403
+ try {
404
+ result.starterPack = installStarterPack({
405
+ packageRoot: result.packageRoot,
406
+ skillsRoot: result.skillsRoot,
407
+ });
408
+ } catch {
409
+ // Non-fatal: starter pack failure shouldn't block core install
410
+ }
411
+ } else if (!skipStarterPack && starterPackAvailable && !asJson) {
412
+ console.log(`\x1b[2m Skipped. Install later with: ape-claw skill install --starter-pack\x1b[0m`);
413
+ result.starterPack.skipped = true;
414
+ } else {
415
+ result.starterPack.skipped = true;
416
+ }
417
+
418
+ if (asJson) return print(result, true);
419
+
420
+ const W = 64;
421
+ const line = "─".repeat(W);
422
+ const dline = "═".repeat(W);
423
+ const thinLine = "╌".repeat(W);
424
+ console.log();
425
+ console.log(`\x1b[1m\x1b[33m${dline}\x1b[0m`);
426
+ console.log(`\x1b[1m\x1b[33m 🦞 APE CLAW — INSTALLATION COMPLETE\x1b[0m`);
427
+ console.log(`\x1b[1m\x1b[33m${dline}\x1b[0m`);
428
+ console.log();
429
+ console.log(` \x1b[36mScope:\x1b[0m ${result.scope}`);
430
+ console.log(` \x1b[36mSkills dir:\x1b[0m ${result.skillsRoot}`);
431
+ console.log(` \x1b[36mCore skill:\x1b[0m ${result.skillPath}`);
432
+
433
+ const sp = result.starterPack;
434
+ if (sp && sp.installed > 0) {
435
+ console.log();
436
+ console.log(`\x1b[1m 📦 STARTER PACK — ${sp.installed} skills installed\x1b[0m`);
437
+ console.log(` ${line}`);
438
+
439
+ const catOrder = Object.entries(sp.categories)
440
+ .sort((a, b) => b[1] - a[1]);
441
+ for (const [cat, count] of catOrder) {
442
+ console.log(` \x1b[33m${cat}\x1b[0m (${count})`);
443
+ const inCat = sp.skills
444
+ .filter((s) => s.category === cat)
445
+ .sort((a, b) => a.name.localeCompare(b.name));
446
+ for (const s of inCat) {
447
+ const nameStr = s.name.length > 38 ? s.name.slice(0, 35) + "..." : s.name;
448
+ const desc = (s.description || "").length > 50 ? s.description.slice(0, 47) + "..." : (s.description || "");
449
+ console.log(` \x1b[32m✓\x1b[0m ${nameStr.padEnd(39)} \x1b[2m${desc}\x1b[0m`);
450
+ }
451
+ }
452
+ console.log(` ${line}`);
453
+ console.log(` \x1b[1m\x1b[32m${sp.installed} skills ready\x1b[0m across \x1b[33m${catOrder.length} categories\x1b[0m`);
454
+
455
+ const featured = [
456
+ { name: "Gog", desc: "Gmail, Calendar, Drive, Sheets, Docs via CLI" },
457
+ { name: "GitHub", desc: "Issues, PRs, CI runs via gh CLI" },
458
+ { name: "Self-Improving Agent", desc: "Learns from mistakes, gets smarter over time" },
459
+ { name: "Find Skills", desc: "Discovers & installs new skills on demand" },
460
+ { name: "Humanizer", desc: "Removes AI writing patterns from text" },
461
+ ];
462
+ const hasFeatured = sp.skills.some((s) =>
463
+ s.slug === "clawhub-gog" || s.slug === "clawhub-github" ||
464
+ s.slug === "clawhub-self-improving-agent" || s.slug === "clawhub-find-skills" ||
465
+ s.slug === "clawhub-humanizer"
466
+ );
467
+ if (hasFeatured) {
468
+ console.log();
469
+ console.log(`\x1b[1m ⭐ FEATURED SKILLS\x1b[0m`);
470
+ console.log(` ${thinLine}`);
471
+ for (const f of featured) {
472
+ console.log(` \x1b[33m${f.name.padEnd(28)}\x1b[0m ${f.desc}`);
473
+ }
474
+ }
475
+ }
476
+
477
+ console.log();
478
+ console.log(`\x1b[1m 🤖 YOUR CLAWBOT CAN NOW:\x1b[0m`);
479
+ console.log(` ${thinLine}`);
480
+ console.log(` • Bridge tokens to ApeChain & buy NFTs on ApeChain marketplaces`);
481
+ console.log(` • Execute DeFi trades (SushiSwap, Uniswap, lending protocols)`);
482
+ console.log(` • Monitor wallets, contracts, and on-chain events in real-time`);
483
+ console.log(` • Analyze blockchain data, token metrics, and portfolio performance`);
484
+ console.log(` • Manage Google Workspace (Gmail, Calendar, Drive, Sheets, Docs)`);
485
+ console.log(` • Interact with GitHub (issues, PRs, CI, code review)`);
486
+ console.log(` • Discover & install 10,000+ skills from the ApeClaw library`);
487
+ console.log(` • Self-improve by logging errors and learning from corrections`);
488
+ console.log(` • Humanize AI-generated text to sound natural`);
489
+ console.log(` • Communicate P2P with other agents via encrypted channels`);
490
+
491
+ console.log();
492
+ console.log(`\x1b[1m 📋 NEXT STEPS:\x1b[0m`);
493
+ console.log(` ${thinLine}`);
494
+ console.log(` \x1b[36m1.\x1b[0m Configure your wallet & RPC endpoint:`);
495
+ console.log(` \x1b[2mSet APE_CLAW_RPC_URL and APE_CLAW_WALLET_KEY in your .env\x1b[0m`);
496
+ console.log(` \x1b[36m2.\x1b[0m Review your policy file:`);
497
+ console.log(` \x1b[2m${path.join(process.cwd(), "config", "policy.json")}\x1b[0m`);
498
+ console.log(` \x1b[36m3.\x1b[0m Start your clawbot:`);
499
+ console.log(` \x1b[32mape-claw clawbot register --agent-id my-bot --name "My ClawBot"\x1b[0m`);
500
+ console.log(` \x1b[36m4.\x1b[0m Browse all skills:`);
501
+ console.log(` \x1b[32mape-claw skill list\x1b[0m \x1b[2mor visit\x1b[0m \x1b[4mhttps://apeclaw.ai/skills\x1b[0m`);
502
+ console.log(` \x1b[36m5.\x1b[0m Try a quote (dry-run):`);
503
+ console.log(` \x1b[32mape-claw quote --collection boredapeyachtclub --budget 5\x1b[0m`);
504
+
505
+ console.log();
506
+ console.log(` \x1b[2mDocs:\x1b[0m https://apeclaw.ai/docs`);
507
+ console.log(` \x1b[2mDash:\x1b[0m https://apeclaw.ai/dashboard`);
508
+ console.log(` \x1b[2mHelp:\x1b[0m ape-claw --help`);
509
+ console.log();
510
+ console.log(`\x1b[1m\x1b[33m${dline}\x1b[0m`);
511
+ console.log();
512
+ return;
513
+ }
514
+
515
+ // ── Clawbot commands (before policy load so they work in any directory)
516
+ if (group === "clawbot" && sub === "register") {
517
+ const agentId = String(args["agent-id"] || "").trim();
518
+ const displayName = String(args.name || agentId || "").trim();
519
+ if (!agentId) fail("--agent-id is required", command, args);
520
+ const apiBase = resolveRemoteApiBase(args);
521
+ const registrationKey = String(args["registration-key"] || process.env.APE_CLAW_REGISTRATION_KEY || "").trim();
522
+ const invite = String(args.invite || process.env.APE_CLAW_INVITE || "").trim();
523
+ try {
524
+ if (apiBase) {
525
+ const reg = await registerClawbotRemote({ apiBase, agentId, displayName, registrationKey, invite });
526
+ const result = {
527
+ ...reg,
528
+ remote: true,
529
+ apiBase,
530
+ };
531
+ emitEvent({ eventType: "clawbot.registered", command, dryRun: true, result: { agentId: reg.agentId, name: reg.name || displayName, remote: true } });
532
+ return print(result, asJson);
533
+ }
534
+ const reg = registerClawbot({ agentId, displayName });
535
+ const result = {
536
+ registered: true,
537
+ agentId: reg.agentId,
538
+ name: reg.displayName,
539
+ token: reg.token,
540
+ note: "Save this token — it is shown only once. Use as APE_CLAW_AGENT_TOKEN or --agent-token.",
541
+ remote: false,
542
+ };
543
+ emitEvent({ eventType: "clawbot.registered", command, dryRun: true, result: { agentId: reg.agentId, name: reg.displayName } });
544
+ return print(result, asJson);
545
+ } catch (err) {
546
+ fail(err.message, command, { agentId, apiBase: apiBase || null });
547
+ }
548
+ }
549
+ if (group === "clawbot" && sub === "list") {
550
+ const bots = listClawbots();
551
+ const result = { count: bots.length, clawbots: bots };
552
+ emitEvent({ eventType: "clawbot.list.read", command, dryRun: true, result: { count: bots.length } });
553
+ return print(result, asJson);
554
+ }
555
+
556
+ // ── Local auth profile commands (stored in ~/.ape-claw/auth.json)
557
+ if (group === "auth" && sub === "set") {
558
+ const current = loadAuthStore();
559
+ const next = { ...current };
560
+ let changed = 0;
561
+
562
+ const setIfProvided = (flag, key) => {
563
+ if (args[flag] !== undefined) {
564
+ const val = String(args[flag] || "").trim();
565
+ if (val) next[key] = val;
566
+ else delete next[key];
567
+ changed++;
568
+ }
569
+ };
570
+
571
+ setIfProvided("agent-id", "agentId");
572
+ setIfProvided("agent-token", "agentToken");
573
+ setIfProvided("opensea-api-key", "openseaApiKey");
574
+ setIfProvided("private-key", "privateKey");
575
+
576
+ if (changed === 0) {
577
+ fail("Provide at least one of --agent-id --agent-token --opensea-api-key --private-key", command, args);
578
+ }
579
+
580
+ writeAuthStore(next);
581
+ const result = {
582
+ ok: true,
583
+ saved: true,
584
+ path: authStorePath(),
585
+ fields: {
586
+ agentId: next.agentId || null,
587
+ agentToken: Boolean(next.agentToken),
588
+ openseaApiKey: Boolean(next.openseaApiKey),
589
+ privateKey: Boolean(next.privateKey),
590
+ },
591
+ note: "Secrets are stored locally in ~/.ape-claw/auth.json (mode 600). Env vars and flags still override these values.",
592
+ };
593
+ emitEvent({ eventType: "auth.saved", command, dryRun: true, result: { path: authStorePath() } });
594
+ return print(result, asJson);
595
+ }
596
+
597
+ if (group === "auth" && sub === "show") {
598
+ const cur = loadAuthStore();
599
+ const result = {
600
+ ok: true,
601
+ path: authStorePath(),
602
+ auth: {
603
+ agentId: cur.agentId || null,
604
+ agentToken: cur.agentToken ? maskSecret(cur.agentToken) : null,
605
+ openseaApiKey: cur.openseaApiKey ? maskSecret(cur.openseaApiKey) : null,
606
+ privateKey: cur.privateKey ? maskSecret(cur.privateKey) : null,
607
+ },
608
+ };
609
+ return print(result, asJson);
610
+ }
611
+
612
+ if (group === "auth" && sub === "clear") {
613
+ const cur = loadAuthStore();
614
+ const field = String(args.field || "").trim();
615
+ if (Boolean(args.all)) {
616
+ writeAuthStore({});
617
+ return print({ ok: true, cleared: "all", path: authStorePath() }, asJson);
618
+ }
619
+ const allowed = new Set(["agent-id", "agent-token", "opensea-api-key", "private-key"]);
620
+ if (!allowed.has(field)) {
621
+ fail('Use --field one of: "agent-id" | "agent-token" | "opensea-api-key" | "private-key", or --all', command, args);
622
+ }
623
+ const keyMap = {
624
+ "agent-id": "agentId",
625
+ "agent-token": "agentToken",
626
+ "opensea-api-key": "openseaApiKey",
627
+ "private-key": "privateKey",
628
+ };
629
+ delete cur[keyMap[field]];
630
+ writeAuthStore(cur);
631
+ return print({ ok: true, cleared: field, path: authStorePath() }, asJson);
632
+ }
633
+
634
+ // ── Resolve agent identity
635
+ const storedAuth = loadAuthStore();
636
+ const agentId = String(args["agent-id"] || process.env.APE_CLAW_AGENT_ID || storedAuth.agentId || "local-cli").trim();
637
+ _agentId = agentId;
638
+ const agentToken = String(args["agent-token"] || process.env.APE_CLAW_AGENT_TOKEN || storedAuth.agentToken || "").trim();
639
+ const apiBaseForIdentity = resolveRemoteApiBase(args);
640
+ let verifiedBot = null;
641
+ let sharedOpenseaKey = "";
642
+ if (agentToken) {
643
+ const v = verifyClawbot({ agentId, agentToken });
644
+ if (v.verified) {
645
+ verifiedBot = v.agent;
646
+ sharedOpenseaKey = v.sharedOpenseaApiKey || "";
647
+ } else {
648
+ // If this machine doesn't have clawbots.json (or it's out of date),
649
+ // try to verify against the shared backend when configured.
650
+ if (apiBaseForIdentity) {
651
+ try {
652
+ const rv = await verifyClawbotRemote({ apiBase: apiBaseForIdentity, agentId, agentToken });
653
+ if (rv?.verified) {
654
+ verifiedBot = rv.agent || { id: agentId, name: agentId };
655
+ sharedOpenseaKey = String(rv.sharedOpenseaApiKey || "");
656
+ } else {
657
+ fail(`Clawbot verification failed: ${v.reason}`, command, { agentId });
658
+ }
659
+ } catch (err) {
660
+ fail(`Clawbot verification failed: ${v.reason}`, command, { agentId, remote: apiBaseForIdentity, remoteError: err.message });
661
+ }
662
+ } else {
663
+ fail(`Clawbot verification failed: ${v.reason}. Register first with: ape-claw clawbot register --agent-id <id> --json`, command, { agentId });
664
+ }
665
+ }
666
+ }
667
+
668
+ // Override emitEvent defaults with agentId
669
+ const emit = (opts) => emitEvent({ ...opts, agentId });
670
+
671
+ const policy = loadPolicy();
672
+ let allowlist = normalizeAllowlist(loadAllowlist());
673
+ // Use shared key for verified bots, else fall back to env
674
+ const openseaKey = process.env.OPENSEA_API_KEY || sharedOpenseaKey || storedAuth.openseaApiKey || "";
675
+ const relayApiKey = process.env.RELAY_API_KEY || "";
676
+ const privateKeyFromEnv = String(process.env.APE_CLAW_PRIVATE_KEY || "").trim();
677
+ const privateKeyFromProfile = String(storedAuth.privateKey || "").trim();
678
+ const privateKey = privateKeyFromEnv || privateKeyFromProfile || "";
679
+ const privateKeySource = privateKeyFromEnv ? "env" : (privateKeyFromProfile ? "local-auth" : "missing");
680
+ const slugOverrides = readJson(OPENSEA_OVERRIDES_PATH, {}) || {};
681
+
682
+ if (group === "doctor") {
683
+ const unresolvedCount = allowlist.filter((c) => !c.contractAddress).length;
684
+ const openseaRequired = String(policy.market.dataSource || "").toLowerCase() === "opensea";
685
+ const clawbotsConfig = loadClawbotsConfig() || {};
686
+ const registeredAgent = Boolean(clawbotsConfig?.agents?.[agentId]);
687
+ const sharedKeyConfigured = Boolean(clawbotsConfig?.sharedOpenseaApiKey || process.env.APE_CLAW_SHARED_OPENSEA_KEY);
688
+ const sharedKeyInjected = Boolean(sharedOpenseaKey);
689
+ const openseaProvided = Boolean(openseaKey);
690
+ const openseaMissing = openseaRequired && !openseaProvided;
691
+ const privateKeyMissing = !privateKey;
692
+ const issues = [];
693
+ const warnings = [];
694
+ if (openseaMissing) {
695
+ warnings.push("OpenSea API key is not available for this agent. Set OPENSEA_API_KEY, or verify a clawbot so sharedOpenseaApiKey can be injected.");
696
+ }
697
+ if (registeredAgent && sharedKeyConfigured && !sharedKeyInjected) {
698
+ warnings.push("This agent is registered and shared OpenSea key is configured, but not injected yet. Provide --agent-token (or save once via: ape-claw auth set --agent-id <id> --agent-token <token> --json).");
699
+ }
700
+ if (privateKeyMissing) {
701
+ warnings.push("Private key not detected for execute flows. Read-only commands are ready. For execution, provide APE_CLAW_PRIVATE_KEY, save with ape-claw auth set --private-key, or map your OpenClaw bot secret to APE_CLAW_PRIVATE_KEY.");
702
+ }
703
+ const executeReady = !openseaMissing && !privateKeyMissing;
704
+ const readOnlyReady = issues.length === 0;
705
+ const nextSteps = [];
706
+ if (registeredAgent && !verifiedBot) {
707
+ nextSteps.push("Verify this registered clawbot to inject shared OpenSea key: ape-claw doctor --agent-id <id> --agent-token <token> --json");
708
+ nextSteps.push("Or persist once: ape-claw auth set --agent-id <id> --agent-token <token> --json");
709
+ }
710
+ if (openseaMissing && !registeredAgent) {
711
+ nextSteps.push("Standalone mode: set OPENSEA_API_KEY (env) or save with ape-claw auth set --opensea-api-key <key> --json");
712
+ }
713
+ if (privateKeyMissing) {
714
+ nextSteps.push("For execute flows, set APE_CLAW_PRIVATE_KEY (env), or save with ape-claw auth set --private-key 0x... --json");
715
+ nextSteps.push("If your OpenClaw bot already has a wallet secret, map/export it as APE_CLAW_PRIVATE_KEY before running execute commands.");
716
+ }
717
+ if (!privateKeyMissing && !openseaMissing) {
718
+ nextSteps.push("Execute-ready: you can run buy/bridge commands with --execute.");
719
+ } else {
720
+ nextSteps.push("Read-only ready: use market/quote/simulate flows now, then complete missing execute prerequisites.");
721
+ }
722
+ const result = {
723
+ ok: issues.length === 0,
724
+ issues,
725
+ warnings,
726
+ chainId: policy.apechainChainId,
727
+ rpcConfigured: Boolean(policy.apechainRpcUrl),
728
+ agent: {
729
+ agentId,
730
+ verified: Boolean(verifiedBot),
731
+ name: verifiedBot?.name || agentId,
732
+ sharedKeyAvailable: sharedKeyInjected,
733
+ sharedKeyInjected,
734
+ localAuthProfile: Boolean(storedAuth.agentId || storedAuth.agentToken || storedAuth.openseaApiKey || storedAuth.privateKey),
735
+ registered: registeredAgent,
736
+ },
737
+ bridge: {
738
+ provider: policy.bridge.provider,
739
+ relayApiKeyRequired: false,
740
+ relayApiKeyProvided: Boolean(process.env.RELAY_API_KEY),
741
+ executeRequiresPrivateKey: true,
742
+ },
743
+ market: {
744
+ dataSource: policy.market.dataSource,
745
+ openseaApiKeyRequired: openseaRequired,
746
+ openseaApiKeyProvided: openseaProvided,
747
+ },
748
+ execution: {
749
+ privateKeyProvided: !privateKeyMissing,
750
+ privateKeySource,
751
+ readOnlyReady,
752
+ executeReady,
753
+ dailySpendCap: policy.execution.dailySpendCap,
754
+ confirmPhraseRequired: policy.execution.confirmPhraseRequired,
755
+ simulationRequired: policy.nftBuy.simulationRequired,
756
+ maxPricePerTx: policy.nftBuy.maxPricePerTx,
757
+ },
758
+ policyPath: POLICY_PATH,
759
+ allowlistPath: ALLOWLIST_PATH,
760
+ allowlistStats: { total: allowlist.length, unresolvedCount },
761
+ recommendations: ["Use --json for agent parsing", "Use --execute for state-changing calls"],
762
+ nextSteps,
763
+ };
764
+ emit({ eventType: "doctor.ran", command, dryRun: true, result });
765
+ return print(result, asJson);
766
+ }
767
+
768
+ if (group === "quickstart") {
769
+ const unresolvedCount = allowlist.filter((c) => !c.contractAddress).length;
770
+ const openseaRequired = String(policy.market.dataSource || "").toLowerCase() === "opensea";
771
+ const clawbotsConfig = loadClawbotsConfig() || {};
772
+ const registeredAgent = Boolean(clawbotsConfig?.agents?.[agentId]);
773
+ const sharedKeyInjected = Boolean(sharedOpenseaKey);
774
+ const openseaProvided = Boolean(openseaKey);
775
+ const openseaMissing = openseaRequired && !openseaProvided;
776
+ const privateKeyMissing = !privateKey;
777
+ const executeReady = !openseaMissing && !privateKeyMissing;
778
+ const readOnlyReady = true;
779
+
780
+ const runner = "npx --yes github:simplefarmer69/ape-claw";
781
+ const suggested = [
782
+ `${runner} doctor --json`,
783
+ `${runner} clawbot register --agent-id my-bot --name "My Bot" --json`,
784
+ `${runner} auth set --agent-id my-bot --agent-token claw_... --json`,
785
+ `${runner} market collections --recommended --json`,
786
+ ];
787
+ if (privateKeyMissing) {
788
+ suggested.push(`${runner} auth set --private-key 0x... --json`);
789
+ }
790
+
791
+ const summary = [];
792
+ summary.push(readOnlyReady ? "Read-only flows are ready." : "Read-only flows need configuration.");
793
+ summary.push(
794
+ executeReady
795
+ ? "Execute flows are ready."
796
+ : "Execute flows need missing secrets (private key and/or OpenSea API key).",
797
+ );
798
+ if (registeredAgent && !sharedKeyInjected) {
799
+ summary.push("Registered bot detected: provide agent token to inject shared OpenSea key.");
800
+ }
801
+ if (openseaMissing && !registeredAgent) {
802
+ summary.push("Standalone mode: set OPENSEA_API_KEY or save one with auth set.");
803
+ }
804
+
805
+ const nextSteps = [];
806
+ if (!registeredAgent) {
807
+ nextSteps.push('Register a bot first: ape-claw clawbot register --agent-id my-bot --name "My Bot" --json');
808
+ } else if (!sharedKeyInjected) {
809
+ nextSteps.push("Save your bot token once: ape-claw auth set --agent-id <id> --agent-token <token> --json");
810
+ }
811
+ if (privateKeyMissing) {
812
+ nextSteps.push("Set private key for execute flows: ape-claw auth set --private-key 0x... --json");
813
+ }
814
+ if (!openseaMissing && !privateKeyMissing) {
815
+ nextSteps.push("Run execute flows now: ape-claw nft buy --quote <quoteId> --execute --autonomous --json");
816
+ } else {
817
+ nextSteps.push("Use read-only flows now: ape-claw market collections --recommended --json");
818
+ }
819
+
820
+ const result = {
821
+ ok: true,
822
+ message: "Personalized onboarding steps for this machine.",
823
+ status: {
824
+ agentId,
825
+ registered: registeredAgent,
826
+ verified: Boolean(verifiedBot),
827
+ sharedKeyInjected,
828
+ readOnlyReady,
829
+ executeReady,
830
+ privateKeyProvided: !privateKeyMissing,
831
+ openseaApiKeyProvided: openseaProvided,
832
+ allowlistUnresolvedCount: unresolvedCount,
833
+ },
834
+ summary,
835
+ recommendedCommands: suggested,
836
+ nextSteps,
837
+ note: "Use npx commands if global ape-claw is not on PATH yet.",
838
+ };
839
+ emit({ eventType: "quickstart.ran", command, dryRun: true, result });
840
+ return print(result, asJson);
841
+ }
842
+
843
+ if (group === "chain" && sub === "info") {
844
+ const chainId = Number(policy.apechainChainId || 33139);
845
+ let latestBlock = null;
846
+ try {
847
+ const rpcUrl = await resolveRpcUrl(chainId, policy);
848
+ const rpcRes = await fetch(rpcUrl, {
849
+ method: "POST",
850
+ headers: { "content-type": "application/json" },
851
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
852
+ });
853
+ const rpcJson = await rpcRes.json();
854
+ if (rpcJson.result) latestBlock = parseInt(rpcJson.result, 16);
855
+ } catch {
856
+ // RPC unavailable — leave null so bots know it's unknown
857
+ }
858
+ const result = {
859
+ chainId,
860
+ nativeGasToken: policy.nativeGasToken,
861
+ bridgeProvider: policy.bridge.provider,
862
+ marketDataSource: policy.market.dataSource,
863
+ latestBlock,
864
+ rpcOk: latestBlock !== null,
865
+ };
866
+ emit({ eventType: "chain.info.read", command, dryRun: true, result });
867
+ return print(result, asJson);
868
+ }
869
+
870
+ if (group === "market" && sub === "collections") {
871
+ const recommendedOnly = Boolean(args.recommended);
872
+ if (String(policy.market.dataSource).toLowerCase() === "opensea" && openseaKey) {
873
+ const enriched = await enrichAllowlistWithOpenSea(allowlist, openseaKey, slugOverrides);
874
+ allowlist = enriched.allowlist;
875
+ }
876
+ const data = recommendedOnly ? allowlist.filter((c) => c.enabled !== false) : allowlist;
877
+ // Strip rank from agent-facing output — rank is metadata only, never a decision signal
878
+ const collections = data.map(({ rank, ...rest }) => rest);
879
+ const result = { count: collections.length, collections, source: policy.market.dataSource };
880
+ emit({
881
+ eventType: "market.collections.read",
882
+ command,
883
+ dryRun: true,
884
+ result: { count: data.length, source: policy.market.dataSource },
885
+ });
886
+ return print(result, asJson);
887
+ }
888
+
889
+ if (group === "market" && sub === "listings") {
890
+ const collection = args.collection;
891
+ if (!collection) fail("--collection is required", command, args);
892
+ const maxPrice = Number(args.maxPrice || policy.nftBuy.maxPricePerTx);
893
+ let result;
894
+ try {
895
+ const out = await getListings({
896
+ collection,
897
+ tokenId: args.tokenId,
898
+ maxPrice,
899
+ dataSource: args.dataSource || policy.market.dataSource,
900
+ apiKey: openseaKey,
901
+ slugOverrides,
902
+ });
903
+ const listings = out.listings || [];
904
+ result = { count: listings.length, listings, source: out.source, notes: out.notes || [] };
905
+ } catch (err) {
906
+ result = { count: 0, listings: [], source: policy.market.dataSource, error: err.message };
907
+ emit({
908
+ eventType: "market.listings.failed",
909
+ command,
910
+ dryRun: true,
911
+ payload: args,
912
+ result,
913
+ ok: false,
914
+ error: err.message,
915
+ });
916
+ return print(result, asJson);
917
+ }
918
+ emit({ eventType: "market.listings.read", command, dryRun: true, payload: args, result });
919
+ return print(result, asJson);
920
+ }
921
+
922
+ if (group === "nft" && sub === "autobuy") {
923
+ const execute = Boolean(args.execute);
924
+ const autonomous = Boolean(args.autonomous);
925
+ const count = Math.max(1, Number(args.count || 1));
926
+ const scanLimit = Math.max(count, Number(args.scan || Math.max(10, count * 4)));
927
+ const maxPrice = Number(args.maxPrice || policy.nftBuy.maxPricePerTx);
928
+ const minPrice = Number(args.minPrice || 0);
929
+ const budget = Number(args.budget || 0);
930
+ const currency = String(args.currency || "APE").toUpperCase();
931
+ const dataSource = String(args.dataSource || policy.market.dataSource || "opensea");
932
+ const includeDisabled = Boolean(args.all);
933
+ const collectionsArg = String(args.collections || "").trim();
934
+ const collectionFilters = collectionsArg
935
+ ? collectionsArg
936
+ .split(",")
937
+ .map((x) => x.trim().toLowerCase())
938
+ .filter(Boolean)
939
+ : [];
940
+
941
+ if (!Number.isFinite(maxPrice) || maxPrice <= 0) fail("--maxPrice must be > 0", command, args);
942
+ if (!Number.isFinite(minPrice) || minPrice < 0) fail("--minPrice must be >= 0", command, args);
943
+ if (minPrice > maxPrice) fail("--minPrice must be <= --maxPrice", command, args);
944
+ if (currency !== "APE") fail("nft autobuy currently supports --currency APE only", command, args);
945
+ if (String(dataSource).toLowerCase() === "opensea" && !openseaKey) {
946
+ fail("OPENSEA_API_KEY is required for nft autobuy with OpenSea data source.", command, args);
947
+ }
948
+ if (execute && !privateKey) {
949
+ fail(
950
+ "APE_CLAW_PRIVATE_KEY is required for live nft autobuy execute. Set env var, save once with `ape-claw auth set --private-key 0x... --json`, or map your OpenClaw bot wallet secret to APE_CLAW_PRIVATE_KEY.",
951
+ command,
952
+ args,
953
+ );
954
+ }
955
+
956
+ let universe = includeDisabled ? [...allowlist] : allowlist.filter((c) => c.enabled !== false);
957
+ if (collectionFilters.length > 0) {
958
+ universe = universe.filter((c) => {
959
+ const hay = `${String(c.name || "").toLowerCase()} ${String(c.slug || "").toLowerCase()}`;
960
+ return collectionFilters.some((needle) => hay.includes(needle));
961
+ });
962
+ }
963
+ universe = universe.slice(0, scanLimit);
964
+ if (universe.length === 0) fail("No collections matched autobuy selection.", command, args);
965
+
966
+ const candidates = [];
967
+ const skipped = [];
968
+ for (const c of universe) {
969
+ const collectionRef = c.slug || c.name;
970
+ if (!collectionRef) {
971
+ skipped.push({ collection: c.name || "unknown", reason: "missing collection reference" });
972
+ continue;
973
+ }
974
+ try {
975
+ const policyCheck = enforceBuyPolicy({
976
+ policy,
977
+ collection: collectionRef,
978
+ maxPrice,
979
+ currency,
980
+ allowUnsafe: Boolean(args["allow-unsafe"]),
981
+ allowlist,
982
+ });
983
+ if (!policyCheck.ok) {
984
+ skipped.push({ collection: collectionRef, reason: policyCheck.errors.join(" ") });
985
+ continue;
986
+ }
987
+ const listingsOut = await getListings({
988
+ collection: c.slug || collectionRef,
989
+ maxPrice,
990
+ dataSource,
991
+ apiKey: openseaKey,
992
+ slugOverrides,
993
+ });
994
+ const live = (listingsOut.listings || [])
995
+ .filter((l) => {
996
+ const p = Number(l.priceApe);
997
+ return Number.isFinite(p) && p > 0 && p >= minPrice && p <= maxPrice;
998
+ })
999
+ .sort((a, b) => Number(a.priceApe) - Number(b.priceApe))[0];
1000
+ if (!live) {
1001
+ skipped.push({ collection: collectionRef, reason: "no listing within price range" });
1002
+ continue;
1003
+ }
1004
+ candidates.push({
1005
+ collection: collectionRef,
1006
+ collectionName: c.name || collectionRef,
1007
+ listing: live,
1008
+ priceApe: Number(live.priceApe),
1009
+ target: policyCheck.target || resolveCollectionTarget(collectionRef, allowlist).exact,
1010
+ });
1011
+ } catch (err) {
1012
+ skipped.push({ collection: collectionRef, reason: err.message });
1013
+ }
1014
+ }
1015
+
1016
+ const sorted = candidates.sort((a, b) => a.priceApe - b.priceApe);
1017
+ const selected = [];
1018
+ let remaining = Number.isFinite(budget) && budget > 0 ? budget : Number.POSITIVE_INFINITY;
1019
+ for (const c of sorted) {
1020
+ if (selected.length >= count) break;
1021
+ if (c.priceApe > remaining) continue;
1022
+ selected.push(c);
1023
+ remaining -= c.priceApe;
1024
+ }
1025
+ if (selected.length === 0) {
1026
+ const result = {
1027
+ ok: true,
1028
+ message: "No autobuy candidates selected under current constraints.",
1029
+ constraints: { count, scanLimit, minPrice, maxPrice, budget: budget > 0 ? budget : null, currency, dataSource, includeDisabled },
1030
+ scannedCollections: universe.length,
1031
+ candidateCount: candidates.length,
1032
+ selectedCount: 0,
1033
+ skipped,
1034
+ };
1035
+ emit({ eventType: "nft.autobuy.planned", command, dryRun: !execute, payload: args, result });
1036
+ return print(result, asJson);
1037
+ }
1038
+
1039
+ const quotes = loadState(QUOTES_PATH);
1040
+ const planned = [];
1041
+ for (const s of selected) {
1042
+ const quoteId = randomId("q");
1043
+ const quote = {
1044
+ quoteId,
1045
+ collection: s.collection,
1046
+ collectionTarget: s.target?.contractAddress || s.target?.slug || s.collection,
1047
+ tokenId: String(s.listing.tokenId),
1048
+ currency,
1049
+ priceApe: s.priceApe,
1050
+ maxPrice,
1051
+ expiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
1052
+ listingId: s.listing.listingId,
1053
+ orderHash: s.listing.orderHash || randomId("order"),
1054
+ routeHash: s.listing.source || "opensea",
1055
+ source: s.listing.source || "opensea",
1056
+ protocolAddress: s.listing.protocolAddress || "0x0000000000000068f116a894984e2db1123eb395",
1057
+ assetContractAddress: s.listing.assetContractAddress || s.target?.contractAddress || null,
1058
+ chainId: Number(policy.apechainChainId || 33139),
1059
+ dryRunDefault: true,
1060
+ };
1061
+ quotes[quoteId] = quote;
1062
+ planned.push({ quoteId, collection: quote.collection, tokenId: quote.tokenId, priceApe: quote.priceApe });
1063
+ }
1064
+ writeJson(QUOTES_PATH, quotes);
1065
+
1066
+ if (!execute) {
1067
+ const result = {
1068
+ ok: true,
1069
+ dryRun: true,
1070
+ message: "Autobuy planned quotes generated. Re-run with --execute --autonomous to send.",
1071
+ constraints: { count, scanLimit, minPrice, maxPrice, budget: budget > 0 ? budget : null, currency, dataSource, includeDisabled },
1072
+ scannedCollections: universe.length,
1073
+ candidateCount: candidates.length,
1074
+ selectedCount: selected.length,
1075
+ planned,
1076
+ skipped,
1077
+ };
1078
+ emit({ eventType: "nft.autobuy.planned", command, dryRun: true, payload: args, result });
1079
+ return print(result, asJson);
1080
+ }
1081
+
1082
+ if (!autonomous) {
1083
+ fail("nft autobuy execute requires --autonomous to enforce generated confirms/simulations.", command, args);
1084
+ }
1085
+
1086
+ const here = fileURLToPath(import.meta.url);
1087
+ const executions = [];
1088
+ for (const p of planned) {
1089
+ const childArgs = [here, "nft", "buy", "--quote", p.quoteId, "--execute", "--autonomous", "--json"];
1090
+ if (agentId) childArgs.push("--agent-id", agentId);
1091
+ if (agentToken) childArgs.push("--agent-token", agentToken);
1092
+ const child = spawnSync(process.execPath, childArgs, {
1093
+ cwd: process.cwd(),
1094
+ encoding: "utf8",
1095
+ env: process.env,
1096
+ });
1097
+ let parsed = null;
1098
+ try {
1099
+ parsed = JSON.parse(String(child.stdout || "").trim() || "{}");
1100
+ } catch {}
1101
+ executions.push({
1102
+ quoteId: p.quoteId,
1103
+ collection: p.collection,
1104
+ tokenId: p.tokenId,
1105
+ ok: child.status === 0 && Boolean(parsed?.ok !== false),
1106
+ txHash: parsed?.txHash || null,
1107
+ error: child.status === 0 ? null : (parsed?.error || String(child.stderr || "execute failed").trim()),
1108
+ });
1109
+ }
1110
+ const failed = executions.filter((x) => !x.ok);
1111
+ const result = {
1112
+ ok: failed.length === 0,
1113
+ execute: true,
1114
+ autonomous: true,
1115
+ constraints: { count, scanLimit, minPrice, maxPrice, budget: budget > 0 ? budget : null, currency, dataSource, includeDisabled },
1116
+ scannedCollections: universe.length,
1117
+ candidateCount: candidates.length,
1118
+ selectedCount: selected.length,
1119
+ executedCount: executions.length - failed.length,
1120
+ failedCount: failed.length,
1121
+ executions,
1122
+ skipped,
1123
+ };
1124
+ emit({
1125
+ eventType: failed.length === 0 ? "nft.autobuy.executed" : "nft.autobuy.partial",
1126
+ command,
1127
+ dryRun: false,
1128
+ payload: args,
1129
+ result,
1130
+ ok: failed.length === 0,
1131
+ error: failed.length === 0 ? null : "one or more autobuy executions failed",
1132
+ });
1133
+ return print(result, asJson);
1134
+ }
1135
+
1136
+ if (group === "nft" && sub === "quote-buy") {
1137
+ const collection = args.collection;
1138
+ const tokenId = args.tokenId;
1139
+ const maxPrice = Number(args.maxPrice);
1140
+ const currency = String(args.currency || "APE").toUpperCase();
1141
+ if (!collection || !tokenId || Number.isNaN(maxPrice)) {
1142
+ fail("Required: --collection --tokenId --maxPrice", command, args);
1143
+ }
1144
+ if (maxPrice <= 0) fail("--maxPrice must be > 0", command, args);
1145
+ const policyCheck = enforceBuyPolicy({
1146
+ policy,
1147
+ collection,
1148
+ maxPrice,
1149
+ currency,
1150
+ allowUnsafe: Boolean(args["allow-unsafe"]),
1151
+ allowlist,
1152
+ });
1153
+ if (!policyCheck.ok) fail(policyCheck.errors.join(" "), command, args);
1154
+ const target = policyCheck.target || resolveCollectionTarget(collection, allowlist).exact;
1155
+ const resolvedCollection = target?.contractAddress || target?.slug || collection;
1156
+
1157
+ let liveListing = null;
1158
+ try {
1159
+ const listingsOut = await getListings({
1160
+ collection: target?.slug || collection,
1161
+ tokenId,
1162
+ maxPrice,
1163
+ dataSource: args.dataSource || policy.market.dataSource,
1164
+ apiKey: openseaKey,
1165
+ slugOverrides,
1166
+ });
1167
+ const candidates = listingsOut.listings || [];
1168
+ liveListing =
1169
+ candidates.find((l) => String(l.tokenId) === String(tokenId)) ||
1170
+ candidates[0] ||
1171
+ null;
1172
+ } catch (err) {
1173
+ fail(`Live listing lookup failed: ${err.message}`, command, args);
1174
+ }
1175
+ if (!liveListing) {
1176
+ fail(`No live listing found for collection=${collection} tokenId=${tokenId} under maxPrice=${maxPrice}.`, command, args);
1177
+ }
1178
+
1179
+ const quoteId = randomId("q");
1180
+ const priceApe = Number(liveListing.priceApe);
1181
+ if (!Number.isFinite(priceApe) || priceApe <= 0) {
1182
+ fail("Invalid live listing price returned by market provider.", command, args);
1183
+ }
1184
+ const quote = {
1185
+ quoteId,
1186
+ collection,
1187
+ collectionTarget: resolvedCollection,
1188
+ tokenId,
1189
+ currency,
1190
+ priceApe,
1191
+ maxPrice,
1192
+ expiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
1193
+ listingId: liveListing.listingId,
1194
+ orderHash: liveListing.orderHash || randomId("order"),
1195
+ routeHash: liveListing.source || "opensea",
1196
+ source: liveListing.source || "opensea",
1197
+ protocolAddress: liveListing.protocolAddress || "0x0000000000000068f116a894984e2db1123eb395",
1198
+ assetContractAddress: liveListing.assetContractAddress || target?.contractAddress || null,
1199
+ chainId: Number(policy.apechainChainId || 33139),
1200
+ dryRunDefault: true,
1201
+ };
1202
+ const quotes = loadState(QUOTES_PATH);
1203
+ quotes[quoteId] = quote;
1204
+ writeJson(QUOTES_PATH, quotes);
1205
+
1206
+ emit({
1207
+ eventType: "nft.quote.created",
1208
+ command,
1209
+ dryRun: true,
1210
+ payload: { collection, tokenId, maxPrice, currency, allowUnsafe: Boolean(args["allow-unsafe"]) },
1211
+ result: quote,
1212
+ });
1213
+ return print(quote, asJson);
1214
+ }
1215
+
1216
+ if (group === "nft" && sub === "simulate") {
1217
+ const quoteId = args.quote;
1218
+ if (!quoteId) fail("--quote is required", command, args);
1219
+ const quotes = loadState(QUOTES_PATH);
1220
+ const quote = quotes[quoteId];
1221
+ if (!quote) fail(`Unknown quote ${quoteId}`, command, args);
1222
+ const ok = new Date(quote.expiresAt).getTime() > Date.now();
1223
+ quote.simulation = {
1224
+ ok,
1225
+ simulatedAt: new Date().toISOString(),
1226
+ reason: ok ? "simulation_passed" : "quote_expired",
1227
+ };
1228
+ quotes[quoteId] = quote;
1229
+ writeJson(QUOTES_PATH, quotes);
1230
+ const result = { quoteId, ok, reason: ok ? "simulation_passed" : "quote_expired" };
1231
+ emit({
1232
+ eventType: ok ? "nft.simulation.passed" : "nft.simulation.failed",
1233
+ command,
1234
+ dryRun: true,
1235
+ payload: { quoteId },
1236
+ result,
1237
+ ok,
1238
+ error: ok ? null : "quote expired",
1239
+ });
1240
+ if (!ok) process.exit(1);
1241
+ return print(result, asJson);
1242
+ }
1243
+
1244
+ if (group === "nft" && sub === "buy") {
1245
+ const quoteId = args.quote;
1246
+ if (!quoteId) fail("--quote is required", command, args);
1247
+ const execute = Boolean(args.execute);
1248
+ const autonomous = Boolean(args.autonomous);
1249
+ const quotes = loadState(QUOTES_PATH);
1250
+ const quote = quotes[quoteId];
1251
+ if (!quote) fail(`Unknown quote ${quoteId}`, command, args);
1252
+ if (quote.executed) fail("Quote already executed. Generate a fresh quote.", command, { quoteId });
1253
+ if (new Date(quote.expiresAt).getTime() <= Date.now()) {
1254
+ fail("Quote expired. Generate a fresh quote.", command, { quoteId });
1255
+ }
1256
+ if (!execute) {
1257
+ const result = { dryRun: true, message: "No broadcast. Pass --execute to send transaction.", quote };
1258
+ emit({ eventType: "nft.buy.dry_run", command, dryRun: true, payload: { quoteId }, result });
1259
+ return print(result, asJson);
1260
+ }
1261
+ if (policy.nftBuy.simulationRequired && autonomous) {
1262
+ const ok = new Date(quote.expiresAt).getTime() > Date.now();
1263
+ quote.simulation = {
1264
+ ok,
1265
+ simulatedAt: new Date().toISOString(),
1266
+ reason: ok ? "simulation_passed" : "quote_expired",
1267
+ };
1268
+ quotes[quoteId] = quote;
1269
+ writeJson(QUOTES_PATH, quotes);
1270
+ const simResult = { quoteId, ok, reason: ok ? "simulation_passed" : "quote_expired", autonomous: true };
1271
+ emit({
1272
+ eventType: ok ? "nft.simulation.passed" : "nft.simulation.failed",
1273
+ command,
1274
+ dryRun: false,
1275
+ payload: { quoteId, autonomous: true },
1276
+ result: simResult,
1277
+ ok,
1278
+ error: ok ? null : "quote expired",
1279
+ });
1280
+ if (!ok) fail("Quote expired. Generate a fresh quote.", command, { quoteId, autonomous: true });
1281
+ }
1282
+ if (policy.nftBuy.simulationRequired && !quote.simulation?.ok) {
1283
+ fail("Simulation required before execute. Run: ape-claw nft simulate --quote <id> --json", command, {
1284
+ quoteId,
1285
+ });
1286
+ }
1287
+ if (policy.execution.confirmPhraseRequired) {
1288
+ const expected = expectedBuyConfirmPhrase(quote);
1289
+ const got = autonomous ? expected : String(args.confirm || "");
1290
+ if (got !== expected) {
1291
+ fail(`Confirmation phrase mismatch. Use --confirm "${expected}"`, command, { quoteId });
1292
+ }
1293
+ }
1294
+ const today = isoDay();
1295
+ const bridgeRequests = loadState(BRIDGE_REQUESTS_PATH);
1296
+ const spentNft = spentTodayFromQuotes(quotes, today);
1297
+ const spentBridge = spentTodayFromBridge(bridgeRequests, today);
1298
+ const spentToday = spentNft + spentBridge;
1299
+ const projected = spentToday + (Number(quote.priceApe) || 0);
1300
+ const cap = Number(policy.execution.dailySpendCap || 0);
1301
+ if (cap > 0 && projected > cap) {
1302
+ fail(`Daily spend cap exceeded (${projected.toFixed(2)} > ${cap} APE, including bridge).`, command, {
1303
+ quoteId,
1304
+ spentNft,
1305
+ spentBridge,
1306
+ projected,
1307
+ cap,
1308
+ });
1309
+ }
1310
+ if (!openseaKey) {
1311
+ fail("OPENSEA_API_KEY is required for live nft execute (fulfillment data).", command, { quoteId });
1312
+ }
1313
+ if (!privateKey) {
1314
+ fail(
1315
+ "APE_CLAW_PRIVATE_KEY is required for live nft execute. Set env var, save once with `ape-claw auth set --private-key 0x... --json`, or map your OpenClaw bot wallet secret to APE_CLAW_PRIVATE_KEY.",
1316
+ command,
1317
+ { quoteId },
1318
+ );
1319
+ }
1320
+
1321
+ const chainId = Number(quote.chainId || policy.apechainChainId || 33139);
1322
+ const rpcUrl = await resolveRpcUrl(chainId, policy);
1323
+ const fulfillerAddress = String(args.user || "");
1324
+ const confirmedPrice = Number(quote.priceApe);
1325
+ const maxRetries = 3;
1326
+ let fulfillment = null;
1327
+ let usedOrderHash = quote.orderHash;
1328
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1329
+ try {
1330
+ fulfillment = await getListingFulfillmentData({
1331
+ apiKey: openseaKey,
1332
+ orderHash: usedOrderHash,
1333
+ protocolAddress: quote.protocolAddress || "0x0000000000000068f116a894984e2db1123eb395",
1334
+ chainId,
1335
+ fulfillerAddress: fulfillerAddress || undefined,
1336
+ privateKey,
1337
+ assetContractAddress: quote.assetContractAddress || undefined,
1338
+ tokenId: quote.tokenId,
1339
+ });
1340
+ break;
1341
+ } catch (err) {
1342
+ const isOrderNotFound = /order not found/i.test(err.message);
1343
+ if (!isOrderNotFound || attempt >= maxRetries) throw err;
1344
+ // Order was sniped/cancelled — re-fetch a fresh listing for the same collection+token
1345
+ // SAFETY: only accept replacement listings at or below the confirmed price
1346
+ emit({
1347
+ eventType: "nft.buy.retry",
1348
+ command,
1349
+ dryRun: false,
1350
+ payload: { quoteId, attempt, reason: "order_not_found", oldOrderHash: usedOrderHash, confirmedPrice },
1351
+ ok: true,
1352
+ });
1353
+ try {
1354
+ const fresh = await getListings({
1355
+ collection: quote.collectionTarget || quote.collection,
1356
+ tokenId: quote.tokenId,
1357
+ maxPrice: confirmedPrice,
1358
+ dataSource: policy.market.dataSource,
1359
+ apiKey: openseaKey,
1360
+ slugOverrides,
1361
+ });
1362
+ const candidates = fresh.listings || [];
1363
+ const match =
1364
+ candidates.find((l) => String(l.tokenId) === String(quote.tokenId) && Number(l.priceApe) <= confirmedPrice) ||
1365
+ candidates.find((l) => Number(l.priceApe) <= confirmedPrice) ||
1366
+ null;
1367
+ if (!match) throw new Error(`No replacement listing found at or below confirmed price (${confirmedPrice} APE).`);
1368
+ usedOrderHash = match.orderHash;
1369
+ quote.orderHash = match.orderHash;
1370
+ quote.listingId = match.listingId;
1371
+ quote.priceApe = match.priceApe;
1372
+ quote.assetContractAddress = match.assetContractAddress || quote.assetContractAddress;
1373
+ quote.protocolAddress = match.protocolAddress || quote.protocolAddress;
1374
+ } catch (refreshErr) {
1375
+ throw new Error(`Original order sniped and refresh failed: ${refreshErr.message}`);
1376
+ }
1377
+ }
1378
+ }
1379
+ if (!fulfillment) fail("Failed to get fulfillment data after retries.", command, { quoteId });
1380
+ const sent = await executeListingFulfillmentTx({
1381
+ fulfillmentData: fulfillment,
1382
+ privateKey,
1383
+ rpcUrl,
1384
+ });
1385
+ quote.executed = true;
1386
+ quote.executedAt = new Date().toISOString();
1387
+ quote.txHash = sent.txHash;
1388
+ quote.seaport = {
1389
+ chainId: sent.chainId,
1390
+ to: sent.to,
1391
+ functionName: sent.functionName,
1392
+ };
1393
+ quotes[quoteId] = quote;
1394
+ writeJson(QUOTES_PATH, quotes);
1395
+ const result = {
1396
+ ok: true,
1397
+ quoteId,
1398
+ txHash: sent.txHash,
1399
+ chainId: sent.chainId,
1400
+ quote: {
1401
+ quoteId: quote.quoteId,
1402
+ collection: quote.collection,
1403
+ collectionTarget: quote.collectionTarget,
1404
+ tokenId: quote.tokenId,
1405
+ priceApe: quote.priceApe,
1406
+ currency: quote.currency,
1407
+ listingId: quote.listingId,
1408
+ orderHash: quote.orderHash,
1409
+ source: quote.source,
1410
+ },
1411
+ };
1412
+ emit({
1413
+ eventType: "nft.buy.confirmed",
1414
+ command,
1415
+ dryRun: false,
1416
+ payload: {
1417
+ quoteId,
1418
+ collection: quote.collection,
1419
+ tokenId: quote.tokenId,
1420
+ priceApe: quote.priceApe,
1421
+ currency: quote.currency,
1422
+ autonomous,
1423
+ },
1424
+ result,
1425
+ });
1426
+ return print(result, asJson);
1427
+ }
1428
+
1429
+ if (group === "bridge" && sub === "quote") {
1430
+ const from = String(args.from || "");
1431
+ const to = String(args.to || policy.bridge.defaultTo || "apechain");
1432
+ const token = String(args.token || policy.bridge.defaultToken || "APE");
1433
+ const amount = Number(args.amount);
1434
+ if (!from || Number.isNaN(amount)) {
1435
+ fail("Required: --from --amount (defaults: --to apechain --token APE)", command, args);
1436
+ }
1437
+ if (amount <= 0) fail("--amount must be > 0", command, args);
1438
+ if (String(policy.bridge.provider || "").toLowerCase() !== "relay") {
1439
+ fail(`Unsupported bridge provider: ${policy.bridge.provider}. Set bridge.provider=relay.`, command, args);
1440
+ }
1441
+ const req = await quoteBridgeRelay({
1442
+ from,
1443
+ to,
1444
+ token,
1445
+ amount,
1446
+ args,
1447
+ apiKey: relayApiKey,
1448
+ privateKey,
1449
+ });
1450
+ const feeBpsForPolicy = req.feeBps ?? 0;
1451
+ const check = enforceBridgePolicy({ policy, feeBps: feeBpsForPolicy });
1452
+ if (!check.ok) fail(check.errors.join(" "), command, args);
1453
+ const requests = loadState(BRIDGE_REQUESTS_PATH);
1454
+ requests[req.requestId] = req;
1455
+ writeJson(BRIDGE_REQUESTS_PATH, requests);
1456
+ emit({ eventType: "bridge.quote.created", command, dryRun: true, payload: args, result: req });
1457
+ return print(req, asJson);
1458
+ }
1459
+
1460
+ if (group === "bridge" && sub === "execute") {
1461
+ const requestId = args.request;
1462
+ if (!requestId) fail("--request is required", command, args);
1463
+ const execute = Boolean(args.execute);
1464
+ const autonomous = Boolean(args.autonomous);
1465
+ const requests = loadState(BRIDGE_REQUESTS_PATH);
1466
+ const req = requests[requestId];
1467
+ if (!req) fail(`Unknown request ${requestId}`, command, args);
1468
+ if (req.status === "confirmed") fail("Bridge request already executed.", command, { requestId });
1469
+ if (new Date(req.expiresAt).getTime() <= Date.now()) fail("Bridge quote expired.", command, { requestId });
1470
+ if (!execute) {
1471
+ const result = { dryRun: true, message: "No broadcast. Pass --execute to bridge.", request: req };
1472
+ emit({ eventType: "bridge.execute.dry_run", command, dryRun: true, payload: { requestId }, result });
1473
+ return print(result, asJson);
1474
+ }
1475
+ if (policy.execution.confirmPhraseRequired) {
1476
+ const expected = expectedBridgeConfirmPhrase(req);
1477
+ const got = autonomous ? expected : String(args.confirm || "");
1478
+ if (got !== expected) {
1479
+ fail(`Confirmation phrase mismatch. Use --confirm "${expected}"`, command, { requestId });
1480
+ }
1481
+ }
1482
+ if (!privateKey) {
1483
+ fail(
1484
+ "APE_CLAW_PRIVATE_KEY is required for live bridge execute. Set env var, save once with `ape-claw auth set --private-key 0x... --json`, or map your OpenClaw bot wallet secret to APE_CLAW_PRIVATE_KEY.",
1485
+ command,
1486
+ { requestId },
1487
+ );
1488
+ }
1489
+ const today = isoDay();
1490
+ const quotes = loadState(QUOTES_PATH);
1491
+ const spentNft = spentTodayFromQuotes(quotes, today);
1492
+ const spentBridge = spentTodayFromBridge(requests, today);
1493
+ const projectedBridge = spentNft + spentBridge + (Number(req.amount) || 0);
1494
+ const cap = Number(policy.execution.dailySpendCap || 0);
1495
+ if (cap > 0 && projectedBridge > cap) {
1496
+ fail(`Daily spend cap exceeded (${projectedBridge.toFixed(2)} > ${cap} APE, including bridge).`, command, {
1497
+ requestId,
1498
+ spentNft,
1499
+ spentBridge,
1500
+ projectedBridge,
1501
+ cap,
1502
+ });
1503
+ }
1504
+ const executed = await executeBridgeRelay({
1505
+ request: req,
1506
+ privateKey,
1507
+ policy,
1508
+ });
1509
+ requests[requestId] = executed;
1510
+ writeJson(BRIDGE_REQUESTS_PATH, requests);
1511
+ emit({
1512
+ eventType: "bridge.execute.confirmed",
1513
+ command,
1514
+ dryRun: false,
1515
+ payload: { requestId, autonomous },
1516
+ result: executed,
1517
+ });
1518
+ return print(executed, asJson);
1519
+ }
1520
+
1521
+ if (group === "bridge" && sub === "status") {
1522
+ const requestId = args.request;
1523
+ if (!requestId) fail("--request is required", command, args);
1524
+ const requests = loadState(BRIDGE_REQUESTS_PATH);
1525
+ const req = requests[requestId];
1526
+ if (!req) fail(`Unknown request ${requestId}`, command, args);
1527
+ const status = await getBridgeRelayStatus({
1528
+ request: req,
1529
+ apiKey: relayApiKey,
1530
+ });
1531
+ const merged = {
1532
+ ...req,
1533
+ status: status.status || req.status,
1534
+ relayStatus: status.relayStatus || null,
1535
+ destinationTxHash: status.destinationTxHash || req.destinationTxHash || null,
1536
+ lastStatusCheckAt: new Date().toISOString(),
1537
+ };
1538
+ requests[requestId] = merged;
1539
+ writeJson(BRIDGE_REQUESTS_PATH, requests);
1540
+ emit({ eventType: "bridge.status.read", command, dryRun: true, payload: { requestId }, result: merged });
1541
+ return print(merged, asJson);
1542
+ }
1543
+
1544
+ if (group === "allowlist" && sub === "audit") {
1545
+ const unresolved = allowlist.filter((c) => !c.contractAddress);
1546
+ // Check for slug collisions (real identity) instead of rank collisions
1547
+ const bySlug = new Map();
1548
+ const slugCollisions = [];
1549
+ for (const c of allowlist) {
1550
+ const slug = String(c.slug || "").toLowerCase();
1551
+ if (slug && bySlug.has(slug)) slugCollisions.push(slug);
1552
+ if (slug) bySlug.set(slug, c);
1553
+ }
1554
+ const result = {
1555
+ total: allowlist.length,
1556
+ unresolvedCount: unresolved.length,
1557
+ unresolved: unresolved.map((c) => ({ name: c.name, slug: c.slug })),
1558
+ slugCollisions,
1559
+ };
1560
+ emit({
1561
+ eventType: "allowlist.audit.ran",
1562
+ command,
1563
+ dryRun: true,
1564
+ result: { total: result.total, unresolvedCount: result.unresolvedCount, slugCollisions },
1565
+ });
1566
+ return print(result, asJson);
1567
+ }
1568
+
1569
+ // ═══════════════════════════════════════════════════════════
1570
+ // V2-ALPHA: ONCHAIN SKILLS (registry + intents)
1571
+ // Additive only: does not change any v1 command behavior.
1572
+ // ═══════════════════════════════════════════════════════════
1573
+ if (group === "v2" && sub === "skill") {
1574
+ const action = args._[2];
1575
+ const rpcUrl = String(args.rpc || process.env.APE_CLAW_V2_RPC_URL || process.env.RPC_URL_33139 || "").trim();
1576
+ const pk = String(args.privateKey || process.env.APE_CLAW_V2_PRIVATE_KEY || "").trim();
1577
+ const skillNftAddress = String(args.skillNft || process.env.APE_CLAW_V2_SKILL_NFT || "").trim();
1578
+ const registryAddress = String(args.registry || process.env.APE_CLAW_V2_SKILL_REGISTRY || "").trim();
1579
+
1580
+ if (!rpcUrl) return fail("Missing --rpc (or APE_CLAW_V2_RPC_URL / RPC_URL_33139)", command, args);
1581
+ if (!pk) return fail("Missing --privateKey (or APE_CLAW_V2_PRIVATE_KEY)", command, args);
1582
+ if (!skillNftAddress) return fail("Missing --skillNft (or APE_CLAW_V2_SKILL_NFT)", command, args);
1583
+ if (!registryAddress) return fail("Missing --registry (or APE_CLAW_V2_SKILL_REGISTRY)", command, args);
1584
+
1585
+ const account = privateKeyToAccount(pk.startsWith("0x") ? pk : `0x${pk}`);
1586
+ const publicClient = createPublicClient({ transport: http(rpcUrl) });
1587
+ const walletClient = createWalletClient({ transport: http(rpcUrl), account });
1588
+
1589
+ const skillNft = getContract({ address: skillNftAddress, abi: SkillNFT_ABI, client: { public: publicClient, wallet: walletClient } });
1590
+ const registry = getContract({ address: registryAddress, abi: SkillRegistry_ABI, client: { public: publicClient, wallet: walletClient } });
1591
+
1592
+ if (action === "mint") {
1593
+ const parentId = BigInt(args.parentId || 0);
1594
+ const royaltyReceiver = String(args["royalty-receiver"] || args.royaltyReceiver || "").trim();
1595
+ const royaltyBpsRaw = args["royalty-bps"] ?? args.royaltyBps;
1596
+ const royaltyBps = royaltyBpsRaw !== undefined && royaltyBpsRaw !== null && String(royaltyBpsRaw).trim() !== ""
1597
+ ? Number(royaltyBpsRaw)
1598
+ : 0;
1599
+ const useRoyalty = !!royaltyReceiver && Number.isFinite(royaltyBps) && royaltyBps > 0;
1600
+
1601
+ const hash = useRoyalty
1602
+ ? await skillNft.write.mintSkillWithRoyalty([parentId, royaltyReceiver, royaltyBps])
1603
+ : await skillNft.write.mintSkill([parentId]);
1604
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
1605
+ const nextId = await skillNft.read.nextSkillId();
1606
+ const skillId = BigInt(nextId) - 1n;
1607
+ const result = {
1608
+ ok: true,
1609
+ skillId: String(skillId),
1610
+ txHash: receipt.transactionHash,
1611
+ ...(useRoyalty ? { royaltyReceiver, royaltyBps } : {}),
1612
+ };
1613
+ emitEvent({ eventType: "v2.skill.minted", agentId: _agentId, command, dryRun: false, result });
1614
+ return print(result, asJson);
1615
+ }
1616
+
1617
+ if (action === "publish") {
1618
+ const skillId = BigInt(args.skillId || 0);
1619
+ const file = String(args.file || "").trim();
1620
+ const uri = String(args.uri || (file ? `file://${path.resolve(file)}` : "")).trim();
1621
+ const riskTier = Number(args.riskTier || 1);
1622
+ if (!skillId) return fail("Missing --skillId", command, args);
1623
+ if (!file) return fail("Missing --file (skillcard json)", command, args);
1624
+ const obj = readSkillcardJson(file);
1625
+ const versionHash = computeSkillVersionHash(obj.version);
1626
+ const contentHash = computeSkillcardContentHash(obj);
1627
+ const txHash = await registry.write.publishVersion([skillId, versionHash, contentHash, uri, riskTier]);
1628
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
1629
+ const result = {
1630
+ ok: true,
1631
+ skillId: String(skillId),
1632
+ versionHash,
1633
+ contentHash,
1634
+ uri,
1635
+ txHash: receipt.transactionHash,
1636
+ };
1637
+ emitEvent({ eventType: "v2.skill.version.published", agentId: _agentId, command, dryRun: false, result });
1638
+ return print(result, asJson);
1639
+ }
1640
+
1641
+ return fail("Unknown v2 skill action. Use: ape-claw v2 skill mint|publish", command, args);
1642
+ }
1643
+
1644
+ if (group === "v2" && sub === "intent") {
1645
+ const action = args._[2];
1646
+ const rpcUrl = String(args.rpc || process.env.APE_CLAW_V2_RPC_URL || process.env.RPC_URL_33139 || "").trim();
1647
+ const pk = String(args.privateKey || process.env.APE_CLAW_V2_PRIVATE_KEY || "").trim();
1648
+ const intentsAddress = String(args.intents || process.env.APE_CLAW_V2_INTENT_REGISTRY || "").trim();
1649
+ if (!rpcUrl) return fail("Missing --rpc (or APE_CLAW_V2_RPC_URL / RPC_URL_33139)", command, args);
1650
+ if (!pk) return fail("Missing --privateKey (or APE_CLAW_V2_PRIVATE_KEY)", command, args);
1651
+ if (!intentsAddress) return fail("Missing --intents (or APE_CLAW_V2_INTENT_REGISTRY)", command, args);
1652
+
1653
+ const account = privateKeyToAccount(pk.startsWith("0x") ? pk : `0x${pk}`);
1654
+ const publicClient = createPublicClient({ transport: http(rpcUrl) });
1655
+ const walletClient = createWalletClient({ transport: http(rpcUrl), account });
1656
+ const intents = getContract({ address: intentsAddress, abi: IntentRegistry_ABI, client: { public: publicClient, wallet: walletClient } });
1657
+
1658
+ if (action === "create") {
1659
+ const payload = String(args.payload || "").trim();
1660
+ const expiresAt = Number(args.expiresAt || 0);
1661
+ if (!payload) return fail("Missing --payload (stringified intent payload)", command, args);
1662
+ const intentHash = keccak256(toHex(payload));
1663
+ const txHash = await intents.write.createIntent([intentHash, expiresAt]);
1664
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
1665
+ const result = { ok: true, intentHash, txHash: receipt.transactionHash, expiresAt };
1666
+ emitEvent({ eventType: "v2.intent.created", agentId: _agentId, command, dryRun: false, result });
1667
+ return print(result, asJson);
1668
+ }
1669
+
1670
+ if (action === "cancel") {
1671
+ const intentId = BigInt(args.intentId || 0);
1672
+ if (!intentId) return fail("Missing --intentId", command, args);
1673
+ const txHash = await intents.write.cancelIntent([intentId]);
1674
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
1675
+ const result = { ok: true, intentId: String(intentId), txHash: receipt.transactionHash };
1676
+ emitEvent({ eventType: "v2.intent.cancelled", agentId: _agentId, command, dryRun: false, result });
1677
+ return print(result, asJson);
1678
+ }
1679
+
1680
+ return fail("Unknown v2 intent action. Use: ape-claw v2 intent create|cancel", command, args);
1681
+ }
1682
+
1683
+ if (group === "v2" && sub === "receipt") {
1684
+ const action = args._[2];
1685
+ const rpcUrl = String(args.rpc || process.env.APE_CLAW_V2_RPC_URL || process.env.RPC_URL_33139 || "").trim();
1686
+ const pk = String(args.privateKey || process.env.APE_CLAW_V2_PRIVATE_KEY || "").trim();
1687
+ const receiptsAddress = String(args.receipts || process.env.APE_CLAW_V2_RECEIPT_REGISTRY || "").trim();
1688
+ if (!rpcUrl) return fail("Missing --rpc (or APE_CLAW_V2_RPC_URL / RPC_URL_33139)", command, args);
1689
+ if (!receiptsAddress) return fail("Missing --receipts (or APE_CLAW_V2_RECEIPT_REGISTRY)", command, args);
1690
+
1691
+ const publicClient = createPublicClient({ transport: http(rpcUrl) });
1692
+ const receiptsRead = getContract({
1693
+ address: receiptsAddress,
1694
+ abi: ReceiptRegistry_ABI,
1695
+ client: { public: publicClient },
1696
+ });
1697
+
1698
+ if (action === "record") {
1699
+ if (!pk) return fail("Missing --privateKey (or APE_CLAW_V2_PRIVATE_KEY)", command, args);
1700
+ const account = privateKeyToAccount(pk.startsWith("0x") ? pk : `0x${pk}`);
1701
+ const walletClient = createWalletClient({ transport: http(rpcUrl), account });
1702
+ const receipts = getContract({
1703
+ address: receiptsAddress,
1704
+ abi: ReceiptRegistry_ABI,
1705
+ client: { public: publicClient, wallet: walletClient },
1706
+ });
1707
+ const traceId = String(args.traceId || args.trace || "").trim();
1708
+ if (!traceId) return fail("Missing --traceId", command, args);
1709
+ const subjectStr = String(args.subject || (_agentId ? `agent:${_agentId}` : "agent:unknown")).trim();
1710
+ const uri = String(args.uri || "").trim();
1711
+ const payloadStr = String(args.payload || "").trim();
1712
+ let payloadObj = {};
1713
+ if (payloadStr) {
1714
+ try {
1715
+ payloadObj = JSON.parse(payloadStr);
1716
+ } catch {
1717
+ return fail("Invalid --payload JSON string", command, args);
1718
+ }
1719
+ }
1720
+
1721
+ const traceIdHash = keccak256(toHex(traceId));
1722
+ const contentHash = keccak256(toHex(stableJsonStringify({ subject: subjectStr, payload: payloadObj })));
1723
+ const subjectHash = keccak256(toHex(subjectStr));
1724
+ const txHash = await receipts.write.recordReceipt([traceIdHash, contentHash, subjectHash, uri]);
1725
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
1726
+ const result = {
1727
+ ok: true,
1728
+ traceId,
1729
+ traceIdHash,
1730
+ contentHash,
1731
+ subject: subjectStr,
1732
+ subjectHash,
1733
+ uri,
1734
+ txHash: receipt.transactionHash,
1735
+ };
1736
+ emitEvent({ eventType: "v2.receipt.recorded", agentId: _agentId, command, dryRun: false, result });
1737
+ return print(result, asJson);
1738
+ }
1739
+
1740
+ if (action === "get") {
1741
+ const traceId = String(args.traceId || args.trace || "").trim();
1742
+ if (!traceId) return fail("Missing --traceId", command, args);
1743
+ const traceIdHash = keccak256(toHex(traceId));
1744
+ const isRecorded = await receiptsRead.read.isRecorded([traceIdHash]);
1745
+ const receipt = isRecorded ? await receiptsRead.read.getReceipt([traceIdHash]) : null;
1746
+ const result = {
1747
+ ok: true,
1748
+ traceId,
1749
+ traceIdHash,
1750
+ isRecorded: Boolean(isRecorded),
1751
+ receipt,
1752
+ };
1753
+ emitEvent({ eventType: "v2.receipt.read", agentId: _agentId, command, dryRun: true, result: { traceId, traceIdHash, isRecorded } });
1754
+ return print(result, asJson);
1755
+ }
1756
+
1757
+ return fail("Unknown v2 receipt action. Use: ape-claw v2 receipt record|get", command, args);
1758
+ }
1759
+
1760
+ // ── v2 vault (PodVault) ──
1761
+ if (group === "v2" && sub === "vault") {
1762
+ const action = String(args._[2] || "").toLowerCase();
1763
+ const rpcUrl = String(args.rpc || args.rpcUrl || process.env.APE_CLAW_V2_RPC_URL || "").trim();
1764
+ const vaultAddr = String(args.vault || args.podVault || process.env.APE_CLAW_V2_POD_VAULT || "").trim();
1765
+ if (!rpcUrl) return fail("Missing --rpc <url> or APE_CLAW_V2_RPC_URL", command, args);
1766
+ if (!vaultAddr) return fail("Missing --vault <address> or APE_CLAW_V2_POD_VAULT", command, args);
1767
+
1768
+ const publicVault = createPublicClient({ transport: http(rpcUrl) });
1769
+ const vault = getContract({ address: vaultAddr, abi: PodVault_ABI, client: publicVault });
1770
+
1771
+ if (action === "status") {
1772
+ const totalShares = await vault.read.totalShares();
1773
+ const totalReleased = await vault.read.totalReleasedNative();
1774
+ const mCount = await vault.read.memberCount();
1775
+ const balance = await publicVault.getBalance({ address: vaultAddr });
1776
+ const members = [];
1777
+ for (let i = 0n; i < mCount; i++) {
1778
+ const addr = await vault.read.memberAt([i]);
1779
+ const sh = await vault.read.shares([addr]);
1780
+ const pending = await vault.read.pendingNative([addr]);
1781
+ members.push({ address: addr, shares: sh.toString(), pendingNative: pending.toString() });
1782
+ }
1783
+ return print({
1784
+ ok: true,
1785
+ podVault: vaultAddr,
1786
+ totalShares: totalShares.toString(),
1787
+ totalReleasedNative: totalReleased.toString(),
1788
+ balance: balance.toString(),
1789
+ memberCount: Number(mCount),
1790
+ members,
1791
+ }, asJson);
1792
+ }
1793
+
1794
+ if (action === "release") {
1795
+ const pk = String(args.privateKey || process.env.APE_CLAW_V2_PRIVATE_KEY || "").trim();
1796
+ const member = String(args.member || "").trim();
1797
+ if (!pk) return fail("Missing --privateKey or APE_CLAW_V2_PRIVATE_KEY", command, args);
1798
+ if (!member) return fail("Missing --member <address>", command, args);
1799
+ const account = privateKeyToAccount(pk.startsWith("0x") ? pk : `0x${pk}`);
1800
+ const walletVault = createWalletClient({ account, transport: http(rpcUrl) });
1801
+ const wVault = getContract({ address: vaultAddr, abi: PodVault_ABI, client: walletVault });
1802
+ const tx = await wVault.write.releaseNative([member]);
1803
+ await publicVault.waitForTransactionReceipt({ hash: tx });
1804
+ emitEvent({ eventType: "v2.vault.release", agentId: _agentId, command, result: { tx, member } });
1805
+ return print({ ok: true, tx, member, action: "releaseNative" }, asJson);
1806
+ }
1807
+
1808
+ return fail("Unknown v2 vault action. Use: ape-claw v2 vault status|release", command, args);
1809
+ }
1810
+
1811
+ // ── v2 agent (AgentAccount) ──
1812
+ if (group === "v2" && sub === "agent") {
1813
+ const action = String(args._[2] || "").toLowerCase();
1814
+ if (action === "execute") {
1815
+ const rpcUrl = String(args.rpc || args.rpcUrl || process.env.APE_CLAW_V2_RPC_URL || "").trim();
1816
+ const pk = String(args.privateKey || process.env.APE_CLAW_V2_PRIVATE_KEY || "").trim();
1817
+ const agentAddr = String(args.agentAccount || process.env.APE_CLAW_V2_AGENT_ACCOUNT || "").trim();
1818
+ const moduleAddr = String(args.module || "").trim();
1819
+ const inputData = String(args.input || "0x").trim();
1820
+ const value = String(args.value || "0").trim();
1821
+ const traceId = String(args.traceId || `agent_exec_${Date.now()}`).trim();
1822
+ const subject = String(args.subject || `agent:${_agentId}`).trim();
1823
+ const uri = String(args.uri || "").trim();
1824
+
1825
+ if (!rpcUrl) return fail("Missing --rpc", command, args);
1826
+ if (!pk) return fail("Missing --privateKey", command, args);
1827
+ if (!agentAddr) return fail("Missing --agentAccount or APE_CLAW_V2_AGENT_ACCOUNT", command, args);
1828
+ if (!moduleAddr) return fail("Missing --module <address>", command, args);
1829
+
1830
+ const account = privateKeyToAccount(pk.startsWith("0x") ? pk : `0x${pk}`);
1831
+ const pub = createPublicClient({ transport: http(rpcUrl) });
1832
+ const wallet = createWalletClient({ account, transport: http(rpcUrl) });
1833
+ const agentContract = getContract({ address: agentAddr, abi: AgentAccount_ABI, client: wallet });
1834
+
1835
+ const traceIdHash = keccak256(toHex(traceId));
1836
+ const subjectHash = keccak256(toHex(subject));
1837
+
1838
+ const tx = await agentContract.write.executeSkill([
1839
+ moduleAddr,
1840
+ inputData,
1841
+ BigInt(value),
1842
+ traceIdHash,
1843
+ subjectHash,
1844
+ uri,
1845
+ ], { value: BigInt(value) });
1846
+ await pub.waitForTransactionReceipt({ hash: tx });
1847
+ emitEvent({ eventType: "v2.agent.execute", agentId: _agentId, command, result: { tx, module: moduleAddr, traceId } });
1848
+ return print({ ok: true, tx, module: moduleAddr, traceId, traceIdHash, subjectHash }, asJson);
1849
+ }
1850
+ return fail("Unknown v2 agent action. Use: ape-claw v2 agent execute", command, args);
1851
+ }
1852
+
1853
+ if (group === "pod" && sub === "init") {
1854
+ const target = String(args.dir || "./pod-workspace").trim();
1855
+ const templatesDir = path.join(process.cwd(), "pod", "templates");
1856
+ try {
1857
+ const result = initPodWorkspace({ targetDir: target, templatesDir });
1858
+ emitEvent({ eventType: "pod.init.completed", agentId: _agentId, command, dryRun: false, result });
1859
+ return print(result, asJson);
1860
+ } catch (err) {
1861
+ return fail(err.message || "pod init failed", command, args);
1862
+ }
1863
+ }
1864
+
1865
+ const helpObj = {
1866
+ ok: false,
1867
+ error: `Unknown command: ${args._.join(" ")}`,
1868
+ commands: {
1869
+ doctor: "ape-claw doctor --json",
1870
+ quickstart: "ape-claw quickstart --json",
1871
+ "clawbot register": "ape-claw clawbot register --agent-id <id> --name <name> [--api https://api.apeclaw.ai --invite <token>] [--registration-key <key>] --json",
1872
+ "clawbot list": "ape-claw clawbot list --json",
1873
+ "auth set": "ape-claw auth set [--agent-id <id>] [--agent-token <token>] [--opensea-api-key <key>] [--private-key <pk>] --json",
1874
+ "auth show": "ape-claw auth show --json",
1875
+ "auth clear": "ape-claw auth clear --field <agent-id|agent-token|opensea-api-key|private-key> --json",
1876
+ "chain info": "ape-claw chain info --json",
1877
+ "market collections": "ape-claw market collections --recommended --json",
1878
+ "market listings": "ape-claw market listings --collection <slug> --maxPrice <n> --json",
1879
+ "nft autobuy": "ape-claw nft autobuy --count <n> [--minPrice <n>] --maxPrice <n> [--budget <n>] [--scan <n>] [--all] --json",
1880
+ "nft autobuy (execute)": "ape-claw nft autobuy --count <n> [--minPrice <n>] --maxPrice <n> --execute --autonomous --json",
1881
+ "nft quote-buy": "ape-claw nft quote-buy --collection <slug> --tokenId <id> --maxPrice <n> --currency APE --json",
1882
+ "nft simulate": "ape-claw nft simulate --quote <quoteId> --json",
1883
+ "nft buy": 'ape-claw nft buy --quote <quoteId> --execute --confirm "BUY <collection> #<tokenId> <priceApe> APE" --json',
1884
+ "nft buy (autonomous)": "ape-claw nft buy --quote <quoteId> --execute --autonomous --json",
1885
+ "bridge quote": "ape-claw bridge quote --from <chain> --amount <n> --json",
1886
+ "bridge execute": 'ape-claw bridge execute --request <requestId> --execute --confirm "BRIDGE <amount> <token> <from>-><to>" --json',
1887
+ "bridge execute (autonomous)": "ape-claw bridge execute --request <requestId> --execute --autonomous --json",
1888
+ "bridge status": "ape-claw bridge status --request <requestId> --json",
1889
+ "allowlist audit": "ape-claw allowlist audit --json",
1890
+ "skill install": "ape-claw skill install --scope local [--starter-pack | --no-starter-pack] --json",
1891
+ "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",
1892
+ "v2 skill publish": "ape-claw v2 skill publish --rpc <url> --privateKey 0x... --registry 0x... --skillId <id> --file <skillcard.json> [--uri ipfs://...] [--riskTier 1] --json",
1893
+ "v2 intent create": "ape-claw v2 intent create --rpc <url> --privateKey 0x... --intents 0x... --payload '{...}' [--expiresAt <unixSec>] --json",
1894
+ "v2 intent cancel": "ape-claw v2 intent cancel --rpc <url> --privateKey 0x... --intents 0x... --intentId <id> --json",
1895
+ "v2 receipt record": "ape-claw v2 receipt record --rpc <url> --privateKey 0x... --receipts 0x... --traceId <trace> [--subject <string>] [--payload '{...}'] [--uri ipfs://...] --json",
1896
+ "v2 receipt get": "ape-claw v2 receipt get --rpc <url> --receipts 0x... --traceId <trace> --json",
1897
+ "v2 vault status": "ape-claw v2 vault status --rpc <url> --vault 0x... --json",
1898
+ "v2 vault release": "ape-claw v2 vault release --rpc <url> --privateKey 0x... --vault 0x... --member 0x... --json",
1899
+ "v2 agent execute": "ape-claw v2 agent execute --rpc <url> --privateKey 0x... --agentAccount 0x... --module 0x... [--input 0x...] [--value 0] [--traceId ...] --json",
1900
+ "pod init": "ape-claw pod init --dir ./pod-workspace --json",
1901
+ },
1902
+ globalFlags: {
1903
+ "--json": "Recommended for deterministic parsing (all output as JSON).",
1904
+ "--agent-id <id>": "Clawbot agent ID (or APE_CLAW_AGENT_ID env var).",
1905
+ "--agent-token <token>": "Clawbot auth token (or APE_CLAW_AGENT_TOKEN env var).",
1906
+ "--opensea-api-key <key>": "For auth set: persist OpenSea key in local auth profile.",
1907
+ "--private-key <pk>": "For auth set: persist wallet private key in local auth profile.",
1908
+ },
1909
+ note: "Global flags (--agent-id, --agent-token, --json) can appear anywhere in the command.",
1910
+ };
1911
+ if (asJson) {
1912
+ console.log(JSON.stringify(helpObj, null, 2));
1913
+ } else {
1914
+ console.log(`Unknown command: ${args._.join(" ")}\n`);
1915
+ console.log("Commands:");
1916
+ for (const [name, example] of Object.entries(helpObj.commands)) {
1917
+ console.log(` ${name.padEnd(22)} ${example}`);
1918
+ }
1919
+ console.log("\nGlobal flags: --json --agent-id <id> --agent-token <token>");
1920
+ console.log("Note: global flags can appear anywhere in the command.\n");
1921
+ }
1922
+ process.exit(1);
1923
+ }
1924
+
1925
+ main().catch((err) => {
1926
+ console.error(err.message || String(err));
1927
+ process.exit(1);
1928
+ });
1929
+