@tonyclaw/agent-inspector 2.1.11 → 2.1.13
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/cli.js +10 -15
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-CcwXsZp3.js → CompareDrawer-Cz_1vIpR.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-DFFxBlvO.js → ProxyViewerContainer-7QSiluMf.js} +19 -19
- package/.output/public/assets/{ReplayDialog-XXiwmD5o.js → ReplayDialog-sBA1KAYD.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DTCh37so.js → RequestAnatomy-BjlBT-Cy.js} +1 -1
- package/.output/public/assets/{ResponseView-CbFYw9xP.js → ResponseView-PtEKzml9.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-CTLMzUxV.js → StreamingChunkSequence-D2_SMhlE.js} +1 -1
- package/.output/public/assets/_sessionId-DZfB4ruK.js +1 -0
- package/.output/public/assets/index-B-QQLbpz.js +1 -0
- package/.output/public/assets/{main-C-lua4Uq.js → main-Dtspb4Ui.js} +2 -2
- package/.output/server/{_sessionId-CO0WzOBc.mjs → _sessionId-CXDcLuvi.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-_OnPOi0O.mjs → CompareDrawer-CMoCAoeq.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-Bfy-cjD3.mjs → ProxyViewerContainer-BW2vVCBN.mjs} +32 -11
- package/.output/server/_ssr/{ReplayDialog-DBZyQFFR.mjs → ReplayDialog-ChXL1t8H.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-C7m6HJOG.mjs → RequestAnatomy-DtKzIlfU.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-CM2nFoqa.mjs → ResponseView-B5I8drzc.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-Dj61jLHa.mjs → StreamingChunkSequence-DvwjQNcO.mjs} +2 -2
- package/.output/server/_ssr/{index-BI_nCxdk.mjs → index-D_ZHtRfl.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-WBmANYRw.mjs → router-DhL9Wp3N.mjs} +213 -28
- package/.output/server/{_tanstack-start-manifest_v-lODJPovZ.mjs → _tanstack-start-manifest_v-BHgoAmxZ.mjs} +1 -1
- package/.output/server/index.mjs +58 -58
- package/package.json +1 -1
- package/src/cli.ts +24 -8
- package/src/components/proxy-viewer/ConversationGroup.tsx +22 -2
- package/src/components/proxy-viewer/LogEntryHeader.tsx +10 -3
- package/src/proxy/identityProxy.ts +2 -10
- package/src/proxy/socketTracker.ts +100 -24
- package/.output/public/assets/_sessionId-DAwcZWfu.js +0 -1
- package/.output/public/assets/index-B6sDoCll.js +0 -1
package/src/cli.ts
CHANGED
|
@@ -17,8 +17,14 @@ import { startIdentityProxy } from "./proxy/identityProxy.js";
|
|
|
17
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
18
|
const __dirname = dirname(__filename);
|
|
19
19
|
|
|
20
|
+
// External-facing port stays at 25947 so existing tooling / AI clients
|
|
21
|
+
// (Claude Code, OpenCode, MiMo Code, etc.) keep working with the
|
|
22
|
+
// unchanged `ANTHROPIC_BASE_URL=http://localhost:25947/proxy`. The
|
|
23
|
+
// identity-injecting proxy listens on that port; the TanStack Start
|
|
24
|
+
// upstream is moved to a private port (25949 by default) and reverse-
|
|
25
|
+
// proxied through.
|
|
20
26
|
const DEFAULT_PORT = 25947;
|
|
21
|
-
const
|
|
27
|
+
const DEFAULT_UPSTREAM_PORT = 25949;
|
|
22
28
|
const LOCAL_PROBE_TIMEOUT_MS = 2000;
|
|
23
29
|
const BRANDED_WINDOWS_RUNTIME_EXE = "agent-inspector.exe";
|
|
24
30
|
const DEFAULT_CAPTURE_MODE = "simple";
|
|
@@ -247,9 +253,9 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
247
253
|
let captureModeWasSpecified = false;
|
|
248
254
|
let enableIdentityProxy = true;
|
|
249
255
|
let identityProxyPort =
|
|
250
|
-
process.env["
|
|
251
|
-
? Number(process.env["
|
|
252
|
-
:
|
|
256
|
+
process.env["AGENT_INSPECTOR_PORT"] !== undefined
|
|
257
|
+
? Number(process.env["AGENT_INSPECTOR_PORT"])
|
|
258
|
+
: DEFAULT_PORT;
|
|
253
259
|
|
|
254
260
|
if (envMode !== undefined && envMode !== "") {
|
|
255
261
|
const parsedMode = parseCaptureMode(envMode);
|
|
@@ -445,7 +451,12 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
445
451
|
}
|
|
446
452
|
}
|
|
447
453
|
|
|
448
|
-
|
|
454
|
+
// TanStack Start listens on the private upstream port; the identity
|
|
455
|
+
// proxy owns the public-facing `port`. When the identity proxy is
|
|
456
|
+
// disabled (or not yet running) we fall back to TanStack listening on
|
|
457
|
+
// the public port directly.
|
|
458
|
+
const upstreamPort = enableIdentityProxy ? DEFAULT_UPSTREAM_PORT : port;
|
|
459
|
+
process.env["PORT"] = String(upstreamPort);
|
|
449
460
|
|
|
450
461
|
const url = urlForHost(port, host);
|
|
451
462
|
|
|
@@ -475,7 +486,10 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
475
486
|
|
|
476
487
|
if (forceRestart) {
|
|
477
488
|
killProcessOnPort(port);
|
|
478
|
-
if (enableIdentityProxy)
|
|
489
|
+
if (enableIdentityProxy) {
|
|
490
|
+
killProcessOnPort(identityProxyPort);
|
|
491
|
+
killProcessOnPort(upstreamPort);
|
|
492
|
+
}
|
|
479
493
|
}
|
|
480
494
|
|
|
481
495
|
console.log(`Server running at ${url}`);
|
|
@@ -553,12 +567,14 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
553
567
|
// still get accurate session attribution. Started only after TanStack
|
|
554
568
|
// Start is healthy, so the upstream is ready by the time clients connect.
|
|
555
569
|
const identityProxy = enableIdentityProxy
|
|
556
|
-
? await tryStartIdentityProxy(identityProxyPort,
|
|
570
|
+
? await tryStartIdentityProxy(identityProxyPort, upstreamPort, host)
|
|
557
571
|
: null;
|
|
558
572
|
|
|
559
573
|
if (background) {
|
|
560
574
|
serverProcess.unref();
|
|
561
|
-
|
|
575
|
+
// Probe the upstream TanStack port directly — the identity proxy is
|
|
576
|
+
// not started yet so the public port is not yet accepting traffic.
|
|
577
|
+
if (await waitForInspectorHealthy(upstreamPort, 5000, host)) {
|
|
562
578
|
console.log(`agent-inspector background server is ready at ${url}`);
|
|
563
579
|
if (identityProxy !== null) {
|
|
564
580
|
console.log(`Identity proxy ready on port ${identityProxyPort} (PID-attributing proxy).`);
|
|
@@ -77,6 +77,24 @@ function collectProviders(logs: CapturedLog[]): {
|
|
|
77
77
|
}));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export function getFirstClientPid(logs: readonly CapturedLog[]): number | null {
|
|
81
|
+
for (const log of logs) {
|
|
82
|
+
const pid = log.clientPid;
|
|
83
|
+
if (pid !== null && pid !== undefined) return pid;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getFirstClientProjectFolder(logs: readonly CapturedLog[]): string | null {
|
|
89
|
+
for (const log of logs) {
|
|
90
|
+
const projectFolder = log.clientProjectFolder;
|
|
91
|
+
if (projectFolder !== null && projectFolder !== undefined && projectFolder !== "") {
|
|
92
|
+
return projectFolder;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
export const ConversationGroup = memo(function ({
|
|
81
99
|
group,
|
|
82
100
|
viewMode = "simple",
|
|
@@ -110,6 +128,8 @@ export const ConversationGroup = memo(function ({
|
|
|
110
128
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
111
129
|
const isLoading = group.logs.some((log) => log.responseStatus === null);
|
|
112
130
|
const showTraceRollupMetrics = standalone && !hasPinnedSessionContext;
|
|
131
|
+
const clientPid = useMemo(() => getFirstClientPid(group.logs), [group.logs]);
|
|
132
|
+
const clientProjectFolder = useMemo(() => getFirstClientProjectFolder(group.logs), [group.logs]);
|
|
113
133
|
|
|
114
134
|
// Pre-compute stop reasons for each log — used by turnIndices
|
|
115
135
|
const turnGroups = useMemo(() => buildTurnGroups(group.logs), [group.logs]);
|
|
@@ -144,8 +164,8 @@ export const ConversationGroup = memo(function ({
|
|
|
144
164
|
providers={providers}
|
|
145
165
|
isLoading={isLoading}
|
|
146
166
|
userAgent={group.logs[0]?.userAgent ?? null}
|
|
147
|
-
clientPid={
|
|
148
|
-
clientProjectFolder={
|
|
167
|
+
clientPid={clientPid}
|
|
168
|
+
clientProjectFolder={clientProjectFolder}
|
|
149
169
|
timeDisplayFormat={timeDisplayFormat}
|
|
150
170
|
onClear={() => onClearGroup(group.logs.map((l) => l.id))}
|
|
151
171
|
/>
|
|
@@ -162,6 +162,13 @@ export const LogEntryHeader = memo(function ({
|
|
|
162
162
|
const tokenRateLabel =
|
|
163
163
|
tokensPerSecond === null || tokensPerSecond <= 0 ? null : formatTokenRate(tokensPerSecond);
|
|
164
164
|
const warningCount = log.warnings?.length ?? 0;
|
|
165
|
+
const hasClientPid = log.clientPid !== null && log.clientPid !== undefined;
|
|
166
|
+
const hasClientProjectFolder =
|
|
167
|
+
log.clientProjectFolder !== null &&
|
|
168
|
+
log.clientProjectFolder !== undefined &&
|
|
169
|
+
log.clientProjectFolder !== "";
|
|
170
|
+
const hasClientCwd =
|
|
171
|
+
log.clientCwd !== null && log.clientCwd !== undefined && log.clientCwd !== "";
|
|
165
172
|
|
|
166
173
|
return (
|
|
167
174
|
<TooltipProvider>
|
|
@@ -366,12 +373,12 @@ export const LogEntryHeader = memo(function ({
|
|
|
366
373
|
)}
|
|
367
374
|
|
|
368
375
|
{/* Client info (PID + project folder) */}
|
|
369
|
-
{(
|
|
376
|
+
{(hasClientPid || hasClientProjectFolder) && (
|
|
370
377
|
<Tooltip>
|
|
371
378
|
<TooltipTrigger asChild>
|
|
372
379
|
<span className="hidden xl:flex items-center gap-1 text-purple-400/80 text-xs shrink-0">
|
|
373
380
|
<FileTerminal className="size-3" />
|
|
374
|
-
{
|
|
381
|
+
{hasClientProjectFolder ? (
|
|
375
382
|
<span className="font-mono tabular-nums">{log.clientProjectFolder}</span>
|
|
376
383
|
) : (
|
|
377
384
|
<span className="font-mono tabular-nums">PID {log.clientPid}</span>
|
|
@@ -379,7 +386,7 @@ export const LogEntryHeader = memo(function ({
|
|
|
379
386
|
</span>
|
|
380
387
|
</TooltipTrigger>
|
|
381
388
|
<TooltipContent>
|
|
382
|
-
{
|
|
389
|
+
{hasClientCwd
|
|
383
390
|
? `PID: ${log.clientPid ?? "?"} CWD: ${log.clientCwd}`
|
|
384
391
|
: `Process ID: ${log.clientPid ?? "?"}`}
|
|
385
392
|
</TooltipContent>
|
|
@@ -10,7 +10,6 @@ const IDENTITY_HEADERS = {
|
|
|
10
10
|
} as const;
|
|
11
11
|
|
|
12
12
|
const REQUEST_HOP_BY_HOP = new Set([
|
|
13
|
-
"host",
|
|
14
13
|
"connection",
|
|
15
14
|
"keep-alive",
|
|
16
15
|
"proxy-authenticate",
|
|
@@ -34,11 +33,11 @@ const RESPONSE_HOP_BY_HOP = new Set([
|
|
|
34
33
|
]);
|
|
35
34
|
|
|
36
35
|
export type IdentityProxyOptions = {
|
|
37
|
-
/**
|
|
36
|
+
/** Public port to listen on. Clients point at this. */
|
|
38
37
|
listenPort: number;
|
|
39
38
|
/** Host the upstream TanStack Start server listens on. */
|
|
40
39
|
upstreamHost: string;
|
|
41
|
-
/** Port the upstream TanStack Start server listens on
|
|
40
|
+
/** Port the upstream TanStack Start server listens on. */
|
|
42
41
|
upstreamPort: number;
|
|
43
42
|
};
|
|
44
43
|
|
|
@@ -140,13 +139,6 @@ async function handleRequest(
|
|
|
140
139
|
}
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
process.stderr.write(
|
|
144
|
-
`[identity-proxy] req ${req.method} ${req.url} remotePort=${remotePort} pid=${identity?.pid ?? "?"}\n`,
|
|
145
|
-
);
|
|
146
|
-
process.stderr.write(
|
|
147
|
-
`[identity-proxy] forwarding headers: pid=${upstreamHeaders[IDENTITY_HEADERS.pid] ?? "?"} cwd=${upstreamHeaders[IDENTITY_HEADERS.cwd] ?? "?"}\n`,
|
|
148
|
-
);
|
|
149
|
-
|
|
150
142
|
const proxyReq = http.request(
|
|
151
143
|
{
|
|
152
144
|
hostname: options.upstreamHost,
|
|
@@ -17,6 +17,11 @@ type CacheEntry = ClientInfo & {
|
|
|
17
17
|
expiresAt: number;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
type NetstatClient = {
|
|
21
|
+
pid: number;
|
|
22
|
+
localPort: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
20
25
|
// Cache entries: port -> { pid, cwd, projectFolder, expiresAt }
|
|
21
26
|
const cache = new Map<number, CacheEntry>();
|
|
22
27
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
@@ -316,33 +321,23 @@ async function lookupRecentClient(): Promise<ClientInfo> {
|
|
|
316
321
|
const platform = process.platform;
|
|
317
322
|
try {
|
|
318
323
|
if (platform === "win32") {
|
|
319
|
-
// Get-NetTCPConnection
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
"powershell.exe",
|
|
331
|
-
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psScript],
|
|
332
|
-
{ windowsHide: true, timeout: 3000, maxBuffer: 16 * 1024 },
|
|
333
|
-
);
|
|
334
|
-
const trimmed = stdout.trim();
|
|
335
|
-
if (trimmed === "") return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
336
|
-
const [pidStr, portStr] = trimmed.split("|");
|
|
337
|
-
const pid = parseInt(pidStr ?? "", 10);
|
|
338
|
-
const clientPort = parseInt(portStr ?? "", 10);
|
|
339
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
324
|
+
// `Get-NetTCPConnection` can take several seconds on busy Windows
|
|
325
|
+
// machines, which makes direct-port clients (notably MiMo Code) lose
|
|
326
|
+
// their PID before the fallback completes. `netstat` streams quickly
|
|
327
|
+
// and includes the owning process id for established loopback rows.
|
|
328
|
+
const { stdout } = await execFileAsync("netstat.exe", ["-ano", "-p", "TCP"], {
|
|
329
|
+
windowsHide: true,
|
|
330
|
+
timeout: 2000,
|
|
331
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
332
|
+
});
|
|
333
|
+
const client = pickNewestClientFromWindowsNetstat(stdout.split("\n"), port);
|
|
334
|
+
if (client === null) {
|
|
340
335
|
return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
341
336
|
}
|
|
342
|
-
const { cwd, projectFolder } = await lookupProcessInfo(pid);
|
|
337
|
+
const { cwd, projectFolder } = await lookupProcessInfo(client.pid);
|
|
343
338
|
return {
|
|
344
|
-
port:
|
|
345
|
-
pid,
|
|
339
|
+
port: client.localPort,
|
|
340
|
+
pid: client.pid,
|
|
346
341
|
cwd,
|
|
347
342
|
projectFolder,
|
|
348
343
|
};
|
|
@@ -403,6 +398,56 @@ function pickNewestClientPidFromNetstat(lines: string[], proxyPort: number): num
|
|
|
403
398
|
return candidates[0]?.pid ?? null;
|
|
404
399
|
}
|
|
405
400
|
|
|
401
|
+
function parseAddressPort(address: string): number | null {
|
|
402
|
+
const match = address.match(/:(\d+)$/);
|
|
403
|
+
if (match === null) return null;
|
|
404
|
+
const parsed = parseInt(match[1] ?? "", 10);
|
|
405
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65_535) return null;
|
|
406
|
+
return parsed;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function pickNewestClientFromWindowsNetstat(
|
|
410
|
+
lines: string[],
|
|
411
|
+
proxyPort: number,
|
|
412
|
+
): NetstatClient | null {
|
|
413
|
+
const proxyToken = `:${proxyPort}`;
|
|
414
|
+
type Row = NetstatClient & { order: number };
|
|
415
|
+
const candidates: Row[] = [];
|
|
416
|
+
let order = 0;
|
|
417
|
+
|
|
418
|
+
for (const line of lines) {
|
|
419
|
+
order++;
|
|
420
|
+
const cols = line.trim().split(/\s+/);
|
|
421
|
+
if (cols.length < 5) continue;
|
|
422
|
+
const localAddr = cols[1] ?? "";
|
|
423
|
+
const remoteAddr = cols[2] ?? "";
|
|
424
|
+
const state = cols[3] ?? "";
|
|
425
|
+
const pidStr = cols[4] ?? "";
|
|
426
|
+
if (state !== "ESTABLISHED") continue;
|
|
427
|
+
if (!remoteAddr.endsWith(proxyToken)) continue;
|
|
428
|
+
|
|
429
|
+
const localPort = parseAddressPort(localAddr);
|
|
430
|
+
if (localPort === null || localPort === proxyPort) continue;
|
|
431
|
+
|
|
432
|
+
const pid = parseInt(pidStr, 10);
|
|
433
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
434
|
+
candidates.push({ pid, localPort, order });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (candidates.length === 0) return null;
|
|
438
|
+
candidates.sort((a, b) => b.localPort - a.localPort || b.order - a.order);
|
|
439
|
+
const selected = candidates[0];
|
|
440
|
+
if (selected === undefined) return null;
|
|
441
|
+
return { pid: selected.pid, localPort: selected.localPort };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function _pickNewestClientFromWindowsNetstatForTests(
|
|
445
|
+
lines: string[],
|
|
446
|
+
proxyPort: number,
|
|
447
|
+
): NetstatClient | null {
|
|
448
|
+
return pickNewestClientFromWindowsNetstat(lines, proxyPort);
|
|
449
|
+
}
|
|
450
|
+
|
|
406
451
|
/** Custom request headers the client (or a wrapper script) may set so we
|
|
407
452
|
* can identify the originating process without scanning the OS connection
|
|
408
453
|
* table. Recognized names:
|
|
@@ -473,6 +518,37 @@ export async function getClientInfo(request: Request): Promise<ClientInfo> {
|
|
|
473
518
|
const fromHeaders = readClientHeaders(request);
|
|
474
519
|
if (fromHeaders !== null) return fromHeaders;
|
|
475
520
|
|
|
521
|
+
const remotePort = extractRemotePort(request);
|
|
522
|
+
if (remotePort !== null) {
|
|
523
|
+
const cachedPort = getFromCache(remotePort);
|
|
524
|
+
if (cachedPort !== null) return cachedPort;
|
|
525
|
+
|
|
526
|
+
const existingPortLookup = inflight.get(remotePort);
|
|
527
|
+
if (existingPortLookup !== undefined) return existingPortLookup;
|
|
528
|
+
|
|
529
|
+
const portLookup = lookupClientByPort(remotePort)
|
|
530
|
+
.then((info) => {
|
|
531
|
+
setCache(remotePort, info);
|
|
532
|
+
inflight.delete(remotePort);
|
|
533
|
+
return info;
|
|
534
|
+
})
|
|
535
|
+
.catch((err) => {
|
|
536
|
+
inflight.delete(remotePort);
|
|
537
|
+
logger.debug(`[socketTracker] Remote-port lookup failed for ${remotePort}:`, String(err));
|
|
538
|
+
const fallback: ClientInfo = {
|
|
539
|
+
port: remotePort,
|
|
540
|
+
pid: null,
|
|
541
|
+
cwd: null,
|
|
542
|
+
projectFolder: null,
|
|
543
|
+
};
|
|
544
|
+
setCache(remotePort, fallback);
|
|
545
|
+
return fallback;
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
inflight.set(remotePort, portLookup);
|
|
549
|
+
return portLookup;
|
|
550
|
+
}
|
|
551
|
+
|
|
476
552
|
// Fallback: scan the OS connection table for the most recently opened
|
|
477
553
|
// client→proxy connection. Best-effort — in environments with many
|
|
478
554
|
// long-lived keep-alive connections (browsers, background processes) the
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{R as s,j as e}from"./main-C-lua4Uq.js";import{P as i}from"./ProxyViewerContainer-DFFxBlvO.js";function t(){const{sessionId:o}=s.useParams();return e.jsx(i,{initialSessionId:o},o)}export{t as component};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{P as o}from"./ProxyViewerContainer-DFFxBlvO.js";import"./main-C-lua4Uq.js";const r=o;export{r as component};
|