routstrd 0.3.11 → 0.4.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/IMPLEMENTATION.md +253 -0
- package/README.md +41 -0
- package/SECURITY.md +23 -0
- package/SKILL.md +13 -3
- package/bun.lock +68 -41
- package/dist/daemon/index.js +47850 -11349
- package/dist/index.js +4557 -399
- package/package.json +8 -3
- package/src/cli.test.ts +55 -0
- package/src/cli.ts +454 -204
- package/src/daemon/args.ts +8 -1
- package/src/daemon/models.ts +119 -39
- package/src/daemon/wallet/coco-client.npc.test.ts +252 -0
- package/src/daemon/wallet/coco-client.test.ts +508 -0
- package/src/daemon/wallet/coco-client.ts +835 -0
- package/src/daemon/wallet/cocod-client.ts +86 -2
- package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
- package/src/daemon/wallet/migration.test.ts +147 -0
- package/src/daemon/wallet/migration.ts +209 -0
- package/src/daemon/wallet/paths.ts +33 -0
- package/src/integrations/claudecode.ts +2 -1
- package/src/integrations/hermes.ts +46 -6
- package/src/integrations/openclaw.ts +3 -2
- package/src/integrations/opencode.ts +3 -2
- package/src/integrations/pi.ts +3 -2
- package/src/integrations/registry.ts +7 -5
- package/src/start-daemon.ts +168 -22
- package/src/utils/clients.ts +21 -0
- package/src/utils/config.ts +11 -0
- package/src/utils/daemon-client.ts +104 -25
- package/src/utils/logger.ts +42 -28
- package/tests/utils/daemon-client.test.ts +61 -0
- package/tests/wallet/short-keyset-token.test.ts +106 -0
package/src/cli.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
deleteClientAction,
|
|
16
16
|
addClientAction,
|
|
17
17
|
} from "./utils/clients";
|
|
18
|
-
import { existsSync, mkdirSync } from "fs";
|
|
18
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
19
19
|
import { execSync } from "child_process";
|
|
20
|
+
import { dirname, join } from "path";
|
|
20
21
|
import {
|
|
21
22
|
CONFIG_DIR,
|
|
22
23
|
DB_PATH,
|
|
@@ -25,16 +26,26 @@ import {
|
|
|
25
26
|
LOGS_DIR,
|
|
26
27
|
type RoutstrdConfig,
|
|
27
28
|
} from "./utils/config";
|
|
28
|
-
import { logger } from "./utils/logger";
|
|
29
|
+
import { COCO_LOGS_DIR, logger } from "./utils/logger";
|
|
29
30
|
import { setupIntegration, runIntegrationsForClients } from "./integrations";
|
|
31
|
+
import {
|
|
32
|
+
assertLegacyCocodNotRunning,
|
|
33
|
+
claimLegacyCocodPidFile,
|
|
34
|
+
stopLegacyCocod,
|
|
35
|
+
} from "./daemon/wallet/coco-client";
|
|
36
|
+
import { migrateLegacyWallet } from "./daemon/wallet/migration";
|
|
37
|
+
import {
|
|
38
|
+
legacyCocodPidPath,
|
|
39
|
+
legacyCocodSocketPath,
|
|
40
|
+
walletDir as defaultWalletDir,
|
|
41
|
+
walletPidPath,
|
|
42
|
+
} from "./daemon/wallet/paths";
|
|
30
43
|
import { getClientsList } from "./utils/clients";
|
|
31
44
|
import * as QRCode from "qrcode";
|
|
32
45
|
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
|
33
46
|
import { generateSecretKey, nip19 } from "nostr-tools";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
resolveCocodExecutable,
|
|
37
|
-
} from "./daemon/wallet/cocod-client";
|
|
47
|
+
import { generateMnemonic } from "@scure/bip39";
|
|
48
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
38
49
|
import packageJson from "../package.json" with { type: "json" };
|
|
39
50
|
import {
|
|
40
51
|
compareVersions,
|
|
@@ -86,32 +97,42 @@ async function printLightningInvoice(invoice: string): Promise<void> {
|
|
|
86
97
|
console.log(`${qr}\nInvoice:\n${invoice}`);
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
export function initializeWallet(walletDir = defaultWalletDir()): void {
|
|
101
|
+
const walletConfig = join(walletDir, "config.json");
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
stderr: "inherit",
|
|
97
|
-
},
|
|
98
|
-
);
|
|
103
|
+
// The wallet directory and config contain the plaintext seed phrase. Correct
|
|
104
|
+
// permissions on existing installations as well as newly created ones.
|
|
105
|
+
mkdirSync(walletDir, { recursive: true, mode: 0o700 });
|
|
106
|
+
chmodSync(walletDir, 0o700);
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
throw new Error("cocod installation failed");
|
|
108
|
+
if (existsSync(walletConfig)) {
|
|
109
|
+
chmodSync(walletConfig, 0o600);
|
|
110
|
+
console.log("Wallet already initialized.");
|
|
111
|
+
return;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
|
|
114
|
+
const mnemonic = generateMnemonic(wordlist);
|
|
115
|
+
const config = {
|
|
116
|
+
version: 1,
|
|
117
|
+
mnemonic,
|
|
118
|
+
encrypted: false,
|
|
119
|
+
createdAt: new Date().toISOString(),
|
|
120
|
+
};
|
|
121
|
+
writeFileSync(walletConfig, JSON.stringify(config, null, 2), {
|
|
122
|
+
mode: 0o600,
|
|
123
|
+
flag: "wx",
|
|
124
|
+
});
|
|
125
|
+
console.log("Initialized. Mnemonic:", mnemonic);
|
|
126
|
+
console.log("IMPORTANT: Write down this mnemonic and keep it safe!");
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
/**
|
|
112
|
-
* Restart the routstrd
|
|
113
|
-
*
|
|
114
|
-
*
|
|
130
|
+
* Restart the routstrd daemon after an update so the new binary takes
|
|
131
|
+
* effect immediately. Failures are collected and reported but never
|
|
132
|
+
* roll back the update itself.
|
|
133
|
+
*
|
|
134
|
+
* Note: cocod is no longer a separate process — the wallet now runs
|
|
135
|
+
* in-process via coco-core, so there is nothing else to restart.
|
|
115
136
|
*/
|
|
116
137
|
async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
117
138
|
const config = await loadConfig();
|
|
@@ -129,23 +150,28 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
|
129
150
|
} else {
|
|
130
151
|
console.log("\nRestarting routstrd daemon...");
|
|
131
152
|
|
|
132
|
-
// Graceful stop — the /stop endpoint closes the HTTP server
|
|
133
|
-
// (draining active connections) then exits.
|
|
134
153
|
await callDaemon("/stop", { method: "POST" });
|
|
135
154
|
|
|
136
|
-
for
|
|
155
|
+
// Wait for HTTP health check to fail AND wallet lock to be released.
|
|
156
|
+
const pidFilePath = walletPidPath();
|
|
157
|
+
for (let i = 0; i < 100; i++) {
|
|
137
158
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
138
|
-
|
|
159
|
+
const healthDown = !(await isDaemonRunning());
|
|
160
|
+
const pidFileReleased = !existsSync(pidFilePath);
|
|
161
|
+
if (healthDown && pidFileReleased) break;
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
if (await isDaemonRunning()) {
|
|
142
|
-
throw new Error("routstrd did not stop within
|
|
165
|
+
throw new Error("routstrd did not stop within 10 seconds");
|
|
143
166
|
}
|
|
144
167
|
console.log("routstrd daemon stopped.");
|
|
145
168
|
|
|
169
|
+
await stopLegacyCocod();
|
|
170
|
+
|
|
146
171
|
console.log("Starting routstrd daemon...");
|
|
147
172
|
await startDaemon({
|
|
148
173
|
port: String(config.port || 8008),
|
|
174
|
+
host: config.host || undefined,
|
|
149
175
|
provider: config.provider || undefined,
|
|
150
176
|
});
|
|
151
177
|
console.log("routstrd daemon restarted.");
|
|
@@ -156,80 +182,19 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
|
156
182
|
}
|
|
157
183
|
}
|
|
158
184
|
|
|
159
|
-
// --- cocod daemon ---
|
|
160
|
-
try {
|
|
161
|
-
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
162
|
-
|
|
163
|
-
// Check whether cocod is currently running.
|
|
164
|
-
const pingProc = Bun.spawn([cocodExecutable, "ping"], {
|
|
165
|
-
stdout: "ignore",
|
|
166
|
-
stderr: "ignore",
|
|
167
|
-
});
|
|
168
|
-
const cocodWasRunning = (await pingProc.exited) === 0;
|
|
169
|
-
|
|
170
|
-
if (!cocodWasRunning) {
|
|
171
|
-
console.log("\ncocod daemon was not running — skipping restart.");
|
|
172
|
-
} else {
|
|
173
|
-
console.log("\nRestarting cocod daemon...");
|
|
174
|
-
|
|
175
|
-
const stopProc = Bun.spawn([cocodExecutable, "stop"], {
|
|
176
|
-
stdout: "inherit",
|
|
177
|
-
stderr: "inherit",
|
|
178
|
-
});
|
|
179
|
-
const stopCode = await stopProc.exited;
|
|
180
|
-
if (stopCode !== 0) {
|
|
181
|
-
throw new Error(`cocod stop exited with code ${stopCode}`);
|
|
182
|
-
}
|
|
183
|
-
console.log("cocod daemon stopped.");
|
|
184
|
-
|
|
185
|
-
// Start cocod in the background (detached, like the wallet client does).
|
|
186
|
-
const env = { ...process.env };
|
|
187
|
-
const startProc = Bun.spawn([cocodExecutable, "daemon"], {
|
|
188
|
-
stdout: "ignore",
|
|
189
|
-
stderr: "ignore",
|
|
190
|
-
stdin: "ignore",
|
|
191
|
-
detached: true,
|
|
192
|
-
env,
|
|
193
|
-
});
|
|
194
|
-
startProc.unref?.();
|
|
195
|
-
|
|
196
|
-
// Poll until cocod responds to ping (max ~10 s).
|
|
197
|
-
let started = false;
|
|
198
|
-
for (let i = 0; i < 100; i++) {
|
|
199
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
200
|
-
const poll = Bun.spawn([cocodExecutable, "ping"], {
|
|
201
|
-
stdout: "ignore",
|
|
202
|
-
stderr: "ignore",
|
|
203
|
-
});
|
|
204
|
-
if ((await poll.exited) === 0) {
|
|
205
|
-
started = true;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!started) {
|
|
211
|
-
throw new Error("cocod did not come back up within 10 seconds");
|
|
212
|
-
}
|
|
213
|
-
console.log("cocod daemon restarted.");
|
|
214
|
-
}
|
|
215
|
-
} catch (error) {
|
|
216
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
217
|
-
failures.push(`cocod daemon: ${msg}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
185
|
// --- report ---
|
|
221
186
|
if (failures.length > 0) {
|
|
222
|
-
console.error("\n⚠
|
|
187
|
+
console.error("\n⚠ Failed to restart daemon:");
|
|
223
188
|
for (const f of failures) {
|
|
224
189
|
console.error(` - ${f}`);
|
|
225
190
|
}
|
|
226
191
|
console.error(
|
|
227
|
-
"The update was applied but may not take effect until
|
|
192
|
+
"The update was applied but may not take effect until the daemon is manually restarted.",
|
|
228
193
|
);
|
|
229
194
|
process.exit(1);
|
|
230
195
|
}
|
|
231
196
|
|
|
232
|
-
console.log("\n✓
|
|
197
|
+
console.log("\n✓ Daemon restarted successfully.");
|
|
233
198
|
}
|
|
234
199
|
|
|
235
200
|
async function requireLocalDaemon(): Promise<void> {
|
|
@@ -274,76 +239,39 @@ async function initDaemon(): Promise<void> {
|
|
|
274
239
|
console.log(`You can view it in the config file at: ${CONFIG_FILE}\n`);
|
|
275
240
|
}
|
|
276
241
|
|
|
277
|
-
if (!(await isCocodInstalled(config.cocodPath))) {
|
|
278
|
-
if (config.cocodPath) {
|
|
279
|
-
logger.error(
|
|
280
|
-
`Configured cocod executable was not found: ${config.cocodPath}`,
|
|
281
|
-
);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
await installCocodOrExit();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
289
|
-
|
|
290
242
|
console.log(`Database will be stored at: ${DB_PATH}`);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
243
|
+
await stopLegacyCocod();
|
|
244
|
+
|
|
245
|
+
let migrationLockOwner: number | undefined;
|
|
246
|
+
const migration = await migrateLegacyWallet({
|
|
247
|
+
assertLegacyStopped: () =>
|
|
248
|
+
assertLegacyCocodNotRunning({
|
|
249
|
+
socketPath: legacyCocodSocketPath(),
|
|
250
|
+
pidFilePath: legacyCocodPidPath(),
|
|
251
|
+
ignorePid: migrationLockOwner,
|
|
252
|
+
}),
|
|
253
|
+
acquireLegacyLock: () => {
|
|
254
|
+
mkdirSync(dirname(legacyCocodPidPath()), {
|
|
255
|
+
recursive: true,
|
|
256
|
+
mode: 0o700,
|
|
257
|
+
});
|
|
258
|
+
const release = claimLegacyCocodPidFile({
|
|
259
|
+
pidFilePath: legacyCocodPidPath(),
|
|
260
|
+
});
|
|
261
|
+
migrationLockOwner = process.pid;
|
|
262
|
+
return () => {
|
|
263
|
+
migrationLockOwner = undefined;
|
|
264
|
+
release();
|
|
265
|
+
};
|
|
266
|
+
},
|
|
296
267
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const stdoutDone = initProc.stdout
|
|
302
|
-
? initProc.stdout.pipeTo(
|
|
303
|
-
new WritableStream<Uint8Array>({
|
|
304
|
-
write(chunk) {
|
|
305
|
-
const text = new TextDecoder().decode(chunk);
|
|
306
|
-
initStdout += text;
|
|
307
|
-
process.stdout.write(text);
|
|
308
|
-
},
|
|
309
|
-
}),
|
|
310
|
-
)
|
|
311
|
-
: Promise.resolve();
|
|
312
|
-
|
|
313
|
-
const stderrDone = initProc.stderr
|
|
314
|
-
? initProc.stderr.pipeTo(
|
|
315
|
-
new WritableStream<Uint8Array>({
|
|
316
|
-
write(chunk) {
|
|
317
|
-
const text = new TextDecoder().decode(chunk);
|
|
318
|
-
initStderr += text;
|
|
319
|
-
process.stderr.write(text);
|
|
320
|
-
},
|
|
321
|
-
}),
|
|
322
|
-
)
|
|
323
|
-
: Promise.resolve();
|
|
324
|
-
|
|
325
|
-
const [initCode] = await Promise.all([
|
|
326
|
-
initProc.exited,
|
|
327
|
-
stdoutDone,
|
|
328
|
-
stderrDone,
|
|
329
|
-
]);
|
|
330
|
-
const combinedOutput = `${initStdout}\n${initStderr}`.toLowerCase();
|
|
331
|
-
const alreadyInitialized = combinedOutput.includes("already initialized");
|
|
332
|
-
|
|
333
|
-
if (initCode !== 0 && !alreadyInitialized) {
|
|
334
|
-
console.error(
|
|
335
|
-
"Failed to initialize cocod. Please run 'cocod init' manually.",
|
|
336
|
-
);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (alreadyInitialized) {
|
|
341
|
-
console.log("cocod is already initialized.");
|
|
342
|
-
} else {
|
|
343
|
-
console.log("cocod initialized successfully.");
|
|
268
|
+
if (migration.status === "migrated") {
|
|
269
|
+
console.log(`Migrated wallet from ${migration.from} to ${migration.to}.`);
|
|
270
|
+
for (const warning of migration.cleanupWarnings) console.warn(warning);
|
|
344
271
|
}
|
|
272
|
+
initializeWallet();
|
|
345
273
|
|
|
346
|
-
await startDaemon({ port: String(config.port || 8008) });
|
|
274
|
+
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
|
347
275
|
|
|
348
276
|
await setupIntegration(config);
|
|
349
277
|
|
|
@@ -366,7 +294,7 @@ program
|
|
|
366
294
|
|
|
367
295
|
program
|
|
368
296
|
.command("update")
|
|
369
|
-
.description("Update routstrd
|
|
297
|
+
.description("Update routstrd to the latest version")
|
|
370
298
|
.action(async () => {
|
|
371
299
|
const packages = [
|
|
372
300
|
{ name: "routstrd", label: "routstrd" },
|
|
@@ -523,12 +451,36 @@ program
|
|
|
523
451
|
}
|
|
524
452
|
});
|
|
525
453
|
|
|
526
|
-
// Remote - configure a remote daemon URL
|
|
454
|
+
// Remote - show or configure a remote daemon URL
|
|
527
455
|
program
|
|
528
|
-
.command("remote
|
|
529
|
-
.description(
|
|
456
|
+
.command("remote [url]")
|
|
457
|
+
.description(
|
|
458
|
+
"Show the configured remote daemon, or configure one. With no URL, prints the current remote node; pass a URL to set one up.",
|
|
459
|
+
)
|
|
530
460
|
.option("--auth-url <authUrl>", "URL of the auth proxy for management commands (npubs, clients, usage)")
|
|
531
|
-
.action(async (url: string, options: { authUrl?: string }) => {
|
|
461
|
+
.action(async (url: string | undefined, options: { authUrl?: string }) => {
|
|
462
|
+
const config = await loadConfig();
|
|
463
|
+
|
|
464
|
+
// No URL provided -> show the current remote node, or tell the user to set one up.
|
|
465
|
+
if (!url) {
|
|
466
|
+
if (config.daemonUrl) {
|
|
467
|
+
console.log(`Remote daemon URL: ${config.daemonUrl}`);
|
|
468
|
+
if (config.authUrl) {
|
|
469
|
+
console.log(`Auth proxy URL: ${config.authUrl}`);
|
|
470
|
+
}
|
|
471
|
+
const npub = getUserNpub(config);
|
|
472
|
+
if (npub) {
|
|
473
|
+
console.log(`Nostr identity: ${npub}`);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
console.error("No remote node is set up.");
|
|
478
|
+
console.error(
|
|
479
|
+
"Pass a URL to set one up, e.g. 'routstrd remote https://<host>:<port>'.",
|
|
480
|
+
);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
532
484
|
try {
|
|
533
485
|
new URL(url);
|
|
534
486
|
} catch {
|
|
@@ -549,7 +501,6 @@ program
|
|
|
549
501
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
550
502
|
}
|
|
551
503
|
|
|
552
|
-
const config = await loadConfig();
|
|
553
504
|
const updates: Partial<RoutstrdConfig> = { daemonUrl: url };
|
|
554
505
|
if (options.authUrl) {
|
|
555
506
|
updates.authUrl = options.authUrl;
|
|
@@ -586,11 +537,44 @@ program
|
|
|
586
537
|
}
|
|
587
538
|
});
|
|
588
539
|
|
|
540
|
+
// Local - switch back to local daemon mode (removes daemonUrl/authUrl)
|
|
541
|
+
program
|
|
542
|
+
.command("local")
|
|
543
|
+
.description("Switch back to local daemon mode (removes remote daemon URL)")
|
|
544
|
+
.action(async () => {
|
|
545
|
+
const config = await loadConfig();
|
|
546
|
+
|
|
547
|
+
if (!config.daemonUrl) {
|
|
548
|
+
console.log("Already in local mode — no remote daemon URL is configured.");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const previousDaemonUrl = config.daemonUrl;
|
|
553
|
+
const previousAuthUrl = config.authUrl;
|
|
554
|
+
|
|
555
|
+
const { daemonUrl: _daemonUrl, authUrl: _authUrl, ...rest } = config;
|
|
556
|
+
const updatedConfig: RoutstrdConfig = rest;
|
|
557
|
+
|
|
558
|
+
await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
|
|
559
|
+
|
|
560
|
+
console.log("Switched back to local daemon mode.");
|
|
561
|
+
if (previousDaemonUrl) {
|
|
562
|
+
console.log(` Removed remote daemon URL: ${previousDaemonUrl}`);
|
|
563
|
+
}
|
|
564
|
+
if (previousAuthUrl) {
|
|
565
|
+
console.log(` Removed auth proxy URL: ${previousAuthUrl}`);
|
|
566
|
+
}
|
|
567
|
+
console.log(
|
|
568
|
+
`\nUse 'routstrd onboard' for first time using local mode or 'routstrd start' to start the local daemon if you already are using it.`,
|
|
569
|
+
);
|
|
570
|
+
console.log(`You can view the config file at: ${CONFIG_FILE}`);
|
|
571
|
+
});
|
|
572
|
+
|
|
589
573
|
// Onboard - initialize the daemon
|
|
590
574
|
program
|
|
591
575
|
.command("onboard")
|
|
592
576
|
.description(
|
|
593
|
-
"Initialize routstrd (creates config directory and initializes
|
|
577
|
+
"Initialize routstrd (creates config directory and initializes wallet)",
|
|
594
578
|
)
|
|
595
579
|
.action(async () => {
|
|
596
580
|
await requireLocalDaemon();
|
|
@@ -602,19 +586,15 @@ program
|
|
|
602
586
|
.command("start")
|
|
603
587
|
.description("Start the background daemon")
|
|
604
588
|
.option("--port <port>", "Port to listen on")
|
|
589
|
+
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
|
605
590
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
606
|
-
.action(async (options: { port?: string; provider?: string }) => {
|
|
591
|
+
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
|
607
592
|
await requireLocalDaemon();
|
|
608
593
|
const config = await loadConfig();
|
|
609
|
-
|
|
610
|
-
const installHint = config.cocodPath
|
|
611
|
-
? `Configured cocod executable was not found: ${config.cocodPath}`
|
|
612
|
-
: "cocod is not installed. Run 'routstrd onboard' first to install cocod.";
|
|
613
|
-
logger.error(installHint);
|
|
614
|
-
process.exit(1);
|
|
615
|
-
}
|
|
594
|
+
await stopLegacyCocod();
|
|
616
595
|
await startDaemon({
|
|
617
596
|
port: options.port || String(config.port || 8008),
|
|
597
|
+
host: options.host || config.host || undefined,
|
|
618
598
|
provider: options.provider,
|
|
619
599
|
});
|
|
620
600
|
});
|
|
@@ -654,9 +634,93 @@ program
|
|
|
654
634
|
program
|
|
655
635
|
.command("balance")
|
|
656
636
|
.description("Get wallet and API key balances")
|
|
657
|
-
.
|
|
637
|
+
.option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false)
|
|
638
|
+
.option(
|
|
639
|
+
"--delete-api-keys <baseUrl>",
|
|
640
|
+
"Delete the API key stored for the given provider base URL (refunds balance first)",
|
|
641
|
+
)
|
|
642
|
+
.option(
|
|
643
|
+
"--mint-url <url>",
|
|
644
|
+
"Mint URL to refund the deleted API key balance to (defaults to first mint in wallet)",
|
|
645
|
+
)
|
|
646
|
+
.action(async (options: { apiKeys: boolean; deleteApiKeys?: string; mintUrl?: string }) => {
|
|
658
647
|
await ensureDaemonRunning();
|
|
659
648
|
|
|
649
|
+
if (options.deleteApiKeys) {
|
|
650
|
+
const baseUrl = options.deleteApiKeys;
|
|
651
|
+
const queryParts = [`baseUrl=${encodeURIComponent(baseUrl)}`];
|
|
652
|
+
if (options.mintUrl) {
|
|
653
|
+
queryParts.push(`mintUrl=${encodeURIComponent(options.mintUrl)}`);
|
|
654
|
+
}
|
|
655
|
+
const result = await callDaemon(
|
|
656
|
+
`/keys/api/delete?${queryParts.join("&")}`,
|
|
657
|
+
{ method: "DELETE" },
|
|
658
|
+
);
|
|
659
|
+
if (result.error) {
|
|
660
|
+
console.log(result.error);
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
const out = result.output as
|
|
664
|
+
| {
|
|
665
|
+
baseUrl?: string;
|
|
666
|
+
removed?: boolean;
|
|
667
|
+
refunded?: boolean;
|
|
668
|
+
refundedAmount?: number;
|
|
669
|
+
refundMessage?: string;
|
|
670
|
+
message?: string;
|
|
671
|
+
}
|
|
672
|
+
| undefined;
|
|
673
|
+
if (out) {
|
|
674
|
+
console.log(out.message ?? `Removed API key for ${baseUrl}`);
|
|
675
|
+
if (out.refunded && out.refundedAmount !== undefined) {
|
|
676
|
+
console.log(` Refunded: ${out.refundedAmount} sats`);
|
|
677
|
+
} else if (out.refundMessage) {
|
|
678
|
+
console.log(` Refund: ${out.refundMessage}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (options.apiKeys) {
|
|
685
|
+
const result = await callDaemon("/keys/api");
|
|
686
|
+
if (result.error) {
|
|
687
|
+
console.log(result.error);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const data = result.output as
|
|
692
|
+
| {
|
|
693
|
+
apiKeys: Array<{
|
|
694
|
+
baseUrl: string;
|
|
695
|
+
key: string;
|
|
696
|
+
balance: number;
|
|
697
|
+
lastUsed: number | null;
|
|
698
|
+
}>;
|
|
699
|
+
count: number;
|
|
700
|
+
total: number;
|
|
701
|
+
unit: string;
|
|
702
|
+
}
|
|
703
|
+
| undefined;
|
|
704
|
+
|
|
705
|
+
console.log("=== API Keys ===\n");
|
|
706
|
+
if (!data || data.apiKeys.length === 0) {
|
|
707
|
+
console.log(" No API keys stored.");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
for (const k of data.apiKeys) {
|
|
711
|
+
const lastUsed = k.lastUsed
|
|
712
|
+
? new Date(k.lastUsed).toISOString()
|
|
713
|
+
: "never";
|
|
714
|
+
console.log(` ${k.baseUrl}`);
|
|
715
|
+
console.log(` key: ${k.key}`);
|
|
716
|
+
console.log(` balance: ${k.balance} ${data.unit}`);
|
|
717
|
+
console.log(` lastUsed: ${lastUsed}`);
|
|
718
|
+
console.log("");
|
|
719
|
+
}
|
|
720
|
+
console.log(` Total: ${data.apiKeys.length} key(s), ${data.total} ${data.unit}`);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
660
724
|
const [walletResult, keysResult] = await Promise.all([
|
|
661
725
|
callDaemon("/balance"),
|
|
662
726
|
callDaemon("/keys/balance"),
|
|
@@ -780,6 +844,7 @@ program
|
|
|
780
844
|
context_length?: number;
|
|
781
845
|
providers: Array<{
|
|
782
846
|
baseUrl: string;
|
|
847
|
+
disabled?: boolean;
|
|
783
848
|
pricing: {
|
|
784
849
|
prompt: number;
|
|
785
850
|
completion: number;
|
|
@@ -806,7 +871,9 @@ program
|
|
|
806
871
|
}
|
|
807
872
|
console.log(`\n Providers (${modelData.providers.length}):`);
|
|
808
873
|
for (const provider of modelData.providers) {
|
|
809
|
-
console.log(
|
|
874
|
+
console.log(
|
|
875
|
+
`\n ${provider.baseUrl}${provider.disabled ? " [Disabled]" : ""}`,
|
|
876
|
+
);
|
|
810
877
|
console.log(
|
|
811
878
|
` Prompt: ${(provider.pricing.prompt * 1000000).toFixed(2)} sats/M tokens`,
|
|
812
879
|
);
|
|
@@ -1270,6 +1337,103 @@ npubsCmd
|
|
|
1270
1337
|
);
|
|
1271
1338
|
});
|
|
1272
1339
|
|
|
1340
|
+
program
|
|
1341
|
+
.command("history")
|
|
1342
|
+
.description("Show wallet transaction history")
|
|
1343
|
+
.option("-n, --limit <number>", "Number of entries to show", "50")
|
|
1344
|
+
.option("--offset <number>", "Number of entries to skip", "0")
|
|
1345
|
+
.option("-v, --verbose", "Show full details including encoded Cashu tokens")
|
|
1346
|
+
.option("--json", "Output raw JSON with token objects (no encoding)")
|
|
1347
|
+
.action(async (options: { limit: string; offset: string; verbose: boolean; json: boolean }) => {
|
|
1348
|
+
await ensureDaemonRunning();
|
|
1349
|
+
|
|
1350
|
+
const limit = Math.min(parseInt(options.limit, 10) || 50, 1000);
|
|
1351
|
+
const offset = parseInt(options.offset, 10) || 0;
|
|
1352
|
+
|
|
1353
|
+
const result = await callDaemon(
|
|
1354
|
+
`/wallet/history?offset=${offset}&limit=${limit}`,
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
if (result.error) {
|
|
1358
|
+
console.log(result.error);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const data = result.output as
|
|
1363
|
+
| { entries?: Record<string, unknown>[]; offset?: number; limit?: number }
|
|
1364
|
+
| undefined;
|
|
1365
|
+
const entries = data?.entries || [];
|
|
1366
|
+
|
|
1367
|
+
if (entries.length === 0) {
|
|
1368
|
+
console.log("No transaction history yet.");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (options.json) {
|
|
1373
|
+
const jsonOutput = entries.map((e: Record<string, unknown>) => {
|
|
1374
|
+
const entry = { ...e };
|
|
1375
|
+
delete entry.encodedToken;
|
|
1376
|
+
return entry;
|
|
1377
|
+
});
|
|
1378
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (options.verbose) {
|
|
1383
|
+
for (const entry of entries) {
|
|
1384
|
+
const e = entry as Record<string, unknown>;
|
|
1385
|
+
const display: Record<string, unknown> = {};
|
|
1386
|
+
for (const key of Object.keys(e).sort()) {
|
|
1387
|
+
display[key] = e[key];
|
|
1388
|
+
}
|
|
1389
|
+
if (e.encodedToken && e.token) {
|
|
1390
|
+
display.token = e.encodedToken;
|
|
1391
|
+
delete display.encodedToken;
|
|
1392
|
+
}
|
|
1393
|
+
console.log(JSON.stringify(display, null, 2));
|
|
1394
|
+
}
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const idCol = "ID";
|
|
1399
|
+
const timeCol = "Date/Time";
|
|
1400
|
+
const typeCol = "Type";
|
|
1401
|
+
const mintCol = "Mint";
|
|
1402
|
+
const amtCol = "Amount";
|
|
1403
|
+
|
|
1404
|
+
const rows = entries.map((entry: Record<string, unknown>) => {
|
|
1405
|
+
const id = String(entry.id ?? "");
|
|
1406
|
+
const time = new Date(Number(entry.createdAt)).toISOString().replace("T", " ").slice(0, 19);
|
|
1407
|
+
const type = String(entry.type ?? "").toUpperCase();
|
|
1408
|
+
const mint = String(entry.mintUrl ?? "");
|
|
1409
|
+
const unit = String(entry.unit ?? "sat");
|
|
1410
|
+
const amount = `${entry.amount} ${unit}`;
|
|
1411
|
+
return { id, time, type, mint, amount };
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
const widths = {
|
|
1415
|
+
id: Math.max(idCol.length, ...rows.map((r) => r.id.length)),
|
|
1416
|
+
time: Math.max(timeCol.length, ...rows.map((r) => r.time.length)),
|
|
1417
|
+
type: Math.max(typeCol.length, ...rows.map((r) => r.type.length)),
|
|
1418
|
+
mint: Math.max(mintCol.length, ...rows.map((r) => r.mint.length)),
|
|
1419
|
+
amount: Math.max(amtCol.length, ...rows.map((r) => r.amount.length)),
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
const pad = (s: string, w: number) => s.padEnd(w);
|
|
1423
|
+
const sep = Object.values(widths).map((w) => "-".repeat(w)).join(" | ");
|
|
1424
|
+
|
|
1425
|
+
console.log(
|
|
1426
|
+
`${pad(idCol, widths.id)} | ${pad(timeCol, widths.time)} | ${pad(typeCol, widths.type)} | ${pad(mintCol, widths.mint)} | ${pad(amtCol, widths.amount)}`,
|
|
1427
|
+
);
|
|
1428
|
+
console.log(sep);
|
|
1429
|
+
|
|
1430
|
+
for (const row of rows) {
|
|
1431
|
+
console.log(
|
|
1432
|
+
`${pad(row.id, widths.id)} | ${pad(row.time, widths.time)} | ${pad(row.type, widths.type)} | ${pad(row.mint, widths.mint)} | ${pad(row.amount, widths.amount)}`,
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1273
1437
|
// Monitor - interactive TUI
|
|
1274
1438
|
program
|
|
1275
1439
|
.command("monitor")
|
|
@@ -1495,6 +1659,47 @@ walletMintsCmd
|
|
|
1495
1659
|
});
|
|
1496
1660
|
});
|
|
1497
1661
|
|
|
1662
|
+
walletMintsCmd
|
|
1663
|
+
.command("set-default <url>")
|
|
1664
|
+
.description("Set the default mint for wallet operations")
|
|
1665
|
+
.action(async (url: string) => {
|
|
1666
|
+
await handleDaemonCommand("/wallet/mints/default", {
|
|
1667
|
+
method: "POST",
|
|
1668
|
+
body: { url },
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
const walletNpcCmd = walletCmd
|
|
1673
|
+
.command("npc")
|
|
1674
|
+
.description("NPC (npubx.cash) Lightning address operations");
|
|
1675
|
+
|
|
1676
|
+
walletNpcCmd
|
|
1677
|
+
.command("address")
|
|
1678
|
+
.description("Show this wallet's NPC Lightning address")
|
|
1679
|
+
.action(async () => {
|
|
1680
|
+
await handleDaemonCommand("/wallet/npc/address");
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
walletNpcCmd
|
|
1684
|
+
.command("username <name>")
|
|
1685
|
+
.description(
|
|
1686
|
+
"Claim an NPC username (pays the claim fee from the wallet with --confirm)",
|
|
1687
|
+
)
|
|
1688
|
+
.option("--confirm", "Confirm payment of the username claim fee")
|
|
1689
|
+
.action(async (name: string, options: { confirm?: boolean }) => {
|
|
1690
|
+
await handleDaemonCommand("/wallet/npc/username", {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
body: { username: name, confirm: options.confirm === true },
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
walletNpcCmd
|
|
1697
|
+
.command("sync")
|
|
1698
|
+
.description("Manually sync paid NPC quotes into the wallet")
|
|
1699
|
+
.action(async () => {
|
|
1700
|
+
await handleDaemonCommand("/wallet/npc/sync", { method: "POST" });
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1498
1703
|
// ── NWC (Nostr Wallet Connect) commands ─────────────────────────
|
|
1499
1704
|
|
|
1500
1705
|
const nwcCmd = program
|
|
@@ -1661,6 +1866,8 @@ serviceCmd
|
|
|
1661
1866
|
|
|
1662
1867
|
console.log("Starting routstrd via PM2...");
|
|
1663
1868
|
try {
|
|
1869
|
+
await stopLegacyCocod();
|
|
1870
|
+
|
|
1664
1871
|
// Use --interpreter bun to ensure it runs with bun
|
|
1665
1872
|
execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, {
|
|
1666
1873
|
stdio: "inherit",
|
|
@@ -1708,8 +1915,9 @@ program
|
|
|
1708
1915
|
.command("restart")
|
|
1709
1916
|
.description("Restart the background daemon")
|
|
1710
1917
|
.option("--port <port>", "Port to listen on")
|
|
1918
|
+
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
|
1711
1919
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
1712
|
-
.action(async (options: { port?: string; provider?: string }) => {
|
|
1920
|
+
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
|
1713
1921
|
await requireLocalDaemon();
|
|
1714
1922
|
const config = await loadConfig();
|
|
1715
1923
|
const wasRunning = await isDaemonRunning();
|
|
@@ -1718,16 +1926,19 @@ program
|
|
|
1718
1926
|
console.log("Stopping daemon...");
|
|
1719
1927
|
await callDaemon("/stop", { method: "POST" });
|
|
1720
1928
|
|
|
1721
|
-
// Wait for
|
|
1722
|
-
|
|
1929
|
+
// Wait for HTTP health check to fail AND wallet lock to be released.
|
|
1930
|
+
const pidFilePath = walletPidPath();
|
|
1931
|
+
for (let i = 0; i < 100; i++) {
|
|
1723
1932
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1724
|
-
|
|
1933
|
+
const healthDown = !(await isDaemonRunning());
|
|
1934
|
+
const pidFileReleased = !existsSync(pidFilePath);
|
|
1935
|
+
if (healthDown && pidFileReleased) {
|
|
1725
1936
|
break;
|
|
1726
1937
|
}
|
|
1727
1938
|
}
|
|
1728
1939
|
|
|
1729
1940
|
if (await isDaemonRunning()) {
|
|
1730
|
-
logger.error("Daemon failed to stop within
|
|
1941
|
+
logger.error("Daemon failed to stop within 10 seconds");
|
|
1731
1942
|
process.exit(1);
|
|
1732
1943
|
}
|
|
1733
1944
|
console.log("Daemon stopped.");
|
|
@@ -1735,9 +1946,12 @@ program
|
|
|
1735
1946
|
console.log("Daemon was not running.");
|
|
1736
1947
|
}
|
|
1737
1948
|
|
|
1949
|
+
await stopLegacyCocod();
|
|
1950
|
+
|
|
1738
1951
|
console.log("Starting daemon...");
|
|
1739
1952
|
await startDaemon({
|
|
1740
1953
|
port: options.port || String(config.port || 8008),
|
|
1954
|
+
host: options.host || config.host || undefined,
|
|
1741
1955
|
provider: options.provider,
|
|
1742
1956
|
});
|
|
1743
1957
|
console.log("Daemon restarted.");
|
|
@@ -1820,34 +2034,71 @@ program
|
|
|
1820
2034
|
console.log("Starting daemon...");
|
|
1821
2035
|
await startDaemon({
|
|
1822
2036
|
port: String(config.port || 8008),
|
|
2037
|
+
host: config.host || undefined,
|
|
1823
2038
|
provider: config.provider || undefined,
|
|
1824
2039
|
});
|
|
1825
2040
|
console.log(`Daemon restarted with mode '${selectedMode}'.`);
|
|
1826
2041
|
});
|
|
1827
2042
|
|
|
1828
2043
|
// Logs
|
|
1829
|
-
function getLogFileForDate(date: Date = new Date()): string {
|
|
2044
|
+
function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
|
|
1830
2045
|
const year = date.getFullYear();
|
|
1831
2046
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1832
2047
|
const day = String(date.getDate()).padStart(2, "0");
|
|
1833
|
-
return `${
|
|
2048
|
+
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function readLastLines(file: string, lines: number): string {
|
|
2052
|
+
const content = readFileSync(file, "utf8");
|
|
2053
|
+
const allLines = content.replace(/\r\n/g, "\n").split("\n");
|
|
2054
|
+
if (allLines.at(-1) === "") allLines.pop();
|
|
2055
|
+
return allLines.slice(-lines).join("\n");
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async function followLogFile(file: string, lines: number): Promise<void> {
|
|
2059
|
+
const initial = readLastLines(file, lines);
|
|
2060
|
+
if (initial) {
|
|
2061
|
+
console.log(initial);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
let position = statSync(file).size;
|
|
2065
|
+
while (true) {
|
|
2066
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2067
|
+
if (!existsSync(file)) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const size = statSync(file).size;
|
|
2072
|
+
if (size < position) {
|
|
2073
|
+
position = 0;
|
|
2074
|
+
}
|
|
2075
|
+
if (size === position) {
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const text = await Bun.file(file).slice(position, size).text();
|
|
2080
|
+
process.stdout.write(text);
|
|
2081
|
+
position = size;
|
|
2082
|
+
}
|
|
1834
2083
|
}
|
|
1835
2084
|
|
|
1836
2085
|
program
|
|
1837
2086
|
.command("logs")
|
|
1838
2087
|
.description("View daemon logs")
|
|
1839
2088
|
.option("-f, --follow", "Follow log output", false)
|
|
2089
|
+
.option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false)
|
|
1840
2090
|
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
1841
|
-
.action(async (options: { follow: boolean; lines: string }) => {
|
|
2091
|
+
.action(async (options: { follow: boolean; lines: string; coco: boolean }) => {
|
|
1842
2092
|
await requireLocalDaemon();
|
|
1843
|
-
const
|
|
2093
|
+
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
|
|
2094
|
+
const todayFile = getLogFileForDate(logsDir);
|
|
1844
2095
|
const yesterday = new Date();
|
|
1845
2096
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
1846
|
-
const yesterdayFile = getLogFileForDate(yesterday);
|
|
2097
|
+
const yesterdayFile = getLogFileForDate(logsDir, yesterday);
|
|
1847
2098
|
|
|
1848
2099
|
if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
|
|
1849
2100
|
console.log("No log files found. Daemon may not have started yet.");
|
|
1850
|
-
console.log(`Logs directory: ${
|
|
2101
|
+
console.log(`Logs directory: ${logsDir}`);
|
|
1851
2102
|
process.exit(1);
|
|
1852
2103
|
}
|
|
1853
2104
|
|
|
@@ -1858,24 +2109,23 @@ program
|
|
|
1858
2109
|
});
|
|
1859
2110
|
|
|
1860
2111
|
if (options.follow) {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
const exitCode = await proc.exited;
|
|
1868
|
-
process.exit(exitCode);
|
|
2112
|
+
if (existsSync(todayFile)) {
|
|
2113
|
+
await followLogFile(todayFile, lines);
|
|
2114
|
+
} else {
|
|
2115
|
+
console.log("No log file for today to follow.");
|
|
2116
|
+
}
|
|
2117
|
+
return;
|
|
1869
2118
|
}
|
|
1870
2119
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2120
|
+
for (const file of logFiles) {
|
|
2121
|
+
if (logFiles.length > 1) {
|
|
2122
|
+
console.log(`==> ${file} <==`);
|
|
2123
|
+
}
|
|
2124
|
+
const output = readLastLines(file, lines);
|
|
2125
|
+
if (output) {
|
|
2126
|
+
console.log(output);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
1879
2129
|
});
|
|
1880
2130
|
|
|
1881
2131
|
export function cli(args: string[]) {
|