@ldraney/mcp-linkedin 0.1.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.
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview Background scheduler daemon for LinkedIn posts
5
+ * Checks for due posts every minute and publishes them
6
+ */
7
+
8
+ require('dotenv').config();
9
+ const cron = require('node-cron');
10
+ const { getDatabase } = require('./database');
11
+ const LinkedInAPI = require('./linkedin-api');
12
+
13
+ /**
14
+ * @typedef {import('./types').ScheduledPost} ScheduledPost
15
+ */
16
+
17
+ const MAX_RETRIES = 3;
18
+
19
+ /**
20
+ * Get LinkedIn API client instance
21
+ * @returns {LinkedInAPI}
22
+ */
23
+ function getAPIClient() {
24
+ return new LinkedInAPI({
25
+ accessToken: process.env.LINKEDIN_ACCESS_TOKEN,
26
+ apiVersion: process.env.LINKEDIN_API_VERSION,
27
+ personId: process.env.LINKEDIN_PERSON_ID
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Publish a scheduled post to LinkedIn
33
+ * @param {ScheduledPost} scheduledPost - The post to publish
34
+ * @returns {Promise<{success: boolean, postUrn?: string, error?: string}>}
35
+ */
36
+ async function publishPost(scheduledPost) {
37
+ try {
38
+ const api = getAPIClient();
39
+
40
+ const postData = {
41
+ author: `urn:li:person:${process.env.LINKEDIN_PERSON_ID}`,
42
+ commentary: scheduledPost.commentary,
43
+ visibility: scheduledPost.visibility,
44
+ distribution: {
45
+ feedDistribution: 'MAIN_FEED'
46
+ },
47
+ lifecycleState: 'PUBLISHED'
48
+ };
49
+
50
+ // If URL is provided, create a link post
51
+ if (scheduledPost.url) {
52
+ postData.content = {
53
+ article: {
54
+ source: scheduledPost.url
55
+ }
56
+ };
57
+ }
58
+
59
+ const result = await api.createPost(postData);
60
+
61
+ return {
62
+ success: true,
63
+ postUrn: result.postUrn
64
+ };
65
+ } catch (error) {
66
+ return {
67
+ success: false,
68
+ error: error.message || 'Unknown error during publish'
69
+ };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Process all due posts
75
+ * @returns {Promise<{published: number, failed: number}>}
76
+ */
77
+ async function processDuePosts() {
78
+ const db = getDatabase();
79
+ const duePosts = db.getDuePosts();
80
+
81
+ let published = 0;
82
+ let failed = 0;
83
+
84
+ for (const post of duePosts) {
85
+ console.log(`[${new Date().toISOString()}] Processing scheduled post: ${post.id}`);
86
+ console.log(` Commentary: ${post.commentary.substring(0, 50)}...`);
87
+
88
+ const result = await publishPost(post);
89
+
90
+ if (result.success) {
91
+ db.markAsPublished(post.id, result.postUrn);
92
+ console.log(` ✓ Published successfully: ${result.postUrn}`);
93
+ published++;
94
+ } else {
95
+ db.markAsFailed(post.id, result.error);
96
+ console.log(` ✗ Failed: ${result.error} (attempt ${post.retryCount + 1})`);
97
+
98
+ // Check if we should retry
99
+ if (post.retryCount + 1 < MAX_RETRIES) {
100
+ console.log(` Will retry on next cycle`);
101
+ // Reset to pending for retry
102
+ db.resetForRetry(post.id);
103
+ } else {
104
+ console.log(` Max retries reached, marked as failed`);
105
+ }
106
+ failed++;
107
+ }
108
+ }
109
+
110
+ return { published, failed };
111
+ }
112
+
113
+ /**
114
+ * Start the scheduler
115
+ * @param {string} [cronExpression='* * * * *'] - Cron expression (default: every minute)
116
+ */
117
+ function startScheduler(cronExpression = '* * * * *') {
118
+ console.log(`[${new Date().toISOString()}] LinkedIn Post Scheduler Started`);
119
+ console.log(` Cron expression: ${cronExpression}`);
120
+ console.log(` Access token configured: ${!!process.env.LINKEDIN_ACCESS_TOKEN}`);
121
+ console.log(` Person ID: ${process.env.LINKEDIN_PERSON_ID}`);
122
+ console.log('');
123
+
124
+ // Initial check on startup
125
+ console.log('Running initial check...');
126
+ processDuePosts().then(({ published, failed }) => {
127
+ if (published > 0 || failed > 0) {
128
+ console.log(`Initial check: ${published} published, ${failed} failed`);
129
+ } else {
130
+ console.log('No posts due at startup');
131
+ }
132
+ console.log('');
133
+ });
134
+
135
+ // Schedule regular checks
136
+ const task = cron.schedule(cronExpression, async () => {
137
+ const timestamp = new Date().toISOString();
138
+ const db = getDatabase();
139
+ const pendingCount = db.getScheduledPosts('pending', 100).length;
140
+
141
+ if (pendingCount > 0) {
142
+ console.log(`[${timestamp}] Checking for due posts (${pendingCount} pending)...`);
143
+ const { published, failed } = await processDuePosts();
144
+ if (published > 0 || failed > 0) {
145
+ console.log(` Results: ${published} published, ${failed} failed`);
146
+ }
147
+ }
148
+ });
149
+
150
+ // Handle graceful shutdown
151
+ process.on('SIGINT', () => {
152
+ console.log('\nShutting down scheduler...');
153
+ task.stop();
154
+ const db = getDatabase();
155
+ db.close();
156
+ process.exit(0);
157
+ });
158
+
159
+ process.on('SIGTERM', () => {
160
+ console.log('\nShutting down scheduler...');
161
+ task.stop();
162
+ const db = getDatabase();
163
+ db.close();
164
+ process.exit(0);
165
+ });
166
+
167
+ return task;
168
+ }
169
+
170
+ /**
171
+ * CLI entry point
172
+ */
173
+ function main() {
174
+ // Check required environment variables
175
+ if (!process.env.LINKEDIN_ACCESS_TOKEN) {
176
+ console.error('Error: LINKEDIN_ACCESS_TOKEN not set in environment');
177
+ process.exit(1);
178
+ }
179
+
180
+ if (!process.env.LINKEDIN_PERSON_ID) {
181
+ console.error('Error: LINKEDIN_PERSON_ID not set in environment');
182
+ process.exit(1);
183
+ }
184
+
185
+ // Allow custom cron expression via CLI argument
186
+ const cronExpression = process.argv[2] || '* * * * *';
187
+
188
+ startScheduler(cronExpression);
189
+ }
190
+
191
+ // Export for testing
192
+ module.exports = {
193
+ publishPost,
194
+ processDuePosts,
195
+ startScheduler
196
+ };
197
+
198
+ // Run if called directly
199
+ if (require.main === module) {
200
+ main();
201
+ }