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,596 @@
|
|
|
1
|
+
# LinkedIn Connection Feature Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add a `connect` command to send LinkedIn connection requests to specific profiles, handling cases where the button is hidden behind the "..." menu.
|
|
6
|
+
|
|
7
|
+
**Architecture:**
|
|
8
|
+
- Add connection selectors to `selectors.ts` for various button states
|
|
9
|
+
- Create a `LinkedInConnector` class in `linkedin/connector.ts` to handle connection logic
|
|
10
|
+
- Add CLI command `linkedin connect <profile-url>` with option to add a note
|
|
11
|
+
- Handle the "More" (three dots) menu when the connect button is not directly visible
|
|
12
|
+
|
|
13
|
+
**Tech Stack:** TypeScript, Playwright, Commander.js
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Pre-Task: Complete Login First
|
|
18
|
+
|
|
19
|
+
**Status check:** Run `npm run build` and verify the project compiles.
|
|
20
|
+
|
|
21
|
+
**Command:**
|
|
22
|
+
```bash
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Expected:** TypeScript compiles without errors.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Task 1: Add Connection Selectors
|
|
31
|
+
|
|
32
|
+
**Files:**
|
|
33
|
+
- Modify: `src/linkedin/selectors.ts`
|
|
34
|
+
|
|
35
|
+
**Step 1: Add connection selectors to SELECTORS object**
|
|
36
|
+
|
|
37
|
+
Add a new `connection` section after the `messages` section:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// Connection requests
|
|
41
|
+
connection: {
|
|
42
|
+
// Primary connect button (various states)
|
|
43
|
+
connectButton: [
|
|
44
|
+
'button[aria-label*="Connect"]',
|
|
45
|
+
'button:has-text("Connect")',
|
|
46
|
+
'button.connect',
|
|
47
|
+
'[data-testid="connect-button"]',
|
|
48
|
+
],
|
|
49
|
+
// More actions menu (three dots)
|
|
50
|
+
moreActionsButton: [
|
|
51
|
+
'button[aria-label*="More actions"]',
|
|
52
|
+
'button[aria-label*="More"]',
|
|
53
|
+
'button:has-text("More")',
|
|
54
|
+
'.artdeco-dropdown__trigger',
|
|
55
|
+
'button[data-testid="more-actions"]',
|
|
56
|
+
],
|
|
57
|
+
// Connect option in dropdown menu
|
|
58
|
+
connectOptionInMenu: [
|
|
59
|
+
'div[role="menuitem"]:has-text("Connect")',
|
|
60
|
+
'button:has-text("Connect")',
|
|
61
|
+
'[role="menuitem"][aria-label*="Connect"]',
|
|
62
|
+
],
|
|
63
|
+
// Add a note modal
|
|
64
|
+
addNoteButton: [
|
|
65
|
+
'button[aria-label*="Add a note"]',
|
|
66
|
+
'button:has-text("Add a note")',
|
|
67
|
+
],
|
|
68
|
+
noteTextarea: [
|
|
69
|
+
'textarea[name="message"]',
|
|
70
|
+
'textarea[placeholder*="note"]',
|
|
71
|
+
'textarea',
|
|
72
|
+
],
|
|
73
|
+
sendButton: [
|
|
74
|
+
'button[aria-label*="Send invitation"]',
|
|
75
|
+
'button:has-text("Send")',
|
|
76
|
+
'button[type="submit"]',
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Step 2: Run build to verify no TypeScript errors**
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm run build
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Expected:** Compiles successfully.
|
|
88
|
+
|
|
89
|
+
**Step 3: Commit**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git add src/linkedin/selectors.ts
|
|
93
|
+
git commit -m "feat: add connection selectors for LinkedIn connect feature"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Task 2: Create LinkedInConnector Class
|
|
99
|
+
|
|
100
|
+
**Files:**
|
|
101
|
+
- Create: `src/linkedin/connector.ts`
|
|
102
|
+
- Create: `src/linkedin/__tests__/connector.test.ts` (optional, basic test)
|
|
103
|
+
|
|
104
|
+
**Step 1: Write the LinkedInConnector class**
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import type { BrowserController } from '../core/browser';
|
|
108
|
+
import { SELECTORS } from './selectors';
|
|
109
|
+
|
|
110
|
+
export interface ConnectionResult {
|
|
111
|
+
success: boolean;
|
|
112
|
+
error?: string;
|
|
113
|
+
sent?: boolean;
|
|
114
|
+
pending?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ConnectionOptions {
|
|
118
|
+
profileUrl: string;
|
|
119
|
+
note?: string;
|
|
120
|
+
skipNote?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class LinkedInConnector {
|
|
124
|
+
private browser: BrowserController;
|
|
125
|
+
|
|
126
|
+
constructor(browser: BrowserController) {
|
|
127
|
+
this.browser = browser;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Send a connection request to a LinkedIn profile
|
|
132
|
+
*/
|
|
133
|
+
async connect(options: ConnectionOptions): Promise<ConnectionResult> {
|
|
134
|
+
const page = this.browser.getPage();
|
|
135
|
+
if (!page) {
|
|
136
|
+
return { success: false, error: 'Browser not initialized' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Navigate to the profile
|
|
141
|
+
console.log(`Navigating to ${options.profileUrl}...`);
|
|
142
|
+
await page.goto(options.profileUrl, {
|
|
143
|
+
waitUntil: 'domcontentloaded',
|
|
144
|
+
timeout: 30000,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Wait a moment for any dynamic content
|
|
148
|
+
await page.waitForTimeout(2000);
|
|
149
|
+
|
|
150
|
+
// Try to find the Connect button
|
|
151
|
+
console.log('Looking for Connect button...');
|
|
152
|
+
const connectResult = await this.findConnectButton();
|
|
153
|
+
|
|
154
|
+
if (!connectResult) {
|
|
155
|
+
// Check if already connected or pending
|
|
156
|
+
const status = await this.checkConnectionStatus();
|
|
157
|
+
if (status === 'connected') {
|
|
158
|
+
return { success: true, sent: false, error: 'Already connected to this person' };
|
|
159
|
+
} else if (status === 'pending') {
|
|
160
|
+
return { success: true, pending: true, error: 'Connection request already sent (pending)' };
|
|
161
|
+
}
|
|
162
|
+
return { success: false, error: 'Could not find Connect button - LinkedIn UI may have changed' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Click the Connect button
|
|
166
|
+
console.log('Clicking Connect button...');
|
|
167
|
+
await connectResult.element.click();
|
|
168
|
+
await page.waitForTimeout(1000);
|
|
169
|
+
|
|
170
|
+
// Handle adding a note if specified
|
|
171
|
+
if (options.note && !options.skipNote) {
|
|
172
|
+
const noteResult = await this.findElementWithFallbacks(
|
|
173
|
+
SELECTORS.connection.addNoteButton,
|
|
174
|
+
'Add a note button'
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (noteResult?.element) {
|
|
178
|
+
console.log('Clicking "Add a note" button...');
|
|
179
|
+
await noteResult.element.click();
|
|
180
|
+
await page.waitForTimeout(500);
|
|
181
|
+
|
|
182
|
+
// Find and fill the note textarea
|
|
183
|
+
const textareaResult = await this.findElementWithFallbacks(
|
|
184
|
+
SELECTORS.connection.noteTextarea,
|
|
185
|
+
'Note textarea'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (textareaResult?.element) {
|
|
189
|
+
console.log('Adding note...');
|
|
190
|
+
const page = this.browser.getPage();
|
|
191
|
+
if (page) {
|
|
192
|
+
await page.fill(textareaResult.selectorUsed, options.note);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Send the invitation
|
|
199
|
+
console.log('Sending connection request...');
|
|
200
|
+
const sendResult = await this.findElementWithFallbacks(
|
|
201
|
+
SELECTORS.connection.sendButton,
|
|
202
|
+
'Send invitation button'
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (sendResult?.element) {
|
|
206
|
+
await sendResult.element.click();
|
|
207
|
+
await page.waitForTimeout(1000);
|
|
208
|
+
console.log('Connection request sent successfully!');
|
|
209
|
+
return { success: true, sent: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { success: false, error: 'Could not find Send button' };
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
216
|
+
console.error('Connection error:', errorMessage);
|
|
217
|
+
return { success: false, error: `Connection error: ${errorMessage}` };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Try to find the Connect button - either directly or in the More menu
|
|
223
|
+
*/
|
|
224
|
+
private async findConnectButton(): Promise<{ element: Awaited<ReturnType<import('playwright').Page['$']>>; selectorUsed: string } | null> {
|
|
225
|
+
// First try direct Connect button
|
|
226
|
+
const directConnect = await this.findElementWithFallbacks(
|
|
227
|
+
SELECTORS.connection.connectButton,
|
|
228
|
+
'direct Connect button'
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (directConnect) {
|
|
232
|
+
return directConnect;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log('Direct Connect button not found, checking More menu...');
|
|
236
|
+
|
|
237
|
+
// Try the More menu
|
|
238
|
+
const moreButton = await this.findElementWithFallbacks(
|
|
239
|
+
SELECTORS.connection.moreActionsButton,
|
|
240
|
+
'More actions button'
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (moreButton?.element) {
|
|
244
|
+
console.log('Clicking More actions menu...');
|
|
245
|
+
const page = this.browser.getPage();
|
|
246
|
+
if (page) {
|
|
247
|
+
await moreButton.element.click();
|
|
248
|
+
await page.waitForTimeout(1000);
|
|
249
|
+
|
|
250
|
+
// Look for Connect option in the dropdown
|
|
251
|
+
const connectOption = await this.findElementWithFallbacks(
|
|
252
|
+
SELECTORS.connection.connectOptionInMenu,
|
|
253
|
+
'Connect option in menu'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (connectOption) {
|
|
257
|
+
return connectOption;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check the current connection status with a person
|
|
267
|
+
*/
|
|
268
|
+
private async checkConnectionStatus(): Promise<'connected' | 'pending' | 'none' | 'unknown'> {
|
|
269
|
+
const page = this.browser.getPage();
|
|
270
|
+
if (!page) return 'unknown';
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Look for indicators of existing connection
|
|
274
|
+
const indicators = [
|
|
275
|
+
{ selector: 'button[aria-label*="Message"] i', text: 'Message', status: 'connected' as const },
|
|
276
|
+
{ selector: 'button:has-text("Pending")', text: 'Pending', status: 'pending' as const },
|
|
277
|
+
{ selector: 'span:has-text("1st")', text: '1st', status: 'connected' as const },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (const indicator of indicators) {
|
|
281
|
+
const element = await page.$(indicator.selector);
|
|
282
|
+
if (element) {
|
|
283
|
+
return indicator.status;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return 'none';
|
|
288
|
+
} catch {
|
|
289
|
+
return 'unknown';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Try to find an element using multiple selector strategies
|
|
295
|
+
*/
|
|
296
|
+
private async findElementWithFallbacks(
|
|
297
|
+
selectors: readonly string[],
|
|
298
|
+
elementName: string
|
|
299
|
+
): Promise<{ element: Awaited<ReturnType<import('playwright').Page['$']>>; selectorUsed: string } | null> {
|
|
300
|
+
const page = this.browser.getPage();
|
|
301
|
+
if (!page) return null;
|
|
302
|
+
|
|
303
|
+
for (const selector of selectors) {
|
|
304
|
+
try {
|
|
305
|
+
const element = await page.$(selector);
|
|
306
|
+
if (element) {
|
|
307
|
+
console.log(`Found ${elementName} using selector: ${selector}`);
|
|
308
|
+
return { element, selectorUsed: selector };
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// Continue to next selector
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Step 2: Create the CLI command for connections**
|
|
321
|
+
|
|
322
|
+
Create `src/cli/connection.ts`:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { Command } from 'commander';
|
|
326
|
+
import chalk from 'chalk';
|
|
327
|
+
import { BrowserController } from '../core/browser';
|
|
328
|
+
import { LinkedInConnector } from '../linkedin/connector';
|
|
329
|
+
import { getSecureStorage } from '../core/storage';
|
|
330
|
+
import { getAuditLogger } from '../core/audit';
|
|
331
|
+
import { getConfig } from '../core/config';
|
|
332
|
+
|
|
333
|
+
const SESSION_KEY = 'linkedin-session';
|
|
334
|
+
|
|
335
|
+
export function registerConnectionCommands(program: Command): void {
|
|
336
|
+
const connection = program
|
|
337
|
+
.command('connect')
|
|
338
|
+
.description('Send connection requests to LinkedIn profiles');
|
|
339
|
+
|
|
340
|
+
connection
|
|
341
|
+
.command('send')
|
|
342
|
+
.description('Send a connection request to a profile')
|
|
343
|
+
.argument('<profile-url>', 'LinkedIn profile URL (e.g., https://www.linkedin.com/in/williamhgates/)')
|
|
344
|
+
.option('-n, --note <note>', 'Personalized note to include with connection request (max 300 characters)')
|
|
345
|
+
.option('--headless', 'Run browser in headless mode', false)
|
|
346
|
+
.action(async (profileUrl, options) => {
|
|
347
|
+
const storage = getSecureStorage();
|
|
348
|
+
const logger = getAuditLogger();
|
|
349
|
+
const config = getConfig();
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
// Validate profile URL
|
|
353
|
+
if (!profileUrl.match(/^https?:\/\/www\.linkedin\.com\/in\/[^\/]+\/?$/)) {
|
|
354
|
+
console.error(chalk.red('✗ Invalid profile URL'));
|
|
355
|
+
console.log(chalk.gray('Expected format: https://www.linkedin.com/in/username/'));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Check for existing session
|
|
360
|
+
if (!storage.exists(SESSION_KEY)) {
|
|
361
|
+
console.error(chalk.red('✗ Not logged in'));
|
|
362
|
+
console.log(chalk.gray('Run: linkedin-cli auth login'));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const sessionData = storage.load(SESSION_KEY);
|
|
367
|
+
if (!sessionData) {
|
|
368
|
+
console.error(chalk.red('✗ Session data corrupted'));
|
|
369
|
+
console.log(chalk.gray('Run: linkedin-cli auth login'));
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
console.log(chalk.blue(`Preparing to connect with ${profileUrl}...`));
|
|
374
|
+
|
|
375
|
+
// Launch browser
|
|
376
|
+
const browser = new BrowserController({
|
|
377
|
+
headless: options.headless ?? config.getValue('headless'),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await browser.launch();
|
|
381
|
+
|
|
382
|
+
// Restore session
|
|
383
|
+
const session = JSON.parse(sessionData);
|
|
384
|
+
await browser.restoreSession(session);
|
|
385
|
+
|
|
386
|
+
// Create connector and send request
|
|
387
|
+
const connector = new LinkedInConnector(browser);
|
|
388
|
+
const result = await connector.connect({
|
|
389
|
+
profileUrl,
|
|
390
|
+
note: options.note,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (result.success && result.sent) {
|
|
394
|
+
console.log(chalk.green('✓ Connection request sent successfully!'));
|
|
395
|
+
logger.log('connection.send', { profileUrl, hasNote: !!options.note }, true);
|
|
396
|
+
} else if (result.pending) {
|
|
397
|
+
console.log(chalk.yellow('⚠ Connection request already pending'));
|
|
398
|
+
} else if (result.error?.includes('Already connected')) {
|
|
399
|
+
console.log(chalk.yellow('✓ Already connected to this person'));
|
|
400
|
+
} else {
|
|
401
|
+
console.error(chalk.red('✗ Failed to send connection request:'), result.error);
|
|
402
|
+
logger.log('connection.send', { profileUrl, error: result.error }, false);
|
|
403
|
+
await browser.close();
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await browser.close();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error(chalk.red('✗ Connection failed:'), error instanceof Error ? error.message : error);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Step 3: Register the command in the main CLI**
|
|
417
|
+
|
|
418
|
+
Modify `src/index.ts` to import and register the connection commands:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { registerAuthCommands, registerMessageCommands, registerReplyCommands, registerConnectionCommands } from './cli';
|
|
422
|
+
|
|
423
|
+
// ... existing code ...
|
|
424
|
+
|
|
425
|
+
// Register command groups
|
|
426
|
+
registerAuthCommands(program);
|
|
427
|
+
registerMessageCommands(program);
|
|
428
|
+
registerReplyCommands(program);
|
|
429
|
+
registerConnectionCommands(program); // Add this line
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Step 4: Export the command from cli/index.ts**
|
|
433
|
+
|
|
434
|
+
Modify `src/cli/index.ts` to export the connection commands:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
export { registerAuthCommands } from './auth';
|
|
438
|
+
export { registerMessageCommands } from './messages';
|
|
439
|
+
export { registerReplyCommands } from './reply';
|
|
440
|
+
export { registerConnectionCommands } from './connection'; // Add this line
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Step 5: Run build to verify no TypeScript errors**
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
npm run build
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**Expected:** Compiles successfully.
|
|
450
|
+
|
|
451
|
+
**Step 6: Commit**
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
git add src/linkedin/connector.ts src/cli/connection.ts src/cli/index.ts src/index.ts
|
|
455
|
+
git commit -m "feat: add connection command to send LinkedIn connection requests"
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Task 3: Test Connection Feature
|
|
461
|
+
|
|
462
|
+
**Step 1: Verify the command is registered**
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
node dist/index.js --help
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Expected:** See `connect` command in the list.
|
|
469
|
+
|
|
470
|
+
**Step 2: Check help for connect command**
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
node dist/index.js connect send --help
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Expected:** Shows usage: `Usage: linkedin-cli connect send [options] <profile-url>`
|
|
477
|
+
|
|
478
|
+
**Step 3: Test without login (should fail with helpful message)**
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
node dist/index.js connect send "https://www.linkedin.com/in/williamhgates/"
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Expected:** Error message: "Not logged in. Run: linkedin-cli auth login"
|
|
485
|
+
|
|
486
|
+
**Step 4: Commit test results**
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
git add -A
|
|
490
|
+
git commit -m "test: verify connection command structure"
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Task 4: Document the Connection Feature
|
|
496
|
+
|
|
497
|
+
**Files:**
|
|
498
|
+
- Create: `docs/connection-command.md`
|
|
499
|
+
|
|
500
|
+
**Step 1: Write documentation**
|
|
501
|
+
|
|
502
|
+
```markdown
|
|
503
|
+
# Connection Command
|
|
504
|
+
|
|
505
|
+
Send LinkedIn connection requests via CLI.
|
|
506
|
+
|
|
507
|
+
## Prerequisites
|
|
508
|
+
|
|
509
|
+
You must be logged in:
|
|
510
|
+
```bash
|
|
511
|
+
linkedin-cli auth login
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Usage
|
|
515
|
+
|
|
516
|
+
### Basic connection request
|
|
517
|
+
```bash
|
|
518
|
+
linkedin-cli connect send "https://www.linkedin.com/in/williamhgates/"
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### With a personalized note
|
|
522
|
+
```bash
|
|
523
|
+
linkedin-cli connect send "https://www.linkedin.com/in/williamhgates/" \
|
|
524
|
+
--note "Hi Bill, I'd love to connect and discuss technology."
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Run in headless mode
|
|
528
|
+
```bash
|
|
529
|
+
linkedin-cli connect send "https://www.linkedin.com/in/williamhgates/" --headless
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## How It Works
|
|
533
|
+
|
|
534
|
+
1. **Navigates to profile:** Opens the LinkedIn profile URL
|
|
535
|
+
2. **Finds Connect button:** Tries multiple selectors to locate the Connect button
|
|
536
|
+
3. **Handles "More" menu:** If Connect is hidden, clicks the "..." (More actions) button and selects Connect from the dropdown
|
|
537
|
+
4. **Optional note:** If `--note` provided, clicks "Add a note", fills the textarea
|
|
538
|
+
5. **Sends request:** Clicks "Send invitation"
|
|
539
|
+
|
|
540
|
+
## Error Handling
|
|
541
|
+
|
|
542
|
+
- **Not logged in:** Prompts to run `linkedin-cli auth login`
|
|
543
|
+
- **Already connected:** Shows "Already connected to this person"
|
|
544
|
+
- **Pending request:** Shows "Connection request already pending"
|
|
545
|
+
- **Button not found:** Reports "LinkedIn UI may have changed" with debug info
|
|
546
|
+
|
|
547
|
+
## Technical Details
|
|
548
|
+
|
|
549
|
+
The command uses a multi-layer selector system to handle LinkedIn's dynamic UI:
|
|
550
|
+
- Tries `aria-label` selectors first (accessibility-friendly)
|
|
551
|
+
- Falls back to text-based selectors (`:has-text()`)
|
|
552
|
+
- Uses class-based selectors as final fallback
|
|
553
|
+
|
|
554
|
+
This approach makes the tool resilient to LinkedIn's frequent UI updates.
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
**Step 2: Commit documentation**
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
git add docs/connection-command.md
|
|
561
|
+
git commit -m "docs: add connection command documentation"
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Final Verification
|
|
567
|
+
|
|
568
|
+
**Run all tests and verify build:**
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
npm run build
|
|
572
|
+
node dist/index.js --help
|
|
573
|
+
node dist/index.js connect send --help
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Final commit:**
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
git add -A
|
|
580
|
+
git commit -m "feat: complete LinkedIn connection request feature"
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## Execution Summary
|
|
586
|
+
|
|
587
|
+
This implementation adds a complete `connect` command that:
|
|
588
|
+
|
|
589
|
+
1. **Accepts profile URLs** - Validates LinkedIn profile URL format
|
|
590
|
+
2. **Supports personalized notes** - `--note` option for custom messages
|
|
591
|
+
3. **Handles hidden buttons** - Clicks "..." menu when Connect is not directly visible
|
|
592
|
+
4. **Provides clear feedback** - Success/error messages for all scenarios
|
|
593
|
+
5. **Uses resilient selectors** - Multi-layer fallback system
|
|
594
|
+
6. **Includes full documentation** - Usage guide and technical details
|
|
595
|
+
|
|
596
|
+
**Estimated implementation time:** 45-60 minutes for all tasks.
|