@socialneuron/mcp-server 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -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 +2086 -480
- package/dist/index.js +1868 -373
- package/dist/sn.js +1893 -398
- package/package.json +15 -12
- 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: [
|
|
@@ -4164,7 +4475,7 @@ function registerContentTools(server) {
|
|
|
4164
4475
|
);
|
|
4165
4476
|
server.tool(
|
|
4166
4477
|
"generate_image",
|
|
4167
|
-
"Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs
|
|
4478
|
+
"Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 15-50 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video). Pass project_id so the asset is stored with the correct brand/project.",
|
|
4168
4479
|
{
|
|
4169
4480
|
prompt: z2.string().max(2e3).describe(
|
|
4170
4481
|
"Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
|
|
@@ -4186,9 +4497,10 @@ function registerContentTools(server) {
|
|
|
4186
4497
|
image_url: z2.string().optional().describe(
|
|
4187
4498
|
"Reference image URL for image-to-image generation. Required for ideogram model. Optional for others."
|
|
4188
4499
|
),
|
|
4500
|
+
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
4189
4501
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4190
4502
|
},
|
|
4191
|
-
async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
|
|
4503
|
+
async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
|
|
4192
4504
|
const format = response_format ?? "text";
|
|
4193
4505
|
const userId = await getDefaultUserId();
|
|
4194
4506
|
const assetBudget = checkAssetBudget();
|
|
@@ -4206,7 +4518,7 @@ function registerContentTools(server) {
|
|
|
4206
4518
|
isError: true
|
|
4207
4519
|
};
|
|
4208
4520
|
}
|
|
4209
|
-
const rateLimit = checkRateLimit("
|
|
4521
|
+
const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
|
|
4210
4522
|
if (!rateLimit.allowed) {
|
|
4211
4523
|
return {
|
|
4212
4524
|
content: [
|
|
@@ -4224,7 +4536,8 @@ function registerContentTools(server) {
|
|
|
4224
4536
|
prompt: prompt2,
|
|
4225
4537
|
model,
|
|
4226
4538
|
aspectRatio: aspect_ratio ?? "1:1",
|
|
4227
|
-
imageUrl: image_url
|
|
4539
|
+
imageUrl: image_url,
|
|
4540
|
+
...project_id && { projectId: project_id }
|
|
4228
4541
|
},
|
|
4229
4542
|
{ timeoutMs: 3e4 }
|
|
4230
4543
|
);
|
|
@@ -4263,7 +4576,8 @@ function registerContentTools(server) {
|
|
|
4263
4576
|
jobId,
|
|
4264
4577
|
taskId: data.taskId,
|
|
4265
4578
|
asyncJobId: data.asyncJobId,
|
|
4266
|
-
model: data.model
|
|
4579
|
+
model: data.model,
|
|
4580
|
+
projectId: project_id ?? null
|
|
4267
4581
|
}),
|
|
4268
4582
|
null,
|
|
4269
4583
|
2
|
|
@@ -4344,20 +4658,28 @@ function registerContentTools(server) {
|
|
|
4344
4658
|
}
|
|
4345
4659
|
);
|
|
4346
4660
|
if (liveStatus) {
|
|
4661
|
+
const livePayload = buildCheckStatusPayload(job, liveStatus);
|
|
4347
4662
|
const lines2 = [
|
|
4348
4663
|
`Job: ${job.id}`,
|
|
4349
4664
|
`Type: ${job.job_type}`,
|
|
4350
4665
|
`Model: ${job.model}`,
|
|
4351
|
-
`Status: ${
|
|
4352
|
-
`Progress: ${
|
|
4666
|
+
`Status: ${livePayload.status}`,
|
|
4667
|
+
`Progress: ${livePayload.progress}%`
|
|
4353
4668
|
];
|
|
4354
|
-
if (
|
|
4355
|
-
lines2.push(`Result URL: ${
|
|
4669
|
+
if (livePayload.result_url) {
|
|
4670
|
+
lines2.push(`Result URL: ${livePayload.result_url}`);
|
|
4356
4671
|
}
|
|
4357
|
-
if (
|
|
4358
|
-
lines2.push(`Error: ${
|
|
4672
|
+
if (livePayload.error) {
|
|
4673
|
+
lines2.push(`Error: ${livePayload.error}`);
|
|
4359
4674
|
}
|
|
4675
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
|
|
4676
|
+
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
4360
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
|
+
}
|
|
4361
4683
|
lines2.push(`Created: ${job.created_at}`);
|
|
4362
4684
|
if (format === "json") {
|
|
4363
4685
|
return {
|
|
@@ -4375,7 +4697,7 @@ function registerContentTools(server) {
|
|
|
4375
4697
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
4376
4698
|
asEnvelope2({
|
|
4377
4699
|
...liveStatus,
|
|
4378
|
-
...
|
|
4700
|
+
...livePayload
|
|
4379
4701
|
}),
|
|
4380
4702
|
null,
|
|
4381
4703
|
2
|
|
@@ -4418,7 +4740,14 @@ function registerContentTools(server) {
|
|
|
4418
4740
|
if (job.error_message) {
|
|
4419
4741
|
lines.push(`Error: ${job.error_message}`);
|
|
4420
4742
|
}
|
|
4743
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
|
|
4744
|
+
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
4421
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
|
+
}
|
|
4422
4751
|
lines.push(`Created: ${job.created_at}`);
|
|
4423
4752
|
if (job.completed_at) {
|
|
4424
4753
|
lines.push(`Completed: ${job.completed_at}`);
|
|
@@ -4444,7 +4773,7 @@ function registerContentTools(server) {
|
|
|
4444
4773
|
);
|
|
4445
4774
|
server.tool(
|
|
4446
4775
|
"create_storyboard",
|
|
4447
|
-
"Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent
|
|
4776
|
+
"Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile and project_id for consistent, project-scoped production. Costs 10 credits.",
|
|
4448
4777
|
{
|
|
4449
4778
|
concept: z2.string().max(2e3).describe(
|
|
4450
4779
|
'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
|
|
@@ -4468,6 +4797,7 @@ function registerContentTools(server) {
|
|
|
4468
4797
|
style: z2.string().optional().describe(
|
|
4469
4798
|
'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
|
|
4470
4799
|
),
|
|
4800
|
+
project_id: z2.string().optional().describe("Project ID for brand-scoped generation and attribution."),
|
|
4471
4801
|
response_format: z2.enum(["text", "json"]).optional().describe(
|
|
4472
4802
|
"Response format. Defaults to json for structured storyboard data."
|
|
4473
4803
|
)
|
|
@@ -4479,6 +4809,7 @@ function registerContentTools(server) {
|
|
|
4479
4809
|
target_duration,
|
|
4480
4810
|
num_scenes,
|
|
4481
4811
|
style,
|
|
4812
|
+
project_id,
|
|
4482
4813
|
response_format
|
|
4483
4814
|
}) => {
|
|
4484
4815
|
const format = response_format ?? "json";
|
|
@@ -4561,7 +4892,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
4561
4892
|
prompt: storyboardPrompt,
|
|
4562
4893
|
type: "storyboard",
|
|
4563
4894
|
model: "gemini-2.5-flash",
|
|
4564
|
-
responseFormat: "json"
|
|
4895
|
+
responseFormat: "json",
|
|
4896
|
+
...project_id && { projectId: project_id }
|
|
4565
4897
|
},
|
|
4566
4898
|
{ timeoutMs: 6e4 }
|
|
4567
4899
|
);
|
|
@@ -4576,7 +4908,18 @@ Return ONLY valid JSON in this exact format:
|
|
|
4576
4908
|
isError: true
|
|
4577
4909
|
};
|
|
4578
4910
|
}
|
|
4579
|
-
const rawContent = data?.content
|
|
4911
|
+
const rawContent = data?.content?.trim() || data?.text?.trim() || "";
|
|
4912
|
+
if (!rawContent) {
|
|
4913
|
+
return {
|
|
4914
|
+
content: [
|
|
4915
|
+
{
|
|
4916
|
+
type: "text",
|
|
4917
|
+
text: "Storyboard generation failed: the AI service returned an empty response."
|
|
4918
|
+
}
|
|
4919
|
+
],
|
|
4920
|
+
isError: true
|
|
4921
|
+
};
|
|
4922
|
+
}
|
|
4580
4923
|
addCreditsUsed(estimatedCost);
|
|
4581
4924
|
if (format === "json") {
|
|
4582
4925
|
try {
|
|
@@ -4612,16 +4955,17 @@ Return ONLY valid JSON in this exact format:
|
|
|
4612
4955
|
);
|
|
4613
4956
|
server.tool(
|
|
4614
4957
|
"generate_voiceover",
|
|
4615
|
-
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs
|
|
4958
|
+
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Pass project_id to keep the asset with the correct brand/project. Costs 15 credits per generation.",
|
|
4616
4959
|
{
|
|
4617
4960
|
text: z2.string().max(5e3).describe("The script/text to convert to speech."),
|
|
4618
4961
|
voice: z2.enum(["rachel", "domi"]).optional().describe(
|
|
4619
4962
|
"Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
|
|
4620
4963
|
),
|
|
4621
4964
|
speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
|
|
4965
|
+
project_id: z2.string().optional().describe("Project ID to associate the generated voiceover with."),
|
|
4622
4966
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
4623
4967
|
},
|
|
4624
|
-
async ({ text, voice, speed, response_format }) => {
|
|
4968
|
+
async ({ text, voice, speed, project_id, response_format }) => {
|
|
4625
4969
|
const format = response_format ?? "text";
|
|
4626
4970
|
const userId = await getDefaultUserId();
|
|
4627
4971
|
const estimatedCost = 15;
|
|
@@ -4633,7 +4977,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
4633
4977
|
};
|
|
4634
4978
|
}
|
|
4635
4979
|
const rateLimit = checkRateLimit(
|
|
4636
|
-
"
|
|
4980
|
+
"generation",
|
|
4637
4981
|
`generate_voiceover:${userId}`
|
|
4638
4982
|
);
|
|
4639
4983
|
if (!rateLimit.allowed) {
|
|
@@ -4656,7 +5000,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
4656
5000
|
{
|
|
4657
5001
|
text,
|
|
4658
5002
|
voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
|
|
4659
|
-
speed: speed ?? 1
|
|
5003
|
+
speed: speed ?? 1,
|
|
5004
|
+
...project_id && { projectId: project_id }
|
|
4660
5005
|
},
|
|
4661
5006
|
{ timeoutMs: 6e4 }
|
|
4662
5007
|
);
|
|
@@ -4692,7 +5037,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
4692
5037
|
asEnvelope2({
|
|
4693
5038
|
audioUrl: data.audioUrl,
|
|
4694
5039
|
durationSeconds: data.durationSeconds,
|
|
4695
|
-
voice: voice ?? "rachel"
|
|
5040
|
+
voice: voice ?? "rachel",
|
|
5041
|
+
projectId: project_id ?? null
|
|
4696
5042
|
}),
|
|
4697
5043
|
null,
|
|
4698
5044
|
2
|
|
@@ -4798,7 +5144,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
4798
5144
|
}
|
|
4799
5145
|
const userId = await getDefaultUserId();
|
|
4800
5146
|
const rateLimit = checkRateLimit(
|
|
4801
|
-
"
|
|
5147
|
+
"generation",
|
|
4802
5148
|
`generate_carousel:${userId}`
|
|
4803
5149
|
);
|
|
4804
5150
|
if (!rateLimit.allowed) {
|
|
@@ -4952,9 +5298,6 @@ var init_content2 = __esm({
|
|
|
4952
5298
|
// src/lib/sanitize-error.ts
|
|
4953
5299
|
function sanitizeError(error) {
|
|
4954
5300
|
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
4955
|
-
if (process.env.NODE_ENV !== "production") {
|
|
4956
|
-
console.error("[Error]", msg);
|
|
4957
|
-
}
|
|
4958
5301
|
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
4959
5302
|
if (pattern.test(msg)) {
|
|
4960
5303
|
return userMessage;
|
|
@@ -5158,7 +5501,7 @@ var init_ssrf = __esm({
|
|
|
5158
5501
|
|
|
5159
5502
|
// src/tools/distribution.ts
|
|
5160
5503
|
import { z as z3 } from "zod";
|
|
5161
|
-
import { createHash as
|
|
5504
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5162
5505
|
function snakeToCamel(obj) {
|
|
5163
5506
|
const result = {};
|
|
5164
5507
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -5184,6 +5527,87 @@ function asEnvelope3(data) {
|
|
|
5184
5527
|
data
|
|
5185
5528
|
};
|
|
5186
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
|
+
}
|
|
5187
5611
|
function accountEffectiveStatus(account) {
|
|
5188
5612
|
return account.effective_status || account.status;
|
|
5189
5613
|
}
|
|
@@ -5202,22 +5626,11 @@ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
|
5202
5626
|
if (!accountIds) return void 0;
|
|
5203
5627
|
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
5204
5628
|
}
|
|
5205
|
-
function isAlreadyR2Signed(url) {
|
|
5206
|
-
try {
|
|
5207
|
-
const u = new URL(url);
|
|
5208
|
-
return u.searchParams.has("X-Amz-Signature");
|
|
5209
|
-
} catch {
|
|
5210
|
-
return false;
|
|
5211
|
-
}
|
|
5212
|
-
}
|
|
5213
5629
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
5214
5630
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
5215
5631
|
if (!ssrf.isValid) {
|
|
5216
5632
|
return { error: ssrf.error ?? "URL rejected by SSRF check" };
|
|
5217
5633
|
}
|
|
5218
|
-
if (isAlreadyR2Signed(mediaUrl)) {
|
|
5219
|
-
return { signedUrl: mediaUrl, r2Key: "" };
|
|
5220
|
-
}
|
|
5221
5634
|
const { data, error } = await callEdgeFunction(
|
|
5222
5635
|
"upload-to-r2",
|
|
5223
5636
|
{ url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
|
|
@@ -5236,19 +5649,19 @@ function registerDistributionTools(server) {
|
|
|
5236
5649
|
media_url: z3.string().optional().describe(
|
|
5237
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."
|
|
5238
5651
|
),
|
|
5239
|
-
media_urls: z3.array(z3.string()).optional().describe(
|
|
5652
|
+
media_urls: z3.array(z3.string().url()).min(2).max(10).optional().describe(
|
|
5240
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."
|
|
5241
5654
|
),
|
|
5242
5655
|
r2_key: z3.string().optional().describe(
|
|
5243
5656
|
"R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
|
|
5244
5657
|
),
|
|
5245
|
-
r2_keys: z3.array(z3.string()).optional().describe(
|
|
5658
|
+
r2_keys: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
5246
5659
|
"Array of R2 object keys for carousel posts. Each is signed on demand. Alternative to media_urls."
|
|
5247
5660
|
),
|
|
5248
5661
|
job_id: z3.string().optional().describe(
|
|
5249
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."
|
|
5250
5663
|
),
|
|
5251
|
-
job_ids: z3.array(z3.string()).optional().describe(
|
|
5664
|
+
job_ids: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
5252
5665
|
"Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
|
|
5253
5666
|
),
|
|
5254
5667
|
platform_metadata: z3.object({
|
|
@@ -5278,7 +5691,10 @@ function registerDistributionTools(server) {
|
|
|
5278
5691
|
category_id: z3.string().optional(),
|
|
5279
5692
|
tags: z3.array(z3.string()).optional(),
|
|
5280
5693
|
made_for_kids: z3.boolean().optional(),
|
|
5281
|
-
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
|
+
)
|
|
5282
5698
|
}).optional(),
|
|
5283
5699
|
facebook: z3.object({
|
|
5284
5700
|
page_id: z3.string().optional().describe("Facebook Page ID to post to."),
|
|
@@ -5361,14 +5777,8 @@ function registerDistributionTools(server) {
|
|
|
5361
5777
|
auto_rehost: z3.boolean().optional().describe(
|
|
5362
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."
|
|
5363
5779
|
),
|
|
5364
|
-
|
|
5365
|
-
"
|
|
5366
|
-
),
|
|
5367
|
-
origin: z3.enum(["human", "hermes", "user"]).optional().describe(
|
|
5368
|
-
"Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
|
|
5369
|
-
),
|
|
5370
|
-
hermes_run_id: z3.string().optional().describe(
|
|
5371
|
-
"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."
|
|
5372
5782
|
)
|
|
5373
5783
|
},
|
|
5374
5784
|
async ({
|
|
@@ -5391,9 +5801,7 @@ function registerDistributionTools(server) {
|
|
|
5391
5801
|
platform_metadata,
|
|
5392
5802
|
account_id,
|
|
5393
5803
|
account_ids,
|
|
5394
|
-
|
|
5395
|
-
origin,
|
|
5396
|
-
hermes_run_id
|
|
5804
|
+
idempotency_key
|
|
5397
5805
|
}) => {
|
|
5398
5806
|
const format = response_format ?? "text";
|
|
5399
5807
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
@@ -5422,6 +5830,8 @@ function registerDistributionTools(server) {
|
|
|
5422
5830
|
}
|
|
5423
5831
|
let resolvedMediaUrl = media_url;
|
|
5424
5832
|
let resolvedMediaUrls = media_urls;
|
|
5833
|
+
let resolvedMediaUrlIsTrustedR2 = false;
|
|
5834
|
+
let resolvedMediaUrlsAreTrustedR2 = (media_urls ?? []).map(() => false);
|
|
5425
5835
|
const signR2Key = async (key) => {
|
|
5426
5836
|
const cleanKey = key.startsWith("r2://") ? key.slice(5) : key;
|
|
5427
5837
|
const { data: signData } = await callEdgeFunction(
|
|
@@ -5439,8 +5849,11 @@ function registerDistributionTools(server) {
|
|
|
5439
5849
|
);
|
|
5440
5850
|
const resultUrl = jobData?.job?.result_url;
|
|
5441
5851
|
if (!resultUrl) return null;
|
|
5442
|
-
if (!resultUrl.startsWith("http"))
|
|
5443
|
-
|
|
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 };
|
|
5444
5857
|
};
|
|
5445
5858
|
try {
|
|
5446
5859
|
if (r2_key && !resolvedMediaUrl) {
|
|
@@ -5457,6 +5870,7 @@ function registerDistributionTools(server) {
|
|
|
5457
5870
|
};
|
|
5458
5871
|
}
|
|
5459
5872
|
resolvedMediaUrl = signed;
|
|
5873
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
5460
5874
|
} else if (job_id && !resolvedMediaUrl && !r2_key) {
|
|
5461
5875
|
const resolved = await resolveJobId(job_id);
|
|
5462
5876
|
if (!resolved) {
|
|
@@ -5470,7 +5884,8 @@ function registerDistributionTools(server) {
|
|
|
5470
5884
|
isError: true
|
|
5471
5885
|
};
|
|
5472
5886
|
}
|
|
5473
|
-
resolvedMediaUrl = resolved;
|
|
5887
|
+
resolvedMediaUrl = resolved.url;
|
|
5888
|
+
resolvedMediaUrlIsTrustedR2 = resolved.trustedR2;
|
|
5474
5889
|
}
|
|
5475
5890
|
if (r2_keys && r2_keys.length > 0 && !resolvedMediaUrls) {
|
|
5476
5891
|
const signed = await Promise.all(r2_keys.map(signR2Key));
|
|
@@ -5487,6 +5902,7 @@ function registerDistributionTools(server) {
|
|
|
5487
5902
|
};
|
|
5488
5903
|
}
|
|
5489
5904
|
resolvedMediaUrls = signed;
|
|
5905
|
+
resolvedMediaUrlsAreTrustedR2 = signed.map(() => true);
|
|
5490
5906
|
} else if (job_ids && job_ids.length > 0 && !resolvedMediaUrls && !r2_keys) {
|
|
5491
5907
|
const resolved = await Promise.all(job_ids.map(resolveJobId));
|
|
5492
5908
|
const failIdx = resolved.findIndex((r) => !r);
|
|
@@ -5501,10 +5917,12 @@ function registerDistributionTools(server) {
|
|
|
5501
5917
|
isError: true
|
|
5502
5918
|
};
|
|
5503
5919
|
}
|
|
5504
|
-
|
|
5920
|
+
const resolvedJobs = resolved;
|
|
5921
|
+
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
5922
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
|
|
5505
5923
|
}
|
|
5506
5924
|
const shouldRehost = auto_rehost !== false;
|
|
5507
|
-
if (shouldRehost && resolvedMediaUrl) {
|
|
5925
|
+
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
5508
5926
|
const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
|
|
5509
5927
|
if ("error" in rehost) {
|
|
5510
5928
|
return {
|
|
@@ -5518,10 +5936,13 @@ function registerDistributionTools(server) {
|
|
|
5518
5936
|
};
|
|
5519
5937
|
}
|
|
5520
5938
|
resolvedMediaUrl = rehost.signedUrl;
|
|
5939
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
5521
5940
|
}
|
|
5522
5941
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
5523
5942
|
const rehosted = await Promise.all(
|
|
5524
|
-
resolvedMediaUrls.map(
|
|
5943
|
+
resolvedMediaUrls.map(
|
|
5944
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
|
|
5945
|
+
)
|
|
5525
5946
|
);
|
|
5526
5947
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
5527
5948
|
if (failIdx !== -1) {
|
|
@@ -5537,6 +5958,7 @@ function registerDistributionTools(server) {
|
|
|
5537
5958
|
};
|
|
5538
5959
|
}
|
|
5539
5960
|
resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
|
|
5961
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedMediaUrls.map(() => true);
|
|
5540
5962
|
}
|
|
5541
5963
|
} catch (resolveErr) {
|
|
5542
5964
|
return {
|
|
@@ -5549,6 +5971,42 @@ function registerDistributionTools(server) {
|
|
|
5549
5971
|
isError: true
|
|
5550
5972
|
};
|
|
5551
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
|
+
}
|
|
5552
6010
|
const normalizedPlatforms = platforms.map(
|
|
5553
6011
|
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
5554
6012
|
);
|
|
@@ -5674,12 +6132,32 @@ Created with Social Neuron`;
|
|
|
5674
6132
|
};
|
|
5675
6133
|
return true;
|
|
5676
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
|
+
}
|
|
5677
6155
|
const { data, error } = await callEdgeFunction(
|
|
5678
6156
|
"schedule-post",
|
|
5679
6157
|
{
|
|
5680
6158
|
mediaUrl: resolvedMediaUrl,
|
|
5681
6159
|
mediaUrls: resolvedMediaUrls,
|
|
5682
|
-
mediaType:
|
|
6160
|
+
mediaType: resolvedMediaType ?? void 0,
|
|
5683
6161
|
caption: finalCaption,
|
|
5684
6162
|
platforms: normalizedPlatforms,
|
|
5685
6163
|
title,
|
|
@@ -5692,14 +6170,10 @@ Created with Social Neuron`;
|
|
|
5692
6170
|
normalizedPlatformMetadata
|
|
5693
6171
|
)
|
|
5694
6172
|
} : {},
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
//
|
|
5699
|
-
// against the posts.origin CHECK constraint and persists hermesRunId when
|
|
5700
|
-
// origin === 'hermes'.
|
|
5701
|
-
...origin ? { origin } : {},
|
|
5702
|
-
...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.
|
|
5703
6177
|
},
|
|
5704
6178
|
{ timeoutMs: 3e4 }
|
|
5705
6179
|
);
|
|
@@ -5766,75 +6240,172 @@ Created with Social Neuron`;
|
|
|
5766
6240
|
}
|
|
5767
6241
|
);
|
|
5768
6242
|
server.tool(
|
|
5769
|
-
"
|
|
5770
|
-
"
|
|
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.",
|
|
5771
6245
|
{
|
|
5772
|
-
|
|
5773
|
-
|
|
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."
|
|
5774
6249
|
),
|
|
5775
|
-
|
|
5776
|
-
|
|
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")
|
|
5777
6255
|
},
|
|
5778
|
-
async ({
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
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()) {
|
|
5786
6265
|
return {
|
|
5787
6266
|
content: [
|
|
5788
6267
|
{
|
|
5789
6268
|
type: "text",
|
|
5790
|
-
text:
|
|
6269
|
+
text: "scheduled_at must be a valid future ISO datetime with timezone."
|
|
5791
6270
|
}
|
|
5792
6271
|
],
|
|
5793
6272
|
isError: true
|
|
5794
6273
|
};
|
|
5795
6274
|
}
|
|
5796
|
-
const
|
|
5797
|
-
if (
|
|
5798
|
-
if (format === "json") {
|
|
5799
|
-
return {
|
|
5800
|
-
content: [
|
|
5801
|
-
{
|
|
5802
|
-
type: "text",
|
|
5803
|
-
text: JSON.stringify(asEnvelope3({ accounts: [] }), null, 2)
|
|
5804
|
-
}
|
|
5805
|
-
]
|
|
5806
|
-
};
|
|
5807
|
-
}
|
|
6275
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
6276
|
+
if (!resolvedProjectId) {
|
|
5808
6277
|
return {
|
|
5809
6278
|
content: [
|
|
5810
6279
|
{
|
|
5811
6280
|
type: "text",
|
|
5812
|
-
text: "No
|
|
6281
|
+
text: "No project_id was provided and no default project is configured."
|
|
5813
6282
|
}
|
|
5814
|
-
]
|
|
6283
|
+
],
|
|
6284
|
+
isError: true
|
|
5815
6285
|
};
|
|
5816
6286
|
}
|
|
5817
|
-
const
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
];
|
|
5821
|
-
for (const account of accounts) {
|
|
5822
|
-
const name = account.username || "(unnamed)";
|
|
5823
|
-
const platformLower = account.platform.toLowerCase();
|
|
5824
|
-
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
5825
|
-
const status = accountEffectiveStatus(account);
|
|
5826
|
-
lines.push(
|
|
5827
|
-
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
5828
|
-
);
|
|
5829
|
-
}
|
|
5830
|
-
if (format === "json") {
|
|
6287
|
+
const userId = await getDefaultUserId();
|
|
6288
|
+
const rateLimit = checkRateLimit("posting", `reschedule_post:${userId}`);
|
|
6289
|
+
if (!rateLimit.allowed) {
|
|
5831
6290
|
return {
|
|
5832
6291
|
content: [
|
|
5833
6292
|
{
|
|
5834
6293
|
type: "text",
|
|
5835
|
-
text:
|
|
6294
|
+
text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
|
|
5836
6295
|
}
|
|
5837
|
-
]
|
|
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
|
+
);
|
|
6335
|
+
server.tool(
|
|
6336
|
+
"list_connected_accounts",
|
|
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.",
|
|
6338
|
+
{
|
|
6339
|
+
project_id: z3.string().optional().describe(
|
|
6340
|
+
"Brand/project ID to scope connected accounts. Use the same project_id when calling schedule_post."
|
|
6341
|
+
),
|
|
6342
|
+
include_all: z3.boolean().optional().describe("If true, include expired or inactive accounts as well as usable accounts."),
|
|
6343
|
+
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6344
|
+
},
|
|
6345
|
+
async ({ project_id, include_all, response_format }) => {
|
|
6346
|
+
const format = response_format ?? "text";
|
|
6347
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
6348
|
+
action: "connected-accounts",
|
|
6349
|
+
...project_id ? { projectId: project_id, project_id } : {},
|
|
6350
|
+
...include_all ? { includeAll: true } : {}
|
|
6351
|
+
});
|
|
6352
|
+
if (efError || !result?.success) {
|
|
6353
|
+
return {
|
|
6354
|
+
content: [
|
|
6355
|
+
{
|
|
6356
|
+
type: "text",
|
|
6357
|
+
text: `Failed to list connected accounts: ${efError || result?.error || "Unknown error"}`
|
|
6358
|
+
}
|
|
6359
|
+
],
|
|
6360
|
+
isError: true
|
|
6361
|
+
};
|
|
6362
|
+
}
|
|
6363
|
+
const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
6364
|
+
if (accounts.length === 0) {
|
|
6365
|
+
if (format === "json") {
|
|
6366
|
+
const structuredContent = asEnvelope3({ accounts: [] });
|
|
6367
|
+
return {
|
|
6368
|
+
structuredContent,
|
|
6369
|
+
content: [
|
|
6370
|
+
{
|
|
6371
|
+
type: "text",
|
|
6372
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
6373
|
+
}
|
|
6374
|
+
]
|
|
6375
|
+
};
|
|
6376
|
+
}
|
|
6377
|
+
return {
|
|
6378
|
+
content: [
|
|
6379
|
+
{
|
|
6380
|
+
type: "text",
|
|
6381
|
+
text: "No connected social media accounts found. Connect platforms in Social Neuron Settings > Connections."
|
|
6382
|
+
}
|
|
6383
|
+
]
|
|
6384
|
+
};
|
|
6385
|
+
}
|
|
6386
|
+
const lines = [
|
|
6387
|
+
`${accounts.length} connected account(s)${project_id ? ` for project ${project_id}` : ""}:`,
|
|
6388
|
+
""
|
|
6389
|
+
];
|
|
6390
|
+
for (const account of accounts) {
|
|
6391
|
+
const name = account.username || "(unnamed)";
|
|
6392
|
+
const platformLower = account.platform.toLowerCase();
|
|
6393
|
+
const project = account.project_id ? `project_id=${account.project_id}` : "project_id=unassigned";
|
|
6394
|
+
const status = accountEffectiveStatus(account);
|
|
6395
|
+
lines.push(
|
|
6396
|
+
` ${platformLower}: ${name} | id=${account.id} | ${project} | status=${status} (connected ${account.created_at.split("T")[0]})`
|
|
6397
|
+
);
|
|
6398
|
+
}
|
|
6399
|
+
if (format === "json") {
|
|
6400
|
+
const structuredContent = asEnvelope3({ accounts });
|
|
6401
|
+
return {
|
|
6402
|
+
structuredContent,
|
|
6403
|
+
content: [
|
|
6404
|
+
{
|
|
6405
|
+
type: "text",
|
|
6406
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
6407
|
+
}
|
|
6408
|
+
]
|
|
5838
6409
|
};
|
|
5839
6410
|
}
|
|
5840
6411
|
return {
|
|
@@ -5846,6 +6417,9 @@ Created with Social Neuron`;
|
|
|
5846
6417
|
"list_recent_posts",
|
|
5847
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.",
|
|
5848
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
|
+
),
|
|
5849
6423
|
platform: z3.enum([
|
|
5850
6424
|
"youtube",
|
|
5851
6425
|
"tiktok",
|
|
@@ -5861,13 +6435,27 @@ Created with Social Neuron`;
|
|
|
5861
6435
|
limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
5862
6436
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5863
6437
|
},
|
|
5864
|
-
async ({ platform: platform3, status, days, limit, response_format }) => {
|
|
6438
|
+
async ({ project_id, platform: platform3, status, days, limit, response_format }) => {
|
|
5865
6439
|
const format = response_format ?? "text";
|
|
5866
6440
|
const lookbackDays = days ?? 7;
|
|
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
|
+
}
|
|
5867
6453
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5868
6454
|
action: "recent-posts",
|
|
5869
6455
|
days: lookbackDays,
|
|
5870
6456
|
limit: limit ?? 20,
|
|
6457
|
+
projectId: resolvedProjectId,
|
|
6458
|
+
project_id: resolvedProjectId,
|
|
5871
6459
|
...platform3 ? { platform: platform3 } : {},
|
|
5872
6460
|
...status ? { status } : {}
|
|
5873
6461
|
});
|
|
@@ -5882,7 +6470,7 @@ Created with Social Neuron`;
|
|
|
5882
6470
|
isError: true
|
|
5883
6471
|
};
|
|
5884
6472
|
}
|
|
5885
|
-
const rows = result.posts ?? [];
|
|
6473
|
+
const rows = (result.posts ?? []).map(publicPostRecord).filter((post) => post !== null);
|
|
5886
6474
|
if (rows.length === 0) {
|
|
5887
6475
|
if (format === "json") {
|
|
5888
6476
|
const structuredContent2 = asEnvelope3({ posts: [] });
|
|
@@ -5957,8 +6545,11 @@ Created with Social Neuron`;
|
|
|
5957
6545
|
};
|
|
5958
6546
|
server.tool(
|
|
5959
6547
|
"find_next_slots",
|
|
5960
|
-
"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.",
|
|
5961
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
|
+
),
|
|
5962
6553
|
platforms: z3.array(
|
|
5963
6554
|
z3.enum([
|
|
5964
6555
|
"youtube",
|
|
@@ -5977,6 +6568,7 @@ Created with Social Neuron`;
|
|
|
5977
6568
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
5978
6569
|
},
|
|
5979
6570
|
async ({
|
|
6571
|
+
project_id,
|
|
5980
6572
|
platforms,
|
|
5981
6573
|
count,
|
|
5982
6574
|
start_after,
|
|
@@ -5984,13 +6576,35 @@ Created with Social Neuron`;
|
|
|
5984
6576
|
response_format
|
|
5985
6577
|
}) => {
|
|
5986
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
|
+
}
|
|
5987
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
|
+
}
|
|
5988
6600
|
const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
5989
6601
|
const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
|
|
5990
6602
|
action: "scheduled-posts",
|
|
5991
6603
|
start_date: startDate.toISOString(),
|
|
5992
6604
|
end_date: endDate.toISOString(),
|
|
5993
|
-
statuses: ["scheduled", "draft"]
|
|
6605
|
+
statuses: ["pending", "scheduled", "draft"],
|
|
6606
|
+
projectId: resolvedProjectId,
|
|
6607
|
+
project_id: resolvedProjectId
|
|
5994
6608
|
});
|
|
5995
6609
|
const existingPosts = postsError ? [] : postsResult?.posts ?? [];
|
|
5996
6610
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
@@ -6030,19 +6644,17 @@ Created with Social Neuron`;
|
|
|
6030
6644
|
const slots = candidates.filter((s) => !s.conflict).sort((a, b) => b.engagement_score - a.engagement_score).slice(0, count);
|
|
6031
6645
|
const conflictsAvoided = candidates.filter((s) => s.conflict).length;
|
|
6032
6646
|
if (response_format === "json") {
|
|
6647
|
+
const structuredContent = asEnvelope3({
|
|
6648
|
+
slots,
|
|
6649
|
+
total_candidates: candidates.length,
|
|
6650
|
+
conflicts_avoided: conflictsAvoided
|
|
6651
|
+
});
|
|
6033
6652
|
return {
|
|
6653
|
+
structuredContent,
|
|
6034
6654
|
content: [
|
|
6035
6655
|
{
|
|
6036
6656
|
type: "text",
|
|
6037
|
-
text: JSON.stringify(
|
|
6038
|
-
asEnvelope3({
|
|
6039
|
-
slots,
|
|
6040
|
-
total_candidates: candidates.length,
|
|
6041
|
-
conflicts_avoided: conflictsAvoided
|
|
6042
|
-
}),
|
|
6043
|
-
null,
|
|
6044
|
-
2
|
|
6045
|
-
)
|
|
6657
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
6046
6658
|
}
|
|
6047
6659
|
],
|
|
6048
6660
|
isError: false
|
|
@@ -6407,7 +7019,7 @@ Created with Social Neuron`;
|
|
|
6407
7019
|
const results = [];
|
|
6408
7020
|
const buildIdempotencyKey = (post) => {
|
|
6409
7021
|
const planId = effectivePlanId ?? (typeof workingPlan.plan_id === "string" ? String(workingPlan.plan_id) : "inline");
|
|
6410
|
-
const captionHash =
|
|
7022
|
+
const captionHash = createHash2("sha256").update(post.caption).digest("hex").slice(0, 16);
|
|
6411
7023
|
const raw = [
|
|
6412
7024
|
"schedule_content_plan",
|
|
6413
7025
|
planId,
|
|
@@ -6417,7 +7029,7 @@ Created with Social Neuron`;
|
|
|
6417
7029
|
captionHash,
|
|
6418
7030
|
idempotency_seed ?? ""
|
|
6419
7031
|
].join(":");
|
|
6420
|
-
return `plan-${
|
|
7032
|
+
return `plan-${createHash2("sha256").update(raw).digest("hex").slice(0, 48)}`;
|
|
6421
7033
|
};
|
|
6422
7034
|
const scheduleOne = async (post) => {
|
|
6423
7035
|
if (!post.schedule_at) {
|
|
@@ -6603,7 +7215,7 @@ Created with Social Neuron`;
|
|
|
6603
7215
|
}
|
|
6604
7216
|
);
|
|
6605
7217
|
}
|
|
6606
|
-
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;
|
|
6607
7219
|
var init_distribution = __esm({
|
|
6608
7220
|
"src/tools/distribution.ts"() {
|
|
6609
7221
|
"use strict";
|
|
@@ -6624,8 +7236,29 @@ var init_distribution = __esm({
|
|
|
6624
7236
|
threads: "Threads",
|
|
6625
7237
|
bluesky: "Bluesky"
|
|
6626
7238
|
};
|
|
6627
|
-
TIKTOK_AUDIT_APPROVED = false
|
|
7239
|
+
TIKTOK_AUDIT_APPROVED = !["false", "0", "no"].includes(
|
|
7240
|
+
(process.env.TIKTOK_AUDIT_APPROVED ?? "").toLowerCase()
|
|
7241
|
+
);
|
|
6628
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
|
+
]);
|
|
6629
7262
|
}
|
|
6630
7263
|
});
|
|
6631
7264
|
|
|
@@ -6906,7 +7539,10 @@ function registerMediaTools(server) {
|
|
|
6906
7539
|
content: [
|
|
6907
7540
|
{
|
|
6908
7541
|
type: "text",
|
|
6909
|
-
|
|
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.`
|
|
6910
7546
|
}
|
|
6911
7547
|
],
|
|
6912
7548
|
isError: true
|
|
@@ -7118,7 +7754,7 @@ function asEnvelope4(data) {
|
|
|
7118
7754
|
function registerAnalyticsTools(server) {
|
|
7119
7755
|
server.tool(
|
|
7120
7756
|
"fetch_analytics",
|
|
7121
|
-
"Get post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
|
|
7757
|
+
"Get project-scoped post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
|
|
7122
7758
|
{
|
|
7123
7759
|
platform: z5.enum([
|
|
7124
7760
|
"youtube",
|
|
@@ -7136,19 +7772,26 @@ function registerAnalyticsTools(server) {
|
|
|
7136
7772
|
content_id: z5.string().uuid().optional().describe(
|
|
7137
7773
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
7138
7774
|
),
|
|
7775
|
+
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7139
7776
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
7140
7777
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7141
7778
|
},
|
|
7142
|
-
async ({ platform: platform3, days, content_id, limit, response_format }) => {
|
|
7779
|
+
async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
|
|
7143
7780
|
const format = response_format ?? "text";
|
|
7144
7781
|
const lookbackDays = days ?? 30;
|
|
7145
7782
|
const maxPosts = limit ?? 20;
|
|
7783
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
7146
7784
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7147
7785
|
action: "analytics",
|
|
7148
7786
|
platform: platform3,
|
|
7149
7787
|
days: lookbackDays,
|
|
7150
|
-
|
|
7151
|
-
|
|
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,
|
|
7793
|
+
contentId: content_id,
|
|
7794
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
7152
7795
|
});
|
|
7153
7796
|
if (efError) {
|
|
7154
7797
|
return {
|
|
@@ -7156,7 +7799,14 @@ function registerAnalyticsTools(server) {
|
|
|
7156
7799
|
isError: true
|
|
7157
7800
|
};
|
|
7158
7801
|
}
|
|
7159
|
-
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);
|
|
7160
7810
|
if (rows.length === 0) {
|
|
7161
7811
|
if (format === "json") {
|
|
7162
7812
|
const structuredContent = asEnvelope4({
|
|
@@ -7218,14 +7868,19 @@ function registerAnalyticsTools(server) {
|
|
|
7218
7868
|
);
|
|
7219
7869
|
server.tool(
|
|
7220
7870
|
"refresh_platform_analytics",
|
|
7221
|
-
"Queue analytics refresh jobs for
|
|
7871
|
+
"Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
|
|
7222
7872
|
{
|
|
7873
|
+
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7223
7874
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7224
7875
|
},
|
|
7225
|
-
async ({ response_format }) => {
|
|
7876
|
+
async ({ project_id, response_format }) => {
|
|
7226
7877
|
const format = response_format ?? "text";
|
|
7227
7878
|
const userId = await getDefaultUserId();
|
|
7228
|
-
const
|
|
7879
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
7880
|
+
const rateLimit = checkRateLimit(
|
|
7881
|
+
"posting",
|
|
7882
|
+
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
7883
|
+
);
|
|
7229
7884
|
if (!rateLimit.allowed) {
|
|
7230
7885
|
return {
|
|
7231
7886
|
content: [
|
|
@@ -7237,7 +7892,10 @@ function registerAnalyticsTools(server) {
|
|
|
7237
7892
|
isError: true
|
|
7238
7893
|
};
|
|
7239
7894
|
}
|
|
7240
|
-
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
7895
|
+
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
7896
|
+
userId,
|
|
7897
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
7898
|
+
});
|
|
7241
7899
|
if (error) {
|
|
7242
7900
|
return {
|
|
7243
7901
|
content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
|
|
@@ -7266,7 +7924,8 @@ function registerAnalyticsTools(server) {
|
|
|
7266
7924
|
success: true,
|
|
7267
7925
|
postsProcessed: result.postsProcessed,
|
|
7268
7926
|
queued,
|
|
7269
|
-
errored
|
|
7927
|
+
errored,
|
|
7928
|
+
projectId: resolvedProjectId ?? null
|
|
7270
7929
|
});
|
|
7271
7930
|
return {
|
|
7272
7931
|
structuredContent,
|
|
@@ -7326,6 +7985,84 @@ var init_analytics2 = __esm({
|
|
|
7326
7985
|
}
|
|
7327
7986
|
});
|
|
7328
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
|
+
|
|
7329
8066
|
// src/tools/brand.ts
|
|
7330
8067
|
import { z as z6 } from "zod";
|
|
7331
8068
|
function asEnvelope5(data) {
|
|
@@ -7342,12 +8079,31 @@ function registerBrandTools(server) {
|
|
|
7342
8079
|
"extract_brand",
|
|
7343
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.",
|
|
7344
8081
|
{
|
|
7345
|
-
url: z6.string().
|
|
7346
|
-
'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").'
|
|
7347
8084
|
),
|
|
7348
8085
|
response_format: z6.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7349
8086
|
},
|
|
7350
|
-
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;
|
|
7351
8107
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
7352
8108
|
if (!ssrfCheck.isValid) {
|
|
7353
8109
|
return {
|
|
@@ -7655,6 +8411,7 @@ var init_brand = __esm({
|
|
|
7655
8411
|
init_supabase();
|
|
7656
8412
|
init_ssrf();
|
|
7657
8413
|
init_version();
|
|
8414
|
+
init_brandUrlInput();
|
|
7658
8415
|
}
|
|
7659
8416
|
});
|
|
7660
8417
|
|
|
@@ -8030,7 +8787,7 @@ function registerRemotionTools(server) {
|
|
|
8030
8787
|
},
|
|
8031
8788
|
async ({ composition_id, output_format, props }) => {
|
|
8032
8789
|
const userId = await getDefaultUserId();
|
|
8033
|
-
const rateLimit = checkRateLimit("
|
|
8790
|
+
const rateLimit = checkRateLimit("generation", `render_demo_video:${userId}`);
|
|
8034
8791
|
if (!rateLimit.allowed) {
|
|
8035
8792
|
return {
|
|
8036
8793
|
content: [
|
|
@@ -8369,22 +9126,25 @@ function asEnvelope6(data) {
|
|
|
8369
9126
|
function registerInsightsTools(server) {
|
|
8370
9127
|
server.tool(
|
|
8371
9128
|
"get_performance_insights",
|
|
8372
|
-
"Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
|
|
9129
|
+
"Query project-scoped performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
|
|
8373
9130
|
{
|
|
8374
9131
|
insight_type: z9.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
|
|
8375
9132
|
days: z9.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
|
|
8376
9133
|
limit: z9.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
9134
|
+
project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8377
9135
|
response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8378
9136
|
},
|
|
8379
|
-
async ({ insight_type, days, limit, response_format }) => {
|
|
9137
|
+
async ({ insight_type, days, limit, project_id, response_format }) => {
|
|
8380
9138
|
const format = response_format ?? "text";
|
|
8381
9139
|
const lookbackDays = days ?? 30;
|
|
8382
9140
|
const maxRows = limit ?? 10;
|
|
8383
9141
|
const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
|
|
9142
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
8384
9143
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
8385
9144
|
action: "performance-insights",
|
|
8386
9145
|
days: effectiveDays,
|
|
8387
|
-
limit: maxRows
|
|
9146
|
+
limit: maxRows,
|
|
9147
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
8388
9148
|
});
|
|
8389
9149
|
if (efError || !result?.success) {
|
|
8390
9150
|
return {
|
|
@@ -8471,19 +9231,22 @@ function registerInsightsTools(server) {
|
|
|
8471
9231
|
);
|
|
8472
9232
|
server.tool(
|
|
8473
9233
|
"get_best_posting_times",
|
|
8474
|
-
"Analyze post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
|
|
9234
|
+
"Analyze project-scoped post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
|
|
8475
9235
|
{
|
|
8476
9236
|
platform: z9.enum(PLATFORM_ENUM).optional().describe("Filter to a specific platform."),
|
|
8477
9237
|
days: z9.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
|
|
9238
|
+
project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
8478
9239
|
response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
8479
9240
|
},
|
|
8480
|
-
async ({ platform: platform3, days, response_format }) => {
|
|
9241
|
+
async ({ platform: platform3, days, project_id, response_format }) => {
|
|
8481
9242
|
const format = response_format ?? "text";
|
|
8482
9243
|
const lookbackDays = days ?? 30;
|
|
9244
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
8483
9245
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
8484
9246
|
action: "best-posting-times",
|
|
8485
9247
|
days: lookbackDays,
|
|
8486
|
-
platform: platform3 ?? void 0
|
|
9248
|
+
platform: platform3 ?? void 0,
|
|
9249
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
8487
9250
|
});
|
|
8488
9251
|
if (efError || !result?.success) {
|
|
8489
9252
|
return {
|
|
@@ -8597,6 +9360,7 @@ var init_insights = __esm({
|
|
|
8597
9360
|
"src/tools/insights.ts"() {
|
|
8598
9361
|
"use strict";
|
|
8599
9362
|
init_edge_function();
|
|
9363
|
+
init_supabase();
|
|
8600
9364
|
init_version();
|
|
8601
9365
|
MAX_INSIGHT_AGE_DAYS = 30;
|
|
8602
9366
|
PLATFORM_ENUM = [
|
|
@@ -9640,7 +10404,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
9640
10404
|
}
|
|
9641
10405
|
const statusData = {
|
|
9642
10406
|
activeConfigs: result?.activeConfigs ?? 0,
|
|
9643
|
-
recentRuns: [],
|
|
10407
|
+
recentRuns: result?.recentRuns ?? result?.recent_runs ?? [],
|
|
9644
10408
|
pendingApprovals: result?.pendingApprovals ?? 0
|
|
9645
10409
|
};
|
|
9646
10410
|
if (format === "json") {
|
|
@@ -9662,8 +10426,20 @@ ${"=".repeat(40)}
|
|
|
9662
10426
|
text += `Pending Approvals: ${statusData.pendingApprovals}
|
|
9663
10427
|
|
|
9664
10428
|
`;
|
|
9665
|
-
|
|
10429
|
+
if (statusData.recentRuns.length === 0) {
|
|
10430
|
+
text += `No recent runs.
|
|
9666
10431
|
`;
|
|
10432
|
+
} else {
|
|
10433
|
+
text += `Recent Runs (${statusData.recentRuns.length}):
|
|
10434
|
+
`;
|
|
10435
|
+
for (const run of statusData.recentRuns.slice(0, 10)) {
|
|
10436
|
+
const id = String(run.id ?? run.run_id ?? "unknown");
|
|
10437
|
+
const status = String(run.status ?? "unknown");
|
|
10438
|
+
const credits = run.credits_used ?? run.creditsUsed;
|
|
10439
|
+
text += ` ${id}: ${status}${credits == null ? "" : ` (${String(credits)} credits)`}
|
|
10440
|
+
`;
|
|
10441
|
+
}
|
|
10442
|
+
}
|
|
9667
10443
|
return {
|
|
9668
10444
|
content: [{ type: "text", text }]
|
|
9669
10445
|
};
|
|
@@ -11845,7 +12621,9 @@ function toolKnowledgeDocument(tool) {
|
|
|
11845
12621
|
function getKnowledgeDocuments() {
|
|
11846
12622
|
return [
|
|
11847
12623
|
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
11848
|
-
...TOOL_CATALOG.filter((t) => !t.internal).map(
|
|
12624
|
+
...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
|
|
12625
|
+
toolKnowledgeDocument
|
|
12626
|
+
)
|
|
11849
12627
|
];
|
|
11850
12628
|
}
|
|
11851
12629
|
function tokenize(input) {
|
|
@@ -11962,7 +12740,7 @@ function registerDiscoveryTools(server) {
|
|
|
11962
12740
|
if (query) {
|
|
11963
12741
|
results = searchTools(query);
|
|
11964
12742
|
}
|
|
11965
|
-
results = results.filter((t) => !t.internal);
|
|
12743
|
+
results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
|
|
11966
12744
|
if (module) {
|
|
11967
12745
|
const moduleTools = getToolsByModule(module);
|
|
11968
12746
|
results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
|
|
@@ -13893,11 +14671,11 @@ function hexToLab(hex) {
|
|
|
13893
14671
|
const lb = srgbToLinear(b);
|
|
13894
14672
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
13895
14673
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
13896
|
-
const
|
|
14674
|
+
const z40 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
13897
14675
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
13898
14676
|
const fx = f(x / 0.95047);
|
|
13899
14677
|
const fy = f(y / 1);
|
|
13900
|
-
const fz = f(
|
|
14678
|
+
const fz = f(z40 / 1.08883);
|
|
13901
14679
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
13902
14680
|
}
|
|
13903
14681
|
function deltaE2000(lab1, lab2) {
|
|
@@ -14137,10 +14915,10 @@ function registerBrandRuntimeTools(server) {
|
|
|
14137
14915
|
pagesScraped: meta.pagesScraped || 0
|
|
14138
14916
|
}
|
|
14139
14917
|
};
|
|
14140
|
-
const
|
|
14918
|
+
const envelope2 = asEnvelope23(runtime);
|
|
14141
14919
|
return {
|
|
14142
|
-
structuredContent:
|
|
14143
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
14920
|
+
structuredContent: envelope2,
|
|
14921
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14144
14922
|
};
|
|
14145
14923
|
}
|
|
14146
14924
|
);
|
|
@@ -14280,9 +15058,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
14280
15058
|
}
|
|
14281
15059
|
const profile = row.profile_data;
|
|
14282
15060
|
const checkResult = computeBrandConsistency(content, profile);
|
|
14283
|
-
const
|
|
15061
|
+
const envelope2 = asEnvelope23(checkResult);
|
|
14284
15062
|
return {
|
|
14285
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15063
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14286
15064
|
};
|
|
14287
15065
|
}
|
|
14288
15066
|
);
|
|
@@ -14314,9 +15092,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
14314
15092
|
content_colors,
|
|
14315
15093
|
threshold ?? 10
|
|
14316
15094
|
);
|
|
14317
|
-
const
|
|
15095
|
+
const envelope2 = asEnvelope23(auditResult);
|
|
14318
15096
|
return {
|
|
14319
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15097
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14320
15098
|
};
|
|
14321
15099
|
}
|
|
14322
15100
|
);
|
|
@@ -14349,9 +15127,9 @@ function registerBrandRuntimeTools(server) {
|
|
|
14349
15127
|
row.profile_data.typography,
|
|
14350
15128
|
format
|
|
14351
15129
|
);
|
|
14352
|
-
const
|
|
15130
|
+
const envelope2 = asEnvelope23({ format, tokens: output });
|
|
14353
15131
|
return {
|
|
14354
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
15132
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
14355
15133
|
};
|
|
14356
15134
|
}
|
|
14357
15135
|
);
|
|
@@ -14505,7 +15283,7 @@ function registerCarouselTools(server) {
|
|
|
14505
15283
|
};
|
|
14506
15284
|
}
|
|
14507
15285
|
const userId = await getDefaultUserId();
|
|
14508
|
-
const rateLimit = checkRateLimit("
|
|
15286
|
+
const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
|
|
14509
15287
|
if (!rateLimit.allowed) {
|
|
14510
15288
|
return {
|
|
14511
15289
|
content: [
|
|
@@ -14577,7 +15355,12 @@ function registerCarouselTools(server) {
|
|
|
14577
15355
|
slideNumber: slide.slideNumber,
|
|
14578
15356
|
jobId: null,
|
|
14579
15357
|
model: image_model,
|
|
14580
|
-
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
|
|
14581
15364
|
};
|
|
14582
15365
|
}
|
|
14583
15366
|
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
@@ -14589,14 +15372,24 @@ function registerCarouselTools(server) {
|
|
|
14589
15372
|
slideNumber: slide.slideNumber,
|
|
14590
15373
|
jobId,
|
|
14591
15374
|
model: image_model,
|
|
14592
|
-
error: null
|
|
15375
|
+
error: null,
|
|
15376
|
+
creditsReserved: 0,
|
|
15377
|
+
creditsCharged: data.creditsDeducted ?? perImageCost,
|
|
15378
|
+
creditsRefunded: 0,
|
|
15379
|
+
billingStatus: "charged",
|
|
15380
|
+
failureReason: null
|
|
14593
15381
|
};
|
|
14594
15382
|
} catch (err) {
|
|
14595
15383
|
return {
|
|
14596
15384
|
slideNumber: slide.slideNumber,
|
|
14597
15385
|
jobId: null,
|
|
14598
15386
|
model: image_model,
|
|
14599
|
-
error: sanitizeError(err)
|
|
15387
|
+
error: sanitizeError(err),
|
|
15388
|
+
creditsReserved: null,
|
|
15389
|
+
creditsCharged: null,
|
|
15390
|
+
creditsRefunded: null,
|
|
15391
|
+
billingStatus: "unknown",
|
|
15392
|
+
failureReason: null
|
|
14600
15393
|
};
|
|
14601
15394
|
}
|
|
14602
15395
|
})
|
|
@@ -14621,7 +15414,8 @@ function registerCarouselTools(server) {
|
|
|
14621
15414
|
return {
|
|
14622
15415
|
...s,
|
|
14623
15416
|
imageJobId: job?.jobId ?? null,
|
|
14624
|
-
imageError: job?.error ?? null
|
|
15417
|
+
imageError: job?.error ?? null,
|
|
15418
|
+
imageBillingStatus: job?.billingStatus ?? "unknown"
|
|
14625
15419
|
};
|
|
14626
15420
|
}),
|
|
14627
15421
|
imageModel: image_model,
|
|
@@ -14633,11 +15427,19 @@ function registerCarouselTools(server) {
|
|
|
14633
15427
|
jobIds: successfulJobs.map((j) => j.jobId),
|
|
14634
15428
|
failedSlides: failedJobs.map((j) => ({
|
|
14635
15429
|
slideNumber: j.slideNumber,
|
|
14636
|
-
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
|
|
14637
15436
|
})),
|
|
14638
15437
|
credits: {
|
|
14639
15438
|
textGeneration: textCredits,
|
|
14640
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,
|
|
14641
15443
|
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
14642
15444
|
}
|
|
14643
15445
|
}
|
|
@@ -14833,7 +15635,7 @@ function registerHyperframesTools(server) {
|
|
|
14833
15635
|
);
|
|
14834
15636
|
server.tool(
|
|
14835
15637
|
"render_hyperframes",
|
|
14836
|
-
"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.",
|
|
14837
15639
|
{
|
|
14838
15640
|
composition_html: z30.string().max(5e5).optional().describe(
|
|
14839
15641
|
"Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
|
|
@@ -14845,7 +15647,9 @@ function registerHyperframesTools(server) {
|
|
|
14845
15647
|
aspect_ratio: z30.enum(["9:16", "16:9", "1:1"]).optional().describe('Output aspect ratio. Default "9:16".'),
|
|
14846
15648
|
duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
|
|
14847
15649
|
fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
|
|
14848
|
-
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master.")
|
|
15650
|
+
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
|
|
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.")
|
|
14849
15653
|
},
|
|
14850
15654
|
async ({
|
|
14851
15655
|
composition_html,
|
|
@@ -14854,7 +15658,9 @@ function registerHyperframesTools(server) {
|
|
|
14854
15658
|
aspect_ratio,
|
|
14855
15659
|
duration_sec,
|
|
14856
15660
|
fps,
|
|
14857
|
-
quality
|
|
15661
|
+
quality,
|
|
15662
|
+
project_id,
|
|
15663
|
+
response_format
|
|
14858
15664
|
}) => {
|
|
14859
15665
|
const userId = await getDefaultUserId();
|
|
14860
15666
|
const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
|
|
@@ -14899,23 +15705,36 @@ function registerHyperframesTools(server) {
|
|
|
14899
15705
|
aspectRatio: aspect_ratio || "9:16",
|
|
14900
15706
|
durationSec: duration_sec,
|
|
14901
15707
|
fps: fps || 30,
|
|
14902
|
-
quality: quality || "standard"
|
|
15708
|
+
quality: quality || "standard",
|
|
15709
|
+
...project_id && { projectId: project_id }
|
|
14903
15710
|
});
|
|
14904
15711
|
if (error || !data?.jobId) {
|
|
14905
15712
|
throw new Error(error || data?.error || "Failed to create Hyperframes render job");
|
|
14906
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
|
+
};
|
|
14907
15726
|
return {
|
|
14908
15727
|
content: [
|
|
14909
15728
|
{
|
|
14910
15729
|
type: "text",
|
|
14911
|
-
text: [
|
|
15730
|
+
text: response_format === "json" ? JSON.stringify({ data: payload }) : [
|
|
14912
15731
|
`Hyperframes render job queued.`,
|
|
14913
15732
|
` Job ID: ${data.jobId}`,
|
|
14914
15733
|
` Credits: ${data.creditsCost}`,
|
|
14915
15734
|
` Duration: ${duration_sec}s @ ${fps || 30}fps (${aspect_ratio || "9:16"})`,
|
|
14916
15735
|
` Quality: ${quality || "standard"}`,
|
|
14917
15736
|
``,
|
|
14918
|
-
`Poll with check_status
|
|
15737
|
+
`Poll with check_status.`
|
|
14919
15738
|
].join("\n")
|
|
14920
15739
|
}
|
|
14921
15740
|
]
|
|
@@ -14925,7 +15744,7 @@ function registerHyperframesTools(server) {
|
|
|
14925
15744
|
content: [
|
|
14926
15745
|
{
|
|
14927
15746
|
type: "text",
|
|
14928
|
-
text: `Failed to queue Hyperframes render: ${err
|
|
15747
|
+
text: `Failed to queue Hyperframes render: ${sanitizeError(err)}`
|
|
14929
15748
|
}
|
|
14930
15749
|
],
|
|
14931
15750
|
isError: true
|
|
@@ -14941,6 +15760,7 @@ var init_hyperframes = __esm({
|
|
|
14941
15760
|
init_rate_limit();
|
|
14942
15761
|
init_supabase();
|
|
14943
15762
|
init_edge_function();
|
|
15763
|
+
init_sanitize_error();
|
|
14944
15764
|
HYPERFRAMES_BLOCKS = [
|
|
14945
15765
|
// Transitions
|
|
14946
15766
|
{
|
|
@@ -15058,49 +15878,122 @@ import {
|
|
|
15058
15878
|
import { z as z31 } from "zod";
|
|
15059
15879
|
import fs from "node:fs/promises";
|
|
15060
15880
|
import path from "node:path";
|
|
15881
|
+
import { fileURLToPath } from "node:url";
|
|
15061
15882
|
function startOfCurrentWeekMonday() {
|
|
15062
15883
|
const now = /* @__PURE__ */ new Date();
|
|
15063
|
-
const day = now.
|
|
15884
|
+
const day = now.getUTCDay();
|
|
15064
15885
|
const monday = new Date(now);
|
|
15065
|
-
monday.setUTCDate(now.getUTCDate() - day +
|
|
15886
|
+
monday.setUTCDate(now.getUTCDate() - (day + 6) % 7);
|
|
15887
|
+
monday.setUTCHours(0, 0, 0, 0);
|
|
15066
15888
|
return monday.toISOString().split("T")[0];
|
|
15067
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
|
+
}
|
|
15068
15940
|
function registerContentCalendarApp(server) {
|
|
15069
15941
|
registerAppTool(
|
|
15070
15942
|
server,
|
|
15071
15943
|
"open_content_calendar",
|
|
15072
15944
|
{
|
|
15073
15945
|
title: "Content Calendar",
|
|
15074
|
-
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.",
|
|
15075
15947
|
inputSchema: {
|
|
15076
|
-
|
|
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(
|
|
15077
15952
|
"ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
|
|
15078
15953
|
)
|
|
15079
15954
|
},
|
|
15080
15955
|
outputSchema: {
|
|
15081
15956
|
start_date: z31.string(),
|
|
15957
|
+
project_id: z31.string(),
|
|
15082
15958
|
posts: z31.array(RecentPostOutputSchema),
|
|
15083
15959
|
scopes: z31.array(z31.string())
|
|
15084
15960
|
},
|
|
15085
15961
|
_meta: {
|
|
15086
15962
|
ui: {
|
|
15087
|
-
resourceUri: CALENDAR_URI
|
|
15088
|
-
csp: {
|
|
15089
|
-
"img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
|
|
15090
|
-
"connect-src": ["'self'"]
|
|
15091
|
-
}
|
|
15963
|
+
resourceUri: CALENDAR_URI
|
|
15092
15964
|
}
|
|
15093
15965
|
}
|
|
15094
15966
|
},
|
|
15095
|
-
async ({ start_date }
|
|
15096
|
-
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
|
+
}
|
|
15097
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
|
+
}
|
|
15098
15988
|
const { data: result, error } = await callEdgeFunction(
|
|
15099
15989
|
"mcp-data",
|
|
15100
15990
|
{
|
|
15101
|
-
action: "
|
|
15102
|
-
|
|
15103
|
-
|
|
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
|
|
15104
15997
|
},
|
|
15105
15998
|
{ timeoutMs: 15e3 }
|
|
15106
15999
|
);
|
|
@@ -15109,19 +16002,16 @@ function registerContentCalendarApp(server) {
|
|
|
15109
16002
|
content: [
|
|
15110
16003
|
{
|
|
15111
16004
|
type: "text",
|
|
15112
|
-
text:
|
|
16005
|
+
text: "The content calendar could not load posts. Please retry."
|
|
15113
16006
|
}
|
|
15114
16007
|
],
|
|
15115
16008
|
isError: true
|
|
15116
16009
|
};
|
|
15117
16010
|
}
|
|
15118
|
-
const posts = (result.posts ?? []).filter((
|
|
15119
|
-
const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
|
|
15120
|
-
if (!ts) return false;
|
|
15121
|
-
return ts.split("T")[0] >= fromDate;
|
|
15122
|
-
});
|
|
16011
|
+
const posts = (result.posts ?? []).map(publicRecentPost).filter((post) => post !== null);
|
|
15123
16012
|
const structuredContent = {
|
|
15124
16013
|
start_date: fromDate,
|
|
16014
|
+
project_id: resolvedProjectId,
|
|
15125
16015
|
posts,
|
|
15126
16016
|
scopes: userScopes
|
|
15127
16017
|
};
|
|
@@ -15140,37 +16030,60 @@ function registerContentCalendarApp(server) {
|
|
|
15140
16030
|
server,
|
|
15141
16031
|
CALENDAR_URI,
|
|
15142
16032
|
CALENDAR_URI,
|
|
15143
|
-
{
|
|
16033
|
+
{
|
|
16034
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
16035
|
+
description: "Self-contained Social Neuron project content calendar.",
|
|
16036
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
16037
|
+
},
|
|
15144
16038
|
async () => {
|
|
15145
|
-
const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
|
|
15146
16039
|
try {
|
|
15147
|
-
const html = await
|
|
16040
|
+
const html = await readCalendarHtml();
|
|
15148
16041
|
return {
|
|
15149
|
-
contents: [
|
|
16042
|
+
contents: [
|
|
16043
|
+
{
|
|
16044
|
+
uri: CALENDAR_URI,
|
|
16045
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
16046
|
+
text: html,
|
|
16047
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
16048
|
+
}
|
|
16049
|
+
]
|
|
15150
16050
|
};
|
|
15151
|
-
} catch
|
|
16051
|
+
} catch {
|
|
15152
16052
|
const errorHtml = `<!DOCTYPE html>
|
|
15153
16053
|
<html><head><title>Content Calendar \u2014 unavailable</title></head>
|
|
15154
16054
|
<body style="font-family:sans-serif;padding:24px;color:#444;">
|
|
15155
16055
|
<h2>Content Calendar app bundle missing</h2>
|
|
15156
|
-
<p>The
|
|
15157
|
-
<code>apps/content-calendar/dist/mcp-app.html</code> is not built.
|
|
15158
|
-
Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
|
|
15159
|
-
<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>
|
|
15160
16057
|
</body></html>`;
|
|
15161
16058
|
return {
|
|
15162
|
-
contents: [
|
|
16059
|
+
contents: [
|
|
16060
|
+
{
|
|
16061
|
+
uri: CALENDAR_URI,
|
|
16062
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
16063
|
+
text: errorHtml,
|
|
16064
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
16065
|
+
}
|
|
16066
|
+
]
|
|
15163
16067
|
};
|
|
15164
16068
|
}
|
|
15165
16069
|
}
|
|
15166
16070
|
);
|
|
15167
16071
|
}
|
|
15168
|
-
var CALENDAR_URI, RecentPostOutputSchema;
|
|
16072
|
+
var CALENDAR_URI, CALENDAR_CSP, RecentPostOutputSchema;
|
|
15169
16073
|
var init_content_calendar = __esm({
|
|
15170
16074
|
"src/apps/content-calendar.ts"() {
|
|
15171
16075
|
"use strict";
|
|
15172
16076
|
init_edge_function();
|
|
15173
|
-
|
|
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
|
+
};
|
|
15174
16087
|
RecentPostOutputSchema = z31.object({
|
|
15175
16088
|
id: z31.string(),
|
|
15176
16089
|
platform: z31.string(),
|
|
@@ -15184,8 +16097,257 @@ var init_content_calendar = __esm({
|
|
|
15184
16097
|
}
|
|
15185
16098
|
});
|
|
15186
16099
|
|
|
15187
|
-
// 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";
|
|
15188
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";
|
|
15189
16351
|
function findActiveAccount(accounts, platform3) {
|
|
15190
16352
|
const target = platform3.toLowerCase();
|
|
15191
16353
|
return accounts.find((a) => {
|
|
@@ -15198,16 +16360,16 @@ function registerConnectionTools(server) {
|
|
|
15198
16360
|
"start_platform_connection",
|
|
15199
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.",
|
|
15200
16362
|
{
|
|
15201
|
-
platform:
|
|
15202
|
-
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(
|
|
15203
16365
|
"Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
|
|
15204
16366
|
),
|
|
15205
|
-
response_format:
|
|
16367
|
+
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
15206
16368
|
},
|
|
15207
16369
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
15208
16370
|
const format = response_format ?? "text";
|
|
15209
16371
|
const userId = await getDefaultUserId();
|
|
15210
|
-
const rl = checkRateLimit("
|
|
16372
|
+
const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
|
|
15211
16373
|
if (!rl.allowed) {
|
|
15212
16374
|
return {
|
|
15213
16375
|
content: [
|
|
@@ -15287,13 +16449,13 @@ function registerConnectionTools(server) {
|
|
|
15287
16449
|
"wait_for_connection",
|
|
15288
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.",
|
|
15289
16451
|
{
|
|
15290
|
-
platform:
|
|
15291
|
-
project_id:
|
|
16452
|
+
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
16453
|
+
project_id: z33.string().optional().describe(
|
|
15292
16454
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
15293
16455
|
),
|
|
15294
|
-
timeout_s:
|
|
15295
|
-
poll_interval_s:
|
|
15296
|
-
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.")
|
|
15297
16459
|
},
|
|
15298
16460
|
async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
|
|
15299
16461
|
const format = response_format ?? "text";
|
|
@@ -15449,7 +16611,7 @@ var init_connections = __esm({
|
|
|
15449
16611
|
});
|
|
15450
16612
|
|
|
15451
16613
|
// src/tools/harness.ts
|
|
15452
|
-
import { z as
|
|
16614
|
+
import { z as z34 } from "zod";
|
|
15453
16615
|
function registerHarnessTools(server, _ctx) {
|
|
15454
16616
|
server.tool(
|
|
15455
16617
|
"write_agent_reflection",
|
|
@@ -15554,38 +16716,38 @@ var init_harness = __esm({
|
|
|
15554
16716
|
"engager"
|
|
15555
16717
|
];
|
|
15556
16718
|
writeReflectionSchema = {
|
|
15557
|
-
reflection_text:
|
|
15558
|
-
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(
|
|
15559
16721
|
"Which agent produced this reflection. Must be one of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
15560
16722
|
),
|
|
15561
|
-
provenance:
|
|
15562
|
-
content_history_id:
|
|
15563
|
-
outcome_event_id:
|
|
15564
|
-
prm_score_ids:
|
|
15565
|
-
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.")
|
|
15566
16728
|
}).strict().describe(
|
|
15567
16729
|
"Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
|
|
15568
16730
|
),
|
|
15569
|
-
brand_id:
|
|
15570
|
-
pipeline_id:
|
|
15571
|
-
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.")
|
|
15572
16734
|
};
|
|
15573
16735
|
readReflectionSchema = {
|
|
15574
|
-
brand_id:
|
|
15575
|
-
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(
|
|
15576
16738
|
"Optional filter: only return reflections produced by this agent. One of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
15577
16739
|
),
|
|
15578
|
-
limit:
|
|
16740
|
+
limit: z34.number().int().min(1).max(100).optional().describe(
|
|
15579
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)."
|
|
15580
16742
|
)
|
|
15581
16743
|
};
|
|
15582
16744
|
recordOutcomeSchema = {
|
|
15583
|
-
decision_event_id:
|
|
15584
|
-
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(
|
|
15585
16747
|
"Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
|
|
15586
16748
|
),
|
|
15587
|
-
reward:
|
|
15588
|
-
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(
|
|
15589
16751
|
'Optional map of raw metric name \u2192 value (e.g. {"likes": 42, "reach": 1200}). Stored verbatim; not used for learning directly.'
|
|
15590
16752
|
)
|
|
15591
16753
|
};
|
|
@@ -15593,7 +16755,7 @@ var init_harness = __esm({
|
|
|
15593
16755
|
});
|
|
15594
16756
|
|
|
15595
16757
|
// src/tools/hermes.ts
|
|
15596
|
-
import { z as
|
|
16758
|
+
import { z as z35 } from "zod";
|
|
15597
16759
|
function asEnvelope25(data) {
|
|
15598
16760
|
return {
|
|
15599
16761
|
_meta: {
|
|
@@ -15609,12 +16771,12 @@ function registerHermesTools(server) {
|
|
|
15609
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.",
|
|
15610
16772
|
{
|
|
15611
16773
|
platform: PLATFORM.describe("Target platform for the draft."),
|
|
15612
|
-
copy:
|
|
15613
|
-
project_id:
|
|
15614
|
-
media_url:
|
|
15615
|
-
hermes_run_id:
|
|
15616
|
-
source_intel_ids:
|
|
15617
|
-
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()
|
|
15618
16780
|
},
|
|
15619
16781
|
async ({ platform: platform3, copy, project_id, media_url, hermes_run_id, response_format }) => {
|
|
15620
16782
|
const { data, error } = await callEdgeFunction(
|
|
@@ -15652,15 +16814,15 @@ function registerHermesTools(server) {
|
|
|
15652
16814
|
"record_voice_lesson",
|
|
15653
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)".',
|
|
15654
16816
|
{
|
|
15655
|
-
project_id:
|
|
15656
|
-
lesson:
|
|
15657
|
-
evidence:
|
|
15658
|
-
engagement_lift_pct:
|
|
15659
|
-
sample_size:
|
|
15660
|
-
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.")
|
|
15661
16823
|
}).describe("Quantitative evidence backing the lesson."),
|
|
15662
|
-
applies_to:
|
|
15663
|
-
response_format:
|
|
16824
|
+
applies_to: z35.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
|
|
16825
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15664
16826
|
},
|
|
15665
16827
|
async ({ project_id, lesson, evidence, applies_to, response_format }) => {
|
|
15666
16828
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -15696,10 +16858,10 @@ function registerHermesTools(server) {
|
|
|
15696
16858
|
"record_observation",
|
|
15697
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.',
|
|
15698
16860
|
{
|
|
15699
|
-
summary:
|
|
15700
|
-
deltas:
|
|
15701
|
-
run_id:
|
|
15702
|
-
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()
|
|
15703
16865
|
},
|
|
15704
16866
|
async ({ summary, deltas, run_id, response_format }) => {
|
|
15705
16867
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "record-observation", summary, deltas, run_id });
|
|
@@ -15726,13 +16888,13 @@ function registerHermesTools(server) {
|
|
|
15726
16888
|
"record_intel_signal",
|
|
15727
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.",
|
|
15728
16890
|
{
|
|
15729
|
-
source:
|
|
15730
|
-
url:
|
|
15731
|
-
topic:
|
|
15732
|
-
title:
|
|
15733
|
-
summary:
|
|
15734
|
-
score:
|
|
15735
|
-
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()
|
|
15736
16898
|
},
|
|
15737
16899
|
async ({ source, url, topic, title, summary, score, response_format }) => {
|
|
15738
16900
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -15770,16 +16932,16 @@ function registerHermesTools(server) {
|
|
|
15770
16932
|
"record_campaign_spend",
|
|
15771
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.",
|
|
15772
16934
|
{
|
|
15773
|
-
campaign_id:
|
|
15774
|
-
category:
|
|
16935
|
+
campaign_id: z35.string().min(1).max(200).describe("Campaign identifier (slug)."),
|
|
16936
|
+
category: z35.enum([
|
|
15775
16937
|
"hermes_drafts",
|
|
15776
16938
|
"carousel_renders",
|
|
15777
16939
|
"analytics_pulls",
|
|
15778
16940
|
"paid_amplification",
|
|
15779
16941
|
"other"
|
|
15780
16942
|
]).describe("Cost category."),
|
|
15781
|
-
amount_usd:
|
|
15782
|
-
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()
|
|
15783
16945
|
},
|
|
15784
16946
|
async ({ campaign_id, category, amount_usd, response_format }) => {
|
|
15785
16947
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -15814,7 +16976,7 @@ function registerHermesTools(server) {
|
|
|
15814
16976
|
"get_active_campaigns",
|
|
15815
16977
|
"List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
|
|
15816
16978
|
{
|
|
15817
|
-
response_format:
|
|
16979
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
15818
16980
|
},
|
|
15819
16981
|
async ({ response_format }) => {
|
|
15820
16982
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "get-active-campaigns" });
|
|
@@ -15863,7 +17025,7 @@ var init_hermes = __esm({
|
|
|
15863
17025
|
"use strict";
|
|
15864
17026
|
init_edge_function();
|
|
15865
17027
|
init_version();
|
|
15866
|
-
PLATFORM =
|
|
17028
|
+
PLATFORM = z35.enum([
|
|
15867
17029
|
"instagram",
|
|
15868
17030
|
"twitter",
|
|
15869
17031
|
"linkedin",
|
|
@@ -15909,7 +17071,20 @@ var init_skills_manifest = __esm({
|
|
|
15909
17071
|
});
|
|
15910
17072
|
|
|
15911
17073
|
// src/tools/skills.ts
|
|
15912
|
-
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
|
+
}
|
|
15913
17088
|
function asEnvelope26(data) {
|
|
15914
17089
|
return {
|
|
15915
17090
|
_meta: {
|
|
@@ -15937,19 +17112,88 @@ function registerSkillsTools(server) {
|
|
|
15937
17112
|
"list_skills",
|
|
15938
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.',
|
|
15939
17114
|
{
|
|
15940
|
-
studio:
|
|
15941
|
-
|
|
15942
|
-
|
|
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
|
+
)
|
|
15943
17124
|
},
|
|
15944
17125
|
async ({ studio, featured_only, response_format }) => {
|
|
15945
|
-
const skills = listSkills({ studio, featuredOnly: featured_only });
|
|
15946
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 });
|
|
15947
17187
|
if (format === "json") {
|
|
15948
17188
|
return {
|
|
15949
17189
|
content: [
|
|
15950
17190
|
{
|
|
15951
17191
|
type: "text",
|
|
15952
|
-
text: JSON.stringify(
|
|
17192
|
+
text: JSON.stringify(
|
|
17193
|
+
asEnvelope26({ count: skills.length, skills }),
|
|
17194
|
+
null,
|
|
17195
|
+
2
|
|
17196
|
+
)
|
|
15953
17197
|
}
|
|
15954
17198
|
]
|
|
15955
17199
|
};
|
|
@@ -15959,7 +17203,9 @@ function registerSkillsTools(server) {
|
|
|
15959
17203
|
content: [
|
|
15960
17204
|
{
|
|
15961
17205
|
type: "text",
|
|
15962
|
-
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
|
+
)
|
|
15963
17209
|
}
|
|
15964
17210
|
]
|
|
15965
17211
|
};
|
|
@@ -15978,18 +17224,22 @@ ${blocks}` }]
|
|
|
15978
17224
|
"run_skill",
|
|
15979
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.",
|
|
15980
17226
|
{
|
|
15981
|
-
skill_id:
|
|
17227
|
+
skill_id: z36.string().describe(
|
|
15982
17228
|
'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
|
|
15983
17229
|
),
|
|
15984
|
-
topic:
|
|
15985
|
-
|
|
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(
|
|
15986
17234
|
'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
|
|
15987
17235
|
),
|
|
15988
|
-
hook:
|
|
17236
|
+
hook: z36.string().optional().describe(
|
|
15989
17237
|
"Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
|
|
15990
17238
|
),
|
|
15991
|
-
cta:
|
|
15992
|
-
|
|
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.")
|
|
15993
17243
|
},
|
|
15994
17244
|
async ({ skill_id, topic, audience, hook, cta, response_format }) => {
|
|
15995
17245
|
const skill = getSkill(skill_id);
|
|
@@ -16057,19 +17307,96 @@ ${blocks}` }]
|
|
|
16057
17307
|
};
|
|
16058
17308
|
}
|
|
16059
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
|
+
);
|
|
16060
17378
|
}
|
|
16061
17379
|
var STUDIO_VALUES;
|
|
16062
17380
|
var init_skills = __esm({
|
|
16063
17381
|
"src/tools/skills.ts"() {
|
|
16064
17382
|
"use strict";
|
|
16065
17383
|
init_version();
|
|
17384
|
+
init_tool_error();
|
|
17385
|
+
init_edge_function();
|
|
16066
17386
|
init_skills_manifest();
|
|
16067
|
-
STUDIO_VALUES = [
|
|
17387
|
+
STUDIO_VALUES = [
|
|
17388
|
+
"video",
|
|
17389
|
+
"avatar",
|
|
17390
|
+
"carousel",
|
|
17391
|
+
"voice",
|
|
17392
|
+
"caption",
|
|
17393
|
+
"edit"
|
|
17394
|
+
];
|
|
16068
17395
|
}
|
|
16069
17396
|
});
|
|
16070
17397
|
|
|
16071
17398
|
// src/tools/loopPulse.ts
|
|
16072
|
-
import { z as
|
|
17399
|
+
import { z as z37 } from "zod";
|
|
16073
17400
|
function asEnvelope27(data) {
|
|
16074
17401
|
return {
|
|
16075
17402
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -16081,7 +17408,7 @@ function registerLoopPulseTools(server) {
|
|
|
16081
17408
|
"get_loop_pulse",
|
|
16082
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.',
|
|
16083
17410
|
{
|
|
16084
|
-
response_format:
|
|
17411
|
+
response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
16085
17412
|
},
|
|
16086
17413
|
async ({ response_format }) => {
|
|
16087
17414
|
const format = response_format ?? "text";
|
|
@@ -16131,7 +17458,7 @@ var init_loopPulse = __esm({
|
|
|
16131
17458
|
});
|
|
16132
17459
|
|
|
16133
17460
|
// src/tools/banditState.ts
|
|
16134
|
-
import { z as
|
|
17461
|
+
import { z as z38 } from "zod";
|
|
16135
17462
|
function asEnvelope28(data) {
|
|
16136
17463
|
return {
|
|
16137
17464
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -16143,8 +17470,8 @@ function registerBanditStateTools(server) {
|
|
|
16143
17470
|
"get_bandit_state",
|
|
16144
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).",
|
|
16145
17472
|
{
|
|
16146
|
-
project_id:
|
|
16147
|
-
platform:
|
|
17473
|
+
project_id: z38.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
|
|
17474
|
+
platform: z38.enum([
|
|
16148
17475
|
"instagram",
|
|
16149
17476
|
"tiktok",
|
|
16150
17477
|
"youtube",
|
|
@@ -16156,7 +17483,7 @@ function registerBanditStateTools(server) {
|
|
|
16156
17483
|
]).optional().describe(
|
|
16157
17484
|
"Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
|
|
16158
17485
|
),
|
|
16159
|
-
arm_type:
|
|
17486
|
+
arm_type: z38.enum([
|
|
16160
17487
|
"hook_family",
|
|
16161
17488
|
"hook_type",
|
|
16162
17489
|
"format",
|
|
@@ -16170,8 +17497,8 @@ function registerBanditStateTools(server) {
|
|
|
16170
17497
|
"posting_time_bucket",
|
|
16171
17498
|
"content_format"
|
|
16172
17499
|
]).optional().describe("Arm dimension. Omit to return all types grouped."),
|
|
16173
|
-
top_k:
|
|
16174
|
-
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.")
|
|
16175
17502
|
},
|
|
16176
17503
|
async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
|
|
16177
17504
|
const format = response_format ?? "text";
|
|
@@ -16234,12 +17561,134 @@ var init_banditState = __esm({
|
|
|
16234
17561
|
}
|
|
16235
17562
|
});
|
|
16236
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
|
+
|
|
16237
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
|
+
}
|
|
16238
17686
|
function wrapToolWithScanner(toolName, handler) {
|
|
16239
17687
|
return async function scannerWrappedHandler(...handlerArgs) {
|
|
16240
17688
|
const args = handlerArgs[0];
|
|
16241
17689
|
const ctx = handlerArgs[1];
|
|
16242
|
-
const
|
|
17690
|
+
const scanArgs = redactBase64Payloads(args);
|
|
17691
|
+
const inputText = scanArgs === void 0 ? "{}" : typeof scanArgs === "string" ? scanArgs : JSON.stringify(scanArgs);
|
|
16243
17692
|
const inputScan = scan(inputText, {
|
|
16244
17693
|
mode: "block",
|
|
16245
17694
|
source: "mcp_tool_input",
|
|
@@ -16267,6 +17716,16 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
16267
17716
|
source: "mcp_tool_output",
|
|
16268
17717
|
user_id: ctx?.userId
|
|
16269
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
|
+
}
|
|
16270
17729
|
if (outputScan.sanitized_text !== void 0) {
|
|
16271
17730
|
try {
|
|
16272
17731
|
ctx?.logScan?.(toolName, "output", outputScan);
|
|
@@ -16275,7 +17734,10 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
16275
17734
|
try {
|
|
16276
17735
|
return JSON.parse(outputScan.sanitized_text);
|
|
16277
17736
|
} catch {
|
|
16278
|
-
return
|
|
17737
|
+
return toolError(
|
|
17738
|
+
"server_error",
|
|
17739
|
+
"The response could not be returned safely. Please retry or contact support."
|
|
17740
|
+
);
|
|
16279
17741
|
}
|
|
16280
17742
|
}
|
|
16281
17743
|
return result;
|
|
@@ -16325,8 +17787,7 @@ function applyScopeEnforcement(server, scopeResolver) {
|
|
|
16325
17787
|
details: {
|
|
16326
17788
|
source: "wrapper",
|
|
16327
17789
|
// A thrown exception escaped the handler — an unclassified fault.
|
|
16328
|
-
error_type: "server_error"
|
|
16329
|
-
exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
|
|
17790
|
+
error_type: "server_error"
|
|
16330
17791
|
}
|
|
16331
17792
|
});
|
|
16332
17793
|
throw err;
|
|
@@ -16458,12 +17919,14 @@ function registerAllTools(server, options) {
|
|
|
16458
17919
|
registerSkillsTools(server);
|
|
16459
17920
|
registerLoopPulseTools(server);
|
|
16460
17921
|
registerBanditStateTools(server);
|
|
17922
|
+
registerLifecycleTools(server);
|
|
16461
17923
|
if (!options?.skipApps) {
|
|
16462
17924
|
registerContentCalendarApp(server);
|
|
17925
|
+
registerAnalyticsPulseApp(server);
|
|
16463
17926
|
}
|
|
16464
17927
|
applyAnnotations(server);
|
|
16465
17928
|
}
|
|
16466
|
-
var RESPONSE_CHAR_LIMIT;
|
|
17929
|
+
var BASE64_PAYLOAD_KEYS, DATA_URI_PREFIX2, STRICT_BASE64, RESPONSE_CHAR_LIMIT;
|
|
16467
17930
|
var init_register_tools = __esm({
|
|
16468
17931
|
"src/lib/register-tools.ts"() {
|
|
16469
17932
|
"use strict";
|
|
@@ -16505,12 +17968,17 @@ var init_register_tools = __esm({
|
|
|
16505
17968
|
init_niche_research();
|
|
16506
17969
|
init_hyperframes();
|
|
16507
17970
|
init_content_calendar();
|
|
17971
|
+
init_analytics_pulse();
|
|
16508
17972
|
init_connections();
|
|
16509
17973
|
init_harness();
|
|
16510
17974
|
init_hermes();
|
|
16511
17975
|
init_skills();
|
|
16512
17976
|
init_loopPulse();
|
|
16513
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}$/;
|
|
16514
17982
|
RESPONSE_CHAR_LIMIT = 1e5;
|
|
16515
17983
|
}
|
|
16516
17984
|
});
|
|
@@ -16532,7 +18000,11 @@ function getCallHandler() {
|
|
|
16532
18000
|
return cachedCallHandler;
|
|
16533
18001
|
}
|
|
16534
18002
|
function restToolNames() {
|
|
16535
|
-
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
|
+
);
|
|
16536
18008
|
}
|
|
16537
18009
|
async function invokeToolRest(name, args) {
|
|
16538
18010
|
const handler = getCallHandler();
|
|
@@ -17098,7 +18570,7 @@ function printSnUsage() {
|
|
|
17098
18570
|
console.error("");
|
|
17099
18571
|
console.error("Discovery:");
|
|
17100
18572
|
console.error(" tools [--scope <scope>] [--module <module>] [--json]");
|
|
17101
|
-
console.error(" info [--json]");
|
|
18573
|
+
console.error(" info [--offline] [--json]");
|
|
17102
18574
|
console.error("");
|
|
17103
18575
|
console.error("Content:");
|
|
17104
18576
|
console.error(
|
|
@@ -17231,10 +18703,11 @@ var setup_exports = {};
|
|
|
17231
18703
|
__export(setup_exports, {
|
|
17232
18704
|
generatePKCE: () => generatePKCE,
|
|
17233
18705
|
getAppBaseUrl: () => getAppBaseUrl,
|
|
18706
|
+
isValidSetupApiKey: () => isValidSetupApiKey,
|
|
17234
18707
|
runLogout: () => runLogout,
|
|
17235
18708
|
runSetup: () => runSetup
|
|
17236
18709
|
});
|
|
17237
|
-
import { createHash as
|
|
18710
|
+
import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "node:crypto";
|
|
17238
18711
|
import { createServer } from "node:http";
|
|
17239
18712
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
17240
18713
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
@@ -17243,12 +18716,15 @@ function base64url(buffer) {
|
|
|
17243
18716
|
return buffer.toString("base64url");
|
|
17244
18717
|
}
|
|
17245
18718
|
function generatePKCE() {
|
|
17246
|
-
const verifierBytes =
|
|
18719
|
+
const verifierBytes = randomBytes2(32);
|
|
17247
18720
|
const codeVerifier = base64url(verifierBytes);
|
|
17248
|
-
const challengeHash =
|
|
18721
|
+
const challengeHash = createHash3("sha256").update(codeVerifier).digest();
|
|
17249
18722
|
const codeChallenge = base64url(challengeHash);
|
|
17250
18723
|
return { codeVerifier, codeChallenge };
|
|
17251
18724
|
}
|
|
18725
|
+
function isValidSetupApiKey(value) {
|
|
18726
|
+
return typeof value === "string" && value.length >= 32 && value.length <= 512 && /^snk_live_[A-Za-z0-9_-]+$/.test(value);
|
|
18727
|
+
}
|
|
17252
18728
|
function getAppBaseUrl() {
|
|
17253
18729
|
return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
|
|
17254
18730
|
}
|
|
@@ -17310,11 +18786,24 @@ function configureMcpClient(configPath) {
|
|
|
17310
18786
|
return false;
|
|
17311
18787
|
}
|
|
17312
18788
|
}
|
|
17313
|
-
function readBody(req) {
|
|
18789
|
+
function readBody(req, maxBytes = 16 * 1024) {
|
|
17314
18790
|
return new Promise((resolve3, reject) => {
|
|
17315
18791
|
const chunks = [];
|
|
17316
|
-
|
|
17317
|
-
|
|
18792
|
+
let total = 0;
|
|
18793
|
+
let tooLarge = false;
|
|
18794
|
+
req.on("data", (chunk) => {
|
|
18795
|
+
if (tooLarge) return;
|
|
18796
|
+
total += chunk.length;
|
|
18797
|
+
if (total > maxBytes) {
|
|
18798
|
+
tooLarge = true;
|
|
18799
|
+
reject(new Error("request_too_large"));
|
|
18800
|
+
return;
|
|
18801
|
+
}
|
|
18802
|
+
chunks.push(chunk);
|
|
18803
|
+
});
|
|
18804
|
+
req.on("end", () => {
|
|
18805
|
+
if (!tooLarge) resolve3(Buffer.concat(chunks).toString("utf-8"));
|
|
18806
|
+
});
|
|
17318
18807
|
req.on("error", reject);
|
|
17319
18808
|
});
|
|
17320
18809
|
}
|
|
@@ -17327,14 +18816,13 @@ async function completePkceExchange(codeVerifier, state) {
|
|
|
17327
18816
|
body: JSON.stringify({ code_verifier: codeVerifier, state })
|
|
17328
18817
|
});
|
|
17329
18818
|
if (!response.ok) {
|
|
17330
|
-
|
|
17331
|
-
console.error(` PKCE exchange failed: ${text}`);
|
|
18819
|
+
console.error(` PKCE exchange failed (HTTP ${response.status}).`);
|
|
17332
18820
|
return false;
|
|
17333
18821
|
}
|
|
17334
18822
|
const data = await response.json();
|
|
17335
18823
|
return data.success === true;
|
|
17336
|
-
} catch
|
|
17337
|
-
console.error(
|
|
18824
|
+
} catch {
|
|
18825
|
+
console.error(" PKCE exchange failed because the authentication service was unavailable.");
|
|
17338
18826
|
return false;
|
|
17339
18827
|
}
|
|
17340
18828
|
}
|
|
@@ -17345,7 +18833,8 @@ async function runSetup() {
|
|
|
17345
18833
|
console.error("");
|
|
17346
18834
|
console.error(" Privacy Notice:");
|
|
17347
18835
|
console.error(" - Your API key is stored locally in your OS keychain");
|
|
17348
|
-
console.error(" -
|
|
18836
|
+
console.error(" - CLI telemetry records tool name, status, and duration \u2014 not tool input/output");
|
|
18837
|
+
console.error(" - Service-side content processing follows https://socialneuron.com/privacy");
|
|
17349
18838
|
console.error(" - Set DO_NOT_TRACK=1 to disable telemetry");
|
|
17350
18839
|
console.error(" - Data export/delete: https://www.socialneuron.com/settings");
|
|
17351
18840
|
console.error("");
|
|
@@ -17369,7 +18858,6 @@ async function runSetup() {
|
|
|
17369
18858
|
const open = (await import("open")).default;
|
|
17370
18859
|
await open(authorizeUrl.toString());
|
|
17371
18860
|
console.error(" Opening browser for authorization...");
|
|
17372
|
-
console.error(` URL: ${authorizeUrl.toString()}`);
|
|
17373
18861
|
console.error("");
|
|
17374
18862
|
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
17375
18863
|
} catch {
|
|
@@ -17381,12 +18869,20 @@ async function runSetup() {
|
|
|
17381
18869
|
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
17382
18870
|
}
|
|
17383
18871
|
const result = await new Promise((resolve3) => {
|
|
18872
|
+
const callbackOrigin = new URL(baseUrl).origin;
|
|
17384
18873
|
const timeout = setTimeout(() => {
|
|
17385
18874
|
server.close();
|
|
17386
18875
|
resolve3({ error: "Authorization timed out after 120 seconds." });
|
|
17387
18876
|
}, 12e4);
|
|
17388
18877
|
server.on("request", async (req, res) => {
|
|
17389
|
-
|
|
18878
|
+
const requestOrigin = req.headers.origin;
|
|
18879
|
+
if (requestOrigin !== callbackOrigin) {
|
|
18880
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
18881
|
+
res.end(JSON.stringify({ error: "Origin not allowed" }));
|
|
18882
|
+
return;
|
|
18883
|
+
}
|
|
18884
|
+
res.setHeader("Access-Control-Allow-Origin", callbackOrigin);
|
|
18885
|
+
res.setHeader("Vary", "Origin");
|
|
17390
18886
|
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
17391
18887
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
17392
18888
|
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
@@ -17404,9 +18900,9 @@ async function runSetup() {
|
|
|
17404
18900
|
res.end(JSON.stringify({ error: "State mismatch" }));
|
|
17405
18901
|
return;
|
|
17406
18902
|
}
|
|
17407
|
-
if (!data.api_key) {
|
|
18903
|
+
if (!isValidSetupApiKey(data.api_key)) {
|
|
17408
18904
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
17409
|
-
res.end(JSON.stringify({ error: "
|
|
18905
|
+
res.end(JSON.stringify({ error: "Invalid api_key" }));
|
|
17410
18906
|
return;
|
|
17411
18907
|
}
|
|
17412
18908
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -17448,9 +18944,9 @@ async function runSetup() {
|
|
|
17448
18944
|
console.error(` Key prefix: ${apiKey.substring(0, 12)}...`);
|
|
17449
18945
|
const configPaths = getConfigPaths();
|
|
17450
18946
|
let configured = false;
|
|
17451
|
-
for (const { path:
|
|
17452
|
-
if (configureMcpClient(
|
|
17453
|
-
console.error(` Configured ${name}: ${
|
|
18947
|
+
for (const { path: path3, name } of configPaths) {
|
|
18948
|
+
if (configureMcpClient(path3)) {
|
|
18949
|
+
console.error(` Configured ${name}: ${path3}`);
|
|
17454
18950
|
configured = true;
|
|
17455
18951
|
}
|
|
17456
18952
|
}
|
|
@@ -17489,12 +18985,12 @@ __export(validation_cache_exports, {
|
|
|
17489
18985
|
readValidationCache: () => readValidationCache,
|
|
17490
18986
|
writeValidationCache: () => writeValidationCache
|
|
17491
18987
|
});
|
|
17492
|
-
import { createHash as
|
|
18988
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
17493
18989
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "node:fs";
|
|
17494
18990
|
import { join as join4 } from "node:path";
|
|
17495
18991
|
import { homedir as homedir4 } from "node:os";
|
|
17496
18992
|
function keyFingerprint(apiKey) {
|
|
17497
|
-
return `sha256:${
|
|
18993
|
+
return `sha256:${createHash4("sha256").update(apiKey, "utf8").digest("hex")}`;
|
|
17498
18994
|
}
|
|
17499
18995
|
function readValidationCache(apiKey) {
|
|
17500
18996
|
try {
|
|
@@ -17635,8 +19131,7 @@ async function runLoginDevice() {
|
|
|
17635
19131
|
body: JSON.stringify({})
|
|
17636
19132
|
});
|
|
17637
19133
|
if (!response.ok) {
|
|
17638
|
-
|
|
17639
|
-
console.error(` Error: Failed to create device code. ${text}`);
|
|
19134
|
+
console.error(` Error: Failed to create device code (HTTP ${response.status}).`);
|
|
17640
19135
|
process.exit(1);
|
|
17641
19136
|
}
|
|
17642
19137
|
const data = await response.json();
|
|
@@ -17886,7 +19381,7 @@ async function runHealthCheck(options) {
|
|
|
17886
19381
|
checks.push({
|
|
17887
19382
|
name: "Connectivity",
|
|
17888
19383
|
ok: false,
|
|
17889
|
-
detail:
|
|
19384
|
+
detail: "Failed to reach the authentication service."
|
|
17890
19385
|
});
|
|
17891
19386
|
}
|
|
17892
19387
|
}
|
|
@@ -17910,7 +19405,7 @@ async function runHealthCheck(options) {
|
|
|
17910
19405
|
checks.push({
|
|
17911
19406
|
name: "Connectivity",
|
|
17912
19407
|
ok: false,
|
|
17913
|
-
detail:
|
|
19408
|
+
detail: "Network request failed."
|
|
17914
19409
|
});
|
|
17915
19410
|
}
|
|
17916
19411
|
if (apiKey) {
|