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,1598 @@
1
+ # Page Agent Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Implement DOM-text based LinkedIn automation using Claude API, replacing selector-based approach with LLM-powered agent for connect and message commands.
6
+
7
+ **Architecture:** Extract semantic DOM representation from LinkedIn pages, send to Claude API for action planning, execute actions via Playwright. Uses accessibility-first extraction with element IDs mapped to bounding boxes for clicking.
8
+
9
+ **Tech Stack:** TypeScript, Playwright, Claude API (via Dashscope), Zod for validation
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ ```
16
+ src/
17
+ ├── agent/
18
+ │ ├── index.ts # Public exports
19
+ │ ├── types.ts # Core type definitions
20
+ │ ├── dom-extractor.ts # DOM to structured text extraction
21
+ │ ├── claude-client.ts # Claude API client via Dashscope
22
+ │ ├── action-executor.ts # Execute action plans with Playwright
23
+ │ ├── page-agent.ts # Main orchestrator
24
+ │ └── prompts.ts # LLM prompt templates
25
+ ├── cli/
26
+ │ └── agent-commands.ts # New CLI commands
27
+ └── linkedin/
28
+ └── (existing - keep for reference)
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Chunk 1: Types and Interfaces
34
+
35
+ **Purpose:** Define all TypeScript interfaces and types for the agent system.
36
+
37
+ **Files:**
38
+ - Create: `src/agent/types.ts`
39
+
40
+ ---
41
+
42
+ ### Task 1: Define DOM Types
43
+
44
+ - [ ] **Step 1: Write types for DOM representation**
45
+
46
+ Create `src/agent/types.ts`:
47
+
48
+ ```typescript
49
+ /**
50
+ * Bounding box for an element on the page
51
+ */
52
+ export interface BoundingBox {
53
+ x: number;
54
+ y: number;
55
+ width: number;
56
+ height: number;
57
+ }
58
+
59
+ /**
60
+ * Represents a single DOM element extracted from the page
61
+ */
62
+ export interface DOMElement {
63
+ /** Unique identifier assigned during extraction */
64
+ id: string;
65
+ /** HTML tag name */
66
+ tag: string;
67
+ /** ARIA role if present */
68
+ role?: string;
69
+ /** ARIA label if present */
70
+ ariaLabel?: string;
71
+ /** Visible text content */
72
+ text: string;
73
+ /** Element type for interactive elements */
74
+ type?: 'button' | 'link' | 'input' | 'textarea' | 'select' | 'checkbox';
75
+ /** Current value (for form inputs) */
76
+ value?: string;
77
+ /** Whether element is visible */
78
+ visible: boolean;
79
+ /** Whether element is enabled */
80
+ enabled: boolean;
81
+ /** Bounding box for click coordinates */
82
+ bbox: BoundingBox;
83
+ }
84
+
85
+ /**
86
+ * Complete DOM representation of a page
87
+ */
88
+ export interface DOMRepresentation {
89
+ /** Current URL */
90
+ url: string;
91
+ /** Page title */
92
+ title: string;
93
+ /** All extracted elements */
94
+ elements: DOMElement[];
95
+ /** Extracted status/info from the page */
96
+ metadata: PageMetadata;
97
+ }
98
+
99
+ /**
100
+ * Metadata extracted from LinkedIn pages
101
+ */
102
+ export interface PageMetadata {
103
+ /** Type of page */
104
+ pageType: 'profile' | 'messaging' | 'feed' | 'other';
105
+ /** Profile name (if on profile page) */
106
+ profileName?: string;
107
+ /** Profile headline/title (if on profile page) */
108
+ profileTitle?: string;
109
+ /** Connection status with profile owner */
110
+ connectionState?: 'connected' | 'pending' | 'not_connected' | 'unknown';
111
+ /** Any error or status messages visible */
112
+ statusMessages: string[];
113
+ }
114
+ ```
115
+
116
+ - [ ] **Step 2: Commit**
117
+
118
+ ```bash
119
+ git add src/agent/types.ts
120
+ git commit -m "feat(agent): add DOM representation types"
121
+ ```
122
+
123
+ ---
124
+
125
+ ### Task 2: Define Action Types
126
+
127
+ - [ ] **Step 1: Add action type definitions to types.ts**
128
+
129
+ Add to `src/agent/types.ts`:
130
+
131
+ ```typescript
132
+ /**
133
+ * Click action - click an element by ID
134
+ */
135
+ export interface ClickAction {
136
+ type: 'click';
137
+ elementId: string;
138
+ description: string;
139
+ }
140
+
141
+ /**
142
+ * Type action - enter text into an input
143
+ */
144
+ export interface TypeAction {
145
+ type: 'type';
146
+ elementId: string;
147
+ text: string;
148
+ description: string;
149
+ }
150
+
151
+ /**
152
+ * Wait action - pause execution
153
+ */
154
+ export interface WaitAction {
155
+ type: 'wait';
156
+ durationMs: number;
157
+ description: string;
158
+ }
159
+
160
+ /**
161
+ * Navigate action - go to a URL
162
+ */
163
+ export interface NavigateAction {
164
+ type: 'navigate';
165
+ url: string;
166
+ description: string;
167
+ }
168
+
169
+ /**
170
+ * Union type of all possible actions
171
+ */
172
+ export type Action = ClickAction | TypeAction | WaitAction | NavigateAction;
173
+
174
+ /**
175
+ * Complete action plan from LLM
176
+ */
177
+ export interface ActionPlan {
178
+ /** Reasoning for the plan */
179
+ reasoning: string;
180
+ /** Expected outcome after executing */
181
+ expectedOutcome: string;
182
+ /** Actions to execute */
183
+ actions: Action[];
184
+ /** Current status */
185
+ status: 'in_progress' | 'completed' | 'error';
186
+ /** Error message if status is error */
187
+ error?: string;
188
+ }
189
+ ```
190
+
191
+ - [ ] **Step 2: Commit**
192
+
193
+ ```bash
194
+ git add src/agent/types.ts
195
+ git commit -m "feat(agent): add action type definitions"
196
+ ```
197
+
198
+ ---
199
+
200
+ ### Task 3: Define Client and Task Types
201
+
202
+ - [ ] **Step 1: Add remaining type definitions**
203
+
204
+ Add to `src/agent/types.ts`:
205
+
206
+ ```typescript
207
+ /**
208
+ * Configuration for Claude API client
209
+ */
210
+ export interface ClaudeClientConfig {
211
+ /** API key for Dashscope */
212
+ apiKey: string;
213
+ /** Base URL for API */
214
+ baseUrl: string;
215
+ /** Model to use */
216
+ model: string;
217
+ }
218
+
219
+ /**
220
+ * Context for action planning
221
+ */
222
+ export interface ActionContext {
223
+ /** Previous actions taken */
224
+ previousActions: ExecutedAction[];
225
+ /** User's goal */
226
+ goal: string;
227
+ /** Number of retries so far */
228
+ retryCount: number;
229
+ }
230
+
231
+ /**
232
+ * Record of an executed action
233
+ */
234
+ export interface ExecutedAction {
235
+ action: Action;
236
+ success: boolean;
237
+ timestamp: number;
238
+ error?: string;
239
+ }
240
+
241
+ /**
242
+ * Task for the agent to execute
243
+ */
244
+ export interface Task {
245
+ /** What the user wants to accomplish */
246
+ goal: string;
247
+ /** Target LinkedIn profile URL (for connect/message tasks) */
248
+ profileUrl?: string;
249
+ /** Note to include (for connection requests) */
250
+ note?: string;
251
+ /** Message to send (for messaging) */
252
+ message?: string;
253
+ }
254
+
255
+ /**
256
+ * Result of executing a task
257
+ */
258
+ export interface TaskResult {
259
+ success: boolean;
260
+ message: string;
261
+ actionsTaken: Action[];
262
+ finalUrl?: string;
263
+ }
264
+
265
+ /**
266
+ * Connection-specific result
267
+ */
268
+ export interface ConnectionResult extends TaskResult {
269
+ sent: boolean;
270
+ pending: boolean;
271
+ alreadyConnected?: boolean;
272
+ }
273
+
274
+ /**
275
+ * Message-specific result
276
+ */
277
+ export interface MessageResult extends TaskResult {
278
+ sent: boolean;
279
+ }
280
+ ```
281
+
282
+ - [ ] **Step 2: Commit**
283
+
284
+ ```bash
285
+ git add src/agent/types.ts
286
+ git commit -m "feat(agent): add client and task type definitions"
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Chunk 2: DOM Extractor
292
+
293
+ **Purpose:** Extract semantic DOM representation from LinkedIn pages using Playwright.
294
+
295
+ **Files:**
296
+ - Create: `src/agent/dom-extractor.ts`
297
+ - Test: `src/agent/dom-extractor.test.ts`
298
+
299
+ ---
300
+
301
+ ### Task 4: Create DOM Extractor Core
302
+
303
+ - [ ] **Step 1: Write the failing test**
304
+
305
+ Create `src/agent/dom-extractor.test.ts`:
306
+
307
+ ```typescript
308
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
309
+ import { DOMExtractor } from './dom-extractor';
310
+ import type { Page } from 'playwright';
311
+
312
+ describe('DOMExtractor', () => {
313
+ let extractor: DOMExtractor;
314
+ let mockPage: Partial<Page>;
315
+
316
+ beforeEach(() => {
317
+ extractor = new DOMExtractor();
318
+ mockPage = {
319
+ url: vi.fn().mockReturnValue('https://www.linkedin.com/in/test'),
320
+ title: vi.fn().mockResolvedValue('Test User | LinkedIn'),
321
+ evaluate: vi.fn().mockResolvedValue({
322
+ elements: [
323
+ {
324
+ id: 'elem-1',
325
+ tag: 'button',
326
+ role: 'button',
327
+ ariaLabel: 'Connect',
328
+ text: 'Connect',
329
+ type: 'button',
330
+ visible: true,
331
+ enabled: true,
332
+ bbox: { x: 100, y: 200, width: 80, height: 40 },
333
+ },
334
+ ],
335
+ metadata: {
336
+ pageType: 'profile',
337
+ profileName: 'Test User',
338
+ connectionState: 'not_connected',
339
+ statusMessages: [],
340
+ },
341
+ }),
342
+ };
343
+ });
344
+
345
+ it('should extract DOM representation from page', async () => {
346
+ const dom = await extractor.extract(mockPage as Page);
347
+
348
+ expect(dom.url).toBe('https://www.linkedin.com/in/test');
349
+ expect(dom.title).toBe('Test User | LinkedIn');
350
+ expect(dom.elements).toHaveLength(1);
351
+ expect(dom.elements[0].text).toBe('Connect');
352
+ expect(dom.metadata.pageType).toBe('profile');
353
+ });
354
+
355
+ it('should generate unique IDs for elements', async () => {
356
+ const dom = await extractor.extract(mockPage as Page);
357
+
358
+ expect(dom.elements[0].id).toBeDefined();
359
+ expect(typeof dom.elements[0].id).toBe('string');
360
+ });
361
+ });
362
+ ```
363
+
364
+ - [ ] **Step 2: Run test to verify it fails**
365
+
366
+ ```bash
367
+ npm test src/agent/dom-extractor.test.ts
368
+ ```
369
+
370
+ Expected: FAIL with "Cannot find module './dom-extractor'"
371
+
372
+ - [ ] **Step 3: Implement DOM extractor**
373
+
374
+ Create `src/agent/dom-extractor.ts`:
375
+
376
+ ```typescript
377
+ import type { Page } from 'playwright';
378
+ import type { DOMRepresentation, DOMElement, PageMetadata } from './types';
379
+
380
+ /**
381
+ * Extracts semantic DOM representation from a LinkedIn page.
382
+ * Uses Playwright's evaluate to extract clean, structured data.
383
+ */
384
+ export class DOMExtractor {
385
+ /**
386
+ * Extract DOM representation from the current page
387
+ */
388
+ async extract(page: Page): Promise<DOMRepresentation> {
389
+ const url = page.url();
390
+ const title = await page.title();
391
+
392
+ // Extract elements and metadata via page.evaluate
393
+ const result = await page.evaluate(() => {
394
+ const elements: Array<{
395
+ tag: string;
396
+ role?: string;
397
+ ariaLabel?: string;
398
+ text: string;
399
+ type?: string;
400
+ visible: boolean;
401
+ enabled: boolean;
402
+ bbox: { x: number; y: number; width: number; height: number };
403
+ }> = [];
404
+
405
+ // Helper to check visibility
406
+ const isVisible = (el: Element): boolean => {
407
+ const style = window.getComputedStyle(el);
408
+ return (
409
+ style.display !== 'none' &&
410
+ style.visibility !== 'hidden' &&
411
+ style.opacity !== '0' &&
412
+ el.getBoundingClientRect().width > 0 &&
413
+ el.getBoundingClientRect().height > 0
414
+ );
415
+ };
416
+
417
+ // Extract interactive elements
418
+ const interactiveSelectors = [
419
+ 'button',
420
+ 'a[href]',
421
+ 'input',
422
+ 'textarea',
423
+ 'select',
424
+ '[role="button"]',
425
+ '[role="link"]',
426
+ '[role="textbox"]',
427
+ ];
428
+
429
+ interactiveSelectors.forEach((selector) => {
430
+ document.querySelectorAll(selector).forEach((el, index) => {
431
+ const htmlEl = el as HTMLElement;
432
+ const rect = el.getBoundingClientRect();
433
+
434
+ if (!isVisible(el)) return;
435
+
436
+ const text = el.textContent?.trim() || '';
437
+ const ariaLabel =
438
+ el.getAttribute('aria-label') ||
439
+ el.getAttribute('title') ||
440
+ '';
441
+
442
+ // Determine element type
443
+ let type: string | undefined;
444
+ const tag = el.tagName.toLowerCase();
445
+ if (tag === 'button' || el.getAttribute('role') === 'button') {
446
+ type = 'button';
447
+ } else if (tag === 'a' || el.getAttribute('role') === 'link') {
448
+ type = 'link';
449
+ } else if (tag === 'input') {
450
+ type = el.getAttribute('type') || 'input';
451
+ } else if (tag === 'textarea') {
452
+ type = 'textarea';
453
+ }
454
+
455
+ elements.push({
456
+ tag,
457
+ role: el.getAttribute('role') || undefined,
458
+ ariaLabel: ariaLabel || undefined,
459
+ text: text.slice(0, 200), // Limit text length
460
+ type,
461
+ visible: true,
462
+ enabled: !htmlEl.disabled,
463
+ bbox: {
464
+ x: Math.round(rect.x),
465
+ y: Math.round(rect.y),
466
+ width: Math.round(rect.width),
467
+ height: Math.round(rect.height),
468
+ },
469
+ });
470
+ });
471
+ });
472
+
473
+ // Extract metadata
474
+ const metadata: PageMetadata = {
475
+ pageType: 'other',
476
+ statusMessages: [],
477
+ };
478
+
479
+ // Detect page type
480
+ if (window.location.pathname.includes('/in/')) {
481
+ metadata.pageType = 'profile';
482
+
483
+ // Try to find profile name
484
+ const nameEl =
485
+ document.querySelector('h1') ||
486
+ document.querySelector('[data-testid="profile-name"]');
487
+ if (nameEl) {
488
+ metadata.profileName = nameEl.textContent?.trim();
489
+ }
490
+
491
+ // Detect connection state
492
+ const bodyText = document.body.innerText;
493
+ if (bodyText.includes('Pending')) {
494
+ metadata.connectionState = 'pending';
495
+ } else if (
496
+ bodyText.includes('Message') &&
497
+ !bodyText.includes('Connect')
498
+ ) {
499
+ metadata.connectionState = 'connected';
500
+ } else if (bodyText.includes('Connect')) {
501
+ metadata.connectionState = 'not_connected';
502
+ }
503
+ } else if (window.location.pathname.includes('/messaging/')) {
504
+ metadata.pageType = 'messaging';
505
+ }
506
+
507
+ return { elements, metadata };
508
+ });
509
+
510
+ // Assign unique IDs to elements
511
+ const elementsWithIds: DOMElement[] = result.elements.map((el, index) => ({
512
+ ...el,
513
+ id: this.generateElementId(el, index),
514
+ }));
515
+
516
+ return {
517
+ url,
518
+ title,
519
+ elements: elementsWithIds,
520
+ metadata: result.metadata,
521
+ };
522
+ }
523
+
524
+ /**
525
+ * Generate a unique ID for an element
526
+ */
527
+ private generateElementId(
528
+ el: { tag: string; ariaLabel?: string; text: string },
529
+ index: number
530
+ ): string {
531
+ const label = el.ariaLabel || el.text.slice(0, 20);
532
+ const hash = `${el.tag}-${label}-${index}`
533
+ .replace(/[^a-zA-Z0-9-]/g, '-')
534
+ .toLowerCase();
535
+ return `elem-${hash}`;
536
+ }
537
+ }
538
+ ```
539
+
540
+ - [ ] **Step 4: Run tests**
541
+
542
+ ```bash
543
+ npm test src/agent/dom-extractor.test.ts
544
+ ```
545
+
546
+ Expected: PASS
547
+
548
+ - [ ] **Step 5: Commit**
549
+
550
+ ```bash
551
+ git add src/agent/dom-extractor.ts src/agent/dom-extractor.test.ts
552
+ git commit -m "feat(agent): implement DOM extractor"
553
+ ```
554
+
555
+ ---
556
+
557
+ ## Chunk 3: Claude API Client
558
+
559
+ **Purpose:** Client for Dashscope Claude API.
560
+
561
+ **Files:**
562
+ - Create: `src/agent/claude-client.ts`
563
+ - Test: `src/agent/claude-client.test.ts`
564
+
565
+ ---
566
+
567
+ ### Task 5: Create Claude Client
568
+
569
+ - [ ] **Step 1: Write the failing test**
570
+
571
+ Create `src/agent/claude-client.test.ts`:
572
+
573
+ ```typescript
574
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
575
+ import { ClaudeClient } from './claude-client';
576
+ import type { DOMRepresentation, ActionContext } from './types';
577
+
578
+ describe('ClaudeClient', () => {
579
+ let client: ClaudeClient;
580
+
581
+ beforeEach(() => {
582
+ client = new ClaudeClient({
583
+ apiKey: 'test-key',
584
+ baseUrl: 'https://test.example.com',
585
+ model: 'qwen3.5-plus',
586
+ });
587
+
588
+ // Mock fetch
589
+ global.fetch = vi.fn();
590
+ });
591
+
592
+ it('should generate action plan from DOM', async () => {
593
+ const mockResponse = {
594
+ content: [
595
+ {
596
+ type: 'text',
597
+ text: JSON.stringify({
598
+ reasoning: 'Need to click Connect button',
599
+ expectedOutcome: 'Connection modal opens',
600
+ actions: [
601
+ { type: 'click', elementId: 'elem-button-connect', description: 'Click Connect' },
602
+ ],
603
+ status: 'in_progress',
604
+ }),
605
+ },
606
+ ],
607
+ };
608
+
609
+ vi.mocked(fetch).mockResolvedValueOnce({
610
+ ok: true,
611
+ json: async () => mockResponse,
612
+ } as Response);
613
+
614
+ const dom: DOMRepresentation = {
615
+ url: 'https://linkedin.com/in/test',
616
+ title: 'Test | LinkedIn',
617
+ elements: [
618
+ {
619
+ id: 'elem-button-connect',
620
+ tag: 'button',
621
+ text: 'Connect',
622
+ type: 'button',
623
+ visible: true,
624
+ enabled: true,
625
+ bbox: { x: 100, y: 200, width: 80, height: 40 },
626
+ },
627
+ ],
628
+ metadata: {
629
+ pageType: 'profile',
630
+ connectionState: 'not_connected',
631
+ statusMessages: [],
632
+ },
633
+ };
634
+
635
+ const context: ActionContext = {
636
+ goal: 'Send connection request',
637
+ previousActions: [],
638
+ retryCount: 0,
639
+ };
640
+
641
+ const plan = await client.generateActionPlan(dom, context);
642
+
643
+ expect(plan.reasoning).toBe('Need to click Connect button');
644
+ expect(plan.actions).toHaveLength(1);
645
+ expect(plan.actions[0].type).toBe('click');
646
+ });
647
+ });
648
+ ```
649
+
650
+ - [ ] **Step 2: Run test to verify it fails**
651
+
652
+ ```bash
653
+ npm test src/agent/claude-client.test.ts
654
+ ```
655
+
656
+ Expected: FAIL with "Cannot find module"
657
+
658
+ - [ ] **Step 3: Implement Claude client**
659
+
660
+ Create `src/agent/claude-client.ts`:
661
+
662
+ ```typescript
663
+ import type { DOMRepresentation, ActionPlan, ActionContext, ClaudeClientConfig } from './types';
664
+
665
+ export class ClaudeClient {
666
+ private config: ClaudeClientConfig;
667
+
668
+ constructor(config: ClaudeClientConfig) {
669
+ this.config = config;
670
+ }
671
+
672
+ /**
673
+ * Generate an action plan based on current DOM state
674
+ */
675
+ async generateActionPlan(
676
+ dom: DOMRepresentation,
677
+ context: ActionContext
678
+ ): Promise<ActionPlan> {
679
+ const messages = this.buildMessages(dom, context);
680
+
681
+ const response = await fetch(`${this.config.baseUrl}/messages`, {
682
+ method: 'POST',
683
+ headers: {
684
+ 'Content-Type': 'application/json',
685
+ 'Authorization': `Bearer ${this.config.apiKey}`,
686
+ },
687
+ body: JSON.stringify({
688
+ model: this.config.model,
689
+ max_tokens: 4096,
690
+ messages,
691
+ }),
692
+ });
693
+
694
+ if (!response.ok) {
695
+ const error = await response.text();
696
+ throw new Error(`Claude API error: ${response.status} - ${error}`);
697
+ }
698
+
699
+ const result = await response.json();
700
+ const content = result.content?.[0]?.text;
701
+
702
+ if (!content) {
703
+ throw new Error('Empty response from Claude API');
704
+ }
705
+
706
+ return this.parseActionPlan(content);
707
+ }
708
+
709
+ /**
710
+ * Build messages for the API call
711
+ */
712
+ private buildMessages(dom: DOMRepresentation, context: ActionContext): Array<{ role: string; content: string }> {
713
+ const systemPrompt = `You are a web automation agent that controls LinkedIn through a browser.
714
+ You receive a DOM representation of the current page and must decide the next action(s) to achieve the user's goal.
715
+
716
+ Rules:
717
+ 1. Respond ONLY with a JSON action plan
718
+ 2. Each action must reference an element by its ID from the DOM
719
+ 3. Include brief reasoning for your decision
720
+ 4. If the goal is already achieved, return empty actions with status "completed"
721
+ 5. If stuck or error, explain why and suggest recovery with status "error"
722
+
723
+ Available actions:
724
+ - click: Click an element (requires elementId)
725
+ - type: Type text into an input (requires elementId, text)
726
+ - wait: Wait for page to stabilize (requires durationMs in milliseconds)
727
+ - navigate: Go to a URL (requires url)
728
+
729
+ Response format:
730
+ {
731
+ "reasoning": "brief explanation",
732
+ "expectedOutcome": "what should happen",
733
+ "actions": [
734
+ { "type": "click", "elementId": "elem-xxx", "description": "what this does" }
735
+ ],
736
+ "status": "in_progress" | "completed" | "error",
737
+ "error": "error message if status is error"
738
+ }`;
739
+
740
+ const domJson = JSON.stringify(dom, null, 2);
741
+ const previousActionsText =
742
+ context.previousActions.length > 0
743
+ ? `\nPrevious actions taken:\n${context.previousActions
744
+ .map((a) => `- ${a.action.type}: ${a.action.description} (${a.success ? 'success' : 'failed'})`)
745
+ .join('\n')}`
746
+ : '';
747
+
748
+ const userPrompt = `Goal: ${context.goal}
749
+
750
+ Current page DOM:
751
+ \`\`\`json
752
+ ${domJson}
753
+ \`\`\`
754
+ ${previousActionsText}
755
+
756
+ What action(s) should be taken next?`;
757
+
758
+ return [
759
+ { role: 'system', content: systemPrompt },
760
+ { role: 'user', content: userPrompt },
761
+ ];
762
+ }
763
+
764
+ /**
765
+ * Parse the LLM response into an ActionPlan
766
+ */
767
+ private parseActionPlan(content: string): ActionPlan {
768
+ try {
769
+ // Try to extract JSON from the response
770
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
771
+ if (!jsonMatch) {
772
+ throw new Error('No JSON found in response');
773
+ }
774
+
775
+ const parsed = JSON.parse(jsonMatch[0]);
776
+
777
+ // Validate required fields
778
+ if (!parsed.reasoning || !Array.isArray(parsed.actions)) {
779
+ throw new Error('Invalid action plan structure');
780
+ }
781
+
782
+ return {
783
+ reasoning: parsed.reasoning,
784
+ expectedOutcome: parsed.expectedOutcome || '',
785
+ actions: parsed.actions,
786
+ status: parsed.status || 'in_progress',
787
+ error: parsed.error,
788
+ };
789
+ } catch (error) {
790
+ return {
791
+ reasoning: 'Failed to parse LLM response',
792
+ expectedOutcome: 'None',
793
+ actions: [],
794
+ status: 'error',
795
+ error: `Parse error: ${error instanceof Error ? error.message : 'unknown'}`,
796
+ };
797
+ }
798
+ }
799
+ }
800
+ ```
801
+
802
+ - [ ] **Step 4: Run tests**
803
+
804
+ ```bash
805
+ npm test src/agent/claude-client.test.ts
806
+ ```
807
+
808
+ Expected: PASS
809
+
810
+ - [ ] **Step 5: Commit**
811
+
812
+ ```bash
813
+ git add src/agent/claude-client.ts src/agent/claude-client.test.ts
814
+ git commit -m "feat(agent): implement Claude API client"
815
+ ```
816
+
817
+ ---
818
+
819
+ ## Chunk 4: Action Executor
820
+
821
+ **Purpose:** Execute action plans using Playwright.
822
+
823
+ **Files:**
824
+ - Create: `src/agent/action-executor.ts`
825
+ - Test: `src/agent/action-executor.test.ts`
826
+
827
+ ---
828
+
829
+ ### Task 6: Create Action Executor
830
+
831
+ - [ ] **Step 1: Write the failing test**
832
+
833
+ Create `src/agent/action-executor.test.ts`:
834
+
835
+ ```typescript
836
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
837
+ import { ActionExecutor } from './action-executor';
838
+ import type { Page } from 'playwright';
839
+ import type { DOMRepresentation, Action } from './types';
840
+
841
+ describe('ActionExecutor', () => {
842
+ let executor: ActionExecutor;
843
+ let mockPage: Partial<Page>;
844
+ let mockDom: DOMRepresentation;
845
+
846
+ beforeEach(() => {
847
+ mockPage = {
848
+ click: vi.fn().mockResolvedValue(undefined),
849
+ fill: vi.fn().mockResolvedValue(undefined),
850
+ waitForTimeout: vi.fn().mockResolvedValue(undefined),
851
+ goto: vi.fn().mockResolvedValue(undefined),
852
+ };
853
+
854
+ mockDom = {
855
+ url: 'https://linkedin.com/in/test',
856
+ title: 'Test | LinkedIn',
857
+ elements: [
858
+ {
859
+ id: 'elem-1',
860
+ tag: 'button',
861
+ text: 'Connect',
862
+ type: 'button',
863
+ visible: true,
864
+ enabled: true,
865
+ bbox: { x: 100, y: 200, width: 80, height: 40 },
866
+ },
867
+ ],
868
+ metadata: {
869
+ pageType: 'profile',
870
+ statusMessages: [],
871
+ },
872
+ };
873
+
874
+ executor = new ActionExecutor(mockPage as Page, mockDom);
875
+ });
876
+
877
+ it('should execute click action', async () => {
878
+ const action: Action = {
879
+ type: 'click',
880
+ elementId: 'elem-1',
881
+ description: 'Click Connect button',
882
+ };
883
+
884
+ const result = await executor.execute(action);
885
+
886
+ expect(result.success).toBe(true);
887
+ expect(mockPage.click).toHaveBeenCalledWith(140, 220); // center of bbox
888
+ });
889
+
890
+ it('should return error for unknown element', async () => {
891
+ const action: Action = {
892
+ type: 'click',
893
+ elementId: 'unknown',
894
+ description: 'Click unknown',
895
+ };
896
+
897
+ const result = await executor.execute(action);
898
+
899
+ expect(result.success).toBe(false);
900
+ expect(result.error).toContain('not found');
901
+ });
902
+ });
903
+ ```
904
+
905
+ - [ ] **Step 2: Run test to verify it fails**
906
+
907
+ ```bash
908
+ npm test src/agent/action-executor.test.ts
909
+ ```
910
+
911
+ Expected: FAIL
912
+
913
+ - [ ] **Step 3: Implement action executor**
914
+
915
+ Create `src/agent/action-executor.ts`:
916
+
917
+ ```typescript
918
+ import type { Page } from 'playwright';
919
+ import type { Action, DOMRepresentation, ExecutedAction } from './types';
920
+
921
+ export interface ExecutionResult {
922
+ success: boolean;
923
+ error?: string;
924
+ }
925
+
926
+ export class ActionExecutor {
927
+ private page: Page;
928
+ private dom: DOMRepresentation;
929
+
930
+ constructor(page: Page, dom: DOMRepresentation) {
931
+ this.page = page;
932
+ this.dom = dom;
933
+ }
934
+
935
+ /**
936
+ * Execute a single action
937
+ */
938
+ async execute(action: Action): Promise<ExecutionResult> {
939
+ try {
940
+ switch (action.type) {
941
+ case 'click':
942
+ return await this.executeClick(action.elementId);
943
+ case 'type':
944
+ return await this.executeType(action.elementId, action.text);
945
+ case 'wait':
946
+ return await this.executeWait(action.durationMs);
947
+ case 'navigate':
948
+ return await this.executeNavigate(action.url);
949
+ default:
950
+ return { success: false, error: `Unknown action type: ${(action as any).type}` };
951
+ }
952
+ } catch (error) {
953
+ return {
954
+ success: false,
955
+ error: error instanceof Error ? error.message : 'Unknown error',
956
+ };
957
+ }
958
+ }
959
+
960
+ /**
961
+ * Execute a click on an element by ID
962
+ */
963
+ private async executeClick(elementId: string): Promise<ExecutionResult> {
964
+ const element = this.dom.elements.find((el) => el.id === elementId);
965
+
966
+ if (!element) {
967
+ return { success: false, error: `Element ${elementId} not found in DOM` };
968
+ }
969
+
970
+ // Calculate center of bounding box
971
+ const centerX = element.bbox.x + element.bbox.width / 2;
972
+ const centerY = element.bbox.y + element.bbox.height / 2;
973
+
974
+ // Use Playwright's mouse click at coordinates
975
+ await this.page.mouse.click(centerX, centerY);
976
+
977
+ return { success: true };
978
+ }
979
+
980
+ /**
981
+ * Execute typing into an input element
982
+ */
983
+ private async executeType(elementId: string, text: string): Promise<ExecutionResult> {
984
+ const element = this.dom.elements.find((el) => el.id === elementId);
985
+
986
+ if (!element) {
987
+ return { success: false, error: `Element ${elementId} not found in DOM` };
988
+ }
989
+
990
+ // Click first to focus
991
+ const clickResult = await this.executeClick(elementId);
992
+ if (!clickResult.success) {
993
+ return clickResult;
994
+ }
995
+
996
+ // Wait a bit for focus
997
+ await this.page.waitForTimeout(100);
998
+
999
+ // Type the text
1000
+ await this.page.keyboard.type(text);
1001
+
1002
+ return { success: true };
1003
+ }
1004
+
1005
+ /**
1006
+ * Execute wait action
1007
+ */
1008
+ private async executeWait(durationMs: number): Promise<ExecutionResult> {
1009
+ await this.page.waitForTimeout(durationMs);
1010
+ return { success: true };
1011
+ }
1012
+
1013
+ /**
1014
+ * Execute navigation
1015
+ */
1016
+ private async executeNavigate(url: string): Promise<ExecutionResult> {
1017
+ await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
1018
+ return { success: true };
1019
+ }
1020
+
1021
+ /**
1022
+ * Execute multiple actions in sequence
1023
+ */
1024
+ async executeAll(actions: Action[]): Promise<ExecutedAction[]> {
1025
+ const executed: ExecutedAction[] = [];
1026
+
1027
+ for (const action of actions) {
1028
+ console.log(`Executing: ${action.description}`);
1029
+ const result = await this.execute(action);
1030
+
1031
+ executed.push({
1032
+ action,
1033
+ success: result.success,
1034
+ timestamp: Date.now(),
1035
+ error: result.error,
1036
+ });
1037
+
1038
+ if (!result.success) {
1039
+ console.error(`Action failed: ${result.error}`);
1040
+ break;
1041
+ }
1042
+
1043
+ // Small delay between actions
1044
+ await this.page.waitForTimeout(500);
1045
+ }
1046
+
1047
+ return executed;
1048
+ }
1049
+ }
1050
+ ```
1051
+
1052
+ - [ ] **Step 4: Run tests**
1053
+
1054
+ ```bash
1055
+ npm test src/agent/action-executor.test.ts
1056
+ ```
1057
+
1058
+ Expected: PASS
1059
+
1060
+ - [ ] **Step 5: Commit**
1061
+
1062
+ ```bash
1063
+ git add src/agent/action-executor.ts src/agent/action-executor.test.ts
1064
+ git commit -m "feat(agent): implement action executor"
1065
+ ```
1066
+
1067
+ ---
1068
+
1069
+ ## Chunk 5: Page Agent Orchestrator
1070
+
1071
+ **Purpose:** Main orchestrator that combines all components.
1072
+
1073
+ **Files:**
1074
+ - Create: `src/agent/page-agent.ts`
1075
+
1076
+ ---
1077
+
1078
+ ### Task 7: Create Page Agent
1079
+
1080
+ - [ ] **Step 1: Implement Page Agent**
1081
+
1082
+ Create `src/agent/page-agent.ts`:
1083
+
1084
+ ```typescript
1085
+ import type { Page } from 'playwright';
1086
+ import { DOMExtractor } from './dom-extractor';
1087
+ import { ClaudeClient } from './claude-client';
1088
+ import { ActionExecutor } from './action-executor';
1089
+ import type {
1090
+ Task,
1091
+ TaskResult,
1092
+ ConnectionResult,
1093
+ MessageResult,
1094
+ ActionContext,
1095
+ ExecutedAction,
1096
+ ClaudeClientConfig,
1097
+ } from './types';
1098
+
1099
+ export interface PageAgentOptions {
1100
+ claudeConfig: ClaudeClientConfig;
1101
+ maxRetries?: number;
1102
+ debug?: boolean;
1103
+ }
1104
+
1105
+ export class PageAgent {
1106
+ private domExtractor: DOMExtractor;
1107
+ private claudeClient: ClaudeClient;
1108
+ private options: PageAgentOptions;
1109
+
1110
+ constructor(options: PageAgentOptions) {
1111
+ this.domExtractor = new DOMExtractor();
1112
+ this.claudeClient = new ClaudeClient(options.claudeConfig);
1113
+ this.options = {
1114
+ maxRetries: 3,
1115
+ debug: false,
1116
+ ...options,
1117
+ };
1118
+ }
1119
+
1120
+ /**
1121
+ * Execute a task on a LinkedIn page
1122
+ */
1123
+ async execute(task: Task, page: Page): Promise<TaskResult> {
1124
+ const goal = this.buildGoal(task);
1125
+ const actions: ExecutedAction[] = [];
1126
+ let retryCount = 0;
1127
+
1128
+ while (retryCount < this.options.maxRetries!) {
1129
+ // Extract current DOM state
1130
+ const dom = await this.domExtractor.extract(page);
1131
+
1132
+ if (this.options.debug) {
1133
+ console.log('Extracted DOM:', JSON.stringify(dom, null, 2));
1134
+ }
1135
+
1136
+ // Build context
1137
+ const context: ActionContext = {
1138
+ goal,
1139
+ previousActions: actions,
1140
+ retryCount,
1141
+ };
1142
+
1143
+ // Get action plan from Claude
1144
+ let plan;
1145
+ try {
1146
+ plan = await this.claudeClient.generateActionPlan(dom, context);
1147
+ } catch (error) {
1148
+ return {
1149
+ success: false,
1150
+ message: `Failed to get action plan: ${error instanceof Error ? error.message : 'unknown'}`,
1151
+ actionsTaken: actions.map((a) => a.action),
1152
+ };
1153
+ }
1154
+
1155
+ if (this.options.debug) {
1156
+ console.log('Action plan:', plan);
1157
+ }
1158
+
1159
+ // Check if completed
1160
+ if (plan.status === 'completed') {
1161
+ return {
1162
+ success: true,
1163
+ message: plan.reasoning || 'Task completed',
1164
+ actionsTaken: actions.map((a) => a.action),
1165
+ finalUrl: page.url(),
1166
+ };
1167
+ }
1168
+
1169
+ // Check for error
1170
+ if (plan.status === 'error') {
1171
+ return {
1172
+ success: false,
1173
+ message: plan.error || 'Unknown error',
1174
+ actionsTaken: actions.map((a) => a.action),
1175
+ };
1176
+ }
1177
+
1178
+ // Execute actions
1179
+ const executor = new ActionExecutor(page, dom);
1180
+ const executed = await executor.executeAll(plan.actions);
1181
+ actions.push(...executed);
1182
+
1183
+ // Check if any action failed
1184
+ const failedAction = executed.find((e) => !e.success);
1185
+ if (failedAction) {
1186
+ retryCount++;
1187
+ if (retryCount >= this.options.maxRetries!) {
1188
+ return {
1189
+ success: false,
1190
+ message: `Failed after ${retryCount} retries: ${failedAction.error}`,
1191
+ actionsTaken: actions.map((a) => a.action),
1192
+ };
1193
+ }
1194
+ // Wait before retry
1195
+ await page.waitForTimeout(2000);
1196
+ }
1197
+
1198
+ // Wait for page to stabilize
1199
+ await page.waitForTimeout(1000);
1200
+ }
1201
+
1202
+ return {
1203
+ success: false,
1204
+ message: `Exceeded maximum retries (${this.options.maxRetries})`,
1205
+ actionsTaken: actions.map((a) => a.action),
1206
+ };
1207
+ }
1208
+
1209
+ /**
1210
+ * Send connection request to a profile
1211
+ */
1212
+ async connect(profileUrl: string, note: string | undefined, page: Page): Promise<ConnectionResult> {
1213
+ // Navigate to profile first
1214
+ await page.goto(profileUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
1215
+ await page.waitForTimeout(3000);
1216
+
1217
+ const task: Task = {
1218
+ goal: note
1219
+ ? `Send connection request with note: "${note}"`
1220
+ : 'Send connection request',
1221
+ profileUrl,
1222
+ note,
1223
+ };
1224
+
1225
+ const result = await this.execute(task, page);
1226
+
1227
+ // Check final state
1228
+ const dom = await this.domExtractor.extract(page);
1229
+
1230
+ return {
1231
+ ...result,
1232
+ sent: result.success && dom.metadata.connectionState === 'pending',
1233
+ pending: dom.metadata.connectionState === 'pending',
1234
+ alreadyConnected: dom.metadata.connectionState === 'connected',
1235
+ };
1236
+ }
1237
+
1238
+ /**
1239
+ * Send message to a profile
1240
+ */
1241
+ async sendMessage(profileUrl: string, message: string, page: Page): Promise<MessageResult> {
1242
+ // Navigate to profile first
1243
+ await page.goto(profileUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
1244
+ await page.waitForTimeout(3000);
1245
+
1246
+ const task: Task = {
1247
+ goal: `Send message: "${message}"`,
1248
+ profileUrl,
1249
+ message,
1250
+ };
1251
+
1252
+ const result = await this.execute(task, page);
1253
+
1254
+ return {
1255
+ ...result,
1256
+ sent: result.success,
1257
+ };
1258
+ }
1259
+
1260
+ /**
1261
+ * Build natural language goal from task
1262
+ */
1263
+ private buildGoal(task: Task): string {
1264
+ if (task.goal) {
1265
+ return task.goal;
1266
+ }
1267
+ if (task.note) {
1268
+ return `Send connection request to ${task.profileUrl} with note: "${task.note}"`;
1269
+ }
1270
+ if (task.message) {
1271
+ return `Send message to ${task.profileUrl}: "${task.message}"`;
1272
+ }
1273
+ return 'Complete task on LinkedIn';
1274
+ }
1275
+ }
1276
+ ```
1277
+
1278
+ - [ ] **Step 2: Commit**
1279
+
1280
+ ```bash
1281
+ git add src/agent/page-agent.ts
1282
+ git commit -m "feat(agent): implement page agent orchestrator"
1283
+ ```
1284
+
1285
+ ---
1286
+
1287
+ ## Chunk 6: Module Exports
1288
+
1289
+ **Purpose:** Export public API from agent module.
1290
+
1291
+ **Files:**
1292
+ - Create: `src/agent/index.ts`
1293
+
1294
+ ---
1295
+
1296
+ ### Task 8: Create Module Index
1297
+
1298
+ - [ ] **Step 1: Create index.ts**
1299
+
1300
+ Create `src/agent/index.ts`:
1301
+
1302
+ ```typescript
1303
+ // Types
1304
+ export type {
1305
+ Action,
1306
+ ActionContext,
1307
+ ActionPlan,
1308
+ BoundingBox,
1309
+ ClaudeClientConfig,
1310
+ ClickAction,
1311
+ ConnectionResult,
1312
+ DOMElement,
1313
+ DOMRepresentation,
1314
+ ExecutedAction,
1315
+ MessageResult,
1316
+ NavigateAction,
1317
+ PageMetadata,
1318
+ Task,
1319
+ TaskResult,
1320
+ TypeAction,
1321
+ WaitAction,
1322
+ } from './types';
1323
+
1324
+ // Classes
1325
+ export { DOMExtractor } from './dom-extractor';
1326
+ export { ClaudeClient } from './claude-client';
1327
+ export { ActionExecutor } from './action-executor';
1328
+ export { PageAgent } from './page-agent';
1329
+ ```
1330
+
1331
+ - [ ] **Step 2: Commit**
1332
+
1333
+ ```bash
1334
+ git add src/agent/index.ts
1335
+ git commit -m "feat(agent): add module exports"
1336
+ ```
1337
+
1338
+ ---
1339
+
1340
+ ## Chunk 7: CLI Commands
1341
+
1342
+ **Purpose:** Add CLI commands for the agent.
1343
+
1344
+ **Files:**
1345
+ - Create: `src/cli/agent-commands.ts`
1346
+ - Modify: `src/cli/index.ts` (add export)
1347
+ - Modify: `src/index.ts` (register commands)
1348
+
1349
+ ---
1350
+
1351
+ ### Task 9: Create Agent CLI Commands
1352
+
1353
+ - [ ] **Step 1: Create agent commands**
1354
+
1355
+ Create `src/cli/agent-commands.ts`:
1356
+
1357
+ ```typescript
1358
+ import { Command } from 'commander';
1359
+ import chalk from 'chalk';
1360
+ import { PageAgent } from '../agent';
1361
+ import { createBrowser } from '../core/browser';
1362
+ import { getConfig } from '../core/config';
1363
+
1364
+ export function registerAgentCommands(program: Command): void {
1365
+ const agentCmd = program
1366
+ .command('agent')
1367
+ .description('AI-powered LinkedIn automation using DOM-text agent');
1368
+
1369
+ // Connect command
1370
+ agentCmd
1371
+ .command('connect <url>')
1372
+ .description('Send connection request using AI agent')
1373
+ .option('--note <text>', 'Personal note to include')
1374
+ .option('--profile <name>', 'Browser profile to use')
1375
+ .option('--debug', 'Enable debug output')
1376
+ .action(async (url, options) => {
1377
+ const config = getConfig();
1378
+ const apiKey = process.env.DASHSCOPE_API_KEY;
1379
+
1380
+ if (!apiKey) {
1381
+ console.error(chalk.red('Error: DASHSCOPE_API_KEY environment variable not set'));
1382
+ console.log('Set it with: export DASHSCOPE_API_KEY=your_key');
1383
+ process.exit(1);
1384
+ }
1385
+
1386
+ console.log(chalk.blue('Starting browser...'));
1387
+ const browser = await createBrowser({
1388
+ headless: config.get().headless,
1389
+ debug: options.debug,
1390
+ });
1391
+
1392
+ try {
1393
+ const agent = new PageAgent({
1394
+ claudeConfig: {
1395
+ apiKey,
1396
+ baseUrl: process.env.DASHSCOPE_BASE_URL || 'https://coding.dashscope.aliyuncs.com/apps/anthropic/v1',
1397
+ model: process.env.DASHSCOPE_MODEL || 'qwen3.5-plus',
1398
+ },
1399
+ debug: options.debug,
1400
+ });
1401
+
1402
+ console.log(chalk.blue(`Navigating to ${url}...`));
1403
+ const page = browser.getPage()!;
1404
+
1405
+ const result = await agent.connect(url, options.note, page);
1406
+
1407
+ if (result.success) {
1408
+ console.log(chalk.green('✓ Connection request sent successfully'));
1409
+ if (result.alreadyConnected) {
1410
+ console.log(chalk.yellow('Note: Already connected with this person'));
1411
+ }
1412
+ } else {
1413
+ console.error(chalk.red(`✗ Failed: ${result.message}`));
1414
+ process.exit(1);
1415
+ }
1416
+ } finally {
1417
+ await browser.close();
1418
+ }
1419
+ });
1420
+
1421
+ // Message command
1422
+ agentCmd
1423
+ .command('message <url>')
1424
+ .description('Send message using AI agent')
1425
+ .requiredOption('--message <text>', 'Message to send')
1426
+ .option('--profile <name>', 'Browser profile to use')
1427
+ .option('--debug', 'Enable debug output')
1428
+ .action(async (url, options) => {
1429
+ const config = getConfig();
1430
+ const apiKey = process.env.DASHSCOPE_API_KEY;
1431
+
1432
+ if (!apiKey) {
1433
+ console.error(chalk.red('Error: DASHSCOPE_API_KEY environment variable not set'));
1434
+ console.log('Set it with: export DASHSCOPE_API_KEY=your_key');
1435
+ process.exit(1);
1436
+ }
1437
+
1438
+ console.log(chalk.blue('Starting browser...'));
1439
+ const browser = await createBrowser({
1440
+ headless: config.get().headless,
1441
+ debug: options.debug,
1442
+ });
1443
+
1444
+ try {
1445
+ const agent = new PageAgent({
1446
+ claudeConfig: {
1447
+ apiKey,
1448
+ baseUrl: process.env.DASHSCOPE_BASE_URL || 'https://coding.dashscope.aliyuncs.com/apps/anthropic/v1',
1449
+ model: process.env.DASHSCOPE_MODEL || 'qwen3.5-plus',
1450
+ },
1451
+ debug: options.debug,
1452
+ });
1453
+
1454
+ console.log(chalk.blue(`Navigating to ${url}...`));
1455
+ const page = browser.getPage()!;
1456
+
1457
+ const result = await agent.sendMessage(url, options.message, page);
1458
+
1459
+ if (result.success) {
1460
+ console.log(chalk.green('✓ Message sent successfully'));
1461
+ } else {
1462
+ console.error(chalk.red(`✗ Failed: ${result.message}`));
1463
+ process.exit(1);
1464
+ }
1465
+ } finally {
1466
+ await browser.close();
1467
+ }
1468
+ });
1469
+ }
1470
+ ```
1471
+
1472
+ - [ ] **Step 2: Update CLI index**
1473
+
1474
+ Modify `src/cli/index.ts` to add export:
1475
+
1476
+ ```typescript
1477
+ export { registerAuthCommands } from './auth';
1478
+ export { registerMessageCommands } from './messages';
1479
+ export { registerReplyCommands } from './reply';
1480
+ export { registerConnectionCommands } from './connections';
1481
+ export { registerAgentCommands } from './agent-commands';
1482
+ ```
1483
+
1484
+ - [ ] **Step 3: Register commands in main entry**
1485
+
1486
+ Modify `src/index.ts` to add:
1487
+
1488
+ ```typescript
1489
+ import {
1490
+ registerAuthCommands,
1491
+ registerMessageCommands,
1492
+ registerReplyCommands,
1493
+ registerConnectionCommands,
1494
+ registerAgentCommands,
1495
+ } from './cli';
1496
+
1497
+ // ... existing code ...
1498
+
1499
+ // Register command groups
1500
+ registerAuthCommands(program);
1501
+ registerMessageCommands(program);
1502
+ registerReplyCommands(program);
1503
+ registerConnectionCommands(program);
1504
+ registerAgentCommands(program); // Add this line
1505
+ ```
1506
+
1507
+ - [ ] **Step 4: Commit**
1508
+
1509
+ ```bash
1510
+ git add src/cli/agent-commands.ts src/cli/index.ts src/index.ts
1511
+ git commit -m "feat(cli): add agent commands for connect and message"
1512
+ ```
1513
+
1514
+ ---
1515
+
1516
+ ## Chunk 8: Environment Configuration
1517
+
1518
+ **Purpose:** Document environment variables.
1519
+
1520
+ **Files:**
1521
+ - Create: `.env.example`
1522
+
1523
+ ---
1524
+
1525
+ ### Task 10: Create Environment Example
1526
+
1527
+ - [ ] **Step 1: Create .env.example**
1528
+
1529
+ Create `.env.example`:
1530
+
1531
+ ```bash
1532
+ # Dashscope Claude API Configuration
1533
+ # Get your API key from: https://dashscope.aliyun.com/
1534
+ DASHSCOPE_API_KEY=your_key_here
1535
+ DASHSCOPE_BASE_URL=https://coding.dashscope.aliyuncs.com/apps/anthropic/v1
1536
+ DASHSCOPE_MODEL=qwen3.5-plus
1537
+ ```
1538
+
1539
+ - [ ] **Step 2: Commit**
1540
+
1541
+ ```bash
1542
+ git add .env.example
1543
+ git commit -m "docs: add environment configuration example"
1544
+ ```
1545
+
1546
+ ---
1547
+
1548
+ ## Final Steps
1549
+
1550
+ ### Task 11: Build and Test
1551
+
1552
+ - [ ] **Step 1: Build the project**
1553
+
1554
+ ```bash
1555
+ npm run build
1556
+ ```
1557
+
1558
+ Expected: Build completes without errors
1559
+
1560
+ - [ ] **Step 2: Run all tests**
1561
+
1562
+ ```bash
1563
+ npm test
1564
+ ```
1565
+
1566
+ Expected: All tests pass
1567
+
1568
+ - [ ] **Step 3: Final commit**
1569
+
1570
+ ```bash
1571
+ git commit -m "feat: complete Page Agent integration"
1572
+ ```
1573
+
1574
+ ---
1575
+
1576
+ ## Usage Instructions
1577
+
1578
+ After implementation, users can:
1579
+
1580
+ ```bash
1581
+ # Set up environment
1582
+ export DASHSCOPE_API_KEY=your_key
1583
+
1584
+ # Send connection request
1585
+ linkedin agent connect https://www.linkedin.com/in/alice --note "Hi Alice!"
1586
+
1587
+ # Send message
1588
+ linkedin agent message https://www.linkedin.com/in/alice --message "Thanks for connecting!"
1589
+
1590
+ # Debug mode
1591
+ linkedin agent connect https://www.linkedin.com/in/alice --debug
1592
+ ```
1593
+
1594
+ ---
1595
+
1596
+ ## Implementation Complete
1597
+
1598
+ Plan complete and saved to `docs/superpowers/plans/2026-03-14-page-agent-plan.md`. Ready to execute?