@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.
- package/.mcp.json +9 -0
- package/README.md +342 -0
- package/dashboard.html +666 -0
- package/dist/api/server.d.ts +15 -0
- package/dist/api/server.js +73 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli/colors.d.ts +43 -0
- package/dist/cli/colors.js +54 -0
- package/dist/cli/colors.js.map +1 -0
- package/dist/cli/commands/campaign.d.ts +2 -0
- package/dist/cli/commands/campaign.js +62 -0
- package/dist/cli/commands/campaign.js.map +1 -0
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +164 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/dashboard.d.ts +2 -0
- package/dist/cli/commands/dashboard.js +147 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +111 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts +2 -0
- package/dist/cli/commands/export.js +37 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/import.d.ts +2 -0
- package/dist/cli/commands/import.js +76 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/insights.d.ts +2 -0
- package/dist/cli/commands/insights.js +41 -0
- package/dist/cli/commands/insights.js.map +1 -0
- package/dist/cli/commands/learn.d.ts +2 -0
- package/dist/cli/commands/learn.js +22 -0
- package/dist/cli/commands/learn.js.map +1 -0
- package/dist/cli/commands/network.d.ts +2 -0
- package/dist/cli/commands/network.js +66 -0
- package/dist/cli/commands/network.js.map +1 -0
- package/dist/cli/commands/post.d.ts +2 -0
- package/dist/cli/commands/post.js +45 -0
- package/dist/cli/commands/post.js.map +1 -0
- package/dist/cli/commands/query.d.ts +2 -0
- package/dist/cli/commands/query.js +96 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +2 -0
- package/dist/cli/commands/rules.js +25 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.js +91 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +63 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +2 -0
- package/dist/cli/commands/stop.js +34 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/suggest.d.ts +2 -0
- package/dist/cli/commands/suggest.js +57 -0
- package/dist/cli/commands/suggest.js.map +1 -0
- package/dist/cli/ipc-helper.d.ts +2 -0
- package/dist/cli/ipc-helper.js +26 -0
- package/dist/cli/ipc-helper.js.map +1 -0
- package/dist/cli/update-check.d.ts +2 -0
- package/dist/cli/update-check.js +58 -0
- package/dist/cli/update-check.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +111 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/renderer.d.ts +11 -0
- package/dist/dashboard/renderer.js +112 -0
- package/dist/dashboard/renderer.js.map +1 -0
- package/dist/dashboard/server.d.ts +15 -0
- package/dist/dashboard/server.js +122 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +19 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrations/001_core_schema.d.ts +2 -0
- package/dist/db/migrations/001_core_schema.js +62 -0
- package/dist/db/migrations/001_core_schema.js.map +1 -0
- package/dist/db/migrations/002_learning_schema.d.ts +2 -0
- package/dist/db/migrations/002_learning_schema.js +45 -0
- package/dist/db/migrations/002_learning_schema.js.map +1 -0
- package/dist/db/migrations/003_synapse_schema.d.ts +2 -0
- package/dist/db/migrations/003_synapse_schema.js +26 -0
- package/dist/db/migrations/003_synapse_schema.js.map +1 -0
- package/dist/db/migrations/004_insights_schema.d.ts +2 -0
- package/dist/db/migrations/004_insights_schema.js +37 -0
- package/dist/db/migrations/004_insights_schema.js.map +1 -0
- package/dist/db/migrations/005_fts_indexes.d.ts +2 -0
- package/dist/db/migrations/005_fts_indexes.js +76 -0
- package/dist/db/migrations/005_fts_indexes.js.map +1 -0
- package/dist/db/migrations/index.d.ts +2 -0
- package/dist/db/migrations/index.js +47 -0
- package/dist/db/migrations/index.js.map +1 -0
- package/dist/db/repositories/audience.repository.d.ts +18 -0
- package/dist/db/repositories/audience.repository.js +45 -0
- package/dist/db/repositories/audience.repository.js.map +1 -0
- package/dist/db/repositories/campaign.repository.d.ts +15 -0
- package/dist/db/repositories/campaign.repository.js +58 -0
- package/dist/db/repositories/campaign.repository.js.map +1 -0
- package/dist/db/repositories/engagement.repository.d.ts +26 -0
- package/dist/db/repositories/engagement.repository.js +83 -0
- package/dist/db/repositories/engagement.repository.js.map +1 -0
- package/dist/db/repositories/insight.repository.d.ts +18 -0
- package/dist/db/repositories/insight.repository.js +87 -0
- package/dist/db/repositories/insight.repository.js.map +1 -0
- package/dist/db/repositories/post.repository.d.ts +21 -0
- package/dist/db/repositories/post.repository.js +105 -0
- package/dist/db/repositories/post.repository.js.map +1 -0
- package/dist/db/repositories/rule.repository.d.ts +16 -0
- package/dist/db/repositories/rule.repository.js +71 -0
- package/dist/db/repositories/rule.repository.js.map +1 -0
- package/dist/db/repositories/strategy.repository.d.ts +16 -0
- package/dist/db/repositories/strategy.repository.js +69 -0
- package/dist/db/repositories/strategy.repository.js.map +1 -0
- package/dist/db/repositories/synapse.repository.d.ts +25 -0
- package/dist/db/repositories/synapse.repository.js +115 -0
- package/dist/db/repositories/synapse.repository.js.map +1 -0
- package/dist/db/repositories/template.repository.d.ts +16 -0
- package/dist/db/repositories/template.repository.js +61 -0
- package/dist/db/repositories/template.repository.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/client.d.ts +13 -0
- package/dist/ipc/client.js +93 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/protocol.d.ts +8 -0
- package/dist/ipc/protocol.js +29 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/router.d.ts +30 -0
- package/dist/ipc/router.js +88 -0
- package/dist/ipc/router.js.map +1 -0
- package/dist/ipc/server.d.ts +14 -0
- package/dist/ipc/server.js +130 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/learning/confidence-scorer.d.ts +17 -0
- package/dist/learning/confidence-scorer.js +26 -0
- package/dist/learning/confidence-scorer.js.map +1 -0
- package/dist/learning/learning-engine.d.ts +33 -0
- package/dist/learning/learning-engine.js +211 -0
- package/dist/learning/learning-engine.js.map +1 -0
- package/dist/marketing-core.d.ts +17 -0
- package/dist/marketing-core.js +233 -0
- package/dist/marketing-core.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +67 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +3 -0
- package/dist/mcp/tools.js +138 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/research/research-engine.d.ts +28 -0
- package/dist/research/research-engine.js +211 -0
- package/dist/research/research-engine.js.map +1 -0
- package/dist/services/analytics.service.d.ts +116 -0
- package/dist/services/analytics.service.js +69 -0
- package/dist/services/analytics.service.js.map +1 -0
- package/dist/services/audience.service.d.ts +20 -0
- package/dist/services/audience.service.js +30 -0
- package/dist/services/audience.service.js.map +1 -0
- package/dist/services/campaign.service.d.ts +27 -0
- package/dist/services/campaign.service.js +65 -0
- package/dist/services/campaign.service.js.map +1 -0
- package/dist/services/insight.service.d.ts +18 -0
- package/dist/services/insight.service.js +40 -0
- package/dist/services/insight.service.js.map +1 -0
- package/dist/services/post.service.d.ts +48 -0
- package/dist/services/post.service.js +93 -0
- package/dist/services/post.service.js.map +1 -0
- package/dist/services/rule.service.d.ts +29 -0
- package/dist/services/rule.service.js +67 -0
- package/dist/services/rule.service.js.map +1 -0
- package/dist/services/strategy.service.d.ts +17 -0
- package/dist/services/strategy.service.js +39 -0
- package/dist/services/strategy.service.js.map +1 -0
- package/dist/services/synapse.service.d.ts +22 -0
- package/dist/services/synapse.service.js +22 -0
- package/dist/services/synapse.service.js.map +1 -0
- package/dist/services/template.service.d.ts +17 -0
- package/dist/services/template.service.js +37 -0
- package/dist/services/template.service.js.map +1 -0
- package/dist/synapses/activation.d.ts +13 -0
- package/dist/synapses/activation.js +50 -0
- package/dist/synapses/activation.js.map +1 -0
- package/dist/synapses/decay.d.ts +11 -0
- package/dist/synapses/decay.js +27 -0
- package/dist/synapses/decay.js.map +1 -0
- package/dist/synapses/hebbian.d.ts +13 -0
- package/dist/synapses/hebbian.js +35 -0
- package/dist/synapses/hebbian.js.map +1 -0
- package/dist/synapses/pathfinder.d.ts +14 -0
- package/dist/synapses/pathfinder.js +50 -0
- package/dist/synapses/pathfinder.js.map +1 -0
- package/dist/synapses/synapse-manager.d.ts +32 -0
- package/dist/synapses/synapse-manager.js +76 -0
- package/dist/synapses/synapse-manager.js.map +1 -0
- package/dist/types/config.types.d.ts +69 -0
- package/dist/types/config.types.js +2 -0
- package/dist/types/config.types.js.map +1 -0
- package/dist/types/ipc.types.d.ts +11 -0
- package/dist/types/ipc.types.js +2 -0
- package/dist/types/ipc.types.js.map +1 -0
- package/dist/types/post.types.d.ts +141 -0
- package/dist/types/post.types.js +2 -0
- package/dist/types/post.types.js.map +1 -0
- package/dist/types/synapse.types.d.ts +23 -0
- package/dist/types/synapse.types.js +2 -0
- package/dist/types/synapse.types.js.map +1 -0
- package/dist/utils/events.d.ts +57 -0
- package/dist/utils/events.js +23 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +5 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +39 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +3 -0
- package/dist/utils/paths.js +18 -0
- package/dist/utils/paths.js.map +1 -0
- package/package.json +40 -0
- package/seed-data.json +78 -0
- package/src/api/server.ts +86 -0
- package/src/cli/colors.ts +59 -0
- package/src/cli/commands/campaign.ts +66 -0
- package/src/cli/commands/config.ts +168 -0
- package/src/cli/commands/dashboard.ts +165 -0
- package/src/cli/commands/doctor.ts +110 -0
- package/src/cli/commands/export.ts +40 -0
- package/src/cli/commands/import.ts +84 -0
- package/src/cli/commands/insights.ts +44 -0
- package/src/cli/commands/learn.ts +24 -0
- package/src/cli/commands/network.ts +71 -0
- package/src/cli/commands/post.ts +47 -0
- package/src/cli/commands/query.ts +108 -0
- package/src/cli/commands/rules.ts +27 -0
- package/src/cli/commands/start.ts +100 -0
- package/src/cli/commands/status.ts +73 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/commands/suggest.ts +64 -0
- package/src/cli/ipc-helper.ts +22 -0
- package/src/cli/update-check.ts +63 -0
- package/src/config.ts +110 -0
- package/src/dashboard/renderer.ts +136 -0
- package/src/dashboard/server.ts +140 -0
- package/src/db/connection.ts +22 -0
- package/src/db/migrations/001_core_schema.ts +63 -0
- package/src/db/migrations/002_learning_schema.ts +46 -0
- package/src/db/migrations/003_synapse_schema.ts +27 -0
- package/src/db/migrations/004_insights_schema.ts +38 -0
- package/src/db/migrations/005_fts_indexes.ts +77 -0
- package/src/db/migrations/index.ts +62 -0
- package/src/db/repositories/audience.repository.ts +53 -0
- package/src/db/repositories/campaign.repository.ts +72 -0
- package/src/db/repositories/engagement.repository.ts +108 -0
- package/src/db/repositories/insight.repository.ts +100 -0
- package/src/db/repositories/post.repository.ts +123 -0
- package/src/db/repositories/rule.repository.ts +87 -0
- package/src/db/repositories/strategy.repository.ts +82 -0
- package/src/db/repositories/synapse.repository.ts +148 -0
- package/src/db/repositories/template.repository.ts +76 -0
- package/src/index.ts +69 -0
- package/src/ipc/client.ts +110 -0
- package/src/ipc/protocol.ts +35 -0
- package/src/ipc/router.ts +126 -0
- package/src/ipc/server.ts +140 -0
- package/src/learning/confidence-scorer.ts +36 -0
- package/src/learning/learning-engine.ts +254 -0
- package/src/marketing-core.ts +285 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/tools.ts +216 -0
- package/src/research/research-engine.ts +226 -0
- package/src/services/analytics.service.ts +73 -0
- package/src/services/audience.service.ts +40 -0
- package/src/services/campaign.service.ts +80 -0
- package/src/services/insight.service.ts +54 -0
- package/src/services/post.service.ts +116 -0
- package/src/services/rule.service.ts +90 -0
- package/src/services/strategy.service.ts +53 -0
- package/src/services/synapse.service.ts +32 -0
- package/src/services/template.service.ts +50 -0
- package/src/synapses/activation.ts +80 -0
- package/src/synapses/decay.ts +38 -0
- package/src/synapses/hebbian.ts +68 -0
- package/src/synapses/pathfinder.ts +81 -0
- package/src/synapses/synapse-manager.ts +115 -0
- package/src/types/config.types.ts +79 -0
- package/src/types/ipc.types.ts +8 -0
- package/src/types/post.types.ts +156 -0
- package/src/types/synapse.types.ts +43 -0
- package/src/utils/events.ts +44 -0
- package/src/utils/hash.ts +5 -0
- package/src/utils/logger.ts +48 -0
- package/src/utils/paths.ts +19 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getDataDir } from '../../utils/paths.js';
|
|
5
|
+
import { withIpc } from '../ipc-helper.js';
|
|
6
|
+
import { c, icons, header, keyValue, divider } from '../colors.js';
|
|
7
|
+
|
|
8
|
+
export function statusCommand(): Command {
|
|
9
|
+
return new Command('status')
|
|
10
|
+
.description('Show Marketing Brain daemon status')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const pidPath = path.join(getDataDir(), 'marketing-brain.pid');
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(pidPath)) {
|
|
15
|
+
console.log(`${icons.megaphone} Marketing Brain Daemon: ${c.red.bold('NOT RUNNING')}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
20
|
+
let running = false;
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
running = true;
|
|
24
|
+
} catch { /* not running */ }
|
|
25
|
+
|
|
26
|
+
if (!running) {
|
|
27
|
+
console.log(`${icons.megaphone} Marketing Brain Daemon: ${c.red.bold('NOT RUNNING')} ${c.dim('(stale PID file)')}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(header('Marketing Brain Status', icons.megaphone));
|
|
32
|
+
console.log(` ${c.green(`${icons.dot} RUNNING`)} ${c.dim(`(PID ${pid})`)}`);
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
await withIpc(async (client) => {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const summary: any = await client.request('analytics.summary', {});
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const network: any = await client.request('synapse.stats', {});
|
|
40
|
+
|
|
41
|
+
const dbPath = path.join(getDataDir(), 'marketing-brain.db');
|
|
42
|
+
let dbSize = '?';
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.statSync(dbPath);
|
|
45
|
+
dbSize = `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
|
|
46
|
+
} catch { /* ignore */ }
|
|
47
|
+
|
|
48
|
+
console.log(keyValue('Database', `${dbPath} (${dbSize})`));
|
|
49
|
+
console.log();
|
|
50
|
+
|
|
51
|
+
console.log(` ${icons.post} ${c.purple.bold('Content')}`);
|
|
52
|
+
console.log(` ${c.label('Posts:')} ${c.value(summary.posts?.total ?? 0)} total`);
|
|
53
|
+
console.log(` ${c.label('Campaigns:')} ${c.value(summary.campaigns?.total ?? 0)}`);
|
|
54
|
+
console.log(` ${c.label('Templates:')} ${c.value(summary.templates?.total ?? 0)}`);
|
|
55
|
+
console.log();
|
|
56
|
+
|
|
57
|
+
console.log(` ${icons.rule} ${c.blue.bold('Learning')}`);
|
|
58
|
+
console.log(` ${c.label('Strategies:')} ${c.value(summary.strategies?.total ?? 0)}`);
|
|
59
|
+
console.log(` ${c.label('Rules:')} ${c.green(summary.rules?.active ?? 0)} active`);
|
|
60
|
+
console.log();
|
|
61
|
+
|
|
62
|
+
console.log(` ${icons.synapse} ${c.cyan.bold('Synapse Network')}`);
|
|
63
|
+
console.log(` ${c.label('Synapses:')} ${c.value(network.totalSynapses ?? 0)}`);
|
|
64
|
+
console.log(` ${c.label('Avg weight:')} ${c.value((network.avgWeight ?? 0).toFixed(2))}`);
|
|
65
|
+
console.log();
|
|
66
|
+
|
|
67
|
+
console.log(` ${icons.insight} ${c.orange.bold('Research')}`);
|
|
68
|
+
console.log(` ${c.label('Insights:')} ${c.value(summary.insights?.active ?? 0)} active`);
|
|
69
|
+
|
|
70
|
+
console.log(`\n${divider()}`);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getDataDir } from '../../utils/paths.js';
|
|
5
|
+
import { c, icons } from '../colors.js';
|
|
6
|
+
|
|
7
|
+
export function stopCommand(): Command {
|
|
8
|
+
return new Command('stop')
|
|
9
|
+
.description('Stop the Marketing Brain daemon')
|
|
10
|
+
.action(() => {
|
|
11
|
+
const pidPath = path.join(getDataDir(), 'marketing-brain.pid');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(pidPath)) {
|
|
14
|
+
console.log(`${icons.megaphone} ${c.dim('Marketing Brain daemon is not running (no PID file found).')}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 'SIGTERM');
|
|
22
|
+
console.log(`${icons.megaphone} ${c.success('Marketing Brain daemon stopped')} ${c.dim(`(PID: ${pid})`)}`);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
|
|
25
|
+
console.log(`${icons.megaphone} ${c.dim('Marketing Brain daemon was not running (stale PID file removed).')}`);
|
|
26
|
+
} else {
|
|
27
|
+
console.error(`${icons.error} ${c.error(`Failed to stop daemon: ${err}`)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try { fs.unlinkSync(pidPath); } catch { /* ignore */ }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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: 7780,
|
|
15
|
+
enabled: true,
|
|
16
|
+
},
|
|
17
|
+
mcpHttp: {
|
|
18
|
+
port: 7781,
|
|
19
|
+
enabled: true,
|
|
20
|
+
},
|
|
21
|
+
dashboard: {
|
|
22
|
+
port: 7782,
|
|
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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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>❤ ${post.likes ?? 0}</span><span>💬 ${post.comments ?? 0}</span><span>🔁 ${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
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|