@token2chat/t2c 0.2.0-beta.1 → 0.2.1

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 (40) hide show
  1. package/README.md +117 -144
  2. package/dist/cashu-store.d.ts +1 -1
  3. package/dist/cashu-store.js +4 -4
  4. package/dist/commands/audit.d.ts +65 -0
  5. package/dist/commands/audit.js +12 -12
  6. package/dist/commands/balance.js +2 -2
  7. package/dist/commands/doctor.js +2 -2
  8. package/dist/commands/init.js +2 -2
  9. package/dist/commands/mint.js +14 -14
  10. package/dist/commands/monitor.d.ts +51 -0
  11. package/dist/commands/monitor.js +353 -0
  12. package/dist/commands/recover.js +4 -4
  13. package/dist/commands/setup.js +2 -2
  14. package/dist/commands/status.js +2 -3
  15. package/dist/config.d.ts +5 -0
  16. package/dist/config.js +17 -0
  17. package/dist/connectors/cursor.js +44 -15
  18. package/dist/connectors/openclaw.js +32 -24
  19. package/dist/index.js +8 -1
  20. package/dist/proxy/auth.d.ts +20 -0
  21. package/dist/proxy/auth.js +28 -0
  22. package/dist/proxy/errors.d.ts +58 -0
  23. package/dist/proxy/errors.js +95 -0
  24. package/dist/proxy/gate-client.d.ts +34 -0
  25. package/dist/proxy/gate-client.js +81 -0
  26. package/dist/proxy/index.d.ts +10 -0
  27. package/dist/proxy/index.js +17 -0
  28. package/dist/proxy/payment-service.d.ts +65 -0
  29. package/dist/proxy/payment-service.js +101 -0
  30. package/dist/proxy/pricing.d.ts +37 -0
  31. package/dist/proxy/pricing.js +90 -0
  32. package/dist/proxy/response.d.ts +24 -0
  33. package/dist/proxy/response.js +48 -0
  34. package/dist/proxy/sse-parser.d.ts +19 -0
  35. package/dist/proxy/sse-parser.js +80 -0
  36. package/dist/proxy/types.d.ts +113 -0
  37. package/dist/proxy/types.js +74 -0
  38. package/dist/proxy.d.ts +2 -9
  39. package/dist/proxy.js +74 -186
  40. package/package.json +5 -2
@@ -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
+ }
@@ -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} sat\n`);
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.toLocaleString()} sat`);
51
- console.log(`New wallet balance: ${wallet.balance.toLocaleString()} sat\n`);
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.`);
@@ -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} sat)`);
109
+ console.log(`āœ… Wallet initialized (balance: ${formatUnits(wallet.balance)})`);
110
110
  }
