@tokenbuddy/tokenbuddy 1.0.6 → 1.0.7

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/dist/src/buyer-store.d.ts +28 -1
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +71 -16
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +17 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +201 -32
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/daemon.d.ts +5 -0
  10. package/dist/src/daemon.d.ts.map +1 -1
  11. package/dist/src/daemon.js +279 -72
  12. package/dist/src/daemon.js.map +1 -1
  13. package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
  14. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
  15. package/dist/src/doctor-clawtip-wallet.js +54 -0
  16. package/dist/src/doctor-clawtip-wallet.js.map +1 -0
  17. package/dist/src/doctor-diagnostics.d.ts +2 -0
  18. package/dist/src/doctor-diagnostics.d.ts.map +1 -1
  19. package/dist/src/doctor-diagnostics.js +5 -0
  20. package/dist/src/doctor-diagnostics.js.map +1 -1
  21. package/dist/src/init-clawtip-activation.d.ts +48 -0
  22. package/dist/src/init-clawtip-activation.d.ts.map +1 -0
  23. package/dist/src/init-clawtip-activation.js +395 -0
  24. package/dist/src/init-clawtip-activation.js.map +1 -0
  25. package/dist/src/init-payment-options.d.ts +23 -1
  26. package/dist/src/init-payment-options.d.ts.map +1 -1
  27. package/dist/src/init-payment-options.js +97 -22
  28. package/dist/src/init-payment-options.js.map +1 -1
  29. package/dist/src/terminal-image.d.ts +22 -0
  30. package/dist/src/terminal-image.d.ts.map +1 -0
  31. package/dist/src/terminal-image.js +135 -0
  32. package/dist/src/terminal-image.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/buyer-store.ts +140 -17
  35. package/src/cli.ts +251 -33
  36. package/src/daemon.ts +308 -53
  37. package/src/doctor-clawtip-wallet.ts +70 -0
  38. package/src/doctor-diagnostics.ts +11 -0
  39. package/src/init-clawtip-activation.ts +487 -0
  40. package/src/init-payment-options.ts +140 -22
  41. package/src/terminal-image.ts +187 -0
  42. package/tests/e2e.test.ts +79 -5
  43. package/tests/tokenbuddy.test.ts +745 -19
