swipium 1.4.0 → 2.0.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 (541) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +175 -219
  3. package/THREAT_MODEL.md +223 -47
  4. package/dist/appMap/automationLink.js +39 -26
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +68 -20
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +31 -5
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +23 -5
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +44 -8
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +24 -6
  15. package/dist/appMap/firstRunApply.js.map +1 -1
  16. package/dist/appMap/fsWalk.js +19 -2
  17. package/dist/appMap/fsWalk.js.map +1 -1
  18. package/dist/appMap/issues.js +10 -30
  19. package/dist/appMap/issues.js.map +1 -1
  20. package/dist/appMap/migrations.js +6 -6
  21. package/dist/appMap/migrations.js.map +1 -1
  22. package/dist/appMap/prelaunch.js +2 -2
  23. package/dist/appMap/prelaunch.js.map +1 -1
  24. package/dist/appMap/projectRegistry.js +83 -11
  25. package/dist/appMap/projectRegistry.js.map +1 -1
  26. package/dist/appMap/provenance.js +2 -2
  27. package/dist/appMap/provenance.js.map +1 -1
  28. package/dist/appMap/query.js +21 -5
  29. package/dist/appMap/query.js.map +1 -1
  30. package/dist/appMap/runtimeMerge.js +12 -5
  31. package/dist/appMap/runtimeMerge.js.map +1 -1
  32. package/dist/appMap/schema.js +1 -1
  33. package/dist/appMap/schema.js.map +1 -1
  34. package/dist/appMap/screenMatch.js +6 -2
  35. package/dist/appMap/screenMatch.js.map +1 -1
  36. package/dist/appMap/staticScan.js +151 -39
  37. package/dist/appMap/staticScan.js.map +1 -1
  38. package/dist/appMap/store.js +58 -85
  39. package/dist/appMap/store.js.map +1 -1
  40. package/dist/appMap/tsAstScan.js +34 -12
  41. package/dist/appMap/tsAstScan.js.map +1 -1
  42. package/dist/artifacts/bundletool.js +49 -12
  43. package/dist/artifacts/bundletool.js.map +1 -1
  44. package/dist/artifacts/resolve.js +41 -14
  45. package/dist/artifacts/resolve.js.map +1 -1
  46. package/dist/automation/capabilities.js +112 -30
  47. package/dist/automation/capabilities.js.map +1 -1
  48. package/dist/automation/gestures.js +6 -2
  49. package/dist/automation/gestures.js.map +1 -1
  50. package/dist/automation/plan.js +71 -23
  51. package/dist/automation/plan.js.map +1 -1
  52. package/dist/automation/report.js +15 -5
  53. package/dist/automation/report.js.map +1 -1
  54. package/dist/automation/selectors.js +22 -28
  55. package/dist/automation/selectors.js.map +1 -1
  56. package/dist/automation/types.js +1 -1
  57. package/dist/automation/types.js.map +1 -1
  58. package/dist/automation/waits.js +14 -8
  59. package/dist/automation/waits.js.map +1 -1
  60. package/dist/automation/webview.js +9 -3
  61. package/dist/automation/webview.js.map +1 -1
  62. package/dist/automationGen/appiumModel.js +16 -8
  63. package/dist/automationGen/appiumModel.js.map +1 -1
  64. package/dist/automationGen/ciEmitter.js +6 -6
  65. package/dist/automationGen/ciEmitter.js.map +1 -1
  66. package/dist/automationGen/identifiers.js +224 -0
  67. package/dist/automationGen/identifiers.js.map +1 -0
  68. package/dist/automationGen/jsEmitter.js +288 -121
  69. package/dist/automationGen/jsEmitter.js.map +1 -1
  70. package/dist/automationGen/packagePatch.js +2 -2
  71. package/dist/automationGen/packagePatch.js.map +1 -1
  72. package/dist/automationGen/platformResolve.js +36 -0
  73. package/dist/automationGen/platformResolve.js.map +1 -0
  74. package/dist/automationGen/projectProfile.js +76 -27
  75. package/dist/automationGen/projectProfile.js.map +1 -1
  76. package/dist/automationGen/pythonEmitter.js +278 -107
  77. package/dist/automationGen/pythonEmitter.js.map +1 -1
  78. package/dist/automationGen/readmeEmitter.js +7 -6
  79. package/dist/automationGen/readmeEmitter.js.map +1 -1
  80. package/dist/automationGen/run.js +388 -0
  81. package/dist/automationGen/run.js.map +1 -0
  82. package/dist/automationGen/suitePlan.js +17 -10
  83. package/dist/automationGen/suitePlan.js.map +1 -1
  84. package/dist/automationGen/validation.js +52 -12
  85. package/dist/automationGen/validation.js.map +1 -1
  86. package/dist/build/parseBuildLog.js +235 -31
  87. package/dist/build/parseBuildLog.js.map +1 -1
  88. package/dist/build/plan.js +39 -18
  89. package/dist/build/plan.js.map +1 -1
  90. package/dist/ci/preflight.js +13 -4
  91. package/dist/ci/preflight.js.map +1 -1
  92. package/dist/cli/gc.js +74 -0
  93. package/dist/cli/gc.js.map +1 -0
  94. package/dist/cli/init.js +217 -26
  95. package/dist/cli/init.js.map +1 -1
  96. package/dist/cli/main.js +50 -0
  97. package/dist/cli/main.js.map +1 -0
  98. package/dist/cli/report.js +218 -0
  99. package/dist/cli/report.js.map +1 -0
  100. package/dist/cli/scan.js +36 -15
  101. package/dist/cli/scan.js.map +1 -1
  102. package/dist/cli/suite.js +7 -7
  103. package/dist/cli/suite.js.map +1 -1
  104. package/dist/cli/verify.js +7 -7
  105. package/dist/cli/verify.js.map +1 -1
  106. package/dist/consent/consent.js +162 -5
  107. package/dist/consent/consent.js.map +1 -1
  108. package/dist/context/detect.js +55 -18
  109. package/dist/context/detect.js.map +1 -1
  110. package/dist/context/findApps.js +22 -8
  111. package/dist/context/findApps.js.map +1 -1
  112. package/dist/context/projectRoot.js +174 -19
  113. package/dist/context/projectRoot.js.map +1 -1
  114. package/dist/context/scan.js +1 -1
  115. package/dist/context/scan.js.map +1 -1
  116. package/dist/core/capabilityGroups.js +90 -0
  117. package/dist/core/capabilityGroups.js.map +1 -0
  118. package/dist/core/target.js +10 -8
  119. package/dist/core/target.js.map +1 -1
  120. package/dist/core/targetPlan.js +160 -39
  121. package/dist/core/targetPlan.js.map +1 -1
  122. package/dist/drivers/DirectDriver.js +317 -43
  123. package/dist/drivers/DirectDriver.js.map +1 -1
  124. package/dist/drivers/SimctlDriver.js +124 -8
  125. package/dist/drivers/SimctlDriver.js.map +1 -1
  126. package/dist/drivers/WdaDriver.js +365 -55
  127. package/dist/drivers/WdaDriver.js.map +1 -1
  128. package/dist/explore/candidates.js +7 -3
  129. package/dist/explore/candidates.js.map +1 -1
  130. package/dist/explore/graph.js +15 -6
  131. package/dist/explore/graph.js.map +1 -1
  132. package/dist/explore/memory.js +4 -1
  133. package/dist/explore/memory.js.map +1 -1
  134. package/dist/explore/planner.js +54 -9
  135. package/dist/explore/planner.js.map +1 -1
  136. package/dist/explore/policy.js +11 -6
  137. package/dist/explore/policy.js.map +1 -1
  138. package/dist/explore/runner.js +363 -190
  139. package/dist/explore/runner.js.map +1 -1
  140. package/dist/explore/signatures.js +4 -2
  141. package/dist/explore/signatures.js.map +1 -1
  142. package/dist/explore/suite.js +3 -1
  143. package/dist/explore/suite.js.map +1 -1
  144. package/dist/featureTesting/executionBootstrap.js +137 -29
  145. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  146. package/dist/featureTesting/featureMap.js +6 -3
  147. package/dist/featureTesting/featureMap.js.map +1 -1
  148. package/dist/featureTesting/featureScope.js +101 -20
  149. package/dist/featureTesting/featureScope.js.map +1 -1
  150. package/dist/featureTesting/mapFeatureScope.js +44 -15
  151. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  152. package/dist/featureTesting/objectiveModel.js +15 -11
  153. package/dist/featureTesting/objectiveModel.js.map +1 -1
  154. package/dist/featureTesting/resultMerge.js +16 -8
  155. package/dist/featureTesting/resultMerge.js.map +1 -1
  156. package/dist/featureTesting/sources.js +8 -5
  157. package/dist/featureTesting/sources.js.map +1 -1
  158. package/dist/featureTesting/suiteBridge.js +8 -2
  159. package/dist/featureTesting/suiteBridge.js.map +1 -1
  160. package/dist/featureTesting/synonyms.js +32 -4
  161. package/dist/featureTesting/synonyms.js.map +1 -1
  162. package/dist/featureTesting/testCaseFactory.js +27 -15
  163. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  164. package/dist/featureTesting/testPlan.js +34 -12
  165. package/dist/featureTesting/testPlan.js.map +1 -1
  166. package/dist/firstRun/authStateMachine.js +1 -1
  167. package/dist/firstRun/authStateMachine.js.map +1 -1
  168. package/dist/firstRun/classifyScreen.js +9 -6
  169. package/dist/firstRun/classifyScreen.js.map +1 -1
  170. package/dist/firstRun/firstRunPlanner.js +16 -13
  171. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  172. package/dist/firstRun/firstRunRunner.js +92 -31
  173. package/dist/firstRun/firstRunRunner.js.map +1 -1
  174. package/dist/firstRun/generatedDataPolicy.js +25 -15
  175. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  176. package/dist/firstRun/inputPlanner.js +45 -11
  177. package/dist/firstRun/inputPlanner.js.map +1 -1
  178. package/dist/firstRun/onboardingStateMachine.js +1 -1
  179. package/dist/firstRun/onboardingStateMachine.js.map +1 -1
  180. package/dist/firstRun/paywallPolicy.js +3 -3
  181. package/dist/firstRun/paywallPolicy.js.map +1 -1
  182. package/dist/firstRun/types.js +1 -1
  183. package/dist/firstRun/types.js.map +1 -1
  184. package/dist/fixtures/catalog.js +72 -9
  185. package/dist/fixtures/catalog.js.map +1 -1
  186. package/dist/fixtures/load.js +50 -0
  187. package/dist/fixtures/load.js.map +1 -0
  188. package/dist/flows/discover.js +2 -2
  189. package/dist/flows/discover.js.map +1 -1
  190. package/dist/flows/generate.js +68 -17
  191. package/dist/flows/generate.js.map +1 -1
  192. package/dist/flows/lint.js +88 -14
  193. package/dist/flows/lint.js.map +1 -1
  194. package/dist/flows/pack.js +21 -5
  195. package/dist/flows/pack.js.map +1 -1
  196. package/dist/flows/paths.js +57 -0
  197. package/dist/flows/paths.js.map +1 -0
  198. package/dist/flows/repair.js +176 -37
  199. package/dist/flows/repair.js.map +1 -1
  200. package/dist/flows/run.js +308 -99
  201. package/dist/flows/run.js.map +1 -1
  202. package/dist/flows/schema.js +107 -25
  203. package/dist/flows/schema.js.map +1 -1
  204. package/dist/flows/seedExec.js +8 -4
  205. package/dist/flows/seedExec.js.map +1 -1
  206. package/dist/flows/templates.js +5 -5
  207. package/dist/index.js +50 -8
  208. package/dist/index.js.map +1 -1
  209. package/dist/ios/signing.js +32 -3
  210. package/dist/ios/signing.js.map +1 -1
  211. package/dist/issues/classify.js +28 -20
  212. package/dist/issues/classify.js.map +1 -1
  213. package/dist/issues/fingerprint.js +35 -9
  214. package/dist/issues/fingerprint.js.map +1 -1
  215. package/dist/issues/index.js +93 -46
  216. package/dist/issues/index.js.map +1 -1
  217. package/dist/issues/metrics.js +6 -5
  218. package/dist/issues/metrics.js.map +1 -1
  219. package/dist/issues/recurrence.js +35 -8
  220. package/dist/issues/recurrence.js.map +1 -1
  221. package/dist/issues/report.js +12 -8
  222. package/dist/issues/report.js.map +1 -1
  223. package/dist/issues/reportBridge.js +40 -9
  224. package/dist/issues/reportBridge.js.map +1 -1
  225. package/dist/issues/schema.js +2 -2
  226. package/dist/issues/schema.js.map +1 -1
  227. package/dist/issues/sourceRevision.js +2 -2
  228. package/dist/issues/sourceRevision.js.map +1 -1
  229. package/dist/issues/store.js +110 -32
  230. package/dist/issues/store.js.map +1 -1
  231. package/dist/lib/abortScope.js +48 -0
  232. package/dist/lib/abortScope.js.map +1 -0
  233. package/dist/lib/android.js +84 -8
  234. package/dist/lib/android.js.map +1 -1
  235. package/dist/lib/coordSpace.js +1 -1
  236. package/dist/lib/coordSpace.js.map +1 -1
  237. package/dist/lib/device.js +53 -12
  238. package/dist/lib/device.js.map +1 -1
  239. package/dist/lib/gestures.js +141 -0
  240. package/dist/lib/gestures.js.map +1 -0
  241. package/dist/lib/gitignore.js +4 -3
  242. package/dist/lib/gitignore.js.map +1 -1
  243. package/dist/lib/image.js +12 -3
  244. package/dist/lib/image.js.map +1 -1
  245. package/dist/lib/lockfile.js +399 -0
  246. package/dist/lib/lockfile.js.map +1 -0
  247. package/dist/lib/needsInput.js +9 -6
  248. package/dist/lib/needsInput.js.map +1 -1
  249. package/dist/lib/png.js +4 -3
  250. package/dist/lib/png.js.map +1 -1
  251. package/dist/lib/redact.js +170 -9
  252. package/dist/lib/redact.js.map +1 -1
  253. package/dist/lib/result.js +94 -10
  254. package/dist/lib/result.js.map +1 -1
  255. package/dist/lib/schemaHash.js +7 -5
  256. package/dist/lib/schemaHash.js.map +1 -1
  257. package/dist/lib/sensitive.js +6 -3
  258. package/dist/lib/sensitive.js.map +1 -1
  259. package/dist/lib/simctl.js +3 -3
  260. package/dist/lib/simctl.js.map +1 -1
  261. package/dist/lib/spawn.js +78 -17
  262. package/dist/lib/spawn.js.map +1 -1
  263. package/dist/lib/toolAnnotations.js +114 -0
  264. package/dist/lib/toolAnnotations.js.map +1 -0
  265. package/dist/lib/wda.js +321 -25
  266. package/dist/lib/wda.js.map +1 -1
  267. package/dist/lib/wdaConfig.js +12 -5
  268. package/dist/lib/wdaConfig.js.map +1 -1
  269. package/dist/lib/wdaTune.js +52 -23
  270. package/dist/lib/wdaTune.js.map +1 -1
  271. package/dist/mobileAudit/checks.js +150 -28
  272. package/dist/mobileAudit/checks.js.map +1 -1
  273. package/dist/mobileAudit/evidence.js +4 -4
  274. package/dist/mobileAudit/evidence.js.map +1 -1
  275. package/dist/mobileAudit/profiles.js +121 -20
  276. package/dist/mobileAudit/profiles.js.map +1 -1
  277. package/dist/mobileAudit/results.js +9 -4
  278. package/dist/mobileAudit/results.js.map +1 -1
  279. package/dist/mobileAudit/runner.js +214 -35
  280. package/dist/mobileAudit/runner.js.map +1 -1
  281. package/dist/oracle/auth.js +1 -1
  282. package/dist/oracle/auth.js.map +1 -1
  283. package/dist/oracle/failures.js +1085 -145
  284. package/dist/oracle/failures.js.map +1 -1
  285. package/dist/oracle/health.js +148 -26
  286. package/dist/oracle/health.js.map +1 -1
  287. package/dist/oracle/locator.js +148 -19
  288. package/dist/oracle/locator.js.map +1 -1
  289. package/dist/oracle/record.js +3 -3
  290. package/dist/oracle/record.js.map +1 -1
  291. package/dist/orchestration/envelope.js +2 -2
  292. package/dist/orchestration/envelope.js.map +1 -1
  293. package/dist/orchestration/goal.js +7 -13
  294. package/dist/orchestration/goal.js.map +1 -1
  295. package/dist/orchestration/testThis/execute.js +167 -0
  296. package/dist/orchestration/testThis/execute.js.map +1 -0
  297. package/dist/orchestration/testThis/pipeline.js +413 -0
  298. package/dist/orchestration/testThis/pipeline.js.map +1 -0
  299. package/dist/orchestration/testThis/plan.js +519 -0
  300. package/dist/orchestration/testThis/plan.js.map +1 -0
  301. package/dist/orchestration/testThis/sessionIntent.js +72 -0
  302. package/dist/orchestration/testThis/sessionIntent.js.map +1 -0
  303. package/dist/orchestration/testThis/terminal.js +136 -0
  304. package/dist/orchestration/testThis/terminal.js.map +1 -0
  305. package/dist/orchestration/testThis/types.js +12 -0
  306. package/dist/orchestration/testThis/types.js.map +1 -0
  307. package/dist/plan/plan.js +9 -9
  308. package/dist/plan/plan.js.map +1 -1
  309. package/dist/prompts/index.js +24 -24
  310. package/dist/prompts/index.js.map +1 -1
  311. package/dist/report/coverage.js +0 -1
  312. package/dist/report/coverage.js.map +1 -1
  313. package/dist/report/evidence.js.map +1 -1
  314. package/dist/report/export.js +449 -72
  315. package/dist/report/export.js.map +1 -1
  316. package/dist/report/findingsDedupe.js +41 -0
  317. package/dist/report/findingsDedupe.js.map +1 -0
  318. package/dist/report/flake.js +13 -14
  319. package/dist/report/flake.js.map +1 -1
  320. package/dist/report/history.js +72 -38
  321. package/dist/report/history.js.map +1 -1
  322. package/dist/report/policy.js +18 -7
  323. package/dist/report/policy.js.map +1 -1
  324. package/dist/report/qaLevel.js +10 -16
  325. package/dist/report/qaLevel.js.map +1 -1
  326. package/dist/report/readiness.js +42 -12
  327. package/dist/report/readiness.js.map +1 -1
  328. package/dist/report/sarifSources.js +160 -0
  329. package/dist/report/sarifSources.js.map +1 -0
  330. package/dist/report/summary.js +2 -2
  331. package/dist/report/summary.js.map +1 -1
  332. package/dist/report/testCatalog.js +4 -4
  333. package/dist/report/testCatalog.js.map +1 -1
  334. package/dist/report/timing.js +23 -23
  335. package/dist/report/timing.js.map +1 -1
  336. package/dist/report/toolHealth.js +124 -0
  337. package/dist/report/toolHealth.js.map +1 -0
  338. package/dist/server.js +501 -69
  339. package/dist/server.js.map +1 -1
  340. package/dist/services/automationGenerate.js +8 -2
  341. package/dist/services/automationGenerate.js.map +1 -1
  342. package/dist/services/build.js +10 -4
  343. package/dist/services/build.js.map +1 -1
  344. package/dist/services/flowGenerate.js +75 -0
  345. package/dist/services/flowGenerate.js.map +1 -0
  346. package/dist/services/preflight.js +40 -8
  347. package/dist/services/preflight.js.map +1 -1
  348. package/dist/services/prepareAndroid.js +92 -18
  349. package/dist/services/prepareAndroid.js.map +1 -1
  350. package/dist/services/prepareIos.js +94 -17
  351. package/dist/services/prepareIos.js.map +1 -1
  352. package/dist/services/report.js +271 -80
  353. package/dist/services/report.js.map +1 -1
  354. package/dist/services/smoke.js +68 -22
  355. package/dist/services/smoke.js.map +1 -1
  356. package/dist/services/suiteGenerate.js +43 -6
  357. package/dist/services/suiteGenerate.js.map +1 -1
  358. package/dist/services/testSuiteKnowledge.js +78 -33
  359. package/dist/services/testSuiteKnowledge.js.map +1 -1
  360. package/dist/session/attach.js +284 -4
  361. package/dist/session/attach.js.map +1 -1
  362. package/dist/session/processRegistry.js +433 -0
  363. package/dist/session/processRegistry.js.map +1 -0
  364. package/dist/session/progress.js +1 -1
  365. package/dist/session/progress.js.map +1 -1
  366. package/dist/session/retention.js +201 -0
  367. package/dist/session/retention.js.map +1 -0
  368. package/dist/session/store.js +459 -59
  369. package/dist/session/store.js.map +1 -1
  370. package/dist/snapshot/overlays.js +60 -10
  371. package/dist/snapshot/overlays.js.map +1 -1
  372. package/dist/snapshot/parse.js +30 -12
  373. package/dist/snapshot/parse.js.map +1 -1
  374. package/dist/snapshot/present.js +36 -6
  375. package/dist/snapshot/present.js.map +1 -1
  376. package/dist/snapshot/settle.js +39 -9
  377. package/dist/snapshot/settle.js.map +1 -1
  378. package/dist/state/consent.js +99 -0
  379. package/dist/state/consent.js.map +1 -0
  380. package/dist/state/profile.js +82 -11
  381. package/dist/state/profile.js.map +1 -1
  382. package/dist/suite/compile.js +15 -6
  383. package/dist/suite/compile.js.map +1 -1
  384. package/dist/suite/lint.js +22 -4
  385. package/dist/suite/lint.js.map +1 -1
  386. package/dist/suite/pom.js +75 -44
  387. package/dist/suite/pom.js.map +1 -1
  388. package/dist/suite/secretGuard.js +266 -0
  389. package/dist/suite/secretGuard.js.map +1 -0
  390. package/dist/suite/testcase.js +15 -11
  391. package/dist/suite/testcase.js.map +1 -1
  392. package/dist/testSuite/exporter.js +7 -3
  393. package/dist/testSuite/exporter.js.map +1 -1
  394. package/dist/testSuite/generator.js +43 -14
  395. package/dist/testSuite/generator.js.map +1 -1
  396. package/dist/testSuite/history.js +1 -1
  397. package/dist/testSuite/history.js.map +1 -1
  398. package/dist/testSuite/issueLinks.js +3 -3
  399. package/dist/testSuite/issueLinks.js.map +1 -1
  400. package/dist/testSuite/lint.js +4 -4
  401. package/dist/testSuite/lint.js.map +1 -1
  402. package/dist/testSuite/merge.js +13 -7
  403. package/dist/testSuite/merge.js.map +1 -1
  404. package/dist/testSuite/schema.js +21 -16
  405. package/dist/testSuite/schema.js.map +1 -1
  406. package/dist/testSuite/store.js +12 -4
  407. package/dist/testSuite/store.js.map +1 -1
  408. package/dist/testSuite/traceability.js +2 -2
  409. package/dist/testSuite/traceability.js.map +1 -1
  410. package/dist/tools/act.js +868 -174
  411. package/dist/tools/act.js.map +1 -1
  412. package/dist/tools/agent.js +521 -112
  413. package/dist/tools/agent.js.map +1 -1
  414. package/dist/tools/appControl.js +82 -19
  415. package/dist/tools/appControl.js.map +1 -1
  416. package/dist/tools/appMap.js +393 -189
  417. package/dist/tools/appMap.js.map +1 -1
  418. package/dist/tools/build.js +92 -51
  419. package/dist/tools/build.js.map +1 -1
  420. package/dist/tools/bundletool.js +141 -40
  421. package/dist/tools/bundletool.js.map +1 -1
  422. package/dist/tools/clearOverlay.js +98 -18
  423. package/dist/tools/clearOverlay.js.map +1 -1
  424. package/dist/tools/device.js +108 -19
  425. package/dist/tools/device.js.map +1 -1
  426. package/dist/tools/doctor.js +98 -35
  427. package/dist/tools/doctor.js.map +1 -1
  428. package/dist/tools/explore.js +110 -42
  429. package/dist/tools/explore.js.map +1 -1
  430. package/dist/tools/featureTesting.js +152 -130
  431. package/dist/tools/featureTesting.js.map +1 -1
  432. package/dist/tools/firstRun.js +94 -66
  433. package/dist/tools/firstRun.js.map +1 -1
  434. package/dist/tools/flow.js +347 -132
  435. package/dist/tools/flow.js.map +1 -1
  436. package/dist/tools/flowRepair.js +42 -10
  437. package/dist/tools/flowRepair.js.map +1 -1
  438. package/dist/tools/generate.js +199 -0
  439. package/dist/tools/generate.js.map +1 -0
  440. package/dist/tools/getArtifact.js +37 -8
  441. package/dist/tools/getArtifact.js.map +1 -1
  442. package/dist/tools/health.js +17 -12
  443. package/dist/tools/health.js.map +1 -1
  444. package/dist/tools/ios.js +148 -194
  445. package/dist/tools/ios.js.map +1 -1
  446. package/dist/tools/issues.js +256 -171
  447. package/dist/tools/issues.js.map +1 -1
  448. package/dist/tools/jobs.js +55 -11
  449. package/dist/tools/jobs.js.map +1 -1
  450. package/dist/tools/metro.js +84 -38
  451. package/dist/tools/metro.js.map +1 -1
  452. package/dist/tools/mobileAudit.js +83 -26
  453. package/dist/tools/mobileAudit.js.map +1 -1
  454. package/dist/tools/network.js +36 -14
  455. package/dist/tools/network.js.map +1 -1
  456. package/dist/tools/note.js +31 -17
  457. package/dist/tools/note.js.map +1 -1
  458. package/dist/tools/prepareIosTarget.js +31 -14
  459. package/dist/tools/prepareIosTarget.js.map +1 -1
  460. package/dist/tools/prepareTarget.js +193 -47
  461. package/dist/tools/prepareTarget.js.map +1 -1
  462. package/dist/tools/report.js +16 -8
  463. package/dist/tools/report.js.map +1 -1
  464. package/dist/tools/resolveArtifact.js +12 -12
  465. package/dist/tools/resolveArtifact.js.map +1 -1
  466. package/dist/tools/resolveTarget.js +81 -18
  467. package/dist/tools/resolveTarget.js.map +1 -1
  468. package/dist/tools/screenRecord.js +85 -20
  469. package/dist/tools/screenRecord.js.map +1 -1
  470. package/dist/tools/screenshot.js +42 -18
  471. package/dist/tools/screenshot.js.map +1 -1
  472. package/dist/tools/smoke.js +29 -12
  473. package/dist/tools/smoke.js.map +1 -1
  474. package/dist/tools/snapshot.js +157 -107
  475. package/dist/tools/snapshot.js.map +1 -1
  476. package/dist/tools/startSession.js +117 -84
  477. package/dist/tools/startSession.js.map +1 -1
  478. package/dist/tools/suite.js +391 -310
  479. package/dist/tools/suite.js.map +1 -1
  480. package/dist/tools/testSuite.js +112 -36
  481. package/dist/tools/testSuite.js.map +1 -1
  482. package/dist/tools/testThis.js +35 -802
  483. package/dist/tools/testThis.js.map +1 -1
  484. package/dist/tools/visual.js +698 -116
  485. package/dist/tools/visual.js.map +1 -1
  486. package/dist/tools/wait.js +9 -23
  487. package/dist/tools/wait.js.map +1 -1
  488. package/dist/tools/wda.js +312 -54
  489. package/dist/tools/wda.js.map +1 -1
  490. package/dist/version.js +37 -64
  491. package/dist/version.js.map +1 -1
  492. package/dist/visual/ocr.js +65 -8
  493. package/dist/visual/ocr.js.map +1 -1
  494. package/dist/visual/provider.js +87 -15
  495. package/dist/visual/provider.js.map +1 -1
  496. package/docs/README.md +24 -10
  497. package/docs/ci-reports.md +328 -0
  498. package/docs/concepts.md +227 -0
  499. package/docs/flows.md +153 -0
  500. package/docs/mcp-server.md +210 -104
  501. package/docs/physical-devices.md +84 -0
  502. package/docs/tools.md +808 -217
  503. package/package.json +16 -6
  504. package/dist/appMap/diff.js +0 -60
  505. package/dist/appMap/diff.js.map +0 -1
  506. package/dist/automation/maestroIr.js +0 -282
  507. package/dist/automation/maestroIr.js.map +0 -1
  508. package/dist/interop/maestro.js +0 -151
  509. package/dist/interop/maestro.js.map +0 -1
  510. package/dist/tools/assertVisual.js +0 -74
  511. package/dist/tools/assertVisual.js.map +0 -1
  512. package/dist/tools/automationGenerate.js +0 -394
  513. package/dist/tools/automationGenerate.js.map +0 -1
  514. package/dist/tools/capabilities.js +0 -209
  515. package/dist/tools/capabilities.js.map +0 -1
  516. package/dist/tools/detectContext.js +0 -41
  517. package/dist/tools/detectContext.js.map +0 -1
  518. package/dist/tools/flowGenerate.js +0 -55
  519. package/dist/tools/flowGenerate.js.map +0 -1
  520. package/dist/tools/history.js +0 -56
  521. package/dist/tools/history.js.map +0 -1
  522. package/dist/tools/idling.js +0 -104
  523. package/dist/tools/idling.js.map +0 -1
  524. package/dist/tools/inputCapabilities.js +0 -30
  525. package/dist/tools/inputCapabilities.js.map +0 -1
  526. package/dist/tools/locator.js +0 -57
  527. package/dist/tools/locator.js.map +0 -1
  528. package/dist/tools/maestro.js +0 -78
  529. package/dist/tools/maestro.js.map +0 -1
  530. package/dist/tools/permissions.js +0 -149
  531. package/dist/tools/permissions.js.map +0 -1
  532. package/dist/tools/plan.js +0 -49
  533. package/dist/tools/plan.js.map +0 -1
  534. package/dist/tools/screenInfo.js +0 -70
  535. package/dist/tools/screenInfo.js.map +0 -1
  536. package/dist/tools/seed.js +0 -101
  537. package/dist/tools/seed.js.map +0 -1
  538. package/dist/tools/state.js +0 -170
  539. package/dist/tools/state.js.map +0 -1
  540. package/dist/tools/visualText.js +0 -61
  541. package/dist/tools/visualText.js.map +0 -1
