six-degrees 0.4.8 → 0.4.10

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 (239) 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.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 +13 -13
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  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.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +2 -1
  23. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/route.js +2 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/scraper/route.js +2 -2
  39. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/settings/route.js +3 -2
  41. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +2 -1
  43. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +2 -1
  45. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/social/messages/route/app-paths-manifest.json +3 -0
  48. package/.next/standalone/.next/server/app/api/social/messages/route/build-manifest.json +12 -0
  49. package/.next/standalone/.next/server/app/api/social/messages/route/server-reference-manifest.json +4 -0
  50. package/.next/standalone/.next/server/app/api/social/messages/route.js +9 -0
  51. package/.next/standalone/.next/server/app/api/social/messages/route.js.map +5 -0
  52. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -0
  53. package/.next/standalone/.next/server/app/api/social/messages/route_client-reference-manifest.js +3 -0
  54. package/.next/standalone/.next/server/app/api/social/route.js +4 -2
  55. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/import.html +1 -1
  65. package/.next/standalone/.next/server/app/import.rsc +15 -15
  66. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  67. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  68. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
  69. package/.next/standalone/.next/server/app/index.html +1 -1
  70. package/.next/standalone/.next/server/app/index.rsc +15 -15
  71. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  72. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  73. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  74. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/launch.html +2 -2
  77. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  78. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  79. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
  81. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/paths.html +1 -1
  86. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  87. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  88. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
  90. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/profile.html +1 -1
  93. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  94. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  95. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
  97. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/queue.html +1 -1
  100. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  101. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  102. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  103. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
  104. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/scores.html +1 -1
  107. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  108. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  109. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  110. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
  111. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/settings.html +1 -1
  114. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  115. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  116. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  117. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
  118. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/setup.html +1 -1
  121. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  122. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  123. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  124. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
  125. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  127. package/.next/standalone/.next/server/app/social.html +1 -1
  128. package/.next/standalone/.next/server/app/social.rsc +15 -15
  129. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  130. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  131. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
  132. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06mi9k2._.js +3 -0
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +8 -0
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +8 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +3 -0
  139. package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
  140. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_messages_route_actions_0skh09r.js +3 -0
  143. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +8 -0
  144. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
  145. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +1 -1
  146. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +1 -1
  147. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +1 -1
  148. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +1 -1
  149. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  150. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +1 -1
  151. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +1 -1
  152. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +1 -1
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +1 -1
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +1 -1
  155. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +1 -1
  156. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +1 -1
  157. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +1 -1
  158. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +1 -1
  159. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +3 -0
  160. package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1feicqq._.js → [root-of-the-server]__0qdeuyo._.js} +1 -1
  162. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +3 -0
  163. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/ssr/{_1mj739k._.js → _0l8cxmo._.js} +2 -2
  165. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +3 -0
  166. package/.next/standalone/.next/server/chunks/ssr/{_0wxbq78._.js → app_components_19c2d9c._.js} +2 -2
  167. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
  169. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  170. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
  171. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1wax83z._.js → node_modules_0h91jdk._.js} +2 -2
  173. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1nkgb45._.js → node_modules_1f8t_rp._.js} +1 -1
  174. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  175. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  176. package/.next/standalone/.next/server/pages/404.html +1 -1
  177. package/.next/standalone/.next/server/pages/500.html +1 -1
  178. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  179. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  180. package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +1 -0
  181. package/.next/standalone/.next/static/chunks/{3q3pmt5vv-4pi.js → 160wjb73ddwqd.js} +1 -1
  182. package/.next/standalone/.next/static/chunks/{1z-zp93cyoxvz.js → 1c_n0973t07l0.js} +1 -1
  183. package/.next/standalone/.next/static/chunks/{0bo1dxn5gwzpk.css → 1xptlvtcz6654.css} +1 -1
  184. package/.next/standalone/.next/static/chunks/{2_wcobfqry2uj.js → 206rm6j42gl_a.js} +1 -1
  185. package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +1 -0
  186. package/.next/{static/chunks/0g_p4p7oci18u.js → standalone/.next/static/chunks/2kfhc58df2xyo.js} +1 -1
  187. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  188. package/.next/standalone/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
  189. package/.next/standalone/CHANGELOG.md +48 -0
  190. package/.next/standalone/app/api/social/messages/route.js +130 -0
  191. package/.next/standalone/app/api/social/route.js +89 -47
  192. package/.next/standalone/app/components/GalaxyLab.js +4 -1
  193. package/.next/standalone/app/components/ScanStatusBar.js +112 -50
  194. package/.next/standalone/app/components/settings/DataSection.js +2 -1
  195. package/.next/standalone/app/globals.css +4 -0
  196. package/.next/standalone/app/setup/page.js +4 -3
  197. package/.next/standalone/app/social/Conversations.js +246 -0
  198. package/.next/standalone/app/social/page.js +88 -9
  199. package/.next/standalone/lib/galaxy-lab.js +6 -1
  200. package/.next/standalone/lib/island.js +36 -0
  201. package/.next/standalone/lib/linkedin-export.js +175 -5
  202. package/.next/standalone/lib/social-store.js +73 -0
  203. package/.next/standalone/package-lock.json +2 -2
  204. package/.next/standalone/package.json +1 -1
  205. package/.next/standalone/scripts/scrape.py +156 -23
  206. package/.next/static/chunks/07dyt47mp4s9-.js +1 -0
  207. package/.next/static/chunks/{3q3pmt5vv-4pi.js → 160wjb73ddwqd.js} +1 -1
  208. package/.next/static/chunks/{1z-zp93cyoxvz.js → 1c_n0973t07l0.js} +1 -1
  209. package/.next/static/chunks/{0bo1dxn5gwzpk.css → 1xptlvtcz6654.css} +1 -1
  210. package/.next/static/chunks/{2_wcobfqry2uj.js → 206rm6j42gl_a.js} +1 -1
  211. package/.next/static/chunks/2coxzjm_k6ilr.js +1 -0
  212. package/.next/{standalone/.next/static/chunks/0g_p4p7oci18u.js → static/chunks/2kfhc58df2xyo.js} +1 -1
  213. package/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  214. package/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
  215. package/CHANGELOG.md +48 -0
  216. package/lib/galaxy-lab.js +6 -1
  217. package/lib/island.js +36 -0
  218. package/lib/linkedin-export.js +175 -5
  219. package/lib/social-store.js +73 -0
  220. package/package.json +1 -1
  221. package/scripts/scrape.py +156 -23
  222. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +0 -8
  223. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dmc7-t._.js +0 -8
  224. package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +0 -3
  225. package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +0 -3
  226. package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +0 -3
  227. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +0 -3
  228. package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  229. package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +0 -1
  230. package/.next/standalone/.next/static/chunks/15qbli9udo0vh.js +0 -1
  231. package/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  232. package/.next/static/chunks/14m1zu-8warte.js +0 -1
  233. package/.next/static/chunks/15qbli9udo0vh.js +0 -1
  234. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_buildManifest.js +0 -0
  235. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_clientMiddlewareManifest.js +0 -0
  236. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_ssgManifest.js +0 -0
  237. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_buildManifest.js +0 -0
  238. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_clientMiddlewareManifest.js +0 -0
  239. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → 7zWJnSkfSoRhRsqY6ZKtD}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.10] - 2026-09-29
