@timmeck/marketing-brain 0.2.0 → 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.
- package/README.md +28 -13
- package/dist/cli/colors.d.ts +11 -24
- package/dist/cli/colors.js +3 -46
- package/dist/cli/colors.js.map +1 -1
- package/dist/cli/commands/dashboard.js +1 -1
- package/dist/cli/commands/peers.d.ts +2 -0
- package/dist/cli/commands/peers.js +38 -0
- package/dist/cli/commands/peers.js.map +1 -0
- package/dist/config.js +3 -3
- package/dist/db/connection.d.ts +1 -2
- package/dist/db/connection.js +1 -18
- package/dist/db/connection.js.map +1 -1
- package/dist/hooks/post-tool-use.d.ts +2 -0
- package/dist/hooks/post-tool-use.js +182 -0
- package/dist/hooks/post-tool-use.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ipc/__tests__/protocol.test.d.ts +1 -0
- package/dist/ipc/__tests__/protocol.test.js +129 -0
- package/dist/ipc/__tests__/protocol.test.js.map +1 -0
- package/dist/ipc/client.d.ts +1 -13
- package/dist/ipc/client.js +1 -92
- package/dist/ipc/client.js.map +1 -1
- package/dist/ipc/protocol.d.ts +1 -8
- package/dist/ipc/protocol.js +1 -28
- package/dist/ipc/protocol.js.map +1 -1
- package/dist/ipc/router.js +8 -0
- package/dist/ipc/router.js.map +1 -1
- package/dist/ipc/server.d.ts +1 -14
- package/dist/ipc/server.js +1 -129
- package/dist/ipc/server.js.map +1 -1
- package/dist/marketing-core.d.ts +1 -0
- package/dist/marketing-core.js +6 -1
- package/dist/marketing-core.js.map +1 -1
- package/dist/mcp/server.js +5 -60
- package/dist/mcp/server.js.map +1 -1
- package/dist/types/ipc.types.d.ts +1 -11
- package/dist/utils/__tests__/hash.test.d.ts +1 -0
- package/dist/utils/__tests__/hash.test.js +30 -0
- package/dist/utils/__tests__/hash.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.d.ts +1 -0
- package/dist/utils/__tests__/paths.test.js +63 -0
- package/dist/utils/__tests__/paths.test.js.map +1 -0
- package/dist/utils/events.d.ts +4 -8
- package/dist/utils/events.js +2 -14
- package/dist/utils/events.js.map +1 -1
- package/dist/utils/hash.d.ts +1 -1
- package/dist/utils/hash.js +1 -4
- package/dist/utils/hash.js.map +1 -1
- package/dist/utils/logger.d.ts +3 -2
- package/dist/utils/logger.js +8 -35
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/paths.d.ts +2 -1
- package/dist/utils/paths.js +4 -13
- package/dist/utils/paths.js.map +1 -1
- package/package.json +2 -1
- package/.mcp.json +0 -9
- package/src/api/server.ts +0 -86
- package/src/cli/colors.ts +0 -59
- package/src/cli/commands/campaign.ts +0 -66
- package/src/cli/commands/config.ts +0 -168
- package/src/cli/commands/dashboard.ts +0 -165
- package/src/cli/commands/doctor.ts +0 -110
- package/src/cli/commands/export.ts +0 -40
- package/src/cli/commands/import.ts +0 -84
- package/src/cli/commands/insights.ts +0 -44
- package/src/cli/commands/learn.ts +0 -24
- package/src/cli/commands/network.ts +0 -71
- package/src/cli/commands/post.ts +0 -47
- package/src/cli/commands/query.ts +0 -108
- package/src/cli/commands/rules.ts +0 -27
- package/src/cli/commands/start.ts +0 -100
- package/src/cli/commands/status.ts +0 -73
- package/src/cli/commands/stop.ts +0 -33
- package/src/cli/commands/suggest.ts +0 -64
- package/src/cli/ipc-helper.ts +0 -22
- package/src/cli/update-check.ts +0 -63
- package/src/config.ts +0 -110
- package/src/dashboard/renderer.ts +0 -136
- package/src/dashboard/server.ts +0 -140
- package/src/db/connection.ts +0 -22
- package/src/db/migrations/001_core_schema.ts +0 -63
- package/src/db/migrations/002_learning_schema.ts +0 -46
- package/src/db/migrations/003_synapse_schema.ts +0 -27
- package/src/db/migrations/004_insights_schema.ts +0 -38
- package/src/db/migrations/005_fts_indexes.ts +0 -77
- package/src/db/migrations/index.ts +0 -62
- package/src/db/repositories/audience.repository.ts +0 -53
- package/src/db/repositories/campaign.repository.ts +0 -72
- package/src/db/repositories/engagement.repository.ts +0 -108
- package/src/db/repositories/insight.repository.ts +0 -100
- package/src/db/repositories/post.repository.ts +0 -123
- package/src/db/repositories/rule.repository.ts +0 -87
- package/src/db/repositories/strategy.repository.ts +0 -82
- package/src/db/repositories/synapse.repository.ts +0 -148
- package/src/db/repositories/template.repository.ts +0 -76
- package/src/index.ts +0 -69
- package/src/ipc/client.ts +0 -110
- package/src/ipc/protocol.ts +0 -35
- package/src/ipc/router.ts +0 -126
- package/src/ipc/server.ts +0 -140
- package/src/learning/confidence-scorer.ts +0 -36
- package/src/learning/learning-engine.ts +0 -254
- package/src/marketing-core.ts +0 -285
- package/src/mcp/server.ts +0 -72
- package/src/mcp/tools.ts +0 -216
- package/src/research/research-engine.ts +0 -226
- package/src/services/analytics.service.ts +0 -73
- package/src/services/audience.service.ts +0 -40
- package/src/services/campaign.service.ts +0 -80
- package/src/services/insight.service.ts +0 -54
- package/src/services/post.service.ts +0 -116
- package/src/services/rule.service.ts +0 -90
- package/src/services/strategy.service.ts +0 -53
- package/src/services/synapse.service.ts +0 -32
- package/src/services/template.service.ts +0 -50
- package/src/synapses/activation.ts +0 -80
- package/src/synapses/decay.ts +0 -38
- package/src/synapses/hebbian.ts +0 -68
- package/src/synapses/pathfinder.ts +0 -81
- package/src/synapses/synapse-manager.ts +0 -115
- package/src/types/config.types.ts +0 -79
- package/src/types/ipc.types.ts +0 -8
- package/src/types/post.types.ts +0 -156
- package/src/types/synapse.types.ts +0 -43
- package/src/utils/events.ts +0 -44
- package/src/utils/hash.ts +0 -5
- package/src/utils/logger.ts +0 -48
- package/src/utils/paths.ts +0 -19
- package/tsconfig.json +0 -18
|
@@ -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, '&').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
|
-
}
|
|
@@ -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
|
-
}
|
|
@@ -1,40 +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 exportCommand(): Command {
|
|
6
|
-
return new Command('export')
|
|
7
|
-
.description('Export Marketing Brain data')
|
|
8
|
-
.option('--format <fmt>', 'Output format: json (default)', 'json')
|
|
9
|
-
.action(async () => {
|
|
10
|
-
await withIpc(async (client) => {
|
|
11
|
-
process.stderr.write(`${icons.chart} ${c.info('Exporting Marketing Brain data...')}\n`);
|
|
12
|
-
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
-
const summary: any = await client.request('analytics.summary', {});
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
-
const top: any = await client.request('analytics.top', { limit: 50 });
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
-
const insights: any = await client.request('insight.list', { limit: 100 });
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
const rules: any = await client.request('rule.list', {});
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
-
const strategies: any = await client.request('strategy.list', { limit: 100 });
|
|
23
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
-
const network: any = await client.request('synapse.stats', {});
|
|
25
|
-
|
|
26
|
-
const data = {
|
|
27
|
-
exportedAt: new Date().toISOString(),
|
|
28
|
-
summary,
|
|
29
|
-
topPerformers: top,
|
|
30
|
-
insights,
|
|
31
|
-
rules,
|
|
32
|
-
strategies,
|
|
33
|
-
network,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
console.log(JSON.stringify(data, null, 2));
|
|
37
|
-
process.stderr.write(`${icons.ok} ${c.success('Export complete.')}\n`);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
@@ -1,84 +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 importCommand(): Command {
|
|
6
|
-
return new Command('import')
|
|
7
|
-
.description('Import posts from a JSON file')
|
|
8
|
-
.argument('<file>', 'JSON file with posts array')
|
|
9
|
-
.action(async (file) => {
|
|
10
|
-
const fs = await import('node:fs');
|
|
11
|
-
const path = await import('node:path');
|
|
12
|
-
|
|
13
|
-
const filePath = path.default.resolve(file);
|
|
14
|
-
if (!fs.default.existsSync(filePath)) {
|
|
15
|
-
console.error(`${icons.error} ${c.error(`File not found: ${filePath}`)}`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const data = JSON.parse(fs.default.readFileSync(filePath, 'utf-8'));
|
|
20
|
-
const posts = Array.isArray(data) ? data : data.posts;
|
|
21
|
-
|
|
22
|
-
if (!Array.isArray(posts)) {
|
|
23
|
-
console.error(`${icons.error} ${c.error('Expected JSON array or { posts: [...] }')}`);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
await withIpc(async (client) => {
|
|
28
|
-
let imported = 0;
|
|
29
|
-
let skipped = 0;
|
|
30
|
-
|
|
31
|
-
for (const post of posts) {
|
|
32
|
-
try {
|
|
33
|
-
// Create campaign if specified
|
|
34
|
-
let campaignId: number | null = null;
|
|
35
|
-
if (post.campaign) {
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
-
const camp: any = await client.request('campaign.create', { name: post.campaign, brand: post.brand });
|
|
38
|
-
campaignId = camp.id;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
-
const result: any = await client.request('post.report', {
|
|
43
|
-
platform: post.platform,
|
|
44
|
-
content: post.content,
|
|
45
|
-
format: post.format ?? 'text',
|
|
46
|
-
url: post.url ?? null,
|
|
47
|
-
hashtags: post.hashtags ?? null,
|
|
48
|
-
campaign_id: campaignId,
|
|
49
|
-
status: post.status ?? 'published',
|
|
50
|
-
published_at: post.published_at ?? null,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
if (result.isNew) {
|
|
54
|
-
imported++;
|
|
55
|
-
|
|
56
|
-
// Import engagement if provided
|
|
57
|
-
if (post.engagement) {
|
|
58
|
-
await client.request('post.engagement', {
|
|
59
|
-
post_id: result.post.id,
|
|
60
|
-
...post.engagement,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Import strategy if provided
|
|
65
|
-
if (post.strategy) {
|
|
66
|
-
await client.request('strategy.report', {
|
|
67
|
-
post_id: result.post.id,
|
|
68
|
-
description: post.strategy.description,
|
|
69
|
-
approach: post.strategy.approach,
|
|
70
|
-
outcome: post.strategy.outcome,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
} else {
|
|
74
|
-
skipped++;
|
|
75
|
-
}
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error(`${icons.error} ${c.error(`Failed: ${err instanceof Error ? err.message : err}`)}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
console.log(`${icons.ok} ${c.success(`Import complete: ${imported} imported, ${skipped} skipped (duplicates)`)}`);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
}
|
|
@@ -1,44 +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 insightsCommand(): Command {
|
|
6
|
-
return new Command('insights')
|
|
7
|
-
.description('Show current marketing insights')
|
|
8
|
-
.option('-t, --type <type>', 'Filter by type (trend, gap, synergy, template, optimization)')
|
|
9
|
-
.option('-l, --limit <n>', 'Max results', '10')
|
|
10
|
-
.action(async (opts) => {
|
|
11
|
-
await withIpc(async (client) => {
|
|
12
|
-
let insights: unknown[];
|
|
13
|
-
if (opts.type) {
|
|
14
|
-
insights = await client.request('insight.byType', { type: opts.type, limit: Number(opts.limit) }) as unknown[];
|
|
15
|
-
} else {
|
|
16
|
-
insights = await client.request('insight.list', { limit: Number(opts.limit) }) as unknown[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if ((insights as unknown[]).length === 0) {
|
|
20
|
-
console.log(`${c.dim('No active insights. Start tracking posts to generate insights!')}`);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
-
for (const insight of insights as any[]) {
|
|
26
|
-
const typeIcon = insight.type === 'trend' ? '📈'
|
|
27
|
-
: insight.type === 'gap' ? '🕳️'
|
|
28
|
-
: insight.type === 'synergy' ? '🔄'
|
|
29
|
-
: insight.type === 'template' ? icons.template
|
|
30
|
-
: insight.type === 'optimization' ? '⚡'
|
|
31
|
-
: icons.insight;
|
|
32
|
-
|
|
33
|
-
const priority = insight.priority >= 7 ? c.red(`[P${insight.priority}]`)
|
|
34
|
-
: insight.priority >= 4 ? c.orange(`[P${insight.priority}]`)
|
|
35
|
-
: c.dim(`[P${insight.priority}]`);
|
|
36
|
-
|
|
37
|
-
console.log(` ${typeIcon} ${priority} ${c.value(insight.title)}`);
|
|
38
|
-
console.log(` ${c.dim(insight.description)}`);
|
|
39
|
-
console.log(` ${c.dim(`confidence: ${(insight.confidence * 100).toFixed(0)}%`)}`);
|
|
40
|
-
console.log();
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { withIpc } from '../ipc-helper.js';
|
|
3
|
-
import { c, icons, header, keyValue, divider } from '../colors.js';
|
|
4
|
-
|
|
5
|
-
export function learnCommand(): Command {
|
|
6
|
-
return new Command('learn')
|
|
7
|
-
.description('Trigger a learning cycle manually (pattern extraction + rule generation)')
|
|
8
|
-
.action(async () => {
|
|
9
|
-
await withIpc(async (client) => {
|
|
10
|
-
console.log(`${icons.bolt} ${c.info('Running learning cycle...')}`);
|
|
11
|
-
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
const result: any = await client.request('learning.run', {});
|
|
14
|
-
|
|
15
|
-
console.log(header('Learning Cycle Complete', icons.bolt));
|
|
16
|
-
console.log(keyValue('Rules created', result.rulesCreated ?? 0));
|
|
17
|
-
console.log(keyValue('Rules updated', result.rulesUpdated ?? 0));
|
|
18
|
-
console.log(keyValue('Strategies updated', result.strategiesUpdated ?? 0));
|
|
19
|
-
console.log(keyValue('Synapses decayed', result.synapsesDecayed ?? 0));
|
|
20
|
-
console.log(keyValue('Synapses pruned', result.synapsesPruned ?? 0));
|
|
21
|
-
console.log(`\n${divider()}`);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
}
|