diff --git a/pipes/pipe-linkedin-ai-assistant/.eslintrc.json b/pipes/pipe-linkedin-ai-assistant/.eslintrc.json deleted file mode 100644 index 372241854..000000000 --- a/pipes/pipe-linkedin-ai-assistant/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals", "next/typescript"] -} diff --git a/pipes/pipe-linkedin-ai-assistant/.gitignore b/pipes/pipe-linkedin-ai-assistant/.gitignore deleted file mode 100644 index b0cf6a1c5..000000000 --- a/pipes/pipe-linkedin-ai-assistant/.gitignore +++ /dev/null @@ -1,65 +0,0 @@ -# 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 diff --git a/pipes/pipe-linkedin-ai-assistant/README.md b/pipes/pipe-linkedin-ai-assistant/README.md deleted file mode 100644 index 656ce1e8f..000000000 --- a/pipes/pipe-linkedin-ai-assistant/README.md +++ /dev/null @@ -1,21 +0,0 @@ -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 - - - - -## 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 - - diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/check-login/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/chrome/check-login/route.ts deleted file mode 100644 index cdb055119..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/check-login/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 } - ); - } -} diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/navigate/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/chrome/navigate/route.ts deleted file mode 100644 index eb505ebcf..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/navigate/route.ts +++ /dev/null @@ -1,64 +0,0 @@ -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 }); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/chrome/route.ts deleted file mode 100644 index bef71a264..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/route.ts +++ /dev/null @@ -1,106 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/status/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/chrome/status/route.ts deleted file mode 100644 index 52b2dc981..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/chrome/status/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 }); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/harvest/start/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/harvest/start/route.ts deleted file mode 100644 index 08ab50cd7..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/harvest/start/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextResponse } from 'next/server'; -import { startHarvesting, isHarvesting } from '@/lib/logic-sequence/harvest-connections'; -import { saveHarvestingState } from '@/lib/storage/storage'; - -export async function POST() { - try { - // Check if already harvesting - if (await isHarvesting()) { - return NextResponse.json( - { - message: 'harvesting already in progress', - isHarvesting: true, - weeklyLimitReached: false, - dailyLimitReached: false, - connectionsSent: 0 - }, - { status: 409 } // Conflict status code - ); - } - - const result = await startHarvesting(35); - - // If in cooldown, return 429 but include all status info - if (result.nextHarvestTime && result.connectionsSent === 0) { - return NextResponse.json( - { - message: `harvesting cooldown active until ${new Date(result.nextHarvestTime).toLocaleString()}`, - nextHarvestTime: result.nextHarvestTime, - connectionsSent: 0, - weeklyLimitReached: result.weeklyLimitReached || false, - dailyLimitReached: result.dailyLimitReached || false, - isHarvesting: false - }, - { status: 429 } - ); - } - - // Return detailed status messages based on the harvesting result - let message = ''; - if (result.weeklyLimitReached) { - message = `weekly limit reached, retrying at ${new Date(result.nextHarvestTime!).toLocaleString()}`; - } else if (result.dailyLimitReached) { - message = `daily limit of ${result.connectionsSent} connections reached, next harvest at ${new Date(result.nextHarvestTime!).toLocaleString()}`; - } else if (result.connectionsSent === 0) { - message = "no new connections found to harvest"; - } else { - message = `sent ${result.connectionsSent} connections.`; - } - - return NextResponse.json( - { - message, - weeklyLimitReached: result.weeklyLimitReached, - dailyLimitReached: result.dailyLimitReached, - connectionsSent: result.connectionsSent, - nextHarvestTime: result.nextHarvestTime, - }, - { status: 200 } - ); - } catch (error: unknown) { - console.error('error starting harvesting:', error); - await saveHarvestingState(false); - return NextResponse.json( - { - message: (error as Error).message.toLowerCase(), - weeklyLimitReached: false, - dailyLimitReached: false, - connectionsSent: 0, - isHarvesting: false - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/harvest/status/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/harvest/status/route.ts deleted file mode 100644 index c8284d37d..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/harvest/status/route.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { NextResponse } from 'next/server'; -import { loadConnections, saveConnection, saveNextHarvestTime, saveHarvestingState, saveRefreshStats } from '@/lib/storage/storage'; -import { getActiveBrowser } from '@/lib/browser-setup'; -import { clickCancelConnectionRequest } from '@/lib/simple-actions/click-cancel-connection-request'; -import { startHarvesting } from '@/lib/logic-sequence/harvest-connections'; -import { Page } from 'puppeteer-core'; - -export const dynamic = 'force-dynamic' -export const fetchCache = 'force-no-store' - -// Add types for page and connection -type Connection = { - status: string; - timestamp?: string; -}; - -async function checkConnectionStatus(page: Page, profileUrl: string, connection: Connection) { - try { - const maxRetries = 3; - const baseDelay = 60000; // base delay of 1 minute - - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - // Add delay only after first attempt - if (attempt > 0 || (refreshProgress && refreshProgress.current > 1)) { - const nextDelay = Math.floor(Math.random() * 1000) + 20000; - await new Promise(resolve => setTimeout(resolve, nextDelay)); - } - - // check if page is still valid - try { - await page.evaluate(() => document.title); - } catch { - // page is detached, get a new one - const browser = getActiveBrowser(); - if (!browser.page) throw new Error('failed to get new page'); - page = browser.page; - } - - // Navigate once at the start - await page.goto(profileUrl, { waitUntil: 'domcontentloaded' }); - - // check for rate limit error (429) - const is429 = await page.evaluate(() => { - return document.body.textContent?.includes('HTTP ERROR 429') || false; - }); - - if (is429) { - const retryDelay = baseDelay + Math.floor(Math.random() * baseDelay); - console.log(`rate limited on ${profileUrl}, waiting ${retryDelay/1000}s before retry ${attempt + 1}/${maxRetries}`); - await new Promise(resolve => setTimeout(resolve, retryDelay)); - continue; - } - - // First check if we need to cancel old pending request - if (connection.status === 'pending' && connection.timestamp) { - const daysAsPending = (new Date().getTime() - new Date(connection.timestamp).getTime()) / (1000 * 60 * 60 * 24); - - if (daysAsPending > 14) { - console.log(`connection request to ${profileUrl} has been pending for ${Math.floor(daysAsPending)} days, canceling...`); - const result = await clickCancelConnectionRequest(page); - if (result.success) { - return 'declined'; - } - } - } - - // Then check current connection status - await page.waitForSelector('body', { timeout: 30000 }); - const isAccepted = await page.evaluate(() => { - const distanceBadge = document.querySelector('.distance-badge'); - return distanceBadge?.textContent?.trim().includes('1st') || false; - }); - - return isAccepted ? 'accepted' : 'pending'; - - } catch (error) { - console.error(`failed to check status for ${profileUrl} (attempt ${attempt + 1}/${maxRetries}):`, error); - - if (error instanceof Error && error.message.includes('detached Frame')) { - const browser = getActiveBrowser(); - if (!browser.page) throw new Error('failed to get new page'); - page = browser.page; - } - - if (attempt === maxRetries - 1) { - return 'pending'; - } - - const retryDelay = baseDelay + Math.floor(Math.random() * baseDelay); - await new Promise(resolve => setTimeout(resolve, retryDelay)); - } - } - - return 'pending'; - } catch (error) { - console.error(`failed to check status for ${profileUrl}:`, error); - return 'pending'; - } -} - -// Add type for status -interface HarvestStatus { - nextHarvestTime: string; - isHarvesting: string; - connectionsSent: number; -} - -// Initialize with empty strings instead of undefined -let lastStatus: HarvestStatus = { - nextHarvestTime: '', - isHarvesting: '', - connectionsSent: 0 -}; - -// Add cache for cooldown check -let lastCooldownCheck = { - nextTime: '', - shouldRestart: false -}; - -// Add type for progress updates -interface RefreshProgress { - current: number; - total: number; -} - -// Add progress tracking at module level -let refreshProgress: RefreshProgress | null = null; - -export async function GET(request: Request) { - const nextDelay = 0; - try { - const url = new URL(request.url); - const shouldRefresh = url.searchParams.get('refresh') === 'true'; - let connectionsStore = await loadConnections(); - - // Only log if values changed - const currentStatus = { - nextHarvestTime: connectionsStore.nextHarvestTime || '', - isHarvesting: String(connectionsStore.isHarvesting || ''), - connectionsSent: connectionsStore.connectionsSent - }; - - if (JSON.stringify(lastStatus) !== JSON.stringify(currentStatus)) { - console.log('harvest status changed:', currentStatus); - lastStatus = currentStatus; - } - - // If isHarvesting is true but no active harvesting is happening, restart it - if (connectionsStore.isHarvesting && !connectionsStore.nextHarvestTime) { - console.log('detected stale harvesting state, restarting process'); - - // Start harvesting in the background - startHarvesting().then(() => { - // console.log('harvest restart result:', result); - }).catch(error => { - console.error('failed to restart harvesting:', error); - // Reset harvesting state if start fails - saveHarvestingState(false).catch(console.error); - }); - } - - // Original cooldown check - if (connectionsStore.nextHarvestTime) { - const nextTime = new Date(connectionsStore.nextHarvestTime); - const now = new Date(); - const shouldRestart = nextTime <= now; - - // Only track nextTime and shouldRestart state changes - const currentCooldownCheck = { - nextTime: nextTime.toISOString(), - shouldRestart - }; - - if (JSON.stringify(lastCooldownCheck) !== JSON.stringify(currentCooldownCheck)) { - console.log('cooldown check:', { - ...currentCooldownCheck, - now: now.toISOString() // Include now only in the log - }); - lastCooldownCheck = currentCooldownCheck; - } - - if (shouldRestart) { - console.log('cooldown period ended, restarting harvest process'); - await saveNextHarvestTime(''); - await saveHarvestingState(true); - connectionsStore = await loadConnections(); - - startHarvesting().then(() => { - // console.log('harvest restart result:', result); - }).catch(error => { - console.error('failed to restart harvesting:', error); - saveHarvestingState(false).catch(console.error); - }); - } - } - - if (shouldRefresh) { - const { page } = getActiveBrowser(); - if (page) { - const startTime = Date.now(); - - const pendingConnections = Object.entries(connectionsStore.connections) - .filter(([, connection]) => connection.status === 'pending'); - - refreshProgress = { - current: 0, - total: pendingConnections.length - }; - - // Check pending connections - for (const [url, connection] of pendingConnections) { - refreshProgress.current++; - - const newStatus = await checkConnectionStatus(page, url, connection); - if (newStatus !== connection.status) { - await saveConnection({ - ...connection, - status: newStatus, - timestamp: new Date().toISOString() - }); - } - } - - // Calculate and save duration stats - const totalDuration = Date.now() - startTime; - await saveRefreshStats(totalDuration, pendingConnections.length); - - // Reset progress after completion - refreshProgress = null; - } - // Reload after updates - connectionsStore = await loadConnections(); - } - - // Calculate stats from connections - const stats = Object.values(connectionsStore.connections).reduce((acc, connection) => { - const status = connection.status || 'pending'; - acc[status] = (acc[status] || 0) + 1; - return acc; - }, {} as Record); - - return NextResponse.json({ - // Convert boolean isHarvesting to three-state status - isHarvesting: connectionsStore.nextHarvestTime && new Date(connectionsStore.nextHarvestTime) > new Date() - ? 'cooldown' - : connectionsStore.isHarvesting - ? 'running' - : 'stopped', - nextHarvestTime: connectionsStore.nextHarvestTime, - connectionsSent: connectionsStore.connectionsSent || 0, - dailyLimitReached: connectionsStore.connectionsSent >= 35, - weeklyLimitReached: false, - stats: { - pending: stats.pending || 0, - accepted: stats.accepted || 0, - declined: stats.declined || 0, - email_required: stats.email_required || 0, - cooldown: stats.cooldown || 0, - total: Object.keys(connectionsStore.connections).length, - lastRefreshDuration: connectionsStore.lastRefreshDuration, - averageProfileCheckDuration: connectionsStore.averageProfileCheckDuration - }, - // Add refresh progress to response - refreshProgress, - rateLimitedUntil: null, - nextProfileTime: nextDelay ? Date.now() + nextDelay : null, - }); - } catch (error: unknown) { - return NextResponse.json({ message: (error as Error).message?.toLowerCase() }, { status: 500 }); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/harvest/stop/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/harvest/stop/route.ts deleted file mode 100644 index 86564bff4..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/harvest/stop/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextResponse } from 'next/server'; -import { stopHarvesting } from '@/lib/logic-sequence/harvest-connections'; -import { saveHarvestingState } from '@/lib/storage/storage'; - -export async function POST() { - try { - stopHarvesting(); - await saveHarvestingState(false); - - await new Promise(resolve => setTimeout(resolve, 100)); - - return NextResponse.json({ - message: 'stopping harvest process', - isHarvesting: false - }); - } catch (error: unknown) { - return NextResponse.json({ message: (error as Error).message.toLowerCase() }, { status: 500 }); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/messages/check/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/messages/check/route.ts deleted file mode 100644 index 8e474f5a5..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/messages/check/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { startMessageCheck } from '@/lib/logic-sequence/check-messages'; -import { NextResponse } from 'next/server'; - -export async function POST() { - try { - const result = await startMessageCheck(); - return NextResponse.json(result); - } catch (error) { - return NextResponse.json( - { success: false, error: (error as Error).message }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/messages/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/messages/route.ts deleted file mode 100644 index 98b781b9d..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/messages/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { loadMessages } from '@/lib/storage/storage'; -import { NextResponse } from 'next/server'; - -export async function GET() { - const messages = await loadMessages(); - return NextResponse.json(messages); -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/profiles/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/profiles/route.ts deleted file mode 100644 index bdb67f7f3..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/profiles/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { loadProfiles } from '@/lib/storage/storage'; -import { NextResponse } from 'next/server'; - -export async function GET() { - const profiles = await loadProfiles(); - return NextResponse.json(profiles); -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/save-template/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/save-template/route.ts deleted file mode 100644 index a8b610d28..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/save-template/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse } from 'next/server'; -import fs from 'fs/promises'; -import path from 'path'; - -export async function POST(request: Request) { - try { - const template = await request.json(); - - // Save to templates.json - const templatePath = path.join(process.cwd(), 'lib', 'storage', 'templates.json'); - await fs.writeFile(templatePath, JSON.stringify(template, null, 2)); - - console.log('template saved successfully'); - return NextResponse.json({ success: true }); - - } catch (error) { - console.error('failed to save template:', error); - return NextResponse.json( - { error: String(error) }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/state/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/state/route.ts deleted file mode 100644 index c0086b8fc..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/state/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { loadState } from '@/lib/storage/storage'; -import { NextResponse } from 'next/server'; - -export async function GET() { - const state = await loadState(); - return NextResponse.json(state); -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/validate-search/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/validate-search/route.ts deleted file mode 100644 index 92bdc6da5..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/validate-search/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextResponse } from 'next/server'; -import { navigateToSearch } from '@/lib/simple-actions/go-to-search-results'; -import { setupBrowser, getActiveBrowser } from '@/lib/browser-setup'; - -export async function POST(request: Request) { - try { - const { url, allowTruncate } = await request.json(); - - if (!url || !url.includes('linkedin.com/search')) { - return NextResponse.json( - { error: 'invalid linkedin search url' }, - { status: 400 } - ); - } - - // Setup browser with the provided WebSocket URL - await setupBrowser(); - const { page } = getActiveBrowser(); - - if (!page) { - return NextResponse.json( - { error: 'browser not connected' }, - { status: 400 } - ); - } - - const { count } = await navigateToSearch(page, url); - - if (count > 100 && !allowTruncate) { - return NextResponse.json( - { error: 'too many results (limit: 100). please refine your search' }, - { status: 400 } - ); - } - - return NextResponse.json({ count }); - - } catch (error) { - console.error('search validation failed:', error); - return NextResponse.json( - { error: String(error) }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/workflow/start/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/workflow/start/route.ts deleted file mode 100644 index d771c81d3..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/workflow/start/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse } from 'next/server'; -import { startAutomation } from '@/lib/logic-sequence/intro-requester'; - -export async function POST(request: Request) { - try { - const { mode } = await request.json(); - const maxProfiles = mode === 'test' ? 1 : Infinity; // full run will process all profiles - - // start the automation in the background - startAutomation(maxProfiles).catch(error => { - console.error('automation failed:', error); - }); - - return NextResponse.json({ status: 'started', mode }); - } catch (error) { - console.error('failed to start workflow:', error); - return NextResponse.json( - { error: 'failed to start workflow' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/workflow/status/route.ts b/pipes/pipe-linkedin-ai-assistant/app/api/workflow/status/route.ts deleted file mode 100644 index 1fe54e67e..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/workflow/status/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { setRunningState, currentSteps, queueStats } from './state'; - -const isRunning = false; - -export async function GET() { - return new Response(JSON.stringify({ - isRunning, - steps: currentSteps, - queueStats - }), { - headers: { 'Content-Type': 'application/json' }, - }); -} - -export async function POST(request: Request) { - const { state } = await request.json(); - setRunningState(state); - return new Response(JSON.stringify({ success: true })); -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/api/workflow/status/state.ts b/pipes/pipe-linkedin-ai-assistant/app/api/workflow/status/state.ts deleted file mode 100644 index ed36315c8..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/api/workflow/status/state.ts +++ /dev/null @@ -1,48 +0,0 @@ -type WorkflowStep = { - step: string; - status: 'pending' | 'running' | 'done' | 'error'; - details?: string; -}; - -type QueueStats = { - total: number; - alreadyVisited: number; - alreadyQueued: number; - newlyQueued: number; - currentQueueSize: number; - totalVisited: number; -}; - -let isRunning = false; -export let currentSteps: WorkflowStep[] = []; -export let queueStats: QueueStats | null = null; - -export function setRunningState(state: boolean) { - isRunning = state; - if (state) { - currentSteps = []; - queueStats = null; - } -} - -export function updateWorkflowStep(step: string, status: WorkflowStep['status'], details?: string) { - const existingStep = currentSteps.find(s => s.step === step); - if (existingStep) { - existingStep.status = status; - existingStep.details = details; - } else { - currentSteps.push({ step, status, details }); - } -} - -export function updateQueueStats(stats: QueueStats) { - queueStats = stats; -} - -export function getState() { - return { - isRunning, - steps: currentSteps, - queueStats - }; -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/app/favicon.ico b/pipes/pipe-linkedin-ai-assistant/app/favicon.ico deleted file mode 100644 index 718d6fea4..000000000 Binary files a/pipes/pipe-linkedin-ai-assistant/app/favicon.ico and /dev/null differ diff --git a/pipes/pipe-linkedin-ai-assistant/app/fonts/GeistMonoVF.woff b/pipes/pipe-linkedin-ai-assistant/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185cb..000000000 Binary files a/pipes/pipe-linkedin-ai-assistant/app/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/pipes/pipe-linkedin-ai-assistant/app/fonts/GeistVF.woff b/pipes/pipe-linkedin-ai-assistant/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daacf..000000000 Binary files a/pipes/pipe-linkedin-ai-assistant/app/fonts/GeistVF.woff and /dev/null differ diff --git a/pipes/pipe-linkedin-ai-assistant/app/globals.css b/pipes/pipe-linkedin-ai-assistant/app/globals.css deleted file mode 100644 index a7b992b9c..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/globals.css +++ /dev/null @@ -1,88 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -body { - font-family: Arial, Helvetica, sans-serif; -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - --color-1: 0 100% 63%; - --color-2: 270 100% 63%; - --color-3: 210 100% 63%; - --color-4: 195 100% 63%; - --color-5: 90 100% 63%; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --color-1: 0 100% 63%; - --color-2: 270 100% 63%; - --color-3: 210 100% 63%; - --color-4: 195 100% 63%; - --color-5: 90 100% 63%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/pipes/pipe-linkedin-ai-assistant/app/layout.tsx b/pipes/pipe-linkedin-ai-assistant/app/layout.tsx deleted file mode 100644 index 98079f55c..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import './globals.css'; - -export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - {children} - - ) -} diff --git a/pipes/pipe-linkedin-ai-assistant/app/page.tsx b/pipes/pipe-linkedin-ai-assistant/app/page.tsx deleted file mode 100644 index aa9588498..000000000 --- a/pipes/pipe-linkedin-ai-assistant/app/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { LaunchLinkedInChromeSession } from "@/components/launch-linkedin-chrome-session"; -import { IntroRequester } from "@/components/intro-requester"; -import { ReloadButton } from "@/components/reload-button"; -import { HarvestClosestConnections } from "@/components/harvest"; - -export default function Home() { - const [loginStatus, setLoginStatus] = useState<'checking' | 'logged_in' | 'logged_out' | null>(null); - - return ( -
-
-

