@tonyclaw/agent-inspector 2.1.10 → 2.1.12

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 +14676 -399
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BaGKK2R7.js → CompareDrawer-XTdSOto9.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-Cxwizw8H.js → ProxyViewerContainer-Cc944N8i.js} +19 -19
  5. package/.output/public/assets/{ReplayDialog-Io8Ufsif.js → ReplayDialog-Cd1VsfRe.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-CiZE_fKZ.js → RequestAnatomy-QxSpbmsj.js} +1 -1
  7. package/.output/public/assets/{ResponseView-TJwxn-lh.js → ResponseView-8GivXCzp.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-D-9ysFSG.js → StreamingChunkSequence-CwNGqJ5n.js} +1 -1
  9. package/.output/public/assets/_sessionId-D89uDTQs.js +1 -0
  10. package/.output/public/assets/index-BsFdgXac.js +1 -0
  11. package/.output/public/assets/{main-JvXpk_ov.js → main-B_EcGyf-.js} +2 -2
  12. package/.output/server/{_sessionId-DSZ76cjr.mjs → _sessionId-CmO9read.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-BcUJ7lNL.mjs → CompareDrawer-D9wf870o.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-DEtSh7OB.mjs → ProxyViewerContainer-BDLJSawP.mjs} +49 -30
  15. package/.output/server/_ssr/{ReplayDialog-DwSynSXP.mjs → ReplayDialog-B2dS4kmP.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-C6LGadAw.mjs → RequestAnatomy-k49Peshi.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-Dr27DXbx.mjs → ResponseView-Bk3B1suu.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-CDTaUWjj.mjs → StreamingChunkSequence-D6MDGCgh.mjs} +2 -2
  19. package/.output/server/_ssr/{index-D803rHfz.mjs → index-CC8KdlTb.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-D7dDibPa.mjs → router-BEmMnVa7.mjs} +213 -28
  22. package/.output/server/{_tanstack-start-manifest_v-P9lKyaOf.mjs → _tanstack-start-manifest_v-Bf_4hG4w.mjs} +1 -1
  23. package/.output/server/index.mjs +55 -55
  24. package/package.json +1 -1
  25. package/src/cli.ts +107 -2
  26. package/src/components/proxy-viewer/ConversationGroup.tsx +22 -2
  27. package/src/components/proxy-viewer/LogEntryHeader.tsx +35 -29
  28. package/src/proxy/identityProxy.ts +192 -0
  29. package/src/proxy/socketTracker.ts +222 -24
  30. package/.output/public/assets/_sessionId-3JCekYV3.js +0 -1
  31. package/.output/public/assets/index-B3EOwfg7.js +0 -1
@@ -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
@@ -498,3 +574,125 @@ export async function getClientInfo(request: Request): Promise<ClientInfo> {
498
574
  recentInflight = promise;
499
575
  return promise;
500
576
  }
