rea-agents 2.1.0 → 2.3.0

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.
Files changed (193) hide show
  1. package/README.md +84 -18
  2. package/dist/application/AndroidApplicationService.js +17 -0
  3. package/dist/application/AppleApplicationService.js +17 -0
  4. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  5. package/dist/application/BinarySession.js +9 -2
  6. package/dist/application/BinarySessionStatus.js +39 -1
  7. package/dist/application/CapabilityInventory.js +15 -0
  8. package/dist/application/ClientRegistrationStatus.js +11 -4
  9. package/dist/application/CommandShimReplay.js +6 -3
  10. package/dist/application/CompositeProvider.js +5 -2
  11. package/dist/application/DirectAnalysis.js +1 -20
  12. package/dist/application/DirectAnalysisStatus.js +83 -0
  13. package/dist/application/Doctor.js +34 -25
  14. package/dist/application/DoctorDiagnostics.js +1 -4
  15. package/dist/application/DoctorProjection.js +43 -0
  16. package/dist/application/DoctorScope.js +102 -0
  17. package/dist/application/EvidenceReferenceResolver.js +26 -0
  18. package/dist/application/InventoryProjectionEvidence.js +41 -0
  19. package/dist/application/InvestigationProviders.js +12 -0
  20. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  21. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  22. package/dist/application/JavaScriptApplicationService.js +3 -2
  23. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  24. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  25. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  26. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  27. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  28. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  29. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  30. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  31. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  32. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  33. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  34. package/dist/application/LoopbackReplay.js +277 -133
  35. package/dist/application/LoopbackReplayRecorder.js +101 -0
  36. package/dist/application/PermissionAuthority.js +7 -3
  37. package/dist/application/ProcessCaptureJournal.js +95 -0
  38. package/dist/application/ProcessCaptureLifecycle.js +24 -13
  39. package/dist/application/ProcessCheckpoints.js +30 -4
  40. package/dist/application/ProcessCli.js +26 -4
  41. package/dist/application/ProcessHarness.js +20 -50
  42. package/dist/application/ProcessNormalization.js +3 -2
  43. package/dist/application/ProcessPairedExperiment.js +72 -0
  44. package/dist/application/ProcessReactiveCoordinator.js +218 -0
  45. package/dist/application/ProcessReactiveEffects.js +133 -0
  46. package/dist/application/ProcessReactiveObservations.js +20 -0
  47. package/dist/application/ProcessSampling.js +2 -0
  48. package/dist/application/SessionProviderRouter.js +7 -6
  49. package/dist/application/Setup.js +14 -13
  50. package/dist/application/SetupClientConfiguration.js +10 -6
  51. package/dist/application/SetupPlan.js +4 -3
  52. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  53. package/dist/application/SetupSkill.js +44 -40
  54. package/dist/application/TerminalRenderer.js +3 -1
  55. package/dist/application/Uninstall.js +7 -1
  56. package/dist/application/Upgrade.js +16 -1
  57. package/dist/browser/CdpEndpoint.js +37 -4
  58. package/dist/catalogIdentity.js +16 -7
  59. package/dist/cli/coreAnalysisCommands.js +34 -2
  60. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  61. package/dist/cli/setupCommands.js +84 -30
  62. package/dist/cli/utilityCommands.js +10 -2
  63. package/dist/cliApplicationCommands.js +79 -16
  64. package/dist/cliCommandNames.js +3 -0
  65. package/dist/cliElectronCommands.js +23 -33
  66. package/dist/cliObservationOptions.js +1 -1
  67. package/dist/cliOutput.js +56 -0
  68. package/dist/cliProcessCommands.js +32 -1
  69. package/dist/cliSetup.js +55 -29
  70. package/dist/contracts/applicationToolContracts.js +54 -7
  71. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  72. package/dist/contracts/electronToolContracts.js +73 -4
  73. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  74. package/dist/contracts/managedToolContracts.js +7 -2
  75. package/dist/contracts/processCaptureExample.js +1 -0
  76. package/dist/contracts/replayMachineExample.js +37 -0
  77. package/dist/contracts/sessionStatusContract.js +26 -0
  78. package/dist/contracts/toolContractExamples.js +2 -0
  79. package/dist/contracts/toolContractTypes.js +12 -1
  80. package/dist/contracts/toolContracts.js +8 -11
  81. package/dist/contracts/toolEffects.js +3 -0
  82. package/dist/contracts/toolOutputSchemaGroups.js +53 -9
  83. package/dist/contracts/toolOutputSchemaPrimitives.js +73 -32
  84. package/dist/doctorRuntime.js +41 -0
  85. package/dist/domain/analysisErrorPresentation.js +5 -3
  86. package/dist/domain/analysisErrorProjection.js +7 -1
  87. package/dist/domain/androidApplication.js +206 -0
  88. package/dist/domain/appleApplication.js +221 -0
  89. package/dist/domain/boundedCartesianProjection.js +15 -0
  90. package/dist/domain/changedBehavior.js +5 -4
  91. package/dist/domain/completionLedgerGeneration.js +190 -0
  92. package/dist/domain/errors.js +4 -2
  93. package/dist/domain/evidenceCompletionLedger.js +113 -0
  94. package/dist/domain/hopperStartupFailure.js +9 -0
  95. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  96. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  97. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  98. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  99. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  100. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  101. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  102. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  103. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  104. package/dist/domain/javascriptSemanticCalls.js +269 -0
  105. package/dist/domain/javascriptSemanticGraph.js +356 -0
  106. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  107. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  108. package/dist/domain/javascriptSemanticIr.js +14 -1
  109. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  110. package/dist/domain/javascriptSemanticProjection.js +36 -4
  111. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  112. package/dist/domain/javascriptSemanticQuery.js +294 -0
  113. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  114. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  115. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  116. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  117. package/dist/domain/javascriptSemanticReturns.js +187 -0
  118. package/dist/domain/javascriptSemanticState.js +32 -0
  119. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  120. package/dist/domain/javascriptSemanticValues.js +54 -70
  121. package/dist/domain/processCapture.js +36 -0
  122. package/dist/domain/processCaptureValidation.js +101 -12
  123. package/dist/domain/processComparison.js +96 -24
  124. package/dist/domain/processObservation.js +153 -0
  125. package/dist/domain/processPairedExperiment.js +128 -0
  126. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  127. package/dist/domain/processReactiveMatching.js +210 -0
  128. package/dist/domain/processReactiveRuntime.js +109 -0
  129. package/dist/domain/processReactiveScenario.js +361 -0
  130. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  131. package/dist/domain/processReactiveTransition.js +159 -0
  132. package/dist/domain/processScenario.js +12 -0
  133. package/dist/domain/processTraceComparison.js +43 -0
  134. package/dist/domain/processTraceDimensionProjection.js +50 -0
  135. package/dist/domain/processTraceEvaluation.js +312 -0
  136. package/dist/domain/processTraceSpecification.js +373 -0
  137. package/dist/domain/reconstructionVerification.js +5 -4
  138. package/dist/domain/replayMachine.js +332 -0
  139. package/dist/domain/replayMachineRun.js +216 -0
  140. package/dist/domain/replayMachineRuntime.js +294 -0
  141. package/dist/domain/runtimeIdentification.js +210 -0
  142. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  143. package/dist/evaluation/CodexAgentEval.js +156 -0
  144. package/dist/generatedPackageMetadata.js +2 -2
  145. package/dist/ghidra/GhidraClient.js +13 -2
  146. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  147. package/dist/ghidra/GhidraLauncher.js +7 -3
  148. package/dist/ghidra/GhidraProvider.js +8 -1
  149. package/dist/hopper/BridgeLauncher.js +56 -14
  150. package/dist/hopper/HopperClient.js +23 -10
  151. package/dist/hopper/HopperDiagnostics.js +14 -0
  152. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  153. package/dist/hopper/HopperProvider.js +8 -1
  154. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  155. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  156. package/dist/identity.js +2 -1
  157. package/dist/main/transport.js +2 -0
  158. package/dist/mcpDoctor.js +310 -0
  159. package/dist/process/ProcessOwnership.js +200 -53
  160. package/dist/process/ProviderRunLineage.js +21 -0
  161. package/dist/server/createServer.js +8 -2
  162. package/dist/server/javascriptApplicationResult.js +106 -0
  163. package/dist/server/registerApplicationTools/characterization.js +3 -3
  164. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  165. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  166. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  167. package/dist/server/registerApplicationTools/coverage.js +3 -3
  168. package/dist/server/registerApplicationTools/helpers.js +37 -25
  169. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  170. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  171. package/dist/server/registerApplicationTools.js +4 -0
  172. package/dist/server/registerElectronTools.js +19 -5
  173. package/dist/server/registerEvidenceResources.js +7 -1
  174. package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
  175. package/dist/server/registerManagedTools.js +18 -1
  176. package/dist/server/registerProcessComparisonTool.js +17 -7
  177. package/dist/server/registerReplayMachineTool.js +20 -0
  178. package/dist/server/registerSessionStatusTool.js +95 -25
  179. package/dist/server/registerSessionTools.js +12 -7
  180. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  181. package/dist/server/toolResult.js +10 -4
  182. package/dist/serverIdentity.js +19 -0
  183. package/package.json +62 -29
  184. package/scripts/hopper-demo-x11.py +459 -61
  185. package/scripts/prepack.mjs +55 -0
  186. package/scripts/prepare.mjs +28 -0
  187. package/scripts/rea.mjs +26 -5
  188. package/skills/reverse-engineer-anything/SKILL.md +69 -228
  189. package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
  190. package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
  191. package/skills/reverse-engineer-anything/references/javascript-applications.md +35 -0
  192. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
  193. package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