linkedin ai assistant

-

automate your linkedin interactions with ai

-

- this tool helps you manage your linkedin network by automating common tasks. you can harvest connections - from your network and request introductions using ai-powered workflows. make sure you're logged into - linkedin before starting any automation. -

-
- -
-
-
-

getting started

- - -
-
- - {loginStatus === 'logged_in' && ( -
-

available workflows

-
-
- -
- -
-

- send introduction requests to your network based on your criteria -

- -
-
-
- )} -
-
- ); -} diff --git a/pipes/pipe-linkedin-ai-assistant/bun.lockb b/pipes/pipe-linkedin-ai-assistant/bun.lockb deleted file mode 100755 index 223b9b547..000000000 Binary files a/pipes/pipe-linkedin-ai-assistant/bun.lockb and /dev/null differ diff --git a/pipes/pipe-linkedin-ai-assistant/components.json b/pipes/pipe-linkedin-ai-assistant/components.json deleted file mode 100644 index dea737b85..000000000 --- a/pipes/pipe-linkedin-ai-assistant/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/components/harvest.tsx b/pipes/pipe-linkedin-ai-assistant/components/harvest.tsx deleted file mode 100644 index f852ffa49..000000000 --- a/pipes/pipe-linkedin-ai-assistant/components/harvest.tsx +++ /dev/null @@ -1,387 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { RefreshCw, Info } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; - -interface ConnectionStats { - pending: number; - accepted: number; - declined: number; - email_required: number; - cooldown: number; - total: number; - averageProfileCheckDuration?: number; -} - -// Add type for harvesting status -type HarvestingStatus = 'running' | 'stopped' | 'cooldown'; - -// Add helper function for formatting time -function formatTimeRemaining(milliseconds: number): string { - const seconds = Math.ceil(milliseconds / 1000); - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}m ${remainingSeconds}s`; -} - -// Add error type -type ApiError = { - message?: string; - toString: () => string; -}; - -// Add this new component -function CountdownTimer({ targetTime, prefix = "next profile in:" }: { targetTime: number, prefix?: string }) { - const [timeLeft, setTimeLeft] = useState(''); - - useEffect(() => { - const interval = setInterval(() => { - const now = Date.now(); - if (now >= targetTime) { - setTimeLeft(''); - clearInterval(interval); - return; - } - setTimeLeft(formatTimeRemaining(targetTime - now)); - }, 1000); - - return () => clearInterval(interval); - }, [targetTime]); - - if (!timeLeft) return null; - - return ( - - {prefix} {timeLeft} - - ); -} - -export function HarvestClosestConnections() { - const [harvestingStatus, setHarvestingStatus] = useState('stopped'); - const [status, setStatus] = useState(""); - const [nextHarvestTime, setNextHarvestTime] = useState(null); - const [connectionsSent, setConnectionsSent] = useState(0); - const [dailyLimitReached, setDailyLimitReached] = useState(false); - const [weeklyLimitReached, setWeeklyLimitReached] = useState(false); - const [stats, setStats] = useState({ - pending: 0, - accepted: 0, - declined: 0, - email_required: 0, - cooldown: 0, - total: 0 - }); - const [isRefreshing, setIsRefreshing] = useState(false); - const [refreshProgress, setRefreshProgress] = useState<{ current: number; total: number } | null>(null); - const [nextProfileTime, setNextProfileTime] = useState(null); - const [rateLimitedUntil, setRateLimitedUntil] = useState(null); - - useEffect(() => { - // Update initial state - fetch("/api/harvest/status") - .then(res => res.json()) - .then(data => { - setHarvestingStatus(data.isHarvesting); - setConnectionsSent(data.connectionsSent || 0); - setDailyLimitReached(data.dailyLimitReached || false); - setWeeklyLimitReached(data.weeklyLimitReached || false); - if (data.nextHarvestTime) { - setNextHarvestTime(data.nextHarvestTime); - if (new Date(data.nextHarvestTime) > new Date()) { - setStatus(`harvesting cooldown active until ${new Date(data.nextHarvestTime).toLocaleString()}`); - } - } - }) - .catch(console.error); - }, []); - - // Combine both status polling effects into one - useEffect(() => { - if (harvestingStatus !== 'stopped') { - const interval = setInterval(() => { - fetch("/api/harvest/status") - .then(res => res.json()) - .then(data => { - setHarvestingStatus(data.isHarvesting); - setConnectionsSent(data.connectionsSent || 0); - setDailyLimitReached(data.connectionsSent >= 35); - setWeeklyLimitReached(data.weeklyLimitReached || false); - if (data.stats) { - setStats(data.stats); - } - if (data.nextHarvestTime) { - setNextHarvestTime(data.nextHarvestTime); - if (new Date(data.nextHarvestTime) > new Date()) { - const message = data.connectionsSent >= 35 - ? `daily limit of ${data.connectionsSent} connections reached, next harvest at ${new Date(data.nextHarvestTime).toLocaleString()}` - : `harvesting cooldown active until ${new Date(data.nextHarvestTime).toLocaleString()}`; - setStatus(message); - } - } - - // Check for cooldown restart if needed - if (!data.isHarvesting && data.nextHarvestTime) { - const now = new Date(); - const harvestTime = new Date(data.nextHarvestTime); - - if (now >= harvestTime) { - console.log('cooldown period ended, initiating restart'); - setNextHarvestTime(null); - setStatus('cooldown period ended, restarting...'); - - // Force a status refresh to trigger the backend restart - fetch('/api/harvest/status?refresh=true') - .then(() => new Promise(resolve => setTimeout(resolve, 1000))) - .then(() => setHarvestingStatus('running')) - .catch(error => { - console.error('failed to restart harvesting:', error); - setStatus('failed to restart after cooldown'); - }); - } - } - - if (data.nextProfileTime) { - setNextProfileTime(data.nextProfileTime); - } else { - setNextProfileTime(null); - } - }) - .catch(error => { - console.error('failed to fetch status:', error); - }); - }, 1000); - - return () => clearInterval(interval); - } - }, [harvestingStatus]); - - const startHarvesting = async () => { - try { - setHarvestingStatus('running'); - setStatus("starting harvesting process..."); - - const response = await fetch("/api/harvest/start", { - method: "POST", - }); - - const data = await response.json(); - console.log('harvest start response:', data); - - if (response.ok) { - setStatus(data.message?.toLowerCase() || 'unknown status'); - setConnectionsSent(data.connectionsSent || 0); - setDailyLimitReached(data.dailyLimitReached || false); - setWeeklyLimitReached(data.weeklyLimitReached || false); - if (data.nextHarvestTime) { - setNextHarvestTime(data.nextHarvestTime); - } - } else { - // Handle 429 without stopping the workflow - if (response.status === 429) { - setNextHarvestTime(data.nextHarvestTime); - setStatus(data.message?.toLowerCase() || 'rate limit reached'); - } else { - setStatus(`error: ${data.message?.toLowerCase() || 'unknown error'}`); - setHarvestingStatus('stopped'); - } - } - } catch (error: unknown) { - console.error("failed to start harvesting:", error); - const err = error as ApiError; - setStatus(`error: ${err.message?.toLowerCase() || err.toString().toLowerCase()}`); - setHarvestingStatus('stopped'); - } - }; - - const stopHarvesting = async () => { - try { - // Set status immediately to improve UI responsiveness - setHarvestingStatus('stopped'); - setStatus("stopping harvest process..."); - - const response = await fetch("/api/harvest/stop", { - method: "POST", - }); - - if (!response.ok) { - const data = await response.json(); - setStatus(`error stopping: ${data.message?.toLowerCase() || 'unknown error'}`); - // Revert status if there was an error - setHarvestingStatus('running'); - } - } catch (error: unknown) { - console.error("failed to stop harvesting:", error); - const err = error as ApiError; - setStatus(`${err.message?.toLowerCase() || err.toString().toLowerCase()}`); - // Revert status if there was an error - setHarvestingStatus('running'); - } - }; - - const refreshStats = async () => { - try { - setIsRefreshing(true); - - // Start polling for progress - const pollInterval = setInterval(async () => { - const response = await fetch("/api/harvest/status"); - const data = await response.json(); - if (data.refreshProgress) { - setRefreshProgress(data.refreshProgress); - } - }, 1000); - - // Trigger the actual refresh - const response = await fetch("/api/harvest/status?refresh=true"); - const data = await response.json(); - - // Add rate limit handling - if (data.rateLimitedUntil) { - setRateLimitedUntil(data.rateLimitedUntil); - } else { - setRateLimitedUntil(null); - } - - // Clear polling and progress - clearInterval(pollInterval); - setRefreshProgress(null); - - setConnectionsSent(data.connectionsSent || 0); - setDailyLimitReached(data.dailyLimitReached || false); - setWeeklyLimitReached(data.weeklyLimitReached || false); - if (data.stats) { - setStats(data.stats); - } - } catch (error) { - console.error("failed to refresh stats:", error); - } finally { - setIsRefreshing(false); - setRefreshProgress(null); - } - }; - - return ( -
-
-
- - farming connections {connectionsSent > 0 && `(${connectionsSent})`} - -
- {harvestingStatus === 'stopped' && ( - - )} - {(harvestingStatus === 'running' || harvestingStatus === 'cooldown') && ( - - )} -
-
- {harvestingStatus && ( - - {dailyLimitReached && nextHarvestTime - ? `daily limit reached, next harvest at ${new Date(nextHarvestTime).toLocaleString()}` - : weeklyLimitReached && nextHarvestTime - ? `weekly limit reached, next harvest at ${new Date(nextHarvestTime).toLocaleString()}` - : status} - - )} -
-

