@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/sn.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
|
|
|
@@ -66,25 +66,140 @@ __export(credentials_exports, {
|
|
|
66
66
|
saveSupabaseUrl: () => saveSupabaseUrl
|
|
67
67
|
});
|
|
68
68
|
import { execFileSync } from "node:child_process";
|
|
69
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
closeSync,
|
|
71
|
+
constants,
|
|
72
|
+
existsSync,
|
|
73
|
+
fchmodSync,
|
|
74
|
+
fstatSync,
|
|
75
|
+
ftruncateSync,
|
|
76
|
+
lstatSync,
|
|
77
|
+
mkdirSync,
|
|
78
|
+
openSync,
|
|
79
|
+
readFileSync,
|
|
80
|
+
unlinkSync,
|
|
81
|
+
writeFileSync
|
|
82
|
+
} from "node:fs";
|
|
70
83
|
import { homedir, platform } from "node:os";
|
|
71
84
|
import { join } from "node:path";
|
|
85
|
+
function assertSafeCredentialPaths() {
|
|
86
|
+
if (platform() === "win32") return;
|
|
87
|
+
const uid = process.getuid?.();
|
|
88
|
+
if (existsSync(CONFIG_DIR)) {
|
|
89
|
+
const directory = lstatSync(CONFIG_DIR);
|
|
90
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) {
|
|
91
|
+
throw new Error("Unsafe Social Neuron credential directory. Refusing to use it.");
|
|
92
|
+
}
|
|
93
|
+
if (uid !== void 0 && directory.uid !== uid) {
|
|
94
|
+
throw new Error("Social Neuron credential directory is not owned by the current user.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
98
|
+
const file = lstatSync(CREDENTIALS_FILE);
|
|
99
|
+
if (file.isSymbolicLink() || !file.isFile()) {
|
|
100
|
+
throw new Error("Unsafe Social Neuron credential file. Refusing to use it.");
|
|
101
|
+
}
|
|
102
|
+
if (uid !== void 0 && file.uid !== uid) {
|
|
103
|
+
throw new Error("Social Neuron credential file is not owned by the current user.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function openValidatedCredentialPath(target, kind, flags) {
|
|
108
|
+
let fd;
|
|
109
|
+
try {
|
|
110
|
+
fd = openSync(
|
|
111
|
+
target,
|
|
112
|
+
flags | constants.O_NOFOLLOW | (kind === "directory" ? constants.O_DIRECTORY : 0)
|
|
113
|
+
);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.code === "ENOENT") return null;
|
|
116
|
+
throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
|
|
117
|
+
}
|
|
118
|
+
const opened = fstatSync(fd);
|
|
119
|
+
const uid = process.getuid?.();
|
|
120
|
+
if ((kind === "directory" ? !opened.isDirectory() : !opened.isFile()) || uid !== void 0 && opened.uid !== uid || kind === "file" && opened.nlink !== 1) {
|
|
121
|
+
closeSync(fd);
|
|
122
|
+
throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
|
|
123
|
+
}
|
|
124
|
+
return fd;
|
|
125
|
+
}
|
|
126
|
+
function hardenCredentialPath(target, kind, mode) {
|
|
127
|
+
const fd = openValidatedCredentialPath(target, kind, constants.O_RDONLY);
|
|
128
|
+
if (fd === null) return;
|
|
129
|
+
try {
|
|
130
|
+
const opened = fstatSync(fd);
|
|
131
|
+
if ((opened.mode & 63) !== 0) fchmodSync(fd, mode);
|
|
132
|
+
} finally {
|
|
133
|
+
closeSync(fd);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function hardenCredentialPermissions() {
|
|
137
|
+
if (platform() === "win32") return;
|
|
138
|
+
hardenCredentialPath(CONFIG_DIR, "directory", 448);
|
|
139
|
+
hardenCredentialPath(CREDENTIALS_FILE, "file", 384);
|
|
140
|
+
}
|
|
72
141
|
function readCredentialsFile() {
|
|
142
|
+
if (platform() !== "win32") hardenCredentialPath(CONFIG_DIR, "directory", 448);
|
|
143
|
+
const fd = platform() === "win32" ? (() => {
|
|
144
|
+
try {
|
|
145
|
+
return openSync(CREDENTIALS_FILE, constants.O_RDONLY);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error.code === "ENOENT") return null;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
})() : openValidatedCredentialPath(CREDENTIALS_FILE, "file", constants.O_RDONLY);
|
|
151
|
+
if (fd === null) return {};
|
|
73
152
|
try {
|
|
74
|
-
if (
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
153
|
+
if (platform() !== "win32") fchmodSync(fd, 384);
|
|
154
|
+
const parsed = JSON.parse(readFileSync(fd, "utf-8"));
|
|
155
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
156
|
+
throw new Error("Social Neuron credential file is invalid.");
|
|
157
|
+
}
|
|
158
|
+
return parsed;
|
|
159
|
+
} finally {
|
|
160
|
+
closeSync(fd);
|
|
79
161
|
}
|
|
80
162
|
}
|
|
81
163
|
function writeCredentialsFile(data) {
|
|
82
164
|
if (!existsSync(CONFIG_DIR)) {
|
|
83
165
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
84
166
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
167
|
+
assertSafeCredentialPaths();
|
|
168
|
+
const payload = JSON.stringify(data, null, 2) + "\n";
|
|
169
|
+
if (platform() === "win32") {
|
|
170
|
+
writeFileSync(CREDENTIALS_FILE, payload, { mode: 384 });
|
|
171
|
+
} else {
|
|
172
|
+
let fd;
|
|
173
|
+
try {
|
|
174
|
+
fd = openSync(
|
|
175
|
+
CREDENTIALS_FILE,
|
|
176
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
177
|
+
384
|
|
178
|
+
);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error.code !== "EEXIST") {
|
|
181
|
+
throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
fd = openSync(CREDENTIALS_FILE, constants.O_WRONLY | constants.O_NOFOLLOW);
|
|
185
|
+
} catch {
|
|
186
|
+
throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const opened = fstatSync(fd);
|
|
191
|
+
const uid = process.getuid?.();
|
|
192
|
+
if (!opened.isFile() || opened.nlink !== 1 || uid !== void 0 && opened.uid !== uid) {
|
|
193
|
+
throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
|
|
194
|
+
}
|
|
195
|
+
fchmodSync(fd, 384);
|
|
196
|
+
ftruncateSync(fd, 0);
|
|
197
|
+
writeFileSync(fd, payload);
|
|
198
|
+
} finally {
|
|
199
|
+
closeSync(fd);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
hardenCredentialPermissions();
|
|
88
203
|
}
|
|
89
204
|
function macKeychainRead(service) {
|
|
90
205
|
try {
|
|
@@ -294,7 +409,6 @@ async function validateApiKey(apiKey, _attempt = 0) {
|
|
|
294
409
|
}
|
|
295
410
|
);
|
|
296
411
|
if (!response.ok) {
|
|
297
|
-
const text = await response.text();
|
|
298
412
|
const retryable = response.status === 429 || response.status >= 500;
|
|
299
413
|
if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
|
|
300
414
|
await sleep(300 * (_attempt + 1));
|
|
@@ -303,11 +417,19 @@ async function validateApiKey(apiKey, _attempt = 0) {
|
|
|
303
417
|
return {
|
|
304
418
|
valid: false,
|
|
305
419
|
retryable,
|
|
306
|
-
error: `Validation failed (HTTP ${response.status})
|
|
420
|
+
error: `Validation failed (HTTP ${response.status}).`
|
|
307
421
|
};
|
|
308
422
|
}
|
|
309
|
-
|
|
310
|
-
|
|
423
|
+
const result = await response.json();
|
|
424
|
+
if (!result.valid) {
|
|
425
|
+
return {
|
|
426
|
+
valid: false,
|
|
427
|
+
retryable: false,
|
|
428
|
+
error: "API key is invalid, expired, or revoked."
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
} catch {
|
|
311
433
|
if (_attempt < VALIDATE_MAX_RETRIES) {
|
|
312
434
|
await sleep(300 * (_attempt + 1));
|
|
313
435
|
return validateApiKey(apiKey, _attempt + 1);
|
|
@@ -315,7 +437,7 @@ async function validateApiKey(apiKey, _attempt = 0) {
|
|
|
315
437
|
return {
|
|
316
438
|
valid: false,
|
|
317
439
|
retryable: true,
|
|
318
|
-
error:
|
|
440
|
+
error: "Authentication service is temporarily unavailable."
|
|
319
441
|
};
|
|
320
442
|
}
|
|
321
443
|
}
|
|
@@ -336,10 +458,12 @@ __export(posthog_exports, {
|
|
|
336
458
|
initPostHog: () => initPostHog,
|
|
337
459
|
shutdownPostHog: () => shutdownPostHog
|
|
338
460
|
});
|
|
339
|
-
import {
|
|
461
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
340
462
|
import { PostHog } from "posthog-node";
|
|
341
463
|
function hashUserId(userId) {
|
|
342
|
-
|
|
464
|
+
const hmac = createHmac("sha256", POSTHOG_PSEUDONYMIZATION_KEY);
|
|
465
|
+
hmac.update(userId);
|
|
466
|
+
return hmac.digest("hex").substring(0, 32);
|
|
343
467
|
}
|
|
344
468
|
function initPostHog() {
|
|
345
469
|
if (isTelemetryDisabled()) return;
|
|
@@ -375,13 +499,13 @@ async function shutdownPostHog() {
|
|
|
375
499
|
client = null;
|
|
376
500
|
}
|
|
377
501
|
}
|
|
378
|
-
var
|
|
502
|
+
var POSTHOG_PSEUDONYMIZATION_KEY, client;
|
|
379
503
|
var init_posthog = __esm({
|
|
380
504
|
"src/lib/posthog.ts"() {
|
|
381
505
|
"use strict";
|
|
382
506
|
init_supabase();
|
|
383
507
|
init_request_context();
|
|
384
|
-
|
|
508
|
+
POSTHOG_PSEUDONYMIZATION_KEY = process.env.POSTHOG_PSEUDONYMIZATION_KEY || randomBytes(32).toString("hex");
|
|
385
509
|
client = null;
|
|
386
510
|
}
|
|
387
511
|
});
|
|
@@ -603,7 +727,7 @@ var init_supabase = __esm({
|
|
|
603
727
|
});
|
|
604
728
|
|
|
605
729
|
// src/cli/sn/parse.ts
|
|
606
|
-
import { createHash
|
|
730
|
+
import { createHash } from "node:crypto";
|
|
607
731
|
function parseSnArgs(argv) {
|
|
608
732
|
const parsed = { _: [] };
|
|
609
733
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -633,8 +757,8 @@ function isEnabledFlag(value) {
|
|
|
633
757
|
}
|
|
634
758
|
function emitSnResult(payload, asJson) {
|
|
635
759
|
if (asJson) {
|
|
636
|
-
const
|
|
637
|
-
process.stdout.write(JSON.stringify(
|
|
760
|
+
const envelope2 = { schema_version: "1", ...payload };
|
|
761
|
+
process.stdout.write(JSON.stringify(envelope2, null, 2) + "\n");
|
|
638
762
|
}
|
|
639
763
|
}
|
|
640
764
|
function isValidHttpsUrl(value) {
|
|
@@ -669,7 +793,7 @@ async function checkUrlReachability(url) {
|
|
|
669
793
|
} catch (error) {
|
|
670
794
|
return {
|
|
671
795
|
ok: false,
|
|
672
|
-
error:
|
|
796
|
+
error: "Network request failed."
|
|
673
797
|
};
|
|
674
798
|
} finally {
|
|
675
799
|
clearTimeout(timer);
|
|
@@ -699,7 +823,7 @@ function buildPublishIdempotencyKey(input) {
|
|
|
699
823
|
title: input.title ?? "",
|
|
700
824
|
scheduledAt: input.scheduledAt ?? ""
|
|
701
825
|
});
|
|
702
|
-
return `sn_${
|
|
826
|
+
return `sn_${createHash("sha256").update(material).digest("hex").slice(0, 24)}`;
|
|
703
827
|
}
|
|
704
828
|
function normalizePlatforms(platformsRaw) {
|
|
705
829
|
const caseMap = {
|
|
@@ -802,6 +926,52 @@ var edge_function_exports = {};
|
|
|
802
926
|
__export(edge_function_exports, {
|
|
803
927
|
callEdgeFunction: () => callEdgeFunction
|
|
804
928
|
});
|
|
929
|
+
function safeGatewayError(responseText, status) {
|
|
930
|
+
try {
|
|
931
|
+
const parsed = JSON.parse(responseText);
|
|
932
|
+
const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
|
|
933
|
+
const candidates = [
|
|
934
|
+
nested?.error_type,
|
|
935
|
+
nested?.code,
|
|
936
|
+
parsed.error_type,
|
|
937
|
+
parsed.error_code,
|
|
938
|
+
parsed.code,
|
|
939
|
+
typeof parsed.error === "string" ? parsed.error : null
|
|
940
|
+
];
|
|
941
|
+
for (const value of candidates) {
|
|
942
|
+
if (typeof value !== "string") continue;
|
|
943
|
+
const normalized = value.trim().toLowerCase();
|
|
944
|
+
if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
|
|
945
|
+
const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
|
|
946
|
+
if (embedded) return embedded;
|
|
947
|
+
}
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
return `Backend request failed (HTTP ${status}).`;
|
|
951
|
+
}
|
|
952
|
+
function safeFailureData(responseText) {
|
|
953
|
+
try {
|
|
954
|
+
const parsed = JSON.parse(responseText);
|
|
955
|
+
const metric = (name) => {
|
|
956
|
+
const value = parsed[name];
|
|
957
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
958
|
+
};
|
|
959
|
+
const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
|
|
960
|
+
const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
|
|
961
|
+
const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
|
|
962
|
+
const safe = {
|
|
963
|
+
...jobStatus ? { status: jobStatus } : {},
|
|
964
|
+
...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
|
|
965
|
+
...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
|
|
966
|
+
...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
|
|
967
|
+
...billingStatus ? { billing_status: billingStatus } : {},
|
|
968
|
+
...failureReason ? { failure_reason: failureReason } : {}
|
|
969
|
+
};
|
|
970
|
+
return Object.keys(safe).length > 0 ? safe : null;
|
|
971
|
+
} catch {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
805
975
|
function getApiKeyOrNull() {
|
|
806
976
|
const envKey = process.env.SOCIALNEURON_API_KEY;
|
|
807
977
|
if (envKey && envKey.trim().length) return envKey.trim();
|
|
@@ -872,33 +1042,28 @@ async function callEdgeFunction(functionName, body, options) {
|
|
|
872
1042
|
clearTimeout(timer);
|
|
873
1043
|
const responseText = await response.text();
|
|
874
1044
|
if (!response.ok) {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const errorJson = JSON.parse(responseText);
|
|
878
|
-
errorMessage = errorJson.error || errorJson.message || responseText;
|
|
879
|
-
} catch {
|
|
880
|
-
errorMessage = responseText || `HTTP ${response.status}`;
|
|
881
|
-
}
|
|
1045
|
+
const errorCode = safeGatewayError(responseText, response.status);
|
|
1046
|
+
const failureData = safeFailureData(responseText);
|
|
882
1047
|
if (response.status === 401) {
|
|
883
1048
|
return {
|
|
884
|
-
data:
|
|
1049
|
+
data: failureData,
|
|
885
1050
|
error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
|
|
886
1051
|
};
|
|
887
1052
|
}
|
|
888
1053
|
if (response.status === 403) {
|
|
889
1054
|
return {
|
|
890
|
-
data:
|
|
891
|
-
error: `Forbidden (HTTP 403): ${
|
|
1055
|
+
data: failureData,
|
|
1056
|
+
error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
|
|
892
1057
|
};
|
|
893
1058
|
}
|
|
894
1059
|
if (response.status === 429) {
|
|
895
1060
|
const retryAfter = response.headers.get("retry-after") || "60";
|
|
896
1061
|
return {
|
|
897
|
-
data:
|
|
1062
|
+
data: failureData,
|
|
898
1063
|
error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
|
|
899
1064
|
};
|
|
900
1065
|
}
|
|
901
|
-
return { data:
|
|
1066
|
+
return { data: failureData, error: errorCode };
|
|
902
1067
|
}
|
|
903
1068
|
try {
|
|
904
1069
|
const data = JSON.parse(responseText);
|
|
@@ -914,15 +1079,48 @@ async function callEdgeFunction(functionName, body, options) {
|
|
|
914
1079
|
error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
|
|
915
1080
|
};
|
|
916
1081
|
}
|
|
917
|
-
|
|
918
|
-
return { data: null, error: message };
|
|
1082
|
+
return { data: null, error: "Network request failed. Please retry." };
|
|
919
1083
|
}
|
|
920
1084
|
}
|
|
1085
|
+
var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
|
|
921
1086
|
var init_edge_function = __esm({
|
|
922
1087
|
"src/lib/edge-function.ts"() {
|
|
923
1088
|
"use strict";
|
|
924
1089
|
init_supabase();
|
|
925
1090
|
init_request_context();
|
|
1091
|
+
SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1092
|
+
"daily_limit_reached",
|
|
1093
|
+
"insufficient_credits",
|
|
1094
|
+
"project_scope_mismatch",
|
|
1095
|
+
"schedule_conflict",
|
|
1096
|
+
"post_not_found",
|
|
1097
|
+
"post_not_reschedulable",
|
|
1098
|
+
"post_in_progress",
|
|
1099
|
+
"not_cancellable",
|
|
1100
|
+
"publishing_in_progress",
|
|
1101
|
+
"plan_upgrade_required",
|
|
1102
|
+
"rate_limited",
|
|
1103
|
+
"validation_error",
|
|
1104
|
+
"permission_denied",
|
|
1105
|
+
"not_found"
|
|
1106
|
+
]);
|
|
1107
|
+
SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
|
|
1108
|
+
"reserved",
|
|
1109
|
+
"charged",
|
|
1110
|
+
"refunded",
|
|
1111
|
+
"failed_no_charge",
|
|
1112
|
+
"refund_pending",
|
|
1113
|
+
"not_charged"
|
|
1114
|
+
]);
|
|
1115
|
+
SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
1116
|
+
"queued",
|
|
1117
|
+
"pending",
|
|
1118
|
+
"processing",
|
|
1119
|
+
"completed",
|
|
1120
|
+
"failed",
|
|
1121
|
+
"cancelled",
|
|
1122
|
+
"canceled"
|
|
1123
|
+
]);
|
|
926
1124
|
}
|
|
927
1125
|
});
|
|
928
1126
|
|
|
@@ -938,6 +1136,7 @@ function evaluateQuality(input) {
|
|
|
938
1136
|
const firstLine = caption.split("\n")[0]?.trim() ?? "";
|
|
939
1137
|
const hashtags = countHashtags(caption);
|
|
940
1138
|
const threshold = Math.min(35, Math.max(0, input.threshold ?? 26));
|
|
1139
|
+
const isShortFormX = platforms.length > 0 && platforms.every((p) => p === "twitter") && caption.length <= 280;
|
|
941
1140
|
const blockedTerms = [
|
|
942
1141
|
...(input.brandAvoidPatterns ?? []).map((t) => t.trim()).filter(Boolean),
|
|
943
1142
|
...(input.customBannedTerms ?? []).map((t) => t.trim()).filter(Boolean)
|
|
@@ -947,11 +1146,12 @@ function evaluateQuality(input) {
|
|
|
947
1146
|
if (firstLine.length >= 20 && firstLine.length <= 120) hookScore += 1;
|
|
948
1147
|
if (/[!?]/.test(firstLine) || /\b\d+(\.\d+)?\b/.test(firstLine)) hookScore += 1;
|
|
949
1148
|
if (/\b(how|why|stop|avoid|build|launch|scale|grow|mistake)\b/i.test(firstLine)) hookScore += 1;
|
|
1149
|
+
if (isShortFormX) hookScore = Math.max(hookScore, 3);
|
|
950
1150
|
categories.push({
|
|
951
1151
|
name: "Hook Strength",
|
|
952
1152
|
score: Math.min(5, hookScore),
|
|
953
1153
|
maxScore: 5,
|
|
954
|
-
detail: "First line should create curiosity/value within 120 chars."
|
|
1154
|
+
detail: isShortFormX ? "Short-form X: keyword-hook heuristics neutralized; substance judge arbitrates." : "First line should create curiosity/value within 120 chars."
|
|
955
1155
|
});
|
|
956
1156
|
let clarityScore = 2;
|
|
957
1157
|
if (caption.length >= 80 && caption.length <= 1200) clarityScore += 2;
|
|
@@ -977,7 +1177,8 @@ function evaluateQuality(input) {
|
|
|
977
1177
|
detail: "Length, title, and hashtag usage should match target platforms."
|
|
978
1178
|
});
|
|
979
1179
|
let brandScore = 3;
|
|
980
|
-
const
|
|
1180
|
+
const rawBrandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
|
|
1181
|
+
const brandKeyword = rawBrandKeyword ? rawBrandKeyword.slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
|
|
981
1182
|
if (brandKeyword && new RegExp("\\b" + brandKeyword + "\\b", "i").test(title + " " + caption))
|
|
982
1183
|
brandScore += 1;
|
|
983
1184
|
if (!/\b(you|your|customer|audience)\b/i.test(caption)) brandScore -= 1;
|
|
@@ -988,11 +1189,12 @@ function evaluateQuality(input) {
|
|
|
988
1189
|
brandScore -= Math.min(2, matched.length);
|
|
989
1190
|
}
|
|
990
1191
|
}
|
|
1192
|
+
if (isShortFormX) brandScore = Math.max(brandScore, 3);
|
|
991
1193
|
categories.push({
|
|
992
1194
|
name: "Brand Alignment",
|
|
993
1195
|
score: Math.max(0, Math.min(5, brandScore)),
|
|
994
1196
|
maxScore: 5,
|
|
995
|
-
detail: "Voice should match brand context and audience focus."
|
|
1197
|
+
detail: isShortFormX ? "Short-form X: second-person-address heuristic neutralized; blocked terms still enforced." : "Voice should match brand context and audience focus."
|
|
996
1198
|
});
|
|
997
1199
|
let noveltyScore = 2;
|
|
998
1200
|
if (/\b(case study|framework|workflow|playbook|breakdown|behind the scenes)\b/i.test(caption))
|
|
@@ -1003,21 +1205,23 @@ function evaluateQuality(input) {
|
|
|
1003
1205
|
const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
|
|
1004
1206
|
if (matched.length > 0) noveltyScore -= Math.min(2, matched.length);
|
|
1005
1207
|
}
|
|
1208
|
+
if (isShortFormX) noveltyScore = Math.max(noveltyScore, 3);
|
|
1006
1209
|
categories.push({
|
|
1007
1210
|
name: "Novelty",
|
|
1008
1211
|
score: Math.max(0, Math.min(5, noveltyScore)),
|
|
1009
1212
|
maxScore: 5,
|
|
1010
|
-
detail: "Avoid generic phrasing; include distinct angle."
|
|
1213
|
+
detail: isShortFormX ? "Short-form X: keyword-novelty neutralized; substance judge arbitrates." : "Avoid generic phrasing; include distinct angle."
|
|
1011
1214
|
});
|
|
1012
1215
|
let ctaScore = 2;
|
|
1013
1216
|
if (/\b(comment|reply|share|save|follow|subscribe|click|try|book|download)\b/i.test(caption))
|
|
1014
1217
|
ctaScore += 2;
|
|
1015
1218
|
if (/\?$/.test(firstLine)) ctaScore += 1;
|
|
1219
|
+
if (isShortFormX) ctaScore = Math.max(ctaScore, 3);
|
|
1016
1220
|
categories.push({
|
|
1017
1221
|
name: "CTA Strength",
|
|
1018
1222
|
score: Math.min(5, ctaScore),
|
|
1019
1223
|
maxScore: 5,
|
|
1020
|
-
detail: "Should include a clear next action."
|
|
1224
|
+
detail: isShortFormX ? "Short-form X: explicit CTA optional; native posts may close without one." : "Should include a clear next action."
|
|
1021
1225
|
});
|
|
1022
1226
|
let safetyScore = 5;
|
|
1023
1227
|
if (/\b(guarantee|guaranteed|no risk|risk-free|always works|100%)\b/i.test(caption))
|
|
@@ -2022,9 +2226,6 @@ function searchTools(query) {
|
|
|
2022
2226
|
(t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q)
|
|
2023
2227
|
);
|
|
2024
2228
|
}
|
|
2025
|
-
function getModules() {
|
|
2026
|
-
return [...new Set(TOOL_CATALOG.map((t) => t.module))];
|
|
2027
|
-
}
|
|
2028
2229
|
var TOOL_CATALOG;
|
|
2029
2230
|
var init_tool_catalog = __esm({
|
|
2030
2231
|
"src/lib/tool-catalog.ts"() {
|
|
@@ -2099,6 +2300,26 @@ var init_tool_catalog = __esm({
|
|
|
2099
2300
|
module: "carousel",
|
|
2100
2301
|
scope: "mcp:write"
|
|
2101
2302
|
},
|
|
2303
|
+
{
|
|
2304
|
+
name: "cancel_async_job",
|
|
2305
|
+
description: "Cancel an owned pending async job and refund an eligible debit",
|
|
2306
|
+
module: "lifecycle",
|
|
2307
|
+
scope: "mcp:write",
|
|
2308
|
+
task_intent: "Stop a queued generation or render before worker execution",
|
|
2309
|
+
use_when: "The user explicitly wants to cancel a pending job and has confirmed the action.",
|
|
2310
|
+
avoid_when: "The job is already processing or terminal; check_status instead.",
|
|
2311
|
+
next_tools: ["check_status", "get_credit_balance"]
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
name: "delete_carousel",
|
|
2315
|
+
description: "Delete an owned carousel content record from one project",
|
|
2316
|
+
module: "lifecycle",
|
|
2317
|
+
scope: "mcp:write",
|
|
2318
|
+
task_intent: "Remove an unwanted carousel record from Social Neuron",
|
|
2319
|
+
use_when: "The user explicitly confirms deletion and understands stored media follows normal retention.",
|
|
2320
|
+
avoid_when: "The user expects an already-published social post or retained media object to be removed.",
|
|
2321
|
+
next_tools: ["list_recent_posts"]
|
|
2322
|
+
},
|
|
2102
2323
|
// media
|
|
2103
2324
|
{
|
|
2104
2325
|
name: "upload_media",
|
|
@@ -2119,6 +2340,26 @@ var init_tool_catalog = __esm({
|
|
|
2119
2340
|
module: "distribution",
|
|
2120
2341
|
scope: "mcp:distribute"
|
|
2121
2342
|
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "reschedule_post",
|
|
2345
|
+
description: "Atomically move an unclaimed scheduled post to a new time within its brand project",
|
|
2346
|
+
module: "distribution",
|
|
2347
|
+
scope: "mcp:distribute",
|
|
2348
|
+
task_intent: "Move an already scheduled post without creating a duplicate publication",
|
|
2349
|
+
use_when: "A pending or scheduled post needs a different future publication time.",
|
|
2350
|
+
avoid_when: "The post is already publishing or published; create a new post instead.",
|
|
2351
|
+
next_tools: ["list_recent_posts", "open_content_calendar"]
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
name: "cancel_scheduled_post",
|
|
2355
|
+
description: "Cancel an owned scheduled post before publishing begins",
|
|
2356
|
+
module: "lifecycle",
|
|
2357
|
+
scope: "mcp:distribute",
|
|
2358
|
+
task_intent: "Unschedule a post without publishing it",
|
|
2359
|
+
use_when: "The user explicitly confirms cancellation of a draft, pending, or scheduled post.",
|
|
2360
|
+
avoid_when: "The post is already publishing or published.",
|
|
2361
|
+
next_tools: ["list_recent_posts", "open_content_calendar"]
|
|
2362
|
+
},
|
|
2122
2363
|
{
|
|
2123
2364
|
name: "list_recent_posts",
|
|
2124
2365
|
description: "List recently published or scheduled posts",
|
|
@@ -2309,6 +2550,16 @@ var init_tool_catalog = __esm({
|
|
|
2309
2550
|
module: "planning",
|
|
2310
2551
|
scope: "mcp:write"
|
|
2311
2552
|
},
|
|
2553
|
+
{
|
|
2554
|
+
name: "delete_content_plan",
|
|
2555
|
+
description: "Permanently delete an owned content plan from one project",
|
|
2556
|
+
module: "lifecycle",
|
|
2557
|
+
scope: "mcp:write",
|
|
2558
|
+
task_intent: "Remove an obsolete saved content plan",
|
|
2559
|
+
use_when: "The user explicitly confirms permanent deletion of a specific plan.",
|
|
2560
|
+
avoid_when: "The plan has scheduled posts the user also expects to cancel; cancel those posts separately.",
|
|
2561
|
+
next_tools: ["plan_content_week", "list_recent_posts"]
|
|
2562
|
+
},
|
|
2312
2563
|
{
|
|
2313
2564
|
name: "submit_content_plan_for_approval",
|
|
2314
2565
|
description: "Submit a content plan for team approval",
|
|
@@ -2403,6 +2654,16 @@ var init_tool_catalog = __esm({
|
|
|
2403
2654
|
module: "autopilot",
|
|
2404
2655
|
scope: "mcp:autopilot"
|
|
2405
2656
|
},
|
|
2657
|
+
{
|
|
2658
|
+
name: "delete_autopilot_config",
|
|
2659
|
+
description: "Permanently delete an owned autopilot configuration from one project",
|
|
2660
|
+
module: "lifecycle",
|
|
2661
|
+
scope: "mcp:autopilot",
|
|
2662
|
+
task_intent: "Remove an autopilot configuration while retaining historical run records",
|
|
2663
|
+
use_when: "The user explicitly confirms deletion of the configuration.",
|
|
2664
|
+
avoid_when: "The user only wants to pause or edit automation, or expects historical posts to be deleted.",
|
|
2665
|
+
next_tools: ["list_autopilot_configs"]
|
|
2666
|
+
},
|
|
2406
2667
|
// extraction
|
|
2407
2668
|
{
|
|
2408
2669
|
name: "extract_url_content",
|
|
@@ -2519,7 +2780,13 @@ var init_tool_catalog = __esm({
|
|
|
2519
2780
|
// apps (MCP Apps — interactive UI inside the host)
|
|
2520
2781
|
{
|
|
2521
2782
|
name: "open_content_calendar",
|
|
2522
|
-
description: "Open
|
|
2783
|
+
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.",
|
|
2784
|
+
module: "apps",
|
|
2785
|
+
scope: "mcp:read"
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
name: "open_analytics_pulse",
|
|
2789
|
+
description: "Open a project-scoped interactive performance dashboard with views, engagement, platform mix, and top posts.",
|
|
2523
2790
|
module: "apps",
|
|
2524
2791
|
scope: "mcp:read"
|
|
2525
2792
|
},
|
|
@@ -2634,6 +2901,12 @@ var init_tool_catalog = __esm({
|
|
|
2634
2901
|
module: "skills",
|
|
2635
2902
|
scope: "mcp:read"
|
|
2636
2903
|
},
|
|
2904
|
+
{
|
|
2905
|
+
name: "get_skill",
|
|
2906
|
+
description: "Fetch the full body and current compiled guidance for one Social Neuron skill by slug.",
|
|
2907
|
+
module: "skills",
|
|
2908
|
+
scope: "mcp:read"
|
|
2909
|
+
},
|
|
2637
2910
|
{
|
|
2638
2911
|
name: "run_skill",
|
|
2639
2912
|
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.",
|
|
@@ -2665,17 +2938,21 @@ __export(discovery_exports, {
|
|
|
2665
2938
|
handleInfo: () => handleInfo,
|
|
2666
2939
|
handleTools: () => handleTools
|
|
2667
2940
|
});
|
|
2941
|
+
function publicStdioTools() {
|
|
2942
|
+
return TOOL_CATALOG.filter(
|
|
2943
|
+
(t) => !t.internal && !t.hiddenFromPublicCount && t.module !== "apps"
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2668
2946
|
async function handleTools(args, asJson) {
|
|
2669
|
-
let tools =
|
|
2947
|
+
let tools = publicStdioTools();
|
|
2670
2948
|
const scope = args.scope;
|
|
2671
2949
|
if (typeof scope === "string") {
|
|
2672
|
-
tools =
|
|
2950
|
+
tools = tools.filter((tool) => tool.scope === scope);
|
|
2673
2951
|
}
|
|
2674
2952
|
const module = args.module;
|
|
2675
2953
|
if (typeof module === "string") {
|
|
2676
|
-
tools =
|
|
2954
|
+
tools = tools.filter((tool) => tool.module === module);
|
|
2677
2955
|
}
|
|
2678
|
-
tools = tools.filter((t) => !t.internal);
|
|
2679
2956
|
if (asJson) {
|
|
2680
2957
|
emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
|
|
2681
2958
|
return;
|
|
@@ -2704,27 +2981,31 @@ Module: ${moduleName} (${moduleTools.length} tools)`);
|
|
|
2704
2981
|
process.exit(0);
|
|
2705
2982
|
}
|
|
2706
2983
|
async function handleInfo(args, asJson) {
|
|
2984
|
+
const stdioTools = publicStdioTools();
|
|
2707
2985
|
const info = {
|
|
2708
2986
|
version: MCP_VERSION,
|
|
2709
|
-
toolCount:
|
|
2710
|
-
modules:
|
|
2987
|
+
toolCount: stdioTools.length,
|
|
2988
|
+
modules: Array.from(new Set(stdioTools.map((tool) => tool.module))).sort(),
|
|
2989
|
+
auth: null
|
|
2711
2990
|
};
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2991
|
+
if (!isEnabledFlag(args.offline)) {
|
|
2992
|
+
try {
|
|
2993
|
+
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
2994
|
+
const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
|
|
2995
|
+
const apiKey = await loadApiKey2();
|
|
2996
|
+
if (apiKey) {
|
|
2997
|
+
const result = await validateApiKey2(apiKey);
|
|
2998
|
+
if (result.valid) {
|
|
2999
|
+
info.auth = {
|
|
3000
|
+
email: result.email || null,
|
|
3001
|
+
scopes: result.scopes || [],
|
|
3002
|
+
expiresAt: result.expiresAt || null
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
2724
3005
|
}
|
|
3006
|
+
} catch {
|
|
3007
|
+
info.auth = null;
|
|
2725
3008
|
}
|
|
2726
|
-
} catch {
|
|
2727
|
-
info.auth = null;
|
|
2728
3009
|
}
|
|
2729
3010
|
if (info.auth) {
|
|
2730
3011
|
try {
|
|
@@ -2838,6 +3119,9 @@ var init_scopes = __esm({
|
|
|
2838
3119
|
generate_carousel: "mcp:write",
|
|
2839
3120
|
create_carousel: "mcp:write",
|
|
2840
3121
|
upload_media: "mcp:write",
|
|
3122
|
+
cancel_async_job: "mcp:write",
|
|
3123
|
+
delete_carousel: "mcp:write",
|
|
3124
|
+
delete_content_plan: "mcp:write",
|
|
2841
3125
|
// mcp:read (media)
|
|
2842
3126
|
get_media_url: "mcp:read",
|
|
2843
3127
|
// F4 Hyperframes — HTML composition runtime
|
|
@@ -2845,6 +3129,8 @@ var init_scopes = __esm({
|
|
|
2845
3129
|
render_hyperframes: "mcp:write",
|
|
2846
3130
|
// mcp:distribute
|
|
2847
3131
|
schedule_post: "mcp:distribute",
|
|
3132
|
+
reschedule_post: "mcp:distribute",
|
|
3133
|
+
cancel_scheduled_post: "mcp:distribute",
|
|
2848
3134
|
start_platform_connection: "mcp:distribute",
|
|
2849
3135
|
// mcp:analytics
|
|
2850
3136
|
refresh_platform_analytics: "mcp:analytics",
|
|
@@ -2859,6 +3145,7 @@ var init_scopes = __esm({
|
|
|
2859
3145
|
list_autopilot_configs: "mcp:autopilot",
|
|
2860
3146
|
update_autopilot_config: "mcp:autopilot",
|
|
2861
3147
|
get_autopilot_status: "mcp:autopilot",
|
|
3148
|
+
delete_autopilot_config: "mcp:autopilot",
|
|
2862
3149
|
// Recipes
|
|
2863
3150
|
list_recipes: "mcp:read",
|
|
2864
3151
|
get_recipe_details: "mcp:read",
|
|
@@ -2902,6 +3189,7 @@ var init_scopes = __esm({
|
|
|
2902
3189
|
detect_anomalies: "mcp:analytics",
|
|
2903
3190
|
// mcp:read (Apps — entry tool for the Content Calendar MCP App; reads recent posts)
|
|
2904
3191
|
open_content_calendar: "mcp:read",
|
|
3192
|
+
open_analytics_pulse: "mcp:read",
|
|
2905
3193
|
// mcp:write (Agentic harness — learning loop write-back)
|
|
2906
3194
|
write_agent_reflection: "mcp:write",
|
|
2907
3195
|
record_outcome: "mcp:write",
|
|
@@ -2917,6 +3205,7 @@ var init_scopes = __esm({
|
|
|
2917
3205
|
get_active_campaigns: "mcp:read",
|
|
2918
3206
|
// mcp:read / mcp:write (Skills)
|
|
2919
3207
|
list_skills: "mcp:read",
|
|
3208
|
+
get_skill: "mcp:read",
|
|
2920
3209
|
run_skill: "mcp:write",
|
|
2921
3210
|
// mcp:read (Loop observability — growth-loop KPIs + content learning state)
|
|
2922
3211
|
get_loop_pulse: "mcp:read",
|
|
@@ -3043,6 +3332,11 @@ var init_tool_annotations = __esm({
|
|
|
3043
3332
|
OVERRIDES = {
|
|
3044
3333
|
// Destructive or overwrite tools
|
|
3045
3334
|
delete_comment: { destructiveHint: true },
|
|
3335
|
+
cancel_async_job: { destructiveHint: true, idempotentHint: true },
|
|
3336
|
+
cancel_scheduled_post: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
3337
|
+
delete_carousel: { destructiveHint: true, idempotentHint: true },
|
|
3338
|
+
delete_content_plan: { destructiveHint: true, idempotentHint: true },
|
|
3339
|
+
delete_autopilot_config: { destructiveHint: true, idempotentHint: true },
|
|
3046
3340
|
moderate_comment: { destructiveHint: true },
|
|
3047
3341
|
save_brand_profile: { destructiveHint: true, idempotentHint: true },
|
|
3048
3342
|
update_platform_voice: { destructiveHint: true, idempotentHint: true },
|
|
@@ -3207,6 +3501,7 @@ var init_tool_profile = __esm({
|
|
|
3207
3501
|
"list_hyperframes_blocks",
|
|
3208
3502
|
"render_hyperframes",
|
|
3209
3503
|
"list_skills",
|
|
3504
|
+
"get_skill",
|
|
3210
3505
|
"run_skill"
|
|
3211
3506
|
]);
|
|
3212
3507
|
}
|
|
@@ -3240,7 +3535,8 @@ var init_constants = __esm({
|
|
|
3240
3535
|
ssn: "\\d{3}-\\d{2}-\\d{4}",
|
|
3241
3536
|
ip: "(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])"
|
|
3242
3537
|
},
|
|
3243
|
-
MAX_LENGTH: 1e4
|
|
3538
|
+
MAX_LENGTH: 1e4,
|
|
3539
|
+
MAX_OUTPUT_LENGTH: 1e6
|
|
3244
3540
|
};
|
|
3245
3541
|
}
|
|
3246
3542
|
});
|
|
@@ -3337,7 +3633,8 @@ var init_pii = __esm({
|
|
|
3337
3633
|
function scan(text, options) {
|
|
3338
3634
|
const flagged = /* @__PURE__ */ new Set();
|
|
3339
3635
|
let risk = 0;
|
|
3340
|
-
|
|
3636
|
+
const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
|
|
3637
|
+
if (text.length > maxLength) {
|
|
3341
3638
|
return {
|
|
3342
3639
|
passed: false,
|
|
3343
3640
|
risk_score: 1,
|
|
@@ -3412,6 +3709,7 @@ var CATEGORY_CONFIGS, RateLimiter, limiters;
|
|
|
3412
3709
|
var init_rate_limit = __esm({
|
|
3413
3710
|
"src/lib/rate-limit.ts"() {
|
|
3414
3711
|
"use strict";
|
|
3712
|
+
init_scopes();
|
|
3415
3713
|
CATEGORY_CONFIGS = {
|
|
3416
3714
|
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
3417
3715
|
// 30 req/min — publish/schedule/comment
|
|
@@ -3502,8 +3800,8 @@ function registerIdeationTools(server) {
|
|
|
3502
3800
|
brand_voice: z.string().max(500).optional().describe(
|
|
3503
3801
|
'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.'
|
|
3504
3802
|
),
|
|
3505
|
-
model: z.enum(["gemini-2.
|
|
3506
|
-
"AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality."
|
|
3803
|
+
model: z.enum(["gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
|
|
3804
|
+
"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."
|
|
3507
3805
|
),
|
|
3508
3806
|
project_id: z.string().uuid().optional().describe(
|
|
3509
3807
|
"Project ID to auto-load brand profile and performance context for prompt enrichment."
|
|
@@ -3512,7 +3810,7 @@ function registerIdeationTools(server) {
|
|
|
3512
3810
|
async ({ prompt: prompt2, content_type, platform: platform3, brand_voice, model, project_id }) => {
|
|
3513
3811
|
try {
|
|
3514
3812
|
const userId = await getDefaultUserId();
|
|
3515
|
-
const rl = checkRateLimit("
|
|
3813
|
+
const rl = checkRateLimit("generation", `generate_content:${userId}`);
|
|
3516
3814
|
if (!rl.allowed) {
|
|
3517
3815
|
return {
|
|
3518
3816
|
content: [
|
|
@@ -3769,7 +4067,7 @@ Content Type: ${content_type}`;
|
|
|
3769
4067
|
async ({ content, source_platform, target_platform, brand_voice, project_id }) => {
|
|
3770
4068
|
try {
|
|
3771
4069
|
const userId = await getDefaultUserId();
|
|
3772
|
-
const rl = checkRateLimit("
|
|
4070
|
+
const rl = checkRateLimit("generation", `adapt_content:${userId}`);
|
|
3773
4071
|
if (!rl.allowed) {
|
|
3774
4072
|
return {
|
|
3775
4073
|
content: [
|
|
@@ -3881,11 +4179,12 @@ function buildCheckStatusPayload(job, liveStatus) {
|
|
|
3881
4179
|
const progress = liveStatus?.progress ?? null;
|
|
3882
4180
|
const resultUrl = liveStatus?.resultUrl ?? job.result_url ?? null;
|
|
3883
4181
|
const r2Key = resultUrl && !resultUrl.startsWith("http") ? resultUrl : null;
|
|
3884
|
-
const
|
|
4182
|
+
const rawError = liveStatus?.error ?? job.error_message ?? null;
|
|
4183
|
+
const errorMessage = rawError && status === "failed" ? "Generation failed. Retry or choose another model." : rawError && (status === "cancelled" || status === "canceled") ? "Cancelled by user." : null;
|
|
3885
4184
|
const allUrls = job.result_metadata?.all_urls ?? null;
|
|
3886
4185
|
const modelRequested = job.result_metadata?.model_requested ?? null;
|
|
3887
4186
|
const modelDelivered = job.result_metadata?.model_delivered ?? null;
|
|
3888
|
-
const fallbackReason = job.result_metadata?.fallback_reason
|
|
4187
|
+
const fallbackReason = job.result_metadata?.fallback_reason ? "Requested model was unavailable; a fallback model was used." : null;
|
|
3889
4188
|
return {
|
|
3890
4189
|
job_id: job.id,
|
|
3891
4190
|
job_type: job.job_type,
|
|
@@ -3897,6 +4196,11 @@ function buildCheckStatusPayload(job, liveStatus) {
|
|
|
3897
4196
|
all_urls: allUrls,
|
|
3898
4197
|
error: errorMessage,
|
|
3899
4198
|
credits_cost: job.credits_cost,
|
|
4199
|
+
credits_reserved: job.credits_reserved ?? null,
|
|
4200
|
+
credits_charged: job.credits_charged ?? null,
|
|
4201
|
+
credits_refunded: job.credits_refunded ?? null,
|
|
4202
|
+
billing_status: job.billing_status ?? "unknown",
|
|
4203
|
+
failure_reason: job.failure_reason ?? null,
|
|
3900
4204
|
created_at: job.created_at,
|
|
3901
4205
|
completed_at: job.completed_at,
|
|
3902
4206
|
model_requested: modelRequested,
|
|
@@ -3998,6 +4302,13 @@ var init_budget = __esm({
|
|
|
3998
4302
|
|
|
3999
4303
|
// src/tools/content.ts
|
|
4000
4304
|
import { z as z2 } from "zod";
|
|
4305
|
+
function buildFallbackDisclosureLine(meta) {
|
|
4306
|
+
const requested = meta?.model_requested;
|
|
4307
|
+
const delivered = meta?.model_delivered;
|
|
4308
|
+
if (!requested || !delivered || requested === delivered) return null;
|
|
4309
|
+
const reason = meta?.fallback_reason ? " because the requested model was unavailable" : "";
|
|
4310
|
+
return `Note: requested "${requested}" but delivered "${delivered}"${reason} \u2014 cost never exceeds the requested model's price.`;
|
|
4311
|
+
}
|
|
4001
4312
|
function asEnvelope2(data) {
|
|
4002
4313
|
return {
|
|
4003
4314
|
_meta: {
|
|
@@ -4066,7 +4377,7 @@ function registerContentTools(server) {
|
|
|
4066
4377
|
isError: true
|
|
4067
4378
|
};
|
|
4068
4379
|
}
|
|
4069
|
-
const rateLimit = checkRateLimit("
|
|
4380
|
+
const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
|
|
4070
4381
|
if (!rateLimit.allowed) {
|
|
4071
4382
|
return {
|
|
4072
4383
|
content: [
|
|
@@ -4207,7 +4518,7 @@ function registerContentTools(server) {
|
|
|
4207
4518
|
isError: true
|
|
4208
4519
|
};
|
|
4209
4520
|
}
|
|
4210
|
-
const rateLimit = checkRateLimit("
|
|
4521
|
+
const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
|
|
4211
4522
|
if (!rateLimit.allowed) {
|
|
4212
4523
|
return {
|
|
4213
4524
|
content: [
|
|
@@ -4347,20 +4658,28 @@ function registerContentTools(server) {
|
|
|
4347
4658
|
}
|
|
4348
4659
|
);
|
|
4349
4660
|
if (liveStatus) {
|
|
4661
|
+
const livePayload = buildCheckStatusPayload(job, liveStatus);
|
|
4350
4662
|
const lines2 = [
|
|
4351
4663
|
`Job: ${job.id}`,
|
|
4352
4664
|
`Type: ${job.job_type}`,
|
|
4353
4665
|
`Model: ${job.model}`,
|
|
4354
|
-
`Status: ${
|
|
4355
|
-
`Progress: ${
|
|
4666
|
+
`Status: ${livePayload.status}`,
|
|
4667
|
+
`Progress: ${livePayload.progress}%`
|
|
4356
4668
|
];
|
|
4357
|
-
if (
|
|
4358
|
-
lines2.push(`Result URL: ${
|
|
4669
|
+
if (livePayload.result_url) {
|
|
4670
|
+
lines2.push(`Result URL: ${livePayload.result_url}`);
|
|
4359
4671
|
}
|
|
4360
|
-
if (
|
|
4361
|
-
lines2.push(`Error: ${
|
|
4672
|
+
if (livePayload.error) {
|
|
4673
|
+
lines2.push(`Error: ${livePayload.error}`);
|
|
4362
4674
|
}
|
|
4675
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
|
|
4676
|
+
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
4363
4677
|
lines2.push(`Credits: ${job.credits_cost}`);
|
|
4678
|
+
if (job.billing_status) {
|
|
4679
|
+
lines2.push(
|
|
4680
|
+
`Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
|
|
4681
|
+
);
|
|
4682
|
+
}
|
|
4364
4683
|
lines2.push(`Created: ${job.created_at}`);
|
|
4365
4684
|
if (format === "json") {
|
|
4366
4685
|
return {
|
|
@@ -4378,7 +4697,7 @@ function registerContentTools(server) {
|
|
|
4378
4697
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
4379
4698
|
asEnvelope2({
|
|
4380
4699
|
...liveStatus,
|
|
4381
|
-
...
|
|
4700
|
+
...livePayload
|
|
4382
4701
|
}),
|
|
4383
4702
|
null,
|
|
4384
4703
|
2
|
|
@@ -4421,7 +4740,14 @@ function registerContentTools(server) {
|
|
|
4421
4740
|
if (job.error_message) {
|
|
4422
4741
|
lines.push(`Error: ${job.error_message}`);
|
|
4423
4742
|
}
|
|
4743
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
|
|
4744
|
+
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
4424
4745
|
lines.push(`Credits: ${job.credits_cost}`);
|
|
4746
|
+
if (job.billing_status) {
|
|
4747
|
+
lines.push(
|
|
4748
|
+
`Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
|
|
4749
|
+
);
|
|
4750
|
+
}
|
|
4425
4751
|
lines.push(`Created: ${job.created_at}`);
|
|
4426
4752
|
if (job.completed_at) {
|
|
4427
4753
|
lines.push(`Completed: ${job.completed_at}`);
|
|
@@ -4651,7 +4977,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
4651
4977
|
};
|
|
4652
4978
|
}
|
|
4653
4979
|
const rateLimit = checkRateLimit(
|
|
4654
|
-
"
|
|
4980
|
+
"generation",
|
|
4655
4981
|
`generate_voiceover:${userId}`
|
|
4656
4982
|
);
|
|
4657
4983
|
if (!rateLimit.allowed) {
|
|
@@ -4818,7 +5144,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
4818
5144
|
}
|
|
4819
5145
|
const userId = await getDefaultUserId();
|
|
4820
5146
|
const rateLimit = checkRateLimit(
|
|
4821
|
-
"
|
|
5147
|
+
"generation",
|
|
4822
5148
|
`generate_carousel:${userId}`
|
|
4823
5149
|
);
|
|
4824
5150
|
if (!rateLimit.allowed) {
|
|
@@ -4972,9 +5298,6 @@ var init_content2 = __esm({
|
|
|
4972
5298
|
// src/lib/sanitize-error.ts
|
|
4973
5299
|
function sanitizeError(error) {
|
|
4974
5300
|
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
4975
|
-
if (process.env.NODE_ENV !== "production") {
|
|
4976
|
-
console.error("[Error]", msg);
|
|
4977
|
-
}
|
|
4978
5301
|
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
4979
5302
|
if (pattern.test(msg)) {
|
|
4980
5303
|
return userMessage;
|
|
@@ -5178,7 +5501,7 @@ var init_ssrf = __esm({
|
|
|
5178
5501
|
|
|
5179
5502
|
// src/tools/distribution.ts
|
|
5180
5503
|
import { z as z3 } from "zod";
|
|
5181
|
-
import { createHash as
|
|
5504
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5182
5505
|
function snakeToCamel(obj) {
|
|
5183
5506
|
const result = {};
|
|
5184
5507
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -5204,6 +5527,87 @@ function asEnvelope3(data) {
|
|
|
5204
5527
|
data
|
|
5205
5528
|
};
|
|
5206
5529
|
}
|
|
5530
|
+
function publicConnectedAccount(value) {
|
|
5531
|
+
if (!value || typeof value !== "object") return null;
|
|
5532
|
+
const row = value;
|
|
5533
|
+
if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
|
|
5534
|
+
return null;
|
|
5535
|
+
}
|
|
5536
|
+
return {
|
|
5537
|
+
id: row.id,
|
|
5538
|
+
platform: row.platform,
|
|
5539
|
+
status: row.status,
|
|
5540
|
+
...typeof row.effective_status === "string" ? { effective_status: row.effective_status } : {},
|
|
5541
|
+
username: typeof row.username === "string" ? row.username : null,
|
|
5542
|
+
created_at: row.created_at,
|
|
5543
|
+
...typeof row.updated_at === "string" || row.updated_at === null ? { updated_at: row.updated_at } : {},
|
|
5544
|
+
...typeof row.expires_at === "string" || row.expires_at === null ? { expires_at: row.expires_at } : {},
|
|
5545
|
+
...typeof row.has_refresh_token === "boolean" ? { has_refresh_token: row.has_refresh_token } : {},
|
|
5546
|
+
...typeof row.project_id === "string" || row.project_id === null ? { project_id: row.project_id } : {}
|
|
5547
|
+
};
|
|
5548
|
+
}
|
|
5549
|
+
function publicPostRecord(value) {
|
|
5550
|
+
if (!value || typeof value !== "object") return null;
|
|
5551
|
+
const row = value;
|
|
5552
|
+
if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
|
|
5553
|
+
return null;
|
|
5554
|
+
}
|
|
5555
|
+
const nullableString = (field) => typeof row[field] === "string" ? row[field] : null;
|
|
5556
|
+
return {
|
|
5557
|
+
id: row.id,
|
|
5558
|
+
platform: row.platform,
|
|
5559
|
+
status: row.status,
|
|
5560
|
+
title: nullableString("title"),
|
|
5561
|
+
external_post_id: nullableString("external_post_id"),
|
|
5562
|
+
published_at: nullableString("published_at"),
|
|
5563
|
+
scheduled_at: nullableString("scheduled_at"),
|
|
5564
|
+
created_at: row.created_at
|
|
5565
|
+
};
|
|
5566
|
+
}
|
|
5567
|
+
function mediaTypeFromPath(value) {
|
|
5568
|
+
try {
|
|
5569
|
+
const pathname = value.startsWith("http") ? new URL(value).pathname : value;
|
|
5570
|
+
const extension = pathname.split(".").pop()?.toLowerCase();
|
|
5571
|
+
if (!extension) return null;
|
|
5572
|
+
if (VIDEO_FILE_EXTENSIONS.has(extension)) return "VIDEO";
|
|
5573
|
+
if (IMAGE_FILE_EXTENSIONS.has(extension)) return "IMAGE";
|
|
5574
|
+
} catch {
|
|
5575
|
+
return null;
|
|
5576
|
+
}
|
|
5577
|
+
return null;
|
|
5578
|
+
}
|
|
5579
|
+
function inferScheduleMediaType(explicit, singleCandidates, collectionCandidates) {
|
|
5580
|
+
if (explicit) return explicit;
|
|
5581
|
+
const populatedCollection = collectionCandidates.find(
|
|
5582
|
+
(values) => Array.isArray(values) && values.length > 0
|
|
5583
|
+
);
|
|
5584
|
+
if (populatedCollection) {
|
|
5585
|
+
if (populatedCollection.length > 1) return "CAROUSEL_ALBUM";
|
|
5586
|
+
for (const values of collectionCandidates) {
|
|
5587
|
+
if (!values || values.length !== 1) continue;
|
|
5588
|
+
const inferred = mediaTypeFromPath(values[0]);
|
|
5589
|
+
if (inferred) return inferred;
|
|
5590
|
+
}
|
|
5591
|
+
return null;
|
|
5592
|
+
}
|
|
5593
|
+
for (const value of singleCandidates) {
|
|
5594
|
+
if (typeof value !== "string" || value.length === 0) continue;
|
|
5595
|
+
const inferred = mediaTypeFromPath(value);
|
|
5596
|
+
if (inferred) return inferred;
|
|
5597
|
+
}
|
|
5598
|
+
return null;
|
|
5599
|
+
}
|
|
5600
|
+
async function validatePublishMediaUrl(url) {
|
|
5601
|
+
try {
|
|
5602
|
+
if (new URL(url).protocol !== "https:") {
|
|
5603
|
+
return "Media URLs must use HTTPS.";
|
|
5604
|
+
}
|
|
5605
|
+
} catch {
|
|
5606
|
+
return "Media URL is invalid.";
|
|
5607
|
+
}
|
|
5608
|
+
const check = await validateUrlForSSRF(url);
|
|
5609
|
+
return check.isValid ? null : check.error || "Media URL failed safety validation.";
|
|
5610
|
+
}
|
|
5207
5611
|
function accountEffectiveStatus(account) {
|
|
5208
5612
|
return account.effective_status || account.status;
|
|
5209
5613
|
}
|
|
@@ -5222,22 +5626,11 @@ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
|
5222
5626
|
if (!accountIds) return void 0;
|
|
5223
5627
|
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
5224
5628
|
}
|
|
5225
|
-
function isAlreadyR2Signed(url) {
|
|
5226
|
-
try {
|
|
5227
|
-
const u = new URL(url);
|
|
5228
|
-
return u.searchParams.has("X-Amz-Signature");
|
|
5229
|
-
} catch {
|
|
5230
|
-
return false;
|
|
5231
|
-
}
|
|
5232
|
-
}
|
|
5233
5629
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
5234
5630
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
5235
5631
|
if (!ssrf.isValid) {
|
|
5236
5632
|
return { error: ssrf.error ?? "URL rejected by SSRF check" };
|
|
5237
5633
|
}
|
|
5238
|
-
if (isAlreadyR2Signed(mediaUrl)) {
|
|
5239
|
-
return { signedUrl: mediaUrl, r2Key: "" };
|
|
5240
|
-
}
|
|
5241
5634
|
const { data, error } = await callEdgeFunction(
|
|
5242
5635
|
"upload-to-r2",
|
|
5243
5636
|
{ url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
|
|
@@ -5256,19 +5649,19 @@ function registerDistributionTools(server) {
|
|
|
5256
5649
|
media_url: z3.string().optional().describe(
|
|
5257
5650
|
"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."
|
|
5258
5651
|
),
|
|
5259
|
-
media_urls: z3.array(z3.string()).optional().describe(
|
|
5652
|
+
media_urls: z3.array(z3.string().url()).min(2).max(10).optional().describe(
|
|
5260
5653
|
"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."
|
|
5261
5654
|
),
|
|
5262
5655
|
r2_key: z3.string().optional().describe(
|
|
5263
5656
|
"R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
|
|
5264
5657
|
),
|
|
5265
|
-
r2_keys: z3.array(z3.string()).optional().describe(
|
|
5658
|
+
r2_keys: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
5266
5659
|
"Array of R2 object keys for carousel posts. Each is signed on demand. Alternative to media_urls."
|
|
5267
5660
|
),
|
|
5268
5661
|
job_id: z3.string().optional().describe(
|
|
5269
5662
|
"Async job ID from generate_image/generate_video. Resolves the completed job's R2 key and signs it. Alternative to media_url/r2_key."
|
|
5270
5663
|
),
|
|
5271
|
-
job_ids: z3.array(z3.string()).optional().describe(
|
|
5664
|
+
job_ids: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
5272
5665
|
"Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
|
|
5273
5666
|
),
|
|
5274
5667
|
platform_metadata: z3.object({
|
|
@@ -5298,7 +5691,10 @@ function registerDistributionTools(server) {
|
|
|
5298
5691
|
category_id: z3.string().optional(),
|
|
5299
5692
|
tags: z3.array(z3.string()).optional(),
|
|
5300
5693
|
made_for_kids: z3.boolean().optional(),
|
|
5301
|
-
notify_subscribers: z3.boolean().optional()
|
|
5694
|
+
notify_subscribers: z3.boolean().optional(),
|
|
5695
|
+
contains_synthetic_media: z3.boolean().optional().describe(
|
|
5696
|
+
"YouTube altered-or-synthetic-content disclosure. Defaults to true for MCP posts; set false explicitly for verified non-AI media."
|
|
5697
|
+
)
|
|
5302
5698
|
}).optional(),
|
|
5303
5699
|
facebook: z3.object({
|
|
5304
5700
|
page_id: z3.string().optional().describe("Facebook Page ID to post to."),
|
|
@@ -5381,14 +5777,8 @@ function registerDistributionTools(server) {
|
|
|
5381
5777
|
auto_rehost: z3.boolean().optional().describe(
|
|
5382
5778
|
"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."
|
|
5383
5779
|
),
|
|
5384
|
-
|
|
5385
|
-
"
|
|
5386
|
-
),
|
|
5387
|
-
origin: z3.enum(["human", "hermes", "user"]).optional().describe(
|
|
5388
|
-
"Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
|
|
5389
|
-
),
|
|
5390
|
-
hermes_run_id: z3.string().optional().describe(
|
|
5391
|
-
"Optional agent run identifier for traceability between a draft and the run that produced it."
|
|
5780
|
+
idempotency_key: z3.string().regex(/^[a-zA-Z0-9_-]{8,128}$/).optional().describe(
|
|
5781
|
+
"Stable 8-128 character retry key (letters, numbers, underscore, hyphen). Reuse the same key when retrying the same publish request to prevent duplicate posts."
|
|
5392
5782
|
)
|
|
5393
5783
|
},
|
|
5394
5784
|
async ({
|
|
@@ -5411,9 +5801,7 @@ function registerDistributionTools(server) {
|
|
|
5411
5801
|
platform_metadata,
|
|
5412
5802
|
account_id,
|
|
5413
5803
|
account_ids,
|
|
5414
|
-
|
|
5415
|
-
origin,
|
|
5416
|
-
hermes_run_id
|
|
5804
|
+
idempotency_key
|
|
5417
5805
|
}) => {
|
|
5418
5806
|
const format = response_format ?? "text";
|
|
5419
5807
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
@@ -5442,6 +5830,8 @@ function registerDistributionTools(server) {
|
|
|
5442
5830
|
}
|
|
5443
5831
|
let resolvedMediaUrl = media_url;
|
|
5444
5832
|
let resolvedMediaUrls = media_urls;
|
|
5833
|
+
let resolvedMediaUrlIsTrustedR2 = false;
|
|
5834
|
+
let resolvedMediaUrlsAreTrustedR2 = (media_urls ?? []).map(() => false);
|
|
5445
5835
|
const signR2Key = async (key) => {
|
|
5446
5836
|
const cleanKey = key.startsWith("r2://") ? key.slice(5) : key;
|
|
5447
5837
|
const { data: signData } = await callEdgeFunction(
|
|
@@ -5459,8 +5849,11 @@ function registerDistributionTools(server) {
|
|
|
5459
5849
|
);
|
|
5460
5850
|
const resultUrl = jobData?.job?.result_url;
|
|
5461
5851
|
if (!resultUrl) return null;
|
|
5462
|
-
if (!resultUrl.startsWith("http"))
|
|
5463
|
-
|
|
5852
|
+
if (!resultUrl.startsWith("http")) {
|
|
5853
|
+
const signed = await signR2Key(resultUrl);
|
|
5854
|
+
return signed ? { url: signed, trustedR2: true } : null;
|
|
5855
|
+
}
|
|
5856
|
+
return { url: resultUrl, trustedR2: false };
|
|
5464
5857
|
};
|
|
5465
5858
|
try {
|
|
5466
5859
|
if (r2_key && !resolvedMediaUrl) {
|
|
@@ -5477,6 +5870,7 @@ function registerDistributionTools(server) {
|
|
|
5477
5870
|
};
|
|
5478
5871
|
}
|
|
5479
5872
|
resolvedMediaUrl = signed;
|
|
5873
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
5480
5874
|
} else if (job_id && !resolvedMediaUrl && !r2_key) {
|
|
5481
5875
|
const resolved = await resolveJobId(job_id);
|
|
5482
5876
|
if (!resolved) {
|
|
@@ -5490,7 +5884,8 @@ function registerDistributionTools(server) {
|
|
|
5490
5884
|
isError: true
|
|
5491
5885
|
};
|
|
5492
5886
|
}
|
|
5493
|
-
resolvedMediaUrl = resolved;
|
|
5887
|
+
resolvedMediaUrl = resolved.url;
|
|
5888
|
+
resolvedMediaUrlIsTrustedR2 = resolved.trustedR2;
|
|
5494
5889
|
}
|
|
5495
5890
|
if (r2_keys && r2_keys.length > 0 && !resolvedMediaUrls) {
|
|
5496
5891
|
const signed = await Promise.all(r2_keys.map(signR2Key));
|
|
@@ -5507,6 +5902,7 @@ function registerDistributionTools(server) {
|
|
|
5507
5902
|
};
|
|
5508
5903
|
}
|
|
5509
5904
|
resolvedMediaUrls = signed;
|
|
5905
|
+
resolvedMediaUrlsAreTrustedR2 = signed.map(() => true);
|
|
5510
5906
|
} else if (job_ids && job_ids.length > 0 && !resolvedMediaUrls && !r2_keys) {
|
|
5511
5907
|
const resolved = await Promise.all(job_ids.map(resolveJobId));
|
|
5512
5908
|
const failIdx = resolved.findIndex((r) => !r);
|
|
@@ -5521,10 +5917,12 @@ function registerDistributionTools(server) {
|
|
|
5521
5917
|
isError: true
|
|
5522
5918
|
};
|
|
5523
5919
|
}
|
|
5524
|
-
|
|
5920
|
+
const resolvedJobs = resolved;
|
|
5921
|
+
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
5922
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
|
|
5525
5923
|
}
|
|
5526
5924
|
const shouldRehost = auto_rehost !== false;
|
|
5527
|
-
if (shouldRehost && resolvedMediaUrl) {
|
|
5925
|
+
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
5528
5926
|
const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
|
|
5529
5927
|
if ("error" in rehost) {
|
|
5530
5928
|
return {
|
|
@@ -5538,10 +5936,13 @@ function registerDistributionTools(server) {
|
|
|
5538
5936
|
};
|
|
5539
5937
|
}
|
|
5540
5938
|
resolvedMediaUrl = rehost.signedUrl;
|
|
5939
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
5541
5940
|
}
|
|
5542
5941
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
5543
5942
|
const rehosted = await Promise.all(
|
|
5544
|
-
resolvedMediaUrls.map(
|
|
5943
|
+
resolvedMediaUrls.map(
|
|
5944
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
|
|
5945
|
+
)
|
|
5545
5946
|
);
|
|
5546
5947
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
5547
5948
|
if (failIdx !== -1) {
|
|
@@ -5557,6 +5958,7 @@ function registerDistributionTools(server) {
|
|
|
5557
5958
|
};
|
|
5558
5959
|
}
|
|
5559
5960
|
resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
|
|
5961
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedMediaUrls.map(() => true);
|
|
5560
5962
|
}
|
|
5561
5963
|
} catch (resolveErr) {
|
|
5562
5964
|
return {
|
|
@@ -5569,6 +5971,42 @@ function registerDistributionTools(server) {
|
|
|
5569
5971
|
isError: true
|
|
5570
5972
|
};
|
|
5571
5973
|
}
|
|
5974
|
+
const hasResolvedMedia = Boolean(
|
|
5975
|
+
resolvedMediaUrl || resolvedMediaUrls && resolvedMediaUrls.length > 0
|
|
5976
|
+
);
|
|
5977
|
+
const resolvedMediaType = inferScheduleMediaType(
|
|
5978
|
+
media_type,
|
|
5979
|
+
[resolvedMediaUrl, r2_key],
|
|
5980
|
+
[resolvedMediaUrls, r2_keys]
|
|
5981
|
+
);
|
|
5982
|
+
if (hasResolvedMedia && !resolvedMediaType) {
|
|
5983
|
+
return {
|
|
5984
|
+
content: [
|
|
5985
|
+
{
|
|
5986
|
+
type: "text",
|
|
5987
|
+
text: "media_type is required when the media format cannot be inferred from a file extension. Set IMAGE, VIDEO, or CAROUSEL_ALBUM explicitly."
|
|
5988
|
+
}
|
|
5989
|
+
],
|
|
5990
|
+
isError: true
|
|
5991
|
+
};
|
|
5992
|
+
}
|
|
5993
|
+
const finalUrls = [resolvedMediaUrl, ...resolvedMediaUrls ?? []].filter(
|
|
5994
|
+
(value) => typeof value === "string" && value.length > 0
|
|
5995
|
+
);
|
|
5996
|
+
for (const url of finalUrls) {
|
|
5997
|
+
const validationError = await validatePublishMediaUrl(url);
|
|
5998
|
+
if (validationError) {
|
|
5999
|
+
return {
|
|
6000
|
+
content: [
|
|
6001
|
+
{
|
|
6002
|
+
type: "text",
|
|
6003
|
+
text: `Media URL blocked: ${validationError}`
|
|
6004
|
+
}
|
|
6005
|
+
],
|
|
6006
|
+
isError: true
|
|
6007
|
+
};
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
5572
6010
|
const normalizedPlatforms = platforms.map(
|
|
5573
6011
|
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
5574
6012
|
);
|
|
@@ -5694,12 +6132,32 @@ Created with Social Neuron`;
|
|
|
5694
6132
|
};
|
|
5695
6133
|
return true;
|
|
5696
6134
|
})();
|
|
6135
|
+
const defaultAiDisclosure = (platformKey, field) => {
|
|
6136
|
+
const existing = normalizedPlatformMetadata?.[platformKey];
|
|
6137
|
+
if (existing && existing[field] !== void 0) return;
|
|
6138
|
+
normalizedPlatformMetadata = {
|
|
6139
|
+
...normalizedPlatformMetadata ?? {},
|
|
6140
|
+
[platformKey]: {
|
|
6141
|
+
...existing ?? {},
|
|
6142
|
+
[field]: true
|
|
6143
|
+
}
|
|
6144
|
+
};
|
|
6145
|
+
};
|
|
6146
|
+
if (normalizedPlatforms.includes("TikTok")) {
|
|
6147
|
+
defaultAiDisclosure("tiktok", "is_ai_generated");
|
|
6148
|
+
}
|
|
6149
|
+
if (normalizedPlatforms.includes("Instagram")) {
|
|
6150
|
+
defaultAiDisclosure("instagram", "is_ai_generated");
|
|
6151
|
+
}
|
|
6152
|
+
if (normalizedPlatforms.includes("YouTube")) {
|
|
6153
|
+
defaultAiDisclosure("youtube", "contains_synthetic_media");
|
|
6154
|
+
}
|
|
5697
6155
|
const { data, error } = await callEdgeFunction(
|
|
5698
6156
|
"schedule-post",
|
|
5699
6157
|
{
|
|
5700
6158
|
mediaUrl: resolvedMediaUrl,
|
|
5701
6159
|
mediaUrls: resolvedMediaUrls,
|
|
5702
|
-
mediaType:
|
|
6160
|
+
mediaType: resolvedMediaType ?? void 0,
|
|
5703
6161
|
caption: finalCaption,
|
|
5704
6162
|
platforms: normalizedPlatforms,
|
|
5705
6163
|
title,
|
|
@@ -5712,14 +6170,10 @@ Created with Social Neuron`;
|
|
|
5712
6170
|
normalizedPlatformMetadata
|
|
5713
6171
|
)
|
|
5714
6172
|
} : {},
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
//
|
|
5719
|
-
// against the posts.origin CHECK constraint and persists hermesRunId when
|
|
5720
|
-
// origin === 'hermes'.
|
|
5721
|
-
...origin ? { origin } : {},
|
|
5722
|
-
...hermes_run_id ? { hermesRunId: hermes_run_id } : {}
|
|
6173
|
+
...idempotency_key ? { idempotencyKey: idempotency_key } : {}
|
|
6174
|
+
// Attribution is assigned by the authenticated gateway. Visual QA
|
|
6175
|
+
// attestations are server-produced evidence and are deliberately not
|
|
6176
|
+
// accepted from an MCP caller.
|
|
5723
6177
|
},
|
|
5724
6178
|
{ timeoutMs: 3e4 }
|
|
5725
6179
|
);
|
|
@@ -5785,6 +6239,99 @@ Created with Social Neuron`;
|
|
|
5785
6239
|
};
|
|
5786
6240
|
}
|
|
5787
6241
|
);
|
|
6242
|
+
server.tool(
|
|
6243
|
+
"reschedule_post",
|
|
6244
|
+
"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.",
|
|
6245
|
+
{
|
|
6246
|
+
post_id: z3.string().uuid().describe("Post ID returned by list_recent_posts."),
|
|
6247
|
+
project_id: z3.string().uuid().optional().describe(
|
|
6248
|
+
"Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
|
|
6249
|
+
),
|
|
6250
|
+
scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
|
|
6251
|
+
expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
|
|
6252
|
+
"Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
|
|
6253
|
+
),
|
|
6254
|
+
response_format: z3.enum(["text", "json"]).default("text")
|
|
6255
|
+
},
|
|
6256
|
+
async ({
|
|
6257
|
+
post_id,
|
|
6258
|
+
project_id,
|
|
6259
|
+
scheduled_at,
|
|
6260
|
+
expected_scheduled_at,
|
|
6261
|
+
response_format
|
|
6262
|
+
}) => {
|
|
6263
|
+
const next = new Date(scheduled_at);
|
|
6264
|
+
if (!Number.isFinite(next.getTime()) || next.getTime() <= Date.now()) {
|
|
6265
|
+
return {
|
|
6266
|
+
content: [
|
|
6267
|
+
{
|
|
6268
|
+
type: "text",
|
|
6269
|
+
text: "scheduled_at must be a valid future ISO datetime with timezone."
|
|
6270
|
+
}
|
|
6271
|
+
],
|
|
6272
|
+
isError: true
|
|
6273
|
+
};
|
|
6274
|
+
}
|
|
6275
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
6276
|
+
if (!resolvedProjectId) {
|
|
6277
|
+
return {
|
|
6278
|
+
content: [
|
|
6279
|
+
{
|
|
6280
|
+
type: "text",
|
|
6281
|
+
text: "No project_id was provided and no default project is configured."
|
|
6282
|
+
}
|
|
6283
|
+
],
|
|
6284
|
+
isError: true
|
|
6285
|
+
};
|
|
6286
|
+
}
|
|
6287
|
+
const userId = await getDefaultUserId();
|
|
6288
|
+
const rateLimit = checkRateLimit("posting", `reschedule_post:${userId}`);
|
|
6289
|
+
if (!rateLimit.allowed) {
|
|
6290
|
+
return {
|
|
6291
|
+
content: [
|
|
6292
|
+
{
|
|
6293
|
+
type: "text",
|
|
6294
|
+
text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
|
|
6295
|
+
}
|
|
6296
|
+
],
|
|
6297
|
+
isError: true
|
|
6298
|
+
};
|
|
6299
|
+
}
|
|
6300
|
+
const { data: result, error } = await callEdgeFunction("mcp-data", {
|
|
6301
|
+
action: "reschedule-scheduled-post",
|
|
6302
|
+
post_id,
|
|
6303
|
+
projectId: resolvedProjectId,
|
|
6304
|
+
project_id: resolvedProjectId,
|
|
6305
|
+
scheduled_at: next.toISOString(),
|
|
6306
|
+
...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
|
|
6307
|
+
});
|
|
6308
|
+
if (error || !result?.success) {
|
|
6309
|
+
const code = result?.error ?? error ?? "reschedule_failed";
|
|
6310
|
+
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.";
|
|
6311
|
+
return {
|
|
6312
|
+
content: [{ type: "text", text: recovery }],
|
|
6313
|
+
isError: true
|
|
6314
|
+
};
|
|
6315
|
+
}
|
|
6316
|
+
const publicResult = {
|
|
6317
|
+
success: true,
|
|
6318
|
+
post_id: result.post_id ?? post_id,
|
|
6319
|
+
project_id: result.project_id ?? resolvedProjectId,
|
|
6320
|
+
previous_scheduled_at: result.previous_scheduled_at ?? null,
|
|
6321
|
+
scheduled_at: result.scheduled_at ?? next.toISOString()
|
|
6322
|
+
};
|
|
6323
|
+
const structuredContent = asEnvelope3(publicResult);
|
|
6324
|
+
return {
|
|
6325
|
+
structuredContent,
|
|
6326
|
+
content: [
|
|
6327
|
+
{
|
|
6328
|
+
type: "text",
|
|
6329
|
+
text: response_format === "json" ? JSON.stringify(structuredContent, null, 2) : `Post ${publicResult.post_id} rescheduled to ${publicResult.scheduled_at}.`
|
|
6330
|
+
}
|
|
6331
|
+
]
|
|
6332
|
+
};
|
|
6333
|
+
}
|
|
6334
|
+
);
|
|
5788
6335
|
server.tool(
|
|
5789
6336
|
"list_connected_accounts",
|
|
5790
6337
|
"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.",
|
|
@@ -5813,14 +6360,16 @@ Created with Social Neuron`;
|
|
|
5813
6360
|
isError: true
|
|
5814
6361
|
};
|
|
5815
6362
|
}
|
|
5816
|
-
const accounts = result.accounts ?? [];
|
|
6363
|
+
const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
5817
6364
|
if (accounts.length === 0) {
|
|
5818
6365
|
if (format === "json") {
|
|
6366
|
+
const structuredContent = asEnvelope3({ accounts: [] });
|
|
5819
6367
|
return {
|
|
6368
|
+
structuredContent,
|
|
5820
6369
|
content: [
|
|
5821
6370
|
{
|
|
5822
6371
|
type: "text",
|
|
5823
|
-
text: JSON.stringify(
|
|
6372
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
5824
6373
|
}
|
|
5825
6374
|
]
|
|
5826
6375
|
};
|
|
@@ -5848,11 +6397,13 @@ Created with Social Neuron`;
|
|
|
5848
6397
|
);
|
|
5849
6398
|
}
|
|
5850
6399
|
if (format === "json") {
|
|
6400
|
+
const structuredContent = asEnvelope3({ accounts });
|
|
5851
6401
|
return {
|
|
6402
|
+
structuredContent,
|
|
5852
6403
|
content: [
|
|
5853
6404
|
{
|
|
5854
6405
|
type: "text",
|
|
5855
|
-
text: JSON.stringify(
|
|
6406
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
5856
6407
|
}
|
|
5857
6408
|
]
|
|
5858
6409
|
};
|
|
@@ -5866,6 +6417,9 @@ Created with Social Neuron`;
|
|
|
5866
6417
|
"list_recent_posts",
|
|
5867
6418
|
"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.",
|
|
5868
6419
|
{
|
|
6420
|
+
project_id: z3.string().uuid().optional().describe(
|
|
6421
|
+
"Brand/project ID to scope posts. Defaults to the authenticated key's project or the account default."
|
|
6422
|
+
),
|
|
5869
6423
|
platform: z3.enum([
|
|
5870
6424
|
"youtube",
|
|
5871
6425
|
"tiktok",
|
|
@@ -5881,13 +6435,27 @@ Created with Social Neuron`;
|
|
|
5881
6435
|
limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
5882
6436
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5883
6437
|
},
|
|
5884
|
-
async ({ platform: platform3, status, days, limit, response_format }) => {
|
|
6438
|
+
async ({ project_id, platform: platform3, status, days, limit, response_format }) => {
|
|
5885
6439
|
const format = response_format ?? "text";
|
|
5886
6440
|
const lookbackDays = days ?? 7;
|
|
5887
|
-
const
|
|
5888
|
-
|
|
5889
|
-
|
|
6441
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
6442
|
+
if (!resolvedProjectId) {
|
|
6443
|
+
return {
|
|
6444
|
+
content: [
|
|
6445
|
+
{
|
|
6446
|
+
type: "text",
|
|
6447
|
+
text: "No project_id was provided and no default project is configured."
|
|
6448
|
+
}
|
|
6449
|
+
],
|
|
6450
|
+
isError: true
|
|
6451
|
+
};
|
|
6452
|
+
}
|
|
6453
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6454
|
+
action: "recent-posts",
|
|
6455
|
+
days: lookbackDays,
|
|
5890
6456
|
limit: limit ?? 20,
|
|
6457
|
+
projectId: resolvedProjectId,
|
|
6458
|
+
project_id: resolvedProjectId,
|
|
5891
6459
|
...platform3 ? { platform: platform3 } : {},
|
|
5892
6460
|
...status ? { status } : {}
|
|
5893
6461
|
});
|
|
@@ -5902,7 +6470,7 @@ Created with Social Neuron`;
|
|
|
5902
6470
|
isError: true
|
|
5903
6471
|
};
|
|
5904
6472
|
}
|
|
5905
|
-
const rows = result.posts ?? [];
|
|
6473
|
+
const rows = (result.posts ?? []).map(publicPostRecord).filter((post) => post !== null);
|
|
5906
6474
|
if (rows.length === 0) {
|
|
5907
6475
|
if (format === "json") {
|
|
5908
6476
|
const structuredContent2 = asEnvelope3({ posts: [] });
|
|
@@ -5977,8 +6545,11 @@ Created with Social Neuron`;
|
|
|
5977
6545
|
};
|
|
5978
6546
|
server.tool(
|
|
5979
6547
|
"find_next_slots",
|
|
5980
|
-
"Find optimal posting time slots based on
|
|
6548
|
+
"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.",
|
|
5981
6549
|
{
|
|
6550
|
+
project_id: z3.string().uuid().optional().describe(
|
|
6551
|
+
"Brand/project ID used for conflict detection. Defaults to the authenticated key's project or the account default."
|
|
6552
|
+
),
|
|
5982
6553
|
platforms: z3.array(
|
|
5983
6554
|
z3.enum([
|
|
5984
6555
|
"youtube",
|
|
@@ -5997,6 +6568,7 @@ Created with Social Neuron`;
|
|
|
5997
6568
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
5998
6569
|
},
|
|
5999
6570
|
async ({
|
|
6571
|
+
project_id,
|
|
6000
6572
|
platforms,
|
|
6001
6573
|
count,
|
|
6002
6574
|
start_after,
|
|
@@ -6004,13 +6576,35 @@ Created with Social Neuron`;
|
|
|
6004
6576
|
response_format
|
|
6005
6577
|
}) => {
|
|
6006
6578
|
try {
|
|
6579
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
6580
|
+
if (!resolvedProjectId) {
|
|
6581
|
+
return {
|
|
6582
|
+
content: [
|
|
6583
|
+
{
|
|
6584
|
+
type: "text",
|
|
6585
|
+
text: "No project_id was provided and no default project is configured."
|
|
6586
|
+
}
|
|
6587
|
+
],
|
|
6588
|
+
isError: true
|
|
6589
|
+
};
|
|
6590
|
+
}
|
|
6007
6591
|
const startDate = start_after ? new Date(start_after) : /* @__PURE__ */ new Date();
|
|
6592
|
+
if (!Number.isFinite(startDate.getTime())) {
|
|
6593
|
+
return {
|
|
6594
|
+
content: [
|
|
6595
|
+
{ type: "text", text: "start_after must be a valid ISO datetime." }
|
|
6596
|
+
],
|
|
6597
|
+
isError: true
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6008
6600
|
const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
6009
6601
|
const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
|
|
6010
6602
|
action: "scheduled-posts",
|
|
6011
6603
|
start_date: startDate.toISOString(),
|
|
6012
6604
|
end_date: endDate.toISOString(),
|
|
6013
|
-
statuses: ["scheduled", "draft"]
|
|
6605
|
+
statuses: ["pending", "scheduled", "draft"],
|
|
6606
|
+
projectId: resolvedProjectId,
|
|
6607
|
+
project_id: resolvedProjectId
|
|
6014
6608
|
});
|
|
6015
6609
|
const existingPosts = postsError ? [] : postsResult?.posts ?? [];
|
|
6016
6610
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
@@ -6050,19 +6644,17 @@ Created with Social Neuron`;
|
|
|
6050
6644
|
const slots = candidates.filter((s) => !s.conflict).sort((a, b) => b.engagement_score - a.engagement_score).slice(0, count);
|
|
6051
6645
|
const conflictsAvoided = candidates.filter((s) => s.conflict).length;
|
|
6052
6646
|
if (response_format === "json") {
|
|
6647
|
+
const structuredContent = asEnvelope3({
|
|
6648
|
+
slots,
|
|
6649
|
+
total_candidates: candidates.length,
|
|
6650
|
+
conflicts_avoided: conflictsAvoided
|
|
6651
|
+
});
|
|
6053
6652
|
return {
|
|
6653
|
+
structuredContent,
|
|
6054
6654
|
content: [
|
|
6055
6655
|
{
|
|
6056
6656
|
type: "text",
|
|
6057
|
-
text: JSON.stringify(
|
|
6058
|
-
asEnvelope3({
|
|
6059
|
-
slots,
|
|
6060
|
-
total_candidates: candidates.length,
|
|
6061
|
-
conflicts_avoided: conflictsAvoided
|
|
6062
|
-
}),
|
|
6063
|
-
null,
|
|
6064
|
-
2
|
|
6065
|
-
)
|
|
6657
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
6066
6658
|
}
|
|
6067
6659
|
],
|
|
6068
6660
|
isError: false
|
|
@@ -6427,7 +7019,7 @@ Created with Social Neuron`;
|
|
|
6427
7019
|
const results = [];
|
|
6428
7020
|
const buildIdempotencyKey = (post) => {
|
|
6429
7021
|
const planId = effectivePlanId ?? (typeof workingPlan.plan_id === "string" ? String(workingPlan.plan_id) : "inline");
|
|
6430
|
-
const captionHash =
|
|
7022
|
+
const captionHash = createHash2("sha256").update(post.caption).digest("hex").slice(0, 16);
|
|
6431
7023
|
const raw = [
|
|
6432
7024
|
"schedule_content_plan",
|
|
6433
7025
|
planId,
|
|
@@ -6437,7 +7029,7 @@ Created with Social Neuron`;
|
|
|
6437
7029
|
captionHash,
|
|
6438
7030
|
idempotency_seed ?? ""
|
|
6439
7031
|
].join(":");
|
|
6440
|
-
return `plan-${
|
|
7032
|
+
return `plan-${createHash2("sha256").update(raw).digest("hex").slice(0, 48)}`;
|
|
6441
7033
|
};
|
|
6442
7034
|
const scheduleOne = async (post) => {
|
|
6443
7035
|
if (!post.schedule_at) {
|
|
@@ -6623,7 +7215,7 @@ Created with Social Neuron`;
|
|
|
6623
7215
|
}
|
|
6624
7216
|
);
|
|
6625
7217
|
}
|
|
6626
|
-
var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING;
|
|
7218
|
+
var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
|
|
6627
7219
|
var init_distribution = __esm({
|
|
6628
7220
|
"src/tools/distribution.ts"() {
|
|
6629
7221
|
"use strict";
|
|
@@ -6644,8 +7236,29 @@ var init_distribution = __esm({
|
|
|
6644
7236
|
threads: "Threads",
|
|
6645
7237
|
bluesky: "Bluesky"
|
|
6646
7238
|
};
|
|
6647
|
-
TIKTOK_AUDIT_APPROVED = false
|
|
7239
|
+
TIKTOK_AUDIT_APPROVED = !["false", "0", "no"].includes(
|
|
7240
|
+
(process.env.TIKTOK_AUDIT_APPROVED ?? "").toLowerCase()
|
|
7241
|
+
);
|
|
6648
7242
|
MCP_NOT_LIVE_FOR_POSTING = /* @__PURE__ */ new Set(["LinkedIn", "Pinterest", "Reddit"]);
|
|
7243
|
+
VIDEO_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7244
|
+
"mp4",
|
|
7245
|
+
"mov",
|
|
7246
|
+
"m4v",
|
|
7247
|
+
"webm",
|
|
7248
|
+
"avi",
|
|
7249
|
+
"mkv",
|
|
7250
|
+
"mpeg",
|
|
7251
|
+
"mpg"
|
|
7252
|
+
]);
|
|
7253
|
+
IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7254
|
+
"jpg",
|
|
7255
|
+
"jpeg",
|
|
7256
|
+
"png",
|
|
7257
|
+
"webp",
|
|
7258
|
+
"gif",
|
|
7259
|
+
"avif",
|
|
7260
|
+
"heic"
|
|
7261
|
+
]);
|
|
6649
7262
|
}
|
|
6650
7263
|
});
|
|
6651
7264
|
|
|
@@ -6926,7 +7539,10 @@ function registerMediaTools(server) {
|
|
|
6926
7539
|
content: [
|
|
6927
7540
|
{
|
|
6928
7541
|
type: "text",
|
|
6929
|
-
|
|
7542
|
+
// Presigned-store bodies may include provider request IDs,
|
|
7543
|
+
// bucket names, or signed URL fragments. Status is enough
|
|
7544
|
+
// for user recovery and safe diagnostics.
|
|
7545
|
+
text: `R2 upload failed (HTTP ${putResp.status}). Please retry.`
|
|
6930
7546
|
}
|
|
6931
7547
|
],
|
|
6932
7548
|
isError: true
|
|
@@ -7169,7 +7785,11 @@ function registerAnalyticsTools(server) {
|
|
|
7169
7785
|
action: "analytics",
|
|
7170
7786
|
platform: platform3,
|
|
7171
7787
|
days: lookbackDays,
|
|
7172
|
-
|
|
7788
|
+
// Fetch extra snapshots, then deduplicate to the requested post count.
|
|
7789
|
+
// The backend understands latestOnly after the paired application
|
|
7790
|
+
// deployment; older deployments safely ignore the hint.
|
|
7791
|
+
limit: Math.min(maxPosts * 5, 100),
|
|
7792
|
+
latestOnly: true,
|
|
7173
7793
|
contentId: content_id,
|
|
7174
7794
|
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
7175
7795
|
});
|
|
@@ -7179,7 +7799,14 @@ function registerAnalyticsTools(server) {
|
|
|
7179
7799
|
isError: true
|
|
7180
7800
|
};
|
|
7181
7801
|
}
|
|
7182
|
-
const
|
|
7802
|
+
const snapshotRows = result?.rows ?? [];
|
|
7803
|
+
const seenSnapshots = /* @__PURE__ */ new Set();
|
|
7804
|
+
const rows = [...snapshotRows].sort((a, b) => b.captured_at.localeCompare(a.captured_at)).filter((row) => {
|
|
7805
|
+
const key = `${row.post_id}:${row.platform.toLowerCase()}`;
|
|
7806
|
+
if (seenSnapshots.has(key)) return false;
|
|
7807
|
+
seenSnapshots.add(key);
|
|
7808
|
+
return true;
|
|
7809
|
+
}).slice(0, maxPosts);
|
|
7183
7810
|
if (rows.length === 0) {
|
|
7184
7811
|
if (format === "json") {
|
|
7185
7812
|
const structuredContent = asEnvelope4({
|
|
@@ -7358,6 +7985,84 @@ var init_analytics2 = __esm({
|
|
|
7358
7985
|
}
|
|
7359
7986
|
});
|
|
7360
7987
|
|
|
7988
|
+
// src/lib/brandUrlInput.ts
|
|
7989
|
+
function canonicalizeHost(hostname) {
|
|
7990
|
+
return hostname.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, "");
|
|
7991
|
+
}
|
|
7992
|
+
function hostLooksLikeDomain(hostname) {
|
|
7993
|
+
return /\S+\.\S+/.test(canonicalizeHost(hostname));
|
|
7994
|
+
}
|
|
7995
|
+
function resolveHandle(handle, platform3) {
|
|
7996
|
+
const normalized = handle.replace(/^@/, "").trim();
|
|
7997
|
+
if (!normalized) return { url: null, handle: null, platform: null, ambiguous: false };
|
|
7998
|
+
if (!platform3) return { url: null, handle: normalized, platform: null, ambiguous: true };
|
|
7999
|
+
return {
|
|
8000
|
+
url: PROFILE_URL_BUILDERS[platform3](normalized),
|
|
8001
|
+
handle: normalized,
|
|
8002
|
+
platform: platform3,
|
|
8003
|
+
ambiguous: false
|
|
8004
|
+
};
|
|
8005
|
+
}
|
|
8006
|
+
function normalizeBrandUrlInput(raw, explicitPlatform) {
|
|
8007
|
+
const input = (raw ?? "").trim();
|
|
8008
|
+
if (!input) return { url: null, handle: null, platform: null, ambiguous: false };
|
|
8009
|
+
if (/^https?:\/\//i.test(input)) {
|
|
8010
|
+
let parsed;
|
|
8011
|
+
try {
|
|
8012
|
+
parsed = new URL(input);
|
|
8013
|
+
} catch {
|
|
8014
|
+
return { url: null, handle: null, platform: null, ambiguous: false, invalidUrl: true };
|
|
8015
|
+
}
|
|
8016
|
+
if (hostLooksLikeDomain(parsed.hostname)) {
|
|
8017
|
+
return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
|
|
8018
|
+
}
|
|
8019
|
+
return resolveHandle(canonicalizeHost(parsed.hostname), explicitPlatform);
|
|
8020
|
+
}
|
|
8021
|
+
const qualified = input.match(/^([a-z]{1,10}):\s*@?(.+)$/i);
|
|
8022
|
+
if (qualified) {
|
|
8023
|
+
const platform3 = PLATFORM_ALIASES[qualified[1].toLowerCase()];
|
|
8024
|
+
if (platform3) return resolveHandle(qualified[2], platform3);
|
|
8025
|
+
}
|
|
8026
|
+
if (input.startsWith("@")) return resolveHandle(input, explicitPlatform);
|
|
8027
|
+
if (!/\s/.test(input)) {
|
|
8028
|
+
try {
|
|
8029
|
+
const parsed = new URL(`https://${input}`);
|
|
8030
|
+
if (hostLooksLikeDomain(parsed.hostname)) {
|
|
8031
|
+
return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
|
|
8032
|
+
}
|
|
8033
|
+
} catch {
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
return resolveHandle(input.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, ""), explicitPlatform);
|
|
8037
|
+
}
|
|
8038
|
+
var PLATFORM_ALIASES, PROFILE_URL_BUILDERS, UNICODE_DOT_VARIANTS;
|
|
8039
|
+
var init_brandUrlInput = __esm({
|
|
8040
|
+
"src/lib/brandUrlInput.ts"() {
|
|
8041
|
+
"use strict";
|
|
8042
|
+
PLATFORM_ALIASES = {
|
|
8043
|
+
instagram: "instagram",
|
|
8044
|
+
ig: "instagram",
|
|
8045
|
+
insta: "instagram",
|
|
8046
|
+
tiktok: "tiktok",
|
|
8047
|
+
tt: "tiktok",
|
|
8048
|
+
twitter: "twitter",
|
|
8049
|
+
x: "twitter",
|
|
8050
|
+
linkedin: "linkedin",
|
|
8051
|
+
li: "linkedin",
|
|
8052
|
+
youtube: "youtube",
|
|
8053
|
+
yt: "youtube"
|
|
8054
|
+
};
|
|
8055
|
+
PROFILE_URL_BUILDERS = {
|
|
8056
|
+
instagram: (handle) => `https://instagram.com/${encodeURIComponent(handle)}`,
|
|
8057
|
+
tiktok: (handle) => `https://tiktok.com/@${encodeURIComponent(handle)}`,
|
|
8058
|
+
twitter: (handle) => `https://x.com/${encodeURIComponent(handle)}`,
|
|
8059
|
+
linkedin: (handle) => `https://linkedin.com/company/${encodeURIComponent(handle)}`,
|
|
8060
|
+
youtube: (handle) => `https://youtube.com/@${encodeURIComponent(handle)}`
|
|
8061
|
+
};
|
|
8062
|
+
UNICODE_DOT_VARIANTS = /[。.。]/g;
|
|
8063
|
+
}
|
|
8064
|
+
});
|
|
8065
|
+
|
|
7361
8066
|
// src/tools/brand.ts
|
|
7362
8067
|
import { z as z6 } from "zod";
|
|
7363
8068
|
function asEnvelope5(data) {
|
|
@@ -7374,12 +8079,31 @@ function registerBrandTools(server) {
|
|
|
7374
8079
|
"extract_brand",
|
|
7375
8080
|
"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.",
|
|
7376
8081
|
{
|
|
7377
|
-
url: z6.string().
|
|
7378
|
-
'The website URL to analyze for brand identity (e.g. "https://example.com").'
|
|
8082
|
+
url: z6.string().min(1).max(2048).describe(
|
|
8083
|
+
'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").'
|
|
7379
8084
|
),
|
|
7380
8085
|
response_format: z6.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7381
8086
|
},
|
|
7382
|
-
async ({ url, response_format }) => {
|
|
8087
|
+
async ({ url: rawUrl, response_format }) => {
|
|
8088
|
+
const normalized = normalizeBrandUrlInput(rawUrl);
|
|
8089
|
+
if (normalized.invalidUrl) {
|
|
8090
|
+
return {
|
|
8091
|
+
content: [{ type: "text", text: `URL blocked: "${rawUrl}" is not a valid URL.` }],
|
|
8092
|
+
isError: true
|
|
8093
|
+
};
|
|
8094
|
+
}
|
|
8095
|
+
if (!normalized.url) {
|
|
8096
|
+
return {
|
|
8097
|
+
content: [
|
|
8098
|
+
{
|
|
8099
|
+
type: "text",
|
|
8100
|
+
text: `"${rawUrl}" looks like a handle, not a full URL. Provide a profile URL or use "platform:handle" shorthand (for example "instagram:acmefoods").`
|
|
8101
|
+
}
|
|
8102
|
+
],
|
|
8103
|
+
isError: true
|
|
8104
|
+
};
|
|
8105
|
+
}
|
|
8106
|
+
const url = normalized.url;
|
|
7383
8107
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
7384
8108
|
if (!ssrfCheck.isValid) {
|
|
7385
8109
|
return {
|
|
@@ -7687,6 +8411,7 @@ var init_brand = __esm({
|
|
|
7687
8411
|
init_supabase();
|
|
7688
8412
|
init_ssrf();
|
|
7689
8413
|
init_version();
|
|
8414
|
+
init_brandUrlInput();
|
|
7690
8415
|
}
|
|
7691
8416
|
});
|
|
7692
8417
|
|
|
@@ -8062,7 +8787,7 @@ function registerRemotionTools(server) {
|
|
|
8062
8787
|
},
|
|
8063
8788
|
async ({ composition_id, output_format, props }) => {
|
|
8064
8789
|
const userId = await getDefaultUserId();
|
|
8065
|
-
const rateLimit = checkRateLimit("
|
|
8790
|
+
const rateLimit = checkRateLimit("generation", `render_demo_video:${userId}`);
|
|
8066
8791
|
if (!rateLimit.allowed) {
|
|
8067
8792
|
return {
|
|
8068
8793
|
content: [
|
|
@@ -11896,7 +12621,9 @@ function toolKnowledgeDocument(tool) {
|
|
|
11896
12621
|
function getKnowledgeDocuments() {
|
|
11897
12622
|
return [
|
|
11898
12623
|
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
11899
|
-
...TOOL_CATALOG.filter((t) => !t.internal).map(
|
|
12624
|
+
...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
|
|
12625
|
+
toolKnowledgeDocument
|
|
12626
|
+
)
|
|
11900
12627
|
];
|
|
11901
12628
|
}
|
|
11902
12629
|
function tokenize(input) {
|
|
@@ -12013,7 +12740,7 @@ function registerDiscoveryTools(server) {
|
|
|
12013
12740
|
if (query) {
|
|
12014
12741
|
results = searchTools(query);
|
|
12015
12742
|
}
|
|
12016
|
-
results = results.filter((t) => !t.internal);
|
|
12743
|
+
results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
|
|
12017
12744
|
if (module) {
|
|
12018
12745
|
const moduleTools = getToolsByModule(module);
|
|
12019
12746
|
results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
|
|
@@ -13944,11 +14671,11 @@ function hexToLab(hex) {
|
|
|
13944
14671
|
const lb = srgbToLinear(b);
|
|
13945
14672
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
13946
14673
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
13947
|
-
const
|
|
14674
|
+
const z40 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
13948
14675
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
13949
14676
|
const fx = f(x / 0.95047);
|
|
13950
14677
|
const fy = f(y / 1);
|
|
13951
|
-
const fz = f(
|
|
14678
|
+
const fz = f(z40 / 1.08883);
|
|
13952
14679
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
13953
14680
|
}
|
|
13954
14681
|
function deltaE2000(lab1, lab2) {
|
|
@@ -14188,10 +14915,10 @@ function registerBrandRuntimeTools(server) {
|
|
|
14188
14915
|
pagesScraped: meta.pagesScraped || 0
|
|
14189
14916
|
}
|
|
14190
14917
|
};
|
|
14191
|
-
const
|
|
14918
|
+
const envelope2 = asEnvelope23(runtime);
|
|
14192
14919
|
return {
|
|
14193
|
-
structuredContent:
|
|
14194
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
14920
|
+
structuredContent: envelope2,
|
|
14921
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14195
14922
|
};
|
|
14196
14923
|
}
|
|
14197
14924
|
);
|
|
@@ -14331,9 +15058,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
14331
15058
|
}
|
|
14332
15059
|
const profile = row.profile_data;
|
|
14333
15060
|
const checkResult = computeBrandConsistency(content, profile);
|
|
14334
|
-
const
|
|
15061
|
+
const envelope2 = asEnvelope23(checkResult);
|
|
14335
15062
|
return {
|
|
14336
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15063
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14337
15064
|
};
|
|
14338
15065
|
}
|
|
14339
15066
|
);
|
|
@@ -14365,9 +15092,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
14365
15092
|
content_colors,
|
|
14366
15093
|
threshold ?? 10
|
|
14367
15094
|
);
|
|
14368
|
-
const
|
|
15095
|
+
const envelope2 = asEnvelope23(auditResult);
|
|
14369
15096
|
return {
|
|
14370
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15097
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14371
15098
|
};
|
|
14372
15099
|
}
|
|
14373
15100
|
);
|
|
@@ -14400,9 +15127,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
14400
15127
|
row.profile_data.typography,
|
|
14401
15128
|
format
|
|
14402
15129
|
);
|
|
14403
|
-
const
|
|
15130
|
+
const envelope2 = asEnvelope23({ format, tokens: output });
|
|
14404
15131
|
return {
|
|
14405
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15132
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14406
15133
|
};
|
|
14407
15134
|
}
|
|
14408
15135
|
);
|
|
@@ -14556,7 +15283,7 @@ function registerCarouselTools(server) {
|
|
|
14556
15283
|
};
|
|
14557
15284
|
}
|
|
14558
15285
|
const userId = await getDefaultUserId();
|
|
14559
|
-
const rateLimit = checkRateLimit("
|
|
15286
|
+
const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
|
|
14560
15287
|
if (!rateLimit.allowed) {
|
|
14561
15288
|
return {
|
|
14562
15289
|
content: [
|
|
@@ -14628,7 +15355,12 @@ function registerCarouselTools(server) {
|
|
|
14628
15355
|
slideNumber: slide.slideNumber,
|
|
14629
15356
|
jobId: null,
|
|
14630
15357
|
model: image_model,
|
|
14631
|
-
error: error ?? "No job ID returned"
|
|
15358
|
+
error: error ?? "No job ID returned",
|
|
15359
|
+
creditsReserved: data?.credits_reserved ?? null,
|
|
15360
|
+
creditsCharged: data?.credits_charged ?? null,
|
|
15361
|
+
creditsRefunded: data?.credits_refunded ?? null,
|
|
15362
|
+
billingStatus: data?.billing_status ?? "unknown",
|
|
15363
|
+
failureReason: data?.failure_reason ?? null
|
|
14632
15364
|
};
|
|
14633
15365
|
}
|
|
14634
15366
|
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
@@ -14640,14 +15372,24 @@ function registerCarouselTools(server) {
|
|
|
14640
15372
|
slideNumber: slide.slideNumber,
|
|
14641
15373
|
jobId,
|
|
14642
15374
|
model: image_model,
|
|
14643
|
-
error: null
|
|
15375
|
+
error: null,
|
|
15376
|
+
creditsReserved: 0,
|
|
15377
|
+
creditsCharged: data.creditsDeducted ?? perImageCost,
|
|
15378
|
+
creditsRefunded: 0,
|
|
15379
|
+
billingStatus: "charged",
|
|
15380
|
+
failureReason: null
|
|
14644
15381
|
};
|
|
14645
15382
|
} catch (err) {
|
|
14646
15383
|
return {
|
|
14647
15384
|
slideNumber: slide.slideNumber,
|
|
14648
15385
|
jobId: null,
|
|
14649
15386
|
model: image_model,
|
|
14650
|
-
error: sanitizeError(err)
|
|
15387
|
+
error: sanitizeError(err),
|
|
15388
|
+
creditsReserved: null,
|
|
15389
|
+
creditsCharged: null,
|
|
15390
|
+
creditsRefunded: null,
|
|
15391
|
+
billingStatus: "unknown",
|
|
15392
|
+
failureReason: null
|
|
14651
15393
|
};
|
|
14652
15394
|
}
|
|
14653
15395
|
})
|
|
@@ -14672,7 +15414,8 @@ function registerCarouselTools(server) {
|
|
|
14672
15414
|
return {
|
|
14673
15415
|
...s,
|
|
14674
15416
|
imageJobId: job?.jobId ?? null,
|
|
14675
|
-
imageError: job?.error ?? null
|
|
15417
|
+
imageError: job?.error ?? null,
|
|
15418
|
+
imageBillingStatus: job?.billingStatus ?? "unknown"
|
|
14676
15419
|
};
|
|
14677
15420
|
}),
|
|
14678
15421
|
imageModel: image_model,
|
|
@@ -14684,11 +15427,19 @@ function registerCarouselTools(server) {
|
|
|
14684
15427
|
jobIds: successfulJobs.map((j) => j.jobId),
|
|
14685
15428
|
failedSlides: failedJobs.map((j) => ({
|
|
14686
15429
|
slideNumber: j.slideNumber,
|
|
14687
|
-
error: j.error
|
|
15430
|
+
error: j.error,
|
|
15431
|
+
credits_reserved: j.creditsReserved,
|
|
15432
|
+
credits_charged: j.creditsCharged,
|
|
15433
|
+
credits_refunded: j.creditsRefunded,
|
|
15434
|
+
billing_status: j.billingStatus,
|
|
15435
|
+
failure_reason: j.failureReason
|
|
14688
15436
|
})),
|
|
14689
15437
|
credits: {
|
|
14690
15438
|
textGeneration: textCredits,
|
|
14691
15439
|
imagesEstimated: successfulJobs.length * perImageCost,
|
|
15440
|
+
imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
|
|
15441
|
+
imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
|
|
15442
|
+
billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
|
|
14692
15443
|
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
14693
15444
|
}
|
|
14694
15445
|
}
|
|
@@ -14884,7 +15635,7 @@ function registerHyperframesTools(server) {
|
|
|
14884
15635
|
);
|
|
14885
15636
|
server.tool(
|
|
14886
15637
|
"render_hyperframes",
|
|
14887
|
-
"Render an HTML video composition (Hyperframes) to MP4.
|
|
15638
|
+
"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.",
|
|
14888
15639
|
{
|
|
14889
15640
|
composition_html: z30.string().max(5e5).optional().describe(
|
|
14890
15641
|
"Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
|
|
@@ -14897,7 +15648,8 @@ function registerHyperframesTools(server) {
|
|
|
14897
15648
|
duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
|
|
14898
15649
|
fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
|
|
14899
15650
|
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
|
|
14900
|
-
project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
|
|
15651
|
+
project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with."),
|
|
15652
|
+
response_format: z30.enum(["text", "json"]).optional().describe("Response format. Use json for a stable job_id handoff.")
|
|
14901
15653
|
},
|
|
14902
15654
|
async ({
|
|
14903
15655
|
composition_html,
|
|
@@ -14907,7 +15659,8 @@ function registerHyperframesTools(server) {
|
|
|
14907
15659
|
duration_sec,
|
|
14908
15660
|
fps,
|
|
14909
15661
|
quality,
|
|
14910
|
-
project_id
|
|
15662
|
+
project_id,
|
|
15663
|
+
response_format
|
|
14911
15664
|
}) => {
|
|
14912
15665
|
const userId = await getDefaultUserId();
|
|
14913
15666
|
const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
|
|
@@ -14958,11 +15711,23 @@ function registerHyperframesTools(server) {
|
|
|
14958
15711
|
if (error || !data?.jobId) {
|
|
14959
15712
|
throw new Error(error || data?.error || "Failed to create Hyperframes render job");
|
|
14960
15713
|
}
|
|
15714
|
+
const payload = {
|
|
15715
|
+
job_id: data.jobId,
|
|
15716
|
+
jobId: data.jobId,
|
|
15717
|
+
status: data.status,
|
|
15718
|
+
credits_cost: data.creditsCost,
|
|
15719
|
+
credits: data.creditsCost,
|
|
15720
|
+
duration_sec,
|
|
15721
|
+
fps: fps || 30,
|
|
15722
|
+
aspect_ratio: aspect_ratio || "9:16",
|
|
15723
|
+
quality: quality || "standard",
|
|
15724
|
+
project_id: project_id || null
|
|
15725
|
+
};
|
|
14961
15726
|
return {
|
|
14962
15727
|
content: [
|
|
14963
15728
|
{
|
|
14964
15729
|
type: "text",
|
|
14965
|
-
text: [
|
|
15730
|
+
text: response_format === "json" ? JSON.stringify({ data: payload }) : [
|
|
14966
15731
|
`Hyperframes render job queued.`,
|
|
14967
15732
|
` Job ID: ${data.jobId}`,
|
|
14968
15733
|
` Credits: ${data.creditsCost}`,
|
|
@@ -14979,7 +15744,7 @@ function registerHyperframesTools(server) {
|
|
|
14979
15744
|
content: [
|
|
14980
15745
|
{
|
|
14981
15746
|
type: "text",
|
|
14982
|
-
text: `Failed to queue Hyperframes render: ${err
|
|
15747
|
+
text: `Failed to queue Hyperframes render: ${sanitizeError(err)}`
|
|
14983
15748
|
}
|
|
14984
15749
|
],
|
|
14985
15750
|
isError: true
|
|
@@ -14995,6 +15760,7 @@ var init_hyperframes = __esm({
|
|
|
14995
15760
|
init_rate_limit();
|
|
14996
15761
|
init_supabase();
|
|
14997
15762
|
init_edge_function();
|
|
15763
|
+
init_sanitize_error();
|
|
14998
15764
|
HYPERFRAMES_BLOCKS = [
|
|
14999
15765
|
// Transitions
|
|
15000
15766
|
{
|
|
@@ -15112,49 +15878,122 @@ import {
|
|
|
15112
15878
|
import { z as z31 } from "zod";
|
|
15113
15879
|
import fs from "node:fs/promises";
|
|
15114
15880
|
import path from "node:path";
|
|
15881
|
+
import { fileURLToPath } from "node:url";
|
|
15115
15882
|
function startOfCurrentWeekMonday() {
|
|
15116
15883
|
const now = /* @__PURE__ */ new Date();
|
|
15117
|
-
const day = now.
|
|
15884
|
+
const day = now.getUTCDay();
|
|
15118
15885
|
const monday = new Date(now);
|
|
15119
|
-
monday.setUTCDate(now.getUTCDate() - day +
|
|
15886
|
+
monday.setUTCDate(now.getUTCDate() - (day + 6) % 7);
|
|
15887
|
+
monday.setUTCHours(0, 0, 0, 0);
|
|
15120
15888
|
return monday.toISOString().split("T")[0];
|
|
15121
15889
|
}
|
|
15890
|
+
function endOfWeek(startDate) {
|
|
15891
|
+
const end = /* @__PURE__ */ new Date(`${startDate}T00:00:00.000Z`);
|
|
15892
|
+
end.setUTCDate(end.getUTCDate() + 7);
|
|
15893
|
+
end.setUTCMilliseconds(end.getUTCMilliseconds() - 1);
|
|
15894
|
+
return end.toISOString();
|
|
15895
|
+
}
|
|
15896
|
+
function isStrictIsoDate(value) {
|
|
15897
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
15898
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
15899
|
+
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
15900
|
+
}
|
|
15901
|
+
function publicRecentPost(value) {
|
|
15902
|
+
if (!value || typeof value !== "object") return null;
|
|
15903
|
+
const row = value;
|
|
15904
|
+
if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
|
|
15905
|
+
return null;
|
|
15906
|
+
}
|
|
15907
|
+
const optional = (name) => typeof row[name] === "string" ? row[name] : null;
|
|
15908
|
+
return {
|
|
15909
|
+
id: row.id,
|
|
15910
|
+
platform: row.platform,
|
|
15911
|
+
status: row.status,
|
|
15912
|
+
title: optional("title"),
|
|
15913
|
+
external_post_id: optional("external_post_id"),
|
|
15914
|
+
published_at: optional("published_at"),
|
|
15915
|
+
scheduled_at: optional("scheduled_at"),
|
|
15916
|
+
created_at: row.created_at
|
|
15917
|
+
};
|
|
15918
|
+
}
|
|
15919
|
+
function calendarHtmlCandidates() {
|
|
15920
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
15921
|
+
return [
|
|
15922
|
+
// Bundled HTTP entry: import.meta.url is dist/http.js.
|
|
15923
|
+
path.join(moduleDir, "apps/content-calendar/mcp-app.html"),
|
|
15924
|
+
// Source/tsx/vitest: import.meta.url is src/apps/content-calendar.ts.
|
|
15925
|
+
path.resolve(moduleDir, "../../dist/apps/content-calendar/mcp-app.html"),
|
|
15926
|
+
// Local development fallback; never the sole package path.
|
|
15927
|
+
path.resolve(process.cwd(), "dist/apps/content-calendar/mcp-app.html")
|
|
15928
|
+
];
|
|
15929
|
+
}
|
|
15930
|
+
async function readCalendarHtml() {
|
|
15931
|
+
for (const candidate of calendarHtmlCandidates()) {
|
|
15932
|
+
try {
|
|
15933
|
+
return await fs.readFile(candidate, "utf-8");
|
|
15934
|
+
} catch (error) {
|
|
15935
|
+
if (error.code !== "ENOENT") throw error;
|
|
15936
|
+
}
|
|
15937
|
+
}
|
|
15938
|
+
throw new Error("calendar_bundle_missing");
|
|
15939
|
+
}
|
|
15122
15940
|
function registerContentCalendarApp(server) {
|
|
15123
15941
|
registerAppTool(
|
|
15124
15942
|
server,
|
|
15125
15943
|
"open_content_calendar",
|
|
15126
15944
|
{
|
|
15127
15945
|
title: "Content Calendar",
|
|
15128
|
-
description: "Open
|
|
15946
|
+
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.",
|
|
15129
15947
|
inputSchema: {
|
|
15130
|
-
|
|
15948
|
+
project_id: z31.string().uuid().optional().describe(
|
|
15949
|
+
"Brand/project ID. Defaults to the authenticated key's project or the account default."
|
|
15950
|
+
),
|
|
15951
|
+
start_date: z31.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
15131
15952
|
"ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
|
|
15132
15953
|
)
|
|
15133
15954
|
},
|
|
15134
15955
|
outputSchema: {
|
|
15135
15956
|
start_date: z31.string(),
|
|
15957
|
+
project_id: z31.string(),
|
|
15136
15958
|
posts: z31.array(RecentPostOutputSchema),
|
|
15137
15959
|
scopes: z31.array(z31.string())
|
|
15138
15960
|
},
|
|
15139
15961
|
_meta: {
|
|
15140
15962
|
ui: {
|
|
15141
|
-
resourceUri: CALENDAR_URI
|
|
15142
|
-
csp: {
|
|
15143
|
-
"img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
|
|
15144
|
-
"connect-src": ["'self'"]
|
|
15145
|
-
}
|
|
15963
|
+
resourceUri: CALENDAR_URI
|
|
15146
15964
|
}
|
|
15147
15965
|
}
|
|
15148
15966
|
},
|
|
15149
|
-
async ({ start_date }
|
|
15150
|
-
const userScopes =
|
|
15967
|
+
async ({ project_id, start_date }) => {
|
|
15968
|
+
const userScopes = getRequestScopes() ?? getAuthenticatedScopes();
|
|
15969
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId();
|
|
15970
|
+
if (!resolvedProjectId) {
|
|
15971
|
+
return {
|
|
15972
|
+
content: [
|
|
15973
|
+
{
|
|
15974
|
+
type: "text",
|
|
15975
|
+
text: "No project_id was provided and no default project is configured."
|
|
15976
|
+
}
|
|
15977
|
+
],
|
|
15978
|
+
isError: true
|
|
15979
|
+
};
|
|
15980
|
+
}
|
|
15151
15981
|
const fromDate = start_date ?? startOfCurrentWeekMonday();
|
|
15982
|
+
if (!isStrictIsoDate(fromDate)) {
|
|
15983
|
+
return {
|
|
15984
|
+
content: [{ type: "text", text: "start_date must be a valid YYYY-MM-DD date." }],
|
|
15985
|
+
isError: true
|
|
15986
|
+
};
|
|
15987
|
+
}
|
|
15152
15988
|
const { data: result, error } = await callEdgeFunction(
|
|
15153
15989
|
"mcp-data",
|
|
15154
15990
|
{
|
|
15155
|
-
action: "
|
|
15156
|
-
|
|
15157
|
-
|
|
15991
|
+
action: "scheduled-posts",
|
|
15992
|
+
start_date: `${fromDate}T00:00:00.000Z`,
|
|
15993
|
+
end_date: endOfWeek(fromDate),
|
|
15994
|
+
statuses: ["pending", "scheduled", "draft"],
|
|
15995
|
+
projectId: resolvedProjectId,
|
|
15996
|
+
project_id: resolvedProjectId
|
|
15158
15997
|
},
|
|
15159
15998
|
{ timeoutMs: 15e3 }
|
|
15160
15999
|
);
|
|
@@ -15163,19 +16002,16 @@ function registerContentCalendarApp(server) {
|
|
|
15163
16002
|
content: [
|
|
15164
16003
|
{
|
|
15165
16004
|
type: "text",
|
|
15166
|
-
text:
|
|
16005
|
+
text: "The content calendar could not load posts. Please retry."
|
|
15167
16006
|
}
|
|
15168
16007
|
],
|
|
15169
16008
|
isError: true
|
|
15170
16009
|
};
|
|
15171
16010
|
}
|
|
15172
|
-
const posts = (result.posts ?? []).filter((
|
|
15173
|
-
const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
|
|
15174
|
-
if (!ts) return false;
|
|
15175
|
-
return ts.split("T")[0] >= fromDate;
|
|
15176
|
-
});
|
|
16011
|
+
const posts = (result.posts ?? []).map(publicRecentPost).filter((post) => post !== null);
|
|
15177
16012
|
const structuredContent = {
|
|
15178
16013
|
start_date: fromDate,
|
|
16014
|
+
project_id: resolvedProjectId,
|
|
15179
16015
|
posts,
|
|
15180
16016
|
scopes: userScopes
|
|
15181
16017
|
};
|
|
@@ -15194,37 +16030,60 @@ function registerContentCalendarApp(server) {
|
|
|
15194
16030
|
server,
|
|
15195
16031
|
CALENDAR_URI,
|
|
15196
16032
|
CALENDAR_URI,
|
|
15197
|
-
{
|
|
16033
|
+
{
|
|
16034
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
16035
|
+
description: "Self-contained Social Neuron project content calendar.",
|
|
16036
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
16037
|
+
},
|
|
15198
16038
|
async () => {
|
|
15199
|
-
const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
|
|
15200
16039
|
try {
|
|
15201
|
-
const html = await
|
|
16040
|
+
const html = await readCalendarHtml();
|
|
15202
16041
|
return {
|
|
15203
|
-
contents: [
|
|
16042
|
+
contents: [
|
|
16043
|
+
{
|
|
16044
|
+
uri: CALENDAR_URI,
|
|
16045
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
16046
|
+
text: html,
|
|
16047
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
16048
|
+
}
|
|
16049
|
+
]
|
|
15204
16050
|
};
|
|
15205
|
-
} catch
|
|
16051
|
+
} catch {
|
|
15206
16052
|
const errorHtml = `<!DOCTYPE html>
|
|
15207
16053
|
<html><head><title>Content Calendar \u2014 unavailable</title></head>
|
|
15208
16054
|
<body style="font-family:sans-serif;padding:24px;color:#444;">
|
|
15209
16055
|
<h2>Content Calendar app bundle missing</h2>
|
|
15210
|
-
<p>The
|
|
15211
|
-
<code>apps/content-calendar/dist/mcp-app.html</code> is not built.
|
|
15212
|
-
Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
|
|
15213
|
-
<p style="color:#999;font-size:12px;">${err.message}</p>
|
|
16056
|
+
<p>The interactive bundle is unavailable on this deployment. Please contact Social Neuron support.</p>
|
|
15214
16057
|
</body></html>`;
|
|
15215
16058
|
return {
|
|
15216
|
-
contents: [
|
|
16059
|
+
contents: [
|
|
16060
|
+
{
|
|
16061
|
+
uri: CALENDAR_URI,
|
|
16062
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
16063
|
+
text: errorHtml,
|
|
16064
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
16065
|
+
}
|
|
16066
|
+
]
|
|
15217
16067
|
};
|
|
15218
16068
|
}
|
|
15219
16069
|
}
|
|
15220
16070
|
);
|
|
15221
16071
|
}
|
|
15222
|
-
var CALENDAR_URI, RecentPostOutputSchema;
|
|
16072
|
+
var CALENDAR_URI, CALENDAR_CSP, RecentPostOutputSchema;
|
|
15223
16073
|
var init_content_calendar = __esm({
|
|
15224
16074
|
"src/apps/content-calendar.ts"() {
|
|
15225
16075
|
"use strict";
|
|
15226
16076
|
init_edge_function();
|
|
15227
|
-
|
|
16077
|
+
init_request_context();
|
|
16078
|
+
init_supabase();
|
|
16079
|
+
CALENDAR_URI = "ui://content-calendar/v1/mcp-app.html";
|
|
16080
|
+
CALENDAR_CSP = {
|
|
16081
|
+
// The HTML is fully self-contained. Tool calls travel over the host bridge,
|
|
16082
|
+
// not fetch/XHR, so the secure default is no network or remote resources.
|
|
16083
|
+
connectDomains: [],
|
|
16084
|
+
resourceDomains: [],
|
|
16085
|
+
frameDomains: []
|
|
16086
|
+
};
|
|
15228
16087
|
RecentPostOutputSchema = z31.object({
|
|
15229
16088
|
id: z31.string(),
|
|
15230
16089
|
platform: z31.string(),
|
|
@@ -15238,8 +16097,257 @@ var init_content_calendar = __esm({
|
|
|
15238
16097
|
}
|
|
15239
16098
|
});
|
|
15240
16099
|
|
|
15241
|
-
// src/
|
|
16100
|
+
// src/apps/analytics-pulse.ts
|
|
16101
|
+
import {
|
|
16102
|
+
registerAppResource as registerAppResource2,
|
|
16103
|
+
registerAppTool as registerAppTool2,
|
|
16104
|
+
RESOURCE_MIME_TYPE as RESOURCE_MIME_TYPE2
|
|
16105
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
15242
16106
|
import { z as z32 } from "zod";
|
|
16107
|
+
import fs2 from "node:fs/promises";
|
|
16108
|
+
import path2 from "node:path";
|
|
16109
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16110
|
+
function finiteMetric(value) {
|
|
16111
|
+
const parsed = Number(value ?? 0);
|
|
16112
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
16113
|
+
}
|
|
16114
|
+
function publicAnalyticsPost(value) {
|
|
16115
|
+
if (!value || typeof value !== "object") return null;
|
|
16116
|
+
const row = value;
|
|
16117
|
+
const post = row.posts && typeof row.posts === "object" ? row.posts : {};
|
|
16118
|
+
const platform3 = typeof row.platform === "string" ? row.platform : typeof post.platform === "string" ? post.platform : null;
|
|
16119
|
+
const capturedAt = typeof row.captured_at === "string" ? row.captured_at : null;
|
|
16120
|
+
if (!platform3 || !capturedAt) return null;
|
|
16121
|
+
const views = finiteMetric(row.views);
|
|
16122
|
+
const likes = finiteMetric(row.likes);
|
|
16123
|
+
const comments = finiteMetric(row.comments);
|
|
16124
|
+
const shares = finiteMetric(row.shares);
|
|
16125
|
+
const engagement = likes + comments + shares;
|
|
16126
|
+
return {
|
|
16127
|
+
platform: platform3,
|
|
16128
|
+
title: typeof post.title === "string" ? post.title : null,
|
|
16129
|
+
views,
|
|
16130
|
+
likes,
|
|
16131
|
+
comments,
|
|
16132
|
+
shares,
|
|
16133
|
+
engagement_rate: views > 0 ? Number((engagement / views * 100).toFixed(2)) : 0,
|
|
16134
|
+
captured_at: capturedAt,
|
|
16135
|
+
published_at: typeof post.published_at === "string" ? post.published_at : null
|
|
16136
|
+
};
|
|
16137
|
+
}
|
|
16138
|
+
function latestAnalyticsRows(values) {
|
|
16139
|
+
const rows = values.filter((value) => Boolean(value) && typeof value === "object").sort((a, b) => String(b.captured_at ?? "").localeCompare(String(a.captured_at ?? "")));
|
|
16140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16141
|
+
const latest = [];
|
|
16142
|
+
for (const row of rows) {
|
|
16143
|
+
if (typeof row.post_id !== "string" || typeof row.platform !== "string") continue;
|
|
16144
|
+
const key = `${row.post_id}:${row.platform.toLowerCase()}`;
|
|
16145
|
+
if (seen.has(key)) continue;
|
|
16146
|
+
seen.add(key);
|
|
16147
|
+
latest.push(row);
|
|
16148
|
+
}
|
|
16149
|
+
return latest;
|
|
16150
|
+
}
|
|
16151
|
+
function analyticsHtmlCandidates() {
|
|
16152
|
+
const moduleDir = path2.dirname(fileURLToPath2(import.meta.url));
|
|
16153
|
+
return [
|
|
16154
|
+
path2.join(moduleDir, "apps/analytics-pulse/mcp-app.html"),
|
|
16155
|
+
path2.resolve(moduleDir, "../../dist/apps/analytics-pulse/mcp-app.html"),
|
|
16156
|
+
path2.resolve(process.cwd(), "dist/apps/analytics-pulse/mcp-app.html")
|
|
16157
|
+
];
|
|
16158
|
+
}
|
|
16159
|
+
async function readAnalyticsHtml() {
|
|
16160
|
+
for (const candidate of analyticsHtmlCandidates()) {
|
|
16161
|
+
try {
|
|
16162
|
+
return await fs2.readFile(candidate, "utf-8");
|
|
16163
|
+
} catch (error) {
|
|
16164
|
+
if (error.code !== "ENOENT") throw error;
|
|
16165
|
+
}
|
|
16166
|
+
}
|
|
16167
|
+
throw new Error("analytics_bundle_missing");
|
|
16168
|
+
}
|
|
16169
|
+
function registerAnalyticsPulseApp(server) {
|
|
16170
|
+
registerAppTool2(
|
|
16171
|
+
server,
|
|
16172
|
+
"open_analytics_pulse",
|
|
16173
|
+
{
|
|
16174
|
+
title: "Analytics Pulse",
|
|
16175
|
+
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.",
|
|
16176
|
+
inputSchema: {
|
|
16177
|
+
project_id: z32.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default."),
|
|
16178
|
+
platform: z32.enum(PLATFORM_VALUES).optional().describe("Optional platform filter."),
|
|
16179
|
+
days: z32.number().int().min(1).max(365).optional().describe("Lookback window. Defaults to 30 days.")
|
|
16180
|
+
},
|
|
16181
|
+
outputSchema: {
|
|
16182
|
+
project_id: z32.string(),
|
|
16183
|
+
platform: z32.string().nullable(),
|
|
16184
|
+
days: z32.number(),
|
|
16185
|
+
summary: z32.object({
|
|
16186
|
+
views: z32.number(),
|
|
16187
|
+
engagement: z32.number(),
|
|
16188
|
+
engagement_rate: z32.number(),
|
|
16189
|
+
posts: z32.number()
|
|
16190
|
+
}),
|
|
16191
|
+
platform_totals: z32.array(
|
|
16192
|
+
z32.object({
|
|
16193
|
+
platform: z32.string(),
|
|
16194
|
+
views: z32.number(),
|
|
16195
|
+
engagement: z32.number(),
|
|
16196
|
+
posts: z32.number()
|
|
16197
|
+
})
|
|
16198
|
+
),
|
|
16199
|
+
posts: z32.array(AnalyticsPostOutputSchema)
|
|
16200
|
+
},
|
|
16201
|
+
_meta: { ui: { resourceUri: ANALYTICS_URI } }
|
|
16202
|
+
},
|
|
16203
|
+
async ({ project_id, platform: platform3, days }) => {
|
|
16204
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId();
|
|
16205
|
+
if (!resolvedProjectId) {
|
|
16206
|
+
return {
|
|
16207
|
+
content: [{ type: "text", text: "No project_id was provided and no default project is configured." }],
|
|
16208
|
+
isError: true
|
|
16209
|
+
};
|
|
16210
|
+
}
|
|
16211
|
+
const lookbackDays = days ?? 30;
|
|
16212
|
+
const { data: result, error } = await callEdgeFunction(
|
|
16213
|
+
"mcp-data",
|
|
16214
|
+
{
|
|
16215
|
+
action: "analytics",
|
|
16216
|
+
projectId: resolvedProjectId,
|
|
16217
|
+
project_id: resolvedProjectId,
|
|
16218
|
+
days: lookbackDays,
|
|
16219
|
+
// The table stores cumulative snapshots. Request a wider window and
|
|
16220
|
+
// deduplicate below so refresh frequency cannot inflate the totals.
|
|
16221
|
+
limit: 100,
|
|
16222
|
+
latestOnly: true,
|
|
16223
|
+
...platform3 ? { platform: platform3 } : {}
|
|
16224
|
+
},
|
|
16225
|
+
{ timeoutMs: 15e3 }
|
|
16226
|
+
);
|
|
16227
|
+
if (error || !result?.success) {
|
|
16228
|
+
return {
|
|
16229
|
+
content: [{ type: "text", text: "The analytics dashboard could not load data. Please retry." }],
|
|
16230
|
+
isError: true
|
|
16231
|
+
};
|
|
16232
|
+
}
|
|
16233
|
+
const posts = latestAnalyticsRows(result.rows ?? []).map(publicAnalyticsPost).filter((post) => post !== null).sort((a, b) => b.views - a.views);
|
|
16234
|
+
const totals = /* @__PURE__ */ new Map();
|
|
16235
|
+
let totalViews = 0;
|
|
16236
|
+
let totalEngagement = 0;
|
|
16237
|
+
for (const post of posts) {
|
|
16238
|
+
const engagement = post.likes + post.comments + post.shares;
|
|
16239
|
+
totalViews += post.views;
|
|
16240
|
+
totalEngagement += engagement;
|
|
16241
|
+
const aggregate = totals.get(post.platform) ?? {
|
|
16242
|
+
platform: post.platform,
|
|
16243
|
+
views: 0,
|
|
16244
|
+
engagement: 0,
|
|
16245
|
+
posts: 0
|
|
16246
|
+
};
|
|
16247
|
+
aggregate.views += post.views;
|
|
16248
|
+
aggregate.engagement += engagement;
|
|
16249
|
+
aggregate.posts += 1;
|
|
16250
|
+
totals.set(post.platform, aggregate);
|
|
16251
|
+
}
|
|
16252
|
+
const structuredContent = {
|
|
16253
|
+
project_id: resolvedProjectId,
|
|
16254
|
+
platform: platform3 ?? null,
|
|
16255
|
+
days: lookbackDays,
|
|
16256
|
+
summary: {
|
|
16257
|
+
views: totalViews,
|
|
16258
|
+
engagement: totalEngagement,
|
|
16259
|
+
engagement_rate: totalViews > 0 ? Number((totalEngagement / totalViews * 100).toFixed(2)) : 0,
|
|
16260
|
+
posts: posts.length
|
|
16261
|
+
},
|
|
16262
|
+
platform_totals: [...totals.values()].sort((a, b) => b.views - a.views),
|
|
16263
|
+
posts
|
|
16264
|
+
};
|
|
16265
|
+
return {
|
|
16266
|
+
structuredContent,
|
|
16267
|
+
content: [
|
|
16268
|
+
{
|
|
16269
|
+
type: "text",
|
|
16270
|
+
text: `Loaded ${posts.length} analytics record${posts.length === 1 ? "" : "s"} for the last ${lookbackDays} days.`
|
|
16271
|
+
}
|
|
16272
|
+
]
|
|
16273
|
+
};
|
|
16274
|
+
}
|
|
16275
|
+
);
|
|
16276
|
+
registerAppResource2(
|
|
16277
|
+
server,
|
|
16278
|
+
ANALYTICS_URI,
|
|
16279
|
+
ANALYTICS_URI,
|
|
16280
|
+
{
|
|
16281
|
+
mimeType: RESOURCE_MIME_TYPE2,
|
|
16282
|
+
description: "Self-contained Social Neuron project analytics dashboard.",
|
|
16283
|
+
_meta: { ui: { csp: ANALYTICS_CSP } }
|
|
16284
|
+
},
|
|
16285
|
+
async () => {
|
|
16286
|
+
try {
|
|
16287
|
+
const html = await readAnalyticsHtml();
|
|
16288
|
+
return {
|
|
16289
|
+
contents: [
|
|
16290
|
+
{
|
|
16291
|
+
uri: ANALYTICS_URI,
|
|
16292
|
+
mimeType: RESOURCE_MIME_TYPE2,
|
|
16293
|
+
text: html,
|
|
16294
|
+
_meta: { ui: { csp: ANALYTICS_CSP } }
|
|
16295
|
+
}
|
|
16296
|
+
]
|
|
16297
|
+
};
|
|
16298
|
+
} catch {
|
|
16299
|
+
return {
|
|
16300
|
+
contents: [
|
|
16301
|
+
{
|
|
16302
|
+
uri: ANALYTICS_URI,
|
|
16303
|
+
mimeType: RESOURCE_MIME_TYPE2,
|
|
16304
|
+
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>",
|
|
16305
|
+
_meta: { ui: { csp: ANALYTICS_CSP } }
|
|
16306
|
+
}
|
|
16307
|
+
]
|
|
16308
|
+
};
|
|
16309
|
+
}
|
|
16310
|
+
}
|
|
16311
|
+
);
|
|
16312
|
+
}
|
|
16313
|
+
var ANALYTICS_URI, ANALYTICS_CSP, PLATFORM_VALUES, AnalyticsPostOutputSchema;
|
|
16314
|
+
var init_analytics_pulse = __esm({
|
|
16315
|
+
"src/apps/analytics-pulse.ts"() {
|
|
16316
|
+
"use strict";
|
|
16317
|
+
init_edge_function();
|
|
16318
|
+
init_supabase();
|
|
16319
|
+
ANALYTICS_URI = "ui://analytics-pulse/v1/mcp-app.html";
|
|
16320
|
+
ANALYTICS_CSP = {
|
|
16321
|
+
connectDomains: [],
|
|
16322
|
+
resourceDomains: [],
|
|
16323
|
+
frameDomains: []
|
|
16324
|
+
};
|
|
16325
|
+
PLATFORM_VALUES = [
|
|
16326
|
+
"youtube",
|
|
16327
|
+
"tiktok",
|
|
16328
|
+
"instagram",
|
|
16329
|
+
"twitter",
|
|
16330
|
+
"linkedin",
|
|
16331
|
+
"facebook",
|
|
16332
|
+
"threads",
|
|
16333
|
+
"bluesky"
|
|
16334
|
+
];
|
|
16335
|
+
AnalyticsPostOutputSchema = z32.object({
|
|
16336
|
+
platform: z32.string(),
|
|
16337
|
+
title: z32.string().nullable(),
|
|
16338
|
+
views: z32.number(),
|
|
16339
|
+
likes: z32.number(),
|
|
16340
|
+
comments: z32.number(),
|
|
16341
|
+
shares: z32.number(),
|
|
16342
|
+
engagement_rate: z32.number(),
|
|
16343
|
+
captured_at: z32.string(),
|
|
16344
|
+
published_at: z32.string().nullable()
|
|
16345
|
+
});
|
|
16346
|
+
}
|
|
16347
|
+
});
|
|
16348
|
+
|
|
16349
|
+
// src/tools/connections.ts
|
|
16350
|
+
import { z as z33 } from "zod";
|
|
15243
16351
|
function findActiveAccount(accounts, platform3) {
|
|
15244
16352
|
const target = platform3.toLowerCase();
|
|
15245
16353
|
return accounts.find((a) => {
|
|
@@ -15252,16 +16360,16 @@ function registerConnectionTools(server) {
|
|
|
15252
16360
|
"start_platform_connection",
|
|
15253
16361
|
"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.",
|
|
15254
16362
|
{
|
|
15255
|
-
platform:
|
|
15256
|
-
project_id:
|
|
16363
|
+
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
|
|
16364
|
+
project_id: z33.string().optional().describe(
|
|
15257
16365
|
"Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
|
|
15258
16366
|
),
|
|
15259
|
-
response_format:
|
|
16367
|
+
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
15260
16368
|
},
|
|
15261
16369
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
15262
16370
|
const format = response_format ?? "text";
|
|
15263
16371
|
const userId = await getDefaultUserId();
|
|
15264
|
-
const rl = checkRateLimit("
|
|
16372
|
+
const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
|
|
15265
16373
|
if (!rl.allowed) {
|
|
15266
16374
|
return {
|
|
15267
16375
|
content: [
|
|
@@ -15341,13 +16449,13 @@ function registerConnectionTools(server) {
|
|
|
15341
16449
|
"wait_for_connection",
|
|
15342
16450
|
"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.",
|
|
15343
16451
|
{
|
|
15344
|
-
platform:
|
|
15345
|
-
project_id:
|
|
16452
|
+
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
16453
|
+
project_id: z33.string().optional().describe(
|
|
15346
16454
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
15347
16455
|
),
|
|
15348
|
-
timeout_s:
|
|
15349
|
-
poll_interval_s:
|
|
15350
|
-
response_format:
|
|
16456
|
+
timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
|
|
16457
|
+
poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
|
|
16458
|
+
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
15351
16459
|
},
|
|
15352
16460
|
async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
|
|
15353
16461
|
const format = response_format ?? "text";
|
|
@@ -15503,7 +16611,7 @@ var init_connections = __esm({
|
|
|
15503
16611
|
});
|
|
15504
16612
|
|
|
15505
16613
|
// src/tools/harness.ts
|
|
15506
|
-
import { z as
|
|
16614
|
+
import { z as z34 } from "zod";
|
|
15507
16615
|
function registerHarnessTools(server, _ctx) {
|
|
15508
16616
|
server.tool(
|
|
15509
16617
|
"write_agent_reflection",
|
|
@@ -15608,38 +16716,38 @@ var init_harness = __esm({
|
|
|
15608
16716
|
"engager"
|
|
15609
16717
|
];
|
|
15610
16718
|
writeReflectionSchema = {
|
|
15611
|
-
reflection_text:
|
|
15612
|
-
generated_by_agent:
|
|
16719
|
+
reflection_text: z34.string().min(1).max(4e3).describe("The verbal reflection text produced by the agent. 1\u20134000 characters."),
|
|
16720
|
+
generated_by_agent: z34.enum(ALLOWED_AGENTS).describe(
|
|
15613
16721
|
"Which agent produced this reflection. Must be one of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
15614
16722
|
),
|
|
15615
|
-
provenance:
|
|
15616
|
-
content_history_id:
|
|
15617
|
-
outcome_event_id:
|
|
15618
|
-
prm_score_ids:
|
|
15619
|
-
handoff_ids:
|
|
16723
|
+
provenance: z34.object({
|
|
16724
|
+
content_history_id: z34.string().optional().describe("Related content_history row UUID."),
|
|
16725
|
+
outcome_event_id: z34.string().optional().describe("Related outcome event UUID."),
|
|
16726
|
+
prm_score_ids: z34.array(z34.string()).optional().describe("PRM score IDs that informed this reflection."),
|
|
16727
|
+
handoff_ids: z34.array(z34.string()).optional().describe("Handoff event IDs that triggered this reflection.")
|
|
15620
16728
|
}).strict().describe(
|
|
15621
16729
|
"Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
|
|
15622
16730
|
),
|
|
15623
|
-
brand_id:
|
|
15624
|
-
pipeline_id:
|
|
15625
|
-
post_id:
|
|
16731
|
+
brand_id: z34.string().describe("Brand profile UUID this reflection belongs to."),
|
|
16732
|
+
pipeline_id: z34.string().optional().describe("Optional: pipeline run UUID."),
|
|
16733
|
+
post_id: z34.string().optional().describe("Optional: post UUID if reflection targets a post.")
|
|
15626
16734
|
};
|
|
15627
16735
|
readReflectionSchema = {
|
|
15628
|
-
brand_id:
|
|
15629
|
-
generated_by_agent:
|
|
16736
|
+
brand_id: z34.string().describe("Brand profile UUID to read reflections for."),
|
|
16737
|
+
generated_by_agent: z34.enum(ALLOWED_AGENTS).optional().describe(
|
|
15630
16738
|
"Optional filter: only return reflections produced by this agent. One of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
15631
16739
|
),
|
|
15632
|
-
limit:
|
|
16740
|
+
limit: z34.number().int().min(1).max(100).optional().describe(
|
|
15633
16741
|
"Maximum number of reflections to return. Clamped to [1, 100]. Default: 50. Results are ordered by created_at DESC, then id ASC (deterministic tiebreak)."
|
|
15634
16742
|
)
|
|
15635
16743
|
};
|
|
15636
16744
|
recordOutcomeSchema = {
|
|
15637
|
-
decision_event_id:
|
|
15638
|
-
horizon:
|
|
16745
|
+
decision_event_id: z34.string().describe("UUID of the decision_events row to record an outcome for."),
|
|
16746
|
+
horizon: z34.enum(["1h", "6h", "24h"]).describe(
|
|
15639
16747
|
"Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
|
|
15640
16748
|
),
|
|
15641
|
-
reward:
|
|
15642
|
-
outcome_metrics:
|
|
16749
|
+
reward: z34.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
|
|
16750
|
+
outcome_metrics: z34.record(z34.string(), z34.number()).optional().describe(
|
|
15643
16751
|
'Optional map of raw metric name \u2192 value (e.g. {"likes": 42, "reach": 1200}). Stored verbatim; not used for learning directly.'
|
|
15644
16752
|
)
|
|
15645
16753
|
};
|
|
@@ -15647,7 +16755,7 @@ var init_harness = __esm({
|
|
|
15647
16755
|
});
|
|
15648
16756
|
|
|
15649
16757
|
// src/tools/hermes.ts
|
|
15650
|
-
import { z as
|
|
16758
|
+
import { z as z35 } from "zod";
|
|
15651
16759
|
function asEnvelope25(data) {
|
|
15652
16760
|
return {
|
|
15653
16761
|
_meta: {
|
|
@@ -15663,12 +16771,12 @@ function registerHermesTools(server) {
|
|
|
15663
16771
|
"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.",
|
|
15664
16772
|
{
|
|
15665
16773
|
platform: PLATFORM.describe("Target platform for the draft."),
|
|
15666
|
-
copy:
|
|
15667
|
-
project_id:
|
|
15668
|
-
media_url:
|
|
15669
|
-
hermes_run_id:
|
|
15670
|
-
source_intel_ids:
|
|
15671
|
-
response_format:
|
|
16774
|
+
copy: z35.string().min(1).max(8e3).describe("The draft post body."),
|
|
16775
|
+
project_id: z35.string().optional().describe("SN project UUID. Optional but recommended."),
|
|
16776
|
+
media_url: z35.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
|
|
16777
|
+
hermes_run_id: z35.string().optional().describe("Agent run id, for traceability."),
|
|
16778
|
+
source_intel_ids: z35.array(z35.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
|
|
16779
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15672
16780
|
},
|
|
15673
16781
|
async ({ platform: platform3, copy, project_id, media_url, hermes_run_id, response_format }) => {
|
|
15674
16782
|
const { data, error } = await callEdgeFunction(
|
|
@@ -15706,15 +16814,15 @@ function registerHermesTools(server) {
|
|
|
15706
16814
|
"record_voice_lesson",
|
|
15707
16815
|
'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)".',
|
|
15708
16816
|
{
|
|
15709
|
-
project_id:
|
|
15710
|
-
lesson:
|
|
15711
|
-
evidence:
|
|
15712
|
-
engagement_lift_pct:
|
|
15713
|
-
sample_size:
|
|
15714
|
-
top_examples:
|
|
16817
|
+
project_id: z35.string().describe("SN project UUID."),
|
|
16818
|
+
lesson: z35.string().min(1).max(500).describe('One-sentence rule (e.g. "lowercase IG hooks beat title-case").'),
|
|
16819
|
+
evidence: z35.object({
|
|
16820
|
+
engagement_lift_pct: z35.number().describe("Engagement lift vs baseline, in percentage points."),
|
|
16821
|
+
sample_size: z35.number().int().min(1).describe("Number of posts behind this lesson."),
|
|
16822
|
+
top_examples: z35.array(z35.string()).optional().describe("Up to 3 hook examples from the top quartile.")
|
|
15715
16823
|
}).describe("Quantitative evidence backing the lesson."),
|
|
15716
|
-
applies_to:
|
|
15717
|
-
response_format:
|
|
16824
|
+
applies_to: z35.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
|
|
16825
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15718
16826
|
},
|
|
15719
16827
|
async ({ project_id, lesson, evidence, applies_to, response_format }) => {
|
|
15720
16828
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -15750,10 +16858,10 @@ function registerHermesTools(server) {
|
|
|
15750
16858
|
"record_observation",
|
|
15751
16859
|
'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.',
|
|
15752
16860
|
{
|
|
15753
|
-
summary:
|
|
15754
|
-
deltas:
|
|
15755
|
-
run_id:
|
|
15756
|
-
response_format:
|
|
16861
|
+
summary: z35.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
|
|
16862
|
+
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})."),
|
|
16863
|
+
run_id: z35.string().optional().describe("Agent run id for traceability."),
|
|
16864
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15757
16865
|
},
|
|
15758
16866
|
async ({ summary, deltas, run_id, response_format }) => {
|
|
15759
16867
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "record-observation", summary, deltas, run_id });
|
|
@@ -15780,13 +16888,13 @@ function registerHermesTools(server) {
|
|
|
15780
16888
|
"record_intel_signal",
|
|
15781
16889
|
"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.",
|
|
15782
16890
|
{
|
|
15783
|
-
source:
|
|
15784
|
-
url:
|
|
15785
|
-
topic:
|
|
15786
|
-
title:
|
|
15787
|
-
summary:
|
|
15788
|
-
score:
|
|
15789
|
-
response_format:
|
|
16891
|
+
source: z35.string().min(1).max(100).describe('Watcher name (e.g. "news-watch", "hackernews-watch").'),
|
|
16892
|
+
url: z35.string().url().max(2e3).describe("Canonical URL of the source. Used for dedupe."),
|
|
16893
|
+
topic: z35.string().max(200).optional().describe("Best-fit topic key."),
|
|
16894
|
+
title: z35.string().max(500).optional(),
|
|
16895
|
+
summary: z35.string().max(4e3).optional().describe("One-paragraph summary of the signal."),
|
|
16896
|
+
score: z35.number().optional().describe("Relevance score from the watcher (0\u201310 typical)."),
|
|
16897
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15790
16898
|
},
|
|
15791
16899
|
async ({ source, url, topic, title, summary, score, response_format }) => {
|
|
15792
16900
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -15824,16 +16932,16 @@ function registerHermesTools(server) {
|
|
|
15824
16932
|
"record_campaign_spend",
|
|
15825
16933
|
"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.",
|
|
15826
16934
|
{
|
|
15827
|
-
campaign_id:
|
|
15828
|
-
category:
|
|
16935
|
+
campaign_id: z35.string().min(1).max(200).describe("Campaign identifier (slug)."),
|
|
16936
|
+
category: z35.enum([
|
|
15829
16937
|
"hermes_drafts",
|
|
15830
16938
|
"carousel_renders",
|
|
15831
16939
|
"analytics_pulls",
|
|
15832
16940
|
"paid_amplification",
|
|
15833
16941
|
"other"
|
|
15834
16942
|
]).describe("Cost category."),
|
|
15835
|
-
amount_usd:
|
|
15836
|
-
response_format:
|
|
16943
|
+
amount_usd: z35.number().min(0).describe("Amount in USD; supports 4 decimal places."),
|
|
16944
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15837
16945
|
},
|
|
15838
16946
|
async ({ campaign_id, category, amount_usd, response_format }) => {
|
|
15839
16947
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -15868,7 +16976,7 @@ function registerHermesTools(server) {
|
|
|
15868
16976
|
"get_active_campaigns",
|
|
15869
16977
|
"List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
|
|
15870
16978
|
{
|
|
15871
|
-
response_format:
|
|
16979
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15872
16980
|
},
|
|
15873
16981
|
async ({ response_format }) => {
|
|
15874
16982
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "get-active-campaigns" });
|
|
@@ -15917,7 +17025,7 @@ var init_hermes = __esm({
|
|
|
15917
17025
|
"use strict";
|
|
15918
17026
|
init_edge_function();
|
|
15919
17027
|
init_version();
|
|
15920
|
-
PLATFORM =
|
|
17028
|
+
PLATFORM = z35.enum([
|
|
15921
17029
|
"instagram",
|
|
15922
17030
|
"twitter",
|
|
15923
17031
|
"linkedin",
|
|
@@ -15963,7 +17071,20 @@ var init_skills_manifest = __esm({
|
|
|
15963
17071
|
});
|
|
15964
17072
|
|
|
15965
17073
|
// src/tools/skills.ts
|
|
15966
|
-
import { z as
|
|
17074
|
+
import { z as z36 } from "zod";
|
|
17075
|
+
function renderCatalogRow(row) {
|
|
17076
|
+
const lines = [];
|
|
17077
|
+
const platform3 = row.platform ? ` \xB7 ${row.platform}` : "";
|
|
17078
|
+
lines.push(`${row.slug} (${row.kind}${platform3})`);
|
|
17079
|
+
const desc = row.frontmatter && typeof row.frontmatter.description === "string" ? row.frontmatter.description : null;
|
|
17080
|
+
if (desc) lines.push(` ${desc}`);
|
|
17081
|
+
const lock = row.locked ? ` \u{1F512} (upgrade to unlock)` : "";
|
|
17082
|
+
lines.push(` Tier: ${row.tier_minimum}${lock}`);
|
|
17083
|
+
lines.push(
|
|
17084
|
+
` Model: ${row.model_id ?? "n/a"} \xB7 ${row.body_chars} chars` + (row.updated_at ? ` \xB7 updated ${row.updated_at}` : "")
|
|
17085
|
+
);
|
|
17086
|
+
return lines.join("\n");
|
|
17087
|
+
}
|
|
15967
17088
|
function asEnvelope26(data) {
|
|
15968
17089
|
return {
|
|
15969
17090
|
_meta: {
|
|
@@ -15991,19 +17112,88 @@ function registerSkillsTools(server) {
|
|
|
15991
17112
|
"list_skills",
|
|
15992
17113
|
'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.',
|
|
15993
17114
|
{
|
|
15994
|
-
studio:
|
|
15995
|
-
|
|
15996
|
-
|
|
17115
|
+
studio: z36.enum(STUDIO_VALUES).optional().describe(
|
|
17116
|
+
"Filter to one studio (video, avatar, carousel, voice, caption, edit)."
|
|
17117
|
+
),
|
|
17118
|
+
featured_only: z36.boolean().optional().describe(
|
|
17119
|
+
"Return only featured (recommended) skills. Defaults to false."
|
|
17120
|
+
),
|
|
17121
|
+
response_format: z36.enum(["text", "json"]).optional().describe(
|
|
17122
|
+
"Response format. Defaults to text \u2014 human-readable summary."
|
|
17123
|
+
)
|
|
15997
17124
|
},
|
|
15998
17125
|
async ({ studio, featured_only, response_format }) => {
|
|
15999
|
-
const skills = listSkills({ studio, featuredOnly: featured_only });
|
|
16000
17126
|
const format = response_format ?? "text";
|
|
17127
|
+
const { data: efData, error: efError } = await callEdgeFunction("mcp-data", { action: "get-skills" });
|
|
17128
|
+
const catalogRows = efData?.skills;
|
|
17129
|
+
if (!efError && Array.isArray(catalogRows) && catalogRows.length > 0) {
|
|
17130
|
+
const workflows = listSkills({ studio, featuredOnly: featured_only });
|
|
17131
|
+
const filteredGuideRows = catalogRows.filter(
|
|
17132
|
+
(row) => !studio && (!featured_only || row.frontmatter?.featured === true)
|
|
17133
|
+
);
|
|
17134
|
+
const guides = filteredGuideRows.map((row) => ({
|
|
17135
|
+
...row,
|
|
17136
|
+
use_with: "get_skill"
|
|
17137
|
+
}));
|
|
17138
|
+
const total = guides.length + workflows.length;
|
|
17139
|
+
if (format === "json") {
|
|
17140
|
+
return {
|
|
17141
|
+
content: [
|
|
17142
|
+
{
|
|
17143
|
+
type: "text",
|
|
17144
|
+
text: JSON.stringify(
|
|
17145
|
+
asEnvelope26({
|
|
17146
|
+
count: total,
|
|
17147
|
+
guides,
|
|
17148
|
+
workflows: workflows.map((w) => ({
|
|
17149
|
+
...w,
|
|
17150
|
+
use_with: "run_skill"
|
|
17151
|
+
}))
|
|
17152
|
+
}),
|
|
17153
|
+
null,
|
|
17154
|
+
2
|
|
17155
|
+
)
|
|
17156
|
+
}
|
|
17157
|
+
]
|
|
17158
|
+
};
|
|
17159
|
+
}
|
|
17160
|
+
const guideBlocks = filteredGuideRows.map(
|
|
17161
|
+
(row) => `${renderCatalogRow(row)}
|
|
17162
|
+
\u2192 Read it: get_skill(slug: "${row.slug}")`
|
|
17163
|
+
).join("\n\n");
|
|
17164
|
+
const workflowBlocks = workflows.map(renderSkillSummary).join("\n\n");
|
|
17165
|
+
const header2 = `${total} skill${total === 1 ? "" : "s"} available (${guides.length} guide${guides.length === 1 ? "" : "s"} \xB7 ${workflows.length} workflow${workflows.length === 1 ? "" : "s"})
|
|
17166
|
+
${"=".repeat(40)}`;
|
|
17167
|
+
const sections = [
|
|
17168
|
+
guides.length > 0 ? `GUIDES \u2014 living how-to documents. Fetch with get_skill(slug).
|
|
17169
|
+
|
|
17170
|
+
${guideBlocks}` : "",
|
|
17171
|
+
workflows.length > 0 ? `WORKFLOWS \u2014 executable content pipelines. Launch with run_skill(skill_id).
|
|
17172
|
+
|
|
17173
|
+
${workflowBlocks}` : ""
|
|
17174
|
+
].filter(Boolean);
|
|
17175
|
+
return {
|
|
17176
|
+
content: [
|
|
17177
|
+
{
|
|
17178
|
+
type: "text",
|
|
17179
|
+
text: `${header2}
|
|
17180
|
+
|
|
17181
|
+
${sections.join("\n\n")}`
|
|
17182
|
+
}
|
|
17183
|
+
]
|
|
17184
|
+
};
|
|
17185
|
+
}
|
|
17186
|
+
const skills = listSkills({ studio, featuredOnly: featured_only });
|
|
16001
17187
|
if (format === "json") {
|
|
16002
17188
|
return {
|
|
16003
17189
|
content: [
|
|
16004
17190
|
{
|
|
16005
17191
|
type: "text",
|
|
16006
|
-
text: JSON.stringify(
|
|
17192
|
+
text: JSON.stringify(
|
|
17193
|
+
asEnvelope26({ count: skills.length, skills }),
|
|
17194
|
+
null,
|
|
17195
|
+
2
|
|
17196
|
+
)
|
|
16007
17197
|
}
|
|
16008
17198
|
]
|
|
16009
17199
|
};
|
|
@@ -16013,7 +17203,9 @@ function registerSkillsTools(server) {
|
|
|
16013
17203
|
content: [
|
|
16014
17204
|
{
|
|
16015
17205
|
type: "text",
|
|
16016
|
-
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(
|
|
17206
|
+
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(
|
|
17207
|
+
", "
|
|
17208
|
+
)
|
|
16017
17209
|
}
|
|
16018
17210
|
]
|
|
16019
17211
|
};
|
|
@@ -16032,18 +17224,22 @@ ${blocks}` }]
|
|
|
16032
17224
|
"run_skill",
|
|
16033
17225
|
"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.",
|
|
16034
17226
|
{
|
|
16035
|
-
skill_id:
|
|
17227
|
+
skill_id: z36.string().describe(
|
|
16036
17228
|
'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
|
|
16037
17229
|
),
|
|
16038
|
-
topic:
|
|
16039
|
-
|
|
17230
|
+
topic: z36.string().min(1).describe(
|
|
17231
|
+
'What the content is about (e.g. "why we built Social Neuron").'
|
|
17232
|
+
),
|
|
17233
|
+
audience: z36.string().optional().describe(
|
|
16040
17234
|
'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
|
|
16041
17235
|
),
|
|
16042
|
-
hook:
|
|
17236
|
+
hook: z36.string().optional().describe(
|
|
16043
17237
|
"Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
|
|
16044
17238
|
),
|
|
16045
|
-
cta:
|
|
16046
|
-
|
|
17239
|
+
cta: z36.string().optional().describe(
|
|
17240
|
+
"Optional call-to-action override. Defaults to brand standard CTA."
|
|
17241
|
+
),
|
|
17242
|
+
response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
16047
17243
|
},
|
|
16048
17244
|
async ({ skill_id, topic, audience, hook, cta, response_format }) => {
|
|
16049
17245
|
const skill = getSkill(skill_id);
|
|
@@ -16111,19 +17307,96 @@ ${blocks}` }]
|
|
|
16111
17307
|
};
|
|
16112
17308
|
}
|
|
16113
17309
|
);
|
|
17310
|
+
server.tool(
|
|
17311
|
+
"get_skill",
|
|
17312
|
+
`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).`,
|
|
17313
|
+
{
|
|
17314
|
+
slug: z36.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9-]*$/).describe(
|
|
17315
|
+
'The skill slug (e.g. "tiktok-content"). Use list_skills to discover slugs.'
|
|
17316
|
+
),
|
|
17317
|
+
response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
17318
|
+
},
|
|
17319
|
+
async ({ slug, response_format }) => {
|
|
17320
|
+
const format = response_format ?? "text";
|
|
17321
|
+
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
17322
|
+
action: "get-skill",
|
|
17323
|
+
slug
|
|
17324
|
+
});
|
|
17325
|
+
if (error) {
|
|
17326
|
+
return toolError(
|
|
17327
|
+
"upstream_error",
|
|
17328
|
+
"The skill catalogue could not be loaded. Please retry."
|
|
17329
|
+
);
|
|
17330
|
+
}
|
|
17331
|
+
const skill = data?.skill;
|
|
17332
|
+
if (!skill) {
|
|
17333
|
+
return toolError("not_found", `No skill found with slug "${slug}".`, {
|
|
17334
|
+
recover_with: ["Call list_skills to see available skills."]
|
|
17335
|
+
});
|
|
17336
|
+
}
|
|
17337
|
+
if (skill.locked) {
|
|
17338
|
+
return toolError(
|
|
17339
|
+
"permission_denied",
|
|
17340
|
+
"This skill requires a higher plan tier.",
|
|
17341
|
+
{
|
|
17342
|
+
details: { slug: skill.slug, tier_minimum: skill.tier_minimum },
|
|
17343
|
+
recover_with: [
|
|
17344
|
+
"Upgrade the account or choose an unlocked skill from list_skills."
|
|
17345
|
+
]
|
|
17346
|
+
}
|
|
17347
|
+
);
|
|
17348
|
+
}
|
|
17349
|
+
if (format === "json") {
|
|
17350
|
+
return {
|
|
17351
|
+
content: [
|
|
17352
|
+
{
|
|
17353
|
+
type: "text",
|
|
17354
|
+
text: JSON.stringify(asEnvelope26(skill), null, 2)
|
|
17355
|
+
}
|
|
17356
|
+
]
|
|
17357
|
+
};
|
|
17358
|
+
}
|
|
17359
|
+
const platform3 = skill.platform ? ` \xB7 ${skill.platform}` : "";
|
|
17360
|
+
const lock = skill.locked ? " \u{1F512} (upgrade to unlock)" : "";
|
|
17361
|
+
const header = [
|
|
17362
|
+
`${skill.slug} (${skill.kind}${platform3})`,
|
|
17363
|
+
"=".repeat(40),
|
|
17364
|
+
`Tier: ${skill.tier_minimum}${lock} \xB7 version ${skill.version}` + (skill.recipe_slug ? ` \xB7 recipe: ${skill.recipe_slug}` : ""),
|
|
17365
|
+
""
|
|
17366
|
+
].join("\n");
|
|
17367
|
+
const compiled = skill.compiled_section ? `
|
|
17368
|
+
|
|
17369
|
+
--- What's working now ---
|
|
17370
|
+
${skill.compiled_section}` : "";
|
|
17371
|
+
return {
|
|
17372
|
+
content: [
|
|
17373
|
+
{ type: "text", text: `${header}${skill.body}${compiled}` }
|
|
17374
|
+
]
|
|
17375
|
+
};
|
|
17376
|
+
}
|
|
17377
|
+
);
|
|
16114
17378
|
}
|
|
16115
17379
|
var STUDIO_VALUES;
|
|
16116
17380
|
var init_skills = __esm({
|
|
16117
17381
|
"src/tools/skills.ts"() {
|
|
16118
17382
|
"use strict";
|
|
16119
17383
|
init_version();
|
|
17384
|
+
init_tool_error();
|
|
17385
|
+
init_edge_function();
|
|
16120
17386
|
init_skills_manifest();
|
|
16121
|
-
STUDIO_VALUES = [
|
|
17387
|
+
STUDIO_VALUES = [
|
|
17388
|
+
"video",
|
|
17389
|
+
"avatar",
|
|
17390
|
+
"carousel",
|
|
17391
|
+
"voice",
|
|
17392
|
+
"caption",
|
|
17393
|
+
"edit"
|
|
17394
|
+
];
|
|
16122
17395
|
}
|
|
16123
17396
|
});
|
|
16124
17397
|
|
|
16125
17398
|
// src/tools/loopPulse.ts
|
|
16126
|
-
import { z as
|
|
17399
|
+
import { z as z37 } from "zod";
|
|
16127
17400
|
function asEnvelope27(data) {
|
|
16128
17401
|
return {
|
|
16129
17402
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -16135,7 +17408,7 @@ function registerLoopPulseTools(server) {
|
|
|
16135
17408
|
"get_loop_pulse",
|
|
16136
17409
|
'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.',
|
|
16137
17410
|
{
|
|
16138
|
-
response_format:
|
|
17411
|
+
response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
16139
17412
|
},
|
|
16140
17413
|
async ({ response_format }) => {
|
|
16141
17414
|
const format = response_format ?? "text";
|
|
@@ -16185,7 +17458,7 @@ var init_loopPulse = __esm({
|
|
|
16185
17458
|
});
|
|
16186
17459
|
|
|
16187
17460
|
// src/tools/banditState.ts
|
|
16188
|
-
import { z as
|
|
17461
|
+
import { z as z38 } from "zod";
|
|
16189
17462
|
function asEnvelope28(data) {
|
|
16190
17463
|
return {
|
|
16191
17464
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -16197,8 +17470,8 @@ function registerBanditStateTools(server) {
|
|
|
16197
17470
|
"get_bandit_state",
|
|
16198
17471
|
"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).",
|
|
16199
17472
|
{
|
|
16200
|
-
project_id:
|
|
16201
|
-
platform:
|
|
17473
|
+
project_id: z38.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
|
|
17474
|
+
platform: z38.enum([
|
|
16202
17475
|
"instagram",
|
|
16203
17476
|
"tiktok",
|
|
16204
17477
|
"youtube",
|
|
@@ -16210,7 +17483,7 @@ function registerBanditStateTools(server) {
|
|
|
16210
17483
|
]).optional().describe(
|
|
16211
17484
|
"Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
|
|
16212
17485
|
),
|
|
16213
|
-
arm_type:
|
|
17486
|
+
arm_type: z38.enum([
|
|
16214
17487
|
"hook_family",
|
|
16215
17488
|
"hook_type",
|
|
16216
17489
|
"format",
|
|
@@ -16224,8 +17497,8 @@ function registerBanditStateTools(server) {
|
|
|
16224
17497
|
"posting_time_bucket",
|
|
16225
17498
|
"content_format"
|
|
16226
17499
|
]).optional().describe("Arm dimension. Omit to return all types grouped."),
|
|
16227
|
-
top_k:
|
|
16228
|
-
response_format:
|
|
17500
|
+
top_k: z38.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
|
|
17501
|
+
response_format: z38.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
16229
17502
|
},
|
|
16230
17503
|
async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
|
|
16231
17504
|
const format = response_format ?? "text";
|
|
@@ -16288,12 +17561,134 @@ var init_banditState = __esm({
|
|
|
16288
17561
|
}
|
|
16289
17562
|
});
|
|
16290
17563
|
|
|
17564
|
+
// src/tools/lifecycle.ts
|
|
17565
|
+
import { z as z39 } from "zod";
|
|
17566
|
+
function envelope(data) {
|
|
17567
|
+
return {
|
|
17568
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
17569
|
+
data
|
|
17570
|
+
};
|
|
17571
|
+
}
|
|
17572
|
+
function lifecycleFailure(error) {
|
|
17573
|
+
if (/not_found|not found/i.test(error)) {
|
|
17574
|
+
return toolError("not_found", "The requested object was not found in this project.");
|
|
17575
|
+
}
|
|
17576
|
+
if (/not_cancellable|publishing_in_progress|schedule_conflict|post_in_progress/i.test(error)) {
|
|
17577
|
+
return toolError("validation_error", "The object can no longer be cancelled in its current state.", {
|
|
17578
|
+
recover_with: ["Refresh its status before deciding the next action."]
|
|
17579
|
+
});
|
|
17580
|
+
}
|
|
17581
|
+
return toolError("upstream_error", "The lifecycle operation could not be completed. Please retry.");
|
|
17582
|
+
}
|
|
17583
|
+
async function projectContext(projectId) {
|
|
17584
|
+
return projectId ?? await getDefaultProjectId();
|
|
17585
|
+
}
|
|
17586
|
+
async function invokeLifecycle(action, projectId, identifiers) {
|
|
17587
|
+
const resolvedProjectId = await projectContext(projectId);
|
|
17588
|
+
if (!resolvedProjectId) {
|
|
17589
|
+
return toolError("validation_error", "A project_id is required because no default project is configured.");
|
|
17590
|
+
}
|
|
17591
|
+
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
17592
|
+
action,
|
|
17593
|
+
projectId: resolvedProjectId,
|
|
17594
|
+
project_id: resolvedProjectId,
|
|
17595
|
+
...identifiers
|
|
17596
|
+
});
|
|
17597
|
+
if (error) return lifecycleFailure(error);
|
|
17598
|
+
if (!data?.success) return toolError("upstream_error", "The lifecycle operation returned no result.");
|
|
17599
|
+
const result = envelope(data);
|
|
17600
|
+
return {
|
|
17601
|
+
structuredContent: result,
|
|
17602
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
17603
|
+
};
|
|
17604
|
+
}
|
|
17605
|
+
function registerLifecycleTools(server) {
|
|
17606
|
+
server.tool(
|
|
17607
|
+
"cancel_async_job",
|
|
17608
|
+
"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.",
|
|
17609
|
+
{
|
|
17610
|
+
job_id: z39.string().uuid().describe("Owned async_jobs ID returned by a generation tool."),
|
|
17611
|
+
project_id: PROJECT_ID,
|
|
17612
|
+
confirm: CONFIRM
|
|
17613
|
+
},
|
|
17614
|
+
async ({ job_id, project_id }) => invokeLifecycle("cancel-async-job", project_id, { job_id })
|
|
17615
|
+
);
|
|
17616
|
+
server.tool(
|
|
17617
|
+
"cancel_scheduled_post",
|
|
17618
|
+
"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.",
|
|
17619
|
+
{
|
|
17620
|
+
post_id: z39.string().uuid().describe("Owned scheduled post ID."),
|
|
17621
|
+
project_id: PROJECT_ID,
|
|
17622
|
+
confirm: CONFIRM
|
|
17623
|
+
},
|
|
17624
|
+
async ({ post_id, project_id }) => invokeLifecycle("cancel-scheduled-post", project_id, { post_id })
|
|
17625
|
+
);
|
|
17626
|
+
server.tool(
|
|
17627
|
+
"delete_carousel",
|
|
17628
|
+
"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.",
|
|
17629
|
+
{
|
|
17630
|
+
content_id: z39.string().uuid().describe("Owned carousel content_history ID."),
|
|
17631
|
+
project_id: PROJECT_ID,
|
|
17632
|
+
confirm: CONFIRM
|
|
17633
|
+
},
|
|
17634
|
+
async ({ content_id, project_id }) => invokeLifecycle("delete-carousel", project_id, { content_id })
|
|
17635
|
+
);
|
|
17636
|
+
server.tool(
|
|
17637
|
+
"delete_content_plan",
|
|
17638
|
+
"Permanently delete an owned content plan in one project. This does not cancel posts that were already scheduled from the plan.",
|
|
17639
|
+
{
|
|
17640
|
+
plan_id: z39.string().uuid().describe("Owned content plan ID."),
|
|
17641
|
+
project_id: PROJECT_ID,
|
|
17642
|
+
confirm: CONFIRM
|
|
17643
|
+
},
|
|
17644
|
+
async ({ plan_id, project_id }) => invokeLifecycle("delete-content-plan", project_id, { plan_id })
|
|
17645
|
+
);
|
|
17646
|
+
server.tool(
|
|
17647
|
+
"delete_autopilot_config",
|
|
17648
|
+
"Permanently delete an owned autopilot configuration in one project. Historical runs and already-published posts are retained.",
|
|
17649
|
+
{
|
|
17650
|
+
config_id: z39.string().uuid().describe("Owned autopilot configuration ID."),
|
|
17651
|
+
project_id: PROJECT_ID,
|
|
17652
|
+
confirm: CONFIRM
|
|
17653
|
+
},
|
|
17654
|
+
async ({ config_id, project_id }) => invokeLifecycle("delete-autopilot-config", project_id, { config_id })
|
|
17655
|
+
);
|
|
17656
|
+
}
|
|
17657
|
+
var PROJECT_ID, CONFIRM;
|
|
17658
|
+
var init_lifecycle = __esm({
|
|
17659
|
+
"src/tools/lifecycle.ts"() {
|
|
17660
|
+
"use strict";
|
|
17661
|
+
init_edge_function();
|
|
17662
|
+
init_supabase();
|
|
17663
|
+
init_tool_error();
|
|
17664
|
+
init_version();
|
|
17665
|
+
PROJECT_ID = z39.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default.");
|
|
17666
|
+
CONFIRM = z39.literal(true).describe("Must be true only after the user explicitly confirms this destructive action.");
|
|
17667
|
+
}
|
|
17668
|
+
});
|
|
17669
|
+
|
|
16291
17670
|
// src/lib/register-tools.ts
|
|
17671
|
+
function redactBase64Payloads(args) {
|
|
17672
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return args;
|
|
17673
|
+
let changed = false;
|
|
17674
|
+
const out = { ...args };
|
|
17675
|
+
for (const key of BASE64_PAYLOAD_KEYS) {
|
|
17676
|
+
const value = out[key];
|
|
17677
|
+
if (typeof value !== "string" || value.length < 64) continue;
|
|
17678
|
+
const body = value.replace(DATA_URI_PREFIX2, "");
|
|
17679
|
+
if (STRICT_BASE64.test(body)) {
|
|
17680
|
+
out[key] = `[base64:${body.length} chars omitted from prose scan]`;
|
|
17681
|
+
changed = true;
|
|
17682
|
+
}
|
|
17683
|
+
}
|
|
17684
|
+
return changed ? out : args;
|
|
17685
|
+
}
|
|
16292
17686
|
function wrapToolWithScanner(toolName, handler) {
|
|
16293
17687
|
return async function scannerWrappedHandler(...handlerArgs) {
|
|
16294
17688
|
const args = handlerArgs[0];
|
|
16295
17689
|
const ctx = handlerArgs[1];
|
|
16296
|
-
const
|
|
17690
|
+
const scanArgs = redactBase64Payloads(args);
|
|
17691
|
+
const inputText = scanArgs === void 0 ? "{}" : typeof scanArgs === "string" ? scanArgs : JSON.stringify(scanArgs);
|
|
16297
17692
|
const inputScan = scan(inputText, {
|
|
16298
17693
|
mode: "block",
|
|
16299
17694
|
source: "mcp_tool_input",
|
|
@@ -16321,6 +17716,16 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
16321
17716
|
source: "mcp_tool_output",
|
|
16322
17717
|
user_id: ctx?.userId
|
|
16323
17718
|
});
|
|
17719
|
+
if (!outputScan.passed) {
|
|
17720
|
+
try {
|
|
17721
|
+
ctx?.logScan?.(toolName, "output", outputScan);
|
|
17722
|
+
} catch {
|
|
17723
|
+
}
|
|
17724
|
+
return toolError(
|
|
17725
|
+
"server_error",
|
|
17726
|
+
"The response exceeded the safe output limit and was not returned."
|
|
17727
|
+
);
|
|
17728
|
+
}
|
|
16324
17729
|
if (outputScan.sanitized_text !== void 0) {
|
|
16325
17730
|
try {
|
|
16326
17731
|
ctx?.logScan?.(toolName, "output", outputScan);
|
|
@@ -16329,7 +17734,10 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
16329
17734
|
try {
|
|
16330
17735
|
return JSON.parse(outputScan.sanitized_text);
|
|
16331
17736
|
} catch {
|
|
16332
|
-
return
|
|
17737
|
+
return toolError(
|
|
17738
|
+
"server_error",
|
|
17739
|
+
"The response could not be returned safely. Please retry or contact support."
|
|
17740
|
+
);
|
|
16333
17741
|
}
|
|
16334
17742
|
}
|
|
16335
17743
|
return result;
|
|
@@ -16379,8 +17787,7 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
16379
17787
|
details: {
|
|
16380
17788
|
source: "wrapper",
|
|
16381
17789
|
// A thrown exception escaped the handler — an unclassified fault.
|
|
16382
|
-
error_type: "server_error"
|
|
16383
|
-
exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
|
|
17790
|
+
error_type: "server_error"
|
|
16384
17791
|
}
|
|
16385
17792
|
});
|
|
16386
17793
|
throw err;
|
|
@@ -16512,12 +17919,14 @@ function registerAllTools(server, options) {
|
|
|
16512
17919
|
registerSkillsTools(server);
|
|
16513
17920
|
registerLoopPulseTools(server);
|
|
16514
17921
|
registerBanditStateTools(server);
|
|
17922
|
+
registerLifecycleTools(server);
|
|
16515
17923
|
if (!options?.skipApps) {
|
|
16516
17924
|
registerContentCalendarApp(server);
|
|
17925
|
+
registerAnalyticsPulseApp(server);
|
|
16517
17926
|
}
|
|
16518
17927
|
applyAnnotations(server);
|
|
16519
17928
|
}
|
|
16520
|
-
var RESPONSE_CHAR_LIMIT;
|
|
17929
|
+
var BASE64_PAYLOAD_KEYS, DATA_URI_PREFIX2, STRICT_BASE64, RESPONSE_CHAR_LIMIT;
|
|
16521
17930
|
var init_register_tools = __esm({
|
|
16522
17931
|
"src/lib/register-tools.ts"() {
|
|
16523
17932
|
"use strict";
|
|
@@ -16559,12 +17968,17 @@ var init_register_tools = __esm({
|
|
|
16559
17968
|
init_niche_research();
|
|
16560
17969
|
init_hyperframes();
|
|
16561
17970
|
init_content_calendar();
|
|
17971
|
+
init_analytics_pulse();
|
|
16562
17972
|
init_connections();
|
|
16563
17973
|
init_harness();
|
|
16564
17974
|
init_hermes();
|
|
16565
17975
|
init_skills();
|
|
16566
17976
|
init_loopPulse();
|
|
16567
17977
|
init_banditState();
|
|
17978
|
+
init_lifecycle();
|
|
17979
|
+
BASE64_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["file_data", "fileData"]);
|
|
17980
|
+
DATA_URI_PREFIX2 = /^data:[\w.+-]+\/[\w.+-]+;base64,/;
|
|
17981
|
+
STRICT_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
16568
17982
|
RESPONSE_CHAR_LIMIT = 1e5;
|
|
16569
17983
|
}
|
|
16570
17984
|
});
|
|
@@ -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
|
server.close();
|
|
17440
18875
|
resolve3({ error: "Authorization timed out after 120 seconds." });
|
|
17441
18876
|
}, 12e4);
|
|
17442
18877
|
server.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) {
|