prospect-ai-agent 1.0.0 → 1.0.2
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/dist/executor/browser.js +17 -7
- package/dist/executor/comment.js +98 -28
- package/dist/index.js +1 -1
- package/dist/login.js +15 -1
- package/package.json +3 -2
package/dist/executor/browser.js
CHANGED
|
@@ -6,13 +6,23 @@ export class BrowserManager {
|
|
|
6
6
|
context = null;
|
|
7
7
|
async launchForTask(cookies) {
|
|
8
8
|
log.info("Launching headless Chromium for task...");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
try {
|
|
10
|
+
this.browser = await chromium.launch({
|
|
11
|
+
headless: true,
|
|
12
|
+
args: [
|
|
13
|
+
"--disable-blink-features=AutomationControlled",
|
|
14
|
+
"--no-sandbox",
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const msg = err.message;
|
|
20
|
+
if (msg.includes("Executable doesn't exist") || msg.includes("browserType.launch")) {
|
|
21
|
+
log.error("Chromium browser is not installed. Run: npx playwright install chromium");
|
|
22
|
+
throw new Error("Chromium not installed");
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
16
26
|
this.context = await this.browser.newContext({
|
|
17
27
|
viewport: { width: 1280, height: 900 },
|
|
18
28
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
package/dist/executor/comment.js
CHANGED
|
@@ -7,43 +7,113 @@ export async function executeComment(payload, page) {
|
|
|
7
7
|
log.job("comment", `Navigating to post: ${postUrl}`);
|
|
8
8
|
await page.goto(postUrl, { waitUntil: "domcontentloaded" });
|
|
9
9
|
await page.waitForTimeout(3000 + Math.random() * 2000);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// On a single post page, the comment box may already be visible.
|
|
11
|
+
// If not, click the "Comment" action button to reveal it.
|
|
12
|
+
let commentBox = await findCommentBox(page);
|
|
13
|
+
if (!commentBox) {
|
|
14
|
+
log.job("comment", "Comment box not visible, clicking Comment button...");
|
|
15
|
+
const commentButton = page.locator('button[aria-label*="Comment" i], span:text-is("Comment")').first();
|
|
16
|
+
const btnVisible = await commentButton.isVisible().catch(() => false);
|
|
17
|
+
if (btnVisible) {
|
|
18
|
+
await commentButton.click();
|
|
19
|
+
await page.waitForTimeout(2000);
|
|
20
|
+
}
|
|
21
|
+
commentBox = await findCommentBox(page);
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
const boxVisible = await commentBox.isVisible().catch(() => false);
|
|
18
|
-
if (!boxVisible) {
|
|
23
|
+
if (!commentBox) {
|
|
19
24
|
return {
|
|
20
25
|
success: false,
|
|
21
|
-
error: "Comment box not found
|
|
26
|
+
error: "Comment box not found on this post page",
|
|
22
27
|
};
|
|
23
28
|
}
|
|
29
|
+
// Focus and type the comment character by character for reliability
|
|
24
30
|
await commentBox.click();
|
|
25
31
|
await page.waitForTimeout(500);
|
|
26
|
-
|
|
27
|
-
await page.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// Clear any existing content first
|
|
33
|
+
await page.keyboard.press("Meta+a");
|
|
34
|
+
await page.waitForTimeout(200);
|
|
35
|
+
// Type character by character to work with contenteditable divs
|
|
36
|
+
await page.keyboard.type(comment, { delay: 30 + Math.random() * 20 });
|
|
37
|
+
await page.waitForTimeout(1500 + Math.random() * 1000);
|
|
38
|
+
log.job("comment", "Comment typed, looking for submit button...");
|
|
39
|
+
// LinkedIn's submit button inside the comment form - try multiple selectors
|
|
40
|
+
// The button text is "Post" (not "Comment") and it's inside the comment form
|
|
41
|
+
const submitSelectors = [
|
|
42
|
+
'form.comments-comment-box__form button.comments-comment-box__submit-button',
|
|
43
|
+
'button.comments-comment-box__submit-button',
|
|
44
|
+
'button.comments-comment-box-comment__submit-button',
|
|
45
|
+
'div.comments-comment-box button[type="submit"]',
|
|
46
|
+
'button:has-text("Post"):near(.ql-editor)',
|
|
47
|
+
];
|
|
48
|
+
let submitted = false;
|
|
49
|
+
for (const selector of submitSelectors) {
|
|
50
|
+
const btn = page.locator(selector).first();
|
|
51
|
+
const visible = await btn.isVisible().catch(() => false);
|
|
52
|
+
if (visible) {
|
|
53
|
+
const disabled = await btn.isDisabled().catch(() => true);
|
|
54
|
+
if (!disabled) {
|
|
55
|
+
log.job("comment", `Found submit button: ${selector}`);
|
|
56
|
+
await btn.scrollIntoViewIfNeeded().catch(() => { });
|
|
57
|
+
await page.waitForTimeout(300);
|
|
58
|
+
await btn.click({ force: true });
|
|
59
|
+
submitted = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
32
63
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
// Fallback: try Ctrl+Enter / Meta+Enter to submit
|
|
65
|
+
if (!submitted) {
|
|
66
|
+
log.job("comment", "No submit button found, trying Ctrl+Enter...");
|
|
67
|
+
await page.keyboard.press("Control+Enter");
|
|
68
|
+
submitted = true;
|
|
69
|
+
}
|
|
70
|
+
// Wait for the comment to be posted
|
|
71
|
+
await page.waitForTimeout(4000);
|
|
72
|
+
// Verify: check if the page shows our comment was posted
|
|
73
|
+
// After successful submission, LinkedIn clears the comment box
|
|
74
|
+
const boxAfter = await findCommentBox(page);
|
|
75
|
+
if (boxAfter) {
|
|
76
|
+
const remainingText = await boxAfter
|
|
77
|
+
.evaluate((el) => el.innerText.trim())
|
|
78
|
+
.catch(() => "");
|
|
79
|
+
if (remainingText.length > 0 && remainingText === comment) {
|
|
80
|
+
// Try one more time with force click on any visible Post/Submit button
|
|
81
|
+
log.job("comment", "First submit attempt may have failed, retrying...");
|
|
82
|
+
const retryBtn = page.locator('button:has-text("Post"), button[type="submit"]').last();
|
|
83
|
+
const retryVisible = await retryBtn.isVisible().catch(() => false);
|
|
84
|
+
if (retryVisible) {
|
|
85
|
+
await retryBtn.click({ force: true });
|
|
86
|
+
await page.waitForTimeout(4000);
|
|
87
|
+
}
|
|
88
|
+
const finalText = await boxAfter
|
|
89
|
+
.evaluate((el) => el.innerText.trim())
|
|
90
|
+
.catch(() => "");
|
|
91
|
+
if (finalText.length > 0 && finalText === comment) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: "Comment was typed but submit failed — text still in box after retries",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
46
98
|
}
|
|
47
99
|
log.success(`Comment posted on ${postUrl}`);
|
|
48
100
|
return { success: true };
|
|
49
101
|
}
|
|
102
|
+
async function findCommentBox(page) {
|
|
103
|
+
const selectors = [
|
|
104
|
+
'div.ql-editor[data-placeholder*="comment" i]',
|
|
105
|
+
'div.ql-editor[data-placeholder*="Add a comment" i]',
|
|
106
|
+
'.comments-comment-box__form .ql-editor',
|
|
107
|
+
'.comments-comment-texteditor .ql-editor',
|
|
108
|
+
'div[role="textbox"][contenteditable="true"][aria-label*="comment" i]',
|
|
109
|
+
'div.ql-editor[contenteditable="true"]',
|
|
110
|
+
];
|
|
111
|
+
for (const selector of selectors) {
|
|
112
|
+
const el = page.locator(selector).first();
|
|
113
|
+
const visible = await el.isVisible().catch(() => false);
|
|
114
|
+
if (visible) {
|
|
115
|
+
return el;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ApiClient } from "./api-client.js";
|
|
|
6
6
|
import { Agent } from "./agent.js";
|
|
7
7
|
import { runLogin } from "./login.js";
|
|
8
8
|
import { log } from "./logger.js";
|
|
9
|
-
const VERSION = "1.0.
|
|
9
|
+
const VERSION = "1.0.2";
|
|
10
10
|
const DEFAULT_SERVER = "http://localhost:3000";
|
|
11
11
|
const program = new Command();
|
|
12
12
|
program
|
package/dist/login.js
CHANGED
|
@@ -10,7 +10,21 @@ export async function runLogin(serverUrl, token) {
|
|
|
10
10
|
const browser = new BrowserManager();
|
|
11
11
|
log.info("Opening LinkedIn login page in headed browser...");
|
|
12
12
|
log.info("Please log in to your LinkedIn account.\n");
|
|
13
|
-
|
|
13
|
+
let page, context;
|
|
14
|
+
try {
|
|
15
|
+
({ page, context } = await browser.launchHeaded());
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const msg = err.message;
|
|
19
|
+
if (msg.includes("Executable doesn't exist") || msg.includes("browserType.launch")) {
|
|
20
|
+
log.error("Chromium browser is not installed.");
|
|
21
|
+
log.info("Run this command to install it:\n");
|
|
22
|
+
log.info(" npx playwright install chromium\n");
|
|
23
|
+
log.info("Then try 'login' again.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
14
28
|
await page.goto(LINKEDIN_LOGIN_URL, { waitUntil: "domcontentloaded" });
|
|
15
29
|
log.info("Waiting for you to complete login...");
|
|
16
30
|
log.info("(The browser will close automatically once logged in)\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prospect-ai-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "ProspectAI Desktop Agent — browser automation for campaigns (no Chrome extension)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"build:binaries": "npm run build && node build-binaries.mjs",
|
|
24
24
|
"build:mac": "npm run build && npx pkg dist/index.js --target node18-macos-arm64 --output binaries/prospect-ai-agent-macos --compress GZip",
|
|
25
25
|
"build:win": "npm run build && npx pkg dist/index.js --target node18-win-x64 --output binaries/prospect-ai-agent-win.exe --compress GZip",
|
|
26
|
-
"prepublishOnly": "npm run build"
|
|
26
|
+
"prepublishOnly": "npm run build",
|
|
27
|
+
"postinstall": "npx playwright install chromium 2>/dev/null || echo 'Note: Run npx playwright install chromium if browser launch fails.'"
|
|
27
28
|
},
|
|
28
29
|
"pkg": {
|
|
29
30
|
"assets": [
|