@hraness/ghostget 0.17.2

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 (500) hide show
  1. package/CHANGELOG.md +458 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1359 -0
  4. package/bunfig.toml +2 -0
  5. package/dist/apple-photos-client.js +704 -0
  6. package/dist/beeper-client.js +4602 -0
  7. package/dist/client.js +1605 -0
  8. package/dist/index-26yq8q16.js +31 -0
  9. package/dist/index-9t13b4k7.js +508 -0
  10. package/dist/index-axwm9b3v.js +5 -0
  11. package/dist/index-gwk7rbyj.js +30 -0
  12. package/dist/index.js +701 -0
  13. package/dist/messaging.js +1130 -0
  14. package/dist/omni-client.js +1406 -0
  15. package/dist/whatsapp-client.js +355 -0
  16. package/docs/imessage-direct-provider.md +106 -0
  17. package/docs/rental-listings.md +94 -0
  18. package/package.json +600 -0
  19. package/skills/ghostget/SKILL.md +251 -0
  20. package/skills/ghostget/agents/openai.yaml +4 -0
  21. package/skills/ghostget/references/adapter-contract.md +181 -0
  22. package/skills/ghostget/references/apple-photos.md +115 -0
  23. package/skills/ghostget/references/article-drafts.md +235 -0
  24. package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
  25. package/skills/ghostget/references/cross-posting-video.md +139 -0
  26. package/skills/ghostget/references/cross-posting.md +82 -0
  27. package/skills/ghostget/references/derivation.md +390 -0
  28. package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
  29. package/skills/ghostget/references/install.md +66 -0
  30. package/skills/ghostget/references/linkedin-adapter.md +442 -0
  31. package/skills/ghostget/references/messaging.md +168 -0
  32. package/skills/ghostget/references/meta-comet-contract.md +153 -0
  33. package/skills/ghostget/references/platform-patterns.md +122 -0
  34. package/skills/ghostget/references/provider-plugins.md +496 -0
  35. package/skills/ghostget/references/reddit-flair.md +78 -0
  36. package/skills/ghostget/references/safety-and-state.md +214 -0
  37. package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
  38. package/skills/ghostget/references/social-platform-routing.md +66 -0
  39. package/skills/ghostget/references/social-profile-stats.md +195 -0
  40. package/skills/ghostget/references/social-video-platform-routing.md +89 -0
  41. package/skills/ghostget/references/whatsapp-adapter.md +101 -0
  42. package/skills/ghostget/references/x-adapter.md +332 -0
  43. package/skills/ghostget/references/x-ai-disclosure.md +71 -0
  44. package/src/apple-photos-cli.ts +639 -0
  45. package/src/apple-photos-client-types.ts +168 -0
  46. package/src/apple-photos-client.ts +201 -0
  47. package/src/apple-photos-contact-evidence.ts +838 -0
  48. package/src/apple-photos-local-source.ts +1131 -0
  49. package/src/args.ts +2053 -0
  50. package/src/article-draft-document.ts +417 -0
  51. package/src/article-draft-embeds.ts +123 -0
  52. package/src/article-draft-images.ts +214 -0
  53. package/src/assets/adapter-template/wrench-adapter.json +40 -0
  54. package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
  55. package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
  56. package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
  57. package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
  58. package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
  59. package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
  60. package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
  61. package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
  62. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
  63. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
  64. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
  65. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
  66. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
  67. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
  68. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
  69. package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
  70. package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
  71. package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
  72. package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
  73. package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
  74. package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
  75. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
  76. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
  77. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
  78. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
  79. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
  80. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
  81. package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
  82. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
  83. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
  84. package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
  85. package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
  86. package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
  87. package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
  88. package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
  89. package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
  90. package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
  91. package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
  92. package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
  93. package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
  94. package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
  95. package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
  96. package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
  97. package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
  98. package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
  99. package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
  100. package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
  101. package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
  102. package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
  103. package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
  104. package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
  105. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
  106. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
  107. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
  108. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
  109. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
  110. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
  111. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
  112. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
  113. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
  114. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
  115. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
  116. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
  117. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
  118. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
  119. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
  120. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
  121. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
  122. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
  123. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
  124. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
  125. package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
  126. package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
  127. package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
  128. package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
  129. package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
  130. package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
  131. package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
  132. package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
  133. package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
  134. package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
  135. package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
  136. package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
  137. package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
  138. package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
  139. package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
  140. package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
  141. package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
  142. package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
  143. package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
  144. package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
  145. package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
  146. package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
  147. package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
  148. package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
  149. package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
  150. package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
  151. package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
  152. package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
  153. package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
  154. package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
  155. package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
  156. package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
  157. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
  158. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
  159. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
  160. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
  161. package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
  162. package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
  163. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
  164. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
  165. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
  166. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
  167. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
  168. package/src/assets/adapters/x/wrench-adapter.json +749 -0
  169. package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
  170. package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
  171. package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
  172. package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
  173. package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
  174. package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
  175. package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
  176. package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
  177. package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
  178. package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
  179. package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
  180. package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
  181. package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
  182. package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
  183. package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
  184. package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
  185. package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
  186. package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
  187. package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
  188. package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
  189. package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
  190. package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
  191. package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
  192. package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
  193. package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
  194. package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
  195. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  196. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  197. package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
  198. package/src/assets.d.ts +4 -0
  199. package/src/auth.ts +1157 -0
  200. package/src/beeper-client-types.ts +169 -0
  201. package/src/beeper-client.ts +250 -0
  202. package/src/beeper-contact-interactions-cli.ts +180 -0
  203. package/src/beeper-contact-interactions.ts +1612 -0
  204. package/src/beeper-message-bundle-v1.ts +257 -0
  205. package/src/beeper-message-like-me-cli.ts +84 -0
  206. package/src/beeper-message-like-me-export.ts +2274 -0
  207. package/src/beeper-message-like-me-recovery.ts +931 -0
  208. package/src/beeper-message-like-me-source.ts +4034 -0
  209. package/src/browser-admission.ts +928 -0
  210. package/src/browser-snapshots.ts +260 -0
  211. package/src/browser.ts +3873 -0
  212. package/src/canonical-json.ts +60 -0
  213. package/src/catalog-cli.ts +638 -0
  214. package/src/cli.ts +208 -0
  215. package/src/client-types.ts +301 -0
  216. package/src/client.ts +2726 -0
  217. package/src/confirmed-write-failure.ts +44 -0
  218. package/src/confirmed-write-model.ts +72 -0
  219. package/src/confirmed-write-platform.ts +1055 -0
  220. package/src/confirmed-write-program.ts +224 -0
  221. package/src/confirmed-write-runtime.ts +16 -0
  222. package/src/cursor-token.ts +228 -0
  223. package/src/derivation-dnr-readiness.ts +367 -0
  224. package/src/derivation-file-chooser.ts +384 -0
  225. package/src/derivation-network-boundary.ts +835 -0
  226. package/src/derivation-network-guard.ts +782 -0
  227. package/src/derivation-network-proxy-helper.ts +440 -0
  228. package/src/derivation-network-proxy.ts +385 -0
  229. package/src/derive-command-helper.ts +693 -0
  230. package/src/derive-fixtures.ts +343 -0
  231. package/src/derive-review.ts +980 -0
  232. package/src/derive.ts +4754 -0
  233. package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
  234. package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
  235. package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
  236. package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
  237. package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
  238. package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
  239. package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
  240. package/src/ghostget.ts +3672 -0
  241. package/src/gmail-capture.ts +1244 -0
  242. package/src/har-internal.ts +1242 -0
  243. package/src/har.ts +743 -0
  244. package/src/index.ts +218 -0
  245. package/src/invocation-read-platform.ts +55 -0
  246. package/src/invocation-read-program.ts +93 -0
  247. package/src/invocation-read-runtime.ts +11 -0
  248. package/src/linked-device-lifecycle-admission.ts +450 -0
  249. package/src/linked-device-lifecycle-journal.ts +1560 -0
  250. package/src/linked-device-lifecycle-runtime.ts +1602 -0
  251. package/src/local-cli-admission.ts +135 -0
  252. package/src/local-cli-contracts.ts +271 -0
  253. package/src/local-cli-execution.ts +98 -0
  254. package/src/local-cli-surface-contract.ts +1510 -0
  255. package/src/local-cli-tool-identity.ts +336 -0
  256. package/src/media/archive.ts +2325 -0
  257. package/src/media/args.ts +303 -0
  258. package/src/media/cli.ts +287 -0
  259. package/src/media/doctor.ts +475 -0
  260. package/src/media/ffmpeg.ts +1330 -0
  261. package/src/media/http-capture.ts +711 -0
  262. package/src/media/http-probe.ts +593 -0
  263. package/src/media/http.ts +387 -0
  264. package/src/media/index.ts +10 -0
  265. package/src/media/local-transcription.ts +613 -0
  266. package/src/media/lock.ts +321 -0
  267. package/src/media/manifest.ts +1712 -0
  268. package/src/media/metadata.ts +906 -0
  269. package/src/media/process.ts +723 -0
  270. package/src/media/revision.ts +166 -0
  271. package/src/media/runtime-closure.ts +1063 -0
  272. package/src/media/source-router.ts +187 -0
  273. package/src/media/transcriber-config.ts +1122 -0
  274. package/src/media/transcript-persistence-model.ts +15 -0
  275. package/src/media/transcript-persistence-platform.ts +89 -0
  276. package/src/media/transcript-persistence-program.ts +39 -0
  277. package/src/media/transcript-persistence-runtime.ts +24 -0
  278. package/src/media/transcript.ts +616 -0
  279. package/src/media/utf8-order.ts +30 -0
  280. package/src/media/whisper-cpp.ts +464 -0
  281. package/src/media/whisper-language.ts +58 -0
  282. package/src/media/yt-dlp.ts +439 -0
  283. package/src/message-like-me-agentic-messaging.ts +882 -0
  284. package/src/messaging-action-store.ts +793 -0
  285. package/src/messaging-runtime.ts +2136 -0
  286. package/src/messaging-store.ts +539 -0
  287. package/src/messaging-types.ts +1663 -0
  288. package/src/messaging.ts +334 -0
  289. package/src/model.ts +2671 -0
  290. package/src/oauth-google.ts +888 -0
  291. package/src/omni-client-types.ts +306 -0
  292. package/src/omni-client.ts +2225 -0
  293. package/src/omni-limits.ts +34 -0
  294. package/src/omni-model.ts +1614 -0
  295. package/src/omni-request.ts +379 -0
  296. package/src/omni-runtime.ts +1632 -0
  297. package/src/operation-deadline.ts +189 -0
  298. package/src/path-helper.ts +1656 -0
  299. package/src/pinned-https.ts +194 -0
  300. package/src/plan-assets.ts +832 -0
  301. package/src/platform-catalog.ts +1832 -0
  302. package/src/plugins/beeper-linked-device/plugin.ts +310 -0
  303. package/src/plugins/bluesky-web/plugin.ts +178 -0
  304. package/src/plugins/clasificados-web/plugin.ts +66 -0
  305. package/src/plugins/github-web/plugin.ts +80 -0
  306. package/src/plugins/gmail-official/plugin.ts +110 -0
  307. package/src/plugins/hacker-news-web/plugin.ts +52 -0
  308. package/src/plugins/imessage-direct/plugin.ts +195 -0
  309. package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
  310. package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
  311. package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
  312. package/src/plugins/linkedin-official/plugin.ts +84 -0
  313. package/src/plugins/linkedin-web/plugin.ts +412 -0
  314. package/src/plugins/meta-web/plugin.ts +317 -0
  315. package/src/plugins/reddit-web/flair.ts +492 -0
  316. package/src/plugins/reddit-web/plugin.ts +477 -0
  317. package/src/plugins/substack-web/plugin.ts +124 -0
  318. package/src/plugins/tiktok-web/plugin.ts +67 -0
  319. package/src/plugins/twitch-web/plugin.ts +54 -0
  320. package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
  321. package/src/plugins/x-official/plugin.ts +64 -0
  322. package/src/plugins/x-web/plugin.ts +488 -0
  323. package/src/plugins/youtube-web/plugin.ts +101 -0
  324. package/src/portable-run-recovery.ts +466 -0
  325. package/src/process-identity.ts +501 -0
  326. package/src/profile-clone-helper.ts +104 -0
  327. package/src/provider-catalog-views.ts +51 -0
  328. package/src/provider-context.ts +69 -0
  329. package/src/provider-contract-definitions-x.ts +45 -0
  330. package/src/provider-contract-definitions.ts +948 -0
  331. package/src/provider-contract-input-gmail.ts +176 -0
  332. package/src/provider-contract-input-linkedin.ts +77 -0
  333. package/src/provider-contract-input-x.ts +304 -0
  334. package/src/provider-contract-planning.ts +23 -0
  335. package/src/provider-contract-semantic-identity.ts +54 -0
  336. package/src/provider-contracts.ts +251 -0
  337. package/src/provider-http.ts +723 -0
  338. package/src/provider-plugin-auth.ts +36 -0
  339. package/src/provider-plugin-builtins.ts +229 -0
  340. package/src/provider-plugin-cleanup-barrier.ts +229 -0
  341. package/src/provider-plugin-cleanup-execution.ts +79 -0
  342. package/src/provider-plugin-cleanup-resource.ts +470 -0
  343. package/src/provider-plugin-contract-identity.ts +707 -0
  344. package/src/provider-plugin-host.ts +1972 -0
  345. package/src/provider-plugin-identifiers.ts +60 -0
  346. package/src/provider-plugin-invocation-lease.ts +889 -0
  347. package/src/provider-plugin-lifecycle-kernel.ts +566 -0
  348. package/src/provider-plugin-lifecycle.ts +1455 -0
  349. package/src/provider-plugin-module-analysis.ts +336 -0
  350. package/src/provider-plugin-package.ts +2530 -0
  351. package/src/provider-plugin-portable-authority.ts +106 -0
  352. package/src/provider-plugin-portable-catalog.ts +94 -0
  353. package/src/provider-plugin-portable-identity.ts +315 -0
  354. package/src/provider-plugin-portable-runtime.ts +2057 -0
  355. package/src/provider-plugin-protocol.ts +1625 -0
  356. package/src/provider-plugin-registry.ts +3342 -0
  357. package/src/provider-plugin-store.ts +2155 -0
  358. package/src/provider-plugin-test-fixture.ts +5 -0
  359. package/src/provider-plugin.ts +4907 -0
  360. package/src/provider-plugins.generated.ts +43 -0
  361. package/src/provider-plugins.ts +8 -0
  362. package/src/provider-subject.ts +41 -0
  363. package/src/provider.ts +315 -0
  364. package/src/providers/beeper-local-runtime.ts +5426 -0
  365. package/src/providers/beeper-local.ts +2739 -0
  366. package/src/providers/beeper-messaging.ts +462 -0
  367. package/src/providers/beeper-omni.ts +1159 -0
  368. package/src/providers/bluesky-web-runtime.ts +2895 -0
  369. package/src/providers/bluesky-web.ts +1614 -0
  370. package/src/providers/clasificados-web-runtime.ts +255 -0
  371. package/src/providers/clasificados-web.ts +503 -0
  372. package/src/providers/contact-projection.ts +301 -0
  373. package/src/providers/github-read-model.ts +94 -0
  374. package/src/providers/github-read-platform.ts +165 -0
  375. package/src/providers/github-read-program.ts +97 -0
  376. package/src/providers/github-web-runtime.ts +320 -0
  377. package/src/providers/github-web.ts +403 -0
  378. package/src/providers/gmail-api.ts +2834 -0
  379. package/src/providers/gmail-omni.ts +830 -0
  380. package/src/providers/gmail.ts +1130 -0
  381. package/src/providers/hacker-news-web-runtime.ts +220 -0
  382. package/src/providers/hacker-news-web.ts +809 -0
  383. package/src/providers/imessage-direct-install.ts +354 -0
  384. package/src/providers/imessage-direct-messaging.ts +532 -0
  385. package/src/providers/imessage-direct-omni.ts +400 -0
  386. package/src/providers/imessage-direct-runtime.ts +1498 -0
  387. package/src/providers/imessage-direct.ts +351 -0
  388. package/src/providers/instagram-video-foundations.ts +913 -0
  389. package/src/providers/instagram-web-profile-browser.ts +600 -0
  390. package/src/providers/iso-bmff.ts +338 -0
  391. package/src/providers/linkedin-company-platform.ts +42 -0
  392. package/src/providers/linkedin-company-program.ts +88 -0
  393. package/src/providers/linkedin-contact-failure.ts +84 -0
  394. package/src/providers/linkedin-contact-platform.ts +45 -0
  395. package/src/providers/linkedin-contact-program.ts +91 -0
  396. package/src/providers/linkedin-profile-activity-failure.ts +76 -0
  397. package/src/providers/linkedin-profile-activity-platform.ts +41 -0
  398. package/src/providers/linkedin-profile-activity-program.ts +67 -0
  399. package/src/providers/linkedin-read-failure.ts +52 -0
  400. package/src/providers/linkedin-self-platform.ts +53 -0
  401. package/src/providers/linkedin-self-program.ts +122 -0
  402. package/src/providers/linkedin-web-article-browser.ts +794 -0
  403. package/src/providers/linkedin-web-bootstrap.ts +146 -0
  404. package/src/providers/linkedin-web-contact.ts +830 -0
  405. package/src/providers/linkedin-web-feed-browser.ts +832 -0
  406. package/src/providers/linkedin-web-feed.ts +864 -0
  407. package/src/providers/linkedin-web-post-browser.ts +653 -0
  408. package/src/providers/linkedin-web-profile-browser.ts +683 -0
  409. package/src/providers/linkedin-web-runtime.ts +2388 -0
  410. package/src/providers/linkedin-web.ts +3305 -0
  411. package/src/providers/linkedin.ts +1885 -0
  412. package/src/providers/meta-bootstrap.ts +1043 -0
  413. package/src/providers/meta-facebook-group.ts +690 -0
  414. package/src/providers/meta-marketplace-relay.ts +747 -0
  415. package/src/providers/meta-omni.ts +373 -0
  416. package/src/providers/meta-relay-bundle.ts +1049 -0
  417. package/src/providers/meta-web-descriptors.ts +1740 -0
  418. package/src/providers/meta-web-runtime.ts +3388 -0
  419. package/src/providers/meta-web.ts +2931 -0
  420. package/src/providers/read-failure.ts +139 -0
  421. package/src/providers/reddit-omni.ts +462 -0
  422. package/src/providers/reddit-web-runtime.ts +1929 -0
  423. package/src/providers/reddit-web.ts +1611 -0
  424. package/src/providers/rental-listings.ts +460 -0
  425. package/src/providers/substack-omni.ts +406 -0
  426. package/src/providers/substack-video-mp4.ts +33 -0
  427. package/src/providers/substack-web-runtime.ts +2812 -0
  428. package/src/providers/substack-web.ts +1502 -0
  429. package/src/providers/tiktok-video-mp4.ts +33 -0
  430. package/src/providers/tiktok-web-runtime.ts +770 -0
  431. package/src/providers/tiktok-web.ts +2786 -0
  432. package/src/providers/twitch-web-runtime.ts +226 -0
  433. package/src/providers/twitch-web.ts +274 -0
  434. package/src/providers/whatsapp-account-identity.ts +55 -0
  435. package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
  436. package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
  437. package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
  438. package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
  439. package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
  440. package/src/providers/whatsapp-messaging.ts +132 -0
  441. package/src/providers/whatsapp-omni.ts +586 -0
  442. package/src/providers/whatsapp-web-runtime.ts +3108 -0
  443. package/src/providers/whatsapp-web.ts +1037 -0
  444. package/src/providers/x-image-provenance.ts +523 -0
  445. package/src/providers/x-made-with-ai.ts +106 -0
  446. package/src/providers/x-transaction-id.ts +413 -0
  447. package/src/providers/x-web-runtime.ts +3485 -0
  448. package/src/providers/x-web.ts +2332 -0
  449. package/src/providers/x.ts +1889 -0
  450. package/src/providers/youtube-web-runtime.ts +1400 -0
  451. package/src/providers/youtube-web.ts +1221 -0
  452. package/src/read-admission-runtime.ts +91 -0
  453. package/src/read-client.ts +521 -0
  454. package/src/read-effect-platform.ts +7 -0
  455. package/src/read-effect-runtime.ts +14 -0
  456. package/src/read-effect.ts +52 -0
  457. package/src/read-projection-admission.ts +866 -0
  458. package/src/read-projections.ts +3151 -0
  459. package/src/recovery.ts +1492 -0
  460. package/src/reviewed-template.ts +146 -0
  461. package/src/run-journal.ts +1272 -0
  462. package/src/runtime.ts +5116 -0
  463. package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
  464. package/src/scripts/install-whatsapp-protocol.sh +232 -0
  465. package/src/scripts/recover-install-stages.ts +1491 -0
  466. package/src/scripts/resolve-state-home.ts +30 -0
  467. package/src/scripts/scaffold-web-provider.ts +973 -0
  468. package/src/scripts/sync-bundled-adapters.ts +695 -0
  469. package/src/session-secrets.ts +1183 -0
  470. package/src/state-helper.bunfig.toml +2 -0
  471. package/src/state-helper.ts +2914 -0
  472. package/src/storage.ts +3842 -0
  473. package/src/transport-policy.ts +2 -0
  474. package/src/usage.ts +182 -0
  475. package/src/version.ts +2 -0
  476. package/src/web-session-authentication-policy.ts +136 -0
  477. package/src/web-session-cleanup-admission.ts +2344 -0
  478. package/src/web-session-cleanup-join.ts +99 -0
  479. package/src/web-session-client.ts +996 -0
  480. package/src/web-session-contract-definitions.ts +820 -0
  481. package/src/web-session-contract-planning.ts +42 -0
  482. package/src/web-session-contracts.ts +228 -0
  483. package/src/web-session-cookies.ts +500 -0
  484. package/src/web-session-execution.ts +385 -0
  485. package/src/web-session-http.ts +373 -0
  486. package/src/web-session-read-errors.ts +19 -0
  487. package/src/web-session-read-runtime.ts +124 -0
  488. package/src/web-session-recovery.ts +818 -0
  489. package/src/web-session-sites.ts +58 -0
  490. package/src/web-session-template.ts +994 -0
  491. package/src/web-session.ts +93 -0
  492. package/src/whatsapp-client-binding.ts +21 -0
  493. package/src/whatsapp-client-types.ts +75 -0
  494. package/src/whatsapp-client.ts +395 -0
  495. package/src/whatsapp-export-coordinate.ts +17 -0
  496. package/src/whatsapp-message-bundle-v2.ts +195 -0
  497. package/src/whatsapp-message-like-me-cli.ts +195 -0
  498. package/src/whatsapp-message-like-me-export.ts +1072 -0
  499. package/src/whatsapp-message-like-me-source.ts +1012 -0
  500. package/tsconfig.json +67 -0
