routstrd 0.1.1 → 0.1.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.
- package/SKILL.md +260 -0
- package/bun.lock +49 -189
- package/dist/daemon/index.js +1316 -234
- package/dist/index.js +4967 -381
- package/package.json +5 -4
- package/refund.js +33 -0
- package/refund_new.js +20 -0
- package/src/cli-shared.ts +8 -5
- package/src/cli.ts +462 -74
- package/src/daemon/http/index.ts +768 -140
- package/src/daemon/index.ts +106 -16
- package/src/daemon/wallet/cocod-client.ts +340 -0
- package/src/daemon/wallet/index.ts +56 -141
- package/src/integrations/index.ts +5 -3
- package/src/integrations/openclaw.ts +16 -26
- package/src/integrations/opencode.ts +15 -24
- package/src/integrations/pi.ts +15 -25
- package/src/integrations/registry.ts +71 -0
- package/src/start-daemon.ts +17 -12
- package/src/tui/usage/app.ts +1 -1
- package/src/tui/usage/data.ts +24 -14
- package/src/tui/usage/render.ts +10 -7
- package/src/utils/config.ts +1 -1
- package/src/utils/logger.ts +15 -4
- package/test_chat.sh +29 -0
- package/src/daemon/sse.ts +0 -98
package/src/cli.ts
CHANGED
|
@@ -13,13 +13,18 @@ import {
|
|
|
13
13
|
DB_PATH,
|
|
14
14
|
CONFIG_FILE,
|
|
15
15
|
DEFAULT_CONFIG,
|
|
16
|
-
|
|
16
|
+
LOGS_DIR,
|
|
17
17
|
type RoutstrdConfig,
|
|
18
18
|
} from "./utils/config";
|
|
19
19
|
import { logger } from "./utils/logger";
|
|
20
20
|
import { setupIntegration } from "./integrations";
|
|
21
21
|
import { createSdkStore } from "@routstr/sdk";
|
|
22
22
|
import { createBunSqliteDriver } from "@routstr/sdk/storage";
|
|
23
|
+
import * as QRCode from "qrcode";
|
|
24
|
+
import {
|
|
25
|
+
isCocodInstalled,
|
|
26
|
+
resolveCocodExecutable,
|
|
27
|
+
} from "./daemon/wallet/cocod-client";
|
|
23
28
|
|
|
24
29
|
type RoutstrModel = {
|
|
25
30
|
id: string;
|
|
@@ -42,7 +47,26 @@ type UsageEntry = {
|
|
|
42
47
|
client?: string;
|
|
43
48
|
};
|
|
44
49
|
|
|
45
|
-
const cliVersion = "0.1.
|
|
50
|
+
const cliVersion = "0.1.1";
|
|
51
|
+
|
|
52
|
+
function parsePositiveIntOrExit(value: string, fieldName: string): number {
|
|
53
|
+
const parsed = Number.parseInt(value, 10);
|
|
54
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
55
|
+
console.error(`Invalid ${fieldName}: ${value}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function printLightningInvoice(invoice: string): Promise<void> {
|
|
62
|
+
const paymentUri = `lightning:${invoice}`;
|
|
63
|
+
const qr = await QRCode.toString(paymentUri, {
|
|
64
|
+
type: "terminal",
|
|
65
|
+
small: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(`${qr}\nInvoice:\n${invoice}`);
|
|
69
|
+
}
|
|
46
70
|
|
|
47
71
|
async function initDaemon(): Promise<void> {
|
|
48
72
|
logger.log("Initializing routstrd...");
|
|
@@ -57,7 +81,9 @@ async function initDaemon(): Promise<void> {
|
|
|
57
81
|
|
|
58
82
|
const installCode = await installProc.exited;
|
|
59
83
|
if (installCode !== 0 || !(await checkCocodInstalled())) {
|
|
60
|
-
logger.error(
|
|
84
|
+
logger.error(
|
|
85
|
+
"Failed to install cocod. Please run 'bun install --global cocod' manually.",
|
|
86
|
+
);
|
|
61
87
|
return;
|
|
62
88
|
}
|
|
63
89
|
|
|
@@ -80,10 +106,39 @@ async function initDaemon(): Promise<void> {
|
|
|
80
106
|
logger.log(`Created config file: ${CONFIG_FILE}`);
|
|
81
107
|
}
|
|
82
108
|
|
|
109
|
+
const config = await loadConfig();
|
|
110
|
+
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
111
|
+
|
|
112
|
+
if (!(await isCocodInstalled(config.cocodPath))) {
|
|
113
|
+
if (config.cocodPath) {
|
|
114
|
+
logger.error(
|
|
115
|
+
`Configured cocod executable was not found: ${config.cocodPath}`,
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
logger.log("cocod not found. Installing globally with bun...");
|
|
121
|
+
|
|
122
|
+
const installProc = Bun.spawn(["bun", "install", "--global", "cocod"], {
|
|
123
|
+
stdout: "inherit",
|
|
124
|
+
stderr: "inherit",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const installCode = await installProc.exited;
|
|
128
|
+
if (installCode !== 0 || !(await isCocodInstalled(config.cocodPath))) {
|
|
129
|
+
logger.error(
|
|
130
|
+
"Failed to install cocod. Please run 'bun install --global cocod' manually.",
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logger.log("cocod installed successfully.");
|
|
136
|
+
}
|
|
137
|
+
|
|
83
138
|
console.log(`Database will be stored at: ${DB_PATH}`);
|
|
84
139
|
console.log("\nInitializing cocod...");
|
|
85
140
|
|
|
86
|
-
const initProc = Bun.spawn([
|
|
141
|
+
const initProc = Bun.spawn([cocodExecutable, "init"], {
|
|
87
142
|
stdout: "pipe",
|
|
88
143
|
stderr: "pipe",
|
|
89
144
|
});
|
|
@@ -115,12 +170,18 @@ async function initDaemon(): Promise<void> {
|
|
|
115
170
|
)
|
|
116
171
|
: Promise.resolve();
|
|
117
172
|
|
|
118
|
-
const [initCode] = await Promise.all([
|
|
173
|
+
const [initCode] = await Promise.all([
|
|
174
|
+
initProc.exited,
|
|
175
|
+
stdoutDone,
|
|
176
|
+
stderrDone,
|
|
177
|
+
]);
|
|
119
178
|
const combinedOutput = `${initStdout}\n${initStderr}`.toLowerCase();
|
|
120
179
|
const alreadyInitialized = combinedOutput.includes("already initialized");
|
|
121
180
|
|
|
122
181
|
if (initCode !== 0 && !alreadyInitialized) {
|
|
123
|
-
logger.error(
|
|
182
|
+
logger.error(
|
|
183
|
+
"Failed to initialize cocod. Please run 'cocod init' manually.",
|
|
184
|
+
);
|
|
124
185
|
return;
|
|
125
186
|
}
|
|
126
187
|
|
|
@@ -130,7 +191,6 @@ async function initDaemon(): Promise<void> {
|
|
|
130
191
|
logger.log("cocod initialized successfully.");
|
|
131
192
|
}
|
|
132
193
|
|
|
133
|
-
const config = await loadConfig();
|
|
134
194
|
await startDaemon({ port: String(config.port || 8008) });
|
|
135
195
|
|
|
136
196
|
// Create SDK store for integrations
|
|
@@ -140,7 +200,9 @@ async function initDaemon(): Promise<void> {
|
|
|
140
200
|
await setupIntegration(config, store);
|
|
141
201
|
|
|
142
202
|
logger.log("\nInitialization complete!");
|
|
143
|
-
logger.log(
|
|
203
|
+
logger.log(
|
|
204
|
+
"\n use 'routstrd wallet receive cashu <token>' or 'routstrd wallet receive bolt11 2100' to top up your local wallet!",
|
|
205
|
+
);
|
|
144
206
|
}
|
|
145
207
|
|
|
146
208
|
async function checkCocodInstalled(): Promise<boolean> {
|
|
@@ -164,7 +226,9 @@ program
|
|
|
164
226
|
// Onboard - initialize the daemon
|
|
165
227
|
program
|
|
166
228
|
.command("onboard")
|
|
167
|
-
.description(
|
|
229
|
+
.description(
|
|
230
|
+
"Initialize routstrd (creates config directory and initializes cocod)",
|
|
231
|
+
)
|
|
168
232
|
.action(async () => {
|
|
169
233
|
await initDaemon();
|
|
170
234
|
});
|
|
@@ -176,11 +240,14 @@ program
|
|
|
176
240
|
.option("--port <port>", "Port to listen on")
|
|
177
241
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
178
242
|
.action(async (options: { port?: string; provider?: string }) => {
|
|
179
|
-
|
|
180
|
-
|
|
243
|
+
const config = await loadConfig();
|
|
244
|
+
if (!(await isCocodInstalled(config.cocodPath))) {
|
|
245
|
+
const installHint = config.cocodPath
|
|
246
|
+
? `Configured cocod executable was not found: ${config.cocodPath}`
|
|
247
|
+
: "cocod is not installed. Run 'routstrd onboard' first to install cocod.";
|
|
248
|
+
logger.error(installHint);
|
|
181
249
|
process.exit(1);
|
|
182
250
|
}
|
|
183
|
-
const config = await loadConfig();
|
|
184
251
|
await startDaemon({
|
|
185
252
|
port: options.port || String(config.port || 8008),
|
|
186
253
|
provider: options.provider,
|
|
@@ -224,7 +291,7 @@ program
|
|
|
224
291
|
.description("Get wallet and API key balances")
|
|
225
292
|
.action(async () => {
|
|
226
293
|
await ensureDaemonRunning();
|
|
227
|
-
|
|
294
|
+
|
|
228
295
|
const [walletResult, keysResult] = await Promise.all([
|
|
229
296
|
callDaemon("/balance"),
|
|
230
297
|
callDaemon("/keys/balance"),
|
|
@@ -234,8 +301,14 @@ program
|
|
|
234
301
|
|
|
235
302
|
console.log("=== Wallet Balance ===");
|
|
236
303
|
let totalWallet = 0;
|
|
237
|
-
if (
|
|
238
|
-
|
|
304
|
+
if (
|
|
305
|
+
walletResult.output &&
|
|
306
|
+
typeof walletResult.output === "object" &&
|
|
307
|
+
"balances" in walletResult.output
|
|
308
|
+
) {
|
|
309
|
+
const balances = (
|
|
310
|
+
walletResult.output as { balances: Record<string, number> }
|
|
311
|
+
).balances;
|
|
239
312
|
for (const [mintUrl, balance] of Object.entries(balances)) {
|
|
240
313
|
console.log(` ${mintUrl}: ${balance} sats`);
|
|
241
314
|
totalWallet += balance;
|
|
@@ -247,9 +320,17 @@ program
|
|
|
247
320
|
|
|
248
321
|
console.log("\n=== API Keys ===");
|
|
249
322
|
let totalApiKeys = 0;
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
323
|
+
if (
|
|
324
|
+
keysResult.output &&
|
|
325
|
+
typeof keysResult.output === "object" &&
|
|
326
|
+
"keys" in keysResult.output
|
|
327
|
+
) {
|
|
328
|
+
const keys = (
|
|
329
|
+
keysResult.output as {
|
|
330
|
+
keys: Array<{ id: string; name: string; balance: number }>;
|
|
331
|
+
}
|
|
332
|
+
).keys;
|
|
333
|
+
const apiKeyEntries = keys.filter((k) => k.id.startsWith("apikey:"));
|
|
253
334
|
for (const key of apiKeyEntries) {
|
|
254
335
|
const name = key.name.replace("API Key: ", "");
|
|
255
336
|
console.log(` ${name}: ${key.balance} sats`);
|
|
@@ -265,7 +346,9 @@ program
|
|
|
265
346
|
}
|
|
266
347
|
|
|
267
348
|
console.log("\n=== Summary ===");
|
|
268
|
-
console.log(
|
|
349
|
+
console.log(
|
|
350
|
+
` Wallet: ${totalWallet} sats | API Keys: ${totalApiKeys} sats`,
|
|
351
|
+
);
|
|
269
352
|
console.log(` Grand Total: ${totalWallet + totalApiKeys} sats`);
|
|
270
353
|
});
|
|
271
354
|
|
|
@@ -284,7 +367,7 @@ program
|
|
|
284
367
|
.option("-r, --refresh", "Force refresh routstr21 models from Nostr", false)
|
|
285
368
|
.action(async (options: { refresh: boolean }) => {
|
|
286
369
|
await ensureDaemonRunning();
|
|
287
|
-
|
|
370
|
+
|
|
288
371
|
const result = await callDaemon(
|
|
289
372
|
options.refresh ? "/models?refresh=true" : "/models",
|
|
290
373
|
);
|
|
@@ -293,7 +376,11 @@ program
|
|
|
293
376
|
process.exit(1);
|
|
294
377
|
}
|
|
295
378
|
|
|
296
|
-
if (
|
|
379
|
+
if (
|
|
380
|
+
result.output &&
|
|
381
|
+
typeof result.output === "object" &&
|
|
382
|
+
"models" in result.output
|
|
383
|
+
) {
|
|
297
384
|
const models = (result.output as { models: RoutstrModel[] }).models;
|
|
298
385
|
if (models.length === 0) {
|
|
299
386
|
console.log("No routstr21 models found");
|
|
@@ -303,7 +390,9 @@ program
|
|
|
303
390
|
const details = [
|
|
304
391
|
model.name && model.name !== model.id ? model.name : null,
|
|
305
392
|
model.context_length ? `${model.context_length} ctx` : null,
|
|
306
|
-
]
|
|
393
|
+
]
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join(" - ");
|
|
307
396
|
console.log(`${i + 1}. ${model.id}${details ? ` (${details})` : ""}`);
|
|
308
397
|
});
|
|
309
398
|
}
|
|
@@ -319,7 +408,9 @@ program
|
|
|
319
408
|
|
|
320
409
|
const requested = Number.parseInt(options.limit, 10);
|
|
321
410
|
const limit =
|
|
322
|
-
Number.isFinite(requested) && requested > 0
|
|
411
|
+
Number.isFinite(requested) && requested > 0
|
|
412
|
+
? Math.min(requested, 1000)
|
|
413
|
+
: 10;
|
|
323
414
|
|
|
324
415
|
const result = await callDaemon(`/usage?limit=${limit}`);
|
|
325
416
|
if (result.error) {
|
|
@@ -327,20 +418,16 @@ program
|
|
|
327
418
|
process.exit(1);
|
|
328
419
|
}
|
|
329
420
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
entries?: UsageEntry[];
|
|
333
|
-
totalEntries?: number;
|
|
334
|
-
totalSatsCost?: number;
|
|
335
|
-
recentSatsCost?: number;
|
|
336
|
-
limit?: number;
|
|
337
|
-
}
|
|
338
|
-
| undefined;
|
|
421
|
+
// The daemon returns { output: UsageEntry[] } where output is the array directly
|
|
422
|
+
const entries = (result.output as UsageEntry[] | undefined) || [];
|
|
339
423
|
|
|
340
|
-
|
|
341
|
-
const totalEntries =
|
|
342
|
-
const totalSatsCost =
|
|
343
|
-
|
|
424
|
+
// Calculate totals from entries
|
|
425
|
+
const totalEntries = entries.length;
|
|
426
|
+
const totalSatsCost = entries.reduce(
|
|
427
|
+
(sum, e) => sum + (e.satsCost || 0),
|
|
428
|
+
0,
|
|
429
|
+
);
|
|
430
|
+
const recentSatsCost = totalSatsCost; // For now, recent = total since we don't have time window
|
|
344
431
|
|
|
345
432
|
console.log(`Usage entries: showing ${entries.length} of ${totalEntries}`);
|
|
346
433
|
console.log(`Total sats cost (all time): ${totalSatsCost.toFixed(3)} sats`);
|
|
@@ -383,18 +470,26 @@ providersCmd
|
|
|
383
470
|
process.exit(1);
|
|
384
471
|
}
|
|
385
472
|
|
|
386
|
-
const output = result.output as
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
473
|
+
const output = result.output as
|
|
474
|
+
| {
|
|
475
|
+
providers: Array<{
|
|
476
|
+
index: number;
|
|
477
|
+
baseUrl: string;
|
|
478
|
+
disabled: boolean;
|
|
479
|
+
}>;
|
|
480
|
+
disabledCount: number;
|
|
481
|
+
totalCount: number;
|
|
482
|
+
}
|
|
483
|
+
| undefined;
|
|
391
484
|
|
|
392
485
|
if (!output?.providers) {
|
|
393
486
|
console.log("No providers found.");
|
|
394
487
|
return;
|
|
395
488
|
}
|
|
396
489
|
|
|
397
|
-
console.log(
|
|
490
|
+
console.log(
|
|
491
|
+
`Providers (${output.totalCount} total, ${output.disabledCount} disabled):\n`,
|
|
492
|
+
);
|
|
398
493
|
for (const provider of output.providers) {
|
|
399
494
|
const status = provider.disabled ? "DISABLED" : "enabled ";
|
|
400
495
|
console.log(` [${provider.index}] ${status} ${provider.baseUrl}`);
|
|
@@ -403,11 +498,15 @@ providersCmd
|
|
|
403
498
|
|
|
404
499
|
providersCmd
|
|
405
500
|
.command("disable <indices...>")
|
|
406
|
-
.description(
|
|
501
|
+
.description(
|
|
502
|
+
"Disable providers by their indices (e.g., routstrd providers disable 0 2 5)",
|
|
503
|
+
)
|
|
407
504
|
.action(async (indices: string[]) => {
|
|
408
505
|
await ensureDaemonRunning();
|
|
409
506
|
|
|
410
|
-
const indexNums = indices
|
|
507
|
+
const indexNums = indices
|
|
508
|
+
.map((s) => parseInt(s, 10))
|
|
509
|
+
.filter((n) => Number.isFinite(n));
|
|
411
510
|
if (indexNums.length === 0) {
|
|
412
511
|
console.log("No valid indices provided.");
|
|
413
512
|
process.exit(1);
|
|
@@ -423,7 +522,9 @@ providersCmd
|
|
|
423
522
|
process.exit(1);
|
|
424
523
|
}
|
|
425
524
|
|
|
426
|
-
const output = result.output as
|
|
525
|
+
const output = result.output as
|
|
526
|
+
| { message: string; disabled: string[] }
|
|
527
|
+
| undefined;
|
|
427
528
|
if (output) {
|
|
428
529
|
console.log(output.message);
|
|
429
530
|
for (const url of output.disabled) {
|
|
@@ -434,11 +535,15 @@ providersCmd
|
|
|
434
535
|
|
|
435
536
|
providersCmd
|
|
436
537
|
.command("enable <indices...>")
|
|
437
|
-
.description(
|
|
538
|
+
.description(
|
|
539
|
+
"Enable providers by their indices (e.g., routstrd providers enable 0 2 5)",
|
|
540
|
+
)
|
|
438
541
|
.action(async (indices: string[]) => {
|
|
439
542
|
await ensureDaemonRunning();
|
|
440
543
|
|
|
441
|
-
const indexNums = indices
|
|
544
|
+
const indexNums = indices
|
|
545
|
+
.map((s) => parseInt(s, 10))
|
|
546
|
+
.filter((n) => Number.isFinite(n));
|
|
442
547
|
if (indexNums.length === 0) {
|
|
443
548
|
console.log("No valid indices provided.");
|
|
444
549
|
process.exit(1);
|
|
@@ -454,7 +559,9 @@ providersCmd
|
|
|
454
559
|
process.exit(1);
|
|
455
560
|
}
|
|
456
561
|
|
|
457
|
-
const output = result.output as
|
|
562
|
+
const output = result.output as
|
|
563
|
+
| { message: string; enabled: string[] }
|
|
564
|
+
| undefined;
|
|
458
565
|
if (output) {
|
|
459
566
|
console.log(output.message);
|
|
460
567
|
for (const url of output.enabled) {
|
|
@@ -463,6 +570,96 @@ providersCmd
|
|
|
463
570
|
}
|
|
464
571
|
});
|
|
465
572
|
|
|
573
|
+
// Clients - list and manage clients
|
|
574
|
+
const clientsCmd = program
|
|
575
|
+
.command("clients")
|
|
576
|
+
.description("List and manage clients");
|
|
577
|
+
|
|
578
|
+
clientsCmd
|
|
579
|
+
.command("list")
|
|
580
|
+
.description("List all clients")
|
|
581
|
+
.action(async () => {
|
|
582
|
+
await ensureDaemonRunning();
|
|
583
|
+
|
|
584
|
+
const result = await callDaemon("/clients");
|
|
585
|
+
if (result.error) {
|
|
586
|
+
console.log(result.error);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const output = result.output as
|
|
591
|
+
| {
|
|
592
|
+
clients: Array<{
|
|
593
|
+
id: string;
|
|
594
|
+
name: string;
|
|
595
|
+
apiKey: string;
|
|
596
|
+
createdAt: number;
|
|
597
|
+
lastUsed?: number | null;
|
|
598
|
+
}>;
|
|
599
|
+
totalCount: number;
|
|
600
|
+
}
|
|
601
|
+
| undefined;
|
|
602
|
+
|
|
603
|
+
if (!output?.clients || output.clients.length === 0) {
|
|
604
|
+
console.log("No clients found.");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.log(`Clients (${output.totalCount} total):\n`);
|
|
609
|
+
for (const client of output.clients) {
|
|
610
|
+
const createdAt = new Date(client.createdAt).toISOString();
|
|
611
|
+
const lastUsed = client.lastUsed
|
|
612
|
+
? new Date(client.lastUsed).toISOString()
|
|
613
|
+
: "never";
|
|
614
|
+
console.log(` ${client.id}`);
|
|
615
|
+
console.log(` Name: ${client.name}`);
|
|
616
|
+
console.log(` API Key: ${client.apiKey}`);
|
|
617
|
+
console.log(` Created: ${createdAt}`);
|
|
618
|
+
console.log("");
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
clientsCmd
|
|
623
|
+
.command("add")
|
|
624
|
+
.description("Add a new client")
|
|
625
|
+
.requiredOption("-n, --name <name>", "Client name")
|
|
626
|
+
.action(async (options: { name: string }) => {
|
|
627
|
+
await ensureDaemonRunning();
|
|
628
|
+
const config = await loadConfig();
|
|
629
|
+
|
|
630
|
+
const result = await callDaemon("/clients/add", {
|
|
631
|
+
method: "POST",
|
|
632
|
+
body: { name: options.name },
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (result.error) {
|
|
636
|
+
console.log(result.error);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const output = result.output as
|
|
641
|
+
| {
|
|
642
|
+
message: string;
|
|
643
|
+
client: {
|
|
644
|
+
id: string;
|
|
645
|
+
name: string;
|
|
646
|
+
apiKey: string;
|
|
647
|
+
createdAt: number;
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
| undefined;
|
|
651
|
+
|
|
652
|
+
if (output) {
|
|
653
|
+
console.log(output.message);
|
|
654
|
+
console.log(`\n ID: ${output.client.id}`);
|
|
655
|
+
console.log(` Name: ${output.client.name}`);
|
|
656
|
+
console.log(` API Key: ${output.client.apiKey}`);
|
|
657
|
+
console.log(
|
|
658
|
+
`\n Access Routstr at: http://localhost:${config.port || 8008}`,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
466
663
|
// Monitor - interactive TUI
|
|
467
664
|
program
|
|
468
665
|
.command("monitor")
|
|
@@ -472,6 +669,143 @@ program
|
|
|
472
669
|
await runUsageTui();
|
|
473
670
|
});
|
|
474
671
|
|
|
672
|
+
const walletCmd = program.command("wallet").description("Wallet operations");
|
|
673
|
+
|
|
674
|
+
walletCmd
|
|
675
|
+
.command("status")
|
|
676
|
+
.description("Check wallet status")
|
|
677
|
+
.action(async () => {
|
|
678
|
+
await handleDaemonCommand("/wallet/status");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
walletCmd
|
|
682
|
+
.command("unlock <passphrase>")
|
|
683
|
+
.description("Unlock the wallet")
|
|
684
|
+
.action(async (passphrase: string) => {
|
|
685
|
+
await handleDaemonCommand("/wallet/unlock", {
|
|
686
|
+
method: "POST",
|
|
687
|
+
body: { passphrase },
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
walletCmd
|
|
692
|
+
.command("balance")
|
|
693
|
+
.description("Get wallet balance")
|
|
694
|
+
.action(async () => {
|
|
695
|
+
await handleDaemonCommand("/wallet/balance");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const walletReceiveCmd = walletCmd
|
|
699
|
+
.command("receive")
|
|
700
|
+
.description("Wallet receive operations");
|
|
701
|
+
|
|
702
|
+
walletReceiveCmd
|
|
703
|
+
.command("cashu <token>")
|
|
704
|
+
.description("Receive a Cashu token")
|
|
705
|
+
.action(async (token: string) => {
|
|
706
|
+
await handleDaemonCommand("/wallet/receive/cashu", {
|
|
707
|
+
method: "POST",
|
|
708
|
+
body: { token },
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
walletReceiveCmd
|
|
713
|
+
.command("bolt11 <amount>")
|
|
714
|
+
.description("Create a Lightning invoice")
|
|
715
|
+
.option("--mint-url <url>", "Mint URL to use")
|
|
716
|
+
.action(async (amount: string, options: { mintUrl?: string }) => {
|
|
717
|
+
try {
|
|
718
|
+
await ensureDaemonRunning();
|
|
719
|
+
|
|
720
|
+
const result = await callDaemon("/wallet/receive/bolt11", {
|
|
721
|
+
method: "POST",
|
|
722
|
+
body: {
|
|
723
|
+
amount: parsePositiveIntOrExit(amount, "amount"),
|
|
724
|
+
mintUrl: options.mintUrl,
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const output = result.output as
|
|
729
|
+
| { invoice?: string; amount?: number; mintUrl?: string }
|
|
730
|
+
| undefined;
|
|
731
|
+
|
|
732
|
+
if (typeof output?.invoice === "string" && output.invoice) {
|
|
733
|
+
await printLightningInvoice(output.invoice);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (result.output !== undefined) {
|
|
738
|
+
console.log(JSON.stringify(result.output, null, 2));
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.error((error as Error).message);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const walletSendCmd = walletCmd
|
|
747
|
+
.command("send")
|
|
748
|
+
.description("Wallet send operations");
|
|
749
|
+
|
|
750
|
+
walletSendCmd
|
|
751
|
+
.command("cashu <amount>")
|
|
752
|
+
.description("Create a Cashu token to send")
|
|
753
|
+
.option("--mint-url <url>", "Mint URL to use")
|
|
754
|
+
.action(async (amount: string, options: { mintUrl?: string }) => {
|
|
755
|
+
await handleDaemonCommand("/wallet/send/cashu", {
|
|
756
|
+
method: "POST",
|
|
757
|
+
body: {
|
|
758
|
+
amount: parsePositiveIntOrExit(amount, "amount"),
|
|
759
|
+
mintUrl: options.mintUrl,
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
walletSendCmd
|
|
765
|
+
.command("bolt11 <invoice>")
|
|
766
|
+
.description("Pay a Lightning invoice")
|
|
767
|
+
.option("--mint-url <url>", "Mint URL to use")
|
|
768
|
+
.action(async (invoice: string, options: { mintUrl?: string }) => {
|
|
769
|
+
await handleDaemonCommand("/wallet/send/bolt11", {
|
|
770
|
+
method: "POST",
|
|
771
|
+
body: {
|
|
772
|
+
invoice,
|
|
773
|
+
mintUrl: options.mintUrl,
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const walletMintsCmd = walletCmd
|
|
779
|
+
.command("mints")
|
|
780
|
+
.description("Wallet mint operations");
|
|
781
|
+
|
|
782
|
+
walletMintsCmd
|
|
783
|
+
.command("list")
|
|
784
|
+
.description("List configured wallet mints")
|
|
785
|
+
.action(async () => {
|
|
786
|
+
await handleDaemonCommand("/wallet/mints");
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
walletMintsCmd
|
|
790
|
+
.command("add <url>")
|
|
791
|
+
.description("Add a wallet mint")
|
|
792
|
+
.action(async (url: string) => {
|
|
793
|
+
await handleDaemonCommand("/wallet/mints", {
|
|
794
|
+
method: "POST",
|
|
795
|
+
body: { url },
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
walletMintsCmd
|
|
800
|
+
.command("info <url>")
|
|
801
|
+
.description("Get wallet mint info")
|
|
802
|
+
.action(async (url: string) => {
|
|
803
|
+
await handleDaemonCommand("/wallet/mints/info", {
|
|
804
|
+
method: "POST",
|
|
805
|
+
body: { url },
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
|
|
475
809
|
// Stop
|
|
476
810
|
program
|
|
477
811
|
.command("stop")
|
|
@@ -493,7 +827,7 @@ program
|
|
|
493
827
|
if (wasRunning) {
|
|
494
828
|
console.log("Stopping daemon...");
|
|
495
829
|
await callDaemon("/stop", { method: "POST" });
|
|
496
|
-
|
|
830
|
+
|
|
497
831
|
// Wait for daemon to fully stop
|
|
498
832
|
for (let i = 0; i < 50; i++) {
|
|
499
833
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -522,32 +856,43 @@ program
|
|
|
522
856
|
// Mode
|
|
523
857
|
program
|
|
524
858
|
.command("mode")
|
|
525
|
-
.description("Set the client mode (
|
|
859
|
+
.description("Set the client mode (lazyrefund/apikeys or xcashu)")
|
|
526
860
|
.action(async () => {
|
|
527
861
|
const config = await loadConfig();
|
|
528
862
|
const currentMode = config.mode || "apikeys";
|
|
529
|
-
|
|
863
|
+
|
|
530
864
|
console.log("Select client mode:");
|
|
531
|
-
console.log(
|
|
532
|
-
|
|
865
|
+
console.log(
|
|
866
|
+
" 1) lazyrefund/apikeys - Pseudonymous accounts are kept with the Routstr nodes and are refunded after 5 mins if not used.",
|
|
867
|
+
);
|
|
868
|
+
console.log(
|
|
869
|
+
" 2) xcashu (coming soon) - Balances are never kept with the nodes, all balances are refunded in response.",
|
|
870
|
+
);
|
|
533
871
|
console.log(`\nCurrent mode: ${currentMode}`);
|
|
534
872
|
|
|
535
873
|
const modes: Array<"apikeys" | "xcashu"> = ["apikeys", "xcashu"];
|
|
536
|
-
|
|
874
|
+
|
|
537
875
|
const selectedIndex = await new Promise<number>((resolve) => {
|
|
538
876
|
const rl = require("readline").createInterface({
|
|
539
877
|
input: process.stdin,
|
|
540
878
|
output: process.stdout,
|
|
541
879
|
});
|
|
542
|
-
rl.question("\nEnter choice (1-
|
|
880
|
+
rl.question("\nEnter choice (1-2): ", (answer: string) => {
|
|
543
881
|
rl.close();
|
|
544
882
|
const num = parseInt(answer, 10);
|
|
545
|
-
resolve(Number.isFinite(num) && num >= 1 && num <=
|
|
883
|
+
resolve(Number.isFinite(num) && num >= 1 && num <= 2 ? num - 1 : 0);
|
|
546
884
|
});
|
|
547
885
|
});
|
|
548
886
|
|
|
549
887
|
const selectedMode = modes[selectedIndex];
|
|
550
|
-
|
|
888
|
+
|
|
889
|
+
if (selectedMode === "xcashu") {
|
|
890
|
+
console.log(
|
|
891
|
+
"\nxcashu mode is coming soon! Only lazyrefund/apikeys is available at this time.",
|
|
892
|
+
);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
551
896
|
if (selectedMode === currentMode) {
|
|
552
897
|
console.log(`Mode is already set to '${selectedMode}'. No changes made.`);
|
|
553
898
|
return;
|
|
@@ -566,7 +911,7 @@ program
|
|
|
566
911
|
if (wasRunning) {
|
|
567
912
|
console.log("Stopping daemon...");
|
|
568
913
|
await callDaemon("/stop", { method: "POST" });
|
|
569
|
-
|
|
914
|
+
|
|
570
915
|
for (let i = 0; i < 50; i++) {
|
|
571
916
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
572
917
|
if (!(await isDaemonRunning())) {
|
|
@@ -590,22 +935,47 @@ program
|
|
|
590
935
|
});
|
|
591
936
|
|
|
592
937
|
// Logs
|
|
938
|
+
function getLogFileForDate(date: Date = new Date()): string {
|
|
939
|
+
const year = date.getFullYear();
|
|
940
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
941
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
942
|
+
return `${LOGS_DIR}/${year}-${month}-${day}.log`;
|
|
943
|
+
}
|
|
944
|
+
|
|
593
945
|
program
|
|
594
946
|
.command("logs")
|
|
595
947
|
.description("View daemon logs")
|
|
596
948
|
.option("-f, --follow", "Follow log output", false)
|
|
597
949
|
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
598
950
|
.action(async (options: { follow: boolean; lines: string }) => {
|
|
599
|
-
|
|
600
|
-
|
|
951
|
+
const todayFile = getLogFileForDate();
|
|
952
|
+
const yesterday = new Date();
|
|
953
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
954
|
+
const yesterdayFile = getLogFileForDate(yesterday);
|
|
955
|
+
|
|
956
|
+
if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
|
|
957
|
+
console.log("No log files found. Daemon may not have started yet.");
|
|
958
|
+
console.log(`Logs directory: ${LOGS_DIR}`);
|
|
601
959
|
process.exit(1);
|
|
602
960
|
}
|
|
603
961
|
|
|
604
962
|
const lines = parseInt(options.lines, 10);
|
|
605
963
|
|
|
606
964
|
const readLastLines = async (): Promise<string[]> => {
|
|
607
|
-
|
|
608
|
-
|
|
965
|
+
let allLines: string[] = [];
|
|
966
|
+
|
|
967
|
+
// Read yesterday's log first if it exists
|
|
968
|
+
if (existsSync(yesterdayFile)) {
|
|
969
|
+
const yesterdayContent = await Bun.file(yesterdayFile).text();
|
|
970
|
+
allLines = yesterdayContent.split("\n").filter(Boolean);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Then read today's log
|
|
974
|
+
if (existsSync(todayFile)) {
|
|
975
|
+
const todayContent = await Bun.file(todayFile).text();
|
|
976
|
+
allLines = allLines.concat(todayContent.split("\n").filter(Boolean));
|
|
977
|
+
}
|
|
978
|
+
|
|
609
979
|
return allLines.slice(-lines);
|
|
610
980
|
};
|
|
611
981
|
|
|
@@ -617,22 +987,40 @@ program
|
|
|
617
987
|
};
|
|
618
988
|
|
|
619
989
|
if (options.follow) {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
990
|
+
let currentLogFile = todayFile;
|
|
991
|
+
let lastSize = 0;
|
|
992
|
+
|
|
993
|
+
if (existsSync(currentLogFile)) {
|
|
994
|
+
lastSize = (await Bun.file(currentLogFile).text()).length;
|
|
995
|
+
}
|
|
996
|
+
|
|
624
997
|
await printLines();
|
|
625
998
|
|
|
626
999
|
const interval = setInterval(async () => {
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
if (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1000
|
+
// Check if we need to switch to a new date file
|
|
1001
|
+
const newLogFile = getLogFileForDate();
|
|
1002
|
+
if (newLogFile !== currentLogFile) {
|
|
1003
|
+
console.log(`\n--- Switched to ${newLogFile} ---\n`);
|
|
1004
|
+
currentLogFile = newLogFile;
|
|
1005
|
+
lastSize = existsSync(currentLogFile)
|
|
1006
|
+
? (await Bun.file(currentLogFile).text()).length
|
|
1007
|
+
: 0;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (existsSync(currentLogFile)) {
|
|
1011
|
+
const content = await Bun.file(currentLogFile).text();
|
|
1012
|
+
const currentSize = content.length;
|
|
1013
|
+
if (currentSize > lastSize) {
|
|
1014
|
+
const allLines = content.split("\n").filter(Boolean);
|
|
1015
|
+
const newLines = allLines.slice(
|
|
1016
|
+
Math.floor(lastSize === 0 ? 0 : -1),
|
|
1017
|
+
-1,
|
|
1018
|
+
);
|
|
1019
|
+
for (const line of newLines) {
|
|
1020
|
+
console.log(line);
|
|
1021
|
+
}
|
|
1022
|
+
lastSize = currentSize;
|
|
634
1023
|
}
|
|
635
|
-
lastSize = currentSize;
|
|
636
1024
|
}
|
|
637
1025
|
}, 1000);
|
|
638
1026
|
|