@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,35 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import type { IpcMessage } from '../types/ipc.types.js';
3
+
4
+ export function encodeMessage(msg: IpcMessage): Buffer {
5
+ const json = JSON.stringify(msg);
6
+ const payload = Buffer.from(json, 'utf8');
7
+ const frame = Buffer.alloc(4 + payload.length);
8
+ frame.writeUInt32BE(payload.length, 0);
9
+ payload.copy(frame, 4);
10
+ return frame;
11
+ }
12
+
13
+ export class MessageDecoder {
14
+ private buffer = Buffer.alloc(0);
15
+
16
+ feed(chunk: Buffer): IpcMessage[] {
17
+ this.buffer = Buffer.concat([this.buffer, chunk]);
18
+ const messages: IpcMessage[] = [];
19
+
20
+ while (this.buffer.length >= 4) {
21
+ const length = this.buffer.readUInt32BE(0);
22
+ if (this.buffer.length < 4 + length) break;
23
+
24
+ const json = this.buffer.subarray(4, 4 + length).toString('utf8');
25
+ this.buffer = this.buffer.subarray(4 + length);
26
+ messages.push(JSON.parse(json) as IpcMessage);
27
+ }
28
+
29
+ return messages;
30
+ }
31
+
32
+ reset(): void {
33
+ this.buffer = Buffer.alloc(0);
34
+ }
35
+ }
@@ -0,0 +1,126 @@
1
+ import { getLogger } from '../utils/logger.js';
2
+
3
+ const logger = getLogger();
4
+ import type { PostService } from '../services/post.service.js';
5
+ import type { CampaignService } from '../services/campaign.service.js';
6
+ import type { StrategyService } from '../services/strategy.service.js';
7
+ import type { TemplateService } from '../services/template.service.js';
8
+ import type { RuleService } from '../services/rule.service.js';
9
+ import type { AudienceService } from '../services/audience.service.js';
10
+ import type { SynapseService } from '../services/synapse.service.js';
11
+ import type { AnalyticsService } from '../services/analytics.service.js';
12
+ import type { InsightService } from '../services/insight.service.js';
13
+ import type { LearningEngine } from '../learning/learning-engine.js';
14
+
15
+ export interface Services {
16
+ post: PostService;
17
+ campaign: CampaignService;
18
+ strategy: StrategyService;
19
+ template: TemplateService;
20
+ rule: RuleService;
21
+ audience: AudienceService;
22
+ synapse: SynapseService;
23
+ analytics: AnalyticsService;
24
+ insight: InsightService;
25
+ learning?: LearningEngine;
26
+ }
27
+
28
+ type MethodHandler = (params: unknown) => unknown;
29
+
30
+ export class IpcRouter {
31
+ private methods: Map<string, MethodHandler>;
32
+
33
+ constructor(private services: Services) {
34
+ this.methods = this.buildMethodMap();
35
+ }
36
+
37
+ handle(method: string, params: unknown): unknown {
38
+ const handler = this.methods.get(method);
39
+ if (!handler) {
40
+ throw new Error(`Unknown method: ${method}`);
41
+ }
42
+
43
+ logger.debug(`IPC: ${method}`, { params });
44
+ const result = handler(params);
45
+ logger.debug(`IPC: ${method} → done`);
46
+ return result;
47
+ }
48
+
49
+ listMethods(): string[] {
50
+ return [...this.methods.keys()];
51
+ }
52
+
53
+ private buildMethodMap(): Map<string, MethodHandler> {
54
+ const s = this.services;
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ const p = (params: unknown) => params as any;
57
+
58
+ return new Map<string, MethodHandler>([
59
+ // Posts
60
+ ['post.report', (params) => s.post.report(p(params))],
61
+ ['post.publish', (params) => s.post.publish(p(params).id ?? p(params).postId, p(params).url)],
62
+ ['post.get', (params) => s.post.getById(p(params).id)],
63
+ ['post.list', (params) => s.post.listPosts(p(params))],
64
+ ['post.search', (params) => s.post.searchPosts(p(params).query, p(params).limit)],
65
+ ['post.similar', (params) => s.post.findSimilar(p(params).id ?? p(params).postId)],
66
+ ['post.engagement', (params) => s.post.updateEngagement(p(params))],
67
+ ['post.getEngagement', (params) => s.post.getEngagement(p(params).id ?? p(params).postId)],
68
+ ['post.top', (params) => s.post.getTopPosts(p(params)?.limit)],
69
+ ['post.stats', () => s.post.getPostStats()],
70
+ ['post.platformStats', () => s.post.getPlatformStats()],
71
+
72
+ // Campaigns
73
+ ['campaign.create', (params) => s.campaign.create(p(params))],
74
+ ['campaign.get', (params) => s.campaign.getById(p(params).id)],
75
+ ['campaign.list', () => s.campaign.listCampaigns()],
76
+ ['campaign.stats', (params) => s.campaign.getStats(p(params).id ?? p(params).campaignId)],
77
+ ['campaign.update', (params) => s.campaign.update(p(params).id, p(params))],
78
+
79
+ // Strategies
80
+ ['strategy.report', (params) => s.strategy.report(p(params))],
81
+ ['strategy.suggest', (params) => s.strategy.suggest(p(params).query, p(params).limit)],
82
+ ['strategy.top', (params) => s.strategy.getTopStrategies(p(params)?.minConfidence, p(params)?.limit)],
83
+ ['strategy.list', (params) => s.strategy.listAll(p(params)?.limit)],
84
+
85
+ // Templates
86
+ ['template.find', (params) => s.template.find(p(params).query, p(params).limit)],
87
+ ['template.create', (params) => s.template.create(p(params))],
88
+ ['template.list', (params) => s.template.listAll(p(params)?.limit)],
89
+ ['template.byPlatform', (params) => s.template.findByPlatform(p(params).platform, p(params).limit)],
90
+ ['template.use', (params) => s.template.useTemplate(p(params).templateId, p(params).postId)],
91
+
92
+ // Rules
93
+ ['rule.check', (params) => s.rule.check(p(params).content, p(params).platform)],
94
+ ['rule.list', () => s.rule.listRules()],
95
+ ['rule.create', (params) => s.rule.create(p(params))],
96
+
97
+ // Audiences
98
+ ['audience.create', (params) => s.audience.create(p(params))],
99
+ ['audience.list', () => s.audience.listAll()],
100
+ ['audience.linkPost', (params) => s.audience.linkToPost(p(params).audienceId, p(params).postId)],
101
+
102
+ // Insights
103
+ ['insight.list', (params) => s.insight.listActive(p(params)?.limit)],
104
+ ['insight.byType', (params) => s.insight.listByType(p(params).type, p(params).limit)],
105
+ ['insight.byCampaign', (params) => s.insight.listByCampaign(p(params).campaignId)],
106
+
107
+ // Synapses
108
+ ['synapse.context', (params) => s.synapse.getPostContext(p(params).postId ?? p(params).id)],
109
+ ['synapse.path', (params) => s.synapse.findPath(p(params).fromType, p(params).fromId, p(params).toType, p(params).toId)],
110
+ ['synapse.related', (params) => s.synapse.getRelated(p(params))],
111
+ ['synapse.stats', () => s.synapse.getNetworkStats()],
112
+ ['synapse.strongest', (params) => s.synapse.getStrongest(p(params)?.limit)],
113
+
114
+ // Analytics
115
+ ['analytics.summary', () => s.analytics.getSummary()],
116
+ ['analytics.top', (params) => s.analytics.getTopPerformers(p(params)?.limit)],
117
+ ['analytics.dashboard', () => s.analytics.getDashboardData()],
118
+
119
+ // Learning
120
+ ['learning.run', () => {
121
+ if (!s.learning) throw new Error('Learning engine not available');
122
+ return s.learning.runCycle();
123
+ }],
124
+ ]);
125
+ }
126
+ }
@@ -0,0 +1,140 @@
1
+ import net from 'node:net';
2
+ import fs from 'node:fs';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { getLogger } from '../utils/logger.js';
5
+
6
+ const logger = getLogger();
7
+ import type { IpcMessage } from '../types/ipc.types.js';
8
+ import { encodeMessage, MessageDecoder } from './protocol.js';
9
+ import type { IpcRouter } from './router.js';
10
+
11
+ export class IpcServer {
12
+ private server: net.Server | null = null;
13
+ private clients = new Map<string, net.Socket>();
14
+
15
+ constructor(
16
+ private router: IpcRouter,
17
+ private pipeName: string,
18
+ ) {}
19
+
20
+ start(): void {
21
+ this.createServer();
22
+ this.listen();
23
+ }
24
+
25
+ private createServer(): void {
26
+ this.server = net.createServer((socket) => {
27
+ const clientId = randomUUID();
28
+ this.clients.set(clientId, socket);
29
+ const decoder = new MessageDecoder();
30
+
31
+ logger.info(`IPC client connected: ${clientId}`);
32
+
33
+ socket.on('data', (chunk) => {
34
+ const messages = decoder.feed(chunk);
35
+ for (const msg of messages) {
36
+ this.handleMessage(clientId, msg, socket);
37
+ }
38
+ });
39
+
40
+ socket.on('close', () => {
41
+ logger.info(`IPC client disconnected: ${clientId}`);
42
+ this.clients.delete(clientId);
43
+ });
44
+
45
+ socket.on('error', (err) => {
46
+ logger.error(`IPC client ${clientId} error:`, err);
47
+ this.clients.delete(clientId);
48
+ });
49
+ });
50
+ }
51
+
52
+ private listen(retried = false): void {
53
+ if (!this.server) return;
54
+
55
+ this.server.on('error', (err: NodeJS.ErrnoException) => {
56
+ if (err.code === 'EADDRINUSE' && !retried) {
57
+ logger.warn(`IPC pipe in use, attempting to recover: ${this.pipeName}`);
58
+ this.recoverStalePipe();
59
+ } else {
60
+ logger.error('IPC server error:', err);
61
+ }
62
+ });
63
+
64
+ this.server.listen(this.pipeName, () => {
65
+ logger.info(`IPC server listening on ${this.pipeName}`);
66
+ });
67
+ }
68
+
69
+ private recoverStalePipe(): void {
70
+ const probe = net.createConnection(this.pipeName);
71
+
72
+ probe.on('connect', () => {
73
+ probe.destroy();
74
+ logger.error('IPC pipe is held by another running daemon. Stop it first with: marketing stop');
75
+ });
76
+
77
+ probe.on('error', () => {
78
+ probe.destroy();
79
+ logger.info('Stale IPC pipe detected, reclaiming...');
80
+
81
+ if (process.platform !== 'win32') {
82
+ try { fs.unlinkSync(this.pipeName); } catch { /* ignore */ }
83
+ }
84
+
85
+ this.createServer();
86
+ this.server!.on('error', (err) => {
87
+ logger.error('IPC server error after recovery:', err);
88
+ });
89
+ this.server!.listen(this.pipeName, () => {
90
+ logger.info(`IPC server recovered and listening on ${this.pipeName}`);
91
+ });
92
+ });
93
+
94
+ probe.setTimeout(2000, () => {
95
+ probe.destroy();
96
+ logger.warn('IPC pipe probe timed out, treating as stale');
97
+ if (process.platform !== 'win32') {
98
+ try { fs.unlinkSync(this.pipeName); } catch { /* ignore */ }
99
+ }
100
+ this.createServer();
101
+ this.server!.on('error', (err) => {
102
+ logger.error('IPC server error after timeout recovery:', err);
103
+ });
104
+ this.server!.listen(this.pipeName, () => {
105
+ logger.info(`IPC server recovered (timeout) and listening on ${this.pipeName}`);
106
+ });
107
+ });
108
+ }
109
+
110
+ private handleMessage(clientId: string, msg: IpcMessage, socket: net.Socket): void {
111
+ if (msg.type !== 'request' || !msg.method) return;
112
+
113
+ try {
114
+ const result = this.router.handle(msg.method, msg.params);
115
+ const response: IpcMessage = {
116
+ id: msg.id,
117
+ type: 'response',
118
+ result,
119
+ };
120
+ socket.write(encodeMessage(response));
121
+ } catch (err) {
122
+ const response: IpcMessage = {
123
+ id: msg.id,
124
+ type: 'response',
125
+ error: { code: -1, message: err instanceof Error ? err.message : String(err) },
126
+ };
127
+ socket.write(encodeMessage(response));
128
+ }
129
+ }
130
+
131
+ stop(): void {
132
+ for (const socket of this.clients.values()) {
133
+ socket.destroy();
134
+ }
135
+ this.clients.clear();
136
+ this.server?.close();
137
+ this.server = null;
138
+ logger.info('IPC server stopped');
139
+ }
140
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Wilson Score Interval — lower bound for confidence scoring.
3
+ * Used to evaluate rule confidence based on trigger/success counts.
4
+ */
5
+ export function wilsonScore(successes: number, total: number, z: number = 1.96): number {
6
+ if (total === 0) return 0;
7
+
8
+ const p = successes / total;
9
+ const denominator = 1 + z * z / total;
10
+ const centre = p + z * z / (2 * total);
11
+ const offset = z * Math.sqrt((p * (1 - p) + z * z / (4 * total)) / total);
12
+
13
+ return (centre - offset) / denominator;
14
+ }
15
+
16
+ /**
17
+ * Compute engagement score from raw metrics.
18
+ * Weights: shares > comments > clicks > likes > impressions
19
+ */
20
+ export function engagementScore(metrics: {
21
+ likes?: number;
22
+ comments?: number;
23
+ shares?: number;
24
+ impressions?: number;
25
+ clicks?: number;
26
+ saves?: number;
27
+ }): number {
28
+ return (
29
+ (metrics.likes ?? 0) * 1 +
30
+ (metrics.comments ?? 0) * 3 +
31
+ (metrics.shares ?? 0) * 5 +
32
+ (metrics.clicks ?? 0) * 2 +
33
+ (metrics.saves ?? 0) * 4 +
34
+ (metrics.impressions ?? 0) * 0.01
35
+ );
36
+ }
@@ -0,0 +1,254 @@
1
+ import type { LearningConfig } 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 { RuleRepository } from '../db/repositories/rule.repository.js';
5
+ import type { StrategyRepository } from '../db/repositories/strategy.repository.js';
6
+ import type { SynapseManager } from '../synapses/synapse-manager.js';
7
+ import { wilsonScore, engagementScore } from './confidence-scorer.js';
8
+ import { getLogger } from '../utils/logger.js';
9
+
10
+ export interface LearningCycleResult {
11
+ rulesCreated: number;
12
+ rulesUpdated: number;
13
+ strategiesUpdated: number;
14
+ synapsesDecayed: number;
15
+ synapsesPruned: number;
16
+ }
17
+
18
+ export class LearningEngine {
19
+ private timer: ReturnType<typeof setInterval> | null = null;
20
+ private logger = getLogger();
21
+
22
+ constructor(
23
+ private config: LearningConfig,
24
+ private postRepo: PostRepository,
25
+ private engagementRepo: EngagementRepository,
26
+ private ruleRepo: RuleRepository,
27
+ private strategyRepo: StrategyRepository,
28
+ private synapseManager: SynapseManager,
29
+ ) {}
30
+
31
+ start(): void {
32
+ this.timer = setInterval(() => {
33
+ try {
34
+ this.runCycle();
35
+ } catch (err) {
36
+ this.logger.error('Learning cycle error:', err);
37
+ }
38
+ }, this.config.intervalMs);
39
+ }
40
+
41
+ stop(): void {
42
+ if (this.timer) {
43
+ clearInterval(this.timer);
44
+ this.timer = null;
45
+ }
46
+ }
47
+
48
+ runCycle(): LearningCycleResult {
49
+ this.logger.info('Starting learning cycle');
50
+ const result: LearningCycleResult = {
51
+ rulesCreated: 0,
52
+ rulesUpdated: 0,
53
+ strategiesUpdated: 0,
54
+ synapsesDecayed: 0,
55
+ synapsesPruned: 0,
56
+ };
57
+
58
+ // 1. Analyze recent posts for patterns
59
+ result.rulesCreated += this.extractTimingPatterns();
60
+ result.rulesCreated += this.extractFormatPatterns();
61
+ result.rulesCreated += this.extractPlatformPatterns();
62
+
63
+ // 2. Update strategy confidence based on engagement
64
+ result.strategiesUpdated = this.updateStrategyConfidence();
65
+
66
+ // 3. Update rule confidence via Wilson Score
67
+ result.rulesUpdated = this.updateRuleConfidence();
68
+
69
+ // 4. Run synapse decay
70
+ const decay = this.synapseManager.runDecay();
71
+ result.synapsesDecayed = decay.decayed;
72
+ result.synapsesPruned = decay.pruned;
73
+
74
+ // 5. Wire similar posts
75
+ this.wireSimilarPosts();
76
+
77
+ this.logger.info(`Learning cycle complete: ${JSON.stringify(result)}`);
78
+ return result;
79
+ }
80
+
81
+ private extractTimingPatterns(): number {
82
+ let created = 0;
83
+ const recentPosts = this.postRepo.listPublished(100);
84
+
85
+ // Group by hour of day
86
+ const hourBuckets: Record<number, { total: number; avgScore: number }> = {};
87
+
88
+ for (const post of recentPosts) {
89
+ if (!post.published_at) continue;
90
+ const hour = new Date(post.published_at).getHours();
91
+ const eng = this.engagementRepo.getLatestByPost(post.id);
92
+ if (!eng) continue;
93
+
94
+ const score = engagementScore(eng);
95
+ if (!hourBuckets[hour]) hourBuckets[hour] = { total: 0, avgScore: 0 };
96
+ hourBuckets[hour].total++;
97
+ hourBuckets[hour].avgScore += score;
98
+ }
99
+
100
+ // Find best/worst hours
101
+ for (const [hour, data] of Object.entries(hourBuckets)) {
102
+ if (data.total < this.config.minOccurrences) continue;
103
+ data.avgScore /= data.total;
104
+ }
105
+
106
+ const hours = Object.entries(hourBuckets)
107
+ .filter(([, d]) => d.total >= this.config.minOccurrences)
108
+ .sort(([, a], [, b]) => b.avgScore - a.avgScore);
109
+
110
+ if (hours.length >= 2) {
111
+ const bestHour = hours[0];
112
+ const worstHour = hours[hours.length - 1];
113
+
114
+ if (bestHour && worstHour && bestHour[1].avgScore > worstHour[1].avgScore * 2) {
115
+ this.ruleRepo.create({
116
+ pattern: `best_time_${bestHour[0]}h`,
117
+ recommendation: `Posts around ${bestHour[0]}:00 perform ${(bestHour[1].avgScore / Math.max(1, worstHour[1].avgScore)).toFixed(1)}x better than ${worstHour[0]}:00`,
118
+ confidence: wilsonScore(bestHour[1].total, recentPosts.length),
119
+ });
120
+ created++;
121
+ }
122
+ }
123
+
124
+ return created;
125
+ }
126
+
127
+ private extractFormatPatterns(): number {
128
+ let created = 0;
129
+ const recentPosts = this.postRepo.listPublished(100);
130
+
131
+ const formatBuckets: Record<string, { total: number; avgScore: number }> = {};
132
+
133
+ for (const post of recentPosts) {
134
+ const eng = this.engagementRepo.getLatestByPost(post.id);
135
+ if (!eng) continue;
136
+
137
+ const score = engagementScore(eng);
138
+ if (!formatBuckets[post.format]) formatBuckets[post.format] = { total: 0, avgScore: 0 };
139
+ formatBuckets[post.format].total++;
140
+ formatBuckets[post.format].avgScore += score;
141
+ }
142
+
143
+ for (const [format, data] of Object.entries(formatBuckets)) {
144
+ if (data.total < this.config.minOccurrences) continue;
145
+ data.avgScore /= data.total;
146
+ }
147
+
148
+ const formats = Object.entries(formatBuckets)
149
+ .filter(([, d]) => d.total >= this.config.minOccurrences)
150
+ .sort(([, a], [, b]) => b.avgScore - a.avgScore);
151
+
152
+ if (formats.length >= 2 && formats[0]) {
153
+ const [bestFormat, bestData] = formats[0];
154
+ this.ruleRepo.create({
155
+ pattern: `best_format_${bestFormat}`,
156
+ recommendation: `${bestFormat} posts average ${bestData.avgScore.toFixed(0)} engagement score (best format)`,
157
+ confidence: wilsonScore(bestData.total, recentPosts.length),
158
+ });
159
+ created++;
160
+ }
161
+
162
+ return created;
163
+ }
164
+
165
+ private extractPlatformPatterns(): number {
166
+ let created = 0;
167
+ const platformStats = this.engagementRepo.avgByPlatform();
168
+
169
+ if (platformStats.length >= 2) {
170
+ const toScore = (p: typeof platformStats[number]) => engagementScore({
171
+ likes: p.avg_likes, comments: p.avg_comments,
172
+ shares: p.avg_shares, impressions: p.avg_impressions, clicks: p.avg_clicks,
173
+ });
174
+ const sorted = [...platformStats].sort((a, b) => toScore(b) - toScore(a));
175
+ const best = sorted[0];
176
+ if (best && best.post_count >= this.config.minOccurrences) {
177
+ this.ruleRepo.create({
178
+ pattern: `best_platform_${best.platform}`,
179
+ recommendation: `${best.platform} is your top-performing platform (${best.post_count} posts, avg ${best.avg_likes.toFixed(0)} likes)`,
180
+ confidence: wilsonScore(best.post_count, platformStats.reduce((s, p) => s + p.post_count, 0)),
181
+ });
182
+ created++;
183
+ }
184
+ }
185
+
186
+ return created;
187
+ }
188
+
189
+ private updateStrategyConfidence(): number {
190
+ let updated = 0;
191
+ const strategies = this.strategyRepo.listAll(100);
192
+
193
+ for (const strategy of strategies) {
194
+ if (!strategy.post_id) continue;
195
+ const eng = this.engagementRepo.getLatestByPost(strategy.post_id);
196
+ if (!eng) continue;
197
+
198
+ const score = engagementScore(eng);
199
+ // Normalize: 0-10 → 0.0-1.0 confidence
200
+ const newConfidence = Math.min(1.0, score / 100);
201
+
202
+ if (Math.abs(newConfidence - strategy.confidence) > 0.05) {
203
+ this.strategyRepo.update(strategy.id, { confidence: newConfidence });
204
+ updated++;
205
+ }
206
+ }
207
+
208
+ return updated;
209
+ }
210
+
211
+ private updateRuleConfidence(): number {
212
+ let updated = 0;
213
+ const rules = this.ruleRepo.listAll();
214
+
215
+ for (const rule of rules) {
216
+ if (rule.trigger_count < this.config.minOccurrences) continue;
217
+
218
+ const newConfidence = wilsonScore(rule.success_count, rule.trigger_count);
219
+
220
+ if (newConfidence < this.config.pruneThreshold && rule.active) {
221
+ this.ruleRepo.update(rule.id, { active: 0, confidence: newConfidence });
222
+ updated++;
223
+ } else if (Math.abs(newConfidence - rule.confidence) > 0.05) {
224
+ this.ruleRepo.update(rule.id, { confidence: newConfidence });
225
+ updated++;
226
+ }
227
+ }
228
+
229
+ return updated;
230
+ }
231
+
232
+ private wireSimilarPosts(): void {
233
+ const posts = this.postRepo.listPublished(50);
234
+
235
+ for (let i = 0; i < posts.length; i++) {
236
+ for (let j = i + 1; j < Math.min(i + 5, posts.length); j++) {
237
+ const a = posts[i]!;
238
+ const b = posts[j]!;
239
+
240
+ // Simple similarity: same platform, similar content length, similar hashtags
241
+ if (a.platform === b.platform) {
242
+ const lengthRatio = Math.min(a.content.length, b.content.length) / Math.max(a.content.length, b.content.length);
243
+ if (lengthRatio > 0.5) {
244
+ this.synapseManager.strengthen(
245
+ { type: 'post', id: a.id },
246
+ { type: 'post', id: b.id },
247
+ 'similar_to',
248
+ );
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }