six-degrees 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) 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 +32 -4
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +16 -16
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +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 +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/backup/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/csv/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/data/restore/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
  47. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/scraper/route.js +4 -4
  51. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  56. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/social/route.js +1 -1
  60. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  62. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  65. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  67. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/import.html +1 -1
  72. package/.next/standalone/.next/server/app/import.rsc +18 -18
  73. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +18 -18
  74. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  75. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +16 -16
  76. package/.next/standalone/.next/server/app/index.html +1 -1
  77. package/.next/standalone/.next/server/app/index.rsc +19 -19
  78. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -16
  79. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +19 -19
  80. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  81. package/.next/standalone/.next/server/app/insights/page/app-paths-manifest.json +3 -0
  82. package/.next/standalone/.next/server/app/insights/page/build-manifest.json +20 -0
  83. package/.next/standalone/.next/server/app/insights/page/next-font-manifest.json +11 -0
  84. package/.next/standalone/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
  85. package/.next/standalone/.next/server/app/insights/page/server-reference-manifest.json +4 -0
  86. package/.next/standalone/.next/server/app/insights/page.js +13 -0
  87. package/.next/standalone/.next/server/app/insights/page.js.map +5 -0
  88. package/.next/standalone/.next/server/app/insights/page.js.nft.json +1 -0
  89. package/.next/standalone/.next/server/app/insights/page_client-reference-manifest.js +3 -0
  90. package/.next/standalone/.next/server/app/insights.html +1 -0
  91. package/.next/standalone/.next/server/app/insights.meta +26 -0
  92. package/.next/standalone/.next/server/app/insights.rsc +35 -0
  93. package/.next/standalone/.next/server/app/insights.segments/_full.segment.rsc +35 -0
  94. package/.next/standalone/.next/server/app/insights.segments/_tree.segment.rsc +4 -0
  95. package/.next/standalone/.next/server/app/insights.segments/insights/__PAGE__.segment.rsc +33 -0
  96. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  97. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  98. package/.next/standalone/.next/server/app/launch.html +2 -2
  99. package/.next/standalone/.next/server/app/launch.rsc +18 -18
  100. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +18 -18
  101. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  102. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +16 -16
  103. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  104. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  105. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  106. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  107. package/.next/standalone/.next/server/app/paths.html +1 -1
  108. package/.next/standalone/.next/server/app/paths.rsc +19 -19
  109. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +19 -19
  110. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  111. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +16 -16
  112. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  113. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  114. package/.next/standalone/.next/server/app/profile.html +1 -1
  115. package/.next/standalone/.next/server/app/profile.rsc +19 -19
  116. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +19 -19
  117. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  118. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +16 -16
  119. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  120. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  121. package/.next/standalone/.next/server/app/queue.html +1 -1
  122. package/.next/standalone/.next/server/app/queue.rsc +19 -19
  123. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +19 -19
  124. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  125. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +21 -20
  126. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  127. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  128. package/.next/standalone/.next/server/app/scores.html +1 -1
  129. package/.next/standalone/.next/server/app/scores.rsc +18 -18
  130. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +18 -18
  131. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  132. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +16 -16
  133. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  134. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  135. package/.next/standalone/.next/server/app/settings.html +1 -1
  136. package/.next/standalone/.next/server/app/settings.rsc +19 -19
  137. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +19 -19
  138. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  139. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +16 -16
  140. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  141. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  142. package/.next/standalone/.next/server/app/setup.html +1 -1
  143. package/.next/standalone/.next/server/app/setup.rsc +19 -19
  144. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +19 -19
  145. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  146. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +16 -16
  147. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  148. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  149. package/.next/standalone/.next/server/app/social.html +1 -1
  150. package/.next/standalone/.next/server/app/social.rsc +18 -18
  151. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +18 -18
  152. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  153. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +16 -16
  154. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  155. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__063s0x7._.js → [root-of-the-server]__0r-0t-9._.js} +1 -1
  156. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ra6xyg._.js +15 -0
  157. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20rwfdw._.js → [root-of-the-server]__0rcpjad._.js} +5 -2
  158. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
  159. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  160. package/.next/standalone/.next/server/chunks/lib_02iz_9q._.js +2 -2
  161. package/.next/standalone/.next/server/chunks/lib_05agvk3._.js +2 -2
  162. package/.next/standalone/.next/server/chunks/lib_0v84u66._.js +2 -2
  163. package/.next/standalone/.next/server/chunks/lib_db-client_0_dch35.js +9 -9
  164. package/.next/standalone/.next/server/chunks/{lib_rpc_0jnfp2e.js → lib_rpc_00nndai.js} +1 -1
  165. package/.next/standalone/.next/server/chunks/{lib_rpc_0b17r6v.js → lib_rpc_03ibb-x.js} +1 -1
  166. package/.next/standalone/.next/server/chunks/{lib_rpc_0doq0t-.js → lib_rpc_03ri3ji.js} +1 -1
  167. package/.next/standalone/.next/server/chunks/{lib_rpc_0fd2rlw.js → lib_rpc_078ce48.js} +1 -1
  168. package/.next/standalone/.next/server/chunks/lib_rpc_0a69mgr.js +3 -0
  169. package/.next/standalone/.next/server/chunks/{lib_rpc_005abye.js → lib_rpc_0cej3t8.js} +1 -1
  170. package/.next/standalone/.next/server/chunks/lib_rpc_0cmh4g6.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_0nx2_pc.js +3 -0
  172. package/.next/standalone/.next/server/chunks/lib_rpc_0r79-w1.js +3 -0
  173. package/.next/standalone/.next/server/chunks/lib_rpc_0yx8ynd.js +3 -0
  174. package/.next/standalone/.next/server/chunks/lib_rpc_12p-o9-.js +3 -0
  175. package/.next/standalone/.next/server/chunks/lib_rpc_1466dnb.js +3 -0
  176. package/.next/standalone/.next/server/chunks/{lib_rpc_0is4qoc.js → lib_rpc_1hjjjr4.js} +1 -1
  177. package/.next/standalone/.next/server/chunks/lib_rpc_1sryxv6.js +3 -0
  178. package/.next/standalone/.next/server/chunks/lib_rpc_1trn-x5.js +3 -0
  179. package/.next/standalone/.next/server/chunks/lib_rpc_1ygu_mv.js +3 -0
  180. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egamyq._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/ssr/_04am_h8._.js +3 -0
  182. package/.next/standalone/.next/server/chunks/ssr/_07zb7lu._.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/_0_79idq._.js +1 -1
  184. package/.next/standalone/.next/server/chunks/ssr/_0gds903._.js +3 -0
  185. package/.next/standalone/.next/server/chunks/ssr/_0klqt9z._.js +3 -0
  186. package/.next/standalone/.next/server/chunks/ssr/{_1k9qfsk._.js → _14jak5s._.js} +6 -2
  187. package/.next/standalone/.next/server/chunks/ssr/_14nxd3q._.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/_1cj2yog._.js +3 -0
  189. package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +1 -1
  190. package/.next/standalone/.next/server/chunks/ssr/_1hdqw0-._.js +7 -0
  191. package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +1 -1
  192. package/.next/standalone/.next/server/chunks/ssr/{_0gy11ar._.js → _1r62six._.js} +6 -2
  193. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_insights_page_actions_0-0tf75.js +3 -0
  194. package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +2 -2
  195. package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +2 -2
  196. package/.next/standalone/.next/server/chunks/ssr/{_01o3i5y._.js → app_components_settings_SectorPicker_1c-29qw.js} +2 -6
  197. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  198. package/.next/standalone/.next/server/chunks/ssr/app_insights_page_1d_chja.js +3 -0
  199. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -14
  200. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +3 -3
  201. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  202. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -4
  203. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  204. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  205. package/.next/standalone/.next/server/next-font-manifest.js +1 -1
  206. package/.next/standalone/.next/server/next-font-manifest.json +4 -0
  207. package/.next/standalone/.next/server/pages/404.html +1 -1
  208. package/.next/standalone/.next/server/pages/500.html +1 -1
  209. package/.next/standalone/.next/server/prefetch-hints.json +14 -0
  210. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  211. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  212. package/.next/standalone/.next/static/chunks/02wnf8dkxegxg.js +34 -0
  213. package/.next/standalone/.next/static/chunks/0hggugbh84m4t.js +21 -0
  214. package/.next/{static/chunks/2eo9pqq9gay3r.js → standalone/.next/static/chunks/0ka4prpk3w9wn.js} +4 -4
  215. package/.next/standalone/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
  216. package/.next/standalone/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
  217. package/.next/{static/chunks/44yn0tcd9dfk3.js → standalone/.next/static/chunks/1bkt_hhjzvbh8.js} +1 -1
  218. package/.next/standalone/.next/static/chunks/1cmphb3kh2ers.css +3 -0
  219. package/.next/{static/chunks/1qbjermbf68xj.js → standalone/.next/static/chunks/1d8yv3k_atka7.js} +1 -1
  220. package/.next/standalone/.next/static/chunks/1ods5h86raivz.js +1 -0
  221. package/.next/standalone/.next/static/chunks/23hc51oa4vkhc.js +5 -0
  222. package/.next/standalone/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
  223. package/.next/standalone/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
  224. package/.next/{static/chunks/2zf4jmz9l3li2.js → standalone/.next/static/chunks/2w-y83mjlpnse.js} +1 -1
  225. package/.next/standalone/.next/static/chunks/2ws1xf293j-tm.js +5 -0
  226. package/.next/standalone/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
  227. package/.next/standalone/.next/static/chunks/3opgg1efrx8q6.js +1 -0
  228. package/.next/standalone/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
  229. package/.next/{static/chunks/0an_b6zi1r_qq.js → standalone/.next/static/chunks/3y40z6of3r9py.js} +1 -5
  230. package/.next/standalone/.next/static/chunks/403dpflln8ah5.js +1 -0
  231. package/.next/standalone/CHANGELOG.md +83 -0
  232. package/.next/standalone/README.md +40 -14
  233. package/.next/standalone/SECURITY.md +17 -4
  234. package/.next/standalone/app/api/outreach/route.js +13 -73
  235. package/.next/standalone/app/api/scraper/route.js +106 -5
  236. package/.next/standalone/app/components/AppHeader.js +22 -6
  237. package/.next/standalone/app/components/AppTabs.js +4 -0
  238. package/.next/standalone/app/components/AutoConnect.js +286 -0
  239. package/.next/standalone/app/components/ChainView.js +75 -2
  240. package/.next/standalone/app/components/FilterPanel.js +8 -1
  241. package/.next/standalone/app/components/LinkedInLimits.js +38 -13
  242. package/.next/standalone/app/components/OutlinkQuest.js +32 -11
  243. package/.next/standalone/app/components/ScanStatusBar.js +18 -3
  244. package/.next/standalone/app/components/ScanTrail.js +13 -3
  245. package/.next/standalone/app/components/SeparationView.js +6 -22
  246. package/.next/standalone/app/components/Sidebar.js +147 -38
  247. package/.next/standalone/app/components/insights/Boards.js +275 -0
  248. package/.next/standalone/app/components/insights/PeopleView.js +266 -0
  249. package/.next/standalone/app/components/insights/Rail.js +156 -0
  250. package/.next/standalone/app/components/insights/Report.js +382 -0
  251. package/.next/standalone/app/components/insights/parts.js +239 -0
  252. package/.next/standalone/app/components/settings/UsageSection.js +50 -3
  253. package/.next/standalone/app/globals.css +2 -3
  254. package/.next/standalone/app/insights/page.js +220 -0
  255. package/.next/standalone/app/page.js +1 -17
  256. package/.next/standalone/app/queue/page.js +30 -18
  257. package/.next/standalone/app/setup/page.js +71 -13
  258. package/.next/standalone/install.sh +7 -6
  259. package/.next/standalone/lib/auto-connect.js +145 -0
  260. package/.next/standalone/lib/companies.js +1 -1
  261. package/.next/standalone/lib/in-progress.js +46 -0
  262. package/.next/standalone/lib/insights-board.js +613 -0
  263. package/.next/standalone/lib/linkedin-limits.js +36 -11
  264. package/.next/standalone/lib/requests-client.js +10 -0
  265. package/.next/standalone/lib/requests.js +124 -0
  266. package/.next/standalone/lib/scan-state.js +1 -0
  267. package/.next/standalone/lib/scanner-setup.js +82 -3
  268. package/.next/standalone/lib/scraper-client.js +18 -5
  269. package/.next/standalone/lib/settings.js +6 -0
  270. package/.next/standalone/lib/usage.js +5 -0
  271. package/.next/standalone/package-lock.json +2 -2
  272. package/.next/standalone/package.json +1 -1
  273. package/.next/standalone/scripts/build-app.mjs +167 -11
  274. package/.next/standalone/scripts/build-desktop.mjs +3 -2
  275. package/.next/standalone/scripts/ci-mac-signing.sh +128 -0
  276. package/.next/standalone/scripts/entitlements/electron.plist +23 -0
  277. package/.next/standalone/scripts/entitlements/node.plist +27 -0
  278. package/.next/standalone/scripts/mac-sign.mjs +563 -0
  279. package/.next/standalone/scripts/make-dmg-background.mjs +2 -1
  280. package/.next/standalone/scripts/scrape.py +569 -9
  281. package/.next/standalone/scripts/set-signing-secrets.sh +96 -0
  282. package/.next/static/chunks/02wnf8dkxegxg.js +34 -0
  283. package/.next/static/chunks/0hggugbh84m4t.js +21 -0
  284. package/.next/{standalone/.next/static/chunks/2eo9pqq9gay3r.js → static/chunks/0ka4prpk3w9wn.js} +4 -4
  285. package/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
  286. package/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
  287. package/.next/{standalone/.next/static/chunks/44yn0tcd9dfk3.js → static/chunks/1bkt_hhjzvbh8.js} +1 -1
  288. package/.next/static/chunks/1cmphb3kh2ers.css +3 -0
  289. package/.next/{standalone/.next/static/chunks/1qbjermbf68xj.js → static/chunks/1d8yv3k_atka7.js} +1 -1
  290. package/.next/static/chunks/1ods5h86raivz.js +1 -0
  291. package/.next/static/chunks/23hc51oa4vkhc.js +5 -0
  292. package/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
  293. package/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
  294. package/.next/{standalone/.next/static/chunks/2zf4jmz9l3li2.js → static/chunks/2w-y83mjlpnse.js} +1 -1
  295. package/.next/static/chunks/2ws1xf293j-tm.js +5 -0
  296. package/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
  297. package/.next/static/chunks/3opgg1efrx8q6.js +1 -0
  298. package/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
  299. package/.next/{standalone/.next/static/chunks/0an_b6zi1r_qq.js → static/chunks/3y40z6of3r9py.js} +1 -5
  300. package/.next/static/chunks/403dpflln8ah5.js +1 -0
  301. package/CHANGELOG.md +83 -0
  302. package/README.md +40 -14
  303. package/SECURITY.md +17 -4
  304. package/lib/auto-connect.js +145 -0
  305. package/lib/companies.js +1 -1
  306. package/lib/in-progress.js +46 -0
  307. package/lib/insights-board.js +613 -0
  308. package/lib/linkedin-limits.js +36 -11
  309. package/lib/requests-client.js +10 -0
  310. package/lib/requests.js +124 -0
  311. package/lib/scan-state.js +1 -0
  312. package/lib/scanner-setup.js +82 -3
  313. package/lib/scraper-client.js +18 -5
  314. package/lib/settings.js +6 -0
  315. package/lib/usage.js +5 -0
  316. package/package.json +1 -1
  317. package/scripts/scrape.py +569 -9
  318. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_4y8z7._.js +0 -8
  319. package/.next/standalone/.next/server/chunks/_02twlca._.js +0 -10
  320. package/.next/standalone/.next/server/chunks/lib_rpc_0nef_n7.js +0 -3
  321. package/.next/standalone/.next/server/chunks/lib_rpc_0plrzra.js +0 -3
  322. package/.next/standalone/.next/server/chunks/lib_rpc_13a4m0c.js +0 -3
  323. package/.next/standalone/.next/server/chunks/lib_rpc_1if17c-.js +0 -3
  324. package/.next/standalone/.next/server/chunks/lib_rpc_1j-zw7v.js +0 -3
  325. package/.next/standalone/.next/server/chunks/lib_rpc_1sngt-q.js +0 -3
  326. package/.next/standalone/.next/server/chunks/lib_rpc_1v2sgme.js +0 -3
  327. package/.next/standalone/.next/server/chunks/lib_rpc_1vh-tgx.js +0 -3
  328. package/.next/standalone/.next/server/chunks/lib_rpc_1zaubz7.js +0 -3
  329. package/.next/standalone/.next/server/chunks/ssr/_02sband._.js +0 -7
  330. package/.next/standalone/.next/server/chunks/ssr/_189g8lg._.js +0 -3
  331. package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +0 -3
  332. package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +0 -3
  333. package/.next/standalone/.next/static/chunks/0e3g0sil79cus.js +0 -21
  334. package/.next/standalone/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
  335. package/.next/standalone/.next/static/chunks/163b-20vwa06i.js +0 -34
  336. package/.next/standalone/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
  337. package/.next/standalone/.next/static/chunks/23_1z47al92nz.js +0 -57
  338. package/.next/standalone/.next/static/chunks/260bomtr214m8.js +0 -34
  339. package/.next/standalone/.next/static/chunks/2xk2ra4piiuod.js +0 -34
  340. package/.next/standalone/.next/static/chunks/3b0-14cxqsprt.js +0 -1
  341. package/.next/standalone/.next/static/chunks/3jarus1_38s2z.js +0 -1
  342. package/.next/standalone/.next/static/chunks/3ssu341hav_sq.css +0 -3
  343. package/.next/standalone/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
  344. package/.next/static/chunks/0e3g0sil79cus.js +0 -21
  345. package/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
  346. package/.next/static/chunks/163b-20vwa06i.js +0 -34
  347. package/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
  348. package/.next/static/chunks/23_1z47al92nz.js +0 -57
  349. package/.next/static/chunks/260bomtr214m8.js +0 -34
  350. package/.next/static/chunks/2xk2ra4piiuod.js +0 -34
  351. package/.next/static/chunks/3b0-14cxqsprt.js +0 -1
  352. package/.next/static/chunks/3jarus1_38s2z.js +0 -1
  353. package/.next/static/chunks/3ssu341hav_sq.css +0 -3
  354. package/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
  355. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
  356. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
  357. /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
  358. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
  359. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
  360. /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
