@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
package/src/mcp/toolHandlers.ts
DELETED
|
@@ -1,1533 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool handler implementations for the MCP server. These are split out from
|
|
3
|
-
* `server.ts` so the wire-level MCP registration stays thin.
|
|
4
|
-
*
|
|
5
|
-
* Log/provider/knowledge handlers still use an injected `callApi` while those
|
|
6
|
-
* domains are being migrated. Run/group/evidence handlers call the shared
|
|
7
|
-
* service layer directly so MCP and REST API are peers instead of MCP looping
|
|
8
|
-
* back through `/api/*`.
|
|
9
|
-
*
|
|
10
|
-
* Each impl returns a `CallToolResult`-shaped object: either
|
|
11
|
-
* `{ content: [{ type: "text", text: string }] }` on success or
|
|
12
|
-
* `{ content: [...], isError: true }` on failure. The wrapping
|
|
13
|
-
* `safeCall` in `server.ts` only catches *thrown* errors (loopback
|
|
14
|
-
* timeouts, etc); semantic errors (404 from /api) are returned as
|
|
15
|
-
* `isError: true` results from inside the impl.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { Buffer } from "node:buffer";
|
|
19
|
-
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { dirname, join } from "node:path";
|
|
21
|
-
import JSZip from "jszip";
|
|
22
|
-
import { z } from "zod";
|
|
23
|
-
import { getPublicPort } from "../lib/publicBasePathContext";
|
|
24
|
-
import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
|
|
25
|
-
import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
|
|
26
|
-
import {
|
|
27
|
-
getCurrentContext,
|
|
28
|
-
setCurrentContext,
|
|
29
|
-
type InspectorCurrentContextPatch,
|
|
30
|
-
} from "./currentContext";
|
|
31
|
-
import {
|
|
32
|
-
type AddInspectorGroupSessionInput,
|
|
33
|
-
type CreateInspectorGroupInput,
|
|
34
|
-
type DeleteInspectorGroupOptions,
|
|
35
|
-
type UpdateInspectorGroupInput,
|
|
36
|
-
} from "../lib/groupContract";
|
|
37
|
-
import { type CreateInspectorRunInput, type UpdateInspectorRunInput } from "../lib/runContract";
|
|
38
|
-
import { SessionInfoSchema } from "../lib/sessionInfoContract";
|
|
39
|
-
import {
|
|
40
|
-
ProviderConfigSchema,
|
|
41
|
-
toRedactedProviderDto,
|
|
42
|
-
toRedactedProviderDtos,
|
|
43
|
-
} from "../lib/providerContract";
|
|
44
|
-
import { isRecord, textByteLength } from "../lib/unknown";
|
|
45
|
-
import { CapturedLogSchema, type CapturedLog } from "../contracts";
|
|
46
|
-
import {
|
|
47
|
-
createResourceLimitError,
|
|
48
|
-
MAX_PROVIDER_MODELS,
|
|
49
|
-
MAX_SEARCH_PROJECT_CHARS,
|
|
50
|
-
MAX_SEARCH_QUERY_CHARS,
|
|
51
|
-
type ResourceLimitErrorInput,
|
|
52
|
-
} from "../lib/resourceLimits";
|
|
53
|
-
import {
|
|
54
|
-
addInspectorGroupSession,
|
|
55
|
-
createInspectorGroup,
|
|
56
|
-
deleteInspectorGroup,
|
|
57
|
-
exportInspectorGroupEvidence,
|
|
58
|
-
getInspectorGroup,
|
|
59
|
-
listInspectorGroups,
|
|
60
|
-
readInspectorGroupEvidence,
|
|
61
|
-
updateInspectorGroup,
|
|
62
|
-
} from "../services/groups";
|
|
63
|
-
import {
|
|
64
|
-
createInspectorRun,
|
|
65
|
-
exportInspectorRunEvidence,
|
|
66
|
-
getInspectorRun,
|
|
67
|
-
listInspectorRecentFailures,
|
|
68
|
-
readInspectorRunEvidence,
|
|
69
|
-
updateInspectorRun,
|
|
70
|
-
} from "../services/runs";
|
|
71
|
-
|
|
72
|
-
/** Minimal callApi contract: same shape as the real `callApi` in loopback.ts. */
|
|
73
|
-
export type CallApiFn = (path: string, options?: CallApiOptions) => Promise<Response>;
|
|
74
|
-
|
|
75
|
-
export type ToolResult = {
|
|
76
|
-
content: { type: "text"; text: string }[];
|
|
77
|
-
isError?: true;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Loose shape returned by `GET /api/logs`: `logs: CapturedLog[]` and a few
|
|
81
|
-
// pagination fields. The CapturedLog items are fully validated downstream.
|
|
82
|
-
const LogsListResponseSchema = z.object({
|
|
83
|
-
logs: z.array(CapturedLogSchema).optional(),
|
|
84
|
-
total: z.number().optional(),
|
|
85
|
-
offset: z.number().optional(),
|
|
86
|
-
limit: z.number().optional(),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const SearchLogsResponseSchema = z.object({
|
|
90
|
-
query: z.string(),
|
|
91
|
-
logs: z.array(CapturedLogSchema),
|
|
92
|
-
total: z.number(),
|
|
93
|
-
offset: z.number(),
|
|
94
|
-
limit: z.number(),
|
|
95
|
-
scanned: z.number().optional(),
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const PAGINATION_DEFAULTS = { offset: 0, limit: 3 };
|
|
99
|
-
export const LIMIT_HARD_CAP = 5;
|
|
100
|
-
const LOG_DETAIL_TEXT_LIMIT = 200_000;
|
|
101
|
-
const PROCESS_LOG_LIMIT_DEFAULT = 50;
|
|
102
|
-
const PROCESS_LOG_LIMIT_MAX = 200;
|
|
103
|
-
|
|
104
|
-
export function clampLimit(input: number | undefined): number {
|
|
105
|
-
if (input === undefined) return PAGINATION_DEFAULTS.limit;
|
|
106
|
-
if (!Number.isFinite(input) || input < 1) return 1;
|
|
107
|
-
return Math.min(Math.floor(input), LIMIT_HARD_CAP);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function clampProcessLogLimit(input: number | undefined): number {
|
|
111
|
-
if (input === undefined) return PROCESS_LOG_LIMIT_DEFAULT;
|
|
112
|
-
if (!Number.isFinite(input) || input < 1) return 1;
|
|
113
|
-
return Math.min(Math.floor(input), PROCESS_LOG_LIMIT_MAX);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function textJson(data: unknown): ToolResult {
|
|
117
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function toolError(message: string): ToolResult {
|
|
121
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function resourceLimitToolError(input: ResourceLimitErrorInput): ToolResult {
|
|
125
|
-
return toolError(JSON.stringify(createResourceLimitError(input), null, 2));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function redactedProviderResult(value: unknown): ToolResult {
|
|
129
|
-
const parsed = ProviderConfigSchema.safeParse(value);
|
|
130
|
-
if (!parsed.success) return toolError("Provider API returned an invalid provider response");
|
|
131
|
-
return textJson(toRedactedProviderDto(parsed.data));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function redactedProvidersResult(value: unknown): ToolResult {
|
|
135
|
-
const parsed = ProviderConfigSchema.array().safeParse(value);
|
|
136
|
-
if (!parsed.success) return toolError("Provider API returned an invalid providers response");
|
|
137
|
-
return textJson(toRedactedProviderDtos(parsed.data));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function inspectorBaseUrl(): string {
|
|
141
|
-
return `http://127.0.0.1:${String(getPublicPort())}`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function hasBodyText(value: string | null, byteLength: number | null | undefined): boolean {
|
|
145
|
-
if (value !== null && value.length > 0) return true;
|
|
146
|
-
return byteLength !== undefined && byteLength !== null && byteLength > 0;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function truncateToolText(value: string | null): {
|
|
150
|
-
value: string | null;
|
|
151
|
-
byteLength: number | null;
|
|
152
|
-
truncated: boolean;
|
|
153
|
-
} {
|
|
154
|
-
if (value === null) return { value: null, byteLength: null, truncated: false };
|
|
155
|
-
const byteLength = textByteLength(value);
|
|
156
|
-
if (value.length <= LOG_DETAIL_TEXT_LIMIT) {
|
|
157
|
-
return { value, byteLength, truncated: false };
|
|
158
|
-
}
|
|
159
|
-
const suffix = `\n[truncated by agent-inspector MCP: original ${byteLength ?? 0} bytes]`;
|
|
160
|
-
return {
|
|
161
|
-
value: `${value.slice(0, LOG_DETAIL_TEXT_LIMIT)}${suffix}`,
|
|
162
|
-
byteLength,
|
|
163
|
-
truncated: true,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function buildBoundedLogDetail(log: CapturedLog): Record<string, unknown> {
|
|
168
|
-
const { streamingChunks: _chunks, rawRequestBody, responseText, ...rest } = log;
|
|
169
|
-
const boundedRequest = truncateToolText(rawRequestBody);
|
|
170
|
-
const boundedResponse = truncateToolText(responseText);
|
|
171
|
-
return {
|
|
172
|
-
...rest,
|
|
173
|
-
rawRequestBody: boundedRequest.value,
|
|
174
|
-
responseText: boundedResponse.value,
|
|
175
|
-
rawRequestBodyBytes: boundedRequest.byteLength,
|
|
176
|
-
responseTextBytes: boundedResponse.byteLength,
|
|
177
|
-
rawRequestBodyTruncated: boundedRequest.truncated,
|
|
178
|
-
responseTextTruncated: boundedResponse.truncated,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function logHasError(log: CapturedLog): boolean {
|
|
183
|
-
return (
|
|
184
|
-
(log.error !== null && log.error !== undefined && log.error.length > 0) ||
|
|
185
|
-
(log.responseStatus !== null && log.responseStatus !== undefined && log.responseStatus >= 400)
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function buildLogSummary(log: CapturedLog) {
|
|
190
|
-
const hasError = logHasError(log);
|
|
191
|
-
const rawRequestBodyBytes = log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody);
|
|
192
|
-
const responseTextBytes = log.responseTextBytes ?? textByteLength(log.responseText);
|
|
193
|
-
return {
|
|
194
|
-
id: log.id,
|
|
195
|
-
timestamp: log.timestamp,
|
|
196
|
-
provider: log.providerName ?? null,
|
|
197
|
-
model: log.model,
|
|
198
|
-
apiFormat: log.apiFormat,
|
|
199
|
-
method: log.method,
|
|
200
|
-
path: log.path,
|
|
201
|
-
status: log.responseStatus,
|
|
202
|
-
isStreaming: log.streaming,
|
|
203
|
-
hasError,
|
|
204
|
-
latencyMs: log.elapsedMs,
|
|
205
|
-
firstChunkMs: log.firstChunkMs ?? null,
|
|
206
|
-
totalStreamMs: log.totalStreamMs ?? null,
|
|
207
|
-
tokensPerSecond: log.tokensPerSecond ?? null,
|
|
208
|
-
tokens: {
|
|
209
|
-
input: log.inputTokens,
|
|
210
|
-
output: log.outputTokens,
|
|
211
|
-
cacheCreate: log.cacheCreationInputTokens,
|
|
212
|
-
cacheRead: log.cacheReadInputTokens,
|
|
213
|
-
},
|
|
214
|
-
sessionId: log.sessionId,
|
|
215
|
-
userAgent: log.userAgent ?? null,
|
|
216
|
-
clientPid: log.clientPid ?? null,
|
|
217
|
-
clientCwd: log.clientCwd ?? null,
|
|
218
|
-
clientProjectFolder: log.clientProjectFolder ?? null,
|
|
219
|
-
lastUserMessagePreview: extractLastUserMessagePreview(log),
|
|
220
|
-
responsePreview: extractResponsePreview(log),
|
|
221
|
-
hasChunks:
|
|
222
|
-
(log.streamingChunks !== undefined && log.streamingChunks !== null) ||
|
|
223
|
-
(log.streamingChunksPath !== null && log.streamingChunksPath !== undefined),
|
|
224
|
-
hasRawRequestBody: hasBodyText(log.rawRequestBody, rawRequestBodyBytes),
|
|
225
|
-
rawRequestBodyBytes,
|
|
226
|
-
responseTextBytes,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function sortedUniqueStrings(values: Array<string | null | undefined>): string[] {
|
|
231
|
-
return [
|
|
232
|
-
...new Set(
|
|
233
|
-
values.filter(
|
|
234
|
-
(value): value is string => value !== null && value !== undefined && value !== "",
|
|
235
|
-
),
|
|
236
|
-
),
|
|
237
|
-
].sort((left, right) => left.localeCompare(right));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function sortedUniqueNumbers(values: Array<number | null | undefined>): number[] {
|
|
241
|
-
return [
|
|
242
|
-
...new Set(
|
|
243
|
-
values.filter(
|
|
244
|
-
(value): value is number => value !== null && value !== undefined && Number.isFinite(value),
|
|
245
|
-
),
|
|
246
|
-
),
|
|
247
|
-
].sort((left, right) => left - right);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function inferAgentFromUserAgent(userAgent: string | null | undefined): string | null {
|
|
251
|
-
if (userAgent === null || userAgent === undefined) return null;
|
|
252
|
-
const lower = userAgent.toLowerCase();
|
|
253
|
-
if (lower.includes("codex")) return "Codex";
|
|
254
|
-
if (lower.includes("opencode")) return "OpenCode";
|
|
255
|
-
if (lower.includes("claude")) return "Claude Code";
|
|
256
|
-
if (lower.includes("mimocode")) return "MiMoCode";
|
|
257
|
-
if (lower.includes("ai-sdk")) return "AI SDK";
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function processSummary(
|
|
262
|
-
clientPid: number,
|
|
263
|
-
logs: readonly CapturedLog[],
|
|
264
|
-
total: number,
|
|
265
|
-
limit: number,
|
|
266
|
-
): Record<string, unknown> {
|
|
267
|
-
const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
|
|
268
|
-
const first = sortedLogs[0];
|
|
269
|
-
const last = sortedLogs[sortedLogs.length - 1];
|
|
270
|
-
const userAgents = sortedUniqueStrings(sortedLogs.map((log) => log.userAgent));
|
|
271
|
-
const agents = sortedUniqueStrings(userAgents.map(inferAgentFromUserAgent));
|
|
272
|
-
const sessions = sortedUniqueStrings(sortedLogs.map((log) => log.sessionId));
|
|
273
|
-
return {
|
|
274
|
-
clientPid,
|
|
275
|
-
total,
|
|
276
|
-
limit,
|
|
277
|
-
logCount: sortedLogs.length,
|
|
278
|
-
sessionCount: sessions.length,
|
|
279
|
-
sessions,
|
|
280
|
-
agents,
|
|
281
|
-
userAgents,
|
|
282
|
-
clientPorts: sortedUniqueNumbers(sortedLogs.map((log) => log.clientPort)),
|
|
283
|
-
clientCwds: sortedUniqueStrings(sortedLogs.map((log) => log.clientCwd)),
|
|
284
|
-
clientProjectFolders: sortedUniqueStrings(sortedLogs.map((log) => log.clientProjectFolder)),
|
|
285
|
-
models: sortedUniqueStrings(sortedLogs.map((log) => log.model)),
|
|
286
|
-
providers: sortedUniqueStrings(sortedLogs.map((log) => log.providerName)),
|
|
287
|
-
errorCount: sortedLogs.filter(logHasError).length,
|
|
288
|
-
startedAt: first?.timestamp ?? null,
|
|
289
|
-
updatedAt: last?.timestamp ?? null,
|
|
290
|
-
latestLogs: sortedLogs.slice(-Math.min(10, sortedLogs.length)).reverse().map(buildLogSummary),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function readCompactLogsPage(
|
|
295
|
-
callApi: CallApiFn,
|
|
296
|
-
params: URLSearchParams,
|
|
297
|
-
): Promise<{ logs: CapturedLog[]; total: number } | ToolResult> {
|
|
298
|
-
const path = `/api/logs?${params.toString()}`;
|
|
299
|
-
const parsed = await fetchApiJson(callApi, path, LogsListResponseSchema);
|
|
300
|
-
if (!parsed.ok) return parsed.error;
|
|
301
|
-
const logs = parsed.data.logs ?? [];
|
|
302
|
-
return { logs, total: parsed.data.total ?? logs.length };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Fetches a JSON endpoint and validates it against a Zod schema in one call.
|
|
307
|
-
* On HTTP error or schema mismatch returns a `ToolResult` error so the caller
|
|
308
|
-
* can propagate it directly. On success returns the typed parsed value.
|
|
309
|
-
*/
|
|
310
|
-
type FetchApiResult<T> = { ok: true; data: T } | { ok: false; error: ToolResult };
|
|
311
|
-
|
|
312
|
-
async function fetchApiJson<T>(
|
|
313
|
-
callApi: CallApiFn,
|
|
314
|
-
path: string,
|
|
315
|
-
schema: z.ZodType<T>,
|
|
316
|
-
init?: RequestInit,
|
|
317
|
-
): Promise<FetchApiResult<T>> {
|
|
318
|
-
const res = await callApi(path, init);
|
|
319
|
-
if (!res.ok) {
|
|
320
|
-
return { ok: false, error: toolError(`${methodFor(init)} ${path} returned ${res.status}`) };
|
|
321
|
-
}
|
|
322
|
-
const rawBody: unknown = await res.json();
|
|
323
|
-
const parsed = schema.safeParse(rawBody);
|
|
324
|
-
if (!parsed.success) {
|
|
325
|
-
return {
|
|
326
|
-
ok: false,
|
|
327
|
-
error: toolError(`${methodFor(init)} ${path} returned an unparseable shape`),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
return { ok: true, data: parsed.data };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function methodFor(init: RequestInit | undefined): string {
|
|
334
|
-
return (init?.method ?? "GET").toUpperCase();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function isToolResult(value: unknown): value is ToolResult {
|
|
338
|
-
return (
|
|
339
|
-
typeof value === "object" &&
|
|
340
|
-
value !== null &&
|
|
341
|
-
Object.getOwnPropertyDescriptor(value, "content") !== undefined
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ===========================================================================
|
|
346
|
-
// Per-tool impls
|
|
347
|
-
// ===========================================================================
|
|
348
|
-
|
|
349
|
-
export type ListLogsArgs = {
|
|
350
|
-
offset?: number;
|
|
351
|
-
limit?: number;
|
|
352
|
-
sessionId?: string;
|
|
353
|
-
model?: string;
|
|
354
|
-
clientPid?: number;
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
export async function listLogsImpl(callApi: CallApiFn, args: ListLogsArgs): Promise<ToolResult> {
|
|
358
|
-
if (
|
|
359
|
-
args.limit !== undefined &&
|
|
360
|
-
(!Number.isSafeInteger(args.limit) || args.limit < 1 || args.limit > LIMIT_HARD_CAP)
|
|
361
|
-
) {
|
|
362
|
-
return resourceLimitToolError({
|
|
363
|
-
error: `MCP log pages may contain at most ${String(LIMIT_HARD_CAP)} entries.`,
|
|
364
|
-
code: "invalid-page-request",
|
|
365
|
-
status: 400,
|
|
366
|
-
resource: "mcp-log-page-size",
|
|
367
|
-
limit: LIMIT_HARD_CAP,
|
|
368
|
-
retryable: false,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
const offset = args.offset ?? PAGINATION_DEFAULTS.offset;
|
|
372
|
-
const limit = clampLimit(args.limit);
|
|
373
|
-
const params = new URLSearchParams({
|
|
374
|
-
offset: String(offset),
|
|
375
|
-
limit: String(limit),
|
|
376
|
-
compact: "1",
|
|
377
|
-
});
|
|
378
|
-
if (args.sessionId !== undefined && args.sessionId.length > 0) {
|
|
379
|
-
params.set("sessionId", args.sessionId);
|
|
380
|
-
}
|
|
381
|
-
if (args.model !== undefined && args.model.length > 0) {
|
|
382
|
-
params.set("model", args.model);
|
|
383
|
-
}
|
|
384
|
-
if (args.clientPid !== undefined) {
|
|
385
|
-
params.set("clientPid", String(args.clientPid));
|
|
386
|
-
}
|
|
387
|
-
const res = await callApi(`/api/logs?${params.toString()}`);
|
|
388
|
-
if (!res.ok) return toolError(`GET /api/logs returned ${res.status}`);
|
|
389
|
-
const rawBody: unknown = await res.json();
|
|
390
|
-
const parsed = LogsListResponseSchema.safeParse(rawBody);
|
|
391
|
-
if (!parsed.success) return toolError("GET /api/logs returned an unparseable shape");
|
|
392
|
-
const logs = parsed.data.logs ?? [];
|
|
393
|
-
return textJson(logs.map(buildLogSummary));
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export type SearchLogsArgs = {
|
|
397
|
-
query?: string;
|
|
398
|
-
offset?: number;
|
|
399
|
-
limit?: number;
|
|
400
|
-
scanLimit?: number;
|
|
401
|
-
sessionId?: string;
|
|
402
|
-
model?: string;
|
|
403
|
-
clientPid?: number;
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
export async function searchLogsImpl(
|
|
407
|
-
callApi: CallApiFn,
|
|
408
|
-
args: SearchLogsArgs,
|
|
409
|
-
): Promise<ToolResult> {
|
|
410
|
-
if ((args.query ?? "").length > MAX_SEARCH_QUERY_CHARS) {
|
|
411
|
-
return resourceLimitToolError({
|
|
412
|
-
error: `MCP search queries may contain at most ${String(MAX_SEARCH_QUERY_CHARS)} characters.`,
|
|
413
|
-
code: "search-limit-exceeded",
|
|
414
|
-
status: 400,
|
|
415
|
-
resource: "mcp-log-search-query",
|
|
416
|
-
limit: MAX_SEARCH_QUERY_CHARS,
|
|
417
|
-
retryable: false,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
if (
|
|
421
|
-
args.limit !== undefined &&
|
|
422
|
-
(!Number.isSafeInteger(args.limit) || args.limit < 1 || args.limit > LIMIT_HARD_CAP)
|
|
423
|
-
) {
|
|
424
|
-
return resourceLimitToolError({
|
|
425
|
-
error: `MCP search pages may contain at most ${String(LIMIT_HARD_CAP)} entries.`,
|
|
426
|
-
code: "invalid-page-request",
|
|
427
|
-
status: 400,
|
|
428
|
-
resource: "mcp-log-search-page-size",
|
|
429
|
-
limit: LIMIT_HARD_CAP,
|
|
430
|
-
retryable: false,
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
if (
|
|
434
|
-
args.scanLimit !== undefined &&
|
|
435
|
-
(!Number.isSafeInteger(args.scanLimit) || args.scanLimit < 1 || args.scanLimit > 1_000)
|
|
436
|
-
) {
|
|
437
|
-
return resourceLimitToolError({
|
|
438
|
-
error: "MCP log searches may scan at most 1000 entries.",
|
|
439
|
-
code: "search-limit-exceeded",
|
|
440
|
-
status: 400,
|
|
441
|
-
resource: "mcp-log-search-scan-size",
|
|
442
|
-
limit: 1_000,
|
|
443
|
-
retryable: false,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
const offset = args.offset ?? PAGINATION_DEFAULTS.offset;
|
|
447
|
-
const limit = clampLimit(args.limit);
|
|
448
|
-
const params = new URLSearchParams({
|
|
449
|
-
q: args.query ?? "",
|
|
450
|
-
offset: String(offset),
|
|
451
|
-
limit: String(limit),
|
|
452
|
-
});
|
|
453
|
-
if (args.scanLimit !== undefined) {
|
|
454
|
-
params.set("scanLimit", String(args.scanLimit));
|
|
455
|
-
}
|
|
456
|
-
if (args.sessionId !== undefined && args.sessionId.length > 0) {
|
|
457
|
-
params.set("sessionId", args.sessionId);
|
|
458
|
-
}
|
|
459
|
-
if (args.model !== undefined && args.model.length > 0) {
|
|
460
|
-
params.set("model", args.model);
|
|
461
|
-
}
|
|
462
|
-
if (args.clientPid !== undefined) {
|
|
463
|
-
params.set("clientPid", String(args.clientPid));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const res = await callApi(`/api/logs?${params.toString()}`);
|
|
467
|
-
if (!res.ok) return toolError(`GET /api/logs search returned ${res.status}`);
|
|
468
|
-
const rawBody: unknown = await res.json();
|
|
469
|
-
const parsed = SearchLogsResponseSchema.safeParse(rawBody);
|
|
470
|
-
if (!parsed.success) return toolError("GET /api/logs search returned an unparseable shape");
|
|
471
|
-
return textJson({
|
|
472
|
-
...parsed.data,
|
|
473
|
-
logs: parsed.data.logs.map(buildLogSummary),
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
export async function getLogImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
|
|
478
|
-
const parsed = await fetchApiJson(callApi, `/api/logs/${id}`, CapturedLogSchema);
|
|
479
|
-
if (!parsed.ok) return parsed.error;
|
|
480
|
-
return textJson(buildBoundedLogDetail(parsed.data));
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export async function getLogChunksImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
|
|
484
|
-
const res = await callApi(`/api/logs/${id}/chunks`);
|
|
485
|
-
if (!res.ok) return toolError(`GET /api/logs/${id}/chunks returned ${res.status}`);
|
|
486
|
-
return textJson(await res.json());
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
export async function listSessionsImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
490
|
-
const res = await callApi("/api/sessions");
|
|
491
|
-
if (!res.ok) return toolError(`GET /api/sessions returned ${res.status}`);
|
|
492
|
-
return textJson(await res.json());
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
export type GetSessionArgs = {
|
|
496
|
-
sessionId: string;
|
|
497
|
-
includeHistory?: boolean;
|
|
498
|
-
latestLogLimit?: number;
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
export async function getSessionImpl(
|
|
502
|
-
callApi: CallApiFn,
|
|
503
|
-
args: GetSessionArgs,
|
|
504
|
-
): Promise<ToolResult> {
|
|
505
|
-
const params = new URLSearchParams({ sessionId: args.sessionId });
|
|
506
|
-
if (args.latestLogLimit !== undefined) {
|
|
507
|
-
params.set("limit", String(args.latestLogLimit));
|
|
508
|
-
}
|
|
509
|
-
if (args.includeHistory === true) {
|
|
510
|
-
params.set("includeHistory", "1");
|
|
511
|
-
}
|
|
512
|
-
const path = `/api/sessions?${params.toString()}`;
|
|
513
|
-
const parsed = await fetchApiJson(callApi, path, SessionInfoSchema);
|
|
514
|
-
if (!parsed.ok) return parsed.error;
|
|
515
|
-
return textJson(parsed.data);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
export function createRunImpl(args: CreateInspectorRunInput): ToolResult {
|
|
519
|
-
return textJson(createInspectorRun(args));
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
export type GetRunArgs = {
|
|
523
|
-
runId: string;
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
export function getRunImpl(args: GetRunArgs): ToolResult {
|
|
527
|
-
const run = getInspectorRun(args.runId);
|
|
528
|
-
if (!run.ok) return toolError(run.message);
|
|
529
|
-
return textJson(run.value);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
export type UpdateRunArgs = {
|
|
533
|
-
runId: string;
|
|
534
|
-
} & UpdateInspectorRunInput;
|
|
535
|
-
|
|
536
|
-
export function updateRunImpl(args: UpdateRunArgs): ToolResult {
|
|
537
|
-
const { runId, ...patch } = args;
|
|
538
|
-
const run = updateInspectorRun(runId, patch);
|
|
539
|
-
if (!run.ok) return toolError(run.message);
|
|
540
|
-
return textJson(run.value);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
export type GetRecentFailuresArgs = {
|
|
544
|
-
limit?: number;
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
export function getRecentFailuresImpl(args: GetRecentFailuresArgs): ToolResult {
|
|
548
|
-
const limit = args.limit ?? 5;
|
|
549
|
-
return textJson(listInspectorRecentFailures(limit));
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
export type ExportEvidenceArgs = {
|
|
553
|
-
runId: string;
|
|
554
|
-
includeHistory?: boolean;
|
|
555
|
-
latestLogLimit?: number;
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
export async function exportEvidenceImpl(args: ExportEvidenceArgs): Promise<ToolResult> {
|
|
559
|
-
const exported = await exportInspectorRunEvidence(
|
|
560
|
-
args.runId,
|
|
561
|
-
{
|
|
562
|
-
includeHistory: args.includeHistory,
|
|
563
|
-
latestLogLimit: args.latestLogLimit,
|
|
564
|
-
},
|
|
565
|
-
inspectorBaseUrl(),
|
|
566
|
-
);
|
|
567
|
-
if (!exported.ok) return toolError(exported.message);
|
|
568
|
-
return textJson(exported.value);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
export function listGroupsImpl(): ToolResult {
|
|
572
|
-
return textJson(listInspectorGroups());
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
export function createGroupImpl(args: CreateInspectorGroupInput): ToolResult {
|
|
576
|
-
return textJson(createInspectorGroup(args));
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
export type GetGroupArgs = {
|
|
580
|
-
groupId: string;
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
export function getGroupImpl(args: GetGroupArgs): ToolResult {
|
|
584
|
-
const group = getInspectorGroup(args.groupId);
|
|
585
|
-
if (!group.ok) return toolError(group.message);
|
|
586
|
-
return textJson(group.value);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
export type UpdateGroupArgs = {
|
|
590
|
-
groupId: string;
|
|
591
|
-
} & UpdateInspectorGroupInput;
|
|
592
|
-
|
|
593
|
-
export function updateGroupImpl(args: UpdateGroupArgs): ToolResult {
|
|
594
|
-
const { groupId, ...patch } = args;
|
|
595
|
-
const group = updateInspectorGroup(groupId, patch);
|
|
596
|
-
if (!group.ok) return toolError(group.message);
|
|
597
|
-
return textJson(group.value);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export type DeleteGroupArgs = {
|
|
601
|
-
groupId: string;
|
|
602
|
-
} & DeleteInspectorGroupOptions;
|
|
603
|
-
|
|
604
|
-
export function deleteGroupImpl(args: DeleteGroupArgs): ToolResult {
|
|
605
|
-
const deleted = deleteInspectorGroup(args.groupId, { deleteEvidence: args.deleteEvidence });
|
|
606
|
-
if (!deleted.ok) return toolError(deleted.message);
|
|
607
|
-
return textJson(deleted.value);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
export type AddGroupSessionArgs = {
|
|
611
|
-
groupId: string;
|
|
612
|
-
} & AddInspectorGroupSessionInput;
|
|
613
|
-
|
|
614
|
-
export function addGroupSessionImpl(args: AddGroupSessionArgs): ToolResult {
|
|
615
|
-
const { groupId, ...session } = args;
|
|
616
|
-
const group = addInspectorGroupSession(groupId, session);
|
|
617
|
-
if (!group.ok) return toolError(group.message);
|
|
618
|
-
return textJson(group.value);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
export type ExportGroupEvidenceArgs = {
|
|
622
|
-
groupId: string;
|
|
623
|
-
includeHistory?: boolean;
|
|
624
|
-
latestLogLimit?: number;
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
export async function exportGroupEvidenceImpl(args: ExportGroupEvidenceArgs): Promise<ToolResult> {
|
|
628
|
-
const exported = await exportInspectorGroupEvidence(
|
|
629
|
-
args.groupId,
|
|
630
|
-
{
|
|
631
|
-
includeHistory: args.includeHistory,
|
|
632
|
-
latestLogLimit: args.latestLogLimit,
|
|
633
|
-
},
|
|
634
|
-
inspectorBaseUrl(),
|
|
635
|
-
);
|
|
636
|
-
if (!exported.ok) return toolError(exported.message);
|
|
637
|
-
return textJson(exported.value);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const SESSION_TIMELINE_DEFAULT_LIMIT = 20;
|
|
641
|
-
const SESSION_TIMELINE_LIMIT_MAX = 50;
|
|
642
|
-
|
|
643
|
-
export type SessionTimelineArgs = {
|
|
644
|
-
sessionId: string;
|
|
645
|
-
limit?: number;
|
|
646
|
-
offset?: number;
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
function clampTimelineLimit(input: number | undefined): number {
|
|
650
|
-
if (input === undefined) return SESSION_TIMELINE_DEFAULT_LIMIT;
|
|
651
|
-
if (!Number.isFinite(input) || input < 1) return 1;
|
|
652
|
-
return Math.min(Math.floor(input), SESSION_TIMELINE_LIMIT_MAX);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
export function buildSessionTimeline(
|
|
656
|
-
sessionId: string,
|
|
657
|
-
logs: CapturedLog[],
|
|
658
|
-
total: number,
|
|
659
|
-
limit: number,
|
|
660
|
-
): Record<string, unknown> {
|
|
661
|
-
const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
|
|
662
|
-
const events = sortedLogs.map((log, index) => ({
|
|
663
|
-
sequence: index + 1,
|
|
664
|
-
kind: "request",
|
|
665
|
-
logId: log.id,
|
|
666
|
-
...buildLogSummary(log),
|
|
667
|
-
}));
|
|
668
|
-
const first = sortedLogs[0];
|
|
669
|
-
const last = sortedLogs[sortedLogs.length - 1];
|
|
670
|
-
const errorCount = events.filter((event) => event.hasError).length;
|
|
671
|
-
const models = [...new Set(sortedLogs.map((log) => log.model).filter((model) => model !== null))];
|
|
672
|
-
const providers = [
|
|
673
|
-
...new Set(
|
|
674
|
-
sortedLogs.map((log) => log.providerName ?? null).filter((provider) => provider !== null),
|
|
675
|
-
),
|
|
676
|
-
];
|
|
677
|
-
|
|
678
|
-
return {
|
|
679
|
-
sessionId,
|
|
680
|
-
logCoverage: "bounded",
|
|
681
|
-
total,
|
|
682
|
-
limit,
|
|
683
|
-
eventCount: events.length,
|
|
684
|
-
requestCount: events.length,
|
|
685
|
-
errorCount,
|
|
686
|
-
startedAt: first?.timestamp ?? null,
|
|
687
|
-
updatedAt: last?.timestamp ?? null,
|
|
688
|
-
models,
|
|
689
|
-
providers,
|
|
690
|
-
events,
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
export async function getSessionTimelineImpl(
|
|
695
|
-
callApi: CallApiFn,
|
|
696
|
-
args: SessionTimelineArgs,
|
|
697
|
-
): Promise<ToolResult> {
|
|
698
|
-
const limit = clampTimelineLimit(args.limit);
|
|
699
|
-
const offset = args.offset ?? 0;
|
|
700
|
-
const params = new URLSearchParams({
|
|
701
|
-
offset: String(offset),
|
|
702
|
-
limit: String(limit),
|
|
703
|
-
compact: "1",
|
|
704
|
-
sessionId: args.sessionId,
|
|
705
|
-
});
|
|
706
|
-
const path = `/api/logs?${params.toString()}`;
|
|
707
|
-
const res = await callApi(path);
|
|
708
|
-
if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
|
|
709
|
-
const rawBody: unknown = await res.json();
|
|
710
|
-
const parsed = LogsListResponseSchema.safeParse(rawBody);
|
|
711
|
-
if (!parsed.success) return toolError("GET /api/logs returned an unparseable timeline shape");
|
|
712
|
-
const logs = parsed.data.logs ?? [];
|
|
713
|
-
return textJson(
|
|
714
|
-
buildSessionTimeline(args.sessionId, logs, parsed.data.total ?? logs.length, limit),
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
export type ListProcessesArgs = {
|
|
719
|
-
limit?: number;
|
|
720
|
-
scanLimit?: number;
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
export async function listProcessesImpl(
|
|
724
|
-
callApi: CallApiFn,
|
|
725
|
-
args: ListProcessesArgs = {},
|
|
726
|
-
): Promise<ToolResult> {
|
|
727
|
-
const scanLimit = clampProcessLogLimit(args.scanLimit ?? args.limit);
|
|
728
|
-
const params = new URLSearchParams({
|
|
729
|
-
q: "",
|
|
730
|
-
offset: "0",
|
|
731
|
-
limit: String(Math.min(scanLimit, 50)),
|
|
732
|
-
scanLimit: String(scanLimit),
|
|
733
|
-
});
|
|
734
|
-
const res = await callApi(`/api/logs?${params.toString()}`);
|
|
735
|
-
if (!res.ok) return toolError(`GET /api/logs process scan returned ${res.status}`);
|
|
736
|
-
const rawBody: unknown = await res.json();
|
|
737
|
-
const parsed = SearchLogsResponseSchema.safeParse(rawBody);
|
|
738
|
-
if (!parsed.success) return toolError("GET /api/logs returned an unparseable process scan shape");
|
|
739
|
-
|
|
740
|
-
const grouped = new Map<number, CapturedLog[]>();
|
|
741
|
-
for (const log of parsed.data.logs) {
|
|
742
|
-
if (log.clientPid === null || log.clientPid === undefined) continue;
|
|
743
|
-
const existing = grouped.get(log.clientPid) ?? [];
|
|
744
|
-
existing.push(log);
|
|
745
|
-
grouped.set(log.clientPid, existing);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const processes = [...grouped.entries()]
|
|
749
|
-
.map(([clientPid, logs]) => processSummary(clientPid, logs, logs.length, logs.length))
|
|
750
|
-
.sort((left, right) => {
|
|
751
|
-
const leftUpdated = typeof left["updatedAt"] === "string" ? left["updatedAt"] : "";
|
|
752
|
-
const rightUpdated = typeof right["updatedAt"] === "string" ? right["updatedAt"] : "";
|
|
753
|
-
return rightUpdated.localeCompare(leftUpdated);
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
return textJson({
|
|
757
|
-
scanned: parsed.data.scanned ?? parsed.data.logs.length,
|
|
758
|
-
processCount: processes.length,
|
|
759
|
-
processes,
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
export type ProcessArgs = {
|
|
764
|
-
clientPid: number;
|
|
765
|
-
limit?: number;
|
|
766
|
-
offset?: number;
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
export async function getProcessImpl(callApi: CallApiFn, args: ProcessArgs): Promise<ToolResult> {
|
|
770
|
-
const limit = clampProcessLogLimit(args.limit);
|
|
771
|
-
const offset = args.offset ?? 0;
|
|
772
|
-
const params = new URLSearchParams({
|
|
773
|
-
clientPid: String(args.clientPid),
|
|
774
|
-
offset: String(offset),
|
|
775
|
-
limit: String(limit),
|
|
776
|
-
compact: "1",
|
|
777
|
-
});
|
|
778
|
-
const result = await readCompactLogsPage(callApi, params);
|
|
779
|
-
if (isToolResult(result)) return result;
|
|
780
|
-
return textJson(processSummary(args.clientPid, result.logs, result.total, limit));
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export async function getProcessTimelineImpl(
|
|
784
|
-
callApi: CallApiFn,
|
|
785
|
-
args: ProcessArgs,
|
|
786
|
-
): Promise<ToolResult> {
|
|
787
|
-
const limit = clampTimelineLimit(args.limit);
|
|
788
|
-
const offset = args.offset ?? 0;
|
|
789
|
-
const params = new URLSearchParams({
|
|
790
|
-
clientPid: String(args.clientPid),
|
|
791
|
-
offset: String(offset),
|
|
792
|
-
limit: String(limit),
|
|
793
|
-
compact: "1",
|
|
794
|
-
});
|
|
795
|
-
const result = await readCompactLogsPage(callApi, params);
|
|
796
|
-
if (isToolResult(result)) return result;
|
|
797
|
-
const sortedLogs = [...result.logs].sort((left, right) => left.id - right.id);
|
|
798
|
-
return textJson({
|
|
799
|
-
clientPid: args.clientPid,
|
|
800
|
-
logCoverage: "bounded",
|
|
801
|
-
total: result.total,
|
|
802
|
-
limit,
|
|
803
|
-
eventCount: sortedLogs.length,
|
|
804
|
-
requestCount: sortedLogs.length,
|
|
805
|
-
errorCount: sortedLogs.filter(logHasError).length,
|
|
806
|
-
startedAt: sortedLogs[0]?.timestamp ?? null,
|
|
807
|
-
updatedAt: sortedLogs.at(-1)?.timestamp ?? null,
|
|
808
|
-
sessions: sortedUniqueStrings(sortedLogs.map((log) => log.sessionId)),
|
|
809
|
-
models: sortedUniqueStrings(sortedLogs.map((log) => log.model)),
|
|
810
|
-
providers: sortedUniqueStrings(sortedLogs.map((log) => log.providerName)),
|
|
811
|
-
events: sortedLogs.map((log, index) => ({
|
|
812
|
-
sequence: index + 1,
|
|
813
|
-
kind: "request",
|
|
814
|
-
logId: log.id,
|
|
815
|
-
...buildLogSummary(log),
|
|
816
|
-
})),
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
export async function getLogHeadersImpl(callApi: CallApiFn, id: number): Promise<ToolResult> {
|
|
821
|
-
const parsed = await fetchApiJson(callApi, `/api/logs/${id}`, CapturedLogSchema);
|
|
822
|
-
if (!parsed.ok) return parsed.error;
|
|
823
|
-
const log = parsed.data;
|
|
824
|
-
return textJson({
|
|
825
|
-
id: log.id,
|
|
826
|
-
sessionId: log.sessionId,
|
|
827
|
-
userAgent: log.userAgent,
|
|
828
|
-
origin: log.origin,
|
|
829
|
-
clientPid: log.clientPid ?? null,
|
|
830
|
-
clientPort: log.clientPort ?? null,
|
|
831
|
-
clientCwd: log.clientCwd ?? null,
|
|
832
|
-
clientProjectFolder: log.clientProjectFolder ?? null,
|
|
833
|
-
apiFormat: log.apiFormat,
|
|
834
|
-
method: log.method,
|
|
835
|
-
path: log.path,
|
|
836
|
-
model: log.model,
|
|
837
|
-
rawHeaders: log.rawHeaders ?? null,
|
|
838
|
-
headers: log.headers ?? null,
|
|
839
|
-
hasRawHeaders: log.rawHeaders !== undefined && Object.keys(log.rawHeaders).length > 0,
|
|
840
|
-
hasHeaders: log.headers !== undefined && Object.keys(log.headers).length > 0,
|
|
841
|
-
bodyContentMode: log.bodyContentMode ?? null,
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
export async function getRuntimeConfigImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
846
|
-
const configRes = await callApi("/api/config");
|
|
847
|
-
if (!configRes.ok) return toolError(`GET /api/config returned ${configRes.status}`);
|
|
848
|
-
const statsRes = await callApi("/api/logs?stats=1");
|
|
849
|
-
if (!statsRes.ok) return toolError(`GET /api/logs?stats=1 returned ${statsRes.status}`);
|
|
850
|
-
const config: unknown = await configRes.json();
|
|
851
|
-
const storage: unknown = await statsRes.json();
|
|
852
|
-
return textJson({
|
|
853
|
-
config,
|
|
854
|
-
storage,
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
export type DiagnoseCaptureArgs = {
|
|
859
|
-
id?: number;
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
export async function diagnoseCaptureImpl(
|
|
863
|
-
callApi: CallApiFn,
|
|
864
|
-
args: DiagnoseCaptureArgs = {},
|
|
865
|
-
): Promise<ToolResult> {
|
|
866
|
-
const runtime = await getRuntimeConfigImpl(callApi);
|
|
867
|
-
if (runtime.isError === true) return runtime;
|
|
868
|
-
const runtimeText = runtime.content[0]?.text ?? "{}";
|
|
869
|
-
let runtimePayload: unknown = {};
|
|
870
|
-
try {
|
|
871
|
-
runtimePayload = JSON.parse(runtimeText);
|
|
872
|
-
} catch {
|
|
873
|
-
// Non-JSON runtime config text — treat as empty config and continue diagnostics
|
|
874
|
-
}
|
|
875
|
-
const runtimeRecord = isRecord(runtimePayload) ? runtimePayload : {};
|
|
876
|
-
const configRecord = isRecord(runtimeRecord["config"]) ? runtimeRecord["config"] : {};
|
|
877
|
-
const captureMode = stringField(configRecord, "captureMode");
|
|
878
|
-
|
|
879
|
-
let log: CapturedLog | null = null;
|
|
880
|
-
if (args.id !== undefined) {
|
|
881
|
-
const logRes = await callApi(`/api/logs/${args.id}`);
|
|
882
|
-
if (!logRes.ok) return toolError(`GET /api/logs/${args.id} returned ${logRes.status}`);
|
|
883
|
-
const rawLog: unknown = await logRes.json();
|
|
884
|
-
const parsed = CapturedLogSchema.safeParse(rawLog);
|
|
885
|
-
if (!parsed.success) return toolError(`GET /api/logs/${args.id} returned an unparseable log`);
|
|
886
|
-
log = parsed.data;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const findings: string[] = [];
|
|
890
|
-
const recommendations: string[] = [];
|
|
891
|
-
if (captureMode !== "full") {
|
|
892
|
-
findings.push(
|
|
893
|
-
"Runtime captureMode is not full, so request/response bodies and headers may be compacted.",
|
|
894
|
-
);
|
|
895
|
-
recommendations.push(
|
|
896
|
-
"Restart or launch Agent Inspector with full capture mode when header/body evidence is required.",
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
if (log !== null) {
|
|
900
|
-
if (log.rawHeaders === undefined || Object.keys(log.rawHeaders).length === 0) {
|
|
901
|
-
findings.push(`Log ${String(log.id)} has no rawHeaders stored.`);
|
|
902
|
-
}
|
|
903
|
-
if (log.headers === undefined || Object.keys(log.headers).length === 0) {
|
|
904
|
-
findings.push(`Log ${String(log.id)} has no upstream headers stored.`);
|
|
905
|
-
}
|
|
906
|
-
if (log.rawRequestBody === null) {
|
|
907
|
-
findings.push(`Log ${String(log.id)} does not include rawRequestBody in this response.`);
|
|
908
|
-
}
|
|
909
|
-
if (log.bodyContentMode === "compact") {
|
|
910
|
-
findings.push(`Log ${String(log.id)} is in compact body mode.`);
|
|
911
|
-
recommendations.push(
|
|
912
|
-
"Use inspector_get_log_headers for header-only checks and enable full capture for future body inspection.",
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
return textJson({
|
|
918
|
-
captureMode,
|
|
919
|
-
logId: log?.id ?? null,
|
|
920
|
-
hasLog: log !== null,
|
|
921
|
-
findings,
|
|
922
|
-
recommendations,
|
|
923
|
-
runtime: runtimeRecord,
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
export type AddGroupProcessArgs = {
|
|
928
|
-
groupId: string;
|
|
929
|
-
clientPid: number;
|
|
930
|
-
labelPrefix?: string;
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
export async function addGroupProcessImpl(
|
|
934
|
-
callApi: CallApiFn,
|
|
935
|
-
args: AddGroupProcessArgs,
|
|
936
|
-
): Promise<ToolResult> {
|
|
937
|
-
const params = new URLSearchParams({
|
|
938
|
-
clientPid: String(args.clientPid),
|
|
939
|
-
offset: "0",
|
|
940
|
-
limit: String(PROCESS_LOG_LIMIT_MAX),
|
|
941
|
-
compact: "1",
|
|
942
|
-
});
|
|
943
|
-
const result = await readCompactLogsPage(callApi, params);
|
|
944
|
-
if (isToolResult(result)) return result;
|
|
945
|
-
|
|
946
|
-
const bySession = new Map<string, CapturedLog[]>();
|
|
947
|
-
for (const log of result.logs) {
|
|
948
|
-
if (log.sessionId === null || log.sessionId === "") continue;
|
|
949
|
-
const existing = bySession.get(log.sessionId) ?? [];
|
|
950
|
-
existing.push(log);
|
|
951
|
-
bySession.set(log.sessionId, existing);
|
|
952
|
-
}
|
|
953
|
-
if (bySession.size === 0) {
|
|
954
|
-
return toolError(`No sessions found for clientPid ${String(args.clientPid)}`);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
let groupResult: ReturnType<typeof addInspectorGroupSession> | null = null;
|
|
958
|
-
const attached: Array<Record<string, unknown>> = [];
|
|
959
|
-
for (const [sessionId, logs] of bySession.entries()) {
|
|
960
|
-
const first = logs[0];
|
|
961
|
-
const last = logs.at(-1);
|
|
962
|
-
const userAgent = last?.userAgent ?? first?.userAgent ?? null;
|
|
963
|
-
const agent = inferAgentFromUserAgent(userAgent);
|
|
964
|
-
groupResult = addInspectorGroupSession(args.groupId, {
|
|
965
|
-
sessionId,
|
|
966
|
-
label: `${args.labelPrefix ?? `PID ${String(args.clientPid)}`} - ${sessionId}`,
|
|
967
|
-
agent,
|
|
968
|
-
provider: last?.providerName ?? first?.providerName ?? null,
|
|
969
|
-
model: last?.model ?? first?.model ?? null,
|
|
970
|
-
metadata: {
|
|
971
|
-
clientPid: args.clientPid,
|
|
972
|
-
processLogCount: logs.length,
|
|
973
|
-
userAgent: userAgent ?? "",
|
|
974
|
-
},
|
|
975
|
-
});
|
|
976
|
-
if (!groupResult.ok) return toolError(groupResult.message);
|
|
977
|
-
attached.push({ sessionId, logCount: logs.length, agent, userAgent });
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
if (groupResult === null || !groupResult.ok) return toolError("Group process attach failed");
|
|
981
|
-
return textJson({
|
|
982
|
-
group: groupResult.value,
|
|
983
|
-
clientPid: args.clientPid,
|
|
984
|
-
attached,
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
type EvidenceFileKind = "json" | "markdown" | "html" | "zip";
|
|
989
|
-
|
|
990
|
-
type EvidenceFileSummary = {
|
|
991
|
-
kind: EvidenceFileKind;
|
|
992
|
-
path: string;
|
|
993
|
-
exists: boolean;
|
|
994
|
-
bytes: number | null;
|
|
995
|
-
included?: boolean;
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
type EvidencePaths = {
|
|
999
|
-
jsonPath: string;
|
|
1000
|
-
markdownPath: string;
|
|
1001
|
-
htmlPath: string;
|
|
1002
|
-
exportedAt: string;
|
|
1003
|
-
};
|
|
1004
|
-
|
|
1005
|
-
function fileBytes(path: string): number | null {
|
|
1006
|
-
if (!existsSync(path)) return null;
|
|
1007
|
-
try {
|
|
1008
|
-
return statSync(path).size;
|
|
1009
|
-
} catch {
|
|
1010
|
-
return null;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function evidenceFileSummary(kind: EvidenceFileKind, path: string): EvidenceFileSummary {
|
|
1015
|
-
return {
|
|
1016
|
-
kind,
|
|
1017
|
-
path,
|
|
1018
|
-
exists: existsSync(path),
|
|
1019
|
-
bytes: fileBytes(path),
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function evidenceFiles(evidence: EvidencePaths): EvidenceFileSummary[] {
|
|
1024
|
-
return [
|
|
1025
|
-
evidenceFileSummary("json", evidence.jsonPath),
|
|
1026
|
-
evidenceFileSummary("markdown", evidence.markdownPath),
|
|
1027
|
-
evidenceFileSummary("html", evidence.htmlPath),
|
|
1028
|
-
];
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
function readEvidenceFile(path: string): Buffer | null {
|
|
1032
|
-
if (!existsSync(path)) return null;
|
|
1033
|
-
try {
|
|
1034
|
-
return readFileSync(path);
|
|
1035
|
-
} catch {
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
async function exportEvidenceZip(
|
|
1041
|
-
ownerKind: "run" | "group",
|
|
1042
|
-
ownerId: string,
|
|
1043
|
-
evidence: EvidencePaths,
|
|
1044
|
-
): Promise<Record<string, unknown>> {
|
|
1045
|
-
const zip = new JSZip();
|
|
1046
|
-
const sourceFiles = evidenceFiles(evidence);
|
|
1047
|
-
const includedFiles: EvidenceFileSummary[] = [];
|
|
1048
|
-
|
|
1049
|
-
for (const file of sourceFiles) {
|
|
1050
|
-
const content = readEvidenceFile(file.path);
|
|
1051
|
-
const included = content !== null;
|
|
1052
|
-
includedFiles.push({ ...file, included });
|
|
1053
|
-
if (included) {
|
|
1054
|
-
zip.file(`${file.kind}.${file.kind === "markdown" ? "md" : file.kind}`, content);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
zip.file(
|
|
1059
|
-
"manifest.json",
|
|
1060
|
-
`${JSON.stringify(
|
|
1061
|
-
{
|
|
1062
|
-
ownerKind,
|
|
1063
|
-
ownerId,
|
|
1064
|
-
exportedAt: evidence.exportedAt,
|
|
1065
|
-
bundledAt: new Date().toISOString(),
|
|
1066
|
-
files: includedFiles,
|
|
1067
|
-
},
|
|
1068
|
-
null,
|
|
1069
|
-
2,
|
|
1070
|
-
)}\n`,
|
|
1071
|
-
);
|
|
1072
|
-
|
|
1073
|
-
const zipPath = join(
|
|
1074
|
-
dirname(evidence.jsonPath),
|
|
1075
|
-
ownerKind === "run" ? "evidence.zip" : "group-evidence.zip",
|
|
1076
|
-
);
|
|
1077
|
-
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
1078
|
-
writeFileSync(zipPath, buffer);
|
|
1079
|
-
const zipSummary = evidenceFileSummary("zip", zipPath);
|
|
1080
|
-
return {
|
|
1081
|
-
ownerKind,
|
|
1082
|
-
ownerId,
|
|
1083
|
-
bundlePath: zipPath,
|
|
1084
|
-
bytes: zipSummary.bytes,
|
|
1085
|
-
files: [...includedFiles, zipSummary],
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
export function getEvidenceFilesImpl(args: { runId: string }): ToolResult {
|
|
1090
|
-
const evidence = readInspectorRunEvidence(args.runId);
|
|
1091
|
-
if (!evidence.ok) return toolError(evidence.message);
|
|
1092
|
-
return textJson({
|
|
1093
|
-
runId: args.runId,
|
|
1094
|
-
exportedAt: evidence.value.evidence.exportedAt,
|
|
1095
|
-
files: evidenceFiles(evidence.value.evidence),
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
export async function exportEvidenceBundleImpl(args: { runId: string }): Promise<ToolResult> {
|
|
1100
|
-
const evidence = readInspectorRunEvidence(args.runId);
|
|
1101
|
-
if (!evidence.ok) return toolError(evidence.message);
|
|
1102
|
-
return textJson(await exportEvidenceZip("run", args.runId, evidence.value.evidence));
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
export function getGroupEvidenceFilesImpl(args: { groupId: string }): ToolResult {
|
|
1106
|
-
const evidence = readInspectorGroupEvidence(args.groupId);
|
|
1107
|
-
if (!evidence.ok) return toolError(evidence.message);
|
|
1108
|
-
return textJson({
|
|
1109
|
-
groupId: args.groupId,
|
|
1110
|
-
exportedAt: evidence.value.evidence.exportedAt,
|
|
1111
|
-
files: evidenceFiles(evidence.value.evidence),
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
export async function exportGroupEvidenceBundleImpl(args: {
|
|
1116
|
-
groupId: string;
|
|
1117
|
-
}): Promise<ToolResult> {
|
|
1118
|
-
const evidence = readInspectorGroupEvidence(args.groupId);
|
|
1119
|
-
if (!evidence.ok) return toolError(evidence.message);
|
|
1120
|
-
return textJson(await exportEvidenceZip("group", args.groupId, evidence.value.evidence));
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
export function getCurrentContextImpl(): ToolResult {
|
|
1124
|
-
return textJson(getCurrentContext());
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
export function setCurrentContextImpl(args: InspectorCurrentContextPatch): ToolResult {
|
|
1128
|
-
return textJson(setCurrentContext(args));
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
1132
|
-
const res = await callApi("/api/models");
|
|
1133
|
-
if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
|
|
1134
|
-
return textJson(await res.json());
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
export async function listProvidersImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
1138
|
-
const res = await callApi("/api/providers");
|
|
1139
|
-
if (!res.ok) return toolError(`GET /api/providers returned ${res.status}`);
|
|
1140
|
-
return redactedProvidersResult(await res.json());
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
export async function getProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
|
|
1144
|
-
const res = await callApi(`/api/providers/${encodeURIComponent(id)}`);
|
|
1145
|
-
if (!res.ok) return toolError(`GET /api/providers/${id} returned ${res.status}`);
|
|
1146
|
-
return redactedProviderResult(await res.json());
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
export type ReplayLogArgs = { id: number };
|
|
1150
|
-
|
|
1151
|
-
export async function replayLogImpl(callApi: CallApiFn, args: ReplayLogArgs): Promise<ToolResult> {
|
|
1152
|
-
const logRes = await callApi(`/api/logs/${args.id}`);
|
|
1153
|
-
if (!logRes.ok) return toolError(`GET /api/logs/${args.id} returned ${logRes.status}`);
|
|
1154
|
-
const logRaw: unknown = await logRes.json();
|
|
1155
|
-
const logParsed = CapturedLogSchema.safeParse(logRaw);
|
|
1156
|
-
if (!logParsed.success) {
|
|
1157
|
-
return toolError(`GET /api/logs/${args.id} returned an unparseable CapturedLog`);
|
|
1158
|
-
}
|
|
1159
|
-
const log = logParsed.data;
|
|
1160
|
-
if (log.rawRequestBody === null) return toolError("Log has no rawRequestBody to replay");
|
|
1161
|
-
const replayRes = await callApi(`/api/logs/${args.id}/replay`, {
|
|
1162
|
-
method: "POST",
|
|
1163
|
-
headers: { "content-type": "application/json" },
|
|
1164
|
-
body: JSON.stringify({ modifiedBody: log.rawRequestBody }),
|
|
1165
|
-
});
|
|
1166
|
-
if (!replayRes.ok) {
|
|
1167
|
-
return toolError(`POST /api/logs/${args.id}/replay returned ${replayRes.status}`);
|
|
1168
|
-
}
|
|
1169
|
-
return textJson(await replayRes.json());
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
export type AddProviderInput = {
|
|
1173
|
-
name: string;
|
|
1174
|
-
apiKey: string;
|
|
1175
|
-
format?: "anthropic" | "openai";
|
|
1176
|
-
model?: string;
|
|
1177
|
-
models?: string[];
|
|
1178
|
-
anthropicBaseUrl?: string;
|
|
1179
|
-
openaiBaseUrl?: string;
|
|
1180
|
-
openaiResponsesBaseUrl?: string;
|
|
1181
|
-
authHeader?: "bearer" | "x-api-key";
|
|
1182
|
-
apiDocsUrl?: string;
|
|
1183
|
-
};
|
|
1184
|
-
|
|
1185
|
-
function normalizedProviderModels(input: { model?: string; models?: readonly string[] }): string[] {
|
|
1186
|
-
const modelList = input.models ?? [];
|
|
1187
|
-
const models = modelList.map((model) => model.trim()).filter((model) => model !== "");
|
|
1188
|
-
if (models.length > 0) return models;
|
|
1189
|
-
const model = input.model?.trim();
|
|
1190
|
-
return model !== undefined && model !== "" ? [model] : [];
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
export async function addProviderImpl(
|
|
1194
|
-
callApi: CallApiFn,
|
|
1195
|
-
provider: AddProviderInput,
|
|
1196
|
-
): Promise<ToolResult> {
|
|
1197
|
-
const models = normalizedProviderModels(provider);
|
|
1198
|
-
if (models.length > MAX_PROVIDER_MODELS) {
|
|
1199
|
-
return resourceLimitToolError({
|
|
1200
|
-
error: `Provider model lists may contain at most ${String(MAX_PROVIDER_MODELS)} entries.`,
|
|
1201
|
-
code: "provider-model-limit-exceeded",
|
|
1202
|
-
status: 413,
|
|
1203
|
-
resource: "provider-models",
|
|
1204
|
-
limit: MAX_PROVIDER_MODELS,
|
|
1205
|
-
retryable: false,
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
const res = await callApi("/api/providers", {
|
|
1209
|
-
method: "POST",
|
|
1210
|
-
headers: { "content-type": "application/json" },
|
|
1211
|
-
body: JSON.stringify({ ...provider, models }),
|
|
1212
|
-
});
|
|
1213
|
-
if (!res.ok) return toolError(`POST /api/providers returned ${res.status}`);
|
|
1214
|
-
return redactedProviderResult(await res.json());
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
export type UpdateProviderInput = {
|
|
1218
|
-
id: string;
|
|
1219
|
-
name?: string;
|
|
1220
|
-
apiKey?: string;
|
|
1221
|
-
format?: "anthropic" | "openai";
|
|
1222
|
-
model?: string;
|
|
1223
|
-
models?: string[];
|
|
1224
|
-
baseUrl?: string;
|
|
1225
|
-
authHeader?: "bearer" | "x-api-key";
|
|
1226
|
-
anthropicBaseUrl?: string;
|
|
1227
|
-
openaiBaseUrl?: string;
|
|
1228
|
-
openaiResponsesBaseUrl?: string;
|
|
1229
|
-
apiDocsUrl?: string;
|
|
1230
|
-
};
|
|
1231
|
-
|
|
1232
|
-
export async function updateProviderImpl(
|
|
1233
|
-
callApi: CallApiFn,
|
|
1234
|
-
input: UpdateProviderInput,
|
|
1235
|
-
): Promise<ToolResult> {
|
|
1236
|
-
const { id, ...patch } = input;
|
|
1237
|
-
const models =
|
|
1238
|
-
patch.models !== undefined || patch.model !== undefined
|
|
1239
|
-
? normalizedProviderModels(patch)
|
|
1240
|
-
: undefined;
|
|
1241
|
-
if (models !== undefined && models.length > MAX_PROVIDER_MODELS) {
|
|
1242
|
-
return resourceLimitToolError({
|
|
1243
|
-
error: `Provider model lists may contain at most ${String(MAX_PROVIDER_MODELS)} entries.`,
|
|
1244
|
-
code: "provider-model-limit-exceeded",
|
|
1245
|
-
status: 413,
|
|
1246
|
-
resource: "provider-models",
|
|
1247
|
-
limit: MAX_PROVIDER_MODELS,
|
|
1248
|
-
retryable: false,
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
const body = models === undefined ? patch : { ...patch, models };
|
|
1252
|
-
const res = await callApi(`/api/providers/${encodeURIComponent(id)}`, {
|
|
1253
|
-
method: "PUT",
|
|
1254
|
-
headers: { "content-type": "application/json" },
|
|
1255
|
-
body: JSON.stringify(body),
|
|
1256
|
-
});
|
|
1257
|
-
if (!res.ok) return toolError(`PUT /api/providers/${id} returned ${res.status}`);
|
|
1258
|
-
return redactedProviderResult(await res.json());
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
export async function testProviderImpl(callApi: CallApiFn, id: string): Promise<ToolResult> {
|
|
1262
|
-
const res = await callApi(`/api/providers/${encodeURIComponent(id)}/test`, {
|
|
1263
|
-
method: "POST",
|
|
1264
|
-
});
|
|
1265
|
-
if (!res.ok) return toolError(`POST /api/providers/${id}/test returned ${res.status}`);
|
|
1266
|
-
return textJson(await res.json());
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
type ProviderDiagnosticIssue = {
|
|
1270
|
-
path: string;
|
|
1271
|
-
status: number | null;
|
|
1272
|
-
error: string | null;
|
|
1273
|
-
durationMs: number | null;
|
|
1274
|
-
endpoint: string | null;
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
function stringField(record: Record<string, unknown>, key: string): string | null {
|
|
1278
|
-
const value = record[key];
|
|
1279
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
function numberField(record: Record<string, unknown>, key: string): number | null {
|
|
1283
|
-
const value = record[key];
|
|
1284
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
function firstStringField(record: Record<string, unknown>, keys: string[]): string | null {
|
|
1288
|
-
for (const key of keys) {
|
|
1289
|
-
const value = stringField(record, key);
|
|
1290
|
-
if (value !== null) return value;
|
|
1291
|
-
}
|
|
1292
|
-
return null;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
function firstNumberField(record: Record<string, unknown>, keys: string[]): number | null {
|
|
1296
|
-
for (const key of keys) {
|
|
1297
|
-
const value = numberField(record, key);
|
|
1298
|
-
if (value !== null) return value;
|
|
1299
|
-
}
|
|
1300
|
-
return null;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function collectProviderDiagnosticIssues(
|
|
1304
|
-
value: unknown,
|
|
1305
|
-
path: string,
|
|
1306
|
-
issues: ProviderDiagnosticIssue[],
|
|
1307
|
-
): void {
|
|
1308
|
-
if (Array.isArray(value)) {
|
|
1309
|
-
value.forEach((item, index) =>
|
|
1310
|
-
collectProviderDiagnosticIssues(item, `${path}[${String(index)}]`, issues),
|
|
1311
|
-
);
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
if (!isRecord(value)) return;
|
|
1315
|
-
|
|
1316
|
-
const success = value["success"];
|
|
1317
|
-
const status = firstNumberField(value, ["status", "responseStatus", "statusCode"]);
|
|
1318
|
-
const error = firstStringField(value, ["error", "message", "errorMessage"]);
|
|
1319
|
-
if (success === false || error !== null || (status !== null && status >= 400)) {
|
|
1320
|
-
issues.push({
|
|
1321
|
-
path,
|
|
1322
|
-
status,
|
|
1323
|
-
error,
|
|
1324
|
-
durationMs: firstNumberField(value, ["durationMs", "elapsedMs", "latencyMs"]),
|
|
1325
|
-
endpoint: firstStringField(value, ["endpoint", "url", "requestUrl", "baseUrl"]),
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
for (const [key, child] of Object.entries(value)) {
|
|
1330
|
-
if (typeof child === "object" && child !== null) {
|
|
1331
|
-
collectProviderDiagnosticIssues(child, `${path}.${key}`, issues);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
function stringifyUnknown(value: unknown): string {
|
|
1337
|
-
try {
|
|
1338
|
-
const text = JSON.stringify(value);
|
|
1339
|
-
return text === undefined ? "" : text.toLowerCase();
|
|
1340
|
-
} catch {
|
|
1341
|
-
return "";
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
function providerDiagnosticHints(result: unknown): string[] {
|
|
1346
|
-
const text = stringifyUnknown(result);
|
|
1347
|
-
const hints: string[] = [];
|
|
1348
|
-
if (text.includes("timeout") || text.includes("aborted")) {
|
|
1349
|
-
hints.push(
|
|
1350
|
-
"Provider test timed out or was aborted. Increase Settings > Proxy > Provider test timeout when the provider or model is slow.",
|
|
1351
|
-
);
|
|
1352
|
-
}
|
|
1353
|
-
if (text.includes("localhost") || text.includes("127.0.0.1")) {
|
|
1354
|
-
hints.push(
|
|
1355
|
-
"The upstream URL contains localhost. If the agent or provider runs in another container, replace localhost with a reachable host/IP.",
|
|
1356
|
-
);
|
|
1357
|
-
}
|
|
1358
|
-
if (text.includes("econnrefused") || text.includes("enotfound") || text.includes("network")) {
|
|
1359
|
-
hints.push(
|
|
1360
|
-
"This looks like a network or DNS failure. Verify the Inspector container/host can reach the provider base URL.",
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
if (
|
|
1364
|
-
text.includes("401") ||
|
|
1365
|
-
text.includes("403") ||
|
|
1366
|
-
text.includes("unauthorized") ||
|
|
1367
|
-
text.includes("forbidden")
|
|
1368
|
-
) {
|
|
1369
|
-
hints.push(
|
|
1370
|
-
"This looks like an authentication failure. Check the provider API key and auth header mode.",
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
if (text.includes("404") || text.includes("not found")) {
|
|
1374
|
-
hints.push(
|
|
1375
|
-
"This looks like a base URL or path mismatch. Check whether the provider endpoint already includes /v1 or needs a vendor-specific path.",
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
|
-
if (text.includes("nonstream") || text.includes("non-stream")) {
|
|
1379
|
-
hints.push(
|
|
1380
|
-
"The failure mentions non-streaming. Compare the Anthropic, OpenAI Chat, and OpenAI Responses non-streaming path and payload with the streaming request.",
|
|
1381
|
-
);
|
|
1382
|
-
}
|
|
1383
|
-
return [...new Set(hints)];
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
export async function diagnoseProviderTestImpl(
|
|
1387
|
-
callApi: CallApiFn,
|
|
1388
|
-
id: string,
|
|
1389
|
-
): Promise<ToolResult> {
|
|
1390
|
-
const res = await callApi(`/api/providers/${encodeURIComponent(id)}/test`, {
|
|
1391
|
-
method: "POST",
|
|
1392
|
-
});
|
|
1393
|
-
if (!res.ok) return toolError(`POST /api/providers/${id}/test returned ${res.status}`);
|
|
1394
|
-
const result: unknown = await res.json();
|
|
1395
|
-
const issues: ProviderDiagnosticIssue[] = [];
|
|
1396
|
-
collectProviderDiagnosticIssues(result, "result", issues);
|
|
1397
|
-
return textJson({
|
|
1398
|
-
providerId: id,
|
|
1399
|
-
issueCount: issues.length,
|
|
1400
|
-
issues,
|
|
1401
|
-
hints: providerDiagnosticHints(result),
|
|
1402
|
-
result,
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
export type CreateSessionKnowledgeArgs = {
|
|
1407
|
-
sessionId: string;
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
export async function createSessionKnowledgeImpl(
|
|
1411
|
-
callApi: CallApiFn,
|
|
1412
|
-
args: CreateSessionKnowledgeArgs,
|
|
1413
|
-
): Promise<ToolResult> {
|
|
1414
|
-
const res = await callApi(
|
|
1415
|
-
`/api/knowledge/sessions/${encodeURIComponent(args.sessionId)}/candidates`,
|
|
1416
|
-
{
|
|
1417
|
-
method: "POST",
|
|
1418
|
-
},
|
|
1419
|
-
);
|
|
1420
|
-
if (!res.ok) {
|
|
1421
|
-
return toolError(
|
|
1422
|
-
`POST /api/knowledge/sessions/${args.sessionId}/candidates returned ${res.status}`,
|
|
1423
|
-
);
|
|
1424
|
-
}
|
|
1425
|
-
return textJson(await res.json());
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
export async function listKnowledgeCandidatesImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
1429
|
-
const res = await callApi("/api/knowledge/candidates");
|
|
1430
|
-
if (!res.ok) return toolError(`GET /api/knowledge/candidates returned ${res.status}`);
|
|
1431
|
-
return textJson(await res.json());
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
export type PromoteKnowledgeCandidateArgs = {
|
|
1435
|
-
candidateId: string;
|
|
1436
|
-
};
|
|
1437
|
-
|
|
1438
|
-
export async function promoteKnowledgeCandidateImpl(
|
|
1439
|
-
callApi: CallApiFn,
|
|
1440
|
-
args: PromoteKnowledgeCandidateArgs,
|
|
1441
|
-
): Promise<ToolResult> {
|
|
1442
|
-
const res = await callApi(
|
|
1443
|
-
`/api/knowledge/candidates/${encodeURIComponent(args.candidateId)}/promote`,
|
|
1444
|
-
{
|
|
1445
|
-
method: "POST",
|
|
1446
|
-
},
|
|
1447
|
-
);
|
|
1448
|
-
if (!res.ok) {
|
|
1449
|
-
return toolError(
|
|
1450
|
-
`POST /api/knowledge/candidates/${args.candidateId}/promote returned ${res.status}`,
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
return textJson(await res.json());
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
export type SearchKnowledgeArgs = {
|
|
1457
|
-
query: string;
|
|
1458
|
-
project?: string;
|
|
1459
|
-
};
|
|
1460
|
-
|
|
1461
|
-
export async function searchKnowledgeImpl(
|
|
1462
|
-
callApi: CallApiFn,
|
|
1463
|
-
args: SearchKnowledgeArgs,
|
|
1464
|
-
): Promise<ToolResult> {
|
|
1465
|
-
if (args.query.length > MAX_SEARCH_QUERY_CHARS) {
|
|
1466
|
-
return resourceLimitToolError({
|
|
1467
|
-
error: `MCP search queries may contain at most ${String(MAX_SEARCH_QUERY_CHARS)} characters.`,
|
|
1468
|
-
code: "search-limit-exceeded",
|
|
1469
|
-
status: 400,
|
|
1470
|
-
resource: "mcp-knowledge-search-query",
|
|
1471
|
-
limit: MAX_SEARCH_QUERY_CHARS,
|
|
1472
|
-
retryable: false,
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
if (args.project !== undefined && args.project.length > MAX_SEARCH_PROJECT_CHARS) {
|
|
1476
|
-
return resourceLimitToolError({
|
|
1477
|
-
error: `MCP search project filters may contain at most ${String(MAX_SEARCH_PROJECT_CHARS)} characters.`,
|
|
1478
|
-
code: "search-limit-exceeded",
|
|
1479
|
-
status: 400,
|
|
1480
|
-
resource: "mcp-knowledge-search-project",
|
|
1481
|
-
limit: MAX_SEARCH_PROJECT_CHARS,
|
|
1482
|
-
retryable: false,
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
const params = new URLSearchParams({ q: args.query });
|
|
1486
|
-
if (args.project !== undefined && args.project.length > 0) {
|
|
1487
|
-
params.set("project", args.project);
|
|
1488
|
-
}
|
|
1489
|
-
const res = await callApi(`/api/knowledge/search?${params.toString()}`);
|
|
1490
|
-
if (!res.ok) return toolError(`GET /api/knowledge/search returned ${res.status}`);
|
|
1491
|
-
return textJson(await res.json());
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
export type GetProjectContextArgs = {
|
|
1495
|
-
project: string;
|
|
1496
|
-
};
|
|
1497
|
-
|
|
1498
|
-
export async function getProjectContextImpl(
|
|
1499
|
-
callApi: CallApiFn,
|
|
1500
|
-
args: GetProjectContextArgs,
|
|
1501
|
-
): Promise<ToolResult> {
|
|
1502
|
-
if (args.project.length > MAX_SEARCH_PROJECT_CHARS) {
|
|
1503
|
-
return resourceLimitToolError({
|
|
1504
|
-
error: `MCP search project filters may contain at most ${String(MAX_SEARCH_PROJECT_CHARS)} characters.`,
|
|
1505
|
-
code: "search-limit-exceeded",
|
|
1506
|
-
status: 400,
|
|
1507
|
-
resource: "mcp-knowledge-search-project",
|
|
1508
|
-
limit: MAX_SEARCH_PROJECT_CHARS,
|
|
1509
|
-
retryable: false,
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
const params = new URLSearchParams({ project: args.project });
|
|
1513
|
-
const res = await callApi(`/api/knowledge/project-context?${params.toString()}`);
|
|
1514
|
-
if (!res.ok) return toolError(`GET /api/knowledge/project-context returned ${res.status}`);
|
|
1515
|
-
return textJson(await res.json());
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
/**
|
|
1519
|
-
* Wrap an impl so any thrown error becomes a well-formed `isError: true`
|
|
1520
|
-
* CallToolResult (instead of a JSON-RPC protocol error, which would abort
|
|
1521
|
-
* the whole session). Loopback timeouts get a clear message.
|
|
1522
|
-
*/
|
|
1523
|
-
export async function safeCall(fn: () => ToolResult | Promise<ToolResult>): Promise<ToolResult> {
|
|
1524
|
-
try {
|
|
1525
|
-
return await fn();
|
|
1526
|
-
} catch (err) {
|
|
1527
|
-
if (err instanceof LoopbackTimeoutError) {
|
|
1528
|
-
return toolError(`Internal loopback call timed out after ${err.timeoutMs}ms: ${err.path}`);
|
|
1529
|
-
}
|
|
1530
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1531
|
-
return toolError(`Tool execution failed: ${message}`);
|
|
1532
|
-
}
|
|
1533
|
-
}
|