@rubytech/create-maxy-code 0.1.87 → 0.1.90

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 (130) hide show
  1. package/dist/__tests__/entitlement-flag.test.js +43 -0
  2. package/dist/__tests__/tier-flag.test.js +53 -0
  3. package/dist/index.js +68 -0
  4. package/dist/tier-flag.js +85 -0
  5. package/package.json +1 -1
  6. package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts +54 -0
  7. package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts.map +1 -0
  8. package/payload/platform/lib/admin-conversation-purge/dist/index.js +84 -0
  9. package/payload/platform/lib/admin-conversation-purge/dist/index.js.map +1 -0
  10. package/payload/platform/lib/admin-conversation-purge/src/index.ts +120 -0
  11. package/payload/platform/lib/admin-conversation-purge/tsconfig.json +8 -0
  12. package/payload/platform/lib/graph-trash/dist/index.d.ts +12 -1
  13. package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
  14. package/payload/platform/lib/graph-trash/dist/index.js +8 -1
  15. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  16. package/payload/platform/lib/graph-trash/src/index.ts +20 -2
  17. package/payload/platform/package.json +2 -2
  18. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  19. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  20. package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-investigate-gate.test.sh +176 -0
  21. package/payload/platform/plugins/admin/hooks/askuserquestion-investigate-gate.sh +257 -0
  22. package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +2 -0
  23. package/payload/platform/plugins/docs/references/admin-session.md +5 -1
  24. package/payload/platform/plugins/docs/references/linkedin-extension.md +49 -0
  25. package/payload/platform/plugins/linkedin-extension/.claude-plugin/plugin.json +8 -0
  26. package/payload/platform/plugins/linkedin-extension/PLUGIN.md +58 -0
  27. package/payload/platform/plugins/linkedin-extension/extension/README.md +44 -0
  28. package/payload/platform/plugins/linkedin-extension/extension/__tests__/fixtures/profile.html +34 -0
  29. package/payload/platform/plugins/linkedin-extension/extension/__tests__/fixtures/thread.html +36 -0
  30. package/payload/platform/plugins/linkedin-extension/extension/assets/pill.css +52 -0
  31. package/payload/platform/plugins/linkedin-extension/extension/background/sw.js +60 -0
  32. package/payload/platform/plugins/linkedin-extension/extension/content/extractors.js +127 -0
  33. package/payload/platform/plugins/linkedin-extension/extension/content/profile.js +82 -0
  34. package/payload/platform/plugins/linkedin-extension/extension/content/thread.js +84 -0
  35. package/payload/platform/plugins/linkedin-extension/extension/manifest.json +32 -0
  36. package/payload/platform/plugins/linkedin-extension/extension/options/options.html +33 -0
  37. package/payload/platform/plugins/linkedin-extension/extension/options/options.js +30 -0
  38. package/payload/platform/plugins/linkedin-extension/skills/linkedin-extension/SKILL.md +91 -0
  39. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +14 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +28 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -1
  43. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.d.ts +2 -0
  44. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js +168 -0
  46. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js.map +1 -0
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +22 -3
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +61 -13
  50. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  51. package/payload/platform/scripts/seed-neo4j.sh +12 -0
  52. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  53. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +27 -17
  54. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  55. package/payload/platform/templates/agents/admin/IDENTITY.md +3 -1
  56. package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
  57. package/payload/server/public/assets/{ChatInput-CktRrxx1.css → ChatInput-dLWJ0Q8I.css} +1 -1
  58. package/payload/server/public/assets/{Checkbox-D2XqbR29.js → Checkbox-Bx8mRHQ3.js} +1 -1
  59. package/payload/server/public/assets/{admin-BK1F2LMD.js → admin-C57bDUgs.js} +6 -6
  60. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-9F6y6ESU.js → architectureDiagram-Q4EWVU46-BEW_bD50.js} +1 -1
  61. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-LjGbzg0M.js → blockDiagram-DXYQGD6D-DFLT98C3.js} +1 -1
  62. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Bxpn5T5R.js → c4Diagram-AHTNJAMY-BemKtROA.js} +1 -1
  63. package/payload/server/public/assets/channel-CfpByn-m.js +1 -0
  64. package/payload/server/public/assets/{chunk-336JU56O-Cc2kxWVN.js → chunk-336JU56O-BL8fGJ7_.js} +2 -2
  65. package/payload/server/public/assets/{chunk-426QAEUC-DRzEhHvW.js → chunk-426QAEUC-BD6by0MP.js} +1 -1
  66. package/payload/server/public/assets/{chunk-4TB4RGXK-C8zRzH9g.js → chunk-4TB4RGXK-DX-cb6Nd.js} +1 -1
  67. package/payload/server/public/assets/{chunk-5FUZZQ4R-s8V7nzOd.js → chunk-5FUZZQ4R-B2QqtIZS.js} +1 -1
  68. package/payload/server/public/assets/{chunk-5PVQY5BW-KNShBlaM.js → chunk-5PVQY5BW-CzJWmsmd.js} +1 -1
  69. package/payload/server/public/assets/{chunk-EDXVE4YY-C6GvGxn3.js → chunk-EDXVE4YY-DP9vcvEU.js} +1 -1
  70. package/payload/server/public/assets/{chunk-ENJZ2VHE-XvXhU8ZS.js → chunk-ENJZ2VHE-D5xWEjF0.js} +1 -1
  71. package/payload/server/public/assets/{chunk-ICPOFSXX-OSDYUb0H.js → chunk-ICPOFSXX-AKduf9o4.js} +1 -1
  72. package/payload/server/public/assets/{chunk-OYMX7WX6-km_4dA-E.js → chunk-OYMX7WX6-D6jsVNyQ.js} +1 -1
  73. package/payload/server/public/assets/{chunk-U2HBQHQK-BGHqDvzJ.js → chunk-U2HBQHQK-BNAnTYh8.js} +1 -1
  74. package/payload/server/public/assets/{chunk-X2U36JSP-BKKSYdFO.js → chunk-X2U36JSP-UdQhYDnh.js} +1 -1
  75. package/payload/server/public/assets/{chunk-YZCP3GAM-DT_yhfJM.js → chunk-YZCP3GAM-DSSmSGmS.js} +1 -1
  76. package/payload/server/public/assets/{chunk-ZZ45TVLE-CP2JuBPA.js → chunk-ZZ45TVLE-Czm7IDMC.js} +1 -1
  77. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DNc9R1IX.js +1 -0
  78. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DM1DQh8N.js +1 -0
  79. package/payload/server/public/assets/clone-BSrNdvLW.js +1 -0
  80. package/payload/server/public/assets/{dagre-KV5264BT-Dk7osC4w.js → dagre-KV5264BT-CBuRCJE7.js} +1 -1
  81. package/payload/server/public/assets/{dagre-CUhFHAG8.js → dagre-hSIMHJcO.js} +1 -1
  82. package/payload/server/public/assets/data-3n20lir5.js +1 -0
  83. package/payload/server/public/assets/{diagram-5BDNPKRD-CdOEJbjg.js → diagram-5BDNPKRD-dbMrH0le.js} +1 -1
  84. package/payload/server/public/assets/{diagram-G4DWMVQ6-Dyx0MQk6.js → diagram-G4DWMVQ6-DBc6hs9a.js} +1 -1
  85. package/payload/server/public/assets/{diagram-MMDJMWI5-Dw6sxLh8.js → diagram-MMDJMWI5-ubSGybUk.js} +1 -1
  86. package/payload/server/public/assets/{diagram-TYMM5635-Bm3GRH-1.js → diagram-TYMM5635-WxL3cv-9.js} +1 -1
  87. package/payload/server/public/assets/{erDiagram-SMLLAGMA-CAJ3Z9uD.js → erDiagram-SMLLAGMA-CrBBOAN8.js} +1 -1
  88. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-NrVUcQ2d.js → flowDiagram-DWJPFMVM-CXUq5Hzt.js} +1 -1
  89. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-28YyrNXj.js → ganttDiagram-T4ZO3ILL-vTCBnYVS.js} +1 -1
  90. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-D73FSDE8.js → gitGraphDiagram-UUTBAWPF-BHrj_bRa.js} +1 -1
  91. package/payload/server/public/assets/graph-ChwMpxn_.js +1 -0
  92. package/payload/server/public/assets/graph-labels-CUgJgIwl.js +1 -0
  93. package/payload/server/public/assets/{graphlib-n_bUAhub.js → graphlib-CDO75l2X.js} +1 -1
  94. package/payload/server/public/assets/{infoDiagram-42DDH7IO-rFap2NoE.js → infoDiagram-42DDH7IO-CcQdaOhi.js} +1 -1
  95. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-KKAj626l.js → ishikawaDiagram-UXIWVN3A-3KvltFgV.js} +1 -1
  96. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-Do6hVThi.js → journeyDiagram-VCZTEJTY-DZCXaGFE.js} +1 -1
  97. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-C38qlZwy.js → kanban-definition-6JOO6SKY-DAIAKy1-.js} +1 -1
  98. package/payload/server/public/assets/{lib-D-jcbjd4.js → lib-CCLURO3E.js} +1 -1
  99. package/payload/server/public/assets/{line-DsDSEpXU.js → line-C8iCE2Wx.js} +1 -1
  100. package/payload/server/public/assets/{mermaid-parser.core-Cvzc7ePL.js → mermaid-parser.core-ElvUhnV9.js} +1 -1
  101. package/payload/server/public/assets/{mermaid.core-BSZExB1C.js → mermaid.core-7dSH31Ao.js} +3 -3
  102. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CrS3b5u5.js → mindmap-definition-QFDTVHPH-FZ0rcfac.js} +1 -1
  103. package/payload/server/public/assets/{page-C438DjPa.js → page-D3bFmqF4.js} +1 -1
  104. package/payload/server/public/assets/{page-fdrpcuv5.js → page-DNjUtT5q.js} +18 -17
  105. package/payload/server/public/assets/{pieDiagram-DEJITSTG-Dk-6uOEj.js → pieDiagram-DEJITSTG-CDuvMZbO.js} +1 -1
  106. package/payload/server/public/assets/{public-BK3eOHJs.js → public-g_2skbnY.js} +3 -3
  107. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CEMK5xQ0.js → quadrantDiagram-34T5L4WZ-xDjhkqNc.js} +1 -1
  108. package/payload/server/public/assets/{requirementDiagram-MS252O5E-vTyRxQOY.js → requirementDiagram-MS252O5E-BPKpyT0n.js} +1 -1
  109. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-s2z8dMpM.js → sankeyDiagram-XADWPNL6-BvFIrihC.js} +1 -1
  110. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-locoOgnm.js → sequenceDiagram-FGHM5R23-C6G1oAE0.js} +1 -1
  111. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-m1d4aD0M.js → stateDiagram-FHFEXIEX-DbOJT_Xh.js} +1 -1
  112. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ce4GFk-H.js +1 -0
  113. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-HraDHuOp.js → timeline-definition-GMOUNBTQ-D1DSxHh6.js} +1 -1
  114. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DkyF2rFo.js → vennDiagram-DHZGUBPP-BYoSeMdo.js} +1 -1
  115. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BGScmzP0.js → wardleyDiagram-NUSXRM2D-DH3kZdea.js} +1 -1
  116. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CPJ1JLj2.js → xychartDiagram-5P7HB3ND-C6MROMLG.js} +1 -1
  117. package/payload/server/public/data.html +5 -5
  118. package/payload/server/public/graph.html +6 -6
  119. package/payload/server/public/index.html +8 -8
  120. package/payload/server/public/public.html +5 -5
  121. package/payload/server/server.js +531 -118
  122. package/payload/server/public/assets/channel-CY_QocHj.js +0 -1
  123. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BtwH3Rsu.js +0 -1
  124. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-tCRstAZ-.js +0 -1
  125. package/payload/server/public/assets/clone-Bc-lmwcQ.js +0 -1
  126. package/payload/server/public/assets/data-B3WPMQdR.js +0 -1
  127. package/payload/server/public/assets/graph-B6c5nlBL.js +0 -1
  128. package/payload/server/public/assets/graph-labels-BMqBNCYc.js +0 -1
  129. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B7eWHz7x.js +0 -1
  130. /package/payload/server/public/assets/{ChatInput-D62NmYfJ.js → ChatInput-DoS80kaC.js} +0 -0
