@timmeck/marketing-brain 0.2.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 (294) hide show
  1. package/.mcp.json +9 -0
  2. package/README.md +342 -0
  3. package/dashboard.html +666 -0
  4. package/dist/api/server.d.ts +15 -0
  5. package/dist/api/server.js +73 -0
  6. package/dist/api/server.js.map +1 -0
  7. package/dist/cli/colors.d.ts +43 -0
  8. package/dist/cli/colors.js +54 -0
  9. package/dist/cli/colors.js.map +1 -0
  10. package/dist/cli/commands/campaign.d.ts +2 -0
  11. package/dist/cli/commands/campaign.js +62 -0
  12. package/dist/cli/commands/campaign.js.map +1 -0
  13. package/dist/cli/commands/config.d.ts +2 -0
  14. package/dist/cli/commands/config.js +164 -0
  15. package/dist/cli/commands/config.js.map +1 -0
  16. package/dist/cli/commands/dashboard.d.ts +2 -0
  17. package/dist/cli/commands/dashboard.js +147 -0
  18. package/dist/cli/commands/dashboard.js.map +1 -0
  19. package/dist/cli/commands/doctor.d.ts +2 -0
  20. package/dist/cli/commands/doctor.js +111 -0
  21. package/dist/cli/commands/doctor.js.map +1 -0
  22. package/dist/cli/commands/export.d.ts +2 -0
  23. package/dist/cli/commands/export.js +37 -0
  24. package/dist/cli/commands/export.js.map +1 -0
  25. package/dist/cli/commands/import.d.ts +2 -0
  26. package/dist/cli/commands/import.js +76 -0
  27. package/dist/cli/commands/import.js.map +1 -0
  28. package/dist/cli/commands/insights.d.ts +2 -0
  29. package/dist/cli/commands/insights.js +41 -0
  30. package/dist/cli/commands/insights.js.map +1 -0
  31. package/dist/cli/commands/learn.d.ts +2 -0
  32. package/dist/cli/commands/learn.js +22 -0
  33. package/dist/cli/commands/learn.js.map +1 -0
  34. package/dist/cli/commands/network.d.ts +2 -0
  35. package/dist/cli/commands/network.js +66 -0
  36. package/dist/cli/commands/network.js.map +1 -0
  37. package/dist/cli/commands/post.d.ts +2 -0
  38. package/dist/cli/commands/post.js +45 -0
  39. package/dist/cli/commands/post.js.map +1 -0
  40. package/dist/cli/commands/query.d.ts +2 -0
  41. package/dist/cli/commands/query.js +96 -0
  42. package/dist/cli/commands/query.js.map +1 -0
  43. package/dist/cli/commands/rules.d.ts +2 -0
  44. package/dist/cli/commands/rules.js +25 -0
  45. package/dist/cli/commands/rules.js.map +1 -0
  46. package/dist/cli/commands/start.d.ts +2 -0
  47. package/dist/cli/commands/start.js +91 -0
  48. package/dist/cli/commands/start.js.map +1 -0
  49. package/dist/cli/commands/status.d.ts +2 -0
  50. package/dist/cli/commands/status.js +63 -0
  51. package/dist/cli/commands/status.js.map +1 -0
  52. package/dist/cli/commands/stop.d.ts +2 -0
  53. package/dist/cli/commands/stop.js +34 -0
  54. package/dist/cli/commands/stop.js.map +1 -0
  55. package/dist/cli/commands/suggest.d.ts +2 -0
  56. package/dist/cli/commands/suggest.js +57 -0
  57. package/dist/cli/commands/suggest.js.map +1 -0
  58. package/dist/cli/ipc-helper.d.ts +2 -0
  59. package/dist/cli/ipc-helper.js +26 -0
  60. package/dist/cli/ipc-helper.js.map +1 -0
  61. package/dist/cli/update-check.d.ts +2 -0
  62. package/dist/cli/update-check.js +58 -0
  63. package/dist/cli/update-check.js.map +1 -0
  64. package/dist/config.d.ts +2 -0
  65. package/dist/config.js +111 -0
  66. package/dist/config.js.map +1 -0
  67. package/dist/dashboard/renderer.d.ts +11 -0
  68. package/dist/dashboard/renderer.js +112 -0
  69. package/dist/dashboard/renderer.js.map +1 -0
  70. package/dist/dashboard/server.d.ts +15 -0
  71. package/dist/dashboard/server.js +122 -0
  72. package/dist/dashboard/server.js.map +1 -0
  73. package/dist/db/connection.d.ts +2 -0
  74. package/dist/db/connection.js +19 -0
  75. package/dist/db/connection.js.map +1 -0
  76. package/dist/db/migrations/001_core_schema.d.ts +2 -0
  77. package/dist/db/migrations/001_core_schema.js +62 -0
  78. package/dist/db/migrations/001_core_schema.js.map +1 -0
  79. package/dist/db/migrations/002_learning_schema.d.ts +2 -0
  80. package/dist/db/migrations/002_learning_schema.js +45 -0
  81. package/dist/db/migrations/002_learning_schema.js.map +1 -0
  82. package/dist/db/migrations/003_synapse_schema.d.ts +2 -0
  83. package/dist/db/migrations/003_synapse_schema.js +26 -0
  84. package/dist/db/migrations/003_synapse_schema.js.map +1 -0
  85. package/dist/db/migrations/004_insights_schema.d.ts +2 -0
  86. package/dist/db/migrations/004_insights_schema.js +37 -0
  87. package/dist/db/migrations/004_insights_schema.js.map +1 -0
  88. package/dist/db/migrations/005_fts_indexes.d.ts +2 -0
  89. package/dist/db/migrations/005_fts_indexes.js +76 -0
  90. package/dist/db/migrations/005_fts_indexes.js.map +1 -0
  91. package/dist/db/migrations/index.d.ts +2 -0
  92. package/dist/db/migrations/index.js +47 -0
  93. package/dist/db/migrations/index.js.map +1 -0
  94. package/dist/db/repositories/audience.repository.d.ts +18 -0
  95. package/dist/db/repositories/audience.repository.js +45 -0
  96. package/dist/db/repositories/audience.repository.js.map +1 -0
  97. package/dist/db/repositories/campaign.repository.d.ts +15 -0
  98. package/dist/db/repositories/campaign.repository.js +58 -0
  99. package/dist/db/repositories/campaign.repository.js.map +1 -0
  100. package/dist/db/repositories/engagement.repository.d.ts +26 -0
  101. package/dist/db/repositories/engagement.repository.js +83 -0
  102. package/dist/db/repositories/engagement.repository.js.map +1 -0
  103. package/dist/db/repositories/insight.repository.d.ts +18 -0
  104. package/dist/db/repositories/insight.repository.js +87 -0
  105. package/dist/db/repositories/insight.repository.js.map +1 -0
  106. package/dist/db/repositories/post.repository.d.ts +21 -0
  107. package/dist/db/repositories/post.repository.js +105 -0
  108. package/dist/db/repositories/post.repository.js.map +1 -0
  109. package/dist/db/repositories/rule.repository.d.ts +16 -0
  110. package/dist/db/repositories/rule.repository.js +71 -0
  111. package/dist/db/repositories/rule.repository.js.map +1 -0
  112. package/dist/db/repositories/strategy.repository.d.ts +16 -0
  113. package/dist/db/repositories/strategy.repository.js +69 -0
  114. package/dist/db/repositories/strategy.repository.js.map +1 -0
  115. package/dist/db/repositories/synapse.repository.d.ts +25 -0
  116. package/dist/db/repositories/synapse.repository.js +115 -0
  117. package/dist/db/repositories/synapse.repository.js.map +1 -0
  118. package/dist/db/repositories/template.repository.d.ts +16 -0
  119. package/dist/db/repositories/template.repository.js +61 -0
  120. package/dist/db/repositories/template.repository.js.map +1 -0
  121. package/dist/index.d.ts +2 -0
  122. package/dist/index.js +62 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/ipc/client.d.ts +13 -0
  125. package/dist/ipc/client.js +93 -0
  126. package/dist/ipc/client.js.map +1 -0
  127. package/dist/ipc/protocol.d.ts +8 -0
  128. package/dist/ipc/protocol.js +29 -0
  129. package/dist/ipc/protocol.js.map +1 -0
  130. package/dist/ipc/router.d.ts +30 -0
  131. package/dist/ipc/router.js +88 -0
  132. package/dist/ipc/router.js.map +1 -0
  133. package/dist/ipc/server.d.ts +14 -0
  134. package/dist/ipc/server.js +130 -0
  135. package/dist/ipc/server.js.map +1 -0
  136. package/dist/learning/confidence-scorer.d.ts +17 -0
  137. package/dist/learning/confidence-scorer.js +26 -0
  138. package/dist/learning/confidence-scorer.js.map +1 -0
  139. package/dist/learning/learning-engine.d.ts +33 -0
  140. package/dist/learning/learning-engine.js +211 -0
  141. package/dist/learning/learning-engine.js.map +1 -0
  142. package/dist/marketing-core.d.ts +17 -0
  143. package/dist/marketing-core.js +233 -0
  144. package/dist/marketing-core.js.map +1 -0
  145. package/dist/mcp/server.d.ts +1 -0
  146. package/dist/mcp/server.js +67 -0
  147. package/dist/mcp/server.js.map +1 -0
  148. package/dist/mcp/tools.d.ts +3 -0
  149. package/dist/mcp/tools.js +138 -0
  150. package/dist/mcp/tools.js.map +1 -0
  151. package/dist/research/research-engine.d.ts +28 -0
  152. package/dist/research/research-engine.js +211 -0
  153. package/dist/research/research-engine.js.map +1 -0
  154. package/dist/services/analytics.service.d.ts +116 -0
  155. package/dist/services/analytics.service.js +69 -0
  156. package/dist/services/analytics.service.js.map +1 -0
  157. package/dist/services/audience.service.d.ts +20 -0
  158. package/dist/services/audience.service.js +30 -0
  159. package/dist/services/audience.service.js.map +1 -0
  160. package/dist/services/campaign.service.d.ts +27 -0
  161. package/dist/services/campaign.service.js +65 -0
  162. package/dist/services/campaign.service.js.map +1 -0
  163. package/dist/services/insight.service.d.ts +18 -0
  164. package/dist/services/insight.service.js +40 -0
  165. package/dist/services/insight.service.js.map +1 -0
  166. package/dist/services/post.service.d.ts +48 -0
  167. package/dist/services/post.service.js +93 -0
  168. package/dist/services/post.service.js.map +1 -0
  169. package/dist/services/rule.service.d.ts +29 -0
  170. package/dist/services/rule.service.js +67 -0
  171. package/dist/services/rule.service.js.map +1 -0
  172. package/dist/services/strategy.service.d.ts +17 -0
  173. package/dist/services/strategy.service.js +39 -0
  174. package/dist/services/strategy.service.js.map +1 -0
  175. package/dist/services/synapse.service.d.ts +22 -0
  176. package/dist/services/synapse.service.js +22 -0
  177. package/dist/services/synapse.service.js.map +1 -0
  178. package/dist/services/template.service.d.ts +17 -0
  179. package/dist/services/template.service.js +37 -0
  180. package/dist/services/template.service.js.map +1 -0
  181. package/dist/synapses/activation.d.ts +13 -0
  182. package/dist/synapses/activation.js +50 -0
  183. package/dist/synapses/activation.js.map +1 -0
  184. package/dist/synapses/decay.d.ts +11 -0
  185. package/dist/synapses/decay.js +27 -0
  186. package/dist/synapses/decay.js.map +1 -0
  187. package/dist/synapses/hebbian.d.ts +13 -0
  188. package/dist/synapses/hebbian.js +35 -0
  189. package/dist/synapses/hebbian.js.map +1 -0
  190. package/dist/synapses/pathfinder.d.ts +14 -0
  191. package/dist/synapses/pathfinder.js +50 -0
  192. package/dist/synapses/pathfinder.js.map +1 -0
  193. package/dist/synapses/synapse-manager.d.ts +32 -0
  194. package/dist/synapses/synapse-manager.js +76 -0
  195. package/dist/synapses/synapse-manager.js.map +1 -0
  196. package/dist/types/config.types.d.ts +69 -0
  197. package/dist/types/config.types.js +2 -0
  198. package/dist/types/config.types.js.map +1 -0
  199. package/dist/types/ipc.types.d.ts +11 -0
  200. package/dist/types/ipc.types.js +2 -0
  201. package/dist/types/ipc.types.js.map +1 -0
  202. package/dist/types/post.types.d.ts +141 -0
  203. package/dist/types/post.types.js +2 -0
  204. package/dist/types/post.types.js.map +1 -0
  205. package/dist/types/synapse.types.d.ts +23 -0
  206. package/dist/types/synapse.types.js +2 -0
  207. package/dist/types/synapse.types.js.map +1 -0
  208. package/dist/utils/events.d.ts +57 -0
  209. package/dist/utils/events.js +23 -0
  210. package/dist/utils/events.js.map +1 -0
  211. package/dist/utils/hash.d.ts +1 -0
  212. package/dist/utils/hash.js +5 -0
  213. package/dist/utils/hash.js.map +1 -0
  214. package/dist/utils/logger.d.ts +8 -0
  215. package/dist/utils/logger.js +39 -0
  216. package/dist/utils/logger.js.map +1 -0
  217. package/dist/utils/paths.d.ts +3 -0
  218. package/dist/utils/paths.js +18 -0
  219. package/dist/utils/paths.js.map +1 -0
  220. package/package.json +40 -0
  221. package/seed-data.json +78 -0
  222. package/src/api/server.ts +86 -0
  223. package/src/cli/colors.ts +59 -0
  224. package/src/cli/commands/campaign.ts +66 -0
  225. package/src/cli/commands/config.ts +168 -0
  226. package/src/cli/commands/dashboard.ts +165 -0
  227. package/src/cli/commands/doctor.ts +110 -0
  228. package/src/cli/commands/export.ts +40 -0
  229. package/src/cli/commands/import.ts +84 -0
  230. package/src/cli/commands/insights.ts +44 -0
  231. package/src/cli/commands/learn.ts +24 -0
  232. package/src/cli/commands/network.ts +71 -0
  233. package/src/cli/commands/post.ts +47 -0
  234. package/src/cli/commands/query.ts +108 -0
  235. package/src/cli/commands/rules.ts +27 -0
  236. package/src/cli/commands/start.ts +100 -0
  237. package/src/cli/commands/status.ts +73 -0
  238. package/src/cli/commands/stop.ts +33 -0
  239. package/src/cli/commands/suggest.ts +64 -0
  240. package/src/cli/ipc-helper.ts +22 -0
  241. package/src/cli/update-check.ts +63 -0
  242. package/src/config.ts +110 -0
  243. package/src/dashboard/renderer.ts +136 -0
  244. package/src/dashboard/server.ts +140 -0
  245. package/src/db/connection.ts +22 -0
  246. package/src/db/migrations/001_core_schema.ts +63 -0
  247. package/src/db/migrations/002_learning_schema.ts +46 -0
  248. package/src/db/migrations/003_synapse_schema.ts +27 -0
  249. package/src/db/migrations/004_insights_schema.ts +38 -0
  250. package/src/db/migrations/005_fts_indexes.ts +77 -0
  251. package/src/db/migrations/index.ts +62 -0
  252. package/src/db/repositories/audience.repository.ts +53 -0
  253. package/src/db/repositories/campaign.repository.ts +72 -0
  254. package/src/db/repositories/engagement.repository.ts +108 -0
  255. package/src/db/repositories/insight.repository.ts +100 -0
  256. package/src/db/repositories/post.repository.ts +123 -0
  257. package/src/db/repositories/rule.repository.ts +87 -0
  258. package/src/db/repositories/strategy.repository.ts +82 -0
  259. package/src/db/repositories/synapse.repository.ts +148 -0
  260. package/src/db/repositories/template.repository.ts +76 -0
  261. package/src/index.ts +69 -0
  262. package/src/ipc/client.ts +110 -0
  263. package/src/ipc/protocol.ts +35 -0
  264. package/src/ipc/router.ts +126 -0
  265. package/src/ipc/server.ts +140 -0
  266. package/src/learning/confidence-scorer.ts +36 -0
  267. package/src/learning/learning-engine.ts +254 -0
  268. package/src/marketing-core.ts +285 -0
  269. package/src/mcp/server.ts +72 -0
  270. package/src/mcp/tools.ts +216 -0
  271. package/src/research/research-engine.ts +226 -0
  272. package/src/services/analytics.service.ts +73 -0
  273. package/src/services/audience.service.ts +40 -0
  274. package/src/services/campaign.service.ts +80 -0
  275. package/src/services/insight.service.ts +54 -0
  276. package/src/services/post.service.ts +116 -0
  277. package/src/services/rule.service.ts +90 -0
  278. package/src/services/strategy.service.ts +53 -0
  279. package/src/services/synapse.service.ts +32 -0
  280. package/src/services/template.service.ts +50 -0
  281. package/src/synapses/activation.ts +80 -0
  282. package/src/synapses/decay.ts +38 -0
  283. package/src/synapses/hebbian.ts +68 -0
  284. package/src/synapses/pathfinder.ts +81 -0
  285. package/src/synapses/synapse-manager.ts +115 -0
  286. package/src/types/config.types.ts +79 -0
  287. package/src/types/ipc.types.ts +8 -0
  288. package/src/types/post.types.ts +156 -0
  289. package/src/types/synapse.types.ts +43 -0
  290. package/src/utils/events.ts +44 -0
  291. package/src/utils/hash.ts +5 -0
  292. package/src/utils/logger.ts +48 -0
  293. package/src/utils/paths.ts +19 -0
  294. package/tsconfig.json +18 -0