@@ -0,0 +1,135 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ function defaultRunCommand(command, args) {
5
+ return new Promise((resolve, reject) => {
6
+ const child = spawn(command, args, {
7
+ stdio: "ignore",
8
+ });
9
+ child.on("error", reject);
10
+ child.on("close", (code) => {
11
+ if (code === 0) {
12
+ resolve();
13
+ return;
14
+ }
15
+ reject(new Error(`${command} exited with ${code}`));
16
+ });
17
+ });
18
+ }
19
+ function terminalSupportsInlineImages(env, stdoutIsTTY) {
20
+ if (!stdoutIsTTY) {
21
+ return undefined;
22
+ }
23
+ const term = env.TERM || "";
24
+ if (env.KITTY_WINDOW_ID || term.includes("xterm-kitty")) {
25
+ return "kitty";
26
+ }
27
+ const termProgram = env.TERM_PROGRAM || "";
28
+ if (termProgram === "iTerm.app" || termProgram === "WezTerm" || env.WEZTERM_EXECUTABLE) {
29
+ return "iterm";
30
+ }
31
+ return undefined;
32
+ }
33
+ function basenameBase64(filePath) {
34
+ return Buffer.from(path.basename(filePath), "utf8").toString("base64");
35
+ }
36
+ function itermInlineImageSequence(filePath, image) {
37
+ const payload = image.toString("base64");
38
+ return `\u001B]1337;File=name=${basenameBase64(filePath)};inline=1;width=60%;preserveAspectRatio=1:${payload}\u0007\n`;
39
+ }
40
+ function kittyInlineImageSequence(filePath) {
41
+ const payload = Buffer.from(filePath, "utf8").toString("base64");
42
+ return `\u001B_Ga=T,t=f,f=100,c=60;${payload}\u001B\\\n`;
43
+ }
44
+ function openCommandForPlatform(platform, filePath) {
45
+ if (platform === "darwin") {
46
+ return {
47
+ command: "open",
48
+ args: [filePath],
49
+ label: `open ${filePath}`,
50
+ };
51
+ }
52
+ if (platform === "linux") {
53
+ return {
54
+ command: "xdg-open",
55
+ args: [filePath],
56
+ label: `xdg-open ${filePath}`,
57
+ };
58
+ }
59
+ if (platform === "win32") {
60
+ return {
61
+ command: "cmd",
62
+ args: ["/c", "start", "", filePath],
63
+ label: `start ${filePath}`,
64
+ };
65
+ }
66
+ return undefined;
67
+ }
68
+ export function detectTerminalImageDisplay(options = {}) {
69
+ return terminalSupportsInlineImages(options.env || process.env, options.stdoutIsTTY ?? Boolean(process.stdout.isTTY));
70
+ }
71
+ export async function displayTerminalImage(filePath, options = {}) {
72
+ const fileExists = options.fileExists || fs.existsSync;
73
+ const readFile = options.readFile || fs.readFileSync;
74
+ const write = options.write || ((chunk) => process.stdout.write(chunk));
75
+ const env = options.env || process.env;
76
+ const platform = options.platform || process.platform;
77
+ const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
78
+ const openCommand = openCommandForPlatform(platform, filePath);
79
+ const fallbackCommand = openCommand?.label;
80
+ if (!fileExists(filePath)) {
81
+ return {
82
+ method: "manual",
83
+ displayed: false,
84
+ fallbackCommand,
85
+ message: `QR image file is missing: ${filePath}`,
86
+ };
87
+ }
88
+ const inlineProtocol = terminalSupportsInlineImages(env, stdoutIsTTY);
89
+ if (inlineProtocol === "kitty") {
90
+ write(kittyInlineImageSequence(filePath));
91
+ return {
92
+ method: "inline-kitty",
93
+ displayed: true,
94
+ fallbackCommand,
95
+ message: "Displayed the ClawTip wallet QR image inline using the Kitty graphics protocol.",
96
+ };
97
+ }
98
+ if (inlineProtocol === "iterm") {
99
+ write(itermInlineImageSequence(filePath, readFile(filePath)));
100
+ return {
101
+ method: "inline-iterm",
102
+ displayed: true,
103
+ fallbackCommand,
104
+ message: "Displayed the ClawTip wallet QR image inline using the iTerm2 image protocol.",
105
+ };
106
+ }
107
+ if (openCommand) {
108
+ try {
109
+ const runCommand = options.runCommand || defaultRunCommand;
110
+ await runCommand(openCommand.command, openCommand.args);
111
+ return {
112
+ method: "system-open",
113
+ displayed: true,
114
+ fallbackCommand,
115
+ message: `Opened the ClawTip wallet QR image with the system image viewer: ${openCommand.label}`,
116
+ };
117
+ }
118
+ catch (error) {
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ return {
121
+ method: "manual",
122
+ displayed: false,
123
+ fallbackCommand,
124
+ message: `Could not open QR image automatically: ${message}`,
125
+ };
126
+ }
127
+ }
128
+ return {
129
+ method: "manual",
130
+ displayed: false,
131
+ fallbackCommand,
132
+ message: "This terminal does not advertise inline image support and no system opener is available.",
133
+ };
134
+ }
135
+ //# sourceMappingURL=terminal-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-image.js","sourceRoot":"","sources":["../../src/terminal-image.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenbuddy/tokenbuddy",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "TokenBuddy Client CLI and Daemon",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -11,6 +11,9 @@ const logger = createModuleLogger("tb-proxyd");
11
11
  export interface CachedToken {
12
12
  token: string;
13
13
  balanceMicros: number;
14
+ reservedMicros: number;
15
+ spentMicros: number;
16
+ balanceSource?: string;
14
17
  }
15
18
 
