linkedin-automation-cli 1.0.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.
- package/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# Messages Send Feature Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add a new `messages send` command that can start a new conversation with a LinkedIn profile URL.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Create a new `LinkedInMessageSender` class that navigates to a profile page, finds and clicks the "Message" button, and sends a message. The CLI command will accept a profile URL and message text.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Playwright for browser automation, Commander.js for CLI
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Add TypeScript types for send message feature
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Modify: `src/types/index.ts:17-29`
|
|
17
|
+
|
|
18
|
+
**Step 1: Add new types to types/index.ts**
|
|
19
|
+
|
|
20
|
+
Add after the `Message` interface (around line 29):
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// New message sending types
|
|
24
|
+
export interface SendMessageOptions {
|
|
25
|
+
profileUrl: string;
|
|
26
|
+
text: string;
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SendMessageResult {
|
|
31
|
+
success: boolean;
|
|
32
|
+
messageId?: string;
|
|
33
|
+
threadId?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Step 2: Verify types compile**
|
|
39
|
+
|
|
40
|
+
Run: `npm run build`
|
|
41
|
+
Expected: No errors
|
|
42
|
+
|
|
43
|
+
**Step 3: Commit**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git add src/types/index.ts
|
|
47
|
+
git commit -m "types: add SendMessageOptions and SendMessageResult interfaces"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### Task 2: Create LinkedInMessageSender class
|
|
53
|
+
|
|
54
|
+
**Files:**
|
|
55
|
+
- Create: `src/linkedin/message-sender.ts`
|
|
56
|
+
|
|
57
|
+
**Step 1: Create the message sender class**
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import type { Page } from 'playwright';
|
|
61
|
+
import { SelectorEngine } from './selector-engine';
|
|
62
|
+
|
|
63
|
+
export interface SendMessageOptions {
|
|
64
|
+
profileUrl: string;
|
|
65
|
+
text: string;
|
|
66
|
+
dryRun?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SendMessageResult {
|
|
70
|
+
success: boolean;
|
|
71
|
+
messageId?: string;
|
|
72
|
+
threadId?: string;
|
|
73
|
+
error?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class LinkedInMessageSender {
|
|
77
|
+
private page: Page;
|
|
78
|
+
private selectorEngine: SelectorEngine;
|
|
79
|
+
|
|
80
|
+
constructor(page: Page) {
|
|
81
|
+
this.page = page;
|
|
82
|
+
this.selectorEngine = new SelectorEngine(page);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Send a message to a LinkedIn profile
|
|
87
|
+
*/
|
|
88
|
+
async sendMessage(options: SendMessageOptions): Promise<SendMessageResult> {
|
|
89
|
+
try {
|
|
90
|
+
// Navigate to profile
|
|
91
|
+
await this.page.goto(options.profileUrl, {
|
|
92
|
+
waitUntil: 'networkidle',
|
|
93
|
+
timeout: 30000,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Wait for page to load
|
|
97
|
+
await this.page.waitForTimeout(2000);
|
|
98
|
+
|
|
99
|
+
// Find and click the Message button
|
|
100
|
+
const messageButton = await this.findMessageButton();
|
|
101
|
+
if (!messageButton) {
|
|
102
|
+
return { success: false, error: 'Could not find Message button on profile' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Click message button - this opens or navigates to conversation
|
|
106
|
+
await messageButton.click();
|
|
107
|
+
|
|
108
|
+
// Wait for messaging UI to load
|
|
109
|
+
await this.page.waitForTimeout(2000);
|
|
110
|
+
|
|
111
|
+
// Wait for message input field
|
|
112
|
+
const inputResult = await this.selectorEngine.findElement('messages', 'messageInput', {
|
|
113
|
+
timeout: 10000,
|
|
114
|
+
visible: true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!inputResult.element) {
|
|
118
|
+
return { success: false, error: 'Could not find message input field' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Type the message
|
|
122
|
+
await inputResult.element.fill(options.text);
|
|
123
|
+
|
|
124
|
+
// Check if dry run
|
|
125
|
+
if (options.dryRun) {
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
messageId: `dry-run-${Date.now()}`,
|
|
129
|
+
threadId: this.extractThreadId(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Click send button
|
|
134
|
+
const sendResult = await this.selectorEngine.findElement('messages', 'sendButton', {
|
|
135
|
+
timeout: 5000,
|
|
136
|
+
visible: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!sendResult.element) {
|
|
140
|
+
return { success: false, error: 'Could not find send button' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await sendResult.element.click();
|
|
144
|
+
|
|
145
|
+
// Wait for message to be sent
|
|
146
|
+
await this.page.waitForTimeout(1000);
|
|
147
|
+
|
|
148
|
+
// Try to get message ID
|
|
149
|
+
const messageId = await this.page.evaluate(() => {
|
|
150
|
+
const lastMessage = document.querySelector('[data-urn*="urn:li:fsd_message:"]');
|
|
151
|
+
if (lastMessage) {
|
|
152
|
+
const urn = lastMessage.getAttribute('data-urn');
|
|
153
|
+
if (urn) {
|
|
154
|
+
const match = urn.match(/urn:li:fsd_message:(\d+)/);
|
|
155
|
+
return match ? match[1] : urn;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return `msg-${Date.now()}`;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
messageId,
|
|
164
|
+
threadId: this.extractThreadId(),
|
|
165
|
+
};
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: error instanceof Error ? error.message : 'Unknown error sending message',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Find the Message button on a profile page
|
|
176
|
+
*/
|
|
177
|
+
private async findMessageButton() {
|
|
178
|
+
// Try multiple selector strategies
|
|
179
|
+
const selectors = [
|
|
180
|
+
'button[aria-label*="Message"]',
|
|
181
|
+
'button:has-text("Message")',
|
|
182
|
+
'a[href*="messaging/compose"]',
|
|
183
|
+
'[data-test-message-btn]',
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
for (const selector of selectors) {
|
|
187
|
+
try {
|
|
188
|
+
const button = await this.page.$(selector);
|
|
189
|
+
if (button) {
|
|
190
|
+
return button;
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// Continue to next selector
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Extract thread ID from current URL
|
|
202
|
+
*/
|
|
203
|
+
private extractThreadId(): string | undefined {
|
|
204
|
+
const url = this.page.url();
|
|
205
|
+
const match = url.match(/\/messaging\/thread\/([^/]+)/);
|
|
206
|
+
return match ? match[1] : undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Step 2: Verify types compile**
|
|
212
|
+
|
|
213
|
+
Run: `npm run build`
|
|
214
|
+
Expected: No errors
|
|
215
|
+
|
|
216
|
+
**Step 3: Commit**
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
git add src/linkedin/message-sender.ts
|
|
220
|
+
git commit -m "feat(message-sender): create LinkedInMessageSender class for new conversations"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### Task 3: Add selectors for message sending
|
|
226
|
+
|
|
227
|
+
**Files:**
|
|
228
|
+
- Modify: `src/linkedin/selectors.ts:42-64`
|
|
229
|
+
|
|
230
|
+
**Step 1: Add message button selector**
|
|
231
|
+
|
|
232
|
+
Add to the `messages` section:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
messages: {
|
|
236
|
+
// ... existing selectors ...
|
|
237
|
+
messageButton: [
|
|
238
|
+
'button[aria-label*="Message"]',
|
|
239
|
+
'button:has-text("Message")',
|
|
240
|
+
'a[href*="messaging/compose"]',
|
|
241
|
+
'[data-test-message-btn]',
|
|
242
|
+
],
|
|
243
|
+
// ... rest of existing selectors ...
|
|
244
|
+
},
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Step 2: Verify compile**
|
|
248
|
+
|
|
249
|
+
Run: `npm run build`
|
|
250
|
+
Expected: Success
|
|
251
|
+
|
|
252
|
+
**Step 3: Commit**
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
git add src/linkedin/selectors.ts
|
|
256
|
+
git commit -m "selectors: add message button selectors"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### Task 4: Create CLI command for messages send
|
|
262
|
+
|
|
263
|
+
**Files:**
|
|
264
|
+
- Modify: `src/cli/messages.ts`
|
|
265
|
+
|
|
266
|
+
**Step 1: Add send command to registerMessageCommands**
|
|
267
|
+
|
|
268
|
+
Add after the `show` command (around line 187):
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// Send new message
|
|
272
|
+
messages
|
|
273
|
+
.command('send')
|
|
274
|
+
.description('Send a new message to a LinkedIn profile')
|
|
275
|
+
.requiredOption('-u, --url <profile>', 'LinkedIn profile URL')
|
|
276
|
+
.requiredOption('-m, --message <text>', 'Message text to send')
|
|
277
|
+
.option('--dry-run', 'Simulate sending without actually sending', false)
|
|
278
|
+
.option('--headless', 'Run browser in headless mode', false)
|
|
279
|
+
.action(async (options) => {
|
|
280
|
+
const spinner = ora('Preparing to send message...').start();
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
spinner.text = 'Launching browser...';
|
|
284
|
+
|
|
285
|
+
// Launch browser - will connect to existing Edge/Chrome via CDP if available
|
|
286
|
+
const config = getConfig();
|
|
287
|
+
const browser = new BrowserController({
|
|
288
|
+
headless: options.headless ?? config.getValue('headless'),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await browser.launch();
|
|
292
|
+
|
|
293
|
+
// Get the page
|
|
294
|
+
const page = browser.getPage();
|
|
295
|
+
if (!page) throw new Error('Browser page not available');
|
|
296
|
+
|
|
297
|
+
spinner.text = options.dryRun ? 'Simulating message send...' : 'Sending message...';
|
|
298
|
+
|
|
299
|
+
const sender = new LinkedInMessageSender(page);
|
|
300
|
+
|
|
301
|
+
const result = await sender.sendMessage({
|
|
302
|
+
profileUrl: options.url,
|
|
303
|
+
text: options.message,
|
|
304
|
+
dryRun: options.dryRun,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (!result.success) {
|
|
308
|
+
spinner.fail(`Failed to send message: ${result.error}`);
|
|
309
|
+
await browser.close();
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (options.dryRun) {
|
|
314
|
+
spinner.succeed('Dry run completed - message would have been sent');
|
|
315
|
+
console.log(chalk.gray('Profile URL: ' + options.url));
|
|
316
|
+
console.log(chalk.gray('Message content:'));
|
|
317
|
+
console.log(chalk.cyan(options.message));
|
|
318
|
+
if (result.threadId) {
|
|
319
|
+
console.log(chalk.gray(`Thread ID: ${result.threadId}`));
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
spinner.succeed('Message sent successfully!');
|
|
323
|
+
console.log(chalk.gray(`Thread ID: ${result.threadId}`));
|
|
324
|
+
if (result.messageId) {
|
|
325
|
+
console.log(chalk.gray(`Message ID: ${result.messageId}`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Log action
|
|
330
|
+
const logger = getAuditLogger();
|
|
331
|
+
logger.log(
|
|
332
|
+
options.dryRun ? 'messages.send.dry-run' : 'messages.send',
|
|
333
|
+
{ profileUrl: options.url, messageLength: options.message.length },
|
|
334
|
+
true
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
await browser.close();
|
|
338
|
+
} catch (error) {
|
|
339
|
+
spinner.fail('Failed to send message');
|
|
340
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Step 2: Add import for LinkedInMessageSender**
|
|
347
|
+
|
|
348
|
+
At the top of the file, add:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { LinkedInMessageSender } from '../linkedin/message-sender';
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Step 3: Verify compile**
|
|
355
|
+
|
|
356
|
+
Run: `npm run build`
|
|
357
|
+
Expected: Success
|
|
358
|
+
|
|
359
|
+
**Step 4: Commit**
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
git add src/cli/messages.ts
|
|
363
|
+
git commit -m "cli: add messages send command for new conversations"
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
### Task 5: Test with Lily's profile
|
|
369
|
+
|
|
370
|
+
**Files:**
|
|
371
|
+
- No file changes
|
|
372
|
+
|
|
373
|
+
**Step 1: Build the project**
|
|
374
|
+
|
|
375
|
+
Run: `npm run build`
|
|
376
|
+
Expected: Success
|
|
377
|
+
|
|
378
|
+
**Step 2: Test dry run first**
|
|
379
|
+
|
|
380
|
+
Run:
|
|
381
|
+
```bash
|
|
382
|
+
node dist/index.js messages send -u "https://www.linkedin.com/in/lily-q-7145971b9/" -m "Hi Lily, this is a test message from the LinkedIn CLI automation tool. Please ignore." --dry-run
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Expected output:
|
|
386
|
+
```
|
|
387
|
+
✔ Dry run completed - message would have been sent
|
|
388
|
+
Profile URL: https://www.linkedin.com/in/lily-q-7145971b9/
|
|
389
|
+
Message content:
|
|
390
|
+
Hi Lily, this is a test message from the LinkedIn CLI automation tool. Please ignore.
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Step 3: Test actual message send**
|
|
394
|
+
|
|
395
|
+
Run:
|
|
396
|
+
```bash
|
|
397
|
+
node dist/index.js messages send -u "https://www.linkedin.com/in/lily-q-7145971b9/" -m "Hi Lily, this is Thaddeus's LinkedIn automation CLI testing the new message sending feature. This is just a test - no action needed on your end. Thanks!"
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Expected output:
|
|
401
|
+
```
|
|
402
|
+
✔ Message sent successfully!
|
|
403
|
+
Thread ID: 2-xxxxx...
|
|
404
|
+
Message ID: xxxxx
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Step 4: Verify message in LinkedIn**
|
|
408
|
+
|
|
409
|
+
Open your browser and check the messaging conversation with Lily to confirm the message was sent.
|
|
410
|
+
|
|
411
|
+
**Step 5: Commit any fixes if needed**
|
|
412
|
+
|
|
413
|
+
If any issues are found during testing, fix them and commit:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
git add src/linkedin/message-sender.ts
|
|
417
|
+
git commit -m "fix: message sender adjustments based on testing"
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
### Task 6: Add help documentation
|
|
423
|
+
|
|
424
|
+
**Files:**
|
|
425
|
+
- Modify: `README.md` (if exists in project)
|
|
426
|
+
|
|
427
|
+
**Step 1: Add documentation for the new command**
|
|
428
|
+
|
|
429
|
+
Add to the Messages section:
|
|
430
|
+
|
|
431
|
+
```markdown
|
|
432
|
+
### Send a new message
|
|
433
|
+
|
|
434
|
+
Send a message to a LinkedIn profile (starts a new conversation):
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
linkedin-cli messages send -u "https://www.linkedin.com/in/profile-url/" -m "Hello!"
|
|
438
|
+
|
|
439
|
+
# Dry run first (recommended)
|
|
440
|
+
linkedin-cli messages send -u "https://www.linkedin.com/in/profile-url/" -m "Hello!" --dry-run
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Options:
|
|
444
|
+
- `-u, --url <profile>` - LinkedIn profile URL (required)
|
|
445
|
+
- `-m, --message <text>` - Message text to send (required)
|
|
446
|
+
- `--dry-run` - Simulate without sending
|
|
447
|
+
- `--headless` - Run browser in headless mode
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Step 2: Commit**
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
git add README.md
|
|
454
|
+
git commit -m "docs: add messages send command documentation"
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Testing Checklist
|
|
460
|
+
|
|
461
|
+
- [ ] Dry run works without errors
|
|
462
|
+
- [ ] Actual message sends successfully
|
|
463
|
+
- [ ] Error handling works (invalid URL, no message button, etc.)
|
|
464
|
+
- [ ] CDP integration works (uses existing browser session)
|
|
465
|
+
- [ ] Thread ID is correctly extracted
|
|
466
|
+
- [ ] Audit logging works
|
|
467
|
+
- [ ] Help command shows new options
|
|
468
|
+
|
|
469
|
+
## Estimated Effort
|
|
470
|
+
|
|
471
|
+
- **Implementation:** 2-3 hours
|
|
472
|
+
- **Testing:** 30 minutes
|
|
473
|
+
- **Total:** ~3 hours
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Next Steps
|
|
478
|
+
|
|
479
|
+
1. ✅ Plan complete and saved to `docs/plans/2026-02-28-messages-send-feature.md`
|
|
480
|
+
2. ⏳ Execute plan with `superpowers:executing-plans`
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# v0.3.0 Design: Fix `messages show` Command
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Fix the `messages show` command to display full conversation threads by implementing click-to-navigate URL capture for extracting real LinkedIn thread IDs.
|
|
6
|
+
|
|
7
|
+
**v0.2.0 Problem:** Thread IDs are fallback ember IDs (ember50, ember58) which don't work for navigation.
|
|
8
|
+
|
|
9
|
+
**v0.3.0 Solution:** Click conversation cards → capture URL → extract real thread ID → scrape messages.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
### Functional Requirements
|
|
16
|
+
|
|
17
|
+
1. **Show Thread by ID**
|
|
18
|
+
- `linkedin-cli messages show -t ember50` displays full conversation
|
|
19
|
+
- Works with both ember IDs and real thread IDs
|
|
20
|
+
|
|
21
|
+
2. **Message Display**
|
|
22
|
+
- Default: Human-readable Style A (chronological chat format)
|
|
23
|
+
- `--json` flag: Structured JSON for LLM/automation processing
|
|
24
|
+
|
|
25
|
+
3. **Message Metadata**
|
|
26
|
+
- Sender name (with "You" indicator for current user)
|
|
27
|
+
- Timestamp (formatted + raw in JSON)
|
|
28
|
+
- Message content (with newline preservation)
|
|
29
|
+
|
|
30
|
+
### Non-Functional Requirements
|
|
31
|
+
|
|
32
|
+
- **Reliability:** Handle LinkedIn's dynamic UI with fallback strategies
|
|
33
|
+
- **Performance:** Load messages within 10 seconds for typical conversations
|
|
34
|
+
- **Rate Limiting:** Respect LinkedIn's rate limits (max 1 request/second)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
### Component Diagram
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
44
|
+
│ messages show Command │
|
|
45
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
46
|
+
│ │
|
|
47
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
48
|
+
│ │ Card Locator │───▶│ Click & Wait │───▶│ URL Extractor│ │
|
|
49
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
50
|
+
│ │ │
|
|
51
|
+
│ ▼ │
|
|
52
|
+
│ ┌──────────────┐ │
|
|
53
|
+
│ │ Message │ │
|
|
54
|
+
│ │ Scraper │ │
|
|
55
|
+
│ └──────────────┘ │
|
|
56
|
+
│ │ │
|
|
57
|
+
│ ┌──────────────┐ ▼ │
|
|
58
|
+
│ │ JSON Output │◀───┐ ┌──────────────┐ │
|
|
59
|
+
│ │ Formatter │ └─│ Text Output │ │
|
|
60
|
+
│ └──────────────┘ └──────────────┘ │
|
|
61
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Data Flow
|
|
65
|
+
|
|
66
|
+
1. **User Input:** `messages show -t ember50 [--json]`
|
|
67
|
+
2. **Card Lookup:** Find conversation card by ember ID in DOM
|
|
68
|
+
3. **Navigation:** Click card → wait for URL change
|
|
69
|
+
4. **ID Extraction:** Parse real thread ID from new URL
|
|
70
|
+
5. **Message Scraping:** Extract all message bubbles
|
|
71
|
+
6. **Output:** Format as text (default) or JSON
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Implementation Details
|
|
76
|
+
|
|
77
|
+
### 1. Card Locator (`findConversationCard()`)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
async findConversationCard(cardId: string): Promise<ElementHandle> {
|
|
81
|
+
// Try direct ember ID match
|
|
82
|
+
let card = await page.$(`#conversation-card-${cardId}`);
|
|
83
|
+
|
|
84
|
+
// Fallback: search by class + index
|
|
85
|
+
if (!card) {
|
|
86
|
+
const cards = await page.$$('.msg-conversation-card');
|
|
87
|
+
const index = cardId.replace('ember', '');
|
|
88
|
+
card = cards[parseInt(index)];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return card;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Click & Navigate
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
async navigateToThread(card: ElementHandle): Promise<string> {
|
|
99
|
+
// Set up URL change listener
|
|
100
|
+
const [newUrl] = await Promise.all([
|
|
101
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
|
102
|
+
card.click(),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
return page.url(); // Contains real thread ID
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3. Message Scraper
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
async scrapeMessages(threadId: string): Promise<Message[]> {
|
|
113
|
+
const messageBubbles = await page.$$('.msg-s-message-group__message');
|
|
114
|
+
|
|
115
|
+
return Promise.all(messageBubbles.map(async (bubble) => {
|
|
116
|
+
return {
|
|
117
|
+
sender: await bubble.$eval('.msg-s-message-group__name', el => el.textContent),
|
|
118
|
+
content: await bubble.$eval('.msg-s-event-listitem__body', el => el.textContent),
|
|
119
|
+
timestamp: await bubble.$eval('time', el => el.getAttribute('datetime')),
|
|
120
|
+
isMe: await bubble.evaluate(el => el.classList.contains('msg-s-message-group--me')),
|
|
121
|
+
};
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 4. Output Formatters
|
|
127
|
+
|
|
128
|
+
**Text Formatter (Default):**
|
|
129
|
+
```
|
|
130
|
+
Conversations with Lucia Söffge
|
|
131
|
+
────────────────────────────────
|
|
132
|
+
Feb 26, 2024
|
|
133
|
+
|
|
134
|
+
10:30 AM Lucia: Hey! How are you?
|
|
135
|
+
10:32 AM You: I'm good, thanks!
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**JSON Formatter (`--json`):**
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"threadId": "2-ZWY1Mzkx...",
|
|
142
|
+
"participants": [{"name": "Lucia Söffge", "profileUrl": null}],
|
|
143
|
+
"messages": [
|
|
144
|
+
{
|
|
145
|
+
"id": "msg-1",
|
|
146
|
+
"threadId": "2-ZWY1Mzkx...",
|
|
147
|
+
"sender": {"name": "Lucia Söffge", "isMe": false},
|
|
148
|
+
"content": "Hey! How are you?",
|
|
149
|
+
"timestamp": "2024-02-26T10:30:00Z"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Error Handling
|
|
158
|
+
|
|
159
|
+
| Error | Handling |
|
|
160
|
+
|-------|----------|
|
|
161
|
+
| Card not found | Show list of available ember IDs |
|
|
162
|
+
| Navigation timeout | Retry click, then fail with message |
|
|
163
|
+
| No messages found | Show empty state with thread info |
|
|
164
|
+
| Rate limited | Wait + retry with exponential backoff |
|
|
165
|
+
| Session expired | Prompt user to re-authenticate |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Testing Strategy
|
|
170
|
+
|
|
171
|
+
### Unit Tests
|
|
172
|
+
- [ ] `findConversationCard()` with valid/invalid IDs
|
|
173
|
+
- [ ] URL extraction regex for thread IDs
|
|
174
|
+
- [ ] Message scraper with various message types
|
|
175
|
+
|
|
176
|
+
### Integration Tests
|
|
177
|
+
- [ ] Click navigation with real LinkedIn session
|
|
178
|
+
- [ ] JSON output validation against schema
|
|
179
|
+
- [ ] Text output formatting
|
|
180
|
+
|
|
181
|
+
### Manual Testing
|
|
182
|
+
- [ ] Conversations with 1 message
|
|
183
|
+
- [ ] Conversations with 100+ messages
|
|
184
|
+
- [ ] Conversations with attachments
|
|
185
|
+
- [ ] Group conversations (3+ participants)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Success Criteria
|
|
190
|
+
|
|
191
|
+
- [ ] `messages show -t ember50` displays full thread
|
|
192
|
+
- [ ] `messages show -t ember50 --json` outputs valid JSON
|
|
193
|
+
- [ ] Works with real thread IDs (e.g., `2-ZWY1...`)
|
|
194
|
+
- [ ] Handles conversations up to 100 messages
|
|
195
|
+
- [ ] All tests pass (unit + integration)
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Out of Scope (v0.4.0+)
|
|
200
|
+
|
|
201
|
+
- [ ] Message attachments/images
|
|
202
|
+
- [ ] Message reactions
|
|
203
|
+
- [ ] Edit/delete messages
|
|
204
|
+
- [ ] Search within conversations
|
|
205
|
+
- [ ] Export conversations to file
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Files to Modify
|
|
210
|
+
|
|
211
|
+
| File | Changes |
|
|
212
|
+
|------|---------|
|
|
213
|
+
| `src/linkedin/messages.ts` | Add `showThread()` method with click navigation |
|
|
214
|
+
| `src/cli/messages.ts` | Add `--json` flag, update output formatting |
|
|
215
|
+
| `src/types/index.ts` | Add/verify `Message` and `Thread` types |
|
|
216
|
+
| `src/linkedin/selector-engine.ts` | Add message-specific selectors |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Estimated Effort
|
|
221
|
+
|
|
222
|
+
- **Implementation:** 3-4 hours
|
|
223
|
+
- **Testing:** 1-2 hours
|
|
224
|
+
- **Documentation:** 30 minutes
|
|
225
|
+
- **Total:** 5-6 hours
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Approval
|
|
230
|
+
|
|
231
|
+
**Approved by:** [User]
|
|
232
|
+
**Date:** 2026-02-28
|
|
233
|
+
**Status:** Ready for implementation planning
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Next Steps
|
|
238
|
+
|
|
239
|
+
1. ✅ Design approved
|
|
240
|
+
2. ⏳ Invoke `writing-plans` skill for detailed implementation plan
|
|
241
|
+
3. ⏳ Implement features
|
|
242
|
+
4. ⏳ Test with real LinkedIn data
|
|
243
|
+
5. ⏳ Release v0.3.0
|