routstrd 0.4.4 → 0.4.6
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/README.md +14 -0
- package/dist/daemon/index.js +74735 -91633
- package/dist/index.js +203 -41
- package/package.json +9 -2
- package/src/cli.test.ts +138 -1
- package/src/cli.ts +289 -49
- package/src/daemon/http/index.ts +1862 -0
- package/src/daemon/index.ts +408 -0
- package/src/daemon/wallet/index.ts +360 -0
- package/src/index.ts +4 -0
- package/src/integrations/index.ts +159 -0
- package/src/tui/usage/index.ts +1 -0
- package/.claude/settings.local.json +0 -10
- package/.dockerignore +0 -12
- package/COCO-2.0.0-MIGRATION-PLAN.md +0 -1191
- package/Dockerfile +0 -11
- package/IMPLEMENTATION.md +0 -253
- package/SKILL.md +0 -324
- package/bun.lock +0 -494
- package/docker-compose.yml +0 -15
- package/refund.js +0 -33
- package/refund_new.js +0 -20
- package/routstr-cost-logging.md +0 -71
- package/test_box.ts +0 -15
- package/test_chat.sh +0 -29
- package/test_curl.sh +0 -11
- package/test_split_box.ts +0 -17
- package/test_split_box2.ts +0 -23
- package/tests/daemon/config-store.perms.test.ts +0 -55
- package/tests/daemon/config-store.scenario.ts +0 -113
- package/tests/integrations/hermes.test.ts +0 -134
- package/tests/utils/daemon-client.test.ts +0 -61
- package/tests/wallet/short-keyset-token.test.ts +0 -106
- package/tsconfig.json +0 -20
- package/v1-messages-format-report.md +0 -223
package/src/cli.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type RoutstrdConfig,
|
|
29
29
|
} from "./utils/config";
|
|
30
30
|
import { COCO_LOGS_DIR, logger } from "./utils/logger";
|
|
31
|
-
import { setupIntegration, runIntegrationsForClients } from "./integrations";
|
|
31
|
+
import { setupIntegration, runIntegrationsForClients, type IntegrationKey } from "./integrations";
|
|
32
32
|
import {
|
|
33
33
|
assertLegacyCocodNotRunning,
|
|
34
34
|
claimLegacyCocodPidFile,
|
|
@@ -215,7 +215,7 @@ async function requireLocalDaemon(): Promise<void> {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
async function initDaemon(): Promise<void> {
|
|
218
|
+
async function initDaemon(integrationKey?: IntegrationKey): Promise<void> {
|
|
219
219
|
console.log("Initializing routstrd...");
|
|
220
220
|
|
|
221
221
|
// Create config directory (0700, correcting existing installs too)
|
|
@@ -273,7 +273,7 @@ async function initDaemon(): Promise<void> {
|
|
|
273
273
|
|
|
274
274
|
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
|
275
275
|
|
|
276
|
-
await setupIntegration(config);
|
|
276
|
+
await setupIntegration(config, integrationKey);
|
|
277
277
|
|
|
278
278
|
console.log("\nInitialization complete!");
|
|
279
279
|
console.log(
|
|
@@ -574,20 +574,68 @@ program
|
|
|
574
574
|
.description(
|
|
575
575
|
"Initialize routstrd (creates config directory and initializes wallet)",
|
|
576
576
|
)
|
|
577
|
-
.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
577
|
+
.option("--opencode", "Set up OpenCode integration (non-interactive)")
|
|
578
|
+
.option("--openclaw", "Set up OpenClaw integration (non-interactive)")
|
|
579
|
+
.option("--pi-agent", "Set up Pi Agent integration (non-interactive)")
|
|
580
|
+
.option("--claude-code", "Set up Claude Code integration (non-interactive)")
|
|
581
|
+
.option("--hermes", "Set up Hermes integration (non-interactive)")
|
|
582
|
+
.option("--skip-integration", "Skip integration setup")
|
|
583
|
+
.action(
|
|
584
|
+
async (options: {
|
|
585
|
+
opencode?: boolean;
|
|
586
|
+
openclaw?: boolean;
|
|
587
|
+
piAgent?: boolean;
|
|
588
|
+
claudeCode?: boolean;
|
|
589
|
+
hermes?: boolean;
|
|
590
|
+
skipIntegration?: boolean;
|
|
591
|
+
}) => {
|
|
592
|
+
await requireLocalDaemon();
|
|
593
|
+
|
|
594
|
+
const integrationFlags: Record<string, boolean | undefined> = {
|
|
595
|
+
opencode: options.opencode,
|
|
596
|
+
openclaw: options.openclaw,
|
|
597
|
+
"pi-agent": options.piAgent,
|
|
598
|
+
"claude-code": options.claudeCode,
|
|
599
|
+
hermes: options.hermes,
|
|
600
|
+
};
|
|
601
|
+
const selectedIntegrations = Object.keys(integrationFlags).filter(
|
|
602
|
+
(key) => integrationFlags[key],
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
if (selectedIntegrations.length > 1) {
|
|
606
|
+
console.error(
|
|
607
|
+
"Error: use only one integration flag, or use 'routstrd clients add' for multiple clients.",
|
|
608
|
+
);
|
|
586
609
|
process.exit(1);
|
|
587
610
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
611
|
+
|
|
612
|
+
if (options.skipIntegration && selectedIntegrations.length > 0) {
|
|
613
|
+
console.error(
|
|
614
|
+
"Error: --skip-integration cannot be combined with an integration flag.",
|
|
615
|
+
);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
let integrationKey: IntegrationKey | undefined;
|
|
620
|
+
if (options.skipIntegration) {
|
|
621
|
+
integrationKey = "skip";
|
|
622
|
+
} else if (selectedIntegrations.length === 1) {
|
|
623
|
+
integrationKey = selectedIntegrations[0];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
await initDaemon(integrationKey);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
// An expected, user-actionable refusal — print the structured message
|
|
630
|
+
// without Bun's unhandled-rejection source snippet and stack trace.
|
|
631
|
+
if (error instanceof WalletMigrationConflictError) {
|
|
632
|
+
console.error(error.message);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
);
|
|
591
639
|
|
|
592
640
|
// Start - start the background daemon
|
|
593
641
|
program
|
|
@@ -2268,11 +2316,15 @@ function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
|
|
|
2268
2316
|
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
2269
2317
|
}
|
|
2270
2318
|
|
|
2271
|
-
function
|
|
2319
|
+
function readAllLines(file: string): string[] {
|
|
2272
2320
|
const content = readFileSync(file, "utf8");
|
|
2273
2321
|
const allLines = content.replace(/\r\n/g, "\n").split("\n");
|
|
2274
2322
|
if (allLines.at(-1) === "") allLines.pop();
|
|
2275
|
-
return allLines
|
|
2323
|
+
return allLines;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function readLastLines(file: string, lines: number): string {
|
|
2327
|
+
return readAllLines(file).slice(-lines).join("\n");
|
|
2276
2328
|
}
|
|
2277
2329
|
|
|
2278
2330
|
async function followLogFile(file: string, lines: number): Promise<void> {
|
|
@@ -2302,51 +2354,239 @@ async function followLogFile(file: string, lines: number): Promise<void> {
|
|
|
2302
2354
|
}
|
|
2303
2355
|
}
|
|
2304
2356
|
|
|
2357
|
+
type StructuredLogLine = {
|
|
2358
|
+
timestamp?: string;
|
|
2359
|
+
level?: string;
|
|
2360
|
+
requestId?: string;
|
|
2361
|
+
modelId?: string;
|
|
2362
|
+
isRouting: boolean;
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
export function parseStructuredLogLine(line: string): StructuredLogLine {
|
|
2366
|
+
const out: StructuredLogLine = { isRouting: false };
|
|
2367
|
+
|
|
2368
|
+
const head = line.match(/^\[([^\]]+)\]\s+\[([^\]]+)\]/);
|
|
2369
|
+
if (head) {
|
|
2370
|
+
out.timestamp = head[1];
|
|
2371
|
+
out.level = head[2];
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// Request ids are hex (no colons). Stop at the first ':' so legacy
|
|
2375
|
+
// colon-joined child tags like `[req:2c30d5ed:RoutstrClient]` still yield
|
|
2376
|
+
// the bare request id.
|
|
2377
|
+
const req = line.match(/\[req:([^\]\:]+)/);
|
|
2378
|
+
if (req) {
|
|
2379
|
+
out.requestId = req[1]!.trim();
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// Prefer an explicit `[model:...]` tag; fall back to the modelId embedded
|
|
2383
|
+
// in the SDK's `generic request pricing input {"modelId":"..."}` line.
|
|
2384
|
+
const model = line.match(/\[model:([^\]\:]+)\]/);
|
|
2385
|
+
if (model) {
|
|
2386
|
+
out.modelId = model[1]!.trim();
|
|
2387
|
+
} else {
|
|
2388
|
+
const jsonModel = line.match(/"modelId"\s*:\s*"([^"]+)"/);
|
|
2389
|
+
if (jsonModel) {
|
|
2390
|
+
out.modelId = jsonModel[1];
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
out.isRouting = !!out.requestId && line.includes("Routing request with path");
|
|
2395
|
+
return out;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
type RequestSummary = {
|
|
2399
|
+
requestId: string;
|
|
2400
|
+
modelId: string;
|
|
2401
|
+
timestamp?: string;
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Scan log lines newest-first and collect one record per request, keyed off
|
|
2406
|
+
* the "Routing request with path" line (the canonical per-request entry).
|
|
2407
|
+
* The model is enriched in a second pass from any line belonging to the
|
|
2408
|
+
* request (the SDK pricing line embeds modelId even in legacy logs).
|
|
2409
|
+
*/
|
|
2410
|
+
export function collectRecentRequestsFromLines(
|
|
2411
|
+
lines: string[],
|
|
2412
|
+
limit: number,
|
|
2413
|
+
): RequestSummary[] {
|
|
2414
|
+
const result: RequestSummary[] = [];
|
|
2415
|
+
const seen = new Set<string>();
|
|
2416
|
+
|
|
2417
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2418
|
+
const parsed = parseStructuredLogLine(lines[i]!);
|
|
2419
|
+
if (!parsed.isRouting || !parsed.requestId) continue;
|
|
2420
|
+
if (seen.has(parsed.requestId)) continue;
|
|
2421
|
+
seen.add(parsed.requestId);
|
|
2422
|
+
|
|
2423
|
+
result.push({
|
|
2424
|
+
requestId: parsed.requestId,
|
|
2425
|
+
modelId: parsed.modelId ?? "unknown",
|
|
2426
|
+
timestamp: parsed.timestamp,
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
if (result.length >= limit) break;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// Second pass: fill in the model for requests whose routing line predates
|
|
2433
|
+
// the `[model:...]` tag but which logged `{"modelId":"..."}` later on.
|
|
2434
|
+
const byRequestId = new Map(result.map((r) => [r.requestId, r]));
|
|
2435
|
+
for (const line of lines) {
|
|
2436
|
+
const parsed = parseStructuredLogLine(line);
|
|
2437
|
+
if (!parsed.requestId || !parsed.modelId) continue;
|
|
2438
|
+
const record = byRequestId.get(parsed.requestId);
|
|
2439
|
+
if (record && record.modelId === "unknown") {
|
|
2440
|
+
record.modelId = parsed.modelId;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
return result;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function collectRecentRequests(
|
|
2448
|
+
files: string[],
|
|
2449
|
+
limit: number,
|
|
2450
|
+
): RequestSummary[] {
|
|
2451
|
+
const result: RequestSummary[] = [];
|
|
2452
|
+
const seen = new Set<string>();
|
|
2453
|
+
|
|
2454
|
+
// Files are passed oldest-first (yesterday, today); scan newest-first.
|
|
2455
|
+
for (const file of [...files].reverse()) {
|
|
2456
|
+
if (!existsSync(file)) continue;
|
|
2457
|
+
const remaining = limit - result.length;
|
|
2458
|
+
if (remaining <= 0) break;
|
|
2459
|
+
|
|
2460
|
+
for (const request of collectRecentRequestsFromLines(
|
|
2461
|
+
readAllLines(file),
|
|
2462
|
+
remaining,
|
|
2463
|
+
)) {
|
|
2464
|
+
if (seen.has(request.requestId)) continue;
|
|
2465
|
+
seen.add(request.requestId);
|
|
2466
|
+
result.push(request);
|
|
2467
|
+
if (result.length >= limit) return result;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
return result;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
function filterLinesByRequestId(
|
|
2475
|
+
files: string[],
|
|
2476
|
+
requestId: string,
|
|
2477
|
+
): string[] {
|
|
2478
|
+
const matches: string[] = [];
|
|
2479
|
+
for (const file of files) {
|
|
2480
|
+
if (!existsSync(file)) continue;
|
|
2481
|
+
for (const line of readAllLines(file)) {
|
|
2482
|
+
if (parseStructuredLogLine(line).requestId === requestId) {
|
|
2483
|
+
matches.push(line);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return matches;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
function parseLinesOption(value: string): number {
|
|
2491
|
+
const parsed = Number.parseInt(value, 10);
|
|
2492
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
|
|
2305
2496
|
program
|
|
2306
2497
|
.command("logs")
|
|
2307
2498
|
.description("View daemon logs")
|
|
2308
2499
|
.option("-f, --follow", "Follow log output", false)
|
|
2309
2500
|
.option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false)
|
|
2310
2501
|
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
2311
|
-
.
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
process.exit(1);
|
|
2323
|
-
}
|
|
2502
|
+
.option("-r, --recent", "List recent request ids with their model", false)
|
|
2503
|
+
.option("-i, --request-id <id>", "Only show log lines for a specific request id")
|
|
2504
|
+
.action(
|
|
2505
|
+
async (options: {
|
|
2506
|
+
follow: boolean;
|
|
2507
|
+
lines: string;
|
|
2508
|
+
coco: boolean;
|
|
2509
|
+
recent: boolean;
|
|
2510
|
+
requestId?: string;
|
|
2511
|
+
}) => {
|
|
2512
|
+
await requireLocalDaemon();
|
|
2324
2513
|
|
|
2325
|
-
|
|
2514
|
+
if (options.recent && options.requestId) {
|
|
2515
|
+
console.error(
|
|
2516
|
+
"Cannot combine --recent (-r) with --request-id (-i).",
|
|
2517
|
+
);
|
|
2518
|
+
process.exit(1);
|
|
2519
|
+
}
|
|
2326
2520
|
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2521
|
+
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
|
|
2522
|
+
const todayFile = getLogFileForDate(logsDir);
|
|
2523
|
+
const yesterday = new Date();
|
|
2524
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
2525
|
+
const yesterdayFile = getLogFileForDate(logsDir, yesterday);
|
|
2330
2526
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
console.log("No log file for today to follow.");
|
|
2527
|
+
if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
|
|
2528
|
+
console.log("No log files found. Daemon may not have started yet.");
|
|
2529
|
+
console.log(`Logs directory: ${logsDir}`);
|
|
2530
|
+
process.exit(1);
|
|
2336
2531
|
}
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2339
2532
|
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2533
|
+
const lines = parseLinesOption(options.lines);
|
|
2534
|
+
|
|
2535
|
+
const logFiles = [yesterdayFile, todayFile].filter(
|
|
2536
|
+
(file, index, files) =>
|
|
2537
|
+
existsSync(file) && files.indexOf(file) === index,
|
|
2538
|
+
);
|
|
2539
|
+
|
|
2540
|
+
if (options.recent) {
|
|
2541
|
+
const requests = collectRecentRequests(logFiles, lines);
|
|
2542
|
+
if (requests.length === 0) {
|
|
2543
|
+
console.log("No requests found in recent logs.");
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
console.log("Recent requests (newest first):");
|
|
2548
|
+
for (const request of requests) {
|
|
2549
|
+
const when = request.timestamp ? ` ${request.timestamp}` : "";
|
|
2550
|
+
console.log(` ${request.requestId} ${request.modelId}${when}`);
|
|
2551
|
+
}
|
|
2552
|
+
return;
|
|
2343
2553
|
}
|
|
2344
|
-
|
|
2345
|
-
if (
|
|
2346
|
-
|
|
2554
|
+
|
|
2555
|
+
if (options.requestId) {
|
|
2556
|
+
const matches = filterLinesByRequestId(logFiles, options.requestId);
|
|
2557
|
+
if (matches.length === 0) {
|
|
2558
|
+
console.log(
|
|
2559
|
+
`No log lines found for request '${options.requestId}'.`,
|
|
2560
|
+
);
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
for (const line of matches) {
|
|
2565
|
+
console.log(line);
|
|
2566
|
+
}
|
|
2567
|
+
return;
|
|
2347
2568
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2569
|
+
|
|
2570
|
+
if (options.follow) {
|
|
2571
|
+
if (existsSync(todayFile)) {
|
|
2572
|
+
await followLogFile(todayFile, lines);
|
|
2573
|
+
} else {
|
|
2574
|
+
console.log("No log file for today to follow.");
|
|
2575
|
+
}
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
for (const file of logFiles) {
|
|
2580
|
+
if (logFiles.length > 1) {
|
|
2581
|
+
console.log(`==> ${file} <==`);
|
|
2582
|
+
}
|
|
2583
|
+
const output = readLastLines(file, lines);
|
|
2584
|
+
if (output) {
|
|
2585
|
+
console.log(output);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
},
|
|
2589
|
+
);
|
|
2350
2590
|
|
|
2351
2591
|
export function cli(args: string[]) {
|
|
2352
2592
|
program.parse(args);
|