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
@@ -0,0 +1,159 @@
1
+ import { createWalletClient, defineChain, encodeFunctionData, http } from "viem";
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ import { SeaportABI } from "@opensea/seaport-js/lib/abi/Seaport.js";
4
+
5
+ const OPENSEA_API_BASE = "https://api.opensea.io/api/v2";
6
+
7
+ function toHexPrivateKey(pk) {
8
+ const raw = String(pk || "").trim();
9
+ if (!raw) return "";
10
+ return raw.startsWith("0x") ? raw : `0x${raw}`;
11
+ }
12
+
13
+ function functionNameFromSignature(sig) {
14
+ const raw = String(sig || "").split("(")[0];
15
+ if (raw === "fulfillBasicOrder_efficient_6GL6yc") return "fulfillBasicOrder";
16
+ return raw;
17
+ }
18
+
19
+ function buildParams(functionName, inputData = {}) {
20
+ if (functionName === "fulfillAdvancedOrder" && "advancedOrder" in inputData) {
21
+ return [
22
+ inputData.advancedOrder,
23
+ inputData.criteriaResolvers || [],
24
+ inputData.fulfillerConduitKey ||
25
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
26
+ inputData.recipient,
27
+ ];
28
+ }
29
+ if (functionName === "fulfillBasicOrder" && "basicOrderParameters" in inputData) {
30
+ return [inputData.basicOrderParameters];
31
+ }
32
+ if (functionName === "fulfillOrder" && "order" in inputData) {
33
+ return [
34
+ inputData.order,
35
+ inputData.fulfillerConduitKey ||
36
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
37
+ inputData.recipient,
38
+ ];
39
+ }
40
+ return Object.values(inputData);
41
+ }
42
+
43
+ async function fetchJson(url, init = {}) {
44
+ const res = await fetch(url, init);
45
+ if (!res.ok) {
46
+ const body = await res.text().catch(() => "");
47
+ throw new Error(`OpenSea HTTP ${res.status}${body ? `: ${body.slice(0, 220)}` : ""}`);
48
+ }
49
+ return res.json();
50
+ }
51
+
52
+ function openseaChainSlug(chainId) {
53
+ const id = Number(chainId);
54
+ if (id === 33139) return "ape_chain";
55
+ if (id === 1) return "ethereum";
56
+ if (id === 8453) return "base";
57
+ if (id === 42161) return "arbitrum";
58
+ throw new Error(`Unsupported OpenSea chainId for fulfillment: ${chainId}`);
59
+ }
60
+
61
+ export async function getListingFulfillmentData({
62
+ apiKey,
63
+ orderHash,
64
+ protocolAddress,
65
+ chainId,
66
+ fulfillerAddress,
67
+ privateKey,
68
+ assetContractAddress,
69
+ tokenId,
70
+ includeOptionalCreatorFees = false,
71
+ }) {
72
+ if (!apiKey) throw new Error("Missing OPENSEA_API_KEY for live fulfillment.");
73
+ if (!orderHash) throw new Error("Missing order hash for fulfillment.");
74
+ const resolvedFulfiller =
75
+ fulfillerAddress || (privateKey ? privateKeyToAccount(toHexPrivateKey(privateKey)).address : "");
76
+ if (!resolvedFulfiller) throw new Error("Missing fulfiller address for fulfillment.");
77
+
78
+ const payload = {
79
+ listing: {
80
+ hash: orderHash,
81
+ chain: openseaChainSlug(chainId),
82
+ protocol_address: protocolAddress,
83
+ },
84
+ fulfiller: {
85
+ address: resolvedFulfiller,
86
+ },
87
+ units_to_fill: "1",
88
+ include_optional_creator_fees: includeOptionalCreatorFees,
89
+ };
90
+
91
+ if (assetContractAddress && tokenId) {
92
+ payload.consideration = {
93
+ asset_contract_address: assetContractAddress,
94
+ token_id: String(tokenId),
95
+ };
96
+ }
97
+
98
+ const data = await fetchJson(`${OPENSEA_API_BASE}/listings/fulfillment_data`, {
99
+ method: "POST",
100
+ headers: {
101
+ "x-api-key": apiKey,
102
+ accept: "application/json",
103
+ "content-type": "application/json",
104
+ "user-agent": "ape-claw/0.1.0",
105
+ },
106
+ body: JSON.stringify(payload),
107
+ });
108
+ return data;
109
+ }
110
+
111
+ export async function executeListingFulfillmentTx({
112
+ fulfillmentData,
113
+ privateKey,
114
+ rpcUrl,
115
+ }) {
116
+ const hexPk = toHexPrivateKey(privateKey);
117
+ if (!hexPk) throw new Error("Missing APE_CLAW_PRIVATE_KEY for live nft execute.");
118
+ const tx = fulfillmentData?.fulfillment_data?.transaction;
119
+ if (!tx) throw new Error("OpenSea fulfillment data missing transaction object.");
120
+ const fn = functionNameFromSignature(tx.function);
121
+ const params = buildParams(fn, tx.input_data || {});
122
+ const calldata = encodeFunctionData({
123
+ abi: SeaportABI,
124
+ functionName: fn,
125
+ args: params,
126
+ });
127
+
128
+ const chainId = Number(tx.chain);
129
+ const nativeCurrency = chainId === 33139
130
+ ? { name: "ApeCoin", symbol: "APE", decimals: 18 }
131
+ : { name: "Ether", symbol: "ETH", decimals: 18 };
132
+ const chain = defineChain({
133
+ id: chainId,
134
+ name: `chain-${chainId}`,
135
+ nativeCurrency,
136
+ rpcUrls: { default: { http: [rpcUrl] } },
137
+ });
138
+ const account = privateKeyToAccount(hexPk);
139
+ const client = createWalletClient({
140
+ account,
141
+ chain,
142
+ transport: http(rpcUrl),
143
+ });
144
+
145
+ const hash = await client.sendTransaction({
146
+ to: tx.to,
147
+ data: calldata,
148
+ value: BigInt(tx.value || "0"),
149
+ chain,
150
+ });
151
+
152
+ return {
153
+ txHash: hash,
154
+ chainId,
155
+ to: tx.to,
156
+ functionName: fn,
157
+ };
158
+ }
159
+
@@ -0,0 +1,17 @@
1
+ import path from "node:path";
2
+
3
+ const ROOT_OVERRIDE = String(process.env.APE_CLAW_ROOT || "").trim();
4
+ const STATE_DIR_OVERRIDE = String(process.env.APE_CLAW_STATE_DIR || "").trim();
5
+
6
+ export const ROOT = ROOT_OVERRIDE ? path.resolve(ROOT_OVERRIDE) : process.cwd();
7
+ export const STATE_DIR = STATE_DIR_OVERRIDE ? path.resolve(STATE_DIR_OVERRIDE) : path.join(ROOT, "state");
8
+ export const POLICY_PATH = path.join(ROOT, "config", "policy.json");
9
+ export const ALLOWLIST_PATH = path.join(ROOT, "allowlists", "recommended.apechain.json");
10
+ export const OPENSEA_OVERRIDES_PATH = path.join(ROOT, "allowlists", "opensea-slug-overrides.json");
11
+ export const EVENTS_PATH = path.join(STATE_DIR, "events.jsonl");
12
+ export const QUOTES_PATH = path.join(STATE_DIR, "quotes.json");
13
+ export const BRIDGE_REQUESTS_PATH = path.join(STATE_DIR, "bridge-requests.json");
14
+ export const CLAWBOTS_PATH = path.join(ROOT, "config", "clawbots.json");
15
+ export const CHAT_PATH = path.join(STATE_DIR, "chat.jsonl");
16
+ export const INVITES_PATH = path.join(STATE_DIR, "invites.json");
17
+
@@ -0,0 +1,40 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function ensureDir(p) {
5
+ fs.mkdirSync(p, { recursive: true });
6
+ }
7
+
8
+ function copyDir(srcDir, dstDir) {
9
+ ensureDir(dstDir);
10
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
11
+ for (const e of entries) {
12
+ const src = path.join(srcDir, e.name);
13
+ const dst = path.join(dstDir, e.name);
14
+ if (e.isDirectory()) copyDir(src, dst);
15
+ else fs.copyFileSync(src, dst);
16
+ }
17
+ }
18
+
19
+ export function initPodWorkspace({ targetDir, templatesDir }) {
20
+ if (!targetDir) throw new Error("targetDir required");
21
+ if (!templatesDir) throw new Error("templatesDir required");
22
+ const absTarget = path.resolve(targetDir);
23
+ const absTemplates = path.resolve(templatesDir);
24
+ if (!fs.existsSync(absTemplates)) throw new Error(`missing templates: ${absTemplates}`);
25
+ ensureDir(absTarget);
26
+ copyDir(absTemplates, absTarget);
27
+ // Ensure the revenue-sharing agreement exists even if templates are old/partial.
28
+ const rev = path.join(absTarget, "REVENUE_SHARING.md");
29
+ if (!fs.existsSync(rev)) {
30
+ fs.writeFileSync(rev, [
31
+ "# REVENUE_SHARING.md (THE POD)",
32
+ "",
33
+ "When an agent installs/uses onchain skills in this Pod, it agrees to route skill-related revenue",
34
+ "to the Pod's shared receiver (e.g. PodVault) and share among Pod members.",
35
+ "",
36
+ ].join("\n"));
37
+ }
38
+ return { ok: true, targetDir: absTarget };
39
+ }
40
+
@@ -0,0 +1,112 @@
1
+ import { readJson } from "./io.mjs";
2
+ import { POLICY_PATH, ALLOWLIST_PATH } from "./paths.mjs";
3
+
4
+ function normalizeKey(value) {
5
+ return String(value || "")
6
+ .toLowerCase()
7
+ .trim()
8
+ .replace(/[^a-z0-9]/g, "");
9
+ }
10
+
11
+ export function loadPolicy() {
12
+ const policy = readJson(POLICY_PATH, null);
13
+ if (!policy) {
14
+ throw new Error("Missing config/policy.json. Copy config/policy.example.json first.");
15
+ }
16
+ return policy;
17
+ }
18
+
19
+ export function loadAllowlist() {
20
+ const list = readJson(ALLOWLIST_PATH, []);
21
+ return normalizeAllowlist(Array.isArray(list) ? list : []);
22
+ }
23
+
24
+ export function normalizeAllowlist(allowlist) {
25
+ const dedup = new Map();
26
+ for (const item of allowlist) {
27
+ // Dedup by slug or name only — rank is never used as an identity key
28
+ const slug = String(item.slug || "").toLowerCase();
29
+ const name = String(item.name || "").toLowerCase();
30
+ const key = slug || name;
31
+ if (!key) continue; // skip entries with no slug or name
32
+ const existing = dedup.get(key);
33
+ if (!existing) {
34
+ dedup.set(key, item);
35
+ continue;
36
+ }
37
+ const candidateHasCA = Boolean(item.contractAddress);
38
+ const existingHasCA = Boolean(existing.contractAddress);
39
+ if (candidateHasCA && !existingHasCA) dedup.set(key, item);
40
+ }
41
+ return [...dedup.values()];
42
+ }
43
+
44
+ export function resolveCollectionTarget(collection, allowlist) {
45
+ const input = String(collection || "").trim();
46
+ if (!input) return { matches: [], exact: null };
47
+ const needleLower = input.toLowerCase();
48
+ const needleNorm = normalizeKey(input);
49
+ const matches = allowlist.filter((c) => {
50
+ const name = String(c.name || "");
51
+ const slug = String(c.slug || "");
52
+ const ca = String(c.contractAddress || "");
53
+ return (
54
+ needleLower === name.toLowerCase() ||
55
+ needleLower === slug.toLowerCase() ||
56
+ (ca && needleLower === ca.toLowerCase()) ||
57
+ needleNorm === normalizeKey(name) ||
58
+ needleNorm === normalizeKey(slug)
59
+ );
60
+ });
61
+ return { matches, exact: matches.length === 1 ? matches[0] : null };
62
+ }
63
+
64
+ export function enforceBuyPolicy({
65
+ policy,
66
+ collection,
67
+ maxPrice,
68
+ currency,
69
+ allowUnsafe = false,
70
+ allowlist = [],
71
+ }) {
72
+ const errors = [];
73
+ const warnings = [];
74
+ const allowedCurrencies = new Set(policy.nftBuy.currencyAllowlist || []);
75
+ const target = resolveCollectionTarget(collection, allowlist);
76
+ if (!allowedCurrencies.has(currency)) {
77
+ errors.push(`Currency ${currency} is not allowed.`);
78
+ }
79
+ if (Number(maxPrice) > Number(policy.nftBuy.maxPricePerTx)) {
80
+ errors.push(`maxPrice exceeds policy maxPricePerTx (${policy.nftBuy.maxPricePerTx}).`);
81
+ }
82
+ if (
83
+ policy.market.collectionAllowlistMode === "recommended-only" &&
84
+ !allowUnsafe &&
85
+ target.matches.length === 0
86
+ ) {
87
+ errors.push(`Collection ${collection} is not in recommended allowlist.`);
88
+ }
89
+ if (target.matches.length > 1 && !allowUnsafe) {
90
+ errors.push(
91
+ `Collection ${collection} is ambiguous (${target.matches.length} matches). Use contract address or unique slug.`,
92
+ );
93
+ }
94
+ if (target.exact && !target.exact.contractAddress && !allowUnsafe) {
95
+ errors.push(
96
+ `Collection ${target.exact.name} has unresolved contractAddress. Resolve CA or pass --allow-unsafe.`,
97
+ );
98
+ }
99
+ if (allowUnsafe) warnings.push("Unsafe override enabled.");
100
+ return { ok: errors.length === 0, errors, warnings, target: target.exact };
101
+ }
102
+
103
+ export function enforceBridgePolicy({ policy, feeBps }) {
104
+ if (Number(feeBps) > Number(policy.bridge.maxBridgeFeeBps)) {
105
+ return {
106
+ ok: false,
107
+ errors: [`Bridge fee ${feeBps} bps exceeds cap ${policy.bridge.maxBridgeFeeBps} bps.`],
108
+ };
109
+ }
110
+ return { ok: true, errors: [] };
111
+ }
112
+
@@ -0,0 +1,49 @@
1
+ const RELAY_API_BASE = process.env.RELAY_API_BASE || "https://api.relay.link";
2
+ let relayRpcMapCache = null;
3
+
4
+ function relayHeaders() {
5
+ return { accept: "application/json" };
6
+ }
7
+
8
+ async function fetchJson(url, init = {}) {
9
+ const res = await fetch(url, init);
10
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
11
+ return res.json();
12
+ }
13
+
14
+ export async function getRelayRpcMap() {
15
+ if (relayRpcMapCache) return relayRpcMapCache;
16
+ try {
17
+ const data = await fetchJson(`${RELAY_API_BASE}/chains`, {
18
+ method: "GET",
19
+ headers: relayHeaders(),
20
+ });
21
+ const map = new Map();
22
+ for (const chain of data?.chains || []) {
23
+ const id = Number(chain?.id);
24
+ const rpc = String(chain?.httpRpcUrl || "");
25
+ if (Number.isFinite(id) && rpc) map.set(id, rpc);
26
+ }
27
+ relayRpcMapCache = map;
28
+ } catch {
29
+ relayRpcMapCache = new Map();
30
+ }
31
+ return relayRpcMapCache;
32
+ }
33
+
34
+ export async function resolveRpcUrl(chainId, policy = {}) {
35
+ const direct = process.env[`RPC_URL_${chainId}`];
36
+ if (direct) return direct;
37
+ const policyRpc =
38
+ Number(chainId) === Number(policy?.apechainChainId) ? String(policy?.apechainRpcUrl || "") : "";
39
+ const isLocalPolicyRpc =
40
+ policyRpc.startsWith("http://localhost") || policyRpc.startsWith("https://localhost") ||
41
+ policyRpc.startsWith("http://127.0.0.1") || policyRpc.startsWith("https://127.0.0.1");
42
+ if (policyRpc && !isLocalPolicyRpc) return policyRpc;
43
+ const relayMap = await getRelayRpcMap();
44
+ const relayRpc = relayMap.get(Number(chainId));
45
+ if (relayRpc) return relayRpc;
46
+ if (policyRpc) return policyRpc;
47
+ throw new Error(`Missing RPC URL for chain ${chainId}. Set RPC_URL_${chainId}.`);
48
+ }
49
+
@@ -0,0 +1,92 @@
1
+ import { appendJsonl, nowIso, randomId } from "./io.mjs";
2
+ import { EVENTS_PATH } from "./paths.mjs";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ const TELEMETRY_BASE = String(process.env.APE_CLAW_TELEMETRY_URL || "").trim().replace(/\/+$/, "");
8
+ const TELEMETRY_REMOTE_ONLY = /^(1|true|yes|on)$/i.test(String(process.env.APE_CLAW_TELEMETRY_REMOTE_ONLY || "").trim());
9
+
10
+ function authStorePath() {
11
+ return path.join(os.homedir(), ".ape-claw", "auth.json");
12
+ }
13
+
14
+ function loadAuthStore() {
15
+ try {
16
+ const p = authStorePath();
17
+ if (!fs.existsSync(p)) return {};
18
+ const raw = fs.readFileSync(p, "utf8");
19
+ const j = JSON.parse(raw);
20
+ return j && typeof j === "object" ? j : {};
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ function authHeadersForRemoteEvent(evt) {
27
+ const stored = loadAuthStore();
28
+ const fromEnvId = String(process.env.APE_CLAW_AGENT_ID || "").trim();
29
+ const fromEnvToken = String(process.env.APE_CLAW_AGENT_TOKEN || "").trim();
30
+ const agentId = fromEnvId || String(stored.agentId || "").trim() || String(evt.agentId || "").trim();
31
+ const token = fromEnvToken || String(stored.agentToken || "").trim();
32
+ return {
33
+ "content-type": "application/json",
34
+ ...(agentId ? { "x-agent-id": agentId } : {}),
35
+ ...(token ? { "x-agent-token": token } : {}),
36
+ };
37
+ }
38
+
39
+ async function sendRemoteEvent(evt) {
40
+ if (!TELEMETRY_BASE) return;
41
+ const url = `${TELEMETRY_BASE}/api/events`;
42
+ const res = await fetch(url, {
43
+ method: "POST",
44
+ headers: authHeadersForRemoteEvent(evt),
45
+ body: JSON.stringify(evt),
46
+ });
47
+ if (!res.ok) {
48
+ const body = await res.text().catch(() => "");
49
+ throw new Error(`remote telemetry failed (${res.status})${body ? `: ${body.slice(0, 200)}` : ""}`);
50
+ }
51
+ }
52
+
53
+ export function emitEvent({
54
+ eventType,
55
+ agentId = "local-cli",
56
+ sessionId = "local-session",
57
+ traceId = null,
58
+ command = "",
59
+ dryRun = true,
60
+ chainId = 33139,
61
+ payload = {},
62
+ result = {},
63
+ ok = true,
64
+ error = null,
65
+ }) {
66
+ const evt = {
67
+ v: 1,
68
+ ts: nowIso(),
69
+ eventType,
70
+ agentId,
71
+ sessionId,
72
+ traceId: traceId || randomId("trace"),
73
+ command,
74
+ dryRun,
75
+ chainId,
76
+ payload,
77
+ result,
78
+ ok,
79
+ error,
80
+ };
81
+ if (!TELEMETRY_REMOTE_ONLY) {
82
+ appendJsonl(EVENTS_PATH, evt);
83
+ }
84
+ if (TELEMETRY_BASE) {
85
+ void sendRemoteEvent(evt).catch((err) => {
86
+ // Telemetry emission must never break command execution.
87
+ console.warn(`[telemetry] ${err.message}`);
88
+ });
89
+ }
90
+ return evt;
91
+ }
92
+