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,306 @@
1
+ /**
2
+ * Page Agent Extension Client
3
+ *
4
+ * Uses direct WebSocket connection to Chrome DevTools Protocol
5
+ * following the official Page Agent usage guide.
6
+ */
7
+
8
+ import WebSocket from 'ws';
9
+
10
+ /**
11
+ * Configuration for the Page Agent Extension
12
+ */
13
+ export interface ExtensionClientConfig {
14
+ /** Auth token from extension side panel */
15
+ authToken: string;
16
+ /** LLM API key */
17
+ apiKey: string;
18
+ /** LLM API endpoint (OpenAI-compatible) */
19
+ baseURL?: string;
20
+ /** Model name */
21
+ model?: string;
22
+ /** CDP port (default: 9222) */
23
+ port?: number;
24
+ /** Enable debug logging */
25
+ debug?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Result from the Page Agent Extension
30
+ */
31
+ export interface ExtensionResult {
32
+ success: boolean;
33
+ status: string;
34
+ message: string;
35
+ data?: string;
36
+ }
37
+
38
+ const DEFAULT_BASE_URL = 'https://open.bigmodel.cn/api/paas/v4';
39
+ const DEFAULT_MODEL = 'glm-5';
40
+ const DEFAULT_PORT = 9222;
41
+
42
+ /**
43
+ * Client for the Alibaba Page Agent browser extension
44
+ */
45
+ export class PageAgentExtensionClient {
46
+ private readonly config: Required<ExtensionClientConfig>;
47
+ private readonly debug: boolean;
48
+
49
+ constructor(config: ExtensionClientConfig) {
50
+ this.config = {
51
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL,
52
+ model: config.model ?? DEFAULT_MODEL,
53
+ port: config.port ?? DEFAULT_PORT,
54
+ authToken: config.authToken,
55
+ apiKey: config.apiKey,
56
+ debug: config.debug ?? false,
57
+ };
58
+ this.debug = config.debug ?? false;
59
+ }
60
+
61
+ /**
62
+ * List available browser pages
63
+ */
64
+ async listPages(): Promise<Array<{ id: string; url: string; title: string }>> {
65
+ const response = await fetch(`http://localhost:${this.config.port}/json/list`);
66
+ const pages = await response.json();
67
+ return pages
68
+ .filter((p: any) => p.type === 'page')
69
+ .map((p: any) => ({ id: p.id, url: p.url, title: p.title }));
70
+ }
71
+
72
+ /**
73
+ * Find a page by URL pattern
74
+ */
75
+ async findPage(urlPattern: string): Promise<string | null> {
76
+ const pages = await this.listPages();
77
+ const page = pages.find((p) => p.url.includes(urlPattern));
78
+ return page?.id ?? null;
79
+ }
80
+
81
+ /**
82
+ * Execute JavaScript on a page via CDP WebSocket
83
+ * Simplified version following the guide pattern
84
+ */
85
+ private async evaluate(ws: WebSocket, expression: string, awaitPromise = false): Promise<any> {
86
+ return new Promise((resolve, reject) => {
87
+ const id = Math.floor(Math.random() * 1000000);
88
+ let resolved = false;
89
+
90
+ const timeout = setTimeout(() => {
91
+ if (!resolved) {
92
+ resolved = true;
93
+ ws.off('message', handler);
94
+ reject(new Error('Evaluation timeout'));
95
+ }
96
+ }, 60000);
97
+
98
+ const handler = (data: Buffer) => {
99
+ if (resolved) return;
100
+ try {
101
+ const response = JSON.parse(data.toString());
102
+ if (response.id === id) {
103
+ resolved = true;
104
+ clearTimeout(timeout);
105
+ ws.off('message', handler);
106
+ if (response.error) {
107
+ reject(new Error(response.error.message));
108
+ } else {
109
+ resolve(response.result?.result?.value);
110
+ }
111
+ }
112
+ } catch {
113
+ // Ignore parse errors
114
+ }
115
+ };
116
+
117
+ ws.on('message', handler);
118
+
119
+ ws.send(
120
+ JSON.stringify({
121
+ id,
122
+ method: 'Runtime.evaluate',
123
+ params: {
124
+ expression,
125
+ returnByValue: true,
126
+ awaitPromise,
127
+ },
128
+ })
129
+ );
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Execute a task on a specific page
135
+ */
136
+ async executeOnPage(pageId: string, task: string): Promise<ExtensionResult> {
137
+ const wsUrl = `ws://localhost:${this.config.port}/devtools/page/${pageId}`;
138
+ this.log('Connecting to:', wsUrl);
139
+
140
+ return new Promise((resolve, reject) => {
141
+ const ws = new WebSocket(wsUrl);
142
+ let settled = false;
143
+
144
+ ws.on('error', (err) => {
145
+ if (!settled) {
146
+ settled = true;
147
+ reject(new Error(`WebSocket error: ${err.message}`));
148
+ }
149
+ });
150
+
151
+ ws.on('open', async () => {
152
+ if (settled) return;
153
+
154
+ try {
155
+ this.log('Connected, setting auth token...');
156
+
157
+ // Step 1: Set auth token
158
+ await this.evaluate(
159
+ ws,
160
+ `localStorage.setItem('PageAgentExtUserAuthToken', '${this.config.authToken}')`
161
+ );
162
+ this.log('Auth token set');
163
+
164
+ // Step 2: Wait for extension to inject API (3 seconds as per guide)
165
+ this.log('Waiting 3 seconds for extension injection...');
166
+ await new Promise((r) => setTimeout(r, 3000));
167
+
168
+ // Step 3: Verify extension is available
169
+ const extCheck = await this.evaluate(
170
+ ws,
171
+ `({
172
+ version: window.PAGE_AGENT_EXT_VERSION,
173
+ hasExt: !!window.PAGE_AGENT_EXT,
174
+ canExecute: typeof window.PAGE_AGENT_EXT?.execute === 'function'
175
+ })`
176
+ );
177
+
178
+ this.log('Extension check:', extCheck);
179
+
180
+ if (!extCheck?.hasExt) {
181
+ settled = true;
182
+ ws.close();
183
+ resolve({
184
+ success: false,
185
+ status: 'error',
186
+ message: 'Page Agent extension not found. Check auth token.',
187
+ });
188
+ return;
189
+ }
190
+
191
+ this.log('Extension detected, version:', extCheck.version);
192
+
193
+ // Step 4: Execute task
194
+ this.log('Executing task:', task.substring(0, 100) + '...');
195
+
196
+ const result = await this.evaluate(
197
+ ws,
198
+ `(async () => {
199
+ try {
200
+ const response = await window.PAGE_AGENT_EXT.execute(
201
+ ${JSON.stringify(task)},
202
+ {
203
+ baseURL: ${JSON.stringify(this.config.baseURL)},
204
+ apiKey: ${JSON.stringify(this.config.apiKey)},
205
+ model: ${JSON.stringify(this.config.model)},
206
+ includeInitialTab: true,
207
+ onStatusChange: (status) => console.log('[Status]', status),
208
+ onActivity: (activity) => console.log('[Activity]', JSON.stringify(activity))
209
+ }
210
+ );
211
+ return { success: true, data: response };
212
+ } catch (error) {
213
+ return { success: false, error: error.message };
214
+ }
215
+ })()`,
216
+ true // awaitPromise
217
+ );
218
+
219
+ this.log('Task result:', JSON.stringify(result)?.substring(0, 500));
220
+
221
+ settled = true;
222
+ ws.close();
223
+
224
+ resolve({
225
+ success: result?.success ?? false,
226
+ status: result?.success ? 'completed' : 'error',
227
+ message: result?.success ? 'Task completed' : result?.error || 'Unknown error',
228
+ data: typeof result?.data === 'string' ? result.data : JSON.stringify(result?.data),
229
+ });
230
+ } catch (err: any) {
231
+ settled = true;
232
+ ws.close();
233
+ resolve({
234
+ success: false,
235
+ status: 'error',
236
+ message: err.message,
237
+ });
238
+ }
239
+ });
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Send a connection request on a LinkedIn profile
245
+ */
246
+ async connect(_profileUrl: string, note: string): Promise<ExtensionResult> {
247
+ // Step 1: Find LinkedIn page
248
+ this.log('Looking for LinkedIn page...');
249
+ let pageId = await this.findPage('linkedin.com/in/');
250
+
251
+ if (!pageId) {
252
+ pageId = await this.findPage('linkedin.com');
253
+ }
254
+
255
+ if (!pageId) {
256
+ return {
257
+ success: false,
258
+ status: 'error',
259
+ message: 'No LinkedIn page found. Please open a LinkedIn profile in your browser.',
260
+ };
261
+ }
262
+
263
+ this.log('Found LinkedIn page:', pageId);
264
+
265
+ // Step 2: Build task
266
+ const task = `On this LinkedIn profile page:
267
+ 1. Click the "Connect" button (or find it in "More" menu if not visible)
268
+ 2. If a modal opens with "Add a note" option, click it
269
+ 3. Type this message: "${note}"
270
+ 4. STOP - Do NOT click Send button
271
+ 5. Report success
272
+
273
+ Important: Work on the current page. Do not navigate away.`;
274
+
275
+ // Step 3: Execute on that page
276
+ return this.executeOnPage(pageId, task);
277
+ }
278
+
279
+ /**
280
+ * Log debug messages
281
+ */
282
+ private log(message: string, data?: unknown): void {
283
+ if (this.debug) {
284
+ if (data !== undefined) {
285
+ console.log(`[ExtensionClient] ${message}`, data);
286
+ } else {
287
+ console.log(`[ExtensionClient] ${message}`);
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Get the Page Agent auth token from environment variable
295
+ * @throws Error if PAGE_AGENT_AUTH_TOKEN is not set
296
+ */
297
+ export function getAuthToken(): string {
298
+ const token = process.env.PAGE_AGENT_AUTH_TOKEN;
299
+ if (!token) {
300
+ throw new Error(
301
+ 'PAGE_AGENT_AUTH_TOKEN environment variable is required. ' +
302
+ 'Get your auth token from the Page Agent extension side panel.'
303
+ );
304
+ }
305
+ return token;
306
+ }
@@ -0,0 +1,28 @@
1
+ // Types
2
+ export type {
3
+ Action,
4
+ ActionContext,
5
+ ActionPlan,
6
+ BoundingBox,
7
+ ClaudeClientConfig,
8
+ ClickAction,
9
+ ConnectionResult,
10
+ DOMElement,
11
+ DOMRepresentation,
12
+ ExecutedAction,
13
+ MessageResult,
14
+ NavigateAction,
15
+ PageMetadata,
16
+ Task,
17
+ TaskResult,
18
+ TypeAction,
19
+ WaitAction,
20
+ } from './types';
21
+
22
+ // Classes
23
+ export { DOMExtractor } from './dom-extractor';
24
+ export { ClaudeClient } from './claude-client';
25
+ export { ActionExecutor } from './action-executor';
26
+ export { PageAgent } from './page-agent';
27
+ export { PageAgentExtensionClient, getAuthToken } from './extension-client';
28
+ export type { ExtensionClientConfig, ExtensionResult } from './extension-client';
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Page Agent Orchestrator Module
3
+ *
4
+ * Combines DOM extractor, Claude client, and action executor into a cohesive workflow.
5
+ * Provides high-level methods for LinkedIn automation tasks with retry logic and error recovery.
6
+ */
7
+
8
+ import type { Page } from 'playwright';
9
+ import { DOMExtractor } from './dom-extractor';
10
+ import { ClaudeClient } from './claude-client';
11
+ import { ActionExecutor } from './action-executor';
12
+ import type {
13
+ Task,
14
+ TaskResult,
15
+ ConnectionResult,
16
+ MessageResult,
17
+ ActionContext,
18
+ ExecutedAction,
19
+ ClaudeClientConfig,
20
+ } from './types';
21
+
22
+ /**
23
+ * Configuration options for PageAgent
24
+ */
25
+ export interface PageAgentOptions {
26
+ /** Configuration for Claude API client */
27
+ claudeConfig: ClaudeClientConfig;
28
+ /** Maximum number of retry attempts for failed actions (default: 3) */
29
+ maxRetries?: number;
30
+ /** Enable debug logging (default: false) */
31
+ debug?: boolean;
32
+ }
33
+
34
+ /**
35
+ * High-level agent for executing tasks on LinkedIn pages
36
+ * Orchestrates DOM extraction, action planning, and execution
37
+ */
38
+ export class PageAgent {
39
+ private readonly claudeClient: ClaudeClient;
40
+ private readonly domExtractor: DOMExtractor;
41
+ private readonly maxRetries: number;
42
+ private readonly debug: boolean;
43
+
44
+ /**
45
+ * Creates a new PageAgent instance
46
+ * @param options - Configuration options including Claude client config
47
+ */
48
+ constructor(options: PageAgentOptions) {
49
+ this.claudeClient = new ClaudeClient(options.claudeConfig);
50
+ this.domExtractor = new DOMExtractor();
51
+ this.maxRetries = options.maxRetries ?? 3;
52
+ this.debug = options.debug ?? false;
53
+ }
54
+
55
+ /**
56
+ * Execute a task on the given page with retry logic
57
+ * @param task - The task to execute
58
+ * @param page - Playwright page instance
59
+ * @returns Promise<TaskResult> with execution results
60
+ */
61
+ async execute(task: Task, page: Page): Promise<TaskResult> {
62
+ const goal = this.buildGoal(task);
63
+ const previousActions: ExecutedAction[] = [];
64
+ let retryCount = 0;
65
+
66
+ this.logDebug(`Starting task execution: ${goal}`);
67
+
68
+ while (retryCount < this.maxRetries) {
69
+ // Step 1: Extract DOM
70
+ const dom = await this.domExtractor.extract(page);
71
+ this.logDebug(`Extracted DOM with ${dom.elements.length} elements`);
72
+
73
+ if (this.debug) {
74
+ this.logDebug('DOM:', JSON.stringify(dom, null, 2));
75
+ }
76
+
77
+ // Step 2: Build action context
78
+ const context: ActionContext = {
79
+ goal,
80
+ previousActions,
81
+ retryCount,
82
+ };
83
+
84
+ // Step 3: Get action plan from Claude
85
+ const plan = await this.claudeClient.generateActionPlan(dom, context);
86
+ this.logDebug(`Action plan status: ${plan.status}`);
87
+
88
+ // Step 4: Handle plan status
89
+ if (plan.status === 'completed') {
90
+ this.logDebug('Task completed successfully');
91
+ return {
92
+ success: true,
93
+ message: `Task completed successfully. ${plan.reasoning}`,
94
+ actionsTaken: previousActions.map((ea) => ea.action),
95
+ finalUrl: page.url(),
96
+ };
97
+ }
98
+
99
+ if (plan.status === 'failed') {
100
+ this.logDebug(`Task failed: ${plan.error}`);
101
+ return {
102
+ success: false,
103
+ message: `Task failed: ${plan.error || 'Unknown error'}`,
104
+ actionsTaken: previousActions.map((ea) => ea.action),
105
+ finalUrl: page.url(),
106
+ };
107
+ }
108
+
109
+ // Step 5: Execute actions
110
+ const executor = new ActionExecutor(page, dom);
111
+ const executedActions = await executor.executeAll(plan.actions);
112
+
113
+ // Step 6: Add executed actions to history
114
+ previousActions.push(...executedActions);
115
+
116
+ // Step 7: Check for failures
117
+ const hasFailures = executedActions.some((ea) => !ea.success);
118
+
119
+ if (hasFailures) {
120
+ const failedActions = executedActions.filter((ea) => !ea.success);
121
+ this.logDebug(`Action execution failed: ${failedActions.map((ea) => ea.error).join(', ')}`);
122
+ retryCount++;
123
+
124
+ if (retryCount < this.maxRetries) {
125
+ this.logDebug(`Retrying (${retryCount}/${this.maxRetries})...`);
126
+ await page.waitForTimeout(2000);
127
+ }
128
+ } else {
129
+ this.logDebug('Actions executed successfully');
130
+ }
131
+
132
+ // Step 8: Wait between iterations
133
+ await page.waitForTimeout(1000);
134
+ }
135
+
136
+ // Max retries exceeded
137
+ this.logDebug('Max retries exceeded');
138
+ return {
139
+ success: false,
140
+ message: `Task failed after ${this.maxRetries} retries`,
141
+ actionsTaken: previousActions.map((ea) => ea.action),
142
+ finalUrl: page.url(),
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Send a connection request to a LinkedIn profile
148
+ * @param profileUrl - URL of the LinkedIn profile
149
+ * @param note - Optional personalized note for the connection request
150
+ * @param page - Playwright page instance
151
+ * @returns Promise<ConnectionResult> with connection status
152
+ */
153
+ async connect(
154
+ profileUrl: string,
155
+ note: string | undefined,
156
+ page: Page
157
+ ): Promise<ConnectionResult> {
158
+ this.logDebug(`Navigating to profile: ${profileUrl}`);
159
+
160
+ // Step 1: Navigate to profile
161
+ await page.goto(profileUrl, {
162
+ waitUntil: 'domcontentloaded',
163
+ timeout: 30000,
164
+ });
165
+
166
+ // Step 2: Wait for page to settle
167
+ await page.waitForTimeout(3000);
168
+
169
+ // Step 3: Create and execute task
170
+ const task: Task = {
171
+ goal: 'Send connection request',
172
+ profileUrl,
173
+ note,
174
+ };
175
+
176
+ const result = await this.execute(task, page);
177
+
178
+ // Step 4: Extract final DOM to check connection state
179
+ const finalDom = await this.domExtractor.extract(page);
180
+ const connectionState = finalDom.metadata.connectionState;
181
+
182
+ // Determine the outcome
183
+ const sent = result.success && connectionState === 'pending';
184
+ const alreadyConnected = connectionState === 'connected';
185
+ const pending = connectionState === 'pending';
186
+
187
+ let message = result.message;
188
+ if (alreadyConnected) {
189
+ message = 'Already connected with this user';
190
+ } else if (sent) {
191
+ message = 'Connection request sent successfully';
192
+ } else if (pending) {
193
+ message = 'Connection request is pending';
194
+ }
195
+
196
+ return {
197
+ ...result,
198
+ sent,
199
+ pending,
200
+ alreadyConnected,
201
+ message,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Send a message to a LinkedIn connection
207
+ * @param profileUrl - URL of the LinkedIn profile
208
+ * @param message - Message text to send
209
+ * @param page - Playwright page instance
210
+ * @returns Promise<MessageResult> with message status
211
+ */
212
+ async sendMessage(profileUrl: string, message: string, page: Page): Promise<MessageResult> {
213
+ this.logDebug(`Navigating to profile for messaging: ${profileUrl}`);
214
+
215
+ // Step 1: Navigate to profile
216
+ await page.goto(profileUrl, {
217
+ waitUntil: 'domcontentloaded',
218
+ timeout: 30000,
219
+ });
220
+
221
+ // Step 2: Wait for page to settle
222
+ await page.waitForTimeout(3000);
223
+
224
+ // Step 3: Create and execute task
225
+ const task: Task = {
226
+ goal: 'Send message',
227
+ profileUrl,
228
+ message,
229
+ };
230
+
231
+ const result = await this.execute(task, page);
232
+
233
+ // Determine the outcome
234
+ const sent = result.success;
235
+ const finalMessage = sent ? 'Message sent successfully' : result.message;
236
+
237
+ return {
238
+ ...result,
239
+ sent,
240
+ message: finalMessage,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Build a goal string from task properties
246
+ * @param task - Task with goal, note, or message
247
+ * @returns Goal string for the agent
248
+ */
249
+ private buildGoal(task: Task): string {
250
+ // Use explicit goal if provided
251
+ if (task.goal) {
252
+ return task.goal;
253
+ }
254
+
255
+ // Build goal from note (connection request)
256
+ if (task.note !== undefined) {
257
+ return `Send connection request to ${task.profileUrl || 'profile'} with note: ${task.note}`;
258
+ }
259
+
260
+ // Build goal from message
261
+ if (task.message !== undefined) {
262
+ return `Send message to ${task.profileUrl || 'profile'}: ${task.message}`;
263
+ }
264
+
265
+ // Default goal
266
+ return 'Complete task on LinkedIn';
267
+ }
268
+
269
+ /**
270
+ * Log debug message if debug mode is enabled
271
+ * @param message - Message to log
272
+ * @param data - Optional data to log
273
+ */
274
+ private logDebug(message: string, data?: unknown): void {
275
+ if (this.debug) {
276
+ if (data !== undefined) {
277
+ console.log(`[PageAgent] ${message}`, data);
278
+ } else {
279
+ console.log(`[PageAgent] ${message}`);
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Create a new PageAgent instance
287
+ * @param options - Configuration options
288
+ * @returns PageAgent instance
289
+ */
290
+ export function createPageAgent(options: PageAgentOptions): PageAgent {
291
+ return new PageAgent(options);
292
+ }