package/dist/server.js CHANGED
@@ -1,12 +1,12 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { SessionStore } from './session/store.js';
6
+ import { SessionStore, decodeUriSegment, encodeUriSegment, isWithinRoot } from './session/store.js';
7
+ import { burnConsent, peekConsent, requestConsentDecision, runWithConsentScope, setElicitationProvider, } from './consent/consent.js';
5
8
  import { registerDoctor } from './tools/doctor.js';
6
- import { registerCapabilities } from './tools/capabilities.js';
7
9
  import { registerStartSession } from './tools/startSession.js';
8
- import { registerDetectContext } from './tools/detectContext.js';
9
- import { registerPlan } from './tools/plan.js';
10
10
  import { registerPrepareTarget } from './tools/prepareTarget.js';
11
11
  import { registerScreenshot } from './tools/screenshot.js';
12
12
  import { registerSnapshot } from './tools/snapshot.js';
@@ -15,24 +15,17 @@ import { registerCheckHealth } from './tools/health.js';
15
15
  import { registerNetwork, restoreAllNetwork } from './tools/network.js';
16
16
  import { registerScreenRecord, stopAllRecordings } from './tools/screenRecord.js';
17
17
  import { registerDevice } from './tools/device.js';
18
- import { registerPermissions } from './tools/permissions.js';
19
18
  import { registerMetro, stopAllMetro } from './tools/metro.js';
