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,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cookie Import Tool for LinkedIn CLI
|
|
4
|
+
*
|
|
5
|
+
* This tool imports cookies from various sources into the CLI's encrypted storage.
|
|
6
|
+
* Works on macOS and Linux.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node import-cookies.js --file cookies.json
|
|
10
|
+
* node import-cookies.js --browser chrome
|
|
11
|
+
* node import-cookies.js --browser safari
|
|
12
|
+
* node import-cookies.js --browser edge
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const CONFIG_DIR = path.join(require('os').homedir(), '.linkedin-cli');
|
|
20
|
+
const SESSION_FILE = path.join(CONFIG_DIR, 'session.enc');
|
|
21
|
+
|
|
22
|
+
// Parse command line arguments
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
let sourceFile = null;
|
|
25
|
+
let browser = null;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (args[i] === '--file' || args[i] === '-f') {
|
|
29
|
+
sourceFile = args[i + 1];
|
|
30
|
+
i++;
|
|
31
|
+
} else if (args[i] === '--browser' || args[i] === '-b') {
|
|
32
|
+
browser = args[i + 1]?.toLowerCase();
|
|
33
|
+
i++;
|
|
34
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
35
|
+
showHelp();
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showHelp() {
|
|
41
|
+
console.log(`
|
|
42
|
+
Cookie Import Tool for LinkedIn CLI
|
|
43
|
+
====================================
|
|
44
|
+
|
|
45
|
+
Import cookies from various sources into the CLI's encrypted storage.
|
|
46
|
+
|
|
47
|
+
USAGE:
|
|
48
|
+
node import-cookies.js [OPTIONS]
|
|
49
|
+
|
|
50
|
+
OPTIONS:
|
|
51
|
+
--file, -f <path> Import from a JSON/Netscape cookies file
|
|
52
|
+
--browser, -b <name> Import directly from browser
|
|
53
|
+
Supported: chrome, edge, safari, firefox
|
|
54
|
+
--help, -h Show this help
|
|
55
|
+
|
|
56
|
+
EXAMPLES:
|
|
57
|
+
# Import from file
|
|
58
|
+
node import-cookies.js --file ./linkedin_cookies.json
|
|
59
|
+
|
|
60
|
+
# Import from Chrome (macOS)
|
|
61
|
+
node import-cookies.js --browser chrome
|
|
62
|
+
|
|
63
|
+
# Import from Safari (macOS)
|
|
64
|
+
node import-cookies.js --browser safari
|
|
65
|
+
|
|
66
|
+
SUPPORTED COOKIE FORMATS:
|
|
67
|
+
1. JSON format (EditThisCookie extension)
|
|
68
|
+
[{"name": "li_at", "value": "...", "domain": ".linkedin.com", ...}]
|
|
69
|
+
|
|
70
|
+
2. Netscape format
|
|
71
|
+
# Netscape HTTP Cookie File
|
|
72
|
+
.linkedin.com\tTRUE\t/\tFALSE\t1234567890\tli_at\t...
|
|
73
|
+
|
|
74
|
+
3. Direct browser extraction (macOS)
|
|
75
|
+
Automatically extracts from browser's cookie store
|
|
76
|
+
|
|
77
|
+
SECURITY NOTES:
|
|
78
|
+
- Cookies are encrypted with AES-256-GCM
|
|
79
|
+
- Encryption key is machine-bound
|
|
80
|
+
- Original cookie files should be deleted after import
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check if running on macOS
|
|
85
|
+
function isMacOS() {
|
|
86
|
+
return process.platform === 'darwin';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if running on Linux
|
|
90
|
+
function isLinux() {
|
|
91
|
+
return process.platform === 'linux';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract cookies from Chrome/Chromium on macOS
|
|
95
|
+
function extractChromeCookies() {
|
|
96
|
+
const chromePath = path.join(
|
|
97
|
+
require('os').homedir(),
|
|
98
|
+
'Library/Application Support/Google/Chrome/Default/Cookies'
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!fs.existsSync(chromePath)) {
|
|
102
|
+
console.error('❌ Chrome cookies not found at:', chromePath);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('🔍 Found Chrome cookies at:', chromePath);
|
|
107
|
+
|
|
108
|
+
// Try to use sqlite3 to extract LinkedIn cookies
|
|
109
|
+
try {
|
|
110
|
+
const result = execSync(
|
|
111
|
+
`sqlite3 "${chromePath}" "SELECT name, value, host_key, path, expires_utc FROM cookies WHERE host_key LIKE '%linkedin.com%'" 2>/dev/null`,
|
|
112
|
+
{ encoding: 'utf8' }
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!result.trim()) {
|
|
116
|
+
console.error('❌ No LinkedIn cookies found in Chrome');
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const cookies = [];
|
|
121
|
+
const lines = result.trim().split('\n');
|
|
122
|
+
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const [name, value, domain, path, expires] = line.split('|');
|
|
125
|
+
if (name && value) {
|
|
126
|
+
cookies.push({
|
|
127
|
+
name: name.trim(),
|
|
128
|
+
value: value.trim(),
|
|
129
|
+
domain: domain.trim(),
|
|
130
|
+
path: path?.trim() || '/',
|
|
131
|
+
expires: expires ? parseInt(expires.trim()) / 1000000 - 11644473600 : undefined
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return cookies;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('❌ Failed to extract Chrome cookies:', error.message);
|
|
139
|
+
console.log('💡 Chrome might be running. Close Chrome and try again.');
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Extract cookies from Edge on macOS
|
|
145
|
+
function extractEdgeCookies() {
|
|
146
|
+
const edgePath = path.join(
|
|
147
|
+
require('os').homedir(),
|
|
148
|
+
'Library/Application Support/Microsoft Edge/Default/Cookies'
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(edgePath)) {
|
|
152
|
+
console.error('❌ Edge cookies not found at:', edgePath);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('🔍 Found Edge cookies at:', edgePath);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const result = execSync(
|
|
160
|
+
`sqlite3 "${edgePath}" "SELECT name, value, host_key, path, expires_utc FROM cookies WHERE host_key LIKE '%linkedin.com%'" 2>/dev/null`,
|
|
161
|
+
{ encoding: 'utf8' }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (!result.trim()) {
|
|
165
|
+
console.error('❌ No LinkedIn cookies found in Edge');
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cookies = [];
|
|
170
|
+
const lines = result.trim().split('\n');
|
|
171
|
+
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const [name, value, domain, path, expires] = line.split('|');
|
|
174
|
+
if (name && value) {
|
|
175
|
+
cookies.push({
|
|
176
|
+
name: name.trim(),
|
|
177
|
+
value: value.trim(),
|
|
178
|
+
domain: domain.trim(),
|
|
179
|
+
path: path?.trim() || '/',
|
|
180
|
+
expires: expires ? parseInt(expires.trim()) / 1000000 - 11644473600 : undefined
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return cookies;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('❌ Failed to extract Edge cookies:', error.message);
|
|
188
|
+
console.log('💡 Edge might be running. Close Edge and try again.');
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Parse JSON cookie file (from EditThisCookie extension)
|
|
194
|
+
function parseJSONCookies(filePath) {
|
|
195
|
+
try {
|
|
196
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
197
|
+
const cookies = JSON.parse(content);
|
|
198
|
+
|
|
199
|
+
if (!Array.isArray(cookies)) {
|
|
200
|
+
throw new Error('Invalid format: expected array of cookies');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Filter only LinkedIn cookies
|
|
204
|
+
return cookies.filter(cookie =>
|
|
205
|
+
cookie.domain?.includes('linkedin.com') ||
|
|
206
|
+
cookie.host?.includes('linkedin.com')
|
|
207
|
+
);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error('❌ Failed to parse JSON cookies:', error.message);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Parse Netscape cookie file
|
|
215
|
+
function parseNetscapeCookies(filePath) {
|
|
216
|
+
try {
|
|
217
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
218
|
+
const lines = content.split('\n');
|
|
219
|
+
const cookies = [];
|
|
220
|
+
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
// Skip comments and empty lines
|
|
223
|
+
if (line.startsWith('#') || !line.trim()) continue;
|
|
224
|
+
|
|
225
|
+
const parts = line.split('\t');
|
|
226
|
+
if (parts.length >= 7) {
|
|
227
|
+
const [domain, flag, path, secure, expiration, name, value] = parts;
|
|
228
|
+
|
|
229
|
+
if (domain.includes('linkedin.com')) {
|
|
230
|
+
cookies.push({
|
|
231
|
+
name: name.trim(),
|
|
232
|
+
value: value.trim(),
|
|
233
|
+
domain: domain.trim(),
|
|
234
|
+
path: path.trim(),
|
|
235
|
+
secure: secure === 'TRUE',
|
|
236
|
+
expirationDate: expiration !== '0' ? parseInt(expiration) : undefined
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return cookies;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('❌ Failed to parse Netscape cookies:', error.message);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Import cookies into CLI storage
|
|
250
|
+
async function importCookies(cookies) {
|
|
251
|
+
if (!cookies || cookies.length === 0) {
|
|
252
|
+
console.error('❌ No cookies to import');
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log(`\n📦 Found ${cookies.length} LinkedIn cookies:`);
|
|
257
|
+
cookies.forEach(cookie => {
|
|
258
|
+
console.log(` - ${cookie.name} (${cookie.domain})`);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Ensure config directory exists
|
|
262
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
263
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Create session data structure
|
|
267
|
+
const sessionData = {
|
|
268
|
+
cookies: cookies,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Import the secure storage from the CLI
|
|
274
|
+
try {
|
|
275
|
+
// We need to use the CLI's encryption module
|
|
276
|
+
const { getSecureStorage } = require('./dist/core/storage');
|
|
277
|
+
const storage = getSecureStorage();
|
|
278
|
+
|
|
279
|
+
storage.save('linkedin-session', JSON.stringify(sessionData));
|
|
280
|
+
|
|
281
|
+
console.log('\n✅ Cookies imported successfully!');
|
|
282
|
+
console.log(`📁 Session saved to: ${SESSION_FILE}`);
|
|
283
|
+
console.log('\n🔒 Security notes:');
|
|
284
|
+
console.log(' - Cookies are encrypted with AES-256-GCM');
|
|
285
|
+
console.log(' - Encryption key is machine-bound');
|
|
286
|
+
console.log(' - Session expires in 24 hours');
|
|
287
|
+
console.log('\n📝 Next steps:');
|
|
288
|
+
console.log(' 1. Test: node ./dist/index.js auth status');
|
|
289
|
+
console.log(' 2. List messages: node ./dist/index.js messages list');
|
|
290
|
+
|
|
291
|
+
return true;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('❌ Failed to import cookies:', error.message);
|
|
294
|
+
console.log('\n💡 Make sure the CLI is built: npm run build');
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Main function
|
|
300
|
+
async function main() {
|
|
301
|
+
console.log('🍪 LinkedIn CLI Cookie Import Tool\n');
|
|
302
|
+
|
|
303
|
+
let cookies = null;
|
|
304
|
+
|
|
305
|
+
if (browser) {
|
|
306
|
+
console.log(`📱 Extracting cookies from ${browser}...\n`);
|
|
307
|
+
|
|
308
|
+
if (!isMacOS()) {
|
|
309
|
+
console.error('❌ Direct browser extraction is currently only supported on macOS');
|
|
310
|
+
console.log('💡 On Linux, export cookies to a file first, then use --file option');
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
switch (browser) {
|
|
315
|
+
case 'chrome':
|
|
316
|
+
case 'chromium':
|
|
317
|
+
cookies = extractChromeCookies();
|
|
318
|
+
break;
|
|
319
|
+
case 'edge':
|
|
320
|
+
cookies = extractEdgeCookies();
|
|
321
|
+
break;
|
|
322
|
+
case 'safari':
|
|
323
|
+
console.error('❌ Safari cookie extraction is not supported (Safari uses encrypted cookie storage)');
|
|
324
|
+
console.log('💡 Use Chrome or Edge instead, or export cookies manually');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
case 'firefox':
|
|
327
|
+
console.error('❌ Firefox cookie extraction is not yet implemented');
|
|
328
|
+
console.log('💡 Export cookies manually using an extension');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
default:
|
|
331
|
+
console.error(`❌ Unknown browser: ${browser}`);
|
|
332
|
+
console.log('💡 Supported browsers: chrome, edge');
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
} else if (sourceFile) {
|
|
336
|
+
console.log(`📄 Importing cookies from file: ${sourceFile}\n`);
|
|
337
|
+
|
|
338
|
+
if (!fs.existsSync(sourceFile)) {
|
|
339
|
+
console.error('❌ File not found:', sourceFile);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Detect format based on extension
|
|
344
|
+
const ext = path.extname(sourceFile).toLowerCase();
|
|
345
|
+
|
|
346
|
+
if (ext === '.json') {
|
|
347
|
+
cookies = parseJSONCookies(sourceFile);
|
|
348
|
+
} else {
|
|
349
|
+
// Try Netscape format
|
|
350
|
+
cookies = parseNetscapeCookies(sourceFile);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
console.error('❌ No source specified');
|
|
354
|
+
console.log('💡 Use --file or --browser option');
|
|
355
|
+
console.log(' Run with --help for more information');
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (cookies && cookies.length > 0) {
|
|
360
|
+
const success = await importCookies(cookies);
|
|
361
|
+
process.exit(success ? 0 : 1);
|
|
362
|
+
} else {
|
|
363
|
+
console.error('\n❌ No LinkedIn cookies found');
|
|
364
|
+
console.log('\n💡 Troubleshooting:');
|
|
365
|
+
console.log(' 1. Make sure you are logged into LinkedIn in your browser');
|
|
366
|
+
console.log(' 2. Close the browser completely before extracting cookies');
|
|
367
|
+
console.log(' 3. Try using a cookie export extension instead');
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Run main
|
|
373
|
+
main().catch(error => {
|
|
374
|
+
console.error('❌ Unexpected error:', error.message);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
|
|
6
|
+
"github.com/thaddeus-git/linkedin-cli/internal/linkedin"
|
|
7
|
+
"github.com/thaddeus-git/linkedin-cli/internal/pinchtab"
|
|
8
|
+
"github.com/thaddeus-git/linkedin-cli/internal/ratelimit"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func ensureProfile() {
|
|
12
|
+
if profileName == "" {
|
|
13
|
+
exitWithError("Profile name is required (--profile or LINKEDIN_PROFILE)")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func sendConnectionAction(profileURL string, note string) {
|
|
18
|
+
cfg, err := getConfigManager()
|
|
19
|
+
if err != nil {
|
|
20
|
+
exitWithError("Failed to initialize config: %v", err)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
limiter := ratelimit.NewLimiter(profileName, ratelimit.DefaultLimits(), cfg)
|
|
24
|
+
if err := limiter.CheckConnection(); err != nil {
|
|
25
|
+
exitWithError("Rate limit check failed: %v", err)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if dryRun {
|
|
29
|
+
fmt.Println("[dry-run] Would:")
|
|
30
|
+
fmt.Printf(" - Navigate to: %s\n", profileURL)
|
|
31
|
+
fmt.Println(" - Click Connect button")
|
|
32
|
+
if note != "" {
|
|
33
|
+
fmt.Printf(" - Type note: %s\n", note)
|
|
34
|
+
}
|
|
35
|
+
fmt.Println(" - Click Send")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
client := pinchtab.NewClient(getPinchTabHost())
|
|
40
|
+
navigator := linkedin.NewNavigator(client)
|
|
41
|
+
|
|
42
|
+
fmt.Printf("Sending connection request to %s...\n", profileURL)
|
|
43
|
+
logVerbose("Using profile: %s", profileName)
|
|
44
|
+
|
|
45
|
+
logVerbose("Navigating to profile...")
|
|
46
|
+
if err := client.Navigate(profileURL); err != nil {
|
|
47
|
+
exitWithError("Failed to navigate: %v", err)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logVerbose("Clicking Connect button...")
|
|
51
|
+
if err := navigator.ClickConnect(); err != nil {
|
|
52
|
+
exitWithError("Failed to click connect: %v", err)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logVerbose("Sending connection request...")
|
|
56
|
+
if err := navigator.SendConnectionRequest(note); err != nil {
|
|
57
|
+
exitWithError("Failed to send request: %v", err)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if err := limiter.RecordConnection(); err != nil {
|
|
61
|
+
fmt.Printf("Warning: Failed to record connection: %v\n", err)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cfg.UpdateLastUsed(profileName)
|
|
65
|
+
fmt.Println("✓ Connection request sent successfully!")
|
|
66
|
+
|
|
67
|
+
delay := limiter.GetDelay()
|
|
68
|
+
logVerbose("Waiting %v before exiting...", delay)
|
|
69
|
+
limiter.Sleep()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func sendMessageAction(profileURL string, message string) {
|
|
73
|
+
cfg, err := getConfigManager()
|
|
74
|
+
if err != nil {
|
|
75
|
+
exitWithError("Failed to initialize config: %v", err)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
limiter := ratelimit.NewLimiter(profileName, ratelimit.DefaultLimits(), cfg)
|
|
79
|
+
if err := limiter.CheckMessage(); err != nil {
|
|
80
|
+
exitWithError("Rate limit check failed: %v", err)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if dryRun {
|
|
84
|
+
fmt.Println("[dry-run] Would:")
|
|
85
|
+
fmt.Printf(" - Navigate to: %s\n", profileURL)
|
|
86
|
+
fmt.Println(" - Click Message button")
|
|
87
|
+
fmt.Printf(" - Type message: %s\n", message)
|
|
88
|
+
fmt.Println(" - Click Send")
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
client := pinchtab.NewClient(getPinchTabHost())
|
|
93
|
+
navigator := linkedin.NewNavigator(client)
|
|
94
|
+
|
|
95
|
+
fmt.Printf("Sending message to %s...\n", profileURL)
|
|
96
|
+
logVerbose("Using profile: %s", profileName)
|
|
97
|
+
|
|
98
|
+
logVerbose("Navigating to profile...")
|
|
99
|
+
if err := client.Navigate(profileURL); err != nil {
|
|
100
|
+
exitWithError("Failed to navigate: %v", err)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
logVerbose("Opening message modal...")
|
|
104
|
+
if err := navigator.OpenMessageModal(); err != nil {
|
|
105
|
+
exitWithError("Failed to open message modal: %v", err)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
logVerbose("Sending message...")
|
|
109
|
+
if err := navigator.SendMessage(message); err != nil {
|
|
110
|
+
exitWithError("Failed to send message: %v", err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if err := limiter.RecordMessage(); err != nil {
|
|
114
|
+
fmt.Printf("Warning: Failed to record message: %v\n", err)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
cfg.UpdateLastUsed(profileName)
|
|
118
|
+
fmt.Println("✓ Message sent successfully!")
|
|
119
|
+
|
|
120
|
+
delay := limiter.GetDelay()
|
|
121
|
+
logVerbose("Waiting %v before exiting...", delay)
|
|
122
|
+
limiter.Sleep()
|
|
123
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"strings"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
"github.com/thaddeus-git/linkedin-cli/internal/config"
|
|
11
|
+
"github.com/thaddeus-git/linkedin-cli/internal/pinchtab"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func init() {
|
|
15
|
+
rootCmd.AddCommand(authCmd)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var authCmd = &cobra.Command{
|
|
19
|
+
Use: "auth",
|
|
20
|
+
Short: "Authenticate a LinkedIn profile",
|
|
21
|
+
Long: `Authenticate a LinkedIn profile by logging in through PinchTab.
|
|
22
|
+
|
|
23
|
+
This command will:
|
|
24
|
+
1. Start a PinchTab browser instance
|
|
25
|
+
2. Navigate to LinkedIn login page
|
|
26
|
+
3. Wait for you to log in manually
|
|
27
|
+
4. Save the session for future use
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
linkedin auth --profile john
|
|
31
|
+
`,
|
|
32
|
+
Run: runAuth,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func runAuth(cmd *cobra.Command, args []string) {
|
|
36
|
+
if profileName == "" {
|
|
37
|
+
exitWithError("Profile name is required (--profile or LINKEDIN_PROFILE)")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
cfg, err := getConfigManager()
|
|
41
|
+
if err != nil {
|
|
42
|
+
exitWithError("Failed to initialize config: %v", err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
client := pinchtab.NewClient(getPinchTabHost())
|
|
46
|
+
|
|
47
|
+
fmt.Printf("Starting authentication for profile '%s'...\n", profileName)
|
|
48
|
+
logVerbose("Using PinchTab at %s", getPinchTabHost())
|
|
49
|
+
|
|
50
|
+
if dryRun {
|
|
51
|
+
fmt.Println("[dry-run] Would:")
|
|
52
|
+
fmt.Println(" - Navigate to https://linkedin.com/login")
|
|
53
|
+
fmt.Println(" - Wait for manual login")
|
|
54
|
+
fmt.Println(" - Save profile configuration")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fmt.Println("Navigating to LinkedIn login...")
|
|
59
|
+
if err := client.Navigate("https://linkedin.com/login"); err != nil {
|
|
60
|
+
exitWithError("Failed to navigate to LinkedIn: %v", err)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fmt.Print("\n========================================\n")
|
|
64
|
+
fmt.Println("A browser window should be visible.")
|
|
65
|
+
fmt.Println("Please log in to LinkedIn manually.")
|
|
66
|
+
fmt.Println("Press Enter when you're logged in...")
|
|
67
|
+
fmt.Print("========================================\n")
|
|
68
|
+
|
|
69
|
+
os.Stdin.Read(make([]byte, 1))
|
|
70
|
+
|
|
71
|
+
fmt.Println("\nVerifying login...")
|
|
72
|
+
text, err := client.GetText()
|
|
73
|
+
if err != nil {
|
|
74
|
+
exitWithError("Failed to get page text: %v", err)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if !containsAny(text.Text, []string{"Me", "Messaging", "Notifications", "My Network"}) {
|
|
78
|
+
fmt.Println("Warning: Could not verify login. Session may not be saved.")
|
|
79
|
+
fmt.Println("Please try logging in again.")
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
profile := &config.Profile{
|
|
84
|
+
Name: profileName,
|
|
85
|
+
CreatedAt: time.Now(),
|
|
86
|
+
LastUsed: time.Now(),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if err := cfg.SaveProfile(profile); err != nil {
|
|
90
|
+
exitWithError("Failed to save profile: %v", err)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fmt.Printf("\n✓ Profile '%s' authenticated successfully!\n", profileName)
|
|
94
|
+
fmt.Println("You can now use this profile for automation.")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func containsAny(text string, substrs []string) bool {
|
|
98
|
+
for _, substr := range substrs {
|
|
99
|
+
if contains(text, substr) {
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func contains(text, substr string) bool {
|
|
107
|
+
return strings.Contains(text, substr)
|
|
108
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"github.com/spf13/cobra"
|
|
5
|
+
"github.com/thaddeus-git/linkedin-cli/internal/linkedin"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
var (
|
|
9
|
+
connectURL string
|
|
10
|
+
connectNote string
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func init() {
|
|
14
|
+
rootCmd.AddCommand(connectCmd)
|
|
15
|
+
connectCmd.Flags().StringVarP(&connectURL, "url", "u", "", "LinkedIn profile URL (required)")
|
|
16
|
+
connectCmd.Flags().StringVarP(&connectNote, "message", "m", "", "Connection request note (optional)")
|
|
17
|
+
connectCmd.MarkFlagRequired("url")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var connectCmd = &cobra.Command{
|
|
21
|
+
Use: "connect",
|
|
22
|
+
Short: "Send a connection request",
|
|
23
|
+
Long: `Send a connection request to a LinkedIn profile.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
linkedin connect --profile john --url linkedin.com/in/alice
|
|
27
|
+
linkedin connect --profile john --url linkedin.com/in/alice --message "Hi Alice, loved your post!"
|
|
28
|
+
`,
|
|
29
|
+
Run: runConnect,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func runConnect(cmd *cobra.Command, args []string) {
|
|
33
|
+
ensureProfile()
|
|
34
|
+
|
|
35
|
+
// Validate URL
|
|
36
|
+
profileURL, err := linkedin.ValidateProfileURL(connectURL)
|
|
37
|
+
if err != nil {
|
|
38
|
+
exitWithError("Invalid URL: %v", err)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
sendConnectionAction(profileURL, connectNote)
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"github.com/spf13/cobra"
|
|
5
|
+
"github.com/thaddeus-git/linkedin-cli/internal/linkedin"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
var (
|
|
9
|
+
messageURL string
|
|
10
|
+
messageContent string
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func init() {
|
|
14
|
+
rootCmd.AddCommand(messageCmd)
|
|
15
|
+
messageCmd.Flags().StringVarP(&messageURL, "url", "u", "", "LinkedIn profile URL (required)")
|
|
16
|
+
messageCmd.Flags().StringVarP(&messageContent, "message", "m", "", "Message content (required)")
|
|
17
|
+
messageCmd.MarkFlagRequired("url")
|
|
18
|
+
messageCmd.MarkFlagRequired("message")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var messageCmd = &cobra.Command{
|
|
22
|
+
Use: "message",
|
|
23
|
+
Short: "Send a direct message",
|
|
24
|
+
Long: `Send a direct message to a LinkedIn connection.
|
|
25
|
+
|
|
26
|
+
You must already be connected with the recipient.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
linkedin message --profile john --url linkedin.com/in/alice --message "Thanks for connecting!"
|
|
30
|
+
`,
|
|
31
|
+
Run: runMessage,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func runMessage(cmd *cobra.Command, args []string) {
|
|
35
|
+
ensureProfile()
|
|
36
|
+
|
|
37
|
+
// Validate URL
|
|
38
|
+
profileURL, err := linkedin.ValidateProfileURL(messageURL)
|
|
39
|
+
if err != nil {
|
|
40
|
+
exitWithError("Invalid URL: %v", err)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
sendMessageAction(profileURL, messageContent)
|
|
44
|
+
}
|