social-light 0.0.1

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,315 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import chalk from "chalk";
6
+ import { exec } from "child_process";
7
+ import {
8
+ getPosts,
9
+ getPostById,
10
+ createPost,
11
+ updatePost,
12
+ markAsPublished,
13
+ logAction,
14
+ } from "../utils/db.mjs";
15
+ import { getSocialAPI } from "../utils/social/index.mjs";
16
+ import {
17
+ generateTitle,
18
+ suggestPublishDate,
19
+ enhanceContent,
20
+ } from "../utils/ai.mjs";
21
+ import { getConfig } from "../utils/config.mjs";
22
+
23
+ // Get directory name in ESM
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ /**
27
+ * Start the web server
28
+ * @param {Object} argv - Command arguments
29
+ */
30
+ export const startServer = async (argv) => {
31
+ const app = express();
32
+ const port = argv.port || 3000;
33
+ const shouldOpen = !argv.noOpen; // Open by default, disabled with --no-open
34
+
35
+ // Middleware
36
+ app.use(cors());
37
+ app.use(express.json());
38
+
39
+ // Serve static files from 'client' directory
40
+ app.use(express.static(path.join(__dirname, "client")));
41
+
42
+ // API routes
43
+ setupApiRoutes(app);
44
+
45
+ // Serve React app for all other routes
46
+ app.get("*", (req, res) => {
47
+ res.sendFile(path.join(__dirname, "client", "index.html"));
48
+ });
49
+
50
+ // Start server
51
+ app.listen(port, () => {
52
+ const url = `http://localhost:${port}`;
53
+ console.log(chalk.green(`✓ Server started on port ${port}`));
54
+ console.log(
55
+ chalk.cyan(
56
+ `Opening ${url} in your browser...`
57
+ )
58
+ );
59
+ console.log(chalk.gray("Press Ctrl+C to stop the server"));
60
+
61
+ // Open URL in default browser if not disabled
62
+ if (shouldOpen) {
63
+ // Open URL in default browser based on platform
64
+ let command;
65
+ switch (process.platform) {
66
+ case 'darwin': // macOS
67
+ command = `open ${url}`;
68
+ break;
69
+ case 'win32': // Windows
70
+ command = `start ${url}`;
71
+ break;
72
+ default: // Linux and others
73
+ command = `xdg-open ${url}`;
74
+ }
75
+
76
+ // Execute the command to open the browser
77
+ exec(command, (error) => {
78
+ if (error) {
79
+ console.error(chalk.yellow(`Unable to open browser automatically: ${error.message}`));
80
+ console.log(chalk.cyan(`Open ${url} in your browser to access the web interface`));
81
+ }
82
+ });
83
+ } else {
84
+ console.log(chalk.cyan(`Open ${url} in your browser to access the web interface`));
85
+ }
86
+ });
87
+ };
88
+
89
+ /**
90
+ * Set up API routes
91
+ * @param {Express} app - Express app
92
+ */
93
+ const setupApiRoutes = (app) => {
94
+ // Get API info
95
+ app.get("/api", (req, res) => {
96
+ res.json({
97
+ name: "Social Light API",
98
+ version: "1.0.0",
99
+ endpoints: [
100
+ "/api/posts",
101
+ "/api/posts/:id",
102
+ "/api/publish/:id",
103
+ "/api/ai/title",
104
+ "/api/ai/date",
105
+ "/api/ai/enhance",
106
+ "/api/config",
107
+ ],
108
+ });
109
+ });
110
+
111
+ // Get configuration
112
+ app.get("/api/config", (req, res) => {
113
+ const config = getConfig();
114
+
115
+ // Remove sensitive information
116
+ const safeConfig = {
117
+ ...config,
118
+ // Add default platforms for client
119
+ platforms: [
120
+ // { id: 'twitter', name: 'Twitter', icon: 'twitter' },
121
+ { id: "bluesky", name: "Bluesky", icon: "cloud" },
122
+ // { id: 'tiktok', name: 'TikTok', icon: 'music' }
123
+ ],
124
+ };
125
+
126
+ res.json(safeConfig);
127
+ });
128
+
129
+ // Get all posts
130
+ app.get("/api/posts", (req, res) => {
131
+ try {
132
+ const published = req.query.published === "true";
133
+ const posts = getPosts({ published });
134
+ res.json(posts);
135
+ } catch (error) {
136
+ res.status(500).json({ error: error.message });
137
+ }
138
+ });
139
+
140
+ // Get post by ID
141
+ app.get("/api/posts/:id", (req, res) => {
142
+ try {
143
+ const post = getPostById(parseInt(req.params.id, 10));
144
+
145
+ if (!post) {
146
+ return res.status(404).json({ error: "Post not found" });
147
+ }
148
+
149
+ res.json(post);
150
+ } catch (error) {
151
+ res.status(500).json({ error: error.message });
152
+ }
153
+ });
154
+
155
+ // Create new post
156
+ app.post("/api/posts", async (req, res) => {
157
+ try {
158
+ const { title, content, platforms, publish_date } = req.body;
159
+
160
+ if (!content) {
161
+ return res.status(400).json({ error: "Content is required" });
162
+ }
163
+
164
+ const postId = createPost({
165
+ title,
166
+ content,
167
+ platforms: Array.isArray(platforms) ? platforms.join(",") : platforms,
168
+ publish_date,
169
+ });
170
+
171
+ logAction("post_created", { postId, source: "web" });
172
+
173
+ res.status(201).json({ id: postId });
174
+ } catch (error) {
175
+ res.status(500).json({ error: error.message });
176
+ }
177
+ });
178
+
179
+ // Update post
180
+ app.put("/api/posts/:id", async (req, res) => {
181
+ try {
182
+ const id = parseInt(req.params.id, 10);
183
+ const { title, content, platforms, publish_date } = req.body;
184
+
185
+ const post = getPostById(id);
186
+
187
+ if (!post) {
188
+ return res.status(404).json({ error: "Post not found" });
189
+ }
190
+
191
+ const success = updatePost(id, {
192
+ title,
193
+ content,
194
+ platforms: Array.isArray(platforms) ? platforms.join(",") : platforms,
195
+ publish_date,
196
+ });
197
+
198
+ if (!success) {
199
+ return res.status(500).json({ error: "Failed to update post" });
200
+ }
201
+
202
+ logAction("post_updated", { postId: id, source: "web" });
203
+
204
+ res.json({ success: true });
205
+ } catch (error) {
206
+ res.status(500).json({ error: error.message });
207
+ }
208
+ });
209
+
210
+ // Publish post
211
+ app.post("/api/publish/:id", async (req, res) => {
212
+ try {
213
+ const id = parseInt(req.params.id, 10);
214
+ const post = getPostById(id);
215
+
216
+ if (!post) {
217
+ return res.status(404).json({ error: "Post not found" });
218
+ }
219
+
220
+ if (post.published) {
221
+ return res.status(400).json({ error: "Post is already published" });
222
+ }
223
+
224
+ if (!post.platforms) {
225
+ return res
226
+ .status(400)
227
+ .json({ error: "No platforms specified for post" });
228
+ }
229
+
230
+ // Initialize social API
231
+ const socialAPI = getSocialAPI();
232
+ const platforms = post.platforms.split(",").map((p) => p.trim());
233
+
234
+ // Publish post to specified platforms
235
+ const result = await socialAPI.post({
236
+ text: post.content,
237
+ title: post.title,
238
+ platforms,
239
+ });
240
+
241
+ // Check if post was successfully published to at least one platform
242
+ const anySuccess = Object.values(result.results).some((r) => r.success);
243
+
244
+ if (anySuccess) {
245
+ markAsPublished(id);
246
+
247
+ // Log the action with platform results
248
+ logAction("post_published", {
249
+ postId: id,
250
+ platforms: result.results,
251
+ source: "web",
252
+ });
253
+
254
+ res.json({
255
+ success: true,
256
+ platforms: result.results,
257
+ });
258
+ } else {
259
+ res.status(500).json({
260
+ success: false,
261
+ error: "Failed to publish to any platform",
262
+ platforms: result.results,
263
+ });
264
+ }
265
+ } catch (error) {
266
+ res.status(500).json({ error: error.message });
267
+ }
268
+ });
269
+
270
+ // Generate title with AI
271
+ app.post("/api/ai/title", async (req, res) => {
272
+ try {
273
+ const { content } = req.body;
274
+
275
+ if (!content) {
276
+ return res.status(400).json({ error: "Content is required" });
277
+ }
278
+
279
+ const title = await generateTitle(content);
280
+ res.json({ title });
281
+ } catch (error) {
282
+ res.status(500).json({ error: error.message });
283
+ }
284
+ });
285
+
286
+ // Suggest publish date with AI
287
+ app.get("/api/ai/date", async (req, res) => {
288
+ try {
289
+ const date = await suggestPublishDate();
290
+ res.json({ date });
291
+ } catch (error) {
292
+ res.status(500).json({ error: error.message });
293
+ }
294
+ });
295
+
296
+ // Enhance content with AI
297
+ app.post("/api/ai/enhance", async (req, res) => {
298
+ try {
299
+ const { content, platform } = req.body;
300
+
301
+ if (!content) {
302
+ return res.status(400).json({ error: "Content is required" });
303
+ }
304
+
305
+ if (!platform) {
306
+ return res.status(400).json({ error: "Platform is required" });
307
+ }
308
+
309
+ const enhanced = await enhanceContent(content, platform);
310
+ res.json({ enhanced });
311
+ } catch (error) {
312
+ res.status(500).json({ error: error.message });
313
+ }
314
+ });
315
+ };
@@ -0,0 +1,201 @@
1
+ import { OpenAI } from 'openai';
2
+ import { getConfig } from './config.mjs';
3
+ import { getPosts, getDb } from './db.mjs';
4
+
5
+ // Initialize OpenAI client
6
+ let openaiClient = null;
7
+
8
+ /**
9
+ * Get OpenAI client instance
10
+ * @returns {OpenAI|null} OpenAI client or null if AI is disabled
11
+ * @example
12
+ * const openai = getOpenAIClient();
13
+ * if (openai) {
14
+ * // Use openai client
15
+ * }
16
+ */
17
+ export const getOpenAIClient = () => {
18
+ const config = getConfig();
19
+
20
+ if (!config.aiEnabled) {
21
+ return null;
22
+ }
23
+
24
+ if (!openaiClient) {
25
+ // Attempt to get API key from environment variable
26
+ const apiKey = process.env.OPENAI_API_KEY;
27
+
28
+ if (!apiKey) {
29
+ console.warn('Warning: OPENAI_API_KEY environment variable not set. AI features will be limited.');
30
+ return null;
31
+ }
32
+
33
+ openaiClient = new OpenAI({
34
+ apiKey
35
+ });
36
+ }
37
+
38
+ return openaiClient;
39
+ };
40
+
41
+ /**
42
+ * Generate a title for a post using AI
43
+ * @param {string} content - Post content
44
+ * @returns {string} Generated title
45
+ * @example
46
+ * const title = await generateTitle('This is my first post about AI technology...');
47
+ */
48
+ export const generateTitle = async (content) => {
49
+ const openai = getOpenAIClient();
50
+
51
+ if (!openai) {
52
+ // Fallback to simple title generation if AI is disabled
53
+ return content.split(' ').slice(0, 5).join(' ') + '...';
54
+ }
55
+
56
+ try {
57
+ const response = await openai.chat.completions.create({
58
+ model: 'gpt-3.5-turbo',
59
+ messages: [
60
+ {
61
+ role: 'system',
62
+ content: 'You are a headline writer assistant that creates catchy, engaging titles for social media posts. Generate a short, attention-grabbing title based on the content provided. Keep it under 60 characters if possible.'
63
+ },
64
+ {
65
+ role: 'user',
66
+ content: `Create a title for this social media post: "${content.substring(0, 500)}${content.length > 500 ? '...' : ''}"`
67
+ }
68
+ ],
69
+ max_tokens: 60
70
+ });
71
+
72
+ return response.choices[0].message.content.trim().replace(/^"|"$/g, '');
73
+ } catch (error) {
74
+ console.error('Error generating title:', error.message);
75
+ // Fallback
76
+ return content.split(' ').slice(0, 5).join(' ') + '...';
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Suggest a publish date for a post using AI
82
+ * @returns {string} Suggested publish date in YYYY-MM-DD format
83
+ * @example
84
+ * const date = await suggestPublishDate();
85
+ */
86
+ export const suggestPublishDate = async () => {
87
+ const openai = getOpenAIClient();
88
+
89
+ // Get database connection and check if posts table exists
90
+ const db = getDb();
91
+
92
+ try {
93
+ // Check if posts table exists
94
+ const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='posts';").get();
95
+
96
+ if (!tableExists) {
97
+ // If table doesn't exist, just return tomorrow's date
98
+ const tomorrow = new Date();
99
+ tomorrow.setDate(tomorrow.getDate() + 1);
100
+ return tomorrow.toISOString().split('T')[0];
101
+ }
102
+
103
+ // Get historical posts for analysis if table exists
104
+ const posts = getPosts();
105
+ const today = new Date();
106
+
107
+ if (!openai || posts.length < 3) {
108
+ // Fallback to simple date suggestion if AI is disabled or not enough data
109
+ const tomorrow = new Date(today);
110
+ tomorrow.setDate(tomorrow.getDate() + 1);
111
+ return tomorrow.toISOString().split('T')[0];
112
+ }
113
+
114
+ // Format post history for the AI
115
+ const postHistory = posts
116
+ .filter(post => post.publish_date)
117
+ .map(post => ({
118
+ date: post.publish_date,
119
+ platform: post.platforms
120
+ }));
121
+
122
+ const response = await openai.chat.completions.create({
123
+ model: 'gpt-3.5-turbo',
124
+ messages: [
125
+ {
126
+ role: 'system',
127
+ content: 'You are a social media scheduling assistant. Based on the user\'s posting history, suggest an optimal date for their next post. Consider post frequency, patterns, and optimal timing. Return only the date in YYYY-MM-DD format.'
128
+ },
129
+ {
130
+ role: 'user',
131
+ content: `Here is my posting history: ${JSON.stringify(postHistory)}. Today is ${today.toISOString().split('T')[0]}. When should I schedule my next post?`
132
+ }
133
+ ],
134
+ max_tokens: 20
135
+ });
136
+
137
+ const suggestedDate = response.choices[0].message.content.trim();
138
+
139
+ // Validate date format
140
+ if (/^\d{4}-\d{2}-\d{2}$/.test(suggestedDate)) {
141
+ return suggestedDate;
142
+ }
143
+
144
+ // Extract date if the AI included other text
145
+ const dateMatch = suggestedDate.match(/\d{4}-\d{2}-\d{2}/);
146
+ if (dateMatch) {
147
+ return dateMatch[0];
148
+ }
149
+
150
+ // Fallback
151
+ const tomorrow = new Date(today);
152
+ tomorrow.setDate(tomorrow.getDate() + 1);
153
+ return tomorrow.toISOString().split('T')[0];
154
+ } catch (error) {
155
+ console.error('Error suggesting publish date:', error.message);
156
+
157
+ // Fallback
158
+ const today = new Date();
159
+ const tomorrow = new Date(today);
160
+ tomorrow.setDate(tomorrow.getDate() + 1);
161
+ return tomorrow.toISOString().split('T')[0];
162
+ }
163
+ };
164
+
165
+ /**
166
+ * Enhance content with AI suggestions
167
+ * @param {string} content - Original content
168
+ * @param {string} platform - Target platform
169
+ * @returns {string} Enhanced content
170
+ * @example
171
+ * const enhanced = await enhanceContent('Check out our new product!', 'Twitter');
172
+ */
173
+ export const enhanceContent = async (content, platform) => {
174
+ const openai = getOpenAIClient();
175
+
176
+ if (!openai) {
177
+ return content;
178
+ }
179
+
180
+ try {
181
+ const response = await openai.chat.completions.create({
182
+ model: 'gpt-3.5-turbo',
183
+ messages: [
184
+ {
185
+ role: 'system',
186
+ content: `You are a social media expert who helps enhance posts for ${platform}. Provide an improved version of the user's content, optimized for engagement on ${platform}. You may suggest hashtags, emojis, or slight rewording, but preserve the original message and voice.`
187
+ },
188
+ {
189
+ role: 'user',
190
+ content: `Enhance this ${platform} post: "${content}"`
191
+ }
192
+ ],
193
+ max_tokens: 1000
194
+ });
195
+
196
+ return response.choices[0].message.content.trim().replace(/^"|"$/g, '');
197
+ } catch (error) {
198
+ console.error('Error enhancing content:', error.message);
199
+ return content;
200
+ }
201
+ };
@@ -0,0 +1,127 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ // Default configuration
6
+ const defaultConfig = {
7
+ dbPath: "~/.social-light/social-light.db",
8
+ defaultPlatforms: ["Bluesky"],
9
+ aiEnabled: true,
10
+ credentials: {
11
+ openai: {
12
+ apiKey: "",
13
+ },
14
+ bluesky: {
15
+ handle: "",
16
+ password: "",
17
+ service: "https://bsky.social",
18
+ },
19
+ },
20
+ };
21
+
22
+ /**
23
+ * Get config file path
24
+ * @returns {string} Path to config file
25
+ * @example
26
+ * const configPath = getConfigPath();
27
+ */
28
+ export const getConfigPath = () => {
29
+ return path.join(os.homedir(), ".social-light", "config.json");
30
+ };
31
+
32
+ /**
33
+ * Check if config exists
34
+ * @returns {boolean} True if config exists
35
+ * @example
36
+ * const exists = configExists();
37
+ */
38
+ export const configExists = () => {
39
+ return fs.existsSync(getConfigPath());
40
+ };
41
+
42
+ /**
43
+ * Create default config
44
+ * @returns {Object} Created config object
45
+ * @example
46
+ * const config = createDefaultConfig();
47
+ */
48
+ export const createDefaultConfig = () => {
49
+ const configPath = getConfigPath();
50
+ const configDir = path.dirname(configPath);
51
+
52
+ fs.ensureDirSync(configDir);
53
+ fs.writeJsonSync(configPath, defaultConfig, { spaces: 2 });
54
+
55
+ return defaultConfig;
56
+ };
57
+
58
+ /**
59
+ * Get current config, creating default if it doesn't exist
60
+ * @returns {Object} Config object
61
+ * @example
62
+ * const config = getConfig();
63
+ * console.log(config.aiEnabled);
64
+ */
65
+ export const getConfig = () => {
66
+ if (!configExists()) {
67
+ return createDefaultConfig();
68
+ }
69
+
70
+ return fs.readJsonSync(getConfigPath());
71
+ };
72
+
73
+ /**
74
+ * Update config
75
+ * @param {Object} updates - Config fields to update
76
+ * @returns {Object} Updated config
77
+ * @example
78
+ * const updatedConfig = updateConfig({ aiEnabled: false });
79
+ */
80
+ export const updateConfig = (updates) => {
81
+ const config = getConfig();
82
+ const newConfig = { ...config, ...updates };
83
+
84
+ fs.writeJsonSync(getConfigPath(), newConfig, { spaces: 2 });
85
+
86
+ return newConfig;
87
+ };
88
+
89
+ /**
90
+ * Get credentials from config
91
+ * @param {string} platform - Platform name (e.g., 'bluesky', 'openai')
92
+ * @returns {Object} Credentials object
93
+ * @example
94
+ * const blueskyCredentials = getCredentials('bluesky');
95
+ */
96
+ export const getCredentials = (platform) => {
97
+ const config = getConfig();
98
+ return config.credentials?.[platform.toLowerCase()] || {};
99
+ };
100
+
101
+ /**
102
+ * Update credentials in config
103
+ * @param {string} platform - Platform name (e.g., 'bluesky', 'openai')
104
+ * @param {Object} credentials - Credentials object
105
+ * @returns {Object} Updated config
106
+ * @example
107
+ * const updatedConfig = updateCredentials('bluesky', { handle: 'user.bsky.social', password: 'apppassword' });
108
+ */
109
+ export const updateCredentials = (platform, credentials) => {
110
+ const config = getConfig();
111
+
112
+ // Ensure credentials object exists
113
+ if (!config.credentials) {
114
+ config.credentials = {};
115
+ }
116
+
117
+ // Update credentials for the platform
118
+ config.credentials[platform.toLowerCase()] = {
119
+ ...config.credentials[platform.toLowerCase()],
120
+ ...credentials,
121
+ };
122
+
123
+ // Save updated config
124
+ fs.writeJsonSync(getConfigPath(), config, { spaces: 2 });
125
+
126
+ return config;
127
+ };