@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
|
@@ -7,7 +7,46 @@ import { getDatabase } from "./database.js";
|
|
|
7
7
|
// ---- Types ----
|
|
8
8
|
|
|
9
9
|
export type Platform = "x" | "linkedin" | "instagram" | "threads" | "bluesky";
|
|
10
|
-
export type PostStatus = "draft" | "scheduled" | "published" | "failed";
|
|
10
|
+
export type PostStatus = "draft" | "scheduled" | "published" | "failed" | "pending_review";
|
|
11
|
+
|
|
12
|
+
export type Recurrence = "daily" | "weekly" | "biweekly" | "monthly";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Platform character limits for post content validation
|
|
16
|
+
*/
|
|
17
|
+
export const PLATFORM_LIMITS: Record<Platform, number> = {
|
|
18
|
+
x: 280,
|
|
19
|
+
linkedin: 3000,
|
|
20
|
+
instagram: 2200,
|
|
21
|
+
threads: 500,
|
|
22
|
+
bluesky: 300,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface PlatformLimitWarning {
|
|
26
|
+
platform: Platform;
|
|
27
|
+
limit: number;
|
|
28
|
+
content_length: number;
|
|
29
|
+
over_by: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if content exceeds platform character limit for a given account
|
|
34
|
+
*/
|
|
35
|
+
export function checkPlatformLimit(content: string, accountId: string): PlatformLimitWarning | null {
|
|
36
|
+
const account = getAccount(accountId);
|
|
37
|
+
if (!account) return null;
|
|
38
|
+
|
|
39
|
+
const limit = PLATFORM_LIMITS[account.platform];
|
|
40
|
+
if (content.length > limit) {
|
|
41
|
+
return {
|
|
42
|
+
platform: account.platform,
|
|
43
|
+
limit,
|
|
44
|
+
content_length: content.length,
|
|
45
|
+
over_by: content.length - limit,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
11
50
|
|
|
12
51
|
export interface Account {
|
|
13
52
|
id: string;
|
|
@@ -59,6 +98,7 @@ export interface Post {
|
|
|
59
98
|
platform_post_id: string | null;
|
|
60
99
|
engagement: Engagement;
|
|
61
100
|
tags: string[];
|
|
101
|
+
recurrence: Recurrence | null;
|
|
62
102
|
created_at: string;
|
|
63
103
|
updated_at: string;
|
|
64
104
|
}
|
|
@@ -74,6 +114,7 @@ interface PostRow {
|
|
|
74
114
|
platform_post_id: string | null;
|
|
75
115
|
engagement: string;
|
|
76
116
|
tags: string;
|
|
117
|
+
recurrence: string | null;
|
|
77
118
|
created_at: string;
|
|
78
119
|
updated_at: string;
|
|
79
120
|
}
|
|
@@ -85,6 +126,7 @@ function rowToPost(row: PostRow): Post {
|
|
|
85
126
|
media_urls: JSON.parse(row.media_urls || "[]"),
|
|
86
127
|
engagement: JSON.parse(row.engagement || "{}"),
|
|
87
128
|
tags: JSON.parse(row.tags || "[]"),
|
|
129
|
+
recurrence: (row.recurrence as Recurrence) || null,
|
|
88
130
|
};
|
|
89
131
|
}
|
|
90
132
|
|
|
@@ -256,6 +298,7 @@ export interface CreatePostInput {
|
|
|
256
298
|
status?: PostStatus;
|
|
257
299
|
scheduled_at?: string;
|
|
258
300
|
tags?: string[];
|
|
301
|
+
recurrence?: Recurrence;
|
|
259
302
|
}
|
|
260
303
|
|
|
261
304
|
export function createPost(input: CreatePostInput): Post {
|
|
@@ -265,8 +308,8 @@ export function createPost(input: CreatePostInput): Post {
|
|
|
265
308
|
const tags = JSON.stringify(input.tags || []);
|
|
266
309
|
|
|
267
310
|
db.prepare(
|
|
268
|
-
`INSERT INTO posts (id, account_id, content, media_urls, status, scheduled_at, tags)
|
|
269
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
311
|
+
`INSERT INTO posts (id, account_id, content, media_urls, status, scheduled_at, tags, recurrence)
|
|
312
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
270
313
|
).run(
|
|
271
314
|
id,
|
|
272
315
|
input.account_id,
|
|
@@ -274,7 +317,8 @@ export function createPost(input: CreatePostInput): Post {
|
|
|
274
317
|
media_urls,
|
|
275
318
|
input.status || "draft",
|
|
276
319
|
input.scheduled_at || null,
|
|
277
|
-
tags
|
|
320
|
+
tags,
|
|
321
|
+
input.recurrence || null
|
|
278
322
|
);
|
|
279
323
|
|
|
280
324
|
return getPost(id)!;
|
|
@@ -348,6 +392,7 @@ export interface UpdatePostInput {
|
|
|
348
392
|
platform_post_id?: string;
|
|
349
393
|
engagement?: Engagement;
|
|
350
394
|
tags?: string[];
|
|
395
|
+
recurrence?: Recurrence | null;
|
|
351
396
|
}
|
|
352
397
|
|
|
353
398
|
export function updatePost(id: string, input: UpdatePostInput): Post | null {
|
|
@@ -390,6 +435,10 @@ export function updatePost(id: string, input: UpdatePostInput): Post | null {
|
|
|
390
435
|
sets.push("tags = ?");
|
|
391
436
|
params.push(JSON.stringify(input.tags));
|
|
392
437
|
}
|
|
438
|
+
if (input.recurrence !== undefined) {
|
|
439
|
+
sets.push("recurrence = ?");
|
|
440
|
+
params.push(input.recurrence);
|
|
441
|
+
}
|
|
393
442
|
|
|
394
443
|
if (sets.length === 0) return existing;
|
|
395
444
|
|
|
@@ -670,3 +719,328 @@ export function getOverallStats(): {
|
|
|
670
719
|
engagement,
|
|
671
720
|
};
|
|
672
721
|
}
|
|
722
|
+
|
|
723
|
+
// ---- Bulk Schedule ----
|
|
724
|
+
|
|
725
|
+
export interface BatchScheduleInput {
|
|
726
|
+
account_id: string;
|
|
727
|
+
content: string;
|
|
728
|
+
scheduled_at: string;
|
|
729
|
+
media_urls?: string[];
|
|
730
|
+
tags?: string[];
|
|
731
|
+
recurrence?: Recurrence;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export interface BatchScheduleResult {
|
|
735
|
+
scheduled: Post[];
|
|
736
|
+
errors: { index: number; error: string }[];
|
|
737
|
+
warnings: PlatformLimitWarning[];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Schedule multiple posts at once from an array of post definitions
|
|
742
|
+
*/
|
|
743
|
+
export function batchSchedule(posts: BatchScheduleInput[]): BatchScheduleResult {
|
|
744
|
+
const scheduled: Post[] = [];
|
|
745
|
+
const errors: { index: number; error: string }[] = [];
|
|
746
|
+
const warnings: PlatformLimitWarning[] = [];
|
|
747
|
+
|
|
748
|
+
for (let i = 0; i < posts.length; i++) {
|
|
749
|
+
const input = posts[i];
|
|
750
|
+
try {
|
|
751
|
+
// Validate account exists
|
|
752
|
+
const account = getAccount(input.account_id);
|
|
753
|
+
if (!account) {
|
|
754
|
+
errors.push({ index: i, error: `Account '${input.account_id}' not found` });
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Check platform limit
|
|
759
|
+
const warning = checkPlatformLimit(input.content, input.account_id);
|
|
760
|
+
if (warning) {
|
|
761
|
+
warnings.push(warning);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const post = createPost({
|
|
765
|
+
account_id: input.account_id,
|
|
766
|
+
content: input.content,
|
|
767
|
+
media_urls: input.media_urls,
|
|
768
|
+
status: "scheduled",
|
|
769
|
+
scheduled_at: input.scheduled_at,
|
|
770
|
+
tags: input.tags,
|
|
771
|
+
recurrence: input.recurrence,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
scheduled.push(post);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
errors.push({ index: i, error: err instanceof Error ? err.message : String(err) });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return { scheduled, errors, warnings };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ---- Cross-Post ----
|
|
784
|
+
|
|
785
|
+
export interface CrossPostResult {
|
|
786
|
+
posts: Post[];
|
|
787
|
+
warnings: PlatformLimitWarning[];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Create identical post on multiple platform accounts
|
|
792
|
+
*/
|
|
793
|
+
export function crossPost(
|
|
794
|
+
content: string,
|
|
795
|
+
platforms: Platform[],
|
|
796
|
+
options?: { media_urls?: string[]; tags?: string[]; scheduled_at?: string }
|
|
797
|
+
): CrossPostResult {
|
|
798
|
+
const posts: Post[] = [];
|
|
799
|
+
const warnings: PlatformLimitWarning[] = [];
|
|
800
|
+
|
|
801
|
+
for (const platform of platforms) {
|
|
802
|
+
// Find an account for this platform
|
|
803
|
+
const accounts = listAccounts({ platform });
|
|
804
|
+
if (accounts.length === 0) {
|
|
805
|
+
throw new Error(`No account found for platform '${platform}'`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const account = accounts[0]; // Use first matching account
|
|
809
|
+
|
|
810
|
+
// Check platform limit
|
|
811
|
+
const limit = PLATFORM_LIMITS[platform];
|
|
812
|
+
if (content.length > limit) {
|
|
813
|
+
warnings.push({
|
|
814
|
+
platform,
|
|
815
|
+
limit,
|
|
816
|
+
content_length: content.length,
|
|
817
|
+
over_by: content.length - limit,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const post = createPost({
|
|
822
|
+
account_id: account.id,
|
|
823
|
+
content,
|
|
824
|
+
media_urls: options?.media_urls,
|
|
825
|
+
status: options?.scheduled_at ? "scheduled" : "draft",
|
|
826
|
+
scheduled_at: options?.scheduled_at,
|
|
827
|
+
tags: options?.tags,
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
posts.push(post);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return { posts, warnings };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// ---- Best Time to Post ----
|
|
837
|
+
|
|
838
|
+
export interface BestTimeSlot {
|
|
839
|
+
day_of_week: number; // 0=Sunday, 6=Saturday
|
|
840
|
+
day_name: string;
|
|
841
|
+
hour: number;
|
|
842
|
+
avg_engagement: number;
|
|
843
|
+
post_count: number;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export interface BestTimeResult {
|
|
847
|
+
best_hours: BestTimeSlot[];
|
|
848
|
+
best_days: { day_of_week: number; day_name: string; avg_engagement: number; post_count: number }[];
|
|
849
|
+
total_analyzed: number;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Analyze historical engagement data to find best time to post
|
|
856
|
+
*/
|
|
857
|
+
export function getBestTimeToPost(accountId: string): BestTimeResult {
|
|
858
|
+
const db = getDatabase();
|
|
859
|
+
const rows = db.prepare(
|
|
860
|
+
"SELECT published_at, engagement FROM posts WHERE account_id = ? AND status = 'published' AND published_at IS NOT NULL"
|
|
861
|
+
).all(accountId) as { published_at: string; engagement: string }[];
|
|
862
|
+
|
|
863
|
+
if (rows.length === 0) {
|
|
864
|
+
return { best_hours: [], best_days: [], total_analyzed: 0 };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Group by day of week and hour
|
|
868
|
+
const hourBuckets: Record<string, { total_engagement: number; count: number; day: number; hour: number }> = {};
|
|
869
|
+
const dayBuckets: Record<number, { total_engagement: number; count: number }> = {};
|
|
870
|
+
|
|
871
|
+
for (const row of rows) {
|
|
872
|
+
const date = new Date(row.published_at.replace(" ", "T"));
|
|
873
|
+
const dayOfWeek = date.getUTCDay();
|
|
874
|
+
const hour = date.getUTCHours();
|
|
875
|
+
const engagement = JSON.parse(row.engagement || "{}") as Engagement;
|
|
876
|
+
const totalEng = (engagement.likes || 0) + (engagement.shares || 0) * 2 +
|
|
877
|
+
(engagement.comments || 0) * 3 + (engagement.clicks || 0);
|
|
878
|
+
|
|
879
|
+
const key = `${dayOfWeek}-${hour}`;
|
|
880
|
+
if (!hourBuckets[key]) {
|
|
881
|
+
hourBuckets[key] = { total_engagement: 0, count: 0, day: dayOfWeek, hour };
|
|
882
|
+
}
|
|
883
|
+
hourBuckets[key].total_engagement += totalEng;
|
|
884
|
+
hourBuckets[key].count += 1;
|
|
885
|
+
|
|
886
|
+
if (!dayBuckets[dayOfWeek]) {
|
|
887
|
+
dayBuckets[dayOfWeek] = { total_engagement: 0, count: 0 };
|
|
888
|
+
}
|
|
889
|
+
dayBuckets[dayOfWeek].total_engagement += totalEng;
|
|
890
|
+
dayBuckets[dayOfWeek].count += 1;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Sort hours by avg engagement
|
|
894
|
+
const best_hours: BestTimeSlot[] = Object.values(hourBuckets)
|
|
895
|
+
.map((b) => ({
|
|
896
|
+
day_of_week: b.day,
|
|
897
|
+
day_name: DAY_NAMES[b.day],
|
|
898
|
+
hour: b.hour,
|
|
899
|
+
avg_engagement: b.count > 0 ? Math.round(b.total_engagement / b.count) : 0,
|
|
900
|
+
post_count: b.count,
|
|
901
|
+
}))
|
|
902
|
+
.sort((a, b) => b.avg_engagement - a.avg_engagement)
|
|
903
|
+
.slice(0, 10);
|
|
904
|
+
|
|
905
|
+
// Sort days by avg engagement
|
|
906
|
+
const best_days = Object.entries(dayBuckets)
|
|
907
|
+
.map(([day, b]) => ({
|
|
908
|
+
day_of_week: parseInt(day),
|
|
909
|
+
day_name: DAY_NAMES[parseInt(day)],
|
|
910
|
+
avg_engagement: b.count > 0 ? Math.round(b.total_engagement / b.count) : 0,
|
|
911
|
+
post_count: b.count,
|
|
912
|
+
}))
|
|
913
|
+
.sort((a, b) => b.avg_engagement - a.avg_engagement);
|
|
914
|
+
|
|
915
|
+
return { best_hours, best_days, total_analyzed: rows.length };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ---- Reschedule ----
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Update scheduled_at on a post without delete/recreate
|
|
922
|
+
*/
|
|
923
|
+
export function reschedulePost(id: string, newDate: string): Post | null {
|
|
924
|
+
const post = getPost(id);
|
|
925
|
+
if (!post) return null;
|
|
926
|
+
|
|
927
|
+
if (post.status !== "scheduled" && post.status !== "draft") {
|
|
928
|
+
throw new Error(`Cannot reschedule post with status '${post.status}'. Must be 'draft' or 'scheduled'.`);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return updatePost(id, { scheduled_at: newDate, status: "scheduled" });
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ---- Approval Workflow ----
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Submit a draft post for review — moves draft→pending_review
|
|
938
|
+
*/
|
|
939
|
+
export function submitPostForReview(id: string): Post | null {
|
|
940
|
+
const post = getPost(id);
|
|
941
|
+
if (!post) return null;
|
|
942
|
+
|
|
943
|
+
if (post.status !== "draft") {
|
|
944
|
+
throw new Error(`Cannot submit post with status '${post.status}'. Must be 'draft'.`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return updatePost(id, { status: "pending_review" });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Approve a post — moves pending_review→scheduled (requires scheduled_at)
|
|
952
|
+
*/
|
|
953
|
+
export function approvePost(id: string, scheduledAt?: string): Post | null {
|
|
954
|
+
const post = getPost(id);
|
|
955
|
+
if (!post) return null;
|
|
956
|
+
|
|
957
|
+
if (post.status !== "pending_review") {
|
|
958
|
+
throw new Error(`Cannot approve post with status '${post.status}'. Must be 'pending_review'.`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const targetDate = scheduledAt || post.scheduled_at;
|
|
962
|
+
if (!targetDate) {
|
|
963
|
+
throw new Error("Cannot approve post without a scheduled date. Provide --at <datetime>.");
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return updatePost(id, { status: "scheduled", scheduled_at: targetDate });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ---- Recurring Posts ----
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Create a recurring post — sets recurrence and schedules the first instance
|
|
973
|
+
*/
|
|
974
|
+
export function createRecurringPost(input: CreatePostInput & { recurrence: Recurrence }): Post {
|
|
975
|
+
if (!input.scheduled_at) {
|
|
976
|
+
throw new Error("Recurring posts must have a scheduled_at date for the first occurrence.");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return createPost({
|
|
980
|
+
...input,
|
|
981
|
+
status: "scheduled",
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ---- Hashtag Analytics ----
|
|
986
|
+
|
|
987
|
+
export interface HashtagStat {
|
|
988
|
+
hashtag: string;
|
|
989
|
+
post_count: number;
|
|
990
|
+
total_likes: number;
|
|
991
|
+
total_shares: number;
|
|
992
|
+
total_comments: number;
|
|
993
|
+
total_impressions: number;
|
|
994
|
+
avg_engagement: number;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Extract hashtags from published posts and correlate with engagement
|
|
999
|
+
*/
|
|
1000
|
+
export function getHashtagStats(accountId: string): HashtagStat[] {
|
|
1001
|
+
const db = getDatabase();
|
|
1002
|
+
const rows = db.prepare(
|
|
1003
|
+
"SELECT content, engagement FROM posts WHERE account_id = ? AND status = 'published'"
|
|
1004
|
+
).all(accountId) as { content: string; engagement: string }[];
|
|
1005
|
+
|
|
1006
|
+
const hashtagMap: Record<string, {
|
|
1007
|
+
count: number;
|
|
1008
|
+
likes: number;
|
|
1009
|
+
shares: number;
|
|
1010
|
+
comments: number;
|
|
1011
|
+
impressions: number;
|
|
1012
|
+
}> = {};
|
|
1013
|
+
|
|
1014
|
+
const hashtagRegex = /#(\w+)/g;
|
|
1015
|
+
|
|
1016
|
+
for (const row of rows) {
|
|
1017
|
+
const engagement = JSON.parse(row.engagement || "{}") as Engagement;
|
|
1018
|
+
const matches = row.content.matchAll(hashtagRegex);
|
|
1019
|
+
|
|
1020
|
+
for (const match of matches) {
|
|
1021
|
+
const tag = match[1].toLowerCase();
|
|
1022
|
+
if (!hashtagMap[tag]) {
|
|
1023
|
+
hashtagMap[tag] = { count: 0, likes: 0, shares: 0, comments: 0, impressions: 0 };
|
|
1024
|
+
}
|
|
1025
|
+
hashtagMap[tag].count += 1;
|
|
1026
|
+
hashtagMap[tag].likes += engagement.likes || 0;
|
|
1027
|
+
hashtagMap[tag].shares += engagement.shares || 0;
|
|
1028
|
+
hashtagMap[tag].comments += engagement.comments || 0;
|
|
1029
|
+
hashtagMap[tag].impressions += engagement.impressions || 0;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return Object.entries(hashtagMap)
|
|
1034
|
+
.map(([hashtag, data]) => ({
|
|
1035
|
+
hashtag,
|
|
1036
|
+
post_count: data.count,
|
|
1037
|
+
total_likes: data.likes,
|
|
1038
|
+
total_shares: data.shares,
|
|
1039
|
+
total_comments: data.comments,
|
|
1040
|
+
total_impressions: data.impressions,
|
|
1041
|
+
avg_engagement: data.count > 0
|
|
1042
|
+
? Math.round((data.likes + data.shares * 2 + data.comments * 3) / data.count)
|
|
1043
|
+
: 0,
|
|
1044
|
+
}))
|
|
1045
|
+
.sort((a, b) => b.avg_engagement - a.avg_engagement);
|
|
1046
|
+
}
|
|
@@ -25,10 +25,20 @@ import {
|
|
|
25
25
|
getStatsByPlatform,
|
|
26
26
|
getCalendar,
|
|
27
27
|
getOverallStats,
|
|
28
|
+
batchSchedule,
|
|
29
|
+
crossPost,
|
|
30
|
+
getBestTimeToPost,
|
|
31
|
+
reschedulePost,
|
|
32
|
+
submitPostForReview,
|
|
33
|
+
approvePost,
|
|
34
|
+
createRecurringPost,
|
|
35
|
+
getHashtagStats,
|
|
36
|
+
PLATFORM_LIMITS,
|
|
28
37
|
} from "../db/social.js";
|
|
29
38
|
|
|
30
39
|
const PlatformEnum = z.enum(["x", "linkedin", "instagram", "threads", "bluesky"]);
|
|
31
|
-
const PostStatusEnum = z.enum(["draft", "scheduled", "published", "failed"]);
|
|
40
|
+
const PostStatusEnum = z.enum(["draft", "scheduled", "published", "failed", "pending_review"]);
|
|
41
|
+
const RecurrenceEnum = z.enum(["daily", "weekly", "biweekly", "monthly"]);
|
|
32
42
|
|
|
33
43
|
const server = new McpServer({
|
|
34
44
|
name: "microservice-social",
|
|
@@ -422,6 +432,216 @@ server.registerTool(
|
|
|
422
432
|
}
|
|
423
433
|
);
|
|
424
434
|
|
|
435
|
+
// --- Batch Schedule ---
|
|
436
|
+
|
|
437
|
+
server.registerTool(
|
|
438
|
+
"batch_schedule_posts",
|
|
439
|
+
{
|
|
440
|
+
title: "Batch Schedule Posts",
|
|
441
|
+
description: "Schedule multiple posts at once from an array of post definitions.",
|
|
442
|
+
inputSchema: {
|
|
443
|
+
posts: z.array(z.object({
|
|
444
|
+
account_id: z.string(),
|
|
445
|
+
content: z.string(),
|
|
446
|
+
scheduled_at: z.string(),
|
|
447
|
+
media_urls: z.array(z.string()).optional(),
|
|
448
|
+
tags: z.array(z.string()).optional(),
|
|
449
|
+
recurrence: RecurrenceEnum.optional(),
|
|
450
|
+
})),
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
async ({ posts }) => {
|
|
454
|
+
const result = batchSchedule(posts);
|
|
455
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// --- Cross-Post ---
|
|
460
|
+
|
|
461
|
+
server.registerTool(
|
|
462
|
+
"crosspost",
|
|
463
|
+
{
|
|
464
|
+
title: "Cross-Post",
|
|
465
|
+
description: "Create identical post on multiple platform accounts.",
|
|
466
|
+
inputSchema: {
|
|
467
|
+
content: z.string(),
|
|
468
|
+
platforms: z.array(PlatformEnum),
|
|
469
|
+
media_urls: z.array(z.string()).optional(),
|
|
470
|
+
tags: z.array(z.string()).optional(),
|
|
471
|
+
scheduled_at: z.string().optional(),
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
async ({ content, platforms, ...options }) => {
|
|
475
|
+
try {
|
|
476
|
+
const result = crossPost(content, platforms, options);
|
|
477
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return {
|
|
480
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
481
|
+
isError: true,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// --- Best Time to Post ---
|
|
488
|
+
|
|
489
|
+
server.registerTool(
|
|
490
|
+
"best_time_to_post",
|
|
491
|
+
{
|
|
492
|
+
title: "Best Time to Post",
|
|
493
|
+
description: "Analyze historical engagement to find the best hours and days to post.",
|
|
494
|
+
inputSchema: {
|
|
495
|
+
account_id: z.string(),
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
async ({ account_id }) => {
|
|
499
|
+
const result = getBestTimeToPost(account_id);
|
|
500
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// --- Reschedule Post ---
|
|
505
|
+
|
|
506
|
+
server.registerTool(
|
|
507
|
+
"reschedule_post",
|
|
508
|
+
{
|
|
509
|
+
title: "Reschedule Post",
|
|
510
|
+
description: "Update the scheduled date/time of a post without delete/recreate.",
|
|
511
|
+
inputSchema: {
|
|
512
|
+
id: z.string(),
|
|
513
|
+
new_date: z.string(),
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
async ({ id, new_date }) => {
|
|
517
|
+
try {
|
|
518
|
+
const post = reschedulePost(id, new_date);
|
|
519
|
+
if (!post) {
|
|
520
|
+
return { content: [{ type: "text", text: `Post '${id}' not found.` }], isError: true };
|
|
521
|
+
}
|
|
522
|
+
return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return {
|
|
525
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
526
|
+
isError: true,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// --- Approval Workflow ---
|
|
533
|
+
|
|
534
|
+
server.registerTool(
|
|
535
|
+
"submit_post_for_review",
|
|
536
|
+
{
|
|
537
|
+
title: "Submit Post for Review",
|
|
538
|
+
description: "Submit a draft post for review (draft → pending_review).",
|
|
539
|
+
inputSchema: {
|
|
540
|
+
id: z.string(),
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
async ({ id }) => {
|
|
544
|
+
try {
|
|
545
|
+
const post = submitPostForReview(id);
|
|
546
|
+
if (!post) {
|
|
547
|
+
return { content: [{ type: "text", text: `Post '${id}' not found.` }], isError: true };
|
|
548
|
+
}
|
|
549
|
+
return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return {
|
|
552
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
553
|
+
isError: true,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
server.registerTool(
|
|
560
|
+
"approve_post",
|
|
561
|
+
{
|
|
562
|
+
title: "Approve Post",
|
|
563
|
+
description: "Approve a post pending review (pending_review → scheduled).",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
id: z.string(),
|
|
566
|
+
scheduled_at: z.string().optional(),
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
async ({ id, scheduled_at }) => {
|
|
570
|
+
try {
|
|
571
|
+
const post = approvePost(id, scheduled_at);
|
|
572
|
+
if (!post) {
|
|
573
|
+
return { content: [{ type: "text", text: `Post '${id}' not found.` }], isError: true };
|
|
574
|
+
}
|
|
575
|
+
return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
|
|
576
|
+
} catch (error) {
|
|
577
|
+
return {
|
|
578
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
579
|
+
isError: true,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// --- Platform Limits ---
|
|
586
|
+
|
|
587
|
+
server.registerTool(
|
|
588
|
+
"get_platform_limits",
|
|
589
|
+
{
|
|
590
|
+
title: "Get Platform Limits",
|
|
591
|
+
description: "Get character limits for all supported platforms.",
|
|
592
|
+
inputSchema: {},
|
|
593
|
+
},
|
|
594
|
+
async () => {
|
|
595
|
+
return { content: [{ type: "text", text: JSON.stringify(PLATFORM_LIMITS, null, 2) }] };
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
// --- Recurring Posts ---
|
|
600
|
+
|
|
601
|
+
server.registerTool(
|
|
602
|
+
"create_recurring_post",
|
|
603
|
+
{
|
|
604
|
+
title: "Create Recurring Post",
|
|
605
|
+
description: "Create a post that recurs on a schedule (daily, weekly, biweekly, monthly).",
|
|
606
|
+
inputSchema: {
|
|
607
|
+
account_id: z.string(),
|
|
608
|
+
content: z.string(),
|
|
609
|
+
scheduled_at: z.string(),
|
|
610
|
+
recurrence: RecurrenceEnum,
|
|
611
|
+
media_urls: z.array(z.string()).optional(),
|
|
612
|
+
tags: z.array(z.string()).optional(),
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
async (params) => {
|
|
616
|
+
try {
|
|
617
|
+
const post = createRecurringPost(params);
|
|
618
|
+
return { content: [{ type: "text", text: JSON.stringify(post, null, 2) }] };
|
|
619
|
+
} catch (error) {
|
|
620
|
+
return {
|
|
621
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
622
|
+
isError: true,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// --- Hashtag Analytics ---
|
|
629
|
+
|
|
630
|
+
server.registerTool(
|
|
631
|
+
"hashtag_analytics",
|
|
632
|
+
{
|
|
633
|
+
title: "Hashtag Analytics",
|
|
634
|
+
description: "Extract hashtags from published posts and correlate with engagement metrics.",
|
|
635
|
+
inputSchema: {
|
|
636
|
+
account_id: z.string(),
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
async ({ account_id }) => {
|
|
640
|
+
const stats = getHashtagStats(account_id);
|
|
641
|
+
return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
|
|
642
|
+
}
|
|
643
|
+
);
|
|
644
|
+
|
|
425
645
|
// --- Start ---
|
|
426
646
|
async function main() {
|
|
427
647
|
const transport = new StdioServerTransport();
|