@socialneuron/mcp-server 1.8.0 → 1.8.2

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
@@ -19,7 +19,7 @@ var MCP_VERSION;
19
19
  var init_version = __esm({
20
20
  "src/lib/version.ts"() {
21
21
  "use strict";
22
- MCP_VERSION = "1.8.0";
22
+ MCP_VERSION = "1.8.2";
23
23
  }
24
24
  });
25
25
 
@@ -91,6 +91,9 @@ var init_scopes = __esm({
91
91
  generate_carousel: "mcp:write",
92
92
  create_carousel: "mcp:write",
93
93
  upload_media: "mcp:write",
94
+ cancel_async_job: "mcp:write",
95
+ delete_carousel: "mcp:write",
96
+ delete_content_plan: "mcp:write",
94
97
  // mcp:read (media)
95
98
  get_media_url: "mcp:read",
96
99
  // F4 Hyperframes — HTML composition runtime
@@ -98,6 +101,8 @@ var init_scopes = __esm({
98
101
  render_hyperframes: "mcp:write",
99
102
  // mcp:distribute
100
103
  schedule_post: "mcp:distribute",
104
+ reschedule_post: "mcp:distribute",
105
+ cancel_scheduled_post: "mcp:distribute",
101
106
  start_platform_connection: "mcp:distribute",
102
107
  // mcp:analytics
103
108
  refresh_platform_analytics: "mcp:analytics",
@@ -112,6 +117,7 @@ var init_scopes = __esm({
112
117
  list_autopilot_configs: "mcp:autopilot",
113
118
  update_autopilot_config: "mcp:autopilot",
114
119
  get_autopilot_status: "mcp:autopilot",
120
+ delete_autopilot_config: "mcp:autopilot",
115
121
  // Recipes
116
122
  list_recipes: "mcp:read",
117
123
  get_recipe_details: "mcp:read",
@@ -155,6 +161,7 @@ var init_scopes = __esm({
155
161
  detect_anomalies: "mcp:analytics",
156
162
  // mcp:read (Apps — entry tool for the Content Calendar MCP App; reads recent posts)
157
163
  open_content_calendar: "mcp:read",
164
+ open_analytics_pulse: "mcp:read",
158
165
  // mcp:write (Agentic harness — learning loop write-back)
159
166
  write_agent_reflection: "mcp:write",
160
167
  record_outcome: "mcp:write",
@@ -170,6 +177,7 @@ var init_scopes = __esm({
170
177
  get_active_campaigns: "mcp:read",
171
178
  // mcp:read / mcp:write (Skills)
172
179
  list_skills: "mcp:read",
180
+ get_skill: "mcp:read",
173
181
  run_skill: "mcp:write",
174
182
  // mcp:read (Loop observability — growth-loop KPIs + content learning state)
175
183
  get_loop_pulse: "mcp:read",
@@ -296,6 +304,11 @@ var init_tool_annotations = __esm({
296
304
  OVERRIDES = {
297
305
  // Destructive or overwrite tools
298
306
  delete_comment: { destructiveHint: true },
307
+ cancel_async_job: { destructiveHint: true, idempotentHint: true },
308
+ cancel_scheduled_post: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
309
+ delete_carousel: { destructiveHint: true, idempotentHint: true },
310
+ delete_content_plan: { destructiveHint: true, idempotentHint: true },
311
+ delete_autopilot_config: { destructiveHint: true, idempotentHint: true },
299
312
  moderate_comment: { destructiveHint: true },
300
313
  save_brand_profile: { destructiveHint: true, idempotentHint: true },
301
314
  update_platform_voice: { destructiveHint: true, idempotentHint: true },
@@ -381,25 +394,140 @@ __export(credentials_exports, {
381
394
  saveSupabaseUrl: () => saveSupabaseUrl
382
395
  });
383
396
  import { execFileSync } from "node:child_process";
384
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
397
+ import {
398
+ closeSync,
399
+ constants,
400
+ existsSync,
401
+ fchmodSync,
402
+ fstatSync,
403
+ ftruncateSync,
404
+ lstatSync,
405
+ mkdirSync,
406
+ openSync,
407
+ readFileSync,
408
+ unlinkSync,
409
+ writeFileSync
410
+ } from "node:fs";
385
411
  import { homedir, platform } from "node:os";
386
412
  import { join } from "node:path";
413
+ function assertSafeCredentialPaths() {
414
+ if (platform() === "win32") return;
415
+ const uid = process.getuid?.();
416
+ if (existsSync(CONFIG_DIR)) {
417
+ const directory = lstatSync(CONFIG_DIR);
418
+ if (directory.isSymbolicLink() || !directory.isDirectory()) {
419
+ throw new Error("Unsafe Social Neuron credential directory. Refusing to use it.");
420
+ }
421
+ if (uid !== void 0 && directory.uid !== uid) {
422
+ throw new Error("Social Neuron credential directory is not owned by the current user.");
423
+ }
424
+ }
425
+ if (existsSync(CREDENTIALS_FILE)) {
426
+ const file = lstatSync(CREDENTIALS_FILE);
427
+ if (file.isSymbolicLink() || !file.isFile()) {
428
+ throw new Error("Unsafe Social Neuron credential file. Refusing to use it.");
429
+ }
430
+ if (uid !== void 0 && file.uid !== uid) {
431
+ throw new Error("Social Neuron credential file is not owned by the current user.");
432
+ }
433
+ }
434
+ }
435
+ function openValidatedCredentialPath(target, kind, flags) {
436
+ let fd;
437
+ try {
438
+ fd = openSync(
439
+ target,
440
+ flags | constants.O_NOFOLLOW | (kind === "directory" ? constants.O_DIRECTORY : 0)
441
+ );
442
+ } catch (error) {
443
+ if (error.code === "ENOENT") return null;
444
+ throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
445
+ }
446
+ const opened = fstatSync(fd);
447
+ const uid = process.getuid?.();
448
+ if ((kind === "directory" ? !opened.isDirectory() : !opened.isFile()) || uid !== void 0 && opened.uid !== uid || kind === "file" && opened.nlink !== 1) {
449
+ closeSync(fd);
450
+ throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
451
+ }
452
+ return fd;
453
+ }
454
+ function hardenCredentialPath(target, kind, mode) {
455
+ const fd = openValidatedCredentialPath(target, kind, constants.O_RDONLY);
456
+ if (fd === null) return;
457
+ try {
458
+ const opened = fstatSync(fd);
459
+ if ((opened.mode & 63) !== 0) fchmodSync(fd, mode);
460
+ } finally {
461
+ closeSync(fd);
462
+ }
463
+ }
464
+ function hardenCredentialPermissions() {
465
+ if (platform() === "win32") return;
466
+ hardenCredentialPath(CONFIG_DIR, "directory", 448);
467
+ hardenCredentialPath(CREDENTIALS_FILE, "file", 384);
468
+ }
387
469
  function readCredentialsFile() {
470
+ if (platform() !== "win32") hardenCredentialPath(CONFIG_DIR, "directory", 448);
471
+ const fd = platform() === "win32" ? (() => {
472
+ try {
473
+ return openSync(CREDENTIALS_FILE, constants.O_RDONLY);
474
+ } catch (error) {
475
+ if (error.code === "ENOENT") return null;
476
+ throw error;
477
+ }
478
+ })() : openValidatedCredentialPath(CREDENTIALS_FILE, "file", constants.O_RDONLY);
479
+ if (fd === null) return {};
388
480
  try {
389
- if (!existsSync(CREDENTIALS_FILE)) return {};
390
- const raw = readFileSync(CREDENTIALS_FILE, "utf-8");
391
- return JSON.parse(raw);
392
- } catch {
393
- return {};
481
+ if (platform() !== "win32") fchmodSync(fd, 384);
482
+ const parsed = JSON.parse(readFileSync(fd, "utf-8"));
483
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
484
+ throw new Error("Social Neuron credential file is invalid.");
485
+ }
486
+ return parsed;
487
+ } finally {
488
+ closeSync(fd);
394
489
  }
395
490
  }
396
491
  function writeCredentialsFile(data) {
397
492
  if (!existsSync(CONFIG_DIR)) {
398
493
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
399
494
  }
400
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2) + "\n", {
401
- mode: 384
402
- });
495
+ assertSafeCredentialPaths();
496
+ const payload = JSON.stringify(data, null, 2) + "\n";
497
+ if (platform() === "win32") {
498
+ writeFileSync(CREDENTIALS_FILE, payload, { mode: 384 });
499
+ } else {
500
+ let fd;
501
+ try {
502
+ fd = openSync(
503
+ CREDENTIALS_FILE,
504
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
505
+ 384
506
+ );
507
+ } catch (error) {
508
+ if (error.code !== "EEXIST") {
509
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
510
+ }
511
+ try {
512
+ fd = openSync(CREDENTIALS_FILE, constants.O_WRONLY | constants.O_NOFOLLOW);
513
+ } catch {
514
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
515
+ }
516
+ }
517
+ try {
518
+ const opened = fstatSync(fd);
519
+ const uid = process.getuid?.();
520
+ if (!opened.isFile() || opened.nlink !== 1 || uid !== void 0 && opened.uid !== uid) {
521
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
522
+ }
523
+ fchmodSync(fd, 384);
524
+ ftruncateSync(fd, 0);
525
+ writeFileSync(fd, payload);
526
+ } finally {
527
+ closeSync(fd);
528
+ }
529
+ }
530
+ hardenCredentialPermissions();
403
531
  }
404
532
  function macKeychainRead(service) {
405
533
  try {
@@ -609,7 +737,6 @@ async function validateApiKey(apiKey, _attempt = 0) {
609
737
  }
610
738
  );
611
739
  if (!response.ok) {
612
- const text = await response.text();
613
740
  const retryable = response.status === 429 || response.status >= 500;
614
741
  if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
615
742
  await sleep(300 * (_attempt + 1));
@@ -618,11 +745,19 @@ async function validateApiKey(apiKey, _attempt = 0) {
618
745
  return {
619
746
  valid: false,
620
747
  retryable,
621
- error: `Validation failed (HTTP ${response.status}): ${text}`
748
+ error: `Validation failed (HTTP ${response.status}).`
622
749
  };
623
750
  }
624
- return await response.json();
625
- } catch (err) {
751
+ const result = await response.json();
752
+ if (!result.valid) {
753
+ return {
754
+ valid: false,
755
+ retryable: false,
756
+ error: "API key is invalid, expired, or revoked."
757
+ };
758
+ }
759
+ return result;
760
+ } catch {
626
761
  if (_attempt < VALIDATE_MAX_RETRIES) {
627
762
  await sleep(300 * (_attempt + 1));
628
763
  return validateApiKey(apiKey, _attempt + 1);
@@ -630,7 +765,7 @@ async function validateApiKey(apiKey, _attempt = 0) {
630
765
  return {
631
766
  valid: false,
632
767
  retryable: true,
633
- error: err instanceof Error ? err.message : String(err)
768
+ error: "Authentication service is temporarily unavailable."
634
769
  };
635
770
  }
636
771
  }
@@ -651,10 +786,12 @@ __export(posthog_exports, {
651
786
  initPostHog: () => initPostHog,
652
787
  shutdownPostHog: () => shutdownPostHog
653
788
  });
654
- import { createHash } from "node:crypto";
789
+ import { createHmac, randomBytes } from "node:crypto";
655
790
  import { PostHog } from "posthog-node";
656
791
  function hashUserId(userId) {
657
- return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
792
+ const hmac = createHmac("sha256", POSTHOG_PSEUDONYMIZATION_KEY);
793
+ hmac.update(userId);
794
+ return hmac.digest("hex").substring(0, 32);
658
795
  }
659
796
  function initPostHog() {
660
797
  if (isTelemetryDisabled()) return;
@@ -690,13 +827,13 @@ async function shutdownPostHog() {
690
827
  client = null;
691
828
  }
692
829
  }
693
- var POSTHOG_SALT, client;
830
+ var POSTHOG_PSEUDONYMIZATION_KEY, client;
694
831
  var init_posthog = __esm({
695
832
  "src/lib/posthog.ts"() {
696
833
  "use strict";
697
834
  init_supabase();
698
835
  init_request_context();
699
- POSTHOG_SALT = "socialneuron-mcp-ph-v1";
836
+ POSTHOG_PSEUDONYMIZATION_KEY = process.env.POSTHOG_PSEUDONYMIZATION_KEY || randomBytes(32).toString("hex");
700
837
  client = null;
701
838
  }
702
839
  });
@@ -1004,9 +1141,6 @@ function searchTools(query) {
1004
1141
  (t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q)
1005
1142
  );
1006
1143
  }
1007
- function getModules() {
1008
- return [...new Set(TOOL_CATALOG.map((t) => t.module))];
1009
- }
1010
1144
  var TOOL_CATALOG;
1011
1145
  var init_tool_catalog = __esm({
1012
1146
  "src/lib/tool-catalog.ts"() {
@@ -1081,6 +1215,26 @@ var init_tool_catalog = __esm({
1081
1215
  module: "carousel",
1082
1216
  scope: "mcp:write"
1083
1217
  },
1218
+ {
1219
+ name: "cancel_async_job",
1220
+ description: "Cancel an owned pending async job and refund an eligible debit",
1221
+ module: "lifecycle",
1222
+ scope: "mcp:write",
1223
+ task_intent: "Stop a queued generation or render before worker execution",
1224
+ use_when: "The user explicitly wants to cancel a pending job and has confirmed the action.",
1225
+ avoid_when: "The job is already processing or terminal; check_status instead.",
1226
+ next_tools: ["check_status", "get_credit_balance"]
1227
+ },
1228
+ {
1229
+ name: "delete_carousel",
1230
+ description: "Delete an owned carousel content record from one project",
1231
+ module: "lifecycle",
1232
+ scope: "mcp:write",
1233
+ task_intent: "Remove an unwanted carousel record from Social Neuron",
1234
+ use_when: "The user explicitly confirms deletion and understands stored media follows normal retention.",
1235
+ avoid_when: "The user expects an already-published social post or retained media object to be removed.",
1236
+ next_tools: ["list_recent_posts"]
1237
+ },
1084
1238
  // media
1085
1239
  {
1086
1240
  name: "upload_media",
@@ -1101,6 +1255,26 @@ var init_tool_catalog = __esm({
1101
1255
  module: "distribution",
1102
1256
  scope: "mcp:distribute"
1103
1257
  },
1258
+ {
1259
+ name: "reschedule_post",
1260
+ description: "Atomically move an unclaimed scheduled post to a new time within its brand project",
1261
+ module: "distribution",
1262
+ scope: "mcp:distribute",
1263
+ task_intent: "Move an already scheduled post without creating a duplicate publication",
1264
+ use_when: "A pending or scheduled post needs a different future publication time.",
1265
+ avoid_when: "The post is already publishing or published; create a new post instead.",
1266
+ next_tools: ["list_recent_posts", "open_content_calendar"]
1267
+ },
1268
+ {
1269
+ name: "cancel_scheduled_post",
1270
+ description: "Cancel an owned scheduled post before publishing begins",
1271
+ module: "lifecycle",
1272
+ scope: "mcp:distribute",
1273
+ task_intent: "Unschedule a post without publishing it",
1274
+ use_when: "The user explicitly confirms cancellation of a draft, pending, or scheduled post.",
1275
+ avoid_when: "The post is already publishing or published.",
1276
+ next_tools: ["list_recent_posts", "open_content_calendar"]
1277
+ },
1104
1278
  {
1105
1279
  name: "list_recent_posts",
1106
1280
  description: "List recently published or scheduled posts",
@@ -1291,6 +1465,16 @@ var init_tool_catalog = __esm({
1291
1465
  module: "planning",
1292
1466
  scope: "mcp:write"
1293
1467
  },
1468
+ {
1469
+ name: "delete_content_plan",
1470
+ description: "Permanently delete an owned content plan from one project",
1471
+ module: "lifecycle",
1472
+ scope: "mcp:write",
1473
+ task_intent: "Remove an obsolete saved content plan",
1474
+ use_when: "The user explicitly confirms permanent deletion of a specific plan.",
1475
+ avoid_when: "The plan has scheduled posts the user also expects to cancel; cancel those posts separately.",
1476
+ next_tools: ["plan_content_week", "list_recent_posts"]
1477
+ },
1294
1478
  {
1295
1479
  name: "submit_content_plan_for_approval",
1296
1480
  description: "Submit a content plan for team approval",
@@ -1385,6 +1569,16 @@ var init_tool_catalog = __esm({
1385
1569
  module: "autopilot",
1386
1570
  scope: "mcp:autopilot"
1387
1571
  },
1572
+ {
1573
+ name: "delete_autopilot_config",
1574
+ description: "Permanently delete an owned autopilot configuration from one project",
1575
+ module: "lifecycle",
1576
+ scope: "mcp:autopilot",
1577
+ task_intent: "Remove an autopilot configuration while retaining historical run records",
1578
+ use_when: "The user explicitly confirms deletion of the configuration.",
1579
+ avoid_when: "The user only wants to pause or edit automation, or expects historical posts to be deleted.",
1580
+ next_tools: ["list_autopilot_configs"]
1581
+ },
1388
1582
  // extraction
1389
1583
  {
1390
1584
  name: "extract_url_content",
@@ -1501,7 +1695,13 @@ var init_tool_catalog = __esm({
1501
1695
  // apps (MCP Apps — interactive UI inside the host)
1502
1696
  {
1503
1697
  name: "open_content_calendar",
1504
- description: "Open an interactive drag-drop calendar of the user's scheduled posts inside the host (Claude Desktop / claude.ai). Renders an MCP App; backed by list_recent_posts, schedule_post, find_next_slots \u2014 no new tools needed.",
1698
+ description: "Open a project-scoped interactive calendar inside MCP App-capable hosts. Uses reschedule_post for conflict-safe drag/drop and schedule_post for new posts.",
1699
+ module: "apps",
1700
+ scope: "mcp:read"
1701
+ },
1702
+ {
1703
+ name: "open_analytics_pulse",
1704
+ description: "Open a project-scoped interactive performance dashboard with views, engagement, platform mix, and top posts.",
1505
1705
  module: "apps",
1506
1706
  scope: "mcp:read"
1507
1707
  },
@@ -1616,6 +1816,12 @@ var init_tool_catalog = __esm({
1616
1816
  module: "skills",
1617
1817
  scope: "mcp:read"
1618
1818
  },
1819
+ {
1820
+ name: "get_skill",
1821
+ description: "Fetch the full body and current compiled guidance for one Social Neuron skill by slug.",
1822
+ module: "skills",
1823
+ scope: "mcp:read"
1824
+ },
1619
1825
  {
1620
1826
  name: "run_skill",
1621
1827
  description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
@@ -1689,6 +1895,7 @@ var init_tool_profile = __esm({
1689
1895
  "list_hyperframes_blocks",
1690
1896
  "render_hyperframes",
1691
1897
  "list_skills",
1898
+ "get_skill",
1692
1899
  "run_skill"
1693
1900
  ]);
1694
1901
  }
@@ -1722,7 +1929,8 @@ var init_constants = __esm({
1722
1929
  ssn: "\\d{3}-\\d{2}-\\d{4}",
1723
1930
  ip: "(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])"
1724
1931
  },
1725
- MAX_LENGTH: 1e4
1932
+ MAX_LENGTH: 1e4,
1933
+ MAX_OUTPUT_LENGTH: 1e6
1726
1934
  };
1727
1935
  }
1728
1936
  });
@@ -1819,7 +2027,8 @@ var init_pii = __esm({
1819
2027
  function scan(text, options) {
1820
2028
  const flagged = /* @__PURE__ */ new Set();
1821
2029
  let risk = 0;
1822
- if (text.length > CONSTANTS.MAX_LENGTH) {
2030
+ const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
2031
+ if (text.length > maxLength) {
1823
2032
  return {
1824
2033
  passed: false,
1825
2034
  risk_score: 1,
@@ -1875,6 +2084,52 @@ var edge_function_exports = {};
1875
2084
  __export(edge_function_exports, {
1876
2085
  callEdgeFunction: () => callEdgeFunction
1877
2086
  });
2087
+ function safeGatewayError(responseText, status) {
2088
+ try {
2089
+ const parsed = JSON.parse(responseText);
2090
+ const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
2091
+ const candidates = [
2092
+ nested?.error_type,
2093
+ nested?.code,
2094
+ parsed.error_type,
2095
+ parsed.error_code,
2096
+ parsed.code,
2097
+ typeof parsed.error === "string" ? parsed.error : null
2098
+ ];
2099
+ for (const value of candidates) {
2100
+ if (typeof value !== "string") continue;
2101
+ const normalized = value.trim().toLowerCase();
2102
+ if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
2103
+ const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
2104
+ if (embedded) return embedded;
2105
+ }
2106
+ } catch {
2107
+ }
2108
+ return `Backend request failed (HTTP ${status}).`;
2109
+ }
2110
+ function safeFailureData(responseText) {
2111
+ try {
2112
+ const parsed = JSON.parse(responseText);
2113
+ const metric = (name) => {
2114
+ const value = parsed[name];
2115
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2116
+ };
2117
+ const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
2118
+ const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
2119
+ const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
2120
+ const safe = {
2121
+ ...jobStatus ? { status: jobStatus } : {},
2122
+ ...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
2123
+ ...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
2124
+ ...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
2125
+ ...billingStatus ? { billing_status: billingStatus } : {},
2126
+ ...failureReason ? { failure_reason: failureReason } : {}
2127
+ };
2128
+ return Object.keys(safe).length > 0 ? safe : null;
2129
+ } catch {
2130
+ return null;
2131
+ }
2132
+ }
1878
2133
  function getApiKeyOrNull() {
1879
2134
  const envKey = process.env.SOCIALNEURON_API_KEY;
1880
2135
  if (envKey && envKey.trim().length) return envKey.trim();
@@ -1945,33 +2200,28 @@ async function callEdgeFunction(functionName, body, options) {
1945
2200
  clearTimeout(timer);
1946
2201
  const responseText = await response.text();
1947
2202
  if (!response.ok) {
1948
- let errorMessage;
1949
- try {
1950
- const errorJson = JSON.parse(responseText);
1951
- errorMessage = errorJson.error || errorJson.message || responseText;
1952
- } catch {
1953
- errorMessage = responseText || `HTTP ${response.status}`;
1954
- }
2203
+ const errorCode = safeGatewayError(responseText, response.status);
2204
+ const failureData = safeFailureData(responseText);
1955
2205
  if (response.status === 401) {
1956
2206
  return {
1957
- data: null,
2207
+ data: failureData,
1958
2208
  error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
1959
2209
  };
1960
2210
  }
1961
2211
  if (response.status === 403) {
1962
2212
  return {
1963
- data: null,
1964
- error: `Forbidden (HTTP 403): ${errorMessage}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
2213
+ data: failureData,
2214
+ error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
1965
2215
  };
1966
2216
  }
1967
2217
  if (response.status === 429) {
1968
2218
  const retryAfter = response.headers.get("retry-after") || "60";
1969
2219
  return {
1970
- data: null,
2220
+ data: failureData,
1971
2221
  error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
1972
2222
  };
1973
2223
  }
1974
- return { data: null, error: errorMessage };
2224
+ return { data: failureData, error: errorCode };
1975
2225
  }
1976
2226
  try {
1977
2227
  const data = JSON.parse(responseText);
@@ -1987,15 +2237,48 @@ async function callEdgeFunction(functionName, body, options) {
1987
2237
  error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
1988
2238
  };
1989
2239
  }
1990
- const message = err instanceof Error ? err.message : String(err);
1991
- return { data: null, error: message };
2240
+ return { data: null, error: "Network request failed. Please retry." };
1992
2241
  }
1993
2242
  }
2243
+ var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
1994
2244
  var init_edge_function = __esm({
1995
2245
  "src/lib/edge-function.ts"() {
1996
2246
  "use strict";
1997
2247
  init_supabase();
1998
2248
  init_request_context();
2249
+ SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
2250
+ "daily_limit_reached",
2251
+ "insufficient_credits",
2252
+ "project_scope_mismatch",
2253
+ "schedule_conflict",
2254
+ "post_not_found",
2255
+ "post_not_reschedulable",
2256
+ "post_in_progress",
2257
+ "not_cancellable",
2258
+ "publishing_in_progress",
2259
+ "plan_upgrade_required",
2260
+ "rate_limited",
2261
+ "validation_error",
2262
+ "permission_denied",
2263
+ "not_found"
2264
+ ]);
2265
+ SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
2266
+ "reserved",
2267
+ "charged",
2268
+ "refunded",
2269
+ "failed_no_charge",
2270
+ "refund_pending",
2271
+ "not_charged"
2272
+ ]);
2273
+ SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
2274
+ "queued",
2275
+ "pending",
2276
+ "processing",
2277
+ "completed",
2278
+ "failed",
2279
+ "cancelled",
2280
+ "canceled"
2281
+ ]);
1999
2282
  }
2000
2283
  });
2001
2284
 
@@ -2023,6 +2306,7 @@ var CATEGORY_CONFIGS, RateLimiter, limiters;
2023
2306
  var init_rate_limit = __esm({
2024
2307
  "src/lib/rate-limit.ts"() {
2025
2308
  "use strict";
2309
+ init_scopes();
2026
2310
  CATEGORY_CONFIGS = {
2027
2311
  posting: { maxTokens: 30, refillRate: 30 / 60 },
2028
2312
  // 30 req/min — publish/schedule/comment
@@ -2113,8 +2397,8 @@ function registerIdeationTools(server2) {
2113
2397
  brand_voice: z.string().max(500).optional().describe(
2114
2398
  'Tone directive (e.g. "direct, no jargon, second person" or "witty Gen-Z energy with emoji"). Leave blank to auto-load from project brand profile if project_id is set.'
2115
2399
  ),
2116
- model: z.enum(["gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
2117
- "AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality."
2400
+ model: z.enum(["gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
2401
+ "AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality. Retired provider models are intentionally not accepted."
2118
2402
  ),
2119
2403
  project_id: z.string().uuid().optional().describe(
2120
2404
  "Project ID to auto-load brand profile and performance context for prompt enrichment."
@@ -2123,7 +2407,7 @@ function registerIdeationTools(server2) {
2123
2407
  async ({ prompt: prompt2, content_type, platform: platform3, brand_voice, model, project_id }) => {
2124
2408
  try {
2125
2409
  const userId = await getDefaultUserId();
2126
- const rl = checkRateLimit("posting", userId);
2410
+ const rl = checkRateLimit("generation", `generate_content:${userId}`);
2127
2411
  if (!rl.allowed) {
2128
2412
  return {
2129
2413
  content: [
@@ -2380,7 +2664,7 @@ Content Type: ${content_type}`;
2380
2664
  async ({ content, source_platform, target_platform, brand_voice, project_id }) => {
2381
2665
  try {
2382
2666
  const userId = await getDefaultUserId();
2383
- const rl = checkRateLimit("posting", userId);
2667
+ const rl = checkRateLimit("generation", `adapt_content:${userId}`);
2384
2668
  if (!rl.allowed) {
2385
2669
  return {
2386
2670
  content: [
@@ -2492,11 +2776,12 @@ function buildCheckStatusPayload(job, liveStatus) {
2492
2776
  const progress = liveStatus?.progress ?? null;
2493
2777
  const resultUrl = liveStatus?.resultUrl ?? job.result_url ?? null;
2494
2778
  const r2Key = resultUrl && !resultUrl.startsWith("http") ? resultUrl : null;
2495
- const errorMessage = liveStatus?.error ?? job.error_message ?? null;
2779
+ const rawError = liveStatus?.error ?? job.error_message ?? null;
2780
+ const errorMessage = rawError && status === "failed" ? "Generation failed. Retry or choose another model." : rawError && (status === "cancelled" || status === "canceled") ? "Cancelled by user." : null;
2496
2781
  const allUrls = job.result_metadata?.all_urls ?? null;
2497
2782
  const modelRequested = job.result_metadata?.model_requested ?? null;
2498
2783
  const modelDelivered = job.result_metadata?.model_delivered ?? null;
2499
- const fallbackReason = job.result_metadata?.fallback_reason ?? null;
2784
+ const fallbackReason = job.result_metadata?.fallback_reason ? "Requested model was unavailable; a fallback model was used." : null;
2500
2785
  return {
2501
2786
  job_id: job.id,
2502
2787
  job_type: job.job_type,
@@ -2508,6 +2793,11 @@ function buildCheckStatusPayload(job, liveStatus) {
2508
2793
  all_urls: allUrls,
2509
2794
  error: errorMessage,
2510
2795
  credits_cost: job.credits_cost,
2796
+ credits_reserved: job.credits_reserved ?? null,
2797
+ credits_charged: job.credits_charged ?? null,
2798
+ credits_refunded: job.credits_refunded ?? null,
2799
+ billing_status: job.billing_status ?? "unknown",
2800
+ failure_reason: job.failure_reason ?? null,
2511
2801
  created_at: job.created_at,
2512
2802
  completed_at: job.completed_at,
2513
2803
  model_requested: modelRequested,
@@ -2609,6 +2899,13 @@ var init_budget = __esm({
2609
2899
 
2610
2900
  // src/tools/content.ts
2611
2901
  import { z as z2 } from "zod";
2902
+ function buildFallbackDisclosureLine(meta) {
2903
+ const requested = meta?.model_requested;
2904
+ const delivered = meta?.model_delivered;
2905
+ if (!requested || !delivered || requested === delivered) return null;
2906
+ const reason = meta?.fallback_reason ? " because the requested model was unavailable" : "";
2907
+ return `Note: requested "${requested}" but delivered "${delivered}"${reason} \u2014 cost never exceeds the requested model's price.`;
2908
+ }
2612
2909
  function asEnvelope2(data) {
2613
2910
  return {
2614
2911
  _meta: {
@@ -2677,7 +2974,7 @@ function registerContentTools(server2) {
2677
2974
  isError: true
2678
2975
  };
2679
2976
  }
2680
- const rateLimit = checkRateLimit("posting", `generate_video:${userId}`);
2977
+ const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
2681
2978
  if (!rateLimit.allowed) {
2682
2979
  return {
2683
2980
  content: [
@@ -2775,7 +3072,7 @@ function registerContentTools(server2) {
2775
3072
  );
2776
3073
  server2.tool(
2777
3074
  "generate_image",
2778
- "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 2-10 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video).",
3075
+ "Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 15-50 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video). Pass project_id so the asset is stored with the correct brand/project.",
2779
3076
  {
2780
3077
  prompt: z2.string().max(2e3).describe(
2781
3078
  "Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
@@ -2797,9 +3094,10 @@ function registerContentTools(server2) {
2797
3094
  image_url: z2.string().optional().describe(
2798
3095
  "Reference image URL for image-to-image generation. Required for ideogram model. Optional for others."
2799
3096
  ),
3097
+ project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
2800
3098
  response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
2801
3099
  },
2802
- async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
3100
+ async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
2803
3101
  const format = response_format ?? "text";
2804
3102
  const userId = await getDefaultUserId();
2805
3103
  const assetBudget = checkAssetBudget();
@@ -2817,7 +3115,7 @@ function registerContentTools(server2) {
2817
3115
  isError: true
2818
3116
  };
2819
3117
  }
2820
- const rateLimit = checkRateLimit("posting", `generate_image:${userId}`);
3118
+ const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
2821
3119
  if (!rateLimit.allowed) {
2822
3120
  return {
2823
3121
  content: [
@@ -2835,7 +3133,8 @@ function registerContentTools(server2) {
2835
3133
  prompt: prompt2,
2836
3134
  model,
2837
3135
  aspectRatio: aspect_ratio ?? "1:1",
2838
- imageUrl: image_url
3136
+ imageUrl: image_url,
3137
+ ...project_id && { projectId: project_id }
2839
3138
  },
2840
3139
  { timeoutMs: 3e4 }
2841
3140
  );
@@ -2874,7 +3173,8 @@ function registerContentTools(server2) {
2874
3173
  jobId,
2875
3174
  taskId: data.taskId,
2876
3175
  asyncJobId: data.asyncJobId,
2877
- model: data.model
3176
+ model: data.model,
3177
+ projectId: project_id ?? null
2878
3178
  }),
2879
3179
  null,
2880
3180
  2
@@ -2955,20 +3255,28 @@ function registerContentTools(server2) {
2955
3255
  }
2956
3256
  );
2957
3257
  if (liveStatus) {
3258
+ const livePayload = buildCheckStatusPayload(job, liveStatus);
2958
3259
  const lines2 = [
2959
3260
  `Job: ${job.id}`,
2960
3261
  `Type: ${job.job_type}`,
2961
3262
  `Model: ${job.model}`,
2962
- `Status: ${liveStatus.status}`,
2963
- `Progress: ${liveStatus.progress}%`
3263
+ `Status: ${livePayload.status}`,
3264
+ `Progress: ${livePayload.progress}%`
2964
3265
  ];
2965
- if (liveStatus.resultUrl) {
2966
- lines2.push(`Result URL: ${liveStatus.resultUrl}`);
3266
+ if (livePayload.result_url) {
3267
+ lines2.push(`Result URL: ${livePayload.result_url}`);
2967
3268
  }
2968
- if (liveStatus.error) {
2969
- lines2.push(`Error: ${liveStatus.error}`);
3269
+ if (livePayload.error) {
3270
+ lines2.push(`Error: ${livePayload.error}`);
2970
3271
  }
3272
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
3273
+ if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
2971
3274
  lines2.push(`Credits: ${job.credits_cost}`);
3275
+ if (job.billing_status) {
3276
+ lines2.push(
3277
+ `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
3278
+ );
3279
+ }
2972
3280
  lines2.push(`Created: ${job.created_at}`);
2973
3281
  if (format === "json") {
2974
3282
  return {
@@ -2986,7 +3294,7 @@ function registerContentTools(server2) {
2986
3294
  // `job` + `liveStatus`; do not duplicate them here.
2987
3295
  asEnvelope2({
2988
3296
  ...liveStatus,
2989
- ...buildCheckStatusPayload(job, liveStatus)
3297
+ ...livePayload
2990
3298
  }),
2991
3299
  null,
2992
3300
  2
@@ -3029,7 +3337,14 @@ function registerContentTools(server2) {
3029
3337
  if (job.error_message) {
3030
3338
  lines.push(`Error: ${job.error_message}`);
3031
3339
  }
3340
+ const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
3341
+ if (fallbackDisclosure) lines.push(fallbackDisclosure);
3032
3342
  lines.push(`Credits: ${job.credits_cost}`);
3343
+ if (job.billing_status) {
3344
+ lines.push(
3345
+ `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
3346
+ );
3347
+ }
3033
3348
  lines.push(`Created: ${job.created_at}`);
3034
3349
  if (job.completed_at) {
3035
3350
  lines.push(`Completed: ${job.completed_at}`);
@@ -3055,7 +3370,7 @@ function registerContentTools(server2) {
3055
3370
  );
3056
3371
  server2.tool(
3057
3372
  "create_storyboard",
3058
- "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent visual branding across frames.",
3373
+ "Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile and project_id for consistent, project-scoped production. Costs 10 credits.",
3059
3374
  {
3060
3375
  concept: z2.string().max(2e3).describe(
3061
3376
  'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
@@ -3079,6 +3394,7 @@ function registerContentTools(server2) {
3079
3394
  style: z2.string().optional().describe(
3080
3395
  'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
3081
3396
  ),
3397
+ project_id: z2.string().optional().describe("Project ID for brand-scoped generation and attribution."),
3082
3398
  response_format: z2.enum(["text", "json"]).optional().describe(
3083
3399
  "Response format. Defaults to json for structured storyboard data."
3084
3400
  )
@@ -3090,6 +3406,7 @@ function registerContentTools(server2) {
3090
3406
  target_duration,
3091
3407
  num_scenes,
3092
3408
  style,
3409
+ project_id,
3093
3410
  response_format
3094
3411
  }) => {
3095
3412
  const format = response_format ?? "json";
@@ -3172,7 +3489,8 @@ Return ONLY valid JSON in this exact format:
3172
3489
  prompt: storyboardPrompt,
3173
3490
  type: "storyboard",
3174
3491
  model: "gemini-2.5-flash",
3175
- responseFormat: "json"
3492
+ responseFormat: "json",
3493
+ ...project_id && { projectId: project_id }
3176
3494
  },
3177
3495
  { timeoutMs: 6e4 }
3178
3496
  );
@@ -3187,7 +3505,18 @@ Return ONLY valid JSON in this exact format:
3187
3505
  isError: true
3188
3506
  };
3189
3507
  }
3190
- const rawContent = data?.content ?? "";
3508
+ const rawContent = data?.content?.trim() || data?.text?.trim() || "";
3509
+ if (!rawContent) {
3510
+ return {
3511
+ content: [
3512
+ {
3513
+ type: "text",
3514
+ text: "Storyboard generation failed: the AI service returned an empty response."
3515
+ }
3516
+ ],
3517
+ isError: true
3518
+ };
3519
+ }
3191
3520
  addCreditsUsed(estimatedCost);
3192
3521
  if (format === "json") {
3193
3522
  try {
@@ -3223,16 +3552,17 @@ Return ONLY valid JSON in this exact format:
3223
3552
  );
3224
3553
  server2.tool(
3225
3554
  "generate_voiceover",
3226
- "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs ~2 credits per generation.",
3555
+ "Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Pass project_id to keep the asset with the correct brand/project. Costs 15 credits per generation.",
3227
3556
  {
3228
3557
  text: z2.string().max(5e3).describe("The script/text to convert to speech."),
3229
3558
  voice: z2.enum(["rachel", "domi"]).optional().describe(
3230
3559
  "Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
3231
3560
  ),
3232
3561
  speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
3562
+ project_id: z2.string().optional().describe("Project ID to associate the generated voiceover with."),
3233
3563
  response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
3234
3564
  },
3235
- async ({ text, voice, speed, response_format }) => {
3565
+ async ({ text, voice, speed, project_id, response_format }) => {
3236
3566
  const format = response_format ?? "text";
3237
3567
  const userId = await getDefaultUserId();
3238
3568
  const estimatedCost = 15;
@@ -3244,7 +3574,7 @@ Return ONLY valid JSON in this exact format:
3244
3574
  };
3245
3575
  }
3246
3576
  const rateLimit = checkRateLimit(
3247
- "posting",
3577
+ "generation",
3248
3578
  `generate_voiceover:${userId}`
3249
3579
  );
3250
3580
  if (!rateLimit.allowed) {
@@ -3267,7 +3597,8 @@ Return ONLY valid JSON in this exact format:
3267
3597
  {
3268
3598
  text,
3269
3599
  voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
3270
- speed: speed ?? 1
3600
+ speed: speed ?? 1,
3601
+ ...project_id && { projectId: project_id }
3271
3602
  },
3272
3603
  { timeoutMs: 6e4 }
3273
3604
  );
@@ -3303,7 +3634,8 @@ Return ONLY valid JSON in this exact format:
3303
3634
  asEnvelope2({
3304
3635
  audioUrl: data.audioUrl,
3305
3636
  durationSeconds: data.durationSeconds,
3306
- voice: voice ?? "rachel"
3637
+ voice: voice ?? "rachel",
3638
+ projectId: project_id ?? null
3307
3639
  }),
3308
3640
  null,
3309
3641
  2
@@ -3409,7 +3741,7 @@ Return ONLY valid JSON in this exact format:
3409
3741
  }
3410
3742
  const userId = await getDefaultUserId();
3411
3743
  const rateLimit = checkRateLimit(
3412
- "posting",
3744
+ "generation",
3413
3745
  `generate_carousel:${userId}`
3414
3746
  );
3415
3747
  if (!rateLimit.allowed) {
@@ -3563,9 +3895,6 @@ var init_content = __esm({
3563
3895
  // src/lib/sanitize-error.ts
3564
3896
  function sanitizeError(error) {
3565
3897
  const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
3566
- if (process.env.NODE_ENV !== "production") {
3567
- console.error("[Error]", msg);
3568
- }
3569
3898
  for (const [pattern, userMessage] of ERROR_PATTERNS) {
3570
3899
  if (pattern.test(msg)) {
3571
3900
  return userMessage;
@@ -3779,6 +4108,7 @@ function evaluateQuality(input) {
3779
4108
  const firstLine = caption.split("\n")[0]?.trim() ?? "";
3780
4109
  const hashtags = countHashtags(caption);
3781
4110
  const threshold = Math.min(35, Math.max(0, input.threshold ?? 26));
4111
+ const isShortFormX = platforms.length > 0 && platforms.every((p) => p === "twitter") && caption.length <= 280;
3782
4112
  const blockedTerms = [
3783
4113
  ...(input.brandAvoidPatterns ?? []).map((t) => t.trim()).filter(Boolean),
3784
4114
  ...(input.customBannedTerms ?? []).map((t) => t.trim()).filter(Boolean)
@@ -3788,11 +4118,12 @@ function evaluateQuality(input) {
3788
4118
  if (firstLine.length >= 20 && firstLine.length <= 120) hookScore += 1;
3789
4119
  if (/[!?]/.test(firstLine) || /\b\d+(\.\d+)?\b/.test(firstLine)) hookScore += 1;
3790
4120
  if (/\b(how|why|stop|avoid|build|launch|scale|grow|mistake)\b/i.test(firstLine)) hookScore += 1;
4121
+ if (isShortFormX) hookScore = Math.max(hookScore, 3);
3791
4122
  categories.push({
3792
4123
  name: "Hook Strength",
3793
4124
  score: Math.min(5, hookScore),
3794
4125
  maxScore: 5,
3795
- detail: "First line should create curiosity/value within 120 chars."
4126
+ detail: isShortFormX ? "Short-form X: keyword-hook heuristics neutralized; substance judge arbitrates." : "First line should create curiosity/value within 120 chars."
3796
4127
  });
3797
4128
  let clarityScore = 2;
3798
4129
  if (caption.length >= 80 && caption.length <= 1200) clarityScore += 2;
@@ -3818,7 +4149,8 @@ function evaluateQuality(input) {
3818
4149
  detail: "Length, title, and hashtag usage should match target platforms."
3819
4150
  });
3820
4151
  let brandScore = 3;
3821
- const brandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
4152
+ const rawBrandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
4153
+ const brandKeyword = rawBrandKeyword ? rawBrandKeyword.slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
3822
4154
  if (brandKeyword && new RegExp("\\b" + brandKeyword + "\\b", "i").test(title + " " + caption))
3823
4155
  brandScore += 1;
3824
4156
  if (!/\b(you|your|customer|audience)\b/i.test(caption)) brandScore -= 1;
@@ -3829,11 +4161,12 @@ function evaluateQuality(input) {
3829
4161
  brandScore -= Math.min(2, matched.length);
3830
4162
  }
3831
4163
  }
4164
+ if (isShortFormX) brandScore = Math.max(brandScore, 3);
3832
4165
  categories.push({
3833
4166
  name: "Brand Alignment",
3834
4167
  score: Math.max(0, Math.min(5, brandScore)),
3835
4168
  maxScore: 5,
3836
- detail: "Voice should match brand context and audience focus."
4169
+ detail: isShortFormX ? "Short-form X: second-person-address heuristic neutralized; blocked terms still enforced." : "Voice should match brand context and audience focus."
3837
4170
  });
3838
4171
  let noveltyScore = 2;
3839
4172
  if (/\b(case study|framework|workflow|playbook|breakdown|behind the scenes)\b/i.test(caption))
@@ -3844,21 +4177,23 @@ function evaluateQuality(input) {
3844
4177
  const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
3845
4178
  if (matched.length > 0) noveltyScore -= Math.min(2, matched.length);
3846
4179
  }
4180
+ if (isShortFormX) noveltyScore = Math.max(noveltyScore, 3);
3847
4181
  categories.push({
3848
4182
  name: "Novelty",
3849
4183
  score: Math.max(0, Math.min(5, noveltyScore)),
3850
4184
  maxScore: 5,
3851
- detail: "Avoid generic phrasing; include distinct angle."
4185
+ detail: isShortFormX ? "Short-form X: keyword-novelty neutralized; substance judge arbitrates." : "Avoid generic phrasing; include distinct angle."
3852
4186
  });
3853
4187
  let ctaScore = 2;
3854
4188
  if (/\b(comment|reply|share|save|follow|subscribe|click|try|book|download)\b/i.test(caption))
3855
4189
  ctaScore += 2;
3856
4190
  if (/\?$/.test(firstLine)) ctaScore += 1;
4191
+ if (isShortFormX) ctaScore = Math.max(ctaScore, 3);
3857
4192
  categories.push({
3858
4193
  name: "CTA Strength",
3859
4194
  score: Math.min(5, ctaScore),
3860
4195
  maxScore: 5,
3861
- detail: "Should include a clear next action."
4196
+ detail: isShortFormX ? "Short-form X: explicit CTA optional; native posts may close without one." : "Should include a clear next action."
3862
4197
  });
3863
4198
  let safetyScore = 5;
3864
4199
  if (/\b(guarantee|guaranteed|no risk|risk-free|always works|100%)\b/i.test(caption))
@@ -3896,7 +4231,7 @@ var init_quality = __esm({
3896
4231
 
3897
4232
  // src/tools/distribution.ts
3898
4233
  import { z as z3 } from "zod";
3899
- import { createHash as createHash2 } from "node:crypto";
4234
+ import { createHash } from "node:crypto";
3900
4235
  function snakeToCamel(obj) {
3901
4236
  const result = {};
3902
4237
  for (const [key, value] of Object.entries(obj)) {
@@ -3922,6 +4257,87 @@ function asEnvelope3(data) {
3922
4257
  data
3923
4258
  };
3924
4259
  }
4260
+ function publicConnectedAccount(value) {
4261
+ if (!value || typeof value !== "object") return null;
4262
+ const row = value;
4263
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
4264
+ return null;
4265
+ }
4266
+ return {
4267
+ id: row.id,
4268
+ platform: row.platform,
4269
+ status: row.status,
4270
+ ...typeof row.effective_status === "string" ? { effective_status: row.effective_status } : {},
4271
+ username: typeof row.username === "string" ? row.username : null,
4272
+ created_at: row.created_at,
4273
+ ...typeof row.updated_at === "string" || row.updated_at === null ? { updated_at: row.updated_at } : {},
4274
+ ...typeof row.expires_at === "string" || row.expires_at === null ? { expires_at: row.expires_at } : {},
4275
+ ...typeof row.has_refresh_token === "boolean" ? { has_refresh_token: row.has_refresh_token } : {},
4276
+ ...typeof row.project_id === "string" || row.project_id === null ? { project_id: row.project_id } : {}
4277
+ };
4278
+ }
4279
+ function publicPostRecord(value) {
4280
+ if (!value || typeof value !== "object") return null;
4281
+ const row = value;
4282
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
4283
+ return null;
4284
+ }
4285
+ const nullableString = (field) => typeof row[field] === "string" ? row[field] : null;
4286
+ return {
4287
+ id: row.id,
4288
+ platform: row.platform,
4289
+ status: row.status,
4290
+ title: nullableString("title"),
4291
+ external_post_id: nullableString("external_post_id"),
4292
+ published_at: nullableString("published_at"),
4293
+ scheduled_at: nullableString("scheduled_at"),
4294
+ created_at: row.created_at
4295
+ };
4296
+ }
4297
+ function mediaTypeFromPath(value) {
4298
+ try {
4299
+ const pathname = value.startsWith("http") ? new URL(value).pathname : value;
4300
+ const extension = pathname.split(".").pop()?.toLowerCase();
4301
+ if (!extension) return null;
4302
+ if (VIDEO_FILE_EXTENSIONS.has(extension)) return "VIDEO";
4303
+ if (IMAGE_FILE_EXTENSIONS.has(extension)) return "IMAGE";
4304
+ } catch {
4305
+ return null;
4306
+ }
4307
+ return null;
4308
+ }
4309
+ function inferScheduleMediaType(explicit, singleCandidates, collectionCandidates) {
4310
+ if (explicit) return explicit;
4311
+ const populatedCollection = collectionCandidates.find(
4312
+ (values) => Array.isArray(values) && values.length > 0
4313
+ );
4314
+ if (populatedCollection) {
4315
+ if (populatedCollection.length > 1) return "CAROUSEL_ALBUM";
4316
+ for (const values of collectionCandidates) {
4317
+ if (!values || values.length !== 1) continue;
4318
+ const inferred = mediaTypeFromPath(values[0]);
4319
+ if (inferred) return inferred;
4320
+ }
4321
+ return null;
4322
+ }
4323
+ for (const value of singleCandidates) {
4324
+ if (typeof value !== "string" || value.length === 0) continue;
4325
+ const inferred = mediaTypeFromPath(value);
4326
+ if (inferred) return inferred;
4327
+ }
4328
+ return null;
4329
+ }
4330
+ async function validatePublishMediaUrl(url) {
4331
+ try {
4332
+ if (new URL(url).protocol !== "https:") {
4333
+ return "Media URLs must use HTTPS.";
4334
+ }
4335
+ } catch {
4336
+ return "Media URL is invalid.";
4337
+ }
4338
+ const check = await validateUrlForSSRF(url);
4339
+ return check.isValid ? null : check.error || "Media URL failed safety validation.";
4340
+ }
3925
4341
  function accountEffectiveStatus(account) {
3926
4342
  return account.effective_status || account.status;
3927
4343
  }
@@ -3940,22 +4356,11 @@ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
3940
4356
  if (!accountIds) return void 0;
3941
4357
  return accountIds[platform3] || accountIds[platform3.toLowerCase()];
3942
4358
  }
3943
- function isAlreadyR2Signed(url) {
3944
- try {
3945
- const u = new URL(url);
3946
- return u.searchParams.has("X-Amz-Signature");
3947
- } catch {
3948
- return false;
3949
- }
3950
- }
3951
4359
  async function rehostExternalUrl(mediaUrl, projectId) {
3952
4360
  const ssrf = await validateUrlForSSRF(mediaUrl);
3953
4361
  if (!ssrf.isValid) {
3954
4362
  return { error: ssrf.error ?? "URL rejected by SSRF check" };
3955
4363
  }
3956
- if (isAlreadyR2Signed(mediaUrl)) {
3957
- return { signedUrl: mediaUrl, r2Key: "" };
3958
- }
3959
4364
  const { data, error } = await callEdgeFunction(
3960
4365
  "upload-to-r2",
3961
4366
  { url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
@@ -3974,19 +4379,19 @@ function registerDistributionTools(server2) {
3974
4379
  media_url: z3.string().optional().describe(
3975
4380
  "URL of the media file to post. Any public HTTPS URL works \u2014 including ephemeral generator URLs (Replicate, OpenAI, DALL-E). The server persists non-R2 URLs into R2 before posting so scheduled posts and byte-upload platforms (X, LinkedIn, YouTube, Bluesky) do not 404 when the source URL expires. Set auto_rehost=false to skip. Not needed if media_urls, r2_key, or job_id is provided."
3976
4381
  ),
3977
- media_urls: z3.array(z3.string()).optional().describe(
4382
+ media_urls: z3.array(z3.string().url()).min(2).max(10).optional().describe(
3978
4383
  "Array of 2-10 image URLs for carousel posts. Same rehosting rules as media_url \u2014 ephemeral URLs are persisted automatically. Use with media_type=CAROUSEL_ALBUM."
3979
4384
  ),
3980
4385
  r2_key: z3.string().optional().describe(
3981
4386
  "R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
3982
4387
  ),
3983
- r2_keys: z3.array(z3.string()).optional().describe(
4388
+ r2_keys: z3.array(z3.string()).min(2).max(10).optional().describe(
3984
4389
  "Array of R2 object keys for carousel posts. Each is signed on demand. Alternative to media_urls."
3985
4390
  ),
3986
4391
  job_id: z3.string().optional().describe(
3987
4392
  "Async job ID from generate_image/generate_video. Resolves the completed job's R2 key and signs it. Alternative to media_url/r2_key."
3988
4393
  ),
3989
- job_ids: z3.array(z3.string()).optional().describe(
4394
+ job_ids: z3.array(z3.string()).min(2).max(10).optional().describe(
3990
4395
  "Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
3991
4396
  ),
3992
4397
  platform_metadata: z3.object({
@@ -4016,7 +4421,10 @@ function registerDistributionTools(server2) {
4016
4421
  category_id: z3.string().optional(),
4017
4422
  tags: z3.array(z3.string()).optional(),
4018
4423
  made_for_kids: z3.boolean().optional(),
4019
- notify_subscribers: z3.boolean().optional()
4424
+ notify_subscribers: z3.boolean().optional(),
4425
+ contains_synthetic_media: z3.boolean().optional().describe(
4426
+ "YouTube altered-or-synthetic-content disclosure. Defaults to true for MCP posts; set false explicitly for verified non-AI media."
4427
+ )
4020
4428
  }).optional(),
4021
4429
  facebook: z3.object({
4022
4430
  page_id: z3.string().optional().describe("Facebook Page ID to post to."),
@@ -4099,14 +4507,8 @@ function registerDistributionTools(server2) {
4099
4507
  auto_rehost: z3.boolean().optional().describe(
4100
4508
  "Whether to persist non-R2 media_url/media_urls into R2 before posting. Default: true. Set to false only if you know the source URL will outlive the scheduling window and every target platform supports URL ingest."
4101
4509
  ),
4102
- visual_gate_result: z3.object({ passed: z3.boolean() }).passthrough().optional().describe(
4103
- "Visual QA gate verdict from the carousel/image generation pipeline. Required when mediaType is CAROUSEL_ALBUM, IMAGE, or VIDEO and VISUAL_GATE_ENFORCE is enabled on the EF. Produce with visual_quality_check tool. Must have passed=true for the gate to allow the publish."
4104
- ),
4105
- origin: z3.enum(["human", "hermes", "user"]).optional().describe(
4106
- "Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
4107
- ),
4108
- hermes_run_id: z3.string().optional().describe(
4109
- "Optional agent run identifier for traceability between a draft and the run that produced it."
4510
+ idempotency_key: z3.string().regex(/^[a-zA-Z0-9_-]{8,128}$/).optional().describe(
4511
+ "Stable 8-128 character retry key (letters, numbers, underscore, hyphen). Reuse the same key when retrying the same publish request to prevent duplicate posts."
4110
4512
  )
4111
4513
  },
4112
4514
  async ({
@@ -4129,9 +4531,7 @@ function registerDistributionTools(server2) {
4129
4531
  platform_metadata,
4130
4532
  account_id,
4131
4533
  account_ids,
4132
- visual_gate_result,
4133
- origin,
4134
- hermes_run_id
4534
+ idempotency_key
4135
4535
  }) => {
4136
4536
  const format = response_format ?? "text";
4137
4537
  if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
@@ -4160,6 +4560,8 @@ function registerDistributionTools(server2) {
4160
4560
  }
4161
4561
  let resolvedMediaUrl = media_url;
4162
4562
  let resolvedMediaUrls = media_urls;
4563
+ let resolvedMediaUrlIsTrustedR2 = false;
4564
+ let resolvedMediaUrlsAreTrustedR2 = (media_urls ?? []).map(() => false);
4163
4565
  const signR2Key = async (key) => {
4164
4566
  const cleanKey = key.startsWith("r2://") ? key.slice(5) : key;
4165
4567
  const { data: signData } = await callEdgeFunction(
@@ -4177,8 +4579,11 @@ function registerDistributionTools(server2) {
4177
4579
  );
4178
4580
  const resultUrl = jobData?.job?.result_url;
4179
4581
  if (!resultUrl) return null;
4180
- if (!resultUrl.startsWith("http")) return signR2Key(resultUrl);
4181
- return resultUrl;
4582
+ if (!resultUrl.startsWith("http")) {
4583
+ const signed = await signR2Key(resultUrl);
4584
+ return signed ? { url: signed, trustedR2: true } : null;
4585
+ }
4586
+ return { url: resultUrl, trustedR2: false };
4182
4587
  };
4183
4588
  try {
4184
4589
  if (r2_key && !resolvedMediaUrl) {
@@ -4195,6 +4600,7 @@ function registerDistributionTools(server2) {
4195
4600
  };
4196
4601
  }
4197
4602
  resolvedMediaUrl = signed;
4603
+ resolvedMediaUrlIsTrustedR2 = true;
4198
4604
  } else if (job_id && !resolvedMediaUrl && !r2_key) {
4199
4605
  const resolved = await resolveJobId(job_id);
4200
4606
  if (!resolved) {
@@ -4208,7 +4614,8 @@ function registerDistributionTools(server2) {
4208
4614
  isError: true
4209
4615
  };
4210
4616
  }
4211
- resolvedMediaUrl = resolved;
4617
+ resolvedMediaUrl = resolved.url;
4618
+ resolvedMediaUrlIsTrustedR2 = resolved.trustedR2;
4212
4619
  }
4213
4620
  if (r2_keys && r2_keys.length > 0 && !resolvedMediaUrls) {
4214
4621
  const signed = await Promise.all(r2_keys.map(signR2Key));
@@ -4225,6 +4632,7 @@ function registerDistributionTools(server2) {
4225
4632
  };
4226
4633
  }
4227
4634
  resolvedMediaUrls = signed;
4635
+ resolvedMediaUrlsAreTrustedR2 = signed.map(() => true);
4228
4636
  } else if (job_ids && job_ids.length > 0 && !resolvedMediaUrls && !r2_keys) {
4229
4637
  const resolved = await Promise.all(job_ids.map(resolveJobId));
4230
4638
  const failIdx = resolved.findIndex((r) => !r);
@@ -4239,10 +4647,12 @@ function registerDistributionTools(server2) {
4239
4647
  isError: true
4240
4648
  };
4241
4649
  }
4242
- resolvedMediaUrls = resolved;
4650
+ const resolvedJobs = resolved;
4651
+ resolvedMediaUrls = resolvedJobs.map((item) => item.url);
4652
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
4243
4653
  }
4244
4654
  const shouldRehost = auto_rehost !== false;
4245
- if (shouldRehost && resolvedMediaUrl) {
4655
+ if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
4246
4656
  const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
4247
4657
  if ("error" in rehost) {
4248
4658
  return {
@@ -4256,10 +4666,13 @@ function registerDistributionTools(server2) {
4256
4666
  };
4257
4667
  }
4258
4668
  resolvedMediaUrl = rehost.signedUrl;
4669
+ resolvedMediaUrlIsTrustedR2 = true;
4259
4670
  }
4260
4671
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
4261
4672
  const rehosted = await Promise.all(
4262
- resolvedMediaUrls.map((u) => rehostExternalUrl(u, project_id))
4673
+ resolvedMediaUrls.map(
4674
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
4675
+ )
4263
4676
  );
4264
4677
  const failIdx = rehosted.findIndex((r) => "error" in r);
4265
4678
  if (failIdx !== -1) {
@@ -4275,6 +4688,7 @@ function registerDistributionTools(server2) {
4275
4688
  };
4276
4689
  }
4277
4690
  resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
4691
+ resolvedMediaUrlsAreTrustedR2 = resolvedMediaUrls.map(() => true);
4278
4692
  }
4279
4693
  } catch (resolveErr) {
4280
4694
  return {
@@ -4287,6 +4701,42 @@ function registerDistributionTools(server2) {
4287
4701
  isError: true
4288
4702
  };
4289
4703
  }
4704
+ const hasResolvedMedia = Boolean(
4705
+ resolvedMediaUrl || resolvedMediaUrls && resolvedMediaUrls.length > 0
4706
+ );
4707
+ const resolvedMediaType = inferScheduleMediaType(
4708
+ media_type,
4709
+ [resolvedMediaUrl, r2_key],
4710
+ [resolvedMediaUrls, r2_keys]
4711
+ );
4712
+ if (hasResolvedMedia && !resolvedMediaType) {
4713
+ return {
4714
+ content: [
4715
+ {
4716
+ type: "text",
4717
+ text: "media_type is required when the media format cannot be inferred from a file extension. Set IMAGE, VIDEO, or CAROUSEL_ALBUM explicitly."
4718
+ }
4719
+ ],
4720
+ isError: true
4721
+ };
4722
+ }
4723
+ const finalUrls = [resolvedMediaUrl, ...resolvedMediaUrls ?? []].filter(
4724
+ (value) => typeof value === "string" && value.length > 0
4725
+ );
4726
+ for (const url of finalUrls) {
4727
+ const validationError = await validatePublishMediaUrl(url);
4728
+ if (validationError) {
4729
+ return {
4730
+ content: [
4731
+ {
4732
+ type: "text",
4733
+ text: `Media URL blocked: ${validationError}`
4734
+ }
4735
+ ],
4736
+ isError: true
4737
+ };
4738
+ }
4739
+ }
4290
4740
  const normalizedPlatforms = platforms.map(
4291
4741
  (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
4292
4742
  );
@@ -4412,12 +4862,32 @@ Created with Social Neuron`;
4412
4862
  };
4413
4863
  return true;
4414
4864
  })();
4865
+ const defaultAiDisclosure = (platformKey, field) => {
4866
+ const existing = normalizedPlatformMetadata?.[platformKey];
4867
+ if (existing && existing[field] !== void 0) return;
4868
+ normalizedPlatformMetadata = {
4869
+ ...normalizedPlatformMetadata ?? {},
4870
+ [platformKey]: {
4871
+ ...existing ?? {},
4872
+ [field]: true
4873
+ }
4874
+ };
4875
+ };
4876
+ if (normalizedPlatforms.includes("TikTok")) {
4877
+ defaultAiDisclosure("tiktok", "is_ai_generated");
4878
+ }
4879
+ if (normalizedPlatforms.includes("Instagram")) {
4880
+ defaultAiDisclosure("instagram", "is_ai_generated");
4881
+ }
4882
+ if (normalizedPlatforms.includes("YouTube")) {
4883
+ defaultAiDisclosure("youtube", "contains_synthetic_media");
4884
+ }
4415
4885
  const { data, error } = await callEdgeFunction(
4416
4886
  "schedule-post",
4417
4887
  {
4418
4888
  mediaUrl: resolvedMediaUrl,
4419
4889
  mediaUrls: resolvedMediaUrls,
4420
- mediaType: media_type,
4890
+ mediaType: resolvedMediaType ?? void 0,
4421
4891
  caption: finalCaption,
4422
4892
  platforms: normalizedPlatforms,
4423
4893
  title,
@@ -4430,14 +4900,10 @@ Created with Social Neuron`;
4430
4900
  normalizedPlatformMetadata
4431
4901
  )
4432
4902
  } : {},
4433
- // Forward visual gate verdict + source so the EF can enforce on media posts.
4434
- ...visual_gate_result ? { visualGateResult: visual_gate_result } : {},
4435
- visualGateSource: "mcp",
4436
- // Originator lineage (Hermes integration, 2026-05-22). EF validates origin
4437
- // against the posts.origin CHECK constraint and persists hermesRunId when
4438
- // origin === 'hermes'.
4439
- ...origin ? { origin } : {},
4440
- ...hermes_run_id ? { hermesRunId: hermes_run_id } : {}
4903
+ ...idempotency_key ? { idempotencyKey: idempotency_key } : {}
4904
+ // Attribution is assigned by the authenticated gateway. Visual QA
4905
+ // attestations are server-produced evidence and are deliberately not
4906
+ // accepted from an MCP caller.
4441
4907
  },
4442
4908
  { timeoutMs: 3e4 }
4443
4909
  );
@@ -4503,6 +4969,99 @@ Created with Social Neuron`;
4503
4969
  };
4504
4970
  }
4505
4971
  );
4972
+ server2.tool(
4973
+ "reschedule_post",
4974
+ "Move an existing pending or scheduled post to a new future time without creating a duplicate. Pass project_id for the post's brand. expected_scheduled_at is recommended: it prevents overwriting a change made in another client after the calendar was loaded.",
4975
+ {
4976
+ post_id: z3.string().uuid().describe("Post ID returned by list_recent_posts."),
4977
+ project_id: z3.string().uuid().optional().describe(
4978
+ "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
4979
+ ),
4980
+ scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
4981
+ expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
4982
+ "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
4983
+ ),
4984
+ response_format: z3.enum(["text", "json"]).default("text")
4985
+ },
4986
+ async ({
4987
+ post_id,
4988
+ project_id,
4989
+ scheduled_at,
4990
+ expected_scheduled_at,
4991
+ response_format
4992
+ }) => {
4993
+ const next = new Date(scheduled_at);
4994
+ if (!Number.isFinite(next.getTime()) || next.getTime() <= Date.now()) {
4995
+ return {
4996
+ content: [
4997
+ {
4998
+ type: "text",
4999
+ text: "scheduled_at must be a valid future ISO datetime with timezone."
5000
+ }
5001
+ ],
5002
+ isError: true
5003
+ };
5004
+ }
5005
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5006
+ if (!resolvedProjectId) {
5007
+ return {
5008
+ content: [
5009
+ {
5010
+ type: "text",
5011
+ text: "No project_id was provided and no default project is configured."
5012
+ }
5013
+ ],
5014
+ isError: true
5015
+ };
5016
+ }
5017
+ const userId = await getDefaultUserId();
5018
+ const rateLimit = checkRateLimit("posting", `reschedule_post:${userId}`);
5019
+ if (!rateLimit.allowed) {
5020
+ return {
5021
+ content: [
5022
+ {
5023
+ type: "text",
5024
+ text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
5025
+ }
5026
+ ],
5027
+ isError: true
5028
+ };
5029
+ }
5030
+ const { data: result, error } = await callEdgeFunction("mcp-data", {
5031
+ action: "reschedule-scheduled-post",
5032
+ post_id,
5033
+ projectId: resolvedProjectId,
5034
+ project_id: resolvedProjectId,
5035
+ scheduled_at: next.toISOString(),
5036
+ ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
5037
+ });
5038
+ if (error || !result?.success) {
5039
+ const code = result?.error ?? error ?? "reschedule_failed";
5040
+ const recovery = code === "publishing_in_progress" ? "The worker has already started publishing this post." : code === "schedule_conflict" ? `The schedule changed in another client${result?.current_scheduled_at ? ` to ${result.current_scheduled_at}` : ""}; refresh the calendar before retrying.` : code === "not_found" ? "The post was not found in this project." : code === "not_reschedulable" ? `This post can no longer be rescheduled${result?.status ? ` (status: ${result.status})` : ""}.` : "The post could not be rescheduled.";
5041
+ return {
5042
+ content: [{ type: "text", text: recovery }],
5043
+ isError: true
5044
+ };
5045
+ }
5046
+ const publicResult = {
5047
+ success: true,
5048
+ post_id: result.post_id ?? post_id,
5049
+ project_id: result.project_id ?? resolvedProjectId,
5050
+ previous_scheduled_at: result.previous_scheduled_at ?? null,
5051
+ scheduled_at: result.scheduled_at ?? next.toISOString()
5052
+ };
5053
+ const structuredContent = asEnvelope3(publicResult);
5054
+ return {
5055
+ structuredContent,
5056
+ content: [
5057
+ {
5058
+ type: "text",
5059
+ text: response_format === "json" ? JSON.stringify(structuredContent, null, 2) : `Post ${publicResult.post_id} rescheduled to ${publicResult.scheduled_at}.`
5060
+ }
5061
+ ]
5062
+ };
5063
+ }
5064
+ );
4506
5065
  server2.tool(
4507
5066
  "list_connected_accounts",
4508
5067
  "Check which social platforms have active OAuth connections for posting. Call this before schedule_post to verify credentials. Pass project_id to list the accounts for a specific brand/project, then pass the returned account id as account_id/account_ids when posting. If a platform is missing or expired, the user needs to reconnect at socialneuron.com/settings/connections.",
@@ -4531,14 +5090,16 @@ Created with Social Neuron`;
4531
5090
  isError: true
4532
5091
  };
4533
5092
  }
4534
- const accounts = result.accounts ?? [];
5093
+ const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
4535
5094
  if (accounts.length === 0) {
4536
5095
  if (format === "json") {
5096
+ const structuredContent = asEnvelope3({ accounts: [] });
4537
5097
  return {
5098
+ structuredContent,
4538
5099
  content: [
4539
5100
  {
4540
5101
  type: "text",
4541
- text: JSON.stringify(asEnvelope3({ accounts: [] }), null, 2)
5102
+ text: JSON.stringify(structuredContent, null, 2)
4542
5103
  }
4543
5104
  ]
4544
5105
  };
@@ -4566,11 +5127,13 @@ Created with Social Neuron`;
4566
5127
  );
4567
5128
  }
4568
5129
  if (format === "json") {
5130
+ const structuredContent = asEnvelope3({ accounts });
4569
5131
  return {
5132
+ structuredContent,
4570
5133
  content: [
4571
5134
  {
4572
5135
  type: "text",
4573
- text: JSON.stringify(asEnvelope3({ accounts }), null, 2)
5136
+ text: JSON.stringify(structuredContent, null, 2)
4574
5137
  }
4575
5138
  ]
4576
5139
  };
@@ -4584,6 +5147,9 @@ Created with Social Neuron`;
4584
5147
  "list_recent_posts",
4585
5148
  "List recent published and scheduled posts with status, platform, title, and timestamps. Use to check what has been posted before planning new content, or to find post IDs for fetch_analytics. Filter by platform or status to narrow results.",
4586
5149
  {
5150
+ project_id: z3.string().uuid().optional().describe(
5151
+ "Brand/project ID to scope posts. Defaults to the authenticated key's project or the account default."
5152
+ ),
4587
5153
  platform: z3.enum([
4588
5154
  "youtube",
4589
5155
  "tiktok",
@@ -4599,13 +5165,27 @@ Created with Social Neuron`;
4599
5165
  limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
4600
5166
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4601
5167
  },
4602
- async ({ platform: platform3, status, days, limit, response_format }) => {
5168
+ async ({ project_id, platform: platform3, status, days, limit, response_format }) => {
4603
5169
  const format = response_format ?? "text";
4604
5170
  const lookbackDays = days ?? 7;
4605
- const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
4606
- action: "recent-posts",
4607
- days: lookbackDays,
4608
- limit: limit ?? 20,
5171
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5172
+ if (!resolvedProjectId) {
5173
+ return {
5174
+ content: [
5175
+ {
5176
+ type: "text",
5177
+ text: "No project_id was provided and no default project is configured."
5178
+ }
5179
+ ],
5180
+ isError: true
5181
+ };
5182
+ }
5183
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5184
+ action: "recent-posts",
5185
+ days: lookbackDays,
5186
+ limit: limit ?? 20,
5187
+ projectId: resolvedProjectId,
5188
+ project_id: resolvedProjectId,
4609
5189
  ...platform3 ? { platform: platform3 } : {},
4610
5190
  ...status ? { status } : {}
4611
5191
  });
@@ -4620,7 +5200,7 @@ Created with Social Neuron`;
4620
5200
  isError: true
4621
5201
  };
4622
5202
  }
4623
- const rows = result.posts ?? [];
5203
+ const rows = (result.posts ?? []).map(publicPostRecord).filter((post) => post !== null);
4624
5204
  if (rows.length === 0) {
4625
5205
  if (format === "json") {
4626
5206
  const structuredContent2 = asEnvelope3({ posts: [] });
@@ -4695,8 +5275,11 @@ Created with Social Neuron`;
4695
5275
  };
4696
5276
  server2.tool(
4697
5277
  "find_next_slots",
4698
- "Find optimal posting time slots based on best posting times and existing schedule. Returns non-conflicting slots sorted by engagement score.",
5278
+ "Find optimal posting time slots for one brand/project based on preferred posting times and that project's existing schedule. Returns non-conflicting slots sorted by engagement score.",
4699
5279
  {
5280
+ project_id: z3.string().uuid().optional().describe(
5281
+ "Brand/project ID used for conflict detection. Defaults to the authenticated key's project or the account default."
5282
+ ),
4700
5283
  platforms: z3.array(
4701
5284
  z3.enum([
4702
5285
  "youtube",
@@ -4715,6 +5298,7 @@ Created with Social Neuron`;
4715
5298
  response_format: z3.enum(["text", "json"]).default("text")
4716
5299
  },
4717
5300
  async ({
5301
+ project_id,
4718
5302
  platforms,
4719
5303
  count,
4720
5304
  start_after,
@@ -4722,13 +5306,35 @@ Created with Social Neuron`;
4722
5306
  response_format
4723
5307
  }) => {
4724
5308
  try {
5309
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5310
+ if (!resolvedProjectId) {
5311
+ return {
5312
+ content: [
5313
+ {
5314
+ type: "text",
5315
+ text: "No project_id was provided and no default project is configured."
5316
+ }
5317
+ ],
5318
+ isError: true
5319
+ };
5320
+ }
4725
5321
  const startDate = start_after ? new Date(start_after) : /* @__PURE__ */ new Date();
5322
+ if (!Number.isFinite(startDate.getTime())) {
5323
+ return {
5324
+ content: [
5325
+ { type: "text", text: "start_after must be a valid ISO datetime." }
5326
+ ],
5327
+ isError: true
5328
+ };
5329
+ }
4726
5330
  const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
4727
5331
  const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
4728
5332
  action: "scheduled-posts",
4729
5333
  start_date: startDate.toISOString(),
4730
5334
  end_date: endDate.toISOString(),
4731
- statuses: ["scheduled", "draft"]
5335
+ statuses: ["pending", "scheduled", "draft"],
5336
+ projectId: resolvedProjectId,
5337
+ project_id: resolvedProjectId
4732
5338
  });
4733
5339
  const existingPosts = postsError ? [] : postsResult?.posts ?? [];
4734
5340
  const gapMs = min_gap_hours * 60 * 60 * 1e3;
@@ -4768,19 +5374,17 @@ Created with Social Neuron`;
4768
5374
  const slots = candidates.filter((s) => !s.conflict).sort((a, b) => b.engagement_score - a.engagement_score).slice(0, count);
4769
5375
  const conflictsAvoided = candidates.filter((s) => s.conflict).length;
4770
5376
  if (response_format === "json") {
5377
+ const structuredContent = asEnvelope3({
5378
+ slots,
5379
+ total_candidates: candidates.length,
5380
+ conflicts_avoided: conflictsAvoided
5381
+ });
4771
5382
  return {
5383
+ structuredContent,
4772
5384
  content: [
4773
5385
  {
4774
5386
  type: "text",
4775
- text: JSON.stringify(
4776
- asEnvelope3({
4777
- slots,
4778
- total_candidates: candidates.length,
4779
- conflicts_avoided: conflictsAvoided
4780
- }),
4781
- null,
4782
- 2
4783
- )
5387
+ text: JSON.stringify(structuredContent, null, 2)
4784
5388
  }
4785
5389
  ],
4786
5390
  isError: false
@@ -5145,7 +5749,7 @@ Created with Social Neuron`;
5145
5749
  const results = [];
5146
5750
  const buildIdempotencyKey = (post) => {
5147
5751
  const planId = effectivePlanId ?? (typeof workingPlan.plan_id === "string" ? String(workingPlan.plan_id) : "inline");
5148
- const captionHash = createHash2("sha256").update(post.caption).digest("hex").slice(0, 16);
5752
+ const captionHash = createHash("sha256").update(post.caption).digest("hex").slice(0, 16);
5149
5753
  const raw = [
5150
5754
  "schedule_content_plan",
5151
5755
  planId,
@@ -5155,7 +5759,7 @@ Created with Social Neuron`;
5155
5759
  captionHash,
5156
5760
  idempotency_seed ?? ""
5157
5761
  ].join(":");
5158
- return `plan-${createHash2("sha256").update(raw).digest("hex").slice(0, 48)}`;
5762
+ return `plan-${createHash("sha256").update(raw).digest("hex").slice(0, 48)}`;
5159
5763
  };
5160
5764
  const scheduleOne = async (post) => {
5161
5765
  if (!post.schedule_at) {
@@ -5341,7 +5945,7 @@ Created with Social Neuron`;
5341
5945
  }
5342
5946
  );
5343
5947
  }
5344
- var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING;
5948
+ var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
5345
5949
  var init_distribution = __esm({
5346
5950
  "src/tools/distribution.ts"() {
5347
5951
  "use strict";
@@ -5362,8 +5966,29 @@ var init_distribution = __esm({
5362
5966
  threads: "Threads",
5363
5967
  bluesky: "Bluesky"
5364
5968
  };
5365
- TIKTOK_AUDIT_APPROVED = false;
5969
+ TIKTOK_AUDIT_APPROVED = !["false", "0", "no"].includes(
5970
+ (process.env.TIKTOK_AUDIT_APPROVED ?? "").toLowerCase()
5971
+ );
5366
5972
  MCP_NOT_LIVE_FOR_POSTING = /* @__PURE__ */ new Set(["LinkedIn", "Pinterest", "Reddit"]);
5973
+ VIDEO_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
5974
+ "mp4",
5975
+ "mov",
5976
+ "m4v",
5977
+ "webm",
5978
+ "avi",
5979
+ "mkv",
5980
+ "mpeg",
5981
+ "mpg"
5982
+ ]);
5983
+ IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
5984
+ "jpg",
5985
+ "jpeg",
5986
+ "png",
5987
+ "webp",
5988
+ "gif",
5989
+ "avif",
5990
+ "heic"
5991
+ ]);
5367
5992
  }
5368
5993
  });
5369
5994
 
@@ -5644,7 +6269,10 @@ function registerMediaTools(server2) {
5644
6269
  content: [
5645
6270
  {
5646
6271
  type: "text",
5647
- text: `R2 upload failed (HTTP ${putResp.status}): ${await putResp.text().catch(() => "Unknown error")}`
6272
+ // Presigned-store bodies may include provider request IDs,
6273
+ // bucket names, or signed URL fragments. Status is enough
6274
+ // for user recovery and safe diagnostics.
6275
+ text: `R2 upload failed (HTTP ${putResp.status}). Please retry.`
5648
6276
  }
5649
6277
  ],
5650
6278
  isError: true
@@ -5856,7 +6484,7 @@ function asEnvelope4(data) {
5856
6484
  function registerAnalyticsTools(server2) {
5857
6485
  server2.tool(
5858
6486
  "fetch_analytics",
5859
- "Get post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
6487
+ "Get project-scoped post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
5860
6488
  {
5861
6489
  platform: z5.enum([
5862
6490
  "youtube",
@@ -5874,19 +6502,26 @@ function registerAnalyticsTools(server2) {
5874
6502
  content_id: z5.string().uuid().optional().describe(
5875
6503
  "Filter to a specific content_history ID to see performance of one piece of content."
5876
6504
  ),
6505
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
5877
6506
  limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
5878
6507
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5879
6508
  },
5880
- async ({ platform: platform3, days, content_id, limit, response_format }) => {
6509
+ async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
5881
6510
  const format = response_format ?? "text";
5882
6511
  const lookbackDays = days ?? 30;
5883
6512
  const maxPosts = limit ?? 20;
6513
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5884
6514
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5885
6515
  action: "analytics",
5886
6516
  platform: platform3,
5887
6517
  days: lookbackDays,
5888
- limit: maxPosts,
5889
- contentId: content_id
6518
+ // Fetch extra snapshots, then deduplicate to the requested post count.
6519
+ // The backend understands latestOnly after the paired application
6520
+ // deployment; older deployments safely ignore the hint.
6521
+ limit: Math.min(maxPosts * 5, 100),
6522
+ latestOnly: true,
6523
+ contentId: content_id,
6524
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
5890
6525
  });
5891
6526
  if (efError) {
5892
6527
  return {
@@ -5894,7 +6529,14 @@ function registerAnalyticsTools(server2) {
5894
6529
  isError: true
5895
6530
  };
5896
6531
  }
5897
- const rows = result?.rows ?? [];
6532
+ const snapshotRows = result?.rows ?? [];
6533
+ const seenSnapshots = /* @__PURE__ */ new Set();
6534
+ const rows = [...snapshotRows].sort((a, b) => b.captured_at.localeCompare(a.captured_at)).filter((row) => {
6535
+ const key = `${row.post_id}:${row.platform.toLowerCase()}`;
6536
+ if (seenSnapshots.has(key)) return false;
6537
+ seenSnapshots.add(key);
6538
+ return true;
6539
+ }).slice(0, maxPosts);
5898
6540
  if (rows.length === 0) {
5899
6541
  if (format === "json") {
5900
6542
  const structuredContent = asEnvelope4({
@@ -5956,14 +6598,19 @@ function registerAnalyticsTools(server2) {
5956
6598
  );
5957
6599
  server2.tool(
5958
6600
  "refresh_platform_analytics",
5959
- "Queue analytics refresh jobs for all posts from the last 7 days across connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
6601
+ "Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
5960
6602
  {
6603
+ project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
5961
6604
  response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5962
6605
  },
5963
- async ({ response_format }) => {
6606
+ async ({ project_id, response_format }) => {
5964
6607
  const format = response_format ?? "text";
5965
6608
  const userId = await getDefaultUserId();
5966
- const rateLimit = checkRateLimit("posting", `refresh_platform_analytics:${userId}`);
6609
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
6610
+ const rateLimit = checkRateLimit(
6611
+ "posting",
6612
+ `refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
6613
+ );
5967
6614
  if (!rateLimit.allowed) {
5968
6615
  return {
5969
6616
  content: [
@@ -5975,7 +6622,10 @@ function registerAnalyticsTools(server2) {
5975
6622
  isError: true
5976
6623
  };
5977
6624
  }
5978
- const { data, error } = await callEdgeFunction("fetch-analytics", { userId });
6625
+ const { data, error } = await callEdgeFunction("fetch-analytics", {
6626
+ userId,
6627
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
6628
+ });
5979
6629
  if (error) {
5980
6630
  return {
5981
6631
  content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
@@ -6004,7 +6654,8 @@ function registerAnalyticsTools(server2) {
6004
6654
  success: true,
6005
6655
  postsProcessed: result.postsProcessed,
6006
6656
  queued,
6007
- errored
6657
+ errored,
6658
+ projectId: resolvedProjectId ?? null
6008
6659
  });
6009
6660
  return {
6010
6661
  structuredContent,
@@ -6064,6 +6715,84 @@ var init_analytics = __esm({
6064
6715
  }
6065
6716
  });
6066
6717
 
6718
+ // src/lib/brandUrlInput.ts
6719
+ function canonicalizeHost(hostname) {
6720
+ return hostname.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, "");
6721
+ }
6722
+ function hostLooksLikeDomain(hostname) {
6723
+ return /\S+\.\S+/.test(canonicalizeHost(hostname));
6724
+ }
6725
+ function resolveHandle(handle, platform3) {
6726
+ const normalized = handle.replace(/^@/, "").trim();
6727
+ if (!normalized) return { url: null, handle: null, platform: null, ambiguous: false };
6728
+ if (!platform3) return { url: null, handle: normalized, platform: null, ambiguous: true };
6729
+ return {
6730
+ url: PROFILE_URL_BUILDERS[platform3](normalized),
6731
+ handle: normalized,
6732
+ platform: platform3,
6733
+ ambiguous: false
6734
+ };
6735
+ }
6736
+ function normalizeBrandUrlInput(raw, explicitPlatform) {
6737
+ const input = (raw ?? "").trim();
6738
+ if (!input) return { url: null, handle: null, platform: null, ambiguous: false };
6739
+ if (/^https?:\/\//i.test(input)) {
6740
+ let parsed;
6741
+ try {
6742
+ parsed = new URL(input);
6743
+ } catch {
6744
+ return { url: null, handle: null, platform: null, ambiguous: false, invalidUrl: true };
6745
+ }
6746
+ if (hostLooksLikeDomain(parsed.hostname)) {
6747
+ return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
6748
+ }
6749
+ return resolveHandle(canonicalizeHost(parsed.hostname), explicitPlatform);
6750
+ }
6751
+ const qualified = input.match(/^([a-z]{1,10}):\s*@?(.+)$/i);
6752
+ if (qualified) {
6753
+ const platform3 = PLATFORM_ALIASES[qualified[1].toLowerCase()];
6754
+ if (platform3) return resolveHandle(qualified[2], platform3);
6755
+ }
6756
+ if (input.startsWith("@")) return resolveHandle(input, explicitPlatform);
6757
+ if (!/\s/.test(input)) {
6758
+ try {
6759
+ const parsed = new URL(`https://${input}`);
6760
+ if (hostLooksLikeDomain(parsed.hostname)) {
6761
+ return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
6762
+ }
6763
+ } catch {
6764
+ }
6765
+ }
6766
+ return resolveHandle(input.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, ""), explicitPlatform);
6767
+ }
6768
+ var PLATFORM_ALIASES, PROFILE_URL_BUILDERS, UNICODE_DOT_VARIANTS;
6769
+ var init_brandUrlInput = __esm({
6770
+ "src/lib/brandUrlInput.ts"() {
6771
+ "use strict";
6772
+ PLATFORM_ALIASES = {
6773
+ instagram: "instagram",
6774
+ ig: "instagram",
6775
+ insta: "instagram",
6776
+ tiktok: "tiktok",
6777
+ tt: "tiktok",
6778
+ twitter: "twitter",
6779
+ x: "twitter",
6780
+ linkedin: "linkedin",
6781
+ li: "linkedin",
6782
+ youtube: "youtube",
6783
+ yt: "youtube"
6784
+ };
6785
+ PROFILE_URL_BUILDERS = {
6786
+ instagram: (handle) => `https://instagram.com/${encodeURIComponent(handle)}`,
6787
+ tiktok: (handle) => `https://tiktok.com/@${encodeURIComponent(handle)}`,
6788
+ twitter: (handle) => `https://x.com/${encodeURIComponent(handle)}`,
6789
+ linkedin: (handle) => `https://linkedin.com/company/${encodeURIComponent(handle)}`,
6790
+ youtube: (handle) => `https://youtube.com/@${encodeURIComponent(handle)}`
6791
+ };
6792
+ UNICODE_DOT_VARIANTS = /[。.。]/g;
6793
+ }
6794
+ });
6795
+
6067
6796
  // src/tools/brand.ts
6068
6797
  import { z as z6 } from "zod";
6069
6798
  function asEnvelope5(data) {
@@ -6080,12 +6809,31 @@ function registerBrandTools(server2) {
6080
6809
  "extract_brand",
6081
6810
  "Analyze a website URL and extract brand identity data including brand name, colors, voice/tone, target audience, and logo. Uses AI-powered analysis of the page HTML. Useful for understanding a brand before generating content for it.",
6082
6811
  {
6083
- url: z6.string().url().describe(
6084
- 'The website URL to analyze for brand identity (e.g. "https://example.com").'
6812
+ url: z6.string().min(1).max(2048).describe(
6813
+ 'The website URL to analyze for brand identity (e.g. "https://example.com"). Bare handles are rejected; supply a full profile URL or use "platform:handle" shorthand (e.g. "instagram:acmefoods").'
6085
6814
  ),
6086
6815
  response_format: z6.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6087
6816
  },
6088
- async ({ url, response_format }) => {
6817
+ async ({ url: rawUrl, response_format }) => {
6818
+ const normalized = normalizeBrandUrlInput(rawUrl);
6819
+ if (normalized.invalidUrl) {
6820
+ return {
6821
+ content: [{ type: "text", text: `URL blocked: "${rawUrl}" is not a valid URL.` }],
6822
+ isError: true
6823
+ };
6824
+ }
6825
+ if (!normalized.url) {
6826
+ return {
6827
+ content: [
6828
+ {
6829
+ type: "text",
6830
+ text: `"${rawUrl}" looks like a handle, not a full URL. Provide a profile URL or use "platform:handle" shorthand (for example "instagram:acmefoods").`
6831
+ }
6832
+ ],
6833
+ isError: true
6834
+ };
6835
+ }
6836
+ const url = normalized.url;
6089
6837
  const ssrfCheck = await validateUrlForSSRF(url);
6090
6838
  if (!ssrfCheck.isValid) {
6091
6839
  return {
@@ -6393,6 +7141,7 @@ var init_brand = __esm({
6393
7141
  init_supabase();
6394
7142
  init_ssrf();
6395
7143
  init_version();
7144
+ init_brandUrlInput();
6396
7145
  }
6397
7146
  });
6398
7147
 
@@ -6768,7 +7517,7 @@ function registerRemotionTools(server2) {
6768
7517
  },
6769
7518
  async ({ composition_id, output_format, props }) => {
6770
7519
  const userId = await getDefaultUserId();
6771
- const rateLimit = checkRateLimit("screenshot", `render_demo_video:${userId}`);
7520
+ const rateLimit = checkRateLimit("generation", `render_demo_video:${userId}`);
6772
7521
  if (!rateLimit.allowed) {
6773
7522
  return {
6774
7523
  content: [
@@ -7107,22 +7856,25 @@ function asEnvelope6(data) {
7107
7856
  function registerInsightsTools(server2) {
7108
7857
  server2.tool(
7109
7858
  "get_performance_insights",
7110
- "Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
7859
+ "Query project-scoped performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
7111
7860
  {
7112
7861
  insight_type: z9.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
7113
7862
  days: z9.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
7114
7863
  limit: z9.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
7864
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
7115
7865
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7116
7866
  },
7117
- async ({ insight_type, days, limit, response_format }) => {
7867
+ async ({ insight_type, days, limit, project_id, response_format }) => {
7118
7868
  const format = response_format ?? "text";
7119
7869
  const lookbackDays = days ?? 30;
7120
7870
  const maxRows = limit ?? 10;
7121
7871
  const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
7872
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7122
7873
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7123
7874
  action: "performance-insights",
7124
7875
  days: effectiveDays,
7125
- limit: maxRows
7876
+ limit: maxRows,
7877
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7126
7878
  });
7127
7879
  if (efError || !result?.success) {
7128
7880
  return {
@@ -7209,19 +7961,22 @@ function registerInsightsTools(server2) {
7209
7961
  );
7210
7962
  server2.tool(
7211
7963
  "get_best_posting_times",
7212
- "Analyze post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
7964
+ "Analyze project-scoped post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
7213
7965
  {
7214
7966
  platform: z9.enum(PLATFORM_ENUM).optional().describe("Filter to a specific platform."),
7215
7967
  days: z9.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
7968
+ project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
7216
7969
  response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7217
7970
  },
7218
- async ({ platform: platform3, days, response_format }) => {
7971
+ async ({ platform: platform3, days, project_id, response_format }) => {
7219
7972
  const format = response_format ?? "text";
7220
7973
  const lookbackDays = days ?? 30;
7974
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
7221
7975
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
7222
7976
  action: "best-posting-times",
7223
7977
  days: lookbackDays,
7224
- platform: platform3 ?? void 0
7978
+ platform: platform3 ?? void 0,
7979
+ ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7225
7980
  });
7226
7981
  if (efError || !result?.success) {
7227
7982
  return {
@@ -7335,6 +8090,7 @@ var init_insights = __esm({
7335
8090
  "src/tools/insights.ts"() {
7336
8091
  "use strict";
7337
8092
  init_edge_function();
8093
+ init_supabase();
7338
8094
  init_version();
7339
8095
  MAX_INSIGHT_AGE_DAYS = 30;
7340
8096
  PLATFORM_ENUM = [
@@ -8378,7 +9134,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
8378
9134
  }
8379
9135
  const statusData = {
8380
9136
  activeConfigs: result?.activeConfigs ?? 0,
8381
- recentRuns: [],
9137
+ recentRuns: result?.recentRuns ?? result?.recent_runs ?? [],
8382
9138
  pendingApprovals: result?.pendingApprovals ?? 0
8383
9139
  };
8384
9140
  if (format === "json") {
@@ -8400,8 +9156,20 @@ ${"=".repeat(40)}
8400
9156
  text += `Pending Approvals: ${statusData.pendingApprovals}
8401
9157
 
8402
9158
  `;
8403
- text += `No recent runs.
9159
+ if (statusData.recentRuns.length === 0) {
9160
+ text += `No recent runs.
9161
+ `;
9162
+ } else {
9163
+ text += `Recent Runs (${statusData.recentRuns.length}):
8404
9164
  `;
9165
+ for (const run of statusData.recentRuns.slice(0, 10)) {
9166
+ const id = String(run.id ?? run.run_id ?? "unknown");
9167
+ const status = String(run.status ?? "unknown");
9168
+ const credits = run.credits_used ?? run.creditsUsed;
9169
+ text += ` ${id}: ${status}${credits == null ? "" : ` (${String(credits)} credits)`}
9170
+ `;
9171
+ }
9172
+ }
8405
9173
  return {
8406
9174
  content: [{ type: "text", text }]
8407
9175
  };
@@ -10583,7 +11351,9 @@ function toolKnowledgeDocument(tool) {
10583
11351
  function getKnowledgeDocuments() {
10584
11352
  return [
10585
11353
  ...STATIC_KNOWLEDGE_DOCUMENTS,
10586
- ...TOOL_CATALOG.filter((t) => !t.internal).map(toolKnowledgeDocument)
11354
+ ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
11355
+ toolKnowledgeDocument
11356
+ )
10587
11357
  ];
10588
11358
  }
10589
11359
  function tokenize(input) {
@@ -10700,7 +11470,7 @@ function registerDiscoveryTools(server2) {
10700
11470
  if (query) {
10701
11471
  results = searchTools(query);
10702
11472
  }
10703
- results = results.filter((t) => !t.internal);
11473
+ results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
10704
11474
  if (module) {
10705
11475
  const moduleTools = getToolsByModule(module);
10706
11476
  results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
@@ -12631,11 +13401,11 @@ function hexToLab(hex) {
12631
13401
  const lb = srgbToLinear(b);
12632
13402
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
12633
13403
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
12634
- const z39 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
13404
+ const z41 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
12635
13405
  const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
12636
13406
  const fx = f(x / 0.95047);
12637
13407
  const fy = f(y / 1);
12638
- const fz = f(z39 / 1.08883);
13408
+ const fz = f(z41 / 1.08883);
12639
13409
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
12640
13410
  }
12641
13411
  function deltaE2000(lab1, lab2) {
@@ -12875,10 +13645,10 @@ function registerBrandRuntimeTools(server2) {
12875
13645
  pagesScraped: meta.pagesScraped || 0
12876
13646
  }
12877
13647
  };
12878
- const envelope = asEnvelope23(runtime);
13648
+ const envelope2 = asEnvelope23(runtime);
12879
13649
  return {
12880
- structuredContent: envelope,
12881
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13650
+ structuredContent: envelope2,
13651
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
12882
13652
  };
12883
13653
  }
12884
13654
  );
@@ -13018,9 +13788,9 @@ function registerBrandRuntimeTools(server2) {
13018
13788
  }
13019
13789
  const profile = row.profile_data;
13020
13790
  const checkResult = computeBrandConsistency(content, profile);
13021
- const envelope = asEnvelope23(checkResult);
13791
+ const envelope2 = asEnvelope23(checkResult);
13022
13792
  return {
13023
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13793
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
13024
13794
  };
13025
13795
  }
13026
13796
  );
@@ -13052,9 +13822,9 @@ function registerBrandRuntimeTools(server2) {
13052
13822
  content_colors,
13053
13823
  threshold ?? 10
13054
13824
  );
13055
- const envelope = asEnvelope23(auditResult);
13825
+ const envelope2 = asEnvelope23(auditResult);
13056
13826
  return {
13057
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13827
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
13058
13828
  };
13059
13829
  }
13060
13830
  );
@@ -13087,9 +13857,9 @@ function registerBrandRuntimeTools(server2) {
13087
13857
  row.profile_data.typography,
13088
13858
  format
13089
13859
  );
13090
- const envelope = asEnvelope23({ format, tokens: output });
13860
+ const envelope2 = asEnvelope23({ format, tokens: output });
13091
13861
  return {
13092
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13862
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
13093
13863
  };
13094
13864
  }
13095
13865
  );
@@ -13243,7 +14013,7 @@ function registerCarouselTools(server2) {
13243
14013
  };
13244
14014
  }
13245
14015
  const userId = await getDefaultUserId();
13246
- const rateLimit = checkRateLimit("posting", `create_carousel:${userId}`);
14016
+ const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
13247
14017
  if (!rateLimit.allowed) {
13248
14018
  return {
13249
14019
  content: [
@@ -13315,7 +14085,12 @@ function registerCarouselTools(server2) {
13315
14085
  slideNumber: slide.slideNumber,
13316
14086
  jobId: null,
13317
14087
  model: image_model,
13318
- error: error ?? "No job ID returned"
14088
+ error: error ?? "No job ID returned",
14089
+ creditsReserved: data?.credits_reserved ?? null,
14090
+ creditsCharged: data?.credits_charged ?? null,
14091
+ creditsRefunded: data?.credits_refunded ?? null,
14092
+ billingStatus: data?.billing_status ?? "unknown",
14093
+ failureReason: data?.failure_reason ?? null
13319
14094
  };
13320
14095
  }
13321
14096
  const jobId = data.asyncJobId ?? data.taskId ?? null;
@@ -13327,14 +14102,24 @@ function registerCarouselTools(server2) {
13327
14102
  slideNumber: slide.slideNumber,
13328
14103
  jobId,
13329
14104
  model: image_model,
13330
- error: null
14105
+ error: null,
14106
+ creditsReserved: 0,
14107
+ creditsCharged: data.creditsDeducted ?? perImageCost,
14108
+ creditsRefunded: 0,
14109
+ billingStatus: "charged",
14110
+ failureReason: null
13331
14111
  };
13332
14112
  } catch (err) {
13333
14113
  return {
13334
14114
  slideNumber: slide.slideNumber,
13335
14115
  jobId: null,
13336
14116
  model: image_model,
13337
- error: sanitizeError(err)
14117
+ error: sanitizeError(err),
14118
+ creditsReserved: null,
14119
+ creditsCharged: null,
14120
+ creditsRefunded: null,
14121
+ billingStatus: "unknown",
14122
+ failureReason: null
13338
14123
  };
13339
14124
  }
13340
14125
  })
@@ -13359,7 +14144,8 @@ function registerCarouselTools(server2) {
13359
14144
  return {
13360
14145
  ...s,
13361
14146
  imageJobId: job?.jobId ?? null,
13362
- imageError: job?.error ?? null
14147
+ imageError: job?.error ?? null,
14148
+ imageBillingStatus: job?.billingStatus ?? "unknown"
13363
14149
  };
13364
14150
  }),
13365
14151
  imageModel: image_model,
@@ -13371,11 +14157,19 @@ function registerCarouselTools(server2) {
13371
14157
  jobIds: successfulJobs.map((j) => j.jobId),
13372
14158
  failedSlides: failedJobs.map((j) => ({
13373
14159
  slideNumber: j.slideNumber,
13374
- error: j.error
14160
+ error: j.error,
14161
+ credits_reserved: j.creditsReserved,
14162
+ credits_charged: j.creditsCharged,
14163
+ credits_refunded: j.creditsRefunded,
14164
+ billing_status: j.billingStatus,
14165
+ failure_reason: j.failureReason
13375
14166
  })),
13376
14167
  credits: {
13377
14168
  textGeneration: textCredits,
13378
14169
  imagesEstimated: successfulJobs.length * perImageCost,
14170
+ imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
14171
+ imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
14172
+ billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
13379
14173
  totalEstimated: textCredits + successfulJobs.length * perImageCost
13380
14174
  }
13381
14175
  }
@@ -13571,7 +14365,7 @@ function registerHyperframesTools(server2) {
13571
14365
  );
13572
14366
  server2.tool(
13573
14367
  "render_hyperframes",
13574
- "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Returns a job ID \u2014 poll with check_status. Note: F4 v1 ships the EF + scaffold; the worker container needs the hyperframes runtime installed (Phase 2) before this returns finished MP4s.",
14368
+ "Render an HTML video composition (Hyperframes) to MP4 \u2014 frame-accurate, no React build step. The page MUST expose window.__hf = { duration: <seconds>, seek: (t) => void }; the renderer calls seek(t) per frame (GSAP timelines work when driven from seek). Missing window.__hf causes a terminal timeout; poll check_status and verify its reported billing/refund outcome. Use list_hyperframes_blocks for the pre-built block catalog. Pass project_id to keep the render with the correct brand/project. Returns a job ID \u2014 poll with check_status.",
13575
14369
  {
13576
14370
  composition_html: z30.string().max(5e5).optional().describe(
13577
14371
  "Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
@@ -13583,7 +14377,9 @@ function registerHyperframesTools(server2) {
13583
14377
  aspect_ratio: z30.enum(["9:16", "16:9", "1:1"]).optional().describe('Output aspect ratio. Default "9:16".'),
13584
14378
  duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
13585
14379
  fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
13586
- quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master.")
14380
+ quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
14381
+ project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with."),
14382
+ response_format: z30.enum(["text", "json"]).optional().describe("Response format. Use json for a stable job_id handoff.")
13587
14383
  },
13588
14384
  async ({
13589
14385
  composition_html,
@@ -13592,7 +14388,9 @@ function registerHyperframesTools(server2) {
13592
14388
  aspect_ratio,
13593
14389
  duration_sec,
13594
14390
  fps,
13595
- quality
14391
+ quality,
14392
+ project_id,
14393
+ response_format
13596
14394
  }) => {
13597
14395
  const userId = await getDefaultUserId();
13598
14396
  const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
@@ -13637,23 +14435,36 @@ function registerHyperframesTools(server2) {
13637
14435
  aspectRatio: aspect_ratio || "9:16",
13638
14436
  durationSec: duration_sec,
13639
14437
  fps: fps || 30,
13640
- quality: quality || "standard"
14438
+ quality: quality || "standard",
14439
+ ...project_id && { projectId: project_id }
13641
14440
  });
13642
14441
  if (error || !data?.jobId) {
13643
14442
  throw new Error(error || data?.error || "Failed to create Hyperframes render job");
13644
14443
  }
14444
+ const payload = {
14445
+ job_id: data.jobId,
14446
+ jobId: data.jobId,
14447
+ status: data.status,
14448
+ credits_cost: data.creditsCost,
14449
+ credits: data.creditsCost,
14450
+ duration_sec,
14451
+ fps: fps || 30,
14452
+ aspect_ratio: aspect_ratio || "9:16",
14453
+ quality: quality || "standard",
14454
+ project_id: project_id || null
14455
+ };
13645
14456
  return {
13646
14457
  content: [
13647
14458
  {
13648
14459
  type: "text",
13649
- text: [
14460
+ text: response_format === "json" ? JSON.stringify({ data: payload }) : [
13650
14461
  `Hyperframes render job queued.`,
13651
14462
  ` Job ID: ${data.jobId}`,
13652
14463
  ` Credits: ${data.creditsCost}`,
13653
14464
  ` Duration: ${duration_sec}s @ ${fps || 30}fps (${aspect_ratio || "9:16"})`,
13654
14465
  ` Quality: ${quality || "standard"}`,
13655
14466
  ``,
13656
- `Poll with check_status. Note: until F4 Phase 2 wires the runtime, the job will fail with a clear "HyperframesNotInstalled" message.`
14467
+ `Poll with check_status.`
13657
14468
  ].join("\n")
13658
14469
  }
13659
14470
  ]
@@ -13663,7 +14474,7 @@ function registerHyperframesTools(server2) {
13663
14474
  content: [
13664
14475
  {
13665
14476
  type: "text",
13666
- text: `Failed to queue Hyperframes render: ${err instanceof Error ? err.message : "Unknown error"}`
14477
+ text: `Failed to queue Hyperframes render: ${sanitizeError(err)}`
13667
14478
  }
13668
14479
  ],
13669
14480
  isError: true
@@ -13679,6 +14490,7 @@ var init_hyperframes = __esm({
13679
14490
  init_rate_limit();
13680
14491
  init_supabase();
13681
14492
  init_edge_function();
14493
+ init_sanitize_error();
13682
14494
  HYPERFRAMES_BLOCKS = [
13683
14495
  // Transitions
13684
14496
  {
@@ -13796,49 +14608,122 @@ import {
13796
14608
  import { z as z31 } from "zod";
13797
14609
  import fs from "node:fs/promises";
13798
14610
  import path from "node:path";
14611
+ import { fileURLToPath } from "node:url";
13799
14612
  function startOfCurrentWeekMonday() {
13800
14613
  const now = /* @__PURE__ */ new Date();
13801
- const day = now.getDay();
14614
+ const day = now.getUTCDay();
13802
14615
  const monday = new Date(now);
13803
- monday.setUTCDate(now.getUTCDate() - day + (day === 0 ? -6 : 1));
14616
+ monday.setUTCDate(now.getUTCDate() - (day + 6) % 7);
14617
+ monday.setUTCHours(0, 0, 0, 0);
13804
14618
  return monday.toISOString().split("T")[0];
13805
14619
  }
14620
+ function endOfWeek(startDate) {
14621
+ const end = /* @__PURE__ */ new Date(`${startDate}T00:00:00.000Z`);
14622
+ end.setUTCDate(end.getUTCDate() + 7);
14623
+ end.setUTCMilliseconds(end.getUTCMilliseconds() - 1);
14624
+ return end.toISOString();
14625
+ }
14626
+ function isStrictIsoDate(value) {
14627
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
14628
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
14629
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
14630
+ }
14631
+ function publicRecentPost(value) {
14632
+ if (!value || typeof value !== "object") return null;
14633
+ const row = value;
14634
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
14635
+ return null;
14636
+ }
14637
+ const optional = (name) => typeof row[name] === "string" ? row[name] : null;
14638
+ return {
14639
+ id: row.id,
14640
+ platform: row.platform,
14641
+ status: row.status,
14642
+ title: optional("title"),
14643
+ external_post_id: optional("external_post_id"),
14644
+ published_at: optional("published_at"),
14645
+ scheduled_at: optional("scheduled_at"),
14646
+ created_at: row.created_at
14647
+ };
14648
+ }
14649
+ function calendarHtmlCandidates() {
14650
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
14651
+ return [
14652
+ // Bundled HTTP entry: import.meta.url is dist/http.js.
14653
+ path.join(moduleDir, "apps/content-calendar/mcp-app.html"),
14654
+ // Source/tsx/vitest: import.meta.url is src/apps/content-calendar.ts.
14655
+ path.resolve(moduleDir, "../../dist/apps/content-calendar/mcp-app.html"),
14656
+ // Local development fallback; never the sole package path.
14657
+ path.resolve(process.cwd(), "dist/apps/content-calendar/mcp-app.html")
14658
+ ];
14659
+ }
14660
+ async function readCalendarHtml() {
14661
+ for (const candidate of calendarHtmlCandidates()) {
14662
+ try {
14663
+ return await fs.readFile(candidate, "utf-8");
14664
+ } catch (error) {
14665
+ if (error.code !== "ENOENT") throw error;
14666
+ }
14667
+ }
14668
+ throw new Error("calendar_bundle_missing");
14669
+ }
13806
14670
  function registerContentCalendarApp(server2) {
13807
14671
  registerAppTool(
13808
14672
  server2,
13809
14673
  "open_content_calendar",
13810
14674
  {
13811
14675
  title: "Content Calendar",
13812
- description: "Open an interactive drag-drop calendar showing the user's scheduled posts for the current week. Users can reschedule via drag, filter by platform, drill into any post, or quick-create a new post. Backed by list_recent_posts, schedule_post, and find_next_slots \u2014 no new tools needed.",
14676
+ description: "Open a project-scoped interactive calendar for the current week. Users can filter, inspect, quick-create, suggest a slot, and reschedule pending posts with optimistic conflict protection.",
13813
14677
  inputSchema: {
13814
- start_date: z31.string().optional().describe(
14678
+ project_id: z31.string().uuid().optional().describe(
14679
+ "Brand/project ID. Defaults to the authenticated key's project or the account default."
14680
+ ),
14681
+ start_date: z31.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
13815
14682
  "ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
13816
14683
  )
13817
14684
  },
13818
14685
  outputSchema: {
13819
14686
  start_date: z31.string(),
14687
+ project_id: z31.string(),
13820
14688
  posts: z31.array(RecentPostOutputSchema),
13821
14689
  scopes: z31.array(z31.string())
13822
14690
  },
13823
14691
  _meta: {
13824
14692
  ui: {
13825
- resourceUri: CALENDAR_URI,
13826
- csp: {
13827
- "img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
13828
- "connect-src": ["'self'"]
13829
- }
14693
+ resourceUri: CALENDAR_URI
13830
14694
  }
13831
14695
  }
13832
14696
  },
13833
- async ({ start_date }, extra) => {
13834
- const userScopes = extra.authInfo?.scopes ?? [];
14697
+ async ({ project_id, start_date }) => {
14698
+ const userScopes = getRequestScopes() ?? getAuthenticatedScopes();
14699
+ const resolvedProjectId = project_id ?? await getDefaultProjectId();
14700
+ if (!resolvedProjectId) {
14701
+ return {
14702
+ content: [
14703
+ {
14704
+ type: "text",
14705
+ text: "No project_id was provided and no default project is configured."
14706
+ }
14707
+ ],
14708
+ isError: true
14709
+ };
14710
+ }
13835
14711
  const fromDate = start_date ?? startOfCurrentWeekMonday();
14712
+ if (!isStrictIsoDate(fromDate)) {
14713
+ return {
14714
+ content: [{ type: "text", text: "start_date must be a valid YYYY-MM-DD date." }],
14715
+ isError: true
14716
+ };
14717
+ }
13836
14718
  const { data: result, error } = await callEdgeFunction(
13837
14719
  "mcp-data",
13838
14720
  {
13839
- action: "recent-posts",
13840
- days: 14,
13841
- limit: 50
14721
+ action: "scheduled-posts",
14722
+ start_date: `${fromDate}T00:00:00.000Z`,
14723
+ end_date: endOfWeek(fromDate),
14724
+ statuses: ["pending", "scheduled", "draft"],
14725
+ projectId: resolvedProjectId,
14726
+ project_id: resolvedProjectId
13842
14727
  },
13843
14728
  { timeoutMs: 15e3 }
13844
14729
  );
@@ -13847,19 +14732,16 @@ function registerContentCalendarApp(server2) {
13847
14732
  content: [
13848
14733
  {
13849
14734
  type: "text",
13850
- text: `Failed to load posts: ${error || result?.error || "Unknown error"}`
14735
+ text: "The content calendar could not load posts. Please retry."
13851
14736
  }
13852
14737
  ],
13853
14738
  isError: true
13854
14739
  };
13855
14740
  }
13856
- const posts = (result.posts ?? []).filter((p) => {
13857
- const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
13858
- if (!ts) return false;
13859
- return ts.split("T")[0] >= fromDate;
13860
- });
14741
+ const posts = (result.posts ?? []).map(publicRecentPost).filter((post) => post !== null);
13861
14742
  const structuredContent = {
13862
14743
  start_date: fromDate,
14744
+ project_id: resolvedProjectId,
13863
14745
  posts,
13864
14746
  scopes: userScopes
13865
14747
  };
@@ -13878,37 +14760,60 @@ function registerContentCalendarApp(server2) {
13878
14760
  server2,
13879
14761
  CALENDAR_URI,
13880
14762
  CALENDAR_URI,
13881
- { mimeType: RESOURCE_MIME_TYPE },
14763
+ {
14764
+ mimeType: RESOURCE_MIME_TYPE,
14765
+ description: "Self-contained Social Neuron project content calendar.",
14766
+ _meta: { ui: { csp: CALENDAR_CSP } }
14767
+ },
13882
14768
  async () => {
13883
- const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
13884
14769
  try {
13885
- const html = await fs.readFile(htmlPath, "utf-8");
14770
+ const html = await readCalendarHtml();
13886
14771
  return {
13887
- contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: html }]
14772
+ contents: [
14773
+ {
14774
+ uri: CALENDAR_URI,
14775
+ mimeType: RESOURCE_MIME_TYPE,
14776
+ text: html,
14777
+ _meta: { ui: { csp: CALENDAR_CSP } }
14778
+ }
14779
+ ]
13888
14780
  };
13889
- } catch (err) {
14781
+ } catch {
13890
14782
  const errorHtml = `<!DOCTYPE html>
13891
14783
  <html><head><title>Content Calendar \u2014 unavailable</title></head>
13892
14784
  <body style="font-family:sans-serif;padding:24px;color:#444;">
13893
14785
  <h2>Content Calendar app bundle missing</h2>
13894
- <p>The server registered <code>open_content_calendar</code> but
13895
- <code>apps/content-calendar/dist/mcp-app.html</code> is not built.
13896
- Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
13897
- <p style="color:#999;font-size:12px;">${err.message}</p>
14786
+ <p>The interactive bundle is unavailable on this deployment. Please contact Social Neuron support.</p>
13898
14787
  </body></html>`;
13899
14788
  return {
13900
- contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: errorHtml }]
14789
+ contents: [
14790
+ {
14791
+ uri: CALENDAR_URI,
14792
+ mimeType: RESOURCE_MIME_TYPE,
14793
+ text: errorHtml,
14794
+ _meta: { ui: { csp: CALENDAR_CSP } }
14795
+ }
14796
+ ]
13901
14797
  };
13902
14798
  }
13903
14799
  }
13904
14800
  );
13905
14801
  }
13906
- var CALENDAR_URI, RecentPostOutputSchema;
14802
+ var CALENDAR_URI, CALENDAR_CSP, RecentPostOutputSchema;
13907
14803
  var init_content_calendar = __esm({
13908
14804
  "src/apps/content-calendar.ts"() {
13909
14805
  "use strict";
13910
14806
  init_edge_function();
13911
- CALENDAR_URI = "ui://content-calendar/mcp-app.html";
14807
+ init_request_context();
14808
+ init_supabase();
14809
+ CALENDAR_URI = "ui://content-calendar/v1/mcp-app.html";
14810
+ CALENDAR_CSP = {
14811
+ // The HTML is fully self-contained. Tool calls travel over the host bridge,
14812
+ // not fetch/XHR, so the secure default is no network or remote resources.
14813
+ connectDomains: [],
14814
+ resourceDomains: [],
14815
+ frameDomains: []
14816
+ };
13912
14817
  RecentPostOutputSchema = z31.object({
13913
14818
  id: z31.string(),
13914
14819
  platform: z31.string(),
@@ -13922,8 +14827,257 @@ var init_content_calendar = __esm({
13922
14827
  }
13923
14828
  });
13924
14829
 
13925
- // src/tools/connections.ts
14830
+ // src/apps/analytics-pulse.ts
14831
+ import {
14832
+ registerAppResource as registerAppResource2,
14833
+ registerAppTool as registerAppTool2,
14834
+ RESOURCE_MIME_TYPE as RESOURCE_MIME_TYPE2
14835
+ } from "@modelcontextprotocol/ext-apps/server";
13926
14836
  import { z as z32 } from "zod";
14837
+ import fs2 from "node:fs/promises";
14838
+ import path2 from "node:path";
14839
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
14840
+ function finiteMetric(value) {
14841
+ const parsed = Number(value ?? 0);
14842
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
14843
+ }
14844
+ function publicAnalyticsPost(value) {
14845
+ if (!value || typeof value !== "object") return null;
14846
+ const row = value;
14847
+ const post = row.posts && typeof row.posts === "object" ? row.posts : {};
14848
+ const platform3 = typeof row.platform === "string" ? row.platform : typeof post.platform === "string" ? post.platform : null;
14849
+ const capturedAt = typeof row.captured_at === "string" ? row.captured_at : null;
14850
+ if (!platform3 || !capturedAt) return null;
14851
+ const views = finiteMetric(row.views);
14852
+ const likes = finiteMetric(row.likes);
14853
+ const comments = finiteMetric(row.comments);
14854
+ const shares = finiteMetric(row.shares);
14855
+ const engagement = likes + comments + shares;
14856
+ return {
14857
+ platform: platform3,
14858
+ title: typeof post.title === "string" ? post.title : null,
14859
+ views,
14860
+ likes,
14861
+ comments,
14862
+ shares,
14863
+ engagement_rate: views > 0 ? Number((engagement / views * 100).toFixed(2)) : 0,
14864
+ captured_at: capturedAt,
14865
+ published_at: typeof post.published_at === "string" ? post.published_at : null
14866
+ };
14867
+ }
14868
+ function latestAnalyticsRows(values) {
14869
+ const rows = values.filter((value) => Boolean(value) && typeof value === "object").sort((a, b) => String(b.captured_at ?? "").localeCompare(String(a.captured_at ?? "")));
14870
+ const seen = /* @__PURE__ */ new Set();
14871
+ const latest = [];
14872
+ for (const row of rows) {
14873
+ if (typeof row.post_id !== "string" || typeof row.platform !== "string") continue;
14874
+ const key = `${row.post_id}:${row.platform.toLowerCase()}`;
14875
+ if (seen.has(key)) continue;
14876
+ seen.add(key);
14877
+ latest.push(row);
14878
+ }
14879
+ return latest;
14880
+ }
14881
+ function analyticsHtmlCandidates() {
14882
+ const moduleDir = path2.dirname(fileURLToPath2(import.meta.url));
14883
+ return [
14884
+ path2.join(moduleDir, "apps/analytics-pulse/mcp-app.html"),
14885
+ path2.resolve(moduleDir, "../../dist/apps/analytics-pulse/mcp-app.html"),
14886
+ path2.resolve(process.cwd(), "dist/apps/analytics-pulse/mcp-app.html")
14887
+ ];
14888
+ }
14889
+ async function readAnalyticsHtml() {
14890
+ for (const candidate of analyticsHtmlCandidates()) {
14891
+ try {
14892
+ return await fs2.readFile(candidate, "utf-8");
14893
+ } catch (error) {
14894
+ if (error.code !== "ENOENT") throw error;
14895
+ }
14896
+ }
14897
+ throw new Error("analytics_bundle_missing");
14898
+ }
14899
+ function registerAnalyticsPulseApp(server2) {
14900
+ registerAppTool2(
14901
+ server2,
14902
+ "open_analytics_pulse",
14903
+ {
14904
+ title: "Analytics Pulse",
14905
+ description: "Open a project-scoped performance dashboard with views, engagement, platform mix, and top-post metrics. Use it to review results visually before planning the next content cycle.",
14906
+ inputSchema: {
14907
+ project_id: z32.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default."),
14908
+ platform: z32.enum(PLATFORM_VALUES).optional().describe("Optional platform filter."),
14909
+ days: z32.number().int().min(1).max(365).optional().describe("Lookback window. Defaults to 30 days.")
14910
+ },
14911
+ outputSchema: {
14912
+ project_id: z32.string(),
14913
+ platform: z32.string().nullable(),
14914
+ days: z32.number(),
14915
+ summary: z32.object({
14916
+ views: z32.number(),
14917
+ engagement: z32.number(),
14918
+ engagement_rate: z32.number(),
14919
+ posts: z32.number()
14920
+ }),
14921
+ platform_totals: z32.array(
14922
+ z32.object({
14923
+ platform: z32.string(),
14924
+ views: z32.number(),
14925
+ engagement: z32.number(),
14926
+ posts: z32.number()
14927
+ })
14928
+ ),
14929
+ posts: z32.array(AnalyticsPostOutputSchema)
14930
+ },
14931
+ _meta: { ui: { resourceUri: ANALYTICS_URI } }
14932
+ },
14933
+ async ({ project_id, platform: platform3, days }) => {
14934
+ const resolvedProjectId = project_id ?? await getDefaultProjectId();
14935
+ if (!resolvedProjectId) {
14936
+ return {
14937
+ content: [{ type: "text", text: "No project_id was provided and no default project is configured." }],
14938
+ isError: true
14939
+ };
14940
+ }
14941
+ const lookbackDays = days ?? 30;
14942
+ const { data: result, error } = await callEdgeFunction(
14943
+ "mcp-data",
14944
+ {
14945
+ action: "analytics",
14946
+ projectId: resolvedProjectId,
14947
+ project_id: resolvedProjectId,
14948
+ days: lookbackDays,
14949
+ // The table stores cumulative snapshots. Request a wider window and
14950
+ // deduplicate below so refresh frequency cannot inflate the totals.
14951
+ limit: 100,
14952
+ latestOnly: true,
14953
+ ...platform3 ? { platform: platform3 } : {}
14954
+ },
14955
+ { timeoutMs: 15e3 }
14956
+ );
14957
+ if (error || !result?.success) {
14958
+ return {
14959
+ content: [{ type: "text", text: "The analytics dashboard could not load data. Please retry." }],
14960
+ isError: true
14961
+ };
14962
+ }
14963
+ const posts = latestAnalyticsRows(result.rows ?? []).map(publicAnalyticsPost).filter((post) => post !== null).sort((a, b) => b.views - a.views);
14964
+ const totals = /* @__PURE__ */ new Map();
14965
+ let totalViews = 0;
14966
+ let totalEngagement = 0;
14967
+ for (const post of posts) {
14968
+ const engagement = post.likes + post.comments + post.shares;
14969
+ totalViews += post.views;
14970
+ totalEngagement += engagement;
14971
+ const aggregate = totals.get(post.platform) ?? {
14972
+ platform: post.platform,
14973
+ views: 0,
14974
+ engagement: 0,
14975
+ posts: 0
14976
+ };
14977
+ aggregate.views += post.views;
14978
+ aggregate.engagement += engagement;
14979
+ aggregate.posts += 1;
14980
+ totals.set(post.platform, aggregate);
14981
+ }
14982
+ const structuredContent = {
14983
+ project_id: resolvedProjectId,
14984
+ platform: platform3 ?? null,
14985
+ days: lookbackDays,
14986
+ summary: {
14987
+ views: totalViews,
14988
+ engagement: totalEngagement,
14989
+ engagement_rate: totalViews > 0 ? Number((totalEngagement / totalViews * 100).toFixed(2)) : 0,
14990
+ posts: posts.length
14991
+ },
14992
+ platform_totals: [...totals.values()].sort((a, b) => b.views - a.views),
14993
+ posts
14994
+ };
14995
+ return {
14996
+ structuredContent,
14997
+ content: [
14998
+ {
14999
+ type: "text",
15000
+ text: `Loaded ${posts.length} analytics record${posts.length === 1 ? "" : "s"} for the last ${lookbackDays} days.`
15001
+ }
15002
+ ]
15003
+ };
15004
+ }
15005
+ );
15006
+ registerAppResource2(
15007
+ server2,
15008
+ ANALYTICS_URI,
15009
+ ANALYTICS_URI,
15010
+ {
15011
+ mimeType: RESOURCE_MIME_TYPE2,
15012
+ description: "Self-contained Social Neuron project analytics dashboard.",
15013
+ _meta: { ui: { csp: ANALYTICS_CSP } }
15014
+ },
15015
+ async () => {
15016
+ try {
15017
+ const html = await readAnalyticsHtml();
15018
+ return {
15019
+ contents: [
15020
+ {
15021
+ uri: ANALYTICS_URI,
15022
+ mimeType: RESOURCE_MIME_TYPE2,
15023
+ text: html,
15024
+ _meta: { ui: { csp: ANALYTICS_CSP } }
15025
+ }
15026
+ ]
15027
+ };
15028
+ } catch {
15029
+ return {
15030
+ contents: [
15031
+ {
15032
+ uri: ANALYTICS_URI,
15033
+ mimeType: RESOURCE_MIME_TYPE2,
15034
+ text: "<!doctype html><html><body><h2>Analytics Pulse unavailable</h2><p>The interactive bundle is unavailable on this deployment. Please contact Social Neuron support.</p></body></html>",
15035
+ _meta: { ui: { csp: ANALYTICS_CSP } }
15036
+ }
15037
+ ]
15038
+ };
15039
+ }
15040
+ }
15041
+ );
15042
+ }
15043
+ var ANALYTICS_URI, ANALYTICS_CSP, PLATFORM_VALUES, AnalyticsPostOutputSchema;
15044
+ var init_analytics_pulse = __esm({
15045
+ "src/apps/analytics-pulse.ts"() {
15046
+ "use strict";
15047
+ init_edge_function();
15048
+ init_supabase();
15049
+ ANALYTICS_URI = "ui://analytics-pulse/v1/mcp-app.html";
15050
+ ANALYTICS_CSP = {
15051
+ connectDomains: [],
15052
+ resourceDomains: [],
15053
+ frameDomains: []
15054
+ };
15055
+ PLATFORM_VALUES = [
15056
+ "youtube",
15057
+ "tiktok",
15058
+ "instagram",
15059
+ "twitter",
15060
+ "linkedin",
15061
+ "facebook",
15062
+ "threads",
15063
+ "bluesky"
15064
+ ];
15065
+ AnalyticsPostOutputSchema = z32.object({
15066
+ platform: z32.string(),
15067
+ title: z32.string().nullable(),
15068
+ views: z32.number(),
15069
+ likes: z32.number(),
15070
+ comments: z32.number(),
15071
+ shares: z32.number(),
15072
+ engagement_rate: z32.number(),
15073
+ captured_at: z32.string(),
15074
+ published_at: z32.string().nullable()
15075
+ });
15076
+ }
15077
+ });
15078
+
15079
+ // src/tools/connections.ts
15080
+ import { z as z33 } from "zod";
13927
15081
  function findActiveAccount(accounts, platform3) {
13928
15082
  const target = platform3.toLowerCase();
13929
15083
  return accounts.find((a) => {
@@ -13936,16 +15090,16 @@ function registerConnectionTools(server2) {
13936
15090
  "start_platform_connection",
13937
15091
  "Begin connecting a social platform (Instagram, TikTok, YouTube, etc.). Returns a single-use deep link the user opens in a browser to complete the one-time OAuth handshake on socialneuron.com. This is NOT another OAuth in Claude \u2014 platform connections require a browser session because the social platforms (Meta, Google, TikTok) only accept callbacks on socialneuron.com. After the user clicks the link and approves on the platform, call `wait_for_connection` to detect completion. Link expires in 2 minutes; mint a new one if needed. Use `list_connected_accounts` first to check whether the platform is already connected before calling this.",
13938
15092
  {
13939
- platform: z32.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
13940
- project_id: z32.string().optional().describe(
15093
+ platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
15094
+ project_id: z33.string().optional().describe(
13941
15095
  "Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
13942
15096
  ),
13943
- response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15097
+ response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
13944
15098
  },
13945
15099
  async ({ platform: platform3, project_id, response_format }) => {
13946
15100
  const format = response_format ?? "text";
13947
15101
  const userId = await getDefaultUserId();
13948
- const rl = checkRateLimit("read", `start_platform_connection:${userId}`);
15102
+ const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
13949
15103
  if (!rl.allowed) {
13950
15104
  return {
13951
15105
  content: [
@@ -14025,13 +15179,13 @@ function registerConnectionTools(server2) {
14025
15179
  "wait_for_connection",
14026
15180
  "Poll until a platform connection becomes active. Use after `start_platform_connection` while the user completes the browser OAuth flow. Returns when the account row appears with status=active, or when the timeout elapses. Default timeout 120s, max 600s.",
14027
15181
  {
14028
- platform: z32.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
14029
- project_id: z32.string().optional().describe(
15182
+ platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
15183
+ project_id: z33.string().optional().describe(
14030
15184
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
14031
15185
  ),
14032
- timeout_s: z32.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
14033
- poll_interval_s: z32.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
14034
- response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15186
+ timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
15187
+ poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
15188
+ response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14035
15189
  },
14036
15190
  async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
14037
15191
  const format = response_format ?? "text";
@@ -14187,7 +15341,7 @@ var init_connections = __esm({
14187
15341
  });
14188
15342
 
14189
15343
  // src/tools/harness.ts
14190
- import { z as z33 } from "zod";
15344
+ import { z as z34 } from "zod";
14191
15345
  function registerHarnessTools(server2, _ctx) {
14192
15346
  server2.tool(
14193
15347
  "write_agent_reflection",
@@ -14292,38 +15446,38 @@ var init_harness = __esm({
14292
15446
  "engager"
14293
15447
  ];
14294
15448
  writeReflectionSchema = {
14295
- reflection_text: z33.string().min(1).max(4e3).describe("The verbal reflection text produced by the agent. 1\u20134000 characters."),
14296
- generated_by_agent: z33.enum(ALLOWED_AGENTS).describe(
15449
+ reflection_text: z34.string().min(1).max(4e3).describe("The verbal reflection text produced by the agent. 1\u20134000 characters."),
15450
+ generated_by_agent: z34.enum(ALLOWED_AGENTS).describe(
14297
15451
  "Which agent produced this reflection. Must be one of: conductor, brand-brain, drafter, publisher, analyst, engager."
14298
15452
  ),
14299
- provenance: z33.object({
14300
- content_history_id: z33.string().optional().describe("Related content_history row UUID."),
14301
- outcome_event_id: z33.string().optional().describe("Related outcome event UUID."),
14302
- prm_score_ids: z33.array(z33.string()).optional().describe("PRM score IDs that informed this reflection."),
14303
- handoff_ids: z33.array(z33.string()).optional().describe("Handoff event IDs that triggered this reflection.")
15453
+ provenance: z34.object({
15454
+ content_history_id: z34.string().optional().describe("Related content_history row UUID."),
15455
+ outcome_event_id: z34.string().optional().describe("Related outcome event UUID."),
15456
+ prm_score_ids: z34.array(z34.string()).optional().describe("PRM score IDs that informed this reflection."),
15457
+ handoff_ids: z34.array(z34.string()).optional().describe("Handoff event IDs that triggered this reflection.")
14304
15458
  }).strict().describe(
14305
15459
  "Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
14306
15460
  ),
14307
- brand_id: z33.string().describe("Brand profile UUID this reflection belongs to."),
14308
- pipeline_id: z33.string().optional().describe("Optional: pipeline run UUID."),
14309
- post_id: z33.string().optional().describe("Optional: post UUID if reflection targets a post.")
15461
+ brand_id: z34.string().describe("Brand profile UUID this reflection belongs to."),
15462
+ pipeline_id: z34.string().optional().describe("Optional: pipeline run UUID."),
15463
+ post_id: z34.string().optional().describe("Optional: post UUID if reflection targets a post.")
14310
15464
  };
14311
15465
  readReflectionSchema = {
14312
- brand_id: z33.string().describe("Brand profile UUID to read reflections for."),
14313
- generated_by_agent: z33.enum(ALLOWED_AGENTS).optional().describe(
15466
+ brand_id: z34.string().describe("Brand profile UUID to read reflections for."),
15467
+ generated_by_agent: z34.enum(ALLOWED_AGENTS).optional().describe(
14314
15468
  "Optional filter: only return reflections produced by this agent. One of: conductor, brand-brain, drafter, publisher, analyst, engager."
14315
15469
  ),
14316
- limit: z33.number().int().min(1).max(100).optional().describe(
15470
+ limit: z34.number().int().min(1).max(100).optional().describe(
14317
15471
  "Maximum number of reflections to return. Clamped to [1, 100]. Default: 50. Results are ordered by created_at DESC, then id ASC (deterministic tiebreak)."
14318
15472
  )
14319
15473
  };
14320
15474
  recordOutcomeSchema = {
14321
- decision_event_id: z33.string().describe("UUID of the decision_events row to record an outcome for."),
14322
- horizon: z33.enum(["1h", "6h", "24h"]).describe(
15475
+ decision_event_id: z34.string().describe("UUID of the decision_events row to record an outcome for."),
15476
+ horizon: z34.enum(["1h", "6h", "24h"]).describe(
14323
15477
  "Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
14324
15478
  ),
14325
- reward: z33.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
14326
- outcome_metrics: z33.record(z33.string(), z33.number()).optional().describe(
15479
+ reward: z34.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
15480
+ outcome_metrics: z34.record(z34.string(), z34.number()).optional().describe(
14327
15481
  'Optional map of raw metric name \u2192 value (e.g. {"likes": 42, "reach": 1200}). Stored verbatim; not used for learning directly.'
14328
15482
  )
14329
15483
  };
@@ -14331,7 +15485,7 @@ var init_harness = __esm({
14331
15485
  });
14332
15486
 
14333
15487
  // src/tools/hermes.ts
14334
- import { z as z34 } from "zod";
15488
+ import { z as z35 } from "zod";
14335
15489
  function asEnvelope25(data) {
14336
15490
  return {
14337
15491
  _meta: {
@@ -14347,12 +15501,12 @@ function registerHermesTools(server2) {
14347
15501
  "Save a draft post to the SN content library. Use when an autonomous agent wants to persist a draft for review before publishing. Lands in the content library with status='draft'. The draft can then be approved/edited in the SN UI.",
14348
15502
  {
14349
15503
  platform: PLATFORM.describe("Target platform for the draft."),
14350
- copy: z34.string().min(1).max(8e3).describe("The draft post body."),
14351
- project_id: z34.string().optional().describe("SN project UUID. Optional but recommended."),
14352
- media_url: z34.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
14353
- hermes_run_id: z34.string().optional().describe("Agent run id, for traceability."),
14354
- source_intel_ids: z34.array(z34.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
14355
- response_format: z34.enum(["text", "json"]).optional()
15504
+ copy: z35.string().min(1).max(8e3).describe("The draft post body."),
15505
+ project_id: z35.string().optional().describe("SN project UUID. Optional but recommended."),
15506
+ media_url: z35.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
15507
+ hermes_run_id: z35.string().optional().describe("Agent run id, for traceability."),
15508
+ source_intel_ids: z35.array(z35.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
15509
+ response_format: z35.enum(["text", "json"]).optional()
14356
15510
  },
14357
15511
  async ({ platform: platform3, copy, project_id, media_url, hermes_run_id, response_format }) => {
14358
15512
  const { data, error } = await callEdgeFunction(
@@ -14390,15 +15544,15 @@ function registerHermesTools(server2) {
14390
15544
  "record_voice_lesson",
14391
15545
  'Persist a learned voice lesson to brand_profiles.platform_voice.voice_lessons. Use after weekly reflection identifies a hook/format/CTA pattern that beats median engagement by \u226530%. Appears in SN Brand > BrandBrainPreview > "Voice lessons (auto)".',
14392
15546
  {
14393
- project_id: z34.string().describe("SN project UUID."),
14394
- lesson: z34.string().min(1).max(500).describe('One-sentence rule (e.g. "lowercase IG hooks beat title-case").'),
14395
- evidence: z34.object({
14396
- engagement_lift_pct: z34.number().describe("Engagement lift vs baseline, in percentage points."),
14397
- sample_size: z34.number().int().min(1).describe("Number of posts behind this lesson."),
14398
- top_examples: z34.array(z34.string()).optional().describe("Up to 3 hook examples from the top quartile.")
15547
+ project_id: z35.string().describe("SN project UUID."),
15548
+ lesson: z35.string().min(1).max(500).describe('One-sentence rule (e.g. "lowercase IG hooks beat title-case").'),
15549
+ evidence: z35.object({
15550
+ engagement_lift_pct: z35.number().describe("Engagement lift vs baseline, in percentage points."),
15551
+ sample_size: z35.number().int().min(1).describe("Number of posts behind this lesson."),
15552
+ top_examples: z35.array(z35.string()).optional().describe("Up to 3 hook examples from the top quartile.")
14399
15553
  }).describe("Quantitative evidence backing the lesson."),
14400
- applies_to: z34.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
14401
- response_format: z34.enum(["text", "json"]).optional()
15554
+ applies_to: z35.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
15555
+ response_format: z35.enum(["text", "json"]).optional()
14402
15556
  },
14403
15557
  async ({ project_id, lesson, evidence, applies_to, response_format }) => {
14404
15558
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -14434,10 +15588,10 @@ function registerHermesTools(server2) {
14434
15588
  "record_observation",
14435
15589
  'Record an agent observation (e.g. "topic X engagement up 23% this week"). Surfaces in the analytics Playbook. Use for weekly reflection digests and mid-campaign pulse summaries.',
14436
15590
  {
14437
- summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
14438
- deltas: z34.record(z34.string(), z34.union([z34.number(), z34.string(), z34.boolean()])).optional().describe("Optional structured key/value payload (e.g. {topic_x_er_pct: 23})."),
14439
- run_id: z34.string().optional().describe("Agent run id for traceability."),
14440
- response_format: z34.enum(["text", "json"]).optional()
15591
+ summary: z35.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
15592
+ deltas: z35.record(z35.string(), z35.union([z35.number(), z35.string(), z35.boolean()])).optional().describe("Optional structured key/value payload (e.g. {topic_x_er_pct: 23})."),
15593
+ run_id: z35.string().optional().describe("Agent run id for traceability."),
15594
+ response_format: z35.enum(["text", "json"]).optional()
14441
15595
  },
14442
15596
  async ({ summary, deltas, run_id, response_format }) => {
14443
15597
  const { data, error } = await callEdgeFunction("mcp-data", { action: "record-observation", summary, deltas, run_id });
@@ -14464,13 +15618,13 @@ function registerHermesTools(server2) {
14464
15618
  "record_intel_signal",
14465
15619
  "Record a research/trend signal (news, HN post, competitor change, arxiv paper). Surfaces in SN Brand > Niche Intelligence. Dedupes by URL per user \u2014 safe to call multiple times for the same source.",
14466
15620
  {
14467
- source: z34.string().min(1).max(100).describe('Watcher name (e.g. "news-watch", "hackernews-watch").'),
14468
- url: z34.string().url().max(2e3).describe("Canonical URL of the source. Used for dedupe."),
14469
- topic: z34.string().max(200).optional().describe("Best-fit topic key."),
14470
- title: z34.string().max(500).optional(),
14471
- summary: z34.string().max(4e3).optional().describe("One-paragraph summary of the signal."),
14472
- score: z34.number().optional().describe("Relevance score from the watcher (0\u201310 typical)."),
14473
- response_format: z34.enum(["text", "json"]).optional()
15621
+ source: z35.string().min(1).max(100).describe('Watcher name (e.g. "news-watch", "hackernews-watch").'),
15622
+ url: z35.string().url().max(2e3).describe("Canonical URL of the source. Used for dedupe."),
15623
+ topic: z35.string().max(200).optional().describe("Best-fit topic key."),
15624
+ title: z35.string().max(500).optional(),
15625
+ summary: z35.string().max(4e3).optional().describe("One-paragraph summary of the signal."),
15626
+ score: z35.number().optional().describe("Relevance score from the watcher (0\u201310 typical)."),
15627
+ response_format: z35.enum(["text", "json"]).optional()
14474
15628
  },
14475
15629
  async ({ source, url, topic, title, summary, score, response_format }) => {
14476
15630
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -14508,16 +15662,16 @@ function registerHermesTools(server2) {
14508
15662
  "record_campaign_spend",
14509
15663
  "Log a campaign cost line. Use when an autonomous agent incurs spend that should be attributed to a campaign \u2014 drafts, renders, paid amp. Read aggregate via get_active_campaigns.",
14510
15664
  {
14511
- campaign_id: z34.string().min(1).max(200).describe("Campaign identifier (slug)."),
14512
- category: z34.enum([
15665
+ campaign_id: z35.string().min(1).max(200).describe("Campaign identifier (slug)."),
15666
+ category: z35.enum([
14513
15667
  "hermes_drafts",
14514
15668
  "carousel_renders",
14515
15669
  "analytics_pulls",
14516
15670
  "paid_amplification",
14517
15671
  "other"
14518
15672
  ]).describe("Cost category."),
14519
- amount_usd: z34.number().min(0).describe("Amount in USD; supports 4 decimal places."),
14520
- response_format: z34.enum(["text", "json"]).optional()
15673
+ amount_usd: z35.number().min(0).describe("Amount in USD; supports 4 decimal places."),
15674
+ response_format: z35.enum(["text", "json"]).optional()
14521
15675
  },
14522
15676
  async ({ campaign_id, category, amount_usd, response_format }) => {
14523
15677
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -14552,7 +15706,7 @@ function registerHermesTools(server2) {
14552
15706
  "get_active_campaigns",
14553
15707
  "List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
14554
15708
  {
14555
- response_format: z34.enum(["text", "json"]).optional()
15709
+ response_format: z35.enum(["text", "json"]).optional()
14556
15710
  },
14557
15711
  async ({ response_format }) => {
14558
15712
  const { data, error } = await callEdgeFunction("mcp-data", { action: "get-active-campaigns" });
@@ -14601,7 +15755,7 @@ var init_hermes = __esm({
14601
15755
  "use strict";
14602
15756
  init_edge_function();
14603
15757
  init_version();
14604
- PLATFORM = z34.enum([
15758
+ PLATFORM = z35.enum([
14605
15759
  "instagram",
14606
15760
  "twitter",
14607
15761
  "linkedin",
@@ -14647,7 +15801,20 @@ var init_skills_manifest = __esm({
14647
15801
  });
14648
15802
 
14649
15803
  // src/tools/skills.ts
14650
- import { z as z35 } from "zod";
15804
+ import { z as z36 } from "zod";
15805
+ function renderCatalogRow(row) {
15806
+ const lines = [];
15807
+ const platform3 = row.platform ? ` \xB7 ${row.platform}` : "";
15808
+ lines.push(`${row.slug} (${row.kind}${platform3})`);
15809
+ const desc = row.frontmatter && typeof row.frontmatter.description === "string" ? row.frontmatter.description : null;
15810
+ if (desc) lines.push(` ${desc}`);
15811
+ const lock = row.locked ? ` \u{1F512} (upgrade to unlock)` : "";
15812
+ lines.push(` Tier: ${row.tier_minimum}${lock}`);
15813
+ lines.push(
15814
+ ` Model: ${row.model_id ?? "n/a"} \xB7 ${row.body_chars} chars` + (row.updated_at ? ` \xB7 updated ${row.updated_at}` : "")
15815
+ );
15816
+ return lines.join("\n");
15817
+ }
14651
15818
  function asEnvelope26(data) {
14652
15819
  return {
14653
15820
  _meta: {
@@ -14675,19 +15842,88 @@ function registerSkillsTools(server2) {
14675
15842
  "list_skills",
14676
15843
  'List Social Neuron content workflow skills available to the authenticated user. A skill is a brand-locked multi-step pipeline (research \u2192 hook \u2192 script \u2192 visuals \u2192 voice \u2192 captions \u2192 assembly \u2192 quality gate \u2192 schedule) inspired by documented viral patterns (MrBeast 3-second hook, Hormozi pattern interrupt, etc.). Use this tool when the user asks "what can SN do", "what skills are available", "show me viral templates", or before calling run_skill so you can pick the right one.',
14677
15844
  {
14678
- studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
14679
- featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
14680
- response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
15845
+ studio: z36.enum(STUDIO_VALUES).optional().describe(
15846
+ "Filter to one studio (video, avatar, carousel, voice, caption, edit)."
15847
+ ),
15848
+ featured_only: z36.boolean().optional().describe(
15849
+ "Return only featured (recommended) skills. Defaults to false."
15850
+ ),
15851
+ response_format: z36.enum(["text", "json"]).optional().describe(
15852
+ "Response format. Defaults to text \u2014 human-readable summary."
15853
+ )
14681
15854
  },
14682
15855
  async ({ studio, featured_only, response_format }) => {
14683
- const skills = listSkills({ studio, featuredOnly: featured_only });
14684
15856
  const format = response_format ?? "text";
15857
+ const { data: efData, error: efError } = await callEdgeFunction("mcp-data", { action: "get-skills" });
15858
+ const catalogRows = efData?.skills;
15859
+ if (!efError && Array.isArray(catalogRows) && catalogRows.length > 0) {
15860
+ const workflows = listSkills({ studio, featuredOnly: featured_only });
15861
+ const filteredGuideRows = catalogRows.filter(
15862
+ (row) => !studio && (!featured_only || row.frontmatter?.featured === true)
15863
+ );
15864
+ const guides = filteredGuideRows.map((row) => ({
15865
+ ...row,
15866
+ use_with: "get_skill"
15867
+ }));
15868
+ const total = guides.length + workflows.length;
15869
+ if (format === "json") {
15870
+ return {
15871
+ content: [
15872
+ {
15873
+ type: "text",
15874
+ text: JSON.stringify(
15875
+ asEnvelope26({
15876
+ count: total,
15877
+ guides,
15878
+ workflows: workflows.map((w) => ({
15879
+ ...w,
15880
+ use_with: "run_skill"
15881
+ }))
15882
+ }),
15883
+ null,
15884
+ 2
15885
+ )
15886
+ }
15887
+ ]
15888
+ };
15889
+ }
15890
+ const guideBlocks = filteredGuideRows.map(
15891
+ (row) => `${renderCatalogRow(row)}
15892
+ \u2192 Read it: get_skill(slug: "${row.slug}")`
15893
+ ).join("\n\n");
15894
+ const workflowBlocks = workflows.map(renderSkillSummary).join("\n\n");
15895
+ const header2 = `${total} skill${total === 1 ? "" : "s"} available (${guides.length} guide${guides.length === 1 ? "" : "s"} \xB7 ${workflows.length} workflow${workflows.length === 1 ? "" : "s"})
15896
+ ${"=".repeat(40)}`;
15897
+ const sections = [
15898
+ guides.length > 0 ? `GUIDES \u2014 living how-to documents. Fetch with get_skill(slug).
15899
+
15900
+ ${guideBlocks}` : "",
15901
+ workflows.length > 0 ? `WORKFLOWS \u2014 executable content pipelines. Launch with run_skill(skill_id).
15902
+
15903
+ ${workflowBlocks}` : ""
15904
+ ].filter(Boolean);
15905
+ return {
15906
+ content: [
15907
+ {
15908
+ type: "text",
15909
+ text: `${header2}
15910
+
15911
+ ${sections.join("\n\n")}`
15912
+ }
15913
+ ]
15914
+ };
15915
+ }
15916
+ const skills = listSkills({ studio, featuredOnly: featured_only });
14685
15917
  if (format === "json") {
14686
15918
  return {
14687
15919
  content: [
14688
15920
  {
14689
15921
  type: "text",
14690
- text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
15922
+ text: JSON.stringify(
15923
+ asEnvelope26({ count: skills.length, skills }),
15924
+ null,
15925
+ 2
15926
+ )
14691
15927
  }
14692
15928
  ]
14693
15929
  };
@@ -14697,7 +15933,9 @@ function registerSkillsTools(server2) {
14697
15933
  content: [
14698
15934
  {
14699
15935
  type: "text",
14700
- text: "No skills match the filter." + (studio ? ` Studio "${studio}" has no skills yet.` : "") + "\n\nAvailable studios with skills: " + Array.from(new Set(SKILLS_MANIFEST.map((s) => s.studio))).join(", ")
15936
+ text: "No skills match the filter." + (studio ? ` Studio "${studio}" has no skills yet.` : "") + "\n\nAvailable studios with skills: " + Array.from(new Set(SKILLS_MANIFEST.map((s) => s.studio))).join(
15937
+ ", "
15938
+ )
14701
15939
  }
14702
15940
  ]
14703
15941
  };
@@ -14716,18 +15954,22 @@ ${blocks}` }]
14716
15954
  "run_skill",
14717
15955
  "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the exact step plan, credit cost, and a deep-link to launch the run in the SN dashboard. A future release executes in-process so you can stream step-by-step progress back to the user. Call list_skills first to discover available skill ids.",
14718
15956
  {
14719
- skill_id: z35.string().describe(
15957
+ skill_id: z36.string().describe(
14720
15958
  'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
14721
15959
  ),
14722
- topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
14723
- audience: z35.string().optional().describe(
15960
+ topic: z36.string().min(1).describe(
15961
+ 'What the content is about (e.g. "why we built Social Neuron").'
15962
+ ),
15963
+ audience: z36.string().optional().describe(
14724
15964
  'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
14725
15965
  ),
14726
- hook: z35.string().optional().describe(
15966
+ hook: z36.string().optional().describe(
14727
15967
  "Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
14728
15968
  ),
14729
- cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
14730
- response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
15969
+ cta: z36.string().optional().describe(
15970
+ "Optional call-to-action override. Defaults to brand standard CTA."
15971
+ ),
15972
+ response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
14731
15973
  },
14732
15974
  async ({ skill_id, topic, audience, hook, cta, response_format }) => {
14733
15975
  const skill = getSkill(skill_id);
@@ -14795,19 +16037,96 @@ ${blocks}` }]
14795
16037
  };
14796
16038
  }
14797
16039
  );
16040
+ server2.tool(
16041
+ "get_skill",
16042
+ `Fetch the full body of a single Social Neuron skill by slug \u2014 the hand-maintained strategy/specs plus the machine-maintained "what's working now" compiled section. Use this after list_skills when the user wants the actual playbook for a platform (e.g. "show me the TikTok skill"). Returns the skill body, compiled section, tier, and linked recipe slug (if any).`,
16043
+ {
16044
+ slug: z36.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9-]*$/).describe(
16045
+ 'The skill slug (e.g. "tiktok-content"). Use list_skills to discover slugs.'
16046
+ ),
16047
+ response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
16048
+ },
16049
+ async ({ slug, response_format }) => {
16050
+ const format = response_format ?? "text";
16051
+ const { data, error } = await callEdgeFunction("mcp-data", {
16052
+ action: "get-skill",
16053
+ slug
16054
+ });
16055
+ if (error) {
16056
+ return toolError(
16057
+ "upstream_error",
16058
+ "The skill catalogue could not be loaded. Please retry."
16059
+ );
16060
+ }
16061
+ const skill = data?.skill;
16062
+ if (!skill) {
16063
+ return toolError("not_found", `No skill found with slug "${slug}".`, {
16064
+ recover_with: ["Call list_skills to see available skills."]
16065
+ });
16066
+ }
16067
+ if (skill.locked) {
16068
+ return toolError(
16069
+ "permission_denied",
16070
+ "This skill requires a higher plan tier.",
16071
+ {
16072
+ details: { slug: skill.slug, tier_minimum: skill.tier_minimum },
16073
+ recover_with: [
16074
+ "Upgrade the account or choose an unlocked skill from list_skills."
16075
+ ]
16076
+ }
16077
+ );
16078
+ }
16079
+ if (format === "json") {
16080
+ return {
16081
+ content: [
16082
+ {
16083
+ type: "text",
16084
+ text: JSON.stringify(asEnvelope26(skill), null, 2)
16085
+ }
16086
+ ]
16087
+ };
16088
+ }
16089
+ const platform3 = skill.platform ? ` \xB7 ${skill.platform}` : "";
16090
+ const lock = skill.locked ? " \u{1F512} (upgrade to unlock)" : "";
16091
+ const header = [
16092
+ `${skill.slug} (${skill.kind}${platform3})`,
16093
+ "=".repeat(40),
16094
+ `Tier: ${skill.tier_minimum}${lock} \xB7 version ${skill.version}` + (skill.recipe_slug ? ` \xB7 recipe: ${skill.recipe_slug}` : ""),
16095
+ ""
16096
+ ].join("\n");
16097
+ const compiled = skill.compiled_section ? `
16098
+
16099
+ --- What's working now ---
16100
+ ${skill.compiled_section}` : "";
16101
+ return {
16102
+ content: [
16103
+ { type: "text", text: `${header}${skill.body}${compiled}` }
16104
+ ]
16105
+ };
16106
+ }
16107
+ );
14798
16108
  }
14799
16109
  var STUDIO_VALUES;
14800
16110
  var init_skills = __esm({
14801
16111
  "src/tools/skills.ts"() {
14802
16112
  "use strict";
14803
16113
  init_version();
16114
+ init_tool_error();
16115
+ init_edge_function();
14804
16116
  init_skills_manifest();
14805
- STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
16117
+ STUDIO_VALUES = [
16118
+ "video",
16119
+ "avatar",
16120
+ "carousel",
16121
+ "voice",
16122
+ "caption",
16123
+ "edit"
16124
+ ];
14806
16125
  }
14807
16126
  });
14808
16127
 
14809
16128
  // src/tools/loopPulse.ts
14810
- import { z as z36 } from "zod";
16129
+ import { z as z37 } from "zod";
14811
16130
  function asEnvelope27(data) {
14812
16131
  return {
14813
16132
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -14819,7 +16138,7 @@ function registerLoopPulseTools(server2) {
14819
16138
  "get_loop_pulse",
14820
16139
  'Read the dynamic loop-health KPIs for the Social Neuron growth loop over the last 7 days. Returns reflection coverage, decision coverage, visual gate pass rate, learning-update application rate, per-platform learning uptake, autopilot lag, and pattern aggregation counts \u2014 each with a status ("ok" / "warn" / "bad") and a why-line explaining what the metric measures. Use this to decide whether the loop is closing or where it is stuck before recommending next moves.',
14821
16140
  {
14822
- response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16141
+ response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
14823
16142
  },
14824
16143
  async ({ response_format }) => {
14825
16144
  const format = response_format ?? "text";
@@ -14869,7 +16188,7 @@ var init_loopPulse = __esm({
14869
16188
  });
14870
16189
 
14871
16190
  // src/tools/banditState.ts
14872
- import { z as z37 } from "zod";
16191
+ import { z as z38 } from "zod";
14873
16192
  function asEnvelope28(data) {
14874
16193
  return {
14875
16194
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -14881,8 +16200,8 @@ function registerBanditStateTools(server2) {
14881
16200
  "get_bandit_state",
14882
16201
  "Read the current content learning state for a project. Returns top-K arms per (arm_type, platform) with expected performance and uncertainty. Use this to reason about which hook family / format / timing slot currently performs best on each platform before recommending next moves. Arm types: hook_family, length_bucket (xs/s/m/l/xl by platform), posting_time_bucket (morning/midday/evening/late), content_format (video/carousel/image/caption/text/avatar/storyboard).",
14883
16202
  {
14884
- project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
14885
- platform: z37.enum([
16203
+ project_id: z38.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
16204
+ platform: z38.enum([
14886
16205
  "instagram",
14887
16206
  "tiktok",
14888
16207
  "youtube",
@@ -14894,7 +16213,7 @@ function registerBanditStateTools(server2) {
14894
16213
  ]).optional().describe(
14895
16214
  "Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
14896
16215
  ),
14897
- arm_type: z37.enum([
16216
+ arm_type: z38.enum([
14898
16217
  "hook_family",
14899
16218
  "hook_type",
14900
16219
  "format",
@@ -14908,8 +16227,8 @@ function registerBanditStateTools(server2) {
14908
16227
  "posting_time_bucket",
14909
16228
  "content_format"
14910
16229
  ]).optional().describe("Arm dimension. Omit to return all types grouped."),
14911
- top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
14912
- response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16230
+ top_k: z38.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
16231
+ response_format: z38.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
14913
16232
  },
14914
16233
  async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
14915
16234
  const format = response_format ?? "text";
@@ -14972,12 +16291,134 @@ var init_banditState = __esm({
14972
16291
  }
14973
16292
  });
14974
16293
 
16294
+ // src/tools/lifecycle.ts
16295
+ import { z as z39 } from "zod";
16296
+ function envelope(data) {
16297
+ return {
16298
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
16299
+ data
16300
+ };
16301
+ }
16302
+ function lifecycleFailure(error) {
16303
+ if (/not_found|not found/i.test(error)) {
16304
+ return toolError("not_found", "The requested object was not found in this project.");
16305
+ }
16306
+ if (/not_cancellable|publishing_in_progress|schedule_conflict|post_in_progress/i.test(error)) {
16307
+ return toolError("validation_error", "The object can no longer be cancelled in its current state.", {
16308
+ recover_with: ["Refresh its status before deciding the next action."]
16309
+ });
16310
+ }
16311
+ return toolError("upstream_error", "The lifecycle operation could not be completed. Please retry.");
16312
+ }
16313
+ async function projectContext(projectId) {
16314
+ return projectId ?? await getDefaultProjectId();
16315
+ }
16316
+ async function invokeLifecycle(action, projectId, identifiers) {
16317
+ const resolvedProjectId = await projectContext(projectId);
16318
+ if (!resolvedProjectId) {
16319
+ return toolError("validation_error", "A project_id is required because no default project is configured.");
16320
+ }
16321
+ const { data, error } = await callEdgeFunction("mcp-data", {
16322
+ action,
16323
+ projectId: resolvedProjectId,
16324
+ project_id: resolvedProjectId,
16325
+ ...identifiers
16326
+ });
16327
+ if (error) return lifecycleFailure(error);
16328
+ if (!data?.success) return toolError("upstream_error", "The lifecycle operation returned no result.");
16329
+ const result = envelope(data);
16330
+ return {
16331
+ structuredContent: result,
16332
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
16333
+ };
16334
+ }
16335
+ function registerLifecycleTools(server2) {
16336
+ server2.tool(
16337
+ "cancel_async_job",
16338
+ "Cancel an owned pending async generation/render job before a worker starts it. If credits were already debited, the backend attempts an idempotent refund and reports the result. Processing or terminal jobs are not cancellable.",
16339
+ {
16340
+ job_id: z39.string().uuid().describe("Owned async_jobs ID returned by a generation tool."),
16341
+ project_id: PROJECT_ID,
16342
+ confirm: CONFIRM
16343
+ },
16344
+ async ({ job_id, project_id }) => invokeLifecycle("cancel-async-job", project_id, { job_id })
16345
+ );
16346
+ server2.tool(
16347
+ "cancel_scheduled_post",
16348
+ "Cancel an owned draft, pending, or scheduled post before publishing starts. This closes its pending schedule job first; it refuses once a worker has claimed the publication.",
16349
+ {
16350
+ post_id: z39.string().uuid().describe("Owned scheduled post ID."),
16351
+ project_id: PROJECT_ID,
16352
+ confirm: CONFIRM
16353
+ },
16354
+ async ({ post_id, project_id }) => invokeLifecycle("cancel-scheduled-post", project_id, { post_id })
16355
+ );
16356
+ server2.tool(
16357
+ "delete_carousel",
16358
+ "Delete an owned carousel content-history record from one project. Stored media is retained until the normal retention cleanup; this does not delete already-published platform posts.",
16359
+ {
16360
+ content_id: z39.string().uuid().describe("Owned carousel content_history ID."),
16361
+ project_id: PROJECT_ID,
16362
+ confirm: CONFIRM
16363
+ },
16364
+ async ({ content_id, project_id }) => invokeLifecycle("delete-carousel", project_id, { content_id })
16365
+ );
16366
+ server2.tool(
16367
+ "delete_content_plan",
16368
+ "Permanently delete an owned content plan in one project. This does not cancel posts that were already scheduled from the plan.",
16369
+ {
16370
+ plan_id: z39.string().uuid().describe("Owned content plan ID."),
16371
+ project_id: PROJECT_ID,
16372
+ confirm: CONFIRM
16373
+ },
16374
+ async ({ plan_id, project_id }) => invokeLifecycle("delete-content-plan", project_id, { plan_id })
16375
+ );
16376
+ server2.tool(
16377
+ "delete_autopilot_config",
16378
+ "Permanently delete an owned autopilot configuration in one project. Historical runs and already-published posts are retained.",
16379
+ {
16380
+ config_id: z39.string().uuid().describe("Owned autopilot configuration ID."),
16381
+ project_id: PROJECT_ID,
16382
+ confirm: CONFIRM
16383
+ },
16384
+ async ({ config_id, project_id }) => invokeLifecycle("delete-autopilot-config", project_id, { config_id })
16385
+ );
16386
+ }
16387
+ var PROJECT_ID, CONFIRM;
16388
+ var init_lifecycle = __esm({
16389
+ "src/tools/lifecycle.ts"() {
16390
+ "use strict";
16391
+ init_edge_function();
16392
+ init_supabase();
16393
+ init_tool_error();
16394
+ init_version();
16395
+ PROJECT_ID = z39.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default.");
16396
+ CONFIRM = z39.literal(true).describe("Must be true only after the user explicitly confirms this destructive action.");
16397
+ }
16398
+ });
16399
+
14975
16400
  // src/lib/register-tools.ts
16401
+ function redactBase64Payloads(args) {
16402
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return args;
16403
+ let changed = false;
16404
+ const out = { ...args };
16405
+ for (const key of BASE64_PAYLOAD_KEYS) {
16406
+ const value = out[key];
16407
+ if (typeof value !== "string" || value.length < 64) continue;
16408
+ const body = value.replace(DATA_URI_PREFIX2, "");
16409
+ if (STRICT_BASE64.test(body)) {
16410
+ out[key] = `[base64:${body.length} chars omitted from prose scan]`;
16411
+ changed = true;
16412
+ }
16413
+ }
16414
+ return changed ? out : args;
16415
+ }
14976
16416
  function wrapToolWithScanner(toolName, handler) {
14977
16417
  return async function scannerWrappedHandler(...handlerArgs) {
14978
16418
  const args = handlerArgs[0];
14979
16419
  const ctx = handlerArgs[1];
14980
- const inputText = args === void 0 ? "{}" : typeof args === "string" ? args : JSON.stringify(args);
16420
+ const scanArgs = redactBase64Payloads(args);
16421
+ const inputText = scanArgs === void 0 ? "{}" : typeof scanArgs === "string" ? scanArgs : JSON.stringify(scanArgs);
14981
16422
  const inputScan = scan(inputText, {
14982
16423
  mode: "block",
14983
16424
  source: "mcp_tool_input",
@@ -15005,6 +16446,16 @@ function wrapToolWithScanner(toolName, handler) {
15005
16446
  source: "mcp_tool_output",
15006
16447
  user_id: ctx?.userId
15007
16448
  });
16449
+ if (!outputScan.passed) {
16450
+ try {
16451
+ ctx?.logScan?.(toolName, "output", outputScan);
16452
+ } catch {
16453
+ }
16454
+ return toolError(
16455
+ "server_error",
16456
+ "The response exceeded the safe output limit and was not returned."
16457
+ );
16458
+ }
15008
16459
  if (outputScan.sanitized_text !== void 0) {
15009
16460
  try {
15010
16461
  ctx?.logScan?.(toolName, "output", outputScan);
@@ -15013,7 +16464,10 @@ function wrapToolWithScanner(toolName, handler) {
15013
16464
  try {
15014
16465
  return JSON.parse(outputScan.sanitized_text);
15015
16466
  } catch {
15016
- return result;
16467
+ return toolError(
16468
+ "server_error",
16469
+ "The response could not be returned safely. Please retry or contact support."
16470
+ );
15017
16471
  }
15018
16472
  }
15019
16473
  return result;
@@ -15063,8 +16517,7 @@ function applyScopeEnforcement(server2, scopeResolver) {
15063
16517
  details: {
15064
16518
  source: "wrapper",
15065
16519
  // A thrown exception escaped the handler — an unclassified fault.
15066
- error_type: "server_error",
15067
- exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
16520
+ error_type: "server_error"
15068
16521
  }
15069
16522
  });
15070
16523
  throw err;
@@ -15196,12 +16649,14 @@ function registerAllTools(server2, options) {
15196
16649
  registerSkillsTools(server2);
15197
16650
  registerLoopPulseTools(server2);
15198
16651
  registerBanditStateTools(server2);
16652
+ registerLifecycleTools(server2);
15199
16653
  if (!options?.skipApps) {
15200
16654
  registerContentCalendarApp(server2);
16655
+ registerAnalyticsPulseApp(server2);
15201
16656
  }
15202
16657
  applyAnnotations(server2);
15203
16658
  }
15204
- var RESPONSE_CHAR_LIMIT;
16659
+ var BASE64_PAYLOAD_KEYS, DATA_URI_PREFIX2, STRICT_BASE64, RESPONSE_CHAR_LIMIT;
15205
16660
  var init_register_tools = __esm({
15206
16661
  "src/lib/register-tools.ts"() {
15207
16662
  "use strict";
@@ -15243,18 +16698,23 @@ var init_register_tools = __esm({
15243
16698
  init_niche_research();
15244
16699
  init_hyperframes();
15245
16700
  init_content_calendar();
16701
+ init_analytics_pulse();
15246
16702
  init_connections();
15247
16703
  init_harness();
15248
16704
  init_hermes();
15249
16705
  init_skills();
15250
16706
  init_loopPulse();
15251
16707
  init_banditState();
16708
+ init_lifecycle();
16709
+ BASE64_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["file_data", "fileData"]);
16710
+ DATA_URI_PREFIX2 = /^data:[\w.+-]+\/[\w.+-]+;base64,/;
16711
+ STRICT_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
15252
16712
  RESPONSE_CHAR_LIMIT = 1e5;
15253
16713
  }
15254
16714
  });
15255
16715
 
15256
16716
  // src/cli/sn/parse.ts
15257
- import { createHash as createHash3 } from "node:crypto";
16717
+ import { createHash as createHash2 } from "node:crypto";
15258
16718
  function parseSnArgs(argv) {
15259
16719
  const parsed = { _: [] };
15260
16720
  for (let i = 0; i < argv.length; i += 1) {
@@ -15284,8 +16744,8 @@ function isEnabledFlag(value) {
15284
16744
  }
15285
16745
  function emitSnResult(payload, asJson) {
15286
16746
  if (asJson) {
15287
- const envelope = { schema_version: "1", ...payload };
15288
- process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
16747
+ const envelope2 = { schema_version: "1", ...payload };
16748
+ process.stdout.write(JSON.stringify(envelope2, null, 2) + "\n");
15289
16749
  }
15290
16750
  }
15291
16751
  function isValidHttpsUrl(value) {
@@ -15320,7 +16780,7 @@ async function checkUrlReachability(url) {
15320
16780
  } catch (error) {
15321
16781
  return {
15322
16782
  ok: false,
15323
- error: error instanceof Error ? error.message : String(error)
16783
+ error: "Network request failed."
15324
16784
  };
15325
16785
  } finally {
15326
16786
  clearTimeout(timer);
@@ -15350,7 +16810,7 @@ function buildPublishIdempotencyKey(input) {
15350
16810
  title: input.title ?? "",
15351
16811
  scheduledAt: input.scheduledAt ?? ""
15352
16812
  });
15353
- return `sn_${createHash3("sha256").update(material).digest("hex").slice(0, 24)}`;
16813
+ return `sn_${createHash2("sha256").update(material).digest("hex").slice(0, 24)}`;
15354
16814
  }
15355
16815
  function normalizePlatforms(platformsRaw) {
15356
16816
  const caseMap = {
@@ -16410,17 +17870,21 @@ __export(discovery_exports, {
16410
17870
  handleInfo: () => handleInfo,
16411
17871
  handleTools: () => handleTools
16412
17872
  });
17873
+ function publicStdioTools() {
17874
+ return TOOL_CATALOG.filter(
17875
+ (t) => !t.internal && !t.hiddenFromPublicCount && t.module !== "apps"
17876
+ );
17877
+ }
16413
17878
  async function handleTools(args, asJson) {
16414
- let tools = TOOL_CATALOG;
17879
+ let tools = publicStdioTools();
16415
17880
  const scope = args.scope;
16416
17881
  if (typeof scope === "string") {
16417
- tools = getToolsByScope(scope);
17882
+ tools = tools.filter((tool) => tool.scope === scope);
16418
17883
  }
16419
17884
  const module = args.module;
16420
17885
  if (typeof module === "string") {
16421
- tools = getToolsByModule(module);
17886
+ tools = tools.filter((tool) => tool.module === module);
16422
17887
  }
16423
- tools = tools.filter((t) => !t.internal);
16424
17888
  if (asJson) {
16425
17889
  emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
16426
17890
  return;
@@ -16449,27 +17913,31 @@ Module: ${moduleName} (${moduleTools.length} tools)`);
16449
17913
  process.exit(0);
16450
17914
  }
16451
17915
  async function handleInfo(args, asJson) {
17916
+ const stdioTools = publicStdioTools();
16452
17917
  const info = {
16453
17918
  version: MCP_VERSION,
16454
- toolCount: TOOL_CATALOG.filter((t) => !t.internal).length,
16455
- modules: getModules()
17919
+ toolCount: stdioTools.length,
17920
+ modules: Array.from(new Set(stdioTools.map((tool) => tool.module))).sort(),
17921
+ auth: null
16456
17922
  };
16457
- try {
16458
- const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
16459
- const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
16460
- const apiKey = await loadApiKey2();
16461
- if (apiKey) {
16462
- const result = await validateApiKey2(apiKey);
16463
- if (result.valid) {
16464
- info.auth = {
16465
- email: result.email || null,
16466
- scopes: result.scopes || [],
16467
- expiresAt: result.expiresAt || null
16468
- };
17923
+ if (!isEnabledFlag(args.offline)) {
17924
+ try {
17925
+ const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
17926
+ const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
17927
+ const apiKey = await loadApiKey2();
17928
+ if (apiKey) {
17929
+ const result = await validateApiKey2(apiKey);
17930
+ if (result.valid) {
17931
+ info.auth = {
17932
+ email: result.email || null,
17933
+ scopes: result.scopes || [],
17934
+ expiresAt: result.expiresAt || null
17935
+ };
17936
+ }
16469
17937
  }
17938
+ } catch {
17939
+ info.auth = null;
16470
17940
  }
16471
- } catch {
16472
- info.auth = null;
16473
17941
  }
16474
17942
  if (info.auth) {
16475
17943
  try {
@@ -16532,7 +18000,11 @@ function getCallHandler() {
16532
18000
  return cachedCallHandler;
16533
18001
  }
16534
18002
  function restToolNames() {
16535
- return new Set(TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => t.name));
18003
+ return new Set(
18004
+ TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal && !t.hiddenFromPublicCount).map(
18005
+ (t) => t.name
18006
+ )
18007
+ );
16536
18008
  }
16537
18009
  async function invokeToolRest(name, args) {
16538
18010
  const handler = getCallHandler();
@@ -17098,7 +18570,7 @@ function printSnUsage() {
17098
18570
  console.error("");
17099
18571
  console.error("Discovery:");
17100
18572
  console.error(" tools [--scope <scope>] [--module <module>] [--json]");
17101
- console.error(" info [--json]");
18573
+ console.error(" info [--offline] [--json]");
17102
18574
  console.error("");
17103
18575
  console.error("Content:");
17104
18576
  console.error(
@@ -17231,10 +18703,11 @@ var setup_exports = {};
17231
18703
  __export(setup_exports, {
17232
18704
  generatePKCE: () => generatePKCE,
17233
18705
  getAppBaseUrl: () => getAppBaseUrl,
18706
+ isValidSetupApiKey: () => isValidSetupApiKey,
17234
18707
  runLogout: () => runLogout,
17235
18708
  runSetup: () => runSetup
17236
18709
  });
17237
- import { createHash as createHash4, randomBytes, randomUUID as randomUUID4 } from "node:crypto";
18710
+ import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "node:crypto";
17238
18711
  import { createServer } from "node:http";
17239
18712
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
17240
18713
  import { homedir as homedir3, platform as platform2 } from "node:os";
@@ -17243,12 +18716,15 @@ function base64url(buffer) {
17243
18716
  return buffer.toString("base64url");
17244
18717
  }
17245
18718
  function generatePKCE() {
17246
- const verifierBytes = randomBytes(32);
18719
+ const verifierBytes = randomBytes2(32);
17247
18720
  const codeVerifier = base64url(verifierBytes);
17248
- const challengeHash = createHash4("sha256").update(codeVerifier).digest();
18721
+ const challengeHash = createHash3("sha256").update(codeVerifier).digest();
17249
18722
  const codeChallenge = base64url(challengeHash);
17250
18723
  return { codeVerifier, codeChallenge };
17251
18724
  }
18725
+ function isValidSetupApiKey(value) {
18726
+ return typeof value === "string" && value.length >= 32 && value.length <= 512 && /^snk_live_[A-Za-z0-9_-]+$/.test(value);
18727
+ }
17252
18728
  function getAppBaseUrl() {
17253
18729
  return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
17254
18730
  }
@@ -17310,11 +18786,24 @@ function configureMcpClient(configPath) {
17310
18786
  return false;
17311
18787
  }
17312
18788
  }
17313
- function readBody(req) {
18789
+ function readBody(req, maxBytes = 16 * 1024) {
17314
18790
  return new Promise((resolve3, reject) => {
17315
18791
  const chunks = [];
17316
- req.on("data", (chunk) => chunks.push(chunk));
17317
- req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
18792
+ let total = 0;
18793
+ let tooLarge = false;
18794
+ req.on("data", (chunk) => {
18795
+ if (tooLarge) return;
18796
+ total += chunk.length;
18797
+ if (total > maxBytes) {
18798
+ tooLarge = true;
18799
+ reject(new Error("request_too_large"));
18800
+ return;
18801
+ }
18802
+ chunks.push(chunk);
18803
+ });
18804
+ req.on("end", () => {
18805
+ if (!tooLarge) resolve3(Buffer.concat(chunks).toString("utf-8"));
18806
+ });
17318
18807
  req.on("error", reject);
17319
18808
  });
17320
18809
  }
@@ -17327,14 +18816,13 @@ async function completePkceExchange(codeVerifier, state) {
17327
18816
  body: JSON.stringify({ code_verifier: codeVerifier, state })
17328
18817
  });
17329
18818
  if (!response.ok) {
17330
- const text = await response.text();
17331
- console.error(` PKCE exchange failed: ${text}`);
18819
+ console.error(` PKCE exchange failed (HTTP ${response.status}).`);
17332
18820
  return false;
17333
18821
  }
17334
18822
  const data = await response.json();
17335
18823
  return data.success === true;
17336
- } catch (err) {
17337
- console.error(` PKCE exchange error: ${err instanceof Error ? err.message : String(err)}`);
18824
+ } catch {
18825
+ console.error(" PKCE exchange failed because the authentication service was unavailable.");
17338
18826
  return false;
17339
18827
  }
17340
18828
  }
@@ -17345,7 +18833,8 @@ async function runSetup() {
17345
18833
  console.error("");
17346
18834
  console.error(" Privacy Notice:");
17347
18835
  console.error(" - Your API key is stored locally in your OS keychain");
17348
- console.error(" - Tool invocations are logged for usage metering (no content stored)");
18836
+ console.error(" - CLI telemetry records tool name, status, and duration \u2014 not tool input/output");
18837
+ console.error(" - Service-side content processing follows https://socialneuron.com/privacy");
17349
18838
  console.error(" - Set DO_NOT_TRACK=1 to disable telemetry");
17350
18839
  console.error(" - Data export/delete: https://www.socialneuron.com/settings");
17351
18840
  console.error("");
@@ -17369,7 +18858,6 @@ async function runSetup() {
17369
18858
  const open = (await import("open")).default;
17370
18859
  await open(authorizeUrl.toString());
17371
18860
  console.error(" Opening browser for authorization...");
17372
- console.error(` URL: ${authorizeUrl.toString()}`);
17373
18861
  console.error("");
17374
18862
  console.error(" Waiting for authorization (timeout: 120s)...");
17375
18863
  } catch {
@@ -17381,12 +18869,20 @@ async function runSetup() {
17381
18869
  console.error(" Waiting for authorization (timeout: 120s)...");
17382
18870
  }
17383
18871
  const result = await new Promise((resolve3) => {
18872
+ const callbackOrigin = new URL(baseUrl).origin;
17384
18873
  const timeout = setTimeout(() => {
17385
18874
  server2.close();
17386
18875
  resolve3({ error: "Authorization timed out after 120 seconds." });
17387
18876
  }, 12e4);
17388
18877
  server2.on("request", async (req, res) => {
17389
- res.setHeader("Access-Control-Allow-Origin", "*");
18878
+ const requestOrigin = req.headers.origin;
18879
+ if (requestOrigin !== callbackOrigin) {
18880
+ res.writeHead(403, { "Content-Type": "application/json" });
18881
+ res.end(JSON.stringify({ error: "Origin not allowed" }));
18882
+ return;
18883
+ }
18884
+ res.setHeader("Access-Control-Allow-Origin", callbackOrigin);
18885
+ res.setHeader("Vary", "Origin");
17390
18886
  res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
17391
18887
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
17392
18888
  res.setHeader("Access-Control-Allow-Private-Network", "true");
@@ -17404,9 +18900,9 @@ async function runSetup() {
17404
18900
  res.end(JSON.stringify({ error: "State mismatch" }));
17405
18901
  return;
17406
18902
  }
17407
- if (!data.api_key) {
18903
+ if (!isValidSetupApiKey(data.api_key)) {
17408
18904
  res.writeHead(400, { "Content-Type": "application/json" });
17409
- res.end(JSON.stringify({ error: "Missing api_key" }));
18905
+ res.end(JSON.stringify({ error: "Invalid api_key" }));
17410
18906
  return;
17411
18907
  }
17412
18908
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -17448,9 +18944,9 @@ async function runSetup() {
17448
18944
  console.error(` Key prefix: ${apiKey.substring(0, 12)}...`);
17449
18945
  const configPaths = getConfigPaths();
17450
18946
  let configured = false;
17451
- for (const { path: path2, name } of configPaths) {
17452
- if (configureMcpClient(path2)) {
17453
- console.error(` Configured ${name}: ${path2}`);
18947
+ for (const { path: path3, name } of configPaths) {
18948
+ if (configureMcpClient(path3)) {
18949
+ console.error(` Configured ${name}: ${path3}`);
17454
18950
  configured = true;
17455
18951
  }
17456
18952
  }
@@ -17489,12 +18985,12 @@ __export(validation_cache_exports, {
17489
18985
  readValidationCache: () => readValidationCache,
17490
18986
  writeValidationCache: () => writeValidationCache
17491
18987
  });
17492
- import { createHash as createHash5 } from "node:crypto";
18988
+ import { createHash as createHash4 } from "node:crypto";
17493
18989
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "node:fs";
17494
18990
  import { join as join4 } from "node:path";
17495
18991
  import { homedir as homedir4 } from "node:os";
17496
18992
  function keyFingerprint(apiKey) {
17497
- return `sha256:${createHash5("sha256").update(apiKey, "utf8").digest("hex")}`;
18993
+ return `sha256:${createHash4("sha256").update(apiKey, "utf8").digest("hex")}`;
17498
18994
  }
17499
18995
  function readValidationCache(apiKey) {
17500
18996
  try {
@@ -17635,8 +19131,7 @@ async function runLoginDevice() {
17635
19131
  body: JSON.stringify({})
17636
19132
  });
17637
19133
  if (!response.ok) {
17638
- const text = await response.text();
17639
- console.error(` Error: Failed to create device code. ${text}`);
19134
+ console.error(` Error: Failed to create device code (HTTP ${response.status}).`);
17640
19135
  process.exit(1);
17641
19136
  }
17642
19137
  const data = await response.json();
@@ -17886,7 +19381,7 @@ async function runHealthCheck(options) {
17886
19381
  checks.push({
17887
19382
  name: "Connectivity",
17888
19383
  ok: false,
17889
- detail: err instanceof Error ? err.message : "Failed to reach server"
19384
+ detail: "Failed to reach the authentication service."
17890
19385
  });
17891
19386
  }
17892
19387
  }
@@ -17910,7 +19405,7 @@ async function runHealthCheck(options) {
17910
19405
  checks.push({
17911
19406
  name: "Connectivity",
17912
19407
  ok: false,
17913
- detail: err instanceof Error ? err.message : "Network error"
19408
+ detail: "Network request failed."
17914
19409
  });
17915
19410
  }
17916
19411
  if (apiKey) {
@@ -18111,17 +19606,17 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
18111
19606
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18112
19607
 
18113
19608
  // src/prompts.ts
18114
- import { z as z38 } from "zod";
19609
+ import { z as z40 } from "zod";
18115
19610
  function registerPrompts(server2) {
18116
19611
  server2.prompt(
18117
19612
  "create_weekly_content_plan",
18118
19613
  "Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
18119
19614
  {
18120
- niche: z38.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
18121
- platforms: z38.string().optional().describe(
19615
+ niche: z40.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
19616
+ platforms: z40.string().optional().describe(
18122
19617
  'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
18123
19618
  ),
18124
- tone: z38.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
19619
+ tone: z40.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
18125
19620
  },
18126
19621
  ({ niche, platforms, tone }) => {
18127
19622
  const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
@@ -18163,8 +19658,8 @@ After building the plan, use \`save_content_plan\` to save it.`
18163
19658
  "analyze_top_content",
18164
19659
  "Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
18165
19660
  {
18166
- timeframe: z38.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
18167
- platform: z38.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
19661
+ timeframe: z40.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
19662
+ platform: z40.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
18168
19663
  },
18169
19664
  ({ timeframe, platform: platform3 }) => {
18170
19665
  const period = timeframe || "30 days";
@@ -18201,10 +19696,10 @@ Format as a clear, actionable performance report.`
18201
19696
  "repurpose_content",
18202
19697
  "Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
18203
19698
  {
18204
- source: z38.string().describe(
19699
+ source: z40.string().describe(
18205
19700
  "The source content to repurpose \u2014 a URL, transcript, or the content text itself"
18206
19701
  ),
18207
- target_platforms: z38.string().optional().describe(
19702
+ target_platforms: z40.string().optional().describe(
18208
19703
  'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
18209
19704
  )
18210
19705
  },
@@ -18246,9 +19741,9 @@ For each piece, include the platform, format, character count, and suggested pos
18246
19741
  "setup_brand_voice",
18247
19742
  "Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
18248
19743
  {
18249
- brand_name: z38.string().describe("Your brand or business name"),
18250
- industry: z38.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
18251
- website: z38.string().optional().describe("Your website URL for context")
19744
+ brand_name: z40.string().describe("Your brand or business name"),
19745
+ industry: z40.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
19746
+ website: z40.string().optional().describe("Your website URL for context")
18252
19747
  },
18253
19748
  ({ brand_name, industry, website }) => {
18254
19749
  const industryContext = industry ? ` in the ${industry} space` : "";
@@ -18652,10 +20147,10 @@ var command = process.argv[2];
18652
20147
  if (command === "--version" || command === "-v") {
18653
20148
  const { readFileSync: readFileSync5 } = await import("node:fs");
18654
20149
  const { resolve: resolve3, dirname } = await import("node:path");
18655
- const { fileURLToPath } = await import("node:url");
20150
+ const { fileURLToPath: fileURLToPath3 } = await import("node:url");
18656
20151
  let version = MCP_VERSION;
18657
20152
  try {
18658
- const pkgPath = resolve3(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
20153
+ const pkgPath = resolve3(dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
18659
20154
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
18660
20155
  version = pkg.version;
18661
20156
  } catch {