20
19
  import { registerAppControl } from './tools/appControl.js';
21
- import { registerScreenInfo } from './tools/screenInfo.js';
22
- import { registerVisual } from './tools/visual.js';
23
- import { registerVisualText } from './tools/visualText.js';
24
- import { registerSeed } from './tools/seed.js';
25
- import { registerState } from './tools/state.js';
26
- import { registerHistory } from './tools/history.js';
27
20
  import { registerIos } from './tools/ios.js';
28
21
  import { registerWda } from './tools/wda.js';
29
22
  import { registerClearOverlay } from './tools/clearOverlay.js';
30
23
  import { registerJobs } from './tools/jobs.js';
31
24
  import { registerGetArtifact } from './tools/getArtifact.js';
32
25
  import { registerNote } from './tools/note.js';
33
- import { registerAssertVisual } from './tools/assertVisual.js';
26
+ import { registerVisual } from './tools/visual.js';
34
27
  import { registerFlow } from './tools/flow.js';
35
- import { registerFlowGenerate } from './tools/flowGenerate.js';
28
+ import { registerGenerate } from './tools/generate.js';
36
29
  import { registerSmoke } from './tools/smoke.js';
37
30
  import { registerReport } from './tools/report.js';
38
31
  import { registerTestThis } from './tools/testThis.js';
