@tonyclaw/agent-inspector 2.0.26 → 2.0.28
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-DBTFzxG5.js → CompareDrawer-CicCP3Hb.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-ivZk8MgE.js → ProxyViewerContainer-BccuA6p5.js} +4 -4
- package/.output/public/assets/{ReplayDialog-syW2hDNE.js → ReplayDialog-Bc8q3ujm.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-BkuRtY9o.js → RequestAnatomy-Coyy3zcH.js} +1 -1
- package/.output/public/assets/{ResponseView-BwyvEvoE.js → ResponseView-BWJzgh0c.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-BY4MbPKg.js → StreamingChunkSequence-BSC3uuE5.js} +1 -1
- package/.output/public/assets/_sessionId-9rEF0uSE.js +1 -0
- package/.output/public/assets/index-B9_VaAWl.js +1 -0
- package/.output/public/assets/{main-DKRDRBdd.js → main-Brj0Gn91.js} +2 -2
- package/.output/server/_libs/modelcontextprotocol__server.mjs +228 -0
- package/.output/server/{_sessionId-VDd4N_1B.mjs → _sessionId-DDdyKVG-.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-BQZlxsOC.mjs → CompareDrawer-XSExe2ml.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-njY2oQCc.mjs → ProxyViewerContainer-B5pIiBZN.mjs} +7 -7
- package/.output/server/_ssr/{ReplayDialog-B8lkdAIh.mjs → ReplayDialog-BOAu0ric.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-CwOTfdwS.mjs → RequestAnatomy-C8Q5lozD.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-3Iz_mZpd.mjs → ResponseView-a_iJfPUF.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-hmJcQNW5.mjs → StreamingChunkSequence-YMKC0j8U.mjs} +2 -2
- package/.output/server/_ssr/{index-UuTKM4aC.mjs → index-CkDwBPzI.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-C7InHrxE.mjs → router-DXOXdFFM.mjs} +1816 -151
- package/.output/server/_tanstack-start-manifest_v-DPf3uJys.mjs +4 -0
- package/.output/server/index.mjs +64 -64
- package/README.md +55 -1
- package/package.json +1 -1
- package/src/cli/networkHints.ts +150 -0
- package/src/cli.ts +72 -13
- package/src/lib/runContract.ts +162 -0
- package/src/mcp/server.ts +554 -2
- package/src/mcp/toolHandlers.ts +154 -0
- package/src/proxy/evidenceAnalysis.ts +522 -0
- package/src/proxy/evidenceExporter.ts +215 -0
- package/src/proxy/logSearch.ts +118 -0
- package/src/proxy/runFailures.ts +100 -0
- package/src/proxy/runStore.ts +159 -0
- package/src/routes/api/logs.ts +16 -0
- package/src/routes/api/runs.$runId.evidence.ts +62 -0
- package/src/routes/api/runs.$runId.ts +50 -0
- package/src/routes/api/runs.ts +58 -0
- package/.output/public/assets/_sessionId-XlPUgIIb.js +0 -1
- package/.output/public/assets/index-By11a28-.js +0 -1
- package/.output/server/_tanstack-start-manifest_v-B6yfnMHA.mjs +0 -4
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/runs", "/api/sessions", "/proxy/$", "/session/$sessionId", "/api/knowledge/candidates", "/api/knowledge/project-context", "/api/knowledge/search", "/api/knowledge/sessions/$sessionId/candidates"], "preloads": ["/assets/main-Brj0Gn91.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-B9_VaAWl.js", "/assets/ProxyViewerContainer-BccuA6p5.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/mcp": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/mcp.ts" }, "/api/models": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import", "/api/providers/scan"] }, "/api/runs": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/runs.ts", "children": ["/api/runs/$runId"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/proxy/$.ts" }, "/session/$sessionId": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/session/$sessionId.tsx", "assets": [], "preloads": ["/assets/_sessionId-9rEF0uSE.js", "/assets/ProxyViewerContainer-BccuA6p5.js"] }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/config.paths.ts" }, "/api/knowledge/candidates": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/knowledge.candidates.ts", "children": ["/api/knowledge/candidates/$candidateId"] }, "/api/knowledge/project-context": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/knowledge.project-context.ts" }, "/api/knowledge/search": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/knowledge.search.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/model-metadata", "/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.import.ts" }, "/api/providers/scan": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.scan.ts" }, "/api/runs/$runId": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/runs.$runId.ts", "children": ["/api/runs/$runId/evidence"] }, "/api/knowledge/candidates/$candidateId": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/knowledge.candidates.$candidateId.ts", "children": ["/api/knowledge/candidates/$candidateId/promote"] }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/model-metadata": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.$providerId.model-metadata.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.$providerId.test.ts", "children": ["/api/providers/$providerId/test/log"] }, "/api/runs/$runId/evidence": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/runs.$runId.evidence.ts" }, "/api/knowledge/candidates/$candidateId/promote": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/knowledge.candidates.$candidateId.promote.ts" }, "/api/knowledge/sessions/$sessionId/candidates": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/knowledge.sessions.$sessionId.candidates.ts" }, "/api/providers/$providerId/test/log": { "filePath": "C:/Users/claw/workspace/agent-inspector/src/routes/api/providers.$providerId.test.log.ts" } }, "clientEntry": "/assets/main-Brj0Gn91.js" });
|
|
2
|
+
export {
|
|
3
|
+
tsrStartManifest
|
|
4
|
+
};
|
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-B9_VaAWl.js": {
|
|
39
|
+
"type": "text/javascript; charset=utf-8",
|
|
40
|
+
"etag": '"74-wegwMejcMaCe2qDEma7y6qUh/6U"',
|
|
41
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
42
|
+
"size": 116,
|
|
43
|
+
"path": "../public/assets/index-B9_VaAWl.js"
|
|
44
|
+
},
|
|
38
45
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
46
|
"type": "image/svg+xml",
|
|
40
47
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
48
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
42
49
|
"size": 5915,
|
|
43
50
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
51
|
},
|
|
45
|
-
"/assets/CompareDrawer-
|
|
52
|
+
"/assets/CompareDrawer-CicCP3Hb.js": {
|
|
46
53
|
"type": "text/javascript; charset=utf-8",
|
|
47
|
-
"etag": '"49fe-
|
|
48
|
-
"mtime": "2026-06-
|
|
54
|
+
"etag": '"49fe-CmOMF3QSwAcX6NWjQdoZrIdyoIw"',
|
|
55
|
+
"mtime": "2026-06-30T03:21:19.570Z",
|
|
49
56
|
"size": 18942,
|
|
50
|
-
"path": "../public/assets/CompareDrawer-
|
|
51
|
-
},
|
|
52
|
-
"/assets/ReplayDialog-syW2hDNE.js": {
|
|
53
|
-
"type": "text/javascript; charset=utf-8",
|
|
54
|
-
"etag": '"318c-NQmbQD7SPA/gh+gt24ulUI098ko"',
|
|
55
|
-
"mtime": "2026-06-30T00:03:39.766Z",
|
|
56
|
-
"size": 12684,
|
|
57
|
-
"path": "../public/assets/ReplayDialog-syW2hDNE.js"
|
|
58
|
-
},
|
|
59
|
-
"/assets/index-By11a28-.js": {
|
|
60
|
-
"type": "text/javascript; charset=utf-8",
|
|
61
|
-
"etag": '"74-wKECK+LXurWi55O6mYvJFEu73U4"',
|
|
62
|
-
"mtime": "2026-06-30T00:03:39.766Z",
|
|
63
|
-
"size": 116,
|
|
64
|
-
"path": "../public/assets/index-By11a28-.js"
|
|
57
|
+
"path": "../public/assets/CompareDrawer-CicCP3Hb.js"
|
|
65
58
|
},
|
|
66
59
|
"/assets/index-DsiKfWCp.css": {
|
|
67
60
|
"type": "text/css; charset=utf-8",
|
|
68
61
|
"etag": '"1a3d5-pnLJEAiyUUKlRw5OUnvwf1dCQeI"',
|
|
69
|
-
"mtime": "2026-06-
|
|
62
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
70
63
|
"size": 107477,
|
|
71
64
|
"path": "../public/assets/index-DsiKfWCp.css"
|
|
72
65
|
},
|
|
73
|
-
"/assets/main-DKRDRBdd.js": {
|
|
74
|
-
"type": "text/javascript; charset=utf-8",
|
|
75
|
-
"etag": '"51726-1JXlqBObsVCWwZtRttZD0+8rmmo"',
|
|
76
|
-
"mtime": "2026-06-30T00:03:39.766Z",
|
|
77
|
-
"size": 333606,
|
|
78
|
-
"path": "../public/assets/main-DKRDRBdd.js"
|
|
79
|
-
},
|
|
80
|
-
"/assets/RequestAnatomy-BkuRtY9o.js": {
|
|
81
|
-
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"4485-ei5S3iwPLFX3XUKIwoNDXt6pfeg"',
|
|
83
|
-
"mtime": "2026-06-30T00:03:39.766Z",
|
|
84
|
-
"size": 17541,
|
|
85
|
-
"path": "../public/assets/RequestAnatomy-BkuRtY9o.js"
|
|
86
|
-
},
|
|
87
|
-
"/assets/ResponseView-BwyvEvoE.js": {
|
|
88
|
-
"type": "text/javascript; charset=utf-8",
|
|
89
|
-
"etag": '"30d0-NcNgH32g8TreiCI4JgpE13I81fo"',
|
|
90
|
-
"mtime": "2026-06-30T00:03:39.766Z",
|
|
91
|
-
"size": 12496,
|
|
92
|
-
"path": "../public/assets/ResponseView-BwyvEvoE.js"
|
|
93
|
-
},
|
|
94
|
-
"/assets/StreamingChunkSequence-BY4MbPKg.js": {
|
|
95
|
-
"type": "text/javascript; charset=utf-8",
|
|
96
|
-
"etag": '"d57-HwRjNFQZ5p5h2qA6wui4VPdas/g"',
|
|
97
|
-
"mtime": "2026-06-30T00:03:39.766Z",
|
|
98
|
-
"size": 3415,
|
|
99
|
-
"path": "../public/assets/StreamingChunkSequence-BY4MbPKg.js"
|
|
100
|
-
},
|
|
101
66
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
102
67
|
"type": "image/jpeg",
|
|
103
68
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
104
|
-
"mtime": "2026-06-
|
|
69
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
105
70
|
"size": 6918,
|
|
106
71
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
107
72
|
},
|
|
73
|
+
"/assets/qwen-CONDcHqt.png": {
|
|
74
|
+
"type": "image/png",
|
|
75
|
+
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
76
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
77
|
+
"size": 357059,
|
|
78
|
+
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
79
|
+
},
|
|
80
|
+
"/assets/ReplayDialog-Bc8q3ujm.js": {
|
|
81
|
+
"type": "text/javascript; charset=utf-8",
|
|
82
|
+
"etag": '"318c-QS16LlsGhExlRZzuuDmYzqWLBN0"',
|
|
83
|
+
"mtime": "2026-06-30T03:21:19.570Z",
|
|
84
|
+
"size": 12684,
|
|
85
|
+
"path": "../public/assets/ReplayDialog-Bc8q3ujm.js"
|
|
86
|
+
},
|
|
108
87
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
109
88
|
"type": "image/svg+xml",
|
|
110
89
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
111
|
-
"mtime": "2026-06-
|
|
90
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
112
91
|
"size": 11256,
|
|
113
92
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
114
93
|
},
|
|
115
|
-
"/assets/
|
|
94
|
+
"/assets/main-Brj0Gn91.js": {
|
|
95
|
+
"type": "text/javascript; charset=utf-8",
|
|
96
|
+
"etag": '"51726-mwq2VuKeQG//deWEjQSFZIL9ZWo"',
|
|
97
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
98
|
+
"size": 333606,
|
|
99
|
+
"path": "../public/assets/main-Brj0Gn91.js"
|
|
100
|
+
},
|
|
101
|
+
"/assets/RequestAnatomy-Coyy3zcH.js": {
|
|
116
102
|
"type": "text/javascript; charset=utf-8",
|
|
117
|
-
"etag": '"
|
|
118
|
-
"mtime": "2026-06-
|
|
103
|
+
"etag": '"4485-KFNCPYVKcx+a4jW1uo7q7Az4T5c"',
|
|
104
|
+
"mtime": "2026-06-30T03:21:19.570Z",
|
|
105
|
+
"size": 17541,
|
|
106
|
+
"path": "../public/assets/RequestAnatomy-Coyy3zcH.js"
|
|
107
|
+
},
|
|
108
|
+
"/assets/_sessionId-9rEF0uSE.js": {
|
|
109
|
+
"type": "text/javascript; charset=utf-8",
|
|
110
|
+
"etag": '"d2-+KgPaqhF/ImO1Zb9tZi0sMB8OPI"',
|
|
111
|
+
"mtime": "2026-06-30T03:21:19.568Z",
|
|
119
112
|
"size": 210,
|
|
120
|
-
"path": "../public/assets/_sessionId-
|
|
113
|
+
"path": "../public/assets/_sessionId-9rEF0uSE.js"
|
|
121
114
|
},
|
|
122
|
-
"/assets/
|
|
123
|
-
"type": "
|
|
124
|
-
"etag": '"
|
|
125
|
-
"mtime": "2026-06-
|
|
126
|
-
"size":
|
|
127
|
-
"path": "../public/assets/
|
|
115
|
+
"/assets/ResponseView-BWJzgh0c.js": {
|
|
116
|
+
"type": "text/javascript; charset=utf-8",
|
|
117
|
+
"etag": '"30d0-XQuX3LcRTXyoZYFeRDp9J2gMpd0"',
|
|
118
|
+
"mtime": "2026-06-30T03:21:19.570Z",
|
|
119
|
+
"size": 12496,
|
|
120
|
+
"path": "../public/assets/ResponseView-BWJzgh0c.js"
|
|
121
|
+
},
|
|
122
|
+
"/assets/StreamingChunkSequence-BSC3uuE5.js": {
|
|
123
|
+
"type": "text/javascript; charset=utf-8",
|
|
124
|
+
"etag": '"d57-1GnBSzcha+n1obLlcv4lwyBhPf4"',
|
|
125
|
+
"mtime": "2026-06-30T03:21:19.570Z",
|
|
126
|
+
"size": 3415,
|
|
127
|
+
"path": "../public/assets/StreamingChunkSequence-BSC3uuE5.js"
|
|
128
128
|
},
|
|
129
|
-
"/assets/ProxyViewerContainer-
|
|
129
|
+
"/assets/ProxyViewerContainer-BccuA6p5.js": {
|
|
130
130
|
"type": "text/javascript; charset=utf-8",
|
|
131
|
-
"etag": '"af111-
|
|
132
|
-
"mtime": "2026-06-
|
|
131
|
+
"etag": '"af111-eqyso0QJHFS7+IdUoP/1PcawtQE"',
|
|
132
|
+
"mtime": "2026-06-30T03:21:19.570Z",
|
|
133
133
|
"size": 717073,
|
|
134
|
-
"path": "../public/assets/ProxyViewerContainer-
|
|
134
|
+
"path": "../public/assets/ProxyViewerContainer-BccuA6p5.js"
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
function readAsset(id) {
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ The npm package and CLI remain `agent-inspector`; the platform name is Agent Ins
|
|
|
9
9
|
- Runs a local transparent proxy for Anthropic and OpenAI-compatible APIs.
|
|
10
10
|
- Captures model requests, responses, tool definitions, token usage, and errors.
|
|
11
11
|
- Provides a web UI for browsing sessions and logs in real time.
|
|
12
|
+
- Exposes sessions, logs, runs, and evidence packs through MCP Tools, Resources, and Prompts.
|
|
12
13
|
- Includes a Chrome companion extension for quick capture status and log navigation.
|
|
13
14
|
- Generates reviewable knowledge candidates from agent sessions.
|
|
14
15
|
- Bridges approved knowledge to OpenClaw while keeping OpenClaw hidden behind the Inspector surface.
|
|
@@ -107,7 +108,7 @@ The web UI runs at http://localhost:25947.
|
|
|
107
108
|
|
|
108
109
|
- [Installation](docs/Installation.md): install and runtime notes.
|
|
109
110
|
- [Usage](docs/Usage.md): daily capture, browsing, replay, and export workflows.
|
|
110
|
-
- [MCP Server](docs/MCP-Server.md): MCP setup and
|
|
111
|
+
- [MCP Server](docs/MCP-Server.md): MCP setup plus tools, resources, prompts, and evidence usage for coding agents.
|
|
111
112
|
- [Troubleshooting](docs/TROUBLESHOOTING.md): port, provider, MCP, Replay, Jenkins, and GitCode fixes.
|
|
112
113
|
- [Jenkins + GitCode WebHook](docs/JENKINS_GITCODE_WEBHOOK_TUNNEL.md): team CI and tunnel setup.
|
|
113
114
|
- [Local Release Helper](docs/LOCAL_RELEASE.md): local maintainer release helper.
|
|
@@ -142,9 +143,62 @@ can also set `AGENT_INSPECTOR_MODE=full` or
|
|
|
142
143
|
`AGENT_INSPECTOR_CAPTURE_MODE=full`; the CLI flag takes precedence over the
|
|
143
144
|
environment.
|
|
144
145
|
|
|
146
|
+
## MCP Evidence Workflows
|
|
147
|
+
|
|
148
|
+
Agent Inspector exposes MCP at:
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
http://localhost:25947/api/mcp
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Coding agents can use the MCP surface in three ways:
|
|
155
|
+
|
|
156
|
+
- Tools: perform actions such as listing logs, reading bounded log details, testing providers,
|
|
157
|
+
creating evaluation runs, and exporting evidence packs.
|
|
158
|
+
- Resources: read stable evidence URIs without invoking a tool call.
|
|
159
|
+
- Prompts: start common analysis/reporting tasks from reusable templates.
|
|
160
|
+
|
|
161
|
+
The evaluation-oriented run flow is:
|
|
162
|
+
|
|
163
|
+
1. Call `inspector_create_run` before a task starts to declare a stable `runId` and optional
|
|
164
|
+
`sessionId`.
|
|
165
|
+
2. Call `inspector_update_run` as the task moves through `running`, `failed`, `completed`, or
|
|
166
|
+
`cancelled`; metadata keys are merged so CI can add build numbers, branch names, or case ids.
|
|
167
|
+
3. Let the coding agent run through the proxy as usual.
|
|
168
|
+
4. If something fails, call `inspector_get_recent_failures` or read
|
|
169
|
+
`inspector://failures/recent`, then use `inspector_search_logs` to locate suspicious log ids.
|
|
170
|
+
5. Call `inspector_get_run` to inspect the declared run.
|
|
171
|
+
6. Call `inspector_export_evidence` to write JSON, Markdown, and HTML artifacts under:
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
<dataDir>/evidence/<runId>/evidence.json
|
|
175
|
+
<dataDir>/evidence/<runId>/evidence.md
|
|
176
|
+
<dataDir>/evidence/<runId>/evidence.html
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Useful MCP resource URIs:
|
|
180
|
+
|
|
181
|
+
| Resource | Purpose |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `inspector://sessions` | List known session ids. |
|
|
184
|
+
| `inspector://sessions/{sessionId}` | Read a compact session summary with Inspector links, counts, models, providers, token totals, and recent logs. |
|
|
185
|
+
| `inspector://logs/{id}` | Read bounded log details for one captured request. Large bodies are capped for agent safety. |
|
|
186
|
+
| `inspector://runs` | List declared evaluation runs. |
|
|
187
|
+
| `inspector://failures/recent` | Read recent failed/cancelled runs and non-success evidence classifications. |
|
|
188
|
+
| `inspector://runs/{runId}` | Read one declared run and its evidence paths. |
|
|
189
|
+
| `inspector://evidence/{runId}` | Read the exported Markdown evidence pack for a run. |
|
|
190
|
+
|
|
191
|
+
Built-in prompt templates help turn captured evidence into user-facing output:
|
|
192
|
+
|
|
193
|
+
- `inspector_analyze_session_failure`: explain why a session failed and cite log evidence.
|
|
194
|
+
- `inspector_triage_recent_failures`: prioritize recent failed runs and suggest next actions.
|
|
195
|
+
- `inspector_generate_jenkins_report`: produce a Jenkins-ready Markdown report for a run.
|
|
196
|
+
- `inspector_extract_repro_steps`: extract concise reproduction steps from a session.
|
|
197
|
+
|
|
145
198
|
## Jenkins And GitCode
|
|
146
199
|
|
|
147
200
|
Team CI, GitCode WebHook setup, Cloudflare Tunnel notes, local push fallback, and the verified PR workflow live in [Jenkins + GitCode WebHook](docs/JENKINS_GITCODE_WEBHOOK_TUNNEL.md). Keep real Jenkins notify tokens, GitCode access tokens, and tunnel URLs out of the repository.
|
|
201
|
+
|
|
148
202
|
## Project Direction
|
|
149
203
|
|
|
150
204
|
Agent Inspector should stay close to the proven inspector codebase while growing into an agent knowledge layer. The memory flow has four layers:
|
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}.`);
|