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