@timmeck/marketing-brain 1.1.3 → 1.3.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.
Files changed (34) hide show
  1. package/dist/db/migrations/009_competitors_scheduling.d.ts +2 -0
  2. package/dist/db/migrations/009_competitors_scheduling.js +63 -0
  3. package/dist/db/migrations/009_competitors_scheduling.js.map +1 -0
  4. package/dist/db/migrations/index.js +2 -0
  5. package/dist/db/migrations/index.js.map +1 -1
  6. package/dist/db/repositories/competitor.repository.d.ts +57 -0
  7. package/dist/db/repositories/competitor.repository.js +88 -0
  8. package/dist/db/repositories/competitor.repository.js.map +1 -0
  9. package/dist/db/repositories/scheduler.repository.d.ts +45 -0
  10. package/dist/db/repositories/scheduler.repository.js +79 -0
  11. package/dist/db/repositories/scheduler.repository.js.map +1 -0
  12. package/dist/ipc/router.d.ts +8 -0
  13. package/dist/ipc/router.js +23 -1
  14. package/dist/ipc/router.js.map +1 -1
  15. package/dist/marketing-core.d.ts +1 -0
  16. package/dist/marketing-core.js +39 -7
  17. package/dist/marketing-core.js.map +1 -1
  18. package/dist/mcp/tools.js +116 -0
  19. package/dist/mcp/tools.js.map +1 -1
  20. package/dist/services/competitor.service.d.ts +54 -0
  21. package/dist/services/competitor.service.js +186 -0
  22. package/dist/services/competitor.service.js.map +1 -0
  23. package/dist/services/content-generator.service.d.ts +52 -0
  24. package/dist/services/content-generator.service.js +164 -0
  25. package/dist/services/content-generator.service.js.map +1 -0
  26. package/dist/services/platform-adapter.service.d.ts +34 -0
  27. package/dist/services/platform-adapter.service.js +220 -0
  28. package/dist/services/platform-adapter.service.js.map +1 -0
  29. package/dist/services/scheduler.service.d.ts +25 -0
  30. package/dist/services/scheduler.service.js +77 -0
  31. package/dist/services/scheduler.service.js.map +1 -0
  32. package/dist/utils/events.d.ts +10 -0
  33. package/dist/utils/events.js.map +1 -1
  34. package/package.json +1 -1
