@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
package/src/api/server.ts DELETED
@@ -1,86 +0,0 @@
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
- }
package/src/cli/colors.ts DELETED
@@ -1,59 +0,0 @@
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
- }
@@ -1,66 +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 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
- }
@@ -1,168 +0,0 @@
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
- }
@@ -1,165 +0,0 @@
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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', '7783')
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>&#10084; ${post.likes ?? 0}</span><span>&#128172; ${post.comments ?? 0}</span><span>&#128257; ${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
- }
@@ -1,110 +0,0 @@
1
- import { Command } from 'commander';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import os from 'node:os';
5
- import { getDataDir, getPipeName } from '../../utils/paths.js';
6
- import { IpcClient } from '../../ipc/client.js';
7
- import { c, icons, header, divider } from '../colors.js';
8
-
9
- function pass(label: string, detail?: string): void {
10
- const extra = detail ? ` ${c.dim(detail)}` : '';
11
- console.log(` ${c.green(icons.check)} ${label}${extra}`);
12
- }
13
-
14
- function fail(label: string, detail?: string): void {
15
- const extra = detail ? ` ${c.dim(detail)}` : '';
16
- console.log(` ${c.red(icons.cross)} ${label}${extra}`);
17
- }
18
-
19
- export function doctorCommand(): Command {
20
- return new Command('doctor')
21
- .description('Check Marketing Brain health')
22
- .action(async () => {
23
- console.log(header('Marketing Brain Doctor', icons.megaphone));
24
- console.log();
25
-
26
- let allGood = true;
27
-
28
- // 1. Daemon running?
29
- const pidPath = path.join(getDataDir(), 'marketing-brain.pid');
30
- let daemonRunning = false;
31
- if (fs.existsSync(pidPath)) {
32
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
33
- try {
34
- process.kill(pid, 0);
35
- daemonRunning = true;
36
- pass('Daemon running', `PID ${pid}`);
37
- } catch {
38
- fail('Daemon not running', 'stale PID file');
39
- allGood = false;
40
- }
41
- } else {
42
- fail('Daemon not running', 'no PID file');
43
- allGood = false;
44
- }
45
-
46
- // 2. DB reachable?
47
- if (daemonRunning) {
48
- const client = new IpcClient(getPipeName(), 3000);
49
- try {
50
- await client.connect();
51
- await client.request('analytics.summary', {});
52
- pass('Database reachable');
53
- } catch {
54
- fail('Database not reachable');
55
- allGood = false;
56
- } finally {
57
- client.disconnect();
58
- }
59
- } else {
60
- fail('Database not reachable', 'daemon not running');
61
- allGood = false;
62
- }
63
-
64
- // 3. MCP configured?
65
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
66
- try {
67
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
68
- if (settings.mcpServers?.['marketing-brain']) {
69
- pass('MCP server configured');
70
- } else {
71
- fail('MCP server not configured', `add "marketing-brain" to ${settingsPath}`);
72
- allGood = false;
73
- }
74
- } catch {
75
- fail('MCP server not configured', 'settings.json not found');
76
- allGood = false;
77
- }
78
-
79
- // 4. DB file exists?
80
- const dbPath = path.join(getDataDir(), 'marketing-brain.db');
81
- try {
82
- const stat = fs.statSync(dbPath);
83
- pass('Database file', `${(stat.size / 1024 / 1024).toFixed(1)} MB at ${dbPath}`);
84
- } catch {
85
- fail('Database file not found');
86
- allGood = false;
87
- }
88
-
89
- // 5. Data dir writable?
90
- const dataDir = getDataDir();
91
- try {
92
- fs.mkdirSync(dataDir, { recursive: true });
93
- const testFile = path.join(dataDir, '.write-test');
94
- fs.writeFileSync(testFile, 'ok');
95
- fs.unlinkSync(testFile);
96
- pass('Data directory writable', dataDir);
97
- } catch {
98
- fail('Data directory not writable', dataDir);
99
- allGood = false;
100
- }
101
-
102
- console.log();
103
- if (allGood) {
104
- console.log(` ${icons.ok} ${c.success('All checks passed!')}`);
105
- } else {
106
- console.log(` ${icons.warn} ${c.warn('Some checks failed.')} Run ${c.cyan('marketing start')} first.`);
107
- }
108
- console.log(`\n${divider()}`);
109
- });
110
- }