@socialneuron/mcp-server 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/http.js +544 -123
  2. package/dist/index.js +549 -78
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var MCP_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- MCP_VERSION = "1.7.0";
17
+ MCP_VERSION = "1.7.2";
18
18
  }
19
19
  });
20
20
 
@@ -5135,7 +5135,24 @@ function registerContentTools(server2) {
5135
5135
  `Status: ${job.status}`
5136
5136
  ];
5137
5137
  if (job.result_url) {
5138
- lines.push(`Result URL: ${job.result_url}`);
5138
+ const isR2Key = !job.result_url.startsWith("http");
5139
+ if (isR2Key) {
5140
+ const segments = job.result_url.split("/");
5141
+ const filename = segments[segments.length - 1] || "media";
5142
+ lines.push(`Media ready: ${filename}`);
5143
+ lines.push(
5144
+ "(Pass job_id directly to schedule_post, or use get_media_url with job_id for a download link)"
5145
+ );
5146
+ } else {
5147
+ lines.push(`Result URL: ${job.result_url}`);
5148
+ }
5149
+ }
5150
+ const allUrls = job.result_metadata?.all_urls;
5151
+ if (allUrls && allUrls.length > 1) {
5152
+ lines.push(`Media files: ${allUrls.length} outputs available`);
5153
+ lines.push(
5154
+ "(Use job_id with schedule_post for carousel, or response_format=json for programmatic access)"
5155
+ );
5139
5156
  }
5140
5157
  if (job.error_message) {
5141
5158
  lines.push(`Error: ${job.error_message}`);
@@ -5152,8 +5169,13 @@ function registerContentTools(server2) {
5152
5169
  details: { status: job.status, jobId: job.id }
5153
5170
  });
5154
5171
  if (format === "json") {
5172
+ const enriched = {
5173
+ ...job,
5174
+ r2_key: job.result_url && !job.result_url.startsWith("http") ? job.result_url : null,
5175
+ all_urls: allUrls ?? null
5176
+ };
5155
5177
  return {
5156
- content: [{ type: "text", text: JSON.stringify(asEnvelope(job), null, 2) }]
5178
+ content: [{ type: "text", text: JSON.stringify(asEnvelope(enriched), null, 2) }]
5157
5179
  };
5158
5180
  }