16
19
  export interface PaymentConfig {
@@ -66,6 +69,12 @@ export interface InferenceLedgerInput {
66
69
  promptTokens: number;
67
70
  completionTokens: number;
68
71
  billedMicros: number;
72
+ estimatedMicros?: number;
73
+ settledMicros?: number;
74
+ settledUsdMicros?: number;
75
+ priceVersion?: string;
76
+ balanceSnapshotMicros?: number;
77
+ balanceSource?: string;
69
78
  prompt?: string;
70
79
  response?: string;
71
80
  }
@@ -79,11 +88,28 @@ export interface SafeInferenceLedgerEntry {
79
88
  promptTokens: number;
80
89
  completionTokens: number;
81
90
  billedMicros: number;
91
+ estimatedMicros?: number;
92
+ settledMicros?: number;
93
+ settledUsdMicros?: number;
94
+ priceVersion?: string;
95
+ balanceSnapshotMicros?: number;
96
+ balanceSource?: string;
82
97
  promptHash?: string;
83
98
  responseHash?: string;
84
99
  createdAt: string;
85
100
  }
86
101
 
102
+ export interface TokenBalanceSnapshotInput {
103
+ sellerKey: string;
104
+ token?: string;
105
+ tokenClass?: string;
106
+ balanceMicros: number;
107
+ reservedMicros?: number;
108
+ spentMicros?: number;
109
+ balanceSource: string;
110
+ expiresAt?: string;
111
+ }
112
+
87
113
  export interface BuyerStoreSummary {
88
114
  journalMode: string;
89
115
  paymentsCount: number;
@@ -295,41 +321,83 @@ export class BuyerStore {
295
321
  }
296
322
 
297
323
  public getToken(sellerKey: string): CachedToken | undefined {
298
- const stmt = this.db.prepare("SELECT token, balance_micros FROM token_cache WHERE seller_key = ?");
299
- const row = stmt.get(sellerKey) as { token: string; balance_micros: number } | undefined;
324
+ const stmt = this.db.prepare(
325
+ "SELECT token, balance_micros, reserved_micros, spent_micros, balance_source FROM token_cache WHERE seller_key = ?"
326
+ );
327
+ const row = stmt.get(sellerKey) as {
328
+ token: string;
329
+ balance_micros: number;
330
+ reserved_micros: number;
331
+ spent_micros: number;
332
+ balance_source: string | null;
333
+ } | undefined;
300
334
  if (!row) {
301
335
  return undefined;
302
336
  }
303
337
  return {
304
338
  token: row.token,
305
- balanceMicros: row.balance_micros
339
+ balanceMicros: row.balance_micros,
340
+ reservedMicros: row.reserved_micros,
341
+ spentMicros: row.spent_micros,
342
+ balanceSource: row.balance_source || undefined
306
343
  };
307
344
  }
308
345
 
309
346
  public saveToken(sellerKey: string, token: string, tokenClass: string, balanceMicros: number, expiresAt: string): void {
310
347
  const stmt = this.db.prepare(
311
348
  `INSERT OR REPLACE INTO token_cache (
312
- seller_key, token, token_class, balance_micros, expires_at, updated_at
313
- ) VALUES (?, ?, ?, ?, ?, ?)`
349
+ seller_key, token, token_class, balance_micros, reserved_micros,
350
+ spent_micros, balance_source, expires_at, updated_at
351
+ ) VALUES (?, ?, ?, ?, 0, 0, 'purchase_complete', ?, ?)`
314
352
  );
315
353
  stmt.run(sellerKey, token, tokenClass, balanceMicros, expiresAt, nowIso());
316
354
  }
317
355
 
318
- public deductBalance(sellerKey: string, amountMicros: number): void {
319
- const token = this.getToken(sellerKey);
356
+ public reconcileTokenBalance(input: TokenBalanceSnapshotInput): void {
357
+ const current = this.db.prepare(
358
+ "SELECT token, token_class, expires_at FROM token_cache WHERE seller_key = ?"
359
+ ).get(input.sellerKey) as {
360
+ token: string;
361
+ token_class: string;
362
+ expires_at: string;
363
+ } | undefined;
364
+ const token = input.token || current?.token;
365
+ const tokenClass = input.tokenClass || current?.token_class || "seller";
366
+ const expiresAt = input.expiresAt || current?.expires_at || new Date(Date.now() + 86400 * 1000).toISOString();
320
367
  if (!token) {
321
368
  return;
322
369
  }
323
- const newBalance = Math.max(0, token.balanceMicros - amountMicros);
324
- const stmt = this.db.prepare("UPDATE token_cache SET balance_micros = ?, updated_at = ? WHERE seller_key = ?");
325
- stmt.run(newBalance, nowIso(), sellerKey);
326
- logger.info("token.cache.debited", "token cache balance debited", {
327
- sellerKey,
328
- amountMicros,
329
- balanceMicros: newBalance
370
+ this.db.prepare(
371
+ `INSERT OR REPLACE INTO token_cache (
372
+ seller_key, token, token_class, balance_micros, reserved_micros,
373
+ spent_micros, balance_source, expires_at, updated_at
374
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
375
+ ).run(
376
+ input.sellerKey,
377
+ token,
378
+ tokenClass,
379
+ input.balanceMicros,
380
+ input.reservedMicros ?? 0,
381
+ input.spentMicros ?? 0,
382
+ input.balanceSource,
383
+ expiresAt,
384
+ nowIso()
385
+ );
386
+ logger.info("token.cache.reconciled", "token cache reconciled from seller balance snapshot", {
387
+ sellerKey: input.sellerKey,
388
+ balanceMicros: input.balanceMicros,
389
+ reservedMicros: input.reservedMicros ?? 0,
390
+ spentMicros: input.spentMicros ?? 0,
391
+ balanceSource: input.balanceSource
330
392
  });
331
393
  }
332
394
 
395
+ public markTokenStale(sellerKey: string): void {
396
+ this.db.prepare(
397
+ "UPDATE token_cache SET balance_micros = 0, balance_source = 'stale', updated_at = ? WHERE seller_key = ?"
398
+ ).run(nowIso(), sellerKey);
399
+ }
400
+
333
401
  public listPayments(): PaymentConfig[] {
334
402
  const rows = this.db.prepare(
335
403
  `SELECT method, enabled, is_default, config_json, updated_at
@@ -500,8 +568,10 @@ export class BuyerStore {
500
568
  this.db.prepare(
501
569
  `INSERT INTO inference_ledger (
502
570
  request_id, seller_key, model_id, endpoint, status, prompt_tokens,
503
- completion_tokens, billed_micros, prompt_hash, response_hash, created_at
504
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
571
+ completion_tokens, billed_micros, estimated_micros, settled_micros,
572
+ settled_usd_micros, price_version, balance_snapshot_micros, balance_source,
573
+ prompt_hash, response_hash, created_at
574
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
505
575
  ).run(
506
576
  input.requestId,
507
577
  input.sellerKey,
@@ -511,6 +581,12 @@ export class BuyerStore {
511
581
  input.promptTokens,
512
582
  input.completionTokens,
513
583
  input.billedMicros,
584
+ input.estimatedMicros ?? input.billedMicros,
585
+ input.settledMicros ?? null,
586
+ input.settledUsdMicros ?? null,
587
+ input.priceVersion || null,
588
+ input.balanceSnapshotMicros ?? null,
589
+ input.balanceSource || "unknown",
514
590
  safeHash(input.prompt) || null,
515
591
  safeHash(input.response) || null,
516
592
  nowIso()
@@ -520,7 +596,9 @@ export class BuyerStore {
520
596
  public listInferenceLedger(): SafeInferenceLedgerEntry[] {
521
597
  const rows = this.db.prepare(
522
598
  `SELECT request_id, seller_key, model_id, endpoint, status, prompt_tokens,
523
- completion_tokens, billed_micros, prompt_hash, response_hash, created_at
599
+ completion_tokens, billed_micros, estimated_micros, settled_micros,
600
+ settled_usd_micros, price_version, balance_snapshot_micros, balance_source,
601
+ prompt_hash, response_hash, created_at
524
602
  FROM inference_ledger
525
603
  ORDER BY id ASC`
526
604
  ).all() as Array<{
@@ -532,6 +610,12 @@ export class BuyerStore {
532
610
  prompt_tokens: number;
533
611
  completion_tokens: number;
534
612
  billed_micros: number;
613
+ estimated_micros: number | null;
614
+ settled_micros: number | null;
615
+ settled_usd_micros: number | null;
616
+ price_version: string | null;
617
+ balance_snapshot_micros: number | null;
618
+ balance_source: string | null;
535
619
  prompt_hash: string | null;
536
620
  response_hash: string | null;
537
621
  created_at: string;
@@ -546,6 +630,12 @@ export class BuyerStore {
546
630
  promptTokens: row.prompt_tokens,
547
631
  completionTokens: row.completion_tokens,
548
632
  billedMicros: row.billed_micros,
633
+ estimatedMicros: row.estimated_micros ?? undefined,
634
+ settledMicros: row.settled_micros ?? undefined,
635
+ settledUsdMicros: row.settled_usd_micros ?? undefined,
636
+ priceVersion: row.price_version || undefined,
637
+ balanceSnapshotMicros: row.balance_snapshot_micros ?? undefined,
638
+ balanceSource: row.balance_source || undefined,
549
639
  promptHash: row.prompt_hash || undefined,
550
640
  responseHash: row.response_hash || undefined,
551
641
  createdAt: row.created_at
@@ -563,6 +653,9 @@ export class BuyerStore {
563
653
  token TEXT NOT NULL,
564
654
  token_class TEXT NOT NULL,
565
655
  balance_micros INTEGER NOT NULL,
656
+ reserved_micros INTEGER NOT NULL DEFAULT 0,
657
+ spent_micros INTEGER NOT NULL DEFAULT 0,
658
+ balance_source TEXT,
566
659
  expires_at TEXT NOT NULL,
567
660
  updated_at TEXT NOT NULL
568
661
  );
@@ -613,6 +706,12 @@ export class BuyerStore {
613
706
  prompt_tokens INTEGER NOT NULL,
614
707
  completion_tokens INTEGER NOT NULL,
615
708
  billed_micros INTEGER NOT NULL,
709
+ estimated_micros INTEGER,
710
+ settled_micros INTEGER,
711
+ settled_usd_micros INTEGER,
712
+ price_version TEXT,
713
+ balance_snapshot_micros INTEGER,
714
+ balance_source TEXT,
616
715
  prompt_hash TEXT,
617
716
  response_hash TEXT,
618
717
  created_at TEXT NOT NULL
@@ -639,6 +738,30 @@ export class BuyerStore {
639
738
  updated_at TEXT NOT NULL
640
739
  );
641
740
  `);
741
+ for (const [column, definition] of [
742
+ ["reserved_micros", "INTEGER NOT NULL DEFAULT 0"],
743
+ ["spent_micros", "INTEGER NOT NULL DEFAULT 0"],
744
+ ["balance_source", "TEXT"]
745
+ ]) {
746
+ this.ensureColumn("token_cache", column, definition);
747
+ }
748
+ for (const [column, definition] of [
749
+ ["estimated_micros", "INTEGER"],
750
+ ["settled_micros", "INTEGER"],
751
+ ["settled_usd_micros", "INTEGER"],
752
+ ["price_version", "TEXT"],
753
+ ["balance_snapshot_micros", "INTEGER"],
754
+ ["balance_source", "TEXT"]
755
+ ]) {
756
+ this.ensureColumn("inference_ledger", column, definition);
757
+ }
758
+ }
759
+
760
+ private ensureColumn(table: string, column: string, definition: string): void {
761
+ const rows = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
762
+ if (!rows.some((row) => row.name === column)) {
763
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
764
+ }
642
765
  }
643
766
 
644
767
  private countRows(tableName: string): number {