@@ -0,0 +1,164 @@
1
+ import { PatternExtractor } from '../learning/pattern-extractor.js';
2
+ import { getLogger } from '../utils/logger.js';
3
+ export class ContentGeneratorService {
4
+ db;
5
+ ruleRepo;
6
+ templateRepo;
7
+ calendarService;
8
+ logger = getLogger();
9
+ patternExtractor;
10
+ constructor(db, ruleRepo, templateRepo, calendarService) {
11
+ this.db = db;
12
+ this.ruleRepo = ruleRepo;
13
+ this.templateRepo = templateRepo;
14
+ this.calendarService = calendarService;
15
+ this.patternExtractor = new PatternExtractor(db);
16
+ }
17
+ // ---------------------------------------------------------------------------
18
+ // generateDraft
19
+ // ---------------------------------------------------------------------------
20
+ generateDraft(platform, topic) {
21
+ this.logger.info(`ContentGenerator: generating draft for platform="${platform}" topic="${topic ?? ''}"`);
22
+ // 1. Extract learned patterns
23
+ const allPatterns = this.patternExtractor.extractPatterns();
24
+ // 2. Best format for platform (look at format-category patterns, pick highest multiplier)
25
+ const suggestedFormat = this.pickBestFormat(allPatterns, platform);
26
+ // 3. Suggested posting time
27
+ const suggestedTime = this.calendarService.suggestNextPostTime(platform);
28
+ // 4. Content guidelines from active marketing rules
29
+ const activeRules = this.ruleRepo.listActive();
30
+ const contentGuidelines = activeRules.map((r) => `${r.pattern}: ${r.recommendation} (confidence ${(r.confidence * 100).toFixed(0)}%)`);
31
+ // 5. Best template for this platform
32
+ const templateSuggestion = this.pickBestTemplate(platform);
33
+ // 6. Hashtag suggestions from historical engagement data
34
+ const hashtagSuggestions = this.getTopHashtags(platform, 5);
35
+ // 7. Estimate engagement level based on high-confidence pattern alignment
36
+ const estimatedEngagement = this.estimateEngagement(allPatterns, platform);
37
+ // 8. Overall confidence (average of pattern confidences, or 0 if none)
38
+ const confidence = allPatterns.length > 0
39
+ ? allPatterns.reduce((sum, p) => sum + p.confidence, 0) / allPatterns.length
40
+ : 0;
41
+ // 9. Build pattern summaries
42
+ const patterns = allPatterns.map((p) => ({
43
+ pattern: p.pattern,
44
+ category: p.category,
45
+ multiplier: p.multiplier,
46
+ confidence: p.confidence,
47
+ }));
48
+ const draft = {
49
+ platform,
50
+ suggestedFormat,
51
+ suggestedTime: {
52
+ time: suggestedTime.time,
53
+ day: suggestedTime.day,
54
+ hour: suggestedTime.hour,
55
+ reason: suggestedTime.reason,
56
+ confidence: suggestedTime.confidence,
57
+ },
58
+ contentGuidelines,
59
+ templateSuggestion,
60
+ hashtagSuggestions,
61
+ estimatedEngagement,
62
+ patterns,
63
+ confidence,
64
+ };
65
+ this.logger.info(`ContentGenerator: draft ready – format="${suggestedFormat}", ` +
66
+ `engagement="${estimatedEngagement}", patterns=${patterns.length}, ` +
67
+ `confidence=${confidence.toFixed(2)}`);
68
+ return draft;
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // suggestHashtags
72
+ // ---------------------------------------------------------------------------
73
+ suggestHashtags(platform, limit = 10) {
74
+ this.logger.info(`ContentGenerator: suggesting hashtags for platform="${platform}" limit=${limit}`);
75
+ const rows = this.db
76
+ .prepare(`SELECT p.hashtags, AVG(e.likes + e.comments*3 + e.shares*5) as score
77
+ FROM posts p JOIN engagement e ON e.post_id = p.id
78
+ WHERE p.platform = ? AND p.hashtags IS NOT NULL
79
+ GROUP BY p.hashtags
80
+ ORDER BY score DESC LIMIT 50`)
81
+ .all(platform);
82
+ // Parse individual hashtags and aggregate
83
+ const tagMap = new Map();
84
+ for (const row of rows) {
85
+ const tags = this.parseHashtags(row.hashtags);
86
+ for (const tag of tags) {
87
+ const existing = tagMap.get(tag);
88
+ if (existing) {
89
+ existing.totalScore += row.score;
90
+ existing.count += 1;
91
+ }
92
+ else {
93
+ tagMap.set(tag, { totalScore: row.score, count: 1 });
94
+ }
95
+ }
96
+ }
97
+ const suggestions = [];
98
+ for (const [hashtag, data] of tagMap.entries()) {
99
+ suggestions.push({
100
+ hashtag,
101
+ frequency: data.count,
102
+ avgEngagement: data.totalScore / data.count,
103
+ });
104
+ }
105
+ // Sort by average engagement descending
106
+ suggestions.sort((a, b) => b.avgEngagement - a.avgEngagement);
107
+ return suggestions.slice(0, limit);
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Private helpers
111
+ // ---------------------------------------------------------------------------
112
+ pickBestFormat(patterns, _platform) {
113
+ const formatPatterns = patterns
114
+ .filter((p) => p.category === 'format')
115
+ .sort((a, b) => b.multiplier - a.multiplier);
116
+ if (formatPatterns.length === 0)
117
+ return 'text';
118
+ // Extract format name from pattern string (e.g. "video posts outperform…")
119
+ const best = formatPatterns[0];
120
+ const match = best.pattern.match(/^(\w+)\s+posts?\s/i);
121
+ if (match)
122
+ return match[1].toLowerCase();
123
+ return 'text';
124
+ }
125
+ pickBestTemplate(platform) {
126
+ // Prefer platform-specific templates, fall back to all templates
127
+ let templates = this.templateRepo.listByPlatform(platform, 1);
128
+ if (templates.length === 0) {
129
+ templates = this.templateRepo.listAll(1);
130
+ }
131
+ if (templates.length === 0)
132
+ return null;
133
+ const t = templates[0];
134
+ return {
135
+ name: t.name,
136
+ structure: t.structure,
137
+ example: t.example,
138
+ };
139
+ }
140
+ getTopHashtags(platform, limit) {
141
+ const suggestions = this.suggestHashtags(platform, limit);
142
+ return suggestions.map((s) => s.hashtag);
143
+ }
144
+ estimateEngagement(patterns, _platform) {
145
+ // Count patterns with high confidence (>= 0.7) and strong multiplier (>= 1.5)
146
+ const strongPatterns = patterns.filter((p) => p.confidence >= 0.7 && p.multiplier >= 1.5);
147
+ if (strongPatterns.length >= 3)
148
+ return 'high';
149
+ if (strongPatterns.length >= 1)
150
+ return 'medium';
151
+ return 'low';
152
+ }
153
+ parseHashtags(raw) {
154
+ if (!raw)
155
+ return [];
156
+ // Split on commas, spaces, or both; normalise to lowercase
157
+ return raw
158
+ .split(/[\s,]+/)
159
+ .map((tag) => tag.trim().toLowerCase())
160
+ .filter((tag) => tag.length > 0)
161
+ .map((tag) => (tag.startsWith('#') ? tag : `#${tag}`));
162
+ }
163
+ }
164
+ //# sourceMappingURL=content-generator.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-generator.service.js","sourceRoot":"","sources":["../../src/services/content-generator.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ export interface PlatformConfig {
2
+ name: string;
3
+ maxLength: number;
4
+ hashtagStrategy: 'inline' | 'end' | 'first-comment' | 'none';
5
+ maxHashtags: number;
6
+ supportsThreads: boolean;
7
+ supportsImages: boolean;
8
+ supportsVideo: boolean;
9
+ bestFormats: string[];
10
+ ctaStyle: 'link' | 'question' | 'poll' | 'none';
11
+ }
12
+ export interface AdaptedPost {
13
+ platform: string;
14
+ content: string;
15
+ format: string;
16
+ hashtags: string | null;
17
+ truncated: boolean;
18
+ threadParts: string[] | null;
19
+ notes: string[];
20
+ }
21
+ export interface CrossPlatformResult {
22
+ original: {
23
+ platform: string;
24
+ content: string;
25
+ };
26
+ adaptations: AdaptedPost[];
27
+ }
28
+ export declare class PlatformAdapterService {
29
+ private logger;
30
+ adaptForPlatform(content: string, targetPlatform: string, sourceFormat?: string): AdaptedPost;
31
+ adaptCrossPlatform(content: string, sourcePlatform: string, targetPlatforms?: string[]): CrossPlatformResult;
32
+ getPlatformConfig(platform: string): PlatformConfig;
33
+ private splitIntoThread;
34
+ }
@@ -0,0 +1,220 @@
1
+ import { getLogger } from '../utils/logger.js';
2
+ const PLATFORM_CONFIGS = {
3
+ x: {
4
+ name: 'X',
5
+ maxLength: 280,
6
+ hashtagStrategy: 'inline',
7
+ maxHashtags: 3,
8
+ supportsThreads: true,
9
+ supportsImages: true,
10
+ supportsVideo: true,
11
+ bestFormats: ['text', 'image', 'poll'],
12
+ ctaStyle: 'question',
13
+ },
14
+ linkedin: {
15
+ name: 'LinkedIn',
16
+ maxLength: 3000,
17
+ hashtagStrategy: 'end',
18
+ maxHashtags: 5,
19
+ supportsImages: true,
20
+ supportsVideo: true,
21
+ supportsThreads: false,
22
+ bestFormats: ['article', 'carousel', 'text'],
23
+ ctaStyle: 'question',
24
+ },
25
+ reddit: {
26
+ name: 'Reddit',
27
+ maxLength: 40000,
28
+ hashtagStrategy: 'none',
29
+ maxHashtags: 0,
30
+ supportsThreads: false,
31
+ supportsImages: true,
32
+ supportsVideo: true,
33
+ bestFormats: ['text', 'image'],
34
+ ctaStyle: 'link',
35
+ },
36
+ bluesky: {
37
+ name: 'Bluesky',
38
+ maxLength: 300,
39
+ hashtagStrategy: 'inline',
40
+ maxHashtags: 3,
41
+ supportsThreads: true,
42
+ supportsImages: true,
43
+ supportsVideo: false,
44
+ bestFormats: ['text', 'image'],
45
+ ctaStyle: 'question',
46
+ },
47
+ mastodon: {
48
+ name: 'Mastodon',
49
+ maxLength: 500,
50
+ hashtagStrategy: 'end',
51
+ maxHashtags: 5,
52
+ supportsThreads: false,
53
+ supportsImages: true,
54
+ supportsVideo: true,
55
+ bestFormats: ['text', 'image'],
56
+ ctaStyle: 'link',
57
+ },
58
+ threads: {
59
+ name: 'Threads',
60
+ maxLength: 500,
61
+ hashtagStrategy: 'inline',
62
+ maxHashtags: 5,
63
+ supportsThreads: true,
64
+ supportsImages: true,
65
+ supportsVideo: true,
66
+ bestFormats: ['text', 'image', 'carousel'],
67
+ ctaStyle: 'question',
68
+ },
69
+ };
70
+ const GENERIC_CONFIG = {
71
+ name: 'Generic',
72
+ maxLength: 5000,
73
+ hashtagStrategy: 'end',
74
+ maxHashtags: 10,
75
+ supportsThreads: false,
76
+ supportsImages: true,
77
+ supportsVideo: false,
78
+ bestFormats: ['text'],
79
+ ctaStyle: 'link',
80
+ };
81
+ export class PlatformAdapterService {
82
+ logger = getLogger();
83
+ adaptForPlatform(content, targetPlatform, sourceFormat) {
84
+ const config = this.getPlatformConfig(targetPlatform);
85
+ const notes = [];
86
+ let adaptedContent = content;
87
+ let truncated = false;
88
+ let threadParts = null;
89
+ this.logger.debug(`Adapting content for ${config.name} (${targetPlatform})`);
90
+ // --- Adapt hashtags ---
91
+ const hashtagRegex = /#\w+/g;
92
+ const extractedHashtags = adaptedContent.match(hashtagRegex) ?? [];
93
+ let finalHashtags = extractedHashtags;
94
+ if (finalHashtags.length > config.maxHashtags) {
95
+ finalHashtags = finalHashtags.slice(0, config.maxHashtags);
96
+ notes.push(`Reduced hashtags from ${extractedHashtags.length} to ${config.maxHashtags}`);
97
+ }
98
+ switch (config.hashtagStrategy) {
99
+ case 'inline':
100
+ // Leave hashtags where they are, but remove excess ones
101
+ if (extractedHashtags.length > config.maxHashtags) {
102
+ const toRemove = extractedHashtags.slice(config.maxHashtags);
103
+ for (const tag of toRemove) {
104
+ adaptedContent = adaptedContent.replace(tag, '').replace(/ +/g, ' ');
105
+ }
106
+ adaptedContent = adaptedContent.trim();
107
+ }
108
+ break;
109
+ case 'end': {
110
+ // Remove all hashtags from content and place at the end
111
+ for (const tag of extractedHashtags) {
112
+ adaptedContent = adaptedContent.replace(tag, '').replace(/ +/g, ' ');
113
+ }
114
+ adaptedContent = adaptedContent.trim();
115
+ if (finalHashtags.length > 0) {
116
+ adaptedContent = `${adaptedContent}\n\n${finalHashtags.join(' ')}`;
117
+ }
118
+ break;
119
+ }
120
+ case 'first-comment':
121
+ // Remove hashtags from content; they go in a first comment
122
+ for (const tag of extractedHashtags) {
123
+ adaptedContent = adaptedContent.replace(tag, '').replace(/ +/g, ' ');
124
+ }
125
+ adaptedContent = adaptedContent.trim();
126
+ if (finalHashtags.length > 0) {
127
+ notes.push(`Place hashtags in first comment: ${finalHashtags.join(' ')}`);
128
+ }
129
+ break;
130
+ case 'none':
131
+ // Remove all hashtags
132
+ for (const tag of extractedHashtags) {
133
+ adaptedContent = adaptedContent.replace(tag, '').replace(/ +/g, ' ');
134
+ }
135
+ adaptedContent = adaptedContent.trim();
136
+ if (extractedHashtags.length > 0) {
137
+ notes.push('Hashtags removed (not supported on this platform)');
138
+ }
139
+ finalHashtags = [];
140
+ break;
141
+ }
142
+ // --- Adapt content length ---
143
+ if (adaptedContent.length > config.maxLength) {
144
+ if (config.supportsThreads) {
145
+ threadParts = this.splitIntoThread(adaptedContent, config.maxLength);
146
+ adaptedContent = threadParts[0];
147
+ notes.push(`Content split into ${threadParts.length}-part thread`);
148
+ this.logger.debug(`Split content into ${threadParts.length} thread parts for ${config.name}`);
149
+ }
150
+ else {
151
+ adaptedContent = adaptedContent.slice(0, config.maxLength - 3) + '...';
152
+ truncated = true;
153
+ notes.push(`Content truncated from ${content.length} to ${config.maxLength} characters`);
154
+ this.logger.debug(`Truncated content for ${config.name}`);
155
+ }
156
+ }
157
+ // --- Determine best format ---
158
+ let format = config.bestFormats[0];
159
+ if (sourceFormat && config.bestFormats.includes(sourceFormat)) {
160
+ format = sourceFormat;
161
+ }
162
+ // --- Platform-specific notes ---
163
+ if (!PLATFORM_CONFIGS[targetPlatform]) {
164
+ notes.push(`Unknown platform "${targetPlatform}"; used generic config`);
165
+ }
166
+ const hashtagsString = finalHashtags.length > 0 ? finalHashtags.join(' ') : null;
167
+ this.logger.info(`Adapted content for ${config.name}: ${adaptedContent.length} chars, format=${format}, truncated=${truncated}`);
168
+ return {
169
+ platform: targetPlatform,
170
+ content: adaptedContent,
171
+ format,
172
+ hashtags: hashtagsString,
173
+ truncated,
174
+ threadParts,
175
+ notes,
176
+ };
177
+ }
178
+ adaptCrossPlatform(content, sourcePlatform, targetPlatforms) {
179
+ const targets = targetPlatforms ?? Object.keys(PLATFORM_CONFIGS).filter(p => p !== sourcePlatform);
180
+ this.logger.info(`Cross-platform adaptation from ${sourcePlatform} to ${targets.length} platforms`);
181
+ const adaptations = targets.map(platform => this.adaptForPlatform(content, platform));
182
+ return {
183
+ original: { platform: sourcePlatform, content },
184
+ adaptations,
185
+ };
186
+ }
187
+ getPlatformConfig(platform) {
188
+ return PLATFORM_CONFIGS[platform] ?? { ...GENERIC_CONFIG, name: platform };
189
+ }
190
+ splitIntoThread(content, maxLength) {
191
+ const sentences = content.match(/[^.!?]+[.!?]+[\s]*/g) ?? [content];
192
+ const parts = [];
193
+ let currentPart = '';
194
+ for (const sentence of sentences) {
195
+ // Reserve space for the part indicator (e.g., " [1/99]")
196
+ const indicatorSpace = 8;
197
+ const effectiveMax = maxLength - indicatorSpace;
198
+ if (currentPart.length + sentence.length > effectiveMax) {
199
+ if (currentPart.length > 0) {
200
+ parts.push(currentPart.trim());
201
+ currentPart = sentence;
202
+ }
203
+ else {
204
+ // Single sentence exceeds max length; force split
205
+ parts.push(sentence.slice(0, effectiveMax).trim());
206
+ currentPart = sentence.slice(effectiveMax);
207
+ }
208
+ }
209
+ else {
210
+ currentPart += sentence;
211
+ }
212
+ }
213
+ if (currentPart.trim().length > 0) {
214
+ parts.push(currentPart.trim());
215
+ }
216
+ const totalParts = parts.length;
217
+ return parts.map((part, i) => `${part} [${i + 1}/${totalParts}]`);
218
+ }
219
+ }
220
+ //# sourceMappingURL=platform-adapter.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-adapter.service.js","sourceRoot":"","sources":["../../src/services/platform-adapter.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { SchedulerRepository, ScheduledPostRecord } from '../db/repositories/scheduler.repository.js';
2
+ import type { CalendarService } from './calendar.service.js';
3
+ export interface SchedulePostInput {
4
+ platform: string;
5
+ content: string;
6
+ format?: string;
7
+ hashtags?: string;
8
+ scheduled_at?: string;
9
+ webhook_url?: string;
10
+ }
11
+ export declare class SchedulerService {
12
+ private schedulerRepo;
13
+ private calendarService;
14
+ private logger;
15
+ constructor(schedulerRepo: SchedulerRepository, calendarService: CalendarService);
16
+ schedulePost(input: SchedulePostInput): {
17
+ scheduledId: number;
18
+ scheduledAt: string;
19
+ };
20
+ listScheduled(): ScheduledPostRecord[];
21
+ listPending(): ScheduledPostRecord[];
22
+ cancelPost(id: number): void;
23
+ checkDue(): ScheduledPostRecord[];
24
+ reschedule(id: number, newTime: string): void;
25
+ }
@@ -0,0 +1,77 @@
1
+ import { getLogger } from '../utils/logger.js';
2
+ import { getEventBus } from '../utils/events.js';
3
+ export class SchedulerService {
4
+ schedulerRepo;
5
+ calendarService;
6
+ logger = getLogger();
7
+ constructor(schedulerRepo, calendarService) {
8
+ this.schedulerRepo = schedulerRepo;
9
+ this.calendarService = calendarService;
10
+ }
11
+ schedulePost(input) {
12
+ let scheduledAt = input.scheduled_at;
13
+ if (!scheduledAt) {
14
+ const suggestion = this.calendarService.suggestNextPostTime(input.platform);
15
+ scheduledAt = suggestion.time;
16
+ this.logger.info(`No scheduled_at provided; using suggested time: ${scheduledAt} (${suggestion.reason})`);
17
+ }
18
+ const scheduledId = this.schedulerRepo.create({
19
+ platform: input.platform,
20
+ content: input.content,
21
+ format: input.format,
22
+ hashtags: input.hashtags,
23
+ scheduled_at: scheduledAt,
24
+ webhook_url: input.webhook_url,
25
+ });
26
+ this.logger.info(`Post scheduled: id=${scheduledId}, platform=${input.platform}, at=${scheduledAt}`);
27
+ getEventBus().emit('post:scheduled', {
28
+ scheduledId,
29
+ platform: input.platform,
30
+ scheduledAt,
31
+ });
32
+ return { scheduledId, scheduledAt };
33
+ }
34
+ listScheduled() {
35
+ return this.schedulerRepo.getAll();
36
+ }
37
+ listPending() {
38
+ return this.schedulerRepo.getPending();
39
+ }
40
+ cancelPost(id) {
41
+ this.schedulerRepo.cancel(id);
42
+ this.logger.info(`Post cancelled: id=${id}`);
43
+ }
44
+ checkDue() {
45
+ const duePosts = this.schedulerRepo.getDue();
46
+ for (const post of duePosts) {
47
+ getEventBus().emit('post:due', {
48
+ scheduledId: post.id,
49
+ platform: post.platform,
50
+ content: post.content,
51
+ });
52
+ if (post.webhook_url) {
53
+ fetch(post.webhook_url, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({
57
+ scheduledId: post.id,
58
+ platform: post.platform,
59
+ content: post.content,
60
+ format: post.format,
61
+ hashtags: post.hashtags,
62
+ }),
63
+ }).catch((err) => {
64
+ this.logger.warn(`Webhook failed for scheduled post ${post.id}: ${err}`);
65
+ });
66
+ }
67
+ this.schedulerRepo.markPublished(post.id);
68
+ this.logger.info(`Post published: id=${post.id}, platform=${post.platform}`);
69
+ }
70
+ return duePosts;
71
+ }
72
+ reschedule(id, newTime) {
73
+ this.schedulerRepo.update(id, { scheduled_at: newTime });
74
+ this.logger.info(`Post rescheduled: id=${id}, newTime=${newTime}`);
75
+ }
76
+ }
77
+ //# sourceMappingURL=scheduler.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.service.js","sourceRoot":"","sources":["../../src/services/scheduler.service.ts"],"names":[],"mappings":"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"}
@@ -61,6 +61,16 @@ export type MarketingEvents = {
61
61
  sessionId: number;
62
62
  summary: string;
63
63
  };
64
+ 'post:scheduled': {
65
+ scheduledId: number;
66
+ platform: string;
67
+ scheduledAt: string;
68
+ };
69
+ 'post:due': {
70
+ scheduledId: number;
71
+ platform: string;
72
+ content: string;
73
+ };
64
74
  };
65
75
  export type MarketingEventName = keyof MarketingEvents;
66
76
  export declare class TypedEventBus extends GenericEventBus<MarketingEvents> {
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/utils/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAsBvE,MAAM,OAAO,aAAc,SAAQ,eAAgC;CAAG;AAEtE,IAAI,WAAW,GAAyB,IAAI,CAAC;AAE7C,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,WAAW,GAAG,IAAI,aAAa,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/utils/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAwBvE,MAAM,OAAO,aAAc,SAAQ,eAAgC;CAAG;AAEtE,IAAI,WAAW,GAAyB,IAAI,CAAC;AAE7C,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,WAAW,GAAG,IAAI,aAAa,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmeck/marketing-brain",
3
- "version": "1.1.3",
3
+ "version": "1.3.0",
4
4
  "description": "Self-learning marketing intelligence system with Hebbian synapse network",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",