@rubytech/create-maxy-code 0.1.88 → 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.
- package/dist/__tests__/entitlement-flag.test.js +43 -0
- package/dist/__tests__/tier-flag.test.js +53 -0
- package/dist/index.js +68 -0
- package/dist/tier-flag.js +85 -0
- package/package.json +1 -1
- package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts +54 -0
- package/payload/platform/lib/admin-conversation-purge/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/admin-conversation-purge/dist/index.js +84 -0
- package/payload/platform/lib/admin-conversation-purge/dist/index.js.map +1 -0
- package/payload/platform/lib/admin-conversation-purge/src/index.ts +120 -0
- package/payload/platform/lib/admin-conversation-purge/tsconfig.json +8 -0
- package/payload/platform/lib/graph-trash/dist/index.d.ts +12 -1
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +8 -1
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +20 -2
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-investigate-gate.test.sh +176 -0
- package/payload/platform/plugins/admin/hooks/askuserquestion-investigate-gate.sh +257 -0
- package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +2 -0
- package/payload/platform/plugins/docs/references/admin-session.md +5 -1
- package/payload/platform/plugins/docs/references/linkedin-extension.md +49 -0
- package/payload/platform/plugins/linkedin-extension/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/linkedin-extension/PLUGIN.md +58 -0
- package/payload/platform/plugins/linkedin-extension/extension/README.md +44 -0
- package/payload/platform/plugins/linkedin-extension/extension/__tests__/fixtures/profile.html +34 -0
- package/payload/platform/plugins/linkedin-extension/extension/__tests__/fixtures/thread.html +36 -0
- package/payload/platform/plugins/linkedin-extension/extension/assets/pill.css +52 -0
- package/payload/platform/plugins/linkedin-extension/extension/background/sw.js +60 -0
- package/payload/platform/plugins/linkedin-extension/extension/content/extractors.js +127 -0
- package/payload/platform/plugins/linkedin-extension/extension/content/profile.js +82 -0
- package/payload/platform/plugins/linkedin-extension/extension/content/thread.js +84 -0
- package/payload/platform/plugins/linkedin-extension/extension/manifest.json +32 -0
- package/payload/platform/plugins/linkedin-extension/extension/options/options.html +33 -0
- package/payload/platform/plugins/linkedin-extension/extension/options/options.js +30 -0
- package/payload/platform/plugins/linkedin-extension/skills/linkedin-extension/SKILL.md +91 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts +14 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +28 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js +168 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-admin-conversation.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +22 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +61 -13
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
- package/payload/platform/scripts/seed-neo4j.sh +12 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +3 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +6 -2
- package/payload/server/public/assets/{ChatInput-BzjU6OoA.css → ChatInput-dLWJ0Q8I.css} +1 -1
- package/payload/server/public/assets/{Checkbox-BWLS5Isf.js → Checkbox-Bx8mRHQ3.js} +1 -1
- package/payload/server/public/assets/{admin-DjU0q63n.js → admin-C57bDUgs.js} +6 -6
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-lcncWUol.js → architectureDiagram-Q4EWVU46-BEW_bD50.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-SqlxCp6m.js → blockDiagram-DXYQGD6D-DFLT98C3.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DWzvXvDc.js → c4Diagram-AHTNJAMY-BemKtROA.js} +1 -1
- package/payload/server/public/assets/channel-CfpByn-m.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-Bz-tPZhS.js → chunk-336JU56O-BL8fGJ7_.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-MYkbqx1u.js → chunk-426QAEUC-BD6by0MP.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-DfZRQtaZ.js → chunk-4TB4RGXK-DX-cb6Nd.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-BFMrOiNe.js → chunk-5FUZZQ4R-B2QqtIZS.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-64laDovl.js → chunk-5PVQY5BW-CzJWmsmd.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-dClAHBNd.js → chunk-EDXVE4YY-DP9vcvEU.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-Bz4gXG1p.js → chunk-ENJZ2VHE-D5xWEjF0.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-D5pWrJcN.js → chunk-ICPOFSXX-AKduf9o4.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BCiWtqaK.js → chunk-OYMX7WX6-D6jsVNyQ.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-Brql314T.js → chunk-U2HBQHQK-BNAnTYh8.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-C-79tMym.js → chunk-X2U36JSP-UdQhYDnh.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-G4idHEoZ.js → chunk-YZCP3GAM-DSSmSGmS.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-DyHGASIn.js → chunk-ZZ45TVLE-Czm7IDMC.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DNc9R1IX.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DM1DQh8N.js +1 -0
- package/payload/server/public/assets/clone-BSrNdvLW.js +1 -0
- package/payload/server/public/assets/{dagre-KV5264BT--Bba_VRf.js → dagre-KV5264BT-CBuRCJE7.js} +1 -1
- package/payload/server/public/assets/{dagre-DfJ8a8tG.js → dagre-hSIMHJcO.js} +1 -1
- package/payload/server/public/assets/data-3n20lir5.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-CfUotMr9.js → diagram-5BDNPKRD-dbMrH0le.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-BWZJChPS.js → diagram-G4DWMVQ6-DBc6hs9a.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-BExS6IWb.js → diagram-MMDJMWI5-ubSGybUk.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-DQ4t_wIt.js → diagram-TYMM5635-WxL3cv-9.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-f1sfB-AQ.js → erDiagram-SMLLAGMA-CrBBOAN8.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CZexxT3v.js → flowDiagram-DWJPFMVM-CXUq5Hzt.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BNcFUX4K.js → ganttDiagram-T4ZO3ILL-vTCBnYVS.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-soyylUaR.js → gitGraphDiagram-UUTBAWPF-BHrj_bRa.js} +1 -1
- package/payload/server/public/assets/graph-ChwMpxn_.js +1 -0
- package/payload/server/public/assets/graph-labels-CUgJgIwl.js +1 -0
- package/payload/server/public/assets/{graphlib-B2ezMQY4.js → graphlib-CDO75l2X.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-4xmenbOP.js → infoDiagram-42DDH7IO-CcQdaOhi.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-C_D6CqVq.js → ishikawaDiagram-UXIWVN3A-3KvltFgV.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DUDx0c35.js → journeyDiagram-VCZTEJTY-DZCXaGFE.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BNr7ErhZ.js → kanban-definition-6JOO6SKY-DAIAKy1-.js} +1 -1
- package/payload/server/public/assets/{lib-iKom7FFe.js → lib-CCLURO3E.js} +1 -1
- package/payload/server/public/assets/{line-Dvs5wrj5.js → line-C8iCE2Wx.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-D0ksZQlW.js → mermaid-parser.core-ElvUhnV9.js} +1 -1
- package/payload/server/public/assets/{mermaid.core--Btu4mYy.js → mermaid.core-7dSH31Ao.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DfyZTghB.js → mindmap-definition-QFDTVHPH-FZ0rcfac.js} +1 -1
- package/payload/server/public/assets/{page-BnBmdRbo.js → page-D3bFmqF4.js} +1 -1
- package/payload/server/public/assets/{page-x6ZXWwMx.js → page-DNjUtT5q.js} +18 -17
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BiG9YoLj.js → pieDiagram-DEJITSTG-CDuvMZbO.js} +1 -1
- package/payload/server/public/assets/{public-DW7yINk7.js → public-g_2skbnY.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CVufUsie.js → quadrantDiagram-34T5L4WZ-xDjhkqNc.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-UKCptCPk.js → requirementDiagram-MS252O5E-BPKpyT0n.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-e8iZYL5P.js → sankeyDiagram-XADWPNL6-BvFIrihC.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-CfZqVRxe.js → sequenceDiagram-FGHM5R23-C6G1oAE0.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DrWX8OY8.js → stateDiagram-FHFEXIEX-DbOJT_Xh.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ce4GFk-H.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Blec2Yhc.js → timeline-definition-GMOUNBTQ-D1DSxHh6.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BtACuGgu.js → vennDiagram-DHZGUBPP-BYoSeMdo.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BWT7orYV.js → wardleyDiagram-NUSXRM2D-DH3kZdea.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DBzoIRiJ.js → xychartDiagram-5P7HB3ND-C6MROMLG.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +531 -118
- package/payload/server/public/assets/channel-dZdJT-Ou.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CP8AWLB9.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-bUe6tAo9.js +0 -1
- package/payload/server/public/assets/clone-gXHQCPU0.js +0 -1
- package/payload/server/public/assets/data-Bt6BTjvP.js +0 -1
- package/payload/server/public/assets/graph-labels-BeDsoEVY.js +0 -1
- package/payload/server/public/assets/graph-szVcmw8D.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvKXSO97.js +0 -1
- /package/payload/server/public/assets/{ChatInput-AR8ym-HW.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
|
+
})()
|