5159
5181
  return {
@@ -5636,9 +5658,75 @@ Return ONLY valid JSON in this exact format:
5636
5658
  init_edge_function();
5637
5659
  import { z as z3 } from "zod";
5638
5660
  import { createHash as createHash2 } from "node:crypto";
5661
+
5662
+ // src/lib/sanitize-error.ts
5663
+ var ERROR_PATTERNS = [
5664
+ // Postgres / PostgREST
5665
+ [/PGRST301|permission denied/i, "Access denied. Check your account permissions."],
5666
+ [/42P01|does not exist/i, "Service temporarily unavailable. Please try again."],
5667
+ [/23505|unique.*constraint|duplicate key/i, "A duplicate record already exists."],
5668
+ [/23503|foreign key/i, "Referenced record not found."],
5669
+ // Gemini / Google AI
5670
+ [/google.*api.*key|googleapis\.com.*40[13]/i, "Content generation failed. Please try again."],
5671
+ [
5672
+ /RESOURCE_EXHAUSTED|quota.*exceeded|429.*google/i,
5673
+ "AI service rate limit reached. Please wait and retry."
5674
+ ],
5675
+ [
5676
+ /SAFETY|prompt.*blocked|content.*filter/i,
5677
+ "Content was blocked by the AI safety filter. Try rephrasing."
5678
+ ],
5679
+ [/gemini.*error|generativelanguage/i, "Content generation failed. Please try again."],
5680
+ // Kie.ai
5681
+ [/kie\.ai|kieai|kie_api/i, "Media generation failed. Please try again."],
5682
+ // Stripe
5683
+ [/stripe.*api|sk_live_|sk_test_/i, "Payment processing error. Please try again."],
5684
+ // Network / fetch
5685
+ [
5686
+ /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET/i,
5687
+ "External service unavailable. Please try again."
5688
+ ],
5689
+ [/fetch failed|network error|abort.*timeout/i, "Network request failed. Please try again."],
5690
+ [/CERT_|certificate|SSL|TLS/i, "Secure connection failed. Please try again."],
5691
+ // Supabase Edge Function internals
5692
+ [/FunctionsHttpError|non-2xx status/i, "Backend service error. Please try again."],
5693
+ [/JWT|token.*expired|token.*invalid/i, "Authentication expired. Please re-authenticate."],
5694
+ // Generic sensitive patterns (API keys, URLs with secrets)
5695
+ [/[a-z0-9]{32,}.*key|Bearer [a-zA-Z0-9._-]+/i, "An internal error occurred. Please try again."]
5696
+ ];
5697
+ function sanitizeError2(error) {
5698
+ const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
5699
+ if (process.env.NODE_ENV !== "production") {
5700
+ console.error("[Error]", msg);
5701
+ }
5702
+ for (const [pattern, userMessage] of ERROR_PATTERNS) {
5703
+ if (pattern.test(msg)) {
5704
+ return userMessage;
5705
+ }
5706
+ }
5707
+ return "An unexpected error occurred. Please try again.";
5708
+ }
5709
+
5710
+ // src/tools/distribution.ts
5639
5711
  init_supabase();
5640
5712
  init_quality();
5641
5713
  init_version();
5714
+ function snakeToCamel(obj) {
5715
+ const result = {};
5716
+ for (const [key, value] of Object.entries(obj)) {
5717
+ const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
5718
+ result[camelKey] = value;
5719
+ }
5720
+ return result;
5721
+ }
5722
+ function convertPlatformMetadata(meta) {
5723
+ if (!meta) return void 0;
5724
+ const converted = {};
5725
+ for (const [platform3, fields] of Object.entries(meta)) {
5726
+ converted[platform3] = snakeToCamel(fields);
5727
+ }
5728
+ return converted;
5729
+ }
5642
5730
  var PLATFORM_CASE_MAP = {
5643
5731
  youtube: "YouTube",
5644
5732
  tiktok: "TikTok",
@@ -5681,8 +5769,53 @@ function registerDistributionTools(server2) {
5681
5769
  job_ids: z3.array(z3.string()).optional().describe(
5682
5770
  "Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
5683
5771
  ),
5684
- platform_metadata: z3.record(z3.string(), z3.record(z3.string(), z3.unknown())).optional().describe(
5685
- 'Platform-specific metadata. Keys: tiktok (privacy_status), youtube (title, description, privacy), facebook (page_id), threads, bluesky. Example: {"tiktok":{"privacy_status":"PUBLIC_TO_ALL"}}'
5772
+ platform_metadata: z3.object({
5773
+ tiktok: z3.object({
5774
+ privacy_status: z3.enum([
5775
+ "PUBLIC_TO_EVERYONE",
5776
+ "MUTUAL_FOLLOW_FRIENDS",
5777
+ "FOLLOWER_OF_CREATOR",
5778
+ "SELF_ONLY"
5779
+ ]).optional().describe("Required unless useInbox=true. Who can view the video."),
5780
+ enable_duet: z3.boolean().optional(),
5781
+ enable_comment: z3.boolean().optional(),
5782
+ enable_stitch: z3.boolean().optional(),
5783
+ is_ai_generated: z3.boolean().optional(),
5784
+ brand_content: z3.boolean().optional(),
5785
+ brand_organic: z3.boolean().optional(),
5786
+ use_inbox: z3.boolean().optional().describe("Post to TikTok inbox/draft instead of direct publish.")
5787
+ }).optional(),
5788
+ youtube: z3.object({
5789
+ title: z3.string().optional().describe("Video title (required for YouTube)."),
5790
+ description: z3.string().optional(),
5791
+ privacy_status: z3.enum(["public", "unlisted", "private"]).optional(),
5792
+ category_id: z3.string().optional(),
5793
+ tags: z3.array(z3.string()).optional(),
5794
+ made_for_kids: z3.boolean().optional(),
5795
+ notify_subscribers: z3.boolean().optional()
5796
+ }).optional(),
5797
+ facebook: z3.object({
5798
+ page_id: z3.string().optional().describe("Facebook Page ID to post to."),
5799
+ audience: z3.string().optional()
5800
+ }).optional(),
5801
+ instagram: z3.object({
5802
+ location: z3.string().optional(),
5803
+ collaborators: z3.array(z3.string()).optional(),
5804
+ cover_timestamp: z3.number().optional(),
5805
+ share_to_feed: z3.boolean().optional(),
5806
+ first_comment: z3.string().optional(),
5807
+ is_ai_generated: z3.boolean().optional()
5808
+ }).optional(),
5809
+ threads: z3.object({}).passthrough().optional(),
5810
+ bluesky: z3.object({
5811
+ content_labels: z3.array(z3.string()).optional()
5812
+ }).optional(),
5813
+ linkedin: z3.object({
5814
+ article_url: z3.string().optional()
5815
+ }).optional(),
5816
+ twitter: z3.object({}).passthrough().optional()
5817
+ }).optional().describe(
5818
+ 'Platform-specific metadata. Example: {"tiktok":{"privacy_status":"PUBLIC_TO_EVERYONE"}, "youtube":{"title":"My Video"}}'
5686
5819
  ),
5687
5820
  media_type: z3.enum(["IMAGE", "VIDEO", "CAROUSEL_ALBUM"]).optional().describe(
5688
5821
  "Media type. Set to CAROUSEL_ALBUM with media_urls for Instagram carousels. Default: auto-detected from media_url."
@@ -5780,7 +5913,12 @@ function registerDistributionTools(server2) {
5780
5913
  const signed = await signR2Key(r2_key);
5781
5914
  if (!signed) {
5782
5915
  return {
5783
- content: [{ type: "text", text: `Failed to sign R2 key: ${r2_key}` }],
5916
+ content: [
5917
+ {
5918
+ type: "text",
5919
+ text: `Failed to sign media key. Verify the key exists and you have access.`
5920
+ }
5921
+ ],
5784
5922
  isError: true
5785
5923
  };
5786
5924
  }
@@ -5808,7 +5946,7 @@ function registerDistributionTools(server2) {
5808
5946
  content: [
5809
5947
  {
5810
5948
  type: "text",
5811
- text: `Failed to sign R2 key at index ${failIdx}: ${r2_keys[failIdx]}`
5949
+ text: `Failed to sign media key at index ${failIdx}. Verify the key exists and you have access.`
5812
5950
  }
5813
5951
  ],
5814
5952
  isError: true
@@ -5836,7 +5974,7 @@ function registerDistributionTools(server2) {
5836
5974
  content: [
5837
5975
  {
5838
5976
  type: "text",
5839
- text: `Failed to resolve media: ${resolveErr instanceof Error ? resolveErr.message : String(resolveErr)}`
5977
+ text: `Failed to resolve media: ${sanitizeError2(resolveErr)}`
5840
5978
  }
5841
5979
  ],
5842
5980
  isError: true
@@ -5861,7 +5999,11 @@ Created with Social Neuron`;
5861
5999
  hashtags,
5862
6000
  scheduledAt: schedule_at,
5863
6001
  projectId: project_id,
5864
- ...platform_metadata ? { platformMetadata: platform_metadata } : {}
6002
+ ...platform_metadata ? {
6003
+ platformMetadata: convertPlatformMetadata(
6004
+ platform_metadata
6005
+ )
6006
+ } : {}
5865
6007
  },
5866
6008
  { timeoutMs: 3e4 }
5867
6009
  );
@@ -6197,7 +6339,7 @@ Created with Social Neuron`;
6197
6339
  return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
6198
6340
  } catch (err) {
6199
6341
  const durationMs = Date.now() - startedAt;
6200
- const message = err instanceof Error ? err.message : String(err);
6342
+ const message = sanitizeError2(err);
6201
6343
  logMcpToolInvocation({
6202
6344
  toolName: "find_next_slots",
6203
6345
  status: "error",
@@ -6717,7 +6859,7 @@ Created with Social Neuron`;
6717
6859
  };
6718
6860
  } catch (err) {
6719
6861
  const durationMs = Date.now() - startedAt;
6720
- const message = err instanceof Error ? err.message : String(err);
6862
+ const message = sanitizeError2(err);
6721
6863
  logMcpToolInvocation({
6722
6864
  toolName: "schedule_content_plan",
6723
6865
  status: "error",
@@ -6740,6 +6882,10 @@ import { readFile } from "node:fs/promises";
6740
6882
  import { basename, extname } from "node:path";
6741
6883
  init_supabase();
6742
6884
  var MAX_BASE64_SIZE = 10 * 1024 * 1024;
6885
+ function maskR2Key(key) {
6886
+ const segments = key.split("/");
6887
+ return segments.length >= 3 ? `\u2026/${segments.slice(-2).join("/")}` : key;
6888
+ }
6743
6889
  function inferContentType(filePath) {
6744
6890
  const ext = extname(filePath).toLowerCase();
6745
6891
  const map = {
@@ -6824,18 +6970,111 @@ function registerMediaTools(server2) {
6824
6970
  isError: true
6825
6971
  };
6826
6972
  }
6973
+ const ct = content_type || inferContentType(source);
6827
6974
  if (fileBuffer.length > MAX_BASE64_SIZE) {
6975
+ const { data: putData, error: putError } = await callEdgeFunction(
6976
+ "get-signed-url",
6977
+ {
6978
+ operation: "put",
6979
+ contentType: ct,
6980
+ filename: basename(source),
6981
+ projectId: project_id
6982
+ },
6983
+ { timeoutMs: 1e4 }
6984
+ );
6985
+ if (putError || !putData?.signedUrl) {
6986
+ return {
6987
+ content: [
6988
+ {
6989
+ type: "text",
6990
+ text: `Failed to get presigned upload URL: ${putError || "No URL returned"}`
6991
+ }
6992
+ ],
6993
+ isError: true
6994
+ };
6995
+ }
6996
+ try {
6997
+ const putResp = await fetch(putData.signedUrl, {
6998
+ method: "PUT",
6999
+ headers: { "Content-Type": ct },
7000
+ body: fileBuffer
7001
+ });
7002
+ if (!putResp.ok) {
7003
+ return {
7004
+ content: [
7005
+ {
7006
+ type: "text",
7007
+ text: `R2 upload failed (HTTP ${putResp.status}): ${await putResp.text().catch(() => "Unknown error")}`
7008
+ }
7009
+ ],
7010
+ isError: true
7011
+ };
7012
+ }
7013
+ } catch (uploadErr) {
7014
+ return {
7015
+ content: [
7016
+ {
7017
+ type: "text",
7018
+ text: `R2 upload failed: ${sanitizeError(uploadErr)}`
7019
+ }
7020
+ ],
7021
+ isError: true
7022
+ };
7023
+ }
7024
+ const { data: signData } = await callEdgeFunction(
7025
+ "get-signed-url",
7026
+ { key: putData.key, operation: "get" },
7027
+ { timeoutMs: 1e4 }
7028
+ );
7029
+ await logMcpToolInvocation({
7030
+ toolName: "upload_media",
7031
+ status: "success",
7032
+ durationMs: Date.now() - startedAt,
7033
+ details: {
7034
+ source: "local-presigned-put",
7035
+ r2Key: putData.key,
7036
+ size: fileBuffer.length,
7037
+ contentType: ct
7038
+ }
7039
+ });
7040
+ if (format === "json") {
7041
+ return {
7042
+ content: [
7043
+ {
7044
+ type: "text",
7045
+ text: JSON.stringify(
7046
+ {
7047
+ r2_key: putData.key,
7048
+ signed_url: signData?.signedUrl ?? null,
7049
+ size: fileBuffer.length,
7050
+ content_type: ct
7051
+ },
7052
+ null,
7053
+ 2
7054
+ )
7055
+ }
7056
+ ],
7057
+ isError: false
7058
+ };
7059
+ }
6828
7060
  return {
6829
7061
  content: [
6830
7062
  {
6831
7063
  type: "text",
6832
- text: `File too large for base64 upload (${(fileBuffer.length / 1024 / 1024).toFixed(1)}MB, max ${MAX_BASE64_SIZE / 1024 / 1024}MB). For large videos, host the file at a public URL first and pass the URL as source.`
7064
+ text: [
7065
+ "Media uploaded successfully (presigned PUT).",
7066
+ `Media key: ${maskR2Key(putData.key)}`,
7067
+ signData?.signedUrl ? `Signed URL: ${signData.signedUrl}` : "",
7068
+ `Size: ${(fileBuffer.length / 1024 / 1024).toFixed(1)}MB`,
7069
+ `Type: ${ct}`,
7070
+ "",
7071
+ "Use job_id or response_format=json with schedule_post to post to any platform."
7072
+ ].filter(Boolean).join("\n")
6833
7073
  }
6834
7074
  ],
6835
- isError: true
7075
+ isError: false
6836
7076
  };
6837
7077
  }
6838
- const ct = content_type || inferContentType(source);
6839
7078
  const base64 = `data:${ct};base64,${fileBuffer.toString("base64")}`;
6840
7079
  uploadBody = {
6841
7080
  fileData: base64,
@@ -6900,12 +7139,12 @@ function registerMediaTools(server2) {
6900
7139
  type: "text",
6901
7140
  text: [
6902
7141
  "Media uploaded successfully.",
6903
- `R2 Key: ${data.key}`,
7142
+ `Media key: ${maskR2Key(data.key)}`,
6904
7143
  `Signed URL: ${data.url}`,
6905
7144
  `Size: ${(data.size / 1024).toFixed(0)}KB`,
6906
7145
  `Type: ${data.contentType}`,
6907
7146
  "",
6908
- "Use this r2_key with schedule_post to post to any platform."
7147
+ "Use job_id or response_format=json with schedule_post to post to any platform."
6909
7148
  ].join("\n")
6910
7149
  }
6911
7150
  ],
@@ -6969,7 +7208,7 @@ function registerMediaTools(server2) {
6969
7208
  type: "text",
6970
7209
  text: [
6971
7210
  `Signed URL: ${data.signedUrl}`,
6972
- `R2 Key: ${r2_key}`,
7211
+ `Media key: ${maskR2Key(r2_key)}`,
6973
7212
  `Expires in: ${data.expiresIn ?? 3600}s`
6974
7213
  ].join("\n")
6975
7214
  }
@@ -7904,7 +8143,7 @@ function registerScreenshotTools(server2) {
7904
8143
  };
7905
8144
  } catch (err) {
7906
8145
  await closeBrowser();
7907
- const message = err instanceof Error ? err.message : String(err);
8146
+ const message = sanitizeError2(err);
7908
8147
  await logMcpToolInvocation({
7909
8148
  toolName: "capture_app_page",
7910
8149
  status: "error",
@@ -8060,7 +8299,7 @@ function registerScreenshotTools(server2) {
8060
8299
  };
8061
8300
  } catch (err) {
8062
8301
  await closeBrowser();
8063
- const message = err instanceof Error ? err.message : String(err);
8302
+ const message = sanitizeError2(err);
8064
8303
  await logMcpToolInvocation({
8065
8304
  toolName: "capture_screenshot",
8066
8305
  status: "error",
@@ -8354,7 +8593,7 @@ function registerRemotionTools(server2) {
8354
8593
  ]
8355
8594
  };
8356
8595
  } catch (err) {
8357
- const message = err instanceof Error ? err.message : String(err);
8596
+ const message = sanitizeError2(err);
8358
8597
  await logMcpToolInvocation({
8359
8598
  toolName: "render_demo_video",
8360
8599
  status: "error",
@@ -8482,7 +8721,7 @@ function registerRemotionTools(server2) {
8482
8721
  ]
8483
8722
  };
8484
8723
  } catch (err) {
8485
- const message = err instanceof Error ? err.message : String(err);
8724
+ const message = sanitizeError2(err);
8486
8725
  await logMcpToolInvocation({
8487
8726
  toolName: "render_template_video",
8488
8727
  status: "error",
@@ -9944,8 +10183,8 @@ Active: ${is_active}`
9944
10183
 
9945
10184
  // src/tools/extraction.ts
9946
10185
  init_edge_function();
9947
- init_supabase();
9948
10186
  import { z as z17 } from "zod";
10187
+ init_supabase();
9949
10188
  init_version();
9950
10189
  function asEnvelope13(data) {
9951
10190
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
@@ -10151,7 +10390,7 @@ function registerExtractionTools(server2) {
10151
10390
  };
10152
10391
  } catch (err) {
10153
10392
  const durationMs = Date.now() - startedAt;
10154
- const message = err instanceof Error ? err.message : String(err);
10393
+ const message = sanitizeError2(err);
10155
10394
  logMcpToolInvocation({
10156
10395
  toolName: "extract_url_content",
10157
10396
  status: "error",
@@ -10341,10 +10580,10 @@ function registerQualityTools(server2) {
10341
10580
 
10342
10581
  // src/tools/planning.ts
10343
10582
  init_edge_function();
10344
- init_supabase();
10345
- init_version();
10346
10583
  import { z as z19 } from "zod";
10347
10584
  import { randomUUID as randomUUID2 } from "node:crypto";
10585
+ init_supabase();
10586
+ init_version();
10348
10587
 
10349
10588
  // src/lib/parse-utils.ts
10350
10589
  function extractJsonArray(text) {
@@ -10711,7 +10950,7 @@ ${rawText.slice(0, 1e3)}`
10711
10950
  }
10712
10951
  } catch (persistErr) {
10713
10952
  const durationMs2 = Date.now() - startedAt;
10714
- const message = persistErr instanceof Error ? persistErr.message : String(persistErr);
10953
+ const message = sanitizeError2(persistErr);
10715
10954
  logMcpToolInvocation({
10716
10955
  toolName: "plan_content_week",
10717
10956
  status: "error",
@@ -10743,7 +10982,7 @@ ${rawText.slice(0, 1e3)}`
10743
10982
  };
10744
10983
  } catch (err) {
10745
10984
  const durationMs = Date.now() - startedAt;
10746
- const message = err instanceof Error ? err.message : String(err);
10985
+ const message = sanitizeError2(err);
10747
10986
  logMcpToolInvocation({
10748
10987
  toolName: "plan_content_week",
10749
10988
  status: "error",
@@ -10835,7 +11074,7 @@ ${rawText.slice(0, 1e3)}`
10835
11074
  };
10836
11075
  } catch (err) {
10837
11076
  const durationMs = Date.now() - startedAt;
10838
- const message = err instanceof Error ? err.message : String(err);
11077
+ const message = sanitizeError2(err);
10839
11078
  logMcpToolInvocation({
10840
11079
  toolName: "save_content_plan",
10841
11080
  status: "error",
@@ -11313,11 +11552,11 @@ function registerDiscoveryTools(server2) {
11313
11552
 
11314
11553
  // src/tools/pipeline.ts
11315
11554
  init_edge_function();
11555
+ import { z as z22 } from "zod";
11556
+ import { randomUUID as randomUUID3 } from "node:crypto";
11316
11557
  init_supabase();
11317
11558
  init_quality();
11318
11559
  init_version();
11319
- import { z as z22 } from "zod";
11320
- import { randomUUID as randomUUID3 } from "node:crypto";
11321
11560
  function asEnvelope17(data) {
11322
11561
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
11323
11562
  }
@@ -11447,7 +11686,7 @@ function registerPipelineTools(server2) {
11447
11686
  return { content: [{ type: "text", text: lines.join("\n") }] };
11448
11687
  } catch (err) {
11449
11688
  const durationMs = Date.now() - startedAt;
11450
- const message = err instanceof Error ? err.message : String(err);
11689
+ const message = sanitizeError2(err);
11451
11690
  logMcpToolInvocation({
11452
11691
  toolName: "check_pipeline_readiness",
11453
11692
  status: "error",
@@ -11608,7 +11847,7 @@ function registerPipelineTools(server2) {
11608
11847
  } catch (deductErr) {
11609
11848
  errors.push({
11610
11849
  stage: "planning",
11611
- message: `Credit deduction failed: ${deductErr instanceof Error ? deductErr.message : String(deductErr)}`
11850
+ message: `Credit deduction failed: ${sanitizeError2(deductErr)}`
11612
11851
  });
11613
11852
  }
11614
11853
  }
@@ -11779,7 +12018,7 @@ function registerPipelineTools(server2) {
11779
12018
  } catch (schedErr) {
11780
12019
  errors.push({
11781
12020
  stage: "schedule",
11782
- message: `Failed to schedule ${post.id}: ${schedErr instanceof Error ? schedErr.message : String(schedErr)}`
12021
+ message: `Failed to schedule ${post.id}: ${sanitizeError2(schedErr)}`
11783
12022
  });
11784
12023
  }
11785
12024
  }
@@ -11868,7 +12107,7 @@ function registerPipelineTools(server2) {
11868
12107
  return { content: [{ type: "text", text: lines.join("\n") }] };
11869
12108
  } catch (err) {
11870
12109
  const durationMs = Date.now() - startedAt;
11871
- const message = err instanceof Error ? err.message : String(err);
12110
+ const message = sanitizeError2(err);
11872
12111
  logMcpToolInvocation({
11873
12112
  toolName: "run_content_pipeline",
11874
12113
  status: "error",
@@ -12092,7 +12331,7 @@ function registerPipelineTools(server2) {
12092
12331
  return { content: [{ type: "text", text: lines.join("\n") }] };
12093
12332
  } catch (err) {
12094
12333
  const durationMs = Date.now() - startedAt;
12095
- const message = err instanceof Error ? err.message : String(err);
12334
+ const message = sanitizeError2(err);
12096
12335
  logMcpToolInvocation({
12097
12336
  toolName: "auto_approve_plan",
12098
12337
  status: "error",
@@ -12127,9 +12366,9 @@ function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
12127
12366
 
12128
12367
  // src/tools/suggest.ts
12129
12368
  init_edge_function();
12369
+ import { z as z23 } from "zod";
12130
12370
  init_supabase();
12131
12371
  init_version();
12132
- import { z as z23 } from "zod";
12133
12372
  function asEnvelope18(data) {
12134
12373
  return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
12135
12374
  }
@@ -12272,7 +12511,7 @@ ${i + 1}. ${s.topic}`);
12272
12511
  return { content: [{ type: "text", text: lines.join("\n") }] };
12273
12512
  } catch (err) {
12274
12513
  const durationMs = Date.now() - startedAt;
12275
- const message = err instanceof Error ? err.message : String(err);
12514
+ const message = sanitizeError2(err);
12276
12515
  logMcpToolInvocation({
12277
12516
  toolName: "suggest_next_content",
12278
12517
  status: "error",
@@ -12290,8 +12529,8 @@ ${i + 1}. ${s.topic}`);
12290
12529
 
12291
12530
  // src/tools/digest.ts
12292
12531
  init_edge_function();
12293
- init_supabase();
12294
12532
  import { z as z24 } from "zod";
12533
+ init_supabase();
12295
12534
 
12296
12535
  // src/lib/anomaly-detector.ts
12297
12536
  var SENSITIVITY_THRESHOLDS = {
@@ -12613,7 +12852,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12613
12852
  return { content: [{ type: "text", text: lines.join("\n") }] };
12614
12853
  } catch (err) {
12615
12854
  const durationMs = Date.now() - startedAt;
12616
- const message = err instanceof Error ? err.message : String(err);
12855
+ const message = sanitizeError2(err);
12617
12856
  logMcpToolInvocation({
12618
12857
  toolName: "generate_performance_digest",
12619
12858
  status: "error",
@@ -12710,7 +12949,7 @@ Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleS
12710
12949
  return { content: [{ type: "text", text: lines.join("\n") }] };
12711
12950
  } catch (err) {
12712
12951
  const durationMs = Date.now() - startedAt;
12713
- const message = err instanceof Error ? err.message : String(err);
12952
+ const message = sanitizeError2(err);
12714
12953
  logMcpToolInvocation({
12715
12954
  toolName: "detect_anomalies",
12716
12955
  status: "error",
@@ -12731,6 +12970,275 @@ init_edge_function();
12731
12970
  init_supabase();
12732
12971
  init_version();
12733
12972
  import { z as z25 } from "zod";
12973
+
12974
+ // src/lib/brandScoring.ts
12975
+ var WEIGHTS = {
12976
+ toneAlignment: 0.3,
12977
+ vocabularyAdherence: 0.25,
12978
+ avoidCompliance: 0.2,
12979
+ audienceRelevance: 0.15,
12980
+ brandMentions: 0.05,
12981
+ structuralPatterns: 0.05
12982
+ };
12983
+ function norm(content) {
12984
+ return content.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
12985
+ }
12986
+ function findMatches(content, terms) {
12987
+ const n = norm(content);
12988
+ return terms.filter((t) => n.includes(t.toLowerCase()));
12989
+ }
12990
+ function findMissing(content, terms) {
12991
+ const n = norm(content);
12992
+ return terms.filter((t) => !n.includes(t.toLowerCase()));
12993
+ }
12994
+ var FABRICATION_PATTERNS = [
12995
+ { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
12996
+ { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
12997
+ {
12998
+ regex: /\b(guaranteed|proven to|studies show|scientifically proven)\b/gi,
12999
+ label: "unverified claim"
13000
+ },
13001
+ {
13002
+ regex: /\b(always works|100% effective|risk[- ]?free|no risk)\b/gi,
13003
+ label: "absolute claim"
13004
+ }
13005
+ ];
13006
+ function detectFabricationPatterns(content) {
13007
+ const matches = [];
13008
+ for (const { regex, label } of FABRICATION_PATTERNS) {
13009
+ const re = new RegExp(regex.source, regex.flags);
13010
+ let m;
13011
+ while ((m = re.exec(content)) !== null) {
13012
+ matches.push({ label, match: m[0] });
13013
+ }
13014
+ }
13015
+ return matches;
13016
+ }
13017
+ function scoreTone(content, profile) {
13018
+ const terms = profile.voiceProfile?.tone || [];
13019
+ if (!terms.length)
13020
+ return {
13021
+ score: 50,
13022
+ weight: WEIGHTS.toneAlignment,
13023
+ issues: [],
13024
+ suggestions: ["Define brand tone words for better consistency measurement"]
13025
+ };
13026
+ const matched = findMatches(content, terms);
13027
+ const missing = findMissing(content, terms);
13028
+ const score = Math.min(100, Math.round(matched.length / terms.length * 100));
13029
+ const issues = [];
13030
+ const suggestions = [];
13031
+ if (missing.length > 0) {
13032
+ issues.push(`Missing tone signals: ${missing.join(", ")}`);
13033
+ suggestions.push(`Try incorporating tone words: ${missing.slice(0, 3).join(", ")}`);
13034
+ }
13035
+ return { score, weight: WEIGHTS.toneAlignment, issues, suggestions };
13036
+ }
13037
+ function scoreVocab(content, profile) {
13038
+ const preferred = [
13039
+ ...profile.voiceProfile?.languagePatterns || [],
13040
+ ...profile.vocabularyRules?.preferredTerms || []
13041
+ ];
13042
+ if (!preferred.length)
13043
+ return {
13044
+ score: 50,
13045
+ weight: WEIGHTS.vocabularyAdherence,
13046
+ issues: [],
13047
+ suggestions: ["Add preferred terms to improve vocabulary scoring"]
13048
+ };
13049
+ const matched = findMatches(content, preferred);
13050
+ const missing = findMissing(content, preferred);
13051
+ const score = Math.min(100, Math.round(matched.length / preferred.length * 100));
13052
+ const issues = [];
13053
+ const suggestions = [];
13054
+ if (missing.length > 0 && score < 60) {
13055
+ issues.push(`Low preferred term usage (${matched.length}/${preferred.length})`);
13056
+ suggestions.push(`Consider using: ${missing.slice(0, 3).join(", ")}`);
13057
+ }
13058
+ return { score, weight: WEIGHTS.vocabularyAdherence, issues, suggestions };
13059
+ }
13060
+ function scoreAvoid(content, profile) {
13061
+ const banned = [
13062
+ ...profile.voiceProfile?.avoidPatterns || [],
13063
+ ...profile.vocabularyRules?.bannedTerms || []
13064
+ ];
13065
+ if (!banned.length)
13066
+ return {
13067
+ score: 100,
13068
+ weight: WEIGHTS.avoidCompliance,
13069
+ issues: [],
13070
+ suggestions: []
13071
+ };
13072
+ const violations = findMatches(content, banned);
13073
+ const score = violations.length === 0 ? 100 : Math.max(0, 100 - violations.length * 25);
13074
+ const issues = [];
13075
+ const suggestions = [];
13076
+ if (violations.length > 0) {
13077
+ issues.push(`Banned/avoided terms found: ${violations.join(", ")}`);
13078
+ suggestions.push(`Remove or replace: ${violations.join(", ")}`);
13079
+ }
13080
+ return { score, weight: WEIGHTS.avoidCompliance, issues, suggestions };
13081
+ }
13082
+ function scoreAudience(content, profile) {
13083
+ const terms = [];
13084
+ const d = profile.targetAudience?.demographics;
13085
+ const p = profile.targetAudience?.psychographics;
13086
+ if (d?.ageRange) terms.push(d.ageRange);
13087
+ if (d?.location) terms.push(d.location);
13088
+ if (p?.interests) terms.push(...p.interests);
13089
+ if (p?.painPoints) terms.push(...p.painPoints);
13090
+ if (p?.aspirations) terms.push(...p.aspirations);
13091
+ const valid = terms.filter(Boolean);
13092
+ if (!valid.length)
13093
+ return {
13094
+ score: 50,
13095
+ weight: WEIGHTS.audienceRelevance,
13096
+ issues: [],
13097
+ suggestions: ["Define target audience details for relevance scoring"]
13098
+ };
13099
+ const matched = findMatches(content, valid);
13100
+ const score = Math.min(100, Math.round(matched.length / valid.length * 100));
13101
+ const issues = [];
13102
+ const suggestions = [];
13103
+ if (score < 40) {
13104
+ issues.push("Content has low audience relevance");
13105
+ suggestions.push(
13106
+ `Reference audience pain points or interests: ${valid.slice(0, 3).join(", ")}`
13107
+ );
13108
+ }
13109
+ return { score, weight: WEIGHTS.audienceRelevance, issues, suggestions };
13110
+ }
13111
+ function scoreBrand(content, profile) {
13112
+ const name = profile.name?.toLowerCase();
13113
+ if (!name)
13114
+ return {
13115
+ score: 50,
13116
+ weight: WEIGHTS.brandMentions,
13117
+ issues: [],
13118
+ suggestions: []
13119
+ };
13120
+ const mentioned = norm(content).includes(name);
13121
+ const issues = [];
13122
+ const suggestions = [];
13123
+ if (!mentioned) {
13124
+ issues.push("Brand name not mentioned");
13125
+ suggestions.push(`Include "${profile.name}" in the content`);
13126
+ }
13127
+ return {
13128
+ score: mentioned ? 100 : 0,
13129
+ weight: WEIGHTS.brandMentions,
13130
+ issues,
13131
+ suggestions
13132
+ };
13133
+ }
13134
+ function scoreStructure(content, profile) {
13135
+ const rules = profile.writingStyleRules;
13136
+ if (!rules)
13137
+ return {
13138
+ score: 50,
13139
+ weight: WEIGHTS.structuralPatterns,
13140
+ issues: [],
13141
+ suggestions: []
13142
+ };
13143
+ let score = 100;
13144
+ const issues = [];
13145
+ const suggestions = [];
13146
+ if (rules.perspective) {
13147
+ const markers = {
13148
+ "first-singular": [/\bI\b/g, /\bmy\b/gi],
13149
+ "first-plural": [/\bwe\b/gi, /\bour\b/gi],
13150
+ second: [/\byou\b/gi, /\byour\b/gi],
13151
+ third: [/\bthey\b/gi, /\btheir\b/gi]
13152
+ };
13153
+ const expected = markers[rules.perspective];
13154
+ if (expected && !expected.some((r) => r.test(content))) {
13155
+ score -= 30;
13156
+ issues.push(`Expected ${rules.perspective} perspective not detected`);
13157
+ suggestions.push(`Use ${rules.perspective} perspective pronouns`);
13158
+ }
13159
+ }
13160
+ if (rules.useContractions === false) {
13161
+ const found = content.match(
13162
+ /\b(don't|won't|can't|isn't|aren't|wasn't|weren't|hasn't|haven't|doesn't|didn't|wouldn't|couldn't|shouldn't|it's|that's|there's|here's|what's|who's|let's|we're|they're|you're|I'm|he's|she's)\b/gi
13163
+ );
13164
+ if (found && found.length > 0) {
13165
+ score -= Math.min(40, found.length * 10);
13166
+ issues.push(`Contractions found (${found.length}): ${found.slice(0, 3).join(", ")}`);
13167
+ suggestions.push("Expand contractions to full forms");
13168
+ }
13169
+ }
13170
+ if (rules.emojiPolicy === "none") {
13171
+ const emojis = content.match(
13172
+ /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu
13173
+ );
13174
+ if (emojis && emojis.length > 0) {
13175
+ score -= 20;
13176
+ issues.push('Emojis found but emoji policy is "none"');
13177
+ suggestions.push("Remove emojis from content");
13178
+ }
13179
+ }
13180
+ return {
13181
+ score: Math.max(0, score),
13182
+ weight: WEIGHTS.structuralPatterns,
13183
+ issues,
13184
+ suggestions
13185
+ };
13186
+ }
13187
+ function computeBrandConsistency(content, profile, threshold = 60) {
13188
+ if (!content || !profile) {
13189
+ const neutral = {
13190
+ score: 50,
13191
+ weight: 0,
13192
+ issues: [],
13193
+ suggestions: []
13194
+ };
13195
+ return {
13196
+ overall: 50,
13197
+ passed: false,
13198
+ dimensions: {
13199
+ toneAlignment: { ...neutral, weight: WEIGHTS.toneAlignment },
13200
+ vocabularyAdherence: { ...neutral, weight: WEIGHTS.vocabularyAdherence },
13201
+ avoidCompliance: { ...neutral, weight: WEIGHTS.avoidCompliance },
13202
+ audienceRelevance: { ...neutral, weight: WEIGHTS.audienceRelevance },
13203
+ brandMentions: { ...neutral, weight: WEIGHTS.brandMentions },
13204
+ structuralPatterns: { ...neutral, weight: WEIGHTS.structuralPatterns }
13205
+ },
13206
+ preferredTermsUsed: [],
13207
+ bannedTermsFound: [],
13208
+ fabricationWarnings: []
13209
+ };
13210
+ }
13211
+ const dimensions = {
13212
+ toneAlignment: scoreTone(content, profile),
13213
+ vocabularyAdherence: scoreVocab(content, profile),
13214
+ avoidCompliance: scoreAvoid(content, profile),
13215
+ audienceRelevance: scoreAudience(content, profile),
13216
+ brandMentions: scoreBrand(content, profile),
13217
+ structuralPatterns: scoreStructure(content, profile)
13218
+ };
13219
+ const overall = Math.round(
13220
+ Object.values(dimensions).reduce((sum, d) => sum + d.score * d.weight, 0)
13221
+ );
13222
+ const preferred = [
13223
+ ...profile.voiceProfile?.languagePatterns || [],
13224
+ ...profile.vocabularyRules?.preferredTerms || []
13225
+ ];
13226
+ const banned = [
13227
+ ...profile.voiceProfile?.avoidPatterns || [],
13228
+ ...profile.vocabularyRules?.bannedTerms || []
13229
+ ];
13230
+ const fabrications = detectFabricationPatterns(content);
13231
+ return {
13232
+ overall: Math.max(0, Math.min(100, overall)),
13233
+ passed: overall >= threshold,
13234
+ dimensions,
13235
+ preferredTermsUsed: findMatches(content, preferred),
13236
+ bannedTermsFound: findMatches(content, banned),
13237
+ fabricationWarnings: fabrications.map((f) => `${f.label}: "${f.match}"`)
13238
+ };
13239
+ }
13240
+
13241
+ // src/tools/brandRuntime.ts
12734
13242
  function asEnvelope20(data) {
12735
13243
  return {
12736
13244
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -12943,44 +13451,7 @@ function registerBrandRuntimeTools(server2) {
12943
13451
  };
12944
13452
  }
12945
13453
  const profile = row.profile_data;
12946
- const contentLower = content.toLowerCase();
12947
- const issues = [];
12948
- let score = 70;
12949
- const banned = profile.vocabularyRules?.bannedTerms || [];
12950
- const bannedFound = banned.filter((t) => contentLower.includes(t.toLowerCase()));
12951
- if (bannedFound.length > 0) {
12952
- score -= bannedFound.length * 15;
12953
- issues.push(`Banned terms found: ${bannedFound.join(", ")}`);
12954
- }
12955
- const avoid = profile.voiceProfile?.avoidPatterns || [];
12956
- const avoidFound = avoid.filter((p) => contentLower.includes(p.toLowerCase()));
12957
- if (avoidFound.length > 0) {
12958
- score -= avoidFound.length * 10;
12959
- issues.push(`Avoid patterns found: ${avoidFound.join(", ")}`);
12960
- }
12961
- const preferred = profile.vocabularyRules?.preferredTerms || [];
12962
- const prefUsed = preferred.filter((t) => contentLower.includes(t.toLowerCase()));
12963
- score += Math.min(15, prefUsed.length * 5);
12964
- const fabPatterns = [
12965
- { regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
12966
- { regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
12967
- { regex: /\b(guaranteed|proven to|studies show)\b/gi, label: "unverified claim" }
12968
- ];
12969
- for (const { regex, label } of fabPatterns) {
12970
- regex.lastIndex = 0;
12971
- if (regex.test(content)) {
12972
- score -= 10;
12973
- issues.push(`Potential ${label} detected`);
12974
- }
12975
- }
12976
- score = Math.max(0, Math.min(100, score));
12977
- const checkResult = {
12978
- score,
12979
- passed: score >= 60,
12980
- issues,
12981
- preferredTermsUsed: prefUsed,
12982
- bannedTermsFound: bannedFound
12983
- };
13454
+ const checkResult = computeBrandConsistency(content, profile);
12984
13455
  const envelope = asEnvelope20(checkResult);
12985
13456
  return {
12986
13457
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]