routstrd 0.2.6 → 0.2.9
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 +17 -20
- package/SKILL.md +46 -13
- package/bun.lock +16 -217
- package/dist/daemon/index.js +588 -404
- package/dist/index.js +10328 -31646
- package/package.json +2 -1
- package/src/cli.ts +291 -208
- package/src/daemon/wallet/cocod-client.ts +22 -6
- package/src/integrations/claudecode.ts +19 -40
- package/src/integrations/openclaw.ts +8 -34
- package/src/integrations/opencode.ts +8 -34
- package/src/integrations/pi.ts +7 -34
- package/src/integrations/registry.ts +4 -12
- package/src/start-daemon.ts +52 -30
- package/src/tui/usage/data.ts +19 -7
- package/src/utils/clients.ts +304 -0
- package/src/utils/config.ts +2 -0
- package/src/utils/daemon-client.ts +79 -28
- package/src/utils/nip98.ts +102 -0
- package/src/utils/process-lock.ts +136 -0
- package/src/daemon/http/index.ts +0 -1130
- package/src/daemon/index.ts +0 -242
- package/src/daemon/wallet/index.ts +0 -122
- package/src/index.ts +0 -4
- package/src/integrations/index.ts +0 -76
- package/src/tui/usage/index.ts +0 -1
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,15 @@ import {
|
|
|
6
6
|
ensureDaemonRunning,
|
|
7
7
|
isDaemonRunning,
|
|
8
8
|
loadConfig,
|
|
9
|
+
getDaemonBaseUrl,
|
|
10
|
+
getNpubSuffix,
|
|
11
|
+
getUserNpub,
|
|
9
12
|
} from "./utils/daemon-client";
|
|
13
|
+
import {
|
|
14
|
+
listClientsAction,
|
|
15
|
+
deleteClientAction,
|
|
16
|
+
addClientAction,
|
|
17
|
+
} from "./utils/clients";
|
|
10
18
|
import { existsSync, mkdirSync } from "fs";
|
|
11
19
|
import { execSync } from "child_process";
|
|
12
20
|
import {
|
|
@@ -18,18 +26,16 @@ import {
|
|
|
18
26
|
type RoutstrdConfig,
|
|
19
27
|
} from "./utils/config";
|
|
20
28
|
import { logger } from "./utils/logger";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
CLIENT_CONFIGS,
|
|
24
|
-
CLIENT_INTEGRATIONS,
|
|
25
|
-
} from "./integrations";
|
|
26
|
-
import { createSdkStore } from "@routstr/sdk";
|
|
27
|
-
import { createBunSqliteDriver } from "@routstr/sdk/storage";
|
|
29
|
+
import { setupIntegration, runIntegrationsForClients } from "./integrations";
|
|
30
|
+
import { getClientsList } from "./utils/clients";
|
|
28
31
|
import * as QRCode from "qrcode";
|
|
32
|
+
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
|
33
|
+
import { generateSecretKey, nip19 } from "nostr-tools";
|
|
29
34
|
import {
|
|
30
35
|
isCocodInstalled,
|
|
31
36
|
resolveCocodExecutable,
|
|
32
37
|
} from "./daemon/wallet/cocod-client";
|
|
38
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
33
39
|
|
|
34
40
|
type RoutstrModel = {
|
|
35
41
|
id: string;
|
|
@@ -52,8 +58,6 @@ type UsageEntry = {
|
|
|
52
58
|
client?: string;
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
const cliVersion = "0.1.1";
|
|
56
|
-
|
|
57
61
|
function parsePositiveIntOrExit(value: string, fieldName: string): number {
|
|
58
62
|
const parsed = Number.parseInt(value, 10);
|
|
59
63
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -80,10 +84,13 @@ async function printLightningInvoice(invoice: string): Promise<void> {
|
|
|
80
84
|
async function installCocodOrExit(): Promise<void> {
|
|
81
85
|
logger.log("cocod not found. Installing globally with bun...");
|
|
82
86
|
|
|
83
|
-
const installProc = Bun.spawn(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
const installProc = Bun.spawn(
|
|
88
|
+
["bun", "install", "--global", "@routstr/cocod"],
|
|
89
|
+
{
|
|
90
|
+
stdout: "inherit",
|
|
91
|
+
stderr: "inherit",
|
|
92
|
+
},
|
|
93
|
+
);
|
|
87
94
|
|
|
88
95
|
const installCode = await installProc.exited;
|
|
89
96
|
if (installCode !== 0 || !(await isCocodInstalled())) {
|
|
@@ -96,6 +103,16 @@ async function installCocodOrExit(): Promise<void> {
|
|
|
96
103
|
logger.log("cocod installed successfully.");
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
async function requireLocalDaemon(): Promise<void> {
|
|
107
|
+
const config = await loadConfig();
|
|
108
|
+
if (config.daemonUrl) {
|
|
109
|
+
console.error(
|
|
110
|
+
`This command is not available when using a remote daemon (${config.daemonUrl}).`,
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
async function initDaemon(): Promise<void> {
|
|
100
117
|
logger.log("Initializing routstrd...");
|
|
101
118
|
|
|
@@ -188,11 +205,7 @@ async function initDaemon(): Promise<void> {
|
|
|
188
205
|
|
|
189
206
|
await startDaemon({ port: String(config.port || 8008) });
|
|
190
207
|
|
|
191
|
-
|
|
192
|
-
const sqliteDriver = await createBunSqliteDriver(DB_PATH);
|
|
193
|
-
const { store } = await createSdkStore({ driver: sqliteDriver });
|
|
194
|
-
|
|
195
|
-
await setupIntegration(config, store);
|
|
208
|
+
await setupIntegration(config);
|
|
196
209
|
|
|
197
210
|
logger.log("\nInitialization complete!");
|
|
198
211
|
logger.log(
|
|
@@ -209,28 +222,33 @@ async function initDaemon(): Promise<void> {
|
|
|
209
222
|
program
|
|
210
223
|
.name("routstrd")
|
|
211
224
|
.description("Routstr daemon - Manage routstr processes")
|
|
212
|
-
.version(
|
|
225
|
+
.version(packageJson.version, "--version", "output the version number");
|
|
213
226
|
|
|
214
227
|
program
|
|
215
228
|
.command("refund")
|
|
216
229
|
.description("Refund pending tokens and API keys to a specified mint")
|
|
217
|
-
.option(
|
|
230
|
+
.option(
|
|
231
|
+
"-m, --mint-url <mintUrl>",
|
|
232
|
+
"Mint URL to refund to (defaults to first mint in wallet)",
|
|
233
|
+
)
|
|
218
234
|
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
219
235
|
.action(async (options: { mintUrl?: string; yes: boolean }) => {
|
|
220
|
-
|
|
236
|
+
await ensureDaemonRunning();
|
|
221
237
|
|
|
222
238
|
let mintUrl = options.mintUrl;
|
|
223
239
|
if (!mintUrl) {
|
|
224
|
-
const
|
|
225
|
-
const balanceResult = (await balanceResponse.json()) as {
|
|
226
|
-
output?: { balances?: Record<string, number> };
|
|
227
|
-
error?: string;
|
|
228
|
-
};
|
|
240
|
+
const balanceResult = await callDaemon("/balance");
|
|
229
241
|
if (balanceResult.error) {
|
|
230
242
|
console.log(balanceResult.error);
|
|
231
243
|
process.exit(1);
|
|
232
244
|
}
|
|
233
|
-
const balances =
|
|
245
|
+
const balances = (
|
|
246
|
+
balanceResult.output as
|
|
247
|
+
| {
|
|
248
|
+
balances?: Record<string, number>;
|
|
249
|
+
}
|
|
250
|
+
| undefined
|
|
251
|
+
)?.balances;
|
|
234
252
|
if (!balances || Object.keys(balances).length === 0) {
|
|
235
253
|
console.log("No mint URLs found in wallet balance");
|
|
236
254
|
process.exit(1);
|
|
@@ -240,44 +258,40 @@ program
|
|
|
240
258
|
}
|
|
241
259
|
|
|
242
260
|
try {
|
|
243
|
-
const
|
|
261
|
+
const result = await callDaemon("/refund", {
|
|
244
262
|
method: "POST",
|
|
245
|
-
|
|
246
|
-
body: JSON.stringify({ mintUrl }),
|
|
263
|
+
body: { mintUrl },
|
|
247
264
|
});
|
|
248
265
|
|
|
249
|
-
if (!response.ok) {
|
|
250
|
-
const errorData = (await response.json()) as { error?: string };
|
|
251
|
-
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const result = (await response.json()) as {
|
|
255
|
-
output?: {
|
|
256
|
-
message: string;
|
|
257
|
-
pendingTokens: number;
|
|
258
|
-
apiKeys: number;
|
|
259
|
-
results: Array<{ baseUrl: string; success: boolean }>;
|
|
260
|
-
};
|
|
261
|
-
error?: string;
|
|
262
|
-
};
|
|
263
|
-
|
|
264
266
|
if (result.error) {
|
|
265
267
|
console.log(result.error);
|
|
266
268
|
process.exit(1);
|
|
267
269
|
}
|
|
268
270
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
const output = result.output as
|
|
272
|
+
| {
|
|
273
|
+
message: string;
|
|
274
|
+
pendingTokens: number;
|
|
275
|
+
apiKeys: number;
|
|
276
|
+
results: Array<{ baseUrl: string; success: boolean }>;
|
|
277
|
+
}
|
|
278
|
+
| undefined;
|
|
279
|
+
|
|
280
|
+
if (output) {
|
|
281
|
+
console.log(output.message);
|
|
282
|
+
console.log(`\nPending tokens: ${output.pendingTokens}`);
|
|
283
|
+
console.log(`API keys: ${output.apiKeys}`);
|
|
273
284
|
console.log("\nResults:");
|
|
274
|
-
for (const r of
|
|
285
|
+
for (const r of output.results) {
|
|
275
286
|
console.log(` - ${r.baseUrl}: ${r.success ? "success" : "failed"}`);
|
|
276
287
|
}
|
|
277
288
|
}
|
|
278
289
|
} catch (error) {
|
|
279
290
|
const message = (error as Error).message;
|
|
280
|
-
if (
|
|
291
|
+
if (
|
|
292
|
+
message?.includes("fetch failed") ||
|
|
293
|
+
message?.includes("Connection refused")
|
|
294
|
+
) {
|
|
281
295
|
console.error("Daemon is not running");
|
|
282
296
|
process.exit(1);
|
|
283
297
|
}
|
|
@@ -286,6 +300,53 @@ program
|
|
|
286
300
|
}
|
|
287
301
|
});
|
|
288
302
|
|
|
303
|
+
// Remote - configure a remote daemon URL
|
|
304
|
+
program
|
|
305
|
+
.command("remote <url>")
|
|
306
|
+
.description("Configure a remote daemon URL")
|
|
307
|
+
.action(async (url: string) => {
|
|
308
|
+
try {
|
|
309
|
+
new URL(url);
|
|
310
|
+
} catch {
|
|
311
|
+
console.error(`Invalid URL: ${url}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
316
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const config = await loadConfig();
|
|
320
|
+
const updates: Partial<RoutstrdConfig> = { daemonUrl: url };
|
|
321
|
+
let generatedNpub: string | undefined;
|
|
322
|
+
|
|
323
|
+
if (!config.nsec) {
|
|
324
|
+
const secretKey = generateSecretKey();
|
|
325
|
+
const nsec = nip19.nsecEncode(secretKey);
|
|
326
|
+
const npub = npubFromSecretKey(secretKey);
|
|
327
|
+
updates.nsec = nsec;
|
|
328
|
+
generatedNpub = npub;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const updatedConfig: RoutstrdConfig = {
|
|
332
|
+
...config,
|
|
333
|
+
...updates,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
|
|
337
|
+
|
|
338
|
+
console.log(`Remote daemon URL set to: ${url}`);
|
|
339
|
+
if (generatedNpub) {
|
|
340
|
+
console.log(
|
|
341
|
+
`\nA new Nostr identity has been generated for remote authentication.`,
|
|
342
|
+
);
|
|
343
|
+
console.log(`Your npub: ${generatedNpub}`);
|
|
344
|
+
console.log(
|
|
345
|
+
`You can view it in the config file at: ${CONFIG_FILE}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
289
350
|
// Onboard - initialize the daemon
|
|
290
351
|
program
|
|
291
352
|
.command("onboard")
|
|
@@ -293,6 +354,7 @@ program
|
|
|
293
354
|
"Initialize routstrd (creates config directory and initializes cocod)",
|
|
294
355
|
)
|
|
295
356
|
.action(async () => {
|
|
357
|
+
await requireLocalDaemon();
|
|
296
358
|
await initDaemon();
|
|
297
359
|
});
|
|
298
360
|
|
|
@@ -303,6 +365,7 @@ program
|
|
|
303
365
|
.option("--port <port>", "Port to listen on")
|
|
304
366
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
305
367
|
.action(async (options: { port?: string; provider?: string }) => {
|
|
368
|
+
await requireLocalDaemon();
|
|
306
369
|
const config = await loadConfig();
|
|
307
370
|
if (!(await isCocodInstalled(config.cocodPath))) {
|
|
308
371
|
const installHint = config.cocodPath
|
|
@@ -423,6 +486,34 @@ program
|
|
|
423
486
|
await handleDaemonCommand("/ping");
|
|
424
487
|
});
|
|
425
488
|
|
|
489
|
+
// Refresh - refresh models and integrations
|
|
490
|
+
program
|
|
491
|
+
.command("refresh")
|
|
492
|
+
.description("Refresh routstr21 models and client integrations")
|
|
493
|
+
.action(async () => {
|
|
494
|
+
await ensureDaemonRunning();
|
|
495
|
+
const config = await loadConfig();
|
|
496
|
+
|
|
497
|
+
// Refresh models via daemon API
|
|
498
|
+
console.log("Refreshing routstr21 models...");
|
|
499
|
+
const result = await callDaemon("/v1/models?refresh=true");
|
|
500
|
+
if (result.error) {
|
|
501
|
+
console.log(`Model refresh failed: ${result.error}`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
console.log("Models refreshed.");
|
|
505
|
+
|
|
506
|
+
// Refresh integrations for all clients
|
|
507
|
+
const clients = await getClientsList();
|
|
508
|
+
if (clients.length > 0) {
|
|
509
|
+
console.log(`Refreshing ${clients.length} client integration(s)...`);
|
|
510
|
+
await runIntegrationsForClients(clients, config);
|
|
511
|
+
console.log("Client integrations refreshed.");
|
|
512
|
+
} else {
|
|
513
|
+
console.log("No clients to refresh.");
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
426
517
|
// Models - list routstr21 models
|
|
427
518
|
program
|
|
428
519
|
.command("models")
|
|
@@ -434,27 +525,31 @@ program
|
|
|
434
525
|
|
|
435
526
|
if (options.model) {
|
|
436
527
|
// Show providers for specific model
|
|
437
|
-
const result = await callDaemon(
|
|
528
|
+
const result = await callDaemon(
|
|
529
|
+
`/models/${encodeURIComponent(options.model)}/providers`,
|
|
530
|
+
);
|
|
438
531
|
if (result.error) {
|
|
439
532
|
console.log(result.error);
|
|
440
533
|
process.exit(1);
|
|
441
534
|
}
|
|
442
535
|
|
|
443
|
-
const modelData = result.output as
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
536
|
+
const modelData = result.output as
|
|
537
|
+
| {
|
|
538
|
+
id: string;
|
|
539
|
+
name?: string;
|
|
540
|
+
description?: string;
|
|
541
|
+
context_length?: number;
|
|
542
|
+
providers: Array<{
|
|
543
|
+
baseUrl: string;
|
|
544
|
+
pricing: {
|
|
545
|
+
prompt: number;
|
|
546
|
+
completion: number;
|
|
547
|
+
request: number;
|
|
548
|
+
max_cost: number;
|
|
549
|
+
};
|
|
550
|
+
}>;
|
|
551
|
+
}
|
|
552
|
+
| undefined;
|
|
458
553
|
|
|
459
554
|
if (!modelData) {
|
|
460
555
|
console.log("Model not found");
|
|
@@ -466,15 +561,25 @@ program
|
|
|
466
561
|
console.log(` ${modelData.description}`);
|
|
467
562
|
}
|
|
468
563
|
if (modelData.context_length) {
|
|
469
|
-
console.log(
|
|
564
|
+
console.log(
|
|
565
|
+
` Context: ${modelData.context_length.toLocaleString()} tokens`,
|
|
566
|
+
);
|
|
470
567
|
}
|
|
471
568
|
console.log(`\n Providers (${modelData.providers.length}):`);
|
|
472
569
|
for (const provider of modelData.providers) {
|
|
473
570
|
console.log(`\n ${provider.baseUrl}`);
|
|
474
|
-
console.log(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
console.log(
|
|
571
|
+
console.log(
|
|
572
|
+
` Prompt: ${(provider.pricing.prompt * 1000000).toFixed(2)} sats/M tokens`,
|
|
573
|
+
);
|
|
574
|
+
console.log(
|
|
575
|
+
` Completion: ${(provider.pricing.completion * 1000000).toFixed(2)} sats/M tokens`,
|
|
576
|
+
);
|
|
577
|
+
console.log(
|
|
578
|
+
` Request: ${provider.pricing.request.toFixed(2)} sats`,
|
|
579
|
+
);
|
|
580
|
+
console.log(
|
|
581
|
+
` Max cost: ${provider.pricing.max_cost.toFixed(2)} sats`,
|
|
582
|
+
);
|
|
478
583
|
}
|
|
479
584
|
console.log("");
|
|
480
585
|
return;
|
|
@@ -499,7 +604,9 @@ program
|
|
|
499
604
|
console.log("No routstr21 models found");
|
|
500
605
|
} else {
|
|
501
606
|
console.log(`\nFound ${models.length} routstr21 models:`);
|
|
502
|
-
console.log(
|
|
607
|
+
console.log(
|
|
608
|
+
"(Use 'routstrd models -m <model_id>' to see providers and pricing)\n",
|
|
609
|
+
);
|
|
503
610
|
models.forEach((model, i) => {
|
|
504
611
|
const details = [
|
|
505
612
|
model.name && model.name !== model.id ? model.name : null,
|
|
@@ -507,7 +614,9 @@ program
|
|
|
507
614
|
]
|
|
508
615
|
.filter(Boolean)
|
|
509
616
|
.join(" - ");
|
|
510
|
-
console.log(
|
|
617
|
+
console.log(
|
|
618
|
+
` ${String(i + 1).padStart(2)}. ${model.id}${details ? ` (${details})` : ""}`,
|
|
619
|
+
);
|
|
511
620
|
});
|
|
512
621
|
console.log("");
|
|
513
622
|
}
|
|
@@ -694,72 +803,14 @@ clientsCmd
|
|
|
694
803
|
.command("list")
|
|
695
804
|
.description("List all clients")
|
|
696
805
|
.action(async () => {
|
|
697
|
-
await
|
|
698
|
-
|
|
699
|
-
const result = await callDaemon("/clients");
|
|
700
|
-
if (result.error) {
|
|
701
|
-
console.log(result.error);
|
|
702
|
-
process.exit(1);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const output = result.output as
|
|
706
|
-
| {
|
|
707
|
-
clients: Array<{
|
|
708
|
-
id: string;
|
|
709
|
-
name: string;
|
|
710
|
-
apiKey: string;
|
|
711
|
-
createdAt: number;
|
|
712
|
-
lastUsed?: number | null;
|
|
713
|
-
}>;
|
|
714
|
-
totalCount: number;
|
|
715
|
-
}
|
|
716
|
-
| undefined;
|
|
717
|
-
|
|
718
|
-
if (!output?.clients || output.clients.length === 0) {
|
|
719
|
-
console.log("No clients found.");
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
console.log(`Clients (${output.totalCount} total):\n`);
|
|
724
|
-
for (const client of output.clients) {
|
|
725
|
-
const createdAt = new Date(client.createdAt).toISOString();
|
|
726
|
-
const lastUsed = client.lastUsed
|
|
727
|
-
? new Date(client.lastUsed).toISOString()
|
|
728
|
-
: "never";
|
|
729
|
-
console.log(` ${client.id}`);
|
|
730
|
-
console.log(` Name: ${client.name}`);
|
|
731
|
-
console.log(` API Key: ${client.apiKey}`);
|
|
732
|
-
console.log(` Created: ${createdAt}`);
|
|
733
|
-
console.log("");
|
|
734
|
-
}
|
|
806
|
+
await listClientsAction();
|
|
735
807
|
});
|
|
736
808
|
|
|
737
809
|
clientsCmd
|
|
738
810
|
.command("delete <id>")
|
|
739
811
|
.description("Delete a client by its ID")
|
|
740
812
|
.action(async (id: string) => {
|
|
741
|
-
await
|
|
742
|
-
|
|
743
|
-
const result = await callDaemon("/clients/delete", {
|
|
744
|
-
method: "POST",
|
|
745
|
-
body: { id },
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
if (result.error) {
|
|
749
|
-
console.log(result.error);
|
|
750
|
-
process.exit(1);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
const output = result.output as
|
|
754
|
-
| {
|
|
755
|
-
message: string;
|
|
756
|
-
id: string;
|
|
757
|
-
}
|
|
758
|
-
| undefined;
|
|
759
|
-
|
|
760
|
-
if (output) {
|
|
761
|
-
console.log(output.message);
|
|
762
|
-
}
|
|
813
|
+
await deleteClientAction(id);
|
|
763
814
|
});
|
|
764
815
|
|
|
765
816
|
clientsCmd
|
|
@@ -770,98 +821,118 @@ clientsCmd
|
|
|
770
821
|
.option("--openclaw", "Set up OpenClaw integration")
|
|
771
822
|
.option("--pi-agent", "Set up Pi Agent integration")
|
|
772
823
|
.option("--claude-code", "Set up Claude Code integration")
|
|
773
|
-
.action(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (options.opencode) integrationKeys.push("opencode");
|
|
785
|
-
if (options.openclaw) integrationKeys.push("openclaw");
|
|
786
|
-
if (options.piAgent) integrationKeys.push("pi-agent");
|
|
787
|
-
if (options.claudeCode) integrationKeys.push("claude-code");
|
|
788
|
-
|
|
789
|
-
if (integrationKeys.length > 0) {
|
|
790
|
-
const sqliteDriver = await createBunSqliteDriver(DB_PATH);
|
|
791
|
-
const { store } = await createSdkStore({ driver: sqliteDriver });
|
|
792
|
-
|
|
793
|
-
for (const key of integrationKeys) {
|
|
794
|
-
const integrationFn = CLIENT_INTEGRATIONS[key];
|
|
795
|
-
const integrationConfig = CLIENT_CONFIGS[key];
|
|
796
|
-
if (!integrationFn || !integrationConfig) continue;
|
|
797
|
-
|
|
798
|
-
try {
|
|
799
|
-
await integrationFn(config, store, integrationConfig);
|
|
800
|
-
} catch (error) {
|
|
801
|
-
logger.error(
|
|
802
|
-
`Failed to set up ${integrationConfig.name} integration:`,
|
|
803
|
-
error,
|
|
804
|
-
);
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
824
|
+
.action(
|
|
825
|
+
async (options: {
|
|
826
|
+
name?: string;
|
|
827
|
+
opencode?: boolean;
|
|
828
|
+
openclaw?: boolean;
|
|
829
|
+
piAgent?: boolean;
|
|
830
|
+
claudeCode?: boolean;
|
|
831
|
+
}) => {
|
|
832
|
+
await addClientAction(options);
|
|
833
|
+
},
|
|
834
|
+
);
|
|
807
835
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (client) {
|
|
813
|
-
console.log(`\n ${integrationConfig.name}:`);
|
|
814
|
-
console.log(` Client ID: ${client.clientId}`);
|
|
815
|
-
console.log(` API Key: ${client.apiKey}`);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
836
|
+
// Npubs - manage admin npubs
|
|
837
|
+
const npubsCmd = program
|
|
838
|
+
.command("npubs")
|
|
839
|
+
.description("Manage admin npubs on the daemon");
|
|
818
840
|
|
|
841
|
+
npubsCmd
|
|
842
|
+
.command("list")
|
|
843
|
+
.description("List configured admin npubs")
|
|
844
|
+
.action(async () => {
|
|
845
|
+
await ensureDaemonRunning();
|
|
846
|
+
const config = await loadConfig();
|
|
847
|
+
const userNpub = getUserNpub(config);
|
|
848
|
+
const result = await callDaemon("/npubs");
|
|
849
|
+
if (result.error) {
|
|
850
|
+
console.log(result.error);
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
// Handle both wrapped { output: { npubs } } and direct { npubs } response formats
|
|
854
|
+
const data = (result.output as { npubs?: string[] } | undefined)?.npubs
|
|
855
|
+
? result.output
|
|
856
|
+
: result;
|
|
857
|
+
const npubs = (data as { npubs?: string[] } | undefined)?.npubs ?? [];
|
|
858
|
+
if (npubs.length === 0) {
|
|
859
|
+
console.log("No admin npubs configured.");
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
console.log(`Admin npubs (${npubs.length}):`);
|
|
863
|
+
let found = false;
|
|
864
|
+
for (const npub of npubs) {
|
|
865
|
+
const marker = npub === userNpub ? " → you" : "";
|
|
866
|
+
if (npub === userNpub) found = true;
|
|
867
|
+
console.log(`- ${npub}${marker}`);
|
|
868
|
+
}
|
|
869
|
+
if (userNpub && !found) {
|
|
870
|
+
console.log("");
|
|
819
871
|
console.log(
|
|
820
|
-
|
|
872
|
+
"Your npub is not in the admin list. Ask the admin to add your npub:",
|
|
821
873
|
);
|
|
822
|
-
|
|
874
|
+
console.log(` ${userNpub}`);
|
|
823
875
|
}
|
|
876
|
+
});
|
|
824
877
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
878
|
+
npubsCmd
|
|
879
|
+
.command("add <npub>")
|
|
880
|
+
.description("Add an admin npub (hex pubkey or npub1...)")
|
|
881
|
+
.action(async (npubArg: string) => {
|
|
882
|
+
await ensureDaemonRunning();
|
|
883
|
+
const normalized = normalizeNostrPubkey(npubArg);
|
|
884
|
+
if (!normalized) {
|
|
885
|
+
console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
|
|
829
886
|
process.exit(1);
|
|
830
887
|
}
|
|
831
|
-
|
|
832
|
-
const result = await callDaemon("/clients/add", {
|
|
888
|
+
const result = await callDaemon("/npubs", {
|
|
833
889
|
method: "POST",
|
|
834
|
-
body: {
|
|
890
|
+
body: { npub: npubFromPubkey(normalized) },
|
|
835
891
|
});
|
|
836
|
-
|
|
837
892
|
if (result.error) {
|
|
838
893
|
console.log(result.error);
|
|
839
894
|
process.exit(1);
|
|
840
895
|
}
|
|
841
|
-
|
|
842
896
|
const output = result.output as
|
|
843
|
-
| {
|
|
844
|
-
message: string;
|
|
845
|
-
client: {
|
|
846
|
-
id: string;
|
|
847
|
-
name: string;
|
|
848
|
-
apiKey: string;
|
|
849
|
-
createdAt: number;
|
|
850
|
-
};
|
|
851
|
-
}
|
|
897
|
+
| { npub?: string; added?: boolean; error?: string }
|
|
852
898
|
| undefined;
|
|
853
|
-
|
|
854
|
-
if (output) {
|
|
855
|
-
console.log(output.message);
|
|
856
|
-
console.log(`\n ID: ${output.client.id}`);
|
|
857
|
-
console.log(` Name: ${output.client.name}`);
|
|
858
|
-
console.log(` API Key: ${output.client.apiKey}`);
|
|
899
|
+
if (output?.npub) {
|
|
859
900
|
console.log(
|
|
860
|
-
|
|
901
|
+
`${output.added ? "Added" : "Already configured"} admin npub: ${output.npub}`,
|
|
861
902
|
);
|
|
862
903
|
}
|
|
863
904
|
});
|
|
864
905
|
|
|
906
|
+
npubsCmd
|
|
907
|
+
.command("delete <npub>")
|
|
908
|
+
.description("Delete an admin npub (hex pubkey or npub1...)")
|
|
909
|
+
.action(async (npubArg: string) => {
|
|
910
|
+
await ensureDaemonRunning();
|
|
911
|
+
const normalized = normalizeNostrPubkey(npubArg);
|
|
912
|
+
if (!normalized) {
|
|
913
|
+
console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
const result = await callDaemon(
|
|
917
|
+
`/npubs/${encodeURIComponent(npubFromPubkey(normalized))}`,
|
|
918
|
+
{
|
|
919
|
+
method: "DELETE",
|
|
920
|
+
},
|
|
921
|
+
);
|
|
922
|
+
if (result.error) {
|
|
923
|
+
console.log(result.error);
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
const output = result.output as
|
|
927
|
+
| { removed?: boolean; error?: string }
|
|
928
|
+
| undefined;
|
|
929
|
+
console.log(
|
|
930
|
+
output?.removed
|
|
931
|
+
? "Removed admin npub."
|
|
932
|
+
: "Admin npub was not configured.",
|
|
933
|
+
);
|
|
934
|
+
});
|
|
935
|
+
|
|
865
936
|
// Monitor - interactive TUI
|
|
866
937
|
program
|
|
867
938
|
.command("monitor")
|
|
@@ -1104,6 +1175,7 @@ serviceCmd
|
|
|
1104
1175
|
.command("install")
|
|
1105
1176
|
.description("Install and start routstrd using PM2 for persistence")
|
|
1106
1177
|
.action(async () => {
|
|
1178
|
+
await requireLocalDaemon();
|
|
1107
1179
|
// 1. Check if PM2 is installed
|
|
1108
1180
|
try {
|
|
1109
1181
|
execSync("pm2 -v", { stdio: "ignore" });
|
|
@@ -1112,7 +1184,9 @@ serviceCmd
|
|
|
1112
1184
|
try {
|
|
1113
1185
|
execSync("bun install -g pm2", { stdio: "inherit" });
|
|
1114
1186
|
} catch (err) {
|
|
1115
|
-
console.error(
|
|
1187
|
+
console.error(
|
|
1188
|
+
"Failed to install PM2. Please install it manually: bun install -g pm2",
|
|
1189
|
+
);
|
|
1116
1190
|
process.exit(1);
|
|
1117
1191
|
}
|
|
1118
1192
|
}
|
|
@@ -1126,11 +1200,17 @@ serviceCmd
|
|
|
1126
1200
|
} catch (e) {
|
|
1127
1201
|
// Fallback for some bundling scenarios
|
|
1128
1202
|
const path = require("path");
|
|
1129
|
-
daemonPath = path.join(
|
|
1203
|
+
daemonPath = path.join(
|
|
1204
|
+
path.dirname(import.meta.url).replace("file://", ""),
|
|
1205
|
+
"daemon",
|
|
1206
|
+
"index.js",
|
|
1207
|
+
);
|
|
1130
1208
|
}
|
|
1131
1209
|
|
|
1132
1210
|
if (!existsSync(daemonPath)) {
|
|
1133
|
-
console.error(
|
|
1211
|
+
console.error(
|
|
1212
|
+
`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`,
|
|
1213
|
+
);
|
|
1134
1214
|
process.exit(1);
|
|
1135
1215
|
}
|
|
1136
1216
|
|
|
@@ -1185,6 +1265,7 @@ program
|
|
|
1185
1265
|
.option("--port <port>", "Port to listen on")
|
|
1186
1266
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
1187
1267
|
.action(async (options: { port?: string; provider?: string }) => {
|
|
1268
|
+
await requireLocalDaemon();
|
|
1188
1269
|
const config = await loadConfig();
|
|
1189
1270
|
const wasRunning = await isDaemonRunning();
|
|
1190
1271
|
|
|
@@ -1222,6 +1303,7 @@ program
|
|
|
1222
1303
|
.command("mode")
|
|
1223
1304
|
.description("Set the client mode (lazyrefund/apikeys or xcashu)")
|
|
1224
1305
|
.action(async () => {
|
|
1306
|
+
await requireLocalDaemon();
|
|
1225
1307
|
const config = await loadConfig();
|
|
1226
1308
|
const currentMode = config.mode || "apikeys";
|
|
1227
1309
|
|
|
@@ -1312,6 +1394,7 @@ program
|
|
|
1312
1394
|
.option("-f, --follow", "Follow log output", false)
|
|
1313
1395
|
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
1314
1396
|
.action(async (options: { follow: boolean; lines: string }) => {
|
|
1397
|
+
await requireLocalDaemon();
|
|
1315
1398
|
const todayFile = getLogFileForDate();
|
|
1316
1399
|
const yesterday = new Date();
|
|
1317
1400
|
yesterday.setDate(yesterday.getDate() - 1);
|