- automatically send connection requests to your closest linkedin connections -

- -
- stats - - {nextProfileTime && nextProfileTime > Date.now() && ( -
- -
- )} - {rateLimitedUntil && ( - - rate limited ({formatTimeRemaining(new Date(rateLimitedUntil).getTime() - Date.now())}) - - )} - {refreshProgress && !rateLimitedUntil && ( - - checking {refreshProgress.current}/{refreshProgress.total} profiles - {stats.averageProfileCheckDuration && nextProfileTime && ( - - )} - - )} -
- -
-
- {stats.total} - total -
-
- {stats.pending} - pending -
-
- {stats.accepted} - accepted -
-
- {stats.declined} - - - - declined - - - -

profiles that didn't accept your connection request within 14 days are marked as declined and the request is withdrawn

-
-
-
-
-
- {stats.email_required} - email required -
-
- {stats.cooldown} - cooldown -
-
-
- ); -} diff --git a/pipes/pipe-linkedin-ai-assistant/components/header.tsx b/pipes/pipe-linkedin-ai-assistant/components/header.tsx deleted file mode 100644 index a150e09e1..000000000 --- a/pipes/pipe-linkedin-ai-assistant/components/header.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client" - -import Image from 'next/image'; - -export default function Header() { - return ( -
- screenpipe-logo -

