linkedin-automation-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Extractor Module
|
|
3
|
+
*
|
|
4
|
+
* Extracts semantic DOM representation from LinkedIn pages using Playwright.
|
|
5
|
+
* Provides structured data extraction for AI agent action planning.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Page } from 'playwright';
|
|
9
|
+
import type {
|
|
10
|
+
DOMElement,
|
|
11
|
+
DOMRepresentation,
|
|
12
|
+
PageMetadata,
|
|
13
|
+
PageType,
|
|
14
|
+
ConnectionState,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extracts DOM representation from a Playwright page
|
|
19
|
+
*/
|
|
20
|
+
export class DOMExtractor {
|
|
21
|
+
/**
|
|
22
|
+
* Extract complete DOM representation from the page
|
|
23
|
+
*/
|
|
24
|
+
async extract(page: Page): Promise<DOMRepresentation> {
|
|
25
|
+
const url = page.url();
|
|
26
|
+
const title = await page.title();
|
|
27
|
+
|
|
28
|
+
// Extract elements and metadata in parallel
|
|
29
|
+
const [elements, metadata] = await Promise.all([
|
|
30
|
+
this.extractElements(page),
|
|
31
|
+
this.extractMetadata(page),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
url,
|
|
36
|
+
title,
|
|
37
|
+
elements,
|
|
38
|
+
metadata,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract interactive elements from the page
|
|
44
|
+
*/
|
|
45
|
+
private async extractElements(page: Page): Promise<DOMElement[]> {
|
|
46
|
+
return page.evaluate(() => {
|
|
47
|
+
const elements: DOMElement[] = [];
|
|
48
|
+
const seenElements = new Set<Element>();
|
|
49
|
+
|
|
50
|
+
// Define selectors for interactive elements
|
|
51
|
+
const selectors = [
|
|
52
|
+
'button',
|
|
53
|
+
'a[href]',
|
|
54
|
+
'input',
|
|
55
|
+
'textarea',
|
|
56
|
+
'select',
|
|
57
|
+
'[role="button"]',
|
|
58
|
+
'[role="link"]',
|
|
59
|
+
'[role="textbox"]',
|
|
60
|
+
'[role="checkbox"]',
|
|
61
|
+
'[role="radio"]',
|
|
62
|
+
'[role="combobox"]',
|
|
63
|
+
'[role="menuitem"]',
|
|
64
|
+
'[role="tab"]',
|
|
65
|
+
'[contenteditable="true"]',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Find all matching elements
|
|
69
|
+
const candidates = document.querySelectorAll(selectors.join(', '));
|
|
70
|
+
|
|
71
|
+
candidates.forEach((element, index) => {
|
|
72
|
+
// Skip duplicates
|
|
73
|
+
if (seenElements.has(element)) return;
|
|
74
|
+
seenElements.add(element);
|
|
75
|
+
|
|
76
|
+
// Check visibility
|
|
77
|
+
if (!isElementVisible(element)) return;
|
|
78
|
+
|
|
79
|
+
const extracted = extractElementData(element, index);
|
|
80
|
+
if (extracted) {
|
|
81
|
+
elements.push(extracted);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return elements;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if element is visible
|
|
89
|
+
*/
|
|
90
|
+
function isElementVisible(element: Element): boolean {
|
|
91
|
+
const style = window.getComputedStyle(element);
|
|
92
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if element has dimensions
|
|
97
|
+
const rect = element.getBoundingClientRect();
|
|
98
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extract data from a single element
|
|
107
|
+
*/
|
|
108
|
+
function extractElementData(element: Element, index: number): DOMElement | null {
|
|
109
|
+
const rect = element.getBoundingClientRect();
|
|
110
|
+
const htmlElement = element as HTMLElement;
|
|
111
|
+
|
|
112
|
+
// Get basic properties
|
|
113
|
+
const tag = element.tagName.toLowerCase();
|
|
114
|
+
const text = getElementText(element);
|
|
115
|
+
const ariaLabel = element.getAttribute('aria-label') || undefined;
|
|
116
|
+
const role = element.getAttribute('role') || undefined;
|
|
117
|
+
|
|
118
|
+
// Get input-specific properties
|
|
119
|
+
let type: string | undefined;
|
|
120
|
+
let value: string | undefined;
|
|
121
|
+
|
|
122
|
+
// eslint-disable-next-line no-undef
|
|
123
|
+
if (element instanceof HTMLInputElement) {
|
|
124
|
+
type = (element as unknown as { type: string }).type;
|
|
125
|
+
value = (element as unknown as { value: string }).value || undefined;
|
|
126
|
+
// eslint-disable-next-line no-undef
|
|
127
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
128
|
+
value = (element as unknown as { value: string }).value || undefined;
|
|
129
|
+
// eslint-disable-next-line no-undef
|
|
130
|
+
} else if (element instanceof HTMLTextAreaElement) {
|
|
131
|
+
type = 'textarea';
|
|
132
|
+
value = (element as unknown as { value: string }).value || undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if enabled
|
|
136
|
+
const enabled = !(
|
|
137
|
+
'disabled' in htmlElement &&
|
|
138
|
+
(htmlElement as unknown as { disabled?: boolean }).disabled === true
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Generate ID
|
|
142
|
+
const id = generateElementId(tag, text || ariaLabel || '', index);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
tag,
|
|
147
|
+
role,
|
|
148
|
+
ariaLabel,
|
|
149
|
+
text: truncateText(text, 200),
|
|
150
|
+
type,
|
|
151
|
+
value,
|
|
152
|
+
visible: true,
|
|
153
|
+
enabled,
|
|
154
|
+
bbox: {
|
|
155
|
+
x: Math.round(rect.x),
|
|
156
|
+
y: Math.round(rect.y),
|
|
157
|
+
width: Math.round(rect.width),
|
|
158
|
+
height: Math.round(rect.height),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get visible text content of an element
|
|
165
|
+
*/
|
|
166
|
+
function getElementText(element: Element): string {
|
|
167
|
+
// For inputs, use placeholder or value
|
|
168
|
+
// eslint-disable-next-line no-undef
|
|
169
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
170
|
+
return (
|
|
171
|
+
(element as unknown as { placeholder?: string }).placeholder ||
|
|
172
|
+
(element as unknown as { value?: string }).value ||
|
|
173
|
+
''
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// For links, get text content
|
|
178
|
+
const text = element.textContent?.trim() || '';
|
|
179
|
+
|
|
180
|
+
// For buttons, get aria-label if no text
|
|
181
|
+
if (!text && element.getAttribute('aria-label')) {
|
|
182
|
+
return element.getAttribute('aria-label')!;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return text;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Truncate text to max length
|
|
190
|
+
*/
|
|
191
|
+
function truncateText(text: string, maxLength: number): string {
|
|
192
|
+
if (!text) return '';
|
|
193
|
+
if (text.length <= maxLength) return text;
|
|
194
|
+
return text.substring(0, maxLength) + '...';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generate unique element ID
|
|
199
|
+
* Format: elem-{tag}-{label}-{index}
|
|
200
|
+
*/
|
|
201
|
+
function generateElementId(tag: string, label: string, index: number): string {
|
|
202
|
+
const cleanLabel = label
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
205
|
+
.replace(/^-+|-+$/g, '')
|
|
206
|
+
.substring(0, 30);
|
|
207
|
+
|
|
208
|
+
if (cleanLabel) {
|
|
209
|
+
return `elem-${tag}-${cleanLabel}-${index}`;
|
|
210
|
+
}
|
|
211
|
+
return `elem-${tag}-${index}`;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extract metadata from the page
|
|
218
|
+
*/
|
|
219
|
+
private async extractMetadata(page: Page): Promise<PageMetadata> {
|
|
220
|
+
return page.evaluate(() => {
|
|
221
|
+
const url = window.location.href;
|
|
222
|
+
const pageType = detectPageType(url);
|
|
223
|
+
const profileName = extractProfileName();
|
|
224
|
+
const profileTitle = extractProfileTitle();
|
|
225
|
+
const connectionState = detectConnectionState();
|
|
226
|
+
const statusMessages = extractStatusMessages();
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
pageType,
|
|
230
|
+
profileName,
|
|
231
|
+
profileTitle,
|
|
232
|
+
connectionState,
|
|
233
|
+
statusMessages,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Detect the type of LinkedIn page
|
|
238
|
+
*/
|
|
239
|
+
function detectPageType(url: string): PageType {
|
|
240
|
+
if (url.includes('/in/')) return 'profile';
|
|
241
|
+
if (url.includes('/messaging')) return 'messaging';
|
|
242
|
+
if (url.includes('/feed')) return 'feed';
|
|
243
|
+
if (url.includes('/search')) return 'search';
|
|
244
|
+
if (url.includes('/notifications')) return 'notifications';
|
|
245
|
+
return 'unknown';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extract profile name from page
|
|
250
|
+
*/
|
|
251
|
+
function extractProfileName(): string | undefined {
|
|
252
|
+
// Try h1 first
|
|
253
|
+
const h1 = document.querySelector('h1');
|
|
254
|
+
if (h1) {
|
|
255
|
+
const text = h1.textContent?.trim();
|
|
256
|
+
if (text) return text;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Try data-testid attributes common in LinkedIn
|
|
260
|
+
const selectors = [
|
|
261
|
+
'[data-testid="profile-name"]',
|
|
262
|
+
'[data-testid="top-card-profile-name"]',
|
|
263
|
+
'.profile-name',
|
|
264
|
+
'.top-card-layout__title',
|
|
265
|
+
'h1.text-heading-xlarge',
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
for (const selector of selectors) {
|
|
269
|
+
const element = document.querySelector(selector);
|
|
270
|
+
if (element) {
|
|
271
|
+
const text = element.textContent?.trim();
|
|
272
|
+
if (text) return text;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Extract profile title/headline
|
|
281
|
+
*/
|
|
282
|
+
function extractProfileTitle(): string | undefined {
|
|
283
|
+
const selectors = [
|
|
284
|
+
'[data-testid="profile-headline"]',
|
|
285
|
+
'[data-testid="top-card-profile-headline"]',
|
|
286
|
+
'.profile-headline',
|
|
287
|
+
'.top-card-layout__headline',
|
|
288
|
+
'.text-body-medium',
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
for (const selector of selectors) {
|
|
292
|
+
const element = document.querySelector(selector);
|
|
293
|
+
if (element) {
|
|
294
|
+
const text = element.textContent?.trim();
|
|
295
|
+
if (text) return text;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Detect connection state from page
|
|
304
|
+
*/
|
|
305
|
+
function detectConnectionState(): ConnectionState {
|
|
306
|
+
// Look for connection indicators
|
|
307
|
+
const pageText = document.body.innerText.toLowerCase();
|
|
308
|
+
|
|
309
|
+
// Check for "Connect" button text
|
|
310
|
+
const hasConnectButton = document.querySelector(
|
|
311
|
+
'button[aria-label*="connect" i], button:contains("Connect")'
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Check for pending status
|
|
315
|
+
const hasPendingText = pageText.includes('pending') || pageText.includes('invitation sent');
|
|
316
|
+
|
|
317
|
+
// Check for "Message" button (indicates connected)
|
|
318
|
+
const hasMessageButton = document.querySelector(
|
|
319
|
+
'button[aria-label*="message" i], button[data-control-name="message"], a[href*="messaging"]:not([href*="invite"])'
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Check for "Connected" text or similar indicators
|
|
323
|
+
const hasConnectedText =
|
|
324
|
+
pageText.includes('connected') ||
|
|
325
|
+
pageText.includes('1st') ||
|
|
326
|
+
pageText.includes('1st degree');
|
|
327
|
+
|
|
328
|
+
if (hasConnectedText || hasMessageButton) {
|
|
329
|
+
return 'connected';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (hasPendingText) {
|
|
333
|
+
return 'pending';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (hasConnectButton) {
|
|
337
|
+
return 'not_connected';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return 'unknown';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Extract status messages/notifications visible on page
|
|
345
|
+
*/
|
|
346
|
+
function extractStatusMessages(): string[] {
|
|
347
|
+
const messages: string[] = [];
|
|
348
|
+
|
|
349
|
+
// Look for common status message containers
|
|
350
|
+
const selectors = [
|
|
351
|
+
'[data-testid="status-message"]',
|
|
352
|
+
'.artdeco-toast-item',
|
|
353
|
+
'.alert',
|
|
354
|
+
'.notification-badge',
|
|
355
|
+
'[role="alert"]',
|
|
356
|
+
'[role="status"]',
|
|
357
|
+
'.feed-shared-update-v2__description',
|
|
358
|
+
'.artdeco-inline-feedback',
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
for (const selector of selectors) {
|
|
362
|
+
const elements = document.querySelectorAll(selector);
|
|
363
|
+
elements.forEach((el) => {
|
|
364
|
+
const text = el.textContent?.trim();
|
|
365
|
+
if (text && text.length > 0 && text.length < 500) {
|
|
366
|
+
messages.push(text);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Deduplicate
|
|
372
|
+
return [...new Set(messages)];
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Find element by ID in the extracted DOM representation
|
|
379
|
+
*/
|
|
380
|
+
findElementById(dom: DOMRepresentation, elementId: string): DOMElement | undefined {
|
|
381
|
+
return dom.elements.find((el) => el.id === elementId);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Find elements by tag name
|
|
386
|
+
*/
|
|
387
|
+
findElementsByTag(dom: DOMRepresentation, tag: string): DOMElement[] {
|
|
388
|
+
return dom.elements.filter((el) => el.tag === tag.toLowerCase());
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Find elements by text content (partial match)
|
|
393
|
+
*/
|
|
394
|
+
findElementsByText(dom: DOMRepresentation, text: string): DOMElement[] {
|
|
395
|
+
const searchText = text.toLowerCase();
|
|
396
|
+
return dom.elements.filter(
|
|
397
|
+
(el) =>
|
|
398
|
+
el.text.toLowerCase().includes(searchText) ||
|
|
399
|
+
(el.ariaLabel && el.ariaLabel.toLowerCase().includes(searchText))
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get clickable elements (buttons and links)
|
|
405
|
+
*/
|
|
406
|
+
getClickableElements(dom: DOMRepresentation): DOMElement[] {
|
|
407
|
+
return dom.elements.filter(
|
|
408
|
+
(el) =>
|
|
409
|
+
(el.tag === 'button' || el.tag === 'a' || el.role === 'button' || el.role === 'link') &&
|
|
410
|
+
el.visible &&
|
|
411
|
+
el.enabled
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get input elements (inputs, textareas, selects)
|
|
417
|
+
*/
|
|
418
|
+
getInputElements(dom: DOMRepresentation): DOMElement[] {
|
|
419
|
+
return dom.elements.filter(
|
|
420
|
+
(el) =>
|
|
421
|
+
(el.tag === 'input' ||
|
|
422
|
+
el.tag === 'textarea' ||
|
|
423
|
+
el.tag === 'select' ||
|
|
424
|
+
el.role === 'textbox' ||
|
|
425
|
+
el.role === 'combobox') &&
|
|
426
|
+
el.visible &&
|
|
427
|
+
el.enabled
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Create a new DOMExtractor instance
|
|
434
|
+
*/
|
|
435
|
+
export function createDOMExtractor(): DOMExtractor {
|
|
436
|
+
return new DOMExtractor();
|
|
437
|
+
}
|