@@ -0,0 +1,3672 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, lstatSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+
5
+ import { BoundedByteBuffer } from "@hraness/kb/clip/bounded-byte-buffer";
6
+ import {
7
+ clipMain,
8
+ inspectClipEnvironment,
9
+ parseCaptureArguments,
10
+ renderDoctorReport,
11
+ type ClipRuntimeOptions,
12
+ } from "@hraness/kb/capture";
13
+ import { redactSensitiveText } from "@hraness/kb/clip/persist";
14
+ import { sanitizeTerminalLine, sanitizeTerminalText } from "@hraness/kb/clip/terminal";
15
+ import {
16
+ createAuth,
17
+ listAuth,
18
+ loadAuth,
19
+ loadAuthSnapshot,
20
+ removeAuth,
21
+ replaceAuthIfUnchanged,
22
+ saveAuth,
23
+ type GhostgetAuth,
24
+ } from "./auth";
25
+ import {
26
+ isPublicWebSessionInvocationAuthority,
27
+ type InvocationAuthority,
28
+ } from "./web-session-authentication-policy";
29
+ import { parseGhostgetArguments, ghostgetUsage, type GhostgetArguments } from "./args";
30
+ import type * as BeeperMessageLikeMeCliRuntimeModule from "./beeper-message-like-me-cli";
31
+ import type * as BeeperContactInteractionCliRuntimeModule from "./beeper-contact-interactions-cli";
32
+ import type * as ApplePhotosCliRuntimeModule from "./apple-photos-cli";
33
+ import type * as WhatsAppMessageLikeMeCliRuntimeModule from "./whatsapp-message-like-me-cli";
34
+ import type { BeeperMessageLikeMeProgress } from "./beeper-message-like-me-source";
35
+ import type { BeeperContactInteractionCliProgress } from "./beeper-contact-interactions-cli";
36
+ import type { WhatsAppMessageLikeMeCliProgress } from "./whatsapp-message-like-me-cli";
37
+ import type { GmailCaptureRunner } from "./gmail-capture";
38
+ import type * as MediaRuntimeModule from "./media";
39
+ import {
40
+ cloneBrowserProfile,
41
+ PreservedBrowserArtifactsError,
42
+ profilePath,
43
+ } from "./browser";
44
+ import { runCaptureWithBrowserAdmission } from "./browser-admission";
45
+ import {
46
+ createBrowserSnapshotDirectory,
47
+ purgeOrphanedBrowserSnapshots,
48
+ removeBrowserSnapshotDirectory,
49
+ } from "./browser-snapshots";
50
+ import {
51
+ runCapabilities,
52
+ runPluginList,
53
+ runPluginShow,
54
+ } from "./catalog-cli";
55
+ import {
56
+ discardDerivation,
57
+ finishDerivation,
58
+ listDerivations,
59
+ reviewDerivation,
60
+ runDerivationBrowserCommand,
61
+ startDerivation,
62
+ } from "./derive";
63
+ import {
64
+ parseDerivationReviewFieldNames,
65
+ parseDerivationReviewFixtures,
66
+ type DerivationReviewFieldNames,
67
+ type DerivationReviewFixtures,
68
+ type DerivationReviewSelection,
69
+ } from "./derive-review";
70
+ import { derivationFixtureSummaries } from "./derive-fixtures";
71
+ import { analyzeHarFile, emptyManifest, writeDerivationScaffold } from "./har";
72
+ import {
73
+ canonicalJson,
74
+ DOM_ACTION_TRANSPORT_DISABLED_MESSAGE,
75
+ isProviderOperation,
76
+ isLocalCliOperation,
77
+ isReviewedTemplateOperation,
78
+ isWebSessionOperation,
79
+ manifestHash,
80
+ sha256,
81
+ GHOSTGET_LEGACY_LINKEDIN_MANIFEST_HASH,
82
+ type OperationInput,
83
+ type GhostgetOperation,
84
+ } from "./model";
85
+ import { getProviderContract, providerContractHash } from "./provider-contracts";
86
+ import {
87
+ getLocalCliContract,
88
+ localCliContractHash,
89
+ } from "./local-cli-contracts";
90
+ import { withLocalCliProviderCleanupAdmission } from "./local-cli-admission";
91
+ import {
92
+ portableProviderPluginSubjectProbeIdentity,
93
+ type LocalCliPluginBindingV1,
94
+ type LinkedDevicePluginBindingV1,
95
+ type ProviderPluginBindingV1,
96
+ type ProviderPluginLinkedDeviceAttemptBoundaryV1,
97
+ } from "./provider-plugin";
98
+ import {
99
+ LEGACY_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
100
+ WRENCH_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
101
+ PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
102
+ } from "./provider-plugin-package";
103
+ import {
104
+ acquirePortableProviderPluginInvocationLease,
105
+ createPortableProviderPluginInvocationLeaseContainmentController,
106
+ releasePortableProviderPluginInvocationLease,
107
+ } from "./provider-plugin-invocation-lease";
108
+ import { requireProviderPluginAuth } from "./provider-plugin-auth";
109
+ import { acquireReadProjectionAuthAdmission } from "./read-projection-admission";
110
+ import {
111
+ recoverLinkedDeviceLifecycleAdmissions,
112
+ } from "./linked-device-lifecycle-admission";
113
+ import {
114
+ readLinkedDeviceLifecycleJournal,
115
+ } from "./linked-device-lifecycle-journal";
116
+ import {
117
+ LinkedDeviceLifecycleIndeterminateError,
118
+ reconcileLinkedDeviceLifecycleJournal,
119
+ recoverLinkedDeviceLifecycleJournals,
120
+ runLinkedDevicePairLifecycle,
121
+ runLinkedDeviceSyncOnceLifecycle,
122
+ } from "./linked-device-lifecycle-runtime";
123
+ import {
124
+ checkPortableProviderPlugin,
125
+ disablePortableProviderPlugin,
126
+ doctorPortableProviderPlugins,
127
+ initPortableProviderPlugin,
128
+ installPortableProviderPlugin,
129
+ packPortableProviderPlugin,
130
+ portableProviderPluginStoreRoot,
131
+ removePortableProviderPlugin,
132
+ showPortableProviderPlugin,
133
+ testPortableProviderPlugin,
134
+ } from "./provider-plugin-lifecycle";
135
+ import {
136
+ assertPortableProviderPluginActivatable,
137
+ assertPortableProviderPluginQuiescent,
138
+ } from "./provider-plugin-lifecycle-kernel";
139
+ import { createPortableProviderPluginCatalog } from "./provider-plugin-portable-catalog";
140
+ import type { ProviderPluginRegistry } from "./provider-plugin-registry";
141
+ import { withPortableProviderPluginCatalogLock } from "./provider-plugin-store";
142
+ import { providerPluginRegistry } from "./provider-plugins";
143
+ import { reconcilePortableProviderPluginRun } from "./portable-run-recovery";
144
+ import { getWebSessionContract, webSessionContractHash } from "./web-session-contracts";
145
+ import { isCookieCapableWebAuth, reviewedTemplateHash } from "./reviewed-template";
146
+ import {
147
+ recoverWebSessionCleanupAdmissions,
148
+ } from "./web-session-cleanup-admission";
149
+ import {
150
+ readFailureProjection,
151
+ WEB_SESSION_CLEANUP_JOIN_TIMEOUT_MS,
152
+ } from "./web-session-execution";
153
+ import { reconcileWebSessionRun } from "./web-session-recovery";
154
+ import { loadOAuthToken, requireOAuthScopes } from "./provider-http";
155
+ import {
156
+ installManagedGoogleOAuth,
157
+ loginGoogleOAuth,
158
+ } from "./oauth-google";
159
+ import {
160
+ compositionNames,
161
+ platformSurfaceIds,
162
+ semanticOperationNames,
163
+ socialPlatformCatalog,
164
+ splitWeightedThread,
165
+ textWeightPolicies,
166
+ weightedTextLength,
167
+ type PlatformSurfaceCatalogEntry,
168
+ type PlatformSurfaceId,
169
+ } from "./platform-catalog";
170
+ import { isPlanBoundFile, summarizePlanFile } from "./plan-assets";
171
+ import {
172
+ cancelInvocationPlan,
173
+ confirmMessagingInvocation,
174
+ confirmInvocation,
175
+ createAndSaveInvocationPlan,
176
+ createReadProjectionQueryForInvocation,
177
+ type createInvocationPlan,
178
+ type executeReadInvocation,
179
+ listInvocationPlans,
180
+ listRunReceipts,
181
+ prepareInvocation,
182
+ loadInvocationPlan,
183
+ purgeExpiredPlans,
184
+ readRunReceipt,
185
+ repairInterruptedConfirmationClaims,
186
+ repairInterruptedRunJournals,
187
+ } from "./runtime";
188
+ import {
189
+ readCachedPreparedCapability,
190
+ revalidatePreparedCapability,
191
+ type RevalidatedCapability,
192
+ } from "./read-client";
193
+ import type { ReadProjectionCacheResult } from "./read-projections";
194
+ import {
195
+ identifyOmniView,
196
+ readCachedOmniViewInternal,
197
+ rebuildOmniViewFromExactCache,
198
+ revalidateOmniViewInternal,
199
+ } from "./omni-runtime";
200
+ import {
201
+ discoverMessagingRoutesInternal,
202
+ loadMessagingPreviewForConfirmationInternal,
203
+ previewMessagingTurnInternal,
204
+ readMessagingContextInternal,
205
+ reserveMessagingPrivateOutputPair,
206
+ reconcileMessagingRunInternal,
207
+ resolveMessagingRouteInternal,
208
+ showMessagingRunInternal,
209
+ validateMessagingPrivateOutputPath,
210
+ writeMessagingPrivateOutput,
211
+ writeReservedMessagingPrivateOutput,
212
+ } from "./messaging-runtime";
213
+ import { readMessagingRunIfPresent } from "./messaging-action-store";
214
+ import {
215
+ checkSourceProviderPluginDirectory,
216
+ scaffoldWebProvider,
217
+ } from "./scripts/scaffold-web-provider";
218
+ import {
219
+ createPrivateJsonIfAbsent,
220
+ installManifest,
221
+ listInstalledManifests,
222
+ loadInstalledDiagnosticManifestSnapshot,
223
+ readDiagnosticManifestFile,
224
+ readManifestFile,
225
+ readRegularFile,
226
+ removeInstalledManifest,
227
+ ensurePrivateStateDirectory,
228
+ ghostgetStateHome,
229
+ writePrivateJson,
230
+ } from "./storage";
231
+
232
+ type Output = {
233
+ readonly stdout: (value: string) => void;
234
+ readonly stderr: (value: string) => void;
235
+ };
236
+
237
+ type MediaRuntime = typeof MediaRuntimeModule;
238
+ type BeeperMessageLikeMeCliRuntime = typeof BeeperMessageLikeMeCliRuntimeModule;
239
+ type BeeperContactInteractionCliRuntime =
240
+ typeof BeeperContactInteractionCliRuntimeModule;
241
+ type ApplePhotosCliRuntime = typeof ApplePhotosCliRuntimeModule;
242
+ type WhatsAppMessageLikeMeCliRuntime = typeof WhatsAppMessageLikeMeCliRuntimeModule;
243
+ type ImsgDirectInstallRuntime =
244
+ typeof import("./providers/imessage-direct-install");
245
+
246
+ /**
247
+ * Ghostget's stable boundary around the independently versioned KB doctor.
248
+ *
249
+ * The default adapter preserves the complete KB report for JSON output while
250
+ * keeping its concrete schema and terminal renderer out of Ghostget's dependency
251
+ * and test contracts.
252
+ */
253
+ export type GhostgetClipEnvironmentInspection = {
254
+ readonly report: unknown;
255
+ readonly renderReport: () => string;
256
+ readonly browserCaptureBootstrapReady: boolean;
257
+ };
258
+
259
+ const defaultOutput: Output = {
260
+ stdout: (value) => process.stdout.write(value),
261
+ stderr: (value) => process.stderr.write(value),
262
+ };
263
+
264
+ const loadMediaRuntime = (): Promise<MediaRuntime> => import("./media");
265
+ const loadBeeperMessageLikeMeCliRuntime = (): Promise<BeeperMessageLikeMeCliRuntime> =>
266
+ import("./beeper-message-like-me-cli");
267
+ const loadBeeperContactInteractionCliRuntime = (): Promise<BeeperContactInteractionCliRuntime> =>
268
+ import("./beeper-contact-interactions-cli");
269
+ const loadApplePhotosCliRuntime = (): Promise<ApplePhotosCliRuntime> =>
270
+ import("./apple-photos-cli");
271
+ const loadWhatsAppMessageLikeMeCliRuntime = (): Promise<WhatsAppMessageLikeMeCliRuntime> =>
272
+ import("./whatsapp-message-like-me-cli");
273
+ const loadImsgDirectInstallRuntime = (): Promise<ImsgDirectInstallRuntime> =>
274
+ import("./providers/imessage-direct-install");
275
+
276
+ const runDefaultGmailCapture: GmailCaptureRunner = async (...arguments_) => {
277
+ const { runGmailCapture } = await import("./gmail-capture");
278
+ return runGmailCapture(...arguments_);
279
+ };
280
+
281
+ const inspectDefaultClipEnvironment = async (): Promise<GhostgetClipEnvironmentInspection> => {
282
+ const report = await inspectClipEnvironment();
283
+ return {
284
+ report,
285
+ renderReport: () => renderDoctorReport(report),
286
+ browserCaptureBootstrapReady: report.deriveClient.status === "ready"
287
+ && report.dependencies.find(
288
+ (dependency) => dependency.name === "agent-browser",
289
+ )?.status === "ready"
290
+ && report.dependencies.find(
291
+ (dependency) => dependency.name === "@steipete/sweet-cookie",
292
+ )?.status === "ready",
293
+ };
294
+ };
295
+
296
+ export function renderGhostgetUsage(): string {
297
+ return ghostgetUsage;
298
+ }
299
+
300
+ export type GhostgetDependencies = {
301
+ readonly clipMain: typeof clipMain;
302
+ readonly runCapture: typeof runCaptureWithBrowserAdmission;
303
+ readonly gmailCaptureMain: GmailCaptureRunner;
304
+ readonly inspectClipEnvironment: () => Promise<GhostgetClipEnvironmentInspection>;
305
+ readonly loadMediaRuntime: () => Promise<MediaRuntime>;
306
+ readonly loadBeeperMessageLikeMeCliRuntime:
307
+ () => Promise<BeeperMessageLikeMeCliRuntime>;
308
+ readonly loadBeeperContactInteractionCliRuntime:
309
+ () => Promise<BeeperContactInteractionCliRuntime>;
310
+ readonly loadApplePhotosCliRuntime: () => Promise<ApplePhotosCliRuntime>;
311
+ readonly loadWhatsAppMessageLikeMeCliRuntime:
312
+ () => Promise<WhatsAppMessageLikeMeCliRuntime>;
313
+ readonly loadImsgDirectInstallRuntime:
314
+ () => Promise<ImsgDirectInstallRuntime>;
315
+ readonly providerPluginRegistry: ProviderPluginRegistry;
316
+ readonly probePluginSubject: (
317
+ binding: ProviderPluginBindingV1,
318
+ auth: GhostgetAuth,
319
+ signal?: AbortSignal,
320
+ registry?: ProviderPluginRegistry,
321
+ environment?: Readonly<Record<string, string | undefined>>,
322
+ ) => Promise<string>;
323
+ /** Test seam after subject validation and before the auth snapshot CAS. */
324
+ readonly beforeAuthBindCommit: () => void | Promise<void>;
325
+ readonly pairLinkedDeviceAuth: (
326
+ binding: LinkedDevicePluginBindingV1,
327
+ auth: GhostgetAuth,
328
+ options: {
329
+ readonly phone?: string;
330
+ readonly environment: Readonly<Record<string, string | undefined>>;
331
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
332
+ },
333
+ ) => Promise<string>;
334
+ readonly syncLinkedDeviceAuthOnce: (
335
+ binding: LinkedDevicePluginBindingV1,
336
+ auth: GhostgetAuth,
337
+ options: {
338
+ readonly environment: Readonly<Record<string, string | undefined>>;
339
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
340
+ },
341
+ ) => Promise<{
342
+ readonly itemsStored: number;
343
+ readonly projection: string;
344
+ readonly emitsProtocolAcknowledgements: boolean;
345
+ }>;
346
+ readonly reconcileWebSessionRun: typeof reconcileWebSessionRun;
347
+ readonly reconcileLinkedDeviceLifecycleJournal:
348
+ typeof reconcileLinkedDeviceLifecycleJournal;
349
+ readonly reconcilePortableProviderPluginRun:
350
+ typeof reconcilePortableProviderPluginRun;
351
+ readonly createPortableProviderPluginCatalog:
352
+ typeof createPortableProviderPluginCatalog;
353
+ readonly readCachedPreparedCapability:
354
+ typeof readCachedPreparedCapability;
355
+ readonly revalidatePreparedCapability:
356
+ typeof revalidatePreparedCapability;
357
+ readonly identifyOmniView: typeof identifyOmniView;
358
+ readonly readCachedOmniViewInternal: typeof readCachedOmniViewInternal;
359
+ readonly rebuildOmniViewFromExactCache: typeof rebuildOmniViewFromExactCache;
360
+ readonly revalidateOmniViewInternal: typeof revalidateOmniViewInternal;
361
+ };
362
+
363
+ const defaultDependencies: GhostgetDependencies = {
364
+ clipMain,
365
+ runCapture: runCaptureWithBrowserAdmission,
366
+ gmailCaptureMain: runDefaultGmailCapture,
367
+ inspectClipEnvironment: inspectDefaultClipEnvironment,
368
+ loadMediaRuntime,
369
+ loadBeeperMessageLikeMeCliRuntime,
370
+ loadBeeperContactInteractionCliRuntime,
371
+ loadApplePhotosCliRuntime,
372
+ loadWhatsAppMessageLikeMeCliRuntime,
373
+ loadImsgDirectInstallRuntime,
374
+ providerPluginRegistry,
375
+ probePluginSubject: async (
376
+ binding,
377
+ auth,
378
+ signal,
379
+ registry = providerPluginRegistry,
380
+ environment = process.env,
381
+ ) => {
382
+ requireProviderPluginAuth(binding, auth);
383
+ const probe = binding.subject.probe;
384
+ if (probe === undefined) {
385
+ throw new Error(
386
+ `provider plugin surface ${binding.surfaceId} has no current-account probe`,
387
+ );
388
+ }
389
+ const subject = binding.transport === "local-cli"
390
+ ? await withLocalCliProviderCleanupAdmission(
391
+ {
392
+ registry,
393
+ binding,
394
+ auth,
395
+ purpose: { kind: "subject-probe" },
396
+ environment,
397
+ },
398
+ (registerCleanupBarrier) => probe(auth, {
399
+ ...(signal === undefined ? {} : { signal }),
400
+ environment,
401
+ registerCleanupBarrier,
402
+ }),
403
+ )
404
+ : await probe(
405
+ auth,
406
+ signal === undefined ? undefined : { signal, environment },
407
+ );
408
+ if (!binding.subject.matches(subject)) {
409
+ throw new Error(
410
+ `provider plugin surface ${binding.surfaceId} returned a subject outside ${binding.subject.format}`,
411
+ );
412
+ }
413
+ return subject;
414
+ },
415
+ beforeAuthBindCommit: () => undefined,
416
+ pairLinkedDeviceAuth: async (binding, auth, options) => {
417
+ const lifecycle = binding.linkedDeviceLifecycle;
418
+ if (lifecycle === undefined) {
419
+ throw new Error(
420
+ `linked-device plugin surface ${binding.surfaceId} does not declare pairing`,
421
+ );
422
+ }
423
+ return lifecycle.pair(auth, options);
424
+ },
425
+ syncLinkedDeviceAuthOnce: async (binding, auth, options) => {
426
+ const lifecycle = binding.linkedDeviceLifecycle;
427
+ if (lifecycle === undefined) {
428
+ throw new Error(
429
+ `linked-device plugin surface ${binding.surfaceId} does not declare one-shot sync`,
430
+ );
431
+ }
432
+ return lifecycle.syncOnce(auth, options);
433
+ },
434
+ reconcileWebSessionRun,
435
+ reconcileLinkedDeviceLifecycleJournal,
436
+ reconcilePortableProviderPluginRun,
437
+ createPortableProviderPluginCatalog,
438
+ readCachedPreparedCapability,
439
+ revalidatePreparedCapability,
440
+ identifyOmniView,
441
+ readCachedOmniViewInternal,
442
+ rebuildOmniViewFromExactCache,
443
+ revalidateOmniViewInternal,
444
+ };
445
+
446
+ function resolveDependencies(overrides: Partial<GhostgetDependencies>): GhostgetDependencies {
447
+ return {
448
+ clipMain: overrides.clipMain ?? defaultDependencies.clipMain,
449
+ runCapture: overrides.runCapture ?? defaultDependencies.runCapture,
450
+ gmailCaptureMain:
451
+ overrides.gmailCaptureMain ?? defaultDependencies.gmailCaptureMain,
452
+ inspectClipEnvironment: overrides.inspectClipEnvironment ?? defaultDependencies.inspectClipEnvironment,
453
+ loadMediaRuntime: overrides.loadMediaRuntime ?? defaultDependencies.loadMediaRuntime,
454
+ loadBeeperMessageLikeMeCliRuntime:
455
+ overrides.loadBeeperMessageLikeMeCliRuntime
456
+ ?? defaultDependencies.loadBeeperMessageLikeMeCliRuntime,
457
+ loadBeeperContactInteractionCliRuntime:
458
+ overrides.loadBeeperContactInteractionCliRuntime
459
+ ?? defaultDependencies.loadBeeperContactInteractionCliRuntime,
460
+ loadApplePhotosCliRuntime:
461
+ overrides.loadApplePhotosCliRuntime
462
+ ?? defaultDependencies.loadApplePhotosCliRuntime,
463
+ loadWhatsAppMessageLikeMeCliRuntime:
464
+ overrides.loadWhatsAppMessageLikeMeCliRuntime
465
+ ?? defaultDependencies.loadWhatsAppMessageLikeMeCliRuntime,
466
+ loadImsgDirectInstallRuntime:
467
+ overrides.loadImsgDirectInstallRuntime
468
+ ?? defaultDependencies.loadImsgDirectInstallRuntime,
469
+ providerPluginRegistry: overrides.providerPluginRegistry
470
+ ?? defaultDependencies.providerPluginRegistry,
471
+ probePluginSubject: overrides.probePluginSubject
472
+ ?? defaultDependencies.probePluginSubject,
473
+ beforeAuthBindCommit: overrides.beforeAuthBindCommit
474
+ ?? defaultDependencies.beforeAuthBindCommit,
475
+ pairLinkedDeviceAuth: overrides.pairLinkedDeviceAuth
476
+ ?? defaultDependencies.pairLinkedDeviceAuth,
477
+ syncLinkedDeviceAuthOnce: overrides.syncLinkedDeviceAuthOnce
478
+ ?? defaultDependencies.syncLinkedDeviceAuthOnce,
479
+ reconcileWebSessionRun: overrides.reconcileWebSessionRun ?? defaultDependencies.reconcileWebSessionRun,
480
+ reconcileLinkedDeviceLifecycleJournal:
481
+ overrides.reconcileLinkedDeviceLifecycleJournal
482
+ ?? defaultDependencies.reconcileLinkedDeviceLifecycleJournal,
483
+ reconcilePortableProviderPluginRun:
484
+ overrides.reconcilePortableProviderPluginRun
485
+ ?? defaultDependencies.reconcilePortableProviderPluginRun,
486
+ createPortableProviderPluginCatalog:
487
+ overrides.createPortableProviderPluginCatalog
488
+ ?? defaultDependencies.createPortableProviderPluginCatalog,
489
+ readCachedPreparedCapability:
490
+ overrides.readCachedPreparedCapability
491
+ ?? defaultDependencies.readCachedPreparedCapability,
492
+ revalidatePreparedCapability:
493
+ overrides.revalidatePreparedCapability
494
+ ?? defaultDependencies.revalidatePreparedCapability,
495
+ identifyOmniView:
496
+ overrides.identifyOmniView ?? defaultDependencies.identifyOmniView,
497
+ readCachedOmniViewInternal:
498
+ overrides.readCachedOmniViewInternal
499
+ ?? defaultDependencies.readCachedOmniViewInternal,
500
+ rebuildOmniViewFromExactCache:
501
+ overrides.rebuildOmniViewFromExactCache
502
+ ?? defaultDependencies.rebuildOmniViewFromExactCache,
503
+ revalidateOmniViewInternal:
504
+ overrides.revalidateOmniViewInternal
505
+ ?? defaultDependencies.revalidateOmniViewInternal,
506
+ };
507
+ }
508
+
509
+ function safe(value: string): string {
510
+ return sanitizeTerminalLine(redactSensitiveText(value));
511
+ }
512
+
513
+ function safeJson(value: unknown): string {
514
+ return `${JSON.stringify(value, (_key, candidate: unknown) =>
515
+ typeof candidate === "string" ? sanitizeTerminalText(redactSensitiveText(candidate)) : candidate, 2)}\n`;
516
+ }
517
+
518
+ const MAX_CLI_RECOVERY_HANDLE_BYTES = 8 * 1024;
519
+
520
+ function boundedCliRecoveryHandle(value: string): string | null {
521
+ if (
522
+ value.length < 1
523
+ || Buffer.byteLength(value, "utf8") > MAX_CLI_RECOVERY_HANDLE_BYTES
524
+ ) return null;
525
+ return /^[A-Za-z0-9_./:;=+-]+$/u.test(value) ? value : null;
526
+ }
527
+
528
+ function exactTerminalJson(value: unknown): string {
529
+ const json = JSON.stringify(value, null, 2);
530
+ // JSON escapes preserve the exact value when the document is parsed,
531
+ // while keeping terminal-control and bidi characters inert on screen.
532
+ return `${json.replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu, (character) =>
533
+ `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`)}\n`;
534
+ }
535
+
536
+ function print(output: Output, value: unknown, json: boolean): void {
537
+ output.stdout(json ? safeJson(value) : `${safe(typeof value === "string" ? value : JSON.stringify(value, null, 2))}\n`);
538
+ }
539
+
540
+ function beeperProgressInteger(value: number, minimum: number): number {
541
+ if (!Number.isSafeInteger(value) || value < minimum) {
542
+ throw new Error("Beeper export progress was invalid");
543
+ }
544
+ return value;
545
+ }
546
+
547
+ function beeperProgressPosition(
548
+ progress: Readonly<{ account: number; accounts: number }>,
549
+ ): string {
550
+ const account = beeperProgressInteger(progress.account, 1);
551
+ const accounts = beeperProgressInteger(progress.accounts, 1);
552
+ if (account > accounts) throw new Error("Beeper export progress was invalid");
553
+ return `${account}/${accounts}`;
554
+ }
555
+
556
+ function plural(count: number, singular: string, plural_: string): string {
557
+ return count === 1 ? singular : plural_;
558
+ }
559
+
560
+ function renderBeeperMessageLikeMeProgress(
561
+ progress: BeeperMessageLikeMeProgress | BeeperContactInteractionCliProgress,
562
+ ): string {
563
+ if (progress.phase === "recovery-started") {
564
+ return "ghostget: Beeper export: checking prior private export state\n";
565
+ }
566
+ if (progress.phase === "recovery-completed") {
567
+ const recovered = beeperProgressInteger(progress.recovered, 0);
568
+ const published = beeperProgressInteger(progress.published, 0);
569
+ return `ghostget: Beeper export: private recovery complete; ${recovered} ${plural(recovered, "directory", "directories")} reclaimed, ${published} published ${plural(published, "bundle", "bundles")} preserved\n`;
570
+ }
571
+ if (progress.phase === "preparing") {
572
+ return "ghostget: Beeper export: preparing pinned official CLI\n";
573
+ }
574
+ if (progress.phase === "accounts-discovered") {
575
+ const accounts = beeperProgressInteger(progress.accounts, 1);
576
+ return `ghostget: Beeper export: ${accounts} ${plural(accounts, "account", "accounts")} discovered\n`;
577
+ }
578
+ if (progress.phase === "accounts-progress") {
579
+ if (progress.stage !== "discovering" && progress.stage !== "verifying") {
580
+ throw new Error("Beeper export progress was invalid");
581
+ }
582
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
583
+ const action = progress.stage === "discovering"
584
+ ? "discovering accounts"
585
+ : "verifying connected accounts";
586
+ return `ghostget: Beeper export: ${action}; ${elapsedSeconds}s elapsed\n`;
587
+ }
588
+ if (progress.phase === "account-started") {
589
+ return `ghostget: Beeper export: account ${beeperProgressPosition(progress)} started\n`;
590
+ }
591
+ if (progress.phase === "account-validating") {
592
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
593
+ return `ghostget: Beeper export: account ${beeperProgressPosition(progress)} validating; ${elapsedSeconds}s elapsed\n`;
594
+ }
595
+ if (progress.phase === "account-progress") {
596
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
597
+ return `ghostget: Beeper export: account ${beeperProgressPosition(progress)} running; ${elapsedSeconds}s elapsed\n`;
598
+ }
599
+ if (progress.phase === "account-skipped") {
600
+ if (progress.reason !== "chat-limit-reached") {
601
+ throw new Error("Beeper export progress was invalid");
602
+ }
603
+ return `ghostget: Beeper export: account ${beeperProgressPosition(progress)} skipped; chat limit reached\n`;
604
+ }
605
+ if (progress.phase === "account-completed") {
606
+ const chats = beeperProgressInteger(progress.chats, 0);
607
+ const messages = beeperProgressInteger(progress.messages, 0);
608
+ return `ghostget: Beeper export: account ${beeperProgressPosition(progress)} complete; ${chats} ${plural(chats, "chat", "chats")}, ${messages} ${plural(messages, "message", "messages")} total\n`;
609
+ }
610
+ if (progress.phase === "accounts-verifying") {
611
+ const accounts = beeperProgressInteger(progress.accounts, 1);
612
+ return `ghostget: Beeper export: verifying ${accounts} connected ${plural(accounts, "account", "accounts")}\n`;
613
+ }
614
+ if (progress.phase === "conversion-started") {
615
+ const accounts = beeperProgressInteger(progress.accounts, 1);
616
+ const chats = beeperProgressInteger(progress.chats, 0);
617
+ const messages = beeperProgressInteger(progress.messages, 0);
618
+ return `ghostget: Beeper export: converting ${accounts} ${plural(accounts, "account", "accounts")}; ${chats} ${plural(chats, "chat", "chats")}, ${messages} ${plural(messages, "message", "messages")} total\n`;
619
+ }
620
+ if (progress.phase === "conversion-progress") {
621
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
622
+ return `ghostget: Beeper export: converting local bundle; ${elapsedSeconds}s elapsed\n`;
623
+ }
624
+ if (progress.phase === "bundle-building") {
625
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
626
+ const records = beeperProgressInteger(progress.records, 0);
627
+ const bytes = beeperProgressInteger(progress.bytes, 0);
628
+ return `ghostget: Beeper export: building local bundle; ${records} ${plural(records, "record", "records")}, ${bytes} bytes; ${elapsedSeconds}s elapsed\n`;
629
+ }
630
+ if (progress.phase === "bundle-validating") {
631
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
632
+ const records = beeperProgressInteger(progress.records, 0);
633
+ const bytes = beeperProgressInteger(progress.bytes, 0);
634
+ return `ghostget: Beeper export: validating local bundle; ${records} ${plural(records, "record", "records")}, ${bytes} bytes; ${elapsedSeconds}s elapsed\n`;
635
+ }
636
+ if (progress.phase === "bundle-publishing") {
637
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
638
+ const records = beeperProgressInteger(progress.records, 0);
639
+ const bytes = beeperProgressInteger(progress.bytes, 0);
640
+ return `ghostget: Beeper export: publishing local bundle atomically; ${records} ${plural(records, "record", "records")}, ${bytes} bytes; ${elapsedSeconds}s elapsed\n`;
641
+ }
642
+ if (progress.phase === "private-cleanup") {
643
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
644
+ return `ghostget: Beeper export: removing private raw shards; ${elapsedSeconds}s elapsed\n`;
645
+ }
646
+ if (progress.phase === "summary-building") {
647
+ const elapsedSeconds = beeperProgressInteger(progress.elapsedSeconds, 0);
648
+ const records = beeperProgressInteger(progress.records, 0);
649
+ return `ghostget: Beeper export: deriving body-free relationship summary; ${records} ${plural(records, "record", "records")}; ${elapsedSeconds}s elapsed\n`;
650
+ }
651
+ if (progress.phase === "summary-completed") {
652
+ const records = beeperProgressInteger(progress.records, 0);
653
+ const relationships = beeperProgressInteger(progress.relationships, 0);
654
+ const interactions = beeperProgressInteger(progress.interactions, 0);
655
+ return `ghostget: Beeper export: body-free summary complete; ${relationships} ${plural(relationships, "relationship", "relationships")}, ${interactions} ${plural(interactions, "interaction", "interactions")} from ${records} ${plural(records, "record", "records")}\n`;
656
+ }
657
+ const exhaustive: never = progress;
658
+ void exhaustive;
659
+ throw new Error("Beeper export progress was invalid");
660
+ }
661
+
662
+ function renderWhatsAppMessageLikeMeProgress(
663
+ progress: WhatsAppMessageLikeMeCliProgress,
664
+ ): string {
665
+ const integer = (value: number): number => {
666
+ if (!Number.isSafeInteger(value) || value < 0) {
667
+ throw new Error("WhatsApp export progress was invalid");
668
+ }
669
+ return value;
670
+ };
671
+ if (progress.phase === "recovery-started") {
672
+ return "ghostget: WhatsApp export: checking prior private export state\n";
673
+ }
674
+ if (progress.phase === "recovery-completed") {
675
+ const recovered = integer(progress.recovered);
676
+ const published = integer(progress.published);
677
+ return `ghostget: WhatsApp export: private recovery complete; ${recovered} ${plural(recovered, "directory", "directories")} reclaimed, ${published} published ${plural(published, "bundle", "bundles")} preserved\n`;
678
+ }
679
+ if (progress.phase === "preparing") {
680
+ return "ghostget: WhatsApp export: binding the quiescent local store\n";
681
+ }
682
+ if (progress.phase === "page-started" || progress.phase === "page-completed") {
683
+ const page = integer(progress.page);
684
+ const messages = integer(progress.messages);
685
+ const state = progress.phase === "page-started" ? "reading" : "completed";
686
+ return `ghostget: WhatsApp export: page ${page} ${state}; ${messages} local message rows observed\n`;
687
+ }
688
+ if (progress.phase === "conversion-completed") {
689
+ return `ghostget: WhatsApp export: local projection complete; ${integer(progress.messages)} rows, ${integer(progress.conversations)} conversations, ${integer(progress.participants)} participants\n`;
690
+ }
691
+ if (progress.phase === "v2-conversion") {
692
+ return `ghostget: WhatsApp export: publishing bundle v2 artifact ${integer(progress.artifact)}/${integer(progress.artifacts)}; ${integer(progress.records)} records converted\n`;
693
+ }
694
+ if (
695
+ progress.phase === "bundle-building"
696
+ || progress.phase === "bundle-validating"
697
+ || progress.phase === "bundle-publishing"
698
+ ) {
699
+ const action = progress.phase === "bundle-building"
700
+ ? "building"
701
+ : progress.phase === "bundle-validating"
702
+ ? "validating"
703
+ : "publishing";
704
+ return `ghostget: WhatsApp export: ${action} private bundle; ${integer(progress.records)} records, ${integer(progress.bytes)} bytes; ${integer(progress.elapsedSeconds)}s elapsed\n`;
705
+ }
706
+ if (progress.phase === "private-cleanup") {
707
+ return `ghostget: WhatsApp export: cleaning private validation state; ${integer(progress.elapsedSeconds)}s elapsed\n`;
708
+ }
709
+ const exhaustive: never = progress;
710
+ void exhaustive;
711
+ throw new Error("WhatsApp export progress was invalid");
712
+ }
713
+
714
+ type PreparedCapture = {
715
+ readonly arguments: readonly string[];
716
+ readonly runtimeOptions: ClipRuntimeOptions;
717
+ readonly cleanup: () => void;
718
+ };
719
+
720
+ type ResolvedCapture = {
721
+ readonly arguments: readonly string[];
722
+ readonly auth?: GhostgetAuth;
723
+ };
724
+
725
+ function resolveCaptureArgumentsWithAuth(
726
+ arguments_: readonly string[],
727
+ environment: Readonly<Record<string, string | undefined>>,
728
+ ): ResolvedCapture {
729
+ const forwarded: string[] = [];
730
+ let authId: string | undefined;
731
+ for (let index = 0; index < arguments_.length; index += 1) {
732
+ const argument = arguments_[index];
733
+ if (argument !== "--auth") {
734
+ forwarded.push(argument ?? "");
735
+ continue;
736
+ }
737
+ const value = arguments_[index + 1];
738
+ if (value === undefined || value.startsWith("--")) throw new Error("--auth requires a stored auth locator ID");
739
+ if (authId !== undefined) throw new Error("capture accepts at most one --auth locator");
740
+ authId = value;
741
+ index += 1;
742
+ }
743
+ if (authId === undefined) return { arguments: forwarded };
744
+ const explicitAuthFlags = new Set(["--browser-profile", "--cookie-source", "--cookie-profile", "--cookies-file"]);
745
+ if (forwarded.some((argument) => explicitAuthFlags.has(argument))) {
746
+ throw new Error("--auth cannot be combined with raw browser-profile or cookie-source options");
747
+ }
748
+ const auth = loadAuth(authId, environment);
749
+ if (auth.kind === "oauth-token-file") {
750
+ if (auth.provider === "gmail") {
751
+ return { arguments: forwarded, auth };
752
+ }
753
+ throw new Error(
754
+ `auth locator ${auth.id} is for official ${auth.provider} API capabilities and cannot be used for browser capture; use cookie/profile auth for clip or read`,
755
+ );
756
+ }
757
+ if (auth.kind === "linked-device-store") {
758
+ throw new Error(
759
+ `auth locator ${auth.id} is a ${auth.provider} linked-device protocol realm and cannot be used for browser capture`,
760
+ );
761
+ }
762
+ if (auth.kind === "cookie-source") {
763
+ return {
764
+ arguments: [
765
+ ...forwarded,
766
+ "--cookie-source", auth.source,
767
+ ...(auth.profile === undefined ? [] : ["--cookie-profile", auth.profile]),
768
+ ],
769
+ auth,
770
+ };
771
+ }
772
+ if (auth.kind === "cookies-file") {
773
+ return {
774
+ arguments: [...forwarded, "--cookies-file", auth.path],
775
+ auth,
776
+ };
777
+ }
778
+ return {
779
+ arguments: [
780
+ ...forwarded,
781
+ "--browser-profile", auth.profile,
782
+ ...(auth.cookieSource === undefined ? [] : ["--cookie-source", auth.cookieSource]),
783
+ ...(auth.cookieProfile === undefined ? [] : ["--cookie-profile", auth.cookieProfile]),
784
+ ],
785
+ auth,
786
+ };
787
+ }
788
+
789
+ function prepareCapture(
790
+ resolved: ResolvedCapture,
791
+ environment: Readonly<Record<string, string | undefined>>,
792
+ ): PreparedCapture {
793
+ const auth = resolved.auth;
794
+ if (auth?.kind !== "browser-profile") {
795
+ return { arguments: resolved.arguments, runtimeOptions: {}, cleanup: () => undefined };
796
+ }
797
+ const sourceProfile = profilePath(auth.profile);
798
+ if (sourceProfile === null) {
799
+ return {
800
+ arguments: resolved.arguments,
801
+ runtimeOptions: auth.browserExecutable === undefined
802
+ ? {}
803
+ : { browserExecutable: auth.browserExecutable },
804
+ cleanup: () => undefined,
805
+ };
806
+ }
807
+ const privateDirectory = createBrowserSnapshotDirectory(environment);
808
+ try {
809
+ const cloned = cloneBrowserProfile(sourceProfile, privateDirectory.path);
810
+ const clonedArguments = [...resolved.arguments];
811
+ const profileIndex = clonedArguments.indexOf("--browser-profile");
812
+ if (profileIndex < 0 || clonedArguments[profileIndex + 1] !== auth.profile) {
813
+ throw new Error("resolved browser-profile auth does not match its capture arguments");
814
+ }
815
+ clonedArguments[profileIndex + 1] = cloned.userDataPath;
816
+ return {
817
+ arguments: clonedArguments,
818
+ runtimeOptions: {
819
+ ...(auth.browserExecutable === undefined
820
+ ? {}
821
+ : { browserExecutable: auth.browserExecutable }),
822
+ ownedBrowserProfile: {
823
+ path: cloned.userDataPath,
824
+ ...(cloned.profileDirectory === undefined ? {} : { profileDirectory: cloned.profileDirectory }),
825
+ },
826
+ },
827
+ cleanup: () => removeBrowserSnapshotDirectory(privateDirectory, environment),
828
+ };
829
+ } catch (error) {
830
+ removeBrowserSnapshotDirectory(privateDirectory, environment);
831
+ throw error;
832
+ }
833
+ }
834
+
835
+ async function runCaptureCommand(
836
+ prefix: readonly string[],
837
+ arguments_: readonly string[],
838
+ environment: Readonly<Record<string, string | undefined>>,
839
+ output: Output,
840
+ dependencies: GhostgetDependencies,
841
+ signal?: AbortSignal,
842
+ ): Promise<number> {
843
+ const resolved = resolveCaptureArgumentsWithAuth(arguments_, environment);
844
+ const unresolvedArguments = [...prefix, ...resolved.arguments];
845
+ const parsed = parseCaptureArguments(unresolvedArguments, environment);
846
+ if (!parsed.ok) {
847
+ return dependencies.clipMain(unresolvedArguments, environment, output);
848
+ }
849
+ if (
850
+ resolved.auth?.kind === "oauth-token-file"
851
+ && resolved.auth.provider === "gmail"
852
+ && (parsed.value.command === "capture" || parsed.value.command === "inspect")
853
+ ) {
854
+ const hasExplicitMedia = resolved.arguments.includes("--media");
855
+ const normalizedGmailArguments = parsed.value.media === "images" && !hasExplicitMedia
856
+ ? { ...parsed.value, media: "all" as const }
857
+ : parsed.value;
858
+ const hasExplicitOutput = resolved.arguments.includes("--output");
859
+ const persistentPrivateCapture = !normalizedGmailArguments.stdout && !hasExplicitOutput;
860
+ const gmailOptions = persistentPrivateCapture
861
+ ? {
862
+ ...normalizedGmailArguments,
863
+ outputBase: join(ghostgetStateHome(environment), "captures", "gmail"),
864
+ }
865
+ : normalizedGmailArguments;
866
+ if (persistentPrivateCapture) {
867
+ ensurePrivateStateDirectory(gmailOptions.outputBase, environment);
868
+ }
869
+ return dependencies.gmailCaptureMain(
870
+ gmailOptions,
871
+ resolved.auth,
872
+ environment,
873
+ output,
874
+ );
875
+ }
876
+ const prepared = prepareCapture(resolved, environment);
877
+ try {
878
+ return await dependencies.clipMain(
879
+ [...prefix, ...prepared.arguments],
880
+ environment,
881
+ output,
882
+ {
883
+ runCapture: (captureArguments) => dependencies.runCapture(
884
+ captureArguments,
885
+ environment,
886
+ signal === undefined ? {} : { signal },
887
+ ),
888
+ },
889
+ prepared.runtimeOptions,
890
+ );
891
+ } finally {
892
+ prepared.cleanup();
893
+ }
894
+ }
895
+
896
+ async function readStdinBounded(maxBytes: number): Promise<string> {
897
+ const reader = Bun.stdin.stream().getReader();
898
+ const output = new BoundedByteBuffer(maxBytes);
899
+ try {
900
+ for (;;) {
901
+ const next = await reader.read();
902
+ if (next.done) break;
903
+ if (!output.append(next.value)) throw new Error(`stdin exceeds ${maxBytes} bytes`);
904
+ }
905
+ } finally {
906
+ reader.releaseLock();
907
+ }
908
+ return new TextDecoder("utf-8", { fatal: true }).decode(output.toUint8Array());
909
+ }
910
+
911
+ async function readInput(source: string): Promise<unknown> {
912
+ const maximum = 1024 * 1024;
913
+ let text = source;
914
+ if (source === "-") text = await readStdinBounded(maximum);
915
+ else if (source.startsWith("@")) {
916
+ const path = resolve(source.slice(1));
917
+ text = readRegularFile(path, maximum, "input file");
918
+ } else if (Buffer.byteLength(source, "utf8") > maximum) throw new Error(`inline input exceeds ${maximum} bytes`);
919
+ return JSON.parse(text) as unknown;
920
+ }
921
+
922
+ async function readMessagingInput(source: string): Promise<unknown> {
923
+ const maximum = 1024 * 1024;
924
+ if (source === "-") {
925
+ return JSON.parse(await readStdinBounded(maximum)) as unknown;
926
+ }
927
+ if (!source.startsWith("@")) {
928
+ throw new Error("messaging input must arrive through stdin or one checked private file");
929
+ }
930
+ const path = source.slice(1);
931
+ const currentUid = process.getuid?.();
932
+ if (currentUid === undefined) {
933
+ throw new Error("messaging private-file ownership cannot be established");
934
+ }
935
+ const before = lstatSync(path, { bigint: true });
936
+ if (
937
+ !before.isFile()
938
+ || before.uid !== BigInt(currentUid)
939
+ || (before.mode & 0o077n) !== 0n
940
+ ) {
941
+ throw new Error("messaging input must be an owned regular file with no group or other access");
942
+ }
943
+ const text = readRegularFile(path, maximum, "messaging input file");
944
+ const after = lstatSync(path, { bigint: true });
945
+ if (
946
+ !after.isFile()
947
+ || after.uid !== before.uid
948
+ || after.dev !== before.dev
949
+ || after.ino !== before.ino
950
+ || after.size !== before.size
951
+ || after.mtimeNs !== before.mtimeNs
952
+ || after.ctimeNs !== before.ctimeNs
953
+ || after.mode !== before.mode
954
+ || (after.mode & 0o077n) !== 0n
955
+ ) throw new Error("messaging input file changed while it was being read");
956
+ return JSON.parse(text) as unknown;
957
+ }
958
+
959
+ const MAX_THREAD_TEXT_BYTES = 64 * 1024;
960
+
961
+ async function readTextInput(source: string): Promise<string> {
962
+ if (source === "-") return readStdinBounded(MAX_THREAD_TEXT_BYTES);
963
+ if (source.startsWith("@")) {
964
+ return readRegularFile(resolve(source.slice(1)), MAX_THREAD_TEXT_BYTES, "thread text file");
965
+ }
966
+ if (Buffer.byteLength(source, "utf8") > MAX_THREAD_TEXT_BYTES) {
967
+ throw new Error(`inline thread text exceeds ${MAX_THREAD_TEXT_BYTES} bytes`);
968
+ }
969
+ return source;
970
+ }
971
+
972
+ const PLATFORM_POLICY_NOTICE = "Catalog entries are reviewed policy only. adapter-eligible does not mean an adapter or capability is installed.";
973
+
974
+ function selectedPlatformSurfaces(surfaceId?: PlatformSurfaceId): readonly PlatformSurfaceCatalogEntry[] {
975
+ return surfaceId === undefined
976
+ ? platformSurfaceIds.map((id) => socialPlatformCatalog[id])
977
+ : [socialPlatformCatalog[surfaceId]];
978
+ }
979
+
980
+ function platformPolicyView(
981
+ surfaceId?: PlatformSurfaceId,
982
+ ): Record<string, unknown> {
983
+ return {
984
+ ok: true,
985
+ kind: "reviewed-platform-policy",
986
+ policyOnly: true,
987
+ installationStatus: "not-evaluated",
988
+ notice: PLATFORM_POLICY_NOTICE,
989
+ installedCapabilitiesCommand: "ghostget capabilities [adapter]",
990
+ surfaces: selectedPlatformSurfaces(surfaceId),
991
+ };
992
+ }
993
+
994
+ function renderPlatformPolicyText(
995
+ surfaceId?: PlatformSurfaceId,
996
+ ): string {
997
+ const lines = [
998
+ "Ghostget reviewed platform policy",
999
+ PLATFORM_POLICY_NOTICE,
1000
+ "Run 'ghostget capabilities [adapter]' to inspect installed capabilities.",
1001
+ ];
1002
+ for (const surface of selectedPlatformSurfaces(surfaceId)) {
1003
+ lines.push("", `${surface.displayName} (${surface.id})`, ` Exact origins: ${surface.originPolicy.exactOrigins.join(", ")}`);
1004
+ if (surface.originPolicy.additionalExactOrigins.state === "adapter-declared") {
1005
+ lines.push(` Additional origin: ${surface.originPolicy.additionalExactOrigins.note}`);
1006
+ }
1007
+ for (const state of ["adapter-eligible", "unsupported", "not-applicable", "R4"] as const) {
1008
+ const operations = semanticOperationNames.flatMap((name) => {
1009
+ const policy = surface.operations[name];
1010
+ if (policy.state !== state) return [];
1011
+ return [policy.state === "adapter-eligible" ? `${name} (${policy.risk})` : name];
1012
+ });
1013
+ lines.push(` ${state}: ${operations.length === 0 ? "none" : operations.join(", ")}`);
1014
+ }
1015
+ for (const name of compositionNames) {
1016
+ const composition = surface.compositions[name];
1017
+ if (composition === undefined) continue;
1018
+ const text = composition.text
1019
+ .map((field) => `${field.name}<=${field.safeMaxUnits} ${field.measurement}${field.required ? "" : " (optional)"}`)
1020
+ .join(", ");
1021
+ const attachment = composition.attachments.state === "none"
1022
+ ? "no attachments"
1023
+ : `${composition.attachments.minItems}-${composition.attachments.maxItems} ${composition.attachments.kinds.join("/")}`;
1024
+ lines.push(` ${name} composition: ${text}; ${attachment}`);
1025
+ }
1026
+ lines.push(
1027
+ ` Long-form read/publish: ${surface.longForm.read.state}/${surface.longForm.publish.state}`,
1028
+ ` Thread read/publish: ${surface.threads.read.state}/${surface.threads.publish.state}`,
1029
+ );
1030
+ }
1031
+ return `${lines.join("\n")}\n`;
1032
+ }
1033
+
1034
+ type RuntimeThreadPolicy = {
1035
+ readonly surface: PlatformSurfaceCatalogEntry;
1036
+ readonly maxWeightedLength: number;
1037
+ readonly maxItems: number;
1038
+ readonly measurement: keyof typeof textWeightPolicies;
1039
+ readonly operation: "threads.publish";
1040
+ readonly rootOperation: "posts.publish";
1041
+ readonly continuationOperation: "replies.create";
1042
+ };
1043
+
1044
+ function runtimeThreadPolicy(surfaceId: PlatformSurfaceId): RuntimeThreadPolicy {
1045
+ const surface: PlatformSurfaceCatalogEntry = socialPlatformCatalog[surfaceId];
1046
+ const publishing = surface.threads.publish;
1047
+ if (publishing.state !== "adapter-eligible") {
1048
+ throw new Error(`${surfaceId} native thread publishing is ${publishing.state}; no thread split policy is available`);
1049
+ }
1050
+ const rootBody = surface.compositions.post?.text.find((field) => field.name === "body");
1051
+ const replyBody = surface.compositions.reply?.text.find((field) => field.name === "body");
1052
+ if (rootBody === undefined || replyBody === undefined || rootBody.measurement !== replyBody.measurement) {
1053
+ throw new Error(`${surfaceId} has an inconsistent reviewed thread composition policy`);
1054
+ }
1055
+ return {
1056
+ surface,
1057
+ maxWeightedLength: Math.min(rootBody.safeMaxUnits, replyBody.safeMaxUnits),
1058
+ maxItems: publishing.safeMaxItems,
1059
+ measurement: rootBody.measurement,
1060
+ operation: publishing.operation,
1061
+ rootOperation: publishing.rootOperation,
1062
+ continuationOperation: publishing.continuationOperation,
1063
+ };
1064
+ }
1065
+
1066
+ async function runThreadSplit(
1067
+ arguments_: Extract<GhostgetArguments, { readonly command: "thread-split" }>,
1068
+ output: Output,
1069
+ ): Promise<number> {
1070
+ const { policy, text, weightPolicy, chunks } = await prepareThreadDraft(arguments_.surfaceId, arguments_.textSource);
1071
+ const view = {
1072
+ ok: true,
1073
+ kind: "local-thread-split",
1074
+ published: false,
1075
+ localSplitRequiresInstalledCapability: false,
1076
+ publicationInstallationStatus: "not-evaluated",
1077
+ notice: "This command only split local text. Publishing still requires a separately installed reviewed adapter capability.",
1078
+ surfaceId: arguments_.surfaceId,
1079
+ exactOrigins: policy.surface.originPolicy.exactOrigins,
1080
+ operation: policy.operation,
1081
+ rootOperation: policy.rootOperation,
1082
+ continuationOperation: policy.continuationOperation,
1083
+ measurement: policy.measurement,
1084
+ maxWeightedLength: policy.maxWeightedLength,
1085
+ maxItems: policy.maxItems,
1086
+ inputBytes: Buffer.byteLength(text, "utf8"),
1087
+ inputWeightedLength: weightedTextLength(text, weightPolicy),
1088
+ exactRoundTrip: true,
1089
+ chunks: chunks.map((chunk, index) => ({ index: index + 1, ...chunk })),
1090
+ };
1091
+ if (arguments_.json) {
1092
+ output.stdout(exactTerminalJson(view));
1093
+ } else {
1094
+ output.stdout(`Local ${safe(arguments_.surfaceId)} thread split: ${chunks.length} item(s); nothing was published.\n`);
1095
+ output.stdout("Publishing still requires a separately installed reviewed adapter capability.\n");
1096
+ output.stdout(`Each following chunk is an exact JSON string (${safe(policy.measurement)}, max ${policy.maxWeightedLength}).\n`);
1097
+ for (const [index, chunk] of chunks.entries()) {
1098
+ output.stdout(`${index + 1}/${chunks.length} · ${chunk.weightedLength}\n`);
1099
+ output.stdout(exactTerminalJson(chunk.text));
1100
+ }
1101
+ }
1102
+ return 0;
1103
+ }
1104
+
1105
+ async function prepareThreadDraft(surfaceId: PlatformSurfaceId, textSource: string): Promise<{
1106
+ readonly policy: RuntimeThreadPolicy;
1107
+ readonly text: string;
1108
+ readonly weightPolicy: (typeof textWeightPolicies)[keyof typeof textWeightPolicies];
1109
+ readonly chunks: readonly { readonly text: string; readonly weightedLength: number }[];
1110
+ }> {
1111
+ const policy = runtimeThreadPolicy(surfaceId);
1112
+ const text = await readTextInput(textSource);
1113
+ if (text.length === 0) throw new Error("thread text must not be empty");
1114
+ const weightPolicy = textWeightPolicies[policy.measurement];
1115
+ const result = splitWeightedThread(text, {
1116
+ maxWeightedLength: policy.maxWeightedLength,
1117
+ maxItems: policy.maxItems,
1118
+ weightPolicy,
1119
+ });
1120
+ if (!result.ok) {
1121
+ if (result.reason === "too-many-items") {
1122
+ throw new Error(`thread text requires more than the reviewed ${result.maxItems}-item limit for ${surfaceId}`);
1123
+ }
1124
+ if (result.reason === "unit-too-large") {
1125
+ throw new Error(`one Unicode grapheme or URL weighs ${result.unitWeight}, above the ${policy.maxWeightedLength}-unit per-item limit`);
1126
+ }
1127
+ if (result.reason === "invalid-unicode") throw new Error("thread text must contain well-formed Unicode");
1128
+ if (result.reason === "invalid-weight-policy") throw new Error(`invalid reviewed thread weight policy: ${result.issue}`);
1129
+ throw new Error("invalid reviewed thread split bounds");
1130
+ }
1131
+ const joined = result.chunks.map((chunk) => chunk.text).join("");
1132
+ if (joined !== text) throw new Error("thread splitter did not preserve the exact input");
1133
+ return { policy, text, weightPolicy, chunks: result.chunks };
1134
+ }
1135
+
1136
+ async function runThreadPublish(
1137
+ arguments_: Extract<GhostgetArguments, { readonly command: "thread-publish" }>,
1138
+ environment: Readonly<Record<string, string | undefined>>,
1139
+ output: Output,
1140
+ registry: ProviderPluginRegistry = providerPluginRegistry,
1141
+ ): Promise<number> {
1142
+ const draft = await prepareThreadDraft(arguments_.surfaceId, arguments_.textSource);
1143
+ const invocation = prepareInvocation(
1144
+ arguments_.adapterId,
1145
+ draft.policy.operation,
1146
+ { items: draft.chunks.map((chunk) => chunk.text) },
1147
+ arguments_.authId,
1148
+ environment,
1149
+ registry,
1150
+ );
1151
+ if (invocation.manifest.surfaceId !== arguments_.surfaceId) {
1152
+ throw new Error(`adapter ${arguments_.adapterId} is not bound to ${arguments_.surfaceId}`);
1153
+ }
1154
+ const operation = invocation.manifest.operations[draft.policy.operation];
1155
+ if (operation?.risk !== "R3") throw new Error("reviewed thread publishing must be an R3 capability");
1156
+ const stored = createAndSaveInvocationPlan(
1157
+ invocation,
1158
+ environment,
1159
+ new Date(),
1160
+ registry,
1161
+ );
1162
+ printPreview(output, {
1163
+ ...previewView(stored, invocation, arguments_.headed),
1164
+ thread: {
1165
+ surfaceId: arguments_.surfaceId,
1166
+ items: draft.chunks.length,
1167
+ measurement: draft.policy.measurement,
1168
+ maxWeightedLength: draft.policy.maxWeightedLength,
1169
+ exactRoundTrip: draft.chunks.map((chunk) => chunk.text).join("") === draft.text,
1170
+ },
1171
+ }, arguments_.json);
1172
+ return arguments_.preview ? 0 : 4;
1173
+ }
1174
+
1175
+ function manifestFromPlatform(adapterId: string, surfaceId: PlatformSurfaceId): ReturnType<typeof emptyManifest> {
1176
+ const surface = socialPlatformCatalog[surfaceId];
1177
+ const primaryOrigin = surface.originPolicy.exactOrigins[0];
1178
+ if (primaryOrigin === undefined) throw new Error(`${surfaceId} has no reviewed exact origin`);
1179
+ const browserDomains = [...new Set(surface.originPolicy.exactOrigins.map((origin) => new URL(origin).hostname.toLowerCase()))];
1180
+ const manifest = emptyManifest(adapterId, primaryOrigin, browserDomains, surfaceId);
1181
+ return {
1182
+ ...manifest,
1183
+ displayName: surface.displayName,
1184
+ origins: surface.originPolicy.exactOrigins,
1185
+ browserDomains,
1186
+ };
1187
+ }
1188
+
1189
+ function installedOperationTransport(
1190
+ operation: GhostgetOperation,
1191
+ ): "provider-api" | "web-session-api" | "local-cli" | "reviewed-template-api" {
1192
+ if (isProviderOperation(operation)) return "provider-api";
1193
+ if (isWebSessionOperation(operation)) return "web-session-api";
1194
+ if (isReviewedTemplateOperation(operation)) return "reviewed-template-api";
1195
+ if (isLocalCliOperation(operation)) return "local-cli";
1196
+ throw new Error(DOM_ACTION_TRANSPORT_DISABLED_MESSAGE);
1197
+ }
1198
+
1199
+ function capabilitySummary(
1200
+ environment: Readonly<Record<string, string | undefined>>,
1201
+ registry: ProviderPluginRegistry,
1202
+ ): readonly unknown[] {
1203
+ return listRuntimeManifests(environment, registry).map(({ id, result }) => result.ok
1204
+ ? {
1205
+ id,
1206
+ version: result.value.version,
1207
+ displayName: result.value.displayName,
1208
+ surfaceId: result.value.surfaceId ?? null,
1209
+ origins: result.value.origins,
1210
+ manifestHash: manifestHash(result.value),
1211
+ operations: Object.entries(result.value.operations).map(([operationId, operation]) => {
1212
+ const provider = isProviderOperation(operation)
1213
+ ? getProviderContract(operation.provider, registry)
1214
+ : null;
1215
+ const webSession = isWebSessionOperation(operation)
1216
+ ? getWebSessionContract(operation.webSession, registry)
1217
+ : null;
1218
+ const localCli = isLocalCliOperation(operation)
1219
+ ? getLocalCliContract(operation.localCli, registry)
1220
+ : null;
1221
+ const reviewedTemplate = isReviewedTemplateOperation(operation) ? operation.reviewedTemplate : null;
1222
+ return {
1223
+ id: operationId,
1224
+ description: operation.description,
1225
+ risk: operation.risk,
1226
+ sideEffect: operation.sideEffect,
1227
+ idempotency: operation.idempotency,
1228
+ dedupeWindowMs: operation.dedupeWindowMs,
1229
+ transport: installedOperationTransport(operation),
1230
+ input: operation.input,
1231
+ ...(provider === null ? {} : {
1232
+ provider: provider.provider,
1233
+ providerAction: provider.operation,
1234
+ providerContractVersion: provider.contractVersion,
1235
+ providerContractHash: providerContractHash(provider, registry),
1236
+ requiredScopeSets: provider.requiredScopeSets,
1237
+ coverage: provider.coverage,
1238
+ implementation: provider.implementation,
1239
+ }),
1240
+ ...(webSession === null ? {} : {
1241
+ site: webSession.site,
1242
+ webSessionAction: webSession.operation,
1243
+ webSessionContractVersion: webSession.contractVersion,
1244
+ webSessionContractHash: webSessionContractHash(
1245
+ webSession,
1246
+ registry,
1247
+ ),
1248
+ state: webSession.state,
1249
+ implementation: webSession.implementation,
1250
+ }),
1251
+ ...(localCli === null ? {} : {
1252
+ surface: localCli.surface,
1253
+ localCliAction: localCli.operation,
1254
+ localCliContractVersion: localCli.contractVersion,
1255
+ localCliContractHash: localCliContractHash(localCli, registry),
1256
+ localCliTool: localCli.tool,
1257
+ state: localCli.state,
1258
+ implementation: localCli.implementation,
1259
+ }),
1260
+ ...(reviewedTemplate === null ? {} : {
1261
+ state: reviewedTemplate.state,
1262
+ reviewedTemplateContractVersion: reviewedTemplate.contractVersion,
1263
+ reviewedTemplateContractHash: reviewedTemplateHash(reviewedTemplate),
1264
+ ...(reviewedTemplate.state === "capture-required"
1265
+ ? { instructions: reviewedTemplate.instructions }
1266
+ : {
1267
+ reviewedAt: reviewedTemplate.reviewedAt,
1268
+ evidenceSha256: reviewedTemplate.evidenceSha256,
1269
+ origin: reviewedTemplate.template.origin,
1270
+ }),
1271
+ }),
1272
+ };
1273
+ }),
1274
+ }
1275
+ : { id, invalid: true, issues: result.issues });
1276
+ }
1277
+
1278
+ function listRuntimeManifests(
1279
+ environment: Readonly<Record<string, string | undefined>>,
1280
+ registry: ProviderPluginRegistry,
1281
+ ): ReturnType<typeof listInstalledManifests> {
1282
+ const stored = listInstalledManifests(environment, registry);
1283
+ const storedIds = new Set(stored.map((entry) => entry.id));
1284
+ const owned = registry.listOwnedManifests();
1285
+ const collision = owned.find((manifest) => storedIds.has(manifest.id));
1286
+ if (collision !== undefined) {
1287
+ throw new Error(
1288
+ `adapter ${collision.id} collides with an enabled portable provider plugin`,
1289
+ );
1290
+ }
1291
+ return Object.freeze([
1292
+ ...stored,
1293
+ ...owned.map((manifest) => Object.freeze({
1294
+ id: manifest.id,
1295
+ result: Object.freeze({ ok: true as const, value: manifest }),
1296
+ })),
1297
+ ].sort((left, right) => left.id.localeCompare(right.id)));
1298
+ }
1299
+
1300
+ function officialProviderReadiness(
1301
+ environment: Readonly<Record<string, string | undefined>>,
1302
+ registry: ProviderPluginRegistry,
1303
+ ): readonly {
1304
+ readonly provider: string;
1305
+ readonly adapters: readonly string[];
1306
+ readonly auth: readonly { readonly id: string; readonly tokenReady: boolean; readonly usableOperations: number }[];
1307
+ readonly ready: boolean;
1308
+ }[] {
1309
+ const manifests = listRuntimeManifests(environment, registry)
1310
+ .filter((entry) => entry.result.ok)
1311
+ .map((entry) => ({ id: entry.id, manifest: entry.result.ok ? entry.result.value : null }))
1312
+ .filter((entry): entry is { readonly id: string; readonly manifest: NonNullable<typeof entry.manifest> } => entry.manifest !== null);
1313
+ const auth = listAuth(environment).filter((entry) => entry.kind === "oauth-token-file");
1314
+ const bindings = registry.list().flatMap((plugin) =>
1315
+ plugin.bindings.filter((binding) => binding.transport === "provider-api"));
1316
+ return bindings.map((binding) => {
1317
+ const provider = binding.surfaceId;
1318
+ const adapters = manifests.filter((entry) => Object.values(entry.manifest.operations).some((operation) =>
1319
+ isProviderOperation(operation) && operation.provider.provider === provider));
1320
+ const operations = adapters.flatMap((entry) => Object.values(entry.manifest.operations)
1321
+ .filter(isProviderOperation)
1322
+ .filter((operation) => operation.provider.provider === provider)
1323
+ .flatMap((operation) => {
1324
+ const resolution = registry.resolveOperationDefinition(
1325
+ "provider-api",
1326
+ provider,
1327
+ operation.provider.action,
1328
+ operation.provider.contractVersion,
1329
+ );
1330
+ return resolution === undefined ? [] : [resolution.operation];
1331
+ }));
1332
+ const locators = auth.filter((entry) => entry.provider === provider).map((entry) => {
1333
+ let tokenReady = false;
1334
+ let usableOperations = 0;
1335
+ try {
1336
+ loadOAuthToken(entry);
1337
+ tokenReady = true;
1338
+ usableOperations = operations.filter((operation) => {
1339
+ try {
1340
+ if (!("requiredScopeSets" in operation)) return false;
1341
+ requireOAuthScopes(entry, operation.requiredScopeSets);
1342
+ return true;
1343
+ } catch {
1344
+ return false;
1345
+ }
1346
+ }).length;
1347
+ } catch {
1348
+ // Doctor reports only readiness metadata and never token contents or paths.
1349
+ }
1350
+ return { id: entry.id, tokenReady, usableOperations };
1351
+ });
1352
+ return {
1353
+ provider,
1354
+ adapters: adapters.map((entry) => entry.id),
1355
+ auth: locators,
1356
+ ready: adapters.length > 0 && locators.some((entry) => entry.tokenReady && entry.usableOperations > 0),
1357
+ };
1358
+ });
1359
+ }
1360
+
1361
+ type WebSessionReadiness = {
1362
+ readonly site: string;
1363
+ readonly adapters: readonly string[];
1364
+ readonly observedOperations: readonly string[];
1365
+ readonly captureRequiredOperations: readonly string[];
1366
+ readonly accountBoundAuth: readonly string[];
1367
+ readonly ready: boolean;
1368
+ };
1369
+
1370
+ function authenticatedWebSessionReadiness(
1371
+ environment: Readonly<Record<string, string | undefined>>,
1372
+ registry: ProviderPluginRegistry,
1373
+ ): readonly WebSessionReadiness[] {
1374
+ const manifests = listRuntimeManifests(environment, registry);
1375
+ const auth = listAuth(environment).filter((entry) =>
1376
+ isCookieCapableWebAuth(entry) || entry.kind === "linked-device-store"
1377
+ );
1378
+ const bindings = registry.list().flatMap((plugin) =>
1379
+ plugin.bindings.filter((binding) =>
1380
+ binding.transport === "web-session-api"
1381
+ || binding.transport === "linked-device"));
1382
+ return bindings.map((binding) => {
1383
+ const site = binding.surfaceId;
1384
+ const operations = manifests.flatMap((entry) => {
1385
+ if (!entry.result.ok || entry.result.value.schemaVersion !== 4) return [];
1386
+ return Object.entries(entry.result.value.operations).flatMap(([operationId, operation]) => {
1387
+ if (!isWebSessionOperation(operation) || operation.webSession.site !== site) return [];
1388
+ const contract = getWebSessionContract(operation.webSession, registry);
1389
+ return [{ adapter: entry.id, operation: operationId, state: contract.state }];
1390
+ });
1391
+ });
1392
+ const observed = operations.filter((entry) => entry.state === "observed");
1393
+ const captureRequired = operations.filter((entry) => entry.state === "capture-required");
1394
+ const accountBoundAuth = auth
1395
+ .filter((entry) => {
1396
+ try {
1397
+ requireProviderPluginAuth(binding, entry);
1398
+ return true;
1399
+ } catch {
1400
+ return false;
1401
+ }
1402
+ })
1403
+ .filter((entry) =>
1404
+ entry.subject !== undefined && binding.subject.matches(entry.subject))
1405
+ .map((entry) => entry.id)
1406
+ .sort();
1407
+ const adapters = [...new Set(observed.map((entry) => entry.adapter))].sort();
1408
+ const observedOperations = [...new Set(observed.map((entry) => entry.operation))].sort();
1409
+ const captureRequiredOperations = [...new Set(
1410
+ captureRequired.map((entry) => entry.operation),
1411
+ )].sort();
1412
+ return {
1413
+ site,
1414
+ adapters,
1415
+ observedOperations,
1416
+ captureRequiredOperations,
1417
+ accountBoundAuth,
1418
+ ready: adapters.length > 0 && accountBoundAuth.length > 0,
1419
+ };
1420
+ });
1421
+ }
1422
+
1423
+ type LocalCliReadiness = {
1424
+ readonly surface: string;
1425
+ readonly adapters: readonly string[];
1426
+ readonly operations: readonly string[];
1427
+ readonly accountBoundAuth: readonly string[];
1428
+ readonly tool: LocalCliPluginBindingV1["tool"];
1429
+ readonly runtime: Awaited<ReturnType<LocalCliPluginBindingV1["inspect"]>>;
1430
+ readonly ready: boolean;
1431
+ };
1432
+
1433
+ async function localCliReadiness(
1434
+ environment: Readonly<Record<string, string | undefined>>,
1435
+ registry: ProviderPluginRegistry,
1436
+ ): Promise<readonly LocalCliReadiness[]> {
1437
+ const manifests = listRuntimeManifests(environment, registry);
1438
+ const auth = listAuth(environment);
1439
+ const bindings = registry.list().flatMap((plugin) =>
1440
+ plugin.bindings.filter(
1441
+ (binding): binding is LocalCliPluginBindingV1 =>
1442
+ binding.transport === "local-cli",
1443
+ ));
1444
+ return Promise.all(bindings.map(async (binding) => {
1445
+ const installed = manifests.flatMap((entry) => {
1446
+ if (!entry.result.ok || entry.result.value.schemaVersion !== 6) return [];
1447
+ return Object.entries(entry.result.value.operations).flatMap(
1448
+ ([operationId, operation]) =>
1449
+ isLocalCliOperation(operation)
1450
+ && operation.localCli.surface === binding.surfaceId
1451
+ ? [{ adapter: entry.id, operation: operationId }]
1452
+ : [],
1453
+ );
1454
+ });
1455
+ const accountBoundAuth = auth.filter((entry) => {
1456
+ try {
1457
+ requireProviderPluginAuth(binding, entry);
1458
+ return entry.subject !== undefined
1459
+ && binding.subject.matches(entry.subject);
1460
+ } catch {
1461
+ return false;
1462
+ }
1463
+ }).map((entry) => entry.id).sort();
1464
+ let runtime: Awaited<ReturnType<LocalCliPluginBindingV1["inspect"]>>;
1465
+ try {
1466
+ runtime = await withLocalCliProviderCleanupAdmission(
1467
+ {
1468
+ registry,
1469
+ binding,
1470
+ auth: null,
1471
+ purpose: { kind: "inspect" },
1472
+ environment,
1473
+ },
1474
+ (registerCleanupBarrier) => binding.inspect(environment, {
1475
+ registerCleanupBarrier,
1476
+ }),
1477
+ );
1478
+ } catch {
1479
+ runtime = Object.freeze({
1480
+ ready: false,
1481
+ platform: process.platform,
1482
+ arch: process.arch,
1483
+ version: null,
1484
+ executableSha256: null,
1485
+ reason: "inspection-failed",
1486
+ });
1487
+ }
1488
+ const adapters = [...new Set(installed.map((entry) => entry.adapter))]
1489
+ .sort();
1490
+ const operations = [...new Set(installed.map((entry) => entry.operation))]
1491
+ .sort();
1492
+ return Object.freeze({
1493
+ surface: binding.surfaceId,
1494
+ adapters,
1495
+ operations,
1496
+ accountBoundAuth,
1497
+ tool: binding.tool,
1498
+ runtime,
1499
+ ready: runtime.ready
1500
+ && adapters.length > 0
1501
+ && accountBoundAuth.length > 0,
1502
+ });
1503
+ }));
1504
+ }
1505
+
1506
+ function reviewedTemplateReservationStatus(
1507
+ environment: Readonly<Record<string, string | undefined>>,
1508
+ registry: ProviderPluginRegistry,
1509
+ ): {
1510
+ readonly mode: "derivation-reservation-only";
1511
+ readonly adapters: readonly string[];
1512
+ readonly operations: readonly string[];
1513
+ readonly executable: false;
1514
+ readonly requiredContract: "reviewed-template-v2-current-account-preflight";
1515
+ } {
1516
+ const reservations = listRuntimeManifests(environment, registry).flatMap((entry) => {
1517
+ if (!entry.result.ok || entry.result.value.schemaVersion !== 5) return [];
1518
+ return Object.entries(entry.result.value.operations).flatMap(([operationId, operation]) =>
1519
+ isReviewedTemplateOperation(operation)
1520
+ && operation.reviewedTemplate.state === "capture-required"
1521
+ ? [{ adapter: entry.id, operation: operationId }]
1522
+ : []);
1523
+ });
1524
+ const adapters = [...new Set(reservations.map((entry) => entry.adapter))].sort();
1525
+ const operations = [...new Set(reservations.map((entry) => `${entry.adapter}/${entry.operation}`))].sort();
1526
+ return {
1527
+ mode: "derivation-reservation-only",
1528
+ adapters,
1529
+ operations,
1530
+ executable: false,
1531
+ requiredContract: "reviewed-template-v2-current-account-preflight",
1532
+ };
1533
+ }
1534
+
1535
+ async function doctor(
1536
+ arguments_: Extract<GhostgetArguments, { readonly command: "doctor" }>,
1537
+ environment: Readonly<Record<string, string | undefined>>,
1538
+ output: Output,
1539
+ dependencies: GhostgetDependencies,
1540
+ ): Promise<number> {
1541
+ const registry = dependencies.providerPluginRegistry;
1542
+ const confirmationClaimRecovery =
1543
+ repairInterruptedConfirmationClaims(environment);
1544
+ const runJournalRecovery = repairInterruptedRunJournals(environment);
1545
+ const webSessionCleanupAdmissionRecovery =
1546
+ await recoverWebSessionCleanupAdmissions(environment);
1547
+ const linkedDeviceLifecycleAdmissionRecovery =
1548
+ recoverLinkedDeviceLifecycleAdmissions(environment);
1549
+ const linkedDeviceLifecycleRecovery =
1550
+ recoverLinkedDeviceLifecycleJournals({ environment });
1551
+ const linkedDeviceBindings = registry.list().flatMap((plugin) =>
1552
+ plugin.bindings.filter(
1553
+ (binding): binding is LinkedDevicePluginBindingV1 =>
1554
+ binding.transport === "linked-device"
1555
+ && binding.linkedDeviceLifecycle !== undefined,
1556
+ ));
1557
+ const [
1558
+ captureInspection,
1559
+ mediaInspection,
1560
+ linkedDeviceProtocols,
1561
+ localCliProviders,
1562
+ ] = await Promise.all([
1563
+ dependencies.inspectClipEnvironment(),
1564
+ dependencies.loadMediaRuntime().then(async (runtime) => ({
1565
+ runtime,
1566
+ report: await runtime.runDoctor({ env: environment }),
1567
+ })),
1568
+ Promise.all(linkedDeviceBindings.map(async (binding) => {
1569
+ try {
1570
+ return {
1571
+ surface: binding.surfaceId,
1572
+ ...(await binding.linkedDeviceLifecycle!.inspect(environment)),
1573
+ };
1574
+ } catch {
1575
+ return {
1576
+ surface: binding.surfaceId,
1577
+ ready: false,
1578
+ implementation: "unavailable",
1579
+ version: "unknown",
1580
+ integrity: "inspection-failed",
1581
+ };
1582
+ }
1583
+ })),
1584
+ localCliReadiness(environment, registry),
1585
+ ]);
1586
+ const capture = captureInspection.report;
1587
+ const media = mediaInspection.report;
1588
+ const portablePlugins = doctorPortableProviderPlugins(environment);
1589
+ const expiredPlansRemoved = purgeExpiredPlans(environment);
1590
+ const orphanedBrowserSnapshotsRemoved = purgeOrphanedBrowserSnapshots(environment);
1591
+ const adapters = capabilitySummary(environment, registry);
1592
+ const auth = listAuth(environment).map((entry) => ({ id: entry.id, kind: entry.kind }));
1593
+ const officialProviders = officialProviderReadiness(environment, registry);
1594
+ const derivations = listDerivations(environment);
1595
+ const unsettledRuns = listRunReceipts(environment).filter((receipt) =>
1596
+ "status" in receipt && (receipt.status === "pending" || receipt.status === "partial" || receipt.status === "indeterminate"));
1597
+ const browserCaptureBootstrapReady =
1598
+ captureInspection.browserCaptureBootstrapReady;
1599
+ const providerApiReady = officialProviders.some((provider) => provider.ready);
1600
+ const webSessionSites = authenticatedWebSessionReadiness(environment, registry);
1601
+ const webSessionApiReady = webSessionSites.some((site) => site.ready);
1602
+ const localCliReady = localCliProviders.some((provider) => provider.ready);
1603
+ const webSessionAdapters = [...new Set(webSessionSites.flatMap((site) => site.adapters))].sort();
1604
+ const reviewedTemplateReservations = reviewedTemplateReservationStatus(environment, registry);
1605
+ const reviewedTemplateApiReady = false;
1606
+ const actionReady = providerApiReady || webSessionApiReady || localCliReady;
1607
+ const runRecoveryHealthy = confirmationClaimRecovery.invalid === 0
1608
+ && runJournalRecovery.issues.length === 0;
1609
+ const webSessionCleanupRecoveryHealthy =
1610
+ webSessionCleanupAdmissionRecovery.issues.length === 0;
1611
+ const linkedDeviceLifecycleRecoveryHealthy =
1612
+ linkedDeviceLifecycleRecovery.issues.length === 0
1613
+ && linkedDeviceLifecycleAdmissionRecovery.issues.length === 0;
1614
+ const recoveryHealthy = runRecoveryHealthy
1615
+ && webSessionCleanupRecoveryHealthy
1616
+ && linkedDeviceLifecycleRecoveryHealthy;
1617
+ const ghostgetReport = {
1618
+ home: ghostgetStateHome(environment),
1619
+ mediaArchiveReady: media.ok,
1620
+ installedAdapters: adapters.length,
1621
+ configuredAuth: auth,
1622
+ browserCaptureBootstrapReady,
1623
+ browserActionReady: false,
1624
+ providerApiReady,
1625
+ webSessionApiReady,
1626
+ webSessionAdapters,
1627
+ webSessionSites,
1628
+ localCliReady,
1629
+ localCliProviders,
1630
+ reviewedTemplateApiReady,
1631
+ reviewedTemplateReservations,
1632
+ officialProviders,
1633
+ linkedDeviceProtocols,
1634
+ // Compatibility field retained while callers move to the generic list.
1635
+ whatsappProtocol: linkedDeviceProtocols.find(
1636
+ (entry) => entry.surface === "whatsapp",
1637
+ ) ?? null,
1638
+ activeDerivations: derivations,
1639
+ unsettledRuns,
1640
+ confirmationClaimRecovery,
1641
+ runJournalRecovery,
1642
+ webSessionCleanupAdmissionRecovery,
1643
+ linkedDeviceLifecycleRecovery,
1644
+ linkedDeviceLifecycleAdmissionRecovery,
1645
+ portablePlugins,
1646
+ mutationPolicy: "R2/R3 preview+digest confirmation; R4 blocked",
1647
+ rawHarRetention: "private-until-finish-or-discard",
1648
+ expiredPlansRemoved,
1649
+ orphanedBrowserSnapshotsRemoved,
1650
+ };
1651
+ const report = {
1652
+ ok: actionReady && recoveryHealthy && portablePlugins.ok,
1653
+ capture,
1654
+ media,
1655
+ ghostget: ghostgetReport,
1656
+ wrench: ghostgetReport,
1657
+ // Frozen doctor JSON compatibility aliases. Predecessor consumers received
1658
+ // this report under `wrench` and `oh`; all envelopes remain exact.
1659
+ oh: ghostgetReport,
1660
+ };
1661
+ if (arguments_.json) output.stdout(safeJson(report));
1662
+ else {
1663
+ output.stdout(sanitizeTerminalText(captureInspection.renderReport()));
1664
+ output.stdout("\nGhostget verified media archive\n");
1665
+ output.stdout(sanitizeTerminalText(mediaInspection.runtime.renderDoctorReport(media)));
1666
+ output.stdout("\nGhostget actions\n");
1667
+ output.stdout(`- Home: ${safe(report.ghostget.home)}\n`);
1668
+ output.stdout(`- Installed adapters: ${adapters.length}\n`);
1669
+ output.stdout(
1670
+ `- Portable plugins: ${portablePlugins.ok ? "healthy" : "attention required"}`
1671
+ + ` (${portablePlugins.installed} installed)\n`,
1672
+ );
1673
+ output.stdout(`- Auth locators: ${auth.length}\n`);
1674
+ output.stdout(
1675
+ `- Durable run recovery: ${runRecoveryHealthy ? "healthy" : "attention required"}`
1676
+ + ` (${runJournalRecovery.repaired} repaired, ${runJournalRecovery.issues.length} unresolved)\n`,
1677
+ );
1678
+ output.stdout(
1679
+ `- Provider resource cleanup recovery: ${webSessionCleanupRecoveryHealthy ? "healthy" : "attention required"}`
1680
+ + ` (${webSessionCleanupAdmissionRecovery.repaired} repaired, ${webSessionCleanupAdmissionRecovery.issues.length} unresolved)\n`,
1681
+ );
1682
+ output.stdout(
1683
+ `- Linked-device lifecycle recovery: ${linkedDeviceLifecycleRecoveryHealthy ? "healthy" : "attention required"}`
1684
+ + ` (${linkedDeviceLifecycleRecovery.repairedSafeRetry + linkedDeviceLifecycleRecovery.repairedIndeterminate + linkedDeviceLifecycleAdmissionRecovery.repaired} repaired, ${linkedDeviceLifecycleRecovery.issues.length + linkedDeviceLifecycleAdmissionRecovery.issues.length} unresolved)\n`,
1685
+ );
1686
+ output.stdout(`- Browser capture/bootstrap: ${browserCaptureBootstrapReady ? "ready" : "not ready"}\n`);
1687
+ output.stdout("- Generic browser/DOM action execution: disabled (internal API contracts only)\n");
1688
+ output.stdout(`- Official provider plugin APIs: ${providerApiReady ? "ready" : "not ready"}\n`);
1689
+ output.stdout(`- Authenticated internal APIs (any observed contract + account binding): ${webSessionApiReady ? "ready" : "not ready"}\n`);
1690
+ output.stdout(`- Local CLI providers: ${localCliReady ? "ready" : "not ready"}\n`);
1691
+ for (const readiness of localCliProviders) {
1692
+ output.stdout(
1693
+ `- ${safe(readiness.surface)} local CLI: ${readiness.ready ? "ready" : safe(readiness.runtime.reason ?? "not ready")}`
1694
+ + ` (${readiness.operations.length} operation${readiness.operations.length === 1 ? "" : "s"})\n`,
1695
+ );
1696
+ }
1697
+ for (const readiness of webSessionSites) {
1698
+ const state = readiness.ready
1699
+ ? "ready"
1700
+ : readiness.observedOperations.length > 0
1701
+ ? "account binding required"
1702
+ : readiness.captureRequiredOperations.length > 0
1703
+ ? "capture-required"
1704
+ : "not installed";
1705
+ output.stdout(
1706
+ `- ${safe(readiness.site)} authenticated internal API: ${state}`
1707
+ + ` (${readiness.observedOperations.length} observed, ${readiness.captureRequiredOperations.length} capture-required)\n`,
1708
+ );
1709
+ }
1710
+ for (const protocol of linkedDeviceProtocols) {
1711
+ output.stdout(
1712
+ `- ${safe(protocol.surface)} linked-device protocol ${safe(protocol.version)}: ${protocol.ready ? "ready" : "not installed"}\n`,
1713
+ );
1714
+ if (!protocol.ready && protocol.setupCommand !== undefined) {
1715
+ output.stdout(` Setup: ${safe(protocol.setupCommand)}\n`);
1716
+ }
1717
+ }
1718
+ output.stdout(`- Generic internal-API derivation: reservation-only (schema-v5 templates are inert until account preflight)\n`);
1719
+ output.stdout(`- Generic internal-API execution: not available (reviewed-template v2 account preflight required)\n`);
1720
+ output.stdout(`- Active derivations: ${derivations.length}${derivations.length === 0 ? "" : " (run 'ghostget derive list')"}\n`);
1721
+ output.stdout(`- Unsettled runs: ${unsettledRuns.length}${unsettledRuns.length === 0 ? "" : " (run 'ghostget runs list')"}\n`);
1722
+ output.stdout(`- Writes: ${report.ghostget.mutationPolicy}\n`);
1723
+ output.stdout(`- Raw HAR retention: ${report.ghostget.rawHarRetention}\n`);
1724
+ }
1725
+ return report.ok ? 0 : 3;
1726
+ }
1727
+
1728
+ function confirmationContractView(plan: ReturnType<typeof createInvocationPlan>["plan"]): Record<string, unknown> {
1729
+ if (plan.transport === "portable-provider-plugin") {
1730
+ const identity = plan.portablePluginContract;
1731
+ return {
1732
+ transport: plan.transport,
1733
+ identity: `${identity.pluginId}/${identity.adapterId}/${identity.operation}@${identity.contractVersion}`,
1734
+ pluginId: identity.pluginId,
1735
+ pluginVersion: identity.pluginVersion,
1736
+ hostApiVersion: identity.hostApiVersion,
1737
+ bundleSha256: identity.bundleSha256,
1738
+ manifestSha256: identity.manifestSha256,
1739
+ adapterId: identity.adapterId,
1740
+ pluginTransport: identity.transport,
1741
+ surfaceId: identity.surfaceId,
1742
+ operation: identity.operation,
1743
+ version: identity.contractVersion,
1744
+ descriptorSha256: identity.descriptorSha256,
1745
+ };
1746
+ }
1747
+ if (plan.transport === "provider-api") {
1748
+ return {
1749
+ transport: plan.transport,
1750
+ identity: `${plan.providerContract.provider}/${plan.providerContract.action}@${plan.providerContract.version}`,
1751
+ provider: plan.providerContract.provider,
1752
+ action: plan.providerContract.action,
1753
+ version: plan.providerContract.version,
1754
+ hash: plan.providerContract.hash,
1755
+ };
1756
+ }
1757
+ if (plan.transport === "web-session-api") {
1758
+ return {
1759
+ transport: plan.transport,
1760
+ identity: `${plan.webSessionContract.site}/${plan.webSessionContract.action}@${plan.webSessionContract.version}`,
1761
+ site: plan.webSessionContract.site,
1762
+ action: plan.webSessionContract.action,
1763
+ version: plan.webSessionContract.version,
1764
+ hash: plan.webSessionContract.hash,
1765
+ };
1766
+ }
1767
+ if (plan.transport === "local-cli") {
1768
+ return {
1769
+ transport: plan.transport,
1770
+ identity: `${plan.localCliContract.surface}/${plan.localCliContract.action}@${plan.localCliContract.version}`,
1771
+ surface: plan.localCliContract.surface,
1772
+ action: plan.localCliContract.action,
1773
+ version: plan.localCliContract.version,
1774
+ hash: plan.localCliContract.hash,
1775
+ tool: plan.localCliContract.tool,
1776
+ };
1777
+ }
1778
+ if (plan.transport === "reviewed-template-api") {
1779
+ return {
1780
+ transport: plan.transport,
1781
+ identity: `${plan.adapter.id}/${plan.operation}@${plan.reviewedTemplateContract.version}`,
1782
+ version: plan.reviewedTemplateContract.version,
1783
+ hash: plan.reviewedTemplateContract.hash,
1784
+ };
1785
+ }
1786
+ throw new Error(DOM_ACTION_TRANSPORT_DISABLED_MESSAGE);
1787
+ }
1788
+
1789
+ function identityBindingView(
1790
+ operation: GhostgetOperation,
1791
+ input: OperationInput,
1792
+ auth: InvocationAuthority,
1793
+ ): Record<string, unknown> {
1794
+ const subject = auth.subject ?? null;
1795
+ if (isPublicWebSessionInvocationAuthority(auth)) {
1796
+ return {
1797
+ status: "public",
1798
+ subject,
1799
+ accountActor: null,
1800
+ requestedActor: null,
1801
+ };
1802
+ }
1803
+ const requestedActor = isProviderOperation(operation) && (operation.risk === "R2" || operation.risk === "R3")
1804
+ ? typeof input.actor === "string"
1805
+ ? input.actor
1806
+ : typeof input.author === "string" ? input.author : null
1807
+ : null;
1808
+ const accountActor = (
1809
+ requestedActor === null
1810
+ && (isProviderOperation(operation) || isWebSessionOperation(operation))
1811
+ ) ? subject : null;
1812
+ const status = requestedActor !== null
1813
+ ? requestedActor === subject ? "subject-match" : "contract-preflight-required"
1814
+ : accountActor !== null ? "account-subject" : subject !== null ? "subject-only" : "unbound";
1815
+ return {
1816
+ status,
1817
+ subject,
1818
+ accountActor,
1819
+ requestedActor,
1820
+ };
1821
+ }
1822
+
1823
+ function previewView(
1824
+ stored: ReturnType<typeof createInvocationPlan>,
1825
+ invocation: ReturnType<typeof prepareInvocation>,
1826
+ headed: boolean,
1827
+ ): Record<string, unknown> {
1828
+ if (
1829
+ invocation.manifest.id !== stored.plan.adapter.id
1830
+ || invocation.manifest.version !== stored.plan.adapter.version
1831
+ || invocation.operationId !== stored.plan.operation
1832
+ || invocation.auth.id !== stored.plan.auth.id
1833
+ || invocation.auth.kind !== stored.plan.auth.kind
1834
+ || sha256(canonicalJson(invocation.auth)) !== stored.plan.auth.hash
1835
+ ) throw new Error("prepared invocation no longer matches its confirmation plan");
1836
+ const operation = invocation.manifest.operations[invocation.operationId];
1837
+ if (operation === undefined) throw new Error("operation disappeared while previewing its confirmation plan");
1838
+ const previewInputValue = (value: unknown): unknown => isPlanBoundFile(value)
1839
+ ? summarizePlanFile(value)
1840
+ : Array.isArray(value) ? value.map(previewInputValue) : value;
1841
+ const input = Object.fromEntries(Object.entries(stored.plan.input).map(([key, value]) => [key, previewInputValue(value)]));
1842
+ return {
1843
+ ok: true,
1844
+ status: "confirmation-required",
1845
+ digest: stored.digest,
1846
+ expiresAt: stored.plan.expiresAt,
1847
+ adapter: stored.plan.adapter,
1848
+ operation: stored.plan.operation,
1849
+ risk: stored.plan.risk,
1850
+ sideEffect: stored.plan.sideEffect,
1851
+ input,
1852
+ inputHash: stored.plan.inputHash,
1853
+ dispatches: stored.plan.dispatches,
1854
+ auth: {
1855
+ id: stored.plan.auth.id,
1856
+ kind: stored.plan.auth.kind,
1857
+ realmFingerprint: stored.plan.auth.hash.slice(0, 16),
1858
+ },
1859
+ identityBinding: identityBindingView(operation, stored.plan.input, invocation.auth),
1860
+ transport: stored.plan.transport,
1861
+ contract: confirmationContractView(stored.plan),
1862
+ ...(stored.plan.duplicateRisk === undefined
1863
+ ? {}
1864
+ : {
1865
+ duplicateRisk: {
1866
+ sourceRunId: stored.plan.duplicateRisk.sourceRunId,
1867
+ successorIntentFingerprint:
1868
+ stored.plan.duplicateRisk.intentHash.slice(0, 16),
1869
+ warning:
1870
+ "The source post may already exist. Confirming creates one distinct at-most-once successor intent and can publish a duplicate; the source evidence remains preserved.",
1871
+ },
1872
+ }),
1873
+ confirmCommand: `ghostget confirm ${stored.digest}${headed ? " --headed" : ""}`,
1874
+ };
1875
+ }
1876
+
1877
+ function directPreviewView(invocation: ReturnType<typeof prepareInvocation>): Record<string, unknown> {
1878
+ const operation = invocation.manifest.operations[invocation.operationId];
1879
+ if (operation === undefined) throw new Error("operation disappeared while previewing it");
1880
+ return {
1881
+ ok: true,
1882
+ status: "preview",
1883
+ requiresConfirmation: false,
1884
+ adapter: {
1885
+ id: invocation.manifest.id,
1886
+ version: invocation.manifest.version,
1887
+ hash: manifestHash(invocation.manifest),
1888
+ },
1889
+ operation: invocation.operationId,
1890
+ risk: operation.risk,
1891
+ sideEffect: operation.sideEffect,
1892
+ input: invocation.input,
1893
+ auth: {
1894
+ id: invocation.auth.id,
1895
+ kind: invocation.auth.kind,
1896
+ realmFingerprint: sha256(canonicalJson(invocation.auth)).slice(0, 16),
1897
+ },
1898
+ identityBinding: identityBindingView(operation, invocation.input, invocation.auth),
1899
+ transport: invocation.portablePluginContract === undefined
1900
+ ? installedOperationTransport(operation)
1901
+ : "portable-provider-plugin",
1902
+ ...(invocation.portablePluginContract === undefined
1903
+ ? {}
1904
+ : {
1905
+ portablePluginContract: invocation.portablePluginContract,
1906
+ }),
1907
+ };
1908
+ }
1909
+
1910
+ export function invocationView(result: Awaited<ReturnType<typeof executeReadInvocation>>): Record<string, unknown> {
1911
+ const readFailure = result.receipt.risk === "R1"
1912
+ && result.receipt.status === "failed"
1913
+ ? result.readFailure ?? readFailureProjection("contract-drift")
1914
+ : undefined;
1915
+ return {
1916
+ ok: result.receipt.status === "succeeded" || result.receipt.status === "submitted",
1917
+ status: result.receipt.status,
1918
+ runId: result.receipt.runId,
1919
+ replayed: result.replayed,
1920
+ receipt: result.receipt,
1921
+ output: result.output,
1922
+ ...(readFailure === undefined ? {} : { readFailure }),
1923
+ };
1924
+ }
1925
+
1926
+ export function cachedInvocationView(
1927
+ result: ReadProjectionCacheResult,
1928
+ ): Record<string, unknown> {
1929
+ if (result.status === "miss") {
1930
+ return {
1931
+ ok: false,
1932
+ status: "cache-miss",
1933
+ source: "cache",
1934
+ projection: { key: result.key },
1935
+ };
1936
+ }
1937
+ return {
1938
+ ok: true,
1939
+ status: "cached",
1940
+ source: "cache",
1941
+ projection: {
1942
+ key: result.key,
1943
+ dataRevision: result.dataRevision,
1944
+ createdAt: result.createdAt,
1945
+ dataChangedAt: result.dataChangedAt,
1946
+ validatedAt: result.validatedAt,
1947
+ runId: result.runId,
1948
+ ageMs: result.ageMs,
1949
+ freshness: result.freshness,
1950
+ },
1951
+ output: result.output,
1952
+ };
1953
+ }
1954
+
1955
+ export function revalidatedInvocationView(
1956
+ result: RevalidatedCapability,
1957
+ ): Record<string, unknown> {
1958
+ return {
1959
+ ...invocationView(result.live),
1960
+ source: "live",
1961
+ cache: result.cache,
1962
+ };
1963
+ }
1964
+
1965
+ function printPreview(output: Output, value: Record<string, unknown>, json: boolean): void {
1966
+ output.stdout(json ? exactTerminalJson(value) : exactTerminalJson(value));
1967
+ }
1968
+
1969
+ async function runCommand(
1970
+ arguments_: GhostgetArguments,
1971
+ environment: Readonly<Record<string, string | undefined>>,
1972
+ output: Output,
1973
+ dependencies: GhostgetDependencies,
1974
+ signal?: AbortSignal,
1975
+ ): Promise<number> {
1976
+ if (arguments_.command === "help") {
1977
+ output.stdout(renderGhostgetUsage());
1978
+ return 0;
1979
+ }
1980
+ if (arguments_.command === "imessage-transport-install") {
1981
+ const runtime = await dependencies.loadImsgDirectInstallRuntime();
1982
+ const installed = await runtime.installReviewedImsgBinary(
1983
+ arguments_.binary,
1984
+ environment,
1985
+ );
1986
+ const result = Object.freeze({
1987
+ ok: true,
1988
+ installed: true,
1989
+ alreadyPresent: installed.alreadyPresent,
1990
+ tool: "imsg-private-transport",
1991
+ version: installed.version,
1992
+ executableSha256: installed.executableSha256,
1993
+ });
1994
+ print(output, result, arguments_.json);
1995
+ return 0;
1996
+ }
1997
+ if (arguments_.command === "messaging-reconcile") {
1998
+ const claimRecovery = repairInterruptedConfirmationClaims(environment);
1999
+ if (claimRecovery.invalid > 0) {
2000
+ throw new Error(
2001
+ "local messaging recovery has unresolved state; run ghostget doctor before reconciliation",
2002
+ );
2003
+ }
2004
+ const result = reconcileMessagingRunInternal(arguments_.runId, {
2005
+ environment,
2006
+ });
2007
+ print(output, result, arguments_.json);
2008
+ return result.state === "indeterminate"
2009
+ ? 5
2010
+ : result.state === "submitted"
2011
+ ? 0
2012
+ : 3;
2013
+ }
2014
+ if (arguments_.command === "clip") {
2015
+ return runCaptureCommand(
2016
+ [],
2017
+ arguments_.arguments,
2018
+ environment,
2019
+ output,
2020
+ dependencies,
2021
+ signal,
2022
+ );
2023
+ }
2024
+ if (arguments_.command === "read") {
2025
+ return runCaptureCommand(
2026
+ ["inspect"],
2027
+ arguments_.arguments,
2028
+ environment,
2029
+ output,
2030
+ dependencies,
2031
+ signal,
2032
+ );
2033
+ }
2034
+ if (arguments_.command === "media") {
2035
+ const media = await dependencies.loadMediaRuntime();
2036
+ return media.runCli(arguments_.arguments, {
2037
+ io: output,
2038
+ environment,
2039
+ ...(signal === undefined ? {} : { signal }),
2040
+ });
2041
+ }
2042
+ if (arguments_.command === "apple-photos-export-contact-evidence") {
2043
+ const runtime = await dependencies.loadApplePhotosCliRuntime();
2044
+ const result = await runtime.exportApplePhotosContactEvidenceForCli({
2045
+ ...(arguments_.library === undefined
2046
+ ? {}
2047
+ : { library: arguments_.library }),
2048
+ environment,
2049
+ progress: (event) => {
2050
+ output.stderr(runtime.formatApplePhotosContactEvidenceProgress(event));
2051
+ },
2052
+ ...(signal === undefined ? {} : { signal }),
2053
+ });
2054
+ output.stdout(
2055
+ runtime.encodeApplePhotosContactEvidenceCliOutput(result, arguments_.json),
2056
+ );
2057
+ return 0;
2058
+ }
2059
+ if (arguments_.command === "whatsapp-export-message-like-me") {
2060
+ const admission = acquireReadProjectionAuthAdmission(
2061
+ arguments_.authId,
2062
+ environment,
2063
+ );
2064
+ try {
2065
+ const auth = loadAuth(arguments_.authId, environment);
2066
+ if (auth.kind !== "linked-device-store" || auth.provider !== "whatsapp") {
2067
+ throw new Error(
2068
+ "Message Like Me export requires a WhatsApp linked-device-store auth locator",
2069
+ );
2070
+ }
2071
+ const runtime = await dependencies.loadWhatsAppMessageLikeMeCliRuntime();
2072
+ const result = await runtime.exportWhatsAppMessageLikeMeFromAuth({
2073
+ auth,
2074
+ outputRoot: arguments_.output,
2075
+ environment,
2076
+ onProgress: (progress) => {
2077
+ output.stderr(renderWhatsAppMessageLikeMeProgress(progress));
2078
+ },
2079
+ ...(signal === undefined ? {} : { signal }),
2080
+ });
2081
+ if (arguments_.json) {
2082
+ output.stdout(runtime.encodeWhatsAppMessageLikeMeCliResult(result));
2083
+ } else {
2084
+ output.stdout(`WhatsApp Message Like Me bundle: ${safe(result.bundle.outputRoot)}\n`);
2085
+ output.stdout(`Manifest SHA-256: ${safe(result.bundle.manifestSha256)}\n`);
2086
+ output.stdout("Remote history completeness is not claimed. No cloud sync occurred.\n");
2087
+ }
2088
+ return 0;
2089
+ } finally {
2090
+ admission.release();
2091
+ }
2092
+ }
2093
+ if (arguments_.command === "beeper-export-contact-interactions") {
2094
+ const runtime = await dependencies.loadBeeperContactInteractionCliRuntime();
2095
+ runtime.assertBeeperContactInteractionExportRuntime();
2096
+ const admission = acquireReadProjectionAuthAdmission(
2097
+ arguments_.authId,
2098
+ environment,
2099
+ );
2100
+ try {
2101
+ const auth = loadAuth(arguments_.authId, environment);
2102
+ if (auth.kind !== "linked-device-store" || auth.provider !== "beeper") {
2103
+ throw new Error(
2104
+ "Beeper contact interaction export requires a Beeper linked-device-store auth locator",
2105
+ );
2106
+ }
2107
+ const result = await runtime.exportBeeperContactInteractionsFromAuth({
2108
+ auth,
2109
+ limits: {
2110
+ ...(arguments_.limitChats === undefined
2111
+ ? {}
2112
+ : { limitChats: arguments_.limitChats }),
2113
+ ...(arguments_.limitMessages === undefined
2114
+ ? {}
2115
+ : { limitMessages: arguments_.limitMessages }),
2116
+ ...(arguments_.maxParticipants === undefined
2117
+ ? {}
2118
+ : { maxParticipants: arguments_.maxParticipants }),
2119
+ },
2120
+ environment,
2121
+ onProgress: (progress) => {
2122
+ output.stderr(renderBeeperMessageLikeMeProgress(progress));
2123
+ },
2124
+ ...(signal === undefined ? {} : { signal }),
2125
+ });
2126
+ output.stdout(runtime.encodeBeeperContactInteractionExportResult(result));
2127
+ return 0;
2128
+ } finally {
2129
+ admission.release();
2130
+ }
2131
+ }
2132
+ if (arguments_.command === "beeper-export-message-like-me") {
2133
+ const admission = acquireReadProjectionAuthAdmission(
2134
+ arguments_.authId,
2135
+ environment,
2136
+ );
2137
+ try {
2138
+ const auth = loadAuth(arguments_.authId, environment);
2139
+ if (auth.kind !== "linked-device-store" || auth.provider !== "beeper") {
2140
+ throw new Error(
2141
+ "Message Like Me export requires a Beeper linked-device-store auth locator",
2142
+ );
2143
+ }
2144
+ const runtime = await dependencies.loadBeeperMessageLikeMeCliRuntime();
2145
+ const result = await runtime.exportBeeperMessageLikeMeFromAuth({
2146
+ auth,
2147
+ outputRoot: arguments_.output,
2148
+ limits: {
2149
+ ...(arguments_.limitChats === undefined
2150
+ ? {}
2151
+ : { limitChats: arguments_.limitChats }),
2152
+ ...(arguments_.limitMessages === undefined
2153
+ ? {}
2154
+ : { limitMessages: arguments_.limitMessages }),
2155
+ ...(arguments_.maxParticipants === undefined
2156
+ ? {}
2157
+ : { maxParticipants: arguments_.maxParticipants }),
2158
+ },
2159
+ environment,
2160
+ onProgress: (progress) => {
2161
+ output.stderr(renderBeeperMessageLikeMeProgress(progress));
2162
+ },
2163
+ ...(signal === undefined ? {} : { signal }),
2164
+ });
2165
+ const summary = Object.freeze({
2166
+ ok: true,
2167
+ outputRoot: result.outputRoot,
2168
+ manifestPath: result.manifestPath,
2169
+ manifestSha256: result.manifestSha256,
2170
+ completeness: result.manifest.completeness,
2171
+ warnings: result.manifest.warnings,
2172
+ counts: result.manifest.counts,
2173
+ });
2174
+ print(output, summary, arguments_.json);
2175
+ return 0;
2176
+ } finally {
2177
+ admission.release();
2178
+ }
2179
+ }
2180
+ if (
2181
+ arguments_.command === "messaging-routes"
2182
+ || arguments_.command === "messaging-resolve"
2183
+ || arguments_.command === "messaging-context"
2184
+ || arguments_.command === "messaging-preview"
2185
+ ) {
2186
+ const privateOutput = validateMessagingPrivateOutputPath(
2187
+ arguments_.privateOutput,
2188
+ environment,
2189
+ );
2190
+ const request = await readMessagingInput(arguments_.inputSource);
2191
+ const options = {
2192
+ environment,
2193
+ registry: dependencies.providerPluginRegistry,
2194
+ ...(signal === undefined ? {} : { signal }),
2195
+ } as const;
2196
+ const artifact = arguments_.command === "messaging-routes"
2197
+ ? await discoverMessagingRoutesInternal(request, options)
2198
+ : arguments_.command === "messaging-resolve"
2199
+ ? await resolveMessagingRouteInternal(request, options)
2200
+ : arguments_.command === "messaging-context"
2201
+ ? await readMessagingContextInternal(request, options)
2202
+ : await previewMessagingTurnInternal(request, options);
2203
+ const receipt = writeMessagingPrivateOutput(
2204
+ privateOutput,
2205
+ artifact,
2206
+ environment,
2207
+ );
2208
+ output.stdout(exactTerminalJson(receipt));
2209
+ return 0;
2210
+ }
2211
+ if (arguments_.command === "doctor") {
2212
+ return doctor(arguments_, environment, output, dependencies);
2213
+ }
2214
+ if (arguments_.command === "capabilities") {
2215
+ return runCapabilities(
2216
+ arguments_,
2217
+ environment,
2218
+ output,
2219
+ dependencies.providerPluginRegistry,
2220
+ );
2221
+ }
2222
+ if (arguments_.command === "plugin-list") {
2223
+ return await runPluginList(
2224
+ arguments_,
2225
+ environment,
2226
+ output,
2227
+ dependencies.providerPluginRegistry,
2228
+ );
2229
+ }
2230
+ if (arguments_.command === "plugin-show") {
2231
+ return await runPluginShow(
2232
+ arguments_,
2233
+ environment,
2234
+ output,
2235
+ dependencies.providerPluginRegistry,
2236
+ );
2237
+ }
2238
+ if (arguments_.command === "platforms") {
2239
+ if (arguments_.json) {
2240
+ output.stdout(safeJson(platformPolicyView(arguments_.surfaceId)));
2241
+ } else {
2242
+ output.stdout(
2243
+ sanitizeTerminalText(
2244
+ redactSensitiveText(renderPlatformPolicyText(arguments_.surfaceId)),
2245
+ ),
2246
+ );
2247
+ }
2248
+ return 0;
2249
+ }
2250
+ if (arguments_.command === "thread-split") return runThreadSplit(arguments_, output);
2251
+ if (arguments_.command === "thread-publish") {
2252
+ return runThreadPublish(
2253
+ arguments_,
2254
+ environment,
2255
+ output,
2256
+ dependencies.providerPluginRegistry,
2257
+ );
2258
+ }
2259
+ if (arguments_.command === "auth-login") {
2260
+ const login = await loginGoogleOAuth({
2261
+ clientFile: resolve(arguments_.clientFile),
2262
+ openBrowser: arguments_.openBrowser,
2263
+ onAuthorizationUrl: (url) => {
2264
+ output.stderr(
2265
+ `Approve Google access in your system browser. If it did not open, use this one-time URL:\n${safe(url)}\n`,
2266
+ );
2267
+ },
2268
+ ...(signal === undefined ? {} : { signal }),
2269
+ });
2270
+ const installed = installManagedGoogleOAuth(
2271
+ arguments_.id,
2272
+ login,
2273
+ environment,
2274
+ arguments_.force ? { force: true } : {},
2275
+ );
2276
+ const result = {
2277
+ ok: true,
2278
+ id: installed.auth.id,
2279
+ provider: installed.auth.provider,
2280
+ subject: installed.auth.subject,
2281
+ scopes: installed.auth.scopes,
2282
+ renewal: "automatic",
2283
+ refreshExpiresAt: login.refreshTokenExpiresAt,
2284
+ } as const;
2285
+ // This bounded view contains intentionally exposed, validated auth metadata
2286
+ // and no credential or token-file fields. Keep its public scope identifiers
2287
+ // exact instead of passing them through the generic URL credential redactor.
2288
+ if (arguments_.json) output.stdout(exactTerminalJson(result));
2289
+ else {
2290
+ output.stdout(
2291
+ `Connected ${safe(login.subject)} as ${safe(arguments_.id)}. Ghostget will renew Google access automatically.\n`,
2292
+ );
2293
+ }
2294
+ if (login.refreshTokenExpiresAt !== null) {
2295
+ output.stderr(
2296
+ `Google made this refresh credential time-limited until ${safe(login.refreshTokenExpiresAt)}. Publish the personal OAuth app to production and repeat with --force for durable renewal.\n`,
2297
+ );
2298
+ }
2299
+ return 0;
2300
+ }
2301
+ if (arguments_.command === "auth-list") {
2302
+ const values = listAuth(environment).map((auth) => ({
2303
+ id: auth.id,
2304
+ kind: auth.kind,
2305
+ realmFingerprint: sha256(canonicalJson(auth)).slice(0, 16),
2306
+ subject: auth.subject ?? null,
2307
+ ...(auth.kind === "cookie-source" ? { source: auth.source, profile: auth.profile ?? null } : {}),
2308
+ ...(auth.kind === "browser-profile" ? {
2309
+ trustUnfilteredEgress: true,
2310
+ ...(auth.cookieSource === undefined ? {} : { cookieSource: auth.cookieSource }),
2311
+ ...(auth.cookieProfile === undefined ? {} : { cookieProfile: auth.cookieProfile }),
2312
+ } : {}),
2313
+ ...(auth.kind === "oauth-token-file" ? {
2314
+ provider: auth.provider,
2315
+ scopes: auth.scopes,
2316
+ managed: auth.managed === true,
2317
+ } : {}),
2318
+ ...(auth.kind === "linked-device-store" ? {
2319
+ provider: auth.provider,
2320
+ } : {}),
2321
+ }));
2322
+ if (arguments_.json) output.stdout(exactTerminalJson({ ok: true, auth: values }));
2323
+ else print(output, values, false);
2324
+ return 0;
2325
+ }
2326
+ if (arguments_.command === "auth-bind") {
2327
+ const authSnapshot = loadAuthSnapshot(arguments_.id, environment);
2328
+ const auth = authSnapshot.auth;
2329
+ const binding = dependencies.providerPluginRegistry.requireAccountRoute(
2330
+ arguments_.site,
2331
+ );
2332
+ requireProviderPluginAuth(binding, auth);
2333
+ const portableIdentity =
2334
+ portableProviderPluginSubjectProbeIdentity(binding);
2335
+ const lease = portableIdentity === null
2336
+ ? null
2337
+ : acquirePortableProviderPluginInvocationLease(
2338
+ portableIdentity,
2339
+ crypto.randomUUID(),
2340
+ environment,
2341
+ );
2342
+ const containment = lease === null
2343
+ ? null
2344
+ : createPortableProviderPluginInvocationLeaseContainmentController(
2345
+ lease,
2346
+ environment,
2347
+ );
2348
+ try {
2349
+ const subject = await dependencies.probePluginSubject(
2350
+ binding,
2351
+ auth,
2352
+ signal,
2353
+ dependencies.providerPluginRegistry,
2354
+ environment,
2355
+ );
2356
+ if (!binding.subject.matches(subject)) {
2357
+ throw new Error(
2358
+ `provider plugin surface ${binding.surfaceId} returned a subject outside ${binding.subject.format}`,
2359
+ );
2360
+ }
2361
+ if (
2362
+ auth.subject !== undefined
2363
+ && auth.subject !== subject
2364
+ && !arguments_.force
2365
+ ) {
2366
+ throw new Error("auth locator is already bound to a different account; repeat with --force only after reviewing the active signed-in account");
2367
+ }
2368
+ const bound = { ...auth, subject } satisfies GhostgetAuth;
2369
+ await dependencies.beforeAuthBindCommit();
2370
+ if (!replaceAuthIfUnchanged(
2371
+ authSnapshot,
2372
+ bound,
2373
+ environment,
2374
+ ).replaced) {
2375
+ throw new Error(
2376
+ `auth locator ${auth.id} changed while its account was being probed; the concurrent value was preserved`,
2377
+ );
2378
+ }
2379
+ print(output, {
2380
+ ok: true,
2381
+ id: bound.id,
2382
+ site: arguments_.site,
2383
+ subject: bound.subject,
2384
+ realmFingerprint: sha256(canonicalJson(bound)).slice(0, 16),
2385
+ }, arguments_.json);
2386
+ return 0;
2387
+ } finally {
2388
+ if (containment !== null) {
2389
+ containment.cleanupComplete();
2390
+ releasePortableProviderPluginInvocationLease(
2391
+ containment.current,
2392
+ environment,
2393
+ );
2394
+ }
2395
+ }
2396
+ }
2397
+ if (arguments_.command === "auth-pair") {
2398
+ const auth = loadAuth(arguments_.id, environment);
2399
+ if (auth.kind !== "linked-device-store") {
2400
+ throw new Error("auth pair requires a linked-device-store locator");
2401
+ }
2402
+ const binding = dependencies.providerPluginRegistry.requireSessionRoute(
2403
+ auth.provider,
2404
+ );
2405
+ if (binding.transport !== "linked-device") {
2406
+ throw new Error(
2407
+ `provider plugin surface ${auth.provider} is not a linked-device transport`,
2408
+ );
2409
+ }
2410
+ requireProviderPluginAuth(binding, auth);
2411
+ if (binding.linkedDeviceLifecycle === undefined) {
2412
+ throw new Error(
2413
+ `provider plugin surface ${auth.provider} does not declare pairing`,
2414
+ );
2415
+ }
2416
+ output.stdout(
2417
+ `Starting explicit ${safe(auth.provider)} linked-device pairing. This creates a separate device session and performs a bounded bootstrap sync; it does not reuse browser cookies.\n`,
2418
+ );
2419
+ const paired = await runLinkedDevicePairLifecycle(
2420
+ binding,
2421
+ auth.id,
2422
+ {
2423
+ registry: dependencies.providerPluginRegistry,
2424
+ environment,
2425
+ ...(arguments_.phone === undefined
2426
+ ? {}
2427
+ : { phone: arguments_.phone }),
2428
+ ...(signal === undefined ? {} : { signal }),
2429
+ invokePair: dependencies.pairLinkedDeviceAuth,
2430
+ },
2431
+ );
2432
+ output.stdout(
2433
+ `Paired and bound ${safe(auth.id)} to ${safe(paired.subject)}. Use 'ghostget auth sync ${safe(auth.id)} --once' when you explicitly want to refresh its local projection.\n`,
2434
+ );
2435
+ return 0;
2436
+ }
2437
+ if (arguments_.command === "auth-sync") {
2438
+ const auth = loadAuth(arguments_.id, environment);
2439
+ if (
2440
+ auth.kind !== "linked-device-store"
2441
+ || auth.subject === undefined
2442
+ ) {
2443
+ throw new Error(
2444
+ "auth sync requires a paired and account-bound linked-device-store locator",
2445
+ );
2446
+ }
2447
+ const binding = dependencies.providerPluginRegistry.requireSessionRoute(
2448
+ auth.provider,
2449
+ );
2450
+ if (
2451
+ binding.transport !== "linked-device"
2452
+ || binding.linkedDeviceLifecycle === undefined
2453
+ ) {
2454
+ throw new Error(
2455
+ `provider plugin surface ${auth.provider} does not declare one-shot linked-device sync`,
2456
+ );
2457
+ }
2458
+ requireProviderPluginAuth(binding, auth);
2459
+ if (!binding.subject.matches(auth.subject)) {
2460
+ throw new Error(
2461
+ `auth subject does not match ${binding.subject.format}`,
2462
+ );
2463
+ }
2464
+ output.stderr(
2465
+ `${auth.provider} sync explicitly connects the linked device and may emit protocol transport acknowledgements according to its registered lifecycle contract.\n`,
2466
+ );
2467
+ const lifecycle = await runLinkedDeviceSyncOnceLifecycle(
2468
+ binding,
2469
+ auth.id,
2470
+ {
2471
+ registry: dependencies.providerPluginRegistry,
2472
+ environment,
2473
+ ...(signal === undefined ? {} : { signal }),
2474
+ invokeSyncOnce: dependencies.syncLinkedDeviceAuthOnce,
2475
+ },
2476
+ );
2477
+ const result = lifecycle.result;
2478
+ print(output, {
2479
+ ok: true,
2480
+ id: auth.id,
2481
+ subject: auth.subject,
2482
+ itemsStored: result.itemsStored,
2483
+ // Compatibility field for the bundled WhatsApp projection.
2484
+ ...(auth.provider === "whatsapp"
2485
+ ? { messagesStored: result.itemsStored }
2486
+ : {}),
2487
+ projection: result.projection,
2488
+ presenceMode: "quiet",
2489
+ emitsProtocolAcknowledgements: result.emitsProtocolAcknowledgements,
2490
+ }, arguments_.json);
2491
+ return 0;
2492
+ }
2493
+ if (arguments_.command === "auth-add") {
2494
+ const auth = arguments_.linkedDeviceProvider !== undefined
2495
+ ? createAuth(arguments_.id, {
2496
+ linkedDeviceProvider: arguments_.linkedDeviceProvider,
2497
+ deviceStore: arguments_.deviceStore === undefined
2498
+ ? join(
2499
+ ghostgetStateHome(environment),
2500
+ "linked-device-stores",
2501
+ arguments_.id,
2502
+ )
2503
+ : resolve(arguments_.deviceStore),
2504
+ ...(arguments_.subject === undefined ? {} : { subject: arguments_.subject }),
2505
+ })
2506
+ : arguments_.oauthProvider !== undefined
2507
+ ? createAuth(arguments_.id, {
2508
+ oauthProvider: arguments_.oauthProvider,
2509
+ tokenFile: arguments_.tokenFile ?? "",
2510
+ scopes: arguments_.scopes ?? [],
2511
+ ...(arguments_.subject === undefined ? {} : { subject: arguments_.subject }),
2512
+ })
2513
+ : arguments_.browserProfile !== undefined
2514
+ ? createAuth(arguments_.id, {
2515
+ browserProfile: arguments_.browserProfile,
2516
+ ...(arguments_.browserExecutable === undefined
2517
+ ? {}
2518
+ : { browserExecutable: arguments_.browserExecutable }),
2519
+ trustUnfilteredEgress: arguments_.trustProfileEgress,
2520
+ ...(arguments_.cookieSource === undefined ? {} : { cookieSource: arguments_.cookieSource }),
2521
+ ...(arguments_.cookieProfile === undefined ? {} : { cookieProfile: arguments_.cookieProfile }),
2522
+ ...(arguments_.subject === undefined ? {} : { subject: arguments_.subject }),
2523
+ })
2524
+ : arguments_.cookieSource !== undefined
2525
+ ? createAuth(arguments_.id, {
2526
+ source: arguments_.cookieSource,
2527
+ ...(arguments_.cookieProfile === undefined ? {} : { profile: arguments_.cookieProfile }),
2528
+ ...(arguments_.subject === undefined ? {} : { subject: arguments_.subject }),
2529
+ })
2530
+ : arguments_.cookiesFile !== undefined
2531
+ ? createAuth(arguments_.id, {
2532
+ cookiesFile: resolve(arguments_.cookiesFile),
2533
+ ...(arguments_.subject === undefined ? {} : { subject: arguments_.subject }),
2534
+ })
2535
+ : (() => { throw new Error("auth add has no selected locator"); })();
2536
+ const path = saveAuth(auth, environment, { force: arguments_.force });
2537
+ output.stdout(`Saved ${safe(auth.id)} auth locator (${auth.kind}) to ${safe(path)}.\n`);
2538
+ if (auth.kind === "linked-device-store") {
2539
+ const binding = dependencies.providerPluginRegistry.resolveAccountRoute(
2540
+ auth.provider,
2541
+ );
2542
+ if (
2543
+ binding?.transport === "linked-device"
2544
+ && binding.linkedDeviceLifecycle !== undefined
2545
+ ) {
2546
+ output.stdout(
2547
+ `Next: ghostget auth pair ${safe(auth.id)} (optionally add --phone <international-number>).\n`,
2548
+ );
2549
+ } else {
2550
+ output.stdout(
2551
+ `Next: ghostget auth bind ${safe(auth.id)} --site ${safe(auth.provider)}.\n`,
2552
+ );
2553
+ }
2554
+ }
2555
+ return 0;
2556
+ }
2557
+ if (arguments_.command === "auth-remove") {
2558
+ if (!arguments_.yes) {
2559
+ throw new Error(
2560
+ "auth remove requires --yes; this removes the locator, any Ghostget-managed OAuth credential, and local caches, but does not revoke browser or provider-side grants",
2561
+ );
2562
+ }
2563
+ const removed = removeAuth(arguments_.id, environment);
2564
+ output.stdout(removed ? `Removed auth locator ${safe(arguments_.id)}.\n` : `Auth locator ${safe(arguments_.id)} was not present.\n`);
2565
+ return 0;
2566
+ }
2567
+ if (arguments_.command === "plugin-init") {
2568
+ const result = initPortableProviderPlugin({
2569
+ id: arguments_.id,
2570
+ displayName: arguments_.displayName,
2571
+ surfaceId: arguments_.surfaceId,
2572
+ origin: arguments_.origin,
2573
+ operation: arguments_.operation,
2574
+ transport: arguments_.transport,
2575
+ ...(arguments_.requiredScopeSets === undefined
2576
+ ? {}
2577
+ : { requiredScopeSets: arguments_.requiredScopeSets }),
2578
+ ...(arguments_.coverage === undefined
2579
+ ? {}
2580
+ : { coverage: arguments_.coverage }),
2581
+ output: resolve(arguments_.output),
2582
+ });
2583
+ if (arguments_.json) {
2584
+ print(output, { ok: true, ...result }, true);
2585
+ } else {
2586
+ output.stdout(
2587
+ `Created inert portable plugin ${safe(result.manifest.id)} at ${safe(result.path)}.\n`,
2588
+ );
2589
+ output.stdout(
2590
+ `Next: ghostget plugin check ${safe(result.path)} && ghostget plugin test ${safe(result.path)}\n`,
2591
+ );
2592
+ }
2593
+ return 0;
2594
+ }
2595
+ if (arguments_.command === "plugin-scaffold") {
2596
+ const outputDirectory = resolve(arguments_.output);
2597
+ const files = scaffoldWebProvider({
2598
+ site: arguments_.site,
2599
+ displayName: arguments_.displayName,
2600
+ origin: arguments_.origin,
2601
+ operation: arguments_.operation,
2602
+ risk: arguments_.risk,
2603
+ evidencePath: resolve(arguments_.evidence),
2604
+ candidateIndex: arguments_.candidate,
2605
+ outputDirectory,
2606
+ });
2607
+ const pluginId = `${arguments_.site}-web`;
2608
+ const result = {
2609
+ status: "capture-required",
2610
+ executable: false,
2611
+ pluginId,
2612
+ outputDirectory,
2613
+ files: files.map((file) => file.slice(outputDirectory.length + 1)),
2614
+ next:
2615
+ `Review the source unit at src/plugins/${pluginId}, then run 'ghostget plugin check src/plugins/${pluginId}'; the scaffold performs no request and contains no browser action.`,
2616
+ } as const;
2617
+ if (arguments_.json) {
2618
+ print(output, result, true);
2619
+ } else {
2620
+ output.stdout(
2621
+ `Created network-inert capture-required source plugin ${safe(pluginId)} at ${safe(outputDirectory)}.\n`,
2622
+ );
2623
+ for (const file of result.files) output.stdout(`- ${safe(file)}\n`);
2624
+ output.stdout(`${safe(result.next)}\n`);
2625
+ }
2626
+ return 0;
2627
+ }
2628
+ if (arguments_.command === "plugin-check") {
2629
+ const directory = resolve(arguments_.path);
2630
+ if (
2631
+ existsSync(join(directory, PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME))
2632
+ || existsSync(join(directory, WRENCH_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME))
2633
+ || existsSync(join(directory, LEGACY_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME))
2634
+ ) {
2635
+ const result = checkPortableProviderPlugin(directory);
2636
+ if (arguments_.json) {
2637
+ print(output, { ok: true, plugin: result }, true);
2638
+ } else {
2639
+ output.stdout(
2640
+ `Checked portable provider plugin ${safe(result.id)} ${safe(result.bundleSha256)} at ${safe(result.path)}.\n`,
2641
+ );
2642
+ output.stdout(
2643
+ `Activation: ${result.activation}; execution requires explicit trust at install.\n`,
2644
+ );
2645
+ }
2646
+ return 0;
2647
+ }
2648
+ const result = checkSourceProviderPluginDirectory(directory);
2649
+ if (arguments_.json) {
2650
+ print(output, result, true);
2651
+ } else {
2652
+ output.stdout(
2653
+ `Checked source provider plugin ${safe(result.pluginId)} at ${safe(result.directory)}.\n`,
2654
+ );
2655
+ output.stdout("State: capture-required; executable: no.\n");
2656
+ for (const file of result.files) output.stdout(`- ${safe(file)}\n`);
2657
+ }
2658
+ return 0;
2659
+ }
2660
+ if (arguments_.command === "plugin-test") {
2661
+ const result = await testPortableProviderPlugin(
2662
+ resolve(arguments_.path),
2663
+ { trustExecutableCode: arguments_.trustCode },
2664
+ );
2665
+ print(
2666
+ output,
2667
+ arguments_.json
2668
+ ? result
2669
+ : `Portable plugin ${result.pluginId}: ${result.inert} inert reservation(s), ${result.executed} secret-free fixture(s) passed.`,
2670
+ arguments_.json,
2671
+ );
2672
+ return 0;
2673
+ }
2674
+ if (arguments_.command === "plugin-pack") {
2675
+ const result = packPortableProviderPlugin(
2676
+ resolve(arguments_.path),
2677
+ resolve(arguments_.output),
2678
+ );
2679
+ print(
2680
+ output,
2681
+ arguments_.json
2682
+ ? { ok: true, ...result }
2683
+ : `Packed immutable portable plugin ${result.bundleSha256} at ${result.path}.`,
2684
+ arguments_.json,
2685
+ );
2686
+ return 0;
2687
+ }
2688
+ if (arguments_.command === "plugin-install") {
2689
+ const packagePath = resolve(arguments_.path);
2690
+ const checked = checkPortableProviderPlugin(packagePath);
2691
+ if (dependencies.providerPluginRegistry.get(checked.id) !== undefined) {
2692
+ throw new Error(
2693
+ `portable plugin ${checked.id} conflicts with a trusted source plugin ID`,
2694
+ );
2695
+ }
2696
+ const current = showPortableProviderPlugin(checked.id, environment);
2697
+ const currentDigest = current?.summary.bundleSha256;
2698
+ if (
2699
+ arguments_.expectedCurrent !== undefined
2700
+ && arguments_.expectedCurrent !== currentDigest
2701
+ ) {
2702
+ throw new Error(
2703
+ `portable plugin ${checked.id} changed from --expected-current`,
2704
+ );
2705
+ }
2706
+ const installed = installPortableProviderPlugin(packagePath, {
2707
+ trustExecutableCode: arguments_.trustCode,
2708
+ expectedCurrentBundleSha256:
2709
+ arguments_.expectedCurrent ?? currentDigest ?? null,
2710
+ assertActivatable: (candidate) =>
2711
+ assertPortableProviderPluginActivatable(
2712
+ candidate,
2713
+ dependencies.providerPluginRegistry,
2714
+ environment,
2715
+ ),
2716
+ assertCurrentQuiescent: (bundleSha256, artifactPath) =>
2717
+ assertPortableProviderPluginQuiescent(
2718
+ bundleSha256,
2719
+ artifactPath,
2720
+ environment,
2721
+ ),
2722
+ environment,
2723
+ });
2724
+ print(
2725
+ output,
2726
+ arguments_.json
2727
+ ? { ok: true, plugin: installed }
2728
+ : `Trusted and enabled portable plugin ${installed.id} ${installed.bundleSha256}.`,
2729
+ arguments_.json,
2730
+ );
2731
+ return 0;
2732
+ }
2733
+ if (arguments_.command === "plugin-doctor") {
2734
+ const report = doctorPortableProviderPlugins(environment);
2735
+ print(
2736
+ output,
2737
+ arguments_.json
2738
+ ? report
2739
+ : report.ok
2740
+ ? `Portable plugin store: ${report.installed} installation(s) verified.`
2741
+ : `Portable plugin store is invalid: ${report.issues.join("; ")}`,
2742
+ arguments_.json,
2743
+ );
2744
+ return report.ok ? 0 : 3;
2745
+ }
2746
+ if (
2747
+ arguments_.command === "plugin-disable"
2748
+ || arguments_.command === "plugin-remove"
2749
+ ) {
2750
+ if (
2751
+ arguments_.command === "plugin-remove"
2752
+ && !arguments_.yes
2753
+ ) {
2754
+ throw new Error(
2755
+ "plugin remove requires --yes; activation is removed while immutable trust and artifact evidence are retained",
2756
+ );
2757
+ }
2758
+ const current = showPortableProviderPlugin(arguments_.id, environment);
2759
+ if (current === null) {
2760
+ throw new Error(`portable plugin ${arguments_.id} is not installed`);
2761
+ }
2762
+ if (
2763
+ arguments_.expectedCurrent !== undefined
2764
+ && arguments_.expectedCurrent
2765
+ !== current.summary.bundleSha256
2766
+ ) {
2767
+ throw new Error(
2768
+ `portable plugin ${arguments_.id} changed from --expected-current`,
2769
+ );
2770
+ }
2771
+ const expectedBundleSha256 =
2772
+ arguments_.expectedCurrent ?? current.summary.bundleSha256;
2773
+ const result = arguments_.command === "plugin-disable"
2774
+ ? disablePortableProviderPlugin(arguments_.id, {
2775
+ expectedBundleSha256,
2776
+ assertQuiescent: (bundleSha256, artifactPath) =>
2777
+ assertPortableProviderPluginQuiescent(
2778
+ bundleSha256,
2779
+ artifactPath,
2780
+ environment,
2781
+ ),
2782
+ environment,
2783
+ })
2784
+ : removePortableProviderPlugin(arguments_.id, {
2785
+ expectedBundleSha256,
2786
+ assertQuiescent: (bundleSha256, artifactPath) =>
2787
+ assertPortableProviderPluginQuiescent(
2788
+ bundleSha256,
2789
+ artifactPath,
2790
+ environment,
2791
+ ),
2792
+ environment,
2793
+ });
2794
+ print(
2795
+ output,
2796
+ arguments_.json
2797
+ ? { ok: true, plugin: result }
2798
+ : arguments_.command === "plugin-disable"
2799
+ ? `Disabled portable plugin ${result.id} ${result.bundleSha256}.`
2800
+ : `Removed portable plugin ${result.id} activation; immutable audit bytes were retained.`,
2801
+ arguments_.json,
2802
+ );
2803
+ return 0;
2804
+ }
2805
+ if (arguments_.command === "adapter-init") {
2806
+ const directory = resolve(arguments_.output);
2807
+ const path = join(directory, "wrench-adapter.json");
2808
+ const manifest = arguments_.target.kind === "origin"
2809
+ ? emptyManifest(arguments_.id, arguments_.target.origin)
2810
+ : manifestFromPlatform(arguments_.id, arguments_.target.surfaceId);
2811
+ if (arguments_.force) writePrivateJson(path, manifest);
2812
+ else if (!createPrivateJsonIfAbsent(path, manifest).created) throw new Error(`manifest already exists: ${path}`);
2813
+ output.stdout(`Created adapter manifest ${safe(path)}.\n`);
2814
+ if (arguments_.target.kind === "platform") {
2815
+ output.stdout(`Copied reviewed ${safe(arguments_.target.surfaceId)} origins into an empty, uninstalled manifest; no capability was created.\n`);
2816
+ if (socialPlatformCatalog[arguments_.target.surfaceId].originPolicy.additionalExactOrigins.state === "adapter-declared") {
2817
+ output.stdout("No custom publication origin was inferred; initialize that exact HTTPS origin with --origin instead.\n");
2818
+ }
2819
+ }
2820
+ return 0;
2821
+ }
2822
+ if (arguments_.command === "adapter-sync-bundled") {
2823
+ const { syncBundledAdapters } = await import(
2824
+ "./scripts/sync-bundled-adapters"
2825
+ );
2826
+ const result = await syncBundledAdapters({
2827
+ environment,
2828
+ output: arguments_.json
2829
+ ? { stdout: () => {}, stderr: output.stderr }
2830
+ : output,
2831
+ });
2832
+ if (arguments_.json) {
2833
+ print(output, { ok: true, ...result }, true);
2834
+ } else {
2835
+ output.stdout(
2836
+ `Bundled adapter generation ${safe(result.commitId)} installed ${String(result.installed)} and preserved ${String(result.preserved)}.\n`,
2837
+ );
2838
+ }
2839
+ return 0;
2840
+ }
2841
+ if (arguments_.command === "adapter-validate") {
2842
+ const result = readManifestFile(
2843
+ resolve(arguments_.path),
2844
+ dependencies.providerPluginRegistry,
2845
+ );
2846
+ print(output, result.ok
2847
+ ? {
2848
+ ok: true,
2849
+ id: result.value.id,
2850
+ version: result.value.version,
2851
+ surfaceId: result.value.surfaceId ?? null,
2852
+ manifestHash: manifestHash(result.value),
2853
+ operations: Object.keys(result.value.operations),
2854
+ }
2855
+ : { ok: false, issues: result.issues }, arguments_.json);
2856
+ return result.ok ? 0 : 2;
2857
+ }
2858
+ if (arguments_.command === "adapter-install") {
2859
+ const installed = withPortableProviderPluginCatalogLock(
2860
+ portableProviderPluginStoreRoot(environment),
2861
+ new Date(),
2862
+ () => {
2863
+ const currentRegistry =
2864
+ dependencies.createPortableProviderPluginCatalog(
2865
+ dependencies.providerPluginRegistry,
2866
+ environment,
2867
+ ).registry;
2868
+ const result = readManifestFile(
2869
+ resolve(arguments_.path),
2870
+ currentRegistry,
2871
+ );
2872
+ if (!result.ok) {
2873
+ throw new Error(`invalid manifest: ${result.issues.join("; ")}`);
2874
+ }
2875
+ if (
2876
+ currentRegistry.resolveOwnedManifest(
2877
+ result.value.id,
2878
+ ) !== undefined
2879
+ ) {
2880
+ throw new Error(
2881
+ `adapter ${result.value.id} is owned by an enabled portable provider plugin`,
2882
+ );
2883
+ }
2884
+ if (
2885
+ result.value.schemaVersion === 1
2886
+ && (
2887
+ result.value.id !== "linkedin"
2888
+ || manifestHash(result.value)
2889
+ !== GHOSTGET_LEGACY_LINKEDIN_MANIFEST_HASH
2890
+ )
2891
+ ) {
2892
+ throw new Error(
2893
+ "schemaVersion 1 installs are restricted to the exact archived LinkedIn v0.4.0 migration fixture",
2894
+ );
2895
+ }
2896
+ let force = arguments_.force;
2897
+ let expectedCurrentContentSha256: string | undefined;
2898
+ if (arguments_.upgradeFrom !== undefined) {
2899
+ const priors = arguments_.upgradeFrom.map((path) => {
2900
+ const prior = readDiagnosticManifestFile(
2901
+ resolve(path),
2902
+ currentRegistry,
2903
+ );
2904
+ if (!prior.ok) {
2905
+ throw new Error(
2906
+ `invalid prior bundled manifest: ${prior.issues.join("; ")}`,
2907
+ );
2908
+ }
2909
+ if (prior.value.id !== result.value.id) {
2910
+ throw new Error(
2911
+ "--upgrade-from manifests must have the same adapter ID",
2912
+ );
2913
+ }
2914
+ return prior.value;
2915
+ });
2916
+ const priorHashes = new Set(
2917
+ priors.map((prior) => manifestHash(prior)),
2918
+ );
2919
+ if (priorHashes.size !== priors.length) {
2920
+ throw new Error(
2921
+ "--upgrade-from manifests must identify distinct adapter versions",
2922
+ );
2923
+ }
2924
+ const installedSnapshot =
2925
+ loadInstalledDiagnosticManifestSnapshot(
2926
+ result.value.id,
2927
+ environment,
2928
+ currentRegistry,
2929
+ );
2930
+ const current = installedSnapshot.result;
2931
+ force = current.ok
2932
+ && priorHashes.has(manifestHash(current.value));
2933
+ if (force) {
2934
+ if (installedSnapshot.contentSha256 === null) {
2935
+ throw new Error(
2936
+ "installed adapter snapshot omitted its content hash",
2937
+ );
2938
+ }
2939
+ expectedCurrentContentSha256 =
2940
+ installedSnapshot.contentSha256;
2941
+ }
2942
+ if (
2943
+ current.ok
2944
+ && !force
2945
+ && manifestHash(current.value) !== manifestHash(result.value)
2946
+ ) {
2947
+ throw new Error(
2948
+ `adapter ${result.value.id} differs from the current manifest and all upgrade baselines; preserved the user-modified install`,
2949
+ );
2950
+ }
2951
+ }
2952
+ const path = installManifest(result.value, {
2953
+ force,
2954
+ environment,
2955
+ registry: currentRegistry,
2956
+ ...(expectedCurrentContentSha256 === undefined
2957
+ ? {}
2958
+ : { expectedCurrentContentSha256 }),
2959
+ });
2960
+ return Object.freeze({ id: result.value.id, path });
2961
+ },
2962
+ );
2963
+ output.stdout(
2964
+ `Installed ${safe(installed.id)} at ${safe(installed.path)}.\n`,
2965
+ );
2966
+ return 0;
2967
+ }
2968
+ if (arguments_.command === "adapter-remove") {
2969
+ if (!arguments_.yes) {
2970
+ throw new Error(
2971
+ "adapter remove requires --yes; existing previews for it will fail closed",
2972
+ );
2973
+ }
2974
+ const removed = withPortableProviderPluginCatalogLock(
2975
+ portableProviderPluginStoreRoot(environment),
2976
+ new Date(),
2977
+ () => removeInstalledManifest(arguments_.id, environment),
2978
+ );
2979
+ output.stdout(removed ? `Removed adapter ${safe(arguments_.id)}.\n` : `Adapter ${safe(arguments_.id)} was not present.\n`);
2980
+ return 0;
2981
+ }
2982
+ if (arguments_.command === "derive-start") {
2983
+ const auth = loadAuth(arguments_.authId, environment);
2984
+ if (auth.kind === "oauth-token-file") {
2985
+ throw new Error(
2986
+ `auth locator ${auth.id} is for official ${auth.provider} API capabilities and cannot start an agent-browser derivation; use cookie/profile auth`,
2987
+ );
2988
+ }
2989
+ if (auth.kind === "linked-device-store") {
2990
+ throw new Error(
2991
+ `auth locator ${auth.id} is a ${auth.provider} linked-device protocol realm and cannot start an agent-browser derivation`,
2992
+ );
2993
+ }
2994
+ const session = await startDerivation(
2995
+ arguments_.adapterId,
2996
+ arguments_.url,
2997
+ auth,
2998
+ {
2999
+ allowRemoteActions: arguments_.allowRemoteActions,
3000
+ contentMode: arguments_.contentMode,
3001
+ browserDomains: arguments_.browserDomains,
3002
+ cookieOrigins: arguments_.cookieOrigins,
3003
+ fixtureSources: arguments_.fixtureSources,
3004
+ headed: arguments_.headed,
3005
+ environment,
3006
+ },
3007
+ );
3008
+ output.stdout(safeJson({
3009
+ ok: true,
3010
+ derivationId: session.id,
3011
+ targetOrigin: session.targetOrigin,
3012
+ allowRemoteActions: session.allowRemoteActions,
3013
+ contentMode: session.contentMode,
3014
+ fixtures: derivationFixtureSummaries(session.fixtures),
3015
+ domainContainment: session.profilePath === null,
3016
+ browserDomains: session.browserDomains,
3017
+ next: [
3018
+ `ghostget derive browser ${session.id} -- snapshot -i`,
3019
+ `ghostget derive review ${session.id} --limit 50 --json`,
3020
+ `ghostget derive finish ${session.id} --output ./wrench-${session.adapterId}`,
3021
+ ],
3022
+ warning: session.profilePath === null
3023
+ ? "Page loads can cause background writes such as read receipts. Private review seals the recorder; raw HAR data remains task-owned temporary state and is deleted at finish/discard."
3024
+ : "This profile-backed browser has unfiltered egress; browserDomains is review metadata, not network containment. Page loads can contact other origins and cause background writes such as read receipts. Private review seals the recorder; raw HAR data remains task-owned temporary state and is deleted at finish/discard.",
3025
+ }));
3026
+ return 0;
3027
+ }
3028
+ if (arguments_.command === "derive-list") {
3029
+ const values = listDerivations(environment);
3030
+ print(output, arguments_.json ? { ok: true, derivations: values } : values, arguments_.json);
3031
+ return 0;
3032
+ }
3033
+ if (arguments_.command === "derive-browser") {
3034
+ const result = await runDerivationBrowserCommand(arguments_.id, arguments_.browserArguments, environment);
3035
+ print(output, result, arguments_.json);
3036
+ return 0;
3037
+ }
3038
+ if (arguments_.command === "derive-review") {
3039
+ let selection: DerivationReviewSelection;
3040
+ if (arguments_.selection.kind === "entry") {
3041
+ if (arguments_.selection.stdinMode === "fixtures") {
3042
+ let value: unknown;
3043
+ try {
3044
+ value = JSON.parse(await readStdinBounded(64 * 1024)) as unknown;
3045
+ } catch {
3046
+ throw new Error("derive review fixtures must be valid UTF-8 JSON on stdin");
3047
+ }
3048
+ const fixtures: DerivationReviewFixtures = parseDerivationReviewFixtures(value);
3049
+ selection = {
3050
+ kind: "entry",
3051
+ entryIndex: arguments_.selection.entryIndex,
3052
+ fixtures,
3053
+ };
3054
+ } else if (arguments_.selection.stdinMode === "field-names") {
3055
+ let value: unknown;
3056
+ try {
3057
+ value = JSON.parse(await readStdinBounded(8 * 1024)) as unknown;
3058
+ } catch {
3059
+ throw new Error("derive review field names must be valid UTF-8 JSON on stdin");
3060
+ }
3061
+ const fieldNames: DerivationReviewFieldNames =
3062
+ parseDerivationReviewFieldNames(value);
3063
+ selection = {
3064
+ kind: "entry",
3065
+ entryIndex: arguments_.selection.entryIndex,
3066
+ fieldNames,
3067
+ };
3068
+ } else {
3069
+ selection = {
3070
+ kind: "entry",
3071
+ entryIndex: arguments_.selection.entryIndex,
3072
+ fixtures: {},
3073
+ };
3074
+ }
3075
+ } else selection = arguments_.selection;
3076
+ const result = await reviewDerivation(
3077
+ arguments_.id,
3078
+ selection,
3079
+ environment,
3080
+ arguments_.reviewOrigin,
3081
+ );
3082
+ print(output, { ok: true, sealed: true, ...result }, arguments_.json);
3083
+ return 0;
3084
+ }
3085
+ if (arguments_.command === "derive-finish") {
3086
+ const result = await finishDerivation(arguments_.id, arguments_.output, {
3087
+ force: arguments_.force,
3088
+ registry: dependencies.providerPluginRegistry,
3089
+ ...(arguments_.reviewOrigin === undefined ? {} : { reviewOrigin: arguments_.reviewOrigin }),
3090
+ ...(arguments_.surfaceId === undefined ? {} : { surfaceId: arguments_.surfaceId }),
3091
+ environment,
3092
+ });
3093
+ print(output, {
3094
+ ok: true,
3095
+ observedEntries: result.analysis.observedEntries,
3096
+ candidates: result.analysis.candidates.length,
3097
+ manifestPath: result.manifestPath,
3098
+ candidatesPath: result.candidatesPath,
3099
+ internalApiCandidates: result.internalEvidence.candidates.length,
3100
+ evidencePath: result.evidencePath,
3101
+ reservationPath: result.reservationPath,
3102
+ rawHarRetained: false,
3103
+ next: [
3104
+ `Review ${result.reservationPath}`,
3105
+ `Edit ${result.manifestPath} only after exact request/response review`,
3106
+ `ghostget adapter validate ${result.manifestPath}`,
3107
+ `ghostget adapter install ${result.manifestPath}`,
3108
+ ],
3109
+ }, arguments_.json);
3110
+ return 0;
3111
+ }
3112
+ if (arguments_.command === "derive-discard") {
3113
+ if (!arguments_.yes) throw new Error("derive discard requires --yes because it deletes the private session and raw HAR");
3114
+ const discarded = await discardDerivation(arguments_.id, environment);
3115
+ output.stdout(discarded ? "Discarded the derivation and deleted its private artifacts.\n" : "Derivation was not present.\n");
3116
+ return 0;
3117
+ }
3118
+ if (arguments_.command === "derive-analyze") {
3119
+ const analysis = analyzeHarFile(resolve(arguments_.har), arguments_.adapterId, arguments_.origin);
3120
+ const result = writeDerivationScaffold(arguments_.output, analysis, {
3121
+ force: arguments_.force,
3122
+ registry: dependencies.providerPluginRegistry,
3123
+ ...(arguments_.surfaceId === undefined ? {} : { surfaceId: arguments_.surfaceId }),
3124
+ });
3125
+ print(output, { ok: true, observedEntries: analysis.observedEntries, candidates: analysis.candidates.length, ...result }, arguments_.json);
3126
+ return 0;
3127
+ }
3128
+ if (arguments_.command === "omni-read") {
3129
+ const request = await readInput(arguments_.inputSource);
3130
+ const common = {
3131
+ environment,
3132
+ registry: dependencies.providerPluginRegistry,
3133
+ } as const;
3134
+ const value = arguments_.identityOnly
3135
+ ? dependencies.identifyOmniView(request, common)
3136
+ : arguments_.cacheOnly
3137
+ ? dependencies.readCachedOmniViewInternal(request, common)
3138
+ : arguments_.fromExactCache
3139
+ ? dependencies.rebuildOmniViewFromExactCache(request, common)
3140
+ : await dependencies.revalidateOmniViewInternal(request, {
3141
+ ...common,
3142
+ headed: arguments_.headed,
3143
+ ...(signal === undefined ? {} : { signal }),
3144
+ });
3145
+ if (arguments_.json) output.stdout(exactTerminalJson(value));
3146
+ else print(output, value, false);
3147
+ return 0;
3148
+ }
3149
+ if (arguments_.command === "invoke") {
3150
+ if (arguments_.duplicateRiskOf.length > 0 && !arguments_.preview) {
3151
+ throw new Error("--duplicate-risk-of requires an explicit --preview");
3152
+ }
3153
+ const invocation = prepareInvocation(
3154
+ arguments_.adapterId,
3155
+ arguments_.operationId,
3156
+ await readInput(arguments_.inputSource),
3157
+ arguments_.authId,
3158
+ environment,
3159
+ dependencies.providerPluginRegistry,
3160
+ );
3161
+ const operation = invocation.manifest.operations[arguments_.operationId];
3162
+ if (operation === undefined) throw new Error("operation disappeared before invocation");
3163
+ if (operation.risk === "R4") {
3164
+ throw new Error("R4 capabilities are blocked by wrench");
3165
+ }
3166
+ if (arguments_.cacheOnly && operation.risk !== "R1") {
3167
+ throw new Error("invoke --cache-only is available only for R1 capabilities");
3168
+ }
3169
+ if (arguments_.projectionIdentityOnly && operation.risk !== "R1") {
3170
+ throw new Error("invoke --projection-identity-only is available only for R1 capabilities");
3171
+ }
3172
+ if (operation.risk === "R1") {
3173
+ if (arguments_.duplicateRiskOf.length > 0) {
3174
+ throw new Error(
3175
+ "--duplicate-risk-of is available only for one-dispatch R3 web-session posts.publish mutations",
3176
+ );
3177
+ }
3178
+ if (arguments_.projectionIdentityOnly) {
3179
+ const inputHash = sha256(canonicalJson(invocation.input));
3180
+ const authIdentity = invocation.readProjectionAuthIdentityHash;
3181
+ if (
3182
+ authIdentity === undefined
3183
+ || !/^[a-f0-9]{64}$/u.test(authIdentity)
3184
+ ) {
3185
+ throw new Error(
3186
+ "prepared invocation is missing its auth lifetime identity; prepare it again",
3187
+ );
3188
+ }
3189
+ const authHash = sha256(canonicalJson(invocation.auth));
3190
+ if (invocation.auth.subject === undefined) {
3191
+ const view = {
3192
+ ok: true,
3193
+ source: "projection-identity",
3194
+ status: "unbound",
3195
+ authIdentity,
3196
+ authHash,
3197
+ inputHash,
3198
+ };
3199
+ if (arguments_.json) output.stdout(exactTerminalJson(view));
3200
+ else print(output, view, false);
3201
+ return 0;
3202
+ }
3203
+ const query = createReadProjectionQueryForInvocation(
3204
+ invocation,
3205
+ environment,
3206
+ dependencies.providerPluginRegistry,
3207
+ );
3208
+ const view = {
3209
+ ok: true,
3210
+ source: "projection-identity",
3211
+ status: "ready",
3212
+ authIdentity,
3213
+ authHash,
3214
+ inputHash,
3215
+ projection: { key: query.key },
3216
+ };
3217
+ if (arguments_.json) output.stdout(exactTerminalJson(view));
3218
+ else print(output, view, false);
3219
+ return 0;
3220
+ }
3221
+ if (arguments_.preview) {
3222
+ printPreview(output, directPreviewView(invocation), arguments_.json);
3223
+ return 0;
3224
+ }
3225
+ if (arguments_.cacheOnly) {
3226
+ const result = dependencies.readCachedPreparedCapability(invocation, {
3227
+ environment,
3228
+ registry: dependencies.providerPluginRegistry,
3229
+ });
3230
+ const view = cachedInvocationView(result);
3231
+ if (arguments_.json) output.stdout(exactTerminalJson(view));
3232
+ else print(output, view, false);
3233
+ return result.status === "hit" ? 0 : 3;
3234
+ }
3235
+ const result = await dependencies.revalidatePreparedCapability(
3236
+ invocation,
3237
+ {
3238
+ headed: arguments_.headed,
3239
+ environment,
3240
+ registry: dependencies.providerPluginRegistry,
3241
+ ...(signal === undefined ? {} : { signal }),
3242
+ },
3243
+ );
3244
+ const view = revalidatedInvocationView(result);
3245
+ if (arguments_.json) output.stdout(exactTerminalJson(view));
3246
+ else print(output, view, false);
3247
+ return result.live.receipt.status === "succeeded" || result.live.receipt.status === "submitted" ? 0 : result.live.receipt.status === "indeterminate" ? 5 : 3;
3248
+ }
3249
+ const stored = createAndSaveInvocationPlan(
3250
+ invocation,
3251
+ environment,
3252
+ new Date(),
3253
+ dependencies.providerPluginRegistry,
3254
+ { duplicateRiskOf: arguments_.duplicateRiskOf },
3255
+ );
3256
+ printPreview(
3257
+ output,
3258
+ previewView(stored, invocation, arguments_.headed),
3259
+ arguments_.json,
3260
+ );
3261
+ return arguments_.preview ? 0 : 4;
3262
+ }
3263
+ if (arguments_.command === "confirm") {
3264
+ const selectedPlan = loadInvocationPlan(arguments_.digest, environment);
3265
+ if (selectedPlan.plan.messagingComposite !== undefined) {
3266
+ if (
3267
+ selectedPlan.plan.messagingComposite.contextBindingSha256 === null
3268
+ || selectedPlan.plan.messagingComposite.sourceConversationCoordinateSha256
3269
+ === null
3270
+ ) throw new Error(
3271
+ "predecessor messaging plan lacks current context evidence; preview the action again",
3272
+ );
3273
+ if (
3274
+ arguments_.privateOutput === undefined
3275
+ || arguments_.receiptBindingOutput === undefined
3276
+ ) throw new Error(
3277
+ "messaging confirmation requires --private-output and --receipt-binding-output",
3278
+ );
3279
+ loadMessagingPreviewForConfirmationInternal(arguments_.digest, { environment });
3280
+ const privateOutput = validateMessagingPrivateOutputPath(
3281
+ arguments_.privateOutput,
3282
+ environment,
3283
+ );
3284
+ const receiptBindingOutput = validateMessagingPrivateOutputPath(
3285
+ arguments_.receiptBindingOutput,
3286
+ environment,
3287
+ );
3288
+ if (privateOutput === receiptBindingOutput) {
3289
+ throw new Error(
3290
+ "messaging confirmation requires distinct private output and receipt-binding paths",
3291
+ );
3292
+ }
3293
+ const reservations = reserveMessagingPrivateOutputPair(
3294
+ privateOutput,
3295
+ receiptBindingOutput,
3296
+ environment,
3297
+ );
3298
+ const result = await confirmMessagingInvocation(arguments_.digest, {
3299
+ environment,
3300
+ registry: dependencies.providerPluginRegistry,
3301
+ ...(signal === undefined ? {} : { signal }),
3302
+ });
3303
+ // Once dispatch settles, publish its body-free recovery handle before
3304
+ // either reserved private export can fail.
3305
+ output.stdout(exactTerminalJson(result.receipt));
3306
+ writeReservedMessagingPrivateOutput(
3307
+ reservations.run,
3308
+ result.run,
3309
+ environment,
3310
+ );
3311
+ writeReservedMessagingPrivateOutput(
3312
+ reservations.receiptBinding,
3313
+ result.receiptBinding,
3314
+ environment,
3315
+ );
3316
+ return result.receipt.state === "submitted"
3317
+ ? 0
3318
+ : result.receipt.state === "indeterminate"
3319
+ ? 5
3320
+ : 3;
3321
+ }
3322
+ if (
3323
+ arguments_.privateOutput !== undefined
3324
+ || arguments_.receiptBindingOutput !== undefined
3325
+ ) throw new Error("messaging output options require a messaging composite plan");
3326
+ const result = await confirmInvocation(arguments_.digest, {
3327
+ headed: arguments_.headed,
3328
+ environment,
3329
+ registry: dependencies.providerPluginRegistry,
3330
+ ...(signal === undefined ? {} : { signal }),
3331
+ });
3332
+ print(output, invocationView(result), arguments_.json);
3333
+ return result.receipt.status === "succeeded" || result.receipt.status === "submitted" ? 0 : result.receipt.status === "indeterminate" ? 5 : 3;
3334
+ }
3335
+ if (arguments_.command === "runs-list") {
3336
+ repairInterruptedConfirmationClaims(environment);
3337
+ repairInterruptedRunJournals(environment);
3338
+ const receipts = listRunReceipts(environment);
3339
+ print(output, arguments_.json ? { ok: true, runs: receipts } : receipts, arguments_.json);
3340
+ return 0;
3341
+ }
3342
+ if (arguments_.command === "runs-reconcile") {
3343
+ const linkedDeviceJournal = readLinkedDeviceLifecycleJournal(
3344
+ arguments_.runId,
3345
+ environment,
3346
+ );
3347
+ if (linkedDeviceJournal !== null) {
3348
+ if (arguments_.inputSource === undefined) {
3349
+ throw new Error(
3350
+ "linked-device lifecycle reconciliation requires --input with an explicit observed outcome and evidence hash",
3351
+ );
3352
+ }
3353
+ const result = await dependencies
3354
+ .reconcileLinkedDeviceLifecycleJournal(
3355
+ arguments_.runId,
3356
+ await readInput(arguments_.inputSource),
3357
+ {
3358
+ environment,
3359
+ registry: dependencies.providerPluginRegistry,
3360
+ },
3361
+ );
3362
+ print(output, result, arguments_.json);
3363
+ return 0;
3364
+ }
3365
+ const claimRecovery = repairInterruptedConfirmationClaims(environment);
3366
+ const runRecovery = repairInterruptedRunJournals(environment);
3367
+ if (claimRecovery.invalid > 0 || runRecovery.issues.length > 0) {
3368
+ throw new Error(
3369
+ "local execution recovery has unresolved state; run ghostget doctor before reconciliation",
3370
+ );
3371
+ }
3372
+ let receipt: ReturnType<typeof readRunReceipt> | null = null;
3373
+ try {
3374
+ receipt = readRunReceipt(arguments_.runId, environment);
3375
+ } catch {
3376
+ // The transport-specific reconciler remains responsible for reporting
3377
+ // missing, malformed, or legacy receipts. This probe only routes exact
3378
+ // schema-6 receipts to the portable evidence path.
3379
+ }
3380
+ if (
3381
+ receipt?.schemaVersion === 6
3382
+ && receipt.transport === "portable-provider-plugin"
3383
+ ) {
3384
+ if (arguments_.inputSource === undefined) {
3385
+ throw new Error(
3386
+ "portable plugin reconciliation requires --input with an explicit observed outcome and evidence hash",
3387
+ );
3388
+ }
3389
+ const result = dependencies.reconcilePortableProviderPluginRun(
3390
+ arguments_.runId,
3391
+ await readInput(arguments_.inputSource),
3392
+ {
3393
+ environment,
3394
+ registry: dependencies.providerPluginRegistry,
3395
+ },
3396
+ );
3397
+ print(output, result, arguments_.json);
3398
+ return 0;
3399
+ }
3400
+ const result = await dependencies.reconcileWebSessionRun(
3401
+ arguments_.runId,
3402
+ arguments_.inputSource === undefined
3403
+ ? undefined
3404
+ : await readInput(arguments_.inputSource),
3405
+ {
3406
+ environment,
3407
+ registry: dependencies.providerPluginRegistry,
3408
+ },
3409
+ );
3410
+ print(output, result, arguments_.json);
3411
+ return result.ok ? 0 : 5;
3412
+ }
3413
+ if (arguments_.command === "plans-list") {
3414
+ const plans = listInvocationPlans(environment);
3415
+ print(output, arguments_.json ? { ok: true, plans } : plans, arguments_.json);
3416
+ return 0;
3417
+ }
3418
+ if (arguments_.command === "plans-cancel") {
3419
+ if (!arguments_.yes) throw new Error("plans cancel requires --yes; cancellation permanently removes that encrypted preview");
3420
+ const removed = cancelInvocationPlan(arguments_.digest, environment);
3421
+ output.stdout(removed ? `Cancelled preview ${safe(arguments_.digest)}.\n` : `Preview ${safe(arguments_.digest)} was not present.\n`);
3422
+ return 0;
3423
+ }
3424
+ if (arguments_.command === "runs-show") {
3425
+ const messaging = readMessagingRunIfPresent(arguments_.runId, environment);
3426
+ if (messaging !== null) {
3427
+ if (
3428
+ arguments_.privateOutput === undefined
3429
+ || arguments_.receiptBindingOutput === undefined
3430
+ ) throw new Error(
3431
+ "messaging runs show requires --private-output and --receipt-binding-output",
3432
+ );
3433
+ const privateOutput = validateMessagingPrivateOutputPath(
3434
+ arguments_.privateOutput,
3435
+ environment,
3436
+ );
3437
+ const receiptBindingOutput = validateMessagingPrivateOutputPath(
3438
+ arguments_.receiptBindingOutput,
3439
+ environment,
3440
+ );
3441
+ if (privateOutput === receiptBindingOutput) {
3442
+ throw new Error(
3443
+ "messaging runs show requires distinct private output and receipt-binding paths",
3444
+ );
3445
+ }
3446
+ const result = showMessagingRunInternal(arguments_.runId, { environment });
3447
+ writeMessagingPrivateOutput(privateOutput, result.run, environment);
3448
+ writeMessagingPrivateOutput(
3449
+ receiptBindingOutput,
3450
+ result.receiptBinding,
3451
+ environment,
3452
+ );
3453
+ output.stdout(exactTerminalJson(result.receipt));
3454
+ return result.receipt.state === "indeterminate" ? 5 : result.receipt.state === "submitted" ? 0 : 3;
3455
+ }
3456
+ if (
3457
+ arguments_.privateOutput !== undefined
3458
+ || arguments_.receiptBindingOutput !== undefined
3459
+ ) throw new Error("messaging output options require a messaging run");
3460
+ }
3461
+ repairInterruptedConfirmationClaims(environment);
3462
+ repairInterruptedRunJournals(environment);
3463
+ const receipt = readRunReceipt(arguments_.runId, environment);
3464
+ print(output, receipt, arguments_.json);
3465
+ return 0;
3466
+ }
3467
+
3468
+ export async function main(
3469
+ rawArguments: readonly string[] = process.argv.slice(2),
3470
+ environment: Readonly<Record<string, string | undefined>> = process.env,
3471
+ output: Output = defaultOutput,
3472
+ dependencyOverrides: Partial<GhostgetDependencies> = {},
3473
+ signal?: AbortSignal,
3474
+ ): Promise<number> {
3475
+ const parsed = parseGhostgetArguments(rawArguments);
3476
+ if (!parsed.ok) {
3477
+ output.stderr(
3478
+ `${safe(parsed.message)}\n\n${renderGhostgetUsage()}`,
3479
+ );
3480
+ return 2;
3481
+ }
3482
+ try {
3483
+ let dependencies = resolveDependencies(dependencyOverrides);
3484
+ if (
3485
+ dependencyOverrides.providerPluginRegistry === undefined
3486
+ && commandUsesPortableProviderCatalog(parsed.value.command)
3487
+ ) {
3488
+ dependencies = {
3489
+ ...dependencies,
3490
+ providerPluginRegistry:
3491
+ dependencies.createPortableProviderPluginCatalog(
3492
+ dependencies.providerPluginRegistry,
3493
+ environment,
3494
+ ).registry,
3495
+ };
3496
+ }
3497
+ return await runCommand(
3498
+ parsed.value,
3499
+ environment,
3500
+ output,
3501
+ dependencies,
3502
+ signal,
3503
+ );
3504
+ } catch (error) {
3505
+ if (error instanceof LinkedDeviceLifecycleIndeterminateError) {
3506
+ const failure = {
3507
+ ok: false,
3508
+ status: "indeterminate",
3509
+ journalId: error.journalId,
3510
+ };
3511
+ if ("json" in parsed.value && parsed.value.json === true) {
3512
+ output.stdout(safeJson(failure));
3513
+ } else {
3514
+ output.stderr(`ghostget: ${safe(error.message)}\n`);
3515
+ }
3516
+ return 5;
3517
+ }
3518
+ if (error instanceof PreservedBrowserArtifactsError) {
3519
+ const recoveryHandle = boundedCliRecoveryHandle(error.recoveryHandle);
3520
+ const failure = {
3521
+ ok: false,
3522
+ status: "indeterminate",
3523
+ privateArtifactsPreserved: true,
3524
+ error: safe(error.message),
3525
+ ...(recoveryHandle === null ? {} : { recoveryHandle }),
3526
+ };
3527
+ if ("json" in parsed.value && parsed.value.json === true) {
3528
+ // The recovery handle is an owned, bounded terminal-safe locator.
3529
+ // Generic redaction treats its `session=` field as a credential and
3530
+ // would destroy the only actionable cleanup handle.
3531
+ output.stdout(exactTerminalJson(failure));
3532
+ } else {
3533
+ output.stderr(
3534
+ `ghostget: ${failure.error}${recoveryHandle === null
3535
+ ? "; recovery handle was unavailable; run ghostget doctor before retrying"
3536
+ : `; recovery handle: ${recoveryHandle}`}\n`,
3537
+ );
3538
+ }
3539
+ return 5;
3540
+ }
3541
+ output.stderr(
3542
+ `ghostget: ${safe(error instanceof Error ? error.message : String(error))}\n`,
3543
+ );
3544
+ return 3;
3545
+ }
3546
+ }
3547
+
3548
+ function commandUsesPortableProviderCatalog(
3549
+ command: GhostgetArguments["command"],
3550
+ ): boolean {
3551
+ return command === "doctor"
3552
+ || command === "capabilities"
3553
+ || command === "adapter-validate"
3554
+ || command === "derive-analyze"
3555
+ || command === "derive-finish"
3556
+ || command === "thread-publish"
3557
+ || command === "auth-bind"
3558
+ || command === "auth-pair"
3559
+ || command === "auth-sync"
3560
+ || command === "omni-read"
3561
+ || command === "messaging-routes"
3562
+ || command === "messaging-resolve"
3563
+ || command === "messaging-context"
3564
+ || command === "messaging-preview"
3565
+ || command === "invoke"
3566
+ || command === "confirm"
3567
+ || command === "runs-reconcile";
3568
+ }
3569
+
3570
+ const GHOSTGET_PROCESS_CLEANUP_PERSISTENCE_MARGIN_MS = 5_000;
3571
+ const GHOSTGET_PROCESS_GRACEFUL_TERMINATION_TIMEOUT_MS =
3572
+ WEB_SESSION_CLEANUP_JOIN_TIMEOUT_MS
3573
+ + GHOSTGET_PROCESS_CLEANUP_PERSISTENCE_MARGIN_MS;
3574
+
3575
+ type GhostgetProcessBoundaryDependencies = {
3576
+ readonly rawArguments: readonly string[];
3577
+ readonly environment: Readonly<Record<string, string | undefined>>;
3578
+ readonly output: Output;
3579
+ readonly runMain: typeof main;
3580
+ readonly subscribeTermination: (
3581
+ terminate: (signal: NodeJS.Signals) => void,
3582
+ ) => () => void;
3583
+ readonly scheduleForcedTermination: (
3584
+ callback: () => void,
3585
+ delayMs: number,
3586
+ ) => () => void;
3587
+ readonly resendSignal: (signal: NodeJS.Signals) => void;
3588
+ readonly setExitCode: (code: number) => void;
3589
+ };
3590
+
3591
+ function subscribeProcessTermination(
3592
+ terminate: (signal: NodeJS.Signals) => void,
3593
+ ): () => void {
3594
+ const onInterrupt = (): void => terminate("SIGINT");
3595
+ const onTerminate = (): void => terminate("SIGTERM");
3596
+ process.once("SIGINT", onInterrupt);
3597
+ process.once("SIGTERM", onTerminate);
3598
+ let subscribed = true;
3599
+ return () => {
3600
+ if (!subscribed) return;
3601
+ subscribed = false;
3602
+ process.removeListener("SIGINT", onInterrupt);
3603
+ process.removeListener("SIGTERM", onTerminate);
3604
+ };
3605
+ }
3606
+
3607
+ function scheduleForcedTermination(
3608
+ callback: () => void,
3609
+ delayMs: number,
3610
+ ): () => void {
3611
+ const timer = setTimeout(callback, delayMs);
3612
+ timer.unref();
3613
+ return () => clearTimeout(timer);
3614
+ }
3615
+
3616
+ export async function runGhostgetProcess(
3617
+ overrides: Partial<GhostgetProcessBoundaryDependencies> = {},
3618
+ ): Promise<void> {
3619
+ const dependencies: GhostgetProcessBoundaryDependencies = {
3620
+ rawArguments: overrides.rawArguments ?? process.argv.slice(2),
3621
+ environment: overrides.environment ?? process.env,
3622
+ output: overrides.output ?? defaultOutput,
3623
+ runMain: overrides.runMain ?? main,
3624
+ subscribeTermination:
3625
+ overrides.subscribeTermination ?? subscribeProcessTermination,
3626
+ scheduleForcedTermination:
3627
+ overrides.scheduleForcedTermination ?? scheduleForcedTermination,
3628
+ resendSignal:
3629
+ overrides.resendSignal ?? ((signal) => process.kill(process.pid, signal)),
3630
+ setExitCode:
3631
+ overrides.setExitCode ?? ((code) => {
3632
+ process.exitCode = code;
3633
+ }),
3634
+ };
3635
+ const controller = new AbortController();
3636
+ const terminationState: {
3637
+ cancelFallback: (() => void) | null;
3638
+ } = { cancelFallback: null };
3639
+ let unsubscribeTermination = (): void => undefined;
3640
+ let terminating = false;
3641
+ const terminate = (signal: NodeJS.Signals): void => {
3642
+ if (terminating) return;
3643
+ terminating = true;
3644
+ controller.abort();
3645
+ unsubscribeTermination();
3646
+ // Not every upstream browser/capture primitive accepts AbortSignal yet.
3647
+ // Leave a hard upper bound, but reserve enough time for the browser's
3648
+ // bounded TERM/KILL/stream, close, proxy, and recovery-handle joins. A
3649
+ // second OS signal uses its restored default disposition immediately.
3650
+ terminationState.cancelFallback = dependencies.scheduleForcedTermination(
3651
+ () => dependencies.resendSignal(signal),
3652
+ GHOSTGET_PROCESS_GRACEFUL_TERMINATION_TIMEOUT_MS,
3653
+ );
3654
+ };
3655
+ unsubscribeTermination = dependencies.subscribeTermination(terminate);
3656
+ try {
3657
+ dependencies.setExitCode(await dependencies.runMain(
3658
+ dependencies.rawArguments,
3659
+ dependencies.environment,
3660
+ dependencies.output,
3661
+ {},
3662
+ controller.signal,
3663
+ ));
3664
+ } finally {
3665
+ terminationState.cancelFallback?.();
3666
+ unsubscribeTermination();
3667
+ }
3668
+ }
3669
+
3670
+ if (import.meta.main) {
3671
+ await runGhostgetProcess();
3672
+ }