577
+
578
+ /**
579
+ * Lookup the PID owning a specific client-side local port, then enrich it
580
+ * with the process's cwd / project folder. Used by the identity-injecting
581
+ * native HTTP proxy in `identityProxy.ts`, which has access to
582
+ * `req.socket.remotePort` (the client's ephemeral port) and needs to know
583
+ * which process opened the connection.
584
+ *
585
+ * Results are cached by `clientPort` for `PORT_LOOKUP_TTL_MS` — a single
586
+ * keep-alive connection typically reuses the same client port for many
587
+ * requests, so we don't want to spawn a subprocess per request.
588
+ */
589
+ const PORT_LOOKUP_TTL_MS = 5 * 60 * 1000;
590
+ const portInfoCache = new Map<number, { info: ClientInfo; expiresAt: number }>();
591
+
592
+ export async function lookupClientByPort(clientPort: number): Promise<ClientInfo> {
593
+ if (!Number.isInteger(clientPort) || clientPort <= 0 || clientPort > 65_535) {
594
+ return { port: null, pid: null, cwd: null, projectFolder: null };
595
+ }
596
+ const cached = portInfoCache.get(clientPort);
597
+ if (cached !== undefined && Date.now() < cached.expiresAt) {
598
+ return cached.info;
599
+ }
600
+ try {
601
+ const pid = await lookupPidByLocalPort(clientPort);
602
+ if (pid === null) {
603
+ const fallback: ClientInfo = {
604
+ port: clientPort,
605
+ pid: null,
606
+ cwd: null,
607
+ projectFolder: null,
608
+ };
609
+ portInfoCache.set(clientPort, { info: fallback, expiresAt: Date.now() + PORT_LOOKUP_TTL_MS });
610
+ return fallback;
611
+ }
612
+ const { cwd, projectFolder } = await lookupProcessInfo(pid);
613
+ const info: ClientInfo = { port: clientPort, pid, cwd, projectFolder };
614
+ portInfoCache.set(clientPort, { info, expiresAt: Date.now() + PORT_LOOKUP_TTL_MS });
615
+ return info;
616
+ } catch (err) {
617
+ logger.debug(`[socketTracker] lookupClientByPort(${clientPort}) failed:`, String(err));
618
+ return { port: clientPort, pid: null, cwd: null, projectFolder: null };
619
+ }
620
+ }
621
+
622
+ /** Returns the PID owning `clientPort` on the local machine, or `null`
623
+ * if the port isn't held by any known process. Cross-platform wrapper
624
+ * around the OS-specific connection table. */
625
+ async function lookupPidByLocalPort(clientPort: number): Promise<number | null> {
626
+ const platform = process.platform;
627
+ try {
628
+ if (platform === "win32") {
629
+ // We deliberately use `netstat -ano | findstr` instead of
630
+ // `Get-NetTCPConnection -LocalPort`. The PowerShell cmdlet
631
+ // enumerates the entire socket table before filtering, which on a
632
+ // busy machine takes 10+ seconds — longer than our per-request
633
+ // timeout. `netstat -ano` streams its output and `findstr` filters
634
+ // line by line, completing in ~0.5s.
635
+ //
636
+ // `netstat -ano` lists every TCP connection twice — once for each
637
+ // endpoint. The format is:
638
+ //
639
+ // Proto LocalAddress RemoteAddress State PID
640
+ // TCP [::1]:25949 [::1]:55193 ESTABLISHED 10936 ← server-side row (proxy owns the listener)
641
+ // TCP [::1]:55193 [::1]:25949 ESTABLISHED 29356 ← client-side row (curl owns the ephemeral port)
642
+ //
643
+ // For `clientPort` (the client's ephemeral port) we want the row
644
+ // whose LOCAL column ends with `:<clientPort>` — that's the row
645
+ // attributed to the client process.
646
+ const { stdout } = await execAsync(
647
+ `netstat -ano | findstr :${clientPort} | findstr ESTABLISHED`,
648
+ { shell: "cmd.exe", timeout: 2000, windowsHide: true },
649
+ );
650
+ const clientToken = `:${clientPort}`;
651
+ for (const rawLine of stdout.split("\n")) {
652
+ const line = rawLine.trim();
653
+ if (line === "") continue;
654
+ const cols = line.split(/\s+/);
655
+ if (cols.length < 5) continue;
656
+ const localAddr = cols[1] ?? "";
657
+ if (!localAddr.endsWith(clientToken)) continue;
658
+ const pidStr = cols[cols.length - 1] ?? "";
659
+ const parsed = parseInt(pidStr, 10);
660
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
661
+ }
662
+ return null;
663
+ }
664
+ // Unix: try `ss` first, fall back to `lsof`. Both need root or
665
+ // CAP_NET_ADMIN to see PIDs of processes owned by other users; when
666
+ // permission is denied the command exits non-zero and we return null.
667
+ try {
668
+ const { stdout } = await execAsync(
669
+ `ss -tanp state established 'sport = :${clientPort}' 2>/dev/null | tail -n +2 | head -n 1`,
670
+ { shell: "/bin/sh" },
671
+ );
672
+ const match = stdout.match(/pid=(\d+)/);
673
+ if (match !== null) {
674
+ const parsed = parseInt(match[1] ?? "", 10);
675
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
676
+ }
677
+ } catch {
678
+ // fall through
679
+ }
680
+ try {
681
+ const { stdout } = await execAsync(
682
+ `lsof -ti tcp:${clientPort} -sTCP:ESTABLISHED 2>/dev/null`,
683
+ {
684
+ shell: "/bin/sh",
685
+ },
686
+ );
687
+ const trimmed = stdout.trim().split("\n")[0] ?? "";
688
+ const parsed = parseInt(trimmed, 10);
689
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
690
+ } catch {
691
+ // fall through
692
+ }
693
+ return null;
694
+ } catch (err) {
695
+ logger.debug(`[socketTracker] lookupPidByLocalPort(${clientPort}) failed:`, String(err));
696
+ return null;
697
+ }
698
+ }
@@ -1 +0,0 @@
1
- import{R as s,j as e}from"./main-JvXpk_ov.js";import{P as i}from"./ProxyViewerContainer-Cxwizw8H.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-Cxwizw8H.js";import"./main-JvXpk_ov.js";const r=o;export{r as component};