@tokenbuddy/tokenbuddy 1.0.6 → 1.0.8
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/dist/src/buyer-store.d.ts +28 -1
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +71 -16
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +17 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +201 -32
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +5 -0
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +279 -72
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
- package/dist/src/doctor-clawtip-wallet.js +54 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -0
- package/dist/src/doctor-diagnostics.d.ts +2 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +5 -0
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +48 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -0
- package/dist/src/init-clawtip-activation.js +395 -0
- package/dist/src/init-clawtip-activation.js.map +1 -0
- package/dist/src/init-payment-options.d.ts +23 -1
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +97 -22
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/terminal-image.d.ts +22 -0
- package/dist/src/terminal-image.d.ts.map +1 -0
- package/dist/src/terminal-image.js +135 -0
- package/dist/src/terminal-image.js.map +1 -0
- package/package.json +1 -1
- package/src/buyer-store.ts +140 -17
- package/src/cli.ts +251 -33
- package/src/daemon.ts +308 -53
- package/src/doctor-clawtip-wallet.ts +70 -0
- package/src/doctor-diagnostics.ts +11 -0
- package/src/init-clawtip-activation.ts +487 -0
- package/src/init-payment-options.ts +140 -22
- package/src/terminal-image.ts +187 -0
- package/tests/e2e.test.ts +79 -5
- package/tests/tokenbuddy.test.ts +745 -19
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
type TerminalEnv = Readonly<Record<string, string | undefined>>;
|
|
6
|
+
|
|
7
|
+
export type TerminalImageDisplayMethod =
|
|
8
|
+
| "inline-iterm"
|
|
9
|
+
| "inline-kitty"
|
|
10
|
+
| "system-open"
|
|
11
|
+
| "manual";
|
|
12
|
+
|
|
13
|
+
export interface TerminalImageDisplayResult {
|
|
14
|
+
method: TerminalImageDisplayMethod;
|
|
15
|
+
displayed: boolean;
|
|
16
|
+
message: string;
|
|
17
|
+
fallbackCommand?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DisplayTerminalImageOptions {
|
|
21
|
+
env?: TerminalEnv;
|
|
22
|
+
platform?: NodeJS.Platform;
|
|
23
|
+
stdoutIsTTY?: boolean;
|
|
24
|
+
write?: (chunk: string) => void;
|
|
25
|
+
runCommand?: (command: string, args: string[]) => Promise<void>;
|
|
26
|
+
fileExists?: (filePath: string) => boolean;
|
|
27
|
+
readFile?: (filePath: string) => Buffer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type InlineImageProtocol = "iterm" | "kitty";
|
|
31
|
+
|
|
32
|
+
function defaultRunCommand(command: string, args: string[]): Promise<void> {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn(command, args, {
|
|
35
|
+
stdio: "ignore",
|
|
36
|
+
});
|
|
37
|
+
child.on("error", reject);
|
|
38
|
+
child.on("close", (code) => {
|
|
39
|
+
if (code === 0) {
|
|
40
|
+
resolve();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
reject(new Error(`${command} exited with ${code}`));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function terminalSupportsInlineImages(
|
|
49
|
+
env: TerminalEnv,
|
|
50
|
+
stdoutIsTTY: boolean,
|
|
51
|
+
): InlineImageProtocol | undefined {
|
|
52
|
+
if (!stdoutIsTTY) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const term = env.TERM || "";
|
|
57
|
+
if (env.KITTY_WINDOW_ID || term.includes("xterm-kitty")) {
|
|
58
|
+
return "kitty";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const termProgram = env.TERM_PROGRAM || "";
|
|
62
|
+
if (termProgram === "iTerm.app" || termProgram === "WezTerm" || env.WEZTERM_EXECUTABLE) {
|
|
63
|
+
return "iterm";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function basenameBase64(filePath: string): string {
|
|
70
|
+
return Buffer.from(path.basename(filePath), "utf8").toString("base64");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function itermInlineImageSequence(filePath: string, image: Buffer): string {
|
|
74
|
+
const payload = image.toString("base64");
|
|
75
|
+
return `\u001B]1337;File=name=${basenameBase64(filePath)};inline=1;width=60%;preserveAspectRatio=1:${payload}\u0007\n`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function kittyInlineImageSequence(filePath: string): string {
|
|
79
|
+
const payload = Buffer.from(filePath, "utf8").toString("base64");
|
|
80
|
+
return `\u001B_Ga=T,t=f,f=100,c=60;${payload}\u001B\\\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function openCommandForPlatform(platform: NodeJS.Platform, filePath: string): { command: string; args: string[]; label: string } | undefined {
|
|
84
|
+
if (platform === "darwin") {
|
|
85
|
+
return {
|
|
86
|
+
command: "open",
|
|
87
|
+
args: [filePath],
|
|
88
|
+
label: `open ${filePath}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (platform === "linux") {
|
|
92
|
+
return {
|
|
93
|
+
command: "xdg-open",
|
|
94
|
+
args: [filePath],
|
|
95
|
+
label: `xdg-open ${filePath}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (platform === "win32") {
|
|
99
|
+
return {
|
|
100
|
+
command: "cmd",
|
|
101
|
+
args: ["/c", "start", "", filePath],
|
|
102
|
+
label: `start ${filePath}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function detectTerminalImageDisplay(
|
|
109
|
+
options: DisplayTerminalImageOptions = {},
|
|
110
|
+
): InlineImageProtocol | undefined {
|
|
111
|
+
return terminalSupportsInlineImages(
|
|
112
|
+
options.env || process.env,
|
|
113
|
+
options.stdoutIsTTY ?? Boolean(process.stdout.isTTY),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function displayTerminalImage(
|
|
118
|
+
filePath: string,
|
|
119
|
+
options: DisplayTerminalImageOptions = {},
|
|
120
|
+
): Promise<TerminalImageDisplayResult> {
|
|
121
|
+
const fileExists = options.fileExists || fs.existsSync;
|
|
122
|
+
const readFile = options.readFile || fs.readFileSync;
|
|
123
|
+
const write = options.write || ((chunk: string) => process.stdout.write(chunk));
|
|
124
|
+
const env = options.env || process.env;
|
|
125
|
+
const platform = options.platform || process.platform;
|
|
126
|
+
const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
127
|
+
const openCommand = openCommandForPlatform(platform, filePath);
|
|
128
|
+
const fallbackCommand = openCommand?.label;
|
|
129
|
+
|
|
130
|
+
if (!fileExists(filePath)) {
|
|
131
|
+
return {
|
|
132
|
+
method: "manual",
|
|
133
|
+
displayed: false,
|
|
134
|
+
fallbackCommand,
|
|
135
|
+
message: `QR image file is missing: ${filePath}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const inlineProtocol = terminalSupportsInlineImages(env, stdoutIsTTY);
|
|
140
|
+
if (inlineProtocol === "kitty") {
|
|
141
|
+
write(kittyInlineImageSequence(filePath));
|
|
142
|
+
return {
|
|
143
|
+
method: "inline-kitty",
|
|
144
|
+
displayed: true,
|
|
145
|
+
fallbackCommand,
|
|
146
|
+
message: "Displayed the ClawTip wallet QR image inline using the Kitty graphics protocol.",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (inlineProtocol === "iterm") {
|
|
151
|
+
write(itermInlineImageSequence(filePath, readFile(filePath)));
|
|
152
|
+
return {
|
|
153
|
+
method: "inline-iterm",
|
|
154
|
+
displayed: true,
|
|
155
|
+
fallbackCommand,
|
|
156
|
+
message: "Displayed the ClawTip wallet QR image inline using the iTerm2 image protocol.",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (openCommand) {
|
|
161
|
+
try {
|
|
162
|
+
const runCommand = options.runCommand || defaultRunCommand;
|
|
163
|
+
await runCommand(openCommand.command, openCommand.args);
|
|
164
|
+
return {
|
|
165
|
+
method: "system-open",
|
|
166
|
+
displayed: true,
|
|
167
|
+
fallbackCommand,
|
|
168
|
+
message: `Opened the ClawTip wallet QR image with the system image viewer: ${openCommand.label}`,
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
return {
|
|
173
|
+
method: "manual",
|
|
174
|
+
displayed: false,
|
|
175
|
+
fallbackCommand,
|
|
176
|
+
message: `Could not open QR image automatically: ${message}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
method: "manual",
|
|
183
|
+
displayed: false,
|
|
184
|
+
fallbackCommand,
|
|
185
|
+
message: "This terminal does not advertise inline image support and no system opener is available.",
|
|
186
|
+
};
|
|
187
|
+
}
|
package/tests/e2e.test.ts
CHANGED
|
@@ -243,6 +243,32 @@ describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
|
|
|
243
243
|
|
|
244
244
|
verifyDb.close();
|
|
245
245
|
|
|
246
|
+
const buyerAfterConcurrent = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
247
|
+
try {
|
|
248
|
+
const tokenCache = buyerAfterConcurrent.getToken("seller-e2e-node");
|
|
249
|
+
expect(tokenCache).toMatchObject({
|
|
250
|
+
balanceMicros: 1999450,
|
|
251
|
+
reservedMicros: 0,
|
|
252
|
+
spentMicros: 550,
|
|
253
|
+
balanceSource: "seller_settlement_summary"
|
|
254
|
+
});
|
|
255
|
+
const inferenceLedger = buyerAfterConcurrent.listInferenceLedger();
|
|
256
|
+
expect(inferenceLedger.filter((entry) => entry.requestId.startsWith("req_p_"))).toHaveLength(5);
|
|
257
|
+
expect(inferenceLedger).toEqual(expect.arrayContaining([
|
|
258
|
+
expect.objectContaining({
|
|
259
|
+
requestId: "req_p_1",
|
|
260
|
+
billedMicros: 110,
|
|
261
|
+
estimatedMicros: 200,
|
|
262
|
+
settledMicros: 110,
|
|
263
|
+
settledUsdMicros: 110,
|
|
264
|
+
priceVersion: "openrouter_usd.v1",
|
|
265
|
+
balanceSource: "seller_authoritative"
|
|
266
|
+
})
|
|
267
|
+
]));
|
|
268
|
+
} finally {
|
|
269
|
+
buyerAfterConcurrent.close();
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
// Step 7: Fire subsequent inference request and assert it HITS local daemon token cache immediately (Zero-Purchase)
|
|
247
273
|
// We capture time elapsed to verify cache speed
|
|
248
274
|
const start = Date.now();
|
|
@@ -260,14 +286,62 @@ describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
|
|
|
260
286
|
expect(purchaseCount).toBe(1);
|
|
261
287
|
verifyDb2.close();
|
|
262
288
|
|
|
263
|
-
// Step 8:
|
|
289
|
+
// Step 8: Force seller-authoritative insufficiency while buyer cache still looks funded.
|
|
290
|
+
// The daemon must refresh /v1/balance, auto-purchase once, and retry the same request idempotently.
|
|
291
|
+
const forcedDb = new DatabaseSync(TEMP_SELLER_DB);
|
|
292
|
+
const oldCredential = forcedDb.prepare("SELECT credential_id FROM credentials ORDER BY created_at ASC LIMIT 1").get() as any;
|
|
293
|
+
forcedDb.prepare("UPDATE credentials SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
|
|
294
|
+
forcedDb.prepare("UPDATE tokens SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
|
|
295
|
+
forcedDb.close();
|
|
296
|
+
const buyerBefore402 = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
297
|
+
try {
|
|
298
|
+
const cached = buyerBefore402.getToken("seller-e2e-node");
|
|
299
|
+
expect(cached?.balanceMicros).toBeGreaterThan(200000);
|
|
300
|
+
} finally {
|
|
301
|
+
buyerBefore402.close();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const retryResponse = await sendProxyRequest("req_402_rebuy_retry");
|
|
305
|
+
expect(retryResponse.ok).toBe(true);
|
|
306
|
+
const retryData = await retryResponse.json() as any;
|
|
307
|
+
expect(retryData.id).toBe("chatcmpl-e2e-ok");
|
|
308
|
+
|
|
309
|
+
const verifyDb3 = new DatabaseSync(TEMP_SELLER_DB);
|
|
310
|
+
const purchaseCountAfterRetry = (verifyDb3.prepare("SELECT COUNT(*) as count FROM purchases").get() as any).count;
|
|
311
|
+
const retryRows = verifyDb3.prepare("SELECT state, settled_micros FROM requests WHERE request_id = ?").all("req_402_rebuy_retry") as any[];
|
|
312
|
+
expect(purchaseCountAfterRetry).toBe(2);
|
|
313
|
+
expect(retryRows).toEqual([
|
|
314
|
+
expect.objectContaining({ state: "settled", settled_micros: 110 })
|
|
315
|
+
]);
|
|
316
|
+
verifyDb3.close();
|
|
317
|
+
|
|
318
|
+
const buyerAfterRetry = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
319
|
+
try {
|
|
320
|
+
expect(buyerAfterRetry.listInferenceLedger()).toEqual(expect.arrayContaining([
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
requestId: "req_402_rebuy_retry",
|
|
323
|
+
billedMicros: 110,
|
|
324
|
+
estimatedMicros: 200,
|
|
325
|
+
settledMicros: 110,
|
|
326
|
+
balanceSource: "seller_authoritative"
|
|
327
|
+
})
|
|
328
|
+
]));
|
|
329
|
+
expect(buyerAfterRetry.getToken("seller-e2e-node")).toMatchObject({
|
|
330
|
+
balanceMicros: 1999890,
|
|
331
|
+
balanceSource: "seller_settlement_summary"
|
|
332
|
+
});
|
|
333
|
+
} finally {
|
|
334
|
+
buyerAfterRetry.close();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Step 9: Verify newly implemented admin query billing APIs (purchases & requests)
|
|
264
338
|
const billingPurchases = await admin.get("/operator/admin/purchases");
|
|
265
|
-
expect(billingPurchases.purchases.length).toBe(
|
|
266
|
-
expect(billingPurchases.purchases
|
|
339
|
+
expect(billingPurchases.purchases.length).toBe(2);
|
|
340
|
+
expect(billingPurchases.purchases.every((purchase: any) => purchase.state === "funded")).toBe(true);
|
|
267
341
|
|
|
268
342
|
const billingRequests = await admin.get("/operator/admin/requests");
|
|
269
|
-
// 5 concurrent requests + 1
|
|
270
|
-
expect(billingRequests.requests.length).toBe(
|
|
343
|
+
// 5 concurrent requests + 1 cache hit + 1 retry-after-402 request = 7 settled inference requests.
|
|
344
|
+
expect(billingRequests.requests.length).toBe(7);
|
|
271
345
|
expect(billingRequests.requests.every((r: any) => r.state === "settled")).toBe(true);
|
|
272
346
|
});
|
|
273
347
|
});
|