@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.
Files changed (130) hide show
  1. package/README.md +28 -13
  2. package/dist/cli/colors.d.ts +11 -24
  3. package/dist/cli/colors.js +3 -46
  4. package/dist/cli/colors.js.map +1 -1
  5. package/dist/cli/commands/dashboard.js +1 -1
  6. package/dist/cli/commands/peers.d.ts +2 -0
  7. package/dist/cli/commands/peers.js +38 -0
  8. package/dist/cli/commands/peers.js.map +1 -0
  9. package/dist/config.js +3 -3
  10. package/dist/db/connection.d.ts +1 -2
  11. package/dist/db/connection.js +1 -18
  12. package/dist/db/connection.js.map +1 -1
  13. package/dist/hooks/post-tool-use.d.ts +2 -0
  14. package/dist/hooks/post-tool-use.js +182 -0
  15. package/dist/hooks/post-tool-use.js.map +1 -0
  16. package/dist/index.js +2 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/ipc/__tests__/protocol.test.d.ts +1 -0
  19. package/dist/ipc/__tests__/protocol.test.js +129 -0
  20. package/dist/ipc/__tests__/protocol.test.js.map +1 -0
  21. package/dist/ipc/client.d.ts +1 -13
  22. package/dist/ipc/client.js +1 -92
  23. package/dist/ipc/client.js.map +1 -1
  24. package/dist/ipc/protocol.d.ts +1 -8
  25. package/dist/ipc/protocol.js +1 -28
  26. package/dist/ipc/protocol.js.map +1 -1
  27. package/dist/ipc/router.js +8 -0
  28. package/dist/ipc/router.js.map +1 -1
  29. package/dist/ipc/server.d.ts +1 -14
  30. package/dist/ipc/server.js +1 -129
  31. package/dist/ipc/server.js.map +1 -1
  32. package/dist/marketing-core.d.ts +1 -0
  33. package/dist/marketing-core.js +6 -1
  34. package/dist/marketing-core.js.map +1 -1
  35. package/dist/mcp/server.js +5 -60
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/types/ipc.types.d.ts +1 -11
  38. package/dist/utils/__tests__/hash.test.d.ts +1 -0
  39. package/dist/utils/__tests__/hash.test.js +30 -0
  40. package/dist/utils/__tests__/hash.test.js.map +1 -0
  41. package/dist/utils/__tests__/paths.test.d.ts +1 -0
  42. package/dist/utils/__tests__/paths.test.js +63 -0
  43. package/dist/utils/__tests__/paths.test.js.map +1 -0
  44. package/dist/utils/events.d.ts +4 -8
  45. package/dist/utils/events.js +2 -14
  46. package/dist/utils/events.js.map +1 -1
  47. package/dist/utils/hash.d.ts +1 -1
  48. package/dist/utils/hash.js +1 -4
  49. package/dist/utils/hash.js.map +1 -1
  50. package/dist/utils/logger.d.ts +3 -2
  51. package/dist/utils/logger.js +8 -35
  52. package/dist/utils/logger.js.map +1 -1
  53. package/dist/utils/paths.d.ts +2 -1
  54. package/dist/utils/paths.js +4 -13
  55. package/dist/utils/paths.js.map +1 -1
  56. package/package.json +2 -1
  57. package/.mcp.json +0 -9
  58. package/src/api/server.ts +0 -86
  59. package/src/cli/colors.ts +0 -59
  60. package/src/cli/commands/campaign.ts +0 -66
  61. package/src/cli/commands/config.ts +0 -168
  62. package/src/cli/commands/dashboard.ts +0 -165
  63. package/src/cli/commands/doctor.ts +0 -110
  64. package/src/cli/commands/export.ts +0 -40
  65. package/src/cli/commands/import.ts +0 -84
  66. package/src/cli/commands/insights.ts +0 -44
  67. package/src/cli/commands/learn.ts +0 -24
  68. package/src/cli/commands/network.ts +0 -71
  69. package/src/cli/commands/post.ts +0 -47
  70. package/src/cli/commands/query.ts +0 -108
  71. package/src/cli/commands/rules.ts +0 -27
  72. package/src/cli/commands/start.ts +0 -100
  73. package/src/cli/commands/status.ts +0 -73
  74. package/src/cli/commands/stop.ts +0 -33
  75. package/src/cli/commands/suggest.ts +0 -64
  76. package/src/cli/ipc-helper.ts +0 -22
  77. package/src/cli/update-check.ts +0 -63
  78. package/src/config.ts +0 -110
  79. package/src/dashboard/renderer.ts +0 -136
  80. package/src/dashboard/server.ts +0 -140
  81. package/src/db/connection.ts +0 -22
  82. package/src/db/migrations/001_core_schema.ts +0 -63
  83. package/src/db/migrations/002_learning_schema.ts +0 -46
  84. package/src/db/migrations/003_synapse_schema.ts +0 -27
  85. package/src/db/migrations/004_insights_schema.ts +0 -38
  86. package/src/db/migrations/005_fts_indexes.ts +0 -77
  87. package/src/db/migrations/index.ts +0 -62
  88. package/src/db/repositories/audience.repository.ts +0 -53
  89. package/src/db/repositories/campaign.repository.ts +0 -72
  90. package/src/db/repositories/engagement.repository.ts +0 -108
  91. package/src/db/repositories/insight.repository.ts +0 -100
  92. package/src/db/repositories/post.repository.ts +0 -123
  93. package/src/db/repositories/rule.repository.ts +0 -87
  94. package/src/db/repositories/strategy.repository.ts +0 -82
  95. package/src/db/repositories/synapse.repository.ts +0 -148
  96. package/src/db/repositories/template.repository.ts +0 -76
  97. package/src/index.ts +0 -69
  98. package/src/ipc/client.ts +0 -110
  99. package/src/ipc/protocol.ts +0 -35
  100. package/src/ipc/router.ts +0 -126
  101. package/src/ipc/server.ts +0 -140
  102. package/src/learning/confidence-scorer.ts +0 -36
  103. package/src/learning/learning-engine.ts +0 -254
  104. package/src/marketing-core.ts +0 -285
  105. package/src/mcp/server.ts +0 -72
  106. package/src/mcp/tools.ts +0 -216
  107. package/src/research/research-engine.ts +0 -226
  108. package/src/services/analytics.service.ts +0 -73
  109. package/src/services/audience.service.ts +0 -40
  110. package/src/services/campaign.service.ts +0 -80
  111. package/src/services/insight.service.ts +0 -54
  112. package/src/services/post.service.ts +0 -116
  113. package/src/services/rule.service.ts +0 -90
  114. package/src/services/strategy.service.ts +0 -53
  115. package/src/services/synapse.service.ts +0 -32
  116. package/src/services/template.service.ts +0 -50
  117. package/src/synapses/activation.ts +0 -80
  118. package/src/synapses/decay.ts +0 -38
  119. package/src/synapses/hebbian.ts +0 -68
  120. package/src/synapses/pathfinder.ts +0 -81
  121. package/src/synapses/synapse-manager.ts +0 -115
  122. package/src/types/config.types.ts +0 -79
  123. package/src/types/ipc.types.ts +0 -8
  124. package/src/types/post.types.ts +0 -156
  125. package/src/types/synapse.types.ts +0 -43
  126. package/src/utils/events.ts +0 -44
  127. package/src/utils/hash.ts +0 -5
  128. package/src/utils/logger.ts +0 -48
  129. package/src/utils/paths.ts +0 -19
  130. package/tsconfig.json +0 -18
