agentbnb 2.2.0 → 3.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.
@@ -0,0 +1,372 @@
1
+ import {
2
+ interpolateObject,
3
+ requestCapability,
4
+ scorePeers,
5
+ searchCards
6
+ } from "./chunk-QVIGMCHA.js";
7
+
8
+ // src/conductor/task-decomposer.ts
9
+ import { randomUUID } from "crypto";
10
+ var TEMPLATES = {
11
+ "video-production": {
12
+ keywords: ["video", "demo", "clip", "animation"],
13
+ steps: [
14
+ {
15
+ description: "Generate script from task description",
16
+ required_capability: "text_gen",
17
+ estimated_credits: 2,
18
+ depends_on_indices: []
19
+ },
20
+ {
21
+ description: "Generate voiceover from script",
22
+ required_capability: "tts",
23
+ estimated_credits: 3,
24
+ depends_on_indices: [0]
25
+ },
26
+ {
27
+ description: "Generate video visuals from script",
28
+ required_capability: "video_gen",
29
+ estimated_credits: 5,
30
+ depends_on_indices: [0]
31
+ },
32
+ {
33
+ description: "Composite voiceover and video into final output",
34
+ required_capability: "video_edit",
35
+ estimated_credits: 3,
36
+ depends_on_indices: [1, 2]
37
+ }
38
+ ]
39
+ },
40
+ "deep-analysis": {
41
+ keywords: ["analyze", "analysis", "research", "report", "evaluate"],
42
+ steps: [
43
+ {
44
+ description: "Research and gather relevant data",
45
+ required_capability: "web_search",
46
+ estimated_credits: 2,
47
+ depends_on_indices: []
48
+ },
49
+ {
50
+ description: "Analyze gathered data",
51
+ required_capability: "text_gen",
52
+ estimated_credits: 3,
53
+ depends_on_indices: [0]
54
+ },
55
+ {
56
+ description: "Summarize analysis findings",
57
+ required_capability: "text_gen",
58
+ estimated_credits: 2,
59
+ depends_on_indices: [1]
60
+ },
61
+ {
62
+ description: "Format into final report",
63
+ required_capability: "text_gen",
64
+ estimated_credits: 1,
65
+ depends_on_indices: [2]
66
+ }
67
+ ]
68
+ },
69
+ "content-generation": {
70
+ keywords: ["write", "blog", "article", "content", "post", "essay"],
71
+ steps: [
72
+ {
73
+ description: "Create content outline",
74
+ required_capability: "text_gen",
75
+ estimated_credits: 1,
76
+ depends_on_indices: []
77
+ },
78
+ {
79
+ description: "Draft content from outline",
80
+ required_capability: "text_gen",
81
+ estimated_credits: 3,
82
+ depends_on_indices: [0]
83
+ },
84
+ {
85
+ description: "Review and refine draft",
86
+ required_capability: "text_gen",
87
+ estimated_credits: 2,
88
+ depends_on_indices: [1]
89
+ },
90
+ {
91
+ description: "Finalize and polish content",
92
+ required_capability: "text_gen",
93
+ estimated_credits: 1,
94
+ depends_on_indices: [2]
95
+ }
96
+ ]
97
+ }
98
+ };
99
+ function decompose(task, _availableCapabilities) {
100
+ const lower = task.toLowerCase();
101
+ for (const template of Object.values(TEMPLATES)) {
102
+ const matched = template.keywords.some((kw) => lower.includes(kw));
103
+ if (!matched) continue;
104
+ const ids = template.steps.map(() => randomUUID());
105
+ return template.steps.map((step, i) => ({
106
+ id: ids[i],
107
+ description: step.description,
108
+ required_capability: step.required_capability,
109
+ params: {},
110
+ depends_on: step.depends_on_indices.map((idx) => ids[idx]),
111
+ estimated_credits: step.estimated_credits
112
+ }));
113
+ }
114
+ return [];
115
+ }
116
+
117
+ // src/conductor/capability-matcher.ts
118
+ var MAX_ALTERNATIVES = 2;
119
+ function matchSubTasks(opts) {
120
+ const { db, subtasks, conductorOwner } = opts;
121
+ return subtasks.map((subtask) => {
122
+ const cards = searchCards(db, subtask.required_capability, { online: true });
123
+ const candidates = [];
124
+ for (const card of cards) {
125
+ const cardAsV2 = card;
126
+ if (Array.isArray(cardAsV2.skills)) {
127
+ for (const skill of cardAsV2.skills) {
128
+ candidates.push({
129
+ card,
130
+ cost: skill.pricing.credits_per_call,
131
+ skillId: skill.id
132
+ });
133
+ }
134
+ } else {
135
+ candidates.push({
136
+ card,
137
+ cost: card.pricing.credits_per_call,
138
+ skillId: void 0
139
+ });
140
+ }
141
+ }
142
+ const scored = scorePeers(candidates, conductorOwner);
143
+ if (scored.length === 0) {
144
+ return {
145
+ subtask_id: subtask.id,
146
+ selected_agent: "",
147
+ selected_skill: "",
148
+ score: 0,
149
+ credits: 0,
150
+ alternatives: []
151
+ };
152
+ }
153
+ const top = scored[0];
154
+ const alternatives = scored.slice(1, 1 + MAX_ALTERNATIVES).map((s) => ({
155
+ agent: s.card.owner,
156
+ skill: s.skillId ?? "",
157
+ score: s.rawScore,
158
+ credits: s.cost
159
+ }));
160
+ return {
161
+ subtask_id: subtask.id,
162
+ selected_agent: top.card.owner,
163
+ selected_skill: top.skillId ?? "",
164
+ score: top.rawScore,
165
+ credits: top.cost,
166
+ alternatives
167
+ };
168
+ });
169
+ }
170
+
171
+ // src/conductor/budget-controller.ts
172
+ var ORCHESTRATION_FEE = 5;
173
+ var BudgetController = class {
174
+ /**
175
+ * Creates a new BudgetController.
176
+ *
177
+ * @param budgetManager - Underlying BudgetManager for reserve floor enforcement.
178
+ * @param maxBudget - Hard ceiling for the orchestration run.
179
+ */
180
+ constructor(budgetManager, maxBudget) {
181
+ this.budgetManager = budgetManager;
182
+ this.maxBudget = maxBudget;
183
+ }
184
+ /**
185
+ * Pre-calculates the total budget for an orchestration run.
186
+ *
187
+ * Sums all matched sub-task credits, adds the orchestration fee,
188
+ * and determines whether approval is required (estimated > max).
189
+ *
190
+ * @param matches - MatchResult[] from the CapabilityMatcher.
191
+ * @returns An ExecutionBudget with cost breakdown and approval status.
192
+ */
193
+ calculateBudget(matches) {
194
+ const perTaskSpending = /* @__PURE__ */ new Map();
195
+ let subTotal = 0;
196
+ for (const match of matches) {
197
+ perTaskSpending.set(match.subtask_id, match.credits);
198
+ subTotal += match.credits;
199
+ }
200
+ const estimatedTotal = subTotal + ORCHESTRATION_FEE;
201
+ return {
202
+ estimated_total: estimatedTotal,
203
+ max_budget: this.maxBudget,
204
+ orchestration_fee: ORCHESTRATION_FEE,
205
+ per_task_spending: perTaskSpending,
206
+ requires_approval: estimatedTotal > this.maxBudget
207
+ };
208
+ }
209
+ /**
210
+ * Checks whether orchestration can proceed without explicit approval.
211
+ *
212
+ * Returns true only when:
213
+ * 1. The budget does NOT require approval (estimated_total <= max_budget)
214
+ * 2. The BudgetManager confirms sufficient credits (respecting reserve floor)
215
+ *
216
+ * @param budget - ExecutionBudget from calculateBudget().
217
+ * @returns true if execution can proceed autonomously.
218
+ */
219
+ canExecute(budget) {
220
+ if (budget.requires_approval) return false;
221
+ return this.budgetManager.canSpend(budget.estimated_total);
222
+ }
223
+ /**
224
+ * Checks budget after explicit user/agent approval.
225
+ *
226
+ * Ignores the requires_approval flag — used when the caller has already
227
+ * obtained explicit approval for the over-budget orchestration.
228
+ * Still enforces the reserve floor via BudgetManager.canSpend().
229
+ *
230
+ * @param budget - ExecutionBudget from calculateBudget().
231
+ * @returns true if the agent has sufficient credits (reserve floor check only).
232
+ */
233
+ approveAndCheck(budget) {
234
+ return this.budgetManager.canSpend(budget.estimated_total);
235
+ }
236
+ };
237
+
238
+ // src/conductor/pipeline-orchestrator.ts
239
+ function computeWaves(subtasks) {
240
+ const waves = [];
241
+ const completed = /* @__PURE__ */ new Set();
242
+ const remaining = new Map(subtasks.map((s) => [s.id, s]));
243
+ while (remaining.size > 0) {
244
+ const wave = [];
245
+ for (const [id, task] of remaining) {
246
+ const depsResolved = task.depends_on.every((dep) => completed.has(dep));
247
+ if (depsResolved) {
248
+ wave.push(id);
249
+ }
250
+ }
251
+ if (wave.length === 0) {
252
+ break;
253
+ }
254
+ for (const id of wave) {
255
+ remaining.delete(id);
256
+ completed.add(id);
257
+ }
258
+ waves.push(wave);
259
+ }
260
+ return waves;
261
+ }
262
+ async function orchestrate(opts) {
263
+ const { subtasks, matches, gatewayToken, resolveAgentUrl, timeoutMs = 3e4, maxBudget } = opts;
264
+ const startTime = Date.now();
265
+ if (subtasks.length === 0) {
266
+ return {
267
+ success: true,
268
+ results: /* @__PURE__ */ new Map(),
269
+ total_credits: 0,
270
+ latency_ms: Date.now() - startTime
271
+ };
272
+ }
273
+ const results = /* @__PURE__ */ new Map();
274
+ const errors = [];
275
+ let totalCredits = 0;
276
+ const waves = computeWaves(subtasks);
277
+ const subtaskMap = new Map(subtasks.map((s) => [s.id, s]));
278
+ for (const wave of waves) {
279
+ if (maxBudget !== void 0 && totalCredits >= maxBudget) {
280
+ errors.push(`Budget exceeded: spent ${totalCredits} cr, max ${maxBudget} cr`);
281
+ break;
282
+ }
283
+ const executableIds = [];
284
+ for (const taskId of wave) {
285
+ const m = matches.get(taskId);
286
+ if (maxBudget !== void 0 && m && totalCredits + m.credits > maxBudget) {
287
+ errors.push(`Skipping task ${taskId}: would exceed budget (${totalCredits} + ${m.credits} > ${maxBudget})`);
288
+ continue;
289
+ }
290
+ executableIds.push(taskId);
291
+ }
292
+ const waveResults = await Promise.allSettled(
293
+ executableIds.map(async (taskId) => {
294
+ const subtask = subtaskMap.get(taskId);
295
+ const m = matches.get(taskId);
296
+ if (!m) {
297
+ throw new Error(`No match found for subtask ${taskId}`);
298
+ }
299
+ const stepsContext = {};
300
+ for (const [id, val] of results) {
301
+ stepsContext[id] = val;
302
+ }
303
+ const interpContext = { steps: stepsContext, prev: void 0 };
304
+ if (subtask.depends_on.length > 0) {
305
+ const lastDep = subtask.depends_on[subtask.depends_on.length - 1];
306
+ interpContext.prev = results.get(lastDep);
307
+ }
308
+ const interpolatedParams = interpolateObject(
309
+ subtask.params,
310
+ interpContext
311
+ );
312
+ const primary = resolveAgentUrl(m.selected_agent);
313
+ try {
314
+ const res = await requestCapability({
315
+ gatewayUrl: primary.url,
316
+ token: gatewayToken,
317
+ cardId: primary.cardId,
318
+ params: interpolatedParams,
319
+ timeoutMs
320
+ });
321
+ return { taskId, result: res, credits: m.credits };
322
+ } catch (primaryErr) {
323
+ if (m.alternatives.length > 0) {
324
+ const alt = m.alternatives[0];
325
+ const altAgent = resolveAgentUrl(alt.agent);
326
+ try {
327
+ const altRes = await requestCapability({
328
+ gatewayUrl: altAgent.url,
329
+ token: gatewayToken,
330
+ cardId: altAgent.cardId,
331
+ params: interpolatedParams,
332
+ timeoutMs
333
+ });
334
+ return { taskId, result: altRes, credits: alt.credits };
335
+ } catch (altErr) {
336
+ throw new Error(
337
+ `Task ${taskId}: primary (${m.selected_agent}) failed: ${primaryErr instanceof Error ? primaryErr.message : String(primaryErr)}; alternative (${alt.agent}) failed: ${altErr instanceof Error ? altErr.message : String(altErr)}`
338
+ );
339
+ }
340
+ }
341
+ throw new Error(
342
+ `Task ${taskId}: ${primaryErr instanceof Error ? primaryErr.message : String(primaryErr)}`
343
+ );
344
+ }
345
+ })
346
+ );
347
+ for (const settlement of waveResults) {
348
+ if (settlement.status === "fulfilled") {
349
+ const { taskId, result, credits } = settlement.value;
350
+ results.set(taskId, result);
351
+ totalCredits += credits;
352
+ } else {
353
+ errors.push(settlement.reason instanceof Error ? settlement.reason.message : String(settlement.reason));
354
+ }
355
+ }
356
+ }
357
+ return {
358
+ success: errors.length === 0,
359
+ results,
360
+ total_credits: totalCredits,
361
+ latency_ms: Date.now() - startTime,
362
+ errors: errors.length > 0 ? errors : void 0
363
+ };
364
+ }
365
+
366
+ export {
367
+ decompose,
368
+ matchSubTasks,
369
+ ORCHESTRATION_FEE,
370
+ BudgetController,
371
+ orchestrate
372
+ };
@@ -0,0 +1,125 @@
1
+ // src/types/index.ts
2
+ import { z } from "zod";
3
+ var IOSchemaSchema = z.object({
4
+ name: z.string(),
5
+ type: z.enum(["text", "json", "file", "audio", "image", "video", "stream"]),
6
+ description: z.string().optional(),
7
+ required: z.boolean().default(true),
8
+ schema: z.record(z.unknown()).optional()
9
+ // JSON Schema
10
+ });
11
+ var PoweredBySchema = z.object({
12
+ provider: z.string().min(1),
13
+ model: z.string().optional(),
14
+ tier: z.string().optional()
15
+ });
16
+ var CapabilityCardSchema = z.object({
17
+ spec_version: z.literal("1.0").default("1.0"),
18
+ id: z.string().uuid(),
19
+ owner: z.string().min(1),
20
+ name: z.string().min(1).max(100),
21
+ description: z.string().max(500),
22
+ level: z.union([z.literal(1), z.literal(2), z.literal(3)]),
23
+ inputs: z.array(IOSchemaSchema),
24
+ outputs: z.array(IOSchemaSchema),
25
+ pricing: z.object({
26
+ credits_per_call: z.number().nonnegative(),
27
+ credits_per_minute: z.number().nonnegative().optional(),
28
+ /** Number of free monthly calls. Shown as a "N free/mo" badge in the Hub. */
29
+ free_tier: z.number().nonnegative().optional()
30
+ }),
31
+ availability: z.object({
32
+ online: z.boolean(),
33
+ schedule: z.string().optional()
34
+ // cron expression
35
+ }),
36
+ powered_by: z.array(PoweredBySchema).optional(),
37
+ /**
38
+ * Private per-card metadata. Stripped from all API and CLI responses —
39
+ * never transmitted beyond the local store.
40
+ */
41
+ _internal: z.record(z.unknown()).optional(),
42
+ metadata: z.object({
43
+ apis_used: z.array(z.string()).optional(),
44
+ avg_latency_ms: z.number().nonnegative().optional(),
45
+ success_rate: z.number().min(0).max(1).optional(),
46
+ tags: z.array(z.string()).optional()
47
+ }).optional(),
48
+ created_at: z.string().datetime().optional(),
49
+ updated_at: z.string().datetime().optional()
50
+ });
51
+ var SkillSchema = z.object({
52
+ /** Stable skill identifier, e.g. 'tts-elevenlabs'. Used for gateway routing and idle tracking. */
53
+ id: z.string().min(1),
54
+ name: z.string().min(1).max(100),
55
+ description: z.string().max(500),
56
+ level: z.union([z.literal(1), z.literal(2), z.literal(3)]),
57
+ /** Optional grouping category, e.g. 'tts' | 'video_gen' | 'code_review'. */
58
+ category: z.string().optional(),
59
+ inputs: z.array(IOSchemaSchema),
60
+ outputs: z.array(IOSchemaSchema),
61
+ pricing: z.object({
62
+ credits_per_call: z.number().nonnegative(),
63
+ credits_per_minute: z.number().nonnegative().optional(),
64
+ free_tier: z.number().nonnegative().optional()
65
+ }),
66
+ /** Per-skill online flag — overrides card-level availability for this skill. */
67
+ availability: z.object({ online: z.boolean() }).optional(),
68
+ powered_by: z.array(PoweredBySchema).optional(),
69
+ metadata: z.object({
70
+ apis_used: z.array(z.string()).optional(),
71
+ avg_latency_ms: z.number().nonnegative().optional(),
72
+ success_rate: z.number().min(0).max(1).optional(),
73
+ tags: z.array(z.string()).optional(),
74
+ capacity: z.object({
75
+ calls_per_hour: z.number().positive().default(60)
76
+ }).optional()
77
+ }).optional(),
78
+ /**
79
+ * Private per-skill metadata. Stripped from all API and CLI responses —
80
+ * never transmitted beyond the local store.
81
+ */
82
+ _internal: z.record(z.unknown()).optional()
83
+ });
84
+ var CapabilityCardV2Schema = z.object({
85
+ spec_version: z.literal("2.0"),
86
+ id: z.string().uuid(),
87
+ owner: z.string().min(1),
88
+ /** Agent display name — was 'name' in v1.0. */
89
+ agent_name: z.string().min(1).max(100),
90
+ /** At least one skill is required. */
91
+ skills: z.array(SkillSchema).min(1),
92
+ availability: z.object({
93
+ online: z.boolean(),
94
+ schedule: z.string().optional()
95
+ }),
96
+ /** Optional deployment environment metadata. */
97
+ environment: z.object({
98
+ runtime: z.string(),
99
+ region: z.string().optional()
100
+ }).optional(),
101
+ /**
102
+ * Private per-card metadata. Stripped from all API and CLI responses —
103
+ * never transmitted beyond the local store.
104
+ */
105
+ _internal: z.record(z.unknown()).optional(),
106
+ created_at: z.string().datetime().optional(),
107
+ updated_at: z.string().datetime().optional()
108
+ });
109
+ var AnyCardSchema = z.discriminatedUnion("spec_version", [
110
+ CapabilityCardSchema,
111
+ CapabilityCardV2Schema
112
+ ]);
113
+ var AgentBnBError = class extends Error {
114
+ constructor(message, code) {
115
+ super(message);
116
+ this.code = code;
117
+ this.name = "AgentBnBError";
118
+ }
119
+ };
120
+
121
+ export {
122
+ CapabilityCardSchema,
123
+ CapabilityCardV2Schema,
124
+ AgentBnBError
125
+ };
@@ -0,0 +1,192 @@
1
+ import {
2
+ AgentBnBError
3
+ } from "./chunk-TQMI73LL.js";
4
+
5
+ // src/credit/escrow.ts
6
+ import { randomUUID } from "crypto";
7
+ function holdEscrow(db, owner, amount, cardId) {
8
+ const escrowId = randomUUID();
9
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10
+ const hold = db.transaction(() => {
11
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
12
+ if (!row || row.balance < amount) {
13
+ throw new AgentBnBError("Insufficient credits", "INSUFFICIENT_CREDITS");
14
+ }
15
+ db.prepare(
16
+ "UPDATE credit_balances SET balance = balance - ?, updated_at = ? WHERE owner = ? AND balance >= ?"
17
+ ).run(amount, now, owner, amount);
18
+ db.prepare(
19
+ "INSERT INTO credit_escrow (id, owner, amount, card_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?)"
20
+ ).run(escrowId, owner, amount, cardId, "held", now);
21
+ db.prepare(
22
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
23
+ ).run(randomUUID(), owner, -amount, "escrow_hold", escrowId, now);
24
+ });
25
+ hold();
26
+ return escrowId;
27
+ }
28
+ function settleEscrow(db, escrowId, recipientOwner) {
29
+ const now = (/* @__PURE__ */ new Date()).toISOString();
30
+ const settle = db.transaction(() => {
31
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
32
+ if (!escrow) {
33
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
34
+ }
35
+ if (escrow.status !== "held") {
36
+ throw new AgentBnBError(
37
+ `Escrow ${escrowId} is already ${escrow.status}`,
38
+ "ESCROW_ALREADY_SETTLED"
39
+ );
40
+ }
41
+ db.prepare(
42
+ "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
43
+ ).run(recipientOwner, now);
44
+ db.prepare(
45
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
46
+ ).run(escrow.amount, now, recipientOwner);
47
+ db.prepare(
48
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
49
+ ).run("settled", now, escrowId);
50
+ db.prepare(
51
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
52
+ ).run(randomUUID(), recipientOwner, escrow.amount, "settlement", escrowId, now);
53
+ });
54
+ settle();
55
+ }
56
+ function releaseEscrow(db, escrowId) {
57
+ const now = (/* @__PURE__ */ new Date()).toISOString();
58
+ const release = db.transaction(() => {
59
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
60
+ if (!escrow) {
61
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
62
+ }
63
+ if (escrow.status !== "held") {
64
+ throw new AgentBnBError(
65
+ `Escrow ${escrowId} is already ${escrow.status}`,
66
+ "ESCROW_ALREADY_SETTLED"
67
+ );
68
+ }
69
+ db.prepare(
70
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
71
+ ).run(escrow.amount, now, escrow.owner);
72
+ db.prepare(
73
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
74
+ ).run("released", now, escrowId);
75
+ db.prepare(
76
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
77
+ ).run(randomUUID(), escrow.owner, escrow.amount, "refund", escrowId, now);
78
+ });
79
+ release();
80
+ }
81
+ function confirmEscrowDebit(db, escrowId) {
82
+ const now = (/* @__PURE__ */ new Date()).toISOString();
83
+ const confirm = db.transaction(() => {
84
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
85
+ if (!escrow) {
86
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
87
+ }
88
+ if (escrow.status !== "held") {
89
+ throw new AgentBnBError(
90
+ `Escrow ${escrowId} is already ${escrow.status}`,
91
+ "ESCROW_ALREADY_SETTLED"
92
+ );
93
+ }
94
+ db.prepare(
95
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
96
+ ).run("settled", now, escrowId);
97
+ db.prepare(
98
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
99
+ ).run(randomUUID(), escrow.owner, 0, "remote_settlement_confirmed", escrowId, now);
100
+ });
101
+ confirm();
102
+ }
103
+
104
+ // src/credit/ledger.ts
105
+ import Database from "better-sqlite3";
106
+ import { randomUUID as randomUUID2 } from "crypto";
107
+ var CREDIT_SCHEMA = `
108
+ CREATE TABLE IF NOT EXISTS credit_balances (
109
+ owner TEXT PRIMARY KEY,
110
+ balance INTEGER NOT NULL DEFAULT 0,
111
+ updated_at TEXT NOT NULL
112
+ );
113
+
114
+ CREATE TABLE IF NOT EXISTS credit_transactions (
115
+ id TEXT PRIMARY KEY,
116
+ owner TEXT NOT NULL,
117
+ amount INTEGER NOT NULL,
118
+ reason TEXT NOT NULL,
119
+ reference_id TEXT,
120
+ created_at TEXT NOT NULL
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS credit_escrow (
124
+ id TEXT PRIMARY KEY,
125
+ owner TEXT NOT NULL,
126
+ amount INTEGER NOT NULL,
127
+ card_id TEXT NOT NULL,
128
+ status TEXT NOT NULL DEFAULT 'held',
129
+ created_at TEXT NOT NULL,
130
+ settled_at TEXT
131
+ );
132
+
133
+ CREATE INDEX IF NOT EXISTS idx_transactions_owner ON credit_transactions(owner, created_at);
134
+ CREATE INDEX IF NOT EXISTS idx_escrow_owner ON credit_escrow(owner);
135
+ `;
136
+ function openCreditDb(path = ":memory:") {
137
+ const db = new Database(path);
138
+ db.pragma("journal_mode = WAL");
139
+ db.pragma("foreign_keys = ON");
140
+ db.exec(CREDIT_SCHEMA);
141
+ return db;
142
+ }
143
+ function bootstrapAgent(db, owner, amount = 100) {
144
+ const now = (/* @__PURE__ */ new Date()).toISOString();
145
+ db.transaction(() => {
146
+ const result = db.prepare("INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, ?, ?)").run(owner, amount, now);
147
+ if (result.changes > 0) {
148
+ db.prepare(
149
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
150
+ ).run(randomUUID2(), owner, amount, "bootstrap", null, now);
151
+ }
152
+ })();
153
+ }
154
+ function getBalance(db, owner) {
155
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
156
+ return row?.balance ?? 0;
157
+ }
158
+ function getTransactions(db, owner, limit = 100) {
159
+ return db.prepare(
160
+ "SELECT id, owner, amount, reason, reference_id, created_at FROM credit_transactions WHERE owner = ? ORDER BY created_at DESC LIMIT ?"
161
+ ).all(owner, limit);
162
+ }
163
+ function recordEarning(db, owner, amount, _cardId, receiptNonce) {
164
+ const now = (/* @__PURE__ */ new Date()).toISOString();
165
+ db.transaction(() => {
166
+ const existing = db.prepare(
167
+ "SELECT id FROM credit_transactions WHERE reference_id = ? AND reason = 'remote_earning'"
168
+ ).get(receiptNonce);
169
+ if (existing) return;
170
+ db.prepare(
171
+ "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
172
+ ).run(owner, now);
173
+ db.prepare(
174
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
175
+ ).run(amount, now, owner);
176
+ db.prepare(
177
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
178
+ ).run(randomUUID2(), owner, amount, "remote_earning", receiptNonce, now);
179
+ })();
180
+ }
181
+
182
+ export {
183
+ holdEscrow,
184
+ settleEscrow,
185
+ releaseEscrow,
186
+ confirmEscrowDebit,
187
+ openCreditDb,
188
+ bootstrapAgent,
189
+ getBalance,
190
+ getTransactions,
191
+ recordEarning
192
+ };