@hasna/microservices 0.0.4 → 0.0.5
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/microservices/microservice-ads/src/cli/index.ts +198 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
- package/microservices/microservice-ads/src/mcp/index.ts +160 -0
- package/microservices/microservice-contracts/src/cli/index.ts +410 -23
- package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
- package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
- package/microservices/microservice-domains/src/cli/index.ts +253 -0
- package/microservices/microservice-domains/src/db/domains.ts +613 -0
- package/microservices/microservice-domains/src/index.ts +21 -0
- package/microservices/microservice-domains/src/mcp/index.ts +168 -0
- package/microservices/microservice-hiring/src/cli/index.ts +318 -8
- package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
- package/microservices/microservice-hiring/src/index.ts +29 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
- package/microservices/microservice-payments/src/cli/index.ts +255 -3
- package/microservices/microservice-payments/src/db/migrations.ts +18 -0
- package/microservices/microservice-payments/src/db/payments.ts +552 -0
- package/microservices/microservice-payments/src/mcp/index.ts +223 -0
- package/microservices/microservice-payroll/src/cli/index.ts +269 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
- package/microservices/microservice-shipping/src/cli/index.ts +211 -3
- package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
- package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
- package/microservices/microservice-social/src/cli/index.ts +244 -2
- package/microservices/microservice-social/src/db/migrations.ts +33 -0
- package/microservices/microservice-social/src/db/social.ts +378 -4
- package/microservices/microservice-social/src/mcp/index.ts +221 -1
- package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
- package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
- package/package.json +1 -1
|
@@ -12,6 +12,15 @@ import {
|
|
|
12
12
|
getCampaignStats,
|
|
13
13
|
getSpendByPlatform,
|
|
14
14
|
getPlatforms,
|
|
15
|
+
bulkPause,
|
|
16
|
+
bulkResume,
|
|
17
|
+
getRankedCampaigns,
|
|
18
|
+
checkBudgetStatus,
|
|
19
|
+
comparePlatforms,
|
|
20
|
+
exportCampaigns,
|
|
21
|
+
cloneCampaign,
|
|
22
|
+
getBudgetRemaining,
|
|
23
|
+
getAdGroupStats,
|
|
15
24
|
} from "../db/campaigns.js";
|
|
16
25
|
import {
|
|
17
26
|
createAdGroup,
|
|
@@ -218,6 +227,118 @@ campaignCmd
|
|
|
218
227
|
}
|
|
219
228
|
});
|
|
220
229
|
|
|
230
|
+
// 1. Bulk pause/resume
|
|
231
|
+
campaignCmd
|
|
232
|
+
.command("bulk-pause")
|
|
233
|
+
.description("Pause all active campaigns on a platform")
|
|
234
|
+
.requiredOption("--platform <platform>", "Platform (google/meta/linkedin/tiktok)")
|
|
235
|
+
.option("--json", "Output as JSON", false)
|
|
236
|
+
.action((opts) => {
|
|
237
|
+
const result = bulkPause(opts.platform);
|
|
238
|
+
if (opts.json) {
|
|
239
|
+
console.log(JSON.stringify(result, null, 2));
|
|
240
|
+
} else {
|
|
241
|
+
console.log(`Paused ${result.updated_count} campaign(s) on ${result.platform}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
campaignCmd
|
|
246
|
+
.command("bulk-resume")
|
|
247
|
+
.description("Resume all paused campaigns on a platform")
|
|
248
|
+
.requiredOption("--platform <platform>", "Platform (google/meta/linkedin/tiktok)")
|
|
249
|
+
.option("--json", "Output as JSON", false)
|
|
250
|
+
.action((opts) => {
|
|
251
|
+
const result = bulkResume(opts.platform);
|
|
252
|
+
if (opts.json) {
|
|
253
|
+
console.log(JSON.stringify(result, null, 2));
|
|
254
|
+
} else {
|
|
255
|
+
console.log(`Resumed ${result.updated_count} campaign(s) on ${result.platform}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 3. Budget alerts
|
|
260
|
+
campaignCmd
|
|
261
|
+
.command("check-budget")
|
|
262
|
+
.description("Check budget status for a campaign")
|
|
263
|
+
.argument("<id>", "Campaign ID")
|
|
264
|
+
.option("--json", "Output as JSON", false)
|
|
265
|
+
.action((id, opts) => {
|
|
266
|
+
const status = checkBudgetStatus(id);
|
|
267
|
+
if (!status) {
|
|
268
|
+
console.error(`Campaign '${id}' not found.`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (opts.json) {
|
|
273
|
+
console.log(JSON.stringify(status, null, 2));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`Budget Status: ${status.campaign_name}`);
|
|
276
|
+
console.log(` Over budget: ${status.over_budget ? "YES" : "No"}`);
|
|
277
|
+
console.log(` Daily remaining: $${status.daily_remaining}`);
|
|
278
|
+
console.log(` Total remaining: $${status.total_remaining}`);
|
|
279
|
+
console.log(` % used: ${status.pct_used}%`);
|
|
280
|
+
console.log(` Days active: ${status.days_active}`);
|
|
281
|
+
console.log(` Expected spend: $${status.expected_spend}`);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// 5. CSV export
|
|
286
|
+
campaignCmd
|
|
287
|
+
.command("export")
|
|
288
|
+
.description("Export campaigns to CSV or JSON")
|
|
289
|
+
.option("--format <format>", "Format (csv/json)", "csv")
|
|
290
|
+
.action((opts) => {
|
|
291
|
+
const output = exportCampaigns(opts.format);
|
|
292
|
+
console.log(output);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 6. Campaign cloning
|
|
296
|
+
campaignCmd
|
|
297
|
+
.command("clone")
|
|
298
|
+
.description("Clone a campaign with all ad groups and ads")
|
|
299
|
+
.argument("<id>", "Campaign ID to clone")
|
|
300
|
+
.requiredOption("--name <name>", "New campaign name")
|
|
301
|
+
.option("--json", "Output as JSON", false)
|
|
302
|
+
.action((id, opts) => {
|
|
303
|
+
const cloned = cloneCampaign(id, opts.name);
|
|
304
|
+
if (!cloned) {
|
|
305
|
+
console.error(`Campaign '${id}' not found.`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (opts.json) {
|
|
310
|
+
console.log(JSON.stringify(cloned, null, 2));
|
|
311
|
+
} else {
|
|
312
|
+
console.log(`Cloned campaign: ${cloned.name} (${cloned.id})`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// 7. Budget remaining
|
|
317
|
+
campaignCmd
|
|
318
|
+
.command("budget-remaining")
|
|
319
|
+
.description("Show budget remaining for a campaign")
|
|
320
|
+
.argument("<id>", "Campaign ID")
|
|
321
|
+
.option("--json", "Output as JSON", false)
|
|
322
|
+
.action((id, opts) => {
|
|
323
|
+
const remaining = getBudgetRemaining(id);
|
|
324
|
+
if (!remaining) {
|
|
325
|
+
console.error(`Campaign '${id}' not found.`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (opts.json) {
|
|
330
|
+
console.log(JSON.stringify(remaining, null, 2));
|
|
331
|
+
} else {
|
|
332
|
+
console.log(`Budget Remaining: ${remaining.campaign_name}`);
|
|
333
|
+
console.log(` Daily budget: $${remaining.budget_daily}`);
|
|
334
|
+
console.log(` Total budget: $${remaining.budget_total}`);
|
|
335
|
+
console.log(` Spend: $${remaining.spend}`);
|
|
336
|
+
console.log(` Daily remaining: $${remaining.daily_remaining}`);
|
|
337
|
+
console.log(` Total remaining: $${remaining.total_remaining}`);
|
|
338
|
+
console.log(` Days remaining at current rate: ${remaining.days_remaining_at_daily_rate}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
221
342
|
// --- Ad Groups ---
|
|
222
343
|
|
|
223
344
|
const adGroupCmd = program
|
|
@@ -269,6 +390,35 @@ adGroupCmd
|
|
|
269
390
|
}
|
|
270
391
|
});
|
|
271
392
|
|
|
393
|
+
// 8. Ad group stats
|
|
394
|
+
adGroupCmd
|
|
395
|
+
.command("stats")
|
|
396
|
+
.description("Show aggregated stats for an ad group")
|
|
397
|
+
.argument("<id>", "Ad group ID")
|
|
398
|
+
.option("--json", "Output as JSON", false)
|
|
399
|
+
.action((id, opts) => {
|
|
400
|
+
const stats = getAdGroupStats(id);
|
|
401
|
+
if (!stats) {
|
|
402
|
+
console.error(`Ad group '${id}' not found.`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (opts.json) {
|
|
407
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
408
|
+
} else {
|
|
409
|
+
console.log(`Ad Group Stats: ${stats.ad_group_name}`);
|
|
410
|
+
console.log(` Campaign: ${stats.campaign_id}`);
|
|
411
|
+
console.log(` Total ads: ${stats.total_ads}`);
|
|
412
|
+
console.log(` Active ads: ${stats.active_ads}`);
|
|
413
|
+
if (Object.keys(stats.metrics).length > 0) {
|
|
414
|
+
console.log(" Aggregated metrics:");
|
|
415
|
+
for (const [key, value] of Object.entries(stats.metrics)) {
|
|
416
|
+
console.log(` ${key}: ${value}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
272
422
|
// --- Ads ---
|
|
273
423
|
|
|
274
424
|
const adCmd = program
|
|
@@ -327,8 +477,26 @@ adCmd
|
|
|
327
477
|
program
|
|
328
478
|
.command("stats")
|
|
329
479
|
.description("Show campaign statistics")
|
|
480
|
+
.option("--sort-by <metric>", "Sort campaigns by metric (roas/ctr/spend)")
|
|
481
|
+
.option("--limit <n>", "Limit ranked results", "10")
|
|
330
482
|
.option("--json", "Output as JSON", false)
|
|
331
483
|
.action((opts) => {
|
|
484
|
+
// 2. Performance ranking via --sort-by
|
|
485
|
+
if (opts.sortBy) {
|
|
486
|
+
const ranked = getRankedCampaigns(opts.sortBy, parseInt(opts.limit));
|
|
487
|
+
if (opts.json) {
|
|
488
|
+
console.log(JSON.stringify(ranked, null, 2));
|
|
489
|
+
} else {
|
|
490
|
+
console.log(`Campaigns ranked by ${opts.sortBy}:`);
|
|
491
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
492
|
+
const c = ranked[i];
|
|
493
|
+
const ctr = c.impressions > 0 ? ((c.clicks / c.impressions) * 100).toFixed(2) : "0.00";
|
|
494
|
+
console.log(` ${i + 1}. ${c.name} [${c.platform}] — ROAS: ${c.roas}, CTR: ${ctr}%, Spend: $${c.spend}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
332
500
|
const stats = getCampaignStats();
|
|
333
501
|
|
|
334
502
|
if (opts.json) {
|
|
@@ -345,6 +513,36 @@ program
|
|
|
345
513
|
}
|
|
346
514
|
});
|
|
347
515
|
|
|
516
|
+
// 4. Cross-platform comparison
|
|
517
|
+
program
|
|
518
|
+
.command("compare-platforms")
|
|
519
|
+
.description("Compare ROAS/CPA/spend across platforms side-by-side")
|
|
520
|
+
.option("--json", "Output as JSON", false)
|
|
521
|
+
.action((opts) => {
|
|
522
|
+
const comparison = comparePlatforms();
|
|
523
|
+
|
|
524
|
+
if (opts.json) {
|
|
525
|
+
console.log(JSON.stringify(comparison, null, 2));
|
|
526
|
+
} else {
|
|
527
|
+
if (comparison.length === 0) {
|
|
528
|
+
console.log("No platform data.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
console.log("Platform Comparison:");
|
|
532
|
+
console.log(" Platform | Campaigns | Spend | ROAS | CTR | CPA");
|
|
533
|
+
console.log(" -------------|-----------|--------------|-------|--------|-------");
|
|
534
|
+
for (const p of comparison) {
|
|
535
|
+
const platform = p.platform.padEnd(12);
|
|
536
|
+
const campaigns = String(p.campaign_count).padEnd(9);
|
|
537
|
+
const spend = `$${p.total_spend.toFixed(2)}`.padEnd(12);
|
|
538
|
+
const roas = p.avg_roas.toFixed(2).padEnd(5);
|
|
539
|
+
const ctr = `${p.avg_ctr.toFixed(2)}%`.padEnd(6);
|
|
540
|
+
const cpa = p.avg_cpa > 0 ? `$${p.avg_cpa.toFixed(2)}` : "N/A";
|
|
541
|
+
console.log(` ${platform} | ${campaigns} | ${spend} | ${roas} | ${ctr} | ${cpa}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
348
546
|
program
|
|
349
547
|
.command("spend")
|
|
350
548
|
.description("Show spend by platform")
|
|
@@ -491,3 +491,307 @@ export function getPlatforms(): string[] {
|
|
|
491
491
|
).all() as { platform: string }[];
|
|
492
492
|
return rows.map((r) => r.platform);
|
|
493
493
|
}
|
|
494
|
+
|
|
495
|
+
// --- QoL Features ---
|
|
496
|
+
|
|
497
|
+
// 1. Bulk pause/resume by platform
|
|
498
|
+
export interface BulkUpdateResult {
|
|
499
|
+
updated_count: number;
|
|
500
|
+
platform: Platform;
|
|
501
|
+
new_status: CampaignStatus;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function bulkPause(platform: Platform): BulkUpdateResult {
|
|
505
|
+
const db = getDatabase();
|
|
506
|
+
const result = db.prepare(
|
|
507
|
+
"UPDATE campaigns SET status = 'paused', updated_at = datetime('now') WHERE platform = ? AND status = 'active'"
|
|
508
|
+
).run(platform);
|
|
509
|
+
return { updated_count: result.changes, platform, new_status: "paused" };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function bulkResume(platform: Platform): BulkUpdateResult {
|
|
513
|
+
const db = getDatabase();
|
|
514
|
+
const result = db.prepare(
|
|
515
|
+
"UPDATE campaigns SET status = 'active', updated_at = datetime('now') WHERE platform = ? AND status = 'paused'"
|
|
516
|
+
).run(platform);
|
|
517
|
+
return { updated_count: result.changes, platform, new_status: "active" };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 2. Performance ranking
|
|
521
|
+
export type RankMetric = "roas" | "ctr" | "spend";
|
|
522
|
+
|
|
523
|
+
export function getRankedCampaigns(sortBy: RankMetric = "roas", limit: number = 10): Campaign[] {
|
|
524
|
+
const db = getDatabase();
|
|
525
|
+
let orderExpr: string;
|
|
526
|
+
|
|
527
|
+
switch (sortBy) {
|
|
528
|
+
case "roas":
|
|
529
|
+
orderExpr = "roas DESC";
|
|
530
|
+
break;
|
|
531
|
+
case "ctr":
|
|
532
|
+
// CTR = clicks / impressions (handle division by zero)
|
|
533
|
+
orderExpr = "CASE WHEN impressions > 0 THEN CAST(clicks AS REAL) / impressions ELSE 0 END DESC";
|
|
534
|
+
break;
|
|
535
|
+
case "spend":
|
|
536
|
+
orderExpr = "spend DESC";
|
|
537
|
+
break;
|
|
538
|
+
default:
|
|
539
|
+
orderExpr = "roas DESC";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const rows = db.prepare(
|
|
543
|
+
`SELECT * FROM campaigns ORDER BY ${orderExpr} LIMIT ?`
|
|
544
|
+
).all(limit) as CampaignRow[];
|
|
545
|
+
return rows.map(rowToCampaign);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 3. Budget alerts
|
|
549
|
+
export interface BudgetStatus {
|
|
550
|
+
campaign_id: string;
|
|
551
|
+
campaign_name: string;
|
|
552
|
+
over_budget: boolean;
|
|
553
|
+
daily_remaining: number;
|
|
554
|
+
total_remaining: number;
|
|
555
|
+
pct_used: number;
|
|
556
|
+
days_active: number;
|
|
557
|
+
expected_spend: number;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function checkBudgetStatus(id: string): BudgetStatus | null {
|
|
561
|
+
const campaign = getCampaign(id);
|
|
562
|
+
if (!campaign) return null;
|
|
563
|
+
|
|
564
|
+
const now = new Date();
|
|
565
|
+
let daysActive = 1;
|
|
566
|
+
|
|
567
|
+
if (campaign.start_date) {
|
|
568
|
+
const start = new Date(campaign.start_date);
|
|
569
|
+
const diffMs = now.getTime() - start.getTime();
|
|
570
|
+
daysActive = Math.max(1, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const expectedSpend = campaign.budget_daily * daysActive;
|
|
574
|
+
const overBudget = campaign.budget_total > 0
|
|
575
|
+
? campaign.spend > campaign.budget_total
|
|
576
|
+
: campaign.spend > expectedSpend;
|
|
577
|
+
|
|
578
|
+
const dailySpentToday = daysActive > 0 ? campaign.spend / daysActive : campaign.spend;
|
|
579
|
+
const dailyRemaining = Math.max(0, campaign.budget_daily - dailySpentToday);
|
|
580
|
+
|
|
581
|
+
const totalRemaining = campaign.budget_total > 0
|
|
582
|
+
? Math.max(0, campaign.budget_total - campaign.spend)
|
|
583
|
+
: Math.max(0, expectedSpend - campaign.spend);
|
|
584
|
+
|
|
585
|
+
const pctUsed = campaign.budget_total > 0
|
|
586
|
+
? (campaign.spend / campaign.budget_total) * 100
|
|
587
|
+
: expectedSpend > 0 ? (campaign.spend / expectedSpend) * 100 : 0;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
campaign_id: id,
|
|
591
|
+
campaign_name: campaign.name,
|
|
592
|
+
over_budget: overBudget,
|
|
593
|
+
daily_remaining: Math.round(dailyRemaining * 100) / 100,
|
|
594
|
+
total_remaining: Math.round(totalRemaining * 100) / 100,
|
|
595
|
+
pct_used: Math.round(pctUsed * 100) / 100,
|
|
596
|
+
days_active: daysActive,
|
|
597
|
+
expected_spend: Math.round(expectedSpend * 100) / 100,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 4. Cross-platform comparison
|
|
602
|
+
export interface PlatformComparison {
|
|
603
|
+
platform: Platform;
|
|
604
|
+
campaign_count: number;
|
|
605
|
+
total_spend: number;
|
|
606
|
+
total_impressions: number;
|
|
607
|
+
total_clicks: number;
|
|
608
|
+
total_conversions: number;
|
|
609
|
+
avg_roas: number;
|
|
610
|
+
avg_ctr: number;
|
|
611
|
+
avg_cpa: number;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function comparePlatforms(): PlatformComparison[] {
|
|
615
|
+
const db = getDatabase();
|
|
616
|
+
const rows = db.prepare(`
|
|
617
|
+
SELECT
|
|
618
|
+
platform,
|
|
619
|
+
COUNT(*) as campaign_count,
|
|
620
|
+
COALESCE(SUM(spend), 0) as total_spend,
|
|
621
|
+
COALESCE(SUM(impressions), 0) as total_impressions,
|
|
622
|
+
COALESCE(SUM(clicks), 0) as total_clicks,
|
|
623
|
+
COALESCE(SUM(conversions), 0) as total_conversions,
|
|
624
|
+
COALESCE(AVG(CASE WHEN roas > 0 THEN roas END), 0) as avg_roas,
|
|
625
|
+
CASE WHEN SUM(impressions) > 0
|
|
626
|
+
THEN CAST(SUM(clicks) AS REAL) / SUM(impressions) * 100
|
|
627
|
+
ELSE 0
|
|
628
|
+
END as avg_ctr,
|
|
629
|
+
CASE WHEN SUM(conversions) > 0
|
|
630
|
+
THEN SUM(spend) / SUM(conversions)
|
|
631
|
+
ELSE 0
|
|
632
|
+
END as avg_cpa
|
|
633
|
+
FROM campaigns
|
|
634
|
+
GROUP BY platform
|
|
635
|
+
ORDER BY total_spend DESC
|
|
636
|
+
`).all() as PlatformComparison[];
|
|
637
|
+
return rows;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 5. CSV export
|
|
641
|
+
export function exportCampaigns(format: "csv" | "json" = "csv"): string {
|
|
642
|
+
const campaigns = listCampaigns();
|
|
643
|
+
|
|
644
|
+
if (format === "json") {
|
|
645
|
+
return JSON.stringify(campaigns, null, 2);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// CSV format
|
|
649
|
+
const headers = [
|
|
650
|
+
"id", "platform", "name", "status", "budget_daily", "budget_total",
|
|
651
|
+
"spend", "impressions", "clicks", "conversions", "roas",
|
|
652
|
+
"start_date", "end_date", "created_at", "updated_at",
|
|
653
|
+
];
|
|
654
|
+
|
|
655
|
+
const rows = campaigns.map((c) =>
|
|
656
|
+
headers.map((h) => {
|
|
657
|
+
const val = c[h as keyof Campaign];
|
|
658
|
+
if (val === null || val === undefined) return "";
|
|
659
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
660
|
+
const str = String(val);
|
|
661
|
+
// Escape CSV fields that contain commas or quotes
|
|
662
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
663
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
664
|
+
}
|
|
665
|
+
return str;
|
|
666
|
+
}).join(",")
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
return [headers.join(","), ...rows].join("\n");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 6. Campaign cloning
|
|
673
|
+
export function cloneCampaign(id: string, newName: string): Campaign | null {
|
|
674
|
+
const original = getCampaign(id);
|
|
675
|
+
if (!original) return null;
|
|
676
|
+
|
|
677
|
+
const db = getDatabase();
|
|
678
|
+
|
|
679
|
+
// Clone the campaign itself (reset metrics)
|
|
680
|
+
const cloned = createCampaign({
|
|
681
|
+
platform: original.platform,
|
|
682
|
+
name: newName,
|
|
683
|
+
status: "draft",
|
|
684
|
+
budget_daily: original.budget_daily,
|
|
685
|
+
budget_total: original.budget_total,
|
|
686
|
+
start_date: original.start_date || undefined,
|
|
687
|
+
end_date: original.end_date || undefined,
|
|
688
|
+
metadata: original.metadata,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Clone ad groups and their ads
|
|
692
|
+
const adGroups = listAdGroups(id);
|
|
693
|
+
for (const ag of adGroups) {
|
|
694
|
+
const clonedGroup = createAdGroup({
|
|
695
|
+
campaign_id: cloned.id,
|
|
696
|
+
name: ag.name,
|
|
697
|
+
targeting: ag.targeting,
|
|
698
|
+
status: "draft",
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const ads = listAds(ag.id);
|
|
702
|
+
for (const ad of ads) {
|
|
703
|
+
createAd({
|
|
704
|
+
ad_group_id: clonedGroup.id,
|
|
705
|
+
headline: ad.headline,
|
|
706
|
+
description: ad.description || undefined,
|
|
707
|
+
creative_url: ad.creative_url || undefined,
|
|
708
|
+
status: "draft",
|
|
709
|
+
metrics: {},
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return getCampaign(cloned.id)!;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 7. Budget remaining (reuses checkBudgetStatus but provides a focused view)
|
|
718
|
+
export interface BudgetRemaining {
|
|
719
|
+
campaign_id: string;
|
|
720
|
+
campaign_name: string;
|
|
721
|
+
budget_daily: number;
|
|
722
|
+
budget_total: number;
|
|
723
|
+
spend: number;
|
|
724
|
+
daily_remaining: number;
|
|
725
|
+
total_remaining: number;
|
|
726
|
+
days_remaining_at_daily_rate: number;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function getBudgetRemaining(id: string): BudgetRemaining | null {
|
|
730
|
+
const campaign = getCampaign(id);
|
|
731
|
+
if (!campaign) return null;
|
|
732
|
+
|
|
733
|
+
const now = new Date();
|
|
734
|
+
let daysActive = 1;
|
|
735
|
+
if (campaign.start_date) {
|
|
736
|
+
const start = new Date(campaign.start_date);
|
|
737
|
+
const diffMs = now.getTime() - start.getTime();
|
|
738
|
+
daysActive = Math.max(1, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const avgDailySpend = campaign.spend / daysActive;
|
|
742
|
+
const dailyRemaining = Math.max(0, campaign.budget_daily - avgDailySpend);
|
|
743
|
+
const totalRemaining = campaign.budget_total > 0
|
|
744
|
+
? Math.max(0, campaign.budget_total - campaign.spend)
|
|
745
|
+
: 0;
|
|
746
|
+
const daysRemainingAtRate = avgDailySpend > 0 && campaign.budget_total > 0
|
|
747
|
+
? Math.floor(totalRemaining / avgDailySpend)
|
|
748
|
+
: 0;
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
campaign_id: id,
|
|
752
|
+
campaign_name: campaign.name,
|
|
753
|
+
budget_daily: campaign.budget_daily,
|
|
754
|
+
budget_total: campaign.budget_total,
|
|
755
|
+
spend: campaign.spend,
|
|
756
|
+
daily_remaining: Math.round(dailyRemaining * 100) / 100,
|
|
757
|
+
total_remaining: Math.round(totalRemaining * 100) / 100,
|
|
758
|
+
days_remaining_at_daily_rate: daysRemainingAtRate,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 8. Ad group stats — aggregate metrics for ads in a group
|
|
763
|
+
export interface AdGroupStats {
|
|
764
|
+
ad_group_id: string;
|
|
765
|
+
ad_group_name: string;
|
|
766
|
+
campaign_id: string;
|
|
767
|
+
total_ads: number;
|
|
768
|
+
active_ads: number;
|
|
769
|
+
metrics: Record<string, number>;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function getAdGroupStats(adGroupId: string): AdGroupStats | null {
|
|
773
|
+
const adGroup = getAdGroup(adGroupId);
|
|
774
|
+
if (!adGroup) return null;
|
|
775
|
+
|
|
776
|
+
const ads = listAds(adGroupId);
|
|
777
|
+
const activeAds = ads.filter((a) => a.status === "active").length;
|
|
778
|
+
|
|
779
|
+
// Aggregate numeric metrics from all ads
|
|
780
|
+
const aggregated: Record<string, number> = {};
|
|
781
|
+
for (const ad of ads) {
|
|
782
|
+
for (const [key, value] of Object.entries(ad.metrics)) {
|
|
783
|
+
if (typeof value === "number") {
|
|
784
|
+
aggregated[key] = (aggregated[key] || 0) + value;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
ad_group_id: adGroupId,
|
|
791
|
+
ad_group_name: adGroup.name,
|
|
792
|
+
campaign_id: adGroup.campaign_id,
|
|
793
|
+
total_ads: ads.length,
|
|
794
|
+
active_ads: activeAds,
|
|
795
|
+
metrics: aggregated,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
@@ -18,6 +18,15 @@ import {
|
|
|
18
18
|
listAdGroups,
|
|
19
19
|
createAd,
|
|
20
20
|
listAds,
|
|
21
|
+
bulkPause,
|
|
22
|
+
bulkResume,
|
|
23
|
+
getRankedCampaigns,
|
|
24
|
+
checkBudgetStatus,
|
|
25
|
+
comparePlatforms,
|
|
26
|
+
exportCampaigns,
|
|
27
|
+
cloneCampaign,
|
|
28
|
+
getBudgetRemaining,
|
|
29
|
+
getAdGroupStats,
|
|
21
30
|
} from "../db/campaigns.js";
|
|
22
31
|
|
|
23
32
|
const server = new McpServer({
|
|
@@ -307,6 +316,157 @@ server.registerTool(
|
|
|
307
316
|
}
|
|
308
317
|
);
|
|
309
318
|
|
|
319
|
+
// --- QoL Tools ---
|
|
320
|
+
|
|
321
|
+
// 1. Bulk pause/resume
|
|
322
|
+
server.registerTool(
|
|
323
|
+
"bulk_pause_campaigns",
|
|
324
|
+
{
|
|
325
|
+
title: "Bulk Pause Campaigns",
|
|
326
|
+
description: "Pause all active campaigns on a specific platform.",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
platform: PlatformEnum,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
async ({ platform }) => {
|
|
332
|
+
const result = bulkPause(platform);
|
|
333
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
server.registerTool(
|
|
338
|
+
"bulk_resume_campaigns",
|
|
339
|
+
{
|
|
340
|
+
title: "Bulk Resume Campaigns",
|
|
341
|
+
description: "Resume all paused campaigns on a specific platform.",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
platform: PlatformEnum,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
async ({ platform }) => {
|
|
347
|
+
const result = bulkResume(platform);
|
|
348
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// 2. Performance ranking
|
|
353
|
+
server.registerTool(
|
|
354
|
+
"ranked_campaigns",
|
|
355
|
+
{
|
|
356
|
+
title: "Ranked Campaigns",
|
|
357
|
+
description: "Get campaigns ranked by a performance metric (roas, ctr, or spend).",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
sort_by: z.enum(["roas", "ctr", "spend"]).optional(),
|
|
360
|
+
limit: z.number().optional(),
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
async ({ sort_by, limit }) => {
|
|
364
|
+
const campaigns = getRankedCampaigns(sort_by || "roas", limit || 10);
|
|
365
|
+
return { content: [{ type: "text", text: JSON.stringify(campaigns, null, 2) }] };
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// 3. Budget alerts
|
|
370
|
+
server.registerTool(
|
|
371
|
+
"check_budget",
|
|
372
|
+
{
|
|
373
|
+
title: "Check Budget Status",
|
|
374
|
+
description: "Check if a campaign is over budget and get remaining budget details.",
|
|
375
|
+
inputSchema: { id: z.string() },
|
|
376
|
+
},
|
|
377
|
+
async ({ id }) => {
|
|
378
|
+
const status = checkBudgetStatus(id);
|
|
379
|
+
if (!status) {
|
|
380
|
+
return { content: [{ type: "text", text: `Campaign '${id}' not found.` }], isError: true };
|
|
381
|
+
}
|
|
382
|
+
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// 4. Cross-platform comparison
|
|
387
|
+
server.registerTool(
|
|
388
|
+
"compare_platforms",
|
|
389
|
+
{
|
|
390
|
+
title: "Compare Platforms",
|
|
391
|
+
description: "Compare ROAS, CPA, and spend across all platforms side-by-side.",
|
|
392
|
+
inputSchema: {},
|
|
393
|
+
},
|
|
394
|
+
async () => {
|
|
395
|
+
const comparison = comparePlatforms();
|
|
396
|
+
return { content: [{ type: "text", text: JSON.stringify(comparison, null, 2) }] };
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// 5. CSV export
|
|
401
|
+
server.registerTool(
|
|
402
|
+
"export_campaigns",
|
|
403
|
+
{
|
|
404
|
+
title: "Export Campaigns",
|
|
405
|
+
description: "Export all campaigns in CSV or JSON format.",
|
|
406
|
+
inputSchema: {
|
|
407
|
+
format: z.enum(["csv", "json"]).optional(),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
async ({ format }) => {
|
|
411
|
+
const output = exportCampaigns(format || "csv");
|
|
412
|
+
return { content: [{ type: "text", text: output }] };
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// 6. Campaign cloning
|
|
417
|
+
server.registerTool(
|
|
418
|
+
"clone_campaign",
|
|
419
|
+
{
|
|
420
|
+
title: "Clone Campaign",
|
|
421
|
+
description: "Clone a campaign with all its ad groups and ads.",
|
|
422
|
+
inputSchema: {
|
|
423
|
+
id: z.string(),
|
|
424
|
+
new_name: z.string(),
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
async ({ id, new_name }) => {
|
|
428
|
+
const cloned = cloneCampaign(id, new_name);
|
|
429
|
+
if (!cloned) {
|
|
430
|
+
return { content: [{ type: "text", text: `Campaign '${id}' not found.` }], isError: true };
|
|
431
|
+
}
|
|
432
|
+
return { content: [{ type: "text", text: JSON.stringify(cloned, null, 2) }] };
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// 7. Budget remaining
|
|
437
|
+
server.registerTool(
|
|
438
|
+
"budget_remaining",
|
|
439
|
+
{
|
|
440
|
+
title: "Budget Remaining",
|
|
441
|
+
description: "Show daily and total budget remaining for a campaign.",
|
|
442
|
+
inputSchema: { id: z.string() },
|
|
443
|
+
},
|
|
444
|
+
async ({ id }) => {
|
|
445
|
+
const remaining = getBudgetRemaining(id);
|
|
446
|
+
if (!remaining) {
|
|
447
|
+
return { content: [{ type: "text", text: `Campaign '${id}' not found.` }], isError: true };
|
|
448
|
+
}
|
|
449
|
+
return { content: [{ type: "text", text: JSON.stringify(remaining, null, 2) }] };
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// 8. Ad group stats
|
|
454
|
+
server.registerTool(
|
|
455
|
+
"ad_group_stats",
|
|
456
|
+
{
|
|
457
|
+
title: "Ad Group Stats",
|
|
458
|
+
description: "Get aggregated metrics for all ads within an ad group.",
|
|
459
|
+
inputSchema: { ad_group_id: z.string() },
|
|
460
|
+
},
|
|
461
|
+
async ({ ad_group_id }) => {
|
|
462
|
+
const stats = getAdGroupStats(ad_group_id);
|
|
463
|
+
if (!stats) {
|
|
464
|
+
return { content: [{ type: "text", text: `Ad group '${ad_group_id}' not found.` }], isError: true };
|
|
465
|
+
}
|
|
466
|
+
return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
310
470
|
// --- Start ---
|
|
311
471
|
async function main() {
|
|
312
472
|
const transport = new StdioServerTransport();
|