@@ -0,0 +1,217 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { hopperStartupExitCode, } from "../domain/hopperStartupFailure.js";
3
+ import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
4
+ import { ProviderProcessSupervisor, spawnOwnedProviderProcess, } from "../process/ProviderProcess.js";
5
+ import { parseLinuxPrivateDisplayDiagnostic } from "./LinuxPrivateDisplayDiagnostic.js";
6
+ const PYTHON_PATH = "/usr/bin/python3";
7
+ const UNSHARE_PATH = "/usr/bin/unshare";
8
+ const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
9
+ const PROBE_OUTPUT_LIMIT_BYTES = 64 * 1024;
10
+ /** Probe the exact Xvfb boundary and select the least-privileged viable strategy. */
11
+ export const selectLinuxPrivateDisplayStrategy = async (options) => {
12
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
13
+ const runner = options.runProbe ?? runLinuxPrivateDisplayProbe;
14
+ const direct = await evaluatedProbe("direct", options, timeoutMs, runner);
15
+ if (direct.ok)
16
+ return direct;
17
+ if (direct.diagnostic.failure_code !== "x11_socket_directory_unusable")
18
+ return unavailable(direct.diagnostic);
19
+ const isolated = await evaluatedProbe("user-mount-namespace", options, timeoutMs, runner);
20
+ if (!isolated.ok)
21
+ return unavailable({
22
+ ...direct.diagnostic,
23
+ strategy: "unavailable",
24
+ reason: "namespace_unavailable",
25
+ fallback_reason: isolated.diagnostic.reason,
26
+ effective_socket_directory_mode: isolated.diagnostic.effective_socket_directory_mode,
27
+ effective_mount_read_only: isolated.diagnostic.effective_mount_read_only,
28
+ xvfb_stderr_bytes: isolated.diagnostic.xvfb_stderr_bytes,
29
+ xvfb_stderr_truncated: isolated.diagnostic.xvfb_stderr_truncated,
30
+ });
31
+ return {
32
+ ok: true,
33
+ strategy: "user-mount-namespace",
34
+ diagnostic: {
35
+ ...isolated.diagnostic,
36
+ socket_directory_mode: direct.diagnostic.socket_directory_mode,
37
+ mount_read_only: direct.diagnostic.mount_read_only,
38
+ fallback_reason: null,
39
+ },
40
+ };
41
+ };
42
+ /** Run one helper probe in an owned process group with a hard output bound. */
43
+ export const runLinuxPrivateDisplayProbe = async (strategy, options) => {
44
+ if (options.signal?.aborted === true)
45
+ return emptyProcessResult({ cancelled: true });
46
+ const command = privateDisplayProbeCommand(options.helperPath, strategy);
47
+ let started;
48
+ try {
49
+ started = await spawnOwnedProviderProcess({
50
+ command: command.command,
51
+ arguments: command.arguments,
52
+ runId: randomUUID(),
53
+ expectedCommand: null,
54
+ });
55
+ }
56
+ catch {
57
+ return emptyProcessResult();
58
+ }
59
+ const launch = {
60
+ process: started.process,
61
+ ownsProcessLifetime: true,
62
+ cleanup: () => cleanupOwnedProcessGroup(started.ownership),
63
+ };
64
+ const supervisor = new ProviderProcessSupervisor(launch, {
65
+ maxOutputBytesPerStream: PROBE_OUTPUT_LIMIT_BYTES,
66
+ });
67
+ const outcome = await waitForProbeExit(supervisor, options.timeoutMs, options.signal);
68
+ let cleanupIncomplete = false;
69
+ if (outcome !== "exit") {
70
+ const stopped = await supervisor.stop();
71
+ cleanupIncomplete = stopped.status === "incomplete";
72
+ }
73
+ else
74
+ supervisor.dispose();
75
+ const snapshot = supervisor.snapshot();
76
+ return {
77
+ exitCode: snapshot.exitCode,
78
+ stderr: snapshot.stderr.text,
79
+ stderrBytes: snapshot.stderr.bytes,
80
+ stderrTruncated: snapshot.stderr.truncated,
81
+ timedOut: outcome === "timeout",
82
+ cancelled: outcome === "cancelled",
83
+ cleanupIncomplete,
84
+ };
85
+ };
86
+ const privateDisplayProbeCommand = (helperPath, strategy) => strategy === "direct"
87
+ ? {
88
+ command: PYTHON_PATH,
89
+ arguments: [helperPath, "--probe", "--strategy", "direct"],
90
+ }
91
+ : {
92
+ command: UNSHARE_PATH,
93
+ arguments: [
94
+ "--user",
95
+ "--map-root-user",
96
+ "--mount",
97
+ "--propagation",
98
+ "private",
99
+ PYTHON_PATH,
100
+ helperPath,
101
+ "--probe",
102
+ "--strategy",
103
+ "user-mount-namespace",
104
+ "--mount-private-x11",
105
+ ],
106
+ };
107
+ const evaluatedProbe = async (strategy, options, timeoutMs, runner) => {
108
+ let processResult;
109
+ try {
110
+ processResult = await runner(strategy, {
111
+ helperPath: options.helperPath,
112
+ timeoutMs,
113
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
114
+ });
115
+ }
116
+ catch {
117
+ return {
118
+ ok: false,
119
+ diagnostic: syntheticDiagnostic(strategy, "probe_failed"),
120
+ };
121
+ }
122
+ if (processResult.cancelled)
123
+ return {
124
+ ok: false,
125
+ diagnostic: syntheticDiagnostic(strategy, "cancelled"),
126
+ };
127
+ if (processResult.timedOut)
128
+ return {
129
+ ok: false,
130
+ diagnostic: syntheticDiagnostic(strategy, "probe_timeout"),
131
+ };
132
+ if (processResult.cleanupIncomplete)
133
+ return {
134
+ ok: false,
135
+ diagnostic: syntheticDiagnostic(strategy, "cleanup_incomplete"),
136
+ };
137
+ const parsed = parseLinuxPrivateDisplayDiagnostic(processResult.stderr, processResult.stderrTruncated);
138
+ if (!parsed.ok)
139
+ return {
140
+ ok: false,
141
+ diagnostic: syntheticDiagnostic(strategy, parsed.reason),
142
+ };
143
+ const diagnostic = parsed.value;
144
+ if (diagnostic.strategy !== strategy ||
145
+ diagnostic.operation !== "probe" ||
146
+ !exitMatchesDiagnostic(processResult.exitCode, diagnostic))
147
+ return {
148
+ ok: false,
149
+ diagnostic: syntheticDiagnostic(strategy, "diagnostic_malformed"),
150
+ };
151
+ return diagnostic.status === "ready"
152
+ ? { ok: true, strategy, diagnostic }
153
+ : { ok: false, diagnostic };
154
+ };
155
+ const exitMatchesDiagnostic = (exitCode, diagnostic) => diagnostic.status === "ready"
156
+ ? exitCode === 0
157
+ : diagnostic.failure_code !== null &&
158
+ hopperStartupExitCode(diagnostic.failure_code) === exitCode;
159
+ const syntheticDiagnostic = (strategy, reason, failureCode = "private_display_unavailable") => ({
160
+ schema_version: 1,
161
+ component: "hopper_private_display",
162
+ operation: "probe",
163
+ status: "error",
164
+ failure_code: failureCode,
165
+ reason,
166
+ socket_directory: "/tmp/.X11-unix",
167
+ socket_directory_mode: null,
168
+ mount_read_only: null,
169
+ effective_socket_directory_mode: null,
170
+ effective_mount_read_only: null,
171
+ wsl: false,
172
+ strategy,
173
+ fallback_reason: null,
174
+ xvfb_stderr_bytes: 0,
175
+ xvfb_stderr_truncated: false,
176
+ });
177
+ const unavailable = (diagnostic) => ({
178
+ ok: false,
179
+ strategy: "unavailable",
180
+ exitCode: diagnostic.failure_code === null
181
+ ? 70
182
+ : (hopperStartupExitCode(diagnostic.failure_code) ?? 70),
183
+ diagnostic: { ...diagnostic, strategy: "unavailable" },
184
+ });
185
+ const emptyProcessResult = (overrides = {}) => ({
186
+ exitCode: undefined,
187
+ stderr: "",
188
+ stderrBytes: 0,
189
+ stderrTruncated: false,
190
+ timedOut: false,
191
+ cancelled: false,
192
+ cleanupIncomplete: false,
193
+ ...overrides,
194
+ });
195
+ const waitForProbeExit = async (supervisor, timeoutMs, signal) => {
196
+ if (signal?.aborted === true)
197
+ return "cancelled";
198
+ let releaseAbort;
199
+ const aborted = new Promise((resolve) => {
200
+ if (signal === undefined)
201
+ return;
202
+ const listener = () => resolve("cancelled");
203
+ signal.addEventListener("abort", listener, { once: true });
204
+ releaseAbort = () => signal.removeEventListener("abort", listener);
205
+ });
206
+ try {
207
+ return await Promise.race([
208
+ supervisor
209
+ .waitForExit(Math.max(1, timeoutMs))
210
+ .then((exited) => (exited ? "exit" : "timeout")),
211
+ aborted,
212
+ ]);
213
+ }
214
+ finally {
215
+ releaseAbort?.();
216
+ }
217
+ };
package/dist/identity.js CHANGED
@@ -7,9 +7,10 @@ export const PRODUCT_IDENTITY = {
7
7
  displayName: "REA",
8
8
  packageName: PACKAGE_METADATA.name,
9
9
  packageSpecifier: `${PACKAGE_METADATA.name}@latest`,
10
+ registrationPackageSpecifier: `${PACKAGE_METADATA.name}@${PACKAGE_METADATA.version}`,
10
11
  packageVersion: PACKAGE_METADATA.version,
11
12
  cliBinary: "rea",
12
- mcpCommand: `npx -y ${PACKAGE_METADATA.name}@latest mcp`,
13
+ mcpCommand: `npx -y ${PACKAGE_METADATA.name}@${PACKAGE_METADATA.version} mcp`,
13
14
  mcpServerKey: "rea",
14
15
  skillName: "reverse-engineer-anything",
15
16
  skillVersion: PACKAGE_METADATA.skillVersion,
@@ -24,6 +24,8 @@ export const startMcpTransport = async (dependencies, session, serverContext) =>
24
24
  .enabled,
25
25
  evidenceFileRoots: serverContext.runtimeState.currentConfig.evidenceFilePolicy
26
26
  .roots.length,
27
+ investigationInputRoots: serverContext.runtimeState.currentConfig.investigationInputRoots
28
+ .length,
27
29
  browserObservationEnabled: serverContext.runtimeState.currentConfig
28
30
  .browserObservationEnabled &&
29
31
  serverContext.runtimeState.currentConfig.browserCdpEndpoints
@@ -0,0 +1,310 @@
1
+ import { Client } from "@modelcontextprotocol/client";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
3
+ import { Formatter } from "incur";
4
+ import { resolve } from "node:path";
5
+ import { CATALOG_IDENTITY, MCP_RESOURCE_CATALOG, MCP_RESOURCE_TEMPLATE_CATALOG, } from "./catalogIdentity.js";
6
+ import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
7
+ import { PRODUCT_IDENTITY } from "./identity.js";
8
+ const DEFAULT_DEADLINE_MS = 20_000;
9
+ const STDERR_LIMIT_BYTES = 64 * 1_024;
10
+ const OUTPUT_FORMATS = ["toon", "json", "yaml", "md", "jsonl"];
11
+ /** Diagnose the actual production stdio adapter with one bounded SDK session. */
12
+ export const runProductionMcpDoctor = async (options) => {
13
+ const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(new Error("Production MCP doctor deadline expired")), Math.max(1, deadline - Date.now()));
16
+ const stderr = boundedStderr();
17
+ const transport = new TrackedStdioClientTransport({
18
+ command: options.command,
19
+ args: [...options.args],
20
+ cwd: options.cwd,
21
+ env: targetFreeEnvironment(options.environment),
22
+ stderr: "pipe",
23
+ });
24
+ transport.stderr?.on("data", stderr.append);
25
+ const client = new Client({ name: "rea-mcp-doctor", version: "1" });
26
+ try {
27
+ await client.connect(transport, requestOptions(deadline, controller.signal));
28
+ return {
29
+ ...(await inspectProductionMcpSession(client, deadline, controller.signal)),
30
+ diagnostics: stderr.value(),
31
+ };
32
+ }
33
+ catch (cause) {
34
+ return {
35
+ healthy: false,
36
+ adapter: "production-stdio",
37
+ server: null,
38
+ inventory: null,
39
+ identity: null,
40
+ request_flow: { tool: "binary_session", ok: false },
41
+ checks: [
42
+ {
43
+ name: "transport",
44
+ ok: false,
45
+ detail: errorMessage(cause),
46
+ },
47
+ ],
48
+ diagnostics: stderr.value(),
49
+ };
50
+ }
51
+ finally {
52
+ clearTimeout(timer);
53
+ await closeMcpChild(client, transport);
54
+ }
55
+ };
56
+ const inspectProductionMcpSession = async (client, deadline, signal) => {
57
+ const request = requestOptions(deadline, signal);
58
+ const [tools, prompts, resources, templates, identity, requestFlow] = await Promise.all([
59
+ client.listTools(undefined, request),
60
+ client.listPrompts(undefined, request),
61
+ client.listResources(undefined, request),
62
+ client.listResourceTemplates(undefined, request),
63
+ client.readResource({ uri: "rea://server/identity" }, request),
64
+ client.callTool({ name: "binary_session", arguments: {} }, request),
65
+ ]);
66
+ const inventory = {
67
+ tools: compareInventory(CATALOG_IDENTITY.tools.map(({ name }) => name), tools.tools.map(({ name }) => name)),
68
+ prompts: compareInventory(PROMPT_CONTRACTS.map(({ name }) => name), prompts.prompts.map(({ name }) => name)),
69
+ resources: compareInventory(MCP_RESOURCE_CATALOG.map(({ name }) => name), resources.resources.map(({ name }) => name)),
70
+ resource_templates: compareInventory(MCP_RESOURCE_TEMPLATE_CATALOG.map(({ name }) => name), templates.resourceTemplates.map(({ name }) => name)),
71
+ };
72
+ const observedIdentity = parseIdentityResource(identity.contents);
73
+ const serverVersion = client.getServerVersion();
74
+ const protocolVersion = client.getNegotiatedProtocolVersion();
75
+ const checks = [
76
+ {
77
+ name: "initialize",
78
+ ok: serverVersion?.name === PRODUCT_IDENTITY.mcpServerKey &&
79
+ serverVersion.version === PRODUCT_IDENTITY.packageVersion &&
80
+ protocolVersion !== undefined,
81
+ detail: `${serverVersion?.name ?? "unknown"}@${serverVersion?.version ?? "unknown"}; protocol=${protocolVersion ?? "unknown"}`,
82
+ },
83
+ ...Object.entries(inventory).map(([name, comparison]) => inventoryCheck(name, comparison)),
84
+ {
85
+ name: "server-identity",
86
+ ok: observedIdentity.packageVersion === PRODUCT_IDENTITY.packageVersion &&
87
+ observedIdentity.catalogDigest ===
88
+ CATALOG_IDENTITY.digests.combined_sha256,
89
+ detail: `package=${observedIdentity.packageVersion ?? "missing"}; catalog=${observedIdentity.catalogDigest ?? "missing"}`,
90
+ },
91
+ {
92
+ name: "request-flow",
93
+ ok: requestFlow.isError !== true,
94
+ detail: "binary_session target-free request",
95
+ },
96
+ ];
97
+ return {
98
+ healthy: checks.every(({ ok }) => ok),
99
+ adapter: "production-stdio",
100
+ server: {
101
+ name: serverVersion?.name ?? null,
102
+ version: serverVersion?.version ?? null,
103
+ protocol_version: protocolVersion ?? null,
104
+ },
105
+ inventory,
106
+ identity: {
107
+ expected_package_version: PRODUCT_IDENTITY.packageVersion,
108
+ observed_package_version: observedIdentity.packageVersion,
109
+ expected_catalog_digest: CATALOG_IDENTITY.digests.combined_sha256,
110
+ observed_catalog_digest: observedIdentity.catalogDigest,
111
+ },
112
+ request_flow: {
113
+ tool: "binary_session",
114
+ ok: requestFlow.isError !== true,
115
+ },
116
+ checks,
117
+ };
118
+ };
119
+ /**
120
+ * Dispatcher entry for `rea mcp doctor`, kept separate from Incur MCP setup.
121
+ * @public
122
+ */
123
+ export const runProductionMcpDoctorCli = async (arguments_, input) => {
124
+ const parsed = parseOutputArguments(arguments_);
125
+ if (!parsed.ok)
126
+ return {
127
+ output: `${Formatter.format({ code: "VALIDATION_ERROR", message: parsed.message }, parsed.format)}\n`,
128
+ exitCode: 1,
129
+ };
130
+ const result = await runProductionMcpDoctor({
131
+ command: process.execPath,
132
+ args: [resolve(input.dispatcherPath), "mcp"],
133
+ cwd: input.packageRoot,
134
+ environment: process.env,
135
+ });
136
+ return {
137
+ output: `${Formatter.format(result, parsed.format)}\n`,
138
+ exitCode: result.healthy ? 0 : 1,
139
+ };
140
+ };
141
+ /** Compare exact names while retaining actionable mismatch locations. */
142
+ export const compareInventory = (expectedNames, observedNames) => {
143
+ const expected = new Set(expectedNames);
144
+ const observed = new Set(observedNames);
145
+ return {
146
+ expected: expectedNames.length,
147
+ observed: observedNames.length,
148
+ missing: [...expected].filter((name) => !observed.has(name)).sort(),
149
+ unexpected: [...observed].filter((name) => !expected.has(name)).sort(),
150
+ duplicates: [...observed]
151
+ .filter((name) => observedNames.filter((candidate) => candidate === name).length > 1)
152
+ .sort(),
153
+ };
154
+ };
155
+ const inventoryCheck = (name, comparison) => ({
156
+ name: `inventory:${name}`,
157
+ ok: comparison.expected === comparison.observed &&
158
+ comparison.missing.length === 0 &&
159
+ comparison.unexpected.length === 0 &&
160
+ comparison.duplicates.length === 0,
161
+ detail: `expected=${String(comparison.expected)} observed=${String(comparison.observed)} missing=${comparison.missing.join(",") || "none"} unexpected=${comparison.unexpected.join(",") || "none"} duplicates=${comparison.duplicates.join(",") || "none"}`,
162
+ });
163
+ const parseIdentityResource = (contents) => {
164
+ const first = contents.find((candidate) => isRecord(candidate) && typeof candidate.text === "string");
165
+ if (!isRecord(first) || typeof first.text !== "string")
166
+ return { packageVersion: null, catalogDigest: null };
167
+ try {
168
+ const identity = JSON.parse(first.text);
169
+ if (!isRecord(identity))
170
+ return { packageVersion: null, catalogDigest: null };
171
+ const packageIdentity = identity.package;
172
+ const catalog = identity.catalog;
173
+ const digests = isRecord(catalog) ? catalog.digests : undefined;
174
+ return {
175
+ packageVersion: isRecord(packageIdentity) && typeof packageIdentity.version === "string"
176
+ ? packageIdentity.version
177
+ : null,
178
+ catalogDigest: isRecord(digests) && typeof digests.combined_sha256 === "string"
179
+ ? digests.combined_sha256
180
+ : null,
181
+ };
182
+ }
183
+ catch {
184
+ return { packageVersion: null, catalogDigest: null };
185
+ }
186
+ };
187
+ const parseOutputArguments = (arguments_) => {
188
+ let format = "toon";
189
+ for (let index = 0; index < arguments_.length; index += 1) {
190
+ const argument = arguments_[index];
191
+ if (argument === "--json") {
192
+ format = "json";
193
+ continue;
194
+ }
195
+ const inline = argument?.startsWith("--format=")
196
+ ? argument.slice("--format=".length)
197
+ : undefined;
198
+ if (argument === "--format" || inline !== undefined) {
199
+ const candidate = inline ?? arguments_[index + 1];
200
+ if (inline === undefined)
201
+ index += 1;
202
+ if (!isOutputFormat(candidate))
203
+ return {
204
+ ok: false,
205
+ format,
206
+ message: `Invalid output format: ${candidate ?? "missing"}`,
207
+ };
208
+ format = candidate;
209
+ continue;
210
+ }
211
+ if (argument === "--full-output")
212
+ continue;
213
+ return {
214
+ ok: false,
215
+ format,
216
+ message: `Unknown mcp doctor option: ${argument ?? "missing"}`,
217
+ };
218
+ }
219
+ return { ok: true, format };
220
+ };
221
+ const isOutputFormat = (value) => OUTPUT_FORMATS.some((candidate) => candidate === value);
222
+ const requestOptions = (deadline, signal) => ({
223
+ signal,
224
+ timeout: Math.max(1, deadline - Date.now()),
225
+ });
226
+ const targetFreeEnvironment = (environment) => {
227
+ const result = Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined));
228
+ for (const name of [
229
+ "HOPPER_TARGET_PATH",
230
+ "HOPPER_TARGET_KIND",
231
+ "HOPPER_LOADER_ARGS_JSON",
232
+ ])
233
+ delete result[name];
234
+ return result;
235
+ };
236
+ const boundedStderr = () => {
237
+ let text = "";
238
+ let bytes = 0;
239
+ let truncated = false;
240
+ return {
241
+ append(chunk) {
242
+ const buffer = Buffer.isBuffer(chunk)
243
+ ? chunk
244
+ : Buffer.from(String(chunk), "utf8");
245
+ bytes += buffer.byteLength;
246
+ const retained = Buffer.byteLength(text, "utf8");
247
+ if (retained < STDERR_LIMIT_BYTES)
248
+ text += buffer
249
+ .subarray(0, STDERR_LIMIT_BYTES - retained)
250
+ .toString("utf8");
251
+ if (bytes > STDERR_LIMIT_BYTES)
252
+ truncated = true;
253
+ },
254
+ value: () => ({
255
+ stderr: text,
256
+ stderr_bytes: bytes,
257
+ stderr_truncated: truncated,
258
+ }),
259
+ };
260
+ };
261
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
262
+ const errorMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
263
+ class TrackedStdioClientTransport extends StdioClientTransport {
264
+ spawnedPid = null;
265
+ async start() {
266
+ await super.start();
267
+ this.spawnedPid = this.pid;
268
+ }
269
+ }
270
+ const closeMcpChild = async (client, transport) => {
271
+ const pid = transport.spawnedPid ?? transport.pid;
272
+ await Promise.allSettled([client.close()]);
273
+ await Promise.allSettled([transport.close()]);
274
+ if (pid === null || !(await processExists(pid)))
275
+ return;
276
+ await terminateProcess(pid, "SIGTERM");
277
+ if (await waitForProcessExit(pid, 1_000))
278
+ return;
279
+ await terminateProcess(pid, "SIGKILL");
280
+ await waitForProcessExit(pid, 1_000);
281
+ };
282
+ const terminateProcess = async (pid, signal) => {
283
+ try {
284
+ process.kill(pid, signal);
285
+ }
286
+ catch (cause) {
287
+ if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH"))
288
+ throw cause;
289
+ }
290
+ };
291
+ const waitForProcessExit = async (pid, timeoutMs) => {
292
+ const deadline = Date.now() + timeoutMs;
293
+ do {
294
+ if (!(await processExists(pid)))
295
+ return true;
296
+ await new Promise((resolve) => setTimeout(resolve, 20));
297
+ } while (Date.now() < deadline);
298
+ return !(await processExists(pid));
299
+ };
300
+ const processExists = async (pid) => {
301
+ try {
302
+ process.kill(pid, 0);
303
+ return true;
304
+ }
305
+ catch (cause) {
306
+ if (cause instanceof Error && "code" in cause && cause.code === "ESRCH")
307
+ return false;
308
+ throw cause;
309
+ }
310
+ };