@tonyclaw/agent-inspector 4.0.1 → 4.0.3
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/backend/nitro.json +1 -1
- package/.output/cli.js +193 -110
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-CTHqmroT.mjs → router-ChILzhmz.mjs} +92 -4
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-L3n4J-Or.js → CompareDrawer-Co3cUF_I.js} +1 -1
- package/.output/ui/assets/{InspectorPet-D5o_3OZS.js → InspectorPet-jFcrL37X.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-Ocagx75S.js → ProxyViewerContainer-BPzt_VpM.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-Bk7x1h-y.js → ReplayDialog-C6O_zuwI.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-U4lzL7Sc.js → RequestAnatomy-Ca6NvB7L.js} +1 -1
- package/.output/ui/assets/{ResponseView-LOWfvBLK.js → ResponseView-BzqKcveJ.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-DPfsrrXQ.js → StreamingChunkSequence-9pCZIsS_.js} +1 -1
- package/.output/ui/assets/{_sessionId-0MlWS-CO.js → _sessionId-BMP3KVGR.js} +1 -1
- package/.output/ui/assets/{_sessionId-OdX2Jvt1.js → _sessionId-y4eRoT1H.js} +1 -1
- package/.output/ui/assets/{index-Ea-c8hPg.js → index-CH5w-haG.js} +1 -1
- package/.output/ui/assets/{index-zVVS53Hd.js → index-CmocEhyU.js} +1 -1
- package/.output/ui/assets/{index-Du4qNOOt.js → index-DT8sMJ_n.js} +2 -2
- package/.output/ui/assets/{index-gvFj_9K6.js → index-bfsbIRZa.js} +1 -1
- package/.output/ui/assets/{json-viewer-C1WAMzg3.js → json-viewer-BfFOSDG5.js} +1 -1
- package/.output/ui/assets/{jszip.min-B8AKu2zm.js → jszip.min-BkbVkOsp.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/package.json +5 -10
- package/src/assets/IDE/claudecode-color.svg +0 -1
- package/src/assets/IDE/codex-color.svg +0 -1
- package/src/assets/IDE/opencode.svg +0 -1
- package/src/assets/IDE/xiaomimimo.svg +0 -1
- package/src/assets/favicon.svg +0 -21
- package/src/assets/logos/alibaba.svg +0 -1
- package/src/assets/logos/anthropic.svg +0 -1
- package/src/assets/logos/claude-code.svg +0 -4
- package/src/assets/logos/deepseek.svg +0 -1
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +0 -1
- package/src/assets/logos/opencode.svg +0 -4
- package/src/assets/logos/qwen.webp +0 -0
- package/src/assets/logos/zhipuai.svg +0 -219
- package/src/backend/router.tsx +0 -36
- package/src/backend/routes/$.ts +0 -26
- package/src/backend/routes/__root.ts +0 -4
- package/src/backend/routes/api/-instances.ts +0 -7
- package/src/backend/routes/api/alerts.summary.ts +0 -28
- package/src/backend/routes/api/alerts.ts +0 -70
- package/src/backend/routes/api/config.paths.ts +0 -14
- package/src/backend/routes/api/config.ts +0 -73
- package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.ts +0 -21
- package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
- package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
- package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
- package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
- package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
- package/src/backend/routes/api/groups.$groupId.ts +0 -93
- package/src/backend/routes/api/groups.ts +0 -79
- package/src/backend/routes/api/health.ts +0 -22
- package/src/backend/routes/api/instances.$name.connection.ts +0 -12
- package/src/backend/routes/api/instances.$name.restart.ts +0 -23
- package/src/backend/routes/api/instances.$name.start.ts +0 -18
- package/src/backend/routes/api/instances.$name.stop.ts +0 -23
- package/src/backend/routes/api/instances.$name.ts +0 -12
- package/src/backend/routes/api/instances.ts +0 -16
- package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
- package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
- package/src/backend/routes/api/knowledge.candidates.ts +0 -10
- package/src/backend/routes/api/knowledge.project-context.ts +0 -29
- package/src/backend/routes/api/knowledge.search.ts +0 -62
- package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
- package/src/backend/routes/api/logs.$id.body.ts +0 -58
- package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
- package/src/backend/routes/api/logs.$id.replay.ts +0 -297
- package/src/backend/routes/api/logs.$id.ts +0 -23
- package/src/backend/routes/api/logs.import.ts +0 -39
- package/src/backend/routes/api/logs.stream.ts +0 -174
- package/src/backend/routes/api/logs.ts +0 -214
- package/src/backend/routes/api/mcp.ts +0 -25
- package/src/backend/routes/api/models.ts +0 -10
- package/src/backend/routes/api/pi-agent.ts +0 -29
- package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
- package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
- package/src/backend/routes/api/providers.$providerId.ts +0 -111
- package/src/backend/routes/api/providers.export.ts +0 -44
- package/src/backend/routes/api/providers.import.ts +0 -81
- package/src/backend/routes/api/providers.scan.ts +0 -28
- package/src/backend/routes/api/providers.ts +0 -102
- package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
- package/src/backend/routes/api/runs.$runId.ts +0 -50
- package/src/backend/routes/api/runs.ts +0 -62
- package/src/backend/routes/api/runtime.ts +0 -46
- package/src/backend/routes/api/sessions.ts +0 -51
- package/src/backend/routes/api/storage.ts +0 -56
- package/src/backend/routes/livez.ts +0 -13
- package/src/backend/routes/metrics.ts +0 -14
- package/src/backend/routes/proxy/$.ts +0 -15
- package/src/backend/routes/readyz.ts +0 -18
- package/src/backend/start.ts +0 -10
- package/src/cli/alias.ts +0 -431
- package/src/cli/detect-tools.ts +0 -123
- package/src/cli/doctor.ts +0 -836
- package/src/cli/instance.ts +0 -1071
- package/src/cli/instanceApi.ts +0 -213
- package/src/cli/instanceArgs.ts +0 -301
- package/src/cli/instanceControl.ts +0 -191
- package/src/cli/instanceModel.ts +0 -276
- package/src/cli/instanceRegistry.ts +0 -731
- package/src/cli/networkHints.ts +0 -170
- package/src/cli/onboard.ts +0 -1668
- package/src/cli/onboardTarget.ts +0 -228
- package/src/cli/rustBackendPackage.ts +0 -458
- package/src/cli/startupOutput.ts +0 -16
- package/src/cli/templates/codex-skill-onboard.ts +0 -320
- package/src/cli/templates/command-onboard.ts +0 -25
- package/src/cli/templates/skill-onboard.ts +0 -625
- package/src/cli.ts +0 -1433
- package/src/components/OnboardingBanner.tsx +0 -115
- package/src/components/ProxyViewer.tsx +0 -2259
- package/src/components/ProxyViewerContainer.tsx +0 -1076
- package/src/components/alerts/AlertsDialog.tsx +0 -615
- package/src/components/clients/ClientLogo.tsx +0 -175
- package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
- package/src/components/errors/SafeErrorBoundary.tsx +0 -201
- package/src/components/groups/GroupsDialog.tsx +0 -1304
- package/src/components/inspector-pet/InspectorPet.tsx +0 -398
- package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
- package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
- package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
- package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
- package/src/components/providers/ImportWizardDialog.tsx +0 -281
- package/src/components/providers/ProviderCard.tsx +0 -347
- package/src/components/providers/ProviderForm.tsx +0 -1183
- package/src/components/providers/ProviderLogo.tsx +0 -122
- package/src/components/providers/ProviderTestResultsView.tsx +0 -734
- package/src/components/providers/ProvidersPanel.tsx +0 -714
- package/src/components/providers/SettingsDialog.tsx +0 -1042
- package/src/components/providers/providerFormCapabilities.ts +0 -36
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
- package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
- package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
- package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
- package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
- package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
- package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
- package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
- package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
- package/src/components/proxy-viewer/ResponseView.tsx +0 -202
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
- package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
- package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
- package/src/components/proxy-viewer/accessibility.ts +0 -8
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
- package/src/components/proxy-viewer/anatomy/types.ts +0 -41
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
- package/src/components/proxy-viewer/bodyHydration.ts +0 -123
- package/src/components/proxy-viewer/cacheTrend.ts +0 -50
- package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
- package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
- package/src/components/proxy-viewer/diff/index.ts +0 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
- package/src/components/proxy-viewer/formats/index.tsx +0 -47
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
- package/src/components/proxy-viewer/index.ts +0 -9
- package/src/components/proxy-viewer/lazy.ts +0 -41
- package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
- package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
- package/src/components/proxy-viewer/log-formats/index.ts +0 -23
- package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
- package/src/components/proxy-viewer/log-formats/types.ts +0 -40
- package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
- package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
- package/src/components/proxy-viewer/logFocus.ts +0 -54
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
- package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
- package/src/components/proxy-viewer/replayComparison.ts +0 -131
- package/src/components/proxy-viewer/requestDiff.ts +0 -277
- package/src/components/proxy-viewer/requestTools.ts +0 -220
- package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
- package/src/components/proxy-viewer/viewerState.ts +0 -176
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/collapsible.tsx +0 -21
- package/src/components/ui/confirm-dialog.tsx +0 -51
- package/src/components/ui/crab-logo.tsx +0 -45
- package/src/components/ui/crab-variants.tsx +0 -467
- package/src/components/ui/dialog.tsx +0 -130
- package/src/components/ui/icon-trigger.ts +0 -2
- package/src/components/ui/json-viewer-bulk.ts +0 -132
- package/src/components/ui/json-viewer.tsx +0 -508
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/select.tsx +0 -178
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -88
- package/src/components/ui/tooltip.tsx +0 -51
- package/src/components/ui/transient-toast.tsx +0 -105
- package/src/contracts/anthropic.ts +0 -230
- package/src/contracts/index.ts +0 -59
- package/src/contracts/json.ts +0 -37
- package/src/contracts/log.ts +0 -139
- package/src/contracts/openai.ts +0 -267
- package/src/index.css +0 -11
- package/src/knowledge/candidateStore.ts +0 -94
- package/src/knowledge/distiller.ts +0 -98
- package/src/knowledge/openclawClient.ts +0 -161
- package/src/knowledge/openclawGatewayClient.ts +0 -249
- package/src/knowledge/openclawMarkdown.ts +0 -146
- package/src/knowledge/redactor.ts +0 -80
- package/src/knowledge/types.ts +0 -83
- package/src/lib/alertContract.ts +0 -76
- package/src/lib/apiClient.ts +0 -166
- package/src/lib/apiFormat.ts +0 -29
- package/src/lib/backendImplementation.ts +0 -16
- package/src/lib/basePath.ts +0 -107
- package/src/lib/browserRuntimeContract.ts +0 -124
- package/src/lib/browserRuntimeResolver.ts +0 -333
- package/src/lib/chunkLoadRecovery.ts +0 -83
- package/src/lib/claudeCodeStrip.ts +0 -98
- package/src/lib/clipboard.ts +0 -67
- package/src/lib/codingAgentEndpoints.ts +0 -54
- package/src/lib/ecosystemContract.ts +0 -130
- package/src/lib/export-logs.ts +0 -287
- package/src/lib/groupContract.ts +0 -166
- package/src/lib/instanceContract.ts +0 -239
- package/src/lib/jsonFile.ts +0 -46
- package/src/lib/logImportContract.ts +0 -12
- package/src/lib/managedInstance.ts +0 -112
- package/src/lib/mask.ts +0 -4
- package/src/lib/objectUtils.ts +0 -22
- package/src/lib/piAgentContract.ts +0 -102
- package/src/lib/providerContract.ts +0 -125
- package/src/lib/providerImportContract.ts +0 -27
- package/src/lib/providerModelMetadata.ts +0 -458
- package/src/lib/providerTestContract.ts +0 -170
- package/src/lib/providerTestPrompt.ts +0 -100
- package/src/lib/publicBasePathContext.ts +0 -137
- package/src/lib/resourceLimits.ts +0 -345
- package/src/lib/runContract.ts +0 -164
- package/src/lib/runtimeConfig.ts +0 -43
- package/src/lib/safeDiagnostic.ts +0 -38
- package/src/lib/safeFetch.ts +0 -508
- package/src/lib/serverPort.ts +0 -47
- package/src/lib/sessionInfoContract.ts +0 -78
- package/src/lib/sessionUrl.ts +0 -46
- package/src/lib/ssrfGuard.ts +0 -133
- package/src/lib/stopReason.ts +0 -130
- package/src/lib/themeMode.ts +0 -41
- package/src/lib/timeDisplay.ts +0 -22
- package/src/lib/toolTrace.ts +0 -290
- package/src/lib/uiScale.ts +0 -117
- package/src/lib/unknown.ts +0 -27
- package/src/lib/upstreamUrl.ts +0 -150
- package/src/lib/useAlerts.ts +0 -82
- package/src/lib/useGroupEvidence.ts +0 -37
- package/src/lib/useGroups.ts +0 -32
- package/src/lib/useOnboarding.ts +0 -93
- package/src/lib/useProviders.ts +0 -30
- package/src/lib/useStripConfig.ts +0 -154
- package/src/lib/useThemeMode.ts +0 -40
- package/src/lib/useUiScale.ts +0 -52
- package/src/lib/utils.ts +0 -78
- package/src/mcp/currentContext.ts +0 -65
- package/src/mcp/instanceHandlers.ts +0 -151
- package/src/mcp/loopback.ts +0 -88
- package/src/mcp/mode.ts +0 -75
- package/src/mcp/previewExtractor.ts +0 -233
- package/src/mcp/server.ts +0 -2274
- package/src/mcp/toolHandlers.ts +0 -1533
- package/src/proxy/alerts.ts +0 -664
- package/src/proxy/chunkStorage.ts +0 -123
- package/src/proxy/config.ts +0 -218
- package/src/proxy/constants.ts +0 -66
- package/src/proxy/dataDir.ts +0 -147
- package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
- package/src/proxy/ecosystemExecutionRoute.ts +0 -116
- package/src/proxy/ecosystemRegistry.ts +0 -296
- package/src/proxy/ecosystemTasks.ts +0 -415
- package/src/proxy/evidenceAnalysis.ts +0 -526
- package/src/proxy/evidenceExporter.ts +0 -235
- package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
- package/src/proxy/formats/anthropic/handler.ts +0 -75
- package/src/proxy/formats/anthropic/index.ts +0 -13
- package/src/proxy/formats/anthropic/register.ts +0 -4
- package/src/proxy/formats/anthropic/schemas.ts +0 -6
- package/src/proxy/formats/anthropic/stream.ts +0 -205
- package/src/proxy/formats/handler.ts +0 -46
- package/src/proxy/formats/index.ts +0 -13
- package/src/proxy/formats/jsonSchema.ts +0 -2
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
- package/src/proxy/formats/openai/codexMetadata.ts +0 -61
- package/src/proxy/formats/openai/handler.ts +0 -133
- package/src/proxy/formats/openai/index.ts +0 -33
- package/src/proxy/formats/openai/provider.ts +0 -50
- package/src/proxy/formats/openai/register.ts +0 -4
- package/src/proxy/formats/openai/schemas.ts +0 -25
- package/src/proxy/formats/openai/stream.ts +0 -600
- package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
- package/src/proxy/formats/providerAdapter.ts +0 -50
- package/src/proxy/formats/providerRegistry.ts +0 -51
- package/src/proxy/formats/providers/index.ts +0 -3
- package/src/proxy/formats/registry.ts +0 -65
- package/src/proxy/groupEvidenceExporter.ts +0 -371
- package/src/proxy/groupStore.ts +0 -336
- package/src/proxy/handler.ts +0 -729
- package/src/proxy/identityProxy.ts +0 -1842
- package/src/proxy/jsonlRecovery.ts +0 -554
- package/src/proxy/logBodyChunks.ts +0 -91
- package/src/proxy/logFinalizer.ts +0 -777
- package/src/proxy/logFinalizer.worker.ts +0 -24
- package/src/proxy/logImportUpload.ts +0 -128
- package/src/proxy/logImporter.ts +0 -813
- package/src/proxy/logIndex.ts +0 -663
- package/src/proxy/logSearch.ts +0 -122
- package/src/proxy/logger.ts +0 -537
- package/src/proxy/openaiOrphanToolStrip.ts +0 -142
- package/src/proxy/platformCommands.ts +0 -258
- package/src/proxy/privateDataPath.ts +0 -419
- package/src/proxy/providerImporters.ts +0 -578
- package/src/proxy/providerScanStore.ts +0 -87
- package/src/proxy/providerSecretStore.ts +0 -216
- package/src/proxy/providers.ts +0 -998
- package/src/proxy/rawStreamCapture.ts +0 -342
- package/src/proxy/runFailures.ts +0 -94
- package/src/proxy/runStore.ts +0 -214
- package/src/proxy/runtimeAdmission.ts +0 -55
- package/src/proxy/runtimeHealth.ts +0 -221
- package/src/proxy/runtimeMetrics.ts +0 -149
- package/src/proxy/runtimeShutdown.ts +0 -157
- package/src/proxy/schemas.ts +0 -112
- package/src/proxy/sessionArchive.ts +0 -479
- package/src/proxy/sessionInfo.ts +0 -266
- package/src/proxy/sessionProcess.ts +0 -194
- package/src/proxy/sessionRuntime.ts +0 -121
- package/src/proxy/sessionSupervisor.ts +0 -305
- package/src/proxy/sessionWorkerEntry.ts +0 -26
- package/src/proxy/shutdownCoordinator.ts +0 -90
- package/src/proxy/socketTracker.ts +0 -639
- package/src/proxy/sqliteLogIndex.ts +0 -733
- package/src/proxy/storageLifecycle.ts +0 -432
- package/src/proxy/store.ts +0 -1773
- package/src/proxy/streamTiming.ts +0 -31
- package/src/proxy/toolSchemaWarnings.ts +0 -529
- package/src/proxy/uiAssetServer.ts +0 -106
- package/src/proxy/upstream.ts +0 -127
- package/src/services/alerts.ts +0 -12
- package/src/services/groups.ts +0 -119
- package/src/services/piAgent.ts +0 -421
- package/src/services/piAgentRetrieval.ts +0 -1180
- package/src/services/result.ts +0 -17
- package/src/services/runs.ts +0 -76
- package/src/ui/bootstrap.ts +0 -167
- package/src/ui/index.html +0 -38
- package/src/ui/logsStreamClient.ts +0 -20
- package/src/ui/main.tsx +0 -13
- package/src/ui/public/runtime-config.js +0 -2
- package/src/ui/router.tsx +0 -13
- package/src/ui/routes/__root.tsx +0 -69
- package/src/ui/routes/index.tsx +0 -35
- package/src/ui/routes/session/$sessionId.tsx +0 -50
- package/src/ui/runtimeDiscovery.ts +0 -50
|
@@ -1,639 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { logger } from "./logger";
|
|
6
|
-
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
|
|
9
|
-
type ClientInfo = {
|
|
10
|
-
port: number | null;
|
|
11
|
-
pid: number | null;
|
|
12
|
-
cwd: string | null;
|
|
13
|
-
projectFolder: string | null;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type CacheEntry = ClientInfo & {
|
|
17
|
-
expiresAt: number;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type NetstatClient = {
|
|
21
|
-
pid: number;
|
|
22
|
-
localPort: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Cache entries: port -> { pid, cwd, projectFolder, expiresAt }
|
|
26
|
-
const cache = new Map<number, CacheEntry>();
|
|
27
|
-
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
28
|
-
const MAX_CACHE_SIZE = 200;
|
|
29
|
-
const RemotePortSchema = z.number().int().min(1).max(65_535);
|
|
30
|
-
const SocketSchema = z
|
|
31
|
-
.object({
|
|
32
|
-
remotePort: RemotePortSchema.nullish(),
|
|
33
|
-
})
|
|
34
|
-
.passthrough();
|
|
35
|
-
|
|
36
|
-
// Deduplicate concurrent lookups for the same port
|
|
37
|
-
const inflight = new Map<number, Promise<ClientInfo>>();
|
|
38
|
-
|
|
39
|
-
// --- Recent-client scan cache -------------------------------------------
|
|
40
|
-
// `getClientInfo` no longer needs a per-request client port; it scans all
|
|
41
|
-
// current connections to our listen port and returns the freshest. We cache
|
|
42
|
-
// the result for a short window so a burst of requests from one client
|
|
43
|
-
// converges on a single subprocess call.
|
|
44
|
-
const RECENT_CLIENT_TTL_MS = 1000;
|
|
45
|
-
let recentInflight: Promise<ClientInfo> | null = null;
|
|
46
|
-
let recentCache: ClientInfo | null = null;
|
|
47
|
-
let recentCacheExpiresAt = 0;
|
|
48
|
-
|
|
49
|
-
/** The port the proxy is listening on. Falls back to the dev default. */
|
|
50
|
-
function getProxyPort(): number {
|
|
51
|
-
// The proxy server exposes its chosen port via the `PROXY_PORT` env var.
|
|
52
|
-
// Default to the well-known dev port so the scan still works in dev mode
|
|
53
|
-
// (where `proxy.ts` reads from `getConfig()` instead of an env var).
|
|
54
|
-
const raw = process.env["PROXY_PORT"];
|
|
55
|
-
if (raw !== undefined) {
|
|
56
|
-
const parsed = parseInt(raw, 10);
|
|
57
|
-
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535) return parsed;
|
|
58
|
-
}
|
|
59
|
-
return 9527;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function getRecentClientFromCache(): ClientInfo | null {
|
|
63
|
-
if (recentCache === null) return null;
|
|
64
|
-
if (Date.now() >= recentCacheExpiresAt) {
|
|
65
|
-
recentCache = null;
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
return recentCache;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function setRecentClientCache(info: ClientInfo): void {
|
|
72
|
-
recentCache = info;
|
|
73
|
-
recentCacheExpiresAt = Date.now() + RECENT_CLIENT_TTL_MS;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function evictCacheIfNeeded(): void {
|
|
77
|
-
while (cache.size > MAX_CACHE_SIZE) {
|
|
78
|
-
const oldestKey = cache.keys().next().value;
|
|
79
|
-
if (oldestKey === undefined) break;
|
|
80
|
-
cache.delete(oldestKey);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getFromCache(port: number): ClientInfo | null {
|
|
85
|
-
const entry = cache.get(port);
|
|
86
|
-
if (entry && Date.now() < entry.expiresAt) {
|
|
87
|
-
return { port: entry.port, pid: entry.pid, cwd: entry.cwd, projectFolder: entry.projectFolder };
|
|
88
|
-
}
|
|
89
|
-
if (entry) cache.delete(port);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function setCache(port: number, info: ClientInfo): void {
|
|
94
|
-
cache.set(port, { ...info, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
95
|
-
evictCacheIfNeeded();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Best-effort remote-port extraction across runtimes. Tries, in order:
|
|
100
|
-
* 1. TanStack Start / Nitro: `request.context.nitro.node.req.socket` (h3
|
|
101
|
-
* wraps the underlying Node request, where the socket lives).
|
|
102
|
-
* 2. Bun-style: `request.socket` accessor.
|
|
103
|
-
* 3. Direct `socket` / `connection` properties on the request (legacy Node
|
|
104
|
-
* IncomingMessage-style wrapping).
|
|
105
|
-
* Returns `null` when nothing matches 鈥?the caller will then leave `pid`
|
|
106
|
-
* (and downstream `clientPid`) as `null`.
|
|
107
|
-
*/
|
|
108
|
-
export function extractRemotePort(request: Request): number | null {
|
|
109
|
-
const candidateSockets: unknown[] = [];
|
|
110
|
-
|
|
111
|
-
// 1) Nitro / h3 path. TanStack Start attaches the h3 event under
|
|
112
|
-
// `request.context.nitro`, and the underlying Node request lives at
|
|
113
|
-
// `event.node.req`. The socket may be on `req.socket` or
|
|
114
|
-
// `req.connection` depending on the Node version / TLS config.
|
|
115
|
-
const requestAsUnknown: unknown = request;
|
|
116
|
-
const ctx = pick(requestAsUnknown, "context");
|
|
117
|
-
const nitro = pick(ctx, "nitro");
|
|
118
|
-
const node = pick(nitro, "node");
|
|
119
|
-
const innerReq = pick(node, "req");
|
|
120
|
-
const sock = pick(innerReq, "socket") ?? pick(innerReq, "connection");
|
|
121
|
-
if (sock !== undefined) candidateSockets.push(sock);
|
|
122
|
-
|
|
123
|
-
// 2) Bun-style direct socket accessor on the Request.
|
|
124
|
-
const descriptor = Object.getOwnPropertyDescriptor(request, "socket");
|
|
125
|
-
if (descriptor !== undefined && descriptor.get === undefined) {
|
|
126
|
-
candidateSockets.push(descriptor.value);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 3) Raw socket/connection properties on the request itself.
|
|
130
|
-
const directSocket = pick(requestAsUnknown, "socket");
|
|
131
|
-
if (directSocket !== undefined) candidateSockets.push(directSocket);
|
|
132
|
-
const directConnection = pick(requestAsUnknown, "connection");
|
|
133
|
-
if (directConnection !== undefined) candidateSockets.push(directConnection);
|
|
134
|
-
|
|
135
|
-
for (const candidate of candidateSockets) {
|
|
136
|
-
const parsed = SocketSchema.safeParse(candidate);
|
|
137
|
-
if (!parsed.success) continue;
|
|
138
|
-
const remotePort = parsed.data.remotePort;
|
|
139
|
-
if (remotePort === undefined || remotePort === null) continue;
|
|
140
|
-
return remotePort;
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
146
|
-
return value !== undefined && value !== null && typeof value === "object";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Read a property from an `unknown` without triggering lint rules that
|
|
150
|
-
* forbid type assertions. Returns `undefined` for non-object inputs or
|
|
151
|
-
* when the property is missing. */
|
|
152
|
-
function pick(value: unknown, key: string): unknown {
|
|
153
|
-
if (!isObject(value)) return undefined;
|
|
154
|
-
return value[key];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function parseCommandLine(
|
|
158
|
-
commandLine: string,
|
|
159
|
-
sep: string,
|
|
160
|
-
): { cwd: string | null; projectFolder: string | null } {
|
|
161
|
-
if (!commandLine) return { cwd: null, projectFolder: null };
|
|
162
|
-
const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
|
|
163
|
-
if (exeMatch?.[1] !== undefined) {
|
|
164
|
-
const exePath = exeMatch[1];
|
|
165
|
-
const lastSep = exePath.lastIndexOf(sep);
|
|
166
|
-
if (lastSep > 0) {
|
|
167
|
-
const exeDir = exePath.substring(0, lastSep);
|
|
168
|
-
return { cwd: exeDir, projectFolder: exeDir.split(sep).pop() ?? null };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return { cwd: null, projectFolder: null };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Get the working directory and command line for a process by PID.
|
|
176
|
-
*/
|
|
177
|
-
async function lookupProcessInfo(
|
|
178
|
-
pid: number,
|
|
179
|
-
): Promise<{ cwd: string | null; projectFolder: string | null }> {
|
|
180
|
-
const platform = process.platform;
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
if (platform === "win32") {
|
|
184
|
-
const psScript = [
|
|
185
|
-
`$proc = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}" -ErrorAction SilentlyContinue`,
|
|
186
|
-
`if ($proc) { Write-Output $proc.CommandLine }`,
|
|
187
|
-
].join("; ");
|
|
188
|
-
const { stdout } = await execFileAsync(
|
|
189
|
-
"powershell.exe",
|
|
190
|
-
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psScript],
|
|
191
|
-
{
|
|
192
|
-
windowsHide: true,
|
|
193
|
-
timeout: 3000,
|
|
194
|
-
maxBuffer: 64 * 1024,
|
|
195
|
-
},
|
|
196
|
-
);
|
|
197
|
-
const commandLine = stdout.trim();
|
|
198
|
-
if (commandLine.length > 0) return parseCommandLine(commandLine, "\\");
|
|
199
|
-
} else {
|
|
200
|
-
try {
|
|
201
|
-
const raw = await readFile(`/proc/${String(pid)}/cmdline`, "utf8");
|
|
202
|
-
const commandLine = raw.replace(/\0/g, " ").trim();
|
|
203
|
-
if (commandLine) {
|
|
204
|
-
return parseCommandLine(commandLine, "/");
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
// Fall back to ps
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (platform === "darwin") {
|
|
211
|
-
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "comm="], {
|
|
212
|
-
timeout: 3000,
|
|
213
|
-
maxBuffer: 64 * 1024,
|
|
214
|
-
});
|
|
215
|
-
const exePath = stdout.trim();
|
|
216
|
-
if (exePath) {
|
|
217
|
-
return parseCommandLine(exePath, "/");
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return { cwd: null, projectFolder: null };
|
|
222
|
-
} catch {
|
|
223
|
-
return { cwd: null, projectFolder: null };
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Scan the OS connection table for the freshest client鈫抪roxy connection and
|
|
229
|
-
* return the matching PID plus its cwd/projectFolder.
|
|
230
|
-
*/
|
|
231
|
-
async function lookupRecentClient(): Promise<ClientInfo> {
|
|
232
|
-
const port = getProxyPort();
|
|
233
|
-
const platform = process.platform;
|
|
234
|
-
try {
|
|
235
|
-
if (platform === "win32") {
|
|
236
|
-
// `Get-NetTCPConnection` can take several seconds on busy Windows
|
|
237
|
-
// machines, which makes direct-port clients (notably MiMo Code) lose
|
|
238
|
-
// their PID before the fallback completes. `netstat` streams quickly
|
|
239
|
-
// and includes the owning process id for established loopback rows.
|
|
240
|
-
const { stdout } = await execFileAsync("netstat.exe", ["-ano", "-p", "TCP"], {
|
|
241
|
-
windowsHide: true,
|
|
242
|
-
timeout: 2000,
|
|
243
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
244
|
-
});
|
|
245
|
-
const client = pickNewestClientFromWindowsNetstat(stdout.split("\n"), port);
|
|
246
|
-
if (client === null) {
|
|
247
|
-
return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
248
|
-
}
|
|
249
|
-
const { cwd, projectFolder } = await lookupProcessInfo(client.pid);
|
|
250
|
-
return {
|
|
251
|
-
port: client.localPort,
|
|
252
|
-
pid: client.pid,
|
|
253
|
-
cwd,
|
|
254
|
-
projectFolder,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Unix: netstat with -p prints `pid/program` next to each entry. Pick
|
|
259
|
-
// the row matching our proxy port on the remote side (i.e. the client's
|
|
260
|
-
// ephemeral port + the proxy's listen port). When multiple clients are
|
|
261
|
-
// connected we take the highest ephemeral port 鈥?Linux/macOS assign
|
|
262
|
-
// ports in ascending order, so the largest one is the freshest.
|
|
263
|
-
const lines = await readUnixEstablishedConnectionLines(port);
|
|
264
|
-
const clientPid = pickNewestClientPidFromNetstat(lines, port);
|
|
265
|
-
if (clientPid === null) {
|
|
266
|
-
return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
267
|
-
}
|
|
268
|
-
const { cwd, projectFolder } = await lookupProcessInfo(clientPid);
|
|
269
|
-
return { port: null, pid: clientPid, cwd, projectFolder };
|
|
270
|
-
} catch (err) {
|
|
271
|
-
logger.debug(`[socketTracker] Recent-client lookup failed for port ${port}:`, String(err));
|
|
272
|
-
return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** Parse the output of `netstat -tanp` (or `ss -tanp`) and return the PID
|
|
277
|
-
* on the most recently opened client-side connection to `proxyPort`. */
|
|
278
|
-
function pickNewestClientPidFromNetstat(lines: string[], proxyPort: number): number | null {
|
|
279
|
-
const proxyToken = `:${proxyPort}`;
|
|
280
|
-
type Row = { pid: number; localPort: number; order: number };
|
|
281
|
-
const candidates: Row[] = [];
|
|
282
|
-
let order = 0;
|
|
283
|
-
for (const line of lines) {
|
|
284
|
-
order++;
|
|
285
|
-
const cols = line.trim().split(/\s+/);
|
|
286
|
-
if (cols.length < 5) continue;
|
|
287
|
-
const localAddr = cols[3] ?? "";
|
|
288
|
-
const remoteAddr = cols[4] ?? "";
|
|
289
|
-
const pidField = cols[6] ?? "";
|
|
290
|
-
// Client-side rows: `RemoteAddress` (5th col) is the proxy's listen port;
|
|
291
|
-
// `LocalAddress` (4th col) is the client's ephemeral port.
|
|
292
|
-
if (!remoteAddr.endsWith(proxyToken)) continue;
|
|
293
|
-
const pidMatch = pidField.match(/^(\d+)/) ?? line.match(/pid=(\d+)/);
|
|
294
|
-
if (pidMatch === null) continue;
|
|
295
|
-
const pid = parseInt(pidMatch[1] ?? "", 10);
|
|
296
|
-
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
297
|
-
const localPortMatch = localAddr.match(/:(\d+)$/);
|
|
298
|
-
const localPort = localPortMatch !== null ? parseInt(localPortMatch[1] ?? "", 10) : 0;
|
|
299
|
-
candidates.push({ pid, localPort, order });
|
|
300
|
-
}
|
|
301
|
-
if (candidates.length === 0) return null;
|
|
302
|
-
candidates.sort((a, b) => b.localPort - a.localPort || b.order - a.order);
|
|
303
|
-
return candidates[0]?.pid ?? null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async function readUnixEstablishedConnectionLines(port: number): Promise<string[]> {
|
|
307
|
-
const commands = [
|
|
308
|
-
{ command: "netstat", args: ["-tanp"] },
|
|
309
|
-
{ command: "ss", args: ["-tanp"] },
|
|
310
|
-
];
|
|
311
|
-
for (const spec of commands) {
|
|
312
|
-
try {
|
|
313
|
-
const { stdout } = await execFileAsync(spec.command, spec.args, {
|
|
314
|
-
timeout: 2000,
|
|
315
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
316
|
-
});
|
|
317
|
-
const portToken = `:${port}`;
|
|
318
|
-
const lines = stdout
|
|
319
|
-
.trim()
|
|
320
|
-
.split("\n")
|
|
321
|
-
.filter((line) => line.includes(portToken) && line.includes("ESTAB"));
|
|
322
|
-
if (lines.length > 0) return lines;
|
|
323
|
-
} catch {
|
|
324
|
-
// Try the next socket table command.
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return [];
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function parseAddressPort(address: string): number | null {
|
|
331
|
-
const match = address.match(/:(\d+)$/);
|
|
332
|
-
if (match === null) return null;
|
|
333
|
-
const parsed = parseInt(match[1] ?? "", 10);
|
|
334
|
-
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65_535) return null;
|
|
335
|
-
return parsed;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function pickNewestClientFromWindowsNetstat(
|
|
339
|
-
lines: string[],
|
|
340
|
-
proxyPort: number,
|
|
341
|
-
): NetstatClient | null {
|
|
342
|
-
const proxyToken = `:${proxyPort}`;
|
|
343
|
-
type Row = NetstatClient & { order: number };
|
|
344
|
-
const candidates: Row[] = [];
|
|
345
|
-
let order = 0;
|
|
346
|
-
|
|
347
|
-
for (const line of lines) {
|
|
348
|
-
order++;
|
|
349
|
-
const cols = line.trim().split(/\s+/);
|
|
350
|
-
if (cols.length < 5) continue;
|
|
351
|
-
const localAddr = cols[1] ?? "";
|
|
352
|
-
const remoteAddr = cols[2] ?? "";
|
|
353
|
-
const state = cols[3] ?? "";
|
|
354
|
-
const pidStr = cols[4] ?? "";
|
|
355
|
-
if (state !== "ESTABLISHED") continue;
|
|
356
|
-
if (!remoteAddr.endsWith(proxyToken)) continue;
|
|
357
|
-
|
|
358
|
-
const localPort = parseAddressPort(localAddr);
|
|
359
|
-
if (localPort === null || localPort === proxyPort) continue;
|
|
360
|
-
|
|
361
|
-
const pid = parseInt(pidStr, 10);
|
|
362
|
-
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
363
|
-
candidates.push({ pid, localPort, order });
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (candidates.length === 0) return null;
|
|
367
|
-
candidates.sort((a, b) => b.localPort - a.localPort || b.order - a.order);
|
|
368
|
-
const selected = candidates[0];
|
|
369
|
-
if (selected === undefined) return null;
|
|
370
|
-
return { pid: selected.pid, localPort: selected.localPort };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export function _pickNewestClientFromWindowsNetstatForTests(
|
|
374
|
-
lines: string[],
|
|
375
|
-
proxyPort: number,
|
|
376
|
-
): NetstatClient | null {
|
|
377
|
-
return pickNewestClientFromWindowsNetstat(lines, proxyPort);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
export function _pickNewestClientPidFromNetstatForTests(
|
|
381
|
-
lines: string[],
|
|
382
|
-
proxyPort: number,
|
|
383
|
-
): number | null {
|
|
384
|
-
return pickNewestClientPidFromNetstat(lines, proxyPort);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** Custom request headers the client (or a wrapper script) may set so we
|
|
388
|
-
* can identify the originating process without scanning the OS connection
|
|
389
|
-
* table. Recognized names:
|
|
390
|
-
* - `X-Agent-Inspector-Client-Pid`
|
|
391
|
-
* - `X-Agent-Inspector-Client-Port`
|
|
392
|
-
* - `X-Agent-Inspector-Client-Cwd`
|
|
393
|
-
* - `X-Agent-Inspector-Client-Project-Folder`
|
|
394
|
-
* Clients should set these via a small launcher script that exports the
|
|
395
|
-
* values from `$$` and `pwd` before invoking the AI tool, e.g.:
|
|
396
|
-
*
|
|
397
|
-
* curl -H "X-Agent-Inspector-Client-Pid: $$" \
|
|
398
|
-
* -H "X-Agent-Inspector-Client-Cwd: $(pwd)" ...
|
|
399
|
-
*/
|
|
400
|
-
const CLIENT_HEADERS = {
|
|
401
|
-
pid: "x-agent-inspector-client-pid",
|
|
402
|
-
port: "x-agent-inspector-client-port",
|
|
403
|
-
cwd: "x-agent-inspector-client-cwd",
|
|
404
|
-
projectFolder: "x-agent-inspector-client-project-folder",
|
|
405
|
-
} as const;
|
|
406
|
-
|
|
407
|
-
/** Try to read a positive-integer value out of a header. Returns `null` for
|
|
408
|
-
* missing / malformed values. */
|
|
409
|
-
function readIntHeader(headers: Headers, name: string): number | null {
|
|
410
|
-
const raw = headers.get(name);
|
|
411
|
-
if (raw === null || raw === "") return null;
|
|
412
|
-
const parsed = parseInt(raw, 10);
|
|
413
|
-
if (!Number.isInteger(parsed) || parsed < 0) return null;
|
|
414
|
-
return parsed;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** Read client-supplied headers. Returns `null` when no PID header is
|
|
418
|
-
* present 鈥?caller falls back to OS scanning. */
|
|
419
|
-
function readClientHeaders(request: Request): ClientInfo | null {
|
|
420
|
-
const pid = readIntHeader(request.headers, CLIENT_HEADERS.pid);
|
|
421
|
-
if (pid === null || pid === 0) return null;
|
|
422
|
-
const port = readIntHeader(request.headers, CLIENT_HEADERS.port);
|
|
423
|
-
const cwd = request.headers.get(CLIENT_HEADERS.cwd);
|
|
424
|
-
const projectFolder = request.headers.get(CLIENT_HEADERS.projectFolder);
|
|
425
|
-
return {
|
|
426
|
-
pid,
|
|
427
|
-
port: port !== null && port > 0 ? port : null,
|
|
428
|
-
cwd: cwd !== null && cwd !== "" ? cwd : null,
|
|
429
|
-
projectFolder: projectFolder !== null && projectFolder !== "" ? projectFolder : null,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Get client info (PID, CWD, project folder) for the request that triggered
|
|
435
|
-
* this call. The TanStack Start fetch-API `Request` does not expose the
|
|
436
|
-
* underlying TCP socket (no `request.socket`, no `event.node.req`), so we
|
|
437
|
-
* cannot derive the per-request client port from the request itself.
|
|
438
|
-
*
|
|
439
|
-
* Strategy: enumerate the OS-level TCP connections to our proxy's listen
|
|
440
|
-
* port and pick the most recently established client-side connection. This
|
|
441
|
-
* is essentially "who is currently talking to us" 鈥?for a session with many
|
|
442
|
-
* rapid-fire requests the same client holds a long-lived connection, so
|
|
443
|
-
* consecutive lookups within the cache TTL converge on the same PID.
|
|
444
|
-
*
|
|
445
|
-
* Deduplicates concurrent lookups with a single in-flight promise. Results
|
|
446
|
-
* are cached by PID for `RECENT_CLIENT_TTL_MS` to avoid spawning a
|
|
447
|
-
* `Get-NetTCPConnection` / `ss` subprocess on every single request.
|
|
448
|
-
*/
|
|
449
|
-
export async function getClientInfo(request: Request): Promise<ClientInfo> {
|
|
450
|
-
// Preferred path: explicit client identity in headers. AI coding tools
|
|
451
|
-
// (or the wrapper scripts that launch them) attach
|
|
452
|
-
// `X-Agent-Inspector-Client-Pid` (and optionally 鈥?Cwd /
|
|
453
|
-
// 鈥?Project-Folder / 鈥?Port) so we never have to guess.
|
|
454
|
-
const fromHeaders = readClientHeaders(request);
|
|
455
|
-
if (fromHeaders !== null) return fromHeaders;
|
|
456
|
-
|
|
457
|
-
const remotePort = extractRemotePort(request);
|
|
458
|
-
if (remotePort !== null) {
|
|
459
|
-
const cachedPort = getFromCache(remotePort);
|
|
460
|
-
if (cachedPort !== null) return cachedPort;
|
|
461
|
-
|
|
462
|
-
const existingPortLookup = inflight.get(remotePort);
|
|
463
|
-
if (existingPortLookup !== undefined) return existingPortLookup;
|
|
464
|
-
|
|
465
|
-
const portLookup = lookupClientByPort(remotePort)
|
|
466
|
-
.then((info) => {
|
|
467
|
-
setCache(remotePort, info);
|
|
468
|
-
inflight.delete(remotePort);
|
|
469
|
-
return info;
|
|
470
|
-
})
|
|
471
|
-
.catch((err) => {
|
|
472
|
-
inflight.delete(remotePort);
|
|
473
|
-
logger.debug(`[socketTracker] Remote-port lookup failed for ${remotePort}:`, String(err));
|
|
474
|
-
const fallback: ClientInfo = {
|
|
475
|
-
port: remotePort,
|
|
476
|
-
pid: null,
|
|
477
|
-
cwd: null,
|
|
478
|
-
projectFolder: null,
|
|
479
|
-
};
|
|
480
|
-
setCache(remotePort, fallback);
|
|
481
|
-
return fallback;
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
inflight.set(remotePort, portLookup);
|
|
485
|
-
return portLookup;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Fallback: scan the OS connection table for the most recently opened
|
|
489
|
-
// client鈫抪roxy connection. Best-effort 鈥?in environments with many
|
|
490
|
-
// long-lived keep-alive connections (browsers, background processes) the
|
|
491
|
-
// picked connection may not belong to the current request.
|
|
492
|
-
const cached = getRecentClientFromCache();
|
|
493
|
-
if (cached !== null) return cached;
|
|
494
|
-
|
|
495
|
-
const existing = recentInflight;
|
|
496
|
-
if (existing) return existing;
|
|
497
|
-
|
|
498
|
-
const promise = lookupRecentClient()
|
|
499
|
-
.then((info) => {
|
|
500
|
-
setRecentClientCache(info);
|
|
501
|
-
recentInflight = null;
|
|
502
|
-
return info;
|
|
503
|
-
})
|
|
504
|
-
.catch((err) => {
|
|
505
|
-
recentInflight = null;
|
|
506
|
-
logger.debug(`[socketTracker] Recent-client lookup failed:`, String(err));
|
|
507
|
-
return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
recentInflight = promise;
|
|
511
|
-
return promise;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Lookup the PID owning a specific client-side local port, then enrich it
|
|
516
|
-
* with the process's cwd / project folder. Used by the identity-injecting
|
|
517
|
-
* native HTTP proxy in `identityProxy.ts`, which has access to
|
|
518
|
-
* `req.socket.remotePort` (the client's ephemeral port) and needs to know
|
|
519
|
-
* which process opened the connection.
|
|
520
|
-
*
|
|
521
|
-
* Results are cached by `clientPort` for `PORT_LOOKUP_TTL_MS` 鈥?a single
|
|
522
|
-
* keep-alive connection typically reuses the same client port for many
|
|
523
|
-
* requests, so we don't want to spawn a subprocess per request.
|
|
524
|
-
*/
|
|
525
|
-
const PORT_LOOKUP_TTL_MS = 5 * 60 * 1000;
|
|
526
|
-
const portInfoCache = new Map<number, { info: ClientInfo; expiresAt: number }>();
|
|
527
|
-
|
|
528
|
-
export async function lookupClientByPort(clientPort: number): Promise<ClientInfo> {
|
|
529
|
-
if (!Number.isInteger(clientPort) || clientPort <= 0 || clientPort > 65_535) {
|
|
530
|
-
return { port: null, pid: null, cwd: null, projectFolder: null };
|
|
531
|
-
}
|
|
532
|
-
const cached = portInfoCache.get(clientPort);
|
|
533
|
-
if (cached !== undefined && Date.now() < cached.expiresAt) {
|
|
534
|
-
return cached.info;
|
|
535
|
-
}
|
|
536
|
-
try {
|
|
537
|
-
const pid = await lookupPidByLocalPort(clientPort);
|
|
538
|
-
if (pid === null) {
|
|
539
|
-
const fallback: ClientInfo = {
|
|
540
|
-
port: clientPort,
|
|
541
|
-
pid: null,
|
|
542
|
-
cwd: null,
|
|
543
|
-
projectFolder: null,
|
|
544
|
-
};
|
|
545
|
-
portInfoCache.set(clientPort, { info: fallback, expiresAt: Date.now() + PORT_LOOKUP_TTL_MS });
|
|
546
|
-
return fallback;
|
|
547
|
-
}
|
|
548
|
-
const { cwd, projectFolder } = await lookupProcessInfo(pid);
|
|
549
|
-
const info: ClientInfo = { port: clientPort, pid, cwd, projectFolder };
|
|
550
|
-
portInfoCache.set(clientPort, { info, expiresAt: Date.now() + PORT_LOOKUP_TTL_MS });
|
|
551
|
-
return info;
|
|
552
|
-
} catch (err) {
|
|
553
|
-
logger.debug(`[socketTracker] lookupClientByPort(${clientPort}) failed:`, String(err));
|
|
554
|
-
return { port: clientPort, pid: null, cwd: null, projectFolder: null };
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/** Returns the PID owning `clientPort` on the local machine, or `null`
|
|
559
|
-
* if the port isn't held by any known process. Cross-platform wrapper
|
|
560
|
-
* around the OS-specific connection table. */
|
|
561
|
-
async function lookupPidByLocalPort(clientPort: number): Promise<number | null> {
|
|
562
|
-
const platform = process.platform;
|
|
563
|
-
try {
|
|
564
|
-
if (platform === "win32") {
|
|
565
|
-
// `Get-NetTCPConnection -LocalPort` can enumerate the entire socket table
|
|
566
|
-
// before filtering on busy machines. `netstat.exe` streams quickly and
|
|
567
|
-
// avoids PowerShell process startup for this hot lookup path.
|
|
568
|
-
//
|
|
569
|
-
// `netstat -ano` lists every TCP connection twice: once for each endpoint.
|
|
570
|
-
//
|
|
571
|
-
// Proto LocalAddress RemoteAddress State PID
|
|
572
|
-
// TCP [::1]:25949 [::1]:55193 ESTABLISHED 10936 鈫?server-side row (proxy owns the listener)
|
|
573
|
-
// TCP [::1]:55193 [::1]:25949 ESTABLISHED 29356 鈫?client-side row (curl owns the ephemeral port)
|
|
574
|
-
//
|
|
575
|
-
// For `clientPort` (the client's ephemeral port) we want the row
|
|
576
|
-
// whose LOCAL column ends with `:<clientPort>` 鈥?that's the row
|
|
577
|
-
// attributed to the client process.
|
|
578
|
-
const { stdout } = await execFileAsync("netstat.exe", ["-ano", "-p", "TCP"], {
|
|
579
|
-
windowsHide: true,
|
|
580
|
-
timeout: 2000,
|
|
581
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
582
|
-
});
|
|
583
|
-
const clientToken = `:${clientPort}`;
|
|
584
|
-
for (const rawLine of stdout.split("\n")) {
|
|
585
|
-
const line = rawLine.trim();
|
|
586
|
-
if (line === "") continue;
|
|
587
|
-
const cols = line.split(/\s+/);
|
|
588
|
-
if (cols.length < 5) continue;
|
|
589
|
-
const localAddr = cols[1] ?? "";
|
|
590
|
-
const state = cols[3] ?? "";
|
|
591
|
-
if (state !== "ESTABLISHED") continue;
|
|
592
|
-
if (!localAddr.endsWith(clientToken)) continue;
|
|
593
|
-
const pidStr = cols[cols.length - 1] ?? "";
|
|
594
|
-
const parsed = parseInt(pidStr, 10);
|
|
595
|
-
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
596
|
-
}
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
// Unix: try `ss` first, fall back to `lsof`. Both need root or
|
|
600
|
-
// CAP_NET_ADMIN to see PIDs of processes owned by other users; when
|
|
601
|
-
// permission is denied the command exits non-zero and we return null.
|
|
602
|
-
try {
|
|
603
|
-
const { stdout } = await execFileAsync(
|
|
604
|
-
"ss",
|
|
605
|
-
["-tanp", "state", "established", "sport", `= :${clientPort}`],
|
|
606
|
-
{
|
|
607
|
-
timeout: 2000,
|
|
608
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
609
|
-
},
|
|
610
|
-
);
|
|
611
|
-
const match = stdout.match(/pid=(\d+)/);
|
|
612
|
-
if (match !== null) {
|
|
613
|
-
const parsed = parseInt(match[1] ?? "", 10);
|
|
614
|
-
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
615
|
-
}
|
|
616
|
-
} catch {
|
|
617
|
-
// fall through
|
|
618
|
-
}
|
|
619
|
-
try {
|
|
620
|
-
const { stdout } = await execFileAsync(
|
|
621
|
-
"lsof",
|
|
622
|
-
["-ti", `tcp:${clientPort}`, "-sTCP:ESTABLISHED"],
|
|
623
|
-
{
|
|
624
|
-
timeout: 2000,
|
|
625
|
-
maxBuffer: 64 * 1024,
|
|
626
|
-
},
|
|
627
|
-
);
|
|
628
|
-
const trimmed = stdout.trim().split("\n")[0] ?? "";
|
|
629
|
-
const parsed = parseInt(trimmed, 10);
|
|
630
|
-
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
631
|
-
} catch {
|
|
632
|
-
// fall through
|
|
633
|
-
}
|
|
634
|
-
return null;
|
|
635
|
-
} catch (err) {
|
|
636
|
-
logger.debug(`[socketTracker] lookupPidByLocalPort(${clientPort}) failed:`, String(err));
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
}
|