@aikeytake/social-automation 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/query.js ADDED
@@ -0,0 +1,316 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ class DataQuery {
8
+ constructor() {
9
+ this.dataDir = path.join(__dirname, '../data');
10
+ }
11
+
12
+ getTodayFolder() {
13
+ const today = new Date().toISOString().split('T')[0];
14
+ return path.join(this.dataDir, today);
15
+ }
16
+
17
+ getDateFolder(dateStr) {
18
+ return path.join(this.dataDir, dateStr);
19
+ }
20
+
21
+ loadTrending(dateStr = null) {
22
+ const folder = dateStr ? this.getDateFolder(dateStr) : this.getTodayFolder();
23
+ const filePath = path.join(folder, 'trending.json');
24
+
25
+ if (!fs.existsSync(filePath)) {
26
+ console.error(`❌ No trending data found for ${dateStr || 'today'}`);
27
+ return null;
28
+ }
29
+
30
+ const content = fs.readFileSync(filePath, 'utf-8');
31
+ return JSON.parse(content);
32
+ }
33
+
34
+ loadAll(dateStr = null) {
35
+ const folder = dateStr ? this.getDateFolder(dateStr) : this.getTodayFolder();
36
+ const filePath = path.join(folder, 'all.json');
37
+
38
+ if (!fs.existsSync(filePath)) {
39
+ console.error(`❌ No data found for ${dateStr || 'today'}`);
40
+ return null;
41
+ }
42
+
43
+ const content = fs.readFileSync(filePath, 'utf-8');
44
+ return JSON.parse(content);
45
+ }
46
+
47
+ // Query: Get trending items
48
+ getTrending(limit = 20, dateStr = null) {
49
+ const data = this.loadTrending(dateStr);
50
+ if (!data) return [];
51
+
52
+ return data.items.slice(0, limit);
53
+ }
54
+
55
+ // Query: Get by topic
56
+ getByTopic(topic, dateStr = null) {
57
+ const data = this.loadAll(dateStr);
58
+ if (!data) return [];
59
+
60
+ const lowerTopic = topic.toLowerCase();
61
+ return data.items.filter(item => {
62
+ const title = (item.title || '').toLowerCase();
63
+ const summary = (item.summary || item.content || '').toLowerCase();
64
+ const keywords = (item.keywords || item.tags || []).map(k => k.toLowerCase());
65
+
66
+ return title.includes(lowerTopic) ||
67
+ summary.includes(lowerTopic) ||
68
+ keywords.some(k => k.includes(lowerTopic));
69
+ });
70
+ }
71
+
72
+ // Query: Get fresh items (last N hours)
73
+ getFresh(hours = 24, dateStr = null) {
74
+ const data = this.loadAll(dateStr);
75
+ if (!data) return [];
76
+
77
+ const cutoffTime = Date.now() - (hours * 60 * 60 * 1000);
78
+
79
+ return data.items.filter(item => {
80
+ const itemTime = new Date(item.pubDate || item.created_at || item.scraped_at).getTime();
81
+ return itemTime > cutoffTime;
82
+ });
83
+ }
84
+
85
+ // Query: Search
86
+ search(query, dateStr = null) {
87
+ const data = this.loadAll(dateStr);
88
+ if (!data) return [];
89
+
90
+ const lowerQuery = query.toLowerCase();
91
+ return data.items.filter(item => {
92
+ const title = (item.title || '').toLowerCase();
93
+ const content = (item.content || item.summary || '').toLowerCase();
94
+
95
+ return title.includes(lowerQuery) || content.includes(lowerQuery);
96
+ });
97
+ }
98
+
99
+ // Query: Get by source
100
+ getBySource(source, dateStr = null) {
101
+ const folder = dateStr ? this.getDateFolder(dateStr) : this.getTodayFolder();
102
+ const filePath = path.join(folder, `${source}.json`);
103
+
104
+ if (!fs.existsSync(filePath)) {
105
+ console.error(`❌ No data found for source: ${source}`);
106
+ return [];
107
+ }
108
+
109
+ const content = fs.readFileSync(filePath, 'utf-8');
110
+ const data = JSON.parse(content);
111
+ return data.items || [];
112
+ }
113
+
114
+ // Query: Compare two days
115
+ compareDays(date1, date2) {
116
+ const data1 = this.loadTrending(date1);
117
+ const data2 = this.loadTrending(date2);
118
+
119
+ if (!data1 || !data2) {
120
+ console.error('❌ Could not load data for comparison');
121
+ return null;
122
+ }
123
+
124
+ const titles1 = new Set(data1.items.map(i => i.title));
125
+ const titles2 = new Set(data2.items.map(i => i.title));
126
+
127
+ const newToday = [...titles2].filter(t => !titles1.has(t));
128
+ const goneToday = [...titles1].filter(t => !titles2.has(t));
129
+
130
+ return {
131
+ new: newToday,
132
+ gone: goneToday,
133
+ common: [...titles1].filter(t => titles2.has(t))
134
+ };
135
+ }
136
+
137
+ // Display: Show trending items
138
+ displayTrending(items, limit = 10) {
139
+ console.log('\n📊 Trending Items:\n');
140
+ items.slice(0, limit).forEach((item, index) => {
141
+ console.log(`${index + 1}. ${item.title || 'No title'}`);
142
+ console.log(` Score: ${item.score || item.combined_score || 'N/A'}`);
143
+ console.log(` Sources: ${(item.sources || [item.source]).join(', ')}`);
144
+ console.log(` URL: ${item.url || item.link || 'N/A'}`);
145
+ console.log();
146
+ });
147
+ }
148
+
149
+ // Display: Show by topic
150
+ displayByTopic(items, topic) {
151
+ console.log(`\n🔍 Items about "${topic}":\n`);
152
+ items.forEach((item, index) => {
153
+ console.log(`${index + 1}. ${item.title || 'No title'}`);
154
+ console.log(` Source: ${item.source || 'N/A'}`);
155
+ console.log(` URL: ${item.url || item.link || 'N/A'}`);
156
+ console.log();
157
+ });
158
+ }
159
+
160
+ // Display: Show search results
161
+ displaySearchResults(items, query) {
162
+ console.log(`\n🔎 Search results for "${query}":\n`);
163
+ items.forEach((item, index) => {
164
+ console.log(`${index + 1}. ${item.title || 'No title'}`);
165
+ console.log(` Source: ${item.source || 'N/A'}`);
166
+ if (item.summary) {
167
+ console.log(` Summary: ${item.summary.substring(0, 100)}...`);
168
+ }
169
+ console.log();
170
+ });
171
+ }
172
+
173
+ // Display: Show comparison
174
+ displayComparison(comparison, date1, date2) {
175
+ console.log(`\n📅 Comparison: ${date1} vs ${date2}\n`);
176
+
177
+ if (comparison.new.length > 0) {
178
+ console.log('✨ New today:');
179
+ comparison.new.slice(0, 5).forEach(title => {
180
+ console.log(` + ${title}`);
181
+ });
182
+ console.log();
183
+ }
184
+
185
+ if (comparison.gone.length > 0) {
186
+ console.log('📉 Gone today:');
187
+ comparison.gone.slice(0, 5).forEach(title => {
188
+ console.log(` - ${title}`);
189
+ });
190
+ console.log();
191
+ }
192
+
193
+ console.log(`🔄 Common: ${comparison.common.length} items`);
194
+ }
195
+ }
196
+
197
+ // CLI interface
198
+ async function main() {
199
+ const query = new DataQuery();
200
+ const args = process.argv.slice(2);
201
+
202
+ if (args.length === 0) {
203
+ console.log(`
204
+ Usage: npm run query [command] [options]
205
+
206
+ Commands:
207
+ trending Show today's trending items
208
+ trending [date] Show trending for specific date (YYYY-MM-DD)
209
+ topic [name] Get items by topic
210
+ fresh [hours] Get items from last N hours (default: 24)
211
+ search [query] Search content
212
+ source [name] Get items by source (reddit, hackernews, rss)
213
+ compare [d1] [d2] Compare two days
214
+
215
+ Options:
216
+ --limit=N Limit results (default: 10)
217
+ --date=YYYY-MM-DD Use specific date
218
+
219
+ Examples:
220
+ npm run query trending
221
+ npm run query trending --limit=5
222
+ npm run query topic GPT
223
+ npm run query fresh 6
224
+ npm run query search "openai"
225
+ npm run query source reddit
226
+ npm run query compare 2025-03-05 2025-03-06
227
+ `);
228
+ return;
229
+ }
230
+
231
+ const command = args[0];
232
+ const limit = parseInt(args.find(a => a.startsWith('--limit='))?.split('=')[1]) || 10;
233
+ const dateArg = args.find(a => a.startsWith('--date='))?.split('=')[1];
234
+
235
+ switch (command) {
236
+ case 'trending': {
237
+ const items = query.getTrending(limit, dateArg);
238
+ query.displayTrending(items, limit);
239
+ break;
240
+ }
241
+
242
+ case 'topic': {
243
+ const topic = args[1];
244
+ if (!topic) {
245
+ console.error('❌ Please provide a topic name');
246
+ return;
247
+ }
248
+ const items = query.getByTopic(topic, dateArg);
249
+ query.displayByTopic(items, topic);
250
+ break;
251
+ }
252
+
253
+ case 'fresh': {
254
+ const hours = parseInt(args[1]) || 24;
255
+ const items = query.getFresh(hours, dateArg);
256
+ console.log(`\n⏰ Items from last ${hours} hours:\n`);
257
+ items.slice(0, limit).forEach((item, index) => {
258
+ console.log(`${index + 1}. ${item.title || 'No title'}`);
259
+ console.log(` Age: ${item.age_hours || 'N/A'} hours`);
260
+ console.log(` Source: ${item.source || 'N/A'}`);
261
+ console.log();
262
+ });
263
+ break;
264
+ }
265
+
266
+ case 'search': {
267
+ const searchQuery = args[1];
268
+ if (!searchQuery) {
269
+ console.error('❌ Please provide a search query');
270
+ return;
271
+ }
272
+ const items = query.search(searchQuery, dateArg);
273
+ query.displaySearchResults(items, searchQuery);
274
+ break;
275
+ }
276
+
277
+ case 'source': {
278
+ const source = args[1];
279
+ if (!source) {
280
+ console.error('❌ Please provide a source name (reddit, hackernews, rss)');
281
+ return;
282
+ }
283
+ const items = query.getBySource(source, dateArg);
284
+ console.log(`\n📰 Items from ${source}:\n`);
285
+ items.slice(0, limit).forEach((item, index) => {
286
+ console.log(`${index + 1}. ${item.title || 'No title'}`);
287
+ console.log(` Score: ${item.metadata?.score || item.engagement?.upvotes || 'N/A'}`);
288
+ console.log(` URL: ${item.url || item.link || 'N/A'}`);
289
+ console.log();
290
+ });
291
+ break;
292
+ }
293
+
294
+ case 'compare': {
295
+ const date1 = args[1];
296
+ const date2 = args[2];
297
+ if (!date1 || !date2) {
298
+ console.error('❌ Please provide two dates to compare (YYYY-MM-DD YYYY-MM-DD)');
299
+ return;
300
+ }
301
+ const comparison = query.compareDays(date1, date2);
302
+ if (comparison) {
303
+ query.displayComparison(comparison, date1, date2);
304
+ }
305
+ break;
306
+ }
307
+
308
+ default:
309
+ console.error(`❌ Unknown command: ${command}`);
310
+ console.log('Run "npm run query" to see available commands');
311
+ }
312
+ }
313
+
314
+ main().catch(console.error);
315
+
316
+ export default DataQuery;
@@ -0,0 +1,74 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const LOGS_DIR = path.join(__dirname, '../../logs');
7
+
8
+ class Logger {
9
+ constructor(component) {
10
+ this.component = component;
11
+ this.ensureLogsDir();
12
+ }
13
+
14
+ ensureLogsDir() {
15
+ if (!fs.existsSync(LOGS_DIR)) {
16
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
17
+ }
18
+ }
19
+
20
+ getLogFilePath(type) {
21
+ const date = new Date().toISOString().split('T')[0];
22
+ return path.join(LOGS_DIR, `${date}-${type}.log`);
23
+ }
24
+
25
+ formatMessage(level, message, data = null) {
26
+ const timestamp = new Date().toISOString();
27
+ const baseMsg = `[${timestamp}] [${level}] [${this.component}] ${message}`;
28
+ if (data) {
29
+ return `${baseMsg}\n${JSON.stringify(data, null, 2)}`;
30
+ }
31
+ return baseMsg;
32
+ }
33
+
34
+ writeToFile(type, message) {
35
+ const filePath = this.getLogFilePath(type);
36
+ fs.appendFileSync(filePath, message + '\n');
37
+ }
38
+
39
+ info(message, data) {
40
+ const msg = this.formatMessage('INFO', message, data);
41
+ console.log(msg);
42
+ this.writeToFile('info', msg);
43
+ }
44
+
45
+ error(message, data) {
46
+ const msg = this.formatMessage('ERROR', message, data);
47
+ console.error(msg);
48
+ this.writeToFile('error', msg);
49
+ }
50
+
51
+ warn(message, data) {
52
+ const msg = this.formatMessage('WARN', message, data);
53
+ console.warn(msg);
54
+ this.writeToFile('warn', msg);
55
+ }
56
+
57
+ debug(message, data) {
58
+ const msg = this.formatMessage('DEBUG', message, data);
59
+ if (process.env.DEBUG === 'true') {
60
+ console.debug(msg);
61
+ }
62
+ this.writeToFile('debug', msg);
63
+ }
64
+
65
+ success(message, data) {
66
+ const msg = this.formatMessage('SUCCESS', message, data);
67
+ console.log(`\x1b[32m${msg}\x1b[0m`);
68
+ this.writeToFile('success', msg);
69
+ }
70
+ }
71
+
72
+ export default function createLogger(component) {
73
+ return new Logger(component);
74
+ }
@@ -0,0 +1,134 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const DATA_DIR = path.join(__dirname, '../../data');
7
+
8
+ class Storage {
9
+ constructor() {
10
+ this.ensureDataDirs();
11
+ }
12
+
13
+ ensureDataDirs() {
14
+ const dirs = ['queue', 'drafts', 'published'];
15
+ dirs.forEach(dir => {
16
+ const dirPath = path.join(DATA_DIR, dir);
17
+ if (!fs.existsSync(dirPath)) {
18
+ fs.mkdirSync(dirPath, { recursive: true });
19
+ }
20
+ });
21
+ }
22
+
23
+ getFilePath(type, filename) {
24
+ return path.join(DATA_DIR, type, filename);
25
+ }
26
+
27
+ readAll(type) {
28
+ const dirPath = path.join(DATA_DIR, type);
29
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
30
+ return files.map(file => {
31
+ const content = fs.readFileSync(path.join(dirPath, file), 'utf-8');
32
+ return JSON.parse(content);
33
+ });
34
+ }
35
+
36
+ read(type, id) {
37
+ const filePath = this.getFilePath(type, `${id}.json`);
38
+ if (fs.existsSync(filePath)) {
39
+ const content = fs.readFileSync(filePath, 'utf-8');
40
+ return JSON.parse(content);
41
+ }
42
+ return null;
43
+ }
44
+
45
+ write(type, id, data) {
46
+ const filePath = this.getFilePath(type, `${id}.json`);
47
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
48
+ return true;
49
+ }
50
+
51
+ delete(type, id) {
52
+ const filePath = this.getFilePath(type, `${id}.json`);
53
+ if (fs.existsSync(filePath)) {
54
+ fs.unlinkSync(filePath);
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ exists(type, id) {
61
+ const filePath = this.getFilePath(type, `${id}.json`);
62
+ return fs.existsSync(filePath);
63
+ }
64
+
65
+ moveTo(fromType, toType, id) {
66
+ const data = this.read(fromType, id);
67
+ if (data) {
68
+ this.write(toType, id, data);
69
+ this.delete(fromType, id);
70
+ return true;
71
+ }
72
+ return false;
73
+ }
74
+
75
+ addToQueue(item) {
76
+ const id = this.generateId();
77
+ item.id = id;
78
+ item.queuedAt = new Date().toISOString();
79
+ item.status = 'queued';
80
+ this.write('queue', id, item);
81
+ return id;
82
+ }
83
+
84
+ getQueueItems(status = null) {
85
+ const items = this.readAll('queue');
86
+ if (status) {
87
+ return items.filter(item => item.status === status);
88
+ }
89
+ return items.sort((a, b) => new Date(a.queuedAt) - new Date(b.queuedAt));
90
+ }
91
+
92
+ updateQueueStatus(id, status) {
93
+ const item = this.read('queue', id);
94
+ if (item) {
95
+ item.status = status;
96
+ item.updatedAt = new Date().toISOString();
97
+ this.write('queue', id, item);
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+
103
+ generateId() {
104
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
105
+ }
106
+
107
+ isDuplicate(content) {
108
+ // Check if content already exists in queue or drafts
109
+ const normalizedContent = this.normalizeContent(content);
110
+ const queueItems = this.readAll('queue');
111
+ const draftItems = this.readAll('drafts');
112
+ const publishedItems = this.readAll('published');
113
+
114
+ const allItems = [...queueItems, ...draftItems, ...publishedItems];
115
+
116
+ for (const item of allItems) {
117
+ if (this.normalizeContent(item.originalContent || item.content) === normalizedContent) {
118
+ return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
124
+ normalizeContent(content) {
125
+ return content
126
+ .toLowerCase()
127
+ .replace(/\s+/g, ' ')
128
+ .replace(/[^\w\s]/g, '')
129
+ .trim()
130
+ .substring(0, 200);
131
+ }
132
+ }
133
+
134
+ export default new Storage();
@@ -0,0 +1,111 @@
1
+ # Writing Agents - Quick Reference Guide
2
+
3
+ ## Quick Comparison: Original vs Improved
4
+
5
+ | Aspect | Original | Improved |
6
+ |--------|----------|----------|
7
+ | **Writer guidance** | Basic requirements | Examples + anti-patterns |
8
+ | **Critic rubrics** | Basic descriptions | 5-level score rubrics |
9
+ | **Evaluation** | Generic questions | Specific questions per criterion |
10
+ | **Common issues** | Not documented | Listed with point deductions |
11
+ | **Tone guidance** | "Be objective" | Good vs bad examples |
12
+
13
+ ## Key Files
14
+
15
+ | File | Purpose |
16
+ |------|---------|
17
+ | `prompt-templates.js` | Original prompts (currently in use) |
18
+ | `prompt-templates-improved.js` | Improved prompts (ready for testing) |
19
+ | `WRITING-SKILLS-IMPROVEMENTS.md` | Full documentation |
20
+ | `QUICK-REFERENCE.md` | This file |
21
+
22
+ ## Quality Criteria At a Glance
23
+
24
+ | Criterion | Weight | Key Question |
25
+ |-----------|--------|--------------|
26
+ | **Accuracy** | 20% | Claims supported by sources? |
27
+ | **Clarity** | 20% | Easy to understand? |
28
+ | **Value** | 25% | Actionable insights? |
29
+ | **Completeness** | 15% | Key points covered? |
30
+ | **Voice** | 10% | Objective tone? |
31
+ | **Citations** | 10% | Sources credited? |
32
+
33
+ ## Score Ranges
34
+
35
+ | Score | Meaning | Action |
36
+ |-------|---------|--------|
37
+ | 9-10 | Excellent | Ready |
38
+ | 7-8 | Good | Minor tweaks |
39
+ | 5-6 | Fair | Needs revision |
40
+ | 3-4 | Poor | Major revision |
41
+ | 1-2 | Fail | Start over |
42
+
43
+ ## Thresholds
44
+
45
+ - **Quality threshold:** 8.0/10 (satisfactory)
46
+ - **Max iterations:** 3
47
+ - **Target iterations:** 1-2
48
+
49
+ ## Common Issues Quick Reference
50
+
51
+ | Issue | Impact | Criterion |
52
+ |-------|--------|-----------|
53
+ | Claim not in sources | -2 | Accuracy |
54
+ - Promotional language | -3 | Voice |
55
+ - Only summarizes | -3 | Value |
56
+ - Unexplained jargon | -1 | Clarity |
57
+ - Missing citations | -2 | Citations |
58
+ - Prescriptive (you must) | -2 | Voice |
59
+
60
+ ## Testing Checklist
61
+
62
+ ```
63
+ □ Generate newsletter with improved prompts
64
+ □ Compare quality vs baseline
65
+ □ Check critic scoring consistency
66
+ □ Verify no promotional language
67
+ □ Confirm all claims cited
68
+ □ Validate recommendation actionability
69
+ □ Measure iterations to threshold
70
+ ```
71
+
72
+ ## Migration Steps
73
+
74
+ 1. Test improved prompts in parallel
75
+ 2. Compare 5-10 newsletters side by side
76
+ 3. Validate quality improvement
77
+ 4. Update environment if needed
78
+ 5. Deploy improved prompts
79
+ 6. Monitor metrics for 2 weeks
80
+
81
+ ## Environment Variables (No Changes)
82
+
83
+ ```bash
84
+ WRITING_AGENTS_ENABLED=true
85
+ WRITING_AGENTS_MAX_ITERATIONS=3
86
+ WRITING_AGENTS_QUALITY_THRESHOLD=8
87
+ WRITING_AGENTS_MODEL=claude-3-5-sonnet-20241022
88
+ ```
89
+
90
+ ## CLI Commands (No Changes)
91
+
92
+ ```bash
93
+ # Generate with existing prompts
94
+ npm run newsletter:generate:enhanced 2026-03-13
95
+
96
+ # To use improved prompts: Update import in agents
97
+ # Change from: './prompt-templates.js'
98
+ # Change to: './prompt-templates-improved.js'
99
+ ```
100
+
101
+ ## Contacts
102
+
103
+ For issues or questions about the improved writing skills:
104
+ - Documentation: `WRITING-SKILLS-IMPROVEMENTS.md`
105
+ - Implementation: `agents/writer-agent.js`, `agents/critic-agent.js`
106
+ - Original design: `../../WRITING-AGENTS-IMPLEMENTATION.md`
107
+
108
+ ---
109
+
110
+ **Last Updated:** 2026-03-13
111
+ **Status:** Improved prompts ready for testing