@socialneuron/mcp-server 1.2.1 → 1.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.
package/dist/index.js CHANGED
@@ -9,6 +9,15 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/lib/version.ts
13
+ var MCP_VERSION;
14
+ var init_version = __esm({
15
+ "src/lib/version.ts"() {
16
+ "use strict";
17
+ MCP_VERSION = "1.3.0";
18
+ }
19
+ });
20
+
12
21
  // src/lib/posthog.ts
13
22
  import { createHash } from "node:crypto";
14
23
  import { PostHog } from "posthog-node";
@@ -802,12 +811,364 @@ var init_quality = __esm({
802
811
  }
803
812
  });
804
813
 
805
- // src/cli/sn.ts
806
- var sn_exports = {};
807
- __export(sn_exports, {
808
- printSnUsage: () => printSnUsage,
809
- runSnCli: () => runSnCli
814
+ // src/lib/tool-catalog.ts
815
+ function getToolsByModule(module) {
816
+ return TOOL_CATALOG.filter((t) => t.module === module);
817
+ }
818
+ function getToolsByScope(scope) {
819
+ return TOOL_CATALOG.filter((t) => t.scope === scope);
820
+ }
821
+ function searchTools(query) {
822
+ const q = query.toLowerCase();
823
+ return TOOL_CATALOG.filter(
824
+ (t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q)
825
+ );
826
+ }
827
+ function getModules() {
828
+ return [...new Set(TOOL_CATALOG.map((t) => t.module))];
829
+ }
830
+ var TOOL_CATALOG;
831
+ var init_tool_catalog = __esm({
832
+ "src/lib/tool-catalog.ts"() {
833
+ "use strict";
834
+ TOOL_CATALOG = [
835
+ // ideation
836
+ {
837
+ name: "generate_content",
838
+ description: "Generate social media content ideas based on brand profile and trends",
839
+ module: "ideation",
840
+ scope: "mcp:write"
841
+ },
842
+ {
843
+ name: "fetch_trends",
844
+ description: "Fetch current trending topics for content ideation",
845
+ module: "ideation",
846
+ scope: "mcp:read"
847
+ },
848
+ // ideation-context
849
+ {
850
+ name: "get_ideation_context",
851
+ description: "Get full ideation context including brand, analytics, and trends",
852
+ module: "ideation-context",
853
+ scope: "mcp:read"
854
+ },
855
+ // content
856
+ {
857
+ name: "adapt_content",
858
+ description: "Adapt existing content for different platforms",
859
+ module: "content",
860
+ scope: "mcp:write"
861
+ },
862
+ {
863
+ name: "generate_video",
864
+ description: "Generate video content using AI",
865
+ module: "content",
866
+ scope: "mcp:write"
867
+ },
868
+ {
869
+ name: "generate_image",
870
+ description: "Generate images using AI",
871
+ module: "content",
872
+ scope: "mcp:write"
873
+ },
874
+ {
875
+ name: "check_status",
876
+ description: "Check status of async content generation job",
877
+ module: "content",
878
+ scope: "mcp:write"
879
+ },
880
+ {
881
+ name: "create_storyboard",
882
+ description: "Create a video storyboard with scenes and shots",
883
+ module: "content",
884
+ scope: "mcp:write"
885
+ },
886
+ {
887
+ name: "generate_voiceover",
888
+ description: "Generate AI voiceover audio",
889
+ module: "content",
890
+ scope: "mcp:write"
891
+ },
892
+ {
893
+ name: "generate_carousel",
894
+ description: "Generate carousel/slide content",
895
+ module: "content",
896
+ scope: "mcp:write"
897
+ },
898
+ // distribution
899
+ {
900
+ name: "schedule_post",
901
+ description: "Schedule content for publishing to social platforms",
902
+ module: "distribution",
903
+ scope: "mcp:distribute"
904
+ },
905
+ {
906
+ name: "list_recent_posts",
907
+ description: "List recently published or scheduled posts",
908
+ module: "distribution",
909
+ scope: "mcp:read"
910
+ },
911
+ {
912
+ name: "list_connected_accounts",
913
+ description: "List connected social media accounts",
914
+ module: "distribution",
915
+ scope: "mcp:read"
916
+ },
917
+ // analytics
918
+ {
919
+ name: "fetch_analytics",
920
+ description: "Fetch post performance analytics",
921
+ module: "analytics",
922
+ scope: "mcp:read"
923
+ },
924
+ {
925
+ name: "refresh_platform_analytics",
926
+ description: "Refresh analytics data from connected platforms",
927
+ module: "analytics",
928
+ scope: "mcp:analytics"
929
+ },
930
+ // insights
931
+ {
932
+ name: "get_performance_insights",
933
+ description: "Get AI-generated performance insights",
934
+ module: "insights",
935
+ scope: "mcp:read"
936
+ },
937
+ {
938
+ name: "get_best_posting_times",
939
+ description: "Get recommended posting times based on audience data",
940
+ module: "insights",
941
+ scope: "mcp:read"
942
+ },
943
+ // brand
944
+ {
945
+ name: "extract_brand",
946
+ description: "Extract brand identity from URL or text",
947
+ module: "brand",
948
+ scope: "mcp:read"
949
+ },
950
+ {
951
+ name: "get_brand_profile",
952
+ description: "Get the current brand profile",
953
+ module: "brand",
954
+ scope: "mcp:read"
955
+ },
956
+ {
957
+ name: "save_brand_profile",
958
+ description: "Save or update brand profile",
959
+ module: "brand",
960
+ scope: "mcp:write"
961
+ },
962
+ {
963
+ name: "update_platform_voice",
964
+ description: "Update platform-specific brand voice settings",
965
+ module: "brand",
966
+ scope: "mcp:write"
967
+ },
968
+ // screenshot
969
+ {
970
+ name: "capture_screenshot",
971
+ description: "Capture a screenshot of a URL",
972
+ module: "screenshot",
973
+ scope: "mcp:read"
974
+ },
975
+ {
976
+ name: "capture_app_page",
977
+ description: "Capture a screenshot of an app page",
978
+ module: "screenshot",
979
+ scope: "mcp:read"
980
+ },
981
+ // remotion
982
+ {
983
+ name: "render_demo_video",
984
+ description: "Render a demo video using Remotion",
985
+ module: "remotion",
986
+ scope: "mcp:write"
987
+ },
988
+ {
989
+ name: "list_compositions",
990
+ description: "List available Remotion video compositions",
991
+ module: "remotion",
992
+ scope: "mcp:read"
993
+ },
994
+ // youtube-analytics
995
+ {
996
+ name: "fetch_youtube_analytics",
997
+ description: "Fetch YouTube channel analytics data",
998
+ module: "youtube-analytics",
999
+ scope: "mcp:analytics"
1000
+ },
1001
+ // comments
1002
+ {
1003
+ name: "list_comments",
1004
+ description: "List comments on published posts",
1005
+ module: "comments",
1006
+ scope: "mcp:comments"
1007
+ },
1008
+ {
1009
+ name: "reply_to_comment",
1010
+ description: "Reply to a comment on a post",
1011
+ module: "comments",
1012
+ scope: "mcp:comments"
1013
+ },
1014
+ {
1015
+ name: "post_comment",
1016
+ description: "Post a new comment",
1017
+ module: "comments",
1018
+ scope: "mcp:comments"
1019
+ },
1020
+ {
1021
+ name: "moderate_comment",
1022
+ description: "Moderate a comment (approve/hide/flag)",
1023
+ module: "comments",
1024
+ scope: "mcp:comments"
1025
+ },
1026
+ {
1027
+ name: "delete_comment",
1028
+ description: "Delete a comment",
1029
+ module: "comments",
1030
+ scope: "mcp:comments"
1031
+ },
1032
+ // planning
1033
+ {
1034
+ name: "plan_content_week",
1035
+ description: "Generate a weekly content plan",
1036
+ module: "planning",
1037
+ scope: "mcp:write"
1038
+ },
1039
+ {
1040
+ name: "save_content_plan",
1041
+ description: "Save a content plan",
1042
+ module: "planning",
1043
+ scope: "mcp:write"
1044
+ },
1045
+ {
1046
+ name: "get_content_plan",
1047
+ description: "Get a specific content plan by ID",
1048
+ module: "planning",
1049
+ scope: "mcp:read"
1050
+ },
1051
+ {
1052
+ name: "update_content_plan",
1053
+ description: "Update an existing content plan",
1054
+ module: "planning",
1055
+ scope: "mcp:write"
1056
+ },
1057
+ {
1058
+ name: "submit_content_plan_for_approval",
1059
+ description: "Submit a content plan for team approval",
1060
+ module: "planning",
1061
+ scope: "mcp:write"
1062
+ },
1063
+ {
1064
+ name: "schedule_content_plan",
1065
+ description: "Schedule all posts in an approved content plan",
1066
+ module: "planning",
1067
+ scope: "mcp:distribute"
1068
+ },
1069
+ {
1070
+ name: "find_next_slots",
1071
+ description: "Find next available scheduling slots",
1072
+ module: "planning",
1073
+ scope: "mcp:read"
1074
+ },
1075
+ // plan-approvals
1076
+ {
1077
+ name: "create_plan_approvals",
1078
+ description: "Create approval requests for a content plan",
1079
+ module: "plan-approvals",
1080
+ scope: "mcp:write"
1081
+ },
1082
+ {
1083
+ name: "respond_plan_approval",
1084
+ description: "Respond to a plan approval request",
1085
+ module: "plan-approvals",
1086
+ scope: "mcp:write"
1087
+ },
1088
+ {
1089
+ name: "list_plan_approvals",
1090
+ description: "List pending plan approval requests",
1091
+ module: "plan-approvals",
1092
+ scope: "mcp:read"
1093
+ },
1094
+ // quality
1095
+ {
1096
+ name: "quality_check",
1097
+ description: "Run quality checks on content before publishing",
1098
+ module: "quality",
1099
+ scope: "mcp:read"
1100
+ },
1101
+ {
1102
+ name: "quality_check_plan",
1103
+ description: "Run quality checks on an entire content plan",
1104
+ module: "quality",
1105
+ scope: "mcp:read"
1106
+ },
1107
+ // credits
1108
+ {
1109
+ name: "get_credit_balance",
1110
+ description: "Get current credit balance",
1111
+ module: "credits",
1112
+ scope: "mcp:read"
1113
+ },
1114
+ {
1115
+ name: "get_budget_status",
1116
+ description: "Get budget and spending status",
1117
+ module: "credits",
1118
+ scope: "mcp:read"
1119
+ },
1120
+ // autopilot
1121
+ {
1122
+ name: "list_autopilot_configs",
1123
+ description: "List autopilot configurations",
1124
+ module: "autopilot",
1125
+ scope: "mcp:autopilot"
1126
+ },
1127
+ {
1128
+ name: "update_autopilot_config",
1129
+ description: "Update autopilot configuration",
1130
+ module: "autopilot",
1131
+ scope: "mcp:autopilot"
1132
+ },
1133
+ {
1134
+ name: "get_autopilot_status",
1135
+ description: "Get current autopilot status",
1136
+ module: "autopilot",
1137
+ scope: "mcp:autopilot"
1138
+ },
1139
+ // extraction
1140
+ {
1141
+ name: "extract_url_content",
1142
+ description: "Extract content from a URL for repurposing",
1143
+ module: "extraction",
1144
+ scope: "mcp:read"
1145
+ },
1146
+ // loop-summary
1147
+ {
1148
+ name: "get_loop_summary",
1149
+ description: "Get growth loop summary and recommendations",
1150
+ module: "loop-summary",
1151
+ scope: "mcp:read"
1152
+ },
1153
+ // usage
1154
+ {
1155
+ name: "get_mcp_usage",
1156
+ description: "Get MCP usage statistics for the current billing period",
1157
+ module: "usage",
1158
+ scope: "mcp:read"
1159
+ },
1160
+ // discovery
1161
+ {
1162
+ name: "search_tools",
1163
+ description: "Search and discover available MCP tools",
1164
+ module: "discovery",
1165
+ scope: "mcp:read"
1166
+ }
1167
+ ];
1168
+ }
810
1169
  });
1170
+
1171
+ // src/cli/sn/parse.ts
811
1172
  import { createHash as createHash3 } from "node:crypto";
812
1173
  function parseSnArgs(argv) {
813
1174
  const parsed = { _: [] };
@@ -828,34 +1189,6 @@ function parseSnArgs(argv) {
828
1189
  }
829
1190
  return parsed;
830
1191
  }
831
- function printSnUsage() {
832
- console.error("");
833
- console.error("Usage: socialneuron-mcp sn <command> [flags]");
834
- console.error("");
835
- console.error("Commands:");
836
- console.error(
837
- " publish --media-url <url> --caption <text> --platforms <comma-list> --confirm [--title <text>] [--schedule-at <iso8601>] [--idempotency-key <key>] [--json]"
838
- );
839
- console.error(" preflight [--privacy-url <url>] [--terms-url <url>] [--check-urls] [--json]");
840
- console.error(" oauth-health [--warn-days <1-90>] [--platforms <comma-list>] [--all] [--json]");
841
- console.error(" oauth-refresh (--platforms <comma-list> | --all) [--json]");
842
- console.error(
843
- " quality-check --caption <text> [--title <text>] [--platforms <comma-list>] [--threshold <0-35>] [--json]"
844
- );
845
- console.error(
846
- " e2e --media-url <url> --caption <text> --platforms <comma-list> --confirm [--title <text>] [--schedule-at <iso8601>] [--check-urls] [--threshold <0-35>] [--dry-run] [--force] [--json]"
847
- );
848
- console.error(" status --job-id <id> [--json]");
849
- console.error(
850
- " posts [--days <1-90>] [--platform <name>] [--status <draft|scheduled|published|failed>] [--json]"
851
- );
852
- console.error(" refresh-analytics [--json]");
853
- console.error(" autopilot [--json]");
854
- console.error(" usage [--json]");
855
- console.error(" loop [--json]");
856
- console.error(" credits [--json]");
857
- console.error("");
858
- }
859
1192
  function isEnabledFlag(value) {
860
1193
  if (value === true) return true;
861
1194
  if (typeof value === "string") {
@@ -866,7 +1199,8 @@ function isEnabledFlag(value) {
866
1199
  }
867
1200
  function emitSnResult(payload, asJson) {
868
1201
  if (asJson) {
869
- process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
1202
+ const envelope = { schema_version: "1", ...payload };
1203
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
870
1204
  }
871
1205
  }
872
1206
  function isValidHttpsUrl(value) {
@@ -953,1057 +1287,1646 @@ function tryGetSupabaseClient() {
953
1287
  return null;
954
1288
  }
955
1289
  }
956
- async function runSnCli(argv) {
957
- const [subcommand, ...rest] = argv;
958
- if (!subcommand) {
959
- printSnUsage();
960
- process.exit(1);
1290
+ var init_parse = __esm({
1291
+ "src/cli/sn/parse.ts"() {
1292
+ "use strict";
1293
+ init_supabase();
961
1294
  }
962
- const args = parseSnArgs(rest);
963
- const userId = await getDefaultUserId();
964
- const asJson = isEnabledFlag(args.json);
965
- if (subcommand === "publish") {
966
- const mediaUrl = args["media-url"];
967
- const caption = args.caption;
968
- const platformsRaw = args.platforms;
969
- if (typeof mediaUrl !== "string" || typeof caption !== "string" || typeof platformsRaw !== "string") {
970
- console.error("Missing required flags for publish.");
971
- printSnUsage();
972
- process.exit(1);
973
- }
974
- const confirmed = isEnabledFlag(args.confirm);
975
- if (!confirmed) {
976
- const message = "Publish confirmation required. Re-run with --confirm to execute schedule-post.";
977
- if (asJson) {
978
- emitSnResult(
979
- {
980
- ok: false,
981
- command: "publish",
982
- error: message,
983
- hint: "Use --confirm (or --confirm true)."
984
- },
985
- true
986
- );
987
- } else {
988
- console.error(message);
989
- }
990
- process.exit(1);
991
- }
992
- const caseMap = {
993
- youtube: "YouTube",
994
- tiktok: "TikTok",
995
- instagram: "Instagram",
996
- twitter: "Twitter",
997
- linkedin: "LinkedIn",
998
- facebook: "Facebook",
999
- threads: "Threads",
1000
- bluesky: "Bluesky"
1295
+ });
1296
+
1297
+ // src/cli/error-handling.ts
1298
+ function classifyError(err) {
1299
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err ?? "Unknown error");
1300
+ const lower = message.toLowerCase();
1301
+ if (lower.includes("no authentication") || lower.includes("unauthorized") || lower.includes("api key invalid") || lower.includes("expired") || lower.includes("not logged in") || lower.includes("invalid api key") || lower.includes("invalid signature")) {
1302
+ return {
1303
+ message,
1304
+ errorType: "AUTH",
1305
+ retryable: false,
1306
+ hint: "Run: socialneuron-mcp login"
1001
1307
  };
1002
- const platforms = platformsRaw.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean).map((p) => caseMap[p] ?? p);
1003
- const title = typeof args.title === "string" ? args.title : void 0;
1004
- const scheduledAt = typeof args["schedule-at"] === "string" ? args["schedule-at"] : void 0;
1005
- const idempotencyKey = typeof args["idempotency-key"] === "string" ? args["idempotency-key"] : buildPublishIdempotencyKey({
1006
- mediaUrl,
1007
- caption,
1008
- platforms,
1009
- title,
1010
- scheduledAt
1011
- });
1012
- const { data, error } = await callEdgeFunction("schedule-post", {
1013
- mediaUrl,
1014
- caption,
1015
- platforms,
1016
- title,
1017
- scheduledAt,
1018
- idempotencyKey,
1019
- userId
1020
- });
1021
- if (error || !data) {
1022
- const message = `Publish failed: ${error ?? "Unknown error"}`;
1023
- if (asJson) {
1024
- emitSnResult({ ok: false, command: "publish", error: message }, true);
1025
- } else {
1026
- console.error(message);
1027
- }
1028
- process.exit(1);
1029
- }
1030
- if (asJson) {
1031
- emitSnResult(
1032
- {
1033
- ok: data.success,
1034
- command: "publish",
1035
- idempotencyKey,
1036
- scheduledAt: data.scheduledAt,
1037
- results: data.results
1038
- },
1039
- true
1040
- );
1041
- } else {
1042
- console.error(`Idempotency key: ${idempotencyKey}`);
1043
- console.error(`Scheduled for: ${data.scheduledAt}`);
1044
- for (const [platform3, result] of Object.entries(
1045
- data.results
1046
- )) {
1047
- if (result.success) {
1048
- console.error(`${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`);
1049
- } else {
1050
- console.error(`${platform3}: FAILED (${result.error})`);
1051
- }
1052
- }
1053
- }
1054
- process.exit(data.success ? 0 : 1);
1055
1308
  }
1056
- if (subcommand === "quality-check") {
1057
- const caption = args.caption;
1058
- if (typeof caption !== "string") {
1059
- const message = "Missing required flag: --caption";
1060
- if (asJson) {
1061
- emitSnResult({ ok: false, command: "quality-check", error: message }, true);
1062
- } else {
1063
- console.error(message);
1064
- printSnUsage();
1065
- }
1066
- process.exit(1);
1067
- }
1068
- const title = typeof args.title === "string" ? args.title : void 0;
1069
- const platformsRaw = typeof args.platforms === "string" ? args.platforms : "youtube";
1070
- const platformsNormalized = normalizePlatforms(platformsRaw);
1071
- const thresholdRaw = typeof args.threshold === "string" ? Number(args.threshold) : void 0;
1072
- const quality = evaluateQuality({
1073
- caption,
1074
- title,
1075
- platforms: platformsNormalized,
1076
- threshold: Number.isFinite(thresholdRaw) ? thresholdRaw : void 0
1077
- });
1078
- const payload = {
1079
- ok: quality.passed,
1080
- command: "quality-check",
1081
- platforms: platformsNormalized,
1082
- threshold: quality.threshold,
1083
- score: quality.total,
1084
- maxScore: quality.maxTotal,
1085
- blockers: quality.blockers,
1086
- categories: quality.categories
1309
+ if (lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("fetch failed") || lower.includes("aborterror") || lower.includes("network") || lower.includes("dns")) {
1310
+ return {
1311
+ message,
1312
+ errorType: "NETWORK",
1313
+ retryable: true,
1314
+ hint: "Check your network connection and try again."
1315
+ };
1316
+ }
1317
+ if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
1318
+ return {
1319
+ message,
1320
+ errorType: "RATE_LIMIT",
1321
+ retryable: true,
1322
+ hint: "Wait a moment and retry."
1087
1323
  };
1324
+ }
1325
+ if (lower.includes("not found") || lower.includes("404") || lower.includes("no job found")) {
1326
+ return { message, errorType: "NOT_FOUND", retryable: false };
1327
+ }
1328
+ if (lower.includes("missing required") || lower.includes("invalid")) {
1329
+ return { message, errorType: "VALIDATION", retryable: false };
1330
+ }
1331
+ return { message, errorType: "INTERNAL", retryable: true };
1332
+ }
1333
+ async function withSnErrorHandling(command2, asJson, fn, replMode = false) {
1334
+ try {
1335
+ await fn();
1336
+ } catch (err) {
1337
+ const classified = classifyError(err);
1088
1338
  if (asJson) {
1089
- emitSnResult(payload, true);
1339
+ const response = {
1340
+ ok: false,
1341
+ command: command2,
1342
+ error: classified.message,
1343
+ errorType: classified.errorType,
1344
+ retryable: classified.retryable,
1345
+ ...classified.hint ? { hint: classified.hint } : {},
1346
+ schema_version: "1"
1347
+ };
1348
+ process.stdout.write(JSON.stringify(response, null, 2) + "\n");
1090
1349
  } else {
1091
- console.error(
1092
- "QUALITY SCORE: " + quality.total + "/" + quality.maxTotal + " (threshold " + quality.threshold + ")"
1093
- );
1094
- for (const c of quality.categories) {
1095
- console.error("- " + c.name + ": " + c.score + "/" + c.maxScore);
1096
- }
1097
- if (quality.blockers.length) {
1098
- console.error("Blockers: " + quality.blockers.join("; "));
1099
- }
1100
- console.error("Decision: " + (quality.passed ? "Publish-ready" : "Needs revision"));
1350
+ process.stderr.write(`Error [${command2}]: ${classified.message}
1351
+ `);
1352
+ if (classified.hint) process.stderr.write(`Hint: ${classified.hint}
1353
+ `);
1354
+ }
1355
+ if (!replMode) {
1356
+ process.exit(1);
1101
1357
  }
1102
- process.exit(quality.passed ? 0 : 1);
1103
1358
  }
1104
- if (subcommand === "e2e") {
1105
- const mediaUrl = args["media-url"];
1106
- const caption = args.caption;
1107
- const platformsRaw = args.platforms;
1108
- if (typeof mediaUrl !== "string" || typeof caption !== "string" || typeof platformsRaw !== "string") {
1109
- const message = "Missing required flags for e2e: --media-url, --caption, --platforms";
1110
- if (asJson) {
1111
- emitSnResult({ ok: false, command: "e2e", error: message }, true);
1359
+ }
1360
+ var init_error_handling = __esm({
1361
+ "src/cli/error-handling.ts"() {
1362
+ "use strict";
1363
+ }
1364
+ });
1365
+
1366
+ // src/cli/sn/content.ts
1367
+ var content_exports = {};
1368
+ __export(content_exports, {
1369
+ handleE2e: () => handleE2e,
1370
+ handlePublish: () => handlePublish,
1371
+ handleQualityCheck: () => handleQualityCheck
1372
+ });
1373
+ async function ensureAuth() {
1374
+ await initializeAuth();
1375
+ return getDefaultUserId();
1376
+ }
1377
+ async function handlePublish(args, asJson) {
1378
+ const mediaUrl = args["media-url"];
1379
+ const caption = args.caption;
1380
+ const platformsRaw = args.platforms;
1381
+ if (typeof mediaUrl !== "string" || typeof caption !== "string" || typeof platformsRaw !== "string") {
1382
+ throw new Error("Missing required flags for publish: --media-url, --caption, --platforms");
1383
+ }
1384
+ const confirmed = isEnabledFlag(args.confirm);
1385
+ if (!confirmed) {
1386
+ throw new Error(
1387
+ "Missing required flag: --confirm. Re-run with --confirm to execute schedule-post."
1388
+ );
1389
+ }
1390
+ const platforms = normalizePlatforms(platformsRaw);
1391
+ const title = typeof args.title === "string" ? args.title : void 0;
1392
+ const scheduledAt = typeof args["schedule-at"] === "string" ? args["schedule-at"] : void 0;
1393
+ const idempotencyKey = typeof args["idempotency-key"] === "string" ? args["idempotency-key"] : buildPublishIdempotencyKey({
1394
+ mediaUrl,
1395
+ caption,
1396
+ platforms,
1397
+ title,
1398
+ scheduledAt
1399
+ });
1400
+ const userId = await ensureAuth();
1401
+ const { data, error } = await callEdgeFunction("schedule-post", {
1402
+ mediaUrl,
1403
+ caption,
1404
+ platforms,
1405
+ title,
1406
+ scheduledAt,
1407
+ idempotencyKey,
1408
+ userId
1409
+ });
1410
+ if (error || !data) {
1411
+ throw new Error(`Publish failed: ${error ?? "Unknown error"}`);
1412
+ }
1413
+ if (asJson) {
1414
+ emitSnResult(
1415
+ {
1416
+ ok: data.success,
1417
+ command: "publish",
1418
+ idempotencyKey,
1419
+ scheduledAt: data.scheduledAt,
1420
+ results: data.results
1421
+ },
1422
+ true
1423
+ );
1424
+ } else {
1425
+ console.error(`Idempotency key: ${idempotencyKey}`);
1426
+ console.error(`Scheduled for: ${data.scheduledAt}`);
1427
+ for (const [platform3, result] of Object.entries(
1428
+ data.results
1429
+ )) {
1430
+ if (result.success) {
1431
+ console.error(`${platform3}: OK (jobId=${result.jobId}, postId=${result.postId})`);
1112
1432
  } else {
1113
- console.error(message);
1114
- printSnUsage();
1433
+ console.error(`${platform3}: FAILED (${result.error})`);
1115
1434
  }
1116
- process.exit(1);
1117
1435
  }
1118
- const title = typeof args.title === "string" ? args.title : void 0;
1119
- const scheduledAt = typeof args["schedule-at"] === "string" ? args["schedule-at"] : void 0;
1120
- const checkUrls = isEnabledFlag(args["check-urls"]);
1121
- const dryRun = isEnabledFlag(args["dry-run"]);
1122
- const force = isEnabledFlag(args.force);
1123
- const platformsNormalized = normalizePlatforms(platformsRaw);
1124
- const thresholdRaw = typeof args.threshold === "string" ? Number(args.threshold) : void 0;
1125
- const supabase = tryGetSupabaseClient();
1126
- const privacyUrl = process.env.SOCIALNEURON_PRIVACY_POLICY_URL ?? null;
1127
- const termsUrl = process.env.SOCIALNEURON_TERMS_URL ?? null;
1128
- const preflightChecks = [];
1129
- preflightChecks.push({
1130
- name: "privacy_policy_url_present",
1131
- ok: Boolean(privacyUrl),
1132
- detail: privacyUrl ? privacyUrl : "Missing SOCIALNEURON_PRIVACY_POLICY_URL"
1133
- });
1134
- preflightChecks.push({
1135
- name: "terms_url_present",
1136
- ok: Boolean(termsUrl),
1137
- detail: termsUrl ? termsUrl : "Missing SOCIALNEURON_TERMS_URL"
1138
- });
1139
- if (checkUrls && privacyUrl && isValidHttpsUrl(privacyUrl)) {
1140
- const r = await checkUrlReachability(privacyUrl);
1141
- preflightChecks.push({
1142
- name: "privacy_policy_url_reachable",
1143
- ok: r.ok,
1144
- detail: r.ok ? "Reachable (HTTP " + (r.status ?? 200) + ")" : "Unreachable (" + (r.error ?? "HTTP " + (r.status ?? "unknown")) + ")"
1145
- });
1146
- }
1147
- if (checkUrls && termsUrl && isValidHttpsUrl(termsUrl)) {
1148
- const r = await checkUrlReachability(termsUrl);
1149
- preflightChecks.push({
1150
- name: "terms_url_reachable",
1151
- ok: r.ok,
1152
- detail: r.ok ? "Reachable (HTTP " + (r.status ?? 200) + ")" : "Unreachable (" + (r.error ?? "HTTP " + (r.status ?? "unknown")) + ")"
1153
- });
1154
- }
1155
- let activeAccounts = [];
1156
- if (supabase) {
1157
- const { data: accounts, error: accountsError } = await supabase.from("connected_accounts").select("platform, status, username, expires_at").eq("user_id", userId).eq("status", "active");
1158
- if (accountsError) {
1159
- const formatted = classifySupabaseCliError("load connected accounts", accountsError);
1160
- if (asJson) {
1161
- emitSnResult(
1162
- { ok: false, command: "e2e", error: formatted.message, hint: formatted.hint },
1163
- true
1164
- );
1165
- } else {
1166
- console.error(formatted.message);
1167
- if (formatted.hint) console.error("Hint: " + formatted.hint);
1168
- }
1169
- process.exit(1);
1170
- }
1171
- activeAccounts = accounts ?? [];
1172
- } else {
1173
- const { data: data2, error: error2 } = await callEdgeFunction(
1174
- "mcp-data",
1175
- { action: "connected-accounts", userId }
1176
- );
1177
- if (error2 || !data2?.success) {
1178
- const message = "Failed to load connected accounts: " + (error2 ?? "Unknown error");
1179
- if (asJson) emitSnResult({ ok: false, command: "e2e", error: message }, true);
1180
- else console.error(message);
1181
- process.exit(1);
1182
- }
1183
- activeAccounts = data2.accounts ?? [];
1184
- }
1185
- const expired = activeAccounts.filter(
1186
- (a) => a.expires_at && new Date(a.expires_at).getTime() <= Date.now()
1436
+ }
1437
+ process.exit(data.success ? 0 : 1);
1438
+ }
1439
+ async function handleQualityCheck(args, asJson) {
1440
+ const caption = args.caption;
1441
+ if (typeof caption !== "string") {
1442
+ throw new Error("Missing required flag: --caption");
1443
+ }
1444
+ const title = typeof args.title === "string" ? args.title : void 0;
1445
+ const platformsRaw = typeof args.platforms === "string" ? args.platforms : "youtube";
1446
+ const platformsNormalized = normalizePlatforms(platformsRaw);
1447
+ const thresholdRaw = typeof args.threshold === "string" ? Number(args.threshold) : void 0;
1448
+ const quality = evaluateQuality({
1449
+ caption,
1450
+ title,
1451
+ platforms: platformsNormalized,
1452
+ threshold: Number.isFinite(thresholdRaw) ? thresholdRaw : void 0
1453
+ });
1454
+ const payload = {
1455
+ ok: quality.passed,
1456
+ command: "quality-check",
1457
+ platforms: platformsNormalized,
1458
+ threshold: quality.threshold,
1459
+ score: quality.total,
1460
+ maxScore: quality.maxTotal,
1461
+ blockers: quality.blockers,
1462
+ categories: quality.categories
1463
+ };
1464
+ if (asJson) {
1465
+ emitSnResult(payload, true);
1466
+ } else {
1467
+ console.error(
1468
+ "QUALITY SCORE: " + quality.total + "/" + quality.maxTotal + " (threshold " + quality.threshold + ")"
1187
1469
  );
1470
+ for (const c of quality.categories) {
1471
+ console.error("- " + c.name + ": " + c.score + "/" + c.maxScore);
1472
+ }
1473
+ if (quality.blockers.length) {
1474
+ console.error("Blockers: " + quality.blockers.join("; "));
1475
+ }
1476
+ console.error("Decision: " + (quality.passed ? "Publish-ready" : "Needs revision"));
1477
+ }
1478
+ process.exit(quality.passed ? 0 : 1);
1479
+ }
1480
+ async function handleE2e(args, asJson) {
1481
+ const mediaUrl = args["media-url"];
1482
+ const caption = args.caption;
1483
+ const platformsRaw = args.platforms;
1484
+ if (typeof mediaUrl !== "string" || typeof caption !== "string" || typeof platformsRaw !== "string") {
1485
+ throw new Error("Missing required flags for e2e: --media-url, --caption, --platforms");
1486
+ }
1487
+ const title = typeof args.title === "string" ? args.title : void 0;
1488
+ const scheduledAt = typeof args["schedule-at"] === "string" ? args["schedule-at"] : void 0;
1489
+ const checkUrls = isEnabledFlag(args["check-urls"]);
1490
+ const dryRun = isEnabledFlag(args["dry-run"]);
1491
+ const force = isEnabledFlag(args.force);
1492
+ const platformsNormalized = normalizePlatforms(platformsRaw);
1493
+ const thresholdRaw = typeof args.threshold === "string" ? Number(args.threshold) : void 0;
1494
+ const userId = await ensureAuth();
1495
+ const supabase = tryGetSupabaseClient();
1496
+ const privacyUrl = process.env.SOCIALNEURON_PRIVACY_POLICY_URL ?? null;
1497
+ const termsUrl = process.env.SOCIALNEURON_TERMS_URL ?? null;
1498
+ const preflightChecks = [];
1499
+ preflightChecks.push({
1500
+ name: "privacy_policy_url_present",
1501
+ ok: Boolean(privacyUrl),
1502
+ detail: privacyUrl ? privacyUrl : "Missing SOCIALNEURON_PRIVACY_POLICY_URL"
1503
+ });
1504
+ preflightChecks.push({
1505
+ name: "terms_url_present",
1506
+ ok: Boolean(termsUrl),
1507
+ detail: termsUrl ? termsUrl : "Missing SOCIALNEURON_TERMS_URL"
1508
+ });
1509
+ if (checkUrls && privacyUrl && isValidHttpsUrl(privacyUrl)) {
1510
+ const r = await checkUrlReachability(privacyUrl);
1188
1511
  preflightChecks.push({
1189
- name: "oauth_connections_present",
1190
- ok: activeAccounts.length > 0,
1191
- detail: activeAccounts.length ? activeAccounts.length + " active account(s)" : "No active connected_accounts found"
1512
+ name: "privacy_policy_url_reachable",
1513
+ ok: r.ok,
1514
+ detail: r.ok ? "Reachable (HTTP " + (r.status ?? 200) + ")" : "Unreachable (" + (r.error ?? "HTTP " + (r.status ?? "unknown")) + ")"
1192
1515
  });
1516
+ }
1517
+ if (checkUrls && termsUrl && isValidHttpsUrl(termsUrl)) {
1518
+ const r = await checkUrlReachability(termsUrl);
1193
1519
  preflightChecks.push({
1194
- name: "oauth_tokens_not_expired",
1195
- ok: expired.length === 0,
1196
- detail: expired.length ? "Expired: " + expired.map((a) => a.platform).join(", ") : "No expired tokens detected"
1197
- });
1198
- const preflightOk = preflightChecks.every((c) => c.ok);
1199
- const quality = evaluateQuality({
1200
- caption,
1201
- title,
1202
- platforms: platformsNormalized,
1203
- threshold: Number.isFinite(thresholdRaw) ? thresholdRaw : void 0
1520
+ name: "terms_url_reachable",
1521
+ ok: r.ok,
1522
+ detail: r.ok ? "Reachable (HTTP " + (r.status ?? 200) + ")" : "Unreachable (" + (r.error ?? "HTTP " + (r.status ?? "unknown")) + ")"
1204
1523
  });
1205
- const confirmed = isEnabledFlag(args.confirm);
1206
- const canPublish = confirmed && preflightOk && quality.passed;
1207
- const blockedReasons = [];
1208
- if (!preflightOk) blockedReasons.push("preflight_failed");
1209
- if (!quality.passed) blockedReasons.push("quality_failed");
1210
- if (!confirmed) blockedReasons.push("missing_confirm");
1211
- const report = {
1212
- ok: canPublish,
1213
- command: "e2e",
1214
- dryRun,
1215
- mediaUrl,
1216
- platforms: platformsNormalized,
1217
- scheduledAt: scheduledAt ?? null,
1218
- preflight: {
1219
- ok: preflightOk,
1220
- checks: preflightChecks,
1221
- connectedPlatforms: activeAccounts.map((a) => ({
1222
- platform: a.platform,
1223
- username: a.username,
1224
- expiresAt: a.expires_at
1225
- }))
1226
- },
1227
- quality: {
1228
- passed: quality.passed,
1229
- threshold: quality.threshold,
1230
- score: quality.total,
1231
- maxScore: quality.maxTotal,
1232
- blockers: quality.blockers,
1233
- categories: quality.categories
1234
- },
1235
- blockedReasons
1236
- };
1237
- if (dryRun || !canPublish && !force) {
1238
- if (asJson) {
1239
- emitSnResult(report, true);
1240
- } else {
1241
- console.error("E2E: " + (canPublish ? "READY" : "BLOCKED"));
1242
- console.error("Preflight: " + (preflightOk ? "PASS" : "FAIL"));
1243
- console.error(
1244
- "Quality: " + (quality.passed ? "PASS" : "FAIL") + " (" + quality.total + "/" + quality.maxTotal + ", threshold " + quality.threshold + ")"
1245
- );
1246
- if (blockedReasons.length) console.error("Blocked: " + blockedReasons.join(", "));
1247
- console.error(
1248
- "Use --dry-run for JSON output; use --force to publish despite blockers (not recommended)."
1249
- );
1250
- }
1251
- process.exit(canPublish ? 0 : 1);
1524
+ }
1525
+ let activeAccounts = [];
1526
+ if (supabase) {
1527
+ const { data: accounts, error: accountsError } = await supabase.from("connected_accounts").select("platform, status, username, expires_at").eq("user_id", userId).eq("status", "active");
1528
+ if (accountsError) {
1529
+ const formatted = classifySupabaseCliError("load connected accounts", accountsError);
1530
+ throw new Error(formatted.message);
1252
1531
  }
1253
- const idempotencyKey = buildPublishIdempotencyKey({
1254
- mediaUrl,
1255
- caption,
1256
- platforms: platformsNormalized,
1257
- title,
1258
- scheduledAt
1259
- });
1260
- const { data, error } = await callEdgeFunction("schedule-post", {
1261
- mediaUrl,
1262
- caption,
1263
- platforms: platformsNormalized,
1264
- title,
1265
- scheduledAt,
1266
- idempotencyKey,
1267
- userId
1268
- });
1269
- if (error || !data) {
1270
- const message = "Publish failed: " + (error ?? "Unknown error");
1271
- if (asJson)
1272
- emitSnResult({ ok: false, command: "e2e", error: message, idempotencyKey, report }, true);
1273
- else console.error(message);
1274
- process.exit(1);
1532
+ activeAccounts = accounts ?? [];
1533
+ } else {
1534
+ const { data: data2, error: error2 } = await callEdgeFunction(
1535
+ "mcp-data",
1536
+ { action: "connected-accounts", userId }
1537
+ );
1538
+ if (error2 || !data2?.success) {
1539
+ throw new Error("Failed to load connected accounts: " + (error2 ?? "Unknown error"));
1275
1540
  }
1541
+ activeAccounts = data2.accounts ?? [];
1542
+ }
1543
+ const expired = activeAccounts.filter(
1544
+ (a) => a.expires_at && new Date(a.expires_at).getTime() <= Date.now()
1545
+ );
1546
+ preflightChecks.push({
1547
+ name: "oauth_connections_present",
1548
+ ok: activeAccounts.length > 0,
1549
+ detail: activeAccounts.length ? activeAccounts.length + " active account(s)" : "No active connected_accounts found"
1550
+ });
1551
+ preflightChecks.push({
1552
+ name: "oauth_tokens_not_expired",
1553
+ ok: expired.length === 0,
1554
+ detail: expired.length ? "Expired: " + expired.map((a) => a.platform).join(", ") : "No expired tokens detected"
1555
+ });
1556
+ const preflightOk = preflightChecks.every((c) => c.ok);
1557
+ const quality = evaluateQuality({
1558
+ caption,
1559
+ title,
1560
+ platforms: platformsNormalized,
1561
+ threshold: Number.isFinite(thresholdRaw) ? thresholdRaw : void 0
1562
+ });
1563
+ const confirmed = isEnabledFlag(args.confirm);
1564
+ const canPublish = confirmed && preflightOk && quality.passed;
1565
+ const blockedReasons = [];
1566
+ if (!preflightOk) blockedReasons.push("preflight_failed");
1567
+ if (!quality.passed) blockedReasons.push("quality_failed");
1568
+ if (!confirmed) blockedReasons.push("missing_confirm");
1569
+ const report = {
1570
+ ok: canPublish,
1571
+ command: "e2e",
1572
+ dryRun,
1573
+ mediaUrl,
1574
+ platforms: platformsNormalized,
1575
+ scheduledAt: scheduledAt ?? null,
1576
+ preflight: {
1577
+ ok: preflightOk,
1578
+ checks: preflightChecks,
1579
+ connectedPlatforms: activeAccounts.map((a) => ({
1580
+ platform: a.platform,
1581
+ username: a.username,
1582
+ expiresAt: a.expires_at
1583
+ }))
1584
+ },
1585
+ quality: {
1586
+ passed: quality.passed,
1587
+ threshold: quality.threshold,
1588
+ score: quality.total,
1589
+ maxScore: quality.maxTotal,
1590
+ blockers: quality.blockers,
1591
+ categories: quality.categories
1592
+ },
1593
+ blockedReasons
1594
+ };
1595
+ if (dryRun || !canPublish && !force) {
1276
1596
  if (asJson) {
1277
- emitSnResult(
1278
- {
1279
- ok: data.success,
1280
- command: "e2e",
1281
- idempotencyKey,
1282
- scheduledAt: data.scheduledAt,
1283
- results: data.results,
1284
- report
1285
- },
1286
- true
1287
- );
1597
+ emitSnResult(report, true);
1288
1598
  } else {
1289
- console.error("E2E publish executed. Idempotency key: " + idempotencyKey);
1290
- console.error("Scheduled for: " + data.scheduledAt);
1291
- for (const [platform3, result] of Object.entries(data.results)) {
1292
- if (result.success)
1293
- console.error(
1294
- platform3 + ": OK (jobId=" + result.jobId + ", postId=" + result.postId + ")"
1295
- );
1296
- else console.error(platform3 + ": FAILED (" + result.error + ")");
1599
+ console.error("E2E: " + (canPublish ? "READY" : "BLOCKED"));
1600
+ console.error("Preflight: " + (preflightOk ? "PASS" : "FAIL"));
1601
+ console.error(
1602
+ "Quality: " + (quality.passed ? "PASS" : "FAIL") + " (" + quality.total + "/" + quality.maxTotal + ", threshold " + quality.threshold + ")"
1603
+ );
1604
+ if (blockedReasons.length) console.error("Blocked: " + blockedReasons.join(", "));
1605
+ console.error(
1606
+ "Use --dry-run for JSON output; use --force to publish despite blockers (not recommended)."
1607
+ );
1608
+ }
1609
+ process.exit(canPublish ? 0 : 1);
1610
+ }
1611
+ const idempotencyKey = buildPublishIdempotencyKey({
1612
+ mediaUrl,
1613
+ caption,
1614
+ platforms: platformsNormalized,
1615
+ title,
1616
+ scheduledAt
1617
+ });
1618
+ const { data, error } = await callEdgeFunction("schedule-post", {
1619
+ mediaUrl,
1620
+ caption,
1621
+ platforms: platformsNormalized,
1622
+ title,
1623
+ scheduledAt,
1624
+ idempotencyKey,
1625
+ userId
1626
+ });
1627
+ if (error || !data) {
1628
+ throw new Error("Publish failed: " + (error ?? "Unknown error"));
1629
+ }
1630
+ if (asJson) {
1631
+ emitSnResult(
1632
+ {
1633
+ ok: data.success,
1634
+ command: "e2e",
1635
+ idempotencyKey,
1636
+ scheduledAt: data.scheduledAt,
1637
+ results: data.results,
1638
+ report
1639
+ },
1640
+ true
1641
+ );
1642
+ } else {
1643
+ console.error("E2E publish executed. Idempotency key: " + idempotencyKey);
1644
+ console.error("Scheduled for: " + data.scheduledAt);
1645
+ for (const [platform3, result] of Object.entries(data.results)) {
1646
+ if (result.success)
1647
+ console.error(platform3 + ": OK (jobId=" + result.jobId + ", postId=" + result.postId + ")");
1648
+ else console.error(platform3 + ": FAILED (" + result.error + ")");
1649
+ }
1650
+ }
1651
+ process.exit(data.success ? 0 : 1);
1652
+ }
1653
+ var init_content = __esm({
1654
+ "src/cli/sn/content.ts"() {
1655
+ "use strict";
1656
+ init_edge_function();
1657
+ init_quality();
1658
+ init_supabase();
1659
+ init_parse();
1660
+ }
1661
+ });
1662
+
1663
+ // src/cli/sn/account.ts
1664
+ var account_exports = {};
1665
+ __export(account_exports, {
1666
+ handleOauthHealth: () => handleOauthHealth,
1667
+ handleOauthRefresh: () => handleOauthRefresh,
1668
+ handlePreflight: () => handlePreflight
1669
+ });
1670
+ async function ensureAuth2() {
1671
+ await initializeAuth();
1672
+ return getDefaultUserId();
1673
+ }
1674
+ async function handleOauthHealth(args, asJson) {
1675
+ const userId = await ensureAuth2();
1676
+ const supabase = tryGetSupabaseClient();
1677
+ const warnDaysRaw = typeof args["warn-days"] === "string" ? Number(args["warn-days"]) : 7;
1678
+ const warnDays = Number.isFinite(warnDaysRaw) && warnDaysRaw > 0 ? Math.min(warnDaysRaw, 90) : 7;
1679
+ const includeAll = isEnabledFlag(args.all);
1680
+ const platformsFilter = typeof args.platforms === "string" ? normalizePlatforms(args.platforms) : null;
1681
+ let accounts = [];
1682
+ if (supabase) {
1683
+ let query = supabase.from("connected_accounts").select("platform, status, username, expires_at, refresh_token, updated_at, created_at").eq("user_id", userId).order("platform");
1684
+ if (!includeAll) {
1685
+ query = query.eq("status", "active");
1686
+ }
1687
+ const { data, error } = await query;
1688
+ if (error) {
1689
+ const formatted = classifySupabaseCliError("load oauth health", error);
1690
+ throw new Error(formatted.message);
1691
+ }
1692
+ accounts = data ?? [];
1693
+ } else {
1694
+ const { data, error } = await callEdgeFunction(
1695
+ "mcp-data",
1696
+ {
1697
+ action: "connected-accounts",
1698
+ userId,
1699
+ includeAll
1297
1700
  }
1701
+ );
1702
+ if (error || !data?.success) {
1703
+ throw new Error("Failed to load oauth health: " + (error ?? "Unknown error"));
1704
+ }
1705
+ accounts = data.accounts ?? [];
1706
+ }
1707
+ const now = Date.now();
1708
+ const rows = (accounts ?? []).map((a) => {
1709
+ const expiresAtMs = a.expires_at ? new Date(a.expires_at).getTime() : null;
1710
+ const daysLeft = expiresAtMs ? Math.ceil((expiresAtMs - now) / (1e3 * 60 * 60 * 24)) : null;
1711
+ const refreshTokenPresent = Boolean(a.refresh_token ?? a.has_refresh_token);
1712
+ let state = "ok";
1713
+ if (a.status && String(a.status).toLowerCase() !== "active") state = "inactive";
1714
+ if (expiresAtMs && expiresAtMs <= now) state = "expired";
1715
+ else if (daysLeft !== null && daysLeft <= warnDays) state = "expiring_soon";
1716
+ if (!refreshTokenPresent && state === "ok") state = "missing_refresh_token";
1717
+ return {
1718
+ platform: a.platform,
1719
+ username: a.username ?? null,
1720
+ status: a.status,
1721
+ expiresAt: a.expires_at ?? null,
1722
+ daysLeft,
1723
+ refreshTokenPresent,
1724
+ state
1725
+ };
1726
+ });
1727
+ const filtered = platformsFilter ? rows.filter(
1728
+ (r) => platformsFilter.some(
1729
+ (p) => p.toLowerCase() === String(r.platform ?? "").toLowerCase()
1730
+ )
1731
+ ) : rows;
1732
+ const ok = filtered.every((r) => {
1733
+ if (String(r.status).toLowerCase() !== "active") return false;
1734
+ if (r.state === "expired") return false;
1735
+ if (!r.refreshTokenPresent) return false;
1736
+ return true;
1737
+ });
1738
+ const payload = {
1739
+ ok,
1740
+ command: "oauth-health",
1741
+ warnDays,
1742
+ accountCount: filtered.length,
1743
+ accounts: filtered
1744
+ };
1745
+ if (asJson) {
1746
+ emitSnResult(payload, true);
1747
+ } else {
1748
+ console.error("OAuth Health: " + (ok ? "PASS" : "WARN/FAIL"));
1749
+ for (const row of filtered) {
1750
+ const exp = row.daysLeft === null ? "n/a" : row.daysLeft + "d";
1751
+ console.error(
1752
+ String(row.platform).toLowerCase() + " | " + (row.username ?? "(unnamed)") + " | status=" + row.status + " | expires=" + exp + " | refresh=" + (row.refreshTokenPresent ? "yes" : "no") + " | state=" + row.state
1753
+ );
1754
+ }
1755
+ if (!ok) {
1756
+ console.error("");
1757
+ console.error(
1758
+ "Recommended: run oauth-refresh for expiring accounts, or reconnect expired/missing-refresh accounts."
1759
+ );
1298
1760
  }
1299
- process.exit(data.success ? 0 : 1);
1300
1761
  }
1301
- if (subcommand === "oauth-health") {
1302
- const supabase = tryGetSupabaseClient();
1303
- const warnDaysRaw = typeof args["warn-days"] === "string" ? Number(args["warn-days"]) : 7;
1304
- const warnDays = Number.isFinite(warnDaysRaw) && warnDaysRaw > 0 ? Math.min(warnDaysRaw, 90) : 7;
1305
- const includeAll = isEnabledFlag(args.all);
1306
- const platformsFilter = typeof args.platforms === "string" ? normalizePlatforms(args.platforms) : null;
1307
- let accounts = [];
1762
+ process.exit(ok ? 0 : 1);
1763
+ }
1764
+ async function handleOauthRefresh(args, asJson) {
1765
+ const userId = await ensureAuth2();
1766
+ const supabase = tryGetSupabaseClient();
1767
+ const includeAll = isEnabledFlag(args.all);
1768
+ let platforms = [];
1769
+ if (typeof args.platforms === "string") {
1770
+ platforms = normalizePlatforms(args.platforms);
1771
+ } else if (includeAll) {
1308
1772
  if (supabase) {
1309
- let query = supabase.from("connected_accounts").select("platform, status, username, expires_at, refresh_token, updated_at, created_at").eq("user_id", userId).order("platform");
1310
- if (!includeAll) {
1311
- query = query.eq("status", "active");
1312
- }
1313
- const { data, error } = await query;
1773
+ const { data: accounts, error } = await supabase.from("connected_accounts").select("platform").eq("user_id", userId).eq("status", "active");
1314
1774
  if (error) {
1315
- const formatted = classifySupabaseCliError("load oauth health", error);
1316
- if (asJson) {
1317
- emitSnResult(
1318
- { ok: false, command: "oauth-health", error: formatted.message, hint: formatted.hint },
1319
- true
1320
- );
1321
- } else {
1322
- console.error(formatted.message);
1323
- if (formatted.hint) console.error("Hint: " + formatted.hint);
1324
- }
1325
- process.exit(1);
1775
+ const formatted = classifySupabaseCliError("load connected accounts", error);
1776
+ throw new Error(formatted.message);
1326
1777
  }
1327
- accounts = data ?? [];
1778
+ platforms = (accounts ?? []).map((a) => String(a.platform));
1328
1779
  } else {
1329
1780
  const { data, error } = await callEdgeFunction(
1330
1781
  "mcp-data",
1331
1782
  {
1332
1783
  action: "connected-accounts",
1333
- userId,
1334
- includeAll
1784
+ userId
1335
1785
  }
1336
1786
  );
1337
1787
  if (error || !data?.success) {
1338
- const message = "Failed to load oauth health: " + (error ?? "Unknown error");
1339
- if (asJson) emitSnResult({ ok: false, command: "oauth-health", error: message }, true);
1340
- else console.error(message);
1341
- process.exit(1);
1788
+ throw new Error("Failed to load connected accounts: " + (error ?? "Unknown error"));
1342
1789
  }
1343
- accounts = data.accounts ?? [];
1790
+ platforms = (data.accounts ?? []).map((a) => String(a.platform));
1344
1791
  }
1345
- const now = Date.now();
1346
- const rows = (accounts ?? []).map((a) => {
1347
- const expiresAtMs = a.expires_at ? new Date(a.expires_at).getTime() : null;
1348
- const daysLeft = expiresAtMs ? Math.ceil((expiresAtMs - now) / (1e3 * 60 * 60 * 24)) : null;
1349
- const refreshTokenPresent = Boolean(a.refresh_token ?? a.has_refresh_token);
1350
- let state = "ok";
1351
- if (a.status && String(a.status).toLowerCase() !== "active") state = "inactive";
1352
- if (expiresAtMs && expiresAtMs <= now) state = "expired";
1353
- else if (daysLeft !== null && daysLeft <= warnDays) state = "expiring_soon";
1354
- if (!refreshTokenPresent && state === "ok") state = "missing_refresh_token";
1355
- return {
1356
- platform: a.platform,
1357
- username: a.username ?? null,
1358
- status: a.status,
1359
- expiresAt: a.expires_at ?? null,
1360
- daysLeft,
1361
- refreshTokenPresent,
1362
- state
1363
- };
1364
- });
1365
- const filtered = platformsFilter ? rows.filter(
1366
- (r) => platformsFilter.some((p) => p.toLowerCase() === String(r.platform ?? "").toLowerCase())
1367
- ) : rows;
1368
- const ok = filtered.every((r) => {
1369
- if (String(r.status).toLowerCase() !== "active") return false;
1370
- if (r.state === "expired") return false;
1371
- if (!r.refreshTokenPresent) return false;
1372
- return true;
1373
- });
1374
- const payload = {
1375
- ok,
1376
- command: "oauth-health",
1377
- warnDays,
1378
- accountCount: filtered.length,
1379
- accounts: filtered
1380
- };
1381
- if (asJson) {
1382
- emitSnResult(payload, true);
1792
+ }
1793
+ if (!platforms.length) {
1794
+ throw new Error('Missing required flags: pass --platforms "youtube,tiktok" or --all');
1795
+ }
1796
+ const results = {};
1797
+ for (const platform3 of platforms) {
1798
+ const { data, error } = await callEdgeFunction(
1799
+ "social-auth",
1800
+ {},
1801
+ { query: { action: "refresh", platform: platform3 }, timeoutMs: 3e4 }
1802
+ );
1803
+ if (error || !data?.success) {
1804
+ results[platform3] = { ok: false, expiresAt: null, error: error ?? "Refresh failed" };
1383
1805
  } else {
1384
- console.error("OAuth Health: " + (ok ? "PASS" : "WARN/FAIL"));
1385
- for (const row of filtered) {
1386
- const exp = row.daysLeft === null ? "n/a" : row.daysLeft + "d";
1387
- console.error(
1388
- String(row.platform).toLowerCase() + " | " + (row.username ?? "(unnamed)") + " | status=" + row.status + " | expires=" + exp + " | refresh=" + (row.refreshTokenPresent ? "yes" : "no") + " | state=" + row.state
1389
- );
1390
- }
1391
- if (!ok) {
1392
- console.error("");
1393
- console.error(
1394
- "Recommended: run oauth-refresh for expiring accounts, or reconnect expired/missing-refresh accounts."
1395
- );
1396
- }
1806
+ results[platform3] = { ok: true, expiresAt: data.expires_at ?? null };
1397
1807
  }
1398
- process.exit(ok ? 0 : 1);
1399
1808
  }
1400
- if (subcommand === "oauth-refresh") {
1401
- const supabase = tryGetSupabaseClient();
1402
- const includeAll = isEnabledFlag(args.all);
1403
- let platforms = [];
1404
- if (typeof args.platforms === "string") {
1405
- platforms = normalizePlatforms(args.platforms);
1406
- } else if (includeAll) {
1407
- if (supabase) {
1408
- const { data: accounts, error } = await supabase.from("connected_accounts").select("platform").eq("user_id", userId).eq("status", "active");
1409
- if (error) {
1410
- const formatted = classifySupabaseCliError("load connected accounts", error);
1411
- if (asJson) {
1412
- emitSnResult(
1413
- {
1414
- ok: false,
1415
- command: "oauth-refresh",
1416
- error: formatted.message,
1417
- hint: formatted.hint
1418
- },
1419
- true
1420
- );
1421
- } else {
1422
- console.error(formatted.message);
1423
- if (formatted.hint) console.error("Hint: " + formatted.hint);
1424
- }
1425
- process.exit(1);
1426
- }
1427
- platforms = (accounts ?? []).map((a) => String(a.platform));
1428
- } else {
1429
- const { data, error } = await callEdgeFunction(
1430
- "mcp-data",
1431
- {
1432
- action: "connected-accounts",
1433
- userId
1434
- }
1435
- );
1436
- if (error || !data?.success) {
1437
- const message = "Failed to load connected accounts: " + (error ?? "Unknown error");
1438
- if (asJson) emitSnResult({ ok: false, command: "oauth-refresh", error: message }, true);
1439
- else console.error(message);
1440
- process.exit(1);
1441
- }
1442
- platforms = (data.accounts ?? []).map((a) => String(a.platform));
1443
- }
1444
- }
1445
- if (!platforms.length) {
1446
- const message = 'Missing required flags: pass --platforms "youtube,tiktok" or --all';
1447
- if (asJson) {
1448
- emitSnResult({ ok: false, command: "oauth-refresh", error: message }, true);
1449
- } else {
1450
- console.error(message);
1451
- printSnUsage();
1452
- }
1453
- process.exit(1);
1454
- }
1455
- const results = {};
1456
- for (const platform3 of platforms) {
1457
- const { data, error } = await callEdgeFunction(
1458
- "social-auth",
1459
- {},
1460
- { query: { action: "refresh", platform: platform3 }, timeoutMs: 3e4 }
1461
- );
1462
- if (error || !data?.success) {
1463
- results[platform3] = { ok: false, expiresAt: null, error: error ?? "Refresh failed" };
1809
+ const ok = Object.values(results).every((r) => r.ok);
1810
+ if (asJson) {
1811
+ emitSnResult({ ok, command: "oauth-refresh", results }, true);
1812
+ } else {
1813
+ console.error("OAuth refresh: " + (ok ? "OK" : "ERRORS"));
1814
+ for (const platform3 of Object.keys(results)) {
1815
+ const result = results[platform3];
1816
+ if (result.ok) {
1817
+ console.error(platform3 + " refreshed (expires_at=" + (result.expiresAt ?? "n/a") + ")");
1464
1818
  } else {
1465
- results[platform3] = { ok: true, expiresAt: data.expires_at ?? null };
1819
+ console.error(platform3 + " FAILED (" + result.error + ")");
1466
1820
  }
1467
1821
  }
1468
- const ok = Object.values(results).every((r) => r.ok);
1469
- if (asJson) {
1470
- emitSnResult({ ok, command: "oauth-refresh", results }, true);
1471
- } else {
1472
- console.error("OAuth refresh: " + (ok ? "OK" : "ERRORS"));
1473
- for (const platform3 of Object.keys(results)) {
1474
- const result = results[platform3];
1475
- if (result.ok) {
1476
- console.error(platform3 + " refreshed (expires_at=" + (result.expiresAt ?? "n/a") + ")");
1477
- } else {
1478
- console.error(platform3 + " FAILED (" + result.error + ")");
1479
- }
1480
- }
1481
- }
1482
- process.exit(ok ? 0 : 1);
1483
- }
1484
- if (subcommand === "preflight") {
1485
- const supabase = tryGetSupabaseClient();
1486
- const privacyFromArg = typeof args["privacy-url"] === "string" ? args["privacy-url"] : null;
1487
- const termsFromArg = typeof args["terms-url"] === "string" ? args["terms-url"] : null;
1488
- const privacyUrl = privacyFromArg ?? process.env.SOCIALNEURON_PRIVACY_POLICY_URL ?? null;
1489
- const termsUrl = termsFromArg ?? process.env.SOCIALNEURON_TERMS_URL ?? null;
1490
- const checkUrls = isEnabledFlag(args["check-urls"]);
1491
- const creditCapRaw = process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN ?? "";
1492
- const assetCapRaw = process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN ?? "";
1493
- const creditCap = Number(creditCapRaw);
1494
- const assetCap = Number(assetCapRaw);
1495
- const checks = [];
1822
+ }
1823
+ process.exit(ok ? 0 : 1);
1824
+ }
1825
+ async function handlePreflight(args, asJson) {
1826
+ const userId = await ensureAuth2();
1827
+ const supabase = tryGetSupabaseClient();
1828
+ const privacyFromArg = typeof args["privacy-url"] === "string" ? args["privacy-url"] : null;
1829
+ const termsFromArg = typeof args["terms-url"] === "string" ? args["terms-url"] : null;
1830
+ const privacyUrl = privacyFromArg ?? process.env.SOCIALNEURON_PRIVACY_POLICY_URL ?? null;
1831
+ const termsUrl = termsFromArg ?? process.env.SOCIALNEURON_TERMS_URL ?? null;
1832
+ const checkUrls = isEnabledFlag(args["check-urls"]);
1833
+ const creditCapRaw = process.env.SOCIALNEURON_MAX_CREDITS_PER_RUN ?? "";
1834
+ const assetCapRaw = process.env.SOCIALNEURON_MAX_ASSETS_PER_RUN ?? "";
1835
+ const creditCap = Number(creditCapRaw);
1836
+ const assetCap = Number(assetCapRaw);
1837
+ const checks = [];
1838
+ checks.push({
1839
+ name: "privacy_policy_url_present",
1840
+ ok: Boolean(privacyUrl),
1841
+ detail: privacyUrl ? privacyUrl : "Missing SOCIALNEURON_PRIVACY_POLICY_URL"
1842
+ });
1843
+ checks.push({
1844
+ name: "terms_url_present",
1845
+ ok: Boolean(termsUrl),
1846
+ detail: termsUrl ? termsUrl : "Missing SOCIALNEURON_TERMS_URL"
1847
+ });
1848
+ if (privacyUrl) {
1496
1849
  checks.push({
1497
- name: "privacy_policy_url_present",
1498
- ok: Boolean(privacyUrl),
1499
- detail: privacyUrl ? privacyUrl : "Missing SOCIALNEURON_PRIVACY_POLICY_URL"
1850
+ name: "privacy_policy_url_https",
1851
+ ok: isValidHttpsUrl(privacyUrl),
1852
+ detail: isValidHttpsUrl(privacyUrl) ? "Uses HTTPS" : "Privacy Policy URL must be a valid https:// URL"
1500
1853
  });
1854
+ }
1855
+ if (termsUrl) {
1501
1856
  checks.push({
1502
- name: "terms_url_present",
1503
- ok: Boolean(termsUrl),
1504
- detail: termsUrl ? termsUrl : "Missing SOCIALNEURON_TERMS_URL"
1857
+ name: "terms_url_https",
1858
+ ok: isValidHttpsUrl(termsUrl),
1859
+ detail: isValidHttpsUrl(termsUrl) ? "Uses HTTPS" : "Terms URL must be a valid https:// URL"
1505
1860
  });
1506
- if (privacyUrl) {
1507
- checks.push({
1508
- name: "privacy_policy_url_https",
1509
- ok: isValidHttpsUrl(privacyUrl),
1510
- detail: isValidHttpsUrl(privacyUrl) ? "Uses HTTPS" : "Privacy Policy URL must be a valid https:// URL"
1511
- });
1512
- }
1513
- if (termsUrl) {
1514
- checks.push({
1515
- name: "terms_url_https",
1516
- ok: isValidHttpsUrl(termsUrl),
1517
- detail: isValidHttpsUrl(termsUrl) ? "Uses HTTPS" : "Terms URL must be a valid https:// URL"
1518
- });
1519
- }
1520
- if (checkUrls && privacyUrl && isValidHttpsUrl(privacyUrl)) {
1521
- const result = await checkUrlReachability(privacyUrl);
1522
- checks.push({
1523
- name: "privacy_policy_url_reachable",
1524
- ok: result.ok,
1525
- detail: result.ok ? `Reachable (HTTP ${result.status ?? 200})` : `Unreachable (${result.error ?? `HTTP ${result.status ?? "unknown"}`})`
1526
- });
1527
- }
1528
- if (checkUrls && termsUrl && isValidHttpsUrl(termsUrl)) {
1529
- const result = await checkUrlReachability(termsUrl);
1530
- checks.push({
1531
- name: "terms_url_reachable",
1532
- ok: result.ok,
1533
- detail: result.ok ? `Reachable (HTTP ${result.status ?? 200})` : `Unreachable (${result.error ?? `HTTP ${result.status ?? "unknown"}`})`
1534
- });
1535
- }
1861
+ }
1862
+ if (checkUrls && privacyUrl && isValidHttpsUrl(privacyUrl)) {
1863
+ const result = await checkUrlReachability(privacyUrl);
1536
1864
  checks.push({
1537
- name: "max_credits_per_run_configured",
1538
- ok: Number.isFinite(creditCap) && creditCap > 0,
1539
- detail: Number.isFinite(creditCap) && creditCap > 0 ? `${creditCap} credits cap` : "Set SOCIALNEURON_MAX_CREDITS_PER_RUN to a positive number"
1865
+ name: "privacy_policy_url_reachable",
1866
+ ok: result.ok,
1867
+ detail: result.ok ? `Reachable (HTTP ${result.status ?? 200})` : `Unreachable (${result.error ?? `HTTP ${result.status ?? "unknown"}`})`
1540
1868
  });
1869
+ }
1870
+ if (checkUrls && termsUrl && isValidHttpsUrl(termsUrl)) {
1871
+ const result = await checkUrlReachability(termsUrl);
1541
1872
  checks.push({
1542
- name: "max_assets_per_run_configured",
1543
- ok: Number.isFinite(assetCap) && assetCap > 0,
1544
- detail: Number.isFinite(assetCap) && assetCap > 0 ? `${assetCap} assets cap` : "Set SOCIALNEURON_MAX_ASSETS_PER_RUN to a positive number"
1873
+ name: "terms_url_reachable",
1874
+ ok: result.ok,
1875
+ detail: result.ok ? `Reachable (HTTP ${result.status ?? 200})` : `Unreachable (${result.error ?? `HTTP ${result.status ?? "unknown"}`})`
1545
1876
  });
1546
- let activeAccounts = [];
1547
- if (supabase) {
1548
- const { data: accounts, error: accountsError } = await supabase.from("connected_accounts").select("platform, status, username, expires_at").eq("user_id", userId).eq("status", "active").order("platform");
1549
- if (accountsError) {
1550
- const formatted = classifySupabaseCliError("load connected accounts", accountsError);
1551
- if (asJson) {
1552
- emitSnResult(
1553
- {
1554
- ok: false,
1555
- command: "preflight",
1556
- error: formatted.message,
1557
- hint: formatted.hint
1558
- },
1559
- true
1560
- );
1561
- } else {
1562
- console.error(formatted.message);
1563
- if (formatted.hint) console.error(`Hint: ${formatted.hint}`);
1564
- }
1565
- process.exit(1);
1566
- }
1567
- activeAccounts = accounts ?? [];
1568
- } else {
1569
- const { data, error } = await callEdgeFunction(
1570
- "mcp-data",
1571
- { action: "connected-accounts", userId }
1572
- );
1573
- if (error || !data?.success) {
1574
- const message = "Failed to load connected accounts: " + (error ?? "Unknown error");
1575
- if (asJson) {
1576
- emitSnResult({ ok: false, command: "preflight", error: message }, true);
1577
- } else {
1578
- console.error(message);
1579
- }
1580
- process.exit(1);
1581
- }
1582
- activeAccounts = data.accounts ?? [];
1877
+ }
1878
+ checks.push({
1879
+ name: "max_credits_per_run_configured",
1880
+ ok: Number.isFinite(creditCap) && creditCap > 0,
1881
+ detail: Number.isFinite(creditCap) && creditCap > 0 ? `${creditCap} credits cap` : "Set SOCIALNEURON_MAX_CREDITS_PER_RUN to a positive number"
1882
+ });
1883
+ checks.push({
1884
+ name: "max_assets_per_run_configured",
1885
+ ok: Number.isFinite(assetCap) && assetCap > 0,
1886
+ detail: Number.isFinite(assetCap) && assetCap > 0 ? `${assetCap} assets cap` : "Set SOCIALNEURON_MAX_ASSETS_PER_RUN to a positive number"
1887
+ });
1888
+ let activeAccounts = [];
1889
+ if (supabase) {
1890
+ const { data: accounts, error: accountsError } = await supabase.from("connected_accounts").select("platform, status, username, expires_at").eq("user_id", userId).eq("status", "active").order("platform");
1891
+ if (accountsError) {
1892
+ const formatted = classifySupabaseCliError("load connected accounts", accountsError);
1893
+ throw new Error(formatted.message);
1894
+ }
1895
+ activeAccounts = accounts ?? [];
1896
+ } else {
1897
+ const { data, error } = await callEdgeFunction(
1898
+ "mcp-data",
1899
+ { action: "connected-accounts", userId }
1900
+ );
1901
+ if (error || !data?.success) {
1902
+ throw new Error("Failed to load connected accounts: " + (error ?? "Unknown error"));
1583
1903
  }
1584
- const expiredAccounts = activeAccounts.filter((account) => {
1585
- if (!account.expires_at) return false;
1586
- const expiresAt = new Date(account.expires_at);
1587
- return Number.isFinite(expiresAt.getTime()) && expiresAt.getTime() <= Date.now();
1588
- });
1589
- checks.push({
1590
- name: "oauth_connections_present",
1591
- ok: activeAccounts.length > 0,
1592
- detail: activeAccounts.length > 0 ? `${activeAccounts.length} active account(s)` : "No active connected_accounts found"
1593
- });
1594
- checks.push({
1595
- name: "oauth_tokens_not_expired",
1596
- ok: expiredAccounts.length === 0,
1597
- detail: expiredAccounts.length === 0 ? "No expired tokens detected" : `Expired: ${expiredAccounts.map((a) => a.platform).join(", ")}`
1598
- });
1599
- const ok = checks.every((check) => check.ok);
1600
- const summary = {
1601
- ok,
1602
- command: "preflight",
1603
- checkCount: checks.length,
1604
- passed: checks.filter((check) => check.ok).length,
1605
- failed: checks.filter((check) => !check.ok).length,
1606
- connectedPlatforms: activeAccounts.map((a) => ({
1607
- platform: a.platform,
1608
- username: a.username,
1609
- expiresAt: a.expires_at
1610
- })),
1611
- checks
1612
- };
1613
- if (asJson) {
1614
- emitSnResult(summary, true);
1615
- } else {
1616
- console.error(`Preflight: ${ok ? "PASS" : "FAIL"}`);
1617
- console.error(`Checks: ${summary.passed}/${summary.checkCount} passed`);
1618
- for (const check of checks) {
1619
- console.error(`${check.ok ? "[ok]" : "[x]"} ${check.name}: ${check.detail}`);
1620
- }
1621
- if (!ok) {
1622
- console.error("");
1623
- console.error("Blocking checks failed. Recommended: run in Draft-only mode until fixed.");
1624
- }
1904
+ activeAccounts = data.accounts ?? [];
1905
+ }
1906
+ const expiredAccounts = activeAccounts.filter((account) => {
1907
+ if (!account.expires_at) return false;
1908
+ const expiresAt = new Date(account.expires_at);
1909
+ return Number.isFinite(expiresAt.getTime()) && expiresAt.getTime() <= Date.now();
1910
+ });
1911
+ checks.push({
1912
+ name: "oauth_connections_present",
1913
+ ok: activeAccounts.length > 0,
1914
+ detail: activeAccounts.length > 0 ? `${activeAccounts.length} active account(s)` : "No active connected_accounts found"
1915
+ });
1916
+ checks.push({
1917
+ name: "oauth_tokens_not_expired",
1918
+ ok: expiredAccounts.length === 0,
1919
+ detail: expiredAccounts.length === 0 ? "No expired tokens detected" : `Expired: ${expiredAccounts.map((a) => a.platform).join(", ")}`
1920
+ });
1921
+ const ok = checks.every((check) => check.ok);
1922
+ const summary = {
1923
+ ok,
1924
+ command: "preflight",
1925
+ checkCount: checks.length,
1926
+ passed: checks.filter((check) => check.ok).length,
1927
+ failed: checks.filter((check) => !check.ok).length,
1928
+ connectedPlatforms: activeAccounts.map((a) => ({
1929
+ platform: a.platform,
1930
+ username: a.username,
1931
+ expiresAt: a.expires_at
1932
+ })),
1933
+ checks
1934
+ };
1935
+ if (asJson) {
1936
+ emitSnResult(summary, true);
1937
+ } else {
1938
+ console.error(`Preflight: ${ok ? "PASS" : "FAIL"}`);
1939
+ console.error(`Checks: ${summary.passed}/${summary.checkCount} passed`);
1940
+ for (const check of checks) {
1941
+ console.error(`${check.ok ? "[ok]" : "[x]"} ${check.name}: ${check.detail}`);
1942
+ }
1943
+ if (!ok) {
1944
+ console.error("");
1945
+ console.error("Blocking checks failed. Recommended: run in Draft-only mode until fixed.");
1625
1946
  }
1626
- process.exit(ok ? 0 : 1);
1627
1947
  }
1628
- if (subcommand === "status") {
1629
- const jobId = args["job-id"];
1630
- if (typeof jobId !== "string") {
1631
- const message = "Missing required flag: --job-id";
1632
- if (asJson) {
1633
- emitSnResult({ ok: false, command: "status", error: message }, true);
1634
- } else {
1635
- console.error(message);
1636
- }
1637
- process.exit(1);
1948
+ process.exit(ok ? 0 : 1);
1949
+ }
1950
+ var init_account = __esm({
1951
+ "src/cli/sn/account.ts"() {
1952
+ "use strict";
1953
+ init_edge_function();
1954
+ init_supabase();
1955
+ init_parse();
1956
+ }
1957
+ });
1958
+
1959
+ // src/cli/sn/analytics.ts
1960
+ var analytics_exports = {};
1961
+ __export(analytics_exports, {
1962
+ handleLoop: () => handleLoop,
1963
+ handlePosts: () => handlePosts,
1964
+ handleRefreshAnalytics: () => handleRefreshAnalytics
1965
+ });
1966
+ async function ensureAuth3() {
1967
+ await initializeAuth();
1968
+ return getDefaultUserId();
1969
+ }
1970
+ async function handlePosts(args, asJson) {
1971
+ const userId = await ensureAuth3();
1972
+ const supabase = tryGetSupabaseClient();
1973
+ const daysRaw = args.days;
1974
+ const days = typeof daysRaw === "string" ? Number(daysRaw) : 7;
1975
+ const lookbackDays = Number.isFinite(days) && days > 0 ? Math.min(days, 90) : 7;
1976
+ const since = /* @__PURE__ */ new Date();
1977
+ since.setDate(since.getDate() - lookbackDays);
1978
+ let posts = [];
1979
+ if (supabase) {
1980
+ let query = supabase.from("posts").select(
1981
+ "id, platform, status, title, external_post_id, scheduled_at, published_at, created_at"
1982
+ ).eq("user_id", userId).gte("created_at", since.toISOString()).order("created_at", { ascending: false }).limit(50);
1983
+ if (typeof args.platform === "string") {
1984
+ query = query.eq("platform", args.platform);
1985
+ }
1986
+ if (typeof args.status === "string") {
1987
+ query = query.eq("status", args.status);
1988
+ }
1989
+ const { data, error } = await query;
1990
+ if (error) {
1991
+ const formatted = classifySupabaseCliError("fetch posts", error);
1992
+ throw new Error(formatted.message);
1993
+ }
1994
+ posts = data ?? [];
1995
+ } else {
1996
+ const { data, error } = await callEdgeFunction("mcp-data", {
1997
+ action: "recent-posts",
1998
+ userId,
1999
+ days: lookbackDays,
2000
+ limit: 50,
2001
+ platform: typeof args.platform === "string" ? args.platform : void 0,
2002
+ status: typeof args.status === "string" ? args.status : void 0
2003
+ });
2004
+ if (error || !data?.success) {
2005
+ throw new Error("Failed to fetch posts: " + (error ?? "Unknown error"));
1638
2006
  }
1639
- const supabase = tryGetSupabaseClient();
1640
- let job = null;
1641
- if (supabase) {
1642
- const { data: byId, error: byIdError } = await supabase.from("async_jobs").select(
1643
- "id, external_id, status, job_type, model, result_url, error_message, created_at, completed_at"
1644
- ).eq("user_id", userId).eq("id", jobId).maybeSingle();
1645
- if (byIdError) {
1646
- const formatted = classifySupabaseCliError("fetch job status", byIdError);
1647
- if (asJson) {
1648
- emitSnResult(
1649
- { ok: false, command: "status", error: formatted.message, hint: formatted.hint, jobId },
1650
- true
1651
- );
1652
- } else {
1653
- console.error(formatted.message);
1654
- if (formatted.hint) console.error(`Hint: ${formatted.hint}`);
1655
- }
1656
- process.exit(1);
1657
- }
1658
- if (byId) {
1659
- job = byId;
1660
- } else {
1661
- const { data: byExternal, error: byExternalError } = await supabase.from("async_jobs").select(
1662
- "id, external_id, status, job_type, model, result_url, error_message, created_at, completed_at"
1663
- ).eq("user_id", userId).eq("external_id", jobId).maybeSingle();
1664
- if (byExternalError) {
1665
- const formatted = classifySupabaseCliError("fetch job status", byExternalError);
1666
- if (asJson) {
1667
- emitSnResult(
1668
- {
1669
- ok: false,
1670
- command: "status",
1671
- error: formatted.message,
1672
- hint: formatted.hint,
1673
- jobId
1674
- },
1675
- true
1676
- );
1677
- } else {
1678
- console.error(formatted.message);
1679
- if (formatted.hint) console.error(`Hint: ${formatted.hint}`);
1680
- }
1681
- process.exit(1);
1682
- }
1683
- job = byExternal;
1684
- }
2007
+ posts = data.posts ?? [];
2008
+ }
2009
+ if (!posts || posts.length === 0) {
2010
+ if (asJson) {
2011
+ emitSnResult({ ok: true, command: "posts", posts: [] }, true);
1685
2012
  } else {
1686
- const { data, error } = await callEdgeFunction("mcp-data", {
1687
- action: "job-status",
1688
- userId,
1689
- jobId
1690
- });
1691
- if (error || !data?.success) {
1692
- const message = `Failed to fetch job status: ${error ?? data?.error ?? "Unknown error"}`;
1693
- if (asJson) {
1694
- emitSnResult({ ok: false, command: "status", error: message, jobId }, true);
1695
- } else {
1696
- console.error(message);
1697
- }
1698
- process.exit(1);
1699
- }
1700
- job = data.job ?? null;
2013
+ console.error("No posts found.");
1701
2014
  }
1702
- if (!job) {
1703
- const message = `No job found with ID "${jobId}".`;
1704
- if (asJson) {
1705
- emitSnResult({ ok: false, command: "status", error: message, jobId }, true);
1706
- } else {
1707
- console.error(message);
1708
- }
1709
- process.exit(1);
2015
+ process.exit(0);
2016
+ }
2017
+ if (asJson) {
2018
+ emitSnResult({ ok: true, command: "posts", posts }, true);
2019
+ } else {
2020
+ for (const post of posts) {
2021
+ console.error(
2022
+ `${post.created_at} | ${post.platform} | ${post.status} | ${post.title ?? "(untitled)"} | ${post.id}`
2023
+ );
1710
2024
  }
2025
+ }
2026
+ process.exit(0);
2027
+ }
2028
+ async function handleRefreshAnalytics(args, asJson) {
2029
+ await ensureAuth3();
2030
+ const { data, error } = await callEdgeFunction(
2031
+ "fetch-analytics",
2032
+ {}
2033
+ );
2034
+ if (error || !data?.success) {
2035
+ throw new Error(`Analytics refresh failed: ${error ?? "Unknown error"}`);
2036
+ }
2037
+ if (asJson) {
2038
+ emitSnResult(
2039
+ { ok: true, command: "refresh-analytics", postsProcessed: data.postsProcessed },
2040
+ true
2041
+ );
2042
+ } else {
2043
+ console.error(`Analytics refresh queued for ${data.postsProcessed} post(s).`);
2044
+ }
2045
+ process.exit(0);
2046
+ }
2047
+ async function handleLoop(args, asJson) {
2048
+ const userId = await ensureAuth3();
2049
+ try {
2050
+ const supabase = getSupabaseClient();
2051
+ const thirtyDaysAgo = /* @__PURE__ */ new Date();
2052
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
2053
+ const [brandResult, contentResult, insightsResult] = await Promise.all([
2054
+ supabase.from("brand_profiles").select("id").eq("user_id", userId).eq("is_active", true).limit(1).maybeSingle(),
2055
+ supabase.from("content_history").select("id, content_type, created_at").eq("user_id", userId).gte("created_at", thirtyDaysAgo.toISOString()).order("created_at", { ascending: false }).limit(10),
2056
+ supabase.from("performance_insights").select("id, insight_type, generated_at").gte("generated_at", thirtyDaysAgo.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).limit(20)
2057
+ ]);
2058
+ const hasProfile = !!brandResult.data;
2059
+ const recentCount = contentResult.data?.length ?? 0;
2060
+ const insightsCount = insightsResult.data?.length ?? 0;
2061
+ let nextAction = "Generate content to start building your feedback loop";
2062
+ if (!hasProfile) nextAction = "Set up your brand profile first";
2063
+ else if (recentCount === 0)
2064
+ nextAction = "Generate and publish content to collect performance data";
2065
+ else if (insightsCount === 0)
2066
+ nextAction = "Publish more content \u2014 insights need 5+ data points";
2067
+ else nextAction = "Loop is active \u2014 use insights to improve next content batch";
1711
2068
  if (asJson) {
1712
- emitSnResult({ ok: true, command: "status", job }, true);
2069
+ emitSnResult(
2070
+ {
2071
+ ok: true,
2072
+ command: "loop",
2073
+ brandStatus: { hasProfile },
2074
+ recentContent: contentResult.data ?? [],
2075
+ currentInsights: insightsResult.data ?? [],
2076
+ recommendedNextAction: nextAction
2077
+ },
2078
+ true
2079
+ );
1713
2080
  } else {
1714
- console.error(`Job: ${job.id}`);
1715
- console.error(`Status: ${job.status}`);
1716
- console.error(`Type: ${job.job_type}`);
1717
- console.error(`Model: ${job.model}`);
1718
- if (job.result_url) console.error(`Result URL: ${job.result_url}`);
1719
- if (job.error_message) console.error(`Error: ${job.error_message}`);
1720
- console.error(`Created: ${job.created_at}`);
1721
- if (job.completed_at) console.error(`Completed: ${job.completed_at}`);
2081
+ console.error("Feedback Loop Summary");
2082
+ console.error("=====================");
2083
+ console.error(`Brand Profile: ${hasProfile ? "Ready" : "Missing"}`);
2084
+ console.error(`Recent Content: ${recentCount} items (last 30 days)`);
2085
+ console.error(`Current Insights: ${insightsCount} active`);
2086
+ console.error(`
2087
+ Next Action: ${nextAction}`);
1722
2088
  }
1723
- process.exit(0);
2089
+ } catch (err) {
2090
+ throw new Error(`Loop summary failed: ${err instanceof Error ? err.message : String(err)}`);
1724
2091
  }
1725
- if (subcommand === "posts") {
1726
- const supabase = tryGetSupabaseClient();
1727
- const daysRaw = args.days;
1728
- const days = typeof daysRaw === "string" ? Number(daysRaw) : 7;
1729
- const lookbackDays = Number.isFinite(days) && days > 0 ? Math.min(days, 90) : 7;
1730
- const since = /* @__PURE__ */ new Date();
1731
- since.setDate(since.getDate() - lookbackDays);
1732
- let posts = [];
1733
- if (supabase) {
1734
- let query = supabase.from("posts").select(
1735
- "id, platform, status, title, external_post_id, scheduled_at, published_at, created_at"
1736
- ).eq("user_id", userId).gte("created_at", since.toISOString()).order("created_at", { ascending: false }).limit(50);
1737
- if (typeof args.platform === "string") {
1738
- query = query.eq("platform", args.platform);
1739
- }
1740
- if (typeof args.status === "string") {
1741
- query = query.eq("status", args.status);
1742
- }
1743
- const { data, error } = await query;
1744
- if (error) {
1745
- const formatted = classifySupabaseCliError("fetch posts", error);
1746
- if (asJson) {
1747
- emitSnResult(
1748
- { ok: false, command: "posts", error: formatted.message, hint: formatted.hint },
1749
- true
1750
- );
1751
- } else {
1752
- console.error(formatted.message);
1753
- if (formatted.hint) console.error(`Hint: ${formatted.hint}`);
1754
- }
1755
- process.exit(1);
1756
- }
1757
- posts = data ?? [];
2092
+ process.exit(0);
2093
+ }
2094
+ var init_analytics = __esm({
2095
+ "src/cli/sn/analytics.ts"() {
2096
+ "use strict";
2097
+ init_edge_function();
2098
+ init_supabase();
2099
+ init_parse();
2100
+ }
2101
+ });
2102
+
2103
+ // src/cli/sn/system.ts
2104
+ var system_exports = {};
2105
+ __export(system_exports, {
2106
+ handleAutopilot: () => handleAutopilot,
2107
+ handleCredits: () => handleCredits,
2108
+ handleStatus: () => handleStatus,
2109
+ handleUsage: () => handleUsage
2110
+ });
2111
+ async function ensureAuth4() {
2112
+ await initializeAuth();
2113
+ return getDefaultUserId();
2114
+ }
2115
+ async function handleStatus(args, asJson) {
2116
+ const jobId = args["job-id"];
2117
+ if (typeof jobId !== "string") {
2118
+ throw new Error("Missing required flag: --job-id");
2119
+ }
2120
+ const userId = await ensureAuth4();
2121
+ const supabase = tryGetSupabaseClient();
2122
+ let job = null;
2123
+ if (supabase) {
2124
+ const { data: byId, error: byIdError } = await supabase.from("async_jobs").select(
2125
+ "id, external_id, status, job_type, model, result_url, error_message, created_at, completed_at"
2126
+ ).eq("user_id", userId).eq("id", jobId).maybeSingle();
2127
+ if (byIdError) {
2128
+ const formatted = classifySupabaseCliError("fetch job status", byIdError);
2129
+ throw new Error(formatted.message);
2130
+ }
2131
+ if (byId) {
2132
+ job = byId;
1758
2133
  } else {
1759
- const { data, error } = await callEdgeFunction(
1760
- "mcp-data",
1761
- {
1762
- action: "recent-posts",
1763
- userId,
1764
- days: lookbackDays,
1765
- limit: 50,
1766
- platform: typeof args.platform === "string" ? args.platform : void 0,
1767
- status: typeof args.status === "string" ? args.status : void 0
1768
- }
1769
- );
1770
- if (error || !data?.success) {
1771
- const message = "Failed to fetch posts: " + (error ?? "Unknown error");
1772
- if (asJson) emitSnResult({ ok: false, command: "posts", error: message }, true);
1773
- else console.error(message);
1774
- process.exit(1);
2134
+ const { data: byExternal, error: byExternalError } = await supabase.from("async_jobs").select(
2135
+ "id, external_id, status, job_type, model, result_url, error_message, created_at, completed_at"
2136
+ ).eq("user_id", userId).eq("external_id", jobId).maybeSingle();
2137
+ if (byExternalError) {
2138
+ const formatted = classifySupabaseCliError("fetch job status", byExternalError);
2139
+ throw new Error(formatted.message);
1775
2140
  }
1776
- posts = data.posts ?? [];
2141
+ job = byExternal;
1777
2142
  }
1778
- if (!posts || posts.length === 0) {
1779
- if (asJson) {
1780
- emitSnResult({ ok: true, command: "posts", posts: [] }, true);
1781
- } else {
1782
- console.error("No posts found.");
1783
- }
1784
- process.exit(0);
2143
+ } else {
2144
+ const { data, error } = await callEdgeFunction("mcp-data", {
2145
+ action: "job-status",
2146
+ userId,
2147
+ jobId
2148
+ });
2149
+ if (error || !data?.success) {
2150
+ throw new Error(`Failed to fetch job status: ${error ?? data?.error ?? "Unknown error"}`);
1785
2151
  }
2152
+ job = data.job ?? null;
2153
+ }
2154
+ if (!job) {
2155
+ throw new Error(`No job found with ID "${jobId}".`);
2156
+ }
2157
+ if (asJson) {
2158
+ emitSnResult({ ok: true, command: "status", job }, true);
2159
+ } else {
2160
+ console.error(`Job: ${job.id}`);
2161
+ console.error(`Status: ${job.status}`);
2162
+ console.error(`Type: ${job.job_type}`);
2163
+ console.error(`Model: ${job.model}`);
2164
+ if (job.result_url) console.error(`Result URL: ${job.result_url}`);
2165
+ if (job.error_message) console.error(`Error: ${job.error_message}`);
2166
+ console.error(`Created: ${job.created_at}`);
2167
+ if (job.completed_at) console.error(`Completed: ${job.completed_at}`);
2168
+ }
2169
+ process.exit(0);
2170
+ }
2171
+ async function handleAutopilot(args, asJson) {
2172
+ const userId = await ensureAuth4();
2173
+ try {
2174
+ const supabase = getSupabaseClient();
2175
+ const [configsResult, approvalsResult] = await Promise.all([
2176
+ supabase.from("autopilot_configs").select("id, platform, is_enabled, schedule_config, updated_at").eq("user_id", userId).eq("is_enabled", true),
2177
+ supabase.from("approval_queue").select("id").eq("user_id", userId).eq("status", "pending")
2178
+ ]);
2179
+ const activeConfigs = configsResult.data?.length ?? 0;
2180
+ const pendingApprovals = approvalsResult.data?.length ?? 0;
1786
2181
  if (asJson) {
1787
- emitSnResult({ ok: true, command: "posts", posts }, true);
2182
+ emitSnResult(
2183
+ {
2184
+ ok: true,
2185
+ command: "autopilot",
2186
+ activeConfigs,
2187
+ pendingApprovals,
2188
+ configs: configsResult.data ?? []
2189
+ },
2190
+ true
2191
+ );
1788
2192
  } else {
1789
- for (const post of posts) {
1790
- console.error(
1791
- `${post.created_at} | ${post.platform} | ${post.status} | ${post.title ?? "(untitled)"} | ${post.id}`
1792
- );
2193
+ console.error("Autopilot Status");
2194
+ console.error("================");
2195
+ console.error(`Active Configs: ${activeConfigs}`);
2196
+ console.error(`Pending Approvals: ${pendingApprovals}`);
2197
+ if (configsResult.data?.length) {
2198
+ console.error("\nConfigs:");
2199
+ for (const cfg of configsResult.data) {
2200
+ console.error(`- ${cfg.platform}: enabled (updated ${cfg.updated_at})`);
2201
+ }
1793
2202
  }
1794
2203
  }
1795
- process.exit(0);
2204
+ } catch (err) {
2205
+ throw new Error(`Autopilot status failed: ${err instanceof Error ? err.message : String(err)}`);
1796
2206
  }
1797
- if (subcommand === "refresh-analytics") {
1798
- const { data, error } = await callEdgeFunction(
1799
- "fetch-analytics",
1800
- {}
1801
- );
1802
- if (error || !data?.success) {
1803
- const message = `Analytics refresh failed: ${error ?? "Unknown error"}`;
2207
+ process.exit(0);
2208
+ }
2209
+ async function handleUsage(args, asJson) {
2210
+ const userId = await ensureAuth4();
2211
+ try {
2212
+ const supabase = getSupabaseClient();
2213
+ const startOfMonth = /* @__PURE__ */ new Date();
2214
+ startOfMonth.setDate(1);
2215
+ startOfMonth.setHours(0, 0, 0, 0);
2216
+ const { data: rows, error: rpcError } = await supabase.rpc("get_mcp_monthly_usage", {
2217
+ p_user_id: userId,
2218
+ p_since: startOfMonth.toISOString()
2219
+ });
2220
+ if (rpcError) {
2221
+ const { data: logs } = await supabase.from("activity_logs").select("action, metadata").eq("user_id", userId).gte("created_at", startOfMonth.toISOString()).like("action", "mcp:%");
2222
+ const totalCalls = logs?.length ?? 0;
1804
2223
  if (asJson) {
1805
- emitSnResult({ ok: false, command: "refresh-analytics", error: message }, true);
2224
+ emitSnResult({ ok: true, command: "usage", totalCalls, totalCredits: 0, tools: [] }, true);
1806
2225
  } else {
1807
- console.error(message);
2226
+ console.error("MCP Usage This Month");
2227
+ console.error("====================");
2228
+ console.error(`Total Calls: ${totalCalls}`);
2229
+ console.error("(Detailed breakdown requires get_mcp_monthly_usage RPC function)");
2230
+ }
2231
+ } else {
2232
+ const tools = rows ?? [];
2233
+ const totalCalls = tools.reduce((sum, t) => sum + (t.call_count ?? 0), 0);
2234
+ const totalCredits = tools.reduce((sum, t) => sum + (t.credits_total ?? 0), 0);
2235
+ if (asJson) {
2236
+ emitSnResult({ ok: true, command: "usage", totalCalls, totalCredits, tools }, true);
2237
+ } else {
2238
+ console.error("MCP Usage This Month");
2239
+ console.error("====================");
2240
+ console.error(`Total Calls: ${totalCalls}`);
2241
+ console.error(`Total Credits: ${totalCredits}`);
2242
+ if (tools.length) {
2243
+ console.error("\nPer-Tool Breakdown:");
2244
+ for (const tool of tools) {
2245
+ console.error(
2246
+ `- ${tool.tool_name}: ${tool.call_count} calls, ${tool.credits_total} credits`
2247
+ );
2248
+ }
2249
+ }
1808
2250
  }
1809
- process.exit(1);
1810
2251
  }
2252
+ } catch (err) {
2253
+ throw new Error(`Usage fetch failed: ${err instanceof Error ? err.message : String(err)}`);
2254
+ }
2255
+ process.exit(0);
2256
+ }
2257
+ async function handleCredits(args, asJson) {
2258
+ const userId = await ensureAuth4();
2259
+ try {
2260
+ const supabase = getSupabaseClient();
2261
+ const [profileResult, subResult] = await Promise.all([
2262
+ supabase.from("user_profiles").select("credits, monthly_credits_used").eq("id", userId).maybeSingle(),
2263
+ supabase.from("subscriptions").select("tier, status, monthly_credits").eq("user_id", userId).eq("status", "active").order("created_at", { ascending: false }).limit(1).maybeSingle()
2264
+ ]);
2265
+ if (profileResult.error) throw profileResult.error;
2266
+ const balance = Number(profileResult.data?.credits || 0);
2267
+ const monthlyUsed = Number(profileResult.data?.monthly_credits_used || 0);
2268
+ const monthlyLimit = Number(subResult.data?.monthly_credits || 0);
2269
+ const plan = subResult.data?.tier || "free";
1811
2270
  if (asJson) {
1812
2271
  emitSnResult(
1813
- { ok: true, command: "refresh-analytics", postsProcessed: data.postsProcessed },
2272
+ { ok: true, command: "credits", balance, monthlyUsed, monthlyLimit, plan },
1814
2273
  true
1815
2274
  );
1816
2275
  } else {
1817
- console.error(`Analytics refresh queued for ${data.postsProcessed} post(s).`);
2276
+ console.error("Credit Balance");
2277
+ console.error("==============");
2278
+ console.error(`Plan: ${plan.toUpperCase()}`);
2279
+ console.error(`Balance: ${balance} credits`);
2280
+ if (monthlyLimit) {
2281
+ console.error(`Monthly Usage: ${monthlyUsed} / ${monthlyLimit}`);
2282
+ }
1818
2283
  }
2284
+ } catch (err) {
2285
+ throw new Error(`Credit balance failed: ${err instanceof Error ? err.message : String(err)}`);
2286
+ }
2287
+ process.exit(0);
2288
+ }
2289
+ var init_system = __esm({
2290
+ "src/cli/sn/system.ts"() {
2291
+ "use strict";
2292
+ init_edge_function();
2293
+ init_supabase();
2294
+ init_parse();
2295
+ }
2296
+ });
2297
+
2298
+ // src/cli/sn/discovery.ts
2299
+ var discovery_exports = {};
2300
+ __export(discovery_exports, {
2301
+ handleInfo: () => handleInfo,
2302
+ handleTools: () => handleTools
2303
+ });
2304
+ async function handleTools(args, asJson) {
2305
+ let tools = TOOL_CATALOG;
2306
+ const scope = args.scope;
2307
+ if (typeof scope === "string") {
2308
+ tools = getToolsByScope(scope);
2309
+ }
2310
+ const module = args.module;
2311
+ if (typeof module === "string") {
2312
+ tools = getToolsByModule(module);
2313
+ }
2314
+ if (asJson) {
2315
+ emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
1819
2316
  process.exit(0);
2317
+ return;
1820
2318
  }
1821
- if (subcommand === "autopilot") {
1822
- try {
1823
- const supabase = getSupabaseClient();
1824
- const [configsResult, approvalsResult] = await Promise.all([
1825
- supabase.from("autopilot_configs").select("id, platform, is_enabled, schedule_config, updated_at").eq("user_id", userId).eq("is_enabled", true),
1826
- supabase.from("approval_queue").select("id").eq("user_id", userId).eq("status", "pending")
1827
- ]);
1828
- const activeConfigs = configsResult.data?.length ?? 0;
1829
- const pendingApprovals = approvalsResult.data?.length ?? 0;
1830
- if (asJson) {
1831
- emitSnResult(
1832
- {
1833
- ok: true,
1834
- command: "autopilot",
1835
- activeConfigs,
1836
- pendingApprovals,
1837
- configs: configsResult.data ?? []
1838
- },
1839
- true
1840
- );
1841
- } else {
1842
- console.error("Autopilot Status");
1843
- console.error("================");
1844
- console.error(`Active Configs: ${activeConfigs}`);
1845
- console.error(`Pending Approvals: ${pendingApprovals}`);
1846
- if (configsResult.data?.length) {
1847
- console.error("\nConfigs:");
1848
- for (const cfg of configsResult.data) {
1849
- console.error(`- ${cfg.platform}: enabled (updated ${cfg.updated_at})`);
1850
- }
1851
- }
2319
+ const grouped = /* @__PURE__ */ new Map();
2320
+ for (const tool of tools) {
2321
+ const group = grouped.get(tool.module) ?? [];
2322
+ group.push(tool);
2323
+ grouped.set(tool.module, group);
2324
+ }
2325
+ if (grouped.size === 0) {
2326
+ console.error("No tools found matching the given filters.");
2327
+ process.exit(0);
2328
+ return;
2329
+ }
2330
+ for (const [moduleName, moduleTools] of grouped) {
2331
+ console.error(`
2332
+ Module: ${moduleName} (${moduleTools.length} tools)`);
2333
+ const maxNameLen = Math.max(...moduleTools.map((t) => t.name.length));
2334
+ for (const tool of moduleTools) {
2335
+ const padded = tool.name.padEnd(maxNameLen + 2);
2336
+ console.error(` ${padded}${tool.description}`);
2337
+ }
2338
+ }
2339
+ console.error("");
2340
+ process.exit(0);
2341
+ }
2342
+ async function handleInfo(args, asJson) {
2343
+ const info = {
2344
+ version: MCP_VERSION,
2345
+ toolCount: TOOL_CATALOG.length,
2346
+ modules: getModules()
2347
+ };
2348
+ try {
2349
+ const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
2350
+ const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
2351
+ const apiKey = await loadApiKey2();
2352
+ if (apiKey) {
2353
+ const result = await validateApiKey2(apiKey);
2354
+ if (result.valid) {
2355
+ info.auth = {
2356
+ email: result.email || null,
2357
+ scopes: result.scopes || [],
2358
+ expiresAt: result.expiresAt || null
2359
+ };
1852
2360
  }
1853
- } catch (err) {
1854
- const message = `Autopilot status failed: ${err instanceof Error ? err.message : String(err)}`;
1855
- if (asJson) emitSnResult({ ok: false, command: "autopilot", error: message }, true);
1856
- else console.error(message);
1857
- process.exit(1);
1858
2361
  }
2362
+ } catch {
2363
+ info.auth = null;
2364
+ }
2365
+ if (info.auth) {
2366
+ try {
2367
+ const { callEdgeFunction: callEdgeFunction2 } = await Promise.resolve().then(() => (init_edge_function(), edge_function_exports));
2368
+ const { data } = await callEdgeFunction2("mcp-data", {
2369
+ action: "credit-balance"
2370
+ });
2371
+ info.creditBalance = data?.balance ?? null;
2372
+ } catch {
2373
+ info.creditBalance = null;
2374
+ }
2375
+ }
2376
+ if (asJson) {
2377
+ emitSnResult({ ok: true, command: "info", data: info }, true);
1859
2378
  process.exit(0);
2379
+ return;
2380
+ }
2381
+ console.error(`Version: ${info.version}`);
2382
+ console.error(`Tools: ${info.toolCount}`);
2383
+ console.error(`Modules: ${info.modules.join(", ")}`);
2384
+ if (info.auth === null) {
2385
+ console.error("Auth: not configured");
2386
+ } else if (info.auth) {
2387
+ const auth = info.auth;
2388
+ console.error(`Auth: ${auth.email ?? "authenticated"}`);
2389
+ console.error(`Scopes: ${auth.scopes.length > 0 ? auth.scopes.join(", ") : "none"}`);
2390
+ if (auth.expiresAt) {
2391
+ console.error(`Expires: ${auth.expiresAt}`);
2392
+ }
2393
+ }
2394
+ if (info.creditBalance !== void 0) {
2395
+ console.error(`Credits: ${info.creditBalance !== null ? info.creditBalance : "unavailable"}`);
2396
+ }
2397
+ console.error("");
2398
+ process.exit(0);
2399
+ }
2400
+ var init_discovery = __esm({
2401
+ "src/cli/sn/discovery.ts"() {
2402
+ "use strict";
2403
+ init_tool_catalog();
2404
+ init_parse();
2405
+ init_version();
2406
+ }
2407
+ });
2408
+
2409
+ // src/cli/sn/planning.ts
2410
+ var planning_exports = {};
2411
+ __export(planning_exports, {
2412
+ handlePlan: () => handlePlan
2413
+ });
2414
+ async function ensureAuth5() {
2415
+ await initializeAuth();
2416
+ return getDefaultUserId();
2417
+ }
2418
+ async function handlePlanList(args, asJson) {
2419
+ const status = typeof args.status === "string" ? args.status : void 0;
2420
+ const userId = await ensureAuth5();
2421
+ const body = { action: "list-content-plans", userId };
2422
+ if (status) {
2423
+ body.status = status;
2424
+ }
2425
+ const { data, error } = await callEdgeFunction("mcp-data", body);
2426
+ if (error || !data) {
2427
+ throw new Error(`Failed to list plans: ${error ?? "Unknown error"}`);
2428
+ }
2429
+ const plans = data.plans ?? [];
2430
+ if (asJson) {
2431
+ emitSnResult({ ok: true, command: "plan list", plans }, true);
2432
+ } else {
2433
+ if (plans.length === 0) {
2434
+ console.error("No content plans found.");
2435
+ } else {
2436
+ console.error("plan-id | status | created_at | title");
2437
+ console.error("--------|--------|------------|------");
2438
+ for (const p of plans) {
2439
+ const title = p.title ?? p.summary ?? "(untitled)";
2440
+ console.error(`${p.id} | ${p.status} | ${p.created_at} | ${title}`);
2441
+ }
2442
+ }
2443
+ }
2444
+ }
2445
+ async function handlePlanView(args, asJson) {
2446
+ const planId = args["plan-id"];
2447
+ if (typeof planId !== "string") {
2448
+ throw new Error("Missing required flag: --plan-id");
2449
+ }
2450
+ const userId = await ensureAuth5();
2451
+ const { data, error } = await callEdgeFunction("mcp-data", {
2452
+ action: "get-content-plan",
2453
+ userId,
2454
+ planId
2455
+ });
2456
+ if (error || !data) {
2457
+ throw new Error(`Failed to view plan: ${error ?? "Unknown error"}`);
2458
+ }
2459
+ const plan = data.plan;
2460
+ if (asJson) {
2461
+ emitSnResult({ ok: true, command: "plan view", plan }, true);
2462
+ } else {
2463
+ const title = plan.title ?? plan.summary ?? "(untitled)";
2464
+ console.error(`Plan: ${plan.id}`);
2465
+ console.error(`Title: ${title}`);
2466
+ console.error(`Status: ${plan.status}`);
2467
+ console.error(`Created: ${plan.created_at}`);
2468
+ if (plan.plan_payload) {
2469
+ console.error(`Payload: ${JSON.stringify(plan.plan_payload, null, 2)}`);
2470
+ }
2471
+ }
2472
+ }
2473
+ async function handlePlanApprove(args, asJson) {
2474
+ const planId = args["plan-id"];
2475
+ if (typeof planId !== "string") {
2476
+ throw new Error("Missing required flag: --plan-id");
2477
+ }
2478
+ const userId = await ensureAuth5();
2479
+ const { data, error } = await callEdgeFunction("mcp-data", {
2480
+ action: "respond-plan-approval",
2481
+ userId,
2482
+ planId,
2483
+ response: "approved"
2484
+ });
2485
+ if (error || !data) {
2486
+ throw new Error(`Failed to approve plan: ${error ?? "Unknown error"}`);
2487
+ }
2488
+ if (asJson) {
2489
+ emitSnResult(
2490
+ {
2491
+ ok: data.success,
2492
+ command: "plan approve",
2493
+ planId,
2494
+ message: data.message ?? "Plan approved"
2495
+ },
2496
+ true
2497
+ );
2498
+ } else {
2499
+ console.error(`Plan ${planId}: ${data.success ? "Approved" : "Failed"}`);
2500
+ if (data.message) {
2501
+ console.error(data.message);
2502
+ }
2503
+ }
2504
+ }
2505
+ async function handlePlan(args, asJson) {
2506
+ const subcommand = args._[0];
2507
+ if (!subcommand || args.help === true) {
2508
+ console.error(PLAN_USAGE);
2509
+ return;
2510
+ }
2511
+ switch (subcommand) {
2512
+ case "list":
2513
+ return handlePlanList({ ...args, _: args._.slice(1) }, asJson);
2514
+ case "view":
2515
+ return handlePlanView({ ...args, _: args._.slice(1) }, asJson);
2516
+ case "approve":
2517
+ return handlePlanApprove({ ...args, _: args._.slice(1) }, asJson);
2518
+ default:
2519
+ throw new Error(`Unknown plan subcommand: '${subcommand}'. Run 'sn plan --help' for usage.`);
2520
+ }
2521
+ }
2522
+ var PLAN_USAGE;
2523
+ var init_planning = __esm({
2524
+ "src/cli/sn/planning.ts"() {
2525
+ "use strict";
2526
+ init_edge_function();
2527
+ init_supabase();
2528
+ init_parse();
2529
+ PLAN_USAGE = `Usage: sn plan <subcommand> [flags]
2530
+
2531
+ Subcommands:
2532
+ list List content plans
2533
+ view View a single content plan
2534
+ approve Approve a content plan
2535
+
2536
+ Flags:
2537
+ list:
2538
+ --status <draft|submitted|approved> Filter by status (optional)
2539
+
2540
+ view:
2541
+ --plan-id <id> Plan ID (required)
2542
+
2543
+ approve:
2544
+ --plan-id <id> Plan ID (required)
2545
+ `;
2546
+ }
2547
+ });
2548
+
2549
+ // src/cli/sn/presets.ts
2550
+ var presets_exports = {};
2551
+ __export(presets_exports, {
2552
+ BUILTIN_PRESETS: () => BUILTIN_PRESETS,
2553
+ handlePreset: () => handlePreset,
2554
+ resolvePreset: () => resolvePreset
2555
+ });
2556
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "node:fs";
2557
+ import { join as join2 } from "node:path";
2558
+ import { homedir as homedir2 } from "node:os";
2559
+ function loadUserPresets() {
2560
+ if (!existsSync2(PRESETS_FILE)) return [];
2561
+ try {
2562
+ const raw = readFileSync2(PRESETS_FILE, "utf-8");
2563
+ const parsed = JSON.parse(raw);
2564
+ if (!Array.isArray(parsed)) return [];
2565
+ return parsed;
2566
+ } catch {
2567
+ return [];
2568
+ }
2569
+ }
2570
+ function saveUserPresets(presets) {
2571
+ if (!existsSync2(PRESETS_DIR)) {
2572
+ mkdirSync2(PRESETS_DIR, { recursive: true });
2573
+ }
2574
+ writeFileSync2(PRESETS_FILE, JSON.stringify(presets, null, 2) + "\n", "utf-8");
2575
+ }
2576
+ function resolvePreset(name) {
2577
+ const lower = name.toLowerCase();
2578
+ const builtin = BUILTIN_PRESETS.find((p) => p.name === lower);
2579
+ if (builtin) return builtin;
2580
+ const userPresets = loadUserPresets();
2581
+ return userPresets.find((p) => p.name === lower) ?? null;
2582
+ }
2583
+ function handlePresetList(asJson) {
2584
+ const all = [...BUILTIN_PRESETS, ...loadUserPresets()];
2585
+ if (asJson) {
2586
+ emitSnResult({ ok: true, command: "preset", presets: all, count: all.length }, asJson);
2587
+ return;
2588
+ }
2589
+ const header = "NAME PLATFORM MAX-LEN RATIO SOURCE";
2590
+ const lines = all.map((p) => {
2591
+ const name = p.name.padEnd(20);
2592
+ const plat = p.platform.padEnd(12);
2593
+ const len = String(p.maxLength).padEnd(8);
2594
+ const ratio = (p.aspectRatio ?? "-").padEnd(8);
2595
+ const src = p.builtin ? "builtin" : "user";
2596
+ return `${name} ${plat} ${len} ${ratio} ${src}`;
2597
+ });
2598
+ process.stdout.write(header + "\n" + lines.join("\n") + "\n");
2599
+ }
2600
+ function handlePresetShow(args, asJson) {
2601
+ const name = args["name"];
2602
+ if (!name || typeof name !== "string") {
2603
+ throw new Error('--name is required for "preset show"');
2604
+ }
2605
+ const preset = resolvePreset(name);
2606
+ if (!preset) {
2607
+ throw new Error(`Preset "${name}" not found`);
2608
+ }
2609
+ if (asJson) {
2610
+ emitSnResult({ ok: true, command: "preset", preset }, asJson);
2611
+ return;
2612
+ }
2613
+ process.stdout.write(
2614
+ `name: ${preset.name}
2615
+ platform: ${preset.platform}
2616
+ maxLength: ${preset.maxLength}
2617
+ aspectRatio: ${preset.aspectRatio ?? "-"}
2618
+ source: ${preset.builtin ? "builtin" : "user"}
2619
+ `
2620
+ );
2621
+ }
2622
+ function handlePresetSave(args, asJson) {
2623
+ const name = args["name"];
2624
+ if (!name || typeof name !== "string") {
2625
+ throw new Error('--name is required for "preset save"');
2626
+ }
2627
+ const platform3 = args["platform"];
2628
+ if (!platform3 || typeof platform3 !== "string") {
2629
+ throw new Error('--platform is required for "preset save"');
2630
+ }
2631
+ const maxLengthRaw = args["max-length"];
2632
+ if (!maxLengthRaw) {
2633
+ throw new Error('--max-length is required for "preset save"');
2634
+ }
2635
+ const maxLength = Number(maxLengthRaw);
2636
+ if (!Number.isFinite(maxLength) || maxLength <= 0) {
2637
+ throw new Error("--max-length must be a positive number");
2638
+ }
2639
+ const lowerName = name.toLowerCase();
2640
+ if (BUILTIN_PRESETS.some((p) => p.name === lowerName)) {
2641
+ throw new Error(`Cannot overwrite builtin preset "${lowerName}"`);
2642
+ }
2643
+ const aspectRatio = typeof args["aspect-ratio"] === "string" ? args["aspect-ratio"] : void 0;
2644
+ const preset = {
2645
+ name: lowerName,
2646
+ platform: platform3,
2647
+ maxLength,
2648
+ aspectRatio,
2649
+ builtin: false
2650
+ };
2651
+ const userPresets = loadUserPresets();
2652
+ const idx = userPresets.findIndex((p) => p.name === lowerName);
2653
+ if (idx >= 0) {
2654
+ userPresets[idx] = preset;
2655
+ } else {
2656
+ userPresets.push(preset);
2657
+ }
2658
+ saveUserPresets(userPresets);
2659
+ if (asJson) {
2660
+ emitSnResult({ ok: true, command: "preset", saved: preset }, asJson);
2661
+ return;
2662
+ }
2663
+ process.stdout.write(`Preset "${lowerName}" saved.
2664
+ `);
2665
+ }
2666
+ function handlePresetDelete(args, asJson) {
2667
+ const name = args["name"];
2668
+ if (!name || typeof name !== "string") {
2669
+ throw new Error('--name is required for "preset delete"');
2670
+ }
2671
+ const lowerName = name.toLowerCase();
2672
+ if (BUILTIN_PRESETS.some((p) => p.name === lowerName)) {
2673
+ throw new Error(`Cannot delete builtin preset "${lowerName}"`);
2674
+ }
2675
+ const userPresets = loadUserPresets();
2676
+ const idx = userPresets.findIndex((p) => p.name === lowerName);
2677
+ if (idx < 0) {
2678
+ throw new Error(`User preset "${lowerName}" not found`);
2679
+ }
2680
+ userPresets.splice(idx, 1);
2681
+ saveUserPresets(userPresets);
2682
+ if (asJson) {
2683
+ emitSnResult({ ok: true, command: "preset", deleted: lowerName }, asJson);
2684
+ return;
2685
+ }
2686
+ process.stdout.write(`Preset "${lowerName}" deleted.
2687
+ `);
2688
+ }
2689
+ async function handlePreset(args, asJson) {
2690
+ const sub = args._[0];
2691
+ if (!sub || sub === "--help" || args["help"] === true) {
2692
+ process.stdout.write(USAGE);
2693
+ return;
2694
+ }
2695
+ switch (sub) {
2696
+ case "list":
2697
+ return handlePresetList(asJson);
2698
+ case "show":
2699
+ return handlePresetShow(args, asJson);
2700
+ case "save":
2701
+ return handlePresetSave(args, asJson);
2702
+ case "delete":
2703
+ return handlePresetDelete(args, asJson);
2704
+ default:
2705
+ throw new Error(`Unknown preset sub-command: "${sub}". Run "sn preset --help" for usage.`);
2706
+ }
2707
+ }
2708
+ var BUILTIN_PRESETS, PRESETS_DIR, PRESETS_FILE, USAGE;
2709
+ var init_presets = __esm({
2710
+ "src/cli/sn/presets.ts"() {
2711
+ "use strict";
2712
+ init_parse();
2713
+ BUILTIN_PRESETS = [
2714
+ {
2715
+ name: "instagram-reel",
2716
+ platform: "Instagram",
2717
+ maxLength: 2200,
2718
+ aspectRatio: "9:16",
2719
+ builtin: true
2720
+ },
2721
+ {
2722
+ name: "instagram-post",
2723
+ platform: "Instagram",
2724
+ maxLength: 2200,
2725
+ aspectRatio: "1:1",
2726
+ builtin: true
2727
+ },
2728
+ { name: "tiktok", platform: "TikTok", maxLength: 4e3, aspectRatio: "9:16", builtin: true },
2729
+ {
2730
+ name: "youtube-short",
2731
+ platform: "YouTube",
2732
+ maxLength: 5e3,
2733
+ aspectRatio: "9:16",
2734
+ builtin: true
2735
+ },
2736
+ { name: "linkedin-post", platform: "LinkedIn", maxLength: 3e3, builtin: true },
2737
+ { name: "twitter-post", platform: "Twitter", maxLength: 280, builtin: true }
2738
+ ];
2739
+ PRESETS_DIR = join2(homedir2(), ".config", "socialneuron");
2740
+ PRESETS_FILE = join2(PRESETS_DIR, "presets.json");
2741
+ USAGE = `Usage: sn preset <sub-command> [options]
2742
+
2743
+ Sub-commands:
2744
+ list List all presets (builtin + user)
2745
+ show --name <preset> Show a single preset
2746
+ save --name <n> --platform <p> --max-length <len> [--aspect-ratio <r>]
2747
+ delete --name <preset> Delete a user preset
2748
+ `;
2749
+ }
2750
+ });
2751
+
2752
+ // src/cli/sn.ts
2753
+ var sn_exports = {};
2754
+ __export(sn_exports, {
2755
+ printSnUsage: () => printSnUsage,
2756
+ runSnCli: () => runSnCli
2757
+ });
2758
+ function lazyContent(name) {
2759
+ return async (args, asJson) => {
2760
+ const mod = await Promise.resolve().then(() => (init_content(), content_exports));
2761
+ return mod[name](args, asJson);
2762
+ };
2763
+ }
2764
+ function lazyAccount(name) {
2765
+ return async (args, asJson) => {
2766
+ const mod = await Promise.resolve().then(() => (init_account(), account_exports));
2767
+ return mod[name](args, asJson);
2768
+ };
2769
+ }
2770
+ function lazyAnalytics(name) {
2771
+ return async (args, asJson) => {
2772
+ const mod = await Promise.resolve().then(() => (init_analytics(), analytics_exports));
2773
+ return mod[name](args, asJson);
2774
+ };
2775
+ }
2776
+ function lazySystem(name) {
2777
+ return async (args, asJson) => {
2778
+ const mod = await Promise.resolve().then(() => (init_system(), system_exports));
2779
+ return mod[name](args, asJson);
2780
+ };
2781
+ }
2782
+ function lazyDiscovery(name) {
2783
+ return async (args, asJson) => {
2784
+ const mod = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
2785
+ return mod[name](args, asJson);
2786
+ };
2787
+ }
2788
+ function lazyPlanning(name) {
2789
+ return async (args, asJson) => {
2790
+ const mod = await Promise.resolve().then(() => (init_planning(), planning_exports));
2791
+ return mod[name](args, asJson);
2792
+ };
2793
+ }
2794
+ function lazyPresets(name) {
2795
+ return async (args, asJson) => {
2796
+ const mod = await Promise.resolve().then(() => (init_presets(), presets_exports));
2797
+ return mod[name](args, asJson);
2798
+ };
2799
+ }
2800
+ function printSnUsage() {
2801
+ console.error("");
2802
+ console.error("Usage: socialneuron-mcp sn <command> [flags]");
2803
+ console.error("");
2804
+ console.error("Discovery:");
2805
+ console.error(" tools [--scope <scope>] [--module <module>] [--json]");
2806
+ console.error(" info [--json]");
2807
+ console.error("");
2808
+ console.error("Content:");
2809
+ console.error(
2810
+ " publish --media-url <url> --caption <text> --platforms <comma-list> --confirm [--title <text>] [--schedule-at <iso8601>] [--idempotency-key <key>] [--json]"
2811
+ );
2812
+ console.error(
2813
+ " quality-check --caption <text> [--title <text>] [--platforms <comma-list>] [--threshold <0-35>] [--json]"
2814
+ );
2815
+ console.error(
2816
+ " e2e --media-url <url> --caption <text> --platforms <comma-list> --confirm [--title <text>] [--schedule-at <iso8601>] [--check-urls] [--threshold <0-35>] [--dry-run] [--force] [--json]"
2817
+ );
2818
+ console.error(
2819
+ " plan (list|view|approve) [--plan-id <id>] [--status <draft|submitted|approved>] [--json]"
2820
+ );
2821
+ console.error(
2822
+ " preset (list|show|save|delete) [--name <name>] [--platform <name>] [--max-length <n>] [--aspect-ratio <ratio>] [--json]"
2823
+ );
2824
+ console.error("");
2825
+ console.error("Account:");
2826
+ console.error(" preflight [--privacy-url <url>] [--terms-url <url>] [--check-urls] [--json]");
2827
+ console.error(" oauth-health [--warn-days <1-90>] [--platforms <comma-list>] [--all] [--json]");
2828
+ console.error(" oauth-refresh (--platforms <comma-list> | --all) [--json]");
2829
+ console.error("");
2830
+ console.error("Analytics:");
2831
+ console.error(
2832
+ " posts [--days <1-90>] [--platform <name>] [--status <draft|scheduled|published|failed>] [--json]"
2833
+ );
2834
+ console.error(" refresh-analytics [--json]");
2835
+ console.error(" loop [--json]");
2836
+ console.error("");
2837
+ console.error("System:");
2838
+ console.error(" status --job-id <id> [--json]");
2839
+ console.error(" autopilot [--json]");
2840
+ console.error(" usage [--json]");
2841
+ console.error(" credits [--json]");
2842
+ console.error("");
2843
+ }
2844
+ async function runSnCli(argv) {
2845
+ const [first, ...rest] = argv;
2846
+ if (!first) {
2847
+ printSnUsage();
2848
+ process.exit(1);
1860
2849
  }
1861
- if (subcommand === "usage") {
1862
- try {
1863
- const supabase = getSupabaseClient();
1864
- const startOfMonth = /* @__PURE__ */ new Date();
1865
- startOfMonth.setDate(1);
1866
- startOfMonth.setHours(0, 0, 0, 0);
1867
- const { data: rows, error: rpcError } = await supabase.rpc("get_mcp_monthly_usage", {
1868
- p_user_id: userId,
1869
- p_since: startOfMonth.toISOString()
1870
- });
1871
- if (rpcError) {
1872
- const { data: logs } = await supabase.from("activity_logs").select("action, metadata").eq("user_id", userId).gte("created_at", startOfMonth.toISOString()).like("action", "mcp:%");
1873
- const totalCalls = logs?.length ?? 0;
1874
- if (asJson) {
1875
- emitSnResult(
1876
- { ok: true, command: "usage", totalCalls, totalCredits: 0, tools: [] },
1877
- true
1878
- );
1879
- } else {
1880
- console.error("MCP Usage This Month");
1881
- console.error("====================");
1882
- console.error(`Total Calls: ${totalCalls}`);
1883
- console.error("(Detailed breakdown requires get_mcp_monthly_usage RPC function)");
1884
- }
1885
- } else {
1886
- const tools = rows ?? [];
1887
- const totalCalls = tools.reduce((sum, t) => sum + (t.call_count ?? 0), 0);
1888
- const totalCredits = tools.reduce((sum, t) => sum + (t.credits_total ?? 0), 0);
1889
- if (asJson) {
1890
- emitSnResult({ ok: true, command: "usage", totalCalls, totalCredits, tools }, true);
1891
- } else {
1892
- console.error("MCP Usage This Month");
1893
- console.error("====================");
1894
- console.error(`Total Calls: ${totalCalls}`);
1895
- console.error(`Total Credits: ${totalCredits}`);
1896
- if (tools.length) {
1897
- console.error("\nPer-Tool Breakdown:");
1898
- for (const tool of tools) {
1899
- console.error(
1900
- `- ${tool.tool_name}: ${tool.call_count} calls, ${tool.credits_total} credits`
1901
- );
1902
- }
1903
- }
1904
- }
2850
+ if (GROUP_COMMANDS[first]) {
2851
+ const [subcommand, ...groupRest] = rest;
2852
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
2853
+ console.error(`
2854
+ Commands in "${first}" group:`);
2855
+ for (const cmd of GROUP_COMMANDS[first]) {
2856
+ console.error(` ${cmd}`);
1905
2857
  }
1906
- } catch (err) {
1907
- const message = `Usage fetch failed: ${err instanceof Error ? err.message : String(err)}`;
1908
- if (asJson) emitSnResult({ ok: false, command: "usage", error: message }, true);
1909
- else console.error(message);
1910
- process.exit(1);
2858
+ console.error("");
2859
+ process.exit(subcommand ? 0 : 1);
1911
2860
  }
1912
- process.exit(0);
1913
- }
1914
- if (subcommand === "loop") {
1915
- try {
1916
- const supabase = getSupabaseClient();
1917
- const thirtyDaysAgo = /* @__PURE__ */ new Date();
1918
- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
1919
- const [brandResult, contentResult, insightsResult] = await Promise.all([
1920
- supabase.from("brand_profiles").select("id").eq("user_id", userId).eq("is_active", true).limit(1).maybeSingle(),
1921
- supabase.from("content_history").select("id, content_type, created_at").eq("user_id", userId).gte("created_at", thirtyDaysAgo.toISOString()).order("created_at", { ascending: false }).limit(10),
1922
- supabase.from("performance_insights").select("id, insight_type, generated_at").gte("generated_at", thirtyDaysAgo.toISOString()).gt("expires_at", (/* @__PURE__ */ new Date()).toISOString()).limit(20)
1923
- ]);
1924
- const hasProfile = !!brandResult.data;
1925
- const recentCount = contentResult.data?.length ?? 0;
1926
- const insightsCount = insightsResult.data?.length ?? 0;
1927
- let nextAction = "Generate content to start building your feedback loop";
1928
- if (!hasProfile) nextAction = "Set up your brand profile first";
1929
- else if (recentCount === 0)
1930
- nextAction = "Generate and publish content to collect performance data";
1931
- else if (insightsCount === 0)
1932
- nextAction = "Publish more content \u2014 insights need 5+ data points";
1933
- else nextAction = "Loop is active \u2014 use insights to improve next content batch";
1934
- if (asJson) {
1935
- emitSnResult(
1936
- {
1937
- ok: true,
1938
- command: "loop",
1939
- brandStatus: { hasProfile },
1940
- recentContent: contentResult.data ?? [],
1941
- currentInsights: insightsResult.data ?? [],
1942
- recommendedNextAction: nextAction
1943
- },
1944
- true
1945
- );
1946
- } else {
1947
- console.error("Feedback Loop Summary");
1948
- console.error("=====================");
1949
- console.error(`Brand Profile: ${hasProfile ? "Ready" : "Missing"}`);
1950
- console.error(`Recent Content: ${recentCount} items (last 30 days)`);
1951
- console.error(`Current Insights: ${insightsCount} active`);
1952
- console.error(`
1953
- Next Action: ${nextAction}`);
1954
- }
1955
- } catch (err) {
1956
- const message = `Loop summary failed: ${err instanceof Error ? err.message : String(err)}`;
1957
- if (asJson) emitSnResult({ ok: false, command: "loop", error: message }, true);
1958
- else console.error(message);
2861
+ const entry2 = COMMAND_REGISTRY[subcommand];
2862
+ if (!entry2 || entry2.group !== first) {
2863
+ console.error(`Unknown ${first} subcommand: ${subcommand}`);
2864
+ console.error(`Available: ${GROUP_COMMANDS[first].join(", ")}`);
1959
2865
  process.exit(1);
1960
2866
  }
1961
- process.exit(0);
2867
+ const args2 = parseSnArgs(groupRest);
2868
+ const asJson2 = isEnabledFlag(args2.json);
2869
+ await withSnErrorHandling(subcommand, asJson2, () => entry2.handler(args2, asJson2));
2870
+ return;
1962
2871
  }
1963
- if (subcommand === "credits") {
1964
- try {
1965
- const supabase = getSupabaseClient();
1966
- const [profileResult, subResult] = await Promise.all([
1967
- supabase.from("user_profiles").select("credits, monthly_credits_used").eq("id", userId).maybeSingle(),
1968
- supabase.from("subscriptions").select("tier, status, monthly_credits").eq("user_id", userId).eq("status", "active").order("created_at", { ascending: false }).limit(1).maybeSingle()
1969
- ]);
1970
- if (profileResult.error) throw profileResult.error;
1971
- const balance = Number(profileResult.data?.credits || 0);
1972
- const monthlyUsed = Number(profileResult.data?.monthly_credits_used || 0);
1973
- const monthlyLimit = Number(subResult.data?.monthly_credits || 0);
1974
- const plan = subResult.data?.tier || "free";
1975
- if (asJson) {
1976
- emitSnResult(
1977
- { ok: true, command: "credits", balance, monthlyUsed, monthlyLimit, plan },
1978
- true
1979
- );
1980
- } else {
1981
- console.error("Credit Balance");
1982
- console.error("==============");
1983
- console.error(`Plan: ${plan.toUpperCase()}`);
1984
- console.error(`Balance: ${balance} credits`);
1985
- if (monthlyLimit) {
1986
- console.error(`Monthly Usage: ${monthlyUsed} / ${monthlyLimit}`);
1987
- }
1988
- }
1989
- } catch (err) {
1990
- const message = `Credit balance failed: ${err instanceof Error ? err.message : String(err)}`;
1991
- if (asJson) emitSnResult({ ok: false, command: "credits", error: message }, true);
1992
- else console.error(message);
1993
- process.exit(1);
1994
- }
1995
- process.exit(0);
2872
+ const entry = COMMAND_REGISTRY[first];
2873
+ if (!entry) {
2874
+ console.error(`Unknown subcommand: ${first}`);
2875
+ printSnUsage();
2876
+ process.exit(1);
1996
2877
  }
1997
- console.error(`Unknown subcommand: ${subcommand}`);
1998
- printSnUsage();
1999
- process.exit(1);
2878
+ const args = parseSnArgs(rest);
2879
+ const asJson = isEnabledFlag(args.json);
2880
+ await withSnErrorHandling(first, asJson, () => entry.handler(args, asJson));
2000
2881
  }
2882
+ var COMMAND_REGISTRY, GROUP_COMMANDS;
2001
2883
  var init_sn = __esm({
2002
2884
  "src/cli/sn.ts"() {
2003
2885
  "use strict";
2004
- init_edge_function();
2005
- init_quality();
2006
- init_supabase();
2886
+ init_parse();
2887
+ init_error_handling();
2888
+ COMMAND_REGISTRY = {
2889
+ publish: { handler: lazyContent("handlePublish"), needsAuth: true, group: "content" },
2890
+ "quality-check": {
2891
+ handler: lazyContent("handleQualityCheck"),
2892
+ needsAuth: false,
2893
+ group: "content"
2894
+ },
2895
+ e2e: { handler: lazyContent("handleE2e"), needsAuth: true, group: "content" },
2896
+ "oauth-health": {
2897
+ handler: lazyAccount("handleOauthHealth"),
2898
+ needsAuth: true,
2899
+ group: "account"
2900
+ },
2901
+ "oauth-refresh": {
2902
+ handler: lazyAccount("handleOauthRefresh"),
2903
+ needsAuth: true,
2904
+ group: "account"
2905
+ },
2906
+ preflight: { handler: lazyAccount("handlePreflight"), needsAuth: true, group: "account" },
2907
+ status: { handler: lazySystem("handleStatus"), needsAuth: true, group: "system" },
2908
+ autopilot: { handler: lazySystem("handleAutopilot"), needsAuth: true, group: "system" },
2909
+ usage: { handler: lazySystem("handleUsage"), needsAuth: true, group: "system" },
2910
+ credits: { handler: lazySystem("handleCredits"), needsAuth: true, group: "system" },
2911
+ posts: { handler: lazyAnalytics("handlePosts"), needsAuth: true, group: "analytics" },
2912
+ "refresh-analytics": {
2913
+ handler: lazyAnalytics("handleRefreshAnalytics"),
2914
+ needsAuth: true,
2915
+ group: "analytics"
2916
+ },
2917
+ loop: { handler: lazyAnalytics("handleLoop"), needsAuth: true, group: "analytics" },
2918
+ tools: { handler: lazyDiscovery("handleTools"), needsAuth: false, group: "discovery" },
2919
+ info: { handler: lazyDiscovery("handleInfo"), needsAuth: false, group: "discovery" },
2920
+ plan: { handler: lazyPlanning("handlePlan"), needsAuth: true, group: "content" },
2921
+ preset: { handler: lazyPresets("handlePreset"), needsAuth: false, group: "content" }
2922
+ };
2923
+ GROUP_COMMANDS = {
2924
+ content: ["publish", "quality-check", "e2e", "plan", "preset"],
2925
+ account: ["oauth-health", "oauth-refresh", "preflight"],
2926
+ analytics: ["posts", "refresh-analytics", "loop"],
2927
+ system: ["status", "autopilot", "usage", "credits"],
2928
+ discovery: ["tools", "info"]
2929
+ };
2007
2930
  }
2008
2931
  });
2009
2932
 
@@ -2017,9 +2940,9 @@ __export(setup_exports, {
2017
2940
  });
2018
2941
  import { createHash as createHash4, randomBytes, randomUUID as randomUUID3 } from "node:crypto";
2019
2942
  import { createServer } from "node:http";
2020
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
2021
- import { homedir as homedir2, platform as platform2 } from "node:os";
2022
- import { join as join2 } from "node:path";
2943
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
2944
+ import { homedir as homedir3, platform as platform2 } from "node:os";
2945
+ import { join as join3 } from "node:path";
2023
2946
  function base64url(buffer) {
2024
2947
  return buffer.toString("base64url");
2025
2948
  }
@@ -2041,8 +2964,8 @@ function getConfigPaths() {
2041
2964
  const os = platform2();
2042
2965
  if (os === "darwin") {
2043
2966
  paths.push({
2044
- path: join2(
2045
- homedir2(),
2967
+ path: join3(
2968
+ homedir3(),
2046
2969
  "Library",
2047
2970
  "Application Support",
2048
2971
  "Claude",
@@ -2052,16 +2975,16 @@ function getConfigPaths() {
2052
2975
  });
2053
2976
  } else if (os === "linux") {
2054
2977
  paths.push({
2055
- path: join2(homedir2(), ".config", "claude", "claude_desktop_config.json"),
2978
+ path: join3(homedir3(), ".config", "claude", "claude_desktop_config.json"),
2056
2979
  name: "Claude Desktop"
2057
2980
  });
2058
2981
  }
2059
- const claudeCodeGlobal = join2(homedir2(), ".claude", ".mcp.json");
2060
- if (existsSync2(claudeCodeGlobal)) {
2982
+ const claudeCodeGlobal = join3(homedir3(), ".claude", ".mcp.json");
2983
+ if (existsSync3(claudeCodeGlobal)) {
2061
2984
  paths.push({ path: claudeCodeGlobal, name: "Claude Code (global)" });
2062
2985
  }
2063
- const projectConfig = join2(process.cwd(), ".mcp.json");
2064
- if (existsSync2(projectConfig)) {
2986
+ const projectConfig = join3(process.cwd(), ".mcp.json");
2987
+ if (existsSync3(projectConfig)) {
2065
2988
  paths.push({ path: projectConfig, name: "Claude Code (project)" });
2066
2989
  }
2067
2990
  return paths;
@@ -2069,13 +2992,13 @@ function getConfigPaths() {
2069
2992
  function configureMcpClient(configPath) {
2070
2993
  try {
2071
2994
  let config = {};
2072
- if (existsSync2(configPath)) {
2073
- const raw = readFileSync2(configPath, "utf-8");
2995
+ if (existsSync3(configPath)) {
2996
+ const raw = readFileSync3(configPath, "utf-8");
2074
2997
  config = JSON.parse(raw);
2075
2998
  } else {
2076
- const dir = join2(configPath, "..");
2077
- if (!existsSync2(dir)) {
2078
- mkdirSync2(dir, { recursive: true });
2999
+ const dir = join3(configPath, "..");
3000
+ if (!existsSync3(dir)) {
3001
+ mkdirSync3(dir, { recursive: true });
2079
3002
  }
2080
3003
  }
2081
3004
  if (!config.mcpServers) {
@@ -2085,7 +3008,7 @@ function configureMcpClient(configPath) {
2085
3008
  command: "npx",
2086
3009
  args: ["-y", "@socialneuron/mcp-server"]
2087
3010
  };
2088
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
3011
+ writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n");
2089
3012
  return true;
2090
3013
  } catch {
2091
3014
  return false;
@@ -2399,18 +3322,19 @@ async function runLoginDevice() {
2399
3322
  console.error(" Error: Authorization timed out. Please try again.");
2400
3323
  process.exit(1);
2401
3324
  }
2402
- async function runLogoutCommand() {
2403
- console.error("");
2404
- console.error(" Social Neuron - Logout");
2405
- console.error(" ======================");
2406
- console.error("");
3325
+ async function runLogoutCommand(options) {
3326
+ const asJson = options?.json ?? false;
3327
+ if (!asJson) {
3328
+ console.error("");
3329
+ console.error(" Social Neuron - Logout");
3330
+ console.error(" ======================");
3331
+ console.error("");
3332
+ }
2407
3333
  const apiKey = await loadApiKey();
2408
3334
  if (apiKey) {
2409
3335
  try {
2410
- const supabaseUrl = getDefaultSupabaseUrl2();
2411
- const serviceKey = process.env.SOCIALNEURON_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
2412
3336
  const validation = await validateApiKey(apiKey);
2413
- if (validation.valid) {
3337
+ if (validation.valid && !asJson) {
2414
3338
  console.error(" Key removed from this device.");
2415
3339
  console.error(
2416
3340
  " Note: To revoke the key server-side, visit socialneuron.com/settings/developer"
@@ -2420,54 +3344,96 @@ async function runLogoutCommand() {
2420
3344
  }
2421
3345
  }
2422
3346
  await deleteApiKey();
2423
- console.error(" Credentials removed from keychain.");
2424
- console.error("");
3347
+ if (asJson) {
3348
+ process.stdout.write(
3349
+ JSON.stringify({ ok: true, message: "Credentials removed", schema_version: "1" }, null, 2) + "\n"
3350
+ );
3351
+ } else {
3352
+ console.error(" Credentials removed from keychain.");
3353
+ console.error("");
3354
+ }
2425
3355
  }
2426
- async function runWhoami() {
2427
- console.error("");
2428
- console.error(" Social Neuron - Current Identity");
2429
- console.error(" ================================");
2430
- console.error("");
3356
+ async function runWhoami(options) {
3357
+ const asJson = options?.json ?? false;
2431
3358
  const apiKey = await loadApiKey();
2432
3359
  if (!apiKey) {
2433
- console.error(" Not logged in.");
2434
- console.error(" Run: npx @socialneuron/mcp-server login");
2435
- console.error("");
3360
+ if (asJson) {
3361
+ process.stdout.write(
3362
+ JSON.stringify({ ok: false, error: "Not logged in", schema_version: "1" }, null, 2) + "\n"
3363
+ );
3364
+ } else {
3365
+ console.error("");
3366
+ console.error(" Not logged in.");
3367
+ console.error(" Run: npx @socialneuron/mcp-server login");
3368
+ console.error("");
3369
+ }
2436
3370
  process.exit(1);
2437
3371
  }
2438
- console.error(" Validating key...");
2439
- const result = await validateApiKey(apiKey);
2440
- if (!result.valid) {
2441
- console.error(" Key is invalid or expired.");
2442
- console.error(` Error: ${result.error || "Unknown"}`);
2443
- console.error(" Run: npx @socialneuron/mcp-server login");
3372
+ if (!asJson) {
2444
3373
  console.error("");
2445
- process.exit(1);
3374
+ console.error(" Social Neuron - Current Identity");
3375
+ console.error(" ================================");
3376
+ console.error("");
3377
+ console.error(" Validating key...");
2446
3378
  }
2447
- console.error("");
2448
- console.error(` Email: ${result.email || "(not available)"}`);
2449
- console.error(` User ID: ${result.userId}`);
2450
- console.error(` Key: ${apiKey.substring(0, 12)}...`);
2451
- console.error(` Scopes: ${result.scopes?.join(", ") || "mcp:full"}`);
2452
- if (result.expiresAt) {
2453
- const expiresMs = new Date(result.expiresAt).getTime();
2454
- const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
2455
- console.error(` Expires: ${result.expiresAt} (${daysLeft} days)`);
2456
- if (daysLeft <= 7) {
2457
- console.error("");
2458
- console.error(` Warning: Key expires in ${daysLeft} day(s).`);
3379
+ const result = await validateApiKey(apiKey);
3380
+ if (!result.valid) {
3381
+ if (asJson) {
3382
+ process.stdout.write(
3383
+ JSON.stringify(
3384
+ { ok: false, error: result.error || "Key invalid or expired", schema_version: "1" },
3385
+ null,
3386
+ 2
3387
+ ) + "\n"
3388
+ );
3389
+ } else {
3390
+ console.error(" Key is invalid or expired.");
3391
+ console.error(` Error: ${result.error || "Unknown"}`);
2459
3392
  console.error(" Run: npx @socialneuron/mcp-server login");
3393
+ console.error("");
2460
3394
  }
3395
+ process.exit(1);
3396
+ }
3397
+ if (asJson) {
3398
+ const payload = {
3399
+ ok: true,
3400
+ email: result.email || null,
3401
+ userId: result.userId,
3402
+ keyPrefix: apiKey.substring(0, 12) + "...",
3403
+ scopes: result.scopes || ["mcp:full"],
3404
+ schema_version: "1"
3405
+ };
3406
+ if (result.expiresAt) payload.expiresAt = result.expiresAt;
3407
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
2461
3408
  } else {
2462
- console.error(" Expires: never");
3409
+ console.error("");
3410
+ console.error(` Email: ${result.email || "(not available)"}`);
3411
+ console.error(` User ID: ${result.userId}`);
3412
+ console.error(` Key: ${apiKey.substring(0, 12)}...`);
3413
+ console.error(` Scopes: ${result.scopes?.join(", ") || "mcp:full"}`);
3414
+ if (result.expiresAt) {
3415
+ const expiresMs = new Date(result.expiresAt).getTime();
3416
+ const daysLeft = Math.ceil((expiresMs - Date.now()) / (1e3 * 60 * 60 * 24));
3417
+ console.error(` Expires: ${result.expiresAt} (${daysLeft} days)`);
3418
+ if (daysLeft <= 7) {
3419
+ console.error("");
3420
+ console.error(` Warning: Key expires in ${daysLeft} day(s).`);
3421
+ console.error(" Run: npx @socialneuron/mcp-server login");
3422
+ }
3423
+ } else {
3424
+ console.error(" Expires: never");
3425
+ }
3426
+ console.error("");
2463
3427
  }
2464
- console.error("");
2465
3428
  }
2466
- async function runHealthCheck() {
2467
- console.error("");
2468
- console.error(" Social Neuron \u2014 Health Check");
2469
- console.error(" ============================");
2470
- console.error("");
3429
+ async function runHealthCheck(options) {
3430
+ const asJson = options?.json ?? false;
3431
+ if (!asJson) {
3432
+ console.error("");
3433
+ console.error(" Social Neuron \u2014 Health Check");
3434
+ console.error(" ============================");
3435
+ console.error("");
3436
+ }
2471
3437
  const checks = [];
2472
3438
  const apiKey = await loadApiKey();
2473
3439
  if (!apiKey) {
@@ -2552,13 +3518,26 @@ async function runHealthCheck() {
2552
3518
  }
2553
3519
  }
2554
3520
  const allOk = checks.every((c) => c.ok);
2555
- for (const check of checks) {
2556
- const icon = check.ok ? "\u2713" : "\u2717";
2557
- console.error(` ${icon} ${check.name}: ${check.detail}`);
3521
+ if (asJson) {
3522
+ const checksObj = {};
3523
+ for (const check of checks) {
3524
+ checksObj[check.name.toLowerCase().replace(/\s+/g, "_")] = {
3525
+ status: check.ok ? "pass" : "fail",
3526
+ detail: check.detail
3527
+ };
3528
+ }
3529
+ process.stdout.write(
3530
+ JSON.stringify({ ok: allOk, checks: checksObj, schema_version: "1" }, null, 2) + "\n"
3531
+ );
3532
+ } else {
3533
+ for (const check of checks) {
3534
+ const icon = check.ok ? "\u2713" : "\u2717";
3535
+ console.error(` ${icon} ${check.name}: ${check.detail}`);
3536
+ }
3537
+ console.error("");
3538
+ console.error(` Overall: ${allOk ? "All checks passed" : "Some checks failed"}`);
3539
+ console.error("");
2558
3540
  }
2559
- console.error("");
2560
- console.error(` Overall: ${allOk ? "All checks passed" : "Some checks failed"}`);
2561
- console.error("");
2562
3541
  if (!allOk) {
2563
3542
  process.exit(1);
2564
3543
  }
@@ -2573,13 +3552,153 @@ var init_commands = __esm({
2573
3552
  }
2574
3553
  });
2575
3554
 
3555
+ // src/cli/repl.ts
3556
+ var repl_exports = {};
3557
+ __export(repl_exports, {
3558
+ runRepl: () => runRepl
3559
+ });
3560
+ import { createInterface as createInterface2 } from "node:readline";
3561
+ async function runRepl() {
3562
+ process.stderr.write(`
3563
+ Social Neuron CLI v${MCP_VERSION} \u2014 Interactive Mode
3564
+ `);
3565
+ process.stderr.write("Type a command, .help for help, or .exit to quit.\n\n");
3566
+ let authEmail = null;
3567
+ try {
3568
+ const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
3569
+ const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
3570
+ const key = await loadApiKey2();
3571
+ if (key) {
3572
+ const result = await validateApiKey2(key);
3573
+ if (result.valid) {
3574
+ authEmail = result.email || null;
3575
+ process.stderr.write(` Authenticated as: ${authEmail || "unknown"}
3576
+
3577
+ `);
3578
+ }
3579
+ }
3580
+ } catch {
3581
+ process.stderr.write(" Not authenticated (some commands will require login)\n\n");
3582
+ }
3583
+ const COMPLETIONS = [
3584
+ "publish",
3585
+ "quality-check",
3586
+ "e2e",
3587
+ "oauth-health",
3588
+ "oauth-refresh",
3589
+ "preflight",
3590
+ "posts",
3591
+ "refresh-analytics",
3592
+ "loop",
3593
+ "status",
3594
+ "autopilot",
3595
+ "usage",
3596
+ "credits",
3597
+ "tools",
3598
+ "info",
3599
+ "plan",
3600
+ "preset",
3601
+ "content",
3602
+ "account",
3603
+ "analytics",
3604
+ "system",
3605
+ ".help",
3606
+ ".exit",
3607
+ ".clear"
3608
+ ];
3609
+ const promptStr = authEmail ? `sn[${authEmail.split("@")[0]}]> ` : "sn> ";
3610
+ const rl = createInterface2({
3611
+ input: process.stdin,
3612
+ output: process.stderr,
3613
+ prompt: promptStr,
3614
+ completer: (line) => {
3615
+ const hits = COMPLETIONS.filter((c) => c.startsWith(line.trim()));
3616
+ return [hits.length ? hits : COMPLETIONS, line];
3617
+ }
3618
+ });
3619
+ rl.prompt();
3620
+ rl.on("line", async (line) => {
3621
+ const trimmed = line.trim();
3622
+ if (!trimmed) {
3623
+ rl.prompt();
3624
+ return;
3625
+ }
3626
+ if (trimmed === ".exit" || trimmed === "exit" || trimmed === "quit") {
3627
+ process.stderr.write("Goodbye.\n");
3628
+ rl.close();
3629
+ process.exit(0);
3630
+ }
3631
+ if (trimmed === ".help") {
3632
+ process.stderr.write("\nREPL Commands:\n");
3633
+ process.stderr.write(" .help Show this help\n");
3634
+ process.stderr.write(" .clear Clear the screen\n");
3635
+ process.stderr.write(" .exit Exit the REPL\n");
3636
+ process.stderr.write("\nCLI Commands:\n");
3637
+ process.stderr.write(" publish, quality-check, e2e, posts, credits, etc.\n");
3638
+ process.stderr.write(' Type any sn subcommand directly (no "sn" prefix needed)\n\n');
3639
+ rl.prompt();
3640
+ return;
3641
+ }
3642
+ if (trimmed === ".clear") {
3643
+ process.stderr.write("\x1B[2J\x1B[H");
3644
+ rl.prompt();
3645
+ return;
3646
+ }
3647
+ const originalExit = process.exit;
3648
+ process.exit = ((_code) => {
3649
+ });
3650
+ try {
3651
+ const { runSnCli: runSnCli2 } = await Promise.resolve().then(() => (init_sn(), sn_exports));
3652
+ const argv = splitArgs(trimmed);
3653
+ await runSnCli2(argv);
3654
+ } catch (err) {
3655
+ const message = err instanceof Error ? err.message : String(err);
3656
+ process.stderr.write(`Error: ${message}
3657
+ `);
3658
+ } finally {
3659
+ process.exit = originalExit;
3660
+ }
3661
+ rl.prompt();
3662
+ });
3663
+ rl.on("close", () => {
3664
+ process.exit(0);
3665
+ });
3666
+ await new Promise(() => {
3667
+ });
3668
+ }
3669
+ function splitArgs(line) {
3670
+ const args = [];
3671
+ let current = "";
3672
+ let inQuotes = false;
3673
+ for (const char of line) {
3674
+ if (char === '"') {
3675
+ inQuotes = !inQuotes;
3676
+ continue;
3677
+ }
3678
+ if (char === " " && !inQuotes) {
3679
+ if (current) {
3680
+ args.push(current);
3681
+ current = "";
3682
+ }
3683
+ continue;
3684
+ }
3685
+ current += char;
3686
+ }
3687
+ if (current) args.push(current);
3688
+ return args;
3689
+ }
3690
+ var init_repl = __esm({
3691
+ "src/cli/repl.ts"() {
3692
+ "use strict";
3693
+ init_version();
3694
+ }
3695
+ });
3696
+
2576
3697
  // src/index.ts
3698
+ init_version();
2577
3699
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2578
3700
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2579
3701
 
2580
- // src/lib/version.ts
2581
- var MCP_VERSION = "1.2.1";
2582
-
2583
3702
  // src/auth/scopes.ts
2584
3703
  var SCOPE_HIERARCHY = {
2585
3704
  "mcp:full": [
@@ -2658,7 +3777,8 @@ var TOOL_SCOPES = {
2658
3777
  schedule_content_plan: "mcp:distribute",
2659
3778
  // mcp:read (usage is read-only)
2660
3779
  get_mcp_usage: "mcp:read",
2661
- list_plan_approvals: "mcp:read"
3780
+ list_plan_approvals: "mcp:read",
3781
+ search_tools: "mcp:read"
2662
3782
  };
2663
3783
  function hasScope(userScopes, required) {
2664
3784
  if (userScopes.includes(required)) return true;
@@ -3193,11 +4313,11 @@ function asEnvelope(data) {
3193
4313
  };
3194
4314
  }
3195
4315
  var VIDEO_CREDIT_ESTIMATES = {
3196
- "veo3-fast": 156,
3197
- "veo3-quality": 780,
3198
- "runway-aleph": 260,
3199
- sora2: 390,
3200
- "sora2-pro": 1170,
4316
+ "veo3-fast": 200,
4317
+ "veo3-quality": 1e3,
4318
+ "runway-aleph": 340,
4319
+ sora2: 500,
4320
+ "sora2-pro": 1500,
3201
4321
  kling: 170,
3202
4322
  "kling-3": 100,
3203
4323
  "kling-3-pro": 135
@@ -8000,6 +9120,7 @@ Next Action: ${payload.recommendedNextAction}`
8000
9120
  // src/tools/usage.ts
8001
9121
  init_supabase();
8002
9122
  import { z as z14 } from "zod";
9123
+ init_version();
8003
9124
  function asEnvelope10(data) {
8004
9125
  return {
8005
9126
  _meta: {
@@ -8088,6 +9209,7 @@ ${"=".repeat(40)}
8088
9209
  // src/tools/autopilot.ts
8089
9210
  init_supabase();
8090
9211
  import { z as z15 } from "zod";
9212
+ init_version();
8091
9213
  function asEnvelope11(data) {
8092
9214
  return {
8093
9215
  _meta: {
@@ -9669,6 +10791,59 @@ function registerPlanApprovalTools(server2) {
9669
10791
  );
9670
10792
  }
9671
10793
 
10794
+ // src/tools/discovery.ts
10795
+ init_tool_catalog();
10796
+ import { z as z20 } from "zod";
10797
+ function registerDiscoveryTools(server2) {
10798
+ server2.tool(
10799
+ "search_tools",
10800
+ 'Search and discover available MCP tools. Use detail level to control token usage: "name" (~50 tokens), "summary" (~500 tokens), "full" (complete schemas).',
10801
+ {
10802
+ query: z20.string().optional().describe("Search query to filter tools by name or description"),
10803
+ module: z20.string().optional().describe('Filter by module name (e.g. "planning", "content", "analytics")'),
10804
+ scope: z20.string().optional().describe('Filter by required scope (e.g. "mcp:read", "mcp:write")'),
10805
+ detail: z20.enum(["name", "summary", "full"]).default("summary").describe(
10806
+ 'Detail level: "name" for just tool names, "summary" for names + descriptions, "full" for complete info including scope and module'
10807
+ )
10808
+ },
10809
+ async ({ query, module, scope, detail }) => {
10810
+ let results = [...TOOL_CATALOG];
10811
+ if (query) {
10812
+ results = searchTools(query);
10813
+ }
10814
+ if (module) {
10815
+ const moduleTools = getToolsByModule(module);
10816
+ results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
10817
+ }
10818
+ if (scope) {
10819
+ const scopeTools = getToolsByScope(scope);
10820
+ results = results.filter((t) => scopeTools.some((st) => st.name === t.name));
10821
+ }
10822
+ let output;
10823
+ switch (detail) {
10824
+ case "name":
10825
+ output = results.map((t) => t.name);
10826
+ break;
10827
+ case "summary":
10828
+ output = results.map((t) => ({ name: t.name, description: t.description }));
10829
+ break;
10830
+ case "full":
10831
+ default:
10832
+ output = results;
10833
+ break;
10834
+ }
10835
+ return {
10836
+ content: [
10837
+ {
10838
+ type: "text",
10839
+ text: JSON.stringify({ toolCount: results.length, tools: output }, null, 2)
10840
+ }
10841
+ ]
10842
+ };
10843
+ }
10844
+ );
10845
+ }
10846
+
9672
10847
  // src/lib/register-tools.ts
9673
10848
  function applyScopeEnforcement(server2, scopeResolver) {
9674
10849
  const originalTool = server2.tool.bind(server2);
@@ -9732,6 +10907,7 @@ function registerAllTools(server2, options) {
9732
10907
  registerQualityTools(server2);
9733
10908
  registerPlanningTools(server2);
9734
10909
  registerPlanApprovalTools(server2);
10910
+ registerDiscoveryTools(server2);
9735
10911
  }
9736
10912
 
9737
10913
  // src/index.ts
@@ -9751,21 +10927,59 @@ process.on("unhandledRejection", (reason) => {
9751
10927
  });
9752
10928
  var command = process.argv[2];
9753
10929
  if (command === "--version" || command === "-v") {
9754
- const { readFileSync: readFileSync3 } = await import("node:fs");
10930
+ const { readFileSync: readFileSync4 } = await import("node:fs");
9755
10931
  const { resolve: resolve3, dirname } = await import("node:path");
9756
10932
  const { fileURLToPath } = await import("node:url");
10933
+ let version = MCP_VERSION;
9757
10934
  try {
9758
10935
  const pkgPath = resolve3(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
9759
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
9760
- console.log(`@socialneuron/mcp-server v${pkg.version}`);
10936
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
10937
+ version = pkg.version;
9761
10938
  } catch {
9762
- console.log(`@socialneuron/mcp-server v${MCP_VERSION}`);
10939
+ }
10940
+ if (process.argv.includes("--json")) {
10941
+ process.stdout.write(
10942
+ JSON.stringify(
10943
+ {
10944
+ ok: true,
10945
+ command: "version",
10946
+ version,
10947
+ name: "@socialneuron/mcp-server",
10948
+ schema_version: "1"
10949
+ },
10950
+ null,
10951
+ 2
10952
+ ) + "\n"
10953
+ );
10954
+ } else {
10955
+ console.log(`@socialneuron/mcp-server v${version}`);
9763
10956
  }
9764
10957
  process.exit(0);
9765
10958
  }
9766
10959
  if (command === "--help" || command === "-h") {
9767
- console.log(
9768
- `
10960
+ if (process.argv.includes("--json")) {
10961
+ process.stdout.write(
10962
+ JSON.stringify(
10963
+ {
10964
+ ok: true,
10965
+ command: "help",
10966
+ commands: [
10967
+ { name: "setup", aliases: ["login"], description: "Interactive OAuth setup" },
10968
+ { name: "logout", description: "Remove credentials" },
10969
+ { name: "whoami", description: "Show auth info" },
10970
+ { name: "health", description: "Check connectivity" },
10971
+ { name: "sn", description: "CLI tools (publish, preflight, etc.)" },
10972
+ { name: "repl", description: "Interactive REPL mode" }
10973
+ ],
10974
+ schema_version: "1"
10975
+ },
10976
+ null,
10977
+ 2
10978
+ ) + "\n"
10979
+ );
10980
+ } else {
10981
+ console.log(
10982
+ `
9769
10983
  @socialneuron/mcp-server \u2014 AI content creation tools for Claude
9770
10984
 
9771
10985
  Usage:
@@ -9778,6 +10992,7 @@ Usage:
9778
10992
  socialneuron-mcp whoami Show authenticated user info
9779
10993
  socialneuron-mcp health Check connectivity, key validity, credits
9780
10994
  socialneuron-mcp sn <command> CLI tools (publish, preflight, e2e, etc.)
10995
+ socialneuron-mcp repl Interactive REPL mode
9781
10996
  socialneuron-mcp --version Show version
9782
10997
  socialneuron-mcp --help Show this help
9783
10998
 
@@ -9788,7 +11003,8 @@ Environment:
9788
11003
 
9789
11004
  Docs: https://github.com/socialneuron/mcp-server#readme
9790
11005
  `.trim()
9791
- );
11006
+ );
11007
+ }
9792
11008
  process.exit(0);
9793
11009
  }
9794
11010
  if (command === "setup" || command === "login") {
@@ -9807,19 +11023,26 @@ if (command === "setup" || command === "login") {
9807
11023
  }
9808
11024
  if (command === "logout") {
9809
11025
  const { runLogoutCommand: runLogoutCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
9810
- await runLogoutCommand2();
11026
+ const jsonFlag = process.argv.slice(3).includes("--json");
11027
+ await runLogoutCommand2({ json: jsonFlag });
9811
11028
  process.exit(0);
9812
11029
  }
9813
11030
  if (command === "whoami") {
9814
11031
  const { runWhoami: runWhoami2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
9815
- await runWhoami2();
11032
+ const jsonFlag = process.argv.slice(3).includes("--json");
11033
+ await runWhoami2({ json: jsonFlag });
9816
11034
  process.exit(0);
9817
11035
  }
9818
11036
  if (command === "health") {
9819
11037
  const { runHealthCheck: runHealthCheck2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
9820
- await runHealthCheck2();
11038
+ const jsonFlag = process.argv.slice(3).includes("--json");
11039
+ await runHealthCheck2({ json: jsonFlag });
9821
11040
  process.exit(0);
9822
11041
  }
11042
+ if (command === "repl") {
11043
+ const { runRepl: runRepl2 } = await Promise.resolve().then(() => (init_repl(), repl_exports));
11044
+ await runRepl2();
11045
+ }
9823
11046
  if (command === "sn") {
9824
11047
  const snSubcommand = process.argv[3];
9825
11048
  if (!snSubcommand || snSubcommand === "--help" || snSubcommand === "-h") {
@@ -9827,11 +11050,10 @@ if (command === "sn") {
9827
11050
  printSnUsage2();
9828
11051
  process.exit(snSubcommand ? 0 : 1);
9829
11052
  }
9830
- await initializeAuth();
9831
11053
  await runSnCli(process.argv.slice(3));
9832
11054
  process.exit(0);
9833
11055
  }
9834
- if (command && !["setup", "login", "logout", "whoami", "health", "sn"].includes(command)) {
11056
+ if (command && !["setup", "login", "logout", "whoami", "health", "sn", "repl"].includes(command)) {
9835
11057
  process.stderr.write(`Unknown command: ${command}
9836
11058
  Run socialneuron-mcp --help for usage.
9837
11059
  `);