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.
Files changed (314) hide show
  1. package/.env.example +12 -0
  2. package/.github/workflows/ci.yml +66 -0
  3. package/.github/workflows/publish.yml +48 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +10 -0
  7. package/AGENTS.md +294 -0
  8. package/CHANGELOG.md +40 -0
  9. package/GIT_RELEASE.md +167 -0
  10. package/LICENSE +21 -0
  11. package/Makefile +30 -0
  12. package/NPM_PUBLISHING.md +230 -0
  13. package/PYEOF +0 -0
  14. package/README.md +295 -0
  15. package/TESTING-GUIDE.md +151 -0
  16. package/cmd/linkedin/main.go +9 -0
  17. package/dist/agent/action-executor.d.ts +81 -0
  18. package/dist/agent/action-executor.d.ts.map +1 -0
  19. package/dist/agent/action-executor.js +170 -0
  20. package/dist/agent/action-executor.js.map +1 -0
  21. package/dist/agent/action-executor.test.d.ts +2 -0
  22. package/dist/agent/action-executor.test.d.ts.map +1 -0
  23. package/dist/agent/action-executor.test.js +366 -0
  24. package/dist/agent/action-executor.test.js.map +1 -0
  25. package/dist/agent/claude-client.d.ts +74 -0
  26. package/dist/agent/claude-client.d.ts.map +1 -0
  27. package/dist/agent/claude-client.js +314 -0
  28. package/dist/agent/claude-client.js.map +1 -0
  29. package/dist/agent/claude-client.test.d.ts +2 -0
  30. package/dist/agent/claude-client.test.d.ts.map +1 -0
  31. package/dist/agent/claude-client.test.js +590 -0
  32. package/dist/agent/claude-client.test.js.map +1 -0
  33. package/dist/agent/dom-extractor.d.ts +50 -0
  34. package/dist/agent/dom-extractor.d.ts.map +1 -0
  35. package/dist/agent/dom-extractor.js +374 -0
  36. package/dist/agent/dom-extractor.js.map +1 -0
  37. package/dist/agent/dom-extractor.test.d.ts +7 -0
  38. package/dist/agent/dom-extractor.test.d.ts.map +1 -0
  39. package/dist/agent/dom-extractor.test.js +504 -0
  40. package/dist/agent/dom-extractor.test.js.map +1 -0
  41. package/dist/agent/extension-client.d.ts +75 -0
  42. package/dist/agent/extension-client.d.ts.map +1 -0
  43. package/dist/agent/extension-client.js +245 -0
  44. package/dist/agent/extension-client.js.map +1 -0
  45. package/dist/agent/index.d.ts +8 -0
  46. package/dist/agent/index.d.ts.map +1 -0
  47. package/dist/agent/index.js +16 -0
  48. package/dist/agent/index.js.map +1 -0
  49. package/dist/agent/page-agent.d.ts +76 -0
  50. package/dist/agent/page-agent.d.ts.map +1 -0
  51. package/dist/agent/page-agent.js +236 -0
  52. package/dist/agent/page-agent.js.map +1 -0
  53. package/dist/agent/types.d.ts +236 -0
  54. package/dist/agent/types.d.ts.map +1 -0
  55. package/dist/agent/types.js +37 -0
  56. package/dist/agent/types.js.map +1 -0
  57. package/dist/cli/agent-commands.d.ts +3 -0
  58. package/dist/cli/agent-commands.d.ts.map +1 -0
  59. package/dist/cli/agent-commands.js +250 -0
  60. package/dist/cli/agent-commands.js.map +1 -0
  61. package/dist/cli/auth.d.ts +3 -0
  62. package/dist/cli/auth.d.ts.map +1 -0
  63. package/dist/cli/auth.js +288 -0
  64. package/dist/cli/auth.js.map +1 -0
  65. package/dist/cli/company.d.ts +3 -0
  66. package/dist/cli/company.d.ts.map +1 -0
  67. package/dist/cli/company.js +55 -0
  68. package/dist/cli/company.js.map +1 -0
  69. package/dist/cli/connection.d.ts +3 -0
  70. package/dist/cli/connection.d.ts.map +1 -0
  71. package/dist/cli/connection.js +79 -0
  72. package/dist/cli/connection.js.map +1 -0
  73. package/dist/cli/index.d.ts +7 -0
  74. package/dist/cli/index.d.ts.map +1 -0
  75. package/dist/cli/index.js +17 -0
  76. package/dist/cli/index.js.map +1 -0
  77. package/dist/cli/messages.d.ts +3 -0
  78. package/dist/cli/messages.d.ts.map +1 -0
  79. package/dist/cli/messages.js +268 -0
  80. package/dist/cli/messages.js.map +1 -0
  81. package/dist/cli/profile.d.ts +3 -0
  82. package/dist/cli/profile.d.ts.map +1 -0
  83. package/dist/cli/profile.js +81 -0
  84. package/dist/cli/profile.js.map +1 -0
  85. package/dist/cli/profile.test.d.ts +2 -0
  86. package/dist/cli/profile.test.d.ts.map +1 -0
  87. package/dist/cli/profile.test.js +15 -0
  88. package/dist/cli/profile.test.js.map +1 -0
  89. package/dist/cli/reply.d.ts +3 -0
  90. package/dist/cli/reply.d.ts.map +1 -0
  91. package/dist/cli/reply.js +129 -0
  92. package/dist/cli/reply.js.map +1 -0
  93. package/dist/core/audit.d.ts +17 -0
  94. package/dist/core/audit.d.ts.map +1 -0
  95. package/dist/core/audit.js +121 -0
  96. package/dist/core/audit.js.map +1 -0
  97. package/dist/core/audit.test.d.ts +2 -0
  98. package/dist/core/audit.test.d.ts.map +1 -0
  99. package/dist/core/audit.test.js +142 -0
  100. package/dist/core/audit.test.js.map +1 -0
  101. package/dist/core/browser-cookies.d.ts +19 -0
  102. package/dist/core/browser-cookies.d.ts.map +1 -0
  103. package/dist/core/browser-cookies.js +181 -0
  104. package/dist/core/browser-cookies.js.map +1 -0
  105. package/dist/core/browser.d.ts +50 -0
  106. package/dist/core/browser.d.ts.map +1 -0
  107. package/dist/core/browser.js +318 -0
  108. package/dist/core/browser.js.map +1 -0
  109. package/dist/core/config.d.ts +20 -0
  110. package/dist/core/config.d.ts.map +1 -0
  111. package/dist/core/config.js +103 -0
  112. package/dist/core/config.js.map +1 -0
  113. package/dist/core/config.test.d.ts +2 -0
  114. package/dist/core/config.test.d.ts.map +1 -0
  115. package/dist/core/config.test.js +111 -0
  116. package/dist/core/config.test.js.map +1 -0
  117. package/dist/core/storage.d.ts +19 -0
  118. package/dist/core/storage.d.ts.map +1 -0
  119. package/dist/core/storage.js +124 -0
  120. package/dist/core/storage.js.map +1 -0
  121. package/dist/core/storage.test.d.ts +2 -0
  122. package/dist/core/storage.test.d.ts.map +1 -0
  123. package/dist/core/storage.test.js +142 -0
  124. package/dist/core/storage.test.js.map +1 -0
  125. package/dist/index.d.ts +3 -0
  126. package/dist/index.d.ts.map +1 -0
  127. package/dist/index.js +63 -0
  128. package/dist/index.js.map +1 -0
  129. package/dist/linkedin/auth.d.ts +22 -0
  130. package/dist/linkedin/auth.d.ts.map +1 -0
  131. package/dist/linkedin/auth.js +167 -0
  132. package/dist/linkedin/auth.js.map +1 -0
  133. package/dist/linkedin/company-extractor.d.ts +36 -0
  134. package/dist/linkedin/company-extractor.d.ts.map +1 -0
  135. package/dist/linkedin/company-extractor.js +211 -0
  136. package/dist/linkedin/company-extractor.js.map +1 -0
  137. package/dist/linkedin/company-extractor.test.d.ts +2 -0
  138. package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
  139. package/dist/linkedin/company-extractor.test.js +52 -0
  140. package/dist/linkedin/company-extractor.test.js.map +1 -0
  141. package/dist/linkedin/connector.d.ts +45 -0
  142. package/dist/linkedin/connector.d.ts.map +1 -0
  143. package/dist/linkedin/connector.js +245 -0
  144. package/dist/linkedin/connector.js.map +1 -0
  145. package/dist/linkedin/message-sender.d.ts +32 -0
  146. package/dist/linkedin/message-sender.d.ts.map +1 -0
  147. package/dist/linkedin/message-sender.js +112 -0
  148. package/dist/linkedin/message-sender.js.map +1 -0
  149. package/dist/linkedin/messages.d.ts +78 -0
  150. package/dist/linkedin/messages.d.ts.map +1 -0
  151. package/dist/linkedin/messages.js +745 -0
  152. package/dist/linkedin/messages.js.map +1 -0
  153. package/dist/linkedin/profile.d.ts +37 -0
  154. package/dist/linkedin/profile.d.ts.map +1 -0
  155. package/dist/linkedin/profile.js +268 -0
  156. package/dist/linkedin/profile.js.map +1 -0
  157. package/dist/linkedin/profile.test.d.ts +2 -0
  158. package/dist/linkedin/profile.test.d.ts.map +1 -0
  159. package/dist/linkedin/profile.test.js +68 -0
  160. package/dist/linkedin/profile.test.js.map +1 -0
  161. package/dist/linkedin/reply.d.ts +21 -0
  162. package/dist/linkedin/reply.d.ts.map +1 -0
  163. package/dist/linkedin/reply.js +76 -0
  164. package/dist/linkedin/reply.js.map +1 -0
  165. package/dist/linkedin/selector-engine.d.ts +69 -0
  166. package/dist/linkedin/selector-engine.d.ts.map +1 -0
  167. package/dist/linkedin/selector-engine.js +339 -0
  168. package/dist/linkedin/selector-engine.js.map +1 -0
  169. package/dist/linkedin/selector-engine.test.d.ts +2 -0
  170. package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
  171. package/dist/linkedin/selector-engine.test.js +135 -0
  172. package/dist/linkedin/selector-engine.test.js.map +1 -0
  173. package/dist/linkedin/selectors.d.ts +65 -0
  174. package/dist/linkedin/selectors.d.ts.map +1 -0
  175. package/dist/linkedin/selectors.js +261 -0
  176. package/dist/linkedin/selectors.js.map +1 -0
  177. package/dist/templates/engine.d.ts +37 -0
  178. package/dist/templates/engine.d.ts.map +1 -0
  179. package/dist/templates/engine.js +215 -0
  180. package/dist/templates/engine.js.map +1 -0
  181. package/dist/templates/engine.test.d.ts +2 -0
  182. package/dist/templates/engine.test.d.ts.map +1 -0
  183. package/dist/templates/engine.test.js +212 -0
  184. package/dist/templates/engine.test.js.map +1 -0
  185. package/dist/templates/index.d.ts +2 -0
  186. package/dist/templates/index.d.ts.map +1 -0
  187. package/dist/templates/index.js +7 -0
  188. package/dist/templates/index.js.map +1 -0
  189. package/dist/types/index.d.ts +113 -0
  190. package/dist/types/index.d.ts.map +1 -0
  191. package/dist/types/index.js +3 -0
  192. package/dist/types/index.js.map +1 -0
  193. package/dist/types/index.test.d.ts +2 -0
  194. package/dist/types/index.test.d.ts.map +1 -0
  195. package/dist/types/index.test.js +90 -0
  196. package/dist/types/index.test.js.map +1 -0
  197. package/dist/utils/paths.d.ts +8 -0
  198. package/dist/utils/paths.d.ts.map +1 -0
  199. package/dist/utils/paths.js +68 -0
  200. package/dist/utils/paths.js.map +1 -0
  201. package/dist/utils/rate-limiter.d.ts +22 -0
  202. package/dist/utils/rate-limiter.d.ts.map +1 -0
  203. package/dist/utils/rate-limiter.js +57 -0
  204. package/dist/utils/rate-limiter.js.map +1 -0
  205. package/dist/utils/retry.d.ts +18 -0
  206. package/dist/utils/retry.d.ts.map +1 -0
  207. package/dist/utils/retry.js +49 -0
  208. package/dist/utils/retry.js.map +1 -0
  209. package/docs/connection-command.md +52 -0
  210. package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
  211. package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
  212. package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
  213. package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
  214. package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
  215. package/docs/plans/2026-02-28-messages-show-design.md +243 -0
  216. package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
  217. package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
  218. package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
  219. package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
  220. package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
  221. package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
  222. package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
  223. package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
  224. package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
  225. package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
  226. package/eslint.config.mjs +58 -0
  227. package/go.mod +9 -0
  228. package/go.sum +10 -0
  229. package/import-cookies.js +376 -0
  230. package/internal/cmd/actions.go +123 -0
  231. package/internal/cmd/auth.go +108 -0
  232. package/internal/cmd/connect.go +42 -0
  233. package/internal/cmd/message.go +44 -0
  234. package/internal/cmd/people.go +454 -0
  235. package/internal/cmd/profiles.go +121 -0
  236. package/internal/cmd/root.go +89 -0
  237. package/internal/cmd/sequence.go +192 -0
  238. package/internal/config/config.go +187 -0
  239. package/internal/config/config_test.go +121 -0
  240. package/internal/config/profile.go +65 -0
  241. package/internal/linkedin/navigator.go +195 -0
  242. package/internal/linkedin/selectors.go +39 -0
  243. package/internal/linkedin/validator.go +69 -0
  244. package/internal/pinchtab/client.go +183 -0
  245. package/internal/pinchtab/client_test.go +67 -0
  246. package/internal/pinchtab/types.go +50 -0
  247. package/internal/ratelimit/limiter.go +115 -0
  248. package/internal/ratelimit/limits.go +32 -0
  249. package/package.json +67 -0
  250. package/release.sh +66 -0
  251. package/scripts/debug-linkedin.js +156 -0
  252. package/scripts/debug-login.js +193 -0
  253. package/scripts/extract-from-edge.js +96 -0
  254. package/scripts/import-cookies.js +101 -0
  255. package/scripts/poc-show-data.js +205 -0
  256. package/scripts/proof-of-access.js +87 -0
  257. package/scripts/prove-connection.js +110 -0
  258. package/scripts/show-linkedin-data.js +173 -0
  259. package/src/agent/action-executor.test.ts +464 -0
  260. package/src/agent/action-executor.ts +203 -0
  261. package/src/agent/claude-client.test.ts +707 -0
  262. package/src/agent/claude-client.ts +422 -0
  263. package/src/agent/dom-extractor.test.ts +574 -0
  264. package/src/agent/dom-extractor.ts +437 -0
  265. package/src/agent/extension-client.ts +306 -0
  266. package/src/agent/index.ts +28 -0
  267. package/src/agent/page-agent.ts +292 -0
  268. package/src/agent/types.ts +288 -0
  269. package/src/cli/agent-commands.ts +274 -0
  270. package/src/cli/auth.ts +343 -0
  271. package/src/cli/company.ts +66 -0
  272. package/src/cli/connection.ts +89 -0
  273. package/src/cli/index.ts +7 -0
  274. package/src/cli/messages.ts +338 -0
  275. package/src/cli/profile.test.ts +14 -0
  276. package/src/cli/profile.ts +95 -0
  277. package/src/cli/reply.ts +110 -0
  278. package/src/core/audit.test.ts +134 -0
  279. package/src/core/audit.ts +98 -0
  280. package/src/core/browser-cookies.ts +203 -0
  281. package/src/core/browser.ts +304 -0
  282. package/src/core/config.test.ts +90 -0
  283. package/src/core/config.ts +81 -0
  284. package/src/core/storage.test.ts +129 -0
  285. package/src/core/storage.ts +100 -0
  286. package/src/index.ts +70 -0
  287. package/src/linkedin/auth.ts +218 -0
  288. package/src/linkedin/company-extractor.test.ts +58 -0
  289. package/src/linkedin/company-extractor.ts +222 -0
  290. package/src/linkedin/connector.ts +336 -0
  291. package/src/linkedin/message-sender.ts +141 -0
  292. package/src/linkedin/messages.ts +894 -0
  293. package/src/linkedin/profile.test.ts +79 -0
  294. package/src/linkedin/profile.ts +314 -0
  295. package/src/linkedin/reply.ts +96 -0
  296. package/src/linkedin/selector-engine.test.ts +167 -0
  297. package/src/linkedin/selector-engine.ts +393 -0
  298. package/src/linkedin/selectors.ts +268 -0
  299. package/src/templates/defaults/followup.txt +14 -0
  300. package/src/templates/defaults/meeting.txt +16 -0
  301. package/src/templates/defaults/welcome.txt +14 -0
  302. package/src/templates/engine.test.ts +228 -0
  303. package/src/templates/engine.ts +208 -0
  304. package/src/templates/index.ts +1 -0
  305. package/src/types/index.test.ts +94 -0
  306. package/src/types/index.ts +143 -0
  307. package/src/types/sql.js.d.ts +23 -0
  308. package/src/utils/paths.ts +33 -0
  309. package/src/utils/rate-limiter.ts +75 -0
  310. package/src/utils/retry.ts +78 -0
  311. package/test-cli.sh +85 -0
  312. package/test-real-data.sh +97 -0
  313. package/tsconfig.json +23 -0
  314. 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
+ }