routstrd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts ADDED
@@ -0,0 +1,650 @@
1
+ import { startDaemon } from "./start-daemon";
2
+ import {
3
+ program,
4
+ handleDaemonCommand,
5
+ callDaemon,
6
+ ensureDaemonRunning,
7
+ isDaemonRunning,
8
+ loadConfig,
9
+ } from "./cli-shared";
10
+ import { existsSync, mkdirSync } from "fs";
11
+ import {
12
+ CONFIG_DIR,
13
+ DB_PATH,
14
+ CONFIG_FILE,
15
+ DEFAULT_CONFIG,
16
+ LOG_FILE,
17
+ type RoutstrdConfig,
18
+ } from "./utils/config";
19
+ import { logger } from "./utils/logger";
20
+ import { setupIntegration } from "./integrations";
21
+ import { createSdkStore } from "@routstr/sdk";
22
+ import { createBunSqliteDriver } from "@routstr/sdk/storage";
23
+
24
+ type RoutstrModel = {
25
+ id: string;
26
+ name?: string;
27
+ description?: string;
28
+ context_length?: number;
29
+ };
30
+
31
+ type UsageEntry = {
32
+ id: string;
33
+ timestamp: number;
34
+ modelId: string;
35
+ baseUrl: string;
36
+ requestId: string;
37
+ cost: number;
38
+ satsCost: number;
39
+ promptTokens: number;
40
+ completionTokens: number;
41
+ totalTokens: number;
42
+ client?: string;
43
+ };
44
+
45
+ const cliVersion = "0.1.0";
46
+
47
+ async function initDaemon(): Promise<void> {
48
+ logger.log("Initializing routstrd...");
49
+
50
+ if (!(await checkCocodInstalled())) {
51
+ logger.log("cocod not found. Installing globally with bun...");
52
+
53
+ const installProc = Bun.spawn(["bun", "install", "--global", "cocod"], {
54
+ stdout: "inherit",
55
+ stderr: "inherit",
56
+ });
57
+
58
+ const installCode = await installProc.exited;
59
+ if (installCode !== 0 || !(await checkCocodInstalled())) {
60
+ logger.error("Failed to install cocod. Please run 'bun install --global cocod' manually.");
61
+ return;
62
+ }
63
+
64
+ logger.log("cocod installed successfully.");
65
+ }
66
+
67
+ // Create config directory
68
+ if (!existsSync(CONFIG_DIR)) {
69
+ mkdirSync(CONFIG_DIR, { recursive: true });
70
+ logger.log(`Created config directory: ${CONFIG_DIR}`);
71
+ }
72
+
73
+ // Create initial config
74
+ if (!existsSync(CONFIG_FILE)) {
75
+ const config: RoutstrdConfig = {
76
+ ...DEFAULT_CONFIG,
77
+ cocodPath: null,
78
+ };
79
+ await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
80
+ logger.log(`Created config file: ${CONFIG_FILE}`);
81
+ }
82
+
83
+ console.log(`Database will be stored at: ${DB_PATH}`);
84
+ console.log("\nInitializing cocod...");
85
+
86
+ const initProc = Bun.spawn(["cocod", "init"], {
87
+ stdout: "pipe",
88
+ stderr: "pipe",
89
+ });
90
+
91
+ let initStdout = "";
92
+ let initStderr = "";
93
+
94
+ const stdoutDone = initProc.stdout
95
+ ? initProc.stdout.pipeTo(
96
+ new WritableStream<Uint8Array>({
97
+ write(chunk) {
98
+ const text = new TextDecoder().decode(chunk);
99
+ initStdout += text;
100
+ process.stdout.write(text);
101
+ },
102
+ }),
103
+ )
104
+ : Promise.resolve();
105
+
106
+ const stderrDone = initProc.stderr
107
+ ? initProc.stderr.pipeTo(
108
+ new WritableStream<Uint8Array>({
109
+ write(chunk) {
110
+ const text = new TextDecoder().decode(chunk);
111
+ initStderr += text;
112
+ process.stderr.write(text);
113
+ },
114
+ }),
115
+ )
116
+ : Promise.resolve();
117
+
118
+ const [initCode] = await Promise.all([initProc.exited, stdoutDone, stderrDone]);
119
+ const combinedOutput = `${initStdout}\n${initStderr}`.toLowerCase();
120
+ const alreadyInitialized = combinedOutput.includes("already initialized");
121
+
122
+ if (initCode !== 0 && !alreadyInitialized) {
123
+ logger.error("Failed to initialize cocod. Please run 'cocod init' manually.");
124
+ return;
125
+ }
126
+
127
+ if (alreadyInitialized) {
128
+ logger.log("cocod is already initialized.");
129
+ } else {
130
+ logger.log("cocod initialized successfully.");
131
+ }
132
+
133
+ const config = await loadConfig();
134
+ await startDaemon({ port: String(config.port || 8008) });
135
+
136
+ // Create SDK store for integrations
137
+ const sqliteDriver = await createBunSqliteDriver(DB_PATH);
138
+ const { store } = await createSdkStore({ driver: sqliteDriver });
139
+
140
+ await setupIntegration(config, store);
141
+
142
+ logger.log("\nInitialization complete!");
143
+ logger.log("\n use 'cocod receive cashu' or 'cocod receive bolt11 2100' to top up your local wallet!");
144
+ }
145
+
146
+ async function checkCocodInstalled(): Promise<boolean> {
147
+ try {
148
+ const proc = Bun.spawn({
149
+ cmd: ["which", "cocod"],
150
+ stdout: "pipe",
151
+ });
152
+ const code = await proc.exited;
153
+ return code === 0;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ program
160
+ .name("routstrd")
161
+ .description("Routstr daemon - Manage routstr processes")
162
+ .version(cliVersion, "--version", "output the version number");
163
+
164
+ // Onboard - initialize the daemon
165
+ program
166
+ .command("onboard")
167
+ .description("Initialize routstrd (creates config directory and initializes cocod)")
168
+ .action(async () => {
169
+ await initDaemon();
170
+ });
171
+
172
+ // Start - start the background daemon
173
+ program
174
+ .command("start")
175
+ .description("Start the background daemon")
176
+ .option("--port <port>", "Port to listen on")
177
+ .option("-p, --provider <provider>", "Default provider to use")
178
+ .action(async (options: { port?: string; provider?: string }) => {
179
+ if (!(await checkCocodInstalled())) {
180
+ logger.error("cocod is not installed. Run 'routstrd onboard' first to install cocod.");
181
+ process.exit(1);
182
+ }
183
+ const config = await loadConfig();
184
+ await startDaemon({
185
+ port: options.port || String(config.port || 8008),
186
+ provider: options.provider,
187
+ });
188
+ });
189
+
190
+ // Status - check daemon status
191
+ program
192
+ .command("status")
193
+ .description("Check daemon and wallet status")
194
+ .action(async () => {
195
+ const running = await isDaemonRunning();
196
+ if (!running) {
197
+ console.log("Daemon is not running");
198
+ process.exit(1);
199
+ }
200
+
201
+ const result = await callDaemon("/status");
202
+ if (result.error) {
203
+ console.log(result.error);
204
+ process.exit(1);
205
+ }
206
+
207
+ if (result.output !== undefined) {
208
+ if (typeof result.output === "string") {
209
+ console.log(result.output);
210
+ } else {
211
+ try {
212
+ const formatted = JSON.stringify(result.output, null, 2);
213
+ console.log(formatted ?? String(result.output));
214
+ } catch {
215
+ console.log(String(result.output));
216
+ }
217
+ }
218
+ }
219
+ });
220
+
221
+ // Balance - get wallet and API key balances
222
+ program
223
+ .command("balance")
224
+ .description("Get wallet and API key balances")
225
+ .action(async () => {
226
+ await ensureDaemonRunning();
227
+
228
+ const [walletResult, keysResult] = await Promise.all([
229
+ callDaemon("/balance"),
230
+ callDaemon("/keys/balance"),
231
+ ]);
232
+
233
+ console.log("Checking full system balance...\n");
234
+
235
+ console.log("=== Wallet Balance ===");
236
+ let totalWallet = 0;
237
+ if (walletResult.output && typeof walletResult.output === "object" && "balances" in walletResult.output) {
238
+ const balances = (walletResult.output as { balances: Record<string, number> }).balances;
239
+ for (const [mintUrl, balance] of Object.entries(balances)) {
240
+ console.log(` ${mintUrl}: ${balance} sats`);
241
+ totalWallet += balance;
242
+ }
243
+ console.log(` Total: ${totalWallet} sats`);
244
+ } else if (walletResult.error) {
245
+ console.error("Wallet error:", walletResult.error);
246
+ }
247
+
248
+ console.log("\n=== API Keys ===");
249
+ let totalApiKeys = 0;
250
+ if (keysResult.output && typeof keysResult.output === "object" && "keys" in keysResult.output) {
251
+ const keys = (keysResult.output as { keys: Array<{ id: string; name: string; balance: number }> }).keys;
252
+ const apiKeyEntries = keys.filter(k => k.id.startsWith("apikey:"));
253
+ for (const key of apiKeyEntries) {
254
+ const name = key.name.replace("API Key: ", "");
255
+ console.log(` ${name}: ${key.balance} sats`);
256
+ totalApiKeys += key.balance;
257
+ }
258
+ if (apiKeyEntries.length === 0) {
259
+ console.log(" No API keys found");
260
+ } else {
261
+ console.log(` Total: ${totalApiKeys} sats`);
262
+ }
263
+ } else if (keysResult.error) {
264
+ console.error("Keys error:", keysResult.error);
265
+ }
266
+
267
+ console.log("\n=== Summary ===");
268
+ console.log(` Wallet: ${totalWallet} sats | API Keys: ${totalApiKeys} sats`);
269
+ console.log(` Grand Total: ${totalWallet + totalApiKeys} sats`);
270
+ });
271
+
272
+ // Ping
273
+ program
274
+ .command("ping")
275
+ .description("Test connection to the daemon")
276
+ .action(async () => {
277
+ await handleDaemonCommand("/ping");
278
+ });
279
+
280
+ // Models - list routstr21 models
281
+ program
282
+ .command("models")
283
+ .description("List available routstr21 models")
284
+ .option("-r, --refresh", "Force refresh routstr21 models from Nostr", false)
285
+ .action(async (options: { refresh: boolean }) => {
286
+ await ensureDaemonRunning();
287
+
288
+ const result = await callDaemon(
289
+ options.refresh ? "/models?refresh=true" : "/models",
290
+ );
291
+ if (result.error) {
292
+ console.log(result.error);
293
+ process.exit(1);
294
+ }
295
+
296
+ if (result.output && typeof result.output === "object" && "models" in result.output) {
297
+ const models = (result.output as { models: RoutstrModel[] }).models;
298
+ if (models.length === 0) {
299
+ console.log("No routstr21 models found");
300
+ } else {
301
+ console.log(`\nFound ${models.length} routstr21 models:`);
302
+ models.forEach((model, i) => {
303
+ const details = [
304
+ model.name && model.name !== model.id ? model.name : null,
305
+ model.context_length ? `${model.context_length} ctx` : null,
306
+ ].filter(Boolean).join(" - ");
307
+ console.log(`${i + 1}. ${model.id}${details ? ` (${details})` : ""}`);
308
+ });
309
+ }
310
+ }
311
+ });
312
+
313
+ program
314
+ .command("usage")
315
+ .description("Show recent usage logs and total sats cost")
316
+ .option("-n, --limit <number>", "Number of recent usage entries", "10")
317
+ .action(async (options: { limit: string }) => {
318
+ await ensureDaemonRunning();
319
+
320
+ const requested = Number.parseInt(options.limit, 10);
321
+ const limit =
322
+ Number.isFinite(requested) && requested > 0 ? Math.min(requested, 1000) : 10;
323
+
324
+ const result = await callDaemon(`/usage?limit=${limit}`);
325
+ if (result.error) {
326
+ console.log(result.error);
327
+ process.exit(1);
328
+ }
329
+
330
+ const output = result.output as
331
+ | {
332
+ entries?: UsageEntry[];
333
+ totalEntries?: number;
334
+ totalSatsCost?: number;
335
+ recentSatsCost?: number;
336
+ limit?: number;
337
+ }
338
+ | undefined;
339
+
340
+ const entries = output?.entries || [];
341
+ const totalEntries = output?.totalEntries || 0;
342
+ const totalSatsCost = output?.totalSatsCost || 0;
343
+ const recentSatsCost = output?.recentSatsCost || 0;
344
+
345
+ console.log(`Usage entries: showing ${entries.length} of ${totalEntries}`);
346
+ console.log(`Total sats cost (all time): ${totalSatsCost.toFixed(3)} sats`);
347
+ console.log(`Sats cost (shown entries): ${recentSatsCost.toFixed(3)} sats`);
348
+
349
+ if (entries.length === 0) {
350
+ console.log("No usage entries yet.");
351
+ return;
352
+ }
353
+
354
+ console.log("");
355
+ entries.forEach((entry, index) => {
356
+ const time = new Date(entry.timestamp).toISOString();
357
+ const provider = entry.baseUrl || "unknown";
358
+ const reqId = entry.requestId || "unknown";
359
+ const client = entry.client ? ` | client: ${entry.client}` : "";
360
+ console.log(
361
+ `${index + 1}. ${time} | ${entry.modelId} | ${provider} | ${entry.satsCost.toFixed(3)} sats${client}`,
362
+ );
363
+ console.log(
364
+ ` tokens p/c/t: ${entry.promptTokens}/${entry.completionTokens}/${entry.totalTokens} | request: ${reqId}`,
365
+ );
366
+ });
367
+ });
368
+
369
+ // Providers - list and manage providers
370
+ const providersCmd = program
371
+ .command("providers")
372
+ .description("List and manage providers");
373
+
374
+ providersCmd
375
+ .command("list")
376
+ .description("List all providers with their enabled/disabled status")
377
+ .action(async () => {
378
+ await ensureDaemonRunning();
379
+
380
+ const result = await callDaemon("/providers");
381
+ if (result.error) {
382
+ console.log(result.error);
383
+ process.exit(1);
384
+ }
385
+
386
+ const output = result.output as {
387
+ providers: Array<{ index: number; baseUrl: string; disabled: boolean }>;
388
+ disabledCount: number;
389
+ totalCount: number;
390
+ } | undefined;
391
+
392
+ if (!output?.providers) {
393
+ console.log("No providers found.");
394
+ return;
395
+ }
396
+
397
+ console.log(`Providers (${output.totalCount} total, ${output.disabledCount} disabled):\n`);
398
+ for (const provider of output.providers) {
399
+ const status = provider.disabled ? "DISABLED" : "enabled ";
400
+ console.log(` [${provider.index}] ${status} ${provider.baseUrl}`);
401
+ }
402
+ });
403
+
404
+ providersCmd
405
+ .command("disable <indices...>")
406
+ .description("Disable providers by their indices (e.g., routstrd providers disable 0 2 5)")
407
+ .action(async (indices: string[]) => {
408
+ await ensureDaemonRunning();
409
+
410
+ const indexNums = indices.map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n));
411
+ if (indexNums.length === 0) {
412
+ console.log("No valid indices provided.");
413
+ process.exit(1);
414
+ }
415
+
416
+ const result = await callDaemon("/providers/disable", {
417
+ method: "POST",
418
+ body: { indices: indexNums },
419
+ });
420
+
421
+ if (result.error) {
422
+ console.log(result.error);
423
+ process.exit(1);
424
+ }
425
+
426
+ const output = result.output as { message: string; disabled: string[] } | undefined;
427
+ if (output) {
428
+ console.log(output.message);
429
+ for (const url of output.disabled) {
430
+ console.log(` - ${url}`);
431
+ }
432
+ }
433
+ });
434
+
435
+ providersCmd
436
+ .command("enable <indices...>")
437
+ .description("Enable providers by their indices (e.g., routstrd providers enable 0 2 5)")
438
+ .action(async (indices: string[]) => {
439
+ await ensureDaemonRunning();
440
+
441
+ const indexNums = indices.map((s) => parseInt(s, 10)).filter((n) => Number.isFinite(n));
442
+ if (indexNums.length === 0) {
443
+ console.log("No valid indices provided.");
444
+ process.exit(1);
445
+ }
446
+
447
+ const result = await callDaemon("/providers/enable", {
448
+ method: "POST",
449
+ body: { indices: indexNums },
450
+ });
451
+
452
+ if (result.error) {
453
+ console.log(result.error);
454
+ process.exit(1);
455
+ }
456
+
457
+ const output = result.output as { message: string; enabled: string[] } | undefined;
458
+ if (output) {
459
+ console.log(output.message);
460
+ for (const url of output.enabled) {
461
+ console.log(` - ${url}`);
462
+ }
463
+ }
464
+ });
465
+
466
+ // Monitor - interactive TUI
467
+ program
468
+ .command("monitor")
469
+ .description("Open interactive TUI for usage monitoring (htop-like)")
470
+ .action(async () => {
471
+ const { runUsageTui } = await import("./tui/usage/index.ts");
472
+ await runUsageTui();
473
+ });
474
+
475
+ // Stop
476
+ program
477
+ .command("stop")
478
+ .description("Stop the background daemon")
479
+ .action(async () => {
480
+ await handleDaemonCommand("/stop", { method: "POST" });
481
+ });
482
+
483
+ // Restart
484
+ program
485
+ .command("restart")
486
+ .description("Restart the background daemon")
487
+ .option("--port <port>", "Port to listen on")
488
+ .option("-p, --provider <provider>", "Default provider to use")
489
+ .action(async (options: { port?: string; provider?: string }) => {
490
+ const config = await loadConfig();
491
+ const wasRunning = await isDaemonRunning();
492
+
493
+ if (wasRunning) {
494
+ console.log("Stopping daemon...");
495
+ await callDaemon("/stop", { method: "POST" });
496
+
497
+ // Wait for daemon to fully stop
498
+ for (let i = 0; i < 50; i++) {
499
+ await new Promise((resolve) => setTimeout(resolve, 100));
500
+ if (!(await isDaemonRunning())) {
501
+ break;
502
+ }
503
+ }
504
+
505
+ if (await isDaemonRunning()) {
506
+ logger.error("Daemon failed to stop within 5 seconds");
507
+ process.exit(1);
508
+ }
509
+ console.log("Daemon stopped.");
510
+ } else {
511
+ console.log("Daemon was not running.");
512
+ }
513
+
514
+ console.log("Starting daemon...");
515
+ await startDaemon({
516
+ port: options.port || String(config.port || 8008),
517
+ provider: options.provider,
518
+ });
519
+ console.log("Daemon restarted.");
520
+ });
521
+
522
+ // Mode
523
+ program
524
+ .command("mode")
525
+ .description("Set the client mode (xcashu or apikeys)")
526
+ .action(async () => {
527
+ const config = await loadConfig();
528
+ const currentMode = config.mode || "apikeys";
529
+
530
+ console.log("Select client mode:");
531
+ console.log(" 1) apikeys - Pseudonymous accounts are kept with the Routstr nodes for easy topup and refunds.");
532
+ console.log(" 2) xcashu - Balances are never kept with the nodes, all balances are refunded in response.");
533
+ console.log(`\nCurrent mode: ${currentMode}`);
534
+
535
+ const modes: Array<"apikeys" | "xcashu"> = ["apikeys", "xcashu"];
536
+
537
+ const selectedIndex = await new Promise<number>((resolve) => {
538
+ const rl = require("readline").createInterface({
539
+ input: process.stdin,
540
+ output: process.stdout,
541
+ });
542
+ rl.question("\nEnter choice (1-3): ", (answer: string) => {
543
+ rl.close();
544
+ const num = parseInt(answer, 10);
545
+ resolve(Number.isFinite(num) && num >= 1 && num <= 3 ? num - 1 : 0);
546
+ });
547
+ });
548
+
549
+ const selectedMode = modes[selectedIndex];
550
+
551
+ if (selectedMode === currentMode) {
552
+ console.log(`Mode is already set to '${selectedMode}'. No changes made.`);
553
+ return;
554
+ }
555
+
556
+ // Update config
557
+ const updatedConfig: RoutstrdConfig = {
558
+ ...config,
559
+ mode: selectedMode,
560
+ };
561
+ await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
562
+ console.log(`Mode set to '${selectedMode}'. Restarting daemon...`);
563
+
564
+ // Restart daemon
565
+ const wasRunning = await isDaemonRunning();
566
+ if (wasRunning) {
567
+ console.log("Stopping daemon...");
568
+ await callDaemon("/stop", { method: "POST" });
569
+
570
+ for (let i = 0; i < 50; i++) {
571
+ await new Promise((resolve) => setTimeout(resolve, 100));
572
+ if (!(await isDaemonRunning())) {
573
+ break;
574
+ }
575
+ }
576
+
577
+ if (await isDaemonRunning()) {
578
+ logger.error("Daemon failed to stop within 5 seconds");
579
+ process.exit(1);
580
+ }
581
+ console.log("Daemon stopped.");
582
+ }
583
+
584
+ console.log("Starting daemon...");
585
+ await startDaemon({
586
+ port: String(config.port || 8008),
587
+ provider: config.provider || undefined,
588
+ });
589
+ console.log(`Daemon restarted with mode '${selectedMode}'.`);
590
+ });
591
+
592
+ // Logs
593
+ program
594
+ .command("logs")
595
+ .description("View daemon logs")
596
+ .option("-f, --follow", "Follow log output", false)
597
+ .option("-n, --lines <number>", "Number of lines to show", "50")
598
+ .action(async (options: { follow: boolean; lines: string }) => {
599
+ if (!existsSync(LOG_FILE)) {
600
+ console.log("No log file found. Daemon may not have started yet.");
601
+ process.exit(1);
602
+ }
603
+
604
+ const lines = parseInt(options.lines, 10);
605
+
606
+ const readLastLines = async (): Promise<string[]> => {
607
+ const content = await Bun.file(LOG_FILE).text();
608
+ const allLines = content.split("\n").filter(Boolean);
609
+ return allLines.slice(-lines);
610
+ };
611
+
612
+ const printLines = async (): Promise<void> => {
613
+ const lastLines = await readLastLines();
614
+ for (const line of lastLines) {
615
+ console.log(line);
616
+ }
617
+ };
618
+
619
+ if (options.follow) {
620
+ const logFile = Bun.file(LOG_FILE);
621
+ const initialContent = await logFile.text();
622
+ let lastSize = initialContent.length;
623
+
624
+ await printLines();
625
+
626
+ const interval = setInterval(async () => {
627
+ const content = await Bun.file(LOG_FILE).text();
628
+ const currentSize = content.length;
629
+ if (currentSize > lastSize) {
630
+ const allLines = content.split("\n").filter(Boolean);
631
+ const newLines = allLines.slice(Math.floor(lastSize === 0 ? 0 : -1), -1);
632
+ for (const line of newLines) {
633
+ console.log(line);
634
+ }
635
+ lastSize = currentSize;
636
+ }
637
+ }, 1000);
638
+
639
+ process.on("SIGINT", () => {
640
+ clearInterval(interval);
641
+ process.exit(0);
642
+ });
643
+ } else {
644
+ await printLines();
645
+ }
646
+ });
647
+
648
+ export function cli(args: string[]) {
649
+ program.parse(args);
650
+ }
@@ -0,0 +1,19 @@
1
+ export function parseArgs(argv: string[]): {
2
+ port: number;
3
+ provider: string | null;
4
+ } {
5
+ const portFlagIndex = argv.findIndex((arg) => arg === "--port");
6
+ const providerFlagIndex = argv.findIndex(
7
+ (arg) => arg === "--provider" || arg === "-p",
8
+ );
9
+
10
+ const port =
11
+ portFlagIndex !== -1
12
+ ? Number.parseInt(argv[portFlagIndex + 1] || "8008", 10)
13
+ : 8008;
14
+ const providerValue =
15
+ providerFlagIndex !== -1 ? argv[providerFlagIndex + 1] : undefined;
16
+ const provider = providerValue ? providerValue.trim() : null;
17
+
18
+ return { port, provider };
19
+ }
@@ -0,0 +1,36 @@
1
+ import { mkdir } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import {
4
+ CONFIG_DIR,
5
+ CONFIG_FILE,
6
+ DEFAULT_CONFIG,
7
+ type RoutstrdConfig,
8
+ } from "../utils/config";
9
+ import { logger } from "../utils/logger";
10
+
11
+ export const REQUESTS_DIR = `${CONFIG_DIR}/requests`;
12
+
13
+ export async function ensureDirs(): Promise<void> {
14
+ try {
15
+ await mkdir(CONFIG_DIR, { recursive: true });
16
+ await mkdir(REQUESTS_DIR, { recursive: true });
17
+ } catch {
18
+ // Directory may already exist
19
+ }
20
+ }
21
+
22
+ export async function loadDaemonConfig(): Promise<RoutstrdConfig> {
23
+ try {
24
+ if (existsSync(CONFIG_FILE)) {
25
+ const content = await Bun.file(CONFIG_FILE).text();
26
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
27
+ }
28
+ } catch (error) {
29
+ logger.error("Failed to load config:", error);
30
+ }
31
+ return DEFAULT_CONFIG;
32
+ }
33
+
34
+ export function saveDaemonConfig(config: RoutstrdConfig): void {
35
+ Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
36
+ }