# lhremote: LinkedHelper Automation Toolkit [![CI](https://github.com/alexey-pelykh/lhremote/actions/workflows/ci.yml/badge.svg)](https://github.com/alexey-pelykh/lhremote/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/alexey-pelykh/lhremote/graph/badge.svg)](https://codecov.io/gh/alexey-pelykh/lhremote) [![npm version](https://img.shields.io/npm/v/lhremote?logo=npm)](https://www.npmjs.com/package/lhremote) [![npm downloads](https://img.shields.io/npm/dm/lhremote?logo=npm)](https://www.npmjs.com/package/lhremote) [![GitHub Repo stars](https://img.shields.io/github/stars/alexey-pelykh/lhremote?style=flat&logo=github)](https://github.com/alexey-pelykh/lhremote) [![License](https://img.shields.io/github/license/alexey-pelykh/lhremote)](LICENSE) CLI and MCP server for [LinkedHelper](https://linkedhelper.com) automation. This project is brought to you by [Alexey Pelykh](https://github.com/alexey-pelykh). ## What It Does lhremote lets AI assistants (Claude, etc.) control LinkedHelper through the [Model Context Protocol](https://modelcontextprotocol.io). It can: - **App management** — detect, launch, and quit LinkedHelper instances - **Account & instance control** — list accounts, start/stop instances, check status - **Campaign automation** — create, configure, start, stop, and monitor campaigns with full action-chain management - **People import** — import LinkedIn profile URLs into campaign target lists - **Profile queries** — look up and search cached LinkedIn profiles from the local database - **Messaging** — send direct messages, InMails, and connection requests; query messaging history and check for replies - **LinkedIn engagement** — visit profiles, endorse skills, follow/unfollow, like posts, comment, and react - **Feed & post intelligence** — read the LinkedIn feed, search posts, get post details, engagement stats, and engager lists - **Profile enrichment** — extract emails, phones, socials, and company data from LinkedIn profiles - **LinkedIn search** — build search URLs, resolve entity IDs, and query reference data for search filters - **Budget & throttle monitoring** — check daily action limits and LinkedIn throttling status - **Action discovery** — list available LinkedHelper action types with configuration schemas ## Quick Start ```sh npm install -g lhremote # or: npx lhremote --help lhremote launch-app # start LinkedHelper with remote debugging lhremote list-accounts # find your LinkedIn account ID lhremote start-instance # start an instance (auto-selects single account) lhremote campaign-create --file my-campaign.yaml # create a campaign lhremote import-people-from-urls --urls "https://www.linkedin.com/in/..." lhremote campaign-start --person-ids lhremote campaign-status # monitor progress ``` > **Pacing**: LinkedIn monitors automated activity. See the [Rate Limiting guide](docs/rate-limiting.md) for recommended settings. ## Prerequisites - **Node.js** >= 24 - **LinkedHelper** desktop application (requires a paid subscription) ## Installation ```sh npm install -g lhremote ``` Or run directly with npx: ```sh npx lhremote --help ``` ## Usage with Claude Desktop Add to your Claude Desktop configuration (`claude_desktop_config.json`): ```json { "mcpServers": { "lhremote": { "command": "npx", "args": ["lhremote", "mcp"] } } } ``` Once configured, Claude can use all 68 tools directly. A typical workflow: 1. **`find-app`** — Detect a running LinkedHelper instance (or **`launch-app`** to start one) 2. **`list-accounts`** — See available LinkedIn accounts 3. **`start-instance`** — Start an instance for an account 4. **`describe-actions`** — Explore available action types 5. **`campaign-create`** — Create a campaign from YAML/JSON configuration 6. **`import-people-from-urls`** — Import target LinkedIn profiles into the campaign 7. **`campaign-start`** — Run the campaign 8. **`campaign-status`** / **`campaign-statistics`** — Monitor progress 9. **`query-messages`** / **`check-replies`** — Review messaging results ## CLI Usage The `lhremote` command provides the same functionality as the MCP server. Every MCP tool has a corresponding CLI command. ### App Management ```sh lhremote find-app [--json] lhremote launch-app lhremote quit-app ``` ### Account & Instance ```sh lhremote list-accounts [--cdp-port ] [--json] lhremote start-instance [--cdp-port ] lhremote stop-instance [--cdp-port ] lhremote check-status [--cdp-port ] [--json] ``` ### Campaigns ```sh lhremote campaign-list [--include-archived] [--json] lhremote campaign-create --file | --yaml | --json-input [--cdp-port ] [--json] lhremote campaign-get [--cdp-port ] [--json] lhremote campaign-export [--format yaml|json] [--output ] [--cdp-port ] lhremote campaign-update [--name ] [--description ] [--clear-description] [--cdp-port ] [--json] lhremote campaign-delete [--cdp-port ] [--json] lhremote campaign-erase [--cdp-port ] [--json] lhremote campaign-start --person-ids | --person-ids-file [--cdp-port ] [--json] lhremote campaign-stop [--cdp-port ] [--json] lhremote campaign-status [--include-results] [--limit ] [--cdp-port ] [--json] lhremote campaign-statistics [--action-id ] [--max-errors ] [--cdp-port ] [--json] lhremote campaign-retry --person-ids | --person-ids-file [--cdp-port ] [--json] lhremote campaign-list-people [--action-id ] [--status ] [--limit ] [--offset ] [--cdp-port ] [--json] ``` ### Campaign Actions ```sh lhremote campaign-add-action --name --action-type [--description ] [--cool-down ] [--max-results ] [--action-settings ] [--cdp-port ] [--json] lhremote campaign-remove-action [--cdp-port ] [--json] lhremote campaign-update-action [--name ] [--description ] [--clear-description] [--cool-down ] [--max-results ] [--action-settings ] [--cdp-port ] [--json] lhremote campaign-reorder-actions --action-ids [--cdp-port ] [--json] lhremote campaign-move-next --person-ids | --person-ids-file [--cdp-port ] [--json] ``` ### Campaign Targeting ```sh lhremote campaign-exclude-list [--action-id ] [--cdp-port ] [--json] lhremote campaign-exclude-add --person-ids | --person-ids-file [--action-id ] [--cdp-port ] [--json] lhremote campaign-exclude-remove --person-ids | --person-ids-file [--action-id ] [--cdp-port ] [--json] lhremote campaign-remove-people --person-ids | --person-ids-file [--cdp-port ] [--json] lhremote import-people-from-urls --urls | --urls-file [--cdp-port ] [--json] lhremote collect-people [--limit ] [--max-pages ] [--page-size ] [--source-type ] [--cdp-port ] [--json] ``` ### Collections ```sh lhremote list-collections [--json] lhremote create-collection [--cdp-port ] [--json] lhremote delete-collection [--cdp-port ] [--json] lhremote add-people-to-collection --person-ids | --person-ids-file [--cdp-port ] [--json] lhremote remove-people-from-collection --person-ids | --person-ids-file [--cdp-port ] [--json] lhremote import-people-from-collection [--cdp-port ] [--json] ``` ### Profiles & Messaging ```sh lhremote query-profile --person-id | --public-id [--include-positions] [--json] lhremote query-profiles [--query ] [--company ] [--include-history] [--limit ] [--offset ] [--json] lhremote query-profiles-bulk --person-id ... | --public-id ... [--include-positions] [--json] lhremote query-messages [--person-id ] [--chat-id ] [--search ] [--limit ] [--offset ] [--json] lhremote check-replies [--since ] [--cdp-port ] [--json] lhremote scrape-messaging-history --person-ids | --person-ids-file [--cdp-port ] [--json] ``` ### LinkedIn Actions ```sh lhremote visit-profile --person-id | --url [--extract-current-organizations] [--cdp-port ] [--json] lhremote endorse-skills --person-id | --url [--skill-name ]... [--limit ] [--skip-if-not-endorsable] [--keep-campaign] [--cdp-port ] [--json] lhremote enrich-profile --person-id | --url [--enrich-profile-info] [--enrich-phones] [--enrich-emails] [--enrich-socials] [--enrich-companies] [--keep-campaign] [--cdp-port ] [--json] lhremote follow-person --person-id | --url [--mode ] [--skip-if-unfollowable] [--keep-campaign] [--cdp-port ] [--json] lhremote like-person-posts --person-id | --url [--number-of-articles ] [--number-of-posts ] [--max-age-of-articles ] [--max-age-of-posts ] [--should-add-comment] [--message-template ] [--skip-if-not-liked] [--keep-campaign] [--cdp-port ] [--json] lhremote message-person --person-id | --url --message-template [--subject-template ] [--reject-if-replied] [--reject-if-messaged] [--keep-campaign] [--cdp-port ] [--json] lhremote send-invite --person-id | --url [--message-template ] [--save-as-lead-sn] [--keep-campaign] [--cdp-port ] [--json] lhremote send-inmail --person-id | --url --message-template [--subject-template ] [--reject-if-replied] [--proceed-on-out-of-credits] [--keep-campaign] [--cdp-port ] [--json] lhremote remove-connection --person-id | --url [--keep-campaign] [--cdp-port ] [--json] ``` ### Feed & Posts ```sh lhremote get-feed [--count ] [--cursor ] [--cdp-port ] [--json] lhremote get-post [--comment-count ] [--cdp-port ] [--json] lhremote get-post-stats [--cdp-port ] [--json] lhremote get-profile-activity [--count ] [--cursor ] [--cdp-port ] [--json] lhremote search-posts [--count ] [--cursor ] [--cdp-port ] [--json] lhremote comment-on-post --url --text [--cdp-port ] [--json] lhremote react-to-post [--type ] [--cdp-port ] [--json] lhremote react-to-comment [--type ] [--dry-run] [--cdp-port ] [--json] ``` ### LinkedIn Search & Reference ```sh lhremote build-url [--keywords ] [--current-company ]... [--past-company ]... [--geo ]... [--industry ]... [--school ]... [--network ]... [--profile-language ]... [--service-category ]... [--filter ]... [--slug ] [--id ] [--json] lhremote resolve-entity [--limit ] [--json] lhremote list-reference-data [--json] ``` ### Utilities ```sh lhremote describe-actions [--category ] [--type ] [--json] lhremote get-errors [--cdp-port ] [--json] lhremote dismiss-errors [--cdp-port ] [--json] lhremote get-action-budget [--cdp-port ] [--json] lhremote get-throttle-status [--cdp-port ] [--json] ``` ## MCP Tools ### Common Parameters Most tools and CLI commands connect to LinkedHelper via the Chrome DevTools Protocol (CDP). In addition to the tool-specific parameters listed below, all CDP-connected tools accept: | Parameter | CLI Flag | Type | Default | Description | |-----------|----------|------|---------|-------------| | `cdpPort` | `--cdp-port` | number | 9222 | CDP debugging port | | `cdpHost` | `--cdp-host` | string | `127.0.0.1` | CDP host address | | `allowRemote` | `--allow-remote` | boolean | false | Allow connections to non-loopback addresses | > **Security warning:** Enabling `allowRemote` permits CDP connections to remote hosts. CDP is an unsandboxed protocol that grants full control over the target browser — equivalent to remote code execution. Only enable this when the network path between your machine and the target host is fully secured (e.g., SSH tunnel, VPN, or trusted LAN). ### App Management #### `find-app` Detect running LinkedHelper application instances and their CDP connection details. *No parameters.* #### `launch-app` Launch the LinkedHelper application with remote debugging enabled. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | auto-select | CDP port to use | | `force` | boolean | No | false | Kill existing LinkedHelper processes before launching | #### `quit-app` Quit the LinkedHelper application. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | ### Account & Instance #### `list-accounts` List available LinkedHelper accounts. Returns account ID, LinkedIn ID, name, and email for each account. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | #### `start-instance` Start a LinkedHelper instance for a LinkedIn account. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `accountId` | number | No | auto-select if single account | Account ID | | `cdpPort` | number | No | 9222 | CDP port | #### `stop-instance` Stop a running LinkedHelper instance. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `accountId` | number | No | auto-select if single account | Account ID | | `cdpPort` | number | No | 9222 | CDP port | #### `check-status` Check LinkedHelper connection status, running instances, and database health. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | ### Campaigns #### `campaign-list` List existing campaigns with summary statistics. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `includeArchived` | boolean | No | false | Include archived campaigns | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-create` Create a new campaign from YAML or JSON configuration. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `config` | string | Yes | — | Campaign configuration in YAML or JSON format | | `format` | string | No | yaml | Configuration format (`yaml` or `json`) | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-get` Get detailed campaign information including action chain. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-export` Export campaign configuration as YAML or JSON. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `format` | string | No | yaml | Export format (`yaml` or `json`) | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-update` Update a campaign's name and/or description. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `name` | string | No | — | New campaign name | | `description` | string | No | — | New description (empty string to clear) | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-delete` Delete (archive) a campaign. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-erase` Permanently erase a campaign and all related data from the database. This is irreversible — unlike `campaign-delete` (which archives), this removes all campaign data permanently. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-start` Start a campaign with specified target persons. Returns immediately (async execution). | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `personIds` | number[] | Yes | — | Person IDs to target | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-stop` Stop a running campaign. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-status` Check campaign execution status and results. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `includeResults` | boolean | No | false | Include execution results | | `limit` | number | No | 20 | Max results to return | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-statistics` Get per-action statistics for a campaign. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `actionId` | number | No | — | Filter to a specific action | | `maxErrors` | number | No | 5 | Max top errors per action | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-retry` Reset specified people for re-run in a campaign. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `personIds` | number[] | Yes | — | Person IDs to retry | | `cdpPort` | number | No | 9222 | CDP port | ### Campaign Actions #### `campaign-add-action` Add a new action to a campaign's action chain. Use `describe-actions` to explore available action types. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `name` | string | Yes | — | Display name for the action | | `actionType` | string | Yes | — | Action type (e.g., `VisitAndExtract`, `MessageToPerson`) | | `description` | string | No | — | Action description | | `coolDown` | number | No | — | Milliseconds between executions | | `maxResults` | number | No | — | Max results per iteration (-1 for unlimited) | | `actionSettings` | object | No | — | Action-specific settings | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-remove-action` Remove an action from a campaign's action chain. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `actionId` | number | Yes | — | Action ID to remove | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-update-action` Update an existing action's configuration in a campaign. Only provided fields are changed. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `actionId` | number | Yes | — | Action ID to update | | `name` | string | No | — | New display name | | `description` | string \| null | No | — | New description (null to clear) | | `coolDown` | number | No | — | Milliseconds between executions | | `maxActionResultsPerIteration` | number | No | — | Max results per iteration (-1 for unlimited) | | `actionSettings` | string | No | — | Action-specific settings as JSON (merged with existing) | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-reorder-actions` Reorder actions in a campaign's action chain. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `actionIds` | number[] | Yes | — | Action IDs in desired order | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-move-next` Move people from one action to the next in a campaign. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `actionId` | number | Yes | — | Action ID to move people from | | `personIds` | number[] | Yes | — | Person IDs to move | | `cdpPort` | number | No | 9222 | CDP port | ### Campaign Targeting #### `campaign-exclude-list` View the exclude list for a campaign or action. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `actionId` | number | No | — | Action ID (for action-level list) | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-exclude-add` Add people to a campaign or action exclude list. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `personIds` | number[] | Yes | — | Person IDs to exclude | | `actionId` | number | No | — | Action ID (for action-level list) | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-exclude-remove` Remove people from a campaign or action exclude list. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `personIds` | number[] | Yes | — | Person IDs to remove from exclude list | | `actionId` | number | No | — | Action ID (for action-level list) | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-list-people` List people assigned to a campaign with their processing status. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `actionId` | number | No | — | Filter to a specific action | | `status` | string | No | — | Filter by status (`queued`, `processed`, `successful`, `failed`) | | `limit` | number | No | 20 | Max results | | `offset` | number | No | 0 | Pagination offset | | `cdpPort` | number | No | 9222 | CDP port | #### `campaign-remove-people` Remove people from a campaign's target list entirely. This is the inverse of `import-people-from-urls`. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `personIds` | number[] | Yes | — | Person IDs to remove | | `cdpPort` | number | No | 9222 | CDP port | #### `import-people-from-urls` Import LinkedIn profile URLs into a campaign action target list. Idempotent — previously imported URLs are skipped. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID | | `urls` | string[] | Yes | — | LinkedIn profile URLs | | `cdpPort` | number | No | 9222 | CDP port | #### `collect-people` Collect people from a LinkedIn page into a campaign. Detects the source type from the URL automatically. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `campaignId` | number | Yes | — | Campaign ID to collect into | | `sourceUrl` | string | Yes | — | LinkedIn page URL (search results, company people, group members) | | `limit` | number | No | — | Max profiles to collect | | `maxPages` | number | No | — | Max pages to process | | `pageSize` | number | No | — | Results per page | | `sourceType` | string | No | — | Explicit source type (bypasses URL detection) | | `cdpPort` | number | No | 9222 | CDP port | ### Collections #### `list-collections` List all LinkedHelper collections (Lists) with people counts. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | #### `create-collection` Create a new named LinkedHelper collection (List). | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `name` | string | Yes | — | Name for the new collection | | `cdpPort` | number | No | 9222 | CDP port | #### `delete-collection` Delete a LinkedHelper collection (List) and all its people associations. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `collectionId` | number | Yes | — | Collection ID to delete | | `cdpPort` | number | No | 9222 | CDP port | #### `add-people-to-collection` Add people to a LinkedHelper collection. Idempotent — adding an already-present person is a no-op. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `collectionId` | number | Yes | — | Collection ID | | `personIds` | number[] | Yes | — | Person IDs to add | | `cdpPort` | number | No | 9222 | CDP port | #### `remove-people-from-collection` Remove people from a LinkedHelper collection. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `collectionId` | number | Yes | — | Collection ID | | `personIds` | number[] | Yes | — | Person IDs to remove | | `cdpPort` | number | No | 9222 | CDP port | #### `import-people-from-collection` Import all people from a LinkedHelper collection into a campaign. Large sets are automatically chunked. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `collectionId` | number | Yes | — | Collection ID to import from | | `campaignId` | number | Yes | — | Campaign ID to import into | | `cdpPort` | number | No | 9222 | CDP port | ### Profiles & Messaging #### `query-profile` Look up a cached LinkedIn profile from the local database by person ID or public ID. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID | | `publicId` | string | No | — | LinkedIn public ID (URL slug) | | `includePositions` | boolean | No | false | Include full position history (career history) | #### `query-profiles` Search for profiles in the local database with name, headline, or company filters. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `query` | string | No | — | Search name or headline | | `company` | string | No | — | Filter by company | | `includeHistory` | boolean | No | false | Also search past positions (company history), not just current | | `limit` | number | No | 20 | Max results | | `offset` | number | No | 0 | Pagination offset | #### `query-profiles-bulk` Look up multiple cached LinkedIn profiles in a single call. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personIds` | number[] | No | — | Look up by internal person IDs | | `publicIds` | string[] | No | — | Look up by LinkedIn public IDs (URL slugs) | | `includePositions` | boolean | No | false | Include full position history | #### `query-messages` Query messaging history from the local database. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Filter by person ID | | `chatId` | number | No | — | Show specific conversation thread | | `search` | string | No | — | Search message text | | `limit` | number | No | 20 | Max results | | `offset` | number | No | 0 | Pagination offset | #### `check-replies` Check for new message replies from LinkedIn. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `since` | string | No | — | Only show replies after this ISO timestamp | | `cdpPort` | number | No | 9222 | CDP port | #### `scrape-messaging-history` Scrape messaging history from LinkedIn for specified people into the local database. This is a long-running operation that may take several minutes. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personIds` | number[] | Yes | — | Person IDs whose messaging history should be scraped | | `cdpPort` | number | No | 9222 | CDP port | ### LinkedIn Actions #### `visit-profile` Visit a LinkedIn profile via LinkedHelper's VisitAndExtract action and return the extracted profile data. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `extractCurrentOrganizations` | boolean | No | false | Extract current company info during visit | | `cdpPort` | number | No | 9222 | CDP port | #### `endorse-skills` Endorse skills on a LinkedIn profile via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `skillNames` | string[] | No | — | Specific skill names to endorse (mutually exclusive with `limit`) | | `limit` | number | No | — | Max number of skills to endorse (mutually exclusive with `skillNames`) | | `skipIfNotEndorsable` | boolean | No | true | Skip if person has no endorsable skills | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | #### `enrich-profile` Enrich a LinkedIn profile by extracting additional data (emails, phones, socials, company info) via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `profileInfo` | object | No | — | Enrich profile info (`shouldEnrich` required) | | `phones` | object | No | — | Enrich phone numbers (`shouldEnrich` required) | | `emails` | object | No | — | Enrich email addresses (`shouldEnrich` required) | | `socials` | object | No | — | Enrich social profiles (`shouldEnrich` required) | | `companies` | object | No | — | Enrich company data (`shouldEnrich` required) | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | #### `follow-person` Follow or unfollow a LinkedIn profile via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `mode` | string | No | `follow` | `follow` or `unfollow` | | `skipIfUnfollowable` | boolean | No | true | Skip if person cannot be unfollowed | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | #### `like-person-posts` Like and optionally comment on posts and articles by a LinkedIn profile via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `numberOfArticles` | number | No | — | Number of articles to like | | `numberOfPosts` | number | No | — | Number of posts to like | | `maxAgeOfArticles` | number | No | — | Maximum age of articles in days | | `maxAgeOfPosts` | number | No | — | Maximum age of posts in days | | `shouldAddComment` | boolean | No | false | Also add a comment to liked posts/articles | | `messageTemplate` | string | No | — | Comment text template as JSON (required when `shouldAddComment` is true) | | `skipIfNotLiked` | boolean | No | true | Skip if nothing was liked | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | #### `message-person` Send a direct message to a 1st-degree LinkedIn connection via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `messageTemplate` | string | Yes | — | Message template as JSON | | `subjectTemplate` | string | No | — | Subject line template as JSON | | `rejectIfReplied` | boolean | No | false | Skip if person already replied | | `rejectIfMessaged` | boolean | No | false | Skip if already messaged | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | #### `send-invite` Send a LinkedIn connection request via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `messageTemplate` | string | No | — | Invitation message template as JSON (empty for no message) | | `saveAsLeadSN` | boolean | No | false | Save as lead in Sales Navigator | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | #### `send-inmail` Send an InMail message to a LinkedIn member (no connection required) via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `messageTemplate` | string | Yes | — | InMail body template as JSON | | `subjectTemplate` | string | No | — | InMail subject line template as JSON | | `rejectIfReplied` | boolean | No | false | Skip if person already replied | | `proceedOnOutOfCredits` | boolean | No | false | Continue even when InMail credits are exhausted | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | #### `remove-connection` Remove a person from 1st-degree LinkedIn connections (unfriend) via an ephemeral campaign. Deducts from the daily action budget. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `personId` | number | No | — | Internal person ID (provide this or `url`) | | `url` | string | No | — | LinkedIn profile URL (provide this or `personId`) | | `keepCampaign` | boolean | No | false | Archive the ephemeral campaign instead of deleting it | | `cdpPort` | number | No | 9222 | CDP port | ### Feed & Posts #### `get-feed` Read the LinkedIn home feed. Returns structured post data with cursor-based pagination. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `count` | number | No | 10 | Number of posts per page | | `cursor` | string | No | — | Cursor token from a previous call for the next page | | `cdpPort` | number | No | 9222 | CDP port | #### `get-post` Get detailed data for a single LinkedIn post including its comment thread. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `postUrl` | string | Yes | — | LinkedIn post URL or URN | | `commentCount` | number | No | 100 | Maximum number of comments to load (0 to skip) | | `cdpPort` | number | No | 9222 | CDP port | #### `get-post-stats` Get engagement statistics for a LinkedIn post: reaction count (broken down by type), comment count, and share count. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `postUrl` | string | Yes | — | LinkedIn post URL or URN | | `cdpPort` | number | No | 9222 | CDP port | #### `get-post-engagers` List people who engaged with a LinkedIn post (reacted, etc.) with their profile info and engagement type. Supports pagination. *MCP tool only — no CLI command.* | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `postUrl` | string | Yes | — | LinkedIn post URL or URN | | `start` | number | No | 0 | Pagination offset | | `count` | number | No | 20 | Number of engagers per page | | `cdpPort` | number | No | 9222 | CDP port | #### `get-profile-activity` Get recent posts/activity from a LinkedIn profile with cursor-based pagination. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `profile` | string | Yes | — | LinkedIn profile public ID or URL | | `count` | number | No | 10 | Number of posts per page | | `cursor` | string | No | — | Cursor token from a previous call for the next page | | `cdpPort` | number | No | 9222 | CDP port | #### `search-posts` Search LinkedIn for posts by keyword or hashtag. Returns structured post data with cursor-based pagination. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `query` | string | Yes | — | Search query — keywords or hashtag | | `count` | number | No | 10 | Number of results per page | | `cursor` | number | No | — | Index-based cursor from a previous call for the next page | | `cdpPort` | number | No | 9222 | CDP port | #### `comment-on-post` Post a comment on a LinkedIn post. Checks action budget before attempting. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `postUrl` | string | Yes | — | LinkedIn post URL | | `text` | string | Yes | — | Comment text to post | | `cdpPort` | number | No | 9222 | CDP port | #### `react-to-post` React to a LinkedIn post with a specific reaction type. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `postUrl` | string | Yes | — | LinkedIn post URL | | `reactionType` | string | No | `like` | `like`, `celebrate`, `support`, `love`, `insightful`, or `funny` | | `cdpPort` | number | No | 9222 | CDP port | #### `react-to-comment` React to a specific LinkedIn comment with a specific reaction type. Use this for the organic-engagement pattern of acknowledging a reply (e.g., the post author replied to your comment) without extending the thread. Mirrors `react-to-post` semantics scoped to one comment via its URN (as returned by `get-post`'s `commentUrn` field). | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `postUrl` | string | Yes | — | LinkedIn post URL containing the target comment | | `commentUrn` | string | Yes | — | Comment URN (e.g. `urn:li:comment:(activity:1234567890,9876543210)`) | | `reactionType` | string | No | `like` | `like`, `celebrate`, `support`, `love`, `insightful`, or `funny` | | `dryRun` | boolean | No | `false` | When true, detects current reaction state without clicking | | `cdpPort` | number | No | 9222 | CDP port | ### LinkedIn Search & Reference #### `build-linkedin-url` Build a LinkedIn URL for any supported source type. Supports SearchPage (basic search with faceted filters), SNSearchPage (Sales Navigator), and parameterised templates for company, school, group, and event pages. CLI command: `build-url`. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `sourceType` | string | Yes | — | LinkedIn source type (e.g., `SearchPage`, `SNSearchPage`, `OrganizationPeople`) | | `keywords` | string | No | — | Search keywords | | `currentCompany` | string[] | No | — | Current company IDs (SearchPage) | | `pastCompany` | string[] | No | — | Past company IDs (SearchPage) | | `geoUrn` | string[] | No | — | Geographic URN IDs (SearchPage) | | `industry` | string[] | No | — | Industry IDs (SearchPage) | | `school` | string[] | No | — | School IDs (SearchPage) | | `network` | string[] | No | — | Connection degree codes: `F`, `S`, `O` (SearchPage) | | `profileLanguage` | string[] | No | — | Profile language codes (SearchPage) | | `serviceCategory` | string[] | No | — | Service category IDs (SearchPage) | | `filters` | object[] | No | — | Sales Navigator filters (SNSearchPage) — each with `type`, `values[]` | | `slug` | string | No | — | Company or school slug (OrganizationPeople, Alumni) | | `id` | string | No | — | Entity ID (Group, Event, SNListPage, etc.) | #### `resolve-linkedin-entity` Resolve human-readable names (company names, locations, schools) to LinkedIn entity IDs via LinkedIn's public typeahead endpoint. No authentication and no running LinkedHelper instance required. CLI command: `resolve-entity`. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `query` | string | Yes | — | Search query (e.g., company name, city) | | `entityType` | string | Yes | — | `COMPANY`, `GEO`, or `SCHOOL` | #### `list-linkedin-reference-data` List LinkedIn reference data for finite enumerations (industries, seniority levels, functions, company sizes, connection degrees, profile languages). Use this to discover valid IDs for search filters. CLI command: `list-reference-data`. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `dataType` | string | Yes | — | `INDUSTRY`, `SENIORITY`, `FUNCTION`, `COMPANY_SIZE`, `CONNECTION_DEGREE`, or `PROFILE_LANGUAGE` | ### Utilities #### `describe-actions` List available LinkedHelper action types with descriptions and configuration schemas. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `category` | string | No | — | Filter by category (`people`, `messaging`, `engagement`, `crm`, `workflow`) | | `actionType` | string | No | — | Get details for a specific action type | #### `get-errors` Query current LinkedHelper UI errors, dialogs, and blocking popups. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | #### `dismiss-errors` Dismiss closable error popups in the LinkedHelper instance UI by clicking their close/OK buttons. Recommended after `UIBlockedError`. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | #### `get-action-budget` Get daily action budget showing limit types, thresholds, and today's usage from LH campaigns and CDP-direct actions. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | #### `get-throttle-status` Check if LinkedIn is currently throttling the account. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `cdpPort` | number | No | 9222 | CDP port | ## Known Limitations - **Platform support**: LinkedHelper runs on macOS, Windows, and Linux. Binary paths are detected automatically but can be overridden with the `LINKEDHELPER_PATH` environment variable. - **Instance startup time**: Starting an instance loads LinkedIn, which may take up to 45 seconds. - **Profile data is cached**: `query-profile` and `query-profiles` search the local LinkedHelper database. Profiles must have been visited or imported by LinkedHelper to appear in results. - **Messaging scrape is slow**: `scrape-messaging-history` navigates LinkedIn's messaging UI and can take several minutes depending on conversation volume. - **Same-machine requirement**: lhremote must run on the same machine as LinkedHelper. CDP connections are localhost-only by default (for security), and database access requires direct file system access to the LinkedHelper SQLite database. ## Troubleshooting ### LinkedHelper is not running **Error**: `LinkedHelper is not running (no CDP endpoint at port 9222)` **Solution**: Use `launch-app` to start LinkedHelper, or start it manually. lhremote communicates with LinkedHelper via the Chrome DevTools Protocol (CDP), which requires the application to be running. ### LinkedHelper is unreachable **Error**: `LinkedHelper processes detected but CDP endpoint is unreachable` **Solution**: LinkedHelper is running but its CDP port is not responding. This typically means a stale or zombie process. Use `launch-app --force` to kill stale processes and relaunch, or manually restart LinkedHelper. ### Application binary not found **Error**: `LinkedHelper application binary not found. Set LINKEDHELPER_PATH to override.` **Solution**: Install LinkedHelper from [linkedhelper.com](https://linkedhelper.com). If installed in a non-standard location, set the `LINKEDHELPER_PATH` environment variable to the binary path. ### No accounts found **Error**: `No accounts found.` **Solution**: Open LinkedHelper and configure at least one LinkedIn account before using lhremote. ### Multiple accounts found **Error**: `Multiple accounts found. Specify accountId. Use list-accounts to see available accounts.` **Solution**: Use `list-accounts` to see available accounts, then pass the desired `accountId` to `start-instance`, `stop-instance`, or other tools. ### No instance running **Error**: `No LinkedHelper instance is running. Use start-instance first.` **Solution**: Run `start-instance` before using campaign or messaging tools. An instance must be running to interact with LinkedIn. ### Instance initialization timeout **Error**: `Instance started but failed to initialize within timeout.` **Solution**: The instance was started but took too long to finish loading. This can happen on slow connections. Try again; the instance may still be starting in the background. Use `check-status` to verify. ### Database not found **Error**: `No database found for account` **Solution**: The LinkedHelper database file is missing for the specified account. Ensure the account has been used at least once in LinkedHelper so that a local database has been created. ## Disclaimer `lhremote` is an **independent project** not affiliated with, endorsed by, or officially connected to: - **LinkedIn** or LinkedIn Corporation - **LinkedHelper** or its parent company LinkedIn is a trademark of LinkedIn Corporation. LinkedHelper is a trademark of its respective owner. ## Purpose This project enables **interoperability** between automation tools and LinkedHelper, as permitted under DMCA § 1201(f). Implementation is based on publicly observable behavior (Chrome DevTools Protocol) without access to protected source code. ## What This Project Does NOT Do - Circumvent copy protection or licensing - Bypass LinkedHelper authentication - Enable use without a valid LinkedHelper subscription - Provide access to LinkedIn without LinkedHelper ## User Responsibility Use of `lhremote` requires a valid LinkedHelper subscription and is subject to LinkedHelper's and LinkedIn's terms of service. Users accept all responsibility for compliance. ## Ethical Use This tool is for **legitimate productivity**. Do NOT use for spam, scraping at scale, or harassment. ## License [AGPL-3.0-only](LICENSE) — For commercial licensing, contact the maintainer.