@timmeck/marketing-brain 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/cli/colors.d.ts +11 -24
  2. package/dist/cli/colors.js +3 -46
  3. package/dist/cli/colors.js.map +1 -1
  4. package/dist/cli/commands/peers.d.ts +2 -0
  5. package/dist/cli/commands/peers.js +38 -0
  6. package/dist/cli/commands/peers.js.map +1 -0
  7. package/dist/db/connection.d.ts +1 -2
  8. package/dist/db/connection.js +1 -18
  9. package/dist/db/connection.js.map +1 -1
  10. package/dist/hooks/post-tool-use.d.ts +2 -0
  11. package/dist/hooks/post-tool-use.js +182 -0
  12. package/dist/hooks/post-tool-use.js.map +1 -0
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/ipc/client.d.ts +1 -13
  16. package/dist/ipc/client.js +1 -92
  17. package/dist/ipc/client.js.map +1 -1
  18. package/dist/ipc/protocol.d.ts +1 -8
  19. package/dist/ipc/protocol.js +1 -28
  20. package/dist/ipc/protocol.js.map +1 -1
  21. package/dist/ipc/router.js +8 -0
  22. package/dist/ipc/router.js.map +1 -1
  23. package/dist/ipc/server.d.ts +1 -14
  24. package/dist/ipc/server.js +1 -129
  25. package/dist/ipc/server.js.map +1 -1
  26. package/dist/marketing-core.d.ts +1 -0
  27. package/dist/marketing-core.js +6 -1
  28. package/dist/marketing-core.js.map +1 -1
  29. package/dist/mcp/server.js +5 -60
  30. package/dist/mcp/server.js.map +1 -1
  31. package/dist/types/ipc.types.d.ts +1 -11
  32. package/dist/utils/events.d.ts +4 -8
  33. package/dist/utils/events.js +2 -14
  34. package/dist/utils/events.js.map +1 -1
  35. package/dist/utils/hash.d.ts +1 -1
  36. package/dist/utils/hash.js +1 -4
  37. package/dist/utils/hash.js.map +1 -1
  38. package/dist/utils/logger.d.ts +3 -2
  39. package/dist/utils/logger.js +8 -35
  40. package/dist/utils/logger.js.map +1 -1
  41. package/dist/utils/paths.d.ts +2 -1
  42. package/dist/utils/paths.js +4 -13
  43. package/dist/utils/paths.js.map +1 -1
  44. package/package.json +2 -1
  45. package/.github/FUNDING.yml +0 -1
  46. package/.github/workflows/ci.yml +0 -27
  47. package/.mcp.json +0 -9
  48. package/src/api/server.ts +0 -86
  49. package/src/cli/colors.ts +0 -59
  50. package/src/cli/commands/campaign.ts +0 -66
  51. package/src/cli/commands/config.ts +0 -168
  52. package/src/cli/commands/dashboard.ts +0 -165
  53. package/src/cli/commands/doctor.ts +0 -110
  54. package/src/cli/commands/export.ts +0 -40
  55. package/src/cli/commands/import.ts +0 -84
  56. package/src/cli/commands/insights.ts +0 -44
  57. package/src/cli/commands/learn.ts +0 -24
  58. package/src/cli/commands/network.ts +0 -71
  59. package/src/cli/commands/post.ts +0 -47
  60. package/src/cli/commands/query.ts +0 -108
  61. package/src/cli/commands/rules.ts +0 -27
  62. package/src/cli/commands/start.ts +0 -100
  63. package/src/cli/commands/status.ts +0 -73
  64. package/src/cli/commands/stop.ts +0 -33
  65. package/src/cli/commands/suggest.ts +0 -64
  66. package/src/cli/ipc-helper.ts +0 -22
  67. package/src/cli/update-check.ts +0 -63
  68. package/src/config.ts +0 -110
  69. package/src/dashboard/renderer.ts +0 -136
  70. package/src/dashboard/server.ts +0 -140
  71. package/src/db/connection.ts +0 -22
  72. package/src/db/migrations/001_core_schema.ts +0 -63
  73. package/src/db/migrations/002_learning_schema.ts +0 -46
  74. package/src/db/migrations/003_synapse_schema.ts +0 -27
  75. package/src/db/migrations/004_insights_schema.ts +0 -38
  76. package/src/db/migrations/005_fts_indexes.ts +0 -77
  77. package/src/db/migrations/index.ts +0 -62
  78. package/src/db/repositories/audience.repository.ts +0 -53
  79. package/src/db/repositories/campaign.repository.ts +0 -72
  80. package/src/db/repositories/engagement.repository.ts +0 -108
  81. package/src/db/repositories/insight.repository.ts +0 -100
  82. package/src/db/repositories/post.repository.ts +0 -123
  83. package/src/db/repositories/rule.repository.ts +0 -87
  84. package/src/db/repositories/strategy.repository.ts +0 -82
  85. package/src/db/repositories/synapse.repository.ts +0 -148
  86. package/src/db/repositories/template.repository.ts +0 -76
  87. package/src/index.ts +0 -69
  88. package/src/ipc/__tests__/protocol.test.ts +0 -153
  89. package/src/ipc/client.ts +0 -110
  90. package/src/ipc/protocol.ts +0 -35
  91. package/src/ipc/router.ts +0 -126
  92. package/src/ipc/server.ts +0 -140
  93. package/src/learning/confidence-scorer.ts +0 -36
  94. package/src/learning/learning-engine.ts +0 -254
  95. package/src/marketing-core.ts +0 -285
  96. package/src/mcp/server.ts +0 -72
  97. package/src/mcp/tools.ts +0 -216
  98. package/src/research/research-engine.ts +0 -226
  99. package/src/services/analytics.service.ts +0 -73
  100. package/src/services/audience.service.ts +0 -40
  101. package/src/services/campaign.service.ts +0 -80
  102. package/src/services/insight.service.ts +0 -54
  103. package/src/services/post.service.ts +0 -116
  104. package/src/services/rule.service.ts +0 -90
  105. package/src/services/strategy.service.ts +0 -53
  106. package/src/services/synapse.service.ts +0 -32
  107. package/src/services/template.service.ts +0 -50
  108. package/src/synapses/activation.ts +0 -80
  109. package/src/synapses/decay.ts +0 -38
  110. package/src/synapses/hebbian.ts +0 -68
  111. package/src/synapses/pathfinder.ts +0 -81
  112. package/src/synapses/synapse-manager.ts +0 -115
  113. package/src/types/config.types.ts +0 -79
  114. package/src/types/ipc.types.ts +0 -8
  115. package/src/types/post.types.ts +0 -156
  116. package/src/types/synapse.types.ts +0 -43
  117. package/src/utils/__tests__/hash.test.ts +0 -39
  118. package/src/utils/__tests__/paths.test.ts +0 -70
  119. package/src/utils/events.ts +0 -44
  120. package/src/utils/hash.ts +0 -5
  121. package/src/utils/logger.ts +0 -48
  122. package/src/utils/paths.ts +0 -19
  123. package/tsconfig.json +0 -18
