@token2chat/t2c 0.2.0-beta.1 → 0.2.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.
- package/README.md +117 -144
- package/dist/cashu-store.d.ts +1 -1
- package/dist/cashu-store.js +4 -4
- package/dist/commands/audit.d.ts +65 -0
- package/dist/commands/audit.js +12 -12
- package/dist/commands/balance.js +2 -2
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/mint.js +14 -14
- package/dist/commands/monitor.d.ts +51 -0
- package/dist/commands/monitor.js +353 -0
- package/dist/commands/recover.js +4 -4
- package/dist/commands/setup.js +2 -2
- package/dist/commands/status.js +2 -3
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/cursor.js +44 -15
- package/dist/index.js +8 -1
- package/dist/proxy/auth.d.ts +20 -0
- package/dist/proxy/auth.js +28 -0
- package/dist/proxy/errors.d.ts +58 -0
- package/dist/proxy/errors.js +95 -0
- package/dist/proxy/gate-client.d.ts +34 -0
- package/dist/proxy/gate-client.js +81 -0
- package/dist/proxy/index.d.ts +10 -0
- package/dist/proxy/index.js +17 -0
- package/dist/proxy/payment-service.d.ts +65 -0
- package/dist/proxy/payment-service.js +101 -0
- package/dist/proxy/pricing.d.ts +37 -0
- package/dist/proxy/pricing.js +90 -0
- package/dist/proxy/response.d.ts +24 -0
- package/dist/proxy/response.js +48 -0
- package/dist/proxy/sse-parser.d.ts +19 -0
- package/dist/proxy/sse-parser.js +80 -0
- package/dist/proxy/types.d.ts +113 -0
- package/dist/proxy/types.js +74 -0
- package/dist/proxy.d.ts +2 -9
- package/dist/proxy.js +74 -186
- package/package.json +4 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Mint /stats response shape */
|
|
2
|
+
export interface MintStats {
|
|
3
|
+
totalMintedSats: number;
|
|
4
|
+
totalMeltedSats: number;
|
|
5
|
+
mintCount: number;
|
|
6
|
+
meltCount: number;
|
|
7
|
+
}
|
|
8
|
+
/** Gate /stats response summary shape */
|
|
9
|
+
export interface GateStatsSummary {
|
|
10
|
+
total_requests: number;
|
|
11
|
+
success_count: number;
|
|
12
|
+
error_count: number;
|
|
13
|
+
ecash_received: number;
|
|
14
|
+
model_breakdown: Record<string, {
|
|
15
|
+
count: number;
|
|
16
|
+
ecash_in: number;
|
|
17
|
+
errors: number;
|
|
18
|
+
}>;
|
|
19
|
+
error_breakdown: Record<string, number>;
|
|
20
|
+
}
|
|
21
|
+
export interface GateStats {
|
|
22
|
+
generated_at: string;
|
|
23
|
+
today: GateStatsSummary;
|
|
24
|
+
last_7_days: GateStatsSummary;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fetch Gate statistics from the /stats endpoint.
|
|
28
|
+
* Returns null on error (network, parse, etc).
|
|
29
|
+
*/
|
|
30
|
+
export declare function fetchGateStats(gateUrl: string): Promise<GateStats | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Fetch Mint statistics from the /stats endpoint.
|
|
33
|
+
* Returns null on error (network, parse, etc).
|
|
34
|
+
*/
|
|
35
|
+
export declare function fetchMintStats(mintUrl: string): Promise<MintStats | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Format sats with thousands separators.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatSats(sats: number): string;
|
|
40
|
+
/**
|
|
41
|
+
* Build proxy panel content from transaction history.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildProxyContent(maxLines: number, maxWidth: number): Promise<string>;
|
|
44
|
+
export interface MonitorOptions {
|
|
45
|
+
refresh?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create and run the TUI monitor dashboard.
|
|
49
|
+
* Layout: 2x2 grid with Gate, Mint, Proxy, Funds panels.
|
|
50
|
+
*/
|
|
51
|
+
export declare function monitorCommand(opts: MonitorOptions): Promise<void>;
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* t2c monitor — Live TUI dashboard
|
|
3
|
+
*
|
|
4
|
+
* Real-time monitoring of Gate, Mint, Proxy, and Funds.
|
|
5
|
+
* Uses blessed-contrib for terminal UI.
|
|
6
|
+
*/
|
|
7
|
+
import blessed from "blessed";
|
|
8
|
+
import contrib from "blessed-contrib";
|
|
9
|
+
import { loadConfig, resolveHome, loadTransactions, formatUnits } from "../config.js";
|
|
10
|
+
import { CashuStore } from "../cashu-store.js";
|
|
11
|
+
/**
|
|
12
|
+
* Fetch Gate statistics from the /stats endpoint.
|
|
13
|
+
* Returns null on error (network, parse, etc).
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchGateStats(gateUrl) {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`${gateUrl}/stats`, { signal: AbortSignal.timeout(5000) });
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
return null;
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fetch Mint statistics from the /stats endpoint.
|
|
29
|
+
* Returns null on error (network, parse, etc).
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchMintStats(mintUrl) {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${mintUrl}/stats`, { signal: AbortSignal.timeout(5000) });
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
return null;
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format sats with thousands separators.
|
|
45
|
+
*/
|
|
46
|
+
export function formatSats(sats) {
|
|
47
|
+
return sats.toLocaleString("en-US");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Format a transaction record for TUI display.
|
|
51
|
+
*/
|
|
52
|
+
function formatTransaction(tx, maxWidth) {
|
|
53
|
+
const time = new Date(tx.timestamp).toLocaleTimeString("en-US", {
|
|
54
|
+
hour: "2-digit",
|
|
55
|
+
minute: "2-digit",
|
|
56
|
+
second: "2-digit",
|
|
57
|
+
hour12: false,
|
|
58
|
+
});
|
|
59
|
+
// Status indicator with color
|
|
60
|
+
const statusIcon = tx.error
|
|
61
|
+
? "{red-fg}✗{/red-fg}"
|
|
62
|
+
: tx.gateStatus === 200
|
|
63
|
+
? "{green-fg}✓{/green-fg}"
|
|
64
|
+
: `{yellow-fg}${tx.gateStatus}{/yellow-fg}`;
|
|
65
|
+
// Cost display
|
|
66
|
+
const cost = formatUnits(tx.priceSat).padStart(8);
|
|
67
|
+
// Truncate model name to fit available space
|
|
68
|
+
const modelMaxLen = Math.max(10, maxWidth - 26);
|
|
69
|
+
const model = tx.model.length > modelMaxLen
|
|
70
|
+
? tx.model.slice(0, modelMaxLen - 2) + ".."
|
|
71
|
+
: tx.model;
|
|
72
|
+
return ` ${time} ${statusIcon} ${cost} ${model}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build proxy panel content from transaction history.
|
|
76
|
+
*/
|
|
77
|
+
export async function buildProxyContent(maxLines, maxWidth) {
|
|
78
|
+
const transactions = await loadTransactions();
|
|
79
|
+
if (transactions.length === 0) {
|
|
80
|
+
return "{center}No transactions yet{/center}\n\n" +
|
|
81
|
+
" Run requests through the proxy\n" +
|
|
82
|
+
" to see activity here.";
|
|
83
|
+
}
|
|
84
|
+
// Show most recent first
|
|
85
|
+
const recent = transactions.slice().reverse();
|
|
86
|
+
// Calculate summary stats
|
|
87
|
+
const totalSpent = transactions.reduce((s, t) => s + t.priceSat, 0);
|
|
88
|
+
const totalChange = transactions.reduce((s, t) => s + t.changeSat, 0);
|
|
89
|
+
const errorCount = transactions.filter((t) => t.error).length;
|
|
90
|
+
const netCost = totalSpent - totalChange;
|
|
91
|
+
const lines = [
|
|
92
|
+
` Requests: ${transactions.length} | Spent: ${formatUnits(netCost)}`,
|
|
93
|
+
errorCount > 0
|
|
94
|
+
? ` {red-fg}Errors: ${errorCount}{/red-fg}`
|
|
95
|
+
: ` {green-fg}All OK{/green-fg}`,
|
|
96
|
+
" " + "─".repeat(Math.min(36, maxWidth - 4)),
|
|
97
|
+
];
|
|
98
|
+
// Add recent transactions (leave room for header)
|
|
99
|
+
const displayCount = Math.min(recent.length, Math.max(1, maxLines - 5));
|
|
100
|
+
for (let i = 0; i < displayCount; i++) {
|
|
101
|
+
lines.push(formatTransaction(recent[i], maxWidth));
|
|
102
|
+
}
|
|
103
|
+
if (recent.length > displayCount) {
|
|
104
|
+
lines.push(` ... +${recent.length - displayCount} more`);
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
/** Low balance threshold for warning highlight */
|
|
109
|
+
const LOW_BALANCE_THRESHOLD = 500;
|
|
110
|
+
/** Default refresh interval in milliseconds */
|
|
111
|
+
const DEFAULT_REFRESH_MS = 5000;
|
|
112
|
+
/**
|
|
113
|
+
* Create and run the TUI monitor dashboard.
|
|
114
|
+
* Layout: 2x2 grid with Gate, Mint, Proxy, Funds panels.
|
|
115
|
+
*/
|
|
116
|
+
export async function monitorCommand(opts) {
|
|
117
|
+
// Parse refresh interval
|
|
118
|
+
const refreshMs = opts.refresh
|
|
119
|
+
? parseInt(opts.refresh, 10) * 1000
|
|
120
|
+
: DEFAULT_REFRESH_MS;
|
|
121
|
+
// Create the main screen
|
|
122
|
+
const screen = blessed.screen({
|
|
123
|
+
smartCSR: true,
|
|
124
|
+
title: "t2c monitor",
|
|
125
|
+
fullUnicode: true,
|
|
126
|
+
});
|
|
127
|
+
// Create a 2x2 grid layout
|
|
128
|
+
const grid = new contrib.grid({
|
|
129
|
+
rows: 2,
|
|
130
|
+
cols: 2,
|
|
131
|
+
screen: screen,
|
|
132
|
+
});
|
|
133
|
+
// ── Gate Panel (top-left) ──
|
|
134
|
+
const gateBox = grid.set(0, 0, 1, 1, blessed.box, {
|
|
135
|
+
label: " Gate ",
|
|
136
|
+
border: { type: "line" },
|
|
137
|
+
style: {
|
|
138
|
+
border: { fg: "cyan" },
|
|
139
|
+
label: { fg: "cyan", bold: true },
|
|
140
|
+
},
|
|
141
|
+
content: "{center}Loading...{/center}",
|
|
142
|
+
tags: true,
|
|
143
|
+
});
|
|
144
|
+
// ── Mint Panel (top-right) ──
|
|
145
|
+
const mintBox = grid.set(0, 1, 1, 1, blessed.box, {
|
|
146
|
+
label: " Mint ",
|
|
147
|
+
border: { type: "line" },
|
|
148
|
+
style: {
|
|
149
|
+
border: { fg: "green" },
|
|
150
|
+
label: { fg: "green", bold: true },
|
|
151
|
+
},
|
|
152
|
+
content: "{center}Loading...{/center}",
|
|
153
|
+
tags: true,
|
|
154
|
+
});
|
|
155
|
+
// ── Proxy Panel (bottom-left) ──
|
|
156
|
+
const proxyBox = grid.set(1, 0, 1, 1, blessed.box, {
|
|
157
|
+
label: " Proxy ",
|
|
158
|
+
border: { type: "line" },
|
|
159
|
+
style: {
|
|
160
|
+
border: { fg: "yellow" },
|
|
161
|
+
label: { fg: "yellow", bold: true },
|
|
162
|
+
},
|
|
163
|
+
content: "{center}Loading...{/center}",
|
|
164
|
+
tags: true,
|
|
165
|
+
});
|
|
166
|
+
// ── Funds Panel (bottom-right) ──
|
|
167
|
+
const fundsBox = grid.set(1, 1, 1, 1, blessed.box, {
|
|
168
|
+
label: " Funds ",
|
|
169
|
+
border: { type: "line" },
|
|
170
|
+
style: {
|
|
171
|
+
border: { fg: "magenta" },
|
|
172
|
+
label: { fg: "magenta", bold: true },
|
|
173
|
+
},
|
|
174
|
+
content: "{center}Loading...{/center}",
|
|
175
|
+
tags: true,
|
|
176
|
+
});
|
|
177
|
+
// Status bar at the bottom
|
|
178
|
+
const statusBar = blessed.box({
|
|
179
|
+
parent: screen,
|
|
180
|
+
bottom: 0,
|
|
181
|
+
left: 0,
|
|
182
|
+
width: "100%",
|
|
183
|
+
height: 1,
|
|
184
|
+
style: {
|
|
185
|
+
bg: "blue",
|
|
186
|
+
fg: "white",
|
|
187
|
+
},
|
|
188
|
+
content: ` t2c monitor | q: quit | r: refresh | interval: ${refreshMs / 1000}s `,
|
|
189
|
+
tags: true,
|
|
190
|
+
});
|
|
191
|
+
// Reference to refresh timer for cleanup
|
|
192
|
+
let refreshTimer = null;
|
|
193
|
+
// Key bindings for clean exit
|
|
194
|
+
screen.key(["escape", "q", "C-c"], () => {
|
|
195
|
+
cleanup();
|
|
196
|
+
});
|
|
197
|
+
// Refresh key
|
|
198
|
+
screen.key(["r"], () => {
|
|
199
|
+
updatePanels();
|
|
200
|
+
});
|
|
201
|
+
// Cleanup function
|
|
202
|
+
function cleanup() {
|
|
203
|
+
if (refreshTimer) {
|
|
204
|
+
clearInterval(refreshTimer);
|
|
205
|
+
refreshTimer = null;
|
|
206
|
+
}
|
|
207
|
+
screen.destroy();
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
// Handle process signals for clean exit
|
|
211
|
+
process.on("SIGINT", cleanup);
|
|
212
|
+
process.on("SIGTERM", cleanup);
|
|
213
|
+
// Panel update function
|
|
214
|
+
async function updatePanels() {
|
|
215
|
+
const now = new Date().toLocaleTimeString();
|
|
216
|
+
// Calculate available lines for proxy panel (rough estimate)
|
|
217
|
+
const panelHeight = Math.floor(screen.height / 2) - 3;
|
|
218
|
+
const panelWidth = Math.floor(screen.width / 2) - 4;
|
|
219
|
+
// Load config for panel updates
|
|
220
|
+
const config = await loadConfig();
|
|
221
|
+
// Update Gate panel with stats
|
|
222
|
+
await updateGatePanel(gateBox, config.gateUrl, now);
|
|
223
|
+
// Update Mint panel with stats
|
|
224
|
+
const mintStats = await fetchMintStats(config.mintUrl);
|
|
225
|
+
if (mintStats) {
|
|
226
|
+
const netFlow = mintStats.totalMintedSats - mintStats.totalMeltedSats;
|
|
227
|
+
const netFlowColor = netFlow >= 0 ? "green-fg" : "red-fg";
|
|
228
|
+
const netFlowSign = netFlow >= 0 ? "+" : "";
|
|
229
|
+
mintBox.setContent(`{center}Mint Statistics{/center}\n\n` +
|
|
230
|
+
` {bold}Minted:{/bold} ${formatUnits(mintStats.totalMintedSats)} (${mintStats.mintCount} ops)\n` +
|
|
231
|
+
` {bold}Melted:{/bold} ${formatUnits(mintStats.totalMeltedSats)} (${mintStats.meltCount} ops)\n` +
|
|
232
|
+
` {bold}Net:{/bold} {${netFlowColor}}${netFlowSign}${formatUnits(netFlow)}{/${netFlowColor}}\n\n` +
|
|
233
|
+
`{right}Updated: ${now}{/right}`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
mintBox.setContent(`{center}Mint Statistics{/center}\n\n` +
|
|
237
|
+
` {red-fg}●{/red-fg} Unable to fetch stats\n` +
|
|
238
|
+
` URL: ${config.mintUrl}\n\n` +
|
|
239
|
+
`{right}Updated: ${now}{/right}`);
|
|
240
|
+
}
|
|
241
|
+
// Update Proxy panel with real transaction data
|
|
242
|
+
try {
|
|
243
|
+
const proxyContent = await buildProxyContent(panelHeight, panelWidth);
|
|
244
|
+
proxyBox.setContent(proxyContent + `\n\n{right}${now}{/right}`);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
proxyBox.setContent(`{center}Proxy Events{/center}\n\n` +
|
|
248
|
+
` {red-fg}Error loading transactions{/red-fg}\n` +
|
|
249
|
+
` ${err instanceof Error ? err.message : String(err)}\n\n` +
|
|
250
|
+
`{right}${now}{/right}`);
|
|
251
|
+
}
|
|
252
|
+
// Update Funds panel with wallet balance and fund flow statistics
|
|
253
|
+
await updateFundsPanel(fundsBox, now);
|
|
254
|
+
screen.render();
|
|
255
|
+
}
|
|
256
|
+
/** Update the Funds panel with wallet balance and fund flow statistics */
|
|
257
|
+
async function updateFundsPanel(box, now) {
|
|
258
|
+
try {
|
|
259
|
+
const config = await loadConfig();
|
|
260
|
+
const walletPath = resolveHome(config.walletPath);
|
|
261
|
+
// Load wallet and transactions in parallel
|
|
262
|
+
const [store, transactions] = await Promise.all([
|
|
263
|
+
CashuStore.load(walletPath, config.mintUrl),
|
|
264
|
+
loadTransactions(),
|
|
265
|
+
]);
|
|
266
|
+
const balance = store.balance;
|
|
267
|
+
const proofCount = store.proofCount;
|
|
268
|
+
// Calculate fund flow statistics from transactions
|
|
269
|
+
const totalSpent = transactions.reduce((s, t) => s + t.priceSat, 0);
|
|
270
|
+
const totalChange = transactions.reduce((s, t) => s + t.changeSat, 0);
|
|
271
|
+
const totalRefund = transactions.reduce((s, t) => s + t.refundSat, 0);
|
|
272
|
+
const netCost = totalSpent - totalChange - totalRefund;
|
|
273
|
+
// Format balance with low balance warning
|
|
274
|
+
const balanceColor = balance < LOW_BALANCE_THRESHOLD ? "red" : "green";
|
|
275
|
+
const balanceWarning = balance < LOW_BALANCE_THRESHOLD ? " {red-fg}⚠ LOW{/red-fg}" : "";
|
|
276
|
+
const balanceStr = `{${balanceColor}-fg}${formatUnits(balance)}{/${balanceColor}-fg}${balanceWarning}`;
|
|
277
|
+
box.setContent(`{center}Wallet Funds{/center}\n\n` +
|
|
278
|
+
` Balance: ${balanceStr}\n` +
|
|
279
|
+
` Proofs: ${proofCount}\n\n` +
|
|
280
|
+
` {bold}Fund Flow{/bold}\n` +
|
|
281
|
+
` Spent: ${formatUnits(totalSpent)}\n` +
|
|
282
|
+
` Change: +${formatUnits(totalChange)}\n` +
|
|
283
|
+
` Refund: +${formatUnits(totalRefund)}\n` +
|
|
284
|
+
` Net: ${formatUnits(netCost)}\n\n` +
|
|
285
|
+
`{right}Updated: ${now}{/right}`);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
box.setContent(`{center}Wallet Funds{/center}\n\n` +
|
|
289
|
+
` {red-fg}Error loading wallet{/red-fg}\n` +
|
|
290
|
+
` ${err instanceof Error ? err.message : String(err)}\n\n` +
|
|
291
|
+
`{right}Updated: ${now}{/right}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Update the Gate panel with stats from /stats endpoint */
|
|
295
|
+
async function updateGatePanel(box, gateUrl, now) {
|
|
296
|
+
const stats = await fetchGateStats(gateUrl);
|
|
297
|
+
if (!stats) {
|
|
298
|
+
box.setContent(`{center}Gate Statistics{/center}\n\n` +
|
|
299
|
+
` Status: {red-fg}●{/red-fg} Unreachable\n` +
|
|
300
|
+
` URL: ${gateUrl}\n\n` +
|
|
301
|
+
`{right}Updated: ${now}{/right}`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const today = stats.today;
|
|
305
|
+
const week = stats.last_7_days;
|
|
306
|
+
// Format error count with red highlight if > 0
|
|
307
|
+
const todayErrors = today.error_count > 0
|
|
308
|
+
? `{red-fg}${today.error_count}{/red-fg}`
|
|
309
|
+
: `${today.error_count}`;
|
|
310
|
+
const weekErrors = week.error_count > 0
|
|
311
|
+
? `{red-fg}${week.error_count}{/red-fg}`
|
|
312
|
+
: `${week.error_count}`;
|
|
313
|
+
// Calculate success rate
|
|
314
|
+
const todayRate = today.total_requests > 0
|
|
315
|
+
? ((today.success_count / today.total_requests) * 100).toFixed(1)
|
|
316
|
+
: "100.0";
|
|
317
|
+
const weekRate = week.total_requests > 0
|
|
318
|
+
? ((week.success_count / week.total_requests) * 100).toFixed(1)
|
|
319
|
+
: "100.0";
|
|
320
|
+
// Build content
|
|
321
|
+
let content = `{center}Gate Statistics{/center}\n\n`;
|
|
322
|
+
content += ` {bold}Today{/bold}\n`;
|
|
323
|
+
content += ` Total: ${formatSats(today.total_requests)} `;
|
|
324
|
+
content += `OK: ${formatSats(today.success_count)} `;
|
|
325
|
+
content += `Err: ${todayErrors}\n`;
|
|
326
|
+
content += ` Rate: ${todayRate}% `;
|
|
327
|
+
content += `Ecash: ${formatUnits(today.ecash_received)}\n\n`;
|
|
328
|
+
content += ` {bold}Last 7 Days{/bold}\n`;
|
|
329
|
+
content += ` Total: ${formatSats(week.total_requests)} `;
|
|
330
|
+
content += `OK: ${formatSats(week.success_count)} `;
|
|
331
|
+
content += `Err: ${weekErrors}\n`;
|
|
332
|
+
content += ` Rate: ${weekRate}% `;
|
|
333
|
+
content += `Ecash: ${formatUnits(week.ecash_received)}\n`;
|
|
334
|
+
// Show error breakdown if any errors today
|
|
335
|
+
if (today.error_count > 0 && Object.keys(today.error_breakdown).length > 0) {
|
|
336
|
+
content += `\n {bold}{red-fg}Errors Today{/red-fg}{/bold}\n`;
|
|
337
|
+
for (const [code, count] of Object.entries(today.error_breakdown)) {
|
|
338
|
+
content += ` {red-fg}• ${code}: ${count}{/red-fg}\n`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
content += `\n{right}Updated: ${now}{/right}`;
|
|
342
|
+
box.setContent(content);
|
|
343
|
+
}
|
|
344
|
+
// Initial render
|
|
345
|
+
await updatePanels();
|
|
346
|
+
screen.render();
|
|
347
|
+
// Set up auto-refresh
|
|
348
|
+
refreshTimer = setInterval(() => {
|
|
349
|
+
updatePanels().catch(() => {
|
|
350
|
+
// Ignore refresh errors, will retry next interval
|
|
351
|
+
});
|
|
352
|
+
}, refreshMs);
|
|
353
|
+
}
|
package/dist/commands/recover.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* t2c recover - Recover failed tokens
|
|
3
3
|
*/
|
|
4
|
-
import { loadConfig, resolveHome, FAILED_TOKENS_PATH, loadFailedTokens, saveFailedTokens, } from "../config.js";
|
|
4
|
+
import { loadConfig, resolveHome, FAILED_TOKENS_PATH, loadFailedTokens, saveFailedTokens, formatUnits, } from "../config.js";
|
|
5
5
|
import { CashuStore } from "../cashu-store.js";
|
|
6
6
|
export async function recoverCommand() {
|
|
7
7
|
const config = await loadConfig();
|
|
@@ -30,7 +30,7 @@ export async function recoverCommand() {
|
|
|
30
30
|
console.log(`Attempting to recover ${ft.type} token from ${date}...`);
|
|
31
31
|
try {
|
|
32
32
|
const amount = await wallet.receiveToken(ft.token);
|
|
33
|
-
console.log(` ✅ Recovered ${amount}
|
|
33
|
+
console.log(` ✅ Recovered ${formatUnits(amount)}\n`);
|
|
34
34
|
recoveredTotal += amount;
|
|
35
35
|
}
|
|
36
36
|
catch (e) {
|
|
@@ -47,8 +47,8 @@ export async function recoverCommand() {
|
|
|
47
47
|
await saveFailedTokens({ tokens: stillFailed });
|
|
48
48
|
console.log("─".repeat(40));
|
|
49
49
|
if (recoveredTotal > 0) {
|
|
50
|
-
console.log(`\n🎉 Recovered total: ${recoveredTotal
|
|
51
|
-
console.log(`New wallet balance: ${wallet.balance
|
|
50
|
+
console.log(`\n🎉 Recovered total: ${formatUnits(recoveredTotal)}`);
|
|
51
|
+
console.log(`New wallet balance: ${formatUnits(wallet.balance)}\n`);
|
|
52
52
|
}
|
|
53
53
|
if (stillFailed.length > 0) {
|
|
54
54
|
console.log(`\n⚠️ ${stillFailed.length} token(s) still failed.`);
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* t2c setup - Interactive setup wizard
|
|
3
3
|
*/
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
|
-
import { loadConfig, saveConfig, configExists, resolveHome, checkGateHealth, checkMintHealth, DEFAULT_CONFIG, } from "../config.js";
|
|
5
|
+
import { loadConfig, saveConfig, configExists, resolveHome, checkGateHealth, checkMintHealth, DEFAULT_CONFIG, formatUnits, } from "../config.js";
|
|
6
6
|
import { CashuStore } from "../cashu-store.js";
|
|
7
7
|
function createPrompt() {
|
|
8
8
|
return readline.createInterface({
|
|
@@ -106,7 +106,7 @@ export async function setupCommand() {
|
|
|
106
106
|
try {
|
|
107
107
|
const resolvedPath = resolveHome(walletPath);
|
|
108
108
|
const wallet = await CashuStore.load(resolvedPath, mintUrl);
|
|
109
|
-
console.log(`✅ Wallet initialized (balance: ${wallet.balance}
|
|
109
|
+
console.log(`✅ Wallet initialized (balance: ${formatUnits(wallet.balance)})`);
|
|
110
110
|
}
|
|
111
111
|
catch (e) {
|
|
112
112
|
console.log(`⚠️ Failed to initialize wallet: ${e}`);
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* t2c status - Show service status and wallet balance
|
|
3
3
|
*/
|
|
4
|
-
import { loadConfig, resolveHome, configExists, checkGateHealth } from "../config.js";
|
|
4
|
+
import { loadConfig, resolveHome, configExists, checkGateHealth, formatUnits } from "../config.js";
|
|
5
5
|
import { CashuStore } from "../cashu-store.js";
|
|
6
6
|
async function checkProxy(port) {
|
|
7
7
|
try {
|
|
@@ -70,9 +70,8 @@ export async function statusCommand(opts) {
|
|
|
70
70
|
// Wallet
|
|
71
71
|
console.log("");
|
|
72
72
|
if (walletInfo) {
|
|
73
|
-
const balanceStr = walletInfo.balance.toLocaleString();
|
|
74
73
|
const status = walletInfo.balance > 0 ? "✅" : "⚠️";
|
|
75
|
-
console.log(`Wallet: ${status} ${
|
|
74
|
+
console.log(`Wallet: ${status} ${formatUnits(walletInfo.balance)} (${walletInfo.proofs} proofs)`);
|
|
76
75
|
if (walletInfo.balance === 0) {
|
|
77
76
|
console.log(` Run 't2c mint' to add funds`);
|
|
78
77
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare function configExists(): Promise<boolean>;
|
|
|
30
30
|
/**
|
|
31
31
|
* Custom error classes for better error handling
|
|
32
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* Format units (1 unit = $0.00001) as USD string.
|
|
35
|
+
* 100000 units = $1.00
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatUnits(units: number): string;
|
|
33
38
|
export declare class ConfigError extends Error {
|
|
34
39
|
readonly recoverable: boolean;
|
|
35
40
|
constructor(message: string, recoverable?: boolean);
|
package/dist/config.js
CHANGED
|
@@ -128,6 +128,23 @@ export async function configExists() {
|
|
|
128
128
|
/**
|
|
129
129
|
* Custom error classes for better error handling
|
|
130
130
|
*/
|
|
131
|
+
/**
|
|
132
|
+
* Format units (1 unit = $0.00001) as USD string.
|
|
133
|
+
* 100000 units = $1.00
|
|
134
|
+
*/
|
|
135
|
+
export function formatUnits(units) {
|
|
136
|
+
const dollars = units / 100000;
|
|
137
|
+
if (dollars >= 1 || dollars === 0) {
|
|
138
|
+
return "$" + dollars.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
139
|
+
}
|
|
140
|
+
// For sub-dollar: show enough decimal places to be meaningful
|
|
141
|
+
const str = dollars.toFixed(5).replace(/0+$/, "");
|
|
142
|
+
// Ensure at least 2 decimal places
|
|
143
|
+
const parts = str.split(".");
|
|
144
|
+
const decimals = parts[1] || "";
|
|
145
|
+
const padded = decimals.length < 2 ? decimals.padEnd(2, "0") : decimals;
|
|
146
|
+
return "$" + parts[0] + "." + padded;
|
|
147
|
+
}
|
|
131
148
|
export class ConfigError extends Error {
|
|
132
149
|
recoverable;
|
|
133
150
|
constructor(message, recoverable = false) {
|
|
@@ -1,28 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Connector
|
|
3
|
+
*
|
|
4
|
+
* Detects Cursor IDE installation and provides configuration instructions.
|
|
5
|
+
*/
|
|
6
|
+
import { accessSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { homedir, platform } from "node:os";
|
|
9
|
+
function detectCursorInstallation() {
|
|
10
|
+
const home = homedir();
|
|
11
|
+
const plat = platform();
|
|
12
|
+
const candidates = [];
|
|
13
|
+
if (plat === "darwin") {
|
|
14
|
+
candidates.push(join(home, "Library", "Application Support", "Cursor"));
|
|
15
|
+
}
|
|
16
|
+
else if (plat === "win32") {
|
|
17
|
+
const appData = process.env.APPDATA;
|
|
18
|
+
if (appData)
|
|
19
|
+
candidates.push(join(appData, "Cursor"));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Linux and others
|
|
23
|
+
candidates.push(join(home, ".config", "Cursor"));
|
|
24
|
+
}
|
|
25
|
+
for (const p of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
accessSync(p);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// not found
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
1
36
|
export const cursorConnector = {
|
|
2
37
|
id: "cursor",
|
|
3
38
|
name: "Cursor",
|
|
4
|
-
description: "Configure Cursor IDE
|
|
39
|
+
description: "Configure Cursor IDE for Token2Chat",
|
|
5
40
|
async detect() {
|
|
6
|
-
|
|
7
|
-
// Could check for:
|
|
8
|
-
// - ~/Library/Application Support/Cursor (macOS)
|
|
9
|
-
// - ~/.config/Cursor (Linux)
|
|
10
|
-
// - %APPDATA%\Cursor (Windows)
|
|
11
|
-
return false;
|
|
41
|
+
return detectCursorInstallation();
|
|
12
42
|
},
|
|
13
43
|
async connect(_config) {
|
|
14
|
-
console.log("\n
|
|
15
|
-
console.log(" Cursor
|
|
16
|
-
console.log(" In the meantime, you can configure Cursor manually:\n");
|
|
44
|
+
console.log("\n Cursor Integration\n");
|
|
45
|
+
console.log(" Configure Cursor manually:\n");
|
|
17
46
|
console.log(" 1. Open Cursor Settings (Cmd/Ctrl + ,)");
|
|
18
|
-
console.log(" 2.
|
|
47
|
+
console.log(" 2. Search for 'OpenAI Base URL' and set:");
|
|
19
48
|
console.log(` http://127.0.0.1:${_config.proxyPort}/v1\n`);
|
|
20
|
-
console.log(" 3. Set OpenAI API Key:");
|
|
21
|
-
console.log("
|
|
22
|
-
console.log(" 4. Use model
|
|
49
|
+
console.log(" 3. Set 'OpenAI API Key' to your proxy secret:");
|
|
50
|
+
console.log(" (find it with: t2c status --json)\n");
|
|
51
|
+
console.log(" 4. Use any model from the Gate, e.g.:");
|
|
23
52
|
console.log(" anthropic/claude-sonnet-4\n");
|
|
24
53
|
},
|
|
25
54
|
async verify() {
|
|
26
|
-
return
|
|
55
|
+
return detectCursorInstallation();
|
|
27
56
|
},
|
|
28
57
|
};
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { recoverCommand } from "./commands/recover.js";
|
|
|
16
16
|
import { doctorCommand } from "./commands/doctor.js";
|
|
17
17
|
import { balanceCommand } from "./commands/balance.js";
|
|
18
18
|
import { auditCommand } from "./commands/audit.js";
|
|
19
|
+
import { monitorCommand } from "./commands/monitor.js";
|
|
19
20
|
// debug command is loaded dynamically — excluded from npm package
|
|
20
21
|
const program = new Command();
|
|
21
22
|
program
|
|
@@ -108,6 +109,12 @@ program
|
|
|
108
109
|
.option("--json", "Output as JSON")
|
|
109
110
|
.option("-n, --lines <n>", "Number of recent transactions to show", "20")
|
|
110
111
|
.action(auditCommand);
|
|
112
|
+
// t2c monitor - Live TUI dashboard
|
|
113
|
+
program
|
|
114
|
+
.command("monitor")
|
|
115
|
+
.description("Live TUI dashboard for Gate, Mint, Proxy, and Funds")
|
|
116
|
+
.option("-r, --refresh <seconds>", "Refresh interval in seconds", "5")
|
|
117
|
+
.action(monitorCommand);
|
|
111
118
|
// t2c config - Generate config for AI tools
|
|
112
119
|
const config = program
|
|
113
120
|
.command("config")
|
|
@@ -168,7 +175,7 @@ try {
|
|
|
168
175
|
debug
|
|
169
176
|
.command("topup")
|
|
170
177
|
.description("Transfer ecash from Gate to local plugin wallet")
|
|
171
|
-
.requiredOption("--amount <
|
|
178
|
+
.requiredOption("--amount <units>", "Amount in units to withdraw from Gate")
|
|
172
179
|
.action((opts) => debugCommand("topup", opts));
|
|
173
180
|
}
|
|
174
181
|
catch {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-like object with authorization header.
|
|
3
|
+
*/
|
|
4
|
+
export interface AuthRequest {
|
|
5
|
+
headers: {
|
|
6
|
+
authorization?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Function that checks if a request is authenticated.
|
|
11
|
+
*/
|
|
12
|
+
export type AuthChecker = (req: AuthRequest) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Create an auth checker function for Bearer token authentication.
|
|
15
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
16
|
+
*
|
|
17
|
+
* @param secret - The expected Bearer token value
|
|
18
|
+
* @returns A function that returns true if the request has a valid Bearer token
|
|
19
|
+
*/
|
|
20
|
+
export declare function createAuthChecker(secret: string): AuthChecker;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication middleware for proxy requests.
|
|
3
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
4
|
+
*/
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
/**
|
|
7
|
+
* Create an auth checker function for Bearer token authentication.
|
|
8
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
9
|
+
*
|
|
10
|
+
* @param secret - The expected Bearer token value
|
|
11
|
+
* @returns A function that returns true if the request has a valid Bearer token
|
|
12
|
+
*/
|
|
13
|
+
export function createAuthChecker(secret) {
|
|
14
|
+
return (req) => {
|
|
15
|
+
const auth = req.headers.authorization;
|
|
16
|
+
if (!auth)
|
|
17
|
+
return false;
|
|
18
|
+
const parts = auth.split(" ");
|
|
19
|
+
if (parts.length !== 2 || parts[0] !== "Bearer")
|
|
20
|
+
return false;
|
|
21
|
+
const provided = Buffer.from(parts[1]);
|
|
22
|
+
const expected = Buffer.from(secret);
|
|
23
|
+
// Length check before timing-safe comparison
|
|
24
|
+
if (provided.length !== expected.length)
|
|
25
|
+
return false;
|
|
26
|
+
return crypto.timingSafeEqual(provided, expected);
|
|
27
|
+
};
|
|
28
|
+
}
|