@@ -19,6 +19,7 @@ Usage:
19
19
  python3 scripts/scrape.py --auto-bridge --tiers=S,A --max-bridges=10
20
20
  python3 scripts/scrape.py --rescrape "Name" # Delete + re-scrape a bridge
21
21
  python3 scripts/scrape.py --auto-bridge --deeper # ...and finish lists read only partly
22
+ python3 scripts/scrape.py --connect URL # Auto: one connection request, no note
22
23
 
23
24
  IMPORTANT: Only scrape ONE bridge at a time. Do NOT batch multiple bridges
24
25
  in one session — LinkedIn detects automation and flags your account.
@@ -1425,7 +1426,8 @@ def note_unclear(profile_url, clear=False):
1425
1426
  # profile in the app: three app profiles must not triple the budget. The app's
1426
1427
  # Scan page reads and edits the same files (lib/linkedin-limits.js).
1427
1428
  #
1428
- # linkedin-activity.json {"searches": [epoch s, ...], "profiles": [...]}
1429
+ # linkedin-activity.json {"searches": [epoch s, ...], "profiles": [...],
1430
+ # "invites": [...]} Auto's connection requests (--connect)
1429
1431
  # scan-limits.json {"daily": 50, "monthly": 250, "profiles": 50}
1430
1432
  # 0 = no cap on searches; profile views always have one
