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.
- package/.cursor/skills/ape-claw/SKILL.md +322 -0
- package/LICENSE +21 -0
- package/README.md +826 -0
- package/allowlists/opensea-slug-overrides.json +13 -0
- package/allowlists/recommended.apechain.json +322 -0
- package/config/clawbots.example.json +3 -0
- package/config/policy.example.json +27 -0
- package/data/starter-pack-bundle.json +1 -0
- package/data/starter-pack.json +495 -0
- package/docs/ACP_BOUNTIES.md +108 -0
- package/docs/APECLAW_V2_ALPHA.md +206 -0
- package/docs/AUTONOMY_AND_SUBSTRATE.md +69 -0
- package/docs/CLAWBOTS_AND_INVITES.md +102 -0
- package/docs/CLI_GUIDE.md +124 -0
- package/docs/CONTRIBUTING.md +130 -0
- package/docs/DASHBOARD_GUIDE.md +108 -0
- package/docs/GLOBAL_BACKEND.md +145 -0
- package/docs/ONCHAIN_V2_GUIDE.md +140 -0
- package/docs/PRODUCT_OVERVIEW.md +127 -0
- package/docs/README.md +40 -0
- package/docs/SKILLCARDS_AND_IMPORTER.md +147 -0
- package/docs/STARTER_PACK.md +297 -0
- package/docs/SUPPORTED_NETWORKS.md +58 -0
- package/docs/TELEMETRY_AND_EVENTS.md +103 -0
- package/docs/THE_POD_RUNNER.md +198 -0
- package/docs/V1_WORKFLOWS.md +108 -0
- package/docs/V2_ONCHAIN_SKILLS.md +157 -0
- package/docs/WEB4_PLAN_STATUS.md +95 -0
- package/docs/WEB4_SWARM_MODEL.md +104 -0
- package/docs/archive/AUTONOMY_AND_SUBSTRATE.md +66 -0
- package/docs/archive/WEB4_PLAN_STATUS.md +93 -0
- package/docs/archive/WEB4_SWARM_MODEL.md +98 -0
- package/docs/developer/01-architecture.md +345 -0
- package/docs/developer/02-contracts.md +1034 -0
- package/docs/developer/03-writing-modules.md +513 -0
- package/docs/developer/04-skillcard-spec.md +336 -0
- package/docs/developer/05-backend-api.md +1079 -0
- package/docs/developer/06-telemetry.md +798 -0
- package/docs/developer/07-testing.md +546 -0
- package/docs/developer/08-contributing.md +211 -0
- package/docs/operator/01-quickstart.md +49 -0
- package/docs/operator/02-dashboard.md +174 -0
- package/docs/operator/03-cli-reference.md +818 -0
- package/docs/operator/04-skills-library.md +169 -0
- package/docs/operator/05-pod-operations.md +314 -0
- package/docs/operator/06-deployment.md +299 -0
- package/docs/operator/07-safety-and-policy.md +311 -0
- package/docs/operator/08-troubleshooting.md +457 -0
- package/docs/operator/09-env-reference.md +238 -0
- package/docs/social/STARTER_PACK_THREAD.md +209 -0
- package/package.json +77 -0
- package/skillcards/import-sources.json +93 -0
- package/skillcards/seed/acp-bounty-poll.v1.json +38 -0
- package/skillcards/seed/acp-bounty-post.v1.json +55 -0
- package/skillcards/seed/acp-browse.v1.json +41 -0
- package/skillcards/seed/acp-fulfill-and-route.v1.json +56 -0
- package/skillcards/seed/apeclaw-bridge-relay.v1.json +46 -0
- package/skillcards/seed/apeclaw-nft-autobuy.v1.json +60 -0
- package/skillcards/seed/apeclaw-receipt-recorder.v1.json +64 -0
- package/skillcards/seed/humanizer.v1.json +74 -0
- package/skillcards/seed/otherside-navigator.v1.json +116 -0
- package/skillcards/seed/stonkbrokers-launcher.v1.json +280 -0
- package/skillcards/seed/walkie-p2p.v1.json +66 -0
- package/src/cli/index.mjs +8 -0
- package/src/cli.mjs +1929 -0
- package/src/lib/bridge-relay.mjs +294 -0
- package/src/lib/clawbots.mjs +94 -0
- package/src/lib/io.mjs +36 -0
- package/src/lib/market.mjs +233 -0
- package/src/lib/nft-opensea.mjs +159 -0
- package/src/lib/paths.mjs +17 -0
- package/src/lib/pod-init.mjs +40 -0
- package/src/lib/policy.mjs +112 -0
- package/src/lib/rpc.mjs +49 -0
- package/src/lib/telemetry.mjs +92 -0
- package/src/lib/v2-onchain-abi.mjs +294 -0
- package/src/lib/v2-skillcard.mjs +27 -0
- package/src/server/index.mjs +169 -0
- package/src/server/logger.mjs +21 -0
- package/src/server/middleware/auth.mjs +90 -0
- package/src/server/middleware/body-limit.mjs +35 -0
- package/src/server/middleware/cors.mjs +33 -0
- package/src/server/middleware/rate-limit.mjs +44 -0
- package/src/server/routes/chat.mjs +178 -0
- package/src/server/routes/clawbots.mjs +182 -0
- package/src/server/routes/events.mjs +95 -0
- package/src/server/routes/health.mjs +72 -0
- package/src/server/routes/pod.mjs +64 -0
- package/src/server/routes/quotes.mjs +161 -0
- package/src/server/routes/skills.mjs +239 -0
- package/src/server/routes/static.mjs +161 -0
- package/src/server/routes/v2.mjs +48 -0
- package/src/server/sse.mjs +73 -0
- package/src/server/storage/file-backend.mjs +295 -0
- package/src/server/storage/index.mjs +37 -0
- package/src/server/storage/sqlite-backend.mjs +380 -0
- package/src/telemetry-server.mjs +1604 -0
- package/ui/css/dashboard.css +792 -0
- package/ui/css/skills.css +689 -0
- package/ui/docs.html +840 -0
- package/ui/favicon-180.png +0 -0
- package/ui/favicon-192.png +0 -0
- package/ui/favicon-32.png +0 -0
- package/ui/favicon-lobster.png +0 -0
- package/ui/favicon.svg +10 -0
- package/ui/index.html +2957 -0
- package/ui/js/dashboard.js +1766 -0
- package/ui/js/skills.js +1621 -0
- package/ui/pod.html +909 -0
- package/ui/shared/motion.css +286 -0
- package/ui/shared/motion.js +170 -0
- package/ui/shared/sidebar-nav.css +379 -0
- package/ui/shared/sidebar-nav.js +137 -0
- package/ui/skills.html +2879 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE (Server-Sent Events) client management.
|
|
3
|
+
*
|
|
4
|
+
* Listens to the storage EventEmitter for new events and broadcasts
|
|
5
|
+
* to connected SSE clients. Works with any storage backend.
|
|
6
|
+
*
|
|
7
|
+
* Supports Last-Event-ID for reconnection gap-fill.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { storageEvents } from "./storage/index.mjs";
|
|
11
|
+
|
|
12
|
+
const telemetryClients = new Set();
|
|
13
|
+
const chatClients = new Set();
|
|
14
|
+
|
|
15
|
+
let _eventCounter = Date.now();
|
|
16
|
+
|
|
17
|
+
export function nextEventId() {
|
|
18
|
+
return ++_eventCounter;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sendSse(res, data, id) {
|
|
22
|
+
try {
|
|
23
|
+
if (id !== undefined) res.write(`id: ${id}\n`);
|
|
24
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
25
|
+
} catch { /* client disconnected */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function addTelemetryClient(res) {
|
|
29
|
+
telemetryClients.add(res);
|
|
30
|
+
return () => telemetryClients.delete(res);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addChatClient(res, room) {
|
|
34
|
+
const client = { res, room };
|
|
35
|
+
chatClients.add(client);
|
|
36
|
+
return () => chatClients.delete(client);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function broadcastTelemetry(evt) {
|
|
40
|
+
const id = nextEventId();
|
|
41
|
+
for (const c of telemetryClients) sendSse(c, evt, id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeRoom(r) {
|
|
45
|
+
return String(r || "general").toLowerCase().trim()
|
|
46
|
+
.replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "general";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function broadcastChat(msg) {
|
|
50
|
+
const id = nextEventId();
|
|
51
|
+
for (const client of chatClients) {
|
|
52
|
+
try {
|
|
53
|
+
const want = normalizeRoom(client.room || "all");
|
|
54
|
+
if (want !== "all" && want !== normalizeRoom(msg.room)) continue;
|
|
55
|
+
sendSse(client.res, msg, id);
|
|
56
|
+
} catch { chatClients.delete(client); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getTelemetryClientCount() { return telemetryClients.size; }
|
|
61
|
+
export function getChatClientCount() { return chatClients.size; }
|
|
62
|
+
|
|
63
|
+
export function closeAllClients() {
|
|
64
|
+
for (const c of telemetryClients) { try { c.end(); } catch {} }
|
|
65
|
+
telemetryClients.clear();
|
|
66
|
+
for (const c of chatClients) { try { c.res.end(); } catch {} }
|
|
67
|
+
chatClients.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function initSseBroadcast() {
|
|
71
|
+
storageEvents.on("telemetryEvent", broadcastTelemetry);
|
|
72
|
+
storageEvents.on("chatMessage", broadcastChat);
|
|
73
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based storage backend.
|
|
3
|
+
*
|
|
4
|
+
* Implements the storage interface using JSON/JSONL files
|
|
5
|
+
* (the original telemetry-server.mjs approach).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { ensureDir, readJson } from "../../lib/io.mjs";
|
|
12
|
+
import {
|
|
13
|
+
STATE_DIR, EVENTS_PATH, CHAT_PATH, INVITES_PATH, CLAWBOTS_PATH,
|
|
14
|
+
ROOT as PROJECT_ROOT, ALLOWLIST_PATH, POLICY_PATH, OPENSEA_OVERRIDES_PATH,
|
|
15
|
+
QUOTES_PATH, BRIDGE_REQUESTS_PATH,
|
|
16
|
+
} from "../../lib/paths.mjs";
|
|
17
|
+
import { storageEvents } from "./index.mjs";
|
|
18
|
+
|
|
19
|
+
const SKILLCARDS_USER_DIR = path.join(STATE_DIR, "skillcards-user");
|
|
20
|
+
const SKILLCARDS_USER_INDEX_PATH = path.join(SKILLCARDS_USER_DIR, "index.json");
|
|
21
|
+
const SKILLCARDS_SEED_DIR = path.join(PROJECT_ROOT, "skillcards", "seed");
|
|
22
|
+
const SKILLCARDS_IMPORTED_INDEX_PATH = path.join(PROJECT_ROOT, "skillcards", "imported", "index.json");
|
|
23
|
+
|
|
24
|
+
const MERGED_INDEX_CACHE_TTL_MS = 60_000;
|
|
25
|
+
let mergedSkillIndexCache = { data: null, expiresAt: 0 };
|
|
26
|
+
|
|
27
|
+
function sha256(input) {
|
|
28
|
+
return createHash("sha256").update(String(input)).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function atomicWriteJson(filePath, data) {
|
|
32
|
+
ensureDir(path.dirname(filePath));
|
|
33
|
+
const tmp = `${filePath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
34
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
35
|
+
fs.renameSync(tmp, filePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildMergedSkillIndex() {
|
|
39
|
+
const merged = [];
|
|
40
|
+
let seedTokenId = 1;
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(SKILLCARDS_SEED_DIR)) {
|
|
43
|
+
const seedFiles = fs.readdirSync(SKILLCARDS_SEED_DIR).filter((f) => f.endsWith(".json")).sort();
|
|
44
|
+
for (const fileName of seedFiles) {
|
|
45
|
+
try {
|
|
46
|
+
const filePath = path.join(SKILLCARDS_SEED_DIR, fileName);
|
|
47
|
+
const skill = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
48
|
+
if (skill && typeof skill === "object" && skill.name && skill.slug) {
|
|
49
|
+
merged.push({
|
|
50
|
+
name: String(skill.name || "").trim(),
|
|
51
|
+
slug: String(skill.slug || "").trim(),
|
|
52
|
+
description: String(skill.description || "").trim(),
|
|
53
|
+
source: "seed",
|
|
54
|
+
vettedOk: true,
|
|
55
|
+
importOk: true,
|
|
56
|
+
riskTier: Number(skill?.constraints?.riskTier ?? skill?.riskTier ?? 2),
|
|
57
|
+
sourceUrl: String(skill?.provenance?.sourceUrl || "").trim() || null,
|
|
58
|
+
provenance: skill.provenance || { publisher: "apeclaw", signed: false },
|
|
59
|
+
onchainTokenId: String(seedTokenId),
|
|
60
|
+
});
|
|
61
|
+
seedTokenId++;
|
|
62
|
+
}
|
|
63
|
+
} catch { /* skip malformed */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch { /* skip */ }
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(SKILLCARDS_IMPORTED_INDEX_PATH)) {
|
|
70
|
+
const index = JSON.parse(fs.readFileSync(SKILLCARDS_IMPORTED_INDEX_PATH, "utf8"));
|
|
71
|
+
const imported = Array.isArray(index?.imported) ? index.imported : [];
|
|
72
|
+
for (const item of imported) {
|
|
73
|
+
if (item && typeof item === "object" && item.name && item.slug) {
|
|
74
|
+
merged.push({
|
|
75
|
+
name: String(item.name || "").trim(),
|
|
76
|
+
slug: String(item.slug || "").trim(),
|
|
77
|
+
description: String(item.description || "").trim(),
|
|
78
|
+
fileName: String(item.fileName || "").trim() || null,
|
|
79
|
+
source: "imported",
|
|
80
|
+
vettedOk: Boolean(item.vettedOk),
|
|
81
|
+
importOk: Boolean(item.importOk),
|
|
82
|
+
riskTier: Number(item.riskTier ?? 2),
|
|
83
|
+
sourceUrl: String(item.sourceUrl || "").trim() || null,
|
|
84
|
+
provenance: item.provenance || { publisher: "imported", signed: false },
|
|
85
|
+
onchainTokenId: item.onchainTokenId || null,
|
|
86
|
+
onchainMintTx: item.onchainMintTx || null,
|
|
87
|
+
onchainPublishTx: item.onchainPublishTx || null,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch { /* skip */ }
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (fs.existsSync(SKILLCARDS_USER_INDEX_PATH)) {
|
|
96
|
+
const index = JSON.parse(fs.readFileSync(SKILLCARDS_USER_INDEX_PATH, "utf8"));
|
|
97
|
+
const userSkills = Array.isArray(index?.skills) ? index.skills : [];
|
|
98
|
+
for (const item of userSkills) {
|
|
99
|
+
if (item && typeof item === "object" && item.name && item.slug) {
|
|
100
|
+
merged.push({
|
|
101
|
+
name: String(item.name || "").trim(),
|
|
102
|
+
slug: String(item.slug || "").trim(),
|
|
103
|
+
description: String(item.description || "").trim(),
|
|
104
|
+
source: "user",
|
|
105
|
+
vettedOk: false,
|
|
106
|
+
importOk: true,
|
|
107
|
+
riskTier: Number(item.riskTier ?? 2),
|
|
108
|
+
sourceUrl: String(item.sourceUrl || "").trim() || null,
|
|
109
|
+
provenance: { publisher: "user", signed: false, addedBy: item.addedBy, addedByAgentId: item.addedByAgentId },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch { /* skip */ }
|
|
115
|
+
|
|
116
|
+
return merged;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createFileBackend(opts = {}) {
|
|
120
|
+
ensureDir(path.dirname(EVENTS_PATH));
|
|
121
|
+
if (!fs.existsSync(EVENTS_PATH)) fs.writeFileSync(EVENTS_PATH, "");
|
|
122
|
+
if (!fs.existsSync(CHAT_PATH)) fs.writeFileSync(CHAT_PATH, "");
|
|
123
|
+
ensureDir(path.dirname(INVITES_PATH));
|
|
124
|
+
ensureDir(SKILLCARDS_USER_DIR);
|
|
125
|
+
if (!fs.existsSync(SKILLCARDS_USER_INDEX_PATH)) {
|
|
126
|
+
fs.writeFileSync(SKILLCARDS_USER_INDEX_PATH, JSON.stringify({ skills: [] }, null, 2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
// ── Events ──
|
|
131
|
+
appendEvent(evt) {
|
|
132
|
+
fs.appendFileSync(EVENTS_PATH, JSON.stringify(evt) + "\n");
|
|
133
|
+
storageEvents.emit("telemetryEvent", evt);
|
|
134
|
+
},
|
|
135
|
+
getEventBacklog(limit = 300) {
|
|
136
|
+
const raw = fs.readFileSync(EVENTS_PATH, "utf8");
|
|
137
|
+
const lines = raw.trim() ? raw.trim().split("\n") : [];
|
|
138
|
+
return lines.slice(-limit).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// ── Chat ──
|
|
142
|
+
appendChat(msg) {
|
|
143
|
+
fs.appendFileSync(CHAT_PATH, JSON.stringify(msg) + "\n");
|
|
144
|
+
storageEvents.emit("chatMessage", msg);
|
|
145
|
+
},
|
|
146
|
+
readChatEntries() {
|
|
147
|
+
if (!fs.existsSync(CHAT_PATH)) return [];
|
|
148
|
+
const raw = fs.readFileSync(CHAT_PATH, "utf8").trim();
|
|
149
|
+
if (!raw) return [];
|
|
150
|
+
return raw.split("\n").map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// ── Invites ──
|
|
154
|
+
readInvites() {
|
|
155
|
+
try {
|
|
156
|
+
if (!fs.existsSync(INVITES_PATH)) return { invites: {} };
|
|
157
|
+
const parsed = JSON.parse(fs.readFileSync(INVITES_PATH, "utf8"));
|
|
158
|
+
if (!parsed || typeof parsed !== "object" || !parsed.invites) return { invites: {} };
|
|
159
|
+
return parsed;
|
|
160
|
+
} catch { return { invites: {} }; }
|
|
161
|
+
},
|
|
162
|
+
writeInvites(data) {
|
|
163
|
+
try {
|
|
164
|
+
ensureDir(path.dirname(INVITES_PATH));
|
|
165
|
+
fs.writeFileSync(INVITES_PATH, JSON.stringify(data, null, 2));
|
|
166
|
+
} catch { /* best-effort */ }
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// ── Skills ──
|
|
170
|
+
getMergedSkillIndex() {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
if (mergedSkillIndexCache.data && mergedSkillIndexCache.expiresAt > now) return mergedSkillIndexCache.data;
|
|
173
|
+
const index = buildMergedSkillIndex();
|
|
174
|
+
mergedSkillIndexCache = { data: index, expiresAt: now + MERGED_INDEX_CACHE_TTL_MS };
|
|
175
|
+
return index;
|
|
176
|
+
},
|
|
177
|
+
getUserSkillsIndex() {
|
|
178
|
+
return JSON.parse(fs.readFileSync(SKILLCARDS_USER_INDEX_PATH, "utf8"));
|
|
179
|
+
},
|
|
180
|
+
writeUserSkillsIndex(data) {
|
|
181
|
+
atomicWriteJson(SKILLCARDS_USER_INDEX_PATH, data);
|
|
182
|
+
},
|
|
183
|
+
writeUserSkillFile(fileName, data) {
|
|
184
|
+
fs.writeFileSync(path.join(SKILLCARDS_USER_DIR, fileName), JSON.stringify(data, null, 2));
|
|
185
|
+
},
|
|
186
|
+
deleteUserSkillFile(fileName) {
|
|
187
|
+
const fp = path.join(SKILLCARDS_USER_DIR, fileName);
|
|
188
|
+
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
189
|
+
},
|
|
190
|
+
resolveSkillFilePath(source, fileName) {
|
|
191
|
+
const dirs = {
|
|
192
|
+
seed: SKILLCARDS_SEED_DIR,
|
|
193
|
+
imported: path.join(PROJECT_ROOT, "skillcards", "imported"),
|
|
194
|
+
user: SKILLCARDS_USER_DIR,
|
|
195
|
+
};
|
|
196
|
+
const dir = dirs[source];
|
|
197
|
+
if (!dir || !fileName) return null;
|
|
198
|
+
const fp = path.join(dir, fileName);
|
|
199
|
+
if (fs.existsSync(fp)) return fp;
|
|
200
|
+
return null;
|
|
201
|
+
},
|
|
202
|
+
get SKILLCARDS_USER_DIR() { return SKILLCARDS_USER_DIR; },
|
|
203
|
+
get SKILLCARDS_SEED_DIR() { return SKILLCARDS_SEED_DIR; },
|
|
204
|
+
|
|
205
|
+
// ── V2 deployment records ──
|
|
206
|
+
resolveV2DeploymentRecord() {
|
|
207
|
+
try {
|
|
208
|
+
const dir = path.join(STATE_DIR, "v2-deployments");
|
|
209
|
+
if (!fs.existsSync(dir)) return null;
|
|
210
|
+
const entries = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
211
|
+
if (!entries.length) return null;
|
|
212
|
+
let pick = entries[0], best = -1;
|
|
213
|
+
for (const f of entries) {
|
|
214
|
+
try {
|
|
215
|
+
const mt = Number(fs.statSync(path.join(dir, f)).mtimeMs || 0);
|
|
216
|
+
if (mt > best) { best = mt; pick = f; }
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, pick), "utf8"));
|
|
220
|
+
return raw && typeof raw === "object" ? raw : null;
|
|
221
|
+
} catch { return null; }
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// ── Pod helpers ──
|
|
225
|
+
findPodWorkspaceDir() {
|
|
226
|
+
const envDir = process.env.APE_CLAW_POD_DIR;
|
|
227
|
+
if (envDir) {
|
|
228
|
+
const p = path.resolve(envDir);
|
|
229
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p;
|
|
230
|
+
}
|
|
231
|
+
for (const name of ["pod-workspace", "pod"]) {
|
|
232
|
+
const p = path.join(PROJECT_ROOT, name);
|
|
233
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
// ── Quotes ──
|
|
239
|
+
getQuote(quoteId) {
|
|
240
|
+
const all = readJson(QUOTES_PATH, {});
|
|
241
|
+
return all?.[quoteId] || null;
|
|
242
|
+
},
|
|
243
|
+
saveQuote(quoteId, data) {
|
|
244
|
+
const all = readJson(QUOTES_PATH, {}) || {};
|
|
245
|
+
all[quoteId] = data;
|
|
246
|
+
atomicWriteJson(QUOTES_PATH, all);
|
|
247
|
+
},
|
|
248
|
+
updateQuote(quoteId, patch) {
|
|
249
|
+
const all = readJson(QUOTES_PATH, {}) || {};
|
|
250
|
+
if (!all[quoteId]) return null;
|
|
251
|
+
all[quoteId] = { ...all[quoteId], ...patch };
|
|
252
|
+
atomicWriteJson(QUOTES_PATH, all);
|
|
253
|
+
return all[quoteId];
|
|
254
|
+
},
|
|
255
|
+
getQuotesSpendToday() {
|
|
256
|
+
const all = readJson(QUOTES_PATH, {}) || {};
|
|
257
|
+
const dayKey = new Date().toISOString().slice(0, 10);
|
|
258
|
+
return Object.values(all).reduce((sum, q) => {
|
|
259
|
+
if (!q || !q.executedAt || !q.executed) return sum;
|
|
260
|
+
if (new Date(q.executedAt).toISOString().slice(0, 10) !== dayKey) return sum;
|
|
261
|
+
return sum + (Number(q.priceApe) || 0);
|
|
262
|
+
}, 0);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// ── Bridge requests ──
|
|
266
|
+
getBridgeRequest(requestId) {
|
|
267
|
+
const all = readJson(BRIDGE_REQUESTS_PATH, {});
|
|
268
|
+
return all?.[requestId] || null;
|
|
269
|
+
},
|
|
270
|
+
saveBridgeRequest(requestId, data) {
|
|
271
|
+
const all = readJson(BRIDGE_REQUESTS_PATH, {}) || {};
|
|
272
|
+
all[requestId] = data;
|
|
273
|
+
atomicWriteJson(BRIDGE_REQUESTS_PATH, all);
|
|
274
|
+
},
|
|
275
|
+
updateBridgeRequest(requestId, patch) {
|
|
276
|
+
const all = readJson(BRIDGE_REQUESTS_PATH, {}) || {};
|
|
277
|
+
if (!all[requestId]) return null;
|
|
278
|
+
all[requestId] = { ...all[requestId], ...patch };
|
|
279
|
+
atomicWriteJson(BRIDGE_REQUESTS_PATH, all);
|
|
280
|
+
return all[requestId];
|
|
281
|
+
},
|
|
282
|
+
getBridgeSpendToday() {
|
|
283
|
+
const all = readJson(BRIDGE_REQUESTS_PATH, {}) || {};
|
|
284
|
+
const dayKey = new Date().toISOString().slice(0, 10);
|
|
285
|
+
return Object.values(all).reduce((sum, r) => {
|
|
286
|
+
if (!r || !r.submittedAt) return sum;
|
|
287
|
+
if (r.status === "quoted") return sum;
|
|
288
|
+
if (new Date(r.submittedAt).toISOString().slice(0, 10) !== dayKey) return sum;
|
|
289
|
+
return sum + (Number(r.amount) || 0);
|
|
290
|
+
}, 0);
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
close() { /* no-op for file backend */ },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage abstraction layer.
|
|
3
|
+
*
|
|
4
|
+
* Exports a singleton that wraps the active backend (file or SQLite).
|
|
5
|
+
* All route handlers go through this interface so the backend can be swapped.
|
|
6
|
+
*
|
|
7
|
+
* Backend selection: set APE_CLAW_STORAGE=sqlite to use SQLite.
|
|
8
|
+
* Default: file-based (original behavior).
|
|
9
|
+
*
|
|
10
|
+
* Also exports an EventEmitter that fires on mutations:
|
|
11
|
+
* 'telemetryEvent' (evt) -- new telemetry event stored
|
|
12
|
+
* 'chatMessage' (msg) -- new chat message/reaction stored
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { EventEmitter } from "node:events";
|
|
16
|
+
import { createFileBackend } from "./file-backend.mjs";
|
|
17
|
+
import { createSqliteBackend } from "./sqlite-backend.mjs";
|
|
18
|
+
|
|
19
|
+
export const storageEvents = new EventEmitter();
|
|
20
|
+
storageEvents.setMaxListeners(1000);
|
|
21
|
+
|
|
22
|
+
let _backend = null;
|
|
23
|
+
|
|
24
|
+
export function initStorage(opts = {}) {
|
|
25
|
+
const storageType = String(opts.type || process.env.APE_CLAW_STORAGE || "file").toLowerCase();
|
|
26
|
+
if (storageType === "sqlite") {
|
|
27
|
+
_backend = createSqliteBackend(opts);
|
|
28
|
+
} else {
|
|
29
|
+
_backend = createFileBackend(opts);
|
|
30
|
+
}
|
|
31
|
+
return _backend;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getStorage() {
|
|
35
|
+
if (!_backend) throw new Error("Storage not initialized. Call initStorage() first.");
|
|
36
|
+
return _backend;
|
|
37
|
+
}
|