@@ -1,27 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
-
3
- export function up(db: Database.Database): void {
4
- db.exec(`
5
- CREATE TABLE IF NOT EXISTS synapses (
6
- id INTEGER PRIMARY KEY AUTOINCREMENT,
7
- source_type TEXT NOT NULL,
8
- source_id INTEGER NOT NULL,
9
- target_type TEXT NOT NULL,
10
- target_id INTEGER NOT NULL,
11
- synapse_type TEXT NOT NULL,
12
- weight REAL NOT NULL DEFAULT 0.5,
13
- activation_count INTEGER NOT NULL DEFAULT 1,
14
- last_activated_at TEXT NOT NULL DEFAULT (datetime('now')),
15
- metadata TEXT,
16
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
17
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
18
- UNIQUE(source_type, source_id, target_type, target_id, synapse_type)
19
- );
20
-
21
- CREATE INDEX IF NOT EXISTS idx_synapses_source ON synapses(source_type, source_id);
22
- CREATE INDEX IF NOT EXISTS idx_synapses_target ON synapses(target_type, target_id);
23
- CREATE INDEX IF NOT EXISTS idx_synapses_type ON synapses(synapse_type);
24
- CREATE INDEX IF NOT EXISTS idx_synapses_weight ON synapses(weight);
25
- CREATE INDEX IF NOT EXISTS idx_synapses_last_activated ON synapses(last_activated_at);
26
- `);
27
- }
@@ -1,38 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
-
3
- export function up(db: Database.Database): void {
4
- db.exec(`
5
- CREATE TABLE IF NOT EXISTS insights (
6
- id INTEGER PRIMARY KEY AUTOINCREMENT,
7
- type TEXT NOT NULL,
8
- title TEXT NOT NULL,
9
- description TEXT NOT NULL,
10
- confidence REAL NOT NULL DEFAULT 0.5,
11
- priority INTEGER NOT NULL DEFAULT 0,
12
- campaign_id INTEGER,
13
- active INTEGER NOT NULL DEFAULT 1,
14
- expires_at TEXT,
15
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
16
- FOREIGN KEY (campaign_id) REFERENCES campaigns(id) ON DELETE SET NULL
17
- );
18
-
19
- CREATE TABLE IF NOT EXISTS notifications (
20
- id INTEGER PRIMARY KEY AUTOINCREMENT,
21
- type TEXT NOT NULL,
22
- title TEXT NOT NULL,
23
- message TEXT NOT NULL,
24
- priority INTEGER NOT NULL DEFAULT 0,
25
- campaign_id INTEGER,
26
- acknowledged INTEGER NOT NULL DEFAULT 0,
27
- acknowledged_at TEXT,
28
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
29
- FOREIGN KEY (campaign_id) REFERENCES campaigns(id) ON DELETE SET NULL
30
- );
31
-
32
- CREATE INDEX IF NOT EXISTS idx_insights_type ON insights(type);
33
- CREATE INDEX IF NOT EXISTS idx_insights_campaign ON insights(campaign_id);
34
- CREATE INDEX IF NOT EXISTS idx_insights_active ON insights(active);
35
- CREATE INDEX IF NOT EXISTS idx_insights_priority ON insights(priority);
36
- CREATE INDEX IF NOT EXISTS idx_notifications_acknowledged ON notifications(acknowledged);
37
- `);
38
- }
@@ -1,77 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
-
3
- export function up(db: Database.Database): void {
4
- db.exec(`
5
- -- Full-text search for posts
6
- CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
7
- content, hashtags, platform,
8
- content='posts',
9
- content_rowid='id'
10
- );
11
-
12
- CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN
13
- INSERT INTO posts_fts(rowid, content, hashtags, platform)
14
- VALUES (new.id, new.content, new.hashtags, new.platform);
15
- END;
16
-
17
- CREATE TRIGGER IF NOT EXISTS posts_ad AFTER DELETE ON posts BEGIN
18
- INSERT INTO posts_fts(posts_fts, rowid, content, hashtags, platform)
19
- VALUES ('delete', old.id, old.content, old.hashtags, old.platform);
20
- END;
21
-
22
- CREATE TRIGGER IF NOT EXISTS posts_au AFTER UPDATE ON posts BEGIN
23
- INSERT INTO posts_fts(posts_fts, rowid, content, hashtags, platform)
24
- VALUES ('delete', old.id, old.content, old.hashtags, old.platform);
25
- INSERT INTO posts_fts(rowid, content, hashtags, platform)
26
- VALUES (new.id, new.content, new.hashtags, new.platform);
27
- END;
28
-
29
- -- Full-text search for strategies
30
- CREATE VIRTUAL TABLE IF NOT EXISTS strategies_fts USING fts5(
31
- description, approach, outcome,
32
- content='strategies',
33
- content_rowid='id'
34
- );
35
-
36
- CREATE TRIGGER IF NOT EXISTS strategies_ai AFTER INSERT ON strategies BEGIN
37
- INSERT INTO strategies_fts(rowid, description, approach, outcome)
38
- VALUES (new.id, new.description, new.approach, new.outcome);
39
- END;
40
-
41
- CREATE TRIGGER IF NOT EXISTS strategies_ad AFTER DELETE ON strategies BEGIN
42
- INSERT INTO strategies_fts(strategies_fts, rowid, description, approach, outcome)
43
- VALUES ('delete', old.id, old.description, old.approach, old.outcome);
44
- END;
45
-
46
- CREATE TRIGGER IF NOT EXISTS strategies_au AFTER UPDATE ON strategies BEGIN
47
- INSERT INTO strategies_fts(strategies_fts, rowid, description, approach, outcome)
48
- VALUES ('delete', old.id, old.description, old.approach, old.outcome);
49
- INSERT INTO strategies_fts(rowid, description, approach, outcome)
50
- VALUES (new.id, new.description, new.approach, new.outcome);
51
- END;
52
-
53
- -- Full-text search for content templates
54
- CREATE VIRTUAL TABLE IF NOT EXISTS content_templates_fts USING fts5(
55
- name, structure, example,
56
- content='content_templates',
57
- content_rowid='id'
58
- );
59
-
60
- CREATE TRIGGER IF NOT EXISTS templates_ai AFTER INSERT ON content_templates BEGIN
61
- INSERT INTO content_templates_fts(rowid, name, structure, example)
62
- VALUES (new.id, new.name, new.structure, new.example);
63
- END;
64
-
65
- CREATE TRIGGER IF NOT EXISTS templates_ad AFTER DELETE ON content_templates BEGIN
66
- INSERT INTO content_templates_fts(content_templates_fts, rowid, name, structure, example)
67
- VALUES ('delete', old.id, old.name, old.structure, old.example);
68
- END;
69
-
70
- CREATE TRIGGER IF NOT EXISTS templates_au AFTER UPDATE ON content_templates BEGIN
71
- INSERT INTO content_templates_fts(content_templates_fts, rowid, name, structure, example)
72
- VALUES ('delete', old.id, old.name, old.structure, old.example);
73
- INSERT INTO content_templates_fts(rowid, name, structure, example)
74
- VALUES (new.id, new.name, new.structure, new.example);
75
- END;
76
- `);
77
- }
@@ -1,62 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
- import { getLogger } from '../../utils/logger.js';
3
- import { up as coreSchema } from './001_core_schema.js';
4
- import { up as learningSchema } from './002_learning_schema.js';
5
- import { up as synapseSchema } from './003_synapse_schema.js';
6
- import { up as insightsSchema } from './004_insights_schema.js';
7
- import { up as ftsIndexes } from './005_fts_indexes.js';
8
-
9
- interface Migration {
10
- version: number;
11
- name: string;
12
- up: (db: Database.Database) => void;
13
- }
14
-
15
- const migrations: Migration[] = [
16
- { version: 1, name: '001_core_schema', up: coreSchema },
17
- { version: 2, name: '002_learning_schema', up: learningSchema },
18
- { version: 3, name: '003_synapse_schema', up: synapseSchema },
19
- { version: 4, name: '004_insights_schema', up: insightsSchema },
20
- { version: 5, name: '005_fts_indexes', up: ftsIndexes },
21
- ];
22
-
23
- function ensureMigrationsTable(db: Database.Database): void {
24
- db.exec(`
25
- CREATE TABLE IF NOT EXISTS migrations (
26
- version INTEGER PRIMARY KEY,
27
- name TEXT NOT NULL,
28
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
29
- );
30
- `);
31
- }
32
-
33
- function getCurrentVersion(db: Database.Database): number {
34
- const row = db.prepare('SELECT MAX(version) as version FROM migrations').get() as { version: number | null } | undefined;
35
- return row?.version ?? 0;
36
- }
37
-
38
- export function runMigrations(db: Database.Database): void {
39
- const logger = getLogger();
40
- ensureMigrationsTable(db);
41
-
42
- const currentVersion = getCurrentVersion(db);
43
- const pending = migrations.filter(m => m.version > currentVersion);
44
-
45
- if (pending.length === 0) {
46
- logger.info('Database is up to date');
47
- return;
48
- }
49
-
50
- logger.info(`Running ${pending.length} migration(s) from version ${currentVersion}`);
51
-
52
- const runAll = db.transaction(() => {
53
- for (const migration of pending) {
54
- logger.info(`Applying migration ${migration.name}`);
55
- migration.up(db);
56
- db.prepare('INSERT INTO migrations (version, name) VALUES (?, ?)').run(migration.version, migration.name);
57
- }
58
- });
59
-
60
- runAll();
61
- logger.info(`Migrations complete. Now at version ${pending[pending.length - 1]!.version}`);
62
- }
@@ -1,53 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
- import type { Statement } from 'better-sqlite3';
3
- import type { Audience } from '../../types/post.types.js';
4
-
5
- export class AudienceRepository {
6
- private stmts: Record<string, Statement>;
7
-
8
- constructor(private db: Database.Database) {
9
- this.stmts = {
10
- create: db.prepare(`
11
- INSERT INTO audiences (name, platform, demographics, interests)
12
- VALUES (@name, @platform, @demographics, @interests)
13
- `),
14
- getById: db.prepare('SELECT * FROM audiences WHERE id = ?'),
15
- getByName: db.prepare('SELECT * FROM audiences WHERE name = ?'),
16
- listAll: db.prepare('SELECT * FROM audiences ORDER BY created_at DESC'),
17
- countAll: db.prepare('SELECT COUNT(*) as count FROM audiences'),
18
- delete: db.prepare('DELETE FROM audiences WHERE id = ?'),
19
- };
20
- }
21
-
22
- create(data: { name: string; platform?: string; demographics?: string; interests?: string }): number {
23
- const result = this.stmts.create.run({
24
- name: data.name,
25
- platform: data.platform ?? null,
26
- demographics: data.demographics ?? null,
27
- interests: data.interests ?? null,
28
- });
29
- return result.lastInsertRowid as number;
30
- }
31
-
32
- getById(id: number): Audience | undefined {
33
- return this.stmts.getById.get(id) as Audience | undefined;
34
- }
35
-
36
- getByName(name: string): Audience | undefined {
37
- return this.stmts.getByName.get(name) as Audience | undefined;
38
- }
39
-
40
- listAll(): Audience[] {
41
- return this.stmts.listAll.all() as Audience[];
42
- }
43
-
44
- countAll(): number {
45
- const row = this.stmts.countAll.get() as { count: number };
46
- return row.count;
47
- }
48
-
49
- delete(id: number): boolean {
50
- const result = this.stmts.delete.run(id);
51
- return result.changes > 0;
52
- }
53
- }
@@ -1,72 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
- import type { Statement } from 'better-sqlite3';
3
- import type { Campaign, CampaignCreate } from '../../types/post.types.js';
4
-
5
- export class CampaignRepository {
6
- private stmts: Record<string, Statement>;
7
-
8
- constructor(private db: Database.Database) {
9
- this.stmts = {
10
- create: db.prepare(`
11
- INSERT INTO campaigns (name, brand, goal, platform)
12
- VALUES (@name, @brand, @goal, @platform)
13
- `),
14
- getById: db.prepare('SELECT * FROM campaigns WHERE id = ?'),
15
- getByName: db.prepare('SELECT * FROM campaigns WHERE name = ?'),
16
- listAll: db.prepare('SELECT * FROM campaigns ORDER BY created_at DESC'),
17
- listActive: db.prepare(`SELECT * FROM campaigns WHERE status = 'active' ORDER BY created_at DESC`),
18
- countAll: db.prepare('SELECT COUNT(*) as count FROM campaigns'),
19
- delete: db.prepare('DELETE FROM campaigns WHERE id = ?'),
20
- };
21
- }
22
-
23
- create(data: CampaignCreate): number {
24
- const result = this.stmts.create.run({
25
- name: data.name,
26
- brand: data.brand ?? null,
27
- goal: data.goal ?? null,
28
- platform: data.platform ?? null,
29
- });
30
- return result.lastInsertRowid as number;
31
- }
32
-
33
- getById(id: number): Campaign | undefined {
34
- return this.stmts.getById.get(id) as Campaign | undefined;
35
- }
36
-
37
- getByName(name: string): Campaign | undefined {
38
- return this.stmts.getByName.get(name) as Campaign | undefined;
39
- }
40
-
41
- listAll(): Campaign[] {
42
- return this.stmts.listAll.all() as Campaign[];
43
- }
44
-
45
- listActive(): Campaign[] {
46
- return this.stmts.listActive.all() as Campaign[];
47
- }
48
-
49
- update(id: number, data: Partial<Campaign>): boolean {
50
- const fields = Object.keys(data).filter(
51
- (key) => key !== 'id' && key !== 'created_at' && (data as Record<string, unknown>)[key] !== undefined
52
- );
53
- if (fields.length === 0) return false;
54
-
55
- const setClauses = fields.map((f) => `${f} = @${f}`).join(', ');
56
- const stmt = this.db.prepare(
57
- `UPDATE campaigns SET ${setClauses}, updated_at = datetime('now') WHERE id = @id`
58
- );
59
- const result = stmt.run({ ...data, id });
60
- return result.changes > 0;
61
- }
62
-
63
- countAll(): number {
64
- const row = this.stmts.countAll.get() as { count: number };
65
- return row.count;
66
- }
67
-
68
- delete(id: number): boolean {
69
- const result = this.stmts.delete.run(id);
70
- return result.changes > 0;
71
- }
72
- }
@@ -1,108 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
- import type { Statement } from 'better-sqlite3';
3
- import type { Engagement, EngagementCreate } from '../../types/post.types.js';
4
-
5
- export class EngagementRepository {
6
- private stmts: Record<string, Statement>;
7
-
8
- constructor(private db: Database.Database) {
9
- this.stmts = {
10
- create: db.prepare(`
11
- INSERT INTO engagement (post_id, likes, comments, shares, impressions, clicks, saves, reach)
12
- VALUES (@post_id, @likes, @comments, @shares, @impressions, @clicks, @saves, @reach)
13
- `),
14
- getById: db.prepare('SELECT * FROM engagement WHERE id = ?'),
15
- getByPost: db.prepare('SELECT * FROM engagement WHERE post_id = ? ORDER BY timestamp DESC LIMIT 1'),
16
- listByPost: db.prepare('SELECT * FROM engagement WHERE post_id = ? ORDER BY timestamp DESC'),
17
- getLatestPerPost: db.prepare(`
18
- SELECT e.* FROM engagement e
19
- INNER JOIN (
20
- SELECT post_id, MAX(timestamp) as max_ts FROM engagement GROUP BY post_id
21
- ) latest ON e.post_id = latest.post_id AND e.timestamp = latest.max_ts
22
- ORDER BY (e.likes + e.comments + e.shares + e.impressions) DESC
23
- LIMIT ?
24
- `),
25
- topPosts: db.prepare(`
26
- SELECT e.*, p.platform, p.content, p.published_at FROM engagement e
27
- INNER JOIN posts p ON p.id = e.post_id
28
- INNER JOIN (
29
- SELECT post_id, MAX(timestamp) as max_ts FROM engagement GROUP BY post_id
30
- ) latest ON e.post_id = latest.post_id AND e.timestamp = latest.max_ts
31
- WHERE p.status = 'published'
32
- ORDER BY (e.likes + e.comments * 3 + e.shares * 5 + e.clicks * 2) DESC
33
- LIMIT ?
34
- `),
35
- avgByPlatform: db.prepare(`
36
- SELECT p.platform,
37
- AVG(e.likes) as avg_likes,
38
- AVG(e.comments) as avg_comments,
39
- AVG(e.shares) as avg_shares,
40
- AVG(e.impressions) as avg_impressions,
41
- AVG(e.clicks) as avg_clicks,
42
- COUNT(DISTINCT e.post_id) as post_count
43
- FROM engagement e
44
- INNER JOIN posts p ON p.id = e.post_id
45
- INNER JOIN (
46
- SELECT post_id, MAX(timestamp) as max_ts FROM engagement GROUP BY post_id
47
- ) latest ON e.post_id = latest.post_id AND e.timestamp = latest.max_ts
48
- GROUP BY p.platform
49
- `),
50
- delete: db.prepare('DELETE FROM engagement WHERE id = ?'),
51
- };
52
- }
53
-
54
- create(data: EngagementCreate): number {
55
- const result = this.stmts.create.run({
56
- post_id: data.post_id,
57
- likes: data.likes ?? 0,
58
- comments: data.comments ?? 0,
59
- shares: data.shares ?? 0,
60
- impressions: data.impressions ?? 0,
61
- clicks: data.clicks ?? 0,
62
- saves: data.saves ?? 0,
63
- reach: data.reach ?? 0,
64
- });
65
- return result.lastInsertRowid as number;
66
- }
67
-
68
- getById(id: number): Engagement | undefined {
69
- return this.stmts.getById.get(id) as Engagement | undefined;
70
- }
71
-
72
- getLatestByPost(postId: number): Engagement | undefined {
73
- return this.stmts.getByPost.get(postId) as Engagement | undefined;
74
- }
75
-
76
- listByPost(postId: number): Engagement[] {
77
- return this.stmts.listByPost.all(postId) as Engagement[];
78
- }
79
-
80
- topPosts(limit: number = 10): Array<Engagement & { platform: string; content: string; published_at: string }> {
81
- return this.stmts.topPosts.all(limit) as Array<Engagement & { platform: string; content: string; published_at: string }>;
82
- }
83
-
84
- avgByPlatform(): Array<{
85
- platform: string;
86
- avg_likes: number;
87
- avg_comments: number;
88
- avg_shares: number;
89
- avg_impressions: number;
90
- avg_clicks: number;
91
- post_count: number;
92
- }> {
93
- return this.stmts.avgByPlatform.all() as Array<{
94
- platform: string;
95
- avg_likes: number;
96
- avg_comments: number;
97
- avg_shares: number;
98
- avg_impressions: number;
99
- avg_clicks: number;
100
- post_count: number;
101
- }>;
102
- }
103
-
104
- delete(id: number): boolean {
105
- const result = this.stmts.delete.run(id);
106
- return result.changes > 0;
107
- }
108
- }
@@ -1,100 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
- import type { Statement } from 'better-sqlite3';
3
- import type { Insight, InsightCreate } from '../../types/post.types.js';
4
-
5
- export class InsightRepository {
6
- private stmts: Record<string, Statement>;
7
-
8
- constructor(private db: Database.Database) {
9
- this.stmts = {
10
- create: db.prepare(`
11
- INSERT INTO insights (type, title, description, confidence, priority, campaign_id, expires_at)
12
- VALUES (@type, @title, @description, @confidence, @priority, @campaign_id, @expires_at)
13
- `),
14
- getById: db.prepare('SELECT * FROM insights WHERE id = ?'),
15
- listActive: db.prepare(`
16
- SELECT * FROM insights
17
- WHERE active = 1 AND (expires_at IS NULL OR expires_at > datetime('now'))
18
- ORDER BY priority DESC, confidence DESC
19
- LIMIT ?
20
- `),
21
- listAll: db.prepare('SELECT * FROM insights ORDER BY created_at DESC LIMIT ?'),
22
- listByType: db.prepare(`
23
- SELECT * FROM insights WHERE type = ? AND active = 1
24
- ORDER BY priority DESC LIMIT ?
25
- `),
26
- listByCampaign: db.prepare(`
27
- SELECT * FROM insights WHERE campaign_id = ? AND active = 1
28
- ORDER BY priority DESC
29
- `),
30
- countActive: db.prepare(`
31
- SELECT COUNT(*) as count FROM insights
32
- WHERE active = 1 AND (expires_at IS NULL OR expires_at > datetime('now'))
33
- `),
34
- countAll: db.prepare('SELECT COUNT(*) as count FROM insights'),
35
- deactivate: db.prepare('UPDATE insights SET active = 0 WHERE id = ?'),
36
- expireOld: db.prepare(`
37
- UPDATE insights SET active = 0
38
- WHERE active = 1 AND expires_at IS NOT NULL AND expires_at <= datetime('now')
39
- `),
40
- delete: db.prepare('DELETE FROM insights WHERE id = ?'),
41
- };
42
- }
43
-
44
- create(data: InsightCreate): number {
45
- const result = this.stmts.create.run({
46
- type: data.type,
47
- title: data.title,
48
- description: data.description,
49
- confidence: data.confidence ?? 0.5,
50
- priority: data.priority ?? 0,
51
- campaign_id: data.campaign_id ?? null,
52
- expires_at: data.expires_at ?? null,
53
- });
54
- return result.lastInsertRowid as number;
55
- }
56
-
57
- getById(id: number): Insight | undefined {
58
- return this.stmts.getById.get(id) as Insight | undefined;
59
- }
60
-
61
- listActive(limit: number = 20): Insight[] {
62
- return this.stmts.listActive.all(limit) as Insight[];
63
- }
64
-
65
- listAll(limit: number = 50): Insight[] {
66
- return this.stmts.listAll.all(limit) as Insight[];
67
- }
68
-
69
- listByType(type: string, limit: number = 20): Insight[] {
70
- return this.stmts.listByType.all(type, limit) as Insight[];
71
- }
72
-
73
- listByCampaign(campaignId: number): Insight[] {
74
- return this.stmts.listByCampaign.all(campaignId) as Insight[];
75
- }
76
-
77
- deactivate(id: number): void {
78
- this.stmts.deactivate.run(id);
79
- }
80
-
81
- expireOld(): number {
82
- const result = this.stmts.expireOld.run();
83
- return result.changes;
84
- }
85
-
86
- countActive(): number {
87
- const row = this.stmts.countActive.get() as { count: number };
88
- return row.count;
89
- }
90
-
91
- countAll(): number {
92
- const row = this.stmts.countAll.get() as { count: number };
93
- return row.count;
94
- }
95
-
96
- delete(id: number): boolean {
97
- const result = this.stmts.delete.run(id);
98
- return result.changes > 0;
99
- }
100
- }
@@ -1,123 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
- import type { Statement } from 'better-sqlite3';
3
- import type { Post, PostCreate } from '../../types/post.types.js';
4
- import { sha256 } from '../../utils/hash.js';
5
-
6
- export class PostRepository {
7
- private stmts: Record<string, Statement>;
8
-
9
- constructor(private db: Database.Database) {
10
- this.stmts = {
11
- create: db.prepare(`
12
- INSERT INTO posts (campaign_id, platform, content, format, hashtags, url, published_at, fingerprint, status)
13
- VALUES (@campaign_id, @platform, @content, @format, @hashtags, @url, @published_at, @fingerprint, @status)
14
- `),
15
- getById: db.prepare('SELECT * FROM posts WHERE id = ?'),
16
- getByFingerprint: db.prepare('SELECT * FROM posts WHERE fingerprint = ?'),
17
- listAll: db.prepare('SELECT * FROM posts ORDER BY created_at DESC LIMIT ?'),
18
- listByPlatform: db.prepare('SELECT * FROM posts WHERE platform = ? ORDER BY created_at DESC LIMIT ?'),
19
- listByCampaign: db.prepare('SELECT * FROM posts WHERE campaign_id = ? ORDER BY created_at DESC LIMIT ?'),
20
- listPublished: db.prepare(`SELECT * FROM posts WHERE status = 'published' ORDER BY published_at DESC LIMIT ?`),
21
- countAll: db.prepare('SELECT COUNT(*) as count FROM posts'),
22
- countByPlatform: db.prepare('SELECT platform, COUNT(*) as count FROM posts GROUP BY platform'),
23
- countByStatus: db.prepare('SELECT status, COUNT(*) as count FROM posts GROUP BY status'),
24
- delete: db.prepare('DELETE FROM posts WHERE id = ?'),
25
- search: db.prepare(`
26
- SELECT p.* FROM posts p
27
- JOIN posts_fts f ON f.rowid = p.id
28
- WHERE posts_fts MATCH ?
29
- ORDER BY rank
30
- LIMIT ?
31
- `),
32
- recentPublished: db.prepare(`
33
- SELECT * FROM posts WHERE status = 'published' AND published_at > ? ORDER BY published_at DESC
34
- `),
35
- };
36
- }
37
-
38
- create(data: PostCreate): number {
39
- const fingerprint = sha256(`${data.platform}:${data.content}`);
40
- const result = this.stmts.create.run({
41
- campaign_id: data.campaign_id ?? null,
42
- platform: data.platform,
43
- content: data.content,
44
- format: data.format ?? 'text',
45
- hashtags: data.hashtags ?? null,
46
- url: data.url ?? null,
47
- published_at: data.published_at ?? null,
48
- fingerprint,
49
- status: data.status ?? 'draft',
50
- });
51
- return result.lastInsertRowid as number;
52
- }
53
-
54
- getById(id: number): Post | undefined {
55
- return this.stmts.getById.get(id) as Post | undefined;
56
- }
57
-
58
- getByFingerprint(fingerprint: string): Post | undefined {
59
- return this.stmts.getByFingerprint.get(fingerprint) as Post | undefined;
60
- }
61
-
62
- listAll(limit: number = 50): Post[] {
63
- return this.stmts.listAll.all(limit) as Post[];
64
- }
65
-
66
- listByPlatform(platform: string, limit: number = 50): Post[] {
67
- return this.stmts.listByPlatform.all(platform, limit) as Post[];
68
- }
69
-
70
- listByCampaign(campaignId: number, limit: number = 50): Post[] {
71
- return this.stmts.listByCampaign.all(campaignId, limit) as Post[];
72
- }
73
-
74
- listPublished(limit: number = 50): Post[] {
75
- return this.stmts.listPublished.all(limit) as Post[];
76
- }
77
-
78
- recentPublished(sinceDate: string): Post[] {
79
- return this.stmts.recentPublished.all(sinceDate) as Post[];
80
- }
81
-
82
- update(id: number, data: Partial<Post>): boolean {
83
- const fields = Object.keys(data).filter(
84
- (key) => key !== 'id' && key !== 'created_at' && (data as Record<string, unknown>)[key] !== undefined
85
- );
86
- if (fields.length === 0) return false;
87
-
88
- const setClauses = fields.map((f) => `${f} = @${f}`).join(', ');
89
- const stmt = this.db.prepare(
90
- `UPDATE posts SET ${setClauses}, updated_at = datetime('now') WHERE id = @id`
91
- );
92
- const result = stmt.run({ ...data, id });
93
- return result.changes > 0;
94
- }
95
-
96
- delete(id: number): boolean {
97
- const result = this.stmts.delete.run(id);
98
- return result.changes > 0;
99
- }
100
-
101
- search(query: string, limit: number = 20): Post[] {
102
- return this.stmts.search.all(query, limit) as Post[];
103
- }
104
-
105
- countAll(): number {
106
- const row = this.stmts.countAll.get() as { count: number };
107
- return row.count;
108
- }
109
-
110
- countByPlatform(): Record<string, number> {
111
- const rows = this.stmts.countByPlatform.all() as Array<{ platform: string; count: number }>;
112
- const result: Record<string, number> = {};
113
- for (const row of rows) result[row.platform] = row.count;
114
- return result;
115
- }
116
-
117
- countByStatus(): Record<string, number> {
118
- const rows = this.stmts.countByStatus.all() as Array<{ status: string; count: number }>;
119
- const result: Record<string, number> = {};
120
- for (const row of rows) result[row.status] = row.count;
121
- return result;
122
- }
123
- }