@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.
Files changed (38) hide show
  1. package/microservices/microservice-ads/src/cli/index.ts +198 -0
  2. package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
  3. package/microservices/microservice-ads/src/mcp/index.ts +160 -0
  4. package/microservices/microservice-contracts/src/cli/index.ts +410 -23
  5. package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
  6. package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
  7. package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
  8. package/microservices/microservice-domains/src/cli/index.ts +253 -0
  9. package/microservices/microservice-domains/src/db/domains.ts +613 -0
  10. package/microservices/microservice-domains/src/index.ts +21 -0
  11. package/microservices/microservice-domains/src/mcp/index.ts +168 -0
  12. package/microservices/microservice-hiring/src/cli/index.ts +318 -8
  13. package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
  14. package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
  15. package/microservices/microservice-hiring/src/index.ts +29 -0
  16. package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
  17. package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
  18. package/microservices/microservice-payments/src/cli/index.ts +255 -3
  19. package/microservices/microservice-payments/src/db/migrations.ts +18 -0
  20. package/microservices/microservice-payments/src/db/payments.ts +552 -0
  21. package/microservices/microservice-payments/src/mcp/index.ts +223 -0
  22. package/microservices/microservice-payroll/src/cli/index.ts +269 -0
  23. package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
  24. package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
  25. package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
  26. package/microservices/microservice-shipping/src/cli/index.ts +211 -3
  27. package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
  28. package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
  29. package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
  30. package/microservices/microservice-social/src/cli/index.ts +244 -2
  31. package/microservices/microservice-social/src/db/migrations.ts +33 -0
  32. package/microservices/microservice-social/src/db/social.ts +378 -4
  33. package/microservices/microservice-social/src/mcp/index.ts +221 -1
  34. package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
  35. package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
  36. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
  37. package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
  38. 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();