@@ -0,0 +1,226 @@
1
+ import type { ResearchConfig } from '../types/config.types.js';
2
+ import type { PostRepository } from '../db/repositories/post.repository.js';
3
+ import type { EngagementRepository } from '../db/repositories/engagement.repository.js';
4
+ import type { CampaignRepository } from '../db/repositories/campaign.repository.js';
5
+ import type { TemplateRepository } from '../db/repositories/template.repository.js';
6
+ import type { InsightRepository } from '../db/repositories/insight.repository.js';
7
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
8
+ import { engagementScore } from '../learning/confidence-scorer.js';
9
+ import { getLogger } from '../utils/logger.js';
10
+
11
+ export class ResearchEngine {
12
+ private timer: ReturnType<typeof setInterval> | null = null;
13
+ private logger = getLogger();
14
+
15
+ constructor(
16
+ private config: ResearchConfig,
17
+ private postRepo: PostRepository,
18
+ private engagementRepo: EngagementRepository,
19
+ private campaignRepo: CampaignRepository,
20
+ private templateRepo: TemplateRepository,
21
+ private insightRepo: InsightRepository,
22
+ private synapseManager: SynapseManager,
23
+ ) {}
24
+
25
+ start(): void {
26
+ // Initial delay before first run
27
+ setTimeout(() => {
28
+ this.runCycle();
29
+ this.timer = setInterval(() => {
30
+ try {
31
+ this.runCycle();
32
+ } catch (err) {
33
+ this.logger.error('Research cycle error:', err);
34
+ }
35
+ }, this.config.intervalMs);
36
+ }, this.config.initialDelayMs);
37
+ }
38
+
39
+ stop(): void {
40
+ if (this.timer) {
41
+ clearInterval(this.timer);
42
+ this.timer = null;
43
+ }
44
+ }
45
+
46
+ runCycle(): void {
47
+ this.logger.info('Starting research cycle');
48
+
49
+ // Expire old insights
50
+ const expired = this.insightRepo.expireOld();
51
+ if (expired > 0) this.logger.info(`Expired ${expired} old insights`);
52
+
53
+ // Generate new insights
54
+ this.detectTrends();
55
+ this.detectGaps();
56
+ this.detectSynergies();
57
+ this.suggestTemplates();
58
+ this.suggestOptimizations();
59
+
60
+ this.logger.info('Research cycle complete');
61
+ }
62
+
63
+ private detectTrends(): void {
64
+ const cutoff = new Date();
65
+ cutoff.setDate(cutoff.getDate() - this.config.trendWindowDays);
66
+ const recentPosts = this.postRepo.recentPublished(cutoff.toISOString());
67
+
68
+ if (recentPosts.length < this.config.minDataPoints) return;
69
+
70
+ // Detect platform engagement trends
71
+ const platformScores: Record<string, number[]> = {};
72
+ for (const post of recentPosts) {
73
+ const eng = this.engagementRepo.getLatestByPost(post.id);
74
+ if (!eng) continue;
75
+ const score = engagementScore(eng);
76
+ if (!platformScores[post.platform]) platformScores[post.platform] = [];
77
+ platformScores[post.platform].push(score);
78
+ }
79
+
80
+ for (const [platform, scores] of Object.entries(platformScores)) {
81
+ if (scores.length < 3) continue;
82
+ const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
83
+ const recentAvg = scores.slice(0, Math.ceil(scores.length / 2)).reduce((a, b) => a + b, 0) / Math.ceil(scores.length / 2);
84
+ const olderAvg = scores.slice(Math.ceil(scores.length / 2)).reduce((a, b) => a + b, 0) / Math.floor(scores.length / 2);
85
+
86
+ if (olderAvg > 0 && Math.abs(recentAvg - olderAvg) / olderAvg > 0.2) {
87
+ const direction = recentAvg > olderAvg ? 'up' : 'down';
88
+ const pct = Math.round(Math.abs(recentAvg - olderAvg) / olderAvg * 100);
89
+
90
+ this.insightRepo.create({
91
+ type: 'trend',
92
+ title: `${platform} engagement trending ${direction}`,
93
+ description: `Engagement on ${platform} is ${direction} ${pct}% over the last ${this.config.trendWindowDays} days (avg score: ${avg.toFixed(0)})`,
94
+ confidence: Math.min(0.9, scores.length / 20),
95
+ priority: pct > 30 ? 8 : 5,
96
+ expires_at: this.expiresAt(),
97
+ });
98
+ }
99
+ }
100
+ }
101
+
102
+ private detectGaps(): void {
103
+ const platforms = ['x', 'reddit', 'linkedin', 'bluesky'];
104
+ const postsByPlatform = this.postRepo.countByPlatform();
105
+
106
+ for (const platform of platforms) {
107
+ if (!postsByPlatform[platform] || postsByPlatform[platform] === 0) {
108
+ this.insightRepo.create({
109
+ type: 'gap',
110
+ title: `No posts on ${platform}`,
111
+ description: `You haven't posted on ${platform} yet. Consider expanding your reach to this platform.`,
112
+ confidence: 0.8,
113
+ priority: 4,
114
+ expires_at: this.expiresAt(),
115
+ });
116
+ } else if (postsByPlatform[platform] < 3) {
117
+ this.insightRepo.create({
118
+ type: 'gap',
119
+ title: `Low activity on ${platform}`,
120
+ description: `Only ${postsByPlatform[platform]} posts on ${platform}. Increasing frequency could improve visibility.`,
121
+ confidence: 0.6,
122
+ priority: 3,
123
+ expires_at: this.expiresAt(),
124
+ });
125
+ }
126
+ }
127
+ }
128
+
129
+ private detectSynergies(): void {
130
+ const topPosts = this.engagementRepo.topPosts(20);
131
+
132
+ // Look for patterns in top posts
133
+ const platformFormat: Record<string, Record<string, number>> = {};
134
+ for (const post of topPosts) {
135
+ const fullPost = this.postRepo.getById(post.post_id);
136
+ if (!fullPost) continue;
137
+
138
+ const key = fullPost.platform;
139
+ if (!platformFormat[key]) platformFormat[key] = {};
140
+ const format = fullPost.format;
141
+ platformFormat[key][format] = (platformFormat[key][format] ?? 0) + 1;
142
+ }
143
+
144
+ for (const [platform, formats] of Object.entries(platformFormat)) {
145
+ const sorted = Object.entries(formats).sort(([, a], [, b]) => b - a);
146
+ if (sorted[0] && sorted[0][1] >= 3) {
147
+ this.insightRepo.create({
148
+ type: 'synergy',
149
+ title: `${sorted[0][0]} works best on ${platform}`,
150
+ description: `${sorted[0][1]} of your top posts on ${platform} use the ${sorted[0][0]} format. This combination consistently performs well.`,
151
+ confidence: sorted[0][1] / topPosts.length,
152
+ priority: 6,
153
+ expires_at: this.expiresAt(),
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ private suggestTemplates(): void {
160
+ const topPosts = this.engagementRepo.topPosts(10);
161
+
162
+ for (const post of topPosts) {
163
+ const fullPost = this.postRepo.getById(post.post_id);
164
+ if (!fullPost) continue;
165
+
166
+ // Check if a template already exists for similar structure
167
+ const existing = this.templateRepo.search(fullPost.format, 1);
168
+ if (existing.length > 0) continue;
169
+
170
+ // If top post has no matching template, suggest creating one
171
+ this.insightRepo.create({
172
+ type: 'template',
173
+ title: `Create template from top ${fullPost.platform} post`,
174
+ description: `Post #${fullPost.id} (${fullPost.format}) has high engagement. Consider extracting its structure as a reusable template.`,
175
+ confidence: 0.7,
176
+ priority: 5,
177
+ expires_at: this.expiresAt(),
178
+ });
179
+ }
180
+ }
181
+
182
+ private suggestOptimizations(): void {
183
+ const campaigns = this.campaignRepo.listActive();
184
+
185
+ for (const campaign of campaigns) {
186
+ const posts = this.postRepo.listByCampaign(campaign.id, 50);
187
+ if (posts.length < 3) continue;
188
+
189
+ let totalScore = 0;
190
+ let postCount = 0;
191
+ for (const post of posts) {
192
+ const eng = this.engagementRepo.getLatestByPost(post.id);
193
+ if (!eng) continue;
194
+ totalScore += engagementScore(eng);
195
+ postCount++;
196
+ }
197
+
198
+ if (postCount === 0) continue;
199
+ const avgScore = totalScore / postCount;
200
+
201
+ // Suggest cross-posting top content
202
+ const topPost = this.engagementRepo.topPosts(1);
203
+ if (topPost.length > 0 && topPost[0]) {
204
+ const top = topPost[0];
205
+ const topFullPost = this.postRepo.getById(top.post_id);
206
+ if (topFullPost && topFullPost.campaign_id === campaign.id) {
207
+ this.insightRepo.create({
208
+ type: 'optimization',
209
+ title: `Cross-post top content from "${campaign.name}"`,
210
+ description: `Your top post in "${campaign.name}" (score: ${engagementScore(top).toFixed(0)}) could be adapted for other platforms. Campaign avg: ${avgScore.toFixed(0)}.`,
211
+ confidence: 0.6,
212
+ priority: 4,
213
+ campaign_id: campaign.id,
214
+ expires_at: this.expiresAt(),
215
+ });
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ private expiresAt(): string {
222
+ const d = new Date();
223
+ d.setDate(d.getDate() + this.config.insightExpiryDays);
224
+ return d.toISOString();
225
+ }
226
+ }
@@ -0,0 +1,73 @@
1
+ import type { PostRepository } from '../db/repositories/post.repository.js';
2
+ import type { EngagementRepository } from '../db/repositories/engagement.repository.js';
3
+ import type { CampaignRepository } from '../db/repositories/campaign.repository.js';
4
+ import type { StrategyRepository } from '../db/repositories/strategy.repository.js';
5
+ import type { RuleRepository } from '../db/repositories/rule.repository.js';
6
+ import type { TemplateRepository } from '../db/repositories/template.repository.js';
7
+ import type { InsightRepository } from '../db/repositories/insight.repository.js';
8
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
9
+
10
+ export class AnalyticsService {
11
+ constructor(
12
+ private postRepo: PostRepository,
13
+ private engagementRepo: EngagementRepository,
14
+ private campaignRepo: CampaignRepository,
15
+ private strategyRepo: StrategyRepository,
16
+ private ruleRepo: RuleRepository,
17
+ private templateRepo: TemplateRepository,
18
+ private insightRepo: InsightRepository,
19
+ private synapseManager: SynapseManager,
20
+ ) {}
21
+
22
+ getSummary() {
23
+ const network = this.synapseManager.getNetworkStats();
24
+
25
+ return {
26
+ posts: {
27
+ total: this.postRepo.countAll(),
28
+ byPlatform: this.postRepo.countByPlatform(),
29
+ byStatus: this.postRepo.countByStatus(),
30
+ },
31
+ campaigns: {
32
+ total: this.campaignRepo.countAll(),
33
+ },
34
+ strategies: {
35
+ total: this.strategyRepo.countAll(),
36
+ },
37
+ rules: {
38
+ total: this.ruleRepo.countAll(),
39
+ active: this.ruleRepo.countActive(),
40
+ },
41
+ templates: {
42
+ total: this.templateRepo.countAll(),
43
+ },
44
+ insights: {
45
+ active: this.insightRepo.countActive(),
46
+ total: this.insightRepo.countAll(),
47
+ },
48
+ network: {
49
+ synapses: network.totalSynapses,
50
+ nodes: network.totalNodes,
51
+ avgWeight: network.avgWeight,
52
+ },
53
+ };
54
+ }
55
+
56
+ getTopPerformers(limit: number = 10) {
57
+ return {
58
+ topPosts: this.engagementRepo.topPosts(limit),
59
+ platformStats: this.engagementRepo.avgByPlatform(),
60
+ topStrategies: this.strategyRepo.topByConfidence(0.6, limit),
61
+ };
62
+ }
63
+
64
+ getDashboardData() {
65
+ return {
66
+ summary: this.getSummary(),
67
+ topPerformers: this.getTopPerformers(5),
68
+ recentInsights: this.insightRepo.listActive(5),
69
+ activeRules: this.ruleRepo.listActive(),
70
+ strongestConnections: this.synapseManager.getStrongestSynapses(10),
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,40 @@
1
+ import type { AudienceRepository } from '../db/repositories/audience.repository.js';
2
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
3
+ import type { Audience } from '../types/post.types.js';
4
+
5
+ export class AudienceService {
6
+ constructor(
7
+ private audienceRepo: AudienceRepository,
8
+ private synapseManager: SynapseManager,
9
+ ) {}
10
+
11
+ create(data: { name: string; platform?: string; demographics?: string; interests?: string }): Audience {
12
+ const existing = this.audienceRepo.getByName(data.name);
13
+ if (existing) return existing;
14
+
15
+ const id = this.audienceRepo.create(data);
16
+ return this.audienceRepo.getById(id)!;
17
+ }
18
+
19
+ linkToPost(audienceId: number, postId: number): void {
20
+ this.synapseManager.strengthen(
21
+ { type: 'post', id: postId },
22
+ { type: 'audience', id: audienceId },
23
+ 'engages_with',
24
+ );
25
+ }
26
+
27
+ getById(id: number): Audience | undefined {
28
+ return this.audienceRepo.getById(id);
29
+ }
30
+
31
+ listAll(): Audience[] {
32
+ return this.audienceRepo.listAll();
33
+ }
34
+
35
+ getStats() {
36
+ return {
37
+ total: this.audienceRepo.countAll(),
38
+ };
39
+ }
40
+ }
@@ -0,0 +1,80 @@
1
+ import type { CampaignRepository } from '../db/repositories/campaign.repository.js';
2
+ import type { PostRepository } from '../db/repositories/post.repository.js';
3
+ import type { EngagementRepository } from '../db/repositories/engagement.repository.js';
4
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
5
+ import type { Campaign, CampaignCreate } from '../types/post.types.js';
6
+ import { getEventBus } from '../utils/events.js';
7
+
8
+ export class CampaignService {
9
+ constructor(
10
+ private campaignRepo: CampaignRepository,
11
+ private postRepo: PostRepository,
12
+ private engagementRepo: EngagementRepository,
13
+ private synapseManager: SynapseManager,
14
+ ) {}
15
+
16
+ create(data: CampaignCreate): Campaign {
17
+ const existing = this.campaignRepo.getByName(data.name);
18
+ if (existing) return existing;
19
+
20
+ const id = this.campaignRepo.create(data);
21
+ const campaign = this.campaignRepo.getById(id)!;
22
+
23
+ getEventBus().emit('campaign:created', { campaignId: id, name: data.name });
24
+ return campaign;
25
+ }
26
+
27
+ getById(id: number): Campaign | undefined {
28
+ return this.campaignRepo.getById(id);
29
+ }
30
+
31
+ getByName(name: string): Campaign | undefined {
32
+ return this.campaignRepo.getByName(name);
33
+ }
34
+
35
+ listCampaigns(): Campaign[] {
36
+ return this.campaignRepo.listAll();
37
+ }
38
+
39
+ getStats(campaignId: number) {
40
+ const campaign = this.campaignRepo.getById(campaignId);
41
+ if (!campaign) return null;
42
+
43
+ const posts = this.postRepo.listByCampaign(campaignId, 1000);
44
+ let totalLikes = 0, totalComments = 0, totalShares = 0, totalImpressions = 0;
45
+
46
+ for (const post of posts) {
47
+ const eng = this.engagementRepo.getLatestByPost(post.id);
48
+ if (eng) {
49
+ totalLikes += eng.likes;
50
+ totalComments += eng.comments;
51
+ totalShares += eng.shares;
52
+ totalImpressions += eng.impressions;
53
+ }
54
+ }
55
+
56
+ return {
57
+ campaign,
58
+ postCount: posts.length,
59
+ totalLikes,
60
+ totalComments,
61
+ totalShares,
62
+ totalImpressions,
63
+ avgEngagement: posts.length > 0
64
+ ? (totalLikes + totalComments * 3 + totalShares * 5) / posts.length
65
+ : 0,
66
+ };
67
+ }
68
+
69
+ update(id: number, data: Partial<Campaign>): boolean {
70
+ return this.campaignRepo.update(id, data);
71
+ }
72
+
73
+ crossPromote(campaignA: number, campaignB: number): void {
74
+ this.synapseManager.strengthen(
75
+ { type: 'campaign', id: campaignA },
76
+ { type: 'campaign', id: campaignB },
77
+ 'cross_promotes',
78
+ );
79
+ }
80
+ }
@@ -0,0 +1,54 @@
1
+ import type { InsightRepository } from '../db/repositories/insight.repository.js';
2
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
3
+ import type { Insight, InsightCreate } from '../types/post.types.js';
4
+ import { getEventBus } from '../utils/events.js';
5
+
6
+ export class InsightService {
7
+ constructor(
8
+ private insightRepo: InsightRepository,
9
+ private synapseManager: SynapseManager,
10
+ ) {}
11
+
12
+ create(data: InsightCreate): Insight {
13
+ const id = this.insightRepo.create(data);
14
+ const insight = this.insightRepo.getById(id)!;
15
+
16
+ if (data.campaign_id) {
17
+ this.synapseManager.strengthen(
18
+ { type: 'insight', id },
19
+ { type: 'campaign', id: data.campaign_id },
20
+ 'informs',
21
+ );
22
+ }
23
+
24
+ getEventBus().emit('insight:created', { insightId: id, type: data.type });
25
+ return insight;
26
+ }
27
+
28
+ listActive(limit?: number): Insight[] {
29
+ return this.insightRepo.listActive(limit);
30
+ }
31
+
32
+ listByType(type: string, limit?: number): Insight[] {
33
+ return this.insightRepo.listByType(type, limit);
34
+ }
35
+
36
+ listByCampaign(campaignId: number): Insight[] {
37
+ return this.insightRepo.listByCampaign(campaignId);
38
+ }
39
+
40
+ deactivate(id: number): void {
41
+ this.insightRepo.deactivate(id);
42
+ }
43
+
44
+ expireOld(): number {
45
+ return this.insightRepo.expireOld();
46
+ }
47
+
48
+ getStats() {
49
+ return {
50
+ active: this.insightRepo.countActive(),
51
+ total: this.insightRepo.countAll(),
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,116 @@
1
+ import type { PostRepository } from '../db/repositories/post.repository.js';
2
+ import type { EngagementRepository } from '../db/repositories/engagement.repository.js';
3
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
4
+ import type { Post, PostCreate, EngagementCreate } from '../types/post.types.js';
5
+ import { getEventBus } from '../utils/events.js';
6
+ import { sha256 } from '../utils/hash.js';
7
+
8
+ export class PostService {
9
+ constructor(
10
+ private postRepo: PostRepository,
11
+ private engagementRepo: EngagementRepository,
12
+ private synapseManager: SynapseManager,
13
+ ) {}
14
+
15
+ report(data: PostCreate): { post: Post; isNew: boolean } {
16
+ const fingerprint = sha256(`${data.platform}:${data.content}`);
17
+ const existing = this.postRepo.getByFingerprint(fingerprint);
18
+
19
+ if (existing) {
20
+ if (data.url && !existing.url) {
21
+ this.postRepo.update(existing.id, { url: data.url, status: 'published', published_at: data.published_at ?? new Date().toISOString() });
22
+ }
23
+ return { post: this.postRepo.getById(existing.id)!, isNew: false };
24
+ }
25
+
26
+ const id = this.postRepo.create(data);
27
+ const post = this.postRepo.getById(id)!;
28
+
29
+ if (post.campaign_id) {
30
+ this.synapseManager.strengthen(
31
+ { type: 'post', id: post.id },
32
+ { type: 'campaign', id: post.campaign_id },
33
+ 'belongs_to',
34
+ );
35
+ }
36
+
37
+ getEventBus().emit('post:created', {
38
+ postId: post.id,
39
+ campaignId: post.campaign_id,
40
+ platform: post.platform,
41
+ });
42
+
43
+ return { post, isNew: true };
44
+ }
45
+
46
+ publish(postId: number, url?: string): Post | undefined {
47
+ const post = this.postRepo.getById(postId);
48
+ if (!post) return undefined;
49
+
50
+ this.postRepo.update(postId, {
51
+ status: 'published',
52
+ url: url ?? post.url,
53
+ published_at: new Date().toISOString(),
54
+ });
55
+
56
+ getEventBus().emit('post:published', { postId, platform: post.platform });
57
+ return this.postRepo.getById(postId);
58
+ }
59
+
60
+ updateEngagement(data: EngagementCreate): { engagementId: number; postId: number } {
61
+ const engagementId = this.engagementRepo.create(data);
62
+
63
+ getEventBus().emit('engagement:updated', { postId: data.post_id, engagementId });
64
+ return { engagementId, postId: data.post_id };
65
+ }
66
+
67
+ getById(id: number): Post | undefined {
68
+ return this.postRepo.getById(id);
69
+ }
70
+
71
+ listPosts(opts?: { platform?: string; campaignId?: number; limit?: number }): Post[] {
72
+ if (opts?.campaignId) return this.postRepo.listByCampaign(opts.campaignId, opts?.limit);
73
+ if (opts?.platform) return this.postRepo.listByPlatform(opts.platform, opts?.limit);
74
+ return this.postRepo.listAll(opts?.limit);
75
+ }
76
+
77
+ searchPosts(query: string, limit?: number): Post[] {
78
+ return this.postRepo.search(query, limit);
79
+ }
80
+
81
+ findSimilar(postId: number): Post[] {
82
+ const activated = this.synapseManager.activate({ type: 'post', id: postId });
83
+ const similarPostIds = activated
84
+ .filter(a => a.node.type === 'post' && a.node.id !== postId)
85
+ .slice(0, 10)
86
+ .map(a => a.node.id);
87
+
88
+ return similarPostIds
89
+ .map(id => this.postRepo.getById(id))
90
+ .filter((p): p is Post => p !== undefined);
91
+ }
92
+
93
+ getTopPosts(limit: number = 10) {
94
+ return this.engagementRepo.topPosts(limit);
95
+ }
96
+
97
+ getEngagement(postId: number) {
98
+ return this.engagementRepo.getLatestByPost(postId);
99
+ }
100
+
101
+ getEngagementHistory(postId: number) {
102
+ return this.engagementRepo.listByPost(postId);
103
+ }
104
+
105
+ getPlatformStats() {
106
+ return this.engagementRepo.avgByPlatform();
107
+ }
108
+
109
+ getPostStats() {
110
+ return {
111
+ total: this.postRepo.countAll(),
112
+ byPlatform: this.postRepo.countByPlatform(),
113
+ byStatus: this.postRepo.countByStatus(),
114
+ };
115
+ }
116
+ }
@@ -0,0 +1,90 @@
1
+ import type { RuleRepository } from '../db/repositories/rule.repository.js';
2
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
3
+ import type { MarketingRule, RuleCreate } from '../types/post.types.js';
4
+ import { getEventBus } from '../utils/events.js';
5
+
6
+ export interface RuleCheckResult {
7
+ passed: boolean;
8
+ violations: Array<{
9
+ rule: MarketingRule;
10
+ reason: string;
11
+ }>;
12
+ recommendations: Array<{
13
+ rule: MarketingRule;
14
+ suggestion: string;
15
+ }>;
16
+ }
17
+
18
+ export class RuleService {
19
+ constructor(
20
+ private ruleRepo: RuleRepository,
21
+ private synapseManager: SynapseManager,
22
+ ) {}
23
+
24
+ create(data: RuleCreate): MarketingRule {
25
+ const id = this.ruleRepo.create(data);
26
+ const rule = this.ruleRepo.getById(id)!;
27
+
28
+ getEventBus().emit('rule:learned', { ruleId: id, pattern: data.pattern });
29
+ return rule;
30
+ }
31
+
32
+ check(content: string, platform: string): RuleCheckResult {
33
+ const rules = this.ruleRepo.listActive();
34
+ const violations: RuleCheckResult['violations'] = [];
35
+ const recommendations: RuleCheckResult['recommendations'] = [];
36
+
37
+ for (const rule of rules) {
38
+ const pattern = rule.pattern.toLowerCase();
39
+ const contentLower = content.toLowerCase();
40
+ const platformLower = platform.toLowerCase();
41
+
42
+ // Check if rule pattern matches
43
+ if (pattern.includes('platform:')) {
44
+ const rulePlatform = pattern.split('platform:')[1]?.split(' ')[0] ?? '';
45
+ if (rulePlatform && rulePlatform !== platformLower) continue;
46
+ }
47
+
48
+ if (pattern.includes('no_') || pattern.includes('avoid_') || pattern.includes('never_')) {
49
+ // Preventive rules
50
+ const keywords = pattern.replace(/no_|avoid_|never_/g, '').split('_');
51
+ const matches = keywords.some(kw => contentLower.includes(kw));
52
+ if (matches) {
53
+ violations.push({ rule, reason: rule.recommendation });
54
+ }
55
+ } else {
56
+ // Recommendation rules
57
+ recommendations.push({ rule, suggestion: rule.recommendation });
58
+ }
59
+ }
60
+
61
+ return {
62
+ passed: violations.length === 0,
63
+ violations,
64
+ recommendations: recommendations.slice(0, 5),
65
+ };
66
+ }
67
+
68
+ listRules(): MarketingRule[] {
69
+ return this.ruleRepo.listActive();
70
+ }
71
+
72
+ listAllRules(): MarketingRule[] {
73
+ return this.ruleRepo.listAll();
74
+ }
75
+
76
+ recordTrigger(ruleId: number, success: boolean): void {
77
+ this.ruleRepo.incrementTrigger(ruleId, success);
78
+ }
79
+
80
+ getById(id: number): MarketingRule | undefined {
81
+ return this.ruleRepo.getById(id);
82
+ }
83
+
84
+ getStats() {
85
+ return {
86
+ total: this.ruleRepo.countAll(),
87
+ active: this.ruleRepo.countActive(),
88
+ };
89
+ }
90
+ }