@socialneuron/mcp-server 1.8.1 → 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/CHANGELOG.md +30 -0
- package/README.md +76 -47
- package/dist/apps/analytics-pulse/mcp-app.html +167 -0
- package/dist/apps/content-calendar/mcp-app.html +475 -0
- package/dist/http.js +1950 -398
- package/dist/index.js +1786 -345
- package/dist/sn.js +1771 -330
- package/package.json +14 -11
- package/tools.lock.json +116 -101
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.
|
|
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 {
|
|
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 (
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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})
|
|
748
|
+
error: `Validation failed (HTTP ${response.status}).`
|
|
622
749
|
};
|
|
623
750
|
}
|
|
624
|
-
|
|
625
|
-
|
|
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:
|
|
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 {
|
|
789
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
655
790
|
import { PostHog } from "posthog-node";
|
|
656
791
|
function hashUserId(userId) {
|
|
657
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1949
|
-
|
|
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:
|
|
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:
|
|
1964
|
-
error: `Forbidden (HTTP 403): ${
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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("
|
|
2977
|
+
const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
|
|
2681
2978
|
if (!rateLimit.allowed) {
|
|
2682
2979
|
return {
|
|
2683
2980
|
content: [
|
|
@@ -2818,7 +3115,7 @@ function registerContentTools(server2) {
|
|
|
2818
3115
|
isError: true
|
|
2819
3116
|
};
|
|
2820
3117
|
}
|
|
2821
|
-
const rateLimit = checkRateLimit("
|
|
3118
|
+
const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
|
|
2822
3119
|
if (!rateLimit.allowed) {
|
|
2823
3120
|
return {
|
|
2824
3121
|
content: [
|
|
@@ -2958,20 +3255,28 @@ function registerContentTools(server2) {
|
|
|
2958
3255
|
}
|
|
2959
3256
|
);
|
|
2960
3257
|
if (liveStatus) {
|
|
3258
|
+
const livePayload = buildCheckStatusPayload(job, liveStatus);
|
|
2961
3259
|
const lines2 = [
|
|
2962
3260
|
`Job: ${job.id}`,
|
|
2963
3261
|
`Type: ${job.job_type}`,
|
|
2964
3262
|
`Model: ${job.model}`,
|
|
2965
|
-
`Status: ${
|
|
2966
|
-
`Progress: ${
|
|
3263
|
+
`Status: ${livePayload.status}`,
|
|
3264
|
+
`Progress: ${livePayload.progress}%`
|
|
2967
3265
|
];
|
|
2968
|
-
if (
|
|
2969
|
-
lines2.push(`Result URL: ${
|
|
3266
|
+
if (livePayload.result_url) {
|
|
3267
|
+
lines2.push(`Result URL: ${livePayload.result_url}`);
|
|
2970
3268
|
}
|
|
2971
|
-
if (
|
|
2972
|
-
lines2.push(`Error: ${
|
|
3269
|
+
if (livePayload.error) {
|
|
3270
|
+
lines2.push(`Error: ${livePayload.error}`);
|
|
2973
3271
|
}
|
|
3272
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
|
|
3273
|
+
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
2974
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
|
+
}
|
|
2975
3280
|
lines2.push(`Created: ${job.created_at}`);
|
|
2976
3281
|
if (format === "json") {
|
|
2977
3282
|
return {
|
|
@@ -2989,7 +3294,7 @@ function registerContentTools(server2) {
|
|
|
2989
3294
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
2990
3295
|
asEnvelope2({
|
|
2991
3296
|
...liveStatus,
|
|
2992
|
-
...
|
|
3297
|
+
...livePayload
|
|
2993
3298
|
}),
|
|
2994
3299
|
null,
|
|
2995
3300
|
2
|
|
@@ -3032,7 +3337,14 @@ function registerContentTools(server2) {
|
|
|
3032
3337
|
if (job.error_message) {
|
|
3033
3338
|
lines.push(`Error: ${job.error_message}`);
|
|
3034
3339
|
}
|
|
3340
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
|
|
3341
|
+
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
3035
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
|
+
}
|
|
3036
3348
|
lines.push(`Created: ${job.created_at}`);
|
|
3037
3349
|
if (job.completed_at) {
|
|
3038
3350
|
lines.push(`Completed: ${job.completed_at}`);
|
|
@@ -3262,7 +3574,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3262
3574
|
};
|
|
3263
3575
|
}
|
|
3264
3576
|
const rateLimit = checkRateLimit(
|
|
3265
|
-
"
|
|
3577
|
+
"generation",
|
|
3266
3578
|
`generate_voiceover:${userId}`
|
|
3267
3579
|
);
|
|
3268
3580
|
if (!rateLimit.allowed) {
|
|
@@ -3429,7 +3741,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3429
3741
|
}
|
|
3430
3742
|
const userId = await getDefaultUserId();
|
|
3431
3743
|
const rateLimit = checkRateLimit(
|
|
3432
|
-
"
|
|
3744
|
+
"generation",
|
|
3433
3745
|
`generate_carousel:${userId}`
|
|
3434
3746
|
);
|
|
3435
3747
|
if (!rateLimit.allowed) {
|
|
@@ -3583,9 +3895,6 @@ var init_content = __esm({
|
|
|
3583
3895
|
// src/lib/sanitize-error.ts
|
|
3584
3896
|
function sanitizeError(error) {
|
|
3585
3897
|
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
3586
|
-
if (process.env.NODE_ENV !== "production") {
|
|
3587
|
-
console.error("[Error]", msg);
|
|
3588
|
-
}
|
|
3589
3898
|
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
3590
3899
|
if (pattern.test(msg)) {
|
|
3591
3900
|
return userMessage;
|
|
@@ -3799,6 +4108,7 @@ function evaluateQuality(input) {
|
|
|
3799
4108
|
const firstLine = caption.split("\n")[0]?.trim() ?? "";
|
|
3800
4109
|
const hashtags = countHashtags(caption);
|
|
3801
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;
|
|
3802
4112
|
const blockedTerms = [
|
|
3803
4113
|
...(input.brandAvoidPatterns ?? []).map((t) => t.trim()).filter(Boolean),
|
|
3804
4114
|
...(input.customBannedTerms ?? []).map((t) => t.trim()).filter(Boolean)
|
|
@@ -3808,11 +4118,12 @@ function evaluateQuality(input) {
|
|
|
3808
4118
|
if (firstLine.length >= 20 && firstLine.length <= 120) hookScore += 1;
|
|
3809
4119
|
if (/[!?]/.test(firstLine) || /\b\d+(\.\d+)?\b/.test(firstLine)) hookScore += 1;
|
|
3810
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);
|
|
3811
4122
|
categories.push({
|
|
3812
4123
|
name: "Hook Strength",
|
|
3813
4124
|
score: Math.min(5, hookScore),
|
|
3814
4125
|
maxScore: 5,
|
|
3815
|
-
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."
|
|
3816
4127
|
});
|
|
3817
4128
|
let clarityScore = 2;
|
|
3818
4129
|
if (caption.length >= 80 && caption.length <= 1200) clarityScore += 2;
|
|
@@ -3838,7 +4149,8 @@ function evaluateQuality(input) {
|
|
|
3838
4149
|
detail: "Length, title, and hashtag usage should match target platforms."
|
|
3839
4150
|
});
|
|
3840
4151
|
let brandScore = 3;
|
|
3841
|
-
const
|
|
4152
|
+
const rawBrandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
|
|
4153
|
+
const brandKeyword = rawBrandKeyword ? rawBrandKeyword.slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
|
|
3842
4154
|
if (brandKeyword && new RegExp("\\b" + brandKeyword + "\\b", "i").test(title + " " + caption))
|
|
3843
4155
|
brandScore += 1;
|
|
3844
4156
|
if (!/\b(you|your|customer|audience)\b/i.test(caption)) brandScore -= 1;
|
|
@@ -3849,11 +4161,12 @@ function evaluateQuality(input) {
|
|
|
3849
4161
|
brandScore -= Math.min(2, matched.length);
|
|
3850
4162
|
}
|
|
3851
4163
|
}
|
|
4164
|
+
if (isShortFormX) brandScore = Math.max(brandScore, 3);
|
|
3852
4165
|
categories.push({
|
|
3853
4166
|
name: "Brand Alignment",
|
|
3854
4167
|
score: Math.max(0, Math.min(5, brandScore)),
|
|
3855
4168
|
maxScore: 5,
|
|
3856
|
-
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."
|
|
3857
4170
|
});
|
|
3858
4171
|
let noveltyScore = 2;
|
|
3859
4172
|
if (/\b(case study|framework|workflow|playbook|breakdown|behind the scenes)\b/i.test(caption))
|
|
@@ -3864,21 +4177,23 @@ function evaluateQuality(input) {
|
|
|
3864
4177
|
const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
|
|
3865
4178
|
if (matched.length > 0) noveltyScore -= Math.min(2, matched.length);
|
|
3866
4179
|
}
|
|
4180
|
+
if (isShortFormX) noveltyScore = Math.max(noveltyScore, 3);
|
|
3867
4181
|
categories.push({
|
|
3868
4182
|
name: "Novelty",
|
|
3869
4183
|
score: Math.max(0, Math.min(5, noveltyScore)),
|
|
3870
4184
|
maxScore: 5,
|
|
3871
|
-
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."
|
|
3872
4186
|
});
|
|
3873
4187
|
let ctaScore = 2;
|
|
3874
4188
|
if (/\b(comment|reply|share|save|follow|subscribe|click|try|book|download)\b/i.test(caption))
|
|
3875
4189
|
ctaScore += 2;
|
|
3876
4190
|
if (/\?$/.test(firstLine)) ctaScore += 1;
|
|
4191
|
+
if (isShortFormX) ctaScore = Math.max(ctaScore, 3);
|
|
3877
4192
|
categories.push({
|
|
3878
4193
|
name: "CTA Strength",
|
|
3879
4194
|
score: Math.min(5, ctaScore),
|
|
3880
4195
|
maxScore: 5,
|
|
3881
|
-
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."
|
|
3882
4197
|
});
|
|
3883
4198
|
let safetyScore = 5;
|
|
3884
4199
|
if (/\b(guarantee|guaranteed|no risk|risk-free|always works|100%)\b/i.test(caption))
|
|
@@ -3916,7 +4231,7 @@ var init_quality = __esm({
|
|
|
3916
4231
|
|
|
3917
4232
|
// src/tools/distribution.ts
|
|
3918
4233
|
import { z as z3 } from "zod";
|
|
3919
|
-
import { createHash
|
|
4234
|
+
import { createHash } from "node:crypto";
|
|
3920
4235
|
function snakeToCamel(obj) {
|
|
3921
4236
|
const result = {};
|
|
3922
4237
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -3942,6 +4257,87 @@ function asEnvelope3(data) {
|
|
|
3942
4257
|
data
|
|
3943
4258
|
};
|
|
3944
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
|
+
}
|
|
3945
4341
|
function accountEffectiveStatus(account) {
|
|
3946
4342
|
return account.effective_status || account.status;
|
|
3947
4343
|
}
|
|
@@ -3960,22 +4356,11 @@ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
|
3960
4356
|
if (!accountIds) return void 0;
|
|
3961
4357
|
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
3962
4358
|
}
|
|
3963
|
-
function isAlreadyR2Signed(url) {
|
|
3964
|
-
try {
|
|
3965
|
-
const u = new URL(url);
|
|
3966
|
-
return u.searchParams.has("X-Amz-Signature");
|
|
3967
|
-
} catch {
|
|
3968
|
-
return false;
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
4359
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
3972
4360
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
3973
4361
|
if (!ssrf.isValid) {
|
|
3974
4362
|
return { error: ssrf.error ?? "URL rejected by SSRF check" };
|
|
3975
4363
|
}
|
|
3976
|
-
if (isAlreadyR2Signed(mediaUrl)) {
|
|
3977
|
-
return { signedUrl: mediaUrl, r2Key: "" };
|
|
3978
|
-
}
|
|
3979
4364
|
const { data, error } = await callEdgeFunction(
|
|
3980
4365
|
"upload-to-r2",
|
|
3981
4366
|
{ url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
|
|
@@ -3994,19 +4379,19 @@ function registerDistributionTools(server2) {
|
|
|
3994
4379
|
media_url: z3.string().optional().describe(
|
|
3995
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."
|
|
3996
4381
|
),
|
|
3997
|
-
media_urls: z3.array(z3.string()).optional().describe(
|
|
4382
|
+
media_urls: z3.array(z3.string().url()).min(2).max(10).optional().describe(
|
|
3998
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."
|
|
3999
4384
|
),
|
|
4000
4385
|
r2_key: z3.string().optional().describe(
|
|
4001
4386
|
"R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
|
|
4002
4387
|
),
|
|
4003
|
-
r2_keys: z3.array(z3.string()).optional().describe(
|
|
4388
|
+
r2_keys: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
4004
4389
|
"Array of R2 object keys for carousel posts. Each is signed on demand. Alternative to media_urls."
|
|
4005
4390
|
),
|
|
4006
4391
|
job_id: z3.string().optional().describe(
|
|
4007
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."
|
|
4008
4393
|
),
|
|
4009
|
-
job_ids: z3.array(z3.string()).optional().describe(
|
|
4394
|
+
job_ids: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
4010
4395
|
"Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
|
|
4011
4396
|
),
|
|
4012
4397
|
platform_metadata: z3.object({
|
|
@@ -4036,7 +4421,10 @@ function registerDistributionTools(server2) {
|
|
|
4036
4421
|
category_id: z3.string().optional(),
|
|
4037
4422
|
tags: z3.array(z3.string()).optional(),
|
|
4038
4423
|
made_for_kids: z3.boolean().optional(),
|
|
4039
|
-
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
|
+
)
|
|
4040
4428
|
}).optional(),
|
|
4041
4429
|
facebook: z3.object({
|
|
4042
4430
|
page_id: z3.string().optional().describe("Facebook Page ID to post to."),
|
|
@@ -4119,14 +4507,8 @@ function registerDistributionTools(server2) {
|
|
|
4119
4507
|
auto_rehost: z3.boolean().optional().describe(
|
|
4120
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."
|
|
4121
4509
|
),
|
|
4122
|
-
|
|
4123
|
-
"
|
|
4124
|
-
),
|
|
4125
|
-
origin: z3.enum(["human", "hermes", "user"]).optional().describe(
|
|
4126
|
-
"Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
|
|
4127
|
-
),
|
|
4128
|
-
hermes_run_id: z3.string().optional().describe(
|
|
4129
|
-
"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."
|
|
4130
4512
|
)
|
|
4131
4513
|
},
|
|
4132
4514
|
async ({
|
|
@@ -4149,9 +4531,7 @@ function registerDistributionTools(server2) {
|
|
|
4149
4531
|
platform_metadata,
|
|
4150
4532
|
account_id,
|
|
4151
4533
|
account_ids,
|
|
4152
|
-
|
|
4153
|
-
origin,
|
|
4154
|
-
hermes_run_id
|
|
4534
|
+
idempotency_key
|
|
4155
4535
|
}) => {
|
|
4156
4536
|
const format = response_format ?? "text";
|
|
4157
4537
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
@@ -4180,6 +4560,8 @@ function registerDistributionTools(server2) {
|
|
|
4180
4560
|
}
|
|
4181
4561
|
let resolvedMediaUrl = media_url;
|
|
4182
4562
|
let resolvedMediaUrls = media_urls;
|
|
4563
|
+
let resolvedMediaUrlIsTrustedR2 = false;
|
|
4564
|
+
let resolvedMediaUrlsAreTrustedR2 = (media_urls ?? []).map(() => false);
|
|
4183
4565
|
const signR2Key = async (key) => {
|
|
4184
4566
|
const cleanKey = key.startsWith("r2://") ? key.slice(5) : key;
|
|
4185
4567
|
const { data: signData } = await callEdgeFunction(
|
|
@@ -4197,8 +4579,11 @@ function registerDistributionTools(server2) {
|
|
|
4197
4579
|
);
|
|
4198
4580
|
const resultUrl = jobData?.job?.result_url;
|
|
4199
4581
|
if (!resultUrl) return null;
|
|
4200
|
-
if (!resultUrl.startsWith("http"))
|
|
4201
|
-
|
|
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 };
|
|
4202
4587
|
};
|
|
4203
4588
|
try {
|
|
4204
4589
|
if (r2_key && !resolvedMediaUrl) {
|
|
@@ -4215,6 +4600,7 @@ function registerDistributionTools(server2) {
|
|
|
4215
4600
|
};
|
|
4216
4601
|
}
|
|
4217
4602
|
resolvedMediaUrl = signed;
|
|
4603
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
4218
4604
|
} else if (job_id && !resolvedMediaUrl && !r2_key) {
|
|
4219
4605
|
const resolved = await resolveJobId(job_id);
|
|
4220
4606
|
if (!resolved) {
|
|
@@ -4228,7 +4614,8 @@ function registerDistributionTools(server2) {
|
|
|
4228
4614
|
isError: true
|
|
4229
4615
|
};
|
|
4230
4616
|
}
|
|
4231
|
-
resolvedMediaUrl = resolved;
|
|
4617
|
+
resolvedMediaUrl = resolved.url;
|
|
4618
|
+
resolvedMediaUrlIsTrustedR2 = resolved.trustedR2;
|
|
4232
4619
|
}
|
|
4233
4620
|
if (r2_keys && r2_keys.length > 0 && !resolvedMediaUrls) {
|
|
4234
4621
|
const signed = await Promise.all(r2_keys.map(signR2Key));
|
|
@@ -4245,6 +4632,7 @@ function registerDistributionTools(server2) {
|
|
|
4245
4632
|
};
|
|
4246
4633
|
}
|
|
4247
4634
|
resolvedMediaUrls = signed;
|
|
4635
|
+
resolvedMediaUrlsAreTrustedR2 = signed.map(() => true);
|
|
4248
4636
|
} else if (job_ids && job_ids.length > 0 && !resolvedMediaUrls && !r2_keys) {
|
|
4249
4637
|
const resolved = await Promise.all(job_ids.map(resolveJobId));
|
|
4250
4638
|
const failIdx = resolved.findIndex((r) => !r);
|
|
@@ -4259,10 +4647,12 @@ function registerDistributionTools(server2) {
|
|
|
4259
4647
|
isError: true
|
|
4260
4648
|
};
|
|
4261
4649
|
}
|
|
4262
|
-
|
|
4650
|
+
const resolvedJobs = resolved;
|
|
4651
|
+
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
4652
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
|
|
4263
4653
|
}
|
|
4264
4654
|
const shouldRehost = auto_rehost !== false;
|
|
4265
|
-
if (shouldRehost && resolvedMediaUrl) {
|
|
4655
|
+
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
4266
4656
|
const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
|
|
4267
4657
|
if ("error" in rehost) {
|
|
4268
4658
|
return {
|
|
@@ -4276,10 +4666,13 @@ function registerDistributionTools(server2) {
|
|
|
4276
4666
|
};
|
|
4277
4667
|
}
|
|
4278
4668
|
resolvedMediaUrl = rehost.signedUrl;
|
|
4669
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
4279
4670
|
}
|
|
4280
4671
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
4281
4672
|
const rehosted = await Promise.all(
|
|
4282
|
-
resolvedMediaUrls.map(
|
|
4673
|
+
resolvedMediaUrls.map(
|
|
4674
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
|
|
4675
|
+
)
|
|
4283
4676
|
);
|
|
4284
4677
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
4285
4678
|
if (failIdx !== -1) {
|
|
@@ -4295,6 +4688,7 @@ function registerDistributionTools(server2) {
|
|
|
4295
4688
|
};
|
|
4296
4689
|
}
|
|
4297
4690
|
resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
|
|
4691
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedMediaUrls.map(() => true);
|
|
4298
4692
|
}
|
|
4299
4693
|
} catch (resolveErr) {
|
|
4300
4694
|
return {
|
|
@@ -4307,6 +4701,42 @@ function registerDistributionTools(server2) {
|
|
|
4307
4701
|
isError: true
|
|
4308
4702
|
};
|
|
4309
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
|
+
}
|
|
4310
4740
|
const normalizedPlatforms = platforms.map(
|
|
4311
4741
|
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
4312
4742
|
);
|
|
@@ -4432,12 +4862,32 @@ Created with Social Neuron`;
|
|
|
4432
4862
|
};
|
|
4433
4863
|
return true;
|
|
4434
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
|
+
}
|
|
4435
4885
|
const { data, error } = await callEdgeFunction(
|
|
4436
4886
|
"schedule-post",
|
|
4437
4887
|
{
|
|
4438
4888
|
mediaUrl: resolvedMediaUrl,
|
|
4439
4889
|
mediaUrls: resolvedMediaUrls,
|
|
4440
|
-
mediaType:
|
|
4890
|
+
mediaType: resolvedMediaType ?? void 0,
|
|
4441
4891
|
caption: finalCaption,
|
|
4442
4892
|
platforms: normalizedPlatforms,
|
|
4443
4893
|
title,
|
|
@@ -4450,14 +4900,10 @@ Created with Social Neuron`;
|
|
|
4450
4900
|
normalizedPlatformMetadata
|
|
4451
4901
|
)
|
|
4452
4902
|
} : {},
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
//
|
|
4457
|
-
// against the posts.origin CHECK constraint and persists hermesRunId when
|
|
4458
|
-
// origin === 'hermes'.
|
|
4459
|
-
...origin ? { origin } : {},
|
|
4460
|
-
...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.
|
|
4461
4907
|
},
|
|
4462
4908
|
{ timeoutMs: 3e4 }
|
|
4463
4909
|
);
|
|
@@ -4523,6 +4969,99 @@ Created with Social Neuron`;
|
|
|
4523
4969
|
};
|
|
4524
4970
|
}
|
|
4525
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
|
+
);
|
|
4526
5065
|
server2.tool(
|
|
4527
5066
|
"list_connected_accounts",
|
|
4528
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.",
|
|
@@ -4551,14 +5090,16 @@ Created with Social Neuron`;
|
|
|
4551
5090
|
isError: true
|
|
4552
5091
|
};
|
|
4553
5092
|
}
|
|
4554
|
-
const accounts = result.accounts ?? [];
|
|
5093
|
+
const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
4555
5094
|
if (accounts.length === 0) {
|
|
4556
5095
|
if (format === "json") {
|
|
5096
|
+
const structuredContent = asEnvelope3({ accounts: [] });
|
|
4557
5097
|
return {
|
|
5098
|
+
structuredContent,
|
|
4558
5099
|
content: [
|
|
4559
5100
|
{
|
|
4560
5101
|
type: "text",
|
|
4561
|
-
text: JSON.stringify(
|
|
5102
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4562
5103
|
}
|
|
4563
5104
|
]
|
|
4564
5105
|
};
|
|
@@ -4586,11 +5127,13 @@ Created with Social Neuron`;
|
|
|
4586
5127
|
);
|
|
4587
5128
|
}
|
|
4588
5129
|
if (format === "json") {
|
|
5130
|
+
const structuredContent = asEnvelope3({ accounts });
|
|
4589
5131
|
return {
|
|
5132
|
+
structuredContent,
|
|
4590
5133
|
content: [
|
|
4591
5134
|
{
|
|
4592
5135
|
type: "text",
|
|
4593
|
-
text: JSON.stringify(
|
|
5136
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4594
5137
|
}
|
|
4595
5138
|
]
|
|
4596
5139
|
};
|
|
@@ -4604,6 +5147,9 @@ Created with Social Neuron`;
|
|
|
4604
5147
|
"list_recent_posts",
|
|
4605
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.",
|
|
4606
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
|
+
),
|
|
4607
5153
|
platform: z3.enum([
|
|
4608
5154
|
"youtube",
|
|
4609
5155
|
"tiktok",
|
|
@@ -4619,13 +5165,27 @@ Created with Social Neuron`;
|
|
|
4619
5165
|
limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
4620
5166
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4621
5167
|
},
|
|
4622
|
-
async ({ platform: platform3, status, days, limit, response_format }) => {
|
|
5168
|
+
async ({ project_id, platform: platform3, status, days, limit, response_format }) => {
|
|
4623
5169
|
const format = response_format ?? "text";
|
|
4624
5170
|
const lookbackDays = days ?? 7;
|
|
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
|
+
}
|
|
4625
5183
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
4626
5184
|
action: "recent-posts",
|
|
4627
5185
|
days: lookbackDays,
|
|
4628
5186
|
limit: limit ?? 20,
|
|
5187
|
+
projectId: resolvedProjectId,
|
|
5188
|
+
project_id: resolvedProjectId,
|
|
4629
5189
|
...platform3 ? { platform: platform3 } : {},
|
|
4630
5190
|
...status ? { status } : {}
|
|
4631
5191
|
});
|
|
@@ -4640,7 +5200,7 @@ Created with Social Neuron`;
|
|
|
4640
5200
|
isError: true
|
|
4641
5201
|
};
|
|
4642
5202
|
}
|
|
4643
|
-
const rows = result.posts ?? [];
|
|
5203
|
+
const rows = (result.posts ?? []).map(publicPostRecord).filter((post) => post !== null);
|
|
4644
5204
|
if (rows.length === 0) {
|
|
4645
5205
|
if (format === "json") {
|
|
4646
5206
|
const structuredContent2 = asEnvelope3({ posts: [] });
|
|
@@ -4715,8 +5275,11 @@ Created with Social Neuron`;
|
|
|
4715
5275
|
};
|
|
4716
5276
|
server2.tool(
|
|
4717
5277
|
"find_next_slots",
|
|
4718
|
-
"Find optimal posting time slots based on
|
|
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.",
|
|
4719
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
|
+
),
|
|
4720
5283
|
platforms: z3.array(
|
|
4721
5284
|
z3.enum([
|
|
4722
5285
|
"youtube",
|
|
@@ -4735,6 +5298,7 @@ Created with Social Neuron`;
|
|
|
4735
5298
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
4736
5299
|
},
|
|
4737
5300
|
async ({
|
|
5301
|
+
project_id,
|
|
4738
5302
|
platforms,
|
|
4739
5303
|
count,
|
|
4740
5304
|
start_after,
|
|
@@ -4742,13 +5306,35 @@ Created with Social Neuron`;
|
|
|
4742
5306
|
response_format
|
|
4743
5307
|
}) => {
|
|
4744
5308
|
try {
|
|
4745
|
-
const
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
5330
|
+
const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
5331
|
+
const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
|
|
5332
|
+
action: "scheduled-posts",
|
|
5333
|
+
start_date: startDate.toISOString(),
|
|
4750
5334
|
end_date: endDate.toISOString(),
|
|
4751
|
-
statuses: ["scheduled", "draft"]
|
|
5335
|
+
statuses: ["pending", "scheduled", "draft"],
|
|
5336
|
+
projectId: resolvedProjectId,
|
|
5337
|
+
project_id: resolvedProjectId
|
|
4752
5338
|
});
|
|
4753
5339
|
const existingPosts = postsError ? [] : postsResult?.posts ?? [];
|
|
4754
5340
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
@@ -4788,19 +5374,17 @@ Created with Social Neuron`;
|
|
|
4788
5374
|
const slots = candidates.filter((s) => !s.conflict).sort((a, b) => b.engagement_score - a.engagement_score).slice(0, count);
|
|
4789
5375
|
const conflictsAvoided = candidates.filter((s) => s.conflict).length;
|
|
4790
5376
|
if (response_format === "json") {
|
|
5377
|
+
const structuredContent = asEnvelope3({
|
|
5378
|
+
slots,
|
|
5379
|
+
total_candidates: candidates.length,
|
|
5380
|
+
conflicts_avoided: conflictsAvoided
|
|
5381
|
+
});
|
|
4791
5382
|
return {
|
|
5383
|
+
structuredContent,
|
|
4792
5384
|
content: [
|
|
4793
5385
|
{
|
|
4794
5386
|
type: "text",
|
|
4795
|
-
text: JSON.stringify(
|
|
4796
|
-
asEnvelope3({
|
|
4797
|
-
slots,
|
|
4798
|
-
total_candidates: candidates.length,
|
|
4799
|
-
conflicts_avoided: conflictsAvoided
|
|
4800
|
-
}),
|
|
4801
|
-
null,
|
|
4802
|
-
2
|
|
4803
|
-
)
|
|
5387
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4804
5388
|
}
|
|
4805
5389
|
],
|
|
4806
5390
|
isError: false
|
|
@@ -5165,7 +5749,7 @@ Created with Social Neuron`;
|
|
|
5165
5749
|
const results = [];
|
|
5166
5750
|
const buildIdempotencyKey = (post) => {
|
|
5167
5751
|
const planId = effectivePlanId ?? (typeof workingPlan.plan_id === "string" ? String(workingPlan.plan_id) : "inline");
|
|
5168
|
-
const captionHash =
|
|
5752
|
+
const captionHash = createHash("sha256").update(post.caption).digest("hex").slice(0, 16);
|
|
5169
5753
|
const raw = [
|
|
5170
5754
|
"schedule_content_plan",
|
|
5171
5755
|
planId,
|
|
@@ -5175,7 +5759,7 @@ Created with Social Neuron`;
|
|
|
5175
5759
|
captionHash,
|
|
5176
5760
|
idempotency_seed ?? ""
|
|
5177
5761
|
].join(":");
|
|
5178
|
-
return `plan-${
|
|
5762
|
+
return `plan-${createHash("sha256").update(raw).digest("hex").slice(0, 48)}`;
|
|
5179
5763
|
};
|
|
5180
5764
|
const scheduleOne = async (post) => {
|
|
5181
5765
|
if (!post.schedule_at) {
|
|
@@ -5361,7 +5945,7 @@ Created with Social Neuron`;
|
|
|
5361
5945
|
}
|
|
5362
5946
|
);
|
|
5363
5947
|
}
|
|
5364
|
-
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;
|
|
5365
5949
|
var init_distribution = __esm({
|
|
5366
5950
|
"src/tools/distribution.ts"() {
|
|
5367
5951
|
"use strict";
|
|
@@ -5382,8 +5966,29 @@ var init_distribution = __esm({
|
|
|
5382
5966
|
threads: "Threads",
|
|
5383
5967
|
bluesky: "Bluesky"
|
|
5384
5968
|
};
|
|
5385
|
-
TIKTOK_AUDIT_APPROVED = false
|
|
5969
|
+
TIKTOK_AUDIT_APPROVED = !["false", "0", "no"].includes(
|
|
5970
|
+
(process.env.TIKTOK_AUDIT_APPROVED ?? "").toLowerCase()
|
|
5971
|
+
);
|
|
5386
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
|
+
]);
|
|
5387
5992
|
}
|
|
5388
5993
|
});
|
|
5389
5994
|
|
|
@@ -5664,7 +6269,10 @@ function registerMediaTools(server2) {
|
|
|
5664
6269
|
content: [
|
|
5665
6270
|
{
|
|
5666
6271
|
type: "text",
|
|
5667
|
-
|
|
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.`
|
|
5668
6276
|
}
|
|
5669
6277
|
],
|
|
5670
6278
|
isError: true
|
|
@@ -5907,7 +6515,11 @@ function registerAnalyticsTools(server2) {
|
|
|
5907
6515
|
action: "analytics",
|
|
5908
6516
|
platform: platform3,
|
|
5909
6517
|
days: lookbackDays,
|
|
5910
|
-
|
|
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,
|
|
5911
6523
|
contentId: content_id,
|
|
5912
6524
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
5913
6525
|
});
|
|
@@ -5917,7 +6529,14 @@ function registerAnalyticsTools(server2) {
|
|
|
5917
6529
|
isError: true
|
|
5918
6530
|
};
|
|
5919
6531
|
}
|
|
5920
|
-
const
|
|
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);
|
|
5921
6540
|
if (rows.length === 0) {
|
|
5922
6541
|
if (format === "json") {
|
|
5923
6542
|
const structuredContent = asEnvelope4({
|
|
@@ -6096,6 +6715,84 @@ var init_analytics = __esm({
|
|
|
6096
6715
|
}
|
|
6097
6716
|
});
|
|
6098
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
|
+
|
|
6099
6796
|
// src/tools/brand.ts
|
|
6100
6797
|
import { z as z6 } from "zod";
|
|
6101
6798
|
function asEnvelope5(data) {
|
|
@@ -6112,12 +6809,31 @@ function registerBrandTools(server2) {
|
|
|
6112
6809
|
"extract_brand",
|
|
6113
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.",
|
|
6114
6811
|
{
|
|
6115
|
-
url: z6.string().
|
|
6116
|
-
'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").'
|
|
6117
6814
|
),
|
|
6118
6815
|
response_format: z6.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6119
6816
|
},
|
|
6120
|
-
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;
|
|
6121
6837
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
6122
6838
|
if (!ssrfCheck.isValid) {
|
|
6123
6839
|
return {
|
|
@@ -6425,6 +7141,7 @@ var init_brand = __esm({
|
|
|
6425
7141
|
init_supabase();
|
|
6426
7142
|
init_ssrf();
|
|
6427
7143
|
init_version();
|
|
7144
|
+
init_brandUrlInput();
|
|
6428
7145
|
}
|
|
6429
7146
|
});
|
|
6430
7147
|
|
|
@@ -6800,7 +7517,7 @@ function registerRemotionTools(server2) {
|
|
|
6800
7517
|
},
|
|
6801
7518
|
async ({ composition_id, output_format, props }) => {
|
|
6802
7519
|
const userId = await getDefaultUserId();
|
|
6803
|
-
const rateLimit = checkRateLimit("
|
|
7520
|
+
const rateLimit = checkRateLimit("generation", `render_demo_video:${userId}`);
|
|
6804
7521
|
if (!rateLimit.allowed) {
|
|
6805
7522
|
return {
|
|
6806
7523
|
content: [
|
|
@@ -10634,7 +11351,9 @@ function toolKnowledgeDocument(tool) {
|
|
|
10634
11351
|
function getKnowledgeDocuments() {
|
|
10635
11352
|
return [
|
|
10636
11353
|
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
10637
|
-
...TOOL_CATALOG.filter((t) => !t.internal).map(
|
|
11354
|
+
...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
|
|
11355
|
+
toolKnowledgeDocument
|
|
11356
|
+
)
|
|
10638
11357
|
];
|
|
10639
11358
|
}
|
|
10640
11359
|
function tokenize(input) {
|
|
@@ -10751,7 +11470,7 @@ function registerDiscoveryTools(server2) {
|
|
|
10751
11470
|
if (query) {
|
|
10752
11471
|
results = searchTools(query);
|
|
10753
11472
|
}
|
|
10754
|
-
results = results.filter((t) => !t.internal);
|
|
11473
|
+
results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
|
|
10755
11474
|
if (module) {
|
|
10756
11475
|
const moduleTools = getToolsByModule(module);
|
|
10757
11476
|
results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
|
|
@@ -12682,11 +13401,11 @@ function hexToLab(hex) {
|
|
|
12682
13401
|
const lb = srgbToLinear(b);
|
|
12683
13402
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
12684
13403
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
12685
|
-
const
|
|
13404
|
+
const z41 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
12686
13405
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
12687
13406
|
const fx = f(x / 0.95047);
|
|
12688
13407
|
const fy = f(y / 1);
|
|
12689
|
-
const fz = f(
|
|
13408
|
+
const fz = f(z41 / 1.08883);
|
|
12690
13409
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
12691
13410
|
}
|
|
12692
13411
|
function deltaE2000(lab1, lab2) {
|
|
@@ -12926,10 +13645,10 @@ function registerBrandRuntimeTools(server2) {
|
|
|
12926
13645
|
pagesScraped: meta.pagesScraped || 0
|
|
12927
13646
|
}
|
|
12928
13647
|
};
|
|
12929
|
-
const
|
|
13648
|
+
const envelope2 = asEnvelope23(runtime);
|
|
12930
13649
|
return {
|
|
12931
|
-
structuredContent:
|
|
12932
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13650
|
+
structuredContent: envelope2,
|
|
13651
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
12933
13652
|
};
|
|
12934
13653
|
}
|
|
12935
13654
|
);
|
|
@@ -13069,9 +13788,9 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13069
13788
|
}
|
|
13070
13789
|
const profile = row.profile_data;
|
|
13071
13790
|
const checkResult = computeBrandConsistency(content, profile);
|
|
13072
|
-
const
|
|
13791
|
+
const envelope2 = asEnvelope23(checkResult);
|
|
13073
13792
|
return {
|
|
13074
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13793
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
13075
13794
|
};
|
|
13076
13795
|
}
|
|
13077
13796
|
);
|
|
@@ -13103,9 +13822,9 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13103
13822
|
content_colors,
|
|
13104
13823
|
threshold ?? 10
|
|
13105
13824
|
);
|
|
13106
|
-
const
|
|
13825
|
+
const envelope2 = asEnvelope23(auditResult);
|
|
13107
13826
|
return {
|
|
13108
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13827
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
13109
13828
|
};
|
|
13110
13829
|
}
|
|
13111
13830
|
);
|
|
@@ -13138,9 +13857,9 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13138
13857
|
row.profile_data.typography,
|
|
13139
13858
|
format
|
|
13140
13859
|
);
|
|
13141
|
-
const
|
|
13860
|
+
const envelope2 = asEnvelope23({ format, tokens: output });
|
|
13142
13861
|
return {
|
|
13143
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13862
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
13144
13863
|
};
|
|
13145
13864
|
}
|
|
13146
13865
|
);
|
|
@@ -13294,7 +14013,7 @@ function registerCarouselTools(server2) {
|
|
|
13294
14013
|
};
|
|
13295
14014
|
}
|
|
13296
14015
|
const userId = await getDefaultUserId();
|
|
13297
|
-
const rateLimit = checkRateLimit("
|
|
14016
|
+
const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
|
|
13298
14017
|
if (!rateLimit.allowed) {
|
|
13299
14018
|
return {
|
|
13300
14019
|
content: [
|
|
@@ -13366,7 +14085,12 @@ function registerCarouselTools(server2) {
|
|
|
13366
14085
|
slideNumber: slide.slideNumber,
|
|
13367
14086
|
jobId: null,
|
|
13368
14087
|
model: image_model,
|
|
13369
|
-
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
|
|
13370
14094
|
};
|
|
13371
14095
|
}
|
|
13372
14096
|
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
@@ -13378,14 +14102,24 @@ function registerCarouselTools(server2) {
|
|
|
13378
14102
|
slideNumber: slide.slideNumber,
|
|
13379
14103
|
jobId,
|
|
13380
14104
|
model: image_model,
|
|
13381
|
-
error: null
|
|
14105
|
+
error: null,
|
|
14106
|
+
creditsReserved: 0,
|
|
14107
|
+
creditsCharged: data.creditsDeducted ?? perImageCost,
|
|
14108
|
+
creditsRefunded: 0,
|
|
14109
|
+
billingStatus: "charged",
|
|
14110
|
+
failureReason: null
|
|
13382
14111
|
};
|
|
13383
14112
|
} catch (err) {
|
|
13384
14113
|
return {
|
|
13385
14114
|
slideNumber: slide.slideNumber,
|
|
13386
14115
|
jobId: null,
|
|
13387
14116
|
model: image_model,
|
|
13388
|
-
error: sanitizeError(err)
|
|
14117
|
+
error: sanitizeError(err),
|
|
14118
|
+
creditsReserved: null,
|
|
14119
|
+
creditsCharged: null,
|
|
14120
|
+
creditsRefunded: null,
|
|
14121
|
+
billingStatus: "unknown",
|
|
14122
|
+
failureReason: null
|
|
13389
14123
|
};
|
|
13390
14124
|
}
|
|
13391
14125
|
})
|
|
@@ -13410,7 +14144,8 @@ function registerCarouselTools(server2) {
|
|
|
13410
14144
|
return {
|
|
13411
14145
|
...s,
|
|
13412
14146
|
imageJobId: job?.jobId ?? null,
|
|
13413
|
-
imageError: job?.error ?? null
|
|
14147
|
+
imageError: job?.error ?? null,
|
|
14148
|
+
imageBillingStatus: job?.billingStatus ?? "unknown"
|
|
13414
14149
|
};
|
|
13415
14150
|
}),
|
|
13416
14151
|
imageModel: image_model,
|
|
@@ -13422,11 +14157,19 @@ function registerCarouselTools(server2) {
|
|
|
13422
14157
|
jobIds: successfulJobs.map((j) => j.jobId),
|
|
13423
14158
|
failedSlides: failedJobs.map((j) => ({
|
|
13424
14159
|
slideNumber: j.slideNumber,
|
|
13425
|
-
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
|
|
13426
14166
|
})),
|
|
13427
14167
|
credits: {
|
|
13428
14168
|
textGeneration: textCredits,
|
|
13429
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,
|
|
13430
14173
|
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
13431
14174
|
}
|
|
13432
14175
|
}
|
|
@@ -13622,7 +14365,7 @@ function registerHyperframesTools(server2) {
|
|
|
13622
14365
|
);
|
|
13623
14366
|
server2.tool(
|
|
13624
14367
|
"render_hyperframes",
|
|
13625
|
-
"Render an HTML video composition (Hyperframes) to MP4.
|
|
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.",
|
|
13626
14369
|
{
|
|
13627
14370
|
composition_html: z30.string().max(5e5).optional().describe(
|
|
13628
14371
|
"Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
|
|
@@ -13635,7 +14378,8 @@ function registerHyperframesTools(server2) {
|
|
|
13635
14378
|
duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
|
|
13636
14379
|
fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
|
|
13637
14380
|
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
|
|
13638
|
-
project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
|
|
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.")
|
|
13639
14383
|
},
|
|
13640
14384
|
async ({
|
|
13641
14385
|
composition_html,
|
|
@@ -13645,7 +14389,8 @@ function registerHyperframesTools(server2) {
|
|
|
13645
14389
|
duration_sec,
|
|
13646
14390
|
fps,
|
|
13647
14391
|
quality,
|
|
13648
|
-
project_id
|
|
14392
|
+
project_id,
|
|
14393
|
+
response_format
|
|
13649
14394
|
}) => {
|
|
13650
14395
|
const userId = await getDefaultUserId();
|
|
13651
14396
|
const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
|
|
@@ -13696,11 +14441,23 @@ function registerHyperframesTools(server2) {
|
|
|
13696
14441
|
if (error || !data?.jobId) {
|
|
13697
14442
|
throw new Error(error || data?.error || "Failed to create Hyperframes render job");
|
|
13698
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
|
+
};
|
|
13699
14456
|
return {
|
|
13700
14457
|
content: [
|
|
13701
14458
|
{
|
|
13702
14459
|
type: "text",
|
|
13703
|
-
text: [
|
|
14460
|
+
text: response_format === "json" ? JSON.stringify({ data: payload }) : [
|
|
13704
14461
|
`Hyperframes render job queued.`,
|
|
13705
14462
|
` Job ID: ${data.jobId}`,
|
|
13706
14463
|
` Credits: ${data.creditsCost}`,
|
|
@@ -13717,7 +14474,7 @@ function registerHyperframesTools(server2) {
|
|
|
13717
14474
|
content: [
|
|
13718
14475
|
{
|
|
13719
14476
|
type: "text",
|
|
13720
|
-
text: `Failed to queue Hyperframes render: ${err
|
|
14477
|
+
text: `Failed to queue Hyperframes render: ${sanitizeError(err)}`
|
|
13721
14478
|
}
|
|
13722
14479
|
],
|
|
13723
14480
|
isError: true
|
|
@@ -13733,6 +14490,7 @@ var init_hyperframes = __esm({
|
|
|
13733
14490
|
init_rate_limit();
|
|
13734
14491
|
init_supabase();
|
|
13735
14492
|
init_edge_function();
|
|
14493
|
+
init_sanitize_error();
|
|
13736
14494
|
HYPERFRAMES_BLOCKS = [
|
|
13737
14495
|
// Transitions
|
|
13738
14496
|
{
|
|
@@ -13850,49 +14608,122 @@ import {
|
|
|
13850
14608
|
import { z as z31 } from "zod";
|
|
13851
14609
|
import fs from "node:fs/promises";
|
|
13852
14610
|
import path from "node:path";
|
|
14611
|
+
import { fileURLToPath } from "node:url";
|
|
13853
14612
|
function startOfCurrentWeekMonday() {
|
|
13854
14613
|
const now = /* @__PURE__ */ new Date();
|
|
13855
|
-
const day = now.
|
|
14614
|
+
const day = now.getUTCDay();
|
|
13856
14615
|
const monday = new Date(now);
|
|
13857
|
-
monday.setUTCDate(now.getUTCDate() - day +
|
|
14616
|
+
monday.setUTCDate(now.getUTCDate() - (day + 6) % 7);
|
|
14617
|
+
monday.setUTCHours(0, 0, 0, 0);
|
|
13858
14618
|
return monday.toISOString().split("T")[0];
|
|
13859
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
|
+
}
|
|
13860
14670
|
function registerContentCalendarApp(server2) {
|
|
13861
14671
|
registerAppTool(
|
|
13862
14672
|
server2,
|
|
13863
14673
|
"open_content_calendar",
|
|
13864
14674
|
{
|
|
13865
14675
|
title: "Content Calendar",
|
|
13866
|
-
description: "Open
|
|
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.",
|
|
13867
14677
|
inputSchema: {
|
|
13868
|
-
|
|
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(
|
|
13869
14682
|
"ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
|
|
13870
14683
|
)
|
|
13871
14684
|
},
|
|
13872
14685
|
outputSchema: {
|
|
13873
14686
|
start_date: z31.string(),
|
|
14687
|
+
project_id: z31.string(),
|
|
13874
14688
|
posts: z31.array(RecentPostOutputSchema),
|
|
13875
14689
|
scopes: z31.array(z31.string())
|
|
13876
14690
|
},
|
|
13877
14691
|
_meta: {
|
|
13878
14692
|
ui: {
|
|
13879
|
-
resourceUri: CALENDAR_URI
|
|
13880
|
-
csp: {
|
|
13881
|
-
"img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
|
|
13882
|
-
"connect-src": ["'self'"]
|
|
13883
|
-
}
|
|
14693
|
+
resourceUri: CALENDAR_URI
|
|
13884
14694
|
}
|
|
13885
14695
|
}
|
|
13886
14696
|
},
|
|
13887
|
-
async ({ start_date }
|
|
13888
|
-
const userScopes =
|
|
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
|
+
}
|
|
13889
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
|
+
}
|
|
13890
14718
|
const { data: result, error } = await callEdgeFunction(
|
|
13891
14719
|
"mcp-data",
|
|
13892
14720
|
{
|
|
13893
|
-
action: "
|
|
13894
|
-
|
|
13895
|
-
|
|
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
|
|
13896
14727
|
},
|
|
13897
14728
|
{ timeoutMs: 15e3 }
|
|
13898
14729
|
);
|
|
@@ -13901,19 +14732,16 @@ function registerContentCalendarApp(server2) {
|
|
|
13901
14732
|
content: [
|
|
13902
14733
|
{
|
|
13903
14734
|
type: "text",
|
|
13904
|
-
text:
|
|
14735
|
+
text: "The content calendar could not load posts. Please retry."
|
|
13905
14736
|
}
|
|
13906
14737
|
],
|
|
13907
14738
|
isError: true
|
|
13908
14739
|
};
|
|
13909
14740
|
}
|
|
13910
|
-
const posts = (result.posts ?? []).filter((
|
|
13911
|
-
const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
|
|
13912
|
-
if (!ts) return false;
|
|
13913
|
-
return ts.split("T")[0] >= fromDate;
|
|
13914
|
-
});
|
|
14741
|
+
const posts = (result.posts ?? []).map(publicRecentPost).filter((post) => post !== null);
|
|
13915
14742
|
const structuredContent = {
|
|
13916
14743
|
start_date: fromDate,
|
|
14744
|
+
project_id: resolvedProjectId,
|
|
13917
14745
|
posts,
|
|
13918
14746
|
scopes: userScopes
|
|
13919
14747
|
};
|
|
@@ -13932,37 +14760,60 @@ function registerContentCalendarApp(server2) {
|
|
|
13932
14760
|
server2,
|
|
13933
14761
|
CALENDAR_URI,
|
|
13934
14762
|
CALENDAR_URI,
|
|
13935
|
-
{
|
|
14763
|
+
{
|
|
14764
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
14765
|
+
description: "Self-contained Social Neuron project content calendar.",
|
|
14766
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
14767
|
+
},
|
|
13936
14768
|
async () => {
|
|
13937
|
-
const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
|
|
13938
14769
|
try {
|
|
13939
|
-
const html = await
|
|
14770
|
+
const html = await readCalendarHtml();
|
|
13940
14771
|
return {
|
|
13941
|
-
contents: [
|
|
14772
|
+
contents: [
|
|
14773
|
+
{
|
|
14774
|
+
uri: CALENDAR_URI,
|
|
14775
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
14776
|
+
text: html,
|
|
14777
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
14778
|
+
}
|
|
14779
|
+
]
|
|
13942
14780
|
};
|
|
13943
|
-
} catch
|
|
14781
|
+
} catch {
|
|
13944
14782
|
const errorHtml = `<!DOCTYPE html>
|
|
13945
14783
|
<html><head><title>Content Calendar \u2014 unavailable</title></head>
|
|
13946
14784
|
<body style="font-family:sans-serif;padding:24px;color:#444;">
|
|
13947
14785
|
<h2>Content Calendar app bundle missing</h2>
|
|
13948
|
-
<p>The
|
|
13949
|
-
<code>apps/content-calendar/dist/mcp-app.html</code> is not built.
|
|
13950
|
-
Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
|
|
13951
|
-
<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>
|
|
13952
14787
|
</body></html>`;
|
|
13953
14788
|
return {
|
|
13954
|
-
contents: [
|
|
14789
|
+
contents: [
|
|
14790
|
+
{
|
|
14791
|
+
uri: CALENDAR_URI,
|
|
14792
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
14793
|
+
text: errorHtml,
|
|
14794
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
14795
|
+
}
|
|
14796
|
+
]
|
|
13955
14797
|
};
|
|
13956
14798
|
}
|
|
13957
14799
|
}
|
|
13958
14800
|
);
|
|
13959
14801
|
}
|
|
13960
|
-
var CALENDAR_URI, RecentPostOutputSchema;
|
|
14802
|
+
var CALENDAR_URI, CALENDAR_CSP, RecentPostOutputSchema;
|
|
13961
14803
|
var init_content_calendar = __esm({
|
|
13962
14804
|
"src/apps/content-calendar.ts"() {
|
|
13963
14805
|
"use strict";
|
|
13964
14806
|
init_edge_function();
|
|
13965
|
-
|
|
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
|
+
};
|
|
13966
14817
|
RecentPostOutputSchema = z31.object({
|
|
13967
14818
|
id: z31.string(),
|
|
13968
14819
|
platform: z31.string(),
|
|
@@ -13976,8 +14827,257 @@ var init_content_calendar = __esm({
|
|
|
13976
14827
|
}
|
|
13977
14828
|
});
|
|
13978
14829
|
|
|
13979
|
-
// src/
|
|
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";
|
|
13980
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";
|
|
13981
15081
|
function findActiveAccount(accounts, platform3) {
|
|
13982
15082
|
const target = platform3.toLowerCase();
|
|
13983
15083
|
return accounts.find((a) => {
|
|
@@ -13990,16 +15090,16 @@ function registerConnectionTools(server2) {
|
|
|
13990
15090
|
"start_platform_connection",
|
|
13991
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.",
|
|
13992
15092
|
{
|
|
13993
|
-
platform:
|
|
13994
|
-
project_id:
|
|
15093
|
+
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
|
|
15094
|
+
project_id: z33.string().optional().describe(
|
|
13995
15095
|
"Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
|
|
13996
15096
|
),
|
|
13997
|
-
response_format:
|
|
15097
|
+
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
13998
15098
|
},
|
|
13999
15099
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
14000
15100
|
const format = response_format ?? "text";
|
|
14001
15101
|
const userId = await getDefaultUserId();
|
|
14002
|
-
const rl = checkRateLimit("
|
|
15102
|
+
const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
|
|
14003
15103
|
if (!rl.allowed) {
|
|
14004
15104
|
return {
|
|
14005
15105
|
content: [
|
|
@@ -14079,13 +15179,13 @@ function registerConnectionTools(server2) {
|
|
|
14079
15179
|
"wait_for_connection",
|
|
14080
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.",
|
|
14081
15181
|
{
|
|
14082
|
-
platform:
|
|
14083
|
-
project_id:
|
|
15182
|
+
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
15183
|
+
project_id: z33.string().optional().describe(
|
|
14084
15184
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
14085
15185
|
),
|
|
14086
|
-
timeout_s:
|
|
14087
|
-
poll_interval_s:
|
|
14088
|
-
response_format:
|
|
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.")
|
|
14089
15189
|
},
|
|
14090
15190
|
async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
|
|
14091
15191
|
const format = response_format ?? "text";
|
|
@@ -14241,7 +15341,7 @@ var init_connections = __esm({
|
|
|
14241
15341
|
});
|
|
14242
15342
|
|
|
14243
15343
|
// src/tools/harness.ts
|
|
14244
|
-
import { z as
|
|
15344
|
+
import { z as z34 } from "zod";
|
|
14245
15345
|
function registerHarnessTools(server2, _ctx) {
|
|
14246
15346
|
server2.tool(
|
|
14247
15347
|
"write_agent_reflection",
|
|
@@ -14346,38 +15446,38 @@ var init_harness = __esm({
|
|
|
14346
15446
|
"engager"
|
|
14347
15447
|
];
|
|
14348
15448
|
writeReflectionSchema = {
|
|
14349
|
-
reflection_text:
|
|
14350
|
-
generated_by_agent:
|
|
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(
|
|
14351
15451
|
"Which agent produced this reflection. Must be one of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
14352
15452
|
),
|
|
14353
|
-
provenance:
|
|
14354
|
-
content_history_id:
|
|
14355
|
-
outcome_event_id:
|
|
14356
|
-
prm_score_ids:
|
|
14357
|
-
handoff_ids:
|
|
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.")
|
|
14358
15458
|
}).strict().describe(
|
|
14359
15459
|
"Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
|
|
14360
15460
|
),
|
|
14361
|
-
brand_id:
|
|
14362
|
-
pipeline_id:
|
|
14363
|
-
post_id:
|
|
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.")
|
|
14364
15464
|
};
|
|
14365
15465
|
readReflectionSchema = {
|
|
14366
|
-
brand_id:
|
|
14367
|
-
generated_by_agent:
|
|
15466
|
+
brand_id: z34.string().describe("Brand profile UUID to read reflections for."),
|
|
15467
|
+
generated_by_agent: z34.enum(ALLOWED_AGENTS).optional().describe(
|
|
14368
15468
|
"Optional filter: only return reflections produced by this agent. One of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
14369
15469
|
),
|
|
14370
|
-
limit:
|
|
15470
|
+
limit: z34.number().int().min(1).max(100).optional().describe(
|
|
14371
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)."
|
|
14372
15472
|
)
|
|
14373
15473
|
};
|
|
14374
15474
|
recordOutcomeSchema = {
|
|
14375
|
-
decision_event_id:
|
|
14376
|
-
horizon:
|
|
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(
|
|
14377
15477
|
"Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
|
|
14378
15478
|
),
|
|
14379
|
-
reward:
|
|
14380
|
-
outcome_metrics:
|
|
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(
|
|
14381
15481
|
'Optional map of raw metric name \u2192 value (e.g. {"likes": 42, "reach": 1200}). Stored verbatim; not used for learning directly.'
|
|
14382
15482
|
)
|
|
14383
15483
|
};
|
|
@@ -14385,7 +15485,7 @@ var init_harness = __esm({
|
|
|
14385
15485
|
});
|
|
14386
15486
|
|
|
14387
15487
|
// src/tools/hermes.ts
|
|
14388
|
-
import { z as
|
|
15488
|
+
import { z as z35 } from "zod";
|
|
14389
15489
|
function asEnvelope25(data) {
|
|
14390
15490
|
return {
|
|
14391
15491
|
_meta: {
|
|
@@ -14401,12 +15501,12 @@ function registerHermesTools(server2) {
|
|
|
14401
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.",
|
|
14402
15502
|
{
|
|
14403
15503
|
platform: PLATFORM.describe("Target platform for the draft."),
|
|
14404
|
-
copy:
|
|
14405
|
-
project_id:
|
|
14406
|
-
media_url:
|
|
14407
|
-
hermes_run_id:
|
|
14408
|
-
source_intel_ids:
|
|
14409
|
-
response_format:
|
|
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()
|
|
14410
15510
|
},
|
|
14411
15511
|
async ({ platform: platform3, copy, project_id, media_url, hermes_run_id, response_format }) => {
|
|
14412
15512
|
const { data, error } = await callEdgeFunction(
|
|
@@ -14444,15 +15544,15 @@ function registerHermesTools(server2) {
|
|
|
14444
15544
|
"record_voice_lesson",
|
|
14445
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)".',
|
|
14446
15546
|
{
|
|
14447
|
-
project_id:
|
|
14448
|
-
lesson:
|
|
14449
|
-
evidence:
|
|
14450
|
-
engagement_lift_pct:
|
|
14451
|
-
sample_size:
|
|
14452
|
-
top_examples:
|
|
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.")
|
|
14453
15553
|
}).describe("Quantitative evidence backing the lesson."),
|
|
14454
|
-
applies_to:
|
|
14455
|
-
response_format:
|
|
15554
|
+
applies_to: z35.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
|
|
15555
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
14456
15556
|
},
|
|
14457
15557
|
async ({ project_id, lesson, evidence, applies_to, response_format }) => {
|
|
14458
15558
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -14488,10 +15588,10 @@ function registerHermesTools(server2) {
|
|
|
14488
15588
|
"record_observation",
|
|
14489
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.',
|
|
14490
15590
|
{
|
|
14491
|
-
summary:
|
|
14492
|
-
deltas:
|
|
14493
|
-
run_id:
|
|
14494
|
-
response_format:
|
|
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()
|
|
14495
15595
|
},
|
|
14496
15596
|
async ({ summary, deltas, run_id, response_format }) => {
|
|
14497
15597
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "record-observation", summary, deltas, run_id });
|
|
@@ -14518,13 +15618,13 @@ function registerHermesTools(server2) {
|
|
|
14518
15618
|
"record_intel_signal",
|
|
14519
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.",
|
|
14520
15620
|
{
|
|
14521
|
-
source:
|
|
14522
|
-
url:
|
|
14523
|
-
topic:
|
|
14524
|
-
title:
|
|
14525
|
-
summary:
|
|
14526
|
-
score:
|
|
14527
|
-
response_format:
|
|
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()
|
|
14528
15628
|
},
|
|
14529
15629
|
async ({ source, url, topic, title, summary, score, response_format }) => {
|
|
14530
15630
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -14562,16 +15662,16 @@ function registerHermesTools(server2) {
|
|
|
14562
15662
|
"record_campaign_spend",
|
|
14563
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.",
|
|
14564
15664
|
{
|
|
14565
|
-
campaign_id:
|
|
14566
|
-
category:
|
|
15665
|
+
campaign_id: z35.string().min(1).max(200).describe("Campaign identifier (slug)."),
|
|
15666
|
+
category: z35.enum([
|
|
14567
15667
|
"hermes_drafts",
|
|
14568
15668
|
"carousel_renders",
|
|
14569
15669
|
"analytics_pulls",
|
|
14570
15670
|
"paid_amplification",
|
|
14571
15671
|
"other"
|
|
14572
15672
|
]).describe("Cost category."),
|
|
14573
|
-
amount_usd:
|
|
14574
|
-
response_format:
|
|
15673
|
+
amount_usd: z35.number().min(0).describe("Amount in USD; supports 4 decimal places."),
|
|
15674
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
14575
15675
|
},
|
|
14576
15676
|
async ({ campaign_id, category, amount_usd, response_format }) => {
|
|
14577
15677
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -14606,7 +15706,7 @@ function registerHermesTools(server2) {
|
|
|
14606
15706
|
"get_active_campaigns",
|
|
14607
15707
|
"List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
|
|
14608
15708
|
{
|
|
14609
|
-
response_format:
|
|
15709
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
14610
15710
|
},
|
|
14611
15711
|
async ({ response_format }) => {
|
|
14612
15712
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "get-active-campaigns" });
|
|
@@ -14655,7 +15755,7 @@ var init_hermes = __esm({
|
|
|
14655
15755
|
"use strict";
|
|
14656
15756
|
init_edge_function();
|
|
14657
15757
|
init_version();
|
|
14658
|
-
PLATFORM =
|
|
15758
|
+
PLATFORM = z35.enum([
|
|
14659
15759
|
"instagram",
|
|
14660
15760
|
"twitter",
|
|
14661
15761
|
"linkedin",
|
|
@@ -14701,7 +15801,20 @@ var init_skills_manifest = __esm({
|
|
|
14701
15801
|
});
|
|
14702
15802
|
|
|
14703
15803
|
// src/tools/skills.ts
|
|
14704
|
-
import { z as
|
|
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
|
+
}
|
|
14705
15818
|
function asEnvelope26(data) {
|
|
14706
15819
|
return {
|
|
14707
15820
|
_meta: {
|
|
@@ -14729,19 +15842,88 @@ function registerSkillsTools(server2) {
|
|
|
14729
15842
|
"list_skills",
|
|
14730
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.',
|
|
14731
15844
|
{
|
|
14732
|
-
studio:
|
|
14733
|
-
|
|
14734
|
-
|
|
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
|
+
)
|
|
14735
15854
|
},
|
|
14736
15855
|
async ({ studio, featured_only, response_format }) => {
|
|
14737
|
-
const skills = listSkills({ studio, featuredOnly: featured_only });
|
|
14738
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 });
|
|
14739
15917
|
if (format === "json") {
|
|
14740
15918
|
return {
|
|
14741
15919
|
content: [
|
|
14742
15920
|
{
|
|
14743
15921
|
type: "text",
|
|
14744
|
-
text: JSON.stringify(
|
|
15922
|
+
text: JSON.stringify(
|
|
15923
|
+
asEnvelope26({ count: skills.length, skills }),
|
|
15924
|
+
null,
|
|
15925
|
+
2
|
|
15926
|
+
)
|
|
14745
15927
|
}
|
|
14746
15928
|
]
|
|
14747
15929
|
};
|
|
@@ -14751,7 +15933,9 @@ function registerSkillsTools(server2) {
|
|
|
14751
15933
|
content: [
|
|
14752
15934
|
{
|
|
14753
15935
|
type: "text",
|
|
14754
|
-
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
|
+
)
|
|
14755
15939
|
}
|
|
14756
15940
|
]
|
|
14757
15941
|
};
|
|
@@ -14770,18 +15954,22 @@ ${blocks}` }]
|
|
|
14770
15954
|
"run_skill",
|
|
14771
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.",
|
|
14772
15956
|
{
|
|
14773
|
-
skill_id:
|
|
15957
|
+
skill_id: z36.string().describe(
|
|
14774
15958
|
'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
|
|
14775
15959
|
),
|
|
14776
|
-
topic:
|
|
14777
|
-
|
|
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(
|
|
14778
15964
|
'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
|
|
14779
15965
|
),
|
|
14780
|
-
hook:
|
|
15966
|
+
hook: z36.string().optional().describe(
|
|
14781
15967
|
"Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
|
|
14782
15968
|
),
|
|
14783
|
-
cta:
|
|
14784
|
-
|
|
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.")
|
|
14785
15973
|
},
|
|
14786
15974
|
async ({ skill_id, topic, audience, hook, cta, response_format }) => {
|
|
14787
15975
|
const skill = getSkill(skill_id);
|
|
@@ -14849,19 +16037,96 @@ ${blocks}` }]
|
|
|
14849
16037
|
};
|
|
14850
16038
|
}
|
|
14851
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
|
+
);
|
|
14852
16108
|
}
|
|
14853
16109
|
var STUDIO_VALUES;
|
|
14854
16110
|
var init_skills = __esm({
|
|
14855
16111
|
"src/tools/skills.ts"() {
|
|
14856
16112
|
"use strict";
|
|
14857
16113
|
init_version();
|
|
16114
|
+
init_tool_error();
|
|
16115
|
+
init_edge_function();
|
|
14858
16116
|
init_skills_manifest();
|
|
14859
|
-
STUDIO_VALUES = [
|
|
16117
|
+
STUDIO_VALUES = [
|
|
16118
|
+
"video",
|
|
16119
|
+
"avatar",
|
|
16120
|
+
"carousel",
|
|
16121
|
+
"voice",
|
|
16122
|
+
"caption",
|
|
16123
|
+
"edit"
|
|
16124
|
+
];
|
|
14860
16125
|
}
|
|
14861
16126
|
});
|
|
14862
16127
|
|
|
14863
16128
|
// src/tools/loopPulse.ts
|
|
14864
|
-
import { z as
|
|
16129
|
+
import { z as z37 } from "zod";
|
|
14865
16130
|
function asEnvelope27(data) {
|
|
14866
16131
|
return {
|
|
14867
16132
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -14873,7 +16138,7 @@ function registerLoopPulseTools(server2) {
|
|
|
14873
16138
|
"get_loop_pulse",
|
|
14874
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.',
|
|
14875
16140
|
{
|
|
14876
|
-
response_format:
|
|
16141
|
+
response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
14877
16142
|
},
|
|
14878
16143
|
async ({ response_format }) => {
|
|
14879
16144
|
const format = response_format ?? "text";
|
|
@@ -14923,7 +16188,7 @@ var init_loopPulse = __esm({
|
|
|
14923
16188
|
});
|
|
14924
16189
|
|
|
14925
16190
|
// src/tools/banditState.ts
|
|
14926
|
-
import { z as
|
|
16191
|
+
import { z as z38 } from "zod";
|
|
14927
16192
|
function asEnvelope28(data) {
|
|
14928
16193
|
return {
|
|
14929
16194
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -14935,8 +16200,8 @@ function registerBanditStateTools(server2) {
|
|
|
14935
16200
|
"get_bandit_state",
|
|
14936
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).",
|
|
14937
16202
|
{
|
|
14938
|
-
project_id:
|
|
14939
|
-
platform:
|
|
16203
|
+
project_id: z38.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
|
|
16204
|
+
platform: z38.enum([
|
|
14940
16205
|
"instagram",
|
|
14941
16206
|
"tiktok",
|
|
14942
16207
|
"youtube",
|
|
@@ -14948,7 +16213,7 @@ function registerBanditStateTools(server2) {
|
|
|
14948
16213
|
]).optional().describe(
|
|
14949
16214
|
"Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
|
|
14950
16215
|
),
|
|
14951
|
-
arm_type:
|
|
16216
|
+
arm_type: z38.enum([
|
|
14952
16217
|
"hook_family",
|
|
14953
16218
|
"hook_type",
|
|
14954
16219
|
"format",
|
|
@@ -14962,8 +16227,8 @@ function registerBanditStateTools(server2) {
|
|
|
14962
16227
|
"posting_time_bucket",
|
|
14963
16228
|
"content_format"
|
|
14964
16229
|
]).optional().describe("Arm dimension. Omit to return all types grouped."),
|
|
14965
|
-
top_k:
|
|
14966
|
-
response_format:
|
|
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.")
|
|
14967
16232
|
},
|
|
14968
16233
|
async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
|
|
14969
16234
|
const format = response_format ?? "text";
|
|
@@ -15026,12 +16291,134 @@ var init_banditState = __esm({
|
|
|
15026
16291
|
}
|
|
15027
16292
|
});
|
|
15028
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
|
+
|
|
15029
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
|
+
}
|
|
15030
16416
|
function wrapToolWithScanner(toolName, handler) {
|
|
15031
16417
|
return async function scannerWrappedHandler(...handlerArgs) {
|
|
15032
16418
|
const args = handlerArgs[0];
|
|
15033
16419
|
const ctx = handlerArgs[1];
|
|
15034
|
-
const
|
|
16420
|
+
const scanArgs = redactBase64Payloads(args);
|
|
16421
|
+
const inputText = scanArgs === void 0 ? "{}" : typeof scanArgs === "string" ? scanArgs : JSON.stringify(scanArgs);
|
|
15035
16422
|
const inputScan = scan(inputText, {
|
|
15036
16423
|
mode: "block",
|
|
15037
16424
|
source: "mcp_tool_input",
|
|
@@ -15059,6 +16446,16 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
15059
16446
|
source: "mcp_tool_output",
|
|
15060
16447
|
user_id: ctx?.userId
|
|
15061
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
|
+
}
|
|
15062
16459
|
if (outputScan.sanitized_text !== void 0) {
|
|
15063
16460
|
try {
|
|
15064
16461
|
ctx?.logScan?.(toolName, "output", outputScan);
|
|
@@ -15067,7 +16464,10 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
15067
16464
|
try {
|
|
15068
16465
|
return JSON.parse(outputScan.sanitized_text);
|
|
15069
16466
|
} catch {
|
|
15070
|
-
return
|
|
16467
|
+
return toolError(
|
|
16468
|
+
"server_error",
|
|
16469
|
+
"The response could not be returned safely. Please retry or contact support."
|
|
16470
|
+
);
|
|
15071
16471
|
}
|
|
15072
16472
|
}
|
|
15073
16473
|
return result;
|
|
@@ -15117,8 +16517,7 @@ function applyScopeEnforcement(server2, scopeResolver) {
|
|
|
15117
16517
|
details: {
|
|
15118
16518
|
source: "wrapper",
|
|
15119
16519
|
// A thrown exception escaped the handler — an unclassified fault.
|
|
15120
|
-
error_type: "server_error"
|
|
15121
|
-
exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
|
|
16520
|
+
error_type: "server_error"
|
|
15122
16521
|
}
|
|
15123
16522
|
});
|
|
15124
16523
|
throw err;
|
|
@@ -15250,12 +16649,14 @@ function registerAllTools(server2, options) {
|
|
|
15250
16649
|
registerSkillsTools(server2);
|
|
15251
16650
|
registerLoopPulseTools(server2);
|
|
15252
16651
|
registerBanditStateTools(server2);
|
|
16652
|
+
registerLifecycleTools(server2);
|
|
15253
16653
|
if (!options?.skipApps) {
|
|
15254
16654
|
registerContentCalendarApp(server2);
|
|
16655
|
+
registerAnalyticsPulseApp(server2);
|
|
15255
16656
|
}
|
|
15256
16657
|
applyAnnotations(server2);
|
|
15257
16658
|
}
|
|
15258
|
-
var RESPONSE_CHAR_LIMIT;
|
|
16659
|
+
var BASE64_PAYLOAD_KEYS, DATA_URI_PREFIX2, STRICT_BASE64, RESPONSE_CHAR_LIMIT;
|
|
15259
16660
|
var init_register_tools = __esm({
|
|
15260
16661
|
"src/lib/register-tools.ts"() {
|
|
15261
16662
|
"use strict";
|
|
@@ -15297,18 +16698,23 @@ var init_register_tools = __esm({
|
|
|
15297
16698
|
init_niche_research();
|
|
15298
16699
|
init_hyperframes();
|
|
15299
16700
|
init_content_calendar();
|
|
16701
|
+
init_analytics_pulse();
|
|
15300
16702
|
init_connections();
|
|
15301
16703
|
init_harness();
|
|
15302
16704
|
init_hermes();
|
|
15303
16705
|
init_skills();
|
|
15304
16706
|
init_loopPulse();
|
|
15305
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}$/;
|
|
15306
16712
|
RESPONSE_CHAR_LIMIT = 1e5;
|
|
15307
16713
|
}
|
|
15308
16714
|
});
|
|
15309
16715
|
|
|
15310
16716
|
// src/cli/sn/parse.ts
|
|
15311
|
-
import { createHash as
|
|
16717
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
15312
16718
|
function parseSnArgs(argv) {
|
|
15313
16719
|
const parsed = { _: [] };
|
|
15314
16720
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -15338,8 +16744,8 @@ function isEnabledFlag(value) {
|
|
|
15338
16744
|
}
|
|
15339
16745
|
function emitSnResult(payload, asJson) {
|
|
15340
16746
|
if (asJson) {
|
|
15341
|
-
const
|
|
15342
|
-
process.stdout.write(JSON.stringify(
|
|
16747
|
+
const envelope2 = { schema_version: "1", ...payload };
|
|
16748
|
+
process.stdout.write(JSON.stringify(envelope2, null, 2) + "\n");
|
|
15343
16749
|
}
|
|
15344
16750
|
}
|
|
15345
16751
|
function isValidHttpsUrl(value) {
|
|
@@ -15374,7 +16780,7 @@ async function checkUrlReachability(url) {
|
|
|
15374
16780
|
} catch (error) {
|
|
15375
16781
|
return {
|
|
15376
16782
|
ok: false,
|
|
15377
|
-
error:
|
|
16783
|
+
error: "Network request failed."
|
|
15378
16784
|
};
|
|
15379
16785
|
} finally {
|
|
15380
16786
|
clearTimeout(timer);
|
|
@@ -15404,7 +16810,7 @@ function buildPublishIdempotencyKey(input) {
|
|
|
15404
16810
|
title: input.title ?? "",
|
|
15405
16811
|
scheduledAt: input.scheduledAt ?? ""
|
|
15406
16812
|
});
|
|
15407
|
-
return `sn_${
|
|
16813
|
+
return `sn_${createHash2("sha256").update(material).digest("hex").slice(0, 24)}`;
|
|
15408
16814
|
}
|
|
15409
16815
|
function normalizePlatforms(platformsRaw) {
|
|
15410
16816
|
const caseMap = {
|
|
@@ -16464,17 +17870,21 @@ __export(discovery_exports, {
|
|
|
16464
17870
|
handleInfo: () => handleInfo,
|
|
16465
17871
|
handleTools: () => handleTools
|
|
16466
17872
|
});
|
|
17873
|
+
function publicStdioTools() {
|
|
17874
|
+
return TOOL_CATALOG.filter(
|
|
17875
|
+
(t) => !t.internal && !t.hiddenFromPublicCount && t.module !== "apps"
|
|
17876
|
+
);
|
|
17877
|
+
}
|
|
16467
17878
|
async function handleTools(args, asJson) {
|
|
16468
|
-
let tools =
|
|
17879
|
+
let tools = publicStdioTools();
|
|
16469
17880
|
const scope = args.scope;
|
|
16470
17881
|
if (typeof scope === "string") {
|
|
16471
|
-
tools =
|
|
17882
|
+
tools = tools.filter((tool) => tool.scope === scope);
|
|
16472
17883
|
}
|
|
16473
17884
|
const module = args.module;
|
|
16474
17885
|
if (typeof module === "string") {
|
|
16475
|
-
tools =
|
|
17886
|
+
tools = tools.filter((tool) => tool.module === module);
|
|
16476
17887
|
}
|
|
16477
|
-
tools = tools.filter((t) => !t.internal);
|
|
16478
17888
|
if (asJson) {
|
|
16479
17889
|
emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
|
|
16480
17890
|
return;
|
|
@@ -16503,27 +17913,31 @@ Module: ${moduleName} (${moduleTools.length} tools)`);
|
|
|
16503
17913
|
process.exit(0);
|
|
16504
17914
|
}
|
|
16505
17915
|
async function handleInfo(args, asJson) {
|
|
17916
|
+
const stdioTools = publicStdioTools();
|
|
16506
17917
|
const info = {
|
|
16507
17918
|
version: MCP_VERSION,
|
|
16508
|
-
toolCount:
|
|
16509
|
-
modules:
|
|
17919
|
+
toolCount: stdioTools.length,
|
|
17920
|
+
modules: Array.from(new Set(stdioTools.map((tool) => tool.module))).sort(),
|
|
17921
|
+
auth: null
|
|
16510
17922
|
};
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
|
|
16522
|
-
|
|
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
|
+
}
|
|
16523
17937
|
}
|
|
17938
|
+
} catch {
|
|
17939
|
+
info.auth = null;
|
|
16524
17940
|
}
|
|
16525
|
-
} catch {
|
|
16526
|
-
info.auth = null;
|
|
16527
17941
|
}
|
|
16528
17942
|
if (info.auth) {
|
|
16529
17943
|
try {
|
|
@@ -16586,7 +18000,11 @@ function getCallHandler() {
|
|
|
16586
18000
|
return cachedCallHandler;
|
|
16587
18001
|
}
|
|
16588
18002
|
function restToolNames() {
|
|
16589
|
-
return new Set(
|
|
18003
|
+
return new Set(
|
|
18004
|
+
TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal && !t.hiddenFromPublicCount).map(
|
|
18005
|
+
(t) => t.name
|
|
18006
|
+
)
|
|
18007
|
+
);
|
|
16590
18008
|
}
|
|
16591
18009
|
async function invokeToolRest(name, args) {
|
|
16592
18010
|
const handler = getCallHandler();
|
|
@@ -17152,7 +18570,7 @@ function printSnUsage() {
|
|
|
17152
18570
|
console.error("");
|
|
17153
18571
|
console.error("Discovery:");
|
|
17154
18572
|
console.error(" tools [--scope <scope>] [--module <module>] [--json]");
|
|
17155
|
-
console.error(" info [--json]");
|
|
18573
|
+
console.error(" info [--offline] [--json]");
|
|
17156
18574
|
console.error("");
|
|
17157
18575
|
console.error("Content:");
|
|
17158
18576
|
console.error(
|
|
@@ -17285,10 +18703,11 @@ var setup_exports = {};
|
|
|
17285
18703
|
__export(setup_exports, {
|
|
17286
18704
|
generatePKCE: () => generatePKCE,
|
|
17287
18705
|
getAppBaseUrl: () => getAppBaseUrl,
|
|
18706
|
+
isValidSetupApiKey: () => isValidSetupApiKey,
|
|
17288
18707
|
runLogout: () => runLogout,
|
|
17289
18708
|
runSetup: () => runSetup
|
|
17290
18709
|
});
|
|
17291
|
-
import { createHash as
|
|
18710
|
+
import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "node:crypto";
|
|
17292
18711
|
import { createServer } from "node:http";
|
|
17293
18712
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
17294
18713
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
@@ -17297,12 +18716,15 @@ function base64url(buffer) {
|
|
|
17297
18716
|
return buffer.toString("base64url");
|
|
17298
18717
|
}
|
|
17299
18718
|
function generatePKCE() {
|
|
17300
|
-
const verifierBytes =
|
|
18719
|
+
const verifierBytes = randomBytes2(32);
|
|
17301
18720
|
const codeVerifier = base64url(verifierBytes);
|
|
17302
|
-
const challengeHash =
|
|
18721
|
+
const challengeHash = createHash3("sha256").update(codeVerifier).digest();
|
|
17303
18722
|
const codeChallenge = base64url(challengeHash);
|
|
17304
18723
|
return { codeVerifier, codeChallenge };
|
|
17305
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
|
+
}
|
|
17306
18728
|
function getAppBaseUrl() {
|
|
17307
18729
|
return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
|
|
17308
18730
|
}
|
|
@@ -17364,11 +18786,24 @@ function configureMcpClient(configPath) {
|
|
|
17364
18786
|
return false;
|
|
17365
18787
|
}
|
|
17366
18788
|
}
|
|
17367
|
-
function readBody(req) {
|
|
18789
|
+
function readBody(req, maxBytes = 16 * 1024) {
|
|
17368
18790
|
return new Promise((resolve3, reject) => {
|
|
17369
18791
|
const chunks = [];
|
|
17370
|
-
|
|
17371
|
-
|
|
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
|
+
});
|
|
17372
18807
|
req.on("error", reject);
|
|
17373
18808
|
});
|
|
17374
18809
|
}
|
|
@@ -17381,14 +18816,13 @@ async function completePkceExchange(codeVerifier, state) {
|
|
|
17381
18816
|
body: JSON.stringify({ code_verifier: codeVerifier, state })
|
|
17382
18817
|
});
|
|
17383
18818
|
if (!response.ok) {
|
|
17384
|
-
|
|
17385
|
-
console.error(` PKCE exchange failed: ${text}`);
|
|
18819
|
+
console.error(` PKCE exchange failed (HTTP ${response.status}).`);
|
|
17386
18820
|
return false;
|
|
17387
18821
|
}
|
|
17388
18822
|
const data = await response.json();
|
|
17389
18823
|
return data.success === true;
|
|
17390
|
-
} catch
|
|
17391
|
-
console.error(
|
|
18824
|
+
} catch {
|
|
18825
|
+
console.error(" PKCE exchange failed because the authentication service was unavailable.");
|
|
17392
18826
|
return false;
|
|
17393
18827
|
}
|
|
17394
18828
|
}
|
|
@@ -17399,7 +18833,8 @@ async function runSetup() {
|
|
|
17399
18833
|
console.error("");
|
|
17400
18834
|
console.error(" Privacy Notice:");
|
|
17401
18835
|
console.error(" - Your API key is stored locally in your OS keychain");
|
|
17402
|
-
console.error(" -
|
|
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");
|
|
17403
18838
|
console.error(" - Set DO_NOT_TRACK=1 to disable telemetry");
|
|
17404
18839
|
console.error(" - Data export/delete: https://www.socialneuron.com/settings");
|
|
17405
18840
|
console.error("");
|
|
@@ -17423,7 +18858,6 @@ async function runSetup() {
|
|
|
17423
18858
|
const open = (await import("open")).default;
|
|
17424
18859
|
await open(authorizeUrl.toString());
|
|
17425
18860
|
console.error(" Opening browser for authorization...");
|
|
17426
|
-
console.error(` URL: ${authorizeUrl.toString()}`);
|
|
17427
18861
|
console.error("");
|
|
17428
18862
|
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
17429
18863
|
} catch {
|
|
@@ -17435,12 +18869,20 @@ async function runSetup() {
|
|
|
17435
18869
|
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
17436
18870
|
}
|
|
17437
18871
|
const result = await new Promise((resolve3) => {
|
|
18872
|
+
const callbackOrigin = new URL(baseUrl).origin;
|
|
17438
18873
|
const timeout = setTimeout(() => {
|
|
17439
18874
|
server2.close();
|
|
17440
18875
|
resolve3({ error: "Authorization timed out after 120 seconds." });
|
|
17441
18876
|
}, 12e4);
|
|
17442
18877
|
server2.on("request", async (req, res) => {
|
|
17443
|
-
|
|
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");
|
|
17444
18886
|
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
17445
18887
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
17446
18888
|
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
@@ -17458,9 +18900,9 @@ async function runSetup() {
|
|
|
17458
18900
|
res.end(JSON.stringify({ error: "State mismatch" }));
|
|
17459
18901
|
return;
|
|
17460
18902
|
}
|
|
17461
|
-
if (!data.api_key) {
|
|
18903
|
+
if (!isValidSetupApiKey(data.api_key)) {
|
|
17462
18904
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
17463
|
-
res.end(JSON.stringify({ error: "
|
|
18905
|
+
res.end(JSON.stringify({ error: "Invalid api_key" }));
|
|
17464
18906
|
return;
|
|
17465
18907
|
}
|
|
17466
18908
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -17502,9 +18944,9 @@ async function runSetup() {
|
|
|
17502
18944
|
console.error(` Key prefix: ${apiKey.substring(0, 12)}...`);
|
|
17503
18945
|
const configPaths = getConfigPaths();
|
|
17504
18946
|
let configured = false;
|
|
17505
|
-
for (const { path:
|
|
17506
|
-
if (configureMcpClient(
|
|
17507
|
-
console.error(` Configured ${name}: ${
|
|
18947
|
+
for (const { path: path3, name } of configPaths) {
|
|
18948
|
+
if (configureMcpClient(path3)) {
|
|
18949
|
+
console.error(` Configured ${name}: ${path3}`);
|
|
17508
18950
|
configured = true;
|
|
17509
18951
|
}
|
|
17510
18952
|
}
|
|
@@ -17543,12 +18985,12 @@ __export(validation_cache_exports, {
|
|
|
17543
18985
|
readValidationCache: () => readValidationCache,
|
|
17544
18986
|
writeValidationCache: () => writeValidationCache
|
|
17545
18987
|
});
|
|
17546
|
-
import { createHash as
|
|
18988
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
17547
18989
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "node:fs";
|
|
17548
18990
|
import { join as join4 } from "node:path";
|
|
17549
18991
|
import { homedir as homedir4 } from "node:os";
|
|
17550
18992
|
function keyFingerprint(apiKey) {
|
|
17551
|
-
return `sha256:${
|
|
18993
|
+
return `sha256:${createHash4("sha256").update(apiKey, "utf8").digest("hex")}`;
|
|
17552
18994
|
}
|
|
17553
18995
|
function readValidationCache(apiKey) {
|
|
17554
18996
|
try {
|
|
@@ -17689,8 +19131,7 @@ async function runLoginDevice() {
|
|
|
17689
19131
|
body: JSON.stringify({})
|
|
17690
19132
|
});
|
|
17691
19133
|
if (!response.ok) {
|
|
17692
|
-
|
|
17693
|
-
console.error(` Error: Failed to create device code. ${text}`);
|
|
19134
|
+
console.error(` Error: Failed to create device code (HTTP ${response.status}).`);
|
|
17694
19135
|
process.exit(1);
|
|
17695
19136
|
}
|
|
17696
19137
|
const data = await response.json();
|
|
@@ -17940,7 +19381,7 @@ async function runHealthCheck(options) {
|
|
|
17940
19381
|
checks.push({
|
|
17941
19382
|
name: "Connectivity",
|
|
17942
19383
|
ok: false,
|
|
17943
|
-
detail:
|
|
19384
|
+
detail: "Failed to reach the authentication service."
|
|
17944
19385
|
});
|
|
17945
19386
|
}
|
|
17946
19387
|
}
|
|
@@ -17964,7 +19405,7 @@ async function runHealthCheck(options) {
|
|
|
17964
19405
|
checks.push({
|
|
17965
19406
|
name: "Connectivity",
|
|
17966
19407
|
ok: false,
|
|
17967
|
-
detail:
|
|
19408
|
+
detail: "Network request failed."
|
|
17968
19409
|
});
|
|
17969
19410
|
}
|
|
17970
19411
|
if (apiKey) {
|
|
@@ -18165,17 +19606,17 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
|
|
|
18165
19606
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18166
19607
|
|
|
18167
19608
|
// src/prompts.ts
|
|
18168
|
-
import { z as
|
|
19609
|
+
import { z as z40 } from "zod";
|
|
18169
19610
|
function registerPrompts(server2) {
|
|
18170
19611
|
server2.prompt(
|
|
18171
19612
|
"create_weekly_content_plan",
|
|
18172
19613
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
18173
19614
|
{
|
|
18174
|
-
niche:
|
|
18175
|
-
platforms:
|
|
19615
|
+
niche: z40.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
19616
|
+
platforms: z40.string().optional().describe(
|
|
18176
19617
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
18177
19618
|
),
|
|
18178
|
-
tone:
|
|
19619
|
+
tone: z40.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
18179
19620
|
},
|
|
18180
19621
|
({ niche, platforms, tone }) => {
|
|
18181
19622
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -18217,8 +19658,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
18217
19658
|
"analyze_top_content",
|
|
18218
19659
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
18219
19660
|
{
|
|
18220
|
-
timeframe:
|
|
18221
|
-
platform:
|
|
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")')
|
|
18222
19663
|
},
|
|
18223
19664
|
({ timeframe, platform: platform3 }) => {
|
|
18224
19665
|
const period = timeframe || "30 days";
|
|
@@ -18255,10 +19696,10 @@ Format as a clear, actionable performance report.`
|
|
|
18255
19696
|
"repurpose_content",
|
|
18256
19697
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
18257
19698
|
{
|
|
18258
|
-
source:
|
|
19699
|
+
source: z40.string().describe(
|
|
18259
19700
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
18260
19701
|
),
|
|
18261
|
-
target_platforms:
|
|
19702
|
+
target_platforms: z40.string().optional().describe(
|
|
18262
19703
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
18263
19704
|
)
|
|
18264
19705
|
},
|
|
@@ -18300,9 +19741,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
18300
19741
|
"setup_brand_voice",
|
|
18301
19742
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
18302
19743
|
{
|
|
18303
|
-
brand_name:
|
|
18304
|
-
industry:
|
|
18305
|
-
website:
|
|
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")
|
|
18306
19747
|
},
|
|
18307
19748
|
({ brand_name, industry, website }) => {
|
|
18308
19749
|
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
@@ -18706,10 +20147,10 @@ var command = process.argv[2];
|
|
|
18706
20147
|
if (command === "--version" || command === "-v") {
|
|
18707
20148
|
const { readFileSync: readFileSync5 } = await import("node:fs");
|
|
18708
20149
|
const { resolve: resolve3, dirname } = await import("node:path");
|
|
18709
|
-
const { fileURLToPath } = await import("node:url");
|
|
20150
|
+
const { fileURLToPath: fileURLToPath3 } = await import("node:url");
|
|
18710
20151
|
let version = MCP_VERSION;
|
|
18711
20152
|
try {
|
|
18712
|
-
const pkgPath = resolve3(dirname(
|
|
20153
|
+
const pkgPath = resolve3(dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
18713
20154
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
18714
20155
|
version = pkg.version;
|
|
18715
20156
|
} catch {
|