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,393 @@
|
|
|
1
|
+
import { Page, ElementHandle } from 'playwright';
|
|
2
|
+
import { SELECTORS, SelectorCategory } from './selectors';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
export interface SelectorResult {
|
|
8
|
+
element: ElementHandle | null;
|
|
9
|
+
selector: string | null;
|
|
10
|
+
attempts: string[];
|
|
11
|
+
method: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SelectorLearning {
|
|
15
|
+
successes: Record<string, number>;
|
|
16
|
+
failures: Record<string, number>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SelectorEngine {
|
|
20
|
+
private page: Page;
|
|
21
|
+
private learningPath: string;
|
|
22
|
+
private learning: SelectorLearning;
|
|
23
|
+
|
|
24
|
+
constructor(page: Page) {
|
|
25
|
+
this.page = page;
|
|
26
|
+
this.learningPath = path.join(os.homedir(), '.linkedin-cli', 'selector-learning.json');
|
|
27
|
+
this.learning = this.loadLearning();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private loadLearning(): SelectorLearning {
|
|
31
|
+
try {
|
|
32
|
+
if (fs.existsSync(this.learningPath)) {
|
|
33
|
+
const data = fs.readFileSync(this.learningPath, 'utf-8');
|
|
34
|
+
return JSON.parse(data);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.log('Could not load selector learning:', error);
|
|
38
|
+
}
|
|
39
|
+
return { successes: {}, failures: {} };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private saveLearning(): void {
|
|
43
|
+
try {
|
|
44
|
+
const dir = path.dirname(this.learningPath);
|
|
45
|
+
if (!fs.existsSync(dir)) {
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
fs.writeFileSync(this.learningPath, JSON.stringify(this.learning, null, 2));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.log('Could not save selector learning:', error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private recordSuccess(key: string): void {
|
|
55
|
+
this.learning.successes[key] = (this.learning.successes[key] || 0) + 1;
|
|
56
|
+
this.saveLearning();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private recordFailure(key: string): void {
|
|
60
|
+
this.learning.failures[key] = (this.learning.failures[key] || 0) + 1;
|
|
61
|
+
this.saveLearning();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find element using hybrid approach:
|
|
66
|
+
* 1. Accessibility locators (getByRole, getByLabel, getByTestId)
|
|
67
|
+
* 2. Text pattern matching
|
|
68
|
+
* 3. CSS selectors with fuzzy matching
|
|
69
|
+
* 4. Legacy selectors from SELECTORS
|
|
70
|
+
*/
|
|
71
|
+
async findElement(
|
|
72
|
+
category: SelectorCategory,
|
|
73
|
+
key: string,
|
|
74
|
+
options: { timeout?: number; visible?: boolean; text?: string } = {}
|
|
75
|
+
): Promise<SelectorResult> {
|
|
76
|
+
const attempts: string[] = [];
|
|
77
|
+
const learningKey = `${category}.${key}`;
|
|
78
|
+
|
|
79
|
+
// Layer 1: Accessibility-first queries using Playwright locators
|
|
80
|
+
const a11yResult = await this.findByAccessibility(category, key, options);
|
|
81
|
+
if (a11yResult.element) {
|
|
82
|
+
this.recordSuccess(learningKey);
|
|
83
|
+
return a11yResult;
|
|
84
|
+
}
|
|
85
|
+
attempts.push(...a11yResult.attempts);
|
|
86
|
+
|
|
87
|
+
// Layer 2: Text pattern matching
|
|
88
|
+
if (options.text) {
|
|
89
|
+
const textResult = await this.findByText(options.text, options.timeout);
|
|
90
|
+
if (textResult.element) {
|
|
91
|
+
this.recordSuccess(learningKey);
|
|
92
|
+
return textResult;
|
|
93
|
+
}
|
|
94
|
+
attempts.push(...textResult.attempts);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Layer 3: Pattern-based CSS selectors (fuzzy class matching)
|
|
98
|
+
const patternResult = await this.findByPattern(category, key, options);
|
|
99
|
+
if (patternResult.element) {
|
|
100
|
+
this.recordSuccess(learningKey);
|
|
101
|
+
return patternResult;
|
|
102
|
+
}
|
|
103
|
+
attempts.push(...patternResult.attempts);
|
|
104
|
+
|
|
105
|
+
// Layer 4: Legacy selectors from SELECTORS
|
|
106
|
+
const legacyResult = await this.findByLegacy(category, key, options);
|
|
107
|
+
if (legacyResult.element) {
|
|
108
|
+
this.recordSuccess(learningKey);
|
|
109
|
+
return legacyResult;
|
|
110
|
+
}
|
|
111
|
+
attempts.push(...legacyResult.attempts);
|
|
112
|
+
|
|
113
|
+
// No element found
|
|
114
|
+
this.recordFailure(learningKey);
|
|
115
|
+
return { element: null, selector: null, attempts, method: 'none' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Layer 1: Accessibility-first queries
|
|
120
|
+
*/
|
|
121
|
+
private async findByAccessibility(
|
|
122
|
+
category: SelectorCategory,
|
|
123
|
+
key: string,
|
|
124
|
+
options: { timeout?: number } = {}
|
|
125
|
+
): Promise<SelectorResult> {
|
|
126
|
+
const timeout = options.timeout ?? 5000;
|
|
127
|
+
const attempts: string[] = [];
|
|
128
|
+
|
|
129
|
+
// Define accessibility queries based on category/key
|
|
130
|
+
const queries = this.buildAccessibilityQueries(category, key);
|
|
131
|
+
|
|
132
|
+
for (const query of queries) {
|
|
133
|
+
try {
|
|
134
|
+
attempts.push(`getByRole(${query.role}${query.name ? `, "${query.name}"` : ''})`);
|
|
135
|
+
const element = await this.page
|
|
136
|
+
.getByRole(query.role as any, {
|
|
137
|
+
name: query.name ? new RegExp(query.name, 'i') : undefined,
|
|
138
|
+
})
|
|
139
|
+
.elementHandle({ timeout });
|
|
140
|
+
if (element) {
|
|
141
|
+
return { element, selector: attempts[attempts.length - 1], attempts, method: 'a11y' };
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Continue to next query
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Try getByLabel
|
|
148
|
+
if (query.label) {
|
|
149
|
+
try {
|
|
150
|
+
attempts.push(`getByLabel("${query.label}")`);
|
|
151
|
+
const element = await this.page
|
|
152
|
+
.getByLabel(new RegExp(query.label, 'i'))
|
|
153
|
+
.elementHandle({ timeout });
|
|
154
|
+
if (element) {
|
|
155
|
+
return { element, selector: attempts[attempts.length - 1], attempts, method: 'a11y' };
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Continue
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Try getByTestId
|
|
163
|
+
if (query.testId) {
|
|
164
|
+
try {
|
|
165
|
+
attempts.push(`getByTestId("${query.testId}")`);
|
|
166
|
+
const element = await this.page.getByTestId(query.testId).elementHandle({ timeout });
|
|
167
|
+
if (element) {
|
|
168
|
+
return { element, selector: attempts[attempts.length - 1], attempts, method: 'a11y' };
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Continue
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { element: null, selector: null, attempts, method: 'a11y' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private buildAccessibilityQueries(
|
|
180
|
+
category: SelectorCategory,
|
|
181
|
+
key: string
|
|
182
|
+
): Array<{
|
|
183
|
+
role?: string;
|
|
184
|
+
name?: string;
|
|
185
|
+
label?: string;
|
|
186
|
+
testId?: string;
|
|
187
|
+
}> {
|
|
188
|
+
// Map category+key to accessibility queries
|
|
189
|
+
const queries: Array<{ role?: string; name?: string; label?: string; testId?: string }> = [];
|
|
190
|
+
|
|
191
|
+
if (category === 'messages' && key === 'conversationList') {
|
|
192
|
+
queries.push(
|
|
193
|
+
{ role: 'list', name: 'conversation' },
|
|
194
|
+
{ role: 'list', name: 'message' },
|
|
195
|
+
{ role: 'region', name: 'conversations' },
|
|
196
|
+
{ testId: 'conversations-list' },
|
|
197
|
+
{ testId: 'conversation-list' },
|
|
198
|
+
{ label: 'conversations' },
|
|
199
|
+
{ label: 'messages' }
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (category === 'messages' && key === 'conversationItem') {
|
|
204
|
+
queries.push(
|
|
205
|
+
{ role: 'listitem' },
|
|
206
|
+
{ role: 'article' },
|
|
207
|
+
{ role: 'button', name: 'conversation' },
|
|
208
|
+
{ testId: 'conversation-card' }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (category === 'messages' && key === 'messageInput') {
|
|
213
|
+
queries.push(
|
|
214
|
+
{ role: 'textbox', name: 'message' },
|
|
215
|
+
{ role: 'textbox', name: 'Write a message' },
|
|
216
|
+
{ label: 'Write a message' },
|
|
217
|
+
{ testId: 'message-input' }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return queries;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Layer 2: Find by text content
|
|
226
|
+
*/
|
|
227
|
+
private async findByText(text: string, timeout?: number): Promise<SelectorResult> {
|
|
228
|
+
const attempts: string[] = [];
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
attempts.push(`getByText("${text}")`);
|
|
232
|
+
const element = await this.page
|
|
233
|
+
.getByText(new RegExp(text, 'i'))
|
|
234
|
+
.elementHandle({ timeout: timeout ?? 5000 });
|
|
235
|
+
if (element) {
|
|
236
|
+
return { element, selector: attempts[attempts.length - 1], attempts, method: 'text' };
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { element: null, selector: null, attempts, method: 'text' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Layer 3: Pattern-based CSS selectors
|
|
247
|
+
*/
|
|
248
|
+
private async findByPattern(
|
|
249
|
+
category: SelectorCategory,
|
|
250
|
+
key: string,
|
|
251
|
+
options: { timeout?: number } = {}
|
|
252
|
+
): Promise<SelectorResult> {
|
|
253
|
+
const timeout = options.timeout ?? 5000;
|
|
254
|
+
const attempts: string[] = [];
|
|
255
|
+
|
|
256
|
+
// Get patterns for this category/key
|
|
257
|
+
const patterns = this.getPatternSelectors(category, key);
|
|
258
|
+
|
|
259
|
+
for (const pattern of patterns) {
|
|
260
|
+
try {
|
|
261
|
+
attempts.push(`CSS: ${pattern}`);
|
|
262
|
+
const element = await this.page.waitForSelector(pattern, { timeout, state: 'attached' });
|
|
263
|
+
if (element) {
|
|
264
|
+
return { element, selector: pattern, attempts, method: 'pattern' };
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// Continue
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { element: null, selector: null, attempts, method: 'pattern' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private getPatternSelectors(category: SelectorCategory, key: string): string[] {
|
|
275
|
+
// Pattern-based selectors using common LinkedIn class patterns
|
|
276
|
+
const patterns: Record<string, Record<string, string[]>> = {
|
|
277
|
+
messages: {
|
|
278
|
+
conversationList: [
|
|
279
|
+
'[class*="msg-conversations"]',
|
|
280
|
+
'[class*="conversations-list"]',
|
|
281
|
+
'[class*="msg-options"]',
|
|
282
|
+
'div[role="list"]',
|
|
283
|
+
'ul[role="list"]',
|
|
284
|
+
],
|
|
285
|
+
conversationItem: [
|
|
286
|
+
'[class*="msg-conversation"]',
|
|
287
|
+
'[class*="conversation-card"]',
|
|
288
|
+
'[class*="message-card"]',
|
|
289
|
+
'li[role="listitem"]',
|
|
290
|
+
'div[role="listitem"]',
|
|
291
|
+
],
|
|
292
|
+
messageInput: [
|
|
293
|
+
'[class*="msg-form"]',
|
|
294
|
+
'[class*="message-input"]',
|
|
295
|
+
'[contenteditable="true"]',
|
|
296
|
+
'div[role="textbox"]',
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return (patterns[category] as any)?.[key] || [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Layer 4: Legacy selectors from SELECTORS constant
|
|
306
|
+
*/
|
|
307
|
+
private async findByLegacy(
|
|
308
|
+
category: SelectorCategory,
|
|
309
|
+
key: string,
|
|
310
|
+
options: { timeout?: number; visible?: boolean } = {}
|
|
311
|
+
): Promise<SelectorResult> {
|
|
312
|
+
const selectors = this.getSelectors(category, key);
|
|
313
|
+
const attempts: string[] = [];
|
|
314
|
+
|
|
315
|
+
for (const selector of selectors) {
|
|
316
|
+
attempts.push(`Legacy: ${selector}`);
|
|
317
|
+
try {
|
|
318
|
+
const element = await this.page.waitForSelector(selector, {
|
|
319
|
+
timeout: options.timeout ?? 5000,
|
|
320
|
+
state: options.visible ? 'visible' : 'attached',
|
|
321
|
+
});
|
|
322
|
+
if (element) {
|
|
323
|
+
return { element, selector, attempts, method: 'legacy' };
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// Continue to next selector
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { element: null, selector: null, attempts, method: 'legacy' };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get selector strings for a category/key
|
|
335
|
+
*/
|
|
336
|
+
private getSelectors(category: SelectorCategory, key: string): string[] {
|
|
337
|
+
const categorySelectors = SELECTORS[category];
|
|
338
|
+
if (!categorySelectors) return [];
|
|
339
|
+
|
|
340
|
+
const keySelectors = categorySelectors[key as keyof typeof categorySelectors];
|
|
341
|
+
if (!keySelectors) return [];
|
|
342
|
+
|
|
343
|
+
return keySelectors as string[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Quickly check if an element exists without waiting
|
|
348
|
+
*/
|
|
349
|
+
async hasElement(category: SelectorCategory, key: string): Promise<boolean> {
|
|
350
|
+
const result = await this.findElement(category, key, { timeout: 2000 });
|
|
351
|
+
if (result.element) {
|
|
352
|
+
await result.element.dispose();
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Try to find element using semantic/accessible attributes first
|
|
360
|
+
*/
|
|
361
|
+
async findAccessible(
|
|
362
|
+
options: {
|
|
363
|
+
role?: string;
|
|
364
|
+
name?: string;
|
|
365
|
+
label?: string;
|
|
366
|
+
},
|
|
367
|
+
timeout = 5000
|
|
368
|
+
): Promise<ElementHandle | null> {
|
|
369
|
+
const selectors: string[] = [];
|
|
370
|
+
|
|
371
|
+
if (options.role && options.name) {
|
|
372
|
+
selectors.push(`[role="${options.role}"][aria-label*="${options.name}"]`);
|
|
373
|
+
selectors.push(`[role="${options.role}"][title*="${options.name}"]`);
|
|
374
|
+
}
|
|
375
|
+
if (options.label) {
|
|
376
|
+
selectors.push(`[aria-label*="${options.label}"]`);
|
|
377
|
+
selectors.push(`[placeholder*="${options.label}"]`);
|
|
378
|
+
}
|
|
379
|
+
if (options.role) {
|
|
380
|
+
selectors.push(`[role="${options.role}"]`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const selector of selectors) {
|
|
384
|
+
try {
|
|
385
|
+
const element = await this.page.waitForSelector(selector, { timeout });
|
|
386
|
+
if (element) return element;
|
|
387
|
+
} catch {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-layer selector system for LinkedIn's unstable UI
|
|
3
|
+
* Each selector key has multiple fallback strategies
|
|
4
|
+
*/
|
|
5
|
+
export const SELECTORS = {
|
|
6
|
+
// Login page
|
|
7
|
+
login: {
|
|
8
|
+
emailInput: ['input#username', 'input[name="session_key"]', 'input[type="email"]'],
|
|
9
|
+
passwordInput: ['input#password', 'input[name="session_password"]', 'input[type="password"]'],
|
|
10
|
+
submitButton: [
|
|
11
|
+
'button[type="submit"]',
|
|
12
|
+
'button.sign-in-form__submit-btn',
|
|
13
|
+
'button:has-text("Sign in")',
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// 2FA
|
|
18
|
+
twoFA: {
|
|
19
|
+
pinInput: [
|
|
20
|
+
'input#input__phone_verification_pin',
|
|
21
|
+
'input[name="pin"]',
|
|
22
|
+
'input[type="text"][*="pin" i]',
|
|
23
|
+
],
|
|
24
|
+
submitButton: [
|
|
25
|
+
'button[type="submit"]',
|
|
26
|
+
'button:has-text("Submit")',
|
|
27
|
+
'button:has-text("Verify")',
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Navigation / logged in indicators
|
|
32
|
+
nav: {
|
|
33
|
+
globalNav: ['nav.global-nav', 'header.global-nav', '[data-testid="global-nav"]'],
|
|
34
|
+
profileDropdown: [
|
|
35
|
+
'button[aria-label*="Settings"]',
|
|
36
|
+
'.global-nav__me-menu button',
|
|
37
|
+
'[data-testid="settings-menu-trigger"]',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Messaging
|
|
42
|
+
messages: {
|
|
43
|
+
conversationList: [
|
|
44
|
+
'div.msg-conversations-container__conversations-list',
|
|
45
|
+
'[data-testid="conversations-list"]',
|
|
46
|
+
'div[class*="conversations-list"]',
|
|
47
|
+
],
|
|
48
|
+
conversationItem: [
|
|
49
|
+
'div.msg-conversation-card',
|
|
50
|
+
'[data-testid="conversation-card"]',
|
|
51
|
+
'div[class*="conversation-card"]',
|
|
52
|
+
],
|
|
53
|
+
messageBubble: [
|
|
54
|
+
'div.msg-s-message-group__message',
|
|
55
|
+
'[data-testid="message-bubble"]',
|
|
56
|
+
'div[class*="message-bubble"]',
|
|
57
|
+
],
|
|
58
|
+
messageInput: [
|
|
59
|
+
'div.msg-form__contenteditable',
|
|
60
|
+
'div[contenteditable="true"][role="textbox"]',
|
|
61
|
+
'[data-testid="message-input"]',
|
|
62
|
+
],
|
|
63
|
+
sendButton: ['button.msg-form__send-btn', 'button[type="submit"]', 'button:has-text("Send")'],
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Connection requests
|
|
67
|
+
connection: {
|
|
68
|
+
// Primary connect button (various states)
|
|
69
|
+
// LinkedIn has many variants - we need comprehensive fallbacks
|
|
70
|
+
connectButton: [
|
|
71
|
+
// Aria-label variants
|
|
72
|
+
'button[aria-label*="Connect"]',
|
|
73
|
+
'button[aria-label*="Invite"]',
|
|
74
|
+
'button[aria-label*="to connect"]',
|
|
75
|
+
// Text content variants
|
|
76
|
+
'button:has-text("Connect")',
|
|
77
|
+
'button:has-text("Invite")',
|
|
78
|
+
'button:has-text("Connect ")',
|
|
79
|
+
// Class-based variants (LinkedIn uses artdeco classes)
|
|
80
|
+
'button.artdeco-button--primary:has-text("Connect")',
|
|
81
|
+
'button.artdeco-button--secondary:has-text("Connect")',
|
|
82
|
+
'button.artdeco-button--primary',
|
|
83
|
+
'button.artdeco-button--secondary',
|
|
84
|
+
// Specific profile page selectors
|
|
85
|
+
'.pv-top-card-v2-ctas button:has-text("Connect")',
|
|
86
|
+
'.profile-topcard-actions button:has-text("Connect")',
|
|
87
|
+
'div.pv-top-card-v2-ctas button',
|
|
88
|
+
// Data test IDs
|
|
89
|
+
'[data-testid="connect-button"]',
|
|
90
|
+
'[data-testid="invite-button"]',
|
|
91
|
+
// Generic fallback - any button with connect-related text
|
|
92
|
+
'button[id*="connect"]',
|
|
93
|
+
'button[id*="invite"]',
|
|
94
|
+
],
|
|
95
|
+
// More actions menu (three dots)
|
|
96
|
+
moreActionsButton: [
|
|
97
|
+
'button[aria-label*="More actions"]',
|
|
98
|
+
'button[aria-label*="More"]',
|
|
99
|
+
'button:has-text("More")',
|
|
100
|
+
'.artdeco-dropdown__trigger',
|
|
101
|
+
'button[data-testid="more-actions"]',
|
|
102
|
+
],
|
|
103
|
+
// Connect option in dropdown menu
|
|
104
|
+
connectOptionInMenu: [
|
|
105
|
+
'div[role="menuitem"]:has-text("Connect")',
|
|
106
|
+
'button:has-text("Connect")',
|
|
107
|
+
'[role="menuitem"][aria-label*="Connect"]',
|
|
108
|
+
],
|
|
109
|
+
// Add a note modal
|
|
110
|
+
addNoteButton: ['button[aria-label*="Add a note"]', 'button:has-text("Add a note")'],
|
|
111
|
+
noteTextarea: ['textarea[name="message"]', 'textarea[placeholder*="note"]', 'textarea'],
|
|
112
|
+
sendButton: [
|
|
113
|
+
// Aria-label variants
|
|
114
|
+
'button[aria-label*="Send invitation"]',
|
|
115
|
+
'button[aria-label*="Send invite"]',
|
|
116
|
+
'button[aria-label="Send"]',
|
|
117
|
+
// Text content variants
|
|
118
|
+
'button:has-text("Send")',
|
|
119
|
+
'button:has-text("Send invitation")',
|
|
120
|
+
'button:has-text("Send invite")',
|
|
121
|
+
// Modal-specific primary buttons
|
|
122
|
+
'.artdeco-modal button.artdeco-button--primary',
|
|
123
|
+
'.artdeco-modal button:has-text("Send")',
|
|
124
|
+
'.artdeco-modal button[type="submit"]',
|
|
125
|
+
// Generic modal submit
|
|
126
|
+
'[role="dialog"] button.artdeco-button--primary',
|
|
127
|
+
'[role="dialog"] button:has-text("Send")',
|
|
128
|
+
// Form submit in modal
|
|
129
|
+
'form button[type="submit"]',
|
|
130
|
+
'.artdeco-modal form button.artdeco-button--primary',
|
|
131
|
+
// Generic fallback
|
|
132
|
+
'button[type="submit"]',
|
|
133
|
+
'.artdeco-button--primary',
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Company profile extraction
|
|
138
|
+
company: {
|
|
139
|
+
name: [
|
|
140
|
+
'h1.text-heading-xlarge',
|
|
141
|
+
'.org-top-card-primary-content h1',
|
|
142
|
+
'h1.org-top-card-summary__title',
|
|
143
|
+
],
|
|
144
|
+
website: [
|
|
145
|
+
'a[data-testid="website-link"]',
|
|
146
|
+
'.org-about-us-module__website a',
|
|
147
|
+
'dd a[href^="http"]:not([href*="linkedin"])',
|
|
148
|
+
],
|
|
149
|
+
industry: [
|
|
150
|
+
'dt:has-text("Industry") + dd',
|
|
151
|
+
'.org-about-company-module__dl dt:has-text("Industry") + dd',
|
|
152
|
+
],
|
|
153
|
+
company_size: [
|
|
154
|
+
'dt:has-text("Company size") + dd',
|
|
155
|
+
'dt:has-text("Employees") + dd',
|
|
156
|
+
'.org-about-company-module__dl dt:has-text("Company size") + dd',
|
|
157
|
+
],
|
|
158
|
+
headquarters: [
|
|
159
|
+
'dt:has-text("Headquarters") + dd',
|
|
160
|
+
'.org-about-company-module__dl dt:has-text("Headquarters") + dd',
|
|
161
|
+
],
|
|
162
|
+
founded: [
|
|
163
|
+
'dt:has-text("Founded") + dd',
|
|
164
|
+
'.org-about-company-module__dl dt:has-text("Founded") + dd',
|
|
165
|
+
],
|
|
166
|
+
specialties: [
|
|
167
|
+
'dt:has-text("Specialties") + dd',
|
|
168
|
+
'.org-about-company-module__dl dt:has-text("Specialties") + dd',
|
|
169
|
+
],
|
|
170
|
+
type: [
|
|
171
|
+
'dt:has-text("Company type") + dd',
|
|
172
|
+
'dt:has-text("Type") + dd',
|
|
173
|
+
'.org-about-company-module__dl dt:has-text("Company type") + dd',
|
|
174
|
+
],
|
|
175
|
+
follower_count: [
|
|
176
|
+
'.org-top-card-primary-content__followers-count',
|
|
177
|
+
'.org-top-card-module__followers-count',
|
|
178
|
+
'span:has-text("followers")',
|
|
179
|
+
'.org-top-card-summary__followers',
|
|
180
|
+
],
|
|
181
|
+
// Authwall dismiss button
|
|
182
|
+
dismissAuthwall: [
|
|
183
|
+
'button[aria-label="Dismiss"]',
|
|
184
|
+
'button[aria-label="Close"]',
|
|
185
|
+
'.artdeco-modal__dismiss',
|
|
186
|
+
'button.artdeco-modal__dismiss',
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Profile extraction
|
|
191
|
+
profile: {
|
|
192
|
+
// Top card
|
|
193
|
+
name: [
|
|
194
|
+
'h1.text-heading-xlarge',
|
|
195
|
+
'[data-testid="top-card-profile-name"]',
|
|
196
|
+
'.pv-top-card .text-heading-xlarge',
|
|
197
|
+
'section.artdeco-card h1',
|
|
198
|
+
],
|
|
199
|
+
headline: [
|
|
200
|
+
'.text-body-medium',
|
|
201
|
+
'[data-testid="top-card-profile-headline"]',
|
|
202
|
+
'.pv-top-card .text-body-medium',
|
|
203
|
+
],
|
|
204
|
+
location: [
|
|
205
|
+
'.text-body-small.inline.t-black--light',
|
|
206
|
+
'[data-testid="top-card-profile-location"]',
|
|
207
|
+
'.pv-top-card .text-body-small',
|
|
208
|
+
],
|
|
209
|
+
|
|
210
|
+
// Experience section - find by heading text pattern
|
|
211
|
+
experienceSection: [
|
|
212
|
+
'section:has(h2:has-text("Experience"))',
|
|
213
|
+
'section[data-view-name="profile-card-experience"]',
|
|
214
|
+
'section[id*="experience"]',
|
|
215
|
+
],
|
|
216
|
+
experienceList: [
|
|
217
|
+
'li[data-view-name="profile-component-entity"]',
|
|
218
|
+
'section:has(h2:has-text("Experience")) li',
|
|
219
|
+
'.pvs-list__item',
|
|
220
|
+
],
|
|
221
|
+
// Title: First .t-bold span in each experience item
|
|
222
|
+
experienceTitle: [
|
|
223
|
+
'.t-bold span[aria-hidden="true"]',
|
|
224
|
+
'.hoverable-link-text span[aria-hidden="true"]',
|
|
225
|
+
'.display-flex.align-items-center.t-bold span',
|
|
226
|
+
],
|
|
227
|
+
// Company: First .t-14.t-normal span (after title)
|
|
228
|
+
experienceCompany: ['.t-14.t-normal span[aria-hidden="true"]', 'span[class*="t-14"]'],
|
|
229
|
+
// Duration: caption wrapper contains "X yrs Y mos"
|
|
230
|
+
experienceDuration: [
|
|
231
|
+
'.pvs-entity__caption-wrapper',
|
|
232
|
+
'.t-14.t-black--light span[aria-hidden="true"]',
|
|
233
|
+
'span[class*="caption"]',
|
|
234
|
+
],
|
|
235
|
+
companyLink: ['a[data-field="experience_company_logo"]', 'a[href*="/company/"]'],
|
|
236
|
+
|
|
237
|
+
// Contact info
|
|
238
|
+
contactInfoButton: [
|
|
239
|
+
'a[href*="contact-info"]',
|
|
240
|
+
'button[aria-label*="contact info" i]',
|
|
241
|
+
'[data-control-name="contact_see_more"]',
|
|
242
|
+
],
|
|
243
|
+
contactInfoPanel: [
|
|
244
|
+
'.pv-contact-info',
|
|
245
|
+
'[data-testid="contact-info-panel"]',
|
|
246
|
+
'.artdeco-modal__content',
|
|
247
|
+
],
|
|
248
|
+
contactInfoCloseButton: [
|
|
249
|
+
'.artdeco-modal__dismiss',
|
|
250
|
+
'button[aria-label*="Dismiss"]',
|
|
251
|
+
'button[aria-label*="Close"]',
|
|
252
|
+
'[data-testid="modal-close"]',
|
|
253
|
+
],
|
|
254
|
+
email: [
|
|
255
|
+
'a[href^="mailto:"]',
|
|
256
|
+
'[data-testid="contact-email"]',
|
|
257
|
+
'.pv-contact-info__contact-type[href*="mailto"]',
|
|
258
|
+
],
|
|
259
|
+
phone: [
|
|
260
|
+
'a[href^="tel:"]',
|
|
261
|
+
'[data-testid="contact-phone"]',
|
|
262
|
+
'.pv-contact-info__contact-type[href*="tel"]',
|
|
263
|
+
],
|
|
264
|
+
} as const,
|
|
265
|
+
} as const;
|
|
266
|
+
|
|
267
|
+
export type SelectorCategory = keyof typeof SELECTORS;
|
|
268
|
+
export type SelectorKey<C extends SelectorCategory> = keyof (typeof SELECTORS)[C];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Follow-up Message
|
|
3
|
+
description: Follow-up after initial outreach
|
|
4
|
+
category: followup
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Hi {{firstName}},
|
|
8
|
+
|
|
9
|
+
Just following up on my previous message. I know you're busy, so no pressure at all.
|
|
10
|
+
|
|
11
|
+
I'd still love to connect when you have a moment. Let me know if you're interested!
|
|
12
|
+
|
|
13
|
+
Best,
|
|
14
|
+
{{senderName}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Meeting Request
|
|
3
|
+
description: Request a meeting or call
|
|
4
|
+
category: meeting
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Hi {{firstName}},
|
|
8
|
+
|
|
9
|
+
I've been following {{company}}'s work and I'm really impressed with what you're building.
|
|
10
|
+
|
|
11
|
+
I have some ideas that might be relevant to your {{topic}} initiatives. Would you be open to a 15-minute call next week to explore this?
|
|
12
|
+
|
|
13
|
+
I'm flexible on timing - just let me know what works for you!
|
|
14
|
+
|
|
15
|
+
Best regards,
|
|
16
|
+
{{senderName}}
|