routstrd 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- setupIntegration,
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(["bun", "install", "--global", "@routstr/cocod"], {
84
- stdout: "inherit",
85
- stderr: "inherit",
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
- // Create SDK store for integrations
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(cliVersion, "--version", "output the version number");
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("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)")
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
- const config = await loadConfig();
236
+ await ensureDaemonRunning();
221
237
 
222
238
  let mintUrl = options.mintUrl;
223
239
  if (!mintUrl) {
224
- const balanceResponse = await fetch(`http://localhost:${config.port}/balance`);
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 = balanceResult.output?.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 response = await fetch(`http://localhost:${config.port}/refund`, {
261
+ const result = await callDaemon("/refund", {
244
262
  method: "POST",
245
- headers: { "Content-Type": "application/json" },
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
- if (result.output) {
270
- console.log(result.output.message);
271
- console.log(`\nPending tokens: ${result.output.pendingTokens}`);
272
- console.log(`API keys: ${result.output.apiKeys}`);
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 result.output.results) {
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 (message?.includes("fetch failed") || message?.includes("Connection refused")) {
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(`/models/${encodeURIComponent(options.model)}/providers`);
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
- id: string;
445
- name?: string;
446
- description?: string;
447
- context_length?: number;
448
- providers: Array<{
449
- baseUrl: string;
450
- pricing: {
451
- prompt: number;
452
- completion: number;
453
- request: number;
454
- max_cost: number;
455
- };
456
- }>;
457
- } | undefined;
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(` Context: ${modelData.context_length.toLocaleString()} tokens`);
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(` Prompt: ${(provider.pricing.prompt * 1000000).toFixed(2)} sats/M tokens`);
475
- console.log(` Completion: ${(provider.pricing.completion * 1000000).toFixed(2)} sats/M tokens`);
476
- console.log(` Request: ${provider.pricing.request.toFixed(2)} sats`);
477
- console.log(` Max cost: ${provider.pricing.max_cost.toFixed(2)} sats`);
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("(Use 'routstrd models -m <model_id>' to see providers and pricing)\n");
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(` ${String(i + 1).padStart(2)}. ${model.id}${details ? ` (${details})` : ""}`);
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 ensureDaemonRunning();
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 ensureDaemonRunning();
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(async (options: {
774
- name?: string;
775
- opencode?: boolean;
776
- openclaw?: boolean;
777
- piAgent?: boolean;
778
- claudeCode?: boolean;
779
- }) => {
780
- await ensureDaemonRunning();
781
- const config = await loadConfig();
782
-
783
- const integrationKeys: string[] = [];
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
- const state = store.getState();
809
- const client = (state.clientIds || []).find(
810
- (c: { clientId: string }) => c.clientId === key,
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
- `\n Access Routstr at: http://localhost:${config.port || 8008}/v1`,
872
+ "Your npub is not in the admin list. Ask the admin to add your npub:",
821
873
  );
822
- return;
874
+ console.log(` ${userNpub}`);
823
875
  }
876
+ });
824
877
 
825
- if (!options.name) {
826
- console.error(
827
- "error: required option '-n, --name <name>' not specified",
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: { name: options.name },
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
- `\n Access Routstr at: http://localhost:${config.port || 8008}/v1`,
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("Failed to install PM2. Please install it manually: bun install -g pm2");
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(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
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(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
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);