@@ -0,0 +1,49 @@
1
+ # LinkedIn Extension — operator guide
2
+
3
+ Capture a LinkedIn profile or DM thread to your Maxy graph with one click. The plugin ships a small Chrome extension; the admin already knows how to receive its payloads.
4
+
5
+ ## Install (one time)
6
+
7
+ 1. Open `chrome://extensions`.
8
+ 2. Toggle **Developer mode** on (top right).
9
+ 3. Click **Load unpacked**.
10
+ 4. Select `platform/plugins/linkedin-extension/extension/` on disk.
11
+ 5. Click the puzzle icon → pin **Maxy LinkedIn Ingest** → open its options.
12
+ 6. Paste two values:
13
+ - **Admin host** — your tunnel URL, e.g. `https://your-tunnel.example.com`.
14
+ - **Session key** — open your admin browser, copy the value of `session_key` from the cookie. (Same key the admin uses to authenticate every other admin request.)
15
+ 7. Save. The pill is now armed.
16
+
17
+ ## Use
18
+
19
+ - **Profile** — open any `https://www.linkedin.com/in/<slug>/` page. An **Add to Maxy** pill appears in the top section. Click it. Within a few seconds the pill turns green: the profile is in your graph.
20
+ - **DM thread** — open any `https://www.linkedin.com/messaging/thread/<id>/` conversation. The same pill appears. Click it; the full transcript is captured plus the participants and any explicit commitments (meetings booked, actions promised, prices discussed).
21
+ - **Re-click** — the pill is idempotent. Re-clicking the same URL refreshes the document body and regenerates the summary; identities and entities `MERGE` rather than duplicate.
22
+
23
+ ## What lands in the graph
24
+
25
+ Every click produces **one `:KnowledgeDocument`** keyed on the page URL, holding the verbatim scraped text as its body and a child `:Section:Note` for the LLM summary. Structured entities layer on top, but only when the body **assertively states** them:
26
+
27
+ - A `:Person` for the profile subject (or each thread participant), `MERGE`d on canonical keys.
28
+ - A `:Organization` for an asserted current employer, with a `WORKS_AT` edge from the person.
29
+ - An `:Event` with `ATTENDED_BY` edges when a meeting time is explicitly proposed and confirmed.
30
+ - A `:Task` with `RAISED_BY` / `ABOUT` edges when an action is promised without a specific time.
31
+ - A `:Service` / `:PriceSpecification` when an offer is discussed.
32
+
33
+ Soft signals ("interested in chatting", "would love to compare notes") stay in the document body. They are never promoted to graph nodes.
34
+
35
+ The plugin will never create `:Communication`, `:ConversationArchive`, or `:Message` nodes — those labels are reserved for other flows (live chat, archive ingest).
36
+
37
+ ## When the pill turns amber
38
+
39
+ The pill shows **Sign in to Maxy** when your session key has expired. Click it to open the options page; paste a fresh `session_key` from your admin browser; save. The next click on the LinkedIn pill will succeed.
40
+
41
+ ## When the pill turns red
42
+
43
+ - **Missing: ...** — LinkedIn shipped a DOM change and the extractor cannot find a required field. Open a console tab on the LinkedIn page and check the `[linkedin-ext-scrape]` log line for the field names. Drop a ticket pointing at the affected selector; the [`SKILL.md`](../../plugins/linkedin-extension/skills/linkedin-extension/SKILL.md) lists the selector table and the steps for adding a fallback.
44
+ - **Capture failed** — the admin reached, but the request did not complete cleanly. Check the admin logs (`journalctl -u maxy.service | grep linkedin-ingest`). The `[linkedin-ingest-route]` lines name the reason (`schema`, `dispatch-failed`).
45
+
46
+ ## Related plugins
47
+
48
+ - **linkedin-import** — bulk ingest of the LinkedIn ZIP export (history). Different surface; both ship and complement each other.
49
+ - **memory.document-ingest** — the generic ingest pipeline this plugin's payloads route through. Future communication surfaces (email, Telegram, WhatsApp) plug in here too.
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "linkedin-extension",
3
+ "description": "One-click LinkedIn ingest. Ships a manifest-v3 Chrome extension that injects an Add-to-Maxy pill into `linkedin.com/in/*` profiles and `linkedin.com/messaging/thread/*` DM threads. Each click scrapes the page DOM, POSTs to the admin `linkedin-ingest` route, which dispatches `database-operator` against the generic `document-ingest` skill. The plugin is the **first consumer** of the generic communication-ingest path; every future communication surface (email, Telegram, WhatsApp, group chats, voice memos) plugs into the same backend. Opt-in per brand.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: linkedin-extension
3
+ description: "One-click LinkedIn ingest. Ships a manifest-v3 Chrome extension that injects an Add-to-Maxy pill into `linkedin.com/in/*` profiles and `linkedin.com/messaging/thread/*` DM threads. Each click scrapes the page DOM, POSTs to the admin `linkedin-ingest` route, which dispatches `database-operator` against the generic `document-ingest` skill. The plugin is the **first consumer** of the generic communication-ingest path; every future communication surface (email, Telegram, WhatsApp, group chats, voice memos) plugs into the same backend. Opt-in per brand."
4
+ tools: []
5
+ always: false
6
+ embed: false
7
+ specialist: database-operator
8
+ metadata: {"platform":{"optional":true,"pluginKey":"linkedin-extension"}}
9
+ ---
10
+
11
+ # LinkedIn Extension
12
+
13
+ Capturing a LinkedIn lead today costs 30-90 seconds per record. This plugin replaces that with one click. The Chrome extension scrapes the open page, posts the verbatim text to the admin, and `database-operator` writes one `:KnowledgeDocument` plus the structured entities the body assertively states.
14
+
15
+ ## Two surfaces
16
+
17
+ | URL pattern | What gets captured |
18
+ |---|---|
19
+ | `linkedin.com/in/<slug>/` | The full profile text plus, where explicitly stated, the subject `:Person`, their current `:Organization`, and a `WORKS_AT` edge. |
20
+ | `linkedin.com/messaging/thread/<id>/` | The full chronological transcript plus `(:KD)-[:PARTICIPANT]->(:Person)` per participant and any explicitly-asserted `:Event` / `:Task` / `:Service` / `:PriceSpecification` etc., wired back to the right participants. |
21
+
22
+ Re-clicking the same URL refreshes the KD body and regenerates the summary. Person / Organization / derived entities `MERGE` on canonical keys, so the graph does not duplicate.
23
+
24
+ ## Architecture
25
+
26
+ ```
27
+ Chrome extension Hono admin Specialist
28
+ ───────────────── ────────── ──────────
29
+ content/profile.js ──POST──> /api/admin/linkedin-ingest ──> database-operator
30
+ content/thread.js requireAdminSession document-ingest skill
31
+ schema validate memory-ingest tools
32
+ spawn database-operator graph writes
33
+ ```
34
+
35
+ The route's job is auth + schema + dispatch. Extraction logic lives in `document-ingest`; ontology decisions live in the schema reference. The extension is the only LinkedIn-bespoke part.
36
+
37
+ ## Files
38
+
39
+ - `extension/manifest.json` — manifest v3
40
+ - `extension/content/profile.js` — content script for `linkedin.com/in/*`
41
+ - `extension/content/thread.js` — content script for `linkedin.com/messaging/thread/*`
42
+ - `extension/background/sw.js` — service worker handling POSTs
43
+ - `extension/options/options.{html,js}` — admin host + bearer token entry
44
+ - `extension/assets/pill.css` — pill styling
45
+ - `extension/README.md` — developer install guide
46
+ - `extension/__tests__/{profile,thread}.test.ts` — jsdom extractor tests
47
+ - `extension/__tests__/fixtures/{profile,thread}.html` — frozen DOM fixtures captured 2026-05-22
48
+ - `skills/linkedin-extension/SKILL.md` — payload schema + DOM-selector rules
49
+
50
+ ## Sibling plugins
51
+
52
+ - `linkedin-import` — consumes a LinkedIn ZIP export (bulk historical). Different surface; both ship.
53
+ - `memory.document-ingest` — the generic communication-ingest pipeline this plugin's payloads route through.
54
+ - `contacts` — `:Person` nodes minted here surface in `contact-list` like any other.
55
+
56
+ ## Enabling
57
+
58
+ Add `linkedin-extension` to a brand's `enabledPlugins` in `brand.json`. Default brands ship without it.
@@ -0,0 +1,44 @@
1
+ # Maxy LinkedIn Ingest — Chrome Extension
2
+
3
+ Unpacked-extension dev build. No Chrome Web Store publication.
4
+
5
+ ## Install
6
+
7
+ 1. `chrome://extensions`
8
+ 2. Enable **Developer mode** (top right).
9
+ 3. **Load unpacked** → select this `extension/` directory.
10
+ 4. Click the new puzzle icon → open **Maxy LinkedIn Ingest** options.
11
+ 5. Paste your admin host (e.g. `https://your-tunnel.example.com`) and your admin **session key** (the value of `session_key` on the cookie or query in your admin browser). Save.
12
+
13
+ ## Use
14
+
15
+ - Open any `https://www.linkedin.com/in/<slug>/` profile or `https://www.linkedin.com/messaging/thread/<id>/` thread.
16
+ - Click the **Add to Maxy** pill at the top of the page.
17
+ - The pill turns green and writes one `:KnowledgeDocument` plus the entities the body assertively states.
18
+
19
+ If the pill turns amber and shows **Sign in to Maxy**, your session key has expired — open the options page and paste a fresh one.
20
+
21
+ ## File layout
22
+
23
+ | File | Role |
24
+ |---|---|
25
+ | `manifest.json` | MV3 manifest. Declares profile + thread content scripts. |
26
+ | `content/extractors.js` | Pure DOM extractor functions. Loaded first into each content-script context. |
27
+ | `content/profile.js` | Profile pill + click handler. |
28
+ | `content/thread.js` | Thread pill + click handler. |
29
+ | `background/sw.js` | Service worker. POSTs envelopes to the admin route. |
30
+ | `options/options.html` + `options/options.js` | Admin host + session-key entry. |
31
+ | `assets/pill.css` | Pill styling. |
32
+ | `__tests__/` | jsdom unit tests for the extractors, run via vitest from `platform/ui`. |
33
+
34
+ ## How the wire works
35
+
36
+ The extension does no extraction or graph-write logic — those live in `platform/plugins/memory/skills/document-ingest/`. Each click POSTs the payload defined in `../skills/linkedin-extension/SKILL.md` to `/api/admin/linkedin-ingest`, which authenticates and dispatches `database-operator` to run the generic ingest pipeline.
37
+
38
+ ## Updating selectors
39
+
40
+ LinkedIn ships DOM changes from time to time. When the pill misses a field:
41
+
42
+ 1. Save the current page as `__tests__/fixtures/<kind>.html` (overwrite).
43
+ 2. Update the primary selector in `content/extractors.js`; keep the prior one as fallback if older clients may still serve it.
44
+ 3. From `platform/ui/`, run `npx vitest run server/routes/admin/__tests__/linkedin` — the fixture-driven tests will fail loudly if any required field has gone missing.
@@ -0,0 +1,34 @@
1
+ <!doctype html>
2
+ <!-- Frozen fixture captured 2026-05-22 from a logged-in linkedin.com/in/<slug>/ page.
3
+ Reduced to the DOM regions our extractor reads. Update when the live DOM drifts. -->
4
+ <html>
5
+ <head><title>Renato Rufus | LinkedIn</title></head>
6
+ <body>
7
+ <main>
8
+ <section class="top-card-layout">
9
+ <h1 class="text-heading-xlarge">Renato Rufus</h1>
10
+ <div class="text-body-medium break-words" data-test-headline>Founder · Operator coach</div>
11
+ <div class="text-body-small inline t-black--light" data-test-location>London, United Kingdom</div>
12
+ </section>
13
+
14
+ <section id="experience" data-test-current-position>
15
+ <ul>
16
+ <li>
17
+ <div class="t-bold position-title"><span>Founder</span></div>
18
+ <div class="t-normal position-company"><span>Acme Studios</span></div>
19
+ <div class="t-normal">2022 – Present · 4 yrs</div>
20
+ </li>
21
+ <li>
22
+ <div class="t-bold"><span>Operator-in-Residence</span></div>
23
+ <div class="t-normal"><span>Beta Ventures</span></div>
24
+ </li>
25
+ </ul>
26
+ </section>
27
+
28
+ <section id="about">
29
+ <p>I work with founders one-on-one to ship product faster. Previously CTO at Beta Ventures (2018-2022).</p>
30
+ <p>Interested in AI-native operator tooling. Open to selective advisory engagements.</p>
31
+ </section>
32
+ </main>
33
+ </body>
34
+ </html>
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <!-- Frozen fixture captured 2026-05-22 from a logged-in
3
+ linkedin.com/messaging/thread/<id>/ page. Reduced to the regions the
4
+ extractor reads. -->
5
+ <html>
6
+ <head><title>Messaging | LinkedIn</title></head>
7
+ <body>
8
+ <main>
9
+ <section class="msg-thread">
10
+ <ul>
11
+ <li class="msg-s-message-list__event">
12
+ <a class="msg-s-event-listitem__link" href="/in/renato-rufus-7a633b4/">
13
+ <span class="msg-s-event-listitem__name">Renato Rufus</span>
14
+ </a>
15
+ <time class="msg-s-event-listitem__timestamp" datetime="2026-05-20T15:02:00Z">Wed 3:02 PM</time>
16
+ <div class="msg-s-event-listitem__body">Hey Joel, would love to compare notes on operator-tooling next week. Could you spare 40 minutes?</div>
17
+ </li>
18
+ <li class="msg-s-message-list__event">
19
+ <a class="msg-s-event-listitem__link" href="/in/joel-smalley/">
20
+ <span class="msg-s-event-listitem__name">Joel Smalley</span>
21
+ </a>
22
+ <time class="msg-s-event-listitem__timestamp" datetime="2026-05-20T15:18:00Z">Wed 3:18 PM</time>
23
+ <div class="msg-s-event-listitem__body">Yes — Thursday 22 May 14:00–14:40 BST works. My EA is sarah@acme.example; she'll send the invite.</div>
24
+ </li>
25
+ <li class="msg-s-message-list__event">
26
+ <a class="msg-s-event-listitem__link" href="/in/renato-rufus-7a633b4/">
27
+ <span class="msg-s-event-listitem__name">Renato Rufus</span>
28
+ </a>
29
+ <time class="msg-s-event-listitem__timestamp" datetime="2026-05-20T15:25:00Z">Wed 3:25 PM</time>
30
+ <div class="msg-s-event-listitem__body">Perfect. Talk Thursday.</div>
31
+ </li>
32
+ </ul>
33
+ </section>
34
+ </main>
35
+ </body>
36
+ </html>
@@ -0,0 +1,52 @@
1
+ /* "Add to Maxy" pill injected into LinkedIn profile and DM thread pages. */
2
+
3
+ #maxy-linkedin-pill.maxy-pill {
4
+ position: relative;
5
+ z-index: 9999;
6
+ display: inline-flex;
7
+ align-items: center;
8
+ gap: 6px;
9
+ margin: 12px;
10
+ padding: 6px 14px;
11
+ border: 1px solid #0a66c2;
12
+ border-radius: 999px;
13
+ background: #ffffff;
14
+ color: #0a66c2;
15
+ font: 600 13px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
16
+ cursor: pointer;
17
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
18
+ transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
19
+ }
20
+
21
+ #maxy-linkedin-pill.maxy-pill:hover {
22
+ background: #0a66c2;
23
+ color: #ffffff;
24
+ }
25
+
26
+ #maxy-linkedin-pill.maxy-pill[data-state="busy"] {
27
+ background: #f4f7fb;
28
+ border-color: #a3b6c9;
29
+ color: #46596d;
30
+ cursor: progress;
31
+ }
32
+
33
+ #maxy-linkedin-pill.maxy-pill[data-state="ok"] {
34
+ background: #e6f4ea;
35
+ border-color: #2e7d32;
36
+ color: #2e7d32;
37
+ cursor: default;
38
+ }
39
+
40
+ #maxy-linkedin-pill.maxy-pill[data-state="error"] {
41
+ background: #fdecea;
42
+ border-color: #b00020;
43
+ color: #b00020;
44
+ cursor: pointer;
45
+ }
46
+
47
+ #maxy-linkedin-pill.maxy-pill[data-state="auth"] {
48
+ background: #fff7e0;
49
+ border-color: #b8860b;
50
+ color: #b8860b;
51
+ cursor: pointer;
52
+ }
@@ -0,0 +1,60 @@
1
+ // Service worker. Receives envelopes from content scripts, POSTs them
2
+ // to the admin host with the bearer token stored in extension options.
3
+
4
+ const TAG = '[linkedin-ext-post]'
5
+
6
+ async function getConfig() {
7
+ const stored = await chrome.storage.local.get(['adminHost', 'sessionKey'])
8
+ return {
9
+ adminHost: typeof stored.adminHost === 'string' ? stored.adminHost.replace(/\/+$/, '') : '',
10
+ sessionKey: typeof stored.sessionKey === 'string' ? stored.sessionKey : '',
11
+ }
12
+ }
13
+
14
+ async function postEnvelope(envelope) {
15
+ const { adminHost, sessionKey } = await getConfig()
16
+ if (!adminHost) return { ok: false, reason: 'auth', detail: 'admin-host-missing' }
17
+ if (!sessionKey) return { ok: false, reason: 'auth', detail: 'session-key-missing' }
18
+
19
+ const url = adminHost + '/api/admin/linkedin-ingest'
20
+ const started = Date.now()
21
+ let res
22
+ try {
23
+ res = await fetch(url, {
24
+ method: 'POST',
25
+ headers: { 'content-type': 'application/json' },
26
+ body: JSON.stringify({ ...envelope, session_key: sessionKey }),
27
+ })
28
+ } catch (err) {
29
+ console.error(TAG + ' dispatchId=' + envelope.dispatchId + ' status=network ms=' + (Date.now() - started))
30
+ return { ok: false, reason: 'network', detail: err && err.message ? err.message : String(err) }
31
+ }
32
+
33
+ const ms = Date.now() - started
34
+ console.log(TAG + ' dispatchId=' + envelope.dispatchId + ' status=' + res.status + ' ms=' + ms)
35
+
36
+ if (res.status === 401) return { ok: false, reason: 'auth', detail: 'session-rejected' }
37
+ if (res.status >= 500) return { ok: false, reason: 'server', detail: 'http-' + res.status }
38
+ if (!res.ok) {
39
+ let body = ''
40
+ try { body = await res.text() } catch { /* ignore */ }
41
+ return { ok: false, reason: 'extract', detail: 'http-' + res.status + (body ? ' ' + body.slice(0, 200) : '') }
42
+ }
43
+
44
+ return { ok: true }
45
+ }
46
+
47
+ chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) {
48
+ if (!msg || msg.type !== 'linkedin-ingest' || !msg.envelope) {
49
+ sendResponse({ ok: false, reason: 'extract', detail: 'bad-message' })
50
+ return false
51
+ }
52
+ postEnvelope(msg.envelope).then(sendResponse).catch(function (err) {
53
+ sendResponse({ ok: false, reason: 'extract', detail: err && err.message ? err.message : String(err) })
54
+ })
55
+ return true // async sendResponse
56
+ })
57
+
58
+ chrome.action.onClicked.addListener(function () {
59
+ chrome.runtime.openOptionsPage()
60
+ })
@@ -0,0 +1,127 @@
1
+ // Plain script. Two roles:
2
+ // - Loaded as the first entry in each content_scripts.js array. Chrome
3
+ // concatenates the array into one isolated-world script, so the
4
+ // downstream content script can read MaxyLinkedinExtractors directly.
5
+ // - Loaded into jsdom by the tests, which evaluate this file in a
6
+ // wrapped function scope so the same globalThis assignment exposes the
7
+ // functions to the test runtime.
8
+ //
9
+ // No imports, no exports — the manifest-v3 content script context does not
10
+ // support ES module imports, and the bundler-free deploy path is the point.
11
+
12
+ ;(function () {
13
+ function sel(doc, primary, fallback) {
14
+ return doc.querySelector(primary) || doc.querySelector(fallback)
15
+ }
16
+
17
+ function txt(el) {
18
+ return el ? el.textContent.trim().replace(/\s+/g, ' ') : ''
19
+ }
20
+
21
+ function allTxt(el) {
22
+ if (!el) return ''
23
+ return el.textContent
24
+ .replace(/\r/g, '')
25
+ .replace(/[ \t]+/g, ' ')
26
+ .replace(/\n{3,}/g, '\n\n')
27
+ .trim()
28
+ }
29
+
30
+ function extractProfile(doc) {
31
+ const name = txt(sel(doc, 'h1.text-heading-xlarge', 'section.top-card-layout h1'))
32
+ const headline = txt(sel(doc, '[data-test-headline]', '.text-body-medium.break-words'))
33
+ const location = txt(sel(doc, '[data-test-location]', '.text-body-small.inline.t-black--light'))
34
+ const currentTitle = txt(sel(
35
+ doc,
36
+ '[data-test-current-position] .position-title',
37
+ 'section#experience li:first-child .t-bold > span:first-child',
38
+ ))
39
+ const currentCompany = txt(sel(
40
+ doc,
41
+ '[data-test-current-position] .position-company',
42
+ 'section#experience li:first-child .t-normal > span:first-child',
43
+ ))
44
+ const bodyEl = sel(doc, 'main', 'body')
45
+ const bodyText = allTxt(bodyEl)
46
+
47
+ const missing = []
48
+ if (!name) missing.push('name')
49
+ if (!headline) missing.push('headline')
50
+ if (!bodyText) missing.push('bodyText')
51
+ if (missing.length) return { ok: false, missing: missing }
52
+
53
+ return {
54
+ ok: true,
55
+ payload: {
56
+ name: name,
57
+ headline: headline,
58
+ location: location || '',
59
+ currentTitle: currentTitle || null,
60
+ currentCompany: currentCompany || null,
61
+ bodyText: bodyText,
62
+ },
63
+ }
64
+ }
65
+
66
+ function extractThread(doc) {
67
+ const root =
68
+ doc.querySelector('.msg-thread') ||
69
+ doc.querySelector('[data-test-thread-root]')
70
+ if (!root) return { ok: false, missing: ['threadRoot'] }
71
+
72
+ const events = root.querySelectorAll('.msg-s-message-list__event, [data-test-message-item]')
73
+ if (!events.length) return { ok: false, missing: ['messages'] }
74
+
75
+ const participantsMap = new Map()
76
+ const lines = []
77
+
78
+ events.forEach(function (ev) {
79
+ const nameEl =
80
+ ev.querySelector('.msg-s-event-listitem__name') ||
81
+ ev.querySelector('[data-test-sender-name]')
82
+ const linkEl =
83
+ ev.querySelector('a.msg-s-event-listitem__link[href*="/in/"]') ||
84
+ ev.querySelector('[data-test-sender-link]')
85
+ const timeEl =
86
+ ev.querySelector('time.msg-s-event-listitem__timestamp[datetime]') ||
87
+ ev.querySelector('[data-test-message-time][datetime]')
88
+ const bodyEl =
89
+ ev.querySelector('.msg-s-event-listitem__body') ||
90
+ ev.querySelector('[data-test-message-body]')
91
+
92
+ const name = txt(nameEl)
93
+ const body = allTxt(bodyEl)
94
+ if (!name || !body) return
95
+
96
+ const href = linkEl && linkEl.getAttribute('href')
97
+ const profileUrl = href
98
+ ? href.startsWith('http')
99
+ ? href
100
+ : 'https://www.linkedin.com' + href
101
+ : null
102
+ if (!participantsMap.has(name)) participantsMap.set(name, profileUrl)
103
+
104
+ const datetime = timeEl && timeEl.getAttribute('datetime')
105
+ lines.push(name + (datetime ? ' [' + datetime + ']' : '') + ': ' + body)
106
+ })
107
+
108
+ if (!lines.length) return { ok: false, missing: ['messages'] }
109
+
110
+ const participants = Array.from(participantsMap.entries()).map(function (entry) {
111
+ return { name: entry[0], profileUrl: entry[1] }
112
+ })
113
+
114
+ return {
115
+ ok: true,
116
+ payload: {
117
+ participants: participants,
118
+ bodyText: lines.join('\n'),
119
+ },
120
+ }
121
+ }
122
+
123
+ globalThis.MaxyLinkedinExtractors = {
124
+ extractProfile: extractProfile,
125
+ extractThread: extractThread,
126
+ }
127
+ })()
@@ -0,0 +1,82 @@
1
+ // Profile content script. Runs on linkedin.com/in/*.
2
+ // Manifest concatenates extractors.js + this file, so MaxyLinkedinExtractors
3
+ // is in scope. One click → scrape → post to the service worker → pill state.
4
+
5
+ ;(function () {
6
+ const TAG_MOUNT = '[linkedin-ext-mount]'
7
+ const TAG_SCRAPE = '[linkedin-ext-scrape]'
8
+ const TAG_ERROR = '[linkedin-ext-error]'
9
+ const PILL_ID = 'maxy-linkedin-pill'
10
+
11
+ function setState(pill, state, label) {
12
+ pill.dataset.state = state
13
+ if (label) pill.textContent = label
14
+ }
15
+
16
+ async function onClick(pill) {
17
+ const dispatchId = crypto.randomUUID()
18
+ setState(pill, 'busy', 'Capturing…')
19
+
20
+ const result = globalThis.MaxyLinkedinExtractors.extractProfile(document)
21
+ if (!result.ok) {
22
+ console.error(TAG_SCRAPE + ' page=profile ok=false missing=' + result.missing.join(','))
23
+ setState(pill, 'error', 'Missing: ' + result.missing.join(', '))
24
+ return
25
+ }
26
+ console.log(TAG_SCRAPE + ' page=profile ok=true fields=' + Object.keys(result.payload).join(','))
27
+
28
+ const envelope = {
29
+ source: 'linkedin',
30
+ kind: 'profile',
31
+ capturedAt: new Date().toISOString(),
32
+ pageUrl: location.href.split('?')[0].split('#')[0],
33
+ dispatchId: dispatchId,
34
+ profile: result.payload,
35
+ }
36
+
37
+ let reply
38
+ try {
39
+ reply = await chrome.runtime.sendMessage({ type: 'linkedin-ingest', envelope: envelope })
40
+ } catch (err) {
41
+ console.error(TAG_ERROR + ' reason=network detail=' + (err && err.message ? err.message : String(err)))
42
+ setState(pill, 'error', 'Capture failed')
43
+ return
44
+ }
45
+
46
+ if (!reply || !reply.ok) {
47
+ const reason = (reply && reply.reason) || 'network'
48
+ const detail = (reply && reply.detail) || ''
49
+ console.error(TAG_ERROR + ' reason=' + reason + ' detail=' + detail)
50
+ if (reason === 'auth') {
51
+ setState(pill, 'auth', 'Sign in to Maxy')
52
+ pill.onclick = function () { chrome.runtime.openOptionsPage() }
53
+ return
54
+ }
55
+ setState(pill, 'error', 'Capture failed')
56
+ return
57
+ }
58
+
59
+ setState(pill, 'ok', 'Captured ✓')
60
+ }
61
+
62
+ function mount() {
63
+ if (document.getElementById(PILL_ID)) return
64
+ const anchor = document.querySelector('section.top-card-layout, main') || document.body
65
+ if (!anchor) return
66
+ const pill = document.createElement('button')
67
+ pill.id = PILL_ID
68
+ pill.className = 'maxy-pill'
69
+ pill.type = 'button'
70
+ pill.textContent = 'Add to Maxy'
71
+ pill.dataset.state = 'idle'
72
+ pill.addEventListener('click', function () { onClick(pill) })
73
+ anchor.prepend(pill)
74
+ console.log(TAG_MOUNT + ' page=profile url=' + location.href)
75
+ }
76
+
77
+ if (document.readyState === 'loading') {
78
+ document.addEventListener('DOMContentLoaded', mount, { once: true })
79
+ } else {
80
+ mount()
81
+ }
82
+ })()
@@ -0,0 +1,84 @@
1
+ // Thread content script. Runs on linkedin.com/messaging/thread/*.
2
+ // Manifest concatenates extractors.js + this file.
3
+
4
+ ;(function () {
5
+ const TAG_MOUNT = '[linkedin-ext-mount]'
6
+ const TAG_SCRAPE = '[linkedin-ext-scrape]'
7
+ const TAG_ERROR = '[linkedin-ext-error]'
8
+ const PILL_ID = 'maxy-linkedin-pill'
9
+
10
+ function setState(pill, state, label) {
11
+ pill.dataset.state = state
12
+ if (label) pill.textContent = label
13
+ }
14
+
15
+ async function onClick(pill) {
16
+ const dispatchId = crypto.randomUUID()
17
+ setState(pill, 'busy', 'Capturing…')
18
+
19
+ const result = globalThis.MaxyLinkedinExtractors.extractThread(document)
20
+ if (!result.ok) {
21
+ console.error(TAG_SCRAPE + ' page=thread ok=false missing=' + result.missing.join(','))
22
+ setState(pill, 'error', 'Missing: ' + result.missing.join(', '))
23
+ return
24
+ }
25
+ console.log(
26
+ TAG_SCRAPE + ' page=thread ok=true fields=participants,bodyText participantCount=' +
27
+ result.payload.participants.length,
28
+ )
29
+
30
+ const envelope = {
31
+ source: 'linkedin',
32
+ kind: 'thread',
33
+ capturedAt: new Date().toISOString(),
34
+ pageUrl: location.href.split('?')[0].split('#')[0],
35
+ dispatchId: dispatchId,
36
+ thread: result.payload,
37
+ }
38
+
39
+ let reply
40
+ try {
41
+ reply = await chrome.runtime.sendMessage({ type: 'linkedin-ingest', envelope: envelope })
42
+ } catch (err) {
43
+ console.error(TAG_ERROR + ' reason=network detail=' + (err && err.message ? err.message : String(err)))
44
+ setState(pill, 'error', 'Capture failed')
45
+ return
46
+ }
47
+
48
+ if (!reply || !reply.ok) {
49
+ const reason = (reply && reply.reason) || 'network'
50
+ const detail = (reply && reply.detail) || ''
51
+ console.error(TAG_ERROR + ' reason=' + reason + ' detail=' + detail)
52
+ if (reason === 'auth') {
53
+ setState(pill, 'auth', 'Sign in to Maxy')
54
+ pill.onclick = function () { chrome.runtime.openOptionsPage() }
55
+ return
56
+ }
57
+ setState(pill, 'error', 'Capture failed')
58
+ return
59
+ }
60
+
61
+ setState(pill, 'ok', 'Captured ✓')
62
+ }
63
+
64
+ function mount() {
65
+ if (document.getElementById(PILL_ID)) return
66
+ const anchor = document.querySelector('.msg-thread, header.msg-overlay-bubble-header, main') || document.body
67
+ if (!anchor) return
68
+ const pill = document.createElement('button')
69
+ pill.id = PILL_ID
70
+ pill.className = 'maxy-pill'
71
+ pill.type = 'button'
72
+ pill.textContent = 'Add to Maxy'
73
+ pill.dataset.state = 'idle'
74
+ pill.addEventListener('click', function () { onClick(pill) })
75
+ anchor.prepend(pill)
76
+ console.log(TAG_MOUNT + ' page=thread url=' + location.href)
77
+ }
78
+
79
+ if (document.readyState === 'loading') {
80
+ document.addEventListener('DOMContentLoaded', mount, { once: true })
81
+ } else {
82
+ mount()
83
+ }
84
+ })()