@lhremote/core 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/operations/comment-on-post.d.ts +7 -0
  6. package/dist/operations/comment-on-post.d.ts.map +1 -1
  7. package/dist/operations/comment-on-post.js +26 -14
  8. package/dist/operations/comment-on-post.js.map +1 -1
  9. package/dist/operations/dismiss-feed-post.d.ts +28 -0
  10. package/dist/operations/dismiss-feed-post.d.ts.map +1 -0
  11. package/dist/operations/dismiss-feed-post.js +115 -0
  12. package/dist/operations/dismiss-feed-post.js.map +1 -0
  13. package/dist/operations/dismiss-feed-post.test.d.ts +2 -0
  14. package/dist/operations/dismiss-feed-post.test.d.ts.map +1 -0
  15. package/dist/operations/dismiss-feed-post.test.js +137 -0
  16. package/dist/operations/dismiss-feed-post.test.js.map +1 -0
  17. package/dist/operations/hide-feed-author.d.ts +32 -0
  18. package/dist/operations/hide-feed-author.d.ts.map +1 -0
  19. package/dist/operations/hide-feed-author.js +116 -0
  20. package/dist/operations/hide-feed-author.js.map +1 -0
  21. package/dist/operations/hide-feed-author.test.d.ts +2 -0
  22. package/dist/operations/hide-feed-author.test.d.ts.map +1 -0
  23. package/dist/operations/hide-feed-author.test.js +160 -0
  24. package/dist/operations/hide-feed-author.test.js.map +1 -0
  25. package/dist/operations/index.d.ts +3 -0
  26. package/dist/operations/index.d.ts.map +1 -1
  27. package/dist/operations/index.js +3 -0
  28. package/dist/operations/index.js.map +1 -1
  29. package/dist/operations/react-to-post.d.ts +9 -0
  30. package/dist/operations/react-to-post.d.ts.map +1 -1
  31. package/dist/operations/react-to-post.js +15 -4
  32. package/dist/operations/react-to-post.js.map +1 -1
  33. package/dist/operations/react-to-post.test.js +49 -0
  34. package/dist/operations/react-to-post.test.js.map +1 -1
  35. package/dist/operations/unfollow-from-feed.d.ts +30 -0
  36. package/dist/operations/unfollow-from-feed.d.ts.map +1 -0
  37. package/dist/operations/unfollow-from-feed.js +114 -0
  38. package/dist/operations/unfollow-from-feed.js.map +1 -0
  39. package/dist/operations/unfollow-from-feed.test.d.ts +2 -0
  40. package/dist/operations/unfollow-from-feed.test.d.ts.map +1 -0
  41. package/dist/operations/unfollow-from-feed.test.js +141 -0
  42. package/dist/operations/unfollow-from-feed.test.js.map +1 -0
  43. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ import type { HumanizedMouse } from "../linkedin/humanized-mouse.js";