screenpipe

-
- ); -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/components/intro-requester.tsx b/pipes/pipe-linkedin-ai-assistant/components/intro-requester.tsx deleted file mode 100644 index cbb65c4cf..000000000 --- a/pipes/pipe-linkedin-ai-assistant/components/intro-requester.tsx +++ /dev/null @@ -1,177 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { useState, useEffect } from "react"; -import { Play, Loader2, Check } from "lucide-react"; -import TemplateEditor from "@/components/template-editor"; -import StateViewer from "@/components/state-viewer"; -import template from '@/lib/storage/templates.json'; - -type WorkflowStep = { - step: string; - status: 'pending' | 'running' | 'done' | 'error'; - details?: string; -}; - -type QueueStats = { - total: number; - alreadyVisited: number; - alreadyQueued: number; - newlyQueued: number; - currentQueueSize: number; - totalVisited: number; -}; - -export function IntroRequester() { - const [status, setStatus] = useState<'running' | 'idle' | 'error' | 'complete'>('idle'); - const [completedMode, setCompletedMode] = useState<'test' | 'full' | null>(null); - const [steps, setSteps] = useState([]); - const [queueStats, setQueueStats] = useState(null); - const [showSettings, setShowSettings] = useState(false); - const [searchUrl, setSearchUrl] = useState(template['paste-here-url-from-linkedin-with-2nd-grade-connections']); - - useEffect(() => { - const handleStorageChange = async () => { - try { - const response = await fetch('/api/get-template'); - const template = await response.json(); - setSearchUrl(template.paste_here_url_from_linkedin_with_2nd_grade_connections); - } catch (error) { - console.error('failed to get template:', error); - } - }; - - window.addEventListener('storage', handleStorageChange); - return () => window.removeEventListener('storage', handleStorageChange); - }, []); - - const isUrlValid = searchUrl.includes('linkedin.com/search'); - - useEffect(() => { - if (status === 'running') { - const interval = setInterval(async () => { - const statusRes = await fetch('/api/workflow/status'); - const data = await statusRes.json(); - - setSteps(data.steps); - setQueueStats(data.queueStats); - - if (!data.isRunning) { - setStatus('complete'); - clearInterval(interval); - } - }, 1000); - - return () => clearInterval(interval); - } - }, [status]); - - const startWorkflow = async (mode: 'test' | 'full') => { - try { - setStatus('running'); - setCompletedMode(null); - const response = await fetch('/api/workflow/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - mode, - allowTruncate: true - }) - }); - - if (!response.ok) { - throw new Error('failed to start workflow'); - } - } catch (error) { - console.error('workflow error:', error); - setStatus('error'); - } - }; - - return ( -
-
- introduction requester (experimental) - -
- - {showSettings && ( -
-
-
-
- - -
- {!isUrlValid && ( - - please provide URL for target LinkedIn search in settings - - )} -
- - -
-
- )} - - {steps.length > 0 && ( -
- {steps.map((step, i) => ( -
- {step.status === 'running' && } - {step.status === 'done' &&
} - {step.status === 'error' &&
} - {step.step}: - {step.details} -
- ))} - - {queueStats && ( -
-
-
profiles in queue: {queueStats.currentQueueSize}
-
total visited: {queueStats.totalVisited}
-
newly queued: {queueStats.newlyQueued}
-
already processed: {queueStats.alreadyVisited}
-
-
- )} -
- )} -
- ); -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/components/launch-linkedin-chrome-session.tsx b/pipes/pipe-linkedin-ai-assistant/components/launch-linkedin-chrome-session.tsx deleted file mode 100644 index e5c5bdf7e..000000000 --- a/pipes/pipe-linkedin-ai-assistant/components/launch-linkedin-chrome-session.tsx +++ /dev/null @@ -1,208 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { useState, useEffect } from "react"; -import { Chrome, CheckCircle, LogIn, Loader2, Info } from "lucide-react"; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/components/ui/hover-card"; - -interface Props { - loginStatus: 'checking' | 'logged_in' | 'logged_out' | null; - setLoginStatus: (status: 'checking' | 'logged_in' | 'logged_out' | null) => void; -} - -type StatusType = 'connecting' | 'connected' | 'error' | 'idle'; - -export function LaunchLinkedInChromeSession({ loginStatus, setLoginStatus }: Props) { - const [status, setStatus] = useState('idle'); - const [loginCheckInterval, setLoginCheckInterval] = useState(null); - - useEffect(() => { - return () => { - if (loginCheckInterval) { - clearInterval(loginCheckInterval); - } - }; - }, [loginCheckInterval]); - - const killChrome = async () => { - try { - await fetch('/api/chrome', { method: 'DELETE' }); - setStatus('idle'); - } catch (error) { - console.error('failed to kill chrome:', error); - } - }; - - const launchChrome = async () => { - try { - await killChrome(); - setStatus('connecting'); - - const response = await fetch('/api/chrome', { method: 'POST' }); - if (!response.ok) throw new Error('Failed to launch chrome'); - - // Start polling the server-side API for debugger URL - pollDebuggerStatus(); - } catch (error) { - console.error('failed to launch chrome:', error); - setStatus('error'); - } - }; - - const pollDebuggerStatus = async () => { - let attempts = 0; - const maxAttempts = 15; - - while (attempts < maxAttempts) { - try { - const response = await fetch('/api/chrome/status'); - const data = await response.json(); - - if (data.status === 'connected') { - setStatus('connected'); - // Automatically navigate to LinkedIn when Chrome connects - await navigateToLinkedIn(); - return; - } - } catch (error) { - console.error('error checking debugger status:', error); - } - await new Promise(resolve => setTimeout(resolve, 1000)); - attempts++; - } - setStatus('error'); - }; - - const checkLoginStatus = async (wsUrl: string) => { - try { - const response = await fetch('/api/chrome/check-login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ wsUrl }), - }); - - if (!response.ok) throw new Error('Failed to check login status'); - const data = await response.json(); - const isLoggedIn = data.isLoggedIn; - setLoginStatus(isLoggedIn ? 'logged_in' : 'logged_out'); - - if (isLoggedIn && loginCheckInterval) { - clearInterval(loginCheckInterval); - setLoginCheckInterval(null); - } - else if (!isLoggedIn && !loginCheckInterval) { - const interval = setInterval(() => checkLoginStatus(wsUrl), 5000); - setLoginCheckInterval(interval); - } - } catch (error) { - console.error('failed to check login status:', error); - if (loginCheckInterval) { - clearInterval(loginCheckInterval); - setLoginCheckInterval(null); - } - } - }; - - const navigateToLinkedIn = async () => { - try { - // First get the wsUrl from status endpoint - const statusResponse = await fetch('/api/chrome/status'); - const statusData = await statusResponse.json(); - - if (statusData.status !== 'connected' || !statusData.wsUrl) { - throw new Error('chrome not connected'); - } - - const response = await fetch('/api/chrome/navigate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - url: 'https://www.linkedin.com', - wsUrl: statusData.wsUrl - }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.details || 'Failed to navigate'); - } - - // Add login status check after navigation - setLoginStatus('checking'); - await checkLoginStatus(statusData.wsUrl); - } catch (error) { - console.error('failed to navigate:', error); - } - }; - - return ( -
-
- {(status === 'idle' || status === 'connecting') && ( -
- - - - - - -

- it will close your chrome browser, but you can restore tabs -

-
-
-
- )} - {status === 'connected' && ( - - )} -
- - {status === 'connected' && ( - <> - {loginStatus === 'checking' && ( -
- - checking linkedin login... -
- )} - {loginStatus === 'logged_in' && ( -
- - logged in to linkedin -
- )} - {loginStatus === 'logged_out' && ( -
- - please log in to linkedin -
- )} - - )} - - {status === 'error' && ( -
- failed to launch chrome -
- )} -
- ); -} diff --git a/pipes/pipe-linkedin-ai-assistant/components/reload-button.tsx b/pipes/pipe-linkedin-ai-assistant/components/reload-button.tsx deleted file mode 100644 index f60a983ad..000000000 --- a/pipes/pipe-linkedin-ai-assistant/components/reload-button.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { RotateCw } from "lucide-react"; - -export function ReloadButton() { - const handleReload = () => { - window.location.reload(); - }; - - return ( -
- - sometimes you need to refresh page -
- ); -} \ No newline at end of file diff --git a/pipes/pipe-linkedin-ai-assistant/components/settings-editor.tsx b/pipes/pipe-linkedin-ai-assistant/components/settings-editor.tsx deleted file mode 100644 index 76c1bba9e..000000000 --- a/pipes/pipe-linkedin-ai-assistant/components/settings-editor.tsx +++ /dev/null @@ -1,220 +0,0 @@ -"use client"; - -import { useState, useMemo } from "react"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { ChevronDown, Loader2 } from "lucide-react"; -import { debounce } from "lodash"; -import Image from 'next/image'; - -interface TemplateEditorProps { - initialTemplate: unknown; - defaultOpen?: boolean; -} - -export default function TemplateEditor({ initialTemplate, defaultOpen = true }: TemplateEditorProps) { - const [template, setTemplate] = useState(initialTemplate || {}); - const [isOpen, setIsOpen] = useState(defaultOpen); - const [searchResults, setSearchResults] = useState(null); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); - - const handleChange = async (key: string, value: string) => { - console.log(`handleChange called: key=${key}, value=${value}`); - - if (key === 'paste_here_url_from_linkedin_with_2nd_grade_connections') { - console.log('search url field changed'); - if (value.includes('linkedin.com/search')) { - console.log('Valid LinkedIn search URL detected'); - await validateSearchLink(value); - } else { - console.log('Invalid LinkedIn search URL'); - } - } - - setTemplate(prev => { - const newTemplate = { ...prev, [key]: value }; - debouncedSave(newTemplate); - return newTemplate; - }); - }; - - const debouncedSave = useMemo( - () => debounce(async (template) => { - try { - const response = await fetch('/api/save-template', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(template) - }); - - if (!response.ok) { - throw new Error('failed to save template'); - } - - console.log('template auto-saved'); - } catch (error) { - console.error('error auto-saving template:', error); - } - }, 1000), - [] - ); - - const validateSearchLink = async (url: string) => { - try { - setIsValidating(true); - setValidationError(null); - console.log('starting search validation...'); - - // First check Chrome status - let statusRes = await fetch('/api/chrome/status'); - let { wsUrl, status } = await statusRes.json(); - console.log('initial chrome status:', status, 'wsUrl:', wsUrl); - - // If not connected, launch Chrome and wait for connection - if (status !== 'connected') { - console.log('chrome not connected, launching...'); - await fetch('/api/chrome', { method: 'POST' }); - - // Poll for connection status - for (let i = 0; i < 10; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); - statusRes = await fetch('/api/chrome/status'); - const newStatus = await statusRes.json(); - console.log('checking chrome status:', newStatus); - - if (newStatus.status === 'connected' && newStatus.wsUrl) { - wsUrl = newStatus.wsUrl; - status = newStatus.status; - break; - } - } - - if (status !== 'connected' || !wsUrl) { - throw new Error('failed to connect to chrome after multiple attempts'); - } - } - - console.log('chrome connected, sending validation request with wsUrl:', wsUrl); - const response = await fetch('/api/validate-search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - url, - wsUrl, - allowTruncate: searchResults === 100 // true if we've clicked the truncate button - }) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error); - } - - const { count } = await response.json(); - setSearchResults(count); - - // Add warning message for large result sets - if (count > 100) { - setValidationError('too many results. please refine your search to less than 100 connections'); - setSearchResults(null); - } else { - setValidationError(null); - } - - console.log(`search validated: ${count} results found`); - } catch (error) { - console.error('validation failed:', error); - setSearchResults(null); - setValidationError(String(error)); - } finally { - setIsValidating(false); - } - }; - - return ( -
- - -

settings

- -
- -
-
-

Start off by creating your target LinkedIn search:

-
    -
  1. Go to this link to open all your 2nd grade connections: link
  2. -
  3. Modify filters as you want, e.g. Locations, Company
  4. -
  5. Open All filters to add even more targeted criteria, e.g. industry, keywords, etc.
  6. -
  7. When finished copy the url
  8. -
  9. Insert it in the field below
  10. -
- LinkedIn search guide -
- {Object.entries(template).map(([key, value]) => ( -
- -