routstrd 0.4.0 → 0.4.2

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/dist/index.js CHANGED
@@ -9914,6 +9914,17 @@ async function startDaemonUnlocked(options) {
9914
9914
  detached: true
9915
9915
  });
9916
9916
  proc.unref();
9917
+ const killSpawnedDaemon = () => {
9918
+ try {
9919
+ process.kill(-proc.pid, "SIGTERM");
9920
+ } catch {}
9921
+ };
9922
+ const abandonOnSignal = () => {
9923
+ killSpawnedDaemon();
9924
+ process.exit(1);
9925
+ };
9926
+ process.once("SIGINT", abandonOnSignal);
9927
+ process.once("SIGTERM", abandonOnSignal);
9917
9928
  let exitCode = null;
9918
9929
  proc.exited.then((code) => {
9919
9930
  exitCode = code;
@@ -9944,12 +9955,15 @@ Daemon output:
9944
9955
  ${daemonOutput}` : ` Check logs in ${LOGS_DIR2}`));
9945
9956
  }
9946
9957
  if (await healthyDaemonHost(port, host)) {
9958
+ process.off("SIGINT", abandonOnSignal);
9959
+ process.off("SIGTERM", abandonOnSignal);
9947
9960
  printStartupProgress(progressLogOffset);
9948
9961
  console.log(`Routstr daemon started (PID: ${proc.pid}, ${formatElapsed(Date.now() - startedAt)}).`);
9949
9962
  return;
9950
9963
  }
9951
9964
  }
9952
- throw new Error(`Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs in ${LOGS_DIR2}`);
9965
+ killSpawnedDaemon();
9966
+ throw new Error(`Daemon did not become healthy within ${Math.round(startupTimeoutMs / 1000)} seconds; ` + `the incomplete daemon (PID ${proc.pid}) was stopped. Check logs in ${LOGS_DIR2}`);
9953
9967
  }
9954
9968
  async function startDaemon(options = {}) {
9955
9969
  const port = options.port || "8008";
@@ -21220,7 +21234,10 @@ var init_constants = __esm(() => {
21220
21234
  // src/tui/usage/data.ts
21221
21235
  async function fetchStatus() {
21222
21236
  try {
21223
- const result = await callDaemon("/status");
21237
+ const [result, config] = await Promise.all([
21238
+ callDaemon("/status"),
21239
+ loadConfig()
21240
+ ]);
21224
21241
  if (result.error)
21225
21242
  return null;
21226
21243
  const output = result.output;
@@ -21228,6 +21245,7 @@ async function fetchStatus() {
21228
21245
  daemon: output?.daemon || "unknown",
21229
21246
  wallet: output?.wallet || "unknown",
21230
21247
  mode: output?.mode || "apikeys",
21248
+ url: getDaemonBaseUrl(config),
21231
21249
  error: output?.error
21232
21250
  };
21233
21251
  } catch {
@@ -21276,7 +21294,7 @@ function formatTime(timestamp) {
21276
21294
  const p = (n) => String(n).padStart(2, "0");
21277
21295
  return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
21278
21296
  }
21279
- function formatNumber(n) {
21297
+ function formatNumber2(n) {
21280
21298
  if (n >= 1e6)
21281
21299
  return (n / 1e6).toFixed(1) + "M";
21282
21300
  if (n >= 1000)
@@ -21518,8 +21536,8 @@ function renderOverview(stats, balance, status, width) {
21518
21536
  `${COLORS.bold}Avg Cost/Req:${COLORS.reset} ${formatCost(avgCost)} sats`
21519
21537
  ];
21520
21538
  const rightBox = [
21521
- `${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber(totals.totalTokens)}`,
21522
- `${COLORS.bold}Avg Tokens/Req:${COLORS.reset} ${formatNumber(Math.round(avgTokens))}`,
21539
+ `${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber2(totals.totalTokens)}`,
21540
+ `${COLORS.bold}Avg Tokens/Req:${COLORS.reset} ${formatNumber2(Math.round(avgTokens))}`,
21523
21541
  `${COLORS.bold}Prompt/Comp:${COLORS.reset} ${(totals.promptTokens / Math.max(1, totals.completionTokens)).toFixed(2)}x`
21524
21542
  ];
21525
21543
  const halfWidth1 = Math.floor(width / 2);
@@ -21545,6 +21563,12 @@ function renderOverview(stats, balance, status, width) {
21545
21563
  const walletColor = status.wallet === "connected" ? COLORS.green : COLORS.red;
21546
21564
  const modeColor = COLORS.cyan;
21547
21565
  statusLines.push(`${COLORS.bold}Daemon:${COLORS.reset} ${daemonColor}${status.daemon}${COLORS.reset}`);
21566
+ if (status.url) {
21567
+ const label = "Running on: ";
21568
+ const maxUrlLen = Math.max(8, halfWidth2 - 4 - label.length);
21569
+ const url = status.url.length > maxUrlLen ? `${status.url.slice(0, Math.max(0, maxUrlLen - 1))}\u2026` : status.url;
21570
+ statusLines.push(`${COLORS.bold}${label}${COLORS.reset} ${COLORS.cyan}${url}${COLORS.reset}`);
21571
+ }
21548
21572
  statusLines.push(`${COLORS.bold}Wallet:${COLORS.reset} ${walletColor}${status.wallet}${COLORS.reset}`);
21549
21573
  statusLines.push(`${COLORS.bold}Mode:${COLORS.reset} ${modeColor}${status.mode}${COLORS.reset}`);
21550
21574
  if (status.error) {
@@ -21667,11 +21691,11 @@ function renderToday(stats, width) {
21667
21691
  `${COLORS.bold}Date:${COLORS.reset} ${todayStats.date}`,
21668
21692
  `${COLORS.bold}Requests:${COLORS.reset} ${formatReqs(todayStats.requests)}`,
21669
21693
  `${COLORS.bold}Cost:${COLORS.reset} ${COLORS.green}${formatCost(todayStats.satsCost)} sats${COLORS.reset}`,
21670
- `${COLORS.bold}Tokens:${COLORS.reset} ${formatNumber(todayStats.totalTokens)} (p: ${formatNumber(todayStats.promptTokens)} + c: ${formatNumber(todayStats.completionTokens)})`
21694
+ `${COLORS.bold}Tokens:${COLORS.reset} ${formatNumber2(todayStats.totalTokens)} (p: ${formatNumber2(todayStats.promptTokens)} + c: ${formatNumber2(todayStats.completionTokens)})`
21671
21695
  ];
21672
21696
  let output = renderBox(summaryLines, width, "Today");
21673
21697
  if (recentDays.length > 0) {
21674
- const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber(d.totalTokens)} tokens`);
21698
+ const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber2(d.totalTokens)} tokens`);
21675
21699
  output += `