2
+ import type { ConnectionOptions } from "./types.js";
3
+ export interface UnfollowFromFeedInput extends ConnectionOptions {
4
+ /** Zero-based index of the post in the visible LinkedIn feed. */
5
+ readonly feedIndex: number;
6
+ /** Optional humanized mouse for natural cursor movement and clicks. */
7
+ readonly mouse?: HumanizedMouse | null | undefined;
8
+ /** When true, locate the menu item but do not click it. */
9
+ readonly dryRun?: boolean | undefined;
10
+ }
11
+ export interface UnfollowFromFeedOutput {
12
+ readonly success: true;
13
+ readonly feedIndex: number;
14
+ /** The name extracted from the "Unfollow {Name}" menu item. */
15
+ readonly unfollowedName: string;
16
+ readonly dryRun: boolean;
17
+ }
18
+ /**
19
+ * Unfollow the author of a LinkedIn post via its feed three-dot menu.
20
+ *
21
+ * Navigates to the LinkedIn home feed, opens the three-dot menu of the
22
+ * post at the given `feedIndex`, and clicks the "Unfollow {Name}" menu
23
+ * item. The unfollowed person's name is extracted from the menu item text.
24
+ *
25
+ * @param input - Feed index and CDP connection parameters.
26
+ * @returns Confirmation including the unfollowed person's name.
27
+ * @throws If the three-dot menu does not contain an "Unfollow" item.
28
+ */
29
+ export declare function unfollowFromFeed(input: UnfollowFromFeedInput): Promise<UnfollowFromFeedOutput>;
30
+ //# sourceMappingURL=unfollow-from-feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unfollow-from-feed.d.ts","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAErE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAQpD,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAqHjC"}
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Oleksii PELYKH
3
+ import { resolveInstancePort } from "../cdp/index.js";
4
+ import { CDPClient } from "../cdp/client.js";
5
+ import { discoverTargets } from "../cdp/discovery.js";
6
+ import { humanizedScrollToByIndex, retryInteraction } from "../linkedin/dom-automation.js";
7
+ import { gaussianDelay, maybeHesitate } from "../utils/delay.js";
8
+ import { navigateAwayIf } from "./navigate-away.js";
9
+ import { waitForFeedLoad } from "./get-feed.js";
10
+ /** CSS selector for feed post menu buttons. */
11
+ const FEED_MENU_BUTTON_SELECTOR = '[data-testid="mainFeed"] div[role="listitem"] button[aria-label^="Open control menu for post"]';
12
+ /**
13
+ * Unfollow the author of a LinkedIn post via its feed three-dot menu.
14
+ *
15
+ * Navigates to the LinkedIn home feed, opens the three-dot menu of the
16
+ * post at the given `feedIndex`, and clicks the "Unfollow {Name}" menu
17
+ * item. The unfollowed person's name is extracted from the menu item text.
18
+ *
19
+ * @param input - Feed index and CDP connection parameters.
20
+ * @returns Confirmation including the unfollowed person's name.
21
+ * @throws If the three-dot menu does not contain an "Unfollow" item.
22
+ */
23
+ export async function unfollowFromFeed(input) {
24
+ const cdpPort = await resolveInstancePort(input.cdpPort, input.cdpHost);
25
+ const cdpHost = input.cdpHost ?? "127.0.0.1";
26
+ const allowRemote = input.allowRemote ?? false;
27
+ // Enforce loopback guard
28
+ if (!allowRemote && cdpHost !== "127.0.0.1" && cdpHost !== "localhost") {
29
+ throw new Error(`Non-loopback CDP host "${cdpHost}" requires --allow-remote. ` +
30
+ "This is a security measure to prevent remote code execution.");
31
+ }
32
+ const targets = await discoverTargets(cdpPort, cdpHost);
33
+ const linkedInTarget = targets.find((t) => t.type === "page" && t.url?.includes("linkedin.com"));
34
+ if (!linkedInTarget) {
35
+ throw new Error("No LinkedIn page found in LinkedHelper. " +
36
+ "Ensure LinkedHelper is running with an active LinkedIn session.");
37
+ }
38
+ const client = new CDPClient(cdpPort, { host: cdpHost, allowRemote });
39
+ await client.connect(linkedInTarget.id);
40
+ try {
41
+ const mouse = input.mouse;
42
+ const dryRun = input.dryRun ?? false;
43
+ const feedIndex = input.feedIndex;
44
+ // Navigate to the feed (force fresh load if already there)
45
+ await navigateAwayIf(client, "/feed");
46
+ await client.navigate("https://www.linkedin.com/feed/");
47
+ await waitForFeedLoad(client);
48
+ await maybeHesitate();
49
+ // Scroll the menu button into view by index and click it, retrying if
50
+ // the menu does not open on the first attempt.
51
+ const unfollowedName = await retryInteraction(async () => {
52
+ await humanizedScrollToByIndex(client, FEED_MENU_BUTTON_SELECTOR, feedIndex, mouse);
53
+ const clicked = await client.evaluate(`(() => {
54
+ const btns = document.querySelectorAll(
55
+ ${JSON.stringify(FEED_MENU_BUTTON_SELECTOR)}
56
+ );
57
+ const btn = btns[${feedIndex}];
58
+ if (!btn) return false;
59
+ btn.click();
60
+ return true;
61
+ })()`);
62
+ if (!clicked) {
63
+ throw new Error("No feed post menu button found. " +
64
+ "Ensure the feed index points to a visible feed post.");
65
+ }
66
+ await gaussianDelay(700, 100, 500, 900);
67
+ // Find and click the "Unfollow {Name}" menu item, extracting
68
+ // the name from the text.
69
+ const name = await client.evaluate(`(() => {
70
+ const dryRun = ${dryRun};
71
+ for (const el of document.querySelectorAll('[role="menuitem"]')) {
72
+ const text = el.textContent?.trim() ?? '';
73
+ if (text.startsWith('Unfollow ')) {
74
+ if (!dryRun) el.click();
75
+ return text.slice('Unfollow '.length);
76
+ }
77
+ }
78
+ return null;
79
+ })()`);
80
+ if (!name) {
81
+ // Dismiss any open menu before retrying
82
+ await client.evaluate(`(() => {
83
+ document.dispatchEvent(
84
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
85
+ );
86
+ })()`);
87
+ await gaussianDelay(300, 75, 200, 500);
88
+ throw new Error('No "Unfollow" item found in the post control menu. ' +
89
+ "The post author may already be unfollowed, or the post " +
90
+ "may not support this action.");
91
+ }
92
+ return name;
93
+ }, 3);
94
+ if (dryRun) {
95
+ await client.evaluate(`(() => {
96
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
97
+ })()`);
98
+ await gaussianDelay(300, 75, 200, 500);
99
+ }
100
+ // Let the UI settle after clicking Unfollow
101
+ await gaussianDelay(550, 75, 400, 700);
102
+ await gaussianDelay(1_500, 500, 700, 3_500); // Post-action dwell
103
+ return {
104
+ success: true,
105
+ feedIndex: input.feedIndex,
106
+ unfollowedName,
107
+ dryRun,
108
+ };
109
+ }
110
+ finally {
111
+ client.disconnect();
112
+ }
113
+ }
114
+ //# sourceMappingURL=unfollow-from-feed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unfollow-from-feed.js","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=unfollow-from-feed.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unfollow-from-feed.test.d.ts","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Oleksii PELYKH
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ vi.mock("../cdp/client.js", () => ({
5
+ CDPClient: vi.fn(),
6
+ }));
7
+ vi.mock("../cdp/discovery.js", () => ({
8
+ discoverTargets: vi.fn(),
9
+ }));
10
+ vi.mock("../linkedin/dom-automation.js", () => ({
11
+ humanizedScrollToByIndex: vi.fn().mockResolvedValue(undefined),
12
+ retryInteraction: vi.fn().mockImplementation((fn) => fn()),
13
+ }));
14
+ vi.mock("../utils/delay.js", () => ({
15
+ delay: vi.fn().mockResolvedValue(undefined),
16
+ gaussianDelay: vi.fn().mockResolvedValue(undefined),
17
+ maybeHesitate: vi.fn().mockResolvedValue(undefined),
18
+ }));
19
+ vi.mock("./navigate-away.js", () => ({
20
+ navigateAwayIf: vi.fn().mockResolvedValue(undefined),
21
+ }));
22
+ vi.mock("./get-feed.js", () => ({
23
+ waitForFeedLoad: vi.fn().mockResolvedValue(undefined),
24
+ }));
25
+ import { CDPClient } from "../cdp/client.js";
26
+ import { discoverTargets } from "../cdp/discovery.js";
27
+ import { humanizedScrollToByIndex, retryInteraction } from "../linkedin/dom-automation.js";
28
+ import { unfollowFromFeed } from "./unfollow-from-feed.js";
29
+ const mockClient = {
30
+ connect: vi.fn().mockResolvedValue(undefined),
31
+ navigate: vi.fn().mockResolvedValue(undefined),
32
+ evaluate: vi.fn().mockResolvedValue(null),
33
+ disconnect: vi.fn(),
34
+ };
35
+ function setupMocks(unfollowName = "John Doe") {
36
+ vi.mocked(CDPClient).mockImplementation(function () {
37
+ return mockClient;
38
+ });
39
+ vi.mocked(discoverTargets).mockResolvedValue([
40
+ { id: "target-1", type: "page", title: "LinkedIn", url: "https://www.linkedin.com/feed/", description: "", devtoolsFrontendUrl: "" },
41
+ ]);
42
+ vi.mocked(humanizedScrollToByIndex).mockResolvedValue(undefined);
43
+ // First evaluate: click menu button by index → returns true
44
+ // Second evaluate: click "Unfollow {Name}" menu item → returns name
45
+ mockClient.evaluate
46
+ .mockResolvedValueOnce(true) // menu button clicked
47
+ .mockResolvedValueOnce(unfollowName); // unfollow name from menu item
48
+ }
49
+ describe("unfollowFromFeed", () => {
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ });
53
+ afterEach(() => {
54
+ vi.restoreAllMocks();
55
+ });
56
+ it("throws on non-loopback host without allowRemote", async () => {
57
+ await expect(unfollowFromFeed({
58
+ feedIndex: 0,
59
+ cdpPort: 9222,
60
+ cdpHost: "192.168.1.100",
61
+ })).rejects.toThrow("requires --allow-remote");
62
+ });
63
+ it("allows non-loopback host with allowRemote", async () => {
64
+ setupMocks();
65
+ const result = await unfollowFromFeed({
66
+ feedIndex: 0,
67
+ cdpPort: 9222,
68
+ cdpHost: "192.168.1.100",
69
+ allowRemote: true,
70
+ });
71
+ expect(result.success).toBe(true);
72
+ });
73
+ it("throws when no LinkedIn page is found", async () => {
74
+ vi.mocked(discoverTargets).mockResolvedValue([
75
+ { id: "target-1", type: "page", title: "Example", url: "https://example.com", description: "", devtoolsFrontendUrl: "" },
76
+ ]);
77
+ await expect(unfollowFromFeed({
78
+ feedIndex: 0,
79
+ cdpPort: 9222,
80
+ })).rejects.toThrow("No LinkedIn page found");
81
+ });
82
+ it("navigates to the feed", async () => {
83
+ setupMocks();
84
+ await unfollowFromFeed({
85
+ feedIndex: 0,
86
+ cdpPort: 9222,
87
+ });
88
+ expect(mockClient.navigate).toHaveBeenCalledWith("https://www.linkedin.com/feed/");
89
+ });
90
+ it("returns success with unfollowed name", async () => {
91
+ setupMocks("Jane Smith");
92
+ const result = await unfollowFromFeed({
93
+ feedIndex: 0,
94
+ cdpPort: 9222,
95
+ });
96
+ expect(result).toEqual({
97
+ success: true,
98
+ feedIndex: 0,
99
+ unfollowedName: "Jane Smith",
100
+ dryRun: false,
101
+ });
102
+ });
103
+ it("throws when no Unfollow menu item is found", async () => {
104
+ setupMocks(null);
105
+ // Third evaluate is the Escape dismiss
106
+ mockClient.evaluate.mockResolvedValueOnce(undefined);
107
+ await expect(unfollowFromFeed({
108
+ feedIndex: 0,
109
+ cdpPort: 9222,
110
+ })).rejects.toThrow('No "Unfollow" item found');
111
+ });
112
+ it("wraps menu interaction in retryInteraction", async () => {
113
+ setupMocks();
114
+ await unfollowFromFeed({
115
+ feedIndex: 0,
116
+ cdpPort: 9222,
117
+ });
118
+ expect(retryInteraction).toHaveBeenCalledWith(expect.any(Function), 3);
119
+ });
120
+ it("scrolls menu button into view and clicks by index", async () => {
121
+ setupMocks();
122
+ await unfollowFromFeed({
123
+ feedIndex: 0,
124
+ cdpPort: 9222,
125
+ });
126
+ expect(humanizedScrollToByIndex).toHaveBeenCalledWith(mockClient, '[data-testid="mainFeed"] div[role="listitem"] button[aria-label^="Open control menu for post"]', 0, undefined);
127
+ // Menu button is clicked via evaluate (by index), not humanizedClick
128
+ expect(mockClient.evaluate).toHaveBeenCalled();
129
+ });
130
+ it("disconnects the client even on error", async () => {
131
+ setupMocks(null);
132
+ // Third evaluate is the Escape dismiss
133
+ mockClient.evaluate.mockResolvedValueOnce(undefined);
134
+ await unfollowFromFeed({
135
+ feedIndex: 0,
136
+ cdpPort: 9222,
137
+ }).catch(() => { });
138
+ expect(mockClient.disconnect).toHaveBeenCalled();
139
+ });
140
+ });
141
+ //# sourceMappingURL=unfollow-from-feed.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unfollow-from-feed.test.js","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhremote/core",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Core library for LinkedHelper automation",
5
5
  "type": "module",
6
6
  "engines": {