@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,503 @@
1
+ /**
2
+ * ClasificadosOnline public rental-list policy. Callers choose a locality and
3
+ * optional bed/price bounds. Ghostget owns the reviewed list URL, query names,
4
+ * card projection, and neighborhood verification.
5
+ */
6
+
7
+ import type { WebSessionContract } from "../web-session-contract-definitions";
8
+ import {
9
+ extractPuertoRicoZip,
10
+ listingMatchesSearchFilters,
11
+ projectRentalListing,
12
+ projectRentalListingsSearch,
13
+ rentalListingCoordinates,
14
+ type RentalListing,
15
+ type RentalListingsSearch,
16
+ type RentalListingsSearchInput,
17
+ } from "./rental-listings";
18
+
19
+ export const CLASIFICADOS_WEB_OPERATION_NAMES = Object.freeze([
20
+ "listings.search",
21
+ ] as const);
22
+
23
+ export type ClasificadosWebOperationName =
24
+ (typeof CLASIFICADOS_WEB_OPERATION_NAMES)[number];
25
+
26
+ export const CLASIFICADOS_WEB_OPERATIONS = Object.freeze({
27
+ "listings.search": Object.freeze({
28
+ effect: "read" as const,
29
+ risk: "R1" as const,
30
+ state: "observed" as const,
31
+ reason:
32
+ "fixed credential-free rental-list GET binds reviewed pueblo values, projects each comment-marked card from schema.org and list-row hidden coordinates, then verifies neighborhood from street, ZIP, known address, or those coordinates",
33
+ }),
34
+ });
35
+
36
+ export const CLASIFICADOS_ORIGIN = "https://www.clasificadosonline.com";
37
+ export const CLASIFICADOS_LIST_PATH = "/UDRentalsListingAdv.asp";
38
+ export const CLASIFICADOS_DETAIL_PATH = "/UDRentalsDetail.asp";
39
+ export const CLASIFICADOS_PAGE_SIZE = 15;
40
+ export const CLASIFICADOS_MAX_PAGES_PER_PUEBLO = 1;
41
+ export const CLASIFICADOS_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
42
+
43
+ export const CLASIFICADOS_SAN_JUAN_PUEBLOS = Object.freeze([
44
+ "San Juan - Condado-Miramar",
45
+ "San Juan - Hato Rey",
46
+ "San Juan - Río Piedras",
47
+ "San Juan - Santurce",
48
+ "San Juan - Viejo SJ",
49
+ ] as const);
50
+
51
+ export type ClasificadosPueblo = (typeof CLASIFICADOS_SAN_JUAN_PUEBLOS)[number];
52
+
53
+ export const CLASIFICADOS_LISTINGS_SEARCH_INPUT = Object.freeze({
54
+ properties: Object.freeze({
55
+ location: Object.freeze({
56
+ type: "string" as const,
57
+ description: "Exact rental search locality",
58
+ minLength: 1,
59
+ maxLength: 80,
60
+ }),
61
+ beds_min: Object.freeze({
62
+ type: "number" as const,
63
+ description: "Minimum bedroom count",
64
+ minimum: 0,
65
+ maximum: 12,
66
+ }),
67
+ max_price: Object.freeze({
68
+ type: "number" as const,
69
+ description: "Maximum monthly rent in USD",
70
+ minimum: 1,
71
+ maximum: 100_000,
72
+ }),
73
+ }),
74
+ required: Object.freeze(["location"]),
75
+ });
76
+
77
+ export const CLASIFICADOS_LISTINGS_SEARCH_CONTRACT: WebSessionContract =
78
+ Object.freeze({
79
+ site: "clasificados",
80
+ operation: "listings.search",
81
+ contractVersion: 1,
82
+ risk: "R1",
83
+ input: CLASIFICADOS_LISTINGS_SEARCH_INPUT,
84
+ sideEffect: "none",
85
+ idempotency: "none",
86
+ dedupeWindowMs: 0,
87
+ state: "observed",
88
+ dispatch: "none",
89
+ implementation: CLASIFICADOS_WEB_OPERATIONS["listings.search"].reason,
90
+ });
91
+
92
+ const STREET_PATTERN =
93
+ /\b(?:calle|ave(?:nida)?\.?|av\.?)\s+[A-Za-zÁÉÍÓÚÑáéíóúñ0-9.']+(?:\s+[A-Za-zÁÉÍÓÚÑáéíóúñ0-9.']+){0,5}(?:\s+\d{1,5})?/iu;
94
+ const NUMBERED_STREET_PATTERN =
95
+ /\b\d{1,5}\s+(?:calle|ave(?:nida)?\.?|av\.?|luis\s+mu[nñ]oz\s+rivera)(?:\s+[A-Za-zÁÉÍÓÚÑáéíóúñ0-9.']+){0,4}/iu;
96
+
97
+ function foldLocation(value: string): string {
98
+ return value
99
+ .normalize("NFKD")
100
+ .replace(/\p{M}/gu, "")
101
+ .toLowerCase()
102
+ .replace(/[^a-z0-9]+/gu, " ")
103
+ .trim();
104
+ }
105
+
106
+ export function clasificadosPueblosForLocation(
107
+ location: string,
108
+ ): readonly ClasificadosPueblo[] {
109
+ const folded = foldLocation(location);
110
+ if (
111
+ folded === "san juan"
112
+ || folded === "san juan pr"
113
+ || folded === "san juan puerto rico"
114
+ ) {
115
+ return CLASIFICADOS_SAN_JUAN_PUEBLOS;
116
+ }
117
+ if (folded === "hato rey" || folded === "san juan hato rey") {
118
+ return Object.freeze(["San Juan - Hato Rey"]);
119
+ }
120
+ if (
121
+ folded === "condado"
122
+ || folded === "miramar"
123
+ || folded === "condado miramar"
124
+ || folded === "san juan condado miramar"
125
+ ) {
126
+ return Object.freeze(["San Juan - Condado-Miramar"]);
127
+ }
128
+ if (folded === "santurce" || folded === "san juan santurce") {
129
+ return Object.freeze(["San Juan - Santurce"]);
130
+ }
131
+ if (folded === "rio piedras" || folded === "san juan rio piedras") {
132
+ return Object.freeze(["San Juan - Río Piedras"]);
133
+ }
134
+ if (
135
+ folded === "viejo san juan"
136
+ || folded === "old san juan"
137
+ || folded === "osj"
138
+ || folded === "san juan viejo sj"
139
+ ) {
140
+ return Object.freeze(["San Juan - Viejo SJ"]);
141
+ }
142
+ throw new Error("listings.search location is not one reviewed Puerto Rico rental locality");
143
+ }
144
+
145
+ function encodeLatin1QueryComponent(value: string): string {
146
+ let encoded = "";
147
+ for (const character of value) {
148
+ const code = character.codePointAt(0);
149
+ if (code === undefined || code > 255) {
150
+ throw new Error("Clasificados query value is outside the reviewed latin-1 set");
151
+ }
152
+ if (
153
+ (code >= 0x30 && code <= 0x39)
154
+ || (code >= 0x41 && code <= 0x5a)
155
+ || (code >= 0x61 && code <= 0x7a)
156
+ || character === "-"
157
+ || character === "."
158
+ || character === "_"
159
+ ) {
160
+ encoded += character;
161
+ continue;
162
+ }
163
+ if (character === " ") {
164
+ encoded += "+";
165
+ continue;
166
+ }
167
+ encoded += `%${code.toString(16).toUpperCase().padStart(2, "0")}`;
168
+ }
169
+ return encoded;
170
+ }
171
+
172
+ function clasificadosFormQuery(
173
+ fields: Readonly<Record<string, string>>,
174
+ ): string {
175
+ return Object.entries(fields)
176
+ .map(([key, value]) => `${key}=${encodeLatin1QueryComponent(value)}`)
177
+ .join("&");
178
+ }
179
+
180
+ export function clasificadosListUrl(
181
+ pueblo: ClasificadosPueblo,
182
+ input: RentalListingsSearchInput,
183
+ offset = 0,
184
+ ): URL {
185
+ const query = clasificadosFormQuery({
186
+ RentalsPueblos: pueblo,
187
+ Category: "Apartamento",
188
+ ...(input.max_price === undefined ? {} : { HighPrice: String(input.max_price) }),
189
+ ...(offset > 0 ? { offset: String(offset) } : {}),
190
+ });
191
+ return new URL(`${CLASIFICADOS_LIST_PATH}?${query}`, CLASIFICADOS_ORIGIN);
192
+ }
193
+
194
+ export function clasificadosSearchTargetUrl(
195
+ location: string,
196
+ input: RentalListingsSearchInput,
197
+ ): string {
198
+ const pueblos = clasificadosPueblosForLocation(location);
199
+ if (pueblos.length === 1 && pueblos[0] !== undefined) {
200
+ return clasificadosListUrl(pueblos[0], input).href;
201
+ }
202
+ return clasificadosListUrl("San Juan - Hato Rey", input).href;
203
+ }
204
+
205
+ export function clasificadosDetailUrl(id: string): string {
206
+ if (!/^[0-9]{1,16}$/u.test(id)) {
207
+ throw new Error("Clasificados listing id must be one numeric classified identifier");
208
+ }
209
+ const url = new URL(CLASIFICADOS_DETAIL_PATH, CLASIFICADOS_ORIGIN);
210
+ url.searchParams.set("ReForRentAdID", id);
211
+ return url.href;
212
+ }
213
+
214
+ function decodeHtmlEntities(value: string): string {
215
+ return value
216
+ .replace(/&nbsp;/giu, " ")
217
+ .replace(/&amp;/giu, "&")
218
+ .replace(/&quot;/giu, "\"")
219
+ .replace(/&#39;/giu, "'")
220
+ .replace(/&lt;/giu, "<")
221
+ .replace(/&gt;/giu, ">")
222
+ .replace(/&iacute;/giu, "í")
223
+ .replace(/&oacute;/giu, "ó")
224
+ .replace(/&aacute;/giu, "á")
225
+ .replace(/&eacute;/giu, "é")
226
+ .replace(/&uacute;/giu, "ú")
227
+ .replace(/&ntilde;/giu, "ñ")
228
+ .replace(/&#(\d+);/gu, (_, code) => String.fromCharCode(Number(code)));
229
+ }
230
+
231
+ function collapseWhitespace(value: string): string {
232
+ return decodeHtmlEntities(value).replace(/\s+/gu, " ").trim();
233
+ }
234
+
235
+ function metaContent(html: string, itemprop: string): string | null {
236
+ const pattern = new RegExp(
237
+ `itemprop="${itemprop}"\\s+content="([^"]*)"`,
238
+ "iu",
239
+ );
240
+ const match = pattern.exec(html);
241
+ if (match?.[1] === undefined) return null;
242
+ const value = collapseWhitespace(match[1]);
243
+ return value.length > 0 ? value : null;
244
+ }
245
+
246
+ function hiddenClassValue(html: string, className: string): string | null {
247
+ const pattern = new RegExp(
248
+ `<input\\s+type="hidden"\\s+class="${className}"\\s+value="([^"]*)"`,
249
+ "iu",
250
+ );
251
+ const match = pattern.exec(html);
252
+ if (match?.[1] === undefined) return null;
253
+ const value = collapseWhitespace(match[1]);
254
+ return value.length > 0 ? value : null;
255
+ }
256
+
257
+ function afterIcon(html: string, iconFile: string): string | null {
258
+ const index = html.indexOf(iconFile);
259
+ if (index < 0) return null;
260
+ const tagEnd = html.indexOf(">", index);
261
+ if (tagEnd < 0) return null;
262
+ const match = /^\s*([^<\r\n]+)/u.exec(html.slice(tagEnd + 1));
263
+ if (match?.[1] === undefined) return null;
264
+ const value = collapseWhitespace(match[1]);
265
+ return value.length > 0 ? value : null;
266
+ }
267
+
268
+ function parseCount(value: string, label: string): number {
269
+ if (/^(?:estudio|efficiency|studio)$/iu.test(value)) return 0;
270
+ const half = /^([0-9]{1,2})\s+1\/2$/u.exec(value);
271
+ if (half?.[1] !== undefined) return Number(half[1]) + 0.5;
272
+ const match = /^([0-9]{1,2})(?:\.(5|0|00))?$/u.exec(value);
273
+ if (match?.[1] === undefined) {
274
+ throw new Error(`${label} was not a reviewed room count`);
275
+ }
276
+ return match[2] === "5" ? Number(match[1]) + 0.5 : Number(match[1]);
277
+ }
278
+
279
+ function parseBeds(value: string): number {
280
+ const beds = parseCount(value, "Clasificados beds");
281
+ if (!Number.isSafeInteger(beds)) {
282
+ throw new Error("Clasificados beds was not a reviewed room count");
283
+ }
284
+ return beds;
285
+ }
286
+
287
+ function inferStudioBeds(
288
+ title: string | null,
289
+ description: string | null,
290
+ ): number | null {
291
+ const text = [title, description].filter((part) => part !== null).join(" ");
292
+ return /(?:estudio|efficiency|studio)/iu.test(text) ? 0 : null;
293
+ }
294
+
295
+ function puebloFromCard(html: string): string | null {
296
+ const match = /RentalsPueblos=([^"]+)"/u.exec(html);
297
+ if (match?.[1] === undefined) return null;
298
+ const pueblo = collapseWhitespace(decodeURIComponent(match[1].replace(/\+/gu, " ")));
299
+ return pueblo.length > 0 ? pueblo : null;
300
+ }
301
+
302
+ function listingIdFromCard(html: string, detailUrl: string | null): string {
303
+ const fromUrl = /ReForRentAdID=([0-9]{1,16})/u.exec(detailUrl ?? "");
304
+ if (fromUrl?.[1] !== undefined) return fromUrl[1];
305
+ const fromHref = /UDRentalsDetail\.asp\?ReForRentAdID=([0-9]{1,16})/u.exec(html);
306
+ if (fromHref?.[1] === undefined) {
307
+ throw new Error("Clasificados card did not bind one listing identifier");
308
+ }
309
+ return fromHref[1];
310
+ }
311
+
312
+ function exactDetailUrl(id: string, raw: string | null): string {
313
+ const expected = clasificadosDetailUrl(id);
314
+ if (raw === null) return expected;
315
+ let url: URL;
316
+ try {
317
+ url = new URL(raw, CLASIFICADOS_ORIGIN);
318
+ } catch {
319
+ throw new Error("Clasificados card detail URL was invalid");
320
+ }
321
+ if (
322
+ url.origin !== CLASIFICADOS_ORIGIN
323
+ || url.pathname !== CLASIFICADOS_DETAIL_PATH
324
+ || url.searchParams.get("ReForRentAdID") !== id
325
+ || [...url.searchParams.keys()].join(",") !== "ReForRentAdID"
326
+ || url.username !== ""
327
+ || url.password !== ""
328
+ || url.hash !== ""
329
+ ) {
330
+ throw new Error("Clasificados card detail URL drifted");
331
+ }
332
+ return expected;
333
+ }
334
+
335
+ function extractStreetAddress(
336
+ title: string | null,
337
+ description: string | null,
338
+ ): string | null {
339
+ for (const text of [title, description]) {
340
+ if (text === null) continue;
341
+ const numbered = NUMBERED_STREET_PATTERN.exec(text);
342
+ if (numbered?.[0] !== undefined) return collapseWhitespace(numbered[0]);
343
+ const named = STREET_PATTERN.exec(text);
344
+ if (named?.[0] !== undefined) return collapseWhitespace(named[0]);
345
+ }
346
+ return null;
347
+ }
348
+
349
+ function cardChunks(html: string): readonly string[] {
350
+ const starts = [
351
+ ...html.matchAll(/<!--\s*Start:\s*Classified row\s*-->/gu),
352
+ ].map((match) => match.index);
353
+ const chunks: string[] = [];
354
+ for (let index = 0; index < starts.length; index += 1) {
355
+ const start = starts[index];
356
+ const next = starts[index + 1] ?? html.length;
357
+ if (start === undefined) continue;
358
+ chunks.push(html.slice(start, next));
359
+ }
360
+ return Object.freeze(chunks);
361
+ }
362
+
363
+ export function projectClasificadosListingCard(
364
+ html: string,
365
+ expectedPueblo?: ClasificadosPueblo,
366
+ ): RentalListing {
367
+ const title = metaContent(html, "name");
368
+ const priceText = hiddenClassValue(html, "Price") ?? metaContent(html, "price");
369
+ const description = metaContent(html, "description");
370
+ const bedsText = afterIcon(html, "icon_cuartos.png")
371
+ ?? (inferStudioBeds(title, description) === 0 ? "Efficiency" : null);
372
+ const bathsText = afterIcon(html, "icon_bano.png");
373
+ const rawLat = hiddenClassValue(html, "Lat");
374
+ const rawLon = hiddenClassValue(html, "Lon");
375
+ const rawDetail = hiddenClassValue(html, "DetailUrl");
376
+ const pueblo = puebloFromCard(html);
377
+ if (title === null || priceText === null || bedsText === null || bathsText === null) {
378
+ throw new Error("Clasificados card was missing a reviewed listing field");
379
+ }
380
+ if (pueblo === null) {
381
+ throw new Error("Clasificados card did not bind one pueblo");
382
+ }
383
+ if (expectedPueblo !== undefined && pueblo !== expectedPueblo) {
384
+ throw new Error("Clasificados card pueblo did not match the requested list");
385
+ }
386
+ if (!pueblo.startsWith("San Juan")) {
387
+ throw new Error("Clasificados card is outside the requested San Juan locality");
388
+ }
389
+ const id = listingIdFromCard(html, rawDetail);
390
+ const rent = Number(priceText.replace(/[^0-9]/gu, ""));
391
+ let coordinates = null;
392
+ if (rawLat !== null && rawLon !== null) {
393
+ try {
394
+ coordinates = rentalListingCoordinates(Number(rawLat), Number(rawLon));
395
+ } catch {
396
+ coordinates = null;
397
+ }
398
+ }
399
+ const streetAddress = extractStreetAddress(title, description);
400
+ const zip = extractPuertoRicoZip(`${title} ${description ?? ""}`);
401
+ return projectRentalListing({
402
+ id,
403
+ url: exactDetailUrl(id, rawDetail),
404
+ rent,
405
+ beds: parseBeds(bedsText),
406
+ baths: parseCount(bathsText, "Clasificados baths"),
407
+ streetAddress,
408
+ zip,
409
+ coordinates,
410
+ buildingText: [title, description].filter((part) => part !== null).join(" "),
411
+ });
412
+ }
413
+
414
+ function skippableCardError(error: unknown): boolean {
415
+ if (!(error instanceof Error)) return false;
416
+ return error.message === "Clasificados card pueblo did not match the requested list"
417
+ || error.message === "Clasificados card is outside the requested San Juan locality"
418
+ || error.message === "Clasificados card was missing a reviewed listing field"
419
+ || error.message === "Clasificados beds was not a reviewed room count"
420
+ || error.message === "Clasificados baths was not a reviewed room count"
421
+ || error.message === "Clasificados card did not bind one pueblo"
422
+ || error.message === "Clasificados card did not bind one listing identifier"
423
+ || error.message === "Clasificados card detail URL was invalid"
424
+ || error.message === "Clasificados card detail URL drifted";
425
+ }
426
+
427
+ export function projectClasificadosListPage(
428
+ html: string,
429
+ expectedPueblo: ClasificadosPueblo,
430
+ input: RentalListingsSearchInput,
431
+ ): {
432
+ readonly listings: readonly RentalListing[];
433
+ readonly pageFull: boolean;
434
+ readonly skippedCard: boolean;
435
+ } {
436
+ if (!/<!--\s*Start:\s*Classified row\s*-->/u.test(html)) {
437
+ if (/UDRentalsListingAdv\.asp/u.test(html) || /RentalsPueblos/u.test(html)) {
438
+ return Object.freeze({
439
+ listings: Object.freeze([]),
440
+ pageFull: false,
441
+ skippedCard: false,
442
+ });
443
+ }
444
+ throw new Error("Clasificados list page did not match the reviewed rental-list contract");
445
+ }
446
+ const listings: RentalListing[] = [];
447
+ const chunks = cardChunks(html);
448
+ let readable = 0;
449
+ let skippedCard = false;
450
+ for (const chunk of chunks) {
451
+ try {
452
+ const listing = projectClasificadosListingCard(chunk, expectedPueblo);
453
+ readable += 1;
454
+ if (listingMatchesSearchFilters(listing, input)) listings.push(listing);
455
+ } catch (error) {
456
+ if (skippableCardError(error)) {
457
+ skippedCard = true;
458
+ continue;
459
+ }
460
+ throw error;
461
+ }
462
+ }
463
+ if (chunks.length > 0 && readable === 0) {
464
+ throw new Error("Clasificados list page did not project any reviewed listing cards");
465
+ }
466
+ return Object.freeze({
467
+ listings: Object.freeze(listings),
468
+ pageFull: chunks.length >= CLASIFICADOS_PAGE_SIZE,
469
+ skippedCard,
470
+ });
471
+ }
472
+
473
+ export function projectClasificadosListingsSearch(
474
+ pages: readonly {
475
+ readonly pueblo: ClasificadosPueblo;
476
+ readonly html: string;
477
+ }[],
478
+ input: RentalListingsSearchInput,
479
+ observedAt: string,
480
+ ): RentalListingsSearch {
481
+ const listings: RentalListing[] = [];
482
+ const ids = new Set<string>();
483
+ let pageFull = false;
484
+ let skippedCard = false;
485
+ for (const page of pages) {
486
+ const projected = projectClasificadosListPage(page.html, page.pueblo, input);
487
+ pageFull = pageFull || projected.pageFull;
488
+ skippedCard = skippedCard || projected.skippedCard;
489
+ for (const listing of projected.listings) {
490
+ if (ids.has(listing.id)) continue;
491
+ ids.add(listing.id);
492
+ listings.push(listing);
493
+ }
494
+ }
495
+ return projectRentalListingsSearch({
496
+ provider: "clasificados",
497
+ location: input.location,
498
+ url: clasificadosSearchTargetUrl(input.location, input),
499
+ observedAt,
500
+ completeness: pageFull || skippedCard ? "partial" : "complete",
501
+ listings,
502
+ });
503
+ }