1431
1433
  # linkedin-cooldown.json {"until": epoch s, "reason": "...", "set_at": ...}
@@ -1437,6 +1439,13 @@ DEFAULT_MONTHLY_SEARCHES = 250
1437
1439
  DEFAULT_DAILY_PROFILES = 50
1438
1440
  PROFILE_GAP = 60 # seconds between any two profile opens, whatever opened them
1439
1441
  DAY_SECONDS = 24 * 3600
1442
+ WEEK_SECONDS = 7 * DAY_SECONDS
1443
+ # Auto's connection requests (--connect), in any 24 hours and any 7 days. The
1444
+ # app shows and enforces the same two (lib/auto-connect.js INVITE_CAPS; a test
1445
+ # checks they match). LinkedIn doesn't publish its invitation limit; people
1446
+ # commonly report about 100 a week, so the week stays well under it.
1447
+ INVITE_DAY_CAP = 15
1448
+ INVITE_WEEK_CAP = 80
1440
1449
  PACIFIC = "America/Los_Angeles"
1441
1450
 
1442
1451
 
@@ -1517,9 +1526,10 @@ def _read_activity():
1517
1526
  if not isinstance(data, dict):
1518
1527
  raise ValueError("not an object")
1519
1528
  return {"searches": [float(x) for x in data.get("searches", [])],
1520
- "profiles": [float(x) for x in data.get("profiles", [])]}
1529
+ "profiles": [float(x) for x in data.get("profiles", [])],
1530
+ "invites": [float(x) for x in data.get("invites", [])]}
1521
1531
  except FileNotFoundError:
