@tokenbuddy/tokenbuddy 1.0.4

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 (43) hide show
  1. package/bin/tb-proxyd.js +2 -0
  2. package/bin/tb.js +3 -0
  3. package/bin/tokenbuddy-proxyd.js +2 -0
  4. package/bin/tokenbuddy.js +3 -0
  5. package/dist/src/buyer-store.d.ts +118 -0
  6. package/dist/src/buyer-store.d.ts.map +1 -0
  7. package/dist/src/buyer-store.js +296 -0
  8. package/dist/src/buyer-store.js.map +1 -0
  9. package/dist/src/cli.d.ts +3 -0
  10. package/dist/src/cli.d.ts.map +1 -0
  11. package/dist/src/cli.js +648 -0
  12. package/dist/src/cli.js.map +1 -0
  13. package/dist/src/daemon.d.ts +48 -0
  14. package/dist/src/daemon.d.ts.map +1 -0
  15. package/dist/src/daemon.js +998 -0
  16. package/dist/src/daemon.js.map +1 -0
  17. package/dist/src/index.d.ts +2 -0
  18. package/dist/src/index.d.ts.map +1 -0
  19. package/dist/src/index.js +12 -0
  20. package/dist/src/index.js.map +1 -0
  21. package/dist/src/provider-install.d.ts +44 -0
  22. package/dist/src/provider-install.d.ts.map +1 -0
  23. package/dist/src/provider-install.js +286 -0
  24. package/dist/src/provider-install.js.map +1 -0
  25. package/dist/src/tb-proxyd.d.ts +2 -0
  26. package/dist/src/tb-proxyd.d.ts.map +1 -0
  27. package/dist/src/tb-proxyd.js +54 -0
  28. package/dist/src/tb-proxyd.js.map +1 -0
  29. package/dist/src/terminal-detect.d.ts +29 -0
  30. package/dist/src/terminal-detect.d.ts.map +1 -0
  31. package/dist/src/terminal-detect.js +209 -0
  32. package/dist/src/terminal-detect.js.map +1 -0
  33. package/package.json +29 -0
  34. package/src/buyer-store.ts +536 -0
  35. package/src/cli.ts +732 -0
  36. package/src/daemon.ts +1158 -0
  37. package/src/index.ts +12 -0
  38. package/src/provider-install.ts +363 -0
  39. package/src/tb-proxyd.ts +60 -0
  40. package/src/terminal-detect.ts +225 -0
  41. package/tests/e2e.test.ts +264 -0
  42. package/tests/tokenbuddy.test.ts +1186 -0
  43. package/tsconfig.json +8 -0
