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,343 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { BrowserController } from '../core/browser';
5
+ import { LinkedInAuth } from '../linkedin/auth';
6
+ import { getSecureStorage } from '../core/storage';
7
+ import { getAuditLogger } from '../core/audit';
8
+ import { getConfig } from '../core/config';
9
+ import { getLinkedinCookies } from '../core/browser-cookies';
10
+
11
+ const SESSION_KEY = 'linkedin-session';
12
+
13
+ export function registerAuthCommands(program: Command): void {
14
+ const auth = program.command('auth').description('Authentication commands');
15
+
16
+ auth
17
+ .command('login')
18
+ .description('Log in to LinkedIn')
19
+ .option('-e, --email <email>', 'LinkedIn email')
20
+ .option('--headless', 'Run browser in headless mode', false)
21
+ .option('--debug', 'Enable debug mode with screenshots and verbose logging', false)
22
+ .action(async (options) => {
23
+ const storage = getSecureStorage();
24
+ const logger = getAuditLogger();
25
+ const config = getConfig();
26
+
27
+ try {
28
+ // Get email if not provided
29
+ let email = options.email;
30
+ if (!email) {
31
+ const answer = await inquirer.prompt([
32
+ {
33
+ type: 'input',
34
+ name: 'email',
35
+ message: 'LinkedIn email:',
36
+ validate: (input) => input.includes('@') || 'Please enter a valid email',
37
+ },
38
+ ]);
39
+ email = answer.email;
40
+ }
41
+
42
+ // Get password securely
43
+ const { password } = await inquirer.prompt([
44
+ {
45
+ type: 'password',
46
+ name: 'password',
47
+ message: 'LinkedIn password:',
48
+ mask: '*',
49
+ },
50
+ ]);
51
+
52
+ console.log(chalk.blue('Launching browser...'));
53
+
54
+ // Launch browser with config
55
+ const browser = new BrowserController({
56
+ headless: options.headless ?? config.getValue('headless'),
57
+ debug: options.debug ?? false,
58
+ });
59
+
60
+ // Check for existing session
61
+ if (storage.exists(SESSION_KEY)) {
62
+ const sessionData = storage.load(SESSION_KEY);
63
+ if (sessionData) {
64
+ console.log(chalk.blue('Found existing session, attempting to restore...'));
65
+ await browser.launch();
66
+ const session = JSON.parse(sessionData);
67
+ await browser.restoreSession(session);
68
+
69
+ // Verify session is still valid
70
+ const linkedInAuth = new LinkedInAuth(browser, options.debug ?? false);
71
+ if (await linkedInAuth.isSessionValid()) {
72
+ console.log(chalk.green('✓ Already logged in (restored session)'));
73
+ logger.log('auth.login', { method: 'restored_session' }, true);
74
+ await browser.close().catch(() => {});
75
+ return;
76
+ }
77
+ console.log(chalk.yellow('⚠ Session expired, logging in again...'));
78
+ // Browser is still open, continue to fresh login
79
+ }
80
+ }
81
+
82
+ // Fresh login (browser may already be launched from session check above)
83
+ if (!browser.isLaunched()) {
84
+ await browser.launch();
85
+ }
86
+ const linkedInAuth = new LinkedInAuth(browser, options.debug ?? false);
87
+
88
+ console.log(chalk.blue('Logging in to LinkedIn...'));
89
+
90
+ const result = await linkedInAuth.login({
91
+ email,
92
+ password,
93
+ });
94
+
95
+ if (result.requires2FA) {
96
+ // Prompt for TOTP code
97
+ const { totpCode } = await inquirer.prompt([
98
+ {
99
+ type: 'input',
100
+ name: 'totpCode',
101
+ message: 'Enter TOTP code from Microsoft Authenticator:',
102
+ validate: (input) => /^\d{6}$/.test(input) || 'Please enter a 6-digit code',
103
+ },
104
+ ]);
105
+
106
+ console.log(chalk.blue('Verifying 2FA code...'));
107
+
108
+ // Retry login with TOTP
109
+ const result2FA = await linkedInAuth.login({
110
+ email,
111
+ password,
112
+ totpCode,
113
+ });
114
+
115
+ if (!result2FA.success) {
116
+ console.error(chalk.red('✗ Login failed:'), result2FA.error);
117
+ logger.log('auth.login', { email, error: result2FA.error }, false);
118
+ await browser.close();
119
+ process.exit(1);
120
+ }
121
+
122
+ // Save session
123
+ if (result2FA.session) {
124
+ storage.save(SESSION_KEY, JSON.stringify(result2FA.session));
125
+ }
126
+
127
+ console.log(chalk.green('✓ Login successful!'));
128
+ logger.log('auth.login', { email, method: '2fa' }, true);
129
+ await browser.close().catch(() => {});
130
+ } else if (!result.success) {
131
+ console.error(chalk.red('✗ Login failed:'), result.error);
132
+ logger.log('auth.login', { email, error: result.error }, false);
133
+ await browser.close();
134
+ process.exit(1);
135
+ } else {
136
+ // Success without 2FA
137
+ if (result.session) {
138
+ storage.save(SESSION_KEY, JSON.stringify(result.session));
139
+ }
140
+
141
+ console.log(chalk.green('✓ Login successful!'));
142
+ logger.log('auth.login', { email }, true);
143
+ }
144
+
145
+ await browser.close().catch(() => {});
146
+ } catch (error) {
147
+ console.error(chalk.red('✗ Login failed:'), error instanceof Error ? error.message : error);
148
+ process.exit(1);
149
+ }
150
+ });
151
+
152
+ auth
153
+ .command('logout')
154
+ .description('Log out and clear session')
155
+ .action(async () => {
156
+ const storage = getSecureStorage();
157
+ const logger = getAuditLogger();
158
+
159
+ try {
160
+ console.log(chalk.blue('Logging out...'));
161
+
162
+ // Clear the session from secure storage
163
+ const hadSession = storage.delete(SESSION_KEY);
164
+
165
+ if (hadSession) {
166
+ console.log(chalk.green('✓ Logged out successfully'));
167
+ logger.log('auth.logout', {}, true);
168
+ } else {
169
+ console.log(chalk.yellow('⚠ No active session found'));
170
+ }
171
+ } catch (error) {
172
+ console.error(
173
+ chalk.red('✗ Logout failed:'),
174
+ error instanceof Error ? error.message : error
175
+ );
176
+ logger.log('auth.logout', { error: String(error) }, false);
177
+ process.exit(1);
178
+ }
179
+ });
180
+
181
+ auth
182
+ .command('status')
183
+ .description('Check authentication status')
184
+ .action(async () => {
185
+ const storage = getSecureStorage();
186
+ const config = getConfig();
187
+ let browser: BrowserController | null = null;
188
+
189
+ try {
190
+ console.log(chalk.blue('Checking authentication status...'));
191
+
192
+ // First try to connect to existing browser via CDP
193
+ console.log(chalk.blue('Checking for existing browser (Chrome/Edge)...'));
194
+ browser = new BrowserController({
195
+ headless: config.getValue('headless'),
196
+ });
197
+
198
+ try {
199
+ await browser.launch();
200
+ const linkedInAuth = new LinkedInAuth(browser);
201
+ if (await linkedInAuth.isSessionValid()) {
202
+ console.log(chalk.green('✓ Logged in (via existing browser session)'));
203
+ await browser.close();
204
+ return;
205
+ }
206
+ await browser.close();
207
+ browser = null;
208
+ } catch (cdpError) {
209
+ console.log(chalk.gray('Could not connect to existing browser'));
210
+ if (browser) {
211
+ await browser.close().catch(() => {});
212
+ browser = null;
213
+ }
214
+ }
215
+
216
+ // Check if session exists in storage
217
+ if (!storage.exists(SESSION_KEY)) {
218
+ console.log(chalk.yellow('⚠ Not logged in'));
219
+ console.log(chalk.gray('Run: linkedin-cli auth login'));
220
+ return;
221
+ }
222
+
223
+ // Session exists, try to restore and verify
224
+ const sessionData = storage.load(SESSION_KEY);
225
+ if (!sessionData) {
226
+ console.log(chalk.yellow('⚠ Session data corrupted'));
227
+ console.log(chalk.gray('Run: linkedin-cli auth login'));
228
+ return;
229
+ }
230
+
231
+ console.log(chalk.blue('Found saved session, verifying...'));
232
+
233
+ // Launch browser to verify session
234
+ browser = new BrowserController({
235
+ headless: config.getValue('headless'),
236
+ });
237
+
238
+ await browser.launch();
239
+
240
+ const session = JSON.parse(sessionData);
241
+ await browser.restoreSession(session);
242
+
243
+ const linkedInAuth = new LinkedInAuth(browser);
244
+ const isValid = await linkedInAuth.isSessionValid();
245
+
246
+ if (isValid) {
247
+ const sessionAge = new Date().getTime() - new Date(session.timestamp).getTime();
248
+ const hoursAgo = Math.floor(sessionAge / (1000 * 60 * 60));
249
+ const daysAgo = Math.floor(sessionAge / (1000 * 60 * 60 * 24));
250
+
251
+ console.log(chalk.green('✓ Logged in'));
252
+
253
+ if (daysAgo > 0) {
254
+ console.log(
255
+ chalk.gray(`Session created ${daysAgo} day${daysAgo !== 1 ? 's' : ''} ago`)
256
+ );
257
+ } else if (hoursAgo > 0) {
258
+ console.log(
259
+ chalk.gray(`Session created ${hoursAgo} hour${hoursAgo !== 1 ? 's' : ''} ago`)
260
+ );
261
+ } else {
262
+ console.log(chalk.gray('Session created recently'));
263
+ }
264
+ } else {
265
+ console.log(chalk.yellow('⚠ Session expired'));
266
+ console.log(chalk.gray('Run: linkedin-cli auth login'));
267
+ }
268
+
269
+ await browser.close();
270
+ browser = null;
271
+ } catch (error) {
272
+ if (browser) {
273
+ await browser.close().catch(() => {});
274
+ }
275
+ console.error(
276
+ chalk.red('✗ Error checking status:'),
277
+ error instanceof Error ? error.message : error
278
+ );
279
+ process.exit(1);
280
+ }
281
+ });
282
+
283
+ auth
284
+ .command('login-from-browser <browser>')
285
+ .description('Import LinkedIn session from browser (chrome, edge, arc, brave)')
286
+ .action(async (browserName) => {
287
+ const storage = getSecureStorage();
288
+ const logger = getAuditLogger();
289
+
290
+ try {
291
+ console.log(chalk.blue(`Extracting LinkedIn cookies from ${browserName}...`));
292
+ console.log(chalk.gray('macOS will show a permission dialog to access browser data.'));
293
+
294
+ // Extract LinkedIn cookies from browser
295
+ const cookies = await getLinkedinCookies(browserName);
296
+
297
+ if (cookies.length === 0) {
298
+ console.error(
299
+ chalk.red('✗ No LinkedIn cookies found.'),
300
+ chalk.gray('Make sure you are logged into LinkedIn in', browserName)
301
+ );
302
+ process.exit(1);
303
+ }
304
+
305
+ console.log(chalk.green(`✓ Found ${cookies.length} LinkedIn cookies`));
306
+
307
+ // Save cookies to session storage
308
+ const session = {
309
+ cookies,
310
+ localStorage: {},
311
+ timestamp: new Date(),
312
+ };
313
+
314
+ storage.save(SESSION_KEY, JSON.stringify(session));
315
+
316
+ console.log(chalk.green('✓ Session imported successfully'));
317
+ logger.log(
318
+ 'auth.login-from-browser',
319
+ { browser: browserName, cookieCount: cookies.length },
320
+ true
321
+ );
322
+
323
+ console.log(chalk.yellow('⚠ Note: Chrome encrypts cookies at rest.'));
324
+ console.log(chalk.gray(' For best results, keep the browser open while using the CLI.'));
325
+ console.log(chalk.blue(''));
326
+ console.log(chalk.blue('Next steps:'));
327
+ console.log(chalk.gray(' 1. Close any browser windows completely'));
328
+ console.log(chalk.gray(' 2. Run: linkedin-cli messages list'));
329
+ console.log(chalk.gray(' 3. The CLI will connect to your browser via CDP'));
330
+ } catch (error: any) {
331
+ console.error(
332
+ chalk.red('✗ Failed to import session:'),
333
+ error instanceof Error ? error.message : error
334
+ );
335
+ logger.log(
336
+ 'auth.login-from-browser',
337
+ { browser: browserName, error: String(error) },
338
+ false
339
+ );
340
+ process.exit(1);
341
+ }
342
+ });
343
+ }
@@ -0,0 +1,66 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { CompanyExtractor, isValidCompanyUrl } from '../linkedin/company-extractor';
4
+ import { createBrowser } from '../core/browser';
5
+ import { getAuditLogger } from '../core/audit';
6
+
7
+ export function registerCompanyCommands(program: Command): void {
8
+ const company = program
9
+ .command('company')
10
+ .description('Extract information from LinkedIn company profiles');
11
+
12
+ company
13
+ .command('info')
14
+ .description('Extract company profile information')
15
+ .argument('<url>', 'LinkedIn company URL (e.g., https://www.linkedin.com/company/openai/)')
16
+ .action(async (url) => {
17
+ const logger = getAuditLogger();
18
+ let browserController;
19
+
20
+ try {
21
+ // Validate URL
22
+ if (!isValidCompanyUrl(url)) {
23
+ console.error(chalk.red('✗ Invalid company URL'));
24
+ console.log(chalk.gray('Expected format: https://www.linkedin.com/company/<name>/'));
25
+ process.exit(1);
26
+ }
27
+
28
+ console.error(chalk.gray(`Extracting company info from ${url}...`));
29
+
30
+ // Connect to existing browser via CDP (uses authenticated session)
31
+ browserController = await createBrowser();
32
+ const page = browserController.getPage();
33
+
34
+ if (!page) {
35
+ throw new Error('Failed to get page from browser');
36
+ }
37
+
38
+ // Extract company profile
39
+ const extractor = new CompanyExtractor(page);
40
+ const profile = await extractor.extract(url);
41
+
42
+ // Log successful extraction
43
+ const fieldsExtracted = Object.keys(profile).filter(
44
+ (k) => profile[k as keyof typeof profile] !== null
45
+ );
46
+ logger.log('company.info', { url, fieldsExtracted }, true);
47
+
48
+ // Output JSON to stdout
49
+ console.log(JSON.stringify(profile, null, 2));
50
+
51
+ await browserController.close();
52
+ } catch (error) {
53
+ if (browserController) await browserController.close();
54
+ logger.log(
55
+ 'company.info',
56
+ { url, error: error instanceof Error ? error.message : String(error) },
57
+ false
58
+ );
59
+ console.error(
60
+ chalk.red('✗ Failed to extract company data:'),
61
+ error instanceof Error ? error.message : error
62
+ );
63
+ process.exit(1);
64
+ }
65
+ });
66
+ }
@@ -0,0 +1,89 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { BrowserController } from '../core/browser';
4
+ import { LinkedInConnector } from '../linkedin/connector';
5
+ import { LinkedInAuth } from '../linkedin/auth';
6
+ import { getAuditLogger } from '../core/audit';
7
+ import { getConfig } from '../core/config';
8
+
9
+ export function registerConnectionCommands(program: Command): void {
10
+ const connection = program
11
+ .command('connect')
12
+ .description('Send connection requests to LinkedIn profiles');
13
+
14
+ connection
15
+ .command('send')
16
+ .description('Send a connection request to a profile')
17
+ .argument(
18
+ '<profile-url>',
19
+ 'LinkedIn profile URL (e.g., https://www.linkedin.com/in/williamhgates/)'
20
+ )
21
+ .option(
22
+ '-n, --note <note>',
23
+ 'Personalized note to include with connection request (max 300 characters)'
24
+ )
25
+ .option('--headless', 'Run browser in headless mode', false)
26
+ .action(async (profileUrl, options) => {
27
+ const logger = getAuditLogger();
28
+ const config = getConfig();
29
+ let browser: BrowserController | null = null;
30
+
31
+ try {
32
+ // Validate profile URL
33
+ if (!profileUrl.match(/^https?:\/\/www\.linkedin\.com\/in\/[^\/]+\/?$/)) {
34
+ console.error(chalk.red('✗ Invalid profile URL'));
35
+ console.log(chalk.gray('Expected format: https://www.linkedin.com/in/username/'));
36
+ process.exit(1);
37
+ }
38
+
39
+ console.log(chalk.blue(`Preparing to connect with ${profileUrl}...`));
40
+
41
+ // Launch browser (will try Edge CDP first)
42
+ browser = new BrowserController({
43
+ headless: options.headless ?? config.getValue('headless'),
44
+ });
45
+
46
+ await browser.launch();
47
+
48
+ // Verify we're logged in
49
+ const linkedInAuth = new LinkedInAuth(browser);
50
+ if (!(await linkedInAuth.isSessionValid())) {
51
+ console.error(chalk.red('✗ Not logged in'));
52
+ console.log(chalk.gray('Run: linkedin-cli auth login'));
53
+ await browser.close();
54
+ process.exit(1);
55
+ }
56
+
57
+ console.log(chalk.blue('Sending connection request...'));
58
+
59
+ // Create connector and send request
60
+ const connector = new LinkedInConnector(browser);
61
+ const result = await connector.connect({
62
+ profileUrl,
63
+ note: options.note,
64
+ });
65
+
66
+ if (result.success && result.sent) {
67
+ console.log(chalk.green('✓ Connection request sent successfully!'));
68
+ logger.log('connection.send', { profileUrl, hasNote: !!options.note }, true);
69
+ } else if (result.pending) {
70
+ console.log(chalk.yellow('⚠ Connection request already pending'));
71
+ } else if (result.error?.includes('Already connected')) {
72
+ console.log(chalk.yellow('✓ Already connected to this person'));
73
+ } else {
74
+ console.error(chalk.red('✗ Failed to send connection request:'), result.error);
75
+ logger.log('connection.send', { profileUrl, error: result.error }, false);
76
+ await browser.close();
77
+ process.exit(1);
78
+ }
79
+
80
+ await browser.close();
81
+ } catch (error) {
82
+ console.error(
83
+ chalk.red('✗ Connection failed:'),
84
+ error instanceof Error ? error.message : error
85
+ );
86
+ process.exit(1);
87
+ }
88
+ });
89
+ }
@@ -0,0 +1,7 @@
1
+ // Re-export command registrations from implementation files
2
+ export { registerAuthCommands } from './auth';
3
+ export { registerMessageCommands } from './messages';
4
+ export { registerReplyCommands } from './reply';
5
+ export { registerConnectionCommands } from './connection';
6
+ export { registerCompanyCommands } from './company';
7
+ export { registerProfileCommands } from './profile';