@tonyclaw/agent-inspector 2.0.43 → 2.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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-CxM1gCfL.js → CompareDrawer-VDpcSNGM.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-fsXSjqtm.js +117 -0
- package/.output/public/assets/{ReplayDialog-UseUkucS.js → ReplayDialog-CoDgTzHR.js} +1 -1
- package/.output/public/assets/RequestAnatomy-t3plgkPE.js +1 -0
- package/.output/public/assets/ResponseView-BYuKiNeG.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-JSQEPeNS.js → StreamingChunkSequence-DOdU38Is.js} +1 -1
- package/.output/public/assets/_sessionId-ew5QlyZ2.js +1 -0
- package/.output/public/assets/index-DFICWD6o.js +1 -0
- package/.output/public/assets/index-zLLCkAnF.css +1 -0
- package/.output/public/assets/{main-BpGVJcpB.js → main-DmzLt6ti.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +219 -178
- package/.output/server/{_sessionId-DGn-TENM.mjs → _sessionId-CCN1UoEf.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-CFElCSYY.mjs → CompareDrawer-DkSY3vxP.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-B7SR9mrD.mjs → ProxyViewerContainer-OTh1V_Kh.mjs} +798 -394
- package/.output/server/_ssr/{ReplayDialog-DfKapj0k.mjs → ReplayDialog-DhI6yxrR.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-CUxTCg31.mjs → RequestAnatomy-DN1SJVVL.mjs} +4 -4
- package/.output/server/_ssr/{ResponseView-BXY0w197.mjs → ResponseView-DW-kpJL5.mjs} +4 -4
- package/.output/server/_ssr/{StreamingChunkSequence-CzMnES9H.mjs → StreamingChunkSequence-BAE57Fae.mjs} +4 -4
- package/.output/server/_ssr/{index-CVlT-REW.mjs → index-C9ryJEna.mjs} +3 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-B2d1LUx6.mjs → router-DIIJGyML.mjs} +458 -10
- package/.output/server/{_tanstack-start-manifest_v-Drpi28BM.mjs → _tanstack-start-manifest_v-C_v9-E5d.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +74 -68
- package/src/components/ProxyViewer.tsx +869 -363
- package/src/components/ProxyViewerContainer.tsx +135 -3
- package/src/components/proxy-viewer/LogEntryHeader.tsx +12 -6
- package/src/components/ui/crab-logo.tsx +0 -50
- package/src/proxy/logIndex.ts +188 -1
- package/src/proxy/store.ts +405 -3
- package/src/routes/api/logs.ts +36 -0
- package/.output/public/assets/ProxyViewerContainer-TtRG-0E7.js +0 -117
- package/.output/public/assets/RequestAnatomy-aPxgEJ2L.js +0 -1
- package/.output/public/assets/ResponseView-DA-F4F97.js +0 -1
- package/.output/public/assets/_sessionId-BEXuCWq5.js +0 -1
- package/.output/public/assets/index-Bhsa_2xX.js +0 -1
- package/.output/public/assets/index-DOWlRJ0W.css +0 -1
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef, useMemo, type JSX } from "react";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CapturedLogSchema, type CapturedLog } from "../contracts";
|
|
4
|
+
import { fetchJson } from "../lib/apiClient";
|
|
4
5
|
import { InspectorGroupsListResponseSchema, type InspectorGroup } from "../lib/groupContract";
|
|
5
6
|
import { useStripConfig } from "../lib/useStripConfig";
|
|
6
7
|
import { OnboardingBanner } from "./OnboardingBanner";
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ProxyViewer,
|
|
10
|
+
type LogPaginationControls,
|
|
11
|
+
type SessionMembershipEvidence,
|
|
12
|
+
} from "./ProxyViewer";
|
|
8
13
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
9
14
|
|
|
10
15
|
type SSEUpdate =
|
|
@@ -28,6 +33,24 @@ const SSEUpdateSchema = z.union([
|
|
|
28
33
|
}),
|
|
29
34
|
]);
|
|
30
35
|
|
|
36
|
+
const LogCursorPageSchema = z.object({
|
|
37
|
+
logs: z.array(CapturedLogSchema),
|
|
38
|
+
total: z.number().int().nonnegative(),
|
|
39
|
+
limit: z.number().int().positive(),
|
|
40
|
+
hasOlder: z.boolean(),
|
|
41
|
+
hasNewer: z.boolean(),
|
|
42
|
+
oldestLogId: z.number().int().positive().nullable(),
|
|
43
|
+
newestLogId: z.number().int().positive().nullable(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
type LogCursorPage = z.infer<typeof LogCursorPageSchema>;
|
|
47
|
+
|
|
48
|
+
type SessionPageRequest =
|
|
49
|
+
| { kind: "newest" }
|
|
50
|
+
| { kind: "oldest" }
|
|
51
|
+
| { kind: "older"; beforeLogId: number }
|
|
52
|
+
| { kind: "newer"; afterLogId: number };
|
|
53
|
+
|
|
31
54
|
function buildSessionMembershipEvidence(
|
|
32
55
|
groups: readonly InspectorGroup[],
|
|
33
56
|
sessionId: string,
|
|
@@ -89,6 +112,7 @@ const DEBOUNCE_MS = 50;
|
|
|
89
112
|
const HASH_SCROLL_ATTEMPTS = 12;
|
|
90
113
|
const HASH_HIGHLIGHT_MS = 1800;
|
|
91
114
|
const MAX_CLIENT_LOGS = 500;
|
|
115
|
+
const SESSION_PAGE_LIMIT = 100;
|
|
92
116
|
|
|
93
117
|
function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
94
118
|
const params = new URLSearchParams();
|
|
@@ -100,6 +124,31 @@ function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
|
100
124
|
return query.length > 0 ? `/api/logs/stream?${query}` : "/api/logs/stream";
|
|
101
125
|
}
|
|
102
126
|
|
|
127
|
+
function buildSessionLogsPageUrl(sessionId: string, request: SessionPageRequest): string {
|
|
128
|
+
const params = new URLSearchParams({
|
|
129
|
+
cursor: "1",
|
|
130
|
+
compact: "1",
|
|
131
|
+
limit: String(SESSION_PAGE_LIMIT),
|
|
132
|
+
sessionId,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
switch (request.kind) {
|
|
136
|
+
case "oldest":
|
|
137
|
+
params.set("anchor", "oldest");
|
|
138
|
+
break;
|
|
139
|
+
case "older":
|
|
140
|
+
params.set("beforeLogId", String(request.beforeLogId));
|
|
141
|
+
break;
|
|
142
|
+
case "newer":
|
|
143
|
+
params.set("afterLogId", String(request.afterLogId));
|
|
144
|
+
break;
|
|
145
|
+
case "newest":
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return `/api/logs?${params.toString()}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
103
152
|
function buildLogIndex(logs: readonly CapturedLog[]): Map<number, number> {
|
|
104
153
|
const idx = new Map<number, number>();
|
|
105
154
|
for (let i = 0; i < logs.length; i++) {
|
|
@@ -138,10 +187,13 @@ export function ProxyViewerContainer({
|
|
|
138
187
|
const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
|
|
139
188
|
const [error, setError] = useState<string | null>(null);
|
|
140
189
|
const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
|
|
190
|
+
const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
|
|
191
|
+
const [sessionPageLoading, setSessionPageLoading] = useState(initialSessionId !== undefined);
|
|
141
192
|
const [sessionMemberships, setSessionMemberships] = useState<SessionMembershipEvidence[]>([]);
|
|
142
193
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
143
194
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
144
195
|
const handledHashRef = useRef<string | null>(null);
|
|
196
|
+
const sessionPageRequestIdRef = useRef(0);
|
|
145
197
|
|
|
146
198
|
// O(1) log lookup by id
|
|
147
199
|
const logIndexRef = useRef<Map<number, number>>(new Map());
|
|
@@ -245,6 +297,37 @@ export function ProxyViewerContainer({
|
|
|
245
297
|
};
|
|
246
298
|
}, [initialSessionId, scheduleUpdate]);
|
|
247
299
|
|
|
300
|
+
const loadSessionPage = useCallback(
|
|
301
|
+
(request: SessionPageRequest) => {
|
|
302
|
+
if (initialSessionId === undefined) return;
|
|
303
|
+
|
|
304
|
+
const requestId = sessionPageRequestIdRef.current + 1;
|
|
305
|
+
sessionPageRequestIdRef.current = requestId;
|
|
306
|
+
setSessionPageLoading(true);
|
|
307
|
+
setError(null);
|
|
308
|
+
|
|
309
|
+
void fetchJson(buildSessionLogsPageUrl(initialSessionId, request), LogCursorPageSchema)
|
|
310
|
+
.then((page) => {
|
|
311
|
+
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
312
|
+
const nextLogs = page.logs;
|
|
313
|
+
logIndexRef.current = buildLogIndex(nextLogs);
|
|
314
|
+
setAllLogs(nextLogs);
|
|
315
|
+
setLogPage(page);
|
|
316
|
+
setSelectedSession(initialSessionId);
|
|
317
|
+
setSessionPageLoading(false);
|
|
318
|
+
setError(null);
|
|
319
|
+
})
|
|
320
|
+
.catch((err: unknown) => {
|
|
321
|
+
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
322
|
+
setAllLogs([]);
|
|
323
|
+
setLogPage(null);
|
|
324
|
+
setSessionPageLoading(false);
|
|
325
|
+
setError(err instanceof Error ? err.message : "Failed to load session logs");
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
[initialSessionId],
|
|
329
|
+
);
|
|
330
|
+
|
|
248
331
|
useEffect(() => {
|
|
249
332
|
if (initialSessionId === undefined) {
|
|
250
333
|
setSessionMemberships([]);
|
|
@@ -273,6 +356,12 @@ export function ProxyViewerContainer({
|
|
|
273
356
|
}, [initialSessionId]);
|
|
274
357
|
|
|
275
358
|
useEffect(() => {
|
|
359
|
+
if (initialSessionId === undefined) return;
|
|
360
|
+
loadSessionPage({ kind: "newest" });
|
|
361
|
+
}, [initialSessionId, loadSessionPage]);
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (initialSessionId !== undefined) return undefined;
|
|
276
365
|
connectSSE();
|
|
277
366
|
return () => {
|
|
278
367
|
if (eventSourceRef.current) {
|
|
@@ -288,7 +377,7 @@ export function ProxyViewerContainer({
|
|
|
288
377
|
flushTimerRef.current = null;
|
|
289
378
|
}
|
|
290
379
|
};
|
|
291
|
-
}, [connectSSE]);
|
|
380
|
+
}, [connectSSE, initialSessionId]);
|
|
292
381
|
|
|
293
382
|
useEffect(() => {
|
|
294
383
|
const hash = window.location.hash;
|
|
@@ -402,6 +491,48 @@ export function ProxyViewerContainer({
|
|
|
402
491
|
// Read the strip config once at the container so the virtualized list does
|
|
403
492
|
// not need N independent SWR subscriptions per row.
|
|
404
493
|
const { strip, captureMode, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
|
|
494
|
+
const loadOldestPage = useCallback(() => {
|
|
495
|
+
loadSessionPage({ kind: "oldest" });
|
|
496
|
+
}, [loadSessionPage]);
|
|
497
|
+
const loadNewestPage = useCallback(() => {
|
|
498
|
+
loadSessionPage({ kind: "newest" });
|
|
499
|
+
}, [loadSessionPage]);
|
|
500
|
+
const loadOlderPage = useCallback(() => {
|
|
501
|
+
const firstLog = allLogs[0];
|
|
502
|
+
const beforeLogId = logPage?.oldestLogId ?? firstLog?.id ?? null;
|
|
503
|
+
if (beforeLogId === null) return;
|
|
504
|
+
loadSessionPage({ kind: "older", beforeLogId });
|
|
505
|
+
}, [allLogs, loadSessionPage, logPage]);
|
|
506
|
+
const loadNewerPage = useCallback(() => {
|
|
507
|
+
const lastLog = allLogs[allLogs.length - 1];
|
|
508
|
+
const afterLogId = logPage?.newestLogId ?? lastLog?.id ?? null;
|
|
509
|
+
if (afterLogId === null) return;
|
|
510
|
+
loadSessionPage({ kind: "newer", afterLogId });
|
|
511
|
+
}, [allLogs, loadSessionPage, logPage]);
|
|
512
|
+
const pagination = useMemo<LogPaginationControls | undefined>(() => {
|
|
513
|
+
if (initialSessionId === undefined) return undefined;
|
|
514
|
+
return {
|
|
515
|
+
isLoading: sessionPageLoading,
|
|
516
|
+
total: logPage?.total ?? null,
|
|
517
|
+
pageSize: logPage?.limit ?? SESSION_PAGE_LIMIT,
|
|
518
|
+
hasOlder: logPage?.hasOlder ?? false,
|
|
519
|
+
hasNewer: logPage?.hasNewer ?? false,
|
|
520
|
+
oldestLogId: logPage?.oldestLogId ?? null,
|
|
521
|
+
newestLogId: logPage?.newestLogId ?? null,
|
|
522
|
+
onOldest: loadOldestPage,
|
|
523
|
+
onOlder: loadOlderPage,
|
|
524
|
+
onNewer: loadNewerPage,
|
|
525
|
+
onNewest: loadNewestPage,
|
|
526
|
+
};
|
|
527
|
+
}, [
|
|
528
|
+
initialSessionId,
|
|
529
|
+
loadNewestPage,
|
|
530
|
+
loadNewerPage,
|
|
531
|
+
loadOlderPage,
|
|
532
|
+
loadOldestPage,
|
|
533
|
+
logPage,
|
|
534
|
+
sessionPageLoading,
|
|
535
|
+
]);
|
|
405
536
|
|
|
406
537
|
return (
|
|
407
538
|
<>
|
|
@@ -421,7 +552,8 @@ export function ProxyViewerContainer({
|
|
|
421
552
|
onModelChange={setSelectedModel}
|
|
422
553
|
onClearAll={handleClearAll}
|
|
423
554
|
onClearGroup={handleClearGroup}
|
|
424
|
-
isLoading={initialSessionId
|
|
555
|
+
isLoading={initialSessionId === undefined ? !streamInitialized : sessionPageLoading}
|
|
556
|
+
pagination={pagination}
|
|
425
557
|
sessionMemberships={sessionMemberships}
|
|
426
558
|
viewMode={viewMode}
|
|
427
559
|
onViewModeChange={setViewMode}
|
|
@@ -170,7 +170,7 @@ export const LogEntryHeader = memo(function ({
|
|
|
170
170
|
data-nav-id={`log-${log.id}`}
|
|
171
171
|
data-nav-action={expanded ? "collapse" : "expand"}
|
|
172
172
|
className={cn(
|
|
173
|
-
"flex items-center gap-2 px-3 py-1 cursor-pointer transition-colors",
|
|
173
|
+
"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-3 py-1.5 cursor-pointer transition-colors",
|
|
174
174
|
"hover:bg-muted/50",
|
|
175
175
|
"select-none",
|
|
176
176
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
|
|
@@ -302,17 +302,23 @@ export const LogEntryHeader = memo(function ({
|
|
|
302
302
|
|
|
303
303
|
{/* Token counts */}
|
|
304
304
|
{hasTokens && (
|
|
305
|
-
<span className="flex items-center gap-1 text-xs shrink-0">
|
|
305
|
+
<span className="flex min-w-0 items-center gap-1 text-xs sm:shrink-0">
|
|
306
306
|
<Zap className="size-3 text-muted-foreground" />
|
|
307
|
-
<span className="font-mono tabular-nums">
|
|
307
|
+
<span className="flex min-w-0 flex-wrap items-center gap-x-1 font-mono tabular-nums">
|
|
308
308
|
<span
|
|
309
|
-
className={
|
|
309
|
+
className={cn(
|
|
310
|
+
"whitespace-nowrap",
|
|
311
|
+
log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground",
|
|
312
|
+
)}
|
|
310
313
|
>
|
|
311
314
|
IN {log.inputTokens !== null ? formatTokens(log.inputTokens) : "—"}
|
|
312
315
|
</span>
|
|
313
|
-
|
|
316
|
+
<span className="text-muted-foreground">/</span>
|
|
314
317
|
<span
|
|
315
|
-
className={
|
|
318
|
+
className={cn(
|
|
319
|
+
"whitespace-nowrap",
|
|
320
|
+
log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground",
|
|
321
|
+
)}
|
|
316
322
|
>
|
|
317
323
|
OUT {log.outputTokens !== null ? formatTokens(log.outputTokens) : "—"}
|
|
318
324
|
</span>
|
|
@@ -40,56 +40,6 @@ export function CrabLogo({ className }: { className?: string }): JSX.Element {
|
|
|
40
40
|
<line x1="9" y1="17.5" x2="8" y2="20.5" />
|
|
41
41
|
<line x1="15" y1="17.5" x2="16" y2="20.5" />
|
|
42
42
|
<line x1="17.5" y1="16" x2="19.5" y2="19.5" />
|
|
43
|
-
{/* Duanwu zongzi charms */}
|
|
44
|
-
<g>
|
|
45
|
-
<path
|
|
46
|
-
d="M5.6 17.7 L8.4 15.4 L10.8 18 C9.1 18.9 7.2 18.8 5.6 17.7 Z"
|
|
47
|
-
fill="#2f6b3f"
|
|
48
|
-
stroke="none"
|
|
49
|
-
opacity="0.95"
|
|
50
|
-
/>
|
|
51
|
-
<path
|
|
52
|
-
d="M6.9 17.5 L8.4 16 L9.8 17.7 C8.9 18.1 7.8 18.1 6.9 17.5 Z"
|
|
53
|
-
fill="#6fb36f"
|
|
54
|
-
stroke="none"
|
|
55
|
-
opacity="0.95"
|
|
56
|
-
/>
|
|
57
|
-
<path d="M8.4 16 L8.4 18.4" stroke="#d6b45f" strokeWidth="0.34" />
|
|
58
|
-
<circle cx="10.6" cy="17.75" r="0.36" fill="#c2412d" stroke="none" />
|
|
59
|
-
</g>
|
|
60
|
-
<g>
|
|
61
|
-
<path
|
|
62
|
-
d="M8 16.5 L11.9 13.6 L15.8 16.8 C13.3 18.4 10.4 18.3 8 16.5 Z"
|
|
63
|
-
fill="#2f6b3f"
|
|
64
|
-
stroke="none"
|
|
65
|
-
opacity="0.97"
|
|
66
|
-
/>
|
|
67
|
-
<path
|
|
68
|
-
d="M9.8 16.3 L11.9 14.4 L14.3 16.5 C12.8 17.4 11.3 17.4 9.8 16.3 Z"
|
|
69
|
-
fill="#6fb36f"
|
|
70
|
-
stroke="none"
|
|
71
|
-
opacity="0.95"
|
|
72
|
-
/>
|
|
73
|
-
<path d="M11.9 14.3 L11.9 17.8" stroke="#d6b45f" strokeWidth="0.42" />
|
|
74
|
-
<path d="M9.8 16.3 C11.2 15.8 12.9 15.9 14.3 16.5" stroke="#9fca78" strokeWidth="0.36" />
|
|
75
|
-
<circle cx="15.6" cy="16.75" r="0.43" fill="#c2412d" stroke="none" />
|
|
76
|
-
</g>
|
|
77
|
-
<g>
|
|
78
|
-
<path
|
|
79
|
-
d="M13.6 18.1 L16.2 15.7 L18.5 18.2 C17.1 19.1 15.2 19 13.6 18.1 Z"
|
|
80
|
-
fill="#2f6b3f"
|
|
81
|
-
stroke="none"
|
|
82
|
-
opacity="0.95"
|
|
83
|
-
/>
|
|
84
|
-
<path
|
|
85
|
-
d="M14.8 17.9 L16.2 16.3 L17.5 18 C16.6 18.4 15.7 18.4 14.8 17.9 Z"
|
|
86
|
-
fill="#6fb36f"
|
|
87
|
-
stroke="none"
|
|
88
|
-
opacity="0.95"
|
|
89
|
-
/>
|
|
90
|
-
<path d="M16.2 16.3 L16.2 18.6" stroke="#d6b45f" strokeWidth="0.34" />
|
|
91
|
-
<circle cx="18.35" cy="18.15" r="0.36" fill="#c2412d" stroke="none" />
|
|
92
|
-
</g>
|
|
93
43
|
</svg>
|
|
94
44
|
);
|
|
95
45
|
}
|
package/src/proxy/logIndex.ts
CHANGED
|
@@ -4,12 +4,56 @@ import { join, dirname } from "node:path";
|
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
6
|
import { resolveLogDir, logger } from "./logger";
|
|
7
|
+
import type { ApiFormat, CapturedLog } from "./schemas";
|
|
8
|
+
|
|
9
|
+
export type LogIndexSummary = {
|
|
10
|
+
id: number;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
method: string;
|
|
13
|
+
path: string;
|
|
14
|
+
model: string | null;
|
|
15
|
+
sessionId: string | null;
|
|
16
|
+
responseStatus: number | null;
|
|
17
|
+
inputTokens: number | null;
|
|
18
|
+
outputTokens: number | null;
|
|
19
|
+
cacheCreationInputTokens: number | null;
|
|
20
|
+
cacheReadInputTokens: number | null;
|
|
21
|
+
elapsedMs: number | null;
|
|
22
|
+
firstChunkMs: number | null;
|
|
23
|
+
totalStreamMs: number | null;
|
|
24
|
+
tokensPerSecond: number | null;
|
|
25
|
+
streaming: boolean;
|
|
26
|
+
userAgent: string | null;
|
|
27
|
+
origin: string | null;
|
|
28
|
+
apiFormat: ApiFormat;
|
|
29
|
+
isTest: boolean;
|
|
30
|
+
replayOfLogId: number | null;
|
|
31
|
+
providerName: string | null;
|
|
32
|
+
clientPort: number | null;
|
|
33
|
+
clientPid: number | null;
|
|
34
|
+
clientCwd: string | null;
|
|
35
|
+
clientProjectFolder: string | null;
|
|
36
|
+
streamingChunksPath: string | null;
|
|
37
|
+
rawRequestBodyBytes: number | null;
|
|
38
|
+
responseTextBytes: number | null;
|
|
39
|
+
warnings?: string[];
|
|
40
|
+
error: string | null;
|
|
41
|
+
};
|
|
7
42
|
|
|
8
43
|
export type LogIndexEntry = {
|
|
9
44
|
id: number;
|
|
10
45
|
file: string;
|
|
11
46
|
byteOffset: number;
|
|
12
47
|
byteLength: number;
|
|
48
|
+
sessionId?: string | null;
|
|
49
|
+
model?: string | null;
|
|
50
|
+
summary?: LogIndexSummary;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type LogIndexEntryMetadata = {
|
|
54
|
+
sessionId: string | null;
|
|
55
|
+
model: string | null;
|
|
56
|
+
summary: LogIndexSummary;
|
|
13
57
|
};
|
|
14
58
|
|
|
15
59
|
type LogIndex = {
|
|
@@ -29,6 +73,50 @@ function getIndexPath(): string {
|
|
|
29
73
|
|
|
30
74
|
let cachedIndex: LogIndex | null = null;
|
|
31
75
|
|
|
76
|
+
function textByteLength(value: string | null): number | null {
|
|
77
|
+
return value === null ? null : Buffer.byteLength(value, "utf8");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createLogIndexEntryMetadata(log: CapturedLog): LogIndexEntryMetadata {
|
|
81
|
+
return {
|
|
82
|
+
sessionId: log.sessionId,
|
|
83
|
+
model: log.model,
|
|
84
|
+
summary: {
|
|
85
|
+
id: log.id,
|
|
86
|
+
timestamp: log.timestamp,
|
|
87
|
+
method: log.method,
|
|
88
|
+
path: log.path,
|
|
89
|
+
model: log.model,
|
|
90
|
+
sessionId: log.sessionId,
|
|
91
|
+
responseStatus: log.responseStatus,
|
|
92
|
+
inputTokens: log.inputTokens,
|
|
93
|
+
outputTokens: log.outputTokens,
|
|
94
|
+
cacheCreationInputTokens: log.cacheCreationInputTokens,
|
|
95
|
+
cacheReadInputTokens: log.cacheReadInputTokens,
|
|
96
|
+
elapsedMs: log.elapsedMs,
|
|
97
|
+
firstChunkMs: log.firstChunkMs ?? null,
|
|
98
|
+
totalStreamMs: log.totalStreamMs ?? null,
|
|
99
|
+
tokensPerSecond: log.tokensPerSecond ?? null,
|
|
100
|
+
streaming: log.streaming,
|
|
101
|
+
userAgent: log.userAgent,
|
|
102
|
+
origin: log.origin,
|
|
103
|
+
apiFormat: log.apiFormat,
|
|
104
|
+
isTest: log.isTest ?? false,
|
|
105
|
+
replayOfLogId: log.replayOfLogId ?? null,
|
|
106
|
+
providerName: log.providerName ?? null,
|
|
107
|
+
clientPort: log.clientPort ?? null,
|
|
108
|
+
clientPid: log.clientPid ?? null,
|
|
109
|
+
clientCwd: log.clientCwd ?? null,
|
|
110
|
+
clientProjectFolder: log.clientProjectFolder ?? null,
|
|
111
|
+
streamingChunksPath: log.streamingChunksPath ?? null,
|
|
112
|
+
rawRequestBodyBytes: log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody),
|
|
113
|
+
responseTextBytes: log.responseTextBytes ?? textByteLength(log.responseText),
|
|
114
|
+
warnings: log.warnings,
|
|
115
|
+
error: log.error ?? null,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
32
120
|
function createEmptyIndex(): LogIndex {
|
|
33
121
|
return {
|
|
34
122
|
version: INDEX_VERSION,
|
|
@@ -100,9 +188,13 @@ export async function addToIndex(
|
|
|
100
188
|
file: string,
|
|
101
189
|
byteOffset: number,
|
|
102
190
|
byteLength: number,
|
|
191
|
+
metadata?: LogIndexEntryMetadata,
|
|
103
192
|
): Promise<void> {
|
|
104
193
|
const index = await loadIndex();
|
|
105
|
-
index.entries[id] =
|
|
194
|
+
index.entries[id] =
|
|
195
|
+
metadata === undefined
|
|
196
|
+
? { id, file, byteOffset, byteLength }
|
|
197
|
+
: { id, file, byteOffset, byteLength, ...metadata };
|
|
106
198
|
if (id > index.maxId) {
|
|
107
199
|
index.maxId = id;
|
|
108
200
|
}
|
|
@@ -154,6 +246,98 @@ type FileIndexResult = {
|
|
|
154
246
|
maxId: number;
|
|
155
247
|
};
|
|
156
248
|
|
|
249
|
+
function readStringOrNullField(entry: object, field: string): string | null {
|
|
250
|
+
const desc = Object.getOwnPropertyDescriptor(entry, field);
|
|
251
|
+
if (desc === undefined) return null;
|
|
252
|
+
if (typeof desc.value === "string") return desc.value;
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function readStringField(entry: object, field: string): string {
|
|
257
|
+
return readStringOrNullField(entry, field) ?? "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readNumberOrNullField(entry: object, field: string): number | null {
|
|
261
|
+
const desc = Object.getOwnPropertyDescriptor(entry, field);
|
|
262
|
+
if (desc === undefined) return null;
|
|
263
|
+
return typeof desc.value === "number" ? desc.value : null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readBooleanField(entry: object, field: string): boolean {
|
|
267
|
+
const desc = Object.getOwnPropertyDescriptor(entry, field);
|
|
268
|
+
if (desc === undefined) return false;
|
|
269
|
+
return typeof desc.value === "boolean" ? desc.value : false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readApiFormatField(entry: object): ApiFormat {
|
|
273
|
+
const value = readStringOrNullField(entry, "apiFormat");
|
|
274
|
+
switch (value) {
|
|
275
|
+
case "anthropic":
|
|
276
|
+
return "anthropic";
|
|
277
|
+
case "openai":
|
|
278
|
+
return "openai";
|
|
279
|
+
case "unknown":
|
|
280
|
+
case null:
|
|
281
|
+
return "unknown";
|
|
282
|
+
default:
|
|
283
|
+
return "unknown";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readStringArrayField(entry: object, field: string): string[] | undefined {
|
|
288
|
+
const desc = Object.getOwnPropertyDescriptor(entry, field);
|
|
289
|
+
if (desc === undefined || !Array.isArray(desc.value)) return undefined;
|
|
290
|
+
|
|
291
|
+
const values: string[] = [];
|
|
292
|
+
for (const item of desc.value) {
|
|
293
|
+
if (typeof item !== "string") return undefined;
|
|
294
|
+
values.push(item);
|
|
295
|
+
}
|
|
296
|
+
return values;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function readTextByteLength(entry: object, bytesField: string, textField: string): number | null {
|
|
300
|
+
const explicitBytes = readNumberOrNullField(entry, bytesField);
|
|
301
|
+
if (explicitBytes !== null && explicitBytes >= 0) return explicitBytes;
|
|
302
|
+
return textByteLength(readStringOrNullField(entry, textField));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function readLogIndexSummary(entry: object, id: number): LogIndexSummary {
|
|
306
|
+
return {
|
|
307
|
+
id,
|
|
308
|
+
timestamp: readStringField(entry, "timestamp"),
|
|
309
|
+
method: readStringField(entry, "method"),
|
|
310
|
+
path: readStringField(entry, "path"),
|
|
311
|
+
model: readStringOrNullField(entry, "model"),
|
|
312
|
+
sessionId: readStringOrNullField(entry, "sessionId"),
|
|
313
|
+
responseStatus: readNumberOrNullField(entry, "responseStatus"),
|
|
314
|
+
inputTokens: readNumberOrNullField(entry, "inputTokens"),
|
|
315
|
+
outputTokens: readNumberOrNullField(entry, "outputTokens"),
|
|
316
|
+
cacheCreationInputTokens: readNumberOrNullField(entry, "cacheCreationInputTokens"),
|
|
317
|
+
cacheReadInputTokens: readNumberOrNullField(entry, "cacheReadInputTokens"),
|
|
318
|
+
elapsedMs: readNumberOrNullField(entry, "elapsedMs"),
|
|
319
|
+
firstChunkMs: readNumberOrNullField(entry, "firstChunkMs"),
|
|
320
|
+
totalStreamMs: readNumberOrNullField(entry, "totalStreamMs"),
|
|
321
|
+
tokensPerSecond: readNumberOrNullField(entry, "tokensPerSecond"),
|
|
322
|
+
streaming: readBooleanField(entry, "streaming"),
|
|
323
|
+
userAgent: readStringOrNullField(entry, "userAgent"),
|
|
324
|
+
origin: readStringOrNullField(entry, "origin"),
|
|
325
|
+
apiFormat: readApiFormatField(entry),
|
|
326
|
+
isTest: readBooleanField(entry, "isTest"),
|
|
327
|
+
replayOfLogId: readNumberOrNullField(entry, "replayOfLogId"),
|
|
328
|
+
providerName: readStringOrNullField(entry, "providerName"),
|
|
329
|
+
clientPort: readNumberOrNullField(entry, "clientPort"),
|
|
330
|
+
clientPid: readNumberOrNullField(entry, "clientPid"),
|
|
331
|
+
clientCwd: readStringOrNullField(entry, "clientCwd"),
|
|
332
|
+
clientProjectFolder: readStringOrNullField(entry, "clientProjectFolder"),
|
|
333
|
+
streamingChunksPath: readStringOrNullField(entry, "streamingChunksPath"),
|
|
334
|
+
rawRequestBodyBytes: readTextByteLength(entry, "rawRequestBodyBytes", "rawRequestBody"),
|
|
335
|
+
responseTextBytes: readTextByteLength(entry, "responseTextBytes", "responseText"),
|
|
336
|
+
warnings: readStringArrayField(entry, "warnings"),
|
|
337
|
+
error: readStringOrNullField(entry, "error"),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
157
341
|
async function indexFile(filePath: string, file: string): Promise<FileIndexResult> {
|
|
158
342
|
const entries: Record<number, LogIndexEntry> = {};
|
|
159
343
|
let maxId = 0;
|
|
@@ -179,6 +363,9 @@ async function indexFile(filePath: string, file: string): Promise<FileIndexResul
|
|
|
179
363
|
file,
|
|
180
364
|
byteOffset,
|
|
181
365
|
byteLength: lineBytes,
|
|
366
|
+
sessionId: readStringOrNullField(entry, "sessionId"),
|
|
367
|
+
model: readStringOrNullField(entry, "model"),
|
|
368
|
+
summary: readLogIndexSummary(entry, entryId),
|
|
182
369
|
};
|
|
183
370
|
}
|
|
184
371
|
}
|