six-degrees 0.4.7 → 0.4.9

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 (280) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +2 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/route.js +2 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/scraper/route.js +2 -2
  40. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/settings/route.js +3 -2
  42. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +2 -1
  44. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +2 -1
  46. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/social/messages/route/app-paths-manifest.json +3 -0
  49. package/.next/standalone/.next/server/app/api/social/messages/route/build-manifest.json +12 -0
  50. package/.next/standalone/.next/server/app/api/social/messages/route/server-reference-manifest.json +4 -0
  51. package/.next/standalone/.next/server/app/api/social/messages/route.js +9 -0
  52. package/.next/standalone/.next/server/app/api/social/messages/route.js.map +5 -0
  53. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -0
  54. package/.next/standalone/.next/server/app/api/social/messages/route_client-reference-manifest.js +3 -0
  55. package/.next/standalone/.next/server/app/api/social/route.js +4 -2
  56. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
  64. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/app/import.html +1 -1
  67. package/.next/standalone/.next/server/app/import.rsc +15 -15
  68. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  69. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  70. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  71. package/.next/standalone/.next/server/app/index.html +1 -1
  72. package/.next/standalone/.next/server/app/index.rsc +15 -15
  73. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  74. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  75. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  76. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  78. package/.next/standalone/.next/server/app/launch.html +2 -2
  79. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  80. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  81. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  82. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  83. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  87. package/.next/standalone/.next/server/app/paths.html +1 -1
  88. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  89. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  90. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  91. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  92. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  93. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  94. package/.next/standalone/.next/server/app/profile.html +1 -1
  95. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  96. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  97. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  98. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  99. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/app/queue.html +1 -1
  102. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  103. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  104. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  105. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  106. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/scores.html +1 -1
  109. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  110. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  111. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  112. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  113. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/settings.html +1 -1
  116. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  117. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  118. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  119. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  120. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app/setup.html +1 -1
  123. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  124. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  125. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  126. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  127. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  128. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  129. package/.next/standalone/.next/server/app/social.html +1 -1
  130. package/.next/standalone/.next/server/app/social.rsc +15 -15
  131. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  132. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  133. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  134. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06mi9k2._.js +3 -0
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +8 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +8 -0
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
  142. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_messages_route_actions_0skh09r.js +3 -0
  146. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +8 -0
  147. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
  148. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +1 -1
  149. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +1 -1
  150. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +1 -1
  151. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +1 -1
  152. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +1 -1
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +1 -1
  155. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +1 -1
  156. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +1 -1
  157. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +1 -1
  158. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +1 -1
  159. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +1 -1
  160. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +1 -1
  161. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +1 -1
  162. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +3 -0
  163. package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
  165. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
  166. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
  167. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
  168. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
  169. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1feicqq._.js → [root-of-the-server]__0qdeuyo._.js} +1 -1
  170. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
  171. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
  172. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
  173. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jeztd-._.js +1 -1
  174. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
  175. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
  176. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
  177. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +3 -0
  178. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +1 -1
  179. package/.next/standalone/.next/server/chunks/ssr/{_1mj739k._.js → _0l8cxmo._.js} +2 -2
  180. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/ssr/{_0wxbq78._.js → app_components_19c2d9c._.js} +2 -2
  182. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  183. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
  186. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  187. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
  189. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
  190. package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
  191. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1wax83z._.js → node_modules_0h91jdk._.js} +2 -2
  192. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1nkgb45._.js → node_modules_1f8t_rp._.js} +1 -1
  193. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  194. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  195. package/.next/standalone/.next/server/pages/404.html +1 -1
  196. package/.next/standalone/.next/server/pages/500.html +1 -1
  197. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  198. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  199. package/.next/standalone/.next/static/chunks/{2_zu7rmb8flzl.js → 04bbi55urkrd6.js} +1 -1
  200. package/.next/{static/chunks/3q3pmt5vv-4pi.js → standalone/.next/static/chunks/054zdthcnyd2h.js} +1 -1
  201. package/.next/{static/chunks/236f0jtp68j_h.js → standalone/.next/static/chunks/1_mohm2643nxp.js} +1 -1
  202. package/.next/{static/chunks/3zw8bv7-mc4n6.js → standalone/.next/static/chunks/1z_rt4jteewqp.js} +1 -1
  203. package/.next/standalone/.next/static/chunks/24tvdun1cb3g1.js +1 -0
  204. package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +1 -0
  205. package/.next/{static/chunks/0g_p4p7oci18u.js → standalone/.next/static/chunks/2kfhc58df2xyo.js} +1 -1
  206. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  207. package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +1 -0
  208. package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
  209. package/.next/standalone/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
  210. package/.next/standalone/.next/static/media/favicon.23vm2o6v3er2z.ico +0 -0
  211. package/.next/standalone/CHANGELOG.md +72 -0
  212. package/.next/standalone/CONTRIBUTING.md +5 -0
  213. package/.next/standalone/README.md +168 -56
  214. package/.next/standalone/app/api/social/messages/route.js +130 -0
  215. package/.next/standalone/app/api/social/route.js +89 -47
  216. package/.next/standalone/app/components/ScanStatusBar.js +40 -3
  217. package/.next/standalone/app/components/settings/DataSection.js +2 -1
  218. package/.next/standalone/app/favicon.ico +0 -0
  219. package/.next/standalone/app/paths/page.js +27 -18
  220. package/.next/standalone/app/profile/page.js +2 -0
  221. package/.next/standalone/app/queue/page.js +2 -0
  222. package/.next/standalone/app/scores/page.js +2 -0
  223. package/.next/standalone/app/settings/page.js +2 -0
  224. package/.next/standalone/app/setup/page.js +6 -3
  225. package/.next/standalone/app/social/Conversations.js +246 -0
  226. package/.next/standalone/app/social/page.js +90 -9
  227. package/.next/standalone/lib/galaxy-lab.js +6 -1
  228. package/.next/standalone/lib/linkedin-export.js +175 -5
  229. package/.next/standalone/lib/scoring.js +42 -1
  230. package/.next/standalone/lib/social-store.js +73 -0
  231. package/.next/standalone/package-lock.json +2 -2
  232. package/.next/standalone/package.json +1 -1
  233. package/.next/standalone/scripts/build-site.mjs +537 -0
  234. package/.next/standalone/scripts/scrape.py +156 -23
  235. package/.next/standalone/scripts/site-markdown.mjs +188 -0
  236. package/.next/standalone/scripts/site-news.mjs +311 -0
  237. package/.next/static/chunks/{2_zu7rmb8flzl.js → 04bbi55urkrd6.js} +1 -1
  238. package/.next/{standalone/.next/static/chunks/3q3pmt5vv-4pi.js → static/chunks/054zdthcnyd2h.js} +1 -1
  239. package/.next/{standalone/.next/static/chunks/236f0jtp68j_h.js → static/chunks/1_mohm2643nxp.js} +1 -1
  240. package/.next/{standalone/.next/static/chunks/3zw8bv7-mc4n6.js → static/chunks/1z_rt4jteewqp.js} +1 -1
  241. package/.next/static/chunks/24tvdun1cb3g1.js +1 -0
  242. package/.next/static/chunks/2bftn9gubwf0w.js +1 -0
  243. package/.next/{standalone/.next/static/chunks/0g_p4p7oci18u.js → static/chunks/2kfhc58df2xyo.js} +1 -1
  244. package/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  245. package/.next/static/chunks/39csdcx7yv227.js +1 -0
  246. package/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
  247. package/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
  248. package/.next/static/media/favicon.23vm2o6v3er2z.ico +0 -0
  249. package/CHANGELOG.md +72 -0
  250. package/README.md +168 -56
  251. package/lib/galaxy-lab.js +6 -1
  252. package/lib/linkedin-export.js +175 -5
  253. package/lib/scoring.js +42 -1
  254. package/lib/social-store.js +73 -0
  255. package/package.json +1 -1
  256. package/scripts/scrape.py +156 -23
  257. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +0 -8
  258. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dmc7-t._.js +0 -8
  259. package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +0 -3
  260. package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +0 -3
  261. package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +0 -3
  262. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +0 -3
  263. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  264. package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  265. package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +0 -1
  266. package/.next/standalone/.next/static/chunks/15qbli9udo0vh.js +0 -1
  267. package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +0 -1
  268. package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  269. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  270. package/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  271. package/.next/static/chunks/14m1zu-8warte.js +0 -1
  272. package/.next/static/chunks/15qbli9udo0vh.js +0 -1
  273. package/.next/static/chunks/3p_pwu5gx2i9h.js +0 -1
  274. package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
  275. /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
  276. /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
  277. /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
  278. /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
  279. /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
  280. /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