@@ -40,17 +33,13 @@ import { registerPrepareIosTarget } from './tools/prepareIosTarget.js';
40
33
  import { registerSuite } from './tools/suite.js';
41
34
  import { registerExplore } from './tools/explore.js';
42
35
  import { registerFirstRun } from './tools/firstRun.js';
43
- import { registerAutomationGenerate } from './tools/automationGenerate.js';
44
- import { registerAgentTools } from './tools/agent.js';
45
- import { registerAppMap, resolveAppMapRoot, readAppMapResource } from './tools/appMap.js';
36
+ import { registerAgentTools, SERVER_INSTRUCTIONS } from './tools/agent.js';
37
+ import { registerAppMap, resolveAppMapRoot, readAppMapResource, listAppMapResources } from './tools/appMap.js';
38
+ import { appMapPath } from './appMap/store.js';
46
39
  import { registerIssues } from './tools/issues.js';
47
40
  import { registerTestSuite } from './tools/testSuite.js';
48
41
  import { registerFlowRepair } from './tools/flowRepair.js';
49
- import { registerLocator } from './tools/locator.js';
50
42
  import { registerWait } from './tools/wait.js';
51
- import { registerIdling } from './tools/idling.js';
52
- import { registerInputCapabilities } from './tools/inputCapabilities.js';
53
- import { registerMaestro } from './tools/maestro.js';
54
43
  import { registerMobileAudit } from './tools/mobileAudit.js';
55
44
  import { registerFeatureTesting } from './tools/featureTesting.js';
56
45
  import { registerResolveArtifact } from './tools/resolveArtifact.js';
@@ -59,57 +48,445 @@ import { registerBuild } from './tools/build.js';
59
48
  import { registerBundletool } from './tools/bundletool.js';
60
49
  import { registerPrompts } from './prompts/index.js';
61
50
  import { log } from './lib/logger.js';
62
- import { runWithResponseMode } from './lib/result.js';
51
+ import { qaError, runWithResponseMode } from './lib/result.js';
52
+ import { recordToolErrorFromResult } from './report/toolHealth.js';
63
53
  import { computeSchemaHash, describeZodField, setSchemaHash } from './lib/schemaHash.js';
