@timmeck/marketing-brain 0.2.1 → 0.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 (123) hide show
  1. package/dist/cli/colors.d.ts +11 -24
  2. package/dist/cli/colors.js +3 -46
  3. package/dist/cli/colors.js.map +1 -1
  4. package/dist/cli/commands/peers.d.ts +2 -0
  5. package/dist/cli/commands/peers.js +38 -0
  6. package/dist/cli/commands/peers.js.map +1 -0
  7. package/dist/db/connection.d.ts +1 -2
  8. package/dist/db/connection.js +1 -18
  9. package/dist/db/connection.js.map +1 -1
  10. package/dist/hooks/post-tool-use.d.ts +2 -0
  11. package/dist/hooks/post-tool-use.js +182 -0
  12. package/dist/hooks/post-tool-use.js.map +1 -0
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/ipc/client.d.ts +1 -13
  16. package/dist/ipc/client.js +1 -92
  17. package/dist/ipc/client.js.map +1 -1
  18. package/dist/ipc/protocol.d.ts +1 -8
  19. package/dist/ipc/protocol.js +1 -28
  20. package/dist/ipc/protocol.js.map +1 -1
  21. package/dist/ipc/router.js +8 -0
  22. package/dist/ipc/router.js.map +1 -1
  23. package/dist/ipc/server.d.ts +1 -14
  24. package/dist/ipc/server.js +1 -129
  25. package/dist/ipc/server.js.map +1 -1
  26. package/dist/marketing-core.d.ts +1 -0
  27. package/dist/marketing-core.js +6 -1
  28. package/dist/marketing-core.js.map +1 -1
  29. package/dist/mcp/server.js +5 -60
  30. package/dist/mcp/server.js.map +1 -1
  31. package/dist/types/ipc.types.d.ts +1 -11
  32. package/dist/utils/events.d.ts +4 -8
  33. package/dist/utils/events.js +2 -14
  34. package/dist/utils/events.js.map +1 -1
  35. package/dist/utils/hash.d.ts +1 -1
  36. package/dist/utils/hash.js +1 -4
  37. package/dist/utils/hash.js.map +1 -1
  38. package/dist/utils/logger.d.ts +3 -2
  39. package/dist/utils/logger.js +8 -35
  40. package/dist/utils/logger.js.map +1 -1
  41. package/dist/utils/paths.d.ts +2 -1
  42. package/dist/utils/paths.js +4 -13
  43. package/dist/utils/paths.js.map +1 -1
  44. package/package.json +2 -1
  45. package/.github/FUNDING.yml +0 -1
  46. package/.github/workflows/ci.yml +0 -27
  47. package/.mcp.json +0 -9
  48. package/src/api/server.ts +0 -86
  49. package/src/cli/colors.ts +0 -59
  50. package/src/cli/commands/campaign.ts +0 -66
  51. package/src/cli/commands/config.ts +0 -168
  52. package/src/cli/commands/dashboard.ts +0 -165
  53. package/src/cli/commands/doctor.ts +0 -110
  54. package/src/cli/commands/export.ts +0 -40
  55. package/src/cli/commands/import.ts +0 -84
  56. package/src/cli/commands/insights.ts +0 -44
  57. package/src/cli/commands/learn.ts +0 -24
  58. package/src/cli/commands/network.ts +0 -71
  59. package/src/cli/commands/post.ts +0 -47
  60. package/src/cli/commands/query.ts +0 -108
  61. package/src/cli/commands/rules.ts +0 -27
  62. package/src/cli/commands/start.ts +0 -100
  63. package/src/cli/commands/status.ts +0 -73
  64. package/src/cli/commands/stop.ts +0 -33
  65. package/src/cli/commands/suggest.ts +0 -64
  66. package/src/cli/ipc-helper.ts +0 -22
  67. package/src/cli/update-check.ts +0 -63
  68. package/src/config.ts +0 -110
  69. package/src/dashboard/renderer.ts +0 -136
  70. package/src/dashboard/server.ts +0 -140
  71. package/src/db/connection.ts +0 -22
  72. package/src/db/migrations/001_core_schema.ts +0 -63
  73. package/src/db/migrations/002_learning_schema.ts +0 -46
  74. package/src/db/migrations/003_synapse_schema.ts +0 -27
  75. package/src/db/migrations/004_insights_schema.ts +0 -38
  76. package/src/db/migrations/005_fts_indexes.ts +0 -77
  77. package/src/db/migrations/index.ts +0 -62
  78. package/src/db/repositories/audience.repository.ts +0 -53
  79. package/src/db/repositories/campaign.repository.ts +0 -72
  80. package/src/db/repositories/engagement.repository.ts +0 -108
  81. package/src/db/repositories/insight.repository.ts +0 -100
  82. package/src/db/repositories/post.repository.ts +0 -123
  83. package/src/db/repositories/rule.repository.ts +0 -87
  84. package/src/db/repositories/strategy.repository.ts +0 -82
  85. package/src/db/repositories/synapse.repository.ts +0 -148
  86. package/src/db/repositories/template.repository.ts +0 -76
  87. package/src/index.ts +0 -69
  88. package/src/ipc/__tests__/protocol.test.ts +0 -153
  89. package/src/ipc/client.ts +0 -110
  90. package/src/ipc/protocol.ts +0 -35
  91. package/src/ipc/router.ts +0 -126
  92. package/src/ipc/server.ts +0 -140
  93. package/src/learning/confidence-scorer.ts +0 -36
  94. package/src/learning/learning-engine.ts +0 -254
  95. package/src/marketing-core.ts +0 -285
  96. package/src/mcp/server.ts +0 -72
  97. package/src/mcp/tools.ts +0 -216
  98. package/src/research/research-engine.ts +0 -226
  99. package/src/services/analytics.service.ts +0 -73
  100. package/src/services/audience.service.ts +0 -40
  101. package/src/services/campaign.service.ts +0 -80
  102. package/src/services/insight.service.ts +0 -54
  103. package/src/services/post.service.ts +0 -116
  104. package/src/services/rule.service.ts +0 -90
  105. package/src/services/strategy.service.ts +0 -53
  106. package/src/services/synapse.service.ts +0 -32
  107. package/src/services/template.service.ts +0 -50
  108. package/src/synapses/activation.ts +0 -80
  109. package/src/synapses/decay.ts +0 -38
  110. package/src/synapses/hebbian.ts +0 -68
  111. package/src/synapses/pathfinder.ts +0 -81
  112. package/src/synapses/synapse-manager.ts +0 -115
  113. package/src/types/config.types.ts +0 -79
  114. package/src/types/ipc.types.ts +0 -8
  115. package/src/types/post.types.ts +0 -156
  116. package/src/types/synapse.types.ts +0 -43
  117. package/src/utils/__tests__/hash.test.ts +0 -39
  118. package/src/utils/__tests__/paths.test.ts +0 -70
  119. package/src/utils/events.ts +0 -44
  120. package/src/utils/hash.ts +0 -5
  121. package/src/utils/logger.ts +0 -48
  122. package/src/utils/paths.ts +0 -19
  123. package/tsconfig.json +0 -18
