@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/.env.example +39 -0
- package/CLAUDE.md +256 -0
- package/CURRENT_CAPABILITIES.md +493 -0
- package/DATA_ORGANIZATION.md +416 -0
- package/IMPLEMENTATION_SUMMARY.md +287 -0
- package/INSTRUCTIONS.md +316 -0
- package/MASTER_PLAN.md +1096 -0
- package/README.md +280 -0
- package/config/sources.json +296 -0
- package/package.json +37 -0
- package/src/cli.js +197 -0
- package/src/fetchers/api.js +232 -0
- package/src/fetchers/hackernews.js +86 -0
- package/src/fetchers/linkedin.js +400 -0
- package/src/fetchers/linkedin_browser.js +167 -0
- package/src/fetchers/reddit.js +77 -0
- package/src/fetchers/rss.js +50 -0
- package/src/fetchers/twitter.js +194 -0
- package/src/index.js +346 -0
- package/src/query.js +316 -0
- package/src/utils/logger.js +74 -0
- package/src/utils/storage.js +134 -0
- package/src/writing-agents/QUICK-REFERENCE.md +111 -0
- package/src/writing-agents/WRITING-SKILLS-IMPROVEMENTS.md +273 -0
- package/src/writing-agents/utils/prompt-templates-improved.js +665 -0
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
|