1522
- return {"searches": [], "profiles": []}
1532
+ return {"searches": [], "profiles": [], "invites": []}
1523
1533
  except Exception:
1524
1534
  # Kept aside, and replaced by a record that says today's budget is used —
1525
1535
  # written, so it holds for the whole day rather than one check. (Failing
@@ -1531,7 +1541,8 @@ def _read_activity():
1531
1541
  pass
1532
1542
  print(" (the record of LinkedIn searches couldn't be read; counting today as used)")
1533
1543
  lim = search_limits()
1534
- fresh = {"searches": [now] * max(1, lim["daily"] or 1), "profiles": [now] * lim["profiles"]}
1544
+ fresh = {"searches": [now] * max(1, lim["daily"] or 1), "profiles": [now] * lim["profiles"],
1545
+ "invites": [now] * INVITE_DAY_CAP}
1535
1546
  try:
1536
1547
  _write_json_atomic(path, fresh)
1537
1548
  except Exception:
@@ -1540,7 +1551,7 @@ def _read_activity():
1540
1551
 
1541
1552
 
1542
1553
  def charge_linkedin(kind="searches", n=1):
1543
- """Write down n searches (or profile views) made just now."""
1554
+ """Write down n searches (or profile views, or connection requests) made just now."""
1544
1555
  with _Locked("linkedin-activity"):
1545
1556
  _charge(_read_activity(), kind, n, time.time())
1546
1557
 
@@ -1548,9 +1559,9 @@ def charge_linkedin(kind="searches", n=1):
1548
1559
  def _charge(data, kind, n, now):
1549
1560
  """Add n of `kind` at `now` to the record and write it. The caller holds the lock."""
1550
1561
  keep_from = month_start_pacific(now) - 32 * DAY_SECONDS
1551
- data[kind] = [t for t in data.get(kind, []) if t >= keep_from] + [now] * n
1552
- other = "profiles" if kind == "searches" else "searches"
1553
- data[other] = [t for t in data.get(other, []) if t >= keep_from]
1562
+ for each in ("searches", "profiles", "invites"):
1563
+ data[each] = [t for t in data.get(each, []) if t >= keep_from]
1564
+ data[kind] = data.get(kind, []) + [now] * n
1554
1565
  try:
1555
1566
  _write_json_atomic(_home() / "linkedin-activity.json", data)
1556
1567
  except Exception as exc:
@@ -4133,6 +4144,529 @@ def rescrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES):
4133
4144
  scrape_bridge(bridge_name, headless=headless, max_pages=max_pages, fresh=True)
4134
4145
 
4135
4146
 
