@tonyclaw/agent-inspector 2.0.27 → 2.0.29
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 +169 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-B_p6vmMQ.js → CompareDrawer-DcqxlgSQ.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-D650kQlv.js +115 -0
- package/.output/public/assets/{ReplayDialog-BmprJ6D3.js → ReplayDialog-CBcnPgx1.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DqO0_SVJ.js → RequestAnatomy-DSgSqCYt.js} +1 -1
- package/.output/public/assets/{ResponseView-FZbPNdHa.js → ResponseView-CjqzBSoF.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-BZPzfJWj.js → StreamingChunkSequence-DdBBKolI.js} +1 -1
- package/.output/public/assets/_sessionId-wctOoGh_.js +1 -0
- package/.output/public/assets/index-BhQGSdhG.js +1 -0
- package/.output/public/assets/{index-DsiKfWCp.css → index-CwlHPmgL.css} +1 -1
- package/.output/public/assets/{main-CzItFZlM.js → main-B6OLZCp9.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +13 -13
- package/.output/server/{_sessionId-DZH8SrEZ.mjs → _sessionId-CCfKJJ19.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-L0aE1UV1.mjs → CompareDrawer-uaPI5R6e.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-B62RB9ER.mjs → ProxyViewerContainer-BV2nIxXy.mjs} +83 -20
- package/.output/server/_ssr/{ReplayDialog-FVWnpx2C.mjs → ReplayDialog-eE2Hy2Nl.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-DIyqW-Ny.mjs → RequestAnatomy-B7vwWo57.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-BX4mxEZ5.mjs → ResponseView-Bm6Qp8mo.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-Cs3nzOor.mjs → StreamingChunkSequence-CbAJKJQx.mjs} +2 -2
- package/.output/server/_ssr/{index-Dik2Mc3h.mjs → index-u14_Aj60.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DCPg8ykx.mjs → router-qgeGUp1k.mjs} +161 -18
- package/.output/server/{_tanstack-start-manifest_v-BaoL3JCh.mjs → _tanstack-start-manifest_v-BrfUXnre.mjs} +1 -1
- package/.output/server/index.mjs +59 -59
- package/package.json +1 -1
- package/src/cli/networkHints.ts +150 -0
- package/src/cli.ts +72 -13
- package/src/components/ProxyViewer.tsx +3 -0
- package/src/components/providers/ProviderCard.tsx +32 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +38 -1
- package/src/lib/providerTestContract.ts +3 -0
- package/src/lib/sessionInfoContract.ts +3 -0
- package/src/mcp/toolHandlers.ts +3 -0
- package/src/proxy/evidenceAnalysis.ts +5 -0
- package/src/proxy/evidenceExporter.ts +8 -3
- package/src/proxy/handler.ts +8 -1
- package/src/proxy/logFinalizer.ts +36 -0
- package/src/proxy/schemas.ts +3 -0
- package/src/proxy/sessionInfo.ts +3 -0
- package/src/proxy/store.ts +3 -0
- package/src/proxy/streamTiming.ts +31 -0
- package/src/routes/api/logs.$id.replay.ts +25 -2
- package/src/routes/api/providers.$providerId.test.log.ts +34 -2
- package/.output/public/assets/ProxyViewerContainer-DItqh1EO.js +0 -115
- package/.output/public/assets/_sessionId-y2ATIp51.js +0 -1
- package/.output/public/assets/index-CQT4higx.js +0 -1
package/.output/server/index.mjs
CHANGED
|
@@ -35,103 +35,103 @@ const headers = ((m) => function headersRouteRule(event) {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
const assets = {
|
|
38
|
-
"/assets/index-CQT4higx.js": {
|
|
39
|
-
"type": "text/javascript; charset=utf-8",
|
|
40
|
-
"etag": '"74-gWIAXMUZANjNHlWTIO5NeVHxrXg"',
|
|
41
|
-
"mtime": "2026-06-30T01:22:22.129Z",
|
|
42
|
-
"size": 116,
|
|
43
|
-
"path": "../public/assets/index-CQT4higx.js"
|
|
44
|
-
},
|
|
45
|
-
"/assets/CompareDrawer-B_p6vmMQ.js": {
|
|
46
|
-
"type": "text/javascript; charset=utf-8",
|
|
47
|
-
"etag": '"49fe-MtnQEnhved8xLSD42VJ6I1PWK+E"',
|
|
48
|
-
"mtime": "2026-06-30T01:22:22.129Z",
|
|
49
|
-
"size": 18942,
|
|
50
|
-
"path": "../public/assets/CompareDrawer-B_p6vmMQ.js"
|
|
51
|
-
},
|
|
52
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
53
39
|
"type": "image/svg+xml",
|
|
54
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
55
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
56
42
|
"size": 5915,
|
|
57
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
58
44
|
},
|
|
59
|
-
"/assets/index-
|
|
60
|
-
"type": "text/
|
|
61
|
-
"etag": '"
|
|
62
|
-
"mtime": "2026-06-
|
|
63
|
-
"size":
|
|
64
|
-
"path": "../public/assets/index-
|
|
45
|
+
"/assets/index-BhQGSdhG.js": {
|
|
46
|
+
"type": "text/javascript; charset=utf-8",
|
|
47
|
+
"etag": '"74-1e7gU3IOVaEjblxPQfgH61GRjlw"',
|
|
48
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
49
|
+
"size": 116,
|
|
50
|
+
"path": "../public/assets/index-BhQGSdhG.js"
|
|
51
|
+
},
|
|
52
|
+
"/assets/CompareDrawer-DcqxlgSQ.js": {
|
|
53
|
+
"type": "text/javascript; charset=utf-8",
|
|
54
|
+
"etag": '"49fe-+D2I+/9sd7EYksKARn3HVY2eJf0"',
|
|
55
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
56
|
+
"size": 18942,
|
|
57
|
+
"path": "../public/assets/CompareDrawer-DcqxlgSQ.js"
|
|
65
58
|
},
|
|
66
59
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
67
60
|
"type": "image/jpeg",
|
|
68
61
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
69
|
-
"mtime": "2026-06-
|
|
62
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
70
63
|
"size": 6918,
|
|
71
64
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
72
65
|
},
|
|
73
|
-
"/assets/
|
|
66
|
+
"/assets/index-CwlHPmgL.css": {
|
|
67
|
+
"type": "text/css; charset=utf-8",
|
|
68
|
+
"etag": '"1a3f0-E2f0kOdSBi1RhjoK38+CSij/apg"',
|
|
69
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
70
|
+
"size": 107504,
|
|
71
|
+
"path": "../public/assets/index-CwlHPmgL.css"
|
|
72
|
+
},
|
|
73
|
+
"/assets/ReplayDialog-CBcnPgx1.js": {
|
|
74
74
|
"type": "text/javascript; charset=utf-8",
|
|
75
|
-
"etag": '"318c-
|
|
76
|
-
"mtime": "2026-06-
|
|
75
|
+
"etag": '"318c-Wq35vSCO67O3pnw6riReoXT9gOg"',
|
|
76
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
77
77
|
"size": 12684,
|
|
78
|
-
"path": "../public/assets/ReplayDialog-
|
|
78
|
+
"path": "../public/assets/ReplayDialog-CBcnPgx1.js"
|
|
79
79
|
},
|
|
80
|
-
"/assets/RequestAnatomy-
|
|
80
|
+
"/assets/RequestAnatomy-DSgSqCYt.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"4485-
|
|
83
|
-
"mtime": "2026-06-
|
|
82
|
+
"etag": '"4485-rBai6wnmYHf662t3omQFALcwHSw"',
|
|
83
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
84
84
|
"size": 17541,
|
|
85
|
-
"path": "../public/assets/RequestAnatomy-
|
|
85
|
+
"path": "../public/assets/RequestAnatomy-DSgSqCYt.js"
|
|
86
86
|
},
|
|
87
|
-
"/assets/ResponseView-
|
|
87
|
+
"/assets/ResponseView-CjqzBSoF.js": {
|
|
88
88
|
"type": "text/javascript; charset=utf-8",
|
|
89
|
-
"etag": '"30d0-
|
|
90
|
-
"mtime": "2026-06-
|
|
89
|
+
"etag": '"30d0-13nPwe9jIZSSl43vTEhXlswXwn8"',
|
|
90
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
91
91
|
"size": 12496,
|
|
92
|
-
"path": "../public/assets/ResponseView-
|
|
92
|
+
"path": "../public/assets/ResponseView-CjqzBSoF.js"
|
|
93
93
|
},
|
|
94
|
-
"/assets/StreamingChunkSequence-
|
|
94
|
+
"/assets/StreamingChunkSequence-DdBBKolI.js": {
|
|
95
95
|
"type": "text/javascript; charset=utf-8",
|
|
96
|
-
"etag": '"d57-
|
|
97
|
-
"mtime": "2026-06-
|
|
96
|
+
"etag": '"d57-9H1QMwesXGWHkYNTPJ/9zDoslX0"',
|
|
97
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
98
98
|
"size": 3415,
|
|
99
|
-
"path": "../public/assets/StreamingChunkSequence-
|
|
100
|
-
},
|
|
101
|
-
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
102
|
-
"type": "image/svg+xml",
|
|
103
|
-
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
104
|
-
"mtime": "2026-06-30T01:22:22.129Z",
|
|
105
|
-
"size": 11256,
|
|
106
|
-
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
99
|
+
"path": "../public/assets/StreamingChunkSequence-DdBBKolI.js"
|
|
107
100
|
},
|
|
108
|
-
"/assets/_sessionId-
|
|
101
|
+
"/assets/_sessionId-wctOoGh_.js": {
|
|
109
102
|
"type": "text/javascript; charset=utf-8",
|
|
110
|
-
"etag": '"d2-
|
|
111
|
-
"mtime": "2026-06-
|
|
103
|
+
"etag": '"d2-fiIvvYyX9jF4fd1W7HpowwQ2Zj4"',
|
|
104
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
112
105
|
"size": 210,
|
|
113
|
-
"path": "../public/assets/_sessionId-
|
|
106
|
+
"path": "../public/assets/_sessionId-wctOoGh_.js"
|
|
114
107
|
},
|
|
115
|
-
"/assets/main-
|
|
108
|
+
"/assets/main-B6OLZCp9.js": {
|
|
116
109
|
"type": "text/javascript; charset=utf-8",
|
|
117
|
-
"etag": '"51726-
|
|
118
|
-
"mtime": "2026-06-
|
|
110
|
+
"etag": '"51726-MaBaigWv6OLiiYg6fusFyN758Hw"',
|
|
111
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
119
112
|
"size": 333606,
|
|
120
|
-
"path": "../public/assets/main-
|
|
113
|
+
"path": "../public/assets/main-B6OLZCp9.js"
|
|
114
|
+
},
|
|
115
|
+
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
116
|
+
"type": "image/svg+xml",
|
|
117
|
+
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
118
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
119
|
+
"size": 11256,
|
|
120
|
+
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
121
121
|
},
|
|
122
122
|
"/assets/qwen-CONDcHqt.png": {
|
|
123
123
|
"type": "image/png",
|
|
124
124
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
125
|
-
"mtime": "2026-06-
|
|
125
|
+
"mtime": "2026-06-30T09:01:48.556Z",
|
|
126
126
|
"size": 357059,
|
|
127
127
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
128
128
|
},
|
|
129
|
-
"/assets/ProxyViewerContainer-
|
|
129
|
+
"/assets/ProxyViewerContainer-D650kQlv.js": {
|
|
130
130
|
"type": "text/javascript; charset=utf-8",
|
|
131
|
-
"etag": '"
|
|
132
|
-
"mtime": "2026-06-
|
|
133
|
-
"size":
|
|
134
|
-
"path": "../public/assets/ProxyViewerContainer-
|
|
131
|
+
"etag": '"af8bd-kLHfI4KjZ80Xpnq5/VoFdzyBH10"',
|
|
132
|
+
"mtime": "2026-06-30T09:01:48.557Z",
|
|
133
|
+
"size": 719037,
|
|
134
|
+
"path": "../public/assets/ProxyViewerContainer-D650kQlv.js"
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { networkInterfaces } from "node:os";
|
|
2
|
+
|
|
3
|
+
export type NetworkInterfaceMap = ReturnType<typeof networkInterfaces>;
|
|
4
|
+
|
|
5
|
+
export type BindHost = string | undefined;
|
|
6
|
+
|
|
7
|
+
export function localUrlForPort(port: number): string {
|
|
8
|
+
return `http://localhost:${String(port)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function trimHost(host: BindHost): string | undefined {
|
|
12
|
+
const trimmed = host?.trim();
|
|
13
|
+
return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isWildcardHost(host: BindHost): boolean {
|
|
17
|
+
const trimmed = trimHost(host);
|
|
18
|
+
return trimmed === undefined || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isLoopbackHost(host: BindHost): boolean {
|
|
22
|
+
const trimmed = trimHost(host);
|
|
23
|
+
if (trimmed === undefined) return false;
|
|
24
|
+
return (
|
|
25
|
+
trimmed === "localhost" ||
|
|
26
|
+
trimmed === "::1" ||
|
|
27
|
+
trimmed === "[::1]" ||
|
|
28
|
+
trimmed.startsWith("127.")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function bracketIpv6(host: string): string {
|
|
33
|
+
if (!host.includes(":")) return host;
|
|
34
|
+
if (host.startsWith("[") && host.endsWith("]")) return host;
|
|
35
|
+
return `[${host}]`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function urlForHost(port: number, host: BindHost): string {
|
|
39
|
+
const trimmed = trimHost(host);
|
|
40
|
+
if (trimmed === undefined || isWildcardHost(trimmed)) return localUrlForPort(port);
|
|
41
|
+
return `http://${bracketIpv6(trimmed)}:${String(port)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function probeHostForBindHost(host: BindHost): string {
|
|
45
|
+
const trimmed = trimHost(host);
|
|
46
|
+
if (trimmed === undefined || isWildcardHost(trimmed)) return "127.0.0.1";
|
|
47
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1);
|
|
48
|
+
return trimmed;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isValidBindHost(host: string): boolean {
|
|
52
|
+
const trimmed = host.trim();
|
|
53
|
+
if (trimmed.length === 0) return false;
|
|
54
|
+
if (/^https?:\/\//i.test(trimmed)) return false;
|
|
55
|
+
if (/[\s/\\]/.test(trimmed)) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isUsableAddress(address: string): boolean {
|
|
60
|
+
return address !== "0.0.0.0" && !address.startsWith("169.254.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function is172Private(address: string): boolean {
|
|
64
|
+
const parts = address.split(".");
|
|
65
|
+
const second = Number(parts[1] ?? "");
|
|
66
|
+
return parts[0] === "172" && Number.isInteger(second) && second >= 16 && second <= 31;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function addressScore(address: string): number {
|
|
70
|
+
if (address.startsWith("192.168.")) return 0;
|
|
71
|
+
if (address.startsWith("10.")) return 1;
|
|
72
|
+
if (is172Private(address)) return 2;
|
|
73
|
+
return 3;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function collectNetworkUrls(
|
|
77
|
+
port: number,
|
|
78
|
+
interfaces: NetworkInterfaceMap = networkInterfaces(),
|
|
79
|
+
): string[] {
|
|
80
|
+
const addresses = new Set<string>();
|
|
81
|
+
|
|
82
|
+
for (const entries of Object.values(interfaces)) {
|
|
83
|
+
if (entries === undefined) continue;
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (entry.family === "IPv4" && !entry.internal && isUsableAddress(entry.address)) {
|
|
86
|
+
addresses.add(entry.address);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...addresses]
|
|
92
|
+
.sort((left, right) => {
|
|
93
|
+
const score = addressScore(left) - addressScore(right);
|
|
94
|
+
return score !== 0 ? score : left.localeCompare(right);
|
|
95
|
+
})
|
|
96
|
+
.slice(0, 4)
|
|
97
|
+
.map((address) => `http://${address}:${String(port)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function networkUrlsForBindHost(
|
|
101
|
+
port: number,
|
|
102
|
+
host: BindHost,
|
|
103
|
+
interfaces: NetworkInterfaceMap = networkInterfaces(),
|
|
104
|
+
): string[] {
|
|
105
|
+
if (isLoopbackHost(host)) return [];
|
|
106
|
+
if (isWildcardHost(host)) return collectNetworkUrls(port, interfaces);
|
|
107
|
+
return [urlForHost(port, host)];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatAccessHintLines(
|
|
111
|
+
port: number,
|
|
112
|
+
networkUrls: readonly string[],
|
|
113
|
+
host: BindHost = undefined,
|
|
114
|
+
): string[] {
|
|
115
|
+
const localUrl = localUrlForPort(port);
|
|
116
|
+
const hostLabel = isWildcardHost(host) ? "all interfaces (0.0.0.0)" : (trimHost(host) ?? "");
|
|
117
|
+
const lines = ["Access URLs:", ` Bind host: ${hostLabel}`];
|
|
118
|
+
|
|
119
|
+
if (isLoopbackHost(host) || isWildcardHost(host)) {
|
|
120
|
+
lines.push(` Local UI: ${localUrl}`);
|
|
121
|
+
lines.push(` Local proxy: ${localUrl}/proxy`);
|
|
122
|
+
lines.push(` Local MCP: ${localUrl}/api/mcp`);
|
|
123
|
+
} else {
|
|
124
|
+
const boundUrl = urlForHost(port, host);
|
|
125
|
+
lines.push(` Bound UI: ${boundUrl}`);
|
|
126
|
+
lines.push(` Bound proxy: ${boundUrl}/proxy`);
|
|
127
|
+
lines.push(` Bound MCP: ${boundUrl}/api/mcp`);
|
|
128
|
+
lines.push(" Localhost: may not work because the server is bound to a specific host");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const firstNetworkUrl = networkUrls[0];
|
|
132
|
+
if (isLoopbackHost(host)) {
|
|
133
|
+
lines.push(" Network: disabled by loopback bind host");
|
|
134
|
+
} else if (firstNetworkUrl === undefined) {
|
|
135
|
+
lines.push(" Network: no non-loopback IPv4 address detected");
|
|
136
|
+
} else {
|
|
137
|
+
lines.push(` Network UI: ${firstNetworkUrl}`);
|
|
138
|
+
lines.push(` Network proxy: ${firstNetworkUrl}/proxy`);
|
|
139
|
+
const moreUrls = networkUrls.slice(1);
|
|
140
|
+
if (moreUrls.length > 0) {
|
|
141
|
+
lines.push(` Other IPs: ${moreUrls.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lines.push(
|
|
146
|
+
" Container note: if the coding agent is in another container or machine, replace localhost with a reachable host/IP and verify the port is reachable.",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,14 @@ import { createConnection } from "node:net";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
+
import {
|
|
8
|
+
formatAccessHintLines,
|
|
9
|
+
isValidBindHost,
|
|
10
|
+
localUrlForPort,
|
|
11
|
+
networkUrlsForBindHost,
|
|
12
|
+
probeHostForBindHost,
|
|
13
|
+
urlForHost,
|
|
14
|
+
} from "./cli/networkHints.js";
|
|
7
15
|
|
|
8
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
17
|
const __dirname = dirname(__filename);
|
|
@@ -43,11 +51,11 @@ await runStart(process.argv.slice(2));
|
|
|
43
51
|
// Legacy `start` behavior — start the proxy on the configured port. Extracted
|
|
44
52
|
// into a function so the router above can keep the top-level flow readable.
|
|
45
53
|
// -----------------------------------------------------------------------------
|
|
46
|
-
async function isInspectorHealthy(port: number): Promise<boolean> {
|
|
54
|
+
async function isInspectorHealthy(port: number, host?: string): Promise<boolean> {
|
|
47
55
|
const controller = new AbortController();
|
|
48
56
|
const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
|
|
49
57
|
try {
|
|
50
|
-
const response = await fetch(
|
|
58
|
+
const response = await fetch(`${urlForHost(port, probeHostForBindHost(host))}/api/health`, {
|
|
51
59
|
cache: "no-store",
|
|
52
60
|
signal: controller.signal,
|
|
53
61
|
});
|
|
@@ -59,11 +67,11 @@ async function isInspectorHealthy(port: number): Promise<boolean> {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
async function getRunningCaptureMode(port: number): Promise<CaptureMode | null> {
|
|
70
|
+
async function getRunningCaptureMode(port: number, host?: string): Promise<CaptureMode | null> {
|
|
63
71
|
const controller = new AbortController();
|
|
64
72
|
const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
|
|
65
73
|
try {
|
|
66
|
-
const response = await fetch(
|
|
74
|
+
const response = await fetch(`${urlForHost(port, probeHostForBindHost(host))}/api/config`, {
|
|
67
75
|
cache: "no-store",
|
|
68
76
|
signal: controller.signal,
|
|
69
77
|
});
|
|
@@ -77,9 +85,9 @@ async function getRunningCaptureMode(port: number): Promise<CaptureMode | null>
|
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
function isPortAcceptingConnections(port: number): Promise<boolean> {
|
|
88
|
+
function isPortAcceptingConnections(port: number, host?: string): Promise<boolean> {
|
|
81
89
|
return new Promise((resolve) => {
|
|
82
|
-
const socket = createConnection({ host:
|
|
90
|
+
const socket = createConnection({ host: probeHostForBindHost(host), port });
|
|
83
91
|
const finish = (value: boolean): void => {
|
|
84
92
|
socket.removeAllListeners();
|
|
85
93
|
socket.destroy();
|
|
@@ -99,10 +107,14 @@ function sleep(ms: number): Promise<void> {
|
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
async function waitForInspectorHealthy(
|
|
110
|
+
async function waitForInspectorHealthy(
|
|
111
|
+
port: number,
|
|
112
|
+
timeoutMs: number,
|
|
113
|
+
host?: string,
|
|
114
|
+
): Promise<boolean> {
|
|
103
115
|
const start = Date.now();
|
|
104
116
|
while (Date.now() - start < timeoutMs) {
|
|
105
|
-
if (await isInspectorHealthy(port)) return true;
|
|
117
|
+
if (await isInspectorHealthy(port, host)) return true;
|
|
106
118
|
await sleep(250);
|
|
107
119
|
}
|
|
108
120
|
return false;
|
|
@@ -179,13 +191,22 @@ function readCaptureMode(raw: unknown): CaptureMode | null {
|
|
|
179
191
|
return typeof value === "string" ? parseCaptureMode(value) : null;
|
|
180
192
|
}
|
|
181
193
|
|
|
194
|
+
function printAccessHints(port: number, host?: string): void {
|
|
195
|
+
const networkUrls = networkUrlsForBindHost(port, host);
|
|
196
|
+
for (const line of formatAccessHintLines(port, networkUrls, host)) {
|
|
197
|
+
console.log(line);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
182
201
|
async function runStart(args: string[]): Promise<void> {
|
|
183
202
|
const envPort = process.env["PORT"];
|
|
184
203
|
const portDefault = envPort !== undefined ? Number(envPort) : DEFAULT_PORT;
|
|
204
|
+
const envHost = process.env["NITRO_HOST"] ?? process.env["HOST"];
|
|
185
205
|
const envMode =
|
|
186
206
|
process.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? process.env["AGENT_INSPECTOR_MODE"];
|
|
187
207
|
|
|
188
208
|
let port = portDefault;
|
|
209
|
+
let host = envHost !== undefined && envHost.trim().length > 0 ? envHost.trim() : undefined;
|
|
189
210
|
let open = true;
|
|
190
211
|
let openWasSpecified = false;
|
|
191
212
|
let background = false;
|
|
@@ -206,8 +227,24 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
206
227
|
captureModeWasSpecified = true;
|
|
207
228
|
}
|
|
208
229
|
|
|
230
|
+
if (host !== undefined && !isValidBindHost(host)) {
|
|
231
|
+
console.error(`Invalid host: ${host}. Use an IP or hostname without protocol/path.`);
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
209
236
|
for (let i = 0; i < args.length; i++) {
|
|
210
237
|
const arg = args[i] ?? "";
|
|
238
|
+
if (arg.startsWith("--host=")) {
|
|
239
|
+
const value = arg.slice(arg.indexOf("=") + 1).trim();
|
|
240
|
+
if (!isValidBindHost(value)) {
|
|
241
|
+
console.error(`Invalid host: ${value}. Use an IP or hostname without protocol/path.`);
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
host = value;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
211
248
|
if (arg.startsWith("--mode=") || arg.startsWith("--capture-mode=")) {
|
|
212
249
|
const value = arg.slice(arg.indexOf("=") + 1);
|
|
213
250
|
const parsedMode = parseCaptureMode(value);
|
|
@@ -226,6 +263,18 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
226
263
|
port = Number(args[i + 1]);
|
|
227
264
|
i++;
|
|
228
265
|
break;
|
|
266
|
+
case "--host":
|
|
267
|
+
case "-H": {
|
|
268
|
+
const value = args[i + 1]?.trim() ?? "";
|
|
269
|
+
if (!isValidBindHost(value)) {
|
|
270
|
+
console.error(`Invalid host: ${value}. Use an IP or hostname without protocol/path.`);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
host = value;
|
|
275
|
+
i++;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
229
278
|
case "--no-open":
|
|
230
279
|
open = false;
|
|
231
280
|
openWasSpecified = true;
|
|
@@ -348,12 +397,13 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
348
397
|
|
|
349
398
|
process.env["PORT"] = String(port);
|
|
350
399
|
|
|
351
|
-
const url =
|
|
400
|
+
const url = urlForHost(port, host);
|
|
352
401
|
|
|
353
|
-
if (!forceRestart && (await isInspectorHealthy(port))) {
|
|
402
|
+
if (!forceRestart && (await isInspectorHealthy(port, host))) {
|
|
354
403
|
console.log(`agent-inspector is already running at ${url}`);
|
|
404
|
+
printAccessHints(port, host);
|
|
355
405
|
if (captureModeWasSpecified) {
|
|
356
|
-
const runningMode = await getRunningCaptureMode(port);
|
|
406
|
+
const runningMode = await getRunningCaptureMode(port, host);
|
|
357
407
|
if (runningMode !== null && runningMode !== captureMode) {
|
|
358
408
|
console.log(`Existing instance capture mode is ${runningMode}; requested ${captureMode}.`);
|
|
359
409
|
console.log(`Use --force-restart to restart with ${captureMode} mode.`);
|
|
@@ -366,7 +416,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
366
416
|
return;
|
|
367
417
|
}
|
|
368
418
|
|
|
369
|
-
if (!forceRestart && (await isPortAcceptingConnections(port))) {
|
|
419
|
+
if (!forceRestart && (await isPortAcceptingConnections(port, host))) {
|
|
370
420
|
console.error(`Port ${port} is already in use, but it is not a healthy agent-inspector.`);
|
|
371
421
|
console.error(`Stop that process, choose --port <n>, or re-run with --force-restart.`);
|
|
372
422
|
process.exitCode = 1;
|
|
@@ -380,6 +430,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
380
430
|
console.log(`Server running at ${url}`);
|
|
381
431
|
console.log(` Capture mode: ${captureMode}`);
|
|
382
432
|
console.log(` Proxy: ${url}/proxy`);
|
|
433
|
+
printAccessHints(port, host);
|
|
383
434
|
console.log(``);
|
|
384
435
|
console.log(`Route AI coding tools through the proxy:`);
|
|
385
436
|
console.log(` Claude Code: ANTHROPIC_BASE_URL=${url}/proxy claude`);
|
|
@@ -388,6 +439,9 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
388
439
|
console.log(
|
|
389
440
|
` Direct HTTP: curl ${url}/proxy/v1/messages -d '{"model":"...","messages":[...]}'`,
|
|
390
441
|
);
|
|
442
|
+
console.log(
|
|
443
|
+
` Remote/container tools: use the Network proxy URL above when shown; otherwise restart with --host 0.0.0.0 or a reachable IP.`,
|
|
444
|
+
);
|
|
391
445
|
console.log(``);
|
|
392
446
|
console.log(`Routing environment variables:`);
|
|
393
447
|
console.log(` ROUTES JSON map of model prefix -> upstream URL`);
|
|
@@ -429,6 +483,10 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
429
483
|
if (providersJson !== undefined) {
|
|
430
484
|
serverEnv["AGENT_INSPECTOR_PROVIDERS_JSON"] = providersJson;
|
|
431
485
|
}
|
|
486
|
+
if (host !== undefined) {
|
|
487
|
+
serverEnv["HOST"] = host;
|
|
488
|
+
serverEnv["NITRO_HOST"] = host;
|
|
489
|
+
}
|
|
432
490
|
serverEnv["AGENT_INSPECTOR_CAPTURE_MODE"] = captureMode;
|
|
433
491
|
const serverProcess = spawn(serverCommand.command, serverCommand.args, {
|
|
434
492
|
stdio: background ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
@@ -439,8 +497,9 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
439
497
|
|
|
440
498
|
if (background) {
|
|
441
499
|
serverProcess.unref();
|
|
442
|
-
if (await waitForInspectorHealthy(port, 5000)) {
|
|
500
|
+
if (await waitForInspectorHealthy(port, 5000, host)) {
|
|
443
501
|
console.log(`agent-inspector background server is ready at ${url}`);
|
|
502
|
+
printAccessHints(port, host);
|
|
444
503
|
return;
|
|
445
504
|
}
|
|
446
505
|
console.error(`agent-inspector background server did not become ready at ${url}.`);
|
|
@@ -783,6 +783,9 @@ export function ProxyViewer({
|
|
|
783
783
|
<Plus className="size-4 shrink-0 text-muted-foreground/70" aria-hidden="true" />
|
|
784
784
|
<McpLogo className="size-10 shrink-0" />
|
|
785
785
|
</span>
|
|
786
|
+
<span className="max-w-[calc(100vw-7rem)] text-sm font-semibold text-muted-foreground">
|
|
787
|
+
Local First. Evidence First.
|
|
788
|
+
</span>
|
|
786
789
|
<McpReadyBadge />
|
|
787
790
|
</h1>
|
|
788
791
|
<div className="justify-self-end">
|
|
@@ -87,6 +87,16 @@ function hasHeaderDetails(headers: Record<string, string> | undefined): boolean
|
|
|
87
87
|
return formatHeaders(headers) !== undefined;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function formatElapsed(ms: number): string {
|
|
91
|
+
if (ms < 1000) return `${ms}ms`;
|
|
92
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatTokenRate(tokensPerSecond: number): string {
|
|
96
|
+
if (tokensPerSecond >= 10) return `${tokensPerSecond.toFixed(0)}/s`;
|
|
97
|
+
return `${tokensPerSecond.toFixed(1)}/s`;
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
function hasDiagnosticDetails(result: TestResult): boolean {
|
|
91
101
|
const debug = result.debug;
|
|
92
102
|
return (
|
|
@@ -255,6 +265,7 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
255
265
|
}
|
|
256
266
|
if (result.success) {
|
|
257
267
|
const tokenParts: ReactNode[] = [];
|
|
268
|
+
const timingParts: ReactNode[] = [];
|
|
258
269
|
if (result.inputTokens !== undefined) {
|
|
259
270
|
tokenParts.push(<span key="in">{result.inputTokens} in</span>);
|
|
260
271
|
}
|
|
@@ -275,11 +286,29 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
275
286
|
</span>,
|
|
276
287
|
);
|
|
277
288
|
}
|
|
289
|
+
if (result.firstChunkMs !== undefined && result.firstChunkMs !== null) {
|
|
290
|
+
timingParts.push(<span key="first">first {formatElapsed(result.firstChunkMs)}</span>);
|
|
291
|
+
}
|
|
292
|
+
if (result.totalStreamMs !== undefined && result.totalStreamMs !== null) {
|
|
293
|
+
timingParts.push(<span key="total">total {formatElapsed(result.totalStreamMs)}</span>);
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
result.tokensPerSecond !== undefined &&
|
|
297
|
+
result.tokensPerSecond !== null &&
|
|
298
|
+
result.tokensPerSecond > 0
|
|
299
|
+
) {
|
|
300
|
+
timingParts.push(<span key="rate">{formatTokenRate(result.tokensPerSecond)} tok/s</span>);
|
|
301
|
+
}
|
|
278
302
|
const displayTokens: ReactNode[] = [];
|
|
279
303
|
for (let i = 0; i < tokenParts.length; i++) {
|
|
280
304
|
if (i > 0) displayTokens.push(", ");
|
|
281
305
|
displayTokens.push(tokenParts[i]);
|
|
282
306
|
}
|
|
307
|
+
const displayTiming: ReactNode[] = [];
|
|
308
|
+
for (let i = 0; i < timingParts.length; i++) {
|
|
309
|
+
if (i > 0) displayTiming.push(", ");
|
|
310
|
+
displayTiming.push(timingParts[i]);
|
|
311
|
+
}
|
|
283
312
|
return (
|
|
284
313
|
<TooltipProvider>
|
|
285
314
|
<Tooltip>
|
|
@@ -290,6 +319,9 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
290
319
|
{tokenParts.length > 0 && (
|
|
291
320
|
<span className="text-muted-foreground">({displayTokens})</span>
|
|
292
321
|
)}
|
|
322
|
+
{timingParts.length > 0 && (
|
|
323
|
+
<span className="hidden text-muted-foreground lg:inline">({displayTiming})</span>
|
|
324
|
+
)}
|
|
293
325
|
</div>
|
|
294
326
|
</TooltipTrigger>
|
|
295
327
|
<TooltipContent>Connection test passed</TooltipContent>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Copy,
|
|
12
12
|
FileDiff,
|
|
13
13
|
FileTerminal,
|
|
14
|
+
Gauge,
|
|
14
15
|
Globe,
|
|
15
16
|
History,
|
|
16
17
|
Loader2,
|
|
@@ -45,6 +46,11 @@ function formatElapsed(ms: number): string {
|
|
|
45
46
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function formatTokenRate(tokensPerSecond: number): string {
|
|
50
|
+
if (tokensPerSecond >= 10) return `${tokensPerSecond.toFixed(0)}/s`;
|
|
51
|
+
return `${tokensPerSecond.toFixed(1)}/s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
/**
|
|
49
55
|
* Inline trend indicator: small arrow (green up / red down) plus the absolute
|
|
50
56
|
* delta in compact form. Returns `null` when there is no trend to display.
|
|
@@ -148,6 +154,11 @@ export const LogEntryHeader = memo(function ({
|
|
|
148
154
|
log.elapsedMs > slowResponseThresholdSeconds * 1000;
|
|
149
155
|
|
|
150
156
|
const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
|
|
157
|
+
const firstChunkMs = log.firstChunkMs ?? null;
|
|
158
|
+
const tokensPerSecond = log.tokensPerSecond ?? null;
|
|
159
|
+
const firstChunkLabel = firstChunkMs === null ? null : formatElapsed(firstChunkMs);
|
|
160
|
+
const tokenRateLabel =
|
|
161
|
+
tokensPerSecond === null || tokensPerSecond <= 0 ? null : formatTokenRate(tokensPerSecond);
|
|
151
162
|
|
|
152
163
|
return (
|
|
153
164
|
<TooltipProvider>
|
|
@@ -238,11 +249,37 @@ export const LogEntryHeader = memo(function ({
|
|
|
238
249
|
? `Slow response: ${formatElapsed(log.elapsedMs)} exceeds ${formatElapsed(
|
|
239
250
|
slowResponseThresholdSeconds * 1000,
|
|
240
251
|
)}`
|
|
241
|
-
:
|
|
252
|
+
: log.streaming
|
|
253
|
+
? "Total streaming response time"
|
|
254
|
+
: "Elapsed response time"}
|
|
242
255
|
</TooltipContent>
|
|
243
256
|
</Tooltip>
|
|
244
257
|
)}
|
|
245
258
|
|
|
259
|
+
{/* Streaming timing */}
|
|
260
|
+
{log.streaming && firstChunkLabel !== null && (
|
|
261
|
+
<Tooltip>
|
|
262
|
+
<TooltipTrigger asChild>
|
|
263
|
+
<span className="flex items-center gap-1 text-xs text-cyan-400 shrink-0">
|
|
264
|
+
<Radio className="size-3" />
|
|
265
|
+
<span className="font-mono tabular-nums">first {firstChunkLabel}</span>
|
|
266
|
+
</span>
|
|
267
|
+
</TooltipTrigger>
|
|
268
|
+
<TooltipContent>Time to first streaming chunk</TooltipContent>
|
|
269
|
+
</Tooltip>
|
|
270
|
+
)}
|
|
271
|
+
{log.streaming && tokenRateLabel !== null && (
|
|
272
|
+
<Tooltip>
|
|
273
|
+
<TooltipTrigger asChild>
|
|
274
|
+
<span className="flex items-center gap-1 text-xs text-emerald-400 shrink-0">
|
|
275
|
+
<Gauge className="size-3" />
|
|
276
|
+
<span className="font-mono tabular-nums">{tokenRateLabel}</span>
|
|
277
|
+
</span>
|
|
278
|
+
</TooltipTrigger>
|
|
279
|
+
<TooltipContent>Output token throughput after first chunk</TooltipContent>
|
|
280
|
+
</Tooltip>
|
|
281
|
+
)}
|
|
282
|
+
|
|
246
283
|
{/* Token counts */}
|
|
247
284
|
{hasTokens && (
|
|
248
285
|
<span className="flex items-center gap-1 text-xs shrink-0">
|