social-light 0.1.3 → 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 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-light",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "AI-powered social media scheduling tool",
5
5
  "main": "src/index.mjs",
6
6
  "type": "module",
@@ -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 publishDate = "";
132
+ spinner = ora("Suggesting publish date and time...").start();
133
+ let publishDateTime = "";
134
134
 
135
135
  if (config.aiEnabled) {
136
- publishDate = await suggestPublishDate();
137
- spinner.succeed(`Suggested publish date: ${publishDate}`);
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: publishDate,
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
- publishDate = dateInput;
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: publishDate,
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
- publishDate || "Not scheduled"
328
+ ` ${chalk.gray("•")} ${chalk.bold("Publish Date & Time:")} ${
329
+ publishDateTime || "Not scheduled"
254
330
  }`
255
331
  );
256
332
  console.log(
@@ -125,13 +125,28 @@ export const editPost = async (argv) => {
125
125
  content = result.content;
126
126
  }
127
127
 
128
- // Get publish date and platforms
129
- const { publishDate, platforms } = await inquirer.prompt([
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: post.publish_date || "",
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: publishDate,
239
+ publish_date: fullPublishDate,
168
240
  };
169
241
 
170
242
  const success = updatePost(post.id, updatedPost);
@@ -15,15 +15,21 @@ const isEligibleForPublishing = (post) => {
15
15
  return true;
16
16
  }
17
17
 
18
- // Check if publish date is today or in the past
19
- const publishDate = new Date(post.publish_date);
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
- // Reset time to compare dates only
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
  /**
@@ -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
- .join("")}
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 publishDate = document.getElementById("post-date").value;
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
- const platforms = Array.from(platformElements)
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 error = await response.json();
874
- throw new Error(error.error || "Failed to publish post");
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 = (dateStr) => {
904
- if (!dateStr) return "No date set";
1075
+ const formatDate = (dateTimeStr) => {
1076
+ if (!dateTimeStr) return "No date set";
905
1077
 
906
- const date = new Date(dateStr);
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 "Today";
1085
+ return `Today at ${formatTime(date)}`;
914
1086
  } else if (date.toDateString() === tomorrow.toDateString()) {
915
- return "Tomorrow";
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
- const platformsList = platforms.split(",").map((p) => p.trim());
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 */
@@ -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
- // Add default platforms for client
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 date = await suggestPublishDate();
290
- res.json({ date });
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 date = await suggestPublishDate();
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
- return tomorrow.toISOString().split('T')[0];
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
- return tomorrow.toISOString().split('T')[0];
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
- date: post.publish_date,
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 only the date in YYYY-MM-DD format.'
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: 20
136
+ max_tokens: 30
135
137
  });
136
138
 
137
- const suggestedDate = response.choices[0].message.content.trim();
139
+ const suggestedDateTime = response.choices[0].message.content.trim();
138
140
 
139
- // Validate date format
140
- if (/^\d{4}-\d{2}-\d{2}$/.test(suggestedDate)) {
141
- return suggestedDate;
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 dateMatch = suggestedDate.match(/\d{4}-\d{2}-\d{2}/);
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
- return tomorrow.toISOString().split('T')[0];
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
- return tomorrow.toISOString().split('T')[0];
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
+ };