@timmeck/marketing-brain 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +9 -0
- package/README.md +342 -0
- package/dashboard.html +666 -0
- package/dist/api/server.d.ts +15 -0
- package/dist/api/server.js +73 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli/colors.d.ts +43 -0
- package/dist/cli/colors.js +54 -0
- package/dist/cli/colors.js.map +1 -0
- package/dist/cli/commands/campaign.d.ts +2 -0
- package/dist/cli/commands/campaign.js +62 -0
- package/dist/cli/commands/campaign.js.map +1 -0
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +164 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/dashboard.d.ts +2 -0
- package/dist/cli/commands/dashboard.js +147 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +111 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts +2 -0
- package/dist/cli/commands/export.js +37 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/import.d.ts +2 -0
- package/dist/cli/commands/import.js +76 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/insights.d.ts +2 -0
- package/dist/cli/commands/insights.js +41 -0
- package/dist/cli/commands/insights.js.map +1 -0
- package/dist/cli/commands/learn.d.ts +2 -0
- package/dist/cli/commands/learn.js +22 -0
- package/dist/cli/commands/learn.js.map +1 -0
- package/dist/cli/commands/network.d.ts +2 -0
- package/dist/cli/commands/network.js +66 -0
- package/dist/cli/commands/network.js.map +1 -0
- package/dist/cli/commands/post.d.ts +2 -0
- package/dist/cli/commands/post.js +45 -0
- package/dist/cli/commands/post.js.map +1 -0
- package/dist/cli/commands/query.d.ts +2 -0
- package/dist/cli/commands/query.js +96 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +2 -0
- package/dist/cli/commands/rules.js +25 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.js +91 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +63 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +2 -0
- package/dist/cli/commands/stop.js +34 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/suggest.d.ts +2 -0
- package/dist/cli/commands/suggest.js +57 -0
- package/dist/cli/commands/suggest.js.map +1 -0
- package/dist/cli/ipc-helper.d.ts +2 -0
- package/dist/cli/ipc-helper.js +26 -0
- package/dist/cli/ipc-helper.js.map +1 -0
- package/dist/cli/update-check.d.ts +2 -0
- package/dist/cli/update-check.js +58 -0
- package/dist/cli/update-check.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +111 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/renderer.d.ts +11 -0
- package/dist/dashboard/renderer.js +112 -0
- package/dist/dashboard/renderer.js.map +1 -0
- package/dist/dashboard/server.d.ts +15 -0
- package/dist/dashboard/server.js +122 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +19 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrations/001_core_schema.d.ts +2 -0
- package/dist/db/migrations/001_core_schema.js +62 -0
- package/dist/db/migrations/001_core_schema.js.map +1 -0
- package/dist/db/migrations/002_learning_schema.d.ts +2 -0
- package/dist/db/migrations/002_learning_schema.js +45 -0
- package/dist/db/migrations/002_learning_schema.js.map +1 -0
- package/dist/db/migrations/003_synapse_schema.d.ts +2 -0
- package/dist/db/migrations/003_synapse_schema.js +26 -0
- package/dist/db/migrations/003_synapse_schema.js.map +1 -0
- package/dist/db/migrations/004_insights_schema.d.ts +2 -0
- package/dist/db/migrations/004_insights_schema.js +37 -0
- package/dist/db/migrations/004_insights_schema.js.map +1 -0
- package/dist/db/migrations/005_fts_indexes.d.ts +2 -0
- package/dist/db/migrations/005_fts_indexes.js +76 -0
- package/dist/db/migrations/005_fts_indexes.js.map +1 -0
- package/dist/db/migrations/index.d.ts +2 -0
- package/dist/db/migrations/index.js +47 -0
- package/dist/db/migrations/index.js.map +1 -0
- package/dist/db/repositories/audience.repository.d.ts +18 -0
- package/dist/db/repositories/audience.repository.js +45 -0
- package/dist/db/repositories/audience.repository.js.map +1 -0
- package/dist/db/repositories/campaign.repository.d.ts +15 -0
- package/dist/db/repositories/campaign.repository.js +58 -0
- package/dist/db/repositories/campaign.repository.js.map +1 -0
- package/dist/db/repositories/engagement.repository.d.ts +26 -0
- package/dist/db/repositories/engagement.repository.js +83 -0
- package/dist/db/repositories/engagement.repository.js.map +1 -0
- package/dist/db/repositories/insight.repository.d.ts +18 -0
- package/dist/db/repositories/insight.repository.js +87 -0
- package/dist/db/repositories/insight.repository.js.map +1 -0
- package/dist/db/repositories/post.repository.d.ts +21 -0
- package/dist/db/repositories/post.repository.js +105 -0
- package/dist/db/repositories/post.repository.js.map +1 -0
- package/dist/db/repositories/rule.repository.d.ts +16 -0
- package/dist/db/repositories/rule.repository.js +71 -0
- package/dist/db/repositories/rule.repository.js.map +1 -0
- package/dist/db/repositories/strategy.repository.d.ts +16 -0
- package/dist/db/repositories/strategy.repository.js +69 -0
- package/dist/db/repositories/strategy.repository.js.map +1 -0
- package/dist/db/repositories/synapse.repository.d.ts +25 -0
- package/dist/db/repositories/synapse.repository.js +115 -0
- package/dist/db/repositories/synapse.repository.js.map +1 -0
- package/dist/db/repositories/template.repository.d.ts +16 -0
- package/dist/db/repositories/template.repository.js +61 -0
- package/dist/db/repositories/template.repository.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/client.d.ts +13 -0
- package/dist/ipc/client.js +93 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/protocol.d.ts +8 -0
- package/dist/ipc/protocol.js +29 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/router.d.ts +30 -0
- package/dist/ipc/router.js +88 -0
- package/dist/ipc/router.js.map +1 -0
- package/dist/ipc/server.d.ts +14 -0
- package/dist/ipc/server.js +130 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/learning/confidence-scorer.d.ts +17 -0
- package/dist/learning/confidence-scorer.js +26 -0
- package/dist/learning/confidence-scorer.js.map +1 -0
- package/dist/learning/learning-engine.d.ts +33 -0
- package/dist/learning/learning-engine.js +211 -0
- package/dist/learning/learning-engine.js.map +1 -0
- package/dist/marketing-core.d.ts +17 -0
- package/dist/marketing-core.js +233 -0
- package/dist/marketing-core.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +67 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +3 -0
- package/dist/mcp/tools.js +138 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/research/research-engine.d.ts +28 -0
- package/dist/research/research-engine.js +211 -0
- package/dist/research/research-engine.js.map +1 -0
- package/dist/services/analytics.service.d.ts +116 -0
- package/dist/services/analytics.service.js +69 -0
- package/dist/services/analytics.service.js.map +1 -0
- package/dist/services/audience.service.d.ts +20 -0
- package/dist/services/audience.service.js +30 -0
- package/dist/services/audience.service.js.map +1 -0
- package/dist/services/campaign.service.d.ts +27 -0
- package/dist/services/campaign.service.js +65 -0
- package/dist/services/campaign.service.js.map +1 -0
- package/dist/services/insight.service.d.ts +18 -0
- package/dist/services/insight.service.js +40 -0
- package/dist/services/insight.service.js.map +1 -0
- package/dist/services/post.service.d.ts +48 -0
- package/dist/services/post.service.js +93 -0
- package/dist/services/post.service.js.map +1 -0
- package/dist/services/rule.service.d.ts +29 -0
- package/dist/services/rule.service.js +67 -0
- package/dist/services/rule.service.js.map +1 -0
- package/dist/services/strategy.service.d.ts +17 -0
- package/dist/services/strategy.service.js +39 -0
- package/dist/services/strategy.service.js.map +1 -0
- package/dist/services/synapse.service.d.ts +22 -0
- package/dist/services/synapse.service.js +22 -0
- package/dist/services/synapse.service.js.map +1 -0
- package/dist/services/template.service.d.ts +17 -0
- package/dist/services/template.service.js +37 -0
- package/dist/services/template.service.js.map +1 -0
- package/dist/synapses/activation.d.ts +13 -0
- package/dist/synapses/activation.js +50 -0
- package/dist/synapses/activation.js.map +1 -0
- package/dist/synapses/decay.d.ts +11 -0
- package/dist/synapses/decay.js +27 -0
- package/dist/synapses/decay.js.map +1 -0
- package/dist/synapses/hebbian.d.ts +13 -0
- package/dist/synapses/hebbian.js +35 -0
- package/dist/synapses/hebbian.js.map +1 -0
- package/dist/synapses/pathfinder.d.ts +14 -0
- package/dist/synapses/pathfinder.js +50 -0
- package/dist/synapses/pathfinder.js.map +1 -0
- package/dist/synapses/synapse-manager.d.ts +32 -0
- package/dist/synapses/synapse-manager.js +76 -0
- package/dist/synapses/synapse-manager.js.map +1 -0
- package/dist/types/config.types.d.ts +69 -0
- package/dist/types/config.types.js +2 -0
- package/dist/types/config.types.js.map +1 -0
- package/dist/types/ipc.types.d.ts +11 -0
- package/dist/types/ipc.types.js +2 -0
- package/dist/types/ipc.types.js.map +1 -0
- package/dist/types/post.types.d.ts +141 -0
- package/dist/types/post.types.js +2 -0
- package/dist/types/post.types.js.map +1 -0
- package/dist/types/synapse.types.d.ts +23 -0
- package/dist/types/synapse.types.js +2 -0
- package/dist/types/synapse.types.js.map +1 -0
- package/dist/utils/events.d.ts +57 -0
- package/dist/utils/events.js +23 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +5 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +39 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +3 -0
- package/dist/utils/paths.js +18 -0
- package/dist/utils/paths.js.map +1 -0
- package/package.json +40 -0
- package/seed-data.json +78 -0
- package/src/api/server.ts +86 -0
- package/src/cli/colors.ts +59 -0
- package/src/cli/commands/campaign.ts +66 -0
- package/src/cli/commands/config.ts +168 -0
- package/src/cli/commands/dashboard.ts +165 -0
- package/src/cli/commands/doctor.ts +110 -0
- package/src/cli/commands/export.ts +40 -0
- package/src/cli/commands/import.ts +84 -0
- package/src/cli/commands/insights.ts +44 -0
- package/src/cli/commands/learn.ts +24 -0
- package/src/cli/commands/network.ts +71 -0
- package/src/cli/commands/post.ts +47 -0
- package/src/cli/commands/query.ts +108 -0
- package/src/cli/commands/rules.ts +27 -0
- package/src/cli/commands/start.ts +100 -0
- package/src/cli/commands/status.ts +73 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/commands/suggest.ts +64 -0
- package/src/cli/ipc-helper.ts +22 -0
- package/src/cli/update-check.ts +63 -0
- package/src/config.ts +110 -0
- package/src/dashboard/renderer.ts +136 -0
- package/src/dashboard/server.ts +140 -0
- package/src/db/connection.ts +22 -0
- package/src/db/migrations/001_core_schema.ts +63 -0
- package/src/db/migrations/002_learning_schema.ts +46 -0
- package/src/db/migrations/003_synapse_schema.ts +27 -0
- package/src/db/migrations/004_insights_schema.ts +38 -0
- package/src/db/migrations/005_fts_indexes.ts +77 -0
- package/src/db/migrations/index.ts +62 -0
- package/src/db/repositories/audience.repository.ts +53 -0
- package/src/db/repositories/campaign.repository.ts +72 -0
- package/src/db/repositories/engagement.repository.ts +108 -0
- package/src/db/repositories/insight.repository.ts +100 -0
- package/src/db/repositories/post.repository.ts +123 -0
- package/src/db/repositories/rule.repository.ts +87 -0
- package/src/db/repositories/strategy.repository.ts +82 -0
- package/src/db/repositories/synapse.repository.ts +148 -0
- package/src/db/repositories/template.repository.ts +76 -0
- package/src/index.ts +69 -0
- package/src/ipc/client.ts +110 -0
- package/src/ipc/protocol.ts +35 -0
- package/src/ipc/router.ts +126 -0
- package/src/ipc/server.ts +140 -0
- package/src/learning/confidence-scorer.ts +36 -0
- package/src/learning/learning-engine.ts +254 -0
- package/src/marketing-core.ts +285 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/tools.ts +216 -0
- package/src/research/research-engine.ts +226 -0
- package/src/services/analytics.service.ts +73 -0
- package/src/services/audience.service.ts +40 -0
- package/src/services/campaign.service.ts +80 -0
- package/src/services/insight.service.ts +54 -0
- package/src/services/post.service.ts +116 -0
- package/src/services/rule.service.ts +90 -0
- package/src/services/strategy.service.ts +53 -0
- package/src/services/synapse.service.ts +32 -0
- package/src/services/template.service.ts +50 -0
- package/src/synapses/activation.ts +80 -0
- package/src/synapses/decay.ts +38 -0
- package/src/synapses/hebbian.ts +68 -0
- package/src/synapses/pathfinder.ts +81 -0
- package/src/synapses/synapse-manager.ts +115 -0
- package/src/types/config.types.ts +79 -0
- package/src/types/ipc.types.ts +8 -0
- package/src/types/post.types.ts +156 -0
- package/src/types/synapse.types.ts +43 -0
- package/src/utils/events.ts +44 -0
- package/src/utils/hash.ts +5 -0
- package/src/utils/logger.ts +48 -0
- package/src/utils/paths.ts +19 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
}
|