4147
+ # ---------------------------------------------------------------------------
4148
+ # Auto: one connection request, for the person whose Auto was pressed (--connect)
4149
+ # ---------------------------------------------------------------------------
4150
+ # Blake, 2026-10-03: "auto add and basically adds the person for them in the
4151
+ # card or wherever its available ... sometimes when adding it'll prompt the user
4152
+ # to have a email from their work or to send a personal note if they have
4153
+ # premium so if thats the case then have it close out of that in the scanner
4154
+ # for adding but it should be seamless."
4155
+ #
4156
+ # This is the one thing the scanner ever does on LinkedIn besides reading, and
4157
+ # only because a person pressed Auto on one card: one person per run, never a
4158
+ # batch, never on a timer. It is openly slow like everything else here: fixed
4159
+ # waits, a profile view taken like any other (cap and gap), no "human" timing
4160
+ # and no mouse movement made up to look like someone (Blake's standing scanner
4161
+ # boundary). The app looks the person up by id and refuses first (cooldown,
4162
+ # caps, already connected or requested; app/api/scraper/route.js); this checks
4163
+ # the cooldown and the caps again before anything opens.
4164
+ #
4165
+ # How it finds things, without a CSS class (TRAPS §5): by role and accessible
4166
+ # name, the way a screen reader would. Their Connect is a button (or link, or
4167
+ # menu item) labelled "Invite <their name> to connect"; the name must be theirs,
4168
+ # word for word, because the same page lists other people with Connect buttons
4169
+ # of their own ("People also viewed"). When it isn't on the front of their
4170
+ # profile, it is under "More". A bare "Connect" with no name is only taken from
4171
+ # that menu, and only when the menu is what brought it on screen: on the page
4172
+ # itself, a nameless Connect could be anyone's.
4173
+ #
4174
+ # What LinkedIn asks next is answered like this:
4175
+ # a note? "Send without a note", or the only Send there is
4176
+ # a personal note (Premium) the same: never a note, never Premium
4177
+ # their email address closed, nothing sent ("email-needed")
4178
+ # its invitation limit closed, nothing sent ("linkedin-limit")
4179
+ # anything else closed, nothing sent ("not-sent")
4180
+ # It is "sent" only when their profile then shows it Pending (or LinkedIn says
4181
+ # the invitation to them was sent). Anything else after pressing Send is
4182
+ # "unclear", and the app doesn't mark it as sent: never a confident wrong answer
4183
+ # (TRAPS §7). A security check or a sign-in wall is LinkedIn pushing back, as in
4184
+ # every other run: the page is kept, everything pauses for a day, and it stops.
4185
+ #
4186
+ # None of this has been run against LinkedIn's real page: the labels above are
4187
+ # what LinkedIn is known to use, and tests/auto-connect.test.mjs runs it against
4188
+ # a stand-in page. Watch the first real one.
4189
+
4190
+ CONNECT_LOAD_ATTEMPTS = 6 # looks for their profile to render, CONNECT_LOAD_STEP apart
4191
+ CONNECT_LOAD_STEP = 5 # seconds
4192
+ CONNECT_STEP = 1 # seconds between looks for LinkedIn's answer
4193
+ DIALOG_WAIT = 10 # seconds to wait for the invitation window after Connect
4194
+ CONFIRM_WAIT = 12 # seconds to wait for Pending after Send
4195
+
4196
+ # Every result a run can end with, printed as "Connect result: <name>" for the
4197
+ # app (lib/auto-connect.js OUTCOMES has the same names and their words; a test
4198
+ # checks the two lists match).
4199
+ CONNECT_RESULTS = (
4200
+ "sent", "already-pending", "email-needed", "no-connect", "not-sent", "unclear", "not-loaded",
4201
+ "unavailable", "linkedin-limit", "pushback", "stopped", "not-signed-in", "cooldown",
4202
+ "invites-day", "invites-week", "profiles", "not-found",
4203
+ )
4204
+
4205
+ ACTION_ROLES = ("button", "link", "menuitem")
4206
+ INVITE_LABEL = re.compile(r"^\s*Invite\s+(.+?)\s+to\s+connect\s*$", re.I)
4207
+ PENDING_LABEL = re.compile(r"^\s*Pending\b", re.I)
4208
+ BARE_CONNECT = re.compile(r"^\s*Connect\s*$", re.I)
4209
+ BARE_PENDING = re.compile(r"^\s*Pending\s*$", re.I)
4210
+ MORE_LABEL = re.compile(r"^\s*More( actions)?\s*$", re.I)
4211
+ SEND_PLAIN = re.compile(r"^\s*Send without a note\s*$", re.I)
4212
+ SEND_ANY = re.compile(r"^\s*Send( now| invitation)?\s*$", re.I)
4213
+ CLOSE_LABEL = re.compile(r"^\s*(Dismiss|Close|Cancel|Got it|Not now)\s*$", re.I)
4214
+ EMAIL_BOX = re.compile(r"e-?mail", re.I)
4215
+ EMAIL_ASK = re.compile(r"enter (their|his|her|the member.?s) e-?mail|e-?mail( address)? to connect|knows you.{0,60}e-?mail", re.I)
4216
+ INVITE_LIMIT = re.compile(r"invitation limit|reached the (weekly )?limit|too many (pending )?invitations", re.I)
4217
+ PREMIUM_NOTE = re.compile(r"premium|personali[sz]ed (invitation|note)", re.I)
4218
+ SENT_NOTICE = re.compile(r"invitation to (.+?) (?:was|has been) sent", re.I)
4219
+
4220
+
4221
+ def invites_left(now=None):
4222
+ """(how many more requests Auto may send, "day" | "week": whichever runs out first)."""
4223
+ now = now if now is not None else time.time()
4224
+ invites = _read_activity()["invites"]
4225
+ day = INVITE_DAY_CAP - sum(1 for t in invites if t > now - DAY_SECONDS)
4226
+ week = INVITE_WEEK_CAP - sum(1 for t in invites if t > now - WEEK_SECONDS)
4227
+ return (max(0, day), "day") if day <= week else (max(0, week), "week")
4228
+
4229
+
4230
+ def take_invite(now=None):
4231
+ """Write down one connection request about to be sent, if the caps allow one.
4232
+
4233
+ None once it is written down; "day" or "week" when that cap is reached and
4234
+ nothing was written. The check and the write are one step under the lock.
4235
+ """
4236
+ with _Locked("linkedin-activity"):
4237
+ now = now if now is not None else time.time()
4238
+ data = _read_activity()
4239
+ invites = data["invites"]
4240
+ if sum(1 for t in invites if t > now - DAY_SECONDS) >= INVITE_DAY_CAP:
4241
+ return "day"
4242
+ if sum(1 for t in invites if t > now - WEEK_SECONDS) >= INVITE_WEEK_CAP:
4243
+ return "week"
4244
+ _charge(data, "invites", 1, now)
4245
+ return None
4246
+
4247
+
4248
+ def invite_message(kind):
4249
+ """Why Auto sent nothing: a cap on its requests is reached, and when one frees up."""
4250
+ now = time.time()
4251
+ window = WEEK_SECONDS if kind == "week" else DAY_SECONDS
4252
+ cap = INVITE_WEEK_CAP if kind == "week" else INVITE_DAY_CAP
4253
+ sent = sorted(t for t in _read_activity()["invites"] if t > now - window)
4254
+ extra = len(sent) - cap
4255
+ free = f" The next one frees up at {_when(sent[extra] + window)}." if extra >= 0 else ""
4256
+ span = "7 days" if kind == "week" else "24 hours"
4257
+ return f"Auto has sent {cap} connection requests in the last {span}, its limit.{free} Nothing was opened or sent."
4258
+
4259
+
4260
+ def _names_words(text):
4261
+ """The words of a name, lower case, letters only (any alphabet)."""
4262
+ return re.findall(r"[^\W\d_]+", str(text or "").lower())
4263
+
4264
+
4265
+ def _same_person(shown, name):
4266
+ """Does what LinkedIn shows (a label, a heading) name this person: their first and last name, as whole words?"""
4267
+ want = _names_words(str(name or "").split(",")[0])
4268
+ have = set(_names_words(shown))
4269
+ return bool(want) and want[0] in have and want[-1] in have
4270
+
4271
+
4272
+ def _shown(loc, limit=40):
4273
+ """The items of a locator that are on screen."""
4274
+ out = []
4275
+ try:
4276
+ n = loc.count()
4277
+ except Exception:
4278
+ return out
4279
+ for i in range(min(n, limit)):
4280
+ item = loc.nth(i)
4281
+ try:
4282
+ if item.is_visible():
4283
+ out.append(item)
4284
+ except Exception:
4285
+ pass
4286
+ return out
4287
+
4288
+
4289
+ def _label(item):
4290
+ """An element's accessible name as LinkedIn wrote it: its aria-label, else its text."""
4291
+ try:
4292
+ label = item.get_attribute("aria-label")
4293
+ if label:
4294
+ return label
4295
+ except Exception:
4296
+ pass
4297
+ try:
4298
+ return item.inner_text() or ""
4299
+ except Exception:
4300
+ return ""
4301
+
4302
+
4303
+ def _enabled(item):
4304
+ try:
4305
+ return item.is_enabled()
4306
+ except Exception:
4307
+ return False
4308
+
4309
+
4310
+ def _in_main(page):
4311
+ """The profile's <main>, where its own buttons are, or the page if there is none."""
4312
+ try:
4313
+ main = page.locator("main")
4314
+ if main.count() > 0:
4315
+ return main.first
4316
+ except Exception:
4317
+ pass
4318
+ return page
4319
+
4320
+
4321
+ def _actions(scope, name_pattern):
4322
+ """Every button, link or menu item on screen whose accessible name matches."""
4323
+ found = []
4324
+ for role in ACTION_ROLES:
4325
+ try:
4326
+ found.extend(_shown(scope.get_by_role(role, name=name_pattern)))
4327
+ except Exception:
4328
+ pass
4329
+ return found
4330
+
4331
+
4332
+ def _invite_for(page, name):
4333
+ """Their own Connect: "Invite <their name> to connect", never someone else's."""
4334
+ for item in _actions(page, INVITE_LABEL):
4335
+ m = INVITE_LABEL.match(_label(item))
4336
+ if m and _same_person(m.group(1), name):
4337
+ return item
4338
+ return None
4339
+
4340
+
4341
+ def _pending_for(page, name):
4342
+ """Their profile says a request to them is pending, naming them."""
4343
+ return any(_same_person(_label(item), name) for item in _actions(page, PENDING_LABEL))
4344
+
4345
+
4346
+ def _bare_pending(page):
4347
+ """How many nameless "Pending" buttons their profile shows."""
4348
+ return len(_actions(_in_main(page), BARE_PENDING))
4349
+
4350
+
4351
+ def _sent_shown(page, name, bare_before):
4352
+ """LinkedIn shows the request to them as sent: Pending naming them, a new
4353
+ "Pending" on their profile, or its notice that the invitation to them was sent."""
4354
+ if _pending_for(page, name):
4355
+ return True
4356
+ if _bare_pending(page) > bare_before:
4357
+ return True
4358
+ try:
4359
+ for item in _shown(page.get_by_text(SENT_NOTICE)):
4360
+ m = SENT_NOTICE.search(_label(item))
4361
+ if m and _same_person(m.group(1), name):
4362
+ return True
4363
+ except Exception:
4364
+ pass
4365
+ return False
4366
+
4367
+
4368
+ def _press_escape(page):
4369
+ try:
4370
+ page.keyboard.press("Escape")
4371
+ except Exception:
4372
+ pass
4373
+
4374
+
4375
+ def _connect_under_more(page, name):
4376
+ """Connect from their profile's More menu, or None (the menu is closed again)."""
4377
+ more = _actions(_in_main(page), MORE_LABEL)
4378
+ if not more:
4379
+ return None
4380
+ bare_before = len(_actions(page, BARE_CONNECT))
4381
+ print(" Connect isn't on the front of their profile; looking under More...")
4382
+ try:
4383
+ more[0].click(timeout=10000)
4384
+ except Exception:
4385
+ return None
4386
+ time.sleep(CONNECT_STEP)
4387
+ found = _invite_for(page, name)
4388
+ if found:
4389
+ return found
4390
+ # A nameless "Connect" only when opening the menu is what brought it on screen.
4391
+ bare = _actions(page, BARE_CONNECT)
4392
+ if bare_before == 0 and len(bare) == 1:
4393
+ return bare[0]
4394
+ _press_escape(page)
4395
+ return None
4396
+
4397
+
4398
+ def _dialog(page):
4399
+ """LinkedIn's window on top of the page (the newest), or None."""
4400
+ shown = []
4401
+ for role in ("dialog", "alertdialog"):
4402
+ try:
4403
+ shown.extend(_shown(page.get_by_role(role)))
4404
+ except Exception:
4405
+ pass
4406
+ return shown[-1] if shown else None
4407
+
4408
+
4409
+ def _close_dialog(page, dialog):
4410
+ """Close LinkedIn's window without answering it: its Dismiss (or Close, Cancel), else Escape."""
4411
+ try:
4412
+ close = _shown(dialog.get_by_role("button", name=CLOSE_LABEL))
4413
+ if close:
4414
+ close[0].click(timeout=5000)
4415
+ else:
4416
+ _press_escape(page)
4417
+ except Exception:
4418
+ _press_escape(page)
4419
+ time.sleep(CONNECT_STEP)
4420
+
4421
+
4422
+ def _asks_for_email(dialog, text):
4423
+ """The invitation window wants their email address before it sends."""
4424
+ for loc in (lambda: dialog.get_by_role("textbox", name=EMAIL_BOX), lambda: dialog.locator("input[type=email]")):
4425
+ try:
4426
+ if _shown(loc()):
4427
+ return True
4428
+ except Exception:
4429
+ pass
4430
+ return bool(EMAIL_ASK.search(text or ""))
4431
+
4432
+
4433
+ def _send_button(dialog):
4434
+ """"Send without a note", or the one Send there is. Never "Add a note", never Premium."""
4435
+ plain = [b for b in _shown(dialog.get_by_role("button", name=SEND_PLAIN)) if _enabled(b)]
4436
+ if plain:
4437
+ return plain[0]
4438
+ sends = [b for b in _shown(dialog.get_by_role("button", name=SEND_ANY)) if _enabled(b)]
4439
+ return sends[0] if len(sends) == 1 else None
4440
+
4441
+
4442
+ def _dialog_text(dialog):
4443
+ try:
4444
+ return dialog.inner_text() or ""
4445
+ except Exception:
4446
+ return ""
4447
+
4448
+
4449
+ def _connect_pushback(page, why):
4450
+ """LinkedIn pushed back while Auto was on their profile: keep the page, pause for a day."""
4451
+ print(f" LinkedIn is pushing back ({why}). Stopping here.")
4452
+ _keep_pushback_evidence(page, why)
4453
+ set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {why}")
4454
+
4455
+
4456
+ def _confirm_sent(page, name, bare_before):
4457
+ """After Send: "sent" once their profile shows it, else "unclear" (or "pushback", "linkedin-limit")."""
4458
+ waited = 0
4459
+ while waited < CONFIRM_WAIT:
4460
+ time.sleep(CONNECT_STEP)
4461
+ waited += CONNECT_STEP
4462
+ why = _pushback(page)
4463
+ if why:
4464
+ _connect_pushback(page, why)
4465
+ return "pushback"
4466
+ dialog = _dialog(page)
4467
+ if dialog is not None and INVITE_LIMIT.search(_dialog_text(dialog)):
4468
+ _close_dialog(page, dialog)
4469
+ print(" LinkedIn says this account has reached its invitation limit.")
4470
+ return "linkedin-limit"
4471
+ if _sent_shown(page, name, bare_before):
4472
+ return "sent"
4473
+ leftover = _dialog(page)
4474
+ if leftover is not None:
4475
+ _close_dialog(page, leftover)
4476
+ return "unclear"
4477
+
4478
+
4479
+ def _answer_invitation(page, name, bare_before):
4480
+ """What LinkedIn asks once Connect is pressed, answered without a note."""
4481
+ dialog = None
4482
+ waited = 0
4483
+ while waited < DIALOG_WAIT:
4484
+ time.sleep(CONNECT_STEP)
4485
+ waited += CONNECT_STEP
4486
+ dialog = _dialog(page)
4487
+ if dialog is not None:
4488
+ break
4489
+ if _sent_shown(page, name, bare_before):
4490
+ # Some accounts send straight from Connect, with nothing to answer.
4491
+ charge_linkedin("invites")
4492
+ print(" LinkedIn sent it straight from Connect; their profile shows it pending.")
4493
+ return "sent"
4494
+ if dialog is None:
4495
+ # Nothing came up and nothing shows it pending. It may have gone, so it
4496
+ # counts toward Auto's caps; it isn't called sent.
4497
+ charge_linkedin("invites")
4498
+ print(" Nothing came up after Connect, and their profile doesn't show it pending.")
4499
+ return "unclear"
4500
+
4501
+ text = _dialog_text(dialog)
4502
+ if _asks_for_email(dialog, text):
4503
+ _close_dialog(page, dialog)
4504
+ print(" LinkedIn wants their email address before it sends this one. Closed that; nothing was sent.")
4505
+ return "email-needed"
4506
+ if INVITE_LIMIT.search(text):
4507
+ _close_dialog(page, dialog)
4508
+ print(" LinkedIn says this account has reached its invitation limit. Closed that; nothing was sent.")
4509
+ return "linkedin-limit"
4510
+ send = _send_button(dialog)
4511
+ if send is None:
4512
+ _close_dialog(page, dialog)
4513
+ print(" LinkedIn's invitation window had no way to send without a note. Closed it; nothing was sent.")
4514
+ return "not-sent"
4515
+ if PREMIUM_NOTE.search(text):
4516
+ print(" LinkedIn offered a personal note (Premium). Sending without one.")
4517
+ if stop_requested() or _window_closed(page):
4518
+ _close_dialog(page, dialog)
4519
+ return "stopped"
4520
+ taken = take_invite()
4521
+ if taken:
4522
+ _close_dialog(page, dialog)
4523
+ print(" " + invite_message(taken))
4524
+ return f"invites-{taken}"
4525
+ print(" Sending without a note...")
4526
+ try:
4527
+ send.click(timeout=10000)
4528
+ except Exception as exc:
4529
+ print(f" Send couldn't be pressed ({str(exc)[:60]}).")
4530
+ _close_dialog(page, dialog)
4531
+ return "unclear"
4532
+ result = _confirm_sent(page, name, bare_before)
4533
+ if result == "sent":
4534
+ print(f" Sent. {name}'s profile shows the request pending.")
4535
+ elif result == "unclear":
4536
+ print(" Send was pressed, but their profile doesn't show the request pending. Not calling it sent.")
4537
+ return result
4538
+
4539
+
4540
+ def _connect_on_page(page, profile_url, name):
4541
+ """Open their profile (one profile view) and send the request. Returns a CONNECT_RESULTS name."""
4542
+ why = _wait_for_profile_view(page)
4543
+ if why == "budget":
4544
+ print(budget_message("profiles"))
4545
+ return "profiles"
4546
+ if why:
4547
+ return "stopped"
4548
+
4549
+ print(f" Opening {name}'s profile...")
4550
+ try:
4551
+ page.goto(profile_url, wait_until="commit")
4552
+ except Exception as exc:
4553
+ print(f" Navigation slow: {str(exc)[:50]}... carrying on")
4554
+
4555
+ shown = False
4556
+ for _ in range(CONNECT_LOAD_ATTEMPTS):
4557
+ time.sleep(CONNECT_LOAD_STEP)
4558
+ if stop_requested() or _window_closed(page):
4559
+ return "stopped"
4560
+ why = _pushback(page)
4561
+ if why:
4562
+ _connect_pushback(page, why)
4563
+ return "pushback"
4564
+ if _profile_unavailable(page):
4565
+ print(" LinkedIn says their profile isn't available. Nothing was sent.")
4566
+ return "unavailable"
4567
+ try:
4568
+ shown = bool(page.evaluate(PROFILE_SHOWN_JS, name))
4569
+ except Exception:
4570
+ shown = False
4571
+ if shown:
4572
+ break
4573
+ if not shown:
4574
+ print(" Their profile didn't load properly, so nothing was pressed.")
4575
+ return "not-loaded"
4576
+
4577
+ if _pending_for(page, name):
4578
+ print(f" A request to {name} is already pending on LinkedIn. Nothing new was sent.")
4579
+ return "already-pending"
4580
+ bare_before = _bare_pending(page)
4581
+ target = _invite_for(page, name) or _connect_under_more(page, name)
4582
+ if target is None:
4583
+ print(" LinkedIn shows no Connect for them here (only Follow or Message, say). Nothing was sent.")
4584
+ return "no-connect"
4585
+ if stop_requested() or _window_closed(page):
4586
+ return "stopped"
4587
+ left, kind = invites_left()
4588
+ if left <= 0:
4589
+ print(" " + invite_message(kind))
4590
+ return f"invites-{kind}"
4591
+ print(f" Pressing Connect for {name}...")
4592
+ try:
4593
+ target.click(timeout=10000)
4594
+ except Exception as exc:
4595
+ print(f" Connect couldn't be pressed ({str(exc)[:60]}). Nothing was sent.")
4596
+ return "not-sent"
4597
+ return _answer_invitation(page, name, bare_before)
4598
+
4599
+
4600
+ def connect_person(profile_url, name=None, headless=False):
4601
+ """Send one connection request, without a note, to the person at profile_url.
4602
+
4603
+ Returns one of CONNECT_RESULTS; main() prints it as "Connect result: ...".
4604
+ Nothing opens during a cooldown, with Auto's caps reached, or with no
4605
+ profile view left: each is checked before a browser starts.
4606
+ """
4607
+ from playwright.sync_api import sync_playwright
4608
+
4609
+ cd = read_cooldown()
4610
+ if cd:
4611
+ print(cooldown_message(cd))
4612
+ return "cooldown"
4613
+ left, kind = invites_left()
4614
+ if left <= 0:
4615
+ print(invite_message(kind))
4616
+ return f"invites-{kind}"
4617
+ if profiles_left() <= 0:
4618
+ print(budget_message("profiles"))
4619
+ return "profiles"
4620
+ if not name:
4621
+ params = {"profile_url": f"eq.{profile_url}", "limit": "1"}
4622
+ if _active_user_id:
4623
+ params["user_id"] = f"eq.{_active_user_id}"
4624
+ rows = read_connections(params=params)
4625
+ if not rows:
4626
+ print(f" {profile_url} isn't in your network on this computer. Nothing was sent.")
4627
+ return "not-found"
4628
+ name = rows[0].get("name") or ""
4629
+
4630
+ print(f"\n=== Sending {name} a connection request, without a note ===\n")
4631
+ result = None
4632
+ try:
4633
+ with sync_playwright() as p:
4634
+ browser = p.chromium.launch_persistent_context(
4635
+ user_data_dir=get_scraper_profile_path(),
4636
+ headless=headless,
4637
+ channel="chrome",
4638
+ args=["--disable-blink-features=AutomationControlled"],
4639
+ timeout=120000,
4640
+ )
4641
+ try:
4642
+ page = browser.pages[0] if browser.pages else browser.new_page()
4643
+ page.set_default_timeout(120000)
4644
+ page.set_default_navigation_timeout(120000)
4645
+ # A security check or a sign-out here is LinkedIn pushing back,
4646
+ # as for a circle scan: it raises, after pausing everything.
4647
+ if not ensure_logged_in(page, stop_on_checkpoint=True):
4648
+ result = "stopped" if stop_requested() else "not-signed-in"
4649
+ else:
4650
+ result = _connect_on_page(page, profile_url, name)
4651
+ finally:
4652
+ try:
4653
+ browser.close()
4654
+ except Exception:
4655
+ pass
4656
+ except LinkedInPushedBack as exc:
4657
+ print(f" LinkedIn pushed back ({exc.reason}). Nothing was sent.")
4658
+ result = "pushback"
4659
+ except Exception:
4660
+ # A stop takes the browser down with it. Anything else is a failure
4661
+ # with its reason, never a result that could read as sent.
4662
+ if result is None:
4663
+ if stop_requested():
4664
+ result = "stopped"
4665
+ else:
4666
+ raise
4667
+ return result
4668
+
4669
+
4136
4670
  def run_server(port=5555):
