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,380 @@
1
+ /**
2
+ * SQLite storage backend (WAL mode, better-sqlite3).
3
+ *
4
+ * Drop-in replacement for file-backend.mjs behind the storage abstraction.
5
+ * Enabled by setting APE_CLAW_STORAGE=sqlite (default DB path: <STATE_DIR>/apeclaw.db).
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import Database from "better-sqlite3";
11
+ import { ensureDir } from "../../lib/io.mjs";
12
+ import { STATE_DIR, ROOT as PROJECT_ROOT } from "../../lib/paths.mjs";
13
+ import { storageEvents } from "./index.mjs";
14
+
15
+ const SKILLCARDS_USER_DIR = path.join(STATE_DIR, "skillcards-user");
16
+ const SKILLCARDS_USER_INDEX_PATH = path.join(SKILLCARDS_USER_DIR, "index.json");
17
+ const SKILLCARDS_SEED_DIR = path.join(PROJECT_ROOT, "skillcards", "seed");
18
+ const SKILLCARDS_IMPORTED_INDEX_PATH = path.join(PROJECT_ROOT, "skillcards", "imported", "index.json");
19
+ const MERGED_INDEX_CACHE_TTL_MS = 60_000;
20
+ let mergedSkillIndexCache = { data: null, expiresAt: 0 };
21
+
22
+ function initDb(dbPath) {
23
+ ensureDir(path.dirname(dbPath));
24
+ const db = new Database(dbPath);
25
+ db.pragma("journal_mode = WAL");
26
+ db.pragma("busy_timeout = 5000");
27
+
28
+ db.exec(`
29
+ CREATE TABLE IF NOT EXISTS events (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ ts TEXT NOT NULL,
32
+ eventType TEXT NOT NULL,
33
+ agentId TEXT,
34
+ sessionId TEXT,
35
+ traceId TEXT,
36
+ command TEXT,
37
+ dryRun INTEGER DEFAULT 0,
38
+ chainId INTEGER DEFAULT 33139,
39
+ payload TEXT DEFAULT '{}',
40
+ result TEXT DEFAULT '{}',
41
+ ok INTEGER DEFAULT 1,
42
+ error TEXT
43
+ );
44
+ CREATE INDEX IF NOT EXISTS idx_events_agentId_ts ON events(agentId, ts);
45
+ CREATE INDEX IF NOT EXISTS idx_events_traceId ON events(traceId);
46
+ CREATE INDEX IF NOT EXISTS idx_events_eventType ON events(eventType);
47
+
48
+ CREATE TABLE IF NOT EXISTS quotes (
49
+ quoteId TEXT PRIMARY KEY,
50
+ collection TEXT,
51
+ tokenId TEXT,
52
+ priceApe REAL,
53
+ maxPrice REAL,
54
+ currency TEXT,
55
+ expiresAt TEXT,
56
+ status TEXT DEFAULT 'quoted',
57
+ simulated INTEGER DEFAULT 0,
58
+ executed INTEGER DEFAULT 0,
59
+ executedAt TEXT,
60
+ agentId TEXT,
61
+ createdAt TEXT NOT NULL,
62
+ payload TEXT DEFAULT '{}'
63
+ );
64
+ CREATE INDEX IF NOT EXISTS idx_quotes_agentId ON quotes(agentId, createdAt);
65
+ CREATE INDEX IF NOT EXISTS idx_quotes_executedAt ON quotes(executedAt);
66
+
67
+ CREATE TABLE IF NOT EXISTS bridge_requests (
68
+ requestId TEXT PRIMARY KEY,
69
+ fromChain TEXT,
70
+ toChain TEXT,
71
+ token TEXT,
72
+ amount REAL,
73
+ status TEXT DEFAULT 'quoted',
74
+ feeBps INTEGER,
75
+ expiresAt TEXT,
76
+ submittedAt TEXT,
77
+ agentId TEXT,
78
+ createdAt TEXT NOT NULL,
79
+ payload TEXT DEFAULT '{}'
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_bridge_requests_submittedAt ON bridge_requests(submittedAt);
83
+
84
+ CREATE TABLE IF NOT EXISTS chat (
85
+ id TEXT PRIMARY KEY,
86
+ type TEXT DEFAULT 'message',
87
+ room TEXT DEFAULT 'general',
88
+ ts TEXT NOT NULL,
89
+ agentId TEXT,
90
+ agentName TEXT,
91
+ identityProvider TEXT,
92
+ identityMeta TEXT DEFAULT '{}',
93
+ text TEXT,
94
+ replyTo TEXT,
95
+ messageId TEXT,
96
+ emoji TEXT,
97
+ payload TEXT DEFAULT '{}'
98
+ );
99
+ CREATE INDEX IF NOT EXISTS idx_chat_room_ts ON chat(room, ts);
100
+
101
+ CREATE TABLE IF NOT EXISTS clawbots (
102
+ agentId TEXT PRIMARY KEY,
103
+ name TEXT,
104
+ tokenHash TEXT,
105
+ createdAt TEXT,
106
+ enabled INTEGER DEFAULT 1
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS invites (
110
+ tokenHash TEXT PRIMARY KEY,
111
+ createdAt TEXT,
112
+ expiresAt TEXT,
113
+ usesRemaining INTEGER DEFAULT 1,
114
+ lastUsedAt TEXT
115
+ );
116
+ `);
117
+
118
+ return db;
119
+ }
120
+
121
+ function buildMergedSkillIndex() {
122
+ const merged = [];
123
+ let seedTokenId = 1;
124
+ try {
125
+ if (fs.existsSync(SKILLCARDS_SEED_DIR)) {
126
+ for (const f of fs.readdirSync(SKILLCARDS_SEED_DIR).filter((x) => x.endsWith(".json")).sort()) {
127
+ try {
128
+ const skill = JSON.parse(fs.readFileSync(path.join(SKILLCARDS_SEED_DIR, f), "utf8"));
129
+ if (skill?.name && skill?.slug) {
130
+ merged.push({
131
+ name: String(skill.name).trim(), slug: String(skill.slug).trim(),
132
+ description: String(skill.description || "").trim(), source: "seed",
133
+ vettedOk: true, importOk: true,
134
+ riskTier: Number(skill?.constraints?.riskTier ?? skill?.riskTier ?? 2),
135
+ sourceUrl: String(skill?.provenance?.sourceUrl || "").trim() || null,
136
+ provenance: skill.provenance || { publisher: "apeclaw", signed: false },
137
+ onchainTokenId: String(seedTokenId++),
138
+ });
139
+ }
140
+ } catch {}
141
+ }
142
+ }
143
+ } catch {}
144
+ try {
145
+ if (fs.existsSync(SKILLCARDS_IMPORTED_INDEX_PATH)) {
146
+ const idx = JSON.parse(fs.readFileSync(SKILLCARDS_IMPORTED_INDEX_PATH, "utf8"));
147
+ for (const item of (Array.isArray(idx?.imported) ? idx.imported : [])) {
148
+ if (item?.name && item?.slug) {
149
+ merged.push({
150
+ name: String(item.name).trim(), slug: String(item.slug).trim(),
151
+ description: String(item.description || "").trim(),
152
+ fileName: String(item.fileName || "").trim() || null,
153
+ source: "imported", vettedOk: Boolean(item.vettedOk), importOk: Boolean(item.importOk),
154
+ riskTier: Number(item.riskTier ?? 2),
155
+ sourceUrl: String(item.sourceUrl || "").trim() || null,
156
+ provenance: item.provenance || { publisher: "imported", signed: false },
157
+ onchainTokenId: item.onchainTokenId || null,
158
+ onchainMintTx: item.onchainMintTx || null,
159
+ onchainPublishTx: item.onchainPublishTx || null,
160
+ });
161
+ }
162
+ }
163
+ }
164
+ } catch {}
165
+ try {
166
+ if (fs.existsSync(SKILLCARDS_USER_INDEX_PATH)) {
167
+ const idx = JSON.parse(fs.readFileSync(SKILLCARDS_USER_INDEX_PATH, "utf8"));
168
+ for (const item of (Array.isArray(idx?.skills) ? idx.skills : [])) {
169
+ if (item?.name && item?.slug) {
170
+ merged.push({
171
+ name: String(item.name).trim(), slug: String(item.slug).trim(),
172
+ description: String(item.description || "").trim(),
173
+ source: "user", vettedOk: false, importOk: true,
174
+ riskTier: Number(item.riskTier ?? 2),
175
+ sourceUrl: String(item.sourceUrl || "").trim() || null,
176
+ provenance: { publisher: "user", signed: false, addedBy: item.addedBy, addedByAgentId: item.addedByAgentId },
177
+ });
178
+ }
179
+ }
180
+ }
181
+ } catch {}
182
+ return merged;
183
+ }
184
+
185
+ export function createSqliteBackend(opts = {}) {
186
+ const dbPath = opts.dbPath || path.join(STATE_DIR, "apeclaw.db");
187
+ const db = initDb(dbPath);
188
+
189
+ ensureDir(SKILLCARDS_USER_DIR);
190
+ if (!fs.existsSync(SKILLCARDS_USER_INDEX_PATH)) {
191
+ fs.writeFileSync(SKILLCARDS_USER_INDEX_PATH, JSON.stringify({ skills: [] }, null, 2));
192
+ }
193
+
194
+ const stmts = {
195
+ insertEvent: db.prepare(`INSERT INTO events (ts, eventType, agentId, sessionId, traceId, command, dryRun, chainId, payload, result, ok, error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
196
+ getEventBacklog: db.prepare(`SELECT * FROM events ORDER BY id DESC LIMIT ?`),
197
+ insertChat: db.prepare(`INSERT INTO chat (id, type, room, ts, agentId, agentName, identityProvider, identityMeta, text, replyTo, messageId, emoji) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
198
+ getChat: db.prepare(`SELECT * FROM chat ORDER BY ts ASC`),
199
+ getInvite: db.prepare(`SELECT * FROM invites WHERE tokenHash = ?`),
200
+ upsertInvite: db.prepare(`INSERT OR REPLACE INTO invites (tokenHash, createdAt, expiresAt, usesRemaining, lastUsedAt) VALUES (?, ?, ?, ?, ?)`),
201
+ getAllInvites: db.prepare(`SELECT * FROM invites`),
202
+ getQuote: db.prepare(`SELECT * FROM quotes WHERE quoteId = ?`),
203
+ upsertQuote: db.prepare(`INSERT OR REPLACE INTO quotes (quoteId, collection, tokenId, priceApe, maxPrice, currency, expiresAt, status, simulated, executed, executedAt, agentId, createdAt, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
204
+ getBridgeReq: db.prepare(`SELECT * FROM bridge_requests WHERE requestId = ?`),
205
+ upsertBridgeReq: db.prepare(`INSERT OR REPLACE INTO bridge_requests (requestId, fromChain, toChain, token, amount, status, feeBps, expiresAt, submittedAt, agentId, createdAt, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
206
+ };
207
+
208
+ return {
209
+ appendEvent(evt) {
210
+ stmts.insertEvent.run(
211
+ evt.ts, evt.eventType, evt.agentId, evt.sessionId, evt.traceId,
212
+ evt.command, evt.dryRun ? 1 : 0, evt.chainId,
213
+ JSON.stringify(evt.payload || {}), JSON.stringify(evt.result || {}),
214
+ evt.ok ? 1 : 0, evt.error || null,
215
+ );
216
+ storageEvents.emit("telemetryEvent", evt);
217
+ },
218
+ getEventBacklog(limit = 300) {
219
+ const rows = stmts.getEventBacklog.all(limit).reverse();
220
+ return rows.map((r) => ({
221
+ ...r, dryRun: Boolean(r.dryRun), ok: Boolean(r.ok),
222
+ payload: JSON.parse(r.payload || "{}"), result: JSON.parse(r.result || "{}"),
223
+ }));
224
+ },
225
+
226
+ appendChat(msg) {
227
+ stmts.insertChat.run(
228
+ msg.id, msg.type || "message", msg.room || "general", msg.ts,
229
+ msg.agentId, msg.agentName, msg.identityProvider,
230
+ JSON.stringify(msg.identityMeta || {}), msg.text, msg.replyTo || null,
231
+ msg.messageId || null, msg.emoji || null,
232
+ );
233
+ storageEvents.emit("chatMessage", msg);
234
+ },
235
+ readChatEntries() {
236
+ return stmts.getChat.all().map((r) => ({
237
+ ...r, identityMeta: JSON.parse(r.identityMeta || "{}"),
238
+ }));
239
+ },
240
+
241
+ readInvites() {
242
+ const rows = stmts.getAllInvites.all();
243
+ const invites = {};
244
+ for (const r of rows) invites[r.tokenHash] = r;
245
+ return { invites };
246
+ },
247
+ writeInvites(data) {
248
+ const invites = data?.invites || {};
249
+ for (const [hash, row] of Object.entries(invites)) {
250
+ stmts.upsertInvite.run(hash, row.createdAt, row.expiresAt, row.usesRemaining, row.lastUsedAt || null);
251
+ }
252
+ },
253
+
254
+ getMergedSkillIndex() {
255
+ const now = Date.now();
256
+ if (mergedSkillIndexCache.data && mergedSkillIndexCache.expiresAt > now) return mergedSkillIndexCache.data;
257
+ const index = buildMergedSkillIndex();
258
+ mergedSkillIndexCache = { data: index, expiresAt: now + MERGED_INDEX_CACHE_TTL_MS };
259
+ return index;
260
+ },
261
+ getUserSkillsIndex() { return JSON.parse(fs.readFileSync(SKILLCARDS_USER_INDEX_PATH, "utf8")); },
262
+ writeUserSkillsIndex(data) {
263
+ const tmp = `${SKILLCARDS_USER_INDEX_PATH}.tmp`;
264
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
265
+ fs.renameSync(tmp, SKILLCARDS_USER_INDEX_PATH);
266
+ },
267
+ writeUserSkillFile(fileName, data) {
268
+ fs.writeFileSync(path.join(SKILLCARDS_USER_DIR, fileName), JSON.stringify(data, null, 2));
269
+ },
270
+ deleteUserSkillFile(fileName) {
271
+ const fp = path.join(SKILLCARDS_USER_DIR, fileName);
272
+ if (fs.existsSync(fp)) fs.unlinkSync(fp);
273
+ },
274
+ resolveSkillFilePath(source, fileName) {
275
+ const dirs = { seed: SKILLCARDS_SEED_DIR, imported: path.join(PROJECT_ROOT, "skillcards", "imported"), user: SKILLCARDS_USER_DIR };
276
+ const dir = dirs[source];
277
+ if (!dir || !fileName) return null;
278
+ const fp = path.join(dir, fileName);
279
+ return fs.existsSync(fp) ? fp : null;
280
+ },
281
+ get SKILLCARDS_USER_DIR() { return SKILLCARDS_USER_DIR; },
282
+ get SKILLCARDS_SEED_DIR() { return SKILLCARDS_SEED_DIR; },
283
+
284
+ resolveV2DeploymentRecord() {
285
+ try {
286
+ const dir = path.join(STATE_DIR, "v2-deployments");
287
+ if (!fs.existsSync(dir)) return null;
288
+ const entries = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
289
+ if (!entries.length) return null;
290
+ let pick = entries[0], best = -1;
291
+ for (const f of entries) {
292
+ try { const mt = Number(fs.statSync(path.join(dir, f)).mtimeMs || 0); if (mt > best) { best = mt; pick = f; } } catch {}
293
+ }
294
+ return JSON.parse(fs.readFileSync(path.join(dir, pick), "utf8"));
295
+ } catch { return null; }
296
+ },
297
+
298
+ findPodWorkspaceDir() {
299
+ const envDir = process.env.APE_CLAW_POD_DIR;
300
+ if (envDir) { const p = path.resolve(envDir); if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p; }
301
+ for (const name of ["pod-workspace", "pod"]) {
302
+ const p = path.join(PROJECT_ROOT, name);
303
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return p;
304
+ }
305
+ return null;
306
+ },
307
+
308
+ getQuote(quoteId) {
309
+ const row = stmts.getQuote.get(quoteId);
310
+ return row ? { ...row, payload: JSON.parse(row.payload || "{}"), simulated: Boolean(row.simulated), executed: Boolean(row.executed) } : null;
311
+ },
312
+ saveQuote(quoteId, data) {
313
+ stmts.upsertQuote.run(
314
+ quoteId, data.collection, data.tokenId, data.priceApe, data.maxPrice,
315
+ data.currency, data.expiresAt, data.status || "quoted",
316
+ data.simulated ? 1 : 0, data.executed ? 1 : 0, data.executedAt || null,
317
+ data.agentId, data.createdAt, JSON.stringify(data.payload || data),
318
+ );
319
+ },
320
+ updateQuote(quoteId, patch) {
321
+ const existing = stmts.getQuote.get(quoteId);
322
+ if (!existing) return null;
323
+ const existingPayload = JSON.parse(existing.payload || "{}");
324
+ const mergedPayload = patch.payload ? { ...existingPayload, ...patch.payload } : existingPayload;
325
+ const merged = {
326
+ ...existing, ...patch,
327
+ payload: mergedPayload,
328
+ simulated: Boolean(patch.simulated ?? existing.simulated),
329
+ executed: Boolean(patch.executed ?? existing.executed),
330
+ };
331
+ const payloadStr = JSON.stringify(mergedPayload);
332
+ stmts.upsertQuote.run(
333
+ quoteId, merged.collection, merged.tokenId, merged.priceApe, merged.maxPrice,
334
+ merged.currency, merged.expiresAt, merged.status,
335
+ merged.simulated ? 1 : 0, merged.executed ? 1 : 0, merged.executedAt || null,
336
+ merged.agentId, merged.createdAt, payloadStr,
337
+ );
338
+ return merged;
339
+ },
340
+ getQuotesSpendToday() {
341
+ const dayKey = new Date().toISOString().slice(0, 10);
342
+ const row = db.prepare(`SELECT COALESCE(SUM(priceApe), 0) as total FROM quotes WHERE executed = 1 AND executedAt LIKE ? || '%'`).get(dayKey);
343
+ return row?.total || 0;
344
+ },
345
+
346
+ getBridgeRequest(requestId) {
347
+ const row = stmts.getBridgeReq.get(requestId);
348
+ return row ? { ...row, payload: JSON.parse(row.payload || "{}") } : null;
349
+ },
350
+ saveBridgeRequest(requestId, data) {
351
+ stmts.upsertBridgeReq.run(
352
+ requestId, data.fromChain || data.from, data.toChain || data.to, data.token,
353
+ data.amount, data.status || "quoted", data.feeBps, data.expiresAt,
354
+ data.submittedAt || null, data.agentId, data.createdAt,
355
+ JSON.stringify(data.payload || data),
356
+ );
357
+ },
358
+ updateBridgeRequest(requestId, patch) {
359
+ const existing = stmts.getBridgeReq.get(requestId);
360
+ if (!existing) return null;
361
+ const existingPayload = JSON.parse(existing.payload || "{}");
362
+ const mergedPayload = patch.payload ? { ...existingPayload, ...patch.payload } : existingPayload;
363
+ const merged = { ...existing, ...patch, payload: mergedPayload };
364
+ stmts.upsertBridgeReq.run(
365
+ requestId, merged.fromChain, merged.toChain, merged.token,
366
+ merged.amount, merged.status, merged.feeBps, merged.expiresAt,
367
+ merged.submittedAt || null, merged.agentId, merged.createdAt,
368
+ JSON.stringify(mergedPayload),
369
+ );
370
+ return merged;
371
+ },
372
+ getBridgeSpendToday() {
373
+ const dayKey = new Date().toISOString().slice(0, 10);
374
+ const row = db.prepare(`SELECT COALESCE(SUM(amount), 0) as total FROM bridge_requests WHERE status != 'quoted' AND submittedAt LIKE ? || '%'`).get(dayKey);
375
+ return row?.total || 0;
376
+ },
377
+
378
+ close() { db.close(); },
379
+ };
380
+ }