64
- import { SWIPIUM_VERSION, TOOL_COUNT, TOOL_NAME_SET } from './version.js';
54
+ import { REMOVED_TOOLS, STALE_CLIENT_HINT, SWIPIUM_VERSION, TOOL_COUNT, TOOL_NAMES, TOOL_NAME_SET } from './version.js';
55
+ import { toolAnnotations } from './lib/toolAnnotations.js';
56
+ import { CAPABILITY_GROUPS } from './core/capabilityGroups.js';
57
+ import { ensureAndroidToolsOnPath } from './lib/android.js';
58
+ import { annotateRootSource, withRootResolutionRecording } from './context/projectRoot.js';
59
+ import { reapOrphanedProcesses } from './session/processRegistry.js';
60
+ import { runWithSignal } from './lib/abortScope.js';
61
+ /** How long a consent prompt may stay open before it counts as unanswered (> refusal).
62
+ * Explicit, because the SDK's default request timeout (60 s) is far too short for a human. */
63
+ export const ELICITATION_TIMEOUT_MS = 10 * 60_000;
64
+ /**
65
+ * Elicitation provider (consent.ts header, THREAT_MODEL "compromised client self-approval"):
66
+ * asks the connected client's HUMAN to decide a consent via the MCP elicitation capability
67
+ * (`elicitation/create` with a flat one-boolean-field schema, per spec). Capabilities are only
68
+ * known after `initialize` (and tools are registered before connect), so the capability check
69
+ * happens lazily on every call, never at construction. Only a client that does NOT advertise
70
+ * form elicitation yields 'unavailable' (portable re-call fallback). Once the prompt is sent,
71
+ * `cancel`, a timeout, an aborted tool call or a transport error all reject/return 'cancelled',
72
+ * which is a refusal (MCP spec: cancel = dismissed without an explicit choice, never consent).
73
+ */
74
+ /** Consent-prompt field hygiene: the explain / command strings interpolate repo-derived values
75
+ * (flow names, queries, URLs, configured argv). Strip control characters (incl. newlines, which
76
+ * could fake a second "Will run:" line), bidi overrides and zero-width characters, collapse
77
+ * whitespace, and cap the length. The caller then QUOTES the result. Exported for tests. */
78
+ export function sanitizePromptField(value, max = 300) {
79
+ const flat = String(value ?? '')
80
+ .replace(/[\p{Cc}\u2028\u2029\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF]/gu, ' ')
81
+ .replace(/\s+/g, ' ')
82
+ .trim();
83
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
84
+ }
85
+ /** The out-of-band consent prompt text. Every interpolated field is sanitised and quoted
86
+ * (JSON-style), the command is shown one quoted line per step, and the whole message is capped. */
87
+ export function buildConsentPromptMessage(req) {
88
+ const q = (v, max) => JSON.stringify(sanitizePromptField(v, max));
89
+ const lines = [`Swipium requests consent (risk: ${q(req.risk, 20)}) for action ${q(req.action, 60)}.`, `Details: ${q(req.explain, 600)}`];
90
+ if (req.exactCommand) {
91
+ const steps = String(req.exactCommand)
92
+ .split(/\r?\n/)
93
+ .filter((l) => l.trim())
94
+ .slice(0, 4);
95
+ lines.push(steps.length > 1 ? 'Will run:' : `Will run: ${q(steps[0], 500)}`);
96
+ if (steps.length > 1)
97
+ for (const step of steps)
98
+ lines.push(` ${q(step, 500)}`);
99
+ }
100
+ const msg = lines.join('\n');
101
+ return msg.length > 2000 ? `${msg.slice(0, 1999)}…` : msg;
102
+ }
103
+ function makeElicitationProvider(server) {
104
+ return async (req, ctx) => {
105
+ // The SDK normalises a bare `elicitation: {}` capability to `{ form: {} }`.
106
+ if (!server.server.getClientCapabilities()?.elicitation?.form)
107
+ return 'unavailable';
108
+ const answer = await server.server.elicitInput({
109
+ message: buildConsentPromptMessage(req),
110
+ requestedSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ approve: { type: 'boolean', title: 'Approve', description: 'Allow Swipium to perform this action.' },
114
+ },
115
+ required: ['approve'],
116
+ },
117
+ }, {
118
+ timeout: ELICITATION_TIMEOUT_MS,
119
+ ...(ctx?.signal ? { signal: ctx.signal } : {}),
120
+ ...(ctx?.relatedRequestId !== undefined ? { relatedRequestId: ctx.relatedRequestId } : {}),
121
+ });
122
+ if (answer.action === 'accept')
123
+ return answer.content?.approve === true ? 'approved' : 'declined';
124
+ if (answer.action === 'decline')
125
+ return 'declined';
126
+ return 'cancelled'; // 'cancel': dismissed without answering > refusal, never a self-approval fallback
127
+ };
128
+ }
129
+ /** Ledger a consent that was decided WITHOUT the tool running (declined / cancelled / refused by
130
+ * policy), so the audit trail shows the refusal next to the tool's own `requested` row. */
131
+ function recordConsentRefusal(sessions, tool, sessionId, consentId, req, mechanism, detail) {
132
+ if (!req || typeof sessionId !== 'string')
133
+ return;
134
+ const session = sessions.get(sessionId);
135
+ if (!session)
136
+ return;
137
+ try {
138
+ sessions.recordMutation(session, {
139
+ tool,
140
+ action: req.action,
141
+ risk: req.risk,
142
+ target: req.affects ?? {},
143
+ consent: { required: true, consentId, approved: false, approvalMechanism: mechanism },
144
+ status: 'refused',
145
+ detail,
146
+ });
147
+ }
148
+ catch (e) {
149
+ log('warn', 'failed to ledger a consent refusal', { tool, consentId, err: String(e) });
150
+ }
151
+ }
152
+ function pendingConsentId(result) {
153
+ const sc = result?.structuredContent;
154
+ return sc?.requiresConsent === true && typeof sc.consentId === 'string' ? sc.consentId : undefined;
155
+ }
156
+ /**
157
+ * Consent routing (consent.ts header): when a tool handler returns a requiresConsent envelope,
158
+ * route the pending consent through a REAL out-of-band user prompt before the envelope ever
159
+ * reaches the model. Outcomes:
160
+ * - elicitation approved > re-invoke the SAME handler exactly once with the consent attached
161
+ * (the approval was recorded, so consumeConsent tags mechanism 'elicitation');
162
+ * - elicitation declined > CONSENT_DECLINED; cancelled/timed out/transport error >
163
+ * CONSENT_CANCELLED (retry-safe: a re-call issues a fresh prompt). Either way the challenge
164
+ * is burned (no approve:true self-approval afterwards) and a `refused` ledger row is written;
165
+ * - refused (SWIPIUM_REQUIRE_ELICITATION=1 and no elicitation support) > CONSENT_REFUSED;
166
+ * - client-assertion (client does not advertise elicitation) > the portable envelope unchanged.
167
+ * The re-invocation's result is never routed again, so this cannot loop; if it asks for a NEW
168
+ * consent (the target changed under the prompt) that challenge is burned and CONSENT_CANCELLED
169
+ * returned, so the model never receives a self-approvable envelope on an elicitation client.
170
+ */
171
+ async function routePendingConsent(result, args, reinvoke, ledger) {
172
+ const consentId = pendingConsentId(result);
173
+ if (!consentId)
174
+ return result;
175
+ const first = (args[0] ?? {});
176
+ const extra = args[1];
177
+ const req = peekConsent(consentId);
178
+ const decision = await requestConsentDecision(consentId, { signal: extra?.signal, relatedRequestId: extra?.requestId });
179
+ if (decision.mechanism === 'refused') {
180
+ recordConsentRefusal(ledger.sessions, ledger.tool, first.sessionId, consentId, req, 'policy', decision.reason);
181
+ return qaError({
182
+ what: decision.reason,
183
+ changedState: false,
184
+ retrySafe: false,
185
+ failureCode: 'CONSENT_REFUSED',
186
+ nextSteps: ['Connect with an MCP client that supports elicitation, or unset SWIPIUM_REQUIRE_ELICITATION.'],
187
+ });
188
+ }
189
+ if (decision.mechanism === 'elicitation') {
190
+ if (!decision.approved) {
191
+ recordConsentRefusal(ledger.sessions, ledger.tool, first.sessionId, consentId, req, 'elicitation', decision.reason);
192
+ if (decision.outcome === 'cancelled') {
193
+ return qaError({
194
+ what: decision.reason,
195
+ changedState: false,
196
+ retrySafe: true,
197
+ failureCode: 'CONSENT_CANCELLED',
198
+ nextSteps: ['Nothing ran. Re-call the tool (without consentId) to show the user a fresh consent prompt, or ask them first.'],
199
+ });
200
+ }
201
+ return qaError({
202
+ what: 'User declined via elicitation prompt',
203
+ changedState: false,
204
+ retrySafe: false,
205
+ failureCode: 'CONSENT_DECLINED',
206
+ nextSteps: ['Do not retry this action; ask the user before attempting it again.'],
207
+ });
208
+ }
209
+ const out = await reinvoke([{ ...first, consentId, approve: true }, ...args.slice(1)]);
210
+ const again = pendingConsentId(out);
211
+ if (again) {
212
+ burnConsent(again);
213
+ burnConsent(consentId);
214
+ return qaError({
215
+ what: 'The action changed while the user was deciding, so the approval no longer matches it. Nothing ran.',
216
+ changedState: false,
217
+ retrySafe: true,
218
+ failureCode: 'CONSENT_CANCELLED',
219
+ nextSteps: ['Re-call the tool (without consentId) to prompt the user for the current action.'],
220
+ });
221
+ }
222
+ return out;
223
+ }
224
+ return result; // 'client-assertion': portable path (the model relays the consent envelope)
225
+ }
65
226
  /**
66
- * Wrap every tool handler so it runs inside the calling session's response mode
67
- * (PHASE3-PLAN §2.1). Resolved once, centrally — individual tools stay mode-agnostic.
227
+ * Wrap every tool handler so it runs inside the calling session's response mode.
228
+ * Resolved once, centrally. Individual tools stay mode-agnostic.
68
229
  * `compact` shrinks the text channel; `structuredContent` is always full.
230
+ * The same wrapper also routes requiresConsent envelopes through out-of-band elicitation
231
+ * (routePendingConsent), so every consent-gated tool inherits it with zero per-tool changes.
69
232
  */