@@ -1,64 +0,0 @@
1
- import { Command } from 'commander';
2
- import { withIpc } from '../ipc-helper.js';
3
- import { c, icons } from '../colors.js';
4
-
5
- export function suggestCommand(): Command {
6
- return new Command('suggest')
7
- .description('Get content suggestions for a topic')
8
- .argument('<topic>', 'Topic to get suggestions for')
9
- .option('-p, --platform <platform>', 'Target platform')
10
- .action(async (topic, opts) => {
11
- await withIpc(async (client) => {
12
- // Get matching strategies
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- const strategies: any[] = await client.request('strategy.suggest', { query: topic, limit: 3 }) as any[];
15
-
16
- // Get matching templates
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- const templates: any[] = (opts.platform
19
- ? await client.request('template.byPlatform', { platform: opts.platform, limit: 3 })
20
- : await client.request('template.find', { query: topic, limit: 3 })) as any[];
21
-
22
- // Check rules
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- const ruleCheck: any = await client.request('rule.check', {
25
- content: topic,
26
- platform: opts.platform ?? 'any',
27
- });
28
-
29
- console.log(` ${icons.insight} ${c.heading(`Suggestions for: ${topic}`)}`);
30
- console.log();
31
-
32
- if (strategies.length > 0) {
33
- console.log(` ${c.blue.bold('Strategies:')}`);
34
- for (const s of strategies) {
35
- console.log(` ${icons.arrow} ${s.description} ${c.dim(`(${(s.confidence * 100).toFixed(0)}% confidence)`)}`);
36
- }
37
- console.log();
38
- }
39
-
40
- if (templates.length > 0) {
41
- console.log(` ${c.purple.bold('Templates:')}`);
42
- for (const t of templates) {
43
- console.log(` ${icons.arrow} ${t.name} ${c.dim(`(${t.platform ?? 'any'}, used ${t.use_count}x)`)}`);
44
- }
45
- console.log();
46
- }
47
-
48
- if (ruleCheck.recommendations?.length > 0) {
49
- console.log(` ${c.orange.bold('Rules to consider:')}`);
50
- for (const r of ruleCheck.recommendations) {
51
- console.log(` ${icons.arrow} ${r.suggestion}`);
52
- }
53
- console.log();
54
- }
55
-
56
- if (ruleCheck.violations?.length > 0) {
57
- console.log(` ${c.red.bold('Warnings:')}`);
58
- for (const v of ruleCheck.violations) {
59
- console.log(` ${icons.warn} ${v.reason}`);
60
- }
61
- }
62
- });
63
- });
64
- }
@@ -1,22 +0,0 @@
1
- import { IpcClient } from '../ipc/client.js';
2
- import { getPipeName } from '../utils/paths.js';
3
- import { c, icons } from './colors.js';
4
-
5
- export async function withIpc<T>(fn: (client: IpcClient) => Promise<T>): Promise<T> {
6
- const client = new IpcClient(getPipeName(), 5000);
7
- try {
8
- await client.connect();
9
- return await fn(client);
10
- } catch (err) {
11
- if (err instanceof Error && err.message.includes('ENOENT')) {
12
- console.error(`${icons.error} ${c.error('Marketing Brain daemon is not running.')} Start it with: ${c.cyan('marketing start')}`);
13
- } else if (err instanceof Error && err.message.includes('ECONNREFUSED')) {
14
- console.error(`${icons.error} ${c.error('Marketing Brain daemon is not responding.')} Try: ${c.cyan('marketing stop && marketing start')}`);
15
- } else {
16
- console.error(`${icons.error} ${c.error(err instanceof Error ? err.message : String(err))}`);
17
- }
18
- process.exit(1);
19
- } finally {
20
- client.disconnect();
21
- }
22
- }
@@ -1,63 +0,0 @@
1
- import https from 'node:https';
2
- import { c, icons } from './colors.js';
3
-
4
- // Read current version from package.json at build time
5
- import { createRequire } from 'node:module';
6
- const require = createRequire(import.meta.url);
7
- const pkg = require('../../package.json');
8
- const CURRENT_VERSION: string = pkg.version;
9
-
10
- export function getCurrentVersion(): string {
11
- return CURRENT_VERSION;
12
- }
13
-
14
- function fetchLatestVersion(): Promise<string | null> {
15
- return new Promise((resolve) => {
16
- const timeout = setTimeout(() => resolve(null), 3000);
17
-
18
- const req = https.get(
19
- 'https://registry.npmjs.org/@timmeck/marketing-brain/latest',
20
- { headers: { Accept: 'application/json' } },
21
- (res) => {
22
- let data = '';
23
- res.on('data', (chunk) => { data += chunk; });
24
- res.on('end', () => {
25
- clearTimeout(timeout);
26
- try {
27
- const json = JSON.parse(data);
28
- resolve(json.version ?? null);
29
- } catch {
30
- resolve(null);
31
- }
32
- });
33
- },
34
- );
35
- req.on('error', () => {
36
- clearTimeout(timeout);
37
- resolve(null);
38
- });
39
- });
40
- }
41
-
42
- function isNewer(latest: string, current: string): boolean {
43
- const l = latest.split('.').map(Number);
44
- const cur = current.split('.').map(Number);
45
- for (let i = 0; i < 3; i++) {
46
- if ((l[i] ?? 0) > (cur[i] ?? 0)) return true;
47
- if ((l[i] ?? 0) < (cur[i] ?? 0)) return false;
48
- }
49
- return false;
50
- }
51
-
52
- export async function checkForUpdate(): Promise<void> {
53
- try {
54
- const latest = await fetchLatestVersion();
55
- if (latest && isNewer(latest, CURRENT_VERSION)) {
56
- console.log();
57
- console.log(` ${icons.star} ${c.orange.bold(`Update available: v${CURRENT_VERSION} → v${latest}`)}`);
58
- console.log(` Run: ${c.cyan('npm update -g @timmeck/marketing-brain')}`);
59
- }
60
- } catch {
61
- // silently ignore — update check is best-effort
62
- }
63
- }
package/src/config.ts DELETED
@@ -1,110 +0,0 @@
1
- import path from 'node:path';
2
- import fs from 'node:fs';
3
- import type { MarketingBrainConfig } from './types/config.types.js';
4
- import { getDataDir, getPipeName } from './utils/paths.js';
5
-
6
- const defaults: MarketingBrainConfig = {
7
- dataDir: getDataDir(),
8
- dbPath: path.join(getDataDir(), 'marketing-brain.db'),
9
- ipc: {
10
- pipeName: getPipeName(),
11
- timeout: 5000,
12
- },
13
- api: {
14
- port: 7781,
15
- enabled: true,
16
- },
17
- mcpHttp: {
18
- port: 7782,
19
- enabled: true,
20
- },
21
- dashboard: {
22
- port: 7783,
23
- enabled: true,
24
- },
25
- learning: {
26
- intervalMs: 900_000, // 15 min
27
- minOccurrences: 3,
28
- minConfidence: 0.60,
29
- pruneThreshold: 0.20,
30
- decayHalfLifeDays: 30,
31
- },
32
- matching: {
33
- similarityThreshold: 0.8,
34
- maxResults: 10,
35
- },
36
- synapses: {
37
- initialWeight: 0.1,
38
- learningRate: 0.15,
39
- decayHalfLifeDays: 45,
40
- pruneThreshold: 0.05,
41
- decayAfterDays: 14,
42
- maxDepth: 3,
43
- minActivationWeight: 0.2,
44
- },
45
- research: {
46
- intervalMs: 3_600_000, // 1 hour
47
- initialDelayMs: 300_000, // 5 min
48
- minDataPoints: 5,
49
- trendWindowDays: 7,
50
- insightExpiryDays: 30,
51
- },
52
- log: {
53
- level: 'info',
54
- file: path.join(getDataDir(), 'marketing-brain.log'),
55
- maxSize: 10 * 1024 * 1024,
56
- maxFiles: 3,
57
- },
58
- retention: {
59
- postDays: 365,
60
- strategyDays: 365,
61
- insightDays: 30,
62
- },
63
- };
64
-
65
- function applyEnvOverrides(config: MarketingBrainConfig): void {
66
- if (process.env['MARKETING_BRAIN_DATA_DIR']) {
67
- config.dataDir = process.env['MARKETING_BRAIN_DATA_DIR'];
68
- config.dbPath = path.join(config.dataDir, 'marketing-brain.db');
69
- config.log.file = path.join(config.dataDir, 'marketing-brain.log');
70
- }
71
- if (process.env['MARKETING_BRAIN_DB_PATH']) config.dbPath = process.env['MARKETING_BRAIN_DB_PATH'];
72
- if (process.env['MARKETING_BRAIN_LOG_LEVEL']) config.log.level = process.env['MARKETING_BRAIN_LOG_LEVEL'];
73
- if (process.env['MARKETING_BRAIN_PIPE_NAME']) config.ipc.pipeName = process.env['MARKETING_BRAIN_PIPE_NAME'];
74
- if (process.env['MARKETING_BRAIN_API_PORT']) config.api.port = Number(process.env['MARKETING_BRAIN_API_PORT']);
75
- if (process.env['MARKETING_BRAIN_API_KEY']) config.api.apiKey = process.env['MARKETING_BRAIN_API_KEY'];
76
- }
77
-
78
- function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): void {
79
- for (const key of Object.keys(source)) {
80
- const val = source[key];
81
- if (val && typeof val === 'object' && !Array.isArray(val) && target[key] && typeof target[key] === 'object') {
82
- deepMerge(target[key] as Record<string, unknown>, val as Record<string, unknown>);
83
- } else if (val !== undefined) {
84
- target[key] = val;
85
- }
86
- }
87
- }
88
-
89
- export function loadConfig(configPath?: string): MarketingBrainConfig {
90
- const config = structuredClone(defaults);
91
-
92
- if (configPath) {
93
- const filePath = path.resolve(configPath);
94
- if (fs.existsSync(filePath)) {
95
- const raw = fs.readFileSync(filePath, 'utf-8');
96
- const fileConfig = JSON.parse(raw) as Partial<MarketingBrainConfig>;
97
- deepMerge(config as unknown as Record<string, unknown>, fileConfig as unknown as Record<string, unknown>);
98
- }
99
- } else {
100
- const defaultConfigPath = path.join(getDataDir(), 'config.json');
101
- if (fs.existsSync(defaultConfigPath)) {
102
- const raw = fs.readFileSync(defaultConfigPath, 'utf-8');
103
- const fileConfig = JSON.parse(raw) as Partial<MarketingBrainConfig>;
104
- deepMerge(config as unknown as Record<string, unknown>, fileConfig as unknown as Record<string, unknown>);
105
- }
106
- }
107
-
108
- applyEnvOverrides(config);
109
- return config;
110
- }
@@ -1,136 +0,0 @@
1
- import type { AnalyticsService } from '../services/analytics.service.js';
2
- import type { InsightService } from '../services/insight.service.js';
3
- import type { RuleService } from '../services/rule.service.js';
4
- import type { SynapseService } from '../services/synapse.service.js';
5
-
6
- function escapeHtml(str: string): string {
7
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
8
- }
9
-
10
- export interface DashboardServices {
11
- analytics: AnalyticsService;
12
- insight: InsightService;
13
- rule: RuleService;
14
- synapse: SynapseService;
15
- }
16
-
17
- export function renderDashboard(template: string, services: DashboardServices): string {
18
- let html = template;
19
-
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- const summary: any = services.analytics.getSummary();
22
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
- const dashData: any = services.analytics.getDashboardData();
24
- const insights = services.insight.listActive(200);
25
- const rules = services.rule.listRules();
26
- const strongest = services.synapse.getStrongest(50);
27
-
28
- const s = summary;
29
-
30
- // Stats
31
- html = html.replace('{{POSTS}}', String(s.posts?.total ?? 0));
32
- html = html.replace('{{CAMPAIGNS}}', String(s.campaigns?.total ?? 0));
33
- html = html.replace('{{STRATEGIES}}', String(s.strategies?.total ?? 0));
34
- html = html.replace('{{RULES}}', String(s.rules?.total ?? 0));
35
- html = html.replace('{{TEMPLATES}}', String(s.templates?.total ?? 0));
36
- html = html.replace('{{SYNAPSES}}', String(s.network?.synapses ?? 0));
37
-
38
- // Activity score
39
- const activity = Math.min(100, Math.round(
40
- ((s.posts?.total ?? 0) * 5 +
41
- (s.campaigns?.total ?? 0) * 10 +
42
- (s.strategies?.total ?? 0) * 3 +
43
- (s.rules?.active ?? 0) * 15 +
44
- (s.insights?.active ?? 0) * 5) / 2
45
- ));
46
- html = html.replace(/\{\{ACTIVITY\}\}/g, String(activity));
47
-
48
- // Version
49
- html = html.replace('{{VERSION}}', '0.1.0');
50
-
51
- // Platform chart
52
- const platforms = s.posts?.byPlatform ?? {};
53
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
- const maxCount = Math.max(1, ...Object.values(platforms as Record<string, number>));
55
- let platformHtml = '';
56
- for (const [platform, count] of Object.entries(platforms as Record<string, number>)) {
57
- const width = Math.round((count / maxCount) * 100);
58
- const barClass = `${platform}-bar`;
59
- platformHtml += `<div class="platform-row"><span class="platform-name">${escapeHtml(platform)}</span><div class="platform-bar-bg"><div class="platform-bar ${barClass}" data-width="${width}"></div></div><span class="platform-count">${count}</span></div>\n`;
60
- }
61
- if (!platformHtml) platformHtml = '<p class="empty">No posts tracked yet.</p>';
62
- html = html.replace('{{PLATFORM_CHART}}', platformHtml);
63
-
64
- // Top posts
65
- const topPosts = dashData.topPerformers?.topPosts ?? [];
66
- let postsHtml = '';
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- for (const post of topPosts.slice(0, 10) as any[]) {
69
- const score = (post.likes ?? 0) + (post.comments ?? 0) * 3 + (post.shares ?? 0) * 5 + (post.clicks ?? 0) * 2;
70
- const preview = escapeHtml((post.content ?? '').slice(0, 140));
71
- postsHtml += `<div class="post-card ${post.platform}"><div class="post-meta"><span class="post-platform ${post.platform}">${escapeHtml(post.platform)}</span><strong>${escapeHtml(post.format ?? 'text')}</strong></div><p>${preview}</p><div class="post-engagement"><span>&#10084; ${post.likes ?? 0}</span><span>&#128172; ${post.comments ?? 0}</span><span>&#128257; ${post.shares ?? 0}</span><span>Score: ${score}</span></div></div>\n`;
72
- }
73
- if (!postsHtml) postsHtml = '<p class="empty">No posts with engagement data yet.</p>';
74
- html = html.replace('{{TOP_POSTS}}', postsHtml);
75
-
76
- // Rules
77
- const rulesList = Array.isArray(rules) ? rules : [];
78
- let rulesHtml = '';
79
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
- for (const rule of rulesList as any[]) {
81
- const conf = Math.round((rule.confidence ?? 0) * 100);
82
- rulesHtml += `<div class="rule-card"><div class="rule-pattern">${escapeHtml(rule.pattern ?? '')}</div><div class="rule-recommendation">${escapeHtml(rule.recommendation ?? '')}</div><div class="rule-confidence"><span>Confidence:</span><div class="confidence-bar"><div class="confidence-fill" data-width="${conf}"></div></div><span>${conf}%</span></div></div>\n`;
83
- }
84
- if (!rulesHtml) rulesHtml = '<p class="empty">No rules learned yet. Post more content to discover patterns.</p>';
85
- html = html.replace('{{RULES_LIST}}', rulesHtml);
86
-
87
- // Insights by type
88
- const allInsights = Array.isArray(insights) ? insights : [];
89
- const insightsByType: Record<string, unknown[]> = {
90
- trend: [], gap: [], synergy: [], template: [], optimization: [],
91
- };
92
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
- for (const ins of allInsights as any[]) {
94
- const type = ins.type ?? 'optimization';
95
- if (insightsByType[type]) insightsByType[type]!.push(ins);
96
- else insightsByType.optimization!.push(ins);
97
- }
98
-
99
- const typeColors: Record<string, string> = {
100
- trend: 'cyan', gap: 'orange', synergy: 'green', template: 'purple', optimization: 'blue',
101
- };
102
-
103
- const pluralMap: Record<string, string> = {
104
- trend: 'TRENDS', gap: 'GAPS', synergy: 'SYNERGIES',
105
- template: 'TEMPLATES', optimization: 'OPTIMIZATIONS',
106
- };
107
-
108
- for (const [type, items] of Object.entries(insightsByType)) {
109
- const plural = pluralMap[type] ?? `${type.toUpperCase()}S`;
110
- html = html.replace(`{{${plural}_COUNT}}`, String(items.length));
111
-
112
- let insHtml = '';
113
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
- for (const ins of items as any[]) {
115
- const prio = ins.priority >= 70 ? 'high' : ins.priority >= 40 ? 'medium' : 'low';
116
- insHtml += `<div class="insight-card ${typeColors[type] ?? 'blue'}"><div class="insight-header"><span class="prio prio-${prio}">${ins.priority ?? 0}</span><strong>${escapeHtml(ins.title ?? '')}</strong></div><p>${escapeHtml(ins.description ?? '')}</p></div>\n`;
117
- }
118
- if (!insHtml) insHtml = '<p class="empty">No insights in this category yet.</p>';
119
-
120
- const placeholder = type === 'template' ? '{{TEMPLATES_INSIGHTS}}' : `{{${plural}}}`;
121
- html = html.replace(placeholder, insHtml);
122
- }
123
-
124
- // Graph edges
125
- const edges = Array.isArray(strongest) ? strongest : [];
126
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
- const graphEdges = edges.map((e: any) => ({
128
- s: `${e.source_type}:${e.source_id}`,
129
- t: `${e.target_type}:${e.target_id}`,
130
- type: e.synapse_type ?? 'related',
131
- w: e.weight ?? 0.5,
132
- }));
133
- html = html.replace('{{GRAPH_EDGES}}', JSON.stringify(graphEdges));
134
-
135
- return html;
136
- }
@@ -1,140 +0,0 @@
1
- import http from 'node:http';
2
- import { getEventBus } from '../utils/events.js';
3
- import { getLogger } from '../utils/logger.js';
4
-
5
- export interface DashboardServerOptions {
6
- port: number;
7
- getDashboardHtml: () => string;
8
- getStats: () => unknown;
9
- }
10
-
11
- export class DashboardServer {
12
- private server: http.Server | null = null;
13
- private clients: Set<http.ServerResponse> = new Set();
14
- private logger = getLogger();
15
-
16
- constructor(private options: DashboardServerOptions) {}
17
-
18
- start(): void {
19
- const { port, getDashboardHtml, getStats } = this.options;
20
- const bus = getEventBus();
21
-
22
- this.server = http.createServer((req, res) => {
23
- const url = new URL(req.url ?? '/', `http://localhost:${port}`);
24
-
25
- // CORS
26
- res.setHeader('Access-Control-Allow-Origin', '*');
27
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
28
-
29
- if (req.method === 'OPTIONS') {
30
- res.writeHead(204);
31
- res.end();
32
- return;
33
- }
34
-
35
- if (url.pathname === '/events') {
36
- // SSE endpoint
37
- res.writeHead(200, {
38
- 'Content-Type': 'text/event-stream',
39
- 'Cache-Control': 'no-cache',
40
- 'Connection': 'keep-alive',
41
- });
42
- res.write('data: {"type":"connected"}\n\n');
43
-
44
- this.clients.add(res);
45
- req.on('close', () => this.clients.delete(res));
46
- return;
47
- }
48
-
49
- if (url.pathname === '/api/stats') {
50
- const stats = getStats();
51
- res.writeHead(200, { 'Content-Type': 'application/json' });
52
- res.end(JSON.stringify(stats));
53
- return;
54
- }
55
-
56
- if (url.pathname === '/' || url.pathname === '/dashboard') {
57
- const html = getDashboardHtml();
58
- // Inject SSE script into the dashboard
59
- const sseScript = `
60
- <script>
61
- (function(){
62
- const evtSource = new EventSource('/events');
63
- evtSource.onmessage = function(e) {
64
- try {
65
- const data = JSON.parse(e.data);
66
- if (data.type === 'stats_update') {
67
- document.querySelectorAll('.stat-number').forEach(el => {
68
- const key = el.parentElement?.querySelector('.stat-label')?.textContent?.toLowerCase();
69
- if (key && data.stats[key] !== undefined) {
70
- el.textContent = Number(data.stats[key]).toLocaleString();
71
- }
72
- });
73
- }
74
- if (data.type === 'event') {
75
- const dot = document.querySelector('.activity-dot');
76
- if (dot) { dot.style.background = '#ff5577'; setTimeout(() => dot.style.background = '', 500); }
77
- }
78
- } catch {}
79
- };
80
- evtSource.onerror = function() { setTimeout(() => location.reload(), 5000); };
81
- })();
82
- </script>`;
83
- const liveHtml = html.replace('</body>', sseScript + '</body>');
84
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
85
- res.end(liveHtml);
86
- return;
87
- }
88
-
89
- res.writeHead(404, { 'Content-Type': 'text/plain' });
90
- res.end('Not Found');
91
- });
92
-
93
- // Forward Marketing Brain events to SSE clients
94
- const eventNames = [
95
- 'post:created', 'post:published', 'engagement:updated',
96
- 'strategy:reported', 'rule:learned', 'rule:triggered',
97
- 'template:created', 'campaign:created',
98
- 'insight:created', 'synapse:created', 'synapse:strengthened',
99
- ] as const;
100
-
101
- for (const eventName of eventNames) {
102
- bus.on(eventName, (data: unknown) => {
103
- this.broadcast({ type: 'event', event: eventName, data });
104
- });
105
- }
106
-
107
- // Periodic stats broadcast (every 30s)
108
- setInterval(() => {
109
- if (this.clients.size > 0) {
110
- const stats = getStats();
111
- this.broadcast({ type: 'stats_update', stats });
112
- }
113
- }, 30_000);
114
-
115
- this.server.listen(port, () => {
116
- this.logger.info(`Dashboard server started on http://localhost:${port}`);
117
- });
118
- }
119
-
120
- stop(): void {
121
- for (const client of this.clients) {
122
- client.end();
123
- }
124
- this.clients.clear();
125
- this.server?.close();
126
- this.server = null;
127
- this.logger.info('Dashboard server stopped');
128
- }
129
-
130
- private broadcast(data: unknown): void {
131
- const msg = `data: ${JSON.stringify(data)}\n\n`;
132
- for (const client of this.clients) {
133
- try {
134
- client.write(msg);
135
- } catch {
136
- this.clients.delete(client);
137
- }
138
- }
139
- }
140
- }
@@ -1,22 +0,0 @@
1
- import Database from 'better-sqlite3';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { getLogger } from '../utils/logger.js';
5
-
6
- export function createConnection(dbPath: string): Database.Database {
7
- const logger = getLogger();
8
- const dir = path.dirname(dbPath);
9
- if (!fs.existsSync(dir)) {
10
- fs.mkdirSync(dir, { recursive: true });
11
- }
12
-
13
- logger.info(`Opening database at ${dbPath}`);
14
- const db = new Database(dbPath);
15
-
16
- db.pragma('journal_mode = WAL');
17
- db.pragma('synchronous = NORMAL');
18
- db.pragma('cache_size = 10000');
19
- db.pragma('foreign_keys = ON');
20
-
21
- return db;
22
- }
@@ -1,63 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
-
3
- export function up(db: Database.Database): void {
4
- db.exec(`
5
- CREATE TABLE IF NOT EXISTS campaigns (
6
- id INTEGER PRIMARY KEY AUTOINCREMENT,
7
- name TEXT NOT NULL UNIQUE,
8
- brand TEXT,
9
- goal TEXT,
10
- platform TEXT,
11
- status TEXT NOT NULL DEFAULT 'active',
12
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
13
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
14
- );
15
-
16
- CREATE TABLE IF NOT EXISTS posts (
17
- id INTEGER PRIMARY KEY AUTOINCREMENT,
18
- campaign_id INTEGER,
19
- platform TEXT NOT NULL,
20
- content TEXT NOT NULL,
21
- format TEXT NOT NULL DEFAULT 'text',
22
- hashtags TEXT,
23
- url TEXT,
24
- published_at TEXT,
25
- fingerprint TEXT NOT NULL,
26
- status TEXT NOT NULL DEFAULT 'draft',
27
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
28
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
29
- FOREIGN KEY (campaign_id) REFERENCES campaigns(id) ON DELETE SET NULL
30
- );
31
-
32
- CREATE TABLE IF NOT EXISTS engagement (
33
- id INTEGER PRIMARY KEY AUTOINCREMENT,
34
- post_id INTEGER NOT NULL,
35
- timestamp TEXT NOT NULL DEFAULT (datetime('now')),
36
- likes INTEGER NOT NULL DEFAULT 0,
37
- comments INTEGER NOT NULL DEFAULT 0,
38
- shares INTEGER NOT NULL DEFAULT 0,
39
- impressions INTEGER NOT NULL DEFAULT 0,
40
- clicks INTEGER NOT NULL DEFAULT 0,
41
- saves INTEGER NOT NULL DEFAULT 0,
42
- reach INTEGER NOT NULL DEFAULT 0,
43
- FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
44
- );
45
-
46
- CREATE TABLE IF NOT EXISTS audiences (
47
- id INTEGER PRIMARY KEY AUTOINCREMENT,
48
- name TEXT NOT NULL UNIQUE,
49
- platform TEXT,
50
- demographics TEXT,
51
- interests TEXT,
52
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
53
- );
54
-
55
- CREATE INDEX IF NOT EXISTS idx_posts_campaign ON posts(campaign_id);
56
- CREATE INDEX IF NOT EXISTS idx_posts_platform ON posts(platform);
57
- CREATE INDEX IF NOT EXISTS idx_posts_fingerprint ON posts(fingerprint);
58
- CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status);
59
- CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published_at);
60
- CREATE INDEX IF NOT EXISTS idx_engagement_post ON engagement(post_id);
61
- CREATE INDEX IF NOT EXISTS idx_engagement_timestamp ON engagement(timestamp);
62
- `);
63
- }
@@ -1,46 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
-
3
- export function up(db: Database.Database): void {
4
- db.exec(`
5
- CREATE TABLE IF NOT EXISTS strategies (
6
- id INTEGER PRIMARY KEY AUTOINCREMENT,
7
- post_id INTEGER,
8
- description TEXT NOT NULL,
9
- approach TEXT,
10
- outcome TEXT,
11
- confidence REAL NOT NULL DEFAULT 0.5,
12
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
13
- FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE SET NULL
14
- );
15
-
16
- CREATE TABLE IF NOT EXISTS marketing_rules (
17
- id INTEGER PRIMARY KEY AUTOINCREMENT,
18
- pattern TEXT NOT NULL,
19
- recommendation TEXT NOT NULL,
20
- confidence REAL NOT NULL DEFAULT 0.5,
21
- trigger_count INTEGER NOT NULL DEFAULT 0,
22
- success_count INTEGER NOT NULL DEFAULT 0,
23
- active INTEGER NOT NULL DEFAULT 1,
24
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
25
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
26
- );
27
-
28
- CREATE TABLE IF NOT EXISTS content_templates (
29
- id INTEGER PRIMARY KEY AUTOINCREMENT,
30
- name TEXT NOT NULL,
31
- structure TEXT NOT NULL,
32
- example TEXT,
33
- platform TEXT,
34
- avg_engagement REAL NOT NULL DEFAULT 0,
35
- use_count INTEGER NOT NULL DEFAULT 0,
36
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
37
- );
38
-
39
- CREATE INDEX IF NOT EXISTS idx_strategies_post ON strategies(post_id);
40
- CREATE INDEX IF NOT EXISTS idx_strategies_confidence ON strategies(confidence);
41
- CREATE INDEX IF NOT EXISTS idx_rules_active ON marketing_rules(active);
42
- CREATE INDEX IF NOT EXISTS idx_rules_confidence ON marketing_rules(confidence);
43
- CREATE INDEX IF NOT EXISTS idx_templates_platform ON content_templates(platform);
44
- CREATE INDEX IF NOT EXISTS idx_templates_engagement ON content_templates(avg_engagement);
45
- `);
46
- }