GitHub is our source of truth. You have all history here. Use it when you're tackling a problem:
- Search through previous issues.
- Check commits and comments to understand what was tried before.
- Maybe you've tried to fix this bug before.
Always check the repository history before starting implementation.
LinkedIn Buddy — Playwright-based browser automation for LinkedIn with CLI and MCP server. Three surfaces (CLI, MCP, TypeScript API) share one core runtime. All write operations go through two-phase commit (prepare/confirm). Local-first: SQLite state, persistent browser profiles, structured logs, screenshots.
packages/
core/ @linkedin-buddy/core — automation library, DB, rate limiter, two-phase commit
cli/ @linkedin-buddy/cli — operator CLI (binaries: linkedin, lbud, linkedin-buddy)
mcp/ @linkedin-buddy/mcp — MCP stdio server (binary: linkedin-mcp)
scripts/ build, release, e2e runner, security audit, keep-alive
docs/ feature docs, architecture notes, research briefs
| File | Purpose |
|---|---|
packages/core/src/runtime.ts |
Service graph factory — wires all 25+ services via createCoreRuntime() |
packages/core/src/index.ts |
Barrel export — 54 re-exports for external consumption |
packages/core/src/twoPhaseCommit.ts |
Two-phase commit framework (prepare/confirm, tokens, executors, DB) |
packages/core/src/errors.ts |
Error taxonomy — LinkedInBuddyError with 9 structured codes |
packages/core/src/config.ts |
Configuration resolution (paths, evasion, locale, privacy, webhooks) |
packages/core/src/db/database.ts |
SQLite (better-sqlite3) — prepared actions, logs, artifacts, rate limits, activity |
packages/core/src/db/migrations.ts |
Schema migrations (6 versions) |
packages/core/src/rateLimiter.ts |
Per-action token bucket rate limiting |
packages/core/src/logging.ts |
Structured JSON event logger (domain.operation.stage naming) |
packages/core/src/artifacts.ts |
Screenshot/trace artifact management |
packages/core/src/profileManager.ts |
Playwright persistent context / CDP management |
packages/core/src/connectionPool.ts |
Browser connection pooling |
packages/core/src/humanize.ts |
Human-like typing simulation (typos, pauses, profiles) |
packages/core/src/evasion.ts |
Anti-bot evasion config and profile resolution |
packages/core/src/privacy.ts |
Privacy config, log redaction, JSON sealing |
| File | Purpose |
|---|---|
packages/core/src/linkedinInbox.ts |
Inbox — list threads, view messages, send/react/archive/mute |
packages/core/src/linkedinFeed.ts |
Feed — list, view, like/comment/repost/share/save |
packages/core/src/linkedinConnections.ts |
Connections — list, send/accept/withdraw/remove/follow/unfollow |
packages/core/src/linkedinProfile.ts |
Profile — view + 14 edit actions (intro, sections, photos, skills) |
packages/core/src/linkedinSearch.ts |
Search — people, companies, jobs, posts, groups, events |
packages/core/src/linkedinJobs.ts |
Jobs — search, view, save/unsave, alerts, Easy Apply |
packages/core/src/linkedinNotifications.ts |
Notifications — list, dismiss, preference updates |
packages/core/src/linkedinPosts.ts |
Posts — create, edit, delete (text, media, polls) |
packages/core/src/linkedinPublishing.ts |
Articles and newsletters |
packages/core/src/linkedinFollowups.ts |
Follow-up message scheduling |
packages/core/src/linkedinGroups.ts |
Groups — search, view, join/leave/post |
packages/core/src/linkedinEvents.ts |
Events — search, view, RSVP |
packages/core/src/linkedinCompanyPages.ts |
Company pages — view, follow/unfollow |
packages/core/src/linkedinMembers.ts |
Member safety — block/unblock/report |
packages/core/src/linkedinPrivacySettings.ts |
Privacy settings management |
packages/core/src/linkedinAnalytics.ts |
Analytics — profile views, search appearances, post metrics |
packages/core/src/linkedinImageAssets.ts |
Persona image generation (OpenAI) |
packages/core/src/linkedinPage.ts |
Page navigation, waiting, selector helpers |
| File | Purpose |
|---|---|
packages/core/src/auth/session.ts |
Auth service — login, status, ensureAuthenticated |
packages/core/src/auth/sessionStore.ts |
Session persistence and encryption |
packages/core/src/auth/loginSelectors.ts |
Login page selector strategies |
packages/core/src/auth/rateLimitState.ts |
Rate limit cooldown state management |
packages/core/src/activityWatches.ts |
Activity watch CRUD + webhook subscriptions |
packages/core/src/activityPoller.ts |
Polling engine — tick execution, event diffing, delivery |
packages/core/src/webhookDelivery.ts |
Webhook delivery with signing and retry |
packages/core/src/scheduler.ts |
Deferred job scheduling with lanes and leasing |
packages/core/src/keepAlive.ts |
Session keep-alive daemon |
packages/core/src/healthCheck.ts |
Full system health diagnostics |
| File | Purpose |
|---|---|
packages/core/src/writeValidation.ts |
Tier 3 live write validation harness |
packages/core/src/liveValidation.ts |
Live read-only validation workflows |
packages/core/src/selectorAudit.ts |
Selector resilience auditing |
packages/core/src/selectorLocale.ts |
Locale-aware selectors (en/da) |
packages/core/src/fixtureReplay.ts |
HTTP fixture replay for deterministic testing |
packages/core/src/draftQualityEval.ts |
Draft content quality scoring |
| File | Purpose |
|---|---|
packages/cli/src/bin/linkedin.ts |
CLI entry point — 127+ Commander commands |
packages/mcp/src/bin/linkedin-mcp.ts |
MCP server — 100+ tool handlers |
packages/mcp/src/index.ts |
MCP tool name constants (157 exports) |
createCoreRuntime() in runtime.ts wires all services:
Infrastructure: db → logger → artifacts → profileManager → auth → rateLimiter → twoPhaseCommit
↓
LinkedIn Services: inbox, feed, connections, profile, search, jobs, notifications,
posts, publishing, followups, groups, events, companyPages,
members, privacySettings, analytics, imageAssets
↓
Activity Layer: activityWatches → activityPoller → webhookDelivery → scheduler
All services receive dependencies via constructor injection. No circular dependencies.
npm install # Install dependencies
npx playwright install chromium # Install browser
npm run build # TypeScript → dist/ (tsc -b)
npm test # Unit tests (Vitest, 120+ test files)
npm run typecheck # Type check (tsc -b --force)
npm run lint # ESLint (flat config, strict TypeScript)
npm run test:e2e # E2E tests (requires CDP + auth session)
npm run test:e2e:fixtures # E2E with fixture replayBefore submitting any PR, ensure all pass:
npm run typecheck
npm run lint
npm test
npm run build- Language: TypeScript strict mode, ES modules (
"type": "module"), Node 22+ - Imports: Use
.jsextension in import paths (TypeScript ESM convention) - Exports: All core modules re-exported via
packages/core/src/index.ts - Naming:
camelCasefor variables/functions,PascalCasefor types/classes,SCREAMING_SNAKEfor constants - Error handling: Use
LinkedInBuddyErrorwith structured error codes — never rawthrow new Error() - Logging:
runtime.logger.log(level, "domain.operation.stage", details)— structured JSON events - Artifacts:
runtime.artifactsfor screenshots and traces — relative paths under run directory - Tests: Vitest. Unit tests in
__tests__/. E2E tests in__tests__/e2e/ - Linting: ESLint flat config —
no-explicit-anyis an error. No Prettier. - Feed comment tone for testing: Short, subtle, kind comments relating to post content. Never mention tests, bots, automation.
Every outbound action must:
- Have a
prepare*method that stores action in DB and returns confirm token - Have an
ActionExecutorclass implementingexecute() - Create a
create*ActionExecutors()factory function - Register the executor in
runtime.ts
prepare*() → PreparedAction in DB → { preparedActionId, confirmToken, preview }
↓
confirm(token) → validate token + expiry → ActionExecutor.execute() → record result
Token TTL: 30 minutes. Tokens are HMAC-SHA256 sealed JSON with entropy.
| Code | Meaning |
|---|---|
AUTH_REQUIRED |
Session expired or not authenticated |
CAPTCHA_OR_CHALLENGE |
LinkedIn challenge/checkpoint detected |
RATE_LIMITED |
Action rate-limited in current window |
UI_CHANGED_SELECTOR_FAILED |
Selector no longer matches LinkedIn UI |
NETWORK_ERROR |
Network/connectivity failure |
TIMEOUT |
Operation exceeded timeout |
TARGET_NOT_FOUND |
Target entity not found |
ACTION_PRECONDITION_FAILED |
Precondition check failed |
UNKNOWN |
Fallback for unmapped errors |
Use multi-strategy selectors with SelectorCandidate pattern:
- role (most stable) → 2. attribute → 3. text (fragile) → 4. xpath (last resort)
Locale support: selectorLocale.ts maps UI phrases for en and da.
Token bucket per action. rateLimiter.consume() before writes; rateLimiter.peek() for previews.
State persisted in SQLite rate_limit_counter table.
| Table | Purpose |
|---|---|
prepared_action |
Two-phase commit state (action, token, status, result) |
rate_limit_counter |
Token bucket counters per action |
run_log |
Structured event logs |
artifact_index |
Screenshot/trace metadata |
scheduler_job |
Deferred job queue with lanes |
activity_watch |
Watch definitions (kind, schedule, polling interval) |
activity_entity_state |
Entity snapshots for diff detection |
activity_event |
Emitted activity events |
webhook_subscription |
Webhook targets (URL, signing secret) |
webhook_delivery_attempt |
Delivery history and retry state |
- Create
packages/core/src/linkedinFeature.tswith:- TypeScript interfaces for inputs/outputs
- Service class with read-only methods and
prepare*methods for mutations ActionExecutorclass(es) for confirm flowscreateFeatureActionExecutors()factory function
- Export from
packages/core/src/index.ts - Wire into
packages/core/src/runtime.ts:- Create service instance
- Register executors via
twoPhaseCommit.registerExecutors() - Expose on runtime object
- Add CLI commands in
packages/cli/src/bin/linkedin.ts - Add MCP tools in
packages/mcp/src/bin/linkedin-mcp.ts+ constants inpackages/mcp/src/index.ts - Add unit tests and E2E tests
Anti-bot evasion levels: off → light → moderate (default) → aggressive
Humanize typing profiles: careful (slow, realistic) → casual (balanced) → fast (testing)
Config via --evasion-level flag, LINKEDIN_BUDDY_EVASION_LEVEL env, or runtime options.
See packages/core/src/evasion/ for Bezier mouse paths, Poisson intervals, fingerprint hardening.
Watches poll LinkedIn at configurable intervals. Events detected via entity snapshot diffing.
Webhook subscriptions receive signed HTTP POST with retry logic.
activity run-once executes one polling tick; scheduler start runs continuous daemon.
Outbound actions against real LinkedIn are restricted:
- Messages: Only send to Simon Miller (
linkedin.com/in/realsimonmiller). No other recipients without explicit approval from Joakim. - Connection requests: Only to Simon Miller unless explicitly approved.
- Comments, likes, posts, and other public actions: Ask Joakim for approval before executing. Describe what you plan to test and on which target, and wait for confirmation.
- Read-only operations (profile view, search, inbox list, feed view, notifications) are unrestricted.
Violating these rules risks real social interactions on Joakim's LinkedIn account.
GitHub Actions:
ci.yml— Lint, typecheck, unit tests, fixture E2E, build (on push/PR)release.yml— Calver versioning, npm publish, GitHub release (daily/manual)secret-scan.yml— Gitleaks history + tracked-file auditai-auto-rebase.yml— Auto-rebase conflicting AI PRsai-ci-recovery.yml— Re-label failed AI branches for retry
issue-<number>-<short-description> (e.g., issue-2-post-composer)
feat #N: descriptionfor new featuresfix #N: descriptionfor bug fixeschore: descriptionfor maintenance
You are running inside an Agent Orchestrator managed workspace. Session metadata is updated automatically via shell wrappers.
If automatic updates fail, you can manually update metadata:
~/.ao/bin/ao-metadata-helper.sh # sourced automatically
# Then call: update_ao_metadata <key> <value>