@@ -0,0 +1,536 @@
1
+ // @ts-ignore node:sqlite types are not present in the current @types/node release.
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import * as crypto from "crypto";
4
+ import * as fs from "fs";
5
+ import * as os from "os";
6
+ import * as path from "path";
7
+ import { createModuleLogger } from "@tokenbuddy/logging";
8
+
9
+ const logger = createModuleLogger("tb-proxyd");
10
+
11
+ export interface CachedToken {
12
+ token: string;
13
+ balanceMicros: number;
14
+ }
15
+
16
+ export interface PaymentConfig {
17
+ method: string;
18
+ enabled: boolean;
19
+ isDefault: boolean;
20
+ config?: Record<string, unknown>;
21
+ updatedAt: string;
22
+ }
23
+
24
+ export interface PendingPurchaseInput {
25
+ purchaseId: string;
26
+ sellerKey: string;
27
+ modelId: string;
28
+ paymentMethod: string;
29
+ amountUsdMicros: number;
30
+ status: string;
31
+ paymentReference?: string;
32
+ expiresAt?: string;
33
+ }
34
+
35
+ export interface PurchaseLedgerInput {
36
+ purchaseId: string;
37
+ sellerKey: string;
38
+ modelId: string;
39
+ paymentMethod: string;
40
+ status: string;
41
+ creditMicros: number;
42
+ currency: string;
43
+ paymentReference?: string;
44
+ completedAt?: string;
45
+ }
46
+
47
+ export interface SafePurchaseLedgerEntry {
48
+ purchaseId: string;
49
+ sellerKey: string;
50
+ modelId: string;
51
+ paymentMethod: string;
52
+ status: string;
53
+ creditMicros: number;
54
+ currency: string;
55
+ paymentReferenceHash?: string;
56
+ createdAt: string;
57
+ completedAt?: string;
58
+ }
59
+
60
+ export interface InferenceLedgerInput {
61
+ requestId: string;
62
+ sellerKey: string;
63
+ modelId: string;
64
+ endpoint: string;
65
+ status: string;
66
+ promptTokens: number;
67
+ completionTokens: number;
68
+ billedMicros: number;
69
+ prompt?: string;
70
+ response?: string;
71
+ }
72
+
73
+ export interface SafeInferenceLedgerEntry {
74
+ requestId: string;
75
+ sellerKey: string;
76
+ modelId: string;
77
+ endpoint: string;
78
+ status: string;
79
+ promptTokens: number;
80
+ completionTokens: number;
81
+ billedMicros: number;
82
+ promptHash?: string;
83
+ responseHash?: string;
84
+ createdAt: string;
85
+ }
86
+
87
+ export interface BuyerStoreSummary {
88
+ journalMode: string;
89
+ paymentsCount: number;
90
+ pendingPurchasesCount: number;
91
+ purchaseLedgerCount: number;
92
+ inferenceLedgerCount: number;
93
+ }
94
+
95
+ export interface ProviderInstallSnapshot {
96
+ providerId: string;
97
+ files: Array<{
98
+ path: string;
99
+ existed: boolean;
100
+ content?: string;
101
+ }>;
102
+ }
103
+
104
+ export interface BuyerStoreOptions {
105
+ root?: string;
106
+ dbPath?: string;
107
+ }
108
+
109
+ function nowIso(): string {
110
+ return new Date().toISOString();
111
+ }
112
+
113
+ function safeHash(value?: string): string | undefined {
114
+ if (!value) {
115
+ return undefined;
116
+ }
117
+ return crypto.createHash("sha256").update(value).digest("hex");
118
+ }
119
+
120
+ function boolFromSql(value: unknown): boolean {
121
+ return value === 1 || value === true;
122
+ }
123
+
124
+ function ensureDirForFile(filePath: string): void {
125
+ const dir = path.dirname(filePath);
126
+ if (dir !== "." && !fs.existsSync(dir)) {
127
+ fs.mkdirSync(dir, { recursive: true });
128
+ }
129
+ }
130
+
131
+ export function resolveBuyerStorePath(options: BuyerStoreOptions = {}): string {
132
+ if (options.dbPath) {
133
+ return options.dbPath;
134
+ }
135
+ const root = options.root || process.env.TOKENBUDDY_BUYER_STORE || path.join(os.homedir(), ".tokenbuddy-store");
136
+ return path.join(root, "buyer-store.db");
137
+ }
138
+
139
+ export class BuyerStore {
140
+ private db: DatabaseSync;
141
+
142
+ constructor(options: BuyerStoreOptions | string = {}) {
143
+ const dbPath = typeof options === "string" ? options : resolveBuyerStorePath(options);
144
+ ensureDirForFile(dbPath);
145
+ this.db = new DatabaseSync(dbPath);
146
+ this.db.exec("PRAGMA journal_mode = WAL;");
147
+ this.initSchema();
148
+ }
149
+
150
+ public journalMode(): string {
151
+ const row = this.db.prepare("PRAGMA journal_mode;").get() as { journal_mode: string };
152
+ return row.journal_mode;
153
+ }
154
+
155
+ public summary(): BuyerStoreSummary {
156
+ return {
157
+ journalMode: this.journalMode(),
158
+ paymentsCount: this.countRows("payment_config"),
159
+ pendingPurchasesCount: this.countRows("pending_purchases"),
160
+ purchaseLedgerCount: this.countRows("purchase_ledger"),
161
+ inferenceLedgerCount: this.countRows("inference_ledger")
162
+ };
163
+ }
164
+
165
+ public saveProviderInstallSnapshot(snapshot: ProviderInstallSnapshot): void {
166
+ const updatedAt = nowIso();
167
+ this.db.prepare(
168
+ `INSERT OR REPLACE INTO provider_install_state (
169
+ provider_id, snapshot_json, created_at, updated_at
170
+ ) VALUES (
171
+ ?, ?,
172
+ COALESCE((SELECT created_at FROM provider_install_state WHERE provider_id = ?), ?),
173
+ ?
174
+ )`
175
+ ).run(
176
+ snapshot.providerId,
177
+ JSON.stringify(snapshot),
178
+ snapshot.providerId,
179
+ updatedAt,
180
+ updatedAt
181
+ );
182
+ }
183
+
184
+ public getProviderInstallSnapshot(providerId: string): ProviderInstallSnapshot | undefined {
185
+ const row = this.db.prepare(
186
+ "SELECT snapshot_json FROM provider_install_state WHERE provider_id = ?"
187
+ ).get(providerId) as { snapshot_json: string } | undefined;
188
+ if (!row) {
189
+ return undefined;
190
+ }
191
+ return JSON.parse(row.snapshot_json) as ProviderInstallSnapshot;
192
+ }
193
+
194
+ public removeProviderInstallSnapshot(providerId: string): boolean {
195
+ const result = this.db.prepare("DELETE FROM provider_install_state WHERE provider_id = ?").run(providerId) as { changes: number };
196
+ return result.changes > 0;
197
+ }
198
+
199
+ public getToken(sellerKey: string): CachedToken | undefined {
200
+ const stmt = this.db.prepare("SELECT token, balance_micros FROM token_cache WHERE seller_key = ?");
201
+ const row = stmt.get(sellerKey) as { token: string; balance_micros: number } | undefined;
202
+ if (!row) {
203
+ return undefined;
204
+ }
205
+ return {
206
+ token: row.token,
207
+ balanceMicros: row.balance_micros
208
+ };
209
+ }
210
+
211
+ public saveToken(sellerKey: string, token: string, tokenClass: string, balanceMicros: number, expiresAt: string): void {
212
+ const stmt = this.db.prepare(
213
+ `INSERT OR REPLACE INTO token_cache (
214
+ seller_key, token, token_class, balance_micros, expires_at, updated_at
215
+ ) VALUES (?, ?, ?, ?, ?, ?)`
216
+ );
217
+ stmt.run(sellerKey, token, tokenClass, balanceMicros, expiresAt, nowIso());
218
+ }
219
+
220
+ public deductBalance(sellerKey: string, amountMicros: number): void {
221
+ const token = this.getToken(sellerKey);
222
+ if (!token) {
223
+ return;
224
+ }
225
+ const newBalance = Math.max(0, token.balanceMicros - amountMicros);
226
+ const stmt = this.db.prepare("UPDATE token_cache SET balance_micros = ?, updated_at = ? WHERE seller_key = ?");
227
+ stmt.run(newBalance, nowIso(), sellerKey);
228
+ logger.info("token.cache.debited", "token cache balance debited", {
229
+ sellerKey,
230
+ amountMicros,
231
+ balanceMicros: newBalance
232
+ });
233
+ }
234
+
235
+ public listPayments(): PaymentConfig[] {
236
+ const rows = this.db.prepare(
237
+ `SELECT method, enabled, is_default, config_json, updated_at
238
+ FROM payment_config
239
+ ORDER BY method ASC`
240
+ ).all() as Array<{
241
+ method: string;
242
+ enabled: number;
243
+ is_default: number;
244
+ config_json: string | null;
245
+ updated_at: string;
246
+ }>;
247
+
248
+ return rows.map(row => ({
249
+ method: row.method,
250
+ enabled: boolFromSql(row.enabled),
251
+ isDefault: boolFromSql(row.is_default),
252
+ config: row.config_json ? JSON.parse(row.config_json) as Record<string, unknown> : undefined,
253
+ updatedAt: row.updated_at
254
+ }));
255
+ }
256
+
257
+ public savePayment(config: Omit<PaymentConfig, "updatedAt">): void {
258
+ const updatedAt = nowIso();
259
+ if (config.isDefault) {
260
+ this.db.prepare("UPDATE payment_config SET is_default = 0").run();
261
+ }
262
+ this.db.prepare(
263
+ `INSERT OR REPLACE INTO payment_config (
264
+ method, enabled, is_default, config_json, created_at, updated_at
265
+ ) VALUES (
266
+ ?, ?, ?, ?,
267
+ COALESCE((SELECT created_at FROM payment_config WHERE method = ?), ?),
268
+ ?
269
+ )`
270
+ ).run(
271
+ config.method,
272
+ config.enabled ? 1 : 0,
273
+ config.isDefault ? 1 : 0,
274
+ config.config ? JSON.stringify(config.config) : null,
275
+ config.method,
276
+ updatedAt,
277
+ updatedAt
278
+ );
279
+ }
280
+
281
+ public getPayment(method: string): PaymentConfig | undefined {
282
+ return this.listPayments().find((payment) => payment.method === method);
283
+ }
284
+
285
+ public removePayment(method: string): boolean {
286
+ const result = this.db.prepare("DELETE FROM payment_config WHERE method = ?").run(method) as { changes: number };
287
+ return result.changes > 0;
288
+ }
289
+
290
+ public upsertPendingPurchase(input: PendingPurchaseInput): void {
291
+ const updatedAt = nowIso();
292
+ this.db.prepare(
293
+ `INSERT OR REPLACE INTO pending_purchases (
294
+ purchase_id, seller_key, model_id, payment_method, amount_usd_micros,
295
+ status, payment_reference_hash, created_at, updated_at, expires_at
296
+ ) VALUES (
297
+ ?, ?, ?, ?, ?, ?, ?,
298
+ COALESCE((SELECT created_at FROM pending_purchases WHERE purchase_id = ?), ?),
299
+ ?, ?
300
+ )`
301
+ ).run(
302
+ input.purchaseId,
303
+ input.sellerKey,
304
+ input.modelId,
305
+ input.paymentMethod,
306
+ input.amountUsdMicros,
307
+ input.status,
308
+ safeHash(input.paymentReference) || null,
309
+ input.purchaseId,
310
+ updatedAt,
311
+ updatedAt,
312
+ input.expiresAt || null
313
+ );
314
+ }
315
+
316
+ public listPendingPurchases(): Array<PendingPurchaseInput & { paymentReferenceHash?: string; updatedAt: string }> {
317
+ const rows = this.db.prepare(
318
+ `SELECT purchase_id, seller_key, model_id, payment_method, amount_usd_micros,
319
+ status, payment_reference_hash, updated_at, expires_at
320
+ FROM pending_purchases
321
+ ORDER BY updated_at DESC, purchase_id ASC`
322
+ ).all() as Array<{
323
+ purchase_id: string;
324
+ seller_key: string;
325
+ model_id: string;
326
+ payment_method: string;
327
+ amount_usd_micros: number;
328
+ status: string;
329
+ payment_reference_hash: string | null;
330
+ updated_at: string;
331
+ expires_at: string | null;
332
+ }>;
333
+
334
+ return rows.map(row => ({
335
+ purchaseId: row.purchase_id,
336
+ sellerKey: row.seller_key,
337
+ modelId: row.model_id,
338
+ paymentMethod: row.payment_method,
339
+ amountUsdMicros: row.amount_usd_micros,
340
+ status: row.status,
341
+ paymentReferenceHash: row.payment_reference_hash || undefined,
342
+ updatedAt: row.updated_at,
343
+ expiresAt: row.expires_at || undefined
344
+ }));
345
+ }
346
+
347
+ public recordPurchaseLedger(input: PurchaseLedgerInput): void {
348
+ const createdAt = nowIso();
349
+ this.db.prepare(
350
+ `INSERT INTO purchase_ledger (
351
+ purchase_id, seller_key, model_id, payment_method, status, credit_micros,
352
+ currency, payment_reference_hash, created_at, completed_at
353
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
354
+ ).run(
355
+ input.purchaseId,
356
+ input.sellerKey,
357
+ input.modelId,
358
+ input.paymentMethod,
359
+ input.status,
360
+ input.creditMicros,
361
+ input.currency,
362
+ safeHash(input.paymentReference) || null,
363
+ createdAt,
364
+ input.completedAt || null
365
+ );
366
+ }
367
+
368
+ public listPurchaseLedger(): SafePurchaseLedgerEntry[] {
369
+ const rows = this.db.prepare(
370
+ `SELECT purchase_id, seller_key, model_id, payment_method, status, credit_micros,
371
+ currency, payment_reference_hash, created_at, completed_at
372
+ FROM purchase_ledger
373
+ ORDER BY id ASC`
374
+ ).all() as Array<{
375
+ purchase_id: string;
376
+ seller_key: string;
377
+ model_id: string;
378
+ payment_method: string;
379
+ status: string;
380
+ credit_micros: number;
381
+ currency: string;
382
+ payment_reference_hash: string | null;
383
+ created_at: string;
384
+ completed_at: string | null;
385
+ }>;
386
+
387
+ return rows.map(row => ({
388
+ purchaseId: row.purchase_id,
389
+ sellerKey: row.seller_key,
390
+ modelId: row.model_id,
391
+ paymentMethod: row.payment_method,
392
+ status: row.status,
393
+ creditMicros: row.credit_micros,
394
+ currency: row.currency,
395
+ paymentReferenceHash: row.payment_reference_hash || undefined,
396
+ createdAt: row.created_at,
397
+ completedAt: row.completed_at || undefined
398
+ }));
399
+ }
400
+
401
+ public recordInferenceLedger(input: InferenceLedgerInput): void {
402
+ this.db.prepare(
403
+ `INSERT INTO inference_ledger (
404
+ request_id, seller_key, model_id, endpoint, status, prompt_tokens,
405
+ completion_tokens, billed_micros, prompt_hash, response_hash, created_at
406
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
407
+ ).run(
408
+ input.requestId,
409
+ input.sellerKey,
410
+ input.modelId,
411
+ input.endpoint,
412
+ input.status,
413
+ input.promptTokens,
414
+ input.completionTokens,
415
+ input.billedMicros,
416
+ safeHash(input.prompt) || null,
417
+ safeHash(input.response) || null,
418
+ nowIso()
419
+ );
420
+ }
421
+
422
+ public listInferenceLedger(): SafeInferenceLedgerEntry[] {
423
+ const rows = this.db.prepare(
424
+ `SELECT request_id, seller_key, model_id, endpoint, status, prompt_tokens,
425
+ completion_tokens, billed_micros, prompt_hash, response_hash, created_at
426
+ FROM inference_ledger
427
+ ORDER BY id ASC`
428
+ ).all() as Array<{
429
+ request_id: string;
430
+ seller_key: string;
431
+ model_id: string;
432
+ endpoint: string;
433
+ status: string;
434
+ prompt_tokens: number;
435
+ completion_tokens: number;
436
+ billed_micros: number;
437
+ prompt_hash: string | null;
438
+ response_hash: string | null;
439
+ created_at: string;
440
+ }>;
441
+
442
+ return rows.map(row => ({
443
+ requestId: row.request_id,
444
+ sellerKey: row.seller_key,
445
+ modelId: row.model_id,
446
+ endpoint: row.endpoint,
447
+ status: row.status,
448
+ promptTokens: row.prompt_tokens,
449
+ completionTokens: row.completion_tokens,
450
+ billedMicros: row.billed_micros,
451
+ promptHash: row.prompt_hash || undefined,
452
+ responseHash: row.response_hash || undefined,
453
+ createdAt: row.created_at
454
+ }));
455
+ }
456
+
457
+ public close(): void {
458
+ this.db.close();
459
+ }
460
+
461
+ private initSchema(): void {
462
+ this.db.exec(`
463
+ CREATE TABLE IF NOT EXISTS token_cache (
464
+ seller_key TEXT PRIMARY KEY,
465
+ token TEXT NOT NULL,
466
+ token_class TEXT NOT NULL,
467
+ balance_micros INTEGER NOT NULL,
468
+ expires_at TEXT NOT NULL,
469
+ updated_at TEXT NOT NULL
470
+ );
471
+
472
+ CREATE TABLE IF NOT EXISTS payment_config (
473
+ method TEXT PRIMARY KEY,
474
+ enabled INTEGER NOT NULL,
475
+ is_default INTEGER NOT NULL DEFAULT 0,
476
+ config_json TEXT,
477
+ created_at TEXT NOT NULL,
478
+ updated_at TEXT NOT NULL
479
+ );
480
+
481
+ CREATE TABLE IF NOT EXISTS pending_purchases (
482
+ purchase_id TEXT PRIMARY KEY,
483
+ seller_key TEXT NOT NULL,
484
+ model_id TEXT NOT NULL,
485
+ payment_method TEXT NOT NULL,
486
+ amount_usd_micros INTEGER NOT NULL,
487
+ status TEXT NOT NULL,
488
+ payment_reference_hash TEXT,
489
+ created_at TEXT NOT NULL,
490
+ updated_at TEXT NOT NULL,
491
+ expires_at TEXT
492
+ );
493
+
494
+ CREATE TABLE IF NOT EXISTS purchase_ledger (
495
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
496
+ purchase_id TEXT NOT NULL,
497
+ seller_key TEXT NOT NULL,
498
+ model_id TEXT NOT NULL,
499
+ payment_method TEXT NOT NULL,
500
+ status TEXT NOT NULL,
501
+ credit_micros INTEGER NOT NULL,
502
+ currency TEXT NOT NULL,
503
+ payment_reference_hash TEXT,
504
+ created_at TEXT NOT NULL,
505
+ completed_at TEXT
506
+ );
507
+
508
+ CREATE TABLE IF NOT EXISTS inference_ledger (
509
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
510
+ request_id TEXT NOT NULL,
511
+ seller_key TEXT NOT NULL,
512
+ model_id TEXT NOT NULL,
513
+ endpoint TEXT NOT NULL,
514
+ status TEXT NOT NULL,
515
+ prompt_tokens INTEGER NOT NULL,
516
+ completion_tokens INTEGER NOT NULL,
517
+ billed_micros INTEGER NOT NULL,
518
+ prompt_hash TEXT,
519
+ response_hash TEXT,
520
+ created_at TEXT NOT NULL
521
+ );
522
+
523
+ CREATE TABLE IF NOT EXISTS provider_install_state (
524
+ provider_id TEXT PRIMARY KEY,
525
+ snapshot_json TEXT NOT NULL,
526
+ created_at TEXT NOT NULL,
527
+ updated_at TEXT NOT NULL
528
+ );
529
+ `);
530
+ }
531
+
532
+ private countRows(tableName: string): number {
533
+ const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${tableName}`).get() as { count: number };
534
+ return row.count;
535
+ }
536
+ }