linkedin-automation-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,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?
|