70
- function installResponseModeWrapper(server, sessions, surface) {
233
+ function installResponseModeWrapper(server, sessions, surface, attempted, paramNames) {
71
234
  const orig = server.registerTool.bind(server);
72
235
  const valid = (m) => m === 'compact' || m === 'normal' || m === 'verbose';
73
236
  server.registerTool = (name, config, handler) => {
237
+ attempted.add(name);
74
238
  if (!TOOL_NAME_SET.has(name))
75
- return undefined;
239
+ return undefined; // assertToolSurface() makes this drop loud at startup
76
240
  // Capture the tool surface (name + description + per-field type descriptors) for the schema hash
77
241
  // (3.3 A/§5). Encoding each field's zod shape catches nested enum/type/optionality changes.
78
242
  const cfg = config;
79
243
  const inputKeys = Object.entries(cfg?.inputSchema ?? {}).map(([k, v]) => `${k}:${describeZodField(v)}`);
80
244
  surface.push({ name, description: cfg?.description ?? '', inputKeys });
81
- return orig(name, config, (...a) => {
245
+ paramNames.set(name, Object.keys(cfg?.inputSchema ?? {}));
246
+ // MCP annotations for every tool, from one reviewed table (src/lib/toolAnnotations.ts).
247
+ const annotated = { ...config, annotations: toolAnnotations(name) };
248
+ return orig(name, annotated, async (...a) => {
82
249
  const first = a[0];
83
250
  // Prefer the existing session's mode; fall back to a directly-passed responseMode so the
84
251
  // session-CREATING call (qa_start_session, no sessionId yet) also honors compact.
85
252
  const fromSession = first?.sessionId ? sessions.get(first.sessionId)?.responseMode : undefined;
86
- const mode = fromSession ?? (valid(first?.responseMode) ? first.responseMode : 'normal');
87
- return runWithResponseMode(mode, () => handler(...a));
253
+ const mode = (fromSession ?? (valid(first?.responseMode) ? first.responseMode : 'normal'));
254
+ // Every call records the project root it resolves (if any) so the result carries
255
+ // `rootSource` (+ a note when the root was only guessed from the server cwd).
256
+ // Consents minted/consumed during this call are bound to its sessionId (consent.ts).
257
+ // Cancellation: the call's MCP signal (extra.signal, the handler's last argument) is scoped
258
+ // to THIS call (abortScope): driver adb/WDA calls made by the tool abort with it, and a
259
+ // background job's signal (bound by the job itself) never leaks into or out of it.
260
+ const extra = a[a.length - 1];
261
+ const callSignal = extra?.signal instanceof AbortSignal ? extra.signal : undefined;
262
+ const run = async (callArgs) => {
263
+ const { value, resolved } = await runWithSignal(callSignal, () => withRootResolutionRecording(async () => runWithConsentScope(first?.sessionId, () => runWithResponseMode(mode, () => handler(...callArgs)))));
264
+ return annotateRootSource(value, resolved);
265
+ };
266
+ const result = await run(a);
267
+ const out = await routePendingConsent(result, a, run, { sessions, tool: name });
268
+ recordToolErrorFromResult(sessions, name, a[0], out, callSignal); // qa_report tool status (report/toolHealth.ts)
269
+ return out;
88
270
  });
89
271
  };
90
272
  }
273
+ /** Legacy (≤ 1.5) call shapes that a client spawned before the upgrade may still send, mapped to
274
+ * the 2.0 replacement. Returned as a typed STALE_CLIENT error instead of a raw "Tool not found" /
275
+ * zod validation message, WITHOUT polluting the current schemas/descriptions. Exported for tests. */
276
+ export function staleClientReplacement(name, args) {
277
+ if (!TOOL_NAME_SET.has(name))
278
+ return REMOVED_TOOLS[name];
279
+ const action = args?.action;
280
+ if (name === 'qa_ios' && typeof action === 'string') {
281
+ if (action === 'screenshot')
282
+ return 'qa_screenshot { sessionId }';
283
+ if (action.startsWith('wda_'))
284
+ return `qa_wda { sessionId, action: "${action.slice(4) || 'status'}" }`;
285
+ }
286
+ if (name === 'qa_wait' && args?.for === 'job_done')
287
+ return 'qa_job_status { sessionId, jobId, waitMs }';
288
+ return undefined;
289
+ }
290
+ function staleClientError(name, replacement) {
291
+ return qaError({
292
+ what: `${name} (this call shape) was removed in Swipium v${SWIPIUM_VERSION}; use ${replacement}`,
293
+ changedState: false,
294
+ retrySafe: true,
295
+ failureCode: 'STALE_CLIENT',
296
+ nextSteps: [`Call ${replacement} instead.`, 'Restart the MCP client so it reloads the current tool list.'],
297
+ clientHint: STALE_CLIENT_HINT,
298
+ }, { removedCall: name, replacement });
299
+ }
300
+ /** The SDK stores handlers per method; wrapping the stored (already SDK-wrapped) function keeps
301
+ * the SDK's own request/result validation intact. */
302
+ function wrapRequestHandler(server, method, wrap) {
303
+ const map = server.server._requestHandlers;
304
+ const orig = map?.get(method);
305
+ if (!map || !orig) {
306
+ log('warn', 'could not wrap MCP request handler (SDK internals changed?)', { method });
307
+ return;
308
+ }
309
+ map.set(method, wrap(orig));
310
+ }
311
+ /** Drop the per-schema `$schema` dialect key (~2.8 KB of repetition across the tool list). */
312
+ export function stripSchemaDialect(result) {
313
+ const tools = result?.tools;
314
+ if (!Array.isArray(tools))
315
+ return result;
316
+ for (const t of tools) {
317
+ for (const k of ['inputSchema', 'outputSchema']) {
318
+ const sch = t[k];
319
+ if (sch && typeof sch === 'object' && '$schema' in sch)
320
+ delete sch.$schema;
321
+ }
322
+ }
323
+ return result;
324
+ }
325
+ /** Top-level argument keys a tool's input schema does not declare. The advertised JSON schema
326
+ * says additionalProperties:false, but the SDK's zod object silently STRIPS unknown keys, so a
327
+ * call like qa_app_control { action:"force_stop", appId:"other.app" } used to run against the
328
+ * session's app while the caller believed it targeted another. Deprecated aliases that are still
329
+ * declared in the schema are accepted (they are schema properties). Exported for tests. */
330
+ export function unknownArgumentKeys(args, accepted) {
331
+ if (!args || typeof args !== 'object' || Array.isArray(args))
332
+ return [];
333
+ const known = new Set(accepted);
334
+ return Object.keys(args).filter((k) => !known.has(k));
335
+ }
336
+ export function unknownArgumentsError(name, unknown, accepted) {
337
+ const list = (keys) => keys.map((k) => JSON.stringify(k)).join(', ');
338
+ return qaError({
339
+ what: `${name} does not accept the argument${unknown.length === 1 ? '' : 's'} ${list(unknown)}. Nothing was run.`,
340
+ changedState: false,
341
+ retrySafe: true,
342
+ failureCode: 'INVALID_ARGUMENT',
343
+ nextSteps: [
344
+ `Remove ${list(unknown)} and re-call. Accepted parameters: ${accepted.length ? list(accepted) : '(none)'}.`,
345
+ 'If the tool list looks outdated, restart the MCP client so it reloads the current schemas.',
346
+ ],
347
+ }, { unknownArguments: unknown, acceptedParameters: [...accepted] });
348
+ }
349
+ /** tools/call: unknown removed-tool names and legacy call shapes > STALE_CLIENT (with the
350
+ * replacement + stale-client hint); undeclared top-level arguments > INVALID_ARGUMENT (before the
351
+ * handler runs); tools/list: strip `$schema`. */
352
+ function installProtocolShims(server, paramNames) {
353
+ wrapRequestHandler(server, 'tools/call', (orig) => async (request, extra) => {
354
+ const name = String(request?.params?.name ?? '');
355
+ const args = request?.params?.arguments;
356
+ const replacement = staleClientReplacement(name, args);
357
+ if (replacement && !TOOL_NAME_SET.has(name))
358
+ return staleClientError(name, replacement);
359
+ const accepted = paramNames.get(name);
360
+ if (accepted && !replacement) {
361
+ const unknown = unknownArgumentKeys(args, accepted);
362
+ if (unknown.length)
363
+ return unknownArgumentsError(name, unknown, accepted);
364
+ }
365
+ const result = (await orig(request, extra));
366
+ // Legacy enum values fail the CURRENT schema's validation > rewrite that raw error only.
367
+ if (replacement && result?.isError && !result.structuredContent)
368
+ return staleClientError(`${name} ${JSON.stringify(args?.action ?? args?.for)}`, replacement);
369
+ return result;
370
+ });
371
+ wrapRequestHandler(server, 'tools/list', (orig) => async (request, extra) => stripSchemaDialect(await orig(request, extra)));
372
+ }
373
+ /** Startup assertion: every registerTool() call must be
374
+ * allowlisted in TOOL_NAMES, and every TOOL_NAMES entry must actually get registered.
375
+ * Without this, a tool missing from the allowlist is silently discarded by the wrapper
376
+ * above, and a stale TOOL_NAMES entry silently over-reports the surface. Fail LOUDLY. */
377
+ function assertToolSurface(attempted) {
378
+ const missing = TOOL_NAMES.filter((n) => !attempted.has(n));
379
+ const extra = [...attempted].filter((n) => !TOOL_NAME_SET.has(n));
380
+ // Every tool sits in exactly one capability group (qa_status orientation + docs/tools.md layout).
381
+ const grouped = CAPABILITY_GROUPS.flatMap((g) => g.tools);
382
+ const ungrouped = TOOL_NAMES.filter((n) => grouped.filter((t) => t === n).length !== 1);
383
+ if (ungrouped.length)
384
+ throw new Error(`CAPABILITY_GROUPS (src/core/capabilityGroups.ts) must list each tool exactly once: ${ungrouped.join(', ')}`);
385
+ if (missing.length === 0 && extra.length === 0 && attempted.size === TOOL_NAMES.length)
386
+ return;
387
+ throw new Error(`Tool surface mismatch: ${attempted.size} tools registered vs ${TOOL_NAMES.length} in TOOL_NAMES (src/version.ts).` +
388
+ (missing.length ? `\n Missing (allowlisted but never registered): ${missing.join(', ')}` : '') +
389
+ (extra.length ? `\n Extra (registered but not in TOOL_NAMES, so they would be silently dropped): ${extra.join(', ')}` : '') +
390
+ '\nFix: add/remove the tool in TOOL_NAMES and CAPABILITY_GROUPS, or register it in createServer().');
391
+ }
392
+ /** resources/list size cap. The SDK aggregates every template's list callback into one
393
+ * un-paginated resources/list response, so each listing stays bounded. */
394
+ const RESOURCE_LIST_CAP = 100;
395
+ /** Cap a resources/list result and DISCLOSE the truncation on the final entry's description.
396
+ * Silent truncation is forbidden. Clients read anything omitted here directly by URI
397
+ * (qa_get_artifact / qa_app_map_read always cover the full set). */
398
+ function capResourceListing(all, cap = RESOURCE_LIST_CAP) {
399
+ if (all.length <= cap)
400
+ return all;
401
+ const shown = all.slice(0, cap);
402
+ const last = shown[shown.length - 1];
403
+ last.description = `${last.description ? `${last.description} ` : ''}[listing capped: showing ${cap} of ${all.length}; the rest remain readable by URI]`;
404
+ return shown;
405
+ }
406
+ /** Project roots the CURRENT client works in: its MCP roots (when it advertises the capability)
407
+ * plus the roots of sessions created or used in this server process. resources/list is scoped to
408
+ * these so one client never browses another project's artifacts from the machine-wide registry. */
409
+ async function currentProjectRoots(server, sessions) {
410
+ const roots = new Set(sessions.activeRoots());
411
+ try {
412
+ if (server.server.getClientCapabilities()?.roots) {
413
+ const res = await server.server.listRoots(undefined, { timeout: 5_000 });
414
+ for (const r of res.roots ?? [])
415
+ if (typeof r.uri === 'string' && r.uri.startsWith('file://'))
416
+ roots.add(fileURLToPath(r.uri));
417
+ }
418
+ }
419
+ catch {
420
+ // roots/list failed: fall back to the process-local roots only
421
+ }
422
+ return [...roots];
423
+ }
424
+ /** Sessions whose artifacts/maps may be LISTED: inside a current project root. */
425
+ function sessionsInRoots(sessions, roots) {
426
+ return sessions.list().filter((s) => roots.some((r) => isWithinRoot(s.root, r)));
427
+ }
428
+ /** Re-encode a stored artifact URI so every segment matches the qa-artifact template (older
429
+ * records stored raw names; a raw `/` inside the name spans several segments). */
430
+ function listedArtifactUri(uri) {
431
+ const prefix = 'swipium://session/';
432
+ if (!uri.startsWith(prefix))
433
+ return uri;
434
+ const [id = '', kind = '', ...rest] = uri.slice(prefix.length).split('/');
435
+ const seg = (x) => encodeUriSegment(decodeUriSegment(x));
436
+ return `${prefix}${seg(id)}/${seg(kind)}/${seg(rest.map(decodeUriSegment).join('/'))}`;
437
+ }
438
+ /** App-map listings load every map JSON, so cache per scope, keyed on each root's map mtime.
439
+ * The short TTL also covers roots known only to src/tools/appMap.ts's in-process registry. */
440
+ const APP_MAP_LIST_TTL_MS = 10_000;
441
+ function makeAppMapLister(sessions) {
442
+ const cache = new Map();
443
+ return (roots, which) => {
444
+ const scoped = sessionsInRoots(sessions, roots);
445
+ const scopedRoots = [...new Set([...scoped.map((s) => s.root), ...roots])].sort();
446
+ const key = scopedRoots
447
+ .map((r) => {
448
+ try {
449
+ return `${r}@${statSync(appMapPath(r)).mtimeMs}`;
450
+ }
451
+ catch {
452
+ return `${r}@-`;
453
+ }
454
+ })
455
+ .join('|');
456
+ const hit = cache.get(which);
457
+ if (hit && hit.key === key && Date.now() - hit.at < APP_MAP_LIST_TTL_MS)
458
+ return hit.value;
459
+ // listAppMapResources enumerates roots via store.list(); hand it only the in-scope roots.
460
+ const scopedStore = { list: () => scopedRoots.map((root) => ({ root })) };
461
+ const value = listAppMapResources(scopedStore, which);
462
+ cache.set(which, { key, at: Date.now(), value });
463
+ return value;
464
+ };
465
+ }
91
466
  /** Construct the server and register all tools + the artifact resource. Exported for tests. */
92
467
  export function createServer() {
93
- const server = new McpServer({ name: 'swipium', version: SWIPIUM_VERSION });
468
+ const server = new McpServer({ name: 'swipium', version: SWIPIUM_VERSION }, { instructions: SERVER_INSTRUCTIONS });
469
+ // Out-of-band consent (consent.ts header): when the connected client supports MCP
470
+ // elicitation, pending consents are decided by a real user prompt instead of a
471
+ // model-mediated re-call. The provider checks client capabilities lazily per call,
472
+ // since they are only known after `initialize` (long after tool registration).
473
+ setElicitationProvider(makeElicitationProvider(server));
94
474
  const sessions = new SessionStore();
95
475
  const surface = [];
96
- installResponseModeWrapper(server, sessions, surface);
476
+ const attemptedToolNames = new Set();
477
+ const paramNames = new Map();
478
+ installResponseModeWrapper(server, sessions, surface, attemptedToolNames, paramNames);
97
479
  // Setup / context
98
480
  registerDoctor(server);
99
- registerCapabilities(server, sessions);
100
481
  registerStartSession(server, sessions);
101
- registerDetectContext(server, sessions);
102
- registerPlan(server, sessions);
103
482
  registerPrepareTarget(server, sessions);
104
483
  registerIos(server, sessions);
105
484
  registerWda(server, sessions);
106
485
  // Device / app environment parity (Phase 5)
107
486
  registerDevice(server, sessions);
108
- registerPermissions(server, sessions);
109
487
  registerNetwork(server, sessions);
110
488
  registerMetro(server, sessions);
111
489
  registerAppControl(server, sessions);
112
- registerScreenInfo(server, sessions);
113
490
  registerScreenRecord(server, sessions);
114
491
  // Observation / action / oracle
115
492
  registerScreenshot(server, sessions);
@@ -117,37 +494,27 @@ export function createServer() {
117
494
  registerAct(server, sessions);
118
495
  registerClearOverlay(server, sessions);
119
496
  registerCheckHealth(server, sessions);
120
- // Visual intelligence (Phase 8, local-first)
121
- registerVisual(server, sessions);
122
- registerVisualText(server, sessions);
123
- // Seeded state (Phase 9)
124
- registerSeed(server, sessions);
125
- registerState(server, sessions);
126
497
  // Jobs / artifacts / reporting (M6)
127
498
  registerJobs(server, sessions);
128
499
  registerGetArtifact(server, sessions);
129
500
  registerNote(server, sessions);
130
- registerAssertVisual(server, sessions);
501
+ registerVisual(server, sessions);
131
502
  registerFlow(server, sessions);
132
- registerFlowGenerate(server, sessions);
503
+ registerGenerate(server, sessions);
133
504
  registerSmoke(server, sessions);
134
505
  registerReport(server, sessions);
135
- // Report 2.0: history + comparison (Phase 10)
136
- registerHistory(server, sessions);
137
506
  registerTestThis(server, sessions);
138
507
  registerPrepareIosTarget(server, sessions);
139
508
  registerSuite(server, sessions);
140
509
  registerExplore(server, sessions);
141
510
  registerFirstRun(server, sessions);
142
- registerAutomationGenerate(server, sessions);
143
511
  registerAgentTools(server, sessions);
144
512
  registerAppMap(server, sessions);
145
- // Durable QA memory + repeatable assets (v3)
513
+ // Durable QA memory + repeatable assets
146
514
  registerIssues(server, sessions);
147
515
  registerMobileAudit(server, sessions);
148
516
  registerTestSuite(server, sessions);
149
517
  registerFlowRepair(server, sessions);
150
- registerMaestro(server, sessions);
151
518
  // Feature-focused testing + local build/artifact resolution (v4)
152
519
  registerFeatureTesting(server, sessions);
153
520
  registerResolveArtifact(server, sessions);
@@ -155,16 +522,37 @@ export function createServer() {
155
522
  registerBuild(server, sessions);
156
523
  registerBundletool(server, sessions);
157
524
  // Agent-efficiency helpers (v3)
158
- registerLocator(server, sessions);
159
525
  registerWait(server, sessions);
160
- registerIdling(server, sessions);
161
- registerInputCapabilities(server, sessions);
162
- // Tool surface is now fully registered. Freeze its content fingerprint.
526
+ // Tool surface is now fully registered. Fail loudly on any allowlist mismatch, then
527
+ // freeze the surface's content fingerprint.
528
+ assertToolSurface(attemptedToolNames);
163
529
  setSchemaHash(computeSchemaHash(surface));
164
- // Reusable workflow templates (MCP prompts capability) — thin orchestration of the tools above.
530
+ installProtocolShims(server, paramNames);
531
+ // Reusable workflow templates (MCP prompts capability): thin orchestration of the tools above.
165
532
  registerPrompts(server);
166
533
  // Artifacts as MCP resources (clients that support them); qa_get_artifact is the fallback.
167
- server.registerResource('qa-artifact', new ResourceTemplate('swipium://session/{sessionId}/{kind}/{name}', { list: undefined }), { title: 'QA artifact', description: 'Session artifacts: screenshots, dumps, reports, logs.' }, async (uri) => {
534
+ // The list callback lets resource-aware clients BROWSE artifacts instead of mining
535
+ // URIs out of tool text: newest RESOURCE_LIST_CAP across the sessions of the CURRENT project
536
+ // root(s) (currentProjectRoots), straight from the in-memory ledger (no device calls).
537
+ // Sensitive-mode sessions are never listed (their artifacts stay readable by exact URI only).
538
+ // ArtifactRecord carries no byte size, so the description is kind + label; sizes come from
539
+ // qa_get_artifact { mode: "metadata" }.
540
+ server.registerResource('qa-artifact', new ResourceTemplate('swipium://session/{sessionId}/{kind}/{name}', {
541
+ list: async () => {
542
+ const roots = await currentProjectRoots(server, sessions);
543
+ const all = sessionsInRoots(sessions, roots)
544
+ .filter((s) => !s.sensitive)
545
+ .flatMap((s) => s.artifacts)
546
+ .sort((a, b) => b.createdAt - a.createdAt)
547
+ .map((rec) => ({
548
+ uri: listedArtifactUri(rec.uri),
549
+ name: basename(rec.path),
550
+ mimeType: rec.mime,
551
+ description: rec.label ? `${rec.kind}: ${rec.label}` : rec.kind,
552
+ }));
553
+ return { resources: capResourceListing(all) };
554
+ },
555
+ }), { title: 'QA artifact', description: 'Session artifacts: screenshots, dumps, reports, logs.' }, async (uri) => {
168
556
  const found = sessions.findArtifact(uri.href);
169
557
  if (!found)
170
558
  throw new Error(`Unknown artifact ${uri.href}`);
@@ -174,23 +562,35 @@ export function createServer() {
174
562
  }
175
563
  return { contents: [{ uri: uri.href, mimeType: rec.mime, text: readFileSync(rec.path, 'utf8') }] };
176
564
  });
177
- // App Knowledge Map as MCP resources (SWIPIUM-REQ-01) — full map + per-feature / per-screen /
565
+ // App Knowledge Map as MCP resources: full map + per-feature / per-screen /
178
566
  // test-suite sections, so large map data is read by URI instead of flooding a tool's text result.
179
- server.registerResource('qa-app-map', new ResourceTemplate('swipium://project/{projectId}/app-map/{kind}/{id}', { list: undefined }), { title: 'App knowledge map', description: 'Durable app map: full map, or a feature/screen/test-suite section.' }, async (uri, vars) => {
180
- const root = resolveAppMapRoot(String(vars.projectId), sessions);
567
+ // Both app-map templates list what is ACTUALLY readable right now: section/full URIs
568
+ // for every project root known to this run that has a map on disk; no map > empty list, never
569
+ // an error. Enumeration is a local JSON load (listAppMapResources), scoped to the current project
570
+ // root(s) and cached on map mtime (makeAppMapLister), no device calls. Listed section ids are
571
+ // percent-encoded by listAppMapResources; the read handlers decode template variables.
572
+ const listAppMaps = makeAppMapLister(sessions);
573
+ server.registerResource('qa-app-map', new ResourceTemplate('swipium://project/{projectId}/app-map/{kind}/{id}', {
574
+ list: async () => ({ resources: capResourceListing(listAppMaps(await currentProjectRoots(server, sessions), 'sections')) }),
575
+ }), { title: 'App knowledge map', description: 'Durable app map: full map, or a feature/screen/test-suite section.' }, async (uri, vars) => {
576
+ const projectId = decodeUriSegment(String(vars.projectId));
577
+ const root = resolveAppMapRoot(projectId, sessions);
181
578
  if (!root)
182
- throw new Error(`Unknown project ${String(vars.projectId)} (build the map first)`);
183
- const kind = vars.kind ? String(vars.kind) : undefined;
184
- const res = readAppMapResource(root, { kind, id: vars.id ? String(vars.id) : undefined });
579
+ throw new Error(`Unknown project ${projectId} (build the map first)`);
580
+ const kind = vars.kind ? decodeUriSegment(String(vars.kind)) : undefined;
581
+ const res = readAppMapResource(root, { kind, id: vars.id ? decodeUriSegment(String(vars.id)) : undefined });
185
582
  if (!res)
186
583
  throw new Error(`No app-map resource for ${uri.href}`);
187
584
  return { contents: [{ uri: uri.href, mimeType: res.mimeType, text: res.text }] };
188
585
  });
189
586
  // Bare full-map URI: swipium://project/{projectId}/app-map (no trailing section).
190
- server.registerResource('qa-app-map-full', new ResourceTemplate('swipium://project/{projectId}/app-map', { list: undefined }), { title: 'App knowledge map (full)', description: 'The complete durable app map JSON.' }, async (uri, vars) => {
191
- const root = resolveAppMapRoot(String(vars.projectId), sessions);
587
+ server.registerResource('qa-app-map-full', new ResourceTemplate('swipium://project/{projectId}/app-map', {
588
+ list: async () => ({ resources: capResourceListing(listAppMaps(await currentProjectRoots(server, sessions), 'full')) }),
589
+ }), { title: 'App knowledge map (full)', description: 'The complete durable app map JSON.' }, async (uri, vars) => {
590
+ const projectId = decodeUriSegment(String(vars.projectId));
591
+ const root = resolveAppMapRoot(projectId, sessions);
192
592
  if (!root)
193
- throw new Error(`Unknown project ${String(vars.projectId)} (build the map first)`);
593
+ throw new Error(`Unknown project ${projectId} (build the map first)`);
194
594
  const res = readAppMapResource(root, {});
195
595
  if (!res)
196
596
  throw new Error(`No app map for ${uri.href}`);
@@ -199,8 +599,21 @@ export function createServer() {
199
599
  return { server, sessions };
200
600
  }
201
601
  export async function startServer() {
602
+ // GUI MCP clients don't inherit the shell PATH: APPEND the Android SDK's platform-tools/ and
603
+ // emulator/ to PATH, but only for a tool (adb / emulator) that is not already resolvable on PATH,
604
+ // so the user's own copy is never shadowed. Idempotent (the CLI entry already calls it); done
605
+ // here too so embedders that call startServer() directly get it.
606
+ try {
607
+ ensureAndroidToolsOnPath();
608
+ }
609
+ catch (e) {
610
+ log('warn', 'android sdk path setup failed', { err: String(e) });
611
+ }
202
612
  const { server, sessions } = createServer();
203
613
  const transport = new StdioServerTransport();
614
+ // Persistence is debounced (SessionStore.persist), so make sure a graceful exit never
615
+ // loses the trailing write. 'exit' handlers must be synchronous; flushAll is.
616
+ process.once('exit', () => sessions.flushAll());
204
617
  // Best-effort: restore any network state Swipium changed, on shutdown / client disconnect
205
618
  // (so a budget-stop or crash mid-offline doesn't leave the emulator offline). Idempotent.
206
619
  let restoring = false;
@@ -214,25 +627,44 @@ export async function startServer() {
214
627
  await restoreAllNetwork(sessions);
215
628
  await stopAllRecordings(); // don't leave a device screen-recording after we exit
216
629
  await stopAllMetro(sessions); // don't leave a node bundler holding :8081 after we exit
630
+ // Managed WDA is NOT stopped: the next server adopts it if healthy and < 12 h old
631
+ // (processRegistry.reapOrphanedProcesses), else reaps it. `qa_wda stop` stops it explicitly.
217
632
  log('info', 'shutdown: restore done');
218
633
  }
219
634
  catch (e) {
220
635
  log('warn', 'shutdown: restore failed', { err: String(e) });
221
636
  }
637
+ sessions.flushAll(); // write any debounced session state before exiting
222
638
  process.exit(code);
223
639
  };
224
- process.once('SIGINT', () => void restoreThenExit(130, "SIGINT"));
225
- process.once('SIGTERM', () => void restoreThenExit(143, "SIGTERM"));
640
+ process.once('SIGINT', () => void restoreThenExit(130, 'SIGINT'));
641
+ process.once('SIGTERM', () => void restoreThenExit(143, 'SIGTERM'));
226
642
  // Startup banner (P1.8): version + tool count on stderr so a stale build is obvious in logs.
227
643
  log('info', 'swipium starting', { version: SWIPIUM_VERSION, tools: TOOL_COUNT });
228
644
  await server.connect(transport);
229
- // Chain AFTER connect — server.connect() sets its own transport.onclose, so we must wrap
645
+ // Chain AFTER connect: server.connect() sets its own transport.onclose, so we must wrap
230
646
  // it rather than assign before (which gets overwritten). stdin EOF = client disconnected.
231
647
  const sdkOnClose = transport.onclose;
232
648
  transport.onclose = () => {
233
649
  sdkOnClose?.();
234
- void restoreThenExit(0, "transport-close");
650
+ void restoreThenExit(0, 'transport-close');
235
651
  };
236
652
  log('info', 'swipium connected over stdio');
653
+ // Reap long-lived children (Metro, managed WDA, recorders) left behind by a crashed previous
654
+ // server run. Runs AFTER connect and in the background, so a slow sweep (lock wait, WDA /status
655
+ // probes) never delays the server becoming ready. Ownership + start-time/command fingerprint
656
+ // checks make this safe next to a live concurrent instance and against recycled PIDs. A managed
657
+ // WDA < 12 h old whose /status is healthy is ADOPTED instead (re-owned by this server) so a
658
+ // resumed iOS session keeps its WDA (shutdown intentionally leaves managed WDA running).
659
+ void startOrphanSweep();
660
+ }
661
+ /** Background orphan sweep; never rejects. Exported for tests. */
662
+ export async function startOrphanSweep(reap = reapOrphanedProcesses) {
663
+ try {
664
+ await reap();
665
+ }
666
+ catch (e) {
667
+ log('warn', 'orphaned-process sweep failed', { err: String(e) });
668
+ }
237
669
  }
238
670
  //# sourceMappingURL=server.js.map