routstrd 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27170,7 +27170,7 @@ zoo`.split(`
27170
27170
  // package.json
27171
27171
  var package_default = {
27172
27172
  name: "routstrd",
27173
- version: "0.4.4",
27173
+ version: "0.4.5",
27174
27174
  module: "src/index.ts",
27175
27175
  type: "module",
27176
27176
  private: false,
@@ -28588,14 +28588,17 @@ function getLogFileForDate2(logsDir, date = new Date) {
28588
28588
  const day = String(date.getDate()).padStart(2, "0");
28589
28589
  return `${logsDir}/${year}-${month}-${day}.log`;
28590
28590
  }
28591
- function readLastLines(file, lines) {
28591
+ function readAllLines(file) {
28592
28592
  const content = readFileSync5(file, "utf8");
28593
28593
  const allLines = content.replace(/\r\n/g, `
28594
28594
  `).split(`
28595
28595
  `);
28596
28596
  if (allLines.at(-1) === "")
28597
28597
  allLines.pop();
28598
- return allLines.slice(-lines).join(`
28598
+ return allLines;
28599
+ }
28600
+ function readLastLines(file, lines) {
28601
+ return readAllLines(file).slice(-lines).join(`
28599
28602
  `);
28600
28603
  }
28601
28604
  async function followLogFile(file, lines) {
@@ -28621,8 +28624,102 @@ async function followLogFile(file, lines) {
28621
28624
  position = size;
28622
28625
  }
28623
28626
  }
28624
- program.command("logs").description("View daemon logs").option("-f, --follow", "Follow log output", false).option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false).option("-n, --lines <number>", "Number of lines to show", "50").action(async (options) => {
28627
+ function parseStructuredLogLine(line) {
28628
+ const out = { isRouting: false };
28629
+ const head = line.match(/^\[([^\]]+)\]\s+\[([^\]]+)\]/);
28630
+ if (head) {
28631
+ out.timestamp = head[1];
28632
+ out.level = head[2];
28633
+ }
28634
+ const req = line.match(/\[req:([^\]\:]+)/);
28635
+ if (req) {
28636
+ out.requestId = req[1].trim();
28637
+ }
28638
+ const model = line.match(/\[model:([^\]\:]+)\]/);
28639
+ if (model) {
28640
+ out.modelId = model[1].trim();
28641
+ } else {
28642
+ const jsonModel = line.match(/"modelId"\s*:\s*"([^"]+)"/);
28643
+ if (jsonModel) {
28644
+ out.modelId = jsonModel[1];
28645
+ }
28646
+ }
28647
+ out.isRouting = !!out.requestId && line.includes("Routing request with path");
28648
+ return out;
28649
+ }
28650
+ function collectRecentRequestsFromLines(lines, limit) {
28651
+ const result = [];
28652
+ const seen = new Set;
28653
+ for (let i2 = lines.length - 1;i2 >= 0; i2--) {
28654
+ const parsed = parseStructuredLogLine(lines[i2]);
28655
+ if (!parsed.isRouting || !parsed.requestId)
28656
+ continue;
28657
+ if (seen.has(parsed.requestId))
28658
+ continue;
28659
+ seen.add(parsed.requestId);
28660
+ result.push({
28661
+ requestId: parsed.requestId,
28662
+ modelId: parsed.modelId ?? "unknown",
28663
+ timestamp: parsed.timestamp
28664
+ });
28665
+ if (result.length >= limit)
28666
+ break;
28667
+ }
28668
+ const byRequestId = new Map(result.map((r) => [r.requestId, r]));
28669
+ for (const line of lines) {
28670
+ const parsed = parseStructuredLogLine(line);
28671
+ if (!parsed.requestId || !parsed.modelId)
28672
+ continue;
28673
+ const record = byRequestId.get(parsed.requestId);
28674
+ if (record && record.modelId === "unknown") {
28675
+ record.modelId = parsed.modelId;
28676
+ }
28677
+ }
28678
+ return result;
28679
+ }
28680
+ function collectRecentRequests(files, limit) {
28681
+ const result = [];
28682
+ const seen = new Set;
28683
+ for (const file of [...files].reverse()) {
28684
+ if (!existsSync14(file))
28685
+ continue;
28686
+ const remaining = limit - result.length;
28687
+ if (remaining <= 0)
28688
+ break;
28689
+ for (const request of collectRecentRequestsFromLines(readAllLines(file), remaining)) {
28690
+ if (seen.has(request.requestId))
28691
+ continue;
28692
+ seen.add(request.requestId);
28693
+ result.push(request);
28694
+ if (result.length >= limit)
28695
+ return result;
28696
+ }
28697
+ }
28698
+ return result;
28699
+ }
28700
+ function filterLinesByRequestId(files, requestId) {
28701
+ const matches = [];
28702
+ for (const file of files) {
28703
+ if (!existsSync14(file))
28704
+ continue;
28705
+ for (const line of readAllLines(file)) {
28706
+ if (parseStructuredLogLine(line).requestId === requestId) {
28707
+ matches.push(line);
28708
+ }
28709
+ }
28710
+ }
28711
+ return matches;
28712
+ }
28713
+ function parseLinesOption(value) {
28714
+ const parsed = Number.parseInt(value, 10);
28715
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
28716
+ }
28717
+ program.command("logs").description("View daemon logs").option("-f, --follow", "Follow log output", false).option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false).option("-n, --lines <number>", "Number of lines to show", "50").option("-r, --recent", "List recent request ids with their model", false).option("-i, --request-id <id>", "Only show log lines for a specific request id").action(async (options) => {
28625
28718
  await requireLocalDaemon();
28719
+ if (options.recent && options.requestId) {
28720
+ console.error("Cannot combine --recent (-r) with --request-id (-i).");
28721
+ process.exit(1);
28722
+ }
28626
28723
  const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR2;
28627
28724
  const todayFile = getLogFileForDate2(logsDir);
28628
28725
  const yesterday = new Date;
@@ -28633,10 +28730,32 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
28633
28730
  console.log(`Logs directory: ${logsDir}`);
28634
28731
  process.exit(1);
28635
28732
  }
28636
- const lines = parseInt(options.lines, 10);
28637
- const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
28638
- return existsSync14(file) && files.indexOf(file) === index;
28639
- });
28733
+ const lines = parseLinesOption(options.lines);
28734
+ const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => existsSync14(file) && files.indexOf(file) === index);
28735
+ if (options.recent) {
28736
+ const requests = collectRecentRequests(logFiles, lines);
28737
+ if (requests.length === 0) {
28738
+ console.log("No requests found in recent logs.");
28739
+ return;
28740
+ }
28741
+ console.log("Recent requests (newest first):");
28742
+ for (const request of requests) {
28743
+ const when = request.timestamp ? ` ${request.timestamp}` : "";
28744
+ console.log(` ${request.requestId} ${request.modelId}${when}`);
28745
+ }
28746
+ return;
28747
+ }
28748
+ if (options.requestId) {
28749
+ const matches = filterLinesByRequestId(logFiles, options.requestId);
28750
+ if (matches.length === 0) {
28751
+ console.log(`No log lines found for request '${options.requestId}'.`);
28752
+ return;
28753
+ }
28754
+ for (const line of matches) {
28755
+ console.log(line);
28756
+ }
28757
+ return;
28758
+ }
28640
28759
  if (options.follow) {
28641
28760
  if (existsSync14(todayFile)) {
28642
28761
  await followLogFile(todayFile, lines);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli.test.ts CHANGED
@@ -2,7 +2,11 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
- import { initializeWallet } from "./cli";
5
+ import {
6
+ collectRecentRequestsFromLines,
7
+ initializeWallet,
8
+ parseStructuredLogLine,
9
+ } from "./cli";
6
10
 
7
11
  const tempDirs: string[] = [];
8
12
 
@@ -22,6 +26,139 @@ afterEach(() => {
22
26
  }
23
27
  });
24
28
 
29
+ describe("parseStructuredLogLine", () => {
30
+ test("extracts timestamp, level, request id, model, and routing flag", () => {
31
+ const line =
32
+ "[2026-08-27T13:00:00.000Z] [INFO] [req:a1b2c3d4] [model:gpt-5] Routing request with path: /v1/chat/completions";
33
+
34
+ expect(parseStructuredLogLine(line)).toEqual({
35
+ timestamp: "2026-08-27T13:00:00.000Z",
36
+ level: "INFO",
37
+ requestId: "a1b2c3d4",
38
+ modelId: "gpt-5",
39
+ isRouting: true,
40
+ });
41
+ });
42
+
43
+ test("parses SDK child lines without marking them as routing lines", () => {
44
+ const line =
45
+ "[2026-08-27T13:00:01.000Z] [DEBUG] [req:a1b2c3d4] [model:gpt-5] [BalanceManager] token stuff";
46
+
47
+ expect(parseStructuredLogLine(line)).toEqual({
48
+ timestamp: "2026-08-27T13:00:01.000Z",
49
+ level: "DEBUG",
50
+ requestId: "a1b2c3d4",
51
+ modelId: "gpt-5",
52
+ isRouting: false,
53
+ });
54
+ });
55
+
56
+ test("handles legacy lines that have a request id but no model tag", () => {
57
+ const line =
58
+ "[2026-08-27T13:00:02.000Z] [INFO] [req:deadbeef] Routing request with path: /v1/responses";
59
+
60
+ expect(parseStructuredLogLine(line)).toEqual({
61
+ timestamp: "2026-08-27T13:00:02.000Z",
62
+ level: "INFO",
63
+ requestId: "deadbeef",
64
+ modelId: undefined,
65
+ isRouting: true,
66
+ });
67
+ });
68
+
69
+ test("extracts the bare request id from colon-joined legacy child tags", () => {
70
+ const line =
71
+ "[2026-08-27T13:00:04.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-flash-0731\"}";
72
+
73
+ const parsed = parseStructuredLogLine(line);
74
+ expect(parsed.requestId).toBe("deadbeef");
75
+ expect(parsed.modelId).toBe("deepseek-v4-flash-0731");
76
+ expect(parsed.isRouting).toBe(false);
77
+ });
78
+
79
+ test("falls back to the JSON modelId when no model tag is present", () => {
80
+ const line =
81
+ "[2026-08-27T13:00:05.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-pro-0813\",\"messageCount\":121}";
82
+
83
+ expect(parseStructuredLogLine(line).modelId).toBe("deepseek-v4-pro-0813");
84
+ });
85
+
86
+ test("returns an empty record for plain non-request lines", () => {
87
+ const line = "[2026-08-27T13:00:03.000Z] [WARN] Some startup message";
88
+
89
+ expect(parseStructuredLogLine(line)).toEqual({
90
+ timestamp: "2026-08-27T13:00:03.000Z",
91
+ level: "WARN",
92
+ requestId: undefined,
93
+ modelId: undefined,
94
+ isRouting: false,
95
+ });
96
+ });
97
+ });
98
+
99
+ describe("collectRecentRequestsFromLines", () => {
100
+ const lines = [
101
+ "[2026-08-27T13:00:00.000Z] [INFO] [req:11111111] [model:gpt-5] Routing request with path: /a",
102
+ "[2026-08-27T13:00:01.000Z] [DEBUG] [req:11111111] [model:gpt-5] [BalanceManager] token",
103
+ "[2026-08-27T13:00:02.000Z] [INFO] [req:22222222] [model:claude-sonnet-4.5] Routing request with path: /b",
104
+ "[2026-08-27T13:00:03.000Z] [DEBUG] [req:22222222] [model:claude-sonnet-4.5] [ModelManager] refresh",
105
+ ];
106
+
107
+ test("returns one record per request, newest first", () => {
108
+ expect(collectRecentRequestsFromLines(lines, 10)).toEqual([
109
+ {
110
+ requestId: "22222222",
111
+ modelId: "claude-sonnet-4.5",
112
+ timestamp: "2026-08-27T13:00:02.000Z",
113
+ },
114
+ {
115
+ requestId: "11111111",
116
+ modelId: "gpt-5",
117
+ timestamp: "2026-08-27T13:00:00.000Z",
118
+ },
119
+ ]);
120
+ });
121
+
122
+ test("respects the limit", () => {
123
+ expect(collectRecentRequestsFromLines(lines, 1)).toEqual([
124
+ {
125
+ requestId: "22222222",
126
+ modelId: "claude-sonnet-4.5",
127
+ timestamp: "2026-08-27T13:00:02.000Z",
128
+ },
129
+ ]);
130
+ });
131
+
132
+ test("defaults the model to unknown for legacy routing lines", () => {
133
+ const legacy = [
134
+ "[2026-08-27T13:00:00.000Z] [INFO] [req:deadbeef] Routing request with path: /a",
135
+ ];
136
+
137
+ expect(collectRecentRequestsFromLines(legacy, 10)).toEqual([
138
+ {
139
+ requestId: "deadbeef",
140
+ modelId: "unknown",
141
+ timestamp: "2026-08-27T13:00:00.000Z",
142
+ },
143
+ ]);
144
+ });
145
+
146
+ test("enriches the model from a sibling pricing line in legacy logs", () => {
147
+ const legacy = [
148
+ "[2026-08-27T13:00:00.000Z] [INFO] [req:deadbeef] Routing request with path: /a",
149
+ "[2026-08-27T13:00:01.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-flash-0731\"}",
150
+ ];
151
+
152
+ expect(collectRecentRequestsFromLines(legacy, 10)).toEqual([
153
+ {
154
+ requestId: "deadbeef",
155
+ modelId: "deepseek-v4-flash-0731",
156
+ timestamp: "2026-08-27T13:00:00.000Z",
157
+ },
158
+ ]);
159
+ });
160
+ });
161
+
25
162
  describe("initializeWallet", () => {
26
163
  test("creates the wallet directory and config with restrictive permissions", () => {
27
164
  const walletDir = join(makeTempDir(), "wallet");
package/src/cli.ts CHANGED
@@ -2268,11 +2268,15 @@ function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
2268
2268
  return `${logsDir}/${year}-${month}-${day}.log`;
2269
2269
  }
2270
2270
 
2271
- function readLastLines(file: string, lines: number): string {
2271
+ function readAllLines(file: string): string[] {
2272
2272
  const content = readFileSync(file, "utf8");
2273
2273
  const allLines = content.replace(/\r\n/g, "\n").split("\n");
2274
2274
  if (allLines.at(-1) === "") allLines.pop();
2275
- return allLines.slice(-lines).join("\n");
2275
+ return allLines;
2276
+ }
2277
+
2278
+ function readLastLines(file: string, lines: number): string {
2279
+ return readAllLines(file).slice(-lines).join("\n");
2276
2280
  }
2277
2281
 
2278
2282
  async function followLogFile(file: string, lines: number): Promise<void> {
@@ -2302,51 +2306,239 @@ async function followLogFile(file: string, lines: number): Promise<void> {
2302
2306
  }
2303
2307
  }
2304
2308
 
2309
+ type StructuredLogLine = {
2310
+ timestamp?: string;
2311
+ level?: string;
2312
+ requestId?: string;
2313
+ modelId?: string;
2314
+ isRouting: boolean;
2315
+ };
2316
+
2317
+ export function parseStructuredLogLine(line: string): StructuredLogLine {
2318
+ const out: StructuredLogLine = { isRouting: false };
2319
+
2320
+ const head = line.match(/^\[([^\]]+)\]\s+\[([^\]]+)\]/);
2321
+ if (head) {
2322
+ out.timestamp = head[1];
2323
+ out.level = head[2];
2324
+ }
2325
+
2326
+ // Request ids are hex (no colons). Stop at the first ':' so legacy
2327
+ // colon-joined child tags like `[req:2c30d5ed:RoutstrClient]` still yield
2328
+ // the bare request id.
2329
+ const req = line.match(/\[req:([^\]\:]+)/);
2330
+ if (req) {
2331
+ out.requestId = req[1]!.trim();
2332
+ }
2333
+
2334
+ // Prefer an explicit `[model:...]` tag; fall back to the modelId embedded
2335
+ // in the SDK's `generic request pricing input {"modelId":"..."}` line.
2336
+ const model = line.match(/\[model:([^\]\:]+)\]/);
2337
+ if (model) {
2338
+ out.modelId = model[1]!.trim();
2339
+ } else {
2340
+ const jsonModel = line.match(/"modelId"\s*:\s*"([^"]+)"/);
2341
+ if (jsonModel) {
2342
+ out.modelId = jsonModel[1];
2343
+ }
2344
+ }
2345
+
2346
+ out.isRouting = !!out.requestId && line.includes("Routing request with path");
2347
+ return out;
2348
+ }
2349
+
2350
+ type RequestSummary = {
2351
+ requestId: string;
2352
+ modelId: string;
2353
+ timestamp?: string;
2354
+ };
2355
+
2356
+ /**
2357
+ * Scan log lines newest-first and collect one record per request, keyed off
2358
+ * the "Routing request with path" line (the canonical per-request entry).
2359
+ * The model is enriched in a second pass from any line belonging to the
2360
+ * request (the SDK pricing line embeds modelId even in legacy logs).
2361
+ */
2362
+ export function collectRecentRequestsFromLines(
2363
+ lines: string[],
2364
+ limit: number,
2365
+ ): RequestSummary[] {
2366
+ const result: RequestSummary[] = [];
2367
+ const seen = new Set<string>();
2368
+
2369
+ for (let i = lines.length - 1; i >= 0; i--) {
2370
+ const parsed = parseStructuredLogLine(lines[i]!);
2371
+ if (!parsed.isRouting || !parsed.requestId) continue;
2372
+ if (seen.has(parsed.requestId)) continue;
2373
+ seen.add(parsed.requestId);
2374
+
2375
+ result.push({
2376
+ requestId: parsed.requestId,
2377
+ modelId: parsed.modelId ?? "unknown",
2378
+ timestamp: parsed.timestamp,
2379
+ });
2380
+
2381
+ if (result.length >= limit) break;
2382
+ }
2383
+
2384
+ // Second pass: fill in the model for requests whose routing line predates
2385
+ // the `[model:...]` tag but which logged `{"modelId":"..."}` later on.
2386
+ const byRequestId = new Map(result.map((r) => [r.requestId, r]));
2387
+ for (const line of lines) {
2388
+ const parsed = parseStructuredLogLine(line);
2389
+ if (!parsed.requestId || !parsed.modelId) continue;
2390
+ const record = byRequestId.get(parsed.requestId);
2391
+ if (record && record.modelId === "unknown") {
2392
+ record.modelId = parsed.modelId;
2393
+ }
2394
+ }
2395
+
2396
+ return result;
2397
+ }
2398
+
2399
+ function collectRecentRequests(
2400
+ files: string[],
2401
+ limit: number,
2402
+ ): RequestSummary[] {
2403
+ const result: RequestSummary[] = [];
2404
+ const seen = new Set<string>();
2405
+
2406
+ // Files are passed oldest-first (yesterday, today); scan newest-first.
2407
+ for (const file of [...files].reverse()) {
2408
+ if (!existsSync(file)) continue;
2409
+ const remaining = limit - result.length;
2410
+ if (remaining <= 0) break;
2411
+
2412
+ for (const request of collectRecentRequestsFromLines(
2413
+ readAllLines(file),
2414
+ remaining,
2415
+ )) {
2416
+ if (seen.has(request.requestId)) continue;
2417
+ seen.add(request.requestId);
2418
+ result.push(request);
2419
+ if (result.length >= limit) return result;
2420
+ }
2421
+ }
2422
+
2423
+ return result;
2424
+ }
2425
+
2426
+ function filterLinesByRequestId(
2427
+ files: string[],
2428
+ requestId: string,
2429
+ ): string[] {
2430
+ const matches: string[] = [];
2431
+ for (const file of files) {
2432
+ if (!existsSync(file)) continue;
2433
+ for (const line of readAllLines(file)) {
2434
+ if (parseStructuredLogLine(line).requestId === requestId) {
2435
+ matches.push(line);
2436
+ }
2437
+ }
2438
+ }
2439
+ return matches;
2440
+ }
2441
+
2442
+ function parseLinesOption(value: string): number {
2443
+ const parsed = Number.parseInt(value, 10);
2444
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
2445
+ }
2446
+
2447
+
2305
2448
  program
2306
2449
  .command("logs")
2307
2450
  .description("View daemon logs")
2308
2451
  .option("-f, --follow", "Follow log output", false)
2309
2452
  .option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false)
2310
2453
  .option("-n, --lines <number>", "Number of lines to show", "50")
2311
- .action(async (options: { follow: boolean; lines: string; coco: boolean }) => {
2312
- await requireLocalDaemon();
2313
- const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
2314
- const todayFile = getLogFileForDate(logsDir);
2315
- const yesterday = new Date();
2316
- yesterday.setDate(yesterday.getDate() - 1);
2317
- const yesterdayFile = getLogFileForDate(logsDir, yesterday);
2318
-
2319
- if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
2320
- console.log("No log files found. Daemon may not have started yet.");
2321
- console.log(`Logs directory: ${logsDir}`);
2322
- process.exit(1);
2323
- }
2454
+ .option("-r, --recent", "List recent request ids with their model", false)
2455
+ .option("-i, --request-id <id>", "Only show log lines for a specific request id")
2456
+ .action(
2457
+ async (options: {
2458
+ follow: boolean;
2459
+ lines: string;
2460
+ coco: boolean;
2461
+ recent: boolean;
2462
+ requestId?: string;
2463
+ }) => {
2464
+ await requireLocalDaemon();
2324
2465
 
2325
- const lines = parseInt(options.lines, 10);
2466
+ if (options.recent && options.requestId) {
2467
+ console.error(
2468
+ "Cannot combine --recent (-r) with --request-id (-i).",
2469
+ );
2470
+ process.exit(1);
2471
+ }
2326
2472
 
2327
- const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
2328
- return existsSync(file) && files.indexOf(file) === index;
2329
- });
2473
+ const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
2474
+ const todayFile = getLogFileForDate(logsDir);
2475
+ const yesterday = new Date();
2476
+ yesterday.setDate(yesterday.getDate() - 1);
2477
+ const yesterdayFile = getLogFileForDate(logsDir, yesterday);
2330
2478
 
2331
- if (options.follow) {
2332
- if (existsSync(todayFile)) {
2333
- await followLogFile(todayFile, lines);
2334
- } else {
2335
- console.log("No log file for today to follow.");
2479
+ if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
2480
+ console.log("No log files found. Daemon may not have started yet.");
2481
+ console.log(`Logs directory: ${logsDir}`);
2482
+ process.exit(1);
2336
2483
  }
2337
- return;
2338
- }
2339
2484
 
2340
- for (const file of logFiles) {
2341
- if (logFiles.length > 1) {
2342
- console.log(`==> ${file} <==`);
2485
+ const lines = parseLinesOption(options.lines);
2486
+
2487
+ const logFiles = [yesterdayFile, todayFile].filter(
2488
+ (file, index, files) =>
2489
+ existsSync(file) && files.indexOf(file) === index,
2490
+ );
2491
+
2492
+ if (options.recent) {
2493
+ const requests = collectRecentRequests(logFiles, lines);
2494
+ if (requests.length === 0) {
2495
+ console.log("No requests found in recent logs.");
2496
+ return;
2497
+ }
2498
+
2499
+ console.log("Recent requests (newest first):");
2500
+ for (const request of requests) {
2501
+ const when = request.timestamp ? ` ${request.timestamp}` : "";
2502
+ console.log(` ${request.requestId} ${request.modelId}${when}`);
2503
+ }
2504
+ return;
2343
2505
  }
2344
- const output = readLastLines(file, lines);
2345
- if (output) {
2346
- console.log(output);
2506
+
2507
+ if (options.requestId) {
2508
+ const matches = filterLinesByRequestId(logFiles, options.requestId);
2509
+ if (matches.length === 0) {
2510
+ console.log(
2511
+ `No log lines found for request '${options.requestId}'.`,
2512
+ );
2513
+ return;
2514
+ }
2515
+
2516
+ for (const line of matches) {
2517
+ console.log(line);
2518
+ }
2519
+ return;
2347
2520
  }
2348
- }
2349
- });
2521
+
2522
+ if (options.follow) {
2523
+ if (existsSync(todayFile)) {
2524
+ await followLogFile(todayFile, lines);
2525
+ } else {
2526
+ console.log("No log file for today to follow.");
2527
+ }
2528
+ return;
2529
+ }
2530
+
2531
+ for (const file of logFiles) {
2532
+ if (logFiles.length > 1) {
2533
+ console.log(`==> ${file} <==`);
2534
+ }
2535
+ const output = readLastLines(file, lines);
2536
+ if (output) {
2537
+ console.log(output);
2538
+ }
2539
+ }
2540
+ },
2541
+ );
2350
2542
 
2351
2543
  export function cli(args: string[]) {
2352
2544
  program.parse(args);