4137
4671
  """
4138
4672
  Run a local HTTP server that the website's Setup page can talk to.
@@ -4397,6 +4931,12 @@ Examples:
4397
4931
  "The app's Scan buttons use it, since two connections can share a name")
4398
4932
  parser.add_argument("--only-unfinished", action="store_true",
4399
4933
  help="With --auto-bridge: only people whose read was cut short (Resume all)")
4934
+ parser.add_argument("--connect", type=str,
4935
+ help="Auto: send one connection request, without a note, to the person at this LinkedIn "
4936
+ f"profile URL. Never with a note; at most {INVITE_DAY_CAP} in any 24 hours and "
4937
+ f"{INVITE_WEEK_CAP} in any 7 days")
4938
+ parser.add_argument("--connect-name", type=str, default="",
4939
+ help="With --connect: their name as the app has it (else it is looked up in the app)")
4400
4940
  parser.add_argument("--save-photos", action="store_true",
4401
4941
  help="Save the profile photos still kept as links to LinkedIn, then exit "
4402
4942
  "(every scan also does this at its end)")
@@ -4408,7 +4948,10 @@ Examples:
4408
4948
  args.max_pages = max(1, min(LINKEDIN_MAX_PAGES, args.max_pages))
4409
4949
  # The Scan page's speed. Signing in searches nothing, so it isn't said there.
4410
4950
  _pace = apply_pace()
4411
- if not args.login:
4951
+ if args.connect:
4952
+ # Auto reads no pages: only the gap before opening their profile applies.
4953
+ print(f"Speed: {_pace.capitalize()}. Profiles at least {PROFILE_GAP}s apart.")
4954
+ elif not args.login:
4412
4955
  print(f"Speed: {_pace.capitalize()}. {PAGE_PAUSE}s before each page of results, {CHUNK_COOLDOWN}s more after "
4413
4956
  f"every {SAVE_EVERY_PAGES}, and profiles at least {PROFILE_GAP}s apart.")
4414
4957
 
@@ -4454,6 +4997,23 @@ Examples:
4454
4997
  # A budget or a cooldown ending a run is the design working, not a failure:
4455
4998
  # say why and exit 0, so the Scan page doesn't show it as a red error.
4456
4999
  try:
5000
+ if args.connect:
5001
+ # Auto (lib/auto-connect.js): one request, then out. Its last line is
5002
+ # the result the app reads; no photos are saved after it, since it
5003
+ # read no one. A pushback or no sign-in ends it as a failure, with why.
5004
+ if not re.match(r"^https?://([a-z]{2,3}\.|www\.)?linkedin\.com/in/[^/?#\s]+/?$", args.connect, re.I):
5005
+ print(" --connect takes a LinkedIn profile URL (https://www.linkedin.com/in/...).", file=sys.stderr)
5006
+ raise SystemExit(2)
5007
+ result = connect_person(args.connect, name=args.connect_name.strip() or None, headless=args.headless)
5008
+ print(f"Connect result: {result}", flush=True)
5009
+ if result == "pushback":
5010
+ print(f"\n LinkedIn pushed back, so nothing more is sent. {_pushback_advice('')}\n", file=sys.stderr)
5011
+ raise SystemExit(1)
5012
+ if result == "not-signed-in":
5013
+ print("\n LinkedIn isn't signed in, so nothing was sent. Sign in with --login (or the Scan page), "
5014
+ "then try again.\n", file=sys.stderr)
5015
+ raise SystemExit(1)
5016
+ raise SystemExit(0)
4457
5017
  if args.company:
4458
5018
  # The server mode scraped and pushed as one step; the CLI has to do the
4459
5019
  # same or the scan appears to work and saves nothing.