-
Notifications
You must be signed in to change notification settings - Fork 777
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
50f0265
commit 9855a98
Showing
83 changed files
with
16,002 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": ["next/core-web-vitals", "next/typescript"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
.next | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts | ||
|
||
# chrome profile | ||
.chrome-profile/ | ||
*.db | ||
*.db-journal | ||
leveldb/ | ||
Local State | ||
Last Version | ||
LOCK | ||
CURRENT | ||
LOG | ||
LOG.old | ||
MANIFEST* | ||
|
||
# chrome specific | ||
*.log | ||
*.journal | ||
Preferences | ||
Session Storage/ | ||
Local Storage/ | ||
IndexedDB/ | ||
WebStorage/ | ||
Cookies* | ||
History* | ||
|
||
# storage files | ||
lib/storage/*.json | ||
!lib/storage/templates.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
Automate your LinkedIn outreach with an AI-powered desktop agent that runs locally on your computer. No password required, works in the background without interfering with your keyboard. Smart targeting finds relevant prospects based on industry and role, while AI generates personalized messages using profile context. Built with safety in mind - uses human-like behavior and rate limiting to protect your account. | ||
|
||
|
||
|
||
https://github.com/user-attachments/assets/c53b1156-b279-4a9d-9325-32bc27fba527 | ||
|
||
|
||
<!-- | ||
<img width="1312" alt="LinkedIn Automation Demo" src="./public/li_harvester.gif"> | ||
--> | ||
|
||
## Features | ||
|
||
- 🛡️ Exceptional safety: Runs as a desktop app with human-like behavior | ||
- 👥 Smart targeting: Finds prospects based on industry & mutual connections | ||
- 🤖 AI messaging: Generates personalized outreach using profile context | ||
- 💬 Drip campaigns: Automated sequences with reply detection | ||
- ⚡ Multi-source targeting: Connect via search, groups, and profile visitors | ||
- 📊 Data extraction: Safely logs profile data and messaging history | ||
|
||
|
40 changes: 40 additions & 0 deletions
40
pipes/linkedin-ai-assistant/app/api/chrome/check-login/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { NextResponse } from 'next/server'; | ||
import { setupBrowser, getActiveBrowser } from '@/lib/browser-setup'; | ||
|
||
export async function POST(request: Request) { | ||
try { | ||
await request.json(); // keep reading the request to avoid hanging | ||
console.log('checking linkedin login status'); | ||
|
||
await setupBrowser(); | ||
const { page } = getActiveBrowser(); | ||
|
||
if (!page) { | ||
throw new Error('no active browser session'); | ||
} | ||
|
||
// Check for elements that indicate logged-in state | ||
const isLoggedIn = await page.evaluate(() => { | ||
// Check for feed-specific elements that only appear when logged in | ||
const feedElements = document.querySelector('.scaffold-layout__main') | ||
const navElements = document.querySelector('.global-nav__me') | ||
|
||
// Return true if we find elements specific to logged-in state | ||
return !!(feedElements || navElements) | ||
}) | ||
|
||
console.log(`login status: ${isLoggedIn ? 'logged in' : 'logged out'}`) | ||
|
||
return NextResponse.json({ | ||
success: true, | ||
isLoggedIn: Boolean(isLoggedIn) | ||
}); | ||
|
||
} catch (error) { | ||
console.error('failed to check login status:', error); | ||
return NextResponse.json( | ||
{ success: false, error: String(error) }, | ||
{ status: 500 } | ||
); | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
pipes/linkedin-ai-assistant/app/api/chrome/navigate/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { NextResponse } from 'next/server'; | ||
import { getActiveBrowser, setupBrowser } from '@/lib/browser-setup'; | ||
import type { Page } from 'puppeteer-core'; | ||
|
||
async function navigateToPage(page: Page, url: string) { | ||
try { | ||
console.log('starting navigation'); | ||
|
||
// Set longer timeout but keep navigation simple | ||
await page.setDefaultNavigationTimeout(60000); | ||
|
||
// Navigate to the target URL with same settings as search navigation | ||
const response = await page.goto(url, { | ||
waitUntil: 'domcontentloaded', | ||
timeout: 60000 | ||
}); | ||
|
||
// Wait for the main content to load | ||
await page.waitForSelector('body', { timeout: 30000 }); | ||
|
||
return { | ||
status: response?.status() || 0, | ||
finalUrl: page.url() | ||
}; | ||
|
||
} catch (error) { | ||
console.error('navigation error:', error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function POST(request: Request) { | ||
try { | ||
const { url } = await request.json(); | ||
console.log('attempting to navigate to:', url); | ||
|
||
// Setup the browser connection | ||
await setupBrowser(); | ||
|
||
const { page } = getActiveBrowser(); | ||
if (!page) { | ||
throw new Error('no active browser session'); | ||
} | ||
|
||
// Perform the navigation | ||
const result = await navigateToPage(page, url); | ||
|
||
// Return a successful response with navigation details | ||
return NextResponse.json({ | ||
success: true, | ||
status: result.status, | ||
finalUrl: result.finalUrl | ||
}); | ||
|
||
} catch (error) { | ||
console.error('failed to navigate:', error); | ||
// Return an error response with details | ||
return NextResponse.json({ | ||
success: false, | ||
error: 'failed to navigate', | ||
details: error instanceof Error ? error.message : String(error) | ||
}, { status: 500 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { NextResponse } from 'next/server'; | ||
import { exec, spawn } from 'child_process'; | ||
import { promisify } from 'util'; | ||
import { quitBrowser } from '@/lib/browser-setup'; | ||
import os from 'os'; | ||
|
||
export const runtime = 'nodejs'; // specify node runtime | ||
|
||
const execPromise = promisify(exec); | ||
|
||
// helper to get chrome path based on platform | ||
function getChromePath() { | ||
switch (os.platform()) { | ||
case 'darwin': | ||
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; | ||
case 'linux': | ||
return '/usr/bin/google-chrome'; | ||
case 'win32': | ||
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'; | ||
default: | ||
throw new Error('unsupported platform'); | ||
} | ||
} | ||
|
||
export async function POST() { | ||
try { | ||
console.log('attempting to launch chrome in', process.env.NODE_ENV); | ||
|
||
await quitChrome(); | ||
await quitBrowser(); | ||
|
||
const chromePath = getChromePath(); | ||
console.log('using chrome path:', chromePath); | ||
|
||
const chromeProcess = spawn(chromePath, [ | ||
'--remote-debugging-port=9222', | ||
'--restore-last-session', | ||
'--no-first-run', | ||
'--no-default-browser-check', | ||
'--no-sandbox', | ||
'--disable-setuid-sandbox', | ||
// Add these flags to help with stability | ||
'--disable-dev-shm-usage', | ||
'--disable-gpu' | ||
], { | ||
detached: true, | ||
stdio: 'ignore' | ||
}); | ||
|
||
chromeProcess.unref(); | ||
|
||
// increase timeout and add retries | ||
let attempts = 0; | ||
const maxAttempts = 5; | ||
|
||
while (attempts < maxAttempts) { | ||
try { | ||
await new Promise(resolve => setTimeout(resolve, 2000)); | ||
const response = await fetch('http://127.0.0.1:9222/json/version'); | ||
if (response.ok) { | ||
console.log('chrome debug port responding'); | ||
return NextResponse.json({ success: true }); | ||
} | ||
} catch (err) { | ||
console.log(`attempt ${attempts + 1} failed:`, err); | ||
attempts++; | ||
if (attempts === maxAttempts) { | ||
throw new Error('failed to connect to chrome debug port after multiple attempts'); | ||
} | ||
} | ||
} | ||
} catch (err) { | ||
console.error('failed to launch chrome:', err); | ||
return NextResponse.json({ | ||
success: false, | ||
error: String(err), | ||
details: err instanceof Error ? err.stack : undefined | ||
}, { status: 500 }); | ||
} | ||
} | ||
|
||
export async function DELETE() { | ||
try { | ||
await quitChrome(); | ||
await quitBrowser(); | ||
console.log('chrome process terminated'); | ||
return NextResponse.json({ success: true }); | ||
} catch (error) { | ||
console.error('failed to kill chrome:', error); | ||
return NextResponse.json({ success: false, error: String(error) }, { status: 500 }); | ||
} | ||
} | ||
|
||
async function quitChrome() { | ||
const platform = os.platform(); | ||
const killCommand = platform === 'win32' | ||
? `taskkill /F /IM chrome.exe` | ||
: `pkill -f -- "Google Chrome"`; | ||
|
||
try { | ||
await execPromise(killCommand); | ||
console.log('chrome killed'); | ||
} catch (error) { | ||
console.log('no chrome process found to kill', error); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
pipes/linkedin-ai-assistant/app/api/chrome/status/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { NextResponse } from 'next/server'; | ||
|
||
export const runtime = 'nodejs'; | ||
|
||
export async function GET() { | ||
try { | ||
const response = await fetch('http://127.0.0.1:9222/json/version'); | ||
if (!response.ok) { | ||
return NextResponse.json({ status: 'not_connected' }, { status: 200 }); | ||
} | ||
const data = await response.json() as { webSocketDebuggerUrl: string }; | ||
|
||
const wsUrl = data.webSocketDebuggerUrl.replace('ws://localhost:', 'ws://127.0.0.1:'); | ||
|
||
return NextResponse.json({ | ||
wsUrl, | ||
status: 'connected' | ||
}); | ||
} catch { | ||
return NextResponse.json({ status: 'not_connected' }, { status: 200 }); | ||
} | ||
} |
Oops, something went wrong.