routstrd 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/bun.lock +2 -2
- package/dist/daemon/index.js +147 -60
- package/dist/index.js +79 -54
- package/package.json +2 -2
- package/src/cli.ts +113 -65
package/bun.lock
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "routstrd",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@cashu/cashu-ts": "^3.1.1",
|
|
8
|
-
"@routstr/sdk": "^0.2.
|
|
8
|
+
"@routstr/sdk": "^0.2.8",
|
|
9
9
|
"applesauce-core": "^5.1.0",
|
|
10
10
|
"applesauce-relay": "^5.1.0",
|
|
11
11
|
"commander": "^14.0.2",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
|
33
33
|
|
|
34
|
-
"@routstr/sdk": ["@routstr/sdk@0.2.
|
|
34
|
+
"@routstr/sdk": ["@routstr/sdk@0.2.8", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^11.7.2" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-Pe1C6LtGbn4NaTGbR/SRNCo8P0XWNIVk+TXvEMNBQc5h4Gr/Jn4eLtOSgFg6jMxzJcuoVPrDXD5EQICWDotVQw=="],
|
|
35
35
|
|
|
36
36
|
"@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="],
|
|
37
37
|
|
package/dist/daemon/index.js
CHANGED
|
@@ -29475,17 +29475,49 @@ function extractResponseId(body) {
|
|
|
29475
29475
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
29476
29476
|
}
|
|
29477
29477
|
function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
|
|
29478
|
-
if (!parsed || typeof parsed !== "object"
|
|
29478
|
+
if (!parsed || typeof parsed !== "object") {
|
|
29479
|
+
return null;
|
|
29480
|
+
}
|
|
29481
|
+
if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
|
|
29482
|
+
const costObj = parsed.cost;
|
|
29483
|
+
const msats2 = costObj.total_msats ?? 0;
|
|
29484
|
+
const cost2 = costObj.total_usd ?? 0;
|
|
29485
|
+
if (msats2 === 0 && cost2 === 0)
|
|
29486
|
+
return null;
|
|
29487
|
+
return {
|
|
29488
|
+
promptTokens: Number(costObj.input_tokens ?? 0),
|
|
29489
|
+
completionTokens: Number(costObj.output_tokens ?? 0),
|
|
29490
|
+
totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
|
|
29491
|
+
cost: Number(cost2),
|
|
29492
|
+
satsCost: msats2 > 0 ? msats2 / 1000 : fallbackSatsCost
|
|
29493
|
+
};
|
|
29494
|
+
}
|
|
29495
|
+
if (!parsed.usage) {
|
|
29479
29496
|
return null;
|
|
29480
29497
|
}
|
|
29481
29498
|
const usage = parsed.usage;
|
|
29482
29499
|
const usageCost = usage.cost;
|
|
29483
|
-
|
|
29484
|
-
|
|
29500
|
+
let cost = 0;
|
|
29501
|
+
let msats = 0;
|
|
29502
|
+
if (typeof usageCost === "number") {
|
|
29503
|
+
cost = usageCost;
|
|
29504
|
+
} else if (usageCost && typeof usageCost === "object") {
|
|
29505
|
+
cost = usageCost.total_usd ?? 0;
|
|
29506
|
+
msats = usageCost.total_msats ?? 0;
|
|
29507
|
+
}
|
|
29508
|
+
if (cost === 0) {
|
|
29509
|
+
cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
|
|
29510
|
+
}
|
|
29511
|
+
if (msats === 0) {
|
|
29512
|
+
msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1000 : 0);
|
|
29513
|
+
}
|
|
29514
|
+
const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
|
|
29515
|
+
const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
|
|
29516
|
+
const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
|
|
29485
29517
|
const result = {
|
|
29486
|
-
promptTokens
|
|
29487
|
-
completionTokens
|
|
29488
|
-
totalTokens
|
|
29518
|
+
promptTokens,
|
|
29519
|
+
completionTokens,
|
|
29520
|
+
totalTokens,
|
|
29489
29521
|
cost: Number(cost ?? 0),
|
|
29490
29522
|
satsCost: msats > 0 ? msats / 1000 : fallbackSatsCost
|
|
29491
29523
|
};
|
|
@@ -29653,71 +29685,83 @@ function createSSEParserTransform(onUsage, onResponseId) {
|
|
|
29653
29685
|
let buffer = "";
|
|
29654
29686
|
let usageCaptured = false;
|
|
29655
29687
|
let responseIdCaptured = false;
|
|
29656
|
-
const
|
|
29688
|
+
const inspectDataPayload = (jsonText) => {
|
|
29689
|
+
if (usageCaptured && responseIdCaptured)
|
|
29690
|
+
return;
|
|
29691
|
+
const trimmed = jsonText.trim();
|
|
29692
|
+
if (!trimmed || trimmed === "[DONE]")
|
|
29693
|
+
return;
|
|
29694
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
29695
|
+
return;
|
|
29657
29696
|
try {
|
|
29658
|
-
const data = JSON.parse(
|
|
29659
|
-
|
|
29660
|
-
|
|
29661
|
-
|
|
29662
|
-
|
|
29697
|
+
const data = JSON.parse(trimmed);
|
|
29698
|
+
if (!responseIdCaptured) {
|
|
29699
|
+
const responseId = data?.id;
|
|
29700
|
+
if (typeof responseId === "string" && responseId.trim().length > 0) {
|
|
29701
|
+
onResponseId?.(responseId.trim());
|
|
29702
|
+
responseIdCaptured = true;
|
|
29703
|
+
}
|
|
29663
29704
|
}
|
|
29664
|
-
|
|
29665
|
-
|
|
29666
|
-
|
|
29667
|
-
|
|
29705
|
+
if (!usageCaptured) {
|
|
29706
|
+
const usage = extractUsageFromSSEJson(data);
|
|
29707
|
+
if (usage) {
|
|
29708
|
+
onUsage(usage);
|
|
29709
|
+
usageCaptured = true;
|
|
29710
|
+
}
|
|
29668
29711
|
}
|
|
29669
29712
|
} catch {}
|
|
29670
29713
|
};
|
|
29671
|
-
const
|
|
29672
|
-
|
|
29673
|
-
if (!trimmed) {
|
|
29714
|
+
const inspectEventBlock = (eventBlock) => {
|
|
29715
|
+
if (usageCaptured && responseIdCaptured)
|
|
29674
29716
|
return;
|
|
29717
|
+
const lines = eventBlock.split(/\r?\n/);
|
|
29718
|
+
const dataParts = [];
|
|
29719
|
+
for (const line of lines) {
|
|
29720
|
+
if (!line || line.startsWith(":"))
|
|
29721
|
+
continue;
|
|
29722
|
+
if (line.startsWith("data:")) {
|
|
29723
|
+
const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
|
|
29724
|
+
dataParts.push(value);
|
|
29725
|
+
}
|
|
29675
29726
|
}
|
|
29676
|
-
if (
|
|
29677
|
-
self.push(`data: [DONE]
|
|
29678
|
-
|
|
29679
|
-
`);
|
|
29727
|
+
if (dataParts.length === 0)
|
|
29680
29728
|
return;
|
|
29681
|
-
|
|
29682
|
-
if (trimmed.startsWith("data:")) {
|
|
29683
|
-
const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
|
|
29684
|
-
if (dataStr === "[DONE]") {
|
|
29685
|
-
self.push(`data: [DONE]
|
|
29686
|
-
|
|
29687
|
-
`);
|
|
29688
|
-
return;
|
|
29689
|
-
}
|
|
29690
|
-
maybeCaptureUsageFromJson(dataStr);
|
|
29691
|
-
self.push(`data: ${dataStr}
|
|
29692
|
-
|
|
29729
|
+
const payload = dataParts.join(`
|
|
29693
29730
|
`);
|
|
29731
|
+
inspectDataPayload(payload);
|
|
29732
|
+
};
|
|
29733
|
+
const emitEventBlock = (self, eventBlock) => {
|
|
29734
|
+
if (eventBlock.length === 0)
|
|
29694
29735
|
return;
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
maybeCaptureUsageFromJson(trimmed);
|
|
29698
|
-
self.push(`data: ${trimmed}
|
|
29736
|
+
inspectEventBlock(eventBlock);
|
|
29737
|
+
self.push(eventBlock + `
|
|
29699
29738
|
|
|
29700
|
-
`);
|
|
29701
|
-
return;
|
|
29702
|
-
}
|
|
29703
|
-
self.push(line + `
|
|
29704
29739
|
`);
|
|
29705
29740
|
};
|
|
29706
29741
|
return new Transform({
|
|
29707
|
-
transform(chunk,
|
|
29742
|
+
transform(chunk, _encoding, callback) {
|
|
29708
29743
|
buffer += chunk.toString();
|
|
29709
|
-
const
|
|
29710
|
-
|
|
29711
|
-
|
|
29712
|
-
|
|
29744
|
+
const terminator = /\r?\n\r?\n/g;
|
|
29745
|
+
let lastIndex = 0;
|
|
29746
|
+
let match;
|
|
29747
|
+
while ((match = terminator.exec(buffer)) !== null) {
|
|
29748
|
+
const block = buffer.slice(lastIndex, match.index);
|
|
29749
|
+
lastIndex = match.index + match[0].length;
|
|
29750
|
+
emitEventBlock(this, block);
|
|
29751
|
+
}
|
|
29752
|
+
if (lastIndex > 0) {
|
|
29753
|
+
buffer = buffer.slice(lastIndex);
|
|
29713
29754
|
}
|
|
29714
29755
|
callback();
|
|
29715
29756
|
},
|
|
29716
29757
|
flush(callback) {
|
|
29717
|
-
if (buffer.
|
|
29718
|
-
|
|
29758
|
+
if (buffer.length > 0) {
|
|
29759
|
+
const tail = buffer.replace(/\r?\n+$/, "");
|
|
29760
|
+
if (tail.length > 0) {
|
|
29761
|
+
emitEventBlock(this, tail);
|
|
29762
|
+
}
|
|
29763
|
+
buffer = "";
|
|
29719
29764
|
}
|
|
29720
|
-
buffer = "";
|
|
29721
29765
|
callback();
|
|
29722
29766
|
}
|
|
29723
29767
|
});
|
|
@@ -31428,11 +31472,14 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
|
|
|
31428
31472
|
if (parsed.choices?.[0]?.delta?.reasoning) {
|
|
31429
31473
|
result.reasoning = parsed.choices[0].delta.reasoning;
|
|
31430
31474
|
}
|
|
31431
|
-
|
|
31432
|
-
|
|
31433
|
-
|
|
31434
|
-
|
|
31435
|
-
|
|
31475
|
+
const extractedUsage = extractUsageFromSSEJson(parsed);
|
|
31476
|
+
if (extractedUsage) {
|
|
31477
|
+
result.usage = toUsageStats(extractedUsage);
|
|
31478
|
+
} else if (parsed.usage) {
|
|
31479
|
+
result.usage = {
|
|
31480
|
+
total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
|
|
31481
|
+
prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
|
|
31482
|
+
completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
|
|
31436
31483
|
};
|
|
31437
31484
|
}
|
|
31438
31485
|
if (parsed.id) {
|
|
@@ -31725,23 +31772,62 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
|
|
|
31725
31772
|
try {
|
|
31726
31773
|
const torMode = isTorContext();
|
|
31727
31774
|
const disabledProviders = new Set(this.providerRegistry.getDisabledProviders());
|
|
31775
|
+
console.log(`[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`);
|
|
31776
|
+
console.log(`[findNextBestProvider:${this.instanceId}] currentBaseUrl: ${currentBaseUrl}`);
|
|
31777
|
+
console.log(`[findNextBestProvider:${this.instanceId}] torMode: ${torMode}`);
|
|
31778
|
+
console.log(`[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`);
|
|
31779
|
+
console.log(`[findNextBestProvider:${this.instanceId}] failedProviders: ${[...this.failedProviders]}`);
|
|
31780
|
+
console.log(`[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url2]) => url2)}`);
|
|
31728
31781
|
const allProviders = this.providerRegistry.getAllProvidersModels();
|
|
31782
|
+
console.log(`[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`);
|
|
31729
31783
|
const candidates = [];
|
|
31784
|
+
let skippedCurrent = 0, skippedFailed = 0, skippedDisabled = 0, skippedCooldown = 0, skippedOnion = 0, skippedNoModel = 0;
|
|
31730
31785
|
for (const [baseUrl, models] of Object.entries(allProviders)) {
|
|
31731
|
-
if (baseUrl === currentBaseUrl
|
|
31786
|
+
if (baseUrl === currentBaseUrl) {
|
|
31787
|
+
console.log(`[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`);
|
|
31788
|
+
skippedCurrent++;
|
|
31789
|
+
continue;
|
|
31790
|
+
}
|
|
31791
|
+
if (this.failedProviders.has(baseUrl)) {
|
|
31792
|
+
console.log(`[findNextBestProvider:${this.instanceId}] SKIP (failed): ${baseUrl}`);
|
|
31793
|
+
skippedFailed++;
|
|
31794
|
+
continue;
|
|
31795
|
+
}
|
|
31796
|
+
if (disabledProviders.has(baseUrl)) {
|
|
31797
|
+
console.log(`[findNextBestProvider:${this.instanceId}] SKIP (disabled): ${baseUrl}`);
|
|
31798
|
+
skippedDisabled++;
|
|
31799
|
+
continue;
|
|
31800
|
+
}
|
|
31801
|
+
if (this.isOnCooldown(baseUrl)) {
|
|
31802
|
+
console.log(`[findNextBestProvider:${this.instanceId}] SKIP (cooldown): ${baseUrl}`);
|
|
31803
|
+
skippedCooldown++;
|
|
31732
31804
|
continue;
|
|
31733
31805
|
}
|
|
31734
31806
|
if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
|
|
31807
|
+
console.log(`[findNextBestProvider:${this.instanceId}] SKIP (onion/http): ${baseUrl}`);
|
|
31808
|
+
skippedOnion++;
|
|
31735
31809
|
continue;
|
|
31736
31810
|
}
|
|
31737
31811
|
const model2 = models.find((m2) => m2.id === modelId);
|
|
31738
|
-
if (!model2)
|
|
31812
|
+
if (!model2) {
|
|
31813
|
+
console.log(`[findNextBestProvider:${this.instanceId}] SKIP (no model ${modelId}): ${baseUrl} has models: ${models.map((m2) => m2.id).join(", ")}`);
|
|
31814
|
+
skippedNoModel++;
|
|
31739
31815
|
continue;
|
|
31816
|
+
}
|
|
31740
31817
|
const cost = model2.sats_pricing?.completion ?? 0;
|
|
31818
|
+
console.log(`[findNextBestProvider:${this.instanceId}] CANDIDATE: ${baseUrl} cost: ${cost}`);
|
|
31741
31819
|
candidates.push({ baseUrl, model: model2, cost });
|
|
31742
31820
|
}
|
|
31821
|
+
console.log(`[findNextBestProvider:${this.instanceId}] Skipped: current=${skippedCurrent}, failed=${skippedFailed}, disabled=${skippedDisabled}, cooldown=${skippedCooldown}, onion=${skippedOnion}, noModel=${skippedNoModel}`);
|
|
31822
|
+
console.log(`[findNextBestProvider:${this.instanceId}] Total candidates: ${candidates.length}`);
|
|
31743
31823
|
candidates.sort((a, b) => a.cost - b.cost);
|
|
31744
|
-
|
|
31824
|
+
if (candidates.length > 0) {
|
|
31825
|
+
console.log(`[findNextBestProvider:${this.instanceId}] Selected provider: ${candidates[0].baseUrl} with cost: ${candidates[0].cost}`);
|
|
31826
|
+
return candidates[0].baseUrl;
|
|
31827
|
+
} else {
|
|
31828
|
+
console.log(`[findNextBestProvider:${this.instanceId}] No candidate providers found`);
|
|
31829
|
+
return null;
|
|
31830
|
+
}
|
|
31745
31831
|
} catch (error) {
|
|
31746
31832
|
console.error("Error finding next best provider:", error);
|
|
31747
31833
|
return null;
|
|
@@ -33660,7 +33746,8 @@ var import_rxjs24, InsufficientBalanceError, ProviderError, MintUnreachableError
|
|
|
33660
33746
|
const MAX_RETRIES_PER_PROVIDER = 2;
|
|
33661
33747
|
const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
|
|
33662
33748
|
let tryNextProvider = false;
|
|
33663
|
-
|
|
33749
|
+
const errorMessage = responseBody;
|
|
33750
|
+
this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}, errorMessage=${errorMessage}`);
|
|
33664
33751
|
this._log("DEBUG", `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`);
|
|
33665
33752
|
if (params.token.startsWith("cashu")) {
|
|
33666
33753
|
const receiveResult = await this.cashuSpender.receiveToken(params.token);
|
package/dist/index.js
CHANGED
|
@@ -28790,6 +28790,7 @@ async function startDaemon(options = {}) {
|
|
|
28790
28790
|
init_cli_shared();
|
|
28791
28791
|
init_config();
|
|
28792
28792
|
import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
28793
|
+
import { execSync } from "child_process";
|
|
28793
28794
|
|
|
28794
28795
|
// src/integrations/opencode.ts
|
|
28795
28796
|
import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
@@ -36953,6 +36954,8 @@ ${initStderr}`.toLowerCase();
|
|
|
36953
36954
|
Initialization complete!`);
|
|
36954
36955
|
logger.log(`
|
|
36955
36956
|
use 'routstrd wallet receive cashu <token>' or 'routstrd wallet receive bolt11 2100' to top up your local wallet!`);
|
|
36957
|
+
logger.log(`
|
|
36958
|
+
To ensure routstrd persists across system restarts, run: 'routstrd service install'`);
|
|
36956
36959
|
}
|
|
36957
36960
|
async function checkCocodInstalled() {
|
|
36958
36961
|
try {
|
|
@@ -37259,6 +37262,10 @@ program.command("monitor").description("Open interactive TUI for usage monitorin
|
|
|
37259
37262
|
const { runUsageTui: runUsageTui2 } = await Promise.resolve().then(() => (init_usage(), exports_usage));
|
|
37260
37263
|
await runUsageTui2();
|
|
37261
37264
|
});
|
|
37265
|
+
program.command("top").description("Open interactive TUI for usage monitoring (alias for monitor)").action(async () => {
|
|
37266
|
+
const { runUsageTui: runUsageTui2 } = await Promise.resolve().then(() => (init_usage(), exports_usage));
|
|
37267
|
+
await runUsageTui2();
|
|
37268
|
+
});
|
|
37262
37269
|
var walletCmd = program.command("wallet").description("Wallet operations");
|
|
37263
37270
|
walletCmd.command("status").description("Check wallet status").action(async () => {
|
|
37264
37271
|
await handleDaemonCommand("/wallet/status");
|
|
@@ -37340,6 +37347,62 @@ walletMintsCmd.command("info <url>").description("Get wallet mint info").action(
|
|
|
37340
37347
|
program.command("stop").description("Stop the background daemon").action(async () => {
|
|
37341
37348
|
await handleDaemonCommand("/stop", { method: "POST" });
|
|
37342
37349
|
});
|
|
37350
|
+
var serviceCmd = program.command("service").description("Manage routstrd as a system service using PM2");
|
|
37351
|
+
serviceCmd.command("install").description("Install and start routstrd using PM2 for persistence").action(async () => {
|
|
37352
|
+
try {
|
|
37353
|
+
execSync("pm2 -v", { stdio: "ignore" });
|
|
37354
|
+
} catch (e) {
|
|
37355
|
+
console.log("PM2 not found. Installing PM2 globally with bun...");
|
|
37356
|
+
try {
|
|
37357
|
+
execSync("bun install -g pm2", { stdio: "inherit" });
|
|
37358
|
+
} catch (err) {
|
|
37359
|
+
console.error("Failed to install PM2. Please install it manually: bun install -g pm2");
|
|
37360
|
+
process.exit(1);
|
|
37361
|
+
}
|
|
37362
|
+
}
|
|
37363
|
+
let daemonPath;
|
|
37364
|
+
try {
|
|
37365
|
+
daemonPath = Bun.resolveSync("./daemon/index.js", import.meta.url);
|
|
37366
|
+
} catch (e) {
|
|
37367
|
+
const path = __require("path");
|
|
37368
|
+
daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
|
|
37369
|
+
}
|
|
37370
|
+
if (!existsSync8(daemonPath)) {
|
|
37371
|
+
console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
|
|
37372
|
+
process.exit(1);
|
|
37373
|
+
}
|
|
37374
|
+
console.log("Starting routstrd via PM2...");
|
|
37375
|
+
try {
|
|
37376
|
+
execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, {
|
|
37377
|
+
stdio: "inherit"
|
|
37378
|
+
});
|
|
37379
|
+
console.log(`
|
|
37380
|
+
\u2705 routstrd is now managed by PM2.`);
|
|
37381
|
+
console.log(`
|
|
37382
|
+
To ensure it starts on system reboot, run:`);
|
|
37383
|
+
console.log(" pm2 startup");
|
|
37384
|
+
console.log(" pm2 save");
|
|
37385
|
+
console.log(`
|
|
37386
|
+
To view logs:`);
|
|
37387
|
+
console.log(" pm2 logs routstrd");
|
|
37388
|
+
} catch (err) {
|
|
37389
|
+
console.error("Failed to start routstrd via PM2.");
|
|
37390
|
+
process.exit(1);
|
|
37391
|
+
}
|
|
37392
|
+
});
|
|
37393
|
+
serviceCmd.command("uninstall").description("Stop and remove routstrd from PM2").action(() => {
|
|
37394
|
+
try {
|
|
37395
|
+
execSync("pm2 delete routstrd", { stdio: "inherit" });
|
|
37396
|
+
console.log("\u2705 routstrd service removed from PM2.");
|
|
37397
|
+
} catch (e) {
|
|
37398
|
+
console.error("Failed to remove service. It might not be running in PM2.");
|
|
37399
|
+
}
|
|
37400
|
+
});
|
|
37401
|
+
serviceCmd.command("logs").description("View PM2 logs for routstrd").action(() => {
|
|
37402
|
+
try {
|
|
37403
|
+
execSync("pm2 logs routstrd", { stdio: "inherit" });
|
|
37404
|
+
} catch (e) {}
|
|
37405
|
+
});
|
|
37343
37406
|
program.command("restart").description("Restart the background daemon").option("--port <port>", "Port to listen on").option("-p, --provider <provider>", "Default provider to use").action(async (options) => {
|
|
37344
37407
|
const config = await loadConfig();
|
|
37345
37408
|
const wasRunning = await isDaemonRunning();
|
|
@@ -37444,63 +37507,25 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
|
|
|
37444
37507
|
process.exit(1);
|
|
37445
37508
|
}
|
|
37446
37509
|
const lines = parseInt(options.lines, 10);
|
|
37447
|
-
const
|
|
37448
|
-
|
|
37449
|
-
|
|
37450
|
-
const yesterdayContent = await Bun.file(yesterdayFile).text();
|
|
37451
|
-
allLines = yesterdayContent.split(`
|
|
37452
|
-
`).filter(Boolean);
|
|
37453
|
-
}
|
|
37454
|
-
if (existsSync8(todayFile)) {
|
|
37455
|
-
const todayContent = await Bun.file(todayFile).text();
|
|
37456
|
-
allLines = allLines.concat(todayContent.split(`
|
|
37457
|
-
`).filter(Boolean));
|
|
37458
|
-
}
|
|
37459
|
-
return allLines.slice(-lines);
|
|
37460
|
-
};
|
|
37461
|
-
const printLines = async () => {
|
|
37462
|
-
const lastLines = await readLastLines();
|
|
37463
|
-
for (const line of lastLines) {
|
|
37464
|
-
console.log(line);
|
|
37465
|
-
}
|
|
37466
|
-
};
|
|
37510
|
+
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
|
|
37511
|
+
return existsSync8(file) && files.indexOf(file) === index;
|
|
37512
|
+
});
|
|
37467
37513
|
if (options.follow) {
|
|
37468
|
-
|
|
37469
|
-
|
|
37470
|
-
|
|
37471
|
-
|
|
37472
|
-
}
|
|
37473
|
-
await printLines();
|
|
37474
|
-
const interval = setInterval(async () => {
|
|
37475
|
-
const newLogFile = getLogFileForDate2();
|
|
37476
|
-
if (newLogFile !== currentLogFile) {
|
|
37477
|
-
console.log(`
|
|
37478
|
-
--- Switched to ${newLogFile} ---
|
|
37479
|
-
`);
|
|
37480
|
-
currentLogFile = newLogFile;
|
|
37481
|
-
lastSize = existsSync8(currentLogFile) ? (await Bun.file(currentLogFile).text()).length : 0;
|
|
37482
|
-
}
|
|
37483
|
-
if (existsSync8(currentLogFile)) {
|
|
37484
|
-
const content2 = await Bun.file(currentLogFile).text();
|
|
37485
|
-
const currentSize = content2.length;
|
|
37486
|
-
if (currentSize > lastSize) {
|
|
37487
|
-
const allLines = content2.split(`
|
|
37488
|
-
`).filter(Boolean);
|
|
37489
|
-
const newLines = allLines.slice(Math.floor(lastSize === 0 ? 0 : -1), -1);
|
|
37490
|
-
for (const line of newLines) {
|
|
37491
|
-
console.log(line);
|
|
37492
|
-
}
|
|
37493
|
-
lastSize = currentSize;
|
|
37494
|
-
}
|
|
37495
|
-
}
|
|
37496
|
-
}, 1000);
|
|
37497
|
-
process.on("SIGINT", () => {
|
|
37498
|
-
clearInterval(interval);
|
|
37499
|
-
process.exit(0);
|
|
37514
|
+
const proc2 = Bun.spawn(["tail", "-n", String(lines), "-f", todayFile], {
|
|
37515
|
+
stdout: "inherit",
|
|
37516
|
+
stderr: "inherit",
|
|
37517
|
+
stdin: "inherit"
|
|
37500
37518
|
});
|
|
37501
|
-
|
|
37502
|
-
|
|
37519
|
+
const exitCode2 = await proc2.exited;
|
|
37520
|
+
process.exit(exitCode2);
|
|
37503
37521
|
}
|
|
37522
|
+
const proc = Bun.spawn(["tail", "-n", String(lines), ...logFiles], {
|
|
37523
|
+
stdout: "inherit",
|
|
37524
|
+
stderr: "inherit",
|
|
37525
|
+
stdin: "inherit"
|
|
37526
|
+
});
|
|
37527
|
+
const exitCode = await proc.exited;
|
|
37528
|
+
process.exit(exitCode);
|
|
37504
37529
|
});
|
|
37505
37530
|
function cli(args) {
|
|
37506
37531
|
program.parse(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@cashu/cashu-ts": "^3.1.1",
|
|
27
|
-
"@routstr/sdk": "^0.2.
|
|
27
|
+
"@routstr/sdk": "^0.2.11",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"commander": "^14.0.2",
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
loadConfig,
|
|
9
9
|
} from "./cli-shared";
|
|
10
10
|
import { existsSync, mkdirSync } from "fs";
|
|
11
|
+
import { execSync } from "child_process";
|
|
11
12
|
import {
|
|
12
13
|
CONFIG_DIR,
|
|
13
14
|
DB_PATH,
|
|
@@ -203,6 +204,9 @@ async function initDaemon(): Promise<void> {
|
|
|
203
204
|
logger.log(
|
|
204
205
|
"\n use 'routstrd wallet receive cashu <token>' or 'routstrd wallet receive bolt11 2100' to top up your local wallet!",
|
|
205
206
|
);
|
|
207
|
+
logger.log(
|
|
208
|
+
"\nTo ensure routstrd persists across system restarts, run: 'routstrd service install'",
|
|
209
|
+
);
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
async function checkCocodInstalled(): Promise<boolean> {
|
|
@@ -721,6 +725,14 @@ program
|
|
|
721
725
|
await runUsageTui();
|
|
722
726
|
});
|
|
723
727
|
|
|
728
|
+
program
|
|
729
|
+
.command("top")
|
|
730
|
+
.description("Open interactive TUI for usage monitoring (alias for monitor)")
|
|
731
|
+
.action(async () => {
|
|
732
|
+
const { runUsageTui } = await import("./tui/usage/index.ts");
|
|
733
|
+
await runUsageTui();
|
|
734
|
+
});
|
|
735
|
+
|
|
724
736
|
const walletCmd = program.command("wallet").description("Wallet operations");
|
|
725
737
|
|
|
726
738
|
walletCmd
|
|
@@ -866,6 +878,89 @@ program
|
|
|
866
878
|
await handleDaemonCommand("/stop", { method: "POST" });
|
|
867
879
|
});
|
|
868
880
|
|
|
881
|
+
// Service - PM2 management
|
|
882
|
+
const serviceCmd = program
|
|
883
|
+
.command("service")
|
|
884
|
+
.description("Manage routstrd as a system service using PM2");
|
|
885
|
+
|
|
886
|
+
serviceCmd
|
|
887
|
+
.command("install")
|
|
888
|
+
.description("Install and start routstrd using PM2 for persistence")
|
|
889
|
+
.action(async () => {
|
|
890
|
+
// 1. Check if PM2 is installed
|
|
891
|
+
try {
|
|
892
|
+
execSync("pm2 -v", { stdio: "ignore" });
|
|
893
|
+
} catch (e) {
|
|
894
|
+
console.log("PM2 not found. Installing PM2 globally with bun...");
|
|
895
|
+
try {
|
|
896
|
+
execSync("bun install -g pm2", { stdio: "inherit" });
|
|
897
|
+
} catch (err) {
|
|
898
|
+
console.error("Failed to install PM2. Please install it manually: bun install -g pm2");
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// 2. Resolve the path to the daemon
|
|
904
|
+
// In a global install, we want the bundled daemon in dist/daemon/index.js
|
|
905
|
+
let daemonPath: string;
|
|
906
|
+
try {
|
|
907
|
+
// Try to resolve relative to this file first (works in dev and global)
|
|
908
|
+
daemonPath = Bun.resolveSync("./daemon/index.js", import.meta.url);
|
|
909
|
+
} catch (e) {
|
|
910
|
+
// Fallback for some bundling scenarios
|
|
911
|
+
const path = require("path");
|
|
912
|
+
daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (!existsSync(daemonPath)) {
|
|
916
|
+
console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
console.log("Starting routstrd via PM2...");
|
|
921
|
+
try {
|
|
922
|
+
// Use --interpreter bun to ensure it runs with bun
|
|
923
|
+
execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, {
|
|
924
|
+
stdio: "inherit",
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
console.log("\n✅ routstrd is now managed by PM2.");
|
|
928
|
+
console.log("\nTo ensure it starts on system reboot, run:");
|
|
929
|
+
console.log(" pm2 startup");
|
|
930
|
+
console.log(" pm2 save");
|
|
931
|
+
console.log("\nTo view logs:");
|
|
932
|
+
console.log(" pm2 logs routstrd");
|
|
933
|
+
} catch (err) {
|
|
934
|
+
console.error("Failed to start routstrd via PM2.");
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
serviceCmd
|
|
940
|
+
.command("uninstall")
|
|
941
|
+
.description("Stop and remove routstrd from PM2")
|
|
942
|
+
.action(() => {
|
|
943
|
+
try {
|
|
944
|
+
execSync("pm2 delete routstrd", { stdio: "inherit" });
|
|
945
|
+
console.log("✅ routstrd service removed from PM2.");
|
|
946
|
+
} catch (e) {
|
|
947
|
+
console.error(
|
|
948
|
+
"Failed to remove service. It might not be running in PM2.",
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
serviceCmd
|
|
954
|
+
.command("logs")
|
|
955
|
+
.description("View PM2 logs for routstrd")
|
|
956
|
+
.action(() => {
|
|
957
|
+
try {
|
|
958
|
+
execSync("pm2 logs routstrd", { stdio: "inherit" });
|
|
959
|
+
} catch (e) {
|
|
960
|
+
// Ignored
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
869
964
|
// Restart
|
|
870
965
|
program
|
|
871
966
|
.command("restart")
|
|
@@ -1013,76 +1108,29 @@ program
|
|
|
1013
1108
|
|
|
1014
1109
|
const lines = parseInt(options.lines, 10);
|
|
1015
1110
|
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
// Read yesterday's log first if it exists
|
|
1020
|
-
if (existsSync(yesterdayFile)) {
|
|
1021
|
-
const yesterdayContent = await Bun.file(yesterdayFile).text();
|
|
1022
|
-
allLines = yesterdayContent.split("\n").filter(Boolean);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// Then read today's log
|
|
1026
|
-
if (existsSync(todayFile)) {
|
|
1027
|
-
const todayContent = await Bun.file(todayFile).text();
|
|
1028
|
-
allLines = allLines.concat(todayContent.split("\n").filter(Boolean));
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
return allLines.slice(-lines);
|
|
1032
|
-
};
|
|
1033
|
-
|
|
1034
|
-
const printLines = async (): Promise<void> => {
|
|
1035
|
-
const lastLines = await readLastLines();
|
|
1036
|
-
for (const line of lastLines) {
|
|
1037
|
-
console.log(line);
|
|
1038
|
-
}
|
|
1039
|
-
};
|
|
1111
|
+
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
|
|
1112
|
+
return existsSync(file) && files.indexOf(file) === index;
|
|
1113
|
+
});
|
|
1040
1114
|
|
|
1041
1115
|
if (options.follow) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1116
|
+
const proc = Bun.spawn(["tail", "-n", String(lines), "-f", todayFile], {
|
|
1117
|
+
stdout: "inherit",
|
|
1118
|
+
stderr: "inherit",
|
|
1119
|
+
stdin: "inherit",
|
|
1120
|
+
});
|
|
1048
1121
|
|
|
1049
|
-
await
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
// Check if we need to switch to a new date file
|
|
1053
|
-
const newLogFile = getLogFileForDate();
|
|
1054
|
-
if (newLogFile !== currentLogFile) {
|
|
1055
|
-
console.log(`\n--- Switched to ${newLogFile} ---\n`);
|
|
1056
|
-
currentLogFile = newLogFile;
|
|
1057
|
-
lastSize = existsSync(currentLogFile)
|
|
1058
|
-
? (await Bun.file(currentLogFile).text()).length
|
|
1059
|
-
: 0;
|
|
1060
|
-
}
|
|
1122
|
+
const exitCode = await proc.exited;
|
|
1123
|
+
process.exit(exitCode);
|
|
1124
|
+
}
|
|
1061
1125
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const newLines = allLines.slice(
|
|
1068
|
-
Math.floor(lastSize === 0 ? 0 : -1),
|
|
1069
|
-
-1,
|
|
1070
|
-
);
|
|
1071
|
-
for (const line of newLines) {
|
|
1072
|
-
console.log(line);
|
|
1073
|
-
}
|
|
1074
|
-
lastSize = currentSize;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}, 1000);
|
|
1126
|
+
const proc = Bun.spawn(["tail", "-n", String(lines), ...logFiles], {
|
|
1127
|
+
stdout: "inherit",
|
|
1128
|
+
stderr: "inherit",
|
|
1129
|
+
stdin: "inherit",
|
|
1130
|
+
});
|
|
1078
1131
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
process.exit(0);
|
|
1082
|
-
});
|
|
1083
|
-
} else {
|
|
1084
|
-
await printLines();
|
|
1085
|
-
}
|
|
1132
|
+
const exitCode = await proc.exited;
|
|
1133
|
+
process.exit(exitCode);
|
|
1086
1134
|
});
|
|
1087
1135
|
|
|
1088
1136
|
export function cli(args: string[]) {
|