@@ -1,71 +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 networkCommand(): Command {
6
- return new Command('network')
7
- .description('Explore the synapse network')
8
- .option('--node <type:id>', 'Node to explore (e.g., post:42)')
9
- .option('-l, --limit <n>', 'Max synapses to show', '20')
10
- .action(async (opts) => {
11
- await withIpc(async (client) => {
12
- if (opts.node) {
13
- const [nodeType, nodeIdStr] = opts.node.split(':');
14
- const nodeId = parseInt(nodeIdStr, 10);
15
-
16
- if (!nodeType || isNaN(nodeId)) {
17
- console.error(c.error('Invalid node format. Use: --node post:42'));
18
- return;
19
- }
20
-
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- const related: any = await client.request('synapse.related', {
23
- nodeType,
24
- nodeId,
25
- maxDepth: 2,
26
- });
27
-
28
- if (!related?.length) {
29
- console.log(`${c.dim('No connections found for')} ${c.cyan(`${nodeType}:${nodeId}`)}`);
30
- return;
31
- }
32
-
33
- console.log(header(`Connections from ${nodeType}:${nodeId}`, icons.synapse));
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- for (const r of related as any[]) {
36
- const weight = (r.activation ?? r.weight ?? 0);
37
- const weightColor = weight >= 0.7 ? c.green : weight >= 0.3 ? c.orange : c.dim;
38
- const nodeType = r.node?.type ?? r.nodeType ?? '?';
39
- const nodeId = r.node?.id ?? r.nodeId ?? '?';
40
- console.log(` ${c.cyan(icons.arrow)} ${c.value(`${nodeType}:${nodeId}`)} ${c.label('weight:')} ${weightColor(weight.toFixed(3))}`);
41
- }
42
- } else {
43
- // Show general network stats
44
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
- const stats: any = await client.request('synapse.stats', {});
46
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
- const strongest: any = await client.request('synapse.strongest', {
48
- limit: parseInt(opts.limit, 10),
49
- });
50
-
51
- console.log(header('Synapse Network', icons.synapse));
52
- console.log(keyValue('Total synapses', stats.totalSynapses ?? 0));
53
- console.log(keyValue('Average weight', (stats.avgWeight ?? 0).toFixed(3)));
54
- console.log();
55
-
56
- if (strongest?.length) {
57
- console.log(` ${c.purple.bold('Strongest connections:')}`);
58
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
- for (const s of strongest as any[]) {
60
- const weight = (s.weight ?? 0);
61
- const weightColor = weight >= 0.7 ? c.green : weight >= 0.3 ? c.orange : c.dim;
62
- const src = `${s.source_type}:${s.source_id}`;
63
- const tgt = `${s.target_type}:${s.target_id}`;
64
- console.log(` ${c.dim(src)} ${c.cyan(icons.arrow)} ${c.dim(tgt)} ${c.label(`[${s.synapse_type}]`)} ${weightColor(weight.toFixed(3))}`);
65
- }
66
- }
67
- }
68
- console.log(`\n${divider()}`);
69
- });
70
- });
71
- }
@@ -1,47 +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 postCommand(): Command {
6
- return new Command('post')
7
- .description('Report a published post')
8
- .argument('<platform>', 'Platform (x, reddit, linkedin, bluesky)')
9
- .argument('[url]', 'Post URL')
10
- .option('-c, --content <text>', 'Post content/text')
11
- .option('-f, --format <format>', 'Post format (text, image, video, thread)', 'text')
12
- .option('--campaign <name>', 'Campaign name')
13
- .option('--hashtags <tags>', 'Hashtags (comma-separated)')
14
- .action(async (platform, url, opts) => {
15
- if (!opts.content && !url) {
16
- console.error(`${icons.error} ${c.error('Provide either --content or a URL')}`);
17
- process.exit(1);
18
- }
19
-
20
- await withIpc(async (client) => {
21
- let campaignId: number | null = null;
22
- if (opts.campaign) {
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- const campaign: any = await client.request('campaign.create', { name: opts.campaign });
25
- campaignId = campaign.id;
26
- }
27
-
28
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
- const result: any = await client.request('post.report', {
30
- platform,
31
- content: opts.content ?? `Post at ${url}`,
32
- format: opts.format,
33
- url: url ?? null,
34
- hashtags: opts.hashtags ?? null,
35
- campaign_id: campaignId,
36
- status: 'published',
37
- published_at: new Date().toISOString(),
38
- });
39
-
40
- if (result.isNew) {
41
- console.log(`${icons.ok} ${c.success('Post reported!')} ${c.dim(`#${result.post.id} on ${platform}`)}`);
42
- } else {
43
- console.log(`${icons.post} ${c.info('Post already tracked')} ${c.dim(`#${result.post.id}`)}`);
44
- }
45
- });
46
- });
47
- }
@@ -1,108 +0,0 @@
1
- import { Command } from 'commander';
2
- import { withIpc } from '../ipc-helper.js';
3
- import { c, icons, header, divider } from '../colors.js';
4
-
5
- export function queryCommand(): Command {
6
- return new Command('query')
7
- .description('Search posts, strategies, and insights')
8
- .argument('<search>', 'Search term')
9
- .option('-l, --limit <n>', 'Maximum results per category', '10')
10
- .option('--posts-only', 'Only search posts')
11
- .option('--strategies-only', 'Only search strategies')
12
- .option('--insights-only', 'Only search insights')
13
- .option('--page <n>', 'Page number (starting from 1)', '1')
14
- .action(async (search: string, opts) => {
15
- await withIpc(async (client) => {
16
- const limit = parseInt(opts.limit, 10);
17
- const page = parseInt(opts.page, 10) || 1;
18
- const offset = (page - 1) * limit;
19
- const searchAll = !opts.postsOnly && !opts.strategiesOnly && !opts.insightsOnly;
20
- let totalResults = 0;
21
-
22
- // --- Posts ---
23
- if (searchAll || opts.postsOnly) {
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- const results: any = await client.request('post.search', {
26
- query: search,
27
- limit: limit + offset,
28
- });
29
-
30
- const posts = Array.isArray(results) ? results.slice(offset, offset + limit) : [];
31
- if (posts.length > 0) {
32
- totalResults += posts.length;
33
- console.log(header(`Posts matching "${search}"`, icons.post));
34
-
35
- for (const post of posts) {
36
- const platformTag = c.cyan(`[${post.platform}]`);
37
- const formatTag = c.purple(post.format ?? 'text');
38
- console.log(` ${c.dim(`#${post.id}`)} ${platformTag} ${formatTag}`);
39
- console.log(` ${c.dim((post.content ?? '').slice(0, 120))}`);
40
- if (post.hashtags) {
41
- console.log(` ${c.orange(post.hashtags)}`);
42
- }
43
- console.log();
44
- }
45
- }
46
- }
47
-
48
- // --- Strategies ---
49
- if (searchAll || opts.strategiesOnly) {
50
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
- const strategies: any = await client.request('strategy.suggest', {
52
- query: search,
53
- limit: limit + offset,
54
- });
55
-
56
- const strats = Array.isArray(strategies) ? strategies.slice(offset, offset + limit) : [];
57
- if (strats.length > 0) {
58
- totalResults += strats.length;
59
- console.log(header(`Strategies matching "${search}"`, icons.campaign));
60
-
61
- for (const strat of strats) {
62
- const confidence = strat.confidence ?? 0;
63
- const confColor = confidence >= 0.7 ? c.green : confidence >= 0.4 ? c.orange : c.dim;
64
- console.log(` ${c.dim(`#${strat.id}`)} ${confColor(`[${(confidence * 100).toFixed(0)}%]`)} ${c.value(strat.description ?? '')}`);
65
- if (strat.approach) {
66
- console.log(` ${c.label('Approach:')} ${c.dim(strat.approach.slice(0, 120))}`);
67
- }
68
- console.log();
69
- }
70
- }
71
- }
72
-
73
- // --- Insights ---
74
- if (searchAll || opts.insightsOnly) {
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- const insights: any = await client.request('insight.list', { limit: 100 });
77
-
78
- const allInsights = Array.isArray(insights) ? insights : [];
79
- const searchLower = search.toLowerCase();
80
- const matched = allInsights.filter((i: { title?: string; description?: string }) =>
81
- (i.title ?? '').toLowerCase().includes(searchLower) ||
82
- (i.description ?? '').toLowerCase().includes(searchLower)
83
- ).slice(offset, offset + limit);
84
-
85
- if (matched.length > 0) {
86
- totalResults += matched.length;
87
- console.log(header(`Insights matching "${search}"`, icons.insight));
88
-
89
- for (const ins of matched) {
90
- const typeTag = c.cyan(`[${ins.type}]`);
91
- console.log(` ${typeTag} ${c.value(ins.title)}`);
92
- if (ins.description) {
93
- console.log(` ${c.dim(ins.description.slice(0, 150))}`);
94
- }
95
- console.log();
96
- }
97
- }
98
- }
99
-
100
- if (totalResults === 0) {
101
- console.log(`\n${icons.insight} ${c.dim(`No results found for "${search}".`)}`);
102
- } else {
103
- console.log(` ${c.dim(`Page ${page} — showing ${totalResults} result(s). Use --page ${page + 1} for more.`)}`);
104
- console.log(divider());
105
- }
106
- });
107
- });
108
- }
@@ -1,27 +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 rulesCommand(): Command {
6
- return new Command('rules')
7
- .description('Show learned marketing rules')
8
- .action(async () => {
9
- await withIpc(async (client) => {
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- const rules: any[] = await client.request('rule.list') as any[];
12
-
13
- if (rules.length === 0) {
14
- console.log(`${c.dim('No rules learned yet. Track more posts to generate rules!')}`);
15
- return;
16
- }
17
-
18
- for (const rule of rules) {
19
- const conf = (rule.confidence * 100).toFixed(0);
20
- console.log(` ${icons.rule} ${c.value(rule.pattern)}`);
21
- console.log(` ${c.dim(rule.recommendation)}`);
22
- console.log(` ${c.dim(`confidence: ${conf}% | triggers: ${rule.trigger_count} | success: ${rule.success_count}`)}`);
23
- console.log();
24
- }
25
- });
26
- });
27
- }
@@ -1,100 +0,0 @@
1
- import { Command } from 'commander';
2
- import { spawn, type ChildProcess } from 'node:child_process';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { getDataDir } from '../../utils/paths.js';
6
- import { c, icons } from '../colors.js';
7
-
8
- const MAX_RESTARTS = 5;
9
- const RESTART_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
10
- const BASE_BACKOFF_MS = 1000;
11
-
12
- function spawnDaemon(entryPoint: string, args: string[]): ChildProcess {
13
- const child = spawn(process.execPath, [entryPoint, ...args], {
14
- detached: true,
15
- stdio: 'ignore',
16
- });
17
- child.unref();
18
- return child;
19
- }
20
-
21
- function startWatchdog(entryPoint: string, args: string[], pidPath: string): void {
22
- const restartTimes: number[] = [];
23
-
24
- function launch(): void {
25
- const child = spawnDaemon(entryPoint, args);
26
- console.log(`${icons.megaphone} ${c.info('Marketing Brain daemon starting')} ${c.dim(`(PID: ${child.pid})`)}`);
27
-
28
- child.on('exit', (code) => {
29
- // Normal shutdown (code 0 or SIGTERM) — don't restart
30
- if (code === 0 || code === null) return;
31
-
32
- const now = Date.now();
33
- restartTimes.push(now);
34
-
35
- // Only count restarts within the window
36
- const recentRestarts = restartTimes.filter((t) => now - t < RESTART_WINDOW_MS);
37
- restartTimes.length = 0;
38
- restartTimes.push(...recentRestarts);
39
-
40
- if (recentRestarts.length > MAX_RESTARTS) {
41
- console.error(`${icons.error} ${c.error(`Marketing Brain crashed ${MAX_RESTARTS} times in 5 minutes — giving up.`)}`);
42
- // Clean up stale PID file
43
- try { fs.unlinkSync(pidPath); } catch { /* ignore */ }
44
- return;
45
- }
46
-
47
- const backoff = BASE_BACKOFF_MS * Math.pow(2, recentRestarts.length - 1);
48
- console.log(`${icons.warn} ${c.warn(`Marketing Brain exited (code ${code}) — restarting in ${backoff / 1000}s...`)}`);
49
-
50
- setTimeout(launch, backoff);
51
- });
52
- }
53
-
54
- launch();
55
-
56
- // Wait briefly for PID file to appear
57
- setTimeout(() => {
58
- if (fs.existsSync(pidPath)) {
59
- console.log(`${icons.ok} ${c.success('Marketing Brain daemon started successfully.')} ${c.dim('(watchdog active)')}`);
60
- } else {
61
- console.log(`${icons.clock} ${c.warn('Daemon may still be starting.')} Check: ${c.cyan('marketing status')}`);
62
- }
63
- }, 1000);
64
- }
65
-
66
- export function startCommand(): Command {
67
- return new Command('start')
68
- .description('Start the Marketing Brain daemon')
69
- .option('-f, --foreground', 'Run in foreground (no detach)')
70
- .option('-c, --config <path>', 'Config file path')
71
- .action((opts) => {
72
- const pidPath = path.join(getDataDir(), 'marketing-brain.pid');
73
-
74
- if (fs.existsSync(pidPath)) {
75
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
76
- try {
77
- process.kill(pid, 0);
78
- console.log(`${icons.megaphone} Marketing Brain daemon is ${c.green('already running')} ${c.dim(`(PID: ${pid})`)}`);
79
- return;
80
- } catch {
81
- fs.unlinkSync(pidPath);
82
- }
83
- }
84
-
85
- if (opts.foreground) {
86
- import('../../marketing-core.js').then(({ MarketingCore }) => {
87
- const core = new MarketingCore();
88
- core.start(opts.config);
89
- });
90
- return;
91
- }
92
-
93
- // Spawn detached daemon with watchdog
94
- const args = ['daemon'];
95
- if (opts.config) args.push('-c', opts.config);
96
- const entryPoint = path.resolve(import.meta.dirname, '../../index.js');
97
-
98
- startWatchdog(entryPoint, args, pidPath);
99
- });
100
- }
@@ -1,73 +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 { withIpc } from '../ipc-helper.js';
6
- import { c, icons, header, keyValue, divider } from '../colors.js';
7
-
8
- export function statusCommand(): Command {
9
- return new Command('status')
10
- .description('Show Marketing Brain daemon status')
11
- .action(async () => {
12
- const pidPath = path.join(getDataDir(), 'marketing-brain.pid');
13
-
14
- if (!fs.existsSync(pidPath)) {
15
- console.log(`${icons.megaphone} Marketing Brain Daemon: ${c.red.bold('NOT RUNNING')}`);
16
- return;
17
- }
18
-
19
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
20
- let running = false;
21
- try {
22
- process.kill(pid, 0);
23
- running = true;
24
- } catch { /* not running */ }
25
-
26
- if (!running) {
27
- console.log(`${icons.megaphone} Marketing Brain Daemon: ${c.red.bold('NOT RUNNING')} ${c.dim('(stale PID file)')}`);
28
- return;
29
- }
30
-
31
- console.log(header('Marketing Brain Status', icons.megaphone));
32
- console.log(` ${c.green(`${icons.dot} RUNNING`)} ${c.dim(`(PID ${pid})`)}`);
33
-
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- await withIpc(async (client) => {
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- const summary: any = await client.request('analytics.summary', {});
38
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- const network: any = await client.request('synapse.stats', {});
40
-
41
- const dbPath = path.join(getDataDir(), 'marketing-brain.db');
42
- let dbSize = '?';
43
- try {
44
- const stat = fs.statSync(dbPath);
45
- dbSize = `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
46
- } catch { /* ignore */ }
47
-
48
- console.log(keyValue('Database', `${dbPath} (${dbSize})`));
49
- console.log();
50
-
51
- console.log(` ${icons.post} ${c.purple.bold('Content')}`);
52
- console.log(` ${c.label('Posts:')} ${c.value(summary.posts?.total ?? 0)} total`);
53
- console.log(` ${c.label('Campaigns:')} ${c.value(summary.campaigns?.total ?? 0)}`);
54
- console.log(` ${c.label('Templates:')} ${c.value(summary.templates?.total ?? 0)}`);
55
- console.log();
56
-
57
- console.log(` ${icons.rule} ${c.blue.bold('Learning')}`);
58
- console.log(` ${c.label('Strategies:')} ${c.value(summary.strategies?.total ?? 0)}`);
59
- console.log(` ${c.label('Rules:')} ${c.green(summary.rules?.active ?? 0)} active`);
60
- console.log();
61
-
62
- console.log(` ${icons.synapse} ${c.cyan.bold('Synapse Network')}`);
63
- console.log(` ${c.label('Synapses:')} ${c.value(network.totalSynapses ?? 0)}`);
64
- console.log(` ${c.label('Avg weight:')} ${c.value((network.avgWeight ?? 0).toFixed(2))}`);
65
- console.log();
66
-
67
- console.log(` ${icons.insight} ${c.orange.bold('Research')}`);
68
- console.log(` ${c.label('Insights:')} ${c.value(summary.insights?.active ?? 0)} active`);
69
-
70
- console.log(`\n${divider()}`);
71
- });
72
- });
73
- }
@@ -1,33 +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 } from '../colors.js';
6
-
7
- export function stopCommand(): Command {
8
- return new Command('stop')
9
- .description('Stop the Marketing Brain daemon')
10
- .action(() => {
11
- const pidPath = path.join(getDataDir(), 'marketing-brain.pid');
12
-
13
- if (!fs.existsSync(pidPath)) {
14
- console.log(`${icons.megaphone} ${c.dim('Marketing Brain daemon is not running (no PID file found).')}`);
15
- return;
16
- }
17
-
18
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
19
-
20
- try {
21
- process.kill(pid, 'SIGTERM');
22
- console.log(`${icons.megaphone} ${c.success('Marketing Brain daemon stopped')} ${c.dim(`(PID: ${pid})`)}`);
23
- } catch (err) {
24
- if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
25
- console.log(`${icons.megaphone} ${c.dim('Marketing Brain daemon was not running (stale PID file removed).')}`);
26
- } else {
27
- console.error(`${icons.error} ${c.error(`Failed to stop daemon: ${err}`)}`);
28
- }
29
- }
30
-
31
- try { fs.unlinkSync(pidPath); } catch { /* ignore */ }
32
- });
33
- }
@@ -1,64 +0,0 @@
1
- import { Command } from 'commander';
2
- import { withIpc } from '../ipc-helper.js';
3
- import { c, icons } from '../colors.js';
4
-
5
- export function suggestCommand(): Command {
6
- return new Command('suggest')
7
- .description('Get content suggestions for a topic')
8
- .argument('<topic>', 'Topic to get suggestions for')
9
- .option('-p, --platform <platform>', 'Target platform')
10
- .action(async (topic, opts) => {
11
- await withIpc(async (client) => {
12
- // Get matching strategies
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- const strategies: any[] = await client.request('strategy.suggest', { query: topic, limit: 3 }) as any[];
15
-
16
- // Get matching templates
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- const templates: any[] = (opts.platform
19
- ? await client.request('template.byPlatform', { platform: opts.platform, limit: 3 })
20
- : await client.request('template.find', { query: topic, limit: 3 })) as any[];
21
-
22
- // Check rules
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- const ruleCheck: any = await client.request('rule.check', {
25
- content: topic,
26
- platform: opts.platform ?? 'any',
27
- });
28
-
29
- console.log(` ${icons.insight} ${c.heading(`Suggestions for: ${topic}`)}`);
30
- console.log();
31
-
32
- if (strategies.length > 0) {
33
- console.log(` ${c.blue.bold('Strategies:')}`);
34
- for (const s of strategies) {
35
- console.log(` ${icons.arrow} ${s.description} ${c.dim(`(${(s.confidence * 100).toFixed(0)}% confidence)`)}`);
36
- }
37
- console.log();
38
- }
39
-
40
- if (templates.length > 0) {
41
- console.log(` ${c.purple.bold('Templates:')}`);
42
- for (const t of templates) {
43
- console.log(` ${icons.arrow} ${t.name} ${c.dim(`(${t.platform ?? 'any'}, used ${t.use_count}x)`)}`);
44
- }
45
- console.log();
46
- }
47
-
48
- if (ruleCheck.recommendations?.length > 0) {
49
- console.log(` ${c.orange.bold('Rules to consider:')}`);
50
- for (const r of ruleCheck.recommendations) {
51
- console.log(` ${icons.arrow} ${r.suggestion}`);
52
- }
53
- console.log();
54
- }
55
-
56
- if (ruleCheck.violations?.length > 0) {
57
- console.log(` ${c.red.bold('Warnings:')}`);
58
- for (const v of ruleCheck.violations) {
59
- console.log(` ${icons.warn} ${v.reason}`);
60
- }
61
- }
62
- });
63
- });
64
- }
@@ -1,22 +0,0 @@
1
- import { IpcClient } from '../ipc/client.js';
2
- import { getPipeName } from '../utils/paths.js';
3
- import { c, icons } from './colors.js';
4
-
5
- export async function withIpc<T>(fn: (client: IpcClient) => Promise<T>): Promise<T> {
6
- const client = new IpcClient(getPipeName(), 5000);
7
- try {
8
- await client.connect();
9
- return await fn(client);
10
- } catch (err) {
11
- if (err instanceof Error && err.message.includes('ENOENT')) {
12
- console.error(`${icons.error} ${c.error('Marketing Brain daemon is not running.')} Start it with: ${c.cyan('marketing start')}`);
13
- } else if (err instanceof Error && err.message.includes('ECONNREFUSED')) {
14
- console.error(`${icons.error} ${c.error('Marketing Brain daemon is not responding.')} Try: ${c.cyan('marketing stop && marketing start')}`);
15
- } else {
16
- console.error(`${icons.error} ${c.error(err instanceof Error ? err.message : String(err))}`);
17
- }
18
- process.exit(1);
19
- } finally {
20
- client.disconnect();
21
- }
22
- }
@@ -1,63 +0,0 @@
1
- import https from 'node:https';
2
- import { c, icons } from './colors.js';
3
-
4
- // Read current version from package.json at build time
5
- import { createRequire } from 'node:module';
6
- const require = createRequire(import.meta.url);
7
- const pkg = require('../../package.json');
8
- const CURRENT_VERSION: string = pkg.version;
9
-
10
- export function getCurrentVersion(): string {
11
- return CURRENT_VERSION;
12
- }
13
-
14
- function fetchLatestVersion(): Promise<string | null> {
15
- return new Promise((resolve) => {
16
- const timeout = setTimeout(() => resolve(null), 3000);
17
-
18
- const req = https.get(
19
- 'https://registry.npmjs.org/@timmeck/marketing-brain/latest',
20
- { headers: { Accept: 'application/json' } },
21
- (res) => {
22
- let data = '';
23
- res.on('data', (chunk) => { data += chunk; });
24
- res.on('end', () => {
25
- clearTimeout(timeout);
26
- try {
27
- const json = JSON.parse(data);
28
- resolve(json.version ?? null);
29
- } catch {
30
- resolve(null);
31
- }
32
- });
33
- },
34
- );
35
- req.on('error', () => {
36
- clearTimeout(timeout);
37
- resolve(null);
38
- });
39
- });
40
- }
41
-
42
- function isNewer(latest: string, current: string): boolean {
43
- const l = latest.split('.').map(Number);
44
- const cur = current.split('.').map(Number);
45
- for (let i = 0; i < 3; i++) {
46
- if ((l[i] ?? 0) > (cur[i] ?? 0)) return true;
47
- if ((l[i] ?? 0) < (cur[i] ?? 0)) return false;
48
- }
49
- return false;
50
- }
51
-
52
- export async function checkForUpdate(): Promise<void> {
53
- try {
54
- const latest = await fetchLatestVersion();
55
- if (latest && isNewer(latest, CURRENT_VERSION)) {
56
- console.log();
57
- console.log(` ${icons.star} ${c.orange.bold(`Update available: v${CURRENT_VERSION} → v${latest}`)}`);
58
- console.log(` Run: ${c.cyan('npm update -g @timmeck/marketing-brain')}`);
59
- }
60
- } catch {
61
- // silently ignore — update check is best-effort
62
- }
63
- }