10
+
11
+ ### Changed
12
+ - **The scan status bar is now a notch.** A small pill hangs from the bottom of the header,
13
+ centred, on every page. It shows nothing while nothing runs, only a tiny dimmed *Auto scan*
14
+ while the all-day mode waits, and while a scan runs, what it's doing with a thin progress
15
+ line. Hover or click to open it: who, today's searches, the latest line, *Details* and
16
+ *Stop*. Other long jobs show there too, starting with recording the Galaxy's replay.
17
+
18
+ ## [0.4.9] - 2026-09-29
19
+
20
+ ### Added
21
+ - **Conversations, in the Social tab.** Every conversation with one of your connections, from your
22
+ export and the live messages sync: who it's with (name and tier), when it was last active, who
23
+ wrote last, unread, how many messages, and *Open on LinkedIn* (the thread when the sync found
24
+ its link, else their profile). Waiting on you comes first, then S and A tier, then the most
25
+ recent. Group conversations are shown and marked, and aren't counted in warmth. A search box
26
+ finds people by name.
27
+ - **Keep my messages on this computer**, a switch that's off unless you turn it on. When it's on,
28
+ the messages themselves, what other people wrote to you included, are kept in the app's data
29
+ folder (a file of their own beside the Social tab's), and a conversation opens as a thread,
30
+ newest at the bottom, a page at a time so a thread of thousands opens as fast as a short one.
31
+ The search box then finds what was said, too. They're never sent anywhere, and "Save a copy of
32
+ my network" doesn't carry them (Settings says so). Switching it off asks first, then deletes
33
+ them; *Forget it* deletes them with everything else.
34
+ - **The live messages sync reads a little more of the list.** Each conversation's link, whether
35
+ it's a group, and its newest message: who wrote last, which now feeds warmth and Waiting on you
36
+ where your export is missing or older, and its words, which go to the app only while Keep my
37
+ messages is on. It still opens no conversation, and its saved samples still keep no words.
38
+
39
+ ### Changed
40
+ - **Auto scan is slower and has its own ceilings.** Sittings of 8 pages, then an hour's rest;
41
+ searches only from 9:00 to 18:00; never more than 40 searches in 24 hours or 200 in 7 days,
42
+ however high the daily budget is set (the account restricted on 28 September did 373 in a
43
+ day with the budget at 500); and two days with no searches after any check from LinkedIn.
44
+ - **The scan status bar sits under the buttons.** On each page it now sits in its own row
45
+ under the header, pushing the page down instead of covering it. On Network Circle it floats
46
+ just under the header, measured, so a header that wraps to two rows no longer hides it.
47
+
48
+ ### Fixed
49
+ - **The live messages sync keeps what it finds.** It read your conversations but matched none of
50
+ them: the pattern for a profile link never matched, and LinkedIn's messaging gives a member
51
+ link your connections list doesn't have. It now matches each person to your connections by
52
+ link, or by a name only one connection has (the name isn't kept), and scrolls the list's own
53
+ panel so more than the first 20 conversations load.
54
+ - **Messaging samples keep no words.** The sync's saved samples of LinkedIn's data (for tuning
55
+ the reader) now drop every piece of writing and every name before they're saved.
56
+
9
57
  ## [0.4.8] - 2026-09-29
10
58
 
11
59
  ### Changed
package/lib/galaxy-lab.js CHANGED
@@ -215,7 +215,12 @@ export function mergeSocial(social) {
215
215
  for (const [url, v] of Object.entries(social?.live || {})) {
216
216
  const k = keyFor({ profile_url: url });
217
217
  const cur = merged.get(k) || { total: 0, recent: 0 };
218
- if (v.last && (!cur.last || v.last > cur.last)) cur.last = v.last;
218
+ // The live sync's word on who wrote last counts when it's the newer news:
219
+ // no export yet, or the conversation moved on since the export was made.
220
+ if (v.last && (!cur.last || v.last > cur.last)) {
221
+ cur.last = v.last;
222
+ if (typeof v.lastFromThem === 'boolean') cur.lastFromThem = v.lastFromThem;
223
+ }
219
224
  cur.unread = v.unread;
220
225
  cur.total = Math.max(cur.total || 0, 1);
221
226
  merged.set(k, cur);
package/lib/island.js ADDED
@@ -0,0 +1,36 @@
1
+ // The notch (app/components/ScanStatusBar.js): one small pill hanging under the
2
+ // header that shows what the app is doing. The scanner shows there on its own;
3
+ // anything else that runs for a while (a replay being recorded, an import) can
4
+ // show there too, through this list.
5
+ //
6
+ // const done = showActivity({ id: 'replay', label: 'Recording the replay', detail: '12 s' });
7
+ // … later: done();
8
+ //
9
+ // An activity is { id, label, detail?, progress? (0–1), tone? ('run' | 'idle' | 'warn') }.
10
+ // Showing the same id again replaces it. The list only changes when something
11
+ // in it does, as useSyncExternalStore needs.
12
+
13
+ const listeners = new Set();
14
+ let activities = [];
15
+
16
+ export const activitiesNow = () => activities;
17
+ const EMPTY = [];
18
+ export const noActivities = () => EMPTY;
19
+
20
+ export function watchActivities(f) {
21
+ listeners.add(f);
22
+ return () => listeners.delete(f);
23
+ }
24
+
25
+ export function showActivity(a) {
26
+ if (!a?.id) return () => {};
27
+ activities = [...activities.filter((x) => x.id !== a.id), a];
28
+ listeners.forEach((f) => f());
29
+ return () => clearActivity(a.id);
30
+ }
31
+
32
+ export function clearActivity(id) {
33
+ if (!activities.some((x) => x.id === id)) return;
34
+ activities = activities.filter((x) => x.id !== id);
35
+ listeners.forEach((f) => f());
36
+ }
@@ -6,6 +6,10 @@
6
6
  //
7
7
  // people per profile: when you last messaged, who wrote last, how many
8
8
  // messages (1:1 conversations only). Never a message's text.
9
+ // conversations per conversation: who's in it, when it was last active, who
10
+ // wrote last, how many messages. The text itself only when you turn
11
+ // on Keep my messages, and then in a file of its own
12
+ // (app/api/social/messages).
9
13
  // chapters your jobs (Positions.csv) and the connections made in each
10
14
  // posts your posts (Shares.csv) and the connections that came the week after
11
15
  // invites requests to and from you (Invitations.csv), when the export has them
@@ -49,14 +53,29 @@ export function exportDate(s) {
49
53
 
50
54
  const urlKey = (u) => keyFor({ profile_url: u });
51
55
 
56
+ /** You, in messages.csv: the sender in the most conversations. */
57
+ function selfOf(rows = []) {
58
+ const senders = new Map();
59
+ for (const r of rows) {
60
+ const from = urlKey(r['sender profile url']);
61
+ const id = r['conversation id'];
62
+ if (!id || !from) continue;
63
+ if (!senders.has(from)) senders.set(from, new Set());
64
+ senders.get(from).add(id);
65
+ }
66
+ let self = null;
67
+ for (const [who, ids] of senders) if (!self || ids.size > senders.get(self).size) self = who;
68
+ return self;
69
+ }
70
+
52
71
  /**
53
72
  * From messages.csv, per person (by profile): { last, lastFromThem, total, recent }.
54
73
  * `self` is you: the sender in the most conversations. Only 1:1 conversations
55
74
  * count; group threads say little about one relationship. No text is read.
56
75
  */
57
76
  export function messageStats(rows = []) {
77
+ const self = selfOf(rows);
58
78
  const convos = new Map();
59
- const senders = new Map();
60
79
  for (const r of rows) {
61
80
  const from = urlKey(r['sender profile url']);
62
81
  const id = r['conversation id'];
@@ -69,11 +88,7 @@ export function messageStats(rows = []) {
69
88
  c.people.add(from);
70
89
  to.forEach((p) => c.people.add(p));
71
90
  c.msgs.push({ from, t });
72
- if (!senders.has(from)) senders.set(from, new Set());
73
- senders.get(from).add(id);
74
91
  }
75
- let self = null;
76
- for (const [who, ids] of senders) if (!self || ids.size > senders.get(self).size) self = who;
77
92
  let asOf = 0;
78
93
  const people = new Map();
79
94
  for (const c of convos.values()) {
@@ -96,6 +111,104 @@ export function messageStats(rows = []) {
96
111
  return { self, asOf: asOf || null, people };
97
112
  }
98
113
 
114
+ /**
115
+ * Every conversation in messages.csv you're part of, one record each:
116
+ * { id, title, people, group, messages: [{ t, fromMe, text }] }, messages oldest
117
+ * first. `people` is everyone but you, by profile key; more than one makes it a
118
+ * group. You are found the way messageStats finds you. The words (the subject,
119
+ * when there is one, then the message) are read only with keepText: without it
120
+ * a message is a date and who sent it, and the title (a group's name) is left
121
+ * out too.
122
+ */
123
+ export function buildConversations(rows = [], { keepText = false } = {}) {
124
+ const self = selfOf(rows);
125
+ const convos = new Map();
126
+ for (const r of rows) {
127
+ const from = urlKey(r['sender profile url']);
128
+ const id = r['conversation id'];
129
+ if (!id || !from) continue;
130
+ const t = exportDate(r.date);
131
+ if (t == null) continue;
132
+ if (!convos.has(id)) convos.set(id, { id, title: '', everyone: new Set(), messages: [] });
133
+ const c = convos.get(id);
134
+ c.everyone.add(from);
135
+ String(r['recipient profile urls'] || '').split(/[,;\s]+/).filter(Boolean).forEach((u) => c.everyone.add(urlKey(u)));
136
+ const m = { t, fromMe: from === self };
137
+ if (keepText) {
138
+ m.text = [r.subject, r.content].map((x) => String(x || '').trim()).filter(Boolean).join('\n\n');
139
+ if (!c.title && r['conversation title']) c.title = String(r['conversation title']).trim();
140
+ }
141
+ c.messages.push(m);
142
+ }
143
+ const conversations = [];
144
+ for (const c of convos.values()) {
145
+ if (!c.everyone.has(self)) continue;
146
+ const people = [...c.everyone].filter((p) => p && p !== self).sort();
147
+ if (!people.length) continue;
148
+ c.messages.sort((a, b) => a.t - b.t);
149
+ const out = { id: c.id, people, group: people.length > 1, messages: c.messages };
150
+ if (keepText) out.title = c.title;
151
+ conversations.push(out);
152
+ }
153
+ return { self, conversations };
154
+ }
155
+
156
+ /** What the Conversations list keeps of one conversation, never any words: { id, people, group, last, lastFromThem, count }. */
157
+ export function conversationSummary(c) {
158
+ const last = c.messages[c.messages.length - 1];
159
+ return {
160
+ id: c.id,
161
+ people: c.people,
162
+ group: c.group,
163
+ last: last ? last.t : null,
164
+ lastFromThem: last ? !last.fromMe : null,
165
+ count: c.messages.length,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * The Conversations list: the export's conversations with what the live sync
171
+ * adds (unread, the thread's link, and a newer last message), plus a
172
+ * conversation for each person the sync found and the export doesn't have yet
173
+ * (id `live:<key>`, count unknown).
174
+ */
175
+ export function conversationList(list = [], live = {}) {
176
+ const out = (Array.isArray(list) ? list : []).map((c) => ({ ...c, unread: null, threadUrl: null }));
177
+ const oneToOne = new Map();
178
+ for (const c of out) {
179
+ if (c.group || c.people?.length !== 1) continue;
180
+ const had = oneToOne.get(c.people[0]);
181
+ if (!had || (c.last || 0) > (had.last || 0)) oneToOne.set(c.people[0], c);
182
+ }
183
+ for (const [key, v] of Object.entries(live || {})) {
184
+ if (!v || typeof v !== 'object') continue;
185
+ let c = oneToOne.get(key);
186
+ if (!c) {
187
+ c = { id: `live:${key}`, people: [key], group: false, last: null, lastFromThem: null, count: null, unread: null, threadUrl: null };
188
+ out.push(c);
189
+ oneToOne.set(key, c);
190
+ }
191
+ if (v.last && (!c.last || v.last > c.last)) {
192
+ c.last = v.last;
193
+ if (typeof v.lastFromThem === 'boolean') c.lastFromThem = v.lastFromThem;
194
+ }
195
+ c.unread = Number.isFinite(v.unread) ? v.unread : c.unread;
196
+ c.threadUrl = v.threadUrl || c.threadUrl;
197
+ }
198
+ return out;
199
+ }
200
+
201
+ /**
202
+ * The order the Conversations list shows: waiting on you (they wrote last, one
203
+ * to one) first, then your S and A tier people, then the most recent.
204
+ * `tierOf(conversation)` gives its best tier.
205
+ */
206
+ export function sortConversations(list = [], tierOf = () => null) {
207
+ const waiting = (c) => (!c.group && c.lastFromThem === true ? 0 : 1);
208
+ const tier = (c) => ({ S: 0, A: 1 }[tierOf(c)] ?? 2);
209
+ return [...list].sort((a, b) => waiting(a) - waiting(b) || tier(a) - tier(b) || (b.last || 0) - (a.last || 0));
210
+ }
211
+
99
212
  /** Warm (in the last 30 days, or 3+ in 90), cool (within a year), dormant (longer ago), or never. */
100
213
  export function warmthOf(stat, asOf) {
101
214
  if (!stat || !stat.total) return 'never';
@@ -154,3 +267,60 @@ export function invitationSplit(rows = []) {
154
267
  }
155
268
  return { incoming, outgoing, total: incoming + outgoing };
156
269
  }
270
+
271
+ const nameKey = (s) => String(s || '').split(',')[0].normalize('NFD').replace(/[̀-ͯ]/g, '')
272
+ .toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
273
+
274
+ const THREAD_LINK = /^https:\/\/www\.linkedin\.com\/messaging\/[\w\-/%=.]*$/;
275
+
276
+ /** A link to a LinkedIn messaging thread, or null: nothing else is ever put behind "Open on LinkedIn". */
277
+ export function threadLink(u) {
278
+ const v = String(u || '').trim();
279
+ return THREAD_LINK.test(v) ? v : null;
280
+ }
281
+
282
+ /** A message the live sync read, as it's kept: { t, fromMe, text }, or null. */
283
+ function previewOf(p) {
284
+ if (!p || typeof p !== 'object') return null;
285
+ const t = Number(p.t) || null;
286
+ const text = typeof p.text === 'string' ? p.text.slice(0, 20000) : null;
287
+ if (!t && !text) return null;
288
+ return { t, fromMe: p.fromMe === true, text };
289
+ }
290
+
291
+ /**
292
+ * The live messages sync's people, matched to your connections. LinkedIn's
293
+ * messaging gives a member link (/in/ACoA…) your connections list doesn't have,
294
+ * so a person is matched by their link when it's the same, otherwise by their
295
+ * name when exactly one connection has it. Only the match is kept, never the
296
+ * name. Returns { live: { connectionKey: { last, unread, threadUrl,
297
+ * lastFromThem, preview } }, unmatched }; the caller decides whether the
298
+ * preview's words are kept.
299
+ */
300
+ export function matchLive(incoming = {}, connections = []) {
301
+ const byUrl = new Map();
302
+ const byName = new Map();
303
+ for (const c of connections) {
304
+ const key = keyFor(c);
305
+ byUrl.set(key, key);
306
+ const n = nameKey(c.name);
307
+ if (n) byName.set(n, byName.has(n) ? null : key); // null: two people share it
308
+ }
309
+ const live = {};
310
+ let unmatched = 0;
311
+ for (const [url, v] of Object.entries(incoming || {})) {
312
+ if (!v || typeof v !== 'object') continue;
313
+ const key = byUrl.get(keyFor({ profile_url: url })) || byName.get(nameKey(v.name)) || null;
314
+ if (!key) { unmatched += 1; continue; }
315
+ const last = Number(v.last) || null;
316
+ if (live[key] && (live[key].last || 0) >= (last || 0)) continue;
317
+ live[key] = {
318
+ last,
319
+ unread: Number.isFinite(Number(v.unread)) && v.unread !== null ? Number(v.unread) : null,
320
+ threadUrl: threadLink(v.threadUrl),
321
+ lastFromThem: typeof v.lastFromThem === 'boolean' ? v.lastFromThem : null,
322
+ preview: previewOf(v.preview),
323
+ };
324
+ }
325
+ return { live, unmatched };
326
+ }
@@ -0,0 +1,73 @@
1
+ // Where the Social tab keeps what it knows, per profile, in the data folder:
2
+ //
3
+ // social-<profile>.json numbers and dates (app/api/social): who you
4
+ // messaged, when, who wrote last, the list of
5
+ // conversations. Never any words.
6
+ // social-messages-<profile>.json the messages themselves, only while Keep my
7
+ // messages is on (app/api/social/messages).
8
+ //
9
+ // Two files so that turning Keep my messages off, or Forget it, deletes the
10
+ // words by deleting one file, and so the everyday file the Galaxy and the
11
+ // Social tab read stays small however many messages there are. Neither is in
12
+ // "Save a copy of my network": that carries an allow-list of files
13
+ // (lib/data-folder.js TRAVELLING_FILES) and these aren't on it.
14
+
15
+ import { readFileSync, writeFileSync, renameSync, rmSync, statSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { dataDir } from './db-client';
18
+ import { resolveProfile } from './profile';
19
+
20
+ // The last messages file read, while it's unchanged (readMessages).
21
+ const cache = { file: null, mtimeMs: 0, size: 0, data: null };
22
+
23
+ /** The Social tab's files for the profile in use, or null before there is one. */
24
+ export function socialFiles() {
25
+ const me = resolveProfile({ create: false });
26
+ if (!me?.id || !/^[\w-]+$/.test(String(me.id))) return null;
27
+ const dir = dataDir();
28
+ const messages = path.join(dir, `social-messages-${me.id}.json`);
29
+ return {
30
+ profileId: me.id,
31
+ social: path.join(dir, `social-${me.id}.json`),
32
+ messages,
33
+ // An import's messages arrive in parts (each request stays under the 10 MB
34
+ // middleware reads); they gather here and replace `messages` at the end.
35
+ incoming: `${messages}.incoming`,
36
+ };
37
+ }
38
+
39
+ export function readJson(file, fallback = null) {
40
+ try { return JSON.parse(readFileSync(file, 'utf8')) ?? fallback; } catch { return fallback; }
41
+ }
42
+
43
+ /** Written whole or not at all: a crash mid-write never leaves half a file. */
44
+ export function writeJsonAtomic(file, value) {
45
+ const tmp = `${file}.${process.pid}.tmp`;
46
+ writeFileSync(tmp, JSON.stringify(value));
47
+ renameSync(tmp, file);
48
+ if (cache.file === file) cache.file = null;
49
+ }
50
+
51
+ /** The kept messages and their live-sync previews, or empty ones. */
52
+ export function readMessages(file) {
53
+ // Kept in memory while the file is unchanged: a long history is read once,
54
+ // not on every page of a thread or every letter typed in the search box.
55
+ let st;
56
+ try { st = statSync(file); } catch { return { threads: {}, previews: {} }; }
57
+ if (cache.file === file && cache.mtimeMs === st.mtimeMs && cache.size === st.size) return cache.data;
58
+ const raw = readJson(file, {});
59
+ const data = {
60
+ threads: raw?.threads && typeof raw.threads === 'object' ? raw.threads : {},
61
+ previews: raw?.previews && typeof raw.previews === 'object' ? raw.previews : {},
62
+ };
63
+ Object.assign(cache, { file, mtimeMs: st.mtimeMs, size: st.size, data });
64
+ return data;
65
+ }
66
+
67
+ /** The messages gone: the kept file and any import still arriving. */
68
+ export function deleteMessages(files) {
69
+ if (!files) return;
70
+ rmSync(files.messages, { force: true });
71
+ rmSync(files.incoming, { force: true });
72
+ cache.file = null;
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "six-degrees",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
5
5
  "license": "MIT",
6
6
  "author": "Blake Burford",