@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.
- package/.output/cli.js +14676 -399
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BaGKK2R7.js → CompareDrawer-XTdSOto9.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-Cxwizw8H.js → ProxyViewerContainer-Cc944N8i.js} +19 -19
- package/.output/public/assets/{ReplayDialog-Io8Ufsif.js → ReplayDialog-Cd1VsfRe.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CiZE_fKZ.js → RequestAnatomy-QxSpbmsj.js} +1 -1
- package/.output/public/assets/{ResponseView-TJwxn-lh.js → ResponseView-8GivXCzp.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-D-9ysFSG.js → StreamingChunkSequence-CwNGqJ5n.js} +1 -1
- package/.output/public/assets/_sessionId-D89uDTQs.js +1 -0
- package/.output/public/assets/index-BsFdgXac.js +1 -0
- package/.output/public/assets/{main-JvXpk_ov.js → main-B_EcGyf-.js} +2 -2
- package/.output/server/{_sessionId-DSZ76cjr.mjs → _sessionId-CmO9read.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BcUJ7lNL.mjs → CompareDrawer-D9wf870o.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-DEtSh7OB.mjs → ProxyViewerContainer-BDLJSawP.mjs} +49 -30
- package/.output/server/_ssr/{ReplayDialog-DwSynSXP.mjs → ReplayDialog-B2dS4kmP.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-C6LGadAw.mjs → RequestAnatomy-k49Peshi.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-Dr27DXbx.mjs → ResponseView-Bk3B1suu.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-CDTaUWjj.mjs → StreamingChunkSequence-D6MDGCgh.mjs} +2 -2
- package/.output/server/_ssr/{index-D803rHfz.mjs → index-CC8KdlTb.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-D7dDibPa.mjs → router-BEmMnVa7.mjs} +213 -28
- package/.output/server/{_tanstack-start-manifest_v-P9lKyaOf.mjs → _tanstack-start-manifest_v-Bf_4hG4w.mjs} +1 -1
- package/.output/server/index.mjs +55 -55
- package/package.json +1 -1
- package/src/cli.ts +107 -2
- package/src/components/proxy-viewer/ConversationGroup.tsx +22 -2
- package/src/components/proxy-viewer/LogEntryHeader.tsx +35 -29
- package/src/proxy/identityProxy.ts +192 -0
- package/src/proxy/socketTracker.ts +222 -24
- package/.output/public/assets/_sessionId-3JCekYV3.js +0 -1
- 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
|
|
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
|
|
@@ -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};
|