Skip to content

Commit

Permalink
release-app
Browse files Browse the repository at this point in the history
  • Loading branch information
louis030195 committed Jan 11, 2025
1 parent 50f0265 commit 9855a98
Show file tree
Hide file tree
Showing 83 changed files with 16,002 additions and 1 deletion.
3 changes: 3 additions & 0 deletions pipes/linkedin-ai-assistant/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
65 changes: 65 additions & 0 deletions pipes/linkedin-ai-assistant/.gitignore
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
21 changes: 21 additions & 0 deletions pipes/linkedin-ai-assistant/README.md
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 pipes/linkedin-ai-assistant/app/api/chrome/check-login/route.ts
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 pipes/linkedin-ai-assistant/app/api/chrome/navigate/route.ts
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 });
}
}
106 changes: 106 additions & 0 deletions pipes/linkedin-ai-assistant/app/api/chrome/route.ts
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 pipes/linkedin-ai-assistant/app/api/chrome/status/route.ts
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 });
}
}
Loading

0 comments on commit 9855a98

Please sign in to comment.