@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
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@timmeck/marketing-brain",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Self-learning marketing intelligence system with Hebbian synapse network",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"marketing": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"test": "vitest"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"marketing-intelligence",
|
|
17
|
+
"content-strategy",
|
|
18
|
+
"mcp",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"hebbian-learning",
|
|
21
|
+
"synapse-network",
|
|
22
|
+
"engagement-tracking"
|
|
23
|
+
],
|
|
24
|
+
"author": "Tim Mecklenburg",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"better-sqlite3": "^11.7.0",
|
|
29
|
+
"chalk": "^5.6.2",
|
|
30
|
+
"commander": "^13.0.0",
|
|
31
|
+
"winston": "^3.17.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"tsx": "^4.19.0",
|
|
37
|
+
"typescript": "^5.7.0",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/seed-data.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"posts": [
|
|
3
|
+
{
|
|
4
|
+
"platform": "reddit",
|
|
5
|
+
"content": "I built \"Brain\" — a persistent error memory system for Claude Code that learns from every bug you fix (open source, MCP). Every time you start a new Claude Code session, it forgets everything. So I built Brain — an MCP server that gives Claude Code persistent memory with Hebbian synapse network, hybrid search, and adaptive learning.",
|
|
6
|
+
"format": "article",
|
|
7
|
+
"url": "https://www.reddit.com/r/ClaudeAI/",
|
|
8
|
+
"hashtags": "ClaudeAI,MCP,OpenSource,Brain",
|
|
9
|
+
"campaign": "Brain Launch",
|
|
10
|
+
"brand": "Brain",
|
|
11
|
+
"status": "draft",
|
|
12
|
+
"published_at": "2026-02-20T12:00:00Z",
|
|
13
|
+
"strategy": {
|
|
14
|
+
"description": "Long-form technical post on r/ClaudeAI showing real numbers and architecture",
|
|
15
|
+
"approach": "Problem → Solution → Architecture → Real stats → Call to action",
|
|
16
|
+
"outcome": "Targets Claude Code power users who experience session amnesia"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"platform": "reddit",
|
|
21
|
+
"content": "I built a persistent memory system for Claude Code — it remembers every error, solution, and code module across all your projects. Claude Code starts fresh every session. After months of hitting the same errors, I built Brain — an MCP server with Hebbian learning. 18,160 modules, 37,277 synapses, 4,902 insights.",
|
|
22
|
+
"format": "article",
|
|
23
|
+
"url": "https://www.reddit.com/r/ClaudeAI/",
|
|
24
|
+
"hashtags": "ClaudeAI,MCP,OpenSource",
|
|
25
|
+
"campaign": "Brain Launch",
|
|
26
|
+
"brand": "Brain",
|
|
27
|
+
"status": "draft",
|
|
28
|
+
"published_at": "2026-02-20T12:00:00Z",
|
|
29
|
+
"strategy": {
|
|
30
|
+
"description": "Concise Reddit pitch focusing on key numbers and pain point",
|
|
31
|
+
"approach": "Short hook → Real numbers → Feature list → GitHub link",
|
|
32
|
+
"outcome": "Quick scan-friendly format for Reddit scrollers"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"platform": "x",
|
|
37
|
+
"content": "Just shipped Brain v1.8 — a persistent error memory system for Claude Code. It remembers every bug you fix across all sessions using Hebbian synapse networks. 18K+ modules tracked, 37K+ synapses, 5K insights. Open source + MCP. github.com/timmeck/brain",
|
|
38
|
+
"format": "text",
|
|
39
|
+
"hashtags": "#ClaudeCode,#MCP,#OpenSource,#AI,#DevTools",
|
|
40
|
+
"campaign": "Brain Launch",
|
|
41
|
+
"brand": "Brain",
|
|
42
|
+
"status": "draft",
|
|
43
|
+
"strategy": {
|
|
44
|
+
"description": "X announcement tweet with key stats",
|
|
45
|
+
"approach": "Ship announcement + impressive numbers + GitHub link",
|
|
46
|
+
"outcome": "DevTools audience on X"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"platform": "reddit",
|
|
51
|
+
"content": "REPOSIGNAL — Real-time GitHub signal intelligence. Tracks 1000+ repos, AI-powered scoring, trending detection, auto-posting to X. Built with Python/FastAPI, deployed on Railway.",
|
|
52
|
+
"format": "article",
|
|
53
|
+
"hashtags": "GitHub,OpenSource,AI,DevTools",
|
|
54
|
+
"campaign": "REPOSIGNAL Promotion",
|
|
55
|
+
"brand": "REPOSIGNAL",
|
|
56
|
+
"status": "draft",
|
|
57
|
+
"strategy": {
|
|
58
|
+
"description": "REPOSIGNAL product announcement on Reddit",
|
|
59
|
+
"approach": "Show real-time intelligence angle, emphasize automation",
|
|
60
|
+
"outcome": "Target GitHub power users and open source enthusiasts"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"platform": "x",
|
|
65
|
+
"content": "Built Marketing Brain — a self-learning system that tracks every post, learns what works, and suggests strategies. Hebbian synapse network connecting posts, campaigns, and insights. Same architecture as Brain, applied to marketing.",
|
|
66
|
+
"format": "text",
|
|
67
|
+
"hashtags": "#MarketingBrain,#AI,#ContentStrategy,#DevTools",
|
|
68
|
+
"campaign": "Marketing Brain Launch",
|
|
69
|
+
"brand": "Marketing Brain",
|
|
70
|
+
"status": "draft",
|
|
71
|
+
"strategy": {
|
|
72
|
+
"description": "Meta-marketing: announce the marketing tool itself",
|
|
73
|
+
"approach": "Self-referential hook — the tool tracks its own launch",
|
|
74
|
+
"outcome": "Developer audience interested in AI-powered content tools"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
import type { IpcRouter } from '../ipc/router.js';
|
|
4
|
+
|
|
5
|
+
interface ApiServerOptions {
|
|
6
|
+
port: number;
|
|
7
|
+
router: IpcRouter;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ApiServer {
|
|
12
|
+
private server: http.Server | null = null;
|
|
13
|
+
private logger = getLogger();
|
|
14
|
+
|
|
15
|
+
constructor(private opts: ApiServerOptions) {}
|
|
16
|
+
|
|
17
|
+
start(): void {
|
|
18
|
+
this.server = http.createServer((req, res) => {
|
|
19
|
+
// CORS
|
|
20
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
21
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
22
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
23
|
+
|
|
24
|
+
if (req.method === 'OPTIONS') {
|
|
25
|
+
res.writeHead(204);
|
|
26
|
+
res.end();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auth check
|
|
31
|
+
if (this.opts.apiKey) {
|
|
32
|
+
const auth = req.headers.authorization;
|
|
33
|
+
if (!auth || auth !== `Bearer ${this.opts.apiKey}`) {
|
|
34
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
35
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Health check
|
|
41
|
+
if (req.url === '/api/v1/health') {
|
|
42
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify({ status: 'ok', service: 'marketing-brain' }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Methods list
|
|
48
|
+
if (req.url === '/api/v1/methods') {
|
|
49
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
50
|
+
res.end(JSON.stringify({ methods: this.opts.router.listMethods() }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// RPC endpoint
|
|
55
|
+
if (req.url === '/api/v1/rpc' && req.method === 'POST') {
|
|
56
|
+
let body = '';
|
|
57
|
+
req.on('data', chunk => { body += chunk; });
|
|
58
|
+
req.on('end', () => {
|
|
59
|
+
try {
|
|
60
|
+
const { method, params } = JSON.parse(body);
|
|
61
|
+
const result = this.opts.router.handle(method, params);
|
|
62
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
63
|
+
res.end(JSON.stringify({ result }));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
66
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
73
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.server.listen(this.opts.port, () => {
|
|
77
|
+
this.logger.info(`API server listening on port ${this.opts.port}`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
stop(): void {
|
|
82
|
+
this.server?.close();
|
|
83
|
+
this.server = null;
|
|
84
|
+
this.logger.info('API server stopped');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const c = {
|
|
4
|
+
blue: chalk.hex('#5b9cff'),
|
|
5
|
+
purple: chalk.hex('#b47aff'),
|
|
6
|
+
cyan: chalk.hex('#47e5ff'),
|
|
7
|
+
green: chalk.hex('#3dffa0'),
|
|
8
|
+
red: chalk.hex('#ff5577'),
|
|
9
|
+
orange: chalk.hex('#ffb347'),
|
|
10
|
+
dim: chalk.hex('#8b8fb0'),
|
|
11
|
+
dimmer: chalk.hex('#4a4d6e'),
|
|
12
|
+
|
|
13
|
+
label: chalk.hex('#8b8fb0'),
|
|
14
|
+
value: chalk.white.bold,
|
|
15
|
+
heading: chalk.hex('#5b9cff').bold,
|
|
16
|
+
success: chalk.hex('#3dffa0').bold,
|
|
17
|
+
error: chalk.hex('#ff5577').bold,
|
|
18
|
+
warn: chalk.hex('#ffb347').bold,
|
|
19
|
+
info: chalk.hex('#47e5ff'),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const icons = {
|
|
23
|
+
megaphone: '📣',
|
|
24
|
+
check: '✓',
|
|
25
|
+
cross: '✗',
|
|
26
|
+
arrow: '→',
|
|
27
|
+
dot: '●',
|
|
28
|
+
bar: '█',
|
|
29
|
+
barLight: '░',
|
|
30
|
+
dash: '─',
|
|
31
|
+
star: '★',
|
|
32
|
+
bolt: '⚡',
|
|
33
|
+
chart: '📊',
|
|
34
|
+
post: '📝',
|
|
35
|
+
campaign: '🎯',
|
|
36
|
+
synapse: '🔗',
|
|
37
|
+
insight: '💡',
|
|
38
|
+
rule: '📏',
|
|
39
|
+
template: '📋',
|
|
40
|
+
warn: '⚠',
|
|
41
|
+
error: '❌',
|
|
42
|
+
ok: '✅',
|
|
43
|
+
clock: '⏱',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function header(title: string, icon?: string): string {
|
|
47
|
+
const prefix = icon ? `${icon} ` : '';
|
|
48
|
+
const line = c.dimmer(icons.dash.repeat(40));
|
|
49
|
+
return `\n${line}\n${prefix}${c.heading(title)}\n${line}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function keyValue(key: string, value: string | number, indent = 2): string {
|
|
53
|
+
const pad = ' '.repeat(indent);
|
|
54
|
+
return `${pad}${c.label(key + ':')} ${c.value(String(value))}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function divider(width = 40): string {
|
|
58
|
+
return c.dimmer(icons.dash.repeat(width));
|
|
59
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { withIpc } from '../ipc-helper.js';
|
|
3
|
+
import { c, icons } from '../colors.js';
|
|
4
|
+
|
|
5
|
+
export function campaignCommand(): Command {
|
|
6
|
+
const cmd = new Command('campaign')
|
|
7
|
+
.description('Manage campaigns');
|
|
8
|
+
|
|
9
|
+
cmd.command('create')
|
|
10
|
+
.description('Create a new campaign')
|
|
11
|
+
.argument('<name>', 'Campaign name')
|
|
12
|
+
.option('-b, --brand <brand>', 'Brand name')
|
|
13
|
+
.option('-g, --goal <goal>', 'Campaign goal')
|
|
14
|
+
.option('-p, --platform <platform>', 'Target platform')
|
|
15
|
+
.action(async (name, opts) => {
|
|
16
|
+
await withIpc(async (client) => {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
const result: any = await client.request('campaign.create', {
|
|
19
|
+
name,
|
|
20
|
+
brand: opts.brand,
|
|
21
|
+
goal: opts.goal,
|
|
22
|
+
platform: opts.platform,
|
|
23
|
+
});
|
|
24
|
+
console.log(`${icons.ok} ${c.success('Campaign created!')} ${c.dim(`#${result.id}: ${result.name}`)}`);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
cmd.command('list')
|
|
29
|
+
.description('List all campaigns')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
await withIpc(async (client) => {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const campaigns: any[] = await client.request('campaign.list') as any[];
|
|
34
|
+
if (campaigns.length === 0) {
|
|
35
|
+
console.log(`${c.dim('No campaigns yet.')}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const camp of campaigns) {
|
|
39
|
+
console.log(` ${icons.campaign} ${c.value(`#${camp.id}`)} ${camp.name} ${c.dim(camp.brand ?? '')} ${c.dim(camp.status)}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cmd.command('stats')
|
|
45
|
+
.description('Show campaign stats')
|
|
46
|
+
.argument('<id>', 'Campaign ID')
|
|
47
|
+
.action(async (id) => {
|
|
48
|
+
await withIpc(async (client) => {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
const stats: any = await client.request('campaign.stats', { id: Number(id) });
|
|
51
|
+
if (!stats) {
|
|
52
|
+
console.log(`${c.dim('Campaign not found.')}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log(` ${icons.campaign} ${c.value(stats.campaign.name)}`);
|
|
56
|
+
console.log(` Posts: ${c.value(stats.postCount)}`);
|
|
57
|
+
console.log(` Likes: ${c.value(stats.totalLikes)}`);
|
|
58
|
+
console.log(` Comments: ${c.value(stats.totalComments)}`);
|
|
59
|
+
console.log(` Shares: ${c.value(stats.totalShares)}`);
|
|
60
|
+
console.log(` Impressions: ${c.value(stats.totalImpressions)}`);
|
|
61
|
+
console.log(` Avg Engagement: ${c.green(stats.avgEngagement.toFixed(0))}`);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return cmd;
|
|
66
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
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, header, divider } from '../colors.js';
|
|
6
|
+
|
|
7
|
+
function getConfigPath(): string {
|
|
8
|
+
return path.join(getDataDir(), 'config.json');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readConfig(): Record<string, unknown> {
|
|
12
|
+
const configPath = getConfigPath();
|
|
13
|
+
if (fs.existsSync(configPath)) {
|
|
14
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeConfig(config: Record<string, unknown>): void {
|
|
20
|
+
const configPath = getConfigPath();
|
|
21
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getNestedValue(obj: Record<string, unknown>, keyPath: string): unknown {
|
|
26
|
+
const parts = keyPath.split('.');
|
|
27
|
+
let current: unknown = obj;
|
|
28
|
+
for (const part of parts) {
|
|
29
|
+
if (current === null || current === undefined || typeof current !== 'object') return undefined;
|
|
30
|
+
current = (current as Record<string, unknown>)[part];
|
|
31
|
+
}
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setNestedValue(obj: Record<string, unknown>, keyPath: string, value: unknown): void {
|
|
36
|
+
const parts = keyPath.split('.');
|
|
37
|
+
let current: Record<string, unknown> = obj;
|
|
38
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
39
|
+
const part = parts[i]!;
|
|
40
|
+
if (!current[part] || typeof current[part] !== 'object') {
|
|
41
|
+
current[part] = {};
|
|
42
|
+
}
|
|
43
|
+
current = current[part] as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
current[parts[parts.length - 1]!] = value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deleteNestedValue(obj: Record<string, unknown>, keyPath: string): boolean {
|
|
49
|
+
const parts = keyPath.split('.');
|
|
50
|
+
let current: Record<string, unknown> = obj;
|
|
51
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
52
|
+
const part = parts[i]!;
|
|
53
|
+
if (!current[part] || typeof current[part] !== 'object') return false;
|
|
54
|
+
current = current[part] as Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
const last = parts[parts.length - 1]!;
|
|
57
|
+
if (last in current) {
|
|
58
|
+
delete current[last];
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseValue(value: string): unknown {
|
|
65
|
+
if (value === 'true') return true;
|
|
66
|
+
if (value === 'false') return false;
|
|
67
|
+
if (value === 'null') return null;
|
|
68
|
+
const num = Number(value);
|
|
69
|
+
if (!isNaN(num) && value.trim() !== '') return num;
|
|
70
|
+
if ((value.startsWith('[') && value.endsWith(']')) || (value.startsWith('{') && value.endsWith('}'))) {
|
|
71
|
+
try { return JSON.parse(value); } catch { /* fall through */ }
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printObject(obj: unknown, indent = 0): void {
|
|
77
|
+
const pad = ' '.repeat(indent);
|
|
78
|
+
if (obj === null || obj === undefined) {
|
|
79
|
+
console.log(`${pad}${c.dim('(not set)')}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof obj !== 'object') {
|
|
83
|
+
console.log(`${pad}${c.value(String(obj))}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
|
|
87
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
88
|
+
console.log(`${pad}${c.cyan(key + ':')}`);
|
|
89
|
+
printObject(val, indent + 2);
|
|
90
|
+
} else {
|
|
91
|
+
const display = Array.isArray(val) ? JSON.stringify(val) : String(val);
|
|
92
|
+
console.log(`${pad}${c.label(key + ':')} ${c.value(display)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function configCommand(): Command {
|
|
98
|
+
const cmd = new Command('config')
|
|
99
|
+
.description('View and modify Marketing Brain configuration');
|
|
100
|
+
|
|
101
|
+
cmd
|
|
102
|
+
.command('show')
|
|
103
|
+
.description('Show current configuration')
|
|
104
|
+
.argument('[key]', 'Specific config key (e.g., learning.intervalMs)')
|
|
105
|
+
.action((key?: string) => {
|
|
106
|
+
const config = readConfig();
|
|
107
|
+
|
|
108
|
+
if (key) {
|
|
109
|
+
const value = getNestedValue(config, key);
|
|
110
|
+
if (value === undefined) {
|
|
111
|
+
console.log(`${c.dim(`Key "${key}" is not set in config overrides.`)}`);
|
|
112
|
+
} else {
|
|
113
|
+
console.log(`${c.label(key + ':')} ${c.value(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value))}`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(header('Marketing Brain Configuration', icons.chart));
|
|
119
|
+
console.log(` ${c.label('Config file:')} ${c.dim(getConfigPath())}\n`);
|
|
120
|
+
|
|
121
|
+
if (Object.keys(config).length === 0) {
|
|
122
|
+
console.log(` ${c.dim('No custom overrides. Using defaults.')}`);
|
|
123
|
+
console.log(` ${c.dim('Set values with:')} ${c.cyan('marketing config set <key> <value>')}`);
|
|
124
|
+
} else {
|
|
125
|
+
printObject(config, 2);
|
|
126
|
+
}
|
|
127
|
+
console.log(`\n${divider()}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
cmd
|
|
131
|
+
.command('set')
|
|
132
|
+
.description('Set a configuration value')
|
|
133
|
+
.argument('<key>', 'Config key path (e.g., learning.intervalMs)')
|
|
134
|
+
.argument('<value>', 'Value to set')
|
|
135
|
+
.action((key: string, value: string) => {
|
|
136
|
+
const config = readConfig();
|
|
137
|
+
const parsed = parseValue(value);
|
|
138
|
+
setNestedValue(config, key, parsed);
|
|
139
|
+
writeConfig(config);
|
|
140
|
+
|
|
141
|
+
console.log(`${icons.ok} ${c.label(key)} ${c.dim(icons.arrow)} ${c.value(String(parsed))}`);
|
|
142
|
+
console.log(` ${c.dim('Restart the daemon for changes to take effect:')} ${c.cyan('marketing stop && marketing start')}`);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
cmd
|
|
146
|
+
.command('delete')
|
|
147
|
+
.description('Remove a configuration override (revert to default)')
|
|
148
|
+
.argument('<key>', 'Config key path to remove')
|
|
149
|
+
.action((key: string) => {
|
|
150
|
+
const config = readConfig();
|
|
151
|
+
if (deleteNestedValue(config, key)) {
|
|
152
|
+
writeConfig(config);
|
|
153
|
+
console.log(`${icons.ok} ${c.dim(`Removed "${key}" — will use default value.`)}`);
|
|
154
|
+
console.log(` ${c.dim('Restart the daemon for changes to take effect:')} ${c.cyan('marketing stop && marketing start')}`);
|
|
155
|
+
} else {
|
|
156
|
+
console.log(`${c.dim(`Key "${key}" not found in config overrides.`)}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
cmd
|
|
161
|
+
.command('path')
|
|
162
|
+
.description('Show the config file path')
|
|
163
|
+
.action(() => {
|
|
164
|
+
console.log(getConfigPath());
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return cmd;
|
|
168
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { withIpc } from '../ipc-helper.js';
|
|
6
|
+
import { c, icons, header, divider } from '../colors.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const DASHBOARD_HTML = path.resolve(__dirname, '../../../dashboard.html');
|
|
10
|
+
|
|
11
|
+
function escapeHtml(str: string): string {
|
|
12
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function dashboardCommand(): Command {
|
|
16
|
+
return new Command('dashboard')
|
|
17
|
+
.description('Open the marketing dashboard in browser')
|
|
18
|
+
.option('-o, --output <path>', 'Output HTML file path')
|
|
19
|
+
.option('--no-open', 'Generate HTML but do not open in browser')
|
|
20
|
+
.option('-l, --live', 'Enable live mode (SSE updates from daemon)')
|
|
21
|
+
.option('-p, --port <n>', 'Dashboard server port for live mode', '7781')
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
await withIpc(async (client) => {
|
|
24
|
+
console.log(`${icons.chart} ${c.info('Generating dashboard...')}`);
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
const data: any = await client.request('analytics.dashboard', {});
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
const insights: any = await client.request('insight.list', { limit: 200 });
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
const rules: any = await client.request('rule.list', {});
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const strongest: any = await client.request('synapse.strongest', { limit: 50 });
|
|
34
|
+
|
|
35
|
+
const s = data.summary;
|
|
36
|
+
|
|
37
|
+
// Read template
|
|
38
|
+
let html = fs.readFileSync(DASHBOARD_HTML, 'utf-8');
|
|
39
|
+
|
|
40
|
+
// Stats
|
|
41
|
+
html = html.replace('{{POSTS}}', String(s.posts?.total ?? 0));
|
|
42
|
+
html = html.replace('{{CAMPAIGNS}}', String(s.campaigns?.total ?? 0));
|
|
43
|
+
html = html.replace('{{STRATEGIES}}', String(s.strategies?.total ?? 0));
|
|
44
|
+
html = html.replace('{{RULES}}', String(s.rules?.total ?? 0));
|
|
45
|
+
html = html.replace('{{TEMPLATES}}', String(s.templates?.total ?? 0));
|
|
46
|
+
html = html.replace('{{SYNAPSES}}', String(s.network?.synapses ?? 0));
|
|
47
|
+
|
|
48
|
+
// Activity score (based on data richness)
|
|
49
|
+
const activity = Math.min(100, Math.round(
|
|
50
|
+
((s.posts?.total ?? 0) * 5 +
|
|
51
|
+
(s.campaigns?.total ?? 0) * 10 +
|
|
52
|
+
(s.strategies?.total ?? 0) * 3 +
|
|
53
|
+
(s.rules?.active ?? 0) * 15 +
|
|
54
|
+
(s.insights?.active ?? 0) * 5) / 2
|
|
55
|
+
));
|
|
56
|
+
html = html.replace(/\{\{ACTIVITY\}\}/g, String(activity));
|
|
57
|
+
|
|
58
|
+
// Version
|
|
59
|
+
html = html.replace('{{VERSION}}', '0.1.0');
|
|
60
|
+
|
|
61
|
+
// Platform chart
|
|
62
|
+
const platforms = s.posts?.byPlatform ?? {};
|
|
63
|
+
const maxCount = Math.max(1, ...Object.values(platforms as Record<string, number>));
|
|
64
|
+
let platformHtml = '';
|
|
65
|
+
for (const [platform, count] of Object.entries(platforms as Record<string, number>)) {
|
|
66
|
+
const width = Math.round((count / maxCount) * 100);
|
|
67
|
+
const barClass = `${platform}-bar`;
|
|
68
|
+
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`;
|
|
69
|
+
}
|
|
70
|
+
if (!platformHtml) platformHtml = '<p class="empty">No posts tracked yet.</p>';
|
|
71
|
+
html = html.replace('{{PLATFORM_CHART}}', platformHtml);
|
|
72
|
+
|
|
73
|
+
// Top posts
|
|
74
|
+
const topPosts = data.topPerformers?.topPosts ?? [];
|
|
75
|
+
let postsHtml = '';
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
for (const post of topPosts.slice(0, 10) as any[]) {
|
|
78
|
+
const score = (post.likes ?? 0) + (post.comments ?? 0) * 3 + (post.shares ?? 0) * 5 + (post.clicks ?? 0) * 2;
|
|
79
|
+
const preview = escapeHtml((post.content ?? '').slice(0, 140));
|
|
80
|
+
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`;
|
|
81
|
+
}
|
|
82
|
+
if (!postsHtml) postsHtml = '<p class="empty">No posts with engagement data yet.</p>';
|
|
83
|
+
html = html.replace('{{TOP_POSTS}}', postsHtml);
|
|
84
|
+
|
|
85
|
+
// Rules
|
|
86
|
+
const rulesList = Array.isArray(rules) ? rules : [];
|
|
87
|
+
let rulesHtml = '';
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
for (const rule of rulesList as any[]) {
|
|
90
|
+
const conf = Math.round((rule.confidence ?? 0) * 100);
|
|
91
|
+
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`;
|
|
92
|
+
}
|
|
93
|
+
if (!rulesHtml) rulesHtml = '<p class="empty">No rules learned yet. Post more content to discover patterns.</p>';
|
|
94
|
+
html = html.replace('{{RULES_LIST}}', rulesHtml);
|
|
95
|
+
|
|
96
|
+
// Insights by type
|
|
97
|
+
const allInsights = Array.isArray(insights) ? insights : [];
|
|
98
|
+
const insightsByType: Record<string, unknown[]> = {
|
|
99
|
+
trend: [], gap: [], synergy: [], template: [], optimization: [],
|
|
100
|
+
};
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
for (const ins of allInsights as any[]) {
|
|
103
|
+
const type = ins.type ?? 'optimization';
|
|
104
|
+
if (insightsByType[type]) insightsByType[type]!.push(ins);
|
|
105
|
+
else insightsByType.optimization!.push(ins);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const typeColors: Record<string, string> = {
|
|
109
|
+
trend: 'cyan', gap: 'orange', synergy: 'green', template: 'purple', optimization: 'blue',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Plural mapping for irregular types
|
|
113
|
+
const pluralMap: Record<string, string> = {
|
|
114
|
+
trend: 'TRENDS', gap: 'GAPS', synergy: 'SYNERGIES',
|
|
115
|
+
template: 'TEMPLATES', optimization: 'OPTIMIZATIONS',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
for (const [type, items] of Object.entries(insightsByType)) {
|
|
119
|
+
const plural = pluralMap[type] ?? `${type.toUpperCase()}S`;
|
|
120
|
+
html = html.replace(`{{${plural}_COUNT}}`, String(items.length));
|
|
121
|
+
|
|
122
|
+
let insHtml = '';
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
for (const ins of items as any[]) {
|
|
125
|
+
const prio = ins.priority >= 70 ? 'high' : ins.priority >= 40 ? 'medium' : 'low';
|
|
126
|
+
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`;
|
|
127
|
+
}
|
|
128
|
+
if (!insHtml) insHtml = '<p class="empty">No insights in this category yet.</p>';
|
|
129
|
+
|
|
130
|
+
// Map type to template placeholder
|
|
131
|
+
const placeholder = type === 'template' ? '{{TEMPLATES_INSIGHTS}}' : `{{${plural}}}`;
|
|
132
|
+
html = html.replace(placeholder, insHtml);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Graph edges
|
|
136
|
+
const edges = Array.isArray(strongest) ? strongest : [];
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
const graphEdges = edges.map((e: any) => ({
|
|
139
|
+
s: `${e.source_type}:${e.source_id}`,
|
|
140
|
+
t: `${e.target_type}:${e.target_id}`,
|
|
141
|
+
type: e.synapse_type ?? 'related',
|
|
142
|
+
w: e.weight ?? 0.5,
|
|
143
|
+
}));
|
|
144
|
+
html = html.replace('{{GRAPH_EDGES}}', JSON.stringify(graphEdges));
|
|
145
|
+
|
|
146
|
+
// Write output
|
|
147
|
+
const outputPath = opts.output ?? path.join(process.env['TEMP'] ?? '/tmp', 'marketing-brain-dashboard.html');
|
|
148
|
+
fs.writeFileSync(outputPath, html, 'utf-8');
|
|
149
|
+
|
|
150
|
+
console.log(`${icons.ok} ${c.success('Dashboard generated:')} ${c.dim(outputPath)}`);
|
|
151
|
+
|
|
152
|
+
// CLI summary
|
|
153
|
+
console.log(header('Marketing Brain Dashboard', icons.megaphone));
|
|
154
|
+
console.log(` Posts: ${c.value(s.posts?.total ?? 0)} | Campaigns: ${c.value(s.campaigns?.total ?? 0)} | Strategies: ${c.value(s.strategies?.total ?? 0)}`);
|
|
155
|
+
console.log(` Rules: ${c.green(s.rules?.active ?? 0)} active | Insights: ${c.value(s.insights?.active ?? 0)} | Synapses: ${c.value(s.network?.synapses ?? 0)}`);
|
|
156
|
+
console.log(divider());
|
|
157
|
+
|
|
158
|
+
if (opts.open !== false) {
|
|
159
|
+
const { exec } = await import('node:child_process');
|
|
160
|
+
const cmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
161
|
+
exec(`${cmd} "${outputPath}"`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|