111
111
  catch (e) {
112
112
  console.log(`āš ļø Failed to initialize wallet: ${e}`);
@@ -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} ${balanceStr} sat (${walletInfo.proofs} proofs)`);
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 (coming soon)",
39
+ description: "Configure Cursor IDE for Token2Chat",
5
40
  async detect() {
6
- // TODO: Detect Cursor installation
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šŸŽŸļø Cursor Integration\n");
15
- console.log(" Cursor connector coming soon!\n");
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. Set OpenAI Base URL:");
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(" ecash-proxy\n");
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 false;
55
+ return detectCursorInstallation();
27
56
  },
28
57
  };
@@ -6,7 +6,8 @@
6
6
  import fs from "node:fs/promises";
7
7
  import path from "node:path";
8
8
  import os from "node:os";
9
- import { loadOrCreateProxySecret } from "../config.js";
9
+ import { loadOrCreateProxySecret, WALLET_PATH, formatUnits } from "../config.js";
10
+ import { CashuStore } from "../cashu-store.js";
10
11
  /**
11
12
  * Default models when Gate is unreachable.
12
13
  * IDs use dash format (proxy transforms to slash for Gate/OpenRouter).
@@ -72,25 +73,6 @@ async function getConfigPath() {
72
73
  */
73
74
  function mergeToken2ChatConfig(existingConfig, t2cConfig, gateModels, apiKey) {
74
75
  const config = { ...existingConfig };
75
- // Ensure plugins.entries exists
76
- if (!config.plugins || typeof config.plugins !== "object") {
77
- config.plugins = {};
78
- }
79
- const plugins = config.plugins;
80
- if (!plugins.entries || typeof plugins.entries !== "object") {
81
- plugins.entries = {};
82
- }
83
- const entries = plugins.entries;
84
- // Set/overwrite token2chat plugin config
85
- entries.token2chat = {
86
- enabled: true,
87
- config: {
88
- gateUrl: t2cConfig.gateUrl,
89
- mintUrl: t2cConfig.mintUrl,
90
- proxyPort: t2cConfig.proxyPort,
91
- walletPath: t2cConfig.walletPath,
92
- },
93
- };
94
76
  // Ensure models.providers exists
95
77
  if (!config.models || typeof config.models !== "object") {
96
78
  config.models = {};
@@ -101,11 +83,29 @@ function mergeToken2ChatConfig(existingConfig, t2cConfig, gateModels, apiKey) {
101
83
  }
102
84
  const providers = models.providers;
103
85
  // Set/overwrite token2chat provider config
86
+ const activeModels = gateModels.length > 0 ? gateModels : DEFAULT_MODELS;
104
87
  providers.token2chat = {
105
88
  baseUrl: `http://127.0.0.1:${t2cConfig.proxyPort}/v1`,
106
89
  apiKey,
107
90
  api: "openai-completions",
108
- models: gateModels.length > 0 ? gateModels : DEFAULT_MODELS,
91
+ models: activeModels,
92
+ };
93
+ // Set up agents.defaults with token2chat model allowlist and fallbacks
94
+ if (!config.agents || typeof config.agents !== "object") {
95
+ config.agents = {};
96
+ }
97
+ const agents = config.agents;
98
+ if (!agents.defaults || typeof agents.defaults !== "object") {
99
+ agents.defaults = {};
100
+ }
101
+ const defaults = agents.defaults;
102
+ const modelIds = activeModels.map((m) => m.id);
103
+ defaults.models = modelIds;
104
+ defaults.model = {
105
+ ...(typeof defaults.model === "object" && defaults.model !== null
106
+ ? defaults.model
107
+ : {}),
108
+ fallbacks: modelIds,
109
109
  };
110
110
  return config;
111
111
  }
@@ -175,6 +175,16 @@ export const openclawConnector = {
175
175
  if (existingContent) {
176
176
  console.log(` Backup: ${backupPath}`);
177
177
  }
178
+ // Show wallet balance for onboarding
179
+ try {
180
+ const wallet = await CashuStore.load(WALLET_PATH, config.mintUrl);
181
+ console.log(`\nšŸ’° Wallet balance: ${formatUnits(wallet.balance)}`);
182
+ }
183
+ catch {
184
+ console.log("\nšŸ’° Wallet: not funded yet");
185
+ console.log(` Fund your wallet: t2c mint <amount>`);
186
+ console.log(` Mint URL: ${config.mintUrl}`);
187
+ }
178
188
  console.log("\nšŸ“‹ Next steps:\n");
179
189
  console.log(" 1. Restart OpenClaw gateway:");
180
190
  console.log(" openclaw gateway restart\n");
@@ -189,11 +199,9 @@ export const openclawConnector = {
189
199
  const configPath = await getConfigPath();
190
200
  const content = await fs.readFile(configPath, "utf-8");
191
201
  const doc = JSON.parse(content);
192
- const plugins = doc?.plugins;
193
- const entries = plugins?.entries;
194
202
  const models = doc?.models;
195
203
  const providers = models?.providers;
196
- return (entries?.token2chat !== undefined && providers?.token2chat !== undefined);
204
+ return providers?.token2chat !== undefined;
197
205
  }
198
206
  catch {
199
207
  return false;