@@ -59,7 +59,7 @@ _ENV_LOCAL = _load_env_local()
59
59
  # a server they don't control.
60
60
  APP_URL = os.getenv("APP_URL", "http://localhost:3000")
61
61
  MESSAGING_URL = "https://www.linkedin.com/messaging/"
62
- MESSAGE_SCROLLS = 6 # scrolls down the conversation list, a few seconds apart
62
+ MESSAGE_SCROLLS = 15
63
63
 
64
64
  CONNECTIONS_URL = "https://www.linkedin.com/mynetwork/invite-connect/connections/"
65
65
 
@@ -330,7 +330,7 @@ def ensure_logged_in(page, timeout_s=LOGIN_WAIT_SECONDS, log_fn=None, stop_on_ch
330
330
  if stop_on_checkpoint and signed_out:
331
331
  reason = "a security check" if wall == "checkpoint" else "being signed out"
332
332
  _keep_pushback_evidence(page, reason)
333
- set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
333
+ set_cooldown(seconds=AUTO_PUSHBACK_REST if EXPERIMENT["on"] else DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
334
334
  raise LinkedInPushedBack(0, reason)
335
335
 
336
336
  if _has_session_cookie(context) and not _looks_logged_out(page) and not wall:
@@ -1282,10 +1282,17 @@ LEGACY_PAGES_READ = 10 # how far every read before 0.1.6 went, at mos
1282
1282
  # A sitting of SESSION_PAGES searches, then a long rest; searches only in the
1283
1283
  # daytime on this computer's clock; and at the daily budget it waits for the
1284
1284
  # budget to free up instead of stopping. Pages are saved one at a time.
1285
+ #
1286
+ # Its own ceilings sit under whatever budget is set on the Scan page: the account
1287
+ # restricted on 2026-09-28 did 373 searches in 24 hours with the budget raised to
1288
+ # 500. Auto scan stays far below that however high the budget is set.
1285
1289
  EXPERIMENT = {"on": False, "pages": 0, "wire": 0}
1286
- SESSION_PAGES = 10 # searches in one sitting
1287
- SESSION_REST = 45 * 60 # the rest after each sitting
1288
- DRIP_HOURS = (9, 19) # searches only from 09:00 to 19:00, local time
1290
+ SESSION_PAGES = 8 # searches in one sitting
1291
+ SESSION_REST = 60 * 60 # the rest after each sitting
1292
+ DRIP_HOURS = (9, 18) # searches only from 09:00 to 18:00, local time
1293
+ AUTO_DAY_CAP = 40 # at most this many searches in any 24 hours
1294
+ AUTO_WEEK_CAP = 200 # and this many in any 7 days
1295
+ AUTO_PUSHBACK_REST = 2 * 24 * 3600 # after any LinkedIn check, nothing for two days
1289
1296
  WIRE_SAMPLES = 5 # raw LinkedIn responses kept per run, for research
1290
1297
 
1291
1298
 
@@ -1899,6 +1906,18 @@ def voyager_total(payload):
1899
1906
  return None
1900
1907
 
1901
1908
 
1909
+ _WORDS = {"body", "text", "subject", "attributedBody", "snippet", "messages", "headline", "firstName", "lastName", "rootUrl", "artifacts", "sponsoredPreviewText", "customPronoun", "advertiserLabel"}
1910
+
1911
+
1912
+ def _redact_words(x):
1913
+ """A copy of a LinkedIn response with every piece of writing and every name removed."""
1914
+ if isinstance(x, dict):
1915
+ return {k: ("<removed>" if k in _WORDS else _redact_words(v)) for k, v in x.items()}
1916
+ if isinstance(x, list):
1917
+ return [_redact_words(v) for v in x]
1918
+ return x
1919
+
1920
+
1902
1921
  def _wire_sample(data, prefix="search"):
1903
1922
  if EXPERIMENT["wire"] >= WIRE_SAMPLES:
1904
1923
  return
@@ -1908,6 +1927,10 @@ def _wire_sample(data, prefix="search"):
1908
1927
  return
1909
1928
  folder = _home() / "wire-samples"
1910
1929
  folder.mkdir(parents=True, exist_ok=True)
1930
+ if prefix == "messages":
1931
+ # Messaging responses carry what people wrote, and their names: a sample
1932
+ # keeps only the shape (keys, links, dates, counts), never the words.
1933
+ raw = json.dumps(_redact_words(data))
1911
1934
  (folder / f"{prefix}-{int(time.time() * 1000)}.json").write_text(raw)
1912
1935
  EXPERIMENT["wire"] += 1
1913
1936
  except Exception:
@@ -1948,14 +1971,51 @@ def _wire_merge(page, profile_url, page_results):
1948
1971
  return page_results
1949
1972
 
1950
1973
 
1951
- _PROFILE_LINK = re.compile(r"https://www\\.linkedin\\.com/in/[^/?#\\s\"']+")
1974
+ _PROFILE_LINK = re.compile(r"https://www\.linkedin\.com/in/[^/?#\s\"']+")
1975
+
1976
+
1977
+ def _dig(x, *keys):
1978
+ """x[k1][k2]..., or None as soon as a step isn't there."""
1979
+ for k in keys:
1980
+ if not isinstance(x, dict):
1981
+ return None
1982
+ x = x.get(k)
1983
+ return x
1984
+
1985
+
1986
+ def _link_key(link):
1987
+ """A profile link as the messages sync compares them: no query, one closing slash."""
1988
+ return link.split("?")[0].rstrip("/") + "/" if isinstance(link, str) and "/in/" in link else None
1952
1989
 
1953
1990
 
1954
1991
  def voyager_conversations(payload):
1955
1992
  """Conversations in a LinkedIn messaging response (experimental): per conversation,
1956
- everyone in it (profile links), when it was last active, and unread messages.
1957
- Anything with a lastActivityAt and participants, wherever it sits. Never the text."""
1993
+ everyone in it (profile links), when it was last active, unread messages, its link,
1994
+ whether it's a group, and the newest message LinkedIn sent along with it (when,
1995
+ from whom, and its words). Anything with a lastActivityAt and participants,
1996
+ wherever it sits. The words are handed to the app only while Keep my messages is
1997
+ on there (sync_messages); a wire sample never has them (_redact_words)."""
1958
1998
  out = []
1999
+ names = {}
2000
+
2001
+ def text_of(v):
2002
+ return v.get("text", "") if isinstance(v, dict) else (v if isinstance(v, str) else "")
2003
+
2004
+ def latest_of(x):
2005
+ # conversation.messages.elements[]: body.text, deliveredAt, and who sent it
2006
+ # (actor.participantType.member.profileUrl, the same /in/ACoA... link the
2007
+ # participants have, so it tells you from them).
2008
+ msgs = x.get("messages")
2009
+ elements = msgs.get("elements") if isinstance(msgs, dict) else msgs
2010
+ best = None
2011
+ for m in elements if isinstance(elements, list) else []:
2012
+ t = _dig(m, "deliveredAt")
2013
+ if not isinstance(t, (int, float)) or t < 1e12 or (best and t <= best["t"]):
2014
+ continue
2015
+ text = text_of(_dig(m, "body")).strip()
2016
+ best = {"t": int(t), "from": _link_key(_dig(m, "actor", "participantType", "member", "profileUrl")),
2017
+ "text": text or None}
2018
+ return best
1959
2019
 
1960
2020
  def people(x):
1961
2021
  found = set()
@@ -1966,6 +2026,14 @@ def voyager_conversations(payload):
1966
2026
  pid = v.get("publicIdentifier")
1967
2027
  if isinstance(pid, str) and pid and pid != "UNKNOWN":
1968
2028
  found.add(f"https://www.linkedin.com/in/{pid}/")
2029
+ # Messaging gives a member's link as /in/ACoA… (their member id), not
2030
+ # the /in/name link your connections list has, so their name comes
2031
+ # along to match them up (the app keeps the match, not the name).
2032
+ key = _link_key(v.get("profileUrl"))
2033
+ if key:
2034
+ name = f"{text_of(v.get('firstName'))} {text_of(v.get('lastName'))}".strip()
2035
+ if name:
2036
+ names[key] = name
1969
2037
  stack.extend(v.values())
1970
2038
  elif isinstance(v, list):
1971
2039
  stack.extend(v)
@@ -1984,7 +2052,13 @@ def voyager_conversations(payload):
1984
2052
  who |= people(v)
1985
2053
  if who:
1986
2054
  unread = x.get("unreadCount")
1987
- out.append({"people": sorted(who), "at": int(at), "unread": int(unread) if isinstance(unread, (int, float)) else None})
2055
+ link = x.get("conversationUrl")
2056
+ group = x.get("groupChat")
2057
+ out.append({"people": sorted(who), "at": int(at), "unread": int(unread) if isinstance(unread, (int, float)) else None,
2058
+ "names": {u: names[u] for u in who if u in names},
2059
+ "url": link if isinstance(link, str) and link.startswith("https://www.linkedin.com/messaging/") else None,
2060
+ "group": group if isinstance(group, bool) else None,
2061
+ "latest": latest_of(x)})
1988
2062
  for v in x.values():
1989
2063
  walk(v)
1990
2064
  elif isinstance(x, list):
@@ -1997,8 +2071,9 @@ def voyager_conversations(payload):
1997
2071
  def sync_messages(headless=False):
1998
2072
  """Experimental: read your LinkedIn messages list once, for the Social tab. Opens
1999
2073
  Messaging in your own Chrome, scrolls the list a few times, and keeps from LinkedIn's
2000
- own data only who each 1:1 conversation is with, when it was last active and whether
2001
- it has unread messages. No search budget; no message text is read or kept."""
2074
+ own data who each 1:1 conversation is with, when it was last active, whether it has
2075
+ unread messages, its link and who wrote last. The newest message's words go to the
2076
+ app only while Keep my messages is on there. No search budget."""
2002
2077
  from playwright.sync_api import sync_playwright
2003
2078
  kept = []
2004
2079
  with sync_playwright() as p:
@@ -2021,7 +2096,12 @@ def sync_messages(headless=False):
2021
2096
  if stop_requested() or _window_closed(page):
2022
2097
  break
2023
2098
  try:
2024
- page.evaluate("""() => { const l = document.querySelector('.msg-conversations-container__conversations-list, [class*="conversations-list"]');
2099
+ # The list scrolls inside its own panel, not the window: find the panel
2100
+ # that holds the conversation links and scroll that to its end.
2101
+ page.evaluate("""() => {
2102
+ let l = document.querySelector('.msg-conversations-container__conversations-list, [class*="conversations-list"]');
2103
+ const a = document.querySelector('a[href*="/messaging/thread/"], li[class*="conversation-listitem"]');
2104
+ for (let e = a; !l && e; e = e.parentElement) if (e.scrollHeight > e.clientHeight + 20) l = e;
2025
2105
  if (l) l.scrollTop = l.scrollHeight; }""")
2026
2106
  except Exception:
2027
2107
  pass
@@ -2036,7 +2116,35 @@ def sync_messages(headless=False):
2036
2116
  _wire_sample(data, prefix="messages")
2037
2117
  convos.extend(voyager_conversations(data))
2038
2118
  browser.close()
2039
- # You are the one in the most conversations; keep the 1:1 ones.
2119
+ _me, people = live_people(convos)
2120
+ print(f" Found {len(convos)} conversations in LinkedIn's data, {len(people)} with one person.")
2121
+ # The newest message's words go to the app only if Keep my messages is on
2122
+ # there. The app checks too; this way they don't even leave the scanner.
2123
+ keep = False
2124
+ try:
2125
+ r = requests.get(f"{APP_URL}/api/social", headers=app_headers(json_body=False), timeout=15)
2126
+ keep = r.ok and ((r.json() or {}).get("social") or {}).get("keepMessages") is True
2127
+ except Exception:
2128
+ pass
2129
+ if not keep:
2130
+ for v in people.values():
2131
+ if v.get("preview"):
2132
+ v["preview"]["text"] = None
2133
+ try:
2134
+ r = requests.put(f"{APP_URL}/api/social", json={"live": people}, headers=app_headers(), timeout=30)
2135
+ got = r.json() if r.ok else {}
2136
+ matched, unmatched = got.get("people", 0), got.get("unmatched", 0)
2137
+ print(f" Saved for the Social tab: {matched} matched to your connections, {unmatched} not.")
2138
+ except Exception as e:
2139
+ print(f" Couldn't save them: {e}")
2140
+
2141
+
2142
+ def live_people(convos):
2143
+ """The messages sync's conversations, per person: you are the one in the most
2144
+ conversations, and only the 1:1 ones are kept. For each person, their newest
2145
+ conversation's last activity, unread count, name (for the app to match them to
2146
+ a connection; it keeps the match, not the name), link, who wrote last and the
2147
+ newest message. Returns (you, {profile link: ...})."""
2040
2148
  counts = {}
2041
2149
  for c in convos:
2042
2150
  for u in c["people"]:
@@ -2045,17 +2153,23 @@ def sync_messages(headless=False):
2045
2153
  people = {}
2046
2154
  for c in convos:
2047
2155
  others = [u for u in c["people"] if u != me]
2048
- if len(others) != 1:
2156
+ if len(others) != 1 or c.get("group") is True:
2049
2157
  continue
2050
2158
  cur = people.get(others[0])
2051
- if not cur or c["at"] > cur["last"]:
2052
- people[others[0]] = {"last": c["at"], "unread": c["unread"]}
2053
- print(f" Found {len(convos)} conversations in LinkedIn's data, {len(people)} with one person.")
2054
- try:
2055
- requests.put(f"{APP_URL}/api/social", json={"live": people}, headers=app_headers(), timeout=30)
2056
- print(" Saved for the Social tab.")
2057
- except Exception as e:
2058
- print(f" Couldn't save them: {e}")
2159
+ if cur and c["at"] <= cur["last"]:
2160
+ continue
2161
+ entry = {"last": c["at"], "unread": c["unread"], "name": c.get("names", {}).get(others[0]),
2162
+ "threadUrl": c.get("url"), "lastFromThem": None, "preview": None}
2163
+ latest = c.get("latest")
2164
+ # Who sent it is known only when their link is one of the two in the
2165
+ # conversation; a link in some other form says nothing either way.
2166
+ sender = latest.get("from") if latest else None
2167
+ if sender in (me, others[0]):
2168
+ from_me = sender == me
2169
+ entry["lastFromThem"] = not from_me
2170
+ entry["preview"] = {"t": latest["t"], "fromMe": from_me, "text": latest.get("text")}
2171
+ people[others[0]] = entry
2172
+ return me, people
2059
2173
 
2060
2174
 
2061
2175
  def _drip_wait(label, seconds):
@@ -2095,9 +2209,28 @@ def _drip_before_search():
2095
2209
  if not _drip_wait("Today's searches are used", _seconds_until_search_frees()):
2096
2210
  return False
2097
2211
  continue
2212
+ wait = _auto_ceiling_wait()
2213
+ if wait:
2214
+ label, seconds = wait
2215
+ if not _drip_wait(label, seconds):
2216
+ return False
2217
+ continue
2098
2218
  return True
2099
2219
 
2100
2220
 
2221
+ def _auto_ceiling_wait(now=None):
2222
+ """Auto scan's own ceilings (AUTO_DAY_CAP in 24 hours, AUTO_WEEK_CAP in 7 days):
2223
+ None to go ahead, or (why, seconds until the oldest counted search drops out)."""
2224
+ now = now if now is not None else time.time()
2225
+ searches = sorted(t for t in _read_activity()["searches"] if now - 7 * DAY_SECONDS < t <= now)
2226
+ day = [t for t in searches if now - DAY_SECONDS < t]
2227
+ if len(day) >= AUTO_DAY_CAP:
2228
+ return (f"Auto scan's {AUTO_DAY_CAP} searches for today are used", day[-AUTO_DAY_CAP] + DAY_SECONDS - now + 5)
2229
+ if len(searches) >= AUTO_WEEK_CAP:
2230
+ return (f"Auto scan's {AUTO_WEEK_CAP} searches for this week are used", searches[-AUTO_WEEK_CAP] + 7 * DAY_SECONDS - now + 5)
2231
+ return None
2232
+
2233
+
2101
2234
  def interruptible_sleep(seconds, on_tick=None, step=5):
2102
2235
  """Sleep, but notice a stop request. Returns False if we were interrupted."""
2103
2236
  waited = 0
@@ -4073,7 +4206,7 @@ Examples:
4073
4206
  parser.add_argument("--rescrape", type=str, help="Delete + re-scrape a bridge's cluster from scratch")
4074
4207
  parser.add_argument("--company", type=str, help="Scan everyone the app can see at one company")
4075
4208
  parser.add_argument("--messages", action="store_true",
4076
- help="Experimental: read your messages list once for the Social tab (who, when, unread; never text)")
4209
+ help="Experimental: read your messages list once for the Social tab (who, when, unread; the newest message's words only if Keep my messages is on)")
4077
4210
  parser.add_argument("--experimental", action="store_true",
4078
4211
  help="Auto-Bridge, experimental: all-day pacing (sittings, rests, daytime only, "
4079
4212
  "waits for the daily budget) and reading LinkedIn's own data beside the page text")
@@ -0,0 +1,188 @@
1
+ // A small Markdown renderer for the website's blog posts, pages and release notes.
2
+ // No dependencies, on purpose: the site builds with nothing but Node. It covers
3
+ // what the posts and CHANGELOG.md use, and nothing more:
4
+ //
5
+ // # to ####### headings (with an id from their text), paragraphs, blank-line breaks
6
+ // - and 1. lists, nested by two spaces, with continuation lines
7
+ // > block quotes, ``` fenced code, --- rules, | tables |
8
+ // **bold**, *italic*, `code`, [links](url), ![images](src "caption")
9
+ //
10
+ // Raw HTML is escaped, never passed through: a post can't inject markup or script.
11
+
12
+ export const escapeHtml = (s) => String(s)
13
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
14
+
15
+ /** "What it does, for you!" → "what-it-does-for-you" */
16
+ export function slugify(text) {
17
+ return String(text).toLowerCase()
18
+ .replace(/<[^>]+>/g, '')
19
+ .replace(/&[a-z]+;/g, '')
20
+ .replace(/[^a-z0-9\s-]/g, '')
21
+ .trim().replace(/\s+/g, '-').replace(/-+/g, '-');
22
+ }
23
+
24
+ const safeUrl = (url) => (/^(https?:|mailto:|\/|#|\.\.?\/)/i.test(url) || !/^[a-z]+:/i.test(url) ? url : '#');
25
+
26
+ /** Inline marks, on text that is escaped first. */
27
+ export function inline(md) {
28
+ let s = escapeHtml(md);
29
+ const codes = [];
30
+ s = s.replace(/`([^`]+)`/g, (_, c) => { codes.push(c); return `\u0000${codes.length - 1}\u0000`; });
31
+ s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+&quot;([^&]*)&quot;)?\)/g,
32
+ (_, alt, src) => `<img src="${safeUrl(src)}" alt="${alt}" loading="lazy">`);
33
+ s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, text, url) => `<a href="${safeUrl(url)}">${text}</a>`);
34
+ s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
35
+ s = s.replace(/(^|[^\w*])\*(?!\s)(.+?)\*(?!\w)/g, '$1<em>$2</em>');
36
+ s = s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${codes[Number(i)]}</code>`);
37
+ return s;
38
+ }
39
+
40
+ /**
41
+ * Front matter between --- lines: `key: value`, with [a, b] lists. Returns
42
+ * { data, body }. A file without it is all body.
43
+ */
44
+ export function frontMatter(text) {
45
+ const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
46
+ if (!m) return { data: {}, body: text };
47
+ const data = {};
48
+ for (const line of m[1].split('\n')) {
49
+ const kv = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
50
+ if (!kv) continue;
51
+ let v = kv[2].trim();
52
+ if (/^\[.*\]$/.test(v)) v = v.slice(1, -1).split(',').map((x) => x.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
53
+ else v = v.replace(/^["'](.*)["']$/, '$1');
54
+ data[kv[1]] = v;
55
+ }
56
+ return { data, body: text.slice(m[0].length) };
57
+ }
58
+
59
+ /** The same, for an HTML page whose first thing is a <!-- key: value --> comment. */
60
+ export function htmlFrontMatter(text) {
61
+ const m = text.match(/^<!--\n([\s\S]*?)\n-->\n?/);
62
+ if (!m) return { data: {}, body: text };
63
+ return { data: frontMatter(`---\n${m[1]}\n---\n`).data, body: text.slice(m[0].length) };
64
+ }
65
+
66
+ function table(lines) {
67
+ const cells = (l) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
68
+ const head = cells(lines[0]);
69
+ const rows = lines.slice(2).map(cells);
70
+ return `<div class="table-wrap prose-table"><table>\n<thead><tr>${head.map((h) => `<th scope="col">${inline(h)}</th>`).join('')}</tr></thead>\n<tbody>\n${
71
+ rows.map((r) => `<tr>${r.map((c, i) => (i === 0 ? `<th scope="row">${inline(c)}</th>` : `<td>${inline(c)}</td>`)).join('')}</tr>`).join('\n')
72
+ }\n</tbody></table></div>`;
73
+ }
74
+
75
+ /**
76
+ * Lists: items start with "- " or "1. " at an indent; deeper indents nest. An
77
+ * item's text, its sub-lists and any later paragraphs stay in their order.
78
+ */
79
+ function list(lines, start) {
80
+ const indentOf = (l) => l.match(/^(\s*)/)[1].length;
81
+ const base = indentOf(lines[start]);
82
+ const ordered = /^\s*\d+\.\s/.test(lines[start]);
83
+ const items = [];
84
+ let i = start;
85
+ let gap = false; // a blank line: the next text inside this item is a new paragraph
86
+ while (i < lines.length) {
87
+ const l = lines[i];
88
+ if (!l.trim()) {
89
+ // A blank line inside a list continues it only if an indented line follows.
90
+ const next = lines[i + 1];
91
+ if (next && indentOf(next) > base && next.trim()) { gap = true; i += 1; continue; }
92
+ break;
93
+ }
94
+ const ind = indentOf(l);
95
+ const bullet = l.match(/^\s*(?:[-*]|\d+\.)\s+(.*)$/);
96
+ if (ind === base && bullet) { items.push({ parts: [bullet[1]] }); gap = false; i += 1; continue; }
97
+ if (ind < base || (!bullet && ind === base) || !items.length) break;
98
+ const parts = items.at(-1).parts;
99
+ if (bullet && ind > base) {
100
+ const [html, next] = list(lines, i);
101
+ parts.push({ html });
102
+ gap = false;
103
+ i = next;
104
+ continue;
105
+ }
106
+ if (gap || typeof parts.at(-1) !== 'string') parts.push(l.trim());
107
+ else parts[parts.length - 1] += ` ${l.trim()}`;
108
+ gap = false;
109
+ i += 1;
110
+ }
111
+ const tag = ordered ? 'ol' : 'ul';
112
+ const li = (it) => it.parts.map((p, n) => (typeof p !== 'string' ? p.html : n === 0 ? inline(p) : `<p>${inline(p)}</p>`)).join('');
113
+ const html = `<${tag}>\n${items.map((it) => `<li>${li(it)}</li>`).join('\n')}\n</${tag}>`;
114
+ return [html, i];
115
+ }
116
+
117
+ /**
118
+ * Markdown → HTML. `headingOffset` shifts heading levels (1 makes # an <h2>),
119
+ * `ids` adds an id to each heading.
120
+ */
121
+ export function markdown(md, { headingOffset = 0, ids = true } = {}) {
122
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
123
+ const out = [];
124
+ let i = 0;
125
+ while (i < lines.length) {
126
+ const l = lines[i];
127
+ if (!l.trim()) { i += 1; continue; }
128
+ const fence = l.match(/^```(\w*)/);
129
+ if (fence) {
130
+ const code = [];
131
+ i += 1;
132
+ while (i < lines.length && !/^```/.test(lines[i])) code.push(lines[i++]);
133
+ i += 1;
134
+ out.push(`<pre><code${fence[1] ? ` class="lang-${fence[1]}"` : ''}>${escapeHtml(code.join('\n'))}</code></pre>`);
135
+ continue;
136
+ }
137
+ const h = l.match(/^(#{1,6})\s+(.*)$/);
138
+ if (h) {
139
+ const level = Math.min(6, h[1].length + headingOffset);
140
+ const html = inline(h[2]);
141
+ out.push(`<h${level}${ids ? ` id="${slugify(h[2])}"` : ''}>${html}</h${level}>`);
142
+ i += 1;
143
+ continue;
144
+ }
145
+ if (/^(-{3,}|\*{3,})\s*$/.test(l)) { out.push('<hr>'); i += 1; continue; }
146
+ if (/^\s*(?:[-*]|\d+\.)\s+/.test(l)) {
147
+ const [html, next] = list(lines, i);
148
+ out.push(html);
149
+ i = next;
150
+ continue;
151
+ }
152
+ if (/^>\s?/.test(l)) {
153
+ const q = [];
154
+ while (i < lines.length && /^>\s?/.test(lines[i])) q.push(lines[i++].replace(/^>\s?/, ''));
155
+ out.push(`<blockquote>${markdown(q.join('\n'), { headingOffset, ids: false })}</blockquote>`);
156
+ continue;
157
+ }
158
+ if (/^\|.*\|\s*$/.test(l) && /^\|?\s*:?-{2,}/.test(lines[i + 1] || '')) {
159
+ const t = [];
160
+ while (i < lines.length && /^\|.*\|\s*$/.test(lines[i])) t.push(lines[i++]);
161
+ out.push(table(t));
162
+ continue;
163
+ }
164
+ const img = l.match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)\s*$/);
165
+ if (img) {
166
+ out.push(`<figure><img src="${escapeHtml(safeUrl(img[2]))}" alt="${escapeHtml(img[1])}" loading="lazy">${img[3] ? `<figcaption>${inline(img[3])}</figcaption>` : ''}</figure>`);
167
+ i += 1;
168
+ continue;
169
+ }
170
+ const para = [];
171
+ while (i < lines.length && lines[i].trim() && !/^(#{1,6}\s|```|>\s?|\s*(?:[-*]|\d+\.)\s+|\|)/.test(lines[i])) para.push(lines[i++].trim());
172
+ if (!para.length) { para.push(lines[i++].trim()); }
173
+ out.push(`<p>${inline(para.join(' '))}</p>`);
174
+ }
175
+ return out.join('\n');
176
+ }
177
+
178
+ /** Plain words from Markdown, for descriptions and feeds. */
179
+ export function plainText(md) {
180
+ return md
181
+ .replace(/!\[[^\]]*\]\([^)]+\)/g, '')
182
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
183
+ .replace(/\*\*(.+?)\*\*/g, '$1')
184
+ .replace(/(^|[^\w*])\*(?!\s)(.+?)\*(?!\w)/g, '$1$2')
185
+ .replace(/`([^`]+)`/g, '$1')
186
+ .replace(/\s+/g, ' ')
187
+ .trim();
188
+ }