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