social-light 0.1.2 → 0.1.4
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/CLAUDE.md +113 -0
- package/README.md +7 -3
- package/package.json +2 -2
- package/src/commands/create.mjs +88 -12
- package/src/commands/edit.mjs +76 -4
- package/src/commands/init.mjs +0 -1
- package/src/commands/publish.mjs +14 -8
- package/src/commands/uninit.mjs +114 -0
- package/src/index.mjs +2 -0
- package/src/server/client/index.html +17 -0
- package/src/server/client/main.mjs +217 -18
- package/src/server/client/styles.css +23 -0
- package/src/server/index.mjs +79 -9
- package/src/utils/ai.mjs +29 -19
- package/src/utils/db.mjs +37 -0
package/CLAUDE.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
Social Light is an AI-powered social media scheduling tool for Bluesky (with more platforms planned). It provides both a CLI and web interface for creating, scheduling, and publishing posts.
|
8
|
+
|
9
|
+
## Commands
|
10
|
+
|
11
|
+
### Development Commands
|
12
|
+
|
13
|
+
```bash
|
14
|
+
# Install dependencies
|
15
|
+
npm install
|
16
|
+
|
17
|
+
# Run CLI in development mode (with auto-restart)
|
18
|
+
npm run dev
|
19
|
+
|
20
|
+
# Start the web server
|
21
|
+
npm run server
|
22
|
+
# OR with custom port
|
23
|
+
node src/server/index.mjs --port 8080
|
24
|
+
|
25
|
+
# Make CLI globally available
|
26
|
+
chmod +x src/index.mjs
|
27
|
+
npm link
|
28
|
+
```
|
29
|
+
|
30
|
+
### CLI Commands
|
31
|
+
|
32
|
+
```bash
|
33
|
+
# Initialize social-light
|
34
|
+
social-light init
|
35
|
+
|
36
|
+
# Create a new post
|
37
|
+
social-light create
|
38
|
+
|
39
|
+
# List unpublished posts
|
40
|
+
social-light list
|
41
|
+
# List all posts (including published)
|
42
|
+
social-light list --published
|
43
|
+
|
44
|
+
# Edit a post by index
|
45
|
+
social-light edit 1
|
46
|
+
|
47
|
+
# Publish eligible posts
|
48
|
+
social-light publish
|
49
|
+
# Run in continuous mode (daemon)
|
50
|
+
social-light publish --continuous
|
51
|
+
|
52
|
+
# Clean published posts
|
53
|
+
social-light clean
|
54
|
+
# Clean all posts (including unpublished)
|
55
|
+
social-light clean --unpublished
|
56
|
+
|
57
|
+
# Start web interface
|
58
|
+
social-light server
|
59
|
+
# With custom port
|
60
|
+
social-light server --port 8080
|
61
|
+
|
62
|
+
# Remove social-light configuration
|
63
|
+
social-light uninit
|
64
|
+
```
|
65
|
+
|
66
|
+
## Architecture
|
67
|
+
|
68
|
+
### Core Components
|
69
|
+
|
70
|
+
1. **CLI Commands** (`src/commands/`) - Implementations of CLI commands
|
71
|
+
2. **Configuration** (`src/utils/config.mjs`) - Manages app configuration in `~/.social-light/config.json`
|
72
|
+
3. **Database** (`src/utils/db.mjs`) - SQLite database operations using better-sqlite3 and Knex
|
73
|
+
4. **Social Platform APIs** (`src/utils/social/`) - Interface with social media platforms
|
74
|
+
5. **AI Utilities** (`src/utils/ai.mjs`) - OpenAI integration for content enhancement
|
75
|
+
6. **Web Server** (`src/server/`) - Express server and client-side web interface
|
76
|
+
|
77
|
+
### Data Flow
|
78
|
+
|
79
|
+
1. User creates/edits posts via CLI or web interface
|
80
|
+
2. Posts are stored in SQLite database with metadata
|
81
|
+
3. AI enhances content when requested (via OpenAI)
|
82
|
+
4. Scheduled posts are published to platforms (currently Bluesky)
|
83
|
+
5. Post status is updated in database
|
84
|
+
|
85
|
+
### Configuration
|
86
|
+
|
87
|
+
Configuration is stored in `~/.social-light/config.json` and includes:
|
88
|
+
- Database path
|
89
|
+
- Platform credentials
|
90
|
+
- AI settings (OpenAI API key)
|
91
|
+
- Default platforms
|
92
|
+
|
93
|
+
### Database
|
94
|
+
|
95
|
+
SQLite database (`~/.social-light/social-light.db`) with tables for:
|
96
|
+
- Posts (title, content, platforms, publish date, published status)
|
97
|
+
- Activity logs
|
98
|
+
|
99
|
+
## Technology Stack
|
100
|
+
|
101
|
+
- Node.js (ES Modules)
|
102
|
+
- SQLite via better-sqlite3
|
103
|
+
- Express for web server
|
104
|
+
- OpenAI API for AI features
|
105
|
+
- Bluesky AT Protocol integration
|
106
|
+
- Inquirer for CLI interaction
|
107
|
+
|
108
|
+
## Development Notes
|
109
|
+
|
110
|
+
- The application requires Node.js >= 20.9.0
|
111
|
+
- ES Modules are used throughout the codebase (import/export syntax)
|
112
|
+
- No formal testing framework is currently implemented
|
113
|
+
- Configuration follows XDG standards for user data in home directory
|
package/README.md
CHANGED
@@ -4,6 +4,13 @@
|
|
4
4
|
|
5
5
|
An AI-powered social media scheduling tool for Bluesky with CLI and web interface. More platforms coming soon!
|
6
6
|
|
7
|
+
|
8
|
+
## Video overview
|
9
|
+
|
10
|
+
<video src="https://github.com/user-attachments/assets/7ef8f61f-0421-41a9-8828-29ec44c2cc19
|
11
|
+
" style="height:480px" >
|
12
|
+
|
13
|
+
|
7
14
|
## Features
|
8
15
|
|
9
16
|
- **CLI Interface**: Create, manage, and publish posts directly from your terminal
|
@@ -19,9 +26,6 @@ An AI-powered social media scheduling tool for Bluesky with CLI and web interfac
|
|
19
26
|
|
20
27
|
[Node.js/npm](https://nodejs.org) must be installed on your system
|
21
28
|
|
22
|
-
> [!WARNING]
|
23
|
-
> There's a know issues with the latest version of Node.js (v23) and 'better-sqlite3'. Please use Node.js v22 (LTS) or lower.
|
24
|
-
|
25
29
|
## Accounts
|
26
30
|
|
27
31
|
You will need an account on [blue sky](https://bsky.app) and an [OpenAI](https://openai.com) account to use the AI features.
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "social-light",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.4",
|
4
4
|
"description": "AI-powered social media scheduling tool",
|
5
5
|
"main": "src/index.mjs",
|
6
6
|
"type": "module",
|
@@ -24,7 +24,7 @@
|
|
24
24
|
"license": "MIT",
|
25
25
|
"dependencies": {
|
26
26
|
"ai": "^2.2.31",
|
27
|
-
"better-sqlite3": "^9.
|
27
|
+
"better-sqlite3": "^11.9.1",
|
28
28
|
"chalk": "^5.3.0",
|
29
29
|
"cors": "^2.8.5",
|
30
30
|
"dotenv": "^16.3.1",
|
package/src/commands/create.mjs
CHANGED
@@ -129,14 +129,30 @@ export const createPost = async (argv) => {
|
|
129
129
|
title = titleInput;
|
130
130
|
|
131
131
|
// Generate publish date with AI or prompt for manual entry
|
132
|
-
spinner = ora("Suggesting publish date...").start();
|
133
|
-
let
|
132
|
+
spinner = ora("Suggesting publish date and time...").start();
|
133
|
+
let publishDateTime = "";
|
134
134
|
|
135
135
|
if (config.aiEnabled) {
|
136
|
-
|
137
|
-
spinner.succeed(`Suggested publish date: ${
|
136
|
+
publishDateTime = await suggestPublishDate();
|
137
|
+
spinner.succeed(`Suggested publish date and time: ${publishDateTime}`);
|
138
138
|
} else {
|
139
|
-
spinner.info("AI is disabled, skipping date suggestion");
|
139
|
+
spinner.info("AI is disabled, skipping date/time suggestion");
|
140
|
+
// Provide default as tomorrow at noon
|
141
|
+
const tomorrow = new Date();
|
142
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
143
|
+
tomorrow.setHours(12, 0, 0, 0);
|
144
|
+
publishDateTime = `${tomorrow.toISOString().split('T')[0]} 12:00`;
|
145
|
+
}
|
146
|
+
|
147
|
+
// Parse suggested date/time
|
148
|
+
let suggestedDate = "";
|
149
|
+
let suggestedTime = "";
|
150
|
+
|
151
|
+
if (publishDateTime.includes(" ")) {
|
152
|
+
[suggestedDate, suggestedTime] = publishDateTime.split(" ");
|
153
|
+
} else {
|
154
|
+
suggestedDate = publishDateTime;
|
155
|
+
suggestedTime = "12:00";
|
140
156
|
}
|
141
157
|
|
142
158
|
// Allow manual date override
|
@@ -146,7 +162,7 @@ export const createPost = async (argv) => {
|
|
146
162
|
name: "dateInput",
|
147
163
|
message:
|
148
164
|
"Enter publish date (YYYY-MM-DD) or press Enter to use suggestion:",
|
149
|
-
default:
|
165
|
+
default: suggestedDate,
|
150
166
|
validate: (input) => {
|
151
167
|
if (!input) return true;
|
152
168
|
return /^\d{4}-\d{2}-\d{2}$/.test(input)
|
@@ -155,8 +171,68 @@ export const createPost = async (argv) => {
|
|
155
171
|
},
|
156
172
|
},
|
157
173
|
]);
|
158
|
-
|
159
|
-
|
174
|
+
|
175
|
+
// Allow manual time override with flexible input
|
176
|
+
const { timeInput } = await inquirer.prompt([
|
177
|
+
{
|
178
|
+
type: "input",
|
179
|
+
name: "timeInput",
|
180
|
+
message:
|
181
|
+
"Enter publish time (HH:MM, H:MM, HH:MMam/pm or just 'Xpm') or press Enter to use suggestion:",
|
182
|
+
default: suggestedTime,
|
183
|
+
validate: (input) => {
|
184
|
+
if (!input) return true;
|
185
|
+
|
186
|
+
// Support various formats
|
187
|
+
// 24-hour format: 13:45, 9:30
|
188
|
+
if (/^\d{1,2}:\d{2}$/.test(input)) return true;
|
189
|
+
|
190
|
+
// 12-hour with am/pm: 1:45pm, 9:30am
|
191
|
+
if (/^\d{1,2}:\d{2}(am|pm)$/i.test(input)) return true;
|
192
|
+
|
193
|
+
// Simple hour with am/pm: 3pm, 11am
|
194
|
+
if (/^\d{1,2}(am|pm)$/i.test(input)) return true;
|
195
|
+
|
196
|
+
// Just hour: 13, 9 (assumes on the hour)
|
197
|
+
if (/^\d{1,2}$/.test(input)) return true;
|
198
|
+
|
199
|
+
return "Please enter a valid time format";
|
200
|
+
},
|
201
|
+
},
|
202
|
+
]);
|
203
|
+
|
204
|
+
// Convert time input to standardized 24-hour format
|
205
|
+
let standardizedTime = timeInput;
|
206
|
+
|
207
|
+
// Process different time formats
|
208
|
+
if (/^\d{1,2}(am|pm)$/i.test(timeInput)) {
|
209
|
+
// Format like "3pm"
|
210
|
+
const isPM = timeInput.toLowerCase().includes("pm");
|
211
|
+
let hour = parseInt(timeInput.replace(/[^0-9]/g, ""));
|
212
|
+
|
213
|
+
if (isPM && hour < 12) hour += 12;
|
214
|
+
if (!isPM && hour === 12) hour = 0;
|
215
|
+
|
216
|
+
standardizedTime = `${hour.toString().padStart(2, "0")}:00`;
|
217
|
+
} else if (/^\d{1,2}:\d{2}(am|pm)$/i.test(timeInput)) {
|
218
|
+
// Format like "3:30pm"
|
219
|
+
const isPM = timeInput.toLowerCase().includes("pm");
|
220
|
+
const timeParts = timeInput.replace(/[^0-9:]/g, "").split(":");
|
221
|
+
let hour = parseInt(timeParts[0]);
|
222
|
+
const minute = timeParts[1];
|
223
|
+
|
224
|
+
if (isPM && hour < 12) hour += 12;
|
225
|
+
if (!isPM && hour === 12) hour = 0;
|
226
|
+
|
227
|
+
standardizedTime = `${hour.toString().padStart(2, "0")}:${minute}`;
|
228
|
+
} else if (/^\d{1,2}$/.test(timeInput)) {
|
229
|
+
// Format like "15" (just hour)
|
230
|
+
const hour = parseInt(timeInput);
|
231
|
+
standardizedTime = `${hour.toString().padStart(2, "0")}:00`;
|
232
|
+
}
|
233
|
+
|
234
|
+
// Combine date and time
|
235
|
+
publishDateTime = `${dateInput} ${standardizedTime}`;
|
160
236
|
|
161
237
|
// Select platforms
|
162
238
|
const { selectedPlatforms } = await inquirer.prompt([
|
@@ -229,7 +305,7 @@ export const createPost = async (argv) => {
|
|
229
305
|
title,
|
230
306
|
content,
|
231
307
|
platforms,
|
232
|
-
publish_date:
|
308
|
+
publish_date: publishDateTime,
|
233
309
|
});
|
234
310
|
|
235
311
|
// Log the action
|
@@ -237,7 +313,7 @@ export const createPost = async (argv) => {
|
|
237
313
|
postId,
|
238
314
|
title,
|
239
315
|
platforms: selectedPlatforms,
|
240
|
-
publishDate,
|
316
|
+
publishDate: publishDateTime,
|
241
317
|
});
|
242
318
|
|
243
319
|
spinner.succeed(`Post created successfully with ID: ${postId}`);
|
@@ -249,8 +325,8 @@ export const createPost = async (argv) => {
|
|
249
325
|
` ${chalk.gray("•")} ${chalk.bold("Platforms:")} ${platforms || "None"}`
|
250
326
|
);
|
251
327
|
console.log(
|
252
|
-
` ${chalk.gray("•")} ${chalk.bold("Publish Date:")} ${
|
253
|
-
|
328
|
+
` ${chalk.gray("•")} ${chalk.bold("Publish Date & Time:")} ${
|
329
|
+
publishDateTime || "Not scheduled"
|
254
330
|
}`
|
255
331
|
);
|
256
332
|
console.log(
|
package/src/commands/edit.mjs
CHANGED
@@ -125,13 +125,28 @@ export const editPost = async (argv) => {
|
|
125
125
|
content = result.content;
|
126
126
|
}
|
127
127
|
|
128
|
-
//
|
129
|
-
|
128
|
+
// Parse existing publish date and time
|
129
|
+
let existingDate = "";
|
130
|
+
let existingTime = "12:00";
|
131
|
+
|
132
|
+
if (post.publish_date) {
|
133
|
+
if (post.publish_date.includes(" ")) {
|
134
|
+
[existingDate, existingTime] = post.publish_date.split(" ");
|
135
|
+
} else if (post.publish_date.includes("T")) {
|
136
|
+
[existingDate, existingTime] = post.publish_date.split("T");
|
137
|
+
existingTime = existingTime.substring(0, 5); // Get HH:MM part
|
138
|
+
} else {
|
139
|
+
existingDate = post.publish_date;
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
// Get publish date and time
|
144
|
+
const { publishDate, publishTime, platforms } = await inquirer.prompt([
|
130
145
|
{
|
131
146
|
type: "input",
|
132
147
|
name: "publishDate",
|
133
148
|
message: "Edit publish date (YYYY-MM-DD):",
|
134
|
-
default:
|
149
|
+
default: existingDate || "",
|
135
150
|
validate: (input) => {
|
136
151
|
if (!input) return true;
|
137
152
|
return /^\d{4}-\d{2}-\d{2}$/.test(input)
|
@@ -139,6 +154,30 @@ export const editPost = async (argv) => {
|
|
139
154
|
: "Please use YYYY-MM-DD format";
|
140
155
|
},
|
141
156
|
},
|
157
|
+
{
|
158
|
+
type: "input",
|
159
|
+
name: "publishTime",
|
160
|
+
message: "Edit publish time (HH:MM, H:MM, HH:MMam/pm or just 'Xpm'):",
|
161
|
+
default: existingTime || "12:00",
|
162
|
+
validate: (input) => {
|
163
|
+
if (!input) return true;
|
164
|
+
|
165
|
+
// Support various formats
|
166
|
+
// 24-hour format: 13:45, 9:30
|
167
|
+
if (/^\d{1,2}:\d{2}$/.test(input)) return true;
|
168
|
+
|
169
|
+
// 12-hour with am/pm: 1:45pm, 9:30am
|
170
|
+
if (/^\d{1,2}:\d{2}(am|pm)$/i.test(input)) return true;
|
171
|
+
|
172
|
+
// Simple hour with am/pm: 3pm, 11am
|
173
|
+
if (/^\d{1,2}(am|pm)$/i.test(input)) return true;
|
174
|
+
|
175
|
+
// Just hour: 13, 9 (assumes on the hour)
|
176
|
+
if (/^\d{1,2}$/.test(input)) return true;
|
177
|
+
|
178
|
+
return "Please enter a valid time format";
|
179
|
+
},
|
180
|
+
},
|
142
181
|
{
|
143
182
|
type: "checkbox",
|
144
183
|
name: "platforms",
|
@@ -158,13 +197,46 @@ export const editPost = async (argv) => {
|
|
158
197
|
]);
|
159
198
|
|
160
199
|
const config = getConfig();
|
200
|
+
|
201
|
+
// Format the time
|
202
|
+
let standardizedTime = publishTime;
|
203
|
+
|
204
|
+
// Process different time formats
|
205
|
+
if (/^\d{1,2}(am|pm)$/i.test(publishTime)) {
|
206
|
+
// Format like "3pm"
|
207
|
+
const isPM = publishTime.toLowerCase().includes("pm");
|
208
|
+
let hour = parseInt(publishTime.replace(/[^0-9]/g, ""));
|
209
|
+
|
210
|
+
if (isPM && hour < 12) hour += 12;
|
211
|
+
if (!isPM && hour === 12) hour = 0;
|
212
|
+
|
213
|
+
standardizedTime = `${hour.toString().padStart(2, "0")}:00`;
|
214
|
+
} else if (/^\d{1,2}:\d{2}(am|pm)$/i.test(publishTime)) {
|
215
|
+
// Format like "3:30pm"
|
216
|
+
const isPM = publishTime.toLowerCase().includes("pm");
|
217
|
+
const timeParts = publishTime.replace(/[^0-9:]/g, "").split(":");
|
218
|
+
let hour = parseInt(timeParts[0]);
|
219
|
+
const minute = timeParts[1];
|
220
|
+
|
221
|
+
if (isPM && hour < 12) hour += 12;
|
222
|
+
if (!isPM && hour === 12) hour = 0;
|
223
|
+
|
224
|
+
standardizedTime = `${hour.toString().padStart(2, "0")}:${minute}`;
|
225
|
+
} else if (/^\d{1,2}$/.test(publishTime)) {
|
226
|
+
// Format like "15" (just hour)
|
227
|
+
const hour = parseInt(publishTime);
|
228
|
+
standardizedTime = `${hour.toString().padStart(2, "0")}:00`;
|
229
|
+
}
|
230
|
+
|
231
|
+
// Combine date and time if date is provided
|
232
|
+
const fullPublishDate = publishDate ? `${publishDate} ${standardizedTime}` : "";
|
161
233
|
|
162
234
|
// Update post in database
|
163
235
|
const updatedPost = {
|
164
236
|
title,
|
165
237
|
content,
|
166
238
|
platforms: platforms.join(","),
|
167
|
-
publish_date:
|
239
|
+
publish_date: fullPublishDate,
|
168
240
|
};
|
169
241
|
|
170
242
|
const success = updatePost(post.id, updatedPost);
|
package/src/commands/init.mjs
CHANGED
package/src/commands/publish.mjs
CHANGED
@@ -15,15 +15,21 @@ const isEligibleForPublishing = (post) => {
|
|
15
15
|
return true;
|
16
16
|
}
|
17
17
|
|
18
|
-
// Check if publish date is
|
19
|
-
|
18
|
+
// Check if publish date and time is now or in the past
|
19
|
+
let publishDateTime;
|
20
|
+
|
21
|
+
// If the date includes time component (contains 'T' or ' ')
|
22
|
+
if (post.publish_date.includes('T') || post.publish_date.includes(' ')) {
|
23
|
+
publishDateTime = new Date(post.publish_date);
|
24
|
+
} else {
|
25
|
+
// If only date is provided, assume start of day
|
26
|
+
publishDateTime = new Date(post.publish_date);
|
27
|
+
publishDateTime.setHours(0, 0, 0, 0);
|
28
|
+
}
|
29
|
+
|
20
30
|
const now = new Date();
|
21
|
-
|
22
|
-
|
23
|
-
publishDate.setHours(0, 0, 0, 0);
|
24
|
-
now.setHours(0, 0, 0, 0);
|
25
|
-
|
26
|
-
return publishDate <= now;
|
31
|
+
|
32
|
+
return publishDateTime <= now;
|
27
33
|
};
|
28
34
|
|
29
35
|
/**
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import chalk from "chalk";
|
2
|
+
import ora from "ora";
|
3
|
+
import inquirer from "inquirer";
|
4
|
+
import fs from "fs-extra";
|
5
|
+
import path from "path";
|
6
|
+
import dotenv from "dotenv";
|
7
|
+
import os from "os";
|
8
|
+
|
9
|
+
import { configExists } from "../utils/config.mjs";
|
10
|
+
|
11
|
+
// Load environment variables
|
12
|
+
dotenv.config();
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Remove the Social Light application configuration
|
16
|
+
* @example
|
17
|
+
* await uninitialize();
|
18
|
+
*/
|
19
|
+
export const uninitialize = async () => {
|
20
|
+
const spinner = ora("Removing Social Light...").start();
|
21
|
+
const deletedFiles = [];
|
22
|
+
|
23
|
+
try {
|
24
|
+
const exists = configExists();
|
25
|
+
|
26
|
+
// If config already prompt for confirmation
|
27
|
+
if (exists) {
|
28
|
+
spinner.stop();
|
29
|
+
|
30
|
+
const { confirm } = await inquirer.prompt([
|
31
|
+
{
|
32
|
+
type: "confirm",
|
33
|
+
name: "confirm",
|
34
|
+
message: "Delete Social Light configuration?",
|
35
|
+
default: false,
|
36
|
+
},
|
37
|
+
]);
|
38
|
+
|
39
|
+
if (!confirm) {
|
40
|
+
console.log(chalk.yellow("Unitialization cancelled."));
|
41
|
+
return;
|
42
|
+
}
|
43
|
+
|
44
|
+
spinner.start("Removing Social Light configuration...");
|
45
|
+
|
46
|
+
// Remove configuration file in current directory
|
47
|
+
const configPath = path.join(process.cwd(), "config.json");
|
48
|
+
if (fs.existsSync(configPath)) {
|
49
|
+
fs.removeSync(configPath);
|
50
|
+
deletedFiles.push(configPath);
|
51
|
+
spinner.text = "Configuration file removed...";
|
52
|
+
}
|
53
|
+
|
54
|
+
// Remove database files
|
55
|
+
const dbDir = path.join(process.cwd(), "data");
|
56
|
+
if (fs.existsSync(dbDir)) {
|
57
|
+
fs.removeSync(dbDir);
|
58
|
+
deletedFiles.push(dbDir);
|
59
|
+
spinner.text = "Database files removed...";
|
60
|
+
}
|
61
|
+
|
62
|
+
// Remove configuration in $HOME/.social-light directory
|
63
|
+
const homeSocialLightDir = path.join(os.homedir(), ".social-light");
|
64
|
+
if (fs.existsSync(homeSocialLightDir)) {
|
65
|
+
// Get list of files before deletion for reporting
|
66
|
+
const configFiles = fs
|
67
|
+
.readdirSync(homeSocialLightDir)
|
68
|
+
.map((file) => path.join(homeSocialLightDir, file));
|
69
|
+
|
70
|
+
// Add them to deleted files list
|
71
|
+
deletedFiles.push(...configFiles);
|
72
|
+
|
73
|
+
// Remove the directory and all contents
|
74
|
+
fs.removeSync(homeSocialLightDir);
|
75
|
+
spinner.text = "Home social-light directory removed...";
|
76
|
+
}
|
77
|
+
|
78
|
+
// Clean environment variables related to Social Light
|
79
|
+
const envPath = path.join(process.cwd(), ".env");
|
80
|
+
if (fs.existsSync(envPath)) {
|
81
|
+
fs.removeSync(envPath);
|
82
|
+
deletedFiles.push(envPath);
|
83
|
+
spinner.text = "Environment file removed...";
|
84
|
+
}
|
85
|
+
|
86
|
+
spinner.succeed("Social Light configuration removed successfully!");
|
87
|
+
|
88
|
+
// Print list of deleted files
|
89
|
+
console.log(
|
90
|
+
chalk.green("\n✓ All Social Light configurations have been removed.")
|
91
|
+
);
|
92
|
+
|
93
|
+
if (deletedFiles.length > 0) {
|
94
|
+
console.log(chalk.cyan("\nDeleted files and directories:"));
|
95
|
+
deletedFiles.forEach((file) => {
|
96
|
+
console.log(chalk.gray(` • ${file}`));
|
97
|
+
});
|
98
|
+
}
|
99
|
+
|
100
|
+
console.log(
|
101
|
+
chalk.cyan("\nYou can reinitialize anytime with:"),
|
102
|
+
chalk.cyan("social-light init")
|
103
|
+
);
|
104
|
+
return;
|
105
|
+
}
|
106
|
+
|
107
|
+
spinner.info("Not initialized. No configuration to remove.");
|
108
|
+
return;
|
109
|
+
} catch (error) {
|
110
|
+
spinner.fail(`Unintialization failed: ${error.message}`);
|
111
|
+
console.error(chalk.red("Error details:"), error);
|
112
|
+
process.exit(1);
|
113
|
+
}
|
114
|
+
};
|
package/src/index.mjs
CHANGED
@@ -10,6 +10,7 @@ import { hideBin } from "yargs/helpers";
|
|
10
10
|
import chalk from "chalk";
|
11
11
|
|
12
12
|
import { initialize } from "./commands/init.mjs";
|
13
|
+
import { uninitialize } from "./commands/uninit.mjs";
|
13
14
|
import { createPost } from "./commands/create.mjs";
|
14
15
|
|
15
16
|
import { list } from "./commands/list.mjs";
|
@@ -41,6 +42,7 @@ const main = async () => {
|
|
41
42
|
console.log(NPMPackage.version);
|
42
43
|
})
|
43
44
|
.command("init", "Initialize Social Light configuration", {}, initialize)
|
45
|
+
.command("uninit", "Remove Social Light configuration", {}, uninitialize)
|
44
46
|
.command(
|
45
47
|
"create",
|
46
48
|
"Create a new social media post",
|
@@ -23,6 +23,23 @@
|
|
23
23
|
--padding-card: 20px;
|
24
24
|
--padding-section: 16px;
|
25
25
|
}
|
26
|
+
|
27
|
+
/* Fix for calendar layout */
|
28
|
+
.calendar {
|
29
|
+
width: 100%;
|
30
|
+
overflow-x: hidden;
|
31
|
+
}
|
32
|
+
|
33
|
+
/* Fix for time input to match other inputs */
|
34
|
+
input[type="time"] {
|
35
|
+
appearance: none;
|
36
|
+
-webkit-appearance: none;
|
37
|
+
padding: 8px 12px;
|
38
|
+
border-radius: var(--border-radius);
|
39
|
+
border: 1px solid var(--color-border);
|
40
|
+
background-color: var(--color-bg-dark);
|
41
|
+
color: var(--color-text-primary);
|
42
|
+
}
|
26
43
|
</style>
|
27
44
|
</head>
|
28
45
|
<body>
|
@@ -215,6 +215,9 @@ const renderUnpublishedPosts = () => {
|
|
215
215
|
<button class="btn btn-sm btn-action" data-action="publish-post" data-post-id="${
|
216
216
|
post.id
|
217
217
|
}">Publish</button>
|
218
|
+
<button class="btn btn-sm btn-danger" data-action="delete-post" data-post-id="${
|
219
|
+
post.id
|
220
|
+
}">Delete</button>
|
218
221
|
</div>
|
219
222
|
</div>
|
220
223
|
</div>
|
@@ -250,6 +253,18 @@ const renderUnpublishedPosts = () => {
|
|
250
253
|
await publishPost(postId);
|
251
254
|
});
|
252
255
|
});
|
256
|
+
|
257
|
+
// Delete post buttons
|
258
|
+
mainContent
|
259
|
+
.querySelectorAll('[data-action="delete-post"]')
|
260
|
+
.forEach((button) => {
|
261
|
+
button.addEventListener("click", async () => {
|
262
|
+
if (confirm("Are you sure you want to delete this post? This action cannot be undone.")) {
|
263
|
+
const postId = parseInt(button.dataset.postId, 10);
|
264
|
+
await deletePost(postId);
|
265
|
+
}
|
266
|
+
});
|
267
|
+
});
|
253
268
|
|
254
269
|
// Create post button
|
255
270
|
mainContent
|
@@ -337,7 +352,7 @@ const renderPostEditor = () => {
|
|
337
352
|
|
338
353
|
// Get selected platforms
|
339
354
|
const selectedPlatforms = post.platforms
|
340
|
-
? post.platforms.split(",").map((p) => p.trim())
|
355
|
+
? post.platforms.split(",").map((p) => p.trim().toLowerCase())
|
341
356
|
: [];
|
342
357
|
|
343
358
|
mainContent.innerHTML = `
|
@@ -395,7 +410,7 @@ const renderPostEditor = () => {
|
|
395
410
|
type="date"
|
396
411
|
id="post-date"
|
397
412
|
class="form-control"
|
398
|
-
value="${post.publish_date || ""}"
|
413
|
+
value="${extractDatePart(post.publish_date) || ""}"
|
399
414
|
placeholder="YYYY-MM-DD"
|
400
415
|
>
|
401
416
|
${
|
@@ -408,11 +423,31 @@ const renderPostEditor = () => {
|
|
408
423
|
</div>
|
409
424
|
</div>
|
410
425
|
|
426
|
+
<div class="form-group">
|
427
|
+
<label class="form-label" for="post-time">Publish Time</label>
|
428
|
+
<div class="d-flex gap-sm">
|
429
|
+
<input
|
430
|
+
type="time"
|
431
|
+
id="post-time"
|
432
|
+
class="form-control"
|
433
|
+
value="${extractTimePart(post.publish_date) || "12:00"}"
|
434
|
+
placeholder="HH:MM"
|
435
|
+
>
|
436
|
+
${
|
437
|
+
state.config.aiEnabled
|
438
|
+
? `
|
439
|
+
<button type="button" class="btn" id="suggest-time-btn">Suggest with AI</button>
|
440
|
+
`
|
441
|
+
: ""
|
442
|
+
}
|
443
|
+
</div>
|
444
|
+
</div>
|
445
|
+
|
411
446
|
<div class="form-group">
|
412
447
|
<label class="form-label">Platforms</label>
|
413
448
|
<div class="d-flex gap-md flex-wrap">
|
414
|
-
${platformOptions
|
415
|
-
.map(
|
449
|
+
${platformOptions && platformOptions.length > 0 ?
|
450
|
+
platformOptions.map(
|
416
451
|
(platform) => `
|
417
452
|
<label class="d-flex align-center gap-sm">
|
418
453
|
<input
|
@@ -424,8 +459,17 @@ const renderPostEditor = () => {
|
|
424
459
|
${platform.name}
|
425
460
|
</label>
|
426
461
|
`
|
427
|
-
)
|
428
|
-
|
462
|
+
).join("") :
|
463
|
+
`<label class="d-flex align-center gap-sm">
|
464
|
+
<input
|
465
|
+
type="checkbox"
|
466
|
+
name="platforms"
|
467
|
+
value="bluesky"
|
468
|
+
${selectedPlatforms.includes("bluesky") || selectedPlatforms.includes("Bluesky") ? "checked" : ""}
|
469
|
+
>
|
470
|
+
Bluesky
|
471
|
+
</label>`
|
472
|
+
}
|
429
473
|
</div>
|
430
474
|
</div>
|
431
475
|
|
@@ -457,13 +501,21 @@ const renderPostEditor = () => {
|
|
457
501
|
// Get form values
|
458
502
|
const title = document.getElementById("post-title").value;
|
459
503
|
const content = document.getElementById("post-content").value;
|
460
|
-
const
|
504
|
+
const date = document.getElementById("post-date").value;
|
505
|
+
const time = document.getElementById("post-time").value || "12:00";
|
506
|
+
const publishDate = date ? `${date} ${time}` : ""; // Combine date and time
|
461
507
|
const platformElements = document.querySelectorAll(
|
462
508
|
'input[name="platforms"]:checked'
|
463
509
|
);
|
464
|
-
|
510
|
+
// Make sure we have at least one platform (Bluesky is default)
|
511
|
+
let platforms = Array.from(platformElements)
|
465
512
|
.map((el) => el.value)
|
466
513
|
.join(",");
|
514
|
+
|
515
|
+
// If no platforms selected, default to Bluesky
|
516
|
+
if (!platforms) {
|
517
|
+
platforms = "bluesky";
|
518
|
+
}
|
467
519
|
|
468
520
|
// Validate form
|
469
521
|
if (!content) {
|
@@ -709,6 +761,10 @@ const renderPostEditor = () => {
|
|
709
761
|
|
710
762
|
const result = await response.json();
|
711
763
|
document.getElementById("post-date").value = result.date;
|
764
|
+
// Also update the time field if available
|
765
|
+
if (result.time) {
|
766
|
+
document.getElementById("post-time").value = result.time;
|
767
|
+
}
|
712
768
|
} catch (error) {
|
713
769
|
console.error("Error suggesting date:", error);
|
714
770
|
alert(`Error: ${error.message}`);
|
@@ -718,6 +774,35 @@ const renderPostEditor = () => {
|
|
718
774
|
}
|
719
775
|
});
|
720
776
|
}
|
777
|
+
|
778
|
+
// Suggest time with AI (using same endpoint as date, but only updating time)
|
779
|
+
const suggestTimeBtn = document.getElementById("suggest-time-btn");
|
780
|
+
if (suggestTimeBtn) {
|
781
|
+
suggestTimeBtn.addEventListener("click", async () => {
|
782
|
+
try {
|
783
|
+
suggestTimeBtn.disabled = true;
|
784
|
+
suggestTimeBtn.textContent = "Generating...";
|
785
|
+
|
786
|
+
const response = await fetch("/api/ai/date");
|
787
|
+
|
788
|
+
if (!response.ok) {
|
789
|
+
const error = await response.json();
|
790
|
+
throw new Error(error.error || "Failed to suggest time");
|
791
|
+
}
|
792
|
+
|
793
|
+
const result = await response.json();
|
794
|
+
if (result.time) {
|
795
|
+
document.getElementById("post-time").value = result.time;
|
796
|
+
}
|
797
|
+
} catch (error) {
|
798
|
+
console.error("Error suggesting time:", error);
|
799
|
+
alert(`Error: ${error.message}`);
|
800
|
+
} finally {
|
801
|
+
suggestTimeBtn.disabled = false;
|
802
|
+
suggestTimeBtn.textContent = "Suggest with AI";
|
803
|
+
}
|
804
|
+
});
|
805
|
+
}
|
721
806
|
};
|
722
807
|
|
723
808
|
// Render calendar view
|
@@ -870,8 +955,16 @@ const publishPost = async (postId) => {
|
|
870
955
|
});
|
871
956
|
|
872
957
|
if (!response.ok) {
|
873
|
-
const
|
874
|
-
|
958
|
+
const errorData = await response.json();
|
959
|
+
|
960
|
+
// Special handling for scheduled posts
|
961
|
+
if (errorData.error === "Post is scheduled for future publication" && errorData.scheduledTime) {
|
962
|
+
const scheduledDate = new Date(errorData.scheduledTime);
|
963
|
+
const formattedDate = formatDate(scheduledDate);
|
964
|
+
throw new Error(`This post is scheduled for ${formattedDate}. It cannot be published before that time.`);
|
965
|
+
}
|
966
|
+
|
967
|
+
throw new Error(errorData.error || "Failed to publish post");
|
875
968
|
}
|
876
969
|
|
877
970
|
const result = await response.json();
|
@@ -899,32 +992,138 @@ const publishPost = async (postId) => {
|
|
899
992
|
}
|
900
993
|
};
|
901
994
|
|
995
|
+
// Delete a post
|
996
|
+
const deletePost = async (postId) => {
|
997
|
+
try {
|
998
|
+
const button = document.querySelector(
|
999
|
+
`[data-action="delete-post"][data-post-id="${postId}"]`
|
1000
|
+
);
|
1001
|
+
|
1002
|
+
if (button) {
|
1003
|
+
button.disabled = true;
|
1004
|
+
button.textContent = "Deleting...";
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
// Create a distinct request path to avoid conflicts
|
1008
|
+
const response = await fetch(`/api/posts/${postId}/delete`, {
|
1009
|
+
method: "POST",
|
1010
|
+
headers: {
|
1011
|
+
"Content-Type": "application/json"
|
1012
|
+
}
|
1013
|
+
});
|
1014
|
+
|
1015
|
+
if (!response.ok) {
|
1016
|
+
const error = await response.json();
|
1017
|
+
throw new Error(error.error || "Failed to delete post");
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
// Refresh posts
|
1021
|
+
await fetchPosts();
|
1022
|
+
|
1023
|
+
// Update view
|
1024
|
+
renderApp();
|
1025
|
+
|
1026
|
+
// Show success message
|
1027
|
+
alert("Post deleted successfully!");
|
1028
|
+
} catch (error) {
|
1029
|
+
console.error("Error deleting post:", error);
|
1030
|
+
alert(`Error: ${error.message}`);
|
1031
|
+
|
1032
|
+
// Re-enable button
|
1033
|
+
const button = document.querySelector(
|
1034
|
+
`[data-action="delete-post"][data-post-id="${postId}"]`
|
1035
|
+
);
|
1036
|
+
if (button) {
|
1037
|
+
button.disabled = false;
|
1038
|
+
button.textContent = "Delete";
|
1039
|
+
}
|
1040
|
+
}
|
1041
|
+
};
|
1042
|
+
|
1043
|
+
// Helper function to extract date part from datetime string
|
1044
|
+
const extractDatePart = (dateTimeStr) => {
|
1045
|
+
if (!dateTimeStr) return "";
|
1046
|
+
|
1047
|
+
// Handle different formats
|
1048
|
+
if (dateTimeStr.includes('T')) {
|
1049
|
+
return dateTimeStr.split('T')[0];
|
1050
|
+
} else if (dateTimeStr.includes(' ')) {
|
1051
|
+
return dateTimeStr.split(' ')[0];
|
1052
|
+
}
|
1053
|
+
|
1054
|
+
// If only date is present
|
1055
|
+
return dateTimeStr;
|
1056
|
+
};
|
1057
|
+
|
1058
|
+
// Helper function to extract time part from datetime string
|
1059
|
+
const extractTimePart = (dateTimeStr) => {
|
1060
|
+
if (!dateTimeStr) return "12:00";
|
1061
|
+
|
1062
|
+
// Handle different formats
|
1063
|
+
if (dateTimeStr.includes('T')) {
|
1064
|
+
const timePart = dateTimeStr.split('T')[1];
|
1065
|
+
return timePart ? timePart.substr(0, 5) : "12:00"; // Get HH:MM part
|
1066
|
+
} else if (dateTimeStr.includes(' ')) {
|
1067
|
+
return dateTimeStr.split(' ')[1] || "12:00";
|
1068
|
+
}
|
1069
|
+
|
1070
|
+
// If only date is present, return default time
|
1071
|
+
return "12:00";
|
1072
|
+
};
|
1073
|
+
|
902
1074
|
// Format date for display
|
903
|
-
const formatDate = (
|
904
|
-
if (!
|
1075
|
+
const formatDate = (dateTimeStr) => {
|
1076
|
+
if (!dateTimeStr) return "No date set";
|
905
1077
|
|
906
|
-
const date = new Date(
|
1078
|
+
const date = new Date(dateTimeStr);
|
907
1079
|
const now = new Date();
|
908
1080
|
const tomorrow = new Date(now);
|
909
1081
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
910
1082
|
|
911
1083
|
// Check if date is today or tomorrow
|
912
1084
|
if (date.toDateString() === now.toDateString()) {
|
913
|
-
return
|
1085
|
+
return `Today at ${formatTime(date)}`;
|
914
1086
|
} else if (date.toDateString() === tomorrow.toDateString()) {
|
915
|
-
return
|
1087
|
+
return `Tomorrow at ${formatTime(date)}`;
|
916
1088
|
}
|
917
1089
|
|
918
|
-
// Format as Month Day, Year
|
1090
|
+
// Format as Month Day, Year at Time
|
919
1091
|
const options = { month: "short", day: "numeric", year: "numeric" };
|
920
|
-
return date.toLocaleDateString(undefined, options)
|
1092
|
+
return `${date.toLocaleDateString(undefined, options)} at ${formatTime(date)}`;
|
1093
|
+
};
|
1094
|
+
|
1095
|
+
// Format time for display
|
1096
|
+
const formatTime = (dateTimeStr) => {
|
1097
|
+
if (!dateTimeStr) return "";
|
1098
|
+
|
1099
|
+
// Handle date object or string
|
1100
|
+
let date;
|
1101
|
+
if (dateTimeStr instanceof Date) {
|
1102
|
+
date = dateTimeStr;
|
1103
|
+
} else {
|
1104
|
+
date = new Date(dateTimeStr);
|
1105
|
+
}
|
1106
|
+
|
1107
|
+
// Use the Date object's built-in time formatting methods
|
1108
|
+
// This handles timezone correctly
|
1109
|
+
const hours = date.getHours();
|
1110
|
+
const minutes = date.getMinutes().toString().padStart(2, '0');
|
1111
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
1112
|
+
const formattedHour = hours % 12 || 12; // Convert 0 to 12 for 12 AM
|
1113
|
+
|
1114
|
+
return `${formattedHour}:${minutes} ${ampm}`;
|
921
1115
|
};
|
922
1116
|
|
923
1117
|
// Format platforms for display
|
924
1118
|
const formatPlatforms = (platforms) => {
|
925
1119
|
if (!platforms) return "";
|
926
1120
|
|
927
|
-
|
1121
|
+
// Handle empty string but not null/undefined
|
1122
|
+
if (platforms === "") return "";
|
1123
|
+
|
1124
|
+
// Make sure we have a string and filter out empty values
|
1125
|
+
const platformsStr = String(platforms);
|
1126
|
+
const platformsList = platformsStr.split(",").map((p) => p.trim()).filter(p => p);
|
928
1127
|
|
929
1128
|
return platformsList
|
930
1129
|
.map((platform) => {
|
@@ -125,6 +125,18 @@ p {
|
|
125
125
|
color: white;
|
126
126
|
}
|
127
127
|
|
128
|
+
.btn-sm.btn-danger {
|
129
|
+
font-size: 12px;
|
130
|
+
padding: 4px 8px;
|
131
|
+
border-color: var(--color-accent-danger);
|
132
|
+
color: var(--color-accent-danger);
|
133
|
+
}
|
134
|
+
|
135
|
+
.btn-sm.btn-danger:hover {
|
136
|
+
background-color: var(--color-accent-danger);
|
137
|
+
color: white;
|
138
|
+
}
|
139
|
+
|
128
140
|
.btn-sm {
|
129
141
|
padding: 4px 8px;
|
130
142
|
font-size: 12px;
|
@@ -304,6 +316,8 @@ textarea.form-control {
|
|
304
316
|
display: grid;
|
305
317
|
grid-template-columns: repeat(7, 1fr);
|
306
318
|
gap: 8px;
|
319
|
+
width: 100%;
|
320
|
+
overflow-x: hidden;
|
307
321
|
}
|
308
322
|
|
309
323
|
.calendar-day {
|
@@ -312,17 +326,25 @@ textarea.form-control {
|
|
312
326
|
border-radius: var(--border-radius);
|
313
327
|
border: 1px solid var(--color-border);
|
314
328
|
background-color: var(--color-bg-card);
|
329
|
+
min-width: 0; /* Prevent overflow */
|
330
|
+
width: 100%;
|
331
|
+
max-width: 100%;
|
315
332
|
}
|
316
333
|
|
317
334
|
.calendar-day-header {
|
318
335
|
text-align: center;
|
319
336
|
font-weight: 600;
|
320
337
|
margin-bottom: 8px;
|
338
|
+
min-width: 0; /* Prevent overflow */
|
339
|
+
width: 100%;
|
340
|
+
max-width: 100%;
|
321
341
|
}
|
322
342
|
|
323
343
|
.calendar-day-content {
|
324
344
|
font-size: 12px;
|
325
345
|
overflow: hidden;
|
346
|
+
min-width: 0; /* Prevent overflow */
|
347
|
+
width: 100%;
|
326
348
|
}
|
327
349
|
|
328
350
|
.calendar-day-post {
|
@@ -333,6 +355,7 @@ textarea.form-control {
|
|
333
355
|
white-space: nowrap;
|
334
356
|
overflow: hidden;
|
335
357
|
text-overflow: ellipsis;
|
358
|
+
max-width: 100%;
|
336
359
|
}
|
337
360
|
|
338
361
|
/* Loading */
|
package/src/server/index.mjs
CHANGED
@@ -11,6 +11,7 @@ import {
|
|
11
11
|
updatePost,
|
12
12
|
markAsPublished,
|
13
13
|
logAction,
|
14
|
+
deletePost,
|
14
15
|
} from "../utils/db.mjs";
|
15
16
|
import { getSocialAPI } from "../utils/social/index.mjs";
|
16
17
|
import {
|
@@ -100,6 +101,7 @@ const setupApiRoutes = (app) => {
|
|
100
101
|
"/api/posts",
|
101
102
|
"/api/posts/:id",
|
102
103
|
"/api/publish/:id",
|
104
|
+
"/api/posts/:id/delete",
|
103
105
|
"/api/ai/title",
|
104
106
|
"/api/ai/date",
|
105
107
|
"/api/ai/enhance",
|
@@ -115,10 +117,11 @@ const setupApiRoutes = (app) => {
|
|
115
117
|
// Remove sensitive information
|
116
118
|
const safeConfig = {
|
117
119
|
...config,
|
118
|
-
//
|
120
|
+
// Always ensure platforms are available
|
119
121
|
platforms: [
|
120
|
-
// { id: 'twitter', name: 'Twitter', icon: 'twitter' },
|
121
122
|
{ id: "bluesky", name: "Bluesky", icon: "cloud" },
|
123
|
+
// Add more platforms here when they become available
|
124
|
+
// { id: 'twitter', name: 'Twitter', icon: 'twitter' },
|
122
125
|
// { id: 'tiktok', name: 'TikTok', icon: 'music' }
|
123
126
|
],
|
124
127
|
};
|
@@ -155,17 +158,23 @@ const setupApiRoutes = (app) => {
|
|
155
158
|
// Create new post
|
156
159
|
app.post("/api/posts", async (req, res) => {
|
157
160
|
try {
|
158
|
-
const { title, content, platforms, publish_date } = req.body;
|
161
|
+
const { title, content, platforms, publish_date, publish_time } = req.body;
|
159
162
|
|
160
163
|
if (!content) {
|
161
164
|
return res.status(400).json({ error: "Content is required" });
|
162
165
|
}
|
163
166
|
|
167
|
+
// Combine date and time if both are provided
|
168
|
+
let dateTimeValue = publish_date;
|
169
|
+
if (publish_date && publish_time) {
|
170
|
+
dateTimeValue = `${publish_date} ${publish_time}`;
|
171
|
+
}
|
172
|
+
|
164
173
|
const postId = createPost({
|
165
174
|
title,
|
166
175
|
content,
|
167
176
|
platforms: Array.isArray(platforms) ? platforms.join(",") : platforms,
|
168
|
-
publish_date,
|
177
|
+
publish_date: dateTimeValue,
|
169
178
|
});
|
170
179
|
|
171
180
|
logAction("post_created", { postId, source: "web" });
|
@@ -180,7 +189,7 @@ const setupApiRoutes = (app) => {
|
|
180
189
|
app.put("/api/posts/:id", async (req, res) => {
|
181
190
|
try {
|
182
191
|
const id = parseInt(req.params.id, 10);
|
183
|
-
const { title, content, platforms, publish_date } = req.body;
|
192
|
+
const { title, content, platforms, publish_date, publish_time } = req.body;
|
184
193
|
|
185
194
|
const post = getPostById(id);
|
186
195
|
|
@@ -188,11 +197,17 @@ const setupApiRoutes = (app) => {
|
|
188
197
|
return res.status(404).json({ error: "Post not found" });
|
189
198
|
}
|
190
199
|
|
200
|
+
// Combine date and time if both are provided
|
201
|
+
let dateTimeValue = publish_date;
|
202
|
+
if (publish_date && publish_time) {
|
203
|
+
dateTimeValue = `${publish_date} ${publish_time}`;
|
204
|
+
}
|
205
|
+
|
191
206
|
const success = updatePost(id, {
|
192
207
|
title,
|
193
208
|
content,
|
194
209
|
platforms: Array.isArray(platforms) ? platforms.join(",") : platforms,
|
195
|
-
publish_date,
|
210
|
+
publish_date: dateTimeValue,
|
196
211
|
});
|
197
212
|
|
198
213
|
if (!success) {
|
@@ -207,6 +222,28 @@ const setupApiRoutes = (app) => {
|
|
207
222
|
}
|
208
223
|
});
|
209
224
|
|
225
|
+
// Delete post - using a different route pattern to avoid conflicts
|
226
|
+
app.post("/api/posts/:id/delete", async (req, res) => {
|
227
|
+
try {
|
228
|
+
const id = parseInt(req.params.id, 10);
|
229
|
+
const post = getPostById(id);
|
230
|
+
|
231
|
+
if (!post) {
|
232
|
+
return res.status(404).json({ error: "Post not found" });
|
233
|
+
}
|
234
|
+
|
235
|
+
const success = deletePost(id);
|
236
|
+
|
237
|
+
if (success) {
|
238
|
+
res.json({ success: true });
|
239
|
+
} else {
|
240
|
+
res.status(500).json({ error: "Failed to delete post" });
|
241
|
+
}
|
242
|
+
} catch (error) {
|
243
|
+
res.status(500).json({ error: error.message });
|
244
|
+
}
|
245
|
+
});
|
246
|
+
|
210
247
|
// Publish post
|
211
248
|
app.post("/api/publish/:id", async (req, res) => {
|
212
249
|
try {
|
@@ -226,6 +263,29 @@ const setupApiRoutes = (app) => {
|
|
226
263
|
.status(400)
|
227
264
|
.json({ error: "No platforms specified for post" });
|
228
265
|
}
|
266
|
+
|
267
|
+
// Check if post is eligible for publishing based on date/time
|
268
|
+
if (post.publish_date) {
|
269
|
+
let publishDateTime;
|
270
|
+
|
271
|
+
// If the date includes time component (contains 'T' or ' ')
|
272
|
+
if (post.publish_date.includes('T') || post.publish_date.includes(' ')) {
|
273
|
+
publishDateTime = new Date(post.publish_date);
|
274
|
+
} else {
|
275
|
+
// If only date is provided, assume start of day
|
276
|
+
publishDateTime = new Date(post.publish_date);
|
277
|
+
publishDateTime.setHours(0, 0, 0, 0);
|
278
|
+
}
|
279
|
+
|
280
|
+
const now = new Date();
|
281
|
+
|
282
|
+
if (publishDateTime > now) {
|
283
|
+
return res.status(400).json({
|
284
|
+
error: "Post is scheduled for future publication",
|
285
|
+
scheduledTime: publishDateTime.toISOString()
|
286
|
+
});
|
287
|
+
}
|
288
|
+
}
|
229
289
|
|
230
290
|
// Initialize social API
|
231
291
|
const socialAPI = getSocialAPI();
|
@@ -283,11 +343,21 @@ const setupApiRoutes = (app) => {
|
|
283
343
|
}
|
284
344
|
});
|
285
345
|
|
286
|
-
// Suggest publish date with AI
|
346
|
+
// Suggest publish date and time with AI
|
287
347
|
app.get("/api/ai/date", async (req, res) => {
|
288
348
|
try {
|
289
|
-
const
|
290
|
-
|
349
|
+
const dateTime = await suggestPublishDate();
|
350
|
+
|
351
|
+
// Parse datetime to separate date and time if needed for client
|
352
|
+
let date, time;
|
353
|
+
if (dateTime.includes(" ")) {
|
354
|
+
[date, time] = dateTime.split(" ");
|
355
|
+
} else {
|
356
|
+
date = dateTime;
|
357
|
+
time = "12:00"; // Default time
|
358
|
+
}
|
359
|
+
|
360
|
+
res.json({ dateTime, date, time });
|
291
361
|
} catch (error) {
|
292
362
|
res.status(500).json({ error: error.message });
|
293
363
|
}
|
package/src/utils/ai.mjs
CHANGED
@@ -78,10 +78,10 @@ export const generateTitle = async (content) => {
|
|
78
78
|
};
|
79
79
|
|
80
80
|
/**
|
81
|
-
* Suggest a publish date for a post using AI
|
82
|
-
* @returns {string} Suggested publish date in YYYY-MM-DD format
|
81
|
+
* Suggest a publish date and time for a post using AI
|
82
|
+
* @returns {string} Suggested publish date and time in YYYY-MM-DD HH:MM format
|
83
83
|
* @example
|
84
|
-
* const
|
84
|
+
* const dateTime = await suggestPublishDate();
|
85
85
|
*/
|
86
86
|
export const suggestPublishDate = async () => {
|
87
87
|
const openai = getOpenAIClient();
|
@@ -94,10 +94,11 @@ export const suggestPublishDate = async () => {
|
|
94
94
|
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='posts';").get();
|
95
95
|
|
96
96
|
if (!tableExists) {
|
97
|
-
// If table doesn't exist, just return tomorrow's date
|
97
|
+
// If table doesn't exist, just return tomorrow's date at noon
|
98
98
|
const tomorrow = new Date();
|
99
99
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
100
|
-
|
100
|
+
tomorrow.setHours(12, 0, 0, 0);
|
101
|
+
return `${tomorrow.toISOString().split('T')[0]} 12:00`;
|
101
102
|
}
|
102
103
|
|
103
104
|
// Get historical posts for analysis if table exists
|
@@ -108,14 +109,15 @@ export const suggestPublishDate = async () => {
|
|
108
109
|
// Fallback to simple date suggestion if AI is disabled or not enough data
|
109
110
|
const tomorrow = new Date(today);
|
110
111
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
111
|
-
|
112
|
+
tomorrow.setHours(12, 0, 0, 0);
|
113
|
+
return `${tomorrow.toISOString().split('T')[0]} 12:00`;
|
112
114
|
}
|
113
115
|
|
114
116
|
// Format post history for the AI
|
115
117
|
const postHistory = posts
|
116
118
|
.filter(post => post.publish_date)
|
117
119
|
.map(post => ({
|
118
|
-
|
120
|
+
dateTime: post.publish_date,
|
119
121
|
platform: post.platforms
|
120
122
|
}));
|
121
123
|
|
@@ -124,33 +126,40 @@ export const suggestPublishDate = async () => {
|
|
124
126
|
messages: [
|
125
127
|
{
|
126
128
|
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
|
129
|
+
content: 'You are a social media scheduling assistant. Based on the user\'s posting history, suggest an optimal date and time for their next post. Consider post frequency, patterns, and optimal timing for engagement. Return the date and time in YYYY-MM-DD HH:MM format using 24-hour time.'
|
128
130
|
},
|
129
131
|
{
|
130
132
|
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?`
|
133
|
+
content: `Here is my posting history: ${JSON.stringify(postHistory)}. Today is ${today.toISOString().split('T')[0]} ${today.getHours()}:${today.getMinutes().toString().padStart(2, '0')}. When should I schedule my next post?`
|
132
134
|
}
|
133
135
|
],
|
134
|
-
max_tokens:
|
136
|
+
max_tokens: 30
|
135
137
|
});
|
136
138
|
|
137
|
-
const
|
139
|
+
const suggestedDateTime = response.choices[0].message.content.trim();
|
138
140
|
|
139
|
-
// Validate date format
|
140
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(
|
141
|
-
return
|
141
|
+
// Validate date and time format
|
142
|
+
if (/^\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}$/.test(suggestedDateTime)) {
|
143
|
+
return suggestedDateTime;
|
142
144
|
}
|
143
145
|
|
144
|
-
// Extract date if the AI included other text
|
145
|
-
const
|
146
|
+
// Extract date and time if the AI included other text
|
147
|
+
const dateTimeMatch = suggestedDateTime.match(/(\d{4}-\d{2}-\d{2}) (\d{1,2}:\d{2})/);
|
148
|
+
if (dateTimeMatch) {
|
149
|
+
return `${dateTimeMatch[1]} ${dateTimeMatch[2]}`;
|
150
|
+
}
|
151
|
+
|
152
|
+
// Try to extract just the date if time format fails
|
153
|
+
const dateMatch = suggestedDateTime.match(/\d{4}-\d{2}-\d{2}/);
|
146
154
|
if (dateMatch) {
|
147
|
-
return dateMatch[0]
|
155
|
+
return `${dateMatch[0]} 12:00`;
|
148
156
|
}
|
149
157
|
|
150
158
|
// Fallback
|
151
159
|
const tomorrow = new Date(today);
|
152
160
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
153
|
-
|
161
|
+
tomorrow.setHours(12, 0, 0, 0);
|
162
|
+
return `${tomorrow.toISOString().split('T')[0]} 12:00`;
|
154
163
|
} catch (error) {
|
155
164
|
console.error('Error suggesting publish date:', error.message);
|
156
165
|
|
@@ -158,7 +167,8 @@ export const suggestPublishDate = async () => {
|
|
158
167
|
const today = new Date();
|
159
168
|
const tomorrow = new Date(today);
|
160
169
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
161
|
-
|
170
|
+
tomorrow.setHours(12, 0, 0, 0);
|
171
|
+
return `${tomorrow.toISOString().split('T')[0]} 12:00`;
|
162
172
|
}
|
163
173
|
};
|
164
174
|
|
package/src/utils/db.mjs
CHANGED
@@ -90,6 +90,7 @@ export const initializeDb = (existingDb = null) => {
|
|
90
90
|
content TEXT NOT NULL,
|
91
91
|
platforms TEXT,
|
92
92
|
publish_date TEXT,
|
93
|
+
publish_time TEXT,
|
93
94
|
published INTEGER DEFAULT 0,
|
94
95
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
95
96
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
@@ -316,3 +317,39 @@ export const deletePosts = (options = { published: true }) => {
|
|
316
317
|
throw error;
|
317
318
|
}
|
318
319
|
};
|
320
|
+
|
321
|
+
/**
|
322
|
+
* Delete a single post by ID
|
323
|
+
* @param {number} id - Post ID
|
324
|
+
* @returns {boolean} True if successful
|
325
|
+
* @example
|
326
|
+
* const success = deletePost(1);
|
327
|
+
*/
|
328
|
+
export const deletePost = (id) => {
|
329
|
+
const db = getDb();
|
330
|
+
|
331
|
+
try {
|
332
|
+
// Get post to determine if it was published (for logging)
|
333
|
+
const post = getPostById(id);
|
334
|
+
if (!post) return false;
|
335
|
+
|
336
|
+
// Delete the post
|
337
|
+
const result = db.prepare('DELETE FROM posts WHERE id = ?').run(id);
|
338
|
+
|
339
|
+
if (result.changes > 0) {
|
340
|
+
// Log the action
|
341
|
+
logAction('post_deleted', {
|
342
|
+
postId: id,
|
343
|
+
wasPublished: post.published === 1,
|
344
|
+
title: post.title
|
345
|
+
});
|
346
|
+
|
347
|
+
return true;
|
348
|
+
}
|
349
|
+
|
350
|
+
return false;
|
351
|
+
} catch (error) {
|
352
|
+
console.error('Error deleting post:', error.message);
|
353
|
+
return false;
|
354
|
+
}
|
355
|
+
};
|