21676
21700
  ` + renderBox(dayLines, width, "Recent Days");
21677
21701
  }
@@ -21713,7 +21737,7 @@ function renderModels(stats, width) {
21713
21737
  lines.push(`${color}${COLORS.bold}${model.modelId}${COLORS.reset}`);
21714
21738
  lines.push(` ${COLORS.dim}Cost:${COLORS.reset} ${formatCost(model.satsCost)} sats (${pct}%)`);
21715
21739
  lines.push(` ${COLORS.dim}Requests:${COLORS.reset} ${formatReqs(model.requests)}`);
21716
- lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber(model.totalTokens)}`);
21740
+ lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber2(model.totalTokens)}`);
21717
21741
  lines.push(` ${COLORS.dim}Avg:${COLORS.reset} ${formatCost(model.satsCost / model.requests)} sats/req`);
21718
21742
  lines.push(` ${renderBarChart(" ", model.satsCost, maxCost, width - 6, color, Number(pct), "model-detail")}`);
21719
21743
  lines.push("");
@@ -21731,7 +21755,7 @@ function renderProviders(stats, width) {
21731
21755
  lines.push(`${COLORS.cyan}${COLORS.bold}${shortUrl}${COLORS.reset}`);
21732
21756
  lines.push(` ${COLORS.dim}Requests:${COLORS.reset} ${formatReqs(provider.requests)}`);
21733
21757
  lines.push(` ${COLORS.dim}Cost:${COLORS.reset} ${formatCost(provider.satsCost)} sats`);
21734
- lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber(provider.totalTokens)}`);
21758
+ lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber2(provider.totalTokens)}`);
21735
21759
  lines.push("");
21736
21760
  }
21737
21761
  return renderBox(lines, width, "Provider Breakdown");
@@ -21740,9 +21764,9 @@ function renderTokens(stats, width) {
21740
21764
  const totals = stats.summary.totals;
21741
21765
  const modelStats = stats.summary.models;
21742
21766
  const summaryLines = [
21743
- `${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${formatNumber(totals.promptTokens)}`,
21744
- `${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${formatNumber(totals.completionTokens)}`,
21745
- `${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber(totals.totalTokens)}`,
21767
+ `${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${formatNumber2(totals.promptTokens)}`,
21768
+ `${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${formatNumber2(totals.completionTokens)}`,
21769
+ `${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber2(totals.totalTokens)}`,
21746
21770
  `${COLORS.bold}Prompt/Completion Ratio:${COLORS.reset} ${(totals.promptTokens / Math.max(1, totals.completionTokens)).toFixed(2)}x`,
21747
21771
  `${COLORS.bold}Avg Tokens/Request:${COLORS.reset} ${(totals.totalTokens / Math.max(1, stats.totalEntries)).toFixed(0)}`
21748
21772
  ];
@@ -21751,7 +21775,7 @@ function renderTokens(stats, width) {
21751
21775
  const tokenLines = modelStats.slice(0, 6).map((m) => {
21752
21776
  const color = MODEL_COLORS[m.modelId] || MODEL_COLORS.default;
21753
21777
  const promptPct = m.totalTokens > 0 ? (m.promptTokens / m.totalTokens * 100).toFixed(0) : "0";
21754
- return `${color}${m.modelId.padEnd(15)}${COLORS.reset} ${formatNumber(m.totalTokens).padStart(8)} tokens (${promptPct}% prompt)`;
21778
+ return `${color}${m.modelId.padEnd(15)}${COLORS.reset} ${formatNumber2(m.totalTokens).padStart(8)} tokens (${promptPct}% prompt)`;
21755
21779
  });
21756
21780
  output += `
21757
21781
  ` + renderBox(tokenLines, width, "Tokens by Model");
@@ -21787,7 +21811,7 @@ function renderClients(stats, width) {
21787
21811
  const dClient = client.client.slice(0, col1 - 1).padEnd(col1);
21788
21812
  const dReqs = formatReqs(client.requests).padEnd(col2);
21789
21813
  const dCost = `${formatCost(client.satsCost)} sats (${pct}%)`.padEnd(col3);
21790
- const dTok = formatNumber(client.totalTokens).padEnd(col4);
21814
+ const dTok = formatNumber2(client.totalTokens).padEnd(col4);
21791
21815
  const dAvg = `${avgCostFormatted} sats/req`;
21792
21816
  lines.push(`${color}${COLORS.bold}${dClient}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
21793
21817
  lines.push(` ${renderBarChart("", client.satsCost, maxCost, width - 6, color, Number(pct), "client-detail")}`);
@@ -21802,7 +21826,7 @@ function renderClients(stats, width) {
21802
21826
  continue;
21803
21827
  clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`);
21804
21828
  for (const m of topClient.topModels) {
21805
- clientModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
21829
+ clientModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber2(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
21806
21830
  }
21807
21831
  clientModelLines.push("");
21808
21832
  }
@@ -21838,7 +21862,7 @@ function renderNpubs(stats, clients, width) {
21838
21862
  const dNpub = shortNpub.padEnd(col1);
21839
21863
  const dReqs = formatReqs(npub.requests).padEnd(col2);
21840
21864
  const dCost = `${formatCost(npub.satsCost)} sats (${pct}%)`.padEnd(col3);
21841
- const dTok = formatNumber(npub.totalTokens).padEnd(col4);
21865
+ const dTok = formatNumber2(npub.totalTokens).padEnd(col4);
21842
21866
  const dAvg = `${avgCostFormatted} sats/req`;
21843
21867
  lines.push(`${COLORS.magenta}${COLORS.bold}${dNpub}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
21844
21868
  lines.push(` ${COLORS.dim}${npub.npub}${COLORS.reset}`);
@@ -21854,7 +21878,7 @@ function renderNpubs(stats, clients, width) {
21854
21878
  continue;
21855
21879
  npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
21856
21880
  for (const m of topNpub.topModels) {
21857
- npubModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
21881
+ npubModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber2(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
21858
21882
  }
21859
21883
  npubModelLines.push("");
21860
21884
  }
@@ -21890,7 +21914,7 @@ function renderRecent(stats, width) {
21890
21914
  entry.cacheReadInputTokens || 0,
21891
21915
  entry.cacheCreationInputTokens || 0,
21892
21916
  entry.completionTokens
21893
- ].map(formatNumber).join("/");
21917
+ ].map(formatNumber2).join("/");
21894
21918
  const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
21895
21919
  const cost = [
21896
21920
  formatCost(msatsToSats(entry.inputMsats)),
@@ -22262,6 +22286,53 @@ var {
22262
22286
 
22263
22287
  // src/cli.ts
22264
22288
  init_start_daemon();
22289
+
22290
+ // src/daemon/config-store.ts
22291
+ init_config();
22292
+ init_logger();
22293
+ import {
22294
+ chmodSync,
22295
+ existsSync as existsSync4,
22296
+ mkdirSync as mkdirSync2,
22297
+ readFileSync,
22298
+ renameSync,
22299
+ unlinkSync,
22300
+ writeFileSync
22301
+ } from "fs";
22302
+ var REQUESTS_DIR = `${CONFIG_DIR}/requests`;
22303
+ function chmodIgnoreErrors(path, mode) {
22304
+ try {
22305
+ chmodSync(path, mode);
22306
+ } catch {}
22307
+ }
22308
+ function ensureDirsSync() {
22309
+ try {
22310
+ mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
22311
+ } catch {}
22312
+ chmodSync(CONFIG_DIR, 448);
22313
+ try {
22314
+ mkdirSync2(REQUESTS_DIR, { recursive: true, mode: 448 });
22315
+ } catch {}
22316
+ chmodSync(REQUESTS_DIR, 448);
22317
+ }
22318
+ function saveDaemonConfig(config) {
22319
+ ensureDirsSync();
22320
+ const temporaryFile = `${CONFIG_FILE}.${process.pid}.tmp`;
22321
+ try {
22322
+ writeFileSync(temporaryFile, JSON.stringify(config, null, 2), {
22323
+ mode: 384
22324
+ });
22325
+ renameSync(temporaryFile, CONFIG_FILE);
22326
+ } catch (error) {
22327
+ try {
22328
+ unlinkSync(temporaryFile);
22329
+ } catch {}
22330
+ throw error;
22331
+ }
22332
+ chmodIgnoreErrors(CONFIG_FILE, 384);
22333
+ }
22334
+
22335
+ // src/cli.ts
22265
22336
  init_daemon_client();
22266
22337
 
22267
22338
  // src/utils/clients.ts
@@ -22273,7 +22344,7 @@ import { join as join3 } from "path";
22273
22344
 
22274
22345
  // src/integrations/opencode.ts
22275
22346
  init_daemon_client();
22276
- import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
22347
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
22277
22348
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
22278
22349
  import { dirname as dirname2 } from "path";
22279
22350
  var OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
@@ -22285,7 +22356,7 @@ Installing routstr models in opencode.json...`);
22285
22356
  const baseUrl = getDaemonBaseUrl(config);
22286
22357
  let opencodeConfig;
22287
22358
  try {
22288
- if (existsSync4(configPath)) {
22359
+ if (existsSync5(configPath)) {
22289
22360
  const content = await readFile2(configPath, "utf-8");
22290
22361
  opencodeConfig = JSON.parse(content);
22291
22362
  } else {
@@ -22299,7 +22370,7 @@ Installing routstr models in opencode.json...`);
22299
22370
  opencodeConfig.provider = {};
22300
22371
  }
22301
22372
  try {
22302
- mkdirSync2(dirname2(configPath), { recursive: true });
22373
+ mkdirSync3(dirname2(configPath), { recursive: true });
22303
22374
  const data = await callDaemon("/models");
22304
22375
  const models = data.output?.models || [];
22305
22376
  if (models.length === 0) {
@@ -22330,7 +22401,7 @@ Installing routstr models in opencode.json...`);
22330
22401
 
22331
22402
  // src/integrations/pi.ts
22332
22403
  init_daemon_client();
22333
- import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
22404
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
22334
22405
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
22335
22406
  import { dirname as dirname3 } from "path";
22336
22407
  async function installPiIntegration(config, apiKey, integrationConfig) {
@@ -22341,7 +22412,7 @@ Installing routstr models in pi models.json...`);
22341
22412
  const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
22342
22413
  let piConfig = {};
22343
22414
  try {
22344
- if (existsSync5(configPath)) {
22415
+ if (existsSync6(configPath)) {
22345
22416
  const content = await readFile3(configPath, "utf-8");
22346
22417
  piConfig = JSON.parse(content);
22347
22418
  }
@@ -22353,21 +22424,25 @@ Installing routstr models in pi models.json...`);
22353
22424
  piConfig.providers = {};
22354
22425
  }
22355
22426
  try {
22356
- mkdirSync3(dirname3(configPath), { recursive: true });
22427
+ mkdirSync4(dirname3(configPath), { recursive: true });
22357
22428
  const data = await callDaemon("/models");
22358
22429
  const models = data.output?.models || [];
22359
22430
  if (models.length === 0) {
22360
22431
  console.log("No models found from routstr daemon.");
22361
22432
  return;
22362
22433
  }
22434
+ const existingModels = new Map((piConfig.providers["routstr"]?.models ?? []).map((m) => [m.id, m]));
22363
22435
  const providerModels = models.map((model) => {
22364
- const entry = { id: model.id };
22436
+ const previous = existingModels.get(model.id);
22437
+ const entry = previous ? { ...previous } : { id: model.id };
22438
+ entry.id = model.id;
22365
22439
  if (model.context_length !== undefined && model.context_length > 0) {
22366
22440
  entry.contextWindow = model.context_length;
22367
22441
  }
22368
22442
  return entry;
22369
22443
  });
22370
22444
  piConfig.providers["routstr"] = {
22445
+ ...piConfig.providers["routstr"] ?? {},
22371
22446
  baseUrl,
22372
22447
  api: "openai-completions",
22373
22448
  apiKey,
@@ -22382,7 +22457,7 @@ Installing routstr models in pi models.json...`);
22382
22457
 
22383
22458
  // src/integrations/openclaw.ts
22384
22459
  init_daemon_client();
22385
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
22460
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
22386
22461
  import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
22387
22462
  import { dirname as dirname4 } from "path";
22388
22463
  var OPENCLAW_PROVIDER_ID = "routstr";
@@ -22396,7 +22471,7 @@ Installing routstr models in openclaw.json...`);
22396
22471
  const baseUrl = getDaemonBaseUrl(config);
22397
22472
  let openclawConfig = {};
22398
22473
  try {
22399
- if (existsSync6(configPath)) {
22474
+ if (existsSync7(configPath)) {
22400
22475
  const content = await readFile4(configPath, "utf-8");
22401
22476
  openclawConfig = JSON.parse(content);
22402
22477
  }
@@ -22417,7 +22492,7 @@ Installing routstr models in openclaw.json...`);
22417
22492
  openclawConfig.agents.defaults = {};
22418
22493
  }
22419
22494
  try {
22420
- mkdirSync4(dirname4(configPath), { recursive: true });
22495
+ mkdirSync5(dirname4(configPath), { recursive: true });
22421
22496
  const data = await callDaemon("/models");
22422
22497
  const models = data.output?.models || [];
22423
22498
  if (models.length === 0) {
@@ -22458,7 +22533,7 @@ Installing routstr models in openclaw.json...`);
22458
22533
 
22459
22534
  // src/integrations/claudecode.ts
22460
22535
  init_daemon_client();
22461
- import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
22536
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
22462
22537
  import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
22463
22538
  import { dirname as dirname5 } from "path";
22464
22539
  async function installClaudeCodeIntegration(config, apiKey, integrationConfig) {
@@ -22469,7 +22544,7 @@ Installing routstr configuration in ${configPath}...`);
22469
22544
  const baseUrl = getDaemonBaseUrl(config);
22470
22545
  let settings = {};
22471
22546
  try {
22472
- if (existsSync7(configPath)) {
22547
+ if (existsSync8(configPath)) {
22473
22548
  const content = await readFile5(configPath, "utf-8");
22474
22549
  settings = JSON.parse(content);
22475
22550
  }
@@ -22506,7 +22581,7 @@ Installing routstr configuration in ${configPath}...`);
22506
22581
  console.error("Failed to fetch models for Claude Code integration:", error);
22507
22582
  }
22508
22583
  try {
22509
- mkdirSync5(dirname5(configPath), { recursive: true });
22584
+ mkdirSync6(dirname5(configPath), { recursive: true });
22510
22585
  await writeFile5(configPath, JSON.stringify(settings, null, 2));
22511
22586
  console.log(`Successfully updated ${configPath} with routstr settings.`);
22512
22587
  } catch (error) {
@@ -22515,7 +22590,7 @@ Installing routstr configuration in ${configPath}...`);
22515
22590
  }
22516
22591
 
22517
22592
  // src/integrations/hermes.ts
22518
- import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
22593
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
22519
22594
  import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
22520
22595
  import { dirname as dirname6 } from "path";
22521
22596
 
@@ -22638,7 +22713,7 @@ Installing routstr configuration in ${configPath}...`);
22638
22713
  }
22639
22714
  let content = "";
22640
22715
  try {
22641
- if (existsSync8(configPath)) {
22716
+ if (existsSync9(configPath)) {
22642
22717
  content = await readFile6(configPath, "utf-8");
22643
22718
  }
22644
22719
  } catch (error) {
@@ -22661,7 +22736,7 @@ Installing routstr configuration in ${configPath}...`);
22661
22736
  console.log(`${configPath} already contains current routstr settings.`);
22662
22737
  return;
22663
22738
  }
22664
- mkdirSync6(dirname6(configPath), { recursive: true });
22739
+ mkdirSync7(dirname6(configPath), { recursive: true });
22665
22740
  await writeFile6(configPath, newContent);
22666
22741
  console.log(`Successfully updated ${configPath} with routstr settings.`);
22667
22742
  } catch (error) {
@@ -22898,9 +22973,9 @@ async function addClientAction(options) {
22898
22973
  // src/cli.ts
22899
22974
  init_config();
22900
22975
  init_logger();
22901
- import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
22976
+ import { chmodSync as chmodSync3, existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync5, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
22902
22977
  import { execSync } from "child_process";
22903
- import { dirname as dirname8, join as join7 } from "path";
22978
+ import { dirname as dirname8, join as join9 } from "path";
22904
22979
 
22905
22980
  // src/integrations/index.ts
22906
22981
  init_logger();
@@ -24180,13 +24255,13 @@ k("Cashu_P2BK_v1");
24180
24255
  // src/daemon/wallet/coco-client.ts
24181
24256
  import {
24182
24257
  closeSync as closeSync2,
24183
- existsSync as existsSync9,
24184
- mkdirSync as mkdirSync7,
24258
+ existsSync as existsSync10,
24259
+ mkdirSync as mkdirSync8,
24185
24260
  openSync as openSync2,
24186
- readFileSync,
24187
- renameSync,
24188
- unlinkSync,
24189
- writeFileSync
24261
+ readFileSync as readFileSync2,
24262
+ renameSync as renameSync2,
24263
+ unlinkSync as unlinkSync2,
24264
+ writeFileSync as writeFileSync2
24190
24265
  } from "fs";
24191
24266
 
24192
24267
  // node_modules/@scure/bip39/index.js
@@ -24255,6 +24330,11 @@ var STALE_SOCKET_ERROR_CODES = new Set([
24255
24330
  "ENOENT",
24256
24331
  "FailedToOpenSocket"
24257
24332
  ]);
24333
+ var RECOVERY_STALL_MARKERS = new Map([
24334
+ ["SendOperationService\x00Could not reach mint for recovery, will retry later", "Send recovery: mint unreachable"],
24335
+ ["ReceiveOperationService\x00Could not reach mint for receive recovery, will retry later", "Receive recovery: mint unreachable"],
24336
+ ["MintOperationService\x00Failed to reconcile stale pending mint operation", "Mint recovery: pending operation check failed"]
24337
+ ]);
24258
24338
  var SAFE_COCO_LOG_FIELDS = new Set([
24259
24339
  "module",
24260
24340
  "mintUrl",
@@ -24271,13 +24351,22 @@ var SAFE_COCO_LOG_FIELDS = new Set([
24271
24351
  "rollingBackOperations",
24272
24352
  "orphanedReservations"
24273
24353
  ]);
24354
+ function isZombieProcess(pid, readFile7 = (path) => readFileSync2(path, "utf-8")) {
24355
+ try {
24356
+ const stat2 = readFile7(`/proc/${pid}/stat`);
24357
+ const commandEnd = stat2.lastIndexOf(")");
24358
+ return commandEnd >= 0 && stat2.charAt(commandEnd + 2) === "Z";
24359
+ } catch {
24360
+ return false;
24361
+ }
24362
+ }
24274
24363
  function defaultIsProcessRunning(pid) {
24275
24364
  try {
24276
24365
  process.kill(pid, 0);
24277
- return true;
24278
24366
  } catch (error) {
24279
24367
  return error.code === "EPERM";
24280
24368
  }
24369
+ return !isZombieProcess(pid);
24281
24370
  }
24282
24371
  function hasErrorCode(error, codes) {
24283
24372
  let current = error;
@@ -24294,24 +24383,7 @@ function hasErrorCode(error, codes) {
24294
24383
  }
24295
24384
  async function assertLegacyCocodNotRunning(options = {}) {
24296
24385
  const socketPath = options.socketPath || legacyCocodSocketPath();
24297
- const pidFilePath = options.pidFilePath || legacyCocodPidPath();
24298
- const pathExists = options.pathExists || existsSync9;
24299
- const readFile7 = options.readFile || ((path) => readFileSync(path, "utf-8"));
24300
- const isProcessRunning2 = options.isProcessRunning || defaultIsProcessRunning;
24301
- const getRunningLegacyPid = () => {
24302
- if (!pathExists(pidFilePath))
24303
- return null;
24304
- try {
24305
- const pid = Number.parseInt(readFile7(pidFilePath).trim(), 10);
24306
- return Number.isInteger(pid) && pid > 0 && pid !== options.ignorePid && isProcessRunning2(pid) ? pid : null;
24307
- } catch {
24308
- return null;
24309
- }
24310
- };
24311
- const runningPid = getRunningLegacyPid();
24312
- if (runningPid !== null) {
24313
- throw new Error(`Legacy cocod daemon is still running with PID ${runningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + `Run 'cocod stop' or 'kill ${runningPid}' and try again.`);
24314
- }
24386
+ const pathExists = options.pathExists || existsSync10;
24315
24387
  if (!pathExists(socketPath))
24316
24388
  return;
24317
24389
  const fetchImpl = options.fetchImpl || fetch;
@@ -24324,12 +24396,8 @@ async function assertLegacyCocodNotRunning(options = {}) {
24324
24396
  await response.body?.cancel();
24325
24397
  } catch (error) {
24326
24398
  if (hasErrorCode(error, STALE_SOCKET_ERROR_CODES)) {
24327
- const newlyRunningPid = getRunningLegacyPid();
24328
- if (newlyRunningPid === null) {
24329
- logger.debug(`Ignoring stale legacy cocod socket at ${socketPath}`);
24330
- return;
24331
- }
24332
- throw new Error(`Legacy cocod daemon is still running with PID ${newlyRunningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + `Run 'cocod stop' or 'kill ${newlyRunningPid}' and try again.`, { cause: error });
24399
+ logger.debug(`Ignoring stale legacy cocod socket at ${socketPath}`);
24400
+ return;
24333
24401
  }
24334
24402
  throw new Error(`Cannot verify whether the legacy cocod daemon has stopped at ${socketPath}. ` + "Refusing to open the wallet database to prevent concurrent access. " + "Run 'cocod stop', verify the daemon has exited, and try again.", { cause: error });
24335
24403
  }
@@ -24338,8 +24406,8 @@ async function assertLegacyCocodNotRunning(options = {}) {
24338
24406
  async function stopLegacyCocod(options = {}) {
24339
24407
  const socketPath = options.socketPath || legacyCocodSocketPath();
24340
24408
  const pidFilePath = options.pidFilePath || legacyCocodPidPath();
24341
- const pathExists = options.pathExists || existsSync9;
24342
- const readFile7 = options.readFile || ((path) => readFileSync(path, "utf-8"));
24409
+ const pathExists = options.pathExists || existsSync10;
24410
+ const readFile7 = options.readFile || ((path) => readFileSync2(path, "utf-8"));
24343
24411
  const isProcessRunning2 = options.isProcessRunning || defaultIsProcessRunning;
24344
24412
  const fetchImpl = options.fetchImpl || fetch;
24345
24413
  const killProcess = options.killProcess || ((pid2, signal) => process.kill(pid2, signal));
@@ -24393,17 +24461,19 @@ async function stopLegacyCocod(options = {}) {
24393
24461
  function claimLegacyCocodPidFile(options = {}) {
24394
24462
  return claimPidFile({
24395
24463
  ...options,
24396
- pidFilePath: options.pidFilePath || legacyCocodPidPath()
24464
+ pidFilePath: options.pidFilePath || legacyCocodPidPath(),
24465
+ label: options.label || "legacy cocod exclusion lock"
24397
24466
  });
24398
24467
  }
24399
24468
  function claimPidFile(options) {
24400
24469
  const pidFilePath = options.pidFilePath;
24401
24470
  const pid = options.pid ?? process.pid;
24471
+ const label = options.label || "wallet process lock";
24402
24472
  const openExclusive = options.openExclusive || ((path) => openSync2(path, "wx", 384));
24403
- const writePid = options.writePid || ((fd2, ownerPid) => writeFileSync(fd2, String(ownerPid)));
24473
+ const writePid = options.writePid || ((fd2, ownerPid) => writeFileSync2(fd2, String(ownerPid)));
24404
24474
  const closeFile = options.closeFile || closeSync2;
24405
- const readFile7 = options.readFile || ((path) => readFileSync(path, "utf-8"));
24406
- const removeFile = options.removeFile || unlinkSync;
24475
+ const readFile7 = options.readFile || ((path) => readFileSync2(path, "utf-8"));
24476
+ const removeFile = options.removeFile || unlinkSync2;
24407
24477
  const isProcessRunning2 = options.isProcessRunning || defaultIsProcessRunning;
24408
24478
  let fd;
24409
24479
  try {
@@ -24415,16 +24485,17 @@ function claimPidFile(options) {
24415
24485
  try {
24416
24486
  stalePid = Number.parseInt(readFile7(pidFilePath).trim(), 10);
24417
24487
  } catch {
24418
- throw new Error(`Cannot claim the wallet process lock at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: error });
24488
+ throw new Error(`Cannot claim the ${label} at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: error });
24419
24489
  }
24420
24490
  if (!Number.isInteger(stalePid) || stalePid <= 0 || isProcessRunning2(stalePid)) {
24421
- throw new Error(`Cannot claim the wallet process lock at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: error });
24491
+ const ownerMessage = Number.isInteger(stalePid) && stalePid > 0 ? `PID ${stalePid} is still running and holds it. ` + `Stop that process first ('routstrd stop' or 'kill ${stalePid}').` : "Another cocod or routstrd process may be starting. Stop it and try again.";
24492
+ throw new Error(`Cannot claim the ${label} at ${pidFilePath}: ${ownerMessage}`, { cause: error });
24422
24493
  }
24423
24494
  try {
24424
24495
  removeFile(pidFilePath);
24425
24496
  fd = openExclusive(pidFilePath);
24426
24497
  } catch (retryError) {
24427
- throw new Error(`Cannot claim the wallet process lock at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: retryError });
24498
+ throw new Error(`Cannot claim the ${label} at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: retryError });
24428
24499
  }
24429
24500
  }
24430
24501
  try {
@@ -24438,38 +24509,50 @@ function claimPidFile(options) {
24438
24509
  closeFile(fd);
24439
24510
  }
24440
24511
  let released = false;
24441
- return () => {
24512
+ const release = () => {
24442
24513
  if (released)
24443
24514
  return;
24444
24515
  released = true;
24516
+ process.removeListener("exit", release);
24445
24517
  try {
24446
24518
  if (readFile7(pidFilePath).trim() === String(pid)) {
24447
24519
  removeFile(pidFilePath);
24448
24520
  }
24449
24521
  } catch (error) {
24450
24522
  if (error.code !== "ENOENT") {
24451
- logger.warn(`Failed to release wallet process lock at ${pidFilePath}:`, error);
24523
+ logger.warn(`Failed to release ${label} at ${pidFilePath}:`, error);
24452
24524
  }
24453
24525
  }
24454
24526
  };
24527
+ process.once("exit", release);
24528
+ return release;
24455
24529
  }
24456
24530
 
24457
24531
  // src/daemon/wallet/migration.ts
24458
24532
  import {
24459
- chmodSync,
24533
+ chmodSync as chmodSync2,
24460
24534
  copyFileSync,
24461
- existsSync as existsSync10,
24462
- mkdirSync as mkdirSync8,
24463
- readFileSync as readFileSync2,
24464
- renameSync as renameSync2,
24535
+ existsSync as existsSync13,
24536
+ mkdirSync as mkdirSync9,
24537
+ renameSync as renameSync3,
24465
24538
  rmSync,
24466
- statSync
24539
+ statSync as statSync3
24467
24540
  } from "fs";
24541
+ import { Database as Database2 } from "bun:sqlite";
24542
+ import { basename, dirname as dirname7, join as join8 } from "path";
24543
+
24544
+ // src/daemon/wallet/diagnostics.ts
24545
+ import { createHash } from "crypto";
24546
+ import { existsSync as existsSync12, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
24468
24547
  import { Database } from "bun:sqlite";
24469
- import { basename, dirname as dirname7, join as join6 } from "path";
24470
- function state(configPath, dbPath) {
24471
- const hasConfig = existsSync10(configPath);
24472
- const hasDb = existsSync10(dbPath);
24548
+ import { join as join7 } from "path";
24549
+
24550
+ // src/daemon/wallet/wallet-state.ts
24551
+ import { existsSync as existsSync11, readFileSync as readFileSync3, statSync } from "fs";
24552
+ import { join as join6 } from "path";
24553
+ function walletState(configPath, dbPath) {
24554
+ const hasConfig = existsSync11(configPath);
24555
+ const hasDb = existsSync11(dbPath);
24473
24556
  if (hasConfig)
24474
24557
  return "initialized";
24475
24558
  if (hasDb)
@@ -24477,15 +24560,42 @@ function state(configPath, dbPath) {
24477
24560
  return "absent";
24478
24561
  }
24479
24562
  function filesEqual(left, right) {
24480
- if (!existsSync10(left) || !existsSync10(right))
24563
+ if (!existsSync11(left) || !existsSync11(right))
24481
24564
  return false;
24482
24565
  if (statSync(left).size !== statSync(right).size)
24483
24566
  return false;
24484
- return readFileSync2(left).equals(readFileSync2(right));
24567
+ return readFileSync3(left).equals(readFileSync3(right));
24485
24568
  }
24486
- function sqlString(value) {
24487
- return `'${value.replaceAll("'", "''")}'`;
24569
+ function classifyWalletMigration(targetDir, sourceDir) {
24570
+ const targetConfig = join6(targetDir, "config.json");
24571
+ const targetDb = join6(targetDir, "coco.db");
24572
+ const sourceConfig = join6(sourceDir, "config.json");
24573
+ const sourceDb = join6(sourceDir, "coco.db");
24574
+ const sourceWal = `${sourceDb}-wal`;
24575
+ const sourceShm = `${sourceDb}-shm`;
24576
+ const targetState = walletState(targetConfig, targetDb);
24577
+ const sourceState = walletState(sourceConfig, sourceDb);
24578
+ if (targetState === "initialized" && sourceState === "initialized") {
24579
+ const configsMatch = filesEqual(targetConfig, sourceConfig);
24580
+ const databasesMatch = !existsSync11(targetDb) && !existsSync11(sourceDb) ? true : filesEqual(targetDb, sourceDb);
24581
+ if (configsMatch && databasesMatch)
24582
+ return { kind: "already-current" };
24583
+ return { kind: "conflict" };
24584
+ }
24585
+ if (targetState === "initialized")
24586
+ return { kind: "already-current" };
24587
+ if (!existsSync11(sourceDb) && (existsSync11(sourceWal) || existsSync11(sourceShm))) {
24588
+ return { kind: "orphaned-sidecars" };
24589
+ }
24590
+ if (targetState === "database-only" || sourceState === "database-only") {
24591
+ return { kind: "database-only", targetState, sourceState };
24592
+ }
24593
+ if (sourceState === "absent")
24594
+ return { kind: "fresh" };
24595
+ return { kind: "migrate" };
24488
24596
  }
24597
+
24598
+ // src/daemon/wallet/diagnostics.ts
24489
24599
  var SUMMARY_QUERIES = {
24490
24600
  proofs: "SELECT mintUrl, state, COUNT(*) count, COALESCE(SUM(amount), 0) amount FROM coco_cashu_proofs GROUP BY mintUrl, state ORDER BY mintUrl, state",
24491
24601
  counters: "SELECT mintUrl, keysetId, counter FROM coco_cashu_counters ORDER BY mintUrl, keysetId",
@@ -24507,8 +24617,339 @@ function verifyDatabase(database, label) {
24507
24617
  }
24508
24618
  return summary;
24509
24619
  }
24620
+ function mnemonicFingerprint(mnemonic) {
24621
+ const normalized = mnemonic.trim().split(/\s+/).join(" ");
24622
+ const hex3 = createHash("sha256").update(normalized).digest("hex");
24623
+ return hex3.slice(0, 16).match(/.{1,4}/g)?.join("-") ?? hex3.slice(0, 16);
24624
+ }
24625
+ function summarizeDbReadonly(dbPath) {
24626
+ let database;
24627
+ try {
24628
+ database = new Database(dbPath, { readonly: true });
24629
+ const raw = verifyDatabase(database, "Wallet database");
24630
+ const proofs = raw.proofs ?? [];
24631
+ const mints = raw.mints ?? [];
24632
+ const amountByState = {};
24633
+ let totalProofs = 0;
24634
+ let totalAmount = 0;
24635
+ for (const row of proofs) {
24636
+ const count = typeof row.count === "number" ? row.count : 0;
24637
+ const amount = typeof row.amount === "number" ? row.amount : 0;
24638
+ totalProofs += count;
24639
+ totalAmount += amount;
24640
+ const state = row.state || "unknown";
24641
+ amountByState[state] = (amountByState[state] ?? 0) + amount;
24642
+ }
24643
+ return {
24644
+ summary: {
24645
+ proofs,
24646
+ mints,
24647
+ totalProofs,
24648
+ totalAmount,
24649
+ amountByState,
24650
+ distinctMints: new Set(proofs.map((row) => row.mintUrl).filter(Boolean)).size
24651
+ }
24652
+ };
24653
+ } catch (error) {
24654
+ return { error: error instanceof Error ? error.message : String(error) };
24655
+ } finally {
24656
+ database?.close();
24657
+ }
24658
+ }
24659
+ function summarizeConfig(configPath) {
24660
+ const result = { exists: false, path: configPath };
24661
+ try {
24662
+ if (!existsSync12(configPath))
24663
+ return result;
24664
+ result.exists = true;
24665
+ result.mtimeMs = statSync2(configPath).mtimeMs;
24666
+ const parsed = JSON.parse(readFileSync4(configPath, "utf-8"));
24667
+ result.encrypted = parsed.encrypted === true;
24668
+ if (typeof parsed.createdAt === "string")
24669
+ result.createdAt = parsed.createdAt;
24670
+ if (typeof parsed.version === "number")
24671
+ result.version = parsed.version;
24672
+ if (typeof parsed.defaultMintUrl === "string") {
24673
+ result.defaultMintUrl = parsed.defaultMintUrl;
24674
+ }
24675
+ if (typeof parsed.mnemonic === "string") {
24676
+ result.hasMnemonic = true;
24677
+ if (!result.encrypted)
24678
+ result.fingerprint = mnemonicFingerprint(parsed.mnemonic);
24679
+ }
24680
+ } catch (error) {
24681
+ result.error = error instanceof Error ? error.message : String(error);
24682
+ }
24683
+ return result;
24684
+ }
24685
+ function summarizeDb(dbPath) {
24686
+ const result = { exists: false, path: dbPath };
24687
+ try {
24688
+ if (!existsSync12(dbPath))
24689
+ return result;
24690
+ result.exists = true;
24691
+ const stats = statSync2(dbPath);
24692
+ result.mtimeMs = stats.mtimeMs;
24693
+ result.sizeBytes = stats.size;
24694
+ const { summary, error } = summarizeDbReadonly(dbPath);
24695
+ if (summary)
24696
+ result.summary = summary;
24697
+ if (error)
24698
+ result.error = error;
24699
+ } catch (error) {
24700
+ result.error = error instanceof Error ? error.message : String(error);
24701
+ }
24702
+ return result;
24703
+ }
24704
+ function summarizeWalletDirectory(dir, role) {
24705
+ return {
24706
+ dir,
24707
+ role,
24708
+ config: summarizeConfig(join7(dir, "config.json")),
24709
+ db: summarizeDb(join7(dir, "coco.db"))
24710
+ };
24711
+ }
24712
+
24713
+ class WalletMigrationConflictError extends Error {
24714
+ target;
24715
+ source;
24716
+ constructor(target, source) {
24717
+ super(renderWalletConflict(target, source));
24718
+ this.name = "WalletMigrationConflictError";
24719
+ this.target = target;
24720
+ this.source = source;
24721
+ }
24722
+ }
24723
+ function timeAgo(ms) {
24724
+ if (ms === undefined || !Number.isFinite(ms))
24725
+ return;
24726
+ const elapsed = Date.now() - ms;
24727
+ if (elapsed < 0)
24728
+ return;
24729
+ const seconds = Math.floor(elapsed / 1000);
24730
+ if (seconds < 5)
24731
+ return "just now";
24732
+ if (seconds < 60)
24733
+ return `${seconds}s ago`;
24734
+ const minutes = Math.floor(seconds / 60);
24735
+ if (minutes < 60)
24736
+ return `${minutes}m ago`;
24737
+ const hours = Math.floor(minutes / 60);
24738
+ if (hours < 48)
24739
+ return `${hours}h ago`;
24740
+ const days = Math.floor(hours / 24);
24741
+ if (days < 60)
24742
+ return `${days}d ago`;
24743
+ const months = Math.floor(days / 30);
24744
+ if (months < 24)
24745
+ return `${months}mo ago`;
24746
+ return `${Math.floor(days / 365)}y ago`;
24747
+ }
24748
+ function modifiedDescription(config) {
24749
+ if (config.createdAt)
24750
+ return `created ${config.createdAt}`;
24751
+ const ago = timeAgo(config.mtimeMs);
24752
+ if (ago)
24753
+ return `modified ${ago}`;
24754
+ return "modified time unknown";
24755
+ }
24756
+ function roleLabel(role) {
24757
+ return role === "canonical" ? "current routstrd wallet" : "legacy cocod wallet";
24758
+ }
24759
+ function configLine(config) {
24760
+ if (!config.exists)
24761
+ return "config.json absent";
24762
+ if (config.error)
24763
+ return `config.json present (unreadable: ${config.error})`;
24764
+ const parts = ["config.json present"];
24765
+ if (config.fingerprint)
24766
+ parts.push(`fingerprint ${config.fingerprint}`);
24767
+ else if (config.encrypted)
24768
+ parts.push("encrypted (mnemonic not readable)");
24769
+ else
24770
+ parts.push("no mnemonic found");
24771
+ parts.push(modifiedDescription(config));
24772
+ if (config.defaultMintUrl)
24773
+ parts.push(`default mint ${config.defaultMintUrl}`);
24774
+ return parts.join(" \xB7 ");
24775
+ }
24776
+ function formatNumber(value) {
24777
+ return value.toLocaleString("en-US");
24778
+ }
24779
+ function amountByStateLine(summary) {
24780
+ const parts = Object.entries(summary.amountByState).filter(([, amount]) => amount > 0);
24781
+ if (parts.length === 0)
24782
+ return "0 sats";
24783
+ return parts.map(([state, amount]) => `${state}: ${formatNumber(amount)} sats`).join(", ");
24784
+ }
24785
+ function dbLine(db) {
24786
+ if (!db.exists)
24787
+ return "coco.db absent";
24788
+ if (db.error && !db.summary)
24789
+ return `coco.db present (unreadable: ${db.error})`;
24790
+ const parts = ["coco.db present"];
24791
+ if (db.summary) {
24792
+ const { totalProofs, mints, distinctMints } = db.summary;
24793
+ const mintCount = mints.length > 0 ? mints.length : distinctMints;
24794
+ parts.push(`${formatNumber(totalProofs)} proof${totalProofs === 1 ? "" : "s"}`);
24795
+ parts.push(amountByStateLine(db.summary));
24796
+ parts.push(`${mintCount} mint${mintCount === 1 ? "" : "s"}`);
24797
+ }
24798
+ return parts.join(" \xB7 ");
24799
+ }
24800
+ function block(diag, letter) {
24801
+ return [
24802
+ ` ${letter}) ${diag.dir} (${roleLabel(diag.role)})`,
24803
+ ` ${configLine(diag.config)}`,
24804
+ ` ${dbLine(diag.db)}`
24805
+ ].join(`
24806
+ `);
24807
+ }
24808
+ function moveAsideCommands(target, source) {
24809
+ return [
24810
+ " \u2022 If B is your wallet (keep the legacy wallet):",
24811
+ ` mv "${target.dir}" "${target.dir}.old"`,
24812
+ "",
24813
+ " \u2022 If A is your wallet (keep the normal routstrd wallet):",
24814
+ ` mv "${source.dir}" "${source.dir}.old"`
24815
+ ].join(`
24816
+ `);
24817
+ }
24818
+ function renderResolutionSteps(target, source) {
24819
+ return [
24820
+ "To resolve, decide which wallet is yours, move the other one aside, and run",
24821
+ "'routstrd onboard' (or 'routstrd start') again. Stop the daemon first:",
24822
+ "",
24823
+ " routstrd stop",
24824
+ "",
24825
+ "Then:",
24826
+ "",
24827
+ moveAsideCommands(target, source)
24828
+ ].join(`
24829
+ `);
24830
+ }
24831
+ function renderWalletConflict(target, source) {
24832
+ return [
24833
+ "Cannot migrate wallet: two different wallets were found, and routstrd will",
24834
+ "not choose a mnemonic or merge databases automatically \u2014 the wrong choice",
24835
+ "could lose access to your funds.",
24836
+ "",
24837
+ block(target, "A"),
24838
+ block(source, "B"),
24839
+ "",
24840
+ "No files were changed.",
24841
+ "",
24842
+ renderResolutionSteps(target, source),
24843
+ "",
24844
+ "For a full comparison and safe cleanup, run: routstrd wallet doctor"
24845
+ ].join(`
24846
+ `);
24847
+ }
24848
+ function conflictVerdict(target, source) {
24849
+ const unreadable = !!target.config.error || !!source.config.error || !!target.db.error || !!source.db.error;
24850
+ if (unreadable) {
24851
+ return {
24852
+ text: "Verdict: both wallets exist but one or both could not be fully read; startup will refuse to migrate. See the details above.",
24853
+ showResolution: true,
24854
+ conflict: true
24855
+ };
24856
+ }
24857
+ if (target.config.fingerprint && source.config.fingerprint) {
24858
+ if (target.config.fingerprint === source.config.fingerprint) {
24859
+ return {
24860
+ text: "Verdict: both wallets share the same mnemonic but the files differ, so startup still refuses. Keep the wallet with your funds and move the other aside.",
24861
+ showResolution: true,
24862
+ conflict: true
24863
+ };
24864
+ }
24865
+ return {
24866
+ text: "Verdict: the two wallets have DIFFERENT mnemonics. Startup will refuse to migrate.",
24867
+ showResolution: true,
24868
+ conflict: true
24869
+ };
24870
+ }
24871
+ return {
24872
+ text: "Verdict: both wallets exist but their mnemonics cannot be compared (encrypted or missing). Startup will refuse to migrate unless the files are identical.",
24873
+ showResolution: true,
24874
+ conflict: true
24875
+ };
24876
+ }
24877
+ function verdictFromClassification(target, source, classification) {
24878
+ switch (classification.kind) {
24879
+ case "fresh":
24880
+ return {
24881
+ text: "Verdict: no wallet found in either location (fresh install).",
24882
+ showResolution: false,
24883
+ conflict: false
24884
+ };
24885
+ case "already-current":
24886
+ return {
24887
+ text: "Verdict: the current routstrd wallet is authoritative; no migration needed.",
24888
+ showResolution: false,
24889
+ conflict: false
24890
+ };
24891
+ case "migrate":
24892
+ return {
24893
+ text: "Verdict: only the legacy cocod wallet exists; it will be migrated on next startup.",
24894
+ showResolution: false,
24895
+ conflict: false
24896
+ };
24897
+ case "conflict":
24898
+ return conflictVerdict(target, source);
24899
+ case "database-only":
24900
+ return {
24901
+ text: "Verdict: a wallet is incomplete (coco.db without config.json). Startup will refuse to migrate. See the details above.",
24902
+ showResolution: false,
24903
+ conflict: true
24904
+ };
24905
+ case "orphaned-sidecars":
24906
+ return {
24907
+ text: "Verdict: the legacy wallet has SQLite sidecar files without coco.db. Startup will refuse to migrate. Restore the matching database first.",
24908
+ showResolution: false,
24909
+ conflict: true
24910
+ };
24911
+ }
24912
+ }
24913
+ function diagnoseWallets(target, source) {
24914
+ let classification;
24915
+ try {
24916
+ classification = classifyWalletMigration(target.dir, source.dir);
24917
+ } catch {
24918
+ if (target.config.error || source.config.error || target.db.error || source.db.error) {
24919
+ return conflictVerdict(target, source);
24920
+ }
24921
+ return {
24922
+ text: "Verdict: the wallets could not be fully compared (read error); startup will refuse to migrate. See the details above.",
24923
+ showResolution: true,
24924
+ conflict: true
24925
+ };
24926
+ }
24927
+ return verdictFromClassification(target, source, classification);
24928
+ }
24929
+ function renderWalletDoctor(target, source) {
24930
+ const verdict = diagnoseWallets(target, source);
24931
+ const lines = [
24932
+ "Routstr wallet diagnostic",
24933
+ "========================",
24934
+ "",
24935
+ block(target, "A"),
24936
+ block(source, "B"),
24937
+ "",
24938
+ verdict.text
24939
+ ];
24940
+ if (verdict.showResolution) {
24941
+ lines.push("", renderResolutionSteps(target, source));
24942
+ }
24943
+ return lines.join(`
24944
+ `);
24945
+ }
24946
+
24947
+ // src/daemon/wallet/migration.ts
24948
+ function sqlString(value) {
24949
+ return `'${value.replaceAll("'", "''")}'`;
24950
+ }
24510
24951
  function snapshotDatabase(sourcePath, destinationPath) {
24511
- const source = new Database(sourcePath);
24952
+ const source = new Database2(sourcePath);
24512
24953
  let sourceSummary;
24513
24954
  try {
24514
24955
  sourceSummary = verifyDatabase(source, "Legacy wallet database");
@@ -24516,7 +24957,7 @@ function snapshotDatabase(sourcePath, destinationPath) {
24516
24957
  } finally {
24517
24958
  source.close();
24518
24959
  }
24519
- const destination = new Database(destinationPath, { readonly: true });
24960
+ const destination = new Database2(destinationPath, { readonly: true });
24520
24961
  try {
24521
24962
  const destinationSummary = verifyDatabase(destination, "Staged wallet database");
24522
24963
  if (JSON.stringify(sourceSummary) !== JSON.stringify(destinationSummary)) {
@@ -24529,51 +24970,45 @@ function snapshotDatabase(sourcePath, destinationPath) {
24529
24970
  async function migrateLegacyWallet(options = {}) {
24530
24971
  const targetDir = options.walletDir || walletDir();
24531
24972
  const sourceDir = options.legacyDir || legacyCocodDir();
24532
- const targetConfig = join6(targetDir, "config.json");
24533
- const targetDb = join6(targetDir, "coco.db");
24534
- const sourceConfig = join6(sourceDir, "config.json");
24535
- const sourceDb = join6(sourceDir, "coco.db");
24973
+ const sourceConfig = join8(sourceDir, "config.json");
24974
+ const sourceDb = join8(sourceDir, "coco.db");
24536
24975
  const sourceWal = `${sourceDb}-wal`;
24537
24976
  const sourceShm = `${sourceDb}-shm`;
24538
- const targetState = state(targetConfig, targetDb);
24539
- const sourceState = state(sourceConfig, sourceDb);
24540
- if (targetState === "initialized" && sourceState === "initialized") {
24541
- const configsMatch = filesEqual(targetConfig, sourceConfig);
24542
- const databasesMatch = !existsSync10(targetDb) && !existsSync10(sourceDb) ? true : filesEqual(targetDb, sourceDb);
24543
- if (configsMatch && databasesMatch)
24977
+ const classification = classifyWalletMigration(targetDir, sourceDir);
24978
+ switch (classification.kind) {
24979
+ case "fresh":
24980
+ return { status: "fresh" };
24981
+ case "already-current":
24544
24982
  return { status: "already-current" };
24545
- throw new Error(`Cannot migrate wallet: both ${targetDir} and ${sourceDir} contain different wallet data. ` + "Refusing to choose a mnemonic or merge wallet databases automatically.");
24546
- }
24547
- if (targetState === "initialized")
24548
- return { status: "already-current" };
24549
- if (!existsSync10(sourceDb) && (existsSync10(sourceWal) || existsSync10(sourceShm))) {
24550
- throw new Error(`Cannot migrate wallet: ${sourceDir} contains SQLite sidecar files without coco.db. ` + "Restore the matching main database before migration.");
24551
- }
24552
- if (targetState === "database-only" || sourceState === "database-only") {
24553
- throw new Error(`Cannot migrate wallet: ${targetDir} is ${targetState} and ${sourceDir} is ${sourceState}. ` + "A coco.db without config.json cannot be opened; restore the matching config first.");
24983
+ case "migrate":
24984
+ break;
24985
+ case "conflict":
24986
+ throw new WalletMigrationConflictError(summarizeWalletDirectory(targetDir, "canonical"), summarizeWalletDirectory(sourceDir, "legacy"));
24987
+ case "orphaned-sidecars":
24988
+ throw new Error(`Cannot migrate wallet: ${sourceDir} contains SQLite sidecar files without coco.db. ` + "Restore the matching main database before migration.");
24989
+ case "database-only":
24990
+ throw new Error(`Cannot migrate wallet: ${targetDir} is ${classification.targetState} and ${sourceDir} is ${classification.sourceState}. ` + "A coco.db without config.json cannot be opened; restore the matching config first.");
24554
24991
  }
24555
- if (sourceState === "absent")
24556
- return { status: "fresh" };
24557
24992
  await options.assertLegacyStopped?.();
24558
24993
  const releaseLegacyLock = await options.acquireLegacyLock?.();
24559
- const stagingDir = join6(dirname7(targetDir), `.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`);
24994
+ const stagingDir = join8(dirname7(targetDir), `.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`);
24560
24995
  try {
24561
24996
  await options.assertLegacyStopped?.();
24562
- mkdirSync8(dirname7(targetDir), { recursive: true, mode: 448 });
24563
- mkdirSync8(stagingDir, { mode: 448 });
24564
- const stagedConfig = join6(stagingDir, "config.json");
24565
- const stagedDb = join6(stagingDir, "coco.db");
24997
+ mkdirSync9(dirname7(targetDir), { recursive: true, mode: 448 });
24998
+ mkdirSync9(stagingDir, { mode: 448 });
24999
+ const stagedConfig = join8(stagingDir, "config.json");
25000
+ const stagedDb = join8(stagingDir, "coco.db");
24566
25001
  copyFileSync(sourceConfig, stagedConfig);
24567
- chmodSync(stagedConfig, 384);
24568
- const hasSourceDb = existsSync10(sourceDb);
25002
+ chmodSync2(stagedConfig, 384);
25003
+ const hasSourceDb = existsSync13(sourceDb);
24569
25004
  if (hasSourceDb) {
24570
25005
  snapshotDatabase(sourceDb, stagedDb);
24571
- chmodSync(stagedDb, 384);
25006
+ chmodSync2(stagedDb, 384);
24572
25007
  }
24573
- if (statSync(stagedConfig).size !== statSync(sourceConfig).size) {
25008
+ if (statSync3(stagedConfig).size !== statSync3(sourceConfig).size) {
24574
25009
  throw new Error("Cannot migrate wallet: staged config failed size validation.");
24575
25010
  }
24576
- renameSync2(stagingDir, targetDir);
25011
+ renameSync3(stagingDir, targetDir);
24577
25012
  } catch (error) {
24578
25013
  rmSync(stagingDir, { recursive: true, force: true });
24579
25014
  throw error;
@@ -24581,13 +25016,13 @@ async function migrateLegacyWallet(options = {}) {
24581
25016
  releaseLegacyLock?.();
24582
25017
  }
24583
25018
  const cleanupWarnings = [];
24584
- const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(existsSync10);
25019
+ const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(existsSync13);
24585
25020
  if (sourceFiles.length > 0) {
24586
- const archiveDir = join6(sourceDir, `wallet-migrated-${Date.now()}`);
25021
+ const archiveDir = join8(sourceDir, `wallet-migrated-${Date.now()}`);
24587
25022
  try {
24588
- mkdirSync8(archiveDir, { mode: 448 });
25023
+ mkdirSync9(archiveDir, { mode: 448 });
24589
25024
  for (const path of sourceFiles)
24590
- renameSync2(path, join6(archiveDir, basename(path)));
25025
+ renameSync3(path, join8(archiveDir, basename(path)));
24591
25026
  } catch (error) {
24592
25027
  const message = error instanceof Error ? error.message : String(error);
24593
25028
  cleanupWarnings.push(`Could not archive legacy wallet files: ${message}`);
@@ -26721,7 +27156,7 @@ zoo`.split(`
26721
27156
  // package.json
26722
27157
  var package_default = {
26723
27158
  name: "routstrd",
26724
- version: "0.4.0",
27159
+ version: "0.4.2",
26725
27160
  module: "src/index.ts",
26726
27161
  type: "module",
26727
27162
  private: false,
@@ -26747,7 +27182,7 @@ var package_default = {
26747
27182
  "@cashu/cashu-ts": "^4.3.0",
26748
27183
  "@cashu/coco-core": "^1.0.1",
26749
27184
  "@cashu/coco-sqlite-bun": "^1.0.1",
26750
- "@routstr/sdk": "^0.4.0",
27185
+ "@routstr/sdk": "^0.4.1",
26751
27186
  "@scure/bip39": "^2.2.0",
26752
27187
  "applesauce-core": "^5.1.0",
26753
27188
  "applesauce-relay": "^5.1.0",
@@ -26787,11 +27222,11 @@ Invoice:
26787
27222
  ${invoice}`);
26788
27223
  }
26789
27224
  function initializeWallet(walletDir2 = walletDir()) {
26790
- const walletConfig = join7(walletDir2, "config.json");
26791
- mkdirSync9(walletDir2, { recursive: true, mode: 448 });
26792
- chmodSync2(walletDir2, 448);
26793
- if (existsSync11(walletConfig)) {
26794
- chmodSync2(walletConfig, 384);
27225
+ const walletConfig = join9(walletDir2, "config.json");
27226
+ mkdirSync10(walletDir2, { recursive: true, mode: 448 });
27227
+ chmodSync3(walletDir2, 448);
27228
+ if (existsSync14(walletConfig)) {
27229
+ chmodSync3(walletConfig, 384);
26795
27230
  console.log("Wallet already initialized.");
26796
27231
  return;
26797
27232
  }
@@ -26802,7 +27237,7 @@ function initializeWallet(walletDir2 = walletDir()) {
26802
27237
  encrypted: false,
26803
27238
  createdAt: new Date().toISOString()
26804
27239
  };
26805
- writeFileSync2(walletConfig, JSON.stringify(config, null, 2), {
27240
+ writeFileSync3(walletConfig, JSON.stringify(config, null, 2), {
26806
27241
  mode: 384,
26807
27242
  flag: "wx"
26808
27243
  });
@@ -26830,7 +27265,7 @@ Restarting routstrd daemon...`);
26830
27265
  for (let i2 = 0;i2 < 100; i2++) {
26831
27266
  await new Promise((resolve) => setTimeout(resolve, 100));
26832
27267
  const healthDown = !await isDaemonRunning();
26833
- const pidFileReleased = !existsSync11(pidFilePath);
27268
+ const pidFileReleased = !existsSync14(pidFilePath);
26834
27269
  if (healthDown && pidFileReleased)
26835
27270
  break;
26836
27271
  }
@@ -26873,16 +27308,16 @@ async function requireLocalDaemon() {
26873
27308
  }
26874
27309
  async function initDaemon() {
26875
27310
  console.log("Initializing routstrd...");
26876
- if (!existsSync11(CONFIG_DIR)) {
26877
- mkdirSync9(CONFIG_DIR, { recursive: true });
27311
+ if (!existsSync14(CONFIG_DIR)) {
26878
27312
  console.log(`Created config directory: ${CONFIG_DIR}`);
26879
27313
  }
26880
- if (!existsSync11(CONFIG_FILE)) {
27314
+ ensureDirsSync();
27315
+ if (!existsSync14(CONFIG_FILE)) {
26881
27316
  const config2 = {
26882
27317
  ...DEFAULT_CONFIG,
26883
27318
  cocodPath: null
26884
27319
  };
26885
- await Bun.write(CONFIG_FILE, JSON.stringify(config2, null, 2));
27320
+ saveDaemonConfig(config2);
26886
27321
  console.log(`Created config file: ${CONFIG_FILE}`);
26887
27322
  }
26888
27323
  const config = await loadConfig();
@@ -26891,7 +27326,7 @@ async function initDaemon() {
26891
27326
  const nsec = nip19_exports.nsecEncode(secretKey);
26892
27327
  const npub = npubFromSecretKey(secretKey);
26893
27328
  config.nsec = nsec;
26894
- await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
27329
+ saveDaemonConfig(config);
26895
27330
  console.log(`
26896
27331
  A new Nostr identity has been generated for authentication.`);
26897
27332
  console.log(`Your npub: ${npub}`);
@@ -26900,26 +27335,18 @@ A new Nostr identity has been generated for authentication.`);
26900
27335
  }
26901
27336
  console.log(`Database will be stored at: ${DB_PATH}`);
26902
27337
  await stopLegacyCocod();
26903
- let migrationLockOwner;
26904
27338
  const migration = await migrateLegacyWallet({
26905
27339
  assertLegacyStopped: () => assertLegacyCocodNotRunning({
26906
- socketPath: legacyCocodSocketPath(),
26907
- pidFilePath: legacyCocodPidPath(),
26908
- ignorePid: migrationLockOwner
27340
+ socketPath: legacyCocodSocketPath()
26909
27341
  }),
26910
27342
  acquireLegacyLock: () => {
26911
- mkdirSync9(dirname8(legacyCocodPidPath()), {
27343
+ mkdirSync10(dirname8(legacyCocodPidPath()), {
26912
27344
  recursive: true,
26913
27345
  mode: 448
26914
27346
  });
26915
- const release = claimLegacyCocodPidFile({
27347
+ return claimLegacyCocodPidFile({
26916
27348
  pidFilePath: legacyCocodPidPath()
26917
27349
  });
26918
- migrationLockOwner = process.pid;
26919
- return () => {
26920
- migrationLockOwner = undefined;
26921
- release();
26922
- };
26923
27350
  }
26924
27351
  });
26925
27352
  if (migration.status === "migrated") {
@@ -26941,10 +27368,7 @@ To ensure routstrd persists across system restarts, run: 'routstrd service insta
26941
27368
  }
26942
27369
  program.name("routstrd").description("Routstr daemon - Manage routstr processes").version(package_default.version, "--version", "output the version number");
26943
27370
  program.command("update").description("Update routstrd to the latest version").action(async () => {
26944
- const packages = [
26945
- { name: "routstrd", label: "routstrd" },
26946
- { name: "@routstr/cocod", label: "cocod" }
26947
- ];
27371
+ const packages = [{ name: "routstrd", label: "routstrd" }];
26948
27372
  let updatedAny = false;
26949
27373
  for (const { name, label } of packages) {
26950
27374
  const installed = await getGlobalPackageVersion(name);
@@ -26977,7 +27401,7 @@ program.command("update").description("Update routstrd to the latest version").a
26977
27401
  All packages are already up to date \u2014 nothing to do.`);
26978
27402
  }
26979
27403
  });
26980
- program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)").option("-y, --yes", "Skip confirmation prompt", false).option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false).action(async (options) => {
27404
+ program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to active wallet mint)").option("-y, --yes", "Skip confirmation prompt", false).option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false).action(async (options) => {
26981
27405
  await ensureDaemonRunning();
26982
27406
  let mintUrl = options.mintUrl;
26983
27407
  if (!mintUrl) {
@@ -26986,12 +27410,12 @@ program.command("refund").description("Refund pending tokens and API keys to a s
26986
27410
  console.log(balanceResult.error);
26987
27411
  process.exit(1);
26988
27412
  }
26989
- const balances = balanceResult.output?.balances;
26990
- if (!balances || Object.keys(balances).length === 0) {
27413
+ const output = balanceResult.output;
27414
+ mintUrl = output?.activeMint ?? (output?.balances ? Object.keys(output.balances)[0] : undefined);
27415
+ if (!mintUrl) {
26991
27416
  console.log("No mint URLs found in wallet balance");
26992
27417
  process.exit(1);
26993
27418
  }
26994
- mintUrl = Object.keys(balances)[0];
26995
27419
  console.log(`Using mint URL: ${mintUrl}`);
26996
27420
  }
26997
27421
  try {
@@ -27078,8 +27502,8 @@ program.command("remote [url]").description("Show the configured remote daemon,
27078
27502
  process.exit(1);
27079
27503
  }
27080
27504
  }
27081
- if (!existsSync11(CONFIG_DIR)) {
27082
- mkdirSync9(CONFIG_DIR, { recursive: true });
27505
+ if (!existsSync14(CONFIG_DIR)) {
27506
+ mkdirSync10(CONFIG_DIR, { recursive: true });
27083
27507
  }
27084
27508
  const updates = { daemonUrl: url };
27085
27509
  if (options.authUrl) {
@@ -27097,7 +27521,7 @@ program.command("remote [url]").description("Show the configured remote daemon,
27097
27521
  ...config,
27098
27522
  ...updates
27099
27523
  };
27100
- await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
27524
+ saveDaemonConfig(updatedConfig);
27101
27525
  console.log(`Remote daemon URL set to: ${url}`);
27102
27526
  if (options.authUrl) {
27103
27527
  console.log(`Auth proxy URL set to: ${options.authUrl}`);
@@ -27119,7 +27543,7 @@ program.command("local").description("Switch back to local daemon mode (removes
27119
27543
  const previousAuthUrl = config.authUrl;
27120
27544
  const { daemonUrl: _daemonUrl, authUrl: _authUrl, ...rest } = config;
27121
27545
  const updatedConfig = rest;
27122
- await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
27546
+ saveDaemonConfig(updatedConfig);
27123
27547
  console.log("Switched back to local daemon mode.");
27124
27548
  if (previousDaemonUrl) {
27125
27549
  console.log(` Removed remote daemon URL: ${previousDaemonUrl}`);
@@ -27133,17 +27557,30 @@ Use 'routstrd onboard' for first time using local mode or 'routstrd start' to st
27133
27557
  });
27134
27558
  program.command("onboard").description("Initialize routstrd (creates config directory and initializes wallet)").action(async () => {
27135
27559
  await requireLocalDaemon();
27136
- await initDaemon();
27560
+ try {
27561
+ await initDaemon();
27562
+ } catch (error) {
27563
+ if (error instanceof WalletMigrationConflictError) {
27564
+ console.error(error.message);
27565
+ process.exit(1);
27566
+ }
27567
+ throw error;
27568
+ }
27137
27569
  });
27138
27570
  program.command("start").description("Start the background daemon").option("--port <port>", "Port to listen on").option("--host <host>", "Bind address (default: 127.0.0.1)").option("-p, --provider <provider>", "Default provider to use").action(async (options) => {
27139
27571
  await requireLocalDaemon();
27140
27572
  const config = await loadConfig();
27141
27573
  await stopLegacyCocod();
27142
- await startDaemon({
27143
- port: options.port || String(config.port || 8008),
27144
- host: options.host || config.host || undefined,
27145
- provider: options.provider
27146
- });
27574
+ try {
27575
+ await startDaemon({
27576
+ port: options.port || String(config.port || 8008),
27577
+ host: options.host || config.host || undefined,
27578
+ provider: options.provider
27579
+ });
27580
+ } catch (error) {
27581
+ console.error(error instanceof Error ? error.message : error);
27582
+ process.exit(1);
27583
+ }
27147
27584
  });
27148
27585
  program.command("status").description("Check daemon and wallet status").action(async () => {
27149
27586
  const running = await isDaemonRunning();
@@ -27169,7 +27606,7 @@ program.command("status").description("Check daemon and wallet status").action(a
27169
27606
  }
27170
27607
  }
27171
27608
  });
27172
- program.command("balance").description("Get wallet and API key balances").option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false).option("--delete-api-keys <baseUrl>", "Delete the API key stored for the given provider base URL (refunds balance first)").option("--mint-url <url>", "Mint URL to refund the deleted API key balance to (defaults to first mint in wallet)").action(async (options) => {
27609
+ program.command("balance").description("Get wallet and API key balances").option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false).option("--delete-api-keys <baseUrl>", "Delete the API key stored for the given provider base URL (refunds balance first)").option("--mint-url <url>", "Mint URL to refund the deleted API key balance to (defaults to active wallet mint)").action(async (options) => {
27173
27610
  await ensureDaemonRunning();
27174
27611
  if (options.deleteApiKeys) {
27175
27612
  const baseUrl = options.deleteApiKeys;
@@ -27437,6 +27874,46 @@ providersCmd.command("enable <indices...>").description("Enable providers by the
27437
27874
  }
27438
27875
  }
27439
27876
  });
27877
+ providersCmd.command("reviews").description("Show all known providers with their stored review events and event IDs").action(async () => {
27878
+ await ensureDaemonRunning();
27879
+ const result = await callDaemon("/providers/reviews");
27880
+ if (result.error) {
27881
+ console.log(result.error);
27882
+ process.exit(1);
27883
+ }
27884
+ const output = result.output;
27885
+ if (!output) {
27886
+ console.log("No provider review data available.");
27887
+ return;
27888
+ }
27889
+ console.log(`Providers (${output.totalCount} total, ${output.reviewEventCount} stored review events):
27890
+ `);
27891
+ for (const provider of output.providers) {
27892
+ const status = provider.disabled ? "DISABLED" : "enabled ";
27893
+ console.log(` [${provider.index}] ${status} ${provider.baseUrl} (${provider.reviewCount} review${provider.reviewCount === 1 ? "" : "s"})`);
27894
+ if (provider.nodePubkeys.length > 0) {
27895
+ console.log(` node pubkeys: ${provider.nodePubkeys.join(", ")}`);
27896
+ }
27897
+ for (const review of provider.reviews) {
27898
+ const label = review.label || "review";
27899
+ const created = new Date(review.createdAt * 1000).toISOString();
27900
+ console.log(` - ${label} eventId: ${review.eventId}`);
27901
+ console.log(` node: ${review.nodePubkey ?? "(none)"}`);
27902
+ console.log(` author: ${review.authorPubkey ?? "(none)"}`);
27903
+ console.log(` at: ${created}`);
27904
+ }
27905
+ }
27906
+ if (output.unmatchedReviews.length > 0) {
27907
+ console.log(`
27908
+ Unmatched review events (${output.unmatchedReviews.length} \u2014 node pubkey not in any known provider):`);
27909
+ for (const review of output.unmatchedReviews) {
27910
+ const created = new Date(review.createdAt * 1000).toISOString();
27911
+ console.log(` - eventId: ${review.eventId}`);
27912
+ console.log(` node: ${review.nodePubkey ?? "(none)"}`);
27913
+ console.log(` at: ${created}`);
27914
+ }
27915
+ }
27916
+ });
27440
27917
  var clientsCmd = program.command("clients").description("List and manage clients");
27441
27918
  clientsCmd.command("list").description("List all clients").action(async () => {
27442
27919
  await listClientsAction();
@@ -27714,6 +28191,13 @@ var walletCmd = program.command("wallet").description("Wallet operations");
27714
28191
  walletCmd.command("status").description("Check wallet status").action(async () => {
27715
28192
  await handleDaemonCommand("/wallet/status");
27716
28193
  });
28194
+ walletCmd.command("doctor").description("Diagnose conflicting wallets (current routstrd wallet vs legacy cocod)").action(async () => {
28195
+ const target = summarizeWalletDirectory(walletDir(), "canonical");
28196
+ const source = summarizeWalletDirectory(legacyCocodDir(), "legacy");
28197
+ console.log(renderWalletDoctor(target, source));
28198
+ if (diagnoseWallets(target, source).conflict)
28199
+ process.exit(1);
28200
+ });
27717
28201
  walletCmd.command("unlock <passphrase>").description("Unlock the wallet").action(async (passphrase) => {
27718
28202
  await handleDaemonCommand("/wallet/unlock", {
27719
28203
  method: "POST",
@@ -27723,6 +28207,68 @@ walletCmd.command("unlock <passphrase>").description("Unlock the wallet").action
27723
28207
  walletCmd.command("balance").description("Get wallet balance").action(async () => {
27724
28208
  await handleDaemonCommand("/wallet/balance");
27725
28209
  });
28210
+ walletCmd.command("cleanup").description("Clear stuck pending/in-flight wallet operations").option("--mint-url <url>", "Only clean up operations for this mint URL").option("--min-age <hours>", "Minimum age for reclaiming sends/cancelling melts, in hours (default: 168, one week; expired mint quotes are always failed)", "168").option("--dry-run", "Report what would be cleaned without applying changes", false).option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
28211
+ const minAgeHours = Number.parseFloat(options.minAge);
28212
+ if (!Number.isFinite(minAgeHours) || minAgeHours < 0) {
28213
+ console.error(`Invalid --min-age value: ${options.minAge}`);
28214
+ process.exit(1);
28215
+ }
28216
+ if (!options.dryRun && !options.yes) {
28217
+ const rl = __require("readline").createInterface({
28218
+ input: process.stdin,
28219
+ output: process.stdout
28220
+ });
28221
+ const answer = await new Promise((resolve) => {
28222
+ rl.question("This will fail expired mint quotes, reclaim old pending sends, and cancel prepared melts. Continue? [y/N] ", (value) => {
28223
+ rl.close();
28224
+ resolve(value.trim().toLowerCase());
28225
+ });
28226
+ });
28227
+ if (answer !== "y" && answer !== "yes") {
28228
+ console.log("Aborted.");
28229
+ return;
28230
+ }
28231
+ }
28232
+ try {
28233
+ await ensureDaemonRunning();
28234
+ const result = await callDaemon("/wallet/cleanup", {
28235
+ method: "POST",
28236
+ body: {
28237
+ mintUrl: options.mintUrl,
28238
+ minAgeMs: Math.round(minAgeHours * 60 * 60 * 1000),
28239
+ dryRun: options.dryRun === true
28240
+ }
28241
+ });
28242
+ if (result.error) {
28243
+ console.log(result.error);
28244
+ process.exit(1);
28245
+ }
28246
+ const output = result.output;
28247
+ if (output) {
28248
+ const prefix = output.dryRun ? "Would clean up:" : "Cleaned up:";
28249
+ console.log(prefix);
28250
+ console.log(` Expired mint quotes failed: ${output.failedMintQuotes ?? 0}`);
28251
+ console.log(` Pending sends reclaimed: ${output.reclaimedSends ?? 0}`);
28252
+ console.log(` Prepared melts cancelled: ${output.cancelledMelts ?? 0}`);
28253
+ console.log(` Skipped (still recent or already terminal): ${output.skipped ?? 0}`);
28254
+ if (output.errors && output.errors.length > 0) {
28255
+ console.log(`
28256
+ Errors:`);
28257
+ for (const e of output.errors) {
28258
+ console.log(` - ${e.operationId}: ${e.error}`);
28259
+ }
28260
+ }
28261
+ }
28262
+ } catch (error) {
28263
+ const message = error.message;
28264
+ if (message?.includes("fetch failed") || message?.includes("Connection refused")) {
28265
+ console.error("Daemon is not running");
28266
+ process.exit(1);
28267
+ }
28268
+ console.error(message);
28269
+ process.exit(1);
28270
+ }
28271
+ });
27726
28272
  var walletReceiveCmd = walletCmd.command("receive").description("Wallet receive operations");
27727
28273
  walletReceiveCmd.command("cashu <token>").description("Receive a Cashu token").action(async (token) => {
27728
28274
  await handleDaemonCommand("/wallet/receive/cashu", {
@@ -27890,7 +28436,7 @@ serviceCmd.command("install").description("Install and start routstrd using PM2
27890
28436
  const path = __require("path");
27891
28437
  daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
27892
28438
  }
27893
- if (!existsSync11(daemonPath)) {
28439
+ if (!existsSync14(daemonPath)) {
27894
28440
  console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
27895
28441
  process.exit(1);
27896
28442
  }
@@ -27938,7 +28484,7 @@ program.command("restart").description("Restart the background daemon").option("
27938
28484
  for (let i2 = 0;i2 < 100; i2++) {
27939
28485
  await new Promise((resolve) => setTimeout(resolve, 100));
27940
28486
  const healthDown = !await isDaemonRunning();
27941
- const pidFileReleased = !existsSync11(pidFilePath);
28487
+ const pidFileReleased = !existsSync14(pidFilePath);
27942
28488
  if (healthDown && pidFileReleased) {
27943
28489
  break;
27944
28490
  }
@@ -27996,7 +28542,7 @@ xcashu mode is coming soon! Only lazyrefund/apikeys is available at this time.`)
27996
28542
  ...config,
27997
28543
  mode: selectedMode
27998
28544
  };
27999
- await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
28545
+ saveDaemonConfig(updatedConfig);
28000
28546
  console.log(`Mode set to '${selectedMode}'. Restarting daemon...`);
28001
28547
  const wasRunning = await isDaemonRunning();
28002
28548
  if (wasRunning) {
@@ -28029,7 +28575,7 @@ function getLogFileForDate2(logsDir, date = new Date) {
28029
28575
  return `${logsDir}/${year}-${month}-${day}.log`;
28030
28576
  }
28031
28577
  function readLastLines(file, lines) {
28032
- const content = readFileSync3(file, "utf8");
28578
+ const content = readFileSync5(file, "utf8");
28033
28579
  const allLines = content.replace(/\r\n/g, `
28034
28580
  `).split(`
28035
28581
  `);
@@ -28043,13 +28589,13 @@ async function followLogFile(file, lines) {
28043
28589
  if (initial) {
28044
28590
  console.log(initial);
28045
28591
  }
28046
- let position = statSync2(file).size;
28592
+ let position = statSync4(file).size;
28047
28593
  while (true) {
28048
28594
  await new Promise((resolve) => setTimeout(resolve, 1000));
28049
- if (!existsSync11(file)) {
28595
+ if (!existsSync14(file)) {
28050
28596
  continue;
28051
28597
  }
28052
- const size = statSync2(file).size;
28598
+ const size = statSync4(file).size;
28053
28599
  if (size < position) {
28054
28600
  position = 0;
28055
28601
  }
@@ -28068,17 +28614,17 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
28068
28614
  const yesterday = new Date;
28069
28615
  yesterday.setDate(yesterday.getDate() - 1);
28070
28616
  const yesterdayFile = getLogFileForDate2(logsDir, yesterday);
28071
- if (!existsSync11(todayFile) && !existsSync11(yesterdayFile)) {
28617
+ if (!existsSync14(todayFile) && !existsSync14(yesterdayFile)) {
28072
28618
  console.log("No log files found. Daemon may not have started yet.");
28073
28619
  console.log(`Logs directory: ${logsDir}`);
28074
28620
  process.exit(1);
28075
28621
  }
28076
28622
  const lines = parseInt(options.lines, 10);
28077
28623
  const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
28078
- return existsSync11(file) && files.indexOf(file) === index;
28624
+ return existsSync14(file) && files.indexOf(file) === index;
28079
28625
  });
28080
28626
  if (options.follow) {
28081
- if (existsSync11(todayFile)) {
28627
+ if (existsSync14(todayFile)) {
28082
28628
  await followLogFile(todayFile, lines);
28083
28629
  } else {
28084
28630
  console.log("No log file for today to follow.");