@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.
Files changed (31) hide show
  1. package/.output/cli.js +10 -15
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-CcwXsZp3.js → CompareDrawer-Cz_1vIpR.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-DFFxBlvO.js → ProxyViewerContainer-7QSiluMf.js} +19 -19
  5. package/.output/public/assets/{ReplayDialog-XXiwmD5o.js → ReplayDialog-sBA1KAYD.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-DTCh37so.js → RequestAnatomy-BjlBT-Cy.js} +1 -1
  7. package/.output/public/assets/{ResponseView-CbFYw9xP.js → ResponseView-PtEKzml9.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-CTLMzUxV.js → StreamingChunkSequence-D2_SMhlE.js} +1 -1
  9. package/.output/public/assets/_sessionId-DZfB4ruK.js +1 -0
  10. package/.output/public/assets/index-B-QQLbpz.js +1 -0
  11. package/.output/public/assets/{main-C-lua4Uq.js → main-Dtspb4Ui.js} +2 -2
  12. package/.output/server/{_sessionId-CO0WzOBc.mjs → _sessionId-CXDcLuvi.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-_OnPOi0O.mjs → CompareDrawer-CMoCAoeq.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-Bfy-cjD3.mjs → ProxyViewerContainer-BW2vVCBN.mjs} +32 -11
  15. package/.output/server/_ssr/{ReplayDialog-DBZyQFFR.mjs → ReplayDialog-ChXL1t8H.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-C7m6HJOG.mjs → RequestAnatomy-DtKzIlfU.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-CM2nFoqa.mjs → ResponseView-B5I8drzc.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-Dj61jLHa.mjs → StreamingChunkSequence-DvwjQNcO.mjs} +2 -2
  19. package/.output/server/_ssr/{index-BI_nCxdk.mjs → index-D_ZHtRfl.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-WBmANYRw.mjs → router-DhL9Wp3N.mjs} +213 -28
  22. package/.output/server/{_tanstack-start-manifest_v-lODJPovZ.mjs → _tanstack-start-manifest_v-BHgoAmxZ.mjs} +1 -1
  23. package/.output/server/index.mjs +58 -58
  24. package/package.json +1 -1
  25. package/src/cli.ts +24 -8
  26. package/src/components/proxy-viewer/ConversationGroup.tsx +22 -2
  27. package/src/components/proxy-viewer/LogEntryHeader.tsx +10 -3
  28. package/src/proxy/identityProxy.ts +2 -10
  29. package/src/proxy/socketTracker.ts +100 -24
  30. package/.output/public/assets/_sessionId-DAwcZWfu.js +0 -1
  31. 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 DEFAULT_IDENTITY_PORT = 25949;
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["AGENT_INSPECTOR_IDENTITY_PORT"] !== undefined
251
- ? Number(process.env["AGENT_INSPECTOR_IDENTITY_PORT"])
252
- : DEFAULT_IDENTITY_PORT;
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
- process.env["PORT"] = String(port);
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) killProcessOnPort(identityProxyPort);
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, port, host)
570
+ ? await tryStartIdentityProxy(identityProxyPort, upstreamPort, host)
557
571
  : null;
558
572
 
559
573
  if (background) {
560
574
  serverProcess.unref();
561
- if (await waitForInspectorHealthy(port, 5000, host)) {
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={group.logs[0]?.clientPid ?? null}
148
- clientProjectFolder={group.logs[0]?.clientProjectFolder ?? null}
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
- {(log.clientPid !== null || log.clientProjectFolder !== null) && (
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
- {log.clientProjectFolder !== null ? (
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
- {log.clientCwd !== null
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
- /** Port to listen on (default 25949). Clients point at this. */
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 (default 25947). */
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 gives us CreationTime on the client-side
320
- // (-RemotePort) entries, which we sort descending to pick the freshest.
321
- const psScript = [
322
- `$rows = Get-NetTCPConnection -RemotePort ${port} -State Established -ErrorAction SilentlyContinue`,
323
- `if ($rows) {`,
324
- ` $rows = $rows | Sort-Object CreationTime -Descending`,
325
- ` $top = $rows | Select-Object -First 1`,
326
- ` if ($top) { Write-Output ("$($top.OwningProcess)|$($top.LocalPort)") }`,
327
- `}`,
328
- ].join("; ");
329
- const { stdout } = await execFileAsync(
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: Number.isInteger(clientPort) && clientPort > 0 ? clientPort : null,
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};